Merge branch 'develop' into feature/vbv-676-berufsbildner-2

# Conflicts:
#	client/src/composables.ts
#	client/src/gql/gql.ts
#	client/src/gql/graphql.ts
#	client/src/graphql/queries.ts
#	client/src/pages/competence/CompetenceCertificateDetailPage.vue
#	client/src/pages/competence/CompetenceCertificateListPage.vue
#	client/src/pages/competence/CompetenceIndexPage.vue
#	client/src/types.ts
#	cypress/support/commands.js
#	server/vbv_lernwelt/shop/migrations/0016_alter_checkoutinformation_refno2.py
This commit is contained in:
Christian Cueni 2024-08-09 18:01:25 +02:00
commit aca066a376
74 changed files with 2188 additions and 449 deletions

View File

@ -10,6 +10,7 @@
"dependencies": { "dependencies": {
"@headlessui/tailwindcss": "^0.2.1", "@headlessui/tailwindcss": "^0.2.1",
"@headlessui/vue": "^1.7.22", "@headlessui/vue": "^1.7.22",
"@parcel/watcher": "^2.4.1",
"@sentry/tracing": "^7.114.0", "@sentry/tracing": "^7.114.0",
"@sentry/vue": "^8.17.0", "@sentry/vue": "^8.17.0",
"@urql/exchange-graphcache": "^7.1.2", "@urql/exchange-graphcache": "^7.1.2",
@ -5722,7 +5723,6 @@
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"dev": true,
"dependencies": { "dependencies": {
"fill-range": "^7.0.1" "fill-range": "^7.0.1"
}, },
@ -8258,7 +8258,6 @@
"version": "7.0.1", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"dev": true,
"dependencies": { "dependencies": {
"to-regex-range": "^5.0.1" "to-regex-range": "^5.0.1"
}, },
@ -9317,7 +9316,6 @@
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@ -9334,7 +9332,6 @@
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"dependencies": { "dependencies": {
"is-extglob": "^2.1.1" "is-extglob": "^2.1.1"
}, },
@ -9388,7 +9385,6 @@
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
"engines": { "engines": {
"node": ">=0.12.0" "node": ">=0.12.0"
} }
@ -10334,7 +10330,6 @@
"version": "4.0.5", "version": "4.0.5",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
"integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
"dev": true,
"dependencies": { "dependencies": {
"braces": "^3.0.2", "braces": "^3.0.2",
"picomatch": "^2.3.1" "picomatch": "^2.3.1"
@ -11072,7 +11067,6 @@
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"engines": { "engines": {
"node": ">=8.6" "node": ">=8.6"
}, },
@ -12672,7 +12666,6 @@
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"dependencies": { "dependencies": {
"is-number": "^7.0.0" "is-number": "^7.0.0"
}, },
@ -18806,7 +18799,6 @@
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"dev": true,
"requires": { "requires": {
"fill-range": "^7.0.1" "fill-range": "^7.0.1"
} }
@ -20667,7 +20659,6 @@
"version": "7.0.1", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"dev": true,
"requires": { "requires": {
"to-regex-range": "^5.0.1" "to-regex-range": "^5.0.1"
} }
@ -21425,8 +21416,7 @@
"is-extglob": { "is-extglob": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="
"dev": true
}, },
"is-fullwidth-code-point": { "is-fullwidth-code-point": {
"version": "3.0.0", "version": "3.0.0",
@ -21437,7 +21427,6 @@
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"requires": { "requires": {
"is-extglob": "^2.1.1" "is-extglob": "^2.1.1"
} }
@ -21475,8 +21464,7 @@
"is-number": { "is-number": {
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
"dev": true
}, },
"is-number-object": { "is-number-object": {
"version": "1.0.7", "version": "1.0.7",
@ -22174,7 +22162,6 @@
"version": "4.0.5", "version": "4.0.5",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
"integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
"dev": true,
"requires": { "requires": {
"braces": "^3.0.2", "braces": "^3.0.2",
"picomatch": "^2.3.1" "picomatch": "^2.3.1"
@ -22727,8 +22714,7 @@
"picomatch": { "picomatch": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="
"dev": true
}, },
"pify": { "pify": {
"version": "2.3.0", "version": "2.3.0",
@ -23824,7 +23810,6 @@
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"requires": { "requires": {
"is-number": "^7.0.0" "is-number": "^7.0.0"
} }

View File

@ -10,8 +10,9 @@
"cypress:open": "cypress open", "cypress:open": "cypress open",
"dev": "concurrently \"vite\" \"npm run codegen:watch\"", "dev": "concurrently \"vite\" \"npm run codegen:watch\"",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore", "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
"lint:errors": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --quiet --ignore-path .gitignore",
"prettier": "prettier . --write", "prettier": "prettier . --write",
"prettier:check": "prettier . --check", "prettier:check": "prettier . --check --ignore-unknown",
"tailwind": "tailwindcss -i tailwind.css -o ../server/vbv_lernwelt/static/css/tailwind.css --watch", "tailwind": "tailwindcss -i tailwind.css -o ../server/vbv_lernwelt/static/css/tailwind.css --watch",
"test": "vitest run", "test": "vitest run",
"typecheck": "npm run codegen && vue-tsc --noEmit -p tsconfig.app.json --composite false", "typecheck": "npm run codegen && vue-tsc --noEmit -p tsconfig.app.json --composite false",
@ -20,6 +21,7 @@
"dependencies": { "dependencies": {
"@headlessui/tailwindcss": "^0.2.1", "@headlessui/tailwindcss": "^0.2.1",
"@headlessui/vue": "^1.7.22", "@headlessui/vue": "^1.7.22",
"@parcel/watcher": "^2.4.1",
"@sentry/tracing": "^7.114.0", "@sentry/tracing": "^7.114.0",
"@sentry/vue": "^8.17.0", "@sentry/vue": "^8.17.0",
"@urql/exchange-graphcache": "^7.1.2", "@urql/exchange-graphcache": "^7.1.2",

View File

@ -342,7 +342,7 @@ const hasSessionTitle = computed(() => {
v-if="hasSessionTitle" v-if="hasSessionTitle"
class="nav-item hidden items-center lg:inline-flex" class="nav-item hidden items-center lg:inline-flex"
> >
<div class=""> <div class="" data-cy="current-course-session-title">
{{ selectedCourseSessionTitle }} {{ selectedCourseSessionTitle }}
</div> </div>
</div> </div>

View File

@ -1,6 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import LearningPathCircle from "@/pages/learningPath/learningPathPage/LearningPathCircle.vue"; import LearningPathCircle from "@/pages/learningPath/learningPathPage/LearningPathCircle.vue";
import { calculateCircleSectorData } from "@/pages/learningPath/learningPathPage/utils"; import {
calculateCircleSectorData,
filterCircles,
useCourseFilter,
} from "@/pages/learningPath/learningPathPage/utils";
import { computed } from "vue"; import { computed } from "vue";
import { useCourseCircleProgress, useCourseDataWithCompletion } from "@/composables"; import { useCourseCircleProgress, useCourseDataWithCompletion } from "@/composables";
import LoadingSpinner from "@/components/ui/LoadingSpinner.vue"; import LoadingSpinner from "@/components/ui/LoadingSpinner.vue";
@ -48,9 +52,13 @@ const wrapperClasses = computed(() => {
return classes; return classes;
}); });
const { inProgressCirclesCount, circlesCount } = useCourseCircleProgress( const { filter } = useCourseFilter(props.courseSlug, props.courseSessionId);
lpQueryResult.circles
); const filteredCircles = computed(() => {
return filterCircles(filter.value, circles.value);
});
const { inProgressCirclesCount, circlesCount } =
useCourseCircleProgress(filteredCircles);
</script> </script>
<template> <template>
@ -66,7 +74,7 @@ const { inProgressCirclesCount, circlesCount } = useCourseCircleProgress(
</h4> </h4>
<div :class="wrapperClasses"> <div :class="wrapperClasses">
<LearningPathCircle <LearningPathCircle
v-for="circle in circles" v-for="circle in filteredCircles"
:key="circle.id" :key="circle.id"
:sectors="calculateCircleSectorData(circle)" :sectors="calculateCircleSectorData(circle)"
></LearningPathCircle> ></LearningPathCircle>

View File

@ -1,15 +1,27 @@
<script setup lang="ts"> <script setup lang="ts">
import ItNavigationProgress from "@/components/ui/ItNavigationProgress.vue"; import ItNavigationProgress from "@/components/ui/ItNavigationProgress.vue";
import { computed } from "vue";
import { useRoute } from "vue-router";
import { isString, startsWith } from "lodash";
const route = useRoute();
const props = defineProps<{ const props = defineProps<{
step: number; step: number;
}>(); }>();
const steps = computed(() => {
const courseType = route.params.courseType;
if (isString(courseType) && startsWith(courseType, "vv-")) {
return 4;
}
return 3;
});
</script> </script>
<template> <template>
<div class="flex h-screen flex-col"> <div class="flex h-screen flex-col">
<div class="flex-grow scroll-smooth p-16 lg:overflow-auto"> <div class="flex-grow scroll-smooth p-16 lg:overflow-auto">
<ItNavigationProgress :steps="3" :current-step="props.step" /> <ItNavigationProgress :steps="steps" :current-step="props.step" />
<slot name="content"></slot> <slot name="content"></slot>
</div> </div>

View File

@ -5,17 +5,14 @@ import { computed } from "vue"; // https://stackoverflow.com/questions/64775876/
// https://stackoverflow.com/questions/64775876/vue-3-pass-reactive-object-to-component-with-two-way-binding // https://stackoverflow.com/questions/64775876/vue-3-pass-reactive-object-to-component-with-two-way-binding
interface Props { interface Props {
modelValue?: { modelValue?: DropdownSelectable;
id: string | number;
name: string;
};
items?: DropdownSelectable[]; items?: DropdownSelectable[];
borderless?: boolean; borderless?: boolean;
placeholderText?: string | null; placeholderText?: string | null;
} }
const emit = defineEmits<{ const emit = defineEmits<{
(e: "update:modelValue", data: object): void; (e: "update:modelValue", data: DropdownSelectable): void;
}>(); }>();
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {

View File

@ -1,33 +1,30 @@
import { useCSRFFetch } from "@/fetchHelpers"; import {useCSRFFetch} from "@/fetchHelpers";
import type { CourseStatisticsType } from "@/gql/graphql"; import type {CourseStatisticsType} from "@/gql/graphql";
import { graphqlClient } from "@/graphql/client"; import {graphqlClient} from "@/graphql/client";
import { COURSE_QUERY, COURSE_SESSION_DETAIL_QUERY } from "@/graphql/queries"; import {COURSE_QUERY, COURSE_SESSION_DETAIL_QUERY} from "@/graphql/queries";
import { import {
circleFlatChildren, circleFlatChildren,
circleFlatLearningContents, circleFlatLearningContents,
circleFlatLearningUnits, circleFlatLearningUnits,
someFinishedInLearningSequence, someFinishedInLearningSequence,
} from "@/services/circle"; } from "@/services/circle";
import type { import type {DashboardDueDate, DashboardPersonRoleType, DashboardPersonType,} from "@/services/dashboard";
DashboardDueDate,
DashboardPersonRoleType,
DashboardPersonType,
} from "@/services/dashboard";
import { import {
courseIdForCourseSlug, courseIdForCourseSlug,
fetchDashboardDueDates, fetchDashboardDueDates,
fetchDashboardPersons, fetchDashboardPersons,
fetchStatisticData, fetchStatisticData,
} from "@/services/dashboard"; } from "@/services/dashboard";
import { presignUpload, uploadFile } from "@/services/files"; import {presignUpload, uploadFile} from "@/services/files";
import { useCompletionStore } from "@/stores/completion"; import {useCompletionStore} from "@/stores/completion";
import { useCourseSessionsStore } from "@/stores/courseSessions"; import {useCourseSessionsStore} from "@/stores/courseSessions";
import { useDashboardStore } from "@/stores/dashboard"; import {useDashboardStore} from "@/stores/dashboard";
import { useUserStore } from "@/stores/user"; import {useUserStore} from "@/stores/user";
import type { import type {
ActionCompetence, ActionCompetence,
AgentParticipantRelation, AgentParticipantRelation,
CircleType, CircleType,
CompetenceCertificate,
Course, Course,
CourseCompletion, CourseCompletion,
CourseCompletionStatus, CourseCompletionStatus,
@ -39,14 +36,16 @@ import type {
LearningUnitPerformanceCriteria, LearningUnitPerformanceCriteria,
PerformanceCriteria, PerformanceCriteria,
} from "@/types"; } from "@/types";
import { useQuery } from "@urql/vue"; import {useQuery} from "@urql/vue";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { t } from "i18next"; import {t} from "i18next";
import orderBy from "lodash/orderBy"; import orderBy from "lodash/orderBy";
import log from "loglevel"; import log from "loglevel";
import type { ComputedRef, Ref } from "vue"; import type {ComputedRef, Ref} from "vue";
import { computed, onMounted, ref, watchEffect } from "vue"; import {computed, onMounted, ref, watchEffect} from "vue";
import { type RouteLocationRaw, useRouter } from "vue-router"; import {type RouteLocationRaw, useRouter} from "vue-router";
import {getCertificates} from "./services/competence";
import {mergeCompetenceCertificates} from "./pages/competence/utils";
export function useCurrentCourseSession() { export function useCurrentCourseSession() {
/** /**
@ -178,7 +177,7 @@ export function useCourseData(courseSlug: string) {
// urql.useQuery is not meant to be used programmatically, so we use graphqlClient.query instead // urql.useQuery is not meant to be used programmatically, so we use graphqlClient.query instead
const resultPromise = graphqlClient const resultPromise = graphqlClient
.query(COURSE_QUERY, { slug: `${courseSlug}` }) .query(COURSE_QUERY, {slug: `${courseSlug}`})
.toPromise(); .toPromise();
resultPromise.then((result) => { resultPromise.then((result) => {
@ -443,7 +442,7 @@ export function useFileUpload() {
const fileInfo = ref({} as { id: string; name: string; url: string }); const fileInfo = ref({} as { id: string; name: string; url: string });
async function upload(e: Event) { async function upload(e: Event) {
const { files } = e.target as HTMLInputElement; const {files} = e.target as HTMLInputElement;
if (!files?.length) return; if (!files?.length) return;
try { try {
@ -461,7 +460,7 @@ export function useFileUpload() {
} }
} }
return { upload, error, loading, fileInfo }; return {upload, error, loading, fileInfo};
} }
export function useMyLearningMentors() { export function useMyLearningMentors() {
@ -471,7 +470,7 @@ export function useMyLearningMentors() {
const fetchMentors = async () => { const fetchMentors = async () => {
loading.value = true; loading.value = true;
const { data } = await useCSRFFetch( const {data} = await useCSRFFetch(
`/api/mentor/${currentCourseSessionId}/mentors` `/api/mentor/${currentCourseSessionId}/mentors`
).json(); ).json();
learningMentors.value = data.value; learningMentors.value = data.value;
@ -623,7 +622,7 @@ export function useCourseCircleProgress(circles: Ref<CircleType[] | undefined>)
return circles.value?.length ?? 0; return circles.value?.length ?? 0;
}); });
return { inProgressCirclesCount, circlesCount }; return {inProgressCirclesCount, circlesCount};
} }
export function useCourseStatisticsv2(courseSlug: string) { export function useCourseStatisticsv2(courseSlug: string) {
@ -669,6 +668,25 @@ export function useCourseStatisticsv2(courseSlug: string) {
}; };
} }
export function useCertificateQuery(
userIds: string[] | undefined,
courseSlug: string,
courseSession: CourseSession
) {
const certificatesQuery = (() => {
return useQuery({
query: COMPETENCE_NAVI_CERTIFICATE_QUERY,
variables: {
courseSlug: courseSlug,
courseSessionId: courseSession.id,
userIds,
},
});
})();
return {certificatesQuery};
}
export function useEvaluationWithFeedback() { export function useEvaluationWithFeedback() {
const currentCourseSession = useCurrentCourseSession(); const currentCourseSession = useCurrentCourseSession();
const hasFeedback = computed( const hasFeedback = computed(
@ -677,7 +695,7 @@ export function useEvaluationWithFeedback() {
currentCourseSession.value.course.configuration.is_vv currentCourseSession.value.course.configuration.is_vv
); );
return { hasFeedback }; return {hasFeedback};
} }
export function useVVByLink() { export function useVVByLink() {
@ -687,9 +705,36 @@ export function useVVByLink() {
() => () =>
router.resolve({ router.resolve({
name: "accountConfirm", name: "accountConfirm",
params: { courseType: `vv-${userStore.language}` }, params: {courseType: `vv-${userStore.language}`},
}).href as RouteLocationRaw }).href as RouteLocationRaw
); );
return { href }; return {href};
}
export function useAllCompetenceCertificates(
userId: string | undefined,
courseSlug: string
) {
const courseSessionsStore = useCourseSessionsStore();
const certificateQueries = courseSessionsStore.allCourseSessions.map(
(courseSession) => {
return useCertificateQuery([userId], courseSlug, courseSession).certificatesQuery;
}
);
const competenceCertificatesPerCs = computed(() =>
certificateQueries.map((query) => {
return getCertificates(query.data.value, userId ?? null)
?.competence_certificates as unknown as CompetenceCertificate[];
})
);
const isLoaded = computed(() => !certificateQueries.some((q) => q.fetching.value));
const competenceCertificates = computed(() =>
mergeCompetenceCertificates(competenceCertificatesPerCs.value.flat())
);
return {
competenceCertificates,
isLoaded,
};
} }

View File

@ -3,3 +3,5 @@ export const itCheckboxDefaultIconCheckedTailwindClass =
export const itCheckboxDefaultIconUncheckedTailwindClass = export const itCheckboxDefaultIconUncheckedTailwindClass =
"bg-[url(/static/icons/icon-checkbox-unchecked.svg)] hover:bg-[url(/static/icons/icon-checkbox-unchecked-hover.svg)]"; "bg-[url(/static/icons/icon-checkbox-unchecked.svg)] hover:bg-[url(/static/icons/icon-checkbox-unchecked-hover.svg)]";
export const COURSE_PROFILE_ALL_FILTER = "all";

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -278,6 +278,8 @@ type CourseObjectType {
configuration: CourseConfigurationObjectType! configuration: CourseConfigurationObjectType!
learning_path: LearningPathObjectType! learning_path: LearningPathObjectType!
action_competences: [ActionCompetenceObjectType!]! action_competences: [ActionCompetenceObjectType!]!
profiles: [String]
course_session_users(id: String): [CourseSessionUserType]!
} }
type ActionCompetenceObjectType implements CoursePageInterface { type ActionCompetenceObjectType implements CoursePageInterface {
@ -343,45 +345,38 @@ type CircleLightObjectType {
slug: String! slug: String!
} }
type TopicObjectType implements CoursePageInterface { type CourseSessionUserType {
is_visible: Boolean! id: UUID!
id: ID! chosen_profile: String!
title: String! course_session: CourseSessionObjectType!
slug: String!
content_type: String!
live: Boolean!
translation_key: String!
frontend_url: String!
course: CourseObjectType
circles: [CircleObjectType!]!
} }
type CircleObjectType implements CoursePageInterface { """
description: String! Leverages the internal Python implementation of UUID (uuid.UUID) to provide native UUID objects
goals: String! in fields, resolvers and input.
"""
scalar UUID
type CourseSessionObjectType {
id: ID! id: ID!
created_at: DateTime!
updated_at: DateTime!
course: CourseObjectType!
title: String! title: String!
slug: String! start_date: Date
content_type: String! end_date: Date
live: Boolean! attendance_courses: [CourseSessionAttendanceCourseObjectType!]!
translation_key: String! assignments: [CourseSessionAssignmentObjectType!]!
frontend_url: String! edoniq_tests: [CourseSessionEdoniqTestObjectType!]!
course: CourseObjectType users: [CourseSessionUserObjectsType!]!
learning_sequences: [LearningSequenceObjectType!]!
} }
type LearningSequenceObjectType implements CoursePageInterface { """
icon: String! The `Date` scalar type represents a Date
id: ID! value as specified by
title: String! [iso8601](https://en.wikipedia.org/wiki/ISO_8601).
slug: String! """
content_type: String! scalar Date
live: Boolean!
translation_key: String!
frontend_url: String!
course: CourseObjectType
learning_units: [LearningUnitObjectType!]!
}
type CourseSessionAttendanceCourseObjectType { type CourseSessionAttendanceCourseObjectType {
id: ID! id: ID!
@ -443,26 +438,19 @@ type DueDateObjectType {
course_session: CourseSessionObjectType! course_session: CourseSessionObjectType!
} }
type CourseSessionObjectType { type AttendanceUserObjectType {
id: ID! user_id: UUID!
created_at: DateTime! status: AttendanceUserStatus!
updated_at: DateTime! first_name: String
course: CourseObjectType! last_name: String
title: String! email: String
start_date: Date
end_date: Date
attendance_courses: [CourseSessionAttendanceCourseObjectType!]!
assignments: [CourseSessionAssignmentObjectType!]!
edoniq_tests: [CourseSessionEdoniqTestObjectType!]!
users: [CourseSessionUserObjectsType!]!
} }
""" """An enumeration."""
The `Date` scalar type represents a Date enum AttendanceUserStatus {
value as specified by PRESENT
[iso8601](https://en.wikipedia.org/wiki/ISO_8601). ABSENT
""" }
scalar Date
type CourseSessionAssignmentObjectType { type CourseSessionAssignmentObjectType {
id: ID! id: ID!
@ -591,12 +579,6 @@ type AssignmentCompletionObjectType {
evaluation_percent: Float evaluation_percent: Float
} }
"""
Leverages the internal Python implementation of UUID (uuid.UUID) to provide native UUID objects
in fields, resolvers and input.
"""
scalar UUID
type UserObjectType { type UserObjectType {
""" """
Erforderlich. 150 Zeichen oder weniger. Nur Buchstaben, Ziffern und @/./+/-/_. Erforderlich. 150 Zeichen oder weniger. Nur Buchstaben, Ziffern und @/./+/-/_.
@ -735,18 +717,46 @@ type CourseSessionUserExpertCircleType {
slug: String! slug: String!
} }
type AttendanceUserObjectType { type TopicObjectType implements CoursePageInterface {
user_id: UUID! is_visible: Boolean!
status: AttendanceUserStatus! id: ID!
first_name: String title: String!
last_name: String slug: String!
email: String content_type: String!
live: Boolean!
translation_key: String!
frontend_url: String!
course: CourseObjectType
circles: [CircleObjectType!]!
} }
"""An enumeration.""" type CircleObjectType implements CoursePageInterface {
enum AttendanceUserStatus { description: String!
PRESENT goals: String!
ABSENT is_base_circle: Boolean!
id: ID!
title: String!
slug: String!
content_type: String!
live: Boolean!
translation_key: String!
frontend_url: String!
course: CourseObjectType
learning_sequences: [LearningSequenceObjectType!]!
profiles: [String]!
}
type LearningSequenceObjectType implements CoursePageInterface {
icon: String!
id: ID!
title: String!
slug: String!
content_type: String!
live: Boolean!
translation_key: String!
frontend_url: String!
course: CourseObjectType
learning_units: [LearningUnitObjectType!]!
} }
type LearningContentMediaLibraryObjectType implements CoursePageInterface & LearningContentInterface { type LearningContentMediaLibraryObjectType implements CoursePageInterface & LearningContentInterface {
@ -909,6 +919,7 @@ type CompetenceCertificateListObjectType implements CoursePageInterface {
type Mutation { type Mutation {
send_feedback(course_session_id: ID!, data: GenericScalar, learning_content_page_id: ID!, learning_content_type: String!, submitted: Boolean = false): SendFeedbackMutation send_feedback(course_session_id: ID!, data: GenericScalar, learning_content_page_id: ID!, learning_content_type: String!, submitted: Boolean = false): SendFeedbackMutation
update_course_session_attendance_course_users(attendance_user_list: [AttendanceUserInputType]!, id: ID!): AttendanceCourseUserMutation update_course_session_attendance_course_users(attendance_user_list: [AttendanceUserInputType]!, id: ID!): AttendanceCourseUserMutation
update_course_session_profile(input: CourseSessionProfileMutationInput!): CourseSessionProfileMutationPayload
upsert_assignment_completion(assignment_id: ID!, assignment_user_id: UUID, completion_data_string: String, completion_status: AssignmentCompletionStatus, course_session_id: ID!, evaluation_passed: Boolean, evaluation_points: Float, evaluation_user_id: ID, initialize_completion: Boolean, learning_content_page_id: ID): AssignmentCompletionMutation upsert_assignment_completion(assignment_id: ID!, assignment_user_id: UUID, completion_data_string: String, completion_status: AssignmentCompletionStatus, course_session_id: ID!, evaluation_passed: Boolean, evaluation_points: Float, evaluation_user_id: ID, initialize_completion: Boolean, learning_content_page_id: ID): AssignmentCompletionMutation
} }
@ -939,6 +950,27 @@ input AttendanceUserInputType {
status: AttendanceUserStatus! status: AttendanceUserStatus!
} }
type CourseSessionProfileMutationPayload {
result: UpdateCourseProfileResult
clientMutationId: String
}
union UpdateCourseProfileResult = UpdateCourseProfileError | UpdateCourseProfileSuccess
type UpdateCourseProfileError {
message: String
}
type UpdateCourseProfileSuccess {
user: CourseSessionUserType!
}
input CourseSessionProfileMutationInput {
course_profile: String!
course_slug: String!
clientMutationId: String
}
type AssignmentCompletionMutation { type AssignmentCompletionMutation {
assignment_completion: AssignmentCompletionObjectType assignment_completion: AssignmentCompletionObjectType
} }

View File

@ -34,8 +34,11 @@ export const CourseSessionAssignmentObjectType = "CourseSessionAssignmentObjectT
export const CourseSessionAttendanceCourseObjectType = "CourseSessionAttendanceCourseObjectType"; export const CourseSessionAttendanceCourseObjectType = "CourseSessionAttendanceCourseObjectType";
export const CourseSessionEdoniqTestObjectType = "CourseSessionEdoniqTestObjectType"; export const CourseSessionEdoniqTestObjectType = "CourseSessionEdoniqTestObjectType";
export const CourseSessionObjectType = "CourseSessionObjectType"; export const CourseSessionObjectType = "CourseSessionObjectType";
export const CourseSessionProfileMutationInput = "CourseSessionProfileMutationInput";
export const CourseSessionProfileMutationPayload = "CourseSessionProfileMutationPayload";
export const CourseSessionUserExpertCircleType = "CourseSessionUserExpertCircleType"; export const CourseSessionUserExpertCircleType = "CourseSessionUserExpertCircleType";
export const CourseSessionUserObjectsType = "CourseSessionUserObjectsType"; export const CourseSessionUserObjectsType = "CourseSessionUserObjectsType";
export const CourseSessionUserType = "CourseSessionUserType";
export const CourseStatisticsType = "CourseStatisticsType"; export const CourseStatisticsType = "CourseStatisticsType";
export const DashboardConfigType = "DashboardConfigType"; export const DashboardConfigType = "DashboardConfigType";
export const DashboardType = "DashboardType"; export const DashboardType = "DashboardType";
@ -84,4 +87,7 @@ export const StatisticsCourseSessionsSelectionMetricType = "StatisticsCourseSess
export const String = "String"; export const String = "String";
export const TopicObjectType = "TopicObjectType"; export const TopicObjectType = "TopicObjectType";
export const UUID = "UUID"; export const UUID = "UUID";
export const UpdateCourseProfileError = "UpdateCourseProfileError";
export const UpdateCourseProfileResult = "UpdateCourseProfileResult";
export const UpdateCourseProfileSuccess = "UpdateCourseProfileSuccess";
export const UserObjectType = "UserObjectType"; export const UserObjectType = "UserObjectType";

View File

@ -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
}
}
}
}
`);

View File

@ -1,4 +1,4 @@
import { graphql } from "@/gql"; import {graphql} from "@/gql";
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const CoursePageFragment = graphql(` const CoursePageFragment = graphql(`
@ -90,6 +90,45 @@ export const ASSIGNMENT_COMPLETION_QUERY = graphql(`
`); `);
export const COMPETENCE_NAVI_CERTIFICATE_QUERY = graphql(` export const COMPETENCE_NAVI_CERTIFICATE_QUERY = graphql(`
query competenceCertificateQuery($courseSlug: String!, $courseSessionId: ID!) {
competence_certificate_list(course_slug: $courseSlug) {
...CoursePageFields
competence_certificates {
...CoursePageFields
assignments {
...CoursePageFields
assignment_type
max_points
competence_certificate_weight
completions(course_session_id: $courseSessionId) {
id
completion_status
submitted_at
evaluation_points
evaluation_points_deducted
evaluation_points_final
evaluation_max_points
evaluation_passed
course_session {
id
title
}
}
learning_content {
...CoursePageFields
circle {
id
title
slug
}
}
}
}
}
}
`);
export const COMPETENCE_NAVI_CERTIFICATE_FOR_USER_QUERY = graphql(`
query competenceCertificateForUserQuery( query competenceCertificateForUserQuery(
$courseSlug: String! $courseSlug: String!
$courseSessionId: ID! $courseSessionId: ID!
@ -117,6 +156,10 @@ export const COMPETENCE_NAVI_CERTIFICATE_QUERY = graphql(`
assignment_user { assignment_user {
id id
} }
course_session {
id
title
}
} }
learning_content { learning_content {
...CoursePageFields ...CoursePageFields
@ -225,18 +268,28 @@ export const COURSE_SESSION_DETAIL_QUERY = graphql(`
`); `);
export const COURSE_QUERY = graphql(` export const COURSE_QUERY = graphql(`
query courseQuery($slug: String!) { query courseQuery($slug: String!, $user: String) {
course(slug: $slug) { course(slug: $slug) {
id id
title title
slug slug
category_name category_name
profiles
course_session_users(id: $user) {
id
__typename
chosen_profile
course_session {
id
}
}
configuration { configuration {
id id
enable_circle_documents enable_circle_documents
enable_learning_mentor enable_learning_mentor
enable_competence_certificates enable_competence_certificates
is_uk is_uk
is_vv
} }
action_competences { action_competences {
competence_id competence_id
@ -259,6 +312,8 @@ export const COURSE_QUERY = graphql(`
circles { circles {
description description
goals goals
profiles
is_base_circle
...CoursePageFields ...CoursePageFields
learning_sequences { learning_sequences {
icon icon

View File

@ -403,6 +403,13 @@ function log(data: any) {
</button> </button>
</div> </div>
<h2 class="mb-8 mt-8">Tags</h2>
<div class="mb-16 flex flex-col flex-wrap content-center gap-4 lg:flex-row">
<button class="tag-active">Active</button>
<button class="tag-inactive">Inactive</button>
</div>
<h2 class="mb-8 mt-8">Dropdown (Work-in-progress)</h2> <h2 class="mb-8 mt-8">Dropdown (Work-in-progress)</h2>
<ItDropdownSelect <ItDropdownSelect

View File

@ -1,11 +1,18 @@
<script setup lang="ts"> <script setup lang="ts">
import { useCurrentCourseSession } from "@/composables";
import router from "@/router";
import { useCourseSessionsStore } from "@/stores/courseSessions";
import type { CompetenceCertificateAssignment } from "@/types"; import type { CompetenceCertificateAssignment } from "@/types";
import * as log from "loglevel"; import * as log from "loglevel";
log.debug("CompetenceAssignmentRow setup"); log.debug("CompetenceAssignmentRow setup");
const currentCourseSession = useCurrentCourseSession();
const { switchCourseSessionById } = useCourseSessionsStore();
export interface Props { export interface Props {
assignment: CompetenceCertificateAssignment; assignment: CompetenceCertificateAssignment;
showCourseSession: boolean;
addBorderBottom?: boolean; addBorderBottom?: boolean;
} }
@ -19,6 +26,13 @@ const getIconName = () => {
} }
return "it-icon-assignment-large"; return "it-icon-assignment-large";
}; };
const openInCircle = (assignment: CompetenceCertificateAssignment) => {
if (assignment.completion?.course_session !== currentCourseSession.value) {
switchCourseSessionById(assignment.completion!.course_session.id);
}
router.push(assignment.frontend_url);
};
</script> </script>
<template> <template>
@ -30,19 +44,26 @@ const getIconName = () => {
<component :is="getIconName()" class="mr-4 hidden h-9 w-9 lg:block"></component> <component :is="getIconName()" class="mr-4 hidden h-9 w-9 lg:block"></component>
<div class="flex flex-col lg:w-[420px]"> <div class="flex flex-col lg:w-[420px]">
<h3 class="text-bold flex items-center gap-2">{{ assignment.title }}</h3> <h3 class="text-bold flex items-center gap-2">{{ assignment.title }}</h3>
<p
v-if="showCourseSession"
:data-cy="`assignment-${assignment.slug}-course-session`"
>
{{ assignment?.completion?.course_session.title }}
</p>
<p class="text-gray-800"> <p class="text-gray-800">
<a <button
v-if="assignment.learning_content" v-if="assignment.learning_content"
:href="assignment.frontend_url" :href="assignment.frontend_url"
class="link" class="link"
data-cy="open-learning-content" data-cy="open-learning-content"
@click="() => openInCircle(assignment)"
> >
{{ {{
$t("general.im circle x anschauen", { $t("general.im circle x anschauen", {
x: assignment.learning_content.circle.title, x: assignment.learning_content.circle.title,
}) })
}} }}
</a> </button>
<span v-else>Fehler, Lerninhalt nicht korrekt verknüpft</span> <span v-else>Fehler, Lerninhalt nicht korrekt verknüpft</span>
</p> </p>
</div> </div>

View File

@ -9,6 +9,7 @@ import {
calcCompetenceCertificateGrade, calcCompetenceCertificateGrade,
competenceCertificateProgressStatusCount, competenceCertificateProgressStatusCount,
} from "@/pages/competence/utils"; } from "@/pages/competence/utils";
import { useCurrentCourseSession } from "@/composables";
log.debug("CompetenceCertificateComponent setup"); log.debug("CompetenceCertificateComponent setup");
@ -51,6 +52,15 @@ const frontendUrl = computed(() => {
? props.frontendUrl ? props.frontendUrl
: props.competenceCertificate.frontend_url; : props.competenceCertificate.frontend_url;
}); });
const showCourseSession = computed(() => {
const currentCourseSession = useCurrentCourseSession();
return props.competenceCertificate.assignments.some((assignment) => {
return (
assignment.completion?.course_session.title !== currentCourseSession.value.title
);
});
});
</script> </script>
<template> <template>
@ -134,6 +144,7 @@ const frontendUrl = computed(() => {
<CompetenceAssignmentRow <CompetenceAssignmentRow
:assignment="assignment" :assignment="assignment"
:add-border-bottom="index < competenceCertificate.assignments.length - 1" :add-border-bottom="index < competenceCertificate.assignments.length - 1"
:show-course-session="showCourseSession"
></CompetenceAssignmentRow> ></CompetenceAssignmentRow>
</div> </div>
</div> </div>

View File

@ -1,13 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import log from "loglevel"; import log from "loglevel";
import { computed } from "vue"; import {computed} from "vue";
import type { CompetenceCertificate } from "@/types"; import type {CompetenceCertificate} from "@/types";
import { useCurrentCourseSession } from "@/composables"; import {useAllCompetenceCertificates} from "@/composables";
import {getPreviousRoute} from "@/router/history";
import CompetenceCertificateComponent from "@/pages/competence/CompetenceCertificateComponent.vue"; import CompetenceCertificateComponent from "@/pages/competence/CompetenceCertificateComponent.vue";
import { getPreviousRoute } from "@/router/history";
import { useQuery } from "@urql/vue";
import { COMPETENCE_NAVI_CERTIFICATE_QUERY } from "@/graphql/queries";
import { useUserStore } from "@/stores/user";
const props = defineProps<{ const props = defineProps<{
courseSlug: string; courseSlug: string;
@ -17,17 +14,6 @@ const props = defineProps<{
log.debug("CompetenceCertificateDetailPage setup", props); log.debug("CompetenceCertificateDetailPage setup", props);
const user = useUserStore();
const courseSession = useCurrentCourseSession();
const certificatesQuery = useQuery({
query: COMPETENCE_NAVI_CERTIFICATE_QUERY,
variables: {
courseSlug: props.courseSlug,
courseSessionId: courseSession.value.id,
userIds: [props.userId ?? user.id],
},
});
const competenceCertificates = computed(() => { const competenceCertificates = computed(() => {
return ( return (
@ -36,7 +22,16 @@ const competenceCertificates = computed(() => {
); );
}); });
const {competenceCertificates} = useAllCompetenceCertificates(
props.userId,
props.courseSlug
);
const certificate = computed(() => { const certificate = computed(() => {
if (!competenceCertificates) {
return null;
}
return competenceCertificates.value.find((cc) => return competenceCertificates.value.find((cc) =>
cc.slug.endsWith(props.certificateSlug) cc.slug.endsWith(props.certificateSlug)
); );

View File

@ -1,17 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import log from "loglevel"; import log from "loglevel";
import { computed, onMounted } from "vue"; import {computed, onMounted} from "vue";
import type { CompetenceCertificate } from "@/types"; import {useAllCompetenceCertificates} from "@/composables";
import { useCurrentCourseSession } from "@/composables";
import CompetenceCertificateComponent from "@/pages/competence/CompetenceCertificateComponent.vue"; import CompetenceCertificateComponent from "@/pages/competence/CompetenceCertificateComponent.vue";
import { import {assignmentsUserPoints, calcCompetencesTotalGrade,} from "@/pages/competence/utils";
assignmentsUserPoints, import {useRoute} from "vue-router";
calcCompetencesTotalGrade,
} from "@/pages/competence/utils";
import { useRoute } from "vue-router";
import { useQuery } from "@urql/vue";
import { COMPETENCE_NAVI_CERTIFICATE_QUERY } from "@/graphql/queries";
import { useUserStore } from "@/stores/user";
const props = defineProps<{ const props = defineProps<{
courseSlug: string; courseSlug: string;
@ -22,27 +15,14 @@ log.debug("CompetenceCertificateListPage setup", props);
const route = useRoute(); const route = useRoute();
const user = useUserStore();
const courseSession = useCurrentCourseSession();
const certificatesQuery = useQuery({ const {competenceCertificates} = useAllCompetenceCertificates(
query: COMPETENCE_NAVI_CERTIFICATE_QUERY, props.userId,
variables: { props.courseSlug
courseSlug: props.courseSlug, );
courseSessionId: courseSession.value.id,
userIds: [props.userId ?? user.id],
},
});
const competenceCertificates = computed(() => {
return (
(certificatesQuery.data.value?.competence_certificate_list
?.competence_certificates as unknown as CompetenceCertificate[]) ?? []
);
});
const assignments = computed(() => { const assignments = computed(() => {
return competenceCertificates?.value?.flatMap((cc) => cc.assignments); return competenceCertificates?.value.flatMap((cc) => cc.assignments);
}); });
const totalGrade = computed(() => { const totalGrade = computed(() => {

View File

@ -1,10 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import log from "loglevel"; import log from "loglevel";
import { COMPETENCE_NAVI_CERTIFICATE_QUERY } from "@/graphql/queries"; import {computed} from "vue";
import { useQuery } from "@urql/vue"; import {useAllCompetenceCertificates, useCurrentCourseSession} from "@/composables";
import { computed } from "vue";
import type { CompetenceCertificate } from "@/types";
import { useCurrentCourseSession } from "@/composables";
import { import {
assignmentsUserPoints, assignmentsUserPoints,
calcCompetenceCertificateGrade, calcCompetenceCertificateGrade,
@ -12,9 +9,10 @@ import {
competenceCertificateProgressStatusCount, competenceCertificateProgressStatusCount,
} from "@/pages/competence/utils"; } from "@/pages/competence/utils";
import ItProgress from "@/components/ui/ItProgress.vue"; import ItProgress from "@/components/ui/ItProgress.vue";
import SelfEvaluationAndFeedbackOverview from "@/components/selfEvaluationFeedback/SelfEvaluationAndFeedbackOverview.vue"; import SelfEvaluationAndFeedbackOverview
import { useUserStore } from "@/stores/user"; from "@/components/selfEvaluationFeedback/SelfEvaluationAndFeedbackOverview.vue";
import { useRouter } from "vue-router"; import {useUserStore} from "@/stores/user";
import {useRouter} from "vue-router";
const props = defineProps<{ const props = defineProps<{
courseSlug: string; courseSlug: string;
@ -22,8 +20,6 @@ const props = defineProps<{
log.debug("CompetenceIndexPage setup", props); log.debug("CompetenceIndexPage setup", props);
const courseSession = useCurrentCourseSession();
const user = useUserStore(); const user = useUserStore();
const certificatesQuery = useQuery({ const certificatesQuery = useQuery({
@ -35,12 +31,10 @@ const certificatesQuery = useQuery({
}, },
}); });
const competenceCertificates = computed(() => { const {competenceCertificates, isLoaded} = useAllCompetenceCertificates(
return ( user.id,
(certificatesQuery.data.value?.competence_certificate_list props.courseSlug
?.competence_certificates as unknown as CompetenceCertificate[]) ?? [] );
);
});
const allAssignments = computed(() => { const allAssignments = computed(() => {
return competenceCertificates.value.flatMap((cc) => cc.assignments); return competenceCertificates.value.flatMap((cc) => cc.assignments);
@ -52,8 +46,6 @@ const userPointsEvaluatedAssignments = computed(() => {
const currentCourseSession = useCurrentCourseSession(); const currentCourseSession = useCurrentCourseSession();
const isLoaded = computed(() => !certificatesQuery.fetching.value);
const router = useRouter(); const router = useRouter();
</script> </script>

View File

@ -1,6 +1,7 @@
import type { StatusCount } from "@/components/ui/ItProgress.vue"; import type { StatusCount } from "@/components/ui/ItProgress.vue";
import { percentToRoundedGrade } from "@/services/assignmentService"; import { percentToRoundedGrade } from "@/services/assignmentService";
import type { CompetenceCertificate, CompetenceCertificateAssignment } from "@/types"; import type { CompetenceCertificate, CompetenceCertificateAssignment } from "@/types";
import dayjs from "dayjs";
import _ from "lodash"; import _ from "lodash";
export function assignmentsMaxEvaluationPoints( export function assignmentsMaxEvaluationPoints(
@ -84,3 +85,55 @@ export function competenceCertificateProgressStatusCount(
FAIL: 0, FAIL: 0,
} as StatusCount; } as StatusCount;
} }
export function mergeCompetenceCertificates(
competenceCertificates: CompetenceCertificate[]
) {
const groupedCompetenceCertificates: Record<
string,
Array<CompetenceCertificate>
> = {};
competenceCertificates.forEach((certificate) => {
if (!certificate) {
return;
}
if (!groupedCompetenceCertificates[certificate.id]) {
groupedCompetenceCertificates[certificate.id] = [];
}
groupedCompetenceCertificates[certificate.id].push(certificate);
});
console.log(
`Found ${Object.keys(groupedCompetenceCertificates).length} competence certificates over all course sessions`
);
return Object.values(groupedCompetenceCertificates).map((certificates) => {
const mergedCertificate: CompetenceCertificate = {
...certificates[0],
assignments: [],
};
certificates.forEach((certificate) => {
certificate.assignments.forEach((assignment) => {
const existingAssignment = mergedCertificate.assignments.find(
(a) => a.id === assignment.id
);
if (!existingAssignment) {
mergedCertificate.assignments.push(assignment);
} else if (
assignment.completion != null &&
(existingAssignment.completion == null ||
dayjs(existingAssignment.completion.evaluation_submitted_at).isBefore(
assignment.completion.evaluation_submitted_at
))
) {
mergedCertificate.assignments.splice(
mergedCertificate.assignments.findIndex((a) => a.id === assignment.id),
1
);
mergedCertificate.assignments.push(assignment);
}
});
});
return mergedCertificate;
});
}

View File

@ -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>

View File

@ -1,26 +1,23 @@
<script setup lang="ts"> <script setup lang="ts">
import LearningPathCircleListTile from "@/pages/learningPath/learningPathPage/LearningPathCircleListTile.vue";
import { computed } from "vue";
import type { LearningContentWithCompletion, LearningPathType } from "@/types"; import type { LearningContentWithCompletion, LearningPathType } from "@/types";
import { computed } from "vue";
import LearningPathListTopic from "./LearningPathListTopic.vue";
const props = defineProps<{ const props = defineProps<{
learningPath: LearningPathType | undefined; learningPath: LearningPathType | undefined;
nextLearningContent: LearningContentWithCompletion | undefined; nextLearningContent: LearningContentWithCompletion | undefined;
filter?: string;
}>(); }>();
const topics = computed(() => props.learningPath?.topics ?? []); const topics = computed(() => props.learningPath?.topics ?? []);
</script> </script>
<template> <template>
<div v-for="topic in topics" :key="topic.title"> <LearningPathListTopic
<div class="pb-2 font-bold text-gray-700"> v-for="topic in topics"
{{ topic.title }} :key="topic.title"
</div> :topic="topic"
<LearningPathCircleListTile :next-learning-content="nextLearningContent"
v-for="circle in topic.circles" :filter="filter"
:key="circle.id" />
:circle="circle"
:next-learning-content="props.nextLearningContent"
></LearningPathCircleListTile>
</div>
</template> </template>

View File

@ -3,6 +3,7 @@ import LearningPathListView from "@/pages/learningPath/learningPathPage/Learning
import LearningPathPathView from "@/pages/learningPath/learningPathPage/LearningPathPathView.vue"; import LearningPathPathView from "@/pages/learningPath/learningPathPage/LearningPathPathView.vue";
import CircleProgress from "@/pages/learningPath/learningPathPage/LearningPathProgress.vue"; import CircleProgress from "@/pages/learningPath/learningPathPage/LearningPathProgress.vue";
import LearningPathTopics from "@/pages/learningPath/learningPathPage/LearningPathTopics.vue"; import LearningPathTopics from "@/pages/learningPath/learningPathPage/LearningPathTopics.vue";
import LearningPathProfileFilter from "@/pages/learningPath/learningPathPage/LearningPathProfileFilter.vue";
import type { ViewType } from "@/pages/learningPath/learningPathPage/LearningPathViewSwitch.vue"; import type { ViewType } from "@/pages/learningPath/learningPathPage/LearningPathViewSwitch.vue";
import LearningPathViewSwitch from "@/pages/learningPath/learningPathPage/LearningPathViewSwitch.vue"; import LearningPathViewSwitch from "@/pages/learningPath/learningPathPage/LearningPathViewSwitch.vue";
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core"; import { breakpointsTailwind, useBreakpoints } from "@vueuse/core";
@ -13,6 +14,9 @@ import {
useCurrentCourseSession, useCurrentCourseSession,
} from "@/composables"; } from "@/composables";
import CourseSessionDueDatesList from "@/components/dueDates/CourseSessionDueDatesList.vue"; import CourseSessionDueDatesList from "@/components/dueDates/CourseSessionDueDatesList.vue";
import { useMutation } from "@urql/vue";
import { UPDATE_COURSE_PROFILE_MUTATION } from "@/graphql/mutations";
import { filterCircles, useCourseFilter } from "./utils";
const props = defineProps<{ const props = defineProps<{
courseSlug: string; courseSlug: string;
@ -33,9 +37,28 @@ const course = computed(() => lpQueryResult.course.value);
const courseSession = useCurrentCourseSession(); const courseSession = useCurrentCourseSession();
const { inProgressCirclesCount, circlesCount } = useCourseCircleProgress( const { filter } = useCourseFilter(props.courseSlug);
lpQueryResult.circles
); const filteredCircles = computed(() => {
if (lpQueryResult.circles.value === undefined) {
return [];
}
return filterCircles(filter.value, lpQueryResult.circles.value);
});
const { inProgressCirclesCount, circlesCount } =
useCourseCircleProgress(filteredCircles);
const updateCourseProfileMutation = useMutation(UPDATE_COURSE_PROFILE_MUTATION);
const updateCourseProfile = (profile: string) => {
updateCourseProfileMutation.executeMutation({
input: {
course_profile: profile,
course_slug: props.courseSlug,
},
});
};
const changeViewType = (viewType: ViewType) => { const changeViewType = (viewType: ViewType) => {
selectedView.value = viewType; selectedView.value = viewType;
@ -46,7 +69,9 @@ const changeViewType = (viewType: ViewType) => {
<template> <template>
<div class="flex flex-col"> <div class="flex flex-col">
<!-- Top --> <!-- Top -->
<div class="flex flex-row justify-between space-x-8 bg-gray-200 p-6 sm:p-12"> <div
class="flex flex-col justify-between gap-8 bg-gray-200 p-6 sm:p-12 xl:flex-row"
>
<!-- Left --> <!-- Left -->
<div class="flex flex-col justify-between lg:w-1/2"> <div class="flex flex-col justify-between lg:w-1/2">
<div> <div>
@ -64,15 +89,21 @@ const changeViewType = (viewType: ViewType) => {
></CircleProgress> ></CircleProgress>
</div> </div>
<!-- Right --> <!-- todo: find out when to display CourseSessionDueDatesList -->
<div v-if="!useMobileLayout" class="flex-grow"> <div v-if="!useMobileLayout && false" class="flex-grow">
<CourseSessionDueDatesList <CourseSessionDueDatesList
:course-session-id="courseSession.id" :course-session-id="courseSession.id"
:max-count="2" :max-count="2"
></CourseSessionDueDatesList> ></CourseSessionDueDatesList>
</div> </div>
<!-- Right -->
<LearningPathProfileFilter
v-if="course?.configuration.is_vv"
:profiles="course?.profiles"
:selected="filter"
@select="updateCourseProfile"
/>
</div> </div>
<!-- Bottom --> <!-- Bottom -->
<div class="bg-white"> <div class="bg-white">
<div v-if="lpQueryResult.learningPath"> <div v-if="lpQueryResult.learningPath">
@ -101,6 +132,7 @@ const changeViewType = (viewType: ViewType) => {
<LearningPathPathView <LearningPathPathView
:learning-path="learningPath" :learning-path="learningPath"
:use-mobile-layout="useMobileLayout" :use-mobile-layout="useMobileLayout"
:filter="filter"
:next-learning-content="lpQueryResult.nextLearningContent.value" :next-learning-content="lpQueryResult.nextLearningContent.value"
></LearningPathPathView> ></LearningPathPathView>
</div> </div>
@ -114,6 +146,7 @@ const changeViewType = (viewType: ViewType) => {
<LearningPathListView <LearningPathListView
:learning-path="learningPath" :learning-path="learningPath"
:next-learning-content="lpQueryResult.nextLearningContent.value" :next-learning-content="lpQueryResult.nextLearningContent.value"
:filter="filter"
></LearningPathListView> ></LearningPathListView>
</div> </div>
<div <div

View File

@ -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>

View File

@ -1,10 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import LearningPathCircleColumn from "@/pages/learningPath/learningPathPage/LearningPathCircleColumn.vue";
import LearningPathScrollButton from "@/pages/learningPath/learningPathPage/LearningPathScrollButton.vue"; import LearningPathScrollButton from "@/pages/learningPath/learningPathPage/LearningPathScrollButton.vue";
import { useScroll } from "@vueuse/core"; import { useScroll } from "@vueuse/core";
import { ref } from "vue"; import { computed, nextTick, ref, watch } from "vue";
import type { LearningContentWithCompletion, LearningPathType } from "@/types"; import type { LearningContentWithCompletion, LearningPathType } from "@/types";
import LoadingSpinner from "@/components/ui/LoadingSpinner.vue"; import LoadingSpinner from "@/components/ui/LoadingSpinner.vue";
import LearningPathPathTopic from "./LearningPathPathTopic.vue";
const props = defineProps<{ const props = defineProps<{
learningPath: LearningPathType | undefined; learningPath: LearningPathType | undefined;
@ -12,6 +12,7 @@ const props = defineProps<{
useMobileLayout: boolean; useMobileLayout: boolean;
hideButtons?: boolean; hideButtons?: boolean;
overrideCircleUrlBase?: string; overrideCircleUrlBase?: string;
filter?: string;
}>(); }>();
const scrollIncrement = 600; const scrollIncrement = 600;
@ -19,13 +20,6 @@ const scrollIncrement = 600;
const learnPathDiagram = ref<HTMLElement | null>(null); const learnPathDiagram = ref<HTMLElement | null>(null);
const { x, arrivedState } = useScroll(learnPathDiagram, { behavior: "smooth" }); const { x, arrivedState } = useScroll(learnPathDiagram, { behavior: "smooth" });
const isFirstCircle = (topicIndex: number, circleIndex: number) =>
topicIndex === 0 && circleIndex === 0;
const isLastCircle = (topicIndex: number, circleIndex: number, numCircles: number) =>
topicIndex === (props.learningPath?.topics ?? []).length - 1 &&
circleIndex === numCircles - 1;
const scrollRight = () => scrollLearnPathDiagram(scrollIncrement); const scrollRight = () => scrollLearnPathDiagram(scrollIncrement);
const scrollLeft = () => scrollLearnPathDiagram(-scrollIncrement); const scrollLeft = () => scrollLearnPathDiagram(-scrollIncrement);
@ -33,6 +27,22 @@ const scrollLeft = () => scrollLearnPathDiagram(-scrollIncrement);
const scrollLearnPathDiagram = (offset: number) => { const scrollLearnPathDiagram = (offset: number) => {
x.value += offset; x.value += offset;
}; };
const topics = computed(() => props.learningPath?.topics ?? []);
watch(
() => props.filter,
() => {
// we need to update the scroll state of the element, otherwise the arrows won't match the scroll state
// https://github.com/vueuse/vueuse/issues/2875
nextTick(() => {
if (learnPathDiagram.value) {
const scrollEvent = new Event("scroll");
learnPathDiagram.value.dispatchEvent(scrollEvent);
}
});
}
);
</script> </script>
<template> <template>
@ -50,37 +60,16 @@ const scrollLearnPathDiagram = (offset: number) => {
ref="learnPathDiagram" ref="learnPathDiagram"
class="no-scrollbar flex h-96 snap-x flex-row overflow-auto py-5 sm:py-10" class="no-scrollbar flex h-96 snap-x flex-row overflow-auto py-5 sm:py-10"
> >
<div <LearningPathPathTopic
v-for="(topic, topicIndex) in props.learningPath?.topics ?? []" v-for="(topic, topicIndex) in props.learningPath?.topics ?? []"
:key="topic.title" :key="topic.title"
class="border-l border-gray-500" :topic-index="topicIndex"
:class="topicIndex == 0 ? 'ml-6 sm:ml-12' : ''" :topic="topic"
> :next-learning-content="nextLearningContent"
<p :override-circle-url-base="overrideCircleUrlBase"
:id="`topic-${topic.slug}`" :filter="filter"
class="inline-block h-12 self-start px-4 font-bold text-gray-800" :is-last-topic="topicIndex === topics.length - 1"
data-cy="lp-topic" />
>
{{ topic.title }}
</p>
<div class="flex flex-row pt-6">
<LearningPathCircleColumn
v-for="(circle, circleIndex) in topic.circles"
:key="circle.id"
:circle="circle"
:next-learning-content="props.nextLearningContent"
:is-first-circle="isFirstCircle(topicIndex, circleIndex)"
:is-last-circle="
isLastCircle(topicIndex, circleIndex, topic.circles.length)
"
:override-circle-url="
props.overrideCircleUrlBase
? `${props.overrideCircleUrlBase}/${circle.slug}`
: undefined
"
></LearningPathCircleColumn>
</div>
</div>
</div> </div>
<LearningPathScrollButton <LearningPathScrollButton
v-show="!arrivedState.right" v-show="!arrivedState.right"

View File

@ -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>

View File

@ -1,3 +1,6 @@
import { useCurrentCourseSession } from "@/composables";
import { COURSE_PROFILE_ALL_FILTER } from "@/constants";
import { COURSE_QUERY } from "@/graphql/queries";
import type { import type {
CircleSectorData, CircleSectorData,
CircleSectorProgress, CircleSectorProgress,
@ -7,6 +10,8 @@ import {
someFinishedInLearningSequence, someFinishedInLearningSequence,
} from "@/services/circle"; } from "@/services/circle";
import type { CircleType } from "@/types"; import type { CircleType } from "@/types";
import { useQuery } from "@urql/vue";
import { computed } from "vue";
export function calculateCircleSectorData(circle: CircleType): CircleSectorData[] { export function calculateCircleSectorData(circle: CircleType): CircleSectorData[] {
return circle.learning_sequences.map((ls) => { return circle.learning_sequences.map((ls) => {
@ -21,3 +26,42 @@ export function calculateCircleSectorData(circle: CircleType): CircleSectorData[
}; };
}); });
} }
export function useCourseFilter(courseSlug: string, courseSessionId?: string) {
const csId = computed(() => {
if (courseSessionId) {
return courseSessionId;
}
// assume we're on a page with a current course session
const courseSession = useCurrentCourseSession();
return courseSession.value.id;
});
const courseReactiveResult = useQuery({
query: COURSE_QUERY,
variables: { slug: courseSlug },
});
const courseReactive = computed(() => courseReactiveResult.data.value?.course);
const courseSessionUser = computed(() => {
return courseReactive.value?.course_session_users.find(
(e) => e?.course_session.id === csId.value
);
});
const filter = computed(() => {
return courseSessionUser.value?.chosen_profile || "";
});
return {
filter,
courseReactive,
courseSessionUser,
};
}
export function filterCircles(filter: string | undefined, circles: CircleType[]) {
if (filter === undefined || filter === "" || filter === COURSE_PROFILE_ALL_FILTER) {
return circles;
}
return circles.filter(
(circle) => circle.profiles.indexOf(filter as string) > -1 || circle.is_base_circle
);
}

View File

@ -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>

View File

@ -229,6 +229,7 @@ const executePayment = async () => {
redirect_url: fullHost, redirect_url: fullHost,
address: addressData, address: addressData,
product: props.courseType, product: props.courseType,
chosen_profile: user.chosen_profile?.id || "",
with_cembra_byjuno_invoice: address.value.payment_method === "cembra_byjuno", with_cembra_byjuno_invoice: address.value.payment_method === "cembra_byjuno",
device_fingerprint_session_key: getLocalSessionKey(), device_fingerprint_session_key: getLocalSessionKey(),
}).then((res: any) => { }).then((res: any) => {

View File

@ -47,6 +47,14 @@ const { t } = useTranslation();
$t("Füge dein Profilbild hinzu und ergänze die fehlenden Angaben.") $t("Füge dein Profilbild hinzu und ergänze die fehlenden Angaben.")
}} }}
</li> </li>
<li class="relative pl-8">
<span class="font-bold">{{ $t("a.Zulassungsprofil auswählen") }}:</span>
{{
$t(
"a.Wähle ein Zulassungsprofil, damit du deinen Lehrgang an der richtigen Stelle beginnen kannst."
)
}}
</li>
<li class="relative pl-8"> <li class="relative pl-8">
<span class="font-bold">{{ $t("a.Lehrgang kaufen") }}:</span> <span class="font-bold">{{ $t("a.Lehrgang kaufen") }}:</span>
{{ {{

View File

@ -1,11 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { calculateCircleSectorData } from "@/pages/learningPath/learningPathPage/utils";
import { useCourseDataWithCompletion } from "@/composables"; import { useCourseDataWithCompletion } from "@/composables";
import UserProfileContent from "@/components/userProfile/UserProfileContent.vue"; import UserProfileContent from "@/components/userProfile/UserProfileContent.vue";
import LearningPathCircle from "@/pages/learningPath/learningPathPage/LearningPathCircle.vue";
import LearningSequence from "@/pages/learningPath/circlePage/LearningSequence.vue"; import LearningSequence from "@/pages/learningPath/circlePage/LearningSequence.vue";
import { ref, watch } from "vue"; import { computed, ref, watch } from "vue";
import type { CircleType } from "@/types"; import type { CircleType } from "@/types";
import { COURSE_QUERY } from "@/graphql/queries";
import { useQuery } from "@urql/vue";
import UserProfileTopicList from "./UserProfileTopicList.vue";
const props = defineProps<{ const props = defineProps<{
userId: string; userId: string;
@ -20,6 +21,23 @@ function selectCircle(circle: CircleType) {
selectedCircle.value = circle; selectedCircle.value = circle;
} }
const courseReactiveResult = useQuery({
query: COURSE_QUERY,
variables: { slug: props.courseSlug, user: props.userId },
});
const courseReactive = computed(() => courseReactiveResult.data.value?.course);
const courseSessionUsers = computed(() => {
return courseReactive.value?.course_session_users;
});
const chosenProfile = computed(() => {
if (courseSessionUsers.value && courseSessionUsers.value.length > 0) {
return courseSessionUsers.value[0]?.chosen_profile;
}
return undefined;
});
watch(lpQueryResult.learningPath, () => { watch(lpQueryResult.learningPath, () => {
if (lpQueryResult.learningPath?.value?.topics?.length) { if (lpQueryResult.learningPath?.value?.topics?.length) {
selectCircle(lpQueryResult.learningPath.value.topics[0].circles[0]); selectCircle(lpQueryResult.learningPath.value.topics[0].circles[0]);
@ -30,28 +48,21 @@ watch(lpQueryResult.learningPath, () => {
<template> <template>
<UserProfileContent> <UserProfileContent>
<template #side> <template #side>
<div <div v-if="chosenProfile">
<h3 class="mb-4 text-base font-bold">
Zulassungsprofil:
<br />
{{ $t(`profile.${chosenProfile}`) }}
</h3>
</div>
<UserProfileTopicList
v-for="topic in lpQueryResult.learningPath?.value?.topics ?? []" v-for="topic in lpQueryResult.learningPath?.value?.topics ?? []"
:key="topic.id" :key="topic.id"
class="mb-4" :topic="topic"
> :filter="chosenProfile"
<h4 class="mb-1 font-semibold text-gray-800"> :selected-circle="selectedCircle"
{{ topic.title }} @select-circle="selectCircle($event)"
</h4> />
<button
v-for="circle in topic.circles"
:key="circle.id"
class="flex w-full items-center space-x-2 p-2 pr-4 hover:bg-gray-200 lg:pr-8"
:class="{ 'bg-gray-200': selectedCircle === circle }"
@click="selectCircle(circle)"
>
<LearningPathCircle
:sectors="calculateCircleSectorData(circle)"
class="h-10 w-10 snap-center rounded-full bg-white p-0.5"
></LearningPathCircle>
<span>{{ circle.title }}</span>
</button>
</div>
</template> </template>
<template #main> <template #main>
<ol v-if="selectedCircle" class="flex-auto bg-gray-200 px-6 py-4 lg:px-16"> <ol v-if="selectedCircle" class="flex-auto bg-gray-200 px-6 py-4 lg:px-16">

View File

@ -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>

View File

@ -61,7 +61,7 @@ describe("Onboarding", () => {
mockNext mockNext
); );
expect(mockNext).toHaveBeenCalledWith({ expect(mockNext).toHaveBeenCalledWith({
name: "checkoutAddress", name: "accountCourseProfile",
params: { courseType: testCase }, params: { courseType: testCase },
}); });
}); });

View File

@ -417,6 +417,12 @@ const router = createRouter({
component: () => import("@/pages/onboarding/uk/SetupComplete.vue"), component: () => import("@/pages/onboarding/uk/SetupComplete.vue"),
name: "setupComplete", name: "setupComplete",
}, },
{
path: "account/course-profile",
component: () => import("@/pages/onboarding/vv/AccountCourseProfile.vue"),
name: "accountCourseProfile",
props: true,
},
{ {
path: "checkout/address", path: "checkout/address",
component: () => import("@/pages/onboarding/vv/CheckoutAddress.vue"), component: () => import("@/pages/onboarding/vv/CheckoutAddress.vue"),

View File

@ -13,14 +13,21 @@ export type Country = {
name: string; name: string;
}; };
export type CourseProfile = {
id: number;
code: string;
};
export function useEntities() { export function useEntities() {
const countries: Ref<Country[]> = ref([]); const countries: Ref<Country[]> = ref([]);
const organisations: Ref<Organisation[]> = ref([]); const organisations: Ref<Organisation[]> = ref([]);
const courseProfiles: Ref<CourseProfile[]> = ref([]);
itGetCached("/api/core/entities/").then((res: any) => { itGetCached("/api/core/entities/").then((res: any) => {
countries.value = res.countries; countries.value = res.countries;
organisations.value = res.organisations; organisations.value = res.organisations;
courseProfiles.value = res.courseProfiles;
}); });
return { organisations, countries }; return { organisations, countries, courseProfiles };
} }

View File

@ -11,7 +11,7 @@ export function profileNextRoute(courseType: string | string[]) {
} }
// vv- -> vv-de, vv-fr or vv-it // vv- -> vv-de, vv-fr or vv-it
if (isString(courseType) && startsWith(courseType, "vv-")) { if (isString(courseType) && startsWith(courseType, "vv-")) {
return "checkoutAddress"; return "accountCourseProfile";
} }
return ""; return "";
} }

View File

@ -1,6 +1,6 @@
import { bustItGetCache, itGetCached, itPost } from "@/fetchHelpers"; import { bustItGetCache, itGetCached, itPost } from "@/fetchHelpers";
import { setI18nLanguage } from "@/i18nextWrapper"; import { setI18nLanguage } from "@/i18nextWrapper";
import type { Country } from "@/services/entities"; import type { Country, CourseProfile } from "@/services/entities";
import { directUpload } from "@/services/files"; import { directUpload } from "@/services/files";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { defineStore } from "pinia"; import { defineStore } from "pinia";
@ -44,6 +44,7 @@ export interface User {
organisation_postal_code: string; organisation_postal_code: string;
organisation_city: string; organisation_city: string;
organisation_country: Country | null; organisation_country: Country | null;
chosen_profile?: CourseProfile;
} }
let defaultLanguage: AvailableLanguages = "de"; let defaultLanguage: AvailableLanguages = "de";
@ -89,6 +90,7 @@ const initialUserState: User = {
organisation_postal_code: "", organisation_postal_code: "",
organisation_city: "", organisation_city: "",
organisation_country: null, organisation_country: null,
chosen_profile: undefined,
}; };
async function setLocale(language: AvailableLanguages) { async function setLocale(language: AvailableLanguages) {
@ -176,5 +178,8 @@ export const useUserStore = defineStore({
await itPost("/api/core/me/", profileData, { method: "PUT" }); await itPost("/api/core/me/", profileData, { method: "PUT" });
Object.assign(this.$state, profileData); Object.assign(this.$state, profileData);
}, },
updateChosenCourseProfile(courseProfile: CourseProfile) {
Object.assign(this.$state, { chosen_profile: courseProfile });
},
}, },
}); });

View File

@ -202,6 +202,7 @@ export interface Course {
title: string; title: string;
category_name: string; category_name: string;
slug: string; slug: string;
profiles: string[];
configuration: CourseConfiguration; configuration: CourseConfiguration;
} }
@ -408,6 +409,10 @@ export interface CompetenceCertificateAssignment extends BaseCourseWagtailPage {
evaluation_points_reason: string; evaluation_points_reason: string;
evaluation_max_points: number | null; evaluation_max_points: number | null;
evaluation_passed: boolean | null; evaluation_passed: boolean | null;
course_session: {
id: string;
title: string;
};
}[]; }[];
} }

View File

@ -93,7 +93,7 @@ textarea {
} }
.link { .link {
@apply underline underline-offset-2; @apply cursor-pointer underline underline-offset-2;
} }
.link-large { .link-large {
@ -167,6 +167,14 @@ textarea {
top: 1rem; top: 1rem;
transform: translateY(-50%); transform: translateY(-50%);
} }
.tag-inactive {
@apply rounded-full border-2 border-blue-900 px-4 py-2 font-semibold text-blue-900;
}
.tag-active {
@apply rounded-full bg-blue-900 px-4 py-2 font-semibold text-white;
}
} }
@layer utilities { @layer utilities {
@ -181,7 +189,9 @@ textarea {
} }
.no-scrollbar { .no-scrollbar {
-ms-overflow-style: none; /* IE and Edge */ -ms-overflow-style: none;
scrollbar-width: none; /* Firefox */ /* IE and Edge */
scrollbar-width: none;
/* Firefox */
} }
} }

View File

@ -1,16 +1,22 @@
// ids for cypress test data // ids for cypress test data
export const ADMIN_USER_ID = "872efd96-3bd7-4a1e-a239-2d72cad9f604" export const ADMIN_USER_ID = "872efd96-3bd7-4a1e-a239-2d72cad9f604";
export const TEST_SUPERVISOR1_USER_ID = "a9a8b741-f115-4521-af2d-7dfef673b8c5" export const TEST_SUPERVISOR1_USER_ID = "a9a8b741-f115-4521-af2d-7dfef673b8c5";
export const TEST_TRAINER1_USER_ID = "b9e71f59-c44f-4290-b93a-9b3151e9a2fc" export const TEST_TRAINER1_USER_ID = "b9e71f59-c44f-4290-b93a-9b3151e9a2fc";
export const TEST_TRAINER2_USER_ID = "299941ae-1e4b-4f45-8180-876c3ad340b4" export const TEST_TRAINER2_USER_ID = "299941ae-1e4b-4f45-8180-876c3ad340b4";
export const TEST_STUDENT1_USER_ID = "65c73ad0-6d53-43a9-a4a4-64143f27b03a" export const TEST_STUDENT1_USER_ID = "65c73ad0-6d53-43a9-a4a4-64143f27b03a";
export const TEST_STUDENT2_USER_ID = "19c40d94-15cc-4198-aaad-ef707c4b0900" export const TEST_STUDENT2_USER_ID = "19c40d94-15cc-4198-aaad-ef707c4b0900";
export const TEST_STUDENT3_USER_ID = "bcf94dba-53bc-474b-a22d-e4af39aa042b" export const TEST_STUDENT3_USER_ID = "bcf94dba-53bc-474b-a22d-e4af39aa042b";
export const TEST_MENTOR1_USER_ID = "d1f5f5a9-5b0a-4e1a-9e1a-9e9b5b5e1b1b" export const TEST_MENTOR1_USER_ID = "d1f5f5a9-5b0a-4e1a-9e1a-9e9b5b5e1b1b";
export const TEST_STUDENT1_VV_USER_ID = "5ff59857-8de5-415e-a387-4449f9a0337a" export const TEST_STUDENT1_VV_USER_ID = "5ff59857-8de5-415e-a387-4449f9a0337a";
export const TEST_STUDENT2_VV_AND_VV_MENTOR_USER_ID = "7e8ebf0b-e6e2-4022-88f4-6e663ba0a9db" export const TEST_STUDENT2_VV_AND_VV_MENTOR_USER_ID =
export const TEST_USER_EMPTY_ID = "daecbabe-4ab9-4edf-a71f-4119042ccb02" "7e8ebf0b-e6e2-4022-88f4-6e663ba0a9db";
export const TEST_USER_EMPTY_ID = "daecbabe-4ab9-4edf-a71f-4119042ccb02";
export const TEST_COURSE_SESSION_BERN_ID = -1; export const TEST_COURSE_SESSION_BERN_ID = -1;
export const TEST_COURSE_SESSION_ZURICH_ID = -2; export const TEST_COURSE_SESSION_ZURICH_ID = -2;
export const TEST_COURSE_SESSION_VV_ID = 1;
export const COURSE_PROFILE_LEBEN_ID = -1;
export const COURSE_PROFILE_NICHTLEBEN_ID = -2;
export const COURSE_PROFILE_KRANKENZUSATZ_ID = -3;
export const COURSE_PROFILE_ALL_ID = -99;

View File

@ -1,4 +1,9 @@
import { TEST_USER_EMPTY_ID } from "../../consts"; import {
COURSE_PROFILE_ALL_ID,
COURSE_PROFILE_NICHTLEBEN_ID,
TEST_COURSE_SESSION_VV_ID,
TEST_USER_EMPTY_ID,
} from "../../consts";
import { login } from "../helpers"; import { login } from "../helpers";
describe("checkout.cy.js", () => { describe("checkout.cy.js", () => {
@ -32,6 +37,15 @@ describe("checkout.cy.js", () => {
cy.get("#organisationDetailName").type("FdH GmbH"); cy.get("#organisationDetailName").type("FdH GmbH");
cy.get('[data-cy="continue-button"]').click(); cy.get('[data-cy="continue-button"]').click();
cy.get('[data-cy="account-course-profile-title"]').should(
"have.text",
"Zulassungsprofil auswählen",
);
cy.get('[data-cy="dropdown-select"]').click();
cy.get('[data-cy="dropdown-select-option-Nichtleben"]').click();
cy.get('[data-cy="continue-button"]').click();
cy.loadUser("id", TEST_USER_EMPTY_ID).then((u) => { cy.loadUser("id", TEST_USER_EMPTY_ID).then((u) => {
expect(u.organisation_detail_name).to.equal("FdH GmbH"); expect(u.organisation_detail_name).to.equal("FdH GmbH");
// 2 -> andere Krankenversicherer // 2 -> andere Krankenversicherer
@ -121,6 +135,12 @@ describe("checkout.cy.js", () => {
cy.loadCheckoutInformation("user_id", TEST_USER_EMPTY_ID).then((ci) => { cy.loadCheckoutInformation("user_id", TEST_USER_EMPTY_ID).then((ci) => {
expect(ci.state).to.equal("paid"); expect(ci.state).to.equal("paid");
}); });
cy.loadCourseSessionUser("user_id", TEST_USER_EMPTY_ID).then((csu) => {
expect(csu.role).to.equal("MEMBER");
expect(csu.course_session).to.equal(TEST_COURSE_SESSION_VV_ID);
expect(csu.chosen_profile).to.equal(COURSE_PROFILE_NICHTLEBEN_ID);
});
}); });
it("can checkout and pay Versicherungsvermittlerin with Cembra invoice", () => { it("can checkout and pay Versicherungsvermittlerin with Cembra invoice", () => {
@ -143,6 +163,15 @@ describe("checkout.cy.js", () => {
cy.get('[data-cy="dropdown-select-option-Baloise"]').click(); cy.get('[data-cy="dropdown-select-option-Baloise"]').click();
cy.get('[data-cy="continue-button"]').click(); cy.get('[data-cy="continue-button"]').click();
cy.get('[data-cy="account-course-profile-title"]').should(
"have.text",
"Zulassungsprofil auswählen",
);
cy.get('[data-cy="dropdown-select"]').click();
cy.get('[data-cy="dropdown-select-option-Allbranche"]').click();
cy.get('[data-cy="continue-button"]').click();
// Adressdaten ausfüllen // Adressdaten ausfüllen
cy.get('[data-cy="account-checkout-title"]').should( cy.get('[data-cy="account-checkout-title"]').should(
"contain", "contain",
@ -236,5 +265,32 @@ describe("checkout.cy.js", () => {
// 7 -> Baloise // 7 -> Baloise
expect(u.organisation).to.equal(7); expect(u.organisation).to.equal(7);
}); });
// pay
cy.get('[data-cy="pay-button"]').click();
cy.get('[data-cy="checkout-success-title"]').should(
"contain",
"Gratuliere",
);
// wait for payment callback
cy.wait(3000);
cy.get('[data-cy="start-vv-button"]').click();
// back on dashboard page
cy.get('[data-cy="db-course-title"]').should(
"contain",
"Versicherungsvermittler",
);
cy.loadCheckoutInformation("user_id", TEST_USER_EMPTY_ID).then((ci) => {
expect(ci.state).to.equal("paid");
});
cy.loadCourseSessionUser("user_id", TEST_USER_EMPTY_ID).then((csu) => {
expect(csu.role).to.equal("MEMBER");
expect(csu.course_session).to.equal(TEST_COURSE_SESSION_VV_ID);
expect(csu.chosen_profile).to.equal(COURSE_PROFILE_ALL_ID);
});
}); });
}); });

View File

@ -1,26 +1,26 @@
import { login } from "../helpers"; import { login } from "../helpers"
describe("competenceCertificate.cy.js", () => { describe("competenceCertificate.cy.js", () => {
beforeEach(() => {}); beforeEach(() => { })
it("check without points", () => { it("check without points", () => {
cy.manageCommand("cypress_reset"); cy.manageCommand("cypress_reset")
login("test-student1@example.com", "test"); login("test-student1@example.com", "test")
cy.visit("/course/test-lehrgang/competence"); cy.visit("/course/test-lehrgang/competence")
cy.get('[data-cy="certificate-total-points-text"]').contains( cy.get('[data-cy="certificate-total-points-text"]').contains(
"Der Punktestand wird zu einem späteren Zeitpunkt berechnet." "Der Punktestand wird zu einem späteren Zeitpunkt berechnet."
); )
cy.get( cy.get(
'[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1"]' '[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1"]'
).and("contain", "0 von 2 Kompetenznachweis-Elementen"); ).and("contain", "0 von 2 Kompetenznachweis-Elementen")
// check on certificates page // check on certificates page
cy.get('[data-cy="certificates-show-all-button"]').click(); cy.get('[data-cy="certificates-show-all-button"]').click()
cy.get('[data-cy="certificate-total-points-text"]').contains( cy.get('[data-cy="certificate-total-points-text"]').contains(
"Der Punktestand wird zu einem späteren Zeitpunkt berechnet." "Der Punktestand wird zu einem späteren Zeitpunkt berechnet."
); )
cy.get( cy.get(
'[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1"]' '[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1"]'
@ -29,100 +29,100 @@ describe("competenceCertificate.cy.js", () => {
"contain", "contain",
"Der Punktestand wird zu einem späteren Zeitpunkt berechnet." "Der Punktestand wird zu einem späteren Zeitpunkt berechnet."
) )
.and("contain", "0 von 2 Kompetenznachweis-Elementen"); .and("contain", "0 von 2 Kompetenznachweis-Elementen")
// check certificate detail page // check certificate detail page
cy.get( cy.get(
'[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1-detail-link"]' '[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1-detail-link"]'
).click(); ).click()
cy.get( cy.get(
'[data-cy="assignment-test-lehrgang-assignment-überprüfen-einer-motorfahrzeugs-versicherungspolice"]' '[data-cy="assignment-test-lehrgang-assignment-überprüfen-einer-motorfahrzeugs-versicherungspolice"]'
).should("contain", "Höchstpunktzahl"); ).should("contain", "Höchstpunktzahl")
cy.get( cy.get(
'[data-cy="assignment-test-lehrgang-assignment-edoniq-wissens-und-verständisfragen-circle-fahrzeug-demo"]' '[data-cy="assignment-test-lehrgang-assignment-edoniq-wissens-und-verständisfragen-circle-fahrzeug-demo"]'
).should("contain", "Höchstpunktzahl"); ).should("contain", "Höchstpunktzahl")
}); })
it("check with finished passed edoniq test", () => { it("check with finished passed edoniq test", () => {
cy.manageCommand( cy.manageCommand(
"cypress_reset --create-assignment-completion --create-edoniq-test-results 19 24 0" "cypress_reset --create-assignment-completion --create-edoniq-test-results 19 24 0"
); )
login("test-student1@example.com", "test"); login("test-student1@example.com", "test")
cy.visit("/course/test-lehrgang/competence"); cy.visit("/course/test-lehrgang/competence")
cy.get('[data-cy="certificate-total-points-text"]').contains( cy.get('[data-cy="certificate-total-points-text"]').contains(
"Erfahrungsnote üK: 5" "Erfahrungsnote üK: 5"
); )
cy.get( cy.get(
'[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1"]' '[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1"]'
) )
.should("contain", "Note: 5") .should("contain", "Note: 5")
.and("contain", "1 von 2 Kompetenznachweis-Elementen"); .and("contain", "1 von 2 Kompetenznachweis-Elementen")
// check on certificates page // check on certificates page
cy.get('[data-cy="certificates-show-all-button"]').click(); cy.get('[data-cy="certificates-show-all-button"]').click()
cy.get('[data-cy="certificate-total-points-text"]') cy.get('[data-cy="certificate-total-points-text"]')
.should("contain", "Erfahrungsnote üK") .should("contain", "Erfahrungsnote üK")
.and("contain", "Zwischenstand"); .and("contain", "Zwischenstand")
cy.get('[data-cy="certificate-total-grade"]').should("contain", "Note: 5"); cy.get('[data-cy="certificate-total-grade"]').should("contain", "Note: 5")
cy.get( cy.get(
'[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1-grade"]' '[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1-grade"]'
).should("contain", "Note: 5"); ).should("contain", "Note: 5")
cy.get( cy.get(
'[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1-grade-percent"]' '[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1-grade-percent"]'
).should("contain", "Ungerundete Note: 4.96"); ).should("contain", "Ungerundete Note: 4.96")
cy.get( cy.get(
'[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1"]' '[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1"]'
) )
.and("contain", "Zwischenstand") .and("contain", "Zwischenstand")
.and("contain", "1 von 2 Kompetenznachweis-Elementen"); .and("contain", "1 von 2 Kompetenznachweis-Elementen")
// check certificate detail page // check certificate detail page
cy.get( cy.get(
'[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1-detail-link"]' '[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1-detail-link"]'
).click(); ).click()
cy.get( cy.get(
'[data-cy="assignment-test-lehrgang-assignment-überprüfen-einer-motorfahrzeugs-versicherungspolice"]' '[data-cy="assignment-test-lehrgang-assignment-überprüfen-einer-motorfahrzeugs-versicherungspolice"]'
) )
.should("contain", "Höchstpunktzahl") .should("contain", "Höchstpunktzahl")
.and("contain", "Ergebnisse abgegeben"); .and("contain", "Ergebnisse abgegeben")
cy.get( cy.get(
'[data-cy="assignment-test-lehrgang-assignment-edoniq-wissens-und-verständisfragen-circle-fahrzeug-demo"]' '[data-cy="assignment-test-lehrgang-assignment-edoniq-wissens-und-verständisfragen-circle-fahrzeug-demo"]'
) )
.should("contain", "19") .should("contain", "19")
.and("contain", "Bewertung freigegeben") .and("contain", "Bewertung freigegeben")
.and("not.contain", "Nicht Bestanden"); .and("not.contain", "Nicht Bestanden")
// it can open learning content page directly // it can open learning content page directly
cy.get( cy.get(
'[data-cy="assignment-test-lehrgang-assignment-edoniq-wissens-und-verständisfragen-circle-fahrzeug-demo"] [data-cy="open-learning-content"]' '[data-cy="assignment-test-lehrgang-assignment-edoniq-wissens-und-verständisfragen-circle-fahrzeug-demo"] [data-cy="open-learning-content"]'
).click(); ).click()
cy.get('[data-cy="test-result"]') cy.get('[data-cy="test-result"]')
.should("contain", "19 von 24 Punkten") .should("contain", "19 von 24 Punkten")
.and("contain", "79%"); .and("contain", "79%")
}); })
it("check with finished failed edoniq test", () => { it("check with finished failed edoniq test", () => {
cy.manageCommand( cy.manageCommand(
"cypress_reset --create-assignment-completion --create-edoniq-test-results 10 24 0" "cypress_reset --create-assignment-completion --create-edoniq-test-results 10 24 0"
); )
login("test-student1@example.com", "test"); login("test-student1@example.com", "test")
// go to certificate detail page // go to certificate detail page
cy.visit( cy.visit(
"/course/test-lehrgang/competence/certificates/kompetenznachweis-1" "/course/test-lehrgang/competence/certificates/kompetenznachweis-1"
); )
cy.get( cy.get(
'[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1-grade"]' '[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1-grade"]'
).should("contain", "Note: 3"); ).should("contain", "Note: 3")
cy.get( cy.get(
'[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1-grade-percent"]' '[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1-grade-percent"]'
).should("contain", "Ungerundete Note: 3.08"); ).should("contain", "Ungerundete Note: 3.08")
cy.get( cy.get(
'[data-cy="assignment-test-lehrgang-assignment-edoniq-wissens-und-verständisfragen-circle-fahrzeug-demo"]' '[data-cy="assignment-test-lehrgang-assignment-edoniq-wissens-und-verständisfragen-circle-fahrzeug-demo"]'
@ -130,90 +130,170 @@ describe("competenceCertificate.cy.js", () => {
.should("contain", "10") .should("contain", "10")
.and("contain", "Bewertung freigegeben") .and("contain", "Bewertung freigegeben")
.and("contain", "42%") .and("contain", "42%")
.and("contain", "Nicht bestanden"); .and("contain", "Nicht bestanden")
// it can open learning content page directly // it can open learning content page directly
cy.get( cy.get(
'[data-cy="assignment-test-lehrgang-assignment-edoniq-wissens-und-verständisfragen-circle-fahrzeug-demo"] [data-cy="open-learning-content"]' '[data-cy="assignment-test-lehrgang-assignment-edoniq-wissens-und-verständisfragen-circle-fahrzeug-demo"] [data-cy="open-learning-content"]'
).click(); ).click()
cy.get('[data-cy="test-result"]') cy.get('[data-cy="test-result"]')
.should("contain", "10 von 24 Punkten") .should("contain", "10 von 24 Punkten")
.and("contain", "42%") .and("contain", "42%")
.and("contain", "Nicht bestanden"); .and("contain", "Nicht bestanden")
}); })
it("check with finished edoniq test and finished casework", () => { it("check with finished edoniq test and finished casework", () => {
cy.manageCommand( cy.manageCommand(
"cypress_reset --create-assignment-evaluation --create-edoniq-test-results 19 24 0" "cypress_reset --create-assignment-evaluation --create-edoniq-test-results 19 24 0"
); )
login("test-student1@example.com", "test"); login("test-student1@example.com", "test")
cy.visit("/course/test-lehrgang/competence"); cy.visit("/course/test-lehrgang/competence")
cy.get('[data-cy="certificate-total-points-text"]').contains( cy.get('[data-cy="certificate-total-points-text"]').contains(
"Erfahrungsnote üK: 5.5" "Erfahrungsnote üK: 5.5"
); )
cy.get( cy.get(
'[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1"]' '[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1"]'
) )
.should("contain", "Note: 5.5") .should("contain", "Note: 5.5")
.and("contain", "2 von 2 Kompetenznachweis-Elementen"); .and("contain", "2 von 2 Kompetenznachweis-Elementen")
// check on certificates page // check on certificates page
cy.get('[data-cy="certificates-show-all-button"]').click(); cy.get('[data-cy="certificates-show-all-button"]').click()
cy.get('[data-cy="certificate-total-points-text"]') cy.get('[data-cy="certificate-total-points-text"]')
.should("contain", "Erfahrungsnote üK") .should("contain", "Erfahrungsnote üK")
.and("contain", "Note: 5.5") .and("contain", "Note: 5.5")
.and("not.contain", "Zwischenstand"); .and("not.contain", "Zwischenstand")
cy.get( cy.get(
'[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1"]' '[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1"]'
) )
.and("not.contain", "Zwischenstand") .and("not.contain", "Zwischenstand")
.and("contain", "2 von 2 Kompetenznachweis-Elementen"); .and("contain", "2 von 2 Kompetenznachweis-Elementen")
cy.get( cy.get(
'[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1-grade"]' '[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1-grade"]'
).should("contain", "Note: 5.5"); ).should("contain", "Note: 5.5")
cy.get( cy.get(
'[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1-grade-percent"]' '[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1-grade-percent"]'
).should("contain", "Ungerundete Note: 5.48"); ).should("contain", "Ungerundete Note: 5.48")
// check certificate detail page // check certificate detail page
cy.get( cy.get(
'[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1-detail-link"]' '[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1-detail-link"]'
).click(); ).click()
cy.get( cy.get(
'[data-cy="assignment-test-lehrgang-assignment-überprüfen-einer-motorfahrzeugs-versicherungspolice"]' '[data-cy="assignment-test-lehrgang-assignment-überprüfen-einer-motorfahrzeugs-versicherungspolice"]'
) )
.should("contain", "24") .should("contain", "24")
.and("contain", "von 24 Punkten") .and("contain", "von 24 Punkten")
.and("contain", "Bewertung freigegeben"); .and("contain", "Bewertung freigegeben")
cy.get( cy.get(
'[data-cy="assignment-test-lehrgang-assignment-edoniq-wissens-und-verständisfragen-circle-fahrzeug-demo"]' '[data-cy="assignment-test-lehrgang-assignment-edoniq-wissens-und-verständisfragen-circle-fahrzeug-demo"]'
) )
.should("contain", "19") .should("contain", "19")
.and("contain", "von 24 Punkten") .and("contain", "von 24 Punkten")
.and("contain", "Bewertung freigegeben"); .and("contain", "Bewertung freigegeben")
}); })
it("check with finished edoniq test and finished casework in different course sessions", () => {
const TEST_TRAINER2_USER_ID = "299941ae-1e4b-4f45-8180-876c3ad340b4"
const TEST_STUDENT2_USER_ID = "19c40d94-15cc-4198-aaad-ef707c4b0900"
const TEST_COURSE_SESSION_ZURICH_ID = -2
cy.manageCommand(
`cypress_reset --create-assignment-evaluation --assignment-evaluation-user-id ${TEST_TRAINER2_USER_ID} --assignment-completion-user-id ${TEST_STUDENT2_USER_ID} --edoniq-user-id ${TEST_STUDENT2_USER_ID} --edoniq-course-session-id '${TEST_COURSE_SESSION_ZURICH_ID}' --create-edoniq-test-results 19 24 0`
)
login("test-student2@example.com", "test")
cy.visit("/course/test-lehrgang/competence")
cy.get('[data-cy="certificate-total-points-text"]').contains(
"Erfahrungsnote üK: 5.5"
)
cy.get(
'[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1"]'
)
.should("contain", "Note: 5.5")
.and("contain", "2 von 2 Kompetenznachweis-Elementen")
// check on certificates page
cy.get('[data-cy="certificates-show-all-button"]').click()
cy.get('[data-cy="certificate-total-points-text"]')
.should("contain", "Erfahrungsnote üK")
.and("contain", "Note: 5.5")
.and("not.contain", "Zwischenstand")
cy.get(
'[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1"]'
)
.and("not.contain", "Zwischenstand")
.and("contain", "2 von 2 Kompetenznachweis-Elementen")
cy.get(
'[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1-grade"]'
).should("contain", "Note: 5.5")
cy.get(
'[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1-grade-percent"]'
).should("contain", "Ungerundete Note: 5.48")
// check certificate detail page
cy.get(
'[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1-detail-link"]'
).click()
cy.get(
'[data-cy="assignment-test-lehrgang-assignment-überprüfen-einer-motorfahrzeugs-versicherungspolice"]'
)
.should("contain", "24")
.and("contain", "von 24 Punkten")
.and("contain", "Bewertung freigegeben")
cy.get(
'[data-cy="assignment-test-lehrgang-assignment-edoniq-wissens-und-verständisfragen-circle-fahrzeug-demo"]'
)
.should("contain", "19")
.and("contain", "von 24 Punkten")
.and("contain", "Bewertung freigegeben")
cy.get('[data-cy="assignment-test-lehrgang-assignment-überprüfen-einer-motorfahrzeugs-versicherungspolice-course-session"]').should("contain", "Test Bern 2022 a")
cy.get('[data-cy="assignment-test-lehrgang-assignment-edoniq-wissens-und-verständisfragen-circle-fahrzeug-demo-course-session"]').should("contain", "Test Zürich 2022 a")
})
it("check show assignment in different course session", () => {
const TEST_TRAINER2_USER_ID = "299941ae-1e4b-4f45-8180-876c3ad340b4"
const TEST_STUDENT2_USER_ID = "19c40d94-15cc-4198-aaad-ef707c4b0900"
const TEST_COURSE_SESSION_ZURICH_ID = -2
cy.manageCommand(
`cypress_reset --create-assignment-evaluation --assignment-evaluation-user-id ${TEST_TRAINER2_USER_ID} --assignment-completion-user-id ${TEST_STUDENT2_USER_ID} --edoniq-user-id ${TEST_STUDENT2_USER_ID} --edoniq-course-session-id '${TEST_COURSE_SESSION_ZURICH_ID}' --create-edoniq-test-results 19 24 0`
)
login("test-student2@example.com", "test")
cy.visit("course/test-lehrgang/competence/certificates/kompetenznachweis-1")
cy.get('[data-cy="current-course-session-title"]').should("contain", "Test Bern 2022 a")
cy.get(
'[data-cy="assignment-test-lehrgang-assignment-edoniq-wissens-und-verständisfragen-circle-fahrzeug-demo"] [data-cy="open-learning-content"]'
).click()
cy.get('[data-cy="current-course-session-title"]').should("contain", "Test Zürich 2022 a")
})
it("check with finished edoniq test with deducted points", () => { it("check with finished edoniq test with deducted points", () => {
cy.manageCommand( cy.manageCommand(
"cypress_reset --create-assignment-completion --create-edoniq-test-results 19 24 8" "cypress_reset --create-assignment-completion --create-edoniq-test-results 19 24 8"
); )
login("test-student1@example.com", "test"); login("test-student1@example.com", "test")
// go to certificate detail page // go to certificate detail page
cy.visit( cy.visit(
"/course/test-lehrgang/competence/certificates/kompetenznachweis-1" "/course/test-lehrgang/competence/certificates/kompetenznachweis-1"
); )
cy.get( cy.get(
'[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1-grade"]' '[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1-grade"]'
).should("contain", "Note: 3.5"); ).should("contain", "Note: 3.5")
cy.get( cy.get(
'[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1-grade-percent"]' '[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1-grade-percent"]'
).should("contain", "Ungerundete Note: 3.29"); ).should("contain", "Ungerundete Note: 3.29")
cy.get( cy.get(
'[data-cy="assignment-test-lehrgang-assignment-edoniq-wissens-und-verständisfragen-circle-fahrzeug-demo"]' '[data-cy="assignment-test-lehrgang-assignment-edoniq-wissens-und-verständisfragen-circle-fahrzeug-demo"]'
@ -222,61 +302,61 @@ describe("competenceCertificate.cy.js", () => {
.and("contain", "Bewertung freigegeben") .and("contain", "Bewertung freigegeben")
.and("contain", "46%") .and("contain", "46%")
.and("contain", "mit Abzug") .and("contain", "mit Abzug")
.and("contain", "Nicht bestanden"); .and("contain", "Nicht bestanden")
// it can open learning content page directly // it can open learning content page directly
cy.get( cy.get(
'[data-cy="assignment-test-lehrgang-assignment-edoniq-wissens-und-verständisfragen-circle-fahrzeug-demo"] [data-cy="open-learning-content"]' '[data-cy="assignment-test-lehrgang-assignment-edoniq-wissens-und-verständisfragen-circle-fahrzeug-demo"] [data-cy="open-learning-content"]'
).click(); ).click()
cy.get('[data-cy="test-result"]') cy.get('[data-cy="test-result"]')
.should("contain", "11 von 24 Punkten") .should("contain", "11 von 24 Punkten")
.and("contain", "46%") .and("contain", "46%")
.and("contain", "Punkte aus Bewertung: 19") .and("contain", "Punkte aus Bewertung: 19")
.and("contain", "Abgezogene Punkte: 8") .and("contain", "Abgezogene Punkte: 8")
.and("contain", "Grund: Edoniq Punkteabzug Test") .and("contain", "Grund: Edoniq Punkteabzug Test")
.and("contain", "Nicht bestanden"); .and("contain", "Nicht bestanden")
}); })
it("check with finished casework and points deducted", () => { it("check with finished casework and points deducted", () => {
cy.manageCommand( cy.manageCommand(
"cypress_reset --create-assignment-evaluation --assignment-evaluation-scores 4,6,4,3,2 --assignment-points-deducted 5" "cypress_reset --create-assignment-evaluation --assignment-evaluation-scores 4,6,4,3,2 --assignment-points-deducted 5"
); )
login("test-student1@example.com", "test"); login("test-student1@example.com", "test")
cy.visit("/course/test-lehrgang/competence"); cy.visit("/course/test-lehrgang/competence")
cy.get('[data-cy="certificate-total-points-text"]').contains( cy.get('[data-cy="certificate-total-points-text"]').contains(
"Erfahrungsnote üK: 4" "Erfahrungsnote üK: 4"
); )
cy.get( cy.get(
'[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1"]' '[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1"]'
) )
.should("contain", "Note: 4") .should("contain", "Note: 4")
.and("contain", "1 von 2 Kompetenznachweis-Elementen"); .and("contain", "1 von 2 Kompetenznachweis-Elementen")
// check on certificates page // check on certificates page
cy.get('[data-cy="certificates-show-all-button"]').click(); cy.get('[data-cy="certificates-show-all-button"]').click()
cy.get('[data-cy="certificate-total-points-text"]') cy.get('[data-cy="certificate-total-points-text"]')
.should("contain", "Erfahrungsnote üK") .should("contain", "Erfahrungsnote üK")
.and("contain", "Note: 4") .and("contain", "Note: 4")
.and("contain", "Zwischenstand"); .and("contain", "Zwischenstand")
cy.get( cy.get(
'[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1"]' '[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1"]'
) )
.and("contain", "Zwischenstand") .and("contain", "Zwischenstand")
.and("contain", "1 von 2 Kompetenznachweis-Elementen"); .and("contain", "1 von 2 Kompetenznachweis-Elementen")
cy.get( cy.get(
'[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1-grade"]' '[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1-grade"]'
).should("contain", "Note: 4"); ).should("contain", "Note: 4")
cy.get( cy.get(
'[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1-grade-percent"]' '[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1-grade-percent"]'
).should("contain", "Ungerundete Note: 3.92"); ).should("contain", "Ungerundete Note: 3.92")
// check certificate detail page // check certificate detail page
cy.get( cy.get(
'[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1-detail-link"]' '[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1-detail-link"]'
).click(); ).click()
cy.get( cy.get(
'[data-cy="assignment-test-lehrgang-assignment-überprüfen-einer-motorfahrzeugs-versicherungspolice"]' '[data-cy="assignment-test-lehrgang-assignment-überprüfen-einer-motorfahrzeugs-versicherungspolice"]'
@ -285,28 +365,28 @@ describe("competenceCertificate.cy.js", () => {
.and("contain", "von 24 Punkten") .and("contain", "von 24 Punkten")
.and("contain", "58%") .and("contain", "58%")
.and("contain", "mit Abzug") .and("contain", "mit Abzug")
.and("contain", "Bewertung freigegeben"); .and("contain", "Bewertung freigegeben")
cy.get( cy.get(
'[data-cy="assignment-test-lehrgang-assignment-überprüfen-einer-motorfahrzeugs-versicherungspolice"] [data-cy="open-learning-content"]' '[data-cy="assignment-test-lehrgang-assignment-überprüfen-einer-motorfahrzeugs-versicherungspolice"] [data-cy="open-learning-content"]'
).click(); ).click()
cy.get('[data-cy="user-points"]').should("contain", "14"); cy.get('[data-cy="user-points"]').should("contain", "14")
cy.get('[data-cy="total-points"]').should( cy.get('[data-cy="total-points"]').should(
"contain", "contain",
"von 24 Punkten (58%)" "von 24 Punkten (58%)"
); )
cy.get('[data-cy="points-deducted"]') cy.get('[data-cy="points-deducted"]')
.should("contain", "Punkte aus Bewertung: 19") .should("contain", "Punkte aus Bewertung: 19")
.and("contain", "Abgezogene Punkte: 5") .and("contain", "Abgezogene Punkte: 5")
.and("contain", "Grund: Assignment Punkteabzug Test"); .and("contain", "Grund: Assignment Punkteabzug Test")
}); })
it("should display link to details", () => { it("should display link to details", () => {
cy.manageCommand("cypress_reset"); cy.manageCommand("cypress_reset")
login("test-student1@example.com", "test"); login("test-student1@example.com", "test")
cy.visit("/course/test-lehrgang/competence/self-evaluation-and-feedback"); cy.visit("/course/test-lehrgang/competence/self-evaluation-and-feedback")
cy.get('[data-cy^="self-eval-"][data-cy$="-detail-url"]:first').contains( cy.get('[data-cy^="self-eval-"][data-cy$="-detail-url"]:first').contains(
"Selbsteinschätzung anschauen" "Selbsteinschätzung anschauen"
); )
}); })
}); })

View File

@ -50,20 +50,20 @@
// -- This is will overwrite an existing command -- // -- This is will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
const _ = Cypress._; const _ = Cypress._
Cypress.Commands.add("manageCommand", (command, preCommand = "") => { Cypress.Commands.add("manageCommand", (command, preCommand = "") => {
const execCommand = `${preCommand} python server/manage.py ${command} --settings=config.settings.test_cypress`; const execCommand = `${preCommand} python server/manage.py ${command} --settings=config.settings.test_cypress`
console.log(execCommand); console.log(execCommand)
// hack to add my asdf python instance to the path // hack to add my asdf python instance to the path
// so I can run the test directly from within IntelliJ // so I can run the test directly from within IntelliJ
let pythonPaths = [ let pythonPaths = [
"/Users/daniel/workspace/vbv_lernwelt/.direnv/python-3.10.6/bin", "/Users/daniel/workspace/vbv_lernwelt/.direnv/python-3.10.6/bin",
"/Users/eliabieri/iterativ/vbv_lernwelt/.direnv/python-3.10.6/bin", "/Users/eliabieri/iterativ/vbv_lernwelt/.direnv/python-3.10/bin",
"/Users/christiancueni/workspace/vbv_lernwelt/.direnv/python-3.10.6/bin", "/Users/christiancueni/workspace/vbv_lernwelt/.direnv/python-3.10.6/bin",
"/Users/renzo/workspace/vbv_lernwelt/.direnv/python-3.10.6/bin", "/Users/renzo/workspace/vbv_lernwelt/.direnv/python-3.10.6/bin",
]; ]
let bashCommand = `PATH=${pythonPaths.join(":")}:$PATH && ${execCommand}`; let bashCommand = `PATH=${pythonPaths.join(":")}:$PATH && ${execCommand}`
return cy return cy
.exec(`bash -c "${bashCommand}"`, { .exec(`bash -c "${bashCommand}"`, {
failOnNonZeroExit: true, failOnNonZeroExit: true,
@ -73,14 +73,14 @@ Cypress.Commands.add("manageCommand", (command, preCommand = "") => {
throw new Error(`Execution of "${command}" failed throw new Error(`Execution of "${command}" failed
Exit code: ${result.code} Exit code: ${result.code}
Stdout:\n${result.stdout} Stdout:\n${result.stdout}
Stderr:\n${result.stderr}`); Stderr:\n${result.stderr}`)
} }
}); })
}); })
Cypress.Commands.add("manageShellCommand", (command) => { Cypress.Commands.add("manageShellCommand", (command) => {
return cy.manageCommand(`shell -c '${command}'`); return cy.manageCommand(`shell -c '${command}'`)
}); })
function loadObjectJson( function loadObjectJson(
key, key,
@ -89,28 +89,28 @@ function loadObjectJson(
serializerModelPath, serializerModelPath,
valueAsString = false, valueAsString = false,
) { ) {
const djangoModel = _.last(djangoModelPath.split(".")); const djangoModel = _.last(djangoModelPath.split("."))
const djangoModelImportPath = _.initial(djangoModelPath.split(".")).join("."); const djangoModelImportPath = _.initial(djangoModelPath.split(".")).join(".")
const serializerModel = _.last(serializerModelPath.split(".")); const serializerModel = _.last(serializerModelPath.split("."))
const serializerModelImportPath = _.initial( const serializerModelImportPath = _.initial(
serializerModelPath.split("."), serializerModelPath.split("."),
).join("."); ).join(".");
let filterPart = `${key}=${value}`; let filterPart = `${key}=${value}`
if (valueAsString) { if (valueAsString) {
filterPart = `${key}=\\"${value}\\"`; filterPart = `${key}=\\"${value}\\"`
} }
if (_.isArray(key)) { if (_.isArray(key)) {
filterPart = _.zip(key, value) filterPart = _.zip(key, value)
.map(([k, v]) => { .map(([k, v]) => {
if (valueAsString) { if (valueAsString) {
return `${k}=\\"${v}\\"`; return `${k}=\\"${v}\\"`
} else { } else {
return `${k}=${v}`; return `${k}=${v}`
} }
}) })
.join(","); .join(",")
} }
const command = `from ${djangoModelImportPath} import ${djangoModel}; const command = `from ${djangoModelImportPath} import ${djangoModel};
@ -119,13 +119,13 @@ function loadObjectJson(
object = ${djangoModel}.objects.filter(${filterPart}).first(); object = ${djangoModel}.objects.filter(${filterPart}).first();
print(create_json_from_objects(object, ${serializerModel}, many=False)); print(create_json_from_objects(object, ${serializerModel}, many=False));
exit(); exit();
`.replace(/(?:\r\n|\r|\n)/g, ""); `.replace(/(?:\r\n|\r|\n)/g, "")
return cy.manageShellCommand(command).then((result) => { return cy.manageShellCommand(command).then((result) => {
const objectJson = JSON.parse(result.stdout); const objectJson = JSON.parse(result.stdout)
// console.log(command); // console.log(command);
console.log(objectJson); console.log(objectJson)
return objectJson; return objectJson
}); })
} }
Cypress.Commands.add("loadAssignmentCompletion", (key, value) => { Cypress.Commands.add("loadAssignmentCompletion", (key, value) => {
@ -138,6 +138,7 @@ Cypress.Commands.add("loadAssignmentCompletion", (key, value) => {
); );
}); });
Cypress.Commands.add("loadSecurityRequestResponseLog", (key, value) => { Cypress.Commands.add("loadSecurityRequestResponseLog", (key, value) => {
return loadObjectJson( return loadObjectJson(
key, key,
@ -148,6 +149,7 @@ Cypress.Commands.add("loadSecurityRequestResponseLog", (key, value) => {
); );
}); });
Cypress.Commands.add("loadExternalApiRequestLog", (key, value) => { Cypress.Commands.add("loadExternalApiRequestLog", (key, value) => {
return loadObjectJson( return loadObjectJson(
key, key,
@ -158,6 +160,7 @@ Cypress.Commands.add("loadExternalApiRequestLog", (key, value) => {
); );
}); });
Cypress.Commands.add("loadFeedbackResponse", (key, value) => { Cypress.Commands.add("loadFeedbackResponse", (key, value) => {
return loadObjectJson( return loadObjectJson(
key, key,
@ -168,6 +171,7 @@ Cypress.Commands.add("loadFeedbackResponse", (key, value) => {
); );
}); });
Cypress.Commands.add("loadCheckoutInformation", (key, value) => { Cypress.Commands.add("loadCheckoutInformation", (key, value) => {
return loadObjectJson( return loadObjectJson(
key, key,
@ -178,6 +182,7 @@ Cypress.Commands.add("loadCheckoutInformation", (key, value) => {
); );
}); });
Cypress.Commands.add("loadUser", (key, value) => { Cypress.Commands.add("loadUser", (key, value) => {
return loadObjectJson( return loadObjectJson(
key, key,
@ -188,6 +193,15 @@ 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, withCompletion = true) => { Cypress.Commands.add("makeSelfEvaluation", (answers, withCompletion = true) => {
for (let i = 0; i < answers.length; i++) { for (let i = 0; i < answers.length; i++) {
@ -200,28 +214,28 @@ Cypress.Commands.add("makeSelfEvaluation", (answers, withCompletion = true) => {
if (withCompletion) { if (withCompletion) {
if (i < answers.length - 1) { if (i < answers.length - 1) {
cy.get('[data-cy="next-step"]').click({ force: true }); cy.get('[data-cy="next-step"]').click({force: true});
} else { } else {
cy.get('[data-cy="complete-and-continue"]').click({ force: true }); cy.get('[data-cy="complete-and-continue"]').click({force: true});
} }
} else { } else {
cy.get('[data-cy="next-step"]').click({ force: true }); cy.get('[data-cy="next-step"]').click({force: true});
} }
} }
}); });
Cypress.Commands.add("learningContentMultiLayoutNextStep", () => { Cypress.Commands.add("learningContentMultiLayoutNextStep", () => {
return cy.get('[data-cy="next-step"]').click({ force: true }); return cy.get('[data-cy="next-step"]').click({force: true})
}); })
Cypress.Commands.add("learningContentMultiLayoutPreviousStep", () => { Cypress.Commands.add("learningContentMultiLayoutPreviousStep", () => {
return cy.get('[data-cy="previous-step"]').click({ force: true }); return cy.get('[data-cy="previous-step"]').click({force: true})
}); })
Cypress.Commands.add("testLearningContentTitle", (title) => { Cypress.Commands.add("testLearningContentTitle", (title) => {
return cy.get('[data-cy="lc-title"]').should("contain", title); return cy.get('[data-cy="lc-title"]').should("contain", title)
}); })
Cypress.Commands.add("testLearningContentSubtitle", (subtitle) => { Cypress.Commands.add("testLearningContentSubtitle", (subtitle) => {
return cy.get('[data-cy="lc-subtitle"]').should("contain", subtitle); return cy.get('[data-cy="lc-subtitle"]').should("contain", subtitle)
}); })

Binary file not shown.

View File

@ -7,7 +7,7 @@ echo 'prettier:check'
(cd client && npm run prettier:check) (cd client && npm run prettier:check)
echo 'lint and typecheck' echo 'lint and typecheck'
(cd client && npm run lint && npm run typecheck) (cd client && npm run lint:errors && npm run typecheck)
echo 'python ufmt check' echo 'python ufmt check'
ufmt check server ufmt check server

View File

@ -6,6 +6,7 @@
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"cypress:open": "cypress open", "cypress:open": "cypress open",
"cypress:ci": "cypress-cloud run --parallel --record", "cypress:ci": "cypress-cloud run --parallel --record",
"cypress:install": "cypress install",
"prettier": "npm run prettier --prefix client" "prettier": "npm run prettier --prefix client"
}, },
"devDependencies": { "devDependencies": {

View File

@ -4,6 +4,8 @@ from rest_framework.response import Response
from vbv_lernwelt.core.models import Country, Organisation from vbv_lernwelt.core.models import Country, Organisation
from vbv_lernwelt.core.serializers import CountrySerializer, OrganisationSerializer from vbv_lernwelt.core.serializers import CountrySerializer, OrganisationSerializer
from vbv_lernwelt.learnpath.models import CourseProfile
from vbv_lernwelt.learnpath.serializers import CourseProfileSerializer
@api_view(["GET"]) @api_view(["GET"])
@ -26,4 +28,13 @@ def list_entities(request):
countries = CountrySerializer( countries = CountrySerializer(
Country.objects.all(), many=True, context=context Country.objects.all(), many=True, context=context
).data ).data
return Response({"organisations": organisations, "countries": countries}) course_profiles = CourseProfileSerializer(
CourseProfile.objects.all(), many=True, context=context
).data
return Response(
{
"organisations": organisations,
"countries": countries,
"courseProfiles": course_profiles,
}
)

View File

@ -1,4 +1,3 @@
from django.shortcuts import get_object_or_404
from rest_framework.decorators import api_view, permission_classes from rest_framework.decorators import api_view, permission_classes
from rest_framework.generics import get_object_or_404 from rest_framework.generics import get_object_or_404
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated

View File

@ -64,12 +64,32 @@ from vbv_lernwelt.shop.models import CheckoutInformation
@click.option( @click.option(
"--create-assignment-completion/--no-create-assignment-completion", "--create-assignment-completion/--no-create-assignment-completion",
default=False, default=False,
help="will create assignment completion data for test-student1@example.com", help="will create assignment completion data for test-student1@example.com by default. Other user can be specified with --assignment-completion-user",
)
@click.option(
"--assignment-completion-user-id",
default=TEST_STUDENT1_USER_ID,
help="user to create assignment completion for. Defaults to test-student1@example.com. Hint: Is only evaluated if --create-assignment-completion is set.",
)
@click.option(
"--assignment-completion-course-session-id",
default=TEST_COURSE_SESSION_BERN_ID,
help="course session to create assignment completion in. Defaults to 'Test Bern 2022 a'. Hint: Is only evaluated if --create-assignment-completion is set.",
) )
@click.option( @click.option(
"--create-assignment-evaluation/--no-create-assignment-evaluation", "--create-assignment-evaluation/--no-create-assignment-evaluation",
default=False, default=False,
help="will create assignment evaluation data for test-student1@example.com", help="will create assignment evaluation data for test-student1@example.com by default. Other user can be specified with --assignment-evaluation-user",
)
@click.option(
"--assignment-evaluation-user-id",
default=TEST_TRAINER1_USER_ID,
help="user to create assignment evaluation for. Defaults to test-trainer1@example.com. Hint: Is only evaluated if --create-assignment-completion is set.",
)
@click.option(
"--assignment-evaluation-course-session-id",
default=TEST_COURSE_SESSION_BERN_ID,
help="course session to create assignment evaluation in. Defaults to 'Test Bern 2022 a'. Hint: Is only evaluated if --create-assignment-completion is set.",
) )
@click.option( @click.option(
"--assignment-evaluation-scores", "--assignment-evaluation-scores",
@ -86,7 +106,17 @@ from vbv_lernwelt.shop.models import CheckoutInformation
type=(int, int, float), type=(int, int, float),
default=(None, None, 0.0), default=(None, None, 0.0),
metavar="USER_POINTS MAX_POINTS POINTS_DEDUCTED", metavar="USER_POINTS MAX_POINTS POINTS_DEDUCTED",
help="Create edoniq result data for test-student1@example.com with user points and max points", help="Create edoniq result data for test-student1@example.com by default with user points and max points. Use --edoniq-user-id to specify a different user.",
)
@click.option(
"--edoniq-user-id",
default=TEST_STUDENT1_USER_ID,
help="User to create edoniq test results for. Defaults to test-student1@example.com. Hint: Is only evaluated if --create-edoniq-test-results is set.",
)
@click.option(
"--edoniq-course-session-id",
default=TEST_COURSE_SESSION_BERN_ID,
help="course session to create edoniq test results in. Defaults to 'Test Bern 2022 a'. Hint: Is only evaluated if --create-edoniq-test-results is set.",
) )
@click.option( @click.option(
"--create-feedback-responses/--no-create-feedback-responses", "--create-feedback-responses/--no-create-feedback-responses",
@ -130,10 +160,16 @@ from vbv_lernwelt.shop.models import CheckoutInformation
) )
def command( def command(
create_assignment_completion, create_assignment_completion,
assignment_completion_user_id,
assignment_completion_course_session_id,
create_assignment_evaluation, create_assignment_evaluation,
assignment_evaluation_user_id,
assignment_evaluation_course_session_id,
assignment_evaluation_scores, assignment_evaluation_scores,
assignment_points_deducted, assignment_points_deducted,
create_edoniq_test_results, create_edoniq_test_results,
edoniq_user_id,
edoniq_course_session_id,
create_feedback_responses, create_feedback_responses,
create_course_completion_performance_criteria, create_course_completion_performance_criteria,
create_attendance_days, create_attendance_days,
@ -186,15 +222,19 @@ def command(
assignment=Assignment.objects.get( assignment=Assignment.objects.get(
slug="test-lehrgang-assignment-überprüfen-einer-motorfahrzeugs-versicherungspolice" slug="test-lehrgang-assignment-überprüfen-einer-motorfahrzeugs-versicherungspolice"
), ),
course_session=CourseSession.objects.get(id=TEST_COURSE_SESSION_BERN_ID), course_session=CourseSession.objects.get(
user=User.objects.get(id=TEST_STUDENT1_USER_ID), id=assignment_completion_course_session_id
),
user=User.objects.get(id=assignment_completion_user_id),
) )
create_test_assignment_submitted_data( create_test_assignment_submitted_data(
assignment=Assignment.objects.get( assignment=Assignment.objects.get(
slug="test-lehrgang-assignment-mein-kundenstamm" slug="test-lehrgang-assignment-mein-kundenstamm"
), ),
course_session=CourseSession.objects.get(id=TEST_COURSE_SESSION_BERN_ID), course_session=CourseSession.objects.get(
user=User.objects.get(id=TEST_STUDENT1_USER_ID), id=assignment_completion_course_session_id
),
user=User.objects.get(id=assignment_completion_user_id),
) )
if create_assignment_evaluation: if create_assignment_evaluation:
if not assignment_evaluation_scores: if not assignment_evaluation_scores:
@ -211,9 +251,11 @@ def command(
assignment=Assignment.objects.get( assignment=Assignment.objects.get(
slug="test-lehrgang-assignment-überprüfen-einer-motorfahrzeugs-versicherungspolice" slug="test-lehrgang-assignment-überprüfen-einer-motorfahrzeugs-versicherungspolice"
), ),
course_session=CourseSession.objects.get(id=TEST_COURSE_SESSION_BERN_ID), course_session=CourseSession.objects.get(
assignment_user=User.objects.get(id=TEST_STUDENT1_USER_ID), id=assignment_evaluation_course_session_id
evaluation_user=User.objects.get(id=TEST_TRAINER1_USER_ID), ),
assignment_user=User.objects.get(id=assignment_completion_user_id),
evaluation_user=User.objects.get(id=assignment_evaluation_user_id),
input_scores=assignment_evaluation_scores, input_scores=assignment_evaluation_scores,
points_deducted=assignment_points_deducted, points_deducted=assignment_points_deducted,
) )
@ -227,8 +269,8 @@ def command(
assignment=Assignment.objects.get( assignment=Assignment.objects.get(
slug="test-lehrgang-assignment-edoniq-wissens-und-verständisfragen-circle-fahrzeug-demo" slug="test-lehrgang-assignment-edoniq-wissens-und-verständisfragen-circle-fahrzeug-demo"
), ),
course_session=CourseSession.objects.get(id=TEST_COURSE_SESSION_BERN_ID), course_session=CourseSession.objects.get(id=edoniq_course_session_id),
assignment_user=User.objects.get(id=TEST_STUDENT1_USER_ID), assignment_user=User.objects.get(id=edoniq_user_id),
user_points=user_points, user_points=user_points,
max_points=max_points, max_points=max_points,
evaluation_points_deducted=points_deducted, evaluation_points_deducted=points_deducted,

View File

@ -8,6 +8,7 @@ from graphql import GraphQLError
from rest_framework.exceptions import PermissionDenied from rest_framework.exceptions import PermissionDenied
from vbv_lernwelt.competence.graphql.types import ActionCompetenceObjectType from vbv_lernwelt.competence.graphql.types import ActionCompetenceObjectType
from vbv_lernwelt.core.models import User
from vbv_lernwelt.course.models import ( from vbv_lernwelt.course.models import (
CircleDocument, CircleDocument,
Course, Course,
@ -29,8 +30,9 @@ from vbv_lernwelt.course_session.models import (
) )
from vbv_lernwelt.course_session_group.models import CourseSessionGroup from vbv_lernwelt.course_session_group.models import CourseSessionGroup
from vbv_lernwelt.iam.permissions import has_course_access from vbv_lernwelt.iam.permissions import has_course_access
from vbv_lernwelt.learnpath.consts import COURSE_PROFILE_ALL_ID
from vbv_lernwelt.learnpath.graphql.types import LearningPathObjectType from vbv_lernwelt.learnpath.graphql.types import LearningPathObjectType
from vbv_lernwelt.learnpath.models import Circle from vbv_lernwelt.learnpath.models import Circle, CourseProfile
logger = structlog.get_logger(__name__) logger = structlog.get_logger(__name__)
@ -106,6 +108,12 @@ class CourseObjectType(DjangoObjectType):
graphene.NonNull(ActionCompetenceObjectType), required=True graphene.NonNull(ActionCompetenceObjectType), required=True
) )
configuration = graphene.Field(CourseConfigurationObjectType, required=True) configuration = graphene.Field(CourseConfigurationObjectType, required=True)
profiles = graphene.List(graphene.String)
course_session_users = graphene.List(
"vbv_lernwelt.course.graphql.types.CourseSessionUserType",
required=True,
id=graphene.String(),
)
class Meta: class Meta:
model = Course model = Course
@ -125,6 +133,22 @@ class CourseObjectType(DjangoObjectType):
def resolve_action_competences(root: Course, info): def resolve_action_competences(root: Course, info):
return root.get_action_competences() return root.get_action_competences()
@staticmethod
def resolve_profiles(root: Course, info, **kwargs):
if root.configuration.is_vv:
return CourseProfile.objects.values_list("code", flat=True)
return []
@staticmethod
def resolve_course_session_users(root: Course, info, id=None, **kwargs):
# todo: restrict users that can be queried
if id is not None:
user = User.objects.get(id=id)
else:
user = info.context.user
users = CourseSessionUser.objects.filter(user=user, course_session__course=root)
return users
class CourseSessionUserExpertCircleType(ObjectType): class CourseSessionUserExpertCircleType(ObjectType):
id = graphene.ID(required=True) id = graphene.ID(required=True)
@ -132,6 +156,21 @@ class CourseSessionUserExpertCircleType(ObjectType):
slug = graphene.String(required=True) slug = graphene.String(required=True)
class CourseSessionUserType(DjangoObjectType):
chosen_profile = graphene.String(required=True)
course_session = graphene.Field(
"vbv_lernwelt.course.graphql.types.CourseSessionObjectType", required=True
)
class Meta:
model = CourseSessionUser
fields = ["chosen_profile", "id"]
@staticmethod
def resolve_chosen_profile(root: CourseSessionUser, info, **kwargs):
return getattr(root.chosen_profile, "code", "")
class CourseSessionUserObjectsType(ObjectType): class CourseSessionUserObjectsType(ObjectType):
""" """
WORKAROUND: WORKAROUND:

View File

@ -97,6 +97,7 @@ from vbv_lernwelt.learnpath.create_vv_new_learning_path import (
create_vv_new_learning_path, create_vv_new_learning_path,
create_vv_pruefung_learning_path, create_vv_pruefung_learning_path,
) )
from vbv_lernwelt.learnpath.creators import assign_circles_to_profiles
from vbv_lernwelt.learnpath.models import ( from vbv_lernwelt.learnpath.models import (
Circle, Circle,
LearningContent, LearningContent,
@ -218,6 +219,7 @@ def create_versicherungsvermittlerin_course(
create_vv_gewinnen_casework(course_id=course_id) create_vv_gewinnen_casework(course_id=course_id)
create_vv_reflection(course_id=course_id) create_vv_reflection(course_id=course_id)
create_vv_new_learning_path(course_id=course_id) create_vv_new_learning_path(course_id=course_id)
assign_circles_to_profiles()
cs = CourseSession.objects.create(course_id=course_id, title=names[language]) cs = CourseSession.objects.create(course_id=course_id, title=names[language])

View File

@ -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",
),
),
]

View File

@ -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,
),
),
]

View File

@ -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 = []

View File

@ -285,6 +285,10 @@ class CourseSessionUser(models.Model):
) )
optional_attendance = models.BooleanField(default=False) optional_attendance = models.BooleanField(default=False)
chosen_profile = models.ForeignKey(
"learnpath.CourseProfile", on_delete=models.SET_NULL, blank=True, null=True
)
class Meta: class Meta:
constraints = [ constraints = [
UniqueConstraint( UniqueConstraint(

View File

@ -8,8 +8,10 @@ from vbv_lernwelt.course.models import (
CourseCompletion, CourseCompletion,
CourseConfiguration, CourseConfiguration,
CourseSession, CourseSession,
CourseSessionUser,
) )
from vbv_lernwelt.iam.permissions import course_session_permissions from vbv_lernwelt.iam.permissions import course_session_permissions
from vbv_lernwelt.learnpath.models import CourseProfile
class CourseConfigurationSerializer(serializers.ModelSerializer): class CourseConfigurationSerializer(serializers.ModelSerializer):
@ -31,10 +33,23 @@ class CourseSerializer(serializers.ModelSerializer):
configuration = CourseConfigurationSerializer( configuration = CourseConfigurationSerializer(
read_only=True, read_only=True,
) )
course_profiles = serializers.SerializerMethodField()
def get_course_profiles(self, obj):
if obj.configuration.is_vv:
return CourseProfile.objects.all().values_list("code", flat=True)
return []
class Meta: class Meta:
model = Course model = Course
fields = ["id", "title", "category_name", "slug", "configuration"] fields = [
"id",
"title",
"category_name",
"slug",
"configuration",
"course_profiles",
]
class CourseCategorySerializer(serializers.ModelSerializer): class CourseCategorySerializer(serializers.ModelSerializer):
@ -103,6 +118,12 @@ class CourseSessionSerializer(serializers.ModelSerializer):
return [] return []
class CypressCourseSessionUserSerializer(serializers.ModelSerializer):
class Meta:
model = CourseSessionUser
fields = "__all__"
class CircleDocumentSerializer(serializers.ModelSerializer): class CircleDocumentSerializer(serializers.ModelSerializer):
learning_sequence = serializers.SerializerMethodField() learning_sequence = serializers.SerializerMethodField()

View File

@ -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")

View File

@ -1,7 +1,10 @@
import graphene import graphene
import structlog import structlog
from graphene import relay
from rest_framework.exceptions import PermissionDenied from rest_framework.exceptions import PermissionDenied
from vbv_lernwelt.course.graphql.types import CourseSessionUserType
from vbv_lernwelt.course.models import CourseSessionUser
from vbv_lernwelt.course_session.graphql.types import ( from vbv_lernwelt.course_session.graphql.types import (
CourseSessionAttendanceCourseObjectType, CourseSessionAttendanceCourseObjectType,
) )
@ -11,10 +14,33 @@ from vbv_lernwelt.course_session.services.attendance import (
update_attendance_list, update_attendance_list,
) )
from vbv_lernwelt.iam.permissions import has_course_access from vbv_lernwelt.iam.permissions import has_course_access
from vbv_lernwelt.learnpath.models import CourseProfile
logger = structlog.get_logger(__name__) logger = structlog.get_logger(__name__)
class UpdateCourseProfileSuccess(graphene.ObjectType):
user = graphene.Field(CourseSessionUserType(), required=True)
class UpdateCourseProfileError(graphene.ObjectType):
message = graphene.String()
class UpdateCourseProfileResult(graphene.Union):
class Meta:
types = (
UpdateCourseProfileError,
UpdateCourseProfileSuccess,
)
@classmethod
def resolve_type(cls, instance, info):
if type(instance).__name__ == "UpdateCourseProfileSuccess":
return UpdateCourseProfileSuccess
return UpdateCourseProfileError
class AttendanceUserInputType(graphene.InputObjectType): class AttendanceUserInputType(graphene.InputObjectType):
user_id = graphene.UUID(required=True) user_id = graphene.UUID(required=True)
status = graphene.Field( status = graphene.Field(
@ -57,5 +83,40 @@ class AttendanceCourseUserMutation(graphene.Mutation):
) )
class CourseSessionProfileMutation(relay.ClientIDMutation):
class Input:
course_profile = graphene.String(required=True)
course_slug = graphene.String(required=True)
result = UpdateCourseProfileResult()
@classmethod
def mutate_and_get_payload(cls, root, info, **input):
course_profile = input.get("course_profile")
course_slug = input.get("course_slug")
user = info.context.user
try:
if course_profile == "":
profile = None
else:
profile = CourseProfile.objects.get(code=course_profile)
# csu = user.coursesessionuser_set.first()
csu = CourseSessionUser.objects.get(
course_session__course__slug=course_slug, user=user
)
csu.chosen_profile = profile
csu.save()
return cls(result=UpdateCourseProfileSuccess(user=csu))
except CourseProfile.DoesNotExist:
return cls(result=UpdateCourseProfileError("Course Profile does not exist"))
except CourseSessionUser.DoesNotExist:
return cls(
result=UpdateCourseProfileError("Course Session User does not exist")
)
class CourseSessionMutation: class CourseSessionMutation:
update_course_session_attendance_course_users = AttendanceCourseUserMutation.Field() update_course_session_attendance_course_users = AttendanceCourseUserMutation.Field()
update_course_session_profile = CourseSessionProfileMutation.Field()

View File

@ -1,3 +1,8 @@
from django.contrib import admin from django.contrib import admin
# Register your models here. from vbv_lernwelt.learnpath.models import CourseProfile
@admin.register(CourseProfile)
class CourseProfileAdmin(admin.ModelAdmin):
pass

View File

@ -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"

View File

@ -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 à lexamen")
make_base_circle("Lexamen")
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()

View File

@ -1,3 +1,5 @@
import random
import graphene import graphene
import structlog import structlog
from graphene_django import DjangoObjectType from graphene_django import DjangoObjectType
@ -6,6 +8,7 @@ from vbv_lernwelt.core.utils import find_first_index
from vbv_lernwelt.course.graphql.interfaces import CoursePageInterface from vbv_lernwelt.course.graphql.interfaces import CoursePageInterface
from vbv_lernwelt.learnpath.models import ( from vbv_lernwelt.learnpath.models import (
Circle, Circle,
CourseProfile,
LearningContentAssignment, LearningContentAssignment,
LearningContentAttendanceCourse, LearningContentAttendanceCourse,
LearningContentDocumentList, LearningContentDocumentList,
@ -299,14 +302,12 @@ class CircleObjectType(DjangoObjectType):
learning_sequences = graphene.List( learning_sequences = graphene.List(
graphene.NonNull(LearningSequenceObjectType), required=True graphene.NonNull(LearningSequenceObjectType), required=True
) )
profiles = graphene.List(graphene.String, required=True)
class Meta: class Meta:
model = Circle model = Circle
interfaces = (CoursePageInterface,) interfaces = (CoursePageInterface,)
fields = [ fields = ["description", "goals", "is_base_circle"]
"description",
"goals",
]
def resolve_learning_sequences(self: Circle, info, **kwargs): def resolve_learning_sequences(self: Circle, info, **kwargs):
circle_descendants = None circle_descendants = None
@ -335,6 +336,10 @@ class CircleObjectType(DjangoObjectType):
if descendant.specific_class == LearningSequence if descendant.specific_class == LearningSequence
] ]
@staticmethod
def resolve_profiles(root: Circle, info, **kwargs):
return root.profiles.all()
class TopicObjectType(DjangoObjectType): class TopicObjectType(DjangoObjectType):
circles = graphene.List(graphene.NonNull(CircleObjectType), required=True) circles = graphene.List(graphene.NonNull(CircleObjectType), required=True)

View File

@ -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"
),
),
]

View File

@ -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"
),
),
]

View File

@ -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)]

View File

@ -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)]

View File

@ -3,6 +3,7 @@ from typing import Tuple
from django.db import models from django.db import models
from django.utils.text import slugify from django.utils.text import slugify
from modelcluster.models import ParentalManyToManyField
from wagtail.admin.panels import FieldPanel, PageChooserPanel from wagtail.admin.panels import FieldPanel, PageChooserPanel
from wagtail.fields import RichTextField, StreamField from wagtail.fields import RichTextField, StreamField
from wagtail.models import Page from wagtail.models import Page
@ -66,6 +67,25 @@ class Topic(CourseBasePage):
return f"{self.title}" return f"{self.title}"
class CourseProfile(models.Model):
code = models.CharField(max_length=255)
order = models.IntegerField(default=999)
def __str__(self) -> str:
return self.code
class Meta:
ordering = [
"order",
]
class CourseProfileToCircle(models.Model):
# this connects the course profile to a circle, because a circle can be in multiple profiles
# todo: to we even need a through model?
pass
class Circle(CourseBasePage): class Circle(CourseBasePage):
parent_page_types = ["learnpath.LearningPath"] parent_page_types = ["learnpath.LearningPath"]
subpage_types = [ subpage_types = [
@ -95,9 +115,25 @@ class Circle(CourseBasePage):
goals = RichTextField(features=DEFAULT_RICH_TEXT_FEATURES_WITH_HEADER) goals = RichTextField(features=DEFAULT_RICH_TEXT_FEATURES_WITH_HEADER)
profiles = ParentalManyToManyField(CourseProfile, related_name="circles")
# base circles do never belong to a course profile and should also get displayed no matter what profile is chosen
is_base_circle = models.BooleanField(default=False)
# profile = models.ForeignKey(
# ApprovalProfile,
# null=True,
# blank=True,
# on_delete=models.SET_NULL,
# related_name="circles",
# help_text="Zulassungsprofil",
# )
content_panels = Page.content_panels + [ content_panels = Page.content_panels + [
FieldPanel("description"), FieldPanel("description"),
FieldPanel("goals"), FieldPanel("goals"),
FieldPanel("is_base_circle"),
FieldPanel("profiles"),
] ]
def get_frontend_url(self): def get_frontend_url(self):

View File

@ -1,3 +1,4 @@
from rest_framework import serializers
from rest_framework.fields import SerializerMethodField from rest_framework.fields import SerializerMethodField
from vbv_lernwelt.competence.serializers import ( from vbv_lernwelt.competence.serializers import (
@ -6,6 +7,7 @@ from vbv_lernwelt.competence.serializers import (
from vbv_lernwelt.core.utils import get_django_content_type from vbv_lernwelt.core.utils import get_django_content_type
from vbv_lernwelt.course.serializer_helpers import get_course_serializer_class from vbv_lernwelt.course.serializer_helpers import get_course_serializer_class
from vbv_lernwelt.learnpath.models import ( from vbv_lernwelt.learnpath.models import (
CourseProfile,
LearningContentAssignment, LearningContentAssignment,
LearningContentEdoniqTest, LearningContentEdoniqTest,
LearningUnit, LearningUnit,
@ -98,3 +100,9 @@ class LearningContentAssignmentSerializer(
} }
except Exception: except Exception:
return None return None
class CourseProfileSerializer(serializers.ModelSerializer):
class Meta:
model = CourseProfile
fields = ["id", "code"]

View File

@ -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",
),
),
]

View File

@ -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",
),
),
]

View File

@ -106,7 +106,9 @@ class CheckoutInformation(models.Model):
null=True, null=True,
blank=True, blank=True,
) )
chosen_profile = models.ForeignKey(
"learnpath.CourseProfile", on_delete=models.SET_NULL, null=True, blank=True
)
# webhook metadata # webhook metadata
webhook_history = models.JSONField(default=list) webhook_history = models.JSONField(default=list)

View File

@ -6,6 +6,8 @@ from rest_framework.test import APITestCase
from vbv_lernwelt.core.admin import User from vbv_lernwelt.core.admin import User
from vbv_lernwelt.core.model_utils import add_countries from vbv_lernwelt.core.model_utils import add_countries
from vbv_lernwelt.learnpath.consts import COURSE_PROFILE_ALL_CODE, COURSE_PROFILE_ALL_ID
from vbv_lernwelt.learnpath.models import CourseProfile
from vbv_lernwelt.shop.const import VV_DE_PRODUCT_SKU from vbv_lernwelt.shop.const import VV_DE_PRODUCT_SKU
from vbv_lernwelt.shop.models import CheckoutInformation, CheckoutState, Product from vbv_lernwelt.shop.models import CheckoutInformation, CheckoutState, Product
from vbv_lernwelt.shop.services import InitTransactionException from vbv_lernwelt.shop.services import InitTransactionException
@ -50,6 +52,10 @@ class CheckoutAPITestCase(APITestCase):
is_active=True, is_active=True,
) )
CourseProfile.objects.get_or_create(
id=COURSE_PROFILE_ALL_ID, code=COURSE_PROFILE_ALL_CODE
)
self.client.login(username=USER_USERNAME, password=USER_PASSWORD) self.client.login(username=USER_USERNAME, password=USER_PASSWORD)
add_countries(small_set=True) add_countries(small_set=True)

View File

@ -8,6 +8,8 @@ from rest_framework.permissions import IsAuthenticated
from sentry_sdk import capture_exception from sentry_sdk import capture_exception
from vbv_lernwelt.course.models import CourseSession, CourseSessionUser from vbv_lernwelt.course.models import CourseSession, CourseSessionUser
from vbv_lernwelt.learnpath.consts import COURSE_PROFILE_ALL_ID
from vbv_lernwelt.learnpath.models import CourseProfile
from vbv_lernwelt.notify.email.email_services import EmailTemplate, send_email from vbv_lernwelt.notify.email.email_services import EmailTemplate, send_email
from vbv_lernwelt.shop.const import ( from vbv_lernwelt.shop.const import (
VV_DE_PRODUCT_SKU, VV_DE_PRODUCT_SKU,
@ -92,6 +94,7 @@ def checkout_vv(request):
sku = request.data["product"] sku = request.data["product"]
base_redirect_url = request.data["redirect_url"] base_redirect_url = request.data["redirect_url"]
chosen_profile_id = request.data.get("chosen_profile", COURSE_PROFILE_ALL_ID)
log.info("Checkout requested: sku", user_id=request.user.id, sku=sku) log.info("Checkout requested: sku", user_id=request.user.id, sku=sku)
@ -106,6 +109,11 @@ def checkout_vv(request):
), ),
) )
try:
chosen_profile = CourseProfile.objects.get(id=chosen_profile_id)
except CourseProfile.DoesNotExist:
chosen_profile = CourseProfile.objects.get(id=COURSE_PROFILE_ALL_ID)
checkouts = CheckoutInformation.objects.filter( checkouts = CheckoutInformation.objects.filter(
user=request.user, user=request.user,
product_sku=sku, product_sku=sku,
@ -151,6 +159,7 @@ def checkout_vv(request):
"device_fingerprint_session_key", "" "device_fingerprint_session_key", ""
), ),
# address # address
chosen_profile=chosen_profile,
**request.data["address"], **request.data["address"],
) )
@ -257,9 +266,11 @@ def create_vv_course_session_user(checkout_info: CheckoutInformation):
_, created = CourseSessionUser.objects.get_or_create( _, created = CourseSessionUser.objects.get_or_create(
user=checkout_info.user, user=checkout_info.user,
role=CourseSessionUser.Role.MEMBER, role=CourseSessionUser.Role.MEMBER,
chosen_profile=checkout_info.chosen_profile,
course_session=CourseSession.objects.get( course_session=CourseSession.objects.get(
id=PRODUCT_SKU_TO_COURSE_SESSION_ID[checkout_info.product_sku] id=PRODUCT_SKU_TO_COURSE_SESSION_ID[checkout_info.product_sku]
), ),
# chosen_profile=bla,
) )
if created: if created: