Skip to content

Commit 5ef1089

Browse files
committed
update celery to remove old tasks that sent notifications
1 parent 5c6c61e commit 5ef1089

2 files changed

Lines changed: 136 additions & 33 deletions

File tree

scripts/triggered_mails.py

Lines changed: 136 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,157 @@
11
import logging
2+
import uuid
23

4+
from django.core.mail import send_mail
35
from django.db import transaction
4-
from django.db.models import Q
6+
from django.db.models import Q, Exists, OuterRef
57
from django.utils import timezone
68

79
from framework.celery_tasks import app as celery_app
810
from osf.models import OSFUser
9-
from osf.models.queued_mail import NO_LOGIN_TYPE, NO_LOGIN, QueuedMail, queue_mail
1011
from website.app import init_app
1112
from website import settings
1213

14+
from osf.models import EmailTask # <-- new
15+
1316
from scripts.utils import add_file_logger
1417

1518
logger = logging.getLogger(__name__)
1619
logging.basicConfig(level=logging.INFO)
1720

21+
NO_LOGIN_PREFIX = 'no_login:' # used to namespace this email type in task_id
22+
23+
24+
def main(dry_run: bool = True):
25+
users = find_inactive_users_without_enqueued_or_sent_no_login()
26+
if not users.exists():
27+
logger.info('No users matched inactivity criteria.')
28+
return
1829

19-
def main(dry_run=True):
20-
for user in find_inactive_users_with_no_inactivity_email_sent_or_queued():
30+
for user in users.iterator():
2131
if dry_run:
2232
logger.warning('Dry run mode')
23-
logger.warning(f'Email of type no_login queued to {user.username}')
24-
if not dry_run:
25-
with transaction.atomic():
26-
queue_mail(
27-
to_addr=user.username,
28-
mail=NO_LOGIN,
29-
send_at=timezone.now(),
30-
user=user,
31-
fullname=user.fullname,
32-
osf_support_email=settings.OSF_SUPPORT_EMAIL,
33-
)
34-
35-
36-
def find_inactive_users_with_no_inactivity_email_sent_or_queued():
37-
users_sent_ids = QueuedMail.objects.filter(email_type=NO_LOGIN_TYPE).values_list('user__guids___id')
38-
return (OSFUser.objects
39-
.filter(
40-
(Q(date_last_login__lt=timezone.now() - settings.NO_LOGIN_WAIT_TIME) & ~Q(tags__name='osf4m')) |
41-
Q(date_last_login__lt=timezone.now() - settings.NO_LOGIN_OSF4M_WAIT_TIME, tags__name='osf4m'),
42-
is_active=True)
43-
.exclude(guids___id__in=users_sent_ids))
44-
45-
@celery_app.task(name='scripts.triggered_mails')
33+
logger.warning(f'[DRY RUN] Would enqueue no_login email for {user.username}')
34+
continue
35+
36+
with transaction.atomic():
37+
# Create the EmailTask row first (status=PENDING)
38+
task_id = f'{NO_LOGIN_PREFIX}{uuid.uuid4()}'
39+
email_task = EmailTask.objects.create(
40+
task_id=task_id,
41+
user=user,
42+
status='PENDING',
43+
)
44+
logger.info(f'Queued EmailTask {email_task.task_id} for user {user.username}')
45+
46+
# Kick off the Celery task with the EmailTask PK
47+
send_no_login_email.delay(email_task_id=email_task.id)
48+
49+
50+
def find_inactive_users_without_enqueued_or_sent_no_login():
51+
"""
52+
Match your original inactivity rules, but exclude users who already have a no_login EmailTask
53+
either pending, started, retrying, or already sent successfully.
54+
"""
55+
56+
# Subquery: Is there already a not-yet-failed/aborted EmailTask for this user with our prefix?
57+
existing_no_login = EmailTask.objects.filter(
58+
user_id=OuterRef('pk'),
59+
task_id__startswith=NO_LOGIN_PREFIX,
60+
status__in=['PENDING', 'STARTED', 'RETRY', 'SUCCESS'],
61+
)
62+
63+
base_q = OSFUser.objects.filter(is_active=True).filter(
64+
Q(
65+
date_last_login__lt=timezone.now() - settings.NO_LOGIN_WAIT_TIME,
66+
# NOT tagged osf4m
67+
) & ~Q(tags__name='osf4m')
68+
|
69+
Q(
70+
date_last_login__lt=timezone.now() - settings.NO_LOGIN_OSF4M_WAIT_TIME,
71+
tags__name='osf4m'
72+
)
73+
)
74+
75+
# Exclude users who already have a task for this email type
76+
return base_q.annotate(_has_task=Exists(existing_no_login)).filter(_has_task=False)
77+
78+
79+
@celery_app.task(name='scripts.triggered_no_login_email')
80+
def send_no_login_email(email_task_id: int):
81+
"""
82+
Worker that sends the no-login email and updates EmailTask.status accordingly.
83+
"""
84+
85+
# Late import to avoid app registry issues in Celery
86+
from osf.models import EmailTask
87+
88+
try:
89+
email_task = EmailTask.objects.select_related('user').get(id=email_task_id)
90+
except EmailTask.DoesNotExist:
91+
logger.error(f'EmailTask {email_task_id} not found')
92+
return
93+
94+
# If this task already reached a terminal state, don't send again (idempotent)
95+
if email_task.status in ['SUCCESS']:
96+
logger.info(f'EmailTask {email_task.id} already SUCCESS; skipping')
97+
return
98+
99+
# Update to STARTED
100+
EmailTask.objects.filter(id=email_task.id).update(status='STARTED')
101+
102+
try:
103+
user = email_task.user
104+
if user is None:
105+
EmailTask.objects.filter(id=email_task.id).update(status='NO_USER_FOUND')
106+
logger.warning(f'EmailTask {email_task.id}: no associated user')
107+
return
108+
109+
if not user.is_active:
110+
EmailTask.objects.filter(id=email_task.id).update(status='USER_DISABLED')
111+
logger.warning(f'EmailTask {email_task.id}: user {user.id} is not active')
112+
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,
134+
)
135+
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+
146+
except Exception as exc: # noqa: BLE001
147+
logger.exception(f'EmailTask {email_task.id}: error while sending')
148+
EmailTask.objects.filter(id=email_task.id).update(
149+
status='FAILURE',
150+
error_message=str(exc)
151+
)
152+
153+
154+
@celery_app.task(name='scripts.triggered_mails') # keep the original entry point for compatibility
46155
def run_main(dry_run=True):
47156
init_app(routes=False)
48157
if not dry_run:

