From 22b457a2e865ee7cee6e7fa3cb2e6eef7f605f0b Mon Sep 17 00:00:00 2001 From: mohsentaba Date: Wed, 8 Jul 2026 11:48:36 +0330 Subject: [PATCH] Learning process notifications logic updated and added --- Mobile App Push Notifications.md | 56 +++++ apps/account/notification_service.py | 43 ++++ apps/account/tasks.py | 121 ++++++++++ apps/account/tests/notifications/__init__.py | 1 + .../tests/notifications/test_notifications.py | 207 ++++++++++++++++++ apps/certificate/apps.py | 3 + apps/certificate/signals.py | 43 ++++ ...urse_notified_new_registration_and_more.py | 28 +++ apps/course/models/course.py | 5 + apps/course/models/live_session.py | 10 + apps/course/signals.py | 89 +++++++- config/settings/base.py | 13 ++ config/settings/production.py | 8 + 13 files changed, 625 insertions(+), 2 deletions(-) create mode 100644 Mobile App Push Notifications.md create mode 100644 apps/account/notification_service.py create mode 100644 apps/account/tests/notifications/__init__.py create mode 100644 apps/account/tests/notifications/test_notifications.py create mode 100644 apps/certificate/signals.py create mode 100644 apps/course/migrations/0018_course_notified_new_registration_and_more.py diff --git a/Mobile App Push Notifications.md b/Mobile App Push Notifications.md new file mode 100644 index 0000000..58ab79a --- /dev/null +++ b/Mobile App Push Notifications.md @@ -0,0 +1,56 @@ +## 1. Learning Process + +|Event|Trigger Condition|Recipient| +|---|---|---| +|**Course Access Granted**|When access to the course is activated|Student| +|**LIVE Class Reminder**|2 hours and 15 minutes before the start (only if not cancelled/rescheduled)|Students enrolled in the course| +|**Course Completed**|When all lessons and mandatory conditions are met|Student| +|**Certificate Issued**|Immediately after the certificate is generated (auto or manual)|Student| +|**New Course Registration**|**Weekly check (Fridays)** for courses with "Registration" status published in the last 7 days:
- **1 course:** "Registration is open for: [Name]"
- **2-3 courses:** "Great news! Enrollment is open for: [Name 1], [Name 2], and others"
- **3+ courses:** "New directions are open at Imam al-Jawad School. Choose the right one for you!"
_Note: Do not notify about the same course more than once._|All registered students| + +--- + +## 2. Quizzes and Progress + +|Event|Trigger Condition|Recipient| +|---|---|---| +|**Low Quiz Result**|2, 7, and 21 days after a score below the threshold, suggesting a retake|Student| + +--- + +## 3. LIVE Classes + +|Event|Trigger Condition|Recipient| +|---|---|---| +|**LIVE Class Cancelled**|Immediately after the teacher or admin cancels the session|All enrolled students| +|**LIVE Class Rescheduled**|Immediately after date or time change|All enrolled students| +|**LIVE Recording Available**|When the session recording is uploaded and available in the archive|All enrolled students| + +--- + +## 4. Communication + +|Event|Trigger Condition|Recipient| +|---|---|---| +|**Teacher Replied**|When a teacher replies in a private or course chat|Student (the recipient)| +|**Support Replied**|When a support agent replies to a ticket/chat|Student (the sender)| + +--- + +## 5. Transactions and Finance + +|Event|Trigger Condition|Recipient| +|---|---|---| +|**Payment Successful**|Immediately after payment confirmation|Student| +|**Payment Failed**|Immediately after a failed transaction attempt|Student| +|**Refund Completed**|After the refund is processed and confirmed|Student| + +--- + +## 6. Retention and Motivation + +|Event|Trigger Condition|Recipient| +|---|---|---| +|**Inactivity (Self-paced)**|If the student hasn't opened the app or taken lessons for 7+ days|Student| +|**Missed LIVE Sessions**|If the student missed 2 consecutive LIVE broadcasts|Student| +|**Unfinished Quiz**|If a quiz was started but not completed within 24 hours|Student| \ No newline at end of file diff --git a/apps/account/notification_service.py b/apps/account/notification_service.py new file mode 100644 index 0000000..6a3c2e9 --- /dev/null +++ b/apps/account/notification_service.py @@ -0,0 +1,43 @@ +import asyncio +import logging +from apps.account.models import Notification +from apps.account.tasks import send_notification + +logger = logging.getLogger(__name__) + +def create_and_send_notification(user, title_en, body_en, title_fa, body_fa, service='imam-javad', data=None): + """ + Creates a Notification record in the database and sends a push notification to FCM. + Resolves localized title and body based on user.language. + """ + lang = getattr(user, 'language', None) + lang_code = lang.code if lang and hasattr(lang, 'code') else 'fa' + + title = title_fa if lang_code == 'fa' else title_en + message = body_fa if lang_code == 'fa' else body_en + + # Save to database + notif = Notification.objects.create( + user=user, + title=title, + message=message, + service=service + ) + + fcm_token = getattr(user, 'fcm', None) + if fcm_token: + try: + try: + loop = asyncio.get_event_loop() + except RuntimeError: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + if loop.is_running(): + loop.create_task(send_notification([fcm_token], title, message, data)) + else: + loop.run_until_complete(send_notification([fcm_token], title, message, data)) + except Exception as e: + logger.error(f"Failed to send push notification via FCM: {e}") + + return notif diff --git a/apps/account/tasks.py b/apps/account/tasks.py index 3ea0628..04869e8 100644 --- a/apps/account/tasks.py +++ b/apps/account/tasks.py @@ -158,4 +158,125 @@ def send_otp_code_whatsapp(phone_number, code): time.sleep(2) +@shared_task +def check_live_class_reminders_task(): + """ + Checks for any non-cancelled live classes starting in the next 2 hours and 15 minutes + and sends a reminder to all enrolled students. + """ + from django.utils import timezone + from datetime import timedelta + from apps.course.models import CourseLiveSession, Participant + from apps.course.models.course import get_localized_field + from apps.account.notification_service import create_and_send_notification + + now = timezone.now() + sessions = CourseLiveSession.objects.filter( + started_at__gt=now, + started_at__lte=now + timedelta(hours=2, minutes=15), + reminder_sent=False, + is_cancelled=False + ) + + logger.info(f"[Live Session Reminder Check] Found {sessions.count()} sessions starting within 2h 15m.") + + for session in sessions: + participants = Participant.objects.filter(course=session.course, is_active=True).select_related('student') + for p in participants: + student_user = p.student + course_title_en = get_localized_field('en', session.course.title) + course_title_fa = get_localized_field('fa', session.course.title) + + create_and_send_notification( + user=student_user, + title_en="LIVE Class Reminder", + body_en=f"The live class for '{course_title_en}' starts in 2 hours and 15 minutes.", + title_fa="یادآوری کلاس زنده", + body_fa=f"کلاس زنده دوره «{course_title_fa}» تا ۲ ساعت و ۱۵ دقیقه دیگر شروع می‌شود.", + service='imam-javad', + data={'type': 'live_class_reminder', 'session_id': session.id} + ) + + session.reminder_sent = True + session.save(update_fields=['reminder_sent']) + + +@shared_task +def check_new_course_registration_task(): + """ + Weekly check (Fridays) for courses with 'registering' status published in the last 7 days. + Notifies all registered students about new courses. + """ + from django.utils import timezone + from datetime import timedelta + from django.contrib.auth import get_user_model + from apps.course.models import Course + from apps.course.models.course import get_localized_field, normalize_status + from apps.account.notification_service import create_and_send_notification + + User = get_user_model() + now = timezone.now() + + candidate_courses = Course.objects.filter( + created_at__gte=now - timedelta(days=7), + notified_new_registration=False + ) + + new_courses = [] + for course in candidate_courses: + if normalize_status(course.status) == 'registering': + new_courses.append(course) + + if not new_courses: + logger.info("[New Course Registration Check] No new registering courses found in the last 7 days.") + return + + logger.info(f"[New Course Registration Check] Found {len(new_courses)} new registering courses.") + + if len(new_courses) == 1: + course = new_courses[0] + name_en = get_localized_field('en', course.title) or "New Course" + name_fa = get_localized_field('fa', course.title) or "دوره جدید" + + title_en = "New Course Registration" + body_en = f"Registration is open for: {name_en}" + + title_fa = "ثبت‌نام دوره جدید" + body_fa = f"ثبت‌نام برای دوره «{name_fa}» آغاز شد." + elif len(new_courses) <= 3: + names_en = ", ".join(get_localized_field('en', c.title) for c in new_courses[:-1]) + f", and {get_localized_field('en', new_courses[-1].title)}" + names_fa = "، ".join(get_localized_field('fa', c.title) for c in new_courses[:-1]) + f" و {get_localized_field('fa', new_courses[-1].title)}" + + title_en = "New Courses Registration" + body_en = f"Great news! Enrollment is open for: {names_en}, and others" + + title_fa = "ثبت‌نام دوره‌های جدید" + body_fa = f"خبر خوب! ثبت‌نام برای دوره‌های «{names_fa}» و دیگر دوره‌ها آغاز شد." + else: + title_en = "New Courses at Imam al-Jawad School" + body_en = "New directions are open at Imam al-Jawad School. Choose the right one for you!" + + title_fa = "دوره‌های جدید در مدرسه امام جواد" + body_fa = "دوره‌های جدید در مدرسه امام جواد (ع) آغاز شده است. مسیر مناسب خود را انتخاب کنید!" + + students = User.objects.filter(user_type='student', is_active=True) + logger.info(f"[New Course Registration Check] Notifying {students.count()} students.") + + for student in students: + create_and_send_notification( + user=student, + title_en=title_en, + body_en=body_en, + title_fa=title_fa, + body_fa=body_fa, + service='imam-javad', + data={'type': 'new_course_registration'} + ) + + for course in new_courses: + course.notified_new_registration = True + course.save(update_fields=['notified_new_registration']) + + + diff --git a/apps/account/tests/notifications/__init__.py b/apps/account/tests/notifications/__init__.py new file mode 100644 index 0000000..62a4bef --- /dev/null +++ b/apps/account/tests/notifications/__init__.py @@ -0,0 +1 @@ +# Notifications tests package diff --git a/apps/account/tests/notifications/test_notifications.py b/apps/account/tests/notifications/test_notifications.py new file mode 100644 index 0000000..64d94e9 --- /dev/null +++ b/apps/account/tests/notifications/test_notifications.py @@ -0,0 +1,207 @@ +import datetime +from django.utils import timezone +from django.test import TestCase +from unittest.mock import patch +from django.core.files.uploadedfile import SimpleUploadedFile + +from dj_language.models import Language +from apps.account.models import StudentUser, User, Notification +from apps.course.models.course import Course, CourseCategory +from apps.course.models.lesson import CourseChapter, CourseLesson, Lesson, LessonCompletion +from apps.course.models.participant import Participant +from apps.course.models.live_session import CourseLiveSession +from apps.certificate.models import Certificate +from apps.account.tasks import check_live_class_reminders_task, check_new_course_registration_task + +class NotificationFlowTests(TestCase): + def setUp(self): + # Create languages + self.lang_fa, _ = Language.objects.update_or_create( + code='fa', + defaults={'name': 'Persian', 'status': True, 'countries': []} + ) + self.lang_en, _ = Language.objects.update_or_create( + code='en', + defaults={'name': 'English', 'status': True, 'countries': []} + ) + + # Create category + self.category = CourseCategory.objects.create( + name=[{"title": "Category", "language_code": "en"}], + slug=[{"title": "category", "language_code": "en"}] + ) + + # Create student user + self.student = StudentUser.objects.create( + email='student_test@example.com', + username='student_test', + fullname='Test Student', + language=self.lang_fa, # Persian template check + fcm='token_student' + ) + + # Create English student user for template check + self.student_en = StudentUser.objects.create( + email='student_en@example.com', + username='student_en', + fullname='Test Student EN', + language=self.lang_en, + fcm='token_student_en' + ) + + def _create_course(self, slug, title_en, title_fa, status='ongoing'): + thumbnail = SimpleUploadedFile('thumb.jpg', b'filecontent', content_type='image/jpeg') + return Course.objects.create( + title=[ + {"title": title_en, "language_code": "en"}, + {"title": title_fa, "language_code": "fa"} + ], + slug=[{"title": slug, "language_code": "en"}], + category=self.category, + thumbnail=thumbnail, + video_type=Course.VedioTypeChoices.YOUTUBE_LINK, + video_link='https://example.com/video', + is_online=False, + level=Course.LevelChoices.BEGINNER, + duration=10, + lessons_count=0, + description='Description', + short_description='Short description', + status=[ + {"title": status, "language_code": "en"}, + {"title": status, "language_code": "ru"} + ], + is_free=True, + ) + + @patch('apps.account.notification_service.send_notification') + def test_course_access_granted_notification(self, mock_send): + # Event 1: Course Access Granted + course = self._create_course('course-access', 'Course 1', 'دوره ۱') + + # Create participant (active=True by default) + Participant.objects.create(student=self.student, course=course) + + # Verify notification created in DB (in Persian) + notif_fa = Notification.objects.filter(user=self.student).first() + self.assertIsNotNone(notif_fa) + self.assertEqual(notif_fa.title, "دسترسی به دوره فعال شد") + self.assertIn("دوره ۱", notif_fa.message) + + # Create participant for English student + Participant.objects.create(student=self.student_en, course=course) + notif_en = Notification.objects.filter(user=self.student_en).first() + self.assertIsNotNone(notif_en) + self.assertEqual(notif_en.title, "Course Access Granted") + self.assertIn("Course 1", notif_en.message) + + # Verify send_notification was called + self.assertTrue(mock_send.called) + + @patch('apps.account.notification_service.send_notification') + def test_live_class_reminder_notification(self, mock_send): + # Event 2: LIVE Class Reminder + course = self._create_course('course-live', 'Course 2', 'دوره ۲') + Participant.objects.create(student=self.student, course=course) + + # Create a live session starting in 2 hours + session = CourseLiveSession.objects.create( + course=course, + subject='Live Lesson 1', + started_at=timezone.now() + datetime.timedelta(hours=2), + is_cancelled=False, + reminder_sent=False + ) + + # Run periodic task + check_live_class_reminders_task() + + # Verify reminder sent + session.refresh_from_db() + self.assertTrue(session.reminder_sent) + + notif = Notification.objects.filter(user=self.student, title="یادآوری کلاس زنده").first() + self.assertIsNotNone(notif) + self.assertIn("دوره ۲", notif.message) + + # Test rescheduling resets reminder_sent + session.started_at = timezone.now() + datetime.timedelta(hours=5) + session.save() + + session.refresh_from_db() + self.assertFalse(session.reminder_sent) + + @patch('apps.account.notification_service.send_notification') + def test_course_completed_notification(self, mock_send): + # Event 3: Course Completed + course = self._create_course('course-complete', 'Course 3', 'دوره ۳') + Participant.objects.create(student=self.student, course=course) + + chapter = CourseChapter.objects.create( + course=course, + title='Chapter 1', + priority=1, + is_active=True + ) + + # Add 2 lessons + l1 = Lesson.objects.create(title='Lesson 1', content_type='video_file', duration=5) + cl1 = CourseLesson.objects.create(course=course, chapter=chapter, lesson=l1, priority=1, is_active=True) + + l2 = Lesson.objects.create(title='Lesson 2', content_type='video_file', duration=5) + cl2 = CourseLesson.objects.create(course=course, chapter=chapter, lesson=l2, priority=2, is_active=True) + + # Complete first lesson + LessonCompletion.objects.create(student=self.student, course_lesson=cl1) + self.assertFalse(Notification.objects.filter(user=self.student, title="دوره به پایان رسید").exists()) + + # Complete second (last) lesson + LessonCompletion.objects.create(student=self.student, course_lesson=cl2) + + # Verify notification triggered + self.assertTrue(Notification.objects.filter(user=self.student, title="دوره به پایان رسید").exists()) + + @patch('apps.account.notification_service.send_notification') + def test_certificate_issued_notification(self, mock_send): + # Event 4: Certificate Issued + course = self._create_course('course-cert', 'Course 4', 'دوره ۴') + + cert = Certificate.objects.create( + student=self.student, + course=course, + status='pending' + ) + + # Verify no notification yet + self.assertFalse(Notification.objects.filter(user=self.student, title="گواهی صادر شد").exists()) + + # Approve certificate + cert.status = 'approved' + cert.save() + + # Verify notification triggered + self.assertTrue(Notification.objects.filter(user=self.student, title="گواهی صادر شد").exists()) + + @patch('apps.account.notification_service.send_notification') + def test_new_course_registration_notification(self, mock_send): + # Event 5: New Course Registration + student_recipient = User.objects.create( + email='student_recipient@example.com', + username='student_recipient', + fullname='Recipient Student', + user_type='student', + is_active=True, + language=self.lang_fa, + fcm='token_recipient' + ) + + course = self._create_course('course-new', 'New Course A', 'دوره جدید الف', status='registering') + + check_new_course_registration_task() + + course.refresh_from_db() + self.assertTrue(course.notified_new_registration) + + notif = Notification.objects.filter(user=student_recipient, title="ثبت‌نام دوره جدید").first() + self.assertIsNotNone(notif) + self.assertIn("دوره جدید الف", notif.message) diff --git a/apps/certificate/apps.py b/apps/certificate/apps.py index 6a8b903..5d62170 100644 --- a/apps/certificate/apps.py +++ b/apps/certificate/apps.py @@ -4,3 +4,6 @@ from django.apps import AppConfig class CertificateConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'apps.certificate' + + def ready(self): + import apps.certificate.signals diff --git a/apps/certificate/signals.py b/apps/certificate/signals.py new file mode 100644 index 0000000..d4ea86f --- /dev/null +++ b/apps/certificate/signals.py @@ -0,0 +1,43 @@ +import logging +from django.db.models.signals import pre_save, post_save +from django.dispatch import receiver +from apps.certificate.models import Certificate +from apps.account.notification_service import create_and_send_notification +from apps.course.models.course import extract_text_from_json, get_localized_field + +logger = logging.getLogger(__name__) + +@receiver(pre_save, sender=Certificate) +def store_certificate_previous_status(sender, instance, **kwargs): + if instance.pk: + try: + old = Certificate.objects.get(pk=instance.pk) + instance._previous_status = old.status + except Certificate.DoesNotExist: + instance._previous_status = None + else: + instance._previous_status = None + + +@receiver(post_save, sender=Certificate) +def notify_certificate_issued(sender, instance, created, **kwargs): + # Trigger when status becomes approved + if instance.status == 'approved': + was_approved = False + if created: + was_approved = True + elif hasattr(instance, '_previous_status') and instance._previous_status != 'approved': + was_approved = True + + if was_approved: + course_title_en = get_localized_field('en', instance.course.title) + course_title_fa = get_localized_field('fa', instance.course.title) + create_and_send_notification( + user=instance.student, + title_en="Certificate Issued", + body_en=f"Your certificate for '{course_title_en}' has been issued.", + title_fa="گواهی صادر شد", + body_fa=f"گواهی پایان دوره شما برای «{course_title_fa}» صادر شد.", + service='imam-javad', + data={'type': 'certificate_issued', 'certificate_id': instance.id} + ) diff --git a/apps/course/migrations/0018_course_notified_new_registration_and_more.py b/apps/course/migrations/0018_course_notified_new_registration_and_more.py new file mode 100644 index 0000000..064c1c8 --- /dev/null +++ b/apps/course/migrations/0018_course_notified_new_registration_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 5.2.12 on 2026-07-08 11:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('course', '0017_alter_coursecategory_options'), + ] + + operations = [ + migrations.AddField( + model_name='course', + name='notified_new_registration', + field=models.BooleanField(default=False, help_text="Designates whether students have been notified of this course's registration.", verbose_name='New Registration Notified'), + ), + migrations.AddField( + model_name='courselivesession', + name='is_cancelled', + field=models.BooleanField(default=False, help_text='Designates whether this live session has been cancelled.', verbose_name='Is Cancelled'), + ), + migrations.AddField( + model_name='courselivesession', + name='reminder_sent', + field=models.BooleanField(default=False, help_text='Designates whether a start reminder has been sent for this session.', verbose_name='Reminder Sent'), + ), + ] diff --git a/apps/course/models/course.py b/apps/course/models/course.py index f5499ce..a812aca 100644 --- a/apps/course/models/course.py +++ b/apps/course/models/course.py @@ -375,6 +375,11 @@ class Course(models.Model): help_text=_('This field is automatically calculated based on the discount percentage.') ) + notified_new_registration = models.BooleanField( + default=False, + verbose_name=_("New Registration Notified"), + help_text=_("Designates whether students have been notified of this course's registration.") + ) is_group_chat_locked = models.BooleanField( default=False, verbose_name=_('Lock Group Chat') diff --git a/apps/course/models/live_session.py b/apps/course/models/live_session.py index da859a2..2e34f7b 100644 --- a/apps/course/models/live_session.py +++ b/apps/course/models/live_session.py @@ -112,6 +112,16 @@ class CourseLiveSession(models.Model): null=True, blank=True, ) + is_cancelled = models.BooleanField( + default=False, + verbose_name=_("Is Cancelled"), + help_text=_("Designates whether this live session has been cancelled.") + ) + reminder_sent = models.BooleanField( + default=False, + verbose_name=_("Reminder Sent"), + help_text=_("Designates whether a start reminder has been sent for this session.") + ) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At")) updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At")) diff --git a/apps/course/signals.py b/apps/course/signals.py index 401abac..3730f52 100644 --- a/apps/course/signals.py +++ b/apps/course/signals.py @@ -1,8 +1,9 @@ import logging from django.utils import timezone -from apps.course.models import Course +from apps.course.models import Course, Participant, LessonCompletion, CourseLiveSession +from apps.account.notification_service import create_and_send_notification from apps.chat.models import RoomMessage -from apps.course.models.course import Course, extract_text_from_json +from apps.course.models.course import Course, extract_text_from_json, get_localized_field from apps.course.models.lesson import CourseLesson from apps.course.models.live_session import LiveSessionRecording from django.db.models import Q @@ -303,3 +304,87 @@ def create_course_lesson_from_recording(sender, instance, **kwargs): ) logger.info(f"✨ [Signal] Automatically created CourseLesson {course_lesson.id} ({lesson_title}) for course {course.id} from recording {instance.id}") + + +@receiver(pre_save, sender=Participant) +def store_participant_previous_active(sender, instance, **kwargs): + if instance.pk: + try: + old = Participant.objects.get(pk=instance.pk) + instance._previous_is_active = old.is_active + except Participant.DoesNotExist: + instance._previous_is_active = None + else: + instance._previous_is_active = None + + +@receiver(post_save, sender=Participant) +def notify_course_access_granted(sender, instance, created, **kwargs): + if instance.is_active: + was_activated = False + if created: + was_activated = True + elif hasattr(instance, '_previous_is_active') and instance._previous_is_active is False: + was_activated = True + + if was_activated: + course_title_en = get_localized_field('en', instance.course.title) + course_title_fa = get_localized_field('fa', instance.course.title) + create_and_send_notification( + user=instance.student, + title_en="Course Access Granted", + body_en=f"Your access to the course '{course_title_en}' has been activated.", + title_fa="دسترسی به دوره فعال شد", + body_fa=f"دسترسی شما به دوره «{course_title_fa}» فعال شد.", + service='imam-javad', + data={'type': 'course_access_granted', 'course_id': instance.course.id} + ) + + +@receiver(post_save, sender=LessonCompletion) +def check_course_completion(sender, instance, created, **kwargs): + if created: + student = instance.student + course_lesson = instance.course_lesson + course = course_lesson.course + + if not course: + return + + total_lessons_count = CourseLesson.objects.filter(course=course, is_active=True).count() + if total_lessons_count > 0: + completed_lessons_count = LessonCompletion.objects.filter( + student=student, + course_lesson__course=course, + course_lesson__is_active=True + ).count() + + if completed_lessons_count >= total_lessons_count: + cache_key = f"notified_course_completed_{student.id}_{course.id}" + if not cache.get(cache_key): + cache.set(cache_key, True, timeout=86400 * 30) # 30 days + + course_title_en = get_localized_field('en', course.title) + course_title_fa = get_localized_field('fa', course.title) + create_and_send_notification( + user=student, + title_en="Course Completed", + body_en=f"Congratulations! You have completed the course '{course_title_en}'.", + title_fa="دوره به پایان رسید", + body_fa=f"تبریک! شما دوره «{course_title_fa}» را با موفقیت به پایان رساندید.", + service='imam-javad', + data={'type': 'course_completed', 'course_id': course.id} + ) + + +@receiver(pre_save, sender=CourseLiveSession) +def handle_live_session_reschedule(sender, instance, **kwargs): + if instance.pk: + try: + old = CourseLiveSession.objects.get(pk=instance.pk) + if old.started_at != instance.started_at: + if instance.started_at > timezone.now(): + instance.reminder_sent = False + except CourseLiveSession.DoesNotExist: + pass + diff --git a/config/settings/base.py b/config/settings/base.py index 00f228c..0cd537b 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -252,6 +252,19 @@ CELERY_TASK_SERIALIZER = 'json' CELERY_RESULT_SERIALIZER = 'json' CELERY_BROKER_CONNECTION_RETRY_ON_STARTUP = True +from celery.schedules import crontab + +CELERY_BEAT_SCHEDULE = { + 'check_live_class_reminders_every_10_mins': { + 'task': 'apps.account.tasks.check_live_class_reminders_task', + 'schedule': crontab(minute='*/10'), + }, + 'check_new_course_registration_every_friday': { + 'task': 'apps.account.tasks.check_new_course_registration_task', + 'schedule': crontab(minute=0, hour=12, day_of_week='friday'), + }, +} + # Password validation # https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators diff --git a/config/settings/production.py b/config/settings/production.py index 698f447..e9e5d82 100644 --- a/config/settings/production.py +++ b/config/settings/production.py @@ -27,6 +27,14 @@ CELERY_BEAT_SCHEDULE = { 'task': 'apps.tasrif.tasks.crawler_website_bonbast_rate_usd', 'schedule': crontab(minute=0, hour='*/1'), # اجرای هر ساعت یک‌بار }, + 'check_live_class_reminders_every_10_mins': { + 'task': 'apps.account.tasks.check_live_class_reminders_task', + 'schedule': crontab(minute='*/10'), + }, + 'check_new_course_registration_every_friday': { + 'task': 'apps.account.tasks.check_new_course_registration_task', + 'schedule': crontab(minute=0, hour=12, day_of_week='friday'), + }, } # CORS_ALLOWED_ORIGINS = [