Skip to content

Commit 186f501

Browse files
authored
Merge pull request #90 from GeoinformationSystems/feature/send_email_subscriptions_71
Send emails for new manuscripts for each subscription
2 parents cbf4788 + cdfd362 commit 186f501

File tree

12 files changed

+254
-117
lines changed

12 files changed

+254
-117
lines changed

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
[![OPTIMETA Logo](https://projects.tib.eu/fileadmin/_processed_/e/8/csm_Optimeta_Logo_web_98c26141b1.png)](https://projects.tib.eu/optimeta/en/) [![KOMET Logo](https://projects.tib.eu/fileadmin/templates/komet/tib_projects_komet_1150.png)](https://projects.tib.eu/komet/en/)
2-
31
# OPTIMAP
42

53
[![Project Status: WIP – Initial development is in progress, but there has not yet been a stable, usable release suitable for the public.](https://www.repostatus.org/badges/latest/wip.svg)](https://www.repostatus.org/#wip) [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.8198944.svg)](https://doi.org/10.5281/zenodo.8198944)
@@ -17,6 +15,10 @@ The OPTIMAP has the following features:
1715

1816
OPTIMAP is based on [Django](https://www.djangoproject.com/) (with [GeoDjango](https://docs.djangoproject.com/en/4.1/ref/contrib/gis/) and [Django REST framework](https://www.django-rest-framework.org/)) with a [PostgreSQL](https://www.postgresql.org/)/[PostGIS](https://postgis.net/) database backend.
1917

18+
The development of OPTIMAP was and is supported by the projects [OPTIMETA](https://projects.tib.eu/optimeta/en/) and [KOMET](https://projects.tib.eu/komet/en/).
19+
20+
[![OPTIMETA Logo](https://projects.tib.eu/fileadmin/_processed_/e/8/csm_Optimeta_Logo_web_98c26141b1.png)](https://projects.tib.eu/optimeta/en/) [![KOMET Logo](https://projects.tib.eu/fileadmin/templates/komet/tib_projects_komet_1150.png)](https://projects.tib.eu/komet/en/)
21+
2022
## Configuration
2123

2224
All configuration is done via the file `optimap/settings.py`.
@@ -222,7 +224,7 @@ python manage.py createsuperuser --username=optimap [email protected]
222224

223225
You will be prompted for a password. After entering one, the superuser will be created immediately. If you omit the --username or --email options, the command will prompt you for those values interactively.
224226

225-
Access the admin interface at http://127.0.0.1:8000/admin/.
227+
Access the admin interface at <http://127.0.0.1:8000/admin/>.
226228

227229
#### Running in a Dockerized App
228230

optimap/settings.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,6 @@
5050
"sesame.backends.ModelBackend",
5151
]
5252

53-
AUTH_USER_MODEL = "publications.CustomUser"
54-
5553
INSTALLED_APPS = [
5654
'django.contrib.admin',
5755
'django.contrib.auth',
@@ -61,16 +59,18 @@
6159
'django.contrib.staticfiles',
6260
'django.contrib.gis',
6361
'django.contrib.sitemaps',
62+
'publications',
6463
'rest_framework',
6564
'rest_framework_gis',
66-
'publications',
6765
'django_q',
6866
'drf_spectacular',
6967
'drf_spectacular_sidecar',
7068
'leaflet',
7169
'import_export',
7270
]
7371

72+
AUTH_USER_MODEL = "publications.CustomUser"
73+
7474
REST_FRAMEWORK = {
7575
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
7676
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
@@ -186,6 +186,8 @@
186186
EMAIL_USE_SSL = env('OPTIMAP_EMAIL_USE_SSL', default=False)
187187
BASE_URL = env("OPTIMAP_BASE_URL", default="http://localhost:8000")
188188
EMAIL_IMAP_SENT_FOLDER = env('OPTIMAP_EMAIL_IMAP_SENT_FOLDER', default='')
189+
OPTIMAP_EMAIL_SEND_DELAY = env("OPTIMAP_EMAIL_SEND_DELAY", default=2)
190+
BASE_URL = env("BASE_URL", default="http://127.0.0.1:8000")
189191
OAI_USERNAME = env("OPTIMAP_OAI_USERNAME", default="")
190192
OAI_PASSWORD = env("OPTIMAP_OAI_PASSWORD", default="")
191193
EMAIL_SEND_DELAY = 2

publications/admin.py

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,11 @@
22
from leaflet.admin import LeafletGeoAdmin
33
from publications.models import Publication, Source, HarvestingEvent, BlockedEmail, BlockedDomain
44
from import_export.admin import ImportExportModelAdmin
5-
from publications.tasks import harvest_oai_endpoint
5+
from publications.models import EmailLog, Subscription, UserProfile
6+
from publications.tasks import harvest_oai_endpoint, schedule_subscription_email_task, send_monthly_email, schedule_monthly_email_task
67
from django_q.models import Schedule
78
from django.utils.timezone import now
8-
from django_q.tasks import schedule
9-
from publications.models import EmailLog, UserProfile
10-
from publications.tasks import send_monthly_email, schedule_monthly_email_task
11-
from django.contrib.auth import get_user_model
12-
User = get_user_model()
9+
from publications.models import CustomUser
1310

1411
@admin.action(description="Mark selected publications as published")
1512
def make_public(modeladmin, request, queryset):
@@ -70,6 +67,33 @@ def trigger_monthly_email_task(modeladmin, request, queryset):
7067
except Exception as e:
7168
messages.error(request, f"Failed to schedule task: {e}")
7269

70+
@admin.action(description="Send subscription-based emails")
71+
def send_subscription_emails(modeladmin, request, queryset):
72+
"""
73+
Admin action to manually send subscription-based emails to selected users.
74+
"""
75+
from publications.tasks import send_subscription_based_email
76+
77+
selected_users = queryset.filter(subscribed=True).values_list('user', flat=True)
78+
if not selected_users:
79+
messages.warning(request, "No active subscribers selected.")
80+
return
81+
82+
send_subscription_based_email(sent_by=request.user, user_ids=list(selected_users))
83+
messages.success(request, "Subscription-based emails have been sent.")
84+
85+
@admin.action(description="Schedule subscription-based Email Task")
86+
def send_subscription_emails_scheduler(modeladmin, request, queryset):
87+
"""
88+
Admin action to manually schedule the email task.
89+
"""
90+
try:
91+
schedule_subscription_email_task(sent_by=request.user)
92+
messages.success(request, "Monthly email task has been scheduled successfully.")
93+
except Exception as e:
94+
messages.error(request, f"Failed to schedule task: {e}")
95+
96+
7397
@admin.action(description="Delete user and block email")
7498
def block_email(modeladmin, request, queryset):
7599
for user in queryset:
@@ -122,15 +146,14 @@ class EmailLogAdmin(admin.ModelAdmin):
122146
search_fields = ("recipient_email", "subject", "sent_by__username")
123147
actions = [trigger_monthly_email, trigger_monthly_email_task]
124148

149+
class SubscriptionAdmin(admin.ModelAdmin):
150+
list_display = ("user", "region", "subscribed")
151+
actions = [send_subscription_emails, send_subscription_emails_scheduler]
125152

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

130-
131-
admin.site.register(EmailLog, EmailLogAdmin)
132-
admin.site.register(UserProfile, UserProfileAdmin)
133-
134157
@admin.register(BlockedEmail)
135158
class BlockedEmailAdmin(admin.ModelAdmin):
136159
list_display = ('email', 'created_at', 'blocked_by')
@@ -141,8 +164,12 @@ class BlockedDomainAdmin(admin.ModelAdmin):
141164
list_display = ('domain', 'created_at', 'blocked_by')
142165
search_fields = ('domain',)
143166

144-
@admin.register(User)
167+
@admin.register(CustomUser)
145168
class UserAdmin(admin.ModelAdmin):
146169
"""User Admin."""
147170
list_display = ("username", "email", "is_active")
148171
actions = [block_email, block_email_and_domain]
172+
173+
admin.site.register(EmailLog, EmailLogAdmin)
174+
admin.site.register(UserProfile, UserProfileAdmin)
175+
admin.site.register(Subscription, SubscriptionAdmin)

publications/migrations/0001_initial.py

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Generated by Django 5.1.7 on 2025-03-19 19:34
1+
# Generated by Django 5.1.7 on 2025-04-08 10:02
22

33
import django.contrib.auth.models
44
import django.contrib.auth.validators
@@ -21,22 +21,6 @@ class Migration(migrations.Migration):
2121
]
2222

2323
operations = [
24-
migrations.CreateModel(
25-
name='Subscription',
26-
fields=[
27-
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
28-
('name', models.CharField(max_length=4096)),
29-
('search_term', models.CharField(max_length=4096, null=True)),
30-
('timeperiod_startdate', models.DateField(null=True)),
31-
('timeperiod_enddate', models.DateField(null=True)),
32-
('search_area', django.contrib.gis.db.models.fields.GeometryCollectionField(blank=True, null=True, srid=4326)),
33-
('user_name', models.CharField(max_length=4096)),
34-
],
35-
options={
36-
'verbose_name': 'subscription',
37-
'ordering': ['user_name'],
38-
},
39-
),
4024
migrations.CreateModel(
4125
name='CustomUser',
4226
fields=[
@@ -121,6 +105,23 @@ class Migration(migrations.Migration):
121105
('source', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='harvesting_events', to='publications.source')),
122106
],
123107
),
108+
migrations.CreateModel(
109+
name='Subscription',
110+
fields=[
111+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
112+
('name', models.CharField(default='default_subscription', max_length=4096)),
113+
('search_term', models.CharField(max_length=4096, null=True)),
114+
('timeperiod_startdate', models.DateField(null=True)),
115+
('timeperiod_enddate', models.DateField(null=True)),
116+
('region', django.contrib.gis.db.models.fields.GeometryCollectionField(blank=True, null=True, srid=4326)),
117+
('subscribed', models.BooleanField(default=True)),
118+
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='subscriptions', to=settings.AUTH_USER_MODEL)),
119+
],
120+
options={
121+
'verbose_name': 'subscription',
122+
'ordering': ['name'],
123+
},
124+
),
124125
migrations.CreateModel(
125126
name='UserProfile',
126127
fields=[

publications/migrations/0002_alter_subscription_options_and_more.py

Lines changed: 0 additions & 56 deletions
This file was deleted.

publications/models.py

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,12 @@
44
from django_q.models import Schedule
55
from django.utils.timezone import now
66
from django.contrib.auth.models import AbstractUser, Group, Permission
7-
import uuid
87
from django.utils.timezone import now
8+
# handle import/export relations, see https://django-import-export.readthedocs.io/en/stable/advanced_usage.html#creating-non-existent-relations
9+
from import_export import fields, resources
10+
from import_export.widgets import ForeignKeyWidget
11+
from django.conf import settings
12+
913
import logging
1014
logger = logging.getLogger(__name__)
1115

@@ -122,26 +126,23 @@ def save(self, *args, **kwargs):
122126
)
123127

124128

125-
126129
class Subscription(models.Model):
127-
name = models.CharField(max_length=4096)
130+
user = models.ForeignKey(CustomUser, on_delete=models.CASCADE, related_name="subscriptions", null=True, blank=True)
131+
name = models.CharField(max_length=4096, default="default_subscription")
128132
search_term = models.CharField(max_length=4096,null=True)
129133
timeperiod_startdate = models.DateField(null=True)
130134
timeperiod_enddate = models.DateField(null=True)
131-
search_area = models.GeometryCollectionField(null=True, blank=True)
132-
user_name = models.CharField(max_length=4096)
135+
region = models.GeometryCollectionField(null=True, blank=True)
136+
subscribed = models.BooleanField(default=True)
133137

134138
def __str__(self):
135139
"""Return string representation."""
136140
return self.name
137141

138142
class Meta:
139-
ordering = ['user_name']
143+
ordering = ['name']
140144
verbose_name = "subscription"
141145

142-
from django.contrib.auth import get_user_model
143-
User = get_user_model()
144-
145146
class EmailLog(models.Model):
146147
TRIGGER_CHOICES = [
147148
("admin", "Admin Panel"),
@@ -152,7 +153,7 @@ class EmailLog(models.Model):
152153
subject = models.CharField(max_length=255)
153154
sent_at = models.DateTimeField(auto_now_add=True)
154155
email_content = models.TextField(blank=True, null=True)
155-
sent_by = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL)
156+
sent_by = models.ForeignKey(CustomUser, null=True, blank=True, on_delete=models.SET_NULL)
156157
trigger_source = models.CharField(max_length=50, choices=TRIGGER_CHOICES, default="manual")
157158
status = models.CharField(max_length=10, choices=STATUS_CHOICES, default="success")
158159
error_message = models.TextField(null=True, blank=True)
@@ -176,18 +177,13 @@ def log_email(cls, recipient, subject, content, sent_by=None, trigger_source="ma
176177

177178
)
178179

179-
# handle import/export relations, see https://django-import-export.readthedocs.io/en/stable/advanced_usage.html#creating-non-existent-relations
180-
from import_export import fields, resources
181-
from import_export.widgets import ForeignKeyWidget
182-
from django.conf import settings
183-
184180
class PublicationResource(resources.ModelResource):
185181
#created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='username')
186182
#updated_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='username')
187183
created_by = fields.Field(
188184
column_name='created_by',
189185
attribute='created_by',
190-
widget=ForeignKeyWidget(User, field='username'))
186+
widget=ForeignKeyWidget(CustomUser, field='username'))
191187
updated_by = fields.Field(
192188
column_name='updated_by',
193189
attribute='updated_by',
@@ -199,7 +195,7 @@ class Meta:
199195

200196
class HarvestingEvent(models.Model):
201197
source = models.ForeignKey('Source', on_delete=models.CASCADE, related_name='harvesting_events')
202-
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
198+
user = models.ForeignKey(CustomUser, on_delete=models.SET_NULL, null=True, blank=True)
203199
started_at = models.DateTimeField(auto_now_add=True)
204200
completed_at = models.DateTimeField(null=True, blank=True)
205201
status = models.CharField(
@@ -218,7 +214,7 @@ def __str__(self):
218214

219215

220216
class UserProfile(models.Model):
221-
user = models.OneToOneField(User, on_delete=models.CASCADE)
217+
user = models.OneToOneField(CustomUser, on_delete=models.CASCADE)
222218
notify_new_manuscripts = models.BooleanField(default=False)
223219

224220
def __str__(self):
@@ -227,15 +223,15 @@ def __str__(self):
227223
class BlockedEmail(models.Model):
228224
email = models.EmailField(unique=True)
229225
created_at = models.DateTimeField(auto_now_add=True)
230-
blocked_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name="blocked_emails")
226+
blocked_by = models.ForeignKey(CustomUser, on_delete=models.SET_NULL, null=True, blank=True, related_name="blocked_emails")
231227

232228
def __str__(self):
233229
return self.email
234230

235231
class BlockedDomain(models.Model):
236232
domain = models.CharField(max_length=255, unique=True)
237233
created_at = models.DateTimeField(auto_now_add=True)
238-
blocked_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name="blocked_domains")
234+
blocked_by = models.ForeignKey(CustomUser, on_delete=models.SET_NULL, null=True, blank=True, related_name="blocked_domains")
239235

240236
def __str__(self):
241237
return self.domain

publications/serializers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ class SubscriptionSerializer(serializers.GeoFeatureModelSerializer):
2525
class Meta:
2626
model = Subscription
2727
fields = ("search_term","timeperiod_startdate","timeperiod_enddate","user_name")
28-
geo_field = "search_area"
28+
geo_field = "region"
2929
auto_bbox = True
3030

3131
class EmailChangeSerializer(serializers.ModelSerializer):

0 commit comments

Comments
 (0)