from django.db import models from django.db.models import UniqueConstraint from django.utils.text import slugify from django.utils.translation import gettext_lazy as _ from django_jsonform.models.fields import JSONField 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 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 ) class Meta: verbose_name = _("Lehrgang") def get_course_url(self): return f"/course/{self.slug}" def get_learning_path(self): from vbv_lernwelt.learnpath.models import LearningPath return self.coursepage.get_children().exact_type(LearningPath).first().specific def get_learning_path_url(self): return self.get_learning_path().get_frontend_url() def get_cockpit_url(self): return self.get_learning_path().get_cockpit_url() def get_competence_url(self): from vbv_lernwelt.competence.models import CompetenceProfilePage competence_page = ( self.coursepage.get_children().exact_type(CompetenceProfilePage).first() ) return competence_page.specific.get_frontend_url() 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_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}" 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 CourseCompletion(models.Model): 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_key = models.UUIDField() page_type = models.CharField(max_length=255, default="", blank=True) page_slug = 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=[ ("unknown", "unknown"), ("success", "success"), ("fail", "fail"), ], default="unknown", ) additional_json_data = models.JSONField(default=dict) class Meta: constraints = [ UniqueConstraint( fields=["user", "page_key", "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 """ ATTENDANCE_COURSES_SCHEMA = { "type": "array", "items": { "type": "object", "properties": { "learningContentId": { "type": "number", "title": "ID des Lerninhalts", "required": True, }, "start": {"type": "string", "format": "datetime"}, "end": {"type": "string", "format": "datetime"}, "location": {"type": "string"}, "trainer": {"type": "string"}, }, }, } 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) attendance_courses = JSONField( schema=ATTENDANCE_COURSES_SCHEMA, blank=True, default=list ) assignment_details_list = models.JSONField(default=list, blank=True) additional_json_data = models.JSONField(default=dict, blank=True) def __str__(self): return f"{self.title}" class CourseSessionUser(models.Model): """ Ein Benutzer der an einer CourseSession teilnimmt """ 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") TUTOR = "TUTOR", _("Lernbegleitung") 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", ) ] def to_dict(self): return { "session_id": self.course_session.id, "session_title": self.course_session.title, "user_id": self.user.id, "first_name": self.user.first_name, "last_name": self.user.last_name, "email": self.user.email, "avatar_url": self.user.avatar_url, "role": self.role, "circles": self.expert.all().values( "id", "title", "slug", "translation_key" ), } class CircleDocument(models.Model): 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 ) @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)