Skip to content

Commit 5ab82dd

Browse files
author
BharatVe
committed
Add article link to notification emails (DOI or URL fallback)
1 parent 50c58e1 commit 5ab82dd

File tree

3 files changed

+118
-45
lines changed

3 files changed

+118
-45
lines changed

publications/tasks.py

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@
3636
DOI_REGEX = re.compile(r'10\.\d{4,9}/[-._;()/:A-Z0-9]+', re.IGNORECASE)
3737
CACHE_DIR = Path(tempfile.gettempdir()) / 'optimap_cache'
3838

39+
def _get_article_link(pub):
40+
"""
41+
Return the DOI resolver URL if pub.doi is set, otherwise fall back to pub.url.
42+
"""
43+
return f"https://doi.org/{pub.doi}" if pub.doi else pub.url
3944

4045
def generate_data_dump_filename(extension: str) -> str:
4146
ts = datetime.now(dt_timezone.utc).strftime("%Y%m%dT%H%M%S")
@@ -247,15 +252,20 @@ def harvest_oai_endpoint(source_id: int, user=None) -> None:
247252

248253

249254
def send_monthly_email(trigger_source='manual', sent_by=None):
250-
recipients = User.objects.filter(userprofile__notify_new_manuscripts=True).values_list('email', flat=True)
255+
recipients = User.objects.filter(userprofile__notify_new_manuscripts=True) \
256+
.values_list('email', flat=True)
251257
last_month = timezone.now().replace(day=1) - timedelta(days=1)
252258
new_manuscripts = Publication.objects.filter(creationDate__month=last_month.month)
253259

254260
if not recipients.exists() or not new_manuscripts.exists():
255261
return
256262

257263
subject = "📚 New Manuscripts This Month"
258-
content = "Here are the new manuscripts:\n" + "\n".join([pub.title for pub in new_manuscripts])
264+
lines = []
265+
for pub in new_manuscripts:
266+
link = _get_article_link(pub)
267+
lines.append(f"- {pub.title}: {link}")
268+
content = "Here are the new manuscripts:\n" + "\n".join(lines)
259269

260270
for recipient in recipients:
261271
try:
@@ -267,14 +277,25 @@ def send_monthly_email(trigger_source='manual', sent_by=None):
267277
fail_silently=False,
268278
)
269279
EmailLog.log_email(
270-
recipient, subject, content, sent_by=sent_by, trigger_source=trigger_source, status="success"
280+
recipient,
281+
subject,
282+
content,
283+
sent_by=sent_by,
284+
trigger_source=trigger_source,
285+
status="success"
271286
)
272287
time.sleep(settings.EMAIL_SEND_DELAY)
273288
except Exception as e:
274289
error_message = str(e)
275290
logger.error(f"Failed to send monthly email to {recipient}: {error_message}")
276291
EmailLog.log_email(
277-
recipient, subject, content, sent_by=sent_by, trigger_source=trigger_source, status="failed", error_message=error_message
292+
recipient,
293+
subject,
294+
content,
295+
sent_by=sent_by,
296+
trigger_source=trigger_source,
297+
status="failed",
298+
error_message=error_message
278299
)
279300

