Add Edoniq testblock

WIP: Add sso code

Update content name

WIP: Add redirect endpoint

mend

Fix after rebase

WIP: Update model

WIP: Add extended time test url

Update trufflehog config, use sso

Update test
This commit is contained in:
Christian Cueni 2023-07-18 16:31:23 +02:00
parent f7de5bae47
commit c140f225ea
30 changed files with 796 additions and 373 deletions

View File

@ -106,6 +106,9 @@
"noDueDatesAvailable": "Keine Termine vorhanden",
"showAllDueDates": "Alle Termine anzeigen"
},
"edoniqTest": {
"qualifiesForExtendedTime": "Ich habe Anrecht auf einen Nachteilsausgleich."
},
"feedback": {
"answers": "Antworten",
"areYouSatisfied": "Wie zufrieden bist du?",

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import LearningContentContainer from "@/pages/learningPath/learningContentPage/LearningContentContainer.vue";
import DocumentListBlock from "@/pages/learningPath/learningContentPage/blocks/DocumentListBlock.vue";
import TestBlock from "@/pages/learningPath/learningContentPage/blocks/TestBlock.vue";
import EdoniqTestBlock from "@/pages/learningPath/learningContentPage/blocks/EdoniqTestBlock.vue";
import { useCircleStore } from "@/stores/circle";
import type { LearningContent, LearningContentType } from "@/types";
import eventBus from "@/utils/eventBus";
@ -35,7 +35,7 @@ const COMPONENTS: Record<LearningContentType, Component> = {
"learnpath.LearningContentMediaLibrary": MediaLibraryBlock,
"learnpath.LearningContentPlaceholder": PlaceholderBlock,
"learnpath.LearningContentRichText": RichTextBlock,
"learnpath.LearningContentTest": TestBlock,
"learnpath.LearningContentEdoniqTest": EdoniqTestBlock,
"learnpath.LearningContentVideo": VideoBlock,
};
const DEFAULT_BLOCK = PlaceholderBlock;

View File

@ -0,0 +1,118 @@
<script setup lang="ts">
import { useTranslation } from "i18next-vue";
import ItCheckbox from "@/components/ui/ItCheckbox.vue";
import LearningContentSimpleLayout from "@/pages/learningPath/learningContentPage/layouts/LearningContentSimpleLayout.vue";
import type { LearningContentEdoniqTest } from "@/types";
import { ref } from "vue";
import * as log from "loglevel";
import { itPost } from "@/fetchHelpers";
const { t } = useTranslation();
const props = defineProps<{
content: LearningContentEdoniqTest;
}>();
const termsAccepted = ref(false);
const extendedTimeTest = ref(false);
async function startTest() {
log.info("start test", props.content);
const response = await itPost("/api/core/edoniq-test/redirect/", {
learning_content_id: props.content.id,
extended_time_test: extendedTimeTest.value,
});
log.info(response.redirect_url);
window.open(response.redirect_url, "_blank");
}
</script>
<template>
<LearningContentSimpleLayout
:title="props.content.title"
:learning-content="props.content"
>
<!-- eslint-disable vue/no-v-html -->
<div class="container-medium">
<p
v-if="props.content.description"
class="default-wagtail-rich-text text-large my-4"
v-html="props.content.description"
></p>
<div class="my-8">
<ItCheckbox
v-if="props.content.checkbox_text"
:checkbox-item="{
label: props.content.checkbox_text,
value: termsAccepted,
checked: termsAccepted,
}"
@toggle="termsAccepted = !termsAccepted"
/>
</div>
<div class="my-8">
<ItCheckbox
v-if="props.content.has_extended_time_test"
:checkbox-item="{
label: t('edoniqTest.qualifiesForExtendedTime'),
value: extendedTimeTest,
checked: extendedTimeTest,
}"
@toggle="extendedTimeTest = !extendedTimeTest"
/>
</div>
<div class="my-8">
<button
:disabled="!termsAccepted"
class="btn-primary inline-flex items-center"
@click="startTest()"
>
Test starten
<it-icon-external-link class="it-icon ml-2 h-5 w-5"></it-icon-external-link>
</button>
</div>
</div>
</LearningContentSimpleLayout>
</template>
<!--<template>-->
<!-- <div class="container-medium">-->
<!-- <div class="lg:mt-8">-->
<!-- <h1>{{ content.title }}</h1>-->
<!-- <p class="text-large my-4 lg:my-8">{{ value.description }}</p>-->
<!-- <a :href="value.url" target="_blank" class="button btn-primary">Test starten</a>-->
<!-- </div>-->
<!-- </div>-->
<!--</template>-->
<!--<script setup lang="ts">-->
<!--import type { LearningContent } from "@/types";-->
<!--interface Value {-->
<!-- description: string;-->
<!-- url: string;-->
<!--}-->
<!--defineProps<{-->
<!-- value: Value;-->
<!-- content: LearningContent;-->
<!--}>();-->
<!--</script>-->

View File

@ -1,59 +0,0 @@
<script setup lang="ts">
import ItCheckbox from "@/components/ui/ItCheckbox.vue";
import LearningContentSimpleLayout from "@/pages/learningPath/learningContentPage/layouts/LearningContentSimpleLayout.vue";
import type { LearningContentTest } from "@/types";
import { ref } from "vue";
const props = defineProps<{
content: LearningContentTest;
}>();
const checked = ref(false);
function toggleChecked() {
checked.value = !checked.value;
}
function startTest() {
window.open(props.content.content_url, "_blank");
}
</script>
<template>
<LearningContentSimpleLayout
:title="props.content.title"
:learning-content="props.content"
>
<!-- eslint-disable vue/no-v-html -->
<div class="container-medium">
<p
v-if="props.content.description"
class="default-wagtail-rich-text text-large my-4"
v-html="props.content.description"
></p>
<div class="my-8">
<ItCheckbox
v-if="props.content.checkbox_text"
:checkbox-item="{
label: props.content.checkbox_text,
value: checked,
checked: checked,
}"
@toggle="toggleChecked()"
/>
</div>
<div class="my-8">
<button
:disabled="!checked"
class="btn-primary inline-flex items-center"
@click="startTest()"
>
Test starten
<it-icon-external-link class="it-icon ml-2 h-5 w-5"></it-icon-external-link>
</button>
</div>
</div>
</LearningContentSimpleLayout>
</template>

View File

@ -26,7 +26,7 @@ function isLearningContentType(object: any): object is LearningContent {
object?.content_type === "learnpath.LearningContentMediaLibrary" ||
object?.content_type === "learnpath.LearningContentPlaceholder" ||
object?.content_type === "learnpath.LearningContentRichText" ||
object?.content_type === "learnpath.LearningContentTest" ||
object?.content_type === "learnpath.LearningContentEdoniqTest" ||
object?.content_type === "learnpath.LearningContentVideo"
);
}

View File

