Skip to content

Commit d24cb2c

Browse files
authored
[ENG-8995] Fix issue with no login not being sent (#11326)
* Add No Login to notification template and add to tests
1 parent ffa65ec commit d24cb2c

File tree

5 files changed

+134
-91
lines changed

5 files changed

+134
-91
lines changed

notifications.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,12 @@ notification_types:
399399
object_content_type_model_name: abstractnode
400400
template: 'website/templates/user_message_institutional_access_request.html.mako'
401401

402+
- name: user_no_login
403+
subject: 'We miss you at OSF'
404+
__docs__: ...
405+
object_content_type_model_name: abstractnode
406+
template: 'website/templates/no_login.html.mako'
407+
402408
#### PROVIDER
403409
- name: provider_new_pending_submissions
404410
subject: 'New Pending Submissions'

osf/models/notification_type.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ class Type(str, Enum):
8080
USER_CAMPAIGN_CONFIRM_EMAIL_REGISTRIES_OSF = 'user_campaign_confirm_email_registries_osf'
8181
USER_CAMPAIGN_CONFIRM_EMAIL_ERPC = 'user_campaign_confirm_email_erpc'
8282
USER_DIGEST = 'user_digest'
83+
USER_NO_LOGIN = 'user_no_login'
8384
DIGEST_REVIEWS_MODERATORS = 'digest_reviews_moderators'
8485

8586
# Node notifications

scripts/tests/test_triggered_mails.py

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

scripts/triggered_mails.py

Lines changed: 9 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
11
import logging
22
import uuid
33

4-
from django.core.mail import send_mail
54
from django.db import transaction
65
from django.db.models import Q, Exists, OuterRef
76
from django.utils import timezone
87

98
from framework.celery_tasks import app as celery_app
10-
from osf.models import OSFUser
9+
from osf.models import OSFUser, NotificationType
1110
from website.app import init_app
1211
from website import settings
1312

14-
from osf.models import EmailTask # <-- new
13+
from osf.models import EmailTask
1514

1615
from scripts.utils import add_file_logger
1716

@@ -27,7 +26,7 @@ def main(dry_run: bool = True):
2726
logger.info('No users matched inactivity criteria.')
2827
return
2928

30-
for user in users.iterator():
29+
for user in users:
3130
if dry_run:
3231
logger.warning('Dry run mode')
3332
logger.warning(f'[DRY RUN] Would enqueue no_login email for {user.username}')
@@ -110,39 +109,14 @@ def send_no_login_email(email_task_id: int):
110109
EmailTask.objects.filter(id=email_task.id).update(status='USER_DISABLED')
111110
logger.warning(f'EmailTask {email_task.id}: user {user.id} is not active')
112111
return
113-
114-
# --- Send the email ---
115-
# Replace this with your real templated email system if desired.
116-
subject = 'We miss you at OSF'
117-
message = (
118-
f'Hello {user.fullname},\n\n'
119-
'We noticed you haven’t logged into OSF in a while. '
120-
'Your projects, registrations, and files are still here whenever you need them.\n\n'
121-
f'If you need help, contact us at {settings.OSF_SUPPORT_EMAIL}.\n\n'
122-
'— OSF Team'
123-
)
124-
from_email = settings.OSF_SUPPORT_EMAIL
125-
recipient_list = [user.username] # assuming username is the email address
126-
127-
# If you want HTML email or a template, swap in EmailMultiAlternatives and render_to_string.
128-
sent_count = send_mail(
129-
subject=subject,
130-
message=message,
131-
from_email=from_email,
132-
recipient_list=recipient_list,
133-
fail_silently=False,
112+
NotificationType.Type.USER_NO_LOGIN.instance.emit(
113+
user=user,
114+
event_context={
115+
'user_fullname': user.fullname,
116+
'domain': settings.DOMAIN,
117+
}
134118
)
135119

136-
if sent_count > 0:
137-
EmailTask.objects.filter(id=email_task.id).update(status='SUCCESS')
138-
logger.info(f'EmailTask {email_task.id}: email sent to {user.username}')
139-
else:
140-
EmailTask.objects.filter(id=email_task.id).update(
141-
status='FAILURE',
142-
error_message='send_mail returned 0'
143-
)
144-
logger.error(f'EmailTask {email_task.id}: send_mail returned 0')
145-
146120
except Exception as exc: # noqa: BLE001
147121
logger.exception(f'EmailTask {email_task.id}: error while sending')
148122
EmailTask.objects.filter(id=email_task.id).update(

tests/test_triggered_mails.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# tests/test_triggered_mails.py
2+
from datetime import timedelta
3+
from unittest import mock
4+
5+
from django.utils import timezone
6+
7+
from tests.base import OsfTestCase
8+
from tests.utils import run_celery_tasks, capture_notifications
9+
10+
from osf_tests.factories import UserFactory
11+
from osf.models import EmailTask, NotificationType
12+
13+
from scripts.triggered_mails import (
14+
find_inactive_users_without_enqueued_or_sent_no_login,
15+
main,
16+
NO_LOGIN_PREFIX,
17+
)
18+
19+
20+
def _inactive_time():
21+
"""Make a timestamp that is definitely 'inactive' regardless of threshold settings."""
22+
# Very conservative: 12 weeks ago
23+
return timezone.now() - timedelta(weeks=12)
24+
25+
26+
def _recent_time():
27+
"""Make a timestamp that is definitely NOT inactive."""
28+
return timezone.now() - timedelta(seconds=10)
29+
30+
31+
class TestTriggeredMails(OsfTestCase):
32+
def setUp(self):
33+
super().setUp()
34+
self.user = UserFactory()
35+
self.user.date_last_login = timezone.now()
36+
self.user.save()
37+
38+
def test_dont_trigger_no_login_mail_for_recent_login(self):
39+
self.user.date_last_login = _recent_time()
40+
self.user.save()
41+
42+
with run_celery_tasks():
43+
main(dry_run=False)
44+
45+
# No task should be created
46+
assert EmailTask.objects.filter(
47+
user=self.user, task_id__startswith=NO_LOGIN_PREFIX
48+
).count() == 0
49+
50+
def test_trigger_no_login_mail_enqueues_and_runs_success_path(self):
51+
"""Inactive user -> EmailTask is enqueued and task runs without failing."""
52+
self.user.date_last_login = _inactive_time()
53+
self.user.save()
54+
55+
# Intercept .emit so we don't depend on template rendering
56+
with capture_notifications(), run_celery_tasks():
57+
main(dry_run=False)
58+
59+
tasks = EmailTask.objects.filter(
60+
user=self.user, task_id__startswith=NO_LOGIN_PREFIX
61+
).order_by('id')
62+
assert tasks.count() == 1
63+
# Current task code sets STARTED and only flips to FAILURE on exception;
64+
# allow either STARTED (no explicit success mark yet) or SUCCESS if added later.
65+
assert tasks.latest('id').status in {'STARTED', 'SUCCESS'}
66+
67+
def test_trigger_no_login_mail_failure_marks_task_failure(self):
68+
"""If sending raises, the task should capture the exception and mark FAILURE."""
69+
self.user.date_last_login = _inactive_time()
70+
self.user.save()
71+
72+
# Force the emit call to raise to exercise failure branch
73+
with mock.patch.object(
74+
NotificationType.Type.USER_NO_LOGIN.instance,
75+
'emit',
76+
side_effect=RuntimeError('kaboom'),
77+
), run_celery_tasks():
78+
main(dry_run=False)
79+
80+
task = EmailTask.objects.filter(
81+
user=self.user, task_id__startswith=NO_LOGIN_PREFIX
82+
).latest('id')
83+
task.refresh_from_db()
84+
assert task.status == 'FAILURE'
85+
assert 'kaboom' in (task.error_message or '')
86+
87+
def test_finder_returns_two_inactive_when_none_queued(self):
88+
"""Two inactive users, no prior tasks -> finder returns both."""
89+
u1 = UserFactory(fullname='Jordan Mailata')
90+
u2 = UserFactory(fullname='Jake Elliot')
91+
u1.date_last_login = _inactive_time()
92+
u2.date_last_login = _inactive_time()
93+
u1.save()
94+
u2.save()
95+
96+
users = list(find_inactive_users_without_enqueued_or_sent_no_login())
97+
ids = {u.id for u in users}
98+
assert ids == {u1.id, u2.id}
99+
100+
def test_finder_excludes_users_with_existing_task(self):
101+
"""Inactive users but one already has a no_login EmailTask -> excluded."""
102+
u1 = UserFactory(fullname='Jalen Hurts')
103+
u2 = UserFactory(fullname='Jason Kelece')
104+
u1.date_last_login = _inactive_time()
105+
u2.date_last_login = _inactive_time()
106+
u1.save()
107+
u2.save()
108+
109+
# Pretend u2 already had this email flow (SUCCESS qualifies for exclusion)
110+
EmailTask.objects.create(
111+
task_id=f"{NO_LOGIN_PREFIX}existing-success",
112+
user=u2,
113+
status='SUCCESS',
114+
)
115+
116+
users = list(find_inactive_users_without_enqueued_or_sent_no_login())
117+
ids = {u.id for u in users}
118+
assert ids == {u1.id} # u2 excluded because of existing task

0 commit comments

Comments
 (0)