Merged in feature/vbv-676-berufsbildner-2 (pull request #365)
Feature/vbv 676 berufsbildner 2
This commit is contained in:
commit
96dfa8bd31
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ const feebackSummary = computed(() => {
|
|||
});
|
||||
|
||||
onMounted(async () => {
|
||||
statistics.value = await dashboardStore.loadStatisticsDatav2(props.courseId);
|
||||
statistics.value = await dashboardStore.loadStatisticsData(props.courseId);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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") }}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ export interface Props {
|
|||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
label: undefined,
|
||||
cyKey: "",
|
||||
cyKey: "default",
|
||||
placeholder: "",
|
||||
});
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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") }}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,8 +23,9 @@ export default defineConfig(({ mode }) => {
|
|||
],
|
||||
define: {},
|
||||
server: {
|
||||
host: true,
|
||||
port: 5173,
|
||||
hmr: { port: 5173 },
|
||||
strictPort: true,
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
@ -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",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import { runSql } from "./tasks.mjs";
|
||||
|
||||
export default (on, config) => {
|
||||
on("task", {
|
||||
runSql,
|
||||
});
|
||||
|
||||
return config;
|
||||
};
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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) => {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
@ -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 = []
|
||||
|
|
@ -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 = []
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 = (
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in New Issue