vbv/server/vbv_lernwelt/course/models.py

359 lines
11 KiB
Python

import uuid
from enum import Enum
from django.db import models
from django.db.models import UniqueConstraint, Value
from django.db.models.functions import Replace
from django.utils.text import slugify
from django.utils.translation import gettext_lazy as _
from grapple.models import GraphQLString
from wagtail.models import Page
from vbv_lernwelt.core.model_utils import find_available_slug
from vbv_lernwelt.core.models import User
from vbv_lernwelt.course.serializer_helpers import get_course_serializer_class
from vbv_lernwelt.files.models import UploadFile
class CircleContactType(Enum):
EXPERT = "EXPERT"
LEARNING_MENTOR = "LEARNING_MENTOR"
class Course(models.Model):
title = models.CharField(_("Titel"), max_length=255)
category_name = models.CharField(
_("Kategorie-Name"), max_length=255, default="Kategorie"
)
slug = models.SlugField(
_("Slug"), max_length=255, unique=True, blank=True, allow_unicode=True
)
def get_course_url(self):
return f"/course/{self.slug}"
def get_cockpit_url(self):
return f"{self.get_course_url()}/cockpit"
def get_learning_path(self):
from vbv_lernwelt.learnpath.models import LearningPath
return self.coursepage.get_children().exact_type(LearningPath).first().specific
def get_action_competences(self):
from vbv_lernwelt.competence.models import ActionCompetence
return self.coursepage.get_descendants().exact_type(ActionCompetence).specific()
def get_media_library_url(self):
from vbv_lernwelt.media_library.models import MediaLibraryPage
media_library_page = (
self.coursepage.get_children().exact_type(MediaLibraryPage).first()
)
return media_library_page.specific.get_frontend_url()
def __str__(self):
return f"{self.title}"
class CourseCategory(models.Model):
# Die Handlungsfelder im "Versicherungsvermittler/in"
title = models.CharField(_("Titel"), max_length=255, blank=True)
course = models.ForeignKey("course.Course", on_delete=models.CASCADE)
general = models.BooleanField(_("Allgemein"), default=False)
def __str__(self):
return f"{self.course} / {self.title}"
class CourseBasePage(Page):
class Meta:
abstract = True
serialize_field_names = []
serialize_base_field_names = [
"id",
"title",
"slug",
"content_type",
"translation_key",
"frontend_url",
]
graphql_fields = [
GraphQLString(
field_name="frontend_url",
source="get_graphql_frontend_url",
)
]
def get_graphql_frontend_url(self, values):
return self.get_frontend_url()
def get_course_parent(self):
return self.get_ancestors(inclusive=True).exact_type(CoursePage).last()
def get_course(self):
course_parent_page = self.get_course_parent()
if course_parent_page:
return course_parent_page.specific.course
return None
def get_circle(self):
from vbv_lernwelt.learnpath.models import Circle
try:
return self.get_ancestors().exact_type(Circle).first()
except Exception:
# noop
pass
return None
def get_circles(self):
course_parent_page = self.get_course_parent()
if course_parent_page:
from vbv_lernwelt.learnpath.models import Circle, LearningPath
circles = (
course_parent_page.get_children()
.exact_type(LearningPath)
.first()
.get_children()
.exact_type(Circle)
)
return circles
return None
@classmethod
def get_serializer_class(cls):
return get_course_serializer_class(
cls,
field_names=cls.serialize_field_names,
base_field_names=cls.serialize_base_field_names,
)
def __str__(self):
return f"{self.title}"
def _update_descendant_slugs(self, old_slug, new_slug):
"""
this method is inspired by `_update_descendant_url_paths` from wagtail Page
"""
Page.objects.filter(path__startswith=self.path).exclude(pk=self.pk).update(
slug=Replace("slug", Value(old_slug), Value(new_slug))
)
def save(self, clean=True, user=None, log_action=False, **kwargs):
slug_changed = False
if self.id is not None:
old_record = Page.objects.get(id=self.id).specific
if old_record.slug != self.slug:
self.set_url_path(self.get_parent())
slug_changed = True
old_slug = old_record.slug
new_slug = self.slug
super().save(**kwargs)
if slug_changed:
self._update_descendant_slugs(old_slug, new_slug)
class CoursePage(CourseBasePage):
content_panels = Page.content_panels
# subpage_types = [
# "learnpath.LearningPath",
# "competence.CompetenceProfilePage",
# "media_library.MediaLibraryPage",
# "assignment.AssignmentListPage",
# ]
course = models.OneToOneField("course.Course", on_delete=models.PROTECT)
class Meta:
verbose_name = _("Lehrgang-Seite")
def save(self, *args, **kwargs):
self.slug = find_available_slug(
slugify(self.title, allow_unicode=True), ignore_page_id=self.id
)
super(CoursePage, self).save(*args, **kwargs)
def __str__(self):
return f"{self.title}"
class CourseCompletionStatus(Enum):
SUCCESS = "SUCCESS"
FAIL = "FAIL"
UNKNOWN = "UNKNOWN"
class CourseCompletionStatusChoices(models.TextChoices):
SUCCESS = CourseCompletionStatus.SUCCESS.value, "Success"
FAIL = CourseCompletionStatus.FAIL.value, "Fail"
UNKNOWN = CourseCompletionStatus.UNKNOWN.value, "Unknown"
class CourseCompletion(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
user = models.ForeignKey(User, on_delete=models.CASCADE)
# page can logically be a LearningContent or a PerformanceCriteria for now
page = models.ForeignKey(Page, on_delete=models.CASCADE)
# store for convenience and performance...
page_type = models.CharField(max_length=255, default="", blank=True)
course_session = models.ForeignKey("course.CourseSession", on_delete=models.CASCADE)
completion_status = models.CharField(
max_length=255,
choices=CourseCompletionStatusChoices.choices,
default=CourseCompletionStatus.UNKNOWN,
)
additional_json_data = models.JSONField(default=dict, blank=True)
class Meta:
constraints = [
UniqueConstraint(
fields=["user", "page", "course_session"],
name="course_completion_unique_user_page_key",
)
]
class CourseSession(models.Model):
"""
Die Durchführung eines Kurses
Benutzer die an eine CourseSession gehängt sind können diesen Lehrgang sehen
Das anhängen kann via CourseSessionUser oder "Schulklasse (TODO)" geschehen
"""
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
course = models.ForeignKey("course.Course", on_delete=models.CASCADE)
title = models.TextField(unique=True)
import_id = models.TextField(blank=True, default="")
generation = models.TextField(blank=True, default="")
region = models.TextField(blank=True, default="")
group = models.TextField(blank=True, default="")
start_date = models.DateField(null=True, blank=True)
end_date = models.DateField(null=True, blank=True)
additional_json_data = models.JSONField(default=dict, blank=True)
def __str__(self):
return f"{self.title}"
class Meta:
ordering = ["title"]
class CourseSessionUser(models.Model):
"""
Ein Benutzer der an einer Durchführung teilnimmt
"""
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
course_session = models.ForeignKey("course.CourseSession", on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.CASCADE)
class Role(models.TextChoices):
MEMBER = "MEMBER", _("Teilnehmer")
EXPERT = "EXPERT", _("Experte/Trainer")
role = models.CharField(choices=Role.choices, max_length=255, default=Role.MEMBER)
expert = models.ManyToManyField(
"learnpath.Circle", related_name="expert", blank=True
)
optional_attendance = models.BooleanField(default=False)
chosen_profile = models.ForeignKey(
"learnpath.CourseProfile", on_delete=models.SET_NULL, blank=True, null=True
)
class Meta:
constraints = [
UniqueConstraint(
fields=[
"course_session",
"user",
],
name="course_session_user_unique_course_session_user",
)
]
ordering = ["user__last_name", "user__first_name", "user__email"]
def __str__(self):
return f"{self.user} ({self.course_session.title})"
class CircleDocument(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
created_at = models.DateTimeField(auto_now_add=True)
file = models.OneToOneField(UploadFile, on_delete=models.CASCADE)
name = models.CharField(max_length=100)
course_session = models.ForeignKey("course.CourseSession", on_delete=models.CASCADE)
learning_sequence = models.ForeignKey(
"learnpath.LearningSequence", on_delete=models.CASCADE
)
def get_circle(self):
return self.learning_sequence.get_circle()
@property
def url(self) -> str:
return self.file.url
@property
def file_name(self) -> str:
return self.file.original_file_name
def delete(self, *args, **kwargs):
self.file.upload_finished_at = None
self.file.save()
return super().delete(*args, **kwargs)
class CourseConfiguration(models.Model):
course = models.OneToOneField(
Course, on_delete=models.CASCADE, related_name="configuration"
)
enable_circle_documents = models.BooleanField(
_("Dokumente im Circle ein/aus"), default=True
)
enable_learning_mentor = models.BooleanField(
_("Lernmentor-Funktion ein/aus"), default=True
)
enable_competence_certificates = models.BooleanField(
_("Kompetenzweise ein/aus"), default=True
)
is_vv = models.BooleanField(_("Versicherungsvermittler-Lehrgang"), default=False)
is_uk = models.BooleanField(_("ÜK-Lehrgang"), default=False)
def __str__(self):
return f"Course Configuration for '{self.course.title}'"