website/settings/defaults.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -436,7 +436,6 @@ class CeleryConfig:
436436
'scripts.generate_sitemap',
437437
'osf.management.commands.clear_expired_sessions',
438438
'osf.management.commands.delete_withdrawn_or_failed_registration_files',
439-
'osf.management.commands.check_crossref_dois',
440439
'osf.management.commands.find_spammy_files',
441440
'osf.management.commands.migrate_pagecounter_data',
442441
'osf.management.commands.migrate_deleted_date',
@@ -565,21 +564,16 @@ class CeleryConfig:
565564
'scripts.approve_registrations',
566565
'scripts.approve_embargo_terminations',
567566
'scripts.triggered_mails',
568-
'scripts.website.notifications.tasks.send_moderators_digest_email',
569-
'scripts.website.notifications.tasks.send_users_digest_email',
570567
'scripts.generate_sitemap',
571568
'scripts.premigrate_created_modified',
572569
'scripts.add_missing_identifiers_to_preprints',
573570
'osf.management.commands.clear_expired_sessions',
574571
'osf.management.commands.deactivate_requested_accounts',
575-
'osf.management.commands.check_crossref_dois',
576-
'osf.management.commands.find_spammy_files',
577572
'osf.management.commands.update_institution_project_counts',
578573
'osf.management.commands.correct_registration_moderation_states',
579574
'osf.management.commands.sync_collection_provider_indices',
580575
'osf.management.commands.sync_datacite_doi_metadata',
581576
'osf.management.commands.archive_registrations_on_IA',
582-
'osf.management.commands.populate_initial_schema_responses',
583577
'osf.management.commands.approve_pending_schema_responses',
584578
'osf.management.commands.sync_doi_metadata',
585579
'api.providers.tasks',

0 commit comments

Comments
 (0)