import re from typing import Tuple from django.db import models from django.utils.text import slugify from modelcluster.models import ParentalManyToManyField from wagtail.admin.panels import FieldPanel, PageChooserPanel from wagtail.fields import RichTextField, StreamField from wagtail.models import Page from vbv_lernwelt.assignment.models import AssignmentType from vbv_lernwelt.core.constants import DEFAULT_RICH_TEXT_FEATURES_WITH_HEADER from vbv_lernwelt.core.model_utils import find_available_slug from vbv_lernwelt.course.models import CourseBasePage, CoursePage from vbv_lernwelt.media_library.content_blocks import LearnMediaBlock class LearningPath(CourseBasePage): serialize_field_names = ["children", "course"] content_panels = Page.content_panels subpage_types = ["learnpath.Circle", "learnpath.Topic"] parent_page_types = ["course.CoursePage"] class Meta: verbose_name = "Learning Path" def save(self, clean=True, user=None, log_action=False, **kwargs): self.slug = find_available_slug( slugify(f"{self.get_parent().slug}-lp", allow_unicode=True), ignore_page_id=self.id, ) super(LearningPath, self).save(clean, user, log_action, **kwargs) def __str__(self): return f"{self.title}" def get_frontend_url(self): return f"/course/{self.slug.replace('-lp', '')}/learn" class Topic(CourseBasePage): serialize_field_names = ["is_visible"] is_visible = models.BooleanField(default=True) parent_page_types = ["learnpath.LearningPath"] panels = [ FieldPanel("title"), FieldPanel("is_visible"), ] def save(self, clean=True, user=None, log_action=False, **kwargs): self.slug = find_slug_with_parent_prefix(self, "topic") super(Topic, self).save(clean, user, log_action, **kwargs) def get_admin_display_title(self): return f"Thema: {self.draft_title}" def get_frontend_url(self): return "" class Meta: verbose_name = "Topic" def __str__(self): return f"{self.title}" class CourseProfile(models.Model): code = models.CharField(max_length=255) def __str__(self) -> str: return self.code class CourseProfileToCircle(models.Model): # this connects the course profile to a circle, because a circle can be in multiple profiles # todo: to we even need a through model? pass class Circle(CourseBasePage): parent_page_types = ["learnpath.LearningPath"] subpage_types = [ "learnpath.LearningSequence", "learnpath.LearningUnit", "learnpath.LearningContentAssignment", "learnpath.LearningContentAttendanceCourse", "learnpath.LearningContentFeedbackUK", "learnpath.LearningContentFeedbackVV", "learnpath.LearningContentLearningModule", "learnpath.LearningContentKnowledgeAssessment", "learnpath.LearningContentMediaLibrary", "learnpath.LearningContentPlaceholder", "learnpath.LearningContentRichText", "learnpath.LearningContentEdoniqTest", "learnpath.LearningContentVideo", "learnpath.LearningContentDocumentList", ] serialize_field_names = [ "children", "description", "goals", ] description = models.TextField(default="", blank=True) goals = RichTextField(features=DEFAULT_RICH_TEXT_FEATURES_WITH_HEADER) profiles = ParentalManyToManyField(CourseProfile, related_name="circles") # base circles do never belong to a course profile and should also get displayed no matter what profile is chosen is_base_circle = models.BooleanField(default=False) # profile = models.ForeignKey( # ApprovalProfile, # null=True, # blank=True, # on_delete=models.SET_NULL, # related_name="circles", # help_text="Zulassungsprofil", # ) content_panels = Page.content_panels + [ FieldPanel("description"), FieldPanel("goals"), FieldPanel("is_base_circle"), FieldPanel("profiles"), ] def get_frontend_url(self): r = re.compile(r"^(?P.+?)-lp-circle-(?P.+?)$") m = r.match(self.slug) if m is None: return "ERROR: could not parse slug" return f"/course/{m.group('coursePart')}/learn/{m.group('circlePart')}" def save(self, clean=True, user=None, log_action=False, **kwargs): self.slug = find_slug_with_parent_prefix(self, "circle") super(Circle, self).save(clean, user, log_action, **kwargs) class Meta: verbose_name = "Circle" def __str__(self): return f"{self.title}" class LearningSequence(CourseBasePage): serialize_field_names = ["icon"] parent_page_types = ["learnpath.Circle"] subpage_types = [] icon = models.CharField(max_length=255, default="it-icon-ls-start") content_panels = Page.content_panels + [ FieldPanel("icon"), ] class Meta: verbose_name = "Learning Sequence" def __str__(self): return f"{self.title}" def get_admin_display_title(self): return f"{self.icon} {self.draft_title}" def get_admin_display_title_html(self): return f""" <{self.icon} style="height: 32px; width: 32px;"> {self.draft_title} """ def get_admin_display_title(self): return f"LS: {self.draft_title}" def save(self, clean=True, user=None, log_action=False, **kwargs): self.slug = find_slug_with_parent_prefix(self, "ls") super(LearningSequence, self).save(clean, user, log_action, **kwargs) def get_frontend_url(self): r = re.compile( r"^(?P.+?)-lp-circle-(?P.+?)-ls-(?P.+?)$" ) m = r.match(self.slug) if m is None: return "ERROR: could not parse slug" return f"/course/{m.group('coursePart')}/learn/{m.group('circlePart')}#ls-{m.group('lsPart')}" class LearningUnit(CourseBasePage): parent_page_types = ["learnpath.Circle"] subpage_types = [] course_category = models.ForeignKey( "course.CourseCategory", on_delete=models.SET_NULL, null=True, blank=True ) title_hidden = models.BooleanField(default=False) content_panels = Page.content_panels + [ FieldPanel("course_category"), FieldPanel("title_hidden"), ] class Meta: verbose_name = "Learning Unit" def __str__(self): return f"{self.title}" def save(self, clean=True, user=None, log_action=False, **kwargs): course = None course_parent_page = self.get_ancestors().exact_type(CoursePage).last() if course_parent_page: course = course_parent_page.specific.course if self.course_category is None and course: self.course_category = course.coursecategory_set.filter( general=True ).first() if self.course_category.general: self.slug = find_slug_with_parent_prefix(self, "lu") else: self.slug = find_slug_with_parent_prefix( self, "lu", self.course_category.title ) super(LearningUnit, self).save(clean, user, log_action, **kwargs) def get_frontend_url_parts(self) -> Tuple[str, str, str]: """ Extracts the course, circle and learning unit part from the slug. :return: Tuple of course, circle and learning unit part """ r = re.compile( r"^(?P.+?)-lp-circle-(?P.+?)-lu-(?P.+?)$" ) m = r.match(self.slug) if m is None: ValueError(f"Could not parse slug: {self.slug}") return m.group("coursePart"), m.group("circlePart"), m.group("luPart") def get_frontend_url(self): course, circle, learning_unit = self.get_frontend_url_parts() return f"/course/{course}/learn/{circle}#lu-{learning_unit}" def get_evaluate_url(self): course, circle, learning_unit = self.get_frontend_url_parts() return f"/course/{course}/learn/{circle}/evaluate/{learning_unit}" def get_admin_display_title(self): return f"LE: {self.draft_title}" @classmethod def get_serializer_class(cls): from vbv_lernwelt.learnpath.serializers import LearningUnitSerializer return LearningUnitSerializer def get_admin_display_title_html(self): return f'{self.draft_title}' class LearningContent(CourseBasePage): class Meta: abstract = True serialize_field_names = [ "minutes", "description", "content_url", "can_user_self_toggle_course_completion", ] minutes = models.PositiveIntegerField(default=15) description = RichTextField(blank=True) content_url = models.TextField(blank=True) has_course_completion_status = models.BooleanField(default=True) can_user_self_toggle_course_completion = models.BooleanField(default=False) content_panels = [ FieldPanel("title", classname="full title"), FieldPanel("minutes"), FieldPanel("content_url"), FieldPanel("description"), FieldPanel("can_user_self_toggle_course_completion"), ] def get_admin_display_title(self): display_title = "" # if len(self.contents) > 0: # display_title += f"{self.contents[0].block_type.capitalize()}: " display_title += self.draft_title return display_title def get_admin_display_title_html(self): return f""" {self.get_admin_display_title()} """ def get_frontend_url(self, course_session_id=None): r = re.compile( r"^(?P.+?)-lp-circle-(?P.+?)-lc-(?P.+)$" ) m = r.match(self.slug) if m is None: return "ERROR: could not parse slug" url = f"/course/{m.group('coursePart')}/learn/{m.group('circlePart')}/{m.group('lcPart')}" if course_session_id: url += f"?courseSessionId={course_session_id}" return url def get_parent_circle(self): try: return self.get_ancestors().exact_type(Circle).first() except Exception: # noop pass return None def save(self, clean=True, user=None, log_action=False, **kwargs): self.slug = find_slug_with_parent_prefix(self, "lc") super().save(**kwargs) class LearningContentAttendanceCourse(LearningContent): """ Präsenzkurs """ parent_page_types = ["learnpath.Circle"] subpage_types = [] class LearningContentVideo(LearningContent): parent_page_types = ["learnpath.Circle"] subpage_types = [] can_user_self_toggle_course_completion = models.BooleanField(default=True) class LearningContentPlaceholder(LearningContent): parent_page_types = ["learnpath.Circle"] subpage_types = [] can_user_self_toggle_course_completion = models.BooleanField(default=True) class LearningContentFeedbackUK(LearningContent): parent_page_types = ["learnpath.Circle"] subpage_types = [] can_user_self_toggle_course_completion = models.BooleanField(default=False) class LearningContentFeedbackVV(LearningContent): parent_page_types = ["learnpath.Circle"] subpage_types = [] can_user_self_toggle_course_completion = models.BooleanField(default=False) class LearningContentLearningModule(LearningContent): parent_page_types = ["learnpath.Circle"] subpage_types = [] can_user_self_toggle_course_completion = models.BooleanField(default=True) class LearningContentKnowledgeAssessment(LearningContent): parent_page_types = ["learnpath.Circle"] subpage_types = [] can_user_self_toggle_course_completion = models.BooleanField(default=True) class LearningContentMediaLibrary(LearningContent): parent_page_types = ["learnpath.Circle"] subpage_types = [] can_user_self_toggle_course_completion = models.BooleanField(default=True) class LearningContentEdoniqTest(LearningContent): serialize_field_names = LearningContent.serialize_field_names + [ "checkbox_text", "has_extended_time_test", "content_assignment_id", ] parent_page_types = ["learnpath.Circle"] subpage_types = [] checkbox_text = models.TextField(blank=True) test_url = models.TextField(blank=True) extended_time_test_url = models.TextField(blank=True) # Sequenz ID von Edoniq edoniq_sequence_id = models.CharField(max_length=255, blank=True, default="") edoniq_extended_sequence_id = models.CharField( max_length=255, blank=True, default="" ) # Kursfreigaben ID von Edoniq edoniq_course_release_id = models.CharField(max_length=255, blank=True, default="") edoniq_extended_course_release_id = models.CharField( max_length=255, blank=True, default="" ) content_panels = LearningContent.content_panels + [ FieldPanel("checkbox_text", classname="Text"), FieldPanel("test_url", classname="Text"), FieldPanel("edoniq_course_release_id", classname="Text"), FieldPanel("edoniq_sequence_id", classname="Text"), FieldPanel("extended_time_test_url", classname="Text"), FieldPanel("edoniq_extended_course_release_id", classname="Text"), FieldPanel("edoniq_extended_sequence_id", classname="Text"), PageChooserPanel("content_assignment", "assignment.Assignment"), ] content_assignment = models.ForeignKey( "assignment.Assignment", on_delete=models.PROTECT, ) @property def has_extended_time_test(self): return bool(self.extended_time_test_url) @classmethod def get_serializer_class(cls): from vbv_lernwelt.learnpath.serializers import ( LearningContentEdoniqTestSerializer, ) return LearningContentEdoniqTestSerializer class LearningContentRichText(LearningContent): text = RichTextField(blank=True, features=DEFAULT_RICH_TEXT_FEATURES_WITH_HEADER) can_user_self_toggle_course_completion = models.BooleanField(default=True) parent_page_types = ["learnpath.Circle"] serialize_field_names = LearningContent.serialize_field_names + [ "text", ] subpage_types = [] content_panels = LearningContent.content_panels + [ FieldPanel("text", classname="Text"), ] class LearningContentAssignment(LearningContent): serialize_field_names = LearningContent.serialize_field_names + [ "content_assignment_id", "assignment_type", ] parent_page_types = ["learnpath.Circle"] subpage_types = [] content_assignment = models.ForeignKey( "assignment.Assignment", on_delete=models.PROTECT, ) assignment_type = models.CharField( max_length=50, choices=[(tag.name, tag.name) for tag in AssignmentType], default=AssignmentType.CASEWORK.name, ) content_panels = [ FieldPanel("title", classname="full title"), FieldPanel("minutes"), PageChooserPanel("content_assignment", "assignment.Assignment"), FieldPanel("content_url"), FieldPanel("description"), ] def save(self, clean=True, user=None, log_action=False, **kwargs): self.assignment_type = self.content_assignment.assignment_type super().save(**kwargs) def __str__(self): return f"{self.id} - {self.title}" @classmethod def get_serializer_class(cls): from vbv_lernwelt.learnpath.serializers import ( LearningContentAssignmentSerializer, ) return LearningContentAssignmentSerializer class LearningContentDocumentList(LearningContent): can_user_self_toggle_course_completion = models.BooleanField(default=True) serialize_field_names = LearningContent.serialize_field_names + [ "documents", ] parent_page_types = ["learnpath.Circle"] subpage_types = [] documents = StreamField( [ ("document", LearnMediaBlock()), ], use_json_field=True, blank=True, ) content_panels = [ FieldPanel("title", classname="full title"), FieldPanel("minutes"), FieldPanel("description"), FieldPanel("documents"), ] def find_slug_with_parent_prefix(page, type_prefix, slug_postfix=None): parent_slug = page.get_ancestors().exact_type(LearningPath, Circle).last().slug if parent_slug: slug_prefix = f"{parent_slug}-{type_prefix}" else: slug_prefix = type_prefix if slug_postfix is None: slug_postfix = page.title return find_available_slug( slugify(f"{slug_prefix}-{slug_postfix}", allow_unicode=True), ignore_page_id=page.id, )