vbv/server/vbv_lernwelt/learnpath/models.py

530 lines
17 KiB
Python

import re
from enum import Enum
from typing import Tuple
from django.db import models
from django.utils.text import slugify
from wagtail.admin.panels import FieldPanel, HelpPanel, 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 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)
content_panels = Page.content_panels + [
FieldPanel("description"),
FieldPanel("goals"),
]
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 LearningUnitPerformanceFeedbackType(Enum):
"""Defines how feedback on the performance criteria (n) of a learning unit are given."""
NO_FEEDBACK = "NO_FEEDBACK"
MENTOR_FEEDBACK = "MENTOR_FEEDBACK"
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"""
<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)
feedback_user = models.CharField(
max_length=255,
choices=[(tag.name, tag.name) for tag in LearningUnitPerformanceFeedbackType],
default=LearningUnitPerformanceFeedbackType.NO_FEEDBACK.name,
)
content_panels = Page.content_panels + [
FieldPanel("course_category"),
FieldPanel("title_hidden"),
FieldPanel("feedback_user"),
HelpPanel(
content="👆 Feedback zur Selbsteinschätzung: Normalerweise <code>NO_FEEDBACK</code>, "
"ausser bei den Lerninhalten Selbsteinschätzungen, die eine Bewertung haben von einer "
"Lernbegleitung haben sollen (z.B. VV)."
),
]
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,
)