Merged in feature/VBV-290-kn-backend-part-2 (pull request #64)

Feature/VBV-290 kn backend part 2

Approved-by: Elia Bieri
This commit is contained in:
Daniel Egger 2023-04-25 13:26:10 +00:00
commit 5b7a213f42
11 changed files with 706 additions and 128 deletions

View File

@ -10,9 +10,10 @@ from grapple import urls as grapple_urls
from ratelimit.exceptions import Ratelimited from ratelimit.exceptions import Ratelimited
from vbv_lernwelt.assignment.views import ( from vbv_lernwelt.assignment.views import (
grade_assignment_completion,
request_assignment_completion, request_assignment_completion,
request_assignment_completion_for_user, request_assignment_completion_for_user,
update_assignment_input, upsert_user_assignment_completion,
) )
from vbv_lernwelt.core.middleware.auth import django_view_authentication_exempt from vbv_lernwelt.core.middleware.auth import django_view_authentication_exempt
from vbv_lernwelt.core.views import ( from vbv_lernwelt.core.views import (
@ -99,8 +100,10 @@ urlpatterns = [
name="request_course_completion_for_user"), name="request_course_completion_for_user"),
# assignment # assignment
path(r"api/assignment/update/", update_assignment_input, path(r"api/assignment/upsert/", upsert_user_assignment_completion,
name="update_assignment_input"), name="upsert_user_assignment_completion"),
path(r"api/assignment/grade/", grade_assignment_completion,
name="grade_assignment_completion"),
path(r"api/assignment/<int:assignment_id>/<int:course_session_id>/", path(r"api/assignment/<int:assignment_id>/<int:course_session_id>/",
request_assignment_completion, request_assignment_completion,
name="request_assignment_completion"), name="request_assignment_completion"),

View File

@ -6,14 +6,15 @@ from vbv_lernwelt.core.admin_utils import PrettyJSONWidget
@admin.register(AssignmentCompletion) @admin.register(AssignmentCompletion)
class CourseSessionAdmin(admin.ModelAdmin): class AssignmentCompletionAdmin(admin.ModelAdmin):
formfield_overrides = { formfield_overrides = {
JSONField: {"widget": PrettyJSONWidget(attrs={"rows": 16, "cols": 80})}, JSONField: {"widget": PrettyJSONWidget(attrs={"rows": 16, "cols": 80})},
} }
date_hierarchy = "created_at" date_hierarchy = "created_at"
list_display = [ list_display = [
"id", "id",
"completion_status",
"assignment", "assignment",
"user", "assignment_user",
"course_session", "course_session",
] ]

View File

@ -1,76 +0,0 @@
# Generated by Django 3.2.13 on 2023-04-14 11:33
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("course", "0004_coursesession_assignment_details_list"),
("assignment", "0001_initial"),
]
operations = [
migrations.CreateModel(
name="AssignmentCompletion",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"completion_status",
models.CharField(
choices=[
(1, "in_progress"),
(2, "submitted"),
(3, "grading_in_progress"),
(4, "graded"),
],
default="in_progress",
max_length=255,
),
),
("completion_data", models.JSONField(default=dict)),
("additional_json_data", models.JSONField(default=dict)),
(
"assignment",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="assignment.assignment",
),
),
(
"course_session",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="course.coursesession",
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
],
),
migrations.AddConstraint(
model_name="assignmentcompletion",
constraint=models.UniqueConstraint(
fields=("user", "assignment", "course_session"),
name="assignment_completion_unique_user_assignment_course_session",
),
),
]

View File

