Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion osf/email/notification_campaign.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
from framework.celery_tasks import app as celery_app
from celery import chord
from django.utils import timezone
from datetime import timedelta
from osf.models.notification_campaign import NotificationCampaign, NotificationCampaignRecipient, NotificationCampaignStatus, NotificationCampaignRecipientStatus
from osf.email import send_email_with_send_grid
from framework import sentry

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -109,7 +111,9 @@ def process_campaign_retry(*args, **kwargs):
return

if campaign.retries < max_retries:
logger.info(f'Retrying {failed_recipients_count} failed recipients for campaign {campaign_id}')
message = f'[Notification Campaign] Retrying {failed_recipients_count} failed recipients for campaign {campaign_id}'
logger.info(message)
sentry.log_message(message)
filters = {'id__in': failed_recipients.values_list('user_id', flat=True)}
tasks = []
for batch in get_filtered_batches(filters=filters, batch_size=batch_size, campaign_id=campaign_id):
Expand Down Expand Up @@ -189,8 +193,20 @@ def send_campaign_batch(context, recipients_ids, notification_type_name='blank',
).first() # TODO cache

if notification_type is None:
if campaign.status != NotificationCampaignStatus.FAILED:
campaign.status = NotificationCampaignStatus.FAILED
campaign.save()
return

execution_time_window = campaign.metadata.get('execution', {}).get('time_window', 8)
if campaign.started_at < timezone.now() - timedelta(hours=execution_time_window):
if not campaign.developer_reminder_sent:
message = f'[Notification Campaign] Campaign {campaign_id} exceeded its execution time window ({execution_time_window}h).'
logger.warning(message)
sentry.log_message(message)
campaign.developer_reminder_sent = True
campaign.save()

recipients_qs = OSFUser.objects.filter(id__in=recipients_ids)
recipient_records = {
'to_create': [],
Expand Down Expand Up @@ -243,6 +259,8 @@ def send_campaign_batch(context, recipients_ids, notification_type_name='blank',

except Exception as exc:
logger.error(exc) # TODO update error
sentry.log_exception(exc) # TODO update error

recipient_record.status = NotificationCampaignRecipientStatus.FAILED
recipient_record.error_message = str(exc)
recipient_records[operation].append(recipient_record)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by Django 4.2.26 on 2026-07-09 11:38
# Generated by Django 4.2.26 on 2026-07-17 11:20
Comment thread
cslzchen marked this conversation as resolved.

from django.conf import settings
from django.db import migrations, models
Expand Down Expand Up @@ -27,6 +27,7 @@ class Migration(migrations.Migration):
('sent_count', models.PositiveIntegerField(default=0)),
('failed_count', models.PositiveIntegerField(default=0)),
('retries', models.PositiveIntegerField(default=0)),
('developer_reminder_sent', models.BooleanField(default=False)),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
('notification_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='osf.notificationtype')),
],
Expand All @@ -36,7 +37,7 @@ class Migration(migrations.Migration):
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('updated_at', models.DateTimeField(auto_now=True)),
('status', models.CharField(choices=[('pending', 'Pending'), ('sent', 'Sent'), ('failed', 'Failed')], db_index=True, default='pending', max_length=20)),
('status', models.CharField(choices=[('pending', 'Pending'), ('sent', 'Sent'), ('failed', 'Failed'), ('skipped', 'Skipped'), ('postponed', 'Postponed')], db_index=True, default='pending', max_length=20)),
('error_message', models.TextField(blank=True, null=True)),
('campaign', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='recipients', to='osf.notificationcampaign')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
Expand Down
4 changes: 4 additions & 0 deletions osf/models/notification_campaign.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ class NotificationCampaignRecipientStatus(models.TextChoices):
PENDING = 'pending', 'Pending'
SENT = 'sent', 'Sent'
FAILED = 'failed', 'Failed'
SKIPPED = 'skipped', 'Skipped'
POSTPONED = 'postponed', 'Postponed'


class NotificationCampaign(models.Model):
Expand Down Expand Up @@ -64,6 +66,8 @@ class NotificationCampaign(models.Model):
failed_count = models.PositiveIntegerField(default=0)
retries = models.PositiveIntegerField(default=0)

developer_reminder_sent = models.BooleanField(default=False)

def start(self, restart_failed=False):
from osf.email.notification_campaign import start_notification_campaign
self.status = NotificationCampaignStatus.RUNNING
Expand Down