450 lines
14 KiB
Python
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)
|