@ -0,0 +1,161 @@
# Generated by Django 3.2.13 on 2023-04-25 06:49
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("course", "0004_coursesession_assignment_details_list"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("assignment", "0001_initial"),
]
operations = [
migrations.CreateModel(
name="AssignmentCompletionAuditLog",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
(
"completion_status",
models.CharField(
choices=[
(1, "in_progress"),
(2, "submitted"),
(3, "grading_in_progress"),
(4, "graded"),
],
default="in_progress",
max_length=255,
),
),
("completion_data", models.JSONField(default=dict)),
("additional_json_data", models.JSONField(default=dict)),
("assignment_user_email", models.CharField(max_length=255)),
("assignment_slug", models.CharField(max_length=255)),
(
"grading_user_email",
models.CharField(blank=True, default="", max_length=255),
),
(
"assignment",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="+",
to="assignment.assignment",
),
),
(
"assignment_user",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="+",
to=settings.AUTH_USER_MODEL,
),
),
(
"course_session",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="+",
to="course.coursesession",
),
),
(
"grading_user",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="+",
to=settings.AUTH_USER_MODEL,
),
),
],
),
migrations.CreateModel(
name="AssignmentCompletion",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
("submitted_at", models.DateTimeField(blank=True, null=True)),
("graded_at", models.DateTimeField(blank=True, null=True)),
(
"completion_status",
models.CharField(
choices=[
(1, "in_progress"),
(2, "submitted"),
(3, "grading_in_progress"),
(4, "graded"),
],
default="in_progress",
max_length=255,
),
),
("completion_data", models.JSONField(default=dict)),
("additional_json_data", models.JSONField(default=dict)),
(
"assignment",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="assignment.assignment",
),
),
(
"assignment_user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
(
"course_session",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="course.coursesession",
),
),
(
"grading_user",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="+",
to=settings.AUTH_USER_MODEL,
),
),
],
),
migrations.AddConstraint(
model_name="assignmentcompletion",
constraint=models.UniqueConstraint(
fields=("assignment_user", "assignment", "course_session"),
name="assignment_completion_unique_user_assignment_course_session",
),
),
]

View File

@ -183,7 +183,17 @@ class AssignmentCompletion(models.Model):
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)
user = models.ForeignKey(User, on_delete=models.CASCADE) submitted_at = models.DateTimeField(null=True, blank=True)
graded_at = models.DateTimeField(null=True, blank=True)
grading_user = models.ForeignKey(
User,
on_delete=models.CASCADE,
null=True,
blank=True,
related_name="+",
)
assignment_user = models.ForeignKey(User, on_delete=models.CASCADE)
assignment = models.ForeignKey(Assignment, on_delete=models.CASCADE) assignment = models.ForeignKey(Assignment, on_delete=models.CASCADE)
course_session = models.ForeignKey("course.CourseSession", on_delete=models.CASCADE) course_session = models.ForeignKey("course.CourseSession", on_delete=models.CASCADE)
@ -194,13 +204,46 @@ class AssignmentCompletion(models.Model):
) )
completion_data = models.JSONField(default=dict) completion_data = models.JSONField(default=dict)
additional_json_data = models.JSONField(default=dict) additional_json_data = models.JSONField(default=dict)
class Meta: class Meta:
constraints = [ constraints = [
UniqueConstraint( UniqueConstraint(
fields=["user", "assignment", "course_session"], fields=["assignment_user", "assignment", "course_session"],
name="assignment_completion_unique_user_assignment_course_session", name="assignment_completion_unique_user_assignment_course_session",
) )
] ]
class AssignmentCompletionAuditLog(models.Model):
"""
This model is used to store the "submitted" and "graded" data separately
"""
created_at = models.DateTimeField(auto_now_add=True)
grading_user = models.ForeignKey(
User, on_delete=models.SET_NULL, null=True, blank=True, related_name="+"
)
assignment_user = models.ForeignKey(
User, on_delete=models.SET_NULL, null=True, related_name="+"
)
assignment = models.ForeignKey(
Assignment, on_delete=models.SET_NULL, null=True, related_name="+"
)
course_session = models.ForeignKey(
"course.CourseSession", on_delete=models.SET_NULL, null=True, related_name="+"
)
completion_status = models.CharField(
max_length=255,
choices=[(acs.value, acs.name) for acs in AssignmentCompletionStatus],
default="in_progress",
)
completion_data = models.JSONField(default=dict)
additional_json_data = models.JSONField(default=dict)
assignment_user_email = models.CharField(max_length=255)
assignment_slug = models.CharField(max_length=255)
grading_user_email = models.CharField(max_length=255, blank=True, default="")

