import os from django.db import models from django.utils.translation import gettext_lazy as _ from filer.fields.image import FilerImageField from filer.fields.file import FilerFileField from apps.account.models import StudentUser from apps.course.models.course import extract_text_from_json, format_multilingual_field, get_localized_field def lesson_file_upload_to(instance, filename): return os.path.join(f"lessons/{filename}") class Lesson(models.Model): """ Base Lesson model that contains the actual content (video file or link) """ class ContentTypeChoices(models.TextChoices): YOUTUBE_LINK = 'youtube_link', _('Youtube Link') VIDEO_FILE = 'video_file', _('Video File') title = models.JSONField(default=list, null=False, blank=False, verbose_name=_('Lesson Title')) content_type = models.JSONField(default=list, null=False, blank=False, verbose_name=_('Content Type')) content_file = models.FileField( null=True, blank=True, upload_to=lesson_file_upload_to, ) video_link = models.CharField(max_length=500, null=True, blank=True, verbose_name=_('Link')) duration = models.PositiveIntegerField(verbose_name=_('Duration (in minutes)')) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created at")) updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At")) def __str__(self): return extract_text_from_json(self.title) def save(self, *args, **kwargs): self.title = format_multilingual_field(self.title) self.content_type = format_multilingual_field(self.content_type) super().save(*args, **kwargs) class Meta: verbose_name = _("Lesson") verbose_name_plural = _("Lessons") indexes = [ models.Index(fields=['created_at']), ] class CourseChapter(models.Model): """ V2 Grouping Layer: Groups lessons together into chapters/sections. """ course = models.ForeignKey("course.Course", on_delete=models.CASCADE, related_name='chapters', verbose_name=_('Course')) title = models.JSONField(default=list, null=False, blank=False, verbose_name=_('Chapter Title')) priority = models.IntegerField(null=True, blank=True, verbose_name=_('Priority')) is_active = models.BooleanField(default=True, verbose_name=_('Is Active')) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) def __str__(self): return f"{extract_text_from_json(self.course.title)} - {extract_text_from_json(self.title)}" def save(self, *args, **kwargs): self.title = format_multilingual_field(self.title) if self.priority is None: max_priority = self.course.chapters.aggregate(max_p=models.Max('priority'))['max_p'] self.priority = (max_priority or 0) + 1 else: self._adjust_priorities() super().save(*args, **kwargs) def _adjust_priorities(self): chapters = self.course.chapters.exclude(pk=self.pk) chapters.filter(priority__gte=self.priority).update(priority=models.F('priority') + 1) class Meta: verbose_name = _("Course Chapter") verbose_name_plural = _("Course Chapters") ordering = ['priority'] class CourseLesson(models.Model): """ Intermediate model that connects a Chapter with a Lesson (the actual context) """ course = models.ForeignKey("course.Course", on_delete=models.CASCADE, related_name='lessons', verbose_name=_('Course')) chapter = models.ForeignKey(CourseChapter, on_delete=models.CASCADE, related_name='lessons', verbose_name=_('Chapter') , null=True , blank=True) lesson = models.ForeignKey(Lesson, on_delete=models.CASCADE, related_name='course_lessons', verbose_name=_('Lesson')) title = models.JSONField(default=list, null=True, blank=True, verbose_name=_('Sub-lesson Title')) priority = models.IntegerField(null=True, blank=True, verbose_name=_('Priority')) is_active = models.BooleanField(default=True, verbose_name=_('Is Active')) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created at")) updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At")) def __str__(self): title = extract_text_from_json(self.title) or extract_text_from_json(self.lesson.title) return f"{extract_text_from_json(self.chapter.title)} - {title}" def is_completed_by(self, student): return self.completions.filter(student=student).exists() @property def content_type(self): return self.lesson.content_type @property def content_file(self): return self.lesson.content_file @property def video_link(self): return self.lesson.video_link @property def duration(self): return self.lesson.duration def save(self, *args, **kwargs): # 👇 3. Auto-sync v1 and v2 relations! if self.chapter and getattr(self, 'course_id', None) != self.chapter.course_id: self.course = self.chapter.course if not self.title or self.title == []: self.title = self.lesson.title self.title = format_multilingual_field(self.title) if self.priority is None: max_priority = self.course.lessons.aggregate(max_priority=models.Max('priority'))['max_priority'] self.priority = (max_priority or 0) + 1 else: self._adjust_priorities() super().save(*args, **kwargs) def _adjust_priorities(self): lessons = self.chapter.lessons.exclude(pk=self.pk) lessons.filter(priority__gte=self.priority).update(priority=models.F('priority') + 1) class Meta: verbose_name = _("Course Lesson") verbose_name_plural = _("Course Lessons") ordering = ['priority'] indexes = [ models.Index(fields=['chapter']), models.Index(fields=['lesson']), models.Index(fields=['priority']), models.Index(fields=['is_active']), ] class LessonCompletion(models.Model): student = models.ForeignKey( StudentUser, on_delete=models.CASCADE, related_name='lesson_completions', verbose_name=_('Student') ) course_lesson = models.ForeignKey( CourseLesson, on_delete=models.CASCADE, related_name='completions', verbose_name=_('Course Lesson') ) completed_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created at")) class Meta: unique_together = ('student', 'course_lesson') verbose_name = _("Lesson Completion") verbose_name_plural = _("Lesson Completions") indexes = [ models.Index(fields=['student']), models.Index(fields=['course_lesson']), models.Index(fields=['completed_at']), models.Index(fields=['student', 'course_lesson']), ] def __str__(self): return f"{self.student.fullname} - {self.course_lesson.title} - Completed"