Skip to content

Commit 69dbaa5

Browse files
authored
Merge pull request #89 from GeoinformationSystems/feature/send_email_new_manuscripts
Send emails for new manuscripts
2 parents 4aff22a + 886b322 commit 69dbaa5

File tree

11 files changed

+522
-81
lines changed

11 files changed

+522
-81
lines changed

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,18 @@ deactivate
103103
docker stop optimapDB
104104
```
105105

106+
#### Debug Mode Configuration
107+
108+
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.
109+
110+
#### Enable Debug Mode for Development
111+
112+
To enable debug mode, add the following to your `.env` file:
113+
114+
```env
115+
OPTIMAP_DEBUG=True
116+
```
117+
106118
### Debug with VS Code
107119

108120
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>.

optimap/settings.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
SECRET_KEY = env('SECRET_KEY', default='django-insecure')
3636

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

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

@@ -183,6 +183,7 @@
183183
EMAIL_USE_TLS = env('OPTIMAP_EMAIL_USE_TLS', default=False)
184184
EMAIL_USE_SSL = env('OPTIMAP_EMAIL_USE_SSL', default=False)
185185
EMAIL_IMAP_SENT_FOLDER = env('OPTIMAP_EMAIL_IMAP_SENT_FOLDER', default='')
186+
EMAIL_SEND_DELAY = 2
186187

187188
MIDDLEWARE = [
188189
'django.middleware.cache.UpdateCacheMiddleware',

publications/admin.py

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
1-
from django.contrib import admin
1+
from django.contrib import admin, messages
22
from leaflet.admin import LeafletGeoAdmin
33
from django.contrib.auth.models import User
44
from publications.models import Publication, BlockedEmail, BlockedDomain
55
from import_export.admin import ImportExportModelAdmin
6+
from django_q.tasks import schedule
7+
from django.utils.timezone import now
8+
from publications.models import EmailLog, UserProfile
9+
from publications.tasks import send_monthly_email, schedule_monthly_email_task
10+
from django_q.models import Schedule
11+
from datetime import datetime, timedelta
12+
613

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

25+
@admin.action(description="Send Monthly Manuscript Email")
26+
def trigger_monthly_email(modeladmin, request, queryset):
27+
"""
28+
Admin action to trigger the email task manually.
29+
"""
30+
try:
31+
send_monthly_email(trigger_source='admin', sent_by=request.user)
32+
messages.success(request, "Monthly manuscript email has been sent successfully.")
33+
except Exception as e:
34+
messages.error(request, f"Failed to send email: {e}")
35+
36+
@admin.action(description="Schedule Monthly Email Task")
37+
def trigger_monthly_email_task(modeladmin, request, queryset):
38+
"""
39+
Admin action to manually schedule the email task.
40+
"""
41+
try:
42+
schedule_monthly_email_task(sent_by=request.user)
43+
messages.success(request, "Monthly email task has been scheduled successfully.")
44+
except Exception as e:
45+
messages.error(request, f"Failed to schedule task: {e}")
46+
1847
@admin.action(description="Delete user and block email")
1948
def block_email(modeladmin, request, queryset):
2049
for user in queryset:
@@ -39,6 +68,29 @@ class PublicationAdmin(LeafletGeoAdmin, ImportExportModelAdmin):
3968

4069
actions = [make_public,make_draft]
4170

71+
class EmailLogAdmin(admin.ModelAdmin):
72+
list_display = (
73+
"recipient_email",
74+
"subject",
75+
"sent_at",
76+
"sent_by",
77+
"trigger_source",
78+
"status",
79+
"error_message",
80+
)
81+
list_filter = ("status", "trigger_source", "sent_at")
82+
search_fields = ("recipient_email", "subject", "sent_by__username")
83+
actions = [trigger_monthly_email, trigger_monthly_email_task]
84+
85+
86+
class UserProfileAdmin(admin.ModelAdmin):
87+
list_display = ("user", "notify_new_manuscripts")
88+
search_fields = ("user__email",)
89+
90+
91+
admin.site.register(EmailLog, EmailLogAdmin)
92+
admin.site.register(UserProfile, UserProfileAdmin)
93+
4294
@admin.register(BlockedEmail)
4395
class BlockedEmailAdmin(admin.ModelAdmin):
4496
list_display = ('email', 'created_at', 'blocked_by')
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Generated by Django 4.0.5 on 2025-02-10 19:44
2+
3+
from django.db import migrations, models
4+
5+
class Migration(migrations.Migration):
6+
7+
dependencies = [
8+
('publications', '0003_alter_publication_timeperiod_enddate_and_more'),
9+
]
10+
11+
operations = [
12+
migrations.CreateModel(
13+
name='SentEmailLog',
14+
fields=[
15+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
16+
('recipient_email', models.EmailField(max_length=254)),
17+
('subject', models.CharField(max_length=255)),
18+
('sent_at', models.DateTimeField(auto_now_add=True)),
19+
('email_content', models.TextField(blank=True, null=True)),
20+
],
21+
),
22+
]
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Generated by Django 4.0.5 on 2025-02-11 20:56
2+
3+
from django.conf import settings
4+
from django.db import migrations, models
5+
import django.db.models.deletion
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
12+
('publications', '0004_sentemaillog'),
13+
]
14+
15+
operations = [
16+
migrations.AddField(
17+
model_name='sentemaillog',
18+
name='sent_by',
19+
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
20+
),
21+
]

publications/models.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
from django.contrib.gis.db import models
22
from django.contrib.postgres.fields import ArrayField
33
from django_currentuser.db.models import CurrentUserField
4+
from django.utils.timezone import now
5+
from django.contrib.auth import get_user_model
6+
7+
User = get_user_model()
48

59
STATUS_CHOICES = (
610
("d", "Draft"),
@@ -84,6 +88,40 @@ class Meta:
8488
ordering = ['user_name']
8589
verbose_name = "subscription"
8690

91+
class EmailLog(models.Model):
92+
TRIGGER_CHOICES = [
93+
("admin", "Admin Panel"),
94+
("scheduled", "Scheduled Task"),
95+
("manual", "Manually Triggered"),
96+
]
97+
recipient_email = models.EmailField()
98+
subject = models.CharField(max_length=255)
99+
sent_at = models.DateTimeField(auto_now_add=True)
100+
email_content = models.TextField(blank=True, null=True)
101+
sent_by = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL)
102+
trigger_source = models.CharField(max_length=50, choices=TRIGGER_CHOICES, default="manual")
103+
status = models.CharField(max_length=10, choices=STATUS_CHOICES, default="success")
104+
error_message = models.TextField(null=True, blank=True)
105+
106+
def __str__(self):
107+
sender = self.sent_by.email if self.sent_by else "System"
108+
return f"Email to {self.recipient_email} by {sender} ({self.get_trigger_source_display()})"
109+
110+
@classmethod
111+
def log_email(cls, recipient, subject, content, sent_by=None, trigger_source="manual", status="success", error_message=None):
112+
"""Logs the sent email, storing who triggered it and how it was sent."""
113+
cls.objects.create(
114+
recipient_email=recipient,
115+
subject=subject,
116+
sent_at=now(),
117+
email_content=content,
118+
sent_by=sent_by,
119+
trigger_source=trigger_source,
120+
status=status,
121+
error_message=error_message,
122+
123+
)
124+
87125
# handle import/export relations, see https://django-import-export.readthedocs.io/en/stable/advanced_usage.html#creating-non-existent-relations
88126
from import_export import fields, resources
89127
from import_export.widgets import ForeignKeyWidget
@@ -105,6 +143,13 @@ class Meta:
105143
model = Publication
106144
fields = ('created_by','updated_by',)
107145

146+
class UserProfile(models.Model):
147+
user = models.OneToOneField(User, on_delete=models.CASCADE)
148+
notify_new_manuscripts = models.BooleanField(default=False)
149+
150+
def __str__(self):
151+
return f"{self.user.username} - Notifications: {self.notify_new_manuscripts}"
152+
108153
class BlockedEmail(models.Model):
109154
email = models.EmailField(unique=True)
110155
created_at = models.DateTimeField(auto_now_add=True)

publications/signals.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@
33

44
from django.db.models.signals import pre_save
55
from django.dispatch import receiver
6-
from django.contrib.auth.models import User
6+
from django.contrib.auth import get_user_model
77
from django.conf import settings
8+
from django.db.models.signals import post_save
9+
User = get_user_model()
10+
from publications.models import UserProfile
811

912
@receiver(pre_save, sender=User)
1013
def update_user_callback(sender, instance, **kwargs):
@@ -14,3 +17,10 @@ def update_user_callback(sender, instance, **kwargs):
1417
logging.warning('Registering user %s as admin', instance.email)
1518
instance.is_staff = True
1619
instance.is_superuser = True
20+
21+
@receiver(post_save, sender=User)
22+
def create_or_update_user_profile(sender, instance, created, **kwargs):
23+
if created:
24+
UserProfile.objects.create(user=instance)
25+
else:
26+
instance.userprofile.save()

publications/tasks.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,16 @@
88
import xml.dom.minidom
99
from django.contrib.gis.geos import GEOSGeometry
1010
import requests
11+
from django.core.mail import send_mail
12+
from django.conf import settings
13+
from django.utils.timezone import now
14+
from django.contrib.auth.models import User
15+
from .models import EmailLog
16+
from datetime import datetime, timedelta
17+
from django_q.tasks import schedule
18+
from django_q.models import Schedule
19+
import time
20+
import calendar
1121

1222

1323
def extract_geometry_from_html(content):
@@ -104,3 +114,50 @@ def harvest_oai_endpoint(url):
104114
parse_oai_xml_and_save_publications(response.content)
105115
except requests.exceptions.RequestException as e:
106116
print ("The requested URL is invalid or has bad connection.Please change the URL")
117+
118+
def send_monthly_email(trigger_source='manual', sent_by=None):
119+
recipients = User.objects.filter(userprofile__notify_new_manuscripts=True).values_list('email', flat=True)
120+
last_month = now().replace(day=1) - timedelta(days=1)
121+
new_manuscripts = Publication.objects.filter(creationDate__month=last_month.month)
122+
123+
if not recipients.exists() or not new_manuscripts.exists():
124+
return
125+
126+
subject = "New Manuscripts This Month"
127+
content = "Here are the new manuscripts:\n" + "\n".join([pub.title for pub in new_manuscripts])
128+
129+
for recipient in recipients:
130+
try:
131+
send_mail(
132+
subject,
133+
content,
134+
settings.EMAIL_HOST_USER,
135+
[recipient],
136+
fail_silently=False,
137+
)
138+
139+
EmailLog.log_email(
140+
recipient, subject, content, sent_by=sent_by, trigger_source=trigger_source, status="success"
141+
)
142+
time.sleep(getattr(settings, "EMAIL_SEND_DELAY", 2))
143+
144+
except Exception as e:
145+
error_message = str(e)
146+
EmailLog.log_email(
147+
recipient, subject, content, sent_by=sent_by, trigger_source=trigger_source, status="failed", error_message=error_message
148+
)
149+
150+
151+
def schedule_monthly_email_task(sent_by=None):
152+
if not Schedule.objects.filter(func='publications.tasks.send_monthly_email').exists():
153+
now = datetime.now()
154+
last_day_of_month = calendar.monthrange(now.year, now.month)[1] # Get last day of the month
155+
156+
next_run_date = now.replace(day=last_day_of_month, hour=23, minute=59) # Run at the end of the last day
157+
schedule(
158+
'publications.tasks.send_monthly_email',
159+
schedule_type='M',
160+
repeats=-1,
161+
next_run=next_run_date,
162+
kwargs={'trigger_source': 'scheduled', 'sent_by': sent_by.id if sent_by else None}
163+
)

0 commit comments

Comments
 (0)