View File

@ -10,10 +10,13 @@ class AssignmentCompletionSerializer(serializers.ModelSerializer):
"id", "id",
"created_at", "created_at",
"updated_at", "updated_at",
"user", "submitted_at",
"graded_at",
"assignment_user",
"assignment", "assignment",
"course_session", "course_session",
"completion_status", "completion_status",
"completion_data", "completion_data",
"grading_user",
"additional_json_data", "additional_json_data",
] ]

View File

@ -1,8 +1,13 @@
from copy import deepcopy
from typing import Type from typing import Type
from django.utils import timezone
from rest_framework import serializers
from vbv_lernwelt.assignment.models import ( from vbv_lernwelt.assignment.models import (
Assignment, Assignment,
AssignmentCompletion, AssignmentCompletion,
AssignmentCompletionAuditLog,
AssignmentCompletionStatus, AssignmentCompletionStatus,
) )
from vbv_lernwelt.core.models import User from vbv_lernwelt.core.models import User
@ -11,11 +16,13 @@ from vbv_lernwelt.course.models import CourseSession
def update_assignment_completion( def update_assignment_completion(
user: User, assignment_user: User,
assignment: Assignment, assignment: Assignment,
course_session: CourseSession, course_session: CourseSession,
completion_data=None, completion_data=None,
completion_status: Type[AssignmentCompletionStatus] = "in_progress", completion_status: Type[AssignmentCompletionStatus] = "in_progress",
grading_user: User | None = None,
validate_completion_status_change: bool = True,
copy_task_data: bool = False, copy_task_data: bool = False,
) -> AssignmentCompletion: ) -> AssignmentCompletion:
""" """
@ -29,7 +36,7 @@ def update_assignment_completion(
it can also contain "trainer_input" when the trainer has entered grading data it can also contain "trainer_input" when the trainer has entered grading data
{ {
"<user_text_input:uuid>": { "<user_text_input:uuid>": {
"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 :param copy_task_data: if true, the task data will be copied to the completion data
@ -41,11 +48,40 @@ def update_assignment_completion(
completion_data = {} completion_data = {}
ac, created = AssignmentCompletion.objects.get_or_create( ac, created = AssignmentCompletion.objects.get_or_create(
user_id=user.id, assignment_user_id=assignment_user.id,
assignment_id=assignment.id, assignment_id=assignment.id,
course_session_id=course_session.id, course_session_id=course_session.id,
) )
if validate_completion_status_change:
# TODO: check time?
if completion_status == "submitted":
if ac.completion_status in ["submitted", "grading_in_progress", "graded"]:
raise serializers.ValidationError(
{
"completion_status": f"Cannot update completion status from {ac.completion_status} to submitted"
}
)
elif completion_status == "graded":
if ac.completion_status == "graded":
raise serializers.ValidationError(
{
"completion_status": f"Cannot update completion status from {ac.completion_status} to graded"
}
)
if completion_status in ["graded", "grading_in_progress"]:
if grading_user is None:
raise serializers.ValidationError(
{"grading_user": "grading_user is required for graded status"}
)
ac.grading_user = grading_user
if completion_status == "submitted":
ac.submitted_at = timezone.now()
elif completion_status == "graded":
ac.graded_at = timezone.now()
ac.completion_status = completion_status ac.completion_status = completion_status
# TODO: make more validation of the provided input -> maybe with graphql # TODO: make more validation of the provided input -> maybe with graphql
@ -65,6 +101,27 @@ def update_assignment_completion(
ac.save() ac.save()
if completion_status in ["graded", "submitted"]:
acl = AssignmentCompletionAuditLog.objects.create(
assignment_user=assignment_user,
assignment=assignment,
course_session=course_session,
grading_user=grading_user,
completion_status=completion_status,
assignment_user_email=assignment_user.email,
assignment_slug=assignment.slug,
completion_data=deepcopy(ac.completion_data),
)
if grading_user:
acl.grading_user_email = grading_user.email
# copy over the question data, so that we don't lose the context
substasks = assignment.filter_user_subtasks()
for key, value in acl.completion_data.items():
task_data = find_first(substasks, pred=lambda x: x["id"] == key)
acl.completion_data[key].update(task_data)
acl.save()
return ac return ac

View File

@ -1,8 +1,13 @@
import json import json
from django.utils import timezone
from rest_framework.test import APITestCase 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.create_default_users import create_default_users
from vbv_lernwelt.core.models import User from vbv_lernwelt.core.models import User
from vbv_lernwelt.core.utils import find_first 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 from vbv_lernwelt.course.models import CourseSession, CourseSessionUser
class AssignmentApiStudentTestCase(APITestCase): class AssignmentApiTestCase(APITestCase):
def setUp(self) -> None: def setUp(self) -> None:
create_default_users() create_default_users()
create_test_course(include_vv=False) create_test_course(include_vv=False)
@ -23,15 +28,20 @@ class AssignmentApiStudentTestCase(APITestCase):
course_id=COURSE_TEST_ID, course_id=COURSE_TEST_ID,
title="Test Lehrgang Session", title="Test Lehrgang Session",
) )
self.user = User.objects.get(username="student") self.student = User.objects.get(username="student")
csu = CourseSessionUser.objects.create( self.student_csu = CourseSessionUser.objects.create(
course_session=self.cs, course_session=self.cs,
user=self.user, user=self.student,
) )
self.client.login(username="student", password="test")
def test_can_updateAssignmentCompletion_asStudent(self): self.expert = User.objects.get(username="admin")
url = f"/api/assignment/update/" 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( user_text_input = find_first(
self.assignment_subtasks, pred=lambda x: x["type"] == "user_text_input" 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)) print(json.dumps(response.json(), indent=2))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response_json["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["assignment"], self.assignment.id)
self.assertEqual(response_json["completion_status"], "in_progress") self.assertEqual(response_json["completion_status"], "in_progress")
self.assertDictEqual( self.assertDictEqual(
@ -64,7 +74,7 @@ class AssignmentApiStudentTestCase(APITestCase):
) )
db_entry = AssignmentCompletion.objects.get( db_entry = AssignmentCompletion.objects.get(
user=self.user, assignment_user=self.student,
course_session_id=self.cs.id, course_session_id=self.cs.id,
assignment_id=self.assignment.id, assignment_id=self.assignment.id,
) )
@ -89,3 +99,198 @@ class AssignmentApiStudentTestCase(APITestCase):
user_text_input["id"]: {"user_data": {"text": "Hallo via API"}}, 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/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!"},
},
},
)

View File

@ -1,7 +1,14 @@
from datetime import date
from django.test import TestCase from django.test import TestCase
from django.utils import timezone
from rest_framework import serializers
from vbv_lernwelt.assignment.creators.create_assignments import create_test_assignment from vbv_lernwelt.assignment.creators.create_assignments import create_test_assignment
from vbv_lernwelt.assignment.models import AssignmentCompletion from vbv_lernwelt.assignment.models import (
AssignmentCompletion,
AssignmentCompletionAuditLog,
)
from vbv_lernwelt.assignment.services import update_assignment_completion from vbv_lernwelt.assignment.services import update_assignment_completion
from vbv_lernwelt.core.create_default_users import create_default_users from vbv_lernwelt.core.create_default_users import create_default_users
from vbv_lernwelt.core.models import User from vbv_lernwelt.core.models import User
@ -26,6 +33,7 @@ class UpdateAssignmentCompletionTestCase(TestCase):
title="Bern 2022 a", title="Bern 2022 a",
) )
self.user = User.objects.get(username="student") self.user = User.objects.get(username="student")
self.trainer = User.objects.get(username="admin")
def test_can_store_new_user_input(self): def test_can_store_new_user_input(self):
subtasks = self.assignment.filter_user_subtasks() subtasks = self.assignment.filter_user_subtasks()
@ -37,7 +45,7 @@ class UpdateAssignmentCompletionTestCase(TestCase):
) )
update_assignment_completion( update_assignment_completion(
user=self.user, assignment_user=self.user,
assignment=self.assignment, assignment=self.assignment,
course_session=self.course_session, course_session=self.course_session,
completion_data={ completion_data={
@ -49,7 +57,7 @@ class UpdateAssignmentCompletionTestCase(TestCase):
) )
ac = AssignmentCompletion.objects.get( ac = AssignmentCompletion.objects.get(
user=self.user, assignment_user=self.user,
assignment=self.assignment, assignment=self.assignment,
course_session=self.course_session, course_session=self.course_session,
) )
@ -71,7 +79,7 @@ class UpdateAssignmentCompletionTestCase(TestCase):
user_text_input1 = subtasks[1] user_text_input1 = subtasks[1]
ac = AssignmentCompletion.objects.create( ac = AssignmentCompletion.objects.create(
user=self.user, assignment_user=self.user,
assignment=self.assignment, assignment=self.assignment,
course_session=self.course_session, course_session=self.course_session,
completion_data={ completion_data={
@ -85,7 +93,7 @@ class UpdateAssignmentCompletionTestCase(TestCase):
) )
update_assignment_completion( update_assignment_completion(
user=self.user, assignment_user=self.user,
assignment=self.assignment, assignment=self.assignment,
course_session=self.course_session, course_session=self.course_session,
completion_data={ completion_data={
@ -96,7 +104,7 @@ class UpdateAssignmentCompletionTestCase(TestCase):
) )
ac = AssignmentCompletion.objects.get( ac = AssignmentCompletion.objects.get(
user=self.user, assignment_user=self.user,
assignment=self.assignment, assignment=self.assignment,
course_session=self.course_session, course_session=self.course_session,
) )
@ -109,7 +117,7 @@ class UpdateAssignmentCompletionTestCase(TestCase):
random_uuid = "7b60903b-d2a5-4798-b6cd-5b51e63e98ab" random_uuid = "7b60903b-d2a5-4798-b6cd-5b51e63e98ab"
update_assignment_completion( update_assignment_completion(
user=self.user, assignment_user=self.user,
assignment=self.assignment, assignment=self.assignment,
course_session=self.course_session, course_session=self.course_session,
completion_data={ completion_data={
@ -120,13 +128,13 @@ class UpdateAssignmentCompletionTestCase(TestCase):
) )
ac = AssignmentCompletion.objects.get( ac = AssignmentCompletion.objects.get(
user=self.user, assignment_user=self.user,
assignment=self.assignment, assignment=self.assignment,
course_session=self.course_session, course_session=self.course_session,
) )
self.assertEqual(ac.completion_data, {}) self.assertEqual(ac.completion_data, {})
def test_change_completion_status(self): def test_completion_status_submitted(self):
subtasks = self.assignment.filter_user_subtasks( subtasks = self.assignment.filter_user_subtasks(
subtask_types=["user_text_input"] subtask_types=["user_text_input"]
) )
@ -134,7 +142,7 @@ class UpdateAssignmentCompletionTestCase(TestCase):
user_text_input1 = subtasks[1] user_text_input1 = subtasks[1]
ac = AssignmentCompletion.objects.create( ac = AssignmentCompletion.objects.create(
user=self.user, assignment_user=self.user,
assignment=self.assignment, assignment=self.assignment,
course_session=self.course_session, course_session=self.course_session,
completion_data={ completion_data={
@ -148,19 +156,97 @@ class UpdateAssignmentCompletionTestCase(TestCase):
) )
update_assignment_completion( update_assignment_completion(
user=self.user, assignment_user=self.user,
assignment=self.assignment, assignment=self.assignment,
course_session=self.course_session, course_session=self.course_session,
completion_status="submitted", completion_status="submitted",
) )
ac = AssignmentCompletion.objects.get( ac = AssignmentCompletion.objects.get(
user=self.user, assignment_user=self.user,
assignment=self.assignment, assignment=self.assignment,
course_session=self.course_session, course_session=self.course_session,
) )
self.assertEqual(ac.completion_status, "submitted") self.assertEqual(ac.completion_status, "submitted")
self.assertEqual(ac.submitted_at.date(), date.today())
# will create AssignmentCompletionAuditLog entry
acl = AssignmentCompletionAuditLog.objects.get(
assignment_user=self.user,
assignment=self.assignment,
course_session=self.course_session,
completion_status="submitted",
)
self.assertEqual(acl.created_at.date(), date.today())
self.assertEqual(acl.assignment_user_email, "student")
self.assertEqual(
acl.assignment_slug,
"versicherungsvermittler-in-assignment-überprüfen-einer-motorfahrzeugs-versicherungspolice",
)
# AssignmentCompletionAuditLog entry will remain event after deletion of foreign keys
ac.delete()
self.user.delete()
self.assignment.delete()
acl = AssignmentCompletionAuditLog.objects.get(id=acl.id)
self.assertEqual(acl.created_at.date(), date.today())
self.assertEqual(acl.assignment_user_email, "student")
self.assertEqual(
acl.assignment_slug,
"versicherungsvermittler-in-assignment-überprüfen-einer-motorfahrzeugs-versicherungspolice",
)
self.assertIsNone(acl.assignment_user)
self.assertIsNone(acl.assignment)
def test_completion_status_submitted_cannot_submit_twice(self):
subtasks = self.assignment.filter_user_subtasks(
subtask_types=["user_text_input"]
)
user_text_input0 = subtasks[0]
user_text_input1 = subtasks[1]
ac = AssignmentCompletion.objects.create(
assignment_user=self.user,
assignment=self.assignment,
course_session=self.course_session,
submitted_at=timezone.now(),
completion_status="submitted",
completion_data={
user_text_input0["id"]: {
"user_data": {"text": "Am Anfang war das Wort... 0"}
},
user_text_input1["id"]: {
"user_data": {"text": "Am Anfang war das Wort... 1"}
},
},
)
with self.assertRaises(serializers.ValidationError) as error:
update_assignment_completion(
assignment_user=self.user,
assignment=self.assignment,
course_session=self.course_session,
completion_status="submitted",
)
# can submit twice with flag
update_assignment_completion(
assignment_user=self.user,
assignment=self.assignment,
course_session=self.course_session,
completion_status="submitted",
validate_completion_status_change=False,
)
ac = AssignmentCompletion.objects.get(
assignment_user=self.user,
assignment=self.assignment,
course_session=self.course_session,
)
self.assertEqual(ac.completion_status, "submitted")
self.assertEqual(ac.submitted_at.date(), date.today())
def test_copy_task_data(self): def test_copy_task_data(self):
subtasks = self.assignment.filter_user_subtasks( subtasks = self.assignment.filter_user_subtasks(
@ -175,7 +261,7 @@ class UpdateAssignmentCompletionTestCase(TestCase):
) )
ac = AssignmentCompletion.objects.create( ac = AssignmentCompletion.objects.create(
user=self.user, assignment_user=self.user,
assignment=self.assignment, assignment=self.assignment,
course_session=self.course_session, course_session=self.course_session,
completion_data={ completion_data={
@ -186,7 +272,7 @@ class UpdateAssignmentCompletionTestCase(TestCase):
) )
update_assignment_completion( update_assignment_completion(
user=self.user, assignment_user=self.user,
assignment=self.assignment, assignment=self.assignment,
course_session=self.course_session, course_session=self.course_session,
completion_status="submitted", completion_status="submitted",
@ -194,7 +280,7 @@ class UpdateAssignmentCompletionTestCase(TestCase):
) )
ac = AssignmentCompletion.objects.get( ac = AssignmentCompletion.objects.get(
user=self.user, assignment_user=self.user,
assignment=self.assignment, assignment=self.assignment,
course_session=self.course_session, course_session=self.course_session,
) )
@ -210,7 +296,7 @@ class UpdateAssignmentCompletionTestCase(TestCase):
) )
) )
def test_can_add_trainer_data_without_loosing_user_input_data(self): def test_can_add_grading_data_without_loosing_user_input_data(self):
subtasks = self.assignment.filter_user_subtasks( subtasks = self.assignment.filter_user_subtasks(
subtask_types=["user_text_input"] subtask_types=["user_text_input"]
) )
@ -223,9 +309,10 @@ class UpdateAssignmentCompletionTestCase(TestCase):
) )
ac = AssignmentCompletion.objects.create( ac = AssignmentCompletion.objects.create(
user=self.user, assignment_user=self.user,
assignment=self.assignment, assignment=self.assignment,
course_session=self.course_session, course_session=self.course_session,
completion_status="submitted",
completion_data={ completion_data={
user_text_input["id"]: { user_text_input["id"]: {
"user_data": {"text": "Ich würde nichts weiteres empfehlen."} "user_data": {"text": "Ich würde nichts weiteres empfehlen."}
@ -234,19 +321,20 @@ class UpdateAssignmentCompletionTestCase(TestCase):
) )
update_assignment_completion( update_assignment_completion(
user=self.user, assignment_user=self.user,
assignment=self.assignment, assignment=self.assignment,
course_session=self.course_session, course_session=self.course_session,
completion_data={ completion_data={
user_text_input["id"]: { user_text_input["id"]: {
"trainer_data": {"points": 1, "comment": "Gut gemacht!"} "expert_data": {"points": 1, "comment": "Gut gemacht!"}
}, },
}, },
completion_status="grading_in_progress", completion_status="grading_in_progress",
grading_user=self.trainer,
) )
ac = AssignmentCompletion.objects.get( ac = AssignmentCompletion.objects.get(
user=self.user, assignment_user=self.user,
assignment=self.assignment, assignment=self.assignment,
course_session=self.course_session, course_session=self.course_session,
) )
@ -254,8 +342,49 @@ class UpdateAssignmentCompletionTestCase(TestCase):
self.assertEqual(ac.completion_status, "grading_in_progress") self.assertEqual(ac.completion_status, "grading_in_progress")
user_input = ac.completion_data[user_text_input["id"]] user_input = ac.completion_data[user_text_input["id"]]
self.assertDictEqual( self.assertDictEqual(
user_input["trainer_data"], {"points": 1, "comment": "Gut gemacht!"} user_input["expert_data"], {"points": 1, "comment": "Gut gemacht!"}
) )
self.assertEqual( self.assertEqual(
user_input["user_data"]["text"], "Ich würde nichts weiteres empfehlen." user_input["user_data"]["text"], "Ich würde nichts weiteres empfehlen."
) )
def test_cannot_grading_data_without_grading_user(self):
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.user,
assignment=self.assignment,
course_session=self.course_session,
completion_status="in_progress",
completion_data={
user_text_input["id"]: {
"user_data": {"text": "Ich würde nichts weiteres empfehlen."}
},
},
)
with self.assertRaises(serializers.ValidationError) as error:
update_assignment_completion(
assignment_user=self.user,
assignment=self.assignment,
course_session=self.course_session,
completion_status="grading_in_progress",
completion_data={
user_text_input["id"]: {
"expert_data": {"points": 1, "comment": "Gut gemacht!"}
},
},
)
self.assertTrue(
"grading_user" in error.exception.detail,
)

View File

@ -8,6 +8,7 @@ from wagtail.models import Page
from vbv_lernwelt.assignment.models import AssignmentCompletion from vbv_lernwelt.assignment.models import AssignmentCompletion
from vbv_lernwelt.assignment.serializers import AssignmentCompletionSerializer from vbv_lernwelt.assignment.serializers import AssignmentCompletionSerializer
from vbv_lernwelt.assignment.services import update_assignment_completion 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.models import CourseSession
from vbv_lernwelt.course.permissions import ( from vbv_lernwelt.course.permissions import (
has_course_access, has_course_access,
@ -18,11 +19,13 @@ from vbv_lernwelt.course.permissions import (
logger = structlog.get_logger(__name__) logger = structlog.get_logger(__name__)
def _request_assignment_completion(assignment_id, course_session_id, user_id): def _request_assignment_completion(
assignment_id, course_session_id, assignment_user_id
):
try: try:
response_data = AssignmentCompletionSerializer( response_data = AssignmentCompletionSerializer(
AssignmentCompletion.objects.get( AssignmentCompletion.objects.get(
user_id=user_id, assignment_user_id=assignment_user_id,
assignment_id=assignment_id, assignment_id=assignment_id,
course_session_id=course_session_id, course_session_id=course_session_id,
), ),
@ -41,7 +44,7 @@ def request_assignment_completion(request, assignment_id, course_session_id):
return _request_assignment_completion( return _request_assignment_completion(
assignment_id=assignment_id, assignment_id=assignment_id,
course_session_id=course_session_id, course_session_id=course_session_id,
user_id=request.user.id, assignment_user_id=request.user.id,
) )
raise PermissionDenied() raise PermissionDenied()
@ -56,13 +59,13 @@ def request_assignment_completion_for_user(
return _request_assignment_completion( return _request_assignment_completion(
assignment_id=assignment_id, assignment_id=assignment_id,
course_session_id=course_session_id, course_session_id=course_session_id,
user_id=user_id, assignment_user_id=user_id,
) )
raise PermissionDenied() raise PermissionDenied()
@api_view(["POST"]) @api_view(["POST"])
def update_assignment_input(request): def upsert_user_assignment_completion(request):
try: try:
assignment_id = request.data.get("assignment_id") assignment_id = request.data.get("assignment_id")
course_session_id = request.data.get("course_session_id") course_session_id = request.data.get("course_session_id")
@ -77,7 +80,7 @@ def update_assignment_input(request):
assignment = assignment_page.specific assignment = assignment_page.specific
ac = update_assignment_completion( ac = update_assignment_completion(
user=request.user, assignment_user=request.user,
assignment=assignment, assignment=assignment,
course_session=CourseSession.objects.get(id=course_session_id), course_session=CourseSession.objects.get(id=course_session_id),
completion_data=completion_data, completion_data=completion_data,
@ -86,11 +89,11 @@ def update_assignment_input(request):
) )
logger.debug( logger.debug(
"store_assignment_input successful", "upsert_user_assignment_completion successful",
label="assignment_api", label="assignment_api",
assignment_id=assignment.id, assignment_id=assignment.id,
assignment_title=assignment.title, assignment_title=assignment.title,
user_id=request.user.id, assignment_user_id=request.user.id,
course_session_id=course_session_id, course_session_id=course_session_id,
completion_status=completion_status, completion_status=completion_status,
) )
@ -101,3 +104,52 @@ def update_assignment_input(request):
except Exception as e: except Exception as e:
logger.error(e, exc_info=True) logger.error(e, exc_info=True)
return Response({"error": str(e)}, status=404) 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)

View File

@ -311,7 +311,7 @@ def create_course_uk_de_assignment_completion_data(assignment, course_session, u
user_text = f"Lorem ipsum dolor sit amet... {index}" user_text = f"Lorem ipsum dolor sit amet... {index}"
update_assignment_completion( update_assignment_completion(
user=user, assignment_user=user,
assignment=assignment, assignment=assignment,
course_session=course_session, course_session=course_session,
completion_data={ completion_data={