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

View File

@ -4,16 +4,27 @@ import { onMounted, ref } from "vue";
import { fetchMenteeCount } from "@/services/dashboard";
import BaseBox from "@/components/dashboard/BaseBox.vue";
const props = defineProps<{
courseId: string;
courseSlug: string;
}>();
const props = withDefaults(
defineProps<{
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 () => {
const data = await fetchMenteeCount(props.courseId);
menteeCount.value = data?.mentee_count;
if (props.count == -1) {
menteeCount.value = 0;
const data = await fetchMenteeCount(props.courseId);
menteeCount.value = data?.mentee_count;
}
});
</script>
@ -21,11 +32,12 @@ onMounted(async () => {
<div class="w-[325px]">
<BaseBox
: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 #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
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;
avgPassed: number;
courseSlug: string;
detailsLink?: string;
}>();
const progress = computed(() => {
@ -20,7 +21,7 @@ const progress = computed(() => {
<template>
<BaseBox
:details-link="`/statistic/${courseSlug}/assignment`"
:details-link="props.detailsLink || `/statistic/${courseSlug}/assignment`"
data-cy="dashboard.stats.assignments"
>
<template #title>{{ $t("a.Kompetenznachweis-Elemente") }}</template>

View File

@ -1,17 +1,23 @@
<script setup lang="ts">
defineProps<{
detailsLink: string;
}>();
withDefaults(
defineProps<{
detailsLink: string;
slim?: boolean;
}>(),
{
slim: false,
}
);
</script>
<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">
<slot name="title"></slot>
</h4>
<slot name="content"></slot>
<div class="flex-grow"></div>
<div class="pt-8">
<div class="pt-0">
<router-link class="underline" :to="detailsLink" data-cy="basebox.detailsLink">
{{ $t("a.Details anschauen") }}
</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 AssignmentSummary from "@/components/dashboard/AssignmentSummary.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 { getCockpitUrl, getLearningMentorUrl, getLearningPathUrl } from "@/utils/utils";
import UkStatistics from "@/components/dashboard/UkStatistics.vue";
import BerufsbildnerStatistics from "@/components/dashboard/BerufsbildnerStatistics.vue";
const mentorWidgets = [
"MentorTasksWidget",
@ -63,6 +64,13 @@ const actionButtonProps = computed<{ href: string; text: string; cyKey: string }
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 {
href: getLearningPathUrl(props.courseConfig?.course_slug),
text: "Weiter lernen",
@ -77,7 +85,11 @@ function hasActionButton(): boolean {
</script>
<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="border-b border-gray-300 pb-8">
<div class="flex flex-row items-start justify-between">
@ -102,12 +114,13 @@ function hasActionButton(): boolean {
target="_blank"
>
<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" />
</div>
</router-link>
</p>
</div>
<div
v-if="
hasWidget('ProgressWidget') &&
@ -123,6 +136,7 @@ function hasActionButton(): boolean {
diagram-type="horizontal"
></LearningPathDiagram>
</div>
<div
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"
@ -140,17 +154,30 @@ function hasActionButton(): boolean {
:course-id="courseConfig.course_id"
></CompetenceSummary>
</div>
<div
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"
>
<UkStatistics :course-slug="courseSlug" :course-id="courseConfig.course_id" />
</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
v-if="numberOfMentorWidgets > 0"
class="flex flex-col flex-wrap items-stretch md:flex-row"
>
<MentorMenteeCount
<AgentConnectionCount
v-if="hasWidget('MentorPersonWidget')"
:course-id="courseConfig.course_id"
:course-slug="courseConfig?.course_slug"
@ -163,6 +190,7 @@ function hasActionButton(): boolean {
<MentorCompetenceSummary
v-if="hasWidget('MentorCompetenceWidget')"
:course-id="courseConfig.course_id"
:agent-role="courseConfig.role_key"
/>
</div>
</div>

View File

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

View File

@ -21,7 +21,7 @@ onMounted(async () => {
<div class="w-[325px]">
<BaseBox
: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 #content>

View File

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

View File

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

View File

@ -49,7 +49,12 @@ async function navigate(routeName: string) {
/>
</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" />
<span class="ml-1">{{ $t("mainNavigation.logout") }}</span>
</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"
>
<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>
<div class="flex space-x-8">

View File

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

View File

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

View File

@ -8,9 +8,9 @@ import LoadingSpinner from "@/components/ui/LoadingSpinner.vue";
const courseSession = useCurrentCourseSession();
const { isLoading, summary, fetchData } = useLearningMentees(courseSession.value.id);
const removeMyMentee = async (menteeId: string) => {
const removeMyMentee = async (relationId: string) => {
await useCSRFFetch(
`/api/mentor/${courseSession.value.id}/mentors/${summary.value?.mentor_id}/remove/${menteeId}`
`/api/mentor/${courseSession.value.id}/mentors/${relationId}/delete`
).delete();
fetchData();
};
@ -28,25 +28,31 @@ const noMenteesText = computed(() =>
</div>
<div v-else>
<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
v-for="participant in summary?.participants ?? []"
:key="participant.id"
v-for="relation in summary?.participant_relations ?? []"
:key="relation.id"
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"
>
<div class="flex items-center space-x-2">
<img
:alt="participant.last_name"
:alt="relation.participant_user.last_name"
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 class="text-bold">
{{ participant.first_name }}
{{ participant.last_name }}
{{ relation.participant_user.first_name }}
{{ relation.participant_user.last_name }}
</div>
{{ participant.email }}
{{ relation.participant_user.email }}
</div>
</div>
<div class="space-x-5">
@ -55,7 +61,7 @@ const noMenteesText = computed(() =>
:to="{
name: 'profileLearningPath',
params: {
userId: participant.id,
userId: relation.participant_user.id,
courseSlug: courseSession.course.slug,
},
}"
@ -66,7 +72,7 @@ const noMenteesText = computed(() =>
<button
class="underline"
data-cy="lm-my-mentee-remove"
@click="removeMyMentee(participant.id)"
@click="removeMyMentee(relation.id)"
>
{{ $t("a.Entfernen") }}
</button>

View File

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

View File

@ -74,9 +74,9 @@ const onSubmit = async () => {
<option
v-for="learningMentor in learningMentors"
: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>
</select>
</div>

View File

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

View File

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

View File

@ -2,7 +2,6 @@ import { useCSRFFetch } from "@/fetchHelpers";
import type { CourseStatisticsType } from "@/gql/graphql";
import { graphqlClient } from "@/graphql/client";
import {
COMPETENCE_NAVI_CERTIFICATE_FOR_USER_QUERY,
COMPETENCE_NAVI_CERTIFICATE_QUERY,
COURSE_QUERY,
COURSE_SESSION_DETAIL_QUERY,
@ -31,6 +30,7 @@ import { useDashboardStore } from "@/stores/dashboard";
import { useUserStore } from "@/stores/user";
import type {
ActionCompetence,
AgentParticipantRelation,
CircleType,
CompetenceCertificate,
Course,
@ -40,7 +40,6 @@ import type {
CourseSessionDetail,
DashboardPersonsPageMode,
LearningContentWithCompletion,
LearningMentor,
LearningPathType,
LearningUnitPerformanceCriteria,
PerformanceCriteria,
@ -52,8 +51,7 @@ import orderBy from "lodash/orderBy";
import log from "loglevel";
import type { ComputedRef, Ref } from "vue";
import { computed, onMounted, ref, watchEffect } from "vue";
import { useRouter, type RouteLocationRaw } from "vue-router";
import { getCertificates } from "./services/competence";
import { type RouteLocationRaw, useRouter } from "vue-router";
import { mergeCompetenceCertificates } from "./pages/competence/utils";
export function useCurrentCourseSession() {
@ -81,14 +79,14 @@ export function useCurrentCourseSession() {
return result;
}
export function useCourseSessionDetailQuery(courSessionId?: string) {
if (!courSessionId) {
courSessionId = useCurrentCourseSession().value.id;
export function useCourseSessionDetailQuery(courseSessionId?: string) {
if (!courseSessionId) {
courseSessionId = useCurrentCourseSession().value.id;
}
const queryResult = useQuery({
query: COURSE_SESSION_DETAIL_QUERY,
variables: {
courseSessionId: courSessionId,
courseSessionId: courseSessionId,
},
});
@ -132,8 +130,11 @@ export function useCourseSessionDetailQuery(courSessionId?: string) {
return findUser(userId);
}
function filterMembers() {
function filterMembers(userSelectionIds: string[] | null = null) {
return (courseSessionDetail.value?.users ?? []).filter((u) => {
if (userSelectionIds && userSelectionIds.length > 0) {
return userSelectionIds.includes(u.user_id) && 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() {
const error = ref(false);
const loading = ref(false);
@ -492,7 +471,7 @@ export function useFileUpload() {
}
export function useMyLearningMentors() {
const learningMentors = ref<LearningMentor[]>([]);
const learningMentors = ref<AgentParticipantRelation[]>([]);
const currentCourseSessionId = useCurrentCourseSession().value.id;
const loading = ref(false);
@ -517,7 +496,7 @@ export function getVvRoleDisplay(role: DashboardPersonRoleType) {
switch (role) {
case "LEARNING_MENTOR":
return t("a.Lernbegleitung");
case "LEARNING_MENTEE":
case "PARTICIPANT":
return t("a.Teilnehmer");
case "EXPERT":
return t("a.Experte");
@ -534,7 +513,7 @@ export function getUkRoleDisplay(role: DashboardPersonRoleType) {
switch (role) {
case "LEARNING_MENTOR":
return t("a.Praxisbildner");
case "LEARNING_MENTEE":
case "PARTICIPANT":
return t("a.Teilnehmer");
case "EXPERT":
return t("a.Trainer");
@ -601,7 +580,7 @@ export function useDashboardPersonsDueDates(
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) => {
if (dueDate.course_session.my_role === "LEARNING_MENTOR") {
dueDate.persons = dashboardPersons.value.filter((person) => {
@ -611,7 +590,7 @@ export function useDashboardPersonsDueDates(
.includes(dueDate.course_session.id)
) {
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(
userId: string | undefined,
userIds: string[],
courseSlug: string,
courseSession: CourseSession
) {
const certificatesQuery = (() => {
if (userId) {
return useQuery({
query: COMPETENCE_NAVI_CERTIFICATE_FOR_USER_QUERY,
variables: {
courseSlug: courseSlug,
courseSessionId: courseSession.id,
userId: userId,
},
});
} else {
return useQuery({
query: COMPETENCE_NAVI_CERTIFICATE_QUERY,
variables: {
courseSlug: courseSlug,
courseSessionId: courseSession.id,
},
});
}
return useQuery({
query: COMPETENCE_NAVI_CERTIFICATE_QUERY,
variables: {
courseSlug: courseSlug,
courseSessionId: courseSession.id,
userIds,
},
});
})();
return { certificatesQuery };
@ -750,21 +719,20 @@ export function useVVByLink() {
return { href };
}
export function useAllCompetenceCertificates(
userId: string | undefined,
courseSlug: string
) {
export function useAllCompetenceCertificates(userId: string, courseSlug: string) {
const courseSessionsStore = useCourseSessionsStore();
const certificateQueries = courseSessionsStore.allCourseSessions.map(
(courseSession) => {
return useCertificateQuery(userId, courseSlug, courseSession).certificatesQuery;
return useCertificateQuery([userId], courseSlug, courseSession).certificatesQuery;
}
);
const competenceCertificatesPerCs = computed(() =>
certificateQueries.map((query) => {
return getCertificates(query.data.value, userId ?? null)
?.competence_certificates as unknown as CompetenceCertificate[];
return (
(query.data.value?.competence_certificate_list
?.competence_certificates as unknown as CompetenceCertificate[]) ?? []
);
})
);
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 {
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
dashboard_config: [DashboardConfigType!]!
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_document_list: LearningContentDocumentListObjectType
competence_certificate(id: ID, slug: String): CompetenceCertificateObjectType
competence_certificate_list(id: ID, slug: String, course_id: ID, course_slug: String): CompetenceCertificateListObjectType
competence_certificate_list_for_user(id: ID, slug: String, course_id: ID, course_slug: String, user_id: UUID): CompetenceCertificateListObjectType
competence_certificate_list(id: ID, slug: String, course_id: ID, course_slug: String, user_ids: [UUID]): CompetenceCertificateListObjectType
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
}
@ -53,10 +52,15 @@ type AssignmentStatisticsRecordType {
course_session_assignment_id: ID!
circle_id: ID!
generation: String!
region: String!
assignment_type_translation_key: String!
assignment_title: String!
deadline: DateTime!
course_session_title: String
competence_certificate_id: ID
competence_certificate_title: String
metrics: AssignmentCompletionMetricsType!
learning_content_id: ID!
details_url: String!
}
@ -74,6 +78,8 @@ type AssignmentCompletionMetricsType {
unranked_count: Int!
ranking_completed: Boolean!
average_passed: Float!
average_evaluation_percent: Float
competence_certificate_weight: Float
}
type AssignmentStatisticsSummaryType {
@ -82,6 +88,7 @@ type AssignmentStatisticsSummaryType {
average_passed: Float!
total_passed: Int!
total_failed: Int!
average_evaluation_percent: Float
}
type StatisticsCourseSessionPropertiesType {
@ -94,6 +101,7 @@ type StatisticsCourseSessionPropertiesType {
type StatisticsCourseSessionDataType {
id: ID!
name: String!
region: String!
}
type StatisticsCircleDataType {
@ -118,6 +126,7 @@ type PresenceRecordStatisticsType {
_id: ID!
course_session_id: ID!
generation: String!
region: String!
circle_id: ID!
due_date: DateTime!
participants_present: Int!
@ -141,6 +150,7 @@ type FeedbackStatisticsRecordType {
_id: ID!
course_session_id: ID!
generation: String!
region: String!
circle_id: ID!
satisfaction_average: Float!
satisfaction_max: Int!
@ -171,6 +181,7 @@ type CompetenceRecordStatisticsType {
_id: ID!
course_session_id: ID!
generation: String!
region: String!
title: String!
circle_id: ID!
success_count: Int!
@ -186,6 +197,7 @@ type BaseStatisticsType {
course_session_selection_ids: [ID]!
user_selection_ids: [ID]
assignments: AssignmentsStatisticsType!
course_session_properties: StatisticsCourseSessionPropertiesType!
}
type CourseProgressType {
@ -268,7 +280,6 @@ type CourseObjectType {
action_competences: [ActionCompetenceObjectType!]!
profiles: [String]
course_session_users(id: String): [CourseSessionUserType]!
chosen_profile(user: String!): String
}
type ActionCompetenceObjectType implements CoursePageInterface {
@ -503,7 +514,7 @@ type AssignmentObjectType implements CoursePageInterface {
max_points: Int
competence_certificate_weight: Float
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
}
@ -565,6 +576,7 @@ type AssignmentCompletionObjectType {
evaluation_points: Float
evaluation_points_final: Float
evaluation_max_points: Float
evaluation_percent: Float
}
type UserObjectType {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -33,7 +33,7 @@ const userGradeRounded2Places = computed(() => {
const numAssignmentsEvaluated = computed(() => {
return props.competenceCertificate.assignments.filter((a) => {
return a.completion?.completion_status === "EVALUATION_SUBMITTED";
return a?.completions?.[0]?.completion_status === "EVALUATION_SUBMITTED";
}).length;
});
@ -57,7 +57,8 @@ const showCourseSession = computed(() => {
const currentCourseSession = useCurrentCourseSession();
return props.competenceCertificate.assignments.some((assignment) => {
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 { computed } from "vue";
import { useAllCompetenceCertificates } from "@/composables";
import CompetenceCertificateComponent from "@/pages/competence/CompetenceCertificateComponent.vue";
import { getPreviousRoute } from "@/router/history";
import CompetenceCertificateComponent from "@/pages/competence/CompetenceCertificateComponent.vue";
import { useUserStore } from "@/stores/user";
const props = defineProps<{
courseSlug: string;
certificateSlug: string;
@ -12,8 +14,9 @@ const props = defineProps<{
log.debug("CompetenceCertificateDetailPage setup", props);
const { id: currentUserId } = useUserStore();
const { competenceCertificates } = useAllCompetenceCertificates(
props.userId,
props.userId ?? currentUserId,
props.courseSlug
);

View File

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

View File

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

View File

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

View File

@ -254,6 +254,7 @@ function exportData() {
course_session_id: csId,
generation: "",
circle_id: "",
region: "",
});
}
exportDataAsXls(items, exportPersons, userStore.language);
@ -407,8 +408,8 @@ watch(selectedRegion, () => {
v-if="
(['SUPERVISOR', 'EXPERT'].includes(cs.my_role) &&
cs.user_role === 'MEMBER') ||
(cs.my_role === 'LEARNING_MENTOR' &&
cs.user_role === 'LEARNING_MENTEE')
(['LEARNING_MENTOR', 'BERUFSBILDNER'].includes(cs.my_role) &&
cs.user_role === 'PARTICIPANT')
"
>
<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 {
AssignmentCompletionMetricsType,
AssignmentStatisticsRecordType,
CourseStatisticsType,
BaseStatisticsType,
StatisticsCircleDataType,
} from "@/gql/graphql";
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
const props = defineProps<{
courseStatistics: CourseStatisticsType;
courseStatistics: BaseStatisticsType;
courseSessionName: (sessionId: string) => string;
circleMeta: (circleId: string) => StatisticsCircleDataType;
detailBaseUrl?: string;
}>();
const statisticFilter: Ref<typeof StatisticFilterList | null> = ref(null);
@ -51,6 +52,13 @@ async function exportData() {
const filteredItems = statisticFilter.value.getFilteredItems();
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>
<template>
@ -114,8 +122,7 @@ async function exportData() {
></ItProgress>
<router-link
class="underline"
target="_blank"
:to="(item as AssignmentStatisticsRecordType).details_url"
:to="itemDetailUrl(item as AssignmentStatisticsRecordType)"
>
{{ $t("a.Details anschauen") }}
</router-link>

View File

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

View File

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

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import type { Assignment, Participant } from "@/services/learningMentees";
import type { Assignment, UserShort } from "@/services/learningMentees";
import { useLearningMentees } from "@/services/learningMentees";
import { computed, type Ref } from "vue";
import { useCurrentCourseSession } from "@/composables";
@ -15,14 +15,12 @@ const selfEvaluationFeedback: Ref<Assignment | null> = computed(() =>
learningMentees.getAssignmentById(props.learningUnitId)
);
const getParticipantById = (id: string): Participant | null => {
if (learningMentees.summary.value?.participants) {
const found = learningMentees.summary.value.participants.find(
(item) => item.id === id
);
return found || null;
}
return null;
const getParticipantById = (id: string): UserShort | undefined => {
return (learningMentees.summary.value?.participant_relations ?? [])
.map((rel) => {
return rel.participant_user;
})
.find((user) => user.id === id);
};
</script>
@ -51,6 +49,7 @@ const getParticipantById = (id: string): Participant | null => {
v-for="item in selfEvaluationFeedback.completions"
: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"
:data-cy="`self-evalution-feedback-${item.user_id}`"
>
<!-- Left -->
<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(() => {
return learningMentors.learningMentors.value.map((mentor) => ({
id: mentor.mentor.id,
name: `${mentor.mentor.first_name} ${mentor.mentor.last_name}`,
id: mentor.agent.id,
name: `${mentor.agent.first_name} ${mentor.agent.last_name}`,
}));
});
@ -100,6 +100,7 @@ const onRequestFeedback = async () => {
variant="primary"
size="large"
:disabled="!currentSessionRequestedMentor"
data-cy="request-feedback-button"
@click="onRequestFeedback"
>
<p v-if="!currentSessionRequestedMentor">

View File

@ -46,7 +46,7 @@ const signUpURL = computed(() => getSignUpURL(constructParams()));
</a>
<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") }}
</a>
</template>

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { onMounted, ref } from "vue";
import { onMounted } from "vue";
import { useRoute, useRouter } from "vue-router";
import { useTranslation } from "i18next-vue";
import { useFetch } from "@vueuse/core";
@ -12,18 +12,24 @@ const props = defineProps<{
const { t } = useTranslation();
const pages = ref([
const pages = [
{
label: t("general.learningPath"),
route: "profileLearningPath",
routeMatch: "profileLearningPath",
childrenRouteMatches: [],
},
{
label: t("a.KompetenzNavi"),
route: "competenceMain",
routeMatch: "profileCompetence",
childrenRouteMatches: [
"competenceCertificates",
"competenceCertificateDetail",
"competenceEvaluations",
],
},
]);
];
const courseSession = useCurrentCourseSession();
const { data: user } = useFetch(
@ -35,9 +41,16 @@ const router = useRouter();
onMounted(() => {
// 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({
name: pages.value[0].route,
name: pages[0].route,
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",
component: () => import("@/pages/ShopPage.vue"),

View File

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

View File

@ -1,8 +1,3 @@
import type {
CompetenceCertificateForUserQueryQuery,
CompetenceCertificateListObjectType,
CompetenceCertificateQueryQuery,
} from "@/gql/graphql";
import type { PerformanceCriteria } from "@/types";
import groupBy from "lodash/groupBy";
@ -22,42 +17,3 @@ export function calcPerformanceCriteriaStatusCount(criteria: PerformanceCriteria
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 type {
AssignmentsStatisticsType,
BaseStatisticsType,
CourseProgressType,
CourseStatisticsType,
DashboardConfigType,
@ -25,14 +25,16 @@ export type DashboardPersonRoleType =
| "EXPERT"
| "MEMBER"
| "LEARNING_MENTOR"
| "LEARNING_MENTEE";
| "BERUFSBILDNER"
| "PARTICIPANT";
export type DashboardRoleKeyType =
| "Supervisor"
| "Trainer"
| "Member"
| "MentorUK"
| "MentorVV";
| "MentorVV"
| "Berufsbildner";
export type WidgetType =
| "ProgressWidget"
@ -41,7 +43,8 @@ export type WidgetType =
| "MentorPersonWidget"
| "MentorCompetenceWidget"
| "CompetenceCertificateWidget"
| "UKStatisticsWidget";
| "UKStatisticsWidget"
| "UKBerufsbildnerStatisticsWidget";
export type DashboardPersonCourseSessionType = {
id: string;
@ -107,6 +110,7 @@ export const fetchStatisticData = async (
console.error("Error fetching statistics for course ID:", courseId, res.error);
}
// @ts-ignore
return res.data?.course_statistics || null;
} catch (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 (
courseId: string
): Promise<AssignmentsStatisticsType | null> => {
courseId: string,
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 {
const res = await graphqlClient.query(DASHBOARD_MENTOR_COMPETENCE_SUMMARY, {
courseId,
agentRole,
});
if (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) {
console.error(`Error fetching data for course ID: ${courseId}`, error);
return null;

View File

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

View File

@ -47,12 +47,6 @@ export const useDashboardStore = defineStore("dashboard", () => {
};
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);
dashBoardDataCache[id] = data;
return data;
@ -68,6 +62,5 @@ export const useDashboardStore = defineStore("dashboard", () => {
currentDashBoardData,
loading,
loadStatisticsData,
loadStatisticsDatav2,
};
});

View File

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

View File

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

View File

@ -1,8 +1,10 @@
const { defineConfig } = require("cypress");
const { cloudPlugin } = require("cypress-cloud/plugin");
import { defineConfig } from "cypress";
import { cloudPlugin } from "cypress-cloud/plugin";
import tasks from "./cypress/plugins/index.mjs";
module.exports = defineConfig({
export default defineConfig({
projectId: "RVEZS1",
chromeWebSecurity: false,
watchForFileChanges: false,
video: true,
viewportWidth: 1280,
@ -19,6 +21,7 @@ module.exports = defineConfig({
e2e: {
// experimentalSessionAndOrigin: true,
setupNodeEvents(on, config) {
tasks(on, config);
return cloudPlugin(on, config);
},
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,
djangoModelPath,
serializerModelPath,
valueAsString = false
valueAsString = false,
) {
const djangoModel = _.last(djangoModelPath.split("."))
const djangoModelImportPath = _.initial(djangoModelPath.split(".")).join(".")
const serializerModel = _.last(serializerModelPath.split("."))
const serializerModelImportPath = _.initial(
serializerModelPath.split(".")
).join(".")
serializerModelPath.split("."),
).join(".");
let filterPart = `${key}=${value}`
if (valueAsString) {
@ -134,9 +134,10 @@ Cypress.Commands.add("loadAssignmentCompletion", (key, value) => {
value,
"vbv_lernwelt.assignment.models.AssignmentCompletion",
"vbv_lernwelt.assignment.serializers.CypressAssignmentCompletionSerializer",
true
)
})
true,
);
});
Cypress.Commands.add("loadSecurityRequestResponseLog", (key, value) => {
return loadObjectJson(
@ -144,9 +145,10 @@ Cypress.Commands.add("loadSecurityRequestResponseLog", (key, value) => {
value,
"vbv_lernwelt.core.models.SecurityRequestResponseLog",
"vbv_lernwelt.core.serializers.CypressSecurityRequestResponseLogSerializer",
true
)
})
true,
);
});
Cypress.Commands.add("loadExternalApiRequestLog", (key, value) => {
return loadObjectJson(
@ -154,9 +156,10 @@ Cypress.Commands.add("loadExternalApiRequestLog", (key, value) => {
value,
"vbv_lernwelt.core.models.ExternalApiRequestLog",
"vbv_lernwelt.core.serializers.CypressExternalApiRequestLogSerializer",
true
)
})
true,
);
});
Cypress.Commands.add("loadFeedbackResponse", (key, value) => {
return loadObjectJson(
@ -164,9 +167,10 @@ Cypress.Commands.add("loadFeedbackResponse", (key, value) => {
value,
"vbv_lernwelt.feedback.models.FeedbackResponse",
"vbv_lernwelt.feedback.serializers.CypressFeedbackResponseSerializer",
true
)
})
true,
);
});
Cypress.Commands.add("loadCheckoutInformation", (key, value) => {
return loadObjectJson(
@ -174,9 +178,10 @@ Cypress.Commands.add("loadCheckoutInformation", (key, value) => {
value,
"vbv_lernwelt.shop.models.CheckoutInformation",
"vbv_lernwelt.shop.serializers.CypressCheckoutInformationSerializer",
true
)
})
true,
);
});
Cypress.Commands.add("loadUser", (key, value) => {
return loadObjectJson(
@ -188,7 +193,6 @@ Cypress.Commands.add("loadUser", (key, value) => {
);
});
Cypress.Commands.add("loadCourseSessionUser", (key, value) => {
return loadObjectJson(
key,
@ -199,29 +203,33 @@ Cypress.Commands.add("loadCourseSessionUser", (key, value) => {
);
});
Cypress.Commands.add("makeSelfEvaluation", (answers) => {
Cypress.Commands.add("makeSelfEvaluation", (answers, withCompletion = true) => {
for (let i = 0; i < answers.length; i++) {
const answer = answers[i]
const answer = answers[i];
if (answer) {
cy.get('[data-cy="success"]').click()
cy.get('[data-cy="success"]').click();
} 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 {
cy.get('[data-cy="complete-and-continue"]').click({ force: true })
cy.get('[data-cy="next-step"]').click({force: true});
}
}
})
});
Cypress.Commands.add("learningContentMultiLayoutNextStep", () => {
return cy.get('[data-cy="next-step"]').click({ force: true })
return cy.get('[data-cy="next-step"]').click({force: true})
})
Cypress.Commands.add("learningContentMultiLayoutPreviousStep", () => {
return cy.get('[data-cy="previous-step"]').click({ force: true })
return cy.get('[data-cy="previous-step"]').click({force: true})
})
Cypress.Commands.add("testLearningContentTitle", (title) => {

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"
},
"devDependencies": {
"cypress": "^12.15.0",
"cypress-cloud": "^1.7.4"
"cypress": "^12.17.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",
template=EmailTemplate.LEARNING_MENTOR_INVITATION,
template_data={
"inviter_name": f"Daniel Egger",
"inviter_name": "Daniel Egger",
"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",
fail_silently=True,

View File

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

View File

@ -21,6 +21,7 @@ class AssignmentCompletionObjectType(DjangoObjectType):
evaluation_points = graphene.Float()
evaluation_points_final = graphene.Float()
evaluation_max_points = graphene.Float()
evaluation_percent = graphene.Float()
class Meta:
model = AssignmentCompletion
@ -61,6 +62,11 @@ class AssignmentCompletionObjectType(DjangoObjectType):
return round(self.evaluation_max_points, 1) # noqa
return None
def resolve_evaluation_percent(self, info):
if self.evaluation_points:
return self.evaluation_percent
return None
class AssignmentObjectType(DjangoObjectType):
tasks = JSONStreamField()
@ -69,11 +75,11 @@ class AssignmentObjectType(DjangoObjectType):
max_points = graphene.Int()
competence_certificate_weight = graphene.Float()
learning_content = graphene.Field(LearningContentInterface)
completion = graphene.Field(
completions = graphene.List(
AssignmentCompletionObjectType,
course_session_id=graphene.ID(required=True),
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)
@ -103,28 +109,33 @@ class AssignmentObjectType(DjangoObjectType):
def resolve_learning_content(self, info):
return self.find_attached_learning_content()
def resolve_completion(
def resolve_completions(
self,
info,
course_session_id,
learning_content_page_id=None,
assignment_user_id=None,
assignment_user_ids=None,
):
if learning_content_page_id is None:
lp = self.find_attached_learning_content()
if lp:
learning_content_page_id = lp.id
if not assignment_user_id:
assignment_user_id = getattr(info.context, "assignment_user_id", None)
if not assignment_user_ids:
assignment_user_ids = getattr(info.context, "assignment_user_ids", [])
return resolve_assignment_completion(
info=info,
course_session_id=course_session_id,
learning_content_page_id=learning_content_page_id,
assignment_user_id=assignment_user_id,
assignment_id=self.id,
)
completions = []
for user_id in assignment_user_ids:
completion = resolve_assignment_completion(
info=info,
course_session_id=course_session_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(
@ -139,7 +150,7 @@ def resolve_assignment_completion(
if str(assignment_user_id) == str(info.context.user.id) or can_evaluate_assignments(
evaluation_user=info.context.user,
assignment_user_id=assignment_user_id,
assignment_user_ids=[assignment_user_id],
course_session_id=course_session_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 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 = models.ForeignKey(Assignment, 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__)
@api_view(["GET"])
@api_view(["GET", "POST"])
def request_assignment_completion_status(request, assignment_id, course_session_id):
# 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(
course_session_id=course_session_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",
"assignment_user_id",
"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
data = list(qs) # Evaluate the queryset
data = list(values) # Evaluate the queryset
for item in data:
if item["evaluation_points"] is not None:
# only `evaluation_points_final` is relevant for the frontend

View File

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

View File

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

View File

@ -71,177 +71,23 @@ class TestCertificateList(GraphQLTestCase):
def test_supervisor_userprofile_certificate_summary(self):
self.client.force_login(self.supervisor)
query = f"""query competenceCertificateForUserQuery(
query = """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_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!,
) {{
$userIds: [UUID!]
) {
competence_certificate_list(
course_slug: $courseSlug,
) {{
user_ids: $userIds
) {
...CoursePageFields
competence_certificates {{
competence_certificates {
...CoursePageFields
assignments {{
assignments {
...CoursePageFields
assignment_type
max_points
completion(course_session_id: $courseSessionId) {{
completions(course_session_id: $courseSessionId) {
id
completion_status
submitted_at
@ -249,38 +95,39 @@ fragment CoursePageFields on CoursePageInterface {{
evaluation_max_points
evaluation_passed
__typename
}}
learning_content {{
}
learning_content {
...CoursePageFields
circle {{
circle {
id
title
slug
__typename
}}
}
__typename
}}
}
__typename
}}
}
__typename
}}
}
__typename
}}
}}
}
}
fragment CoursePageFields on CoursePageInterface {{
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
@ -297,11 +144,165 @@ fragment CoursePageFields on CoursePageInterface {{
assignments = certificates[0]["assignments"]
self.assertEqual(len(assignments), 2)
completion1 = assignments[0]["completion"]
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]["completion"]
self.assertIsNone(completion2)
completion2 = assignments[1]["completions"]
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.utils.translation import gettext_lazy as _
@ -10,6 +10,9 @@ from vbv_lernwelt.core.models import (
SecurityRequestResponseLog,
)
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()
@ -31,6 +34,26 @@ class LogAdmin(admin.ModelAdmin):
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)
class UserAdmin(auth_admin.UserAdmin):
fieldsets = (
@ -91,6 +114,7 @@ class UserAdmin(auth_admin.UserAdmin):
"sso_id",
]
search_fields = ["first_name", "last_name", "email", "username", "sso_id"]
actions = [create_or_sync_berufsbildner]
@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.services.attendance import AttendanceUserStatus
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 (
LearningContentAttendanceCourse,
LearningContentFeedbackUK,
@ -186,7 +189,9 @@ def command(
SelfEvaluationFeedback.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(language="de")
User.objects.all().update(additional_json_data={})
@ -461,48 +466,40 @@ def command(
if create_learning_mentor:
cs_bern = CourseSession.objects.get(id=TEST_COURSE_SESSION_BERN_ID)
uk_mentor = LearningMentor.objects.create(
mentor=User.objects.get(id=TEST_MENTOR1_USER_ID),
course_session=cs_bern,
)
uk_mentor.participants.add(
CourseSessionUser.objects.get(
user__id=TEST_STUDENT1_USER_ID,
course_session=cs_bern,
)
AgentParticipantRelation.objects.create(
agent=User.objects.get(id=TEST_MENTOR1_USER_ID),
participant=CourseSessionUser.objects.get(
user__id=TEST_STUDENT1_USER_ID, course_session=cs_bern
),
role="LEARNING_MENTOR",
)
vv_course = Course.objects.get(id=COURSE_VERSICHERUNGSVERMITTLERIN_ID)
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(
CourseSessionUser.objects.get(
user__id=TEST_STUDENT1_VV_USER_ID, course_session=vv_course_session
)
)
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(
AgentParticipantRelation.objects.create(
agent=User.objects.get(id=TEST_MENTOR1_USER_ID),
participant=CourseSessionUser.objects.get(
user__id=TEST_STUDENT1_VV_USER_ID,
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)

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.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.notify.models import Notification
@ -71,7 +74,7 @@ def create_or_update_uk(language="de"):
cs = CourseSession.objects.get(import_id=data["ID"])
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_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)
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_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:
CourseCompletion.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()
CourseSessionUser.objects.filter(course_session=cs).delete()
learning_mentor_ids = (
LearningMentor.objects.filter(participants__course_session=cs)
.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()
AgentParticipantRelation.objects.filter(course_session=cs).delete()
else:
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]]
):
for mentor, mentee in mentor_mentee_pairs:
lm = LearningMentor.objects.create(
course_session=course_session,
mentor=mentor,
)
lm.participants.add(
CourseSessionUser.objects.get(
user__id=mentee.id,
course_session=course_session,
)
AgentParticipantRelation.objects.create(
agent=mentor,
participant=CourseSessionUser.objects.get(
user__id=mentee.id, course_session=course_session
),
role=AgentParticipantRoleType.LEARNING_MENTOR.value,
)

View File

@ -139,6 +139,20 @@ class UserSerializer(serializers.ModelSerializer):
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 Meta:
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.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 (
Circle,
LearningContentAssignment,
@ -101,13 +104,13 @@ def create_course_session(
def add_learning_mentor(
course_session: CourseSession, mentor: User, mentee: CourseSessionUser
) -> LearningMentor:
learning_mentor = LearningMentor.objects.create(
course_session=course_session, mentor=mentor
mentor: User, mentee: CourseSessionUser
) -> AgentParticipantRelation:
return AgentParticipantRelation.objects.create(
agent=mentor,
participant=mentee,
role=AgentParticipantRoleType.LEARNING_MENTOR.value,
)
learning_mentor.participants.add(mentee)
return learning_mentor
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,
)
from vbv_lernwelt.competence.models import PerformanceCriteria
from vbv_lernwelt.core.constants import (
TEST_MENTOR1_USER_ID,
TEST_STUDENT2_VV_AND_VV_MENTOR_USER_ID,
)
from vbv_lernwelt.core.constants import TEST_STUDENT2_VV_AND_VV_MENTOR_USER_ID
from vbv_lernwelt.core.create_default_users import default_users
from vbv_lernwelt.core.models import User
from vbv_lernwelt.course.consts import (
@ -95,7 +92,6 @@ from vbv_lernwelt.importer.services import (
import_students_from_excel,
import_trainers_from_excel_for_training,
)
from vbv_lernwelt.learning_mentor.models import LearningMentor
from vbv_lernwelt.learnpath.create_vv_new_learning_path import (
create_vv_motorfahrzeug_pruefung_learning_path,
create_vv_new_learning_path,
@ -245,6 +241,11 @@ def create_versicherungsvermittlerin_course(
course_session=cs,
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(
course_session=cs,
@ -264,30 +265,6 @@ def create_versicherungsvermittlerin_course(
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:
CourseSessionUser.objects.create(
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()
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.user = user
client = Client(schema=schema, context_value=request)
@ -98,7 +98,7 @@ class CourseGraphQLTestCase(TestCase):
self.assertEqual(chosen_profile, profile)
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.user = user
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,
is_circle_expert,
)
from vbv_lernwelt.learning_mentor.models import LearningMentor
from vbv_lernwelt.learning_mentor.models import AgentParticipantRelation
logger = structlog.get_logger(__name__)
@ -155,9 +155,10 @@ def get_course_sessions(request):
# enrich with mentor course sessions
mentor_course_sessions = CourseSession.objects.filter(
id__in=LearningMentor.objects.filter(mentor=request.user).values_list(
"course_session", flat=True
)
id__in=[
rel.participant.course_session_id
for rel in AgentParticipantRelation.objects.filter(agent=request.user)
]
).prefetch_related("course")
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_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
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):
course_statistics = graphene.Field(
CourseStatisticsType, course_id=graphene.ID(required=True)
)
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(
@ -88,28 +119,12 @@ class DashboardQuery(graphene.ObjectType):
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
course = Course.objects.get(id=course_id)
mentees_ids = set()
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
)
return _agent_course_statistics(user, course_id, role=agent_role)
def resolve_dashboard_config(root, info): # noqa
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(
user: User, exclude_course_ids: Set[int]
) -> Tuple[List[Dict[str, str]], Set[int]]:
learning_mentor = LearningMentor.objects.filter(mentor=user).exclude(
course_session__course__id__in=exclude_course_ids
)
learning_mentor_relation_qs = AgentParticipantRelation.objects.filter(
agent=user, role=AgentParticipantRoleType.LEARNING_MENTOR.value
).exclude(participant__course_session__course__id__in=exclude_course_ids)
dashboards = []
course_ids = set()
for mentor in learning_mentor:
course = mentor.course_session.course
course_ids.add(course.id)
for rel in learning_mentor_relation_qs:
course = rel.participant.course_session.course
if course.id in UK_COURSE_IDS:
dashboard_type = DashboardType.PRAXISBILDNER_DASHBOARD
else:
dashboard_type = DashboardType.MENTOR_DASHBOARD
dashboards.append(
{
"id": str(course.id),
"name": course.title,
"slug": course.slug,
"dashboard_type": dashboard_type,
"course_configuration": course.configuration,
}
)
if course.id not in course_ids:
course_ids.add(course.id)
dashboards.append(
{
"id": str(course.id),
"name": course.title,
"slug": course.slug,
"dashboard_type": dashboard_type,
"course_configuration": course.configuration,
}
)
return dashboards, course_ids

View File

@ -26,6 +26,8 @@ class AssignmentCompletionMetricsType(graphene.ObjectType):
unranked_count = graphene.Int(required=True)
ranking_completed = graphene.Boolean(required=True)
average_passed = graphene.Float(required=True)
average_evaluation_percent = graphene.Float()
competence_certificate_weight = graphene.Float()
class AssignmentStatisticsRecordType(graphene.ObjectType):
@ -34,10 +36,15 @@ class AssignmentStatisticsRecordType(graphene.ObjectType):
course_session_assignment_id = graphene.ID(required=True)
circle_id = graphene.ID(required=True)
generation = graphene.String(required=True)
region = graphene.String(required=True)
assignment_type_translation_key = graphene.String(required=True)
assignment_title = graphene.String(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)
learning_content_id = graphene.ID(required=True)
details_url = graphene.String(required=True)
@ -47,6 +54,7 @@ class AssignmentStatisticsSummaryType(graphene.ObjectType):
average_passed = graphene.Float(required=True)
total_passed = graphene.Int(required=True)
total_failed = graphene.Int(required=True)
average_evaluation_percent = graphene.Float()
class AssignmentsStatisticsType(graphene.ObjectType):
@ -83,12 +91,17 @@ def create_assignment_summary(
total_passed = sum([m.passed_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(
_id=urql_id, # noqa
completed_count=completed_count, # noqa
average_passed=average_passed_completed, # noqa
total_passed=total_passed, # 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:
context = {}
if user_selection_ids:
course_session_users = user_selection_ids
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]
csu_qs = CourseSessionUser.objects.filter(
course_session_id=course_session.id, role=CourseSessionUser.Role.MEMBER
)
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,
assignment_user__in=course_session_users,
course_session=course_session,
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])
failed_count = len(evaluation_results) - passed_count
passed_count = len([passed for passed in evaluation_passed_results if passed])
failed_count = len(evaluation_passed_results) - passed_count
participants_count = len(course_session_users)
unranked_count = participants_count - passed_count - failed_count
@ -133,6 +147,17 @@ def get_assignment_completion_metrics(
else:
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(
_id=f"{course_session.id}-{assignment.id}@{urql_id_postfix}", # noqa
passed_count=passed_count, # noqa
@ -140,6 +165,8 @@ def get_assignment_completion_metrics(
unranked_count=unranked_count, # noqa
ranking_completed=(passed_count > 0 or failed_count > 0), # 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]
learning_content = course_session_assignment.learning_content
competence_certificate = learning_content.content_assignment.competence_certificate
return (
AssignmentStatisticsRecordType(
@ -178,12 +206,16 @@ def create_record(
_id=f"{course_session_assignment._meta.model_name}#{course_session_assignment.id}@{urql_id_postfix}",
# 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
course_session_assignment_id=str(course_session_assignment.id), # noqa
generation=course_session_assignment.course_session.generation, # noqa
assignment_type_translation_key=due_date.assignment_type_translation_key,
# noqa
region=course_session_assignment.course_session.region, # 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
learning_content_id=str(learning_content.id), # noqa
metrics=get_assignment_completion_metrics( # noqa
course_session=course_session_assignment.course_session, # noqa
assignment=learning_content.content_assignment, # noqa

View File

@ -19,6 +19,7 @@ class PresenceRecordStatisticsType(graphene.ObjectType):
_id = graphene.ID(required=True)
course_session_id = graphene.ID(required=True)
generation = graphene.String(required=True)
region = graphene.String(required=True)
circle_id = graphene.ID(required=True)
due_date = graphene.DateTime(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
course_session_id=course_session.id, # noqa
generation=course_session.generation, # noqa
region=course_session.region, # noqa
circle_id=circle.id, # noqa
due_date=attendance_day.due_date.end, # noqa
participants_present=participants_present, # noqa
@ -98,7 +100,9 @@ def attendance_day_presences(
)
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)
course_session_id = graphene.ID(required=True)
generation = graphene.String(required=True)
region = graphene.String(required=True)
title = graphene.String(required=True)
circle_id = graphene.ID(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}"
if combined_id not in competence_records:
competence_records[combined_id] = {}
if learning_unit not in competence_records[combined_id]:
competence_records[combined_id][
learning_unit
] = CompetenceRecordStatisticsType(
_id=combined_id,
title=learning_unit.title,
course_session_id=completion.course_session.id,
generation=completion.course_session.generation,
circle_id=circle.id,
success_count=0,
fail_count=0,
competence_records.setdefault(combined_id, {}).setdefault(
learning_unit,
CompetenceRecordStatisticsType(
_id=combined_id, # noqa
title=learning_unit.title, # noqa
course_session_id=completion.course_session.id, # noqa
generation=completion.course_session.generation, # noqa
region=completion.course_session.region, # noqa
circle_id=circle.id, # noqa
success_count=0, # noqa
fail_count=0, # noqa
details_url=f"/course/{course_slug}/cockpit?courseSessionId={completion.course_session.id}",
# noqa
)
),
)
if completion.completion_status == CourseCompletionStatus.SUCCESS.value:
competence_records[combined_id][learning_unit].success_count += 1
@ -100,7 +99,7 @@ def competences(
for record in circle_records.values()
]
success_count = sum(c.success_count for c in values)
fail_count = sum(c.fail_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])
return values, success_count, fail_count

View File

@ -26,6 +26,7 @@ from vbv_lernwelt.learnpath.models import Circle
class StatisticsCourseSessionDataType(graphene.ObjectType):
id = graphene.ID(required=True)
name = graphene.String(required=True)
region = graphene.String(required=True)
class StatisticsCircleDataType(graphene.ObjectType):
@ -96,6 +97,10 @@ class BaseStatisticsType(graphene.ObjectType):
user_selection_ids = graphene.List(graphene.ID, required=False)
assignments = graphene.Field(AssignmentsStatisticsType, required=True)
course_session_properties = graphene.Field(
StatisticsCourseSessionPropertiesType, required=True
)
def resolve_assignments(root, _info) -> AssignmentsStatisticsType:
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),
)
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):
return getattr(info.context, "circle_ids", None)
class CourseStatisticsType(BaseStatisticsType):
course_session_properties = graphene.Field(
StatisticsCourseSessionPropertiesType, required=True
)
course_session_selection_metrics = graphene.Field(
StatisticsCourseSessionsSelectionMetricType, required=True
)
@ -195,46 +241,3 @@ class CourseStatisticsType(BaseStatisticsType):
participant_count=participant_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)
course_session_id = graphene.ID(required=True)
generation = graphene.String(required=True)
region = graphene.String(required=True)
circle_id = graphene.ID(required=True)
satisfaction_average = graphene.Float(required=True)
satisfaction_max = graphene.Int(required=True)
@ -68,6 +69,7 @@ def feedback_responses(
feedbacks=fbs,
course_session_id=course_session.id,
generation=course_session.generation,
region=course_session.region,
course_slug=str(course_slug),
urql_id_postfix=urql_id,
)
@ -96,6 +98,7 @@ def circle_feedback_average(
feedbacks: List[FeedbackResponse],
course_session_id,
generation: str,
region: str,
course_slug: 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
course_session_id=course_session_id, # noqa
generation=generation, # noqa
region=region, # noqa
circle_id=circle_id, # noqa
satisfaction_average=data["total"] / data["count"], # noqa
satisfaction_max=4, # noqa

View File

@ -110,22 +110,22 @@ class DashboardTestCase(GraphQLTestCase):
self.client.force_login(member)
query = f"""query($course_id: ID!) {{
course_progress(course_id: $course_id) {{
query = """query($course_id: ID!) {
course_progress(course_id: $course_id) {
course_id
session_to_continue_id
competence {{
competence {
total_count
success_count
fail_count
}}
assignment {{
}
assignment {
total_count
points_max_count
points_achieved_count
}}
}}
}}
}
}
}
"""
variables = {"course_id": str(course.id)}
@ -268,7 +268,7 @@ class DashboardTestCase(GraphQLTestCase):
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)
@ -287,6 +287,7 @@ class DashboardTestCase(GraphQLTestCase):
# THEN
self.assertResponseNoErrors(response)
print(response.json())
self.assertEqual(len(response.json()["data"]["dashboard_config"]), 1)
self.assertEqual(
@ -314,7 +315,7 @@ class DashboardTestCase(GraphQLTestCase):
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
self.client.force_login(mentor_and_member)
@ -350,11 +351,11 @@ class DashboardTestCase(GraphQLTestCase):
self.client.force_login(disallowed_user)
query = f"""query($course_id: ID!) {{
course_statistics(course_id: $course_id) {{
query = """query($course_id: ID!) {
course_statistics(course_id: $course_id) {
course_id
}}
}}
}
}
"""
variables = {"course_id": str(course.id)}
@ -384,13 +385,13 @@ class DashboardTestCase(GraphQLTestCase):
self.client.force_login(supervisor)
query = f"""query($course_id: ID!) {{
course_statistics(course_id: $course_id) {{
query = """query($course_id: ID!) {
course_statistics(course_id: $course_id) {
course_id
course_title
course_slug
}}
}}
}
}
"""
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.models import CourseSessionUser
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):
@ -19,15 +19,13 @@ class MentorStatisticsTestCase(BaseMentorAssignmentTestCase, GraphQLTestCase):
self.course.configuration.save()
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)]
def test_assignment_statistics(self):
# WHEN
has_lb = [True, True, True, False]
has_passed = [True, False, True, False]
for i in range(4):
csu = add_course_session_user(
self.course_session,
@ -35,7 +33,9 @@ class MentorStatisticsTestCase(BaseMentorAssignmentTestCase, GraphQLTestCase):
role=CourseSessionUser.Role.MEMBER,
)
if has_lb[i]:
self.lm.participants.add(csu)
AgentParticipantRelation.objects.create(
agent=self.mentor, participant=csu, role="LEARNING_MENTOR"
)
AssignmentCompletion.objects.create(
course_session=self.course_session,
@ -47,25 +47,25 @@ class MentorStatisticsTestCase(BaseMentorAssignmentTestCase, GraphQLTestCase):
)
# THEN
# WHEN
query = f"""query ($courseId: ID!) {{
mentor_course_statistics(course_id: $courseId) {{
query = """query ($courseId: ID!, $agentRole: String!) {
mentor_course_statistics(course_id: $courseId, agent_role: $agentRole) {
course_session_selection_ids
user_selection_ids
assignments {{
assignments {
_id
summary {{
summary {
_id
completed_count
average_passed
total_passed
total_failed
}}
}}
}}
}}"""
}
}
}
}"""
# THEN
variables = {"courseId": str(self.course.id)}
variables = {"courseId": str(self.course.id), "agentRole": "LEARNING_MENTOR"}
self.client.force_login(self.mentor)
response = self.query(query, variables=variables)
self.assertResponseNoErrors(response)

View File

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

View File

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

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