Browse Source

lessons count enhanced and synced with lesson model.

master
Mohsen Taba 2 months ago
parent
commit
fc332484f5
  1. 42
      apps/course/management/commands/sync_course_lessons_count.py
  2. 17
      apps/course/models/course.py
  3. 4
      apps/course/models/lesson.py
  4. 47
      apps/course/signals.py
  5. 174
      apps/course/tests/test_lessons_count_sync.py

42
apps/course/management/commands/sync_course_lessons_count.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."
)
)

17
apps/course/models/course.py

@ -128,6 +128,23 @@ class Course(models.Model):
def is_student_participant(self, student):
return self.participants.filter(student=student).exists()
@classmethod
def recalculate_lessons_count_for_course(cls, course_id):
if not course_id:
return 0
from apps.course.models.lesson import CourseLesson
lessons_count = CourseLesson.objects.filter(
course_id=course_id,
is_active=True,
).count()
cls.objects.filter(pk=course_id).update(lessons_count=lessons_count)
return lessons_count
def recalculate_lessons_count(self):
return self.__class__.recalculate_lessons_count_for_course(self.pk)
def save(self, *args, **kwargs):
if not self.slug:

4
apps/course/models/lesson.py

@ -117,7 +117,7 @@ class CourseLesson(models.Model):
def save(self, *args, **kwargs):
# 👇 3. Auto-sync v1 and v2 relations!
if self.chapter and not getattr(self, 'course_id', None):
if self.chapter and getattr(self, 'course_id', None) != self.chapter.course_id:
self.course = self.chapter.course
if not self.title:
@ -177,4 +177,4 @@ class LessonCompletion(models.Model):
def __str__(self):
return f"{self.student.fullname} - {self.course_lesson.title} - Completed"

47
apps/course/signals.py

@ -1,8 +1,10 @@
from apps.course.models import Course
from apps.chat.models import RoomMessage
from apps.course.models.course import Course
from apps.course.models.lesson import CourseLesson
from django.db.models import Q
from django.db.models.signals import post_save, post_delete
from django.db.models.signals import post_save, post_delete, pre_save
from django.dispatch import receiver
from django.core.cache import cache
from django.contrib.auth import get_user_model
@ -79,4 +81,45 @@ def sync_course_chat_locks(sender, instance, **kwargs):
# Find rooms where initiator is prof and recipient is student, OR vice versa
Q(initiator_id=instance.professor_id, recipient_id__in=student_ids) |
Q(initiator_id__in=student_ids, recipient_id=instance.professor_id)
).update(is_locked=instance.is_professor_chat_locked)
).update(is_locked=instance.is_professor_chat_locked)
def _recalculate_course_lessons_count(course_id):
Course.recalculate_lessons_count_for_course(course_id)
@receiver(pre_save, sender=CourseLesson)
def capture_previous_course_lesson_state(sender, instance, **kwargs):
if not instance.pk:
instance._previous_course_id = None
instance._previous_is_active = None
return
previous = sender.objects.filter(pk=instance.pk).values('course_id', 'is_active').first()
if previous:
instance._previous_course_id = previous['course_id']
instance._previous_is_active = previous['is_active']
else:
instance._previous_course_id = None
instance._previous_is_active = None
@receiver(post_save, sender=CourseLesson)
def sync_lessons_count_on_course_lesson_save(sender, instance, **kwargs):
affected_course_ids = {instance.course_id}
previous_course_id = getattr(instance, '_previous_course_id', None)
previous_is_active = getattr(instance, '_previous_is_active', None)
if previous_course_id and previous_course_id != instance.course_id:
affected_course_ids.add(previous_course_id)
if previous_is_active is not None and previous_is_active != instance.is_active and previous_course_id:
affected_course_ids.add(previous_course_id)
for course_id in affected_course_ids:
_recalculate_course_lessons_count(course_id)
@receiver(post_delete, sender=CourseLesson)
def sync_lessons_count_on_course_lesson_delete(sender, instance, **kwargs):
_recalculate_course_lessons_count(instance.course_id)

174
apps/course/tests/test_lessons_count_sync.py

@ -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())
Loading…
Cancel
Save