from io import StringIO from django.core.files.uploadedfile import SimpleUploadedFile from django.core.management import call_command from django.test import TestCase from apps.account.models import ProfessorUser from apps.course.models.course import Course, CourseCategory from apps.course.models.lesson import CourseChapter, CourseLesson, Lesson class LessonsCountSyncTests(TestCase): def setUp(self): self.professor = ProfessorUser.objects.create( email='prof-sync@example.com', fullname='Professor Sync', experience_years=5, ) self.category = CourseCategory.objects.create( name='Sync Category', slug='sync-category', ) def _create_course(self, slug, title, lessons_count=0): thumbnail = SimpleUploadedFile(f'{slug}.jpg', b'filecontent', content_type='image/jpeg') return Course.objects.create( title=title, slug=slug, category=self.category, professor=self.professor, 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=lessons_count, description='Description', short_description='Short description', status=Course.StatusChoices.ONGOING, is_free=True, ) def _create_chapter(self, course, title='Chapter', priority=1): return CourseChapter.objects.create( course=course, title=title, priority=priority, is_active=True, ) def _create_course_lesson(self, course, chapter=None, title='Lesson', is_active=True, priority=1): if chapter is None: chapter = self._create_chapter(course, title=f'{course.title} Chapter', priority=priority) lesson = Lesson.objects.create( title=title, content_type=Lesson.ContentTypeChoices.VIDEO_FILE, duration=5, ) return CourseLesson.objects.create( course=course, chapter=chapter, lesson=lesson, priority=priority, is_active=is_active, ) def test_creating_active_course_lesson_updates_lessons_count(self): course = self._create_course('create-active-course', 'Create Active Course') self._create_course_lesson(course, title='Lesson 1') course.refresh_from_db() self.assertEqual(course.lessons_count, 1) def test_deleting_active_course_lesson_updates_lessons_count(self): course = self._create_course('delete-active-course', 'Delete Active Course') course_lesson = self._create_course_lesson(course, title='Lesson 1') course.refresh_from_db() self.assertEqual(course.lessons_count, 1) course_lesson.delete() course.refresh_from_db() self.assertEqual(course.lessons_count, 0) def test_toggling_is_active_updates_lessons_count(self): course = self._create_course('toggle-active-course', 'Toggle Active Course') course_lesson = self._create_course_lesson(course, title='Lesson 1', is_active=False) course.refresh_from_db() self.assertEqual(course.lessons_count, 0) course_lesson.is_active = True course_lesson.save() course.refresh_from_db() self.assertEqual(course.lessons_count, 1) course_lesson.is_active = False course_lesson.save() course.refresh_from_db() self.assertEqual(course.lessons_count, 0) def test_moving_course_lesson_updates_old_and_new_course_counts(self): old_course = self._create_course('old-course', 'Old Course') new_course = self._create_course('new-course', 'New Course') old_chapter = self._create_chapter(old_course, title='Old Chapter', priority=1) new_chapter = self._create_chapter(new_course, title='New Chapter', priority=1) course_lesson = self._create_course_lesson( old_course, chapter=old_chapter, title='Lesson 1', is_active=True, priority=1, ) old_course.refresh_from_db() new_course.refresh_from_db() self.assertEqual(old_course.lessons_count, 1) self.assertEqual(new_course.lessons_count, 0) course_lesson.chapter = new_chapter course_lesson.save() old_course.refresh_from_db() new_course.refresh_from_db() self.assertEqual(old_course.lessons_count, 0) self.assertEqual(new_course.lessons_count, 1) def test_inactive_lessons_are_not_counted(self): course = self._create_course('inactive-course', 'Inactive Course') self._create_course_lesson(course, title='Lesson 1', is_active=False) self._create_course_lesson(course, title='Lesson 2', is_active=True, priority=2) course.refresh_from_db() self.assertEqual(course.lessons_count, 1) def test_sync_course_lessons_count_command_updates_mismatched_courses(self): first_course = self._create_course('sync-first-course', 'Sync First Course', lessons_count=99) second_course = self._create_course('sync-second-course', 'Sync Second Course', lessons_count=7) self._create_course_lesson(first_course, title='Lesson 1', is_active=True) self._create_course_lesson(first_course, title='Lesson 2', is_active=False, priority=2) self._create_course_lesson(second_course, title='Lesson 3', is_active=True) self._create_course_lesson(second_course, title='Lesson 4', is_active=True, priority=2) Course.objects.filter(pk=first_course.pk).update(lessons_count=99) Course.objects.filter(pk=second_course.pk).update(lessons_count=7) stdout = StringIO() call_command('sync_course_lessons_count', stdout=stdout) first_course.refresh_from_db() second_course.refresh_from_db() self.assertEqual(first_course.lessons_count, 1) self.assertEqual(second_course.lessons_count, 2) self.assertIn('Sync complete. 2 course(s) updated.', stdout.getvalue()) def test_sync_course_lessons_count_command_dry_run_does_not_persist_changes(self): course = self._create_course('sync-dry-course', 'Sync Dry Course', lessons_count=5) self._create_course_lesson(course, title='Lesson 1', is_active=True) Course.objects.filter(pk=course.pk).update(lessons_count=5) stdout = StringIO() call_command('sync_course_lessons_count', '--dry-run', stdout=stdout) course.refresh_from_db() self.assertEqual(course.lessons_count, 5) self.assertIn('Dry run complete. 1 course(s) would be updated.', stdout.getvalue())