177 lines
6.6 KiB
Python
177 lines
6.6 KiB
Python
from copy import deepcopy
|
|
from typing import Type
|
|
|
|
from django.utils import timezone
|
|
from rest_framework import serializers
|
|
|
|
from vbv_lernwelt.assignment.models import (
|
|
Assignment,
|
|
AssignmentCompletion,
|
|
AssignmentCompletionAuditLog,
|
|
AssignmentCompletionStatus,
|
|
is_valid_assignment_completion_status,
|
|
)
|
|
from vbv_lernwelt.core.models import User
|
|
from vbv_lernwelt.core.utils import find_first
|
|
from vbv_lernwelt.course.models import CourseSession
|
|
|
|
|
|
def update_assignment_completion(
|
|
assignment_user: User,
|
|
assignment: Assignment,
|
|
course_session: CourseSession,
|
|
completion_data=None,
|
|
completion_status: Type[AssignmentCompletionStatus] = "IN_PROGRESS",
|
|
evaluation_user: User | None = None,
|
|
evaluation_grade: float | None = None,
|
|
evaluation_points: float | None = None,
|
|
validate_completion_status_change: bool = True,
|
|
copy_task_data: bool = False,
|
|
) -> AssignmentCompletion:
|
|
"""
|
|
:param completion_data: should have the following structure:
|
|
{
|
|
"<user_text_input:uuid>": {"user_data": {"text": "some text from user"}},
|
|
"<user_confirmation:uuid>": {"user_data": {"confirmation": true}},
|
|
}
|
|
every input field has the data stored in sub dict of the question uuid
|
|
|
|
it can also contain "trainer_input" when the trainer has entered grading data
|
|
{
|
|
"<user_text_input:uuid>": {
|
|
"expert_data": {"points": 4, "text": "Gute Antwort"}
|
|
},
|
|
}
|
|
:param copy_task_data: if true, the task data will be copied to the completion data
|
|
used for "SUBMITTED" and "EVALUATION_SUBMITTED" status, so that we don't lose the question
|
|
context
|
|
:return: AssignmentCompletion
|
|
"""
|
|
if completion_data is None:
|
|
completion_data = {}
|
|
|
|
ac, created = AssignmentCompletion.objects.get_or_create(
|
|
assignment_user_id=assignment_user.id,
|
|
assignment_id=assignment.id,
|
|
course_session_id=course_session.id,
|
|
)
|
|
|
|
if not is_valid_assignment_completion_status(completion_status):
|
|
raise serializers.ValidationError(
|
|
{"completion_status": f"Invalid completion status {completion_status}"}
|
|
)
|
|
|
|
if validate_completion_status_change:
|
|
# TODO: check time?
|
|
if completion_status == "SUBMITTED":
|
|
if ac.completion_status in [
|
|
"SUBMITTED",
|
|
"EVALUATION_IN_PROGRESS",
|
|
"EVALUATION_SUBMITTED",
|
|
]:
|
|
raise serializers.ValidationError(
|
|
{
|
|
"completion_status": f"Cannot update completion status from {ac.completion_status} to SUBMITTED"
|
|
}
|
|
)
|
|
elif completion_status == "EVALUATION_SUBMITTED":
|
|
if ac.completion_status == "EVALUATION_SUBMITTED":
|
|
raise serializers.ValidationError(
|
|
{
|
|
"completion_status": f"Cannot update completion status from {ac.completion_status} to EVALUATION_SUBMITTED"
|
|
}
|
|
)
|
|
|
|
if completion_status == "IN_PROGRESS" and ac.completion_status != "IN_PROGRESS":
|
|
raise serializers.ValidationError(
|
|
{
|
|
"completion_status": f"Cannot set completion status to IN_PROGRESS when it is {ac.completion_status}"
|
|
}
|
|
)
|
|
|
|
if completion_status in ["EVALUATION_SUBMITTED", "EVALUATION_IN_PROGRESS"]:
|
|
if evaluation_user is None:
|
|
raise serializers.ValidationError(
|
|
{
|
|
"evaluation_user": "evaluation_user is required for EVALUATION_SUBMITTED status"
|
|
}
|
|
)
|
|
ac.evaluation_user = evaluation_user
|
|
|
|
if completion_status == "EVALUATION_SUBMITTED":
|
|
if evaluation_grade is None:
|
|
raise serializers.ValidationError(
|
|
{
|
|
"evaluation_grade": "evaluation_grade is required for EVALUATION_SUBMITTED status"
|
|
}
|
|
)
|
|
if evaluation_points is None:
|
|
raise serializers.ValidationError(
|
|
{
|
|
"evaluation_points": "evaluation_points is required for EVALUATION_SUBMITTED status"
|
|
}
|
|
)
|
|
|
|
ac.evaluation_grade = evaluation_grade
|
|
ac.evaluation_points = evaluation_points
|
|
|
|
if completion_status == "SUBMITTED":
|
|
ac.submitted_at = timezone.now()
|
|
elif completion_status == "EVALUATION_SUBMITTED":
|
|
ac.evaluation_submitted_at = timezone.now()
|
|
|
|
ac.completion_status = completion_status
|
|
|
|
# TODO: make more validation of the provided input -> maybe with graphql
|
|
completion_data = _remove_unknown_entries(assignment, completion_data)
|
|
for key, value in completion_data.items():
|
|
# retain already stored data
|
|
stored_entry = ac.completion_data.get(key, {})
|
|
stored_entry.update(value)
|
|
ac.completion_data[key] = stored_entry
|
|
|
|
if copy_task_data:
|
|
# copy over the question data, so that we don't lose the context
|
|
substasks = assignment.get_input_tasks()
|
|
for key, value in ac.completion_data.items():
|
|
task_data = find_first(substasks, pred=lambda x: x["id"] == key)
|
|
if task_data:
|
|
ac.completion_data[key].update(task_data)
|
|
|
|
ac.save()
|
|
|
|
if completion_status in ["EVALUATION_SUBMITTED", "SUBMITTED"]:
|
|
acl = AssignmentCompletionAuditLog.objects.create(
|
|
assignment_user=assignment_user,
|
|
assignment=assignment,
|
|
course_session=course_session,
|
|
evaluation_user=evaluation_user,
|
|
completion_status=completion_status,
|
|
assignment_user_email=assignment_user.email,
|
|
assignment_slug=assignment.slug,
|
|
completion_data=deepcopy(ac.completion_data),
|
|
)
|
|
if evaluation_user:
|
|
acl.evaluation_user_email = evaluation_user.email
|
|
|
|
# copy over the question data, so that we don't lose the context
|
|
subtasks = assignment.get_input_tasks()
|
|
for key, value in acl.completion_data.items():
|
|
task_data = find_first(subtasks, pred=lambda x: x["id"] == key)
|
|
if task_data:
|
|
acl.completion_data[key].update(task_data)
|
|
acl.save()
|
|
|
|
return ac
|
|
|
|
|
|
def _remove_unknown_entries(assignment, completion_data):
|
|
"""
|
|
Removes all entries from completion_data which are not known to the assignment
|
|
"""
|
|
input_task_ids = [task["id"] for task in assignment.get_input_tasks()]
|
|
filtered_completion_data = {
|
|
key: value for key, value in completion_data.items() if key in input_task_ids
|
|
}
|
|
return filtered_completion_data
|