import copy import uuid from enum import Enum from django.db import models from django.db.models import UniqueConstraint from slugify import slugify from wagtail import blocks from wagtail.admin.panels import FieldPanel, PageChooserPanel from wagtail.fields import RichTextField, StreamField from wagtail.models import Page from vbv_lernwelt.core.constants import ( DEFAULT_RICH_TEXT_FEATURES, DEFAULT_RICH_TEXT_FEATURES_WITH_HEADER, ) from vbv_lernwelt.core.model_utils import find_available_slug from vbv_lernwelt.core.models import User from vbv_lernwelt.course.models import CourseBasePage from vbv_lernwelt.files.models import UploadFile class AssignmentListPage(CourseBasePage): subpage_types = ["assignment.Assignment"] parent_page_types = ["course.CoursePage"] def save(self, clean=True, user=None, log_action=False, **kwargs): self.slug = find_available_slug( slugify(f"{self.get_parent().slug}-assignment", allow_unicode=True), ignore_page_id=self.id, ) super(AssignmentListPage, self).save(clean, user, log_action, **kwargs) def __str__(self): return f"{self.title}" class ExplanationBlock(blocks.StructBlock): text = blocks.RichTextBlock(features=DEFAULT_RICH_TEXT_FEATURES) class Meta: icon = "comment" class PerformanceObjectiveBlock(blocks.StructBlock): text = blocks.TextBlock() class Meta: icon = "tick" class UserTextInputBlock(blocks.StructBlock): text = blocks.RichTextBlock( blank=True, required=False, features=DEFAULT_RICH_TEXT_FEATURES ) class Meta: icon = "edit" class UserConfirmationBlock(blocks.StructBlock): text = blocks.RichTextBlock(features=DEFAULT_RICH_TEXT_FEATURES) class Meta: icon = "tick-inverse" class TaskContentStreamBlock(blocks.StreamBlock): explanation = ExplanationBlock() user_text_input = UserTextInputBlock() user_confirmation = UserConfirmationBlock() class TaskBlock(blocks.StructBlock): title = blocks.TextBlock() file_submission_required = blocks.BooleanBlock(required=False) content = TaskContentStreamBlock( blank=True, ) class Meta: icon = "tasks" label = "Teilauftrag" class EvaluationSubTaskBlock(blocks.StructBlock): title = blocks.TextBlock() description = blocks.RichTextBlock( blank=True, required=False, features=DEFAULT_RICH_TEXT_FEATURES ) points = blocks.IntegerBlock() class Meta: icon = "tick" label = "Beurteilung" class EvaluationTaskBlock(blocks.StructBlock): title = blocks.TextBlock() description = blocks.RichTextBlock( blank=True, required=False, features=DEFAULT_RICH_TEXT_FEATURES ) max_points = blocks.IntegerBlock() sub_tasks = blocks.ListBlock( EvaluationSubTaskBlock(), blank=True, use_json_field=True ) class Meta: icon = "tasks" label = "Beurteilungskriterium" class AssignmentType(Enum): CASEWORK = "CASEWORK" # Geleitete Fallarbeit PREP_ASSIGNMENT = "PREP_ASSIGNMENT" # Vorbereitungsauftrag REFLECTION = "REFLECTION" # Reflexion CONDITION_ACCEPTANCE = "CONDITION_ACCEPTANCE" # Bedingungsannahme EDONIQ_TEST = "EDONIQ_TEST" # EdonIQ Test class Assignment(CourseBasePage): serialize_field_names = [ "intro_text", "effort_required", "performance_objectives", "evaluation_description", "evaluation_document_url", "tasks", "evaluation_tasks", ] def get_admin_display_title(self): circle_title = self.get_attached_circle_title() if circle_title: return f"{circle_title}: {self.title}" return self.title assignment_type = models.CharField( max_length=50, choices=[(tag.value, tag.value) for tag in AssignmentType], default=AssignmentType.CASEWORK.value, ) needs_expert_evaluation = models.BooleanField( default=False, help_text="Muss der Auftrag durch eine Expertin oder einen Experten beurteilt werden?", ) competence_certificate = models.ForeignKey( "competence.CompetenceCertificate", null=True, blank=True, on_delete=models.SET_NULL, ) intro_text = RichTextField( help_text="Erläuterung der Ausgangslage", features=DEFAULT_RICH_TEXT_FEATURES_WITH_HEADER, ) effort_required = models.CharField( max_length=100, help_text="Zeitaufwand als Text", blank=True ) performance_objectives = StreamField( [ ("performance_objective", PerformanceObjectiveBlock()), ], use_json_field=True, blank=True, help_text="Leistungsziele des Auftrags", ) tasks = StreamField( [ ("task", TaskBlock()), ], use_json_field=True, blank=True, help_text="Teilaufgaben", ) evaluation_description = RichTextField( blank=True, help_text="Beschreibung der Bewertung", features=DEFAULT_RICH_TEXT_FEATURES, ) evaluation_document_url = models.CharField( max_length=255, blank=True, help_text="URL zum Beurteilungsinstrument", ) evaluation_tasks = StreamField( [ ("task", EvaluationTaskBlock()), ], use_json_field=True, blank=True, help_text="Beurteilungsschritte", ) content_panels = Page.content_panels + [ FieldPanel("assignment_type"), FieldPanel("needs_expert_evaluation"), PageChooserPanel("competence_certificate", "competence.CompetenceCertificate"), FieldPanel("intro_text"), FieldPanel("effort_required"), FieldPanel("performance_objectives"), FieldPanel("tasks"), FieldPanel("evaluation_description"), FieldPanel("evaluation_document_url"), FieldPanel("evaluation_tasks"), ] subpage_types = [] class Meta: verbose_name = "Auftrag" def save(self, clean=True, user=None, log_action=False, **kwargs): self.slug = find_available_slug( slugify(f"{self.get_parent().slug}-{self.title}", allow_unicode=True), ignore_page_id=self.id, ) super(Assignment, self).save(clean, user, log_action, **kwargs) def filter_user_subtasks(self, subtask_types=None): """ Filters out all the subtasks which require user input :param subtask_types: :return: list of subtasks with the shape: [ { "id": "", "type": "user_confirmation", "value": { "text": "Ja, ich habe Motorfahrzeugversicherungspolice..." } }, { "id": "", "type": "user_text_input", "value": { "text": "Gibt es zusätzliche Deckungen, die du der Person empfehlen..." }, ] """ if subtask_types is None: subtask_types = ["user_text_input", "user_confirmation"] raw_tasks = self.tasks.raw_data return [ sub_dict for task_dict in raw_tasks for sub_dict in task_dict["value"]["content"] if sub_dict["type"] in subtask_types ] def get_evaluation_tasks(self): return [task for task in self.evaluation_tasks.raw_data] def get_input_tasks(self): return self.filter_user_subtasks() + self.get_evaluation_tasks() def get_max_points(self): return sum( [task["value"].get("max_points", 0) for task in self.get_evaluation_tasks()] ) def find_attached_learning_content(self): """ Returns the first learning content page attached to this assignment """ page = self.learningcontentassignment_set.first() if page: return page.specific page = self.learningcontentedoniqtest_set.first() if page: return page.specific return None def get_frontend_url(self): lp = self.find_attached_learning_content() if lp: return lp.get_frontend_url() return "" def get_attached_circle_title(self): if self.learningcontentassignment_set.count() > 1: # probably "Reflexion" which is attached to multiple circles return "" lp = self.find_attached_learning_content() if lp and lp.get_parent_circle(): return lp.get_parent_circle().title return "" class AssignmentCompletionStatus(Enum): IN_PROGRESS = "IN_PROGRESS" SUBMITTED = "SUBMITTED" EVALUATION_IN_PROGRESS = "EVALUATION_IN_PROGRESS" EVALUATION_SUBMITTED = "EVALUATION_SUBMITTED" def is_valid_assignment_completion_status( completion_status: AssignmentCompletionStatus, ): return completion_status.value in AssignmentCompletionStatus.__members__ class AssignmentCompletion(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) submitted_at = models.DateTimeField(null=True, blank=True) evaluation_submitted_at = models.DateTimeField(null=True, blank=True) evaluation_user = models.ForeignKey( User, on_delete=models.CASCADE, null=True, blank=True, related_name="+", ) evaluation_points = models.FloatField(null=True, blank=True) evaluation_max_points = models.FloatField(null=True, blank=True) evaluation_passed = models.BooleanField(null=True, blank=True) edoniq_extended_time_flag = models.BooleanField(default=False) assignment_user = models.ForeignKey(User, on_delete=models.CASCADE) assignment = models.ForeignKey(Assignment, on_delete=models.CASCADE) course_session = models.ForeignKey("course.CourseSession", on_delete=models.CASCADE) learning_content_page = models.ForeignKey( Page, on_delete=models.SET_NULL, null=True, blank=True, default=None, related_name="+", help_text="Page reference mostly needed for 'REFLECTION' assignments", ) completion_status = models.CharField( max_length=255, choices=[(acs.value, acs.value) for acs in AssignmentCompletionStatus], default=AssignmentCompletionStatus.IN_PROGRESS.value, ) completion_data = models.JSONField(default=dict) additional_json_data = models.JSONField(default=dict, blank=True) class Meta: constraints = [ UniqueConstraint( fields=[ "assignment_user", "assignment", "course_session", "learning_content_page", ], name="assignment_completion_unique_user_assignment_course_session", ) ] def get_assignment_evaluation_frontend_url(self): """ Used by the expert to evaluate the assignment Example: /course/überbetriebliche-kurse/cockpit/assignment/371/18 """ return f"{self.course_session.course.get_cockpit_url()}/assignment/{self.assignment.id}/{self.assignment_user.id}" @property def task_completion_data(self): data = {} for task in self.assignment.tasks: data[task.id] = get_task_data(task, self.completion_data) return data def get_file_info(file_id): file_info = UploadFile.objects.filter(id=file_id).first() if file_info: return { "id": str(file_info.id), "name": file_info.original_file_name, "url": file_info.url, } def get_task_data(task, completion_data): task_data = copy.deepcopy(completion_data.get(task.id, {})) user_data = task_data.get("user_data", {}) file_id = user_data.get("fileId") if file_id: user_data["fileInfo"] = get_file_info(file_id) return task_data class AssignmentCompletionAuditLog(models.Model): """ This model is used to store the "SUBMITTED" and "EVALUATION_SUBMITTED" data separately """ created_at = models.DateTimeField(auto_now_add=True) evaluation_user = models.ForeignKey( User, on_delete=models.SET_NULL, null=True, blank=True, related_name="+" ) assignment_user = models.ForeignKey( User, on_delete=models.SET_NULL, null=True, related_name="+" ) assignment = models.ForeignKey( Assignment, on_delete=models.SET_NULL, null=True, related_name="+" ) course_session = models.ForeignKey( "course.CourseSession", on_delete=models.SET_NULL, null=True, related_name="+" ) completion_status = models.CharField( max_length=255, choices=[(acs.value, acs.name) for acs in AssignmentCompletionStatus], default="IN_PROGRESS", ) completion_data = models.JSONField(default=dict) additional_json_data = models.JSONField(default=dict) assignment_user_email = models.CharField(max_length=255) assignment_slug = models.CharField(max_length=255) evaluation_user_email = models.CharField(max_length=255, blank=True, default="") evaluation_points = models.FloatField(null=True, blank=True) evaluation_max_points = models.FloatField(null=True, blank=True) evaluation_passed = models.BooleanField(null=True, blank=True)