544 lines
17 KiB
Python
544 lines
17 KiB
Python
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)
|
|
order = models.IntegerField(default=999)
|
|
|
|
def __str__(self) -> str:
|
|
return self.code
|
|
|
|
class Meta:
|
|
ordering = [
|
|
"order",
|
|
]
|
|
|
|
|
|
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<coursePart>.+?)-lp-circle-(?P<circlePart>.+?)$")
|
|
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_html(self):
|
|
return f"""
|
|
<span style="display: inline-flex; align-items: center; font-size: 1.25rem; font-weight: 700;">
|
|
<{self.icon} style="height: 32px; width: 32px;"></{self.icon}>
|
|
<span style="margin-left: 8px;">{self.draft_title}</span>
|
|
</span>"""
|
|
|
|
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<coursePart>.+?)-lp-circle-(?P<circlePart>.+?)-ls-(?P<lsPart>.+?)$"
|
|
)
|
|
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<coursePart>.+?)-lp-circle-(?P<circlePart>.+?)-lu-(?P<luPart>.+?)$"
|
|
)
|
|
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'<span style="font-weight: 700; font-size: 20px;">{self.draft_title}</span>'
|
|
|
|
|
|
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"""
|
|
<span style="display: inline-flex; align-items: center;">
|
|
<it-icon-checkbox-unchecked style="height: 24px; width: 24px;"></it-icon-checkbox-unchecked>
|
|
<span style="margin-left: 8px;">{self.get_admin_display_title()}</span>
|
|
</span>"""
|
|
|
|
def get_frontend_url(self, course_session_id=None):
|
|
r = re.compile(
|
|
r"^(?P<coursePart>.+?)-lp-circle-(?P<circlePart>.+?)-lc-(?P<lcPart>.+)$"
|
|
)
|
|
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,
|
|
)
|