import uuid from datetime import datetime 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 from vbv_lernwelt.shop.models import CheckoutState 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"] @property def paid_datetime(self) -> datetime | None: """ Returns the datetime when the user paid for the course session """ checkout = self.user.checkout_informations.filter( state=CheckoutState.PAID ).order_by("created_at") if checkout: return checkout.first().created_at return None 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}'"