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:
commit
aca066a376
|
|
@ -10,6 +10,7 @@
|
|||
"dependencies": {
|
||||
"@headlessui/tailwindcss": "^0.2.1",
|
||||
"@headlessui/vue": "^1.7.22",
|
||||
"@parcel/watcher": "^2.4.1",
|
||||
"@sentry/tracing": "^7.114.0",
|
||||
"@sentry/vue": "^8.17.0",
|
||||
"@urql/exchange-graphcache": "^7.1.2",
|
||||
|
|
@ -5722,7 +5723,6 @@
|
|||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"fill-range": "^7.0.1"
|
||||
},
|
||||
|
|
@ -8258,7 +8258,6 @@
|
|||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
},
|
||||
|
|
@ -9317,7 +9316,6 @@
|
|||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
|
|
@ -9334,7 +9332,6 @@
|
|||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-extglob": "^2.1.1"
|
||||
},
|
||||
|
|
@ -9388,7 +9385,6 @@
|
|||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
|
|
@ -10334,7 +10330,6 @@
|
|||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
|
||||
"integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"braces": "^3.0.2",
|
||||
"picomatch": "^2.3.1"
|
||||
|
|
@ -11072,7 +11067,6 @@
|
|||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
|
|
@ -12672,7 +12666,6 @@
|
|||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
},
|
||||
|
|
@ -18806,7 +18799,6 @@
|
|||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fill-range": "^7.0.1"
|
||||
}
|
||||
|
|
@ -20667,7 +20659,6 @@
|
|||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
}
|
||||
|
|
@ -21425,8 +21416,7 @@
|
|||
"is-extglob": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="
|
||||
},
|
||||
"is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
|
|
@ -21437,7 +21427,6 @@
|
|||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-extglob": "^2.1.1"
|
||||
}
|
||||
|
|
@ -21475,8 +21464,7 @@
|
|||
"is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
|
||||
},
|
||||
"is-number-object": {
|
||||
"version": "1.0.7",
|
||||
|
|
@ -22174,7 +22162,6 @@
|
|||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
|
||||
"integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"braces": "^3.0.2",
|
||||
"picomatch": "^2.3.1"
|
||||
|
|
@ -22727,8 +22714,7 @@
|
|||
"picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="
|
||||
},
|
||||
"pify": {
|
||||
"version": "2.3.0",
|
||||
|
|
@ -23824,7 +23810,6 @@
|
|||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-number": "^7.0.0"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,8 +10,9 @@
|
|||
"cypress:open": "cypress open",
|
||||
"dev": "concurrently \"vite\" \"npm run codegen:watch\"",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
||||
"lint:errors": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --quiet --ignore-path .gitignore",
|
||||
"prettier": "prettier . --write",
|
||||
"prettier:check": "prettier . --check",
|
||||
"prettier:check": "prettier . --check --ignore-unknown",
|
||||
"tailwind": "tailwindcss -i tailwind.css -o ../server/vbv_lernwelt/static/css/tailwind.css --watch",
|
||||
"test": "vitest run",
|
||||
"typecheck": "npm run codegen && vue-tsc --noEmit -p tsconfig.app.json --composite false",
|
||||
|
|
@ -20,6 +21,7 @@
|
|||
"dependencies": {
|
||||
"@headlessui/tailwindcss": "^0.2.1",
|
||||
"@headlessui/vue": "^1.7.22",
|
||||
"@parcel/watcher": "^2.4.1",
|
||||
"@sentry/tracing": "^7.114.0",
|
||||
"@sentry/vue": "^8.17.0",
|
||||
"@urql/exchange-graphcache": "^7.1.2",
|
||||
|
|
|
|||
|
|
@ -342,7 +342,7 @@ const hasSessionTitle = computed(() => {
|
|||
v-if="hasSessionTitle"
|
||||
class="nav-item hidden items-center lg:inline-flex"
|
||||
>
|
||||
<div class="">
|
||||
<div class="" data-cy="current-course-session-title">
|
||||
{{ selectedCourseSessionTitle }}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
<script setup lang="ts">
|
||||
import LearningPathCircle from "@/pages/learningPath/learningPathPage/LearningPathCircle.vue";
|
||||
import { calculateCircleSectorData } from "@/pages/learningPath/learningPathPage/utils";
|
||||
import {
|
||||
calculateCircleSectorData,
|
||||
filterCircles,
|
||||
useCourseFilter,
|
||||
} from "@/pages/learningPath/learningPathPage/utils";
|
||||
import { computed } from "vue";
|
||||
import { useCourseCircleProgress, useCourseDataWithCompletion } from "@/composables";
|
||||
import LoadingSpinner from "@/components/ui/LoadingSpinner.vue";
|
||||
|
|
@ -48,9 +52,13 @@ const wrapperClasses = computed(() => {
|
|||
return classes;
|
||||
});
|
||||
|
||||
const { inProgressCirclesCount, circlesCount } = useCourseCircleProgress(
|
||||
lpQueryResult.circles
|
||||
);
|
||||
const { filter } = useCourseFilter(props.courseSlug, props.courseSessionId);
|
||||
|
||||
const filteredCircles = computed(() => {
|
||||
return filterCircles(filter.value, circles.value);
|
||||
});
|
||||
const { inProgressCirclesCount, circlesCount } =
|
||||
useCourseCircleProgress(filteredCircles);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -66,7 +74,7 @@ const { inProgressCirclesCount, circlesCount } = useCourseCircleProgress(
|
|||
</h4>
|
||||
<div :class="wrapperClasses">
|
||||
<LearningPathCircle
|
||||
v-for="circle in circles"
|
||||
v-for="circle in filteredCircles"
|
||||
:key="circle.id"
|
||||
:sectors="calculateCircleSectorData(circle)"
|
||||
></LearningPathCircle>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,27 @@
|
|||
<script setup lang="ts">
|
||||
import ItNavigationProgress from "@/components/ui/ItNavigationProgress.vue";
|
||||
import { computed } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { isString, startsWith } from "lodash";
|
||||
|
||||
const route = useRoute();
|
||||
const props = defineProps<{
|
||||
step: number;
|
||||
}>();
|
||||
|
||||
const steps = computed(() => {
|
||||
const courseType = route.params.courseType;
|
||||
if (isString(courseType) && startsWith(courseType, "vv-")) {
|
||||
return 4;
|
||||
}
|
||||
return 3;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-screen flex-col">
|
||||
<div class="flex-grow scroll-smooth p-16 lg:overflow-auto">
|
||||
<ItNavigationProgress :steps="3" :current-step="props.step" />
|
||||
<ItNavigationProgress :steps="steps" :current-step="props.step" />
|
||||
<slot name="content"></slot>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -5,17 +5,14 @@ import { computed } from "vue"; // https://stackoverflow.com/questions/64775876/
|
|||
|
||||
// https://stackoverflow.com/questions/64775876/vue-3-pass-reactive-object-to-component-with-two-way-binding
|
||||
interface Props {
|
||||
modelValue?: {
|
||||
id: string | number;
|
||||
name: string;
|
||||
};
|
||||
modelValue?: DropdownSelectable;
|
||||
items?: DropdownSelectable[];
|
||||
borderless?: boolean;
|
||||
placeholderText?: string | null;
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", data: object): void;
|
||||
(e: "update:modelValue", data: DropdownSelectable): void;
|
||||
}>();
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
|
|
|
|||
|
|
@ -1,33 +1,30 @@
|
|||
import { useCSRFFetch } from "@/fetchHelpers";
|
||||
import type { CourseStatisticsType } from "@/gql/graphql";
|
||||
import { graphqlClient } from "@/graphql/client";
|
||||
import { COURSE_QUERY, COURSE_SESSION_DETAIL_QUERY } from "@/graphql/queries";
|
||||
import {useCSRFFetch} from "@/fetchHelpers";
|
||||
import type {CourseStatisticsType} from "@/gql/graphql";
|
||||
import {graphqlClient} from "@/graphql/client";
|
||||
import {COURSE_QUERY, COURSE_SESSION_DETAIL_QUERY} from "@/graphql/queries";
|
||||
import {
|
||||
circleFlatChildren,
|
||||
circleFlatLearningContents,
|
||||
circleFlatLearningUnits,
|
||||
someFinishedInLearningSequence,
|
||||
} from "@/services/circle";
|
||||
import type {
|
||||
DashboardDueDate,
|
||||
DashboardPersonRoleType,
|
||||
DashboardPersonType,
|
||||
} from "@/services/dashboard";
|
||||
import type {DashboardDueDate, DashboardPersonRoleType, DashboardPersonType,} from "@/services/dashboard";
|
||||
import {
|
||||
courseIdForCourseSlug,
|
||||
fetchDashboardDueDates,
|
||||
fetchDashboardPersons,
|
||||
fetchStatisticData,
|
||||
} from "@/services/dashboard";
|
||||
import { presignUpload, uploadFile } from "@/services/files";
|
||||
import { useCompletionStore } from "@/stores/completion";
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import { useDashboardStore } from "@/stores/dashboard";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import {presignUpload, uploadFile} from "@/services/files";
|
||||
import {useCompletionStore} from "@/stores/completion";
|
||||
import {useCourseSessionsStore} from "@/stores/courseSessions";
|
||||
import {useDashboardStore} from "@/stores/dashboard";
|
||||
import {useUserStore} from "@/stores/user";
|
||||
import type {
|
||||
ActionCompetence,
|
||||
AgentParticipantRelation,
|
||||
CircleType,
|
||||
CompetenceCertificate,
|
||||
Course,
|
||||
CourseCompletion,
|
||||
CourseCompletionStatus,
|
||||
|
|
@ -39,14 +36,16 @@ import type {
|
|||
LearningUnitPerformanceCriteria,
|
||||
PerformanceCriteria,
|
||||
} from "@/types";
|
||||
import { useQuery } from "@urql/vue";
|
||||
import {useQuery} from "@urql/vue";
|
||||
import dayjs from "dayjs";
|
||||
import { t } from "i18next";
|
||||
import {t} from "i18next";
|
||||
import orderBy from "lodash/orderBy";
|
||||
import log from "loglevel";
|
||||
import type { ComputedRef, Ref } from "vue";
|
||||
import { computed, onMounted, ref, watchEffect } from "vue";
|
||||
import { type RouteLocationRaw, useRouter } from "vue-router";
|
||||
import type {ComputedRef, Ref} from "vue";
|
||||
import {computed, onMounted, ref, watchEffect} from "vue";
|
||||
import {type RouteLocationRaw, useRouter} from "vue-router";
|
||||
import {getCertificates} from "./services/competence";
|
||||
import {mergeCompetenceCertificates} from "./pages/competence/utils";
|
||||
|
||||
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
|
||||
const resultPromise = graphqlClient
|
||||
.query(COURSE_QUERY, { slug: `${courseSlug}` })
|
||||
.query(COURSE_QUERY, {slug: `${courseSlug}`})
|
||||
.toPromise();
|
||||
|
||||
resultPromise.then((result) => {
|
||||
|
|
@ -443,7 +442,7 @@ export function useFileUpload() {
|
|||
const fileInfo = ref({} as { id: string; name: string; url: string });
|
||||
|
||||
async function upload(e: Event) {
|
||||
const { files } = e.target as HTMLInputElement;
|
||||
const {files} = e.target as HTMLInputElement;
|
||||
if (!files?.length) return;
|
||||
|
||||
try {
|
||||
|
|
@ -461,7 +460,7 @@ export function useFileUpload() {
|
|||
}
|
||||
}
|
||||
|
||||
return { upload, error, loading, fileInfo };
|
||||
return {upload, error, loading, fileInfo};
|
||||
}
|
||||
|
||||
export function useMyLearningMentors() {
|
||||
|
|
@ -471,7 +470,7 @@ export function useMyLearningMentors() {
|
|||
|
||||
const fetchMentors = async () => {
|
||||
loading.value = true;
|
||||
const { data } = await useCSRFFetch(
|
||||
const {data} = await useCSRFFetch(
|
||||
`/api/mentor/${currentCourseSessionId}/mentors`
|
||||
).json();
|
||||
learningMentors.value = data.value;
|
||||
|
|
@ -623,7 +622,7 @@ export function useCourseCircleProgress(circles: Ref<CircleType[] | undefined>)
|
|||
return circles.value?.length ?? 0;
|
||||
});
|
||||
|
||||
return { inProgressCirclesCount, circlesCount };
|
||||
return {inProgressCirclesCount, circlesCount};
|
||||
}
|
||||
|
||||
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() {
|
||||
const currentCourseSession = useCurrentCourseSession();
|
||||
const hasFeedback = computed(
|
||||
|
|
@ -677,7 +695,7 @@ export function useEvaluationWithFeedback() {
|
|||
currentCourseSession.value.course.configuration.is_vv
|
||||
);
|
||||
|
||||
return { hasFeedback };
|
||||
return {hasFeedback};
|
||||
}
|
||||
|
||||
export function useVVByLink() {
|
||||
|
|
@ -687,9 +705,36 @@ export function useVVByLink() {
|
|||
() =>
|
||||
router.resolve({
|
||||
name: "accountConfirm",
|
||||
params: { courseType: `vv-${userStore.language}` },
|
||||
params: {courseType: `vv-${userStore.language}`},
|
||||
}).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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,3 +3,5 @@ export const itCheckboxDefaultIconCheckedTailwindClass =
|
|||
|
||||
export const itCheckboxDefaultIconUncheckedTailwindClass =
|
||||
"bg-[url(/static/icons/icon-checkbox-unchecked.svg)] hover:bg-[url(/static/icons/icon-checkbox-unchecked-hover.svg)]";
|
||||
|
||||
export const COURSE_PROFILE_ALL_FILTER = "all";
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -278,6 +278,8 @@ type CourseObjectType {
|
|||
configuration: CourseConfigurationObjectType!
|
||||
learning_path: LearningPathObjectType!
|
||||
action_competences: [ActionCompetenceObjectType!]!
|
||||
profiles: [String]
|
||||
course_session_users(id: String): [CourseSessionUserType]!
|
||||
}
|
||||
|
||||
type ActionCompetenceObjectType implements CoursePageInterface {
|
||||
|
|
@ -343,45 +345,38 @@ type CircleLightObjectType {
|
|||
slug: String!
|
||||
}
|
||||
|
||||
type TopicObjectType implements CoursePageInterface {
|
||||
is_visible: Boolean!
|
||||
id: ID!
|
||||
title: String!
|
||||
slug: String!
|
||||
content_type: String!
|
||||
live: Boolean!
|
||||
translation_key: String!
|
||||
frontend_url: String!
|
||||
course: CourseObjectType
|
||||
circles: [CircleObjectType!]!
|
||||
type CourseSessionUserType {
|
||||
id: UUID!
|
||||
chosen_profile: String!
|
||||
course_session: CourseSessionObjectType!
|
||||
}
|
||||
|
||||
type CircleObjectType implements CoursePageInterface {
|
||||
description: String!
|
||||
goals: String!
|
||||
"""
|
||||
Leverages the internal Python implementation of UUID (uuid.UUID) to provide native UUID objects
|
||||
in fields, resolvers and input.
|
||||
"""
|
||||
scalar UUID
|
||||
|
||||
type CourseSessionObjectType {
|
||||
id: ID!
|
||||
created_at: DateTime!
|
||||
updated_at: DateTime!
|
||||
course: CourseObjectType!
|
||||
title: String!
|
||||
slug: String!
|
||||
content_type: String!
|
||||
live: Boolean!
|
||||
translation_key: String!
|
||||
frontend_url: String!
|
||||
course: CourseObjectType
|
||||
learning_sequences: [LearningSequenceObjectType!]!
|
||||
start_date: Date
|
||||
end_date: Date
|
||||
attendance_courses: [CourseSessionAttendanceCourseObjectType!]!
|
||||
assignments: [CourseSessionAssignmentObjectType!]!
|
||||
edoniq_tests: [CourseSessionEdoniqTestObjectType!]!
|
||||
users: [CourseSessionUserObjectsType!]!
|
||||
}
|
||||
|
||||
type LearningSequenceObjectType implements CoursePageInterface {
|
||||
icon: String!
|
||||
id: ID!
|
||||
title: String!
|
||||
slug: String!
|
||||
content_type: String!
|
||||
live: Boolean!
|
||||
translation_key: String!
|
||||
frontend_url: String!
|
||||
course: CourseObjectType
|
||||
learning_units: [LearningUnitObjectType!]!
|
||||
}
|
||||
"""
|
||||
The `Date` scalar type represents a Date
|
||||
value as specified by
|
||||
[iso8601](https://en.wikipedia.org/wiki/ISO_8601).
|
||||
"""
|
||||
scalar Date
|
||||
|
||||
type CourseSessionAttendanceCourseObjectType {
|
||||
id: ID!
|
||||
|
|
@ -443,26 +438,19 @@ type DueDateObjectType {
|
|||
course_session: CourseSessionObjectType!
|
||||
}
|
||||
|
||||
type CourseSessionObjectType {
|
||||
id: ID!
|
||||
created_at: DateTime!
|
||||
updated_at: DateTime!
|
||||
course: CourseObjectType!
|
||||
title: String!
|
||||
start_date: Date
|
||||
end_date: Date
|
||||
attendance_courses: [CourseSessionAttendanceCourseObjectType!]!
|
||||
assignments: [CourseSessionAssignmentObjectType!]!
|
||||
edoniq_tests: [CourseSessionEdoniqTestObjectType!]!
|
||||
users: [CourseSessionUserObjectsType!]!
|
||||
type AttendanceUserObjectType {
|
||||
user_id: UUID!
|
||||
status: AttendanceUserStatus!
|
||||
first_name: String
|
||||
last_name: String
|
||||
email: String
|
||||
}
|
||||
|
||||
"""
|
||||
The `Date` scalar type represents a Date
|
||||
value as specified by
|
||||
[iso8601](https://en.wikipedia.org/wiki/ISO_8601).
|
||||
"""
|
||||
scalar Date
|
||||
"""An enumeration."""
|
||||
enum AttendanceUserStatus {
|
||||
PRESENT
|
||||
ABSENT
|
||||
}
|
||||
|
||||
type CourseSessionAssignmentObjectType {
|
||||
id: ID!
|
||||
|
|
@ -591,12 +579,6 @@ type AssignmentCompletionObjectType {
|
|||
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 {
|
||||
"""
|
||||
Erforderlich. 150 Zeichen oder weniger. Nur Buchstaben, Ziffern und @/./+/-/_.
|
||||
|
|
@ -735,18 +717,46 @@ type CourseSessionUserExpertCircleType {
|
|||
slug: String!
|
||||
}
|
||||
|
||||
type AttendanceUserObjectType {
|
||||
user_id: UUID!
|
||||
status: AttendanceUserStatus!
|
||||
first_name: String
|
||||
last_name: String
|
||||
email: String
|
||||
type TopicObjectType implements CoursePageInterface {
|
||||
is_visible: Boolean!
|
||||
id: ID!
|
||||
title: String!
|
||||
slug: String!
|
||||
content_type: String!
|
||||
live: Boolean!
|
||||
translation_key: String!
|
||||
frontend_url: String!
|
||||
course: CourseObjectType
|
||||
circles: [CircleObjectType!]!
|
||||
}
|
||||
|
||||
"""An enumeration."""
|
||||
enum AttendanceUserStatus {
|
||||
PRESENT
|
||||
ABSENT
|
||||
type CircleObjectType implements CoursePageInterface {
|
||||
description: String!
|
||||
goals: String!
|
||||
is_base_circle: Boolean!
|
||||
id: ID!
|
||||
title: String!
|
||||
slug: String!
|
||||
content_type: String!
|
||||
live: Boolean!
|
||||
translation_key: String!
|
||||
frontend_url: String!
|
||||
course: CourseObjectType
|
||||
learning_sequences: [LearningSequenceObjectType!]!
|
||||
profiles: [String]!
|
||||
}
|
||||
|
||||
type LearningSequenceObjectType implements CoursePageInterface {
|
||||
icon: String!
|
||||
id: ID!
|
||||
title: String!
|
||||
slug: String!
|
||||
content_type: String!
|
||||
live: Boolean!
|
||||
translation_key: String!
|
||||
frontend_url: String!
|
||||
course: CourseObjectType
|
||||
learning_units: [LearningUnitObjectType!]!
|
||||
}
|
||||
|
||||
type LearningContentMediaLibraryObjectType implements CoursePageInterface & LearningContentInterface {
|
||||
|
|
@ -909,6 +919,7 @@ type CompetenceCertificateListObjectType implements CoursePageInterface {
|
|||
type Mutation {
|
||||
send_feedback(course_session_id: ID!, data: GenericScalar, learning_content_page_id: ID!, learning_content_type: String!, submitted: Boolean = false): SendFeedbackMutation
|
||||
update_course_session_attendance_course_users(attendance_user_list: [AttendanceUserInputType]!, id: ID!): AttendanceCourseUserMutation
|
||||
update_course_session_profile(input: CourseSessionProfileMutationInput!): CourseSessionProfileMutationPayload
|
||||
upsert_assignment_completion(assignment_id: ID!, assignment_user_id: UUID, completion_data_string: String, completion_status: AssignmentCompletionStatus, course_session_id: ID!, evaluation_passed: Boolean, evaluation_points: Float, evaluation_user_id: ID, initialize_completion: Boolean, learning_content_page_id: ID): AssignmentCompletionMutation
|
||||
}
|
||||
|
||||
|
|
@ -939,6 +950,27 @@ input AttendanceUserInputType {
|
|||
status: AttendanceUserStatus!
|
||||
}
|
||||
|
||||
type CourseSessionProfileMutationPayload {
|
||||
result: UpdateCourseProfileResult
|
||||
clientMutationId: String
|
||||
}
|
||||
|
||||
union UpdateCourseProfileResult = UpdateCourseProfileError | UpdateCourseProfileSuccess
|
||||
|
||||
type UpdateCourseProfileError {
|
||||
message: String
|
||||
}
|
||||
|
||||
type UpdateCourseProfileSuccess {
|
||||
user: CourseSessionUserType!
|
||||
}
|
||||
|
||||
input CourseSessionProfileMutationInput {
|
||||
course_profile: String!
|
||||
course_slug: String!
|
||||
clientMutationId: String
|
||||
}
|
||||
|
||||
type AssignmentCompletionMutation {
|
||||
assignment_completion: AssignmentCompletionObjectType
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,8 +34,11 @@ export const CourseSessionAssignmentObjectType = "CourseSessionAssignmentObjectT
|
|||
export const CourseSessionAttendanceCourseObjectType = "CourseSessionAttendanceCourseObjectType";
|
||||
export const CourseSessionEdoniqTestObjectType = "CourseSessionEdoniqTestObjectType";
|
||||
export const CourseSessionObjectType = "CourseSessionObjectType";
|
||||
export const CourseSessionProfileMutationInput = "CourseSessionProfileMutationInput";
|
||||
export const CourseSessionProfileMutationPayload = "CourseSessionProfileMutationPayload";
|
||||
export const CourseSessionUserExpertCircleType = "CourseSessionUserExpertCircleType";
|
||||
export const CourseSessionUserObjectsType = "CourseSessionUserObjectsType";
|
||||
export const CourseSessionUserType = "CourseSessionUserType";
|
||||
export const CourseStatisticsType = "CourseStatisticsType";
|
||||
export const DashboardConfigType = "DashboardConfigType";
|
||||
export const DashboardType = "DashboardType";
|
||||
|
|
@ -84,4 +87,7 @@ export const StatisticsCourseSessionsSelectionMetricType = "StatisticsCourseSess
|
|||
export const String = "String";
|
||||
export const TopicObjectType = "TopicObjectType";
|
||||
export const UUID = "UUID";
|
||||
export const UpdateCourseProfileError = "UpdateCourseProfileError";
|
||||
export const UpdateCourseProfileResult = "UpdateCourseProfileResult";
|
||||
export const UpdateCourseProfileSuccess = "UpdateCourseProfileSuccess";
|
||||
export const UserObjectType = "UserObjectType";
|
||||
|
|
|
|||
|
|
@ -58,3 +58,23 @@ export const UPSERT_ASSIGNMENT_COMPLETION_MUTATION = graphql(`
|
|||
}
|
||||
}
|
||||
`);
|
||||
|
||||
export const UPDATE_COURSE_PROFILE_MUTATION = graphql(`
|
||||
mutation UpdateCourseSessionProfile($input: CourseSessionProfileMutationInput!) {
|
||||
update_course_session_profile(input: $input) {
|
||||
clientMutationId
|
||||
result {
|
||||
__typename
|
||||
... on UpdateCourseProfileSuccess {
|
||||
user {
|
||||
id
|
||||
chosen_profile
|
||||
}
|
||||
}
|
||||
... on UpdateCourseProfileError {
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { graphql } from "@/gql";
|
||||
import {graphql} from "@/gql";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const CoursePageFragment = graphql(`
|
||||
|
|
@ -90,6 +90,45 @@ export const ASSIGNMENT_COMPLETION_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(
|
||||
$courseSlug: String!
|
||||
$courseSessionId: ID!
|
||||
|
|
@ -117,6 +156,10 @@ export const COMPETENCE_NAVI_CERTIFICATE_QUERY = graphql(`
|
|||
assignment_user {
|
||||
id
|
||||
}
|
||||
course_session {
|
||||
id
|
||||
title
|
||||
}
|
||||
}
|
||||
learning_content {
|
||||
...CoursePageFields
|
||||
|
|
@ -225,18 +268,28 @@ export const COURSE_SESSION_DETAIL_QUERY = graphql(`
|
|||
`);
|
||||
|
||||
export const COURSE_QUERY = graphql(`
|
||||
query courseQuery($slug: String!) {
|
||||
query courseQuery($slug: String!, $user: String) {
|
||||
course(slug: $slug) {
|
||||
id
|
||||
title
|
||||
slug
|
||||
category_name
|
||||
profiles
|
||||
course_session_users(id: $user) {
|
||||
id
|
||||
__typename
|
||||
chosen_profile
|
||||
course_session {
|
||||
id
|
||||
}
|
||||
}
|
||||
configuration {
|
||||
id
|
||||
enable_circle_documents
|
||||
enable_learning_mentor
|
||||
enable_competence_certificates
|
||||
is_uk
|
||||
is_vv
|
||||
}
|
||||
action_competences {
|
||||
competence_id
|
||||
|
|
@ -259,6 +312,8 @@ export const COURSE_QUERY = graphql(`
|
|||
circles {
|
||||
description
|
||||
goals
|
||||
profiles
|
||||
is_base_circle
|
||||
...CoursePageFields
|
||||
learning_sequences {
|
||||
icon
|
||||
|
|
|
|||
|
|
@ -403,6 +403,13 @@ function log(data: any) {
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<h2 class="mb-8 mt-8">Tags</h2>
|
||||
|
||||
<div class="mb-16 flex flex-col flex-wrap content-center gap-4 lg:flex-row">
|
||||
<button class="tag-active">Active</button>
|
||||
<button class="tag-inactive">Inactive</button>
|
||||
</div>
|
||||
|
||||
<h2 class="mb-8 mt-8">Dropdown (Work-in-progress)</h2>
|
||||
|
||||
<ItDropdownSelect
|
||||
|
|
|
|||
|
|
@ -1,11 +1,18 @@
|
|||
<script setup lang="ts">
|
||||
import { useCurrentCourseSession } from "@/composables";
|
||||
import router from "@/router";
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import type { CompetenceCertificateAssignment } from "@/types";
|
||||
import * as log from "loglevel";
|
||||
|
||||
log.debug("CompetenceAssignmentRow setup");
|
||||
|
||||
const currentCourseSession = useCurrentCourseSession();
|
||||
const { switchCourseSessionById } = useCourseSessionsStore();
|
||||
|
||||
export interface Props {
|
||||
assignment: CompetenceCertificateAssignment;
|
||||
showCourseSession: boolean;
|
||||
addBorderBottom?: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -19,6 +26,13 @@ const getIconName = () => {
|
|||
}
|
||||
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>
|
||||
|
||||
<template>
|
||||
|
|
@ -30,19 +44,26 @@ const getIconName = () => {
|
|||
<component :is="getIconName()" class="mr-4 hidden h-9 w-9 lg:block"></component>
|
||||
<div class="flex flex-col lg:w-[420px]">
|
||||
<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">
|
||||
<a
|
||||
<button
|
||||
v-if="assignment.learning_content"
|
||||
:href="assignment.frontend_url"
|
||||
class="link"
|
||||
data-cy="open-learning-content"
|
||||
@click="() => openInCircle(assignment)"
|
||||
>
|
||||
{{
|
||||
$t("general.im circle x anschauen", {
|
||||
x: assignment.learning_content.circle.title,
|
||||
})
|
||||
}}
|
||||
</a>
|
||||
</button>
|
||||
<span v-else>Fehler, Lerninhalt nicht korrekt verknüpft</span>
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
calcCompetenceCertificateGrade,
|
||||
competenceCertificateProgressStatusCount,
|
||||
} from "@/pages/competence/utils";
|
||||
import { useCurrentCourseSession } from "@/composables";
|
||||
|
||||
log.debug("CompetenceCertificateComponent setup");
|
||||
|
||||
|
|
@ -51,6 +52,15 @@ const frontendUrl = computed(() => {
|
|||
? props.frontendUrl
|
||||
: 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>
|
||||
|
||||
<template>
|
||||
|
|
@ -134,6 +144,7 @@ const frontendUrl = computed(() => {
|
|||
<CompetenceAssignmentRow
|
||||
:assignment="assignment"
|
||||
:add-border-bottom="index < competenceCertificate.assignments.length - 1"
|
||||
:show-course-session="showCourseSession"
|
||||
></CompetenceAssignmentRow>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,10 @@
|
|||
<script setup lang="ts">
|
||||
import log from "loglevel";
|
||||
import { computed } from "vue";
|
||||
import type { CompetenceCertificate } from "@/types";
|
||||
import { useCurrentCourseSession } from "@/composables";
|
||||
import {computed} from "vue";
|
||||
import type {CompetenceCertificate} from "@/types";
|
||||
import {useAllCompetenceCertificates} from "@/composables";
|
||||
import {getPreviousRoute} from "@/router/history";
|
||||
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<{
|
||||
courseSlug: string;
|
||||
|
|
@ -17,17 +14,6 @@ const props = defineProps<{
|
|||
|
||||
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(() => {
|
||||
return (
|
||||
|
|
@ -36,7 +22,16 @@ const competenceCertificates = computed(() => {
|
|||
);
|
||||
});
|
||||
|
||||
const {competenceCertificates} = useAllCompetenceCertificates(
|
||||
props.userId,
|
||||
props.courseSlug
|
||||
);
|
||||
|
||||
const certificate = computed(() => {
|
||||
if (!competenceCertificates) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return competenceCertificates.value.find((cc) =>
|
||||
cc.slug.endsWith(props.certificateSlug)
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,17 +1,10 @@
|
|||
<script setup lang="ts">
|
||||
import log from "loglevel";
|
||||
import { computed, onMounted } from "vue";
|
||||
import type { CompetenceCertificate } from "@/types";
|
||||
import { useCurrentCourseSession } from "@/composables";
|
||||
import {computed, onMounted} from "vue";
|
||||
import {useAllCompetenceCertificates} from "@/composables";
|
||||
import CompetenceCertificateComponent from "@/pages/competence/CompetenceCertificateComponent.vue";
|
||||
import {
|
||||
assignmentsUserPoints,
|
||||
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";
|
||||
import {assignmentsUserPoints, calcCompetencesTotalGrade,} from "@/pages/competence/utils";
|
||||
import {useRoute} from "vue-router";
|
||||
|
||||
const props = defineProps<{
|
||||
courseSlug: string;
|
||||
|
|
@ -22,27 +15,14 @@ log.debug("CompetenceCertificateListPage setup", props);
|
|||
|
||||
const route = useRoute();
|
||||
|
||||
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(() => {
|
||||
return (
|
||||
(certificatesQuery.data.value?.competence_certificate_list
|
||||
?.competence_certificates as unknown as CompetenceCertificate[]) ?? []
|
||||
);
|
||||
});
|
||||
const {competenceCertificates} = useAllCompetenceCertificates(
|
||||
props.userId,
|
||||
props.courseSlug
|
||||
);
|
||||
|
||||
const assignments = computed(() => {
|
||||
return competenceCertificates?.value?.flatMap((cc) => cc.assignments);
|
||||
return competenceCertificates?.value.flatMap((cc) => cc.assignments);
|
||||
});
|
||||
|
||||
const totalGrade = computed(() => {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import log from "loglevel";
|
||||
import { COMPETENCE_NAVI_CERTIFICATE_QUERY } from "@/graphql/queries";
|
||||
import { useQuery } from "@urql/vue";
|
||||
import { computed } from "vue";
|
||||
import type { CompetenceCertificate } from "@/types";
|
||||
import { useCurrentCourseSession } from "@/composables";
|
||||
import {computed} from "vue";
|
||||
import {useAllCompetenceCertificates, useCurrentCourseSession} from "@/composables";
|
||||
import {
|
||||
assignmentsUserPoints,
|
||||
calcCompetenceCertificateGrade,
|
||||
|
|
@ -12,9 +9,10 @@ import {
|
|||
competenceCertificateProgressStatusCount,
|
||||
} from "@/pages/competence/utils";
|
||||
import ItProgress from "@/components/ui/ItProgress.vue";
|
||||
import SelfEvaluationAndFeedbackOverview from "@/components/selfEvaluationFeedback/SelfEvaluationAndFeedbackOverview.vue";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import { useRouter } from "vue-router";
|
||||
import SelfEvaluationAndFeedbackOverview
|
||||
from "@/components/selfEvaluationFeedback/SelfEvaluationAndFeedbackOverview.vue";
|
||||
import {useUserStore} from "@/stores/user";
|
||||
import {useRouter} from "vue-router";
|
||||
|
||||
const props = defineProps<{
|
||||
courseSlug: string;
|
||||
|
|
@ -22,8 +20,6 @@ const props = defineProps<{
|
|||
|
||||
log.debug("CompetenceIndexPage setup", props);
|
||||
|
||||
const courseSession = useCurrentCourseSession();
|
||||
|
||||
const user = useUserStore();
|
||||
|
||||
const certificatesQuery = useQuery({
|
||||
|
|
@ -35,12 +31,10 @@ const certificatesQuery = useQuery({
|
|||
},
|
||||
});
|
||||
|
||||
const competenceCertificates = computed(() => {
|
||||
return (
|
||||
(certificatesQuery.data.value?.competence_certificate_list
|
||||
?.competence_certificates as unknown as CompetenceCertificate[]) ?? []
|
||||
);
|
||||
});
|
||||
const {competenceCertificates, isLoaded} = useAllCompetenceCertificates(
|
||||
user.id,
|
||||
props.courseSlug
|
||||
);
|
||||
|
||||
const allAssignments = computed(() => {
|
||||
return competenceCertificates.value.flatMap((cc) => cc.assignments);
|
||||
|
|
@ -52,8 +46,6 @@ const userPointsEvaluatedAssignments = computed(() => {
|
|||
|
||||
const currentCourseSession = useCurrentCourseSession();
|
||||
|
||||
const isLoaded = computed(() => !certificatesQuery.fetching.value);
|
||||
|
||||
const router = useRouter();
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import type { StatusCount } from "@/components/ui/ItProgress.vue";
|
||||
import { percentToRoundedGrade } from "@/services/assignmentService";
|
||||
import type { CompetenceCertificate, CompetenceCertificateAssignment } from "@/types";
|
||||
import dayjs from "dayjs";
|
||||
import _ from "lodash";
|
||||
|
||||
export function assignmentsMaxEvaluationPoints(
|
||||
|
|
@ -84,3 +85,55 @@ export function competenceCertificateProgressStatusCount(
|
|||
FAIL: 0,
|
||||
} 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;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
<script lang="ts" setup>
|
||||
import { COURSE_PROFILE_ALL_FILTER } from "@/constants";
|
||||
import LearningPathCircleListTile from "@/pages/learningPath/learningPathPage/LearningPathCircleListTile.vue";
|
||||
import type { LearningContentWithCompletion, TopicType } from "@/types";
|
||||
import { computed } from "vue";
|
||||
|
||||
interface Props {
|
||||
topic: TopicType;
|
||||
nextLearningContent?: LearningContentWithCompletion;
|
||||
filter?: string;
|
||||
}
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const filteredCircles = computed(() => {
|
||||
if (
|
||||
props.filter === undefined ||
|
||||
props.filter === "" ||
|
||||
props.filter === COURSE_PROFILE_ALL_FILTER
|
||||
) {
|
||||
return props.topic.circles;
|
||||
}
|
||||
return props.topic.circles.filter(
|
||||
(circle) =>
|
||||
circle.profiles.indexOf(props.filter as string) > -1 || circle.is_base_circle
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="pb-2 font-bold text-gray-700">
|
||||
{{ topic.title }}
|
||||
</div>
|
||||
<LearningPathCircleListTile
|
||||
v-for="circle in filteredCircles"
|
||||
:key="circle.id"
|
||||
:circle="circle"
|
||||
:next-learning-content="nextLearningContent"
|
||||
></LearningPathCircleListTile>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1,26 +1,23 @@
|
|||
<script setup lang="ts">
|
||||
import LearningPathCircleListTile from "@/pages/learningPath/learningPathPage/LearningPathCircleListTile.vue";
|
||||
import { computed } from "vue";
|
||||
import type { LearningContentWithCompletion, LearningPathType } from "@/types";
|
||||
import { computed } from "vue";
|
||||
import LearningPathListTopic from "./LearningPathListTopic.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
learningPath: LearningPathType | undefined;
|
||||
nextLearningContent: LearningContentWithCompletion | undefined;
|
||||
filter?: string;
|
||||
}>();
|
||||
|
||||
const topics = computed(() => props.learningPath?.topics ?? []);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-for="topic in topics" :key="topic.title">
|
||||
<div class="pb-2 font-bold text-gray-700">
|
||||
{{ topic.title }}
|
||||
</div>
|
||||
<LearningPathCircleListTile
|
||||
v-for="circle in topic.circles"
|
||||
:key="circle.id"
|
||||
:circle="circle"
|
||||
:next-learning-content="props.nextLearningContent"
|
||||
></LearningPathCircleListTile>
|
||||
</div>
|
||||
<LearningPathListTopic
|
||||
v-for="topic in topics"
|
||||
:key="topic.title"
|
||||
:topic="topic"
|
||||
:next-learning-content="nextLearningContent"
|
||||
:filter="filter"
|
||||
/>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import LearningPathListView from "@/pages/learningPath/learningPathPage/Learning
|
|||
import LearningPathPathView from "@/pages/learningPath/learningPathPage/LearningPathPathView.vue";
|
||||
import CircleProgress from "@/pages/learningPath/learningPathPage/LearningPathProgress.vue";
|
||||
import LearningPathTopics from "@/pages/learningPath/learningPathPage/LearningPathTopics.vue";
|
||||
import LearningPathProfileFilter from "@/pages/learningPath/learningPathPage/LearningPathProfileFilter.vue";
|
||||
import type { ViewType } from "@/pages/learningPath/learningPathPage/LearningPathViewSwitch.vue";
|
||||
import LearningPathViewSwitch from "@/pages/learningPath/learningPathPage/LearningPathViewSwitch.vue";
|
||||
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core";
|
||||
|
|
@ -13,6 +14,9 @@ import {
|
|||
useCurrentCourseSession,
|
||||
} from "@/composables";
|
||||
import CourseSessionDueDatesList from "@/components/dueDates/CourseSessionDueDatesList.vue";
|
||||
import { useMutation } from "@urql/vue";
|
||||
import { UPDATE_COURSE_PROFILE_MUTATION } from "@/graphql/mutations";
|
||||
import { filterCircles, useCourseFilter } from "./utils";
|
||||
|
||||
const props = defineProps<{
|
||||
courseSlug: string;
|
||||
|
|
@ -33,9 +37,28 @@ const course = computed(() => lpQueryResult.course.value);
|
|||
|
||||
const courseSession = useCurrentCourseSession();
|
||||
|
||||
const { inProgressCirclesCount, circlesCount } = useCourseCircleProgress(
|
||||
lpQueryResult.circles
|
||||
);
|
||||
const { filter } = useCourseFilter(props.courseSlug);
|
||||
|
||||
const filteredCircles = computed(() => {
|
||||
if (lpQueryResult.circles.value === undefined) {
|
||||
return [];
|
||||
}
|
||||
return filterCircles(filter.value, lpQueryResult.circles.value);
|
||||
});
|
||||
|
||||
const { inProgressCirclesCount, circlesCount } =
|
||||
useCourseCircleProgress(filteredCircles);
|
||||
|
||||
const updateCourseProfileMutation = useMutation(UPDATE_COURSE_PROFILE_MUTATION);
|
||||
|
||||
const updateCourseProfile = (profile: string) => {
|
||||
updateCourseProfileMutation.executeMutation({
|
||||
input: {
|
||||
course_profile: profile,
|
||||
course_slug: props.courseSlug,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const changeViewType = (viewType: ViewType) => {
|
||||
selectedView.value = viewType;
|
||||
|
|
@ -46,7 +69,9 @@ const changeViewType = (viewType: ViewType) => {
|
|||
<template>
|
||||
<div class="flex flex-col">
|
||||
<!-- Top -->
|
||||
<div class="flex flex-row justify-between space-x-8 bg-gray-200 p-6 sm:p-12">
|
||||
<div
|
||||
class="flex flex-col justify-between gap-8 bg-gray-200 p-6 sm:p-12 xl:flex-row"
|
||||
>
|
||||
<!-- Left -->
|
||||
<div class="flex flex-col justify-between lg:w-1/2">
|
||||
<div>
|
||||
|
|
@ -64,15 +89,21 @@ const changeViewType = (viewType: ViewType) => {
|
|||
></CircleProgress>
|
||||
</div>
|
||||
|
||||
<!-- Right -->
|
||||
<div v-if="!useMobileLayout" class="flex-grow">
|
||||
<!-- todo: find out when to display CourseSessionDueDatesList -->
|
||||
<div v-if="!useMobileLayout && false" class="flex-grow">
|
||||
<CourseSessionDueDatesList
|
||||
:course-session-id="courseSession.id"
|
||||
:max-count="2"
|
||||
></CourseSessionDueDatesList>
|
||||
</div>
|
||||
<!-- Right -->
|
||||
<LearningPathProfileFilter
|
||||
v-if="course?.configuration.is_vv"
|
||||
:profiles="course?.profiles"
|
||||
:selected="filter"
|
||||
@select="updateCourseProfile"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Bottom -->
|
||||
<div class="bg-white">
|
||||
<div v-if="lpQueryResult.learningPath">
|
||||
|
|
@ -101,6 +132,7 @@ const changeViewType = (viewType: ViewType) => {
|
|||
<LearningPathPathView
|
||||
:learning-path="learningPath"
|
||||
:use-mobile-layout="useMobileLayout"
|
||||
:filter="filter"
|
||||
:next-learning-content="lpQueryResult.nextLearningContent.value"
|
||||
></LearningPathPathView>
|
||||
</div>
|
||||
|
|
@ -114,6 +146,7 @@ const changeViewType = (viewType: ViewType) => {
|
|||
<LearningPathListView
|
||||
:learning-path="learningPath"
|
||||
:next-learning-content="lpQueryResult.nextLearningContent.value"
|
||||
:filter="filter"
|
||||
></LearningPathListView>
|
||||
</div>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import type { LearningContentWithCompletion, TopicType } from "@/types";
|
||||
import LearningPathCircleColumn from "./LearningPathCircleColumn.vue";
|
||||
import { filterCircles } from "./utils";
|
||||
|
||||
interface Props {
|
||||
topic: TopicType;
|
||||
topicIndex: number;
|
||||
nextLearningContent?: LearningContentWithCompletion;
|
||||
overrideCircleUrlBase?: string;
|
||||
filter?: string;
|
||||
isLastTopic: boolean;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const isFirstCircle = (circleIndex: number) =>
|
||||
props.topicIndex === 0 && circleIndex === 0;
|
||||
|
||||
const isLastCircle = (circleIndex: number, numCircles: number) =>
|
||||
props.isLastTopic && circleIndex === numCircles - 1;
|
||||
|
||||
const filteredCircles = computed(() => {
|
||||
return filterCircles(props.filter, props.topic.circles);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="basis-40 border-l border-gray-500 first:ml-6 first:sm:ml-12">
|
||||
<p
|
||||
:id="`topic-${topic.slug}`"
|
||||
class="inline-block h-12 self-start px-4 font-bold text-gray-800"
|
||||
data-cy="lp-topic"
|
||||
>
|
||||
{{ topic.title }}
|
||||
</p>
|
||||
<div class="flex flex-row pt-6">
|
||||
<LearningPathCircleColumn
|
||||
v-for="(circle, circleIndex) in filteredCircles"
|
||||
:key="circle.id"
|
||||
:circle="circle"
|
||||
:next-learning-content="nextLearningContent"
|
||||
:is-first-circle="isFirstCircle(circleIndex)"
|
||||
:is-last-circle="isLastCircle(circleIndex, filteredCircles.length)"
|
||||
:override-circle-url="
|
||||
overrideCircleUrlBase ? `${overrideCircleUrlBase}/${circle.slug}` : undefined
|
||||
"
|
||||
></LearningPathCircleColumn>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
<script setup lang="ts">
|
||||
import LearningPathCircleColumn from "@/pages/learningPath/learningPathPage/LearningPathCircleColumn.vue";
|
||||
import LearningPathScrollButton from "@/pages/learningPath/learningPathPage/LearningPathScrollButton.vue";
|
||||
import { useScroll } from "@vueuse/core";
|
||||
import { ref } from "vue";
|
||||
import { computed, nextTick, ref, watch } from "vue";
|
||||
import type { LearningContentWithCompletion, LearningPathType } from "@/types";
|
||||
import LoadingSpinner from "@/components/ui/LoadingSpinner.vue";
|
||||
import LearningPathPathTopic from "./LearningPathPathTopic.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
learningPath: LearningPathType | undefined;
|
||||
|
|
@ -12,6 +12,7 @@ const props = defineProps<{
|
|||
useMobileLayout: boolean;
|
||||
hideButtons?: boolean;
|
||||
overrideCircleUrlBase?: string;
|
||||
filter?: string;
|
||||
}>();
|
||||
|
||||
const scrollIncrement = 600;
|
||||
|
|
@ -19,13 +20,6 @@ const scrollIncrement = 600;
|
|||
const learnPathDiagram = ref<HTMLElement | null>(null);
|
||||
const { x, arrivedState } = useScroll(learnPathDiagram, { behavior: "smooth" });
|
||||
|
||||
const isFirstCircle = (topicIndex: number, circleIndex: number) =>
|
||||
topicIndex === 0 && circleIndex === 0;
|
||||
|
||||
const isLastCircle = (topicIndex: number, circleIndex: number, numCircles: number) =>
|
||||
topicIndex === (props.learningPath?.topics ?? []).length - 1 &&
|
||||
circleIndex === numCircles - 1;
|
||||
|
||||
const scrollRight = () => scrollLearnPathDiagram(scrollIncrement);
|
||||
|
||||
const scrollLeft = () => scrollLearnPathDiagram(-scrollIncrement);
|
||||
|
|
@ -33,6 +27,22 @@ const scrollLeft = () => scrollLearnPathDiagram(-scrollIncrement);
|
|||
const scrollLearnPathDiagram = (offset: number) => {
|
||||
x.value += offset;
|
||||
};
|
||||
|
||||
const topics = computed(() => props.learningPath?.topics ?? []);
|
||||
|
||||
watch(
|
||||
() => props.filter,
|
||||
() => {
|
||||
// we need to update the scroll state of the element, otherwise the arrows won't match the scroll state
|
||||
// https://github.com/vueuse/vueuse/issues/2875
|
||||
nextTick(() => {
|
||||
if (learnPathDiagram.value) {
|
||||
const scrollEvent = new Event("scroll");
|
||||
learnPathDiagram.value.dispatchEvent(scrollEvent);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -50,37 +60,16 @@ const scrollLearnPathDiagram = (offset: number) => {
|
|||
ref="learnPathDiagram"
|
||||
class="no-scrollbar flex h-96 snap-x flex-row overflow-auto py-5 sm:py-10"
|
||||
>
|
||||
<div
|
||||
<LearningPathPathTopic
|
||||
v-for="(topic, topicIndex) in props.learningPath?.topics ?? []"
|
||||
:key="topic.title"
|
||||
class="border-l border-gray-500"
|
||||
:class="topicIndex == 0 ? 'ml-6 sm:ml-12' : ''"
|
||||
>
|
||||
<p
|
||||
:id="`topic-${topic.slug}`"
|
||||
class="inline-block h-12 self-start px-4 font-bold text-gray-800"
|
||||
data-cy="lp-topic"
|
||||
>
|
||||
{{ topic.title }}
|
||||
</p>
|
||||
<div class="flex flex-row pt-6">
|
||||
<LearningPathCircleColumn
|
||||
v-for="(circle, circleIndex) in topic.circles"
|
||||
:key="circle.id"
|
||||
:circle="circle"
|
||||
:next-learning-content="props.nextLearningContent"
|
||||
:is-first-circle="isFirstCircle(topicIndex, circleIndex)"
|
||||
:is-last-circle="
|
||||
isLastCircle(topicIndex, circleIndex, topic.circles.length)
|
||||
"
|
||||
:override-circle-url="
|
||||
props.overrideCircleUrlBase
|
||||
? `${props.overrideCircleUrlBase}/${circle.slug}`
|
||||
: undefined
|
||||
"
|
||||
></LearningPathCircleColumn>
|
||||
</div>
|
||||
</div>
|
||||
:topic-index="topicIndex"
|
||||
:topic="topic"
|
||||
:next-learning-content="nextLearningContent"
|
||||
:override-circle-url-base="overrideCircleUrlBase"
|
||||
:filter="filter"
|
||||
:is-last-topic="topicIndex === topics.length - 1"
|
||||
/>
|
||||
</div>
|
||||
<LearningPathScrollButton
|
||||
v-show="!arrivedState.right"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
<script lang="ts" setup>
|
||||
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
|
||||
import { COURSE_PROFILE_ALL_FILTER } from "@/constants";
|
||||
import type { DropdownSelectable } from "@/types";
|
||||
import { useTranslation } from "i18next-vue";
|
||||
import { computed } from "vue";
|
||||
|
||||
interface Props {
|
||||
profiles?: string[];
|
||||
selected?: string;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits(["select"]);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const items = computed(() => {
|
||||
return props.profiles?.map((p) => ({ id: p, name: t(`profile.${p}`) })) || [];
|
||||
});
|
||||
|
||||
const selectedItem = computed(() => {
|
||||
if (props.selected) {
|
||||
return { id: props.selected || "", name: t(`profile.${props.selected}`) };
|
||||
}
|
||||
return {
|
||||
id: COURSE_PROFILE_ALL_FILTER,
|
||||
name: t(`profile.${COURSE_PROFILE_ALL_FILTER}`),
|
||||
};
|
||||
});
|
||||
|
||||
const updateFilter = (e: DropdownSelectable) => {
|
||||
emit("select", e.id);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-grow">
|
||||
<h5 class="mb-4">{{ $t("a.Zulassungsprofil") }}:</h5>
|
||||
<div class="mb-4 flex gap-4">
|
||||
<ItDropdownSelect
|
||||
:items="items"
|
||||
class="min-w-[18rem]"
|
||||
:model-value="selectedItem"
|
||||
@update:model-value="updateFilter"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1,3 +1,6 @@
|
|||
import { useCurrentCourseSession } from "@/composables";
|
||||
import { COURSE_PROFILE_ALL_FILTER } from "@/constants";
|
||||
import { COURSE_QUERY } from "@/graphql/queries";
|
||||
import type {
|
||||
CircleSectorData,
|
||||
CircleSectorProgress,
|
||||
|
|
@ -7,6 +10,8 @@ import {
|
|||
someFinishedInLearningSequence,
|
||||
} from "@/services/circle";
|
||||
import type { CircleType } from "@/types";
|
||||
import { useQuery } from "@urql/vue";
|
||||
import { computed } from "vue";
|
||||
|
||||
export function calculateCircleSectorData(circle: CircleType): CircleSectorData[] {
|
||||
return circle.learning_sequences.map((ls) => {
|
||||
|
|
@ -21,3 +26,42 @@ export function calculateCircleSectorData(circle: CircleType): CircleSectorData[
|
|||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function useCourseFilter(courseSlug: string, courseSessionId?: string) {
|
||||
const csId = computed(() => {
|
||||
if (courseSessionId) {
|
||||
return courseSessionId;
|
||||
}
|
||||
// assume we're on a page with a current course session
|
||||
const courseSession = useCurrentCourseSession();
|
||||
return courseSession.value.id;
|
||||
});
|
||||
const courseReactiveResult = useQuery({
|
||||
query: COURSE_QUERY,
|
||||
variables: { slug: courseSlug },
|
||||
});
|
||||
const courseReactive = computed(() => courseReactiveResult.data.value?.course);
|
||||
const courseSessionUser = computed(() => {
|
||||
return courseReactive.value?.course_session_users.find(
|
||||
(e) => e?.course_session.id === csId.value
|
||||
);
|
||||
});
|
||||
const filter = computed(() => {
|
||||
return courseSessionUser.value?.chosen_profile || "";
|
||||
});
|
||||
|
||||
return {
|
||||
filter,
|
||||
courseReactive,
|
||||
courseSessionUser,
|
||||
};
|
||||
}
|
||||
|
||||
export function filterCircles(filter: string | undefined, circles: CircleType[]) {
|
||||
if (filter === undefined || filter === "" || filter === COURSE_PROFILE_ALL_FILTER) {
|
||||
return circles;
|
||||
}
|
||||
return circles.filter(
|
||||
(circle) => circle.profiles.indexOf(filter as string) > -1 || circle.is_base_circle
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,78 @@
|
|||
<script setup lang="ts">
|
||||
import WizardPage from "@/components/onboarding/WizardPage.vue";
|
||||
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
|
||||
import { useEntities } from "@/services/entities";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import type { DropdownSelectable } from "@/types";
|
||||
import { useTranslation } from "i18next-vue";
|
||||
import { computed, ref, watch } from "vue";
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const user = useUserStore();
|
||||
|
||||
const { courseProfiles } = useEntities();
|
||||
|
||||
const selectedCourseProfile = ref({
|
||||
id: 0,
|
||||
name: t("a.Auswählen"),
|
||||
});
|
||||
|
||||
const validCourseProfile = computed(() => {
|
||||
return selectedCourseProfile.value.id !== 0;
|
||||
});
|
||||
|
||||
watch(selectedCourseProfile, async (courseProfile: DropdownSelectable) => {
|
||||
const courseProfileWithCode = courseProfiles.value.find(
|
||||
(cp) => cp.id === courseProfile.id
|
||||
);
|
||||
if (courseProfileWithCode) {
|
||||
user.updateChosenCourseProfile(courseProfileWithCode);
|
||||
}
|
||||
});
|
||||
|
||||
const courseProfilesToDropdown = computed(() => {
|
||||
return courseProfiles.value.map((profile) => ({
|
||||
...profile,
|
||||
name: t(`profile.${profile.code}`),
|
||||
}));
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<WizardPage :step="2">
|
||||
<template #content>
|
||||
<h2 class="my-10" data-cy="account-course-profile-title">
|
||||
{{ $t("a.Zulassungsprofil auswählen") }}
|
||||
</h2>
|
||||
|
||||
<p class="mb-6 max-w-md hyphens-none">
|
||||
{{
|
||||
$t(
|
||||
"a.Wähle ein Zulassungsprofil, damit du deinen Lehrgang an der richtigen Stelle beginnen kannst. Du kannst ihn später jederzeit ändern."
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
|
||||
<ItDropdownSelect
|
||||
v-model="selectedCourseProfile"
|
||||
:items="courseProfilesToDropdown"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<router-link v-slot="{ navigate }" :to="{ name: 'checkoutAddress' }" custom>
|
||||
<button
|
||||
:disabled="!validCourseProfile"
|
||||
class="btn-blue flex items-center"
|
||||
role="link"
|
||||
data-cy="continue-button"
|
||||
@click="navigate"
|
||||
>
|
||||
{{ $t("general.next") }}
|
||||
<it-icon-arrow-right class="it-icon ml-2 h-6 w-6" />
|
||||
</button>
|
||||
</router-link>
|
||||
</template>
|
||||
</WizardPage>
|
||||
</template>
|
||||
|
|
@ -229,6 +229,7 @@ const executePayment = async () => {
|
|||
redirect_url: fullHost,
|
||||
address: addressData,
|
||||
product: props.courseType,
|
||||
chosen_profile: user.chosen_profile?.id || "",
|
||||
with_cembra_byjuno_invoice: address.value.payment_method === "cembra_byjuno",
|
||||
device_fingerprint_session_key: getLocalSessionKey(),
|
||||
}).then((res: any) => {
|
||||
|
|
|
|||
|
|
@ -47,6 +47,14 @@ const { t } = useTranslation();
|
|||
$t("Füge dein Profilbild hinzu und ergänze die fehlenden Angaben.")
|
||||
}}
|
||||
</li>
|
||||
<li class="relative pl-8">
|
||||
<span class="font-bold">{{ $t("a.Zulassungsprofil auswählen") }}:</span>
|
||||
{{
|
||||
$t(
|
||||
"a.Wähle ein Zulassungsprofil, damit du deinen Lehrgang an der richtigen Stelle beginnen kannst."
|
||||
)
|
||||
}}
|
||||
</li>
|
||||
<li class="relative pl-8">
|
||||
<span class="font-bold">{{ $t("a.Lehrgang kaufen") }}:</span>
|
||||
{{
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
<script setup lang="ts">
|
||||
import { calculateCircleSectorData } from "@/pages/learningPath/learningPathPage/utils";
|
||||
import { useCourseDataWithCompletion } from "@/composables";
|
||||
import UserProfileContent from "@/components/userProfile/UserProfileContent.vue";
|
||||
import LearningPathCircle from "@/pages/learningPath/learningPathPage/LearningPathCircle.vue";
|
||||
import LearningSequence from "@/pages/learningPath/circlePage/LearningSequence.vue";
|
||||
import { ref, watch } from "vue";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import type { CircleType } from "@/types";
|
||||
import { COURSE_QUERY } from "@/graphql/queries";
|
||||
import { useQuery } from "@urql/vue";
|
||||
import UserProfileTopicList from "./UserProfileTopicList.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
userId: string;
|
||||
|
|
@ -20,6 +21,23 @@ function selectCircle(circle: CircleType) {
|
|||
selectedCircle.value = circle;
|
||||
}
|
||||
|
||||
const courseReactiveResult = useQuery({
|
||||
query: COURSE_QUERY,
|
||||
variables: { slug: props.courseSlug, user: props.userId },
|
||||
});
|
||||
|
||||
const courseReactive = computed(() => courseReactiveResult.data.value?.course);
|
||||
const courseSessionUsers = computed(() => {
|
||||
return courseReactive.value?.course_session_users;
|
||||
});
|
||||
|
||||
const chosenProfile = computed(() => {
|
||||
if (courseSessionUsers.value && courseSessionUsers.value.length > 0) {
|
||||
return courseSessionUsers.value[0]?.chosen_profile;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
watch(lpQueryResult.learningPath, () => {
|
||||
if (lpQueryResult.learningPath?.value?.topics?.length) {
|
||||
selectCircle(lpQueryResult.learningPath.value.topics[0].circles[0]);
|
||||
|
|
@ -30,28 +48,21 @@ watch(lpQueryResult.learningPath, () => {
|
|||
<template>
|
||||
<UserProfileContent>
|
||||
<template #side>
|
||||
<div
|
||||
<div v-if="chosenProfile">
|
||||
<h3 class="mb-4 text-base font-bold">
|
||||
Zulassungsprofil:
|
||||
<br />
|
||||
{{ $t(`profile.${chosenProfile}`) }}
|
||||
</h3>
|
||||
</div>
|
||||
<UserProfileTopicList
|
||||
v-for="topic in lpQueryResult.learningPath?.value?.topics ?? []"
|
||||
:key="topic.id"
|
||||
class="mb-4"
|
||||
>
|
||||
<h4 class="mb-1 font-semibold text-gray-800">
|
||||
{{ topic.title }}
|
||||
</h4>
|
||||
<button
|
||||
v-for="circle in topic.circles"
|
||||
:key="circle.id"
|
||||
class="flex w-full items-center space-x-2 p-2 pr-4 hover:bg-gray-200 lg:pr-8"
|
||||
:class="{ 'bg-gray-200': selectedCircle === circle }"
|
||||
@click="selectCircle(circle)"
|
||||
>
|
||||
<LearningPathCircle
|
||||
:sectors="calculateCircleSectorData(circle)"
|
||||
class="h-10 w-10 snap-center rounded-full bg-white p-0.5"
|
||||
></LearningPathCircle>
|
||||
<span>{{ circle.title }}</span>
|
||||
</button>
|
||||
</div>
|
||||
:topic="topic"
|
||||
:filter="chosenProfile"
|
||||
:selected-circle="selectedCircle"
|
||||
@select-circle="selectCircle($event)"
|
||||
/>
|
||||
</template>
|
||||
<template #main>
|
||||
<ol v-if="selectedCircle" class="flex-auto bg-gray-200 px-6 py-4 lg:px-16">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
<script setup lang="ts">
|
||||
import type { CircleType, TopicType } from "@/types";
|
||||
import { calculateCircleSectorData } from "@/pages/learningPath/learningPathPage/utils";
|
||||
import LearningPathCircle from "../learningPath/learningPathPage/LearningPathCircle.vue";
|
||||
import { computed } from "vue";
|
||||
import { COURSE_PROFILE_ALL_FILTER } from "@/constants";
|
||||
|
||||
interface Props {
|
||||
topic: TopicType;
|
||||
selectedCircle?: CircleType;
|
||||
filter?: string;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
defineEmits(["select-circle"]);
|
||||
|
||||
const filteredCircles = computed(() => {
|
||||
const circles = props.topic.circles;
|
||||
const filter = props.filter;
|
||||
if (filter === undefined || filter === "" || filter === COURSE_PROFILE_ALL_FILTER) {
|
||||
return circles;
|
||||
}
|
||||
return circles.filter(
|
||||
(circle) => circle.profiles.indexOf(filter as string) > -1 || circle.is_base_circle
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mb-4">
|
||||
<h4 class="mb-1 font-semibold text-gray-800">{{ topic.title }} {{ filter }}</h4>
|
||||
<button
|
||||
v-for="circle in filteredCircles"
|
||||
:key="circle.id"
|
||||
class="flex w-full items-center space-x-2 p-2 pr-4 hover:bg-gray-200 lg:pr-8"
|
||||
:class="{ 'bg-gray-200': selectedCircle === circle }"
|
||||
@click="$emit('select-circle', circle)"
|
||||
>
|
||||
<LearningPathCircle
|
||||
:sectors="calculateCircleSectorData(circle)"
|
||||
class="h-10 w-10 snap-center rounded-full bg-white p-0.5"
|
||||
></LearningPathCircle>
|
||||
<span>{{ circle.title }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -61,7 +61,7 @@ describe("Onboarding", () => {
|
|||
mockNext
|
||||
);
|
||||
expect(mockNext).toHaveBeenCalledWith({
|
||||
name: "checkoutAddress",
|
||||
name: "accountCourseProfile",
|
||||
params: { courseType: testCase },
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -417,6 +417,12 @@ const router = createRouter({
|
|||
component: () => import("@/pages/onboarding/uk/SetupComplete.vue"),
|
||||
name: "setupComplete",
|
||||
},
|
||||
{
|
||||
path: "account/course-profile",
|
||||
component: () => import("@/pages/onboarding/vv/AccountCourseProfile.vue"),
|
||||
name: "accountCourseProfile",
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "checkout/address",
|
||||
component: () => import("@/pages/onboarding/vv/CheckoutAddress.vue"),
|
||||
|
|
|
|||
|
|
@ -13,14 +13,21 @@ export type Country = {
|
|||
name: string;
|
||||
};
|
||||
|
||||
export type CourseProfile = {
|
||||
id: number;
|
||||
code: string;
|
||||
};
|
||||
|
||||
export function useEntities() {
|
||||
const countries: Ref<Country[]> = ref([]);
|
||||
const organisations: Ref<Organisation[]> = ref([]);
|
||||
const courseProfiles: Ref<CourseProfile[]> = ref([]);
|
||||
|
||||
itGetCached("/api/core/entities/").then((res: any) => {
|
||||
countries.value = res.countries;
|
||||
organisations.value = res.organisations;
|
||||
courseProfiles.value = res.courseProfiles;
|
||||
});
|
||||
|
||||
return { organisations, countries };
|
||||
return { organisations, countries, courseProfiles };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export function profileNextRoute(courseType: string | string[]) {
|
|||
}
|
||||
// vv- -> vv-de, vv-fr or vv-it
|
||||
if (isString(courseType) && startsWith(courseType, "vv-")) {
|
||||
return "checkoutAddress";
|
||||
return "accountCourseProfile";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { bustItGetCache, itGetCached, itPost } from "@/fetchHelpers";
|
||||
import { setI18nLanguage } from "@/i18nextWrapper";
|
||||
import type { Country } from "@/services/entities";
|
||||
import type { Country, CourseProfile } from "@/services/entities";
|
||||
import { directUpload } from "@/services/files";
|
||||
import dayjs from "dayjs";
|
||||
import { defineStore } from "pinia";
|
||||
|
|
@ -44,6 +44,7 @@ export interface User {
|
|||
organisation_postal_code: string;
|
||||
organisation_city: string;
|
||||
organisation_country: Country | null;
|
||||
chosen_profile?: CourseProfile;
|
||||
}
|
||||
|
||||
let defaultLanguage: AvailableLanguages = "de";
|
||||
|
|
@ -89,6 +90,7 @@ const initialUserState: User = {
|
|||
organisation_postal_code: "",
|
||||
organisation_city: "",
|
||||
organisation_country: null,
|
||||
chosen_profile: undefined,
|
||||
};
|
||||
|
||||
async function setLocale(language: AvailableLanguages) {
|
||||
|
|
@ -176,5 +178,8 @@ export const useUserStore = defineStore({
|
|||
await itPost("/api/core/me/", profileData, { method: "PUT" });
|
||||
Object.assign(this.$state, profileData);
|
||||
},
|
||||
updateChosenCourseProfile(courseProfile: CourseProfile) {
|
||||
Object.assign(this.$state, { chosen_profile: courseProfile });
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -202,6 +202,7 @@ export interface Course {
|
|||
title: string;
|
||||
category_name: string;
|
||||
slug: string;
|
||||
profiles: string[];
|
||||
configuration: CourseConfiguration;
|
||||
}
|
||||
|
||||
|
|
@ -408,6 +409,10 @@ export interface CompetenceCertificateAssignment extends BaseCourseWagtailPage {
|
|||
evaluation_points_reason: string;
|
||||
evaluation_max_points: number | null;
|
||||
evaluation_passed: boolean | null;
|
||||
course_session: {
|
||||
id: string;
|
||||
title: string;
|
||||
};
|
||||
}[];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ textarea {
|
|||
}
|
||||
|
||||
.link {
|
||||
@apply underline underline-offset-2;
|
||||
@apply cursor-pointer underline underline-offset-2;
|
||||
}
|
||||
|
||||
.link-large {
|
||||
|
|
@ -167,6 +167,14 @@ textarea {
|
|||
top: 1rem;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.tag-inactive {
|
||||
@apply rounded-full border-2 border-blue-900 px-4 py-2 font-semibold text-blue-900;
|
||||
}
|
||||
|
||||
.tag-active {
|
||||
@apply rounded-full bg-blue-900 px-4 py-2 font-semibold text-white;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
|
|
@ -181,7 +189,9 @@ textarea {
|
|||
}
|
||||
|
||||
.no-scrollbar {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none;
|
||||
/* IE and Edge */
|
||||
scrollbar-width: none;
|
||||
/* Firefox */
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,22 @@
|
|||
// ids for cypress test data
|
||||
export const ADMIN_USER_ID = "872efd96-3bd7-4a1e-a239-2d72cad9f604"
|
||||
export const TEST_SUPERVISOR1_USER_ID = "a9a8b741-f115-4521-af2d-7dfef673b8c5"
|
||||
export const TEST_TRAINER1_USER_ID = "b9e71f59-c44f-4290-b93a-9b3151e9a2fc"
|
||||
export const TEST_TRAINER2_USER_ID = "299941ae-1e4b-4f45-8180-876c3ad340b4"
|
||||
export const TEST_STUDENT1_USER_ID = "65c73ad0-6d53-43a9-a4a4-64143f27b03a"
|
||||
export const TEST_STUDENT2_USER_ID = "19c40d94-15cc-4198-aaad-ef707c4b0900"
|
||||
export const TEST_STUDENT3_USER_ID = "bcf94dba-53bc-474b-a22d-e4af39aa042b"
|
||||
export const TEST_MENTOR1_USER_ID = "d1f5f5a9-5b0a-4e1a-9e1a-9e9b5b5e1b1b"
|
||||
export const TEST_STUDENT1_VV_USER_ID = "5ff59857-8de5-415e-a387-4449f9a0337a"
|
||||
export const TEST_STUDENT2_VV_AND_VV_MENTOR_USER_ID = "7e8ebf0b-e6e2-4022-88f4-6e663ba0a9db"
|
||||
export const TEST_USER_EMPTY_ID = "daecbabe-4ab9-4edf-a71f-4119042ccb02"
|
||||
|
||||
export const ADMIN_USER_ID = "872efd96-3bd7-4a1e-a239-2d72cad9f604";
|
||||
export const TEST_SUPERVISOR1_USER_ID = "a9a8b741-f115-4521-af2d-7dfef673b8c5";
|
||||
export const TEST_TRAINER1_USER_ID = "b9e71f59-c44f-4290-b93a-9b3151e9a2fc";
|
||||
export const TEST_TRAINER2_USER_ID = "299941ae-1e4b-4f45-8180-876c3ad340b4";
|
||||
export const TEST_STUDENT1_USER_ID = "65c73ad0-6d53-43a9-a4a4-64143f27b03a";
|
||||
export const TEST_STUDENT2_USER_ID = "19c40d94-15cc-4198-aaad-ef707c4b0900";
|
||||
export const TEST_STUDENT3_USER_ID = "bcf94dba-53bc-474b-a22d-e4af39aa042b";
|
||||
export const TEST_MENTOR1_USER_ID = "d1f5f5a9-5b0a-4e1a-9e1a-9e9b5b5e1b1b";
|
||||
export const TEST_STUDENT1_VV_USER_ID = "5ff59857-8de5-415e-a387-4449f9a0337a";
|
||||
export const TEST_STUDENT2_VV_AND_VV_MENTOR_USER_ID =
|
||||
"7e8ebf0b-e6e2-4022-88f4-6e663ba0a9db";
|
||||
export const TEST_USER_EMPTY_ID = "daecbabe-4ab9-4edf-a71f-4119042ccb02";
|
||||
|
||||
export const TEST_COURSE_SESSION_BERN_ID = -1;
|
||||
export const TEST_COURSE_SESSION_ZURICH_ID = -2;
|
||||
export const TEST_COURSE_SESSION_VV_ID = 1;
|
||||
|
||||
export const COURSE_PROFILE_LEBEN_ID = -1;
|
||||
export const COURSE_PROFILE_NICHTLEBEN_ID = -2;
|
||||
export const COURSE_PROFILE_KRANKENZUSATZ_ID = -3;
|
||||
export const COURSE_PROFILE_ALL_ID = -99;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
import { TEST_USER_EMPTY_ID } from "../../consts";
|
||||
import {
|
||||
COURSE_PROFILE_ALL_ID,
|
||||
COURSE_PROFILE_NICHTLEBEN_ID,
|
||||
TEST_COURSE_SESSION_VV_ID,
|
||||
TEST_USER_EMPTY_ID,
|
||||
} from "../../consts";
|
||||
import { login } from "../helpers";
|
||||
|
||||
describe("checkout.cy.js", () => {
|
||||
|
|
@ -32,6 +37,15 @@ describe("checkout.cy.js", () => {
|
|||
cy.get("#organisationDetailName").type("FdH GmbH");
|
||||
cy.get('[data-cy="continue-button"]').click();
|
||||
|
||||
cy.get('[data-cy="account-course-profile-title"]').should(
|
||||
"have.text",
|
||||
"Zulassungsprofil auswählen",
|
||||
);
|
||||
|
||||
cy.get('[data-cy="dropdown-select"]').click();
|
||||
cy.get('[data-cy="dropdown-select-option-Nichtleben"]').click();
|
||||
cy.get('[data-cy="continue-button"]').click();
|
||||
|
||||
cy.loadUser("id", TEST_USER_EMPTY_ID).then((u) => {
|
||||
expect(u.organisation_detail_name).to.equal("FdH GmbH");
|
||||
// 2 -> andere Krankenversicherer
|
||||
|
|
@ -121,6 +135,12 @@ describe("checkout.cy.js", () => {
|
|||
cy.loadCheckoutInformation("user_id", TEST_USER_EMPTY_ID).then((ci) => {
|
||||
expect(ci.state).to.equal("paid");
|
||||
});
|
||||
|
||||
cy.loadCourseSessionUser("user_id", TEST_USER_EMPTY_ID).then((csu) => {
|
||||
expect(csu.role).to.equal("MEMBER");
|
||||
expect(csu.course_session).to.equal(TEST_COURSE_SESSION_VV_ID);
|
||||
expect(csu.chosen_profile).to.equal(COURSE_PROFILE_NICHTLEBEN_ID);
|
||||
});
|
||||
});
|
||||
|
||||
it("can checkout and pay Versicherungsvermittlerin with Cembra invoice", () => {
|
||||
|
|
@ -143,6 +163,15 @@ describe("checkout.cy.js", () => {
|
|||
cy.get('[data-cy="dropdown-select-option-Baloise"]').click();
|
||||
cy.get('[data-cy="continue-button"]').click();
|
||||
|
||||
cy.get('[data-cy="account-course-profile-title"]').should(
|
||||
"have.text",
|
||||
"Zulassungsprofil auswählen",
|
||||
);
|
||||
|
||||
cy.get('[data-cy="dropdown-select"]').click();
|
||||
cy.get('[data-cy="dropdown-select-option-Allbranche"]').click();
|
||||
cy.get('[data-cy="continue-button"]').click();
|
||||
|
||||
// Adressdaten ausfüllen
|
||||
cy.get('[data-cy="account-checkout-title"]').should(
|
||||
"contain",
|
||||
|
|
@ -236,5 +265,32 @@ describe("checkout.cy.js", () => {
|
|||
// 7 -> Baloise
|
||||
expect(u.organisation).to.equal(7);
|
||||
});
|
||||
|
||||
// pay
|
||||
cy.get('[data-cy="pay-button"]').click();
|
||||
|
||||
cy.get('[data-cy="checkout-success-title"]').should(
|
||||
"contain",
|
||||
"Gratuliere",
|
||||
);
|
||||
// wait for payment callback
|
||||
cy.wait(3000);
|
||||
cy.get('[data-cy="start-vv-button"]').click();
|
||||
|
||||
// back on dashboard page
|
||||
cy.get('[data-cy="db-course-title"]').should(
|
||||
"contain",
|
||||
"Versicherungsvermittler",
|
||||
);
|
||||
|
||||
cy.loadCheckoutInformation("user_id", TEST_USER_EMPTY_ID).then((ci) => {
|
||||
expect(ci.state).to.equal("paid");
|
||||
});
|
||||
|
||||
cy.loadCourseSessionUser("user_id", TEST_USER_EMPTY_ID).then((csu) => {
|
||||
expect(csu.role).to.equal("MEMBER");
|
||||
expect(csu.course_session).to.equal(TEST_COURSE_SESSION_VV_ID);
|
||||
expect(csu.chosen_profile).to.equal(COURSE_PROFILE_ALL_ID);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,26 +1,26 @@
|
|||
import { login } from "../helpers";
|
||||
import { login } from "../helpers"
|
||||
|
||||
describe("competenceCertificate.cy.js", () => {
|
||||
beforeEach(() => {});
|
||||
beforeEach(() => { })
|
||||
|
||||
it("check without points", () => {
|
||||
cy.manageCommand("cypress_reset");
|
||||
login("test-student1@example.com", "test");
|
||||
cy.visit("/course/test-lehrgang/competence");
|
||||
cy.manageCommand("cypress_reset")
|
||||
login("test-student1@example.com", "test")
|
||||
cy.visit("/course/test-lehrgang/competence")
|
||||
|
||||
cy.get('[data-cy="certificate-total-points-text"]').contains(
|
||||
"Der Punktestand wird zu einem späteren Zeitpunkt berechnet."
|
||||
);
|
||||
)
|
||||
|
||||
cy.get(
|
||||
'[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
|
||||
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(
|
||||
"Der Punktestand wird zu einem späteren Zeitpunkt berechnet."
|
||||
);
|
||||
)
|
||||
|
||||
cy.get(
|
||||
'[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1"]'
|
||||
|
|
@ -29,100 +29,100 @@ describe("competenceCertificate.cy.js", () => {
|
|||
"contain",
|
||||
"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
|
||||
cy.get(
|
||||
'[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1-detail-link"]'
|
||||
).click();
|
||||
).click()
|
||||
|
||||
cy.get(
|
||||
'[data-cy="assignment-test-lehrgang-assignment-überprüfen-einer-motorfahrzeugs-versicherungspolice"]'
|
||||
).should("contain", "Höchstpunktzahl");
|
||||
).should("contain", "Höchstpunktzahl")
|
||||
cy.get(
|
||||
'[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", () => {
|
||||
cy.manageCommand(
|
||||
"cypress_reset --create-assignment-completion --create-edoniq-test-results 19 24 0"
|
||||
);
|
||||
login("test-student1@example.com", "test");
|
||||
cy.visit("/course/test-lehrgang/competence");
|
||||
)
|
||||
login("test-student1@example.com", "test")
|
||||
cy.visit("/course/test-lehrgang/competence")
|
||||
|
||||
cy.get('[data-cy="certificate-total-points-text"]').contains(
|
||||
"Erfahrungsnote üK: 5"
|
||||
);
|
||||
)
|
||||
|
||||
cy.get(
|
||||
'[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1"]'
|
||||
)
|
||||
.should("contain", "Note: 5")
|
||||
.and("contain", "1 von 2 Kompetenznachweis-Elementen");
|
||||
.and("contain", "1 von 2 Kompetenznachweis-Elementen")
|
||||
|
||||
// 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"]')
|
||||
.should("contain", "Erfahrungsnote üK")
|
||||
.and("contain", "Zwischenstand");
|
||||
cy.get('[data-cy="certificate-total-grade"]').should("contain", "Note: 5");
|
||||
.and("contain", "Zwischenstand")
|
||||
cy.get('[data-cy="certificate-total-grade"]').should("contain", "Note: 5")
|
||||
|
||||
cy.get(
|
||||
'[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1-grade"]'
|
||||
).should("contain", "Note: 5");
|
||||
).should("contain", "Note: 5")
|
||||
cy.get(
|
||||
'[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(
|
||||
'[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1"]'
|
||||
)
|
||||
.and("contain", "Zwischenstand")
|
||||
.and("contain", "1 von 2 Kompetenznachweis-Elementen");
|
||||
.and("contain", "1 von 2 Kompetenznachweis-Elementen")
|
||||
|
||||
// check certificate detail page
|
||||
cy.get(
|
||||
'[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1-detail-link"]'
|
||||
).click();
|
||||
).click()
|
||||
|
||||
cy.get(
|
||||
'[data-cy="assignment-test-lehrgang-assignment-überprüfen-einer-motorfahrzeugs-versicherungspolice"]'
|
||||
)
|
||||
.should("contain", "Höchstpunktzahl")
|
||||
.and("contain", "Ergebnisse abgegeben");
|
||||
.and("contain", "Ergebnisse abgegeben")
|
||||
cy.get(
|
||||
'[data-cy="assignment-test-lehrgang-assignment-edoniq-wissens-und-verständisfragen-circle-fahrzeug-demo"]'
|
||||
)
|
||||
.should("contain", "19")
|
||||
.and("contain", "Bewertung freigegeben")
|
||||
.and("not.contain", "Nicht Bestanden");
|
||||
.and("not.contain", "Nicht Bestanden")
|
||||
|
||||
// it can open learning content page directly
|
||||
cy.get(
|
||||
'[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"]')
|
||||
.should("contain", "19 von 24 Punkten")
|
||||
.and("contain", "79%");
|
||||
});
|
||||
.and("contain", "79%")
|
||||
})
|
||||
|
||||
it("check with finished failed edoniq test", () => {
|
||||
cy.manageCommand(
|
||||
"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
|
||||
cy.visit(
|
||||
"/course/test-lehrgang/competence/certificates/kompetenznachweis-1"
|
||||
);
|
||||
)
|
||||
|
||||
cy.get(
|
||||
'[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1-grade"]'
|
||||
).should("contain", "Note: 3");
|
||||
).should("contain", "Note: 3")
|
||||
cy.get(
|
||||
'[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(
|
||||
'[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")
|
||||
.and("contain", "Bewertung freigegeben")
|
||||
.and("contain", "42%")
|
||||
.and("contain", "Nicht bestanden");
|
||||
.and("contain", "Nicht bestanden")
|
||||
|
||||
// it can open learning content page directly
|
||||
cy.get(
|
||||
'[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"]')
|
||||
.should("contain", "10 von 24 Punkten")
|
||||
.and("contain", "42%")
|
||||
.and("contain", "Nicht bestanden");
|
||||
});
|
||||
.and("contain", "Nicht bestanden")
|
||||
})
|
||||
|
||||
it("check with finished edoniq test and finished casework", () => {
|
||||
cy.manageCommand(
|
||||
"cypress_reset --create-assignment-evaluation --create-edoniq-test-results 19 24 0"
|
||||
);
|
||||
login("test-student1@example.com", "test");
|
||||
cy.visit("/course/test-lehrgang/competence");
|
||||
)
|
||||
login("test-student1@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");
|
||||
.and("contain", "2 von 2 Kompetenznachweis-Elementen")
|
||||
|
||||
// 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"]')
|
||||
.should("contain", "Erfahrungsnote üK")
|
||||
.and("contain", "Note: 5.5")
|
||||
.and("not.contain", "Zwischenstand");
|
||||
.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");
|
||||
.and("contain", "2 von 2 Kompetenznachweis-Elementen")
|
||||
cy.get(
|
||||
'[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1-grade"]'
|
||||
).should("contain", "Note: 5.5");
|
||||
).should("contain", "Note: 5.5")
|
||||
cy.get(
|
||||
'[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
|
||||
cy.get(
|
||||
'[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1-detail-link"]'
|
||||
).click();
|
||||
).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");
|
||||
.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");
|
||||
});
|
||||
.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", () => {
|
||||
cy.manageCommand(
|
||||
"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
|
||||
cy.visit(
|
||||
"/course/test-lehrgang/competence/certificates/kompetenznachweis-1"
|
||||
);
|
||||
)
|
||||
|
||||
cy.get(
|
||||
'[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1-grade"]'
|
||||
).should("contain", "Note: 3.5");
|
||||
).should("contain", "Note: 3.5")
|
||||
cy.get(
|
||||
'[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(
|
||||
'[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", "46%")
|
||||
.and("contain", "mit Abzug")
|
||||
.and("contain", "Nicht bestanden");
|
||||
.and("contain", "Nicht bestanden")
|
||||
|
||||
// it can open learning content page directly
|
||||
cy.get(
|
||||
'[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"]')
|
||||
.should("contain", "11 von 24 Punkten")
|
||||
.and("contain", "46%")
|
||||
.and("contain", "Punkte aus Bewertung: 19")
|
||||
.and("contain", "Abgezogene Punkte: 8")
|
||||
.and("contain", "Grund: Edoniq Punkteabzug Test")
|
||||
.and("contain", "Nicht bestanden");
|
||||
});
|
||||
.and("contain", "Nicht bestanden")
|
||||
})
|
||||
|
||||
it("check with finished casework and points deducted", () => {
|
||||
cy.manageCommand(
|
||||
"cypress_reset --create-assignment-evaluation --assignment-evaluation-scores 4,6,4,3,2 --assignment-points-deducted 5"
|
||||
);
|
||||
login("test-student1@example.com", "test");
|
||||
cy.visit("/course/test-lehrgang/competence");
|
||||
)
|
||||
login("test-student1@example.com", "test")
|
||||
cy.visit("/course/test-lehrgang/competence")
|
||||
|
||||
cy.get('[data-cy="certificate-total-points-text"]').contains(
|
||||
"Erfahrungsnote üK: 4"
|
||||
);
|
||||
)
|
||||
|
||||
cy.get(
|
||||
'[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1"]'
|
||||
)
|
||||
.should("contain", "Note: 4")
|
||||
.and("contain", "1 von 2 Kompetenznachweis-Elementen");
|
||||
.and("contain", "1 von 2 Kompetenznachweis-Elementen")
|
||||
|
||||
// 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"]')
|
||||
.should("contain", "Erfahrungsnote üK")
|
||||
.and("contain", "Note: 4")
|
||||
.and("contain", "Zwischenstand");
|
||||
.and("contain", "Zwischenstand")
|
||||
|
||||
cy.get(
|
||||
'[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1"]'
|
||||
)
|
||||
.and("contain", "Zwischenstand")
|
||||
.and("contain", "1 von 2 Kompetenznachweis-Elementen");
|
||||
.and("contain", "1 von 2 Kompetenznachweis-Elementen")
|
||||
cy.get(
|
||||
'[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1-grade"]'
|
||||
).should("contain", "Note: 4");
|
||||
).should("contain", "Note: 4")
|
||||
cy.get(
|
||||
'[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
|
||||
cy.get(
|
||||
'[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1-detail-link"]'
|
||||
).click();
|
||||
).click()
|
||||
|
||||
cy.get(
|
||||
'[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", "58%")
|
||||
.and("contain", "mit Abzug")
|
||||
.and("contain", "Bewertung freigegeben");
|
||||
.and("contain", "Bewertung freigegeben")
|
||||
|
||||
cy.get(
|
||||
'[data-cy="assignment-test-lehrgang-assignment-überprüfen-einer-motorfahrzeugs-versicherungspolice"] [data-cy="open-learning-content"]'
|
||||
).click();
|
||||
cy.get('[data-cy="user-points"]').should("contain", "14");
|
||||
).click()
|
||||
cy.get('[data-cy="user-points"]').should("contain", "14")
|
||||
cy.get('[data-cy="total-points"]').should(
|
||||
"contain",
|
||||
"von 24 Punkten (58%)"
|
||||
);
|
||||
)
|
||||
cy.get('[data-cy="points-deducted"]')
|
||||
.should("contain", "Punkte aus Bewertung: 19")
|
||||
.and("contain", "Abgezogene Punkte: 5")
|
||||
.and("contain", "Grund: Assignment Punkteabzug Test");
|
||||
});
|
||||
.and("contain", "Grund: Assignment Punkteabzug Test")
|
||||
})
|
||||
|
||||
it("should display link to details", () => {
|
||||
cy.manageCommand("cypress_reset");
|
||||
login("test-student1@example.com", "test");
|
||||
cy.visit("/course/test-lehrgang/competence/self-evaluation-and-feedback");
|
||||
cy.manageCommand("cypress_reset")
|
||||
login("test-student1@example.com", "test")
|
||||
cy.visit("/course/test-lehrgang/competence/self-evaluation-and-feedback")
|
||||
cy.get('[data-cy^="self-eval-"][data-cy$="-detail-url"]:first').contains(
|
||||
"Selbsteinschätzung anschauen"
|
||||
);
|
||||
});
|
||||
});
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -50,20 +50,20 @@
|
|||
// -- This is will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
|
||||
|
||||
const _ = Cypress._;
|
||||
const _ = Cypress._
|
||||
|
||||
Cypress.Commands.add("manageCommand", (command, preCommand = "") => {
|
||||
const execCommand = `${preCommand} python server/manage.py ${command} --settings=config.settings.test_cypress`;
|
||||
console.log(execCommand);
|
||||
const execCommand = `${preCommand} python server/manage.py ${command} --settings=config.settings.test_cypress`
|
||||
console.log(execCommand)
|
||||
// hack to add my asdf python instance to the path
|
||||
// so I can run the test directly from within IntelliJ
|
||||
let pythonPaths = [
|
||||
"/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/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
|
||||
.exec(`bash -c "${bashCommand}"`, {
|
||||
failOnNonZeroExit: true,
|
||||
|
|
@ -73,14 +73,14 @@ Cypress.Commands.add("manageCommand", (command, preCommand = "") => {
|
|||
throw new Error(`Execution of "${command}" failed
|
||||
Exit code: ${result.code}
|
||||
Stdout:\n${result.stdout}
|
||||
Stderr:\n${result.stderr}`);
|
||||
Stderr:\n${result.stderr}`)
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
})
|
||||
|
||||
Cypress.Commands.add("manageShellCommand", (command) => {
|
||||
return cy.manageCommand(`shell -c '${command}'`);
|
||||
});
|
||||
return cy.manageCommand(`shell -c '${command}'`)
|
||||
})
|
||||
|
||||
function loadObjectJson(
|
||||
key,
|
||||
|
|
@ -89,28 +89,28 @@ function loadObjectJson(
|
|||
serializerModelPath,
|
||||
valueAsString = false,
|
||||
) {
|
||||
const djangoModel = _.last(djangoModelPath.split("."));
|
||||
const djangoModelImportPath = _.initial(djangoModelPath.split(".")).join(".");
|
||||
const serializerModel = _.last(serializerModelPath.split("."));
|
||||
const djangoModel = _.last(djangoModelPath.split("."))
|
||||
const djangoModelImportPath = _.initial(djangoModelPath.split(".")).join(".")
|
||||
const serializerModel = _.last(serializerModelPath.split("."))
|
||||
const serializerModelImportPath = _.initial(
|
||||
serializerModelPath.split("."),
|
||||
).join(".");
|
||||
|
||||
let filterPart = `${key}=${value}`;
|
||||
let filterPart = `${key}=${value}`
|
||||
if (valueAsString) {
|
||||
filterPart = `${key}=\\"${value}\\"`;
|
||||
filterPart = `${key}=\\"${value}\\"`
|
||||
}
|
||||
|
||||
if (_.isArray(key)) {
|
||||
filterPart = _.zip(key, value)
|
||||
.map(([k, v]) => {
|
||||
if (valueAsString) {
|
||||
return `${k}=\\"${v}\\"`;
|
||||
return `${k}=\\"${v}\\"`
|
||||
} else {
|
||||
return `${k}=${v}`;
|
||||
return `${k}=${v}`
|
||||
}
|
||||
})
|
||||
.join(",");
|
||||
.join(",")
|
||||
}
|
||||
|
||||
const command = `from ${djangoModelImportPath} import ${djangoModel};
|
||||
|
|
@ -119,13 +119,13 @@ function loadObjectJson(
|
|||
object = ${djangoModel}.objects.filter(${filterPart}).first();
|
||||
print(create_json_from_objects(object, ${serializerModel}, many=False));
|
||||
exit();
|
||||
`.replace(/(?:\r\n|\r|\n)/g, "");
|
||||
`.replace(/(?:\r\n|\r|\n)/g, "")
|
||||
return cy.manageShellCommand(command).then((result) => {
|
||||
const objectJson = JSON.parse(result.stdout);
|
||||
const objectJson = JSON.parse(result.stdout)
|
||||
// console.log(command);
|
||||
console.log(objectJson);
|
||||
return objectJson;
|
||||
});
|
||||
console.log(objectJson)
|
||||
return objectJson
|
||||
})
|
||||
}
|
||||
|
||||
Cypress.Commands.add("loadAssignmentCompletion", (key, value) => {
|
||||
|
|
@ -138,6 +138,7 @@ Cypress.Commands.add("loadAssignmentCompletion", (key, value) => {
|
|||
);
|
||||
});
|
||||
|
||||
|
||||
Cypress.Commands.add("loadSecurityRequestResponseLog", (key, value) => {
|
||||
return loadObjectJson(
|
||||
key,
|
||||
|
|
@ -148,6 +149,7 @@ Cypress.Commands.add("loadSecurityRequestResponseLog", (key, value) => {
|
|||
);
|
||||
});
|
||||
|
||||
|
||||
Cypress.Commands.add("loadExternalApiRequestLog", (key, value) => {
|
||||
return loadObjectJson(
|
||||
key,
|
||||
|
|
@ -158,6 +160,7 @@ Cypress.Commands.add("loadExternalApiRequestLog", (key, value) => {
|
|||
);
|
||||
});
|
||||
|
||||
|
||||
Cypress.Commands.add("loadFeedbackResponse", (key, value) => {
|
||||
return loadObjectJson(
|
||||
key,
|
||||
|
|
@ -168,6 +171,7 @@ Cypress.Commands.add("loadFeedbackResponse", (key, value) => {
|
|||
);
|
||||
});
|
||||
|
||||
|
||||
Cypress.Commands.add("loadCheckoutInformation", (key, value) => {
|
||||
return loadObjectJson(
|
||||
key,
|
||||
|
|
@ -178,6 +182,7 @@ Cypress.Commands.add("loadCheckoutInformation", (key, value) => {
|
|||
);
|
||||
});
|
||||
|
||||
|
||||
Cypress.Commands.add("loadUser", (key, value) => {
|
||||
return loadObjectJson(
|
||||
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) => {
|
||||
for (let i = 0; i < answers.length; i++) {
|
||||
|
|
@ -200,28 +214,28 @@ Cypress.Commands.add("makeSelfEvaluation", (answers, withCompletion = true) => {
|
|||
|
||||
if (withCompletion) {
|
||||
if (i < answers.length - 1) {
|
||||
cy.get('[data-cy="next-step"]').click({ force: true });
|
||||
cy.get('[data-cy="next-step"]').click({force: true});
|
||||
} else {
|
||||
cy.get('[data-cy="complete-and-continue"]').click({ force: true });
|
||||
cy.get('[data-cy="complete-and-continue"]').click({force: true});
|
||||
}
|
||||
} else {
|
||||
cy.get('[data-cy="next-step"]').click({ force: true });
|
||||
cy.get('[data-cy="next-step"]').click({force: true});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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", () => {
|
||||
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) => {
|
||||
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) => {
|
||||
return cy.get('[data-cy="lc-subtitle"]').should("contain", subtitle);
|
||||
});
|
||||
return cy.get('[data-cy="lc-subtitle"]').should("contain", subtitle)
|
||||
})
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -7,7 +7,7 @@ echo 'prettier:check'
|
|||
(cd client && npm run prettier:check)
|
||||
|
||||
echo 'lint and typecheck'
|
||||
(cd client && npm run lint && npm run typecheck)
|
||||
(cd client && npm run lint:errors && npm run typecheck)
|
||||
|
||||
echo 'python ufmt check'
|
||||
ufmt check server
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"cypress:open": "cypress open",
|
||||
"cypress:ci": "cypress-cloud run --parallel --record",
|
||||
"cypress:install": "cypress install",
|
||||
"prettier": "npm run prettier --prefix client"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ from rest_framework.response import Response
|
|||
|
||||
from vbv_lernwelt.core.models import Country, Organisation
|
||||
from vbv_lernwelt.core.serializers import CountrySerializer, OrganisationSerializer
|
||||
from vbv_lernwelt.learnpath.models import CourseProfile
|
||||
from vbv_lernwelt.learnpath.serializers import CourseProfileSerializer
|
||||
|
||||
|
||||
@api_view(["GET"])
|
||||
|
|
@ -26,4 +28,13 @@ def list_entities(request):
|
|||
countries = CountrySerializer(
|
||||
Country.objects.all(), many=True, context=context
|
||||
).data
|
||||
return Response({"organisations": organisations, "countries": countries})
|
||||
course_profiles = CourseProfileSerializer(
|
||||
CourseProfile.objects.all(), many=True, context=context
|
||||
).data
|
||||
return Response(
|
||||
{
|
||||
"organisations": organisations,
|
||||
"countries": countries,
|
||||
"courseProfiles": course_profiles,
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework.decorators import api_view, permission_classes
|
||||
from rest_framework.generics import get_object_or_404
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
|
|
|
|||
|
|
@ -64,12 +64,32 @@ from vbv_lernwelt.shop.models import CheckoutInformation
|
|||
@click.option(
|
||||
"--create-assignment-completion/--no-create-assignment-completion",
|
||||
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(
|
||||
"--create-assignment-evaluation/--no-create-assignment-evaluation",
|
||||
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(
|
||||
"--assignment-evaluation-scores",
|
||||
|
|
@ -86,7 +106,17 @@ from vbv_lernwelt.shop.models import CheckoutInformation
|
|||
type=(int, int, float),
|
||||
default=(None, None, 0.0),
|
||||
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(
|
||||
"--create-feedback-responses/--no-create-feedback-responses",
|
||||
|
|
@ -130,10 +160,16 @@ from vbv_lernwelt.shop.models import CheckoutInformation
|
|||
)
|
||||
def command(
|
||||
create_assignment_completion,
|
||||
assignment_completion_user_id,
|
||||
assignment_completion_course_session_id,
|
||||
create_assignment_evaluation,
|
||||
assignment_evaluation_user_id,
|
||||
assignment_evaluation_course_session_id,
|
||||
assignment_evaluation_scores,
|
||||
assignment_points_deducted,
|
||||
create_edoniq_test_results,
|
||||
edoniq_user_id,
|
||||
edoniq_course_session_id,
|
||||
create_feedback_responses,
|
||||
create_course_completion_performance_criteria,
|
||||
create_attendance_days,
|
||||
|
|
@ -186,15 +222,19 @@ def command(
|
|||
assignment=Assignment.objects.get(
|
||||
slug="test-lehrgang-assignment-überprüfen-einer-motorfahrzeugs-versicherungspolice"
|
||||
),
|
||||
course_session=CourseSession.objects.get(id=TEST_COURSE_SESSION_BERN_ID),
|
||||
user=User.objects.get(id=TEST_STUDENT1_USER_ID),
|
||||
course_session=CourseSession.objects.get(
|
||||
id=assignment_completion_course_session_id
|
||||
),
|
||||
user=User.objects.get(id=assignment_completion_user_id),
|
||||
)
|
||||
create_test_assignment_submitted_data(
|
||||
assignment=Assignment.objects.get(
|
||||
slug="test-lehrgang-assignment-mein-kundenstamm"
|
||||
),
|
||||
course_session=CourseSession.objects.get(id=TEST_COURSE_SESSION_BERN_ID),
|
||||
user=User.objects.get(id=TEST_STUDENT1_USER_ID),
|
||||
course_session=CourseSession.objects.get(
|
||||
id=assignment_completion_course_session_id
|
||||
),
|
||||
user=User.objects.get(id=assignment_completion_user_id),
|
||||
)
|
||||
if create_assignment_evaluation:
|
||||
if not assignment_evaluation_scores:
|
||||
|
|
@ -211,9 +251,11 @@ def command(
|
|||
assignment=Assignment.objects.get(
|
||||
slug="test-lehrgang-assignment-überprüfen-einer-motorfahrzeugs-versicherungspolice"
|
||||
),
|
||||
course_session=CourseSession.objects.get(id=TEST_COURSE_SESSION_BERN_ID),
|
||||
assignment_user=User.objects.get(id=TEST_STUDENT1_USER_ID),
|
||||
evaluation_user=User.objects.get(id=TEST_TRAINER1_USER_ID),
|
||||
course_session=CourseSession.objects.get(
|
||||
id=assignment_evaluation_course_session_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,
|
||||
points_deducted=assignment_points_deducted,
|
||||
)
|
||||
|
|
@ -227,8 +269,8 @@ def command(
|
|||
assignment=Assignment.objects.get(
|
||||
slug="test-lehrgang-assignment-edoniq-wissens-und-verständisfragen-circle-fahrzeug-demo"
|
||||
),
|
||||
course_session=CourseSession.objects.get(id=TEST_COURSE_SESSION_BERN_ID),
|
||||
assignment_user=User.objects.get(id=TEST_STUDENT1_USER_ID),
|
||||
course_session=CourseSession.objects.get(id=edoniq_course_session_id),
|
||||
assignment_user=User.objects.get(id=edoniq_user_id),
|
||||
user_points=user_points,
|
||||
max_points=max_points,
|
||||
evaluation_points_deducted=points_deducted,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ from graphql import GraphQLError
|
|||
from rest_framework.exceptions import PermissionDenied
|
||||
|
||||
from vbv_lernwelt.competence.graphql.types import ActionCompetenceObjectType
|
||||
from vbv_lernwelt.core.models import User
|
||||
from vbv_lernwelt.course.models import (
|
||||
CircleDocument,
|
||||
Course,
|
||||
|
|
@ -29,8 +30,9 @@ from vbv_lernwelt.course_session.models import (
|
|||
)
|
||||
from vbv_lernwelt.course_session_group.models import CourseSessionGroup
|
||||
from vbv_lernwelt.iam.permissions import has_course_access
|
||||
from vbv_lernwelt.learnpath.consts import COURSE_PROFILE_ALL_ID
|
||||
from vbv_lernwelt.learnpath.graphql.types import LearningPathObjectType
|
||||
from vbv_lernwelt.learnpath.models import Circle
|
||||
from vbv_lernwelt.learnpath.models import Circle, CourseProfile
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
|
@ -106,6 +108,12 @@ class CourseObjectType(DjangoObjectType):
|
|||
graphene.NonNull(ActionCompetenceObjectType), required=True
|
||||
)
|
||||
configuration = graphene.Field(CourseConfigurationObjectType, required=True)
|
||||
profiles = graphene.List(graphene.String)
|
||||
course_session_users = graphene.List(
|
||||
"vbv_lernwelt.course.graphql.types.CourseSessionUserType",
|
||||
required=True,
|
||||
id=graphene.String(),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Course
|
||||
|
|
@ -125,6 +133,22 @@ class CourseObjectType(DjangoObjectType):
|
|||
def resolve_action_competences(root: Course, info):
|
||||
return root.get_action_competences()
|
||||
|
||||
@staticmethod
|
||||
def resolve_profiles(root: Course, info, **kwargs):
|
||||
if root.configuration.is_vv:
|
||||
return CourseProfile.objects.values_list("code", flat=True)
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def resolve_course_session_users(root: Course, info, id=None, **kwargs):
|
||||
# todo: restrict users that can be queried
|
||||
if id is not None:
|
||||
user = User.objects.get(id=id)
|
||||
else:
|
||||
user = info.context.user
|
||||
users = CourseSessionUser.objects.filter(user=user, course_session__course=root)
|
||||
return users
|
||||
|
||||
|
||||
class CourseSessionUserExpertCircleType(ObjectType):
|
||||
id = graphene.ID(required=True)
|
||||
|
|
@ -132,6 +156,21 @@ class CourseSessionUserExpertCircleType(ObjectType):
|
|||
slug = graphene.String(required=True)
|
||||
|
||||
|
||||
class CourseSessionUserType(DjangoObjectType):
|
||||
chosen_profile = graphene.String(required=True)
|
||||
course_session = graphene.Field(
|
||||
"vbv_lernwelt.course.graphql.types.CourseSessionObjectType", required=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CourseSessionUser
|
||||
fields = ["chosen_profile", "id"]
|
||||
|
||||
@staticmethod
|
||||
def resolve_chosen_profile(root: CourseSessionUser, info, **kwargs):
|
||||
return getattr(root.chosen_profile, "code", "")
|
||||
|
||||
|
||||
class CourseSessionUserObjectsType(ObjectType):
|
||||
"""
|
||||
WORKAROUND:
|
||||
|
|
|
|||
|
|
@ -97,6 +97,7 @@ from vbv_lernwelt.learnpath.create_vv_new_learning_path import (
|
|||
create_vv_new_learning_path,
|
||||
create_vv_pruefung_learning_path,
|
||||
)
|
||||
from vbv_lernwelt.learnpath.creators import assign_circles_to_profiles
|
||||
from vbv_lernwelt.learnpath.models import (
|
||||
Circle,
|
||||
LearningContent,
|
||||
|
|
@ -218,6 +219,7 @@ def create_versicherungsvermittlerin_course(
|
|||
create_vv_gewinnen_casework(course_id=course_id)
|
||||
create_vv_reflection(course_id=course_id)
|
||||
create_vv_new_learning_path(course_id=course_id)
|
||||
assign_circles_to_profiles()
|
||||
|
||||
cs = CourseSession.objects.create(course_id=course_id, title=names[language])
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
# Generated by Django 3.2.20 on 2024-07-11 09:00
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("learnpath", "0017_auto_20240711_1100"),
|
||||
("course", "0008_auto_20240403_1132"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="coursesessionuser",
|
||||
name="chosen_profile",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to="learnpath.courseprofile",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
# Generated by Django 4.2.13 on 2024-07-22 19:45
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
import vbv_lernwelt.course.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("course", "0009_coursesessionuser_chosen_profile"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="coursecompletion",
|
||||
name="completion_status",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("SUCCESS", "Success"),
|
||||
("FAIL", "Fail"),
|
||||
("UNKNOWN", "Unknown"),
|
||||
],
|
||||
default=vbv_lernwelt.course.models.CourseCompletionStatus["UNKNOWN"],
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
# Generated by Django 4.2.13 on 2024-08-07 11:17
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("course", "0009_coursesessionuser_required_attendance_and_more"),
|
||||
("course", "0010_alter_coursecompletion_completion_status"),
|
||||
]
|
||||
|
||||
operations = []
|
||||
|
|
@ -285,6 +285,10 @@ class CourseSessionUser(models.Model):
|
|||
)
|
||||
optional_attendance = models.BooleanField(default=False)
|
||||
|
||||
chosen_profile = models.ForeignKey(
|
||||
"learnpath.CourseProfile", on_delete=models.SET_NULL, blank=True, null=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
constraints = [
|
||||
UniqueConstraint(
|
||||
|
|
|
|||
|
|
@ -8,8 +8,10 @@ from vbv_lernwelt.course.models import (
|
|||
CourseCompletion,
|
||||
CourseConfiguration,
|
||||
CourseSession,
|
||||
CourseSessionUser,
|
||||
)
|
||||
from vbv_lernwelt.iam.permissions import course_session_permissions
|
||||
from vbv_lernwelt.learnpath.models import CourseProfile
|
||||
|
||||
|
||||
class CourseConfigurationSerializer(serializers.ModelSerializer):
|
||||
|
|
@ -31,10 +33,23 @@ class CourseSerializer(serializers.ModelSerializer):
|
|||
configuration = CourseConfigurationSerializer(
|
||||
read_only=True,
|
||||
)
|
||||
course_profiles = serializers.SerializerMethodField()
|
||||
|
||||
def get_course_profiles(self, obj):
|
||||
if obj.configuration.is_vv:
|
||||
return CourseProfile.objects.all().values_list("code", flat=True)
|
||||
return []
|
||||
|
||||
class Meta:
|
||||
model = Course
|
||||
fields = ["id", "title", "category_name", "slug", "configuration"]
|
||||
fields = [
|
||||
"id",
|
||||
"title",
|
||||
"category_name",
|
||||
"slug",
|
||||
"configuration",
|
||||
"course_profiles",
|
||||
]
|
||||
|
||||
|
||||
class CourseCategorySerializer(serializers.ModelSerializer):
|
||||
|
|
@ -103,6 +118,12 @@ class CourseSessionSerializer(serializers.ModelSerializer):
|
|||
return []
|
||||
|
||||
|
||||
class CypressCourseSessionUserSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = CourseSessionUser
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class CircleDocumentSerializer(serializers.ModelSerializer):
|
||||
learning_sequence = serializers.SerializerMethodField()
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,164 @@
|
|||
from django.test import RequestFactory, TestCase
|
||||
from graphene.test import Client
|
||||
|
||||
from vbv_lernwelt.core.admin import User
|
||||
from vbv_lernwelt.core.create_default_users import create_default_users
|
||||
from vbv_lernwelt.core.schema import schema
|
||||
from vbv_lernwelt.course.management.commands.create_default_courses import (
|
||||
create_versicherungsvermittlerin_course,
|
||||
)
|
||||
from vbv_lernwelt.course.models import CourseSessionUser
|
||||
from vbv_lernwelt.learnpath.creators import create_course_profiles
|
||||
from vbv_lernwelt.learnpath.models import CourseProfile
|
||||
|
||||
|
||||
class CourseGraphQLTestCase(TestCase):
|
||||
def setUp(self) -> None:
|
||||
create_default_users()
|
||||
create_course_profiles()
|
||||
create_versicherungsvermittlerin_course()
|
||||
|
||||
def test_update_course_profile(self):
|
||||
user = User.objects.get(username="student-vv@eiger-versicherungen.ch")
|
||||
request = RequestFactory().get("/")
|
||||
request.user = user
|
||||
client = Client(schema=schema, context_value=request)
|
||||
query = """
|
||||
query CourseQuery($slug: String!) {
|
||||
course(slug: $slug){
|
||||
id
|
||||
profiles
|
||||
course_session_users {
|
||||
id
|
||||
__typename
|
||||
chosen_profile
|
||||
course_session {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
slug = "versicherungsvermittler-in"
|
||||
variables = {"slug": slug}
|
||||
result = client.execute(query, variables=variables)
|
||||
|
||||
self.assertIsNone(result.get("errors"))
|
||||
data = result.get("data")
|
||||
course = data.get("course")
|
||||
profiles = course.get("profiles")
|
||||
self.assertEqual(
|
||||
set(profiles),
|
||||
set(["all", "nichtleben", "leben", "krankenzusatzversicherung"]),
|
||||
)
|
||||
course_session_user = course.get("course_session_users")[0]
|
||||
chosen_profile = course_session_user.get("chosen_profile")
|
||||
|
||||
self.assertEqual(chosen_profile, "")
|
||||
|
||||
mutation = """
|
||||
mutation UpdateCourseSessionProfile($input: CourseSessionProfileMutationInput!) {
|
||||
update_course_session_profile(input: $input) {
|
||||
clientMutationId
|
||||
result {
|
||||
__typename
|
||||
... on UpdateCourseProfileSuccess {
|
||||
user {
|
||||
id
|
||||
chosen_profile
|
||||
}
|
||||
}
|
||||
... on UpdateCourseProfileError {
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
profile = "nichtleben"
|
||||
input = {"course_profile": profile, "course_slug": slug}
|
||||
|
||||
mutation_result = client.execute(mutation, variables={"input": input})
|
||||
|
||||
self.assertIsNone(mutation_result.get("errors"))
|
||||
|
||||
second_query_result = client.execute(query, variables=variables)
|
||||
|
||||
self.assertIsNone(second_query_result.get("errors"))
|
||||
data = second_query_result.get("data")
|
||||
course = data.get("course")
|
||||
profiles = course.get("profiles")
|
||||
self.assertEqual(
|
||||
set(profiles),
|
||||
set(["all", "nichtleben", "leben", "krankenzusatzversicherung"]),
|
||||
)
|
||||
course_session_user = course.get("course_session_users")[0]
|
||||
chosen_profile = course_session_user.get("chosen_profile")
|
||||
self.assertEqual(chosen_profile, profile)
|
||||
|
||||
def test_mentor_profile_view(self):
|
||||
user = User.objects.get(username="test-mentor1@example.com")
|
||||
request = RequestFactory().get("/")
|
||||
request.user = user
|
||||
client = Client(schema=schema, context_value=request)
|
||||
|
||||
query = """
|
||||
query courseQuery($slug: String!, $user: String) {
|
||||
course(slug: $slug) {
|
||||
id
|
||||
title
|
||||
slug
|
||||
category_name
|
||||
profiles
|
||||
course_session_users(id: $user) {
|
||||
id
|
||||
__typename
|
||||
chosen_profile
|
||||
course_session {
|
||||
id
|
||||
__typename
|
||||
}
|
||||
}
|
||||
__typename
|
||||
}
|
||||
}
|
||||
"""
|
||||
student = User.objects.get(username="student-vv@eiger-versicherungen.ch")
|
||||
|
||||
slug = "versicherungsvermittler-in"
|
||||
student_id = str(student.id)
|
||||
variables = {"slug": slug, "user": student_id}
|
||||
print(variables)
|
||||
result = client.execute(query, variables=variables)
|
||||
self.assertIsNone(result.get("errors"))
|
||||
data = result.get("data")
|
||||
course = data.get("course")
|
||||
profiles = course.get("profiles")
|
||||
self.assertEqual(
|
||||
set(profiles),
|
||||
set(["all", "nichtleben", "leben", "krankenzusatzversicherung"]),
|
||||
)
|
||||
course_session_user = course.get("course_session_users")[0]
|
||||
chosen_profile = course_session_user.get("chosen_profile")
|
||||
self.assertEqual(chosen_profile, "")
|
||||
|
||||
csu = CourseSessionUser.objects.get(
|
||||
course_session__course__slug=slug, user=student
|
||||
)
|
||||
course_profile = CourseProfile.objects.get(code="nichtleben")
|
||||
csu.chosen_profile = course_profile
|
||||
csu.save()
|
||||
|
||||
second_result = client.execute(query, variables=variables)
|
||||
self.assertIsNone(second_result.get("errors"))
|
||||
data = second_result.get("data")
|
||||
course = data.get("course")
|
||||
profiles = course.get("profiles")
|
||||
self.assertEqual(
|
||||
set(profiles),
|
||||
set(["all", "nichtleben", "leben", "krankenzusatzversicherung"]),
|
||||
)
|
||||
course_session_user = course.get("course_session_users")[0]
|
||||
chosen_profile = course_session_user.get("chosen_profile")
|
||||
self.assertEqual(chosen_profile, "nichtleben")
|
||||
|
|
@ -1,7 +1,10 @@
|
|||
import graphene
|
||||
import structlog
|
||||
from graphene import relay
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
|
||||
from vbv_lernwelt.course.graphql.types import CourseSessionUserType
|
||||
from vbv_lernwelt.course.models import CourseSessionUser
|
||||
from vbv_lernwelt.course_session.graphql.types import (
|
||||
CourseSessionAttendanceCourseObjectType,
|
||||
)
|
||||
|
|
@ -11,10 +14,33 @@ from vbv_lernwelt.course_session.services.attendance import (
|
|||
update_attendance_list,
|
||||
)
|
||||
from vbv_lernwelt.iam.permissions import has_course_access
|
||||
from vbv_lernwelt.learnpath.models import CourseProfile
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class UpdateCourseProfileSuccess(graphene.ObjectType):
|
||||
user = graphene.Field(CourseSessionUserType(), required=True)
|
||||
|
||||
|
||||
class UpdateCourseProfileError(graphene.ObjectType):
|
||||
message = graphene.String()
|
||||
|
||||
|
||||
class UpdateCourseProfileResult(graphene.Union):
|
||||
class Meta:
|
||||
types = (
|
||||
UpdateCourseProfileError,
|
||||
UpdateCourseProfileSuccess,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def resolve_type(cls, instance, info):
|
||||
if type(instance).__name__ == "UpdateCourseProfileSuccess":
|
||||
return UpdateCourseProfileSuccess
|
||||
return UpdateCourseProfileError
|
||||
|
||||
|
||||
class AttendanceUserInputType(graphene.InputObjectType):
|
||||
user_id = graphene.UUID(required=True)
|
||||
status = graphene.Field(
|
||||
|
|
@ -57,5 +83,40 @@ class AttendanceCourseUserMutation(graphene.Mutation):
|
|||
)
|
||||
|
||||
|
||||
class CourseSessionProfileMutation(relay.ClientIDMutation):
|
||||
class Input:
|
||||
course_profile = graphene.String(required=True)
|
||||
course_slug = graphene.String(required=True)
|
||||
|
||||
result = UpdateCourseProfileResult()
|
||||
|
||||
@classmethod
|
||||
def mutate_and_get_payload(cls, root, info, **input):
|
||||
course_profile = input.get("course_profile")
|
||||
course_slug = input.get("course_slug")
|
||||
user = info.context.user
|
||||
|
||||
try:
|
||||
if course_profile == "":
|
||||
profile = None
|
||||
else:
|
||||
profile = CourseProfile.objects.get(code=course_profile)
|
||||
|
||||
# csu = user.coursesessionuser_set.first()
|
||||
csu = CourseSessionUser.objects.get(
|
||||
course_session__course__slug=course_slug, user=user
|
||||
)
|
||||
csu.chosen_profile = profile
|
||||
csu.save()
|
||||
return cls(result=UpdateCourseProfileSuccess(user=csu))
|
||||
except CourseProfile.DoesNotExist:
|
||||
return cls(result=UpdateCourseProfileError("Course Profile does not exist"))
|
||||
except CourseSessionUser.DoesNotExist:
|
||||
return cls(
|
||||
result=UpdateCourseProfileError("Course Session User does not exist")
|
||||
)
|
||||
|
||||
|
||||
class CourseSessionMutation:
|
||||
update_course_session_attendance_course_users = AttendanceCourseUserMutation.Field()
|
||||
update_course_session_profile = CourseSessionProfileMutation.Field()
|
||||
|
|
|
|||
|
|
@ -1,3 +1,8 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
from vbv_lernwelt.learnpath.models import CourseProfile
|
||||
|
||||
|
||||
@admin.register(CourseProfile)
|
||||
class CourseProfileAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
COURSE_PROFILE_LEBEN_ID = -1
|
||||
COURSE_PROFILE_NICHTLEBEN_ID = -2
|
||||
COURSE_PROFILE_KRANKENZUSATZ_ID = -3
|
||||
COURSE_PROFILE_ALL_ID = -99
|
||||
|
||||
COURSE_PROFILE_LEBEN_CODE = "leben"
|
||||
COURSE_PROFILE_NICHTLEBEN_CODE = "nichtleben"
|
||||
COURSE_PROFILE_KRANKENZUSATZ_CODE = "krankenzusatzversicherung"
|
||||
COURSE_PROFILE_ALL_CODE = "all"
|
||||
|
|
@ -0,0 +1,192 @@
|
|||
import structlog
|
||||
|
||||
from vbv_lernwelt.course.consts import (
|
||||
COURSE_VERSICHERUNGSVERMITTLERIN_FR_ID,
|
||||
COURSE_VERSICHERUNGSVERMITTLERIN_ID,
|
||||
COURSE_VERSICHERUNGSVERMITTLERIN_IT_ID,
|
||||
)
|
||||
from vbv_lernwelt.learnpath.consts import (
|
||||
COURSE_PROFILE_ALL_CODE,
|
||||
COURSE_PROFILE_ALL_ID,
|
||||
COURSE_PROFILE_KRANKENZUSATZ_CODE,
|
||||
COURSE_PROFILE_KRANKENZUSATZ_ID,
|
||||
COURSE_PROFILE_LEBEN_CODE,
|
||||
COURSE_PROFILE_LEBEN_ID,
|
||||
COURSE_PROFILE_NICHTLEBEN_CODE,
|
||||
COURSE_PROFILE_NICHTLEBEN_ID,
|
||||
)
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
def create_course_profiles():
|
||||
from vbv_lernwelt.learnpath.models import CourseProfile
|
||||
|
||||
# Allbranche, Krankenzusatzversicherung, nicht Leben, Leben
|
||||
CourseProfile.objects.get_or_create(
|
||||
id=COURSE_PROFILE_ALL_ID, code=COURSE_PROFILE_ALL_CODE, order=1
|
||||
)
|
||||
CourseProfile.objects.get_or_create(
|
||||
id=COURSE_PROFILE_KRANKENZUSATZ_ID,
|
||||
code=COURSE_PROFILE_KRANKENZUSATZ_CODE,
|
||||
order=2,
|
||||
)
|
||||
CourseProfile.objects.get_or_create(
|
||||
id=COURSE_PROFILE_NICHTLEBEN_ID, code=COURSE_PROFILE_NICHTLEBEN_CODE, order=3
|
||||
)
|
||||
CourseProfile.objects.get_or_create(
|
||||
id=COURSE_PROFILE_LEBEN_ID, code=COURSE_PROFILE_LEBEN_CODE, order=4
|
||||
)
|
||||
|
||||
|
||||
def assign_circle_to_profile_curry(course_page):
|
||||
from vbv_lernwelt.learnpath.models import Circle, CourseProfile
|
||||
|
||||
def assign_circle_to_profile(title, code):
|
||||
try:
|
||||
circle = Circle.objects.descendant_of(course_page).get(title=title)
|
||||
course_profile = CourseProfile.objects.get(code=code)
|
||||
circle.profiles.add(course_profile)
|
||||
circle.save()
|
||||
except Circle.DoesNotExist:
|
||||
logger.warning("assign_circle_to_profile: circle not found", title=title)
|
||||
|
||||
return assign_circle_to_profile
|
||||
|
||||
|
||||
def make_base_circle_curry(course_page):
|
||||
from vbv_lernwelt.learnpath.models import Circle
|
||||
|
||||
def make_base_circle(title):
|
||||
try:
|
||||
circle = Circle.objects.descendant_of(course_page).get(title=title)
|
||||
circle.is_base_circle = True
|
||||
circle.save()
|
||||
except Circle.DoesNotExist:
|
||||
logger.warning("assign_circle_to_profile: circle not found", title=title)
|
||||
|
||||
return make_base_circle
|
||||
|
||||
|
||||
def assign_de_circles_to_profiles():
|
||||
from vbv_lernwelt.course.models import CoursePage
|
||||
|
||||
try:
|
||||
course_page = CoursePage.objects.get(
|
||||
course_id=COURSE_VERSICHERUNGSVERMITTLERIN_ID
|
||||
)
|
||||
except CoursePage.DoesNotExist:
|
||||
logger.warning("Course does not exist yet")
|
||||
return
|
||||
|
||||
assign_circle_to_profile = assign_circle_to_profile_curry(course_page)
|
||||
make_base_circle = make_base_circle_curry(course_page)
|
||||
|
||||
assign_circle_to_profile("Fahrzeug", COURSE_PROFILE_NICHTLEBEN_CODE)
|
||||
assign_circle_to_profile("Haushalt", COURSE_PROFILE_NICHTLEBEN_CODE)
|
||||
assign_circle_to_profile("Rechtsstreitigkeiten", COURSE_PROFILE_NICHTLEBEN_CODE)
|
||||
assign_circle_to_profile("Reisen", COURSE_PROFILE_NICHTLEBEN_CODE)
|
||||
assign_circle_to_profile("Wohneigentum", COURSE_PROFILE_NICHTLEBEN_CODE)
|
||||
assign_circle_to_profile(
|
||||
"Selbstständigkeit", COURSE_PROFILE_NICHTLEBEN_CODE
|
||||
) # typo, but that's how it is in prod data
|
||||
assign_circle_to_profile("KMU", COURSE_PROFILE_NICHTLEBEN_CODE)
|
||||
|
||||
assign_circle_to_profile("Einkommenssicherung", COURSE_PROFILE_LEBEN_CODE)
|
||||
assign_circle_to_profile("Pensionierung", COURSE_PROFILE_LEBEN_CODE)
|
||||
assign_circle_to_profile("Erben/Vererben", COURSE_PROFILE_LEBEN_CODE)
|
||||
assign_circle_to_profile("Sparen", COURSE_PROFILE_LEBEN_CODE)
|
||||
assign_circle_to_profile(
|
||||
"Selbstständigkeit", COURSE_PROFILE_LEBEN_CODE
|
||||
) # typo, but that's how it is in prod data
|
||||
assign_circle_to_profile("KMU", COURSE_PROFILE_LEBEN_CODE)
|
||||
|
||||
assign_circle_to_profile("Gesundheit", COURSE_PROFILE_KRANKENZUSATZ_CODE)
|
||||
|
||||
make_base_circle("Kickoff")
|
||||
make_base_circle("Basis")
|
||||
make_base_circle("Gewinnen")
|
||||
make_base_circle("Prüfungsvorbereitung")
|
||||
make_base_circle("Prüfung")
|
||||
|
||||
|
||||
def assign_fr_circles_to_profiles():
|
||||
from vbv_lernwelt.course.models import CoursePage
|
||||
|
||||
try:
|
||||
course_page = CoursePage.objects.get(
|
||||
course_id=COURSE_VERSICHERUNGSVERMITTLERIN_FR_ID
|
||||
)
|
||||
except CoursePage.DoesNotExist:
|
||||
logger.warning("Course does not exist yet")
|
||||
return
|
||||
|
||||
assign_circle_to_profile = assign_circle_to_profile_curry(course_page)
|
||||
make_base_circle = make_base_circle_curry(course_page)
|
||||
|
||||
assign_circle_to_profile("Véhicule", COURSE_PROFILE_NICHTLEBEN_CODE)
|
||||
assign_circle_to_profile("Ménage", COURSE_PROFILE_NICHTLEBEN_CODE)
|
||||
assign_circle_to_profile("Litiges juridiques", COURSE_PROFILE_NICHTLEBEN_CODE)
|
||||
assign_circle_to_profile("Voyages", COURSE_PROFILE_NICHTLEBEN_CODE)
|
||||
assign_circle_to_profile("Propriété du logement", COURSE_PROFILE_NICHTLEBEN_CODE)
|
||||
assign_circle_to_profile("Activité indépendante", COURSE_PROFILE_NICHTLEBEN_CODE)
|
||||
assign_circle_to_profile("PME", COURSE_PROFILE_NICHTLEBEN_CODE)
|
||||
|
||||
assign_circle_to_profile("Garantie des revenus", COURSE_PROFILE_LEBEN_CODE)
|
||||
assign_circle_to_profile("Retraite", COURSE_PROFILE_LEBEN_CODE)
|
||||
assign_circle_to_profile("Hériter\xa0/\xa0léguer", COURSE_PROFILE_LEBEN_CODE)
|
||||
assign_circle_to_profile("Épargne", COURSE_PROFILE_LEBEN_CODE)
|
||||
assign_circle_to_profile("Activité indépendante", COURSE_PROFILE_LEBEN_CODE)
|
||||
assign_circle_to_profile("PME", COURSE_PROFILE_LEBEN_CODE)
|
||||
|
||||
assign_circle_to_profile("Santé", COURSE_PROFILE_KRANKENZUSATZ_CODE)
|
||||
|
||||
make_base_circle("Lancement")
|
||||
make_base_circle("Base")
|
||||
make_base_circle("Acquisition")
|
||||
make_base_circle("Préparation à l’examen")
|
||||
make_base_circle("L’examen")
|
||||
|
||||
|
||||
def assign_it_circles_to_profiles():
|
||||
from vbv_lernwelt.course.models import CoursePage
|
||||
|
||||
try:
|
||||
course_page = CoursePage.objects.get(
|
||||
course_id=COURSE_VERSICHERUNGSVERMITTLERIN_IT_ID
|
||||
)
|
||||
except CoursePage.DoesNotExist:
|
||||
logger.warning("Course does not exist yet")
|
||||
return
|
||||
|
||||
assign_circle_to_profile = assign_circle_to_profile_curry(course_page)
|
||||
make_base_circle = make_base_circle_curry(course_page)
|
||||
|
||||
assign_circle_to_profile("Veicolo", COURSE_PROFILE_NICHTLEBEN_CODE)
|
||||
assign_circle_to_profile("Economia domestica", COURSE_PROFILE_NICHTLEBEN_CODE)
|
||||
assign_circle_to_profile("Controversie giuridiche", COURSE_PROFILE_NICHTLEBEN_CODE)
|
||||
assign_circle_to_profile("Viaggi", COURSE_PROFILE_NICHTLEBEN_CODE)
|
||||
assign_circle_to_profile("Casa di proprietà", COURSE_PROFILE_NICHTLEBEN_CODE)
|
||||
assign_circle_to_profile("Attività indipendente", COURSE_PROFILE_NICHTLEBEN_CODE)
|
||||
assign_circle_to_profile("PMI", COURSE_PROFILE_NICHTLEBEN_CODE)
|
||||
|
||||
assign_circle_to_profile("Protezione del reddito", COURSE_PROFILE_LEBEN_CODE)
|
||||
assign_circle_to_profile("Pensionamento", COURSE_PROFILE_LEBEN_CODE)
|
||||
assign_circle_to_profile("Ereditare/lasciare in eredità", COURSE_PROFILE_LEBEN_CODE)
|
||||
assign_circle_to_profile("Risparmio", COURSE_PROFILE_LEBEN_CODE)
|
||||
assign_circle_to_profile("Attività indipendente", COURSE_PROFILE_LEBEN_CODE)
|
||||
assign_circle_to_profile("PMI", COURSE_PROFILE_LEBEN_CODE)
|
||||
|
||||
assign_circle_to_profile("Salute", COURSE_PROFILE_KRANKENZUSATZ_CODE)
|
||||
|
||||
make_base_circle("Kickoff")
|
||||
make_base_circle("Base")
|
||||
make_base_circle("Acquisizione")
|
||||
make_base_circle("Preparazione all'esame")
|
||||
make_base_circle("Esame")
|
||||
|
||||
|
||||
def assign_circles_to_profiles():
|
||||
assign_de_circles_to_profiles()
|
||||
assign_fr_circles_to_profiles()
|
||||
assign_it_circles_to_profiles()
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
import random
|
||||
|
||||
import graphene
|
||||
import structlog
|
||||
from graphene_django import DjangoObjectType
|
||||
|
|
@ -6,6 +8,7 @@ from vbv_lernwelt.core.utils import find_first_index
|
|||
from vbv_lernwelt.course.graphql.interfaces import CoursePageInterface
|
||||
from vbv_lernwelt.learnpath.models import (
|
||||
Circle,
|
||||
CourseProfile,
|
||||
LearningContentAssignment,
|
||||
LearningContentAttendanceCourse,
|
||||
LearningContentDocumentList,
|
||||
|
|
@ -299,14 +302,12 @@ class CircleObjectType(DjangoObjectType):
|
|||
learning_sequences = graphene.List(
|
||||
graphene.NonNull(LearningSequenceObjectType), required=True
|
||||
)
|
||||
profiles = graphene.List(graphene.String, required=True)
|
||||
|
||||
class Meta:
|
||||
model = Circle
|
||||
interfaces = (CoursePageInterface,)
|
||||
fields = [
|
||||
"description",
|
||||
"goals",
|
||||
]
|
||||
fields = ["description", "goals", "is_base_circle"]
|
||||
|
||||
def resolve_learning_sequences(self: Circle, info, **kwargs):
|
||||
circle_descendants = None
|
||||
|
|
@ -335,6 +336,10 @@ class CircleObjectType(DjangoObjectType):
|
|||
if descendant.specific_class == LearningSequence
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def resolve_profiles(root: Circle, info, **kwargs):
|
||||
return root.profiles.all()
|
||||
|
||||
|
||||
class TopicObjectType(DjangoObjectType):
|
||||
circles = graphene.List(graphene.NonNull(CircleObjectType), required=True)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
# Generated by Django 3.2.20 on 2024-07-11 09:00
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("learnpath", "0016_remove_learningunit_feedback_user"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="CourseProfile",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("code", models.CharField(max_length=255)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="CourseProfileToCircle",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="circle",
|
||||
name="profiles",
|
||||
field=models.ManyToManyField(
|
||||
related_name="circles", to="learnpath.CourseProfile"
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
# Generated by Django 4.2.13 on 2024-07-30 07:03
|
||||
|
||||
import modelcluster.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("learnpath", "0017_auto_20240711_1100"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="courseprofile",
|
||||
options={"ordering": ["order"]},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="circle",
|
||||
name="is_base_circle",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="courseprofile",
|
||||
name="order",
|
||||
field=models.IntegerField(default=999),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="circle",
|
||||
name="profiles",
|
||||
field=modelcluster.fields.ParentalManyToManyField(
|
||||
related_name="circles", to="learnpath.courseprofile"
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
# Generated by Django 4.2.13 on 2024-07-30 07:04
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
from vbv_lernwelt.learnpath.creators import create_course_profiles
|
||||
|
||||
|
||||
def migrate(apps, schema_editor):
|
||||
create_course_profiles()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
(
|
||||
"learnpath",
|
||||
"0018_alter_courseprofile_options_circle_is_base_circle_and_more",
|
||||
),
|
||||
]
|
||||
|
||||
operations = [migrations.RunPython(migrate)]
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
# Generated by Django 4.2.13 on 2024-07-30 07:05
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
from vbv_lernwelt.learnpath.creators import assign_circles_to_profiles
|
||||
|
||||
|
||||
def migrate(apps, schema_editor):
|
||||
assign_circles_to_profiles()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("learnpath", "0019_auto_20240730_0904"),
|
||||
]
|
||||
|
||||
operations = [migrations.RunPython(migrate)]
|
||||
|
|
@ -3,6 +3,7 @@ from typing import Tuple
|
|||
|
||||
from django.db import models
|
||||
from django.utils.text import slugify
|
||||
from modelcluster.models import ParentalManyToManyField
|
||||
from wagtail.admin.panels import FieldPanel, PageChooserPanel
|
||||
from wagtail.fields import RichTextField, StreamField
|
||||
from wagtail.models import Page
|
||||
|
|
@ -66,6 +67,25 @@ class Topic(CourseBasePage):
|
|||
return f"{self.title}"
|
||||
|
||||
|
||||
class CourseProfile(models.Model):
|
||||
code = models.CharField(max_length=255)
|
||||
order = models.IntegerField(default=999)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.code
|
||||
|
||||
class Meta:
|
||||
ordering = [
|
||||
"order",
|
||||
]
|
||||
|
||||
|
||||
class CourseProfileToCircle(models.Model):
|
||||
# this connects the course profile to a circle, because a circle can be in multiple profiles
|
||||
# todo: to we even need a through model?
|
||||
pass
|
||||
|
||||
|
||||
class Circle(CourseBasePage):
|
||||
parent_page_types = ["learnpath.LearningPath"]
|
||||
subpage_types = [
|
||||
|
|
@ -95,9 +115,25 @@ class Circle(CourseBasePage):
|
|||
|
||||
goals = RichTextField(features=DEFAULT_RICH_TEXT_FEATURES_WITH_HEADER)
|
||||
|
||||
profiles = ParentalManyToManyField(CourseProfile, related_name="circles")
|
||||
|
||||
# base circles do never belong to a course profile and should also get displayed no matter what profile is chosen
|
||||
is_base_circle = models.BooleanField(default=False)
|
||||
|
||||
# profile = models.ForeignKey(
|
||||
# ApprovalProfile,
|
||||
# null=True,
|
||||
# blank=True,
|
||||
# on_delete=models.SET_NULL,
|
||||
# related_name="circles",
|
||||
# help_text="Zulassungsprofil",
|
||||
# )
|
||||
|
||||
content_panels = Page.content_panels + [
|
||||
FieldPanel("description"),
|
||||
FieldPanel("goals"),
|
||||
FieldPanel("is_base_circle"),
|
||||
FieldPanel("profiles"),
|
||||
]
|
||||
|
||||
def get_frontend_url(self):
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from rest_framework import serializers
|
||||
from rest_framework.fields import SerializerMethodField
|
||||
|
||||
from vbv_lernwelt.competence.serializers import (
|
||||
|
|
@ -6,6 +7,7 @@ from vbv_lernwelt.competence.serializers import (
|
|||
from vbv_lernwelt.core.utils import get_django_content_type
|
||||
from vbv_lernwelt.course.serializer_helpers import get_course_serializer_class
|
||||
from vbv_lernwelt.learnpath.models import (
|
||||
CourseProfile,
|
||||
LearningContentAssignment,
|
||||
LearningContentEdoniqTest,
|
||||
LearningUnit,
|
||||
|
|
@ -98,3 +100,9 @@ class LearningContentAssignmentSerializer(
|
|||
}
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
class CourseProfileSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = CourseProfile
|
||||
fields = ["id", "code"]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
# Generated by Django 4.2.13 on 2024-07-22 19:45
|
||||
|
||||
import wagtail.images.models
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("media_files", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="contentimagerendition",
|
||||
name="file",
|
||||
field=wagtail.images.models.WagtailImageField(
|
||||
height_field="height",
|
||||
storage=wagtail.images.models.get_rendition_storage,
|
||||
upload_to=wagtail.images.models.get_rendition_upload_to,
|
||||
width_field="width",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="userimagerendition",
|
||||
name="file",
|
||||
field=wagtail.images.models.WagtailImageField(
|
||||
height_field="height",
|
||||
storage=wagtail.images.models.get_rendition_storage,
|
||||
upload_to=wagtail.images.models.get_rendition_upload_to,
|
||||
width_field="width",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
# Generated by Django 4.2.13 on 2024-07-30 07:03
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
(
|
||||
"learnpath",
|
||||
"0018_alter_courseprofile_options_circle_is_base_circle_and_more",
|
||||
),
|
||||
("shop", "0016_alter_checkoutinformation_refno2"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="checkoutinformation",
|
||||
name="chosen_profile",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to="learnpath.courseprofile",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
@ -106,7 +106,9 @@ class CheckoutInformation(models.Model):
|
|||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
chosen_profile = models.ForeignKey(
|
||||
"learnpath.CourseProfile", on_delete=models.SET_NULL, null=True, blank=True
|
||||
)
|
||||
# webhook metadata
|
||||
webhook_history = models.JSONField(default=list)
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ from rest_framework.test import APITestCase
|
|||
|
||||
from vbv_lernwelt.core.admin import User
|
||||
from vbv_lernwelt.core.model_utils import add_countries
|
||||
from vbv_lernwelt.learnpath.consts import COURSE_PROFILE_ALL_CODE, COURSE_PROFILE_ALL_ID
|
||||
from vbv_lernwelt.learnpath.models import CourseProfile
|
||||
from vbv_lernwelt.shop.const import VV_DE_PRODUCT_SKU
|
||||
from vbv_lernwelt.shop.models import CheckoutInformation, CheckoutState, Product
|
||||
from vbv_lernwelt.shop.services import InitTransactionException
|
||||
|
|
@ -50,6 +52,10 @@ class CheckoutAPITestCase(APITestCase):
|
|||
is_active=True,
|
||||
)
|
||||
|
||||
CourseProfile.objects.get_or_create(
|
||||
id=COURSE_PROFILE_ALL_ID, code=COURSE_PROFILE_ALL_CODE
|
||||
)
|
||||
|
||||
self.client.login(username=USER_USERNAME, password=USER_PASSWORD)
|
||||
add_countries(small_set=True)
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ from rest_framework.permissions import IsAuthenticated
|
|||
from sentry_sdk import capture_exception
|
||||
|
||||
from vbv_lernwelt.course.models import CourseSession, CourseSessionUser
|
||||
from vbv_lernwelt.learnpath.consts import COURSE_PROFILE_ALL_ID
|
||||
from vbv_lernwelt.learnpath.models import CourseProfile
|
||||
from vbv_lernwelt.notify.email.email_services import EmailTemplate, send_email
|
||||
from vbv_lernwelt.shop.const import (
|
||||
VV_DE_PRODUCT_SKU,
|
||||
|
|
@ -92,6 +94,7 @@ def checkout_vv(request):
|
|||
|
||||
sku = request.data["product"]
|
||||
base_redirect_url = request.data["redirect_url"]
|
||||
chosen_profile_id = request.data.get("chosen_profile", COURSE_PROFILE_ALL_ID)
|
||||
|
||||
log.info("Checkout requested: sku", user_id=request.user.id, sku=sku)
|
||||
|
||||
|
|
@ -106,6 +109,11 @@ def checkout_vv(request):
|
|||
),
|
||||
)
|
||||
|
||||
try:
|
||||
chosen_profile = CourseProfile.objects.get(id=chosen_profile_id)
|
||||
except CourseProfile.DoesNotExist:
|
||||
chosen_profile = CourseProfile.objects.get(id=COURSE_PROFILE_ALL_ID)
|
||||
|
||||
checkouts = CheckoutInformation.objects.filter(
|
||||
user=request.user,
|
||||
product_sku=sku,
|
||||
|
|
@ -151,6 +159,7 @@ def checkout_vv(request):
|
|||
"device_fingerprint_session_key", ""
|
||||
),
|
||||
# address
|
||||
chosen_profile=chosen_profile,
|
||||
**request.data["address"],
|
||||
)
|
||||
|
||||
|
|
@ -257,9 +266,11 @@ def create_vv_course_session_user(checkout_info: CheckoutInformation):
|
|||
_, created = CourseSessionUser.objects.get_or_create(
|
||||
user=checkout_info.user,
|
||||
role=CourseSessionUser.Role.MEMBER,
|
||||
chosen_profile=checkout_info.chosen_profile,
|
||||
course_session=CourseSession.objects.get(
|
||||
id=PRODUCT_SKU_TO_COURSE_SESSION_ID[checkout_info.product_sku]
|
||||
),
|
||||
# chosen_profile=bla,
|
||||
)
|
||||
|
||||
if created:
|
||||
|
|
|
|||
Loading…
Reference in New Issue