Merged in feat/588-vv-fremdeinschatzung (pull request #277)
Feat/588 - VV Mentor Fremdeinschätzung Approved-by: Daniel Egger
This commit is contained in:
commit
4ab5df0753
|
|
@ -1,6 +1,6 @@
|
|||
2
|
||||
<script setup lang="ts">
|
||||
import AssignmentSubmissionProgress from "@/pages/cockpit/cockpitPage/AssignmentSubmissionProgress.vue";
|
||||
import AssignmentSubmissionProgress from "@/components/cockpit/AssignmentSubmissionProgress.vue";
|
||||
import type {
|
||||
CourseSession,
|
||||
LearningContent,
|
||||
|
|
@ -10,7 +10,7 @@ import type {
|
|||
import log from "loglevel";
|
||||
import { computed } from "vue";
|
||||
import { useTranslation } from "i18next-vue";
|
||||
import FeedbackSubmissionProgress from "@/pages/cockpit/cockpitPage/FeedbackSubmissionProgress.vue";
|
||||
import FeedbackSubmissionProgress from "@/components/cockpit/mentor/FeedbackSubmissionProgress.vue";
|
||||
import { learningContentTypeData } from "@/utils/typeMaps";
|
||||
import {
|
||||
useCourseDataWithCompletion,
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
<script setup lang="ts">
|
||||
import AssignmentItem from "@/components/cockpit/mentor/AssignmentItem.vue";
|
||||
import type { RouteLocationRaw } from "vue-router";
|
||||
|
||||
defineProps<{
|
||||
taskTitle: string;
|
||||
circleTitle: string;
|
||||
pendingTasks: number;
|
||||
taskLink: RouteLocationRaw;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AssignmentItem
|
||||
:task-title="`${$t('a.Selbsteinschätzung')}: ${taskTitle}`"
|
||||
:circle-title="circleTitle"
|
||||
:pending-tasks="pendingTasks"
|
||||
:task-link="taskLink"
|
||||
:pending-tasks-label="$t('a.Selbsteinschätzungen geteilt')"
|
||||
:task-link-pending-label="$t('a.Fremdeinschätzung vornehmen')"
|
||||
:task-link-label="$t('a.Selbsteinschätzung anzeigen')"
|
||||
/>
|
||||
</template>
|
||||
|
|
@ -3,7 +3,7 @@ import { useCircleStore } from "@/stores/circle";
|
|||
import type { CircleType, LearningUnit } from "@/types";
|
||||
import * as log from "loglevel";
|
||||
|
||||
import { useCurrentCourseSession, useCourseDataWithCompletion } from "@/composables";
|
||||
import { useCourseDataWithCompletion, useCurrentCourseSession } from "@/composables";
|
||||
import LearningContentContainer from "@/pages/learningPath/learningContentPage/LearningContentContainer.vue";
|
||||
import LearningContentMultiLayout from "@/pages/learningPath/learningContentPage/layouts/LearningContentMultiLayout.vue";
|
||||
import eventBus from "@/utils/eventBus";
|
||||
|
|
@ -11,35 +11,56 @@ import { useRouteQuery } from "@vueuse/router";
|
|||
import { computed, onUnmounted } from "vue";
|
||||
import { getPreviousRoute } from "@/router/history";
|
||||
import { getCompetenceNaviUrl } from "@/utils/utils";
|
||||
import SelfEvaluationRequestFeedbackPage from "@/pages/learningPath/selfEvaluationPage/SelfEvaluationRequestFeedbackPage.vue";
|
||||
|
||||
log.debug("LearningContent.vue setup");
|
||||
|
||||
const circleStore = useCircleStore();
|
||||
const courseSession = useCurrentCourseSession();
|
||||
const courseCompletionData = useCourseDataWithCompletion();
|
||||
|
||||
const questionIndex = useRouteQuery("step", "0", { transform: Number, mode: "push" });
|
||||
const previousRoute = getPreviousRoute();
|
||||
|
||||
const props = defineProps<{
|
||||
learningUnit: LearningUnit;
|
||||
circle: CircleType;
|
||||
}>();
|
||||
|
||||
const circleStore = useCircleStore();
|
||||
const courseSession = useCurrentCourseSession();
|
||||
const courseCompletionData = useCourseDataWithCompletion();
|
||||
|
||||
const questions = computed(() => props.learningUnit?.performance_criteria ?? []);
|
||||
const numPages = computed(() => {
|
||||
if (learningUnitHasFeedbackPage.value) {
|
||||
return questions.value.length + 1;
|
||||
} else {
|
||||
return questions.value.length;
|
||||
}
|
||||
});
|
||||
|
||||
const questionIndex = useRouteQuery("step", "0", { transform: Number, mode: "push" });
|
||||
const previousRoute = getPreviousRoute();
|
||||
|
||||
const learningUnitHasFeedbackPage = computed(
|
||||
() => props.learningUnit?.feedback_user !== "NO_FEEDBACK"
|
||||
);
|
||||
|
||||
const currentQuestion = computed(() => questions.value[questionIndex.value]);
|
||||
const showPreviousButton = computed(() => questionIndex.value != 0);
|
||||
|
||||
const showNextButton = computed(
|
||||
() => questionIndex.value + 1 < questions.value?.length && questions.value?.length > 1
|
||||
() => questionIndex.value + 1 < numPages.value && numPages.value > 1
|
||||
);
|
||||
const showExitButton = computed(
|
||||
() =>
|
||||
questions.value?.length === 1 || questions.value?.length === questionIndex.value + 1
|
||||
|
||||
const isLastStep = computed(
|
||||
() => questions.value?.length === 1 || numPages.value == questionIndex.value + 1
|
||||
);
|
||||
|
||||
function handleContinue() {
|
||||
log.debug("handleContinue");
|
||||
if (questionIndex.value + 1 < questions.value.length) {
|
||||
|
||||
// not answering a question is allowed especially,
|
||||
// nonetheless we want to still know this state in the backend!
|
||||
if (currentQuestion.value && currentQuestion.value.completion_status === "UNKNOWN") {
|
||||
courseCompletionData.markCompletion(currentQuestion.value, "UNKNOWN");
|
||||
}
|
||||
|
||||
if (questionIndex.value + 1 < numPages.value) {
|
||||
log.debug("increment questionIndex", questionIndex.value);
|
||||
questionIndex.value += 1;
|
||||
} else {
|
||||
|
|
@ -50,7 +71,7 @@ function handleContinue() {
|
|||
|
||||
function handleBack() {
|
||||
log.debug("handleBack");
|
||||
if (questionIndex.value > 0 && questionIndex.value < questions.value.length) {
|
||||
if (questionIndex.value > 0 && questionIndex.value < numPages.value) {
|
||||
questionIndex.value -= 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -78,16 +99,20 @@ onUnmounted(() => {
|
|||
:sub-title="$t('a.Selbsteinschätzung')"
|
||||
:title="`${learningUnit.title}`"
|
||||
icon="it-icon-lc-learning-module"
|
||||
:steps-count="questions.length"
|
||||
:steps-count="numPages"
|
||||
:show-next-button="showNextButton"
|
||||
:show-exit-button="showExitButton"
|
||||
:show-exit-button="isLastStep"
|
||||
:show-start-button="false"
|
||||
:show-previous-button="showPreviousButton"
|
||||
:base-url="props.learningUnit.evaluate_url"
|
||||
:close-button-variant="learningUnitHasFeedbackPage ? 'close' : 'mark_as_done'"
|
||||
:end-badge-text="
|
||||
learningUnitHasFeedbackPage ? $t('general.submission') : undefined
|
||||
"
|
||||
@previous="handleBack()"
|
||||
@next="handleContinue()"
|
||||
>
|
||||
<div class="h-full">
|
||||
<div v-if="currentQuestion" class="h-full">
|
||||
<div class="mt-8">
|
||||
<h3 class="heading-3">
|
||||
{{ currentQuestion.title }}
|
||||
|
|
@ -137,6 +162,11 @@ onUnmounted(() => {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<SelfEvaluationRequestFeedbackPage
|
||||
v-else-if="isLastStep && learningUnit.feedback_user == 'MENTOR_FEEDBACK'"
|
||||
:learning-unit="props.learningUnit"
|
||||
:criteria="questions"
|
||||
/>
|
||||
</LearningContentMultiLayout>
|
||||
</LearningContentContainer>
|
||||
</div>
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
import ItButton from "@/components/ui/ItButton.vue";
|
||||
import ItCheckbox from "@/components/ui/ItCheckbox.vue";
|
||||
import { ref } from "vue";
|
||||
import { bustItGetCache, useCSRFFetch } from "@/fetchHelpers";
|
||||
import { bustItGetCache } from "@/fetchHelpers";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import eventBus from "@/utils/eventBus";
|
||||
import log from "loglevel";
|
||||
|
|
@ -12,9 +12,8 @@ import { useMutation } from "@urql/vue";
|
|||
import { UPSERT_ASSIGNMENT_COMPLETION_MUTATION } from "@/graphql/mutations";
|
||||
import type { Assignment } from "@/types";
|
||||
import DateEmbedding from "@/components/dueDates/DateEmbedding.vue";
|
||||
import { useCurrentCourseSession } from "@/composables";
|
||||
|
||||
const currentCourseSession = useCurrentCourseSession();
|
||||
import { useLearningMentors } from "@/composables";
|
||||
import NoMentorInformationPanel from "@/components/mentor/NoMentorInformationPanel.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
submissionDeadlineStart?: string | null;
|
||||
|
|
@ -29,10 +28,7 @@ const upsertAssignmentCompletionMutation = useMutation(
|
|||
UPSERT_ASSIGNMENT_COMPLETION_MUTATION
|
||||
);
|
||||
|
||||
const { data: learningMentors } = useCSRFFetch(
|
||||
`/api/mentor/${props.courseSessionId}/mentors`
|
||||
).json();
|
||||
|
||||
const learningMentors = useLearningMentors().learningMentors;
|
||||
const selectedLearningMentor = ref();
|
||||
|
||||
const onSubmit = async () => {
|
||||
|
|
@ -85,27 +81,7 @@ const onSubmit = async () => {
|
|||
</div>
|
||||
</div>
|
||||
<div v-else class="my-6">
|
||||
<div class="flex space-x-2 bg-sky-200 p-4">
|
||||
<it-icon-info class="it-icon h-6 w-6 text-sky-700" />
|
||||
<div>
|
||||
<div class="mb-4">
|
||||
{{
|
||||
$t(
|
||||
"a.Aktuell hast du noch keine Person als Lernbegleitung eingeladen. Lade jetzt jemanden ein."
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
<router-link
|
||||
:to="{
|
||||
name: 'learningMentorManagement',
|
||||
params: { courseSlug: currentCourseSession.course.slug },
|
||||
}"
|
||||
class="btn-blue px-4 py-2 font-bold"
|
||||
>
|
||||
{{ $t("a.Lernbegleitung einladen") }}
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<NoMentorInformationPanel />
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="props.submissionDeadlineStart" class="pt-6">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
<script setup lang="ts">
|
||||
import { useCurrentCourseSession } from "@/composables";
|
||||
|
||||
const currentCourseSession = useCurrentCourseSession();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex space-x-2 bg-sky-200 p-4">
|
||||
<it-icon-info class="it-icon h-6 w-6 text-sky-700" />
|
||||
<div>
|
||||
<div class="mb-4">
|
||||
{{
|
||||
$t(
|
||||
"a.Aktuell hast du noch keine Person als Lernbegleitung eingeladen. Lade jetzt jemanden ein."
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
<router-link
|
||||
:to="{
|
||||
name: 'learningMentorManagement',
|
||||
params: { courseSlug: currentCourseSession.course.slug },
|
||||
}"
|
||||
class="btn-blue px-4 py-2 font-bold"
|
||||
>
|
||||
{{ $t("a.Lernbegleitung einladen") }}
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import {
|
||||
type FeedbackRequest,
|
||||
getFeedbackEvaluationCaption,
|
||||
getSelfEvaluationCaption,
|
||||
getSmiley,
|
||||
} from "@/services/selfEvaluationFeedback";
|
||||
|
||||
const props = defineProps<{
|
||||
feedback: FeedbackRequest;
|
||||
}>();
|
||||
|
||||
const feedbackRequesterAvatar = computed(() => {
|
||||
return props.feedback.feedback_requester_user.avatar_url;
|
||||
});
|
||||
|
||||
const feedbackRequesterName = computed(() => {
|
||||
return `${props.feedback.feedback_requester_user.first_name} ${props.feedback.feedback_requester_user.last_name}`;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-for="(criteria, index) in props.feedback.criteria"
|
||||
:key="criteria.course_completion_id"
|
||||
class="pb-10"
|
||||
>
|
||||
<span class="text-gray-900">{{ criteria.title }}</span>
|
||||
<div class="mt-3 grid grid-cols-2 border-2 border-gray-200">
|
||||
<!-- Feedback requester assessment -->
|
||||
<div class="flex h-12 items-center pl-4">
|
||||
<b>
|
||||
{{
|
||||
$t("a.Selbsteinschätzung von FEEDBACK_REQUESTER_NAME", {
|
||||
FEEDBACK_REQUESTER_NAME: feedbackRequesterName,
|
||||
})
|
||||
}}
|
||||
</b>
|
||||
<img class="ml-2 h-7 w-7 rounded-full" :src="feedbackRequesterAvatar" />
|
||||
</div>
|
||||
<div class="flex items-center justify-start space-x-2 bg-white">
|
||||
<component :is="getSmiley(criteria.self_assessment)" class="h-6 w-6" />
|
||||
<span>{{ getSelfEvaluationCaption(criteria.self_assessment) }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Feedback provider assessment -->
|
||||
<div class="flex h-12 items-center bg-gray-200 pl-4">
|
||||
<b>{{ $t("a.Deine Fremdeinschätzung") }}</b>
|
||||
</div>
|
||||
<div class="flex items-center justify-between bg-gray-200">
|
||||
<div class="flex justify-start space-x-2">
|
||||
<component :is="getSmiley(criteria.feedback_assessment)" class="h-6 w-6" />
|
||||
<span>
|
||||
{{
|
||||
getFeedbackEvaluationCaption(
|
||||
criteria.feedback_assessment,
|
||||
feedback.feedback_requester_user
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<router-link
|
||||
v-if="!feedback.feedback_submitted"
|
||||
:to="{
|
||||
name: 'mentorSelfEvaluationFeedback',
|
||||
params: { learningUnitId: feedback.learning_unit_id },
|
||||
query: { step: index },
|
||||
}"
|
||||
class="mr-4 underline"
|
||||
>
|
||||
{{ $t("a.Bearbeiten") }}
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
<script setup lang="ts">
|
||||
import {
|
||||
type Criterion,
|
||||
getFeedbackEvaluationCaption,
|
||||
} from "@/services/selfEvaluationFeedback";
|
||||
import type { User } from "@/types";
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
criteria: Criterion;
|
||||
requester: User;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(["evaluation"]);
|
||||
|
||||
const fullname = computed(
|
||||
() => `${props.requester.first_name} ${props.requester.last_name}`
|
||||
);
|
||||
|
||||
const description = computed(() => `«${props.criteria.title}»`);
|
||||
const currentEvaluation = computed(() => props.criteria.feedback_assessment);
|
||||
|
||||
const onPassed = () => {
|
||||
emit("evaluation", props.criteria, "SUCCESS");
|
||||
};
|
||||
|
||||
const onFailed = () => {
|
||||
emit("evaluation", props.criteria, "FAIL");
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mt-16 space-y-4 bg-gray-200 p-7">
|
||||
<span>{{ $t("a.Leistungsziel") }}:</span>
|
||||
<div class="text-bold text-xl">{{ description }}</div>
|
||||
</div>
|
||||
<div class="mt-16 flex flex-row items-center pb-4">
|
||||
<span class="text-2xl font-bold">
|
||||
{{ $t("a.Kann FULLNAME das?", { FULLNAME: fullname }) }}
|
||||
</span>
|
||||
<img class="ml-4 h-12 w-12 rounded" :src="requester.avatar_url" :alt="fullname" />
|
||||
</div>
|
||||
<div class="flex space-x-10">
|
||||
<button
|
||||
class="inline-flex flex-1 items-center border-2 p-4 text-left"
|
||||
:class="currentEvaluation === 'SUCCESS' ? 'border-green-500' : 'border-gray-300'"
|
||||
@click="onPassed"
|
||||
>
|
||||
<it-icon-smiley-happy class="mr-4 h-16 w-16"></it-icon-smiley-happy>
|
||||
<span class="text-lg font-bold">
|
||||
{{ getFeedbackEvaluationCaption("SUCCESS", requester) }}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex flex-1 items-center border-2 p-4 text-left"
|
||||
:class="currentEvaluation === 'FAIL' ? 'border-orange-500' : 'border-gray-300'"
|
||||
@click="onFailed"
|
||||
>
|
||||
<it-icon-smiley-thinking class="mr-4 h-16 w-16"></it-icon-smiley-thinking>
|
||||
<span class="text-lg font-bold">
|
||||
{{ getFeedbackEvaluationCaption("FAIL", requester) }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
<script setup lang="ts">
|
||||
import type { FeedbackRequest } from "@/services/selfEvaluationFeedback";
|
||||
import FeedbackProvided from "@/components/selfEvaluationFeedback/FeedbackProvided.vue";
|
||||
import ItButton from "@/components/ui/ItButton.vue";
|
||||
|
||||
defineProps<{
|
||||
feedback: FeedbackRequest;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(["release"]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mb-12 mt-12 space-y-5 border-2 border-gray-200 p-7">
|
||||
<div class="text text-bold text-xl">{{ $t("a.Fremdeinschätzung freigeben") }}</div>
|
||||
<template v-if="!feedback.feedback_submitted">
|
||||
<div>
|
||||
{{
|
||||
$t(
|
||||
"a.Überprüfe deine Eingaben unten und gib anschliessend deine Fremdeinschätzung für FEEDBACK_REQUESTER frei",
|
||||
{
|
||||
FEEDBACK_REQUESTER: feedback.feedback_requester_user.first_name,
|
||||
}
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
<ItButton variant="primary" size="large" @click="emit('release')">
|
||||
{{ $t("a.Fremdeinschätzung freigeben") }}
|
||||
</ItButton>
|
||||
</template>
|
||||
<div v-else class="flex space-x-2 bg-green-200 p-4">
|
||||
<it-icon-check class="it-icon h-6 w-6 text-green-700" />
|
||||
<div>
|
||||
{{ $t("a.Du hast deine Fremdeinschätzung freigegeben") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<FeedbackProvided :feedback="feedback" />
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import {
|
||||
type FeedbackRequest,
|
||||
getFeedbackReceivedCaption,
|
||||
getSelfEvaluationCaption,
|
||||
getSmiley,
|
||||
} from "@/services/selfEvaluationFeedback";
|
||||
|
||||
const props = defineProps<{
|
||||
feedback: FeedbackRequest;
|
||||
}>();
|
||||
|
||||
const feedbackProviderAvatar = computed(() => {
|
||||
return props.feedback.feedback_provider_user.avatar_url;
|
||||
});
|
||||
|
||||
const feedbackProviderName = computed(() => {
|
||||
return `${props.feedback.feedback_provider_user.first_name} ${props.feedback.feedback_provider_user.last_name}`;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-for="criteria in props.feedback.criteria"
|
||||
:key="criteria.course_completion_id"
|
||||
class="mb-10"
|
||||
>
|
||||
<span class="text-gray-900">{{ criteria.title }}</span>
|
||||
<div class="mt-3 grid grid-cols-2 border-2 border-gray-200">
|
||||
<!-- Feedback requester assessment -->
|
||||
<div class="flex h-12 items-center pl-4">
|
||||
<b>{{ $t("a.Deine Selbsteinschätzung") }}</b>
|
||||
</div>
|
||||
<div class="flex items-center justify-start space-x-2 bg-white">
|
||||
<component :is="getSmiley(criteria.self_assessment)" class="h-6 w-6" />
|
||||
<span>{{ getSelfEvaluationCaption(criteria.self_assessment) }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Feedback provider assessment -->
|
||||
<div class="flex h-12 items-center bg-gray-200 pl-4">
|
||||
<b>
|
||||
{{
|
||||
$t("a.Fremdeinschätzung von FEEDBACK_PROVIDER_NAME", {
|
||||
FEEDBACK_PROVIDER_NAME: feedbackProviderName,
|
||||
})
|
||||
}}
|
||||
</b>
|
||||
<img class="ml-2 h-7 w-7 rounded-full" :src="feedbackProviderAvatar" />
|
||||
</div>
|
||||
<div class="flex items-center justify-start space-x-2 bg-gray-200">
|
||||
<component :is="getSmiley(criteria.feedback_assessment)" class="h-6 w-6" />
|
||||
<span>{{ getFeedbackReceivedCaption(criteria.feedback_assessment) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
<script setup lang="ts">
|
||||
import type { LearningUnit, LearningUnitPerformanceCriteria } from "@/types";
|
||||
|
||||
defineProps<{
|
||||
learningUnit: LearningUnit;
|
||||
criteria: LearningUnitPerformanceCriteria[];
|
||||
showEditLink: boolean;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-for="(completion, index) in criteria"
|
||||
:key="completion.id"
|
||||
class="flex flex-col space-y-4 border-t border-gray-400 py-8"
|
||||
>
|
||||
<div class="flex justify-between">
|
||||
<b>{{ completion.title }}</b>
|
||||
<router-link
|
||||
v-if="showEditLink"
|
||||
:to="`${learningUnit.evaluate_url}?step=${index}`"
|
||||
class="underline"
|
||||
>
|
||||
{{ $t("a.Bearbeiten") }}
|
||||
</router-link>
|
||||
</div>
|
||||
<div
|
||||
v-if="completion.completion_status == 'SUCCESS'"
|
||||
class="flex flex-row items-center space-x-2"
|
||||
>
|
||||
<it-icon-smiley-happy class="h-6 w-6" />
|
||||
<span>{{ $t("selfEvaluation.yes") }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="completion.completion_status == 'FAIL'"
|
||||
class="flex flex-row items-center space-x-2"
|
||||
>
|
||||
<it-icon-smiley-thinking class="h-6 w-6" />
|
||||
<span>{{ $t("selfEvaluation.no") }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="completion.completion_status == 'UNKNOWN'"
|
||||
class="flex flex-row items-center space-x-2"
|
||||
>
|
||||
<it-icon-smiley-neutral class="h-6 w-6" />
|
||||
<span>{{ $t("a.Nicht bewertet") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
feedbackMentorName: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex space-x-2 bg-green-200 p-4">
|
||||
<it-icon-check class="it-icon h-6 w-6 text-green-700" />
|
||||
<div>
|
||||
{{
|
||||
$t("a.Du hast deine Selbsteinschätzung erfolgreich mit FULL_NAME geteilt.", {
|
||||
FULL_NAME: feedbackMentorName,
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pt-6">
|
||||
{{
|
||||
$t(
|
||||
"a.FULL_NAME wird eine Fremdeinschätzung für dich vornehmen. Du wirst per Benachrichtigung informiert, sobald die Fremdeinschätzung für dich freigegeben wurde.",
|
||||
{ FULL_NAME: feedbackMentorName }
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import type { DropdownSelectable } from "@/types";
|
||||
import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from "@headlessui/vue";
|
||||
import { computed } from "vue";
|
||||
import { computed } from "vue"; // https://stackoverflow.com/questions/64775876/vue-3-pass-reactive-object-to-component-with-two-way-binding
|
||||
|
||||
// https://stackoverflow.com/questions/64775876/vue-3-pass-reactive-object-to-component-with-two-way-binding
|
||||
interface Props {
|
||||
|
|
@ -11,6 +11,7 @@ interface Props {
|
|||
};
|
||||
items?: DropdownSelectable[];
|
||||
borderless?: boolean;
|
||||
placeholderText?: string | null;
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
|
@ -25,6 +26,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|||
};
|
||||
},
|
||||
items: () => [],
|
||||
placeholderText: null,
|
||||
});
|
||||
|
||||
const dropdownSelected = computed<DropdownSelectable>({
|
||||
|
|
@ -47,7 +49,12 @@ const dropdownSelected = computed<DropdownSelectable>({
|
|||
<span v-if="dropdownSelected.iconName" class="mr-4">
|
||||
<component :is="dropdownSelected.iconName"></component>
|
||||
</span>
|
||||
<span class="block truncate">{{ dropdownSelected.name }}</span>
|
||||
<span class="block truncate">
|
||||
{{ dropdownSelected.name }}
|
||||
<span v-if="placeholderText && !dropdownSelected.name" class="text-gray-900">
|
||||
{{ placeholderText }}
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { useCSRFFetch } from "@/fetchHelpers";
|
||||
import type { CourseStatisticsType } from "@/gql/graphql";
|
||||
import { graphqlClient } from "@/graphql/client";
|
||||
import { COURSE_QUERY, COURSE_SESSION_DETAIL_QUERY } from "@/graphql/queries";
|
||||
|
|
@ -19,6 +20,7 @@ import type {
|
|||
CourseSession,
|
||||
CourseSessionDetail,
|
||||
LearningContentWithCompletion,
|
||||
LearningMentor,
|
||||
LearningPathType,
|
||||
LearningUnitPerformanceCriteria,
|
||||
PerformanceCriteria,
|
||||
|
|
@ -27,7 +29,7 @@ import { useQuery } from "@urql/vue";
|
|||
import orderBy from "lodash/orderBy";
|
||||
import log from "loglevel";
|
||||
import type { ComputedRef } from "vue";
|
||||
import { computed, ref, watchEffect } from "vue";
|
||||
import { computed, onMounted, ref, watchEffect } from "vue";
|
||||
|
||||
export function useCurrentCourseSession() {
|
||||
/**
|
||||
|
|
@ -463,3 +465,25 @@ export function useFileUpload() {
|
|||
|
||||
return { upload, error, loading, fileInfo };
|
||||
}
|
||||
|
||||
export function useLearningMentors() {
|
||||
const learningMentors = ref<LearningMentor[]>([]);
|
||||
const currentCourseSessionId = useCurrentCourseSession().value.id;
|
||||
const loading = ref(false);
|
||||
|
||||
const fetchMentors = async () => {
|
||||
loading.value = true;
|
||||
const { data } = await useCSRFFetch(
|
||||
`/api/mentor/${currentCourseSessionId}/mentors`
|
||||
).json();
|
||||
learningMentors.value = data.value;
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
onMounted(fetchMentors);
|
||||
|
||||
return {
|
||||
learningMentors,
|
||||
loading,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ const documents = {
|
|||
"\n query assignmentCompletionQuery(\n $assignmentId: ID!\n $courseSessionId: ID!\n $learningContentId: ID\n $assignmentUserId: UUID\n ) {\n assignment(id: $assignmentId) {\n assignment_type\n needs_expert_evaluation\n max_points\n content_type\n effort_required\n evaluation_description\n evaluation_document_url\n evaluation_tasks\n id\n intro_text\n performance_objectives\n slug\n tasks\n title\n translation_key\n solution_sample {\n id\n url\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n assignment_completion(\n assignment_id: $assignmentId\n course_session_id: $courseSessionId\n assignment_user_id: $assignmentUserId\n learning_content_page_id: $learningContentId\n ) {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_user {\n id\n first_name\n last_name\n }\n assignment_user {\n avatar_url\n first_name\n last_name\n id\n }\n evaluation_points\n evaluation_max_points\n evaluation_passed\n edoniq_extended_time_flag\n completion_data\n task_completion_data\n }\n }\n": types.AssignmentCompletionQueryDocument,
|
||||
"\n query competenceCertificateQuery($courseSlug: String!, $courseSessionId: ID!) {\n competence_certificate_list(course_slug: $courseSlug) {\n ...CoursePageFields\n competence_certificates {\n ...CoursePageFields\n assignments {\n ...CoursePageFields\n assignment_type\n max_points\n completion(course_session_id: $courseSessionId) {\n id\n completion_status\n submitted_at\n evaluation_points\n evaluation_max_points\n evaluation_passed\n }\n learning_content {\n ...CoursePageFields\n circle {\n id\n title\n slug\n }\n }\n }\n }\n }\n }\n": types.CompetenceCertificateQueryDocument,
|
||||
"\n query courseSessionDetail($courseSessionId: ID!) {\n course_session(id: $courseSessionId) {\n id\n title\n course {\n id\n title\n slug\n enable_circle_documents\n circle_contact_type\n }\n users {\n id\n user_id\n first_name\n last_name\n email\n avatar_url\n role\n circles {\n id\n title\n slug\n }\n }\n attendance_courses {\n id\n location\n trainer\n due_date {\n id\n start\n end\n }\n learning_content_id\n learning_content {\n id\n title\n circle {\n id\n title\n slug\n }\n }\n }\n assignments {\n id\n submission_deadline {\n id\n start\n }\n evaluation_deadline {\n id\n start\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n edoniq_tests {\n id\n deadline {\n id\n start\n end\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n }\n }\n": types.CourseSessionDetailDocument,
|
||||
"\n query courseQuery($slug: String!) {\n course(slug: $slug) {\n id\n title\n slug\n category_name\n enable_circle_documents\n circle_contact_type\n action_competences {\n competence_id\n ...CoursePageFields\n performance_criteria {\n competence_id\n learning_unit {\n id\n slug\n evaluate_url\n }\n ...CoursePageFields\n }\n }\n learning_path {\n ...CoursePageFields\n topics {\n is_visible\n ...CoursePageFields\n circles {\n description\n goals\n ...CoursePageFields\n learning_sequences {\n icon\n ...CoursePageFields\n learning_units {\n evaluate_url\n ...CoursePageFields\n performance_criteria {\n ...CoursePageFields\n }\n learning_contents {\n can_user_self_toggle_course_completion\n content_url\n minutes\n description\n ...CoursePageFields\n ... on LearningContentAssignmentObjectType {\n assignment_type\n content_assignment {\n id\n assignment_type\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentEdoniqTestObjectType {\n checkbox_text\n has_extended_time_test\n content_assignment {\n id\n assignment_type\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentRichTextObjectType {\n text\n }\n }\n }\n }\n }\n }\n }\n }\n }\n": types.CourseQueryDocument,
|
||||
"\n query courseQuery($slug: String!) {\n course(slug: $slug) {\n id\n title\n slug\n category_name\n enable_circle_documents\n circle_contact_type\n action_competences {\n competence_id\n ...CoursePageFields\n performance_criteria {\n competence_id\n learning_unit {\n id\n slug\n evaluate_url\n }\n ...CoursePageFields\n }\n }\n learning_path {\n ...CoursePageFields\n topics {\n is_visible\n ...CoursePageFields\n circles {\n description\n goals\n ...CoursePageFields\n learning_sequences {\n icon\n ...CoursePageFields\n learning_units {\n feedback_user\n evaluate_url\n ...CoursePageFields\n performance_criteria {\n ...CoursePageFields\n }\n learning_contents {\n can_user_self_toggle_course_completion\n content_url\n minutes\n description\n ...CoursePageFields\n ... on LearningContentAssignmentObjectType {\n assignment_type\n content_assignment {\n id\n assignment_type\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentEdoniqTestObjectType {\n checkbox_text\n has_extended_time_test\n content_assignment {\n id\n assignment_type\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentRichTextObjectType {\n text\n }\n }\n }\n }\n }\n }\n }\n }\n }\n": types.CourseQueryDocument,
|
||||
"\n query dashboardConfig {\n dashboard_config {\n id\n slug\n name\n dashboard_type\n }\n }\n": types.DashboardConfigDocument,
|
||||
"\n query dashboardProgress($courseId: ID!) {\n course_progress(course_id: $courseId) {\n _id\n course_id\n session_to_continue_id\n competence {\n _id\n total_count\n success_count\n fail_count\n }\n assignment {\n _id\n total_count\n points_max_count\n points_achieved_count\n }\n }\n }\n": types.DashboardProgressDocument,
|
||||
"\n query courseStatistics($courseId: ID!) {\n course_statistics(course_id: $courseId) {\n _id\n course_id\n course_title\n course_slug\n course_session_properties {\n _id\n sessions {\n id\n name\n }\n generations\n circles {\n id\n name\n }\n }\n course_session_selection_ids\n course_session_selection_metrics {\n _id\n session_count\n participant_count\n expert_count\n }\n attendance_day_presences {\n _id\n records {\n _id\n course_session_id\n generation\n circle_id\n due_date\n participants_present\n participants_total\n details_url\n }\n summary {\n _id\n days_completed\n participants_present\n }\n }\n feedback_responses {\n _id\n records {\n _id\n course_session_id\n generation\n circle_id\n experts\n satisfaction_average\n satisfaction_max\n details_url\n }\n summary {\n _id\n satisfaction_average\n satisfaction_max\n total_responses\n }\n }\n assignments {\n _id\n summary {\n _id\n completed_count\n average_passed\n }\n records {\n _id\n course_session_id\n course_session_assignment_id\n circle_id\n generation\n assignment_title\n assignment_type_translation_key\n details_url\n deadline\n metrics {\n _id\n passed_count\n failed_count\n unranked_count\n ranking_completed\n average_passed\n }\n }\n }\n competences {\n _id\n summary {\n _id\n success_total\n fail_total\n }\n records {\n _id\n course_session_id\n generation\n circle_id\n title\n success_count\n fail_count\n details_url\n }\n }\n }\n }\n": types.CourseStatisticsDocument,
|
||||
|
|
@ -72,7 +72,7 @@ export function graphql(source: "\n query courseSessionDetail($courseSessionId:
|
|||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n query courseQuery($slug: String!) {\n course(slug: $slug) {\n id\n title\n slug\n category_name\n enable_circle_documents\n circle_contact_type\n action_competences {\n competence_id\n ...CoursePageFields\n performance_criteria {\n competence_id\n learning_unit {\n id\n slug\n evaluate_url\n }\n ...CoursePageFields\n }\n }\n learning_path {\n ...CoursePageFields\n topics {\n is_visible\n ...CoursePageFields\n circles {\n description\n goals\n ...CoursePageFields\n learning_sequences {\n icon\n ...CoursePageFields\n learning_units {\n evaluate_url\n ...CoursePageFields\n performance_criteria {\n ...CoursePageFields\n }\n learning_contents {\n can_user_self_toggle_course_completion\n content_url\n minutes\n description\n ...CoursePageFields\n ... on LearningContentAssignmentObjectType {\n assignment_type\n content_assignment {\n id\n assignment_type\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentEdoniqTestObjectType {\n checkbox_text\n has_extended_time_test\n content_assignment {\n id\n assignment_type\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentRichTextObjectType {\n text\n }\n }\n }\n }\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query courseQuery($slug: String!) {\n course(slug: $slug) {\n id\n title\n slug\n category_name\n enable_circle_documents\n circle_contact_type\n action_competences {\n competence_id\n ...CoursePageFields\n performance_criteria {\n competence_id\n learning_unit {\n id\n slug\n evaluate_url\n }\n ...CoursePageFields\n }\n }\n learning_path {\n ...CoursePageFields\n topics {\n is_visible\n ...CoursePageFields\n circles {\n description\n goals\n ...CoursePageFields\n learning_sequences {\n icon\n ...CoursePageFields\n learning_units {\n evaluate_url\n ...CoursePageFields\n performance_criteria {\n ...CoursePageFields\n }\n learning_contents {\n can_user_self_toggle_course_completion\n content_url\n minutes\n description\n ...CoursePageFields\n ... on LearningContentAssignmentObjectType {\n assignment_type\n content_assignment {\n id\n assignment_type\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentEdoniqTestObjectType {\n checkbox_text\n has_extended_time_test\n content_assignment {\n id\n assignment_type\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentRichTextObjectType {\n text\n }\n }\n }\n }\n }\n }\n }\n }\n }\n"];
|
||||
export function graphql(source: "\n query courseQuery($slug: String!) {\n course(slug: $slug) {\n id\n title\n slug\n category_name\n enable_circle_documents\n circle_contact_type\n action_competences {\n competence_id\n ...CoursePageFields\n performance_criteria {\n competence_id\n learning_unit {\n id\n slug\n evaluate_url\n }\n ...CoursePageFields\n }\n }\n learning_path {\n ...CoursePageFields\n topics {\n is_visible\n ...CoursePageFields\n circles {\n description\n goals\n ...CoursePageFields\n learning_sequences {\n icon\n ...CoursePageFields\n learning_units {\n feedback_user\n evaluate_url\n ...CoursePageFields\n performance_criteria {\n ...CoursePageFields\n }\n learning_contents {\n can_user_self_toggle_course_completion\n content_url\n minutes\n description\n ...CoursePageFields\n ... on LearningContentAssignmentObjectType {\n assignment_type\n content_assignment {\n id\n assignment_type\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentEdoniqTestObjectType {\n checkbox_text\n has_extended_time_test\n content_assignment {\n id\n assignment_type\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentRichTextObjectType {\n text\n }\n }\n }\n }\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query courseQuery($slug: String!) {\n course(slug: $slug) {\n id\n title\n slug\n category_name\n enable_circle_documents\n circle_contact_type\n action_competences {\n competence_id\n ...CoursePageFields\n performance_criteria {\n competence_id\n learning_unit {\n id\n slug\n evaluate_url\n }\n ...CoursePageFields\n }\n }\n learning_path {\n ...CoursePageFields\n topics {\n is_visible\n ...CoursePageFields\n circles {\n description\n goals\n ...CoursePageFields\n learning_sequences {\n icon\n ...CoursePageFields\n learning_units {\n feedback_user\n evaluate_url\n ...CoursePageFields\n performance_criteria {\n ...CoursePageFields\n }\n learning_contents {\n can_user_self_toggle_course_completion\n content_url\n minutes\n description\n ...CoursePageFields\n ... on LearningContentAssignmentObjectType {\n assignment_type\n content_assignment {\n id\n assignment_type\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentEdoniqTestObjectType {\n checkbox_text\n has_extended_time_test\n content_assignment {\n id\n assignment_type\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentRichTextObjectType {\n text\n }\n }\n }\n }\n }\n }\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -279,6 +279,7 @@ type PerformanceCriteriaObjectType implements CoursePageInterface {
|
|||
|
||||
type LearningUnitObjectType implements CoursePageInterface {
|
||||
title_hidden: Boolean!
|
||||
feedback_user: LearnpathLearningUnitFeedbackUserChoices!
|
||||
id: ID!
|
||||
title: String!
|
||||
slug: String!
|
||||
|
|
@ -292,6 +293,15 @@ type LearningUnitObjectType implements CoursePageInterface {
|
|||
evaluate_url: String!
|
||||
}
|
||||
|
||||
"""An enumeration."""
|
||||
enum LearnpathLearningUnitFeedbackUserChoices {
|
||||
"""NO_FEEDBACK"""
|
||||
NO_FEEDBACK
|
||||
|
||||
"""MENTOR_FEEDBACK"""
|
||||
MENTOR_FEEDBACK
|
||||
}
|
||||
|
||||
interface LearningContentInterface {
|
||||
id: ID!
|
||||
title: String!
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ export const LearningPathObjectType = "LearningPathObjectType";
|
|||
export const LearningSequenceObjectType = "LearningSequenceObjectType";
|
||||
export const LearningUnitObjectType = "LearningUnitObjectType";
|
||||
export const LearnpathLearningContentAssignmentAssignmentTypeChoices = "LearnpathLearningContentAssignmentAssignmentTypeChoices";
|
||||
export const LearnpathLearningUnitFeedbackUserChoices = "LearnpathLearningUnitFeedbackUserChoices";
|
||||
export const Mutation = "Mutation";
|
||||
export const PerformanceCriteriaObjectType = "PerformanceCriteriaObjectType";
|
||||
export const PresenceRecordStatisticsType = "PresenceRecordStatisticsType";
|
||||
|
|
|
|||
|
|
@ -239,6 +239,7 @@ export const COURSE_QUERY = graphql(`
|
|||
icon
|
||||
...CoursePageFields
|
||||
learning_units {
|
||||
feedback_user
|
||||
evaluate_url
|
||||
...CoursePageFields
|
||||
performance_criteria {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import type {
|
|||
} from "@/types";
|
||||
import log from "loglevel";
|
||||
import { computed, onMounted, reactive } from "vue";
|
||||
import AssignmentSubmissionProgress from "@/pages/cockpit/cockpitPage/AssignmentSubmissionProgress.vue";
|
||||
import AssignmentSubmissionProgress from "@/components/cockpit/AssignmentSubmissionProgress.vue";
|
||||
import { useCourseSessionDetailQuery } from "@/composables";
|
||||
import { formatDueDate } from "../../../components/dueDates/dueDatesUtils";
|
||||
import { stringifyParse } from "@/utils/utils";
|
||||
|
|
|
|||
|
|
@ -3,12 +3,12 @@ import LearningPathDiagram from "@/components/learningPath/LearningPathDiagram.v
|
|||
import ItPersonRow from "@/components/ui/ItPersonRow.vue";
|
||||
|
||||
import { useCourseSessionDetailQuery, useCurrentCourseSession } from "@/composables";
|
||||
import SubmissionsOverview from "@/pages/cockpit/cockpitPage/SubmissionsOverview.vue";
|
||||
import SubmissionsOverview from "@/components/cockpit/SubmissionsOverview.vue";
|
||||
import { useExpertCockpitStore } from "@/stores/expertCockpit";
|
||||
import log from "loglevel";
|
||||
import CockpitDates from "@/pages/cockpit/cockpitPage/CockpitDates.vue";
|
||||
import CockpitDates from "@/components/cockpit/CockpitDates.vue";
|
||||
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
|
||||
import UserStatusCount from "@/pages/cockpit/cockpitPage/UserStatusCount.vue";
|
||||
import UserStatusCount from "@/components/cockpit/UserStatusCount.vue";
|
||||
import { useExpertCockpitPageData } from "@/pages/cockpit/cockpitPage/composables";
|
||||
|
||||
const props = defineProps<{
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
<script setup lang="ts">
|
||||
import type { PraxisAssignment } from "@/services/mentorCockpit";
|
||||
import type { Assignment } from "@/services/mentorCockpit";
|
||||
import { useMentorCockpit } from "@/services/mentorCockpit";
|
||||
import { useCurrentCourseSession } from "@/composables";
|
||||
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
|
||||
import { computed, type Ref, ref } from "vue";
|
||||
import PraxisAssignmentItem from "@/components/cockpit/mentor/PraxisAssignmentItem.vue";
|
||||
import { useTranslation } from "i18next-vue";
|
||||
import SelfAssignmentFeedbackAssignmentItem from "@/components/cockpit/mentor/SelfAssignmentFeedbackAssignmentItem.vue";
|
||||
|
||||
const { t } = useTranslation();
|
||||
const courseSession = useCurrentCourseSession();
|
||||
|
|
@ -34,7 +35,7 @@ const circleFilter = computed(() => {
|
|||
];
|
||||
});
|
||||
|
||||
const filteredAssignments: Ref<PraxisAssignment[]> = computed(() => {
|
||||
const filteredAssignments: Ref<Assignment[]> = computed(() => {
|
||||
if (!summary.value) return [];
|
||||
|
||||
let filtered = summary.value.assignments;
|
||||
|
|
@ -80,6 +81,16 @@ const filteredAssignments: Ref<PraxisAssignment[]> = computed(() => {
|
|||
}"
|
||||
:task-title="item.title"
|
||||
/>
|
||||
<SelfAssignmentFeedbackAssignmentItem
|
||||
v-else-if="item.type === 'self_evaluation_feedback'"
|
||||
:circle-title="mentorCockpitStore.getCircleTitleById(item.circle_id)"
|
||||
:pending-tasks="item.pending_evaluations"
|
||||
:task-link="{
|
||||
name: 'mentorCockpitSelfEvaluationFeedbackAssignments',
|
||||
params: { learningUnitId: item.id },
|
||||
}"
|
||||
:task-title="item.title"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import type { Participant, PraxisAssignment } from "@/services/mentorCockpit";
|
||||
import type { Assignment, Participant } from "@/services/mentorCockpit";
|
||||
import { useMentorCockpit } from "@/services/mentorCockpit";
|
||||
import { computed, onMounted, type Ref } from "vue";
|
||||
import { useCurrentCourseSession } from "@/composables";
|
||||
|
|
@ -11,10 +11,9 @@ const props = defineProps<{
|
|||
|
||||
const courseSession = useCurrentCourseSession();
|
||||
const mentorCockpitStore = useMentorCockpit(courseSession.value.id);
|
||||
|
||||
const participants = computed(() => mentorCockpitStore.summary.value?.participants);
|
||||
const praxisAssignment: Ref<PraxisAssignment | null> = computed(() =>
|
||||
mentorCockpitStore.getPraxisAssignmentById(props.praxisAssignmentId)
|
||||
const praxisAssignment: Ref<Assignment | null> = computed(() =>
|
||||
mentorCockpitStore.getAssignmentById(props.praxisAssignmentId)
|
||||
);
|
||||
|
||||
const getParticipantById = (id: string): Participant | null => {
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
<script setup lang="ts">
|
||||
import type { Assignment, Participant } from "@/services/mentorCockpit";
|
||||
import { useMentorCockpit } from "@/services/mentorCockpit";
|
||||
import { computed, type Ref } from "vue";
|
||||
import { useCurrentCourseSession } from "@/composables";
|
||||
|
||||
const props = defineProps<{
|
||||
learningUnitId: string;
|
||||
}>();
|
||||
|
||||
const courseSession = useCurrentCourseSession();
|
||||
const mentorCockpitStore = useMentorCockpit(courseSession.value.id);
|
||||
|
||||
const selfEvaluationFeedback: Ref<Assignment | null> = computed(() =>
|
||||
mentorCockpitStore.getAssignmentById(props.learningUnitId)
|
||||
);
|
||||
|
||||
const getParticipantById = (id: string): Participant | null => {
|
||||
if (mentorCockpitStore.summary.value?.participants) {
|
||||
const found = mentorCockpitStore.summary.value.participants.find(
|
||||
(item) => item.id === id
|
||||
);
|
||||
return found || null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="selfEvaluationFeedback">
|
||||
<div class="p-6">
|
||||
<h2 class="mb-2">
|
||||
{{ $t("a.Selbsteinschätzung") }}: {{ selfEvaluationFeedback.title }}
|
||||
</h2>
|
||||
<span class="text-gray-800">
|
||||
Circle «{{
|
||||
mentorCockpitStore.getCircleTitleById(selfEvaluationFeedback.circle_id)
|
||||
}}»
|
||||
</span>
|
||||
<template v-if="selfEvaluationFeedback.pending_evaluations > 0">
|
||||
<div class="flex flex-row items-center space-x-2 pt-4">
|
||||
<div
|
||||
class="flex h-7 w-7 items-center justify-center rounded-full border-2 border-green-500 px-3 text-sm font-bold"
|
||||
>
|
||||
<span>{{ selfEvaluationFeedback.pending_evaluations }}</span>
|
||||
</div>
|
||||
<span>{{ $t("a.Selbsteinschätzungen geteilt") }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="border-t">
|
||||
<div
|
||||
v-for="item in selfEvaluationFeedback.completions"
|
||||
:key="item.user_id"
|
||||
class="flex flex-col items-start justify-between gap-4 border-b py-2 pl-5 pr-5 last:border-b-0 md:flex-row md:items-center md:justify-between md:gap-16"
|
||||
>
|
||||
<!-- Left -->
|
||||
<div class="flex flex-grow flex-row items-center justify-start">
|
||||
<div class="w-80">
|
||||
<div class="flex items-center space-x-2">
|
||||
<img
|
||||
:alt="item.last_name"
|
||||
class="h-11 w-11 rounded-full"
|
||||
:src="
|
||||
getParticipantById(item.user_id)?.avatar_url ||
|
||||
'/static/avatars/myvbv-default-avatar.png'
|
||||
"
|
||||
/>
|
||||
<div>
|
||||
<div class="text-bold">
|
||||
{{ getParticipantById(item.user_id)?.first_name }}
|
||||
{{ getParticipantById(item.user_id)?.last_name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Center -->
|
||||
<div
|
||||
class="flex flex-grow flex-row items-center justify-start space-x-2 pl-20"
|
||||
>
|
||||
<template v-if="item.status == 'SUBMITTED'">
|
||||
<div
|
||||
class="flex h-7 w-7 items-center justify-center rounded-full border-2 border-green-500 px-3 py-1 text-sm font-bold"
|
||||
>
|
||||
<span class="flex items-center">
|
||||
<it-icon-check class="h-5 w-5"></it-icon-check>
|
||||
</span>
|
||||
</div>
|
||||
<span>{{ $t("a.Selbsteinschätzung geteilt") }}</span>
|
||||
</template>
|
||||
<template v-if="item.status == 'EVALUATED'">
|
||||
<div
|
||||
class="flex h-7 w-7 items-center justify-center rounded-full border-2 border-green-500 bg-green-500 px-3 py-1 text-sm font-bold"
|
||||
>
|
||||
<span class="flex items-center">
|
||||
<it-icon-check class="h-5 w-5"></it-icon-check>
|
||||
</span>
|
||||
</div>
|
||||
<span>{{ $t("a.Fremdeinschätzung freigeben") }}</span>
|
||||
</template>
|
||||
</div>
|
||||
<!-- Right -->
|
||||
<div>
|
||||
<router-link
|
||||
v-if="item.status == 'SUBMITTED'"
|
||||
class="btn-primary"
|
||||
:to="{
|
||||
name: 'mentorSelfEvaluationFeedback',
|
||||
params: {
|
||||
learningUnitId: learningUnitId,
|
||||
},
|
||||
}"
|
||||
>
|
||||
{{ $t("a.Fremdeinschätzung vornehmen") }}
|
||||
</router-link>
|
||||
<router-link
|
||||
v-if="item.status == 'EVALUATED'"
|
||||
class="underline"
|
||||
:to="{
|
||||
name: 'mentorSelfEvaluationFeedback',
|
||||
params: {
|
||||
learningUnitId: learningUnitId,
|
||||
},
|
||||
}"
|
||||
>
|
||||
{{ $t("a.Selbsteinschätzung anzeigen") }}
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
<script setup lang="ts">
|
||||
import LearningContentMultiLayout from "@/pages/learningPath/learningContentPage/layouts/LearningContentMultiLayout.vue";
|
||||
import LearningContentContainer from "@/pages/learningPath/learningContentPage/LearningContentContainer.vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import {
|
||||
type Criterion,
|
||||
useSelfEvaluationFeedback,
|
||||
} from "@/services/selfEvaluationFeedback";
|
||||
import { useRouteQuery } from "@vueuse/router";
|
||||
import FeedbackProviderRankCriteria from "@/components/selfEvaluationFeedback/FeedbackProviderRankCriteria.vue";
|
||||
import FeedbackProviderReleaseOverview from "@/components/selfEvaluationFeedback/FeedbackProviderReleaseOverview.vue";
|
||||
import log from "loglevel";
|
||||
|
||||
const router = useRouter();
|
||||
const props = defineProps<{
|
||||
learningUnitId: string;
|
||||
courseSlug: string;
|
||||
}>();
|
||||
|
||||
const currentStepRouteParam = useRouteQuery("step", "0", {
|
||||
transform: Number,
|
||||
mode: "push",
|
||||
});
|
||||
|
||||
const selfEvaluationFeedback = useSelfEvaluationFeedback(
|
||||
props.learningUnitId,
|
||||
"provider"
|
||||
);
|
||||
|
||||
const feedback = computed(() => selfEvaluationFeedback?.feedback.value);
|
||||
|
||||
watch(
|
||||
() => feedback.value,
|
||||
() => {
|
||||
if (feedback.value && feedback.value.feedback_submitted) {
|
||||
log.info("Feedback submitted, redirecting to overview page!");
|
||||
router.push({
|
||||
name: "mentorSelfEvaluationFeedback",
|
||||
params: {
|
||||
learningUnitId: props.learningUnitId,
|
||||
},
|
||||
query: {
|
||||
step: feedback.value.criteria.length,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const title = computed(() => {
|
||||
if (feedback.value) {
|
||||
return feedback.value.title;
|
||||
}
|
||||
return "";
|
||||
});
|
||||
|
||||
const currentStep = ref(currentStepRouteParam);
|
||||
|
||||
const stepsCount = computed(() => {
|
||||
if (feedback.value) {
|
||||
return feedback.value.criteria.length + 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
const currentCriteria = computed(() => {
|
||||
if (feedback.value && currentStep.value < stepsCount.value - 1) {
|
||||
return feedback.value.criteria[currentStep.value];
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const showNextButton = computed(() => {
|
||||
if (feedback.value) {
|
||||
return currentStep.value < stepsCount.value - 1;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
const handleBack = () => {
|
||||
if (currentStep.value > 0) {
|
||||
currentStep.value--;
|
||||
}
|
||||
};
|
||||
|
||||
const handleContinue = () => {
|
||||
if (currentStep.value < stepsCount.value) {
|
||||
currentStep.value++;
|
||||
}
|
||||
};
|
||||
|
||||
const clickExit = () => {
|
||||
console.log("clickExit");
|
||||
router.push({
|
||||
name: "mentorCockpitSelfEvaluationFeedbackAssignments",
|
||||
params: {
|
||||
learningUnitId: props.learningUnitId,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleFeedbackEvaluation = async (
|
||||
criteria: Criterion,
|
||||
evaluation: "SUCCESS" | "FAIL"
|
||||
) => {
|
||||
if (!feedback.value) {
|
||||
return;
|
||||
}
|
||||
await selfEvaluationFeedback.addFeedbackAssessment(
|
||||
criteria.course_completion_id,
|
||||
evaluation
|
||||
);
|
||||
};
|
||||
|
||||
const handleFeedbackRelease = async () => {
|
||||
if (!feedback.value) {
|
||||
return;
|
||||
}
|
||||
await selfEvaluationFeedback.releaseFeedback();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LearningContentContainer v-if="feedback" @exit="clickExit">
|
||||
<LearningContentMultiLayout
|
||||
icon="it-icon-lc-learning-module"
|
||||
close-button-variant="close"
|
||||
:current-step="currentStep"
|
||||
:sub-title="$t('a.Selbsteinschätzung')"
|
||||
:title="title"
|
||||
:steps-count="stepsCount"
|
||||
:show-next-button="showNextButton"
|
||||
:show-exit-button="!showNextButton"
|
||||
:show-start-button="false"
|
||||
:show-previous-button="currentStep > 0 && !feedback.feedback_submitted"
|
||||
:end-badge-text="$t('general.submission')"
|
||||
@exit="clickExit()"
|
||||
@previous="handleBack()"
|
||||
@next="handleContinue()"
|
||||
>
|
||||
<div v-if="feedback" class="h-full">
|
||||
<!-- Performance Criteria Evaluation -->
|
||||
<FeedbackProviderRankCriteria
|
||||
v-if="currentCriteria"
|
||||
:requester="feedback.feedback_requester_user"
|
||||
:criteria="currentCriteria"
|
||||
@evaluation="handleFeedbackEvaluation"
|
||||
/>
|
||||
<!-- Submission -->
|
||||
<FeedbackProviderReleaseOverview
|
||||
v-else
|
||||
:feedback="feedback"
|
||||
@release="handleFeedbackRelease"
|
||||
/>
|
||||
</div>
|
||||
</LearningContentMultiLayout>
|
||||
</LearningContentContainer>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
@ -12,7 +12,7 @@ defineEmits(["exit"]);
|
|||
|
||||
<template>
|
||||
<div>
|
||||
<div class="absolute bottom-0 top-0 w-full bg-white">
|
||||
<div class="absolute bottom-0 left-0 top-0 w-full bg-white">
|
||||
<CoursePreviewBar v-if="courseSessionsStore.hasCourseSessionPreview" />
|
||||
<div
|
||||
:class="{
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ const icon = computed(() => {
|
|||
const onExit = async () => {
|
||||
await props.beforeExitCallback();
|
||||
eventBus.emit("finishedLearningContent", true);
|
||||
emit("exit");
|
||||
};
|
||||
|
||||
const emit = defineEmits(["previous", "next", "exit"]);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import * as log from "loglevel";
|
||||
|
||||
import SelfEvaluation from "@/pages/learningPath/selfEvaluationPage/SelfEvaluation.vue";
|
||||
import SelfEvaluation from "@/components/learningPath/SelfEvaluation.vue";
|
||||
import { computed } from "vue";
|
||||
import { useCourseDataWithCompletion } from "@/composables";
|
||||
|
||||
|
|
@ -14,9 +14,9 @@ const props = defineProps<{
|
|||
}>();
|
||||
|
||||
const courseData = useCourseDataWithCompletion(props.courseSlug);
|
||||
const learningUnit = computed(() =>
|
||||
courseData.findLearningUnit(props.learningUnitSlug, props.circleSlug)
|
||||
);
|
||||
const learningUnit = computed(() => {
|
||||
return courseData.findLearningUnit(props.learningUnitSlug, props.circleSlug);
|
||||
});
|
||||
const circle = computed(() => {
|
||||
return courseData.findCircle(props.circleSlug);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,133 @@
|
|||
<script setup lang="ts">
|
||||
import type { LearningUnit, LearningUnitPerformanceCriteria } from "@/types";
|
||||
import { useLearningMentors } from "@/composables";
|
||||
import { computed, ref } from "vue";
|
||||
import ItButton from "@/components/ui/ItButton.vue";
|
||||
import NoMentorInformationPanel from "@/components/mentor/NoMentorInformationPanel.vue";
|
||||
import { useSelfEvaluationFeedback } from "@/services/selfEvaluationFeedback";
|
||||
import FeedbackRequestedInformationPanel from "@/components/selfEvaluationFeedback/FeedbackRequestedInformationPanel.vue";
|
||||
import FeedbackReceived from "@/components/selfEvaluationFeedback/FeedbackReceived.vue";
|
||||
import FeedbackRequested from "@/components/selfEvaluationFeedback/FeedbackRequested.vue";
|
||||
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
learningUnit: LearningUnit;
|
||||
criteria: LearningUnitPerformanceCriteria[];
|
||||
}>();
|
||||
|
||||
const selfEvaluationFeedback = useSelfEvaluationFeedback(
|
||||
props.learningUnit.id,
|
||||
"requester"
|
||||
);
|
||||
const storedFeedback = computed(() => selfEvaluationFeedback.feedback.value);
|
||||
const isStoredFeedbackLoading = computed(() => selfEvaluationFeedback.loading.value);
|
||||
const feedbackProvider = computed(() => storedFeedback.value?.feedback_provider_user);
|
||||
|
||||
// if no feedback is stored "current session" state management (mentor selection etc.)
|
||||
const learningMentors = useLearningMentors();
|
||||
const isMentorsLoading = computed(() => learningMentors.loading.value);
|
||||
|
||||
const mentors = computed(() => {
|
||||
return learningMentors.learningMentors.value.map((mentor) => ({
|
||||
id: mentor.mentor.id,
|
||||
name: `${mentor.mentor.first_name} ${mentor.mentor.last_name}`,
|
||||
}));
|
||||
});
|
||||
|
||||
const currentSessionRequestedMentor = ref();
|
||||
|
||||
const VisualState = {
|
||||
LOADING: "LOADING",
|
||||
NO_MENTOR: "NO_MENTOR",
|
||||
HAS_REQUESTED_FEEDBACK: "HAS_REQUESTED_FEEDBACK",
|
||||
HAS_RECEIVED_FEEDBACK: "HAS_RECEIVED_FEEDBACK",
|
||||
HAS_NOT_REQUESTED_FEEDBACK: "HAS_NOT_REQUESTED_FEEDBACK",
|
||||
};
|
||||
|
||||
const currentVisualState = computed(() => {
|
||||
if (isMentorsLoading.value || isStoredFeedbackLoading.value) {
|
||||
return VisualState.LOADING;
|
||||
} else if (mentors.value.length == 0) {
|
||||
return VisualState.NO_MENTOR;
|
||||
} else if (storedFeedback.value && !storedFeedback.value.feedback_submitted) {
|
||||
return VisualState.HAS_REQUESTED_FEEDBACK;
|
||||
} else if (storedFeedback.value && storedFeedback.value.feedback_submitted) {
|
||||
return VisualState.HAS_RECEIVED_FEEDBACK;
|
||||
} else {
|
||||
return VisualState.HAS_NOT_REQUESTED_FEEDBACK;
|
||||
}
|
||||
});
|
||||
|
||||
const onRequestFeedback = async () => {
|
||||
await selfEvaluationFeedback.requestFeedback(currentSessionRequestedMentor.value.id);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="currentVisualState != VisualState.LOADING">
|
||||
<div class="mb-10 w-full pt-8">
|
||||
<div
|
||||
v-if="currentVisualState != VisualState.HAS_RECEIVED_FEEDBACK"
|
||||
class="w-full border border-gray-400"
|
||||
>
|
||||
<div class="m-6 space-y-6">
|
||||
<h3 class="heading-3">
|
||||
{{ $t("a.Selbsteinschätzung teilen") }}
|
||||
</h3>
|
||||
<NoMentorInformationPanel
|
||||
v-if="currentVisualState == VisualState.NO_MENTOR"
|
||||
/>
|
||||
<FeedbackRequestedInformationPanel
|
||||
v-if="currentVisualState == VisualState.HAS_REQUESTED_FEEDBACK"
|
||||
:feedback-mentor-name="`${feedbackProvider?.first_name} ${feedbackProvider?.last_name}`"
|
||||
/>
|
||||
<div v-else-if="currentVisualState == VisualState.HAS_NOT_REQUESTED_FEEDBACK">
|
||||
<p>
|
||||
{{
|
||||
$t(
|
||||
"a.Du kannst deine Selbsteinschätzung mit deiner Lernbegleitung teilen, damit sie eine Fremdeinschätzung vornimmt."
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<ItDropdownSelect
|
||||
v-model="currentSessionRequestedMentor"
|
||||
:placeholder-text="`${$t('a.Lernbegleitung auswählen')}…`"
|
||||
class="mt-6 w-80"
|
||||
:items="mentors"
|
||||
></ItDropdownSelect>
|
||||
<ItButton
|
||||
class="mt-6"
|
||||
variant="primary"
|
||||
size="large"
|
||||
:disabled="!currentSessionRequestedMentor"
|
||||
@click="onRequestFeedback"
|
||||
>
|
||||
<p v-if="!currentSessionRequestedMentor">
|
||||
{{ $t("a.Selbsteinschätzung teilen") }}
|
||||
</p>
|
||||
<p v-else>
|
||||
{{
|
||||
$t("a.Selbsteinschätzung mit MENTOR_NAME teilen", {
|
||||
MENTOR_NAME: currentSessionRequestedMentor?.name,
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
</ItButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<FeedbackReceived
|
||||
v-if="currentVisualState == VisualState.HAS_RECEIVED_FEEDBACK && storedFeedback"
|
||||
:feedback="storedFeedback"
|
||||
/>
|
||||
<FeedbackRequested
|
||||
v-else
|
||||
:criteria="props.criteria"
|
||||
:learning-unit="props.learningUnit"
|
||||
:show-edit-link="currentVisualState != VisualState.HAS_REQUESTED_FEEDBACK"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
@ -180,7 +180,7 @@ const router = createRouter({
|
|||
{
|
||||
path: "",
|
||||
component: () =>
|
||||
import("@/pages/cockpit/cockpitPage/mentor/MentorOverview.vue"),
|
||||
import("@/pages/cockpit/cockpitPage/mentor/MentorOverviewPage.vue"),
|
||||
name: "mentorCockpitOverview",
|
||||
meta: {
|
||||
cockpitType: "mentor",
|
||||
|
|
@ -189,12 +189,24 @@ const router = createRouter({
|
|||
{
|
||||
path: "participants",
|
||||
component: () =>
|
||||
import("@/pages/cockpit/cockpitPage/mentor/MentorParticipants.vue"),
|
||||
import("@/pages/cockpit/cockpitPage/mentor/MentorParticipantsPage.vue"),
|
||||
name: "mentorCockpitParticipants",
|
||||
meta: {
|
||||
cockpitType: "mentor",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "self-evaluation-feedback/:learningUnitId",
|
||||
component: () =>
|
||||
import(
|
||||
"@/pages/cockpit/cockpitPage/mentor/SelfEvaluationFeedbackPage.vue"
|
||||
),
|
||||
name: "mentorSelfEvaluationFeedback",
|
||||
meta: {
|
||||
cockpitType: "mentor",
|
||||
},
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "details",
|
||||
component: () =>
|
||||
|
|
@ -207,7 +219,7 @@ const router = createRouter({
|
|||
path: "praxis-assignments/:praxisAssignmentId",
|
||||
component: () =>
|
||||
import(
|
||||
"@/pages/cockpit/cockpitPage/mentor/MentorPraxisAssignment.vue"
|
||||
"@/pages/cockpit/cockpitPage/mentor/MentorPraxisAssignmentPage.vue"
|
||||
),
|
||||
name: "mentorCockpitPraxisAssignments",
|
||||
meta: {
|
||||
|
|
@ -215,6 +227,18 @@ const router = createRouter({
|
|||
},
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "self-evaluation-feedback-assignments/:learningUnitId",
|
||||
component: () =>
|
||||
import(
|
||||
"@/pages/cockpit/cockpitPage/mentor/MentorSelfEvaluationFeedbackAssignmentPage.vue"
|
||||
),
|
||||
name: "mentorCockpitSelfEvaluationFeedbackAssignments",
|
||||
meta: {
|
||||
cockpitType: "mentor",
|
||||
},
|
||||
props: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ interface Completion {
|
|||
url: string;
|
||||
}
|
||||
|
||||
export interface PraxisAssignment {
|
||||
export interface Assignment {
|
||||
id: string;
|
||||
title: string;
|
||||
circle_id: string;
|
||||
|
|
@ -42,7 +42,7 @@ export interface PraxisAssignment {
|
|||
interface Summary {
|
||||
participants: Participant[];
|
||||
circles: Circle[];
|
||||
assignments: PraxisAssignment[];
|
||||
assignments: Assignment[];
|
||||
}
|
||||
|
||||
export const useMentorCockpit = (
|
||||
|
|
@ -60,7 +60,7 @@ export const useMentorCockpit = (
|
|||
return "";
|
||||
};
|
||||
|
||||
const getPraxisAssignmentById = (id: string): PraxisAssignment | null => {
|
||||
const getAssignmentById = (id: string): Assignment | null => {
|
||||
if (summary.value?.assignments) {
|
||||
const found = summary.value.assignments.find(
|
||||
(assignment) => assignment.id === id
|
||||
|
|
@ -93,7 +93,7 @@ export const useMentorCockpit = (
|
|||
summary,
|
||||
error,
|
||||
getCircleTitleById,
|
||||
getPraxisAssignmentById,
|
||||
fetchData,
|
||||
getAssignmentById,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,182 @@
|
|||
import { useCSRFFetch } from "@/fetchHelpers";
|
||||
import type { User } from "@/types";
|
||||
import { toValue } from "@vueuse/core";
|
||||
import { t } from "i18next";
|
||||
import type { Ref } from "vue";
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
|
||||
export interface FeedbackRequest {
|
||||
feedback_id: string;
|
||||
learning_unit_id: number;
|
||||
circle_name: string;
|
||||
title: string;
|
||||
// submitted => provider submitted (released) his/her feedback
|
||||
feedback_submitted: boolean;
|
||||
feedback_requester_user: User;
|
||||
feedback_provider_user: User;
|
||||
criteria: Criterion[];
|
||||
}
|
||||
|
||||
export interface Criterion {
|
||||
course_completion_id: string;
|
||||
title: string;
|
||||
self_assessment: "FAIL" | "SUCCESS" | "UNKNOWN";
|
||||
feedback_assessment: "FAIL" | "SUCCESS" | "UNKNOWN";
|
||||
}
|
||||
|
||||
/** 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
|
||||
*
|
||||
* Design decision: We generally just re-fetch the whole feedback from the backend
|
||||
* after each action (e.g. request, release, add-assessment) to keep the frontend simple.
|
||||
*/
|
||||
export function useSelfEvaluationFeedback(
|
||||
learningUnitId: Ref<string> | string,
|
||||
feedbackRole: "requester" | "provider"
|
||||
) {
|
||||
const feedback = ref<FeedbackRequest>();
|
||||
const loading = ref(false);
|
||||
const error = ref();
|
||||
|
||||
const url = computed(
|
||||
() => `/api/self-evaluation-feedback/${feedbackRole}/${learningUnitId}/feedback`
|
||||
);
|
||||
|
||||
const fetchFeedback = async () => {
|
||||
error.value = undefined;
|
||||
loading.value = true;
|
||||
|
||||
console.log("Fetching feedback for learning unit", learningUnitId);
|
||||
const { data, statusCode, error: _error } = await useCSRFFetch(url.value).json();
|
||||
loading.value = false;
|
||||
|
||||
if (_error.value) {
|
||||
error.value = _error;
|
||||
feedback.value = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
if (statusCode.value === 404) {
|
||||
feedback.value = undefined;
|
||||
} else {
|
||||
feedback.value = data.value;
|
||||
}
|
||||
};
|
||||
|
||||
const requestFeedback = async (fromProviderUserId: string) => {
|
||||
if (feedbackRole !== "requester") {
|
||||
console.warn("Cannot request feedback");
|
||||
return;
|
||||
}
|
||||
|
||||
const url = `/api/self-evaluation-feedback/requester/${toValue(
|
||||
learningUnitId
|
||||
)}/feedback/start`;
|
||||
await useCSRFFetch(url).post({
|
||||
feedback_provider_user_id: fromProviderUserId,
|
||||
});
|
||||
|
||||
await fetchFeedback();
|
||||
};
|
||||
|
||||
const addFeedbackAssessment = async (
|
||||
courseCompletionId: string,
|
||||
assessment: "FAIL" | "SUCCESS"
|
||||
) => {
|
||||
if (feedbackRole !== "provider" || !feedback.value) {
|
||||
console.warn("Cannot add feedback assessment");
|
||||
return;
|
||||
}
|
||||
|
||||
await useCSRFFetch(
|
||||
`/api/self-evaluation-feedback/provider/feedback/${feedback.value.feedback_id}/add-assessment`
|
||||
).put({
|
||||
course_completion_id: courseCompletionId,
|
||||
feedback_assessment: assessment,
|
||||
});
|
||||
|
||||
await fetchFeedback();
|
||||
};
|
||||
|
||||
const releaseFeedback = async () => {
|
||||
if (feedbackRole !== "provider" || !feedback.value) {
|
||||
console.warn("Cannot release feedback");
|
||||
return;
|
||||
}
|
||||
|
||||
await useCSRFFetch(
|
||||
`/api/self-evaluation-feedback/provider/feedback/${feedback.value.feedback_id}/release`
|
||||
).put({});
|
||||
|
||||
await fetchFeedback();
|
||||
};
|
||||
|
||||
onMounted(fetchFeedback);
|
||||
|
||||
return {
|
||||
feedback,
|
||||
error,
|
||||
loading,
|
||||
// feedback requester actions
|
||||
requestFeedback,
|
||||
// feedback provider actions
|
||||
addFeedbackAssessment,
|
||||
releaseFeedback,
|
||||
};
|
||||
}
|
||||
|
||||
export const getSmiley = (assessment: "FAIL" | "SUCCESS" | "UNKNOWN") => {
|
||||
switch (assessment) {
|
||||
case "SUCCESS":
|
||||
return "it-icon-smiley-happy";
|
||||
case "FAIL":
|
||||
return "it-icon-smiley-thinking";
|
||||
default:
|
||||
return "it-icon-smiley-neutral";
|
||||
}
|
||||
};
|
||||
|
||||
export const getSelfEvaluationCaption = (
|
||||
assessment: "FAIL" | "SUCCESS" | "UNKNOWN"
|
||||
) => {
|
||||
switch (assessment) {
|
||||
case "SUCCESS":
|
||||
return t("selfEvaluation.yes");
|
||||
case "FAIL":
|
||||
return t("selfEvaluation.no");
|
||||
case "UNKNOWN":
|
||||
return t("a.Nicht bewertet");
|
||||
}
|
||||
};
|
||||
|
||||
export const getFeedbackReceivedCaption = (
|
||||
assessment: "FAIL" | "SUCCESS" | "UNKNOWN"
|
||||
) => {
|
||||
switch (assessment) {
|
||||
case "SUCCESS":
|
||||
return t("receivedEvaluation.yes");
|
||||
case "FAIL":
|
||||
return t("receivedEvaluation.no");
|
||||
case "UNKNOWN":
|
||||
return t("a.Nicht bewertet");
|
||||
}
|
||||
};
|
||||
|
||||
export const getFeedbackEvaluationCaption = (
|
||||
assessment: "FAIL" | "SUCCESS" | "UNKNOWN",
|
||||
requester: User
|
||||
) => {
|
||||
switch (assessment) {
|
||||
case "SUCCESS":
|
||||
return t("a.Ja, NAME kann das.", {
|
||||
NAME: requester.first_name,
|
||||
});
|
||||
case "FAIL":
|
||||
return t("a.Nein, NAME muss das nochmals anschauen.", {
|
||||
NAME: requester.first_name,
|
||||
});
|
||||
case "UNKNOWN":
|
||||
return t("a.Nicht bewertet");
|
||||
}
|
||||
};
|
||||
|
|
@ -457,6 +457,17 @@ export interface ExpertSessionUser extends CourseSessionUser {
|
|||
role: "EXPERT";
|
||||
}
|
||||
|
||||
export interface Mentor {
|
||||
id: number;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
}
|
||||
|
||||
export interface LearningMentor {
|
||||
id: number;
|
||||
mentor: Mentor;
|
||||
}
|
||||
|
||||
export type CourseSessionDetail = CourseSessionObjectType;
|
||||
|
||||
// document upload
|
||||
|
|
@ -579,3 +590,16 @@ export interface FeedbackData {
|
|||
};
|
||||
feedbackType: FeedbackType;
|
||||
}
|
||||
|
||||
export type User = {
|
||||
id: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
email: string;
|
||||
username: string;
|
||||
avatar_url: string;
|
||||
organisation: string | null;
|
||||
is_superuser: boolean;
|
||||
course_session_experts: any[];
|
||||
language: string;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -9,71 +9,71 @@ describe("circle.cy.js", () => {
|
|||
});
|
||||
|
||||
it("can open circle page", () => {
|
||||
cy.get("[data-cy=\"circle-title\"]").should("contain", "Fahrzeug");
|
||||
cy.get('[data-cy="circle-title"]').should("contain", "Fahrzeug");
|
||||
});
|
||||
|
||||
it("can toggle learning content", () => {
|
||||
cy.get("[data-cy=\"circle-title\"]").should("contain", "Fahrzeug");
|
||||
cy.get('[data-cy="circle-title"]').should("contain", "Fahrzeug");
|
||||
|
||||
cy.get(
|
||||
"[data-cy=\"test-lehrgang-lp-circle-fahrzeug-lc-handlungsfeld-fahrzeug-checkbox\"]"
|
||||
'[data-cy="test-lehrgang-lp-circle-fahrzeug-lc-handlungsfeld-fahrzeug-checkbox"]'
|
||||
).should("have.class", "cy-unchecked");
|
||||
|
||||
cy.get(
|
||||
"[data-cy=\"test-lehrgang-lp-circle-fahrzeug-lc-handlungsfeld-fahrzeug-checkbox\"]"
|
||||
'[data-cy="test-lehrgang-lp-circle-fahrzeug-lc-handlungsfeld-fahrzeug-checkbox"]'
|
||||
).click();
|
||||
|
||||
cy.get(
|
||||
"[data-cy=\"test-lehrgang-lp-circle-fahrzeug-lc-handlungsfeld-fahrzeug-checkbox\"]"
|
||||
'[data-cy="test-lehrgang-lp-circle-fahrzeug-lc-handlungsfeld-fahrzeug-checkbox"]'
|
||||
).should("have.class", "cy-checked");
|
||||
|
||||
// completion data should still be there after reload
|
||||
cy.reload();
|
||||
cy.get(
|
||||
"[data-cy=\"test-lehrgang-lp-circle-fahrzeug-lc-handlungsfeld-fahrzeug-checkbox\"]"
|
||||
'[data-cy="test-lehrgang-lp-circle-fahrzeug-lc-handlungsfeld-fahrzeug-checkbox"]'
|
||||
).should("have.class", "cy-checked");
|
||||
});
|
||||
|
||||
it("can open learning contents and complete them by continuing", () => {
|
||||
cy.get(
|
||||
"[data-cy=\"test-lehrgang-lp-circle-fahrzeug-lc-verschaffe-dir-einen-überblick\"]"
|
||||
'[data-cy="test-lehrgang-lp-circle-fahrzeug-lc-verschaffe-dir-einen-überblick"]'
|
||||
).click();
|
||||
cy.get("[data-cy=\"lc-title\"]").should(
|
||||
cy.get('[data-cy="lc-title"]').should(
|
||||
"contain",
|
||||
"Verschaffe dir einen Überblick"
|
||||
);
|
||||
cy.get("[data-cy=\"complete-and-continue\"]").click({ force: true });
|
||||
cy.get("[data-cy=\"circle-title\"]").should("contain", "Fahrzeug");
|
||||
cy.get('[data-cy="complete-and-continue"]').click({ force: true });
|
||||
cy.get('[data-cy="circle-title"]').should("contain", "Fahrzeug");
|
||||
|
||||
cy.get("[data-cy=\"ls-continue-button\"]").click({ force: true });
|
||||
cy.get("[data-cy=\"lc-title\"]").should(
|
||||
cy.get('[data-cy="ls-continue-button"]').click();
|
||||
cy.get('[data-cy="lc-title"]').should(
|
||||
"contain",
|
||||
"Handlungsfeld «Fahrzeug»"
|
||||
);
|
||||
cy.get("[data-cy=\"complete-and-continue\"]").click({ force: true });
|
||||
cy.get("[data-cy=\"circle-title\"]").should("contain", "Fahrzeug");
|
||||
cy.get('[data-cy="complete-and-continue"]').click({ force: true });
|
||||
cy.get('[data-cy="circle-title"]').should("contain", "Fahrzeug");
|
||||
|
||||
cy.get(
|
||||
"[data-cy=\"test-lehrgang-lp-circle-fahrzeug-lc-verschaffe-dir-einen-überblick-checkbox\"]"
|
||||
'[data-cy="test-lehrgang-lp-circle-fahrzeug-lc-verschaffe-dir-einen-überblick-checkbox"]'
|
||||
).should("have.class", "cy-checked");
|
||||
cy.get(
|
||||
"[data-cy=\"test-lehrgang-lp-circle-fahrzeug-lc-handlungsfeld-fahrzeug-checkbox\"]"
|
||||
'[data-cy="test-lehrgang-lp-circle-fahrzeug-lc-handlungsfeld-fahrzeug-checkbox"]'
|
||||
).should("have.class", "cy-checked");
|
||||
});
|
||||
|
||||
it("continue button works", () => {
|
||||
cy.get("[data-cy=\"ls-continue-button\"]").should("contain", "Los geht's");
|
||||
cy.get("[data-cy=\"ls-continue-button\"]").click();
|
||||
cy.get('[data-cy="ls-continue-button"]').should("contain", "Los geht's");
|
||||
cy.get('[data-cy="ls-continue-button"]').click();
|
||||
|
||||
cy.get("[data-cy=\"lc-title\"]").should(
|
||||
cy.get('[data-cy="lc-title"]').should(
|
||||
"contain",
|
||||
"Verschaffe dir einen Überblick"
|
||||
);
|
||||
cy.get("[data-cy=\"complete-and-continue\"]").click({ force: true });
|
||||
cy.get('[data-cy="complete-and-continue"]').click({ force: true });
|
||||
|
||||
cy.get("[data-cy=\"ls-continue-button\"]").should("contain", "Weiter geht's");
|
||||
cy.get("[data-cy=\"ls-continue-button\"]").click();
|
||||
cy.get("[data-cy=\"lc-title\"]").should(
|
||||
cy.get('[data-cy="ls-continue-button"]').should("contain", "Weiter geht's");
|
||||
cy.get('[data-cy="ls-continue-button"]').click();
|
||||
cy.get('[data-cy="lc-title"]').should(
|
||||
"contain",
|
||||
"Handlungsfeld «Fahrzeug»"
|
||||
);
|
||||
|
|
@ -81,43 +81,43 @@ describe("circle.cy.js", () => {
|
|||
|
||||
it("can open learning content by url", () => {
|
||||
cy.visit("/course/test-lehrgang/learn/fahrzeug/handlungsfeld-fahrzeug");
|
||||
cy.get("[data-cy=\"lc-title\"]").should(
|
||||
cy.get('[data-cy="lc-title"]').should(
|
||||
"contain",
|
||||
"Handlungsfeld «Fahrzeug»"
|
||||
);
|
||||
|
||||
cy.get("[data-cy=\"close-learning-content\"]").click();
|
||||
cy.get("[data-cy=\"circle-title\"]").should("contain", "Fahrzeug");
|
||||
cy.get('[data-cy="close-learning-content"]').click();
|
||||
cy.get('[data-cy="circle-title"]').should("contain", "Fahrzeug");
|
||||
});
|
||||
|
||||
it("checks number of sequences and contents", () => {
|
||||
cy.get("[data-cy=\"lp-learning-sequence\"]").should("have.length", 3);
|
||||
cy.get("[data-cy=\"lp-learning-sequence\"]")
|
||||
cy.get('[data-cy="lp-learning-sequence"]').should("have.length", 3);
|
||||
cy.get('[data-cy="lp-learning-sequence"]')
|
||||
.first()
|
||||
.should("contain", "Vorbereitung");
|
||||
cy.get("[data-cy=\"lp-learning-sequence\"]")
|
||||
cy.get('[data-cy="lp-learning-sequence"]')
|
||||
.eq(1)
|
||||
.should("contain", "Training");
|
||||
cy.get("[data-cy=\"lp-learning-sequence\"]")
|
||||
cy.get('[data-cy="lp-learning-sequence"]')
|
||||
.last()
|
||||
.should("contain", "Transfer");
|
||||
|
||||
cy.get("[data-cy=\"lp-learning-content\"]").should("have.length", 10);
|
||||
cy.get("[data-cy=\"lp-learning-content\"]")
|
||||
cy.get('[data-cy="lp-learning-content"]').should("have.length", 10);
|
||||
cy.get('[data-cy="lp-learning-content"]')
|
||||
.first()
|
||||
.should("contain", "Verschaffe dir einen Überblick");
|
||||
cy.get("[data-cy=\"lp-learning-content\"]")
|
||||
cy.get('[data-cy="lp-learning-content"]')
|
||||
.eq(4)
|
||||
.should("contain", "Präsenzkurs Fahrzeug");
|
||||
cy.get("[data-cy=\"lp-learning-content\"]")
|
||||
cy.get('[data-cy="lp-learning-content"]')
|
||||
.eq(7)
|
||||
.should("contain", "Reflexion");
|
||||
cy.get("[data-cy=\"lp-learning-content\"]")
|
||||
cy.get('[data-cy="lp-learning-content"]')
|
||||
.last()
|
||||
.should("contain", "Feedback");
|
||||
|
||||
cy.visit("/course/test-lehrgang/learn/reisen");
|
||||
cy.get("[data-cy=\"lp-learning-sequence\"]").should("have.length", 3);
|
||||
cy.get("[data-cy=\"lp-learning-content\"]").should("have.length", 9);
|
||||
cy.get('[data-cy="lp-learning-sequence"]').should("have.length", 3);
|
||||
cy.get('[data-cy="lp-learning-content"]').should("have.length", 9);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -133,6 +133,7 @@ LOCAL_APPS = [
|
|||
"vbv_lernwelt.course_session_group",
|
||||
"vbv_lernwelt.shop",
|
||||
"vbv_lernwelt.learning_mentor",
|
||||
"vbv_lernwelt.self_evaluation_feedback",
|
||||
]
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
|
||||
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS
|
||||
|
|
|
|||
|
|
@ -141,6 +141,9 @@ urlpatterns = [
|
|||
|
||||
path("api/mentor/<signed_int:course_session_id>/", include("vbv_lernwelt.learning_mentor.urls")),
|
||||
|
||||
# self evaluation feedback
|
||||
path("api/self-evaluation-feedback/", include("vbv_lernwelt.self_evaluation_feedback.urls")),
|
||||
|
||||
# assignment
|
||||
path(
|
||||
r"api/assignment/<signed_int:assignment_id>/<signed_int:course_session_id>/status/",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
# Generated by Django 3.2.20 on 2024-01-24 09:04
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("assignment", "0011_assignment_solution_sample"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="assignment",
|
||||
name="assignment_type",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("PRAXIS_ASSIGNMENT", "PRAXIS_ASSIGNMENT"),
|
||||
("CASEWORK", "CASEWORK"),
|
||||
("PREP_ASSIGNMENT", "PREP_ASSIGNMENT"),
|
||||
("REFLECTION", "REFLECTION"),
|
||||
("CONDITION_ACCEPTANCE", "CONDITION_ACCEPTANCE"),
|
||||
("EDONIQ_TEST", "EDONIQ_TEST"),
|
||||
],
|
||||
default="CASEWORK",
|
||||
max_length=50,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="assignment",
|
||||
name="needs_expert_evaluation",
|
||||
field=models.BooleanField(
|
||||
default=False,
|
||||
help_text="Muss der Auftrag durch eine/n Experten/in oder eine Lernbegleitung beurteilt werden?",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
@ -25,6 +25,7 @@ TEST_STUDENT1_USER_ID = "65c73ad0-6d53-43a9-a4a4-64143f27b03a"
|
|||
TEST_STUDENT2_USER_ID = "19c40d94-15cc-4198-aaad-ef707c4b0900"
|
||||
TEST_STUDENT3_USER_ID = "bcf94dba-53bc-474b-a22d-e4af39aa042b"
|
||||
TEST_MENTOR1_USER_ID = "d1f5f5a9-5b0a-4e1a-9e1a-9e9b5b5e1b1b"
|
||||
TEST_STUDENT1_VV_USER_ID = "5ff59857-8de5-415e-a387-4449f9a0337a"
|
||||
|
||||
TEST_COURSE_SESSION_BERN_ID = -1
|
||||
TEST_COURSE_SESSION_ZURICH_ID = -2
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ from vbv_lernwelt.core.constants import (
|
|||
ADMIN_USER_ID,
|
||||
TEST_MENTOR1_USER_ID,
|
||||
TEST_STUDENT1_USER_ID,
|
||||
TEST_STUDENT1_VV_USER_ID,
|
||||
TEST_STUDENT2_USER_ID,
|
||||
TEST_STUDENT3_USER_ID,
|
||||
TEST_SUPERVISOR1_USER_ID,
|
||||
|
|
@ -210,9 +211,10 @@ def create_default_users(default_password="test", set_avatar=False):
|
|||
last_name="Expert3",
|
||||
)
|
||||
_create_student_user(
|
||||
id=TEST_STUDENT1_VV_USER_ID,
|
||||
email="student-vv@eiger-versicherungen.ch",
|
||||
first_name="Student",
|
||||
last_name="VV",
|
||||
first_name="Viktor",
|
||||
last_name="Vollgas",
|
||||
)
|
||||
_create_student_user(
|
||||
email="patrizia.huggel@eiger-versicherungen.ch",
|
||||
|
|
@ -364,10 +366,11 @@ def create_default_users(default_password="test", set_avatar=False):
|
|||
_create_user(
|
||||
_id=TEST_MENTOR1_USER_ID,
|
||||
email="test-mentor1@example.com",
|
||||
first_name="[Mentor]",
|
||||
last_name="Mentor",
|
||||
first_name="Micheala",
|
||||
last_name="Weber-Mentor",
|
||||
password=default_password,
|
||||
language="de",
|
||||
avatar_image="uk1.patrizia.huggel.jpg",
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ from vbv_lernwelt.core.constants import (
|
|||
TEST_STUDENT3_USER_ID,
|
||||
TEST_TRAINER1_USER_ID,
|
||||
)
|
||||
from vbv_lernwelt.core.models import User
|
||||
from vbv_lernwelt.core.models import Organisation, User
|
||||
from vbv_lernwelt.course.consts import COURSE_TEST_ID
|
||||
from vbv_lernwelt.course.creators.test_course import (
|
||||
create_edoniq_test_result_data,
|
||||
|
|
@ -108,6 +108,7 @@ def command(
|
|||
CourseSessionAttendanceCourse.objects.all().update(attendance_user_list=[])
|
||||
|
||||
LearningMentor.objects.all().delete()
|
||||
User.objects.all().update(organisation=Organisation.objects.first())
|
||||
User.objects.all().update(language="de")
|
||||
User.objects.all().update(additional_json_data={})
|
||||
|
||||
|
|
@ -331,16 +332,32 @@ def command(
|
|||
attendance_course.save()
|
||||
|
||||
if create_learning_mentor:
|
||||
print("Create learning mentor")
|
||||
mentor = LearningMentor.objects.create(
|
||||
uk_mentor = LearningMentor.objects.create(
|
||||
course=Course.objects.get(id=COURSE_TEST_ID),
|
||||
mentor=User.objects.get(id=TEST_MENTOR1_USER_ID),
|
||||
)
|
||||
course_session = CourseSession.objects.get(id=TEST_COURSE_SESSION_BERN_ID)
|
||||
csu = CourseSessionUser.objects.get(
|
||||
user__id=TEST_STUDENT1_USER_ID, course_session=course_session
|
||||
uk_mentor.participants.add(
|
||||
CourseSessionUser.objects.get(
|
||||
user__id=TEST_STUDENT1_USER_ID,
|
||||
course_session=CourseSession.objects.get(
|
||||
id=TEST_COURSE_SESSION_BERN_ID
|
||||
),
|
||||
)
|
||||
)
|
||||
mentor.participants.add(csu)
|
||||
|
||||
# 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
|
||||
# )
|
||||
# )
|
||||
|
||||
course = Course.objects.get(id=COURSE_TEST_ID)
|
||||
course.enable_circle_documents = enable_circle_documents
|
||||
|
|
|
|||
|
|
@ -167,6 +167,10 @@ def cypress_reset_view(request):
|
|||
request.data.get("create_attendance_days") == "true"
|
||||
)
|
||||
|
||||
options["create_learning_mentor"] = (
|
||||
request.data.get("create_learning_mentor") == "true"
|
||||
)
|
||||
|
||||
call_command(
|
||||
"cypress_reset",
|
||||
**options,
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ from vbv_lernwelt.learnpath.models import (
|
|||
LearningContentAssignment,
|
||||
LearningContentEdoniqTest,
|
||||
LearningPath,
|
||||
LearningUnit,
|
||||
)
|
||||
from vbv_lernwelt.learnpath.tests.learning_path_factories import (
|
||||
CircleFactory,
|
||||
|
|
@ -58,11 +59,13 @@ from vbv_lernwelt.learnpath.tests.learning_path_factories import (
|
|||
)
|
||||
|
||||
|
||||
def create_course(title: str, _id=None) -> Tuple[Course, CoursePage]:
|
||||
def create_course(
|
||||
title: str = "Course Title", _id=None, course_page_title: str = "Test Lehrgang"
|
||||
) -> Tuple[Course, CoursePage]:
|
||||
course = Course.objects.create(id=_id, title=title, category_name="Handlungsfeld")
|
||||
|
||||
course_page = CoursePageFactory(
|
||||
title="Test Lehrgang",
|
||||
title=course_page_title,
|
||||
parent=get_wagtail_default_site().root_page,
|
||||
course=course,
|
||||
)
|
||||
|
|
@ -268,10 +271,28 @@ def create_course_session_edoniq_test(
|
|||
return cset
|
||||
|
||||
|
||||
def create_learning_unit(
|
||||
circle: Circle,
|
||||
course: Course,
|
||||
course_category_title: str = "Course Category",
|
||||
) -> LearningUnit:
|
||||
cat, _ = CourseCategory.objects.get_or_create(
|
||||
course=course,
|
||||
title=course_category_title,
|
||||
)
|
||||
|
||||
return LearningUnitFactory(
|
||||
title="Learning Unit",
|
||||
parent=circle,
|
||||
course_category=cat,
|
||||
)
|
||||
|
||||
|
||||
def create_performance_criteria_page(
|
||||
course: Course,
|
||||
course_page: CoursePage,
|
||||
circle: Circle,
|
||||
learning_unit: LearningUnitFactory | None = None,
|
||||
) -> PerformanceCriteria:
|
||||
competence_navi_page = CompetenceNaviPageFactory(
|
||||
title="Competence Navi",
|
||||
|
|
@ -290,17 +311,14 @@ def create_performance_criteria_page(
|
|||
items=[("item", "Action Competence Item")],
|
||||
)
|
||||
|
||||
cat, _ = CourseCategory.objects.get_or_create(
|
||||
course=course, title="Course Category"
|
||||
)
|
||||
|
||||
lu = LearningUnitFactory(title="Learning Unit", parent=circle, course_category=cat)
|
||||
if not learning_unit:
|
||||
learning_unit = create_learning_unit(circle=circle, course=course)
|
||||
|
||||
return PerformanceCriteriaFactory(
|
||||
parent=action_competence,
|
||||
competence_id="X1.1",
|
||||
title="Performance Criteria",
|
||||
learning_unit=lu,
|
||||
learning_unit=learning_unit,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
# Generated by Django 3.2.20 on 2024-01-24 09:04
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("duedate", "0008_auto_20231108_0747"),
|
||||
("course_session", "0005_auto_20230825_1723"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="coursesessionassignment",
|
||||
name="evaluation_deadline",
|
||||
field=models.OneToOneField(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="assignment_evaluation_deadline",
|
||||
to="duedate.duedate",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="coursesessionassignment",
|
||||
name="submission_deadline",
|
||||
field=models.OneToOneField(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="assignment_submission_deadline",
|
||||
to="duedate.duedate",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
@ -45,6 +45,7 @@ class Migration(migrations.Migration):
|
|||
dependencies = [
|
||||
("duedate", "0004_alter_duedate_start"),
|
||||
("learnpath", "0008_add_edoniq_sequence_id"),
|
||||
("course_session", "0005_auto_20230825_1723"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
|
|
|||
|
|
@ -10,9 +10,10 @@ from vbv_lernwelt.core.models import User
|
|||
from vbv_lernwelt.course.models import CourseSession
|
||||
from vbv_lernwelt.course_session.models import CourseSessionAssignment
|
||||
from vbv_lernwelt.learning_mentor.entities import (
|
||||
CompletionStatus,
|
||||
PraxisAssignmentCompletion,
|
||||
PraxisAssignmentStatus,
|
||||
MentorAssignmentCompletion,
|
||||
MentorAssignmentStatus,
|
||||
MentorAssignmentStatusType,
|
||||
MentorCompletionStatus,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -21,7 +22,7 @@ def get_assignment_completions(
|
|||
assignment: Assignment,
|
||||
participants: List[User],
|
||||
evaluation_user: User,
|
||||
) -> List[PraxisAssignmentCompletion]:
|
||||
) -> List[MentorAssignmentCompletion]:
|
||||
evaluation_results = AssignmentCompletion.objects.filter(
|
||||
assignment_user__in=participants,
|
||||
course_session=course_session,
|
||||
|
|
@ -34,14 +35,14 @@ def get_assignment_completions(
|
|||
completion_status = result["completion_status"]
|
||||
|
||||
if completion_status == AssignmentCompletionStatus.EVALUATION_SUBMITTED.value:
|
||||
status = CompletionStatus.EVALUATED
|
||||
status = MentorCompletionStatus.EVALUATED
|
||||
elif completion_status in [
|
||||
AssignmentCompletionStatus.SUBMITTED.value,
|
||||
AssignmentCompletionStatus.EVALUATION_IN_PROGRESS.value,
|
||||
]:
|
||||
status = CompletionStatus.SUBMITTED
|
||||
status = MentorCompletionStatus.SUBMITTED
|
||||
else:
|
||||
status = CompletionStatus.UNKNOWN
|
||||
status = MentorCompletionStatus.UNKNOWN
|
||||
|
||||
user_status_map[result["assignment_user"]] = (
|
||||
status,
|
||||
|
|
@ -49,25 +50,25 @@ def get_assignment_completions(
|
|||
)
|
||||
|
||||
status_priority = {
|
||||
CompletionStatus.SUBMITTED: 1,
|
||||
CompletionStatus.EVALUATED: 2,
|
||||
CompletionStatus.UNKNOWN: 3,
|
||||
MentorCompletionStatus.SUBMITTED: 1,
|
||||
MentorCompletionStatus.EVALUATED: 2,
|
||||
MentorCompletionStatus.UNKNOWN: 3,
|
||||
}
|
||||
|
||||
sorted_participants = sorted(
|
||||
participants,
|
||||
key=lambda u: (
|
||||
status_priority.get(
|
||||
user_status_map.get(u.id, (CompletionStatus.UNKNOWN, ""))[0]
|
||||
user_status_map.get(u.id, (MentorCompletionStatus.UNKNOWN, ""))[0]
|
||||
),
|
||||
user_status_map.get(u.id, ("", u.last_name))[1],
|
||||
),
|
||||
)
|
||||
|
||||
return [
|
||||
PraxisAssignmentCompletion(
|
||||
MentorAssignmentCompletion(
|
||||
status=user_status_map.get(
|
||||
user.id, (CompletionStatus.UNKNOWN, user.last_name)
|
||||
user.id, (MentorCompletionStatus.UNKNOWN, user.last_name)
|
||||
)[0],
|
||||
user_id=user.id,
|
||||
last_name=user.last_name,
|
||||
|
|
@ -79,7 +80,7 @@ def get_assignment_completions(
|
|||
|
||||
def get_praxis_assignments(
|
||||
course_session: CourseSession, participants: List[User], evaluation_user: User
|
||||
) -> Tuple[List[PraxisAssignmentStatus], Set[int]]:
|
||||
) -> Tuple[List[MentorAssignmentStatus], Set[int]]:
|
||||
records = []
|
||||
circle_ids = set()
|
||||
|
||||
|
|
@ -105,19 +106,20 @@ def get_praxis_assignments(
|
|||
[
|
||||
completion
|
||||
for completion in completions
|
||||
if completion.status == CompletionStatus.SUBMITTED
|
||||
if completion.status == MentorCompletionStatus.SUBMITTED
|
||||
]
|
||||
)
|
||||
|
||||
circle_id = learning_content.get_circle().id
|
||||
|
||||
records.append(
|
||||
PraxisAssignmentStatus(
|
||||
MentorAssignmentStatus(
|
||||
id=course_session_assignment.id,
|
||||
title=learning_content.content_assignment.title,
|
||||
circle_id=circle_id,
|
||||
pending_evaluations=submitted_count,
|
||||
completions=completions,
|
||||
type=MentorAssignmentStatusType.PRAXIS_ASSIGNMENT,
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,108 @@
|
|||
from typing import List, Set, Tuple
|
||||
|
||||
import structlog
|
||||
|
||||
from vbv_lernwelt.core.models import User
|
||||
from vbv_lernwelt.course.models import Course
|
||||
from vbv_lernwelt.learning_mentor.entities import (
|
||||
MentorAssignmentCompletion,
|
||||
MentorAssignmentStatus,
|
||||
MentorAssignmentStatusType,
|
||||
MentorCompletionStatus,
|
||||
)
|
||||
from vbv_lernwelt.learnpath.models import (
|
||||
LearningUnit,
|
||||
LearningUnitPerformanceFeedbackType,
|
||||
)
|
||||
from vbv_lernwelt.self_evaluation_feedback.models import SelfEvaluationFeedback
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
def create_blank_completions_non_requesters(
|
||||
completions: List[MentorAssignmentCompletion],
|
||||
participants: List[User],
|
||||
) -> List[MentorAssignmentCompletion]:
|
||||
non_requester_completions = []
|
||||
|
||||
participants_user_ids = set([str(p.id) for p in participants])
|
||||
completion_seen_user_ids = set([str(c.user_id) for c in completions])
|
||||
|
||||
user_by_id = {str(p.id): p for p in participants}
|
||||
for non_requester_user_id in participants_user_ids - completion_seen_user_ids:
|
||||
non_requester_user = user_by_id[non_requester_user_id]
|
||||
|
||||
non_requester_completions.append(
|
||||
MentorAssignmentCompletion(
|
||||
status=MentorCompletionStatus.UNKNOWN,
|
||||
user_id=non_requester_user.id,
|
||||
last_name=non_requester_user.last_name,
|
||||
url="",
|
||||
)
|
||||
)
|
||||
|
||||
return non_requester_completions
|
||||
|
||||
|
||||
def get_self_feedback_evaluation(
|
||||
participants: List[User],
|
||||
evaluation_user: User,
|
||||
course: Course,
|
||||
) -> Tuple[List[MentorAssignmentStatus], Set[int]]:
|
||||
records: List[MentorAssignmentStatus] = []
|
||||
circle_ids: Set[int] = set()
|
||||
|
||||
if not participants:
|
||||
return records, circle_ids
|
||||
|
||||
# very unfortunate: we can't simply get all SelfEvaluationFeedback objects since then
|
||||
# we would miss the one where no feedback was requested -> so we get all learning units
|
||||
# and check if we have to take them into account (course, feedback type, etc.)
|
||||
for learning_unit in LearningUnit.objects.filter(
|
||||
feedback_user=LearningUnitPerformanceFeedbackType.MENTOR_FEEDBACK.value,
|
||||
course_category__course_id=course.id,
|
||||
):
|
||||
feedbacks = SelfEvaluationFeedback.objects.filter(
|
||||
learning_unit=learning_unit,
|
||||
feedback_requester_user__in=participants,
|
||||
feedback_provider_user=evaluation_user,
|
||||
)
|
||||
|
||||
circle_id = learning_unit.get_circle().id
|
||||
circle_ids.add(circle_id)
|
||||
|
||||
pending_evaluations = len([f for f in feedbacks if not f.feedback_submitted])
|
||||
|
||||
completions = [
|
||||
MentorAssignmentCompletion(
|
||||
# feedback_submitted as seen from the perspective of the evaluation user (feedback provider)
|
||||
# means that the feedback has been evaluated by the feedback provider, hence the status is EVALUATED
|
||||
status=MentorCompletionStatus.EVALUATED
|
||||
if f.feedback_submitted
|
||||
else MentorCompletionStatus.SUBMITTED,
|
||||
user_id=f.feedback_requester_user.id,
|
||||
last_name=f.feedback_requester_user.last_name,
|
||||
url=f"/course/{course.slug}/cockpit/mentor/self-evaluation-feedback/{f.learning_unit.id}",
|
||||
)
|
||||
for f in feedbacks
|
||||
]
|
||||
|
||||
# requesting feedback is optional, so we need to add blank completions
|
||||
# for those mentees who did not request a feedback
|
||||
completions += create_blank_completions_non_requesters(
|
||||
completions=completions,
|
||||
participants=participants,
|
||||
)
|
||||
|
||||
records.append(
|
||||
MentorAssignmentStatus(
|
||||
id=learning_unit.id,
|
||||
title=learning_unit.title,
|
||||
circle_id=circle_id,
|
||||
pending_evaluations=pending_evaluations,
|
||||
completions=completions,
|
||||
type=MentorAssignmentStatusType.SELF_EVALUATION_FEEDBACK,
|
||||
)
|
||||
)
|
||||
|
||||
return records, circle_ids
|
||||
|
|
@ -3,24 +3,30 @@ from enum import Enum
|
|||
from typing import List
|
||||
|
||||
|
||||
class CompletionStatus(str, Enum):
|
||||
class MentorCompletionStatus(str, Enum):
|
||||
UNKNOWN = "UNKNOWN"
|
||||
SUBMITTED = "SUBMITTED"
|
||||
EVALUATED = "EVALUATED"
|
||||
|
||||
|
||||
class MentorAssignmentStatusType(str, Enum):
|
||||
PRAXIS_ASSIGNMENT = "praxis_assignment"
|
||||
SELF_EVALUATION_FEEDBACK = "self_evaluation_feedback"
|
||||
|
||||
|
||||
@dataclass
|
||||
class PraxisAssignmentCompletion:
|
||||
status: CompletionStatus
|
||||
class MentorAssignmentCompletion:
|
||||
status: MentorCompletionStatus
|
||||
user_id: str
|
||||
last_name: str
|
||||
url: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class PraxisAssignmentStatus:
|
||||
class MentorAssignmentStatus:
|
||||
id: str
|
||||
title: str
|
||||
circle_id: str
|
||||
pending_evaluations: int
|
||||
completions: List[PraxisAssignmentCompletion]
|
||||
completions: List[MentorAssignmentCompletion]
|
||||
type: MentorAssignmentStatusType
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from vbv_lernwelt.core.serializers import UserSerializer
|
|||
from vbv_lernwelt.learning_mentor.models import LearningMentor, MentorInvitation
|
||||
|
||||
|
||||
class PraxisAssignmentCompletionSerializer(serializers.Serializer):
|
||||
class MentorAssignmentCompletionSerializer(serializers.Serializer):
|
||||
status = serializers.SerializerMethodField()
|
||||
user_id = serializers.CharField()
|
||||
last_name = serializers.CharField()
|
||||
|
|
@ -15,13 +15,13 @@ class PraxisAssignmentCompletionSerializer(serializers.Serializer):
|
|||
return obj.status.value
|
||||
|
||||
|
||||
class PraxisAssignmentStatusSerializer(serializers.Serializer):
|
||||
class MentorAssignmentStatusSerializer(serializers.Serializer):
|
||||
id = serializers.CharField()
|
||||
title = serializers.CharField()
|
||||
circle_id = serializers.CharField()
|
||||
pending_evaluations = serializers.IntegerField()
|
||||
completions = PraxisAssignmentCompletionSerializer(many=True)
|
||||
type = serializers.ReadOnlyField(default="praxis_assignment")
|
||||
completions = MentorAssignmentCompletionSerializer(many=True)
|
||||
type = serializers.ReadOnlyField()
|
||||
|
||||
|
||||
class InvitationSerializer(serializers.ModelSerializer):
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ from vbv_lernwelt.learning_mentor.content.praxis_assignment import (
|
|||
get_assignment_completions,
|
||||
get_praxis_assignments,
|
||||
)
|
||||
from vbv_lernwelt.learning_mentor.entities import CompletionStatus
|
||||
from vbv_lernwelt.learning_mentor.entities import MentorCompletionStatus
|
||||
|
||||
|
||||
class AttendanceServicesTestCase(TestCase):
|
||||
|
|
@ -74,10 +74,10 @@ class AttendanceServicesTestCase(TestCase):
|
|||
# THEN
|
||||
expected_order = ["Beta", "Alpha", "Gamma", "Kappa"]
|
||||
expected_statuses = {
|
||||
"Alpha": CompletionStatus.EVALUATED, # user1
|
||||
"Beta": CompletionStatus.SUBMITTED, # user2
|
||||
"Gamma": CompletionStatus.UNKNOWN, # user4 (no AssignmentCompletion)
|
||||
"Kappa": CompletionStatus.UNKNOWN, # user3 (IN_PROGRESS should be PENDING)
|
||||
"Alpha": MentorCompletionStatus.EVALUATED, # user1
|
||||
"Beta": MentorCompletionStatus.SUBMITTED, # user2
|
||||
"Gamma": MentorCompletionStatus.UNKNOWN, # user4 (no AssignmentCompletion)
|
||||
"Kappa": MentorCompletionStatus.UNKNOWN, # user3 (IN_PROGRESS should be PENDING)
|
||||
}
|
||||
|
||||
self.assertEqual(len(results), len(participants))
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
from typing import Dict, List, Optional
|
||||
|
||||
from django.urls import reverse
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
|
|
@ -7,6 +9,7 @@ from vbv_lernwelt.assignment.models import (
|
|||
AssignmentCompletionStatus,
|
||||
AssignmentType,
|
||||
)
|
||||
from vbv_lernwelt.core.admin import User
|
||||
from vbv_lernwelt.course.creators.test_utils import (
|
||||
add_course_session_user,
|
||||
create_assignment,
|
||||
|
|
@ -15,10 +18,22 @@ from vbv_lernwelt.course.creators.test_utils import (
|
|||
create_course,
|
||||
create_course_session,
|
||||
create_course_session_assignment,
|
||||
create_learning_unit,
|
||||
create_user,
|
||||
)
|
||||
from vbv_lernwelt.course.models import CourseSessionUser
|
||||
from vbv_lernwelt.learning_mentor.models import LearningMentor
|
||||
from vbv_lernwelt.learnpath.models import LearningUnitPerformanceFeedbackType
|
||||
from vbv_lernwelt.self_evaluation_feedback.models import SelfEvaluationFeedback
|
||||
|
||||
|
||||
def get_completion_for_user(
|
||||
completions: List[Dict[str, str]], user: User
|
||||
) -> Optional[Dict[str, str]]:
|
||||
for completion in completions:
|
||||
if completion["user_id"] == str(user.id):
|
||||
return completion
|
||||
return None
|
||||
|
||||
|
||||
class LearningMentorAPITest(APITestCase):
|
||||
|
|
@ -28,15 +43,6 @@ class LearningMentorAPITest(APITestCase):
|
|||
|
||||
self.circle, _ = create_circle(title="Circle", course_page=self.course_page)
|
||||
|
||||
self.assignment = create_assignment(
|
||||
course=self.course, assignment_type=AssignmentType.PRAXIS_ASSIGNMENT
|
||||
)
|
||||
|
||||
lca = create_assignment_learning_content(self.circle, self.assignment)
|
||||
create_course_session_assignment(
|
||||
course_session=self.course_session, learning_content_assignment=lca
|
||||
)
|
||||
|
||||
self.mentor = create_user("mentor")
|
||||
self.participant_1 = add_course_session_user(
|
||||
self.course_session,
|
||||
|
|
@ -109,7 +115,7 @@ class LearningMentorAPITest(APITestCase):
|
|||
self.assertEqual(participant_1["first_name"], "Test")
|
||||
self.assertEqual(participant_1["last_name"], "Participant_1")
|
||||
|
||||
def test_api_praxis_assignments(self) -> None:
|
||||
def test_api_self_evaluation_feedback(self) -> None:
|
||||
# GIVEN
|
||||
participants = [self.participant_1, self.participant_2, self.participant_3]
|
||||
self.client.force_login(self.mentor)
|
||||
|
|
@ -118,12 +124,104 @@ class LearningMentorAPITest(APITestCase):
|
|||
mentor=self.mentor,
|
||||
course=self.course_session.course,
|
||||
)
|
||||
|
||||
mentor.participants.set(participants)
|
||||
|
||||
learning_unit = create_learning_unit(
|
||||
circle=self.circle,
|
||||
course=self.course,
|
||||
)
|
||||
|
||||
# performance criteria under this learning unit shall be evaluated by the mentor
|
||||
learning_unit.feedback_user = (
|
||||
LearningUnitPerformanceFeedbackType.MENTOR_FEEDBACK.name
|
||||
)
|
||||
|
||||
learning_unit.save()
|
||||
|
||||
# 1: we already evaluated
|
||||
SelfEvaluationFeedback.objects.create(
|
||||
feedback_requester_user=self.participant_1.user,
|
||||
feedback_provider_user=self.mentor,
|
||||
learning_unit=learning_unit,
|
||||
feedback_submitted=True,
|
||||
)
|
||||
|
||||
# 2: we have not evaluated yet
|
||||
SelfEvaluationFeedback.objects.create(
|
||||
feedback_requester_user=self.participant_2.user,
|
||||
feedback_provider_user=self.mentor,
|
||||
learning_unit=learning_unit,
|
||||
feedback_submitted=False,
|
||||
)
|
||||
|
||||
# 3: did not request feedback
|
||||
# ...
|
||||
|
||||
# WHEN
|
||||
response = self.client.get(self.url)
|
||||
|
||||
# THEN
|
||||
assignments = response.data["assignments"]
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
self.assertEqual(
|
||||
response.data["circles"],
|
||||
[{"id": self.circle.id, "title": self.circle.title}],
|
||||
)
|
||||
|
||||
self.assertEqual(len(assignments), 1)
|
||||
assignment = assignments[0]
|
||||
|
||||
self.assertEqual(assignment["type"], "self_evaluation_feedback")
|
||||
self.assertEqual(assignment["pending_evaluations"], 1)
|
||||
|
||||
completions = assignment["completions"]
|
||||
self.assertEqual(
|
||||
len(completions),
|
||||
3,
|
||||
)
|
||||
|
||||
completion_1 = get_completion_for_user(completions, self.participant_1.user)
|
||||
self.assertEqual(completion_1["status"], "EVALUATED")
|
||||
self.assertEqual(completion_1["last_name"], "Participant_1")
|
||||
self.assertEqual(completion_1["user_id"], str(self.participant_1.user.id))
|
||||
|
||||
completion_2 = get_completion_for_user(completions, self.participant_2.user)
|
||||
self.assertEqual(completion_2["status"], "SUBMITTED")
|
||||
self.assertEqual(completion_2["last_name"], "Participant_2")
|
||||
self.assertEqual(completion_2["user_id"], str(self.participant_2.user.id))
|
||||
|
||||
completion_3 = get_completion_for_user(completions, self.participant_3.user)
|
||||
self.assertEqual(completion_3["status"], "UNKNOWN")
|
||||
self.assertEqual(completion_3["last_name"], "Participant_3")
|
||||
self.assertEqual(completion_3["user_id"], str(self.participant_3.user.id))
|
||||
|
||||
def test_api_praxis_assignments(self) -> None:
|
||||
# GIVEN
|
||||
self.client.force_login(self.mentor)
|
||||
|
||||
assignment = create_assignment(
|
||||
course=self.course, assignment_type=AssignmentType.PRAXIS_ASSIGNMENT
|
||||
)
|
||||
|
||||
lca = create_assignment_learning_content(self.circle, assignment)
|
||||
create_course_session_assignment(
|
||||
course_session=self.course_session, learning_content_assignment=lca
|
||||
)
|
||||
|
||||
mentor = LearningMentor.objects.create(
|
||||
mentor=self.mentor,
|
||||
course=self.course_session.course,
|
||||
)
|
||||
|
||||
participants = [self.participant_1, self.participant_2, self.participant_3]
|
||||
mentor.participants.set(participants)
|
||||
|
||||
AssignmentCompletion.objects.create(
|
||||
assignment_user=self.participant_1.user,
|
||||
course_session=self.course_session,
|
||||
assignment=self.assignment,
|
||||
assignment=assignment,
|
||||
completion_status=AssignmentCompletionStatus.EVALUATION_SUBMITTED.value,
|
||||
evaluation_user=self.mentor,
|
||||
)
|
||||
|
|
@ -131,7 +229,7 @@ class LearningMentorAPITest(APITestCase):
|
|||
AssignmentCompletion.objects.create(
|
||||
assignment_user=self.participant_3.user,
|
||||
course_session=self.course_session,
|
||||
assignment=self.assignment,
|
||||
assignment=assignment,
|
||||
completion_status=AssignmentCompletionStatus.SUBMITTED.value,
|
||||
evaluation_user=self.mentor,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -12,11 +12,14 @@ from vbv_lernwelt.iam.permissions import has_role_in_course, is_course_session_m
|
|||
from vbv_lernwelt.learning_mentor.content.praxis_assignment import (
|
||||
get_praxis_assignments,
|
||||
)
|
||||
from vbv_lernwelt.learning_mentor.content.self_evaluation_feedback import (
|
||||
get_self_feedback_evaluation,
|
||||
)
|
||||
from vbv_lernwelt.learning_mentor.models import LearningMentor, MentorInvitation
|
||||
from vbv_lernwelt.learning_mentor.serializers import (
|
||||
InvitationSerializer,
|
||||
MentorAssignmentStatusSerializer,
|
||||
MentorSerializer,
|
||||
PraxisAssignmentStatusSerializer,
|
||||
)
|
||||
from vbv_lernwelt.learnpath.models import Circle
|
||||
from vbv_lernwelt.notify.email.email_services import EmailTemplate, send_email
|
||||
|
|
@ -37,24 +40,40 @@ def mentor_summary(request, course_session_id: int):
|
|||
assignments = []
|
||||
circle_ids = set()
|
||||
|
||||
praxis_assignments, _circle_ids = get_praxis_assignments(
|
||||
course_session=course_session, participants=users, evaluation_user=request.user
|
||||
praxis_assignments, praxis_assignments_circle_ids = get_praxis_assignments(
|
||||
course_session=course_session,
|
||||
participants=users,
|
||||
evaluation_user=request.user, # noqa
|
||||
)
|
||||
|
||||
(
|
||||
self_evaluation_feedbacks,
|
||||
self_evaluation_feedback_circle_ids,
|
||||
) = get_self_feedback_evaluation(
|
||||
participants=users,
|
||||
evaluation_user=request.user, # noqa
|
||||
course=course_session.course,
|
||||
)
|
||||
|
||||
circle_ids.update(praxis_assignments_circle_ids)
|
||||
circle_ids.update(self_evaluation_feedback_circle_ids)
|
||||
|
||||
assignments.extend(
|
||||
MentorAssignmentStatusSerializer(praxis_assignments, many=True).data
|
||||
)
|
||||
assignments.extend(
|
||||
PraxisAssignmentStatusSerializer(praxis_assignments, many=True).data
|
||||
MentorAssignmentStatusSerializer(self_evaluation_feedbacks, many=True).data
|
||||
)
|
||||
circle_ids.update(_circle_ids)
|
||||
|
||||
circles = Circle.objects.filter(id__in=circle_ids).values("id", "title")
|
||||
|
||||
assignments.sort(
|
||||
key=lambda x: (-x.get("pending_evaluations", 0), x.get("title", "").lower())
|
||||
)
|
||||
|
||||
return Response(
|
||||
{
|
||||
"participants": [UserSerializer(user).data for user in users],
|
||||
"circles": list(circles),
|
||||
"circles": list(
|
||||
Circle.objects.filter(id__in=circle_ids).values("id", "title")
|
||||
),
|
||||
"assignments": assignments,
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -5,11 +5,16 @@ from wagtail.rich_text import RichText
|
|||
from wagtail_localize.models import LocaleSynchronization
|
||||
|
||||
from vbv_lernwelt.assignment.models import Assignment
|
||||
from vbv_lernwelt.competence.factories import PerformanceCriteriaFactory
|
||||
from vbv_lernwelt.competence.factories import (
|
||||
ActionCompetenceFactory,
|
||||
ActionCompetenceListPageFactory,
|
||||
PerformanceCriteriaFactory,
|
||||
)
|
||||
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.models import CourseCategory, CoursePage
|
||||
from vbv_lernwelt.learnpath.models import LearningUnitPerformanceFeedbackType
|
||||
from vbv_lernwelt.learnpath.tests.learning_path_factories import (
|
||||
CircleFactory,
|
||||
LearningContentAssignmentFactory,
|
||||
|
|
@ -43,7 +48,7 @@ def create_vv_new_learning_path(
|
|||
)
|
||||
|
||||
TopicFactory(title="Basis", is_visible=False, parent=lp)
|
||||
create_circle_basis(lp)
|
||||
create_circle_basis(lp, course_page=course_page)
|
||||
|
||||
TopicFactory(title="Gewinnen von Kunden", parent=lp)
|
||||
create_circle_gewinnen(lp)
|
||||
|
|
@ -103,7 +108,7 @@ def create_vv_pruefung_learning_path(
|
|||
Page.objects.update(owner=user)
|
||||
|
||||
|
||||
def create_circle_basis(lp, title="Basis"):
|
||||
def create_circle_basis(lp, title="Basis", course_page=None):
|
||||
circle = CircleFactory(
|
||||
title=title,
|
||||
parent=lp,
|
||||
|
|
@ -145,10 +150,42 @@ def create_circle_basis(lp, title="Basis"):
|
|||
)
|
||||
|
||||
LearningSequenceFactory(title="Arbeitsalltag", parent=circle)
|
||||
LearningUnitFactory(
|
||||
lu = LearningUnitFactory(
|
||||
title="Mein neuer Job, Arbeitstechnik, Soziale Medien, Datenschutz und Beratungspflichten",
|
||||
feedback_user=LearningUnitPerformanceFeedbackType.MENTOR_FEEDBACK.name,
|
||||
parent=circle,
|
||||
)
|
||||
|
||||
competence_profile_page = ActionCompetenceListPageFactory(
|
||||
title="KompetenzNavi",
|
||||
parent=course_page,
|
||||
)
|
||||
|
||||
ace = ActionCompetenceFactory(
|
||||
parent=competence_profile_page,
|
||||
)
|
||||
|
||||
PerformanceCriteriaFactory(
|
||||
parent=ace,
|
||||
competence_id="VV-Arbeitsalltag-A",
|
||||
title="Ich kenne die wichtigsten Aspekte des Arbeitsalltags als Versicherungsvermittler/-in.",
|
||||
learning_unit=lu,
|
||||
)
|
||||
|
||||
PerformanceCriteriaFactory(
|
||||
parent=ace,
|
||||
competence_id="VV-Arbeitsalltag-B",
|
||||
title="Ich identifiziere und analysiere neue Markttrends im Versicherungssektor.",
|
||||
learning_unit=lu,
|
||||
)
|
||||
|
||||
PerformanceCriteriaFactory(
|
||||
parent=ace,
|
||||
competence_id="VV-Arbeitsalltag-C",
|
||||
title="Ich nutze digitale Tools zur Optimierung der Kundenbetreuung und -beratung im Versicherungswesen.",
|
||||
learning_unit=lu,
|
||||
)
|
||||
|
||||
LearningContentPlaceholderFactory(
|
||||
title="Mediathek",
|
||||
parent=circle,
|
||||
|
|
|
|||
|
|
@ -234,7 +234,7 @@ class LearningUnitObjectType(DjangoObjectType):
|
|||
class Meta:
|
||||
model = LearningUnit
|
||||
interfaces = (CoursePageInterface,)
|
||||
fields = ["evaluate_url", "title_hidden"]
|
||||
fields = ["evaluate_url", "title_hidden", "feedback_user"]
|
||||
|
||||
def resolve_evaluate_url(self: LearningUnit, info, **kwargs):
|
||||
return self.get_evaluate_url()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
# Generated by Django 3.2.20 on 2024-01-17 13:00
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("learnpath", "0012_auto_20231129_0827"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="learningunit",
|
||||
name="feedback_user",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("NO_FEEDBACK", "NO_FEEDBACK"),
|
||||
("MENTOR_FEEDBACK", "MENTOR_FEEDBACK"),
|
||||
],
|
||||
default="NO_FEEDBACK",
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="learningcontentassignment",
|
||||
name="assignment_type",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("PRAXIS_ASSIGNMENT", "PRAXIS_ASSIGNMENT"),
|
||||
("CASEWORK", "CASEWORK"),
|
||||
("PREP_ASSIGNMENT", "PREP_ASSIGNMENT"),
|
||||
("REFLECTION", "REFLECTION"),
|
||||
("CONDITION_ACCEPTANCE", "CONDITION_ACCEPTANCE"),
|
||||
("EDONIQ_TEST", "EDONIQ_TEST"),
|
||||
],
|
||||
default="CASEWORK",
|
||||
max_length=50,
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
# Generated by Django 3.2.20 on 2024-01-17 14:50
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("learnpath", "0013_auto_20240117_1400"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="learningunit",
|
||||
name="feedback_user",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("NO_FEEDBACK", "NO_FEEDBACK"),
|
||||
("MENTOR_FEEDBACK", "MENTOR_FEEDBACK"),
|
||||
],
|
||||
default="NO_FEEDBACK",
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
# Generated by Django 3.2.20 on 2024-02-12 15:19
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
VV_COURSE_IDS_WITH_MENTOR_FEEDBACK = [
|
||||
-4, # vv-de
|
||||
-10, # vv-fr
|
||||
-11, # vv-it
|
||||
]
|
||||
|
||||
|
||||
def is_learning_unit_in_vv_course(learning_unit):
|
||||
return learning_unit.course_category.course_id in VV_COURSE_IDS_WITH_MENTOR_FEEDBACK
|
||||
|
||||
|
||||
def mutate_data(apps, schema_editor):
|
||||
"""
|
||||
Enable feedback for learning units in VV courses, this means that on the self-assessment page
|
||||
of the learning unit, the user can request feedback from the mentor.
|
||||
"""
|
||||
|
||||
LearningUnit = apps.get_model("learnpath", "LearningUnit") # noqa
|
||||
for learning_unit in LearningUnit.objects.all():
|
||||
if is_learning_unit_in_vv_course(learning_unit):
|
||||
learning_unit.feedback_user = "MENTOR_FEEDBACK"
|
||||
learning_unit.save()
|
||||
|
||||
|
||||
def rollback_data(apps, schema_editor):
|
||||
"""
|
||||
Disable feedback for learning units in VV courses, this means that on the self-assessment page
|
||||
of the learning unit, the user can not request feedback from the mentor. -> Default behaviour.
|
||||
"""
|
||||
|
||||
LearningUnit = apps.get_model("learnpath", "LearningUnit") # noqa
|
||||
for learning_unit in LearningUnit.objects.all():
|
||||
if is_learning_unit_in_vv_course(learning_unit):
|
||||
learning_unit.feedback_user = "NO_FEEDBACK"
|
||||
learning_unit.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("learnpath", "0014_alter_learningunit_feedback_user"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(mutate_data, rollback_data),
|
||||
]
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
import re
|
||||
from enum import Enum
|
||||
from typing import Tuple
|
||||
|
||||
from django.db import models
|
||||
from django.utils.text import slugify
|
||||
from wagtail.admin.panels import FieldPanel, PageChooserPanel
|
||||
from wagtail.admin.panels import FieldPanel, HelpPanel, PageChooserPanel
|
||||
from wagtail.fields import RichTextField, StreamField
|
||||
from wagtail.models import Page
|
||||
|
||||
|
|
@ -117,6 +119,13 @@ class Circle(CourseBasePage):
|
|||
return f"{self.title}"
|
||||
|
||||
|
||||
class LearningUnitPerformanceFeedbackType(Enum):
|
||||
"""Defines how feedback on the performance criteria (n) of a learning unit are given."""
|
||||
|
||||
NO_FEEDBACK = "NO_FEEDBACK"
|
||||
MENTOR_FEEDBACK = "MENTOR_FEEDBACK"
|
||||
|
||||
|
||||
class LearningSequence(CourseBasePage):
|
||||
serialize_field_names = ["icon"]
|
||||
|
||||
|
|
@ -169,10 +178,21 @@ class LearningUnit(CourseBasePage):
|
|||
"course.CourseCategory", on_delete=models.SET_NULL, null=True, blank=True
|
||||
)
|
||||
title_hidden = models.BooleanField(default=False)
|
||||
feedback_user = models.CharField(
|
||||
max_length=255,
|
||||
choices=[(tag.name, tag.name) for tag in LearningUnitPerformanceFeedbackType],
|
||||
default=LearningUnitPerformanceFeedbackType.NO_FEEDBACK.name,
|
||||
)
|
||||
|
||||
content_panels = Page.content_panels + [
|
||||
FieldPanel("course_category"),
|
||||
FieldPanel("title_hidden"),
|
||||
FieldPanel("feedback_user"),
|
||||
HelpPanel(
|
||||
content="👆 Feedback zur Selbsteinschätzung: Normalerweise <code>NO_FEEDBACK</code>, "
|
||||
"ausser bei den Lerninhalten Selbsteinschätzungen, die eine Bewertung haben von einer "
|
||||
"Lernbegleitung haben sollen (z.B. VV)."
|
||||
),
|
||||
]
|
||||
|
||||
class Meta:
|
||||
|
|
@ -200,21 +220,29 @@ class LearningUnit(CourseBasePage):
|
|||
)
|
||||
super(LearningUnit, self).save(clean, user, log_action, **kwargs)
|
||||
|
||||
def get_frontend_url(self):
|
||||
def get_frontend_url_parts(self) -> Tuple[str, str, str]:
|
||||
"""
|
||||
Extracts the course, circle and learning unit part from the slug.
|
||||
:return: Tuple of course, circle and learning unit part
|
||||
"""
|
||||
|
||||
r = re.compile(
|
||||
r"^(?P<coursePart>.+?)-lp-circle-(?P<circlePart>.+?)-lu-(?P<luPart>.+?)$"
|
||||
)
|
||||
m = r.match(self.slug)
|
||||
|
||||
if m is None:
|
||||
return "ERROR: could not parse slug"
|
||||
return f"/course/{m.group('coursePart')}/learn/{m.group('circlePart')}#lu-{m.group('luPart')}"
|
||||
ValueError(f"Could not parse slug: {self.slug}")
|
||||
|
||||
return m.group("coursePart"), m.group("circlePart"), m.group("luPart")
|
||||
|
||||
def get_frontend_url(self):
|
||||
course, circle, learning_unit = self.get_frontend_url_parts()
|
||||
return f"/course/{course}/learn/{circle}#lu-{learning_unit}"
|
||||
|
||||
def get_evaluate_url(self):
|
||||
r = re.compile(
|
||||
r"^(?P<coursePart>.+?)-lp-circle-(?P<circlePart>.+?)-lu-(?P<luPart>.+?)$"
|
||||
)
|
||||
m = r.match(self.slug)
|
||||
return f"/course/{m.group('coursePart')}/learn/{m.group('circlePart')}/evaluate/{m.group('luPart')}"
|
||||
course, circle, learning_unit = self.get_frontend_url_parts()
|
||||
return f"/course/{course}/learn/{circle}/evaluate/{learning_unit}"
|
||||
|
||||
def get_admin_display_title(self):
|
||||
return f"LE: {self.draft_title}"
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ class LearningUnitSerializer(
|
|||
"course_category",
|
||||
"children",
|
||||
"title_hidden",
|
||||
"feedback_user",
|
||||
],
|
||||
)
|
||||
):
|
||||
|
|
|
|||
|
|
@ -2,6 +2,12 @@ from django.test import TestCase
|
|||
|
||||
from vbv_lernwelt.core.create_default_users import create_default_users
|
||||
from vbv_lernwelt.course.creators.test_course import create_test_course
|
||||
from vbv_lernwelt.course.creators.test_utils import (
|
||||
create_circle,
|
||||
create_course,
|
||||
create_course_session,
|
||||
create_learning_unit,
|
||||
)
|
||||
from vbv_lernwelt.learnpath.models import LearningContentPlaceholder
|
||||
|
||||
|
||||
|
|
@ -31,3 +37,22 @@ class SaveSlugTestCase(TestCase):
|
|||
self.assertEqual(
|
||||
lc_fachcheck.slug, "test-lehrgang-lp-circle-reisen-lc-fachcheck-foobar"
|
||||
)
|
||||
|
||||
def test_learning_unit_frontend_url_parts(self):
|
||||
# GIVEN
|
||||
course, course_page = create_course(course_page_title="What Ever Course Page")
|
||||
course_session = create_course_session(course=course, title=":)")
|
||||
circle, _ = create_circle(title="A-nice Circle", course_page=course_page)
|
||||
|
||||
# WHEN
|
||||
cut = create_learning_unit(
|
||||
course_category_title="course category title",
|
||||
circle=circle,
|
||||
course=course,
|
||||
)
|
||||
course_part, circle_part, learning_unit_part = cut.get_frontend_url_parts()
|
||||
|
||||
# THEN
|
||||
self.assertEqual(course_part, "what-ever-course-page")
|
||||
self.assertEqual(circle_part, "a-nice-circle")
|
||||
self.assertEqual(learning_unit_part, "course-category-title")
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@ def user_image(request, image_id):
|
|||
|
||||
rendition.file.open("rb")
|
||||
image_format = imghdr.what(rendition.file)
|
||||
|
||||
return StreamingHttpResponse(
|
||||
FileWrapper(rendition.file), content_type="image/" + image_format
|
||||
FileWrapper(rendition.file),
|
||||
content_type=f"image/{image_format}" if image_format else "binary/octet-stream",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -76,6 +76,20 @@ class EmailTemplate(Enum):
|
|||
"it": "d-30c6aa9accda4973a940dd25703cb4a9",
|
||||
}
|
||||
|
||||
# Fremdeinschätzung (Requester → Provider)
|
||||
SELF_EVALUATION_FEEDBACK_REQUESTED = {
|
||||
"de": "d-cf9c6681991d4293a7baccaa5b043c5c",
|
||||
"fr": "d-6b103876807a4a0db6a0c31651c1e8ba",
|
||||
"it": "d-403b2f9d09bb41dc9dc85eed6c35c942",
|
||||
}
|
||||
|
||||
# Fremdeinschätzung (Provider → Requester)
|
||||
SELF_EVALUATION_FEEDBACK_PROVIDED = {
|
||||
"de": "d-e547bff40252458fa802759f2c502e3a",
|
||||
"fr": "d-62aa7ce8639c49319f92edb858bbb1cd",
|
||||
"it": "d-de2b5dfaf5d2470dbeea5d3ea2a6c442",
|
||||
}
|
||||
|
||||
|
||||
def send_email(
|
||||
recipient_email: str,
|
||||
|
|
|
|||
|
|
@ -24,6 +24,13 @@ class NotificationTrigger(models.TextChoices):
|
|||
CASEWORK_EVALUATED = "CASEWORK_EVALUATED", _("Casework Evaluated")
|
||||
NEW_FEEDBACK = "NEW_FEEDBACK", _("New Feedback")
|
||||
|
||||
SELF_EVALUATION_FEEDBACK_REQUESTED = "SELF_EVALUATION_FEEDBACK_REQUESTED", _(
|
||||
"Self Evaluation Feedback Requested"
|
||||
)
|
||||
SELF_EVALUATION_FEEDBACK_PROVIDED = "SELF_EVALUATION_FEEDBACK_PROVIDED", _(
|
||||
"Self Evaluation Feedback Provided"
|
||||
)
|
||||
|
||||
|
||||
class Notification(AbstractNotification):
|
||||
# UUIDs are not supported by the notifications app...
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ from vbv_lernwelt.notify.models import (
|
|||
NotificationCategory,
|
||||
NotificationTrigger,
|
||||
)
|
||||
from vbv_lernwelt.self_evaluation_feedback.models import SelfEvaluationFeedback
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from vbv_lernwelt.assignment.models import AssignmentCompletion
|
||||
|
|
@ -73,6 +74,74 @@ class NotificationService:
|
|||
email_template=EmailTemplate.CASEWORK_SUBMITTED,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def send_self_evaluation_feedback_request_feedback_notification(
|
||||
cls,
|
||||
self_evaluation_feedback: SelfEvaluationFeedback,
|
||||
):
|
||||
"""Requester -> Provider"""
|
||||
requester_user = self_evaluation_feedback.feedback_requester_user
|
||||
provider_user = self_evaluation_feedback.feedback_provider_user
|
||||
|
||||
texts = {
|
||||
"de": "%(requester)s hat eine Selbsteinschätzung mit dir geteilt",
|
||||
"fr": "%(requester)s a partagé une auto-évaluation avec vous",
|
||||
"it": "%(requester)s ha condiviso una valutazione personale con te",
|
||||
}
|
||||
|
||||
verb = texts.get(provider_user.language, "de") % {
|
||||
"requester": requester_user.get_full_name(),
|
||||
}
|
||||
|
||||
return cls._send_notification(
|
||||
recipient=provider_user,
|
||||
verb=verb,
|
||||
notification_category=NotificationCategory.USER_INTERACTION,
|
||||
notification_trigger=NotificationTrigger.SELF_EVALUATION_FEEDBACK_REQUESTED,
|
||||
sender=requester_user,
|
||||
target_url=self_evaluation_feedback.feedback_provider_evaluation_url,
|
||||
action_object=self_evaluation_feedback,
|
||||
email_template=EmailTemplate.SELF_EVALUATION_FEEDBACK_REQUESTED,
|
||||
template_data={
|
||||
"mentee_name": requester_user.get_full_name(),
|
||||
"mentee_email": requester_user.email,
|
||||
},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def send_self_evaluation_feedback_received_notification(
|
||||
cls,
|
||||
self_evaluation_feedback: SelfEvaluationFeedback,
|
||||
):
|
||||
"""Provider -> Requester"""
|
||||
requester_user = self_evaluation_feedback.feedback_requester_user
|
||||
provider_user = self_evaluation_feedback.feedback_provider_user
|
||||
|
||||
texts = {
|
||||
"de": "%(provider)s hat dir eine Fremdeinschätzung gegeben",
|
||||
"fr": "%(provider)s vous a donné une évaluation externe",
|
||||
"it": "%(provider)s ti ha dato una valutazione esterna",
|
||||
}
|
||||
|
||||
verb = texts.get(requester_user.language, "de") % {
|
||||
"provider": provider_user.get_full_name(),
|
||||
}
|
||||
|
||||
return cls._send_notification(
|
||||
recipient=requester_user,
|
||||
verb=verb,
|
||||
notification_category=NotificationCategory.USER_INTERACTION,
|
||||
notification_trigger=NotificationTrigger.SELF_EVALUATION_FEEDBACK_PROVIDED,
|
||||
sender=provider_user,
|
||||
target_url=self_evaluation_feedback.feedback_requester_results_url,
|
||||
action_object=self_evaluation_feedback,
|
||||
email_template=EmailTemplate.SELF_EVALUATION_FEEDBACK_PROVIDED,
|
||||
template_data={
|
||||
"mentor_name": provider_user.get_full_name(),
|
||||
"mentor_email": provider_user.email,
|
||||
},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def send_assignment_evaluated_notification(
|
||||
cls,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
from django.contrib import admin
|
||||
|
||||
from vbv_lernwelt.self_evaluation_feedback.models import (
|
||||
CourseCompletionFeedback,
|
||||
SelfEvaluationFeedback,
|
||||
)
|
||||
|
||||
|
||||
@admin.register(SelfEvaluationFeedback)
|
||||
class CourseSessionAdmin(admin.ModelAdmin):
|
||||
list_display = (
|
||||
"id",
|
||||
"feedback_submitted",
|
||||
"feedback_requester_user",
|
||||
"feedback_provider_user",
|
||||
"learning_unit",
|
||||
)
|
||||
list_filter = (
|
||||
"feedback_submitted",
|
||||
"feedback_requester_user",
|
||||
"feedback_provider_user",
|
||||
"learning_unit",
|
||||
)
|
||||
search_fields = (
|
||||
"feedback_submitted",
|
||||
"feedback_requester_user",
|
||||
"feedback_provider_user",
|
||||
"learning_unit",
|
||||
)
|
||||
|
||||
|
||||
@admin.register(CourseCompletionFeedback)
|
||||
class CourseSessionAdmin(admin.ModelAdmin):
|
||||
list_display = (
|
||||
"id",
|
||||
"feedback",
|
||||
"course_completion",
|
||||
"feedback_assessment",
|
||||
)
|
||||
list_filter = (
|
||||
"feedback",
|
||||
"course_completion",
|
||||
"feedback_assessment",
|
||||
)
|
||||
search_fields = (
|
||||
"feedback",
|
||||
"course_completion",
|
||||
"feedback_assessment",
|
||||
)
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class SelfEvaluationFeedbackConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "vbv_lernwelt.self_evaluation_feedback"
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
# Generated by Django 3.2.20 on 2024-01-21 18:42
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
import vbv_lernwelt.course.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("course", "0006_auto_20231221_1411"),
|
||||
("learnpath", "0014_alter_learningunit_feedback_user"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="SelfEvaluationFeedback",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("feedback_submitted", models.BooleanField(default=False)),
|
||||
(
|
||||
"feedback_provider_user",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="feedback_provider_user",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
(
|
||||
"feedback_requester_user",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="feedback_requester_user",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
(
|
||||
"learning_unit",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="learnpath.learningunit",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="CourseCompletionFeedback",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"provider_evaluation_feedback",
|
||||
models.CharField(
|
||||
choices=[
|
||||
(
|
||||
vbv_lernwelt.course.models.CourseCompletionStatus[
|
||||
"SUCCESS"
|
||||
],
|
||||
"SUCCESS",
|
||||
),
|
||||
(
|
||||
vbv_lernwelt.course.models.CourseCompletionStatus[
|
||||
"FAIL"
|
||||
],
|
||||
"FAIL",
|
||||
),
|
||||
(
|
||||
vbv_lernwelt.course.models.CourseCompletionStatus[
|
||||
"UNKNOWN"
|
||||
],
|
||||
"UNKNOWN",
|
||||
),
|
||||
],
|
||||
default="UNKNOWN",
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
(
|
||||
"feedback",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="self_evaluation_feedback.selfevaluationfeedback",
|
||||
),
|
||||
),
|
||||
(
|
||||
"requester_evaluation",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="course.coursecompletion",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
# Generated by Django 3.2.20 on 2024-01-22 13:20
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("self_evaluation_feedback", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name="coursecompletionfeedback",
|
||||
old_name="requester_evaluation",
|
||||
new_name="course_completion",
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
# Generated by Django 3.2.20 on 2024-01-23 15:46
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
(
|
||||
"self_evaluation_feedback",
|
||||
"0002_rename_requester_evaluation_coursecompletionfeedback_course_completion",
|
||||
),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name="coursecompletionfeedback",
|
||||
old_name="provider_evaluation_feedback",
|
||||
new_name="feedback_assessment",
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
from django.db import models
|
||||
|
||||
from vbv_lernwelt.core.admin import User
|
||||
from vbv_lernwelt.course.models import CourseCompletion, CourseCompletionStatus
|
||||
|
||||
|
||||
class SelfEvaluationFeedback(models.Model):
|
||||
feedback_submitted = models.BooleanField(default=False)
|
||||
|
||||
feedback_requester_user = models.ForeignKey(
|
||||
User, on_delete=models.CASCADE, related_name="feedback_requester_user"
|
||||
)
|
||||
|
||||
feedback_provider_user = models.ForeignKey(
|
||||
User, on_delete=models.CASCADE, related_name="feedback_provider_user"
|
||||
)
|
||||
|
||||
learning_unit = models.ForeignKey(
|
||||
"learnpath.LearningUnit", on_delete=models.CASCADE
|
||||
)
|
||||
|
||||
@property
|
||||
def feedback_requester_results_url(self) -> str:
|
||||
url = self.learning_unit.get_evaluate_url()
|
||||
received_evaluation_step = len(self.learning_unit.performancecriteria_set.all())
|
||||
return f"{url}?step={received_evaluation_step}"
|
||||
|
||||
@property
|
||||
def feedback_provider_evaluation_url(self) -> str:
|
||||
course, _, __ = self.learning_unit.get_frontend_url_parts()
|
||||
return f"/course/{course}/cockpit/mentor/self-evaluation-feedback/{self.learning_unit.id}"
|
||||
|
||||
|
||||
class CourseCompletionFeedback(models.Model):
|
||||
feedback = models.ForeignKey(SelfEvaluationFeedback, on_delete=models.CASCADE)
|
||||
|
||||
# the course completion has to be evaluated by the feedback provider
|
||||
course_completion = models.ForeignKey(CourseCompletion, on_delete=models.CASCADE)
|
||||
|
||||
feedback_assessment = models.CharField(
|
||||
max_length=255,
|
||||
choices=[(status, status.value) for status in CourseCompletionStatus],
|
||||
default=CourseCompletionStatus.UNKNOWN.value,
|
||||
)
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
from typing import List
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from vbv_lernwelt.competence.models import PerformanceCriteria
|
||||
from vbv_lernwelt.core.serializers import UserSerializer
|
||||
from vbv_lernwelt.course.models import CourseCompletion, CourseCompletionStatus
|
||||
from vbv_lernwelt.self_evaluation_feedback.models import (
|
||||
CourseCompletionFeedback,
|
||||
SelfEvaluationFeedback,
|
||||
)
|
||||
|
||||
|
||||
class SelfEvaluationFeedbackSerializer(serializers.ModelSerializer):
|
||||
criteria = serializers.SerializerMethodField()
|
||||
feedback_requester_user = UserSerializer(read_only=True)
|
||||
feedback_provider_user = UserSerializer(read_only=True)
|
||||
learning_unit_id = serializers.PrimaryKeyRelatedField(
|
||||
read_only=True, source="learning_unit"
|
||||
)
|
||||
feedback_id = serializers.PrimaryKeyRelatedField(read_only=True, source="id")
|
||||
circle_name = serializers.SerializerMethodField()
|
||||
title = serializers.CharField(source="learning_unit.title")
|
||||
|
||||
class Meta:
|
||||
model = SelfEvaluationFeedback
|
||||
fields = [
|
||||
"feedback_id",
|
||||
"title",
|
||||
"circle_name",
|
||||
"learning_unit_id",
|
||||
"feedback_submitted",
|
||||
"feedback_requester_user",
|
||||
"feedback_provider_user",
|
||||
"criteria",
|
||||
]
|
||||
|
||||
def get_circle_name(self, obj):
|
||||
return obj.learning_unit.get_circle().title
|
||||
|
||||
def get_criteria(self, obj):
|
||||
performance_criteria: List[
|
||||
PerformanceCriteria
|
||||
] = obj.learning_unit.performancecriteria_set.all()
|
||||
|
||||
criteria = []
|
||||
|
||||
for pc in performance_criteria:
|
||||
# requester self assessment
|
||||
completion = CourseCompletion.objects.filter(
|
||||
page_id=pc.id,
|
||||
user=obj.feedback_requester_user,
|
||||
).first()
|
||||
|
||||
self_assessment = (
|
||||
completion.completion_status
|
||||
if completion
|
||||
else CourseCompletionStatus.UNKNOWN.value
|
||||
)
|
||||
|
||||
# provider feedback assessment
|
||||
feedback = CourseCompletionFeedback.objects.filter(
|
||||
course_completion=completion
|
||||
).first()
|
||||
|
||||
feedback_assessment = (
|
||||
feedback.feedback_assessment
|
||||
if feedback
|
||||
else CourseCompletionStatus.UNKNOWN.value
|
||||
)
|
||||
|
||||
criteria.append(
|
||||
{
|
||||
"course_completion_id": completion.id if completion else None,
|
||||
"title": pc.title,
|
||||
"self_assessment": self_assessment,
|
||||
"feedback_assessment": feedback_assessment,
|
||||
}
|
||||
)
|
||||
|
||||
return criteria
|
||||
|
|
@ -0,0 +1,463 @@
|
|||
from unittest.mock import patch
|
||||
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from vbv_lernwelt.course.creators.test_utils import (
|
||||
add_course_session_user,
|
||||
create_circle,
|
||||
create_course,
|
||||
create_course_session,
|
||||
create_learning_unit,
|
||||
create_performance_criteria_page,
|
||||
create_user,
|
||||
)
|
||||
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.self_evaluation_feedback.models import (
|
||||
CourseCompletionFeedback,
|
||||
SelfEvaluationFeedback,
|
||||
)
|
||||
|
||||
|
||||
def create_self_evaluation_feedback(
|
||||
learning_unit, feedback_requester_user, feedback_provider_user
|
||||
):
|
||||
return SelfEvaluationFeedback.objects.create(
|
||||
learning_unit=learning_unit,
|
||||
feedback_requester_user=feedback_requester_user,
|
||||
feedback_provider_user=feedback_provider_user,
|
||||
)
|
||||
|
||||
|
||||
class SelfEvaluationFeedbackAPI(APITestCase):
|
||||
def setUp(self) -> None:
|
||||
self.member = create_user("member")
|
||||
self.mentor = create_user("mentor")
|
||||
|
||||
self.course, self.course_page = create_course("Test Course")
|
||||
self.course_session = create_course_session(
|
||||
course=self.course, title="Test Bern 2022 a"
|
||||
)
|
||||
|
||||
member_csu = add_course_session_user(
|
||||
course_session=self.course_session,
|
||||
user=self.member,
|
||||
role=CourseSessionUser.Role.MEMBER,
|
||||
)
|
||||
|
||||
self.circle, _ = create_circle(
|
||||
title="Test Circle", course_page=self.course_page
|
||||
)
|
||||
|
||||
learning_mentor = LearningMentor.objects.create(
|
||||
mentor=self.mentor,
|
||||
course=self.course_session.course,
|
||||
)
|
||||
|
||||
learning_mentor.participants.add(member_csu)
|
||||
|
||||
@patch(
|
||||
"vbv_lernwelt.notify.services.NotificationService.send_self_evaluation_feedback_request_feedback_notification"
|
||||
)
|
||||
def test_start_self_evaluation_feedback(self, mock_notification_service_send):
|
||||
# GIVEN
|
||||
learning_unit = create_learning_unit(course=self.course, circle=self.circle)
|
||||
|
||||
pc = create_performance_criteria_page(
|
||||
course=self.course,
|
||||
course_page=self.course_page,
|
||||
circle=self.circle,
|
||||
learning_unit=learning_unit,
|
||||
)
|
||||
|
||||
mark_course_completion(
|
||||
page=pc,
|
||||
user=self.member,
|
||||
course_session=self.course_session,
|
||||
completion_status="SUCCESS",
|
||||
)
|
||||
|
||||
self.client.force_login(self.member)
|
||||
|
||||
# WHEN
|
||||
response = self.client.post(
|
||||
reverse(
|
||||
"start_self_evaluation_feedback",
|
||||
args=[learning_unit.id],
|
||||
),
|
||||
{
|
||||
"feedback_provider_user_id": self.mentor.id,
|
||||
},
|
||||
)
|
||||
|
||||
# make sure re-starting is a no-op
|
||||
self.client.post(
|
||||
reverse(
|
||||
"start_self_evaluation_feedback",
|
||||
args=[learning_unit.id],
|
||||
),
|
||||
{
|
||||
"feedback_provider_user_id": self.mentor.id,
|
||||
},
|
||||
)
|
||||
|
||||
# shall be idempotent
|
||||
self.client.post(
|
||||
reverse(
|
||||
"start_self_evaluation_feedback",
|
||||
args=[learning_unit.id],
|
||||
),
|
||||
{
|
||||
"feedback_provider_user_id": self.mentor.id,
|
||||
},
|
||||
)
|
||||
|
||||
# THEN
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data["success"], True)
|
||||
|
||||
self.assertEqual(
|
||||
SelfEvaluationFeedback.objects.count(),
|
||||
1,
|
||||
)
|
||||
|
||||
self_evaluation_feedback = SelfEvaluationFeedback.objects.first()
|
||||
self.assertEqual(self_evaluation_feedback.feedback_requester_user, self.member)
|
||||
self.assertEqual(self_evaluation_feedback.feedback_provider_user, self.mentor)
|
||||
self.assertEqual(self_evaluation_feedback.learning_unit, learning_unit)
|
||||
|
||||
mock_notification_service_send.assert_called_once_with(
|
||||
self_evaluation_feedback=self_evaluation_feedback
|
||||
)
|
||||
|
||||
def test_start_self_evaluation_feedback_not_allowed_user(self):
|
||||
# GIVEN
|
||||
learning_unit = create_learning_unit(course=self.course, circle=self.circle)
|
||||
not_a_mentor = create_user("not_a_mentor")
|
||||
|
||||
self.client.force_login(self.member)
|
||||
|
||||
# WHEN
|
||||
response = self.client.post(
|
||||
reverse("start_self_evaluation_feedback", args=[learning_unit.id]),
|
||||
{
|
||||
"feedback_provider_user_id": not_a_mentor.id,
|
||||
},
|
||||
)
|
||||
|
||||
# THEN
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_get_self_evaluation_feedback_as_requester(self):
|
||||
"""Tests endpoint of feedback REQUESTER"""
|
||||
|
||||
# GIVEN
|
||||
learning_unit = create_learning_unit(course=self.course, circle=self.circle)
|
||||
|
||||
performance_criteria_1 = create_performance_criteria_page(
|
||||
course=self.course,
|
||||
course_page=self.course_page,
|
||||
circle=self.circle,
|
||||
learning_unit=learning_unit,
|
||||
)
|
||||
|
||||
create_performance_criteria_page(
|
||||
course=self.course,
|
||||
course_page=self.course_page,
|
||||
circle=self.circle,
|
||||
learning_unit=learning_unit,
|
||||
)
|
||||
|
||||
completion = mark_course_completion(
|
||||
page=performance_criteria_1,
|
||||
user=self.member,
|
||||
course_session=self.course_session,
|
||||
completion_status=CourseCompletionStatus.SUCCESS.value,
|
||||
)
|
||||
|
||||
self_evaluation_feedback = create_self_evaluation_feedback(
|
||||
learning_unit=learning_unit,
|
||||
feedback_requester_user=self.member,
|
||||
feedback_provider_user=self.mentor,
|
||||
)
|
||||
|
||||
CourseCompletionFeedback.objects.create(
|
||||
feedback=self_evaluation_feedback,
|
||||
course_completion=completion,
|
||||
feedback_assessment=CourseCompletionStatus.FAIL.value,
|
||||
)
|
||||
|
||||
self.client.force_login(self.member)
|
||||
|
||||
# WHEN
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
"get_self_evaluation_feedback_as_requester",
|
||||
args=[learning_unit.id],
|
||||
)
|
||||
)
|
||||
|
||||
# THEN
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
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)
|
||||
|
||||
provider_user = feedback["feedback_provider_user"]
|
||||
self.assertEqual(provider_user["id"], str(self.mentor.id)) # noqa
|
||||
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)
|
||||
|
||||
requester_user = feedback["feedback_requester_user"]
|
||||
self.assertEqual(requester_user["id"], str(self.member.id)) # noqa
|
||||
self.assertEqual(requester_user["first_name"], self.member.first_name)
|
||||
self.assertEqual(requester_user["last_name"], self.member.last_name)
|
||||
self.assertEqual(requester_user["avatar_url"], self.member.avatar_url)
|
||||
|
||||
self.assertEqual(len(feedback["criteria"]), 2)
|
||||
|
||||
first_criteria = feedback["criteria"][0]
|
||||
self.assertEqual(first_criteria["course_completion_id"], completion.id)
|
||||
self.assertEqual(first_criteria["title"], performance_criteria_1.title)
|
||||
self.assertEqual(
|
||||
first_criteria["self_assessment"],
|
||||
CourseCompletionStatus.SUCCESS.value,
|
||||
)
|
||||
self.assertEqual(
|
||||
first_criteria["feedback_assessment"], CourseCompletionStatus.FAIL.value
|
||||
)
|
||||
|
||||
second_criteria = feedback["criteria"][1]
|
||||
self.assertEqual(second_criteria["course_completion_id"], None)
|
||||
self.assertEqual(second_criteria["title"], performance_criteria_1.title)
|
||||
self.assertEqual(
|
||||
second_criteria["self_assessment"], CourseCompletionStatus.UNKNOWN.value
|
||||
)
|
||||
self.assertEqual(
|
||||
second_criteria["feedback_assessment"],
|
||||
CourseCompletionStatus.UNKNOWN.value,
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
performance_criteria_1 = create_performance_criteria_page(
|
||||
course=self.course,
|
||||
course_page=self.course_page,
|
||||
circle=self.circle,
|
||||
learning_unit=learning_unit,
|
||||
)
|
||||
|
||||
create_performance_criteria_page(
|
||||
course=self.course,
|
||||
course_page=self.course_page,
|
||||
circle=self.circle,
|
||||
learning_unit=learning_unit,
|
||||
)
|
||||
|
||||
completion = mark_course_completion(
|
||||
page=performance_criteria_1,
|
||||
user=self.member,
|
||||
course_session=self.course_session,
|
||||
completion_status=CourseCompletionStatus.SUCCESS.value,
|
||||
)
|
||||
|
||||
self_evaluation_feedback = create_self_evaluation_feedback(
|
||||
learning_unit=learning_unit,
|
||||
feedback_requester_user=self.member,
|
||||
feedback_provider_user=self.mentor,
|
||||
)
|
||||
|
||||
CourseCompletionFeedback.objects.create(
|
||||
feedback=self_evaluation_feedback,
|
||||
course_completion=completion,
|
||||
feedback_assessment=CourseCompletionStatus.FAIL.value,
|
||||
)
|
||||
|
||||
self.client.force_login(self.mentor)
|
||||
|
||||
# WHEN
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
"get_self_evaluation_feedback_as_provider",
|
||||
args=[self_evaluation_feedback.learning_unit.id],
|
||||
)
|
||||
)
|
||||
|
||||
# THEN
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
feedback = response.data
|
||||
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)
|
||||
|
||||
provider_user = feedback["feedback_provider_user"]
|
||||
self.assertEqual(provider_user["id"], str(self.mentor.id)) # noqa
|
||||
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)
|
||||
|
||||
requester_user = feedback["feedback_requester_user"]
|
||||
self.assertEqual(requester_user["id"], str(self.member.id)) # noqa
|
||||
self.assertEqual(requester_user["first_name"], self.member.first_name)
|
||||
self.assertEqual(requester_user["last_name"], self.member.last_name)
|
||||
self.assertEqual(requester_user["avatar_url"], self.member.avatar_url)
|
||||
|
||||
self.assertEqual(len(feedback["criteria"]), 2)
|
||||
|
||||
first_criteria = feedback["criteria"][0]
|
||||
self.assertEqual(first_criteria["course_completion_id"], completion.id)
|
||||
self.assertEqual(first_criteria["title"], performance_criteria_1.title)
|
||||
self.assertEqual(
|
||||
first_criteria["self_assessment"],
|
||||
CourseCompletionStatus.SUCCESS.value,
|
||||
)
|
||||
self.assertEqual(
|
||||
first_criteria["feedback_assessment"], CourseCompletionStatus.FAIL.value
|
||||
)
|
||||
|
||||
second_criteria = feedback["criteria"][1]
|
||||
self.assertEqual(second_criteria["course_completion_id"], None)
|
||||
self.assertEqual(second_criteria["title"], performance_criteria_1.title)
|
||||
self.assertEqual(
|
||||
second_criteria["self_assessment"], CourseCompletionStatus.UNKNOWN.value
|
||||
)
|
||||
self.assertEqual(
|
||||
second_criteria["feedback_assessment"],
|
||||
CourseCompletionStatus.UNKNOWN.value,
|
||||
)
|
||||
|
||||
def test_self_evaluation_feedback_assessment(self):
|
||||
# GIVEN
|
||||
learning_unit = create_learning_unit(course=self.course, circle=self.circle)
|
||||
|
||||
performance_criteria_1 = create_performance_criteria_page(
|
||||
course=self.course,
|
||||
course_page=self.course_page,
|
||||
circle=self.circle,
|
||||
learning_unit=learning_unit,
|
||||
)
|
||||
|
||||
completion = mark_course_completion(
|
||||
page=performance_criteria_1,
|
||||
user=self.member,
|
||||
course_session=self.course_session,
|
||||
completion_status=CourseCompletionStatus.SUCCESS.value,
|
||||
)
|
||||
|
||||
self_evaluation_feedback = create_self_evaluation_feedback(
|
||||
learning_unit=learning_unit,
|
||||
feedback_requester_user=self.member,
|
||||
feedback_provider_user=self.mentor,
|
||||
)
|
||||
|
||||
self.client.force_login(self.mentor)
|
||||
|
||||
# WHEN
|
||||
response = self.client.put(
|
||||
reverse(
|
||||
"add_self_evaluation_feedback_assessment",
|
||||
args=[self_evaluation_feedback.id],
|
||||
),
|
||||
{
|
||||
"course_completion_id": completion.id,
|
||||
"feedback_assessment": CourseCompletionStatus.FAIL.value,
|
||||
},
|
||||
)
|
||||
|
||||
# THEN
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data["success"], True)
|
||||
|
||||
feedback = CourseCompletionFeedback.objects.get(
|
||||
feedback=self_evaluation_feedback,
|
||||
course_completion=completion,
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
feedback.feedback_assessment, CourseCompletionStatus.FAIL.value
|
||||
)
|
||||
|
||||
@patch(
|
||||
"vbv_lernwelt.notify.services.NotificationService.send_self_evaluation_feedback_received_notification"
|
||||
)
|
||||
def test_release_self_evaluation_feedback(self, mock_notification_service_send):
|
||||
# GIVEN
|
||||
learning_unit = create_learning_unit(course=self.course, circle=self.circle)
|
||||
self_evaluation_feedback = create_self_evaluation_feedback(
|
||||
learning_unit=learning_unit,
|
||||
feedback_requester_user=self.member,
|
||||
feedback_provider_user=self.mentor,
|
||||
)
|
||||
|
||||
self.assertEqual(self_evaluation_feedback.feedback_submitted, False)
|
||||
self.client.force_login(self.mentor)
|
||||
|
||||
# WHEN
|
||||
response = self.client.put(
|
||||
reverse(
|
||||
"release_self_evaluation_feedback", args=[self_evaluation_feedback.id]
|
||||
),
|
||||
)
|
||||
|
||||
# shall be idempotent
|
||||
response = self.client.put(
|
||||
reverse(
|
||||
"release_self_evaluation_feedback", args=[self_evaluation_feedback.id]
|
||||
),
|
||||
)
|
||||
|
||||
# THEN
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data["success"], True)
|
||||
self.assertEqual(
|
||||
SelfEvaluationFeedback.objects.get(
|
||||
id=self_evaluation_feedback.id
|
||||
).feedback_submitted,
|
||||
True,
|
||||
)
|
||||
|
||||
mock_notification_service_send.assert_called_once_with(
|
||||
self_evaluation_feedback=self_evaluation_feedback
|
||||
)
|
||||
|
||||
def test_get_self_evaluation_feedback_frontend_urls(self):
|
||||
"""Makes sure that the frontend urls are correct (used in notifications)"""
|
||||
# GIVEN
|
||||
learning_unit = create_learning_unit(course=self.course, circle=self.circle)
|
||||
|
||||
cut = create_self_evaluation_feedback(
|
||||
learning_unit=learning_unit,
|
||||
feedback_requester_user=self.member,
|
||||
feedback_provider_user=self.mentor,
|
||||
)
|
||||
|
||||
# WHEN
|
||||
requester_url = cut.feedback_requester_results_url
|
||||
provider_url = cut.feedback_provider_evaluation_url
|
||||
|
||||
# THEN
|
||||
_course, _circle, _learning_unit = learning_unit.get_frontend_url_parts()
|
||||
|
||||
# 0 -> no completions so step=0 is correct
|
||||
_step = len(learning_unit.performancecriteria_set.all())
|
||||
|
||||
self.assertEqual(
|
||||
requester_url,
|
||||
f"/course/{_course}/learn/{_circle}/evaluate/{_learning_unit}?step={_step}",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
provider_url,
|
||||
f"/course/{_course}/cockpit/mentor/self-evaluation-feedback/{learning_unit.id}",
|
||||
)
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
from django.urls import path
|
||||
|
||||
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,
|
||||
release_provider_self_evaluation_feedback,
|
||||
start_self_evaluation_feedback,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"requester/<int:learning_unit_id>/feedback/start",
|
||||
start_self_evaluation_feedback,
|
||||
name="start_self_evaluation_feedback",
|
||||
),
|
||||
path(
|
||||
"requester/<int:learning_unit_id>/feedback",
|
||||
get_self_evaluation_feedback_as_requester,
|
||||
name="get_self_evaluation_feedback_as_requester",
|
||||
),
|
||||
path(
|
||||
"provider/<int:learning_unit_id>/feedback",
|
||||
get_self_evaluation_feedback_as_provider,
|
||||
name="get_self_evaluation_feedback_as_provider",
|
||||
),
|
||||
path(
|
||||
"provider/feedback/<int:feedback_id>/release",
|
||||
release_provider_self_evaluation_feedback,
|
||||
name="release_self_evaluation_feedback",
|
||||
),
|
||||
path(
|
||||
"provider/feedback/<int:feedback_id>/add-assessment",
|
||||
add_provider_self_evaluation_feedback,
|
||||
name="add_self_evaluation_feedback_assessment",
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework.decorators import api_view, permission_classes
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
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.learning_mentor.models import LearningMentor
|
||||
from vbv_lernwelt.learnpath.models import LearningUnit
|
||||
from vbv_lernwelt.notify.services import NotificationService
|
||||
from vbv_lernwelt.self_evaluation_feedback.models import (
|
||||
CourseCompletionFeedback,
|
||||
SelfEvaluationFeedback,
|
||||
)
|
||||
from vbv_lernwelt.self_evaluation_feedback.serializers import (
|
||||
SelfEvaluationFeedbackSerializer,
|
||||
)
|
||||
|
||||
|
||||
@api_view(["POST"])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def start_self_evaluation_feedback(request, learning_unit_id):
|
||||
feedback_provider_user_id = request.data.get("feedback_provider_user_id")
|
||||
|
||||
learning_unit = get_object_or_404(LearningUnit, id=learning_unit_id)
|
||||
feedback_provider_user = get_object_or_404(User, id=feedback_provider_user_id)
|
||||
|
||||
if not LearningMentor.objects.filter(
|
||||
course=learning_unit.get_course(),
|
||||
mentor=feedback_provider_user,
|
||||
participants__user=request.user,
|
||||
).exists():
|
||||
raise PermissionDenied()
|
||||
|
||||
# calling start multiple times shall be a no-op
|
||||
feedback, created = SelfEvaluationFeedback.objects.get_or_create(
|
||||
feedback_requester_user=request.user,
|
||||
feedback_provider_user=feedback_provider_user,
|
||||
learning_unit=learning_unit,
|
||||
)
|
||||
|
||||
if created:
|
||||
NotificationService.send_self_evaluation_feedback_request_feedback_notification(
|
||||
self_evaluation_feedback=feedback
|
||||
)
|
||||
|
||||
return Response({"success": True})
|
||||
|
||||
|
||||
@api_view(["PUT"])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def release_provider_self_evaluation_feedback(request, feedback_id):
|
||||
feedback = get_object_or_404(
|
||||
SelfEvaluationFeedback, id=feedback_id, feedback_provider_user=request.user
|
||||
)
|
||||
|
||||
if feedback.feedback_submitted:
|
||||
return Response({"success": True})
|
||||
|
||||
feedback.feedback_submitted = True
|
||||
feedback.save()
|
||||
|
||||
NotificationService.send_self_evaluation_feedback_received_notification(
|
||||
self_evaluation_feedback=feedback
|
||||
)
|
||||
|
||||
return Response({"success": True})
|
||||
|
||||
|
||||
@api_view(["GET"])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def get_self_evaluation_feedback_as_provider(request, learning_unit_id):
|
||||
feedback = get_object_or_404(
|
||||
SelfEvaluationFeedback,
|
||||
learning_unit_id=learning_unit_id,
|
||||
feedback_provider_user=request.user,
|
||||
)
|
||||
|
||||
return Response(SelfEvaluationFeedbackSerializer(feedback).data)
|
||||
|
||||
|
||||
@api_view(["GET"])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def get_self_evaluation_feedback_as_requester(request, learning_unit_id):
|
||||
learning_unit = get_object_or_404(LearningUnit, id=learning_unit_id)
|
||||
|
||||
feedback = get_object_or_404(
|
||||
SelfEvaluationFeedback,
|
||||
learning_unit=learning_unit,
|
||||
feedback_requester_user=request.user,
|
||||
)
|
||||
|
||||
return Response(SelfEvaluationFeedbackSerializer(feedback).data)
|
||||
|
||||
|
||||
@api_view(["PUT"])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def add_provider_self_evaluation_feedback(request, feedback_id):
|
||||
feedback_assessment = request.data.get("feedback_assessment")
|
||||
|
||||
feedback = get_object_or_404(
|
||||
SelfEvaluationFeedback, id=feedback_id, feedback_provider_user=request.user
|
||||
)
|
||||
|
||||
course_completion = get_object_or_404(
|
||||
CourseCompletion,
|
||||
id=request.data.get("course_completion_id"),
|
||||
user=feedback.feedback_requester_user,
|
||||
)
|
||||
|
||||
(
|
||||
course_completion_feedback,
|
||||
created,
|
||||
) = CourseCompletionFeedback.objects.get_or_create(
|
||||
feedback=feedback,
|
||||
course_completion=course_completion,
|
||||
defaults={"feedback_assessment": feedback_assessment},
|
||||
)
|
||||
|
||||
if not created:
|
||||
course_completion_feedback.feedback_assessment = feedback_assessment
|
||||
course_completion_feedback.save()
|
||||
|
||||
return Response({"success": True})
|
||||
|
|
@ -47,6 +47,12 @@
|
|||
|
||||
<form action="/api/core/cypressreset/" method="post">
|
||||
{% csrf_token %}
|
||||
<label>
|
||||
<input type="checkbox" name="create_learning_mentor" value="true">
|
||||
create_learning_mentor
|
||||
</label>
|
||||
<div style="margin-bottom: 8px; padding: 4px; border-bottom: 1px lightblue solid"></div>
|
||||
|
||||
<label>
|
||||
<input type="checkbox" name="create_assignment_completion" value="true">
|
||||
create_assignment_completion
|
||||
|
|
|
|||
Loading…
Reference in New Issue