280301
def send_subscription_based_email(trigger_source='manual', sent_by=None, user_ids=None):
@@ -284,19 +305,24 @@ def send_subscription_based_email(trigger_source='manual', sent_by=None, user_id
284305

285306
for subscription in query:
286307
user_email = subscription.user.email
287-
288308
new_publications = Publication.objects.filter(
289309
geometry__intersects=subscription.region,
290310
)
291-
292311
if not new_publications.exists():
293312
continue
294313

295-
unsubscribe_specific = f"{BASE_URL}{reverse('optimap:unsubscribe')}?search={quote(subscription.search_term)}"
314+
unsubscribe_specific = (
315+
f"{BASE_URL}{reverse('optimap:unsubscribe')}?search="
316+
f"{quote(subscription.search_term)}"
317+
)
296318
unsubscribe_all = f"{BASE_URL}{reverse('optimap:unsubscribe')}?all=true"
297319

298320
subject = f"📚 New Manuscripts Matching '{subscription.search_term}'"
299-
bullet_list = "\n".join([f"- {pub.title}" for pub in new_publications])
321+
lines = []
322+
for pub in new_publications:
323+
link = _get_article_link(pub)
324+
lines.append(f"- {pub.title}: {link}")
325+
bullet_list = "\n".join(lines)
300326
content = f"""Dear {subscription.user.username},
301327
Here are the latest manuscripts matching your subscription:
302328
@@ -311,17 +337,27 @@ def send_subscription_based_email(trigger_source='manual', sent_by=None, user_id
311337
email = EmailMessage(subject, content, settings.EMAIL_HOST_USER, [user_email])
312338
email.send()
313339
EmailLog.log_email(
314-
user_email, subject, content, sent_by=sent_by, trigger_source=trigger_source, status="success"
340+
user_email,
341+
subject,
342+
content,
343+
sent_by=sent_by,
344+
trigger_source=trigger_source,
345+
status="success"
315346
)
316347
time.sleep(settings.EMAIL_SEND_DELAY)
317348
except Exception as e:
318349
error_message = str(e)
319350
logger.error(f"Failed to send subscription email to {user_email}: {error_message}")
320351
EmailLog.log_email(
321-
user_email, subject, content, sent_by=sent_by, trigger_source=trigger_source, status="failed", error_message=error_message
352+
user_email,
353+
subject,
354+
content,
355+
sent_by=sent_by,
356+
trigger_source=trigger_source,
357+
status="failed",
358+
error_message=error_message
322359
)
323360

324-
# ... (the rest of the file remains unchanged)
325361

326362
def schedule_monthly_email_task(sent_by=None):
327363
if not Schedule.objects.filter(func='publications.tasks.send_monthly_email').exists():

tests/test_email.py

Lines changed: 35 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from django.contrib.auth import get_user_model
1212
User = get_user_model()
1313

14-
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "optimap.settings")
14+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "optimap.settings")
1515
django.setup()
1616

1717
@override_settings(EMAIL_BACKEND='django.core.mail.backends.locmem.EmailBackend')
@@ -21,17 +21,18 @@ def setUp(self):
2121
Publication.objects.all().delete()
2222
EmailLog.objects.all().delete()
2323
User.objects.all().delete()
24-
25-
self.user = User.objects.create_user(username="testuser1", email="test@example.com", password="testpass")
26-
self.user_profile = UserProfile.objects.get(user=self.user)
2724

25+
self.user = User.objects.create_user(
26+
username="testuser1", email="test@example.com", password="testpass"
27+
)
28+
self.user_profile = UserProfile.objects.get(user=self.user)
2829
self.user_profile.notify_new_manuscripts = True
2930
self.user_profile.save()
3031

3132
def test_send_monthly_email_with_publications(self):
3233
"""Test if the monthly email is sent when publications exist"""
33-
34-
last_month = now().replace(day=1) - timedelta(days=1)
34+
# create one publication with a DOI
35+
last_month = now().replace(day=1) - timedelta(days=1)
3536
publication = Publication.objects.create(
3637
title="Point Test",
3738
abstract="Publication with a single point inside a collection.",
@@ -41,38 +42,48 @@ def test_send_monthly_email_with_publications(self):
4142
doi="10.1234/test-doi-1",
4243
geometry=GeometryCollection(Point(12.4924, 41.8902)),
4344
)
44-
45+
# ensure creationDate falls in last month
4546
Publication.objects.filter(id=publication.id).update(creationDate=last_month)
46-
4747
publication.refresh_from_db()
48-
48+
49+
# no emails before sending
4950
self.assertEqual(len(mail.outbox), 0)
5051

52+
# send and assert
5153
send_monthly_email(sent_by=self.user)
52-
5354
self.assertEqual(len(mail.outbox), 1)
54-
5555
sent_email = mail.outbox[0]
5656

57+
# title and DOI-based link should both appear
5758
self.assertIn(publication.title, sent_email.body)
59+
expected_link = f"https://doi.org/{publication.doi}"
60+
self.assertIn(expected_link, sent_email.body)
5861

62+
# recipient and log correctness
5963
self.assertEqual(sent_email.to, ["test@example.com"])
60-
61-
email_log = EmailLog.objects.latest('sent_at')
64+
email_log = EmailLog.objects.latest('sent_at')
6265
self.assertEqual(email_log.recipient_email, "test@example.com")
6366
self.assertEqual(email_log.sent_by, self.user)
6467

65-
66-
def test_send_monthly_email_without_publications(self):
67-
"""Test that no email is sent when no new publications exist"""
68-
69-
self.assertEqual(len(mail.outbox), 0)
68+
def test_send_monthly_email_fallback_to_url_when_no_doi(self):
69+
"""Test monthly email falls back to publication.url when no DOI"""
70+
last_month = now().replace(day=1) - timedelta(days=1)
71+
pub = Publication.objects.create(
72+
title="No DOI Paper",
73+
abstract="No DOI here.",
74+
url="https://example.com/nodoi",
75+
status="p",
76+
publicationDate=last_month,
77+
doi=None,
78+
geometry=GeometryCollection(Point(0, 0)),
79+
)
80+
Publication.objects.filter(id=pub.id).update(creationDate=last_month)
81+
mail.outbox.clear()
7082

7183
send_monthly_email(sent_by=self.user)
84+
self.assertEqual(len(mail.outbox), 1)
85+
body = mail.outbox[0].body
7286

73-
self.assertEqual(len(mail.outbox), 0)
74-
75-
self.assertFalse(EmailLog.objects.exists())
76-
77-
78-
87+
# should include URL fallback instead of DOI
88+
self.assertIn(pub.title, body)
89+
self.assertIn(pub.url, body)

tests/test_subscription_email.py

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,21 @@
1212
@override_settings(EMAIL_BACKEND='django.core.mail.backends.locmem.EmailBackend')
1313
class SubscriptionEmailTest(TestCase):
1414
def setUp(self):
15-
self.user = User.objects.create_user(username="subuser", email="subuser@example.com", password="testpass")
15+
self.user = User.objects.create_user(
16+
username="subuser", email="subuser@example.com", password="testpass"
17+
)
1618
UserProfile.objects.get_or_create(user=self.user)
17-
1819
self.subscription = Subscription.objects.create(
1920
user=self.user,
2021
name="Test Subscription",
2122
search_term="AI",
22-
region=GeometryCollection(Point(12.4924, 41.8902)),
23+
region=GeometryCollection(Point(12.4924, 41.8902)),
2324
subscribed=True
2425
)
2526

2627
def test_subscription_email_sent_when_publication_matches(self):
27-
# Create a publication within the region
28+
"""Email is sent and includes a DOI link when a pub matches the region"""
29+
# publication with DOI inside region
2830
pub = Publication.objects.create(
2931
title="Rome AI Paper",
3032
abstract="Test abstract",
@@ -34,30 +36,54 @@ def test_subscription_email_sent_when_publication_matches(self):
3436
doi="10.1234/sub-doi",
3537
geometry=GeometryCollection(Point(12.4924, 41.8902)),
3638
)
37-
39+
# trigger
3840
send_subscription_based_email(sent_by=self.user)
3941

42+
# one email, contains title, unsubscribe, and DOI link
4043
self.assertEqual(len(mail.outbox), 1)
41-
self.assertIn(pub.title, mail.outbox[0].body)
42-
self.assertIn("unsubscribe", mail.outbox[0].body.lower())
44+
body = mail.outbox[0].body.lower()
45+
self.assertIn(pub.title.lower(), body)
46+
self.assertIn("unsubscribe", body)
47+
expected_link = f"https://doi.org/{pub.doi}"
48+
self.assertIn(expected_link, mail.outbox[0].body)
4349

50+
# log entry
4451
log = EmailLog.objects.latest("sent_at")
4552
self.assertEqual(log.recipient_email, self.user.email)
4653
self.assertEqual(log.sent_by, self.user)
4754

55+
def test_subscription_email_fallback_to_url_when_no_doi(self):
56+
"""Email falls back to pub.url when DOI is missing"""
57+
pub = Publication.objects.create(
58+
title="No DOI Sub Paper",
59+
abstract="Test abstract",
60+
url="https://example.com/no-doi-sub",
61+
status="p",
62+
publicationDate=now() - timedelta(days=2),
63+
doi=None,
64+
geometry=GeometryCollection(Point(12.4924, 41.8902)),
65+
)
66+
mail.outbox.clear()
67+
68+
send_subscription_based_email(sent_by=self.user)
69+
self.assertEqual(len(mail.outbox), 1)
70+
body = mail.outbox[0].body
71+
self.assertIn(pub.title, body)
72+
# should include URL fallback
73+
self.assertIn(pub.url, body)
74+
4875
def test_subscription_email_not_sent_if_no_publication_matches(self):
49-
# Create publication OUTSIDE subscription region
76+
"""No email or log if no pubs intersect the region"""
5077
Publication.objects.create(
5178
title="Outside Region Paper",
5279
abstract="Should not match",
5380
url="https://example.com/outside",
5481
status="p",
5582
publicationDate=now(),
5683
doi="10.1234/outside-doi",
57-
geometry=GeometryCollection(Point(0, 0)), # Outside region
84+
geometry=GeometryCollection(Point(0, 0)),
5885
)
5986

6087
send_subscription_based_email(sent_by=self.user)
61-
6288
self.assertEqual(len(mail.outbox), 0)
6389
self.assertFalse(EmailLog.objects.exists())

0 commit comments

Comments
 (0)