diff --git a/api_tests/notifications/test_notification_digest.py b/api_tests/notifications/test_notification_digest.py index a1065c27604..73067456f27 100644 --- a/api_tests/notifications/test_notification_digest.py +++ b/api_tests/notifications/test_notification_digest.py @@ -1,4 +1,5 @@ import pytest +from django.utils import timezone from django.contrib.contenttypes.models import ContentType from osf.models import Notification, NotificationType, NotificationTypeEnum, EmailTask, Email @@ -186,6 +187,26 @@ def test_get_users_emails(self): assert user_info['user_id'] == user._id assert any(msg['notification_id'] == notification1.id for msg in user_info['info']) + def test_get_users_emails_ignore_scheduled(self): + user = AuthUserFactory() + notification_type = NotificationType.objects.get(name=NotificationTypeEnum.USER_FILE_UPDATED) + notification1 = Notification.objects.create( + subscription=add_notification_subscription(user, notification_type, 'daily'), + event_context={}, + sent=None + ) + Notification.objects.create( + subscription=add_notification_subscription(user, notification_type, 'daily'), + event_context={}, + sent=None, + scheduled=timezone.now() + ) + res = list(get_users_emails('daily')) + assert len(res) == 1 + user_info = res[0] + assert user_info['user_id'] == user._id + assert any(msg['notification_id'] == notification1.id for msg in user_info['info']) + def test_get_moderators_emails(self): user = AuthUserFactory() provider = RegistrationProviderFactory() @@ -204,6 +225,30 @@ def test_get_moderators_emails(self): ] assert entry, 'Expected moderator digest group' + def test_get_moderators_emails_ignore_scheduled(self): + user = AuthUserFactory() + provider = RegistrationProviderFactory() + reg = RegistrationFactory(provider=provider) + notification_type = NotificationType.objects.get(name=NotificationTypeEnum.PROVIDER_NEW_PENDING_SUBMISSIONS) + subscription = add_notification_subscription(user, notification_type, 'daily', subscribed_object=reg) + Notification.objects.create( + subscription=subscription, + event_context={}, + sent=None + ) + Notification.objects.create( + subscription=subscription, + event_context={}, + sent=None, + scheduled=timezone.now() + ) + res = list(get_moderators_emails('daily')) + assert len(res) >= 1 + entry = [ + x for x in res if x['user_id'] == user._id and subscription.subscribed_object.id == reg.id + ] + assert entry, 'Expected moderator digest group' + def test_send_users_digest_email_end_to_end(self): user = AuthUserFactory() notification_type = NotificationType.objects.get(name=NotificationTypeEnum.USER_FILE_UPDATED) diff --git a/notifications/tasks.py b/notifications/tasks.py index daea793e9e0..c9ffae4bf53 100644 --- a/notifications/tasks.py +++ b/notifications/tasks.py @@ -314,6 +314,8 @@ def send_users_digest_email(dry_run=False): user_id = group['user_id'] notification_ids = [msg['notification_id'] for msg in group['info']] if not dry_run: + notifications_qs = Notification.objects.filter(id__in=notification_ids) + notifications_qs.update(scheduled=timezone.now()) send_user_email_task.delay(user_id, notification_ids) @celery_app.task(name='notifications.tasks.send_moderators_digest_email') @@ -334,6 +336,8 @@ def send_moderators_digest_email(dry_run=False): provider_content_type_id = group['provider_content_type_id'] notification_ids = [msg['notification_id'] for msg in group['info']] if not dry_run: + notifications_qs = Notification.objects.filter(id__in=notification_ids) + notifications_qs.update(scheduled=timezone.now()) send_moderator_email_task.delay(user_id, notification_ids, provider_content_type_id, provider_id) def get_moderators_emails(message_freq: str): @@ -358,6 +362,7 @@ def get_moderators_emails(message_freq: str): INNER JOIN osf_notificationtype AS nt ON ns.notification_type_id = nt.id LEFT JOIN osf_guid ON ns.user_id = osf_guid.object_id WHERE n.sent IS NULL + AND n.scheduled IS NULL AND ns.message_frequency = %s AND nt.name IN (%s, %s) AND nt.name NOT IN (%s, %s, %s) @@ -401,6 +406,7 @@ def get_users_emails(message_freq): INNER JOIN osf_notificationtype AS nt ON ns.notification_type_id = nt.id LEFT JOIN osf_guid ON ns.user_id = osf_guid.object_id WHERE n.sent IS NULL + AND n.scheduled IS NULL AND ns.message_frequency = %s AND nt.name NOT IN (%s, %s, %s, %s, %s) AND osf_guid.content_type_id = ( diff --git a/osf/migrations/0040_notification_scheduled.py b/osf/migrations/0040_notification_scheduled.py new file mode 100644 index 00000000000..50f05463629 --- /dev/null +++ b/osf/migrations/0040_notification_scheduled.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.26 on 2026-06-04 13:39 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('osf', '0039_merge_20260427_1359'), + ] + + operations = [ + migrations.AddField( + model_name='notification', + name='scheduled', + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/osf/models/notification.py b/osf/models/notification.py index 533a05a4e97..d26c4efabac 100644 --- a/osf/models/notification.py +++ b/osf/models/notification.py @@ -18,6 +18,7 @@ class Notification(models.Model): sent = models.DateTimeField(null=True, blank=True) created = models.DateTimeField(auto_now_add=True) fake_sent = models.BooleanField(default=False) + scheduled = models.DateTimeField(null=True, blank=True) def send( self,