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": {
|
||||
"@graphql-codegen/cli": "^5.0.2",
|
||||
"@graphql-codegen/client-preset": "^4.3.2",
|
||||
"@parcel/watcher": "^2.4.1",
|
||||
"@rushstack/eslint-patch": "^1.8.0",
|
||||
"@savvywombat/tailwindcss-grid-areas": "^4.0.0",
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
|
|
@ -3013,6 +3014,7 @@
|
|||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.4.1.tgz",
|
||||
"integrity": "sha512-HNjmfLQEVRZmHRET336f20H/8kOozUGwk7yajvsonjNxbj2wBTK1WsQuHkD5yYh9RxFGL2EyDHryOihOwUoKDA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"detect-libc": "^1.0.3",
|
||||
"is-glob": "^4.0.3",
|
||||
|
|
@ -3048,6 +3050,7 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
|
|
@ -3067,6 +3070,7 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
|
|
@ -3086,6 +3090,7 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
|
|
@ -3105,6 +3110,7 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
|
|
@ -3124,6 +3130,7 @@
|
|||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
|
|
@ -3143,6 +3150,7 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
|
|
@ -3162,6 +3170,7 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
|
|
@ -3181,6 +3190,7 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
|
|
@ -3200,6 +3210,7 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
|
|
@ -3219,6 +3230,7 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
|
|
@ -3238,6 +3250,7 @@
|
|||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
|
|
@ -3257,6 +3270,7 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
|
|
@ -7246,6 +7260,7 @@
|
|||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
|
||||
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"detect-libc": "bin/detect-libc.js"
|
||||
},
|
||||
|
|
@ -10481,7 +10496,8 @@
|
|||
"node_modules/node-addon-api": {
|
||||
"version": "7.1.1",
|
||||
"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": {
|
||||
"version": "2.7.0",
|
||||
|
|
@ -16866,6 +16882,7 @@
|
|||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.4.1.tgz",
|
||||
"integrity": "sha512-HNjmfLQEVRZmHRET336f20H/8kOozUGwk7yajvsonjNxbj2wBTK1WsQuHkD5yYh9RxFGL2EyDHryOihOwUoKDA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@parcel/watcher-android-arm64": "2.4.1",
|
||||
"@parcel/watcher-darwin-arm64": "2.4.1",
|
||||
|
|
@ -16889,72 +16906,84 @@
|
|||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.4.1.tgz",
|
||||
"integrity": "sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@parcel/watcher-darwin-arm64": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.4.1.tgz",
|
||||
"integrity": "sha512-ln41eihm5YXIY043vBrrHfn94SIBlqOWmoROhsMVTSXGh0QahKGy77tfEywQ7v3NywyxBBkGIfrWRHm0hsKtzA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@parcel/watcher-darwin-x64": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.4.1.tgz",
|
||||
"integrity": "sha512-yrw81BRLjjtHyDu7J61oPuSoeYWR3lDElcPGJyOvIXmor6DEo7/G2u1o7I38cwlcoBHQFULqF6nesIX3tsEXMg==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@parcel/watcher-freebsd-x64": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.4.1.tgz",
|
||||
"integrity": "sha512-TJa3Pex/gX3CWIx/Co8k+ykNdDCLx+TuZj3f3h7eOjgpdKM+Mnix37RYsYU4LHhiYJz3DK5nFCCra81p6g050w==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@parcel/watcher-linux-arm-glibc": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.4.1.tgz",
|
||||
"integrity": "sha512-4rVYDlsMEYfa537BRXxJ5UF4ddNwnr2/1O4MHM5PjI9cvV2qymvhwZSFgXqbS8YoTk5i/JR0L0JDs69BUn45YA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@parcel/watcher-linux-arm64-glibc": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.4.1.tgz",
|
||||
"integrity": "sha512-BJ7mH985OADVLpbrzCLgrJ3TOpiZggE9FMblfO65PlOCdG++xJpKUJ0Aol74ZUIYfb8WsRlUdgrZxKkz3zXWYA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@parcel/watcher-linux-arm64-musl": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.4.1.tgz",
|
||||
"integrity": "sha512-p4Xb7JGq3MLgAfYhslU2SjoV9G0kI0Xry0kuxeG/41UfpjHGOhv7UoUDAz/jb1u2elbhazy4rRBL8PegPJFBhA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@parcel/watcher-linux-x64-glibc": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.4.1.tgz",
|
||||
"integrity": "sha512-s9O3fByZ/2pyYDPoLM6zt92yu6P4E39a03zvO0qCHOTjxmt3GHRMLuRZEWhWLASTMSrrnVNWdVI/+pUElJBBBg==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@parcel/watcher-linux-x64-musl": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.4.1.tgz",
|
||||
"integrity": "sha512-L2nZTYR1myLNST0O632g0Dx9LyMNHrn6TOt76sYxWLdff3cB22/GZX2UPtJnaqQPdCRoszoY5rcOj4oMTtp5fQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@parcel/watcher-win32-arm64": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.4.1.tgz",
|
||||
"integrity": "sha512-Uq2BPp5GWhrq/lcuItCHoqxjULU1QYEcyjSO5jqqOK8RNFDBQnenMMx4gAl3v8GiWa59E9+uDM7yZ6LxwUIfRg==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@parcel/watcher-win32-ia32": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.4.1.tgz",
|
||||
"integrity": "sha512-maNRit5QQV2kgHFSYwftmPBxiuK5u4DXjbXx7q6eKjq5dsLXZ4FJiVvlcw35QXzk0KrUecJmuVFbj4uV9oYrcw==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@parcel/watcher-win32-x64": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.4.1.tgz",
|
||||
"integrity": "sha512-+DvS92F9ezicfswqrvIRM2njcYJbd5mb9CUgtrHCHmvn7pPPa+nMDRu1o1bYYz/l5IB2NVGNJWiH7h1E58IF2A==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@peculiar/asn1-schema": {
|
||||
|
|
@ -19874,7 +19903,8 @@
|
|||
"detect-libc": {
|
||||
"version": "1.0.3",
|
||||
"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": {
|
||||
"version": "1.2.2",
|
||||
|
|
@ -22250,7 +22280,8 @@
|
|||
"node-addon-api": {
|
||||
"version": "7.1.1",
|
||||
"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": {
|
||||
"version": "2.7.0",
|
||||
|
|
|
|||
|
|
@ -10,8 +10,9 @@
|
|||
"cypress:open": "cypress open",
|
||||
"dev": "concurrently \"vite\" \"npm run codegen:watch\"",
|
||||
"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:check": "prettier . --check",
|
||||
"prettier:check": "prettier . --check --ignore-unknown",
|
||||
"tailwind": "tailwindcss -i tailwind.css -o ../server/vbv_lernwelt/static/css/tailwind.css --watch",
|
||||
"test": "vitest run",
|
||||
"typecheck": "npm run codegen && vue-tsc --noEmit -p tsconfig.app.json --composite false",
|
||||
|
|
@ -47,6 +48,7 @@
|
|||
"devDependencies": {
|
||||
"@graphql-codegen/cli": "^5.0.2",
|
||||
"@graphql-codegen/client-preset": "^4.3.2",
|
||||
"@parcel/watcher": "^2.4.1",
|
||||
"@rushstack/eslint-patch": "^1.8.0",
|
||||
"@savvywombat/tailwindcss-grid-areas": "^4.0.0",
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
<script setup lang="ts">
|
||||
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 { useCourseCircleProgress, useCourseDataWithCompletion } from "@/composables";
|
||||
import LoadingSpinner from "@/components/ui/LoadingSpinner.vue";
|
||||
|
|
@ -48,9 +52,13 @@ const wrapperClasses = computed(() => {
|
|||
return classes;
|
||||
});
|
||||
|
||||
const { inProgressCirclesCount, circlesCount } = useCourseCircleProgress(
|
||||
lpQueryResult.circles
|
||||
);
|
||||
const { filter } = useCourseFilter(props.courseSlug, props.courseSessionId);
|
||||
|
||||
const filteredCircles = computed(() => {
|
||||
return filterCircles(filter.value, circles.value);
|
||||
});
|
||||
const { inProgressCirclesCount, circlesCount } =
|
||||
useCourseCircleProgress(filteredCircles);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -66,7 +74,7 @@ const { inProgressCirclesCount, circlesCount } = useCourseCircleProgress(
|
|||
</h4>
|
||||
<div :class="wrapperClasses">
|
||||
<LearningPathCircle
|
||||
v-for="circle in circles"
|
||||
v-for="circle in filteredCircles"
|
||||
:key="circle.id"
|
||||
:sectors="calculateCircleSectorData(circle)"
|
||||
></LearningPathCircle>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,27 @@
|
|||
<script setup lang="ts">
|
||||
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<{
|
||||
step: number;
|
||||
}>();
|
||||
|
||||
const steps = computed(() => {
|
||||
const courseType = route.params.courseType;
|
||||
if (isString(courseType) && startsWith(courseType, "vv-")) {
|
||||
return 4;
|
||||
}
|
||||
return 3;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-screen flex-col">
|
||||
<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>
|
||||
</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
|
||||
interface Props {
|
||||
modelValue?: {
|
||||
id: string | number;
|
||||
name: string;
|
||||
};
|
||||
modelValue?: DropdownSelectable;
|
||||
items?: DropdownSelectable[];
|
||||
borderless?: boolean;
|
||||
placeholderText?: string | null;
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", data: object): void;
|
||||
(e: "update:modelValue", data: DropdownSelectable): void;
|
||||
}>();
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
|
|
|
|||
|
|
@ -3,3 +3,5 @@ export const itCheckboxDefaultIconCheckedTailwindClass =
|
|||
|
||||
export const itCheckboxDefaultIconUncheckedTailwindClass =
|
||||
"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!
|
||||
learning_path: LearningPathObjectType!
|
||||
action_competences: [ActionCompetenceObjectType!]!
|
||||
profiles: [String]
|
||||
course_session_users(id: String): [CourseSessionUserType]!
|
||||
chosen_profile(user: String!): String
|
||||
}
|
||||
|
||||
type ActionCompetenceObjectType implements CoursePageInterface {
|
||||
|
|
@ -331,45 +334,38 @@ type CircleLightObjectType {
|
|||
slug: String!
|
||||
}
|
||||
|
||||
type TopicObjectType implements CoursePageInterface {
|
||||
is_visible: Boolean!
|
||||
id: ID!
|
||||
title: String!
|
||||
slug: String!
|
||||
content_type: String!
|
||||
live: Boolean!
|
||||
translation_key: String!
|
||||
frontend_url: String!
|
||||
course: CourseObjectType
|
||||
circles: [CircleObjectType!]!
|
||||
type CourseSessionUserType {
|
||||
id: UUID!
|
||||
chosen_profile: String!
|
||||
course_session: CourseSessionObjectType!
|
||||
}
|
||||
|
||||
type CircleObjectType implements CoursePageInterface {
|
||||
description: String!
|
||||
goals: String!
|
||||
"""
|
||||
Leverages the internal Python implementation of UUID (uuid.UUID) to provide native UUID objects
|
||||
in fields, resolvers and input.
|
||||
"""
|
||||
scalar UUID
|
||||
|
||||
type CourseSessionObjectType {
|
||||
id: ID!
|
||||
created_at: DateTime!
|
||||
updated_at: DateTime!
|
||||
course: CourseObjectType!
|
||||
title: String!
|
||||
slug: String!
|
||||
content_type: String!
|
||||
live: Boolean!
|
||||
translation_key: String!
|
||||
frontend_url: String!
|
||||
course: CourseObjectType
|
||||
learning_sequences: [LearningSequenceObjectType!]!
|
||||
start_date: Date
|
||||
end_date: Date
|
||||
attendance_courses: [CourseSessionAttendanceCourseObjectType!]!
|
||||
assignments: [CourseSessionAssignmentObjectType!]!
|
||||
edoniq_tests: [CourseSessionEdoniqTestObjectType!]!
|
||||
users: [CourseSessionUserObjectsType!]!
|
||||
}
|
||||
|
||||
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!]!
|
||||
}
|
||||
"""
|
||||
The `Date` scalar type represents a Date
|
||||
value as specified by
|
||||
[iso8601](https://en.wikipedia.org/wiki/ISO_8601).
|
||||
"""
|
||||
scalar Date
|
||||
|
||||
type CourseSessionAttendanceCourseObjectType {
|
||||
id: ID!
|
||||
|
|
@ -431,26 +427,19 @@ type DueDateObjectType {
|
|||
course_session: CourseSessionObjectType!
|
||||
}
|
||||
|
||||
type CourseSessionObjectType {
|
||||
id: ID!
|
||||
created_at: DateTime!
|
||||
updated_at: DateTime!
|
||||
course: CourseObjectType!
|
||||
title: String!
|
||||
start_date: Date
|
||||
end_date: Date
|
||||
attendance_courses: [CourseSessionAttendanceCourseObjectType!]!
|
||||
assignments: [CourseSessionAssignmentObjectType!]!
|
||||
edoniq_tests: [CourseSessionEdoniqTestObjectType!]!
|
||||
users: [CourseSessionUserObjectsType!]!
|
||||
type AttendanceUserObjectType {
|
||||
user_id: UUID!
|
||||
status: AttendanceUserStatus!
|
||||
first_name: String
|
||||
last_name: String
|
||||
email: String
|
||||
}
|
||||
|
||||
"""
|
||||
The `Date` scalar type represents a Date
|
||||
value as specified by
|
||||
[iso8601](https://en.wikipedia.org/wiki/ISO_8601).
|
||||
"""
|
||||
scalar Date
|
||||
"""An enumeration."""
|
||||
enum AttendanceUserStatus {
|
||||
PRESENT
|
||||
ABSENT
|
||||
}
|
||||
|
||||
type CourseSessionAssignmentObjectType {
|
||||
id: ID!
|
||||
|
|
@ -578,12 +567,6 @@ type AssignmentCompletionObjectType {
|
|||
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 {
|
||||
"""
|
||||
Erforderlich. 150 Zeichen oder weniger. Nur Buchstaben, Ziffern und @/./+/-/_.
|
||||
|
|
@ -722,18 +705,46 @@ type CourseSessionUserExpertCircleType {
|
|||
slug: String!
|
||||
}
|
||||
|
||||
type AttendanceUserObjectType {
|
||||
user_id: UUID!
|
||||
status: AttendanceUserStatus!
|
||||
first_name: String
|
||||
last_name: String
|
||||
email: String
|
||||
type TopicObjectType implements CoursePageInterface {
|
||||
is_visible: Boolean!
|
||||
id: ID!
|
||||
title: String!
|
||||
slug: String!
|
||||
content_type: String!
|
||||
live: Boolean!
|
||||
translation_key: String!
|
||||
frontend_url: String!
|
||||
course: CourseObjectType
|
||||
circles: [CircleObjectType!]!
|
||||
}
|
||||
|
||||
"""An enumeration."""
|
||||
enum AttendanceUserStatus {
|
||||
PRESENT
|
||||
ABSENT
|
||||
type CircleObjectType implements CoursePageInterface {
|
||||
description: String!
|
||||
goals: String!
|
||||
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 {
|
||||
|
|
@ -896,6 +907,7 @@ type CompetenceCertificateListObjectType implements CoursePageInterface {
|
|||
type Mutation {
|
||||
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_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
|
||||
}
|
||||
|
||||
|
|
@ -926,6 +938,27 @@ input AttendanceUserInputType {
|
|||
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 {
|
||||
assignment_completion: AssignmentCompletionObjectType
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,8 +34,11 @@ export const CourseSessionAssignmentObjectType = "CourseSessionAssignmentObjectT
|
|||
export const CourseSessionAttendanceCourseObjectType = "CourseSessionAttendanceCourseObjectType";
|
||||
export const CourseSessionEdoniqTestObjectType = "CourseSessionEdoniqTestObjectType";
|
||||
export const CourseSessionObjectType = "CourseSessionObjectType";
|
||||
export const CourseSessionProfileMutationInput = "CourseSessionProfileMutationInput";
|
||||
export const CourseSessionProfileMutationPayload = "CourseSessionProfileMutationPayload";
|
||||
export const CourseSessionUserExpertCircleType = "CourseSessionUserExpertCircleType";
|
||||
export const CourseSessionUserObjectsType = "CourseSessionUserObjectsType";
|
||||
export const CourseSessionUserType = "CourseSessionUserType";
|
||||
export const CourseStatisticsType = "CourseStatisticsType";
|
||||
export const DashboardConfigType = "DashboardConfigType";
|
||||
export const DashboardType = "DashboardType";
|
||||
|
|
@ -84,4 +87,7 @@ export const StatisticsCourseSessionsSelectionMetricType = "StatisticsCourseSess
|
|||
export const String = "String";
|
||||
export const TopicObjectType = "TopicObjectType";
|
||||
export const UUID = "UUID";
|
||||
export const UpdateCourseProfileError = "UpdateCourseProfileError";
|
||||
export const UpdateCourseProfileResult = "UpdateCourseProfileResult";
|
||||
export const UpdateCourseProfileSuccess = "UpdateCourseProfileSuccess";
|
||||
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(`
|
||||
query courseQuery($slug: String!) {
|
||||
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
|
||||
}
|
||||
}
|
||||
configuration {
|
||||
id
|
||||
enable_circle_documents
|
||||
enable_learning_mentor
|
||||
enable_competence_certificates
|
||||
is_uk
|
||||
is_vv
|
||||
}
|
||||
action_competences {
|
||||
competence_id
|
||||
|
|
@ -298,6 +308,8 @@ export const COURSE_QUERY = graphql(`
|
|||
circles {
|
||||
description
|
||||
goals
|
||||
profiles
|
||||
is_base_circle
|
||||
...CoursePageFields
|
||||
learning_sequences {
|
||||
icon
|
||||
|
|
|
|||
|
|
@ -403,6 +403,13 @@ function log(data: any) {
|
|||
</button>
|
||||
</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>
|
||||
|
||||
<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">
|
||||
import LearningPathCircleListTile from "@/pages/learningPath/learningPathPage/LearningPathCircleListTile.vue";
|
||||
import { computed } from "vue";
|
||||
import type { LearningContentWithCompletion, LearningPathType } from "@/types";
|
||||
import { computed } from "vue";
|
||||
import LearningPathListTopic from "./LearningPathListTopic.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
learningPath: LearningPathType | undefined;
|
||||
nextLearningContent: LearningContentWithCompletion | undefined;
|
||||
filter?: string;
|
||||
}>();
|
||||
|
||||
const topics = computed(() => props.learningPath?.topics ?? []);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-for="topic in topics" :key="topic.title">
|
||||
<div class="pb-2 font-bold text-gray-700">
|
||||
{{ topic.title }}
|
||||
</div>
|
||||
<LearningPathCircleListTile
|
||||
v-for="circle in topic.circles"
|
||||
:key="circle.id"
|
||||
:circle="circle"
|
||||
:next-learning-content="props.nextLearningContent"
|
||||
></LearningPathCircleListTile>
|
||||
</div>
|
||||
<LearningPathListTopic
|
||||
v-for="topic in topics"
|
||||
:key="topic.title"
|
||||
:topic="topic"
|
||||
:next-learning-content="nextLearningContent"
|
||||
:filter="filter"
|
||||
/>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import LearningPathListView from "@/pages/learningPath/learningPathPage/Learning
|
|||
import LearningPathPathView from "@/pages/learningPath/learningPathPage/LearningPathPathView.vue";
|
||||
import CircleProgress from "@/pages/learningPath/learningPathPage/LearningPathProgress.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 LearningPathViewSwitch from "@/pages/learningPath/learningPathPage/LearningPathViewSwitch.vue";
|
||||
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core";
|
||||
|
|
@ -13,6 +14,9 @@ import {
|
|||
useCurrentCourseSession,
|
||||
} from "@/composables";
|
||||
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<{
|
||||
courseSlug: string;
|
||||
|
|
@ -33,9 +37,28 @@ const course = computed(() => lpQueryResult.course.value);
|
|||
|
||||
const courseSession = useCurrentCourseSession();
|
||||
|
||||
const { inProgressCirclesCount, circlesCount } = useCourseCircleProgress(
|
||||
lpQueryResult.circles
|
||||
);
|
||||
const { filter } = useCourseFilter(props.courseSlug);
|
||||
|
||||
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) => {
|
||||
selectedView.value = viewType;
|
||||
|
|
@ -46,7 +69,9 @@ const changeViewType = (viewType: ViewType) => {
|
|||
<template>
|
||||
<div class="flex flex-col">
|
||||
<!-- 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 -->
|
||||
<div class="flex flex-col justify-between lg:w-1/2">
|
||||
<div>
|
||||
|
|
@ -64,15 +89,21 @@ const changeViewType = (viewType: ViewType) => {
|
|||
></CircleProgress>
|
||||
</div>
|
||||
|
||||
<!-- Right -->
|
||||
<div v-if="!useMobileLayout" class="flex-grow">
|
||||
<!-- todo: find out when to display CourseSessionDueDatesList -->
|
||||
<div v-if="!useMobileLayout && false" class="flex-grow">
|
||||
<CourseSessionDueDatesList
|
||||
:course-session-id="courseSession.id"
|
||||
:max-count="2"
|
||||
></CourseSessionDueDatesList>
|
||||
</div>
|
||||
<!-- Right -->
|
||||
<LearningPathProfileFilter
|
||||
v-if="course?.configuration.is_vv"
|
||||
:profiles="course?.profiles"
|
||||
:selected="filter"
|
||||
@select="updateCourseProfile"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Bottom -->
|
||||
<div class="bg-white">
|
||||
<div v-if="lpQueryResult.learningPath">
|
||||
|
|
@ -101,6 +132,7 @@ const changeViewType = (viewType: ViewType) => {
|
|||
<LearningPathPathView
|
||||
:learning-path="learningPath"
|
||||
:use-mobile-layout="useMobileLayout"
|
||||
:filter="filter"
|
||||
:next-learning-content="lpQueryResult.nextLearningContent.value"
|
||||
></LearningPathPathView>
|
||||
</div>
|
||||
|
|
@ -114,6 +146,7 @@ const changeViewType = (viewType: ViewType) => {
|
|||
<LearningPathListView
|
||||
:learning-path="learningPath"
|
||||
:next-learning-content="lpQueryResult.nextLearningContent.value"
|
||||
:filter="filter"
|
||||
></LearningPathListView>
|
||||
</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">
|
||||
import LearningPathCircleColumn from "@/pages/learningPath/learningPathPage/LearningPathCircleColumn.vue";
|
||||
import LearningPathScrollButton from "@/pages/learningPath/learningPathPage/LearningPathScrollButton.vue";
|
||||
import { useScroll } from "@vueuse/core";
|
||||
import { ref } from "vue";
|
||||
import { computed, nextTick, ref, watch } from "vue";
|
||||
import type { LearningContentWithCompletion, LearningPathType } from "@/types";
|
||||
import LoadingSpinner from "@/components/ui/LoadingSpinner.vue";
|
||||
import LearningPathPathTopic from "./LearningPathPathTopic.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
learningPath: LearningPathType | undefined;
|
||||
|
|
@ -12,6 +12,7 @@ const props = defineProps<{
|
|||
useMobileLayout: boolean;
|
||||
hideButtons?: boolean;
|
||||
overrideCircleUrlBase?: string;
|
||||
filter?: string;
|
||||
}>();
|
||||
|
||||
const scrollIncrement = 600;
|
||||
|
|
@ -19,13 +20,6 @@ const scrollIncrement = 600;
|
|||
const learnPathDiagram = ref<HTMLElement | null>(null);
|
||||
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 scrollLeft = () => scrollLearnPathDiagram(-scrollIncrement);
|
||||
|
|
@ -33,6 +27,22 @@ const scrollLeft = () => scrollLearnPathDiagram(-scrollIncrement);
|
|||
const scrollLearnPathDiagram = (offset: number) => {
|
||||
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>
|
||||
|
||||
<template>
|
||||
|
|
@ -50,37 +60,16 @@ const scrollLearnPathDiagram = (offset: number) => {
|
|||
ref="learnPathDiagram"
|
||||
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 ?? []"
|
||||
:key="topic.title"
|
||||
class="border-l border-gray-500"
|
||||
:class="topicIndex == 0 ? 'ml-6 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 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>
|
||||
:topic-index="topicIndex"
|
||||
:topic="topic"
|
||||
:next-learning-content="nextLearningContent"
|
||||
:override-circle-url-base="overrideCircleUrlBase"
|
||||
:filter="filter"
|
||||
:is-last-topic="topicIndex === topics.length - 1"
|
||||
/>
|
||||
</div>
|
||||
<LearningPathScrollButton
|
||||
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 {
|
||||
CircleSectorData,
|
||||
CircleSectorProgress,
|
||||
|
|
@ -7,6 +10,8 @@ import {
|
|||
someFinishedInLearningSequence,
|
||||
} from "@/services/circle";
|
||||
import type { CircleType } from "@/types";
|
||||
import { useQuery } from "@urql/vue";
|
||||
import { computed } from "vue";
|
||||
|
||||
export function calculateCircleSectorData(circle: CircleType): CircleSectorData[] {
|
||||
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,
|
||||
address: addressData,
|
||||
product: props.courseType,
|
||||
chosen_profile: user.chosen_profile?.id || "",
|
||||
with_cembra_byjuno_invoice: address.value.payment_method === "cembra_byjuno",
|
||||
device_fingerprint_session_key: getLocalSessionKey(),
|
||||
}).then((res: any) => {
|
||||
|
|
|
|||
|
|
@ -47,6 +47,14 @@ const { t } = useTranslation();
|
|||
$t("Füge dein Profilbild hinzu und ergänze die fehlenden Angaben.")
|
||||
}}
|
||||
</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">
|
||||
<span class="font-bold">{{ $t("a.Lehrgang kaufen") }}:</span>
|
||||
{{
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
<script setup lang="ts">
|
||||
import { calculateCircleSectorData } from "@/pages/learningPath/learningPathPage/utils";
|
||||
import { useCourseDataWithCompletion } from "@/composables";
|
||||
import UserProfileContent from "@/components/userProfile/UserProfileContent.vue";
|
||||
import LearningPathCircle from "@/pages/learningPath/learningPathPage/LearningPathCircle.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 { COURSE_QUERY } from "@/graphql/queries";
|
||||
import { useQuery } from "@urql/vue";
|
||||
import UserProfileTopicList from "./UserProfileTopicList.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
userId: string;
|
||||
|
|
@ -20,6 +21,23 @@ function selectCircle(circle: CircleType) {
|
|||
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, () => {
|
||||
if (lpQueryResult.learningPath?.value?.topics?.length) {
|
||||
selectCircle(lpQueryResult.learningPath.value.topics[0].circles[0]);
|
||||
|
|
@ -30,28 +48,21 @@ watch(lpQueryResult.learningPath, () => {
|
|||
<template>
|
||||
<UserProfileContent>
|
||||
<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 ?? []"
|
||||
:key="topic.id"
|
||||
class="mb-4"
|
||||
>
|
||||
<h4 class="mb-1 font-semibold text-gray-800">
|
||||
{{ topic.title }}
|
||||
</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>
|
||||
:topic="topic"
|
||||
:filter="chosenProfile"
|
||||
:selected-circle="selectedCircle"
|
||||
@select-circle="selectCircle($event)"
|
||||
/>
|
||||
</template>
|
||||
<template #main>
|
||||
<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
|
||||
);
|
||||
expect(mockNext).toHaveBeenCalledWith({
|
||||
name: "checkoutAddress",
|
||||
name: "accountCourseProfile",
|
||||
params: { courseType: testCase },
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -391,6 +391,12 @@ const router = createRouter({
|
|||
component: () => import("@/pages/onboarding/uk/SetupComplete.vue"),
|
||||
name: "setupComplete",
|
||||
},
|
||||
{
|
||||
path: "account/course-profile",
|
||||
component: () => import("@/pages/onboarding/vv/AccountCourseProfile.vue"),
|
||||
name: "accountCourseProfile",
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "checkout/address",
|
||||
component: () => import("@/pages/onboarding/vv/CheckoutAddress.vue"),
|
||||
|
|
|
|||
|
|
@ -13,14 +13,21 @@ export type Country = {
|
|||
name: string;
|
||||
};
|
||||
|
||||
export type CourseProfile = {
|
||||
id: number;
|
||||
code: string;
|
||||
};
|
||||
|
||||
export function useEntities() {
|
||||
const countries: Ref<Country[]> = ref([]);
|
||||
const organisations: Ref<Organisation[]> = ref([]);
|
||||
const courseProfiles: Ref<CourseProfile[]> = ref([]);
|
||||
|
||||
itGetCached("/api/core/entities/").then((res: any) => {
|
||||
countries.value = res.countries;
|
||||
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
|
||||
if (isString(courseType) && startsWith(courseType, "vv-")) {
|
||||
return "checkoutAddress";
|
||||
return "accountCourseProfile";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { bustItGetCache, itGetCached, itPost } from "@/fetchHelpers";
|
||||
import { setI18nLanguage } from "@/i18nextWrapper";
|
||||
import type { Country } from "@/services/entities";
|
||||
import type { Country, CourseProfile } from "@/services/entities";
|
||||
import { directUpload } from "@/services/files";
|
||||
import dayjs from "dayjs";
|
||||
import { defineStore } from "pinia";
|
||||
|
|
@ -44,6 +44,7 @@ export interface User {
|
|||
organisation_postal_code: string;
|
||||
organisation_city: string;
|
||||
organisation_country: Country | null;
|
||||
chosen_profile?: CourseProfile;
|
||||
}
|
||||
|
||||
let defaultLanguage: AvailableLanguages = "de";
|
||||
|
|
@ -89,6 +90,7 @@ const initialUserState: User = {
|
|||
organisation_postal_code: "",
|
||||
organisation_city: "",
|
||||
organisation_country: null,
|
||||
chosen_profile: undefined,
|
||||
};
|
||||
|
||||
async function setLocale(language: AvailableLanguages) {
|
||||
|
|
@ -176,5 +178,8 @@ export const useUserStore = defineStore({
|
|||
await itPost("/api/core/me/", profileData, { method: "PUT" });
|
||||
Object.assign(this.$state, profileData);
|
||||
},
|
||||
updateChosenCourseProfile(courseProfile: CourseProfile) {
|
||||
Object.assign(this.$state, { chosen_profile: courseProfile });
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -202,6 +202,7 @@ export interface Course {
|
|||
title: string;
|
||||
category_name: string;
|
||||
slug: string;
|
||||
profiles: string[];
|
||||
configuration: CourseConfiguration;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ textarea {
|
|||
}
|
||||
|
||||
.link {
|
||||
@apply underline underline-offset-2;
|
||||
@apply cursor-pointer underline underline-offset-2;
|
||||
}
|
||||
|
||||
.link-large {
|
||||
|
|
@ -167,6 +167,14 @@ textarea {
|
|||
top: 1rem;
|
||||
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 {
|
||||
|
|
@ -181,7 +189,9 @@ textarea {
|
|||
}
|
||||
|
||||
.no-scrollbar {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none;
|
||||
/* IE and Edge */
|
||||
scrollbar-width: none;
|
||||
/* Firefox */
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,22 @@
|
|||
// ids for cypress test data
|
||||
export const ADMIN_USER_ID = "872efd96-3bd7-4a1e-a239-2d72cad9f604"
|
||||
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_TRAINER2_USER_ID = "299941ae-1e4b-4f45-8180-876c3ad340b4"
|
||||
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_STUDENT3_USER_ID = "bcf94dba-53bc-474b-a22d-e4af39aa042b"
|
||||
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_STUDENT2_VV_AND_VV_MENTOR_USER_ID = "7e8ebf0b-e6e2-4022-88f4-6e663ba0a9db"
|
||||
export const TEST_USER_EMPTY_ID = "daecbabe-4ab9-4edf-a71f-4119042ccb02"
|
||||
|
||||
export const ADMIN_USER_ID = "872efd96-3bd7-4a1e-a239-2d72cad9f604";
|
||||
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_TRAINER2_USER_ID = "299941ae-1e4b-4f45-8180-876c3ad340b4";
|
||||
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_STUDENT3_USER_ID = "bcf94dba-53bc-474b-a22d-e4af39aa042b";
|
||||
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_STUDENT2_VV_AND_VV_MENTOR_USER_ID =
|
||||
"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_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";
|
||||
|
||||
describe("checkout.cy.js", () => {
|
||||
|
|
@ -32,6 +37,15 @@ describe("checkout.cy.js", () => {
|
|||
cy.get("#organisationDetailName").type("FdH GmbH");
|
||||
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) => {
|
||||
expect(u.organisation_detail_name).to.equal("FdH GmbH");
|
||||
// 2 -> andere Krankenversicherer
|
||||
|
|
@ -121,6 +135,12 @@ describe("checkout.cy.js", () => {
|
|||
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_NICHTLEBEN_ID);
|
||||
});
|
||||
});
|
||||
|
||||
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="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
|
||||
cy.get('[data-cy="account-checkout-title"]').should(
|
||||
"contain",
|
||||
|
|
@ -236,5 +265,32 @@ describe("checkout.cy.js", () => {
|
|||
// 7 -> Baloise
|
||||
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) => {
|
||||
for (let i = 0; i < answers.length; i++) {
|
||||
const answer = answers[i]
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ echo 'prettier:check'
|
|||
(cd client && npm run prettier:check)
|
||||
|
||||
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'
|
||||
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.serializers import CountrySerializer, OrganisationSerializer
|
||||
from vbv_lernwelt.learnpath.models import CourseProfile
|
||||
from vbv_lernwelt.learnpath.serializers import CourseProfileSerializer
|
||||
|
||||
|
||||
@api_view(["GET"])
|
||||
|
|
@ -26,4 +28,13 @@ def list_entities(request):
|
|||
countries = CountrySerializer(
|
||||
Country.objects.all(), many=True, context=context
|
||||
).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.generics import get_object_or_404
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ from graphql import GraphQLError
|
|||
from rest_framework.exceptions import PermissionDenied
|
||||
|
||||
from vbv_lernwelt.competence.graphql.types import ActionCompetenceObjectType
|
||||
from vbv_lernwelt.core.models import User
|
||||
from vbv_lernwelt.course.models import (
|
||||
CircleDocument,
|
||||
Course,
|
||||
|
|
@ -29,8 +30,9 @@ from vbv_lernwelt.course_session.models import (
|
|||
)
|
||||
from vbv_lernwelt.course_session_group.models import CourseSessionGroup
|
||||
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.models import Circle
|
||||
from vbv_lernwelt.learnpath.models import Circle, CourseProfile
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
|
@ -106,6 +108,12 @@ class CourseObjectType(DjangoObjectType):
|
|||
graphene.NonNull(ActionCompetenceObjectType), 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:
|
||||
model = Course
|
||||
|
|
@ -125,6 +133,22 @@ class CourseObjectType(DjangoObjectType):
|
|||
def resolve_action_competences(root: Course, info):
|
||||
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):
|
||||
id = graphene.ID(required=True)
|
||||
|
|
@ -132,6 +156,21 @@ class CourseSessionUserExpertCircleType(ObjectType):
|
|||
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):
|
||||
"""
|
||||
WORKAROUND:
|
||||
|
|
|
|||
|
|
@ -101,6 +101,7 @@ from vbv_lernwelt.learnpath.create_vv_new_learning_path import (
|
|||
create_vv_new_learning_path,
|
||||
create_vv_pruefung_learning_path,
|
||||
)
|
||||
from vbv_lernwelt.learnpath.creators import assign_circles_to_profiles
|
||||
from vbv_lernwelt.learnpath.models import (
|
||||
Circle,
|
||||
LearningContent,
|
||||
|
|
@ -222,6 +223,7 @@ def create_versicherungsvermittlerin_course(
|
|||
create_vv_gewinnen_casework(course_id=course_id)
|
||||
create_vv_reflection(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])
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
chosen_profile = models.ForeignKey(
|
||||
"learnpath.CourseProfile", on_delete=models.SET_NULL, blank=True, null=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
constraints = [
|
||||
UniqueConstraint(
|
||||
|
|
|
|||
|
|
@ -8,8 +8,10 @@ from vbv_lernwelt.course.models import (
|
|||
CourseCompletion,
|
||||
CourseConfiguration,
|
||||
CourseSession,
|
||||
CourseSessionUser,
|
||||
)
|
||||
from vbv_lernwelt.iam.permissions import course_session_permissions
|
||||
from vbv_lernwelt.learnpath.models import CourseProfile
|
||||
|
||||
|
||||
class CourseConfigurationSerializer(serializers.ModelSerializer):
|
||||
|
|
@ -31,10 +33,23 @@ class CourseSerializer(serializers.ModelSerializer):
|
|||
configuration = CourseConfigurationSerializer(
|
||||
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:
|
||||
model = Course
|
||||
fields = ["id", "title", "category_name", "slug", "configuration"]
|
||||
fields = [
|
||||
"id",
|
||||
"title",
|
||||
"category_name",
|
||||
"slug",
|
||||
"configuration",
|
||||
"course_profiles",
|
||||
]
|
||||
|
||||
|
||||
class CourseCategorySerializer(serializers.ModelSerializer):
|
||||
|
|
@ -103,6 +118,12 @@ class CourseSessionSerializer(serializers.ModelSerializer):
|
|||
return []
|
||||
|
||||
|
||||
class CypressCourseSessionUserSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = CourseSessionUser
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class CircleDocumentSerializer(serializers.ModelSerializer):
|
||||
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 structlog
|
||||
from graphene import relay
|
||||
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 (
|
||||
CourseSessionAttendanceCourseObjectType,
|
||||
)
|
||||
|
|
@ -11,10 +14,33 @@ from vbv_lernwelt.course_session.services.attendance import (
|
|||
update_attendance_list,
|
||||
)
|
||||
from vbv_lernwelt.iam.permissions import has_course_access
|
||||
from vbv_lernwelt.learnpath.models import CourseProfile
|
||||
|
||||
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):
|
||||
user_id = graphene.UUID(required=True)
|
||||
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:
|
||||
update_course_session_attendance_course_users = AttendanceCourseUserMutation.Field()
|
||||
update_course_session_profile = CourseSessionProfileMutation.Field()
|
||||
|
|
|
|||
|
|
@ -1,3 +1,8 @@
|
|||
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 structlog
|
||||
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.learnpath.models import (
|
||||
Circle,
|
||||
CourseProfile,
|
||||
LearningContentAssignment,
|
||||
LearningContentAttendanceCourse,
|
||||
LearningContentDocumentList,
|
||||
|
|
@ -299,14 +302,12 @@ class CircleObjectType(DjangoObjectType):
|
|||
learning_sequences = graphene.List(
|
||||
graphene.NonNull(LearningSequenceObjectType), required=True
|
||||
)
|
||||
profiles = graphene.List(graphene.String, required=True)
|
||||
|
||||
class Meta:
|
||||
model = Circle
|
||||
interfaces = (CoursePageInterface,)
|
||||
fields = [
|
||||
"description",
|
||||
"goals",
|
||||
]
|
||||
fields = ["description", "goals", "is_base_circle"]
|
||||
|
||||
def resolve_learning_sequences(self: Circle, info, **kwargs):
|
||||
circle_descendants = None
|
||||
|
|
@ -335,6 +336,10 @@ class CircleObjectType(DjangoObjectType):
|
|||
if descendant.specific_class == LearningSequence
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def resolve_profiles(root: Circle, info, **kwargs):
|
||||
return root.profiles.all()
|
||||
|
||||
|
||||
class TopicObjectType(DjangoObjectType):
|
||||
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.utils.text import slugify
|
||||
from modelcluster.models import ParentalManyToManyField
|
||||
from wagtail.admin.panels import FieldPanel, PageChooserPanel
|
||||
from wagtail.fields import RichTextField, StreamField
|
||||
from wagtail.models import Page
|
||||
|
|
@ -66,6 +67,25 @@ class Topic(CourseBasePage):
|
|||
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):
|
||||
parent_page_types = ["learnpath.LearningPath"]
|
||||
subpage_types = [
|
||||
|
|
@ -95,9 +115,25 @@ class Circle(CourseBasePage):
|
|||
|
||||
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 + [
|
||||
FieldPanel("description"),
|
||||
FieldPanel("goals"),
|
||||
FieldPanel("is_base_circle"),
|
||||
FieldPanel("profiles"),
|
||||
]
|
||||
|
||||
def get_frontend_url(self):
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from rest_framework import serializers
|
||||
from rest_framework.fields import SerializerMethodField
|
||||
|
||||
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.course.serializer_helpers import get_course_serializer_class
|
||||
from vbv_lernwelt.learnpath.models import (
|
||||
CourseProfile,
|
||||
LearningContentAssignment,
|
||||
LearningContentEdoniqTest,
|
||||
LearningUnit,
|
||||
|
|
@ -98,3 +100,9 @@ class LearningContentAssignmentSerializer(
|
|||
}
|
||||
except Exception:
|
||||
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,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
chosen_profile = models.ForeignKey(
|
||||
"learnpath.CourseProfile", on_delete=models.SET_NULL, null=True, blank=True
|
||||
)
|
||||
# webhook metadata
|
||||
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.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.models import CheckoutInformation, CheckoutState, Product
|
||||
from vbv_lernwelt.shop.services import InitTransactionException
|
||||
|
|
@ -50,6 +52,10 @@ class CheckoutAPITestCase(APITestCase):
|
|||
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)
|
||||
add_countries(small_set=True)
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ from rest_framework.permissions import IsAuthenticated
|
|||
from sentry_sdk import capture_exception
|
||||
|
||||
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.shop.const import (
|
||||
VV_DE_PRODUCT_SKU,
|
||||
|
|
@ -92,6 +94,7 @@ def checkout_vv(request):
|
|||
|
||||
sku = request.data["product"]
|
||||
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)
|
||||
|
||||
|
|
@ -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(
|
||||
user=request.user,
|
||||
product_sku=sku,
|
||||
|
|
@ -151,6 +159,7 @@ def checkout_vv(request):
|
|||
"device_fingerprint_session_key", ""
|
||||
),
|
||||
# address
|
||||
chosen_profile=chosen_profile,
|
||||
**request.data["address"],
|
||||
)
|
||||
|
||||
|
|
@ -257,9 +266,11 @@ def create_vv_course_session_user(checkout_info: CheckoutInformation):
|
|||
_, created = CourseSessionUser.objects.get_or_create(
|
||||
user=checkout_info.user,
|
||||
role=CourseSessionUser.Role.MEMBER,
|
||||
chosen_profile=checkout_info.chosen_profile,
|
||||
course_session=CourseSession.objects.get(
|
||||
id=PRODUCT_SKU_TO_COURSE_SESSION_ID[checkout_info.product_sku]
|
||||
),
|
||||
# chosen_profile=bla,
|
||||
)
|
||||
|
||||
if created:
|
||||
|
|
|
|||
Loading…
Reference in New Issue