vbv/server/vbv_lernwelt/assignment/models.py

450 lines
14 KiB
Python

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):
PRAXIS_ASSIGNMENT = "PRAXIS_ASSIGNMENT" # Praxisauftrag
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/n Experten/in oder eine Lernbegleitung 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",
)
solution_sample = models.ForeignKey(
"media_files.ContentDocument",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="+",
help_text="Musterlösung",
)
content_panels = Page.content_panels + [
FieldPanel("assignment_type"),
FieldPanel("needs_expert_evaluation"),
PageChooserPanel("competence_certificate", "competence.CompetenceCertificate"),
FieldPanel("intro_text"),
FieldPanel("effort_required"),
FieldPanel("solution_sample"),
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": "<uuid>",
"type": "user_confirmation",
"value": {
"text": "Ja, ich habe Motorfahrzeugversicherungspolice..."
}
},
{
"id": "<uuid>",
"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)