diff --git a/client/src/gql/schema.graphql b/client/src/gql/schema.graphql index c35b37a3..c381bea3 100644 --- a/client/src/gql/schema.graphql +++ b/client/src/gql/schema.graphql @@ -184,7 +184,7 @@ scalar JSONString type Mutation { send_feedback(input: SendFeedbackInput!): SendFeedbackPayload - upsert_assignment_completion(assignment_id: ID!, assignment_user_id: ID, completion_data_string: String, completion_status: String, course_session_id: ID!, evaluation_grade: Float, evaluation_points: Float): AssignmentCompletionMutation + upsert_assignment_completion(assignment_id: ID!, assignment_user_id: ID, completion_data_string: String, completion_status: AssignmentCompletionStatus, course_session_id: ID!, evaluation_grade: Float, evaluation_points: Float): AssignmentCompletionMutation } type SendFeedbackPayload { @@ -222,4 +222,12 @@ input SendFeedbackInput { type AssignmentCompletionMutation { assignment_completion: AssignmentCompletionType +} + +"""An enumeration.""" +enum AssignmentCompletionStatus { + IN_PROGRESS + SUBMITTED + EVALUATION_IN_PROGRESS + EVALUATION_SUBMITTED } \ No newline at end of file diff --git a/server/config/settings/base.py b/server/config/settings/base.py index 2f62cdac..bae81804 100644 --- a/server/config/settings/base.py +++ b/server/config/settings/base.py @@ -372,6 +372,11 @@ if IT_DJANGO_LOGGING_CONF == "IT_DJANGO_LOGGING_CONF_CONSOLE_COLOR": "level": "INFO", "propagate": False, }, + "wagtail": { + "handlers": ["default"], + "level": "WARNING" if IT_LOCAL_HIDE_DJANGO_SERVER_LOGS else "INFO", + "propagate": False, + }, "vbv_lernwelt": { "handlers": ["default"], "level": "DEBUG", diff --git a/server/config/urls.py b/server/config/urls.py index d4950815..b0912a61 100644 --- a/server/config/urls.py +++ b/server/config/urls.py @@ -11,13 +11,7 @@ from django.views.decorators.csrf import csrf_exempt from graphene_django.views import GraphQLView from ratelimit.exceptions import Ratelimited -from vbv_lernwelt.assignment.views import ( - evaluate_assignment_completion, - request_assignment_completion, - request_assignment_completion_for_user, - request_assignment_completion_status, - upsert_user_assignment_completion, -) +from vbv_lernwelt.assignment.views import request_assignment_completion_status from vbv_lernwelt.core.middleware.auth import django_view_authentication_exempt from vbv_lernwelt.core.schema import schema from vbv_lernwelt.core.views import ( @@ -120,21 +114,10 @@ urlpatterns = [ name="request_course_completion_for_user"), # assignment - path(r"api/assignment/upsert/", upsert_user_assignment_completion, - name="upsert_user_assignment_completion"), - path(r"api/assignment/evaluate/", evaluate_assignment_completion, - name="evaluate_assignment_completion"), - path(r"api/assignment///", - request_assignment_completion, - name="request_assignment_completion"), path( r"api/assignment///status/", request_assignment_completion_status, name="request_assignment_completion_status"), - path( - r"api/assignment////", - request_assignment_completion_for_user, - name="request_assignment_completion_for_user"), # documents path(r'api/core/document/start/', document_upload_start, diff --git a/server/vbv_lernwelt/assignment/graphql/mutations.py b/server/vbv_lernwelt/assignment/graphql/mutations.py index 52877782..632c04c5 100644 --- a/server/vbv_lernwelt/assignment/graphql/mutations.py +++ b/server/vbv_lernwelt/assignment/graphql/mutations.py @@ -5,7 +5,7 @@ import structlog from rest_framework.exceptions import PermissionDenied from vbv_lernwelt.assignment.graphql.types import AssignmentCompletionType -from vbv_lernwelt.assignment.models import Assignment +from vbv_lernwelt.assignment.models import Assignment, AssignmentCompletionStatus from vbv_lernwelt.assignment.services import update_assignment_completion from vbv_lernwelt.core.models import User from vbv_lernwelt.course.models import CourseSession @@ -17,12 +17,14 @@ logger = structlog.get_logger(__name__) class AssignmentCompletionMutation(graphene.Mutation): assignment_completion = graphene.Field(AssignmentCompletionType) - class Input: + class Arguments: assignment_id = graphene.ID(required=True) course_session_id = graphene.ID(required=True) assignment_user_id = graphene.ID() - completion_status = graphene.String() + completion_status = graphene.Argument( + graphene.Enum.from_enum(AssignmentCompletionStatus) + ) completion_data_string = graphene.String() evaluation_grade = graphene.Float() @@ -36,7 +38,7 @@ class AssignmentCompletionMutation(graphene.Mutation): assignment_id, course_session_id, assignment_user_id=None, - completion_status="IN_PROGRESS", + completion_status: AssignmentCompletionStatus = AssignmentCompletionStatus.IN_PROGRESS, completion_data_string="{}", evaluation_grade=None, evaluation_points=None, @@ -63,7 +65,10 @@ class AssignmentCompletionMutation(graphene.Mutation): evaluation_data = {} - if completion_status in ["EVALUATION_SUBMITTED", "EVALUATION_IN_PROGRESS"]: + if completion_status in [ + AssignmentCompletionStatus.EVALUATION_SUBMITTED, + AssignmentCompletionStatus.EVALUATION_IN_PROGRESS, + ]: if not is_course_session_expert(info.context.user, course_session_id): raise PermissionDenied() diff --git a/server/vbv_lernwelt/assignment/migrations/0005_alter_assignmentcompletionauditlog_completion_status.py b/server/vbv_lernwelt/assignment/migrations/0005_alter_assignmentcompletionauditlog_completion_status.py new file mode 100644 index 00000000..623e4aa8 --- /dev/null +++ b/server/vbv_lernwelt/assignment/migrations/0005_alter_assignmentcompletionauditlog_completion_status.py @@ -0,0 +1,27 @@ +# Generated by Django 3.2.13 on 2023-06-28 14:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("assignment", "0004_assignment_assignment_type"), + ] + + operations = [ + migrations.AlterField( + model_name="assignmentcompletionauditlog", + name="completion_status", + field=models.CharField( + choices=[ + ("IN_PROGRESS", "IN_PROGRESS"), + ("SUBMITTED", "SUBMITTED"), + ("EVALUATION_IN_PROGRESS", "EVALUATION_IN_PROGRESS"), + ("EVALUATION_SUBMITTED", "EVALUATION_SUBMITTED"), + ], + default="IN_PROGRESS", + max_length=255, + ), + ), + ] diff --git a/server/vbv_lernwelt/assignment/migrations/0006_auto_20230628_1616.py b/server/vbv_lernwelt/assignment/migrations/0006_auto_20230628_1616.py new file mode 100644 index 00000000..e2a094bc --- /dev/null +++ b/server/vbv_lernwelt/assignment/migrations/0006_auto_20230628_1616.py @@ -0,0 +1,37 @@ +# Generated by Django 3.2.13 on 2023-06-28 14:16 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("learnpath", "0007_learningunit_title_hidden"), + ("assignment", "0005_alter_assignmentcompletionauditlog_completion_status"), + ] + + operations = [ + migrations.RemoveConstraint( + model_name="assignmentcompletion", + name="assignment_completion_unique_user_assignment_course_session", + ), + migrations.AddField( + model_name="assignmentcompletion", + name="circle", + field=models.ForeignKey( + blank=True, + default=None, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="learnpath.circle", + ), + ), + migrations.AddConstraint( + model_name="assignmentcompletion", + constraint=models.UniqueConstraint( + fields=("assignment_user", "assignment", "course_session", "circle"), + name="assignment_completion_unique_user_assignment_course_session", + ), + ), + ] diff --git a/server/vbv_lernwelt/assignment/models.py b/server/vbv_lernwelt/assignment/models.py index d482a175..9d796804 100644 --- a/server/vbv_lernwelt/assignment/models.py +++ b/server/vbv_lernwelt/assignment/models.py @@ -107,14 +107,10 @@ class EvaluationTaskBlock(blocks.StructBlock): label = "Beurteilungskriterium" -AssignmentType = Enum( - "AssignmentType", - [ - "CASEWORK", # Geleitete Fallarbeit - "PREP_ASSIGNMENT", # Vorbereitungsauftrag - "REFLECTION", # Reflexion - ], -) +class AssignmentType(Enum): + CASEWORK = "CASEWORK" # Geleitete Fallarbeit + PREP_ASSIGNMENT = "PREP_ASSIGNMENT" # Vorbereitungsauftrag + REFLECTION = "REFLECTION" # Reflexion class Assignment(CourseBasePage): @@ -130,8 +126,8 @@ class Assignment(CourseBasePage): assignment_type = models.CharField( max_length=50, - choices=[(tag.name, tag.name) for tag in AssignmentType], - default=AssignmentType.CASEWORK.name, + choices=[(tag.value, tag.value) for tag in AssignmentType], + default=AssignmentType.CASEWORK.value, ) intro_text = RichTextField( @@ -242,14 +238,17 @@ class Assignment(CourseBasePage): return self.filter_user_subtasks() + self.get_evaluation_tasks() -AssignmentCompletionStatus = Enum( - "AssignmentCompletionStatus", - ["IN_PROGRESS", "SUBMITTED", "EVALUATION_IN_PROGRESS", "EVALUATION_SUBMITTED"], -) +class AssignmentCompletionStatus(Enum): + IN_PROGRESS = "IN_PROGRESS" + SUBMITTED = "SUBMITTED" + EVALUATION_IN_PROGRESS = "EVALUATION_IN_PROGRESS" + EVALUATION_SUBMITTED = "EVALUATION_SUBMITTED" -def is_valid_assignment_completion_status(status): - return status in AssignmentCompletionStatus.__members__ +def is_valid_assignment_completion_status( + completion_status: AssignmentCompletionStatus, +): + return completion_status.value in AssignmentCompletionStatus.__members__ class AssignmentCompletion(models.Model): @@ -271,11 +270,18 @@ class AssignmentCompletion(models.Model): assignment_user = models.ForeignKey(User, on_delete=models.CASCADE) assignment = models.ForeignKey(Assignment, on_delete=models.CASCADE) course_session = models.ForeignKey("course.CourseSession", on_delete=models.CASCADE) + circle = models.ForeignKey( + "learnpath.Circle", + on_delete=models.CASCADE, + null=True, + blank=True, + default=None, + ) completion_status = models.CharField( max_length=255, - choices=[(acs.name, acs.name) for acs in AssignmentCompletionStatus], - default="IN_PROGRESS", + choices=[(acs.value, acs.value) for acs in AssignmentCompletionStatus], + default=AssignmentCompletionStatus.IN_PROGRESS.value, ) completion_data = models.JSONField(default=dict) @@ -284,7 +290,7 @@ class AssignmentCompletion(models.Model): class Meta: constraints = [ UniqueConstraint( - fields=["assignment_user", "assignment", "course_session"], + fields=["assignment_user", "assignment", "course_session", "circle"], name="assignment_completion_unique_user_assignment_course_session", ) ] diff --git a/server/vbv_lernwelt/assignment/schema.py b/server/vbv_lernwelt/assignment/schema.py deleted file mode 100644 index a58e2c4b..00000000 --- a/server/vbv_lernwelt/assignment/schema.py +++ /dev/null @@ -1,3 +0,0 @@ -import structlog - -logger = structlog.get_logger(__name__) diff --git a/server/vbv_lernwelt/assignment/services.py b/server/vbv_lernwelt/assignment/services.py index caca03ed..ae872d74 100644 --- a/server/vbv_lernwelt/assignment/services.py +++ b/server/vbv_lernwelt/assignment/services.py @@ -1,5 +1,4 @@ from copy import deepcopy -from typing import Type from django.utils import timezone from rest_framework import serializers @@ -21,7 +20,7 @@ def update_assignment_completion( assignment: Assignment, course_session: CourseSession, completion_data=None, - completion_status: Type[AssignmentCompletionStatus] = "IN_PROGRESS", + completion_status: AssignmentCompletionStatus = AssignmentCompletionStatus.IN_PROGRESS, evaluation_user: User | None = None, evaluation_grade: float | None = None, evaluation_points: float | None = None, @@ -58,12 +57,14 @@ def update_assignment_completion( if not is_valid_assignment_completion_status(completion_status): raise serializers.ValidationError( - {"completion_status": f"Invalid completion status {completion_status}"} + { + "completion_status": f"Invalid completion status {completion_status.value}" + } ) if validate_completion_status_change: # TODO: check time? - if completion_status == "SUBMITTED": + if completion_status == AssignmentCompletionStatus.SUBMITTED: if ac.completion_status in [ "SUBMITTED", "EVALUATION_IN_PROGRESS", @@ -74,7 +75,7 @@ def update_assignment_completion( "completion_status": f"Cannot update completion status from {ac.completion_status} to SUBMITTED" } ) - elif completion_status == "EVALUATION_SUBMITTED": + elif completion_status == AssignmentCompletionStatus.EVALUATION_SUBMITTED: if ac.completion_status == "EVALUATION_SUBMITTED": raise serializers.ValidationError( { @@ -82,14 +83,20 @@ def update_assignment_completion( } ) - if completion_status == "IN_PROGRESS" and ac.completion_status != "IN_PROGRESS": + 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 ["EVALUATION_SUBMITTED", "EVALUATION_IN_PROGRESS"]: + if completion_status in [ + AssignmentCompletionStatus.EVALUATION_SUBMITTED, + AssignmentCompletionStatus.EVALUATION_IN_PROGRESS, + ]: if evaluation_user is None: raise serializers.ValidationError( { @@ -98,7 +105,7 @@ def update_assignment_completion( ) ac.evaluation_user = evaluation_user - if completion_status == "EVALUATION_SUBMITTED": + if completion_status == AssignmentCompletionStatus.EVALUATION_SUBMITTED: if evaluation_grade is None: raise serializers.ValidationError( { @@ -115,12 +122,12 @@ def update_assignment_completion( ac.evaluation_grade = evaluation_grade ac.evaluation_points = evaluation_points - if completion_status == "SUBMITTED": + if completion_status == AssignmentCompletionStatus.SUBMITTED: ac.submitted_at = timezone.now() - elif completion_status == "EVALUATION_SUBMITTED": + elif completion_status == AssignmentCompletionStatus.EVALUATION_SUBMITTED: ac.evaluation_submitted_at = timezone.now() - ac.completion_status = completion_status + 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) @@ -140,13 +147,16 @@ def update_assignment_completion( ac.save() - if completion_status in ["EVALUATION_SUBMITTED", "SUBMITTED"]: + 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, + completion_status=completion_status.value, assignment_user_email=assignment_user.email, assignment_slug=assignment.slug, completion_data=deepcopy(ac.completion_data), diff --git a/server/vbv_lernwelt/assignment/tests/test_assignment_api.py b/server/vbv_lernwelt/assignment/tests/test_assignment_api.py deleted file mode 100644 index 86fe4edf..00000000 --- a/server/vbv_lernwelt/assignment/tests/test_assignment_api.py +++ /dev/null @@ -1,299 +0,0 @@ -import json - -from django.utils import timezone -from rest_framework.test import APITestCase - -from vbv_lernwelt.assignment.creators.create_assignments import ( - create_uk_fahrzeug_casework, -) -from vbv_lernwelt.assignment.models import ( - 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 -from vbv_lernwelt.course.consts import COURSE_TEST_ID -from vbv_lernwelt.course.creators.test_course import create_test_course -from vbv_lernwelt.course.models import CourseSession, CourseSessionUser - - -class AssignmentApiTestCase(APITestCase): - def setUp(self) -> None: - create_default_users() - create_test_course(include_vv=False) - self.assignment = create_uk_fahrzeug_casework(course_id=COURSE_TEST_ID) - self.assignment_subtasks = self.assignment.filter_user_subtasks() - - self.cs = CourseSession.objects.create( - course_id=COURSE_TEST_ID, - title="Test Lehrgang Session", - ) - self.student = User.objects.get(username="student") - self.student_csu = CourseSessionUser.objects.create( - course_session=self.cs, - user=self.student, - ) - - 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/upsert/" - - user_text_input = find_first( - self.assignment_subtasks, pred=lambda x: x["type"] == "user_text_input" - ) - - response = self.client.post( - url, - { - "assignment_id": self.assignment.id, - "course_session_id": self.cs.id, - "completion_status": "IN_PROGRESS", - "completion_data": { - user_text_input["id"]: {"user_data": {"text": "Hallo via API"}}, - }, - }, - 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"], "IN_PROGRESS") - self.assertDictEqual( - response_json["completion_data"], - { - user_text_input["id"]: {"user_data": {"text": "Hallo via API"}}, - }, - ) - - 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, "IN_PROGRESS") - self.assertDictEqual( - db_entry.completion_data, - { - user_text_input["id"]: {"user_data": {"text": "Hallo via API"}}, - }, - ) - - # read data via request api - response = self.client.get( - f"/api/assignment/{self.assignment.id}/{self.cs.id}/", - format="json", - ) - response_json = response.json() - print(json.dumps(response.json(), indent=2)) - self.assertDictEqual( - response_json["completion_data"], - { - user_text_input["id"]: {"user_data": {"text": "Hallo via API"}}, - }, - ) - - # submit the assignment - response = self.client.post( - url, - { - "assignment_id": self.assignment.id, - "course_session_id": self.cs.id, - "completion_status": "SUBMITTED", - "completion_data": { - user_text_input["id"]: {"user_data": {"text": "Hallo via API 2"}}, - }, - }, - 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"], "SUBMITTED") - self.assertDictEqual( - response_json["completion_data"], - { - user_text_input["id"]: {"user_data": {"text": "Hallo via API 2"}}, - }, - ) - - # second submit will fail - response = self.client.post( - url, - { - "assignment_id": self.assignment.id, - "course_session_id": self.cs.id, - "completion_status": "SUBMITTED", - "completion_data": { - user_text_input["id"]: {"user_data": {"text": "Hallo via API 2"}}, - }, - }, - format="json", - ) - response_json = response.json() - 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/evaluate/" - - response = self.client.post( - url, - { - "assignment_id": self.assignment.id, - "assignment_user_id": self.student.id, - "course_session_id": self.cs.id, - "completion_status": "EVALUATION_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"], "EVALUATION_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, "EVALUATION_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": "EVALUATION_SUBMITTED", - "completion_data": { - user_text_input["id"]: { - "expert_data": {"points": 1, "comment": "Gut gemacht!"} - }, - }, - "evaluation_grade": 4.5, - "evaluation_points": 16, - }, - 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"], "EVALUATION_SUBMITTED") - 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, "EVALUATION_SUBMITTED") - 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!"}, - }, - }, - ) - - # `EVALUATION_SUBMITTED` 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="EVALUATION_SUBMITTED", - ) - 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_graphql.py b/server/vbv_lernwelt/assignment/tests/test_graphql.py new file mode 100644 index 00000000..8ad00838 --- /dev/null +++ b/server/vbv_lernwelt/assignment/tests/test_graphql.py @@ -0,0 +1,368 @@ +import json +from datetime import date + +from django.utils import timezone +from graphene_django.utils import GraphQLTestCase + +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 +from vbv_lernwelt.course.creators.test_course import create_test_course +from vbv_lernwelt.course.models import CourseSession + + +class AttendanceCourseUserMutationTestCase(GraphQLTestCase): + GRAPHQL_URL = "/server/graphql/" + + def setUp(self): + create_default_users() + create_test_course(include_vv=False, with_sessions=True) + self.course_session = CourseSession.objects.get(title="Test Bern 2022 a") + self.trainer = User.objects.get(username="test-trainer1@example.com") + self.student = User.objects.get(username="test-student1@example.com") + + self.assignment = Assignment.objects.get( + slug="test-lehrgang-assignment-überprüfen-einer-motorfahrzeugs-versicherungspolice" + ) + self.assignment_subtasks = self.assignment.filter_user_subtasks() + + # self.client.force_login(self.trainer) + + def test_student_can_upsertAssignmentCompletion(self): + self.client.force_login(self.student) + user_text_input = find_first( + self.assignment_subtasks, pred=lambda x: x["type"] == "user_text_input" + ) + + completion_data_string = json.dumps( + { + user_text_input["id"]: {"user_data": {"text": "Hallo via API"}}, + } + ).replace('"', '\\"') + + query = f""" + mutation {{ + upsert_assignment_completion( + assignment_id: {self.assignment.id} + course_session_id: {self.course_session.id} + completion_status: IN_PROGRESS + completion_data_string: "{completion_data_string}" + ) {{ + assignment_completion {{ + id + completion_status + completion_data + assignment_user {{ id }} + assignment {{ id }} + }} + }} + }} + """ + + response = self.query(query) + self.assertResponseNoErrors(response) + + data = json.loads(response.content)["data"]["upsert_assignment_completion"][ + "assignment_completion" + ] + + self.assertEqual(data["assignment_user"]["id"], str(self.student.id)) + self.assertEqual(data["assignment"]["id"], str(self.assignment.id)) + self.assertEqual(data["completion_status"], "IN_PROGRESS") + self.assertDictEqual( + data["completion_data"], + { + user_text_input["id"]: {"user_data": {"text": "Hallo via API"}}, + }, + ) + + # check DB data + db_entry = AssignmentCompletion.objects.get( + assignment_user=self.student, + course_session_id=self.course_session.id, + assignment_id=self.assignment.id, + ) + self.assertEqual(db_entry.completion_status, "IN_PROGRESS") + self.assertDictEqual( + db_entry.completion_data, + { + user_text_input["id"]: {"user_data": {"text": "Hallo via API"}}, + }, + ) + + # submit the response + completion_data_string = json.dumps( + { + user_text_input["id"]: {"user_data": {"text": "Hallo via API 2"}}, + } + ).replace('"', '\\"') + + query = f""" + mutation {{ + upsert_assignment_completion( + assignment_id: {self.assignment.id} + course_session_id: {self.course_session.id} + completion_status: SUBMITTED + completion_data_string: "{completion_data_string}" + ) {{ + assignment_completion {{ + id + completion_status + completion_data + assignment_user {{ id }} + assignment {{ id }} + }} + }} + }} + """ + + response = self.query(query) + self.assertResponseNoErrors(response) + + data = json.loads(response.content)["data"]["upsert_assignment_completion"][ + "assignment_completion" + ] + + self.assertEqual(data["assignment_user"]["id"], str(self.student.id)) + self.assertEqual(data["assignment"]["id"], str(self.assignment.id)) + self.assertEqual(data["completion_status"], "SUBMITTED") + self.assertDictEqual( + data["completion_data"], + { + user_text_input["id"]: {"user_data": {"text": "Hallo via API 2"}}, + }, + ) + + # check DB data + db_entry = AssignmentCompletion.objects.get( + assignment_user=self.student, + course_session_id=self.course_session.id, + assignment_id=self.assignment.id, + ) + self.assertEqual(db_entry.completion_status, "SUBMITTED") + self.assertEqual(db_entry.submitted_at.date(), date.today()) + self.assertDictEqual( + db_entry.completion_data, + { + user_text_input["id"]: {"user_data": {"text": "Hallo via API 2"}}, + }, + ) + + # second submit will fail + completion_data_string = json.dumps( + { + user_text_input["id"]: {"user_data": {"text": "Hallo via API 3"}}, + } + ).replace('"', '\\"') + + query = f""" + mutation {{ + upsert_assignment_completion( + assignment_id: {self.assignment.id} + course_session_id: {self.course_session.id} + completion_status: SUBMITTED + completion_data_string: "{completion_data_string}" + ) {{ + assignment_completion {{ + id + completion_status + completion_data + assignment_user {{ id }} + assignment {{ id }} + }} + }} + }} + """ + + response = self.query(query) + self.assertResponseHasErrors(response) + 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.course_session, + completion_status="SUBMITTED", + submitted_at=timezone.now(), + completion_data={ + user_text_input["id"]: { + "user_data": {"text": "Ich würde nichts weiteres empfehlen."} + }, + }, + ) + + self.client.force_login(self.trainer) + + completion_data_string = json.dumps( + { + user_text_input["id"]: { + "expert_data": {"points": 1, "comment": "Gut gemacht!"} + }, + } + ).replace('"', '\\"') + + query = f""" + mutation {{ + upsert_assignment_completion( + assignment_id: {self.assignment.id} + assignment_user_id: {self.student.id} + course_session_id: {self.course_session.id} + completion_status: EVALUATION_IN_PROGRESS + completion_data_string: "{completion_data_string}" + ) {{ + assignment_completion {{ + id + completion_status + completion_data + assignment_user {{ id }} + assignment {{ id }} + }} + }} + }} + """ + + response = self.query(query) + self.assertResponseNoErrors(response) + + data = json.loads(response.content)["data"]["upsert_assignment_completion"][ + "assignment_completion" + ] + + self.assertEqual(data["assignment_user"]["id"], str(self.student.id)) + self.assertEqual(data["assignment"]["id"], str(self.assignment.id)) + self.assertEqual(data["completion_status"], "EVALUATION_IN_PROGRESS") + self.assertDictEqual( + data["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.course_session.id, + assignment_id=self.assignment.id, + ) + self.assertEqual(db_entry.completion_status, "EVALUATION_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 + completion_data_string = json.dumps( + { + user_text_input["id"]: { + "expert_data": {"points": 1, "comment": "Gut gemacht!"} + }, + } + ).replace('"', '\\"') + + query = f""" + mutation {{ + upsert_assignment_completion( + assignment_id: {self.assignment.id} + assignment_user_id: {self.student.id} + course_session_id: {self.course_session.id} + completion_status: EVALUATION_SUBMITTED + completion_data_string: "{completion_data_string}" + evaluation_grade: 4.5, + evaluation_points: 16, + ) {{ + assignment_completion {{ + id + completion_status + completion_data + assignment_user {{ id }} + assignment {{ id }} + }} + }} + }} + """ + + response = self.query(query) + self.assertResponseNoErrors(response) + + data = json.loads(response.content)["data"]["upsert_assignment_completion"][ + "assignment_completion" + ] + + self.assertEqual(data["assignment_user"]["id"], str(self.student.id)) + self.assertEqual(data["assignment"]["id"], str(self.assignment.id)) + self.assertEqual(data["completion_status"], "EVALUATION_SUBMITTED") + self.assertDictEqual( + data["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.course_session.id, + assignment_id=self.assignment.id, + ) + self.assertEqual(db_entry.completion_status, "EVALUATION_SUBMITTED") + self.assertEqual(db_entry.evaluation_grade, 4.5) + self.assertEqual(db_entry.evaluation_points, 16) + 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!"}, + }, + }, + ) + + # `EVALUATION_SUBMITTED` will create a new AssignmentCompletionAuditLog + acl = AssignmentCompletionAuditLog.objects.get( + assignment_user=self.student, + course_session_id=self.course_session.id, + assignment_id=self.assignment.id, + completion_status="EVALUATION_SUBMITTED", + ) + 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 64e24d77..c523424a 100644 --- a/server/vbv_lernwelt/assignment/tests/test_services.py +++ b/server/vbv_lernwelt/assignment/tests/test_services.py @@ -8,6 +8,7 @@ from vbv_lernwelt.assignment.models import ( Assignment, AssignmentCompletion, AssignmentCompletionAuditLog, + AssignmentCompletionStatus, ) from vbv_lernwelt.assignment.services import update_assignment_completion from vbv_lernwelt.core.create_default_users import create_default_users @@ -161,7 +162,7 @@ class UpdateAssignmentCompletionTestCase(TestCase): assignment_user=self.user, assignment=self.assignment, course_session=self.course_session, - completion_status="SUBMITTED", + completion_status=AssignmentCompletionStatus.SUBMITTED, ) ac = AssignmentCompletion.objects.get( @@ -216,7 +217,7 @@ class UpdateAssignmentCompletionTestCase(TestCase): assignment=self.assignment, course_session=self.course_session, submitted_at=timezone.now(), - completion_status="SUBMITTED", + completion_status=AssignmentCompletionStatus.SUBMITTED.value, completion_data={ user_text_input0["id"]: { "user_data": {"text": "Am Anfang war das Wort... 0"} @@ -232,7 +233,7 @@ class UpdateAssignmentCompletionTestCase(TestCase): assignment_user=self.user, assignment=self.assignment, course_session=self.course_session, - completion_status="SUBMITTED", + completion_status=AssignmentCompletionStatus.SUBMITTED, ) # can submit twice with flag @@ -240,7 +241,7 @@ class UpdateAssignmentCompletionTestCase(TestCase): assignment_user=self.user, assignment=self.assignment, course_session=self.course_session, - completion_status="SUBMITTED", + completion_status=AssignmentCompletionStatus.SUBMITTED, validate_completion_status_change=False, ) @@ -280,7 +281,7 @@ class UpdateAssignmentCompletionTestCase(TestCase): assignment_user=self.user, assignment=self.assignment, course_session=self.course_session, - completion_status="SUBMITTED", + completion_status=AssignmentCompletionStatus.SUBMITTED, copy_task_data=True, ) @@ -306,7 +307,7 @@ class UpdateAssignmentCompletionTestCase(TestCase): assignment_user=self.user, assignment=self.assignment, course_session=self.course_session, - completion_status="SUBMITTED", + completion_status=AssignmentCompletionStatus.SUBMITTED, ) evaluation_task = self.assignment.get_evaluation_tasks()[0] @@ -320,7 +321,7 @@ class UpdateAssignmentCompletionTestCase(TestCase): "expert_data": {"points": 2, "text": "Gut gemacht!"} }, }, - completion_status="EVALUATION_IN_PROGRESS", + completion_status=AssignmentCompletionStatus.EVALUATION_IN_PROGRESS, evaluation_user=self.trainer, ) @@ -352,7 +353,7 @@ class UpdateAssignmentCompletionTestCase(TestCase): assignment_user=self.user, assignment=self.assignment, course_session=self.course_session, - completion_status="SUBMITTED", + completion_status=AssignmentCompletionStatus.SUBMITTED, completion_data={ user_text_input["id"]: { "user_data": {"text": "Ich würde nichts weiteres empfehlen."} @@ -369,7 +370,7 @@ class UpdateAssignmentCompletionTestCase(TestCase): "expert_data": {"points": 1, "comment": "Gut gemacht!"} }, }, - completion_status="EVALUATION_IN_PROGRESS", + completion_status=AssignmentCompletionStatus.EVALUATION_IN_PROGRESS, evaluation_user=self.trainer, ) @@ -404,7 +405,7 @@ class UpdateAssignmentCompletionTestCase(TestCase): assignment_user=self.user, assignment=self.assignment, course_session=self.course_session, - completion_status="IN_PROGRESS", + completion_status=AssignmentCompletionStatus.IN_PROGRESS.value, completion_data={ user_text_input["id"]: { "user_data": {"text": "Ich würde nichts weiteres empfehlen."} @@ -417,7 +418,7 @@ class UpdateAssignmentCompletionTestCase(TestCase): assignment_user=self.user, assignment=self.assignment, course_session=self.course_session, - completion_status="EVALUATION_IN_PROGRESS", + completion_status=AssignmentCompletionStatus.EVALUATION_IN_PROGRESS, completion_data={ user_text_input["id"]: { "expert_data": {"points": 1, "comment": "Gut gemacht!"} @@ -445,7 +446,7 @@ class UpdateAssignmentCompletionTestCase(TestCase): assignment_user=self.user, assignment=self.assignment, course_session=self.course_session, - completion_status="SUBMITTED", + completion_status=AssignmentCompletionStatus.SUBMITTED.value, completion_data={ user_text_input["id"]: { "user_data": {"text": "Ich würde nichts weiteres empfehlen."} @@ -464,7 +465,7 @@ class UpdateAssignmentCompletionTestCase(TestCase): "expert_data": {"points": 2, "text": "Gut gemacht!"} }, }, - completion_status="EVALUATION_IN_PROGRESS", + completion_status=AssignmentCompletionStatus.EVALUATION_IN_PROGRESS, evaluation_user=self.trainer, ) @@ -475,7 +476,7 @@ class UpdateAssignmentCompletionTestCase(TestCase): assignment=self.assignment, course_session=self.course_session, completion_data={}, - completion_status="EVALUATION_SUBMITTED", + completion_status=AssignmentCompletionStatus.EVALUATION_SUBMITTED, evaluation_user=self.trainer, evaluation_grade=None, evaluation_points=None, @@ -486,7 +487,7 @@ class UpdateAssignmentCompletionTestCase(TestCase): assignment=self.assignment, course_session=self.course_session, completion_data={}, - completion_status="EVALUATION_SUBMITTED", + completion_status=AssignmentCompletionStatus.EVALUATION_SUBMITTED, evaluation_user=self.trainer, evaluation_grade=4.5, evaluation_points=16, diff --git a/server/vbv_lernwelt/assignment/views.py b/server/vbv_lernwelt/assignment/views.py index 1b5650bd..9b80dd36 100644 --- a/server/vbv_lernwelt/assignment/views.py +++ b/server/vbv_lernwelt/assignment/views.py @@ -1,69 +1,14 @@ import structlog from rest_framework.decorators import api_view from rest_framework.exceptions import PermissionDenied -from rest_framework.generics import get_object_or_404 from rest_framework.response import Response -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, - has_course_access_by_page_request, - is_course_session_expert, -) +from vbv_lernwelt.course.permissions import is_course_session_expert logger = structlog.get_logger(__name__) -def _request_assignment_completion( - assignment_id, course_session_id, assignment_user_id -): - try: - response_data = AssignmentCompletionSerializer( - AssignmentCompletion.objects.get( - assignment_user_id=assignment_user_id, - assignment_id=assignment_id, - course_session_id=course_session_id, - ), - ).data - - return Response(status=200, data=response_data) - except Exception as e: - logger.error(e) - return Response({"error": str(e)}, status=404) - - -@api_view(["GET"]) -def request_assignment_completion(request, assignment_id, course_session_id): - course_id = get_object_or_404(CourseSession, id=course_session_id).course_id - if has_course_access(request.user, course_id): - return _request_assignment_completion( - assignment_id=assignment_id, - course_session_id=course_session_id, - assignment_user_id=request.user.id, - ) - raise PermissionDenied() - - -@api_view(["GET"]) -def request_assignment_completion_for_user( - request, assignment_id, course_session_id, user_id -): - if request.user.id == user_id or is_course_session_expert( - request.user, course_session_id - ): - return _request_assignment_completion( - assignment_id=assignment_id, - course_session_id=course_session_id, - assignment_user_id=user_id, - ) - raise PermissionDenied() - - @api_view(["GET"]) def request_assignment_completion_status(request, assignment_id, course_session_id): # TODO quickfix before GraphQL... @@ -75,102 +20,3 @@ def request_assignment_completion_status(request, assignment_id, course_session_ return Response(status=200, data=qs) raise PermissionDenied() - - -@api_view(["POST"]) -def upsert_user_assignment_completion(request): - try: - assignment_id = request.data.get("assignment_id") - course_session_id = request.data.get("course_session_id") - completion_status = request.data.get("completion_status", "IN_PROGRESS") - completion_data = request.data.get("completion_data", {}) - - assignment_page = Page.objects.get(id=assignment_id) - - if not has_course_access_by_page_request(request, assignment_page): - raise PermissionDenied() - - assignment = assignment_page.specific - - ac = update_assignment_completion( - assignment_user=request.user, - assignment=assignment, - course_session=CourseSession.objects.get(id=course_session_id), - completion_data=completion_data, - completion_status=completion_status, - copy_task_data=False, - ) - - logger.debug( - "upsert_user_assignment_completion successful", - label="assignment_api", - assignment_id=assignment.id, - assignment_title=assignment.title, - assignment_user_id=request.user.id, - course_session_id=course_session_id, - completion_status=completion_status, - ) - - 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) - - -@api_view(["POST"]) -def evaluate_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", "EVALUATION_IN_PROGRESS" - ) - completion_data = request.data.get("completion_data", {}) - evaluation_grade = request.data.get("evaluation_grade", None) - evaluation_points = request.data.get("evaluation_grade", None) - - 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, - evaluation_user=request.user, - evaluation_grade=evaluation_grade, - evaluation_points=evaluation_points, - ) - - 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, - evaluation_user_id=request.user.id, - evaluation_grade=evaluation_grade, - evaluation_points=evaluation_points, - ) - - 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) diff --git a/server/vbv_lernwelt/core/middleware/graphene.py b/server/vbv_lernwelt/core/middleware/graphene.py index 4128d252..299891ae 100644 --- a/server/vbv_lernwelt/core/middleware/graphene.py +++ b/server/vbv_lernwelt/core/middleware/graphene.py @@ -1,5 +1,3 @@ -import traceback - import structlog from graphene import ResolveInfo @@ -12,5 +10,7 @@ class GrapheneErrorLoggingMiddleware(object): try: return next(root, info, **args) except Exception as error: - logger.error(traceback.format_exc()) + logger.error( + "GraphQL error", label="graphql_error", info=info, exc_info=error + ) raise error