Merged in feature/VBV-290-kn-backend (pull request #59)

Feature/VBV-290 kn backend

* Add initial assignment completion model

* Add first version of `update_assignment_completion`

* Upgrade wagtail>=4 for new functions needed in assignment api

* Add API to update assignment user data

* Post API via assignment not learning_content

* Add GET api endpoints for AssignmentCompletion

* Add some initial assignment completion data

* Add admin view for AssignmentCompletion


Approved-by: Elia Bieri
This commit is contained in:
Daniel Egger 2023-04-18 08:07:29 +00:00 committed by Elia Bieri
parent 25bf90cefd
commit adc61479fc
18 changed files with 808 additions and 83 deletions

View File

@ -9,6 +9,11 @@ from django.views import defaults as default_views
from grapple import urls as grapple_urls
from ratelimit.exceptions import Ratelimited
from vbv_lernwelt.assignment.views import (
request_assignment_completion,
request_assignment_completion_for_user,
update_assignment_input,
)
from vbv_lernwelt.core.middleware.auth import django_view_authentication_exempt
from vbv_lernwelt.core.views import (
check_rate_limit,
@ -93,6 +98,16 @@ urlpatterns = [
request_course_completion_for_user,
name="request_course_completion_for_user"),
# assignment
path(r"api/assignment/update/", update_assignment_input,
name="update_assignment_input"),
path(r"api/assignment/<int:assignment_id>/<int:course_session_id>/",
request_assignment_completion,
name="request_assignment_completion"),
path(r"api/assignment/<int:assignment_id>/<int:course_session_id>/<int:user_id>/",
request_assignment_completion_for_user,
name="request_assignment_completion_for_user"),
# documents
path(r'api/core/document/start/', document_upload_start,
name='file_upload_start'),

View File

@ -4,7 +4,7 @@
#
# pip-compile --output-file=requirements-dev.txt requirements-dev.in
#
aniso8601==7.0.0
aniso8601==9.0.1
# via graphene
anyascii==0.3.1
# via wagtail
@ -205,18 +205,19 @@ gitdb2==4.0.2
# via gitpython
gitpython==3.0.6
# via trufflehog
graphene==2.1.9
graphene==3.2.2
# via graphene-django
graphene-django==2.15.0
graphene-django==3.0.0
# via wagtail-grapple
graphql-core==2.3.2
graphql-core==3.2.3
# via
# graphene
# graphene-django
# graphql-relay
# wagtail-grapple
graphql-relay==2.0.1
# via graphene
graphql-relay==3.2.0
# via
# graphene
# graphene-django
gunicorn==20.1.0
# via -r requirements.in
h11==0.13.0
@ -291,8 +292,8 @@ mypy-extensions==0.4.3
# typing-inspect
nodeenv==1.6.0
# via pre-commit
openpyxl==3.0.9
# via tablib
openpyxl==3.1.2
# via wagtail
packaging==21.3
# via
# build
@ -332,10 +333,7 @@ portalocker==2.4.0
pre-commit==2.17.0
# via -r requirements-dev.in
promise==2.3
# via
# graphene-django
# graphql-core
# graphql-relay
# via graphene-django
prompt-toolkit==3.0.28
# via ipython
psycopg2-binary==2.9.3
@ -414,29 +412,20 @@ requests==2.27.1
# coreapi
# djangorestframework-stubs
# wagtail
rx==1.6.1
# via graphql-core
s3transfer==0.6.0
# via boto3
sendgrid==6.9.7
# via -r requirements.in
sentry-sdk==1.5.8
# via -r requirements.in
singledispatch==3.7.0
# via graphene-django
six==1.16.0
# via
# asttokens
# django-coverage-plugin
# graphene
# graphene-django
# graphql-core
# graphql-relay
# html5lib
# l18n
# promise
# python-dateutil
# singledispatch
# virtualenv
smmap==5.0.0
# via gitdb
@ -458,8 +447,6 @@ structlog==21.5.0
# via -r requirements.in
swapper==1.3.0
# via django-notifications-hq
tablib[xls,xlsx]==3.2.1
# via wagtail
telepath==0.2
# via wagtail
termcolor==1.1.0
@ -533,7 +520,7 @@ uvloop==0.16.0
# via uvicorn
virtualenv==20.14.0
# via pre-commit
wagtail==3.0.1
wagtail==4.2.2
# via
# -r requirements.in
# wagtail-factories
@ -542,11 +529,11 @@ wagtail==3.0.1
# wagtail-localize
wagtail-factories==4.0.0
# via -r requirements.in
wagtail-grapple==0.18.0
wagtail-grapple==0.19.2
# via -r requirements.in
wagtail-headless-preview==0.4.0
# via wagtail-grapple
wagtail-localize==1.2.1
wagtail-localize==1.5
# via -r requirements.in
watchfiles==0.17.0
# via
@ -568,12 +555,6 @@ wrapt==1.14.0
# via
# astroid
# deprecated
xlrd==2.0.1
# via tablib
xlsxwriter==3.0.3
# via wagtail
xlwt==1.3.0
# via tablib
# The following packages are considered to be unsafe in a requirements file:
# pip

View File

@ -36,9 +36,9 @@ structlog
python-json-logger
concurrent-log-handler
wagtail>=3,<4
wagtail>=4
wagtail-factories>=4
wagtail-localize
wagtail_grapple
wagtail-localize>=1.5
wagtail_grapple>=0.19.2
boto3

View File

@ -4,7 +4,7 @@
#
# pip-compile --output-file=requirements.txt requirements.in
#
aniso8601==7.0.0
aniso8601==9.0.1
# via graphene
anyascii==0.3.1
# via wagtail
@ -124,18 +124,19 @@ factory-boy==3.2.1
# via wagtail-factories
faker==13.11.1
# via factory-boy
graphene==2.1.9
graphene==3.2.2
# via graphene-django
graphene-django==2.15.0
graphene-django==3.0.0
# via wagtail-grapple
graphql-core==2.3.2
graphql-core==3.2.3
# via
# graphene
# graphene-django
# graphql-relay
# wagtail-grapple
graphql-relay==2.0.1
# via graphene
graphql-relay==3.2.0
# via
# graphene
# graphene-django
gunicorn==20.1.0
# via -r requirements.in
h11==0.13.0
@ -162,8 +163,8 @@ l18n==2021.3
# via wagtail
marshmallow==3.15.0
# via environs
openpyxl==3.0.9
# via tablib
openpyxl==3.1.2
# via wagtail
packaging==21.3
# via
# marshmallow
@ -177,10 +178,7 @@ polib==1.1.1
portalocker==2.4.0
# via concurrent-log-handler
promise==2.3
# via
# graphene-django
# graphql-core
# graphql-relay
# via graphene-django
psycopg2-binary==2.9.3
# via -r requirements.in
pycparser==2.21
@ -221,27 +219,18 @@ redis==4.2.1
# django-redis
requests==2.27.1
# via wagtail
rx==1.6.1
# via graphql-core
s3transfer==0.6.0
# via boto3
sendgrid==6.9.7
# via -r requirements.in
sentry-sdk==1.5.8
# via -r requirements.in
singledispatch==3.7.0
# via graphene-django
six==1.16.0
# via
# graphene
# graphene-django
# graphql-core
# graphql-relay
# html5lib
# l18n
# promise
# python-dateutil
# singledispatch
sniffio==1.2.0
# via anyio
soupsieve==2.3.2.post1
@ -254,8 +243,6 @@ structlog==21.5.0
# via -r requirements.in
swapper==1.3.0
# via django-notifications-hq
tablib[xls,xlsx]==3.2.1
# via wagtail
telepath==0.2
# via wagtail
text-unidecode==1.3
@ -275,7 +262,7 @@ uvicorn[standard]==0.18.3
# via -r requirements.in
uvloop==0.16.0
# via uvicorn
wagtail==3.0.1
wagtail==4.2.2
# via
# -r requirements.in
# wagtail-factories
@ -284,11 +271,11 @@ wagtail==3.0.1
# wagtail-localize
wagtail-factories==4.0.0
# via -r requirements.in
wagtail-grapple==0.18.0
wagtail-grapple==0.19.2
# via -r requirements.in
wagtail-headless-preview==0.4.0
# via wagtail-grapple
wagtail-localize==1.2.1
wagtail-localize==1.5
# via -r requirements.in
watchfiles==0.17.0
# via uvicorn
@ -302,12 +289,6 @@ willow==1.4.1
# via wagtail
wrapt==1.14.0
# via deprecated
xlrd==2.0.1
# via tablib
xlsxwriter==3.0.3
# via wagtail
xlwt==1.3.0
# via tablib
# The following packages are considered to be unsafe in a requirements file:
# setuptools

View File

@ -1,3 +1,19 @@
from django.contrib import admin
from django.db.models import JSONField
# Register your models here.
from vbv_lernwelt.assignment.models import AssignmentCompletion
from vbv_lernwelt.core.admin_utils import PrettyJSONWidget
@admin.register(AssignmentCompletion)
class CourseSessionAdmin(admin.ModelAdmin):
formfield_overrides = {
JSONField: {"widget": PrettyJSONWidget(attrs={"rows": 16, "cols": 80})},
}
date_hierarchy = "created_at"
list_display = [
"id",
"assignment",
"user",
"course_session",
]

View File

@ -285,7 +285,7 @@ def create_uk_assignments(course_id=COURSE_UK):
assignment.save()
def create_test_assignments(course_id=COURSE_TEST_ID):
def create_test_assignment(course_id=COURSE_TEST_ID):
course_page = CoursePage.objects.get(course_id=course_id)
assignment_page = AssignmentListPageFactory(
parent=course_page,
@ -555,3 +555,5 @@ def create_test_assignments(course_id=COURSE_TEST_ID):
)
assignment.save()
return assignment

View File

@ -0,0 +1,76 @@
# 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

@ -1,4 +1,7 @@
from enum import Enum
from django.db import models
from django.db.models import UniqueConstraint
from slugify import slugify
from wagtail import blocks
from wagtail.admin.panels import FieldPanel
@ -6,6 +9,7 @@ from wagtail.fields import StreamField
from wagtail.models import Page
from vbv_lernwelt.core.model_utils import find_available_slug
from vbv_lernwelt.core.models import User
from vbv_lernwelt.course.models import CourseBasePage
@ -42,7 +46,7 @@ class PerformanceObjectiveBlock(blocks.StructBlock):
icon = "tick"
class UserTextInputBlock(blocks.StaticBlock):
class UserTextInputBlock(blocks.StructBlock):
text = blocks.TextBlock(blank=True)
class Meta:
@ -135,3 +139,68 @@ class Assignment(CourseBasePage):
ignore_page_id=self.id,
)
super(Assignment, self).save(clean, user, log_action, **kwargs)
def filter_user_subtasks(self, subtask_types=None):
"""
Filters out all the subtasks which require user input
:param subtask_types:
:return: list of subtasks with the shape: [
{
"id": "<uuid>",
"type": "user_confirmation",
"value": {
"text": "Ja, ich habe Motorfahrzeugversicherungspolice..."
}
},
{
"id": "<uuid>",
"type": "user_text_input",
"value": {
"text": "Gibt es zusätzliche Deckungen, die du der Person empfehlen..."
},
]
"""
if subtask_types is None:
subtask_types = ["user_text_input", "user_confirmation"]
raw_tasks = self.tasks.raw_data
return [
sub_dict
for task_dict in raw_tasks
for sub_dict in task_dict["value"]["content"]
if sub_dict["type"] in subtask_types
]
AssignmentCompletionStatus = Enum(
"AssignmentCompletionStatus",
["in_progress", "submitted", "grading_in_progress", "graded"],
)
class AssignmentCompletion(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
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)
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)
class Meta:
constraints = [
UniqueConstraint(
fields=["user", "assignment", "course_session"],
name="assignment_completion_unique_user_assignment_course_session",
)
]

