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:
parent
25bf90cefd
commit
adc61479fc
|
|
@ -9,6 +9,11 @@ from django.views import defaults as default_views
|
||||||
from grapple import urls as grapple_urls
|
from grapple import urls as grapple_urls
|
||||||
from ratelimit.exceptions import Ratelimited
|
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.middleware.auth import django_view_authentication_exempt
|
||||||
from vbv_lernwelt.core.views import (
|
from vbv_lernwelt.core.views import (
|
||||||
check_rate_limit,
|
check_rate_limit,
|
||||||
|
|
@ -93,6 +98,16 @@ urlpatterns = [
|
||||||
request_course_completion_for_user,
|
request_course_completion_for_user,
|
||||||
name="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
|
# documents
|
||||||
path(r'api/core/document/start/', document_upload_start,
|
path(r'api/core/document/start/', document_upload_start,
|
||||||
name='file_upload_start'),
|
name='file_upload_start'),
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
#
|
#
|
||||||
# pip-compile --output-file=requirements-dev.txt requirements-dev.in
|
# pip-compile --output-file=requirements-dev.txt requirements-dev.in
|
||||||
#
|
#
|
||||||
aniso8601==7.0.0
|
aniso8601==9.0.1
|
||||||
# via graphene
|
# via graphene
|
||||||
anyascii==0.3.1
|
anyascii==0.3.1
|
||||||
# via wagtail
|
# via wagtail
|
||||||
|
|
@ -205,18 +205,19 @@ gitdb2==4.0.2
|
||||||
# via gitpython
|
# via gitpython
|
||||||
gitpython==3.0.6
|
gitpython==3.0.6
|
||||||
# via trufflehog
|
# via trufflehog
|
||||||
graphene==2.1.9
|
graphene==3.2.2
|
||||||
# via graphene-django
|
# via graphene-django
|
||||||
graphene-django==2.15.0
|
graphene-django==3.0.0
|
||||||
# via wagtail-grapple
|
# via wagtail-grapple
|
||||||
graphql-core==2.3.2
|
graphql-core==3.2.3
|
||||||
# via
|
# via
|
||||||
# graphene
|
# graphene
|
||||||
# graphene-django
|
# graphene-django
|
||||||
# graphql-relay
|
# graphql-relay
|
||||||
# wagtail-grapple
|
graphql-relay==3.2.0
|
||||||
graphql-relay==2.0.1
|
# via
|
||||||
# via graphene
|
# graphene
|
||||||
|
# graphene-django
|
||||||
gunicorn==20.1.0
|
gunicorn==20.1.0
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
h11==0.13.0
|
h11==0.13.0
|
||||||
|
|
@ -291,8 +292,8 @@ mypy-extensions==0.4.3
|
||||||
# typing-inspect
|
# typing-inspect
|
||||||
nodeenv==1.6.0
|
nodeenv==1.6.0
|
||||||
# via pre-commit
|
# via pre-commit
|
||||||
openpyxl==3.0.9
|
openpyxl==3.1.2
|
||||||
# via tablib
|
# via wagtail
|
||||||
packaging==21.3
|
packaging==21.3
|
||||||
# via
|
# via
|
||||||
# build
|
# build
|
||||||
|
|
@ -332,10 +333,7 @@ portalocker==2.4.0
|
||||||
pre-commit==2.17.0
|
pre-commit==2.17.0
|
||||||
# via -r requirements-dev.in
|
# via -r requirements-dev.in
|
||||||
promise==2.3
|
promise==2.3
|
||||||
# via
|
# via graphene-django
|
||||||
# graphene-django
|
|
||||||
# graphql-core
|
|
||||||
# graphql-relay
|
|
||||||
prompt-toolkit==3.0.28
|
prompt-toolkit==3.0.28
|
||||||
# via ipython
|
# via ipython
|
||||||
psycopg2-binary==2.9.3
|
psycopg2-binary==2.9.3
|
||||||
|
|
@ -414,29 +412,20 @@ requests==2.27.1
|
||||||
# coreapi
|
# coreapi
|
||||||
# djangorestframework-stubs
|
# djangorestframework-stubs
|
||||||
# wagtail
|
# wagtail
|
||||||
rx==1.6.1
|
|
||||||
# via graphql-core
|
|
||||||
s3transfer==0.6.0
|
s3transfer==0.6.0
|
||||||
# via boto3
|
# via boto3
|
||||||
sendgrid==6.9.7
|
sendgrid==6.9.7
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
sentry-sdk==1.5.8
|
sentry-sdk==1.5.8
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
singledispatch==3.7.0
|
|
||||||
# via graphene-django
|
|
||||||
six==1.16.0
|
six==1.16.0
|
||||||
# via
|
# via
|
||||||
# asttokens
|
# asttokens
|
||||||
# django-coverage-plugin
|
# django-coverage-plugin
|
||||||
# graphene
|
|
||||||
# graphene-django
|
|
||||||
# graphql-core
|
|
||||||
# graphql-relay
|
|
||||||
# html5lib
|
# html5lib
|
||||||
# l18n
|
# l18n
|
||||||
# promise
|
# promise
|
||||||
# python-dateutil
|
# python-dateutil
|
||||||
# singledispatch
|
|
||||||
# virtualenv
|
# virtualenv
|
||||||
smmap==5.0.0
|
smmap==5.0.0
|
||||||
# via gitdb
|
# via gitdb
|
||||||
|
|
@ -458,8 +447,6 @@ structlog==21.5.0
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
swapper==1.3.0
|
swapper==1.3.0
|
||||||
# via django-notifications-hq
|
# via django-notifications-hq
|
||||||
tablib[xls,xlsx]==3.2.1
|
|
||||||
# via wagtail
|
|
||||||
telepath==0.2
|
telepath==0.2
|
||||||
# via wagtail
|
# via wagtail
|
||||||
termcolor==1.1.0
|
termcolor==1.1.0
|
||||||
|
|
@ -533,7 +520,7 @@ uvloop==0.16.0
|
||||||
# via uvicorn
|
# via uvicorn
|
||||||
virtualenv==20.14.0
|
virtualenv==20.14.0
|
||||||
# via pre-commit
|
# via pre-commit
|
||||||
wagtail==3.0.1
|
wagtail==4.2.2
|
||||||
# via
|
# via
|
||||||
# -r requirements.in
|
# -r requirements.in
|
||||||
# wagtail-factories
|
# wagtail-factories
|
||||||
|
|
@ -542,11 +529,11 @@ wagtail==3.0.1
|
||||||
# wagtail-localize
|
# wagtail-localize
|
||||||
wagtail-factories==4.0.0
|
wagtail-factories==4.0.0
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
wagtail-grapple==0.18.0
|
wagtail-grapple==0.19.2
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
wagtail-headless-preview==0.4.0
|
wagtail-headless-preview==0.4.0
|
||||||
# via wagtail-grapple
|
# via wagtail-grapple
|
||||||
wagtail-localize==1.2.1
|
wagtail-localize==1.5
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
watchfiles==0.17.0
|
watchfiles==0.17.0
|
||||||
# via
|
# via
|
||||||
|
|
@ -568,12 +555,6 @@ wrapt==1.14.0
|
||||||
# via
|
# via
|
||||||
# astroid
|
# astroid
|
||||||
# deprecated
|
# 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:
|
# The following packages are considered to be unsafe in a requirements file:
|
||||||
# pip
|
# pip
|
||||||
|
|
|
||||||
|
|
@ -36,9 +36,9 @@ structlog
|
||||||
python-json-logger
|
python-json-logger
|
||||||
concurrent-log-handler
|
concurrent-log-handler
|
||||||
|
|
||||||
wagtail>=3,<4
|
wagtail>=4
|
||||||
wagtail-factories>=4
|
wagtail-factories>=4
|
||||||
wagtail-localize
|
wagtail-localize>=1.5
|
||||||
wagtail_grapple
|
wagtail_grapple>=0.19.2
|
||||||
|
|
||||||
boto3
|
boto3
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
#
|
#
|
||||||
# pip-compile --output-file=requirements.txt requirements.in
|
# pip-compile --output-file=requirements.txt requirements.in
|
||||||
#
|
#
|
||||||
aniso8601==7.0.0
|
aniso8601==9.0.1
|
||||||
# via graphene
|
# via graphene
|
||||||
anyascii==0.3.1
|
anyascii==0.3.1
|
||||||
# via wagtail
|
# via wagtail
|
||||||
|
|
@ -124,18 +124,19 @@ factory-boy==3.2.1
|
||||||
# via wagtail-factories
|
# via wagtail-factories
|
||||||
faker==13.11.1
|
faker==13.11.1
|
||||||
# via factory-boy
|
# via factory-boy
|
||||||
graphene==2.1.9
|
graphene==3.2.2
|
||||||
# via graphene-django
|
# via graphene-django
|
||||||
graphene-django==2.15.0
|
graphene-django==3.0.0
|
||||||
# via wagtail-grapple
|
# via wagtail-grapple
|
||||||
graphql-core==2.3.2
|
graphql-core==3.2.3
|
||||||
# via
|
# via
|
||||||
# graphene
|
# graphene
|
||||||
# graphene-django
|
# graphene-django
|
||||||
# graphql-relay
|
# graphql-relay
|
||||||
# wagtail-grapple
|
graphql-relay==3.2.0
|
||||||
graphql-relay==2.0.1
|
# via
|
||||||
# via graphene
|
# graphene
|
||||||
|
# graphene-django
|
||||||
gunicorn==20.1.0
|
gunicorn==20.1.0
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
h11==0.13.0
|
h11==0.13.0
|
||||||
|
|
@ -162,8 +163,8 @@ l18n==2021.3
|
||||||
# via wagtail
|
# via wagtail
|
||||||
marshmallow==3.15.0
|
marshmallow==3.15.0
|
||||||
# via environs
|
# via environs
|
||||||
openpyxl==3.0.9
|
openpyxl==3.1.2
|
||||||
# via tablib
|
# via wagtail
|
||||||
packaging==21.3
|
packaging==21.3
|
||||||
# via
|
# via
|
||||||
# marshmallow
|
# marshmallow
|
||||||
|
|
@ -177,10 +178,7 @@ polib==1.1.1
|
||||||
portalocker==2.4.0
|
portalocker==2.4.0
|
||||||
# via concurrent-log-handler
|
# via concurrent-log-handler
|
||||||
promise==2.3
|
promise==2.3
|
||||||
# via
|
# via graphene-django
|
||||||
# graphene-django
|
|
||||||
# graphql-core
|
|
||||||
# graphql-relay
|
|
||||||
psycopg2-binary==2.9.3
|
psycopg2-binary==2.9.3
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
pycparser==2.21
|
pycparser==2.21
|
||||||
|
|
@ -221,27 +219,18 @@ redis==4.2.1
|
||||||
# django-redis
|
# django-redis
|
||||||
requests==2.27.1
|
requests==2.27.1
|
||||||
# via wagtail
|
# via wagtail
|
||||||
rx==1.6.1
|
|
||||||
# via graphql-core
|
|
||||||
s3transfer==0.6.0
|
s3transfer==0.6.0
|
||||||
# via boto3
|
# via boto3
|
||||||
sendgrid==6.9.7
|
sendgrid==6.9.7
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
sentry-sdk==1.5.8
|
sentry-sdk==1.5.8
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
singledispatch==3.7.0
|
|
||||||
# via graphene-django
|
|
||||||
six==1.16.0
|
six==1.16.0
|
||||||
# via
|
# via
|
||||||
# graphene
|
|
||||||
# graphene-django
|
|
||||||
# graphql-core
|
|
||||||
# graphql-relay
|
|
||||||
# html5lib
|
# html5lib
|
||||||
# l18n
|
# l18n
|
||||||
# promise
|
# promise
|
||||||
# python-dateutil
|
# python-dateutil
|
||||||
# singledispatch
|
|
||||||
sniffio==1.2.0
|
sniffio==1.2.0
|
||||||
# via anyio
|
# via anyio
|
||||||
soupsieve==2.3.2.post1
|
soupsieve==2.3.2.post1
|
||||||
|
|
@ -254,8 +243,6 @@ structlog==21.5.0
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
swapper==1.3.0
|
swapper==1.3.0
|
||||||
# via django-notifications-hq
|
# via django-notifications-hq
|
||||||
tablib[xls,xlsx]==3.2.1
|
|
||||||
# via wagtail
|
|
||||||
telepath==0.2
|
telepath==0.2
|
||||||
# via wagtail
|
# via wagtail
|
||||||
text-unidecode==1.3
|
text-unidecode==1.3
|
||||||
|
|
@ -275,7 +262,7 @@ uvicorn[standard]==0.18.3
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
uvloop==0.16.0
|
uvloop==0.16.0
|
||||||
# via uvicorn
|
# via uvicorn
|
||||||
wagtail==3.0.1
|
wagtail==4.2.2
|
||||||
# via
|
# via
|
||||||
# -r requirements.in
|
# -r requirements.in
|
||||||
# wagtail-factories
|
# wagtail-factories
|
||||||
|
|
@ -284,11 +271,11 @@ wagtail==3.0.1
|
||||||
# wagtail-localize
|
# wagtail-localize
|
||||||
wagtail-factories==4.0.0
|
wagtail-factories==4.0.0
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
wagtail-grapple==0.18.0
|
wagtail-grapple==0.19.2
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
wagtail-headless-preview==0.4.0
|
wagtail-headless-preview==0.4.0
|
||||||
# via wagtail-grapple
|
# via wagtail-grapple
|
||||||
wagtail-localize==1.2.1
|
wagtail-localize==1.5
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
watchfiles==0.17.0
|
watchfiles==0.17.0
|
||||||
# via uvicorn
|
# via uvicorn
|
||||||
|
|
@ -302,12 +289,6 @@ willow==1.4.1
|
||||||
# via wagtail
|
# via wagtail
|
||||||
wrapt==1.14.0
|
wrapt==1.14.0
|
||||||
# via deprecated
|
# 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:
|
# The following packages are considered to be unsafe in a requirements file:
|
||||||
# setuptools
|
# setuptools
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,19 @@
|
||||||
from django.contrib import admin
|
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",
|
||||||
|
]
|
||||||
|
|
|
||||||
|
|
@ -285,7 +285,7 @@ def create_uk_assignments(course_id=COURSE_UK):
|
||||||
assignment.save()
|
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)
|
course_page = CoursePage.objects.get(course_id=course_id)
|
||||||
assignment_page = AssignmentListPageFactory(
|
assignment_page = AssignmentListPageFactory(
|
||||||
parent=course_page,
|
parent=course_page,
|
||||||
|
|
@ -555,3 +555,5 @@ def create_test_assignments(course_id=COURSE_TEST_ID):
|
||||||
)
|
)
|
||||||
|
|
||||||
assignment.save()
|
assignment.save()
|
||||||
|
|
||||||
|
return assignment
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.db.models import UniqueConstraint
|
||||||
from slugify import slugify
|
from slugify import slugify
|
||||||
from wagtail import blocks
|
from wagtail import blocks
|
||||||
from wagtail.admin.panels import FieldPanel
|
from wagtail.admin.panels import FieldPanel
|
||||||
|
|
@ -6,6 +9,7 @@ from wagtail.fields import StreamField
|
||||||
from wagtail.models import Page
|
from wagtail.models import Page
|
||||||
|
|
||||||
from vbv_lernwelt.core.model_utils import find_available_slug
|
from vbv_lernwelt.core.model_utils import find_available_slug
|
||||||
|
from vbv_lernwelt.core.models import User
|
||||||
from vbv_lernwelt.course.models import CourseBasePage
|
from vbv_lernwelt.course.models import CourseBasePage
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -42,7 +46,7 @@ class PerformanceObjectiveBlock(blocks.StructBlock):
|
||||||
icon = "tick"
|
icon = "tick"
|
||||||
|
|
||||||
|
|
||||||
class UserTextInputBlock(blocks.StaticBlock):
|
class UserTextInputBlock(blocks.StructBlock):
|
||||||
text = blocks.TextBlock(blank=True)
|
text = blocks.TextBlock(blank=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
@ -135,3 +139,68 @@ class Assignment(CourseBasePage):
|
||||||
ignore_page_id=self.id,
|
ignore_page_id=self.id,
|
||||||
)
|
)
|
||||||
super(Assignment, self).save(clean, user, log_action, **kwargs)
|
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",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
]
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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"}},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
@ -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."
|
||||||
|
)
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ class DayUserRateThrottle(UserRateThrottle):
|
||||||
scope = "day-throttle"
|
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.
|
"""Returns the first true value in the iterable.
|
||||||
|
|
||||||
If no true value is found, returns *default*
|
If no true value is found, returns *default*
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ from django.conf import settings
|
||||||
from slugify import slugify
|
from slugify import slugify
|
||||||
from wagtail.models import Site
|
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.assignment.models import Assignment
|
||||||
from vbv_lernwelt.competence.factories import (
|
from vbv_lernwelt.competence.factories import (
|
||||||
CompetencePageFactory,
|
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):
|
def create_test_course(include_uk=True, include_vv=True, with_sessions=False):
|
||||||
create_locales_for_wagtail()
|
create_locales_for_wagtail()
|
||||||
create_test_course_with_categories()
|
course = create_test_course_with_categories()
|
||||||
|
|
||||||
create_test_competence_profile()
|
create_test_competence_profile()
|
||||||
if include_uk:
|
if include_uk:
|
||||||
create_test_assignments()
|
create_test_assignment()
|
||||||
|
|
||||||
create_test_learning_path(include_uk=include_uk, include_vv=include_vv)
|
create_test_learning_path(include_uk=include_uk, include_vv=include_vv)
|
||||||
create_test_media_library()
|
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",
|
title="Zürich 2022 a",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return course
|
||||||
|
|
||||||
|
|
||||||
def create_test_course_with_categories(apps=None, schema_editor=None):
|
def create_test_course_with_categories(apps=None, schema_editor=None):
|
||||||
if apps is not 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.slug = course_page.slug
|
||||||
course.save()
|
course.save()
|
||||||
|
|
||||||
|
return course
|
||||||
|
|
||||||
|
|
||||||
def create_test_learning_path(include_uk=True, include_vv=True):
|
def create_test_learning_path(include_uk=True, include_vv=True):
|
||||||
course_page = CoursePage.objects.get(course_id=COURSE_TEST_ID)
|
course_page = CoursePage.objects.get(course_id=COURSE_TEST_ID)
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ import djclick as click
|
||||||
from wagtail.models import Page
|
from wagtail.models import Page
|
||||||
|
|
||||||
from vbv_lernwelt.assignment.creators.create_assignments import create_uk_assignments
|
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 (
|
from vbv_lernwelt.competence.create_uk_competence_profile import (
|
||||||
create_uk_competence_profile,
|
create_uk_competence_profile,
|
||||||
create_uk_fr_competence_profile,
|
create_uk_fr_competence_profile,
|
||||||
|
|
@ -62,6 +64,13 @@ def command(course):
|
||||||
create_course_uk_de_completion_data(
|
create_course_uk_de_completion_data(
|
||||||
CourseSession.objects.get(title="Bern 2023 a")
|
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:
|
if COURSE_UK_FR in course:
|
||||||
create_course_uk_fr()
|
create_course_uk_fr()
|
||||||
|
|
@ -296,6 +305,23 @@ def create_course_uk_fr():
|
||||||
csu.expert.add(fr_circle)
|
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):
|
def create_course_uk_de_completion_data(course_session):
|
||||||
# initial completion data
|
# initial completion data
|
||||||
for slug, status, email in [
|
for slug, status, email in [
|
||||||
|
|
|
||||||
|
|
@ -18,12 +18,12 @@ def has_course_access(user, course_id):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def is_course_expert(user, course_id: int):
|
def is_course_session_expert(user, course_session_id: int):
|
||||||
if user.is_superuser:
|
if user.is_superuser:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if CourseSessionUser.objects.filter(
|
if CourseSessionUser.objects.filter(
|
||||||
course_session__course_id=course_id,
|
course_session_id=course_session_id,
|
||||||
user=user,
|
user=user,
|
||||||
role=CourseSessionUser.Role.EXPERT,
|
role=CourseSessionUser.Role.EXPERT,
|
||||||
).exists():
|
).exists():
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ from vbv_lernwelt.course.permissions import (
|
||||||
has_course_access,
|
has_course_access,
|
||||||
has_course_access_by_page_request,
|
has_course_access_by_page_request,
|
||||||
is_circle_expert,
|
is_circle_expert,
|
||||||
is_course_expert,
|
is_course_session_expert,
|
||||||
)
|
)
|
||||||
from vbv_lernwelt.course.serializers import (
|
from vbv_lernwelt.course.serializers import (
|
||||||
CourseCompletionSerializer,
|
CourseCompletionSerializer,
|
||||||
|
|
@ -79,8 +79,9 @@ def request_course_completion(request, course_session_id):
|
||||||
|
|
||||||
@api_view(["GET"])
|
@api_view(["GET"])
|
||||||
def request_course_completion_for_user(request, course_session_id, user_id):
|
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_session_expert(
|
||||||
if request.user.id == user_id or is_course_expert(request.user, course_id):
|
request.user, course_session_id
|
||||||
|
):
|
||||||
return _request_course_completion(course_session_id, user_id)
|
return _request_course_completion(course_session_id, user_id)
|
||||||
raise PermissionDenied()
|
raise PermissionDenied()
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue