From baf5801b6a2bb15d1e5d4f0d5388c7d0d4f2dc7d Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 25 Apr 2023 09:14:45 +0200 Subject: [PATCH] Add grading api endpoint --- server/config/urls.py | 15 +- server/vbv_lernwelt/assignment/services.py | 2 +- .../assignment/tests/test_assignment_api.py | 180 +++++++++++++++++- .../assignment/tests/test_services.py | 6 +- server/vbv_lernwelt/assignment/views.py | 56 +++++- 5 files changed, 236 insertions(+), 23 deletions(-) diff --git a/server/config/urls.py b/server/config/urls.py index 0f110b92..a479c973 100644 --- a/server/config/urls.py +++ b/server/config/urls.py @@ -8,11 +8,15 @@ from django.urls import include, path, re_path from django.views import defaults as default_views from grapple import urls as grapple_urls from ratelimit.exceptions import Ratelimited +from wagtail import urls as wagtail_urls +from wagtail.admin import urls as wagtailadmin_urls +from wagtail.documents import urls as wagtaildocs_urls from vbv_lernwelt.assignment.views import ( request_assignment_completion, request_assignment_completion_for_user, - update_assignment_input, + upsert_user_assignment_completion, + grade_assignment_completion, ) from vbv_lernwelt.core.middleware.auth import django_view_authentication_exempt from vbv_lernwelt.core.views import ( @@ -43,9 +47,6 @@ from vbv_lernwelt.feedback.views import ( get_feedback_for_circle, ) from vbv_lernwelt.notify.views import email_notification_settings -from wagtail import urls as wagtail_urls -from wagtail.admin import urls as wagtailadmin_urls -from wagtail.documents import urls as wagtaildocs_urls def raise_example_error(request): @@ -99,8 +100,10 @@ urlpatterns = [ name="request_course_completion_for_user"), # assignment - path(r"api/assignment/update/", update_assignment_input, - name="update_assignment_input"), + path(r"api/assignment/upsert/", upsert_user_assignment_completion, + name="upsert_user_assignment_completion"), + path(r"api/assignment/grade/", grade_assignment_completion, + name="grade_assignment_completion"), path(r"api/assignment///", request_assignment_completion, name="request_assignment_completion"), diff --git a/server/vbv_lernwelt/assignment/services.py b/server/vbv_lernwelt/assignment/services.py index 4e795664..ae111579 100644 --- a/server/vbv_lernwelt/assignment/services.py +++ b/server/vbv_lernwelt/assignment/services.py @@ -36,7 +36,7 @@ def update_assignment_completion( it can also contain "trainer_input" when the trainer has entered grading data { "": { - "trainer_data": {"points": 4, "text": "Gute Antwort"} + "expert_data": {"points": 4, "text": "Gute Antwort"} }, } :param copy_task_data: if true, the task data will be copied to the completion data diff --git a/server/vbv_lernwelt/assignment/tests/test_assignment_api.py b/server/vbv_lernwelt/assignment/tests/test_assignment_api.py index 2fb3aafb..eb50cf6d 100644 --- a/server/vbv_lernwelt/assignment/tests/test_assignment_api.py +++ b/server/vbv_lernwelt/assignment/tests/test_assignment_api.py @@ -1,8 +1,13 @@ import json +from django.utils import timezone from rest_framework.test import APITestCase -from vbv_lernwelt.assignment.models import Assignment, AssignmentCompletion +from vbv_lernwelt.assignment.models import ( + Assignment, + AssignmentCompletion, + AssignmentCompletionAuditLog, +) from vbv_lernwelt.core.create_default_users import create_default_users from vbv_lernwelt.core.models import User from vbv_lernwelt.core.utils import find_first @@ -11,7 +16,7 @@ from vbv_lernwelt.course.creators.test_course import create_test_course from vbv_lernwelt.course.models import CourseSession, CourseSessionUser -class AssignmentApiStudentTestCase(APITestCase): +class AssignmentApiTestCase(APITestCase): def setUp(self) -> None: create_default_users() create_test_course(include_vv=False) @@ -23,15 +28,20 @@ class AssignmentApiStudentTestCase(APITestCase): course_id=COURSE_TEST_ID, title="Test Lehrgang Session", ) - self.user = User.objects.get(username="student") - csu = CourseSessionUser.objects.create( + self.student = User.objects.get(username="student") + self.student_csu = CourseSessionUser.objects.create( course_session=self.cs, - user=self.user, + user=self.student, ) - def test_can_updateAssignmentCompletion_asStudent(self): + self.expert = User.objects.get(username="admin") + self.expert_csu = CourseSessionUser.objects.create( + course_session=self.cs, user=self.expert, role="EXPERT" + ) + + def test_student_can_upsertAssignmentCompletion(self): self.client.login(username="student", password="test") - url = f"/api/assignment/update/" + url = f"/api/assignment/upsert/" user_text_input = find_first( self.assignment_subtasks, pred=lambda x: x["type"] == "user_text_input" @@ -53,7 +63,7 @@ class AssignmentApiStudentTestCase(APITestCase): print(json.dumps(response.json(), indent=2)) self.assertEqual(response.status_code, 200) - self.assertEqual(response_json["assignment_user"], self.user.id) + self.assertEqual(response_json["assignment_user"], self.student.id) self.assertEqual(response_json["assignment"], self.assignment.id) self.assertEqual(response_json["completion_status"], "in_progress") self.assertDictEqual( @@ -64,7 +74,7 @@ class AssignmentApiStudentTestCase(APITestCase): ) db_entry = AssignmentCompletion.objects.get( - assignment_user=self.user, + assignment_user=self.student, course_session_id=self.cs.id, assignment_id=self.assignment.id, ) @@ -107,7 +117,7 @@ class AssignmentApiStudentTestCase(APITestCase): print(json.dumps(response.json(), indent=2)) self.assertEqual(response.status_code, 200) - self.assertEqual(response_json["assignment_user"], self.user.id) + self.assertEqual(response_json["assignment_user"], self.student.id) self.assertEqual(response_json["assignment"], self.assignment.id) self.assertEqual(response_json["completion_status"], "submitted") self.assertDictEqual( @@ -134,3 +144,153 @@ class AssignmentApiStudentTestCase(APITestCase): print(json.dumps(response.json(), indent=2)) self.assertEqual(response.status_code, 404) self.assertTrue("Cannot update completion status" in str(response_json)) + + def test_expert_can_gradeAssignmentCompletion(self): + # setup AssignmentCompletion + subtasks = self.assignment.filter_user_subtasks( + subtask_types=["user_text_input"] + ) + user_text_input = find_first( + subtasks, + pred=lambda x: (value := x.get("value")) + and value.get("text", "").startswith( + "Gibt es zusätzliche Deckungen, die du der Person empfehlen würdest?" + ), + ) + + ac = AssignmentCompletion.objects.create( + assignment_user=self.student, + assignment=self.assignment, + course_session=self.cs, + completion_status="submitted", + submitted_at=timezone.now(), + completion_data={ + user_text_input["id"]: { + "user_data": {"text": "Ich würde nichts weiteres empfehlen."} + }, + }, + ) + + # make api call + self.client.login(username="admin", password="test") + url = f"/api/assignment/grade/" + + response = self.client.post( + url, + { + "assignment_id": self.assignment.id, + "assignment_user_id": self.student.id, + "course_session_id": self.cs.id, + "completion_status": "grading_in_progress", + "completion_data": { + user_text_input["id"]: { + "expert_data": {"points": 1, "comment": "Gut gemacht!"} + }, + }, + }, + format="json", + ) + response_json = response.json() + print(json.dumps(response.json(), indent=2)) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response_json["assignment_user"], self.student.id) + self.assertEqual(response_json["assignment"], self.assignment.id) + self.assertEqual(response_json["completion_status"], "grading_in_progress") + self.assertDictEqual( + response_json["completion_data"], + { + user_text_input["id"]: { + "user_data": {"text": "Ich würde nichts weiteres empfehlen."}, + "expert_data": {"points": 1, "comment": "Gut gemacht!"}, + }, + }, + ) + + db_entry = AssignmentCompletion.objects.get( + assignment_user=self.student, + course_session_id=self.cs.id, + assignment_id=self.assignment.id, + ) + self.assertEqual(db_entry.completion_status, "grading_in_progress") + self.assertDictEqual( + db_entry.completion_data, + { + user_text_input["id"]: { + "user_data": {"text": "Ich würde nichts weiteres empfehlen."}, + "expert_data": {"points": 1, "comment": "Gut gemacht!"}, + }, + }, + ) + + # finish grading + response = self.client.post( + url, + { + "assignment_id": self.assignment.id, + "assignment_user_id": self.student.id, + "course_session_id": self.cs.id, + "completion_status": "graded", + "completion_data": { + user_text_input["id"]: { + "expert_data": {"points": 1, "comment": "Gut gemacht!"} + }, + }, + }, + format="json", + ) + response_json = response.json() + print(json.dumps(response.json(), indent=2)) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response_json["assignment_user"], self.student.id) + self.assertEqual(response_json["assignment"], self.assignment.id) + self.assertEqual(response_json["completion_status"], "graded") + self.assertDictEqual( + response_json["completion_data"], + { + user_text_input["id"]: { + "user_data": {"text": "Ich würde nichts weiteres empfehlen."}, + "expert_data": {"points": 1, "comment": "Gut gemacht!"}, + }, + }, + ) + + db_entry = AssignmentCompletion.objects.get( + assignment_user=self.student, + course_session_id=self.cs.id, + assignment_id=self.assignment.id, + ) + self.assertEqual(db_entry.completion_status, "graded") + self.assertDictEqual( + db_entry.completion_data, + { + user_text_input["id"]: { + "user_data": {"text": "Ich würde nichts weiteres empfehlen."}, + "expert_data": {"points": 1, "comment": "Gut gemacht!"}, + }, + }, + ) + + # `graded` will create a new AssignmentCompletionAuditLog + acl = AssignmentCompletionAuditLog.objects.get( + assignment_user=self.student, + course_session_id=self.cs.id, + assignment_id=self.assignment.id, + completion_status="graded", + ) + self.maxDiff = None + self.assertDictEqual( + acl.completion_data, + { + user_text_input["id"]: { + "id": user_text_input["id"], + "type": "user_text_input", + "value": { + "text": "Gibt es zusätzliche Deckungen, die du der Person empfehlen würdest? Begründe deine Empfehlung", + }, + "user_data": {"text": "Ich würde nichts weiteres empfehlen."}, + "expert_data": {"points": 1, "comment": "Gut gemacht!"}, + }, + }, + ) diff --git a/server/vbv_lernwelt/assignment/tests/test_services.py b/server/vbv_lernwelt/assignment/tests/test_services.py index 4fd6b7a8..dca1eda8 100644 --- a/server/vbv_lernwelt/assignment/tests/test_services.py +++ b/server/vbv_lernwelt/assignment/tests/test_services.py @@ -326,7 +326,7 @@ class UpdateAssignmentCompletionTestCase(TestCase): course_session=self.course_session, completion_data={ user_text_input["id"]: { - "trainer_data": {"points": 1, "comment": "Gut gemacht!"} + "expert_data": {"points": 1, "comment": "Gut gemacht!"} }, }, completion_status="grading_in_progress", @@ -342,7 +342,7 @@ class UpdateAssignmentCompletionTestCase(TestCase): self.assertEqual(ac.completion_status, "grading_in_progress") user_input = ac.completion_data[user_text_input["id"]] self.assertDictEqual( - user_input["trainer_data"], {"points": 1, "comment": "Gut gemacht!"} + user_input["expert_data"], {"points": 1, "comment": "Gut gemacht!"} ) self.assertEqual( user_input["user_data"]["text"], "Ich würde nichts weiteres empfehlen." @@ -380,7 +380,7 @@ class UpdateAssignmentCompletionTestCase(TestCase): completion_status="grading_in_progress", completion_data={ user_text_input["id"]: { - "trainer_data": {"points": 1, "comment": "Gut gemacht!"} + "expert_data": {"points": 1, "comment": "Gut gemacht!"} }, }, ) diff --git a/server/vbv_lernwelt/assignment/views.py b/server/vbv_lernwelt/assignment/views.py index 688ca8cc..66eeb27a 100644 --- a/server/vbv_lernwelt/assignment/views.py +++ b/server/vbv_lernwelt/assignment/views.py @@ -8,6 +8,7 @@ from wagtail.models import Page from vbv_lernwelt.assignment.models import AssignmentCompletion from vbv_lernwelt.assignment.serializers import AssignmentCompletionSerializer from vbv_lernwelt.assignment.services import update_assignment_completion +from vbv_lernwelt.core.models import User from vbv_lernwelt.course.models import CourseSession from vbv_lernwelt.course.permissions import ( has_course_access, @@ -64,7 +65,7 @@ def request_assignment_completion_for_user( @api_view(["POST"]) -def update_assignment_input(request): +def upsert_user_assignment_completion(request): try: assignment_id = request.data.get("assignment_id") course_session_id = request.data.get("course_session_id") @@ -88,11 +89,11 @@ def update_assignment_input(request): ) logger.debug( - "store_assignment_input successful", + "upsert_user_assignment_completion successful", label="assignment_api", assignment_id=assignment.id, assignment_title=assignment.title, - user_id=request.user.id, + assignment_user_id=request.user.id, course_session_id=course_session_id, completion_status=completion_status, ) @@ -103,3 +104,52 @@ def update_assignment_input(request): except Exception as e: logger.error(e, exc_info=True) return Response({"error": str(e)}, status=404) + + +@api_view(["POST"]) +def grade_assignment_completion(request): + try: + assignment_id = request.data.get("assignment_id") + assignment_user_id = request.data.get("assignment_user_id") + course_session_id = request.data.get("course_session_id") + completion_status = request.data.get("completion_status", "grading_in_progress") + completion_data = request.data.get("completion_data", {}) + + assignment_page = Page.objects.get(id=assignment_id) + assignment_user = User.objects.get(id=assignment_user_id) + + if not has_course_access_by_page_request(request, assignment_page): + raise PermissionDenied() + + if not is_course_session_expert(request.user, course_session_id): + raise PermissionDenied() + + assignment = assignment_page.specific + + ac = update_assignment_completion( + assignment_user=assignment_user, + assignment=assignment, + course_session=CourseSession.objects.get(id=course_session_id), + completion_data=completion_data, + completion_status=completion_status, + copy_task_data=False, + grading_user=request.user, + ) + + logger.debug( + "grade_assignment_completion successful", + label="assignment_api", + assignment_id=assignment.id, + assignment_title=assignment.title, + assignment_user_id=assignment_user_id, + course_session_id=course_session_id, + completion_status=completion_status, + grading_user_id=request.user.id, + ) + + return Response(status=200, data=AssignmentCompletionSerializer(ac).data) + except PermissionDenied as e: + raise e + except Exception as e: + logger.error(e, exc_info=True) + return Response({"error": str(e)}, status=404)