View File

@ -0,0 +1,19 @@
from rest_framework import serializers
from vbv_lernwelt.assignment.models import AssignmentCompletion
class AssignmentCompletionSerializer(serializers.ModelSerializer):
class Meta:
model = AssignmentCompletion
fields = [
"id",
"created_at",
"updated_at",
"user",
"assignment",
"course_session",
"completion_status",
"completion_data",
"additional_json_data",
]

View File

@ -0,0 +1,83 @@
from typing import Type
from vbv_lernwelt.assignment.models import (
Assignment,
AssignmentCompletion,
AssignmentCompletionStatus,
)
from vbv_lernwelt.core.models import User
from vbv_lernwelt.core.utils import find_first
from vbv_lernwelt.course.models import CourseSession
def update_assignment_completion(
user: User,
assignment: Assignment,
course_session: CourseSession,
completion_data=None,
completion_status: Type[AssignmentCompletionStatus] = "in_progress",
copy_task_data: bool = False,
) -> AssignmentCompletion:
"""
:param completion_data: should have the following structure:
{
"<user_text_input:uuid>": {"user_data": {"text": "some text from user"}},
"<user_confirmation:uuid>": {"user_data": {"confirmation": true}},
}
every input field has the data stored in sub dict of the question uuid
it can also contain "trainer_input" when the trainer has entered grading data
{
"<user_text_input:uuid>": {
"trainer_data": {"points": 4, "text": "Gute Antwort"}
},
}
:param copy_task_data: if true, the task data will be copied to the completion data
used for "submitted" and "graded" status, so that we don't lose the question
context
:return: AssignmentCompletion
"""
if completion_data is None:
completion_data = {}
ac, created = AssignmentCompletion.objects.get_or_create(
user_id=user.id,
assignment_id=assignment.id,
course_session_id=course_session.id,
)
ac.completion_status = completion_status
# TODO: make more validation of the provided input -> maybe with graphql
completion_data = _remove_unknown_entries(assignment, completion_data)
for key, value in completion_data.items():
# retain already stored data
stored_entry = ac.completion_data.get(key, {})
stored_entry.update(value)
ac.completion_data[key] = stored_entry
if copy_task_data:
# copy over the question data, so that we don't lose the context
substasks = assignment.filter_user_subtasks()
for key, value in ac.completion_data.items():
task_data = find_first(substasks, pred=lambda x: x["id"] == key)
ac.completion_data[key].update(task_data)
ac.save()
return ac
def _remove_unknown_entries(assignment, completion_data):
"""
Removes all entries from completion_data which are not known to the assignment
"""
possible_subtask_uuids = [
subtask["id"] for subtask in assignment.filter_user_subtasks()
]
filtered_completion_data = {
key: value
for key, value in completion_data.items()
if key in possible_subtask_uuids
}
return filtered_completion_data

