diff --git a/apps/account/migrations/0009_schedulednotification_body_en_and_more.py b/apps/account/migrations/0009_schedulednotification_body_en_and_more.py new file mode 100644 index 0000000..82455be --- /dev/null +++ b/apps/account/migrations/0009_schedulednotification_body_en_and_more.py @@ -0,0 +1,55 @@ +# Generated by Django 4.2.30 on 2026-07-11 16:34 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0008_remove_notificationtemplate_body_fa_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='schedulednotification', + name='body_en', + field=models.TextField(blank=True, max_length=1024, null=True, verbose_name='body (EN)'), + ), + migrations.AddField( + model_name='schedulednotification', + name='body_ru', + field=models.TextField(blank=True, max_length=1024, null=True, verbose_name='body (RU)'), + ), + migrations.AddField( + model_name='schedulednotification', + name='send_to_all', + field=models.BooleanField(default=False, verbose_name='send to all active users'), + ), + migrations.AddField( + model_name='schedulednotification', + name='title_en', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='title (EN)'), + ), + migrations.AddField( + model_name='schedulednotification', + name='title_ru', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='title (RU)'), + ), + migrations.AddField( + model_name='schedulednotification', + name='user', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='scheduled_notifications', to=settings.AUTH_USER_MODEL, verbose_name='user'), + ), + migrations.AlterField( + model_name='schedulednotification', + name='target_group', + field=models.CharField(blank=True, choices=[('all_students', 'All Students'), ('inactive_students', 'Students Inactive for > 7 Days'), ('all_professors', 'All Professors')], max_length=50, null=True, verbose_name='target group'), + ), + migrations.AlterField( + model_name='schedulednotification', + name='template', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='account.notificationtemplate', verbose_name='template'), + ), + ] diff --git a/apps/account/models/notification.py b/apps/account/models/notification.py index 029479d..6c4675f 100644 --- a/apps/account/models/notification.py +++ b/apps/account/models/notification.py @@ -64,8 +64,16 @@ class ScheduledNotification(models.Model): 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')) + template = models.ForeignKey(NotificationTemplate, on_delete=models.CASCADE, null=True, blank=True, verbose_name=_('template')) + target_group = models.CharField(max_length=50, choices=TargetGroupChoices.choices, null=True, blank=True, verbose_name=_('target group')) + send_to_all = models.BooleanField(default=False, verbose_name=_('send to all active users')) + user = models.ForeignKey("account.User", on_delete=models.CASCADE, null=True, blank=True, verbose_name=_('user'), related_name='scheduled_notifications') + + title_ru = models.CharField(max_length=255, blank=True, null=True, verbose_name=_('title (RU)')) + title_en = models.CharField(max_length=255, blank=True, null=True, verbose_name=_('title (EN)')) + body_ru = models.TextField(max_length=1024, blank=True, null=True, verbose_name=_('body (RU)')) + body_en = models.TextField(max_length=1024, blank=True, null=True, verbose_name=_('body (EN)')) + 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')) diff --git a/apps/account/serializers/notification.py b/apps/account/serializers/notification.py index 4145834..85e5b28 100644 --- a/apps/account/serializers/notification.py +++ b/apps/account/serializers/notification.py @@ -1,7 +1,7 @@ from rest_framework import serializers -from apps.account.models import Notification, NotificationTemplate +from apps.account.models import Notification, NotificationTemplate, ScheduledNotification from apps.account.models import User @@ -42,4 +42,19 @@ class NotificationTemplateSerializer(serializers.ModelSerializer): fields = [ 'id', 'notification_type', 'name', 'is_active', 'title_ru', 'title_en', 'body_ru', 'body_en', 'placeholders_info' + ] + + +class AdminScheduledNotificationSerializer(serializers.ModelSerializer): + user_fullname = serializers.CharField(source='user.fullname', read_only=True) + user_email = serializers.CharField(source='user.email', read_only=True) + template_name = serializers.CharField(source='template.name', read_only=True) + + class Meta: + model = ScheduledNotification + fields = [ + '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' ] \ No newline at end of file diff --git a/apps/account/tasks.py b/apps/account/tasks.py index 4c73752..2e8d2bc 100644 --- a/apps/account/tasks.py +++ b/apps/account/tasks.py @@ -409,25 +409,38 @@ def run_scheduled_notification_task(campaign_id): from datetime import timedelta campaign = ScheduledNotification.objects.filter(id=campaign_id).first() - if not campaign or not campaign.is_active or not campaign.template.is_active: + if not campaign or not campaign.is_active or (campaign.template and not campaign.template.is_active): logger.info(f"[Scheduled Notification Task] Campaign {campaign_id} not found or disabled.") return template = campaign.template + title_en = campaign.title_en or (template.title_en if template else "") + body_en = campaign.body_en or (template.body_en if template else "") + title_ru = campaign.title_ru or (template.title_ru if template else "") + body_ru = campaign.body_ru or (template.body_ru if template else "") + notification_type = template.notification_type if template else "custom" + User = get_user_model() users = User.objects.filter(is_active=True) # 1. Filter target audience - if campaign.target_group == ScheduledNotification.TargetGroupChoices.INACTIVE_STUDENTS: - seven_days_ago = timezone.now() - timedelta(days=7) - users = users.filter( - user_type='student', - last_login__lt=seven_days_ago - ) - elif campaign.target_group == ScheduledNotification.TargetGroupChoices.ALL_STUDENTS: - users = users.filter(user_type='student') - elif campaign.target_group == ScheduledNotification.TargetGroupChoices.ALL_PROFESSORS: - users = users.filter(user_type='professor') + if campaign.send_to_all: + pass + elif campaign.user: + users = users.filter(id=campaign.user_id) + else: + if campaign.target_group == ScheduledNotification.TargetGroupChoices.INACTIVE_STUDENTS: + seven_days_ago = timezone.now() - timedelta(days=7) + users = users.filter( + user_type='student', + last_login__lt=seven_days_ago + ) + elif campaign.target_group == ScheduledNotification.TargetGroupChoices.ALL_STUDENTS: + users = users.filter(user_type='student') + elif campaign.target_group == ScheduledNotification.TargetGroupChoices.ALL_PROFESSORS: + users = users.filter(user_type='professor') + else: + users = users.none() logger.info(f"[Scheduled Notification Task] Sending campaign '{campaign.name}' to {users.count()} users...") @@ -435,13 +448,13 @@ def run_scheduled_notification_task(campaign_id): for user in users: create_and_send_notification( user=user, - title_en=template.title_en, - body_en=template.body_en, - title_ru=template.title_ru, - body_ru=template.body_ru, + title_en=title_en, + body_en=body_en, + title_ru=title_ru, + body_ru=body_ru, service='imam-javad', - data={'type': template.notification_type}, - notification_type=template.notification_type + data={'type': notification_type}, + notification_type=notification_type ) diff --git a/apps/account/urls.py b/apps/account/urls.py index aacc7cf..4fb74b0 100644 --- a/apps/account/urls.py +++ b/apps/account/urls.py @@ -12,6 +12,7 @@ admin_router.register(r'users', views.AdminUserViewSet, basename='admin-users') admin_router.register(r'directory-users', views.AdminUserDirectoryViewSet, basename='admin-directory-users') admin_router.register(r'notifications', views.AdminNotificationViewSet, basename='admin-notifications') admin_router.register(r'notification-templates', views.AdminNotificationTemplateViewSet, basename='admin-notification-templates') +admin_router.register(r'scheduled-notifications', views.AdminScheduledNotificationViewSet, basename='admin-scheduled-notifications') # Hide admin viewsets from swagger for prefix, viewset, basename in admin_router.registry: diff --git a/apps/account/views/notification.py b/apps/account/views/notification.py index 422ee61..8376ed0 100644 --- a/apps/account/views/notification.py +++ b/apps/account/views/notification.py @@ -4,8 +4,8 @@ from rest_framework.authentication import TokenAuthentication from drf_yasg.utils import swagger_auto_schema from drf_yasg import openapi from rest_framework.permissions import IsAuthenticated -from apps.account.serializers import NotificationSerializer, NotificationSendSerializer, AdminNotificationSerializer, NotificationTemplateSerializer -from apps.account.models import Notification, User, NotificationTemplate +from apps.account.serializers import NotificationSerializer, NotificationSendSerializer, AdminNotificationSerializer, NotificationTemplateSerializer, AdminScheduledNotificationSerializer +from apps.account.models import Notification, User, NotificationTemplate, ScheduledNotification from apps.account.tasks import send_notification from utils.pagination import StandardResultsSetPagination from django.db.models import Q @@ -395,3 +395,28 @@ class AdminNotificationTemplateViewSet(ModelViewSet): def destroy(self, request, *args, **kwargs): return Response({'error': 'Method not allowed.'}, status=status.HTTP_405_METHOD_NOT_ALLOWED) + + +class AdminScheduledNotificationViewSet(ModelViewSet): + permission_classes = [IsSuperAdmin] + authentication_classes = [TokenAuthentication] + serializer_class = AdminScheduledNotificationSerializer + pagination_class = StandardResultsSetPagination + + def get_queryset(self): + queryset = ScheduledNotification.objects.all().select_related('template', 'user') + + search = self.request.query_params.get('search', None) + if search: + queryset = queryset.filter( + Q(name__icontains=search) | + Q(title_ru__icontains=search) | + Q(title_en__icontains=search) | + Q(template__name__icontains=search) + ) + + is_active_param = self.request.query_params.get('is_active', None) + if is_active_param is not None: + queryset = queryset.filter(is_active=(is_active_param.lower() == 'true')) + + return queryset.order_by('-id')