vbv/server/vbv_lernwelt/assignment/services.py

274 lines
10 KiB
Python

from copy import deepcopy
import structlog
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,
AssignmentType,
is_valid_assignment_completion_status,
recalculate_assignment_passed,
)
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.services import NotificationService
logger = structlog.get_logger(__name__)
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_points: float | None = None,
evaluation_passed: bool | None = None,
evaluation_max_points: float | None = None,
validate_completion_status_change: bool = True,
copy_task_data: bool = False,
initialize_completion: bool = False,
additional_json_data: dict | None = None,
edoniq_extended_time_flag: bool = False,
validate_submission_update: bool = True,
) -> tuple[AssignmentCompletion, bool]:
"""
: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
:param initialize_completion: if true, the completion will be created, but not updated
used as a workaround for initial work with the object on the frontend
"""
if completion_data is None:
completion_data = {}
if additional_json_data is None:
additional_json_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 initialize_completion:
# ignore further updates
return ac, True
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:
completion_status_to_check = [
"EVALUATION_IN_PROGRESS",
"EVALUATION_SUBMITTED",
]
if validate_submission_update:
completion_status_to_check.append("SUBMITTED")
if ac.completion_status in completion_status_to_check:
raise serializers.ValidationError(
{
"completion_status": f"Cannot update completion status from {ac.completion_status} to SUBMITTED"
}
)
elif (
completion_status == AssignmentCompletionStatus.EVALUATION_SUBMITTED
and validate_submission_update
):
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_points is None:
raise serializers.ValidationError(
{
"evaluation_points": "evaluation_points is required for EVALUATION_SUBMITTED status"
}
)
if evaluation_max_points is None:
ac.evaluation_max_points = assignment.get_max_points()
else:
ac.evaluation_max_points = evaluation_max_points
ac.evaluation_points = evaluation_points
# if no evaluation_passed is provided, we calculate it from the points
if evaluation_passed is None and ac.evaluation_max_points > 0:
recalculate_assignment_passed(ac)
else:
ac.evaluation_passed = evaluation_passed
if completion_status == AssignmentCompletionStatus.SUBMITTED:
ac.submitted_at = timezone.now()
if evaluation_user:
ac.evaluation_user = evaluation_user
NotificationService.send_assignment_submitted_notification(
recipient=evaluation_user,
sender=ac.assignment_user,
assignment_completion=ac,
)
elif completion_status == AssignmentCompletionStatus.EVALUATION_SUBMITTED:
ac.evaluation_submitted_at = timezone.now()
learning_content_assignment = (
learning_content_page
if learning_content_page
else assignment.find_attached_learning_content()
)
if learning_content_assignment:
assignment_frontend_url = learning_content_assignment.get_frontend_url()
NotificationService.send_assignment_evaluated_notification(
recipient=ac.assignment_user,
sender=evaluation_user,
assignment_completion=ac,
target_url=assignment_frontend_url,
)
ac.completion_status = completion_status.value
ac.edoniq_extended_time_flag = edoniq_extended_time_flag
ac.additional_json_data = ac.additional_json_data | additional_json_data
task_ids = [task.id for task in assignment.tasks]
for key, value in completion_data.items():
if key in task_ids:
stored_entry = ac.completion_data.get(key, {})
stored_entry.update(value)
ac.completion_data[key] = stored_entry
# 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
sub_tasks = assignment.get_input_tasks()
for key, _ in ac.completion_data.items():
task_data = find_first(sub_tasks, pred=lambda x: x["id"] == key) # noqa
if task_data:
ac.completion_data[key].update(task_data)
ac.save()
if (
completion_status
in [
AssignmentCompletionStatus.EVALUATION_SUBMITTED,
AssignmentCompletionStatus.SUBMITTED,
]
and assignment.assignment_type != AssignmentType.EDONIQ_TEST.value
):
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),
evaluation_points=evaluation_points,
evaluation_max_points=ac.evaluation_max_points,
evaluation_passed=ac.evaluation_passed,
)
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, _ in acl.completion_data.items():
task_data = find_first(subtasks, pred=lambda x: x["id"] == key) # noqa
if task_data:
acl.completion_data[key].update(task_data)
acl.save()
if completion_status in [
AssignmentCompletionStatus.EVALUATION_SUBMITTED,
AssignmentCompletionStatus.EVALUATION_IN_PROGRESS,
AssignmentCompletionStatus.SUBMITTED,
]:
learning_content = (
learning_content_page
if learning_content_page
else assignment.find_attached_learning_content()
)
if learning_content:
mark_course_completion(
user=assignment_user,
page=learning_content,
course_session=course_session,
completion_status=CourseCompletionStatus.SUCCESS.value,
)
return ac, created
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