Merged in feature/vbv-676-berufsbildner-2 (pull request #365)

Feature/vbv 676 berufsbildner 2
This commit is contained in:
Christian Cueni 2024-08-19 12:18:18 +00:00
commit 96dfa8bd31
128 changed files with 3833 additions and 1453 deletions

View File

@ -16,6 +16,7 @@ const props = defineProps<{
courseSession: CourseSession; courseSession: CourseSession;
learningContent: LearningContentAssignment | LearningContentEdoniqTest; learningContent: LearningContentAssignment | LearningContentEdoniqTest;
showTitle: boolean; showTitle: boolean;
userSelectionIds?: string[];
}>(); }>();
log.debug("AssignmentSubmissionProgress created", stringifyParse(props)); log.debug("AssignmentSubmissionProgress created", stringifyParse(props));
@ -31,13 +32,19 @@ const state = reactive({
}); });
onMounted(async () => { onMounted(async () => {
const { assignmentSubmittedUsers, gradedUsers, total } = // eslint-disable-next-line prefer-const
let { assignmentSubmittedUsers, gradedUsers, total } =
await loadAssignmentCompletionStatusData( await loadAssignmentCompletionStatusData(
props.learningContent.content_assignment.id, props.learningContent.content_assignment.id,
props.courseSession.id, props.courseSession.id,
props.learningContent.id props.learningContent.id,
props.userSelectionIds
); );
if (props.userSelectionIds && props.userSelectionIds.length > 0) {
total = props.userSelectionIds.length;
}
state.submissionProgressStatusCount = { state.submissionProgressStatusCount = {
SUCCESS: assignmentSubmittedUsers.length, SUCCESS: assignmentSubmittedUsers.length,
UNKNOWN: total - assignmentSubmittedUsers.length, UNKNOWN: total - assignmentSubmittedUsers.length,
@ -55,6 +62,9 @@ const doneCount = (status: StatusCount) => {
}; };
const totalCount = (status: StatusCount) => { const totalCount = (status: StatusCount) => {
if (props.userSelectionIds && props.userSelectionIds.length > 0) {
return props.userSelectionIds.length;
}
return doneCount(status) + status.UNKNOWN || 0; return doneCount(status) + status.UNKNOWN || 0;
}; };

View File

@ -4,16 +4,27 @@ import { onMounted, ref } from "vue";
import { fetchMenteeCount } from "@/services/dashboard"; import { fetchMenteeCount } from "@/services/dashboard";
import BaseBox from "@/components/dashboard/BaseBox.vue"; import BaseBox from "@/components/dashboard/BaseBox.vue";
const props = defineProps<{ const props = withDefaults(
courseId: string; defineProps<{
courseSlug: string; courseId: string;
}>(); courseSlug: string;
slim?: boolean;
count?: number;
}>(),
{
count: -1,
slim: false,
}
);
const menteeCount: Ref<number> = ref(0); const menteeCount: Ref<number> = ref(props.count);
onMounted(async () => { onMounted(async () => {
const data = await fetchMenteeCount(props.courseId); if (props.count == -1) {
menteeCount.value = data?.mentee_count; menteeCount.value = 0;
const data = await fetchMenteeCount(props.courseId);
menteeCount.value = data?.mentee_count;
}
}); });
</script> </script>
@ -21,11 +32,12 @@ onMounted(async () => {
<div class="w-[325px]"> <div class="w-[325px]">
<BaseBox <BaseBox
:details-link="`/dashboard/persons?course=${props.courseId}`" :details-link="`/dashboard/persons?course=${props.courseId}`"
data-cy="dashboard.mentor.competenceSummary" data-cy="dashboard.mentor.menteeCount"
:slim="props.slim"
> >
<template #title>{{ $t("a.Personen") }}</template> <template #title>{{ $t("a.Personen") }}</template>
<template #content> <template #content>
<div class="flex flex-row space-x-3 bg-white pb-6"> <div :class="['flex flex-row space-x-3 bg-white', slim ? '' : 'pb-6']">
<div <div
class="flex h-[74px] items-center justify-center py-1 pr-3 text-3xl font-bold" class="flex h-[74px] items-center justify-center py-1 pr-3 text-3xl font-bold"
> >

View File

@ -7,6 +7,7 @@ const props = defineProps<{
assignmentsCompleted: number; assignmentsCompleted: number;
avgPassed: number; avgPassed: number;
courseSlug: string; courseSlug: string;
detailsLink?: string;
}>(); }>();
const progress = computed(() => { const progress = computed(() => {
@ -20,7 +21,7 @@ const progress = computed(() => {
<template> <template>
<BaseBox <BaseBox
:details-link="`/statistic/${courseSlug}/assignment`" :details-link="props.detailsLink || `/statistic/${courseSlug}/assignment`"
data-cy="dashboard.stats.assignments" data-cy="dashboard.stats.assignments"
> >
<template #title>{{ $t("a.Kompetenznachweis-Elemente") }}</template> <template #title>{{ $t("a.Kompetenznachweis-Elemente") }}</template>

View File

@ -1,17 +1,23 @@
<script setup lang="ts"> <script setup lang="ts">
defineProps<{ withDefaults(
detailsLink: string; defineProps<{
}>(); detailsLink: string;
slim?: boolean;
}>(),
{
slim: false,
}
);
</script> </script>
<template> <template>
<div class="flex h-full flex-col space-y-4 bg-white"> <div :class="['flex h-full flex-col bg-white', slim ? '' : 'space-y-4']">
<h4 class="mb-1 font-bold"> <h4 class="mb-1 font-bold">
<slot name="title"></slot> <slot name="title"></slot>
</h4> </h4>
<slot name="content"></slot> <slot name="content"></slot>
<div class="flex-grow"></div> <div class="flex-grow"></div>
<div class="pt-8"> <div class="pt-0">
<router-link class="underline" :to="detailsLink" data-cy="basebox.detailsLink"> <router-link class="underline" :to="detailsLink" data-cy="basebox.detailsLink">
{{ $t("a.Details anschauen") }} {{ $t("a.Details anschauen") }}
</router-link> </router-link>

View File

@ -0,0 +1,93 @@
<script setup lang="ts">
import { computed, onMounted, ref } from "vue";
import {
type DashboardRoleKeyType,
fetchMentorCompetenceSummary,
} from "@/services/dashboard";
import type { BaseStatisticsType } from "@/gql/graphql";
import LoadingSpinner from "@/components/ui/LoadingSpinner.vue";
import AssignmentSummaryBox from "@/components/dashboard/AssignmentSummaryBox.vue";
import BaseBox from "@/components/dashboard/BaseBox.vue";
import { percentToRoundedGrade } from "@/services/assignmentService";
import AgentConnectionCount from "@/components/dashboard/AgentConnectionCount.vue";
const props = defineProps<{
courseSlug: string;
courseId: string;
agentRole: DashboardRoleKeyType;
}>();
const mentorData = ref<BaseStatisticsType | null>(null);
const loading = ref(true);
const assignmentStats = computed(() => {
return mentorData.value?.assignments ?? null;
});
const numberOfPerson = computed(() => {
return mentorData.value?.user_selection_ids?.length ?? 0;
});
onMounted(async () => {
mentorData.value = await fetchMentorCompetenceSummary(
props.courseId,
props.agentRole
);
loading.value = false;
});
const averageGrade = computed(() => {
return percentToRoundedGrade(
assignmentStats.value?.summary.average_evaluation_percent ?? 0,
false
);
});
</script>
<template>
<div v-if="loading" class="m-8 flex justify-center">
<LoadingSpinner />
</div>
<div v-if="assignmentStats" class="w-full space-y-8">
<div
class="flex flex-col flex-wrap justify-between gap-x-5 border-b border-gray-300 pb-8 last:border-0 md:flex-row"
>
<div class="flex-1">
<BaseBox
:details-link="`/statistic/berufsbildner/${props.courseSlug}/competence-grade`"
data-cy="dashboard.stats.competenceGrades"
>
<template #title>{{ $t("a.Kompetenznachweise") }}</template>
<template #content>
<div class="flex items-center gap-4">
<div class="min-w-12 text-center">
<div
class="heading-2 rounded bg-green-500 p-4"
:class="{ 'bg-red-400': averageGrade < 4 }"
>
{{ averageGrade }}
</div>
</div>
<div>
{{ $t("a.Durchschnittsnote") }}
</div>
</div>
</template>
</BaseBox>
</div>
<AssignmentSummaryBox
class="flex-1"
:assignments-completed="assignmentStats.summary.completed_count"
:avg-passed="assignmentStats.summary.average_passed"
:course-slug="props.courseSlug"
:details-link="`/statistic/berufsbildner/${props.courseSlug}/assignment`"
/>
</div>
<AgentConnectionCount
:course-id="props.courseId"
:course-slug="props.courseSlug"
:count="numberOfPerson"
:slim="true"
/>
</div>
</template>

View File

@ -5,10 +5,11 @@ import LearningPathDiagram from "@/components/learningPath/LearningPathDiagram.v
import CompetenceSummary from "@/components/dashboard/CompetenceSummary.vue"; import CompetenceSummary from "@/components/dashboard/CompetenceSummary.vue";
import AssignmentSummary from "@/components/dashboard/AssignmentSummary.vue"; import AssignmentSummary from "@/components/dashboard/AssignmentSummary.vue";
import MentorOpenTasksCount from "@/components/dashboard/MentorOpenTasksCount.vue"; import MentorOpenTasksCount from "@/components/dashboard/MentorOpenTasksCount.vue";
import MentorMenteeCount from "@/components/dashboard/MentorMenteeCount.vue"; import AgentConnectionCount from "@/components/dashboard/AgentConnectionCount.vue";
import MentorCompetenceSummary from "@/components/dashboard/MentorCompetenceSummary.vue"; import MentorCompetenceSummary from "@/components/dashboard/MentorCompetenceSummary.vue";
import { getCockpitUrl, getLearningMentorUrl, getLearningPathUrl } from "@/utils/utils"; import { getCockpitUrl, getLearningMentorUrl, getLearningPathUrl } from "@/utils/utils";
import UkStatistics from "@/components/dashboard/UkStatistics.vue"; import UkStatistics from "@/components/dashboard/UkStatistics.vue";
import BerufsbildnerStatistics from "@/components/dashboard/BerufsbildnerStatistics.vue";
const mentorWidgets = [ const mentorWidgets = [
"MentorTasksWidget", "MentorTasksWidget",
@ -63,6 +64,13 @@ const actionButtonProps = computed<{ href: string; text: string; cyKey: string }
cyKey: "lm-dashboard-link", cyKey: "lm-dashboard-link",
}; };
} }
if (props.courseConfig?.role_key === "Berufsbildner") {
return {
href: getLearningPathUrl(props.courseConfig?.course_slug),
text: "a.Vorschau Teilnehmer",
cyKey: "progress-dashboard-continue-course-link",
};
}
return { return {
href: getLearningPathUrl(props.courseConfig?.course_slug), href: getLearningPathUrl(props.courseConfig?.course_slug),
text: "Weiter lernen", text: "Weiter lernen",
@ -77,7 +85,11 @@ function hasActionButton(): boolean {
</script> </script>
<template> <template>
<div v-if="courseConfig" class="mb-14 space-y-8"> <div
v-if="courseConfig"
class="mb-14 space-y-8"
:data-cy="`panel-${courseConfig.course_slug}`"
>
<div class="flex flex-col space-y-8 bg-white p-6"> <div class="flex flex-col space-y-8 bg-white p-6">
<div class="border-b border-gray-300 pb-8"> <div class="border-b border-gray-300 pb-8">
<div class="flex flex-row items-start justify-between"> <div class="flex flex-row items-start justify-between">
@ -102,12 +114,13 @@ function hasActionButton(): boolean {
target="_blank" target="_blank"
> >
<div class="flex items-center"> <div class="flex items-center">
<span>{{ $t("a.VorschauTeilnehmer") }}</span> <span>{{ $t("a.Vorschau Teilnehmer") }}</span>
<it-icon-external-link class="ml-1 !h-4 !w-4" /> <it-icon-external-link class="ml-1 !h-4 !w-4" />
</div> </div>
</router-link> </router-link>
</p> </p>
</div> </div>
<div <div
v-if=" v-if="
hasWidget('ProgressWidget') && hasWidget('ProgressWidget') &&
@ -123,6 +136,7 @@ function hasActionButton(): boolean {
diagram-type="horizontal" diagram-type="horizontal"
></LearningPathDiagram> ></LearningPathDiagram>
</div> </div>
<div <div
v-if="numberOfProgressWidgets" v-if="numberOfProgressWidgets"
class="flex flex-col flex-wrap gap-x-[60px] border-b border-gray-300 pb-8 last:border-0 md:flex-row" class="flex flex-col flex-wrap gap-x-[60px] border-b border-gray-300 pb-8 last:border-0 md:flex-row"
@ -140,17 +154,30 @@ function hasActionButton(): boolean {
:course-id="courseConfig.course_id" :course-id="courseConfig.course_id"
></CompetenceSummary> ></CompetenceSummary>
</div> </div>
<div <div
v-if="hasWidget('UKStatisticsWidget')" v-if="hasWidget('UKStatisticsWidget')"
class="flex flex-col flex-wrap gap-x-[60px] border-b border-gray-300 pb-8 last:border-0 md:flex-row" class="flex flex-col flex-wrap gap-x-[60px] border-b border-gray-300 pb-8 last:border-0 md:flex-row"
> >
<UkStatistics :course-slug="courseSlug" :course-id="courseConfig.course_id" /> <UkStatistics :course-slug="courseSlug" :course-id="courseConfig.course_id" />
</div> </div>
<div
v-if="hasWidget('UKBerufsbildnerStatisticsWidget')"
class="flex flex-col flex-wrap gap-x-[60px] border-b border-gray-300 pb-8 last:border-0 md:flex-row"
>
<BerufsbildnerStatistics
:course-slug="courseSlug"
:course-id="courseConfig.course_id"
:agent-role="courseConfig.role_key"
/>
</div>
<div <div
v-if="numberOfMentorWidgets > 0" v-if="numberOfMentorWidgets > 0"
class="flex flex-col flex-wrap items-stretch md:flex-row" class="flex flex-col flex-wrap items-stretch md:flex-row"
> >
<MentorMenteeCount <AgentConnectionCount
v-if="hasWidget('MentorPersonWidget')" v-if="hasWidget('MentorPersonWidget')"
:course-id="courseConfig.course_id" :course-id="courseConfig.course_id"
:course-slug="courseConfig?.course_slug" :course-slug="courseConfig?.course_slug"
@ -163,6 +190,7 @@ function hasActionButton(): boolean {
<MentorCompetenceSummary <MentorCompetenceSummary
v-if="hasWidget('MentorCompetenceWidget')" v-if="hasWidget('MentorCompetenceWidget')"
:course-id="courseConfig.course_id" :course-id="courseConfig.course_id"
:agent-role="courseConfig.role_key"
/> />
</div> </div>
</div> </div>

View File

@ -1,19 +1,28 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Ref } from "vue"; import { computed, onMounted, ref } from "vue";
import { onMounted, ref } from "vue"; import {
import { fetchMentorCompetenceSummary } from "@/services/dashboard"; type DashboardRoleKeyType,
import type { AssignmentsStatisticsType } from "@/gql/graphql"; fetchMentorCompetenceSummary,
} from "@/services/dashboard";
import type { BaseStatisticsType } from "@/gql/graphql";
import BaseBox from "@/components/dashboard/BaseBox.vue"; import BaseBox from "@/components/dashboard/BaseBox.vue";
const props = defineProps<{ const props = defineProps<{
courseId: string; courseId: string;
agentRole: DashboardRoleKeyType;
}>(); }>();
const summary: Ref<AssignmentsStatisticsType | null> = ref(null); const mentorAssignmentData = ref<BaseStatisticsType | null>(null);
const summary = computed(() => {
return mentorAssignmentData.value?.assignments ?? null;
});
onMounted(async () => { onMounted(async () => {
summary.value = await fetchMentorCompetenceSummary(props.courseId); mentorAssignmentData.value = await fetchMentorCompetenceSummary(
console.log(summary.value); props.courseId,
props.agentRole
);
}); });
</script> </script>

View File

@ -21,7 +21,7 @@ onMounted(async () => {
<div class="w-[325px]"> <div class="w-[325px]">
<BaseBox <BaseBox
:details-link="`/course/${props.courseSlug}/learning-mentor/tasks`" :details-link="`/course/${props.courseSlug}/learning-mentor/tasks`"
data-cy="dashboard.mentor.competenceSummary" data-cy="dashboard.mentor.openTasksCount"
> >
<template #title>{{ $t("Zu erledigen") }}</template> <template #title>{{ $t("Zu erledigen") }}</template>
<template #content> <template #content>

View File

@ -4,27 +4,53 @@ import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
import { useTranslation } from "i18next-vue"; import { useTranslation } from "i18next-vue";
import type { StatisticsCourseSessionPropertiesType } from "@/gql/graphql"; import type { StatisticsCourseSessionPropertiesType } from "@/gql/graphql";
import type { StatisticsFilterItem } from "@/types"; import type { StatisticsFilterItem } from "@/types";
import _ from "lodash";
const { t } = useTranslation(); const { t } = useTranslation();
const props = defineProps<{ const props = defineProps<{
items: StatisticsFilterItem[]; items: StatisticsFilterItem[];
courseSessionProperties: StatisticsCourseSessionPropertiesType; courseSessionProperties: StatisticsCourseSessionPropertiesType;
hideCircleFilter?: boolean;
}>(); }>();
defineExpose({ getFilteredItems }); defineExpose({ getFilteredItems });
const regionFilter = computed(() => {
const regions = _.uniq(
props.courseSessionProperties.sessions.map((session) => session.region)
);
const f = regions.map((region) => ({
name: `Region: ${region}`,
id: region,
}));
return [
{
name: `${t("Region")}: ${t("a.Alle")}`,
id: "_all",
},
...f,
];
});
const sessionFilter = computed(() => { const sessionFilter = computed(() => {
const f = props.courseSessionProperties.sessions.map((session) => ({ let values = props.courseSessionProperties.sessions.map((session) => ({
name: `${t("a.Durchfuehrung")}: ${session.name}`, name: session.name,
region: session.region,
id: session.id, id: session.id,
})); }));
return [{ name: t("a.AlleDurchführungen"), id: "_all" }, ...f];
// filter by selected region
if (regionFilterValue.value.id !== "_all") {
values = values.filter((cs) => cs.region === regionFilterValue.value.id);
}
return [{ name: t("a.AlleDurchführungen"), id: "_all" }, ...values];
}); });
const generationFilter = computed(() => { const generationFilter = computed(() => {
const f = props.courseSessionProperties.generations.map((generation) => ({ const f = props.courseSessionProperties.generations.map((generation) => ({
name: `${t("a.Generation")}: ${generation}`, name: generation,
id: generation, id: generation,
})); }));
return [{ name: t("a.AlleGenerationen"), id: "_all" }, ...f]; return [{ name: t("a.AlleGenerationen"), id: "_all" }, ...f];
@ -32,12 +58,13 @@ const generationFilter = computed(() => {
const circleFilter = computed(() => { const circleFilter = computed(() => {
const f = props.courseSessionProperties.circles.map((circle) => ({ const f = props.courseSessionProperties.circles.map((circle) => ({
name: `Circle: ${circle.name}`, name: circle.name,
id: circle.id, id: circle.id,
})); }));
return [{ name: t("a.AlleCircle"), id: "_all" }, ...f]; return [{ name: t("a.AlleCircle"), id: "_all" }, ...f];
}); });
const regionFilterValue = ref(regionFilter.value[0]);
const sessionFilterValue = ref(sessionFilter.value[0]); const sessionFilterValue = ref(sessionFilter.value[0]);
const generationFilterValue = ref(generationFilter.value[0]); const generationFilterValue = ref(generationFilter.value[0]);
const circleFilterValue = ref(circleFilter.value[0]); const circleFilterValue = ref(circleFilter.value[0]);
@ -48,12 +75,21 @@ watch(
sessionFilterValue.value = sessionFilter.value[0]; sessionFilterValue.value = sessionFilter.value[0];
generationFilterValue.value = generationFilter.value[0]; generationFilterValue.value = generationFilter.value[0];
circleFilterValue.value = circleFilter.value[0]; circleFilterValue.value = circleFilter.value[0];
regionFilterValue.value = regionFilter.value[0];
}, },
{ deep: true } { deep: true }
); );
watch(regionFilterValue, () => {
console.log("regionFilterValue", regionFilterValue.value);
sessionFilterValue.value = sessionFilter.value[0];
});
const filteredItems = computed(() => { const filteredItems = computed(() => {
return props.items.filter((item) => { return props.items.filter((item) => {
const regionMatch =
regionFilterValue.value.id === "_all" ||
item.region === regionFilterValue.value.id;
const sessionMatch = const sessionMatch =
sessionFilterValue.value.id === "_all" || sessionFilterValue.value.id === "_all" ||
item.course_session_id === sessionFilterValue.value.id; item.course_session_id === sessionFilterValue.value.id;
@ -64,7 +100,7 @@ const filteredItems = computed(() => {
circleFilterValue.value.id === "_all" || circleFilterValue.value.id === "_all" ||
item.circle_id === circleFilterValue.value.id; item.circle_id === circleFilterValue.value.id;
return sessionMatch && generationMatch && circleMatch; return regionMatch && sessionMatch && generationMatch && circleMatch;
}); });
}); });
@ -75,30 +111,45 @@ function getFilteredItems() {
<template> <template>
<div> <div>
<div class="flex flex-col space-x-2 lg:flex-row"> <div class="flex flex-col space-x-2 border-b lg:flex-row">
<ItDropdownSelect
v-if="regionFilter.length > 2"
v-model="regionFilterValue"
class="min-w-[12rem]"
:items="regionFilter"
borderless
></ItDropdownSelect>
<ItDropdownSelect <ItDropdownSelect
v-model="sessionFilterValue" v-model="sessionFilterValue"
class="min-w-[18rem]" class="min-w-[12rem]"
:items="sessionFilter" :items="sessionFilter"
borderless borderless
></ItDropdownSelect> ></ItDropdownSelect>
<ItDropdownSelect <ItDropdownSelect
v-model="generationFilterValue" v-model="generationFilterValue"
class="min-w-[18rem]" class="min-w-[12rem]"
:items="generationFilter" :items="generationFilter"
borderless borderless
></ItDropdownSelect> ></ItDropdownSelect>
<ItDropdownSelect <ItDropdownSelect
v-if="!props.hideCircleFilter"
v-model="circleFilterValue" v-model="circleFilterValue"
class="min-w-[18rem]" class="min-w-[12rem]"
:items="circleFilter" :items="circleFilter"
borderless borderless
></ItDropdownSelect> ></ItDropdownSelect>
</div> </div>
<div v-for="item in filteredItems" :key="item._id" class="px-5"> <slot name="header"></slot>
<div class="border-t border-gray-500 py-4"> <section>
<slot :item="item"></slot> <div
v-for="item in filteredItems"
:key="item._id"
class="mx-6 border-t border-gray-500 first:border-t-0"
>
<div class="py-4">
<slot :item="item"></slot>
</div>
</div> </div>
</div> </section>
</div> </div>
</template> </template>

View File

@ -57,7 +57,7 @@ const feebackSummary = computed(() => {
}); });
onMounted(async () => { onMounted(async () => {
statistics.value = await dashboardStore.loadStatisticsDatav2(props.courseId); statistics.value = await dashboardStore.loadStatisticsData(props.courseId);
}); });
</script> </script>

View File

@ -49,7 +49,12 @@ async function navigate(routeName: string) {
/> />
</div> </div>
<button type="button" class="mt-6 flex items-center" @click="emit('logout')"> <button
type="button"
class="mt-6 flex items-center"
data-cy="logout-button"
@click="emit('logout')"
>
<it-icon-logout class="inline-block" /> <it-icon-logout class="inline-block" />
<span class="ml-1">{{ $t("mainNavigation.logout") }}</span> <span class="ml-1">{{ $t("mainNavigation.logout") }}</span>
</button> </button>

View File

@ -18,7 +18,7 @@ const { t } = useTranslation();
class="relative flex h-16 w-full flex-col items-center justify-end space-x-8 lg:flex-row lg:items-stretch lg:justify-center" class="relative flex h-16 w-full flex-col items-center justify-end space-x-8 lg:flex-row lg:items-stretch lg:justify-center"
> >
<span class="flex items-center px-1 pt-1 font-bold text-black"> <span class="flex items-center px-1 pt-1 font-bold text-black">
{{ t("a.VorschauTeilnehmer") }} ({{ courseSession.title }}) {{ t("a.Vorschau Teilnehmer") }} ({{ courseSession.title }})
</span> </span>
<div class="flex space-x-8"> <div class="flex space-x-8">

View File

@ -104,7 +104,8 @@ const hasPreviewMenu = computed(() =>
const hasAppointmentsMenu = computed(() => const hasAppointmentsMenu = computed(() =>
Boolean( Boolean(
courseSessionsStore.currentCourseSession?.actions.includes("appointments") && courseSessionsStore.currentCourseSession?.actions.includes("appointments") &&
userStore.loggedIn userStore.loggedIn &&
inCourse()
) )
); );
@ -130,6 +131,10 @@ const mentorTabTitle = computed(() =>
? "a.Praxisbildner" ? "a.Praxisbildner"
: "a.Lernbegleitung" : "a.Lernbegleitung"
); );
const hasSessionTitle = computed(() => {
return courseSessionsStore.currentCourseSession?.title && inCourse();
});
</script> </script>
<template> <template>
@ -231,7 +236,7 @@ const mentorTabTitle = computed(() =>
class="nav-item" class="nav-item"
> >
<div class="flex items-center"> <div class="flex items-center">
<span>{{ t("a.VorschauTeilnehmer") }}</span> <span>{{ t("a.Vorschau Teilnehmer") }}</span>
<it-icon-external-link class="ml-2" /> <it-icon-external-link class="ml-2" />
</div> </div>
</router-link> </router-link>
@ -334,7 +339,7 @@ const mentorTabTitle = computed(() =>
</div> </div>
<div <div
v-if="selectedCourseSessionTitle" v-if="hasSessionTitle"
class="nav-item hidden items-center lg:inline-flex" class="nav-item hidden items-center lg:inline-flex"
> >
<div class="" data-cy="current-course-session-title"> <div class="" data-cy="current-course-session-title">
@ -343,7 +348,11 @@ const mentorTabTitle = computed(() =>
</div> </div>
<div class="nav-item"> <div class="nav-item">
<div v-if="userStore.loggedIn" class="flex items-center"> <div
v-if="userStore.loggedIn"
class="flex items-center"
data-cy="header-profile"
>
<Popover class="relative"> <Popover class="relative">
<PopoverButton @click="popoverClick($event)"> <PopoverButton @click="popoverClick($event)">
<div v-if="userStore.avatar_url"> <div v-if="userStore.avatar_url">

View File

@ -86,7 +86,7 @@ const mentorTabTitle = computed(() =>
data-cy="navigation-mobile-preview-link" data-cy="navigation-mobile-preview-link"
@click="clickLink(getLearningPathUrl(courseSession.course.slug))" @click="clickLink(getLearningPathUrl(courseSession.course.slug))"
> >
{{ $t("a.VorschauTeilnehmer") }} {{ $t("a.Vorschau Teilnehmer") }}
</button> </button>
</li> </li>
<li v-if="hasLearningPathMenu" class="mb-6"> <li v-if="hasLearningPathMenu" class="mb-6">

View File

@ -8,9 +8,9 @@ import LoadingSpinner from "@/components/ui/LoadingSpinner.vue";
const courseSession = useCurrentCourseSession(); const courseSession = useCurrentCourseSession();
const { isLoading, summary, fetchData } = useLearningMentees(courseSession.value.id); const { isLoading, summary, fetchData } = useLearningMentees(courseSession.value.id);
const removeMyMentee = async (menteeId: string) => { const removeMyMentee = async (relationId: string) => {
await useCSRFFetch( await useCSRFFetch(
`/api/mentor/${courseSession.value.id}/mentors/${summary.value?.mentor_id}/remove/${menteeId}` `/api/mentor/${courseSession.value.id}/mentors/${relationId}/delete`
).delete(); ).delete();
fetchData(); fetchData();
}; };
@ -28,25 +28,31 @@ const noMenteesText = computed(() =>
</div> </div>
<div v-else> <div v-else>
<h2 class="heading-2 py-6">{{ $t("a.Personen, die du begleitest") }}</h2> <h2 class="heading-2 py-6">{{ $t("a.Personen, die du begleitest") }}</h2>
<div v-if="(summary?.participants?.length ?? 0) > 0" class="bg-white px-4 py-2"> <div
v-if="(summary?.participant_relations.length ?? 0) > 0"
class="bg-white px-4 py-2"
>
<div <div
v-for="participant in summary?.participants ?? []" v-for="relation in summary?.participant_relations ?? []"
:key="participant.id" :key="relation.id"
data-cy="lm-my-mentee-list-item" data-cy="lm-my-mentee-list-item"
class="flex flex-col items-start justify-between gap-4 border-b py-2 last:border-b-0 md:flex-row md:items-center md:gap-16" class="flex flex-col items-start justify-between gap-4 border-b py-2 last:border-b-0 md:flex-row md:items-center md:gap-16"
> >
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<img <img
:alt="participant.last_name" :alt="relation.participant_user.last_name"
class="h-11 w-11 rounded-full" class="h-11 w-11 rounded-full"
:src="participant.avatar_url || '/static/avatars/myvbv-default-avatar.png'" :src="
relation.participant_user.avatar_url ||
'/static/avatars/myvbv-default-avatar.png'
"
/> />
<div> <div>
<div class="text-bold"> <div class="text-bold">
{{ participant.first_name }} {{ relation.participant_user.first_name }}
{{ participant.last_name }} {{ relation.participant_user.last_name }}
</div> </div>
{{ participant.email }} {{ relation.participant_user.email }}
</div> </div>
</div> </div>
<div class="space-x-5"> <div class="space-x-5">
@ -55,7 +61,7 @@ const noMenteesText = computed(() =>
:to="{ :to="{
name: 'profileLearningPath', name: 'profileLearningPath',
params: { params: {
userId: participant.id, userId: relation.participant_user.id,
courseSlug: courseSession.course.slug, courseSlug: courseSession.course.slug,
}, },
}" }"
@ -66,7 +72,7 @@ const noMenteesText = computed(() =>
<button <button
class="underline" class="underline"
data-cy="lm-my-mentee-remove" data-cy="lm-my-mentee-remove"
@click="removeMyMentee(participant.id)" @click="removeMyMentee(relation.id)"
> >
{{ $t("a.Entfernen") }} {{ $t("a.Entfernen") }}
</button> </button>

View File

@ -51,11 +51,9 @@ const removeInvitation = async (invitationId: string) => {
await refreshInvitations(); await refreshInvitations();
}; };
const userStore = useUserStore(); const removeMyMentor = async (relationId: string) => {
const removeMyMentor = async (mentorId: string) => {
await useCSRFFetch( await useCSRFFetch(
`/api/mentor/${courseSession.value.id}/mentors/${mentorId}/remove/${userStore.id}` `/api/mentor/${courseSession.value.id}/mentors/${relationId}/delete`
).delete(); ).delete();
await refreshMentors(); await refreshMentors();
}; };
@ -104,12 +102,13 @@ const noLearningMentors = computed(() =>
</div> </div>
</div> </div>
<div class="bg-white px-4 py-2"> <div class="bg-white px-4 py-2">
<main> <main data-cy="my-mentors-list">
<div> <div>
<div <div
v-for="invitation in invitations" v-for="invitation in invitations"
:key="invitation.id" :key="invitation.id"
class="flex flex-col justify-between gap-4 border-b py-2 last:border-b-0 md:flex-row md:gap-16" class="flex flex-col justify-between gap-4 border-b py-2 last:border-b-0 md:flex-row md:gap-16"
:data-cy="`mentor-${invitation.email}`"
> >
<div class="flex flex-col md:flex-grow md:flex-row"> <div class="flex flex-col md:flex-grow md:flex-row">
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
@ -137,16 +136,16 @@ const noLearningMentors = computed(() =>
> >
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<img <img
:alt="learningMentor.mentor.last_name" :alt="learningMentor.agent.last_name"
class="h-11 w-11 rounded-full" class="h-11 w-11 rounded-full"
:src="learningMentor.mentor.avatar_url" :src="learningMentor.agent.avatar_url"
/> />
<div> <div>
<div class="text-bold"> <div class="text-bold">
{{ learningMentor.mentor.first_name }} {{ learningMentor.agent.first_name }}
{{ learningMentor.mentor.last_name }} {{ learningMentor.agent.last_name }}
</div> </div>
{{ learningMentor.mentor.email }} {{ learningMentor.agent.email }}
</div> </div>
</div> </div>
<button <button
@ -178,6 +177,7 @@ const noLearningMentors = computed(() =>
<button <button
:disabled="!validEmail" :disabled="!validEmail"
class="btn-primary mt-8" class="btn-primary mt-8"
data-cy="invite-mentor-button"
@click="inviteMentor()" @click="inviteMentor()"
> >
{{ $t("a.Einladung abschicken") }} {{ $t("a.Einladung abschicken") }}

View File

@ -74,9 +74,9 @@ const onSubmit = async () => {
<option <option
v-for="learningMentor in learningMentors" v-for="learningMentor in learningMentors"
:key="learningMentor.id" :key="learningMentor.id"
:value="learningMentor.mentor.id" :value="learningMentor.agent.id"
> >
{{ learningMentor.mentor.first_name }} {{ learningMentor.mentor.last_name }} {{ learningMentor.agent.first_name }} {{ learningMentor.agent.last_name }}
</option> </option>
</select> </select>
</div> </div>

View File

@ -44,6 +44,7 @@ const onFailed = () => {
<button <button
class="inline-flex flex-1 items-center border-2 p-4 text-left" class="inline-flex flex-1 items-center border-2 p-4 text-left"
:class="currentEvaluation === 'SUCCESS' ? 'border-green-500' : 'border-gray-300'" :class="currentEvaluation === 'SUCCESS' ? 'border-green-500' : 'border-gray-300'"
data-cy="success"
@click="onPassed" @click="onPassed"
> >
<it-icon-smiley-happy class="mr-4 h-16 w-16"></it-icon-smiley-happy> <it-icon-smiley-happy class="mr-4 h-16 w-16"></it-icon-smiley-happy>
@ -54,6 +55,7 @@ const onFailed = () => {
<button <button
class="inline-flex flex-1 items-center border-2 p-4 text-left" class="inline-flex flex-1 items-center border-2 p-4 text-left"
:class="currentEvaluation === 'FAIL' ? 'border-orange-500' : 'border-gray-300'" :class="currentEvaluation === 'FAIL' ? 'border-orange-500' : 'border-gray-300'"
data-cy="fail"
@click="onFailed" @click="onFailed"
> >
<it-icon-smiley-thinking class="mr-4 h-16 w-16"></it-icon-smiley-thinking> <it-icon-smiley-thinking class="mr-4 h-16 w-16"></it-icon-smiley-thinking>

View File

@ -24,7 +24,12 @@ const emit = defineEmits(["release"]);
) )
}} }}
</div> </div>
<ItButton variant="primary" size="large" @click="emit('release')"> <ItButton
variant="primary"
size="large"
data-cy="feedback-release-button"
@click="emit('release')"
>
{{ $t("a.Fremdeinschätzung freigeben") }} {{ $t("a.Fremdeinschätzung freigeben") }}
</ItButton> </ItButton>
</template> </template>

View File

@ -23,7 +23,7 @@ export interface Props {
withDefaults(defineProps<Props>(), { withDefaults(defineProps<Props>(), {
label: undefined, label: undefined,
cyKey: "", cyKey: "default",
placeholder: "", placeholder: "",
}); });
const emit = defineEmits(["update:modelValue"]); const emit = defineEmits(["update:modelValue"]);

View File

@ -2,7 +2,6 @@ import { useCSRFFetch } from "@/fetchHelpers";
import type { CourseStatisticsType } from "@/gql/graphql"; import type { CourseStatisticsType } from "@/gql/graphql";
import { graphqlClient } from "@/graphql/client"; import { graphqlClient } from "@/graphql/client";
import { import {
COMPETENCE_NAVI_CERTIFICATE_FOR_USER_QUERY,
COMPETENCE_NAVI_CERTIFICATE_QUERY, COMPETENCE_NAVI_CERTIFICATE_QUERY,
COURSE_QUERY, COURSE_QUERY,
COURSE_SESSION_DETAIL_QUERY, COURSE_SESSION_DETAIL_QUERY,
@ -31,6 +30,7 @@ import { useDashboardStore } from "@/stores/dashboard";
import { useUserStore } from "@/stores/user"; import { useUserStore } from "@/stores/user";
import type { import type {
ActionCompetence, ActionCompetence,
AgentParticipantRelation,
CircleType, CircleType,
CompetenceCertificate, CompetenceCertificate,
Course, Course,
@ -40,7 +40,6 @@ import type {
CourseSessionDetail, CourseSessionDetail,
DashboardPersonsPageMode, DashboardPersonsPageMode,
LearningContentWithCompletion, LearningContentWithCompletion,
LearningMentor,
LearningPathType, LearningPathType,
LearningUnitPerformanceCriteria, LearningUnitPerformanceCriteria,
PerformanceCriteria, PerformanceCriteria,
@ -52,8 +51,7 @@ import orderBy from "lodash/orderBy";
import log from "loglevel"; import log from "loglevel";
import type { ComputedRef, Ref } from "vue"; import type { ComputedRef, Ref } from "vue";
import { computed, onMounted, ref, watchEffect } from "vue"; import { computed, onMounted, ref, watchEffect } from "vue";
import { useRouter, type RouteLocationRaw } from "vue-router"; import { type RouteLocationRaw, useRouter } from "vue-router";
import { getCertificates } from "./services/competence";
import { mergeCompetenceCertificates } from "./pages/competence/utils"; import { mergeCompetenceCertificates } from "./pages/competence/utils";
export function useCurrentCourseSession() { export function useCurrentCourseSession() {
@ -81,14 +79,14 @@ export function useCurrentCourseSession() {
return result; return result;
} }
export function useCourseSessionDetailQuery(courSessionId?: string) { export function useCourseSessionDetailQuery(courseSessionId?: string) {
if (!courSessionId) { if (!courseSessionId) {
courSessionId = useCurrentCourseSession().value.id; courseSessionId = useCurrentCourseSession().value.id;
} }
const queryResult = useQuery({ const queryResult = useQuery({
query: COURSE_SESSION_DETAIL_QUERY, query: COURSE_SESSION_DETAIL_QUERY,
variables: { variables: {
courseSessionId: courSessionId, courseSessionId: courseSessionId,
}, },
}); });
@ -132,8 +130,11 @@ export function useCourseSessionDetailQuery(courSessionId?: string) {
return findUser(userId); return findUser(userId);
} }
function filterMembers() { function filterMembers(userSelectionIds: string[] | null = null) {
return (courseSessionDetail.value?.users ?? []).filter((u) => { return (courseSessionDetail.value?.users ?? []).filter((u) => {
if (userSelectionIds && userSelectionIds.length > 0) {
return userSelectionIds.includes(u.user_id) && u.role === "MEMBER";
}
return u.role === "MEMBER"; return u.role === "MEMBER";
}); });
} }
@ -442,28 +443,6 @@ export function useCourseDataWithCompletion(
}; };
} }
export function useCourseStatistics() {
const dashboardStore = useDashboardStore();
const statistics = computed(() => {
return dashboardStore.currentDashBoardData as CourseStatisticsType;
});
const courseSessionName = (courseSessionId: string) => {
return statistics.value.course_session_properties.sessions.find(
(session) => session.id === courseSessionId
)?.name;
};
const circleMeta = (circleId: string) => {
return statistics.value.course_session_properties.circles.find(
(circle) => circle.id === circleId
);
};
return { courseSessionName, circleMeta };
}
export function useFileUpload() { export function useFileUpload() {
const error = ref(false); const error = ref(false);
const loading = ref(false); const loading = ref(false);
@ -492,7 +471,7 @@ export function useFileUpload() {
} }
export function useMyLearningMentors() { export function useMyLearningMentors() {
const learningMentors = ref<LearningMentor[]>([]); const learningMentors = ref<AgentParticipantRelation[]>([]);
const currentCourseSessionId = useCurrentCourseSession().value.id; const currentCourseSessionId = useCurrentCourseSession().value.id;
const loading = ref(false); const loading = ref(false);
@ -517,7 +496,7 @@ export function getVvRoleDisplay(role: DashboardPersonRoleType) {
switch (role) { switch (role) {
case "LEARNING_MENTOR": case "LEARNING_MENTOR":
return t("a.Lernbegleitung"); return t("a.Lernbegleitung");
case "LEARNING_MENTEE": case "PARTICIPANT":
return t("a.Teilnehmer"); return t("a.Teilnehmer");
case "EXPERT": case "EXPERT":
return t("a.Experte"); return t("a.Experte");
@ -534,7 +513,7 @@ export function getUkRoleDisplay(role: DashboardPersonRoleType) {
switch (role) { switch (role) {
case "LEARNING_MENTOR": case "LEARNING_MENTOR":
return t("a.Praxisbildner"); return t("a.Praxisbildner");
case "LEARNING_MENTEE": case "PARTICIPANT":
return t("a.Teilnehmer"); return t("a.Teilnehmer");
case "EXPERT": case "EXPERT":
return t("a.Trainer"); return t("a.Trainer");
@ -601,7 +580,7 @@ export function useDashboardPersonsDueDates(
return refDate >= dayjs().startOf("day"); return refDate >= dayjs().startOf("day");
}); });
// attach `LEARNING_MENTEE` to due dates for `LEARNING_MENTOR` persons // attach `PARTICIPANT` to due dates for `LEARNING_MENTOR` persons
currentDueDates.value.forEach((dueDate) => { currentDueDates.value.forEach((dueDate) => {
if (dueDate.course_session.my_role === "LEARNING_MENTOR") { if (dueDate.course_session.my_role === "LEARNING_MENTOR") {
dueDate.persons = dashboardPersons.value.filter((person) => { dueDate.persons = dashboardPersons.value.filter((person) => {
@ -611,7 +590,7 @@ export function useDashboardPersonsDueDates(
.includes(dueDate.course_session.id) .includes(dueDate.course_session.id)
) { ) {
return person.course_sessions.some( return person.course_sessions.some(
(cs) => cs.user_role === "LEARNING_MENTEE" (cs) => cs.user_role === "PARTICIPANT"
); );
} }
}); });
@ -697,29 +676,19 @@ export function useCourseStatisticsv2(courseSlug: string) {
} }
export function useCertificateQuery( export function useCertificateQuery(
userId: string | undefined, userIds: string[],
courseSlug: string, courseSlug: string,
courseSession: CourseSession courseSession: CourseSession
) { ) {
const certificatesQuery = (() => { const certificatesQuery = (() => {
if (userId) { return useQuery({
return useQuery({ query: COMPETENCE_NAVI_CERTIFICATE_QUERY,
query: COMPETENCE_NAVI_CERTIFICATE_FOR_USER_QUERY, variables: {
variables: { courseSlug: courseSlug,
courseSlug: courseSlug, courseSessionId: courseSession.id,
courseSessionId: courseSession.id, userIds,
userId: userId, },
}, });
});
} else {
return useQuery({
query: COMPETENCE_NAVI_CERTIFICATE_QUERY,
variables: {
courseSlug: courseSlug,
courseSessionId: courseSession.id,
},
});
}
})(); })();
return { certificatesQuery }; return { certificatesQuery };
@ -750,21 +719,20 @@ export function useVVByLink() {
return { href }; return { href };
} }
export function useAllCompetenceCertificates( export function useAllCompetenceCertificates(userId: string, courseSlug: string) {
userId: string | undefined,
courseSlug: string
) {
const courseSessionsStore = useCourseSessionsStore(); const courseSessionsStore = useCourseSessionsStore();
const certificateQueries = courseSessionsStore.allCourseSessions.map( const certificateQueries = courseSessionsStore.allCourseSessions.map(
(courseSession) => { (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) => {
return getCertificates(query.data.value, userId ?? null) return (
?.competence_certificates as unknown as CompetenceCertificate[]; (query.data.value?.competence_certificate_list
?.competence_certificates as unknown as CompetenceCertificate[]) ?? []
);
}) })
); );
const isLoaded = computed(() => !certificateQueries.some((q) => q.fetching.value)); const isLoaded = computed(() => !certificateQueries.some((q) => q.fetching.value));

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
type Query { type Query {
course_statistics(course_id: ID!): CourseStatisticsType course_statistics(course_id: ID!): CourseStatisticsType
mentor_course_statistics(course_id: ID!): BaseStatisticsType mentor_course_statistics(course_id: ID!, agent_role: String!): BaseStatisticsType
course_progress(course_id: ID!): CourseProgressType course_progress(course_id: ID!): CourseProgressType
dashboard_config: [DashboardConfigType!]! dashboard_config: [DashboardConfigType!]!
learning_path(id: ID, slug: String, course_id: ID, course_slug: String): LearningPathObjectType learning_path(id: ID, slug: String, course_id: ID, course_slug: String): LearningPathObjectType
@ -20,8 +20,7 @@ type Query {
learning_content_video: LearningContentVideoObjectType learning_content_video: LearningContentVideoObjectType
learning_content_document_list: LearningContentDocumentListObjectType learning_content_document_list: LearningContentDocumentListObjectType
competence_certificate(id: ID, slug: String): CompetenceCertificateObjectType competence_certificate(id: ID, slug: String): CompetenceCertificateObjectType
competence_certificate_list(id: ID, slug: String, course_id: ID, course_slug: String): CompetenceCertificateListObjectType competence_certificate_list(id: ID, slug: String, course_id: ID, course_slug: String, user_ids: [UUID]): CompetenceCertificateListObjectType
competence_certificate_list_for_user(id: ID, slug: String, course_id: ID, course_slug: String, user_id: UUID): CompetenceCertificateListObjectType
assignment(id: ID, slug: String): AssignmentObjectType assignment(id: ID, slug: String): AssignmentObjectType
assignment_completion(assignment_id: ID!, course_session_id: ID!, learning_content_page_id: ID, assignment_user_id: UUID): AssignmentCompletionObjectType assignment_completion(assignment_id: ID!, course_session_id: ID!, learning_content_page_id: ID, assignment_user_id: UUID): AssignmentCompletionObjectType
} }
@ -53,10 +52,15 @@ type AssignmentStatisticsRecordType {
course_session_assignment_id: ID! course_session_assignment_id: ID!
circle_id: ID! circle_id: ID!
generation: String! generation: String!
region: String!
assignment_type_translation_key: String! assignment_type_translation_key: String!
assignment_title: String! assignment_title: String!
deadline: DateTime! deadline: DateTime!
course_session_title: String
competence_certificate_id: ID
competence_certificate_title: String
metrics: AssignmentCompletionMetricsType! metrics: AssignmentCompletionMetricsType!
learning_content_id: ID!
details_url: String! details_url: String!
} }
@ -74,6 +78,8 @@ type AssignmentCompletionMetricsType {
unranked_count: Int! unranked_count: Int!
ranking_completed: Boolean! ranking_completed: Boolean!
average_passed: Float! average_passed: Float!
average_evaluation_percent: Float
competence_certificate_weight: Float
} }
type AssignmentStatisticsSummaryType { type AssignmentStatisticsSummaryType {
@ -82,6 +88,7 @@ type AssignmentStatisticsSummaryType {
average_passed: Float! average_passed: Float!
total_passed: Int! total_passed: Int!
total_failed: Int! total_failed: Int!
average_evaluation_percent: Float
} }
type StatisticsCourseSessionPropertiesType { type StatisticsCourseSessionPropertiesType {
@ -94,6 +101,7 @@ type StatisticsCourseSessionPropertiesType {
type StatisticsCourseSessionDataType { type StatisticsCourseSessionDataType {
id: ID! id: ID!
name: String! name: String!
region: String!
} }
type StatisticsCircleDataType { type StatisticsCircleDataType {
@ -118,6 +126,7 @@ type PresenceRecordStatisticsType {
_id: ID! _id: ID!
course_session_id: ID! course_session_id: ID!
generation: String! generation: String!
region: String!
circle_id: ID! circle_id: ID!
due_date: DateTime! due_date: DateTime!
participants_present: Int! participants_present: Int!
@ -141,6 +150,7 @@ type FeedbackStatisticsRecordType {
_id: ID! _id: ID!
course_session_id: ID! course_session_id: ID!
generation: String! generation: String!
region: String!
circle_id: ID! circle_id: ID!
satisfaction_average: Float! satisfaction_average: Float!
satisfaction_max: Int! satisfaction_max: Int!
@ -171,6 +181,7 @@ type CompetenceRecordStatisticsType {
_id: ID! _id: ID!
course_session_id: ID! course_session_id: ID!
generation: String! generation: String!
region: String!
title: String! title: String!
circle_id: ID! circle_id: ID!
success_count: Int! success_count: Int!
@ -186,6 +197,7 @@ type BaseStatisticsType {
course_session_selection_ids: [ID]! course_session_selection_ids: [ID]!
user_selection_ids: [ID] user_selection_ids: [ID]
assignments: AssignmentsStatisticsType! assignments: AssignmentsStatisticsType!
course_session_properties: StatisticsCourseSessionPropertiesType!
} }
type CourseProgressType { type CourseProgressType {
@ -268,7 +280,6 @@ type CourseObjectType {
action_competences: [ActionCompetenceObjectType!]! action_competences: [ActionCompetenceObjectType!]!
profiles: [String] profiles: [String]
course_session_users(id: String): [CourseSessionUserType]! course_session_users(id: String): [CourseSessionUserType]!
chosen_profile(user: String!): String
} }
type ActionCompetenceObjectType implements CoursePageInterface { type ActionCompetenceObjectType implements CoursePageInterface {
@ -503,7 +514,7 @@ type AssignmentObjectType implements CoursePageInterface {
max_points: Int max_points: Int
competence_certificate_weight: Float competence_certificate_weight: Float
learning_content: LearningContentInterface learning_content: LearningContentInterface
completion(course_session_id: ID!, learning_content_page_id: ID, assignment_user_id: UUID): AssignmentCompletionObjectType completions(course_session_id: ID!, learning_content_page_id: ID, assignment_user_ids: [UUID]): [AssignmentCompletionObjectType]
solution_sample: ContentDocumentObjectType solution_sample: ContentDocumentObjectType
} }
@ -565,6 +576,7 @@ type AssignmentCompletionObjectType {
evaluation_points: Float evaluation_points: Float
evaluation_points_final: Float evaluation_points_final: Float
evaluation_max_points: Float evaluation_max_points: Float
evaluation_percent: Float
} }
type UserObjectType { type UserObjectType {

View File

@ -90,8 +90,12 @@ export const ASSIGNMENT_COMPLETION_QUERY = graphql(`
`); `);
export const COMPETENCE_NAVI_CERTIFICATE_QUERY = graphql(` export const COMPETENCE_NAVI_CERTIFICATE_QUERY = graphql(`
query competenceCertificateQuery($courseSlug: String!, $courseSessionId: ID!) { query competenceCertificateQuery(
competence_certificate_list(course_slug: $courseSlug) { $courseSlug: String!
$courseSessionId: ID!
$userIds: [UUID!]
) {
competence_certificate_list(course_slug: $courseSlug, user_ids: $userIds) {
...CoursePageFields ...CoursePageFields
competence_certificates { competence_certificates {
...CoursePageFields ...CoursePageFields
@ -100,7 +104,7 @@ export const COMPETENCE_NAVI_CERTIFICATE_QUERY = graphql(`
assignment_type assignment_type
max_points max_points
competence_certificate_weight competence_certificate_weight
completion(course_session_id: $courseSessionId) { completions(course_session_id: $courseSessionId) {
id id
completion_status completion_status
submitted_at submitted_at
@ -132,9 +136,9 @@ export const COMPETENCE_NAVI_CERTIFICATE_FOR_USER_QUERY = graphql(`
query competenceCertificateForUserQuery( query competenceCertificateForUserQuery(
$courseSlug: String! $courseSlug: String!
$courseSessionId: ID! $courseSessionId: ID!
$userId: UUID! $userIds: [UUID!]!
) { ) {
competence_certificate_list_for_user(course_slug: $courseSlug, user_id: $userId) { competence_certificate_list(course_slug: $courseSlug, user_ids: $userIds) {
...CoursePageFields ...CoursePageFields
competence_certificates { competence_certificates {
...CoursePageFields ...CoursePageFields
@ -143,7 +147,7 @@ export const COMPETENCE_NAVI_CERTIFICATE_FOR_USER_QUERY = graphql(`
assignment_type assignment_type
max_points max_points
competence_certificate_weight competence_certificate_weight
completion(course_session_id: $courseSessionId) { completions(course_session_id: $courseSessionId) {
id id
completion_status completion_status
submitted_at submitted_at
@ -152,6 +156,10 @@ export const COMPETENCE_NAVI_CERTIFICATE_FOR_USER_QUERY = graphql(`
evaluation_points_deducted evaluation_points_deducted
evaluation_max_points evaluation_max_points
evaluation_passed evaluation_passed
evaluation_percent
assignment_user {
id
}
course_session { course_session {
id id
title title
@ -422,6 +430,7 @@ export const DASHBOARD_COURSE_STATISTICS = graphql(`
sessions { sessions {
id id
name name
region
} }
generations generations
circles { circles {
@ -442,6 +451,7 @@ export const DASHBOARD_COURSE_STATISTICS = graphql(`
_id _id
course_session_id course_session_id
generation generation
region
circle_id circle_id
due_date due_date
participants_present participants_present
@ -460,6 +470,7 @@ export const DASHBOARD_COURSE_STATISTICS = graphql(`
_id _id
course_session_id course_session_id
generation generation
region
circle_id circle_id
experts experts
satisfaction_average satisfaction_average
@ -488,8 +499,11 @@ export const DASHBOARD_COURSE_STATISTICS = graphql(`
course_session_assignment_id course_session_assignment_id
circle_id circle_id
generation generation
region
assignment_title assignment_title
assignment_type_translation_key assignment_type_translation_key
competence_certificate_title
competence_certificate_id
details_url details_url
deadline deadline
metrics { metrics {
@ -498,7 +512,9 @@ export const DASHBOARD_COURSE_STATISTICS = graphql(`
failed_count failed_count
unranked_count unranked_count
ranking_completed ranking_completed
average_evaluation_percent
average_passed average_passed
competence_certificate_weight
} }
} }
} }
@ -513,6 +529,7 @@ export const DASHBOARD_COURSE_STATISTICS = graphql(`
_id _id
course_session_id course_session_id
generation generation
region
circle_id circle_id
title title
success_count success_count
@ -525,14 +542,27 @@ export const DASHBOARD_COURSE_STATISTICS = graphql(`
`); `);
export const DASHBOARD_MENTOR_COMPETENCE_SUMMARY = graphql(` export const DASHBOARD_MENTOR_COMPETENCE_SUMMARY = graphql(`
query mentorCourseStatistics($courseId: ID!) { query mentorCourseStatistics($courseId: ID!, $agentRole: String!) {
mentor_course_statistics(course_id: $courseId) { mentor_course_statistics(course_id: $courseId, agent_role: $agentRole) {
_id _id
course_id course_id
course_title course_title
course_slug course_slug
course_session_selection_ids course_session_selection_ids
user_selection_ids user_selection_ids
course_session_properties {
_id
sessions {
id
name
region
}
generations
circles {
id
name
}
}
assignments { assignments {
_id _id
summary { summary {
@ -541,16 +571,22 @@ export const DASHBOARD_MENTOR_COMPETENCE_SUMMARY = graphql(`
average_passed average_passed
total_passed total_passed
total_failed total_failed
average_evaluation_percent
} }
records { records {
_id _id
course_session_id course_session_id
course_session_assignment_id course_session_assignment_id
course_session_title
circle_id circle_id
generation generation
region
assignment_title assignment_title
assignment_type_translation_key assignment_type_translation_key
competence_certificate_id
competence_certificate_title
details_url details_url
learning_content_id
deadline deadline
metrics { metrics {
_id _id
@ -558,6 +594,8 @@ export const DASHBOARD_MENTOR_COMPETENCE_SUMMARY = graphql(`
failed_count failed_count
unranked_count unranked_count
ranking_completed ranking_completed
competence_certificate_weight
average_evaluation_percent
average_passed average_passed
} }
} }

View File

@ -19,10 +19,14 @@ import { useTranslation } from "i18next-vue";
const { t } = useTranslation(); const { t } = useTranslation();
const props = defineProps<{ export interface Props {
courseSession: CourseSession; courseSession: CourseSession;
learningContent: LearningContentAssignment | LearningContentEdoniqTest; learningContent: LearningContentAssignment | LearningContentEdoniqTest;
}>(); userSelectionIds?: string[];
linkToResults?: boolean;
}
const props = defineProps<Props>();
log.debug("AssignmentDetails created", stringifyParse(props)); log.debug("AssignmentDetails created", stringifyParse(props));
@ -45,11 +49,13 @@ const isPraxisAssignment = computed(() => {
}); });
onMounted(async () => { onMounted(async () => {
log.debug("AssignmentDetails mounted", props.learningContent, props.userSelectionIds);
const { gradedUsers, assignmentSubmittedUsers } = const { gradedUsers, assignmentSubmittedUsers } =
await loadAssignmentCompletionStatusData( await loadAssignmentCompletionStatusData(
props.learningContent.content_assignment.id, props.learningContent.content_assignment.id,
props.courseSession.id, props.courseSession.id,
props.learningContent.id props.learningContent.id,
props.userSelectionIds ?? []
); );
state.gradedUsers = gradedUsers; state.gradedUsers = gradedUsers;
state.assignmentSubmittedUsers = assignmentSubmittedUsers; state.assignmentSubmittedUsers = assignmentSubmittedUsers;
@ -80,6 +86,17 @@ function findUserPointsHtml(userId: string) {
} }
return result; return result;
} }
function generateCertificatesLink(userId: string) {
const parts = props.learningContent.competence_certificate?.frontend_url?.split("/");
if (!parts) {
return "";
}
const certificatePart = parts[parts.length - 1];
return `/course/${props.courseSession.course.slug}/profile/${userId}/competence/certificates/${certificatePart}`;
}
</script> </script>
<template> <template>
@ -111,13 +128,14 @@ function findUserPointsHtml(userId: string) {
:course-session="courseSession" :course-session="courseSession"
:learning-content="learningContent" :learning-content="learningContent"
:show-title="false" :show-title="false"
:user-selection-ids="props.userSelectionIds"
/> />
</div> </div>
<div v-if="courseSessionDetailResult.filterMembers().length" class="mt-6"> <div v-if="courseSessionDetailResult.filterMembers().length" class="mt-6">
<ul> <ul>
<ItPersonRow <ItPersonRow
v-for="csu in courseSessionDetailResult.filterMembers()" v-for="csu in courseSessionDetailResult.filterMembers(props.userSelectionIds)"
:key="csu.user_id" :key="csu.user_id"
:name="`${csu.first_name} ${csu.last_name}`" :name="`${csu.first_name} ${csu.last_name}`"
:avatar-url="csu.avatar_url" :avatar-url="csu.avatar_url"
@ -172,11 +190,19 @@ function findUserPointsHtml(userId: string) {
props.learningContent.content_type !== props.learningContent.content_type !==
'learnpath.LearningContentEdoniqTest' 'learnpath.LearningContentEdoniqTest'
" "
:to="`/course/${props.courseSession.course.slug}/assignment-evaluation/${learningContent.content_assignment.id}/${csu.user_id}`" :to="
props.linkToResults
? `/course/${props.courseSession.course.slug}/assignment-evaluation/${learningContent.content_assignment.id}/${csu.user_id}`
: generateCertificatesLink(csu.user_id)
"
class="link lg:w-full lg:text-right" class="link lg:w-full lg:text-right"
data-cy="show-results" data-cy="show-results"
> >
{{ $t("a.Ergebnisse anschauen") }} {{
props.linkToResults
? $t("a.Ergebnisse anschauen")
: $t("a.Profil anzeigen")
}}
</router-link> </router-link>
</div> </div>
</template> </template>

View File

@ -5,6 +5,7 @@ import * as log from "loglevel";
import { computed, onMounted } from "vue"; import { computed, onMounted } from "vue";
import type { LearningContentAssignment, LearningContentEdoniqTest } from "@/types"; import type { LearningContentAssignment, LearningContentEdoniqTest } from "@/types";
import { useExpertCockpitPageData } from "@/pages/cockpit/cockpitPage/composables"; import { useExpertCockpitPageData } from "@/pages/cockpit/cockpitPage/composables";
import { getPreviousRoute } from "@/router/history";
const props = defineProps<{ const props = defineProps<{
courseSlug: string; courseSlug: string;
@ -25,16 +26,22 @@ const lpQueryResult = useCourseData(props.courseSlug);
const learningContentAssignment = computed(() => { const learningContentAssignment = computed(() => {
return lpQueryResult.findLearningContent(props.assignmentId); return lpQueryResult.findLearningContent(props.assignmentId);
}); });
const previousRoute = getPreviousRoute();
const backRoute = computed(() => {
if (previousRoute?.path.endsWith("/assignment")) {
return previousRoute;
}
return `/course/${props.courseSlug}/cockpit`;
});
</script> </script>
<template> <template>
<div v-if="!loading" class="bg-gray-200"> <div v-if="!loading" class="bg-gray-200">
<div class="container-large"> <div class="container-large">
<nav class="py-4 pb-4"> <nav class="py-4 pb-4">
<router-link <router-link class="btn-text inline-flex items-center pl-0" :to="backRoute">
class="btn-text inline-flex items-center pl-0"
:to="`/course/${props.courseSlug}/cockpit`"
>
<it-icon-arrow-left /> <it-icon-arrow-left />
<span>{{ $t("general.back") }}</span> <span>{{ $t("general.back") }}</span>
</router-link> </router-link>
@ -46,6 +53,7 @@ const learningContentAssignment = computed(() => {
v-if="learningContentAssignment" v-if="learningContentAssignment"
:course-session="courseSession" :course-session="courseSession"
:learning-content="learningContentAssignment as (LearningContentAssignment | LearningContentEdoniqTest)" :learning-content="learningContentAssignment as (LearningContentAssignment | LearningContentEdoniqTest)"
:link-to-results="true"
/> />
</div> </div>
</main> </main>

View File

@ -5,7 +5,10 @@ import {
import { useExpertCockpitStore } from "@/stores/expertCockpit"; import { useExpertCockpitStore } from "@/stores/expertCockpit";
import { ref } from "vue"; import { ref } from "vue";
export function useExpertCockpitPageData(courseSlug: string) { export function useExpertCockpitPageData(
courseSlug: string,
userSelectionIds: string[] | null = null
) {
const loading = ref(true); const loading = ref(true);
const cockpitStore = useExpertCockpitStore(); const cockpitStore = useExpertCockpitStore();
@ -19,10 +22,12 @@ export function useExpertCockpitPageData(courseSlug: string) {
courseSessionDetailResult.findCurrentUser() courseSessionDetailResult.findCurrentUser()
); );
const userDataPromises = courseSessionDetailResult.filterMembers().map((m) => { const userDataPromises = courseSessionDetailResult
const completionData = useCourseDataWithCompletion(courseSlug, m.id); .filterMembers(userSelectionIds)
return completionData.resultPromise; .map((m) => {
}); const completionData = useCourseDataWithCompletion(courseSlug, m.user_id);
return completionData.resultPromise;
});
await Promise.all(userDataPromises); await Promise.all(userDataPromises);
loading.value = false; loading.value = false;

View File

@ -28,8 +28,8 @@ const getIconName = () => {
}; };
const openInCircle = (assignment: CompetenceCertificateAssignment) => { const openInCircle = (assignment: CompetenceCertificateAssignment) => {
if (assignment.completion?.course_session !== currentCourseSession.value) { if (assignment.completions[0]?.course_session !== currentCourseSession.value) {
switchCourseSessionById(assignment.completion!.course_session.id); switchCourseSessionById(assignment.completions[0]!.course_session.id);
} }
router.push(assignment.frontend_url); router.push(assignment.frontend_url);
}; };
@ -48,7 +48,7 @@ const openInCircle = (assignment: CompetenceCertificateAssignment) => {
v-if="showCourseSession" v-if="showCourseSession"
:data-cy="`assignment-${assignment.slug}-course-session`" :data-cy="`assignment-${assignment.slug}-course-session`"
> >
{{ assignment?.completion?.course_session.title }} {{ assignment?.completions?.[0]?.course_session.title }}
</p> </p>
<p class="text-gray-800"> <p class="text-gray-800">
<button <button
@ -69,7 +69,7 @@ const openInCircle = (assignment: CompetenceCertificateAssignment) => {
</div> </div>
<div class="grow lg:px-8"> <div class="grow lg:px-8">
<div <div
v-if="assignment.completion?.completion_status === 'EVALUATION_SUBMITTED'" v-if="assignment.completions?.[0]?.completion_status === 'EVALUATION_SUBMITTED'"
class="flex items-center" class="flex items-center"
> >
<div <div
@ -82,7 +82,7 @@ const openInCircle = (assignment: CompetenceCertificateAssignment) => {
<div <div
v-else-if=" v-else-if="
['EVALUATION_IN_PROGRESS', 'SUBMITTED'].includes( ['EVALUATION_IN_PROGRESS', 'SUBMITTED'].includes(
assignment.completion?.completion_status || '' assignment.completions?.[0]?.completion_status || ''
) )
" "
class="flex items-center" class="flex items-center"
@ -97,31 +97,33 @@ const openInCircle = (assignment: CompetenceCertificateAssignment) => {
</div> </div>
<div> <div>
<div <div
v-if="assignment.completion?.completion_status === 'EVALUATION_SUBMITTED'" v-if="assignment.completions?.[0]?.completion_status === 'EVALUATION_SUBMITTED'"
class="flex flex-col lg:items-center" class="flex flex-col lg:items-center"
> >
<div class="flex flex-col lg:items-center"> <div class="flex flex-col lg:items-center">
<div class="heading-2"> <div class="heading-2">
{{ assignment.completion?.evaluation_points_final }} {{ assignment.completions[0]?.evaluation_points_final }}
</div> </div>
<div> <div>
{{ $t("assignment.von x Punkten", { x: assignment.max_points }) }} {{ $t("assignment.von x Punkten", { x: assignment.max_points }) }}
({{ ({{
( (
((assignment.completion?.evaluation_points_final ?? 0) / ((assignment.completions[0]?.evaluation_points_final ?? 0) /
(assignment.completion?.evaluation_max_points ?? 1)) * (assignment.completions[0]?.evaluation_max_points ?? 1)) *
100 100
).toFixed(0) ).toFixed(0)
}}%) }}%)
</div> </div>
<div <div
v-if="(assignment.completion?.evaluation_points_deducted ?? 0) > 0" v-if="(assignment.completions[0]?.evaluation_points_deducted ?? 0) > 0"
class="text-gray-900" class="text-gray-900"
> >
{{ $t("a.mit Abzug") }} {{ $t("a.mit Abzug") }}
</div> </div>
<div <div
v-if="assignment.completion && !assignment.completion.evaluation_passed" v-if="
assignment.completions[0] && !assignment.completions[0].evaluation_passed
"
class="my-2 rounded-md bg-error-red-200 px-2.5 py-0.5" class="my-2 rounded-md bg-error-red-200 px-2.5 py-0.5"
> >
{{ $t("a.Nicht Bestanden") }} {{ $t("a.Nicht Bestanden") }}

View File

@ -33,7 +33,7 @@ const userGradeRounded2Places = computed(() => {
const numAssignmentsEvaluated = computed(() => { const numAssignmentsEvaluated = computed(() => {
return props.competenceCertificate.assignments.filter((a) => { return props.competenceCertificate.assignments.filter((a) => {
return a.completion?.completion_status === "EVALUATION_SUBMITTED"; return a?.completions?.[0]?.completion_status === "EVALUATION_SUBMITTED";
}).length; }).length;
}); });
@ -57,7 +57,8 @@ const showCourseSession = computed(() => {
const currentCourseSession = useCurrentCourseSession(); const currentCourseSession = useCurrentCourseSession();
return props.competenceCertificate.assignments.some((assignment) => { return props.competenceCertificate.assignments.some((assignment) => {
return ( return (
assignment.completion?.course_session.title !== currentCourseSession.value.title assignment.completions?.[0]?.course_session.title !==
currentCourseSession.value.title
); );
}); });
}); });

View File

@ -2,8 +2,10 @@
import log from "loglevel"; import log from "loglevel";
import { computed } from "vue"; import { computed } from "vue";
import { useAllCompetenceCertificates } from "@/composables"; import { useAllCompetenceCertificates } from "@/composables";
import CompetenceCertificateComponent from "@/pages/competence/CompetenceCertificateComponent.vue";
import { getPreviousRoute } from "@/router/history"; import { getPreviousRoute } from "@/router/history";
import CompetenceCertificateComponent from "@/pages/competence/CompetenceCertificateComponent.vue";
import { useUserStore } from "@/stores/user";
const props = defineProps<{ const props = defineProps<{
courseSlug: string; courseSlug: string;
certificateSlug: string; certificateSlug: string;
@ -12,8 +14,9 @@ const props = defineProps<{
log.debug("CompetenceCertificateDetailPage setup", props); log.debug("CompetenceCertificateDetailPage setup", props);
const { id: currentUserId } = useUserStore();
const { competenceCertificates } = useAllCompetenceCertificates( const { competenceCertificates } = useAllCompetenceCertificates(
props.userId, props.userId ?? currentUserId,
props.courseSlug props.courseSlug
); );

View File

@ -8,6 +8,8 @@ import {
calcCompetencesTotalGrade, calcCompetencesTotalGrade,
} from "@/pages/competence/utils"; } from "@/pages/competence/utils";
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
import { useUserStore } from "@/stores/user";
const props = defineProps<{ const props = defineProps<{
courseSlug: string; courseSlug: string;
userId?: string; userId?: string;
@ -16,9 +18,9 @@ const props = defineProps<{
log.debug("CompetenceCertificateListPage setup", props); log.debug("CompetenceCertificateListPage setup", props);
const route = useRoute(); const route = useRoute();
const { id: currentUserId } = useUserStore();
const { competenceCertificates } = useAllCompetenceCertificates( const { competenceCertificates } = useAllCompetenceCertificates(
props.userId, props.userId ?? currentUserId,
props.courseSlug props.courseSlug
); );
@ -36,7 +38,7 @@ const userPointsEvaluatedAssignments = computed(() => {
const numAssignmentsEvaluated = computed(() => { const numAssignmentsEvaluated = computed(() => {
return (assignments.value ?? []).filter((a) => { return (assignments.value ?? []).filter((a) => {
return a.completion?.completion_status === "EVALUATION_SUBMITTED"; return a.completions?.[0]?.completion_status === "EVALUATION_SUBMITTED";
}).length; }).length;
}); });

View File

@ -19,8 +19,10 @@ const props = defineProps<{
log.debug("CompetenceIndexPage setup", props); log.debug("CompetenceIndexPage setup", props);
const user = useUserStore();
const { competenceCertificates, isLoaded } = useAllCompetenceCertificates( const { competenceCertificates, isLoaded } = useAllCompetenceCertificates(
undefined, user.id,
props.courseSlug props.courseSlug
); );
@ -88,7 +90,8 @@ const router = useRouter();
{{ {{
$t("assignment.x von y Kompetenznachweis-Elementen abgeschlossen", { $t("assignment.x von y Kompetenznachweis-Elementen abgeschlossen", {
x: certificate.assignments.filter( x: certificate.assignments.filter(
(a) => a.completion?.completion_status === "EVALUATION_SUBMITTED" (a) =>
a.completions[0]?.completion_status === "EVALUATION_SUBMITTED"
).length, ).length,
y: certificate.assignments.length, y: certificate.assignments.length,
}) })

View File

@ -9,7 +9,7 @@ export function assignmentsMaxEvaluationPoints(
): number { ): number {
return _.sum( return _.sum(
assignments assignments
.filter((a) => a.completion?.completion_status === "EVALUATION_SUBMITTED") .filter((a) => a.completions[0]?.completion_status === "EVALUATION_SUBMITTED")
.map((a) => a.max_points) .map((a) => a.max_points)
); );
} }
@ -17,8 +17,8 @@ export function assignmentsMaxEvaluationPoints(
export function assignmentsUserPoints(assignments: CompetenceCertificateAssignment[]) { export function assignmentsUserPoints(assignments: CompetenceCertificateAssignment[]) {
return +_.sum( return +_.sum(
assignments assignments
.filter((a) => a.completion?.completion_status === "EVALUATION_SUBMITTED") .filter((a) => a.completions?.[0]?.completion_status === "EVALUATION_SUBMITTED")
.map((a) => a.completion?.evaluation_points_final ?? 0) .map((a) => a.completions?.[0]?.evaluation_points_final ?? 0)
).toFixed(1); ).toFixed(1);
} }
@ -27,12 +27,12 @@ export function calcCompetenceCertificateGrade(
roundedToHalfGrade = true roundedToHalfGrade = true
) { ) {
const evaluatedAssignments = assignments.filter( const evaluatedAssignments = assignments.filter(
(a) => a.completion?.completion_status === "EVALUATION_SUBMITTED" (a) => a.completions?.[0]?.completion_status === "EVALUATION_SUBMITTED"
); );
const adjustedResults = evaluatedAssignments.map((a) => { const adjustedResults = evaluatedAssignments.map((a) => {
return ( return (
((a.completion?.evaluation_points_final ?? 0) / a.max_points) * ((a.completions?.[0]?.evaluation_points_final ?? 0) / a.max_points) *
a.competence_certificate_weight a.competence_certificate_weight
); );
}); });
@ -77,7 +77,7 @@ export function competenceCertificateProgressStatusCount(
assignments: CompetenceCertificateAssignment[] assignments: CompetenceCertificateAssignment[]
) { ) {
const numAssignmentsEvaluated = assignments.filter( const numAssignmentsEvaluated = assignments.filter(
(a) => a.completion?.completion_status === "EVALUATION_SUBMITTED" (a) => a.completions?.[0]?.completion_status === "EVALUATION_SUBMITTED"
).length; ).length;
return { return {
SUCCESS: numAssignmentsEvaluated, SUCCESS: numAssignmentsEvaluated,
@ -120,10 +120,10 @@ export function mergeCompetenceCertificates(
if (!existingAssignment) { if (!existingAssignment) {
mergedCertificate.assignments.push(assignment); mergedCertificate.assignments.push(assignment);
} else if ( } else if (
assignment.completion != null && assignment.completions?.[0] != null &&
(existingAssignment.completion == null || (existingAssignment.completions?.[0] == null ||
dayjs(existingAssignment.completion.evaluation_submitted_at).isBefore( dayjs(existingAssignment.completions[0].evaluation_submitted_at).isBefore(
assignment.completion.evaluation_submitted_at assignment.completions[0].evaluation_submitted_at
)) ))
) { ) {
mergedCertificate.assignments.splice( mergedCertificate.assignments.splice(

View File

@ -254,6 +254,7 @@ function exportData() {
course_session_id: csId, course_session_id: csId,
generation: "", generation: "",
circle_id: "", circle_id: "",
region: "",
}); });
} }
exportDataAsXls(items, exportPersons, userStore.language); exportDataAsXls(items, exportPersons, userStore.language);
@ -407,8 +408,8 @@ watch(selectedRegion, () => {
v-if=" v-if="
(['SUPERVISOR', 'EXPERT'].includes(cs.my_role) && (['SUPERVISOR', 'EXPERT'].includes(cs.my_role) &&
cs.user_role === 'MEMBER') || cs.user_role === 'MEMBER') ||
(cs.my_role === 'LEARNING_MENTOR' && (['LEARNING_MENTOR', 'BERUFSBILDNER'].includes(cs.my_role) &&
cs.user_role === 'LEARNING_MENTEE') cs.user_role === 'PARTICIPANT')
" "
> >
<router-link <router-link

View File

@ -0,0 +1,64 @@
<script setup lang="ts">
import { useCourseData, useCurrentCourseSession } from "@/composables";
import AssignmentDetails from "@/pages/cockpit/assignmentsPage/AssignmentDetails.vue";
import * as log from "loglevel";
import { computed } from "vue";
import type { LearningContentAssignment, LearningContentEdoniqTest } from "@/types";
import { useExpertCockpitPageData } from "@/pages/cockpit/cockpitPage/composables";
const props = defineProps<{
courseSlug: string;
assignmentId: string;
agentRole: string;
participantUserIds: string[];
}>();
log.debug(
"AgentAssignmentDetail created",
props.courseSlug,
props.agentRole,
props.participantUserIds
);
const courseSession = useCurrentCourseSession();
const { loading } = useExpertCockpitPageData(
props.courseSlug,
props.participantUserIds
);
const lpQueryResult = useCourseData(props.courseSlug);
const learningContentAssignment = computed(() => {
return lpQueryResult.findLearningContent(props.assignmentId);
});
</script>
<template>
<div v-if="!loading" class="bg-gray-200">
<div class="container-large">
<nav class="py-4 pb-4">
<router-link
class="btn-text inline-flex items-center pl-0"
:to="`/statistic/${props.agentRole}/${props.courseSlug}/assignment`"
>
<it-icon-arrow-left />
<span>{{ $t("general.back") }}</span>
</router-link>
</nav>
<main>
<div class="bg-white p-6">
<!-- prettier-ignore -->
<AssignmentDetails
v-if="learningContentAssignment"
:course-session="courseSession"
:learning-content="learningContentAssignment as (LearningContentAssignment | LearningContentEdoniqTest)"
:user-selection-ids="participantUserIds"
:link-to-results="props.agentRole.toLowerCase() !== 'berufsbildner'"
/>
</div>
</main>
</div>
</div>
</template>
<style scoped></style>

View File

@ -0,0 +1,45 @@
<script setup lang="ts">
import { useCurrentCourseSession } from "@/composables";
import * as log from "loglevel";
import { onMounted, ref } from "vue";
import { fetchDashboardPersons } from "@/services/dashboard";
import AgentAssignmentDetail from "@/pages/dashboard/agentAssignment/AgentAssignmentDetail.vue";
const props = defineProps<{
courseSlug: string;
assignmentId: string;
agentRole: string;
}>();
log.debug("AgentAssignmentDetailPage created", props.courseSlug, props.agentRole);
const courseSession = useCurrentCourseSession();
const loading = ref(true);
const participantUserIds = ref<string[]>([]);
onMounted(async () => {
log.debug("AgentAssignmentDetailPage mounted", courseSession);
const personData = await fetchDashboardPersons("default");
const participants = personData?.filter((p) => {
return p.course_sessions.find(
(cs) => cs.id === courseSession.value.id && cs.my_role === "BERUFSBILDNER"
);
});
participantUserIds.value = participants?.map((p) => p.user_id);
loading.value = false;
});
</script>
<template>
<AgentAssignmentDetail
v-if="participantUserIds.length"
:assignment-id="props.assignmentId"
:agent-role="props.agentRole"
:course-slug="props.courseSlug"
:participant-user-ids="participantUserIds"
/>
</template>
<style scoped></style>

View File

@ -0,0 +1,81 @@
<script setup lang="ts">
import log from "loglevel";
import { onMounted, ref } from "vue";
import {
courseIdForCourseSlug,
fetchMentorCompetenceSummary,
} from "@/services/dashboard";
import type { BaseStatisticsType } from "@/gql/graphql";
import { useDashboardStore } from "@/stores/dashboard";
import LoadingSpinner from "@/components/ui/LoadingSpinner.vue";
import AssignmentList from "@/pages/dashboard/statistic/AssignmentList.vue";
const dashboardStore = useDashboardStore();
const props = defineProps<{
agentRole: string;
courseSlug: string;
}>();
log.debug("AgentStatisticParentPage created", props);
const loading = ref(true);
const courseId = ref<string | undefined>(undefined);
const agentAssignmentData = ref<BaseStatisticsType | null>(null);
const courseSessionName = (courseSessionId: string) => {
return (
agentAssignmentData.value?.course_session_properties?.sessions.find(
(session) => session.id === courseSessionId
)?.name ?? ""
);
};
const circleMeta = (circleId: string) => {
return agentAssignmentData.value?.course_session_properties.circles.find(
(circle) => circle.id === circleId
);
};
onMounted(async () => {
await dashboardStore.loadDashboardDetails();
courseId.value = courseIdForCourseSlug(
dashboardStore.dashboardConfigsv2,
props.courseSlug
);
if (!courseId.value) {
log.error("CourseId not found for courseSlug", props.courseSlug);
return;
}
log.debug("courseId", courseId.value);
agentAssignmentData.value = await fetchMentorCompetenceSummary(
courseId.value,
props.agentRole
);
loading.value = false;
});
</script>
<template>
<div class="bg-gray-200">
<div v-if="loading" class="m-8 flex justify-center">
<LoadingSpinner />
</div>
<div v-else class="container-large flex flex-col space-y-8">
<router-link class="btn-text inline-flex items-center pl-0" to="/">
<it-icon-arrow-left />
<span>{{ $t("general.back") }}</span>
</router-link>
<AssignmentList
v-if="agentAssignmentData"
:course-statistics="agentAssignmentData"
:course-session-name="courseSessionName"
:circle-meta="circleMeta as any"
:detail-base-url="`/statistic/${props.agentRole}/${props.courseSlug}/assignment/`"
></AssignmentList>
</div>
</div>
</template>

View File

@ -0,0 +1,195 @@
<script setup lang="ts">
import log from "loglevel";
import { computed, onMounted, ref } from "vue";
import { type DashboardPersonType, fetchDashboardPersons } from "@/services/dashboard";
import LoadingSpinner from "@/components/ui/LoadingSpinner.vue";
import { COMPETENCE_NAVI_CERTIFICATE_QUERY } from "@/graphql/queries";
import { graphqlClient } from "@/graphql/client";
import type { CompetenceCertificateObjectType } from "@/gql/graphql";
import { calcCompetenceCertificateGrade } from "@/pages/competence/utils";
import _ from "lodash";
import type { CompetenceCertificateAssignment } from "@/types";
import { percentToRoundedGrade } from "@/services/assignmentService";
const props = defineProps<{
agentRole: string;
courseSlug: string;
competenceCertificateId: string;
courseSessionId: string;
}>();
log.debug("AgentCompetenceGradeDetailPage created", props);
const loading = ref(true);
const participants = ref<DashboardPersonType[]>([]);
const participantUserIds = computed(() => {
return (participants.value ?? []).map((p) => p.user_id);
});
const courseSession = computed(() => {
return participants.value[0]?.course_sessions.find(
(cs) => cs.id === props.courseSessionId
);
});
const certificateData = ref<CompetenceCertificateObjectType | undefined>(undefined);
function userGrade(userId: string) {
if (certificateData.value) {
const assignmentsWithUserCompletions = _.cloneDeep(
certificateData.value.assignments
);
for (const assignment of assignmentsWithUserCompletions) {
assignment.completions = assignment.completions?.filter(
(c) => c?.assignment_user?.id === userId
);
}
return calcCompetenceCertificateGrade(
assignmentsWithUserCompletions as unknown as CompetenceCertificateAssignment[],
false
);
}
}
const totalAverageGrade = computed(() => {
if (certificateData.value) {
let divisor = 0;
const assignmentAverageGrades = certificateData.value.assignments.map(
(assignment) => {
const relevantCompletions = (assignment.completions ?? []).filter(
(c) => c?.completion_status == "EVALUATION_SUBMITTED"
);
const averagePercent =
_.sumBy(relevantCompletions, (c) => c?.evaluation_percent ?? 0) /
relevantCompletions.length;
if (averagePercent > 0.0001) {
divisor += assignment.competence_certificate_weight ?? 1;
}
return averagePercent * (assignment.competence_certificate_weight ?? 1);
}
);
return percentToRoundedGrade(
_.sum(assignmentAverageGrades) / (divisor ?? 1),
false
);
}
return undefined;
});
onMounted(async () => {
log.debug("AgentAssignmentDetailPage mounted");
const personData = await fetchDashboardPersons("default");
participants.value = personData?.filter((p) => {
return p.course_sessions.find(
(cs) => cs.id === props.courseSessionId && cs.my_role === "BERUFSBILDNER"
);
});
const res = await graphqlClient.query(COMPETENCE_NAVI_CERTIFICATE_QUERY, {
courseSlug: props.courseSlug,
courseSessionId: props.courseSessionId,
userIds: participantUserIds.value,
});
// @ts-ignore
certificateData.value =
res.data?.competence_certificate_list?.competence_certificates.find(
// @ts-ignore
(cc) => cc.id === props.competenceCertificateId
);
loading.value = false;
});
</script>
<template>
<div class="bg-gray-200">
<div v-if="loading" class="m-8 flex justify-center">
<LoadingSpinner />
</div>
<div v-else class="container-large flex flex-col space-y-4">
<router-link
class="btn-text inline-flex items-center pl-0"
:to="`/statistic/${props.agentRole}/${props.courseSlug}/competence-grade`"
>
<it-icon-arrow-left />
<span>{{ $t("general.back") }}</span>
</router-link>
<div>
<h2 class="mb-8">{{ certificateData?.title }}</h2>
<div class="border-b bg-white px-6 py-6">
{{ courseSession?.session_title }}
</div>
<div class="heading-3 border-b bg-white px-6 py-6">
{{ $t("a.Durchschnittsnote") }}:
{{ totalAverageGrade }}
</div>
<div class="bg-white px-4 py-2">
<div
v-for="person in participants"
:key="person.user_id"
data-cy="person"
class="flex flex-col justify-between gap-4 border-b p-2 last:border-b-0 md:flex-row md:items-center md:justify-between md:gap-16"
>
<div class="w-full flex-auto md:w-1/2">
<div class="flex items-center space-x-2">
<img
class="inline-block h-11 w-11 rounded-full"
:src="
person.avatar_url_small ||
'/static/avatars/myvbv-default-avatar.png'
"
:alt="`${person.first_name} ${person.last_name}`"
/>
<div>
<div class="text-bold">
{{ person.first_name }}
{{ person.last_name }}
</div>
<div class="text-gray-900">{{ person.email }}</div>
</div>
</div>
</div>
<div class="flex flex-auto items-center gap-2 md:w-1/4">
<div>{{ $t("a.Note") }}:</div>
<div class="min-w-12 text-center">
<div
class="rounded px-2 py-1 font-bold"
:class="{ 'bg-red-400': (userGrade(person.user_id) ?? 4) < 4 }"
>
{{ userGrade(person.user_id) }}
</div>
</div>
</div>
<div class="w-full flex-auto items-end md:w-1/4 md:text-end">
<router-link
:to="{
name: 'profileLearningPath',
params: {
userId: person.user_id,
courseSlug: props.courseSlug,
},
query: { courseSessionId: props.courseSessionId },
}"
data-cy="person-learning-path-link"
class="link w-full lg:text-right"
>
{{ $t("a.Profil anzeigen") }}
</router-link>
</div>
</div>
</div>
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,222 @@
<script setup lang="ts">
import log from "loglevel";
import { computed, onMounted, type Ref, ref } from "vue";
import {
courseIdForCourseSlug,
fetchMentorCompetenceSummary,
} from "@/services/dashboard";
import type { AssignmentStatisticsRecordType, BaseStatisticsType } from "@/gql/graphql";
import { useDashboardStore } from "@/stores/dashboard";
import LoadingSpinner from "@/components/ui/LoadingSpinner.vue";
import _ from "lodash";
import { percentToRoundedGrade } from "@/services/assignmentService";
import StatisticFilterList from "@/components/dashboard/StatisticFilterList.vue";
import type { StatisticsFilterItem } from "@/types";
const dashboardStore = useDashboardStore();
const props = defineProps<{
agentRole: string;
courseSlug: string;
}>();
log.debug("AgentCompetenceGradePage created", props);
const loading = ref(true);
const courseId = ref<string | undefined>(undefined);
const agentAssignmentData = ref<BaseStatisticsType | null>(null);
const courseSessionName = (courseSessionId: string) => {
return (
agentAssignmentData.value?.course_session_properties?.sessions.find(
(session) => session.id === courseSessionId
)?.name ?? ""
);
};
const statisticFilter: Ref<typeof StatisticFilterList | null> = ref(null);
const filteredItems = computed(() => {
if (!statisticFilter.value) {
return [];
}
return statisticFilter.value.getFilteredItems();
});
const totalAverageGrade = computed(() => {
return percentToRoundedGrade(
_.sumBy(filteredItems.value, (i) => {
return (i as GroupedAssignmentEntry).averageEvaluationPercent ?? 0;
}) / filteredItems.value.length,
false
);
});
onMounted(async () => {
await dashboardStore.loadDashboardDetails();
courseId.value = courseIdForCourseSlug(
dashboardStore.dashboardConfigsv2,
props.courseSlug
);
if (!courseId.value) {
log.error("CourseId not found for courseSlug", props.courseSlug);
return;
}
log.debug("courseId", courseId.value);
agentAssignmentData.value = await fetchMentorCompetenceSummary(
courseId.value,
props.agentRole
);
loading.value = false;
});
interface GroupedAssignmentEntry extends StatisticsFilterItem {
competenceCertificateId: string;
competenceCertificateTitle: string;
course_session_title: string;
assignments: AssignmentStatisticsRecordType[];
sumAverageEvaluationPercent: number;
averageEvaluationPercent: number | null;
averageGrade: number | null;
}
function isGroupedAssignmentEntry(
item: StatisticsFilterItem
): item is GroupedAssignmentEntry {
return (item as GroupedAssignmentEntry).competenceCertificateId !== undefined;
}
const courseSessionCompetenceAssignments = computed(() => {
let resultArray = [] as GroupedAssignmentEntry[];
// group assignments by competence and course session
for (const assignment of agentAssignmentData.value?.assignments.records ?? []) {
const entry = resultArray.find(
(r) =>
r.competenceCertificateId === assignment.competence_certificate_id &&
r.course_session_id === assignment.course_session_id
);
if (entry) {
if (assignment.metrics.ranking_completed) {
entry.assignments.push(assignment);
}
} else {
const newEntry = {
_id: `${assignment.competence_certificate_id}-${assignment.course_session_id}`,
competenceCertificateId: assignment.competence_certificate_id ?? "",
competenceCertificateTitle: assignment.competence_certificate_title ?? "",
generation: assignment.generation ?? "",
region: assignment.region ?? "",
course_session_id: assignment.course_session_id ?? "",
course_session_title: assignment.course_session_title ?? "",
assignments: [] as AssignmentStatisticsRecordType[],
sumAverageEvaluationPercent: 0,
averageEvaluationPercent: null,
averageGrade: null,
circle_id: assignment.circle_id ?? "",
};
if (assignment && assignment.metrics.ranking_completed) {
newEntry.assignments.push(assignment);
}
resultArray.push(newEntry);
}
}
// filter out entries without assignments
resultArray = resultArray.filter((entry) => entry.assignments.length > 0);
// calculate average grade
for (const entry of resultArray) {
entry.sumAverageEvaluationPercent = _.sumBy(entry.assignments, (a) => {
return (
(a.metrics.average_evaluation_percent ?? 0) *
(a.metrics.competence_certificate_weight ?? 1)
);
});
entry.averageEvaluationPercent =
entry.sumAverageEvaluationPercent /
_.sumBy(entry.assignments, (a) => {
return a.metrics.competence_certificate_weight ?? 1;
});
entry.averageGrade = percentToRoundedGrade(entry.averageEvaluationPercent, false);
}
return _.orderBy(
resultArray,
["course_session_title", "competenceCertificateTitle"],
["asc", "asc"]
);
});
</script>
<template>
<div class="bg-gray-200">
<div v-if="loading" class="m-8 flex justify-center">
<LoadingSpinner />
</div>
<div v-else class="container-large flex flex-col space-y-4">
<router-link class="btn-text inline-flex items-center pl-0" to="/">
<it-icon-arrow-left />
<span>{{ $t("general.back") }}</span>
</router-link>
<div>
<h2 class="mb-8">{{ $t("a.Kompetenznachweise") }}</h2>
<div class="bg-white py-2">
<StatisticFilterList
v-if="
agentAssignmentData?.course_session_properties &&
courseSessionCompetenceAssignments?.length
"
ref="statisticFilter"
:course-session-properties="agentAssignmentData?.course_session_properties"
:items="courseSessionCompetenceAssignments"
:hide-circle-filter="true"
>
<template #header>
<div class="heading-3 border-b px-6 py-4">
{{ $t("a.Durchschnittsnote") }}: {{ totalAverageGrade }}
</div>
</template>
<template #default="{ item: item }">
<div
v-if="isGroupedAssignmentEntry(item)"
class="flex flex-col justify-between gap-4 border-b last:border-b-0 md:flex-row md:items-center md:justify-between md:gap-16"
>
<div class="w-full flex-auto md:w-1/2">
<span class="text-bold">
{{ item.competenceCertificateTitle }}
</span>
<br />
{{ $t("a.Durchführung") }} «{{
courseSessionName(item.course_session_id)
}}»
</div>
<div class="flex flex-auto items-center gap-2 md:w-1/4">
<div>{{ $t("a.Durchschnittsnote") }}:</div>
<div class="min-w-12 text-center">
<div
class="rounded px-2 py-1 font-bold"
:class="{ 'bg-red-400': (item.averageGrade ?? 4) < 4 }"
>
{{ item.averageGrade }}
</div>
</div>
</div>
<div class="w-full flex-auto items-end md:w-1/4 md:text-end">
<router-link
class="underline"
:to="`/statistic/${props.agentRole}/${props.courseSlug}/competence-grade/${item.course_session_id}/${item.competenceCertificateId}`"
data-cy="basebox.detailsLink"
>
{{ $t("a.Details anschauen") }}
</router-link>
</div>
</div>
</template>
</StatisticFilterList>
</div>
</div>
</div>
</div>
</template>

View File

@ -2,7 +2,7 @@
import type { import type {
AssignmentCompletionMetricsType, AssignmentCompletionMetricsType,
AssignmentStatisticsRecordType, AssignmentStatisticsRecordType,
CourseStatisticsType, BaseStatisticsType,
StatisticsCircleDataType, StatisticsCircleDataType,
} from "@/gql/graphql"; } from "@/gql/graphql";
import StatisticFilterList from "@/components/dashboard/StatisticFilterList.vue"; import StatisticFilterList from "@/components/dashboard/StatisticFilterList.vue";
@ -16,9 +16,10 @@ import { useUserStore } from "@/stores/user";
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const props = defineProps<{ const props = defineProps<{
courseStatistics: CourseStatisticsType; courseStatistics: BaseStatisticsType;
courseSessionName: (sessionId: string) => string; courseSessionName: (sessionId: string) => string;
circleMeta: (circleId: string) => StatisticsCircleDataType; circleMeta: (circleId: string) => StatisticsCircleDataType;
detailBaseUrl?: string;
}>(); }>();
const statisticFilter: Ref<typeof StatisticFilterList | null> = ref(null); const statisticFilter: Ref<typeof StatisticFilterList | null> = ref(null);
@ -51,6 +52,13 @@ async function exportData() {
const filteredItems = statisticFilter.value.getFilteredItems(); const filteredItems = statisticFilter.value.getFilteredItems();
await exportDataAsXls(filteredItems, exportCompetenceElements, userStore.language); await exportDataAsXls(filteredItems, exportCompetenceElements, userStore.language);
} }
const itemDetailUrl = (item: AssignmentStatisticsRecordType) => {
if (props.detailBaseUrl) {
return `${props.detailBaseUrl}${item.learning_content_id}?courseSessionId=${item.course_session_id}`;
}
return item.details_url;
};
</script> </script>
<template> <template>
@ -114,8 +122,7 @@ async function exportData() {
></ItProgress> ></ItProgress>
<router-link <router-link
class="underline" class="underline"
target="_blank" :to="itemDetailUrl(item as AssignmentStatisticsRecordType)"
:to="(item as AssignmentStatisticsRecordType).details_url"
> >
{{ $t("a.Details anschauen") }} {{ $t("a.Details anschauen") }}
</router-link> </router-link>

View File

@ -1,25 +1,36 @@
<script setup lang="ts"> <script setup lang="ts">
import { useCSRFFetch } from "@/fetchHelpers"; import { itPost } from "@/fetchHelpers";
import { getLearningMentorUrl } from "@/utils/utils"; import { getLearningMentorUrl } from "@/utils/utils";
import { onMounted, ref } from "vue";
const props = defineProps<{ const props = defineProps<{
courseId: string; courseId: string;
invitationId: string; invitationId: string;
}>(); }>();
const { data, error } = useCSRFFetch( const loaded = ref<boolean>(false);
`/api/mentor/${props.courseId}/invitations/accept`, const responseData = ref<any>(null);
{ const hasError = ref<boolean>(false);
onFetchError(ctx) { const errorMessage = ref<string>("");
ctx.error = ctx.data;
return ctx; onMounted(async () => {
}, const url = `/api/mentor/${props.courseId}/invitations/accept`;
} itPost(url, {
)
.post({
invitation_id: props.invitationId, invitation_id: props.invitationId,
}) })
.json(); .then((data) => {
responseData.value = data;
})
.catch((error) => {
hasError.value = true;
if (error.toString().includes("404")) {
errorMessage.value = "Einladung bereits akzeptiert";
}
})
.finally(() => {
loaded.value = true;
});
});
</script> </script>
<template> <template>
@ -28,9 +39,9 @@ const { data, error } = useCSRFFetch(
<header class="mb-8 mt-12"> <header class="mb-8 mt-12">
<h1 class="mb-8">{{ $t("a.Einladung") }}</h1> <h1 class="mb-8">{{ $t("a.Einladung") }}</h1>
</header> </header>
<main> <main v-if="loaded">
<div class="bg-white p-6"> <div class="bg-white p-6">
<template v-if="error"> <template v-if="hasError">
{{ {{
$t( $t(
"a.Die Einladung konnte nicht akzeptiert werden. Bitte melde dich beim Support." "a.Die Einladung konnte nicht akzeptiert werden. Bitte melde dich beim Support."
@ -50,8 +61,8 @@ const { data, error } = useCSRFFetch(
</a> </a>
</li> </li>
</ul> </ul>
<div v-if="error.message" class="my-4"> <div v-if="errorMessage" class="my-4">
{{ $t("a.Fehlermeldung") }}: {{ error.message }} {{ $t("a.Fehlermeldung") }}: {{ errorMessage }}
</div> </div>
</template> </template>
<template v-else> <template v-else>
@ -61,11 +72,16 @@ const { data, error } = useCSRFFetch(
" "
> >
<template #name> <template #name>
<b>{{ data.user.first_name }} {{ data.user.last_name }}</b> <b>
{{ responseData.user.first_name }} {{ responseData.user.last_name }}
</b>
</template> </template>
</i18next> </i18next>
<div class="mt-4"> <div class="mt-4">
<a class="underline" :href="getLearningMentorUrl(data.course_slug)"> <a
class="underline"
:href="getLearningMentorUrl(responseData.course_slug)"
>
{{ $t("a.Übersicht anschauen") }} {{ $t("a.Übersicht anschauen") }}
</a> </a>
</div> </div>

View File

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Assignment, Participant } from "@/services/learningMentees"; import type { Assignment, UserShort } from "@/services/learningMentees";
import { useLearningMentees } from "@/services/learningMentees"; import { useLearningMentees } from "@/services/learningMentees";
import { computed, onMounted, type Ref } from "vue"; import { computed, onMounted, type Ref } from "vue";
import { useCurrentCourseSession } from "@/composables"; import { useCurrentCourseSession } from "@/composables";
@ -11,13 +11,16 @@ const props = defineProps<{
const courseSession = useCurrentCourseSession(); const courseSession = useCurrentCourseSession();
const learningMentees = useLearningMentees(courseSession.value.id); const learningMentees = useLearningMentees(courseSession.value.id);
const participants = computed(() => learningMentees.summary.value?.participants);
const praxisAssignment: Ref<Assignment | null> = computed(() => const praxisAssignment: Ref<Assignment | null> = computed(() =>
learningMentees.getAssignmentById(props.praxisAssignmentId) learningMentees.getAssignmentById(props.praxisAssignmentId)
); );
const getParticipantById = (id: string): Participant | null => { const getParticipantById = (id: string): UserShort | undefined => {
return participants.value?.find((participant) => participant.id === id) || null; return (learningMentees.summary.value?.participant_relations ?? [])
.map((rel) => {
return rel.participant_user;
})
.find((user) => user.id === id);
}; };
onMounted(() => { onMounted(() => {
@ -47,6 +50,7 @@ onMounted(() => {
v-for="item in praxisAssignment.completions" v-for="item in praxisAssignment.completions"
:key="item.user_id" :key="item.user_id"
class="flex flex-col items-start justify-between gap-4 border-b py-2 pl-5 pr-5 last:border-b-0 md:flex-row md:items-center md:justify-between md:gap-16" class="flex flex-col items-start justify-between gap-4 border-b py-2 pl-5 pr-5 last:border-b-0 md:flex-row md:items-center md:justify-between md:gap-16"
:data-cy="`praxis-assignment-feedback-${item.user_id}`"
> >
<!-- Left --> <!-- Left -->
<div class="flex flex-grow flex-row items-center justify-start"> <div class="flex flex-grow flex-row items-center justify-start">

View File

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Assignment, Participant } from "@/services/learningMentees"; import type { Assignment, UserShort } from "@/services/learningMentees";
import { useLearningMentees } from "@/services/learningMentees"; import { useLearningMentees } from "@/services/learningMentees";
import { computed, type Ref } from "vue"; import { computed, type Ref } from "vue";
import { useCurrentCourseSession } from "@/composables"; import { useCurrentCourseSession } from "@/composables";
@ -15,14 +15,12 @@ const selfEvaluationFeedback: Ref<Assignment | null> = computed(() =>
learningMentees.getAssignmentById(props.learningUnitId) learningMentees.getAssignmentById(props.learningUnitId)
); );
const getParticipantById = (id: string): Participant | null => { const getParticipantById = (id: string): UserShort | undefined => {
if (learningMentees.summary.value?.participants) { return (learningMentees.summary.value?.participant_relations ?? [])
const found = learningMentees.summary.value.participants.find( .map((rel) => {
(item) => item.id === id return rel.participant_user;
); })
return found || null; .find((user) => user.id === id);
}
return null;
}; };
</script> </script>
@ -51,6 +49,7 @@ const getParticipantById = (id: string): Participant | null => {
v-for="item in selfEvaluationFeedback.completions" v-for="item in selfEvaluationFeedback.completions"
:key="item.user_id" :key="item.user_id"
class="flex flex-col items-start justify-between gap-4 border-b py-2 pl-5 pr-5 last:border-b-0 md:flex-row md:items-center md:justify-between md:gap-16" class="flex flex-col items-start justify-between gap-4 border-b py-2 pl-5 pr-5 last:border-b-0 md:flex-row md:items-center md:justify-between md:gap-16"
:data-cy="`self-evalution-feedback-${item.user_id}`"
> >
<!-- Left --> <!-- Left -->
<div class="flex flex-grow flex-row items-center justify-start"> <div class="flex flex-grow flex-row items-center justify-start">

View File

@ -29,8 +29,8 @@ const isMentorsLoading = computed(() => learningMentors.loading.value);
const mentors = computed(() => { const mentors = computed(() => {
return learningMentors.learningMentors.value.map((mentor) => ({ return learningMentors.learningMentors.value.map((mentor) => ({
id: mentor.mentor.id, id: mentor.agent.id,
name: `${mentor.mentor.first_name} ${mentor.mentor.last_name}`, name: `${mentor.agent.first_name} ${mentor.agent.last_name}`,
})); }));
}); });
@ -100,6 +100,7 @@ const onRequestFeedback = async () => {
variant="primary" variant="primary"
size="large" size="large"
:disabled="!currentSessionRequestedMentor" :disabled="!currentSessionRequestedMentor"
data-cy="request-feedback-button"
@click="onRequestFeedback" @click="onRequestFeedback"
> >
<p v-if="!currentSessionRequestedMentor"> <p v-if="!currentSessionRequestedMentor">

View File

@ -46,7 +46,7 @@ const signUpURL = computed(() => getSignUpURL(constructParams()));
</a> </a>
<p class="mb-4 mt-12">{{ $t("a.Hast du schon ein Konto?") }}</p> <p class="mb-4 mt-12">{{ $t("a.Hast du schon ein Konto?") }}</p>
<a :href="loginURL" class="btn-secondary"> <a :href="loginURL" class="btn-secondary" data-cy="login-button">
{{ $t("a.Anmelden") }} {{ $t("a.Anmelden") }}
</a> </a>
</template> </template>

View File

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, ref } from "vue"; import { onMounted } from "vue";
import { useRoute, useRouter } from "vue-router"; import { useRoute, useRouter } from "vue-router";
import { useTranslation } from "i18next-vue"; import { useTranslation } from "i18next-vue";
import { useFetch } from "@vueuse/core"; import { useFetch } from "@vueuse/core";
@ -12,18 +12,24 @@ const props = defineProps<{
const { t } = useTranslation(); const { t } = useTranslation();
const pages = ref([ const pages = [
{ {
label: t("general.learningPath"), label: t("general.learningPath"),
route: "profileLearningPath", route: "profileLearningPath",
routeMatch: "profileLearningPath", routeMatch: "profileLearningPath",
childrenRouteMatches: [],
}, },
{ {
label: t("a.KompetenzNavi"), label: t("a.KompetenzNavi"),
route: "competenceMain", route: "competenceMain",
routeMatch: "profileCompetence", routeMatch: "profileCompetence",
childrenRouteMatches: [
"competenceCertificates",
"competenceCertificateDetail",
"competenceEvaluations",
],
}, },
]); ];
const courseSession = useCurrentCourseSession(); const courseSession = useCurrentCourseSession();
const { data: user } = useFetch( const { data: user } = useFetch(
@ -35,9 +41,16 @@ const router = useRouter();
onMounted(() => { onMounted(() => {
// if current route name not in pages, redirect to first page // if current route name not in pages, redirect to first page
if (route.name && !pages.value.find((page) => page.route === route.name)) { if (
route.name &&
!pages.find((page) => {
const routeName = route.name?.toString() || "";
const routeMatch = [...page.childrenRouteMatches, page.routeMatch];
return routeMatch.some((match) => routeName.includes(match));
})
) {
router.push({ router.push({
name: pages.value[0].route, name: pages[0].route,
params: { userId: props.userId, courseSlug: props.courseSlug }, params: { userId: props.userId, courseSlug: props.courseSlug },
}); });
} }

View File

@ -339,6 +339,32 @@ const router = createRouter({
}, },
], ],
}, },
{
path: "/statistic/:agentRole/:courseSlug/assignment",
props: true,
component: () =>
import("@/pages/dashboard/agentAssignment/AgentAssignmentStatisticPage.vue"),
},
{
path: "/statistic/:agentRole/:courseSlug/assignment/:assignmentId",
props: true,
component: () =>
import("@/pages/dashboard/agentAssignment/AgentAssignmentDetailPage.vue"),
},
{
path: "/statistic/:agentRole/:courseSlug/competence-grade",
props: true,
component: () =>
import("@/pages/dashboard/agentAssignment/AgentCompetenceGradePage.vue"),
},
{
path: "/statistic/:agentRole/:courseSlug/competence-grade/:courseSessionId/:competenceCertificateId",
props: true,
component: () =>
import("@/pages/dashboard/agentAssignment/AgentCompetenceGradeDetailPage.vue"),
},
{ {
path: "/shop", path: "/shop",
component: () => import("@/pages/ShopPage.vue"), component: () => import("@/pages/ShopPage.vue"),

View File

@ -1,5 +1,5 @@
import { useCourseSessionDetailQuery } from "@/composables"; import { useCourseSessionDetailQuery } from "@/composables";
import { itGet } from "@/fetchHelpers"; import { itPost } from "@/fetchHelpers";
import type { import type {
Assignment, Assignment,
AssignmentCompletion, AssignmentCompletion,
@ -19,12 +19,16 @@ export interface GradedUser {
export async function loadAssignmentCompletionStatusData( export async function loadAssignmentCompletionStatusData(
assignmentId: string, assignmentId: string,
courseSessionId: string, courseSessionId: string,
learningContentId: string learningContentId: string,
userSelectionIds: string[] | undefined = undefined
) { ) {
const courseSessionDetailResult = useCourseSessionDetailQuery(); const courseSessionDetailResult = useCourseSessionDetailQuery();
const assignmentCompletionData = (await itGet( const assignmentCompletionData = (await itPost(
`/api/assignment/${assignmentId}/${courseSessionId}/status/` `/api/assignment/${assignmentId}/${courseSessionId}/status/`,
{
user_selection_ids: userSelectionIds ?? [],
}
)) as UserAssignmentCompletionStatus[]; )) as UserAssignmentCompletionStatus[];
const members = courseSessionDetailResult.filterMembers(); const members = courseSessionDetailResult.filterMembers();

View File

@ -1,8 +1,3 @@
import type {
CompetenceCertificateForUserQueryQuery,
CompetenceCertificateListObjectType,
CompetenceCertificateQueryQuery,
} from "@/gql/graphql";
import type { PerformanceCriteria } from "@/types"; import type { PerformanceCriteria } from "@/types";
import groupBy from "lodash/groupBy"; import groupBy from "lodash/groupBy";
@ -22,42 +17,3 @@ export function calcPerformanceCriteriaStatusCount(criteria: PerformanceCriteria
FAIL: 0, FAIL: 0,
}; };
} }
// Type guards
export function isCompetenceCertificateForUserQueryQuery(
data: any
): data is CompetenceCertificateForUserQueryQuery {
return (
(data as CompetenceCertificateForUserQueryQuery)
.competence_certificate_list_for_user !== undefined
);
}
export function isCompetenceCertificateQueryQuery(
data: any
): data is CompetenceCertificateQueryQuery {
return (
(data as CompetenceCertificateQueryQuery).competence_certificate_list !== undefined
);
}
export function getCertificates(
data: any,
userId: string | null
): CompetenceCertificateListObjectType | null {
if (!data) {
return null;
}
let certificates = null;
if (userId && isCompetenceCertificateForUserQueryQuery(data)) {
certificates = data.competence_certificate_list_for_user;
} else if (isCompetenceCertificateQueryQuery(data)) {
certificates = data.competence_certificate_list;
} else {
// Handle case where data does not match expected types
console.error("Data structure is not recognized!");
return null;
}
return (certificates as unknown as CompetenceCertificateListObjectType) ?? null;
}

View File

@ -8,7 +8,7 @@ import {
import { itGetCached, itPost } from "@/fetchHelpers"; import { itGetCached, itPost } from "@/fetchHelpers";
import type { import type {
AssignmentsStatisticsType, BaseStatisticsType,
CourseProgressType, CourseProgressType,
CourseStatisticsType, CourseStatisticsType,
DashboardConfigType, DashboardConfigType,
@ -25,14 +25,16 @@ export type DashboardPersonRoleType =
| "EXPERT" | "EXPERT"
| "MEMBER" | "MEMBER"
| "LEARNING_MENTOR" | "LEARNING_MENTOR"
| "LEARNING_MENTEE"; | "BERUFSBILDNER"
| "PARTICIPANT";
export type DashboardRoleKeyType = export type DashboardRoleKeyType =
| "Supervisor" | "Supervisor"
| "Trainer" | "Trainer"
| "Member" | "Member"
| "MentorUK" | "MentorUK"
| "MentorVV"; | "MentorVV"
| "Berufsbildner";
export type WidgetType = export type WidgetType =
| "ProgressWidget" | "ProgressWidget"
@ -41,7 +43,8 @@ export type WidgetType =
| "MentorPersonWidget" | "MentorPersonWidget"
| "MentorCompetenceWidget" | "MentorCompetenceWidget"
| "CompetenceCertificateWidget" | "CompetenceCertificateWidget"
| "UKStatisticsWidget"; | "UKStatisticsWidget"
| "UKBerufsbildnerStatisticsWidget";
export type DashboardPersonCourseSessionType = { export type DashboardPersonCourseSessionType = {
id: string; id: string;
@ -107,6 +110,7 @@ export const fetchStatisticData = async (
console.error("Error fetching statistics for course ID:", courseId, res.error); console.error("Error fetching statistics for course ID:", courseId, res.error);
} }
// @ts-ignore
return res.data?.course_statistics || null; return res.data?.course_statistics || null;
} catch (error) { } catch (error) {
console.error(`Error fetching statistics for course ID: ${courseId}`, error); console.error(`Error fetching statistics for course ID: ${courseId}`, error);
@ -149,17 +153,33 @@ export const fetchDashboardConfig = async (): Promise<DashboardConfigType[] | nu
}; };
export const fetchMentorCompetenceSummary = async ( export const fetchMentorCompetenceSummary = async (
courseId: string courseId: string,
): Promise<AssignmentsStatisticsType | null> => { roleKey: string
): Promise<BaseStatisticsType | null> => {
let agentRole = "";
if (
["MentorUK".toLowerCase(), "MentorVV".toLowerCase()].includes(roleKey.toLowerCase())
) {
agentRole = "LEARNING_MENTOR";
} else if (roleKey.toLowerCase() === "Berufsbildner".toLowerCase()) {
agentRole = "BERUFSBILDNER";
}
if (!agentRole) {
console.error(`Invalid role key for competence summary: ${roleKey}`);
return null;
}
try { try {
const res = await graphqlClient.query(DASHBOARD_MENTOR_COMPETENCE_SUMMARY, { const res = await graphqlClient.query(DASHBOARD_MENTOR_COMPETENCE_SUMMARY, {
courseId, courseId,
agentRole,
}); });
if (res.error) { if (res.error) {
console.error("Error fetching data for course ID:", courseId, res.error); console.error("Error fetching data for course ID:", courseId, res.error);
} }
return res.data?.mentor_course_statistics?.assignments || null; return res.data?.mentor_course_statistics || null;
} catch (error) { } catch (error) {
console.error(`Error fetching data for course ID: ${courseId}`, error); console.error(`Error fetching data for course ID: ${courseId}`, error);
return null; return null;

View File

@ -2,7 +2,7 @@ import { itGet } from "@/fetchHelpers";
import type { Ref } from "vue"; import type { Ref } from "vue";
import { ref, watchEffect } from "vue"; import { ref, watchEffect } from "vue";
export interface Participant { export interface UserShort {
id: string; id: string;
first_name: string; first_name: string;
last_name: string; last_name: string;
@ -12,6 +12,14 @@ export interface Participant {
language: string; language: string;
} }
export interface AgentParticipantRelation {
id: string;
role: "LEARNING_MENTOR";
course_session_id: number;
agent: UserShort;
participant_user: UserShort;
}
interface Circle { interface Circle {
id: number; id: number;
title: string; title: string;
@ -40,9 +48,8 @@ export interface Assignment {
type: string; type: string;
} }
export interface Summary { export interface LearningMentorSummary {
mentor_id: string; participant_relations: AgentParticipantRelation[];
participants: Participant[];
circles: Circle[]; circles: Circle[];
assignments: Assignment[]; assignments: Assignment[];
} }
@ -51,7 +58,7 @@ export const useLearningMentees = (
courseSessionId: string | Ref<string> | (() => string) courseSessionId: string | Ref<string> | (() => string)
) => { ) => {
const isLoading = ref(false); const isLoading = ref(false);
const summary: Ref<Summary | null> = ref(null); const summary: Ref<LearningMentorSummary | null> = ref(null);
const error = ref(null); const error = ref(null);
const getAssignmentById = (id: string): Assignment | null => { const getAssignmentById = (id: string): Assignment | null => {

View File

@ -47,12 +47,6 @@ export const useDashboardStore = defineStore("dashboard", () => {
}; };
const loadStatisticsData = async (id: string) => { const loadStatisticsData = async (id: string) => {
const data = await fetchStatisticData(id);
dashBoardDataCache[id] = data;
currentDashBoardData.value = data;
};
const loadStatisticsDatav2 = async (id: string) => {
const data = await fetchStatisticData(id); const data = await fetchStatisticData(id);
dashBoardDataCache[id] = data; dashBoardDataCache[id] = data;
return data; return data;
@ -68,6 +62,5 @@ export const useDashboardStore = defineStore("dashboard", () => {
currentDashBoardData, currentDashBoardData,
loading, loading,
loadStatisticsData, loadStatisticsData,
loadStatisticsDatav2,
}; };
}); });

View File

@ -399,7 +399,7 @@ export interface CompetenceCertificateAssignment extends BaseCourseWagtailPage {
circle: CircleLight; circle: CircleLight;
}) })
| null; | null;
completion: { completions: {
id: string; id: string;
completion_status: AssignmentCompletionStatus; completion_status: AssignmentCompletionStatus;
evaluation_submitted_at: string | null; evaluation_submitted_at: string | null;
@ -413,7 +413,7 @@ export interface CompetenceCertificateAssignment extends BaseCourseWagtailPage {
id: string; id: string;
title: string; title: string;
}; };
} | null; }[];
} }
export interface CompetenceCertificate extends BaseCourseWagtailPage { export interface CompetenceCertificate extends BaseCourseWagtailPage {
@ -473,15 +473,16 @@ export interface ExpertSessionUser extends CourseSessionUser {
role: "EXPERT"; role: "EXPERT";
} }
export interface Mentor { export interface Agent {
id: number; id: number;
first_name: string; first_name: string;
last_name: string; last_name: string;
} }
export interface LearningMentor { export interface AgentParticipantRelation {
id: number; id: number;
mentor: Mentor; role: "LEARNING_MENTOR";
agent: Agent;
} }
export type CourseSessionDetail = CourseSessionObjectType; export type CourseSessionDetail = CourseSessionObjectType;
@ -627,6 +628,7 @@ export type DashboardPersonsPageMode = "default" | "competenceMetrics";
export interface StatisticsFilterItem { export interface StatisticsFilterItem {
_id: string; _id: string;
course_session_id: string; course_session_id: string;
region: string;
generation: string; generation: string;
circle_id: string; circle_id: string;
} }

View File

@ -23,8 +23,9 @@ export default defineConfig(({ mode }) => {
], ],
define: {}, define: {},
server: { server: {
host: true,
port: 5173, port: 5173,
hmr: { port: 5173 }, strictPort: true,
}, },
resolve: { resolve: {
alias: { alias: {

View File

@ -1,8 +1,10 @@
const { defineConfig } = require("cypress"); import { defineConfig } from "cypress";
const { cloudPlugin } = require("cypress-cloud/plugin"); import { cloudPlugin } from "cypress-cloud/plugin";
import tasks from "./cypress/plugins/index.mjs";
module.exports = defineConfig({ export default defineConfig({
projectId: "RVEZS1", projectId: "RVEZS1",
chromeWebSecurity: false,
watchForFileChanges: false, watchForFileChanges: false,
video: true, video: true,
viewportWidth: 1280, viewportWidth: 1280,
@ -19,6 +21,7 @@ module.exports = defineConfig({
e2e: { e2e: {
// experimentalSessionAndOrigin: true, // experimentalSessionAndOrigin: true,
setupNodeEvents(on, config) { setupNodeEvents(on, config) {
tasks(on, config);
return cloudPlugin(on, config); return cloudPlugin(on, config);
}, },
baseUrl: "http://localhost:8001", baseUrl: "http://localhost:8001",

View File

@ -0,0 +1,57 @@
import { login } from "../helpers";
import { TEST_STUDENT1_VV_USER_ID } from "../../consts";
describe("mentorInvitation.cy.js", () => {
beforeEach(() => {
cy.manageCommand("cypress_reset");
});
it("Teilnehmer macht lädt Lernbegleitung ein; Lernbegleitung akzeptiert Einladung", () => {
login("student-vv@eiger-versicherungen.ch", "test");
cy.visit("/course/versicherungsvermittler-in/learn");
cy.get("[data-cy=navigation-learning-mentor-link]").click();
cy.get('[data-cy="lm-invite-mentor-button"]').click();
cy.get("#mentor-email").type("empty@example.com");
cy.get('[data-cy="invite-mentor-button"]').click();
cy.get('[data-cy="mentor-empty@example.com"]').should(
"contain",
"Die Einladung wurde noch nicht angenommen.",
);
cy.task(
"runSql",
"select target_url from learning_mentor_mentorinvitation where email = 'empty@example.com'",
).then((res) => {
const invitationUrl = res.rows[0].target_url;
console.log(invitationUrl);
cy.visit("/");
cy.get('[data-cy="header-profile"]').click();
cy.get('[data-cy="logout-button"]').click();
cy.wait(1000);
// try to accept invitation
cy.visit(invitationUrl);
cy.get('[data-cy="login-button"]').click();
cy.get("#username").type("empty@example.com");
cy.get("#password").type("test");
cy.get('[data-cy="login-button"]').click();
cy.get(".bg-white").should(
"contain",
"Du hast die Einladung von Viktor Vollgas erfolgreich akzeptiert.",
);
cy.contains("Übersicht anschauen").click();
cy.get('[data-cy="lm-my-mentees"]').should(
"contain",
"Personen, die du begleitest",
);
cy.get('[data-cy="lm-my-mentees"]').should(
"contain",
"student-vv@eiger-versicherungen.ch",
);
});
});
});

View File

@ -0,0 +1,54 @@
import { login, logout } from "../../helpers";
import { TEST_STUDENT1_VV_USER_ID } from "../../../consts";
describe("fremdeinschätzung.cy.js", () => {
beforeEach(() => {
cy.manageCommand("cypress_reset --create-learning-mentor");
});
it("teilnehmer macht selbsteinschätzung; begleiter kann fremdeinschätzung machen", () => {
// teilnehmer macht selbsteinschätzung
login("student-vv@eiger-versicherungen.ch", "test");
cy.visit(
"/course/versicherungsvermittler-in/learn/basis/evaluate/mein-neuer-job-arbeitstechnik-soziale-medien-datenschutz-und-beratungspflichten",
);
cy.makeSelfEvaluation([true, false, true], false);
cy.get('[data-cy="dropdown-select"]').click();
cy.get('[data-cy="dropdown-select-option-Micheala Weber-Mentor"]').click();
cy.get('[data-cy="request-feedback-button"]').click();
cy.get('[data-cy="complete-and-continue"]').click({ force: true });
cy.visit("/");
cy.get('[data-cy="header-profile"]').click();
cy.get('[data-cy="logout-button"]').click();
cy.wait(1000);
// fremdeinschätzung vornehmen
login("test-mentor1@example.com", "test");
cy.visit("/");
cy.get(
'[data-cy="panel-versicherungsvermittler-in"] [data-cy="dashboard.mentor.openTasksCount"]',
).should("contain", "1");
cy.get(
'[data-cy="panel-versicherungsvermittler-in"] [data-cy="dashboard.mentor.openTasksCount"] [data-cy="basebox.detailsLink"]',
).click();
cy.contains("Fremdeinschätzung vornehmen").click();
// viktor vollgas auswählen
cy.get(
`[data-cy="self-evalution-feedback-${TEST_STUDENT1_VV_USER_ID}"]`,
).should("contain", "Selbsteinschätzung geteilt");
cy.contains("Fremdeinschätzung vornehmen").click();
cy.makeSelfEvaluation([true, true, true], false);
cy.get('[data-cy="feedback-release-button"]').click();
cy.get('[data-cy="complete-and-continue"]').click({ force: true });
cy.visit("/");
cy.get(
'[data-cy="panel-versicherungsvermittler-in"] [data-cy="dashboard.mentor.openTasksCount"]',
).should("contain", "0");
});
});

View File

@ -0,0 +1,111 @@
import { login, logout } from "../../helpers";
import { TEST_STUDENT1_VV_USER_ID } from "../../../consts";
describe("praxisauftrag.cy.js", () => {
beforeEach(() => {
cy.manageCommand("cypress_reset --create-learning-mentor");
});
it("Teilnehmer macht Praxisauftrag; Begleiter kann Feedback geben", () => {
// teilnehmer macht selbsteinschätzung
login("student-vv@eiger-versicherungen.ch", "test");
cy.visit(
"/course/versicherungsvermittler-in/learn/gewinnen/mein-kundenstamm",
);
cy.learningContentMultiLayoutNextStep();
cy.get('[data-cy="it-textarea-user-text-input-1"]')
.clear()
.type("Hallo Teilaufgabe 1");
// wait because of input debounce
cy.wait(550);
cy.learningContentMultiLayoutNextStep();
cy.get('[data-cy="it-textarea-user-text-input-1"]')
.clear()
.type("Hallo Teilaufgabe 2.1");
cy.wait(550);
cy.learningContentMultiLayoutNextStep();
cy.get('[data-cy="it-textarea-user-text-input-1"]')
.clear()
.type("Hallo Teilaufgabe 3.1");
// wait because of input debounce
cy.wait(550);
cy.learningContentMultiLayoutNextStep();
cy.get('[data-cy="it-textarea-user-text-input-0"]')
.clear()
.type("Hallo Teilaufgabe 4.1");
// wait because of input debounce
cy.wait(550);
cy.learningContentMultiLayoutNextStep();
cy.get('[data-cy="it-textarea-user-text-input-0"]')
.clear()
.type("Hallo Teilaufgabe 5.1");
// wait because of input debounce
cy.wait(550);
cy.learningContentMultiLayoutNextStep();
cy.get('[data-cy="confirm-submit-person"]').click();
cy.get('[data-cy="select-learning-mentor"]').select(
"Micheala Weber-Mentor",
);
cy.get('[data-cy="submit-assignment"]').click();
cy.visit("/");
cy.get('[data-cy="header-profile"]').click();
cy.get('[data-cy="logout-button"]').click();
cy.wait(1000);
// mentor feedback geben
login("test-mentor1@example.com", "test");
cy.visit("/");
cy.get(
'[data-cy="panel-versicherungsvermittler-in"] [data-cy="dashboard.mentor.openTasksCount"]',
).should("contain", "1");
cy.get(
'[data-cy="panel-versicherungsvermittler-in"] [data-cy="dashboard.mentor.openTasksCount"] [data-cy="basebox.detailsLink"]',
).click();
cy.contains("Feedback geben").click();
// viktor vollgas auswählen
cy.get(
`[data-cy="praxis-assignment-feedback-${TEST_STUDENT1_VV_USER_ID}"]`,
).should("contain", "Ergebnisse abgegeben");
cy.contains("Feedback geben").click();
cy.get('[data-cy="start-evaluation"]').click();
cy.get('[data-cy="it-textarea-default"]').clear().type("Hallo Feedback 1");
cy.wait(550);
cy.get('[data-cy="next-step"]').click();
cy.get('[data-cy="it-textarea-default"]').clear().type("Hallo Feedback 2");
cy.wait(550);
cy.get('[data-cy="next-step"]').click();
cy.get('[data-cy="it-textarea-default"]').clear().type("Hallo Feedback 3");
cy.wait(550);
cy.get('[data-cy="next-step"]').click();
cy.get('[data-cy="it-textarea-default"]').clear().type("Hallo Feedback 4");
cy.wait(550);
cy.get('[data-cy="next-step"]').click();
cy.get('[data-cy="it-textarea-default"]').clear().type("Hallo Feedback 5");
cy.wait(550);
cy.get('[data-cy="next-step"]').click();
cy.get('[data-cy="submit-evaluation"]').click();
cy.get('[data-cy="next-step"]').click();
cy.visit("/");
cy.get(
'[data-cy="panel-versicherungsvermittler-in"] [data-cy="dashboard.mentor.openTasksCount"]',
).should("contain", "0");
});
});

View File

@ -1,22 +0,0 @@
/// <reference types="cypress" />
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
/**
* @type {Cypress.PluginConfig}
*/
// eslint-disable-next-line no-unused-vars
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
}

View File

@ -0,0 +1,9 @@
import { runSql } from "./tasks.mjs";
export default (on, config) => {
on("task", {
runSql,
});
return config;
};

18
cypress/plugins/tasks.mjs Normal file
View File

@ -0,0 +1,18 @@
import pg from "pg";
const cypressDatabaseUrl = process.env?.CYPRESS_DATABASE_URL || "postgres://postgres@localhost:5432/vbv_lernwelt_cypress";
if(!cypressDatabaseUrl) {
throw new Error(
"CYPRESS_DATABASE_URL must be set"
);
}
export async function runSql(sqlString) {
// I could not make postgres.js make work, so I use pg directly
const client = new pg.Client(cypressDatabaseUrl);
await client.connect();
const res = await client.query(sqlString);
await client.end();
return res;
}

View File

@ -87,14 +87,14 @@ function loadObjectJson(
value, value,
djangoModelPath, djangoModelPath,
serializerModelPath, serializerModelPath,
valueAsString = false valueAsString = false,
) { ) {
const djangoModel = _.last(djangoModelPath.split(".")) const djangoModel = _.last(djangoModelPath.split("."))
const djangoModelImportPath = _.initial(djangoModelPath.split(".")).join(".") const djangoModelImportPath = _.initial(djangoModelPath.split(".")).join(".")
const serializerModel = _.last(serializerModelPath.split(".")) const serializerModel = _.last(serializerModelPath.split("."))
const serializerModelImportPath = _.initial( const serializerModelImportPath = _.initial(
serializerModelPath.split(".") serializerModelPath.split("."),
).join(".") ).join(".");
let filterPart = `${key}=${value}` let filterPart = `${key}=${value}`
if (valueAsString) { if (valueAsString) {
@ -134,9 +134,10 @@ Cypress.Commands.add("loadAssignmentCompletion", (key, value) => {
value, value,
"vbv_lernwelt.assignment.models.AssignmentCompletion", "vbv_lernwelt.assignment.models.AssignmentCompletion",
"vbv_lernwelt.assignment.serializers.CypressAssignmentCompletionSerializer", "vbv_lernwelt.assignment.serializers.CypressAssignmentCompletionSerializer",
true true,
) );
}) });
Cypress.Commands.add("loadSecurityRequestResponseLog", (key, value) => { Cypress.Commands.add("loadSecurityRequestResponseLog", (key, value) => {
return loadObjectJson( return loadObjectJson(
@ -144,9 +145,10 @@ Cypress.Commands.add("loadSecurityRequestResponseLog", (key, value) => {
value, value,
"vbv_lernwelt.core.models.SecurityRequestResponseLog", "vbv_lernwelt.core.models.SecurityRequestResponseLog",
"vbv_lernwelt.core.serializers.CypressSecurityRequestResponseLogSerializer", "vbv_lernwelt.core.serializers.CypressSecurityRequestResponseLogSerializer",
true true,
) );
}) });
Cypress.Commands.add("loadExternalApiRequestLog", (key, value) => { Cypress.Commands.add("loadExternalApiRequestLog", (key, value) => {
return loadObjectJson( return loadObjectJson(
@ -154,9 +156,10 @@ Cypress.Commands.add("loadExternalApiRequestLog", (key, value) => {
value, value,
"vbv_lernwelt.core.models.ExternalApiRequestLog", "vbv_lernwelt.core.models.ExternalApiRequestLog",
"vbv_lernwelt.core.serializers.CypressExternalApiRequestLogSerializer", "vbv_lernwelt.core.serializers.CypressExternalApiRequestLogSerializer",
true true,
) );
}) });
Cypress.Commands.add("loadFeedbackResponse", (key, value) => { Cypress.Commands.add("loadFeedbackResponse", (key, value) => {
return loadObjectJson( return loadObjectJson(
@ -164,9 +167,10 @@ Cypress.Commands.add("loadFeedbackResponse", (key, value) => {
value, value,
"vbv_lernwelt.feedback.models.FeedbackResponse", "vbv_lernwelt.feedback.models.FeedbackResponse",
"vbv_lernwelt.feedback.serializers.CypressFeedbackResponseSerializer", "vbv_lernwelt.feedback.serializers.CypressFeedbackResponseSerializer",
true true,
) );
}) });
Cypress.Commands.add("loadCheckoutInformation", (key, value) => { Cypress.Commands.add("loadCheckoutInformation", (key, value) => {
return loadObjectJson( return loadObjectJson(
@ -174,9 +178,10 @@ Cypress.Commands.add("loadCheckoutInformation", (key, value) => {
value, value,
"vbv_lernwelt.shop.models.CheckoutInformation", "vbv_lernwelt.shop.models.CheckoutInformation",
"vbv_lernwelt.shop.serializers.CypressCheckoutInformationSerializer", "vbv_lernwelt.shop.serializers.CypressCheckoutInformationSerializer",
true true,
) );
}) });
Cypress.Commands.add("loadUser", (key, value) => { Cypress.Commands.add("loadUser", (key, value) => {
return loadObjectJson( return loadObjectJson(
@ -188,7 +193,6 @@ Cypress.Commands.add("loadUser", (key, value) => {
); );
}); });
Cypress.Commands.add("loadCourseSessionUser", (key, value) => { Cypress.Commands.add("loadCourseSessionUser", (key, value) => {
return loadObjectJson( return loadObjectJson(
key, key,
@ -199,29 +203,33 @@ Cypress.Commands.add("loadCourseSessionUser", (key, value) => {
); );
}); });
Cypress.Commands.add("makeSelfEvaluation", (answers, withCompletion = true) => {
Cypress.Commands.add("makeSelfEvaluation", (answers) => {
for (let i = 0; i < answers.length; i++) { for (let i = 0; i < answers.length; i++) {
const answer = answers[i] const answer = answers[i];
if (answer) { if (answer) {
cy.get('[data-cy="success"]').click() cy.get('[data-cy="success"]').click();
} else { } else {
cy.get('[data-cy="fail"]').click() cy.get('[data-cy="fail"]').click();
} }
if (i < answers.length - 1) {
cy.get('[data-cy="next-step"]').click({ force: true }) if (withCompletion) {
if (i < answers.length - 1) {
cy.get('[data-cy="next-step"]').click({force: true});
} else {
cy.get('[data-cy="complete-and-continue"]').click({force: true});
}
} else { } else {
cy.get('[data-cy="complete-and-continue"]').click({ force: true }) cy.get('[data-cy="next-step"]').click({force: true});
} }
} }
}) });
Cypress.Commands.add("learningContentMultiLayoutNextStep", () => { Cypress.Commands.add("learningContentMultiLayoutNextStep", () => {
return cy.get('[data-cy="next-step"]').click({ force: true }) return cy.get('[data-cy="next-step"]').click({force: true})
}) })
Cypress.Commands.add("learningContentMultiLayoutPreviousStep", () => { Cypress.Commands.add("learningContentMultiLayoutPreviousStep", () => {
return cy.get('[data-cy="previous-step"]').click({ force: true }) return cy.get('[data-cy="previous-step"]').click({force: true})
}) })
Cypress.Commands.add("testLearningContentTitle", (title) => { Cypress.Commands.add("testLearningContentTitle", (title) => {

1061
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -10,7 +10,8 @@
"prettier": "npm run prettier --prefix client" "prettier": "npm run prettier --prefix client"
}, },
"devDependencies": { "devDependencies": {
"cypress": "^12.15.0", "cypress": "^12.17.4",
"cypress-cloud": "^1.7.4" "cypress-cloud": "^1.10.2",
"pg": "^8.12.0"
} }
} }

View File

@ -0,0 +1,47 @@
import os
import sys
import django
from django.contrib.auth.hashers import make_password
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.course.models import CourseSessionUser
from vbv_lernwelt.core.admin import User
from vbv_lernwelt.learning_mentor.models import (
AgentParticipantRelation,
AgentParticipantRoleType,
)
def main():
berufsbildner, _ = User.objects.get_or_create(
id="5f984be9-3024-4169-9c7b-c9e827c18fd8"
)
berufsbildner.username = "berufsbildner-mobi@example.com"
berufsbildner.email = "berufsbildner-mobi@example.com"
berufsbildner.language = "de"
berufsbildner.first_name = "Berufsbildner"
berufsbildner.last_name = "Mobi"
berufsbildner.password = make_password("test")
berufsbildner.save()
for csu in (
CourseSessionUser.objects.filter(user__username__contains="@mobi")
.filter(course_session__course__configuration__is_uk=True)
.filter(role=CourseSessionUser.Role.MEMBER.value)
.exclude(course_session_id__in=[4, 5, 6])
):
AgentParticipantRelation.objects.get_or_create(
agent=berufsbildner,
participant=csu,
role=AgentParticipantRoleType.BERUFSBILDNER.value,
)
if __name__ == "__main__":
main()

View File

@ -39,9 +39,9 @@ def send_learning_mentor_invitation():
recipient_email="daniel.egger+sendgrid@gmail.com", recipient_email="daniel.egger+sendgrid@gmail.com",
template=EmailTemplate.LEARNING_MENTOR_INVITATION, template=EmailTemplate.LEARNING_MENTOR_INVITATION,
template_data={ template_data={
"inviter_name": f"Daniel Egger", "inviter_name": "Daniel Egger",
"inviter_email": "daniel.egger@example.com", "inviter_email": "daniel.egger@example.com",
"target_url": f"https://stage.vbv-afa.ch/foobar", "target_url": "https://stage.vbv-afa.ch/foobar",
}, },
template_language="de", template_language="de",
fail_silently=True, fail_silently=True,

View File

@ -101,7 +101,7 @@ class AssignmentCompletionMutation(graphene.Mutation):
): ):
if not can_evaluate_assignments( if not can_evaluate_assignments(
evaluation_user=info.context.user, evaluation_user=info.context.user,
assignment_user_id=assignment_user_id, assignment_user_ids=[assignment_user_id],
course_session_id=course_session_id, course_session_id=course_session_id,
): ):
raise PermissionDenied() raise PermissionDenied()

View File

@ -21,6 +21,7 @@ class AssignmentCompletionObjectType(DjangoObjectType):
evaluation_points = graphene.Float() evaluation_points = graphene.Float()
evaluation_points_final = graphene.Float() evaluation_points_final = graphene.Float()
evaluation_max_points = graphene.Float() evaluation_max_points = graphene.Float()
evaluation_percent = graphene.Float()
class Meta: class Meta:
model = AssignmentCompletion model = AssignmentCompletion
@ -61,6 +62,11 @@ class AssignmentCompletionObjectType(DjangoObjectType):
return round(self.evaluation_max_points, 1) # noqa return round(self.evaluation_max_points, 1) # noqa
return None return None
def resolve_evaluation_percent(self, info):
if self.evaluation_points:
return self.evaluation_percent
return None
class AssignmentObjectType(DjangoObjectType): class AssignmentObjectType(DjangoObjectType):
tasks = JSONStreamField() tasks = JSONStreamField()
@ -69,11 +75,11 @@ class AssignmentObjectType(DjangoObjectType):
max_points = graphene.Int() max_points = graphene.Int()
competence_certificate_weight = graphene.Float() competence_certificate_weight = graphene.Float()
learning_content = graphene.Field(LearningContentInterface) learning_content = graphene.Field(LearningContentInterface)
completion = graphene.Field( completions = graphene.List(
AssignmentCompletionObjectType, AssignmentCompletionObjectType,
course_session_id=graphene.ID(required=True), course_session_id=graphene.ID(required=True),
learning_content_page_id=graphene.ID(required=False), learning_content_page_id=graphene.ID(required=False),
assignment_user_id=graphene.UUID(required=False), assignment_user_ids=graphene.List(graphene.UUID, required=False),
) )
solution_sample = graphene.Field(ContentDocumentObjectType) solution_sample = graphene.Field(ContentDocumentObjectType)
@ -103,28 +109,33 @@ class AssignmentObjectType(DjangoObjectType):
def resolve_learning_content(self, info): def resolve_learning_content(self, info):
return self.find_attached_learning_content() return self.find_attached_learning_content()
def resolve_completion( def resolve_completions(
self, self,
info, info,
course_session_id, course_session_id,
learning_content_page_id=None, learning_content_page_id=None,
assignment_user_id=None, assignment_user_ids=None,
): ):
if learning_content_page_id is None: if learning_content_page_id is None:
lp = self.find_attached_learning_content() lp = self.find_attached_learning_content()
if lp: if lp:
learning_content_page_id = lp.id learning_content_page_id = lp.id
if not assignment_user_id: if not assignment_user_ids:
assignment_user_id = getattr(info.context, "assignment_user_id", None) assignment_user_ids = getattr(info.context, "assignment_user_ids", [])
return resolve_assignment_completion( completions = []
info=info, for user_id in assignment_user_ids:
course_session_id=course_session_id, completion = resolve_assignment_completion(
learning_content_page_id=learning_content_page_id, info=info,
assignment_user_id=assignment_user_id, course_session_id=course_session_id,
assignment_id=self.id, learning_content_page_id=learning_content_page_id,
) assignment_user_id=user_id,
assignment_id=self.id,
)
if completion:
completions.append(completion)
return completions
def resolve_assignment_completion( def resolve_assignment_completion(
@ -139,7 +150,7 @@ def resolve_assignment_completion(
if str(assignment_user_id) == str(info.context.user.id) or can_evaluate_assignments( if str(assignment_user_id) == str(info.context.user.id) or can_evaluate_assignments(
evaluation_user=info.context.user, evaluation_user=info.context.user,
assignment_user_id=assignment_user_id, assignment_user_ids=[assignment_user_id],
course_session_id=course_session_id, course_session_id=course_session_id,
): ):
course_id = CourseSession.objects.get(id=course_session_id).course_id course_id = CourseSession.objects.get(id=course_session_id).course_id

View File

@ -368,6 +368,10 @@ class AssignmentCompletion(models.Model):
return None return None
return self.evaluation_points - self.evaluation_points_deducted return self.evaluation_points - self.evaluation_points_deducted
@property
def evaluation_percent(self):
return (self.evaluation_points_final or 0) / (self.evaluation_max_points or 1)
assignment_user = models.ForeignKey(User, on_delete=models.CASCADE) assignment_user = models.ForeignKey(User, on_delete=models.CASCADE)
assignment = models.ForeignKey(Assignment, on_delete=models.CASCADE) assignment = models.ForeignKey(Assignment, on_delete=models.CASCADE)
course_session = models.ForeignKey("course.CourseSession", on_delete=models.CASCADE) course_session = models.ForeignKey("course.CourseSession", on_delete=models.CASCADE)

View File

@ -9,14 +9,23 @@ from vbv_lernwelt.iam.permissions import can_evaluate_assignments
logger = structlog.get_logger(__name__) logger = structlog.get_logger(__name__)
@api_view(["GET"]) @api_view(["GET", "POST"])
def request_assignment_completion_status(request, assignment_id, course_session_id): def request_assignment_completion_status(request, assignment_id, course_session_id):
# TODO quickfix before GraphQL... # TODO quickfix before GraphQL...
if can_evaluate_assignments(request.user, course_session_id):
user_selection_ids = request.data.get("user_selection_ids", [])
if can_evaluate_assignments(
request.user, course_session_id, assignment_user_ids=user_selection_ids
):
qs = AssignmentCompletion.objects.filter( qs = AssignmentCompletion.objects.filter(
course_session_id=course_session_id, course_session_id=course_session_id,
assignment_id=assignment_id, assignment_id=assignment_id,
).values( )
if len(user_selection_ids) > 0:
qs = qs.filter(assignment_user_id__in=user_selection_ids)
values = qs.values(
"id", "id",
"assignment_user_id", "assignment_user_id",
"completion_status", "completion_status",
@ -28,7 +37,7 @@ def request_assignment_completion_status(request, assignment_id, course_session_
) )
# Convert the learning_content_page_id to a string # Convert the learning_content_page_id to a string
data = list(qs) # Evaluate the queryset data = list(values) # Evaluate the queryset
for item in data: for item in data:
if item["evaluation_points"] is not None: if item["evaluation_points"] is not None:
# only `evaluation_points_final` is relevant for the frontend # only `evaluation_points_final` is relevant for the frontend

View File

@ -24,47 +24,23 @@ class CompetenceCertificateQuery(object):
slug=graphene.String(), slug=graphene.String(),
course_id=graphene.ID(), course_id=graphene.ID(),
course_slug=graphene.String(), course_slug=graphene.String(),
) user_ids=graphene.List(graphene.UUID),
competence_certificate_list_for_user = graphene.Field(
CompetenceCertificateListObjectType,
id=graphene.ID(),
slug=graphene.String(),
course_id=graphene.ID(),
course_slug=graphene.String(),
user_id=graphene.UUID(),
) )
def resolve_competence_certificate(root, info, id=None, slug=None): def resolve_competence_certificate(root, info, id=None, slug=None):
return resolve_course_page(CompetenceCertificate, root, info, id=id, slug=slug) return resolve_course_page(CompetenceCertificate, root, info, id=id, slug=slug)
def resolve_competence_certificate_list( def resolve_competence_certificate_list(
root, info, id=None, slug=None, course_id=None, course_slug=None root, info, id=None, slug=None, course_id=None, course_slug=None, user_ids=None
): ):
return resolve_course_page( for user_id in user_ids:
CompetenceCertificateList,
root,
info,
id=id,
slug=slug,
course_id=course_id,
course_slug=course_slug,
)
def resolve_competence_certificate_list_for_user(
root, info, id=None, slug=None, course_id=None, course_slug=None, user_id=None
):
try:
course_session_user = CourseSessionUser.objects.filter( course_session_user = CourseSessionUser.objects.filter(
user__id=user_id user__id=user_id
).first() ).first()
except CourseSessionUser.DoesNotExist: if not can_view_profile(info.context.user, course_session_user):
return None return None
if not can_view_profile(info.context.user, course_session_user): setattr(info.context, "assignment_user_ids", user_ids)
return None
setattr(info.context, "assignment_user_id", user_id)
return resolve_course_page( return resolve_course_page(
CompetenceCertificateList, CompetenceCertificateList,

View File

@ -17,6 +17,7 @@ def query_competence_course_session_assignments(course_session_ids, circle_ids=N
AssignmentType.CASEWORK.value, AssignmentType.CASEWORK.value,
], ],
learning_content__content_assignment__competence_certificate__isnull=False, learning_content__content_assignment__competence_certificate__isnull=False,
learning_content__live=True,
).select_related( ).select_related(
"submission_deadline", "submission_deadline",
"learning_content", "learning_content",
@ -39,6 +40,7 @@ def query_competence_course_session_edoniq_tests(course_session_ids, circle_ids=
for cset in CourseSessionEdoniqTest.objects.filter( for cset in CourseSessionEdoniqTest.objects.filter(
course_session_id__in=course_session_ids, course_session_id__in=course_session_ids,
learning_content__content_assignment__competence_certificate__isnull=False, learning_content__content_assignment__competence_certificate__isnull=False,
learning_content__live=True,
).select_related( ).select_related(
"deadline", "deadline",
"learning_content", "learning_content",

View File

@ -71,177 +71,23 @@ class TestCertificateList(GraphQLTestCase):
def test_supervisor_userprofile_certificate_summary(self): def test_supervisor_userprofile_certificate_summary(self):
self.client.force_login(self.supervisor) self.client.force_login(self.supervisor)
query = f"""query competenceCertificateForUserQuery( query = """query competenceCertificateForUserQuery(
$courseSlug: String!, $courseSlug: String!,
$courseSessionId: ID!, $courseSessionId: ID!,
$userId: UUID! $userIds: [UUID!]
) {{ ) {
competence_certificate_list_for_user(
course_slug: $courseSlug,
user_id: $userId
) {{
...CoursePageFields
competence_certificates {{
...CoursePageFields
assignments {{
...CoursePageFields
assignment_type
max_points
completion(course_session_id: $courseSessionId) {{
id
completion_status
submitted_at
evaluation_points
evaluation_max_points
evaluation_passed
__typename
}}
learning_content {{
...CoursePageFields
circle {{
id
title
slug
__typename
}}
__typename
}}
__typename
}}
__typename
}}
__typename
}}
}}
fragment CoursePageFields on CoursePageInterface {{
title
id
slug
content_type
frontend_url
__typename
}}
"""
variables = {
"courseSessionId": str(self.course_session.id),
"courseSlug": self.course.slug,
"userId": str(self.member_one.id),
}
# WHEN
response = self.query(query, variables=variables)
# THEN
self.assertResponseNoErrors(response)
certificates = response.json()["data"]["competence_certificate_list_for_user"][
"competence_certificates"
]
self.assertEqual(len(certificates), 1)
assignments = certificates[0]["assignments"]
self.assertEqual(len(assignments), 2)
completion1 = assignments[0]["completion"]
self.assertEqual(completion1["completion_status"], "SUBMITTED")
self.assertEqual(completion1["evaluation_points"], 5)
self.assertEqual(completion1["evaluation_max_points"], 10)
self.assertEqual(completion1["evaluation_passed"], False)
completion2 = assignments[1]["completion"]
self.assertIsNone(completion2)
def test_member_cannot_see_other_user_certificate_summary(self):
self.client.force_login(self.member_one)
query = f"""query competenceCertificateForUserQuery(
$courseSlug: String!,
$courseSessionId: ID!,
$userId: UUID!
) {{
competence_certificate_list_for_user(
course_slug: $courseSlug,
user_id: $userId
) {{
...CoursePageFields
competence_certificates {{
...CoursePageFields
assignments {{
...CoursePageFields
assignment_type
max_points
completion(course_session_id: $courseSessionId) {{
id
completion_status
submitted_at
evaluation_points
evaluation_max_points
evaluation_passed
__typename
}}
learning_content {{
...CoursePageFields
circle {{
id
title
slug
__typename
}}
__typename
}}
__typename
}}
__typename
}}
__typename
}}
}}
fragment CoursePageFields on CoursePageInterface {{
title
id
slug
content_type
frontend_url
__typename
}}
"""
variables = {
"courseSessionId": str(self.course_session.id),
"courseSlug": self.course.slug,
"userId": str(self.member_two.id),
}
# WHEN
response = self.query(query, variables=variables)
# THEN
self.assertResponseNoErrors(response)
self.assertIsNone(
response.json()["data"]["competence_certificate_list_for_user"]
)
def test_member_userprofile_certificate_summary(self):
self.client.force_login(self.member_one)
query = f"""query competenceCertificateForUserQuery(
$courseSlug: String!,
$courseSessionId: ID!,
) {{
competence_certificate_list( competence_certificate_list(
course_slug: $courseSlug, course_slug: $courseSlug,
) {{ user_ids: $userIds
) {
...CoursePageFields ...CoursePageFields
competence_certificates {{ competence_certificates {
...CoursePageFields ...CoursePageFields
assignments {{ assignments {
...CoursePageFields ...CoursePageFields
assignment_type assignment_type
max_points max_points
completion(course_session_id: $courseSessionId) {{ completions(course_session_id: $courseSessionId) {
id id
completion_status completion_status
submitted_at submitted_at
@ -249,38 +95,39 @@ fragment CoursePageFields on CoursePageInterface {{
evaluation_max_points evaluation_max_points
evaluation_passed evaluation_passed
__typename __typename
}} }
learning_content {{ learning_content {
...CoursePageFields ...CoursePageFields
circle {{ circle {
id id
title title
slug slug
__typename __typename
}} }
__typename __typename
}} }
__typename __typename
}} }
__typename __typename
}} }
__typename __typename
}} }
}} }
fragment CoursePageFields on CoursePageInterface {{ fragment CoursePageFields on CoursePageInterface {
title title
id id
slug slug
content_type content_type
frontend_url frontend_url
__typename __typename
}} }
""" """
variables = { variables = {
"courseSessionId": str(self.course_session.id), "courseSessionId": str(self.course_session.id),
"courseSlug": self.course.slug, "courseSlug": self.course.slug,
"userIds": [str(self.member_one.id)],
} }
# WHEN # WHEN
@ -297,11 +144,165 @@ fragment CoursePageFields on CoursePageInterface {{
assignments = certificates[0]["assignments"] assignments = certificates[0]["assignments"]
self.assertEqual(len(assignments), 2) self.assertEqual(len(assignments), 2)
completion1 = assignments[0]["completion"] completion1 = assignments[0]["completions"][0]
self.assertEqual(completion1["completion_status"], "SUBMITTED") self.assertEqual(completion1["completion_status"], "SUBMITTED")
self.assertEqual(completion1["evaluation_points"], 5) self.assertEqual(completion1["evaluation_points"], 5)
self.assertEqual(completion1["evaluation_max_points"], 10) self.assertEqual(completion1["evaluation_max_points"], 10)
self.assertEqual(completion1["evaluation_passed"], False) self.assertEqual(completion1["evaluation_passed"], False)
completion2 = assignments[1]["completion"] completion2 = assignments[1]["completions"]
self.assertIsNone(completion2) self.assertEqual(len(completion2), 0)
def test_member_cannot_see_other_user_certificate_summary(self):
self.client.force_login(self.member_one)
query = """query competenceCertificateForUserQuery(
$courseSlug: String!,
$courseSessionId: ID!,
$userIds: [UUID!]
) {
competence_certificate_list(
course_slug: $courseSlug,
user_ids: $userIds
) {
...CoursePageFields
competence_certificates {
...CoursePageFields
assignments {
...CoursePageFields
assignment_type
max_points
completions(course_session_id: $courseSessionId) {
id
completion_status
submitted_at
evaluation_points
evaluation_max_points
evaluation_passed
__typename
}
learning_content {
...CoursePageFields
circle {
id
title
slug
__typename
}
__typename
}
__typename
}
__typename
}
__typename
}
}
fragment CoursePageFields on CoursePageInterface {
title
id
slug
content_type
frontend_url
__typename
}
"""
variables = {
"courseSessionId": str(self.course_session.id),
"courseSlug": self.course.slug,
"userIds": [str(self.member_two.id)],
}
# WHEN
response = self.query(query, variables=variables)
# THEN
self.assertResponseNoErrors(response)
self.assertIsNone(response.json()["data"]["competence_certificate_list"])
def test_member_userprofile_certificate_summary(self):
self.client.force_login(self.member_one)
query = """query competenceCertificateForUserQuery(
$courseSlug: String!,
$courseSessionId: ID!,
$userIds: [UUID!],
) {
competence_certificate_list(
course_slug: $courseSlug,
user_ids: $userIds
) {
...CoursePageFields
competence_certificates {
...CoursePageFields
assignments {
...CoursePageFields
assignment_type
max_points
completions(course_session_id: $courseSessionId) {
id
completion_status
submitted_at
evaluation_points
evaluation_max_points
evaluation_passed
__typename
}
learning_content {
...CoursePageFields
circle {
id
title
slug
__typename
}
__typename
}
__typename
}
__typename
}
__typename
}
}
fragment CoursePageFields on CoursePageInterface {
title
id
slug
content_type
frontend_url
__typename
}
"""
variables = {
"courseSessionId": str(self.course_session.id),
"courseSlug": self.course.slug,
"userIds": [str(self.member_one.id)],
}
# WHEN
response = self.query(query, variables=variables)
# THEN
self.assertResponseNoErrors(response)
certificates = response.json()["data"]["competence_certificate_list"][
"competence_certificates"
]
self.assertEqual(len(certificates), 1)
assignments = certificates[0]["assignments"]
self.assertEqual(len(assignments), 2)
completion1 = assignments[0]["completions"][0]
self.assertEqual(completion1["completion_status"], "SUBMITTED")
self.assertEqual(completion1["evaluation_points"], 5)
self.assertEqual(completion1["evaluation_max_points"], 10)
self.assertEqual(completion1["evaluation_passed"], False)
completion2 = assignments[1]["completions"]
self.assertEqual(len(completion2), 0)

View File

@ -1,4 +1,4 @@
from django.contrib import admin from django.contrib import admin, messages
from django.contrib.auth import admin as auth_admin, get_user_model from django.contrib.auth import admin as auth_admin, get_user_model
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -10,6 +10,9 @@ 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_berufsbildner as create_or_sync_bb,
)
User = get_user_model() User = get_user_model()
@ -31,6 +34,26 @@ 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,
f"Berufsbildner erfolgreich erstellt oder synchronisiert",
)
else:
messages.add_message(
request,
messages.ERROR,
f"Einige Berufsbildner konnten nicht erstellt oder synchronisiert werden",
)
@admin.register(User) @admin.register(User)
class UserAdmin(auth_admin.UserAdmin): class UserAdmin(auth_admin.UserAdmin):
fieldsets = ( fieldsets = (
@ -91,6 +114,7 @@ class UserAdmin(auth_admin.UserAdmin):
"sso_id", "sso_id",
] ]
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]
@admin.register(JobLog) @admin.register(JobLog)

View File

@ -43,7 +43,10 @@ from vbv_lernwelt.course.services import mark_course_completion
from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse
from vbv_lernwelt.course_session.services.attendance import AttendanceUserStatus from vbv_lernwelt.course_session.services.attendance import AttendanceUserStatus
from vbv_lernwelt.feedback.models import FeedbackResponse from vbv_lernwelt.feedback.models import FeedbackResponse
from vbv_lernwelt.learning_mentor.models import LearningMentor from vbv_lernwelt.learning_mentor.models import (
AgentParticipantRelation,
MentorInvitation,
)
from vbv_lernwelt.learnpath.models import ( from vbv_lernwelt.learnpath.models import (
LearningContentAttendanceCourse, LearningContentAttendanceCourse,
LearningContentFeedbackUK, LearningContentFeedbackUK,
@ -186,7 +189,9 @@ def command(
SelfEvaluationFeedback.objects.all().delete() SelfEvaluationFeedback.objects.all().delete()
CourseCompletionFeedback.objects.all().delete() CourseCompletionFeedback.objects.all().delete()
LearningMentor.objects.all().delete() AgentParticipantRelation.objects.all().delete()
# LearningMentor.objects.all().delete()
MentorInvitation.objects.all().delete()
User.objects.all().update(organisation=Organisation.objects.first()) User.objects.all().update(organisation=Organisation.objects.first())
User.objects.all().update(language="de") User.objects.all().update(language="de")
User.objects.all().update(additional_json_data={}) User.objects.all().update(additional_json_data={})
@ -461,48 +466,40 @@ def command(
if create_learning_mentor: if create_learning_mentor:
cs_bern = CourseSession.objects.get(id=TEST_COURSE_SESSION_BERN_ID) cs_bern = CourseSession.objects.get(id=TEST_COURSE_SESSION_BERN_ID)
AgentParticipantRelation.objects.create(
uk_mentor = LearningMentor.objects.create( agent=User.objects.get(id=TEST_MENTOR1_USER_ID),
mentor=User.objects.get(id=TEST_MENTOR1_USER_ID), participant=CourseSessionUser.objects.get(
course_session=cs_bern, user__id=TEST_STUDENT1_USER_ID, course_session=cs_bern
) ),
uk_mentor.participants.add( role="LEARNING_MENTOR",
CourseSessionUser.objects.get(
user__id=TEST_STUDENT1_USER_ID,
course_session=cs_bern,
)
) )
vv_course = Course.objects.get(id=COURSE_VERSICHERUNGSVERMITTLERIN_ID) vv_course = Course.objects.get(id=COURSE_VERSICHERUNGSVERMITTLERIN_ID)
vv_course_session = CourseSession.objects.get(course=vv_course) vv_course_session = CourseSession.objects.get(course=vv_course)
vv_mentor = LearningMentor.objects.create(
mentor=User.objects.get(id=TEST_MENTOR1_USER_ID),
course_session=vv_course_session,
)
vv_mentor.participants.add( AgentParticipantRelation.objects.create(
CourseSessionUser.objects.get( agent=User.objects.get(id=TEST_MENTOR1_USER_ID),
user__id=TEST_STUDENT1_VV_USER_ID, course_session=vv_course_session participant=CourseSessionUser.objects.get(
)
)
vv_mentor.participants.add(
CourseSessionUser.objects.get(
user__id=TEST_STUDENT2_VV_AND_VV_MENTOR_USER_ID,
course_session=vv_course_session,
)
)
vv_student_and_mentor = LearningMentor.objects.create(
mentor=User.objects.get(id=TEST_STUDENT2_VV_AND_VV_MENTOR_USER_ID),
course_session=vv_course_session,
)
vv_student_and_mentor.participants.add(
CourseSessionUser.objects.get(
user__id=TEST_STUDENT1_VV_USER_ID, user__id=TEST_STUDENT1_VV_USER_ID,
course_session=vv_course_session, course_session=vv_course_session,
) ),
role="LEARNING_MENTOR",
)
AgentParticipantRelation.objects.create(
agent=User.objects.get(id=TEST_MENTOR1_USER_ID),
participant=CourseSessionUser.objects.get(
user__id=TEST_STUDENT2_VV_AND_VV_MENTOR_USER_ID,
course_session=vv_course_session,
),
role="LEARNING_MENTOR",
)
AgentParticipantRelation.objects.create(
agent=User.objects.get(id=TEST_STUDENT2_VV_AND_VV_MENTOR_USER_ID),
participant=CourseSessionUser.objects.get(
user__id=TEST_STUDENT1_VV_USER_ID,
course_session=vv_course_session,
),
role="LEARNING_MENTOR",
) )
course = Course.objects.get(id=COURSE_TEST_ID) course = Course.objects.get(id=COURSE_TEST_ID)

View File

@ -24,7 +24,10 @@ from vbv_lernwelt.course_session.models import (
) )
from vbv_lernwelt.course_session_group.models import CourseSessionGroup from vbv_lernwelt.course_session_group.models import CourseSessionGroup
from vbv_lernwelt.feedback.models import FeedbackResponse from vbv_lernwelt.feedback.models import FeedbackResponse
from vbv_lernwelt.learning_mentor.models import LearningMentor from vbv_lernwelt.learning_mentor.models import (
AgentParticipantRelation,
AgentParticipantRoleType,
)
from vbv_lernwelt.learnpath.models import Circle from vbv_lernwelt.learnpath.models import Circle
from vbv_lernwelt.notify.models import Notification from vbv_lernwelt.notify.models import Notification
@ -71,7 +74,7 @@ def create_or_update_uk(language="de"):
cs = CourseSession.objects.get(import_id=data["ID"]) cs = CourseSession.objects.get(import_id=data["ID"])
members, trainer, regionenleiter = get_or_create_users_uk() members, trainer, regionenleiter = get_or_create_users_uk()
delete_cs_data(cs, members + [trainer, regionenleiter]) delete_cs_data(cs)
add_to_course_session(cs, members) add_to_course_session(cs, members)
add_trainers_to_course_session(cs, [trainer], uk_circle_keys, language) add_trainers_to_course_session(cs, [trainer], uk_circle_keys, language)
@ -89,13 +92,13 @@ def create_or_update_vv(language="de"):
create_or_update_assignment_course_session(cs) create_or_update_assignment_course_session(cs)
members, member_with_mentor, mentor = get_or_create_users_vv() members, member_with_mentor, mentor = get_or_create_users_vv()
delete_cs_data(cs, members + [member_with_mentor, mentor]) delete_cs_data(cs)
add_to_course_session(cs, members + [member_with_mentor]) add_to_course_session(cs, members + [member_with_mentor])
add_mentor_to_course_session(cs, [(mentor, member_with_mentor)]) add_mentor_to_course_session(cs, [(mentor, member_with_mentor)])
def delete_cs_data(cs: CourseSession, users: list[User]): def delete_cs_data(cs: CourseSession):
if cs: if cs:
CourseCompletion.objects.filter(course_session=cs).delete() CourseCompletion.objects.filter(course_session=cs).delete()
Notification.objects.filter(course_session=cs).delete() Notification.objects.filter(course_session=cs).delete()
@ -105,16 +108,8 @@ def delete_cs_data(cs: CourseSession, users: list[User]):
) )
CourseSessionEdoniqTest.objects.filter(course_session=cs).delete() CourseSessionEdoniqTest.objects.filter(course_session=cs).delete()
CourseSessionUser.objects.filter(course_session=cs).delete() CourseSessionUser.objects.filter(course_session=cs).delete()
learning_mentor_ids = (
LearningMentor.objects.filter(participants__course_session=cs) AgentParticipantRelation.objects.filter(course_session=cs).delete()
.values_list("id", flat=True)
.distinct()
| LearningMentor.objects.filter(mentor__in=users)
.values_list("id", flat=True)
.distinct()
)
# cannot call delete on distinct objects
LearningMentor.objects.filter(id__in=list(learning_mentor_ids)).delete()
else: else:
logger.info("no_course_session_found", import_id=cs.import_id) logger.info("no_course_session_found", import_id=cs.import_id)
@ -138,15 +133,12 @@ def add_mentor_to_course_session(
course_session: CourseSession, mentor_mentee_pairs: list[tuple[User, User]] course_session: CourseSession, mentor_mentee_pairs: list[tuple[User, User]]
): ):
for mentor, mentee in mentor_mentee_pairs: for mentor, mentee in mentor_mentee_pairs:
lm = LearningMentor.objects.create( AgentParticipantRelation.objects.create(
course_session=course_session, agent=mentor,
mentor=mentor, participant=CourseSessionUser.objects.get(
) user__id=mentee.id, course_session=course_session
lm.participants.add( ),
CourseSessionUser.objects.get( role=AgentParticipantRoleType.LEARNING_MENTOR.value,
user__id=mentee.id,
course_session=course_session,
)
) )

View File

@ -139,6 +139,20 @@ class UserSerializer(serializers.ModelSerializer):
return instance return instance
class UserShortSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = [
"id",
"first_name",
"last_name",
"email",
"username",
"avatar_url",
"language",
]
class CypressUserSerializer(serializers.ModelSerializer): class CypressUserSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = User model = User

View File

@ -41,7 +41,10 @@ from vbv_lernwelt.course_session.models import (
) )
from vbv_lernwelt.course_session_group.models import CourseSessionGroup from vbv_lernwelt.course_session_group.models import CourseSessionGroup
from vbv_lernwelt.duedate.models import DueDate from vbv_lernwelt.duedate.models import DueDate
from vbv_lernwelt.learning_mentor.models import LearningMentor from vbv_lernwelt.learning_mentor.models import (
AgentParticipantRelation,
AgentParticipantRoleType,
)
from vbv_lernwelt.learnpath.models import ( from vbv_lernwelt.learnpath.models import (
Circle, Circle,
LearningContentAssignment, LearningContentAssignment,
@ -101,13 +104,13 @@ def create_course_session(
def add_learning_mentor( def add_learning_mentor(
course_session: CourseSession, mentor: User, mentee: CourseSessionUser mentor: User, mentee: CourseSessionUser
) -> LearningMentor: ) -> AgentParticipantRelation:
learning_mentor = LearningMentor.objects.create( return AgentParticipantRelation.objects.create(
course_session=course_session, mentor=mentor agent=mentor,
participant=mentee,
role=AgentParticipantRoleType.LEARNING_MENTOR.value,
) )
learning_mentor.participants.add(mentee)
return learning_mentor
def add_course_session_user( def add_course_session_user(

View File

@ -45,10 +45,7 @@ from vbv_lernwelt.competence.create_vv_new_competence_profile import (
create_vv_new_competence_profile, create_vv_new_competence_profile,
) )
from vbv_lernwelt.competence.models import PerformanceCriteria from vbv_lernwelt.competence.models import PerformanceCriteria
from vbv_lernwelt.core.constants import ( from vbv_lernwelt.core.constants import TEST_STUDENT2_VV_AND_VV_MENTOR_USER_ID
TEST_MENTOR1_USER_ID,
TEST_STUDENT2_VV_AND_VV_MENTOR_USER_ID,
)
from vbv_lernwelt.core.create_default_users import default_users from vbv_lernwelt.core.create_default_users import default_users
from vbv_lernwelt.core.models import User from vbv_lernwelt.core.models import User
from vbv_lernwelt.course.consts import ( from vbv_lernwelt.course.consts import (
@ -95,7 +92,6 @@ from vbv_lernwelt.importer.services import (
import_students_from_excel, import_students_from_excel,
import_trainers_from_excel_for_training, import_trainers_from_excel_for_training,
) )
from vbv_lernwelt.learning_mentor.models import LearningMentor
from vbv_lernwelt.learnpath.create_vv_new_learning_path import ( from vbv_lernwelt.learnpath.create_vv_new_learning_path import (
create_vv_motorfahrzeug_pruefung_learning_path, create_vv_motorfahrzeug_pruefung_learning_path,
create_vv_new_learning_path, create_vv_new_learning_path,
@ -245,6 +241,11 @@ def create_versicherungsvermittlerin_course(
course_session=cs, course_session=cs,
user=User.objects.get(username="student-vv@eiger-versicherungen.ch"), user=User.objects.get(username="student-vv@eiger-versicherungen.ch"),
) )
mentor_and_student_2_learning_csu = CourseSessionUser.objects.create(
course_session=cs,
user=User.objects.get(id=TEST_STUDENT2_VV_AND_VV_MENTOR_USER_ID),
role=CourseSessionUser.Role.MEMBER,
)
CourseSessionUser.objects.create( CourseSessionUser.objects.create(
course_session=cs, course_session=cs,
@ -264,30 +265,6 @@ def create_versicherungsvermittlerin_course(
role=CourseSessionUser.Role.EXPERT, role=CourseSessionUser.Role.EXPERT,
) )
mentor_and_student_2_learning_csu = CourseSessionUser.objects.create(
course_session=cs,
user=User.objects.get(id=TEST_STUDENT2_VV_AND_VV_MENTOR_USER_ID),
role=CourseSessionUser.Role.MEMBER,
)
# TEST_MENTOR1_USER_ID is only mentor
just_mentor = LearningMentor.objects.create(
mentor=User.objects.get(id=TEST_MENTOR1_USER_ID),
course_session=cs,
)
just_mentor.participants.add(student_1_csu)
just_mentor.participants.add(mentor_and_student_2_learning_csu)
# TEST_STUDENT2_VV_AND_VV_MENTOR_USER_ID is both student and mentor
mentor_and_student_learning_mentor = LearningMentor.objects.create(
mentor=User.objects.get(id=TEST_STUDENT2_VV_AND_VV_MENTOR_USER_ID),
course_session=cs,
)
mentor_and_student_learning_mentor.participants.add(student_1_csu)
for admin_email in ADMIN_EMAILS: for admin_email in ADMIN_EMAILS:
CourseSessionUser.objects.create( CourseSessionUser.objects.create(
course_session=cs, course_session=cs,

View File

@ -0,0 +1,27 @@
# Generated by Django 3.2.25 on 2024-07-17 14:53
from django.db import migrations, models
import vbv_lernwelt.course.models
class Migration(migrations.Migration):
dependencies = [
("course", "0008_auto_20240403_1132"),
]
operations = [
migrations.AlterField(
model_name="coursecompletion",
name="completion_status",
field=models.CharField(
choices=[
("SUCCESS", "Success"),
("FAIL", "Fail"),
("UNKNOWN", "Unknown"),
],
default=vbv_lernwelt.course.models.CourseCompletionStatus["UNKNOWN"],
max_length=255,
),
),
]

View File

@ -0,0 +1,12 @@
# Generated by Django 4.2.13 on 2024-08-05 05:31
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("course", "0009_alter_coursecompletion_completion_status"),
("course", "0009_coursesessionuser_required_attendance_and_more"),
]
operations = []

View File

@ -0,0 +1,12 @@
# Generated by Django 4.2.13 on 2024-08-10 11:36
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("course", "0010_merge_20240805_0731"),
("course", "0011_merge_20240807_1317"),
]
operations = []

View File

@ -19,7 +19,7 @@ class CourseGraphQLTestCase(TestCase):
create_versicherungsvermittlerin_course() create_versicherungsvermittlerin_course()
def test_update_course_profile(self): def test_update_course_profile(self):
user = User.objects.get(username="student-vv@eiger-versicherungen.ch") user = User.objects.get(username="test-student-and-mentor2@example.com")
request = RequestFactory().get("/") request = RequestFactory().get("/")
request.user = user request.user = user
client = Client(schema=schema, context_value=request) client = Client(schema=schema, context_value=request)
@ -98,7 +98,7 @@ class CourseGraphQLTestCase(TestCase):
self.assertEqual(chosen_profile, profile) self.assertEqual(chosen_profile, profile)
def test_mentor_profile_view(self): def test_mentor_profile_view(self):
user = User.objects.get(username="test-mentor1@example.com") user = User.objects.get(username="test-student-and-mentor2@example.com")
request = RequestFactory().get("/") request = RequestFactory().get("/")
request.user = user request.user = user
client = Client(schema=schema, context_value=request) client = Client(schema=schema, context_value=request)

View File

@ -27,7 +27,7 @@ from vbv_lernwelt.iam.permissions import (
has_course_access_by_page_request, has_course_access_by_page_request,
is_circle_expert, is_circle_expert,
) )
from vbv_lernwelt.learning_mentor.models import LearningMentor from vbv_lernwelt.learning_mentor.models import AgentParticipantRelation
logger = structlog.get_logger(__name__) logger = structlog.get_logger(__name__)
@ -155,9 +155,10 @@ def get_course_sessions(request):
# enrich with mentor course sessions # enrich with mentor course sessions
mentor_course_sessions = CourseSession.objects.filter( mentor_course_sessions = CourseSession.objects.filter(
id__in=LearningMentor.objects.filter(mentor=request.user).values_list( id__in=[
"course_session", flat=True rel.participant.course_session_id
) for rel in AgentParticipantRelation.objects.filter(agent=request.user)
]
).prefetch_related("course") ).prefetch_related("course")
all_to_serialize = ( all_to_serialize = (

View File

@ -25,17 +25,48 @@ from vbv_lernwelt.iam.permissions import (
can_view_course_session_group_statistics, can_view_course_session_group_statistics,
can_view_course_session_progress, can_view_course_session_progress,
) )
from vbv_lernwelt.learning_mentor.models import LearningMentor from vbv_lernwelt.learning_mentor.models import (
AgentParticipantRelation,
AgentParticipantRoleType,
)
from vbv_lernwelt.learnpath.models import Circle from vbv_lernwelt.learnpath.models import Circle
def _agent_course_statistics(user, course_id: str, role: str):
course = Course.objects.get(id=course_id)
participant_ids = set()
course_session_ids = set()
relations_qs = AgentParticipantRelation.objects.filter(
agent=user,
role=role,
participant__course_session__course=course,
)
for relation in relations_qs:
participant_ids.add(relation.participant.user_id)
course_session_ids.add(relation.participant.course_session_id)
return CourseStatisticsType(
_id=f"{role}:{course.id}", # noqa
course_id=course.id, # noqa
course_title=course.title, # noqa
course_slug=course.slug, # noqa
course_session_selection_ids=list(course_session_ids), # noqa
user_selection_ids=list(participant_ids), # noqa
)
class DashboardQuery(graphene.ObjectType): class DashboardQuery(graphene.ObjectType):
course_statistics = graphene.Field( course_statistics = graphene.Field(
CourseStatisticsType, course_id=graphene.ID(required=True) CourseStatisticsType, course_id=graphene.ID(required=True)
) )
mentor_course_statistics = graphene.Field( mentor_course_statistics = graphene.Field(
BaseStatisticsType, course_id=graphene.ID(required=True) BaseStatisticsType,
course_id=graphene.ID(required=True),
agent_role=graphene.String(required=True),
) )
course_progress = graphene.Field( course_progress = graphene.Field(
@ -88,28 +119,12 @@ class DashboardQuery(graphene.ObjectType):
course_session_selection_ids=list(course_session_ids), # noqa course_session_selection_ids=list(course_session_ids), # noqa
) )
def resolve_mentor_course_statistics(root, info, course_id: str): # noqa def resolve_mentor_course_statistics(
root, info, course_id: str, agent_role: str
): # noqa
user = info.context.user user = info.context.user
course = Course.objects.get(id=course_id)
mentees_ids = set() return _agent_course_statistics(user, course_id, role=agent_role)
course_session_ids = set()
mentees = CourseSessionUser.objects.filter(
participants__mentor=user, course_session__course=course
).values_list("user", "course_session")
for user_id, course_session_id in mentees:
mentees_ids.add(user_id)
course_session_ids.add(course_session_id)
return CourseStatisticsType(
_id=f"mentor:{course.id}", # noqa
course_id=course.id, # noqa
course_title=course.title, # noqa
course_slug=course.slug, # noqa
course_session_selection_ids=list(course_session_ids), # noqa
user_selection_ids=list(mentees_ids), # noqa
)
def resolve_dashboard_config(root, info): # noqa def resolve_dashboard_config(root, info): # noqa
user = info.context.user user = info.context.user
@ -249,29 +264,31 @@ def get_user_statistics_dashboards(user: User) -> Tuple[List[Dict[str, str]], Se
def get_learning_mentor_dashboards( def get_learning_mentor_dashboards(
user: User, exclude_course_ids: Set[int] user: User, exclude_course_ids: Set[int]
) -> Tuple[List[Dict[str, str]], Set[int]]: ) -> Tuple[List[Dict[str, str]], Set[int]]:
learning_mentor = LearningMentor.objects.filter(mentor=user).exclude( learning_mentor_relation_qs = AgentParticipantRelation.objects.filter(
course_session__course__id__in=exclude_course_ids agent=user, role=AgentParticipantRoleType.LEARNING_MENTOR.value
) ).exclude(participant__course_session__course__id__in=exclude_course_ids)
dashboards = [] dashboards = []
course_ids = set() course_ids = set()
for mentor in learning_mentor: for rel in learning_mentor_relation_qs:
course = mentor.course_session.course course = rel.participant.course_session.course
course_ids.add(course.id)
if course.id in UK_COURSE_IDS: if course.id in UK_COURSE_IDS:
dashboard_type = DashboardType.PRAXISBILDNER_DASHBOARD dashboard_type = DashboardType.PRAXISBILDNER_DASHBOARD
else: else:
dashboard_type = DashboardType.MENTOR_DASHBOARD dashboard_type = DashboardType.MENTOR_DASHBOARD
dashboards.append(
{ if course.id not in course_ids:
"id": str(course.id), course_ids.add(course.id)
"name": course.title, dashboards.append(
"slug": course.slug, {
"dashboard_type": dashboard_type, "id": str(course.id),
"course_configuration": course.configuration, "name": course.title,
} "slug": course.slug,
) "dashboard_type": dashboard_type,
"course_configuration": course.configuration,
}
)
return dashboards, course_ids return dashboards, course_ids

View File

@ -26,6 +26,8 @@ class AssignmentCompletionMetricsType(graphene.ObjectType):
unranked_count = graphene.Int(required=True) unranked_count = graphene.Int(required=True)
ranking_completed = graphene.Boolean(required=True) ranking_completed = graphene.Boolean(required=True)
average_passed = graphene.Float(required=True) average_passed = graphene.Float(required=True)
average_evaluation_percent = graphene.Float()
competence_certificate_weight = graphene.Float()
class AssignmentStatisticsRecordType(graphene.ObjectType): class AssignmentStatisticsRecordType(graphene.ObjectType):
@ -34,10 +36,15 @@ class AssignmentStatisticsRecordType(graphene.ObjectType):
course_session_assignment_id = graphene.ID(required=True) course_session_assignment_id = graphene.ID(required=True)
circle_id = graphene.ID(required=True) circle_id = graphene.ID(required=True)
generation = graphene.String(required=True) generation = graphene.String(required=True)
region = graphene.String(required=True)
assignment_type_translation_key = graphene.String(required=True) assignment_type_translation_key = graphene.String(required=True)
assignment_title = graphene.String(required=True) assignment_title = graphene.String(required=True)
deadline = graphene.DateTime(required=True) deadline = graphene.DateTime(required=True)
course_session_title = graphene.String()
competence_certificate_id = graphene.ID()
competence_certificate_title = graphene.String()
metrics = graphene.Field(AssignmentCompletionMetricsType, required=True) metrics = graphene.Field(AssignmentCompletionMetricsType, required=True)
learning_content_id = graphene.ID(required=True)
details_url = graphene.String(required=True) details_url = graphene.String(required=True)
@ -47,6 +54,7 @@ class AssignmentStatisticsSummaryType(graphene.ObjectType):
average_passed = graphene.Float(required=True) average_passed = graphene.Float(required=True)
total_passed = graphene.Int(required=True) total_passed = graphene.Int(required=True)
total_failed = graphene.Int(required=True) total_failed = graphene.Int(required=True)
average_evaluation_percent = graphene.Float()
class AssignmentsStatisticsType(graphene.ObjectType): class AssignmentsStatisticsType(graphene.ObjectType):
@ -83,12 +91,17 @@ def create_assignment_summary(
total_passed = sum([m.passed_count for m in completed_metrics]) total_passed = sum([m.passed_count for m in completed_metrics])
total_failed = sum([m.failed_count for m in completed_metrics]) total_failed = sum([m.failed_count for m in completed_metrics])
total_average_evaluation_percent = (
sum([m.average_evaluation_percent for m in completed_metrics]) / completed_count
)
return AssignmentStatisticsSummaryType( return AssignmentStatisticsSummaryType(
_id=urql_id, # noqa _id=urql_id, # noqa
completed_count=completed_count, # noqa completed_count=completed_count, # noqa
average_passed=average_passed_completed, # noqa average_passed=average_passed_completed, # noqa
total_passed=total_passed, # noqa total_passed=total_passed, # noqa
total_failed=total_failed, # noqa total_failed=total_failed, # noqa
average_evaluation_percent=total_average_evaluation_percent, # noqa
) )
@ -102,28 +115,29 @@ def get_assignment_completion_metrics(
if not context: if not context:
context = {} context = {}
if user_selection_ids: csu_qs = CourseSessionUser.objects.filter(
course_session_users = user_selection_ids course_session_id=course_session.id, role=CourseSessionUser.Role.MEMBER
else: )
key = f"CourseSessionUser_{course_session.id}"
if not key in context:
course_session_users = CourseSessionUser.objects.filter(
course_session=course_session,
role=CourseSessionUser.Role.MEMBER,
).values_list("user", flat=True)
context[key] = course_session_users
else:
course_session_users = context[key]
evaluation_results = AssignmentCompletion.objects.filter( key = f"CourseSessionUsers_{course_session.id}"
if not key in context:
if user_selection_ids:
csu_qs = csu_qs.filter(user_id__in=user_selection_ids)
context[key] = list(csu_qs.values_list("user", flat=True))
course_session_users = context[key]
assignment_completions = AssignmentCompletion.objects.filter(
completion_status=AssignmentCompletionStatus.EVALUATION_SUBMITTED.value, completion_status=AssignmentCompletionStatus.EVALUATION_SUBMITTED.value,
assignment_user__in=course_session_users, assignment_user__in=course_session_users,
course_session=course_session, course_session=course_session,
assignment=assignment, assignment=assignment,
).values_list("evaluation_passed", flat=True) )
evaluation_passed_results = [ac.evaluation_passed for ac in assignment_completions]
passed_count = len([passed for passed in evaluation_results if passed]) passed_count = len([passed for passed in evaluation_passed_results if passed])
failed_count = len(evaluation_results) - passed_count failed_count = len(evaluation_passed_results) - passed_count
participants_count = len(course_session_users) participants_count = len(course_session_users)
unranked_count = participants_count - passed_count - failed_count unranked_count = participants_count - passed_count - failed_count
@ -133,6 +147,17 @@ def get_assignment_completion_metrics(
else: else:
average_passed = math.ceil(passed_count / participants_count * 100) average_passed = math.ceil(passed_count / participants_count * 100)
# calculate average points in percent
evaluation_percent_results = [
((ac.evaluation_points_final or 0) / (ac.evaluation_max_points or 1))
for ac in assignment_completions
]
average_evaluation_percent = (
sum(evaluation_percent_results) / (len(evaluation_percent_results) or 1)
if evaluation_percent_results
else 0
)
return AssignmentCompletionMetricsType( return AssignmentCompletionMetricsType(
_id=f"{course_session.id}-{assignment.id}@{urql_id_postfix}", # noqa _id=f"{course_session.id}-{assignment.id}@{urql_id_postfix}", # noqa
passed_count=passed_count, # noqa passed_count=passed_count, # noqa
@ -140,6 +165,8 @@ def get_assignment_completion_metrics(
unranked_count=unranked_count, # noqa unranked_count=unranked_count, # noqa
ranking_completed=(passed_count > 0 or failed_count > 0), # noqa ranking_completed=(passed_count > 0 or failed_count > 0), # noqa
average_passed=average_passed, # noqa average_passed=average_passed, # noqa
average_evaluation_percent=average_evaluation_percent, # noqa
competence_certificate_weight=assignment.competence_certificate_weight, # noqa
) )
@ -171,6 +198,7 @@ def create_record(
circle_id = context[key] circle_id = context[key]
learning_content = course_session_assignment.learning_content learning_content = course_session_assignment.learning_content
competence_certificate = learning_content.content_assignment.competence_certificate
return ( return (
AssignmentStatisticsRecordType( AssignmentStatisticsRecordType(
@ -178,12 +206,16 @@ def create_record(
_id=f"{course_session_assignment._meta.model_name}#{course_session_assignment.id}@{urql_id_postfix}", _id=f"{course_session_assignment._meta.model_name}#{course_session_assignment.id}@{urql_id_postfix}",
# noqa # noqa
course_session_id=str(course_session_assignment.course_session.id), # noqa course_session_id=str(course_session_assignment.course_session.id), # noqa
course_session_title=course_session_assignment.course_session.title, # noqa
circle_id=circle_id, # noqa circle_id=circle_id, # noqa
course_session_assignment_id=str(course_session_assignment.id), # noqa course_session_assignment_id=str(course_session_assignment.id), # noqa
generation=course_session_assignment.course_session.generation, # noqa generation=course_session_assignment.course_session.generation, # noqa
assignment_type_translation_key=due_date.assignment_type_translation_key, region=course_session_assignment.course_session.region, # noqa
# noqa assignment_type_translation_key=due_date.assignment_type_translation_key, # noqa
competence_certificate_id=str(competence_certificate.id), # noqa
competence_certificate_title=competence_certificate.title, # noqa
assignment_title=learning_content.content_assignment.title, # noqa assignment_title=learning_content.content_assignment.title, # noqa
learning_content_id=str(learning_content.id), # noqa
metrics=get_assignment_completion_metrics( # noqa metrics=get_assignment_completion_metrics( # noqa
course_session=course_session_assignment.course_session, # noqa course_session=course_session_assignment.course_session, # noqa
assignment=learning_content.content_assignment, # noqa assignment=learning_content.content_assignment, # noqa

View File

@ -19,6 +19,7 @@ class PresenceRecordStatisticsType(graphene.ObjectType):
_id = graphene.ID(required=True) _id = graphene.ID(required=True)
course_session_id = graphene.ID(required=True) course_session_id = graphene.ID(required=True)
generation = graphene.String(required=True) generation = graphene.String(required=True)
region = graphene.String(required=True)
circle_id = graphene.ID(required=True) circle_id = graphene.ID(required=True)
due_date = graphene.DateTime(required=True) due_date = graphene.DateTime(required=True)
participants_present = graphene.Int(required=True) participants_present = graphene.Int(required=True)
@ -83,6 +84,7 @@ def attendance_day_presences(
_id=f"{urql_id}:attendance_day:{attendance_day.id}", # noqa _id=f"{urql_id}:attendance_day:{attendance_day.id}", # noqa
course_session_id=course_session.id, # noqa course_session_id=course_session.id, # noqa
generation=course_session.generation, # noqa generation=course_session.generation, # noqa
region=course_session.region, # noqa
circle_id=circle.id, # noqa circle_id=circle.id, # noqa
due_date=attendance_day.due_date.end, # noqa due_date=attendance_day.due_date.end, # noqa
participants_present=participants_present, # noqa participants_present=participants_present, # noqa
@ -98,7 +100,9 @@ def attendance_day_presences(
) )
return AttendanceDayPresencesStatisticsType( return AttendanceDayPresencesStatisticsType(
summary=summary, records=records, _id=course_id # noqa summary=summary,
records=records,
_id=course_id, # noqa
) )

View File

@ -16,6 +16,7 @@ class CompetenceRecordStatisticsType(graphene.ObjectType):
_id = graphene.ID(required=True) _id = graphene.ID(required=True)
course_session_id = graphene.ID(required=True) course_session_id = graphene.ID(required=True)
generation = graphene.String(required=True) generation = graphene.String(required=True)
region = graphene.String(required=True)
title = graphene.String(required=True) title = graphene.String(required=True)
circle_id = graphene.ID(required=True) circle_id = graphene.ID(required=True)
success_count = graphene.Int(required=True) success_count = graphene.Int(required=True)
@ -71,23 +72,21 @@ def competences(
combined_id = f"{circle.id}-{completion.course_session.id}@{urql_id_postfix}" combined_id = f"{circle.id}-{completion.course_session.id}@{urql_id_postfix}"
if combined_id not in competence_records: competence_records.setdefault(combined_id, {}).setdefault(
competence_records[combined_id] = {} learning_unit,
CompetenceRecordStatisticsType(
if learning_unit not in competence_records[combined_id]: _id=combined_id, # noqa
competence_records[combined_id][ title=learning_unit.title, # noqa
learning_unit course_session_id=completion.course_session.id, # noqa
] = CompetenceRecordStatisticsType( generation=completion.course_session.generation, # noqa
_id=combined_id, region=completion.course_session.region, # noqa
title=learning_unit.title, circle_id=circle.id, # noqa
course_session_id=completion.course_session.id, success_count=0, # noqa
generation=completion.course_session.generation, fail_count=0, # noqa
circle_id=circle.id,
success_count=0,
fail_count=0,
details_url=f"/course/{course_slug}/cockpit?courseSessionId={completion.course_session.id}", details_url=f"/course/{course_slug}/cockpit?courseSessionId={completion.course_session.id}",
# noqa # noqa
) ),
)
if completion.completion_status == CourseCompletionStatus.SUCCESS.value: if completion.completion_status == CourseCompletionStatus.SUCCESS.value:
competence_records[combined_id][learning_unit].success_count += 1 competence_records[combined_id][learning_unit].success_count += 1
@ -100,7 +99,7 @@ def competences(
for record in circle_records.values() for record in circle_records.values()
] ]
success_count = sum(c.success_count for c in values) success_count = sum([c.success_count for c in values])
fail_count = sum(c.fail_count for c in values) fail_count = sum([c.fail_count for c in values])
return values, success_count, fail_count return values, success_count, fail_count

View File

@ -26,6 +26,7 @@ from vbv_lernwelt.learnpath.models import Circle
class StatisticsCourseSessionDataType(graphene.ObjectType): class StatisticsCourseSessionDataType(graphene.ObjectType):
id = graphene.ID(required=True) id = graphene.ID(required=True)
name = graphene.String(required=True) name = graphene.String(required=True)
region = graphene.String(required=True)
class StatisticsCircleDataType(graphene.ObjectType): class StatisticsCircleDataType(graphene.ObjectType):
@ -96,6 +97,10 @@ class BaseStatisticsType(graphene.ObjectType):
user_selection_ids = graphene.List(graphene.ID, required=False) user_selection_ids = graphene.List(graphene.ID, required=False)
assignments = graphene.Field(AssignmentsStatisticsType, required=True) assignments = graphene.Field(AssignmentsStatisticsType, required=True)
course_session_properties = graphene.Field(
StatisticsCourseSessionPropertiesType, required=True
)
def resolve_assignments(root, _info) -> AssignmentsStatisticsType: def resolve_assignments(root, _info) -> AssignmentsStatisticsType:
user_selection_ids = ( user_selection_ids = (
[str(user) for user in root.user_selection_ids] [str(user) for user in root.user_selection_ids]
@ -110,14 +115,55 @@ class BaseStatisticsType(graphene.ObjectType):
urql_id=str(root._id), urql_id=str(root._id),
) )
def resolve_course_session_properties(root, info):
course_session_data = []
circle_data = []
generations = set()
course_sessions = CourseSession.objects.filter(
id__in=root.course_session_selection_ids,
course_id=root.course_id,
)
for course_session in course_sessions:
course_session_data.append(
StatisticsCourseSessionDataType(
id=course_session.id, # noqa
name=course_session.title, # noqa
region=course_session.region, # noqa
)
)
generations.add(course_session.generation)
circles = (
course_session.course.get_learning_path()
.get_descendants()
.live()
.specific()
.exact_type(Circle)
)
for circle in circles:
if not any(c.id == circle.id for c in circle_data):
circle_data.append(
StatisticsCircleDataType(
id=circle.id, # noqa
name=circle.title, # noqa
)
)
return StatisticsCourseSessionPropertiesType(
_id=root._id, # noqa
sessions=course_session_data, # noqa
generations=list(generations), # noqa
circles=circle_data, # noqa
)
def get_circle_ids(self, info): def get_circle_ids(self, info):
return getattr(info.context, "circle_ids", None) return getattr(info.context, "circle_ids", None)
class CourseStatisticsType(BaseStatisticsType): class CourseStatisticsType(BaseStatisticsType):
course_session_properties = graphene.Field(
StatisticsCourseSessionPropertiesType, required=True
)
course_session_selection_metrics = graphene.Field( course_session_selection_metrics = graphene.Field(
StatisticsCourseSessionsSelectionMetricType, required=True StatisticsCourseSessionsSelectionMetricType, required=True
) )
@ -195,46 +241,3 @@ class CourseStatisticsType(BaseStatisticsType):
participant_count=participant_count, # noqa participant_count=participant_count, # noqa
expert_count=expert_count, # noqa expert_count=expert_count, # noqa
) )
def resolve_course_session_properties(root, info):
course_session_data = []
circle_data = []
generations = set()
course_sessions = CourseSession.objects.filter(
id__in=root.course_session_selection_ids,
course_id=root.course_id,
)
for course_session in course_sessions:
course_session_data.append(
StatisticsCourseSessionDataType(
id=course_session.id, # noqa
name=course_session.title, # noqa
)
)
generations.add(course_session.generation)
circles = (
course_session.course.get_learning_path()
.get_descendants()
.live()
.specific()
.exact_type(Circle)
)
for circle in circles:
if not any(c.id == circle.id for c in circle_data):
circle_data.append(
StatisticsCircleDataType(
id=circle.id, # noqa
name=circle.title, # noqa
)
)
return StatisticsCourseSessionPropertiesType(
_id=root._id, # noqa
sessions=course_session_data, # noqa
generations=list(generations), # noqa
circles=circle_data, # noqa
)

View File

@ -18,6 +18,7 @@ class FeedbackStatisticsRecordType(graphene.ObjectType):
_id = graphene.ID(required=True) _id = graphene.ID(required=True)
course_session_id = graphene.ID(required=True) course_session_id = graphene.ID(required=True)
generation = graphene.String(required=True) generation = graphene.String(required=True)
region = graphene.String(required=True)
circle_id = graphene.ID(required=True) circle_id = graphene.ID(required=True)
satisfaction_average = graphene.Float(required=True) satisfaction_average = graphene.Float(required=True)
satisfaction_max = graphene.Int(required=True) satisfaction_max = graphene.Int(required=True)
@ -68,6 +69,7 @@ def feedback_responses(
feedbacks=fbs, feedbacks=fbs,
course_session_id=course_session.id, course_session_id=course_session.id,
generation=course_session.generation, generation=course_session.generation,
region=course_session.region,
course_slug=str(course_slug), course_slug=str(course_slug),
urql_id_postfix=urql_id, urql_id_postfix=urql_id,
) )
@ -96,6 +98,7 @@ def circle_feedback_average(
feedbacks: List[FeedbackResponse], feedbacks: List[FeedbackResponse],
course_session_id, course_session_id,
generation: str, generation: str,
region: str,
course_slug: str, course_slug: str,
urql_id_postfix: str = "", urql_id_postfix: str = "",
): ):
@ -128,6 +131,7 @@ def circle_feedback_average(
_id=f"circle:{circle_id}-course_session:{course_session_id}@{urql_id_postfix}", # noqa _id=f"circle:{circle_id}-course_session:{course_session_id}@{urql_id_postfix}", # noqa
course_session_id=course_session_id, # noqa course_session_id=course_session_id, # noqa
generation=generation, # noqa generation=generation, # noqa
region=region, # noqa
circle_id=circle_id, # noqa circle_id=circle_id, # noqa
satisfaction_average=data["total"] / data["count"], # noqa satisfaction_average=data["total"] / data["count"], # noqa
satisfaction_max=4, # noqa satisfaction_max=4, # noqa

View File

@ -110,22 +110,22 @@ class DashboardTestCase(GraphQLTestCase):
self.client.force_login(member) self.client.force_login(member)
query = f"""query($course_id: ID!) {{ query = """query($course_id: ID!) {
course_progress(course_id: $course_id) {{ course_progress(course_id: $course_id) {
course_id course_id
session_to_continue_id session_to_continue_id
competence {{ competence {
total_count total_count
success_count success_count
fail_count fail_count
}} }
assignment {{ assignment {
total_count total_count
points_max_count points_max_count
points_achieved_count points_achieved_count
}} }
}} }
}} }
""" """
variables = {"course_id": str(course.id)} variables = {"course_id": str(course.id)}
@ -268,7 +268,7 @@ class DashboardTestCase(GraphQLTestCase):
role=CourseSessionUser.Role.MEMBER, role=CourseSessionUser.Role.MEMBER,
) )
add_learning_mentor(course_session=cs_1, mentor=mentor, mentee=csu) add_learning_mentor(mentor=mentor, mentee=csu)
self.client.force_login(mentor) self.client.force_login(mentor)
@ -287,6 +287,7 @@ class DashboardTestCase(GraphQLTestCase):
# THEN # THEN
self.assertResponseNoErrors(response) self.assertResponseNoErrors(response)
print(response.json())
self.assertEqual(len(response.json()["data"]["dashboard_config"]), 1) self.assertEqual(len(response.json()["data"]["dashboard_config"]), 1)
self.assertEqual( self.assertEqual(
@ -314,7 +315,7 @@ class DashboardTestCase(GraphQLTestCase):
role=CourseSessionUser.Role.MEMBER, role=CourseSessionUser.Role.MEMBER,
) )
add_learning_mentor(course_session=cs, mentor=mentor_and_member, mentee=mentee) add_learning_mentor(mentor=mentor_and_member, mentee=mentee)
# WHEN # WHEN
self.client.force_login(mentor_and_member) self.client.force_login(mentor_and_member)
@ -350,11 +351,11 @@ class DashboardTestCase(GraphQLTestCase):
self.client.force_login(disallowed_user) self.client.force_login(disallowed_user)
query = f"""query($course_id: ID!) {{ query = """query($course_id: ID!) {
course_statistics(course_id: $course_id) {{ course_statistics(course_id: $course_id) {
course_id course_id
}} }
}} }
""" """
variables = {"course_id": str(course.id)} variables = {"course_id": str(course.id)}
@ -384,13 +385,13 @@ class DashboardTestCase(GraphQLTestCase):
self.client.force_login(supervisor) self.client.force_login(supervisor)
query = f"""query($course_id: ID!) {{ query = """query($course_id: ID!) {
course_statistics(course_id: $course_id) {{ course_statistics(course_id: $course_id) {
course_id course_id
course_title course_title
course_slug course_slug
}} }
}} }
""" """
variables = {"course_id": str(course_2.id)} variables = {"course_id": str(course_2.id)}

View File

@ -7,7 +7,7 @@ from vbv_lernwelt.assignment.models import (
from vbv_lernwelt.course.creators.test_utils import add_course_session_user, create_user from vbv_lernwelt.course.creators.test_utils import add_course_session_user, create_user
from vbv_lernwelt.course.models import CourseSessionUser from vbv_lernwelt.course.models import CourseSessionUser
from vbv_lernwelt.dashboard.tests.test_views import BaseMentorAssignmentTestCase from vbv_lernwelt.dashboard.tests.test_views import BaseMentorAssignmentTestCase
from vbv_lernwelt.learning_mentor.models import LearningMentor from vbv_lernwelt.learning_mentor.models import AgentParticipantRelation
class MentorStatisticsTestCase(BaseMentorAssignmentTestCase, GraphQLTestCase): class MentorStatisticsTestCase(BaseMentorAssignmentTestCase, GraphQLTestCase):
@ -19,15 +19,13 @@ class MentorStatisticsTestCase(BaseMentorAssignmentTestCase, GraphQLTestCase):
self.course.configuration.save() self.course.configuration.save()
self.mentor = create_user("mentor") self.mentor = create_user("mentor")
self.lm = LearningMentor.objects.create(
mentor=self.mentor, course_session=self.course_session
)
self.participants = [create_user(f"participant{i}") for i in range(4)] self.participants = [create_user(f"participant{i}") for i in range(4)]
def test_assignment_statistics(self): def test_assignment_statistics(self):
# WHEN # WHEN
has_lb = [True, True, True, False] has_lb = [True, True, True, False]
has_passed = [True, False, True, False] has_passed = [True, False, True, False]
for i in range(4): for i in range(4):
csu = add_course_session_user( csu = add_course_session_user(
self.course_session, self.course_session,
@ -35,7 +33,9 @@ class MentorStatisticsTestCase(BaseMentorAssignmentTestCase, GraphQLTestCase):
role=CourseSessionUser.Role.MEMBER, role=CourseSessionUser.Role.MEMBER,
) )
if has_lb[i]: if has_lb[i]:
self.lm.participants.add(csu) AgentParticipantRelation.objects.create(
agent=self.mentor, participant=csu, role="LEARNING_MENTOR"
)
AssignmentCompletion.objects.create( AssignmentCompletion.objects.create(
course_session=self.course_session, course_session=self.course_session,
@ -47,25 +47,25 @@ class MentorStatisticsTestCase(BaseMentorAssignmentTestCase, GraphQLTestCase):
) )
# THEN # THEN
# WHEN # WHEN
query = f"""query ($courseId: ID!) {{ query = """query ($courseId: ID!, $agentRole: String!) {
mentor_course_statistics(course_id: $courseId) {{ mentor_course_statistics(course_id: $courseId, agent_role: $agentRole) {
course_session_selection_ids course_session_selection_ids
user_selection_ids user_selection_ids
assignments {{ assignments {
_id _id
summary {{ summary {
_id _id
completed_count completed_count
average_passed average_passed
total_passed total_passed
total_failed total_failed
}} }
}} }
}} }
}}""" }"""
# THEN # THEN
variables = {"courseId": str(self.course.id)} variables = {"courseId": str(self.course.id), "agentRole": "LEARNING_MENTOR"}
self.client.force_login(self.mentor) self.client.force_login(self.mentor)
response = self.query(query, variables=variables) response = self.query(query, variables=variables)
self.assertResponseNoErrors(response) self.assertResponseNoErrors(response)

View File

@ -139,9 +139,7 @@ class PersonsExportTestCase(ExportBaseTestCase):
self.assertEqual(wb.sheetnames[0], "Test Zürich 2022 a") self.assertEqual(wb.sheetnames[0], "Test Zürich 2022 a")
wb.active = wb["Test Zürich 2022 a"] wb.active = wb["Test Zürich 2022 a"]
data = self._generate_expected_data([[None] * 6]) self._check_export(wb, [[None] * 6], 1, 6)
self._check_export(wb, data, 1, 6)
def test_export_in_fr(self): def test_export_in_fr(self):
activate("fr") activate("fr")

View File

@ -37,7 +37,7 @@ from vbv_lernwelt.dashboard.views import (
get_course_config, get_course_config,
get_course_sessions_with_roles_for_user, get_course_sessions_with_roles_for_user,
) )
from vbv_lernwelt.learning_mentor.models import LearningMentor from vbv_lernwelt.learning_mentor.models import AgentParticipantRelation
from vbv_lernwelt.learnpath.models import Circle, LearningUnit from vbv_lernwelt.learnpath.models import Circle, LearningUnit
from vbv_lernwelt.self_evaluation_feedback.models import SelfEvaluationFeedback from vbv_lernwelt.self_evaluation_feedback.models import SelfEvaluationFeedback
@ -98,14 +98,16 @@ class GetCourseSessionsForUserTestCase(TestCase):
def test_learning_mentor_get_sessions(self): def test_learning_mentor_get_sessions(self):
mentor = create_user("mentor") mentor = create_user("mentor")
LearningMentor.objects.create(mentor=mentor, course_session=self.course_session)
participant = create_user("participant") participant = create_user("participant")
add_course_session_user( csu = add_course_session_user(
self.course_session, self.course_session,
participant, participant,
role=CourseSessionUser.Role.MEMBER, role=CourseSessionUser.Role.MEMBER,
) )
AgentParticipantRelation.objects.create(
agent=mentor, participant=csu, role="LEARNING_MENTOR"
)
sessions = get_course_sessions_with_roles_for_user(mentor) sessions = get_course_sessions_with_roles_for_user(mentor)
@ -122,7 +124,7 @@ class GetDashboardConfig(TestCase):
course=self.course, title="Test Session" course=self.course, title="Test Session"
) )
def _test_config(self, user, role, is_uk, is_vv, is_mentor, has_preview, widgets): def _test_config(self, user, role, is_uk, is_vv, has_preview, widgets):
# WHEN # WHEN
sessions = get_course_sessions_with_roles_for_user(user) sessions = get_course_sessions_with_roles_for_user(user)
course_configs = get_course_config(sessions) course_configs = get_course_config(sessions)
@ -132,13 +134,12 @@ class GetDashboardConfig(TestCase):
self.assertEqual(course_configs[0].course_title, self.course.title) self.assertEqual(course_configs[0].course_title, self.course.title)
self.assertEqual(course_configs[0].is_uk, is_uk) self.assertEqual(course_configs[0].is_uk, is_uk)
self.assertEqual(course_configs[0].is_vv, is_vv) self.assertEqual(course_configs[0].is_vv, is_vv)
self.assertEqual(course_configs[0].is_mentor, is_mentor)
self.assertEqual(course_configs[0].has_preview, has_preview) self.assertEqual(course_configs[0].has_preview, has_preview)
self.assertEqual( self.assertEqual(
course_configs[0].session_to_continue_id, str(self.course_session.id) course_configs[0].session_to_continue_id, str(self.course_session.id)
) )
self.assertEqual(course_configs[0].role_key, role) self.assertEqual(course_configs[0].role_key, role)
self.assertEqual(course_configs[0].widgets, widgets) self.assertEqual(set(course_configs[0].widgets), set(widgets))
def test_participant_uk_get_config(self): def test_participant_uk_get_config(self):
participant = create_user("participant") participant = create_user("participant")
@ -155,7 +156,6 @@ class GetDashboardConfig(TestCase):
role="Member", role="Member",
is_uk=True, is_uk=True,
is_vv=False, is_vv=False,
is_mentor=False,
has_preview=False, has_preview=False,
widgets=[ widgets=[
"ProgressWidget", "ProgressWidget",
@ -179,7 +179,6 @@ class GetDashboardConfig(TestCase):
role="Member", role="Member",
is_uk=False, is_uk=False,
is_vv=True, is_vv=True,
is_mentor=False,
has_preview=False, has_preview=False,
widgets=["ProgressWidget", "CompetenceWidget"], widgets=["ProgressWidget", "CompetenceWidget"],
) )
@ -187,7 +186,16 @@ class GetDashboardConfig(TestCase):
def test_mentor_uk_get_config(self): def test_mentor_uk_get_config(self):
# GIVEN # GIVEN
mentor = create_user("mentor") mentor = create_user("mentor")
LearningMentor.objects.create(mentor=mentor, course_session=self.course_session)
participant = create_user("participant")
csu = add_course_session_user(
self.course_session,
participant,
role=CourseSessionUser.Role.MEMBER,
)
AgentParticipantRelation.objects.create(
agent=mentor, participant=csu, role="LEARNING_MENTOR"
)
self.course.configuration.is_uk = True self.course.configuration.is_uk = True
self.course.configuration.save() self.course.configuration.save()
@ -197,7 +205,6 @@ class GetDashboardConfig(TestCase):
role="MentorUK", role="MentorUK",
is_uk=True, is_uk=True,
is_vv=False, is_vv=False,
is_mentor=True,
has_preview=True, has_preview=True,
widgets=["MentorPersonWidget", "MentorCompetenceWidget"], widgets=["MentorPersonWidget", "MentorCompetenceWidget"],
) )
@ -205,7 +212,15 @@ class GetDashboardConfig(TestCase):
def test_mentor_vv_get_config(self): def test_mentor_vv_get_config(self):
# GIVEN # GIVEN
mentor = create_user("mentor") mentor = create_user("mentor")
LearningMentor.objects.create(mentor=mentor, course_session=self.course_session) participant = create_user("participant")
csu = add_course_session_user(
self.course_session,
participant,
role=CourseSessionUser.Role.MEMBER,
)
AgentParticipantRelation.objects.create(
agent=mentor, participant=csu, role="LEARNING_MENTOR"
)
self.course.configuration.is_vv = True self.course.configuration.is_vv = True
self.course.configuration.save() self.course.configuration.save()
@ -215,7 +230,6 @@ class GetDashboardConfig(TestCase):
role="MentorVV", role="MentorVV",
is_uk=False, is_uk=False,
is_vv=True, is_vv=True,
is_mentor=True,
has_preview=True, has_preview=True,
widgets=["MentorPersonWidget", "MentorTasksWidget"], widgets=["MentorPersonWidget", "MentorTasksWidget"],
) )
@ -228,7 +242,16 @@ class GetDashboardConfig(TestCase):
mentor, mentor,
role=CourseSessionUser.Role.MEMBER, role=CourseSessionUser.Role.MEMBER,
) )
LearningMentor.objects.create(mentor=mentor, course_session=self.course_session)
participant = create_user("participant")
csu = add_course_session_user(
self.course_session,
participant,
role=CourseSessionUser.Role.MEMBER,
)
AgentParticipantRelation.objects.create(
agent=mentor, participant=csu, role="LEARNING_MENTOR"
)
self.course.configuration.is_vv = True self.course.configuration.is_vv = True
self.course.configuration.save() self.course.configuration.save()
@ -238,7 +261,6 @@ class GetDashboardConfig(TestCase):
role="Member", role="Member",
is_uk=False, is_uk=False,
is_vv=True, is_vv=True,
is_mentor=True,
has_preview=False, has_preview=False,
widgets=[ widgets=[
"ProgressWidget", "ProgressWidget",
@ -264,9 +286,6 @@ class GetMenteeCountTestCase(TestCase):
participants_with_mentor = [create_user(f"participant{i}") for i in range(2)] participants_with_mentor = [create_user(f"participant{i}") for i in range(2)]
participant = create_user("participant") participant = create_user("participant")
mentor = create_user("mentor") mentor = create_user("mentor")
lm = LearningMentor.objects.create(
mentor=mentor, course_session=self.course_session
)
# WHEN # WHEN
for p in participants_with_mentor: for p in participants_with_mentor:
@ -275,7 +294,9 @@ class GetMenteeCountTestCase(TestCase):
p, p,
role=CourseSessionUser.Role.MEMBER, role=CourseSessionUser.Role.MEMBER,
) )
lm.participants.add(csu) AgentParticipantRelation.objects.create(
agent=mentor, participant=csu, role="LEARNING_MENTOR"
)
add_course_session_user( add_course_session_user(
self.course_session, self.course_session,
@ -305,9 +326,6 @@ class GetMentorOpenTasksTestCase(BaseMentorAssignmentTestCase):
self.course.configuration.save() self.course.configuration.save()
self.mentor = create_user("mentor") self.mentor = create_user("mentor")
self.lm = LearningMentor.objects.create(
mentor=self.mentor, course_session=self.course_session
)
self.participants = [create_user(f"participant{i}") for i in range(2)] self.participants = [create_user(f"participant{i}") for i in range(2)]
def create_and_test_count( def create_and_test_count(
@ -337,7 +355,10 @@ class GetMentorOpenTasksTestCase(BaseMentorAssignmentTestCase):
self.participants[0], self.participants[0],
role=CourseSessionUser.Role.MEMBER, role=CourseSessionUser.Role.MEMBER,
) )
self.lm.participants.add(csu)
AgentParticipantRelation.objects.create(
agent=self.mentor, participant=csu, role="LEARNING_MENTOR"
)
add_course_session_user( add_course_session_user(
self.course_session, self.course_session,
@ -367,7 +388,9 @@ class GetMentorOpenTasksTestCase(BaseMentorAssignmentTestCase):
self.participants[0], self.participants[0],
role=CourseSessionUser.Role.MEMBER, role=CourseSessionUser.Role.MEMBER,
) )
self.lm.participants.add(csu) AgentParticipantRelation.objects.create(
agent=self.mentor, participant=csu, role="LEARNING_MENTOR"
)
add_course_session_user( add_course_session_user(
self.course_session, self.course_session,
@ -389,8 +412,9 @@ class GetMentorOpenTasksTestCase(BaseMentorAssignmentTestCase):
self.participants[0], self.participants[0],
role=CourseSessionUser.Role.MEMBER, role=CourseSessionUser.Role.MEMBER,
) )
self.lm.participants.add(csu) AgentParticipantRelation.objects.create(
agent=self.mentor, participant=csu, role="LEARNING_MENTOR"
)
SelfEvaluationFeedback.objects.create( SelfEvaluationFeedback.objects.create(
feedback_submitted=False, feedback_submitted=False,
feedback_requester_user=self.participants[0], feedback_requester_user=self.participants[0],
@ -411,8 +435,9 @@ class GetMentorOpenTasksTestCase(BaseMentorAssignmentTestCase):
self.participants[0], self.participants[0],
role=CourseSessionUser.Role.MEMBER, role=CourseSessionUser.Role.MEMBER,
) )
self.lm.participants.add(csu) AgentParticipantRelation.objects.create(
agent=self.mentor, participant=csu, role="LEARNING_MENTOR"
)
SelfEvaluationFeedback.objects.create( SelfEvaluationFeedback.objects.create(
feedback_submitted=True, feedback_submitted=True,
feedback_requester_user=self.participants[0], feedback_requester_user=self.participants[0],

Some files were not shown because too many files have changed in this diff Show More