Merged develop into feature/VBV-654-test-users

This commit is contained in:
Christian Cueni 2024-02-28 10:05:40 +00:00
commit 5e5b274add
19 changed files with 1024 additions and 293 deletions

View File

@ -97,12 +97,12 @@ js-linting: &js-linting
default-steps: &default-steps
- parallel:
- step: *e2e
- step: *e2e
- step: *python-tests
- step: *python-linting
- step: *js-tests
- step: *js-linting
- step: *e2e
- step: *e2e
- step: *python-tests
- step: *python-linting
- step: *js-tests
- step: *js-linting
# main pipelines definitions
pipelines:
@ -132,16 +132,16 @@ pipelines:
script:
- echo "Release ready!"
- parallel:
- step:
<<: *deploy
name: deploy prod
deployment: prod
trigger: manual
- step:
<<: *deploy
name: deploy prod-azure
deployment: prod-azure
trigger: manual
- step:
<<: *deploy
name: deploy prod
deployment: prod
trigger: manual
- step:
<<: *deploy
name: deploy prod-azure
deployment: prod-azure
trigger: manual
custom:
deploy-feature-branch:
- step:

View File

@ -0,0 +1,113 @@
<script setup lang="ts">
import { computed } from "vue";
import type { LearningUnitSummary } from "@/services/selfEvaluationFeedback";
import SmileyCell from "@/components/selfEvaluationFeedback/SmileyCell.vue";
const props = defineProps<{
summary: LearningUnitSummary;
}>();
const hasFeedbackReceived = computed(() => {
return props.summary.feedback_assessment?.submitted_by_provider ?? false;
});
const feedbackProviderAvatar = computed(() => {
return props.summary.feedback_assessment?.provider_user.avatar_url ?? "";
});
const feedbackProviderName = computed(() => {
if (!props.summary.feedback_assessment?.provider_user) {
return "";
} else {
return `${props.summary.feedback_assessment.provider_user.first_name} ${props.summary.feedback_assessment.provider_user.last_name}`;
}
});
</script>
<template>
<div class="bg-white" data-cy>
<!-- Top Row -->
<div class="flex items-center justify-between border-b-2 border-gray-200 p-4">
<div class="flex flex-col">
<b>{{ props.summary.title }}</b>
<span>Circle «{{ props.summary.circle_title }}»</span>
</div>
<span class="underline">
<router-link
:to="props.summary.detail_url"
:data-cy="`self-eval-${summary.id}-detail-url`"
>
{{ $t("a.Selbsteinschätzung anschauen") }}
</router-link>
</span>
</div>
<div class="ml-4 mr-4">
<!-- Self Assessment Row-->
<div class="flex pb-2 pt-2">
<div class="w-1/2">
{{ $t("a.Deine Selbsteinschätzung") }}
</div>
<div class="cell">
<SmileyCell
:count="props.summary.self_assessment.counts.pass"
:cypress-identifier="`self-eval-${props.summary.id}-pass`"
smiley="it-icon-smiley-happy"
/>
</div>
<div class="cell">
<SmileyCell
:count="props.summary.self_assessment.counts.fail"
:cypress-identifier="`self-eval-${props.summary.id}-fail`"
smiley="it-icon-smiley-thinking"
/>
</div>
<div class="cell">
<SmileyCell
:count="props.summary.self_assessment.counts.unknown"
:cypress-identifier="`self-eval-${props.summary.id}-unknown`"
smiley="it-icon-smiley-neutral"
/>
</div>
</div>
<!-- Feedback Assessment Row -->
<div v-if="hasFeedbackReceived" class="border-t-2 border-gray-200">
<div class="flex pb-2 pt-2">
<div class="flex w-1/2 items-center">
<span>
{{
$t("a.Fremdeinschätzung von FEEDBACK_PROVIDER_NAME", {
FEEDBACK_PROVIDER_NAME: feedbackProviderName,
})
}}
</span>
<img class="ml-2 h-7 w-7 rounded-full" :src="feedbackProviderAvatar" />
</div>
<div class="cell">
<SmileyCell
:count="props.summary.feedback_assessment?.counts.pass ?? 0"
smiley="it-icon-smiley-happy"
/>
</div>
<div class="cell">
<SmileyCell
:count="props.summary.feedback_assessment?.counts.fail ?? 0"
smiley="it-icon-smiley-thinking"
/>
</div>
<div class="cell">
<SmileyCell
:count="props.summary.feedback_assessment?.counts.unknown ?? 0"
smiley="it-icon-smiley-neutral"
/>
</div>
</div>
</div>
</div>
</div>
</template>
<style lang="postcss" scoped>
.cell {
@apply w-12;
}
</style>

View File

@ -0,0 +1,24 @@
<script setup lang="ts">
import log from "loglevel";
const props = defineProps<{
count: number;
smiley: string;
cypressIdentifier?: string;
}>();
log.info("Rendering SmileyCell:", props.cypressIdentifier);
</script>
<template>
<template v-if="count > 0">
<div class="flex items-center justify-center">
<component :is="smiley" class="mr-1 inline-block h-6 w-6"></component>
<p class="inline-block w-6" :data-cy="cypressIdentifier">
{{ count }}
</p>
</div>
</template>
</template>
<style scoped></style>

View File