View File

@ -0,0 +1,91 @@
import json
from rest_framework.test import APITestCase
from vbv_lernwelt.assignment.models import Assignment, AssignmentCompletion
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 AssignmentApiStudentTestCase(APITestCase):
def setUp(self) -> None:
create_default_users()
create_test_course(include_vv=False)
self.assignment = Assignment.objects.first()
self.assignment_subtasks = self.assignment.filter_user_subtasks()
self.cs = CourseSession.objects.create(
course_id=COURSE_TEST_ID,
title="Test Lehrgang Session",
)
self.user = User.objects.get(username="student")
csu = CourseSessionUser.objects.create(
course_session=self.cs,
user=self.user,
)
self.client.login(username="student", password="test")
def test_can_updateAssignmentCompletion_asStudent(self):
url = f"/api/assignment/update/"
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["user"], self.user.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(
user=self.user,
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"}},
},
)

View File

@ -0,0 +1,261 @@
from django.test import TestCase
from vbv_lernwelt.assignment.creators.create_assignments import create_test_assignment
from vbv_lernwelt.assignment.models import AssignmentCompletion
from vbv_lernwelt.assignment.services import update_assignment_completion
from vbv_lernwelt.core.create_default_users import create_default_users
from vbv_lernwelt.core.models import User
from vbv_lernwelt.core.tests.helpers import create_locales_for_wagtail
from vbv_lernwelt.core.utils import find_first
from vbv_lernwelt.course.consts import COURSE_TEST_ID
from vbv_lernwelt.course.factories import CourseFactory, CoursePageFactory
from vbv_lernwelt.course.models import CourseSession
class UpdateAssignmentCompletionTestCase(TestCase):
def setUp(self):
create_default_users()
create_locales_for_wagtail()
course = CourseFactory(
id=COURSE_TEST_ID,
)
course_page = CoursePageFactory(course=course)
self.assignment = create_test_assignment()
self.course_session = CourseSession.objects.create(
course_id=COURSE_TEST_ID,
title="Bern 2022 a",
)
self.user = User.objects.get(username="student")
def test_can_store_new_user_input(self):
subtasks = self.assignment.filter_user_subtasks()
user_text_input = find_first(
subtasks, pred=lambda x: x["type"] == "user_text_input"
)
user_confirmation = find_first(
subtasks, pred=lambda x: x["type"] == "user_confirmation"
)
update_assignment_completion(
user=self.user,
assignment=self.assignment,
course_session=self.course_session,
completion_data={
user_text_input["id"]: {
"user_data": {"text": "Viel habe ich nicht zu sagen..."}
},
user_confirmation["id"]: {"user_data": {"confirmation": True}},
},
)
ac = AssignmentCompletion.objects.get(
user=self.user,
assignment=self.assignment,
course_session=self.course_session,
)
self.assertDictEqual(
ac.completion_data,
{
user_text_input["id"]: {
"user_data": {"text": "Viel habe ich nicht zu sagen..."}
},
user_confirmation["id"]: {"user_data": {"confirmation": True}},
},
)
def test_can_update_user_input(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(
user=self.user,
assignment=self.assignment,
course_session=self.course_session,
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"}
},
},
)
update_assignment_completion(
user=self.user,
assignment=self.assignment,
course_session=self.course_session,
completion_data={
user_text_input1["id"]: {
"user_data": {"text": "Viel mehr gibt es nicht zu sagen."}
}
},
)
ac = AssignmentCompletion.objects.get(
user=self.user,
assignment=self.assignment,
course_session=self.course_session,
)
user_input0 = ac.completion_data[user_text_input0["id"]]["user_data"]["text"]
user_input1 = ac.completion_data[user_text_input1["id"]]["user_data"]["text"]
self.assertEqual(user_input0, "Am Anfang war das Wort... 0")
self.assertEqual(user_input1, "Viel mehr gibt es nicht zu sagen.")
def test_will_not_store_unknown_user_tasks(self):
random_uuid = "7b60903b-d2a5-4798-b6cd-5b51e63e98ab"
update_assignment_completion(
user=self.user,
assignment=self.assignment,
course_session=self.course_session,
completion_data={
random_uuid: {
"user_data": {"text": "Viel mehr gibt es nicht zu sagen."}
}
},
)
ac = AssignmentCompletion.objects.get(
user=self.user,
assignment=self.assignment,
course_session=self.course_session,
)
self.assertEqual(ac.completion_data, {})
def test_change_completion_status(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(
user=self.user,
assignment=self.assignment,
course_session=self.course_session,
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"}
},
},
)
update_assignment_completion(
user=self.user,
assignment=self.assignment,
course_session=self.course_session,
completion_status="submitted",
)
ac = AssignmentCompletion.objects.get(
user=self.user,
assignment=self.assignment,
course_session=self.course_session,
)
self.assertEqual(ac.completion_status, "submitted")
def test_copy_task_data(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(
user=self.user,
assignment=self.assignment,
course_session=self.course_session,
completion_data={
user_text_input["id"]: {
"user_data": {"text": "Ich würde nichts weiteres empfehlen."}
},
},
)
update_assignment_completion(
user=self.user,
assignment=self.assignment,
course_session=self.course_session,
completion_status="submitted",
copy_task_data=True,
)
ac = AssignmentCompletion.objects.get(
user=self.user,
assignment=self.assignment,
course_session=self.course_session,
)
self.assertEqual(ac.completion_status, "submitted")
user_input = ac.completion_data[user_text_input["id"]]
self.assertEqual(
user_input["user_data"]["text"], "Ich würde nichts weiteres empfehlen."
)
self.assertTrue(
user_input["value"]["text"].startswith(
"Gibt es zusätzliche Deckungen, die du der Person empfehlen würdest?"
)
)
def test_can_add_trainer_data_without_loosing_user_input_data(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(
user=self.user,
assignment=self.assignment,
course_session=self.course_session,
completion_data={
user_text_input["id"]: {
"user_data": {"text": "Ich würde nichts weiteres empfehlen."}
},
},
)
update_assignment_completion(
user=self.user,
assignment=self.assignment,
course_session=self.course_session,
completion_data={
user_text_input["id"]: {
"trainer_data": {"points": 1, "comment": "Gut gemacht!"}
},
},
completion_status="grading_in_progress",
)
ac = AssignmentCompletion.objects.get(
user=self.user,
assignment=self.assignment,
course_session=self.course_session,
)
self.assertEqual(ac.completion_status, "grading_in_progress")
user_input = ac.completion_data[user_text_input["id"]]
self.assertDictEqual(
user_input["trainer_data"], {"points": 1, "comment": "Gut gemacht!"}
)
self.assertEqual(
user_input["user_data"]["text"], "Ich würde nichts weiteres empfehlen."
)

View File

@ -1,3 +1,103 @@
from django.shortcuts import render
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
# Create your views here.
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.course.models import CourseSession
from vbv_lernwelt.course.permissions import (
has_course_access,
has_course_access_by_page_request,
is_course_session_expert,
)
logger = structlog.get_logger(__name__)
def _request_assignment_completion(assignment_id, course_session_id, user_id):
try:
response_data = AssignmentCompletionSerializer(
AssignmentCompletion.objects.get(
user_id=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,
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,
user_id=user_id,
)
raise PermissionDenied()
@api_view(["POST"])
def update_assignment_input(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(
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(
"store_assignment_input successful",
label="assignment_api",
assignment_id=assignment.id,
assignment_title=assignment.title,
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)

View File

@ -39,7 +39,7 @@ class DayUserRateThrottle(UserRateThrottle):
scope = "day-throttle"
def first_true(iterable, default=False, pred=None):
def find_first(iterable, default=False, pred=None):
"""Returns the first true value in the iterable.
If no true value is found, returns *default*

View File

@ -5,7 +5,7 @@ from django.conf import settings
from slugify import slugify
from wagtail.models import Site
from vbv_lernwelt.assignment.creators.create_assignments import create_test_assignments
from vbv_lernwelt.assignment.creators.create_assignments import create_test_assignment
from vbv_lernwelt.assignment.models import Assignment
from vbv_lernwelt.competence.factories import (
CompetencePageFactory,
@ -44,11 +44,11 @@ from vbv_lernwelt.media_library.tests.media_library_factories import (
def create_test_course(include_uk=True, include_vv=True, with_sessions=False):
create_locales_for_wagtail()
create_test_course_with_categories()
course = create_test_course_with_categories()
create_test_competence_profile()
if include_uk:
create_test_assignments()
create_test_assignment()
create_test_learning_path(include_uk=include_uk, include_vv=include_vv)
create_test_media_library()
@ -64,6 +64,8 @@ def create_test_course(include_uk=True, include_vv=True, with_sessions=False):
title="Zürich 2022 a",
)
return course
def create_test_course_with_categories(apps=None, schema_editor=None):
if apps is not None:
@ -102,6 +104,8 @@ def create_test_course_with_categories(apps=None, schema_editor=None):
course.slug = course_page.slug
course.save()
return course
def create_test_learning_path(include_uk=True, include_vv=True):
course_page = CoursePage.objects.get(course_id=COURSE_TEST_ID)

View File

@ -2,6 +2,8 @@ import djclick as click
from wagtail.models import Page
from vbv_lernwelt.assignment.creators.create_assignments import create_uk_assignments
from vbv_lernwelt.assignment.models import Assignment
from vbv_lernwelt.assignment.services import update_assignment_completion
from vbv_lernwelt.competence.create_uk_competence_profile import (
create_uk_competence_profile,
create_uk_fr_competence_profile,
@ -62,6 +64,13 @@ def command(course):
create_course_uk_de_completion_data(
CourseSession.objects.get(title="Bern 2023 a")
)
create_course_uk_de_assignment_completion_data(
assignment=Assignment.objects.get(
slug="überbetriebliche-kurse-assignment-überprüfen-einer-motorfahrzeugs-versicherungspolice"
),
course_session=CourseSession.objects.get(title="Bern 2023 a"),
user=User.objects.get(email="michael.meier@example.com"),
)
if COURSE_UK_FR in course:
create_course_uk_fr()
@ -296,6 +305,23 @@ def create_course_uk_fr():
csu.expert.add(fr_circle)
def create_course_uk_de_assignment_completion_data(assignment, course_session, user):
subtasks = assignment.filter_user_subtasks(subtask_types=["user_text_input"])
for index, subtask in enumerate(subtasks):
user_text = f"Lorem ipsum dolor sit amet... {index}"
update_assignment_completion(
user=user,
assignment=assignment,
course_session=course_session,
completion_data={
subtask["id"]: {
"user_data": {"text": user_text},
}
},
)
def create_course_uk_de_completion_data(course_session):
# initial completion data
for slug, status, email in [

View File

@ -18,12 +18,12 @@ def has_course_access(user, course_id):
return False
def is_course_expert(user, course_id: int):
def is_course_session_expert(user, course_session_id: int):
if user.is_superuser:
return True
if CourseSessionUser.objects.filter(
course_session__course_id=course_id,
course_session_id=course_session_id,
user=user,
role=CourseSessionUser.Role.EXPERT,
).exists():

View File

@ -16,7 +16,7 @@ from vbv_lernwelt.course.permissions import (
has_course_access,
has_course_access_by_page_request,
is_circle_expert,
is_course_expert,
is_course_session_expert,
)
from vbv_lernwelt.course.serializers import (
CourseCompletionSerializer,
@ -79,8 +79,9 @@ def request_course_completion(request, course_session_id):
@api_view(["GET"])
def request_course_completion_for_user(request, course_session_id, user_id):
course_id = get_object_or_404(CourseSession, id=course_session_id).course_id
if request.user.id == user_id or is_course_expert(request.user, course_id):
if request.user.id == user_id or is_course_session_expert(
request.user, course_session_id
):
return _request_course_completion(course_session_id, user_id)
raise PermissionDenied()