Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,18 @@ deactivate
docker stop optimapDB
```

#### Debug Mode Configuration

By default, `OPTIMAP_DEBUG` is now set to `False` to ensure a secure and stable production environment. If you need to enable debug mode for development purposes, explicitly set the environment variable in your `.env` file or pass it as an argument when running the server.

#### Enable Debug Mode for Development

To enable debug mode, add the following to your `.env` file:

```env
OPTIMAP_DEBUG=True
```

### Debug with VS Code

Select the Python interpreter created above (`optimap` environment), see instructions at <https://code.visualstudio.com/docs/python/tutorial-django> and <https://code.visualstudio.com/docs/python/environments>.
Expand Down
3 changes: 2 additions & 1 deletion optimap/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
SECRET_KEY = env('SECRET_KEY', default='django-insecure')

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = env('OPTIMAP_DEBUG', default=True)
DEBUG = env('OPTIMAP_DEBUG', default=False)

ALLOWED_HOSTS = [i.strip('[]') for i in env('OPTIMAP_ALLOWED_HOST', default='*').split(',')]

Expand Down Expand Up @@ -183,6 +183,7 @@
EMAIL_USE_TLS = env('OPTIMAP_EMAIL_USE_TLS', default=False)
EMAIL_USE_SSL = env('OPTIMAP_EMAIL_USE_SSL', default=False)
EMAIL_IMAP_SENT_FOLDER = env('OPTIMAP_EMAIL_IMAP_SENT_FOLDER', default='')
EMAIL_SEND_DELAY = 2

MIDDLEWARE = [
'django.middleware.cache.UpdateCacheMiddleware',
Expand Down
54 changes: 53 additions & 1 deletion publications/admin.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
from django.contrib import admin
from django.contrib import admin, messages
from leaflet.admin import LeafletGeoAdmin
from django.contrib.auth.models import User
from publications.models import Publication, BlockedEmail, BlockedDomain
from import_export.admin import ImportExportModelAdmin
from django_q.tasks import schedule
from django.utils.timezone import now
from publications.models import EmailLog, UserProfile
from publications.tasks import send_monthly_email, schedule_monthly_email_task
from django_q.models import Schedule
from datetime import datetime, timedelta


# Unregister the default User admin
admin.site.unregister(User)
Expand All @@ -15,6 +22,28 @@ def make_public(modeladmin, request, queryset):
def make_draft(modeladmin, request, queryset):
queryset.update(status="d")

@admin.action(description="Send Monthly Manuscript Email")
def trigger_monthly_email(modeladmin, request, queryset):
"""
Admin action to trigger the email task manually.
"""
try:
send_monthly_email(trigger_source='admin', sent_by=request.user)
messages.success(request, "Monthly manuscript email has been sent successfully.")
except Exception as e:
messages.error(request, f"Failed to send email: {e}")

@admin.action(description="Schedule Monthly Email Task")
def trigger_monthly_email_task(modeladmin, request, queryset):
"""
Admin action to manually schedule the email task.
"""
try:
schedule_monthly_email_task(sent_by=request.user)
messages.success(request, "Monthly email task has been scheduled successfully.")
except Exception as e:
messages.error(request, f"Failed to schedule task: {e}")

@admin.action(description="Delete user and block email")
def block_email(modeladmin, request, queryset):
for user in queryset:
Expand All @@ -39,6 +68,29 @@ class PublicationAdmin(LeafletGeoAdmin, ImportExportModelAdmin):

actions = [make_public,make_draft]

class EmailLogAdmin(admin.ModelAdmin):
list_display = (
"recipient_email",
"subject",
"sent_at",
"sent_by",
"trigger_source",
"status",
"error_message",
)
list_filter = ("status", "trigger_source", "sent_at")
search_fields = ("recipient_email", "subject", "sent_by__username")
actions = [trigger_monthly_email, trigger_monthly_email_task]


class UserProfileAdmin(admin.ModelAdmin):
list_display = ("user", "notify_new_manuscripts")
search_fields = ("user__email",)


admin.site.register(EmailLog, EmailLogAdmin)
admin.site.register(UserProfile, UserProfileAdmin)

@admin.register(BlockedEmail)
class BlockedEmailAdmin(admin.ModelAdmin):
list_display = ('email', 'created_at', 'blocked_by')
Expand Down
22 changes: 22 additions & 0 deletions publications/migrations/0004_sentemaillog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Generated by Django 4.0.5 on 2025-02-10 19:44

from django.db import migrations, models

class Migration(migrations.Migration):

dependencies = [
('publications', '0003_alter_publication_timeperiod_enddate_and_more'),
]

operations = [
migrations.CreateModel(
name='SentEmailLog',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('recipient_email', models.EmailField(max_length=254)),
('subject', models.CharField(max_length=255)),
('sent_at', models.DateTimeField(auto_now_add=True)),
('email_content', models.TextField(blank=True, null=True)),
],
),
]
21 changes: 21 additions & 0 deletions publications/migrations/0005_sentemaillog_sent_by.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Generated by Django 4.0.5 on 2025-02-11 20:56

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('publications', '0004_sentemaillog'),
]

operations = [
migrations.AddField(
model_name='sentemaillog',
name='sent_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
),
]
45 changes: 45 additions & 0 deletions publications/models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
from django.contrib.gis.db import models
from django.contrib.postgres.fields import ArrayField
from django_currentuser.db.models import CurrentUserField
from django.utils.timezone import now
from django.contrib.auth import get_user_model

User = get_user_model()

STATUS_CHOICES = (
("d", "Draft"),
Expand Down Expand Up @@ -84,6 +88,40 @@ class Meta:
ordering = ['user_name']
verbose_name = "subscription"

class EmailLog(models.Model):
TRIGGER_CHOICES = [
("admin", "Admin Panel"),
("scheduled", "Scheduled Task"),
("manual", "Manually Triggered"),
]
recipient_email = models.EmailField()
subject = models.CharField(max_length=255)
sent_at = models.DateTimeField(auto_now_add=True)
email_content = models.TextField(blank=True, null=True)
sent_by = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL)
trigger_source = models.CharField(max_length=50, choices=TRIGGER_CHOICES, default="manual")
status = models.CharField(max_length=10, choices=STATUS_CHOICES, default="success")
error_message = models.TextField(null=True, blank=True)

def __str__(self):
sender = self.sent_by.email if self.sent_by else "System"
return f"Email to {self.recipient_email} by {sender} ({self.get_trigger_source_display()})"

@classmethod
def log_email(cls, recipient, subject, content, sent_by=None, trigger_source="manual", status="success", error_message=None):
"""Logs the sent email, storing who triggered it and how it was sent."""
cls.objects.create(
recipient_email=recipient,
subject=subject,
sent_at=now(),
email_content=content,
sent_by=sent_by,
trigger_source=trigger_source,
status=status,
error_message=error_message,

)

# handle import/export relations, see https://django-import-export.readthedocs.io/en/stable/advanced_usage.html#creating-non-existent-relations
from import_export import fields, resources
from import_export.widgets import ForeignKeyWidget
Expand All @@ -105,6 +143,13 @@ class Meta:
model = Publication
fields = ('created_by','updated_by',)

class UserProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
notify_new_manuscripts = models.BooleanField(default=False)

def __str__(self):
return f"{self.user.username} - Notifications: {self.notify_new_manuscripts}"

class BlockedEmail(models.Model):
email = models.EmailField(unique=True)
created_at = models.DateTimeField(auto_now_add=True)
Expand Down
12 changes: 11 additions & 1 deletion publications/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@

from django.db.models.signals import pre_save
from django.dispatch import receiver
from django.contrib.auth.models import User
from django.contrib.auth import get_user_model
from django.conf import settings
from django.db.models.signals import post_save
User = get_user_model()
from publications.models import UserProfile

@receiver(pre_save, sender=User)
def update_user_callback(sender, instance, **kwargs):
Expand All @@ -14,3 +17,10 @@ def update_user_callback(sender, instance, **kwargs):
logging.warning('Registering user %s as admin', instance.email)
instance.is_staff = True
instance.is_superuser = True

@receiver(post_save, sender=User)
def create_or_update_user_profile(sender, instance, created, **kwargs):
if created:
UserProfile.objects.create(user=instance)
else:
instance.userprofile.save()
57 changes: 57 additions & 0 deletions publications/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@
import xml.dom.minidom
from django.contrib.gis.geos import GEOSGeometry
import requests
from django.core.mail import send_mail
from django.conf import settings
from django.utils.timezone import now
from django.contrib.auth.models import User
from .models import EmailLog
from datetime import datetime, timedelta
from django_q.tasks import schedule
from django_q.models import Schedule
import time
import calendar


def extract_geometry_from_html(content):
Expand Down Expand Up @@ -104,3 +114,50 @@ def harvest_oai_endpoint(url):
parse_oai_xml_and_save_publications(response.content)
except requests.exceptions.RequestException as e:
print ("The requested URL is invalid or has bad connection.Please change the URL")

def send_monthly_email(trigger_source='manual', sent_by=None):
recipients = User.objects.filter(userprofile__notify_new_manuscripts=True).values_list('email', flat=True)
last_month = now().replace(day=1) - timedelta(days=1)
new_manuscripts = Publication.objects.filter(creationDate__month=last_month.month)

if not recipients.exists() or not new_manuscripts.exists():
return

subject = "New Manuscripts This Month"
content = "Here are the new manuscripts:\n" + "\n".join([pub.title for pub in new_manuscripts])

for recipient in recipients:
try:
send_mail(
subject,
content,
settings.EMAIL_HOST_USER,
[recipient],
fail_silently=False,
)

EmailLog.log_email(
recipient, subject, content, sent_by=sent_by, trigger_source=trigger_source, status="success"
)
time.sleep(getattr(settings, "EMAIL_SEND_DELAY", 2))

except Exception as e:
error_message = str(e)
EmailLog.log_email(
recipient, subject, content, sent_by=sent_by, trigger_source=trigger_source, status="failed", error_message=error_message
)


def schedule_monthly_email_task(sent_by=None):
if not Schedule.objects.filter(func='publications.tasks.send_monthly_email').exists():
now = datetime.now()
last_day_of_month = calendar.monthrange(now.year, now.month)[1] # Get last day of the month

next_run_date = now.replace(day=last_day_of_month, hour=23, minute=59) # Run at the end of the last day
schedule(
'publications.tasks.send_monthly_email',
schedule_type='M',
repeats=-1,
next_run=next_run_date,
kwargs={'trigger_source': 'scheduled', 'sent_by': sent_by.id if sent_by else None}
)
Loading
Loading