@ -37,7 +37,7 @@ const dropdownSelected = computed<DropdownSelectable>({
<template>
<Listbox v-model="dropdownSelected" as="div">
<div class="relative mt-1 w-full">
<div class="relative w-full">
<ListboxButton
class="relative flex w-full cursor-default flex-row items-center bg-white py-3 pl-5 pr-10 text-left"
:class="{

View File

@ -4,14 +4,15 @@ import { COMPETENCE_NAVI_CERTIFICATE_QUERY } from "@/graphql/queries";
import { useQuery } from "@urql/vue";
import { computed } from "vue";
import type { CompetenceCertificate } from "@/types";
import { useCurrentCourseSession, useCourseDataWithCompletion } from "@/composables";
import { useCurrentCourseSession } from "@/composables";
import {
assignmentsMaxEvaluationPoints,
assignmentsUserPoints,
competenceCertificateProgressStatusCount,
} from "@/pages/competence/utils";
import { useSelfEvaluationFeedbackSummaries } from "@/services/selfEvaluationFeedback";
import ItProgress from "@/components/ui/ItProgress.vue";
import { calcPerformanceCriteriaStatusCount } from "@/services/competence";
import { VV_COURSE_IDS } from "@/constants";
const props = defineProps<{
courseSlug: string;
@ -20,7 +21,6 @@ const props = defineProps<{
log.debug("CompetenceIndexPage setup", props);
const courseSession = useCurrentCourseSession();
const courseData = useCourseDataWithCompletion(props.courseSlug);
const certificatesQuery = useQuery({
query: COMPETENCE_NAVI_CERTIFICATE_QUERY,
@ -49,16 +49,41 @@ const userPointsEvaluatedAssignments = computed(() => {
return assignmentsUserPoints(allAssignments.value);
});
const performanceCriteriaStatusCount = computed(() => {
return calcPerformanceCriteriaStatusCount(courseData.flatPerformanceCriteria.value);
const selfEvaluationFeedbackSummaries = useSelfEvaluationFeedbackSummaries(
useCurrentCourseSession().value.id
);
const selfAssessmentCounts = computed(
() => selfEvaluationFeedbackSummaries.aggregates.value?.self_assessment
);
const feedbackEvaluationCounts = computed(
() => selfEvaluationFeedbackSummaries.aggregates.value?.feedback_assessment
);
const isFeedbackEvaluationVisible = computed(
() =>
selfEvaluationFeedbackSummaries.aggregates.value?.feedback_assessment_visible ??
false
);
// FIXME 22.02.24: To-be-tackled NEXT in a separate PR (shippable member comp.navi)
// -> Do not use the VV_COURSE_ID anymore (discuss with @chrigu) -> We do this next.
const currentCourseSession = useCurrentCourseSession();
const hasCompetenceCertificates = computed(() => {
return !VV_COURSE_IDS.includes(currentCourseSession.value.course.id);
});
const isLoaded = computed(
() =>
!selfEvaluationFeedbackSummaries.loading.value && !certificatesQuery.fetching.value
);
</script>
<template>
<div class="container-large lg:mt-4">
<h1 class="mb-8">{{ $t("a.KompetenzNavi") }}</h1>
<section class="mb-4 bg-white p-8">
<div v-if="isLoaded" class="container-large lg:mt-4">
<!-- Competence certificates -->
<section v-if="hasCompetenceCertificates" class="mb-4 bg-white p-8">
<div class="flex items-center">
<h3>{{ $t("a.Kompetenznachweise") }}</h3>
</div>
@ -80,7 +105,7 @@ const performanceCriteriaStatusCount = computed(() => {
<div
v-for="certificate in competenceCertificates"
:key="certificate.id"
class="flex flex-col justify-between border-b py-4 first:border-t lg:flex-row lg:items-center"
class="flex flex-col justify-between py-4 lg:flex-row lg:items-center"
:data-cy="`certificate-${certificate.slug}`"
>
<div class="text-bold text-xl">
@ -130,59 +155,96 @@ const performanceCriteriaStatusCount = computed(() => {
</div>
</section>
<!-- Self-evaluation -->
<section class="mb-4 bg-white px-8 py-4 lg:mb-8 lg:py-8">
<h3 class="mb-4 border-b pb-4 lg:border-0 lg:pb-0">
{{ $t("a.Selbsteinschätzungen") }}
</h3>
<ul
class="mb-6 flex flex-col lg:flex-row lg:items-center lg:justify-between lg:gap-8"
>
<li
class="mb-4 inline-block flex-1 border-b pb-4 lg:mb-0 lg:w-1/3 lg:border-b-0 lg:border-r lg:pb-0"
<div class="mb-8">
<h3 class="mb-4 pb-4 lg:pb-0">
{{ $t("a.Selbsteinschätzungen") }}
</h3>
<ul
class="mb-6 flex flex-col lg:flex-row lg:items-center lg:justify-between lg:gap-8"
>
<h5 class="mb-4 text-gray-700">«{{ $t("selfEvaluation.no") }}»</h5>
<div class="flex flex-row items-center">
<it-icon-smiley-thinking class="h-16 w-16"></it-icon-smiley-thinking>
<p
class="ml-4 inline-block text-7xl font-bold"
data-cy="self-evaluation-fail"
>
{{ performanceCriteriaStatusCount.FAIL }}
</p>
</div>
</li>
<li
class="mb-4 inline-block flex-1 border-b pb-4 lg:mb-0 lg:w-1/3 lg:border-b-0 lg:border-r lg:pb-0"
<li class="mb-4 inline-block flex-1 pb-4 lg:mb-0 lg:w-1/3 lg:pb-0">
<h5 class="mb-4 text-gray-700">«{{ $t("selfEvaluation.no") }}»</h5>
<div class="flex flex-row items-center">
<it-icon-smiley-thinking class="h-16 w-16"></it-icon-smiley-thinking>
<p
class="ml-4 inline-block text-7xl font-bold"
data-cy="self-evaluation-fail"
>
{{ selfAssessmentCounts?.fail }}
</p>
</div>
</li>
<li class="mb-4 inline-block flex-1 pb-4 lg:mb-0 lg:w-1/3 lg:pb-0">
<h5 class="mb-4 text-gray-700">«{{ $t("selfEvaluation.yes") }}»</h5>
<div class="flex flex-row items-center">
<it-icon-smiley-happy class="h-16 w-16"></it-icon-smiley-happy>
<p
class="ml-4 inline-block text-7xl font-bold"
data-cy="self-evaluation-success"
>
{{ selfAssessmentCounts?.pass }}
</p>
</div>
</li>
<li class="flex-1 pb-4 lg:mb-0 lg:w-1/3 lg:pb-0">
<h5 class="mb-4 text-gray-700">{{ $t("competences.notAssessed") }}</h5>
<div class="flex flex-row items-center">
<it-icon-smiley-neutral class="h-16 w-16"></it-icon-smiley-neutral>
<p
class="ml-4 inline-block text-7xl font-bold"
data-cy="self-evaluation-unknown"
>
{{ selfAssessmentCounts?.unknown }}
</p>
</div>
</li>
</ul>
</div>
<!-- Feedback evaluation -->
<div v-if="isFeedbackEvaluationVisible" class="mb-8 border-t pt-8">
<h3 class="mb-4 pb-4 lg:pb-0">
{{ $t("a.Fremdeinschätzungen") }}
</h3>
<ul
class="mb-6 flex flex-col lg:flex-row lg:items-center lg:justify-between lg:gap-8"
>
<h5 class="mb-4 text-gray-700">«{{ $t("selfEvaluation.yes") }}»</h5>
<div class="flex flex-row items-center">
<it-icon-smiley-happy class="h-16 w-16"></it-icon-smiley-happy>
<p
class="ml-4 inline-block text-7xl font-bold"
data-cy="self-evaluation-success"
>
{{ performanceCriteriaStatusCount.SUCCESS }}
</p>
</div>
</li>
<li class="flex-1 border-b pb-4 lg:mb-0 lg:w-1/3 lg:border-b-0 lg:pb-0">
<h5 class="mb-4 text-gray-700">{{ $t("competences.notAssessed") }}</h5>
<div class="flex flex-row items-center">
<it-icon-smiley-neutral class="h-16 w-16"></it-icon-smiley-neutral>
<p
class="ml-4 inline-block text-7xl font-bold"
data-cy="self-evaluation-unknown"
>
{{ performanceCriteriaStatusCount.UNKNOWN }}
</p>
</div>
</li>
</ul>
<li class="mb-4 inline-block flex-1 pb-4 lg:mb-0 lg:w-1/3 lg:pb-0">
<h5 class="mb-4 text-gray-700">«{{ $t("receivedEvaluation.no") }}»</h5>
<div class="flex flex-row items-center">
<it-icon-smiley-thinking class="h-16 w-16"></it-icon-smiley-thinking>
<p class="ml-4 inline-block text-7xl font-bold">
{{ feedbackEvaluationCounts?.fail }}
</p>
</div>
</li>
<li class="mb-4 inline-block flex-1 pb-4 lg:mb-0 lg:w-1/3 lg:pb-0">
<h5 class="mb-4 text-gray-700">«{{ $t("receivedEvaluation.yes") }}»</h5>
<div class="flex flex-row items-center">
<it-icon-smiley-happy class="h-16 w-16"></it-icon-smiley-happy>
<p class="ml-4 inline-block text-7xl font-bold">
{{ feedbackEvaluationCounts?.pass }}
</p>
</div>
</li>
<li class="flex-1 pb-4 lg:mb-0 lg:w-1/3 lg:pb-0">
<h5 class="mb-4 text-gray-700">{{ $t("competences.notAssessed") }}</h5>
<div class="flex flex-row items-center">
<it-icon-smiley-neutral class="h-16 w-16"></it-icon-smiley-neutral>
<p class="ml-4 inline-block text-7xl font-bold">
{{ feedbackEvaluationCounts?.unknown }}
</p>
</div>
</li>
</ul>
</div>
<div>
<router-link
:to="`/course/${props.courseSlug}/competence/criteria`"
class="btn-text mt-4 inline-flex items-center py-2 pl-0"
:to="`/course/${props.courseSlug}/competence/self-evaluation-and-feedback`"
class="btn-text inline-flex items-center py-2 pl-0"
>
<span>{{ $t("general.showAll") }}</span>
<it-icon-arrow-right></it-icon-arrow-right>

View File

@ -1,7 +1,9 @@
<script setup lang="ts">
import * as log from "loglevel";
import { onMounted } from "vue";
import { computed, onMounted } from "vue";
import { useRoute } from "vue-router";
import { VV_COURSE_IDS } from "@/constants";
import { useCurrentCourseSession } from "@/composables";
log.debug("CompetenceParentPage created");
@ -19,14 +21,21 @@ function routeInCompetenceCertificate() {
return route.path.includes("/certificate");
}
function routeInPerformanceCriteria() {
return route.path.endsWith("/criteria");
}
function routeInActionCompetences() {
return route.path.endsWith("/competences");
}
function routeInSelfEvaluationAndFeedback() {
return route.path.endsWith("/self-evaluation-and-feedback");
}
// FIXME 22.02.24: To-be-tackled NEXT in a separate PR (shippable member comp.navi)
// -> Do not use the VV_COURSE_ID anymore (discuss with @chrigu) -> We do this next.
const currentCourseSession = useCurrentCourseSession();
const isVVCourse = computed(() => {
return VV_COURSE_IDS.includes(currentCourseSession.value.course.id);
});
onMounted(async () => {
log.debug("CompetenceParentPage mounted", props.courseSlug);
});
@ -45,6 +54,7 @@ onMounted(async () => {
</router-link>
</li>
<li
v-if="!isVVCourse"
class="border-t-2 border-t-transparent lg:ml-12"
:class="{ 'border-b-2 border-b-blue-900': routeInCompetenceCertificate() }"
>
@ -57,16 +67,21 @@ onMounted(async () => {
</li>
<li
class="border-t-2 border-t-transparent lg:ml-12"
:class="{ 'border-b-2 border-b-blue-900': routeInPerformanceCriteria() }"
:class="{
'border-b-2 border-b-blue-900': routeInSelfEvaluationAndFeedback(),
}"
>
<router-link
:to="`/course/${courseSlug}/competence/criteria`"
:to="`/course/${courseSlug}/competence/self-evaluation-and-feedback`"
class="block py-3"
>
{{ $t("a.Selbsteinschätzungen") }}
{{
isVVCourse
? $t("a.Selbst- und Fremdeinschätzungen")
: $t("a.Selbsteinschätzungen")
}}
</router-link>
</li>
<li
class="border-t-2 border-t-transparent lg:ml-12"
:class="{ 'border-b-2 border-b-blue-900': routeInActionCompetences() }"

View File

@ -1,115 +0,0 @@
<script setup lang="ts">
import * as log from "loglevel";
import { computed } from "vue";
import _ from "lodash";
import { useCourseDataWithCompletion } from "@/composables";
const props = defineProps<{
courseSlug: string;
}>();
log.debug("PerformanceCriteriaPage created", props);
const courseCompletionData = useCourseDataWithCompletion(props.courseSlug);
const uniqueLearningUnits = computed(() => {
// FIXME: this complex calculation can go away,
// once the criteria are in its own learning content
// get the learningUnits sorted by circle order in the course
const circles = (courseCompletionData.circles.value ?? []).map((c, index) => {
return { ...c, sortKey: index };
});
return _.orderBy(
_.uniqBy(
(courseCompletionData.flatPerformanceCriteria.value ?? [])
.filter((pc) => Boolean(pc.learning_unit))
.map((pc) => {
return {
luId: pc.learning_unit?.id,
luTitle: pc.learning_unit?.title,
luSlug: pc.learning_unit?.slug,
circleId: pc.circle.id,
circleTitle: pc.circle.title,
url: pc.learning_unit?.evaluate_url,
sortKey: circles.find((c) => c.id === pc.circle.id)?.sortKey,
};
}),
"luId"
),
"sortKey"
);
});
const criteriaByLearningUnit = computed(() => {
return uniqueLearningUnits.value.map((lu) => {
const criteria = (courseCompletionData.flatPerformanceCriteria.value ?? []).filter(
(pc) => pc.learning_unit?.id === lu.luId
);
return {
...lu,
countSuccess: criteria.filter((c) => c.completion_status === "SUCCESS").length,
countFail: criteria.filter((c) => c.completion_status === "FAIL").length,
countUnknown: criteria.filter((c) => c.completion_status === "UNKNOWN").length,
criteria: criteria,
};
});
});
</script>
<template>
<div class="container-large">
<h2 class="mb-4 lg:py-4">{{ $t("a.Selbsteinschätzungen") }}</h2>
<section class="mb-4 bg-white px-4 py-2">
<div
v-for="selfEvaluation in criteriaByLearningUnit"
:key="selfEvaluation.luId"
class="flex flex-col justify-between gap-4 border-b py-4 last:border-b-0 lg:flex-row lg:items-center"
>
<div class="lg:w-1/3">
{{ $t("a.Circle") }}
{{ selfEvaluation.circleTitle }}:
{{ selfEvaluation.luTitle }}
</div>
<div class="flex flex-row items-center lg:w-1/3">
<div class="mr-6 flex flex-row items-center">
<it-icon-smiley-thinking
class="mr-2 inline-block h-8 w-8"
></it-icon-smiley-thinking>
<div class="w-6" :data-cy="`${selfEvaluation.luSlug}-fail`">
{{ selfEvaluation.countFail }}
</div>
</div>
<li class="mr-6 flex flex-row items-center">
<it-icon-smiley-happy
class="mr-2 inline-block h-8 w-8"
></it-icon-smiley-happy>
<div class="w-6" :data-cy="`${selfEvaluation.luSlug}-success`">
{{ selfEvaluation.countSuccess }}
</div>
</li>
<li class="flex flex-row items-center">
<it-icon-smiley-neutral
class="mr-2 inline-block h-8 w-8"
></it-icon-smiley-neutral>
<div class="w-6" :data-cy="`${selfEvaluation.luSlug}-unknown`">
{{ selfEvaluation.countUnknown }}
</div>
</li>
</div>
<div>
<router-link
:to="selfEvaluation.url ?? '/'"
class="link"
:data-cy="`${selfEvaluation.luSlug}-open`"
>
{{ $t("a.Selbsteinschätzung anschauen") }}
</router-link>
</div>
</div>
</section>
</div>
</template>
<style scoped></style>

View File

@ -0,0 +1,69 @@
<script setup lang="ts">
import { useSelfEvaluationFeedbackSummaries } from "@/services/selfEvaluationFeedback";
import { useCurrentCourseSession } from "@/composables";
import { computed, ref } from "vue";
import FeedbackByLearningUnitSummary from "@/components/selfEvaluationFeedback/FeedbackByLearningUnitSummary.vue";
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
import { t } from "i18next";
const selfEvaluationFeedbackSummaries = useSelfEvaluationFeedbackSummaries(
useCurrentCourseSession().value.id
);
const isLoaded = computed(() => !selfEvaluationFeedbackSummaries.loading.value);
const selectedCircle = ref({ name: t("a.AlleCircle"), id: "_all" });
const circles = computed(() => [
{ name: t("a.AlleCircle"), id: "_all" },
...selfEvaluationFeedbackSummaries.circles.value.map((circle) => ({
name: `Circle: ${circle.title}`,
id: circle.id,
})),
]);
const summaries = computed(() => {
if (selectedCircle.value.id === "_all") {
return selfEvaluationFeedbackSummaries.summaries.value;
}
return selfEvaluationFeedbackSummaries.summaries.value.filter(
(summary) => summary.circle_id === selectedCircle.value.id
);
});
const headerTitle = computed(() => {
const canHaveFeedback =
selfEvaluationFeedbackSummaries.aggregates.value?.feedback_assessment_visible ??
false;
if (canHaveFeedback) {
return t("a.Selbst- und Fremdeinschätzungen");
} else {
return t("a.Selbsteinschätzungen");
}
});
</script>
<template>
<div v-if="isLoaded">
<div class="container-large">
<div class="col flex items-center justify-between pb-4">
<h2 class="py-4">{{ headerTitle }}</h2>
<ItDropdownSelect
v-model="selectedCircle"
class="text-bold w-24 min-w-[18rem] border-2 border-gray-300"
:items="circles"
borderless
></ItDropdownSelect>
</div>
<div class="space-y-3">
<FeedbackByLearningUnitSummary
v-for="summary in summaries"
:key="summary.id"
:summary="summary"
/>
</div>
</div>
</div>
</template>
<style scoped></style>

View File

@ -45,7 +45,7 @@ const competenceCertificateUrl = computed(() => {
});
const competenceCriteriaUrl = computed(() => {
return `/course/${courseSlug.value}/competence/criteria?courseSessionId=${courseSessionProgress.value?.session_to_continue_id}`;
return `/course/${courseSlug.value}/competence/self-evaluation-and-feedback?courseSessionId=${courseSessionProgress.value?.session_to_continue_id}`;
});
const isVVCourse = computed(() => {

View File

@ -92,7 +92,6 @@ const router = createRouter({
props: true,
component: () => import("@/pages/competence/CompetenceIndexPage.vue"),
},
{
path: "certificates",
props: true,
@ -106,9 +105,10 @@ const router = createRouter({
import("@/pages/competence/CompetenceCertificateDetailPage.vue"),
},
{
path: "criteria",
path: "self-evaluation-and-feedback",
props: true,
component: () => import("@/pages/competence/PerformanceCriteriaPage.vue"),
component: () =>
import("@/pages/competence/SelfEvaluationAndFeedbackPage.vue"),
},
{
path: "competences",

View File

@ -2,6 +2,7 @@ import { useCSRFFetch } from "@/fetchHelpers";
import type { User } from "@/types";
import { toValue } from "@vueuse/core";
import { t } from "i18next";
import log from "loglevel";
import type { Ref } from "vue";
import { computed, onMounted, ref } from "vue";
@ -24,6 +25,45 @@ export interface Criterion {
feedback_assessment: "FAIL" | "SUCCESS" | "UNKNOWN";
}
interface FeedbackSummaryCounts {
pass: number;
fail: number;
unknown: number;
}
export interface FeedbackSummaryAggregates {
// totals across all learning units in the course session
self_assessment: FeedbackSummaryCounts;
feedback_assessment: FeedbackSummaryCounts;
// does this course have any feedback?
feedback_assessment_visible: boolean;
}
interface FeedbackAssessmentSummary {
counts: FeedbackSummaryCounts;
submitted_by_provider: boolean;
provider_user: User;
}
interface SelfAssessmentSummary {
counts: FeedbackSummaryCounts;
}
export interface LearningUnitSummary {
id: string;
title: string;
circle_id: string;
circle_title: string;
feedback_assessment?: FeedbackAssessmentSummary;
self_assessment: SelfAssessmentSummary;
detail_url: string;
}
interface Circle {
id: number;
title: string;
}
/** To keep the backend permissions model simple, we have two endpoints:
* 1. /requester/: for the user who requested the feedback
* 2. /provider/: for the user who provides the feedback
@ -47,7 +87,7 @@ export function useSelfEvaluationFeedback(
error.value = undefined;
loading.value = true;
console.log("Fetching feedback for learning unit", learningUnitId);
log.info("Fetching feedback for learning unit", learningUnitId);
const { data, statusCode, error: _error } = await useCSRFFetch(url.value).json();
loading.value = false;
@ -126,6 +166,52 @@ export function useSelfEvaluationFeedback(
};
}
export function useSelfEvaluationFeedbackSummaries(
courseSessionId: Ref<string> | string
) {
const summaries = ref<LearningUnitSummary[]>([]);
const aggregates = ref<FeedbackSummaryAggregates>();
const circles = ref<Circle[]>([]);
const loading = ref(false);
const error = ref();
const url = computed(
() =>
`/api/self-evaluation-feedback/requester/${courseSessionId}/feedbacks/summaries`
);
const fetchFeedbackSummaries = async () => {
error.value = undefined;
loading.value = true;
log.info("Fetching feedback summaries for course session", courseSessionId);
const { data, error: _error } = await useCSRFFetch(url.value).json();
loading.value = false;
if (_error.value) {
error.value = _error;
summaries.value = [];
circles.value = [];
aggregates.value = undefined;
return;
}
summaries.value = data.value.results;
aggregates.value = data.value.aggregates;
circles.value = data.value.circles;
};
onMounted(fetchFeedbackSummaries);
return {
summaries,
aggregates,
circles,
loading,
error,
};
}
export const getSmiley = (assessment: "FAIL" | "SUCCESS" | "UNKNOWN") => {
switch (assessment) {
case "SUCCESS":

View File

@ -1,4 +1,4 @@
import { login } from "../helpers";
import {login} from "../helpers";
describe("selfEvaluation.cy.js", () => {
beforeEach(() => {
@ -28,37 +28,28 @@ describe("selfEvaluation.cy.js", () => {
cy.get('[data-cy="self-evaluation-success"]').should("have.text", "0");
cy.get('[data-cy="self-evaluation-unknown"]').should("have.text", "4");
// learning unit id = 687 also known as:
// Bedarfsanalyse, Ist- und Soll-Situation <<Reisen>>
const identifier = "self-eval-687"
// data in KompetenzNavi/Selbsteinschätzungen is correct
cy.visit("/course/test-lehrgang/competence/criteria");
cy.get('[data-cy="test-lehrgang-lp-circle-reisen-lu-reisen-fail"]').should(
"have.text",
"0"
);
cy.get(
'[data-cy="test-lehrgang-lp-circle-reisen-lu-reisen-success"]'
).should("have.text", "0");
cy.get(
'[data-cy="test-lehrgang-lp-circle-reisen-lu-reisen-unknown"]'
).should("have.text", "2");
cy.visit("/course/test-lehrgang/competence/self-evaluation-and-feedback");
cy.get(`[data-cy="${identifier}-fail"]`).should("not.exist");
cy.get(`[data-cy="${identifier}-pass"]`).should("not.exist");
cy.get(`[data-cy="${identifier}-unknown"]`).should("have.text", "2");
// it can open self evaluation from within KompetenzNavi
cy.get('[data-cy="test-lehrgang-lp-circle-reisen-lu-reisen-open"]').click();
cy.get(`[data-cy="${identifier}-detail-url"]`).click();
// starting the self evaluation will return to KompetenzNavi
cy.makeSelfEvaluation([true, false]);
cy.url().should("include", "/course/test-lehrgang/competence/criteria");
cy.url().should("include", "/course/test-lehrgang/competence/self-evaluation-and-feedback");
// check data again on KompetenzNavi
cy.get('[data-cy="test-lehrgang-lp-circle-reisen-lu-reisen-fail"]').should(
"have.text",
"1"
);
cy.get(
'[data-cy="test-lehrgang-lp-circle-reisen-lu-reisen-success"]'
).should("have.text", "1");
cy.get(
'[data-cy="test-lehrgang-lp-circle-reisen-lu-reisen-unknown"]'
).should("have.text", "0");
cy.get(`[data-cy="${identifier}-fail"]`).should("have.text", "1");
cy.get(`[data-cy="${identifier}-pass"]`).should("have.text", "1");
cy.get(`[data-cy="${identifier}-unknown"]`).should("not.exist");
// data in KompetenzNavi/Übersicht is correct
cy.visit("/course/test-lehrgang/competence");
@ -76,19 +67,6 @@ describe("selfEvaluation.cy.js", () => {
// starting the self evaluation from circle should return to circle
cy.url().should("include", "/course/test-lehrgang/learn/reisen");
// data in KompetenzNavi / Selbsteinschätzungen is correct
cy.visit("/course/test-lehrgang/competence/criteria");
cy.get('[data-cy="test-lehrgang-lp-circle-reisen-lu-reisen-fail"]').should(
"have.text",
"0"
);
cy.get(
'[data-cy="test-lehrgang-lp-circle-reisen-lu-reisen-success"]'
).should("have.text", "2");
cy.get(
'[data-cy="test-lehrgang-lp-circle-reisen-lu-reisen-unknown"]'
).should("have.text", "0");
});
it("should be able to make a fail self evaluation", () => {
@ -97,19 +75,6 @@ describe("selfEvaluation.cy.js", () => {
cy.get('[data-cy="test-lehrgang-lp-circle-reisen-lu-reisen"]')
.find('[data-cy="fail"]')
.should("exist");
// data in KompetenzNavi / Selbsteinschätzungen is correct
cy.visit("/course/test-lehrgang/competence/criteria");
cy.get('[data-cy="test-lehrgang-lp-circle-reisen-lu-reisen-fail"]').should(
"have.text",
"2"
);
cy.get(
'[data-cy="test-lehrgang-lp-circle-reisen-lu-reisen-success"]'
).should("have.text", "0");
cy.get(
'[data-cy="test-lehrgang-lp-circle-reisen-lu-reisen-unknown"]'
).should("have.text", "0");
});
it("should be able to make a mixed self evaluation", () => {
@ -118,18 +83,5 @@ describe("selfEvaluation.cy.js", () => {
cy.get('[data-cy="test-lehrgang-lp-circle-reisen-lu-reisen"]')
.find('[data-cy="fail"]')
.should("exist");
// data in KompetenzNavi / Selbsteinschätzungen is correct
cy.visit("/course/test-lehrgang/competence/criteria");
cy.get('[data-cy="test-lehrgang-lp-circle-reisen-lu-reisen-fail"]').should(
"have.text",
"1"
);
cy.get(
'[data-cy="test-lehrgang-lp-circle-reisen-lu-reisen-success"]'
).should("have.text", "1");
cy.get(
'[data-cy="test-lehrgang-lp-circle-reisen-lu-reisen-unknown"]'
).should("have.text", "0");
});
});

View File

@ -9,12 +9,16 @@ from vbv_lernwelt.core.constants import (
TEST_COURSE_SESSION_BERN_ID,
TEST_MENTOR1_USER_ID,
TEST_STUDENT1_USER_ID,
TEST_STUDENT1_VV_USER_ID,
TEST_STUDENT2_USER_ID,
TEST_STUDENT3_USER_ID,
TEST_TRAINER1_USER_ID,
)
from vbv_lernwelt.core.models import Organisation, User
from vbv_lernwelt.course.consts import COURSE_TEST_ID
from vbv_lernwelt.course.consts import (
COURSE_TEST_ID,
COURSE_VERSICHERUNGSVERMITTLERIN_ID,
)
from vbv_lernwelt.course.creators.test_course import (
create_edoniq_test_result_data,
create_feedback_response_data,
@ -39,6 +43,10 @@ from vbv_lernwelt.learnpath.models import (
LearningContentFeedbackVV,
)
from vbv_lernwelt.notify.models import Notification
from vbv_lernwelt.self_evaluation_feedback.models import (
CourseCompletionFeedback,
SelfEvaluationFeedback,
)
@click.command()
@ -107,6 +115,9 @@ def command(
FeedbackResponse.objects.all().delete()
CourseSessionAttendanceCourse.objects.all().update(attendance_user_list=[])
SelfEvaluationFeedback.objects.all().delete()
CourseCompletionFeedback.objects.all().delete()
LearningMentor.objects.all().delete()
User.objects.all().update(organisation=Organisation.objects.first())
User.objects.all().update(language="de")
@ -345,19 +356,17 @@ def command(
)
)
# FIXME: Add mentor to VV course as well, once we can:
# -> https://bitbucket.org/iterativ/vbv_lernwelt/pull-requests/287
# vv_course = Course.objects.get(id=COURSE_VERSICHERUNGSVERMITTLERIN_ID)
# vv_course_session = CourseSession.objects.get(course=vv_course)
# vv_mentor = LearningMentor.objects.create(
# course=vv_course,
# mentor=User.objects.get(id=TEST_MENTOR1_USER_ID),
# )
# vv_mentor.participants.add(
# CourseSessionUser.objects.get(
# user__id=TEST_STUDENT1_VV_USER_ID, course_session=vv_course_session
# )
# )
vv_course = Course.objects.get(id=COURSE_VERSICHERUNGSVERMITTLERIN_ID)
vv_course_session = CourseSession.objects.get(course=vv_course)
vv_mentor = LearningMentor.objects.create(
course=vv_course,
mentor=User.objects.get(id=TEST_MENTOR1_USER_ID),
)
vv_mentor.participants.add(
CourseSessionUser.objects.get(
user__id=TEST_STUDENT1_VV_USER_ID, course_session=vv_course_session
)
)
course = Course.objects.get(id=COURSE_TEST_ID)
course.enable_circle_documents = enable_circle_documents

View File

@ -47,6 +47,7 @@ from vbv_lernwelt.learnpath.models import (
LearningContentEdoniqTest,
LearningPath,
LearningUnit,
LearningUnitPerformanceFeedbackType,
)
from vbv_lernwelt.learnpath.tests.learning_path_factories import (
CircleFactory,
@ -275,6 +276,7 @@ def create_learning_unit(
circle: Circle,
course: Course,
course_category_title: str = "Course Category",
feedback_user: LearningUnitPerformanceFeedbackType = LearningUnitPerformanceFeedbackType.NO_FEEDBACK,
) -> LearningUnit:
cat, _ = CourseCategory.objects.get_or_create(
course=course,
@ -285,6 +287,7 @@ def create_learning_unit(
title="Learning Unit",
parent=circle,
course_category=cat,
feedback_user=feedback_user.value,
)
@ -292,7 +295,7 @@ def create_performance_criteria_page(
course: Course,
course_page: CoursePage,
circle: Circle,
learning_unit: LearningUnitFactory | None = None,
learning_unit: LearningUnitFactory | LearningUnit | None = None,
) -> PerformanceCriteria:
competence_navi_page = CompetenceNaviPageFactory(
title="Competence Navi",

View File

@ -54,7 +54,7 @@ def create_vv_new_learning_path(
create_circle_gewinnen(lp)
TopicFactory(title="Beraten und Betreuen von Kunden", parent=lp)
create_circle_fahrzeug(lp)
create_circle_fahrzeug(lp, course_page=course_page)
create_circle_haushalt(lp)
create_circle_rechtsstreitigkeiten(lp)
create_circle_reisen(lp)
@ -340,7 +340,7 @@ def create_circle_gewinnen(lp, title="Gewinnen"):
)
def create_circle_fahrzeug(lp, title="Fahrzeug"):
def create_circle_fahrzeug(lp, title="Fahrzeug", course_page=None):
circle = CircleFactory(
title=title,
parent=lp,
@ -404,7 +404,14 @@ def create_circle_fahrzeug(lp, title="Fahrzeug"):
)
LearningSequenceFactory(title="Transfer", parent=circle, icon="it-icon-ls-end")
LearningUnitFactory(title="Transfer", title_hidden=True, parent=circle)
lu_transfer = LearningUnitFactory(
title="Transfer",
title_hidden=True,
parent=circle,
feedback_user=LearningUnitPerformanceFeedbackType.MENTOR_FEEDBACK.name,
)
LearningContentPlaceholderFactory(
title="Praxisauftrag",
parent=circle,
@ -429,6 +436,36 @@ def create_circle_fahrzeug(lp, title="Fahrzeug"):
parent=circle,
)
competence_profile_page = ActionCompetenceListPageFactory(
title="KompetenzNavi",
parent=course_page,
)
ace = ActionCompetenceFactory(
parent=competence_profile_page,
)
PerformanceCriteriaFactory(
parent=ace,
competence_id="VV-Transfer-A",
title="Ich setze das Gelernte in der Praxis um.",
learning_unit=lu_transfer,
)
PerformanceCriteriaFactory(
parent=ace,
competence_id="VV-Transfer-B",
title="Ich kenne den Unterschied zwischen einem Neuwagen und einem Occasionswagen.",
learning_unit=lu_transfer,
)
PerformanceCriteriaFactory(
parent=ace,
competence_id="VV-Transfer-C",
title="Ich kenne den Unterschied zwischen einem Leasing und einem Kauf.",
learning_unit=lu_transfer,
)
def create_circle_haushalt(lp, title="Haushalt"):
circle = CircleFactory(

View File

@ -15,6 +15,7 @@ from vbv_lernwelt.course.creators.test_utils import (
from vbv_lernwelt.course.models import CourseCompletionStatus, CourseSessionUser
from vbv_lernwelt.course.services import mark_course_completion
from vbv_lernwelt.learning_mentor.models import LearningMentor
from vbv_lernwelt.learnpath.models import LearningUnitPerformanceFeedbackType
from vbv_lernwelt.self_evaluation_feedback.models import (
CourseCompletionFeedback,
SelfEvaluationFeedback,
@ -154,7 +155,9 @@ class SelfEvaluationFeedbackAPI(APITestCase):
"""Tests endpoint of feedback REQUESTER"""
# GIVEN
learning_unit = create_learning_unit(course=self.course, circle=self.circle)
learning_unit = create_learning_unit( # noqa
course=self.course, circle=self.circle
)
performance_criteria_1 = create_performance_criteria_page(
course=self.course,
@ -204,11 +207,11 @@ class SelfEvaluationFeedbackAPI(APITestCase):
feedback = response.data
self.assertEqual(feedback["learning_unit_id"], learning_unit.id)
self.assertEqual(feedback["feedback_submitted"], False)
self.assertEqual(feedback["circle_name"], self.circle.title)
self.assertFalse(feedback["feedback_submitted"])
self.assertEqual(feedback["circle_name"], self.circle.title) # noqa
provider_user = feedback["feedback_provider_user"]
self.assertEqual(provider_user["id"], str(self.mentor.id)) # noqa
self.assertEqual(provider_user["id"], str(self.mentor.id))
self.assertEqual(provider_user["first_name"], self.mentor.first_name)
self.assertEqual(provider_user["last_name"], self.mentor.last_name)
self.assertEqual(provider_user["avatar_url"], self.mentor.avatar_url)
@ -243,11 +246,217 @@ class SelfEvaluationFeedbackAPI(APITestCase):
CourseCompletionStatus.UNKNOWN.value,
)
def test_feedbacks_with_mixed_completion_statuses(self):
"""Case: CourseCompletion AND feedbacks with mixed completion statuses"""
# GIVEN
learning_unit = create_learning_unit(
course=self.course,
circle=self.circle,
feedback_user=LearningUnitPerformanceFeedbackType.MENTOR_FEEDBACK,
)
feedback = create_self_evaluation_feedback(
learning_unit=learning_unit,
feedback_requester_user=self.member,
feedback_provider_user=self.mentor,
)
feedback.feedback_submitted = True
feedback.save()
for status in [
CourseCompletionStatus.SUCCESS,
CourseCompletionStatus.FAIL,
CourseCompletionStatus.UNKNOWN,
]:
criteria_page = create_performance_criteria_page(
course=self.course,
course_page=self.course_page,
circle=self.circle,
learning_unit=learning_unit,
)
# self assessment
completion = mark_course_completion(
page=criteria_page,
user=self.member,
course_session=self.course_session,
completion_status=status.value,
)
# feedback assessment
CourseCompletionFeedback.objects.create(
feedback=feedback,
course_completion=completion,
feedback_assessment=status.value,
)
self.client.force_login(self.member)
# WHEN
response = self.client.get(
reverse(
"get_self_evaluation_feedbacks_as_requester",
args=[self.course_session.id],
)
)
# THEN
self.assertEqual(response.status_code, 200)
result = response.data["results"][0]
self_assessment = result["self_assessment"]
self.assertEqual(self_assessment["counts"]["pass"], 1)
self.assertEqual(self_assessment["counts"]["fail"], 1)
self.assertEqual(self_assessment["counts"]["unknown"], 1)
feedback_assessment = result["feedback_assessment"]
self.assertEqual(feedback_assessment["counts"]["pass"], 1)
self.assertEqual(feedback_assessment["counts"]["fail"], 1)
self.assertEqual(feedback_assessment["counts"]["unknown"], 1)
self.assertTrue(feedback_assessment["submitted_by_provider"])
self.assertEqual(
feedback_assessment["provider_user"]["id"], str(self.mentor.id)
)
aggregate = response.data["aggregates"]
self.assertEqual(aggregate["self_assessment"]["pass"], 1)
self.assertEqual(aggregate["self_assessment"]["fail"], 1)
self.assertEqual(aggregate["self_assessment"]["unknown"], 1)
self.assertEqual(aggregate["feedback_assessment"]["pass"], 1)
self.assertEqual(aggregate["feedback_assessment"]["fail"], 1)
self.assertEqual(aggregate["feedback_assessment"]["unknown"], 1)
def test_no_feedbacks_but_with_completion_status(self):
"""Case: CourseCompletion but NO feedback"""
# GIVEN
learning_unit_with_success_feedback = create_learning_unit(
course=self.course,
circle=self.circle,
feedback_user=LearningUnitPerformanceFeedbackType.MENTOR_FEEDBACK,
)
performance_criteria_page = create_performance_criteria_page(
course=self.course,
course_page=self.course_page,
circle=self.circle,
learning_unit=learning_unit_with_success_feedback,
)
# IMPORTANT: CourseCompletion but NO feedback!
mark_course_completion(
page=performance_criteria_page,
user=self.member,
course_session=self.course_session,
completion_status=CourseCompletionStatus.SUCCESS.value,
)
self.client.force_login(self.member)
# WHEN
response = self.client.get(
reverse(
"get_self_evaluation_feedbacks_as_requester",
args=[self.course_session.id],
)
)
# THEN
self.assertEqual(response.status_code, 200)
result = response.data["results"][0]
counts = result["self_assessment"]["counts"]
self.assertEqual(counts["pass"], 1)
self.assertEqual(counts["fail"], 0)
self.assertEqual(counts["unknown"], 0)
def test_feedbacks_not_started(self):
"""Case: Learning unit with no completion status and no feedback"""
# GIVEN
learning_unit = create_learning_unit( # noqa
course=self.course,
circle=self.circle,
feedback_user=LearningUnitPerformanceFeedbackType.MENTOR_FEEDBACK,
)
create_performance_criteria_page(
course=self.course,
course_page=self.course_page,
circle=self.circle,
learning_unit=learning_unit,
)
self.client.force_login(self.member)
# WHEN
response = self.client.get(
reverse(
"get_self_evaluation_feedbacks_as_requester",
args=[self.course_session.id],
)
)
# THEN
self.assertEqual(response.status_code, 200)
result = response.data["results"][0]
self.assertEqual(result["self_assessment"]["counts"]["pass"], 0)
self.assertEqual(result["self_assessment"]["counts"]["fail"], 0)
self.assertEqual(result["self_assessment"]["counts"]["unknown"], 1)
def test_feedbacks_metadata(self):
# GIVEN
learning_unit = create_learning_unit( # noqa
course=self.course,
circle=self.circle,
feedback_user=LearningUnitPerformanceFeedbackType.MENTOR_FEEDBACK,
)
create_performance_criteria_page(
course=self.course,
course_page=self.course_page,
circle=self.circle,
learning_unit=learning_unit,
)
self.client.force_login(self.member)
# WHEN
response = self.client.get(
reverse(
"get_self_evaluation_feedbacks_as_requester",
args=[self.course_session.id],
)
)
# THEN
self.assertEqual(response.status_code, 200)
result = response.data["results"][0]
self.assertEqual(result["title"], learning_unit.title)
self.assertEqual(result["id"], learning_unit.id)
self.assertEqual(result["circle_id"], self.circle.id)
self.assertEqual(result["circle_title"], self.circle.title)
self.assertEqual(result["detail_url"], learning_unit.get_evaluate_url())
circles = response.data["circles"]
self.assertEqual(len(circles), 1)
self.assertEqual(circles[0]["id"], self.circle.id)
self.assertEqual(circles[0]["title"], self.circle.title)
def test_get_self_evaluation_feedback_as_provider(self):
"""Tests endpoint of feedback PROVIDER"""
# GIVEN
learning_unit = create_learning_unit(course=self.course, circle=self.circle)
learning_unit = create_learning_unit( # noqa
course=self.course, circle=self.circle
)
performance_criteria_1 = create_performance_criteria_page(
course=self.course,
@ -299,7 +508,7 @@ class SelfEvaluationFeedbackAPI(APITestCase):
self.assertEqual(feedback["learning_unit_id"], learning_unit.id)
self.assertEqual(feedback["title"], learning_unit.title)
self.assertEqual(feedback["feedback_submitted"], False)
self.assertEqual(feedback["circle_name"], self.circle.title)
self.assertEqual(feedback["circle_name"], self.circle.title) # noqa
provider_user = feedback["feedback_provider_user"]
self.assertEqual(provider_user["id"], str(self.mentor.id)) # noqa
@ -404,7 +613,7 @@ class SelfEvaluationFeedbackAPI(APITestCase):
self.client.force_login(self.mentor)
# WHEN
response = self.client.put(
self.client.put(
reverse(
"release_self_evaluation_feedback", args=[self_evaluation_feedback.id]
),

View File

@ -4,11 +4,18 @@ from vbv_lernwelt.self_evaluation_feedback.views import (
add_provider_self_evaluation_feedback,
get_self_evaluation_feedback_as_provider,
get_self_evaluation_feedback_as_requester,
get_self_evaluation_feedbacks_as_requester,
release_provider_self_evaluation_feedback,
start_self_evaluation_feedback,
)
urlpatterns = [
# /requester/* URLs -> For the user who requests feedback
path(
"requester/<signed_int:course_session_id>/feedbacks/summaries",
get_self_evaluation_feedbacks_as_requester,
name="get_self_evaluation_feedbacks_as_requester",
),
path(
"requester/<int:learning_unit_id>/feedback/start",
start_self_evaluation_feedback,
@ -19,6 +26,7 @@ urlpatterns = [
get_self_evaluation_feedback_as_requester,
name="get_self_evaluation_feedback_as_requester",
),
# /provider/* URLs -> For the user who is providing feedback
path(
"provider/<int:learning_unit_id>/feedback",
get_self_evaluation_feedback_as_provider,

View File

@ -0,0 +1,126 @@
from typing import NamedTuple
from django.db.models import Case, Count, IntegerField, Sum, Value, When
from django.db.models.functions import Coalesce
from vbv_lernwelt.core.admin import User
from vbv_lernwelt.course.models import CourseCompletion, CourseCompletionStatus
from vbv_lernwelt.learnpath.models import LearningUnit
from vbv_lernwelt.self_evaluation_feedback.models import (
CourseCompletionFeedback,
SelfEvaluationFeedback,
)
class AssessmentCounts(NamedTuple):
pass_count: int
fail_count: int
unknown_count: int
@property
def total_count(self):
return self.pass_count + self.fail_count + self.unknown_count
def get_self_evaluation_feedback_counts(
feedback: SelfEvaluationFeedback,
):
course_completion_feedback = CourseCompletionFeedback.objects.filter(
feedback=feedback
).aggregate(
pass_count=Coalesce(
Sum(
Case(
When(
feedback_assessment=CourseCompletionStatus.SUCCESS.value,
then=Value(1),
),
output_field=IntegerField(),
)
),
Value(0),
),
fail_count=Coalesce(
Sum(
Case(
When(
feedback_assessment=CourseCompletionStatus.FAIL.value,
then=Value(1),
),
output_field=IntegerField(),
)
),
Value(0),
),
unknown_count=Coalesce(
Sum(
Case(
When(
feedback_assessment=CourseCompletionStatus.UNKNOWN.value,
then=Value(1),
),
output_field=IntegerField(),
)
),
Value(0),
),
)
return AssessmentCounts(
pass_count=course_completion_feedback.get("pass_count", 0),
fail_count=course_completion_feedback.get("fail_count", 0),
unknown_count=course_completion_feedback.get("unknown_count", 0),
)
def get_self_assessment_counts(
learning_unit: LearningUnit, user: User
) -> AssessmentCounts:
performance_criteria = learning_unit.performancecriteria_set.all()
completion_counts = CourseCompletion.objects.filter(
page__in=performance_criteria, user=user
).aggregate(
pass_count=Count(
Case(
When(completion_status=CourseCompletionStatus.SUCCESS.value, then=1),
output_field=IntegerField(),
)
),
fail_count=Count(
Case(
When(completion_status=CourseCompletionStatus.FAIL.value, then=1),
output_field=IntegerField(),
)
),
unknown_count=Count(
Case(
When(completion_status=CourseCompletionStatus.UNKNOWN.value, then=1),
output_field=IntegerField(),
)
),
)
pass_count = completion_counts.get("pass_count", 0)
fail_count = completion_counts.get("fail_count", 0)
unknown_count = completion_counts.get("unknown_count", 0)
# not yet completed performance criteria are unknown
if pass_count + fail_count + unknown_count < performance_criteria.count():
unknown_count += performance_criteria.count() - (
pass_count + fail_count + unknown_count
)
return AssessmentCounts(
pass_count=pass_count,
fail_count=fail_count,
unknown_count=unknown_count,
)
def calculate_aggregate(counts: [AssessmentCounts]):
return AssessmentCounts(
pass_count=sum(x.pass_count for x in counts),
fail_count=sum(x.fail_count for x in counts),
unknown_count=sum(x.unknown_count for x in counts),
)

View File

@ -1,3 +1,4 @@
import structlog
from django.shortcuts import get_object_or_404
from rest_framework.decorators import api_view, permission_classes
from rest_framework.exceptions import PermissionDenied
@ -5,9 +6,14 @@ from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from vbv_lernwelt.core.models import User
from vbv_lernwelt.course.models import CourseCompletion
from vbv_lernwelt.core.serializers import UserSerializer
from vbv_lernwelt.course.models import CourseCompletion, CourseSession
from vbv_lernwelt.learning_mentor.models import LearningMentor
from vbv_lernwelt.learnpath.models import LearningUnit
from vbv_lernwelt.learnpath.models import (
Circle,
LearningUnit,
LearningUnitPerformanceFeedbackType,
)
from vbv_lernwelt.notify.services import NotificationService
from vbv_lernwelt.self_evaluation_feedback.models import (
CourseCompletionFeedback,
@ -16,6 +22,13 @@ from vbv_lernwelt.self_evaluation_feedback.models import (
from vbv_lernwelt.self_evaluation_feedback.serializers import (
SelfEvaluationFeedbackSerializer,
)
from vbv_lernwelt.self_evaluation_feedback.utils import (
AssessmentCounts,
get_self_assessment_counts,
get_self_evaluation_feedback_counts,
)
logger = structlog.get_logger(__name__)
@api_view(["POST"])
@ -80,6 +93,126 @@ def get_self_evaluation_feedback_as_provider(request, learning_unit_id):
return Response(SelfEvaluationFeedbackSerializer(feedback).data)
@api_view(["GET"])
@permission_classes([IsAuthenticated])
def get_self_evaluation_feedbacks_as_requester(request, course_session_id: int):
course_session = get_object_or_404(CourseSession, id=course_session_id)
results = []
circle_ids = set()
all_self_assessment_counts = []
all_feedback_assessment_counts = []
for learning_unit in LearningUnit.objects.filter(
course_category__course=course_session.course,
):
# this is not a problem in real life, but in the test environment
# we have a lot of learning units without self assessment criteria
# -> just skip those learning units
if len(learning_unit.performancecriteria_set.all()) == 0:
continue
circle = learning_unit.get_parent().specific
circle_ids.add(circle.id)
feedback = SelfEvaluationFeedback.objects.filter(
learning_unit=learning_unit,
feedback_requester_user=request.user,
).first()
if not feedback:
# no feedback given yet
feedback_assessment = None
else:
# feedback given
feedback_counts = get_self_evaluation_feedback_counts(feedback)
all_feedback_assessment_counts.append(feedback_counts)
feedback_assessment = {
"submitted_by_provider": feedback.feedback_submitted,
"provider_user": UserSerializer(feedback.feedback_provider_user).data,
"counts": {
"pass": feedback_counts.pass_count,
"fail": feedback_counts.fail_count,
"unknown": feedback_counts.unknown_count,
},
}
self_assessment_counts = get_self_assessment_counts(learning_unit, request.user)
all_self_assessment_counts.append(self_assessment_counts)
results.append(
{
"id": learning_unit.id,
"title": learning_unit.title,
"detail_url": learning_unit.get_evaluate_url(),
"circle_id": circle.id,
"circle_title": circle.title,
"feedback_assessment": feedback_assessment,
"self_assessment": {
"counts": {
"pass": self_assessment_counts.pass_count,
"fail": self_assessment_counts.fail_count,
"unknown": self_assessment_counts.unknown_count,
}
},
}
)
self_assessment_counts_aggregate = AssessmentCounts(
pass_count=sum(x.pass_count for x in all_self_assessment_counts),
fail_count=sum(x.fail_count for x in all_self_assessment_counts),
unknown_count=sum(x.unknown_count for x in all_self_assessment_counts),
)
received_feedback_counts_aggregate = AssessmentCounts(
pass_count=sum(x.pass_count for x in all_feedback_assessment_counts),
fail_count=sum(x.fail_count for x in all_feedback_assessment_counts),
unknown_count=sum(x.unknown_count for x in all_feedback_assessment_counts),
)
# pad the feedback counts with unknowns for the
# learning units where we have no feedback yet
feedback_assessment_counts_aggregate = AssessmentCounts(
pass_count=received_feedback_counts_aggregate.pass_count,
fail_count=received_feedback_counts_aggregate.fail_count,
unknown_count=self_assessment_counts_aggregate.total_count
- received_feedback_counts_aggregate.total_count
+ received_feedback_counts_aggregate.unknown_count,
)
# check if there are any learning units with mentor feedback
feedback_assessment_visible = (
LearningUnit.objects.filter(
feedback_user=LearningUnitPerformanceFeedbackType.MENTOR_FEEDBACK.value,
course_category__course=course_session.course,
).count()
> 0
)
return Response(
{
"results": results,
"circles": list(
Circle.objects.filter(id__in=circle_ids).values("id", "title")
),
"aggregates": {
"feedback_assessment_visible": feedback_assessment_visible,
"feedback_assessment": {
"pass": feedback_assessment_counts_aggregate.pass_count,
"fail": feedback_assessment_counts_aggregate.fail_count,
"unknown": feedback_assessment_counts_aggregate.unknown_count,
},
"self_assessment": {
"pass": self_assessment_counts_aggregate.pass_count,
"fail": self_assessment_counts_aggregate.fail_count,
"unknown": self_assessment_counts_aggregate.unknown_count,
},
},
}
)
@api_view(["GET"])
@permission_classes([IsAuthenticated])
def get_self_evaluation_feedback_as_requester(request, learning_unit_id):