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.quiz.models import Quiz, QuizParticipant from apps.account.tasks import check_low_quiz_results_task class QuizNotificationFlowTests(TestCase): def setUp(self): # Create languages self.lang_ru, _ = Language.objects.update_or_create( code='ru', defaults={'name': 'Russian', '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_quiz@example.com', username='student_quiz', fullname='Quiz Student', language=self.lang_ru, fcm='token_quiz_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": "Курс 1", "language_code": "ru"} ], 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, ) # Create Quiz self.quiz = Quiz.objects.create( course=self.course, title=[ {"title": "Quiz A", "language_code": "en"}, {"title": "Тест А", "language_code": "ru"} ], each_question_timing=60, status=True ) @patch('apps.account.notification_service.send_notification') def test_low_quiz_result_triggers_on_intervals(self, mock_send): # Event: Low Quiz Result (score < 70) # Create failed attempt exactly 2 days ago attempt_2_days = QuizParticipant.objects.create( quiz=self.quiz, user=self.student, started_at=timezone.now() - datetime.timedelta(days=2, hours=1), ended_at=timezone.now() - datetime.timedelta(days=2), total_timing=300, question_score=50, timing_score=0, total_score=50 ) # Run periodic task check_low_quiz_results_task() # Verify reminder sent to the student (Russian template check) notif = Notification.objects.filter(user=self.student, title="Предложение пересдать тест").first() self.assertIsNotNone(notif) self.assertIn("Тест А", notif.message) self.assertTrue(mock_send.called) @patch('apps.account.notification_service.send_notification') def test_no_reminder_if_passed_initially(self, mock_send): # Create successful attempt exactly 2 days ago (score = 80) QuizParticipant.objects.create( quiz=self.quiz, user=self.student, started_at=timezone.now() - datetime.timedelta(days=2, hours=1), ended_at=timezone.now() - datetime.timedelta(days=2), total_timing=300, question_score=80, timing_score=0, total_score=80 ) check_low_quiz_results_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_no_reminder_if_retaken_and_passed(self, mock_send): # Create failed attempt exactly 7 days ago attempt_failed = QuizParticipant.objects.create( quiz=self.quiz, user=self.student, started_at=timezone.now() - datetime.timedelta(days=7, hours=1), ended_at=timezone.now() - datetime.timedelta(days=7), total_timing=300, question_score=40, timing_score=0, total_score=40 ) # Create a newer successful attempt 3 days ago QuizParticipant.objects.create( quiz=self.quiz, user=self.student, started_at=timezone.now() - datetime.timedelta(days=3, hours=1), ended_at=timezone.now() - datetime.timedelta(days=3), total_timing=300, question_score=90, timing_score=0, total_score=90 ) check_low_quiz_results_task() # Should not suggest retake since they passed subsequently self.assertFalse(Notification.objects.filter(user=self.student, title="Предложение пересдать тест").exists()) @patch('apps.account.notification_service.send_notification') def test_no_reminder_if_already_retaken_even_if_failed(self, mock_send): # Create failed attempt exactly 21 days ago attempt_failed = QuizParticipant.objects.create( quiz=self.quiz, user=self.student, started_at=timezone.now() - datetime.timedelta(days=21, hours=1), ended_at=timezone.now() - datetime.timedelta(days=21), total_timing=300, question_score=30, timing_score=0, total_score=30 ) # Create a newer attempt 10 days ago (also failed, e.g. score = 50) # Since they already attempted a retake 10 days ago, we shouldn't trigger the 21-day reminder of the first failure. QuizParticipant.objects.create( quiz=self.quiz, user=self.student, started_at=timezone.now() - datetime.timedelta(days=10, hours=1), ended_at=timezone.now() - datetime.timedelta(days=10), total_timing=300, question_score=50, timing_score=0, total_score=50 ) check_low_quiz_results_task() self.assertFalse(Notification.objects.filter(user=self.student, title="Предложение пересдать тест").exists())