from copy import deepcopy from gettext import gettext from django.utils import timezone from rest_framework import serializers from wagtail.models import Page 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 CourseCompletionStatus, CourseSession from vbv_lernwelt.course.services import mark_course_completion from vbv_lernwelt.notify.service import NotificationService def update_assignment_completion( assignment_user: User, assignment: Assignment, course_session: CourseSession, learning_content_page: Page | None = None, completion_data=None, completion_status: AssignmentCompletionStatus = 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, learning_content_page_id=learning_content_page.id if learning_content_page else None, ) if not is_valid_assignment_completion_status(completion_status): raise serializers.ValidationError( { "completion_status": f"Invalid completion status {completion_status.value}" } ) if validate_completion_status_change: if completion_status == AssignmentCompletionStatus.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 == AssignmentCompletionStatus.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 == AssignmentCompletionStatus.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 [ AssignmentCompletionStatus.EVALUATION_SUBMITTED, AssignmentCompletionStatus.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 == AssignmentCompletionStatus.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 == AssignmentCompletionStatus.SUBMITTED: ac.submitted_at = timezone.now() if evaluation_user: verb = gettext( "%(sender)s hat die geleitete Fallarbeit «%(assignment_title)s» abgegeben." ) % { "sender": assignment_user.get_full_name(), "assignment_title": assignment.title, } NotificationService.send_user_interaction_notification( recipient=evaluation_user, verb=verb, sender=ac.assignment_user, course=course_session.course.title, target_url=ac.get_assignment_evaluation_frontend_url(), ) elif completion_status == AssignmentCompletionStatus.EVALUATION_SUBMITTED: ac.evaluation_submitted_at = timezone.now() learning_content_assignment = assignment.learningcontentassignment_set.first() if learning_content_assignment: assignment_frontend_url = learning_content_assignment.get_frontend_url() verb = gettext( "%(sender)s hat die geleitete Fallarbeit «%(assignment_title)s» bewertet." ) % { "sender": evaluation_user.get_full_name(), "assignment_title": assignment.title, } NotificationService.send_user_interaction_notification( recipient=ac.assignment_user, verb=verb, sender=evaluation_user, course=course_session.course.title, target_url=assignment_frontend_url, ) ac.completion_status = completion_status.value # 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 [ AssignmentCompletionStatus.EVALUATION_SUBMITTED, AssignmentCompletionStatus.SUBMITTED, ]: acl = AssignmentCompletionAuditLog.objects.create( assignment_user=assignment_user, assignment=assignment, course_session=course_session, evaluation_user=evaluation_user, completion_status=completion_status.value, 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() if completion_status == AssignmentCompletionStatus.SUBMITTED: learning_content = assignment.learningcontentassignment_set.first() if learning_content: mark_course_completion( user=assignment_user, page=learning_content, course_session=course_session, completion_status=CourseCompletionStatus.SUCCESS.value, ) 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