Merged in feature/VBV-496-dash-regionalleiter (pull request #224)
Feature/VBV-496 dash regionalleiter Approved-by: Daniel Egger
This commit is contained in:
commit
25c6021b82
|
|
@ -0,0 +1,48 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from "vue";
|
||||||
|
import ItProgress from "@/components/ui/ItProgress.vue";
|
||||||
|
import BaseBox from "@/components/dashboard/BaseBox.vue";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
detailsLink: string;
|
||||||
|
totalAssignments: number;
|
||||||
|
achievedPointsCount: number;
|
||||||
|
maxPointsCount: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const progress = computed(() => ({
|
||||||
|
SUCCESS: props.achievedPointsCount,
|
||||||
|
FAIL: 0,
|
||||||
|
UNKNOWN: props.maxPointsCount - props.achievedPointsCount,
|
||||||
|
}));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<BaseBox :details-link="detailsLink">
|
||||||
|
<template #title>{{ $t("a.Kompetenznachweis-Elemente") }}</template>
|
||||||
|
<template #content>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i18next :translation="$t('a.NUMBER Elemente abgeschlossen')">
|
||||||
|
<template #NUMBER>
|
||||||
|
<span class="mr-3 text-4xl font-bold">{{ totalAssignments }}</span>
|
||||||
|
</template>
|
||||||
|
</i18next>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i18next :translation="$t('a.xOfY Punkten erreicht')">
|
||||||
|
<template #xOfY>
|
||||||
|
<span class="mr-3 text-4xl font-bold">
|
||||||
|
{{
|
||||||
|
$t("a.VALUE von MAXIMUM", {
|
||||||
|
VALUE: props.achievedPointsCount,
|
||||||
|
MAXIMUM: props.maxPointsCount,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</i18next>
|
||||||
|
</div>
|
||||||
|
<ItProgress :status-count="progress"></ItProgress>
|
||||||
|
</template>
|
||||||
|
</BaseBox>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import ItProgress from "@/components/ui/ItProgress.vue";
|
||||||
|
import { computed } from "vue";
|
||||||
|
import BaseBox from "@/components/dashboard/BaseBox.vue";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
assignmentsCompleted: number;
|
||||||
|
avgPassed: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const progress = computed(() => {
|
||||||
|
return {
|
||||||
|
SUCCESS: props.avgPassed,
|
||||||
|
FAIL: 100 - props.avgPassed,
|
||||||
|
UNKNOWN: 0,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<BaseBox
|
||||||
|
:details-link="'/statistic/assignment'"
|
||||||
|
data-cy="dashboard.stats.assignments"
|
||||||
|
>
|
||||||
|
<template #title>{{ $t("a.Kompetenznachweis-Elemente") }}</template>
|
||||||
|
<template #content>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i18next :translation="$t('a.NUMBER Elemente abgeschlossen')">
|
||||||
|
<template #NUMBER>
|
||||||
|
<span
|
||||||
|
class="mr-3 text-4xl font-bold"
|
||||||
|
data-cy="dashboard.stats.assignments.completed"
|
||||||
|
>
|
||||||
|
{{ assignmentsCompleted }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</i18next>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i18next :translation="$t('a.{NUMBER} Bestanden')">
|
||||||
|
<template #NUMBER>
|
||||||
|
<span
|
||||||
|
class="mr-3 text-4xl font-bold"
|
||||||
|
data-cy="dashboard.stats.assignments.passed"
|
||||||
|
>
|
||||||
|
{{ `${Math.round(props.avgPassed)}%` }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</i18next>
|
||||||
|
</div>
|
||||||
|
<ItProgress :status-count="progress"></ItProgress>
|
||||||
|
</template>
|
||||||
|
</BaseBox>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import ItProgress from "@/components/ui/ItProgress.vue";
|
||||||
|
import { computed } from "vue";
|
||||||
|
import BaseBox from "@/components/dashboard/BaseBox.vue";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
daysCompleted: number;
|
||||||
|
avgParticipantsPresent: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const progressRecord = computed(() => {
|
||||||
|
return {
|
||||||
|
SUCCESS: props.avgParticipantsPresent,
|
||||||
|
FAIL: 100 - props.avgParticipantsPresent,
|
||||||
|
UNKNOWN: 0,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<BaseBox :details-link="'/statistic/attendance'" data-cy="dashboard.stats.attendance">
|
||||||
|
<template #title>{{ $t("a.Anwesenheit") }}</template>
|
||||||
|
<template #content>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i18next :translation="$t('a.NUMBER Präsenztage abgeschlossen')">
|
||||||
|
<template #NUMBER>
|
||||||
|
<span
|
||||||
|
class="mr-3 text-4xl font-bold"
|
||||||
|
data-cy="dashboard.stats.attendance.dayCompleted"
|
||||||
|
>
|
||||||
|
{{ daysCompleted }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</i18next>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i18next :translation="$t('a.NUMBER Teilnehmer anwesend')">
|
||||||
|
<template #NUMBER>
|
||||||
|
<span
|
||||||
|
class="mr-3 text-4xl font-bold"
|
||||||
|
data-cy="dashboard.stats.attendance.participantsPresent"
|
||||||
|
>
|
||||||
|
{{ `${avgParticipantsPresent}%` }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</i18next>
|
||||||
|
</div>
|
||||||
|
<ItProgress :status-count="progressRecord"></ItProgress>
|
||||||
|
</template>
|
||||||
|
</BaseBox>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineProps<{
|
||||||
|
detailsLink: string;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col space-y-4 bg-white p-6">
|
||||||
|
<h4 class="mb-1 font-bold">
|
||||||
|
<slot name="title"></slot>
|
||||||
|
</h4>
|
||||||
|
<slot name="content"></slot>
|
||||||
|
<div class="flex-grow"></div>
|
||||||
|
<div class="pt-8">
|
||||||
|
<router-link class="underline" :to="detailsLink" data-cy="basebox.detailsLink">
|
||||||
|
{{ $t("a.Details anschauen") }}
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import BaseBox from "@/components/dashboard/BaseBox.vue";
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
failCount: number;
|
||||||
|
successCount: number;
|
||||||
|
detailsLink: string;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<BaseBox :details-link="detailsLink" data-cy="dashboard.stats.competence">
|
||||||
|
<template #title>{{ $t("a.Selbsteinschätzungen") }}</template>
|
||||||
|
<template #content>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<it-icon-smiley-happy class="mr-4 h-12 w-12"></it-icon-smiley-happy>
|
||||||
|
<i18next :translation="$t('a.{NUMBER} Das kann ich')">
|
||||||
|
<template #NUMBER>
|
||||||
|
<span
|
||||||
|
class="mr-3 text-4xl font-bold"
|
||||||
|
data-cy="dashboard.stats.competence.success"
|
||||||
|
>
|
||||||
|
{{ successCount }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</i18next>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<it-icon-smiley-thinking class="mr-4 h-12 w-12"></it-icon-smiley-thinking>
|
||||||
|
<i18next :translation="$t('a.{NUMBER} Das will ich nochmals anschauen')">
|
||||||
|
<template #NUMBER>
|
||||||
|
<span
|
||||||
|
class="mr-3 text-4xl font-bold"
|
||||||
|
data-cy="dashboard.stats.competence.fail"
|
||||||
|
>
|
||||||
|
{{ failCount }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</i18next>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</BaseBox>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import DueDatesList from "@/components/dueDates/DueDatesList.vue";
|
||||||
|
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||||
|
import { useDashboardStore } from "@/stores/dashboard";
|
||||||
|
import { getCockpitUrl } from "@/utils/utils";
|
||||||
|
|
||||||
|
const dashboardStore = useDashboardStore();
|
||||||
|
const courseSessionsStore = useCourseSessionsStore();
|
||||||
|
const allDueDates = courseSessionsStore.allDueDates();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<template v-if="dashboardStore.currentDashboardConfig">
|
||||||
|
<h4 class="mb-6 text-xl font-bold">{{ $t("a.Aktueller Lehrgang") }}</h4>
|
||||||
|
|
||||||
|
<div class="mb-6 border border-gray-300 p-6">
|
||||||
|
<h3 class="mb-6">{{ dashboardStore.currentDashboardConfig.name }}</h3>
|
||||||
|
<router-link
|
||||||
|
class="btn-blue"
|
||||||
|
target="_blank"
|
||||||
|
:to="getCockpitUrl(dashboardStore.currentDashboardConfig.slug)"
|
||||||
|
>
|
||||||
|
{{ $t("a.Cockpit anschauen") }}
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<router-link class="mb-16 block text-sm underline" to="/statistic/list">
|
||||||
|
{{ $t("a.Alle Lehrgänge anzeigen") }}
|
||||||
|
</router-link>
|
||||||
|
|
||||||
|
<h3 class="mb-6 text-xl font-bold">{{ $t("a.AlleTermine") }}</h3>
|
||||||
|
<DueDatesList
|
||||||
|
:due-dates="allDueDates"
|
||||||
|
:max-count="13"
|
||||||
|
:show-top-border="true"
|
||||||
|
:show-all-due-dates-link="true"
|
||||||
|
:show-bottom-border="true"
|
||||||
|
:show-course-session="true"
|
||||||
|
></DueDatesList>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineProps<{
|
||||||
|
participantCount: number;
|
||||||
|
expertCount: number;
|
||||||
|
sessionCount: number;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex items-center justify-between bg-white p-6">
|
||||||
|
<div class="flex space-x-6">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<span class="text-4xl font-bold" data-cy="dashboard.stats.participant.count">
|
||||||
|
{{ participantCount }}
|
||||||
|
</span>
|
||||||
|
<span>{{ $t("a.Teilnehmer") }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<span class="text-4xl font-bold" data-cy="dashboard.stats.expert.count">
|
||||||
|
{{ expertCount }}
|
||||||
|
</span>
|
||||||
|
<span>{{ $t("a.Trainer") }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<span class="text-4xl font-bold" data-cy="dashboard.stats.session.count">
|
||||||
|
{{ sessionCount }}
|
||||||
|
</span>
|
||||||
|
<span>{{ $t("a.Durchführungen") }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import BaseBox from "@/components/dashboard/BaseBox.vue";
|
||||||
|
import { getBlendedColorForRating } from "@/utils/ratingToColor";
|
||||||
|
import { computed } from "vue";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
feedbackCount: number;
|
||||||
|
statisfactionMax: number;
|
||||||
|
statisfactionAvg: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const satisfactionColor = computed(() => {
|
||||||
|
return getBlendedColorForRating(props.statisfactionAvg);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<BaseBox :details-link="'/statistic/feedback'" data-cy="dashboard.stats.feedback">
|
||||||
|
<template #title>{{ $t("a.Feedback Teilnehmer") }}</template>
|
||||||
|
<template #content>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<!-- Left Pane -->
|
||||||
|
<div
|
||||||
|
class="mr-3 flex items-center justify-center rounded p-4"
|
||||||
|
:style="{ backgroundColor: satisfactionColor }"
|
||||||
|
>
|
||||||
|
<i18next :translation="$t('a.{AVG} von {MAX}')">
|
||||||
|
<template #AVG>
|
||||||
|
<span
|
||||||
|
class="pr-2 text-4xl font-bold"
|
||||||
|
data-cy="dashboard.stats.feedback.average"
|
||||||
|
>
|
||||||
|
{{ props.statisfactionAvg.toFixed(1) }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template #MAX>
|
||||||
|
<span class="pl-1 text-sm">{{ props.statisfactionMax }}</span>
|
||||||
|
</template>
|
||||||
|
</i18next>
|
||||||
|
</div>
|
||||||
|
<!-- Right Pane -->
|
||||||
|
<div class="flex flex-col items-center space-y-1">
|
||||||
|
<span class="font-bold">{{ $t("a.Allgemeine Zufriedenheit") }}</span>
|
||||||
|
<div class="self-start">
|
||||||
|
<i18next :translation="$t('a.Total {NUMBER} Antworten')">
|
||||||
|
<template #NUMBER>
|
||||||
|
<span class="font-bold" data-cy="dashboard.stats.feedback.count">
|
||||||
|
{{ props.feedbackCount }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</i18next>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</BaseBox>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import DueDatesList from "@/components/dueDates/DueDatesList.vue";
|
||||||
|
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||||
|
|
||||||
|
const courseSessionsStore = useCourseSessionsStore();
|
||||||
|
const allDueDates = courseSessionsStore.allDueDates();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<h4 class="mb-6 text-xl font-bold">{{ $t("a.AlleTermine") }}</h4>
|
||||||
|
<DueDatesList
|
||||||
|
:due-dates="allDueDates"
|
||||||
|
:max-count="13"
|
||||||
|
:show-top-border="true"
|
||||||
|
:show-all-due-dates-link="true"
|
||||||
|
:show-bottom-border="true"
|
||||||
|
:show-course-session="true"
|
||||||
|
></DueDatesList>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,104 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, watch } from "vue";
|
||||||
|
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
|
||||||
|
import { useTranslation } from "i18next-vue";
|
||||||
|
import type { StatisticsCourseSessionPropertiesType } from "@/gql/graphql";
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
interface Item {
|
||||||
|
_id: string;
|
||||||
|
course_session_id: string;
|
||||||
|
generation: string;
|
||||||
|
circle_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
items: Item[];
|
||||||
|
courseSessionProperties: StatisticsCourseSessionPropertiesType;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const sessionFilter = computed(() => {
|
||||||
|
const f = props.courseSessionProperties.sessions.map((session) => ({
|
||||||
|
name: `${t("a.Durchfuehrung")}: ${session.name}`,
|
||||||
|
id: session.id,
|
||||||
|
}));
|
||||||
|
return [{ name: t("a.AlleDurchführungen"), id: "_all" }, ...f];
|
||||||
|
});
|
||||||
|
|
||||||
|
const generationFilter = computed(() => {
|
||||||
|
const f = props.courseSessionProperties.generations.map((generation) => ({
|
||||||
|
name: `${t("a.Generation")}: ${generation}`,
|
||||||
|
id: generation,
|
||||||
|
}));
|
||||||
|
return [{ name: t("a.AlleGenerationen"), id: "_all" }, ...f];
|
||||||
|
});
|
||||||
|
|
||||||
|
const circleFilter = computed(() => {
|
||||||
|
const f = props.courseSessionProperties.circles.map((circle) => ({
|
||||||
|
name: `Circle: ${circle.name}`,
|
||||||
|
id: circle.id,
|
||||||
|
}));
|
||||||
|
return [{ name: t("a.AlleCircle"), id: "_all" }, ...f];
|
||||||
|
});
|
||||||
|
|
||||||
|
const sessionFilterValue = ref(sessionFilter.value[0]);
|
||||||
|
const generationFilterValue = ref(generationFilter.value[0]);
|
||||||
|
const circleFilterValue = ref(circleFilter.value[0]);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.courseSessionProperties,
|
||||||
|
() => {
|
||||||
|
sessionFilterValue.value = sessionFilter.value[0];
|
||||||
|
generationFilterValue.value = generationFilter.value[0];
|
||||||
|
circleFilterValue.value = circleFilter.value[0];
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
const filteredItems = computed(() => {
|
||||||
|
return props.items.filter((item) => {
|
||||||
|
const sessionMatch =
|
||||||
|
sessionFilterValue.value.id === "_all" ||
|
||||||
|
item.course_session_id === sessionFilterValue.value.id;
|
||||||
|
const generationMatch =
|
||||||
|
generationFilterValue.value.id === "_all" ||
|
||||||
|
item.generation === generationFilterValue.value.id;
|
||||||
|
const circleMatch =
|
||||||
|
circleFilterValue.value.id === "_all" ||
|
||||||
|
item.circle_id === circleFilterValue.value.id;
|
||||||
|
|
||||||
|
return sessionMatch && generationMatch && circleMatch;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="flex flex-col space-x-2 lg:flex-row">
|
||||||
|
<ItDropdownSelect
|
||||||
|
v-model="sessionFilterValue"
|
||||||
|
class="min-w-[18rem]"
|
||||||
|
:items="sessionFilter"
|
||||||
|
borderless
|
||||||
|
></ItDropdownSelect>
|
||||||
|
<ItDropdownSelect
|
||||||
|
v-model="generationFilterValue"
|
||||||
|
class="min-w-[18rem]"
|
||||||
|
:items="generationFilter"
|
||||||
|
borderless
|
||||||
|
></ItDropdownSelect>
|
||||||
|
<ItDropdownSelect
|
||||||
|
v-model="circleFilterValue"
|
||||||
|
class="min-w-[18rem]"
|
||||||
|
: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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -36,23 +36,16 @@ const circles = computed(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const wrapperClasses = computed(() => {
|
const wrapperClasses = computed(() => {
|
||||||
let classes = "flex my-5";
|
let classes = "flex";
|
||||||
if (props.diagramType === "horizontal") {
|
if (props.diagramType === "horizontal") {
|
||||||
classes += " flex-row h-8";
|
classes += " flex-row h-8 space-x-2";
|
||||||
} else if (props.diagramType === "horizontalSmall") {
|
} else if (props.diagramType === "horizontalSmall") {
|
||||||
classes += " flex-row h-5";
|
classes += " flex-row h-5 space-x-1";
|
||||||
} else if (props.diagramType === "singleSmall") {
|
} else if (props.diagramType === "singleSmall") {
|
||||||
classes += " h-8";
|
classes += " h-8";
|
||||||
}
|
}
|
||||||
return classes;
|
return classes;
|
||||||
});
|
});
|
||||||
|
|
||||||
const circleClasses = computed(() => {
|
|
||||||
if (props.diagramType === "horizontal" || props.diagramType === "horizontalSmall") {
|
|
||||||
return "pl-1";
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -60,7 +53,6 @@ const circleClasses = computed(() => {
|
||||||
<LearningPathCircle
|
<LearningPathCircle
|
||||||
v-for="circle in circles"
|
v-for="circle in circles"
|
||||||
:key="circle.id"
|
:key="circle.id"
|
||||||
:class="circleClasses"
|
|
||||||
:sectors="calculateCircleSectorData(circle)"
|
:sectors="calculateCircleSectorData(circle)"
|
||||||
></LearningPathCircle>
|
></LearningPathCircle>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<template>
|
||||||
|
<div role="status">
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="mr-2 h-8 w-8 animate-spin fill-blue-900 text-gray-200 dark:text-gray-600"
|
||||||
|
viewBox="0 0 100 101"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
||||||
|
fill="currentFill"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span class="sr-only">Loading...</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -77,15 +77,10 @@ import { Popover, PopoverButton, PopoverPanel } from "@headlessui/vue";
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
import { useTranslation } from "i18next-vue";
|
import { useTranslation } from "i18next-vue";
|
||||||
import log from "loglevel";
|
import log from "loglevel";
|
||||||
|
import { getBlendedColorForRating } from "@/utils/ratingToColor";
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
type RGB = [number, number, number];
|
|
||||||
const red: RGB = [221, 103, 81]; // red-600
|
|
||||||
const yellow: RGB = [250, 200, 82]; // yellow-500
|
|
||||||
const lightGreen: RGB = [120, 222, 163]; // green-500
|
|
||||||
const darkGreen: RGB = [91, 183, 130]; // green-600
|
|
||||||
|
|
||||||
const legends = [
|
const legends = [
|
||||||
{ index: 1, label: t("feedback.veryUnhappy") },
|
{ index: 1, label: t("feedback.veryUnhappy") },
|
||||||
{ index: 2, label: t("feedback.unhappy") },
|
{ index: 2, label: t("feedback.unhappy") },
|
||||||
|
|
@ -101,19 +96,11 @@ const props = defineProps<{
|
||||||
|
|
||||||
log.debug("RatingScale created", props);
|
log.debug("RatingScale created", props);
|
||||||
|
|
||||||
const rating = computed((): number => {
|
const rating = computed(() => {
|
||||||
const sum = props.ratings.reduce((a, b) => a + b, 0);
|
const sum = props.ratings.reduce((a, b) => a + b, 0);
|
||||||
return sum / props.ratings.length;
|
return sum / props.ratings.length;
|
||||||
});
|
});
|
||||||
|
|
||||||
const weight = computed(() => {
|
|
||||||
return rating.value % 1;
|
|
||||||
});
|
|
||||||
|
|
||||||
const scale = computed(() => {
|
|
||||||
return Math.floor(rating.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
const answers = computed(() => props.ratings.length);
|
const answers = computed(() => props.ratings.length);
|
||||||
|
|
||||||
const numberOfRatings = computed(() => {
|
const numberOfRatings = computed(() => {
|
||||||
|
|
@ -122,57 +109,14 @@ const numberOfRatings = computed(() => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const colors = computed(() => {
|
|
||||||
switch (scale.value) {
|
|
||||||
case 1:
|
|
||||||
return [red, yellow];
|
|
||||||
case 2:
|
|
||||||
return [yellow, lightGreen];
|
|
||||||
case 3:
|
|
||||||
default:
|
|
||||||
return [lightGreen, darkGreen];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const blendColorValue = (v1: number, v2: number, weight: number) => {
|
|
||||||
return v1 * (1 - weight) + v2 * weight;
|
|
||||||
};
|
|
||||||
|
|
||||||
const blendColors = (c1: RGB, c2: RGB, weight: number): RGB => {
|
|
||||||
const [r1, g1, b1] = c1;
|
|
||||||
const [r2, g2, b2] = c2;
|
|
||||||
return [
|
|
||||||
blendColorValue(r1, r2, weight),
|
|
||||||
blendColorValue(g1, g2, weight),
|
|
||||||
blendColorValue(b1, b2, weight),
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
const getRGBString = ([r, g, b]: RGB) => {
|
|
||||||
return `rgb(${Math.floor(r)}, ${Math.floor(g)}, ${Math.floor(b)})`;
|
|
||||||
};
|
|
||||||
|
|
||||||
// const getRGBStyle = (c1: RGB, c2: RGB, weight: number) => {
|
|
||||||
// return getRGBString(blendColors(c1, c2, weight));
|
|
||||||
// };
|
|
||||||
|
|
||||||
const percent = computed(() => {
|
const percent = computed(() => {
|
||||||
return (scale.value - 1) * 33.33 + weight.value * 33.33;
|
return (Math.floor(rating.value) - 1) * 33.33 + (rating.value % 1) * 33.33;
|
||||||
});
|
});
|
||||||
|
|
||||||
const leftPosition = computed(() => {
|
const leftPosition = computed(() => `${percent.value.toPrecision(3)}%`);
|
||||||
return `${percent.value.toPrecision(3)}%`;
|
const rightClip = computed(() => `${Math.round(100 - percent.value)}%`);
|
||||||
});
|
|
||||||
|
|
||||||
const rightClip = computed(() => {
|
const backgroundColor = getBlendedColorForRating(rating.value);
|
||||||
return `${Math.round(100 - percent.value)}%`;
|
|
||||||
});
|
|
||||||
|
|
||||||
const blendedColor = computed(() => {
|
|
||||||
return blendColors(colors.value[0], colors.value[1], weight.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
const backgroundColor = getRGBString(blendedColor.value);
|
|
||||||
|
|
||||||
const circleStyle = {
|
const circleStyle = {
|
||||||
backgroundColor,
|
backgroundColor,
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import type { CourseStatisticsType } from "@/gql/graphql";
|
||||||
import { graphqlClient } from "@/graphql/client";
|
import { graphqlClient } from "@/graphql/client";
|
||||||
import { COURSE_QUERY, COURSE_SESSION_DETAIL_QUERY } from "@/graphql/queries";
|
import { COURSE_QUERY, COURSE_SESSION_DETAIL_QUERY } from "@/graphql/queries";
|
||||||
import {
|
import {
|
||||||
|
|
@ -7,6 +8,7 @@ import {
|
||||||
} from "@/services/circle";
|
} from "@/services/circle";
|
||||||
import { useCompletionStore } from "@/stores/completion";
|
import { useCompletionStore } from "@/stores/completion";
|
||||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||||
|
import { useDashboardStore } from "@/stores/dashboard";
|
||||||
import { useUserStore } from "@/stores/user";
|
import { useUserStore } from "@/stores/user";
|
||||||
import type {
|
import type {
|
||||||
ActionCompetence,
|
ActionCompetence,
|
||||||
|
|
@ -411,3 +413,25 @@ export function useCourseDataWithCompletion(
|
||||||
nextLearningContent,
|
nextLearningContent,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,9 @@ const documents = {
|
||||||
"\n query competenceCertificateQuery($courseSlug: String!, $courseSessionId: ID!) {\n competence_certificate_list(course_slug: $courseSlug) {\n ...CoursePageFields\n competence_certificates {\n ...CoursePageFields\n assignments {\n ...CoursePageFields\n assignment_type\n max_points\n completion(course_session_id: $courseSessionId) {\n id\n completion_status\n submitted_at\n evaluation_points\n evaluation_max_points\n evaluation_passed\n }\n learning_content {\n ...CoursePageFields\n circle {\n id\n title\n slug\n }\n }\n }\n }\n }\n }\n": types.CompetenceCertificateQueryDocument,
|
"\n query competenceCertificateQuery($courseSlug: String!, $courseSessionId: ID!) {\n competence_certificate_list(course_slug: $courseSlug) {\n ...CoursePageFields\n competence_certificates {\n ...CoursePageFields\n assignments {\n ...CoursePageFields\n assignment_type\n max_points\n completion(course_session_id: $courseSessionId) {\n id\n completion_status\n submitted_at\n evaluation_points\n evaluation_max_points\n evaluation_passed\n }\n learning_content {\n ...CoursePageFields\n circle {\n id\n title\n slug\n }\n }\n }\n }\n }\n }\n": types.CompetenceCertificateQueryDocument,
|
||||||
"\n query courseSessionDetail($courseSessionId: ID!) {\n course_session(id: $courseSessionId) {\n id\n title\n course {\n id\n title\n slug\n }\n users {\n id\n user_id\n first_name\n last_name\n email\n avatar_url\n role\n circles {\n id\n title\n slug\n }\n }\n attendance_courses {\n id\n location\n trainer\n due_date {\n id\n start\n end\n }\n learning_content_id\n learning_content {\n id\n title\n circle {\n id\n title\n slug\n }\n }\n }\n assignments {\n id\n submission_deadline {\n id\n start\n }\n evaluation_deadline {\n id\n start\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n edoniq_tests {\n id\n deadline {\n id\n start\n end\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n }\n }\n": types.CourseSessionDetailDocument,
|
"\n query courseSessionDetail($courseSessionId: ID!) {\n course_session(id: $courseSessionId) {\n id\n title\n course {\n id\n title\n slug\n }\n users {\n id\n user_id\n first_name\n last_name\n email\n avatar_url\n role\n circles {\n id\n title\n slug\n }\n }\n attendance_courses {\n id\n location\n trainer\n due_date {\n id\n start\n end\n }\n learning_content_id\n learning_content {\n id\n title\n circle {\n id\n title\n slug\n }\n }\n }\n assignments {\n id\n submission_deadline {\n id\n start\n }\n evaluation_deadline {\n id\n start\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n edoniq_tests {\n id\n deadline {\n id\n start\n end\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n }\n }\n": types.CourseSessionDetailDocument,
|
||||||
"\n query courseQuery($slug: String!) {\n course(slug: $slug) {\n id\n title\n slug\n category_name\n action_competences {\n competence_id\n ...CoursePageFields\n performance_criteria {\n competence_id\n learning_unit {\n id\n slug\n evaluate_url\n }\n ...CoursePageFields\n }\n }\n learning_path {\n ...CoursePageFields\n topics {\n is_visible\n ...CoursePageFields\n circles {\n description\n goals\n ...CoursePageFields\n learning_sequences {\n icon\n ...CoursePageFields\n learning_units {\n evaluate_url\n ...CoursePageFields\n performance_criteria {\n ...CoursePageFields\n }\n learning_contents {\n can_user_self_toggle_course_completion\n content_url\n minutes\n description\n ...CoursePageFields\n ... on LearningContentAssignmentObjectType {\n assignment_type\n content_assignment {\n id\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentEdoniqTestObjectType {\n checkbox_text\n has_extended_time_test\n content_assignment {\n id\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentRichTextObjectType {\n text\n }\n }\n }\n }\n }\n }\n }\n }\n }\n": types.CourseQueryDocument,
|
"\n query courseQuery($slug: String!) {\n course(slug: $slug) {\n id\n title\n slug\n category_name\n action_competences {\n competence_id\n ...CoursePageFields\n performance_criteria {\n competence_id\n learning_unit {\n id\n slug\n evaluate_url\n }\n ...CoursePageFields\n }\n }\n learning_path {\n ...CoursePageFields\n topics {\n is_visible\n ...CoursePageFields\n circles {\n description\n goals\n ...CoursePageFields\n learning_sequences {\n icon\n ...CoursePageFields\n learning_units {\n evaluate_url\n ...CoursePageFields\n performance_criteria {\n ...CoursePageFields\n }\n learning_contents {\n can_user_self_toggle_course_completion\n content_url\n minutes\n description\n ...CoursePageFields\n ... on LearningContentAssignmentObjectType {\n assignment_type\n content_assignment {\n id\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentEdoniqTestObjectType {\n checkbox_text\n has_extended_time_test\n content_assignment {\n id\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentRichTextObjectType {\n text\n }\n }\n }\n }\n }\n }\n }\n }\n }\n": types.CourseQueryDocument,
|
||||||
|
"\n query dashboardConfig {\n dashboard_config {\n id\n slug\n name\n dashboard_type\n }\n }\n": types.DashboardConfigDocument,
|
||||||
|
"\n query dashboardProgress($courseId: ID!) {\n course_progress(course_id: $courseId) {\n _id\n course_id\n session_to_continue_id\n competence {\n _id\n total_count\n success_count\n fail_count\n }\n assignment {\n _id\n total_count\n points_max_count\n points_achieved_count\n }\n }\n }\n": types.DashboardProgressDocument,
|
||||||
|
"\n query courseStatistics($courseId: ID!) {\n course_statistics(course_id: $courseId) {\n _id\n course_id\n course_title\n course_slug\n course_session_properties {\n _id\n sessions {\n id\n name\n }\n generations\n circles {\n id\n name\n }\n }\n course_session_selection_ids\n course_session_selection_metrics {\n _id\n session_count\n participant_count\n expert_count\n }\n attendance_day_presences {\n _id\n records {\n _id\n course_session_id\n generation\n circle_id\n due_date\n participants_present\n participants_total\n details_url\n }\n summary {\n _id\n days_completed\n participants_present\n }\n }\n feedback_responses {\n _id\n records {\n _id\n course_session_id\n generation\n circle_id\n experts\n satisfaction_average\n satisfaction_max\n details_url\n }\n summary {\n _id\n satisfaction_average\n satisfaction_max\n total_responses\n }\n }\n assignments {\n _id\n summary {\n _id\n completed_count\n average_passed\n }\n records {\n _id\n course_session_id\n course_session_assignment_id\n circle_id\n generation\n assignment_title\n assignment_type_translation_key\n details_url\n deadline\n metrics {\n _id\n passed_count\n failed_count\n unranked_count\n ranking_completed\n average_passed\n }\n }\n }\n competences {\n _id\n summary {\n _id\n success_total\n fail_total\n }\n records {\n _id\n course_session_id\n generation\n circle_id\n title\n success_count\n fail_count\n details_url\n }\n }\n }\n }\n": types.CourseStatisticsDocument,
|
||||||
"\n mutation SendFeedbackMutation(\n $courseSessionId: ID!\n $learningContentId: ID!\n $data: GenericScalar!\n $submitted: Boolean\n ) {\n send_feedback(\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n data: $data\n submitted: $submitted\n ) {\n feedback_response {\n id\n data\n submitted\n }\n errors {\n field\n messages\n }\n }\n }\n": types.SendFeedbackMutationDocument,
|
"\n mutation SendFeedbackMutation(\n $courseSessionId: ID!\n $learningContentId: ID!\n $data: GenericScalar!\n $submitted: Boolean\n ) {\n send_feedback(\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n data: $data\n submitted: $submitted\n ) {\n feedback_response {\n id\n data\n submitted\n }\n errors {\n field\n messages\n }\n }\n }\n": types.SendFeedbackMutationDocument,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -70,6 +73,18 @@ export function graphql(source: "\n query courseSessionDetail($courseSessionId:
|
||||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||||
*/
|
*/
|
||||||
export function graphql(source: "\n query courseQuery($slug: String!) {\n course(slug: $slug) {\n id\n title\n slug\n category_name\n action_competences {\n competence_id\n ...CoursePageFields\n performance_criteria {\n competence_id\n learning_unit {\n id\n slug\n evaluate_url\n }\n ...CoursePageFields\n }\n }\n learning_path {\n ...CoursePageFields\n topics {\n is_visible\n ...CoursePageFields\n circles {\n description\n goals\n ...CoursePageFields\n learning_sequences {\n icon\n ...CoursePageFields\n learning_units {\n evaluate_url\n ...CoursePageFields\n performance_criteria {\n ...CoursePageFields\n }\n learning_contents {\n can_user_self_toggle_course_completion\n content_url\n minutes\n description\n ...CoursePageFields\n ... on LearningContentAssignmentObjectType {\n assignment_type\n content_assignment {\n id\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentEdoniqTestObjectType {\n checkbox_text\n has_extended_time_test\n content_assignment {\n id\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentRichTextObjectType {\n text\n }\n }\n }\n }\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query courseQuery($slug: String!) {\n course(slug: $slug) {\n id\n title\n slug\n category_name\n action_competences {\n competence_id\n ...CoursePageFields\n performance_criteria {\n competence_id\n learning_unit {\n id\n slug\n evaluate_url\n }\n ...CoursePageFields\n }\n }\n learning_path {\n ...CoursePageFields\n topics {\n is_visible\n ...CoursePageFields\n circles {\n description\n goals\n ...CoursePageFields\n learning_sequences {\n icon\n ...CoursePageFields\n learning_units {\n evaluate_url\n ...CoursePageFields\n performance_criteria {\n ...CoursePageFields\n }\n learning_contents {\n can_user_self_toggle_course_completion\n content_url\n minutes\n description\n ...CoursePageFields\n ... on LearningContentAssignmentObjectType {\n assignment_type\n content_assignment {\n id\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentEdoniqTestObjectType {\n checkbox_text\n has_extended_time_test\n content_assignment {\n id\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentRichTextObjectType {\n text\n }\n }\n }\n }\n }\n }\n }\n }\n }\n"];
|
export function graphql(source: "\n query courseQuery($slug: String!) {\n course(slug: $slug) {\n id\n title\n slug\n category_name\n action_competences {\n competence_id\n ...CoursePageFields\n performance_criteria {\n competence_id\n learning_unit {\n id\n slug\n evaluate_url\n }\n ...CoursePageFields\n }\n }\n learning_path {\n ...CoursePageFields\n topics {\n is_visible\n ...CoursePageFields\n circles {\n description\n goals\n ...CoursePageFields\n learning_sequences {\n icon\n ...CoursePageFields\n learning_units {\n evaluate_url\n ...CoursePageFields\n performance_criteria {\n ...CoursePageFields\n }\n learning_contents {\n can_user_self_toggle_course_completion\n content_url\n minutes\n description\n ...CoursePageFields\n ... on LearningContentAssignmentObjectType {\n assignment_type\n content_assignment {\n id\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentEdoniqTestObjectType {\n checkbox_text\n has_extended_time_test\n content_assignment {\n id\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentRichTextObjectType {\n text\n }\n }\n }\n }\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query courseQuery($slug: String!) {\n course(slug: $slug) {\n id\n title\n slug\n category_name\n action_competences {\n competence_id\n ...CoursePageFields\n performance_criteria {\n competence_id\n learning_unit {\n id\n slug\n evaluate_url\n }\n ...CoursePageFields\n }\n }\n learning_path {\n ...CoursePageFields\n topics {\n is_visible\n ...CoursePageFields\n circles {\n description\n goals\n ...CoursePageFields\n learning_sequences {\n icon\n ...CoursePageFields\n learning_units {\n evaluate_url\n ...CoursePageFields\n performance_criteria {\n ...CoursePageFields\n }\n learning_contents {\n can_user_self_toggle_course_completion\n content_url\n minutes\n description\n ...CoursePageFields\n ... on LearningContentAssignmentObjectType {\n assignment_type\n content_assignment {\n id\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentEdoniqTestObjectType {\n checkbox_text\n has_extended_time_test\n content_assignment {\n id\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentRichTextObjectType {\n text\n }\n }\n }\n }\n }\n }\n }\n }\n }\n"];
|
||||||
|
/**
|
||||||
|
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||||
|
*/
|
||||||
|
export function graphql(source: "\n query dashboardConfig {\n dashboard_config {\n id\n slug\n name\n dashboard_type\n }\n }\n"): (typeof documents)["\n query dashboardConfig {\n dashboard_config {\n id\n slug\n name\n dashboard_type\n }\n }\n"];
|
||||||
|
/**
|
||||||
|
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||||
|
*/
|
||||||
|
export function graphql(source: "\n query dashboardProgress($courseId: ID!) {\n course_progress(course_id: $courseId) {\n _id\n course_id\n session_to_continue_id\n competence {\n _id\n total_count\n success_count\n fail_count\n }\n assignment {\n _id\n total_count\n points_max_count\n points_achieved_count\n }\n }\n }\n"): (typeof documents)["\n query dashboardProgress($courseId: ID!) {\n course_progress(course_id: $courseId) {\n _id\n course_id\n session_to_continue_id\n competence {\n _id\n total_count\n success_count\n fail_count\n }\n assignment {\n _id\n total_count\n points_max_count\n points_achieved_count\n }\n }\n }\n"];
|
||||||
|
/**
|
||||||
|
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||||
|
*/
|
||||||
|
export function graphql(source: "\n query courseStatistics($courseId: ID!) {\n course_statistics(course_id: $courseId) {\n _id\n course_id\n course_title\n course_slug\n course_session_properties {\n _id\n sessions {\n id\n name\n }\n generations\n circles {\n id\n name\n }\n }\n course_session_selection_ids\n course_session_selection_metrics {\n _id\n session_count\n participant_count\n expert_count\n }\n attendance_day_presences {\n _id\n records {\n _id\n course_session_id\n generation\n circle_id\n due_date\n participants_present\n participants_total\n details_url\n }\n summary {\n _id\n days_completed\n participants_present\n }\n }\n feedback_responses {\n _id\n records {\n _id\n course_session_id\n generation\n circle_id\n experts\n satisfaction_average\n satisfaction_max\n details_url\n }\n summary {\n _id\n satisfaction_average\n satisfaction_max\n total_responses\n }\n }\n assignments {\n _id\n summary {\n _id\n completed_count\n average_passed\n }\n records {\n _id\n course_session_id\n course_session_assignment_id\n circle_id\n generation\n assignment_title\n assignment_type_translation_key\n details_url\n deadline\n metrics {\n _id\n passed_count\n failed_count\n unranked_count\n ranking_completed\n average_passed\n }\n }\n }\n competences {\n _id\n summary {\n _id\n success_total\n fail_total\n }\n records {\n _id\n course_session_id\n generation\n circle_id\n title\n success_count\n fail_count\n details_url\n }\n }\n }\n }\n"): (typeof documents)["\n query courseStatistics($courseId: ID!) {\n course_statistics(course_id: $courseId) {\n _id\n course_id\n course_title\n course_slug\n course_session_properties {\n _id\n sessions {\n id\n name\n }\n generations\n circles {\n id\n name\n }\n }\n course_session_selection_ids\n course_session_selection_metrics {\n _id\n session_count\n participant_count\n expert_count\n }\n attendance_day_presences {\n _id\n records {\n _id\n course_session_id\n generation\n circle_id\n due_date\n participants_present\n participants_total\n details_url\n }\n summary {\n _id\n days_completed\n participants_present\n }\n }\n feedback_responses {\n _id\n records {\n _id\n course_session_id\n generation\n circle_id\n experts\n satisfaction_average\n satisfaction_max\n details_url\n }\n summary {\n _id\n satisfaction_average\n satisfaction_max\n total_responses\n }\n }\n assignments {\n _id\n summary {\n _id\n completed_count\n average_passed\n }\n records {\n _id\n course_session_id\n course_session_assignment_id\n circle_id\n generation\n assignment_title\n assignment_type_translation_key\n details_url\n deadline\n metrics {\n _id\n passed_count\n failed_count\n unranked_count\n ranking_completed\n average_passed\n }\n }\n }\n competences {\n _id\n summary {\n _id\n success_total\n fail_total\n }\n records {\n _id\n course_session_id\n generation\n circle_id\n title\n success_count\n fail_count\n details_url\n }\n }\n }\n }\n"];
|
||||||
/**
|
/**
|
||||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -1,4 +1,7 @@
|
||||||
type Query {
|
type Query {
|
||||||
|
course_statistics(course_id: ID!): CourseStatisticsType
|
||||||
|
course_progress(course_id: ID!): CourseProgressType
|
||||||
|
dashboard_config: [DashboardConfigType!]!
|
||||||
learning_path(id: ID, slug: String, course_id: ID, course_slug: String): LearningPathObjectType
|
learning_path(id: ID, slug: String, course_id: ID, course_slug: String): LearningPathObjectType
|
||||||
course_session_attendance_course(id: ID!, assignment_user_id: ID): CourseSessionAttendanceCourseObjectType
|
course_session_attendance_course(id: ID!, assignment_user_id: ID): CourseSessionAttendanceCourseObjectType
|
||||||
course(id: ID, slug: String): CourseObjectType
|
course(id: ID, slug: String): CourseObjectType
|
||||||
|
|
@ -19,6 +22,190 @@ type Query {
|
||||||
assignment_completion(assignment_id: ID!, course_session_id: ID!, learning_content_page_id: ID, assignment_user_id: UUID): AssignmentCompletionObjectType
|
assignment_completion(assignment_id: ID!, course_session_id: ID!, learning_content_page_id: ID, assignment_user_id: UUID): AssignmentCompletionObjectType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CourseStatisticsType {
|
||||||
|
_id: ID!
|
||||||
|
course_id: ID!
|
||||||
|
course_title: String!
|
||||||
|
course_slug: String!
|
||||||
|
course_session_properties: StatisticsCourseSessionPropertiesType!
|
||||||
|
course_session_selection_ids: [ID]!
|
||||||
|
course_session_selection_metrics: StatisticsCourseSessionsSelectionMetricType!
|
||||||
|
attendance_day_presences: AttendanceDayPresencesStatisticsType!
|
||||||
|
feedback_responses: FeedbackStatisticsResponsesType!
|
||||||
|
assignments: AssignmentsStatisticsType!
|
||||||
|
competences: CompetencesStatisticsType!
|
||||||
|
}
|
||||||
|
|
||||||
|
type StatisticsCourseSessionPropertiesType {
|
||||||
|
_id: ID!
|
||||||
|
sessions: [StatisticsCourseSessionDataType!]!
|
||||||
|
generations: [String!]!
|
||||||
|
circles: [StatisticsCircleDataType!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
type StatisticsCourseSessionDataType {
|
||||||
|
id: ID!
|
||||||
|
name: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type StatisticsCircleDataType {
|
||||||
|
id: ID!
|
||||||
|
name: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type StatisticsCourseSessionsSelectionMetricType {
|
||||||
|
_id: ID!
|
||||||
|
session_count: Int!
|
||||||
|
participant_count: Int!
|
||||||
|
expert_count: Int!
|
||||||
|
}
|
||||||
|
|
||||||
|
type AttendanceDayPresencesStatisticsType {
|
||||||
|
_id: ID!
|
||||||
|
records: [PresenceRecordStatisticsType!]!
|
||||||
|
summary: AttendanceSummaryStatisticsType!
|
||||||
|
}
|
||||||
|
|
||||||
|
type PresenceRecordStatisticsType {
|
||||||
|
_id: ID!
|
||||||
|
course_session_id: ID!
|
||||||
|
generation: String!
|
||||||
|
circle_id: ID!
|
||||||
|
due_date: DateTime!
|
||||||
|
participants_present: Int!
|
||||||
|
participants_total: Int!
|
||||||
|
details_url: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
The `DateTime` scalar type represents a DateTime
|
||||||
|
value as specified by
|
||||||
|
[iso8601](https://en.wikipedia.org/wiki/ISO_8601).
|
||||||
|
"""
|
||||||
|
scalar DateTime
|
||||||
|
|
||||||
|
type AttendanceSummaryStatisticsType {
|
||||||
|
_id: ID!
|
||||||
|
days_completed: Int!
|
||||||
|
participants_present: Int!
|
||||||
|
}
|
||||||
|
|
||||||
|
type FeedbackStatisticsResponsesType {
|
||||||
|
_id: ID!
|
||||||
|
records: [FeedbackStatisticsRecordType!]!
|
||||||
|
summary: FeedbackStatisticsSummaryType!
|
||||||
|
}
|
||||||
|
|
||||||
|
type FeedbackStatisticsRecordType {
|
||||||
|
_id: ID!
|
||||||
|
course_session_id: ID!
|
||||||
|
generation: String!
|
||||||
|
circle_id: ID!
|
||||||
|
satisfaction_average: Float!
|
||||||
|
satisfaction_max: Int!
|
||||||
|
details_url: String!
|
||||||
|
experts: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type FeedbackStatisticsSummaryType {
|
||||||
|
_id: ID!
|
||||||
|
satisfaction_average: Float!
|
||||||
|
satisfaction_max: Int!
|
||||||
|
total_responses: Int!
|
||||||
|
}
|
||||||
|
|
||||||
|
type AssignmentsStatisticsType {
|
||||||
|
_id: ID!
|
||||||
|
records: [AssignmentStatisticsRecordType!]!
|
||||||
|
summary: AssignmentStatisticsSummaryType!
|
||||||
|
}
|
||||||
|
|
||||||
|
type AssignmentStatisticsRecordType {
|
||||||
|
_id: ID!
|
||||||
|
course_session_id: ID!
|
||||||
|
course_session_assignment_id: ID!
|
||||||
|
circle_id: ID!
|
||||||
|
generation: String!
|
||||||
|
assignment_type_translation_key: String!
|
||||||
|
assignment_title: String!
|
||||||
|
deadline: DateTime!
|
||||||
|
metrics: AssignmentCompletionMetricsType!
|
||||||
|
details_url: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type AssignmentCompletionMetricsType {
|
||||||
|
_id: ID!
|
||||||
|
passed_count: Int!
|
||||||
|
failed_count: Int!
|
||||||
|
unranked_count: Int!
|
||||||
|
ranking_completed: Boolean!
|
||||||
|
average_passed: Float!
|
||||||
|
}
|
||||||
|
|
||||||
|
type AssignmentStatisticsSummaryType {
|
||||||
|
_id: ID!
|
||||||
|
completed_count: Int!
|
||||||
|
average_passed: Float!
|
||||||
|
}
|
||||||
|
|
||||||
|
type CompetencesStatisticsType {
|
||||||
|
_id: ID!
|
||||||
|
summary: CompetencePerformanceStatisticsSummaryType!
|
||||||
|
records: [CompetenceRecordStatisticsType!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
type CompetencePerformanceStatisticsSummaryType {
|
||||||
|
_id: ID!
|
||||||
|
success_total: Int!
|
||||||
|
fail_total: Int!
|
||||||
|
}
|
||||||
|
|
||||||
|
type CompetenceRecordStatisticsType {
|
||||||
|
_id: ID!
|
||||||
|
course_session_id: ID!
|
||||||
|
generation: String!
|
||||||
|
title: String!
|
||||||
|
circle_id: ID!
|
||||||
|
success_count: Int!
|
||||||
|
fail_count: Int!
|
||||||
|
details_url: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type CourseProgressType {
|
||||||
|
_id: ID!
|
||||||
|
course_id: ID!
|
||||||
|
session_to_continue_id: ID
|
||||||
|
competence: ProgressDashboardCompetenceType!
|
||||||
|
assignment: ProgressDashboardAssignmentType!
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProgressDashboardCompetenceType {
|
||||||
|
_id: ID!
|
||||||
|
total_count: Int!
|
||||||
|
success_count: Int!
|
||||||
|
fail_count: Int!
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProgressDashboardAssignmentType {
|
||||||
|
_id: ID!
|
||||||
|
total_count: Int!
|
||||||
|
points_max_count: Int!
|
||||||
|
points_achieved_count: Int!
|
||||||
|
}
|
||||||
|
|
||||||
|
type DashboardConfigType {
|
||||||
|
id: ID!
|
||||||
|
name: String!
|
||||||
|
slug: String!
|
||||||
|
dashboard_type: DashboardType!
|
||||||
|
}
|
||||||
|
|
||||||
|
enum DashboardType {
|
||||||
|
STATISTICS_DASHBOARD
|
||||||
|
PROGRESS_DASHBOARD
|
||||||
|
SIMPLE_DASHBOARD
|
||||||
|
}
|
||||||
|
|
||||||
type LearningPathObjectType implements CoursePageInterface {
|
type LearningPathObjectType implements CoursePageInterface {
|
||||||
id: ID!
|
id: ID!
|
||||||
title: String!
|
title: String!
|
||||||
|
|
@ -214,13 +401,6 @@ type DueDateObjectType {
|
||||||
course_session: CourseSessionObjectType!
|
course_session: CourseSessionObjectType!
|
||||||
}
|
}
|
||||||
|
|
||||||
"""
|
|
||||||
The `DateTime` scalar type represents a DateTime
|
|
||||||
value as specified by
|
|
||||||
[iso8601](https://en.wikipedia.org/wiki/ISO_8601).
|
|
||||||
"""
|
|
||||||
scalar DateTime
|
|
||||||
|
|
||||||
type CourseSessionObjectType {
|
type CourseSessionObjectType {
|
||||||
id: ID!
|
id: ID!
|
||||||
created_at: DateTime!
|
created_at: DateTime!
|
||||||
|
|
@ -466,29 +646,24 @@ type LearningContentEdoniqTestObjectType implements CoursePageInterface & Learni
|
||||||
has_extended_time_test: Boolean!
|
has_extended_time_test: Boolean!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
WORKAROUND:
|
||||||
|
Why is this no DjangoObjectType? It's because we have to "inject"
|
||||||
|
the supervisor into the list of users. This is done in the resolve_users
|
||||||
|
of the CourseSessionObjectType. And there we have to be able to construct
|
||||||
|
a CourseSessionUserObjectsType with the CIRCLES of the supervisor!
|
||||||
|
"""
|
||||||
type CourseSessionUserObjectsType {
|
type CourseSessionUserObjectsType {
|
||||||
id: UUID!
|
id: ID!
|
||||||
role: CourseCourseSessionUserRoleChoices!
|
|
||||||
user_id: UUID!
|
user_id: UUID!
|
||||||
first_name: String!
|
first_name: String!
|
||||||
last_name: String!
|
last_name: String!
|
||||||
email: String!
|
email: String!
|
||||||
avatar_url: String!
|
avatar_url: String!
|
||||||
|
role: String!
|
||||||
circles: [CourseSessionUserExpertCircleType!]!
|
circles: [CourseSessionUserExpertCircleType!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
"""An enumeration."""
|
|
||||||
enum CourseCourseSessionUserRoleChoices {
|
|
||||||
"""Teilnehmer"""
|
|
||||||
MEMBER
|
|
||||||
|
|
||||||
"""Experte/Trainer"""
|
|
||||||
EXPERT
|
|
||||||
|
|
||||||
"""Lernbegleitung"""
|
|
||||||
TUTOR
|
|
||||||
}
|
|
||||||
|
|
||||||
type CourseSessionUserExpertCircleType {
|
type CourseSessionUserExpertCircleType {
|
||||||
id: ID!
|
id: ID!
|
||||||
title: String!
|
title: String!
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,17 @@
|
||||||
export const ActionCompetenceObjectType = "ActionCompetenceObjectType";
|
export const ActionCompetenceObjectType = "ActionCompetenceObjectType";
|
||||||
export const AssignmentAssignmentAssignmentTypeChoices = "AssignmentAssignmentAssignmentTypeChoices";
|
export const AssignmentAssignmentAssignmentTypeChoices = "AssignmentAssignmentAssignmentTypeChoices";
|
||||||
export const AssignmentAssignmentCompletionCompletionStatusChoices = "AssignmentAssignmentCompletionCompletionStatusChoices";
|
export const AssignmentAssignmentCompletionCompletionStatusChoices = "AssignmentAssignmentCompletionCompletionStatusChoices";
|
||||||
|
export const AssignmentCompletionMetricsType = "AssignmentCompletionMetricsType";
|
||||||
export const AssignmentCompletionMutation = "AssignmentCompletionMutation";
|
export const AssignmentCompletionMutation = "AssignmentCompletionMutation";
|
||||||
export const AssignmentCompletionObjectType = "AssignmentCompletionObjectType";
|
export const AssignmentCompletionObjectType = "AssignmentCompletionObjectType";
|
||||||
export const AssignmentCompletionStatus = "AssignmentCompletionStatus";
|
export const AssignmentCompletionStatus = "AssignmentCompletionStatus";
|
||||||
export const AssignmentObjectType = "AssignmentObjectType";
|
export const AssignmentObjectType = "AssignmentObjectType";
|
||||||
|
export const AssignmentStatisticsRecordType = "AssignmentStatisticsRecordType";
|
||||||
|
export const AssignmentStatisticsSummaryType = "AssignmentStatisticsSummaryType";
|
||||||
|
export const AssignmentsStatisticsType = "AssignmentsStatisticsType";
|
||||||
export const AttendanceCourseUserMutation = "AttendanceCourseUserMutation";
|
export const AttendanceCourseUserMutation = "AttendanceCourseUserMutation";
|
||||||
|
export const AttendanceDayPresencesStatisticsType = "AttendanceDayPresencesStatisticsType";
|
||||||
|
export const AttendanceSummaryStatisticsType = "AttendanceSummaryStatisticsType";
|
||||||
export const AttendanceUserInputType = "AttendanceUserInputType";
|
export const AttendanceUserInputType = "AttendanceUserInputType";
|
||||||
export const AttendanceUserObjectType = "AttendanceUserObjectType";
|
export const AttendanceUserObjectType = "AttendanceUserObjectType";
|
||||||
export const AttendanceUserStatus = "AttendanceUserStatus";
|
export const AttendanceUserStatus = "AttendanceUserStatus";
|
||||||
|
|
@ -14,21 +20,30 @@ export const CircleLightObjectType = "CircleLightObjectType";
|
||||||
export const CircleObjectType = "CircleObjectType";
|
export const CircleObjectType = "CircleObjectType";
|
||||||
export const CompetenceCertificateListObjectType = "CompetenceCertificateListObjectType";
|
export const CompetenceCertificateListObjectType = "CompetenceCertificateListObjectType";
|
||||||
export const CompetenceCertificateObjectType = "CompetenceCertificateObjectType";
|
export const CompetenceCertificateObjectType = "CompetenceCertificateObjectType";
|
||||||
|
export const CompetencePerformanceStatisticsSummaryType = "CompetencePerformanceStatisticsSummaryType";
|
||||||
|
export const CompetenceRecordStatisticsType = "CompetenceRecordStatisticsType";
|
||||||
|
export const CompetencesStatisticsType = "CompetencesStatisticsType";
|
||||||
export const CoreUserLanguageChoices = "CoreUserLanguageChoices";
|
export const CoreUserLanguageChoices = "CoreUserLanguageChoices";
|
||||||
export const CourseCourseSessionUserRoleChoices = "CourseCourseSessionUserRoleChoices";
|
|
||||||
export const CourseObjectType = "CourseObjectType";
|
export const CourseObjectType = "CourseObjectType";
|
||||||
export const CoursePageInterface = "CoursePageInterface";
|
export const CoursePageInterface = "CoursePageInterface";
|
||||||
|
export const CourseProgressType = "CourseProgressType";
|
||||||
export const CourseSessionAssignmentObjectType = "CourseSessionAssignmentObjectType";
|
export const CourseSessionAssignmentObjectType = "CourseSessionAssignmentObjectType";
|
||||||
export const CourseSessionAttendanceCourseObjectType = "CourseSessionAttendanceCourseObjectType";
|
export const CourseSessionAttendanceCourseObjectType = "CourseSessionAttendanceCourseObjectType";
|
||||||
export const CourseSessionEdoniqTestObjectType = "CourseSessionEdoniqTestObjectType";
|
export const CourseSessionEdoniqTestObjectType = "CourseSessionEdoniqTestObjectType";
|
||||||
export const CourseSessionObjectType = "CourseSessionObjectType";
|
export const CourseSessionObjectType = "CourseSessionObjectType";
|
||||||
export const CourseSessionUserExpertCircleType = "CourseSessionUserExpertCircleType";
|
export const CourseSessionUserExpertCircleType = "CourseSessionUserExpertCircleType";
|
||||||
export const CourseSessionUserObjectsType = "CourseSessionUserObjectsType";
|
export const CourseSessionUserObjectsType = "CourseSessionUserObjectsType";
|
||||||
|
export const CourseStatisticsType = "CourseStatisticsType";
|
||||||
|
export const DashboardConfigType = "DashboardConfigType";
|
||||||
|
export const DashboardType = "DashboardType";
|
||||||
export const Date = "Date";
|
export const Date = "Date";
|
||||||
export const DateTime = "DateTime";
|
export const DateTime = "DateTime";
|
||||||
export const DueDateObjectType = "DueDateObjectType";
|
export const DueDateObjectType = "DueDateObjectType";
|
||||||
export const ErrorType = "ErrorType";
|
export const ErrorType = "ErrorType";
|
||||||
export const FeedbackResponseObjectType = "FeedbackResponseObjectType";
|
export const FeedbackResponseObjectType = "FeedbackResponseObjectType";
|
||||||
|
export const FeedbackStatisticsRecordType = "FeedbackStatisticsRecordType";
|
||||||
|
export const FeedbackStatisticsResponsesType = "FeedbackStatisticsResponsesType";
|
||||||
|
export const FeedbackStatisticsSummaryType = "FeedbackStatisticsSummaryType";
|
||||||
export const Float = "Float";
|
export const Float = "Float";
|
||||||
export const GenericScalar = "GenericScalar";
|
export const GenericScalar = "GenericScalar";
|
||||||
export const ID = "ID";
|
export const ID = "ID";
|
||||||
|
|
@ -52,8 +67,15 @@ export const LearningUnitObjectType = "LearningUnitObjectType";
|
||||||
export const LearnpathLearningContentAssignmentAssignmentTypeChoices = "LearnpathLearningContentAssignmentAssignmentTypeChoices";
|
export const LearnpathLearningContentAssignmentAssignmentTypeChoices = "LearnpathLearningContentAssignmentAssignmentTypeChoices";
|
||||||
export const Mutation = "Mutation";
|
export const Mutation = "Mutation";
|
||||||
export const PerformanceCriteriaObjectType = "PerformanceCriteriaObjectType";
|
export const PerformanceCriteriaObjectType = "PerformanceCriteriaObjectType";
|
||||||
|
export const PresenceRecordStatisticsType = "PresenceRecordStatisticsType";
|
||||||
|
export const ProgressDashboardAssignmentType = "ProgressDashboardAssignmentType";
|
||||||
|
export const ProgressDashboardCompetenceType = "ProgressDashboardCompetenceType";
|
||||||
export const Query = "Query";
|
export const Query = "Query";
|
||||||
export const SendFeedbackMutation = "SendFeedbackMutation";
|
export const SendFeedbackMutation = "SendFeedbackMutation";
|
||||||
|
export const StatisticsCircleDataType = "StatisticsCircleDataType";
|
||||||
|
export const StatisticsCourseSessionDataType = "StatisticsCourseSessionDataType";
|
||||||
|
export const StatisticsCourseSessionPropertiesType = "StatisticsCourseSessionPropertiesType";
|
||||||
|
export const StatisticsCourseSessionsSelectionMetricType = "StatisticsCourseSessionsSelectionMetricType";
|
||||||
export const String = "String";
|
export const String = "String";
|
||||||
export const TopicObjectType = "TopicObjectType";
|
export const TopicObjectType = "TopicObjectType";
|
||||||
export const UUID = "UUID";
|
export const UUID = "UUID";
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
import { cacheExchange } from "@urql/exchange-graphcache";
|
import { cacheExchange } from "@urql/exchange-graphcache";
|
||||||
import { Client, fetchExchange } from "@urql/vue";
|
import { Client, fetchExchange } from "@urql/vue";
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
import schema from "../gql/dist/minifiedSchema.json";
|
import schema from "../gql/dist/minifiedSchema.json";
|
||||||
import {
|
import {
|
||||||
AssignmentCompletionMutation,
|
AssignmentCompletionMutation,
|
||||||
|
|
|
||||||
|
|
@ -268,3 +268,148 @@ export const COURSE_QUERY = graphql(`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
export const DASHBOARD_CONFIG = graphql(`
|
||||||
|
query dashboardConfig {
|
||||||
|
dashboard_config {
|
||||||
|
id
|
||||||
|
slug
|
||||||
|
name
|
||||||
|
dashboard_type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
export const DASHBOARD_COURSE_SESSION_PROGRESS = graphql(`
|
||||||
|
query dashboardProgress($courseId: ID!) {
|
||||||
|
course_progress(course_id: $courseId) {
|
||||||
|
_id
|
||||||
|
course_id
|
||||||
|
session_to_continue_id
|
||||||
|
competence {
|
||||||
|
_id
|
||||||
|
total_count
|
||||||
|
success_count
|
||||||
|
fail_count
|
||||||
|
}
|
||||||
|
assignment {
|
||||||
|
_id
|
||||||
|
total_count
|
||||||
|
points_max_count
|
||||||
|
points_achieved_count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
export const DASHBOARD_COURSE_STATISTICS = graphql(`
|
||||||
|
query courseStatistics($courseId: ID!) {
|
||||||
|
course_statistics(course_id: $courseId) {
|
||||||
|
_id
|
||||||
|
course_id
|
||||||
|
course_title
|
||||||
|
course_slug
|
||||||
|
course_session_properties {
|
||||||
|
_id
|
||||||
|
sessions {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
generations
|
||||||
|
circles {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
course_session_selection_ids
|
||||||
|
course_session_selection_metrics {
|
||||||
|
_id
|
||||||
|
session_count
|
||||||
|
participant_count
|
||||||
|
expert_count
|
||||||
|
}
|
||||||
|
attendance_day_presences {
|
||||||
|
_id
|
||||||
|
records {
|
||||||
|
_id
|
||||||
|
course_session_id
|
||||||
|
generation
|
||||||
|
circle_id
|
||||||
|
due_date
|
||||||
|
participants_present
|
||||||
|
participants_total
|
||||||
|
details_url
|
||||||
|
}
|
||||||
|
summary {
|
||||||
|
_id
|
||||||
|
days_completed
|
||||||
|
participants_present
|
||||||
|
}
|
||||||
|
}
|
||||||
|
feedback_responses {
|
||||||
|
_id
|
||||||
|
records {
|
||||||
|
_id
|
||||||
|
course_session_id
|
||||||
|
generation
|
||||||
|
circle_id
|
||||||
|
experts
|
||||||
|
satisfaction_average
|
||||||
|
satisfaction_max
|
||||||
|
details_url
|
||||||
|
}
|
||||||
|
summary {
|
||||||
|
_id
|
||||||
|
satisfaction_average
|
||||||
|
satisfaction_max
|
||||||
|
total_responses
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assignments {
|
||||||
|
_id
|
||||||
|
summary {
|
||||||
|
_id
|
||||||
|
completed_count
|
||||||
|
average_passed
|
||||||
|
}
|
||||||
|
records {
|
||||||
|
_id
|
||||||
|
course_session_id
|
||||||
|
course_session_assignment_id
|
||||||
|
circle_id
|
||||||
|
generation
|
||||||
|
assignment_title
|
||||||
|
assignment_type_translation_key
|
||||||
|
details_url
|
||||||
|
deadline
|
||||||
|
metrics {
|
||||||
|
_id
|
||||||
|
passed_count
|
||||||
|
failed_count
|
||||||
|
unranked_count
|
||||||
|
ranking_completed
|
||||||
|
average_passed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
competences {
|
||||||
|
_id
|
||||||
|
summary {
|
||||||
|
_id
|
||||||
|
success_total
|
||||||
|
fail_total
|
||||||
|
}
|
||||||
|
records {
|
||||||
|
_id
|
||||||
|
course_session_id
|
||||||
|
generation
|
||||||
|
circle_id
|
||||||
|
title
|
||||||
|
success_count
|
||||||
|
fail_count
|
||||||
|
details_url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { getCockpitUrl } from "@/utils/utils";
|
||||||
|
import { useDashboardStore } from "@/stores/dashboard";
|
||||||
|
|
||||||
|
const dashboardStore = useDashboardStore();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="dashboardStore.currentDashboardConfig">
|
||||||
|
<div class="mb-14 flex flex-col space-y-8">
|
||||||
|
<div
|
||||||
|
v-for="config in dashboardStore.dashboardConfigs"
|
||||||
|
:key="config.id"
|
||||||
|
class="h-full bg-white p-6"
|
||||||
|
>
|
||||||
|
<h3 class="mb-4">{{ config.name }}</h3>
|
||||||
|
<div>
|
||||||
|
<router-link
|
||||||
|
class="btn-blue"
|
||||||
|
target="_blank"
|
||||||
|
:to="getCockpitUrl(config.slug)"
|
||||||
|
:data-cy="`continue-course-${config.id}`"
|
||||||
|
>
|
||||||
|
{{ $t("a.Cockpit anschauen") }}
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Component } from "vue";
|
||||||
|
import { onMounted } from "vue";
|
||||||
|
import StatisticPage from "@/pages/dashboard/StatisticPage.vue";
|
||||||
|
import ProgressPage from "@/pages/dashboard/ProgressPage.vue";
|
||||||
|
import SimpleDates from "@/components/dashboard/SimpleDates.vue";
|
||||||
|
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
|
||||||
|
import { useDashboardStore } from "@/stores/dashboard";
|
||||||
|
import type { DashboardType } from "@/gql/graphql";
|
||||||
|
import SimpleCoursePage from "@/pages/dashboard/SimpleCoursePage.vue";
|
||||||
|
import LoadingSpinner from "@/components/ui/LoadingSpinner.vue";
|
||||||
|
import CourseDetailDates from "@/components/dashboard/CourseDetailDates.vue";
|
||||||
|
|
||||||
|
const dashboardStore = useDashboardStore();
|
||||||
|
|
||||||
|
interface DashboardPage {
|
||||||
|
main: Component;
|
||||||
|
aside: Component;
|
||||||
|
}
|
||||||
|
|
||||||
|
const boards: Record<DashboardType, DashboardPage> = {
|
||||||
|
PROGRESS_DASHBOARD: { main: ProgressPage, aside: SimpleDates },
|
||||||
|
SIMPLE_DASHBOARD: { main: SimpleCoursePage, aside: SimpleDates },
|
||||||
|
STATISTICS_DASHBOARD: { main: StatisticPage, aside: CourseDetailDates },
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(dashboardStore.loadDashboardDetails);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="dashboardStore.loading" class="m-8 flex justify-center">
|
||||||
|
<LoadingSpinner />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="dashboardStore.currentDashboardConfig"
|
||||||
|
class="flex flex-col lg:flex-row"
|
||||||
|
>
|
||||||
|
<main class="grow bg-gray-200 lg:order-2">
|
||||||
|
<div class="m-8">
|
||||||
|
<div class="mb-10 flex items-center justify-between">
|
||||||
|
<h1 data-cy="dashboard-title">Dashboard</h1>
|
||||||
|
<ItDropdownSelect
|
||||||
|
:model-value="dashboardStore.currentDashboardConfig"
|
||||||
|
class="mt-4 w-full lg:mt-0 lg:w-96"
|
||||||
|
:items="dashboardStore.dashboardConfigs"
|
||||||
|
@update:model-value="dashboardStore.switchAndLoadDashboardConfig"
|
||||||
|
></ItDropdownSelect>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<component
|
||||||
|
:is="boards[dashboardStore.currentDashboardConfig.dashboard_type].main"
|
||||||
|
></component>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<aside class="m-8 lg:order-1 lg:w-[343px]">
|
||||||
|
<component
|
||||||
|
:is="boards[dashboardStore.currentDashboardConfig.dashboard_type].aside"
|
||||||
|
></component>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useDashboardStore } from "@/stores/dashboard";
|
||||||
|
import { computed } from "vue";
|
||||||
|
import LearningPathDiagram from "@/components/learningPath/LearningPathDiagram.vue";
|
||||||
|
import { getLearningPathUrl } from "@/utils/utils";
|
||||||
|
import type {
|
||||||
|
CourseProgressType,
|
||||||
|
ProgressDashboardAssignmentType,
|
||||||
|
ProgressDashboardCompetenceType,
|
||||||
|
} from "@/gql/graphql";
|
||||||
|
import CompetenceSummaryBox from "@/components/dashboard/CompetenceSummaryBox.vue";
|
||||||
|
import AssignmentProgressSummaryBox from "@/components/dashboard/AssignmentProgressSummaryBox.vue";
|
||||||
|
|
||||||
|
const dashboardStore = useDashboardStore();
|
||||||
|
|
||||||
|
const courseSlug = computed(() => dashboardStore.currentDashboardConfig?.slug);
|
||||||
|
const courseName = computed(() => dashboardStore.currentDashboardConfig?.name);
|
||||||
|
|
||||||
|
const courseSessionProgress = computed(() => {
|
||||||
|
return dashboardStore.currentDashBoardData as CourseProgressType;
|
||||||
|
});
|
||||||
|
|
||||||
|
const DEFAULT_ASSIGNMENT = {
|
||||||
|
points_achieved_count: 0,
|
||||||
|
points_max_count: 0,
|
||||||
|
total_count: 0,
|
||||||
|
};
|
||||||
|
const DEFAULT_COMPETENCE = { total_count: 0, success_count: 0, fail_count: 0 };
|
||||||
|
|
||||||
|
const assignment = computed<ProgressDashboardAssignmentType>(
|
||||||
|
() =>
|
||||||
|
(courseSessionProgress.value?.assignment as ProgressDashboardAssignmentType) ??
|
||||||
|
DEFAULT_ASSIGNMENT
|
||||||
|
);
|
||||||
|
|
||||||
|
const competence = computed<ProgressDashboardCompetenceType>(
|
||||||
|
() =>
|
||||||
|
(courseSessionProgress.value?.competence as ProgressDashboardCompetenceType) ??
|
||||||
|
DEFAULT_COMPETENCE
|
||||||
|
);
|
||||||
|
|
||||||
|
const competenceCertificateUrl = computed(() => {
|
||||||
|
return `/course/${courseSlug.value}/competence/certificates?courseSessionId=${courseSessionProgress.value?.session_to_continue_id}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const competenceCriteriaUrl = computed(() => {
|
||||||
|
return `/course/${courseSlug.value}/competence/criteria?courseSessionId=${courseSessionProgress.value?.session_to_continue_id}`;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="courseSessionProgress && dashboardStore.currentDashboardConfig"
|
||||||
|
class="mb-14 space-y-8"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col space-y-7 bg-white p-6">
|
||||||
|
<h3>{{ courseName }}</h3>
|
||||||
|
<LearningPathDiagram
|
||||||
|
v-if="courseSessionProgress.session_to_continue_id && courseSlug"
|
||||||
|
:key="courseSlug"
|
||||||
|
:course-slug="courseSlug"
|
||||||
|
:course-session-id="courseSessionProgress.session_to_continue_id"
|
||||||
|
diagram-type="horizontal"
|
||||||
|
></LearningPathDiagram>
|
||||||
|
<div>
|
||||||
|
<router-link
|
||||||
|
class="btn-blue"
|
||||||
|
:to="getLearningPathUrl(dashboardStore.currentDashboardConfig.slug)"
|
||||||
|
:data-cy="`continue-course-${dashboardStore.currentDashboardConfig.id}`"
|
||||||
|
>
|
||||||
|
{{ $t("general.nextStep") }}
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid auto-rows-fr grid-cols-1 gap-8 xl:grid-cols-2">
|
||||||
|
<AssignmentProgressSummaryBox
|
||||||
|
:total-assignments="assignment.total_count"
|
||||||
|
:achieved-points-count="assignment.points_achieved_count"
|
||||||
|
:max-points-count="assignment.points_max_count"
|
||||||
|
:details-link="competenceCertificateUrl"
|
||||||
|
/>
|
||||||
|
<CompetenceSummaryBox
|
||||||
|
:fail-count="competence.fail_count"
|
||||||
|
:success-count="competence.success_count"
|
||||||
|
:details-link="competenceCriteriaUrl"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { getCockpitUrl } from "@/utils/utils";
|
||||||
|
import { useDashboardStore } from "@/stores/dashboard";
|
||||||
|
|
||||||
|
const dashboardStore = useDashboardStore();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="dashboardStore.currentDashboardConfig">
|
||||||
|
<div class="mb-14">
|
||||||
|
<div class="h-full bg-white p-6">
|
||||||
|
<h3 class="mb-4">{{ dashboardStore.currentDashboardConfig.name }}</h3>
|
||||||
|
<div>
|
||||||
|
<router-link
|
||||||
|
class="btn-blue"
|
||||||
|
:to="getCockpitUrl(dashboardStore.currentDashboardConfig.slug)"
|
||||||
|
:data-cy="`continue-course-${dashboardStore.currentDashboardConfig.id}`"
|
||||||
|
>
|
||||||
|
{{ $t("a.Cockpit anschauen") }}
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useDashboardStore } from "@/stores/dashboard";
|
||||||
|
import { computed } from "vue";
|
||||||
|
import CourseStatistics from "@/components/dashboard/CourseStatistics.vue";
|
||||||
|
import AttendanceSummaryBox from "@/components/dashboard/AttendanceSummaryBox.vue";
|
||||||
|
import type { CourseStatisticsType } from "@/gql/graphql";
|
||||||
|
import AssignmentSummaryBox from "@/components/dashboard/AssignmentSummaryBox.vue";
|
||||||
|
import FeedbackSummaryBox from "@/components/dashboard/FeedbackSummaryBox.vue";
|
||||||
|
import CompetenceSummaryBox from "@/components/dashboard/CompetenceSummaryBox.vue";
|
||||||
|
|
||||||
|
const dashboardStore = useDashboardStore();
|
||||||
|
|
||||||
|
const statistics = computed(() => {
|
||||||
|
return dashboardStore.currentDashBoardData as CourseStatisticsType;
|
||||||
|
});
|
||||||
|
|
||||||
|
const courseSessionSelectionMetrics = computed(() => {
|
||||||
|
return statistics.value.course_session_selection_metrics;
|
||||||
|
});
|
||||||
|
|
||||||
|
const attendanceDayPresences = computed(() => {
|
||||||
|
return statistics.value.attendance_day_presences.summary;
|
||||||
|
});
|
||||||
|
|
||||||
|
const assigmentSummary = computed(() => {
|
||||||
|
return statistics.value.assignments.summary;
|
||||||
|
});
|
||||||
|
|
||||||
|
const competenceSummary = computed(() => {
|
||||||
|
return statistics.value.competences.summary;
|
||||||
|
});
|
||||||
|
|
||||||
|
const feebackSummary = computed(() => {
|
||||||
|
return statistics.value.feedback_responses.summary;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="statistics" class="mb-14 space-y-8">
|
||||||
|
<CourseStatistics
|
||||||
|
:session-count="courseSessionSelectionMetrics.session_count"
|
||||||
|
:participant-count="courseSessionSelectionMetrics.participant_count"
|
||||||
|
:expert-count="courseSessionSelectionMetrics.expert_count"
|
||||||
|
/>
|
||||||
|
<div class="grid auto-rows-fr grid-cols-1 gap-8 xl:grid-cols-2">
|
||||||
|
<AttendanceSummaryBox
|
||||||
|
:days-completed="attendanceDayPresences.days_completed"
|
||||||
|
:avg-participants-present="attendanceDayPresences.participants_present"
|
||||||
|
/>
|
||||||
|
<AssignmentSummaryBox
|
||||||
|
:assignments-completed="assigmentSummary.completed_count"
|
||||||
|
:avg-passed="assigmentSummary.average_passed"
|
||||||
|
/>
|
||||||
|
<FeedbackSummaryBox
|
||||||
|
:feedback-count="feebackSummary.total_responses"
|
||||||
|
:statisfaction-max="feebackSummary.satisfaction_max"
|
||||||
|
:statisfaction-avg="feebackSummary.satisfaction_average"
|
||||||
|
/>
|
||||||
|
<CompetenceSummaryBox
|
||||||
|
:fail-count="competenceSummary.fail_total"
|
||||||
|
:success-count="competenceSummary.success_total"
|
||||||
|
details-link="/statistic/competence"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,108 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useDashboardStore } from "@/stores/dashboard";
|
||||||
|
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
|
||||||
|
import { computed } from "vue";
|
||||||
|
import type {
|
||||||
|
AssignmentCompletionMetricsType,
|
||||||
|
AssignmentStatisticsRecordType,
|
||||||
|
CourseStatisticsType,
|
||||||
|
} from "@/gql/graphql";
|
||||||
|
import StatisticFilterList from "@/components/dashboard/StatisticFilterList.vue";
|
||||||
|
import { useCourseStatistics } from "@/composables";
|
||||||
|
import ItProgress from "@/components/ui/ItProgress.vue";
|
||||||
|
import { getDateString } from "@/components/dueDates/dueDatesUtils";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
|
const dashboardStore = useDashboardStore();
|
||||||
|
|
||||||
|
const statistics = computed(() => {
|
||||||
|
return dashboardStore.currentDashBoardData as CourseStatisticsType;
|
||||||
|
});
|
||||||
|
|
||||||
|
const { courseSessionName, circleMeta } = useCourseStatistics();
|
||||||
|
|
||||||
|
const assignmentStats = (metrics: AssignmentCompletionMetricsType) => {
|
||||||
|
if (!metrics.ranking_completed) {
|
||||||
|
return {
|
||||||
|
SUCCESS: 0,
|
||||||
|
FAIL: 0,
|
||||||
|
UNKNOWN: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
SUCCESS: metrics.passed_count,
|
||||||
|
FAIL: metrics.failed_count,
|
||||||
|
UNKNOWN: metrics.unranked_count,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const total = (metrics: AssignmentCompletionMetricsType) => {
|
||||||
|
return metrics.passed_count + metrics.failed_count;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<main v-if="statistics">
|
||||||
|
<div class="mb-10 flex items-center justify-between">
|
||||||
|
<h3>{{ $t("a.Arbeiten") }}</h3>
|
||||||
|
<ItDropdownSelect
|
||||||
|
:model-value="dashboardStore.currentDashboardConfig"
|
||||||
|
class="mt-4 w-full lg:mt-0 lg:w-96"
|
||||||
|
:items="dashboardStore.dashboardConfigs"
|
||||||
|
@update:model-value="dashboardStore.switchAndLoadDashboardConfig"
|
||||||
|
></ItDropdownSelect>
|
||||||
|
</div>
|
||||||
|
<div v-if="statistics.assignments.records" class="mt-8 bg-white">
|
||||||
|
<StatisticFilterList
|
||||||
|
:course-session-properties="statistics.course_session_properties"
|
||||||
|
:items="statistics.assignments.records"
|
||||||
|
>
|
||||||
|
<template #default="{ item }">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<div>
|
||||||
|
<h4 class="font-bold">
|
||||||
|
{{ (item as AssignmentStatisticsRecordType).assignment_title }}
|
||||||
|
</h4>
|
||||||
|
<div>
|
||||||
|
{{ $t("a.Durchfuehrung") }} «{{
|
||||||
|
courseSessionName(item.course_session_id)
|
||||||
|
}}» - Circle «{{ circleMeta(item.circle_id)?.name }}»
|
||||||
|
</div>
|
||||||
|
<div class="mt-4">
|
||||||
|
{{ $t("a.Abgabetermin") }}:
|
||||||
|
{{
|
||||||
|
getDateString(
|
||||||
|
dayjs((item as AssignmentStatisticsRecordType).deadline)
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
v-if="(item as AssignmentStatisticsRecordType).metrics.ranking_completed"
|
||||||
|
>
|
||||||
|
{{ (item as AssignmentStatisticsRecordType).metrics.passed_count }} von
|
||||||
|
{{ total((item as AssignmentStatisticsRecordType).metrics) }}
|
||||||
|
bestanden
|
||||||
|
</div>
|
||||||
|
<div v-else>Noch nicht bestätigt</div>
|
||||||
|
<ItProgress
|
||||||
|
:status-count="
|
||||||
|
assignmentStats((item as AssignmentStatisticsRecordType).metrics)
|
||||||
|
"
|
||||||
|
></ItProgress>
|
||||||
|
<router-link
|
||||||
|
class="underline"
|
||||||
|
target="_blank"
|
||||||
|
:to="(item as AssignmentStatisticsRecordType).details_url"
|
||||||
|
>
|
||||||
|
{{ $t("a.Details anschauen") }}
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</StatisticFilterList>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useDashboardStore } from "@/stores/dashboard";
|
||||||
|
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
|
||||||
|
import { computed } from "vue";
|
||||||
|
import type { CourseStatisticsType, PresenceRecordStatisticsType } from "@/gql/graphql";
|
||||||
|
import StatisticFilterList from "@/components/dashboard/StatisticFilterList.vue";
|
||||||
|
import ItProgress from "@/components/ui/ItProgress.vue";
|
||||||
|
import { useCourseStatistics } from "@/composables";
|
||||||
|
import { getDateString } from "@/components/dueDates/dueDatesUtils";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
|
const dashboardStore = useDashboardStore();
|
||||||
|
|
||||||
|
const statistics = computed(() => {
|
||||||
|
return dashboardStore.currentDashBoardData as CourseStatisticsType;
|
||||||
|
});
|
||||||
|
|
||||||
|
const { courseSessionName, circleMeta } = useCourseStatistics();
|
||||||
|
|
||||||
|
const attendanceStats = (present: number, total: number) => {
|
||||||
|
return {
|
||||||
|
SUCCESS: present,
|
||||||
|
FAIL: total - present,
|
||||||
|
UNKNOWN: 0,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<main v-if="statistics">
|
||||||
|
<div class="mb-10 flex items-center justify-between">
|
||||||
|
<h3>{{ $t("Anwesenheit") }}</h3>
|
||||||
|
<ItDropdownSelect
|
||||||
|
:model-value="dashboardStore.currentDashboardConfig"
|
||||||
|
class="mt-4 w-full lg:mt-0 lg:w-96"
|
||||||
|
:items="dashboardStore.dashboardConfigs"
|
||||||
|
@update:model-value="dashboardStore.switchAndLoadDashboardConfig"
|
||||||
|
></ItDropdownSelect>
|
||||||
|
</div>
|
||||||
|
<div v-if="statistics.attendance_day_presences.records" class="mt-8 bg-white">
|
||||||
|
<StatisticFilterList
|
||||||
|
:course-session-properties="statistics.course_session_properties"
|
||||||
|
:items="statistics.attendance_day_presences.records"
|
||||||
|
>
|
||||||
|
<template #default="{ item }">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<div>
|
||||||
|
<h4 class="font-bold">
|
||||||
|
{{ $t("a.Präsenztag") }}: Circle «{{
|
||||||
|
circleMeta(item.circle_id)?.name
|
||||||
|
}}»
|
||||||
|
</h4>
|
||||||
|
<div>
|
||||||
|
{{ $t("a.Durchfuehrung") }} «{{
|
||||||
|
courseSessionName(item.course_session_id)
|
||||||
|
}}»
|
||||||
|
</div>
|
||||||
|
<div class="mt-4">
|
||||||
|
{{ $t("a.Termin") }}:
|
||||||
|
{{
|
||||||
|
getDateString(dayjs((item as PresenceRecordStatisticsType).due_date))
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
{{
|
||||||
|
$t("a.present von total Teilnehmenden anwesend", {
|
||||||
|
present: (item as PresenceRecordStatisticsType)
|
||||||
|
.participants_present,
|
||||||
|
total: (item as PresenceRecordStatisticsType).participants_total,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
<ItProgress
|
||||||
|
:status-count="
|
||||||
|
attendanceStats((item as PresenceRecordStatisticsType).participants_present, (item as PresenceRecordStatisticsType).participants_total)
|
||||||
|
"
|
||||||
|
></ItProgress>
|
||||||
|
<router-link
|
||||||
|
class="underline"
|
||||||
|
target="_blank"
|
||||||
|
:to="(item as PresenceRecordStatisticsType).details_url"
|
||||||
|
>
|
||||||
|
{{ $t("a.Details anschauen") }}
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</StatisticFilterList>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useDashboardStore } from "@/stores/dashboard";
|
||||||
|
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
|
||||||
|
import { computed } from "vue";
|
||||||
|
import type {
|
||||||
|
CompetenceRecordStatisticsType,
|
||||||
|
CourseStatisticsType,
|
||||||
|
} from "@/gql/graphql";
|
||||||
|
import StatisticFilterList from "@/components/dashboard/StatisticFilterList.vue";
|
||||||
|
import { useCourseStatistics } from "@/composables";
|
||||||
|
|
||||||
|
const dashboardStore = useDashboardStore();
|
||||||
|
|
||||||
|
const statistics = computed(() => {
|
||||||
|
return dashboardStore.currentDashBoardData as CourseStatisticsType;
|
||||||
|
});
|
||||||
|
|
||||||
|
const { courseSessionName, circleMeta } = useCourseStatistics();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<main v-if="statistics">
|
||||||
|
<div class="mb-10 flex items-center justify-between">
|
||||||
|
<h3>{{ $t("a.Selbsteinschätzung") }}</h3>
|
||||||
|
<ItDropdownSelect
|
||||||
|
:model-value="dashboardStore.currentDashboardConfig"
|
||||||
|
class="mt-4 w-full lg:mt-0 lg:w-96"
|
||||||
|
:items="dashboardStore.dashboardConfigs"
|
||||||
|
@update:model-value="dashboardStore.switchAndLoadDashboardConfig"
|
||||||
|
></ItDropdownSelect>
|
||||||
|
</div>
|
||||||
|
<div v-if="statistics.competences.records" class="mt-8 bg-white">
|
||||||
|
<StatisticFilterList
|
||||||
|
:course-session-properties="statistics.course_session_properties"
|
||||||
|
:items="statistics.competences.records"
|
||||||
|
>
|
||||||
|
<template #default="{ item }">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<div>
|
||||||
|
<h4 class="font-bold">
|
||||||
|
{{ $t("a.Selbsteinschätzung") }}:
|
||||||
|
{{ (item as CompetenceRecordStatisticsType).title }}
|
||||||
|
</h4>
|
||||||
|
<div>
|
||||||
|
Durchführung «{{ courseSessionName(item.course_session_id) }}» - Circle
|
||||||
|
«{{ circleMeta(item.circle_id)?.name }}»
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="mb-4 flex items-center space-x-2">
|
||||||
|
<it-icon-smiley-happy class="h-8 w-8"></it-icon-smiley-happy>
|
||||||
|
<span class="pr-3">
|
||||||
|
{{ (item as CompetenceRecordStatisticsType).success_count }}
|
||||||
|
</span>
|
||||||
|
<it-icon-smiley-thinking class="h-8 w-8"></it-icon-smiley-thinking>
|
||||||
|
<span>{{ (item as CompetenceRecordStatisticsType).fail_count }}</span>
|
||||||
|
</div>
|
||||||
|
<router-link
|
||||||
|
class="whitespace-nowrap underline"
|
||||||
|
target="_blank"
|
||||||
|
:to="(item as CompetenceRecordStatisticsType).details_url"
|
||||||
|
>
|
||||||
|
{{ $t("a.Details anschauen") }}
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</StatisticFilterList>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useDashboardStore } from "@/stores/dashboard";
|
||||||
|
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
|
||||||
|
import { computed } from "vue";
|
||||||
|
import type {
|
||||||
|
CourseStatisticsType,
|
||||||
|
FeedbackStatisticsRecordType,
|
||||||
|
PresenceRecordStatisticsType,
|
||||||
|
} from "@/gql/graphql";
|
||||||
|
import StatisticFilterList from "@/components/dashboard/StatisticFilterList.vue";
|
||||||
|
import { useCourseStatistics } from "@/composables";
|
||||||
|
import { getBlendedColorForRating } from "@/utils/ratingToColor";
|
||||||
|
|
||||||
|
const dashboardStore = useDashboardStore();
|
||||||
|
|
||||||
|
const statistics = computed(() => {
|
||||||
|
return dashboardStore.currentDashBoardData as CourseStatisticsType;
|
||||||
|
});
|
||||||
|
|
||||||
|
const { courseSessionName, circleMeta } = useCourseStatistics();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<main v-if="statistics">
|
||||||
|
<div class="mb-10 flex items-center justify-between">
|
||||||
|
<h3>{{ $t("a.Feedback Teilnehmer") }}</h3>
|
||||||
|
<ItDropdownSelect
|
||||||
|
:model-value="dashboardStore.currentDashboardConfig"
|
||||||
|
class="mt-4 w-full lg:mt-0 lg:w-96"
|
||||||
|
:items="dashboardStore.dashboardConfigs"
|
||||||
|
@update:model-value="dashboardStore.switchAndLoadDashboardConfig"
|
||||||
|
></ItDropdownSelect>
|
||||||
|
</div>
|
||||||
|
<div v-if="statistics.feedback_responses.records" class="mt-8 bg-white">
|
||||||
|
<StatisticFilterList
|
||||||
|
:course-session-properties="statistics.course_session_properties"
|
||||||
|
:items="statistics.feedback_responses.records"
|
||||||
|
>
|
||||||
|
<template #default="{ item }">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<div>
|
||||||
|
<h4 class="font-bold">
|
||||||
|
Feedback: Circle «{{ circleMeta(item.circle_id)?.name }}»
|
||||||
|
</h4>
|
||||||
|
<div>
|
||||||
|
{{ $t("a.Durchfuehrung") }} «{{
|
||||||
|
courseSessionName(item.course_session_id)
|
||||||
|
}}» - Trainer: {{ (item as FeedbackStatisticsRecordType).experts }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="mb-4 flex items-center space-x-2">
|
||||||
|
<div
|
||||||
|
class="rounded px-2 py-1"
|
||||||
|
:style="{ backgroundColor: getBlendedColorForRating((item as FeedbackStatisticsRecordType).satisfaction_average) }"
|
||||||
|
>
|
||||||
|
<i18next :translation="$t('a.{AVG} von {MAX}')">
|
||||||
|
<template #AVG>
|
||||||
|
<span class="font-bold">
|
||||||
|
{{
|
||||||
|
(
|
||||||
|
item as FeedbackStatisticsRecordType
|
||||||
|
).satisfaction_average.toFixed(1)
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template #MAX>
|
||||||
|
<span>
|
||||||
|
{{ (item as FeedbackStatisticsRecordType).satisfaction_max }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</i18next>
|
||||||
|
</div>
|
||||||
|
<span>
|
||||||
|
{{ $t("a.Allgemeine Zufriedenheit") }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<router-link
|
||||||
|
class="underline"
|
||||||
|
target="_blank"
|
||||||
|
:to="(item as PresenceRecordStatisticsType).details_url"
|
||||||
|
>
|
||||||
|
{{ $t("a.Details anschauen") }}
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</StatisticFilterList>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useDashboardStore } from "@/stores/dashboard";
|
||||||
|
import { onMounted } from "vue";
|
||||||
|
import LoadingSpinner from "@/components/ui/LoadingSpinner.vue";
|
||||||
|
|
||||||
|
const dashboardStore = useDashboardStore();
|
||||||
|
onMounted(dashboardStore.loadDashboardDetails);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="bg-gray-200">
|
||||||
|
<div v-if="dashboardStore.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>
|
||||||
|
<router-view></router-view>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -130,7 +130,7 @@ function render() {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="svg-container content-center">
|
<div class="aspect-square content-center">
|
||||||
<pre hidden>{{ pieData }}</pre>
|
<pre hidden>{{ pieData }}</pre>
|
||||||
<pre hidden>{{ render() }}</pre>
|
<pre hidden>{{ render() }}</pre>
|
||||||
<svg :id="svgId" class="h-full min-w-[20px]">
|
<svg :id="svgId" class="h-full min-w-[20px]">
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import DashboardPage from "@/pages/DashboardPage.vue";
|
import DashboardPage from "@/pages/dashboard/DashboardPage.vue";
|
||||||
import LoginPage from "@/pages/LoginPage.vue";
|
import LoginPage from "@/pages/LoginPage.vue";
|
||||||
import {
|
import {
|
||||||
handleCourseSessionAsQueryParam,
|
handleCourseSessionAsQueryParam,
|
||||||
|
|
@ -173,6 +173,38 @@ const router = createRouter({
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/statistic",
|
||||||
|
props: true,
|
||||||
|
component: () => import("@/pages/dashboard/statistic/StatisticParentPage.vue"),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "attendance",
|
||||||
|
props: true,
|
||||||
|
component: () => import("@/pages/dashboard/statistic/AttendanceList.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "assignment",
|
||||||
|
props: true,
|
||||||
|
component: () => import("@/pages/dashboard/statistic/AssignmentList.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "competence",
|
||||||
|
props: true,
|
||||||
|
component: () => import("@/pages/dashboard/statistic/CompetenceList.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "feedback",
|
||||||
|
props: true,
|
||||||
|
component: () => import("@/pages/dashboard/statistic/FeedbackList.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "list",
|
||||||
|
props: true,
|
||||||
|
component: () => import("@/pages/dashboard/CourseListPage.vue"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/shop",
|
path: "/shop",
|
||||||
component: () => import("@/pages/ShopPage.vue"),
|
component: () => import("@/pages/ShopPage.vue"),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { graphqlClient } from "@/graphql/client";
|
||||||
|
import {
|
||||||
|
DASHBOARD_CONFIG,
|
||||||
|
DASHBOARD_COURSE_SESSION_PROGRESS,
|
||||||
|
DASHBOARD_COURSE_STATISTICS,
|
||||||
|
} from "@/graphql/queries";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
CourseProgressType,
|
||||||
|
CourseStatisticsType,
|
||||||
|
DashboardConfigType,
|
||||||
|
} from "@/gql/graphql";
|
||||||
|
|
||||||
|
export const fetchStatisticData = async (
|
||||||
|
courseId: string
|
||||||
|
): Promise<CourseStatisticsType | null> => {
|
||||||
|
try {
|
||||||
|
console.log("fetching statistics for course ID: ", courseId);
|
||||||
|
const res = await graphqlClient.query(DASHBOARD_COURSE_STATISTICS, { courseId });
|
||||||
|
|
||||||
|
if (res.error) {
|
||||||
|
console.error("Error fetching statistics for course ID:", courseId, res.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.data?.course_statistics || null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error fetching statistics for course ID: ${courseId}`, error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchProgressData = async (
|
||||||
|
courseId: string
|
||||||
|
): Promise<CourseProgressType | null> => {
|
||||||
|
try {
|
||||||
|
const res = await graphqlClient.query(DASHBOARD_COURSE_SESSION_PROGRESS, {
|
||||||
|
courseId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.error) {
|
||||||
|
console.error("Error fetching progress for course ID:", courseId, res.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.data?.course_progress || null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error fetching progress for course ID: ${courseId}`, error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export const fetchDashboardConfig = async (): Promise<DashboardConfigType[] | null> => {
|
||||||
|
try {
|
||||||
|
const res = await graphqlClient.query(DASHBOARD_CONFIG, {});
|
||||||
|
|
||||||
|
if (res.error) {
|
||||||
|
console.error("Error fetching dashboard config:", res.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.data?.dashboard_config || null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching dashboard config:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
import type {
|
||||||
|
CourseProgressType,
|
||||||
|
CourseStatisticsType,
|
||||||
|
DashboardConfigType,
|
||||||
|
DashboardType,
|
||||||
|
} from "@/gql/graphql";
|
||||||
|
import {
|
||||||
|
fetchDashboardConfig,
|
||||||
|
fetchProgressData,
|
||||||
|
fetchStatisticData,
|
||||||
|
} from "@/services/dashboard";
|
||||||
|
import { defineStore } from "pinia";
|
||||||
|
import type { Ref } from "vue";
|
||||||
|
import { ref } from "vue";
|
||||||
|
|
||||||
|
export const useDashboardStore = defineStore("dashboard", () => {
|
||||||
|
const dashboardConfigs: Ref<DashboardConfigType[]> = ref([]);
|
||||||
|
const currentDashboardConfig: Ref<DashboardConfigType | undefined> = ref();
|
||||||
|
const dashBoardDataCache: Record<
|
||||||
|
string,
|
||||||
|
CourseStatisticsType | CourseProgressType | null
|
||||||
|
> = {};
|
||||||
|
const currentDashBoardData: Ref<CourseStatisticsType | CourseProgressType | null> =
|
||||||
|
ref(null);
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
const loadDashboardData = async (type: DashboardType, id: string) => {
|
||||||
|
let data;
|
||||||
|
switch (type) {
|
||||||
|
case "STATISTICS_DASHBOARD":
|
||||||
|
data = await fetchStatisticData(id);
|
||||||
|
break;
|
||||||
|
case "PROGRESS_DASHBOARD":
|
||||||
|
data = await fetchProgressData(id);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dashBoardDataCache[id] = data;
|
||||||
|
currentDashBoardData.value = data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const switchAndLoadDashboardConfig = async (config: DashboardConfigType) => {
|
||||||
|
currentDashboardConfig.value = config;
|
||||||
|
await loadDashboardDetails();
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadDashboardConfig = async () => {
|
||||||
|
if (dashboardConfigs.value.length > 0) return;
|
||||||
|
const configData = await fetchDashboardConfig();
|
||||||
|
if (configData) {
|
||||||
|
dashboardConfigs.value = configData;
|
||||||
|
await switchAndLoadDashboardConfig(configData[0]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadDashboardDetails = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
if (!currentDashboardConfig.value) {
|
||||||
|
await loadDashboardConfig();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { id, dashboard_type } = currentDashboardConfig.value;
|
||||||
|
if (dashBoardDataCache[id]) {
|
||||||
|
currentDashBoardData.value = dashBoardDataCache[id];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await loadDashboardData(dashboard_type, id);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
dashboardConfigs,
|
||||||
|
currentDashboardConfig,
|
||||||
|
switchAndLoadDashboardConfig,
|
||||||
|
loadDashboardConfig,
|
||||||
|
loadDashboardDetails,
|
||||||
|
currentDashBoardData,
|
||||||
|
loading,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
@ -5,7 +5,6 @@ import type {
|
||||||
AssignmentCompletionStatus as AssignmentCompletionStatusGenerated,
|
AssignmentCompletionStatus as AssignmentCompletionStatusGenerated,
|
||||||
AssignmentObjectType,
|
AssignmentObjectType,
|
||||||
CircleObjectType,
|
CircleObjectType,
|
||||||
CourseCourseSessionUserRoleChoices,
|
|
||||||
CourseSessionObjectType,
|
CourseSessionObjectType,
|
||||||
CourseSessionUserObjectsType,
|
CourseSessionUserObjectsType,
|
||||||
LearningContentAssignmentObjectType,
|
LearningContentAssignmentObjectType,
|
||||||
|
|
@ -435,8 +434,6 @@ export interface CourseSession {
|
||||||
due_dates: DueDate[];
|
due_dates: DueDate[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Role = CourseCourseSessionUserRoleChoices;
|
|
||||||
|
|
||||||
export type CourseSessionUser = CourseSessionUserObjectsType;
|
export type CourseSessionUser = CourseSessionUserObjectsType;
|
||||||
|
|
||||||
export interface ExpertSessionUser extends CourseSessionUser {
|
export interface ExpertSessionUser extends CourseSessionUser {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { getBlendedColorForRating } from "@/utils/ratingToColor";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
describe("getBlendedColorForRating", () => {
|
||||||
|
// Normal cases
|
||||||
|
it("should return red for rating 1", () => {
|
||||||
|
expect(getBlendedColorForRating(1)).toBe("rgb(221, 103, 81)");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return yellow for rating 2", () => {
|
||||||
|
expect(getBlendedColorForRating(2)).toBe("rgb(250, 200, 82)");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return light green for rating 3", () => {
|
||||||
|
expect(getBlendedColorForRating(3)).toBe("rgb(120, 222, 163)");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return dark green for rating 4", () => {
|
||||||
|
expect(getBlendedColorForRating(4)).toBe("rgb(91, 183, 130)");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Edge cases
|
||||||
|
it("should blend red and yellow for a rating between 1 and 2", () => {
|
||||||
|
expect(getBlendedColorForRating(1.5)).toBe("rgb(235, 151, 81)");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should blend yellow and light green for a rating between 2 and 3", () => {
|
||||||
|
expect(getBlendedColorForRating(2.5)).toBe("rgb(185, 211, 122)");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should blend light green and dark green for a rating between 3 and 4", () => {
|
||||||
|
expect(getBlendedColorForRating(3.5)).toBe("rgb(105, 202, 146)");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ratings beyond the scale
|
||||||
|
it("should return dark green for ratings above 4", () => {
|
||||||
|
expect(getBlendedColorForRating(4.5)).toBe("rgb(91, 183, 130)");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return grey for ratings below 1", () => {
|
||||||
|
expect(getBlendedColorForRating(0.5)).toBe("rgb(237, 242, 246)");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
type RGB = [number, number, number];
|
||||||
|
|
||||||
|
const grey: RGB = [237, 242, 246]; // grey-200
|
||||||
|
const red: RGB = [221, 103, 81]; // red-600
|
||||||
|
const yellow: RGB = [250, 200, 82]; // yellow-500
|
||||||
|
const lightGreen: RGB = [120, 222, 163]; // green-500
|
||||||
|
const darkGreen: RGB = [91, 183, 130]; // green-600
|
||||||
|
|
||||||
|
const blendColorValue = (v1: number, v2: number, weight: number): number => {
|
||||||
|
return v1 * (1 - weight) + v2 * weight;
|
||||||
|
};
|
||||||
|
|
||||||
|
const blendColors = (c1: RGB, c2: RGB, weight: number): RGB => {
|
||||||
|
const [r1, g1, b1] = c1;
|
||||||
|
const [r2, g2, b2] = c2;
|
||||||
|
return [
|
||||||
|
blendColorValue(r1, r2, weight),
|
||||||
|
blendColorValue(g1, g2, weight),
|
||||||
|
blendColorValue(b1, b2, weight),
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRGBString = (color: RGB): string => {
|
||||||
|
const [r, g, b] = color;
|
||||||
|
return `rgb(${Math.floor(r)}, ${Math.floor(g)}, ${Math.floor(b)})`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getBlendedColorForRating = (rating: number): string => {
|
||||||
|
const scale = Math.floor(rating);
|
||||||
|
const weight = rating % 1;
|
||||||
|
let colors: [RGB, RGB];
|
||||||
|
|
||||||
|
switch (scale) {
|
||||||
|
case 0:
|
||||||
|
return getRGBString(grey);
|
||||||
|
case 1:
|
||||||
|
colors = [red, yellow];
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
colors = [yellow, lightGreen];
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
colors = [lightGreen, darkGreen];
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
default:
|
||||||
|
return getRGBString(darkGreen);
|
||||||
|
}
|
||||||
|
|
||||||
|
const blendedColor = blendColors(colors[0], colors[1], weight);
|
||||||
|
return getRGBString(blendedColor);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
import {login} from "../helpers";
|
||||||
|
|
||||||
|
const getDashboardStatistics = (what) => {
|
||||||
|
return cy.get(`[data-cy="dashboard.stats.${what}"]`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const clickOnDetailsLink = (within) => {
|
||||||
|
cy.get(`[data-cy="dashboard.stats.${within}"]`).within(() => {
|
||||||
|
cy.get('[data-cy="basebox.detailsLink"]').click();
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("dashboardSupervisor.cy.js", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.manageCommand("cypress_reset --create-assignment-evaluation --create-feedback-responses --create-course-completion-performance-criteria --create-attendance-days");
|
||||||
|
login("test-supervisor1@example.com", "test");
|
||||||
|
cy.visit("/");
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("assignment summary box", () => {
|
||||||
|
it("contains correct numbers", () => {
|
||||||
|
// we have no completed assignments, but some are in progress
|
||||||
|
// -> makes sure that the numbers are correct
|
||||||
|
getDashboardStatistics("assignments.completed").should("have.text", "0");
|
||||||
|
getDashboardStatistics("assignments.passed").should("have.text", "0%");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("contains correct details link", () => {
|
||||||
|
clickOnDetailsLink("assignments");
|
||||||
|
|
||||||
|
// might be improved: roughly check
|
||||||
|
// that the correct data is displayed
|
||||||
|
cy.contains("Noch nicht bestätigt");
|
||||||
|
cy.contains("Fahrzeug - Mein erstes Auto");
|
||||||
|
cy.contains("Test Bern 2022 a");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("attendance day summary box", () => {
|
||||||
|
it("contains correct numbers", () => {
|
||||||
|
getDashboardStatistics("attendance.dayCompleted").should("have.text", "1");
|
||||||
|
getDashboardStatistics("attendance.participantsPresent").should("have.text", "34%");
|
||||||
|
});
|
||||||
|
it("contains correct details link", () => {
|
||||||
|
clickOnDetailsLink("attendance");
|
||||||
|
cy.url().should("contain", "/statistic/attendance");
|
||||||
|
|
||||||
|
// might be improved: roughly check
|
||||||
|
// that the correct data is displayed
|
||||||
|
cy.contains("Durchführung «Test Bern 2022 a»");
|
||||||
|
cy.contains("Circle «Fahrzeug»");
|
||||||
|
cy.contains("Termin: 31. Oktober 2000");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe("overall summary box", () => {
|
||||||
|
it("contains correct numbers (members, experts etc.)", () => {
|
||||||
|
getDashboardStatistics("participant.count").should("have.text", "4");
|
||||||
|
getDashboardStatistics("expert.count").should("have.text", "2");
|
||||||
|
getDashboardStatistics("session.count").should("have.text", "2");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("feedback summary box", () => {
|
||||||
|
it("contains correct numbers", () => {
|
||||||
|
getDashboardStatistics("feedback.average").should("have.text", "3.3");
|
||||||
|
getDashboardStatistics("feedback.count").should("have.text", "3");
|
||||||
|
|
||||||
|
});
|
||||||
|
it("contains correct details link", () => {
|
||||||
|
clickOnDetailsLink("feedback");
|
||||||
|
cy.url().should("contain", "/statistic/feedback");
|
||||||
|
|
||||||
|
// might be improved: roughly check
|
||||||
|
// that the correct data is displayed
|
||||||
|
cy.contains("3.3 von 4");
|
||||||
|
cy.contains("Test Trainer1");
|
||||||
|
cy.contains("Durchführung «Test Bern 2022 a»")
|
||||||
|
cy.contains("Circle «Fahrzeug»")
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("competence summary box", () => {
|
||||||
|
it("contains correct numbers", () => {
|
||||||
|
getDashboardStatistics("competence.success").should("have.text", "1");
|
||||||
|
getDashboardStatistics("competence.fail").should("have.text", "0");
|
||||||
|
});
|
||||||
|
it("contains correct details link", () => {
|
||||||
|
clickOnDetailsLink("competence");
|
||||||
|
cy.url().should("contain", "/statistic/competence");
|
||||||
|
|
||||||
|
// might be improved: roughly check
|
||||||
|
// that the correct data is displayed
|
||||||
|
cy.contains("Selbsteinschätzung: Vorbereitung");
|
||||||
|
cy.contains("Durchführung «Test Bern 2022 a»");
|
||||||
|
cy.contains("Circle «Fahrzeug»")
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
@ -19,14 +19,14 @@ describe("login.cy.js", () => {
|
||||||
cy.get('[data-cy="login-button"]').click();
|
cy.get('[data-cy="login-button"]').click();
|
||||||
cy.request("/api/core/me").its("status").should("eq", 200);
|
cy.request("/api/core/me").its("status").should("eq", 200);
|
||||||
|
|
||||||
cy.get('[data-cy="welcome-message"]').should("contain", "Willkommen, Test");
|
cy.get('[data-cy="dashboard-title"]').should("contain", "Dashboard");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("can login with helper function", () => {
|
it("can login with helper function", () => {
|
||||||
login("test-student1@example.com", "test");
|
login("test-student1@example.com", "test");
|
||||||
cy.visit("/");
|
cy.visit("/");
|
||||||
cy.request("/api/core/me").its("status").should("eq", 200);
|
cy.request("/api/core/me").its("status").should("eq", 200);
|
||||||
cy.get('[data-cy="welcome-message"]').should("contain", "Willkommen, Test");
|
cy.get('[data-cy="dashboard-title"]').should("contain", "Dashboard");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("login will redirect to requestet page", () => {
|
it("login will redirect to requestet page", () => {
|
||||||
|
|
@ -40,7 +40,7 @@ describe("login.cy.js", () => {
|
||||||
|
|
||||||
cy.get('[data-cy="learning-path-title"]').should(
|
cy.get('[data-cy="learning-path-title"]').should(
|
||||||
"contain",
|
"contain",
|
||||||
"Test Lehrgang"
|
"Test Lehrgang",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -129,6 +129,7 @@ LOCAL_APPS = [
|
||||||
"vbv_lernwelt.duedate",
|
"vbv_lernwelt.duedate",
|
||||||
"vbv_lernwelt.importer",
|
"vbv_lernwelt.importer",
|
||||||
"vbv_lernwelt.edoniq_test",
|
"vbv_lernwelt.edoniq_test",
|
||||||
|
"vbv_lernwelt.course_session_group",
|
||||||
]
|
]
|
||||||
# https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
|
# https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
|
||||||
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS
|
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ from vbv_lernwelt.assignment.models import Assignment, AssignmentCompletionStatu
|
||||||
from vbv_lernwelt.assignment.services import update_assignment_completion
|
from vbv_lernwelt.assignment.services import update_assignment_completion
|
||||||
from vbv_lernwelt.core.models import User
|
from vbv_lernwelt.core.models import User
|
||||||
from vbv_lernwelt.course.models import CourseSession, CourseSessionUser
|
from vbv_lernwelt.course.models import CourseSession, CourseSessionUser
|
||||||
from vbv_lernwelt.course.permissions import has_course_access, is_course_session_expert
|
from vbv_lernwelt.iam.permissions import has_course_access, is_course_session_expert
|
||||||
|
|
||||||
logger = structlog.get_logger(__name__)
|
logger = structlog.get_logger(__name__)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ from vbv_lernwelt.assignment.models import Assignment, AssignmentCompletion
|
||||||
from vbv_lernwelt.core.graphql.types import JSONStreamField
|
from vbv_lernwelt.core.graphql.types import JSONStreamField
|
||||||
from vbv_lernwelt.course.graphql.interfaces import CoursePageInterface
|
from vbv_lernwelt.course.graphql.interfaces import CoursePageInterface
|
||||||
from vbv_lernwelt.course.models import CourseSession
|
from vbv_lernwelt.course.models import CourseSession
|
||||||
from vbv_lernwelt.course.permissions import has_course_access, is_course_session_expert
|
from vbv_lernwelt.iam.permissions import has_course_access, is_course_session_expert
|
||||||
from vbv_lernwelt.learnpath.graphql.types import LearningContentInterface
|
from vbv_lernwelt.learnpath.graphql.types import LearningContentInterface
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ from rest_framework.exceptions import PermissionDenied
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from vbv_lernwelt.assignment.models import AssignmentCompletion
|
from vbv_lernwelt.assignment.models import AssignmentCompletion
|
||||||
from vbv_lernwelt.course.permissions import is_course_session_expert
|
from vbv_lernwelt.iam.permissions import is_course_session_expert
|
||||||
|
|
||||||
logger = structlog.get_logger(__name__)
|
logger = structlog.get_logger(__name__)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ DEFAULT_RICH_TEXT_FEATURES_WITH_HEADER = [
|
||||||
|
|
||||||
# ids for cypress test data
|
# ids for cypress test data
|
||||||
ADMIN_USER_ID = "872efd96-3bd7-4a1e-a239-2d72cad9f604"
|
ADMIN_USER_ID = "872efd96-3bd7-4a1e-a239-2d72cad9f604"
|
||||||
|
TEST_SUPERVISOR1_USER_ID = "a9a8b741-f115-4521-af2d-7dfef673b8c5"
|
||||||
TEST_TRAINER1_USER_ID = "b9e71f59-c44f-4290-b93a-9b3151e9a2fc"
|
TEST_TRAINER1_USER_ID = "b9e71f59-c44f-4290-b93a-9b3151e9a2fc"
|
||||||
TEST_TRAINER2_USER_ID = "299941ae-1e4b-4f45-8180-876c3ad340b4"
|
TEST_TRAINER2_USER_ID = "299941ae-1e4b-4f45-8180-876c3ad340b4"
|
||||||
TEST_STUDENT1_USER_ID = "65c73ad0-6d53-43a9-a4a4-64143f27b03a"
|
TEST_STUDENT1_USER_ID = "65c73ad0-6d53-43a9-a4a4-64143f27b03a"
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ from vbv_lernwelt.core.constants import (
|
||||||
TEST_STUDENT1_USER_ID,
|
TEST_STUDENT1_USER_ID,
|
||||||
TEST_STUDENT2_USER_ID,
|
TEST_STUDENT2_USER_ID,
|
||||||
TEST_STUDENT3_USER_ID,
|
TEST_STUDENT3_USER_ID,
|
||||||
|
TEST_SUPERVISOR1_USER_ID,
|
||||||
TEST_TRAINER1_USER_ID,
|
TEST_TRAINER1_USER_ID,
|
||||||
TEST_TRAINER2_USER_ID,
|
TEST_TRAINER2_USER_ID,
|
||||||
)
|
)
|
||||||
|
|
@ -67,15 +68,33 @@ default_users = [
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def create_default_users(user_model=User, group_model=Group, default_password=None):
|
def create_default_users(default_password="test"):
|
||||||
if default_password is None:
|
admin_group, created = Group.objects.get_or_create(name="admin_group")
|
||||||
default_password = "test"
|
_content_creator_grop, _created = Group.objects.get_or_create(
|
||||||
|
|
||||||
admin_group, created = group_model.objects.get_or_create(name="admin_group")
|
|
||||||
_content_creator_grop, _created = group_model.objects.get_or_create(
|
|
||||||
name="content_creator_grop"
|
name="content_creator_grop"
|
||||||
)
|
)
|
||||||
student_group, created = group_model.objects.get_or_create(name="student_group")
|
student_group, created = Group.objects.get_or_create(name="student_group")
|
||||||
|
|
||||||
|
def _create_user(
|
||||||
|
_id,
|
||||||
|
email,
|
||||||
|
first_name,
|
||||||
|
last_name,
|
||||||
|
avatar_url,
|
||||||
|
language,
|
||||||
|
password,
|
||||||
|
):
|
||||||
|
user, _ = User.objects.get_or_create(
|
||||||
|
id=_id,
|
||||||
|
username=email,
|
||||||
|
email=email,
|
||||||
|
language=language,
|
||||||
|
first_name=first_name,
|
||||||
|
last_name=last_name,
|
||||||
|
avatar_url=avatar_url,
|
||||||
|
password=make_password(password),
|
||||||
|
)
|
||||||
|
return user
|
||||||
|
|
||||||
def _create_student_user(
|
def _create_student_user(
|
||||||
email,
|
email,
|
||||||
|
|
@ -86,43 +105,52 @@ def create_default_users(user_model=User, group_model=Group, default_password=No
|
||||||
language="de",
|
language="de",
|
||||||
id=None,
|
id=None,
|
||||||
):
|
):
|
||||||
student_user, created = _get_or_create_user(
|
student_user = _create_user(
|
||||||
user_model=user_model,
|
email=email,
|
||||||
username=email,
|
first_name=first_name,
|
||||||
password=password,
|
last_name=last_name,
|
||||||
|
avatar_url=avatar_url,
|
||||||
language=language,
|
language=language,
|
||||||
id=id,
|
password=password,
|
||||||
|
_id=id,
|
||||||
)
|
)
|
||||||
student_user.first_name = first_name
|
|
||||||
student_user.last_name = last_name
|
|
||||||
student_user.avatar_url = avatar_url
|
|
||||||
student_user.groups.add(student_group)
|
student_user.groups.add(student_group)
|
||||||
student_user.save()
|
student_user.save()
|
||||||
|
|
||||||
def _create_admin_user(
|
def _create_admin_user(
|
||||||
email, first_name, last_name, avatar_url="", id=None, password=default_password
|
email, first_name, last_name, avatar_url="", id=None, password=default_password
|
||||||
):
|
):
|
||||||
admin_user, created = _get_or_create_user(
|
admin_user = _create_user(
|
||||||
user_model=user_model, username=email, password=password, id=id
|
email=email,
|
||||||
|
first_name=first_name,
|
||||||
|
last_name=last_name,
|
||||||
|
avatar_url=avatar_url,
|
||||||
|
password=password,
|
||||||
|
language="de",
|
||||||
|
_id=id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
admin_user.groups.add(admin_group)
|
||||||
admin_user.is_superuser = True
|
admin_user.is_superuser = True
|
||||||
admin_user.is_staff = True
|
admin_user.is_staff = True
|
||||||
admin_user.first_name = first_name
|
|
||||||
admin_user.last_name = last_name
|
|
||||||
admin_user.avatar_url = avatar_url
|
|
||||||
admin_user.groups.add(admin_group)
|
|
||||||
admin_user.save()
|
admin_user.save()
|
||||||
|
|
||||||
def _create_staff_user(
|
def _create_staff_user(
|
||||||
email, first_name, last_name, id=None, password=default_password
|
email, first_name, last_name, id=None, password=default_password
|
||||||
):
|
):
|
||||||
staff_user, created = _get_or_create_user(
|
staff_user = _create_user(
|
||||||
user_model=user_model, username=email, password=password, id=id
|
_id=id,
|
||||||
|
email=email,
|
||||||
|
first_name=first_name,
|
||||||
|
last_name=last_name,
|
||||||
|
avatar_url="",
|
||||||
|
language="de",
|
||||||
|
password=password,
|
||||||
)
|
)
|
||||||
staff_user.is_staff = True
|
|
||||||
staff_user.first_name = first_name
|
|
||||||
staff_user.last_name = last_name
|
|
||||||
staff_user.groups.add(_get_or_create_vbv_staff_group())
|
staff_user.groups.add(_get_or_create_vbv_staff_group())
|
||||||
|
staff_user.is_staff = True
|
||||||
staff_user.save()
|
staff_user.save()
|
||||||
|
|
||||||
_create_admin_user(
|
_create_admin_user(
|
||||||
|
|
@ -305,6 +333,15 @@ def create_default_users(user_model=User, group_model=Group, default_password=No
|
||||||
first_name="Matthias",
|
first_name="Matthias",
|
||||||
last_name="Wirth",
|
last_name="Wirth",
|
||||||
)
|
)
|
||||||
|
_create_user(
|
||||||
|
_id=TEST_SUPERVISOR1_USER_ID,
|
||||||
|
email="test-supervisor1@example.com",
|
||||||
|
first_name="[Supervisor]",
|
||||||
|
last_name="Regionalleiter",
|
||||||
|
password=default_password,
|
||||||
|
language="de",
|
||||||
|
avatar_url="",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _get_or_create_user(user_model, *args, **kwargs):
|
def _get_or_create_user(user_model, *args, **kwargs):
|
||||||
|
|
@ -319,7 +356,6 @@ def _get_or_create_user(user_model, *args, **kwargs):
|
||||||
if not user:
|
if not user:
|
||||||
user = user_model.objects.create(
|
user = user_model.objects.create(
|
||||||
username=username,
|
username=username,
|
||||||
password=make_password(password),
|
|
||||||
email=username,
|
email=username,
|
||||||
language=language,
|
language=language,
|
||||||
id=id,
|
id=id,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
import djclick as click
|
import djclick as click
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
from vbv_lernwelt.assignment.models import Assignment, AssignmentCompletion
|
from vbv_lernwelt.assignment.models import Assignment, AssignmentCompletion
|
||||||
|
from vbv_lernwelt.competence.models import PerformanceCriteria
|
||||||
from vbv_lernwelt.core.constants import (
|
from vbv_lernwelt.core.constants import (
|
||||||
TEST_COURSE_SESSION_BERN_ID,
|
TEST_COURSE_SESSION_BERN_ID,
|
||||||
TEST_STUDENT1_USER_ID,
|
TEST_STUDENT1_USER_ID,
|
||||||
|
|
@ -15,9 +19,19 @@ from vbv_lernwelt.course.creators.test_course import (
|
||||||
create_test_assignment_evaluation_data,
|
create_test_assignment_evaluation_data,
|
||||||
create_test_assignment_submitted_data,
|
create_test_assignment_submitted_data,
|
||||||
)
|
)
|
||||||
from vbv_lernwelt.course.models import CourseCompletion, CourseSession
|
from vbv_lernwelt.course.models import (
|
||||||
|
CourseCompletion,
|
||||||
|
CourseCompletionStatus,
|
||||||
|
CourseSession,
|
||||||
|
)
|
||||||
|
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.feedback.models import FeedbackResponse
|
||||||
from vbv_lernwelt.learnpath.models import LearningContentFeedback
|
from vbv_lernwelt.learnpath.models import (
|
||||||
|
LearningContentAttendanceCourse,
|
||||||
|
LearningContentFeedback,
|
||||||
|
)
|
||||||
from vbv_lernwelt.notify.models import Notification
|
from vbv_lernwelt.notify.models import Notification
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -49,18 +63,32 @@ from vbv_lernwelt.notify.models import Notification
|
||||||
default=False,
|
default=False,
|
||||||
help="will create feedback response data",
|
help="will create feedback response data",
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
"--create-course-completion-performance-criteria/--no-create-course-completion-performance-criteria",
|
||||||
|
default=False,
|
||||||
|
help="will create course completion performance criteria data",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--create-attendance-days/--no-create-attendance-days",
|
||||||
|
default=False,
|
||||||
|
help="will create attendance days data",
|
||||||
|
)
|
||||||
def command(
|
def command(
|
||||||
create_assignment_completion,
|
create_assignment_completion,
|
||||||
create_assignment_evaluation,
|
create_assignment_evaluation,
|
||||||
assignment_evaluation_scores,
|
assignment_evaluation_scores,
|
||||||
create_edoniq_test_results,
|
create_edoniq_test_results,
|
||||||
create_feedback_responses,
|
create_feedback_responses,
|
||||||
|
create_course_completion_performance_criteria,
|
||||||
|
create_attendance_days,
|
||||||
):
|
):
|
||||||
print("cypress reset data")
|
print("cypress reset data")
|
||||||
CourseCompletion.objects.all().delete()
|
CourseCompletion.objects.all().delete()
|
||||||
Notification.objects.all().delete()
|
Notification.objects.all().delete()
|
||||||
AssignmentCompletion.objects.all().delete()
|
AssignmentCompletion.objects.all().delete()
|
||||||
FeedbackResponse.objects.all().delete()
|
FeedbackResponse.objects.all().delete()
|
||||||
|
CourseSessionAttendanceCourse.objects.all().update(attendance_user_list=[])
|
||||||
|
|
||||||
User.objects.all().update(language="de")
|
User.objects.all().update(language="de")
|
||||||
User.objects.all().update(additional_json_data={})
|
User.objects.all().update(additional_json_data={})
|
||||||
|
|
||||||
|
|
@ -171,3 +199,47 @@ def command(
|
||||||
"course_positive_feedback": "Die Präsentation war super",
|
"course_positive_feedback": "Die Präsentation war super",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if create_course_completion_performance_criteria:
|
||||||
|
member = User.objects.get(id=TEST_STUDENT1_USER_ID)
|
||||||
|
course_session = CourseSession.objects.get(id=TEST_COURSE_SESSION_BERN_ID)
|
||||||
|
page = PerformanceCriteria.objects.get(
|
||||||
|
slug="test-lehrgang-competencenavi-competences-crit-x11-allgemein"
|
||||||
|
)
|
||||||
|
|
||||||
|
mark_course_completion(
|
||||||
|
page=page,
|
||||||
|
user=member,
|
||||||
|
course_session=course_session,
|
||||||
|
completion_status=CourseCompletionStatus.SUCCESS.value,
|
||||||
|
)
|
||||||
|
|
||||||
|
if create_attendance_days:
|
||||||
|
course_session = CourseSession.objects.get(id=TEST_COURSE_SESSION_BERN_ID)
|
||||||
|
|
||||||
|
attendance_user_list = [
|
||||||
|
{
|
||||||
|
"user_id": TEST_STUDENT1_USER_ID,
|
||||||
|
"status": AttendanceUserStatus.PRESENT.value,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_id": TEST_STUDENT2_USER_ID,
|
||||||
|
"status": AttendanceUserStatus.ABSENT.value,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
attendance_course = CourseSessionAttendanceCourse.objects.get(
|
||||||
|
course_session=course_session,
|
||||||
|
learning_content=LearningContentAttendanceCourse.objects.get(
|
||||||
|
slug="test-lehrgang-lp-circle-fahrzeug-lc-präsenzkurs-fahrzeug"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
attendance_course.attendance_user_list = attendance_user_list
|
||||||
|
attendance_course.due_date.start = timezone.make_aware(
|
||||||
|
datetime(year=2000, month=10, day=31, hour=8)
|
||||||
|
)
|
||||||
|
attendance_course.due_date.end = timezone.make_aware(
|
||||||
|
datetime(year=2000, month=10, day=31, hour=11)
|
||||||
|
)
|
||||||
|
attendance_course.save()
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ from vbv_lernwelt.competence.graphql.queries import CompetenceCertificateQuery
|
||||||
from vbv_lernwelt.course.graphql.queries import CourseQuery
|
from vbv_lernwelt.course.graphql.queries import CourseQuery
|
||||||
from vbv_lernwelt.course_session.graphql.mutations import CourseSessionMutation
|
from vbv_lernwelt.course_session.graphql.mutations import CourseSessionMutation
|
||||||
from vbv_lernwelt.course_session.graphql.queries import CourseSessionQuery
|
from vbv_lernwelt.course_session.graphql.queries import CourseSessionQuery
|
||||||
|
from vbv_lernwelt.dashboard.graphql.queries import DashboardQuery
|
||||||
from vbv_lernwelt.feedback.graphql.mutations import FeedbackMutation
|
from vbv_lernwelt.feedback.graphql.mutations import FeedbackMutation
|
||||||
from vbv_lernwelt.learnpath.graphql.queries import LearningPathQuery
|
from vbv_lernwelt.learnpath.graphql.queries import LearningPathQuery
|
||||||
|
|
||||||
|
|
@ -16,6 +17,7 @@ class Query(
|
||||||
CourseQuery,
|
CourseQuery,
|
||||||
CourseSessionQuery,
|
CourseSessionQuery,
|
||||||
LearningPathQuery,
|
LearningPathQuery,
|
||||||
|
DashboardQuery,
|
||||||
graphene.ObjectType,
|
graphene.ObjectType,
|
||||||
):
|
):
|
||||||
pass
|
pass
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
|
from typing import List
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.renderers import JSONRenderer
|
from rest_framework.renderers import JSONRenderer
|
||||||
|
|
||||||
from vbv_lernwelt.core.models import User
|
from vbv_lernwelt.core.models import User
|
||||||
from vbv_lernwelt.course.models import CourseSessionUser
|
from vbv_lernwelt.course.models import CourseSessionUser
|
||||||
|
from vbv_lernwelt.course_session_group.models import CourseSessionGroup
|
||||||
|
|
||||||
|
|
||||||
def create_json_from_objects(objects, serializer_class, many=True) -> str:
|
def create_json_from_objects(objects, serializer_class, many=True) -> str:
|
||||||
|
|
@ -35,9 +38,17 @@ class UserSerializer(serializers.ModelSerializer):
|
||||||
"username",
|
"username",
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_course_session_experts(self, obj):
|
def get_course_session_experts(self, obj: User) -> List[str]:
|
||||||
qs = CourseSessionUser.objects.filter(
|
supervisor_in_session_ids = set(
|
||||||
role=CourseSessionUser.Role.EXPERT, user=obj
|
CourseSessionGroup.objects.filter(supervisor=obj).values_list(
|
||||||
|
"course_session__id", flat=True
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
return [str(csu.course_session.id) for csu in qs]
|
expert_in_session_ids = set(
|
||||||
|
CourseSessionUser.objects.filter(
|
||||||
|
role=CourseSessionUser.Role.EXPERT, user=obj
|
||||||
|
).values_list("course_session__id", flat=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
return [str(_id) for _id in (supervisor_in_session_ids | expert_in_session_ids)]
|
||||||
|
|
|
||||||
|
|
@ -153,11 +153,9 @@ def cypress_reset_view(request):
|
||||||
if assignment_evaluation_scores:
|
if assignment_evaluation_scores:
|
||||||
options["assignment_evaluation_scores"] = assignment_evaluation_scores
|
options["assignment_evaluation_scores"] = assignment_evaluation_scores
|
||||||
|
|
||||||
create_feedback_responses = (
|
options["create_feedback_responses"] = (
|
||||||
request.data.get("create_feedback_responses") == "true"
|
request.data.get("create_feedback_responses") == "true"
|
||||||
)
|
)
|
||||||
if create_feedback_responses:
|
|
||||||
options["create_feedback_responses"] = create_feedback_responses
|
|
||||||
|
|
||||||
# edoniq test results
|
# edoniq test results
|
||||||
edoniq_test_user_points = request.data.get("edoniq_test_user_points")
|
edoniq_test_user_points = request.data.get("edoniq_test_user_points")
|
||||||
|
|
@ -168,6 +166,14 @@ def cypress_reset_view(request):
|
||||||
int(edoniq_test_max_points),
|
int(edoniq_test_max_points),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
options["create_course_completion_performance_criteria"] = (
|
||||||
|
request.data.get("create_course_completion_performance_criteria") == "true"
|
||||||
|
)
|
||||||
|
|
||||||
|
options["create_attendance_days"] = (
|
||||||
|
request.data.get("create_attendance_days") == "true"
|
||||||
|
)
|
||||||
|
|
||||||
call_command(
|
call_command(
|
||||||
"cypress_reset",
|
"cypress_reset",
|
||||||
**options,
|
**options,
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,11 @@ from vbv_lernwelt.competence.models import ActionCompetence
|
||||||
from vbv_lernwelt.core.constants import (
|
from vbv_lernwelt.core.constants import (
|
||||||
TEST_COURSE_SESSION_BERN_ID,
|
TEST_COURSE_SESSION_BERN_ID,
|
||||||
TEST_COURSE_SESSION_ZURICH_ID,
|
TEST_COURSE_SESSION_ZURICH_ID,
|
||||||
|
TEST_STUDENT1_USER_ID,
|
||||||
|
TEST_STUDENT2_USER_ID,
|
||||||
|
TEST_STUDENT3_USER_ID,
|
||||||
|
TEST_SUPERVISOR1_USER_ID,
|
||||||
|
TEST_TRAINER1_USER_ID,
|
||||||
)
|
)
|
||||||
from vbv_lernwelt.core.models import User
|
from vbv_lernwelt.core.models import User
|
||||||
from vbv_lernwelt.course.consts import COURSE_TEST_ID
|
from vbv_lernwelt.course.consts import COURSE_TEST_ID
|
||||||
|
|
@ -51,6 +56,7 @@ from vbv_lernwelt.course_session.models import (
|
||||||
CourseSessionAttendanceCourse,
|
CourseSessionAttendanceCourse,
|
||||||
CourseSessionEdoniqTest,
|
CourseSessionEdoniqTest,
|
||||||
)
|
)
|
||||||
|
from vbv_lernwelt.course_session_group.models import CourseSessionGroup
|
||||||
from vbv_lernwelt.feedback.services import update_feedback_response
|
from vbv_lernwelt.feedback.services import update_feedback_response
|
||||||
from vbv_lernwelt.learnpath.models import (
|
from vbv_lernwelt.learnpath.models import (
|
||||||
Circle,
|
Circle,
|
||||||
|
|
@ -191,10 +197,18 @@ def create_test_course(include_uk=True, include_vv=True, with_sessions=False):
|
||||||
start_date=now,
|
start_date=now,
|
||||||
)
|
)
|
||||||
|
|
||||||
trainer1 = User.objects.get(email="test-trainer1@example.com")
|
region1 = CourseSessionGroup.objects.create(
|
||||||
|
name="Region 1",
|
||||||
|
course=course,
|
||||||
|
)
|
||||||
|
|
||||||
|
region1.course_session.add(cs_bern)
|
||||||
|
region1.course_session.add(cs_zurich)
|
||||||
|
region1.supervisor.set([User.objects.get(id=TEST_SUPERVISOR1_USER_ID)])
|
||||||
|
|
||||||
csu = CourseSessionUser.objects.create(
|
csu = CourseSessionUser.objects.create(
|
||||||
course_session=cs_bern,
|
course_session=cs_bern,
|
||||||
user=trainer1,
|
user=User.objects.get(id=TEST_TRAINER1_USER_ID),
|
||||||
role=CourseSessionUser.Role.EXPERT,
|
role=CourseSessionUser.Role.EXPERT,
|
||||||
)
|
)
|
||||||
csu.expert.add(Circle.objects.get(slug="test-lehrgang-lp-circle-fahrzeug"))
|
csu.expert.add(Circle.objects.get(slug="test-lehrgang-lp-circle-fahrzeug"))
|
||||||
|
|
@ -207,27 +221,18 @@ def create_test_course(include_uk=True, include_vv=True, with_sessions=False):
|
||||||
)
|
)
|
||||||
csu.expert.add(Circle.objects.get(slug="test-lehrgang-lp-circle-fahrzeug"))
|
csu.expert.add(Circle.objects.get(slug="test-lehrgang-lp-circle-fahrzeug"))
|
||||||
|
|
||||||
student1 = User.objects.get(email="test-student1@example.com")
|
CourseSessionUser.objects.create(
|
||||||
_csu = CourseSessionUser.objects.create(
|
course_session=cs_bern, user=User.objects.get(id=TEST_STUDENT1_USER_ID)
|
||||||
course_session=cs_bern,
|
|
||||||
user=student1,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
student2 = User.objects.get(email="test-student2@example.com")
|
# in both sessions (BE and ZH)
|
||||||
_csu = CourseSessionUser.objects.create(
|
test_student_2 = User.objects.get(id=TEST_STUDENT2_USER_ID)
|
||||||
course_session=cs_bern,
|
CourseSessionUser.objects.create(course_session=cs_bern, user=test_student_2)
|
||||||
user=student2,
|
CourseSessionUser.objects.create(course_session=cs_zurich, user=test_student_2)
|
||||||
)
|
|
||||||
student2 = User.objects.get(email="test-student2@example.com")
|
|
||||||
_csu = CourseSessionUser.objects.create(
|
|
||||||
course_session=cs_zurich,
|
|
||||||
user=student2,
|
|
||||||
)
|
|
||||||
|
|
||||||
student3 = User.objects.get(email="test-student3@example.com")
|
CourseSessionUser.objects.create(
|
||||||
_csu = CourseSessionUser.objects.create(
|
|
||||||
course_session=cs_bern,
|
course_session=cs_bern,
|
||||||
user=student3,
|
user=User.objects.get(id=TEST_STUDENT3_USER_ID),
|
||||||
)
|
)
|
||||||
|
|
||||||
return course
|
return course
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,301 @@
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from typing import List, Tuple
|
||||||
|
|
||||||
|
from django.contrib.auth.hashers import make_password
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from vbv_lernwelt.assignment.models import (
|
||||||
|
Assignment,
|
||||||
|
AssignmentCompletion,
|
||||||
|
AssignmentCompletionStatus,
|
||||||
|
AssignmentType,
|
||||||
|
)
|
||||||
|
from vbv_lernwelt.assignment.tests.assignment_factories import (
|
||||||
|
AssignmentFactory,
|
||||||
|
AssignmentListPageFactory,
|
||||||
|
)
|
||||||
|
from vbv_lernwelt.competence.factories import (
|
||||||
|
ActionCompetenceFactory,
|
||||||
|
ActionCompetenceListPageFactory,
|
||||||
|
CompetenceNaviPageFactory,
|
||||||
|
PerformanceCriteriaFactory,
|
||||||
|
)
|
||||||
|
from vbv_lernwelt.competence.models import PerformanceCriteria
|
||||||
|
from vbv_lernwelt.core.models import User
|
||||||
|
from vbv_lernwelt.course.factories import CoursePageFactory
|
||||||
|
from vbv_lernwelt.course.models import (
|
||||||
|
Course,
|
||||||
|
CourseCategory,
|
||||||
|
CoursePage,
|
||||||
|
CourseSession,
|
||||||
|
CourseSessionUser,
|
||||||
|
)
|
||||||
|
from vbv_lernwelt.course.utils import get_wagtail_default_site
|
||||||
|
from vbv_lernwelt.course_session.models import (
|
||||||
|
CourseSessionAssignment,
|
||||||
|
CourseSessionAttendanceCourse,
|
||||||
|
CourseSessionEdoniqTest,
|
||||||
|
)
|
||||||
|
from vbv_lernwelt.course_session_group.models import CourseSessionGroup
|
||||||
|
from vbv_lernwelt.duedate.models import DueDate
|
||||||
|
from vbv_lernwelt.learnpath.models import (
|
||||||
|
Circle,
|
||||||
|
LearningContentAssignment,
|
||||||
|
LearningContentEdoniqTest,
|
||||||
|
LearningPath,
|
||||||
|
)
|
||||||
|
from vbv_lernwelt.learnpath.tests.learning_path_factories import (
|
||||||
|
CircleFactory,
|
||||||
|
LearningContentAssignmentFactory,
|
||||||
|
LearningContentAttendanceCourseFactory,
|
||||||
|
LearningContentEdoniqTestFactory,
|
||||||
|
LearningPathFactory,
|
||||||
|
LearningUnitFactory,
|
||||||
|
TopicFactory,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_course(title: str) -> Tuple[Course, CoursePage]:
|
||||||
|
course = Course.objects.create(title=title, category_name="Handlungsfeld")
|
||||||
|
|
||||||
|
course_page = CoursePageFactory(
|
||||||
|
title="Test Lehrgang",
|
||||||
|
parent=get_wagtail_default_site().root_page,
|
||||||
|
course=course,
|
||||||
|
)
|
||||||
|
course.slug = course_page.slug
|
||||||
|
course.save()
|
||||||
|
|
||||||
|
return course, course_page
|
||||||
|
|
||||||
|
|
||||||
|
def create_user(username: str) -> User:
|
||||||
|
return User.objects.create_user(
|
||||||
|
username=username,
|
||||||
|
password=make_password("test"),
|
||||||
|
email=f"{username}@example.com",
|
||||||
|
language="de",
|
||||||
|
first_name="Test",
|
||||||
|
last_name=username.capitalize(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_course_session(
|
||||||
|
course: Course, title: str, generation: str = "2023"
|
||||||
|
) -> CourseSession:
|
||||||
|
return CourseSession.objects.create(
|
||||||
|
course=course,
|
||||||
|
title=title,
|
||||||
|
import_id=title,
|
||||||
|
generation=generation,
|
||||||
|
start_date=timezone.now(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def add_course_session_user(
|
||||||
|
course_session: CourseSession, user: User, role: CourseSessionUser.Role
|
||||||
|
) -> CourseSessionUser:
|
||||||
|
return CourseSessionUser.objects.create(
|
||||||
|
course_session=course_session,
|
||||||
|
user=user,
|
||||||
|
role=role,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_course_session_group(course_session: CourseSession) -> CourseSessionGroup:
|
||||||
|
group = CourseSessionGroup.objects.create(
|
||||||
|
course=course_session.course,
|
||||||
|
)
|
||||||
|
|
||||||
|
group.course_session.add(course_session)
|
||||||
|
|
||||||
|
return group
|
||||||
|
|
||||||
|
|
||||||
|
def add_course_session_group_supervisor(group: CourseSessionGroup, user: User):
|
||||||
|
group.supervisor.add(user)
|
||||||
|
|
||||||
|
|
||||||
|
def add_course_session_group_course_session(
|
||||||
|
group: CourseSessionGroup, course_session: CourseSession
|
||||||
|
):
|
||||||
|
group.course_session.add(course_session)
|
||||||
|
|
||||||
|
|
||||||
|
def create_circle(
|
||||||
|
title: str, course_page: CoursePage, learning_path: LearningPath | None = None
|
||||||
|
) -> Tuple[Circle, LearningPath]:
|
||||||
|
if not learning_path:
|
||||||
|
learning_path = LearningPathFactory(title="Test Lernpfad", parent=course_page)
|
||||||
|
|
||||||
|
TopicFactory(title="Circle Test Topic", is_visible=False, parent=learning_path)
|
||||||
|
|
||||||
|
circle = CircleFactory(
|
||||||
|
title=title, parent=learning_path, description="Circle Description"
|
||||||
|
)
|
||||||
|
|
||||||
|
return circle, learning_path
|
||||||
|
|
||||||
|
|
||||||
|
def create_attendance_course(
|
||||||
|
course_session: CourseSession,
|
||||||
|
circle: Circle,
|
||||||
|
attendance_user_list: List,
|
||||||
|
due_date_end: datetime,
|
||||||
|
) -> CourseSessionAttendanceCourse:
|
||||||
|
learning_content_dummy = LearningContentAttendanceCourseFactory(
|
||||||
|
title="Lerninhalt Dummy",
|
||||||
|
parent=circle,
|
||||||
|
)
|
||||||
|
|
||||||
|
return CourseSessionAttendanceCourse.objects.create(
|
||||||
|
course_session=course_session,
|
||||||
|
learning_content=learning_content_dummy,
|
||||||
|
attendance_user_list=attendance_user_list,
|
||||||
|
due_date=DueDate.objects.create(
|
||||||
|
course_session=course_session,
|
||||||
|
start=due_date_end - timedelta(hours=8),
|
||||||
|
end=due_date_end,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_assignment(
|
||||||
|
course: Course,
|
||||||
|
assignment_type: AssignmentType,
|
||||||
|
) -> Assignment:
|
||||||
|
return AssignmentFactory(
|
||||||
|
parent=AssignmentListPageFactory(
|
||||||
|
parent=course.coursepage,
|
||||||
|
),
|
||||||
|
assignment_type=assignment_type.name,
|
||||||
|
title=f"Dummy Assignment ({assignment_type.name})",
|
||||||
|
effort_required=":)",
|
||||||
|
intro_text=":)",
|
||||||
|
performance_objectives=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_assignment_completion(
|
||||||
|
user: User,
|
||||||
|
assignment: Assignment,
|
||||||
|
course_session: CourseSession,
|
||||||
|
has_passed: bool | None = None,
|
||||||
|
max_points: int = 0,
|
||||||
|
achieved_points: int = 0,
|
||||||
|
status: AssignmentCompletionStatus = AssignmentCompletionStatus.EVALUATION_SUBMITTED,
|
||||||
|
) -> AssignmentCompletion:
|
||||||
|
return AssignmentCompletion.objects.create(
|
||||||
|
completion_status=status.value,
|
||||||
|
assignment_user=user,
|
||||||
|
assignment=assignment,
|
||||||
|
evaluation_passed=has_passed,
|
||||||
|
course_session=course_session,
|
||||||
|
completion_data={},
|
||||||
|
evaluation_max_points=max_points,
|
||||||
|
evaluation_points=achieved_points,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_assignment_learning_content(
|
||||||
|
circle: Circle,
|
||||||
|
assignment: Assignment,
|
||||||
|
) -> LearningContentAssignment | LearningContentEdoniqTest:
|
||||||
|
if AssignmentType(assignment.assignment_type) == AssignmentType.EDONIQ_TEST:
|
||||||
|
return LearningContentEdoniqTestFactory(
|
||||||
|
title="Learning Content (EDONIQ_TEST)",
|
||||||
|
parent=circle,
|
||||||
|
content_assignment=assignment,
|
||||||
|
)
|
||||||
|
|
||||||
|
return LearningContentAssignmentFactory(
|
||||||
|
title=f"Learning Content ({assignment.assignment_type})",
|
||||||
|
parent=circle,
|
||||||
|
content_assignment=assignment,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_course_session_assignment(
|
||||||
|
course_session: CourseSession,
|
||||||
|
learning_content_assignment: LearningContentAssignment,
|
||||||
|
deadline_at: datetime | None = None,
|
||||||
|
) -> CourseSessionAssignment:
|
||||||
|
cas = CourseSessionAssignment.objects.create(
|
||||||
|
course_session=course_session,
|
||||||
|
learning_content=learning_content_assignment,
|
||||||
|
)
|
||||||
|
|
||||||
|
if deadline_at:
|
||||||
|
# the save on the course_session_assignment already sets a lot
|
||||||
|
# of due date fields, so it's easier to just overwrite the this
|
||||||
|
cas.submission_deadline.start = timezone.make_aware(deadline_at)
|
||||||
|
cas.submission_deadline.save()
|
||||||
|
|
||||||
|
return cas
|
||||||
|
|
||||||
|
|
||||||
|
def create_course_session_edoniq_test(
|
||||||
|
course_session: CourseSession,
|
||||||
|
learning_content_edoniq_test: LearningContentEdoniqTest,
|
||||||
|
deadline_at: datetime,
|
||||||
|
) -> CourseSessionEdoniqTest:
|
||||||
|
cset = CourseSessionEdoniqTest.objects.create(
|
||||||
|
course_session=course_session,
|
||||||
|
learning_content=learning_content_edoniq_test,
|
||||||
|
)
|
||||||
|
|
||||||
|
# same as above (see create_course_session_assignment)
|
||||||
|
cset.deadline.start = timezone.make_aware(deadline_at)
|
||||||
|
cset.deadline.save()
|
||||||
|
|
||||||
|
return cset
|
||||||
|
|
||||||
|
|
||||||
|
def create_performance_criteria_page(
|
||||||
|
course: Course,
|
||||||
|
course_page: CoursePage,
|
||||||
|
circle: Circle,
|
||||||
|
) -> PerformanceCriteria:
|
||||||
|
competence_navi_page = CompetenceNaviPageFactory(
|
||||||
|
title="Competence Navi",
|
||||||
|
parent=course_page,
|
||||||
|
)
|
||||||
|
|
||||||
|
competence_profile_page = ActionCompetenceListPageFactory(
|
||||||
|
title="Action Competence Page",
|
||||||
|
parent=competence_navi_page,
|
||||||
|
)
|
||||||
|
|
||||||
|
action_competence = ActionCompetenceFactory(
|
||||||
|
parent=competence_profile_page,
|
||||||
|
competence_id="X1",
|
||||||
|
title="Action Competence",
|
||||||
|
items=[("item", "Action Competence Item")],
|
||||||
|
)
|
||||||
|
|
||||||
|
cat, _ = CourseCategory.objects.get_or_create(
|
||||||
|
course=course, title="Course Category"
|
||||||
|
)
|
||||||
|
|
||||||
|
lu = LearningUnitFactory(title="Learning Unit", parent=circle, course_category=cat)
|
||||||
|
|
||||||
|
return PerformanceCriteriaFactory(
|
||||||
|
parent=action_competence,
|
||||||
|
competence_id="X1.1",
|
||||||
|
title="Performance Criteria",
|
||||||
|
learning_unit=lu,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_circle_expert(
|
||||||
|
course_session: CourseSession, circle: Circle, username: str
|
||||||
|
) -> CourseSessionUser:
|
||||||
|
expert_user = create_user(username)
|
||||||
|
course_session_expert_user = add_course_session_user(
|
||||||
|
course_session=course_session,
|
||||||
|
user=expert_user,
|
||||||
|
role=CourseSessionUser.Role.EXPERT,
|
||||||
|
)
|
||||||
|
course_session_expert_user.expert.add(circle)
|
||||||
|
|
||||||
|
return course_session_expert_user
|
||||||
|
|
@ -4,7 +4,7 @@ from graphql import GraphQLError
|
||||||
|
|
||||||
from vbv_lernwelt.course.graphql.types import CourseObjectType, CourseSessionObjectType
|
from vbv_lernwelt.course.graphql.types import CourseObjectType, CourseSessionObjectType
|
||||||
from vbv_lernwelt.course.models import Course, CourseSession
|
from vbv_lernwelt.course.models import Course, CourseSession
|
||||||
from vbv_lernwelt.course.permissions import has_course_access
|
from vbv_lernwelt.iam.permissions import has_course_access
|
||||||
from vbv_lernwelt.learnpath.graphql.types import (
|
from vbv_lernwelt.learnpath.graphql.types import (
|
||||||
LearningContentAssignmentObjectType,
|
LearningContentAssignmentObjectType,
|
||||||
LearningContentAttendanceCourseObjectType,
|
LearningContentAttendanceCourseObjectType,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
from typing import Type
|
from typing import List, Type
|
||||||
|
|
||||||
import graphene
|
import graphene
|
||||||
import structlog
|
import structlog
|
||||||
|
|
@ -16,7 +16,6 @@ from vbv_lernwelt.course.models import (
|
||||||
CourseSession,
|
CourseSession,
|
||||||
CourseSessionUser,
|
CourseSessionUser,
|
||||||
)
|
)
|
||||||
from vbv_lernwelt.course.permissions import has_course_access
|
|
||||||
from vbv_lernwelt.course_session.graphql.types import (
|
from vbv_lernwelt.course_session.graphql.types import (
|
||||||
CourseSessionAssignmentObjectType,
|
CourseSessionAssignmentObjectType,
|
||||||
CourseSessionAttendanceCourseObjectType,
|
CourseSessionAttendanceCourseObjectType,
|
||||||
|
|
@ -27,7 +26,10 @@ from vbv_lernwelt.course_session.models import (
|
||||||
CourseSessionAttendanceCourse,
|
CourseSessionAttendanceCourse,
|
||||||
CourseSessionEdoniqTest,
|
CourseSessionEdoniqTest,
|
||||||
)
|
)
|
||||||
|
from vbv_lernwelt.course_session_group.models import CourseSessionGroup
|
||||||
|
from vbv_lernwelt.iam.permissions import has_course_access
|
||||||
from vbv_lernwelt.learnpath.graphql.types import LearningPathObjectType
|
from vbv_lernwelt.learnpath.graphql.types import LearningPathObjectType
|
||||||
|
from vbv_lernwelt.learnpath.models import Circle
|
||||||
|
|
||||||
logger = structlog.get_logger(__name__)
|
logger = structlog.get_logger(__name__)
|
||||||
|
|
||||||
|
|
@ -109,50 +111,26 @@ class CourseSessionUserExpertCircleType(ObjectType):
|
||||||
slug = graphene.String(required=True)
|
slug = graphene.String(required=True)
|
||||||
|
|
||||||
|
|
||||||
class CourseSessionUserObjectsType(DjangoObjectType):
|
class CourseSessionUserObjectsType(ObjectType):
|
||||||
|
"""
|
||||||
|
WORKAROUND:
|
||||||
|
Why is this no DjangoObjectType? It's because we have to "inject"
|
||||||
|
the supervisor into the list of users. This is done in the resolve_users
|
||||||
|
of the CourseSessionObjectType. And there we have to be able to construct
|
||||||
|
a CourseSessionUserObjectsType with the CIRCLES of the supervisor!
|
||||||
|
"""
|
||||||
|
|
||||||
|
id = graphene.ID(required=True)
|
||||||
user_id = graphene.UUID(required=True)
|
user_id = graphene.UUID(required=True)
|
||||||
first_name = graphene.String(required=True)
|
first_name = graphene.String(required=True)
|
||||||
last_name = graphene.String(required=True)
|
last_name = graphene.String(required=True)
|
||||||
email = graphene.String(required=True)
|
email = graphene.String(required=True)
|
||||||
avatar_url = graphene.String(required=True)
|
avatar_url = graphene.String(required=True)
|
||||||
# role = graphene.String(required=True)
|
role = graphene.String(required=True)
|
||||||
circles = graphene.List(
|
circles = graphene.List(
|
||||||
graphene.NonNull(CourseSessionUserExpertCircleType), required=True
|
graphene.NonNull(CourseSessionUserExpertCircleType), required=True
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = CourseSessionUser
|
|
||||||
fields = (
|
|
||||||
"id",
|
|
||||||
"user_id",
|
|
||||||
"first_name",
|
|
||||||
"last_name",
|
|
||||||
"email",
|
|
||||||
"avatar_url",
|
|
||||||
"role",
|
|
||||||
)
|
|
||||||
|
|
||||||
def resolve_user_id(self, info):
|
|
||||||
return self.user.id
|
|
||||||
|
|
||||||
def resolve_first_name(self, info):
|
|
||||||
return self.user.first_name
|
|
||||||
|
|
||||||
def resolve_last_name(self, info):
|
|
||||||
return self.user.last_name
|
|
||||||
|
|
||||||
def resolve_email(self, info):
|
|
||||||
return self.user.email
|
|
||||||
|
|
||||||
def resolve_avatar_url(self, info):
|
|
||||||
return self.user.avatar_url
|
|
||||||
|
|
||||||
def resolve_role(self, info):
|
|
||||||
return self.role
|
|
||||||
|
|
||||||
def resolve_circles(self, info):
|
|
||||||
return self.expert.all().values("id", "title", "slug", "translation_key")
|
|
||||||
|
|
||||||
|
|
||||||
class CircleDocumentObjectType(DjangoObjectType):
|
class CircleDocumentObjectType(DjangoObjectType):
|
||||||
file_name = graphene.String()
|
file_name = graphene.String()
|
||||||
|
|
@ -211,4 +189,69 @@ class CourseSessionObjectType(DjangoObjectType):
|
||||||
return CourseSessionEdoniqTest.objects.filter(course_session=self)
|
return CourseSessionEdoniqTest.objects.filter(course_session=self)
|
||||||
|
|
||||||
def resolve_users(self, info):
|
def resolve_users(self, info):
|
||||||
return CourseSessionUser.objects.filter(course_session_id=self.id).distinct()
|
course_session_users_resolved: List[CourseSessionUserObjectsType] = []
|
||||||
|
|
||||||
|
# happy path, members and experts
|
||||||
|
for course_session_user in CourseSessionUser.objects.filter(
|
||||||
|
course_session_id=self.id
|
||||||
|
).distinct():
|
||||||
|
course_session_users_resolved.append(
|
||||||
|
CourseSessionUserObjectsType(
|
||||||
|
id=course_session_user.id, # noqa
|
||||||
|
user_id=course_session_user.user.id, # noqa
|
||||||
|
first_name=course_session_user.user.first_name, # noqa
|
||||||
|
last_name=course_session_user.user.last_name, # noqa
|
||||||
|
email=course_session_user.user.email, # noqa
|
||||||
|
avatar_url=course_session_user.user.avatar_url, # noqa
|
||||||
|
role=course_session_user.role, # noqa
|
||||||
|
circles=[ # noqa
|
||||||
|
CourseSessionUserExpertCircleType( # noqa
|
||||||
|
id=circle.id, # noqa
|
||||||
|
title=circle.title, # noqa
|
||||||
|
slug=circle.slug, # noqa
|
||||||
|
)
|
||||||
|
for circle in course_session_user.expert.all() # noqa
|
||||||
|
],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# workaround for supervisor
|
||||||
|
# add supervisor to the list of users (as expert)
|
||||||
|
course_session_id = self.id # noqa
|
||||||
|
user = info.context.user # noqa
|
||||||
|
|
||||||
|
if CourseSessionGroup.objects.filter(
|
||||||
|
course_session=course_session_id, supervisor=user
|
||||||
|
).exists():
|
||||||
|
if course_session := CourseSession.objects.filter(
|
||||||
|
id=course_session_id
|
||||||
|
).first():
|
||||||
|
circles = (
|
||||||
|
course_session.course.get_learning_path()
|
||||||
|
.get_descendants()
|
||||||
|
.live()
|
||||||
|
.specific()
|
||||||
|
.exact_type(Circle)
|
||||||
|
)
|
||||||
|
|
||||||
|
course_session_users_resolved.append(
|
||||||
|
CourseSessionUserObjectsType(
|
||||||
|
id=f"{user.id}-as-ephemeral-supervisor", # noqa
|
||||||
|
user_id=user.id, # noqa
|
||||||
|
first_name=user.first_name, # noqa
|
||||||
|
last_name=user.last_name, # noqa
|
||||||
|
email=user.email, # noqa
|
||||||
|
avatar_url=user.avatar_url, # noqa
|
||||||
|
role=CourseSessionUser.Role.EXPERT, # noqa
|
||||||
|
circles=[ # noqa
|
||||||
|
CourseSessionUserExpertCircleType( # noqa
|
||||||
|
id=circle.id, # noqa
|
||||||
|
title=circle.title, # noqa
|
||||||
|
slug=circle.slug, # noqa
|
||||||
|
)
|
||||||
|
for circle in circles
|
||||||
|
],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return course_session_users_resolved
|
||||||
|
|
|
||||||
|
|
@ -6,19 +6,7 @@ from rest_framework.response import Response
|
||||||
from wagtail.models import Page
|
from wagtail.models import Page
|
||||||
|
|
||||||
from vbv_lernwelt.core.utils import get_django_content_type
|
from vbv_lernwelt.core.utils import get_django_content_type
|
||||||
from vbv_lernwelt.course.models import (
|
from vbv_lernwelt.course.models import CircleDocument, CourseCompletion, CourseSession
|
||||||
CircleDocument,
|
|
||||||
CourseCompletion,
|
|
||||||
CourseSession,
|
|
||||||
CourseSessionUser,
|
|
||||||
)
|
|
||||||
from vbv_lernwelt.course.permissions import (
|
|
||||||
course_sessions_for_user_qs,
|
|
||||||
has_course_access,
|
|
||||||
has_course_access_by_page_request,
|
|
||||||
is_circle_expert,
|
|
||||||
is_course_session_expert,
|
|
||||||
)
|
|
||||||
from vbv_lernwelt.course.serializers import (
|
from vbv_lernwelt.course.serializers import (
|
||||||
CourseCompletionSerializer,
|
CourseCompletionSerializer,
|
||||||
CourseSessionSerializer,
|
CourseSessionSerializer,
|
||||||
|
|
@ -26,8 +14,16 @@ from vbv_lernwelt.course.serializers import (
|
||||||
DocumentUploadStartInputSerializer,
|
DocumentUploadStartInputSerializer,
|
||||||
)
|
)
|
||||||
from vbv_lernwelt.course.services import mark_course_completion
|
from vbv_lernwelt.course.services import mark_course_completion
|
||||||
|
from vbv_lernwelt.course_session_group.models import CourseSessionGroup
|
||||||
from vbv_lernwelt.files.models import UploadFile
|
from vbv_lernwelt.files.models import UploadFile
|
||||||
from vbv_lernwelt.files.services import FileDirectUploadService
|
from vbv_lernwelt.files.services import FileDirectUploadService
|
||||||
|
from vbv_lernwelt.iam.permissions import (
|
||||||
|
course_sessions_for_user_qs,
|
||||||
|
has_course_access,
|
||||||
|
has_course_access_by_page_request,
|
||||||
|
is_circle_expert,
|
||||||
|
is_course_session_expert,
|
||||||
|
)
|
||||||
|
|
||||||
logger = structlog.get_logger(__name__)
|
logger = structlog.get_logger(__name__)
|
||||||
|
|
||||||
|
|
@ -134,11 +130,24 @@ def mark_course_completion_view(request):
|
||||||
@api_view(["GET"])
|
@api_view(["GET"])
|
||||||
def get_course_sessions(request):
|
def get_course_sessions(request):
|
||||||
try:
|
try:
|
||||||
course_sessions = course_sessions_for_user_qs(request.user).prefetch_related(
|
# participant/member/expert course sessions
|
||||||
"course"
|
regular_course_sessions = course_sessions_for_user_qs(
|
||||||
)
|
request.user
|
||||||
|
).prefetch_related("course")
|
||||||
|
|
||||||
|
# enrich with supervisor course sessions
|
||||||
|
supervisor_course_sessions = CourseSession.objects.filter(
|
||||||
|
id__in=CourseSessionGroup.objects.filter(
|
||||||
|
supervisor=request.user
|
||||||
|
).values_list("course_session", flat=True)
|
||||||
|
).prefetch_related("course")
|
||||||
|
|
||||||
|
all_to_serialize = (
|
||||||
|
regular_course_sessions | supervisor_course_sessions
|
||||||
|
).distinct()
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
status=200, data=CourseSessionSerializer(course_sessions, many=True).data
|
status=200, data=CourseSessionSerializer(all_to_serialize, many=True).data
|
||||||
)
|
)
|
||||||
except PermissionDenied as e:
|
except PermissionDenied as e:
|
||||||
raise e
|
raise e
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import graphene
|
||||||
import structlog
|
import structlog
|
||||||
from rest_framework.exceptions import PermissionDenied
|
from rest_framework.exceptions import PermissionDenied
|
||||||
|
|
||||||
from vbv_lernwelt.course.permissions import has_course_access
|
|
||||||
from vbv_lernwelt.course_session.graphql.types import (
|
from vbv_lernwelt.course_session.graphql.types import (
|
||||||
CourseSessionAttendanceCourseObjectType,
|
CourseSessionAttendanceCourseObjectType,
|
||||||
)
|
)
|
||||||
|
|
@ -11,6 +10,7 @@ from vbv_lernwelt.course_session.services.attendance import (
|
||||||
AttendanceUserStatus,
|
AttendanceUserStatus,
|
||||||
update_attendance_list,
|
update_attendance_list,
|
||||||
)
|
)
|
||||||
|
from vbv_lernwelt.iam.permissions import has_course_access
|
||||||
|
|
||||||
logger = structlog.get_logger(__name__)
|
logger = structlog.get_logger(__name__)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,11 @@ import graphene
|
||||||
from rest_framework.exceptions import PermissionDenied
|
from rest_framework.exceptions import PermissionDenied
|
||||||
|
|
||||||
from vbv_lernwelt.course.models import CourseSession
|
from vbv_lernwelt.course.models import CourseSession
|
||||||
from vbv_lernwelt.course.permissions import has_course_access, is_course_session_expert
|
|
||||||
from vbv_lernwelt.course_session.graphql.types import (
|
from vbv_lernwelt.course_session.graphql.types import (
|
||||||
CourseSessionAttendanceCourseObjectType,
|
CourseSessionAttendanceCourseObjectType,
|
||||||
)
|
)
|
||||||
from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse
|
from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse
|
||||||
|
from vbv_lernwelt.iam.permissions import has_course_access, is_course_session_expert
|
||||||
|
|
||||||
|
|
||||||
class CourseSessionQuery(object):
|
class CourseSessionQuery(object):
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import graphene
|
import graphene
|
||||||
from graphene_django import DjangoObjectType
|
from graphene_django import DjangoObjectType
|
||||||
|
|
||||||
from vbv_lernwelt.course.permissions import is_course_session_expert
|
|
||||||
from vbv_lernwelt.course_session.models import (
|
from vbv_lernwelt.course_session.models import (
|
||||||
CourseSessionAssignment,
|
CourseSessionAssignment,
|
||||||
CourseSessionAttendanceCourse,
|
CourseSessionAttendanceCourse,
|
||||||
|
|
@ -9,6 +8,7 @@ from vbv_lernwelt.course_session.models import (
|
||||||
)
|
)
|
||||||
from vbv_lernwelt.course_session.services.attendance import AttendanceUserStatus
|
from vbv_lernwelt.course_session.services.attendance import AttendanceUserStatus
|
||||||
from vbv_lernwelt.duedate.graphql.types import DueDateObjectType
|
from vbv_lernwelt.duedate.graphql.types import DueDateObjectType
|
||||||
|
from vbv_lernwelt.iam.permissions import is_course_session_expert
|
||||||
from vbv_lernwelt.learnpath.graphql.types import (
|
from vbv_lernwelt.learnpath.graphql.types import (
|
||||||
LearningContentAssignmentObjectType,
|
LearningContentAssignmentObjectType,
|
||||||
LearningContentAttendanceCourseObjectType,
|
LearningContentAttendanceCourseObjectType,
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@ from rest_framework.exceptions import PermissionDenied
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from vbv_lernwelt.course.models import CircleDocument
|
from vbv_lernwelt.course.models import CircleDocument
|
||||||
from vbv_lernwelt.course.permissions import has_course_session_access
|
|
||||||
from vbv_lernwelt.course.serializers import CircleDocumentSerializer
|
from vbv_lernwelt.course.serializers import CircleDocumentSerializer
|
||||||
|
from vbv_lernwelt.iam.permissions import has_course_session_access
|
||||||
|
|
||||||
|
|
||||||
@api_view(["GET"])
|
@api_view(["GET"])
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from vbv_lernwelt.course_session_group.models import CourseSessionGroup
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(CourseSessionGroup)
|
||||||
|
class CourseSessionAssignmentAdmin(admin.ModelAdmin):
|
||||||
|
...
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class CourseSessionGroupConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "vbv_lernwelt.course_session_group"
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
import vbv_lernwelt.course_session_group.signals # noqa F401
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
# Generated by Django 3.2.20 on 2023-10-23 14:53
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("course", "0004_auto_20230823_1744"),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="CourseSessionGroup",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("name", models.CharField(max_length=255)),
|
||||||
|
(
|
||||||
|
"course",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE, to="course.course"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"course_session",
|
||||||
|
models.ManyToManyField(blank=True, to="course.CourseSession"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"supervisor",
|
||||||
|
models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"ordering": ["name"],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from vbv_lernwelt.core.models import User
|
||||||
|
|
||||||
|
|
||||||
|
class CourseSessionGroup(models.Model):
|
||||||
|
name = models.CharField(max_length=255)
|
||||||
|
|
||||||
|
course = models.ForeignKey("course.Course", on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
course_session = models.ManyToManyField(
|
||||||
|
"course.CourseSession",
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
supervisor = models.ManyToManyField(
|
||||||
|
User,
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ["name"]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.db.models.signals import m2m_changed
|
||||||
|
from django.dispatch import receiver
|
||||||
|
|
||||||
|
from .models import CourseSessionGroup
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(m2m_changed, sender=CourseSessionGroup.course_session.through)
|
||||||
|
def validate_course(sender, instance, action, reverse, model, pk_set, **kwargs):
|
||||||
|
if action == "pre_add":
|
||||||
|
course_sessions = model.objects.filter(pk__in=pk_set)
|
||||||
|
for session in course_sessions:
|
||||||
|
if session.course != instance.course:
|
||||||
|
raise ValidationError(
|
||||||
|
"CourseSession does not match the Course of this Group."
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
|
|
@ -0,0 +1,221 @@
|
||||||
|
from typing import Dict, List, Set, Tuple
|
||||||
|
|
||||||
|
import graphene
|
||||||
|
|
||||||
|
from vbv_lernwelt.assignment.models import (
|
||||||
|
AssignmentCompletion,
|
||||||
|
AssignmentCompletionStatus,
|
||||||
|
)
|
||||||
|
from vbv_lernwelt.core.admin import User
|
||||||
|
from vbv_lernwelt.course.models import Course, CourseSession, CourseSessionUser
|
||||||
|
from vbv_lernwelt.course_session_group.models import CourseSessionGroup
|
||||||
|
from vbv_lernwelt.dashboard.graphql.types.competence import competences
|
||||||
|
from vbv_lernwelt.dashboard.graphql.types.dashboard import (
|
||||||
|
CourseProgressType,
|
||||||
|
CourseStatisticsType,
|
||||||
|
DashboardConfigType,
|
||||||
|
DashboardType,
|
||||||
|
ProgressDashboardAssignmentType,
|
||||||
|
ProgressDashboardCompetenceType,
|
||||||
|
)
|
||||||
|
from vbv_lernwelt.iam.permissions import (
|
||||||
|
can_view_course_session,
|
||||||
|
can_view_course_session_group_statistics,
|
||||||
|
can_view_course_session_progress,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DashboardQuery(graphene.ObjectType):
|
||||||
|
course_statistics = graphene.Field(
|
||||||
|
CourseStatisticsType, course_id=graphene.ID(required=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
course_progress = graphene.Field(
|
||||||
|
CourseProgressType, course_id=graphene.ID(required=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
dashboard_config = graphene.List(
|
||||||
|
graphene.NonNull(DashboardConfigType), required=True
|
||||||
|
)
|
||||||
|
|
||||||
|
def resolve_course_statistics(root, info, course_id: str): # noqa
|
||||||
|
user = info.context.user
|
||||||
|
course = Course.objects.get(id=course_id)
|
||||||
|
|
||||||
|
course_session_ids = set()
|
||||||
|
|
||||||
|
for group in CourseSessionGroup.objects.filter(course=course):
|
||||||
|
if can_view_course_session_group_statistics(user=user, group=group):
|
||||||
|
course_session_ids.update(
|
||||||
|
group.course_session.all().values_list("id", flat=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not course_session_ids:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return CourseStatisticsType(
|
||||||
|
_id=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
|
||||||
|
)
|
||||||
|
|
||||||
|
def resolve_dashboard_config(root, info): # noqa
|
||||||
|
user = info.context.user
|
||||||
|
|
||||||
|
if user.is_superuser:
|
||||||
|
courses = Course.objects.all().values("id", "title", "slug")
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"id": c["id"],
|
||||||
|
"course_id": c["id"],
|
||||||
|
"name": c["title"],
|
||||||
|
"slug": c["slug"],
|
||||||
|
"dashboard_type": DashboardType.SIMPLE_DASHBOARD,
|
||||||
|
}
|
||||||
|
for c in courses
|
||||||
|
]
|
||||||
|
|
||||||
|
(
|
||||||
|
statistic_dashboards,
|
||||||
|
statistics_dashboard_course_ids,
|
||||||
|
) = get_user_statistics_dashboards(user=user)
|
||||||
|
|
||||||
|
course_session_dashboards = get_user_course_session_dashboards(
|
||||||
|
user=user, exclude_course_ids=statistics_dashboard_course_ids
|
||||||
|
)
|
||||||
|
|
||||||
|
return statistic_dashboards + course_session_dashboards
|
||||||
|
|
||||||
|
def resolve_course_progress(root, info, course_id: str): # noqa
|
||||||
|
"""
|
||||||
|
Slightly fragile but could be good enough: most only have one
|
||||||
|
course session per course anyway but if there are multiple, we
|
||||||
|
just pick the newest one (by generation) as best guess.
|
||||||
|
"""
|
||||||
|
|
||||||
|
user = info.context.user
|
||||||
|
course = Course.objects.get(id=course_id)
|
||||||
|
|
||||||
|
newest: CourseSession | None = None
|
||||||
|
course_session_for_user: List[str] = []
|
||||||
|
|
||||||
|
# generation
|
||||||
|
for course_session in CourseSession.objects.filter(course_id=course_id):
|
||||||
|
if can_view_course_session_progress(
|
||||||
|
user=user, course_session=course_session
|
||||||
|
):
|
||||||
|
course_session_for_user.append(course_session)
|
||||||
|
generation_newest = newest.generation if newest else None
|
||||||
|
if (
|
||||||
|
generation_newest is None
|
||||||
|
or course_session.generation > generation_newest
|
||||||
|
):
|
||||||
|
newest = course_session
|
||||||
|
|
||||||
|
# competence
|
||||||
|
_, success_total, fail_total = competences(
|
||||||
|
course_slug=str(course.slug),
|
||||||
|
course_session_selection_ids=course_session_for_user,
|
||||||
|
)
|
||||||
|
|
||||||
|
# assignment
|
||||||
|
evaluation_results = AssignmentCompletion.objects.filter(
|
||||||
|
completion_status=AssignmentCompletionStatus.EVALUATION_SUBMITTED.value,
|
||||||
|
assignment_user=user,
|
||||||
|
course_session__course=course,
|
||||||
|
).values("evaluation_max_points", "evaluation_points")
|
||||||
|
|
||||||
|
evaluation_results = list(evaluation_results)
|
||||||
|
points_max_count = sum(
|
||||||
|
[result.get("evaluation_max_points", 0) for result in evaluation_results]
|
||||||
|
)
|
||||||
|
points_achieved_count = sum(
|
||||||
|
[result.get("evaluation_points", 0) for result in evaluation_results]
|
||||||
|
)
|
||||||
|
|
||||||
|
return CourseProgressType(
|
||||||
|
_id=course_id, # noqa
|
||||||
|
course_id=course_id, # noqa
|
||||||
|
session_to_continue_id=newest.id if newest else None, # noqa
|
||||||
|
competence=ProgressDashboardCompetenceType( # noqa
|
||||||
|
_id=course_id, # noqa
|
||||||
|
total_count=success_total + fail_total, # noqa
|
||||||
|
success_count=success_total, # noqa
|
||||||
|
fail_count=fail_total, # noqa
|
||||||
|
),
|
||||||
|
assignment=ProgressDashboardAssignmentType( # noqa
|
||||||
|
_id=course_id, # noqa
|
||||||
|
total_count=len(evaluation_results), # noqa
|
||||||
|
points_max_count=points_max_count, # noqa
|
||||||
|
points_achieved_count=points_achieved_count, # noqa
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_statistics_dashboards(user: User) -> Tuple[List[Dict[str, str]], Set[int]]:
|
||||||
|
course_index = set()
|
||||||
|
dashboards = []
|
||||||
|
|
||||||
|
for group in CourseSessionGroup.objects.all():
|
||||||
|
if can_view_course_session_group_statistics(user=user, group=group):
|
||||||
|
course = group.course
|
||||||
|
course_index.add(course)
|
||||||
|
dashboards.append(
|
||||||
|
{
|
||||||
|
"id": str(course.id),
|
||||||
|
"name": course.title,
|
||||||
|
"slug": course.slug,
|
||||||
|
"dashboard_type": DashboardType.STATISTICS_DASHBOARD,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return dashboards, course_index
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_course_session_dashboards(
|
||||||
|
user: User, exclude_course_ids: Set[int]
|
||||||
|
) -> List[Dict[str, str]]:
|
||||||
|
"""
|
||||||
|
Edge case: what do we show to users with access to multiple
|
||||||
|
sessions of a course, but with varying permissions?
|
||||||
|
-> We just show the simple list dashboard for now.
|
||||||
|
"""
|
||||||
|
|
||||||
|
dashboards = []
|
||||||
|
|
||||||
|
course_sessions = CourseSession.objects.exclude(course__in=exclude_course_ids)
|
||||||
|
roles_by_course: Dict[Course, Set[DashboardType]] = {}
|
||||||
|
|
||||||
|
for course_session in course_sessions:
|
||||||
|
if can_view_course_session(user=user, course_session=course_session):
|
||||||
|
role = CourseSessionUser.objects.get(
|
||||||
|
course_session=course_session, user=user
|
||||||
|
).role
|
||||||
|
roles_by_course.setdefault(course_session.course, set())
|
||||||
|
roles_by_course[course_session.course].add(role)
|
||||||
|
|
||||||
|
for course, roles in roles_by_course.items():
|
||||||
|
resolved_dashboard_type = None
|
||||||
|
|
||||||
|
if len(roles) == 1:
|
||||||
|
course_role = roles.pop()
|
||||||
|
if course_role == CourseSessionUser.Role.EXPERT:
|
||||||
|
resolved_dashboard_type = DashboardType.SIMPLE_DASHBOARD
|
||||||
|
elif course_role == CourseSessionUser.Role.MEMBER:
|
||||||
|
resolved_dashboard_type = DashboardType.PROGRESS_DASHBOARD
|
||||||
|
else:
|
||||||
|
# fallback: just go with simple list dashboard
|
||||||
|
resolved_dashboard_type = DashboardType.SIMPLE_DASHBOARD
|
||||||
|
|
||||||
|
dashboards.append(
|
||||||
|
{
|
||||||
|
"id": str(course.id),
|
||||||
|
"name": course.title,
|
||||||
|
"slug": course.slug,
|
||||||
|
"dashboard_type": resolved_dashboard_type,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return dashboards
|
||||||
|
|
@ -0,0 +1,172 @@
|
||||||
|
import math
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
import graphene
|
||||||
|
|
||||||
|
import vbv_lernwelt.assignment.models
|
||||||
|
from vbv_lernwelt.assignment.models import (
|
||||||
|
AssignmentCompletion,
|
||||||
|
AssignmentCompletionStatus,
|
||||||
|
AssignmentType,
|
||||||
|
)
|
||||||
|
from vbv_lernwelt.course.models import CourseSession, CourseSessionUser
|
||||||
|
from vbv_lernwelt.course_session.models import (
|
||||||
|
CourseSessionAssignment,
|
||||||
|
CourseSessionEdoniqTest,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AssignmentCompletionMetricsType(graphene.ObjectType):
|
||||||
|
_id = graphene.ID(required=True)
|
||||||
|
passed_count = graphene.Int(required=True)
|
||||||
|
failed_count = graphene.Int(required=True)
|
||||||
|
unranked_count = graphene.Int(required=True)
|
||||||
|
ranking_completed = graphene.Boolean(required=True)
|
||||||
|
average_passed = graphene.Float(required=True)
|
||||||
|
|
||||||
|
|
||||||
|
class AssignmentStatisticsRecordType(graphene.ObjectType):
|
||||||
|
_id = graphene.ID(required=True)
|
||||||
|
course_session_id = graphene.ID(required=True)
|
||||||
|
course_session_assignment_id = graphene.ID(required=True)
|
||||||
|
circle_id = graphene.ID(required=True)
|
||||||
|
generation = graphene.String(required=True)
|
||||||
|
assignment_type_translation_key = graphene.String(required=True)
|
||||||
|
assignment_title = graphene.String(required=True)
|
||||||
|
deadline = graphene.DateTime(required=True)
|
||||||
|
metrics = graphene.Field(AssignmentCompletionMetricsType, required=True)
|
||||||
|
details_url = graphene.String(required=True)
|
||||||
|
|
||||||
|
|
||||||
|
class AssignmentStatisticsSummaryType(graphene.ObjectType):
|
||||||
|
_id = graphene.ID(required=True)
|
||||||
|
completed_count = graphene.Int(required=True)
|
||||||
|
average_passed = graphene.Float(required=True)
|
||||||
|
|
||||||
|
|
||||||
|
class AssignmentsStatisticsType(graphene.ObjectType):
|
||||||
|
_id = graphene.ID(required=True)
|
||||||
|
records = graphene.List(
|
||||||
|
graphene.NonNull(AssignmentStatisticsRecordType), required=True
|
||||||
|
)
|
||||||
|
summary = graphene.Field(AssignmentStatisticsSummaryType, required=True)
|
||||||
|
|
||||||
|
|
||||||
|
def create_assignment_summary(course_id, metrics) -> AssignmentStatisticsSummaryType:
|
||||||
|
completed_metrics = [m for m in metrics if m.ranking_completed]
|
||||||
|
|
||||||
|
if not completed_metrics:
|
||||||
|
return AssignmentStatisticsSummaryType(
|
||||||
|
_id=course_id, completed_count=0, average_passed=0 # noqa
|
||||||
|
)
|
||||||
|
|
||||||
|
completed_count = len(completed_metrics)
|
||||||
|
|
||||||
|
average_passed_completed = (
|
||||||
|
sum([m.average_passed for m in completed_metrics]) / completed_count
|
||||||
|
)
|
||||||
|
|
||||||
|
return AssignmentStatisticsSummaryType(
|
||||||
|
_id=course_id, # noqa
|
||||||
|
completed_count=completed_count, # noqa
|
||||||
|
average_passed=average_passed_completed, # noqa
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_assignment_completion_metrics(
|
||||||
|
course_session: CourseSession, assignment: vbv_lernwelt.assignment.models.Assignment
|
||||||
|
) -> AssignmentCompletionMetricsType:
|
||||||
|
course_session_users = CourseSessionUser.objects.filter(
|
||||||
|
course_session=course_session,
|
||||||
|
role=CourseSessionUser.Role.MEMBER,
|
||||||
|
).values_list("user", flat=True)
|
||||||
|
|
||||||
|
evaluation_results = 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)
|
||||||
|
|
||||||
|
passed_count = len([passed for passed in evaluation_results if passed])
|
||||||
|
failed_count = len(evaluation_results) - passed_count
|
||||||
|
|
||||||
|
participants_count = len(course_session_users)
|
||||||
|
unranked_count = participants_count - passed_count - failed_count
|
||||||
|
|
||||||
|
if participants_count == 0:
|
||||||
|
average_passed = 0
|
||||||
|
else:
|
||||||
|
average_passed = math.ceil(passed_count / participants_count * 100)
|
||||||
|
|
||||||
|
return AssignmentCompletionMetricsType(
|
||||||
|
_id=assignment.id, # noqa
|
||||||
|
passed_count=passed_count, # noqa
|
||||||
|
failed_count=failed_count, # noqa
|
||||||
|
unranked_count=unranked_count, # noqa
|
||||||
|
ranking_completed=unranked_count == 0, # noqa
|
||||||
|
average_passed=average_passed, # noqa
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_record(
|
||||||
|
course_session_assignment: CourseSessionAssignment | CourseSessionEdoniqTest,
|
||||||
|
) -> AssignmentStatisticsRecordType:
|
||||||
|
if isinstance(course_session_assignment, CourseSessionAssignment):
|
||||||
|
due_date = course_session_assignment.submission_deadline
|
||||||
|
else:
|
||||||
|
due_date = course_session_assignment.deadline
|
||||||
|
|
||||||
|
learning_content = course_session_assignment.learning_content
|
||||||
|
|
||||||
|
return AssignmentStatisticsRecordType(
|
||||||
|
# make sure it's unique, across all types of assignments!
|
||||||
|
_id=f"{course_session_assignment._meta.model_name}#{course_session_assignment.id}", # noqa
|
||||||
|
course_session_id=str(course_session_assignment.course_session.id), # noqa
|
||||||
|
circle_id=learning_content.get_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
|
||||||
|
assignment_title=learning_content.content_assignment.title, # noqa
|
||||||
|
metrics=get_assignment_completion_metrics( # noqa
|
||||||
|
course_session=course_session_assignment.course_session, # noqa
|
||||||
|
assignment=learning_content.content_assignment, # noqa
|
||||||
|
),
|
||||||
|
details_url=due_date.url_expert, # noqa
|
||||||
|
deadline=due_date.start, # noqa
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def assignments(
|
||||||
|
course_id: graphene.ID(required=True),
|
||||||
|
course_session_selection_ids: graphene.List(graphene.ID),
|
||||||
|
) -> AssignmentsStatisticsType:
|
||||||
|
course_sessions = CourseSession.objects.filter(
|
||||||
|
id__in=course_session_selection_ids,
|
||||||
|
)
|
||||||
|
records: List[AssignmentStatisticsRecordType] = []
|
||||||
|
|
||||||
|
for course_session in course_sessions:
|
||||||
|
for csa in CourseSessionAssignment.objects.filter(
|
||||||
|
course_session=course_session,
|
||||||
|
learning_content__assignment_type__in=[
|
||||||
|
AssignmentType.CASEWORK.value,
|
||||||
|
AssignmentType.PREP_ASSIGNMENT.value,
|
||||||
|
],
|
||||||
|
):
|
||||||
|
record = create_record(course_session_assignment=csa)
|
||||||
|
records.append(record)
|
||||||
|
|
||||||
|
for cset in CourseSessionEdoniqTest.objects.filter(
|
||||||
|
course_session=course_session
|
||||||
|
):
|
||||||
|
record = create_record(course_session_assignment=cset)
|
||||||
|
records.append(record)
|
||||||
|
|
||||||
|
return AssignmentsStatisticsType(
|
||||||
|
_id=course_id, # noqa
|
||||||
|
records=sorted(records, key=lambda r: r.deadline), # noqa
|
||||||
|
summary=create_assignment_summary( # noqa
|
||||||
|
course_id=course_id, metrics=[r.metrics for r in records] # noqa
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
import math
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
import graphene
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from vbv_lernwelt.course.models import CourseSessionUser
|
||||||
|
from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse
|
||||||
|
from vbv_lernwelt.course_session.services.attendance import AttendanceUserStatus
|
||||||
|
|
||||||
|
|
||||||
|
class AttendanceSummaryStatisticsType(graphene.ObjectType):
|
||||||
|
_id = graphene.ID(required=True)
|
||||||
|
days_completed = graphene.Int(required=True)
|
||||||
|
participants_present = graphene.Int(required=True)
|
||||||
|
|
||||||
|
|
||||||
|
class PresenceRecordStatisticsType(graphene.ObjectType):
|
||||||
|
_id = graphene.ID(required=True)
|
||||||
|
course_session_id = graphene.ID(required=True)
|
||||||
|
generation = graphene.String(required=True)
|
||||||
|
circle_id = graphene.ID(required=True)
|
||||||
|
due_date = graphene.DateTime(required=True)
|
||||||
|
participants_present = graphene.Int(required=True)
|
||||||
|
participants_total = graphene.Int(required=True)
|
||||||
|
details_url = graphene.String(required=True)
|
||||||
|
|
||||||
|
|
||||||
|
class AttendanceDayPresencesStatisticsType(graphene.ObjectType):
|
||||||
|
_id = graphene.ID(required=True)
|
||||||
|
records = graphene.List(
|
||||||
|
graphene.NonNull(PresenceRecordStatisticsType), required=True
|
||||||
|
)
|
||||||
|
summary = graphene.Field(AttendanceSummaryStatisticsType, required=True)
|
||||||
|
|
||||||
|
|
||||||
|
def attendance_day_presences(
|
||||||
|
course_id: graphene.ID,
|
||||||
|
course_session_selection_ids: graphene.List(graphene.ID),
|
||||||
|
) -> AttendanceDayPresencesStatisticsType:
|
||||||
|
completed = CourseSessionAttendanceCourse.objects.filter(
|
||||||
|
course_session_id__in=course_session_selection_ids,
|
||||||
|
due_date__end__lt=timezone.now(),
|
||||||
|
).order_by("-due_date__end")
|
||||||
|
|
||||||
|
records = []
|
||||||
|
|
||||||
|
for attendance_day in completed:
|
||||||
|
circle = attendance_day.learning_content.get_parent_circle()
|
||||||
|
|
||||||
|
course_session = attendance_day.course_session
|
||||||
|
|
||||||
|
url = f"/course/{course_session.course.slug}/cockpit/attendance?id={attendance_day.learning_content.id}&courseSessionId={course_session.id}"
|
||||||
|
|
||||||
|
participant_user_ids = [
|
||||||
|
str(user_id)
|
||||||
|
for user_id in CourseSessionUser.objects.filter(
|
||||||
|
course_session=course_session, role=CourseSessionUser.Role.MEMBER
|
||||||
|
).values_list("user_id", flat=True)
|
||||||
|
]
|
||||||
|
participants_total = len(participant_user_ids)
|
||||||
|
participants_present = len(
|
||||||
|
[
|
||||||
|
participant
|
||||||
|
for participant in attendance_day.attendance_user_list
|
||||||
|
if participant["status"] == AttendanceUserStatus.PRESENT.value
|
||||||
|
# in the `attendance_user_list` are users present, which are not
|
||||||
|
# (anymore) in the course session -> so we need to filter them out
|
||||||
|
and participant["user_id"] in participant_user_ids
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
records.append(
|
||||||
|
PresenceRecordStatisticsType(
|
||||||
|
_id=f"attendance_day:{attendance_day.id}", # noqa
|
||||||
|
course_session_id=course_session.id, # noqa
|
||||||
|
generation=course_session.generation, # noqa
|
||||||
|
circle_id=circle.id, # noqa
|
||||||
|
due_date=attendance_day.due_date.end, # noqa
|
||||||
|
participants_present=participants_present, # noqa
|
||||||
|
participants_total=participants_total, # noqa
|
||||||
|
details_url=url, # noqa
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
summary = AttendanceSummaryStatisticsType(
|
||||||
|
_id=course_id, # noqa
|
||||||
|
days_completed=completed.count(), # noqa
|
||||||
|
participants_present=calculate_avg_participation(records), # noqa
|
||||||
|
)
|
||||||
|
|
||||||
|
return AttendanceDayPresencesStatisticsType(
|
||||||
|
summary=summary, records=records, _id=course_id # noqa
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_avg_participation(records: List[PresenceRecordStatisticsType]) -> float:
|
||||||
|
if not records:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
total_ratio = 0.0
|
||||||
|
for record in records:
|
||||||
|
if record.participants_total == 0:
|
||||||
|
continue
|
||||||
|
total_ratio += float(record.participants_present) / float(
|
||||||
|
record.participants_total
|
||||||
|
)
|
||||||
|
|
||||||
|
return math.ceil(total_ratio / len(records) * 100)
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
from typing import List, Tuple
|
||||||
|
|
||||||
|
import graphene
|
||||||
|
from wagtail.models import Page
|
||||||
|
|
||||||
|
from vbv_lernwelt.course.models import CourseCompletion, CourseCompletionStatus
|
||||||
|
|
||||||
|
|
||||||
|
class CompetencePerformanceStatisticsSummaryType(graphene.ObjectType):
|
||||||
|
_id = graphene.ID(required=True)
|
||||||
|
success_total = graphene.Int(required=True)
|
||||||
|
fail_total = graphene.Int(required=True)
|
||||||
|
|
||||||
|
|
||||||
|
class CompetenceRecordStatisticsType(graphene.ObjectType):
|
||||||
|
_id = graphene.ID(required=True)
|
||||||
|
course_session_id = graphene.ID(required=True)
|
||||||
|
generation = graphene.String(required=True)
|
||||||
|
title = graphene.String(required=True)
|
||||||
|
circle_id = graphene.ID(required=True)
|
||||||
|
success_count = graphene.Int(required=True)
|
||||||
|
fail_count = graphene.Int(required=True)
|
||||||
|
details_url = graphene.String(required=True)
|
||||||
|
|
||||||
|
|
||||||
|
class CompetencesStatisticsType(graphene.ObjectType):
|
||||||
|
_id = graphene.ID(required=True)
|
||||||
|
summary = graphene.Field(CompetencePerformanceStatisticsSummaryType, required=True)
|
||||||
|
records = graphene.List(
|
||||||
|
graphene.NonNull(CompetenceRecordStatisticsType), required=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def competences(
|
||||||
|
course_session_selection_ids: List[str],
|
||||||
|
course_slug: str,
|
||||||
|
user_selection_ids: List[str] | None = None,
|
||||||
|
) -> Tuple[List[CompetenceRecordStatisticsType], int, int]:
|
||||||
|
completions = CourseCompletion.objects.filter(
|
||||||
|
course_session_id__in=course_session_selection_ids,
|
||||||
|
page_type="competence.PerformanceCriteria",
|
||||||
|
).prefetch_related("course_session", "page")
|
||||||
|
|
||||||
|
if user_selection_ids is not None:
|
||||||
|
completions = completions.filter(user_id__in=user_selection_ids)
|
||||||
|
|
||||||
|
competence_records = {}
|
||||||
|
|
||||||
|
unique_page_ids = {completion.page.id for completion in completions}
|
||||||
|
learning_units = {
|
||||||
|
page_id: Page.objects.get(id=page_id).specific.learning_unit
|
||||||
|
for page_id in unique_page_ids
|
||||||
|
}
|
||||||
|
circles = {lu.id: lu.get_circle() for lu in learning_units.values()}
|
||||||
|
|
||||||
|
for completion in completions:
|
||||||
|
learning_unit = learning_units.get(completion.page.id)
|
||||||
|
circle = circles.get(learning_unit.id)
|
||||||
|
|
||||||
|
combined_id = f"{circle.id}-{completion.course_session.id}"
|
||||||
|
|
||||||
|
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
|
||||||
|
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
|
||||||
|
elif completion.completion_status == CourseCompletionStatus.FAIL.value:
|
||||||
|
competence_records[combined_id][learning_unit].fail_count += 1
|
||||||
|
|
||||||
|
values = [
|
||||||
|
record
|
||||||
|
for circle_records in competence_records.values()
|
||||||
|
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])
|
||||||
|
|
||||||
|
return values, success_count, fail_count
|
||||||
|
|
@ -0,0 +1,209 @@
|
||||||
|
import graphene
|
||||||
|
from graphene import Enum
|
||||||
|
|
||||||
|
from vbv_lernwelt.course.models import CourseSession, CourseSessionUser
|
||||||
|
from vbv_lernwelt.dashboard.graphql.types.assignment import (
|
||||||
|
assignments,
|
||||||
|
AssignmentsStatisticsType,
|
||||||
|
)
|
||||||
|
from vbv_lernwelt.dashboard.graphql.types.attendance import (
|
||||||
|
attendance_day_presences,
|
||||||
|
AttendanceDayPresencesStatisticsType,
|
||||||
|
)
|
||||||
|
from vbv_lernwelt.dashboard.graphql.types.competence import (
|
||||||
|
CompetencePerformanceStatisticsSummaryType,
|
||||||
|
competences,
|
||||||
|
CompetencesStatisticsType,
|
||||||
|
)
|
||||||
|
from vbv_lernwelt.dashboard.graphql.types.feedback import (
|
||||||
|
feedback_responses,
|
||||||
|
FeedbackStatisticsResponsesType,
|
||||||
|
)
|
||||||
|
from vbv_lernwelt.learnpath.models import Circle
|
||||||
|
|
||||||
|
|
||||||
|
class StatisticsCourseSessionDataType(graphene.ObjectType):
|
||||||
|
id = graphene.ID(required=True)
|
||||||
|
name = graphene.String(required=True)
|
||||||
|
|
||||||
|
|
||||||
|
class StatisticsCircleDataType(graphene.ObjectType):
|
||||||
|
id = graphene.ID(required=True)
|
||||||
|
name = graphene.String(required=True)
|
||||||
|
|
||||||
|
|
||||||
|
class StatisticsCourseSessionsSelectionMetricType(graphene.ObjectType):
|
||||||
|
_id = graphene.ID(required=True)
|
||||||
|
session_count = graphene.Int(required=True)
|
||||||
|
participant_count = graphene.Int(required=True)
|
||||||
|
expert_count = graphene.Int(required=True)
|
||||||
|
|
||||||
|
|
||||||
|
class StatisticsCourseSessionPropertiesType(graphene.ObjectType):
|
||||||
|
_id = graphene.ID(required=True)
|
||||||
|
sessions = graphene.List(
|
||||||
|
graphene.NonNull(StatisticsCourseSessionDataType), required=True
|
||||||
|
)
|
||||||
|
generations = graphene.List(graphene.NonNull(graphene.String), required=True)
|
||||||
|
circles = graphene.List(graphene.NonNull(StatisticsCircleDataType), required=True)
|
||||||
|
|
||||||
|
|
||||||
|
class DashboardType(Enum):
|
||||||
|
STATISTICS_DASHBOARD = "StatisticsDashboard"
|
||||||
|
PROGRESS_DASHBOARD = "ProgressDashboard"
|
||||||
|
SIMPLE_DASHBOARD = "SimpleDashboard"
|
||||||
|
|
||||||
|
|
||||||
|
class DashboardConfigType(graphene.ObjectType):
|
||||||
|
id = graphene.ID(required=True)
|
||||||
|
name = graphene.String(required=True)
|
||||||
|
slug = graphene.String(required=True)
|
||||||
|
dashboard_type = graphene.Field(DashboardType, required=True)
|
||||||
|
|
||||||
|
|
||||||
|
class ProgressDashboardCompetenceType(graphene.ObjectType):
|
||||||
|
_id = graphene.ID(required=True)
|
||||||
|
total_count = graphene.Int(required=True)
|
||||||
|
success_count = graphene.Int(required=True)
|
||||||
|
fail_count = graphene.Int(required=True)
|
||||||
|
|
||||||
|
|
||||||
|
class ProgressDashboardAssignmentType(graphene.ObjectType):
|
||||||
|
_id = graphene.ID(required=True)
|
||||||
|
total_count = graphene.Int(required=True)
|
||||||
|
points_max_count = graphene.Int(required=True)
|
||||||
|
points_achieved_count = graphene.Int(required=True)
|
||||||
|
|
||||||
|
|
||||||
|
class CourseProgressType(graphene.ObjectType):
|
||||||
|
_id = graphene.ID(required=True)
|
||||||
|
course_id = graphene.ID(required=True)
|
||||||
|
session_to_continue_id = graphene.ID(required=False)
|
||||||
|
competence = graphene.Field(ProgressDashboardCompetenceType, required=True)
|
||||||
|
assignment = graphene.Field(ProgressDashboardAssignmentType, required=True)
|
||||||
|
|
||||||
|
|
||||||
|
class CourseStatisticsType(graphene.ObjectType):
|
||||||
|
_id = graphene.ID(required=True)
|
||||||
|
course_id = graphene.ID(required=True)
|
||||||
|
course_title = graphene.String(required=True)
|
||||||
|
course_slug = graphene.String(required=True)
|
||||||
|
course_session_properties = graphene.Field(
|
||||||
|
StatisticsCourseSessionPropertiesType, required=True
|
||||||
|
)
|
||||||
|
course_session_selection_ids = graphene.List(graphene.ID, required=True)
|
||||||
|
course_session_selection_metrics = graphene.Field(
|
||||||
|
StatisticsCourseSessionsSelectionMetricType, required=True
|
||||||
|
)
|
||||||
|
attendance_day_presences = graphene.Field(
|
||||||
|
AttendanceDayPresencesStatisticsType, required=True
|
||||||
|
)
|
||||||
|
feedback_responses = graphene.Field(FeedbackStatisticsResponsesType, required=True)
|
||||||
|
assignments = graphene.Field(AssignmentsStatisticsType, required=True)
|
||||||
|
competences = graphene.Field(CompetencesStatisticsType, required=True)
|
||||||
|
|
||||||
|
def resolve_attendance_day_presences(
|
||||||
|
root, info
|
||||||
|
) -> AttendanceDayPresencesStatisticsType:
|
||||||
|
return attendance_day_presences(
|
||||||
|
course_id=root.course_id,
|
||||||
|
course_session_selection_ids=root.course_session_selection_ids,
|
||||||
|
)
|
||||||
|
|
||||||
|
def resolve_feedback_responses(root, info) -> FeedbackStatisticsResponsesType:
|
||||||
|
return feedback_responses(
|
||||||
|
course_session_selection_ids=root.course_session_selection_ids,
|
||||||
|
course_id=root.course_id,
|
||||||
|
course_slug=root.course_slug,
|
||||||
|
)
|
||||||
|
|
||||||
|
def resolve_competences(root, info) -> CompetencesStatisticsType:
|
||||||
|
records, success_total, fail_total = competences(
|
||||||
|
course_slug=str(root.course_slug),
|
||||||
|
course_session_selection_ids=[
|
||||||
|
str(cs) for cs in root.course_session_selection_ids # noqa
|
||||||
|
],
|
||||||
|
)
|
||||||
|
return CompetencesStatisticsType(
|
||||||
|
_id=root._id, # noqa
|
||||||
|
records=records, # noqa
|
||||||
|
summary=CompetencePerformanceStatisticsSummaryType( # noqa
|
||||||
|
_id=root._id, # noqa
|
||||||
|
success_total=success_total, # noqa
|
||||||
|
fail_total=fail_total, # noqa
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def resolve_assignments(root, info) -> AssignmentsStatisticsType:
|
||||||
|
return assignments(
|
||||||
|
course_id=root.course_id,
|
||||||
|
course_session_selection_ids=root.course_session_selection_ids,
|
||||||
|
)
|
||||||
|
|
||||||
|
def resolve_course_session_selection_metrics(
|
||||||
|
root, info
|
||||||
|
) -> StatisticsCourseSessionsSelectionMetricType:
|
||||||
|
course_session_count = CourseSession.objects.filter(
|
||||||
|
id__in=root.course_session_selection_ids,
|
||||||
|
course_id=root.course_id,
|
||||||
|
).count()
|
||||||
|
|
||||||
|
expert_count = CourseSessionUser.objects.filter(
|
||||||
|
course_session_id__in=root.course_session_selection_ids,
|
||||||
|
role=CourseSessionUser.Role.EXPERT,
|
||||||
|
).count()
|
||||||
|
|
||||||
|
participant_count = CourseSessionUser.objects.filter(
|
||||||
|
course_session_id__in=root.course_session_selection_ids,
|
||||||
|
role=CourseSessionUser.Role.MEMBER,
|
||||||
|
).count()
|
||||||
|
|
||||||
|
return StatisticsCourseSessionsSelectionMetricType(
|
||||||
|
_id=root._id, # noqa
|
||||||
|
session_count=course_session_count, # noqa
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,129 @@
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
import graphene
|
||||||
|
|
||||||
|
from vbv_lernwelt.course.models import CourseSession, CourseSessionUser
|
||||||
|
from vbv_lernwelt.feedback.models import FeedbackResponse
|
||||||
|
from vbv_lernwelt.feedback.utils import feedback_users
|
||||||
|
|
||||||
|
|
||||||
|
class FeedbackStatisticsSummaryType(graphene.ObjectType):
|
||||||
|
_id = graphene.ID(required=True)
|
||||||
|
satisfaction_average = graphene.Float(required=True)
|
||||||
|
satisfaction_max = graphene.Int(required=True)
|
||||||
|
total_responses = graphene.Int(required=True)
|
||||||
|
|
||||||
|
|
||||||
|
class FeedbackStatisticsRecordType(graphene.ObjectType):
|
||||||
|
_id = graphene.ID(required=True)
|
||||||
|
course_session_id = graphene.ID(required=True)
|
||||||
|
generation = graphene.String(required=True)
|
||||||
|
circle_id = graphene.ID(required=True)
|
||||||
|
satisfaction_average = graphene.Float(required=True)
|
||||||
|
satisfaction_max = graphene.Int(required=True)
|
||||||
|
details_url = graphene.String(required=True)
|
||||||
|
experts = graphene.String(required=True)
|
||||||
|
|
||||||
|
|
||||||
|
class FeedbackStatisticsResponsesType(graphene.ObjectType):
|
||||||
|
_id = graphene.ID(required=True)
|
||||||
|
records = graphene.List(
|
||||||
|
graphene.NonNull(FeedbackStatisticsRecordType), required=True
|
||||||
|
)
|
||||||
|
summary = graphene.Field(FeedbackStatisticsSummaryType, required=True)
|
||||||
|
|
||||||
|
|
||||||
|
def feedback_responses(
|
||||||
|
course_session_selection_ids: graphene.List(graphene.ID),
|
||||||
|
course_id: graphene.ID,
|
||||||
|
course_slug: graphene.String,
|
||||||
|
) -> FeedbackStatisticsResponsesType:
|
||||||
|
# Get all course sessions for this user in the given course
|
||||||
|
course_sessions = CourseSession.objects.filter(
|
||||||
|
id__in=course_session_selection_ids,
|
||||||
|
)
|
||||||
|
|
||||||
|
circle_feedbacks = []
|
||||||
|
total_responses = 0
|
||||||
|
|
||||||
|
for course_session in course_sessions:
|
||||||
|
fbs = FeedbackResponse.objects.filter(
|
||||||
|
submitted=True,
|
||||||
|
course_session=course_session,
|
||||||
|
feedback_user__in=feedback_users(course_session.id),
|
||||||
|
)
|
||||||
|
|
||||||
|
total_responses += len(fbs)
|
||||||
|
|
||||||
|
circle_feedbacks.extend(
|
||||||
|
circle_feedback_average(
|
||||||
|
feedbacks=fbs,
|
||||||
|
course_session_id=course_session.id,
|
||||||
|
generation=course_session.generation,
|
||||||
|
course_slug=str(course_slug),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(circle_feedbacks):
|
||||||
|
avg = sum([fb.satisfaction_average for fb in circle_feedbacks]) / len(
|
||||||
|
circle_feedbacks
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
avg = 0
|
||||||
|
|
||||||
|
return FeedbackStatisticsResponsesType(
|
||||||
|
_id=course_id, # noqa
|
||||||
|
records=circle_feedbacks, # noqa
|
||||||
|
summary=FeedbackStatisticsSummaryType( # noqa
|
||||||
|
_id=course_id, # noqa
|
||||||
|
satisfaction_average=avg, # noqa
|
||||||
|
satisfaction_max=4, # noqa
|
||||||
|
total_responses=total_responses, # noqa
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def circle_feedback_average(
|
||||||
|
feedbacks: List[FeedbackResponse],
|
||||||
|
course_session_id,
|
||||||
|
generation: str,
|
||||||
|
course_slug: str,
|
||||||
|
):
|
||||||
|
circle_data = {}
|
||||||
|
records = []
|
||||||
|
|
||||||
|
for fb in feedbacks:
|
||||||
|
circle_id = fb.circle.id
|
||||||
|
satisfaction = fb.data.get("satisfaction", None)
|
||||||
|
|
||||||
|
if satisfaction is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
circle_data.setdefault(circle_id, {"total": 0, "count": 0, "experts": []})
|
||||||
|
circle_data[circle_id]["total"] += satisfaction
|
||||||
|
circle_data[circle_id]["count"] += 1
|
||||||
|
|
||||||
|
for circle_id, data in circle_data.items():
|
||||||
|
details_url = f"/course/{course_slug}/cockpit/feedback/{circle_id}?courseSessionId={course_session_id}"
|
||||||
|
|
||||||
|
experts = CourseSessionUser.objects.filter(
|
||||||
|
role="EXPERT",
|
||||||
|
course_session_id=course_session_id,
|
||||||
|
expert__id__in=[circle_id],
|
||||||
|
).values_list("user__first_name", "user__last_name")
|
||||||
|
experts_names = [f"{first} {last}" for first, last in experts]
|
||||||
|
|
||||||
|
records.append(
|
||||||
|
FeedbackStatisticsRecordType(
|
||||||
|
_id=f"circle:{circle_id}-course_session:{course_session_id}", # noqa
|
||||||
|
course_session_id=course_session_id, # noqa
|
||||||
|
generation=generation, # noqa
|
||||||
|
circle_id=circle_id, # noqa
|
||||||
|
satisfaction_average=data["total"] / data["count"], # noqa
|
||||||
|
satisfaction_max=4, # noqa
|
||||||
|
details_url=details_url, # noqa
|
||||||
|
experts=", ".join(experts_names), # noqa
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return records
|
||||||
|
|
@ -0,0 +1,363 @@
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
|
from graphene_django.utils import GraphQLTestCase
|
||||||
|
|
||||||
|
from vbv_lernwelt.assignment.models import Assignment, AssignmentType
|
||||||
|
from vbv_lernwelt.course.creators.test_utils import (
|
||||||
|
add_course_session_group_supervisor,
|
||||||
|
add_course_session_user,
|
||||||
|
create_assignment,
|
||||||
|
create_assignment_completion,
|
||||||
|
create_assignment_learning_content,
|
||||||
|
create_circle,
|
||||||
|
create_course,
|
||||||
|
create_course_session,
|
||||||
|
create_course_session_assignment,
|
||||||
|
create_course_session_edoniq_test,
|
||||||
|
create_course_session_group,
|
||||||
|
create_user,
|
||||||
|
)
|
||||||
|
from vbv_lernwelt.course.models import CourseSession, CourseSessionUser
|
||||||
|
from vbv_lernwelt.course_session.models import (
|
||||||
|
CourseSessionAssignment,
|
||||||
|
CourseSessionEdoniqTest,
|
||||||
|
)
|
||||||
|
from vbv_lernwelt.learnpath.models import Circle
|
||||||
|
|
||||||
|
|
||||||
|
class AssignmentTestCase(GraphQLTestCase):
|
||||||
|
GRAPHQL_URL = "/server/graphql/"
|
||||||
|
GRAPHQL_QUERY = f"""query($course_id: ID!) {{
|
||||||
|
course_statistics(course_id: $course_id) {{
|
||||||
|
assignments{{
|
||||||
|
summary{{
|
||||||
|
completed_count
|
||||||
|
average_passed
|
||||||
|
}}
|
||||||
|
records{{
|
||||||
|
course_session_id
|
||||||
|
course_session_assignment_id
|
||||||
|
circle_id
|
||||||
|
generation
|
||||||
|
assignment_title
|
||||||
|
assignment_type_translation_key
|
||||||
|
details_url
|
||||||
|
deadline
|
||||||
|
metrics {{
|
||||||
|
passed_count
|
||||||
|
failed_count
|
||||||
|
unranked_count
|
||||||
|
ranking_completed
|
||||||
|
average_passed
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
}}"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.course, self.course_page = create_course("Test Course")
|
||||||
|
self.course_session = create_course_session(course=self.course, title=":)")
|
||||||
|
self.circle, _ = create_circle(title="Circle", course_page=self.course_page)
|
||||||
|
|
||||||
|
self.supervisor = create_user("supervisor")
|
||||||
|
|
||||||
|
group = create_course_session_group(course_session=self.course_session)
|
||||||
|
add_course_session_group_supervisor(group=group, user=self.supervisor)
|
||||||
|
|
||||||
|
self.m1 = create_user("member_1")
|
||||||
|
add_course_session_user(
|
||||||
|
course_session=self.course_session,
|
||||||
|
user=self.m1,
|
||||||
|
role=CourseSessionUser.Role.MEMBER,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.m2 = create_user("member_2")
|
||||||
|
add_course_session_user(
|
||||||
|
course_session=self.course_session,
|
||||||
|
user=self.m2,
|
||||||
|
role=CourseSessionUser.Role.MEMBER,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.m3 = create_user("member_3")
|
||||||
|
add_course_session_user(
|
||||||
|
course_session=self.course_session,
|
||||||
|
user=self.m3,
|
||||||
|
role=CourseSessionUser.Role.MEMBER,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.e1 = create_user("expert_1")
|
||||||
|
add_course_session_user(
|
||||||
|
course_session=self.course_session,
|
||||||
|
user=self.e1,
|
||||||
|
role=CourseSessionUser.Role.EXPERT,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.client.force_login(self.supervisor)
|
||||||
|
|
||||||
|
def test_dashboard_contains_casework(self):
|
||||||
|
self._test_assignment_type_dashboard_details(
|
||||||
|
assignment_type=AssignmentType.CASEWORK
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_dashboard_contains_prep_assignments(self):
|
||||||
|
self._test_assignment_type_dashboard_details(
|
||||||
|
assignment_type=AssignmentType.PREP_ASSIGNMENT
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_dashboard_contains_edoniq_tests(self):
|
||||||
|
self._test_assignment_type_dashboard_details(
|
||||||
|
assignment_type=AssignmentType.EDONIQ_TEST
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_dashboard_not_contains_unsupported_types(self):
|
||||||
|
"""
|
||||||
|
Since everything is mixed in the same table, we need to make sure
|
||||||
|
that the dashboard only contains the supported types does not
|
||||||
|
get confused by the unsupported ones.
|
||||||
|
"""
|
||||||
|
|
||||||
|
irrelevant_types_for_dashboard = set(AssignmentType) - {
|
||||||
|
AssignmentType.CASEWORK,
|
||||||
|
AssignmentType.PREP_ASSIGNMENT,
|
||||||
|
AssignmentType.EDONIQ_TEST,
|
||||||
|
}
|
||||||
|
|
||||||
|
for assignment_type in irrelevant_types_for_dashboard:
|
||||||
|
self._test_assignment_type_not_in_dashboard(assignment_type=assignment_type)
|
||||||
|
|
||||||
|
def _test_assignment_type_dashboard_details(self, assignment_type: AssignmentType):
|
||||||
|
# GIVEN
|
||||||
|
assignment, csa = mix_assignment_cocktail(
|
||||||
|
assignment_type=assignment_type,
|
||||||
|
deadline_at=datetime(2000, 4, 1),
|
||||||
|
course_session=self.course_session,
|
||||||
|
circle=self.circle,
|
||||||
|
)
|
||||||
|
|
||||||
|
create_assignment_completion(
|
||||||
|
user=self.m1,
|
||||||
|
assignment=assignment,
|
||||||
|
course_session=self.course_session,
|
||||||
|
)
|
||||||
|
|
||||||
|
# WHEN
|
||||||
|
response = self.query(
|
||||||
|
self.GRAPHQL_QUERY, variables={"course_id": str(self.course.id)}
|
||||||
|
)
|
||||||
|
|
||||||
|
# THEN
|
||||||
|
self.assertResponseNoErrors(response)
|
||||||
|
dashboard = response.json()["data"]["course_statistics"]
|
||||||
|
|
||||||
|
records = dashboard["assignments"]["records"]
|
||||||
|
self.assertEqual(len(records), 1)
|
||||||
|
|
||||||
|
record = records[0]
|
||||||
|
|
||||||
|
if isinstance(csa, CourseSessionAssignment):
|
||||||
|
due_date = csa.submission_deadline
|
||||||
|
else:
|
||||||
|
due_date = csa.deadline
|
||||||
|
|
||||||
|
self.assertEqual(record["course_session_id"], str(self.course_session.id))
|
||||||
|
self.assertEqual(record["course_session_assignment_id"], str(csa.id))
|
||||||
|
self.assertEqual(record["generation"], str(self.course_session.generation))
|
||||||
|
self.assertEqual(record["circle_id"], str(self.circle.id))
|
||||||
|
self.assertEqual(record["details_url"], due_date.url_expert)
|
||||||
|
self.assertEqual(datetime.fromisoformat(record["deadline"]), due_date.start)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
record["assignment_title"],
|
||||||
|
csa.learning_content.content_assignment.title,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
record["assignment_type_translation_key"],
|
||||||
|
due_date.assignment_type_translation_key,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _test_assignment_type_not_in_dashboard(self, assignment_type: AssignmentType):
|
||||||
|
_, csa = mix_assignment_cocktail(
|
||||||
|
assignment_type=assignment_type,
|
||||||
|
course_session=self.course_session,
|
||||||
|
circle=self.circle,
|
||||||
|
)
|
||||||
|
|
||||||
|
# WHEN
|
||||||
|
response = self.query(
|
||||||
|
self.GRAPHQL_QUERY, variables={"course_id": str(self.course.id)}
|
||||||
|
)
|
||||||
|
|
||||||
|
# THEN
|
||||||
|
self.assertResponseNoErrors(response)
|
||||||
|
dashboard = response.json()["data"]["course_statistics"]
|
||||||
|
|
||||||
|
records = dashboard["assignments"]["records"]
|
||||||
|
self.assertEqual(len(records), 0)
|
||||||
|
|
||||||
|
def test_metrics_summary(self):
|
||||||
|
# GIVEN
|
||||||
|
assignment_1, _ = mix_assignment_cocktail(
|
||||||
|
deadline_at=datetime(1990, 4, 1),
|
||||||
|
assignment_type=AssignmentType.CASEWORK,
|
||||||
|
course_session=self.course_session,
|
||||||
|
circle=self.circle,
|
||||||
|
)
|
||||||
|
|
||||||
|
assignment_2, _ = mix_assignment_cocktail(
|
||||||
|
deadline_at=datetime(2000, 4, 1),
|
||||||
|
assignment_type=AssignmentType.EDONIQ_TEST,
|
||||||
|
course_session=self.course_session,
|
||||||
|
circle=self.circle,
|
||||||
|
)
|
||||||
|
|
||||||
|
assignment_3, _ = mix_assignment_cocktail(
|
||||||
|
deadline_at=datetime(2010, 4, 1),
|
||||||
|
assignment_type=AssignmentType.PREP_ASSIGNMENT,
|
||||||
|
course_session=self.course_session,
|
||||||
|
circle=self.circle,
|
||||||
|
)
|
||||||
|
|
||||||
|
# no completions for this assignment yet
|
||||||
|
assignment_4, _ = mix_assignment_cocktail(
|
||||||
|
deadline_at=datetime(2020, 4, 1),
|
||||||
|
assignment_type=AssignmentType.EDONIQ_TEST,
|
||||||
|
course_session=self.course_session,
|
||||||
|
circle=self.circle,
|
||||||
|
)
|
||||||
|
|
||||||
|
# assignment 1
|
||||||
|
assigment_1_results = [
|
||||||
|
(self.m1, True), # passed
|
||||||
|
(self.m2, False), # failed
|
||||||
|
(self.m3, None), # unranked
|
||||||
|
]
|
||||||
|
|
||||||
|
for user, has_passed in assigment_1_results:
|
||||||
|
if has_passed is None:
|
||||||
|
continue
|
||||||
|
create_assignment_completion(
|
||||||
|
user=user,
|
||||||
|
assignment=assignment_1,
|
||||||
|
course_session=self.course_session,
|
||||||
|
has_passed=has_passed,
|
||||||
|
)
|
||||||
|
|
||||||
|
# assignment 2
|
||||||
|
assignment_2_results = [
|
||||||
|
(self.m1, True), # passed
|
||||||
|
(self.m2, True), # passed
|
||||||
|
(self.m3, False), # failed
|
||||||
|
]
|
||||||
|
|
||||||
|
for user, has_passed in assignment_2_results:
|
||||||
|
create_assignment_completion(
|
||||||
|
user=user,
|
||||||
|
assignment=assignment_2,
|
||||||
|
course_session=self.course_session,
|
||||||
|
has_passed=has_passed,
|
||||||
|
)
|
||||||
|
|
||||||
|
# assignment 3
|
||||||
|
assignment_3_results = [
|
||||||
|
(self.m1, True), # passed
|
||||||
|
(self.m2, True), # passed
|
||||||
|
(self.m3, True), # passed
|
||||||
|
]
|
||||||
|
for user, has_passed in assignment_3_results:
|
||||||
|
create_assignment_completion(
|
||||||
|
user=user,
|
||||||
|
assignment=assignment_3,
|
||||||
|
course_session=self.course_session,
|
||||||
|
has_passed=has_passed,
|
||||||
|
)
|
||||||
|
|
||||||
|
# WHEN
|
||||||
|
response = self.query(
|
||||||
|
self.GRAPHQL_QUERY, variables={"course_id": str(self.course.id)}
|
||||||
|
)
|
||||||
|
|
||||||
|
# THEN
|
||||||
|
self.assertResponseNoErrors(response)
|
||||||
|
dashboard = response.json()["data"]["course_statistics"]
|
||||||
|
|
||||||
|
# 1 -> incomplete (not counted for average)
|
||||||
|
# 2 -> complete 66% passed ...
|
||||||
|
# 3 -> complete 100% passed --> 83.5%
|
||||||
|
# 4 -> incomplete (not counted for average)
|
||||||
|
summary = dashboard["assignments"]["summary"]
|
||||||
|
self.assertEqual(summary["completed_count"], 2)
|
||||||
|
self.assertEqual(summary["average_passed"], 83.5)
|
||||||
|
|
||||||
|
records = dashboard["assignments"]["records"]
|
||||||
|
self.assertEqual(len(records), 4)
|
||||||
|
|
||||||
|
# 1 -> assigment_1_results (oldest)
|
||||||
|
assignment_1_metrics = records[0]["metrics"]
|
||||||
|
self.assertEqual(assignment_1_metrics["passed_count"], 1)
|
||||||
|
self.assertEqual(assignment_1_metrics["failed_count"], 1)
|
||||||
|
self.assertEqual(assignment_1_metrics["unranked_count"], 1)
|
||||||
|
self.assertEqual(assignment_1_metrics["ranking_completed"], False)
|
||||||
|
self.assertEqual(assignment_1_metrics["average_passed"], 34)
|
||||||
|
|
||||||
|
# 2 -> assignment_2_results
|
||||||
|
assignment_2_metrics = records[1]["metrics"]
|
||||||
|
self.assertEqual(assignment_2_metrics["passed_count"], 2)
|
||||||
|
self.assertEqual(assignment_2_metrics["failed_count"], 1)
|
||||||
|
self.assertEqual(assignment_2_metrics["unranked_count"], 0)
|
||||||
|
self.assertEqual(assignment_2_metrics["ranking_completed"], True)
|
||||||
|
self.assertEqual(assignment_2_metrics["average_passed"], 67)
|
||||||
|
|
||||||
|
# 3 -> assignment_3_results
|
||||||
|
assignment_3_metrics = records[2]["metrics"]
|
||||||
|
self.assertEqual(assignment_3_metrics["passed_count"], 3)
|
||||||
|
self.assertEqual(assignment_3_metrics["failed_count"], 0)
|
||||||
|
self.assertEqual(assignment_3_metrics["unranked_count"], 0)
|
||||||
|
self.assertEqual(assignment_3_metrics["ranking_completed"], True)
|
||||||
|
self.assertEqual(assignment_3_metrics["average_passed"], 100)
|
||||||
|
|
||||||
|
# 4 -> no completions (newest)
|
||||||
|
assignment_4_metrics = records[3]["metrics"]
|
||||||
|
self.assertEqual(assignment_4_metrics["passed_count"], 0)
|
||||||
|
self.assertEqual(assignment_4_metrics["failed_count"], 0)
|
||||||
|
self.assertEqual(assignment_4_metrics["unranked_count"], 3)
|
||||||
|
self.assertEqual(assignment_4_metrics["ranking_completed"], False)
|
||||||
|
self.assertEqual(assignment_4_metrics["average_passed"], 0)
|
||||||
|
|
||||||
|
|
||||||
|
def mix_assignment_cocktail(
|
||||||
|
assignment_type: AssignmentType,
|
||||||
|
course_session: CourseSession,
|
||||||
|
circle: Circle,
|
||||||
|
deadline_at: datetime | None = None,
|
||||||
|
) -> Tuple[Assignment, CourseSessionAssignment | CourseSessionEdoniqTest]:
|
||||||
|
"""
|
||||||
|
Little test helper to create a course session assignment or edoniq test based
|
||||||
|
on the given assignment type.
|
||||||
|
"""
|
||||||
|
|
||||||
|
assignment = create_assignment(
|
||||||
|
course=course_session.course, assignment_type=assignment_type
|
||||||
|
)
|
||||||
|
|
||||||
|
if assignment_type == AssignmentType.EDONIQ_TEST:
|
||||||
|
cset = create_course_session_edoniq_test(
|
||||||
|
deadline_at=deadline_at,
|
||||||
|
course_session=course_session,
|
||||||
|
learning_content_edoniq_test=create_assignment_learning_content(
|
||||||
|
circle=circle,
|
||||||
|
assignment=assignment,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return assignment, cset
|
||||||
|
else:
|
||||||
|
csa = create_course_session_assignment(
|
||||||
|
deadline_at=deadline_at,
|
||||||
|
course_session=course_session,
|
||||||
|
learning_content_assignment=create_assignment_learning_content(
|
||||||
|
circle=circle,
|
||||||
|
assignment=assignment,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return assignment, csa
|
||||||
|
|
@ -0,0 +1,123 @@
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from django.utils import timezone
|
||||||
|
from graphene_django.utils import GraphQLTestCase
|
||||||
|
|
||||||
|
from vbv_lernwelt.course.creators.test_utils import (
|
||||||
|
add_course_session_group_supervisor,
|
||||||
|
add_course_session_user,
|
||||||
|
create_attendance_course,
|
||||||
|
create_circle,
|
||||||
|
create_course,
|
||||||
|
create_course_session,
|
||||||
|
create_course_session_group,
|
||||||
|
create_user,
|
||||||
|
)
|
||||||
|
from vbv_lernwelt.course.models import CourseSessionUser
|
||||||
|
from vbv_lernwelt.course_session.services.attendance import AttendanceUserStatus
|
||||||
|
|
||||||
|
|
||||||
|
class DashboardAttendanceTestCase(GraphQLTestCase):
|
||||||
|
GRAPHQL_URL = "/server/graphql/"
|
||||||
|
|
||||||
|
def test_attendance_day_presences(self):
|
||||||
|
# GIVEN
|
||||||
|
course, course_page = create_course("Test Course")
|
||||||
|
course_session = create_course_session(course=course, title="Test Bern 2022 a")
|
||||||
|
|
||||||
|
supervisor = create_user("supervisor")
|
||||||
|
|
||||||
|
group = create_course_session_group(course_session=course_session)
|
||||||
|
add_course_session_group_supervisor(group=group, user=supervisor)
|
||||||
|
|
||||||
|
circle, _ = create_circle(title="Test Circle", course_page=course_page)
|
||||||
|
|
||||||
|
m1 = create_user("member_1")
|
||||||
|
add_course_session_user(
|
||||||
|
course_session=course_session,
|
||||||
|
user=m1,
|
||||||
|
role=CourseSessionUser.Role.MEMBER,
|
||||||
|
)
|
||||||
|
|
||||||
|
m2 = create_user("member_2")
|
||||||
|
add_course_session_user(
|
||||||
|
course_session=course_session,
|
||||||
|
user=m2,
|
||||||
|
role=CourseSessionUser.Role.MEMBER,
|
||||||
|
)
|
||||||
|
|
||||||
|
m3 = create_user("member_3")
|
||||||
|
add_course_session_user(
|
||||||
|
course_session=course_session,
|
||||||
|
user=m3,
|
||||||
|
role=CourseSessionUser.Role.MEMBER,
|
||||||
|
)
|
||||||
|
|
||||||
|
e1 = create_user("expert_1")
|
||||||
|
add_course_session_user(
|
||||||
|
course_session=course_session,
|
||||||
|
user=e1,
|
||||||
|
role=CourseSessionUser.Role.EXPERT,
|
||||||
|
)
|
||||||
|
|
||||||
|
attendance_user_list = [
|
||||||
|
{"user_id": str(m1.id), "status": AttendanceUserStatus.PRESENT.value},
|
||||||
|
{"user_id": str(m2.id), "status": AttendanceUserStatus.ABSENT.value},
|
||||||
|
]
|
||||||
|
|
||||||
|
due_date_end = timezone.now() - timedelta(hours=2)
|
||||||
|
attendance_course = create_attendance_course(
|
||||||
|
course_session=course_session,
|
||||||
|
circle=circle,
|
||||||
|
attendance_user_list=attendance_user_list,
|
||||||
|
due_date_end=due_date_end,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.client.force_login(supervisor)
|
||||||
|
|
||||||
|
query = f"""
|
||||||
|
query($course_id: ID!) {{
|
||||||
|
course_statistics(course_id: $course_id) {{
|
||||||
|
attendance_day_presences{{
|
||||||
|
summary{{
|
||||||
|
days_completed
|
||||||
|
participants_present
|
||||||
|
}}
|
||||||
|
records{{
|
||||||
|
course_session_id
|
||||||
|
generation
|
||||||
|
circle_id
|
||||||
|
due_date
|
||||||
|
participants_present
|
||||||
|
participants_total
|
||||||
|
details_url
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
|
||||||
|
# WHEN
|
||||||
|
response = self.query(query, variables={"course_id": str(course.id)})
|
||||||
|
|
||||||
|
self.assertResponseNoErrors(response)
|
||||||
|
|
||||||
|
data = response.json()["data"]
|
||||||
|
|
||||||
|
attendance_day_presences = data["course_statistics"]["attendance_day_presences"]
|
||||||
|
|
||||||
|
record = attendance_day_presences["records"][0]
|
||||||
|
|
||||||
|
self.assertEqual(record["course_session_id"], str(course_session.id))
|
||||||
|
self.assertEqual(record["generation"], "2023")
|
||||||
|
self.assertEqual(record["participants_present"], 1)
|
||||||
|
self.assertEqual(record["participants_total"], 3)
|
||||||
|
self.assertEqual(
|
||||||
|
record["details_url"],
|
||||||
|
f"/course/test-lehrgang/cockpit/attendance?id={attendance_course.learning_content.id}"
|
||||||
|
f"&courseSessionId={course_session.id}",
|
||||||
|
)
|
||||||
|
|
||||||
|
summary = attendance_day_presences["summary"]
|
||||||
|
self.assertEqual(summary["days_completed"], 1)
|
||||||
|
self.assertEqual(summary["participants_present"], 34)
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
from graphene_django.utils import GraphQLTestCase
|
||||||
|
|
||||||
|
from vbv_lernwelt.course.creators.test_utils import (
|
||||||
|
add_course_session_group_supervisor,
|
||||||
|
add_course_session_user,
|
||||||
|
create_circle,
|
||||||
|
create_course,
|
||||||
|
create_course_session,
|
||||||
|
create_course_session_group,
|
||||||
|
create_performance_criteria_page,
|
||||||
|
create_user,
|
||||||
|
)
|
||||||
|
from vbv_lernwelt.course.models import CourseSessionUser
|
||||||
|
from vbv_lernwelt.course.services import mark_course_completion
|
||||||
|
|
||||||
|
|
||||||
|
class DashboardCompetenceTestCase(GraphQLTestCase):
|
||||||
|
GRAPHQL_URL = "/server/graphql/"
|
||||||
|
|
||||||
|
def test_competence(self):
|
||||||
|
# GIVEN
|
||||||
|
course, course_page = create_course("Test Course")
|
||||||
|
course_session = create_course_session(course=course, title="Test Bern 2022 a")
|
||||||
|
|
||||||
|
supervisor = create_user("supervisor")
|
||||||
|
group = create_course_session_group(course_session=course_session)
|
||||||
|
add_course_session_group_supervisor(group=group, user=supervisor)
|
||||||
|
|
||||||
|
member_one = create_user("member one")
|
||||||
|
add_course_session_user(
|
||||||
|
course_session=course_session,
|
||||||
|
user=member_one,
|
||||||
|
role=CourseSessionUser.Role.MEMBER,
|
||||||
|
)
|
||||||
|
|
||||||
|
member_two = create_user("member two")
|
||||||
|
add_course_session_user(
|
||||||
|
course_session=course_session,
|
||||||
|
user=member_two,
|
||||||
|
role=CourseSessionUser.Role.MEMBER,
|
||||||
|
)
|
||||||
|
|
||||||
|
circle, _ = create_circle(title="Test Circle", course_page=course_page)
|
||||||
|
|
||||||
|
pc = create_performance_criteria_page(
|
||||||
|
course=course, course_page=course_page, circle=circle
|
||||||
|
)
|
||||||
|
|
||||||
|
mark_course_completion(
|
||||||
|
page=pc,
|
||||||
|
user=member_one,
|
||||||
|
course_session=course_session,
|
||||||
|
completion_status="SUCCESS",
|
||||||
|
)
|
||||||
|
|
||||||
|
mark_course_completion(
|
||||||
|
page=pc,
|
||||||
|
user=member_two,
|
||||||
|
course_session=course_session,
|
||||||
|
completion_status="FAIL",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.client.force_login(supervisor)
|
||||||
|
|
||||||
|
query = f"""query($course_id: ID!) {{
|
||||||
|
course_statistics(course_id: $course_id) {{
|
||||||
|
competences {{
|
||||||
|
records {{
|
||||||
|
title
|
||||||
|
course_session_id
|
||||||
|
generation
|
||||||
|
circle_id
|
||||||
|
success_count
|
||||||
|
fail_count
|
||||||
|
details_url
|
||||||
|
}}
|
||||||
|
summary {{
|
||||||
|
success_total
|
||||||
|
fail_total
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
variables = {"course_id": str(course.id)}
|
||||||
|
|
||||||
|
# WHEN
|
||||||
|
response = self.query(query, variables=variables)
|
||||||
|
|
||||||
|
# THEN
|
||||||
|
self.assertResponseNoErrors(response)
|
||||||
|
|
||||||
|
competences = response.json()["data"]["course_statistics"]["competences"]
|
||||||
|
records = competences["records"]
|
||||||
|
|
||||||
|
self.assertEqual(records[0]["title"], "Learning Unit")
|
||||||
|
self.assertEqual(records[0]["success_count"], 1)
|
||||||
|
self.assertEqual(records[0]["fail_count"], 1)
|
||||||
|
self.assertEqual(records[0]["circle_id"], str(circle.id))
|
||||||
|
self.assertEqual(records[0]["course_session_id"], str(course_session.id))
|
||||||
|
self.assertEqual(records[0]["generation"], "2023")
|
||||||
|
self.assertEqual(
|
||||||
|
records[0]["details_url"],
|
||||||
|
f"/course/{course.slug}/cockpit?courseSessionId={course_session.id}",
|
||||||
|
)
|
||||||
|
|
||||||
|
summary = competences["summary"]
|
||||||
|
self.assertEqual(summary["success_total"], 1)
|
||||||
|
self.assertEqual(summary["fail_total"], 1)
|
||||||
|
|
@ -0,0 +1,284 @@
|
||||||
|
from graphene_django.utils import GraphQLTestCase
|
||||||
|
|
||||||
|
from vbv_lernwelt.assignment.models import AssignmentType
|
||||||
|
from vbv_lernwelt.course.creators.test_utils import (
|
||||||
|
add_course_session_group_supervisor,
|
||||||
|
add_course_session_user,
|
||||||
|
create_assignment,
|
||||||
|
create_assignment_completion,
|
||||||
|
create_circle,
|
||||||
|
create_course,
|
||||||
|
create_course_session,
|
||||||
|
create_course_session_group,
|
||||||
|
create_performance_criteria_page,
|
||||||
|
create_user,
|
||||||
|
)
|
||||||
|
from vbv_lernwelt.course.models import CourseSessionUser
|
||||||
|
from vbv_lernwelt.course.services import mark_course_completion
|
||||||
|
|
||||||
|
|
||||||
|
class DashboardTestCase(GraphQLTestCase):
|
||||||
|
GRAPHQL_URL = "/server/graphql/"
|
||||||
|
|
||||||
|
def test_course_progress(self):
|
||||||
|
# GIVEN
|
||||||
|
course, course_page = create_course("Test Course")
|
||||||
|
|
||||||
|
cs_1 = create_course_session(
|
||||||
|
course=course, title="Test Course Session 1", generation=""
|
||||||
|
)
|
||||||
|
cs_2 = create_course_session(
|
||||||
|
course=course, title="Test Course Session 2", generation="2020"
|
||||||
|
)
|
||||||
|
cs_3 = create_course_session(
|
||||||
|
course=course, title="Test Course Session 3", generation="1984"
|
||||||
|
)
|
||||||
|
|
||||||
|
member = create_user("sepp")
|
||||||
|
|
||||||
|
add_course_session_user(
|
||||||
|
course_session=cs_1, user=member, role=CourseSessionUser.Role.MEMBER
|
||||||
|
)
|
||||||
|
add_course_session_user(
|
||||||
|
course_session=cs_2, user=member, role=CourseSessionUser.Role.MEMBER
|
||||||
|
)
|
||||||
|
add_course_session_user(
|
||||||
|
course_session=cs_3, user=member, role=CourseSessionUser.Role.MEMBER
|
||||||
|
)
|
||||||
|
|
||||||
|
# setup assignments
|
||||||
|
create_assignment_completion(
|
||||||
|
user=member,
|
||||||
|
assignment=create_assignment(
|
||||||
|
course=course, assignment_type=AssignmentType.CASEWORK
|
||||||
|
),
|
||||||
|
course_session=cs_1,
|
||||||
|
has_passed=True,
|
||||||
|
achieved_points=10,
|
||||||
|
max_points=10,
|
||||||
|
)
|
||||||
|
|
||||||
|
create_assignment_completion(
|
||||||
|
user=member,
|
||||||
|
assignment=create_assignment(
|
||||||
|
course=course, assignment_type=AssignmentType.CASEWORK
|
||||||
|
),
|
||||||
|
course_session=cs_2,
|
||||||
|
has_passed=False,
|
||||||
|
achieved_points=10,
|
||||||
|
max_points=40,
|
||||||
|
)
|
||||||
|
|
||||||
|
# setup competence
|
||||||
|
circle, _ = create_circle(
|
||||||
|
title="How to circle like a pro!", course_page=course_page
|
||||||
|
)
|
||||||
|
|
||||||
|
mark_course_completion(
|
||||||
|
page=create_performance_criteria_page(
|
||||||
|
course=course, course_page=course_page, circle=circle
|
||||||
|
),
|
||||||
|
user=member,
|
||||||
|
course_session=cs_1,
|
||||||
|
completion_status="SUCCESS",
|
||||||
|
)
|
||||||
|
|
||||||
|
mark_course_completion(
|
||||||
|
page=create_performance_criteria_page(
|
||||||
|
course=course, course_page=course_page, circle=circle
|
||||||
|
),
|
||||||
|
user=member,
|
||||||
|
course_session=cs_2,
|
||||||
|
completion_status="FAIL",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.client.force_login(member)
|
||||||
|
|
||||||
|
query = f"""query($course_id: ID!) {{
|
||||||
|
course_progress(course_id: $course_id) {{
|
||||||
|
course_id
|
||||||
|
session_to_continue_id
|
||||||
|
competence {{
|
||||||
|
total_count
|
||||||
|
success_count
|
||||||
|
fail_count
|
||||||
|
}}
|
||||||
|
assignment {{
|
||||||
|
total_count
|
||||||
|
points_max_count
|
||||||
|
points_achieved_count
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
|
||||||
|
variables = {"course_id": str(course.id)}
|
||||||
|
|
||||||
|
# WHEN
|
||||||
|
response = self.query(query, variables=variables)
|
||||||
|
|
||||||
|
# THEN
|
||||||
|
self.assertResponseNoErrors(response)
|
||||||
|
|
||||||
|
course_progress = response.json()["data"]["course_progress"]
|
||||||
|
self.assertEqual(course_progress["course_id"], str(course.id))
|
||||||
|
self.assertEqual(course_progress["session_to_continue_id"], str(cs_2.id))
|
||||||
|
|
||||||
|
competence = course_progress["competence"]
|
||||||
|
self.assertEqual(competence["total_count"], 2)
|
||||||
|
self.assertEqual(competence["success_count"], 1)
|
||||||
|
self.assertEqual(competence["fail_count"], 1)
|
||||||
|
|
||||||
|
assignment = course_progress["assignment"]
|
||||||
|
self.assertEqual(assignment["total_count"], 2)
|
||||||
|
self.assertEqual(assignment["points_max_count"], 50)
|
||||||
|
self.assertEqual(assignment["points_achieved_count"], 20)
|
||||||
|
|
||||||
|
def test_dashboard_config(self):
|
||||||
|
# GIVEN
|
||||||
|
course_1, _ = create_course("Test Course 1")
|
||||||
|
course_2, _ = create_course("Test Course 2")
|
||||||
|
course_3, _ = create_course("Test Course 3")
|
||||||
|
|
||||||
|
cs_1 = create_course_session(course=course_1, title="Test Course 1 Session")
|
||||||
|
cs_2 = create_course_session(course=course_2, title="Test Course 2 Session")
|
||||||
|
|
||||||
|
cs_3_a = create_course_session(course=course_3, title="CS 3 A (as member)")
|
||||||
|
cs_3_b = create_course_session(course=course_3, title="CS 3 B (as expert)")
|
||||||
|
|
||||||
|
supervisor = create_user("supervisor")
|
||||||
|
|
||||||
|
# CS 1
|
||||||
|
add_course_session_user(
|
||||||
|
course_session=cs_1, user=supervisor, role=CourseSessionUser.Role.MEMBER
|
||||||
|
)
|
||||||
|
|
||||||
|
# CS 2
|
||||||
|
add_course_session_group_supervisor(
|
||||||
|
group=create_course_session_group(course_session=cs_2), user=supervisor
|
||||||
|
)
|
||||||
|
|
||||||
|
# CS 3 A
|
||||||
|
add_course_session_user(
|
||||||
|
course_session=cs_3_a, user=supervisor, role=CourseSessionUser.Role.MEMBER
|
||||||
|
)
|
||||||
|
|
||||||
|
# CS 3 B
|
||||||
|
add_course_session_user(
|
||||||
|
course_session=cs_3_b, user=supervisor, role=CourseSessionUser.Role.EXPERT
|
||||||
|
)
|
||||||
|
|
||||||
|
self.client.force_login(supervisor)
|
||||||
|
|
||||||
|
# WHEN
|
||||||
|
query = """query {
|
||||||
|
dashboard_config {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
slug
|
||||||
|
dashboard_type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
response = self.query(query)
|
||||||
|
|
||||||
|
# THEN
|
||||||
|
self.assertResponseNoErrors(response)
|
||||||
|
|
||||||
|
dashboard_config = response.json()["data"]["dashboard_config"]
|
||||||
|
self.assertEqual(len(dashboard_config), 3)
|
||||||
|
|
||||||
|
course_1_config = find_dashboard_config_by_course_id(
|
||||||
|
dashboard_config, course_1.id
|
||||||
|
)
|
||||||
|
self.assertIsNotNone(course_1_config)
|
||||||
|
self.assertEqual(course_1_config["name"], course_1.title)
|
||||||
|
self.assertEqual(course_1_config["slug"], course_1.slug)
|
||||||
|
self.assertEqual(course_1_config["dashboard_type"], "PROGRESS_DASHBOARD")
|
||||||
|
|
||||||
|
course_2_config = find_dashboard_config_by_course_id(
|
||||||
|
dashboard_config, course_2.id
|
||||||
|
)
|
||||||
|
self.assertIsNotNone(course_2_config)
|
||||||
|
self.assertEqual(course_2_config["name"], course_2.title)
|
||||||
|
self.assertEqual(course_2_config["slug"], course_2.slug)
|
||||||
|
self.assertEqual(course_2_config["dashboard_type"], "STATISTICS_DASHBOARD")
|
||||||
|
|
||||||
|
course_3_config = find_dashboard_config_by_course_id(
|
||||||
|
dashboard_config, course_3.id
|
||||||
|
)
|
||||||
|
self.assertIsNotNone(course_3_config)
|
||||||
|
self.assertEqual(course_3_config["name"], course_3.title)
|
||||||
|
self.assertEqual(course_3_config["slug"], course_3.slug)
|
||||||
|
self.assertEqual(course_3_config["dashboard_type"], "SIMPLE_DASHBOARD")
|
||||||
|
|
||||||
|
def test_course_statistics_deny_not_allowed_user(self):
|
||||||
|
# GIVEN
|
||||||
|
disallowed_user = create_user("1337_hacker_schorsch")
|
||||||
|
course, _ = create_course("Test Course")
|
||||||
|
create_course_session(course=course, title="Test Course Session")
|
||||||
|
|
||||||
|
self.client.force_login(disallowed_user)
|
||||||
|
|
||||||
|
query = f"""query($course_id: ID!) {{
|
||||||
|
course_statistics(course_id: $course_id) {{
|
||||||
|
course_id
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
variables = {"course_id": str(course.id)}
|
||||||
|
|
||||||
|
# WHEN
|
||||||
|
response = self.query(query, variables=variables)
|
||||||
|
|
||||||
|
# THEN
|
||||||
|
self.assertResponseNoErrors(response)
|
||||||
|
|
||||||
|
course_statistics = response.json()["data"]["course_statistics"]
|
||||||
|
self.assertEqual(course_statistics, None)
|
||||||
|
|
||||||
|
def test_course_statistics_data(self):
|
||||||
|
# GIVEN
|
||||||
|
supervisor = create_user("supervisor")
|
||||||
|
course_1, _ = create_course("Test Course 1")
|
||||||
|
course_2, _ = create_course("Test Course 2")
|
||||||
|
|
||||||
|
cs_1 = create_course_session(course=course_1, title="Test Course 1 Session")
|
||||||
|
cs_2 = create_course_session(course=course_2, title="Test Course 2 Session")
|
||||||
|
|
||||||
|
cs_group_1 = create_course_session_group(course_session=cs_1)
|
||||||
|
add_course_session_group_supervisor(group=cs_group_1, user=supervisor)
|
||||||
|
|
||||||
|
cs_group_2 = create_course_session_group(course_session=cs_2)
|
||||||
|
add_course_session_group_supervisor(group=cs_group_2, user=supervisor)
|
||||||
|
|
||||||
|
self.client.force_login(supervisor)
|
||||||
|
|
||||||
|
query = f"""query($course_id: ID!) {{
|
||||||
|
course_statistics(course_id: $course_id) {{
|
||||||
|
course_id
|
||||||
|
course_title
|
||||||
|
course_slug
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
variables = {"course_id": str(course_2.id)}
|
||||||
|
|
||||||
|
# WHEN
|
||||||
|
response = self.query(query, variables=variables)
|
||||||
|
|
||||||
|
# THEN
|
||||||
|
self.assertResponseNoErrors(response)
|
||||||
|
|
||||||
|
course_statistics = response.json()["data"]["course_statistics"]
|
||||||
|
|
||||||
|
self.assertEqual(course_statistics["course_id"], str(course_2.id))
|
||||||
|
self.assertEqual(course_statistics["course_title"], course_2.title)
|
||||||
|
self.assertEqual(course_statistics["course_slug"], course_2.slug)
|
||||||
|
|
||||||
|
|
||||||
|
def find_dashboard_config_by_course_id(dashboard_configs, course_id):
|
||||||
|
return next(
|
||||||
|
(config for config in dashboard_configs if config["id"] == str(course_id)), None
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,142 @@
|
||||||
|
from graphene_django.utils import GraphQLTestCase
|
||||||
|
|
||||||
|
from vbv_lernwelt.course.creators.test_utils import (
|
||||||
|
add_course_session_group_supervisor,
|
||||||
|
add_course_session_user,
|
||||||
|
create_circle,
|
||||||
|
create_circle_expert,
|
||||||
|
create_course,
|
||||||
|
create_course_session,
|
||||||
|
create_course_session_group,
|
||||||
|
create_user,
|
||||||
|
)
|
||||||
|
from vbv_lernwelt.course.models import CourseSessionUser
|
||||||
|
from vbv_lernwelt.feedback.models import FeedbackResponse
|
||||||
|
|
||||||
|
|
||||||
|
class DashboardFeedbackTestCase(GraphQLTestCase):
|
||||||
|
GRAPHQL_URL = "/server/graphql/"
|
||||||
|
|
||||||
|
def test_feedback(self):
|
||||||
|
# GIVEN
|
||||||
|
course, course_page = create_course("Test Course")
|
||||||
|
course_session = create_course_session(course=course, title="Test Bern 2022 a")
|
||||||
|
|
||||||
|
supervisor = create_user("supervisor")
|
||||||
|
|
||||||
|
group = create_course_session_group(course_session=course_session)
|
||||||
|
add_course_session_group_supervisor(group=group, user=supervisor)
|
||||||
|
|
||||||
|
member = create_user("member")
|
||||||
|
add_course_session_user(
|
||||||
|
course_session=course_session,
|
||||||
|
user=member,
|
||||||
|
role=CourseSessionUser.Role.MEMBER,
|
||||||
|
)
|
||||||
|
|
||||||
|
circle1, _ = create_circle(title="Test Circle 1", course_page=course_page)
|
||||||
|
circle2, _ = create_circle(title="Test Circle 2", course_page=course_page)
|
||||||
|
|
||||||
|
create_circle_expert(
|
||||||
|
course_session=course_session, circle=circle1, username="Expert 1"
|
||||||
|
)
|
||||||
|
create_circle_expert(
|
||||||
|
course_session=course_session, circle=circle1, username="Expert 2"
|
||||||
|
)
|
||||||
|
|
||||||
|
FeedbackResponse.objects.create(
|
||||||
|
feedback_user=member,
|
||||||
|
data={"satisfaction": 3},
|
||||||
|
circle=circle1,
|
||||||
|
course_session=course_session,
|
||||||
|
submitted=True,
|
||||||
|
)
|
||||||
|
FeedbackResponse.objects.create(
|
||||||
|
feedback_user=member,
|
||||||
|
data={"satisfaction": 4},
|
||||||
|
circle=circle1,
|
||||||
|
course_session=course_session,
|
||||||
|
submitted=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create Feedbacks for circle2
|
||||||
|
FeedbackResponse.objects.create(
|
||||||
|
feedback_user=member,
|
||||||
|
data={"satisfaction": 1},
|
||||||
|
circle=circle2,
|
||||||
|
course_session=course_session,
|
||||||
|
submitted=True,
|
||||||
|
)
|
||||||
|
FeedbackResponse.objects.create(
|
||||||
|
feedback_user=member,
|
||||||
|
data={"satisfaction": 2},
|
||||||
|
circle=circle2,
|
||||||
|
course_session=course_session,
|
||||||
|
submitted=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.client.force_login(supervisor)
|
||||||
|
|
||||||
|
query = f"""query($course_id: ID!) {{
|
||||||
|
course_statistics(course_id: $course_id) {{
|
||||||
|
feedback_responses {{
|
||||||
|
records {{
|
||||||
|
course_session_id
|
||||||
|
generation
|
||||||
|
circle_id
|
||||||
|
satisfaction_average
|
||||||
|
satisfaction_max
|
||||||
|
details_url
|
||||||
|
experts
|
||||||
|
}}
|
||||||
|
summary {{
|
||||||
|
satisfaction_average
|
||||||
|
satisfaction_max
|
||||||
|
total_responses
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
variables = {"course_id": str(course.id)}
|
||||||
|
|
||||||
|
# WHEN
|
||||||
|
response = self.query(query, variables=variables)
|
||||||
|
|
||||||
|
# THEN
|
||||||
|
self.assertResponseNoErrors(response)
|
||||||
|
|
||||||
|
course_statistics = response.json()["data"]["course_statistics"]
|
||||||
|
feedback_responses = course_statistics["feedback_responses"]
|
||||||
|
|
||||||
|
records = feedback_responses["records"]
|
||||||
|
self.assertEqual(len(records), 2)
|
||||||
|
|
||||||
|
circle1_record = next(
|
||||||
|
(r for r in records if r["circle_id"] == str(circle1.id)), None
|
||||||
|
)
|
||||||
|
self.assertEqual(circle1_record["satisfaction_average"], 3.5)
|
||||||
|
self.assertEqual(circle1_record["course_session_id"], str(course_session.id))
|
||||||
|
self.assertEqual(circle1_record["generation"], "2023")
|
||||||
|
self.assertEqual(
|
||||||
|
circle1_record["details_url"],
|
||||||
|
f"/course/{course.slug}/cockpit/feedback/{circle1.id}?courseSessionId={course_session.id}",
|
||||||
|
)
|
||||||
|
self.assertEqual(circle1_record["experts"], "Test Expert 1, Test Expert 2")
|
||||||
|
|
||||||
|
circle2_record = next(
|
||||||
|
(r for r in records if r["circle_id"] == str(circle2.id)), None
|
||||||
|
)
|
||||||
|
self.assertEqual(circle2_record["satisfaction_average"], 1.5)
|
||||||
|
self.assertEqual(circle2_record["course_session_id"], str(course_session.id))
|
||||||
|
self.assertEqual(circle2_record["generation"], "2023")
|
||||||
|
self.assertEqual(
|
||||||
|
circle2_record["details_url"],
|
||||||
|
f"/course/{course.slug}/cockpit/feedback/{circle2.id}?courseSessionId={course_session.id}",
|
||||||
|
)
|
||||||
|
self.assertEqual(circle2_record["experts"], "")
|
||||||
|
|
||||||
|
summary = feedback_responses["summary"]
|
||||||
|
self.assertEqual(summary["satisfaction_average"], 2.5)
|
||||||
|
self.assertEqual(summary["satisfaction_max"], 4)
|
||||||
|
self.assertEqual(summary["total_responses"], 4)
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
from graphene_django.utils import GraphQLTestCase
|
||||||
|
|
||||||
|
from vbv_lernwelt.course.creators.test_utils import (
|
||||||
|
add_course_session_group_course_session,
|
||||||
|
add_course_session_group_supervisor,
|
||||||
|
add_course_session_user,
|
||||||
|
create_course,
|
||||||
|
create_course_session,
|
||||||
|
create_course_session_group,
|
||||||
|
create_user,
|
||||||
|
)
|
||||||
|
from vbv_lernwelt.course.models import CourseSessionUser
|
||||||
|
|
||||||
|
|
||||||
|
class DashboardTestCase(GraphQLTestCase):
|
||||||
|
GRAPHQL_URL = "/server/graphql/"
|
||||||
|
|
||||||
|
def test_selection_metrics(self):
|
||||||
|
# GIVEN
|
||||||
|
course_1, _ = create_course("Test Course 1")
|
||||||
|
course_2, _ = create_course("Dummy Course 2")
|
||||||
|
|
||||||
|
cs_1_a = create_course_session(course=course_1, title="Zug", generation="1984")
|
||||||
|
cs_1_b = create_course_session(course=course_1, title="Bern", generation="1984")
|
||||||
|
cs_1_c = create_course_session(course=course_1, title="Wil", generation="1984")
|
||||||
|
cs_2_a = create_course_session(course=course_2, title="Baar", generation="1984")
|
||||||
|
|
||||||
|
member_1 = create_user("member_1")
|
||||||
|
member_2 = create_user("member_2")
|
||||||
|
member_3 = create_user("member_3")
|
||||||
|
member_4 = create_user("member_4")
|
||||||
|
|
||||||
|
expert_1 = create_user("expert_1")
|
||||||
|
expert_2 = create_user("expert_2")
|
||||||
|
expert_3 = create_user("expert_3")
|
||||||
|
expert_4 = create_user("expert_4")
|
||||||
|
|
||||||
|
# CS 1 A
|
||||||
|
add_course_session_user(
|
||||||
|
course_session=cs_1_a, user=member_1, role=CourseSessionUser.Role.MEMBER
|
||||||
|
)
|
||||||
|
add_course_session_user(
|
||||||
|
course_session=cs_1_b, user=member_2, role=CourseSessionUser.Role.MEMBER
|
||||||
|
)
|
||||||
|
|
||||||
|
# CS 1 B
|
||||||
|
add_course_session_user(
|
||||||
|
course_session=cs_1_a, user=expert_1, role=CourseSessionUser.Role.EXPERT
|
||||||
|
)
|
||||||
|
add_course_session_user(
|
||||||
|
course_session=cs_1_b, user=expert_2, role=CourseSessionUser.Role.EXPERT
|
||||||
|
)
|
||||||
|
|
||||||
|
# CS 1 C
|
||||||
|
add_course_session_user(
|
||||||
|
course_session=cs_1_c, user=member_3, role=CourseSessionUser.Role.MEMBER
|
||||||
|
)
|
||||||
|
add_course_session_user(
|
||||||
|
course_session=cs_1_c, user=expert_3, role=CourseSessionUser.Role.EXPERT
|
||||||
|
)
|
||||||
|
|
||||||
|
# CS 2 A
|
||||||
|
add_course_session_user(
|
||||||
|
course_session=cs_2_a, user=member_4, role=CourseSessionUser.Role.MEMBER
|
||||||
|
)
|
||||||
|
add_course_session_user(
|
||||||
|
course_session=cs_2_a, user=expert_4, role=CourseSessionUser.Role.EXPERT
|
||||||
|
)
|
||||||
|
|
||||||
|
# SUPERVISOR of course 1, session a and b BUT NOT
|
||||||
|
# of course 1, session c or course 2, session a
|
||||||
|
cs_1_ab_supervisor = create_user("supervisor")
|
||||||
|
group = create_course_session_group(course_session=cs_1_a)
|
||||||
|
add_course_session_group_course_session(course_session=cs_1_b, group=group)
|
||||||
|
add_course_session_group_supervisor(group=group, user=cs_1_ab_supervisor)
|
||||||
|
|
||||||
|
self.client.force_login(cs_1_ab_supervisor)
|
||||||
|
|
||||||
|
# WHEN
|
||||||
|
query = f"""query($course_id: ID!) {{
|
||||||
|
course_statistics(course_id: $course_id) {{
|
||||||
|
course_session_selection_metrics {{
|
||||||
|
expert_count
|
||||||
|
participant_count
|
||||||
|
session_count
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
}}"""
|
||||||
|
|
||||||
|
variables = {"course_id": str(course_1.id)}
|
||||||
|
response = self.query(query, variables=variables)
|
||||||
|
|
||||||
|
# THEN
|
||||||
|
self.assertResponseNoErrors(response)
|
||||||
|
|
||||||
|
metrics = response.json()["data"]["course_statistics"][
|
||||||
|
"course_session_selection_metrics"
|
||||||
|
]
|
||||||
|
self.assertEqual(metrics["expert_count"], 2)
|
||||||
|
self.assertEqual(metrics["participant_count"], 2)
|
||||||
|
self.assertEqual(metrics["session_count"], 2)
|
||||||
|
|
@ -3,6 +3,11 @@ import csv
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from vbv_lernwelt.core.admin import User
|
from vbv_lernwelt.core.admin import User
|
||||||
|
from vbv_lernwelt.core.constants import (
|
||||||
|
TEST_STUDENT1_USER_ID,
|
||||||
|
TEST_STUDENT2_USER_ID,
|
||||||
|
TEST_TRAINER1_USER_ID,
|
||||||
|
)
|
||||||
from vbv_lernwelt.core.create_default_users import create_default_users
|
from vbv_lernwelt.core.create_default_users import create_default_users
|
||||||
from vbv_lernwelt.course.consts import COURSE_TEST_ID
|
from vbv_lernwelt.course.consts import COURSE_TEST_ID
|
||||||
from vbv_lernwelt.course.creators.test_course import create_test_course
|
from vbv_lernwelt.course.creators.test_course import create_test_course
|
||||||
|
|
@ -19,14 +24,14 @@ class EdoniqUserExportTestCase(TestCase):
|
||||||
create_default_users()
|
create_default_users()
|
||||||
create_test_course(with_sessions=True)
|
create_test_course(with_sessions=True)
|
||||||
|
|
||||||
user1 = User.objects.get(email="test-student1@example.com")
|
user1 = User.objects.get(id=TEST_STUDENT1_USER_ID)
|
||||||
user1.additional_json_data = {
|
user1.additional_json_data = {
|
||||||
"Lehrvertragsnummer": "23456",
|
"Lehrvertragsnummer": "23456",
|
||||||
"Geburtsdatum": "01.01.1991",
|
"Geburtsdatum": "01.01.1991",
|
||||||
}
|
}
|
||||||
user1.save()
|
user1.save()
|
||||||
|
|
||||||
user2 = User.objects.get(email="test-student2@example.com")
|
user2 = User.objects.get(id=TEST_STUDENT2_USER_ID)
|
||||||
user2.additional_json_data = {
|
user2.additional_json_data = {
|
||||||
"Firmenname": "Test AG",
|
"Firmenname": "Test AG",
|
||||||
"Lehrvertragsnummer": "12345",
|
"Lehrvertragsnummer": "12345",
|
||||||
|
|
@ -45,7 +50,7 @@ class EdoniqUserExportTestCase(TestCase):
|
||||||
self.assertEqual(len(users), 2)
|
self.assertEqual(len(users), 2)
|
||||||
|
|
||||||
def test_remove_eiger_versicherungen(self):
|
def test_remove_eiger_versicherungen(self):
|
||||||
user1 = User.objects.get(email="test-student1@example.com")
|
user1 = User.objects.get(id=TEST_STUDENT1_USER_ID)
|
||||||
user1.email = "some@eiger-versicherungen.ch"
|
user1.email = "some@eiger-versicherungen.ch"
|
||||||
user1.save()
|
user1.save()
|
||||||
users = fetch_course_session_users(
|
users = fetch_course_session_users(
|
||||||
|
|
@ -58,7 +63,7 @@ class EdoniqUserExportTestCase(TestCase):
|
||||||
self.assertEqual(len(users), 5)
|
self.assertEqual(len(users), 5)
|
||||||
|
|
||||||
def test_deduplicates_users(self):
|
def test_deduplicates_users(self):
|
||||||
trainer1 = User.objects.get(email="test-trainer1@example.com")
|
trainer1 = User.objects.get(id=TEST_TRAINER1_USER_ID)
|
||||||
cs_zrh = CourseSession.objects.get(
|
cs_zrh = CourseSession.objects.get(
|
||||||
title="Test Zürich 2022 a",
|
title="Test Zürich 2022 a",
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,8 @@ from rest_framework.decorators import api_view
|
||||||
from vbv_lernwelt.core.models import User
|
from vbv_lernwelt.core.models import User
|
||||||
from vbv_lernwelt.course.consts import COURSE_UK, COURSE_UK_FR, COURSE_UK_IT
|
from vbv_lernwelt.course.consts import COURSE_UK, COURSE_UK_FR, COURSE_UK_IT
|
||||||
from vbv_lernwelt.course.models import CourseSessionUser
|
from vbv_lernwelt.course.models import CourseSessionUser
|
||||||
from vbv_lernwelt.course.permissions import has_course_access_by_page_request
|
|
||||||
from vbv_lernwelt.edoniq_test.edoniq_sso import create_token
|
from vbv_lernwelt.edoniq_test.edoniq_sso import create_token
|
||||||
|
from vbv_lernwelt.iam.permissions import has_course_access_by_page_request
|
||||||
from vbv_lernwelt.learnpath.models import LearningContentEdoniqTest
|
from vbv_lernwelt.learnpath.models import LearningContentEdoniqTest
|
||||||
|
|
||||||
logger = structlog.get_logger(__name__)
|
logger = structlog.get_logger(__name__)
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,12 @@ from graphene.types.generic import GenericScalar
|
||||||
from graphene_django.types import ErrorType
|
from graphene_django.types import ErrorType
|
||||||
|
|
||||||
from vbv_lernwelt.course.models import CourseSession
|
from vbv_lernwelt.course.models import CourseSession
|
||||||
from vbv_lernwelt.course.permissions import has_course_session_access
|
|
||||||
from vbv_lernwelt.feedback.graphql.types import (
|
from vbv_lernwelt.feedback.graphql.types import (
|
||||||
FeedbackResponseObjectType as FeedbackResponseType,
|
FeedbackResponseObjectType as FeedbackResponseType,
|
||||||
)
|
)
|
||||||
from vbv_lernwelt.feedback.serializers import CourseFeedbackSerializer
|
from vbv_lernwelt.feedback.serializers import CourseFeedbackSerializer
|
||||||
from vbv_lernwelt.feedback.services import update_feedback_response
|
from vbv_lernwelt.feedback.services import update_feedback_response
|
||||||
|
from vbv_lernwelt.iam.permissions import has_course_session_access
|
||||||
from vbv_lernwelt.learnpath.models import LearningContentFeedback
|
from vbv_lernwelt.learnpath.models import LearningContentFeedback
|
||||||
|
|
||||||
logger = structlog.get_logger(__name__)
|
logger = structlog.get_logger(__name__)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
|
from vbv_lernwelt.core.constants import ADMIN_USER_ID
|
||||||
|
from vbv_lernwelt.course.models import CourseSessionUser
|
||||||
|
|
||||||
|
|
||||||
|
def feedback_users(course_session_id):
|
||||||
|
"""
|
||||||
|
Solely accept feedback originating from members of the course session and the illustrious
|
||||||
|
administrative user, who serves as the repository for feedbacks heretofore submitted anonymously ;-)
|
||||||
|
"""
|
||||||
|
return CourseSessionUser.objects.filter(
|
||||||
|
Q(course_session_id=course_session_id, role=CourseSessionUser.Role.MEMBER)
|
||||||
|
| Q(user__id=ADMIN_USER_ID)
|
||||||
|
).values_list("user", flat=True)
|
||||||
|
|
@ -5,8 +5,9 @@ from rest_framework.decorators import api_view
|
||||||
from rest_framework.exceptions import PermissionDenied
|
from rest_framework.exceptions import PermissionDenied
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from vbv_lernwelt.course.permissions import is_course_session_expert
|
|
||||||
from vbv_lernwelt.feedback.models import FeedbackResponse
|
from vbv_lernwelt.feedback.models import FeedbackResponse
|
||||||
|
from vbv_lernwelt.feedback.utils import feedback_users
|
||||||
|
from vbv_lernwelt.iam.permissions import is_course_session_expert
|
||||||
|
|
||||||
logger = structlog.get_logger(__name__)
|
logger = structlog.get_logger(__name__)
|
||||||
|
|
||||||
|
|
@ -57,11 +58,7 @@ def get_feedback_for_circle(request, course_session_id, circle_id):
|
||||||
course_session__id=course_session_id,
|
course_session__id=course_session_id,
|
||||||
submitted=True,
|
submitted=True,
|
||||||
circle_id=circle_id,
|
circle_id=circle_id,
|
||||||
# filter out experts that might have submitted just for testing
|
feedback_user__in=feedback_users(course_session_id),
|
||||||
# important: the commented code causes to return no feedbacks with prod data
|
|
||||||
# feedback_user__in=CourseSessionUser.objects.filter(
|
|
||||||
# course_session_id=course_session_id, role=CourseSessionUser.Role.MEMBER
|
|
||||||
# ).values_list("user", flat=True),
|
|
||||||
).order_by("created_at")
|
).order_by("created_at")
|
||||||
|
|
||||||
# I guess this is ok for the üK case
|
# I guess this is ok for the üK case
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
|
from vbv_lernwelt.core.models import User
|
||||||
from vbv_lernwelt.course.models import CourseSession, CourseSessionUser
|
from vbv_lernwelt.course.models import CourseSession, CourseSessionUser
|
||||||
|
from vbv_lernwelt.course_session_group.models import CourseSessionGroup
|
||||||
from vbv_lernwelt.learnpath.models import LearningSequence
|
from vbv_lernwelt.learnpath.models import LearningSequence
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -10,38 +12,40 @@ def has_course_access(user, course_id):
|
||||||
if user.is_superuser:
|
if user.is_superuser:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if CourseSessionUser.objects.filter(
|
if CourseSessionGroup.objects.filter(
|
||||||
course_session__course_id=course_id, user=user
|
course_session__course_id=course_id, supervisor=user
|
||||||
).exists():
|
).exists():
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return CourseSessionUser.objects.filter(
|
||||||
|
course_session__course_id=course_id, user=user
|
||||||
|
).exists()
|
||||||
|
|
||||||
|
|
||||||
def has_course_session_access(user, course_session_id: int):
|
def has_course_session_access(user, course_session_id: int):
|
||||||
if user.is_superuser:
|
if user.is_superuser:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if CourseSessionUser.objects.filter(
|
return CourseSessionUser.objects.filter(
|
||||||
course_session_id=course_session_id, user=user
|
course_session_id=course_session_id, user=user
|
||||||
).exists():
|
).exists()
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def is_course_session_expert(user, course_session_id: int):
|
def is_course_session_expert(user, course_session_id: int):
|
||||||
if user.is_superuser:
|
if user.is_superuser:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if CourseSessionUser.objects.filter(
|
is_supervisor = CourseSessionGroup.objects.filter(
|
||||||
|
supervisor=user, course_session__id=course_session_id
|
||||||
|
).exists()
|
||||||
|
|
||||||
|
is_expert = CourseSessionUser.objects.filter(
|
||||||
course_session_id=course_session_id,
|
course_session_id=course_session_id,
|
||||||
user=user,
|
user=user,
|
||||||
role=CourseSessionUser.Role.EXPERT,
|
role=CourseSessionUser.Role.EXPERT,
|
||||||
).exists():
|
).exists()
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
return is_supervisor or is_expert
|
||||||
|
|
||||||
|
|
||||||
def course_sessions_for_user_qs(user):
|
def course_sessions_for_user_qs(user):
|
||||||
|
|
@ -64,12 +68,41 @@ def is_circle_expert(user, course_session_id: int, learning_sequence_id: int) ->
|
||||||
|
|
||||||
circle_id = learning_sequence.get_parent().circle.id
|
circle_id = learning_sequence.get_parent().circle.id
|
||||||
|
|
||||||
if CourseSessionUser.objects.filter(
|
return CourseSessionUser.objects.filter(
|
||||||
course_session_id=course_session_id,
|
course_session_id=course_session_id,
|
||||||
user=user,
|
user=user,
|
||||||
role=CourseSessionUser.Role.EXPERT,
|
role=CourseSessionUser.Role.EXPERT,
|
||||||
expert__id=circle_id,
|
expert__id=circle_id,
|
||||||
|
).exists()
|
||||||
|
|
||||||
|
|
||||||
|
def can_view_course_session_group_statistics(
|
||||||
|
user: User, group: CourseSessionGroup
|
||||||
|
) -> bool:
|
||||||
|
if user.is_superuser:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return user in group.supervisor.all()
|
||||||
|
|
||||||
|
|
||||||
|
def can_view_course_session_progress(user: User, course_session: CourseSession) -> bool:
|
||||||
|
return CourseSessionUser.objects.filter(
|
||||||
|
course_session=course_session,
|
||||||
|
user=user,
|
||||||
|
role=CourseSessionUser.Role.MEMBER,
|
||||||
|
).exists()
|
||||||
|
|
||||||
|
|
||||||
|
def can_view_course_session(user: User, course_session: CourseSession) -> bool:
|
||||||
|
if user.is_superuser:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if CourseSessionGroup.objects.filter(
|
||||||
|
course_session=course_session, supervisor=user
|
||||||
).exists():
|
).exists():
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return CourseSessionUser.objects.filter(
|
||||||
|
course_session=course_session,
|
||||||
|
user=user,
|
||||||
|
).exists()
|
||||||
|
|
@ -5,6 +5,7 @@ from django.utils.timezone import make_naive
|
||||||
from openpyxl.reader.excel import load_workbook
|
from openpyxl.reader.excel import load_workbook
|
||||||
|
|
||||||
from vbv_lernwelt.assignment.models import AssignmentType
|
from vbv_lernwelt.assignment.models import AssignmentType
|
||||||
|
from vbv_lernwelt.core.constants import TEST_TRAINER1_USER_ID
|
||||||
from vbv_lernwelt.core.create_default_users import create_default_users
|
from vbv_lernwelt.core.create_default_users import create_default_users
|
||||||
from vbv_lernwelt.core.models import User
|
from vbv_lernwelt.core.models import User
|
||||||
from vbv_lernwelt.course.creators.test_course import create_test_course
|
from vbv_lernwelt.course.creators.test_course import create_test_course
|
||||||
|
|
@ -127,7 +128,7 @@ class CreateOrUpdateCourseSessionTestCase(TestCase):
|
||||||
self.course, data, language="de", circle_keys=["Fahrzeug"]
|
self.course, data, language="de", circle_keys=["Fahrzeug"]
|
||||||
)
|
)
|
||||||
|
|
||||||
trainer1 = User.objects.get(email="test-trainer1@example.com")
|
trainer1 = User.objects.get(id=TEST_TRAINER1_USER_ID)
|
||||||
csu = CourseSessionUser.objects.create(
|
csu = CourseSessionUser.objects.create(
|
||||||
course_session=cs,
|
course_session=cs,
|
||||||
user=trainer1,
|
user=trainer1,
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,18 @@
|
||||||
</label>
|
</label>
|
||||||
<div style="margin-bottom: 8px; padding: 4px; border-bottom: 1px lightblue solid"></div>
|
<div style="margin-bottom: 8px; padding: 4px; border-bottom: 1px lightblue solid"></div>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="create_course_completion_performance_criteria" value="true">
|
||||||
|
create_course_completion_performance_criteria
|
||||||
|
</label>
|
||||||
|
<div style="margin-bottom: 8px; padding: 4px; border-bottom: 1px lightblue solid"></div>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="create_attendance_days" value="true">
|
||||||
|
create_attendance_days
|
||||||
|
</label>
|
||||||
|
<div style="margin-bottom: 8px; padding: 4px; border-bottom: 1px lightblue solid"></div>
|
||||||
|
|
||||||
<button class="btn">Testdaten zurück setzen</button>
|
<button class="btn">Testdaten zurück setzen</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue