diff --git a/apps/account/migrations/0010_schedulednotification_scheduled_time_and_more.py b/apps/account/migrations/0010_schedulednotification_scheduled_time_and_more.py new file mode 100644 index 0000000..b21e02b --- /dev/null +++ b/apps/account/migrations/0010_schedulednotification_scheduled_time_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.30 on 2026-07-12 10:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0009_schedulednotification_body_en_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='schedulednotification', + name='scheduled_time', + field=models.DateTimeField(blank=True, null=True, verbose_name='scheduled time'), + ), + migrations.AlterField( + model_name='schedulednotification', + name='schedule_type', + field=models.CharField(choices=[('do_not_repeat', 'Do Not Repeat'), ('daily', 'Daily'), ('weekly', 'Weekly'), ('monthly', 'Monthly'), ('cron', 'Custom Cron Expression')], default='do_not_repeat', max_length=20, verbose_name='schedule type'), + ), + ] diff --git a/apps/account/models/notification.py b/apps/account/models/notification.py index 6c4675f..027db29 100644 --- a/apps/account/models/notification.py +++ b/apps/account/models/notification.py @@ -58,6 +58,7 @@ class ScheduledNotification(models.Model): ALL_PROFESSORS = 'all_professors', _('All Professors') class ScheduleTypeChoices(models.TextChoices): + DO_NOT_REPEAT = 'do_not_repeat', _('Do Not Repeat') DAILY = 'daily', _('Daily') WEEKLY = 'weekly', _('Weekly') MONTHLY = 'monthly', _('Monthly') @@ -76,7 +77,8 @@ class ScheduledNotification(models.Model): is_active = models.BooleanField(default=True, verbose_name=_('is active')) - schedule_type = models.CharField(max_length=20, choices=ScheduleTypeChoices.choices, default=ScheduleTypeChoices.DAILY, verbose_name=_('schedule type')) + schedule_type = models.CharField(max_length=20, choices=ScheduleTypeChoices.choices, default=ScheduleTypeChoices.DO_NOT_REPEAT, verbose_name=_('schedule type')) + scheduled_time = models.DateTimeField(null=True, blank=True, verbose_name=_('scheduled time')) time_of_day = models.TimeField(default="09:00:00", verbose_name=_('time of day')) days_of_week = models.CharField(max_length=50, blank=True, null=True, help_text=_('Comma-separated days (1 for Mon, 6 for Sat, 0 for Sun). E.g. "6,0" for Sat and Sun.'), verbose_name=_('days of week')) @@ -99,38 +101,59 @@ class ScheduledNotification(models.Model): return self.name def save(self, *args, **kwargs): - from django_celery_beat.models import CrontabSchedule, PeriodicTask + from django_celery_beat.models import CrontabSchedule, ClockedSchedule, PeriodicTask import json from django.conf import settings - # Calculate cron fields based on schedule_type - minute = str(self.time_of_day.minute) - hour = str(self.time_of_day.hour) - day_of_month = '*' - month_of_year = '*' - day_of_week = '*' - - if self.schedule_type == self.ScheduleTypeChoices.DAILY: - pass - elif self.schedule_type == self.ScheduleTypeChoices.WEEKLY: - day_of_week = self.days_of_week or '*' - elif self.schedule_type == self.ScheduleTypeChoices.MONTHLY: - day_of_month = self.days_of_month or '1' - elif self.schedule_type == self.ScheduleTypeChoices.CRON: - if self.custom_cron: - parts = self.custom_cron.split() - if len(parts) >= 5: - minute, hour, day_of_month, month_of_year, day_of_week = parts[:5] - - schedule, _ = CrontabSchedule.objects.get_or_create( - minute=minute, - hour=hour, - day_of_week=day_of_week, - day_of_month=day_of_month, - month_of_year=month_of_year, - timezone=getattr(settings, 'CELERY_TIMEZONE', 'Asia/Tehran') - ) + schedule = None + clocked = None + one_off = False + if self.schedule_type == self.ScheduleTypeChoices.DO_NOT_REPEAT: + if self.scheduled_time: + clocked, _ = ClockedSchedule.objects.get_or_create( + clocked_time=self.scheduled_time + ) + one_off = True + else: + if self.periodic_task: + task = self.periodic_task + self.periodic_task = None + super().save(*args, **kwargs) + task.delete() + return + else: + super().save(*args, **kwargs) + return + else: + # Calculate cron fields based on schedule_type + minute = str(self.time_of_day.minute) + hour = str(self.time_of_day.hour) + day_of_month = '*' + month_of_year = '*' + day_of_week = '*' + + if self.schedule_type == self.ScheduleTypeChoices.DAILY: + pass + elif self.schedule_type == self.ScheduleTypeChoices.WEEKLY: + day_of_week = self.days_of_week or '*' + elif self.schedule_type == self.ScheduleTypeChoices.MONTHLY: + day_of_month = self.days_of_month or '1' + elif self.schedule_type == self.ScheduleTypeChoices.CRON: + if self.custom_cron: + parts = self.custom_cron.split() + if len(parts) >= 5: + minute, hour, day_of_month, month_of_year, day_of_week = parts[:5] + + schedule, _ = CrontabSchedule.objects.get_or_create( + minute=minute, + hour=hour, + day_of_week=day_of_week, + day_of_month=day_of_month, + month_of_year=month_of_year, + timezone=getattr(settings, 'CELERY_TIMEZONE', 'Asia/Tehran') + ) + task_name = f"Scheduled Notification: {self.name}" task_kwargs = json.dumps({ 'campaign_id': self.id @@ -140,6 +163,8 @@ class ScheduledNotification(models.Model): task = self.periodic_task task.name = task_name task.crontab = schedule + task.clocked = clocked + task.one_off = one_off task.enabled = self.is_active task.kwargs = task_kwargs task.save() @@ -148,6 +173,8 @@ class ScheduledNotification(models.Model): name=task_name, task='apps.account.tasks.run_scheduled_notification_task', crontab=schedule, + clocked=clocked, + one_off=one_off, enabled=self.is_active, kwargs=task_kwargs ) diff --git a/apps/account/serializers/notification.py b/apps/account/serializers/notification.py index 85e5b28..1b77d30 100644 --- a/apps/account/serializers/notification.py +++ b/apps/account/serializers/notification.py @@ -56,5 +56,5 @@ class AdminScheduledNotificationSerializer(serializers.ModelSerializer): 'id', 'name', 'template', 'template_name', 'target_group', 'send_to_all', 'user', 'user_fullname', 'user_email', 'title_ru', 'title_en', 'body_ru', 'body_en', 'is_active', 'schedule_type', 'time_of_day', - 'days_of_week', 'days_of_month', 'custom_cron' + 'days_of_week', 'days_of_month', 'custom_cron', 'scheduled_time' ] \ No newline at end of file diff --git a/apps/account/tasks.py b/apps/account/tasks.py index 2e8d2bc..0b549d3 100644 --- a/apps/account/tasks.py +++ b/apps/account/tasks.py @@ -457,6 +457,10 @@ def run_scheduled_notification_task(campaign_id): notification_type=notification_type ) + if campaign.schedule_type == 'do_not_repeat': + campaign.is_active = False + campaign.save() +