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 ) enable_circle_documents = models.BooleanField( _("Trainer Dokumente in Circles"), default=True ) circle_contact_type = models.CharField( max_length=50, choices=[(cct.value, cct.value) for cct in CircleContactType], default=CircleContactType.EXPERT.value, ) 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 not self.id is 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 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=[(status, status.value) for status in CourseCompletionStatus], default=CourseCompletionStatus.UNKNOWN.value, ) 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 ) 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)