Merge branch 'develop' into feature/circle-page-unify
This commit is contained in:
commit
fff16da479
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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="{
|
||||
|
|
|
|||
|
|
@ -1042,7 +1042,7 @@ export type TopicObjectType = CoursePageInterface & {
|
|||
|
||||
export type UserObjectType = {
|
||||
__typename?: 'UserObjectType';
|
||||
avatar_url: Scalars['String']['output'];
|
||||
avatar_url?: Maybe<Scalars['String']['output']>;
|
||||
email: Scalars['String']['output'];
|
||||
first_name: Scalars['String']['output'];
|
||||
id: Scalars['UUID']['output'];
|
||||
|
|
@ -1139,7 +1139,7 @@ export type AssignmentCompletionQueryQueryVariables = Exact<{
|
|||
export type AssignmentCompletionQueryQuery = { __typename?: 'Query', assignment?: { __typename?: 'AssignmentObjectType', assignment_type: AssignmentAssignmentAssignmentTypeChoices, needs_expert_evaluation: boolean, max_points?: number | null, content_type: string, effort_required: string, evaluation_description: string, evaluation_document_url: string, evaluation_tasks?: any | null, id: string, intro_text: string, performance_objectives?: any | null, slug: string, tasks?: any | null, title: string, translation_key: string, solution_sample?: { __typename?: 'ContentDocumentObjectType', id: string, url?: string | null } | null, competence_certificate?: (
|
||||
{ __typename?: 'CompetenceCertificateObjectType' }
|
||||
& { ' $fragmentRefs'?: { 'CoursePageFieldsCompetenceCertificateObjectTypeFragment': CoursePageFieldsCompetenceCertificateObjectTypeFragment } }
|
||||
) | null } | null, assignment_completion?: { __typename?: 'AssignmentCompletionObjectType', id: string, completion_status: AssignmentAssignmentCompletionCompletionStatusChoices, submitted_at?: string | null, evaluation_submitted_at?: string | null, evaluation_points?: number | null, evaluation_max_points?: number | null, evaluation_passed?: boolean | null, edoniq_extended_time_flag: boolean, completion_data?: any | null, task_completion_data?: any | null, evaluation_user?: { __typename?: 'UserObjectType', id: string, first_name: string, last_name: string } | null, assignment_user: { __typename?: 'UserObjectType', avatar_url: string, first_name: string, last_name: string, id: string } } | null };
|
||||
) | null } | null, assignment_completion?: { __typename?: 'AssignmentCompletionObjectType', id: string, completion_status: AssignmentAssignmentCompletionCompletionStatusChoices, submitted_at?: string | null, evaluation_submitted_at?: string | null, evaluation_points?: number | null, evaluation_max_points?: number | null, evaluation_passed?: boolean | null, edoniq_extended_time_flag: boolean, completion_data?: any | null, task_completion_data?: any | null, evaluation_user?: { __typename?: 'UserObjectType', id: string, first_name: string, last_name: string } | null, assignment_user: { __typename?: 'UserObjectType', avatar_url?: string | null, first_name: string, last_name: string, id: string } } | null };
|
||||
|
||||
export type CompetenceCertificateQueryQueryVariables = Exact<{
|
||||
courseSlug: Scalars['String']['input'];
|
||||
|
|
|
|||
|
|
@ -552,8 +552,6 @@ type AssignmentCompletionObjectType {
|
|||
submitted_at: DateTime
|
||||
evaluation_submitted_at: DateTime
|
||||
evaluation_user: UserObjectType
|
||||
evaluation_points: Float
|
||||
evaluation_max_points: Float
|
||||
evaluation_passed: Boolean
|
||||
edoniq_extended_time_flag: Boolean!
|
||||
assignment_user: UserObjectType!
|
||||
|
|
@ -564,6 +562,8 @@ type AssignmentCompletionObjectType {
|
|||
additional_json_data: JSONString!
|
||||
task_completion_data: GenericScalar
|
||||
learning_content_page_id: ID
|
||||
evaluation_points: Float
|
||||
evaluation_max_points: Float
|
||||
}
|
||||
|
||||
"""
|
||||
|
|
@ -580,9 +580,9 @@ type UserObjectType {
|
|||
first_name: String!
|
||||
last_name: String!
|
||||
id: UUID!
|
||||
avatar_url: String!
|
||||
email: String!
|
||||
language: CoreUserLanguageChoices!
|
||||
avatar_url: String
|
||||
}
|
||||
|
||||
"""An enumeration."""
|
||||
|
|
|
|||
|
|
@ -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,16 +155,16 @@ 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">
|
||||
<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"
|
||||
>
|
||||
<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>
|
||||
|
|
@ -147,13 +172,11 @@ const performanceCriteriaStatusCount = computed(() => {
|
|||
class="ml-4 inline-block text-7xl font-bold"
|
||||
data-cy="self-evaluation-fail"
|
||||
>
|
||||
{{ performanceCriteriaStatusCount.FAIL }}
|
||||
{{ selfAssessmentCounts?.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.yes") }}»</h5>
|
||||
<div class="flex flex-row items-center">
|
||||
<it-icon-smiley-happy class="h-16 w-16"></it-icon-smiley-happy>
|
||||
|
|
@ -161,11 +184,11 @@ const performanceCriteriaStatusCount = computed(() => {
|
|||
class="ml-4 inline-block text-7xl font-bold"
|
||||
data-cy="self-evaluation-success"
|
||||
>
|
||||
{{ performanceCriteriaStatusCount.SUCCESS }}
|
||||
{{ selfAssessmentCounts?.pass }}
|
||||
</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">
|
||||
<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>
|
||||
|
|
@ -173,16 +196,55 @@ const performanceCriteriaStatusCount = computed(() => {
|
|||
class="ml-4 inline-block text-7xl font-bold"
|
||||
data-cy="self-evaluation-unknown"
|
||||
>
|
||||
{{ performanceCriteriaStatusCount.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"
|
||||
>
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -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() }"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -24,5 +24,8 @@ fi
|
|||
# Create Prüfungslehrgang
|
||||
python /app/manage.py create_vermittler_pruefung
|
||||
|
||||
# Create Motorfahrzeug Prüfungslehrgang
|
||||
python /app/manage.py create_motorfahrzeug_pruefung
|
||||
|
||||
# Set the command to run supervisord
|
||||
/home/django/.local/bin/supervisord -c /app/supervisord.conf
|
||||
|
|
|
|||
|
|
@ -28,108 +28,60 @@ 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");
|
||||
cy.get("[data-cy=\"self-evaluation-fail\"]").should("have.text", "1");
|
||||
cy.get("[data-cy=\"self-evaluation-success\"]").should("have.text", "1");
|
||||
cy.get("[data-cy=\"self-evaluation-unknown\"]").should("have.text", "2");
|
||||
cy.get('[data-cy="self-evaluation-fail"]').should("have.text", "1");
|
||||
cy.get('[data-cy="self-evaluation-success"]').should("have.text", "1");
|
||||
cy.get('[data-cy="self-evaluation-unknown"]').should("have.text", "2");
|
||||
});
|
||||
|
||||
it("should be able to make a happy self evaluation", () => {
|
||||
cy.get("[data-cy=\"test-lehrgang-lp-circle-reisen-lu-reisen\"]").click();
|
||||
cy.get('[data-cy="test-lehrgang-lp-circle-reisen-lu-reisen"]').click();
|
||||
cy.makeSelfEvaluation([true, true]);
|
||||
cy.get("[data-cy=\"test-lehrgang-lp-circle-reisen-lu-reisen\"]")
|
||||
.find("[data-cy=\"success\"]")
|
||||
cy.get('[data-cy="test-lehrgang-lp-circle-reisen-lu-reisen"]')
|
||||
.find('[data-cy="success"]')
|
||||
.should("exist");
|
||||
|
||||
// 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", () => {
|
||||
cy.get("[data-cy=\"test-lehrgang-lp-circle-reisen-lu-reisen\"]").click();
|
||||
cy.get('[data-cy="test-lehrgang-lp-circle-reisen-lu-reisen"]').click();
|
||||
cy.makeSelfEvaluation([false, false]);
|
||||
cy.get("[data-cy=\"test-lehrgang-lp-circle-reisen-lu-reisen\"]")
|
||||
.find("[data-cy=\"fail\"]")
|
||||
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", () => {
|
||||
cy.get("[data-cy=\"test-lehrgang-lp-circle-reisen-lu-reisen\"]").click();
|
||||
cy.get('[data-cy="test-lehrgang-lp-circle-reisen-lu-reisen"]').click();
|
||||
cy.makeSelfEvaluation([false, true]);
|
||||
cy.get("[data-cy=\"test-lehrgang-lp-circle-reisen-lu-reisen\"]")
|
||||
.find("[data-cy=\"fail\"]")
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -737,6 +737,7 @@ CONSTANCE_CONFIG = {
|
|||
"Default value is empty and will not send any emails. (No regex support!)",
|
||||
),
|
||||
}
|
||||
TRACKING_TAG = env("IT_TRACKING_TAG", default="")
|
||||
|
||||
if APP_ENVIRONMENT == "local":
|
||||
# http://whitenoise.evans.io/en/latest/django.html#using-whitenoise-in-development
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ from vbv_lernwelt.core.views import (
|
|||
check_rate_limit,
|
||||
cypress_reset_view,
|
||||
generate_web_component_icons,
|
||||
iterativ_test_coursesessions_reset_view,
|
||||
permission_denied_view,
|
||||
rate_limit_exceeded_view,
|
||||
vue_home,
|
||||
|
|
@ -209,6 +210,13 @@ urlpatterns = [
|
|||
name="t2l_sync",
|
||||
),
|
||||
|
||||
# iterativ Test course sessions
|
||||
path(
|
||||
r"api/core/resetiterativsessions/",
|
||||
iterativ_test_coursesessions_reset_view,
|
||||
name="iterativ_test_coursesessions_reset_view",
|
||||
),
|
||||
|
||||
path("server/graphql/",
|
||||
csrf_exempt(GraphQLView.as_view(graphiql=True, schema=schema))),
|
||||
# testing and debug
|
||||
|
|
|
|||
|
|
@ -17,6 +17,10 @@ class AssignmentCompletionObjectType(DjangoObjectType):
|
|||
task_completion_data = GenericScalar()
|
||||
learning_content_page_id = graphene.ID(source="learning_content_page_id")
|
||||
|
||||
# rounded to sensible representation
|
||||
evaluation_points = graphene.Float()
|
||||
evaluation_max_points = graphene.Float()
|
||||
|
||||
class Meta:
|
||||
model = AssignmentCompletion
|
||||
fields = (
|
||||
|
|
@ -34,12 +38,20 @@ class AssignmentCompletionObjectType(DjangoObjectType):
|
|||
"evaluation_user",
|
||||
"additional_json_data",
|
||||
"edoniq_extended_time_flag",
|
||||
"evaluation_points",
|
||||
"evaluation_passed",
|
||||
"evaluation_max_points",
|
||||
"task_completion_data",
|
||||
)
|
||||
|
||||
def resolve_evaluation_points(self, info):
|
||||
if self.evaluation_points:
|
||||
return round(self.evaluation_points, 1) # noqa
|
||||
return None
|
||||
|
||||
def resolve_evaluation_max_points(self, info):
|
||||
if self.evaluation_max_points:
|
||||
return round(self.evaluation_max_points, 1) # noqa
|
||||
return None
|
||||
|
||||
|
||||
class AssignmentObjectType(DjangoObjectType):
|
||||
tasks = JSONStreamField()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,344 @@
|
|||
from datetime import datetime, time, timedelta
|
||||
|
||||
import djclick as click
|
||||
import structlog
|
||||
from django.utils import timezone
|
||||
|
||||
from vbv_lernwelt.assignment.models import Assignment, AssignmentCompletion
|
||||
from vbv_lernwelt.core.admin import User
|
||||
from vbv_lernwelt.course.consts import (
|
||||
COURSE_VERSICHERUNGSVERMITTLERIN_FR_ID,
|
||||
COURSE_VERSICHERUNGSVERMITTLERIN_ID,
|
||||
COURSE_VERSICHERUNGSVERMITTLERIN_IT_ID,
|
||||
)
|
||||
from vbv_lernwelt.course.models import (
|
||||
Course,
|
||||
CourseCompletion,
|
||||
CourseSession,
|
||||
CourseSessionUser,
|
||||
)
|
||||
from vbv_lernwelt.course_session.models import (
|
||||
CourseSessionAssignment,
|
||||
CourseSessionAttendanceCourse,
|
||||
CourseSessionEdoniqTest,
|
||||
)
|
||||
from vbv_lernwelt.course_session_group.models import CourseSessionGroup
|
||||
from vbv_lernwelt.feedback.models import FeedbackResponse
|
||||
from vbv_lernwelt.learning_mentor.models import LearningMentor
|
||||
from vbv_lernwelt.learnpath.models import Circle
|
||||
from vbv_lernwelt.notify.models import Notification
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
from vbv_lernwelt.importer.services import (
|
||||
create_or_update_course_session,
|
||||
get_uk_course,
|
||||
LP_DATA,
|
||||
TRANSLATIONS,
|
||||
)
|
||||
|
||||
IT_VV_TEST_COURSE = "Iterativ VV Testkurs"
|
||||
IT_UK_TEST_COURSE = "Iterativ üK Testkurs"
|
||||
IT_UK_TEST_REGION = "Iterativ Region"
|
||||
TIME_FORMAT = "%d.%m.%Y, %H:%M"
|
||||
PASSWORD = "myvbv1234"
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
@click.command()
|
||||
def command():
|
||||
create_or_update_uk()
|
||||
create_or_update_vv()
|
||||
|
||||
|
||||
def create_or_update_uk(language="de"):
|
||||
uk_course = get_uk_course(language)
|
||||
uk_circle_keys = [
|
||||
"Kickoff",
|
||||
"Basis",
|
||||
"Fahrzeug",
|
||||
"Haushalt Teil 1",
|
||||
"Haushalt Teil 2",
|
||||
]
|
||||
|
||||
data = create_uk_data(language)
|
||||
create_or_update_course_session(
|
||||
uk_course,
|
||||
data,
|
||||
language,
|
||||
circle_keys=uk_circle_keys,
|
||||
)
|
||||
cs = CourseSession.objects.get(import_id=data["ID"])
|
||||
|
||||
members, trainer, regionenleiter = get_or_create_users_uk()
|
||||
delete_cs_data(cs, members + [trainer, regionenleiter])
|
||||
|
||||
add_to_course_session(cs, members)
|
||||
add_trainers_to_course_session(cs, [trainer], uk_circle_keys, language)
|
||||
create_and_add_to_cs_group(cs.course, IT_UK_TEST_REGION, [cs], regionenleiter)
|
||||
|
||||
|
||||
def create_or_update_vv(language="de"):
|
||||
vv_course = get_vv_course(language)
|
||||
|
||||
cs, _created = CourseSession.objects.get_or_create(
|
||||
course=vv_course, import_id=IT_VV_TEST_COURSE
|
||||
)
|
||||
cs.title = IT_VV_TEST_COURSE
|
||||
cs.save()
|
||||
|
||||
create_or_update_assignment_course_session(cs)
|
||||
members, member_with_mentor, mentor = get_or_create_users_vv()
|
||||
delete_cs_data(cs, members + [member_with_mentor, mentor])
|
||||
|
||||
add_to_course_session(cs, members + [member_with_mentor])
|
||||
add_mentor_to_course_session(cs, [(mentor, member_with_mentor)])
|
||||
|
||||
|
||||
def delete_cs_data(cs: CourseSession, users: list[User]):
|
||||
if cs:
|
||||
CourseCompletion.objects.filter(course_session=cs).delete()
|
||||
Notification.objects.filter(course_session=cs).delete()
|
||||
AssignmentCompletion.objects.filter(course_session=cs).delete()
|
||||
CourseSessionAttendanceCourse.objects.filter(course_session=cs).update(
|
||||
attendance_user_list=[]
|
||||
)
|
||||
CourseSessionEdoniqTest.objects.filter(course_session=cs).delete()
|
||||
CourseSessionUser.objects.filter(course_session=cs).delete()
|
||||
learning_mentor_ids = (
|
||||
LearningMentor.objects.filter(participants__course_session=cs)
|
||||
.values_list("id", flat=True)
|
||||
.distinct()
|
||||
| LearningMentor.objects.filter(mentor__in=users)
|
||||
.values_list("id", flat=True)
|
||||
.distinct()
|
||||
)
|
||||
# cannot call delete on distinct objects
|
||||
LearningMentor.objects.filter(id__in=list(learning_mentor_ids)).delete()
|
||||
else:
|
||||
logger.info("no_course_session_found", import_id=cs.import_id)
|
||||
|
||||
FeedbackResponse.objects.filter(feedback_user__in=users).delete()
|
||||
|
||||
|
||||
def add_to_course_session(
|
||||
course_session: CourseSession,
|
||||
members: list[User],
|
||||
role=CourseSessionUser.Role.MEMBER,
|
||||
):
|
||||
if course_session:
|
||||
for user in members:
|
||||
csu, _created = CourseSessionUser.objects.get_or_create(
|
||||
course_session_id=course_session.id, user_id=user.id, role=role
|
||||
)
|
||||
csu.save()
|
||||
|
||||
|
||||
def add_mentor_to_course_session(
|
||||
course_session: CourseSession, mentor_mentee_pairs: list[tuple[User, User]]
|
||||
):
|
||||
for mentor, mentee in mentor_mentee_pairs:
|
||||
lm = LearningMentor.objects.create(
|
||||
course=course_session.course,
|
||||
mentor=mentor,
|
||||
)
|
||||
lm.participants.add(
|
||||
CourseSessionUser.objects.get(
|
||||
user__id=mentee.id,
|
||||
course_session=course_session,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def add_trainers_to_course_session(
|
||||
course_session: CourseSession,
|
||||
trainers: list[User],
|
||||
circle_keys: list[str],
|
||||
language,
|
||||
):
|
||||
add_to_course_session(course_session, trainers, CourseSessionUser.Role.EXPERT)
|
||||
for user in trainers:
|
||||
for circle_key in circle_keys:
|
||||
circle_name = LP_DATA[circle_key][language]["title"]
|
||||
circle = Circle.objects.filter(
|
||||
slug=f"{course_session.course.slug}-lp-circle-{circle_name.lower()}"
|
||||
).first()
|
||||
|
||||
if course_session and circle:
|
||||
csu = CourseSessionUser.objects.filter(
|
||||
course_session_id=course_session.id, user_id=user.id
|
||||
).first()
|
||||
if csu:
|
||||
csu.expert.add(circle)
|
||||
csu.save()
|
||||
|
||||
|
||||
def get_or_create_users_uk():
|
||||
members = [
|
||||
_create_or_update_user(
|
||||
f"teilnehmer{n}.uk@iterativ.ch",
|
||||
f"Teilnehmer{n} üK",
|
||||
f"Iterativ{n}",
|
||||
PASSWORD,
|
||||
"de",
|
||||
)
|
||||
for n in range(1, 10)
|
||||
]
|
||||
trainer = _create_or_update_user(
|
||||
"trainer1.uk@iterativ.ch", "Trainer1 üK", "Iterativ1", PASSWORD, "de"
|
||||
)
|
||||
regionenleiter = _create_or_update_user(
|
||||
"regionenleiter1.uk@iterativ.ch",
|
||||
"Regionenleiter1 üK",
|
||||
"Iterativ1",
|
||||
PASSWORD,
|
||||
"de",
|
||||
)
|
||||
return (
|
||||
members,
|
||||
trainer,
|
||||
regionenleiter,
|
||||
)
|
||||
|
||||
|
||||
def get_or_create_users_vv():
|
||||
members = [
|
||||
_create_or_update_user(
|
||||
f"teilnehmer{n}.vv@iterativ.ch",
|
||||
f"Teilnehmer{n} VV ",
|
||||
f"Iterativ{n}",
|
||||
PASSWORD,
|
||||
"de",
|
||||
)
|
||||
for n in range(1, 10)
|
||||
]
|
||||
member_with_mentor = _create_or_update_user(
|
||||
"teilnehmer1.mitlb.vv@iterativ.ch",
|
||||
"Teilnehmer1 VV mit LB",
|
||||
"Iterativ1",
|
||||
PASSWORD,
|
||||
"de",
|
||||
)
|
||||
mentor = _create_or_update_user(
|
||||
"lernbegleitung1.vv@iterativ.ch",
|
||||
"Lernbegleitung1 VV",
|
||||
"Iterativ1",
|
||||
PASSWORD,
|
||||
"de",
|
||||
)
|
||||
return members, member_with_mentor, mentor
|
||||
|
||||
|
||||
def _create_or_update_user(email, first_name, last_name, password, language):
|
||||
try:
|
||||
user = User.objects.get(email=email)
|
||||
except User.DoesNotExist:
|
||||
user = User(
|
||||
email=email,
|
||||
username=email,
|
||||
)
|
||||
|
||||
user.email = email
|
||||
user.first_name = first_name or user.first_name
|
||||
user.last_name = last_name or user.last_name
|
||||
user.username = email
|
||||
user.language = language
|
||||
user.set_password(password)
|
||||
user.save()
|
||||
return user
|
||||
|
||||
|
||||
def create_uk_data(language):
|
||||
return {
|
||||
"Klasse": IT_UK_TEST_COURSE,
|
||||
"ID": IT_UK_TEST_COURSE,
|
||||
"Generation": 2024,
|
||||
"Region": "Bern",
|
||||
"Sprache": language,
|
||||
f"Kickoff {TRANSLATIONS[language]['start']}": timezone.make_aware(
|
||||
datetime.combine((timezone.now() + timedelta(weeks=2)).date(), time(9, 0))
|
||||
).strftime("%d.%m.%Y, %H:%M"),
|
||||
f"Kickoff {TRANSLATIONS[language]['ende']}": timezone.make_aware(
|
||||
datetime.combine((timezone.now() + timedelta(weeks=2)).date(), time(16, 0))
|
||||
).strftime("%d.%m.%Y, %H:%M"),
|
||||
f"Kickoff {TRANSLATIONS[language]['raum']}": "Raum 1",
|
||||
f"Kickoff {TRANSLATIONS[language]['standort']}": "Bern",
|
||||
f"Kickoff {TRANSLATIONS[language]['adresse']}": "Musterstrasse 1",
|
||||
f"Basis {TRANSLATIONS[language]['start']}": timezone.make_aware(
|
||||
datetime.combine((timezone.now() + timedelta(weeks=4)).date(), time(9, 0))
|
||||
).strftime("%d.%m.%Y, %H:%M"),
|
||||
f"Basis {TRANSLATIONS[language]['ende']}": timezone.make_aware(
|
||||
datetime.combine((timezone.now() + timedelta(weeks=4)).date(), time(16, 0))
|
||||
).strftime("%d.%m.%Y, %H:%M"),
|
||||
f"Basis {TRANSLATIONS[language]['raum']}": "Raum 1",
|
||||
f"Basis {TRANSLATIONS[language]['standort']}": "Bern",
|
||||
f"Basis {TRANSLATIONS[language]['adresse']}": "Musterstrasse 1",
|
||||
f"Fahrzeug {TRANSLATIONS[language]['start']}": timezone.make_aware(
|
||||
datetime.combine((timezone.now() + timedelta(weeks=6)).date(), time(9, 0))
|
||||
).strftime("%d.%m.%Y, %H:%M"),
|
||||
f"Fahrzeug {TRANSLATIONS[language]['ende']}": timezone.make_aware(
|
||||
datetime.combine((timezone.now() + timedelta(weeks=6)).date(), time(16, 0))
|
||||
).strftime("%d.%m.%Y, %H:%M"),
|
||||
f"Fahrzeug {TRANSLATIONS[language]['raum']}": "Raum 1",
|
||||
f"Fahrzeug {TRANSLATIONS[language]['standort']}": "Bern",
|
||||
f"Fahrzeug {TRANSLATIONS[language]['adresse']}": "Musterstrasse 1",
|
||||
f"Haushalt Teil 1 {TRANSLATIONS[language]['start']}": timezone.make_aware(
|
||||
datetime.combine((timezone.now() + timedelta(weeks=8)).date(), time(9, 0))
|
||||
).strftime("%d.%m.%Y, %H:%M"),
|
||||
f"Haushalt Teil 1 {TRANSLATIONS[language]['ende']}": timezone.make_aware(
|
||||
datetime.combine((timezone.now() + timedelta(weeks=8)).date(), time(16, 0))
|
||||
).strftime("%d.%m.%Y, %H:%M"),
|
||||
f"Haushalt Teil 1 {TRANSLATIONS[language]['raum']}": "Raum 1",
|
||||
f"Haushalt Teil 1 {TRANSLATIONS[language]['standort']}": "Bern",
|
||||
f"Haushalt Teil 1 {TRANSLATIONS[language]['adresse']}": "Musterstrasse 1",
|
||||
f"Haushalt Teil 2 {TRANSLATIONS[language]['start']}": timezone.make_aware(
|
||||
datetime.combine((timezone.now() + timedelta(weeks=10)).date(), time(9, 0))
|
||||
).strftime("%d.%m.%Y, %H:%M"),
|
||||
f"Haushalt Teil 2 {TRANSLATIONS[language]['ende']}": timezone.make_aware(
|
||||
datetime.combine((timezone.now() + timedelta(weeks=10)).date(), time(16, 0))
|
||||
).strftime("%d.%m.%Y, %H:%M"),
|
||||
f"Haushalt Teil 2 {TRANSLATIONS[language]['raum']}": "Raum 1",
|
||||
f"Haushalt Teil 2 {TRANSLATIONS[language]['standort']}": "Bern",
|
||||
f"Haushalt Teil 2 {TRANSLATIONS[language]['adresse']}": "Musterstrasse 1",
|
||||
}
|
||||
|
||||
|
||||
def create_and_add_to_cs_group(
|
||||
course: Course, name: str, course_sessions: list[CourseSession], supervisor: User
|
||||
):
|
||||
region, _ = CourseSessionGroup.objects.get_or_create(
|
||||
name=name,
|
||||
course=course,
|
||||
)
|
||||
|
||||
for cs in course_sessions:
|
||||
region.course_session.add(cs)
|
||||
|
||||
region.supervisor.add(supervisor)
|
||||
|
||||
|
||||
def get_vv_course(language: str) -> Course:
|
||||
if language == "fr":
|
||||
course_id = COURSE_VERSICHERUNGSVERMITTLERIN_FR_ID
|
||||
elif language == "it":
|
||||
course_id = COURSE_VERSICHERUNGSVERMITTLERIN_IT_ID
|
||||
else:
|
||||
course_id = COURSE_VERSICHERUNGSVERMITTLERIN_ID
|
||||
|
||||
return Course.objects.get(id=course_id)
|
||||
|
||||
|
||||
def create_or_update_assignment_course_session(cs: CourseSession):
|
||||
# not nice but works for now
|
||||
for assignment in Assignment.objects.all():
|
||||
if assignment.get_course().id == cs.course.id:
|
||||
logger.debug(
|
||||
"create_course_session_assigments",
|
||||
assignment=assignment,
|
||||
label="reset_test_courses",
|
||||
)
|
||||
for lca in assignment.learningcontentassignment_set.all():
|
||||
_csa, _created = CourseSessionAssignment.objects.get_or_create(
|
||||
course_session=cs,
|
||||
learning_content=lca,
|
||||
)
|
||||
|
|
@ -58,6 +58,12 @@ def vue_home(request, *args):
|
|||
|
||||
# render index.html from `npm run build`
|
||||
content = loader.render_to_string("vue/index.html", context={}, request=request)
|
||||
# inject Plausible tracking tag
|
||||
if settings.TRACKING_TAG:
|
||||
content = content.replace(
|
||||
"</head>",
|
||||
f"\n{settings.TRACKING_TAG}\n</head>",
|
||||
)
|
||||
return HttpResponse(content)
|
||||
|
||||
|
||||
|
|
@ -179,6 +185,17 @@ def cypress_reset_view(request):
|
|||
return HttpResponseRedirect("/server/admin/")
|
||||
|
||||
|
||||
@api_view(["POST"])
|
||||
@authentication_classes((authentication.SessionAuthentication,))
|
||||
@permission_classes((IsAdminUser,))
|
||||
def iterativ_test_coursesessions_reset_view(request):
|
||||
call_command(
|
||||
"reset_iterativ_test_sessions",
|
||||
)
|
||||
|
||||
return HttpResponseRedirect("/server/admin/")
|
||||
|
||||
|
||||
@django_view_authentication_exempt
|
||||
def generate_web_component_icons(request):
|
||||
svg_files = []
|
||||
|
|
|
|||
|
|
@ -1,8 +1,15 @@
|
|||
from django.contrib import admin
|
||||
|
||||
from vbv_lernwelt.course.models import Course, CourseSession, CourseSessionUser
|
||||
from vbv_lernwelt.feedback.services import (
|
||||
get_feedbacks_for_course_sessions,
|
||||
get_feedbacks_for_courses,
|
||||
)
|
||||
from vbv_lernwelt.learnpath.models import Circle
|
||||
|
||||
get_feedbacks_for_course_sessions.short_description = "Feedback export"
|
||||
get_feedbacks_for_courses.short_description = "Feedback export"
|
||||
|
||||
|
||||
@admin.register(Course)
|
||||
class CourseAdmin(admin.ModelAdmin):
|
||||
|
|
@ -12,6 +19,7 @@ class CourseAdmin(admin.ModelAdmin):
|
|||
"category_name",
|
||||
"slug",
|
||||
]
|
||||
actions = [get_feedbacks_for_courses]
|
||||
|
||||
|
||||
@admin.register(CourseSession)
|
||||
|
|
@ -26,6 +34,7 @@ class CourseSessionAdmin(admin.ModelAdmin):
|
|||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
actions = [get_feedbacks_for_course_sessions]
|
||||
|
||||
|
||||
@admin.register(CourseSessionUser)
|
||||
|
|
|
|||
|
|
@ -9,3 +9,4 @@ COURSE_UK_TRAINING_IT = -9
|
|||
COURSE_VERSICHERUNGSVERMITTLERIN_FR_ID = -10
|
||||
COURSE_VERSICHERUNGSVERMITTLERIN_IT_ID = -11
|
||||
COURSE_VERSICHERUNGSVERMITTLERIN_PRUEFUNG_ID = -12
|
||||
COURSE_MOTORFAHRZEUG_PRUEFUNG_ID = -13
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ from vbv_lernwelt.core.constants import TEST_MENTOR1_USER_ID
|
|||
from vbv_lernwelt.core.create_default_users import default_users
|
||||
from vbv_lernwelt.core.models import User
|
||||
from vbv_lernwelt.course.consts import (
|
||||
COURSE_MOTORFAHRZEUG_PRUEFUNG_ID,
|
||||
COURSE_TEST_ID,
|
||||
COURSE_UK,
|
||||
COURSE_UK_FR,
|
||||
|
|
@ -95,6 +96,7 @@ from vbv_lernwelt.importer.services import (
|
|||
)
|
||||
from vbv_lernwelt.learning_mentor.models import LearningMentor
|
||||
from vbv_lernwelt.learnpath.create_vv_new_learning_path import (
|
||||
create_vv_motorfahrzeug_pruefung_learning_path,
|
||||
create_vv_new_learning_path,
|
||||
create_vv_pruefung_learning_path,
|
||||
)
|
||||
|
|
@ -309,6 +311,34 @@ def create_versicherungsvermittlerin_pruefung_course(
|
|||
create_vv_pruefung_learning_path(course_id=course_id)
|
||||
|
||||
|
||||
def create_motorfahrzeug_pruefung_course(
|
||||
course_id=COURSE_MOTORFAHRZEUG_PRUEFUNG_ID, language="de"
|
||||
):
|
||||
names = {
|
||||
"de": "Motorfahrzeug Versicherungsvermittler/-in VBV Prüfung",
|
||||
"fr": "Véhicules à moteur Intermédiaire d’assurance AFA Examen",
|
||||
"it": "Veicolo a motore Intermediario/a assicurativo/a AFA Esame",
|
||||
}
|
||||
# Versicherungsvermittler/in mit neuen Circles
|
||||
course = create_versicherungsvermittlerin_with_categories(
|
||||
course_id=course_id,
|
||||
title=names[language],
|
||||
)
|
||||
|
||||
# assignments create assignments parent page
|
||||
_assignment_list_page = AssignmentListPageFactory(
|
||||
parent=course.coursepage,
|
||||
)
|
||||
|
||||
create_vv_new_competence_profile(course_id=course_id)
|
||||
create_default_media_library(course_id=course_id)
|
||||
create_vv_reflection(course_id=course_id)
|
||||
|
||||
CourseSession.objects.create(course_id=course_id, title=names[language])
|
||||
|
||||
create_vv_motorfahrzeug_pruefung_learning_path(course_id=course_id)
|
||||
|
||||
|
||||
def create_course_uk_de(course_id=COURSE_UK, lang="de"):
|
||||
names = {
|
||||
"de": "Überbetriebliche Kurse",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
import djclick as click
|
||||
|
||||
from vbv_lernwelt.course.consts import COURSE_MOTORFAHRZEUG_PRUEFUNG_ID
|
||||
from vbv_lernwelt.course.management.commands.create_default_courses import (
|
||||
create_motorfahrzeug_pruefung_course,
|
||||
)
|
||||
from vbv_lernwelt.course.models import Course
|
||||
|
||||
ADMIN_EMAILS = ["info@iterativ.ch", "admin"]
|
||||
|
||||
|
||||
@click.command()
|
||||
def command():
|
||||
print(
|
||||
"Creating Motorfahrzeug Vermittler Prüfung course",
|
||||
COURSE_MOTORFAHRZEUG_PRUEFUNG_ID,
|
||||
)
|
||||
|
||||
if Course.objects.filter(id=COURSE_MOTORFAHRZEUG_PRUEFUNG_ID).exists():
|
||||
print("Course already exists, skipping")
|
||||
return
|
||||
|
||||
create_motorfahrzeug_pruefung_course()
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import djclick as click
|
||||
import structlog
|
||||
|
||||
from vbv_lernwelt.feedback.services import export_feedback
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.argument("course_session_id")
|
||||
@click.option(
|
||||
"--save-as-file/--no-save-as-file",
|
||||
default=True,
|
||||
help="`save-as-file` to save the file, `no-save-as-file` returns bytes. Default is `save-as-file`.",
|
||||
)
|
||||
def command(course_session_id, save_as_file):
|
||||
# using the output from call_command was a bit cumbersome, so this is just a wrapper for the actual function
|
||||
export_feedback([course_session_id], save_as_file)
|
||||
|
|
@ -1,6 +1,12 @@
|
|||
from datetime import datetime
|
||||
from io import BytesIO
|
||||
from itertools import groupby
|
||||
from operator import attrgetter
|
||||
from typing import Union
|
||||
|
||||
import structlog
|
||||
from django.http import HttpResponse
|
||||
from openpyxl import Workbook
|
||||
|
||||
from vbv_lernwelt.core.models import User
|
||||
from vbv_lernwelt.course.models import CourseCompletionStatus, CourseSession
|
||||
|
|
@ -13,6 +19,47 @@ from vbv_lernwelt.learnpath.models import (
|
|||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
VV_FEEDBACK_QUESTIONS = [
|
||||
("satisfaction", "Zufriedenheit insgesamt"),
|
||||
("goal_attainment", "Zielerreichung insgesamt"),
|
||||
(
|
||||
"proficiency",
|
||||
"Wie beurteilst du deine Sicherheit bezüglichen den Themen nach dem Circle?",
|
||||
),
|
||||
("preparation_task_clarity", "Waren die Praxisaufträge klar und verständlich?"),
|
||||
("would_recommend", "Würdest du den Circle weiterempfehlen?"),
|
||||
("course_positive_feedback", "Was hat dir besonders gut gefallen?"),
|
||||
("course_negative_feedback", "Wo siehst du Verbesserungspotential?"),
|
||||
]
|
||||
|
||||
UK_FEEDBACK_QUESTIONS = [
|
||||
("satisfaction", "Zufriedenheit insgesamt"),
|
||||
("goal_attainment", "Zielerreichung insgesamt"),
|
||||
(
|
||||
"proficiency",
|
||||
"Wie beurteilst du deine Sicherheit bezüglichen den Themen nach dem Kurs?",
|
||||
),
|
||||
(
|
||||
"preparation_task_clarity",
|
||||
"Waren die Vorbereitungsaufträge klar und verständlich?",
|
||||
),
|
||||
(
|
||||
"instructor_competence",
|
||||
"Wie beurteilst du die Themensicherheit und Fachkompetenz des Kursleiters/der Kursleiterin?",
|
||||
),
|
||||
(
|
||||
"instructor_respect",
|
||||
"Wurden Fragen und Anregungen der Kursteilnehmenden ernst genommen und aufgegriffen?",
|
||||
),
|
||||
(
|
||||
"instructor_open_feedback",
|
||||
"Was möchtest du dem Kursleiter/der Kursleiterin sonst noch sagen?",
|
||||
),
|
||||
("would_recommend", "Würdest du den Kurs weiterempfehlen?"),
|
||||
("course_positive_feedback", "Was hat dir besonders gut gefallen?"),
|
||||
("course_negative_feedback", "Wo siehst du Verbesserungspotential?"),
|
||||
]
|
||||
|
||||
|
||||
def update_feedback_response(
|
||||
feedback_user: User,
|
||||
|
|
@ -100,3 +147,104 @@ def initial_data_for_feedback_page(
|
|||
"feedback_type": "vv",
|
||||
}
|
||||
return {}
|
||||
|
||||
|
||||
def export_feedback(course_session_ids: list[str], save_as_file: bool):
|
||||
wb = Workbook()
|
||||
|
||||
# remove the first sheet is just easier than keeping track of the active sheet
|
||||
wb.remove_sheet(wb.active)
|
||||
|
||||
feedbacks = FeedbackResponse.objects.filter(
|
||||
course_session_id__in=course_session_ids,
|
||||
submitted=True,
|
||||
).order_by("circle", "course_session", "updated_at")
|
||||
grouped_feedbacks = groupby(feedbacks, key=attrgetter("circle"))
|
||||
|
||||
for circle, group_feedbacks in grouped_feedbacks:
|
||||
group_feedbacks = list(group_feedbacks)
|
||||
logger.debug(
|
||||
"export_feedback_for_circle",
|
||||
data={
|
||||
"circle": circle.id,
|
||||
"course_session_ids": course_session_ids,
|
||||
"count": len(group_feedbacks),
|
||||
},
|
||||
label="feedback_export",
|
||||
)
|
||||
_create_sheet(wb, circle.title, group_feedbacks)
|
||||
|
||||
if save_as_file:
|
||||
wb.save(make_export_filename())
|
||||
else:
|
||||
output = BytesIO()
|
||||
wb.save(output)
|
||||
|
||||
output.seek(0)
|
||||
return output.getvalue()
|
||||
|
||||
|
||||
def _create_sheet(wb: Workbook, title: str, data: list[FeedbackResponse]):
|
||||
sheet = wb.create_sheet(title=title)
|
||||
|
||||
if len(data) == 0:
|
||||
return sheet
|
||||
|
||||
# we instruct the users not to mix exports of different courses, so we can assume the questions are the same and of the first type
|
||||
question_data = (
|
||||
UK_FEEDBACK_QUESTIONS
|
||||
if data[0].data["feedback_type"] == "uk"
|
||||
else VV_FEEDBACK_QUESTIONS
|
||||
)
|
||||
|
||||
# add header
|
||||
sheet.cell(row=1, column=1, value="Durchführung")
|
||||
sheet.cell(row=1, column=2, value="Datum")
|
||||
questions = [q[1] for q in question_data]
|
||||
for col_idx, title in enumerate(questions, start=3):
|
||||
sheet.cell(row=1, column=col_idx, value=title)
|
||||
|
||||
_add_rows(sheet, data, question_data)
|
||||
return sheet
|
||||
|
||||
|
||||
def _add_rows(sheet, data, question_data):
|
||||
for row_idx, feedback in enumerate(data, start=2):
|
||||
sheet.cell(row=row_idx, column=1, value=feedback.course_session.title)
|
||||
sheet.cell(
|
||||
row=row_idx, column=2, value=feedback.updated_at.date().strftime("%d.%m.%Y")
|
||||
)
|
||||
for col_idx, question in enumerate(question_data, start=3):
|
||||
response = feedback.data.get(question[0], "")
|
||||
sheet.cell(row=row_idx, column=col_idx, value=response)
|
||||
|
||||
|
||||
def make_export_filename(name: str = "feedback_export"):
|
||||
today_date = datetime.today().strftime("%Y-%m-%d")
|
||||
return f"{name}_{today_date}.xlsx"
|
||||
|
||||
|
||||
# used as admin action, that's why it's not in the views.py
|
||||
def get_feedbacks_for_course_sessions(_modeladmin, _request, queryset):
|
||||
file_name = "feedback_export_durchfuehrungen"
|
||||
return _handle_feedback_export_action(queryset, file_name)
|
||||
|
||||
|
||||
def get_feedbacks_for_courses(_modeladmin, _request, queryset):
|
||||
course_sessions = CourseSession.objects.filter(course__in=queryset)
|
||||
file_name = "feedback_export_lehrgaenge"
|
||||
return _handle_feedback_export_action(course_sessions, file_name)
|
||||
|
||||
|
||||
def _handle_feedback_export_action(course_seesions, file_name):
|
||||
excel_bytes = export_feedback(course_seesions, False)
|
||||
|
||||
response = HttpResponse(
|
||||
content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
)
|
||||
response[
|
||||
"Content-Disposition"
|
||||
] = f"attachment; filename={make_export_filename(file_name)}"
|
||||
response.write(excel_bytes)
|
||||
|
||||
return response
|
||||
|
|
|
|||
|
|
@ -12,7 +12,11 @@ from vbv_lernwelt.competence.factories import (
|
|||
)
|
||||
from vbv_lernwelt.competence.models import ActionCompetence
|
||||
from vbv_lernwelt.core.admin import User
|
||||
from vbv_lernwelt.course.consts import COURSE_VERSICHERUNGSVERMITTLERIN_ID
|
||||
from vbv_lernwelt.course.consts import (
|
||||
COURSE_MOTORFAHRZEUG_PRUEFUNG_ID,
|
||||
COURSE_VERSICHERUNGSVERMITTLERIN_ID,
|
||||
COURSE_VERSICHERUNGSVERMITTLERIN_PRUEFUNG_ID,
|
||||
)
|
||||
from vbv_lernwelt.course.models import CourseCategory, CoursePage
|
||||
from vbv_lernwelt.learnpath.models import LearningUnitPerformanceFeedbackType
|
||||
from vbv_lernwelt.learnpath.tests.learning_path_factories import (
|
||||
|
|
@ -54,7 +58,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)
|
||||
|
|
@ -89,7 +93,7 @@ def create_vv_new_learning_path(
|
|||
|
||||
|
||||
def create_vv_pruefung_learning_path(
|
||||
course_id=COURSE_VERSICHERUNGSVERMITTLERIN_ID, user=None
|
||||
course_id=COURSE_VERSICHERUNGSVERMITTLERIN_PRUEFUNG_ID, user=None
|
||||
):
|
||||
if user is None:
|
||||
user = User.objects.get(username="info@iterativ.ch")
|
||||
|
|
@ -108,6 +112,25 @@ def create_vv_pruefung_learning_path(
|
|||
Page.objects.update(owner=user)
|
||||
|
||||
|
||||
def create_vv_motorfahrzeug_pruefung_learning_path(
|
||||
course_id=COURSE_MOTORFAHRZEUG_PRUEFUNG_ID, user=None
|
||||
):
|
||||
if user is None:
|
||||
user = User.objects.get(username="info@iterativ.ch")
|
||||
|
||||
course_page = CoursePage.objects.get(course_id=course_id)
|
||||
lp = LearningPathFactory(
|
||||
title="Lernpfad",
|
||||
parent=course_page,
|
||||
)
|
||||
|
||||
TopicFactory(title="Fahrzeug", parent=lp)
|
||||
create_circle_fahrzeug(lp, course_page=course_page)
|
||||
|
||||
# all pages belong to 'admin' by default
|
||||
Page.objects.update(owner=user)
|
||||
|
||||
|
||||
def create_circle_basis(lp, title="Basis", course_page=None):
|
||||
circle = CircleFactory(
|
||||
title=title,
|
||||
|
|
@ -340,7 +363,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 +427,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 +459,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(
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -45,6 +45,14 @@
|
|||
|
||||
<hr style="margin: 24px 0">
|
||||
|
||||
<form action="/api/core/resetiterativsessions/" method="post">
|
||||
{% csrf_token %}
|
||||
<p>Zurücksetzen der Iterativ Testdurchführungen (üK: "Iterativ üK Testkurs", VV: "Iterativ VV Testkurs")</p>
|
||||
<button class="btn">Iterativ Testdurchführungen zurücksetzen</button>
|
||||
</form>
|
||||
|
||||
<hr style="margin: 24px 0">
|
||||
|
||||
<form action="/api/core/cypressreset/" method="post">
|
||||
{% csrf_token %}
|
||||
<label>
|
||||
|
|
|
|||
Loading…
Reference in New Issue