Compare commits
14 Commits
b785e327c6
...
34efe68842
| Author | SHA1 | Date |
|---|---|---|
|
|
34efe68842 | |
|
|
cc2ae195c0 | |
|
|
2c4512ba91 | |
|
|
f9d952a5e8 | |
|
|
9692c71fbc | |
|
|
1a909431cf | |
|
|
cf13030f24 | |
|
|
bc5055324c | |
|
|
a620803413 | |
|
|
aa7137b764 | |
|
|
e87ab1da57 | |
|
|
68938ba44b | |
|
|
6ca8469455 | |
|
|
5e84490703 |
|
|
@ -11,7 +11,7 @@ const { t } = useTranslation();
|
||||||
<template v-if="isInCourse">
|
<template v-if="isInCourse">
|
||||||
<div class="flex h-full items-center border-r border-slate-500">
|
<div class="flex h-full items-center border-r border-slate-500">
|
||||||
<router-link to="/" class="flex items-center pr-3">
|
<router-link to="/" class="flex items-center pr-3">
|
||||||
<it-icon-arrow-left />
|
<it-icon-arrow-left class="fill-current text-slate-500" />
|
||||||
<span class="hidden text-slate-500 lg:inline">
|
<span class="hidden text-slate-500 lg:inline">
|
||||||
{{ t("a.Dashboard") }}
|
{{ t("a.Dashboard") }}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ const dropdownSelected = computed<DropdownSelectable>({
|
||||||
'font-bold': !borderless,
|
'font-bold': !borderless,
|
||||||
},
|
},
|
||||||
asHeading
|
asHeading
|
||||||
? 'group flex w-full items-center gap-1 rounded-md bg-transparent px-3 text-base focus:outline-none'
|
? 'group flex w-full items-center gap-1 rounded-md bg-transparent text-base focus:outline-none'
|
||||||
: 'relative flex w-full cursor-default flex-row items-center bg-white py-3 pl-5 pr-10 text-left',
|
: 'relative flex w-full cursor-default flex-row items-center bg-white py-3 pl-5 pr-10 text-left',
|
||||||
]"
|
]"
|
||||||
data-cy="dropdown-select"
|
data-cy="dropdown-select"
|
||||||
|
|
@ -91,7 +91,7 @@ const dropdownSelected = computed<DropdownSelectable>({
|
||||||
active ? 'bg-blue-900 text-white' : 'text-black',
|
active ? 'bg-blue-900 text-white' : 'text-black',
|
||||||
'relative cursor-default select-none py-2 pl-3 pr-9',
|
'relative cursor-default select-none py-2 pl-3 pr-9',
|
||||||
]"
|
]"
|
||||||
class="flex flex-row items-center"
|
class="group flex flex-row items-center"
|
||||||
:data-cy="`dropdown-select-option-${item.name}`"
|
:data-cy="`dropdown-select-option-${item.name}`"
|
||||||
>
|
>
|
||||||
<span v-if="item.iconName" class="mr-4">
|
<span v-if="item.iconName" class="mr-4">
|
||||||
|
|
@ -110,7 +110,11 @@ const dropdownSelected = computed<DropdownSelectable>({
|
||||||
v-if="dropdownSelected"
|
v-if="dropdownSelected"
|
||||||
class="absolute inset-y-0 right-0 flex items-center pr-4 text-blue-900"
|
class="absolute inset-y-0 right-0 flex items-center pr-4 text-blue-900"
|
||||||
>
|
>
|
||||||
<it-icon-check v-if="selected" class="h-5 w-5" aria-hidden="true" />
|
<it-icon-check
|
||||||
|
v-if="selected"
|
||||||
|
class="h-5 w-5 fill-current group-hover:text-white"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
</ListboxOption>
|
</ListboxOption>
|
||||||
|
|
|
||||||
|
|
@ -722,13 +722,25 @@ export function useVVByLink() {
|
||||||
return { href };
|
return { href };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAllCompetenceCertificates(userId: string, courseSlug: string) {
|
export function useAllCompetenceCertificates(
|
||||||
|
userId: string,
|
||||||
|
courseSlug: string,
|
||||||
|
currentCourseSessionOnly: boolean = false
|
||||||
|
) {
|
||||||
const courseSessionsStore = useCourseSessionsStore();
|
const courseSessionsStore = useCourseSessionsStore();
|
||||||
const certificateQueries = courseSessionsStore.allCourseSessions.map(
|
let certificateQueries;
|
||||||
(courseSession) => {
|
if (currentCourseSessionOnly) {
|
||||||
|
const courseSession = useCurrentCourseSession();
|
||||||
|
certificateQueries = [
|
||||||
|
useCertificateQuery([userId], courseSlug, courseSession.value).certificatesQuery,
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
// wtf
|
||||||
|
certificateQueries = courseSessionsStore.allCourseSessions.map((courseSession) => {
|
||||||
|
// todo: use a single query, instead of one for every courseSession
|
||||||
return useCertificateQuery([userId], courseSlug, courseSession).certificatesQuery;
|
return useCertificateQuery([userId], courseSlug, courseSession).certificatesQuery;
|
||||||
}
|
});
|
||||||
);
|
}
|
||||||
|
|
||||||
const competenceCertificatesPerCs = computed(() =>
|
const competenceCertificatesPerCs = computed(() =>
|
||||||
certificateQueries.map((query) => {
|
certificateQueries.map((query) => {
|
||||||
|
|
|
||||||
|
|
@ -172,7 +172,7 @@ const formattedCourseDueDate = computed(() => {
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<div
|
<div
|
||||||
class="col-span-2 flex flex-col gap-4 px-6 lg:gap-6"
|
class="col-span-2 flex flex-col items-start gap-4 px-6 lg:gap-6"
|
||||||
:class="attendanceSaved ? 'lg:flex-row lg:items-center' : 'gap-8 lg:gap-8'"
|
:class="attendanceSaved ? 'lg:flex-row lg:items-center' : 'gap-8 lg:gap-8'"
|
||||||
>
|
>
|
||||||
<AttendanceStatus
|
<AttendanceStatus
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import type { CourseSessionAttendanceCourseObjectType } from "@/gql/graphql";
|
||||||
import { ATTENDANCE_CHECK_QUERY } from "@/graphql/queries";
|
import { ATTENDANCE_CHECK_QUERY } from "@/graphql/queries";
|
||||||
import { ATTENDANCE_ROUTE } from "@/router/names";
|
import { ATTENDANCE_ROUTE } from "@/router/names";
|
||||||
import { useExpertCockpitStore } from "@/stores/expertCockpit";
|
import { useExpertCockpitStore } from "@/stores/expertCockpit";
|
||||||
|
import { getStatus } from "@/utils/attendance";
|
||||||
import { useQuery } from "@urql/vue";
|
import { useQuery } from "@urql/vue";
|
||||||
import { useDateFormat } from "@vueuse/core";
|
import { useDateFormat } from "@vueuse/core";
|
||||||
import { useTranslation } from "i18next-vue";
|
import { useTranslation } from "i18next-vue";
|
||||||
|
|
@ -51,6 +52,9 @@ const formattedCourseDueDate = computed(() => {
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
});
|
});
|
||||||
|
const status = computed(() => {
|
||||||
|
return getStatus(attendanceSaved.value, courseDueDate.value);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -62,8 +66,16 @@ const formattedCourseDueDate = computed(() => {
|
||||||
<p class="text-sm text-gray-800">{{ formattedCourseDueDate }}</p>
|
<p class="text-sm text-gray-800">{{ formattedCourseDueDate }}</p>
|
||||||
</div>
|
</div>
|
||||||
<AttendanceStatus :date="courseDueDate" :done="attendanceSaved" />
|
<AttendanceStatus :date="courseDueDate" :done="attendanceSaved" />
|
||||||
<router-link :to="attendanceRoute" class="underline">
|
<router-link
|
||||||
{{ $t("a.Anwesenheit anschauen") }}
|
:to="attendanceRoute"
|
||||||
|
:class="
|
||||||
|
status === 'now' ? 'bg-blue-900 px-4 py-2 font-bold text-white' : 'underline'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<template v-if="status === 'now'">
|
||||||
|
{{ $t("Anwesenheit prüfen") }}
|
||||||
|
</template>
|
||||||
|
<template v-else>{{ $t("a.Anwesenheit anschauen") }}</template>
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { howManyDaysInFuture, isInFuture } from "@/components/dueDates/dueDatesUtils";
|
import { howManyDaysInFuture } from "@/components/dueDates/dueDatesUtils";
|
||||||
|
import { getStatus } from "@/utils/attendance";
|
||||||
import { useTranslation } from "i18next-vue";
|
import { useTranslation } from "i18next-vue";
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
|
|
||||||
export type Status = "done" | "soon" | "now"; // todo: define this
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
done: boolean;
|
done: boolean;
|
||||||
date: string;
|
date: string;
|
||||||
|
|
@ -14,14 +13,8 @@ const { t } = useTranslation();
|
||||||
|
|
||||||
const props = defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
const status = computed<Status>(() => {
|
const status = computed(() => {
|
||||||
if (props.done) {
|
return getStatus(props.done, props.date);
|
||||||
return "done";
|
|
||||||
}
|
|
||||||
if (isInFuture(props.date)) {
|
|
||||||
return "soon";
|
|
||||||
}
|
|
||||||
return "now";
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const style = computed(() => {
|
const style = computed(() => {
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ const props = defineProps<{
|
||||||
:as-heading="true"
|
:as-heading="true"
|
||||||
:model-value="expertCockpitStore.currentCircle"
|
:model-value="expertCockpitStore.currentCircle"
|
||||||
type-name="Circle:"
|
type-name="Circle:"
|
||||||
class="mt-4 w-full lg:mt-0 lg:w-96"
|
class="mt-4 w-full lg:mt-0 lg:w-auto"
|
||||||
:items="expertCockpitStore.circles"
|
:items="expertCockpitStore.circles"
|
||||||
@update:model-value="expertCockpitStore.setCurrentCourseCircleFromEvent"
|
@update:model-value="expertCockpitStore.setCurrentCourseCircleFromEvent"
|
||||||
></ItDropdownSelect>
|
></ItDropdownSelect>
|
||||||
|
|
|
||||||
|
|
@ -140,7 +140,7 @@ async function uploadDocument(data: DocumentUploadData) {
|
||||||
<div class="mb-9 flex flex-col lg:flex-row lg:items-center lg:justify-between">
|
<div class="mb-9 flex flex-col lg:flex-row lg:items-center lg:justify-between">
|
||||||
<ItDropdownSelect
|
<ItDropdownSelect
|
||||||
:model-value="cockpitStore.currentCircle"
|
:model-value="cockpitStore.currentCircle"
|
||||||
class="mt-4 w-full lg:mt-0 lg:w-96"
|
class="mt-4 w-full lg:mt-0 lg:w-auto"
|
||||||
:as-heading="true"
|
:as-heading="true"
|
||||||
type-name="Circle:"
|
type-name="Circle:"
|
||||||
:items="cockpitStore.circles"
|
:items="cockpitStore.circles"
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,9 @@ import {
|
||||||
competenceCertificateProgressStatusCount,
|
competenceCertificateProgressStatusCount,
|
||||||
} from "@/pages/competence/utils";
|
} from "@/pages/competence/utils";
|
||||||
import type { CompetenceCertificate } from "@/types";
|
import type { CompetenceCertificate } from "@/types";
|
||||||
import * as log from "loglevel";
|
import log from "loglevel";
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
|
import CompetenceCertificateGrade from "./CompetenceCertificateGrade.vue";
|
||||||
|
|
||||||
log.debug("CompetenceCertificateComponent setup");
|
log.debug("CompetenceCertificateComponent setup");
|
||||||
|
|
||||||
|
|
@ -85,34 +86,10 @@ const showCourseSession = computed(() => {
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section v-if="userPointsEvaluatedAssignments > 0">
|
<CompetenceCertificateGrade
|
||||||
<div class="flex items-center">
|
:detail-view="detailView"
|
||||||
<div
|
:competence-certificate="competenceCertificate"
|
||||||
class="py-4"
|
/>
|
||||||
:class="{ 'heading-1': props.detailView, 'heading-2': !props.detailView }"
|
|
||||||
:data-cy="`certificate-${competenceCertificate.slug}-grade`"
|
|
||||||
>
|
|
||||||
{{ $t("a.Note") }}: {{ userGrade }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="text-gray-900"
|
|
||||||
:data-cy="`certificate-${competenceCertificate.slug}-grade-percent`"
|
|
||||||
>
|
|
||||||
{{ $t("a.Ungerundete Note") }}: {{ userGradeRounded2Places }}.
|
|
||||||
<a
|
|
||||||
:href="$t('a.wegleitungUkUrl')"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
class="underline"
|
|
||||||
>
|
|
||||||
{{ $t("a.Wegleitung üK") }}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<section v-else class="py-2">
|
|
||||||
{{ $t("a.competenceCertificateNoUserPoints") }}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<ItProgress :status-count="progressStatusCount" />
|
<ItProgress :status-count="progressStatusCount" />
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { CompetenceCertificate } from "@/types";
|
||||||
|
import { useTranslation } from "i18next-vue";
|
||||||
|
import { computed } from "vue";
|
||||||
|
import { assignmentsUserPoints, calcCompetenceCertificateGrade } from "./utils";
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
detailView: boolean;
|
||||||
|
competenceCertificate: CompetenceCertificate;
|
||||||
|
compactView?: boolean;
|
||||||
|
label?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
compactView: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const userGrade = computed(() => {
|
||||||
|
return calcCompetenceCertificateGrade(props.competenceCertificate.assignments, true);
|
||||||
|
});
|
||||||
|
const userPointsEvaluatedAssignments = computed(() => {
|
||||||
|
return assignmentsUserPoints(props.competenceCertificate.assignments);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section v-if="userPointsEvaluatedAssignments > 0">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div
|
||||||
|
class="py-4"
|
||||||
|
:class="detailView ? 'heading-1' : compactView ? 'heading-3' : 'heading-2'"
|
||||||
|
:data-cy="`certificate-${competenceCertificate.slug}-grade`"
|
||||||
|
>
|
||||||
|
{{ label ?? $t("a.Note") }}: {{ userGrade }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section v-else class="py-2">
|
||||||
|
{{ $t("a.competenceCertificateNoUserPoints") }}
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
@ -22,7 +22,8 @@ const { id: currentUserId } = useUserStore();
|
||||||
|
|
||||||
const { competenceCertificates } = useAllCompetenceCertificates(
|
const { competenceCertificates } = useAllCompetenceCertificates(
|
||||||
props.userId ?? currentUserId,
|
props.userId ?? currentUserId,
|
||||||
props.courseSlug
|
props.courseSlug,
|
||||||
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
const assignments = computed(() => {
|
const assignments = computed(() => {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useAllCompetenceCertificates } from "@/composables";
|
||||||
|
import { useUserStore } from "@/stores/user";
|
||||||
|
import CompetenceCertificateComponent from "./CompetenceCertificateComponent.vue";
|
||||||
|
import CompetenceCertificateGrade from "./CompetenceCertificateGrade.vue";
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
courseSlug: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const { id: currentUserId } = useUserStore();
|
||||||
|
const { competenceCertificates } = useAllCompetenceCertificates(
|
||||||
|
currentUserId,
|
||||||
|
props.courseSlug
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="bg-gray-200">
|
||||||
|
<div class="container-large">
|
||||||
|
<h1>Certificate Overview</h1>
|
||||||
|
<div v-for="certificate in competenceCertificates" :key="certificate.id">
|
||||||
|
<CompetenceCertificateGrade
|
||||||
|
:label="$t('a.Note')"
|
||||||
|
:compact-view="true"
|
||||||
|
:detail-view="false"
|
||||||
|
:competence-certificate="certificate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<CompetenceCertificateComponent
|
||||||
|
v-for="certificate in competenceCertificates"
|
||||||
|
:key="certificate.id"
|
||||||
|
:detail-view="false"
|
||||||
|
:competence-certificate="certificate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -23,14 +23,21 @@ export function assignmentsUserPoints(assignments: CompetenceCertificateAssignme
|
||||||
).toFixed(1);
|
).toFixed(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the grade by summing up all the weighted percentage of points of each assignment.
|
||||||
|
*
|
||||||
|
* @param assignments - list of assignments
|
||||||
|
* @param roundedToHalfGrade - should the grade be rounded?
|
||||||
|
*/
|
||||||
export function calcCompetenceCertificateGrade(
|
export function calcCompetenceCertificateGrade(
|
||||||
assignments: CompetenceCertificateAssignment[],
|
assignments: CompetenceCertificateAssignment[],
|
||||||
roundedToHalfGrade = true
|
roundedToHalfGrade: boolean = true
|
||||||
) {
|
) {
|
||||||
const evaluatedAssignments = assignments.filter(
|
const evaluatedAssignments = assignments.filter(
|
||||||
(a) => a.completions?.[0]?.completion_status === "EVALUATION_SUBMITTED"
|
(a) => a.completions?.[0]?.completion_status === "EVALUATION_SUBMITTED"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// sum((points_x / max_points) * weight_x)
|
||||||
const adjustedResults = evaluatedAssignments.map((a) => {
|
const adjustedResults = evaluatedAssignments.map((a) => {
|
||||||
return (
|
return (
|
||||||
((a.completions?.[0]?.evaluation_points_final ?? 0) / a.max_points) *
|
((a.completions?.[0]?.evaluation_points_final ?? 0) / a.max_points) *
|
||||||
|
|
@ -38,6 +45,7 @@ export function calcCompetenceCertificateGrade(
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// count only assignments with weight
|
||||||
const adjustedAssignmentCount = _.sum(
|
const adjustedAssignmentCount = _.sum(
|
||||||
evaluatedAssignments.map((a) => a.competence_certificate_weight)
|
evaluatedAssignments.map((a) => a.competence_certificate_weight)
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import { onboardingRedirect } from "@/router/onboarding";
|
||||||
import { createRouter, createWebHistory } from "vue-router";
|
import { createRouter, createWebHistory } from "vue-router";
|
||||||
import {
|
import {
|
||||||
ATTENDANCE_ROUTE,
|
ATTENDANCE_ROUTE,
|
||||||
|
CERTIFICATE_OVERVIEW_ROUTE,
|
||||||
CERTIFICATES_ROUTE,
|
CERTIFICATES_ROUTE,
|
||||||
COCKPIT_ROUTE,
|
COCKPIT_ROUTE,
|
||||||
COMPETENCE_ROUTE,
|
COMPETENCE_ROUTE,
|
||||||
|
|
@ -89,6 +90,12 @@ const router = createRouter({
|
||||||
component: () => import("@/pages/dashboard/DashboardCostPage.vue"),
|
component: () => import("@/pages/dashboard/DashboardCostPage.vue"),
|
||||||
props: true,
|
props: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/dashboard/certificates/:courseSlug",
|
||||||
|
component: () =>
|
||||||
|
import("@/pages/competence/CompetenceCertificateOverviewPage.vue"),
|
||||||
|
props: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/course/:courseSlug",
|
path: "/course/:courseSlug",
|
||||||
props: true,
|
props: true,
|
||||||
|
|
@ -117,6 +124,7 @@ const router = createRouter({
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
path: "competence",
|
path: "competence",
|
||||||
component: () => import("@/pages/competence/CompetenceParentPage.vue"),
|
component: () => import("@/pages/competence/CompetenceParentPage.vue"),
|
||||||
|
|
@ -189,6 +197,14 @@ const router = createRouter({
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
path: "certificates",
|
||||||
|
name: CERTIFICATE_OVERVIEW_ROUTE,
|
||||||
|
props: true,
|
||||||
|
component: () =>
|
||||||
|
import("@/pages/competence/CompetenceCertificateOverviewPage.vue"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "profile/:userId",
|
path: "profile/:userId",
|
||||||
component: () => import("@/pages/userProfile/UserProfilePage.vue"),
|
component: () => import("@/pages/userProfile/UserProfilePage.vue"),
|
||||||
|
|
|
||||||
|
|
@ -7,3 +7,7 @@ export const COCKPIT_ROUTE = "cockpit-home";
|
||||||
export const ATTENDANCE_ROUTE = "attendance";
|
export const ATTENDANCE_ROUTE = "attendance";
|
||||||
export const DOCUMENTS_ROUTE = "documents";
|
export const DOCUMENTS_ROUTE = "documents";
|
||||||
export const PERSONAL_PROFILE_ROUTE = "personalProfile";
|
export const PERSONAL_PROFILE_ROUTE = "personalProfile";
|
||||||
|
export const MENTORS_PARTICIPANTS_ROUTE = "mentorsAndParticipants";
|
||||||
|
export const MENTOR_OVERVIEW_ROUTE = "learningMentorOverview";
|
||||||
|
export const LEARN_ROUTE = "learn";
|
||||||
|
export const CERTIFICATE_OVERVIEW_ROUTE = "certificateOverview";
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { isInFuture } from "@/components/dueDates/dueDatesUtils";
|
||||||
|
|
||||||
|
export type Status = "done" | "soon" | "now";
|
||||||
|
|
||||||
|
export const getStatus = (done: boolean, date: string): Status => {
|
||||||
|
if (done) {
|
||||||
|
return "done";
|
||||||
|
}
|
||||||
|
if (isInFuture(date)) {
|
||||||
|
return "soon";
|
||||||
|
}
|
||||||
|
return "now";
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import django
|
||||||
|
from django.db import transaction
|
||||||
|
|
||||||
|
sys.path.append("../server")
|
||||||
|
|
||||||
|
os.environ.setdefault("IT_APP_ENVIRONMENT", "local")
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.base")
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
|
||||||
|
from vbv_lernwelt.core.models import User
|
||||||
|
|
||||||
|
# Get the user whose password you want to use as the reference
|
||||||
|
reference_user = User.objects.get(email='axel.manderbach@lernetz.ch')
|
||||||
|
reference_user.set_password('test')
|
||||||
|
reference_user.save()
|
||||||
|
|
||||||
|
# Update the password for all users
|
||||||
|
with transaction.atomic():
|
||||||
|
User.objects.update(password=reference_user.password)
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
from django.contrib import admin, messages
|
from django.contrib import admin
|
||||||
from django.contrib.auth import admin as auth_admin
|
from django.contrib.auth import admin as auth_admin
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
@ -11,12 +11,6 @@ from vbv_lernwelt.core.models import (
|
||||||
SecurityRequestResponseLog,
|
SecurityRequestResponseLog,
|
||||||
)
|
)
|
||||||
from vbv_lernwelt.core.utils import pretty_print_json
|
from vbv_lernwelt.core.utils import pretty_print_json
|
||||||
from vbv_lernwelt.learning_mentor.services import (
|
|
||||||
create_or_sync_ausbildungsverantwortlicher as create_or_sync_av,
|
|
||||||
)
|
|
||||||
from vbv_lernwelt.learning_mentor.services import (
|
|
||||||
create_or_sync_berufsbildner as create_or_sync_bb,
|
|
||||||
)
|
|
||||||
|
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
@ -38,45 +32,6 @@ class LogAdmin(admin.ModelAdmin):
|
||||||
return pretty_print_json(json_string)
|
return pretty_print_json(json_string)
|
||||||
|
|
||||||
|
|
||||||
@admin.action(description="Berufsbildner: Create or Sync")
|
|
||||||
def create_or_sync_berufsbildner(modeladmin, request, queryset):
|
|
||||||
# keep it easy
|
|
||||||
success = []
|
|
||||||
for user in queryset:
|
|
||||||
success.append(create_or_sync_bb(user))
|
|
||||||
if all(success):
|
|
||||||
messages.add_message(
|
|
||||||
request,
|
|
||||||
messages.SUCCESS,
|
|
||||||
"Berufsbildner erfolgreich erstellt oder synchronisiert",
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
messages.add_message(
|
|
||||||
request,
|
|
||||||
messages.ERROR,
|
|
||||||
"Einige Berufsbildner konnten nicht erstellt oder synchronisiert werden",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@admin.action(description="Ausbildungsverantwortlicher: Create or Sync")
|
|
||||||
def create_or_sync_ausbildungsverantwortlicher(modeladmin, request, queryset):
|
|
||||||
success = []
|
|
||||||
for user in queryset:
|
|
||||||
success.append(create_or_sync_av(user))
|
|
||||||
if all(success):
|
|
||||||
messages.add_message(
|
|
||||||
request,
|
|
||||||
messages.SUCCESS,
|
|
||||||
"Ausbildungsverantwortlicher erfolgreich erstellt oder synchronisiert",
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
messages.add_message(
|
|
||||||
request,
|
|
||||||
messages.ERROR,
|
|
||||||
"Einige Ausbildungsverantwortliche konnten nicht erstellt oder synchronisiert werden",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(User)
|
@admin.register(User)
|
||||||
class UserAdmin(auth_admin.UserAdmin):
|
class UserAdmin(auth_admin.UserAdmin):
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
|
|
@ -139,7 +94,6 @@ class UserAdmin(auth_admin.UserAdmin):
|
||||||
]
|
]
|
||||||
list_filter = ("is_staff", "is_superuser", "is_active", "groups", "organisation")
|
list_filter = ("is_staff", "is_superuser", "is_active", "groups", "organisation")
|
||||||
search_fields = ["first_name", "last_name", "email", "username", "sso_id"]
|
search_fields = ["first_name", "last_name", "email", "username", "sso_id"]
|
||||||
actions = [create_or_sync_berufsbildner, create_or_sync_ausbildungsverantwortlicher]
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(JobLog)
|
@admin.register(JobLog)
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,39 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin, messages
|
||||||
|
|
||||||
from vbv_lernwelt.learning_mentor.models import (
|
from vbv_lernwelt.learning_mentor.models import (
|
||||||
AgentParticipantRelation,
|
AgentParticipantRelation,
|
||||||
MentorInvitation,
|
MentorInvitation,
|
||||||
|
OrganisationSupervisor,
|
||||||
|
OrganisationSupervisortRoleType,
|
||||||
)
|
)
|
||||||
|
from vbv_lernwelt.learning_mentor.services import (
|
||||||
|
create_or_sync_ausbildungsverantwortlicher,
|
||||||
|
create_or_sync_berufsbildner,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.action(description="Organisation Supervisor: Sync")
|
||||||
|
def create_or_sync_org_supervisor(_modeladmin, request, queryset):
|
||||||
|
success = []
|
||||||
|
for supervisor in queryset:
|
||||||
|
sync_fn = (
|
||||||
|
create_or_sync_berufsbildner
|
||||||
|
if supervisor.role == OrganisationSupervisortRoleType.BERUFSBILDNER.value
|
||||||
|
else create_or_sync_ausbildungsverantwortlicher
|
||||||
|
)
|
||||||
|
success.append(sync_fn(supervisor.supervisor, supervisor.organisation))
|
||||||
|
if all(success):
|
||||||
|
messages.add_message(
|
||||||
|
request,
|
||||||
|
messages.SUCCESS,
|
||||||
|
"Organisation Supervisor synchronisiert",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
messages.add_message(
|
||||||
|
request,
|
||||||
|
messages.ERROR,
|
||||||
|
"Einige Organisation Supervisors konnten nicht synchronisiert werden",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(AgentParticipantRelation)
|
@admin.register(AgentParticipantRelation)
|
||||||
|
|
@ -33,3 +63,17 @@ class MentorInvitationAdmin(admin.ModelAdmin):
|
||||||
list_display = ["id", "email", "participant", "created"]
|
list_display = ["id", "email", "participant", "created"]
|
||||||
readonly_fields = ["id", "created", "email", "participant"]
|
readonly_fields = ["id", "created", "email", "participant"]
|
||||||
search_fields = ["email"]
|
search_fields = ["email"]
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(OrganisationSupervisor)
|
||||||
|
class OrganisationSupervisorAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ["supervisor", "organisation", "role"]
|
||||||
|
|
||||||
|
search_fields = ["supervisor"]
|
||||||
|
|
||||||
|
raw_id_fields = [
|
||||||
|
"supervisor",
|
||||||
|
]
|
||||||
|
|
||||||
|
list_filter = ["role", "organisation"]
|
||||||
|
actions = [create_or_sync_org_supervisor]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
# Generated by Django 4.2.13 on 2024-11-20 06:22
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("core", "0012_auto_20240621_1626"),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
("learning_mentor", "0010_alter_agentparticipantrelation_role"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="OrganisationSupervisor",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"role",
|
||||||
|
models.CharField(
|
||||||
|
choices=[
|
||||||
|
(
|
||||||
|
"AUSBILDUNGSVERANTWORTLICHER",
|
||||||
|
"AUSBILDUNGSVERANTWORTLICHER",
|
||||||
|
),
|
||||||
|
("BERUFSBILDNER", "BERUFSBILDNER"),
|
||||||
|
],
|
||||||
|
default="AUSBILDUNGSVERANTWORTLICHER",
|
||||||
|
max_length=255,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"organisation",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="core.organisation",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"supervisor",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -4,7 +4,7 @@ from enum import Enum
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django_extensions.db.models import TimeStampedModel
|
from django_extensions.db.models import TimeStampedModel
|
||||||
|
|
||||||
from vbv_lernwelt.core.models import User
|
from vbv_lernwelt.core.models import Organisation, User
|
||||||
from vbv_lernwelt.course.models import CourseSessionUser
|
from vbv_lernwelt.course.models import CourseSessionUser
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -65,3 +65,39 @@ class MentorInvitation(TimeStampedModel):
|
||||||
verbose_name = "Lernbegleiter Einladung"
|
verbose_name = "Lernbegleiter Einladung"
|
||||||
verbose_name_plural = "Lernbegleiter Einladungen"
|
verbose_name_plural = "Lernbegleiter Einladungen"
|
||||||
unique_together = [["email", "participant"]]
|
unique_together = [["email", "participant"]]
|
||||||
|
|
||||||
|
|
||||||
|
class OrganisationSupervisortRoleType(Enum):
|
||||||
|
AUSBILDUNGSVERANTWORTLICHER = "AUSBILDUNGSVERANTWORTLICHER"
|
||||||
|
BERUFSBILDNER = "BERUFSBILDNER"
|
||||||
|
|
||||||
|
|
||||||
|
class OrganisationSupervisor(models.Model):
|
||||||
|
supervisor = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
|
organisation = models.ForeignKey(Organisation, on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
role = models.CharField(
|
||||||
|
max_length=255,
|
||||||
|
choices=[(t.value, t.value) for t in OrganisationSupervisortRoleType],
|
||||||
|
default=OrganisationSupervisortRoleType.AUSBILDUNGSVERANTWORTLICHER.value,
|
||||||
|
)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if (
|
||||||
|
self.role
|
||||||
|
== OrganisationSupervisortRoleType.AUSBILDUNGSVERANTWORTLICHER.value
|
||||||
|
):
|
||||||
|
from vbv_lernwelt.learning_mentor.services import (
|
||||||
|
create_or_sync_ausbildungsverantwortlicher,
|
||||||
|
)
|
||||||
|
|
||||||
|
create_or_sync_ausbildungsverantwortlicher(
|
||||||
|
self.supervisor, self.organisation
|
||||||
|
)
|
||||||
|
elif self.role == OrganisationSupervisortRoleType.BERUFSBILDNER.value:
|
||||||
|
from vbv_lernwelt.learning_mentor.services import (
|
||||||
|
create_or_sync_berufsbildner,
|
||||||
|
)
|
||||||
|
|
||||||
|
create_or_sync_berufsbildner(self.supervisor, self.organisation)
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,15 @@
|
||||||
|
from typing import List
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
|
|
||||||
from vbv_lernwelt.core.models import User
|
from vbv_lernwelt.core.models import Organisation, User
|
||||||
from vbv_lernwelt.course.consts import (
|
from vbv_lernwelt.course.consts import (
|
||||||
COURSE_UK,
|
COURSE_UK,
|
||||||
COURSE_UK_FR,
|
COURSE_UK_FR,
|
||||||
COURSE_UK_IT,
|
COURSE_UK_IT,
|
||||||
VV_COURSE_IDS,
|
VV_COURSE_IDS,
|
||||||
)
|
)
|
||||||
from vbv_lernwelt.course.models import CourseSessionUser
|
from vbv_lernwelt.course.models import Course, CourseSessionUser
|
||||||
from vbv_lernwelt.learning_mentor.models import (
|
from vbv_lernwelt.learning_mentor.models import (
|
||||||
AgentParticipantRelation,
|
AgentParticipantRelation,
|
||||||
AgentParticipantRoleType,
|
AgentParticipantRoleType,
|
||||||
|
|
@ -18,50 +20,75 @@ logger = structlog.get_logger(__name__)
|
||||||
UK_COURSES = [COURSE_UK, COURSE_UK_FR, COURSE_UK_IT]
|
UK_COURSES = [COURSE_UK, COURSE_UK_FR, COURSE_UK_IT]
|
||||||
|
|
||||||
|
|
||||||
def create_or_sync_berufsbildner(berufsbildner: User) -> bool:
|
def users_by_org(
|
||||||
new_members = set(
|
org: Organisation,
|
||||||
CourseSessionUser.objects.filter(user__organisation=berufsbildner.organisation)
|
is_uk: bool,
|
||||||
.filter(course_session__course__configuration__is_uk=True)
|
courses: List[Course],
|
||||||
|
excluded_course_sessions: List[int] = None,
|
||||||
|
) -> set[CourseSessionUser]:
|
||||||
|
if not excluded_course_sessions:
|
||||||
|
excluded_course_sessions = []
|
||||||
|
|
||||||
|
return set(
|
||||||
|
CourseSessionUser.objects.filter(user__organisation=org)
|
||||||
|
.filter(course_session__course__configuration__is_uk=is_uk)
|
||||||
.filter(role=CourseSessionUser.Role.MEMBER.value)
|
.filter(role=CourseSessionUser.Role.MEMBER.value)
|
||||||
.filter(course_session__course_id__in=UK_COURSES)
|
.filter(course_session__course_id__in=courses)
|
||||||
.exclude(course_session_id__in=[4, 5, 6])
|
.exclude(course_session_id__in=excluded_course_sessions)
|
||||||
)
|
)
|
||||||
return create_or_sync_learning_mentor(berufsbildner, new_members)
|
|
||||||
|
|
||||||
|
def uk_cs_users_by_org(org: Organisation) -> set[CourseSessionUser]:
|
||||||
|
return users_by_org(
|
||||||
|
org,
|
||||||
|
is_uk=True,
|
||||||
|
courses=UK_COURSES,
|
||||||
|
# ignore "Demo" course sessions
|
||||||
|
excluded_course_sessions=[4, 5, 6],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def vv_cs_users_by_org(org: Organisation) -> set[CourseSessionUser]:
|
||||||
|
return users_by_org(org, False, VV_COURSE_IDS)
|
||||||
|
|
||||||
|
|
||||||
|
def create_or_sync_berufsbildner(
|
||||||
|
berufsbildner: User, organisation: Organisation
|
||||||
|
) -> bool:
|
||||||
|
org_members = uk_cs_users_by_org(organisation)
|
||||||
|
return create_or_sync_learning_mentor(berufsbildner, org_members, organisation)
|
||||||
|
|
||||||
|
|
||||||
def create_or_sync_ausbildungsverantwortlicher(
|
def create_or_sync_ausbildungsverantwortlicher(
|
||||||
ausbildungsverantwortlicher: User,
|
ausbildungsverantwortlicher: User, organisation: Organisation
|
||||||
) -> bool:
|
) -> bool:
|
||||||
new_members = set(
|
org_members = vv_cs_users_by_org(organisation)
|
||||||
CourseSessionUser.objects.filter(
|
return create_or_sync_learning_mentor(
|
||||||
user__organisation=ausbildungsverantwortlicher.organisation
|
ausbildungsverantwortlicher, org_members, organisation
|
||||||
)
|
|
||||||
.filter(course_session__course__configuration__is_uk=False)
|
|
||||||
.filter(role=CourseSessionUser.Role.MEMBER.value)
|
|
||||||
.filter(course_session__course_id__in=VV_COURSE_IDS)
|
|
||||||
)
|
)
|
||||||
return create_or_sync_learning_mentor(ausbildungsverantwortlicher, new_members)
|
|
||||||
|
|
||||||
|
|
||||||
def create_or_sync_learning_mentor(
|
def create_or_sync_learning_mentor(
|
||||||
agent: User, new_members: set[CourseSessionUser]
|
agent: User, org_members: set[CourseSessionUser], organisation: Organisation
|
||||||
) -> bool:
|
) -> bool:
|
||||||
logger.info(
|
logger.info(
|
||||||
"Creating or syncing berufsbildner",
|
"Creating or syncing berufsbildner/ausbildungsverantwortlicher",
|
||||||
berufsbildner=agent,
|
berufsbildner=agent,
|
||||||
org=agent.organisation.name_de,
|
org=organisation.name_de,
|
||||||
)
|
)
|
||||||
|
|
||||||
# check if it is a valid organisation
|
# Check if it is a valid organisation
|
||||||
if agent.organisation and agent.organisation.organisation_id < 4:
|
# ids < 4 are "andere Broker/Krankenversicherer"
|
||||||
logger.error("Invalid organisation", org=agent.organisation)
|
if organisation and organisation.organisation_id < 4:
|
||||||
|
logger.error("Invalid organisation", org=organisation)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# get existing connections
|
# Get existing connections (full relation objects)
|
||||||
existing_members = set(agent.agentparticipantrelation_set.all())
|
existing_relations = set(agent.agentparticipantrelation_set.all())
|
||||||
|
|
||||||
# add new relations that are not in existing relations
|
# Add new relations that are not in existing relations
|
||||||
for csu in new_members:
|
existing_members = {relation.participant for relation in existing_relations}
|
||||||
|
for csu in org_members:
|
||||||
if csu not in existing_members:
|
if csu not in existing_members:
|
||||||
AgentParticipantRelation.objects.get_or_create(
|
AgentParticipantRelation.objects.get_or_create(
|
||||||
agent=agent,
|
agent=agent,
|
||||||
|
|
@ -69,9 +96,42 @@ def create_or_sync_learning_mentor(
|
||||||
role=AgentParticipantRoleType.BERUFSBILDNER.value,
|
role=AgentParticipantRoleType.BERUFSBILDNER.value,
|
||||||
)
|
)
|
||||||
|
|
||||||
# remove old relations that are not in the new relations
|
# Remove old relations that are not in the new relations
|
||||||
for relation in existing_members:
|
for relation in existing_relations:
|
||||||
if relation.participant not in new_members:
|
if relation.participant not in org_members:
|
||||||
relation.delete()
|
relation.delete()
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def delete_berufsbildner_relation(berufsbildner: User, organisation: Organisation):
|
||||||
|
org_members = uk_cs_users_by_org(organisation)
|
||||||
|
delete_org_supervisor_relation(berufsbildner, org_members)
|
||||||
|
|
||||||
|
|
||||||
|
def delete_ausbildungsverantwortlicher_relation(
|
||||||
|
ausbildungsverantwortlicher: User, organisation: Organisation
|
||||||
|
):
|
||||||
|
org_members = vv_cs_users_by_org(organisation)
|
||||||
|
delete_org_supervisor_relation(ausbildungsverantwortlicher, org_members)
|
||||||
|
|
||||||
|
|
||||||
|
def delete_org_supervisor_relation(
|
||||||
|
agent: User,
|
||||||
|
org_members: set[CourseSessionUser],
|
||||||
|
):
|
||||||
|
# As the key berufsbildner is used in several courses, we use org_members to select the ones from the correct
|
||||||
|
# course sessions
|
||||||
|
relations_to_delete = agent.agentparticipantrelation_set.filter(
|
||||||
|
participant__in=org_members
|
||||||
|
)
|
||||||
|
|
||||||
|
# Bulk delete the identified relations
|
||||||
|
deleted_count, _ = relations_to_delete.delete()
|
||||||
|
|
||||||
|
# Log the result
|
||||||
|
logger.info(
|
||||||
|
"Deleted ausbildungsverantwortlicher relations",
|
||||||
|
agent=agent,
|
||||||
|
deleted_count=deleted_count,
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
import structlog
|
||||||
|
from django.db.models.signals import pre_delete
|
||||||
|
from django.dispatch import receiver
|
||||||
|
|
||||||
|
from vbv_lernwelt.learning_mentor.models import (
|
||||||
|
OrganisationSupervisor,
|
||||||
|
OrganisationSupervisortRoleType,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger = structlog.get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# CourseSessionGroup
|
||||||
|
@receiver(
|
||||||
|
pre_delete,
|
||||||
|
sender=OrganisationSupervisor,
|
||||||
|
dispatch_uid="remove_org_supervisor_relations",
|
||||||
|
)
|
||||||
|
def remove_org_supervisor_relations(sender, instance: OrganisationSupervisor, **kwargs):
|
||||||
|
if (
|
||||||
|
instance.role
|
||||||
|
== OrganisationSupervisortRoleType.AUSBILDUNGSVERANTWORTLICHER.value
|
||||||
|
):
|
||||||
|
from vbv_lernwelt.learning_mentor.services import (
|
||||||
|
delete_ausbildungsverantwortlicher_relation,
|
||||||
|
)
|
||||||
|
|
||||||
|
delete_ausbildungsverantwortlicher_relation(
|
||||||
|
instance.supervisor, instance.organisation
|
||||||
|
)
|
||||||
|
elif instance.role == OrganisationSupervisortRoleType.BERUFSBILDNER.value:
|
||||||
|
from vbv_lernwelt.learning_mentor.services import (
|
||||||
|
delete_berufsbildner_relation,
|
||||||
|
)
|
||||||
|
|
||||||
|
delete_berufsbildner_relation(instance.supervisor, instance.organisation)
|
||||||
|
|
@ -0,0 +1,184 @@
|
||||||
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
|
from vbv_lernwelt.core.admin import User
|
||||||
|
from vbv_lernwelt.core.models import Organisation
|
||||||
|
from vbv_lernwelt.course.creators.test_utils import (
|
||||||
|
add_course_session_user,
|
||||||
|
create_course,
|
||||||
|
create_course_session,
|
||||||
|
create_user,
|
||||||
|
)
|
||||||
|
from vbv_lernwelt.course.models import CourseConfiguration, CourseSessionUser
|
||||||
|
from vbv_lernwelt.learning_mentor.services import (
|
||||||
|
create_or_sync_learning_mentor,
|
||||||
|
users_by_org,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_completion_for_user(
|
||||||
|
completions: List[Dict[str, str]], user: User
|
||||||
|
) -> Optional[Dict[str, str]]:
|
||||||
|
for completion in completions:
|
||||||
|
if completion["user_id"] == str(user.id):
|
||||||
|
return completion
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class OrganisationSupervisorTestCase(APITestCase):
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self.course, self.course_page = create_course("Test Course")
|
||||||
|
self.course_session = create_course_session(course=self.course, title="Test VV")
|
||||||
|
self.course_config = CourseConfiguration.objects.get(course=self.course)
|
||||||
|
|
||||||
|
self.mobi = Organisation.objects.get_or_create(
|
||||||
|
name_de="Die Mobiliar",
|
||||||
|
defaults={
|
||||||
|
"organisation_id": 100,
|
||||||
|
"name_de": "Die Mobiliar",
|
||||||
|
},
|
||||||
|
)[0]
|
||||||
|
self.baloise = Organisation.objects.get_or_create(
|
||||||
|
name_de="Baloise",
|
||||||
|
defaults={
|
||||||
|
"organisation_id": 101,
|
||||||
|
"name_de": "Baloise",
|
||||||
|
},
|
||||||
|
)[0]
|
||||||
|
|
||||||
|
self.supervisor = create_user("supervisor")
|
||||||
|
self.participant_1 = add_course_session_user(
|
||||||
|
self.course_session,
|
||||||
|
create_user("participant_1"),
|
||||||
|
role=CourseSessionUser.Role.MEMBER,
|
||||||
|
)
|
||||||
|
self.participant_2 = add_course_session_user(
|
||||||
|
self.course_session,
|
||||||
|
create_user("participant_2"),
|
||||||
|
role=CourseSessionUser.Role.MEMBER,
|
||||||
|
)
|
||||||
|
self.participant_3 = add_course_session_user(
|
||||||
|
self.course_session,
|
||||||
|
create_user("participant_3"),
|
||||||
|
role=CourseSessionUser.Role.MEMBER,
|
||||||
|
)
|
||||||
|
self.participant_4 = add_course_session_user(
|
||||||
|
self.course_session,
|
||||||
|
create_user("participant_4"),
|
||||||
|
role=CourseSessionUser.Role.MEMBER,
|
||||||
|
)
|
||||||
|
self.participant_1.user.organisation = self.mobi
|
||||||
|
self.participant_1.user.save()
|
||||||
|
self.participant_2.user.organisation = self.mobi
|
||||||
|
self.participant_2.user.save()
|
||||||
|
self.participant_3.user.organisation = self.mobi
|
||||||
|
self.participant_3.user.save()
|
||||||
|
self.participant_4.user.organisation = self.baloise
|
||||||
|
self.participant_4.user.save()
|
||||||
|
|
||||||
|
def test_add_can_berufsbildner(self) -> None:
|
||||||
|
# GIVEN
|
||||||
|
self.course_config.is_uk = True
|
||||||
|
self.course_config.save()
|
||||||
|
|
||||||
|
# WHEN
|
||||||
|
org_members = users_by_org(self.mobi, True, [self.course])
|
||||||
|
success = create_or_sync_learning_mentor(
|
||||||
|
self.supervisor, org_members, self.mobi
|
||||||
|
)
|
||||||
|
agent_participant_relations = self.supervisor.agentparticipantrelation_set.all()
|
||||||
|
|
||||||
|
# THEN
|
||||||
|
self.assertTrue(success)
|
||||||
|
self.assertEqual(len(agent_participant_relations), 3)
|
||||||
|
|
||||||
|
def test_add_cannot_berufsbildner_if_excluded(self) -> None:
|
||||||
|
# GIVEN
|
||||||
|
self.course_config.is_uk = True
|
||||||
|
self.course_config.save()
|
||||||
|
|
||||||
|
# WHEN
|
||||||
|
org_members = users_by_org(
|
||||||
|
self.mobi,
|
||||||
|
True,
|
||||||
|
[self.course],
|
||||||
|
excluded_course_sessions=[self.course_session.id],
|
||||||
|
)
|
||||||
|
success = create_or_sync_learning_mentor(
|
||||||
|
self.supervisor, org_members, self.mobi
|
||||||
|
)
|
||||||
|
agent_participant_relations = self.supervisor.agentparticipantrelation_set.all()
|
||||||
|
|
||||||
|
# THEN
|
||||||
|
self.assertTrue(success)
|
||||||
|
self.assertEqual(len(agent_participant_relations), 0)
|
||||||
|
|
||||||
|
def test_add_cannot_berufsbildner_if_not_uk(self) -> None:
|
||||||
|
# GIVEN
|
||||||
|
self.course_config.is_uk = False
|
||||||
|
self.course_config.save()
|
||||||
|
|
||||||
|
# WHEN
|
||||||
|
org_members = users_by_org(self.mobi, True, [self.course])
|
||||||
|
success = create_or_sync_learning_mentor(
|
||||||
|
self.supervisor, org_members, self.mobi
|
||||||
|
)
|
||||||
|
agent_participant_relations = self.supervisor.agentparticipantrelation_set.all()
|
||||||
|
|
||||||
|
# THEN
|
||||||
|
self.assertTrue(success)
|
||||||
|
self.assertEqual(len(agent_participant_relations), 0)
|
||||||
|
|
||||||
|
def test_add_can_ausbildungsverantwortlicher(self) -> None:
|
||||||
|
# GIVEN
|
||||||
|
self.course_config.is_uk = False
|
||||||
|
self.course_config.save()
|
||||||
|
|
||||||
|
# WHEN
|
||||||
|
org_members = users_by_org(self.mobi, False, [self.course])
|
||||||
|
success = create_or_sync_learning_mentor(
|
||||||
|
self.supervisor, org_members, self.mobi
|
||||||
|
)
|
||||||
|
agent_participant_relations = self.supervisor.agentparticipantrelation_set.all()
|
||||||
|
|
||||||
|
# THEN
|
||||||
|
self.assertTrue(success)
|
||||||
|
self.assertEqual(len(agent_participant_relations), 3)
|
||||||
|
|
||||||
|
def test_add_cannot_ausbildungsverantwortlicher_if_excluded(self) -> None:
|
||||||
|
# GIVEN
|
||||||
|
self.course_config.is_uk = False
|
||||||
|
self.course_config.save()
|
||||||
|
|
||||||
|
# WHEN
|
||||||
|
org_members = users_by_org(
|
||||||
|
self.mobi,
|
||||||
|
False,
|
||||||
|
[self.course],
|
||||||
|
excluded_course_sessions=[self.course_session.id],
|
||||||
|
)
|
||||||
|
success = create_or_sync_learning_mentor(
|
||||||
|
self.supervisor, org_members, self.mobi
|
||||||
|
)
|
||||||
|
agent_participant_relations = self.supervisor.agentparticipantrelation_set.all()
|
||||||
|
|
||||||
|
# THEN
|
||||||
|
self.assertTrue(success)
|
||||||
|
self.assertEqual(len(agent_participant_relations), 0)
|
||||||
|
|
||||||
|
def test_add_cannot_ausbildungsverantwortlicher_if_not_vv(self) -> None:
|
||||||
|
# GIVEN
|
||||||
|
self.course_config.is_uk = True
|
||||||
|
self.course_config.save()
|
||||||
|
|
||||||
|
# WHEN
|
||||||
|
org_members = users_by_org(self.mobi, False, [self.course])
|
||||||
|
success = create_or_sync_learning_mentor(
|
||||||
|
self.supervisor, org_members, self.mobi
|
||||||
|
)
|
||||||
|
agent_participant_relations = self.supervisor.agentparticipantrelation_set.all()
|
||||||
|
|
||||||
|
# THEN
|
||||||
|
self.assertTrue(success)
|
||||||
|
self.assertEqual(len(agent_participant_relations), 0)
|
||||||
Loading…
Reference in New Issue