from django.db import models from django.utils.translation import gettext_lazy as _ class Notification(models.Model): class ServiceChoices(models.TextChoices): IMAM_JAVAD = 'imam-javad', _('Imam Javad') DOBOODI = 'doboodi', _('Doboodi') title = models.CharField(max_length=255, verbose_name=_('title')) message = models.TextField(max_length=512, verbose_name=_('message')) user = models.ForeignKey("account.User", on_delete=models.CASCADE, verbose_name=_('user'), related_name='notifications') is_read = models.BooleanField(default=False, verbose_name=_('is read')) service = models.CharField( max_length=20, choices=ServiceChoices.choices, default=ServiceChoices.IMAM_JAVAD, verbose_name=_('service') ) notification_type = models.CharField(max_length=50, null=True, blank=True, verbose_name=_('notification type')) action = models.CharField(max_length=50, default='navigate', verbose_name=_('action')) navigate_to = models.CharField(max_length=255, null=True, blank=True, verbose_name=_('navigate to')) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at'), null=True) updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at'), null=True) class Meta: verbose_name = _('Notification') verbose_name_plural = _('Notifications') def __str__(self): return self.title class NotificationTemplate(models.Model): notification_type = models.CharField(max_length=50, unique=True, verbose_name=_('notification type')) name = models.CharField(max_length=100, verbose_name=_('name')) is_active = models.BooleanField(default=True, verbose_name=_('is active')) title_ru = models.CharField(max_length=255, default="", verbose_name=_('title (RU)')) title_en = models.CharField(max_length=255, default="", verbose_name=_('title (EN)')) body_ru = models.TextField(max_length=1024, default="", verbose_name=_('body (RU)')) body_en = models.TextField(max_length=1024, default="", verbose_name=_('body (EN)')) placeholders_info = models.CharField(max_length=255, blank=True, null=True, verbose_name=_('allowed placeholders description')) class Meta: verbose_name = _('Notification Template') verbose_name_plural = _('Notification Templates') def __str__(self): return f"{self.name} ({self.notification_type})" class ScheduledNotification(models.Model): class TargetGroupChoices(models.TextChoices): ALL_STUDENTS = 'all_students', _('All Students') INACTIVE_STUDENTS = 'inactive_students', _('Students Inactive for > 7 Days') ALL_PROFESSORS = 'all_professors', _('All Professors') class ScheduleTypeChoices(models.TextChoices): DAILY = 'daily', _('Daily') WEEKLY = 'weekly', _('Weekly') MONTHLY = 'monthly', _('Monthly') CRON = 'cron', _('Custom Cron Expression') name = models.CharField(max_length=100, verbose_name=_('campaign name')) template = models.ForeignKey(NotificationTemplate, on_delete=models.CASCADE, verbose_name=_('template')) target_group = models.CharField(max_length=50, choices=TargetGroupChoices.choices, verbose_name=_('target group')) 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')) 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')) days_of_month = models.CharField(max_length=50, blank=True, null=True, help_text=_('Comma-separated days of month (1-31). E.g. "1" for first day of the month.'), verbose_name=_('days of month')) custom_cron = models.CharField(max_length=100, blank=True, null=True, help_text=_('Standard 5-field cron expression: minute hour day_of_month month day_of_week'), verbose_name=_('custom cron expression')) periodic_task = models.OneToOneField( 'django_celery_beat.PeriodicTask', on_delete=models.SET_NULL, null=True, blank=True, related_name='scheduled_notification' ) class Meta: verbose_name = _('Scheduled Notification') verbose_name_plural = _('Scheduled Notifications') def __str__(self): return self.name def save(self, *args, **kwargs): from django_celery_beat.models import CrontabSchedule, 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') ) task_name = f"Scheduled Notification: {self.name}" task_kwargs = json.dumps({ 'campaign_id': self.id }) if self.periodic_task: task = self.periodic_task task.name = task_name task.crontab = schedule task.enabled = self.is_active task.kwargs = task_kwargs task.save() else: task = PeriodicTask.objects.create( name=task_name, task='apps.account.tasks.run_scheduled_notification_task', crontab=schedule, enabled=self.is_active, kwargs=task_kwargs ) self.periodic_task = task super().save(*args, **kwargs) def delete(self, *args, **kwargs): if self.periodic_task: self.periodic_task.delete() super().delete(*args, **kwargs)