Clean up assignment graphql

* Remove old assignment api REST code which is unused
* Refactor handling AssignmentCompletionStatus enum
* Add full test for GraphQL assignment completion mutation
* Hide wagtail INFO logs on local dev
This commit is contained in:
Daniel Egger 2023-06-28 18:27:37 +02:00
parent 926ecb0ae0
commit 9860a59cef
14 changed files with 525 additions and 531 deletions

View File

@ -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
}

View File

@ -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",

View File

@ -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/<signed_int:assignment_id>/<signed_int:course_session_id>/",
request_assignment_completion,
name="request_assignment_completion"),
path(
r"api/assignment/<signed_int:assignment_id>/<signed_int:course_session_id>/status/",
request_assignment_completion_status,
name="request_assignment_completion_status"),
path(
r"api/assignment/<signed_int:assignment_id>/<signed_int:course_session_id>/<signed_int:user_id>/",
request_assignment_completion_for_user,
name="request_assignment_completion_for_user"),
# documents
path(r'api/core/document/start/', document_upload_start,

View File

@ -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()

View File

@ -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,
),
),
]

View File

@ -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",
),
),
]

View File

@ -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",
)
]

View File

@ -1,3 +0,0 @@
import structlog
logger = structlog.get_logger(__name__)

View File

@ -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),

View File

@ -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!"},
},
},
)

View File

@ -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!"},
},
},
)

View File

@ -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,

View File

@ -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)

View File

@ -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