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, 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, LiveSessionUser from apps.account.tasks import check_inactive_students_task class RetentionNotificationsFlowTests(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_ret@example.com', username='student_ret', fullname='Ret Student', language=self.lang_fa, fcm='token_ret_student' ) # Create Course thumbnail = SimpleUploadedFile('thumb.jpg', b'filecontent', content_type='image/jpeg') self.course = Course.objects.create( title=[ {"title": "Course 1", "language_code": "en"}, {"title": "دوره ۱", "language_code": "fa"} ], slug=[{"title": "course-1", "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": "ongoing", "language_code": "en"}], is_free=True, ) # Enroll student in the course self.participant = Participant.objects.create( student=self.student, course=self.course, is_active=True ) @patch('apps.account.notification_service.send_notification') def test_inactive_student_inactivity_notification(self, mock_send): # Event 1: Inactivity (Self-paced) # Setup student to be registered 8 days ago with last_login 8 days ago self.student.date_joined = timezone.now() - datetime.timedelta(days=8) self.student.last_login = timezone.now() - datetime.timedelta(days=8) self.student.save() # Run task check_inactive_students_task() # Check DB notification notif = Notification.objects.filter(user=self.student, title="دلمان برایتان تنگ شده!").first() self.assertIsNotNone(notif) self.assertTrue(mock_send.called) @patch('apps.account.notification_service.send_notification') def test_active_student_no_inactivity_notification(self, mock_send): # Date joined 8 days ago, but login 3 days ago self.student.date_joined = timezone.now() - datetime.timedelta(days=8) self.student.last_login = timezone.now() - datetime.timedelta(days=3) self.student.save() check_inactive_students_task() self.assertFalse(Notification.objects.filter(user=self.student, title="دلمان برایتان تنگ شده!").exists()) self.assertFalse(mock_send.called) @patch('apps.account.notification_service.send_notification') def test_student_with_recent_lesson_completion_no_inactivity_notification(self, mock_send): # Date joined 8 days ago, last login 8 days ago, but completed a lesson 2 days ago self.student.date_joined = timezone.now() - datetime.timedelta(days=8) self.student.last_login = timezone.now() - datetime.timedelta(days=8) self.student.save() chapter = CourseChapter.objects.create(course=self.course, title='Chapter 1', priority=1, is_active=True) l1 = Lesson.objects.create(title='Lesson 1', content_type='video_file', duration=5) cl1 = CourseLesson.objects.create(course=self.course, chapter=chapter, lesson=l1, priority=1, is_active=True) # Complete lesson 2 days ago completion = LessonCompletion.objects.create(student=self.student, course_lesson=cl1) # Manually force completed_at to be 2 days ago (since auto_now_add makes it now) LessonCompletion.objects.filter(id=completion.id).update(completed_at=timezone.now() - datetime.timedelta(days=2)) # Reset mock_send because LessonCompletion creation triggered a "Lesson Completed" notification mock_send.reset_mock() check_inactive_students_task() self.assertFalse(Notification.objects.filter(user=self.student, title="دلمان برایتان تنگ شده!").exists()) self.assertFalse(mock_send.called) @patch('apps.account.notification_service.send_notification') def test_missed_live_sessions_notification(self, mock_send): # Event 2: Missed LIVE Sessions # Create session 1 (ended) s1 = CourseLiveSession.objects.create( course=self.course, subject='Live Class One', started_at=timezone.now() - datetime.timedelta(days=3), ended_at=timezone.now() - datetime.timedelta(days=3, hours=2), is_cancelled=False, reminder_sent=True ) # Create session 2 (ended just now) s2 = CourseLiveSession.objects.create( course=self.course, subject='Live Class Two', started_at=timezone.now() - datetime.timedelta(hours=2), is_cancelled=False, reminder_sent=True ) # The student has not joined either s1 or s2 (no LiveSessionUser entries). # Set ended_at to trigger the signal s2.ended_at = timezone.now() s2.save() # Check DB notification notif = Notification.objects.filter(user=self.student, title="جای شما در کلاس زنده خالی بود!").first() self.assertIsNotNone(notif) self.assertIn("دوره ۱", notif.message) self.assertTrue(mock_send.called)