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, ) 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_data": {"text": "some text from user"}}, "": {"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 { "": { "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 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