@ -33,7 +33,7 @@ export type LearningContent =
| LearningContentMediaLibrary
| LearningContentPlaceholder
| LearningContentRichText
| LearningContentTest
| LearningContentEdoniqTest
| LearningContentVideo;
export type LearningContentType = LearningContent["content_type"];
@ -93,9 +93,10 @@ export interface LearningContentRichText extends LearningContentInterface {
readonly text: string;
}
export interface LearningContentTest extends LearningContentInterface {
readonly content_type: "learnpath.LearningContentTest";
export interface LearningContentEdoniqTest extends LearningContentInterface {
readonly content_type: "learnpath.LearningContentEdoniqTest";
readonly checkbox_text: string;
readonly has_extended_time_test: boolean;
}
export interface LearningContentVideo extends LearningContentInterface {

View File

@ -47,7 +47,7 @@ export function learningContentTypeData(
return { title: t("mediaLibrary.title"), icon: "it-icon-lc-media-library" };
case "learnpath.LearningContentVideo":
return { title: t("learningContentTypes.video"), icon: "it-icon-lc-video" };
case "learnpath.LearningContentTest":
case "learnpath.LearningContentEdoniqTest":
return { title: t("learningContentTypes.test"), icon: "it-icon-lc-test" };
case "learnpath.LearningContentRichText":
return { title: t("learningContentTypes.text"), icon: "it-icon-lc-resource" };

Binary file not shown.

View File

@ -640,6 +640,11 @@ GRAPHENE = {
# ],
# }
# edoniq
EDONIQ_ENV = env("IT_EDONIQ_ENV", default="STEAGTEST")
EDONIQ_CERTIFICATE = env("IT_EDONIQ_CERTIFICATE", default="")
# Notifications
# django-notifications
DJANGO_NOTIFICATIONS_CONFIG = {"SOFT_DELETE": True}

View File

@ -8,8 +8,8 @@ from django.urls import include, path, re_path, register_converter
from django.urls.converters import IntConverter
from django.views import defaults as default_views
from django.views.decorators.csrf import csrf_exempt
from django_ratelimit.exceptions import Ratelimited
from graphene_django.views import GraphQLView
from ratelimit.exceptions import Ratelimited
from vbv_lernwelt.assignment.views import request_assignment_completion_status
from vbv_lernwelt.core.middleware.auth import django_view_authentication_exempt
@ -41,6 +41,7 @@ from vbv_lernwelt.edoniq_test.views import (
export_students,
export_students_and_trainers,
export_trainers,
get_edoniq_token_redirect,
)
from vbv_lernwelt.feedback.views import (
get_expert_feedbacks_for_course,
@ -146,10 +147,9 @@ urlpatterns = [
path(r'api/core/feedback/<str:course_session_id>/<str:circle_id>/', get_feedback_for_circle,
name='feedback_for_circle'),
path("server/graphql/",
csrf_exempt(GraphQLView.as_view(graphiql=True, schema=schema))),
# edoniq test
path(r'api/core/edoniq-test/redirect/', get_edoniq_token_redirect,
name='get_edoniq_token_redirect'),
path(r'api/core/edoniq-test/export-users/', export_students, name='edoniq_export_students'),
path(r'api/core/edoniq-test/export-trainers/', export_trainers, name='edoniq_export_trainers'),
path(r'api/core/edoniq-test/export-users-trainers/', export_students_and_trainers,
@ -172,6 +172,8 @@ urlpatterns = [
name="t2l_sync",
),
path("server/graphql/",
csrf_exempt(GraphQLView.as_view(graphiql=True, schema=schema))),
# testing and debug
path('server/raise_error/',
user_passes_test(lambda u: u.is_superuser, login_url='/login/')(

View File

@ -2,13 +2,13 @@
# This file is autogenerated by pip-compile with python 3.10
# To update, run:
#
# pip-compile --output-file=requirements-dev.txt requirements-dev.in
# pip-compile requirements-dev.in
#
aniso8601==9.0.1
# via graphene
anyascii==0.3.1
anyascii==0.3.2
# via wagtail
anyio==3.5.0
anyio==3.7.1
# via watchfiles
appnope==0.1.3
# via ipython
@ -16,64 +16,64 @@ argon2-cffi==21.3.0
# via -r requirements.in
argon2-cffi-bindings==21.2.0
# via argon2-cffi
asgiref==3.5.0
asgiref==3.7.2
# via django
astroid==2.11.2
astroid==2.15.6
# via pylint
asttokens==2.0.5
asttokens==2.2.1
# via stack-data
async-timeout==4.0.2
# via redis
attrs==21.4.0
attrs==23.1.0
# via
# jsonschema
# pytest
# referencing
# usort
authlib==1.0.0
authlib==1.2.1
# via -r requirements.in
azure-core==1.26.4
azure-core==1.29.1
# via
# azure-identity
# azure-storage-blob
azure-identity==1.13.0
azure-identity==1.14.0
# via -r requirements.in
azure-storage-blob==12.16.0
azure-storage-blob==12.17.0
# via
# -r requirements.in
# django-storages
backcall==0.2.0
# via ipython
beautifulsoup4==4.9.3
beautifulsoup4==4.11.2
# via wagtail
black==22.10.0
black==23.7.0
# via
# -r requirements-dev.in
# ufmt
boto3==1.26.11
boto3==1.28.23
# via -r requirements.in
botocore==1.29.11
botocore==1.31.23
# via
# boto3
# s3transfer
brotli==1.0.9
# via whitenoise
build==0.8.0
build==0.10.0
# via pip-tools
caprover-api @ git+https://github.com/iterativ/Caprover-API.git@5013f8fc929e8e3281b9d609e968a782e8e99530
# via -r requirements-dev.in
certifi==2021.10.8
certifi==2023.7.22
# via
# requests
# sentry-sdk
cffi==1.15.0
cffi==1.15.1
# via
# argon2-cffi-bindings
# cryptography
cfgv==3.3.1
# via pre-commit
charset-normalizer==2.0.12
charset-normalizer==3.2.0
# via requests
click==8.1.1
click==8.1.6
# via
# -r requirements.in
# black
@ -83,17 +83,13 @@ click==8.1.1
# ufmt
# usort
# uvicorn
concurrent-log-handler==0.9.20
concurrent-log-handler==0.9.24
# via -r requirements.in
coreapi==2.3.3
# via djangorestframework-stubs
coreschema==0.0.4
# via coreapi
coverage==6.3.2
coverage==7.2.7
# via
# -r requirements-dev.in
# django-coverage-plugin
cryptography==36.0.2
cryptography==41.0.3
# via
# authlib
# azure-identity
@ -104,15 +100,15 @@ decorator==5.1.1
# via
# ipdb
# ipython
deprecated==1.2.13
# via redis
dill==0.3.4
defusedxml==0.7.1
# via willow
dill==0.3.7
# via pylint
distlib==0.3.4
distlib==0.3.7
# via virtualenv
dj-database-url==1.0.0
dj-database-url==2.0.0
# via -r requirements.in
django==3.2.13
django==3.2.20
# via
# -r requirements.in
# dj-database-url
@ -138,27 +134,26 @@ django==3.2.13
# graphene-django
# jsonfield
# wagtail
# wagtail-grapple
# wagtail-localize
django-click==2.3.0
# via -r requirements.in
django-cors-headers==3.11.0
django-cors-headers==4.2.0
# via -r requirements.in
django-coverage-plugin==2.0.2
django-coverage-plugin==3.1.0
# via -r requirements-dev.in
django-csp==3.7
# via -r requirements.in
django-debug-toolbar==3.2.4
django-debug-toolbar==4.1.0
# via -r requirements-dev.in
django-extensions==3.2.0
django-extensions==3.2.3
# via -r requirements-dev.in
django-filter==21.1
django-filter==23.2
# via wagtail
django-ipware==4.0.2
django-ipware==5.0.0
# via -r requirements.in
django-jsonform==2.17.0
django-jsonform==2.18.0
# via -r requirements.in
django-model-utils==4.2.0
django-model-utils==4.3.1
# via
# -r requirements.in
# django-notifications-hq
@ -168,64 +163,70 @@ django-notifications-hq==1.8.2
# via -r requirements.in
django-permissionedforms==0.1
# via wagtail
django-ratelimit==3.0.1
django-ratelimit==4.1.0
# via -r requirements.in
django-redis==5.2.0
django-redis==5.3.0
# via -r requirements.in
django-storages[azure]==1.13.1
django-storages[azure]==1.13.2
# via -r requirements.in
django-stubs==1.10.1
django-stubs==4.2.3
# via
# -r requirements-dev.in
# djangorestframework-stubs
django-stubs-ext==0.4.0
django-stubs-ext==4.2.2
# via django-stubs
django-taggit==2.1.0
django-taggit==4.0.0
# via wagtail
django-treebeard==4.5.1
django-treebeard==4.7
# via wagtail
django-watchfiles @ https://github.com/q0w/django-watchfiles/archive/issue-1.zip
# via -r requirements-dev.in
djangorestframework==3.13.1
djangorestframework==3.14.0
# via
# -r requirements.in
# drf-spectacular
# wagtail
djangorestframework-stubs==1.4.0
djangorestframework-stubs==3.14.2
# via -r requirements-dev.in
draftjs-exporter==2.1.7
# via wagtail
drf-spectacular==0.22.0
drf-spectacular==0.26.4
# via -r requirements.in
environs==9.5.0
# via -r requirements.in
et-xmlfile==1.1.0
# via openpyxl
executing==0.8.3
exceptiongroup==1.1.2
# via
# anyio
# pytest
executing==1.2.0
# via stack-data
factory-boy==3.2.1
factory-boy==3.3.0
# via
# -r requirements-dev.in
# wagtail-factories
faker==13.3.4
faker==19.3.0
# via factory-boy
filelock==3.6.0
filelock==3.12.2
# via virtualenv
flake8==4.0.1
filetype==1.2.0
# via willow
flake8==6.1.0
# via
# -r requirements-dev.in
# flake8-isort
flake8-isort==4.1.1
flake8-isort==6.0.0
# via -r requirements-dev.in
gitdb==4.0.9
gitdb==4.0.10
# via gitdb2
gitdb2==4.0.2
# via gitpython
gitpython==3.0.6
# via trufflehog
graphene==3.2.2
graphene==3.3
# via graphene-django
graphene-django==3.0.0
graphene-django==3.1.5
# via wagtail-grapple
graphql-core==3.2.3
# via
@ -236,63 +237,59 @@ graphql-relay==3.2.0
# via
# graphene
# graphene-django
gunicorn==20.1.0
gunicorn==21.2.0
# via -r requirements.in
h11==0.13.0
h11==0.14.0
# via uvicorn
html5lib==1.1
# via wagtail
httptools==0.4.0
httptools==0.6.0
# via uvicorn
identify==2.4.12
identify==2.5.26
# via pre-commit
idna==3.3
idna==3.4
# via
# anyio
# requests
inflection==0.5.1
# via drf-spectacular
iniconfig==1.1.1
iniconfig==2.0.0
# via pytest
ipdb==0.13.9
ipdb==0.13.13
# via -r requirements-dev.in
ipython==8.2.0
ipython==8.14.0
# via ipdb
isodate==0.6.1
# via azure-storage-blob
isort==5.10.1
isort==5.12.0
# via
# flake8-isort
# pylint
itypes==1.2.0
# via coreapi
jedi==0.18.1
jedi==0.19.0
# via ipython
jinja2==3.1.1
# via coreschema
jmespath==1.0.1
# via
# boto3
# botocore
jsonfield==3.1.0
# via django-notifications-hq
jsonschema==4.4.0
jsonschema==4.19.0
# via drf-spectacular
jsonschema-specifications==2023.7.1
# via jsonschema
l18n==2021.3
# via wagtail
lazy-object-proxy==1.7.1
lazy-object-proxy==1.9.0
# via astroid
libcst==0.4.7
libcst==1.0.1
# via
# ufmt
# usort
markupsafe==2.1.1
# via jinja2
marshmallow==3.15.0
marshmallow==3.20.1
# via environs
matplotlib-inline==0.1.3
matplotlib-inline==0.1.6
# via ipython
mccabe==0.6.1
mccabe==0.7.0
# via
# flake8
# pylint
@ -300,127 +297,127 @@ moreorless==0.4.0
# via
# ufmt
# usort
msal==1.22.0
msal==1.23.0
# via
# azure-identity
# msal-extensions
msal-extensions==1.0.0
# via azure-identity
mypy==0.942
mypy==1.4.1
# via
# -r requirements-dev.in
# django-stubs
# djangorestframework-stubs
mypy-extensions==0.4.3
mypy-extensions==1.0.0
# via
# black
# mypy
# typing-inspect
nodeenv==1.6.0
nodeenv==1.8.0
# via pre-commit
openpyxl==3.1.2
# via
# -r requirements.in
# wagtail
packaging==21.3
packaging==23.1
# via
# black
# build
# gunicorn
# marshmallow
# pytest
# pytest-sugar
# redis
parso==0.8.3
# via jedi
pathspec==0.9.0
pathspec==0.11.2
# via
# black
# trailrunner
pep517==0.12.0
# via build
pexpect==4.8.0
# via ipython
pickleshare==0.7.5
# via ipython
pillow==9.0.1
pillow==10.0.0
# via
# -r requirements.in
# pillow-heif
# wagtail
pip-tools==6.9.0
pillow-heif==0.13.0
# via willow
pip-tools==7.3.0
# via -r requirements-dev.in
platformdirs==2.5.1
platformdirs==3.10.0
# via
# black
# pylint
# virtualenv
pluggy==1.0.0
pluggy==1.2.0
# via pytest
polib==1.1.1
polib==1.2.0
# via wagtail-localize
portalocker==2.4.0
portalocker==2.7.0
# via
# concurrent-log-handler
# msal-extensions
pre-commit==2.17.0
pre-commit==3.3.3
# via -r requirements-dev.in
promise==2.3
# via graphene-django
prompt-toolkit==3.0.28
prompt-toolkit==3.0.39
# via ipython
psycopg2-binary==2.9.3
psycopg2-binary==2.9.7
# via -r requirements.in
ptyprocess==0.7.0
# via pexpect
pure-eval==0.2.2
# via stack-data
py==1.11.0
# via pytest
pycodestyle==2.8.0
pycodestyle==2.11.0
# via flake8
pycparser==2.21
# via cffi
pyflakes==2.4.0
pycryptodome==3.18.0
# via -r requirements.in
pyflakes==3.1.0
# via flake8
pygments==2.11.2
pygments==2.16.1
# via ipython
pyjwt[crypto]==2.7.0
pyjwt[crypto]==2.8.0
# via msal
pylint==2.13.4
pylint==2.17.5
# via
# pylint-django
# pylint-plugin-utils
pylint-django==2.5.3
# via -r requirements-dev.in
pylint-plugin-utils==0.7
pylint-plugin-utils==0.8.2
# via pylint-django
pyparsing==3.0.7
# via packaging
pyrsistent==0.18.1
# via jsonschema
pytest==7.1.3
pyproject-hooks==1.0.0
# via build
pytest==7.4.0
# via
# -r requirements-dev.in
# pytest-django
# pytest-sugar
pytest-django==4.5.2
# via -r requirements-dev.in
pytest-sugar==0.9.4
pytest-sugar==0.9.7
# via -r requirements-dev.in
python-dateutil==2.8.2
# via
# -r requirements.in
# botocore
# faker
python-dotenv==0.20.0
python-dotenv==1.0.0
# via
# environs
# uvicorn
python-http-client==3.3.7
# via sendgrid
python-json-logger==2.0.2
python-json-logger==2.0.7
# via -r requirements.in
python-slugify==6.1.1
python-slugify==8.0.1
# via -r requirements.in
pytz==2022.1
pytz==2023.3
# via
# -r requirements.in
# django
@ -428,94 +425,97 @@ pytz==2022.1
# django-notifications-hq
# djangorestframework
# l18n
pyyaml==6.0
pyyaml==6.0.1
# via
# caprover-api
# drf-spectacular
# libcst
# pre-commit
# uvicorn
redis==4.2.1
redis==4.6.0
# via
# -r requirements.in
# django-redis
requests==2.27.1
referencing==0.30.2
# via
# jsonschema
# jsonschema-specifications
requests==2.31.0
# via
# azure-core
# caprover-api
# coreapi
# djangorestframework-stubs
# msal
# wagtail
s3transfer==0.6.0
rpds-py==0.9.2
# via
# jsonschema
# referencing
s3transfer==0.6.1
# via boto3
sendgrid==6.9.7
sendgrid==6.10.0
# via -r requirements.in
sentry-sdk==1.5.8
sentry-sdk==1.29.2
# via -r requirements.in
six==1.16.0
# via
# asttokens
# azure-core
# azure-identity
# django-coverage-plugin
# html5lib
# isodate
# l18n
# promise
# python-dateutil
# virtualenv
smmap==5.0.0
# via gitdb
sniffio==1.2.0
sniffio==1.3.0
# via anyio
soupsieve==2.3.2.post1
soupsieve==2.4.1
# via beautifulsoup4
sqlparse==0.4.2
sqlparse==0.4.4
# via
# django
# django-debug-toolbar
stack-data==0.2.0
stack-data==0.6.2
# via ipython
starkbank-ecdsa==2.2.0
# via sendgrid
stdlibs==2022.6.8
stdlibs==2022.10.9
# via usort
structlog==21.5.0
structlog==23.1.0
# via -r requirements.in
swapper==1.3.0
# via django-notifications-hq
telepath==0.2
telepath==0.3.1
# via wagtail
termcolor==1.1.0
termcolor==2.3.0
# via pytest-sugar
testfixtures==6.18.5
# via flake8-isort
text-unidecode==1.3
# via
# graphene-django
# python-slugify
toml==0.10.2
# via
# ipdb
# pre-commit
# usort
# via usort
tomli==2.0.1
# via
# black
# build
# django-stubs
# ipdb
# mypy
# pep517
# pip-tools
# pylint
# pyproject-hooks
# pytest
tomlkit==0.11.5
# via ufmt
trailrunner==1.2.1
tomlkit==0.12.1
# via
# pylint
# ufmt
trailrunner==1.4.0
# via
# ufmt
# usort
traitlets==5.1.1
traitlets==5.9.0
# via
# ipython
# matplotlib-inline
@ -523,14 +523,23 @@ trufflehog==2.2.1
# via -r requirements-dev.in
trufflehogregexes==0.0.7
# via trufflehog
types-pytz==2021.3.6
types-pytz==2023.3.0.0
# via django-stubs
types-pyyaml==6.0.5
# via django-stubs
typing-extensions==4.5.0
types-pyyaml==6.0.12.11
# via
# django-stubs
# djangorestframework-stubs
types-requests==2.31.0.2
# via djangorestframework-stubs
types-urllib3==1.26.25.14
# via types-requests
typing-extensions==4.7.1
# via
# asgiref
# astroid
# azure-core
# azure-storage-blob
# dj-database-url
# django-stubs
# django-stubs-ext
# djangorestframework-stubs
@ -538,63 +547,60 @@ typing-extensions==4.5.0
# mypy
# typing-inspect
# ufmt
# uvicorn
# wagtail-localize
typing-inspect==0.8.0
typing-inspect==0.9.0
# via libcst
ufmt==2.0.1
ufmt==2.2.0
# via -r requirements-dev.in
uritemplate==4.1.1
# via
# coreapi
# drf-spectacular
urllib3==1.26.9
# via drf-spectacular
urllib3==1.26.16
# via
# botocore
# requests
# sentry-sdk
usort==1.0.5
usort==1.0.7
# via ufmt
uvicorn[standard]==0.18.3
uvicorn[standard]==0.23.2
# via -r requirements.in
uvloop==0.16.0
uvloop==0.17.0
# via uvicorn
virtualenv==20.14.0
virtualenv==20.24.2
# via pre-commit
wagtail==4.2.2
wagtail==5.1
# via
# -r requirements.in
# wagtail-factories
# wagtail-grapple
# wagtail-headless-preview
# wagtail-localize
wagtail-factories==4.0.0
wagtail-factories==4.1.0
# via -r requirements.in
wagtail-grapple==0.19.2
wagtail-grapple==0.20.0
# via -r requirements.in
wagtail-headless-preview==0.4.0
wagtail-headless-preview==0.6.0
# via wagtail-grapple
wagtail-localize==1.5
wagtail-localize==1.5.1
# via -r requirements.in
watchfiles==0.17.0
watchfiles==0.19.0
# via
# django-watchfiles
# uvicorn
wcwidth==0.2.5
wcwidth==0.2.6
# via prompt-toolkit
webencodings==0.5.1
# via html5lib
websockets==10.2
websockets==11.0.3
# via uvicorn
wheel==0.37.1
wheel==0.41.1
# via pip-tools
whitenoise[brotli]==6.0.0
whitenoise[brotli]==6.5.0
# via -r requirements.in
willow==1.4.1
willow[heif]==1.6.1
# via wagtail
wrapt==1.14.0
# via
# astroid
# deprecated
wrapt==1.15.0
# via astroid
# The following packages are considered to be unsafe in a requirements file:
# pip

View File

@ -38,6 +38,7 @@ structlog
python-json-logger
concurrent-log-handler
python-dateutil
pycryptodome
wagtail>=4
wagtail-factories>=4

View File

@ -2,75 +2,77 @@
# This file is autogenerated by pip-compile with python 3.10
# To update, run:
#
# pip-compile --output-file=requirements.txt requirements.in
# pip-compile requirements.in
#
aniso8601==9.0.1
# via graphene
anyascii==0.3.1
anyascii==0.3.2
# via wagtail
anyio==3.5.0
anyio==3.7.1
# via watchfiles
argon2-cffi==21.3.0
# via -r requirements.in
argon2-cffi-bindings==21.2.0
# via argon2-cffi
asgiref==3.5.0
asgiref==3.7.2
# via django
async-timeout==4.0.2
# via redis
attrs==21.4.0
# via jsonschema
authlib==1.0.0
attrs==23.1.0
# via
# jsonschema
# referencing
authlib==1.2.1
# via -r requirements.in
azure-core==1.26.4
azure-core==1.29.1
# via
# azure-identity
# azure-storage-blob
azure-identity==1.13.0
azure-identity==1.14.0
# via -r requirements.in
azure-storage-blob==12.16.0
azure-storage-blob==12.17.0
# via
# -r requirements.in
# django-storages
beautifulsoup4==4.9.3
beautifulsoup4==4.11.2
# via wagtail
boto3==1.26.11
boto3==1.28.23
# via -r requirements.in
botocore==1.29.11
botocore==1.31.23
# via
# boto3
# s3transfer
brotli==1.0.9
# via whitenoise
certifi==2021.10.8
certifi==2023.7.22
# via
# requests
# sentry-sdk
cffi==1.15.0
cffi==1.15.1
# via
# argon2-cffi-bindings
# cryptography
charset-normalizer==2.0.12
charset-normalizer==3.2.0
# via requests
click==8.1.1
click==8.1.6
# via
# -r requirements.in
# django-click
# uvicorn
concurrent-log-handler==0.9.20
concurrent-log-handler==0.9.24
# via -r requirements.in
cryptography==36.0.2
cryptography==41.0.3
# via
# authlib
# azure-identity
# azure-storage-blob
# msal
# pyjwt
deprecated==1.2.13
# via redis
dj-database-url==1.0.0
defusedxml==0.7.1
# via willow
dj-database-url==2.0.0
# via -r requirements.in
django==3.2.13
django==3.2.20
# via
# -r requirements.in
# dj-database-url
@ -91,21 +93,20 @@ django==3.2.13
# graphene-django
# jsonfield
# wagtail
# wagtail-grapple
# wagtail-localize
django-click==2.3.0
# via -r requirements.in
django-cors-headers==3.11.0
django-cors-headers==4.2.0
# via -r requirements.in
django-csp==3.7
# via -r requirements.in
django-filter==21.1
django-filter==23.2
# via wagtail
django-ipware==4.0.2
django-ipware==5.0.0
# via -r requirements.in
django-jsonform==2.17.0
django-jsonform==2.18.0
# via -r requirements.in
django-model-utils==4.2.0
django-model-utils==4.3.1
# via
# -r requirements.in
# django-notifications-hq
@ -115,36 +116,40 @@ django-notifications-hq==1.8.2
# via -r requirements.in
django-permissionedforms==0.1
# via wagtail
django-ratelimit==3.0.1
django-ratelimit==4.1.0
# via -r requirements.in
django-redis==5.2.0
django-redis==5.3.0
# via -r requirements.in
django-storages[azure]==1.13.1
django-storages[azure]==1.13.2
# via -r requirements.in
django-taggit==2.1.0
django-taggit==4.0.0
# via wagtail
django-treebeard==4.5.1
django-treebeard==4.7
# via wagtail
djangorestframework==3.13.1
djangorestframework==3.14.0
# via
# -r requirements.in
# drf-spectacular
# wagtail
draftjs-exporter==2.1.7
# via wagtail
drf-spectacular==0.22.0
drf-spectacular==0.26.4
# via -r requirements.in
environs==9.5.0
# via -r requirements.in
et-xmlfile==1.1.0
# via openpyxl
factory-boy==3.2.1
exceptiongroup==1.1.2
# via anyio
factory-boy==3.3.0
# via wagtail-factories
faker==13.11.1
faker==19.3.0
# via factory-boy
graphene==3.2.2
filetype==1.2.0
# via willow
graphene==3.3
# via graphene-django
graphene-django==3.0.0
graphene-django==3.1.5
# via wagtail-grapple
graphql-core==3.2.3
# via
@ -155,15 +160,15 @@ graphql-relay==3.2.0
# via
# graphene
# graphene-django
gunicorn==20.1.0
gunicorn==21.2.0
# via -r requirements.in
h11==0.13.0
h11==0.14.0
# via uvicorn
html5lib==1.1
# via wagtail
httptools==0.4.0
httptools==0.6.0
# via uvicorn
idna==3.3
idna==3.4
# via
# anyio
# requests
@ -177,13 +182,15 @@ jmespath==1.0.1
# botocore
jsonfield==3.1.0
# via django-notifications-hq
jsonschema==4.4.0
jsonschema==4.19.0
# via drf-spectacular
jsonschema-specifications==2023.7.1
# via jsonschema
l18n==2021.3
# via wagtail
marshmallow==3.15.0
marshmallow==3.20.1
# via environs
msal==1.22.0
msal==1.23.0
# via
# azure-identity
# msal-extensions
@ -193,48 +200,49 @@ openpyxl==3.1.2
# via
# -r requirements.in
# wagtail
packaging==21.3
packaging==23.1
# via
# gunicorn
# marshmallow
# redis
pillow==9.0.1
pillow==10.0.0
# via
# -r requirements.in
# pillow-heif
# wagtail
polib==1.1.1
pillow-heif==0.13.0
# via willow
polib==1.2.0
# via wagtail-localize
portalocker==2.4.0
portalocker==2.7.0
# via
# concurrent-log-handler
# msal-extensions
promise==2.3
# via graphene-django
psycopg2-binary==2.9.3
psycopg2-binary==2.9.7
# via -r requirements.in
pycparser==2.21
# via cffi
pyjwt[crypto]==2.7.0
pycryptodome==3.18.0
# via -r requirements.in
pyjwt[crypto]==2.8.0
# via msal
pyparsing==3.0.7
# via packaging
pyrsistent==0.18.1
# via jsonschema
python-dateutil==2.8.2
# via
# -r requirements.in
# botocore
# faker
python-dotenv==0.20.0
python-dotenv==1.0.0
# via
# environs
# uvicorn
python-http-client==3.3.7
# via sendgrid
python-json-logger==2.0.2
python-json-logger==2.0.7
# via -r requirements.in
python-slugify==6.1.1
python-slugify==8.0.1
# via -r requirements.in
pytz==2022.1
pytz==2023.3
# via
# -r requirements.in
# django
@ -242,95 +250,100 @@ pytz==2022.1
# django-notifications-hq
# djangorestframework
# l18n
pyyaml==6.0
pyyaml==6.0.1
# via
# drf-spectacular
# uvicorn
redis==4.2.1
redis==4.6.0
# via
# -r requirements.in
# django-redis
requests==2.27.1
referencing==0.30.2
# via
# jsonschema
# jsonschema-specifications
requests==2.31.0
# via
# azure-core
# msal
# wagtail
s3transfer==0.6.0
rpds-py==0.9.2
# via
# jsonschema
# referencing
s3transfer==0.6.1
# via boto3
sendgrid==6.9.7
sendgrid==6.10.0
# via -r requirements.in
sentry-sdk==1.5.8
sentry-sdk==1.29.2
# via -r requirements.in
six==1.16.0
# via
# azure-core
# azure-identity
# html5lib
# isodate
# l18n
# promise
# python-dateutil
sniffio==1.2.0
sniffio==1.3.0
# via anyio
soupsieve==2.3.2.post1
soupsieve==2.4.1
# via beautifulsoup4
sqlparse==0.4.2
sqlparse==0.4.4
# via django
starkbank-ecdsa==2.2.0
# via sendgrid
structlog==21.5.0
structlog==23.1.0
# via -r requirements.in
swapper==1.3.0
# via django-notifications-hq
telepath==0.2
telepath==0.3.1
# via wagtail
text-unidecode==1.3
# via
# graphene-django
# python-slugify
typing-extensions==4.5.0
typing-extensions==4.7.1
# via
# asgiref
# azure-core
# azure-storage-blob
# dj-database-url
# uvicorn
# wagtail-localize
uritemplate==4.1.1
# via drf-spectacular
urllib3==1.26.9
urllib3==1.26.16
# via
# botocore
# requests
# sentry-sdk
uvicorn[standard]==0.18.3
uvicorn[standard]==0.23.2
# via -r requirements.in
uvloop==0.16.0
uvloop==0.17.0
# via uvicorn
wagtail==4.2.2
wagtail==5.1
# via
# -r requirements.in
# wagtail-factories
# wagtail-grapple
# wagtail-headless-preview
# wagtail-localize
wagtail-factories==4.0.0
wagtail-factories==4.1.0
# via -r requirements.in
wagtail-grapple==0.19.2
wagtail-grapple==0.20.0
# via -r requirements.in
wagtail-headless-preview==0.4.0
wagtail-headless-preview==0.6.0
# via wagtail-grapple
wagtail-localize==1.5
wagtail-localize==1.5.1
# via -r requirements.in
watchfiles==0.17.0
watchfiles==0.19.0
# via uvicorn
webencodings==0.5.1
# via html5lib
websockets==10.2
websockets==11.0.3
# via uvicorn
whitenoise[brotli]==6.0.0
whitenoise[brotli]==6.5.0
# via -r requirements.in
willow==1.4.1
willow[heif]==1.6.1
# via wagtail
wrapt==1.14.0
# via deprecated
# The following packages are considered to be unsafe in a requirements file:
# setuptools

View File

@ -11,7 +11,7 @@ from django.http import HttpResponse, HttpResponseRedirect, JsonResponse
from django.shortcuts import render
from django.template import loader
from django.views.decorators.csrf import ensure_csrf_cookie
from ratelimit.decorators import ratelimit
from django_ratelimit.decorators import ratelimit
from rest_framework import authentication
from rest_framework.decorators import (
api_view,

View File

@ -48,6 +48,7 @@ from vbv_lernwelt.learnpath.tests.learning_path_factories import (
CircleFactory,
LearningContentAssignmentFactory,
LearningContentAttendanceCourseFactory,
LearningContentEdoniqTestFactory,
LearningContentFeedbackFactory,
LearningContentLearningModuleFactory,
LearningContentMediaLibraryFactory,
@ -309,6 +310,16 @@ damit du erfolgreich mit deinem Lernpfad (durch-)starten kannst.
slug__startswith="test-lehrgang-assignment-fahrzeug-mein-erstes-auto"
),
),
LearningContentEdoniqTestFactory(
title="Wissens- und Verständnisfragen",
parent=circle,
description=RichText(
"<p>Folgender Test mit Wissens- und Verständnisfragen ist Teil des Kompetenznachweises. Der Test kann nur einmal durchgeführt werden und ist notenrelevant.</p>"
),
checkbox_text="Hiermit bestätige ich, dass ich die Anweisungen verstanden und die Redlichkeitserklärung akzeptiert habe.",
test_url="https://exam.vbv-afa.ch/e-tutor/v4/user/course/pre_course_object?aid=1689096897473,2147466097",
extended_time_test_url="https://exam2.vbv-afa.ch/e-tutor/v4/user/course/pre_course_object?aid=1689096897473,2147466097",
)
PerformanceCriteriaFactory(
parent=CompetencePage.objects.get(competence_id="X1"),

View File

@ -16,10 +16,10 @@ from vbv_lernwelt.learnpath.tests.learning_path_factories import (
LearningContentAssignmentFactory,
LearningContentAttendanceCourseFactory,
LearningContentDocumentListFactory,
LearningContentEdoniqTestFactory,
LearningContentFeedbackFactory,
LearningContentMediaLibraryFactory,
LearningContentPlaceholderFactory,
LearningContentTestFactory,
LearningPathFactory,
LearningSequenceFactory,
LearningUnitFactory,
@ -329,14 +329,14 @@ In diesem Circle erfährst du wie die überbetrieblichen Kurse aufgebaut sind. Z
],
)
LearningUnitFactory(title="Kompetenznachweis", title_hidden=True, parent=circle)
LearningContentTestFactory(
LearningContentEdoniqTestFactory(
title="Wissens- und Verständnisfragen",
parent=circle,
description=RichText(
"<p>Folgender Test mit Wissens- und Verständnisfragen ist Teil des Kompetenznachweises. Der Test kann nur einmal durchgeführt werden und ist notenrelevant.</p>"
),
checkbox_text="Hiermit bestätige ich, dass ich die Anweisungen verstanden habe und den Test durchführen möchte.",
content_url="https://exam.vbv-afa.ch/",
checkbox_text="Hiermit bestätige ich, dass ich die Anweisungen verstanden und die Redlichkeitserklärung akzeptiert habe.",
test_url="https://exam.vbv-afa.ch/e-tutor/v4/user/course/pre_course_object?aid=1689096897473,2147466097",
)
LearningUnitFactory(title="Feedback", title_hidden=True, parent=circle)
LearningContentFeedbackFactory(
@ -444,14 +444,14 @@ Dans ce cercle, tu apprendras comment les cours interentreprises sont structuré
LearningUnitFactory(
title="Contrôle de compétences", title_hidden=True, parent=circle
)
LearningContentTestFactory(
LearningContentEdoniqTestFactory(
title="Questions de connaissances et de compréhension",
parent=circle,
description=RichText(
"<p>Folgender Test mit Wissens- und Verständnisfragen ist Teil des Kompetenznachweises. Der Test kann nur einmal durchgeführt werden und ist notenrelevant.</p>"
),
checkbox_text="Hiermit bestätige ich, dass ich die Anweisungen verstanden habe und den Test durchführen möchte.",
content_url="https://exam.vbv-afa.ch/",
checkbox_text="Hiermit bestätige ich, dass ich die Anweisungen verstanden und die Redlichkeitserklärung akzeptiert habe.",
test_url="https://exam.vbv-afa.ch/e-tutor/v4/user/course/pre_course_object?aid=1689096897473,2147466097",
)
LearningUnitFactory(title="Feedback", title_hidden=True, parent=circle)
LearningContentFeedbackFactory(
@ -559,14 +559,14 @@ In questo Circle imparerai come sono strutturati i corsi interaziendali. Imparer
LearningUnitFactory(
title="Controllo delle competenze", title_hidden=True, parent=circle
)
LearningContentTestFactory(
LearningContentEdoniqTestFactory(
title="Domande di conoscenza e di comprensione",
parent=circle,
description=RichText(
"<p>Folgender Test mit Wissens- und Verständnisfragen ist Teil des Kompetenznachweises. Der Test kann nur einmal durchgeführt werden und ist notenrelevant.</p>"
),
checkbox_text="Hiermit bestätige ich, dass ich die Anweisungen verstanden habe und den Test durchführen möchte.",
content_url="https://exam.vbv-afa.ch/",
checkbox_text="Hiermit bestätige ich, dass ich die Anweisungen verstanden und die Redlichkeitserklärung akzeptiert habe.",
test_url="https://exam.vbv-afa.ch/e-tutor/v4/user/course/pre_course_object?aid=1689096897473,2147466097",
)
LearningUnitFactory(title="Feedback", title_hidden=True, parent=circle)
LearningContentFeedbackFactory(
@ -679,14 +679,15 @@ In diesem Circle lernst du die wichtigsten Grundlagen bezüglich Versicherungswi
)
LearningSequenceFactory(title="Transfer", parent=circle, icon="it-icon-ls-end")
LearningUnitFactory(title="Kompetenznachweis", title_hidden=True, parent=circle)
LearningContentTestFactory(
LearningContentEdoniqTestFactory(
title="Wissens- und Verständnisfragen",
parent=circle,
description=RichText(
"<p>Folgender Test mit Wissens- und Verständnisfragen ist Teil des Kompetenznachweises. Der Test kann nur einmal durchgeführt werden und ist notenrelevant.</p>"
),
checkbox_text="Hiermit bestätige ich, dass ich die Anweisungen verstanden habe und den Test durchführen möchte.",
content_url="https://exam.vbv-afa.ch/",
checkbox_text="Hiermit bestätige ich, dass ich die Anweisungen verstanden und die Redlichkeitserklärung akzeptiert habe.",
test_url="https://exam.vbv-afa.ch/e-tutor/v4/user/course/pre_course_object?aid=1689096523730,2147466125",
extended_time_test_url="https://exam.vbv-afa.ch/e-tutor/v4/user/course/pre_course_object?aid=1691157696911,2147478636",
)
LearningUnitFactory(title="Reflexion", title_hidden=True, parent=circle)
LearningContentAssignmentFactory(
@ -787,14 +788,15 @@ Dans ce cercle, tu apprends les bases les plus importantes en matière d'assuran
LearningUnitFactory(
title="Contrôle de compétences", title_hidden=True, parent=circle
)
LearningContentTestFactory(
LearningContentEdoniqTestFactory(
title="Questions de connaissances et de compréhension",
parent=circle,
description=RichText(
"<p>Folgender Test mit Wissens- und Verständnisfragen ist Teil des Kompetenznachweises. Der Test kann nur einmal durchgeführt werden und ist notenrelevant.</p>"
),
checkbox_text="Hiermit bestätige ich, dass ich die Anweisungen verstanden habe und den Test durchführen möchte.",
content_url="https://exam.vbv-afa.ch/",
checkbox_text="Hiermit bestätige ich, dass ich die Anweisungen verstanden und die Redlichkeitserklärung akzeptiert habe.",
test_url="https://exam.vbv-afa.ch/e-tutor/v4/user/course/pre_course_object?aid=1689096523730,2147466125",
extended_time_test_url="https://exam.vbv-afa.ch/e-tutor/v4/user/course/pre_course_object?aid=1691157696911,2147478636",
)
LearningUnitFactory(title="Réflexion", title_hidden=True, parent=circle)
LearningContentAssignmentFactory(
@ -895,14 +897,15 @@ In questo Circle imparerai le basi più importanti del settore assicurativo e de
LearningUnitFactory(
title="Controllo delle competenze", title_hidden=True, parent=circle
)
LearningContentTestFactory(
LearningContentEdoniqTestFactory(
title="Domande di conoscenza e di comprensione",
parent=circle,
description=RichText(
"<p>Folgender Test mit Wissens- und Verständnisfragen ist Teil des Kompetenznachweises. Der Test kann nur einmal durchgeführt werden und ist notenrelevant.</p>"
),
checkbox_text="Hiermit bestätige ich, dass ich die Anweisungen verstanden habe und den Test durchführen möchte.",
content_url="https://exam.vbv-afa.ch/",
checkbox_text="Hiermit bestätige ich, dass ich die Anweisungen verstanden und die Redlichkeitserklärung akzeptiert habe.",
test_url="https://exam.vbv-afa.ch/e-tutor/v4/user/course/pre_course_object?aid=1689096523730,2147466125",
extended_time_test_url="https://exam.vbv-afa.ch/e-tutor/v4/user/course/pre_course_object?aid=1691157696911,2147478636",
)
LearningUnitFactory(title="Riflessione", title_hidden=True, parent=circle)
LearningContentAssignmentFactory(

View File

@ -0,0 +1,52 @@
# Generated by Django 3.2.20 on 2023-08-17 13:14
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("learnpath", "0003_auto_20230810_0817"),
("course", "0003_alter_coursecompletion_additional_json_data"),
("duedate", "0002_alter_duedate_start"),
("course_session", "0003_initial"),
]
operations = [
migrations.CreateModel(
name="CourseSessionEdoniqTest",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"course_session",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="course.coursesession",
),
),
(
"due_date",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
related_name="edoniq_test_due_date",
to="duedate.duedate",
),
),
(
"learning_content",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="learnpath.learningcontentedoniqtest",
),
),
],
),
]

View File

@ -152,3 +152,22 @@ class CourseSessionAssignment(models.Model):
def __str__(self):
return f"{self.course_session} - {self.learning_content}"
class CourseSessionEdoniqTest(models.Model):
course_session = models.ForeignKey(
"course.CourseSession",
on_delete=models.CASCADE,
)
learning_content = models.ForeignKey(
"learnpath.LearningContentEdoniqTest",
on_delete=models.CASCADE,
)
due_date = models.OneToOneField(
"duedate.DueDate",
on_delete=models.CASCADE,
related_name="edoniq_test_due_date",
)
def __str__(self):
return f"{self.course_session} - {self.learning_content}"

View File

@ -4,7 +4,7 @@ from wagtail.models import Page
from vbv_lernwelt.duedate.models import DueDate
from vbv_lernwelt.learnpath.models import (
LearningContentAttendanceCourse,
LearningContentTest,
LearningContentEdoniqTest,
)
@ -23,7 +23,7 @@ class DueDateAdmin(admin.ModelAdmin):
csd = DueDate.objects.get(id=object_id)
kwargs["queryset"] = Page.objects.descendant_of(
csd.course_session.course.coursepage
).exact_type(LearningContentAttendanceCourse, LearningContentTest)
).exact_type(LearningContentAttendanceCourse, LearningContentEdoniqTest)
else:
kwargs["queryset"] = Page.objects.none()
return super().formfield_for_foreignkey(db_field, request, **kwargs)

View File

@ -0,0 +1,17 @@
# Generated by Django 3.2.20 on 2023-08-17 13:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("duedate", "0001_initial"),
]
operations = [
migrations.AlterField(
model_name="duedate",
name="start",
field=models.DateTimeField(blank=True, db_index=True, null=True),
),
]

View File

@ -0,0 +1,7 @@
from django.apps import AppConfig
class LearnpathConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "vbv_lernwelt.edoniq_test"

View File

@ -0,0 +1,45 @@
import base64
from datetime import datetime
import structlog
from Crypto.Cipher import PKCS1_v1_5
from Crypto.PublicKey import RSA
from django.conf import settings
logger = structlog.get_logger(__name__)
CIPHER_TRANSFORMATION = "RSA/ECB/PKCS1Padding"
def create_token(user_id: str):
try:
token = (
f"user:{user_id};system:{settings.EDONIQ_ENV};date:"
+ str(int(datetime.now().timestamp()))
+ ";"
)
encrypted = _encrypt_message(
token, settings.EDONIQ_CERTIFICATE.replace("\\n", "\n")
)
return str(base64.b64encode(encrypted), "utf-8")
except Exception as e:
logger.debug(
"get_edoniq_test_fail",
label="edoniq_test_api",
error=e,
)
def _encrypt_message(token: str, key: str):
key = RSA.import_key(key)
plainbytes = token.encode("utf-8")
cipherbytes = _encrypt(plainbytes, key)
encrypted_token = base64.b64encode(cipherbytes)
return encrypted_token
def _encrypt(token, key):
cipher = PKCS1_v1_5.new(key)
return cipher.encrypt(token)

View File

@ -0,0 +1,41 @@
from rest_framework.test import APITestCase
from vbv_lernwelt.core.create_default_users import create_default_users
from vbv_lernwelt.course.creators.test_course import create_test_course
from vbv_lernwelt.learnpath.models import LearningContentEdoniqTest
class RedirectTestCase(APITestCase):
def setUp(self) -> None:
create_default_users()
create_test_course(with_sessions=True)
def test_redirects_normal_test(self):
test = LearningContentEdoniqTest.objects.get(
title="Wissens- und Verständnisfragen"
)
data = {"learning_content_id": test.id, "extended_time_test": False}
self.client.login(username="admin", password="test")
self._test_redirect(
data,
"https://exam.vbv-afa.ch/e-tutor/v4/user/course/pre_course_object?aid=1689096897473,2147466097",
)
def test_redirects_extended_test(self):
test = LearningContentEdoniqTest.objects.get(
title="Wissens- und Verständnisfragen"
)
data = {"learning_content_id": test.id, "extended_time_test": True}
self.client.login(username="admin", password="test")
self._test_redirect(
data,
"https://exam2.vbv-afa.ch/e-tutor/v4/user/course/pre_course_object?aid=1689096897473,2147466097",
)
def _test_redirect(self, data, expected_url):
response = self.client.post(
"/api/core/edoniq-test/redirect/", data, format="json"
)
self.assertEqual(200, response.status_code)
content = response.json()
self.assertTrue(content["redirect_url"].startswith(expected_url))

View File

@ -1,3 +1,14 @@
import structlog
from django.core.exceptions import PermissionDenied
from django.http import JsonResponse
from django.shortcuts import get_object_or_404
from rest_framework.decorators import api_view
from vbv_lernwelt.course.permissions import has_course_access_by_page_request
from vbv_lernwelt.edoniq_test.edoniq_sso import create_token
from vbv_lernwelt.learnpath.models import LearningContentEdoniqTest
logger = structlog.get_logger(__name__)
import csv
from datetime import date
from itertools import chain
@ -15,6 +26,62 @@ UK_COURSE_IDS = [COURSE_UK, COURSE_UK_FR, COURSE_UK_IT]
DEFAULT_EXCLUDED_DOMAINS = ["eiger-versicherung.ch", "assurance.ch", "example.com"]
@api_view(["POST"])
def get_edoniq_token_redirect(request):
page_id = request.data.get("learning_content_id", "")
extended_time_test = request.data.get("extended_time_test", False)
logger.debug(
"get_edoniq_test_redirect",
label="edoniq_test_api",
page_id=page_id,
user_id=request.user.id,
extended_time_test=extended_time_test,
)
if not page_id:
return JsonResponse({"error": "learning_content_id is required"}, status=400)
try:
test_page = get_object_or_404(LearningContentEdoniqTest, id=page_id)
if not has_course_access_by_page_request(request, test_page):
logger.debug(
"get_edoniq_test_redirect_permission_denied",
label="edoniq_test_api",
page_id=page_id,
page_slug=test_page.slug,
page_title=test_page.title,
user_id=request.user.id,
)
raise PermissionDenied()
url = (
test_page.test_url
if not extended_time_test
else test_page.extended_time_test_url
)
token = create_token(
str(request.user.id),
)
redirect_url = f"{url}&ssoToken={token}"
response_data = {"redirect_url": redirect_url}
logger.debug(
"get_edoniq_test_redirect_permission_success",
label="edoniq_test_api",
page_id=page_id,
page_slug=test_page.slug,
page_title=test_page.title,
user_id=request.user.id,
)
return JsonResponse(response_data, status=200)
except Exception as e:
logger.error(e, exc_info=True)
return JsonResponse({"error": str(e)}, status=404)
@staff_member_required
def export_students(request):
course_session_users = fetch_course_session_users(UK_COURSE_IDS)
@ -35,6 +102,14 @@ def export_students_and_trainers(request):
return generate_export_response(course_session_users)
def fetch_course_session_users(courses: List[int]):
# if a user is in multiple courses, he should be exported multiple times
# todo: check if this is the case otherwise use .distinct("user")
return CourseSessionUser.objects.filter(
course_session__course__id__in=courses, role=CourseSessionUser.Role.MEMBER
).order_by("user__email")
def fetch_course_session_users(
courses: List[int], role=CourseSessionUser.Role.MEMBER, excluded_domains=None
):

View File

@ -13,12 +13,12 @@ from vbv_lernwelt.course.models import CourseCategory, CoursePage
from vbv_lernwelt.learnpath.tests.learning_path_factories import (
CircleFactory,
LearningContentAssignmentFactory,
LearningContentEdoniqTestFactory,
LearningContentFeedbackFactory,
LearningContentLearningModuleFactory,
LearningContentMediaLibraryFactory,
LearningContentPlaceholderFactory,
LearningContentRichTextFactory,
LearningContentTestFactory,
LearningContentVideoFactory,
LearningPathFactory,
LearningSequenceFactory,
@ -353,13 +353,13 @@ def create_circle_fahrzeug(lp, title="Fahrzeug"):
title="Praxisauftrag",
parent=circle,
)
LearningContentTestFactory(
LearningContentEdoniqTestFactory(
title="Fachcheck Fahrzeug",
parent=circle,
description=RichText(
"<p>Teste dein Wissen mit dem Fachcheck. Der Test kann nur einmal durchgeführt werden.</p>"
),
checkbox_text="Hiermit bestätige ich, dass ich die Anweisungen verstanden habe und den Test durchführen möchte.",
checkbox_text="Hiermit bestätige ich, dass ich die Anweisungen verstanden und die Redlichkeitserklärung akzeptiert habe.",
content_url="https://s3.eu-central-1.amazonaws.com/myvbv-wbt.iterativ.ch/fachcheck-fahrzeug-xapi-LFv8YiyM/index.html#/",
)
LearningContentAssignmentFactory(

View File

@ -9,12 +9,12 @@ from vbv_lernwelt.learnpath.models import (
LearningContentAssignment,
LearningContentAttendanceCourse,
LearningContentDocumentList,
LearningContentEdoniqTest,
LearningContentFeedback,
LearningContentLearningModule,
LearningContentMediaLibrary,
LearningContentPlaceholder,
LearningContentRichText,
LearningContentTest,
LearningContentVideo,
LearningPath,
LearningSequence,
@ -46,7 +46,7 @@ class LearningContentInterface(CoursePageInterface):
return LearningContentPlaceholderObjectType
elif isinstance(instance, LearningContentRichText):
return LearningContentRichTextObjectType
elif isinstance(instance, LearningContentTest):
elif isinstance(instance, LearningContentEdoniqTest):
return LearningContentTestObjectType
elif isinstance(instance, LearningContentVideo):
return LearningContentVideoObjectType
@ -104,7 +104,7 @@ class LearningContentMediaLibraryObjectType(DjangoObjectType):
class LearningContentTestObjectType(DjangoObjectType):
class Meta:
model = LearningContentTest
model = LearningContentEdoniqTest
interfaces = (LearningContentInterface,)
fields = []

View File

@ -0,0 +1,53 @@
# Generated by Django 3.2.13 on 2023-08-10 06:17
import django.db.models.deletion
import wagtail.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("course", "0003_alter_coursecompletion_additional_json_data"),
("wagtailcore", "0083_workflowcontenttype"),
("wagtailredirects", "0008_add_verbose_name_plural"),
("wagtailforms", "0005_alter_formsubmission_form_data"),
("duedate", "0001_initial"),
("learnpath", "0002_alter_learningcontentassignment_assignment_type"),
]
operations = [
migrations.CreateModel(
name="LearningContentEdoniqTest",
fields=[
(
"page_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="wagtailcore.page",
),
),
("minutes", models.PositiveIntegerField(default=15)),
("description", wagtail.fields.RichTextField(blank=True)),
("content_url", models.TextField(blank=True)),
("has_course_completion_status", models.BooleanField(default=True)),
(
"can_user_self_toggle_course_completion",
models.BooleanField(default=False),
),
("checkbox_text", models.TextField(blank=True)),
("test_url", models.TextField(blank=True)),
("extended_time_test_url", models.TextField(blank=True)),
],
options={
"abstract": False,
},
bases=("wagtailcore.page",),
),
migrations.DeleteModel(
name="LearningContentTest",
),
]

View File

@ -77,7 +77,7 @@ class Circle(CourseBasePage):
"learnpath.LearningContentMediaLibrary",
"learnpath.LearningContentPlaceholder",
"learnpath.LearningContentRichText",
"learnpath.LearningContentTest",
"learnpath.LearningContentEdoniqTest",
"learnpath.LearningContentVideo",
]
@ -321,19 +321,28 @@ class LearningContentMediaLibrary(LearningContent):
can_user_self_toggle_course_completion = models.BooleanField(default=True)
class LearningContentTest(LearningContent):
class LearningContentEdoniqTest(LearningContent):
serialize_field_names = LearningContent.serialize_field_names + [
"checkbox_text",
"has_extended_time_test",
]
parent_page_types = ["learnpath.Circle"]
subpage_types = []
checkbox_text = models.TextField(blank=True)
test_url = models.TextField(blank=True)
extended_time_test_url = models.TextField(blank=True)
content_panels = LearningContent.content_panels + [
FieldPanel("checkbox_text", classname="Text"),
FieldPanel("test_url", classname="Text"),
FieldPanel("extended_time_test_url", classname="Text"),
]
@property
def has_extended_time_test(self):
return bool(self.extended_time_test_url)
class LearningContentRichText(LearningContent):
text = RichTextField(blank=True, features=DEFAULT_RICH_TEXT_FEATURES_WITH_HEADER)

View File

@ -6,12 +6,12 @@ from vbv_lernwelt.learnpath.models import (
LearningContentAssignment,
LearningContentAttendanceCourse,
LearningContentDocumentList,
LearningContentEdoniqTest,
LearningContentFeedback,
LearningContentLearningModule,
LearningContentMediaLibrary,
LearningContentPlaceholder,
LearningContentRichText,
LearningContentTest,
LearningContentVideo,
LearningPath,
LearningSequence,
@ -149,14 +149,14 @@ class LearningContentMediaLibraryFactory(wagtail_factories.PageFactory):
model = LearningContentMediaLibrary
class LearningContentTestFactory(wagtail_factories.PageFactory):
class LearningContentEdoniqTestFactory(wagtail_factories.PageFactory):
title = "Fachcheck"
minutes = 0
content_url = ""
description = RichText("")
class Meta:
model = LearningContentTest
model = LearningContentEdoniqTest
class LearningContentRichTextFactory(wagtail_factories.PageFactory):

View File

@ -5,6 +5,7 @@ env/docker_local.env
server/vbv_lernwelt/assignment/creators/create_assignments.py
server/vbv_lernwelt/static/
server/vbv_lernwelt/media/
server/vbv_lernwelt/edoniq_test/certificates/test.key
supabase.md
scripts/supabase/init.sql
ramon.wenger@iterativ.ch.gpg