Merged in feature/zulassungsprofil-VBV-666-2024-08-07 (pull request #372)
Feature/zulassungsprofil VBV-666 2024 08 07
This commit is contained in:
commit
15f19f5756
|
|
@ -37,6 +37,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@graphql-codegen/cli": "^5.0.2",
|
"@graphql-codegen/cli": "^5.0.2",
|
||||||
"@graphql-codegen/client-preset": "^4.3.2",
|
"@graphql-codegen/client-preset": "^4.3.2",
|
||||||
|
"@parcel/watcher": "^2.4.1",
|
||||||
"@rushstack/eslint-patch": "^1.8.0",
|
"@rushstack/eslint-patch": "^1.8.0",
|
||||||
"@savvywombat/tailwindcss-grid-areas": "^4.0.0",
|
"@savvywombat/tailwindcss-grid-areas": "^4.0.0",
|
||||||
"@tailwindcss/forms": "^0.5.7",
|
"@tailwindcss/forms": "^0.5.7",
|
||||||
|
|
@ -3013,6 +3014,7 @@
|
||||||
"version": "2.4.1",
|
"version": "2.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.4.1.tgz",
|
||||||
"integrity": "sha512-HNjmfLQEVRZmHRET336f20H/8kOozUGwk7yajvsonjNxbj2wBTK1WsQuHkD5yYh9RxFGL2EyDHryOihOwUoKDA==",
|
"integrity": "sha512-HNjmfLQEVRZmHRET336f20H/8kOozUGwk7yajvsonjNxbj2wBTK1WsQuHkD5yYh9RxFGL2EyDHryOihOwUoKDA==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"detect-libc": "^1.0.3",
|
"detect-libc": "^1.0.3",
|
||||||
"is-glob": "^4.0.3",
|
"is-glob": "^4.0.3",
|
||||||
|
|
@ -3048,6 +3050,7 @@
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"android"
|
"android"
|
||||||
|
|
@ -3067,6 +3070,7 @@
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"darwin"
|
||||||
|
|
@ -3086,6 +3090,7 @@
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"darwin"
|
||||||
|
|
@ -3105,6 +3110,7 @@
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"freebsd"
|
"freebsd"
|
||||||
|
|
@ -3124,6 +3130,7 @@
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
|
|
@ -3143,6 +3150,7 @@
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
|
|
@ -3162,6 +3170,7 @@
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
|
|
@ -3181,6 +3190,7 @@
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
|
|
@ -3200,6 +3210,7 @@
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
|
|
@ -3219,6 +3230,7 @@
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
|
|
@ -3238,6 +3250,7 @@
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
|
|
@ -3257,6 +3270,7 @@
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
|
|
@ -7246,6 +7260,7 @@
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
|
||||||
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
|
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
|
||||||
|
"dev": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"detect-libc": "bin/detect-libc.js"
|
"detect-libc": "bin/detect-libc.js"
|
||||||
},
|
},
|
||||||
|
|
@ -10481,7 +10496,8 @@
|
||||||
"node_modules/node-addon-api": {
|
"node_modules/node-addon-api": {
|
||||||
"version": "7.1.1",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
|
||||||
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="
|
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/node-fetch": {
|
"node_modules/node-fetch": {
|
||||||
"version": "2.7.0",
|
"version": "2.7.0",
|
||||||
|
|
@ -16866,6 +16882,7 @@
|
||||||
"version": "2.4.1",
|
"version": "2.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.4.1.tgz",
|
||||||
"integrity": "sha512-HNjmfLQEVRZmHRET336f20H/8kOozUGwk7yajvsonjNxbj2wBTK1WsQuHkD5yYh9RxFGL2EyDHryOihOwUoKDA==",
|
"integrity": "sha512-HNjmfLQEVRZmHRET336f20H/8kOozUGwk7yajvsonjNxbj2wBTK1WsQuHkD5yYh9RxFGL2EyDHryOihOwUoKDA==",
|
||||||
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@parcel/watcher-android-arm64": "2.4.1",
|
"@parcel/watcher-android-arm64": "2.4.1",
|
||||||
"@parcel/watcher-darwin-arm64": "2.4.1",
|
"@parcel/watcher-darwin-arm64": "2.4.1",
|
||||||
|
|
@ -16889,72 +16906,84 @@
|
||||||
"version": "2.4.1",
|
"version": "2.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.4.1.tgz",
|
||||||
"integrity": "sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg==",
|
"integrity": "sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg==",
|
||||||
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@parcel/watcher-darwin-arm64": {
|
"@parcel/watcher-darwin-arm64": {
|
||||||
"version": "2.4.1",
|
"version": "2.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.4.1.tgz",
|
||||||
"integrity": "sha512-ln41eihm5YXIY043vBrrHfn94SIBlqOWmoROhsMVTSXGh0QahKGy77tfEywQ7v3NywyxBBkGIfrWRHm0hsKtzA==",
|
"integrity": "sha512-ln41eihm5YXIY043vBrrHfn94SIBlqOWmoROhsMVTSXGh0QahKGy77tfEywQ7v3NywyxBBkGIfrWRHm0hsKtzA==",
|
||||||
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@parcel/watcher-darwin-x64": {
|
"@parcel/watcher-darwin-x64": {
|
||||||
"version": "2.4.1",
|
"version": "2.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.4.1.tgz",
|
||||||
"integrity": "sha512-yrw81BRLjjtHyDu7J61oPuSoeYWR3lDElcPGJyOvIXmor6DEo7/G2u1o7I38cwlcoBHQFULqF6nesIX3tsEXMg==",
|
"integrity": "sha512-yrw81BRLjjtHyDu7J61oPuSoeYWR3lDElcPGJyOvIXmor6DEo7/G2u1o7I38cwlcoBHQFULqF6nesIX3tsEXMg==",
|
||||||
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@parcel/watcher-freebsd-x64": {
|
"@parcel/watcher-freebsd-x64": {
|
||||||
"version": "2.4.1",
|
"version": "2.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.4.1.tgz",
|
||||||
"integrity": "sha512-TJa3Pex/gX3CWIx/Co8k+ykNdDCLx+TuZj3f3h7eOjgpdKM+Mnix37RYsYU4LHhiYJz3DK5nFCCra81p6g050w==",
|
"integrity": "sha512-TJa3Pex/gX3CWIx/Co8k+ykNdDCLx+TuZj3f3h7eOjgpdKM+Mnix37RYsYU4LHhiYJz3DK5nFCCra81p6g050w==",
|
||||||
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@parcel/watcher-linux-arm-glibc": {
|
"@parcel/watcher-linux-arm-glibc": {
|
||||||
"version": "2.4.1",
|
"version": "2.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.4.1.tgz",
|
||||||
"integrity": "sha512-4rVYDlsMEYfa537BRXxJ5UF4ddNwnr2/1O4MHM5PjI9cvV2qymvhwZSFgXqbS8YoTk5i/JR0L0JDs69BUn45YA==",
|
"integrity": "sha512-4rVYDlsMEYfa537BRXxJ5UF4ddNwnr2/1O4MHM5PjI9cvV2qymvhwZSFgXqbS8YoTk5i/JR0L0JDs69BUn45YA==",
|
||||||
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@parcel/watcher-linux-arm64-glibc": {
|
"@parcel/watcher-linux-arm64-glibc": {
|
||||||
"version": "2.4.1",
|
"version": "2.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.4.1.tgz",
|
||||||
"integrity": "sha512-BJ7mH985OADVLpbrzCLgrJ3TOpiZggE9FMblfO65PlOCdG++xJpKUJ0Aol74ZUIYfb8WsRlUdgrZxKkz3zXWYA==",
|
"integrity": "sha512-BJ7mH985OADVLpbrzCLgrJ3TOpiZggE9FMblfO65PlOCdG++xJpKUJ0Aol74ZUIYfb8WsRlUdgrZxKkz3zXWYA==",
|
||||||
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@parcel/watcher-linux-arm64-musl": {
|
"@parcel/watcher-linux-arm64-musl": {
|
||||||
"version": "2.4.1",
|
"version": "2.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.4.1.tgz",
|
||||||
"integrity": "sha512-p4Xb7JGq3MLgAfYhslU2SjoV9G0kI0Xry0kuxeG/41UfpjHGOhv7UoUDAz/jb1u2elbhazy4rRBL8PegPJFBhA==",
|
"integrity": "sha512-p4Xb7JGq3MLgAfYhslU2SjoV9G0kI0Xry0kuxeG/41UfpjHGOhv7UoUDAz/jb1u2elbhazy4rRBL8PegPJFBhA==",
|
||||||
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@parcel/watcher-linux-x64-glibc": {
|
"@parcel/watcher-linux-x64-glibc": {
|
||||||
"version": "2.4.1",
|
"version": "2.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.4.1.tgz",
|
||||||
"integrity": "sha512-s9O3fByZ/2pyYDPoLM6zt92yu6P4E39a03zvO0qCHOTjxmt3GHRMLuRZEWhWLASTMSrrnVNWdVI/+pUElJBBBg==",
|
"integrity": "sha512-s9O3fByZ/2pyYDPoLM6zt92yu6P4E39a03zvO0qCHOTjxmt3GHRMLuRZEWhWLASTMSrrnVNWdVI/+pUElJBBBg==",
|
||||||
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@parcel/watcher-linux-x64-musl": {
|
"@parcel/watcher-linux-x64-musl": {
|
||||||
"version": "2.4.1",
|
"version": "2.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.4.1.tgz",
|
||||||
"integrity": "sha512-L2nZTYR1myLNST0O632g0Dx9LyMNHrn6TOt76sYxWLdff3cB22/GZX2UPtJnaqQPdCRoszoY5rcOj4oMTtp5fQ==",
|
"integrity": "sha512-L2nZTYR1myLNST0O632g0Dx9LyMNHrn6TOt76sYxWLdff3cB22/GZX2UPtJnaqQPdCRoszoY5rcOj4oMTtp5fQ==",
|
||||||
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@parcel/watcher-win32-arm64": {
|
"@parcel/watcher-win32-arm64": {
|
||||||
"version": "2.4.1",
|
"version": "2.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.4.1.tgz",
|
||||||
"integrity": "sha512-Uq2BPp5GWhrq/lcuItCHoqxjULU1QYEcyjSO5jqqOK8RNFDBQnenMMx4gAl3v8GiWa59E9+uDM7yZ6LxwUIfRg==",
|
"integrity": "sha512-Uq2BPp5GWhrq/lcuItCHoqxjULU1QYEcyjSO5jqqOK8RNFDBQnenMMx4gAl3v8GiWa59E9+uDM7yZ6LxwUIfRg==",
|
||||||
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@parcel/watcher-win32-ia32": {
|
"@parcel/watcher-win32-ia32": {
|
||||||
"version": "2.4.1",
|
"version": "2.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.4.1.tgz",
|
||||||
"integrity": "sha512-maNRit5QQV2kgHFSYwftmPBxiuK5u4DXjbXx7q6eKjq5dsLXZ4FJiVvlcw35QXzk0KrUecJmuVFbj4uV9oYrcw==",
|
"integrity": "sha512-maNRit5QQV2kgHFSYwftmPBxiuK5u4DXjbXx7q6eKjq5dsLXZ4FJiVvlcw35QXzk0KrUecJmuVFbj4uV9oYrcw==",
|
||||||
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@parcel/watcher-win32-x64": {
|
"@parcel/watcher-win32-x64": {
|
||||||
"version": "2.4.1",
|
"version": "2.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.4.1.tgz",
|
||||||
"integrity": "sha512-+DvS92F9ezicfswqrvIRM2njcYJbd5mb9CUgtrHCHmvn7pPPa+nMDRu1o1bYYz/l5IB2NVGNJWiH7h1E58IF2A==",
|
"integrity": "sha512-+DvS92F9ezicfswqrvIRM2njcYJbd5mb9CUgtrHCHmvn7pPPa+nMDRu1o1bYYz/l5IB2NVGNJWiH7h1E58IF2A==",
|
||||||
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@peculiar/asn1-schema": {
|
"@peculiar/asn1-schema": {
|
||||||
|
|
@ -19874,7 +19903,8 @@
|
||||||
"detect-libc": {
|
"detect-libc": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
|
||||||
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="
|
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"didyoumean": {
|
"didyoumean": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
|
|
@ -22250,7 +22280,8 @@
|
||||||
"node-addon-api": {
|
"node-addon-api": {
|
||||||
"version": "7.1.1",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
|
||||||
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="
|
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"node-fetch": {
|
"node-fetch": {
|
||||||
"version": "2.7.0",
|
"version": "2.7.0",
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,9 @@
|
||||||
"cypress:open": "cypress open",
|
"cypress:open": "cypress open",
|
||||||
"dev": "concurrently \"vite\" \"npm run codegen:watch\"",
|
"dev": "concurrently \"vite\" \"npm run codegen:watch\"",
|
||||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
||||||
|
"lint:errors": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --quiet --ignore-path .gitignore",
|
||||||
"prettier": "prettier . --write",
|
"prettier": "prettier . --write",
|
||||||
"prettier:check": "prettier . --check",
|
"prettier:check": "prettier . --check --ignore-unknown",
|
||||||
"tailwind": "tailwindcss -i tailwind.css -o ../server/vbv_lernwelt/static/css/tailwind.css --watch",
|
"tailwind": "tailwindcss -i tailwind.css -o ../server/vbv_lernwelt/static/css/tailwind.css --watch",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"typecheck": "npm run codegen && vue-tsc --noEmit -p tsconfig.app.json --composite false",
|
"typecheck": "npm run codegen && vue-tsc --noEmit -p tsconfig.app.json --composite false",
|
||||||
|
|
@ -47,6 +48,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@graphql-codegen/cli": "^5.0.2",
|
"@graphql-codegen/cli": "^5.0.2",
|
||||||
"@graphql-codegen/client-preset": "^4.3.2",
|
"@graphql-codegen/client-preset": "^4.3.2",
|
||||||
|
"@parcel/watcher": "^2.4.1",
|
||||||
"@rushstack/eslint-patch": "^1.8.0",
|
"@rushstack/eslint-patch": "^1.8.0",
|
||||||
"@savvywombat/tailwindcss-grid-areas": "^4.0.0",
|
"@savvywombat/tailwindcss-grid-areas": "^4.0.0",
|
||||||
"@tailwindcss/forms": "^0.5.7",
|
"@tailwindcss/forms": "^0.5.7",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import LearningPathCircle from "@/pages/learningPath/learningPathPage/LearningPathCircle.vue";
|
import LearningPathCircle from "@/pages/learningPath/learningPathPage/LearningPathCircle.vue";
|
||||||
import { calculateCircleSectorData } from "@/pages/learningPath/learningPathPage/utils";
|
import {
|
||||||
|
calculateCircleSectorData,
|
||||||
|
filterCircles,
|
||||||
|
useCourseFilter,
|
||||||
|
} from "@/pages/learningPath/learningPathPage/utils";
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
import { useCourseCircleProgress, useCourseDataWithCompletion } from "@/composables";
|
import { useCourseCircleProgress, useCourseDataWithCompletion } from "@/composables";
|
||||||
import LoadingSpinner from "@/components/ui/LoadingSpinner.vue";
|
import LoadingSpinner from "@/components/ui/LoadingSpinner.vue";
|
||||||
|
|
@ -48,9 +52,13 @@ const wrapperClasses = computed(() => {
|
||||||
return classes;
|
return classes;
|
||||||
});
|
});
|
||||||
|
|
||||||
const { inProgressCirclesCount, circlesCount } = useCourseCircleProgress(
|
const { filter } = useCourseFilter(props.courseSlug, props.courseSessionId);
|
||||||
lpQueryResult.circles
|
|
||||||
);
|
const filteredCircles = computed(() => {
|
||||||
|
return filterCircles(filter.value, circles.value);
|
||||||
|
});
|
||||||
|
const { inProgressCirclesCount, circlesCount } =
|
||||||
|
useCourseCircleProgress(filteredCircles);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -66,7 +74,7 @@ const { inProgressCirclesCount, circlesCount } = useCourseCircleProgress(
|
||||||
</h4>
|
</h4>
|
||||||
<div :class="wrapperClasses">
|
<div :class="wrapperClasses">
|
||||||
<LearningPathCircle
|
<LearningPathCircle
|
||||||
v-for="circle in circles"
|
v-for="circle in filteredCircles"
|
||||||
:key="circle.id"
|
:key="circle.id"
|
||||||
:sectors="calculateCircleSectorData(circle)"
|
:sectors="calculateCircleSectorData(circle)"
|
||||||
></LearningPathCircle>
|
></LearningPathCircle>
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,27 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import ItNavigationProgress from "@/components/ui/ItNavigationProgress.vue";
|
import ItNavigationProgress from "@/components/ui/ItNavigationProgress.vue";
|
||||||
|
import { computed } from "vue";
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
|
import { isString, startsWith } from "lodash";
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
step: number;
|
step: number;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const steps = computed(() => {
|
||||||
|
const courseType = route.params.courseType;
|
||||||
|
if (isString(courseType) && startsWith(courseType, "vv-")) {
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
return 3;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex h-screen flex-col">
|
<div class="flex h-screen flex-col">
|
||||||
<div class="flex-grow scroll-smooth p-16 lg:overflow-auto">
|
<div class="flex-grow scroll-smooth p-16 lg:overflow-auto">
|
||||||
<ItNavigationProgress :steps="3" :current-step="props.step" />
|
<ItNavigationProgress :steps="steps" :current-step="props.step" />
|
||||||
<slot name="content"></slot>
|
<slot name="content"></slot>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,17 +5,14 @@ import { computed } from "vue"; // https://stackoverflow.com/questions/64775876/
|
||||||
|
|
||||||
// https://stackoverflow.com/questions/64775876/vue-3-pass-reactive-object-to-component-with-two-way-binding
|
// https://stackoverflow.com/questions/64775876/vue-3-pass-reactive-object-to-component-with-two-way-binding
|
||||||
interface Props {
|
interface Props {
|
||||||
modelValue?: {
|
modelValue?: DropdownSelectable;
|
||||||
id: string | number;
|
|
||||||
name: string;
|
|
||||||
};
|
|
||||||
items?: DropdownSelectable[];
|
items?: DropdownSelectable[];
|
||||||
borderless?: boolean;
|
borderless?: boolean;
|
||||||
placeholderText?: string | null;
|
placeholderText?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: "update:modelValue", data: object): void;
|
(e: "update:modelValue", data: DropdownSelectable): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
|
|
||||||
|
|
@ -3,3 +3,5 @@ export const itCheckboxDefaultIconCheckedTailwindClass =
|
||||||
|
|
||||||
export const itCheckboxDefaultIconUncheckedTailwindClass =
|
export const itCheckboxDefaultIconUncheckedTailwindClass =
|
||||||
"bg-[url(/static/icons/icon-checkbox-unchecked.svg)] hover:bg-[url(/static/icons/icon-checkbox-unchecked-hover.svg)]";
|
"bg-[url(/static/icons/icon-checkbox-unchecked.svg)] hover:bg-[url(/static/icons/icon-checkbox-unchecked-hover.svg)]";
|
||||||
|
|
||||||
|
export const COURSE_PROFILE_ALL_FILTER = "all";
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -266,6 +266,9 @@ type CourseObjectType {
|
||||||
configuration: CourseConfigurationObjectType!
|
configuration: CourseConfigurationObjectType!
|
||||||
learning_path: LearningPathObjectType!
|
learning_path: LearningPathObjectType!
|
||||||
action_competences: [ActionCompetenceObjectType!]!
|
action_competences: [ActionCompetenceObjectType!]!
|
||||||
|
profiles: [String]
|
||||||
|
course_session_users(id: String): [CourseSessionUserType]!
|
||||||
|
chosen_profile(user: String!): String
|
||||||
}
|
}
|
||||||
|
|
||||||
type ActionCompetenceObjectType implements CoursePageInterface {
|
type ActionCompetenceObjectType implements CoursePageInterface {
|
||||||
|
|
@ -331,45 +334,38 @@ type CircleLightObjectType {
|
||||||
slug: String!
|
slug: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
type TopicObjectType implements CoursePageInterface {
|
type CourseSessionUserType {
|
||||||
is_visible: Boolean!
|
id: UUID!
|
||||||
id: ID!
|
chosen_profile: String!
|
||||||
title: String!
|
course_session: CourseSessionObjectType!
|
||||||
slug: String!
|
|
||||||
content_type: String!
|
|
||||||
live: Boolean!
|
|
||||||
translation_key: String!
|
|
||||||
frontend_url: String!
|
|
||||||
course: CourseObjectType
|
|
||||||
circles: [CircleObjectType!]!
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type CircleObjectType implements CoursePageInterface {
|
"""
|
||||||
description: String!
|
Leverages the internal Python implementation of UUID (uuid.UUID) to provide native UUID objects
|
||||||
goals: String!
|
in fields, resolvers and input.
|
||||||
|
"""
|
||||||
|
scalar UUID
|
||||||
|
|
||||||
|
type CourseSessionObjectType {
|
||||||
id: ID!
|
id: ID!
|
||||||
|
created_at: DateTime!
|
||||||
|
updated_at: DateTime!
|
||||||
|
course: CourseObjectType!
|
||||||
title: String!
|
title: String!
|
||||||
slug: String!
|
start_date: Date
|
||||||
content_type: String!
|
end_date: Date
|
||||||
live: Boolean!
|
attendance_courses: [CourseSessionAttendanceCourseObjectType!]!
|
||||||
translation_key: String!
|
assignments: [CourseSessionAssignmentObjectType!]!
|
||||||
frontend_url: String!
|
edoniq_tests: [CourseSessionEdoniqTestObjectType!]!
|
||||||
course: CourseObjectType
|
users: [CourseSessionUserObjectsType!]!
|
||||||
learning_sequences: [LearningSequenceObjectType!]!
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type LearningSequenceObjectType implements CoursePageInterface {
|
"""
|
||||||
icon: String!
|
The `Date` scalar type represents a Date
|
||||||
id: ID!
|
value as specified by
|
||||||
title: String!
|
[iso8601](https://en.wikipedia.org/wiki/ISO_8601).
|
||||||
slug: String!
|
"""
|
||||||
content_type: String!
|
scalar Date
|
||||||
live: Boolean!
|
|
||||||
translation_key: String!
|
|
||||||
frontend_url: String!
|
|
||||||
course: CourseObjectType
|
|
||||||
learning_units: [LearningUnitObjectType!]!
|
|
||||||
}
|
|
||||||
|
|
||||||
type CourseSessionAttendanceCourseObjectType {
|
type CourseSessionAttendanceCourseObjectType {
|
||||||
id: ID!
|
id: ID!
|
||||||
|
|
@ -431,26 +427,19 @@ type DueDateObjectType {
|
||||||
course_session: CourseSessionObjectType!
|
course_session: CourseSessionObjectType!
|
||||||
}
|
}
|
||||||
|
|
||||||
type CourseSessionObjectType {
|
type AttendanceUserObjectType {
|
||||||
id: ID!
|
user_id: UUID!
|
||||||
created_at: DateTime!
|
status: AttendanceUserStatus!
|
||||||
updated_at: DateTime!
|
first_name: String
|
||||||
course: CourseObjectType!
|
last_name: String
|
||||||
title: String!
|
email: String
|
||||||
start_date: Date
|
|
||||||
end_date: Date
|
|
||||||
attendance_courses: [CourseSessionAttendanceCourseObjectType!]!
|
|
||||||
assignments: [CourseSessionAssignmentObjectType!]!
|
|
||||||
edoniq_tests: [CourseSessionEdoniqTestObjectType!]!
|
|
||||||
users: [CourseSessionUserObjectsType!]!
|
|
||||||
}
|
}
|
||||||
|
|
||||||
"""
|
"""An enumeration."""
|
||||||
The `Date` scalar type represents a Date
|
enum AttendanceUserStatus {
|
||||||
value as specified by
|
PRESENT
|
||||||
[iso8601](https://en.wikipedia.org/wiki/ISO_8601).
|
ABSENT
|
||||||
"""
|
}
|
||||||
scalar Date
|
|
||||||
|
|
||||||
type CourseSessionAssignmentObjectType {
|
type CourseSessionAssignmentObjectType {
|
||||||
id: ID!
|
id: ID!
|
||||||
|
|
@ -578,12 +567,6 @@ type AssignmentCompletionObjectType {
|
||||||
evaluation_max_points: Float
|
evaluation_max_points: Float
|
||||||
}
|
}
|
||||||
|
|
||||||
"""
|
|
||||||
Leverages the internal Python implementation of UUID (uuid.UUID) to provide native UUID objects
|
|
||||||
in fields, resolvers and input.
|
|
||||||
"""
|
|
||||||
scalar UUID
|
|
||||||
|
|
||||||
type UserObjectType {
|
type UserObjectType {
|
||||||
"""
|
"""
|
||||||
Erforderlich. 150 Zeichen oder weniger. Nur Buchstaben, Ziffern und @/./+/-/_.
|
Erforderlich. 150 Zeichen oder weniger. Nur Buchstaben, Ziffern und @/./+/-/_.
|
||||||
|
|
@ -722,18 +705,46 @@ type CourseSessionUserExpertCircleType {
|
||||||
slug: String!
|
slug: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
type AttendanceUserObjectType {
|
type TopicObjectType implements CoursePageInterface {
|
||||||
user_id: UUID!
|
is_visible: Boolean!
|
||||||
status: AttendanceUserStatus!
|
id: ID!
|
||||||
first_name: String
|
title: String!
|
||||||
last_name: String
|
slug: String!
|
||||||
email: String
|
content_type: String!
|
||||||
|
live: Boolean!
|
||||||
|
translation_key: String!
|
||||||
|
frontend_url: String!
|
||||||
|
course: CourseObjectType
|
||||||
|
circles: [CircleObjectType!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
"""An enumeration."""
|
type CircleObjectType implements CoursePageInterface {
|
||||||
enum AttendanceUserStatus {
|
description: String!
|
||||||
PRESENT
|
goals: String!
|
||||||
ABSENT
|
is_base_circle: Boolean!
|
||||||
|
id: ID!
|
||||||
|
title: String!
|
||||||
|
slug: String!
|
||||||
|
content_type: String!
|
||||||
|
live: Boolean!
|
||||||
|
translation_key: String!
|
||||||
|
frontend_url: String!
|
||||||
|
course: CourseObjectType
|
||||||
|
learning_sequences: [LearningSequenceObjectType!]!
|
||||||
|
profiles: [String]!
|
||||||
|
}
|
||||||
|
|
||||||
|
type LearningSequenceObjectType implements CoursePageInterface {
|
||||||
|
icon: String!
|
||||||
|
id: ID!
|
||||||
|
title: String!
|
||||||
|
slug: String!
|
||||||
|
content_type: String!
|
||||||
|
live: Boolean!
|
||||||
|
translation_key: String!
|
||||||
|
frontend_url: String!
|
||||||
|
course: CourseObjectType
|
||||||
|
learning_units: [LearningUnitObjectType!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
type LearningContentMediaLibraryObjectType implements CoursePageInterface & LearningContentInterface {
|
type LearningContentMediaLibraryObjectType implements CoursePageInterface & LearningContentInterface {
|
||||||
|
|
@ -896,6 +907,7 @@ type CompetenceCertificateListObjectType implements CoursePageInterface {
|
||||||
type Mutation {
|
type Mutation {
|
||||||
send_feedback(course_session_id: ID!, data: GenericScalar, learning_content_page_id: ID!, learning_content_type: String!, submitted: Boolean = false): SendFeedbackMutation
|
send_feedback(course_session_id: ID!, data: GenericScalar, learning_content_page_id: ID!, learning_content_type: String!, submitted: Boolean = false): SendFeedbackMutation
|
||||||
update_course_session_attendance_course_users(attendance_user_list: [AttendanceUserInputType]!, id: ID!): AttendanceCourseUserMutation
|
update_course_session_attendance_course_users(attendance_user_list: [AttendanceUserInputType]!, id: ID!): AttendanceCourseUserMutation
|
||||||
|
update_course_session_profile(input: CourseSessionProfileMutationInput!): CourseSessionProfileMutationPayload
|
||||||
upsert_assignment_completion(assignment_id: ID!, assignment_user_id: UUID, completion_data_string: String, completion_status: AssignmentCompletionStatus, course_session_id: ID!, evaluation_passed: Boolean, evaluation_points: Float, evaluation_user_id: ID, initialize_completion: Boolean, learning_content_page_id: ID): AssignmentCompletionMutation
|
upsert_assignment_completion(assignment_id: ID!, assignment_user_id: UUID, completion_data_string: String, completion_status: AssignmentCompletionStatus, course_session_id: ID!, evaluation_passed: Boolean, evaluation_points: Float, evaluation_user_id: ID, initialize_completion: Boolean, learning_content_page_id: ID): AssignmentCompletionMutation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -926,6 +938,27 @@ input AttendanceUserInputType {
|
||||||
status: AttendanceUserStatus!
|
status: AttendanceUserStatus!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CourseSessionProfileMutationPayload {
|
||||||
|
result: UpdateCourseProfileResult
|
||||||
|
clientMutationId: String
|
||||||
|
}
|
||||||
|
|
||||||
|
union UpdateCourseProfileResult = UpdateCourseProfileError | UpdateCourseProfileSuccess
|
||||||
|
|
||||||
|
type UpdateCourseProfileError {
|
||||||
|
message: String
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateCourseProfileSuccess {
|
||||||
|
user: CourseSessionUserType!
|
||||||
|
}
|
||||||
|
|
||||||
|
input CourseSessionProfileMutationInput {
|
||||||
|
course_profile: String!
|
||||||
|
course_slug: String!
|
||||||
|
clientMutationId: String
|
||||||
|
}
|
||||||
|
|
||||||
type AssignmentCompletionMutation {
|
type AssignmentCompletionMutation {
|
||||||
assignment_completion: AssignmentCompletionObjectType
|
assignment_completion: AssignmentCompletionObjectType
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,8 +34,11 @@ export const CourseSessionAssignmentObjectType = "CourseSessionAssignmentObjectT
|
||||||
export const CourseSessionAttendanceCourseObjectType = "CourseSessionAttendanceCourseObjectType";
|
export const CourseSessionAttendanceCourseObjectType = "CourseSessionAttendanceCourseObjectType";
|
||||||
export const CourseSessionEdoniqTestObjectType = "CourseSessionEdoniqTestObjectType";
|
export const CourseSessionEdoniqTestObjectType = "CourseSessionEdoniqTestObjectType";
|
||||||
export const CourseSessionObjectType = "CourseSessionObjectType";
|
export const CourseSessionObjectType = "CourseSessionObjectType";
|
||||||
|
export const CourseSessionProfileMutationInput = "CourseSessionProfileMutationInput";
|
||||||
|
export const CourseSessionProfileMutationPayload = "CourseSessionProfileMutationPayload";
|
||||||
export const CourseSessionUserExpertCircleType = "CourseSessionUserExpertCircleType";
|
export const CourseSessionUserExpertCircleType = "CourseSessionUserExpertCircleType";
|
||||||
export const CourseSessionUserObjectsType = "CourseSessionUserObjectsType";
|
export const CourseSessionUserObjectsType = "CourseSessionUserObjectsType";
|
||||||
|
export const CourseSessionUserType = "CourseSessionUserType";
|
||||||
export const CourseStatisticsType = "CourseStatisticsType";
|
export const CourseStatisticsType = "CourseStatisticsType";
|
||||||
export const DashboardConfigType = "DashboardConfigType";
|
export const DashboardConfigType = "DashboardConfigType";
|
||||||
export const DashboardType = "DashboardType";
|
export const DashboardType = "DashboardType";
|
||||||
|
|
@ -84,4 +87,7 @@ export const StatisticsCourseSessionsSelectionMetricType = "StatisticsCourseSess
|
||||||
export const String = "String";
|
export const String = "String";
|
||||||
export const TopicObjectType = "TopicObjectType";
|
export const TopicObjectType = "TopicObjectType";
|
||||||
export const UUID = "UUID";
|
export const UUID = "UUID";
|
||||||
|
export const UpdateCourseProfileError = "UpdateCourseProfileError";
|
||||||
|
export const UpdateCourseProfileResult = "UpdateCourseProfileResult";
|
||||||
|
export const UpdateCourseProfileSuccess = "UpdateCourseProfileSuccess";
|
||||||
export const UserObjectType = "UserObjectType";
|
export const UserObjectType = "UserObjectType";
|
||||||
|
|
|
||||||
|
|
@ -58,3 +58,23 @@ export const UPSERT_ASSIGNMENT_COMPLETION_MUTATION = graphql(`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
export const UPDATE_COURSE_PROFILE_MUTATION = graphql(`
|
||||||
|
mutation UpdateCourseSessionProfile($input: CourseSessionProfileMutationInput!) {
|
||||||
|
update_course_session_profile(input: $input) {
|
||||||
|
clientMutationId
|
||||||
|
result {
|
||||||
|
__typename
|
||||||
|
... on UpdateCourseProfileSuccess {
|
||||||
|
user {
|
||||||
|
id
|
||||||
|
chosen_profile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
... on UpdateCourseProfileError {
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
|
||||||
|
|
@ -264,18 +264,28 @@ export const COURSE_SESSION_DETAIL_QUERY = graphql(`
|
||||||
`);
|
`);
|
||||||
|
|
||||||
export const COURSE_QUERY = graphql(`
|
export const COURSE_QUERY = graphql(`
|
||||||
query courseQuery($slug: String!) {
|
query courseQuery($slug: String!, $user: String) {
|
||||||
course(slug: $slug) {
|
course(slug: $slug) {
|
||||||
id
|
id
|
||||||
title
|
title
|
||||||
slug
|
slug
|
||||||
category_name
|
category_name
|
||||||
|
profiles
|
||||||
|
course_session_users(id: $user) {
|
||||||
|
id
|
||||||
|
__typename
|
||||||
|
chosen_profile
|
||||||
|
course_session {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
configuration {
|
configuration {
|
||||||
id
|
id
|
||||||
enable_circle_documents
|
enable_circle_documents
|
||||||
enable_learning_mentor
|
enable_learning_mentor
|
||||||
enable_competence_certificates
|
enable_competence_certificates
|
||||||
is_uk
|
is_uk
|
||||||
|
is_vv
|
||||||
}
|
}
|
||||||
action_competences {
|
action_competences {
|
||||||
competence_id
|
competence_id
|
||||||
|
|
@ -298,6 +308,8 @@ export const COURSE_QUERY = graphql(`
|
||||||
circles {
|
circles {
|
||||||
description
|
description
|
||||||
goals
|
goals
|
||||||
|
profiles
|
||||||
|
is_base_circle
|
||||||
...CoursePageFields
|
...CoursePageFields
|
||||||
learning_sequences {
|
learning_sequences {
|
||||||
icon
|
icon
|
||||||
|
|
|
||||||
|
|
@ -403,6 +403,13 @@ function log(data: any) {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<h2 class="mb-8 mt-8">Tags</h2>
|
||||||
|
|
||||||
|
<div class="mb-16 flex flex-col flex-wrap content-center gap-4 lg:flex-row">
|
||||||
|
<button class="tag-active">Active</button>
|
||||||
|
<button class="tag-inactive">Inactive</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h2 class="mb-8 mt-8">Dropdown (Work-in-progress)</h2>
|
<h2 class="mb-8 mt-8">Dropdown (Work-in-progress)</h2>
|
||||||
|
|
||||||
<ItDropdownSelect
|
<ItDropdownSelect
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { COURSE_PROFILE_ALL_FILTER } from "@/constants";
|
||||||
|
import LearningPathCircleListTile from "@/pages/learningPath/learningPathPage/LearningPathCircleListTile.vue";
|
||||||
|
import type { LearningContentWithCompletion, TopicType } from "@/types";
|
||||||
|
import { computed } from "vue";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
topic: TopicType;
|
||||||
|
nextLearningContent?: LearningContentWithCompletion;
|
||||||
|
filter?: string;
|
||||||
|
}
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const filteredCircles = computed(() => {
|
||||||
|
if (
|
||||||
|
props.filter === undefined ||
|
||||||
|
props.filter === "" ||
|
||||||
|
props.filter === COURSE_PROFILE_ALL_FILTER
|
||||||
|
) {
|
||||||
|
return props.topic.circles;
|
||||||
|
}
|
||||||
|
return props.topic.circles.filter(
|
||||||
|
(circle) =>
|
||||||
|
circle.profiles.indexOf(props.filter as string) > -1 || circle.is_base_circle
|
||||||
|
);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="pb-2 font-bold text-gray-700">
|
||||||
|
{{ topic.title }}
|
||||||
|
</div>
|
||||||
|
<LearningPathCircleListTile
|
||||||
|
v-for="circle in filteredCircles"
|
||||||
|
:key="circle.id"
|
||||||
|
:circle="circle"
|
||||||
|
:next-learning-content="nextLearningContent"
|
||||||
|
></LearningPathCircleListTile>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -1,26 +1,23 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import LearningPathCircleListTile from "@/pages/learningPath/learningPathPage/LearningPathCircleListTile.vue";
|
|
||||||
import { computed } from "vue";
|
|
||||||
import type { LearningContentWithCompletion, LearningPathType } from "@/types";
|
import type { LearningContentWithCompletion, LearningPathType } from "@/types";
|
||||||
|
import { computed } from "vue";
|
||||||
|
import LearningPathListTopic from "./LearningPathListTopic.vue";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
learningPath: LearningPathType | undefined;
|
learningPath: LearningPathType | undefined;
|
||||||
nextLearningContent: LearningContentWithCompletion | undefined;
|
nextLearningContent: LearningContentWithCompletion | undefined;
|
||||||
|
filter?: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const topics = computed(() => props.learningPath?.topics ?? []);
|
const topics = computed(() => props.learningPath?.topics ?? []);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-for="topic in topics" :key="topic.title">
|
<LearningPathListTopic
|
||||||
<div class="pb-2 font-bold text-gray-700">
|
v-for="topic in topics"
|
||||||
{{ topic.title }}
|
:key="topic.title"
|
||||||
</div>
|
:topic="topic"
|
||||||
<LearningPathCircleListTile
|
:next-learning-content="nextLearningContent"
|
||||||
v-for="circle in topic.circles"
|
:filter="filter"
|
||||||
:key="circle.id"
|
/>
|
||||||
:circle="circle"
|
|
||||||
:next-learning-content="props.nextLearningContent"
|
|
||||||
></LearningPathCircleListTile>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import LearningPathListView from "@/pages/learningPath/learningPathPage/Learning
|
||||||
import LearningPathPathView from "@/pages/learningPath/learningPathPage/LearningPathPathView.vue";
|
import LearningPathPathView from "@/pages/learningPath/learningPathPage/LearningPathPathView.vue";
|
||||||
import CircleProgress from "@/pages/learningPath/learningPathPage/LearningPathProgress.vue";
|
import CircleProgress from "@/pages/learningPath/learningPathPage/LearningPathProgress.vue";
|
||||||
import LearningPathTopics from "@/pages/learningPath/learningPathPage/LearningPathTopics.vue";
|
import LearningPathTopics from "@/pages/learningPath/learningPathPage/LearningPathTopics.vue";
|
||||||
|
import LearningPathProfileFilter from "@/pages/learningPath/learningPathPage/LearningPathProfileFilter.vue";
|
||||||
import type { ViewType } from "@/pages/learningPath/learningPathPage/LearningPathViewSwitch.vue";
|
import type { ViewType } from "@/pages/learningPath/learningPathPage/LearningPathViewSwitch.vue";
|
||||||
import LearningPathViewSwitch from "@/pages/learningPath/learningPathPage/LearningPathViewSwitch.vue";
|
import LearningPathViewSwitch from "@/pages/learningPath/learningPathPage/LearningPathViewSwitch.vue";
|
||||||
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core";
|
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core";
|
||||||
|
|
@ -13,6 +14,9 @@ import {
|
||||||
useCurrentCourseSession,
|
useCurrentCourseSession,
|
||||||
} from "@/composables";
|
} from "@/composables";
|
||||||
import CourseSessionDueDatesList from "@/components/dueDates/CourseSessionDueDatesList.vue";
|
import CourseSessionDueDatesList from "@/components/dueDates/CourseSessionDueDatesList.vue";
|
||||||
|
import { useMutation } from "@urql/vue";
|
||||||
|
import { UPDATE_COURSE_PROFILE_MUTATION } from "@/graphql/mutations";
|
||||||
|
import { filterCircles, useCourseFilter } from "./utils";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
courseSlug: string;
|
courseSlug: string;
|
||||||
|
|
@ -33,9 +37,28 @@ const course = computed(() => lpQueryResult.course.value);
|
||||||
|
|
||||||
const courseSession = useCurrentCourseSession();
|
const courseSession = useCurrentCourseSession();
|
||||||
|
|
||||||
const { inProgressCirclesCount, circlesCount } = useCourseCircleProgress(
|
const { filter } = useCourseFilter(props.courseSlug);
|
||||||
lpQueryResult.circles
|
|
||||||
);
|
const filteredCircles = computed(() => {
|
||||||
|
if (lpQueryResult.circles.value === undefined) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return filterCircles(filter.value, lpQueryResult.circles.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const { inProgressCirclesCount, circlesCount } =
|
||||||
|
useCourseCircleProgress(filteredCircles);
|
||||||
|
|
||||||
|
const updateCourseProfileMutation = useMutation(UPDATE_COURSE_PROFILE_MUTATION);
|
||||||
|
|
||||||
|
const updateCourseProfile = (profile: string) => {
|
||||||
|
updateCourseProfileMutation.executeMutation({
|
||||||
|
input: {
|
||||||
|
course_profile: profile,
|
||||||
|
course_slug: props.courseSlug,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const changeViewType = (viewType: ViewType) => {
|
const changeViewType = (viewType: ViewType) => {
|
||||||
selectedView.value = viewType;
|
selectedView.value = viewType;
|
||||||
|
|
@ -46,7 +69,9 @@ const changeViewType = (viewType: ViewType) => {
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<!-- Top -->
|
<!-- Top -->
|
||||||
<div class="flex flex-row justify-between space-x-8 bg-gray-200 p-6 sm:p-12">
|
<div
|
||||||
|
class="flex flex-col justify-between gap-8 bg-gray-200 p-6 sm:p-12 xl:flex-row"
|
||||||
|
>
|
||||||
<!-- Left -->
|
<!-- Left -->
|
||||||
<div class="flex flex-col justify-between lg:w-1/2">
|
<div class="flex flex-col justify-between lg:w-1/2">
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -64,15 +89,21 @@ const changeViewType = (viewType: ViewType) => {
|
||||||
></CircleProgress>
|
></CircleProgress>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Right -->
|
<!-- todo: find out when to display CourseSessionDueDatesList -->
|
||||||
<div v-if="!useMobileLayout" class="flex-grow">
|
<div v-if="!useMobileLayout && false" class="flex-grow">
|
||||||
<CourseSessionDueDatesList
|
<CourseSessionDueDatesList
|
||||||
:course-session-id="courseSession.id"
|
:course-session-id="courseSession.id"
|
||||||
:max-count="2"
|
:max-count="2"
|
||||||
></CourseSessionDueDatesList>
|
></CourseSessionDueDatesList>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Right -->
|
||||||
|
<LearningPathProfileFilter
|
||||||
|
v-if="course?.configuration.is_vv"
|
||||||
|
:profiles="course?.profiles"
|
||||||
|
:selected="filter"
|
||||||
|
@select="updateCourseProfile"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Bottom -->
|
<!-- Bottom -->
|
||||||
<div class="bg-white">
|
<div class="bg-white">
|
||||||
<div v-if="lpQueryResult.learningPath">
|
<div v-if="lpQueryResult.learningPath">
|
||||||
|
|
@ -101,6 +132,7 @@ const changeViewType = (viewType: ViewType) => {
|
||||||
<LearningPathPathView
|
<LearningPathPathView
|
||||||
:learning-path="learningPath"
|
:learning-path="learningPath"
|
||||||
:use-mobile-layout="useMobileLayout"
|
:use-mobile-layout="useMobileLayout"
|
||||||
|
:filter="filter"
|
||||||
:next-learning-content="lpQueryResult.nextLearningContent.value"
|
:next-learning-content="lpQueryResult.nextLearningContent.value"
|
||||||
></LearningPathPathView>
|
></LearningPathPathView>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -114,6 +146,7 @@ const changeViewType = (viewType: ViewType) => {
|
||||||
<LearningPathListView
|
<LearningPathListView
|
||||||
:learning-path="learningPath"
|
:learning-path="learningPath"
|
||||||
:next-learning-content="lpQueryResult.nextLearningContent.value"
|
:next-learning-content="lpQueryResult.nextLearningContent.value"
|
||||||
|
:filter="filter"
|
||||||
></LearningPathListView>
|
></LearningPathListView>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from "vue";
|
||||||
|
import type { LearningContentWithCompletion, TopicType } from "@/types";
|
||||||
|
import LearningPathCircleColumn from "./LearningPathCircleColumn.vue";
|
||||||
|
import { filterCircles } from "./utils";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
topic: TopicType;
|
||||||
|
topicIndex: number;
|
||||||
|
nextLearningContent?: LearningContentWithCompletion;
|
||||||
|
overrideCircleUrlBase?: string;
|
||||||
|
filter?: string;
|
||||||
|
isLastTopic: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const isFirstCircle = (circleIndex: number) =>
|
||||||
|
props.topicIndex === 0 && circleIndex === 0;
|
||||||
|
|
||||||
|
const isLastCircle = (circleIndex: number, numCircles: number) =>
|
||||||
|
props.isLastTopic && circleIndex === numCircles - 1;
|
||||||
|
|
||||||
|
const filteredCircles = computed(() => {
|
||||||
|
return filterCircles(props.filter, props.topic.circles);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="basis-40 border-l border-gray-500 first:ml-6 first:sm:ml-12">
|
||||||
|
<p
|
||||||
|
:id="`topic-${topic.slug}`"
|
||||||
|
class="inline-block h-12 self-start px-4 font-bold text-gray-800"
|
||||||
|
data-cy="lp-topic"
|
||||||
|
>
|
||||||
|
{{ topic.title }}
|
||||||
|
</p>
|
||||||
|
<div class="flex flex-row pt-6">
|
||||||
|
<LearningPathCircleColumn
|
||||||
|
v-for="(circle, circleIndex) in filteredCircles"
|
||||||
|
:key="circle.id"
|
||||||
|
:circle="circle"
|
||||||
|
:next-learning-content="nextLearningContent"
|
||||||
|
:is-first-circle="isFirstCircle(circleIndex)"
|
||||||
|
:is-last-circle="isLastCircle(circleIndex, filteredCircles.length)"
|
||||||
|
:override-circle-url="
|
||||||
|
overrideCircleUrlBase ? `${overrideCircleUrlBase}/${circle.slug}` : undefined
|
||||||
|
"
|
||||||
|
></LearningPathCircleColumn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import LearningPathCircleColumn from "@/pages/learningPath/learningPathPage/LearningPathCircleColumn.vue";
|
|
||||||
import LearningPathScrollButton from "@/pages/learningPath/learningPathPage/LearningPathScrollButton.vue";
|
import LearningPathScrollButton from "@/pages/learningPath/learningPathPage/LearningPathScrollButton.vue";
|
||||||
import { useScroll } from "@vueuse/core";
|
import { useScroll } from "@vueuse/core";
|
||||||
import { ref } from "vue";
|
import { computed, nextTick, ref, watch } from "vue";
|
||||||
import type { LearningContentWithCompletion, LearningPathType } from "@/types";
|
import type { LearningContentWithCompletion, LearningPathType } from "@/types";
|
||||||
import LoadingSpinner from "@/components/ui/LoadingSpinner.vue";
|
import LoadingSpinner from "@/components/ui/LoadingSpinner.vue";
|
||||||
|
import LearningPathPathTopic from "./LearningPathPathTopic.vue";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
learningPath: LearningPathType | undefined;
|
learningPath: LearningPathType | undefined;
|
||||||
|
|
@ -12,6 +12,7 @@ const props = defineProps<{
|
||||||
useMobileLayout: boolean;
|
useMobileLayout: boolean;
|
||||||
hideButtons?: boolean;
|
hideButtons?: boolean;
|
||||||
overrideCircleUrlBase?: string;
|
overrideCircleUrlBase?: string;
|
||||||
|
filter?: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const scrollIncrement = 600;
|
const scrollIncrement = 600;
|
||||||
|
|
@ -19,13 +20,6 @@ const scrollIncrement = 600;
|
||||||
const learnPathDiagram = ref<HTMLElement | null>(null);
|
const learnPathDiagram = ref<HTMLElement | null>(null);
|
||||||
const { x, arrivedState } = useScroll(learnPathDiagram, { behavior: "smooth" });
|
const { x, arrivedState } = useScroll(learnPathDiagram, { behavior: "smooth" });
|
||||||
|
|
||||||
const isFirstCircle = (topicIndex: number, circleIndex: number) =>
|
|
||||||
topicIndex === 0 && circleIndex === 0;
|
|
||||||
|
|
||||||
const isLastCircle = (topicIndex: number, circleIndex: number, numCircles: number) =>
|
|
||||||
topicIndex === (props.learningPath?.topics ?? []).length - 1 &&
|
|
||||||
circleIndex === numCircles - 1;
|
|
||||||
|
|
||||||
const scrollRight = () => scrollLearnPathDiagram(scrollIncrement);
|
const scrollRight = () => scrollLearnPathDiagram(scrollIncrement);
|
||||||
|
|
||||||
const scrollLeft = () => scrollLearnPathDiagram(-scrollIncrement);
|
const scrollLeft = () => scrollLearnPathDiagram(-scrollIncrement);
|
||||||
|
|
@ -33,6 +27,22 @@ const scrollLeft = () => scrollLearnPathDiagram(-scrollIncrement);
|
||||||
const scrollLearnPathDiagram = (offset: number) => {
|
const scrollLearnPathDiagram = (offset: number) => {
|
||||||
x.value += offset;
|
x.value += offset;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const topics = computed(() => props.learningPath?.topics ?? []);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.filter,
|
||||||
|
() => {
|
||||||
|
// we need to update the scroll state of the element, otherwise the arrows won't match the scroll state
|
||||||
|
// https://github.com/vueuse/vueuse/issues/2875
|
||||||
|
nextTick(() => {
|
||||||
|
if (learnPathDiagram.value) {
|
||||||
|
const scrollEvent = new Event("scroll");
|
||||||
|
learnPathDiagram.value.dispatchEvent(scrollEvent);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -50,37 +60,16 @@ const scrollLearnPathDiagram = (offset: number) => {
|
||||||
ref="learnPathDiagram"
|
ref="learnPathDiagram"
|
||||||
class="no-scrollbar flex h-96 snap-x flex-row overflow-auto py-5 sm:py-10"
|
class="no-scrollbar flex h-96 snap-x flex-row overflow-auto py-5 sm:py-10"
|
||||||
>
|
>
|
||||||
<div
|
<LearningPathPathTopic
|
||||||
v-for="(topic, topicIndex) in props.learningPath?.topics ?? []"
|
v-for="(topic, topicIndex) in props.learningPath?.topics ?? []"
|
||||||
:key="topic.title"
|
:key="topic.title"
|
||||||
class="border-l border-gray-500"
|
:topic-index="topicIndex"
|
||||||
:class="topicIndex == 0 ? 'ml-6 sm:ml-12' : ''"
|
:topic="topic"
|
||||||
>
|
:next-learning-content="nextLearningContent"
|
||||||
<p
|
:override-circle-url-base="overrideCircleUrlBase"
|
||||||
:id="`topic-${topic.slug}`"
|
:filter="filter"
|
||||||
class="inline-block h-12 self-start px-4 font-bold text-gray-800"
|
:is-last-topic="topicIndex === topics.length - 1"
|
||||||
data-cy="lp-topic"
|
/>
|
||||||
>
|
|
||||||
{{ topic.title }}
|
|
||||||
</p>
|
|
||||||
<div class="flex flex-row pt-6">
|
|
||||||
<LearningPathCircleColumn
|
|
||||||
v-for="(circle, circleIndex) in topic.circles"
|
|
||||||
:key="circle.id"
|
|
||||||
:circle="circle"
|
|
||||||
:next-learning-content="props.nextLearningContent"
|
|
||||||
:is-first-circle="isFirstCircle(topicIndex, circleIndex)"
|
|
||||||
:is-last-circle="
|
|
||||||
isLastCircle(topicIndex, circleIndex, topic.circles.length)
|
|
||||||
"
|
|
||||||
:override-circle-url="
|
|
||||||
props.overrideCircleUrlBase
|
|
||||||
? `${props.overrideCircleUrlBase}/${circle.slug}`
|
|
||||||
: undefined
|
|
||||||
"
|
|
||||||
></LearningPathCircleColumn>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<LearningPathScrollButton
|
<LearningPathScrollButton
|
||||||
v-show="!arrivedState.right"
|
v-show="!arrivedState.right"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
|
||||||
|
import { COURSE_PROFILE_ALL_FILTER } from "@/constants";
|
||||||
|
import type { DropdownSelectable } from "@/types";
|
||||||
|
import { useTranslation } from "i18next-vue";
|
||||||
|
import { computed } from "vue";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
profiles?: string[];
|
||||||
|
selected?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
const emit = defineEmits(["select"]);
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const items = computed(() => {
|
||||||
|
return props.profiles?.map((p) => ({ id: p, name: t(`profile.${p}`) })) || [];
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedItem = computed(() => {
|
||||||
|
if (props.selected) {
|
||||||
|
return { id: props.selected || "", name: t(`profile.${props.selected}`) };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: COURSE_PROFILE_ALL_FILTER,
|
||||||
|
name: t(`profile.${COURSE_PROFILE_ALL_FILTER}`),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateFilter = (e: DropdownSelectable) => {
|
||||||
|
emit("select", e.id);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex-grow">
|
||||||
|
<h5 class="mb-4">{{ $t("a.Zulassungsprofil") }}:</h5>
|
||||||
|
<div class="mb-4 flex gap-4">
|
||||||
|
<ItDropdownSelect
|
||||||
|
:items="items"
|
||||||
|
class="min-w-[18rem]"
|
||||||
|
:model-value="selectedItem"
|
||||||
|
@update:model-value="updateFilter"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
import { useCurrentCourseSession } from "@/composables";
|
||||||
|
import { COURSE_PROFILE_ALL_FILTER } from "@/constants";
|
||||||
|
import { COURSE_QUERY } from "@/graphql/queries";
|
||||||
import type {
|
import type {
|
||||||
CircleSectorData,
|
CircleSectorData,
|
||||||
CircleSectorProgress,
|
CircleSectorProgress,
|
||||||
|
|
@ -7,6 +10,8 @@ import {
|
||||||
someFinishedInLearningSequence,
|
someFinishedInLearningSequence,
|
||||||
} from "@/services/circle";
|
} from "@/services/circle";
|
||||||
import type { CircleType } from "@/types";
|
import type { CircleType } from "@/types";
|
||||||
|
import { useQuery } from "@urql/vue";
|
||||||
|
import { computed } from "vue";
|
||||||
|
|
||||||
export function calculateCircleSectorData(circle: CircleType): CircleSectorData[] {
|
export function calculateCircleSectorData(circle: CircleType): CircleSectorData[] {
|
||||||
return circle.learning_sequences.map((ls) => {
|
return circle.learning_sequences.map((ls) => {
|
||||||
|
|
@ -21,3 +26,42 @@ export function calculateCircleSectorData(circle: CircleType): CircleSectorData[
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useCourseFilter(courseSlug: string, courseSessionId?: string) {
|
||||||
|
const csId = computed(() => {
|
||||||
|
if (courseSessionId) {
|
||||||
|
return courseSessionId;
|
||||||
|
}
|
||||||
|
// assume we're on a page with a current course session
|
||||||
|
const courseSession = useCurrentCourseSession();
|
||||||
|
return courseSession.value.id;
|
||||||
|
});
|
||||||
|
const courseReactiveResult = useQuery({
|
||||||
|
query: COURSE_QUERY,
|
||||||
|
variables: { slug: courseSlug },
|
||||||
|
});
|
||||||
|
const courseReactive = computed(() => courseReactiveResult.data.value?.course);
|
||||||
|
const courseSessionUser = computed(() => {
|
||||||
|
return courseReactive.value?.course_session_users.find(
|
||||||
|
(e) => e?.course_session.id === csId.value
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const filter = computed(() => {
|
||||||
|
return courseSessionUser.value?.chosen_profile || "";
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
filter,
|
||||||
|
courseReactive,
|
||||||
|
courseSessionUser,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function filterCircles(filter: string | undefined, circles: CircleType[]) {
|
||||||
|
if (filter === undefined || filter === "" || filter === COURSE_PROFILE_ALL_FILTER) {
|
||||||
|
return circles;
|
||||||
|
}
|
||||||
|
return circles.filter(
|
||||||
|
(circle) => circle.profiles.indexOf(filter as string) > -1 || circle.is_base_circle
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import WizardPage from "@/components/onboarding/WizardPage.vue";
|
||||||
|
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
|
||||||
|
import { useEntities } from "@/services/entities";
|
||||||
|
import { useUserStore } from "@/stores/user";
|
||||||
|
import type { DropdownSelectable } from "@/types";
|
||||||
|
import { useTranslation } from "i18next-vue";
|
||||||
|
import { computed, ref, watch } from "vue";
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const user = useUserStore();
|
||||||
|
|
||||||
|
const { courseProfiles } = useEntities();
|
||||||
|
|
||||||
|
const selectedCourseProfile = ref({
|
||||||
|
id: 0,
|
||||||
|
name: t("a.Auswählen"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const validCourseProfile = computed(() => {
|
||||||
|
return selectedCourseProfile.value.id !== 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(selectedCourseProfile, async (courseProfile: DropdownSelectable) => {
|
||||||
|
const courseProfileWithCode = courseProfiles.value.find(
|
||||||
|
(cp) => cp.id === courseProfile.id
|
||||||
|
);
|
||||||
|
if (courseProfileWithCode) {
|
||||||
|
user.updateChosenCourseProfile(courseProfileWithCode);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const courseProfilesToDropdown = computed(() => {
|
||||||
|
return courseProfiles.value.map((profile) => ({
|
||||||
|
...profile,
|
||||||
|
name: t(`profile.${profile.code}`),
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<WizardPage :step="2">
|
||||||
|
<template #content>
|
||||||
|
<h2 class="my-10" data-cy="account-course-profile-title">
|
||||||
|
{{ $t("a.Zulassungsprofil auswählen") }}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p class="mb-6 max-w-md hyphens-none">
|
||||||
|
{{
|
||||||
|
$t(
|
||||||
|
"a.Wähle ein Zulassungsprofil, damit du deinen Lehrgang an der richtigen Stelle beginnen kannst. Du kannst ihn später jederzeit ändern."
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ItDropdownSelect
|
||||||
|
v-model="selectedCourseProfile"
|
||||||
|
:items="courseProfilesToDropdown"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<router-link v-slot="{ navigate }" :to="{ name: 'checkoutAddress' }" custom>
|
||||||
|
<button
|
||||||
|
:disabled="!validCourseProfile"
|
||||||
|
class="btn-blue flex items-center"
|
||||||
|
role="link"
|
||||||
|
data-cy="continue-button"
|
||||||
|
@click="navigate"
|
||||||
|
>
|
||||||
|
{{ $t("general.next") }}
|
||||||
|
<it-icon-arrow-right class="it-icon ml-2 h-6 w-6" />
|
||||||
|
</button>
|
||||||
|
</router-link>
|
||||||
|
</template>
|
||||||
|
</WizardPage>
|
||||||
|
</template>
|
||||||
|
|
@ -229,6 +229,7 @@ const executePayment = async () => {
|
||||||
redirect_url: fullHost,
|
redirect_url: fullHost,
|
||||||
address: addressData,
|
address: addressData,
|
||||||
product: props.courseType,
|
product: props.courseType,
|
||||||
|
chosen_profile: user.chosen_profile?.id || "",
|
||||||
with_cembra_byjuno_invoice: address.value.payment_method === "cembra_byjuno",
|
with_cembra_byjuno_invoice: address.value.payment_method === "cembra_byjuno",
|
||||||
device_fingerprint_session_key: getLocalSessionKey(),
|
device_fingerprint_session_key: getLocalSessionKey(),
|
||||||
}).then((res: any) => {
|
}).then((res: any) => {
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,14 @@ const { t } = useTranslation();
|
||||||
$t("Füge dein Profilbild hinzu und ergänze die fehlenden Angaben.")
|
$t("Füge dein Profilbild hinzu und ergänze die fehlenden Angaben.")
|
||||||
}}
|
}}
|
||||||
</li>
|
</li>
|
||||||
|
<li class="relative pl-8">
|
||||||
|
<span class="font-bold">{{ $t("a.Zulassungsprofil auswählen") }}:</span>
|
||||||
|
{{
|
||||||
|
$t(
|
||||||
|
"a.Wähle ein Zulassungsprofil, damit du deinen Lehrgang an der richtigen Stelle beginnen kannst."
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</li>
|
||||||
<li class="relative pl-8">
|
<li class="relative pl-8">
|
||||||
<span class="font-bold">{{ $t("a.Lehrgang kaufen") }}:</span>
|
<span class="font-bold">{{ $t("a.Lehrgang kaufen") }}:</span>
|
||||||
{{
|
{{
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { calculateCircleSectorData } from "@/pages/learningPath/learningPathPage/utils";
|
|
||||||
import { useCourseDataWithCompletion } from "@/composables";
|
import { useCourseDataWithCompletion } from "@/composables";
|
||||||
import UserProfileContent from "@/components/userProfile/UserProfileContent.vue";
|
import UserProfileContent from "@/components/userProfile/UserProfileContent.vue";
|
||||||
import LearningPathCircle from "@/pages/learningPath/learningPathPage/LearningPathCircle.vue";
|
|
||||||
import LearningSequence from "@/pages/learningPath/circlePage/LearningSequence.vue";
|
import LearningSequence from "@/pages/learningPath/circlePage/LearningSequence.vue";
|
||||||
import { ref, watch } from "vue";
|
import { computed, ref, watch } from "vue";
|
||||||
import type { CircleType } from "@/types";
|
import type { CircleType } from "@/types";
|
||||||
|
import { COURSE_QUERY } from "@/graphql/queries";
|
||||||
|
import { useQuery } from "@urql/vue";
|
||||||
|
import UserProfileTopicList from "./UserProfileTopicList.vue";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
userId: string;
|
userId: string;
|
||||||
|
|
@ -20,6 +21,23 @@ function selectCircle(circle: CircleType) {
|
||||||
selectedCircle.value = circle;
|
selectedCircle.value = circle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const courseReactiveResult = useQuery({
|
||||||
|
query: COURSE_QUERY,
|
||||||
|
variables: { slug: props.courseSlug, user: props.userId },
|
||||||
|
});
|
||||||
|
|
||||||
|
const courseReactive = computed(() => courseReactiveResult.data.value?.course);
|
||||||
|
const courseSessionUsers = computed(() => {
|
||||||
|
return courseReactive.value?.course_session_users;
|
||||||
|
});
|
||||||
|
|
||||||
|
const chosenProfile = computed(() => {
|
||||||
|
if (courseSessionUsers.value && courseSessionUsers.value.length > 0) {
|
||||||
|
return courseSessionUsers.value[0]?.chosen_profile;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
|
||||||
watch(lpQueryResult.learningPath, () => {
|
watch(lpQueryResult.learningPath, () => {
|
||||||
if (lpQueryResult.learningPath?.value?.topics?.length) {
|
if (lpQueryResult.learningPath?.value?.topics?.length) {
|
||||||
selectCircle(lpQueryResult.learningPath.value.topics[0].circles[0]);
|
selectCircle(lpQueryResult.learningPath.value.topics[0].circles[0]);
|
||||||
|
|
@ -30,28 +48,21 @@ watch(lpQueryResult.learningPath, () => {
|
||||||
<template>
|
<template>
|
||||||
<UserProfileContent>
|
<UserProfileContent>
|
||||||
<template #side>
|
<template #side>
|
||||||
<div
|
<div v-if="chosenProfile">
|
||||||
|
<h3 class="mb-4 text-base font-bold">
|
||||||
|
Zulassungsprofil:
|
||||||
|
<br />
|
||||||
|
{{ $t(`profile.${chosenProfile}`) }}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<UserProfileTopicList
|
||||||
v-for="topic in lpQueryResult.learningPath?.value?.topics ?? []"
|
v-for="topic in lpQueryResult.learningPath?.value?.topics ?? []"
|
||||||
:key="topic.id"
|
:key="topic.id"
|
||||||
class="mb-4"
|
:topic="topic"
|
||||||
>
|
:filter="chosenProfile"
|
||||||
<h4 class="mb-1 font-semibold text-gray-800">
|
:selected-circle="selectedCircle"
|
||||||
{{ topic.title }}
|
@select-circle="selectCircle($event)"
|
||||||
</h4>
|
/>
|
||||||
<button
|
|
||||||
v-for="circle in topic.circles"
|
|
||||||
:key="circle.id"
|
|
||||||
class="flex w-full items-center space-x-2 p-2 pr-4 hover:bg-gray-200 lg:pr-8"
|
|
||||||
:class="{ 'bg-gray-200': selectedCircle === circle }"
|
|
||||||
@click="selectCircle(circle)"
|
|
||||||
>
|
|
||||||
<LearningPathCircle
|
|
||||||
:sectors="calculateCircleSectorData(circle)"
|
|
||||||
class="h-10 w-10 snap-center rounded-full bg-white p-0.5"
|
|
||||||
></LearningPathCircle>
|
|
||||||
<span>{{ circle.title }}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
<template #main>
|
<template #main>
|
||||||
<ol v-if="selectedCircle" class="flex-auto bg-gray-200 px-6 py-4 lg:px-16">
|
<ol v-if="selectedCircle" class="flex-auto bg-gray-200 px-6 py-4 lg:px-16">
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { CircleType, TopicType } from "@/types";
|
||||||
|
import { calculateCircleSectorData } from "@/pages/learningPath/learningPathPage/utils";
|
||||||
|
import LearningPathCircle from "../learningPath/learningPathPage/LearningPathCircle.vue";
|
||||||
|
import { computed } from "vue";
|
||||||
|
import { COURSE_PROFILE_ALL_FILTER } from "@/constants";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
topic: TopicType;
|
||||||
|
selectedCircle?: CircleType;
|
||||||
|
filter?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
defineEmits(["select-circle"]);
|
||||||
|
|
||||||
|
const filteredCircles = computed(() => {
|
||||||
|
const circles = props.topic.circles;
|
||||||
|
const filter = props.filter;
|
||||||
|
if (filter === undefined || filter === "" || filter === COURSE_PROFILE_ALL_FILTER) {
|
||||||
|
return circles;
|
||||||
|
}
|
||||||
|
return circles.filter(
|
||||||
|
(circle) => circle.profiles.indexOf(filter as string) > -1 || circle.is_base_circle
|
||||||
|
);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="mb-4">
|
||||||
|
<h4 class="mb-1 font-semibold text-gray-800">{{ topic.title }} {{ filter }}</h4>
|
||||||
|
<button
|
||||||
|
v-for="circle in filteredCircles"
|
||||||
|
:key="circle.id"
|
||||||
|
class="flex w-full items-center space-x-2 p-2 pr-4 hover:bg-gray-200 lg:pr-8"
|
||||||
|
:class="{ 'bg-gray-200': selectedCircle === circle }"
|
||||||
|
@click="$emit('select-circle', circle)"
|
||||||
|
>
|
||||||
|
<LearningPathCircle
|
||||||
|
:sectors="calculateCircleSectorData(circle)"
|
||||||
|
class="h-10 w-10 snap-center rounded-full bg-white p-0.5"
|
||||||
|
></LearningPathCircle>
|
||||||
|
<span>{{ circle.title }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -61,7 +61,7 @@ describe("Onboarding", () => {
|
||||||
mockNext
|
mockNext
|
||||||
);
|
);
|
||||||
expect(mockNext).toHaveBeenCalledWith({
|
expect(mockNext).toHaveBeenCalledWith({
|
||||||
name: "checkoutAddress",
|
name: "accountCourseProfile",
|
||||||
params: { courseType: testCase },
|
params: { courseType: testCase },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -391,6 +391,12 @@ const router = createRouter({
|
||||||
component: () => import("@/pages/onboarding/uk/SetupComplete.vue"),
|
component: () => import("@/pages/onboarding/uk/SetupComplete.vue"),
|
||||||
name: "setupComplete",
|
name: "setupComplete",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "account/course-profile",
|
||||||
|
component: () => import("@/pages/onboarding/vv/AccountCourseProfile.vue"),
|
||||||
|
name: "accountCourseProfile",
|
||||||
|
props: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "checkout/address",
|
path: "checkout/address",
|
||||||
component: () => import("@/pages/onboarding/vv/CheckoutAddress.vue"),
|
component: () => import("@/pages/onboarding/vv/CheckoutAddress.vue"),
|
||||||
|
|
|
||||||
|
|
@ -13,14 +13,21 @@ export type Country = {
|
||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type CourseProfile = {
|
||||||
|
id: number;
|
||||||
|
code: string;
|
||||||
|
};
|
||||||
|
|
||||||
export function useEntities() {
|
export function useEntities() {
|
||||||
const countries: Ref<Country[]> = ref([]);
|
const countries: Ref<Country[]> = ref([]);
|
||||||
const organisations: Ref<Organisation[]> = ref([]);
|
const organisations: Ref<Organisation[]> = ref([]);
|
||||||
|
const courseProfiles: Ref<CourseProfile[]> = ref([]);
|
||||||
|
|
||||||
itGetCached("/api/core/entities/").then((res: any) => {
|
itGetCached("/api/core/entities/").then((res: any) => {
|
||||||
countries.value = res.countries;
|
countries.value = res.countries;
|
||||||
organisations.value = res.organisations;
|
organisations.value = res.organisations;
|
||||||
|
courseProfiles.value = res.courseProfiles;
|
||||||
});
|
});
|
||||||
|
|
||||||
return { organisations, countries };
|
return { organisations, countries, courseProfiles };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ export function profileNextRoute(courseType: string | string[]) {
|
||||||
}
|
}
|
||||||
// vv- -> vv-de, vv-fr or vv-it
|
// vv- -> vv-de, vv-fr or vv-it
|
||||||
if (isString(courseType) && startsWith(courseType, "vv-")) {
|
if (isString(courseType) && startsWith(courseType, "vv-")) {
|
||||||
return "checkoutAddress";
|
return "accountCourseProfile";
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { bustItGetCache, itGetCached, itPost } from "@/fetchHelpers";
|
import { bustItGetCache, itGetCached, itPost } from "@/fetchHelpers";
|
||||||
import { setI18nLanguage } from "@/i18nextWrapper";
|
import { setI18nLanguage } from "@/i18nextWrapper";
|
||||||
import type { Country } from "@/services/entities";
|
import type { Country, CourseProfile } from "@/services/entities";
|
||||||
import { directUpload } from "@/services/files";
|
import { directUpload } from "@/services/files";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
|
|
@ -44,6 +44,7 @@ export interface User {
|
||||||
organisation_postal_code: string;
|
organisation_postal_code: string;
|
||||||
organisation_city: string;
|
organisation_city: string;
|
||||||
organisation_country: Country | null;
|
organisation_country: Country | null;
|
||||||
|
chosen_profile?: CourseProfile;
|
||||||
}
|
}
|
||||||
|
|
||||||
let defaultLanguage: AvailableLanguages = "de";
|
let defaultLanguage: AvailableLanguages = "de";
|
||||||
|
|
@ -89,6 +90,7 @@ const initialUserState: User = {
|
||||||
organisation_postal_code: "",
|
organisation_postal_code: "",
|
||||||
organisation_city: "",
|
organisation_city: "",
|
||||||
organisation_country: null,
|
organisation_country: null,
|
||||||
|
chosen_profile: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
async function setLocale(language: AvailableLanguages) {
|
async function setLocale(language: AvailableLanguages) {
|
||||||
|
|
@ -176,5 +178,8 @@ export const useUserStore = defineStore({
|
||||||
await itPost("/api/core/me/", profileData, { method: "PUT" });
|
await itPost("/api/core/me/", profileData, { method: "PUT" });
|
||||||
Object.assign(this.$state, profileData);
|
Object.assign(this.$state, profileData);
|
||||||
},
|
},
|
||||||
|
updateChosenCourseProfile(courseProfile: CourseProfile) {
|
||||||
|
Object.assign(this.$state, { chosen_profile: courseProfile });
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -202,6 +202,7 @@ export interface Course {
|
||||||
title: string;
|
title: string;
|
||||||
category_name: string;
|
category_name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
|
profiles: string[];
|
||||||
configuration: CourseConfiguration;
|
configuration: CourseConfiguration;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@ textarea {
|
||||||
}
|
}
|
||||||
|
|
||||||
.link {
|
.link {
|
||||||
@apply underline underline-offset-2;
|
@apply cursor-pointer underline underline-offset-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.link-large {
|
.link-large {
|
||||||
|
|
@ -167,6 +167,14 @@ textarea {
|
||||||
top: 1rem;
|
top: 1rem;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tag-inactive {
|
||||||
|
@apply rounded-full border-2 border-blue-900 px-4 py-2 font-semibold text-blue-900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-active {
|
||||||
|
@apply rounded-full bg-blue-900 px-4 py-2 font-semibold text-white;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
|
|
@ -181,7 +189,9 @@ textarea {
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-scrollbar {
|
.no-scrollbar {
|
||||||
-ms-overflow-style: none; /* IE and Edge */
|
-ms-overflow-style: none;
|
||||||
scrollbar-width: none; /* Firefox */
|
/* IE and Edge */
|
||||||
|
scrollbar-width: none;
|
||||||
|
/* Firefox */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,22 @@
|
||||||
// ids for cypress test data
|
// ids for cypress test data
|
||||||
export const ADMIN_USER_ID = "872efd96-3bd7-4a1e-a239-2d72cad9f604"
|
export const ADMIN_USER_ID = "872efd96-3bd7-4a1e-a239-2d72cad9f604";
|
||||||
export const TEST_SUPERVISOR1_USER_ID = "a9a8b741-f115-4521-af2d-7dfef673b8c5"
|
export const TEST_SUPERVISOR1_USER_ID = "a9a8b741-f115-4521-af2d-7dfef673b8c5";
|
||||||
export const TEST_TRAINER1_USER_ID = "b9e71f59-c44f-4290-b93a-9b3151e9a2fc"
|
export const TEST_TRAINER1_USER_ID = "b9e71f59-c44f-4290-b93a-9b3151e9a2fc";
|
||||||
export const TEST_TRAINER2_USER_ID = "299941ae-1e4b-4f45-8180-876c3ad340b4"
|
export const TEST_TRAINER2_USER_ID = "299941ae-1e4b-4f45-8180-876c3ad340b4";
|
||||||
export const TEST_STUDENT1_USER_ID = "65c73ad0-6d53-43a9-a4a4-64143f27b03a"
|
export const TEST_STUDENT1_USER_ID = "65c73ad0-6d53-43a9-a4a4-64143f27b03a";
|
||||||
export const TEST_STUDENT2_USER_ID = "19c40d94-15cc-4198-aaad-ef707c4b0900"
|
export const TEST_STUDENT2_USER_ID = "19c40d94-15cc-4198-aaad-ef707c4b0900";
|
||||||
export const TEST_STUDENT3_USER_ID = "bcf94dba-53bc-474b-a22d-e4af39aa042b"
|
export const TEST_STUDENT3_USER_ID = "bcf94dba-53bc-474b-a22d-e4af39aa042b";
|
||||||
export const TEST_MENTOR1_USER_ID = "d1f5f5a9-5b0a-4e1a-9e1a-9e9b5b5e1b1b"
|
export const TEST_MENTOR1_USER_ID = "d1f5f5a9-5b0a-4e1a-9e1a-9e9b5b5e1b1b";
|
||||||
export const TEST_STUDENT1_VV_USER_ID = "5ff59857-8de5-415e-a387-4449f9a0337a"
|
export const TEST_STUDENT1_VV_USER_ID = "5ff59857-8de5-415e-a387-4449f9a0337a";
|
||||||
export const TEST_STUDENT2_VV_AND_VV_MENTOR_USER_ID = "7e8ebf0b-e6e2-4022-88f4-6e663ba0a9db"
|
export const TEST_STUDENT2_VV_AND_VV_MENTOR_USER_ID =
|
||||||
export const TEST_USER_EMPTY_ID = "daecbabe-4ab9-4edf-a71f-4119042ccb02"
|
"7e8ebf0b-e6e2-4022-88f4-6e663ba0a9db";
|
||||||
|
export const TEST_USER_EMPTY_ID = "daecbabe-4ab9-4edf-a71f-4119042ccb02";
|
||||||
|
|
||||||
export const TEST_COURSE_SESSION_BERN_ID = -1;
|
export const TEST_COURSE_SESSION_BERN_ID = -1;
|
||||||
export const TEST_COURSE_SESSION_ZURICH_ID = -2;
|
export const TEST_COURSE_SESSION_ZURICH_ID = -2;
|
||||||
|
export const TEST_COURSE_SESSION_VV_ID = 1;
|
||||||
|
|
||||||
|
export const COURSE_PROFILE_LEBEN_ID = -1;
|
||||||
|
export const COURSE_PROFILE_NICHTLEBEN_ID = -2;
|
||||||
|
export const COURSE_PROFILE_KRANKENZUSATZ_ID = -3;
|
||||||
|
export const COURSE_PROFILE_ALL_ID = -99;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,9 @@
|
||||||
import { TEST_USER_EMPTY_ID } from "../../consts";
|
import {
|
||||||
|
COURSE_PROFILE_ALL_ID,
|
||||||
|
COURSE_PROFILE_NICHTLEBEN_ID,
|
||||||
|
TEST_COURSE_SESSION_VV_ID,
|
||||||
|
TEST_USER_EMPTY_ID,
|
||||||
|
} from "../../consts";
|
||||||
import { login } from "../helpers";
|
import { login } from "../helpers";
|
||||||
|
|
||||||
describe("checkout.cy.js", () => {
|
describe("checkout.cy.js", () => {
|
||||||
|
|
@ -32,6 +37,15 @@ describe("checkout.cy.js", () => {
|
||||||
cy.get("#organisationDetailName").type("FdH GmbH");
|
cy.get("#organisationDetailName").type("FdH GmbH");
|
||||||
cy.get('[data-cy="continue-button"]').click();
|
cy.get('[data-cy="continue-button"]').click();
|
||||||
|
|
||||||
|
cy.get('[data-cy="account-course-profile-title"]').should(
|
||||||
|
"have.text",
|
||||||
|
"Zulassungsprofil auswählen",
|
||||||
|
);
|
||||||
|
|
||||||
|
cy.get('[data-cy="dropdown-select"]').click();
|
||||||
|
cy.get('[data-cy="dropdown-select-option-Nichtleben"]').click();
|
||||||
|
cy.get('[data-cy="continue-button"]').click();
|
||||||
|
|
||||||
cy.loadUser("id", TEST_USER_EMPTY_ID).then((u) => {
|
cy.loadUser("id", TEST_USER_EMPTY_ID).then((u) => {
|
||||||
expect(u.organisation_detail_name).to.equal("FdH GmbH");
|
expect(u.organisation_detail_name).to.equal("FdH GmbH");
|
||||||
// 2 -> andere Krankenversicherer
|
// 2 -> andere Krankenversicherer
|
||||||
|
|
@ -121,6 +135,12 @@ describe("checkout.cy.js", () => {
|
||||||
cy.loadCheckoutInformation("user_id", TEST_USER_EMPTY_ID).then((ci) => {
|
cy.loadCheckoutInformation("user_id", TEST_USER_EMPTY_ID).then((ci) => {
|
||||||
expect(ci.state).to.equal("paid");
|
expect(ci.state).to.equal("paid");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
cy.loadCourseSessionUser("user_id", TEST_USER_EMPTY_ID).then((csu) => {
|
||||||
|
expect(csu.role).to.equal("MEMBER");
|
||||||
|
expect(csu.course_session).to.equal(TEST_COURSE_SESSION_VV_ID);
|
||||||
|
expect(csu.chosen_profile).to.equal(COURSE_PROFILE_NICHTLEBEN_ID);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("can checkout and pay Versicherungsvermittlerin with Cembra invoice", () => {
|
it("can checkout and pay Versicherungsvermittlerin with Cembra invoice", () => {
|
||||||
|
|
@ -143,6 +163,15 @@ describe("checkout.cy.js", () => {
|
||||||
cy.get('[data-cy="dropdown-select-option-Baloise"]').click();
|
cy.get('[data-cy="dropdown-select-option-Baloise"]').click();
|
||||||
cy.get('[data-cy="continue-button"]').click();
|
cy.get('[data-cy="continue-button"]').click();
|
||||||
|
|
||||||
|
cy.get('[data-cy="account-course-profile-title"]').should(
|
||||||
|
"have.text",
|
||||||
|
"Zulassungsprofil auswählen",
|
||||||
|
);
|
||||||
|
|
||||||
|
cy.get('[data-cy="dropdown-select"]').click();
|
||||||
|
cy.get('[data-cy="dropdown-select-option-Allbranche"]').click();
|
||||||
|
cy.get('[data-cy="continue-button"]').click();
|
||||||
|
|
||||||
// Adressdaten ausfüllen
|
// Adressdaten ausfüllen
|
||||||
cy.get('[data-cy="account-checkout-title"]').should(
|
cy.get('[data-cy="account-checkout-title"]').should(
|
||||||
"contain",
|
"contain",
|
||||||
|
|
@ -236,5 +265,32 @@ describe("checkout.cy.js", () => {
|
||||||
// 7 -> Baloise
|
// 7 -> Baloise
|
||||||
expect(u.organisation).to.equal(7);
|
expect(u.organisation).to.equal(7);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// pay
|
||||||
|
cy.get('[data-cy="pay-button"]').click();
|
||||||
|
|
||||||
|
cy.get('[data-cy="checkout-success-title"]').should(
|
||||||
|
"contain",
|
||||||
|
"Gratuliere",
|
||||||
|
);
|
||||||
|
// wait for payment callback
|
||||||
|
cy.wait(3000);
|
||||||
|
cy.get('[data-cy="start-vv-button"]').click();
|
||||||
|
|
||||||
|
// back on dashboard page
|
||||||
|
cy.get('[data-cy="db-course-title"]').should(
|
||||||
|
"contain",
|
||||||
|
"Versicherungsvermittler",
|
||||||
|
);
|
||||||
|
|
||||||
|
cy.loadCheckoutInformation("user_id", TEST_USER_EMPTY_ID).then((ci) => {
|
||||||
|
expect(ci.state).to.equal("paid");
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.loadCourseSessionUser("user_id", TEST_USER_EMPTY_ID).then((csu) => {
|
||||||
|
expect(csu.role).to.equal("MEMBER");
|
||||||
|
expect(csu.course_session).to.equal(TEST_COURSE_SESSION_VV_ID);
|
||||||
|
expect(csu.chosen_profile).to.equal(COURSE_PROFILE_ALL_ID);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -189,6 +189,17 @@ Cypress.Commands.add("loadUser", (key, value) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
Cypress.Commands.add("loadCourseSessionUser", (key, value) => {
|
||||||
|
return loadObjectJson(
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
"vbv_lernwelt.course.models.CourseSessionUser",
|
||||||
|
"vbv_lernwelt.course.serializers.CypressCourseSessionUserSerializer",
|
||||||
|
true
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
Cypress.Commands.add("makeSelfEvaluation", (answers) => {
|
Cypress.Commands.add("makeSelfEvaluation", (answers) => {
|
||||||
for (let i = 0; i < answers.length; i++) {
|
for (let i = 0; i < answers.length; i++) {
|
||||||
const answer = answers[i]
|
const answer = answers[i]
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ echo 'prettier:check'
|
||||||
(cd client && npm run prettier:check)
|
(cd client && npm run prettier:check)
|
||||||
|
|
||||||
echo 'lint and typecheck'
|
echo 'lint and typecheck'
|
||||||
(cd client && npm run lint && npm run typecheck)
|
(cd client && npm run lint:errors && npm run typecheck)
|
||||||
|
|
||||||
echo 'python ufmt check'
|
echo 'python ufmt check'
|
||||||
ufmt check server
|
ufmt check server
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ from rest_framework.response import Response
|
||||||
|
|
||||||
from vbv_lernwelt.core.models import Country, Organisation
|
from vbv_lernwelt.core.models import Country, Organisation
|
||||||
from vbv_lernwelt.core.serializers import CountrySerializer, OrganisationSerializer
|
from vbv_lernwelt.core.serializers import CountrySerializer, OrganisationSerializer
|
||||||
|
from vbv_lernwelt.learnpath.models import CourseProfile
|
||||||
|
from vbv_lernwelt.learnpath.serializers import CourseProfileSerializer
|
||||||
|
|
||||||
|
|
||||||
@api_view(["GET"])
|
@api_view(["GET"])
|
||||||
|
|
@ -26,4 +28,13 @@ def list_entities(request):
|
||||||
countries = CountrySerializer(
|
countries = CountrySerializer(
|
||||||
Country.objects.all(), many=True, context=context
|
Country.objects.all(), many=True, context=context
|
||||||
).data
|
).data
|
||||||
return Response({"organisations": organisations, "countries": countries})
|
course_profiles = CourseProfileSerializer(
|
||||||
|
CourseProfile.objects.all(), many=True, context=context
|
||||||
|
).data
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"organisations": organisations,
|
||||||
|
"countries": countries,
|
||||||
|
"courseProfiles": course_profiles,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
from django.shortcuts import get_object_or_404
|
|
||||||
from rest_framework.decorators import api_view, permission_classes
|
from rest_framework.decorators import api_view, permission_classes
|
||||||
from rest_framework.generics import get_object_or_404
|
from rest_framework.generics import get_object_or_404
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ from graphql import GraphQLError
|
||||||
from rest_framework.exceptions import PermissionDenied
|
from rest_framework.exceptions import PermissionDenied
|
||||||
|
|
||||||
from vbv_lernwelt.competence.graphql.types import ActionCompetenceObjectType
|
from vbv_lernwelt.competence.graphql.types import ActionCompetenceObjectType
|
||||||
|
from vbv_lernwelt.core.models import User
|
||||||
from vbv_lernwelt.course.models import (
|
from vbv_lernwelt.course.models import (
|
||||||
CircleDocument,
|
CircleDocument,
|
||||||
Course,
|
Course,
|
||||||
|
|
@ -29,8 +30,9 @@ from vbv_lernwelt.course_session.models import (
|
||||||
)
|
)
|
||||||
from vbv_lernwelt.course_session_group.models import CourseSessionGroup
|
from vbv_lernwelt.course_session_group.models import CourseSessionGroup
|
||||||
from vbv_lernwelt.iam.permissions import has_course_access
|
from vbv_lernwelt.iam.permissions import has_course_access
|
||||||
|
from vbv_lernwelt.learnpath.consts import COURSE_PROFILE_ALL_ID
|
||||||
from vbv_lernwelt.learnpath.graphql.types import LearningPathObjectType
|
from vbv_lernwelt.learnpath.graphql.types import LearningPathObjectType
|
||||||
from vbv_lernwelt.learnpath.models import Circle
|
from vbv_lernwelt.learnpath.models import Circle, CourseProfile
|
||||||
|
|
||||||
logger = structlog.get_logger(__name__)
|
logger = structlog.get_logger(__name__)
|
||||||
|
|
||||||
|
|
@ -106,6 +108,12 @@ class CourseObjectType(DjangoObjectType):
|
||||||
graphene.NonNull(ActionCompetenceObjectType), required=True
|
graphene.NonNull(ActionCompetenceObjectType), required=True
|
||||||
)
|
)
|
||||||
configuration = graphene.Field(CourseConfigurationObjectType, required=True)
|
configuration = graphene.Field(CourseConfigurationObjectType, required=True)
|
||||||
|
profiles = graphene.List(graphene.String)
|
||||||
|
course_session_users = graphene.List(
|
||||||
|
"vbv_lernwelt.course.graphql.types.CourseSessionUserType",
|
||||||
|
required=True,
|
||||||
|
id=graphene.String(),
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Course
|
model = Course
|
||||||
|
|
@ -125,6 +133,22 @@ class CourseObjectType(DjangoObjectType):
|
||||||
def resolve_action_competences(root: Course, info):
|
def resolve_action_competences(root: Course, info):
|
||||||
return root.get_action_competences()
|
return root.get_action_competences()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def resolve_profiles(root: Course, info, **kwargs):
|
||||||
|
if root.configuration.is_vv:
|
||||||
|
return CourseProfile.objects.values_list("code", flat=True)
|
||||||
|
return []
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def resolve_course_session_users(root: Course, info, id=None, **kwargs):
|
||||||
|
# todo: restrict users that can be queried
|
||||||
|
if id is not None:
|
||||||
|
user = User.objects.get(id=id)
|
||||||
|
else:
|
||||||
|
user = info.context.user
|
||||||
|
users = CourseSessionUser.objects.filter(user=user, course_session__course=root)
|
||||||
|
return users
|
||||||
|
|
||||||
|
|
||||||
class CourseSessionUserExpertCircleType(ObjectType):
|
class CourseSessionUserExpertCircleType(ObjectType):
|
||||||
id = graphene.ID(required=True)
|
id = graphene.ID(required=True)
|
||||||
|
|
@ -132,6 +156,21 @@ class CourseSessionUserExpertCircleType(ObjectType):
|
||||||
slug = graphene.String(required=True)
|
slug = graphene.String(required=True)
|
||||||
|
|
||||||
|
|
||||||
|
class CourseSessionUserType(DjangoObjectType):
|
||||||
|
chosen_profile = graphene.String(required=True)
|
||||||
|
course_session = graphene.Field(
|
||||||
|
"vbv_lernwelt.course.graphql.types.CourseSessionObjectType", required=True
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = CourseSessionUser
|
||||||
|
fields = ["chosen_profile", "id"]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def resolve_chosen_profile(root: CourseSessionUser, info, **kwargs):
|
||||||
|
return getattr(root.chosen_profile, "code", "")
|
||||||
|
|
||||||
|
|
||||||
class CourseSessionUserObjectsType(ObjectType):
|
class CourseSessionUserObjectsType(ObjectType):
|
||||||
"""
|
"""
|
||||||
WORKAROUND:
|
WORKAROUND:
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,7 @@ from vbv_lernwelt.learnpath.create_vv_new_learning_path import (
|
||||||
create_vv_new_learning_path,
|
create_vv_new_learning_path,
|
||||||
create_vv_pruefung_learning_path,
|
create_vv_pruefung_learning_path,
|
||||||
)
|
)
|
||||||
|
from vbv_lernwelt.learnpath.creators import assign_circles_to_profiles
|
||||||
from vbv_lernwelt.learnpath.models import (
|
from vbv_lernwelt.learnpath.models import (
|
||||||
Circle,
|
Circle,
|
||||||
LearningContent,
|
LearningContent,
|
||||||
|
|
@ -222,6 +223,7 @@ def create_versicherungsvermittlerin_course(
|
||||||
create_vv_gewinnen_casework(course_id=course_id)
|
create_vv_gewinnen_casework(course_id=course_id)
|
||||||
create_vv_reflection(course_id=course_id)
|
create_vv_reflection(course_id=course_id)
|
||||||
create_vv_new_learning_path(course_id=course_id)
|
create_vv_new_learning_path(course_id=course_id)
|
||||||
|
assign_circles_to_profiles()
|
||||||
|
|
||||||
cs = CourseSession.objects.create(course_id=course_id, title=names[language])
|
cs = CourseSession.objects.create(course_id=course_id, title=names[language])
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
# Generated by Django 3.2.20 on 2024-07-11 09:00
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("learnpath", "0017_auto_20240711_1100"),
|
||||||
|
("course", "0008_auto_20240403_1132"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="coursesessionuser",
|
||||||
|
name="chosen_profile",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
to="learnpath.courseprofile",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
# Generated by Django 4.2.13 on 2024-07-22 19:45
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import vbv_lernwelt.course.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("course", "0009_coursesessionuser_chosen_profile"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="coursecompletion",
|
||||||
|
name="completion_status",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("SUCCESS", "Success"),
|
||||||
|
("FAIL", "Fail"),
|
||||||
|
("UNKNOWN", "Unknown"),
|
||||||
|
],
|
||||||
|
default=vbv_lernwelt.course.models.CourseCompletionStatus["UNKNOWN"],
|
||||||
|
max_length=255,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
# Generated by Django 4.2.13 on 2024-08-07 11:17
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("course", "0009_coursesessionuser_required_attendance_and_more"),
|
||||||
|
("course", "0010_alter_coursecompletion_completion_status"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = []
|
||||||
|
|
@ -285,6 +285,10 @@ class CourseSessionUser(models.Model):
|
||||||
)
|
)
|
||||||
optional_attendance = models.BooleanField(default=False)
|
optional_attendance = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
chosen_profile = models.ForeignKey(
|
||||||
|
"learnpath.CourseProfile", on_delete=models.SET_NULL, blank=True, null=True
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
constraints = [
|
constraints = [
|
||||||
UniqueConstraint(
|
UniqueConstraint(
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,10 @@ from vbv_lernwelt.course.models import (
|
||||||
CourseCompletion,
|
CourseCompletion,
|
||||||
CourseConfiguration,
|
CourseConfiguration,
|
||||||
CourseSession,
|
CourseSession,
|
||||||
|
CourseSessionUser,
|
||||||
)
|
)
|
||||||
from vbv_lernwelt.iam.permissions import course_session_permissions
|
from vbv_lernwelt.iam.permissions import course_session_permissions
|
||||||
|
from vbv_lernwelt.learnpath.models import CourseProfile
|
||||||
|
|
||||||
|
|
||||||
class CourseConfigurationSerializer(serializers.ModelSerializer):
|
class CourseConfigurationSerializer(serializers.ModelSerializer):
|
||||||
|
|
@ -31,10 +33,23 @@ class CourseSerializer(serializers.ModelSerializer):
|
||||||
configuration = CourseConfigurationSerializer(
|
configuration = CourseConfigurationSerializer(
|
||||||
read_only=True,
|
read_only=True,
|
||||||
)
|
)
|
||||||
|
course_profiles = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
def get_course_profiles(self, obj):
|
||||||
|
if obj.configuration.is_vv:
|
||||||
|
return CourseProfile.objects.all().values_list("code", flat=True)
|
||||||
|
return []
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Course
|
model = Course
|
||||||
fields = ["id", "title", "category_name", "slug", "configuration"]
|
fields = [
|
||||||
|
"id",
|
||||||
|
"title",
|
||||||
|
"category_name",
|
||||||
|
"slug",
|
||||||
|
"configuration",
|
||||||
|
"course_profiles",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class CourseCategorySerializer(serializers.ModelSerializer):
|
class CourseCategorySerializer(serializers.ModelSerializer):
|
||||||
|
|
@ -103,6 +118,12 @@ class CourseSessionSerializer(serializers.ModelSerializer):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
class CypressCourseSessionUserSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = CourseSessionUser
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
class CircleDocumentSerializer(serializers.ModelSerializer):
|
class CircleDocumentSerializer(serializers.ModelSerializer):
|
||||||
learning_sequence = serializers.SerializerMethodField()
|
learning_sequence = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,164 @@
|
||||||
|
from django.test import RequestFactory, TestCase
|
||||||
|
from graphene.test import Client
|
||||||
|
|
||||||
|
from vbv_lernwelt.core.admin import User
|
||||||
|
from vbv_lernwelt.core.create_default_users import create_default_users
|
||||||
|
from vbv_lernwelt.core.schema import schema
|
||||||
|
from vbv_lernwelt.course.management.commands.create_default_courses import (
|
||||||
|
create_versicherungsvermittlerin_course,
|
||||||
|
)
|
||||||
|
from vbv_lernwelt.course.models import CourseSessionUser
|
||||||
|
from vbv_lernwelt.learnpath.creators import create_course_profiles
|
||||||
|
from vbv_lernwelt.learnpath.models import CourseProfile
|
||||||
|
|
||||||
|
|
||||||
|
class CourseGraphQLTestCase(TestCase):
|
||||||
|
def setUp(self) -> None:
|
||||||
|
create_default_users()
|
||||||
|
create_course_profiles()
|
||||||
|
create_versicherungsvermittlerin_course()
|
||||||
|
|
||||||
|
def test_update_course_profile(self):
|
||||||
|
user = User.objects.get(username="student-vv@eiger-versicherungen.ch")
|
||||||
|
request = RequestFactory().get("/")
|
||||||
|
request.user = user
|
||||||
|
client = Client(schema=schema, context_value=request)
|
||||||
|
query = """
|
||||||
|
query CourseQuery($slug: String!) {
|
||||||
|
course(slug: $slug){
|
||||||
|
id
|
||||||
|
profiles
|
||||||
|
course_session_users {
|
||||||
|
id
|
||||||
|
__typename
|
||||||
|
chosen_profile
|
||||||
|
course_session {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
slug = "versicherungsvermittler-in"
|
||||||
|
variables = {"slug": slug}
|
||||||
|
result = client.execute(query, variables=variables)
|
||||||
|
|
||||||
|
self.assertIsNone(result.get("errors"))
|
||||||
|
data = result.get("data")
|
||||||
|
course = data.get("course")
|
||||||
|
profiles = course.get("profiles")
|
||||||
|
self.assertEqual(
|
||||||
|
set(profiles),
|
||||||
|
set(["all", "nichtleben", "leben", "krankenzusatzversicherung"]),
|
||||||
|
)
|
||||||
|
course_session_user = course.get("course_session_users")[0]
|
||||||
|
chosen_profile = course_session_user.get("chosen_profile")
|
||||||
|
|
||||||
|
self.assertEqual(chosen_profile, "")
|
||||||
|
|
||||||
|
mutation = """
|
||||||
|
mutation UpdateCourseSessionProfile($input: CourseSessionProfileMutationInput!) {
|
||||||
|
update_course_session_profile(input: $input) {
|
||||||
|
clientMutationId
|
||||||
|
result {
|
||||||
|
__typename
|
||||||
|
... on UpdateCourseProfileSuccess {
|
||||||
|
user {
|
||||||
|
id
|
||||||
|
chosen_profile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
... on UpdateCourseProfileError {
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
profile = "nichtleben"
|
||||||
|
input = {"course_profile": profile, "course_slug": slug}
|
||||||
|
|
||||||
|
mutation_result = client.execute(mutation, variables={"input": input})
|
||||||
|
|
||||||
|
self.assertIsNone(mutation_result.get("errors"))
|
||||||
|
|
||||||
|
second_query_result = client.execute(query, variables=variables)
|
||||||
|
|
||||||
|
self.assertIsNone(second_query_result.get("errors"))
|
||||||
|
data = second_query_result.get("data")
|
||||||
|
course = data.get("course")
|
||||||
|
profiles = course.get("profiles")
|
||||||
|
self.assertEqual(
|
||||||
|
set(profiles),
|
||||||
|
set(["all", "nichtleben", "leben", "krankenzusatzversicherung"]),
|
||||||
|
)
|
||||||
|
course_session_user = course.get("course_session_users")[0]
|
||||||
|
chosen_profile = course_session_user.get("chosen_profile")
|
||||||
|
self.assertEqual(chosen_profile, profile)
|
||||||
|
|
||||||
|
def test_mentor_profile_view(self):
|
||||||
|
user = User.objects.get(username="test-mentor1@example.com")
|
||||||
|
request = RequestFactory().get("/")
|
||||||
|
request.user = user
|
||||||
|
client = Client(schema=schema, context_value=request)
|
||||||
|
|
||||||
|
query = """
|
||||||
|
query courseQuery($slug: String!, $user: String) {
|
||||||
|
course(slug: $slug) {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
slug
|
||||||
|
category_name
|
||||||
|
profiles
|
||||||
|
course_session_users(id: $user) {
|
||||||
|
id
|
||||||
|
__typename
|
||||||
|
chosen_profile
|
||||||
|
course_session {
|
||||||
|
id
|
||||||
|
__typename
|
||||||
|
}
|
||||||
|
}
|
||||||
|
__typename
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
student = User.objects.get(username="student-vv@eiger-versicherungen.ch")
|
||||||
|
|
||||||
|
slug = "versicherungsvermittler-in"
|
||||||
|
student_id = str(student.id)
|
||||||
|
variables = {"slug": slug, "user": student_id}
|
||||||
|
print(variables)
|
||||||
|
result = client.execute(query, variables=variables)
|
||||||
|
self.assertIsNone(result.get("errors"))
|
||||||
|
data = result.get("data")
|
||||||
|
course = data.get("course")
|
||||||
|
profiles = course.get("profiles")
|
||||||
|
self.assertEqual(
|
||||||
|
set(profiles),
|
||||||
|
set(["all", "nichtleben", "leben", "krankenzusatzversicherung"]),
|
||||||
|
)
|
||||||
|
course_session_user = course.get("course_session_users")[0]
|
||||||
|
chosen_profile = course_session_user.get("chosen_profile")
|
||||||
|
self.assertEqual(chosen_profile, "")
|
||||||
|
|
||||||
|
csu = CourseSessionUser.objects.get(
|
||||||
|
course_session__course__slug=slug, user=student
|
||||||
|
)
|
||||||
|
course_profile = CourseProfile.objects.get(code="nichtleben")
|
||||||
|
csu.chosen_profile = course_profile
|
||||||
|
csu.save()
|
||||||
|
|
||||||
|
second_result = client.execute(query, variables=variables)
|
||||||
|
self.assertIsNone(second_result.get("errors"))
|
||||||
|
data = second_result.get("data")
|
||||||
|
course = data.get("course")
|
||||||
|
profiles = course.get("profiles")
|
||||||
|
self.assertEqual(
|
||||||
|
set(profiles),
|
||||||
|
set(["all", "nichtleben", "leben", "krankenzusatzversicherung"]),
|
||||||
|
)
|
||||||
|
course_session_user = course.get("course_session_users")[0]
|
||||||
|
chosen_profile = course_session_user.get("chosen_profile")
|
||||||
|
self.assertEqual(chosen_profile, "nichtleben")
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
import graphene
|
import graphene
|
||||||
import structlog
|
import structlog
|
||||||
|
from graphene import relay
|
||||||
from rest_framework.exceptions import PermissionDenied
|
from rest_framework.exceptions import PermissionDenied
|
||||||
|
|
||||||
|
from vbv_lernwelt.course.graphql.types import CourseSessionUserType
|
||||||
|
from vbv_lernwelt.course.models import CourseSessionUser
|
||||||
from vbv_lernwelt.course_session.graphql.types import (
|
from vbv_lernwelt.course_session.graphql.types import (
|
||||||
CourseSessionAttendanceCourseObjectType,
|
CourseSessionAttendanceCourseObjectType,
|
||||||
)
|
)
|
||||||
|
|
@ -11,10 +14,33 @@ from vbv_lernwelt.course_session.services.attendance import (
|
||||||
update_attendance_list,
|
update_attendance_list,
|
||||||
)
|
)
|
||||||
from vbv_lernwelt.iam.permissions import has_course_access
|
from vbv_lernwelt.iam.permissions import has_course_access
|
||||||
|
from vbv_lernwelt.learnpath.models import CourseProfile
|
||||||
|
|
||||||
logger = structlog.get_logger(__name__)
|
logger = structlog.get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateCourseProfileSuccess(graphene.ObjectType):
|
||||||
|
user = graphene.Field(CourseSessionUserType(), required=True)
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateCourseProfileError(graphene.ObjectType):
|
||||||
|
message = graphene.String()
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateCourseProfileResult(graphene.Union):
|
||||||
|
class Meta:
|
||||||
|
types = (
|
||||||
|
UpdateCourseProfileError,
|
||||||
|
UpdateCourseProfileSuccess,
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def resolve_type(cls, instance, info):
|
||||||
|
if type(instance).__name__ == "UpdateCourseProfileSuccess":
|
||||||
|
return UpdateCourseProfileSuccess
|
||||||
|
return UpdateCourseProfileError
|
||||||
|
|
||||||
|
|
||||||
class AttendanceUserInputType(graphene.InputObjectType):
|
class AttendanceUserInputType(graphene.InputObjectType):
|
||||||
user_id = graphene.UUID(required=True)
|
user_id = graphene.UUID(required=True)
|
||||||
status = graphene.Field(
|
status = graphene.Field(
|
||||||
|
|
@ -57,5 +83,40 @@ class AttendanceCourseUserMutation(graphene.Mutation):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CourseSessionProfileMutation(relay.ClientIDMutation):
|
||||||
|
class Input:
|
||||||
|
course_profile = graphene.String(required=True)
|
||||||
|
course_slug = graphene.String(required=True)
|
||||||
|
|
||||||
|
result = UpdateCourseProfileResult()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def mutate_and_get_payload(cls, root, info, **input):
|
||||||
|
course_profile = input.get("course_profile")
|
||||||
|
course_slug = input.get("course_slug")
|
||||||
|
user = info.context.user
|
||||||
|
|
||||||
|
try:
|
||||||
|
if course_profile == "":
|
||||||
|
profile = None
|
||||||
|
else:
|
||||||
|
profile = CourseProfile.objects.get(code=course_profile)
|
||||||
|
|
||||||
|
# csu = user.coursesessionuser_set.first()
|
||||||
|
csu = CourseSessionUser.objects.get(
|
||||||
|
course_session__course__slug=course_slug, user=user
|
||||||
|
)
|
||||||
|
csu.chosen_profile = profile
|
||||||
|
csu.save()
|
||||||
|
return cls(result=UpdateCourseProfileSuccess(user=csu))
|
||||||
|
except CourseProfile.DoesNotExist:
|
||||||
|
return cls(result=UpdateCourseProfileError("Course Profile does not exist"))
|
||||||
|
except CourseSessionUser.DoesNotExist:
|
||||||
|
return cls(
|
||||||
|
result=UpdateCourseProfileError("Course Session User does not exist")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class CourseSessionMutation:
|
class CourseSessionMutation:
|
||||||
update_course_session_attendance_course_users = AttendanceCourseUserMutation.Field()
|
update_course_session_attendance_course_users = AttendanceCourseUserMutation.Field()
|
||||||
|
update_course_session_profile = CourseSessionProfileMutation.Field()
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,8 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
# Register your models here.
|
from vbv_lernwelt.learnpath.models import CourseProfile
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(CourseProfile)
|
||||||
|
class CourseProfileAdmin(admin.ModelAdmin):
|
||||||
|
pass
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
COURSE_PROFILE_LEBEN_ID = -1
|
||||||
|
COURSE_PROFILE_NICHTLEBEN_ID = -2
|
||||||
|
COURSE_PROFILE_KRANKENZUSATZ_ID = -3
|
||||||
|
COURSE_PROFILE_ALL_ID = -99
|
||||||
|
|
||||||
|
COURSE_PROFILE_LEBEN_CODE = "leben"
|
||||||
|
COURSE_PROFILE_NICHTLEBEN_CODE = "nichtleben"
|
||||||
|
COURSE_PROFILE_KRANKENZUSATZ_CODE = "krankenzusatzversicherung"
|
||||||
|
COURSE_PROFILE_ALL_CODE = "all"
|
||||||
|
|
@ -0,0 +1,192 @@
|
||||||
|
import structlog
|
||||||
|
|
||||||
|
from vbv_lernwelt.course.consts import (
|
||||||
|
COURSE_VERSICHERUNGSVERMITTLERIN_FR_ID,
|
||||||
|
COURSE_VERSICHERUNGSVERMITTLERIN_ID,
|
||||||
|
COURSE_VERSICHERUNGSVERMITTLERIN_IT_ID,
|
||||||
|
)
|
||||||
|
from vbv_lernwelt.learnpath.consts import (
|
||||||
|
COURSE_PROFILE_ALL_CODE,
|
||||||
|
COURSE_PROFILE_ALL_ID,
|
||||||
|
COURSE_PROFILE_KRANKENZUSATZ_CODE,
|
||||||
|
COURSE_PROFILE_KRANKENZUSATZ_ID,
|
||||||
|
COURSE_PROFILE_LEBEN_CODE,
|
||||||
|
COURSE_PROFILE_LEBEN_ID,
|
||||||
|
COURSE_PROFILE_NICHTLEBEN_CODE,
|
||||||
|
COURSE_PROFILE_NICHTLEBEN_ID,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger = structlog.get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def create_course_profiles():
|
||||||
|
from vbv_lernwelt.learnpath.models import CourseProfile
|
||||||
|
|
||||||
|
# Allbranche, Krankenzusatzversicherung, nicht Leben, Leben
|
||||||
|
CourseProfile.objects.get_or_create(
|
||||||
|
id=COURSE_PROFILE_ALL_ID, code=COURSE_PROFILE_ALL_CODE, order=1
|
||||||
|
)
|
||||||
|
CourseProfile.objects.get_or_create(
|
||||||
|
id=COURSE_PROFILE_KRANKENZUSATZ_ID,
|
||||||
|
code=COURSE_PROFILE_KRANKENZUSATZ_CODE,
|
||||||
|
order=2,
|
||||||
|
)
|
||||||
|
CourseProfile.objects.get_or_create(
|
||||||
|
id=COURSE_PROFILE_NICHTLEBEN_ID, code=COURSE_PROFILE_NICHTLEBEN_CODE, order=3
|
||||||
|
)
|
||||||
|
CourseProfile.objects.get_or_create(
|
||||||
|
id=COURSE_PROFILE_LEBEN_ID, code=COURSE_PROFILE_LEBEN_CODE, order=4
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def assign_circle_to_profile_curry(course_page):
|
||||||
|
from vbv_lernwelt.learnpath.models import Circle, CourseProfile
|
||||||
|
|
||||||
|
def assign_circle_to_profile(title, code):
|
||||||
|
try:
|
||||||
|
circle = Circle.objects.descendant_of(course_page).get(title=title)
|
||||||
|
course_profile = CourseProfile.objects.get(code=code)
|
||||||
|
circle.profiles.add(course_profile)
|
||||||
|
circle.save()
|
||||||
|
except Circle.DoesNotExist:
|
||||||
|
logger.warning("assign_circle_to_profile: circle not found", title=title)
|
||||||
|
|
||||||
|
return assign_circle_to_profile
|
||||||
|
|
||||||
|
|
||||||
|
def make_base_circle_curry(course_page):
|
||||||
|
from vbv_lernwelt.learnpath.models import Circle
|
||||||
|
|
||||||
|
def make_base_circle(title):
|
||||||
|
try:
|
||||||
|
circle = Circle.objects.descendant_of(course_page).get(title=title)
|
||||||
|
circle.is_base_circle = True
|
||||||
|
circle.save()
|
||||||
|
except Circle.DoesNotExist:
|
||||||
|
logger.warning("assign_circle_to_profile: circle not found", title=title)
|
||||||
|
|
||||||
|
return make_base_circle
|
||||||
|
|
||||||
|
|
||||||
|
def assign_de_circles_to_profiles():
|
||||||
|
from vbv_lernwelt.course.models import CoursePage
|
||||||
|
|
||||||
|
try:
|
||||||
|
course_page = CoursePage.objects.get(
|
||||||
|
course_id=COURSE_VERSICHERUNGSVERMITTLERIN_ID
|
||||||
|
)
|
||||||
|
except CoursePage.DoesNotExist:
|
||||||
|
logger.warning("Course does not exist yet")
|
||||||
|
return
|
||||||
|
|
||||||
|
assign_circle_to_profile = assign_circle_to_profile_curry(course_page)
|
||||||
|
make_base_circle = make_base_circle_curry(course_page)
|
||||||
|
|
||||||
|
assign_circle_to_profile("Fahrzeug", COURSE_PROFILE_NICHTLEBEN_CODE)
|
||||||
|
assign_circle_to_profile("Haushalt", COURSE_PROFILE_NICHTLEBEN_CODE)
|
||||||
|
assign_circle_to_profile("Rechtsstreitigkeiten", COURSE_PROFILE_NICHTLEBEN_CODE)
|
||||||
|
assign_circle_to_profile("Reisen", COURSE_PROFILE_NICHTLEBEN_CODE)
|
||||||
|
assign_circle_to_profile("Wohneigentum", COURSE_PROFILE_NICHTLEBEN_CODE)
|
||||||
|
assign_circle_to_profile(
|
||||||
|
"Selbstständigkeit", COURSE_PROFILE_NICHTLEBEN_CODE
|
||||||
|
) # typo, but that's how it is in prod data
|
||||||
|
assign_circle_to_profile("KMU", COURSE_PROFILE_NICHTLEBEN_CODE)
|
||||||
|
|
||||||
|
assign_circle_to_profile("Einkommenssicherung", COURSE_PROFILE_LEBEN_CODE)
|
||||||
|
assign_circle_to_profile("Pensionierung", COURSE_PROFILE_LEBEN_CODE)
|
||||||
|
assign_circle_to_profile("Erben/Vererben", COURSE_PROFILE_LEBEN_CODE)
|
||||||
|
assign_circle_to_profile("Sparen", COURSE_PROFILE_LEBEN_CODE)
|
||||||
|
assign_circle_to_profile(
|
||||||
|
"Selbstständigkeit", COURSE_PROFILE_LEBEN_CODE
|
||||||
|
) # typo, but that's how it is in prod data
|
||||||
|
assign_circle_to_profile("KMU", COURSE_PROFILE_LEBEN_CODE)
|
||||||
|
|
||||||
|
assign_circle_to_profile("Gesundheit", COURSE_PROFILE_KRANKENZUSATZ_CODE)
|
||||||
|
|
||||||
|
make_base_circle("Kickoff")
|
||||||
|
make_base_circle("Basis")
|
||||||
|
make_base_circle("Gewinnen")
|
||||||
|
make_base_circle("Prüfungsvorbereitung")
|
||||||
|
make_base_circle("Prüfung")
|
||||||
|
|
||||||
|
|
||||||
|
def assign_fr_circles_to_profiles():
|
||||||
|
from vbv_lernwelt.course.models import CoursePage
|
||||||
|
|
||||||
|
try:
|
||||||
|
course_page = CoursePage.objects.get(
|
||||||
|
course_id=COURSE_VERSICHERUNGSVERMITTLERIN_FR_ID
|
||||||
|
)
|
||||||
|
except CoursePage.DoesNotExist:
|
||||||
|
logger.warning("Course does not exist yet")
|
||||||
|
return
|
||||||
|
|
||||||
|
assign_circle_to_profile = assign_circle_to_profile_curry(course_page)
|
||||||
|
make_base_circle = make_base_circle_curry(course_page)
|
||||||
|
|
||||||
|
assign_circle_to_profile("Véhicule", COURSE_PROFILE_NICHTLEBEN_CODE)
|
||||||
|
assign_circle_to_profile("Ménage", COURSE_PROFILE_NICHTLEBEN_CODE)
|
||||||
|
assign_circle_to_profile("Litiges juridiques", COURSE_PROFILE_NICHTLEBEN_CODE)
|
||||||
|
assign_circle_to_profile("Voyages", COURSE_PROFILE_NICHTLEBEN_CODE)
|
||||||
|
assign_circle_to_profile("Propriété du logement", COURSE_PROFILE_NICHTLEBEN_CODE)
|
||||||
|
assign_circle_to_profile("Activité indépendante", COURSE_PROFILE_NICHTLEBEN_CODE)
|
||||||
|
assign_circle_to_profile("PME", COURSE_PROFILE_NICHTLEBEN_CODE)
|
||||||
|
|
||||||
|
assign_circle_to_profile("Garantie des revenus", COURSE_PROFILE_LEBEN_CODE)
|
||||||
|
assign_circle_to_profile("Retraite", COURSE_PROFILE_LEBEN_CODE)
|
||||||
|
assign_circle_to_profile("Hériter\xa0/\xa0léguer", COURSE_PROFILE_LEBEN_CODE)
|
||||||
|
assign_circle_to_profile("Épargne", COURSE_PROFILE_LEBEN_CODE)
|
||||||
|
assign_circle_to_profile("Activité indépendante", COURSE_PROFILE_LEBEN_CODE)
|
||||||
|
assign_circle_to_profile("PME", COURSE_PROFILE_LEBEN_CODE)
|
||||||
|
|
||||||
|
assign_circle_to_profile("Santé", COURSE_PROFILE_KRANKENZUSATZ_CODE)
|
||||||
|
|
||||||
|
make_base_circle("Lancement")
|
||||||
|
make_base_circle("Base")
|
||||||
|
make_base_circle("Acquisition")
|
||||||
|
make_base_circle("Préparation à l’examen")
|
||||||
|
make_base_circle("L’examen")
|
||||||
|
|
||||||
|
|
||||||
|
def assign_it_circles_to_profiles():
|
||||||
|
from vbv_lernwelt.course.models import CoursePage
|
||||||
|
|
||||||
|
try:
|
||||||
|
course_page = CoursePage.objects.get(
|
||||||
|
course_id=COURSE_VERSICHERUNGSVERMITTLERIN_IT_ID
|
||||||
|
)
|
||||||
|
except CoursePage.DoesNotExist:
|
||||||
|
logger.warning("Course does not exist yet")
|
||||||
|
return
|
||||||
|
|
||||||
|
assign_circle_to_profile = assign_circle_to_profile_curry(course_page)
|
||||||
|
make_base_circle = make_base_circle_curry(course_page)
|
||||||
|
|
||||||
|
assign_circle_to_profile("Veicolo", COURSE_PROFILE_NICHTLEBEN_CODE)
|
||||||
|
assign_circle_to_profile("Economia domestica", COURSE_PROFILE_NICHTLEBEN_CODE)
|
||||||
|
assign_circle_to_profile("Controversie giuridiche", COURSE_PROFILE_NICHTLEBEN_CODE)
|
||||||
|
assign_circle_to_profile("Viaggi", COURSE_PROFILE_NICHTLEBEN_CODE)
|
||||||
|
assign_circle_to_profile("Casa di proprietà", COURSE_PROFILE_NICHTLEBEN_CODE)
|
||||||
|
assign_circle_to_profile("Attività indipendente", COURSE_PROFILE_NICHTLEBEN_CODE)
|
||||||
|
assign_circle_to_profile("PMI", COURSE_PROFILE_NICHTLEBEN_CODE)
|
||||||
|
|
||||||
|
assign_circle_to_profile("Protezione del reddito", COURSE_PROFILE_LEBEN_CODE)
|
||||||
|
assign_circle_to_profile("Pensionamento", COURSE_PROFILE_LEBEN_CODE)
|
||||||
|
assign_circle_to_profile("Ereditare/lasciare in eredità", COURSE_PROFILE_LEBEN_CODE)
|
||||||
|
assign_circle_to_profile("Risparmio", COURSE_PROFILE_LEBEN_CODE)
|
||||||
|
assign_circle_to_profile("Attività indipendente", COURSE_PROFILE_LEBEN_CODE)
|
||||||
|
assign_circle_to_profile("PMI", COURSE_PROFILE_LEBEN_CODE)
|
||||||
|
|
||||||
|
assign_circle_to_profile("Salute", COURSE_PROFILE_KRANKENZUSATZ_CODE)
|
||||||
|
|
||||||
|
make_base_circle("Kickoff")
|
||||||
|
make_base_circle("Base")
|
||||||
|
make_base_circle("Acquisizione")
|
||||||
|
make_base_circle("Preparazione all'esame")
|
||||||
|
make_base_circle("Esame")
|
||||||
|
|
||||||
|
|
||||||
|
def assign_circles_to_profiles():
|
||||||
|
assign_de_circles_to_profiles()
|
||||||
|
assign_fr_circles_to_profiles()
|
||||||
|
assign_it_circles_to_profiles()
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import random
|
||||||
|
|
||||||
import graphene
|
import graphene
|
||||||
import structlog
|
import structlog
|
||||||
from graphene_django import DjangoObjectType
|
from graphene_django import DjangoObjectType
|
||||||
|
|
@ -6,6 +8,7 @@ from vbv_lernwelt.core.utils import find_first_index
|
||||||
from vbv_lernwelt.course.graphql.interfaces import CoursePageInterface
|
from vbv_lernwelt.course.graphql.interfaces import CoursePageInterface
|
||||||
from vbv_lernwelt.learnpath.models import (
|
from vbv_lernwelt.learnpath.models import (
|
||||||
Circle,
|
Circle,
|
||||||
|
CourseProfile,
|
||||||
LearningContentAssignment,
|
LearningContentAssignment,
|
||||||
LearningContentAttendanceCourse,
|
LearningContentAttendanceCourse,
|
||||||
LearningContentDocumentList,
|
LearningContentDocumentList,
|
||||||
|
|
@ -299,14 +302,12 @@ class CircleObjectType(DjangoObjectType):
|
||||||
learning_sequences = graphene.List(
|
learning_sequences = graphene.List(
|
||||||
graphene.NonNull(LearningSequenceObjectType), required=True
|
graphene.NonNull(LearningSequenceObjectType), required=True
|
||||||
)
|
)
|
||||||
|
profiles = graphene.List(graphene.String, required=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Circle
|
model = Circle
|
||||||
interfaces = (CoursePageInterface,)
|
interfaces = (CoursePageInterface,)
|
||||||
fields = [
|
fields = ["description", "goals", "is_base_circle"]
|
||||||
"description",
|
|
||||||
"goals",
|
|
||||||
]
|
|
||||||
|
|
||||||
def resolve_learning_sequences(self: Circle, info, **kwargs):
|
def resolve_learning_sequences(self: Circle, info, **kwargs):
|
||||||
circle_descendants = None
|
circle_descendants = None
|
||||||
|
|
@ -335,6 +336,10 @@ class CircleObjectType(DjangoObjectType):
|
||||||
if descendant.specific_class == LearningSequence
|
if descendant.specific_class == LearningSequence
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def resolve_profiles(root: Circle, info, **kwargs):
|
||||||
|
return root.profiles.all()
|
||||||
|
|
||||||
|
|
||||||
class TopicObjectType(DjangoObjectType):
|
class TopicObjectType(DjangoObjectType):
|
||||||
circles = graphene.List(graphene.NonNull(CircleObjectType), required=True)
|
circles = graphene.List(graphene.NonNull(CircleObjectType), required=True)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
# Generated by Django 3.2.20 on 2024-07-11 09:00
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("learnpath", "0016_remove_learningunit_feedback_user"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="CourseProfile",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("code", models.CharField(max_length=255)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="CourseProfileToCircle",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="circle",
|
||||||
|
name="profiles",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
related_name="circles", to="learnpath.CourseProfile"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
# Generated by Django 4.2.13 on 2024-07-30 07:03
|
||||||
|
|
||||||
|
import modelcluster.fields
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("learnpath", "0017_auto_20240711_1100"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="courseprofile",
|
||||||
|
options={"ordering": ["order"]},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="circle",
|
||||||
|
name="is_base_circle",
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="courseprofile",
|
||||||
|
name="order",
|
||||||
|
field=models.IntegerField(default=999),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="circle",
|
||||||
|
name="profiles",
|
||||||
|
field=modelcluster.fields.ParentalManyToManyField(
|
||||||
|
related_name="circles", to="learnpath.courseprofile"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
# Generated by Django 4.2.13 on 2024-07-30 07:04
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
from vbv_lernwelt.learnpath.creators import create_course_profiles
|
||||||
|
|
||||||
|
|
||||||
|
def migrate(apps, schema_editor):
|
||||||
|
create_course_profiles()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
(
|
||||||
|
"learnpath",
|
||||||
|
"0018_alter_courseprofile_options_circle_is_base_circle_and_more",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [migrations.RunPython(migrate)]
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
# Generated by Django 4.2.13 on 2024-07-30 07:05
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
from vbv_lernwelt.learnpath.creators import assign_circles_to_profiles
|
||||||
|
|
||||||
|
|
||||||
|
def migrate(apps, schema_editor):
|
||||||
|
assign_circles_to_profiles()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("learnpath", "0019_auto_20240730_0904"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [migrations.RunPython(migrate)]
|
||||||
|
|
@ -3,6 +3,7 @@ from typing import Tuple
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
|
from modelcluster.models import ParentalManyToManyField
|
||||||
from wagtail.admin.panels import FieldPanel, PageChooserPanel
|
from wagtail.admin.panels import FieldPanel, PageChooserPanel
|
||||||
from wagtail.fields import RichTextField, StreamField
|
from wagtail.fields import RichTextField, StreamField
|
||||||
from wagtail.models import Page
|
from wagtail.models import Page
|
||||||
|
|
@ -66,6 +67,25 @@ class Topic(CourseBasePage):
|
||||||
return f"{self.title}"
|
return f"{self.title}"
|
||||||
|
|
||||||
|
|
||||||
|
class CourseProfile(models.Model):
|
||||||
|
code = models.CharField(max_length=255)
|
||||||
|
order = models.IntegerField(default=999)
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self.code
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = [
|
||||||
|
"order",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class CourseProfileToCircle(models.Model):
|
||||||
|
# this connects the course profile to a circle, because a circle can be in multiple profiles
|
||||||
|
# todo: to we even need a through model?
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Circle(CourseBasePage):
|
class Circle(CourseBasePage):
|
||||||
parent_page_types = ["learnpath.LearningPath"]
|
parent_page_types = ["learnpath.LearningPath"]
|
||||||
subpage_types = [
|
subpage_types = [
|
||||||
|
|
@ -95,9 +115,25 @@ class Circle(CourseBasePage):
|
||||||
|
|
||||||
goals = RichTextField(features=DEFAULT_RICH_TEXT_FEATURES_WITH_HEADER)
|
goals = RichTextField(features=DEFAULT_RICH_TEXT_FEATURES_WITH_HEADER)
|
||||||
|
|
||||||
|
profiles = ParentalManyToManyField(CourseProfile, related_name="circles")
|
||||||
|
|
||||||
|
# base circles do never belong to a course profile and should also get displayed no matter what profile is chosen
|
||||||
|
is_base_circle = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
# profile = models.ForeignKey(
|
||||||
|
# ApprovalProfile,
|
||||||
|
# null=True,
|
||||||
|
# blank=True,
|
||||||
|
# on_delete=models.SET_NULL,
|
||||||
|
# related_name="circles",
|
||||||
|
# help_text="Zulassungsprofil",
|
||||||
|
# )
|
||||||
|
|
||||||
content_panels = Page.content_panels + [
|
content_panels = Page.content_panels + [
|
||||||
FieldPanel("description"),
|
FieldPanel("description"),
|
||||||
FieldPanel("goals"),
|
FieldPanel("goals"),
|
||||||
|
FieldPanel("is_base_circle"),
|
||||||
|
FieldPanel("profiles"),
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_frontend_url(self):
|
def get_frontend_url(self):
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
from rest_framework import serializers
|
||||||
from rest_framework.fields import SerializerMethodField
|
from rest_framework.fields import SerializerMethodField
|
||||||
|
|
||||||
from vbv_lernwelt.competence.serializers import (
|
from vbv_lernwelt.competence.serializers import (
|
||||||
|
|
@ -6,6 +7,7 @@ from vbv_lernwelt.competence.serializers import (
|
||||||
from vbv_lernwelt.core.utils import get_django_content_type
|
from vbv_lernwelt.core.utils import get_django_content_type
|
||||||
from vbv_lernwelt.course.serializer_helpers import get_course_serializer_class
|
from vbv_lernwelt.course.serializer_helpers import get_course_serializer_class
|
||||||
from vbv_lernwelt.learnpath.models import (
|
from vbv_lernwelt.learnpath.models import (
|
||||||
|
CourseProfile,
|
||||||
LearningContentAssignment,
|
LearningContentAssignment,
|
||||||
LearningContentEdoniqTest,
|
LearningContentEdoniqTest,
|
||||||
LearningUnit,
|
LearningUnit,
|
||||||
|
|
@ -98,3 +100,9 @@ class LearningContentAssignmentSerializer(
|
||||||
}
|
}
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class CourseProfileSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = CourseProfile
|
||||||
|
fields = ["id", "code"]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
# Generated by Django 4.2.13 on 2024-07-22 19:45
|
||||||
|
|
||||||
|
import wagtail.images.models
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("media_files", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="contentimagerendition",
|
||||||
|
name="file",
|
||||||
|
field=wagtail.images.models.WagtailImageField(
|
||||||
|
height_field="height",
|
||||||
|
storage=wagtail.images.models.get_rendition_storage,
|
||||||
|
upload_to=wagtail.images.models.get_rendition_upload_to,
|
||||||
|
width_field="width",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="userimagerendition",
|
||||||
|
name="file",
|
||||||
|
field=wagtail.images.models.WagtailImageField(
|
||||||
|
height_field="height",
|
||||||
|
storage=wagtail.images.models.get_rendition_storage,
|
||||||
|
upload_to=wagtail.images.models.get_rendition_upload_to,
|
||||||
|
width_field="width",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
# Generated by Django 3.2.20 on 2024-07-11 15:36
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("shop", "0015_cembra_fields"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="checkoutinformation",
|
||||||
|
name="refno2",
|
||||||
|
field=models.CharField(max_length=255),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
# Generated by Django 4.2.13 on 2024-07-30 07:03
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
(
|
||||||
|
"learnpath",
|
||||||
|
"0018_alter_courseprofile_options_circle_is_base_circle_and_more",
|
||||||
|
),
|
||||||
|
("shop", "0016_alter_checkoutinformation_refno2"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="checkoutinformation",
|
||||||
|
name="chosen_profile",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
to="learnpath.courseprofile",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -106,7 +106,9 @@ class CheckoutInformation(models.Model):
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
)
|
)
|
||||||
|
chosen_profile = models.ForeignKey(
|
||||||
|
"learnpath.CourseProfile", on_delete=models.SET_NULL, null=True, blank=True
|
||||||
|
)
|
||||||
# webhook metadata
|
# webhook metadata
|
||||||
webhook_history = models.JSONField(default=list)
|
webhook_history = models.JSONField(default=list)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ from rest_framework.test import APITestCase
|
||||||
|
|
||||||
from vbv_lernwelt.core.admin import User
|
from vbv_lernwelt.core.admin import User
|
||||||
from vbv_lernwelt.core.model_utils import add_countries
|
from vbv_lernwelt.core.model_utils import add_countries
|
||||||
|
from vbv_lernwelt.learnpath.consts import COURSE_PROFILE_ALL_CODE, COURSE_PROFILE_ALL_ID
|
||||||
|
from vbv_lernwelt.learnpath.models import CourseProfile
|
||||||
from vbv_lernwelt.shop.const import VV_DE_PRODUCT_SKU
|
from vbv_lernwelt.shop.const import VV_DE_PRODUCT_SKU
|
||||||
from vbv_lernwelt.shop.models import CheckoutInformation, CheckoutState, Product
|
from vbv_lernwelt.shop.models import CheckoutInformation, CheckoutState, Product
|
||||||
from vbv_lernwelt.shop.services import InitTransactionException
|
from vbv_lernwelt.shop.services import InitTransactionException
|
||||||
|
|
@ -50,6 +52,10 @@ class CheckoutAPITestCase(APITestCase):
|
||||||
is_active=True,
|
is_active=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
CourseProfile.objects.get_or_create(
|
||||||
|
id=COURSE_PROFILE_ALL_ID, code=COURSE_PROFILE_ALL_CODE
|
||||||
|
)
|
||||||
|
|
||||||
self.client.login(username=USER_USERNAME, password=USER_PASSWORD)
|
self.client.login(username=USER_USERNAME, password=USER_PASSWORD)
|
||||||
add_countries(small_set=True)
|
add_countries(small_set=True)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ from rest_framework.permissions import IsAuthenticated
|
||||||
from sentry_sdk import capture_exception
|
from sentry_sdk import capture_exception
|
||||||
|
|
||||||
from vbv_lernwelt.course.models import CourseSession, CourseSessionUser
|
from vbv_lernwelt.course.models import CourseSession, CourseSessionUser
|
||||||
|
from vbv_lernwelt.learnpath.consts import COURSE_PROFILE_ALL_ID
|
||||||
|
from vbv_lernwelt.learnpath.models import CourseProfile
|
||||||
from vbv_lernwelt.notify.email.email_services import EmailTemplate, send_email
|
from vbv_lernwelt.notify.email.email_services import EmailTemplate, send_email
|
||||||
from vbv_lernwelt.shop.const import (
|
from vbv_lernwelt.shop.const import (
|
||||||
VV_DE_PRODUCT_SKU,
|
VV_DE_PRODUCT_SKU,
|
||||||
|
|
@ -92,6 +94,7 @@ def checkout_vv(request):
|
||||||
|
|
||||||
sku = request.data["product"]
|
sku = request.data["product"]
|
||||||
base_redirect_url = request.data["redirect_url"]
|
base_redirect_url = request.data["redirect_url"]
|
||||||
|
chosen_profile_id = request.data.get("chosen_profile", COURSE_PROFILE_ALL_ID)
|
||||||
|
|
||||||
log.info("Checkout requested: sku", user_id=request.user.id, sku=sku)
|
log.info("Checkout requested: sku", user_id=request.user.id, sku=sku)
|
||||||
|
|
||||||
|
|
@ -106,6 +109,11 @@ def checkout_vv(request):
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
chosen_profile = CourseProfile.objects.get(id=chosen_profile_id)
|
||||||
|
except CourseProfile.DoesNotExist:
|
||||||
|
chosen_profile = CourseProfile.objects.get(id=COURSE_PROFILE_ALL_ID)
|
||||||
|
|
||||||
checkouts = CheckoutInformation.objects.filter(
|
checkouts = CheckoutInformation.objects.filter(
|
||||||
user=request.user,
|
user=request.user,
|
||||||
product_sku=sku,
|
product_sku=sku,
|
||||||
|
|
@ -151,6 +159,7 @@ def checkout_vv(request):
|
||||||
"device_fingerprint_session_key", ""
|
"device_fingerprint_session_key", ""
|
||||||
),
|
),
|
||||||
# address
|
# address
|
||||||
|
chosen_profile=chosen_profile,
|
||||||
**request.data["address"],
|
**request.data["address"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -257,9 +266,11 @@ def create_vv_course_session_user(checkout_info: CheckoutInformation):
|
||||||
_, created = CourseSessionUser.objects.get_or_create(
|
_, created = CourseSessionUser.objects.get_or_create(
|
||||||
user=checkout_info.user,
|
user=checkout_info.user,
|
||||||
role=CourseSessionUser.Role.MEMBER,
|
role=CourseSessionUser.Role.MEMBER,
|
||||||
|
chosen_profile=checkout_info.chosen_profile,
|
||||||
course_session=CourseSession.objects.get(
|
course_session=CourseSession.objects.get(
|
||||||
id=PRODUCT_SKU_TO_COURSE_SESSION_ID[checkout_info.product_sku]
|
id=PRODUCT_SKU_TO_COURSE_SESSION_ID[checkout_info.product_sku]
|
||||||
),
|
),
|
||||||
|
# chosen_profile=bla,
|
||||||
)
|
)
|
||||||
|
|
||||||
if created:
|
if created:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue