5 changed files with 280 additions and 4 deletions
-
42apps/course/management/commands/sync_course_lessons_count.py
-
17apps/course/models/course.py
-
2apps/course/models/lesson.py
-
45apps/course/signals.py
-
174apps/course/tests/test_lessons_count_sync.py
@ -0,0 +1,42 @@ |
|||||
|
from django.core.management.base import BaseCommand |
||||
|
|
||||
|
from apps.course.models.course import Course |
||||
|
|
||||
|
|
||||
|
class Command(BaseCommand): |
||||
|
help = "Recalculate lessons_count for all courses based on active assigned course lessons." |
||||
|
|
||||
|
def add_arguments(self, parser): |
||||
|
parser.add_argument( |
||||
|
'--dry-run', |
||||
|
action='store_true', |
||||
|
help='Preview updates without saving changes.', |
||||
|
) |
||||
|
|
||||
|
def handle(self, *args, **options): |
||||
|
dry_run = options['dry_run'] |
||||
|
updated_courses = 0 |
||||
|
|
||||
|
for course in Course.objects.all().order_by('id'): |
||||
|
actual_count = course.lessons.filter(is_active=True).count() |
||||
|
|
||||
|
if course.lessons_count != actual_count: |
||||
|
updated_courses += 1 |
||||
|
self.stdout.write( |
||||
|
f"Course {course.id} ({course.slug}): {course.lessons_count} -> {actual_count}" |
||||
|
) |
||||
|
if not dry_run: |
||||
|
Course.objects.filter(pk=course.pk).update(lessons_count=actual_count) |
||||
|
|
||||
|
if dry_run: |
||||
|
self.stdout.write( |
||||
|
self.style.WARNING( |
||||
|
f"Dry run complete. {updated_courses} course(s) would be updated." |
||||
|
) |
||||
|
) |
||||
|
else: |
||||
|
self.stdout.write( |
||||
|
self.style.SUCCESS( |
||||
|
f"Sync complete. {updated_courses} course(s) updated." |
||||
|
) |
||||
|
) |
||||
@ -0,0 +1,174 @@ |
|||||
|
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()) |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue