Merged develop into feature/tracking-code
This commit is contained in:
commit
e659b4b410
|
|
@ -97,12 +97,12 @@ js-linting: &js-linting
|
||||||
|
|
||||||
default-steps: &default-steps
|
default-steps: &default-steps
|
||||||
- parallel:
|
- parallel:
|
||||||
- step: *e2e
|
- step: *e2e
|
||||||
- step: *e2e
|
- step: *e2e
|
||||||
- step: *python-tests
|
- step: *python-tests
|
||||||
- step: *python-linting
|
- step: *python-linting
|
||||||
- step: *js-tests
|
- step: *js-tests
|
||||||
- step: *js-linting
|
- step: *js-linting
|
||||||
|
|
||||||
# main pipelines definitions
|
# main pipelines definitions
|
||||||
pipelines:
|
pipelines:
|
||||||
|
|
@ -132,16 +132,16 @@ pipelines:
|
||||||
script:
|
script:
|
||||||
- echo "Release ready!"
|
- echo "Release ready!"
|
||||||
- parallel:
|
- parallel:
|
||||||
- step:
|
- step:
|
||||||
<<: *deploy
|
<<: *deploy
|
||||||
name: deploy prod
|
name: deploy prod
|
||||||
deployment: prod
|
deployment: prod
|
||||||
trigger: manual
|
trigger: manual
|
||||||
- step:
|
- step:
|
||||||
<<: *deploy
|
<<: *deploy
|
||||||
name: deploy prod-azure
|
name: deploy prod-azure
|
||||||
deployment: prod-azure
|
deployment: prod-azure
|
||||||
trigger: manual
|
trigger: manual
|
||||||
custom:
|
custom:
|
||||||
deploy-feature-branch:
|
deploy-feature-branch:
|
||||||
- step:
|
- step:
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
2
|
2
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import AssignmentSubmissionProgress from "@/pages/cockpit/cockpitPage/AssignmentSubmissionProgress.vue";
|
import AssignmentSubmissionProgress from "@/components/cockpit/AssignmentSubmissionProgress.vue";
|
||||||
import type {
|
import type {
|
||||||
CourseSession,
|
CourseSession,
|
||||||
LearningContent,
|
LearningContent,
|
||||||
|
|
@ -10,7 +10,7 @@ import type {
|
||||||
import log from "loglevel";
|
import log from "loglevel";
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
import { useTranslation } from "i18next-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 { learningContentTypeData } from "@/utils/typeMaps";
|
||||||
import {
|
import {
|
||||||
useCourseDataWithCompletion,
|
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 type { CircleType, LearningUnit } from "@/types";
|
||||||
import * as log from "loglevel";
|
import * as log from "loglevel";
|
||||||
|
|
||||||
import { useCurrentCourseSession, useCourseDataWithCompletion } from "@/composables";
|
import { useCourseDataWithCompletion, useCurrentCourseSession } from "@/composables";
|
||||||
import LearningContentContainer from "@/pages/learningPath/learningContentPage/LearningContentContainer.vue";
|
import LearningContentContainer from "@/pages/learningPath/learningContentPage/LearningContentContainer.vue";
|
||||||
import LearningContentMultiLayout from "@/pages/learningPath/learningContentPage/layouts/LearningContentMultiLayout.vue";
|
import LearningContentMultiLayout from "@/pages/learningPath/learningContentPage/layouts/LearningContentMultiLayout.vue";
|
||||||
import eventBus from "@/utils/eventBus";
|
import eventBus from "@/utils/eventBus";
|
||||||
|
|
@ -11,35 +11,56 @@ import { useRouteQuery } from "@vueuse/router";
|
||||||
import { computed, onUnmounted } from "vue";
|
import { computed, onUnmounted } from "vue";
|
||||||
import { getPreviousRoute } from "@/router/history";
|
import { getPreviousRoute } from "@/router/history";
|
||||||
import { getCompetenceNaviUrl } from "@/utils/utils";
|
import { getCompetenceNaviUrl } from "@/utils/utils";
|
||||||
|
import SelfEvaluationRequestFeedbackPage from "@/pages/learningPath/selfEvaluationPage/SelfEvaluationRequestFeedbackPage.vue";
|
||||||
|
|
||||||
log.debug("LearningContent.vue setup");
|
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<{
|
const props = defineProps<{
|
||||||
learningUnit: LearningUnit;
|
learningUnit: LearningUnit;
|
||||||
circle: CircleType;
|
circle: CircleType;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const circleStore = useCircleStore();
|
||||||
|
const courseSession = useCurrentCourseSession();
|
||||||
|
const courseCompletionData = useCourseDataWithCompletion();
|
||||||
|
|
||||||
const questions = computed(() => props.learningUnit?.performance_criteria ?? []);
|
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 currentQuestion = computed(() => questions.value[questionIndex.value]);
|
||||||
const showPreviousButton = computed(() => questionIndex.value != 0);
|
const showPreviousButton = computed(() => questionIndex.value != 0);
|
||||||
|
|
||||||
const showNextButton = computed(
|
const showNextButton = computed(
|
||||||
() => questionIndex.value + 1 < questions.value?.length && questions.value?.length > 1
|
() => questionIndex.value + 1 < numPages.value && numPages.value > 1
|
||||||
);
|
);
|
||||||
const showExitButton = computed(
|
|
||||||
() =>
|
const isLastStep = computed(
|
||||||
questions.value?.length === 1 || questions.value?.length === questionIndex.value + 1
|
() => questions.value?.length === 1 || numPages.value == questionIndex.value + 1
|
||||||
);
|
);
|
||||||
|
|
||||||
function handleContinue() {
|
function handleContinue() {
|
||||||
log.debug("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);
|
log.debug("increment questionIndex", questionIndex.value);
|
||||||
questionIndex.value += 1;
|
questionIndex.value += 1;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -50,7 +71,7 @@ function handleContinue() {
|
||||||
|
|
||||||
function handleBack() {
|
function handleBack() {
|
||||||
log.debug("handleBack");
|
log.debug("handleBack");
|
||||||
if (questionIndex.value > 0 && questionIndex.value < questions.value.length) {
|
if (questionIndex.value > 0 && questionIndex.value < numPages.value) {
|
||||||
questionIndex.value -= 1;
|
questionIndex.value -= 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -78,16 +99,20 @@ onUnmounted(() => {
|
||||||
:sub-title="$t('a.Selbsteinschätzung')"
|
:sub-title="$t('a.Selbsteinschätzung')"
|
||||||
:title="`${learningUnit.title}`"
|
:title="`${learningUnit.title}`"
|
||||||
icon="it-icon-lc-learning-module"
|
icon="it-icon-lc-learning-module"
|
||||||
:steps-count="questions.length"
|
:steps-count="numPages"
|
||||||
:show-next-button="showNextButton"
|
:show-next-button="showNextButton"
|
||||||
:show-exit-button="showExitButton"
|
:show-exit-button="isLastStep"
|
||||||
:show-start-button="false"
|
:show-start-button="false"
|
||||||
:show-previous-button="showPreviousButton"
|
:show-previous-button="showPreviousButton"
|
||||||
:base-url="props.learningUnit.evaluate_url"
|
:base-url="props.learningUnit.evaluate_url"
|
||||||
|
:close-button-variant="learningUnitHasFeedbackPage ? 'close' : 'mark_as_done'"
|
||||||
|
:end-badge-text="
|
||||||
|
learningUnitHasFeedbackPage ? $t('general.submission') : undefined
|
||||||
|
"
|
||||||
@previous="handleBack()"
|
@previous="handleBack()"
|
||||||
@next="handleContinue()"
|
@next="handleContinue()"
|
||||||
>
|
>
|
||||||
<div class="h-full">
|
<div v-if="currentQuestion" class="h-full">
|
||||||
<div class="mt-8">
|
<div class="mt-8">
|
||||||
<h3 class="heading-3">
|
<h3 class="heading-3">
|
||||||
{{ currentQuestion.title }}
|
{{ currentQuestion.title }}
|
||||||
|
|
@ -137,6 +162,11 @@ onUnmounted(() => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<SelfEvaluationRequestFeedbackPage
|
||||||
|
v-else-if="isLastStep && learningUnit.feedback_user == 'MENTOR_FEEDBACK'"
|
||||||
|
:learning-unit="props.learningUnit"
|
||||||
|
:criteria="questions"
|
||||||
|
/>
|
||||||
</LearningContentMultiLayout>
|
</LearningContentMultiLayout>
|
||||||
</LearningContentContainer>
|
</LearningContentContainer>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
import ItButton from "@/components/ui/ItButton.vue";
|
import ItButton from "@/components/ui/ItButton.vue";
|
||||||
import ItCheckbox from "@/components/ui/ItCheckbox.vue";
|
import ItCheckbox from "@/components/ui/ItCheckbox.vue";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { bustItGetCache, useCSRFFetch } from "@/fetchHelpers";
|
import { bustItGetCache } from "@/fetchHelpers";
|
||||||
import { useUserStore } from "@/stores/user";
|
import { useUserStore } from "@/stores/user";
|
||||||
import eventBus from "@/utils/eventBus";
|
import eventBus from "@/utils/eventBus";
|
||||||
import log from "loglevel";
|
import log from "loglevel";
|
||||||
|
|
@ -12,9 +12,8 @@ import { useMutation } from "@urql/vue";
|
||||||
import { UPSERT_ASSIGNMENT_COMPLETION_MUTATION } from "@/graphql/mutations";
|
import { UPSERT_ASSIGNMENT_COMPLETION_MUTATION } from "@/graphql/mutations";
|
||||||
import type { Assignment } from "@/types";
|
import type { Assignment } from "@/types";
|
||||||
import DateEmbedding from "@/components/dueDates/DateEmbedding.vue";
|
import DateEmbedding from "@/components/dueDates/DateEmbedding.vue";
|
||||||
import { useCurrentCourseSession } from "@/composables";
|
import { useLearningMentors } from "@/composables";
|
||||||
|
import NoMentorInformationPanel from "@/components/mentor/NoMentorInformationPanel.vue";
|
||||||
const currentCourseSession = useCurrentCourseSession();
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
submissionDeadlineStart?: string | null;
|
submissionDeadlineStart?: string | null;
|
||||||
|
|
@ -29,10 +28,7 @@ const upsertAssignmentCompletionMutation = useMutation(
|
||||||
UPSERT_ASSIGNMENT_COMPLETION_MUTATION
|
UPSERT_ASSIGNMENT_COMPLETION_MUTATION
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: learningMentors } = useCSRFFetch(
|
const learningMentors = useLearningMentors().learningMentors;
|
||||||
`/api/mentor/${props.courseSessionId}/mentors`
|
|
||||||
).json();
|
|
||||||
|
|
||||||
const selectedLearningMentor = ref();
|
const selectedLearningMentor = ref();
|
||||||
|
|
||||||
const onSubmit = async () => {
|
const onSubmit = async () => {
|
||||||
|
|
@ -85,27 +81,7 @@ const onSubmit = async () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="my-6">
|
<div v-else class="my-6">
|
||||||
<div class="flex space-x-2 bg-sky-200 p-4">
|
<NoMentorInformationPanel />
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="props.submissionDeadlineStart" class="pt-6">
|
<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,113 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from "vue";
|
||||||
|
import type { LearningUnitSummary } from "@/services/selfEvaluationFeedback";
|
||||||
|
import SmileyCell from "@/components/selfEvaluationFeedback/SmileyCell.vue";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
summary: LearningUnitSummary;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const hasFeedbackReceived = computed(() => {
|
||||||
|
return props.summary.feedback_assessment?.submitted_by_provider ?? false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const feedbackProviderAvatar = computed(() => {
|
||||||
|
return props.summary.feedback_assessment?.provider_user.avatar_url ?? "";
|
||||||
|
});
|
||||||
|
|
||||||
|
const feedbackProviderName = computed(() => {
|
||||||
|
if (!props.summary.feedback_assessment?.provider_user) {
|
||||||
|
return "";
|
||||||
|
} else {
|
||||||
|
return `${props.summary.feedback_assessment.provider_user.first_name} ${props.summary.feedback_assessment.provider_user.last_name}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="bg-white" data-cy>
|
||||||
|
<!-- Top Row -->
|
||||||
|
<div class="flex items-center justify-between border-b-2 border-gray-200 p-4">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<b>{{ props.summary.title }}</b>
|
||||||
|
<span>Circle «{{ props.summary.circle_title }}»</span>
|
||||||
|
</div>
|
||||||
|
<span class="underline">
|
||||||
|
<router-link
|
||||||
|
:to="props.summary.detail_url"
|
||||||
|
:data-cy="`self-eval-${summary.id}-detail-url`"
|
||||||
|
>
|
||||||
|
{{ $t("a.Selbsteinschätzung anschauen") }}
|
||||||
|
</router-link>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="ml-4 mr-4">
|
||||||
|
<!-- Self Assessment Row-->
|
||||||
|
<div class="flex pb-2 pt-2">
|
||||||
|
<div class="w-1/2">
|
||||||
|
{{ $t("a.Deine Selbsteinschätzung") }}
|
||||||
|
</div>
|
||||||
|
<div class="cell">
|
||||||
|
<SmileyCell
|
||||||
|
:count="props.summary.self_assessment.counts.pass"
|
||||||
|
:cypress-identifier="`self-eval-${props.summary.id}-pass`"
|
||||||
|
smiley="it-icon-smiley-happy"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="cell">
|
||||||
|
<SmileyCell
|
||||||
|
:count="props.summary.self_assessment.counts.fail"
|
||||||
|
:cypress-identifier="`self-eval-${props.summary.id}-fail`"
|
||||||
|
smiley="it-icon-smiley-thinking"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="cell">
|
||||||
|
<SmileyCell
|
||||||
|
:count="props.summary.self_assessment.counts.unknown"
|
||||||
|
:cypress-identifier="`self-eval-${props.summary.id}-unknown`"
|
||||||
|
smiley="it-icon-smiley-neutral"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Feedback Assessment Row -->
|
||||||
|
<div v-if="hasFeedbackReceived" class="border-t-2 border-gray-200">
|
||||||
|
<div class="flex pb-2 pt-2">
|
||||||
|
<div class="flex w-1/2 items-center">
|
||||||
|
<span>
|
||||||
|
{{
|
||||||
|
$t("a.Fremdeinschätzung von FEEDBACK_PROVIDER_NAME", {
|
||||||
|
FEEDBACK_PROVIDER_NAME: feedbackProviderName,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
<img class="ml-2 h-7 w-7 rounded-full" :src="feedbackProviderAvatar" />
|
||||||
|
</div>
|
||||||
|
<div class="cell">
|
||||||
|
<SmileyCell
|
||||||
|
:count="props.summary.feedback_assessment?.counts.pass ?? 0"
|
||||||
|
smiley="it-icon-smiley-happy"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="cell">
|
||||||
|
<SmileyCell
|
||||||
|
:count="props.summary.feedback_assessment?.counts.fail ?? 0"
|
||||||
|
smiley="it-icon-smiley-thinking"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="cell">
|
||||||
|
<SmileyCell
|
||||||
|
:count="props.summary.feedback_assessment?.counts.unknown ?? 0"
|
||||||
|
smiley="it-icon-smiley-neutral"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="postcss" scoped>
|
||||||
|
.cell {
|
||||||
|
@apply w-12;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,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>
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import log from "loglevel";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
count: number;
|
||||||
|
smiley: string;
|
||||||
|
cypressIdentifier?: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
log.info("Rendering SmileyCell:", props.cypressIdentifier);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<template v-if="count > 0">
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
<component :is="smiley" class="mr-1 inline-block h-6 w-6"></component>
|
||||||
|
<p class="inline-block w-6" :data-cy="cypressIdentifier">
|
||||||
|
{{ count }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { DropdownSelectable } from "@/types";
|
import type { DropdownSelectable } from "@/types";
|
||||||
import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from "@headlessui/vue";
|
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
|
// https://stackoverflow.com/questions/64775876/vue-3-pass-reactive-object-to-component-with-two-way-binding
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
@ -11,6 +11,7 @@ interface Props {
|
||||||
};
|
};
|
||||||
items?: DropdownSelectable[];
|
items?: DropdownSelectable[];
|
||||||
borderless?: boolean;
|
borderless?: boolean;
|
||||||
|
placeholderText?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|
@ -25,6 +26,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
items: () => [],
|
items: () => [],
|
||||||
|
placeholderText: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const dropdownSelected = computed<DropdownSelectable>({
|
const dropdownSelected = computed<DropdownSelectable>({
|
||||||
|
|
@ -35,7 +37,7 @@ const dropdownSelected = computed<DropdownSelectable>({
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Listbox v-model="dropdownSelected" as="div">
|
<Listbox v-model="dropdownSelected" as="div">
|
||||||
<div class="relative mt-1 w-full">
|
<div class="relative w-full">
|
||||||
<ListboxButton
|
<ListboxButton
|
||||||
class="relative flex w-full cursor-default flex-row items-center bg-white py-3 pl-5 pr-10 text-left"
|
class="relative flex w-full cursor-default flex-row items-center bg-white py-3 pl-5 pr-10 text-left"
|
||||||
:class="{
|
:class="{
|
||||||
|
|
@ -47,7 +49,12 @@ const dropdownSelected = computed<DropdownSelectable>({
|
||||||
<span v-if="dropdownSelected.iconName" class="mr-4">
|
<span v-if="dropdownSelected.iconName" class="mr-4">
|
||||||
<component :is="dropdownSelected.iconName"></component>
|
<component :is="dropdownSelected.iconName"></component>
|
||||||
</span>
|
</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
|
<span
|
||||||
class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"
|
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 type { CourseStatisticsType } from "@/gql/graphql";
|
||||||
import { graphqlClient } from "@/graphql/client";
|
import { graphqlClient } from "@/graphql/client";
|
||||||
import { COURSE_QUERY, COURSE_SESSION_DETAIL_QUERY } from "@/graphql/queries";
|
import { COURSE_QUERY, COURSE_SESSION_DETAIL_QUERY } from "@/graphql/queries";
|
||||||
|
|
@ -19,6 +20,7 @@ import type {
|
||||||
CourseSession,
|
CourseSession,
|
||||||
CourseSessionDetail,
|
CourseSessionDetail,
|
||||||
LearningContentWithCompletion,
|
LearningContentWithCompletion,
|
||||||
|
LearningMentor,
|
||||||
LearningPathType,
|
LearningPathType,
|
||||||
LearningUnitPerformanceCriteria,
|
LearningUnitPerformanceCriteria,
|
||||||
PerformanceCriteria,
|
PerformanceCriteria,
|
||||||
|
|
@ -27,7 +29,7 @@ import { useQuery } from "@urql/vue";
|
||||||
import orderBy from "lodash/orderBy";
|
import orderBy from "lodash/orderBy";
|
||||||
import log from "loglevel";
|
import log from "loglevel";
|
||||||
import type { ComputedRef } from "vue";
|
import type { ComputedRef } from "vue";
|
||||||
import { computed, ref, watchEffect } from "vue";
|
import { computed, onMounted, ref, watchEffect } from "vue";
|
||||||
|
|
||||||
export function useCurrentCourseSession() {
|
export function useCurrentCourseSession() {
|
||||||
/**
|
/**
|
||||||
|
|
@ -463,3 +465,25 @@ export function useFileUpload() {
|
||||||
|
|
||||||
return { upload, error, loading, fileInfo };
|
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 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 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 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 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 dashboardProgress($courseId: ID!) {\n course_progress(course_id: $courseId) {\n _id\n course_id\n session_to_continue_id\n competence {\n _id\n total_count\n success_count\n fail_count\n }\n assignment {\n _id\n total_count\n points_max_count\n points_achieved_count\n }\n }\n }\n": types.DashboardProgressDocument,
|
||||||
"\n query courseStatistics($courseId: ID!) {\n course_statistics(course_id: $courseId) {\n _id\n course_id\n course_title\n course_slug\n course_session_properties {\n _id\n sessions {\n id\n name\n }\n generations\n circles {\n id\n name\n }\n }\n course_session_selection_ids\n course_session_selection_metrics {\n _id\n session_count\n participant_count\n expert_count\n }\n attendance_day_presences {\n _id\n records {\n _id\n course_session_id\n generation\n circle_id\n due_date\n participants_present\n participants_total\n details_url\n }\n summary {\n _id\n days_completed\n participants_present\n }\n }\n feedback_responses {\n _id\n records {\n _id\n course_session_id\n generation\n circle_id\n experts\n satisfaction_average\n satisfaction_max\n details_url\n }\n summary {\n _id\n satisfaction_average\n satisfaction_max\n total_responses\n }\n }\n assignments {\n _id\n summary {\n _id\n completed_count\n average_passed\n }\n records {\n _id\n course_session_id\n course_session_assignment_id\n circle_id\n generation\n assignment_title\n assignment_type_translation_key\n details_url\n deadline\n metrics {\n _id\n passed_count\n failed_count\n unranked_count\n ranking_completed\n average_passed\n }\n }\n }\n competences {\n _id\n summary {\n _id\n success_total\n fail_total\n }\n records {\n _id\n course_session_id\n generation\n circle_id\n title\n success_count\n fail_count\n details_url\n }\n }\n }\n }\n": types.CourseStatisticsDocument,
|
"\n 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.
|
* 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.
|
* 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 {
|
type LearningUnitObjectType implements CoursePageInterface {
|
||||||
title_hidden: Boolean!
|
title_hidden: Boolean!
|
||||||
|
feedback_user: LearnpathLearningUnitFeedbackUserChoices!
|
||||||
id: ID!
|
id: ID!
|
||||||
title: String!
|
title: String!
|
||||||
slug: String!
|
slug: String!
|
||||||
|
|
@ -292,6 +293,15 @@ type LearningUnitObjectType implements CoursePageInterface {
|
||||||
evaluate_url: String!
|
evaluate_url: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"""An enumeration."""
|
||||||
|
enum LearnpathLearningUnitFeedbackUserChoices {
|
||||||
|
"""NO_FEEDBACK"""
|
||||||
|
NO_FEEDBACK
|
||||||
|
|
||||||
|
"""MENTOR_FEEDBACK"""
|
||||||
|
MENTOR_FEEDBACK
|
||||||
|
}
|
||||||
|
|
||||||
interface LearningContentInterface {
|
interface LearningContentInterface {
|
||||||
id: ID!
|
id: ID!
|
||||||
title: String!
|
title: String!
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,7 @@ export const LearningPathObjectType = "LearningPathObjectType";
|
||||||
export const LearningSequenceObjectType = "LearningSequenceObjectType";
|
export const LearningSequenceObjectType = "LearningSequenceObjectType";
|
||||||
export const LearningUnitObjectType = "LearningUnitObjectType";
|
export const LearningUnitObjectType = "LearningUnitObjectType";
|
||||||
export const LearnpathLearningContentAssignmentAssignmentTypeChoices = "LearnpathLearningContentAssignmentAssignmentTypeChoices";
|
export const LearnpathLearningContentAssignmentAssignmentTypeChoices = "LearnpathLearningContentAssignmentAssignmentTypeChoices";
|
||||||
|
export const LearnpathLearningUnitFeedbackUserChoices = "LearnpathLearningUnitFeedbackUserChoices";
|
||||||
export const Mutation = "Mutation";
|
export const Mutation = "Mutation";
|
||||||
export const PerformanceCriteriaObjectType = "PerformanceCriteriaObjectType";
|
export const PerformanceCriteriaObjectType = "PerformanceCriteriaObjectType";
|
||||||
export const PresenceRecordStatisticsType = "PresenceRecordStatisticsType";
|
export const PresenceRecordStatisticsType = "PresenceRecordStatisticsType";
|
||||||
|
|
|
||||||
|
|
@ -239,6 +239,7 @@ export const COURSE_QUERY = graphql(`
|
||||||
icon
|
icon
|
||||||
...CoursePageFields
|
...CoursePageFields
|
||||||
learning_units {
|
learning_units {
|
||||||
|
feedback_user
|
||||||
evaluate_url
|
evaluate_url
|
||||||
...CoursePageFields
|
...CoursePageFields
|
||||||
performance_criteria {
|
performance_criteria {
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import type {
|
||||||
} from "@/types";
|
} from "@/types";
|
||||||
import log from "loglevel";
|
import log from "loglevel";
|
||||||
import { computed, onMounted, reactive } from "vue";
|
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 { useCourseSessionDetailQuery } from "@/composables";
|
||||||
import { formatDueDate } from "../../../components/dueDates/dueDatesUtils";
|
import { formatDueDate } from "../../../components/dueDates/dueDatesUtils";
|
||||||
import { stringifyParse } from "@/utils/utils";
|
import { stringifyParse } from "@/utils/utils";
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,12 @@ import LearningPathDiagram from "@/components/learningPath/LearningPathDiagram.v
|
||||||
import ItPersonRow from "@/components/ui/ItPersonRow.vue";
|
import ItPersonRow from "@/components/ui/ItPersonRow.vue";
|
||||||
|
|
||||||
import { useCourseSessionDetailQuery, useCurrentCourseSession } from "@/composables";
|
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 { useExpertCockpitStore } from "@/stores/expertCockpit";
|
||||||
import log from "loglevel";
|
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 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";
|
import { useExpertCockpitPageData } from "@/pages/cockpit/cockpitPage/composables";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { PraxisAssignment } from "@/services/mentorCockpit";
|
import type { Assignment } from "@/services/mentorCockpit";
|
||||||
import { useMentorCockpit } from "@/services/mentorCockpit";
|
import { useMentorCockpit } from "@/services/mentorCockpit";
|
||||||
import { useCurrentCourseSession } from "@/composables";
|
import { useCurrentCourseSession } from "@/composables";
|
||||||
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
|
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
|
||||||
import { computed, type Ref, ref } from "vue";
|
import { computed, type Ref, ref } from "vue";
|
||||||
import PraxisAssignmentItem from "@/components/cockpit/mentor/PraxisAssignmentItem.vue";
|
import PraxisAssignmentItem from "@/components/cockpit/mentor/PraxisAssignmentItem.vue";
|
||||||
import { useTranslation } from "i18next-vue";
|
import { useTranslation } from "i18next-vue";
|
||||||
|
import SelfAssignmentFeedbackAssignmentItem from "@/components/cockpit/mentor/SelfAssignmentFeedbackAssignmentItem.vue";
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const courseSession = useCurrentCourseSession();
|
const courseSession = useCurrentCourseSession();
|
||||||
|
|
@ -34,7 +35,7 @@ const circleFilter = computed(() => {
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
const filteredAssignments: Ref<PraxisAssignment[]> = computed(() => {
|
const filteredAssignments: Ref<Assignment[]> = computed(() => {
|
||||||
if (!summary.value) return [];
|
if (!summary.value) return [];
|
||||||
|
|
||||||
let filtered = summary.value.assignments;
|
let filtered = summary.value.assignments;
|
||||||
|
|
@ -80,6 +81,16 @@ const filteredAssignments: Ref<PraxisAssignment[]> = computed(() => {
|
||||||
}"
|
}"
|
||||||
:task-title="item.title"
|
: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>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Participant, PraxisAssignment } from "@/services/mentorCockpit";
|
import type { Assignment, Participant } from "@/services/mentorCockpit";
|
||||||
import { useMentorCockpit } from "@/services/mentorCockpit";
|
import { useMentorCockpit } from "@/services/mentorCockpit";
|
||||||
import { computed, onMounted, type Ref } from "vue";
|
import { computed, onMounted, type Ref } from "vue";
|
||||||
import { useCurrentCourseSession } from "@/composables";
|
import { useCurrentCourseSession } from "@/composables";
|
||||||
|
|
@ -11,10 +11,9 @@ const props = defineProps<{
|
||||||
|
|
||||||
const courseSession = useCurrentCourseSession();
|
const courseSession = useCurrentCourseSession();
|
||||||
const mentorCockpitStore = useMentorCockpit(courseSession.value.id);
|
const mentorCockpitStore = useMentorCockpit(courseSession.value.id);
|
||||||
|
|
||||||
const participants = computed(() => mentorCockpitStore.summary.value?.participants);
|
const participants = computed(() => mentorCockpitStore.summary.value?.participants);
|
||||||
const praxisAssignment: Ref<PraxisAssignment | null> = computed(() =>
|
const praxisAssignment: Ref<Assignment | null> = computed(() =>
|
||||||
mentorCockpitStore.getPraxisAssignmentById(props.praxisAssignmentId)
|
mentorCockpitStore.getAssignmentById(props.praxisAssignmentId)
|
||||||
);
|
);
|
||||||
|
|
||||||
const getParticipantById = (id: string): Participant | null => {
|
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>
|
||||||
|
|
@ -4,14 +4,15 @@ import { COMPETENCE_NAVI_CERTIFICATE_QUERY } from "@/graphql/queries";
|
||||||
import { useQuery } from "@urql/vue";
|
import { useQuery } from "@urql/vue";
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
import type { CompetenceCertificate } from "@/types";
|
import type { CompetenceCertificate } from "@/types";
|
||||||
import { useCurrentCourseSession, useCourseDataWithCompletion } from "@/composables";
|
import { useCurrentCourseSession } from "@/composables";
|
||||||
import {
|
import {
|
||||||
assignmentsMaxEvaluationPoints,
|
assignmentsMaxEvaluationPoints,
|
||||||
assignmentsUserPoints,
|
assignmentsUserPoints,
|
||||||
competenceCertificateProgressStatusCount,
|
competenceCertificateProgressStatusCount,
|
||||||
} from "@/pages/competence/utils";
|
} from "@/pages/competence/utils";
|
||||||
|
import { useSelfEvaluationFeedbackSummaries } from "@/services/selfEvaluationFeedback";
|
||||||
import ItProgress from "@/components/ui/ItProgress.vue";
|
import ItProgress from "@/components/ui/ItProgress.vue";
|
||||||
import { calcPerformanceCriteriaStatusCount } from "@/services/competence";
|
import { VV_COURSE_IDS } from "@/constants";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
courseSlug: string;
|
courseSlug: string;
|
||||||
|
|
@ -20,7 +21,6 @@ const props = defineProps<{
|
||||||
log.debug("CompetenceIndexPage setup", props);
|
log.debug("CompetenceIndexPage setup", props);
|
||||||
|
|
||||||
const courseSession = useCurrentCourseSession();
|
const courseSession = useCurrentCourseSession();
|
||||||
const courseData = useCourseDataWithCompletion(props.courseSlug);
|
|
||||||
|
|
||||||
const certificatesQuery = useQuery({
|
const certificatesQuery = useQuery({
|
||||||
query: COMPETENCE_NAVI_CERTIFICATE_QUERY,
|
query: COMPETENCE_NAVI_CERTIFICATE_QUERY,
|
||||||
|
|
@ -49,16 +49,41 @@ const userPointsEvaluatedAssignments = computed(() => {
|
||||||
return assignmentsUserPoints(allAssignments.value);
|
return assignmentsUserPoints(allAssignments.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
const performanceCriteriaStatusCount = computed(() => {
|
const selfEvaluationFeedbackSummaries = useSelfEvaluationFeedbackSummaries(
|
||||||
return calcPerformanceCriteriaStatusCount(courseData.flatPerformanceCriteria.value);
|
useCurrentCourseSession().value.id
|
||||||
|
);
|
||||||
|
|
||||||
|
const selfAssessmentCounts = computed(
|
||||||
|
() => selfEvaluationFeedbackSummaries.aggregates.value?.self_assessment
|
||||||
|
);
|
||||||
|
|
||||||
|
const feedbackEvaluationCounts = computed(
|
||||||
|
() => selfEvaluationFeedbackSummaries.aggregates.value?.feedback_assessment
|
||||||
|
);
|
||||||
|
|
||||||
|
const isFeedbackEvaluationVisible = computed(
|
||||||
|
() =>
|
||||||
|
selfEvaluationFeedbackSummaries.aggregates.value?.feedback_assessment_visible ??
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
// FIXME 22.02.24: To-be-tackled NEXT in a separate PR (shippable member comp.navi)
|
||||||
|
// -> Do not use the VV_COURSE_ID anymore (discuss with @chrigu) -> We do this next.
|
||||||
|
const currentCourseSession = useCurrentCourseSession();
|
||||||
|
const hasCompetenceCertificates = computed(() => {
|
||||||
|
return !VV_COURSE_IDS.includes(currentCourseSession.value.course.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isLoaded = computed(
|
||||||
|
() =>
|
||||||
|
!selfEvaluationFeedbackSummaries.loading.value && !certificatesQuery.fetching.value
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="container-large lg:mt-4">
|
<div v-if="isLoaded" class="container-large lg:mt-4">
|
||||||
<h1 class="mb-8">{{ $t("a.KompetenzNavi") }}</h1>
|
<!-- Competence certificates -->
|
||||||
|
<section v-if="hasCompetenceCertificates" class="mb-4 bg-white p-8">
|
||||||
<section class="mb-4 bg-white p-8">
|
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<h3>{{ $t("a.Kompetenznachweise") }}</h3>
|
<h3>{{ $t("a.Kompetenznachweise") }}</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -80,7 +105,7 @@ const performanceCriteriaStatusCount = computed(() => {
|
||||||
<div
|
<div
|
||||||
v-for="certificate in competenceCertificates"
|
v-for="certificate in competenceCertificates"
|
||||||
:key="certificate.id"
|
:key="certificate.id"
|
||||||
class="flex flex-col justify-between border-b py-4 first:border-t lg:flex-row lg:items-center"
|
class="flex flex-col justify-between py-4 lg:flex-row lg:items-center"
|
||||||
:data-cy="`certificate-${certificate.slug}`"
|
:data-cy="`certificate-${certificate.slug}`"
|
||||||
>
|
>
|
||||||
<div class="text-bold text-xl">
|
<div class="text-bold text-xl">
|
||||||
|
|
@ -130,59 +155,96 @@ const performanceCriteriaStatusCount = computed(() => {
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- Self-evaluation -->
|
||||||
<section class="mb-4 bg-white px-8 py-4 lg:mb-8 lg:py-8">
|
<section class="mb-4 bg-white px-8 py-4 lg:mb-8 lg:py-8">
|
||||||
<h3 class="mb-4 border-b pb-4 lg:border-0 lg:pb-0">
|
<div class="mb-8">
|
||||||
{{ $t("a.Selbsteinschätzungen") }}
|
<h3 class="mb-4 pb-4 lg:pb-0">
|
||||||
</h3>
|
{{ $t("a.Selbsteinschätzungen") }}
|
||||||
<ul
|
</h3>
|
||||||
class="mb-6 flex flex-col lg:flex-row lg:items-center lg:justify-between lg:gap-8"
|
<ul
|
||||||
>
|
class="mb-6 flex flex-col lg:flex-row lg:items-center lg:justify-between lg:gap-8"
|
||||||
<li
|
|
||||||
class="mb-4 inline-block flex-1 border-b pb-4 lg:mb-0 lg:w-1/3 lg:border-b-0 lg:border-r lg:pb-0"
|
|
||||||
>
|
>
|
||||||
<h5 class="mb-4 text-gray-700">«{{ $t("selfEvaluation.no") }}»</h5>
|
<li class="mb-4 inline-block flex-1 pb-4 lg:mb-0 lg:w-1/3 lg:pb-0">
|
||||||
<div class="flex flex-row items-center">
|
<h5 class="mb-4 text-gray-700">«{{ $t("selfEvaluation.no") }}»</h5>
|
||||||
<it-icon-smiley-thinking class="h-16 w-16"></it-icon-smiley-thinking>
|
<div class="flex flex-row items-center">
|
||||||
<p
|
<it-icon-smiley-thinking class="h-16 w-16"></it-icon-smiley-thinking>
|
||||||
class="ml-4 inline-block text-7xl font-bold"
|
<p
|
||||||
data-cy="self-evaluation-fail"
|
class="ml-4 inline-block text-7xl font-bold"
|
||||||
>
|
data-cy="self-evaluation-fail"
|
||||||
{{ performanceCriteriaStatusCount.FAIL }}
|
>
|
||||||
</p>
|
{{ selfAssessmentCounts?.fail }}
|
||||||
</div>
|
</p>
|
||||||
</li>
|
</div>
|
||||||
<li
|
</li>
|
||||||
class="mb-4 inline-block flex-1 border-b pb-4 lg:mb-0 lg:w-1/3 lg:border-b-0 lg:border-r lg:pb-0"
|
<li class="mb-4 inline-block flex-1 pb-4 lg:mb-0 lg:w-1/3 lg:pb-0">
|
||||||
|
<h5 class="mb-4 text-gray-700">«{{ $t("selfEvaluation.yes") }}»</h5>
|
||||||
|
<div class="flex flex-row items-center">
|
||||||
|
<it-icon-smiley-happy class="h-16 w-16"></it-icon-smiley-happy>
|
||||||
|
<p
|
||||||
|
class="ml-4 inline-block text-7xl font-bold"
|
||||||
|
data-cy="self-evaluation-success"
|
||||||
|
>
|
||||||
|
{{ selfAssessmentCounts?.pass }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="flex-1 pb-4 lg:mb-0 lg:w-1/3 lg:pb-0">
|
||||||
|
<h5 class="mb-4 text-gray-700">{{ $t("competences.notAssessed") }}</h5>
|
||||||
|
<div class="flex flex-row items-center">
|
||||||
|
<it-icon-smiley-neutral class="h-16 w-16"></it-icon-smiley-neutral>
|
||||||
|
<p
|
||||||
|
class="ml-4 inline-block text-7xl font-bold"
|
||||||
|
data-cy="self-evaluation-unknown"
|
||||||
|
>
|
||||||
|
{{ selfAssessmentCounts?.unknown }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Feedback evaluation -->
|
||||||
|
<div v-if="isFeedbackEvaluationVisible" class="mb-8 border-t pt-8">
|
||||||
|
<h3 class="mb-4 pb-4 lg:pb-0">
|
||||||
|
{{ $t("a.Fremdeinschätzungen") }}
|
||||||
|
</h3>
|
||||||
|
<ul
|
||||||
|
class="mb-6 flex flex-col lg:flex-row lg:items-center lg:justify-between lg:gap-8"
|
||||||
>
|
>
|
||||||
<h5 class="mb-4 text-gray-700">«{{ $t("selfEvaluation.yes") }}»</h5>
|
<li class="mb-4 inline-block flex-1 pb-4 lg:mb-0 lg:w-1/3 lg:pb-0">
|
||||||
<div class="flex flex-row items-center">
|
<h5 class="mb-4 text-gray-700">«{{ $t("receivedEvaluation.no") }}»</h5>
|
||||||
<it-icon-smiley-happy class="h-16 w-16"></it-icon-smiley-happy>
|
<div class="flex flex-row items-center">
|
||||||
<p
|
<it-icon-smiley-thinking class="h-16 w-16"></it-icon-smiley-thinking>
|
||||||
class="ml-4 inline-block text-7xl font-bold"
|
<p class="ml-4 inline-block text-7xl font-bold">
|
||||||
data-cy="self-evaluation-success"
|
{{ feedbackEvaluationCounts?.fail }}
|
||||||
>
|
</p>
|
||||||
{{ performanceCriteriaStatusCount.SUCCESS }}
|
</div>
|
||||||
</p>
|
</li>
|
||||||
</div>
|
<li class="mb-4 inline-block flex-1 pb-4 lg:mb-0 lg:w-1/3 lg:pb-0">
|
||||||
</li>
|
<h5 class="mb-4 text-gray-700">«{{ $t("receivedEvaluation.yes") }}»</h5>
|
||||||
<li class="flex-1 border-b pb-4 lg:mb-0 lg:w-1/3 lg:border-b-0 lg:pb-0">
|
<div class="flex flex-row items-center">
|
||||||
<h5 class="mb-4 text-gray-700">{{ $t("competences.notAssessed") }}</h5>
|
<it-icon-smiley-happy class="h-16 w-16"></it-icon-smiley-happy>
|
||||||
<div class="flex flex-row items-center">
|
<p class="ml-4 inline-block text-7xl font-bold">
|
||||||
<it-icon-smiley-neutral class="h-16 w-16"></it-icon-smiley-neutral>
|
{{ feedbackEvaluationCounts?.pass }}
|
||||||
<p
|
</p>
|
||||||
class="ml-4 inline-block text-7xl font-bold"
|
</div>
|
||||||
data-cy="self-evaluation-unknown"
|
</li>
|
||||||
>
|
<li class="flex-1 pb-4 lg:mb-0 lg:w-1/3 lg:pb-0">
|
||||||
{{ performanceCriteriaStatusCount.UNKNOWN }}
|
<h5 class="mb-4 text-gray-700">{{ $t("competences.notAssessed") }}</h5>
|
||||||
</p>
|
<div class="flex flex-row items-center">
|
||||||
</div>
|
<it-icon-smiley-neutral class="h-16 w-16"></it-icon-smiley-neutral>
|
||||||
</li>
|
<p class="ml-4 inline-block text-7xl font-bold">
|
||||||
</ul>
|
{{ feedbackEvaluationCounts?.unknown }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<router-link
|
<router-link
|
||||||
:to="`/course/${props.courseSlug}/competence/criteria`"
|
:to="`/course/${props.courseSlug}/competence/self-evaluation-and-feedback`"
|
||||||
class="btn-text mt-4 inline-flex items-center py-2 pl-0"
|
class="btn-text inline-flex items-center py-2 pl-0"
|
||||||
>
|
>
|
||||||
<span>{{ $t("general.showAll") }}</span>
|
<span>{{ $t("general.showAll") }}</span>
|
||||||
<it-icon-arrow-right></it-icon-arrow-right>
|
<it-icon-arrow-right></it-icon-arrow-right>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import * as log from "loglevel";
|
import * as log from "loglevel";
|
||||||
import { onMounted } from "vue";
|
import { computed, onMounted } from "vue";
|
||||||
import { useRoute } from "vue-router";
|
import { useRoute } from "vue-router";
|
||||||
|
import { VV_COURSE_IDS } from "@/constants";
|
||||||
|
import { useCurrentCourseSession } from "@/composables";
|
||||||
|
|
||||||
log.debug("CompetenceParentPage created");
|
log.debug("CompetenceParentPage created");
|
||||||
|
|
||||||
|
|
@ -19,14 +21,21 @@ function routeInCompetenceCertificate() {
|
||||||
return route.path.includes("/certificate");
|
return route.path.includes("/certificate");
|
||||||
}
|
}
|
||||||
|
|
||||||
function routeInPerformanceCriteria() {
|
|
||||||
return route.path.endsWith("/criteria");
|
|
||||||
}
|
|
||||||
|
|
||||||
function routeInActionCompetences() {
|
function routeInActionCompetences() {
|
||||||
return route.path.endsWith("/competences");
|
return route.path.endsWith("/competences");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function routeInSelfEvaluationAndFeedback() {
|
||||||
|
return route.path.endsWith("/self-evaluation-and-feedback");
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME 22.02.24: To-be-tackled NEXT in a separate PR (shippable member comp.navi)
|
||||||
|
// -> Do not use the VV_COURSE_ID anymore (discuss with @chrigu) -> We do this next.
|
||||||
|
const currentCourseSession = useCurrentCourseSession();
|
||||||
|
const isVVCourse = computed(() => {
|
||||||
|
return VV_COURSE_IDS.includes(currentCourseSession.value.course.id);
|
||||||
|
});
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
log.debug("CompetenceParentPage mounted", props.courseSlug);
|
log.debug("CompetenceParentPage mounted", props.courseSlug);
|
||||||
});
|
});
|
||||||
|
|
@ -45,6 +54,7 @@ onMounted(async () => {
|
||||||
</router-link>
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
|
v-if="!isVVCourse"
|
||||||
class="border-t-2 border-t-transparent lg:ml-12"
|
class="border-t-2 border-t-transparent lg:ml-12"
|
||||||
:class="{ 'border-b-2 border-b-blue-900': routeInCompetenceCertificate() }"
|
:class="{ 'border-b-2 border-b-blue-900': routeInCompetenceCertificate() }"
|
||||||
>
|
>
|
||||||
|
|
@ -57,16 +67,21 @@ onMounted(async () => {
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
class="border-t-2 border-t-transparent lg:ml-12"
|
class="border-t-2 border-t-transparent lg:ml-12"
|
||||||
:class="{ 'border-b-2 border-b-blue-900': routeInPerformanceCriteria() }"
|
:class="{
|
||||||
|
'border-b-2 border-b-blue-900': routeInSelfEvaluationAndFeedback(),
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<router-link
|
<router-link
|
||||||
:to="`/course/${courseSlug}/competence/criteria`"
|
:to="`/course/${courseSlug}/competence/self-evaluation-and-feedback`"
|
||||||
class="block py-3"
|
class="block py-3"
|
||||||
>
|
>
|
||||||
{{ $t("a.Selbsteinschätzungen") }}
|
{{
|
||||||
|
isVVCourse
|
||||||
|
? $t("a.Selbst- und Fremdeinschätzungen")
|
||||||
|
: $t("a.Selbsteinschätzungen")
|
||||||
|
}}
|
||||||
</router-link>
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li
|
<li
|
||||||
class="border-t-2 border-t-transparent lg:ml-12"
|
class="border-t-2 border-t-transparent lg:ml-12"
|
||||||
:class="{ 'border-b-2 border-b-blue-900': routeInActionCompetences() }"
|
:class="{ 'border-b-2 border-b-blue-900': routeInActionCompetences() }"
|
||||||
|
|
|
||||||
|
|
@ -1,115 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import * as log from "loglevel";
|
|
||||||
import { computed } from "vue";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { useCourseDataWithCompletion } from "@/composables";
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
courseSlug: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
log.debug("PerformanceCriteriaPage created", props);
|
|
||||||
|
|
||||||
const courseCompletionData = useCourseDataWithCompletion(props.courseSlug);
|
|
||||||
|
|
||||||
const uniqueLearningUnits = computed(() => {
|
|
||||||
// FIXME: this complex calculation can go away,
|
|
||||||
// once the criteria are in its own learning content
|
|
||||||
// get the learningUnits sorted by circle order in the course
|
|
||||||
const circles = (courseCompletionData.circles.value ?? []).map((c, index) => {
|
|
||||||
return { ...c, sortKey: index };
|
|
||||||
});
|
|
||||||
return _.orderBy(
|
|
||||||
_.uniqBy(
|
|
||||||
(courseCompletionData.flatPerformanceCriteria.value ?? [])
|
|
||||||
.filter((pc) => Boolean(pc.learning_unit))
|
|
||||||
.map((pc) => {
|
|
||||||
return {
|
|
||||||
luId: pc.learning_unit?.id,
|
|
||||||
luTitle: pc.learning_unit?.title,
|
|
||||||
luSlug: pc.learning_unit?.slug,
|
|
||||||
circleId: pc.circle.id,
|
|
||||||
circleTitle: pc.circle.title,
|
|
||||||
url: pc.learning_unit?.evaluate_url,
|
|
||||||
sortKey: circles.find((c) => c.id === pc.circle.id)?.sortKey,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
"luId"
|
|
||||||
),
|
|
||||||
"sortKey"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const criteriaByLearningUnit = computed(() => {
|
|
||||||
return uniqueLearningUnits.value.map((lu) => {
|
|
||||||
const criteria = (courseCompletionData.flatPerformanceCriteria.value ?? []).filter(
|
|
||||||
(pc) => pc.learning_unit?.id === lu.luId
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
...lu,
|
|
||||||
countSuccess: criteria.filter((c) => c.completion_status === "SUCCESS").length,
|
|
||||||
countFail: criteria.filter((c) => c.completion_status === "FAIL").length,
|
|
||||||
countUnknown: criteria.filter((c) => c.completion_status === "UNKNOWN").length,
|
|
||||||
criteria: criteria,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="container-large">
|
|
||||||
<h2 class="mb-4 lg:py-4">{{ $t("a.Selbsteinschätzungen") }}</h2>
|
|
||||||
<section class="mb-4 bg-white px-4 py-2">
|
|
||||||
<div
|
|
||||||
v-for="selfEvaluation in criteriaByLearningUnit"
|
|
||||||
:key="selfEvaluation.luId"
|
|
||||||
class="flex flex-col justify-between gap-4 border-b py-4 last:border-b-0 lg:flex-row lg:items-center"
|
|
||||||
>
|
|
||||||
<div class="lg:w-1/3">
|
|
||||||
{{ $t("a.Circle") }}
|
|
||||||
{{ selfEvaluation.circleTitle }}:
|
|
||||||
{{ selfEvaluation.luTitle }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-row items-center lg:w-1/3">
|
|
||||||
<div class="mr-6 flex flex-row items-center">
|
|
||||||
<it-icon-smiley-thinking
|
|
||||||
class="mr-2 inline-block h-8 w-8"
|
|
||||||
></it-icon-smiley-thinking>
|
|
||||||
<div class="w-6" :data-cy="`${selfEvaluation.luSlug}-fail`">
|
|
||||||
{{ selfEvaluation.countFail }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<li class="mr-6 flex flex-row items-center">
|
|
||||||
<it-icon-smiley-happy
|
|
||||||
class="mr-2 inline-block h-8 w-8"
|
|
||||||
></it-icon-smiley-happy>
|
|
||||||
<div class="w-6" :data-cy="`${selfEvaluation.luSlug}-success`">
|
|
||||||
{{ selfEvaluation.countSuccess }}
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li class="flex flex-row items-center">
|
|
||||||
<it-icon-smiley-neutral
|
|
||||||
class="mr-2 inline-block h-8 w-8"
|
|
||||||
></it-icon-smiley-neutral>
|
|
||||||
<div class="w-6" :data-cy="`${selfEvaluation.luSlug}-unknown`">
|
|
||||||
{{ selfEvaluation.countUnknown }}
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<router-link
|
|
||||||
:to="selfEvaluation.url ?? '/'"
|
|
||||||
class="link"
|
|
||||||
:data-cy="`${selfEvaluation.luSlug}-open`"
|
|
||||||
>
|
|
||||||
{{ $t("a.Selbsteinschätzung anschauen") }}
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useSelfEvaluationFeedbackSummaries } from "@/services/selfEvaluationFeedback";
|
||||||
|
import { useCurrentCourseSession } from "@/composables";
|
||||||
|
import { computed, ref } from "vue";
|
||||||
|
import FeedbackByLearningUnitSummary from "@/components/selfEvaluationFeedback/FeedbackByLearningUnitSummary.vue";
|
||||||
|
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
|
||||||
|
import { t } from "i18next";
|
||||||
|
|
||||||
|
const selfEvaluationFeedbackSummaries = useSelfEvaluationFeedbackSummaries(
|
||||||
|
useCurrentCourseSession().value.id
|
||||||
|
);
|
||||||
|
|
||||||
|
const isLoaded = computed(() => !selfEvaluationFeedbackSummaries.loading.value);
|
||||||
|
|
||||||
|
const selectedCircle = ref({ name: t("a.AlleCircle"), id: "_all" });
|
||||||
|
|
||||||
|
const circles = computed(() => [
|
||||||
|
{ name: t("a.AlleCircle"), id: "_all" },
|
||||||
|
...selfEvaluationFeedbackSummaries.circles.value.map((circle) => ({
|
||||||
|
name: `Circle: ${circle.title}`,
|
||||||
|
id: circle.id,
|
||||||
|
})),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const summaries = computed(() => {
|
||||||
|
if (selectedCircle.value.id === "_all") {
|
||||||
|
return selfEvaluationFeedbackSummaries.summaries.value;
|
||||||
|
}
|
||||||
|
return selfEvaluationFeedbackSummaries.summaries.value.filter(
|
||||||
|
(summary) => summary.circle_id === selectedCircle.value.id
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const headerTitle = computed(() => {
|
||||||
|
const canHaveFeedback =
|
||||||
|
selfEvaluationFeedbackSummaries.aggregates.value?.feedback_assessment_visible ??
|
||||||
|
false;
|
||||||
|
if (canHaveFeedback) {
|
||||||
|
return t("a.Selbst- und Fremdeinschätzungen");
|
||||||
|
} else {
|
||||||
|
return t("a.Selbsteinschätzungen");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="isLoaded">
|
||||||
|
<div class="container-large">
|
||||||
|
<div class="col flex items-center justify-between pb-4">
|
||||||
|
<h2 class="py-4">{{ headerTitle }}</h2>
|
||||||
|
<ItDropdownSelect
|
||||||
|
v-model="selectedCircle"
|
||||||
|
class="text-bold w-24 min-w-[18rem] border-2 border-gray-300"
|
||||||
|
:items="circles"
|
||||||
|
borderless
|
||||||
|
></ItDropdownSelect>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<FeedbackByLearningUnitSummary
|
||||||
|
v-for="summary in summaries"
|
||||||
|
:key="summary.id"
|
||||||
|
:summary="summary"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
|
|
@ -45,7 +45,7 @@ const competenceCertificateUrl = computed(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const competenceCriteriaUrl = computed(() => {
|
const competenceCriteriaUrl = computed(() => {
|
||||||
return `/course/${courseSlug.value}/competence/criteria?courseSessionId=${courseSessionProgress.value?.session_to_continue_id}`;
|
return `/course/${courseSlug.value}/competence/self-evaluation-and-feedback?courseSessionId=${courseSessionProgress.value?.session_to_continue_id}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
const isVVCourse = computed(() => {
|
const isVVCourse = computed(() => {
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ defineEmits(["exit"]);
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<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" />
|
<CoursePreviewBar v-if="courseSessionsStore.hasCourseSessionPreview" />
|
||||||
<div
|
<div
|
||||||
:class="{
|
:class="{
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,7 @@ const icon = computed(() => {
|
||||||
const onExit = async () => {
|
const onExit = async () => {
|
||||||
await props.beforeExitCallback();
|
await props.beforeExitCallback();
|
||||||
eventBus.emit("finishedLearningContent", true);
|
eventBus.emit("finishedLearningContent", true);
|
||||||
|
emit("exit");
|
||||||
};
|
};
|
||||||
|
|
||||||
const emit = defineEmits(["previous", "next", "exit"]);
|
const emit = defineEmits(["previous", "next", "exit"]);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import * as log from "loglevel";
|
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 { computed } from "vue";
|
||||||
import { useCourseDataWithCompletion } from "@/composables";
|
import { useCourseDataWithCompletion } from "@/composables";
|
||||||
|
|
||||||
|
|
@ -14,9 +14,9 @@ const props = defineProps<{
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const courseData = useCourseDataWithCompletion(props.courseSlug);
|
const courseData = useCourseDataWithCompletion(props.courseSlug);
|
||||||
const learningUnit = computed(() =>
|
const learningUnit = computed(() => {
|
||||||
courseData.findLearningUnit(props.learningUnitSlug, props.circleSlug)
|
return courseData.findLearningUnit(props.learningUnitSlug, props.circleSlug);
|
||||||
);
|
});
|
||||||
const circle = computed(() => {
|
const circle = computed(() => {
|
||||||
return courseData.findCircle(props.circleSlug);
|
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>
|
||||||
|
|
@ -92,7 +92,6 @@ const router = createRouter({
|
||||||
props: true,
|
props: true,
|
||||||
component: () => import("@/pages/competence/CompetenceIndexPage.vue"),
|
component: () => import("@/pages/competence/CompetenceIndexPage.vue"),
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
path: "certificates",
|
path: "certificates",
|
||||||
props: true,
|
props: true,
|
||||||
|
|
@ -106,9 +105,10 @@ const router = createRouter({
|
||||||
import("@/pages/competence/CompetenceCertificateDetailPage.vue"),
|
import("@/pages/competence/CompetenceCertificateDetailPage.vue"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "criteria",
|
path: "self-evaluation-and-feedback",
|
||||||
props: true,
|
props: true,
|
||||||
component: () => import("@/pages/competence/PerformanceCriteriaPage.vue"),
|
component: () =>
|
||||||
|
import("@/pages/competence/SelfEvaluationAndFeedbackPage.vue"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "competences",
|
path: "competences",
|
||||||
|
|
@ -180,7 +180,7 @@ const router = createRouter({
|
||||||
{
|
{
|
||||||
path: "",
|
path: "",
|
||||||
component: () =>
|
component: () =>
|
||||||
import("@/pages/cockpit/cockpitPage/mentor/MentorOverview.vue"),
|
import("@/pages/cockpit/cockpitPage/mentor/MentorOverviewPage.vue"),
|
||||||
name: "mentorCockpitOverview",
|
name: "mentorCockpitOverview",
|
||||||
meta: {
|
meta: {
|
||||||
cockpitType: "mentor",
|
cockpitType: "mentor",
|
||||||
|
|
@ -189,12 +189,24 @@ const router = createRouter({
|
||||||
{
|
{
|
||||||
path: "participants",
|
path: "participants",
|
||||||
component: () =>
|
component: () =>
|
||||||
import("@/pages/cockpit/cockpitPage/mentor/MentorParticipants.vue"),
|
import("@/pages/cockpit/cockpitPage/mentor/MentorParticipantsPage.vue"),
|
||||||
name: "mentorCockpitParticipants",
|
name: "mentorCockpitParticipants",
|
||||||
meta: {
|
meta: {
|
||||||
cockpitType: "mentor",
|
cockpitType: "mentor",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "self-evaluation-feedback/:learningUnitId",
|
||||||
|
component: () =>
|
||||||
|
import(
|
||||||
|
"@/pages/cockpit/cockpitPage/mentor/SelfEvaluationFeedbackPage.vue"
|
||||||
|
),
|
||||||
|
name: "mentorSelfEvaluationFeedback",
|
||||||
|
meta: {
|
||||||
|
cockpitType: "mentor",
|
||||||
|
},
|
||||||
|
props: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "details",
|
path: "details",
|
||||||
component: () =>
|
component: () =>
|
||||||
|
|
@ -207,7 +219,7 @@ const router = createRouter({
|
||||||
path: "praxis-assignments/:praxisAssignmentId",
|
path: "praxis-assignments/:praxisAssignmentId",
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import(
|
||||||
"@/pages/cockpit/cockpitPage/mentor/MentorPraxisAssignment.vue"
|
"@/pages/cockpit/cockpitPage/mentor/MentorPraxisAssignmentPage.vue"
|
||||||
),
|
),
|
||||||
name: "mentorCockpitPraxisAssignments",
|
name: "mentorCockpitPraxisAssignments",
|
||||||
meta: {
|
meta: {
|
||||||
|
|
@ -215,6 +227,18 @@ const router = createRouter({
|
||||||
},
|
},
|
||||||
props: true,
|
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;
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PraxisAssignment {
|
export interface Assignment {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
circle_id: string;
|
circle_id: string;
|
||||||
|
|
@ -42,7 +42,7 @@ export interface PraxisAssignment {
|
||||||
interface Summary {
|
interface Summary {
|
||||||
participants: Participant[];
|
participants: Participant[];
|
||||||
circles: Circle[];
|
circles: Circle[];
|
||||||
assignments: PraxisAssignment[];
|
assignments: Assignment[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useMentorCockpit = (
|
export const useMentorCockpit = (
|
||||||
|
|
@ -60,7 +60,7 @@ export const useMentorCockpit = (
|
||||||
return "";
|
return "";
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPraxisAssignmentById = (id: string): PraxisAssignment | null => {
|
const getAssignmentById = (id: string): Assignment | null => {
|
||||||
if (summary.value?.assignments) {
|
if (summary.value?.assignments) {
|
||||||
const found = summary.value.assignments.find(
|
const found = summary.value.assignments.find(
|
||||||
(assignment) => assignment.id === id
|
(assignment) => assignment.id === id
|
||||||
|
|
@ -93,7 +93,7 @@ export const useMentorCockpit = (
|
||||||
summary,
|
summary,
|
||||||
error,
|
error,
|
||||||
getCircleTitleById,
|
getCircleTitleById,
|
||||||
getPraxisAssignmentById,
|
|
||||||
fetchData,
|
fetchData,
|
||||||
|
getAssignmentById,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,268 @@
|
||||||
|
import { useCSRFFetch } from "@/fetchHelpers";
|
||||||
|
import type { User } from "@/types";
|
||||||
|
import { toValue } from "@vueuse/core";
|
||||||
|
import { t } from "i18next";
|
||||||
|
import log from "loglevel";
|
||||||
|
import type { Ref } from "vue";
|
||||||
|
import { computed, onMounted, ref } from "vue";
|
||||||
|
|
||||||
|
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";
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FeedbackSummaryCounts {
|
||||||
|
pass: number;
|
||||||
|
fail: number;
|
||||||
|
unknown: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FeedbackSummaryAggregates {
|
||||||
|
// totals across all learning units in the course session
|
||||||
|
self_assessment: FeedbackSummaryCounts;
|
||||||
|
feedback_assessment: FeedbackSummaryCounts;
|
||||||
|
// does this course have any feedback?
|
||||||
|
feedback_assessment_visible: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FeedbackAssessmentSummary {
|
||||||
|
counts: FeedbackSummaryCounts;
|
||||||
|
submitted_by_provider: boolean;
|
||||||
|
provider_user: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SelfAssessmentSummary {
|
||||||
|
counts: FeedbackSummaryCounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LearningUnitSummary {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
circle_id: string;
|
||||||
|
circle_title: string;
|
||||||
|
feedback_assessment?: FeedbackAssessmentSummary;
|
||||||
|
self_assessment: SelfAssessmentSummary;
|
||||||
|
detail_url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Circle {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** To keep the backend permissions model simple, we have two endpoints:
|
||||||
|
* 1. /requester/: for the user who requested the feedback
|
||||||
|
* 2. /provider/: for the user who provides the feedback
|
||||||
|
*
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
log.info("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 function useSelfEvaluationFeedbackSummaries(
|
||||||
|
courseSessionId: Ref<string> | string
|
||||||
|
) {
|
||||||
|
const summaries = ref<LearningUnitSummary[]>([]);
|
||||||
|
const aggregates = ref<FeedbackSummaryAggregates>();
|
||||||
|
const circles = ref<Circle[]>([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
const error = ref();
|
||||||
|
|
||||||
|
const url = computed(
|
||||||
|
() =>
|
||||||
|
`/api/self-evaluation-feedback/requester/${courseSessionId}/feedbacks/summaries`
|
||||||
|
);
|
||||||
|
|
||||||
|
const fetchFeedbackSummaries = async () => {
|
||||||
|
error.value = undefined;
|
||||||
|
loading.value = true;
|
||||||
|
|
||||||
|
log.info("Fetching feedback summaries for course session", courseSessionId);
|
||||||
|
const { data, error: _error } = await useCSRFFetch(url.value).json();
|
||||||
|
loading.value = false;
|
||||||
|
|
||||||
|
if (_error.value) {
|
||||||
|
error.value = _error;
|
||||||
|
summaries.value = [];
|
||||||
|
circles.value = [];
|
||||||
|
aggregates.value = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
summaries.value = data.value.results;
|
||||||
|
aggregates.value = data.value.aggregates;
|
||||||
|
circles.value = data.value.circles;
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(fetchFeedbackSummaries);
|
||||||
|
|
||||||
|
return {
|
||||||
|
summaries,
|
||||||
|
aggregates,
|
||||||
|
circles,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getSmiley = (assessment: "FAIL" | "SUCCESS" | "UNKNOWN") => {
|
||||||
|
switch (assessment) {
|
||||||
|
case "SUCCESS":
|
||||||
|
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";
|
role: "EXPERT";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Mentor {
|
||||||
|
id: number;
|
||||||
|
first_name: string;
|
||||||
|
last_name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LearningMentor {
|
||||||
|
id: number;
|
||||||
|
mentor: Mentor;
|
||||||
|
}
|
||||||
|
|
||||||
export type CourseSessionDetail = CourseSessionObjectType;
|
export type CourseSessionDetail = CourseSessionObjectType;
|
||||||
|
|
||||||
// document upload
|
// document upload
|
||||||
|
|
@ -579,3 +590,16 @@ export interface FeedbackData {
|
||||||
};
|
};
|
||||||
feedbackType: FeedbackType;
|
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", () => {
|
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", () => {
|
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(
|
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");
|
).should("have.class", "cy-unchecked");
|
||||||
|
|
||||||
cy.get(
|
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();
|
).click();
|
||||||
|
|
||||||
cy.get(
|
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");
|
).should("have.class", "cy-checked");
|
||||||
|
|
||||||
// completion data should still be there after reload
|
// completion data should still be there after reload
|
||||||
cy.reload();
|
cy.reload();
|
||||||
cy.get(
|
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");
|
).should("have.class", "cy-checked");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("can open learning contents and complete them by continuing", () => {
|
it("can open learning contents and complete them by continuing", () => {
|
||||||
cy.get(
|
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();
|
).click();
|
||||||
cy.get("[data-cy=\"lc-title\"]").should(
|
cy.get('[data-cy="lc-title"]').should(
|
||||||
"contain",
|
"contain",
|
||||||
"Verschaffe dir einen Überblick"
|
"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=\"circle-title\"]").should("contain", "Fahrzeug");
|
cy.get('[data-cy="circle-title"]').should("contain", "Fahrzeug");
|
||||||
|
|
||||||
cy.get("[data-cy=\"ls-continue-button\"]").click({ force: true });
|
cy.get('[data-cy="ls-continue-button"]').click();
|
||||||
cy.get("[data-cy=\"lc-title\"]").should(
|
cy.get('[data-cy="lc-title"]').should(
|
||||||
"contain",
|
"contain",
|
||||||
"Handlungsfeld «Fahrzeug»"
|
"Handlungsfeld «Fahrzeug»"
|
||||||
);
|
);
|
||||||
cy.get("[data-cy=\"complete-and-continue\"]").click({ force: true });
|
cy.get('[data-cy="complete-and-continue"]').click({ force: true });
|
||||||
cy.get("[data-cy=\"circle-title\"]").should("contain", "Fahrzeug");
|
cy.get('[data-cy="circle-title"]').should("contain", "Fahrzeug");
|
||||||
|
|
||||||
cy.get(
|
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");
|
).should("have.class", "cy-checked");
|
||||||
cy.get(
|
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");
|
).should("have.class", "cy-checked");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("continue button works", () => {
|
it("continue button works", () => {
|
||||||
cy.get("[data-cy=\"ls-continue-button\"]").should("contain", "Los geht's");
|
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"]').click();
|
||||||
|
|
||||||
cy.get("[data-cy=\"lc-title\"]").should(
|
cy.get('[data-cy="lc-title"]').should(
|
||||||
"contain",
|
"contain",
|
||||||
"Verschaffe dir einen Überblick"
|
"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"]').should("contain", "Weiter geht's");
|
||||||
cy.get("[data-cy=\"ls-continue-button\"]").click();
|
cy.get('[data-cy="ls-continue-button"]').click();
|
||||||
cy.get("[data-cy=\"lc-title\"]").should(
|
cy.get('[data-cy="lc-title"]').should(
|
||||||
"contain",
|
"contain",
|
||||||
"Handlungsfeld «Fahrzeug»"
|
"Handlungsfeld «Fahrzeug»"
|
||||||
);
|
);
|
||||||
|
|
@ -81,43 +81,43 @@ describe("circle.cy.js", () => {
|
||||||
|
|
||||||
it("can open learning content by url", () => {
|
it("can open learning content by url", () => {
|
||||||
cy.visit("/course/test-lehrgang/learn/fahrzeug/handlungsfeld-fahrzeug");
|
cy.visit("/course/test-lehrgang/learn/fahrzeug/handlungsfeld-fahrzeug");
|
||||||
cy.get("[data-cy=\"lc-title\"]").should(
|
cy.get('[data-cy="lc-title"]').should(
|
||||||
"contain",
|
"contain",
|
||||||
"Handlungsfeld «Fahrzeug»"
|
"Handlungsfeld «Fahrzeug»"
|
||||||
);
|
);
|
||||||
|
|
||||||
cy.get("[data-cy=\"close-learning-content\"]").click();
|
cy.get('[data-cy="close-learning-content"]').click();
|
||||||
cy.get("[data-cy=\"circle-title\"]").should("contain", "Fahrzeug");
|
cy.get('[data-cy="circle-title"]').should("contain", "Fahrzeug");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("checks number of sequences and contents", () => {
|
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"]').should("have.length", 3);
|
||||||
cy.get("[data-cy=\"lp-learning-sequence\"]")
|
cy.get('[data-cy="lp-learning-sequence"]')
|
||||||
.first()
|
.first()
|
||||||
.should("contain", "Vorbereitung");
|
.should("contain", "Vorbereitung");
|
||||||
cy.get("[data-cy=\"lp-learning-sequence\"]")
|
cy.get('[data-cy="lp-learning-sequence"]')
|
||||||
.eq(1)
|
.eq(1)
|
||||||
.should("contain", "Training");
|
.should("contain", "Training");
|
||||||
cy.get("[data-cy=\"lp-learning-sequence\"]")
|
cy.get('[data-cy="lp-learning-sequence"]')
|
||||||
.last()
|
.last()
|
||||||
.should("contain", "Transfer");
|
.should("contain", "Transfer");
|
||||||
|
|
||||||
cy.get("[data-cy=\"lp-learning-content\"]").should("have.length", 10);
|
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"]')
|
||||||
.first()
|
.first()
|
||||||
.should("contain", "Verschaffe dir einen Überblick");
|
.should("contain", "Verschaffe dir einen Überblick");
|
||||||
cy.get("[data-cy=\"lp-learning-content\"]")
|
cy.get('[data-cy="lp-learning-content"]')
|
||||||
.eq(4)
|
.eq(4)
|
||||||
.should("contain", "Präsenzkurs Fahrzeug");
|
.should("contain", "Präsenzkurs Fahrzeug");
|
||||||
cy.get("[data-cy=\"lp-learning-content\"]")
|
cy.get('[data-cy="lp-learning-content"]')
|
||||||
.eq(7)
|
.eq(7)
|
||||||
.should("contain", "Reflexion");
|
.should("contain", "Reflexion");
|
||||||
cy.get("[data-cy=\"lp-learning-content\"]")
|
cy.get('[data-cy="lp-learning-content"]')
|
||||||
.last()
|
.last()
|
||||||
.should("contain", "Feedback");
|
.should("contain", "Feedback");
|
||||||
|
|
||||||
cy.visit("/course/test-lehrgang/learn/reisen");
|
cy.visit("/course/test-lehrgang/learn/reisen");
|
||||||
cy.get("[data-cy=\"lp-learning-sequence\"]").should("have.length", 3);
|
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-content"]').should("have.length", 9);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { login } from "../helpers";
|
import {login} from "../helpers";
|
||||||
|
|
||||||
describe("selfEvaluation.cy.js", () => {
|
describe("selfEvaluation.cy.js", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|
@ -28,37 +28,28 @@ describe("selfEvaluation.cy.js", () => {
|
||||||
cy.get('[data-cy="self-evaluation-success"]').should("have.text", "0");
|
cy.get('[data-cy="self-evaluation-success"]').should("have.text", "0");
|
||||||
cy.get('[data-cy="self-evaluation-unknown"]').should("have.text", "4");
|
cy.get('[data-cy="self-evaluation-unknown"]').should("have.text", "4");
|
||||||
|
|
||||||
|
|
||||||
|
// learning unit id = 687 also known as:
|
||||||
|
// Bedarfsanalyse, Ist- und Soll-Situation <<Reisen>>
|
||||||
|
const identifier = "self-eval-687"
|
||||||
|
|
||||||
// data in KompetenzNavi/Selbsteinschätzungen is correct
|
// data in KompetenzNavi/Selbsteinschätzungen is correct
|
||||||
cy.visit("/course/test-lehrgang/competence/criteria");
|
cy.visit("/course/test-lehrgang/competence/self-evaluation-and-feedback");
|
||||||
cy.get('[data-cy="test-lehrgang-lp-circle-reisen-lu-reisen-fail"]').should(
|
cy.get(`[data-cy="${identifier}-fail"]`).should("not.exist");
|
||||||
"have.text",
|
cy.get(`[data-cy="${identifier}-pass"]`).should("not.exist");
|
||||||
"0"
|
cy.get(`[data-cy="${identifier}-unknown"]`).should("have.text", "2");
|
||||||
);
|
|
||||||
cy.get(
|
|
||||||
'[data-cy="test-lehrgang-lp-circle-reisen-lu-reisen-success"]'
|
|
||||||
).should("have.text", "0");
|
|
||||||
cy.get(
|
|
||||||
'[data-cy="test-lehrgang-lp-circle-reisen-lu-reisen-unknown"]'
|
|
||||||
).should("have.text", "2");
|
|
||||||
|
|
||||||
// it can open self evaluation from within KompetenzNavi
|
// it can open self evaluation from within KompetenzNavi
|
||||||
cy.get('[data-cy="test-lehrgang-lp-circle-reisen-lu-reisen-open"]').click();
|
cy.get(`[data-cy="${identifier}-detail-url"]`).click();
|
||||||
|
|
||||||
// starting the self evaluation will return to KompetenzNavi
|
// starting the self evaluation will return to KompetenzNavi
|
||||||
cy.makeSelfEvaluation([true, false]);
|
cy.makeSelfEvaluation([true, false]);
|
||||||
cy.url().should("include", "/course/test-lehrgang/competence/criteria");
|
cy.url().should("include", "/course/test-lehrgang/competence/self-evaluation-and-feedback");
|
||||||
|
|
||||||
// check data again on KompetenzNavi
|
// check data again on KompetenzNavi
|
||||||
cy.get('[data-cy="test-lehrgang-lp-circle-reisen-lu-reisen-fail"]').should(
|
cy.get(`[data-cy="${identifier}-fail"]`).should("have.text", "1");
|
||||||
"have.text",
|
cy.get(`[data-cy="${identifier}-pass"]`).should("have.text", "1");
|
||||||
"1"
|
cy.get(`[data-cy="${identifier}-unknown"]`).should("not.exist");
|
||||||
);
|
|
||||||
cy.get(
|
|
||||||
'[data-cy="test-lehrgang-lp-circle-reisen-lu-reisen-success"]'
|
|
||||||
).should("have.text", "1");
|
|
||||||
cy.get(
|
|
||||||
'[data-cy="test-lehrgang-lp-circle-reisen-lu-reisen-unknown"]'
|
|
||||||
).should("have.text", "0");
|
|
||||||
|
|
||||||
// data in KompetenzNavi/Übersicht is correct
|
// data in KompetenzNavi/Übersicht is correct
|
||||||
cy.visit("/course/test-lehrgang/competence");
|
cy.visit("/course/test-lehrgang/competence");
|
||||||
|
|
@ -76,19 +67,6 @@ describe("selfEvaluation.cy.js", () => {
|
||||||
|
|
||||||
// starting the self evaluation from circle should return to circle
|
// starting the self evaluation from circle should return to circle
|
||||||
cy.url().should("include", "/course/test-lehrgang/learn/reisen");
|
cy.url().should("include", "/course/test-lehrgang/learn/reisen");
|
||||||
|
|
||||||
// data in KompetenzNavi / Selbsteinschätzungen is correct
|
|
||||||
cy.visit("/course/test-lehrgang/competence/criteria");
|
|
||||||
cy.get('[data-cy="test-lehrgang-lp-circle-reisen-lu-reisen-fail"]').should(
|
|
||||||
"have.text",
|
|
||||||
"0"
|
|
||||||
);
|
|
||||||
cy.get(
|
|
||||||
'[data-cy="test-lehrgang-lp-circle-reisen-lu-reisen-success"]'
|
|
||||||
).should("have.text", "2");
|
|
||||||
cy.get(
|
|
||||||
'[data-cy="test-lehrgang-lp-circle-reisen-lu-reisen-unknown"]'
|
|
||||||
).should("have.text", "0");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should be able to make a fail self evaluation", () => {
|
it("should be able to make a fail self evaluation", () => {
|
||||||
|
|
@ -97,19 +75,6 @@ describe("selfEvaluation.cy.js", () => {
|
||||||
cy.get('[data-cy="test-lehrgang-lp-circle-reisen-lu-reisen"]')
|
cy.get('[data-cy="test-lehrgang-lp-circle-reisen-lu-reisen"]')
|
||||||
.find('[data-cy="fail"]')
|
.find('[data-cy="fail"]')
|
||||||
.should("exist");
|
.should("exist");
|
||||||
|
|
||||||
// data in KompetenzNavi / Selbsteinschätzungen is correct
|
|
||||||
cy.visit("/course/test-lehrgang/competence/criteria");
|
|
||||||
cy.get('[data-cy="test-lehrgang-lp-circle-reisen-lu-reisen-fail"]').should(
|
|
||||||
"have.text",
|
|
||||||
"2"
|
|
||||||
);
|
|
||||||
cy.get(
|
|
||||||
'[data-cy="test-lehrgang-lp-circle-reisen-lu-reisen-success"]'
|
|
||||||
).should("have.text", "0");
|
|
||||||
cy.get(
|
|
||||||
'[data-cy="test-lehrgang-lp-circle-reisen-lu-reisen-unknown"]'
|
|
||||||
).should("have.text", "0");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should be able to make a mixed self evaluation", () => {
|
it("should be able to make a mixed self evaluation", () => {
|
||||||
|
|
@ -118,18 +83,5 @@ describe("selfEvaluation.cy.js", () => {
|
||||||
cy.get('[data-cy="test-lehrgang-lp-circle-reisen-lu-reisen"]')
|
cy.get('[data-cy="test-lehrgang-lp-circle-reisen-lu-reisen"]')
|
||||||
.find('[data-cy="fail"]')
|
.find('[data-cy="fail"]')
|
||||||
.should("exist");
|
.should("exist");
|
||||||
|
|
||||||
// data in KompetenzNavi / Selbsteinschätzungen is correct
|
|
||||||
cy.visit("/course/test-lehrgang/competence/criteria");
|
|
||||||
cy.get('[data-cy="test-lehrgang-lp-circle-reisen-lu-reisen-fail"]').should(
|
|
||||||
"have.text",
|
|
||||||
"1"
|
|
||||||
);
|
|
||||||
cy.get(
|
|
||||||
'[data-cy="test-lehrgang-lp-circle-reisen-lu-reisen-success"]'
|
|
||||||
).should("have.text", "1");
|
|
||||||
cy.get(
|
|
||||||
'[data-cy="test-lehrgang-lp-circle-reisen-lu-reisen-unknown"]'
|
|
||||||
).should("have.text", "0");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -133,6 +133,7 @@ LOCAL_APPS = [
|
||||||
"vbv_lernwelt.course_session_group",
|
"vbv_lernwelt.course_session_group",
|
||||||
"vbv_lernwelt.shop",
|
"vbv_lernwelt.shop",
|
||||||
"vbv_lernwelt.learning_mentor",
|
"vbv_lernwelt.learning_mentor",
|
||||||
|
"vbv_lernwelt.self_evaluation_feedback",
|
||||||
]
|
]
|
||||||
# https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
|
# https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
|
||||||
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS
|
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ from vbv_lernwelt.core.views import (
|
||||||
check_rate_limit,
|
check_rate_limit,
|
||||||
cypress_reset_view,
|
cypress_reset_view,
|
||||||
generate_web_component_icons,
|
generate_web_component_icons,
|
||||||
|
iterativ_test_coursesessions_reset_view,
|
||||||
permission_denied_view,
|
permission_denied_view,
|
||||||
rate_limit_exceeded_view,
|
rate_limit_exceeded_view,
|
||||||
vue_home,
|
vue_home,
|
||||||
|
|
@ -141,6 +142,9 @@ urlpatterns = [
|
||||||
|
|
||||||
path("api/mentor/<signed_int:course_session_id>/", include("vbv_lernwelt.learning_mentor.urls")),
|
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
|
# assignment
|
||||||
path(
|
path(
|
||||||
r"api/assignment/<signed_int:assignment_id>/<signed_int:course_session_id>/status/",
|
r"api/assignment/<signed_int:assignment_id>/<signed_int:course_session_id>/status/",
|
||||||
|
|
@ -206,6 +210,13 @@ urlpatterns = [
|
||||||
name="t2l_sync",
|
name="t2l_sync",
|
||||||
),
|
),
|
||||||
|
|
||||||
|
# iterativ Test course sessions
|
||||||
|
path(
|
||||||
|
r"api/core/resetiterativsessions/",
|
||||||
|
iterativ_test_coursesessions_reset_view,
|
||||||
|
name="iterativ_test_coursesessions_reset_view",
|
||||||
|
),
|
||||||
|
|
||||||
path("server/graphql/",
|
path("server/graphql/",
|
||||||
csrf_exempt(GraphQLView.as_view(graphiql=True, schema=schema))),
|
csrf_exempt(GraphQLView.as_view(graphiql=True, schema=schema))),
|
||||||
# testing and debug
|
# testing and debug
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ from rest_framework import status
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
from vbv_lernwelt.core.model_utils import add_countries, add_organisations
|
from vbv_lernwelt.core.model_utils import add_countries, add_organisations
|
||||||
from vbv_lernwelt.core.models import Organisation, User
|
from vbv_lernwelt.core.models import Country, Organisation, User
|
||||||
|
|
||||||
|
|
||||||
class EntitiesViewTest(APITestCase):
|
class EntitiesViewTest(APITestCase):
|
||||||
|
|
@ -15,7 +15,7 @@ class EntitiesViewTest(APITestCase):
|
||||||
add_organisations()
|
add_organisations()
|
||||||
add_countries()
|
add_countries()
|
||||||
|
|
||||||
def test_list_entities(self) -> None:
|
def test_list_organisation_entities(self) -> None:
|
||||||
# It seems that different locales handle ordering differently (especially with lower case letters)
|
# It seems that different locales handle ordering differently (especially with lower case letters)
|
||||||
# As such we delete entries that start with lower case letters
|
# As such we delete entries that start with lower case letters
|
||||||
Organisation.objects.filter(organisation_id__in=[1, 2, 3]).delete()
|
Organisation.objects.filter(organisation_id__in=[1, 2, 3]).delete()
|
||||||
|
|
@ -51,3 +51,49 @@ class EntitiesViewTest(APITestCase):
|
||||||
"name": "Afghanistan",
|
"name": "Afghanistan",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_list_country_entities_ordered_by_country_id(self) -> None:
|
||||||
|
# GIVEN
|
||||||
|
url = reverse("list_entities")
|
||||||
|
|
||||||
|
first_country = Country.objects.get(country_id=1)
|
||||||
|
|
||||||
|
# WHEN
|
||||||
|
response = self.client.get(url)
|
||||||
|
|
||||||
|
# THEN
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
countries = response.data["countries"]
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
countries[0],
|
||||||
|
{
|
||||||
|
"id": first_country.country_id,
|
||||||
|
"name": first_country.name_de,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_list_country_entities_ordered_by_order_id(self) -> None:
|
||||||
|
# GIVEN
|
||||||
|
url = reverse("list_entities")
|
||||||
|
|
||||||
|
switzerland = Country.objects.get(name_de="Schweiz")
|
||||||
|
switzerland.order_id = 1
|
||||||
|
switzerland.save()
|
||||||
|
|
||||||
|
# WHEN
|
||||||
|
response = self.client.get(url)
|
||||||
|
|
||||||
|
# THEN
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
countries = response.data["countries"]
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
countries[0],
|
||||||
|
{
|
||||||
|
"id": switzerland.country_id,
|
||||||
|
"name": switzerland.name_de,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -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?",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -122,6 +122,7 @@ class OrganisationAdmin(admin.ModelAdmin):
|
||||||
@admin.register(Country)
|
@admin.register(Country)
|
||||||
class CountryAdmin(admin.ModelAdmin):
|
class CountryAdmin(admin.ModelAdmin):
|
||||||
list_display = (
|
list_display = (
|
||||||
|
"order_id",
|
||||||
"country_id",
|
"country_id",
|
||||||
"name_de",
|
"name_de",
|
||||||
"name_fr",
|
"name_fr",
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ TEST_STUDENT1_USER_ID = "65c73ad0-6d53-43a9-a4a4-64143f27b03a"
|
||||||
TEST_STUDENT2_USER_ID = "19c40d94-15cc-4198-aaad-ef707c4b0900"
|
TEST_STUDENT2_USER_ID = "19c40d94-15cc-4198-aaad-ef707c4b0900"
|
||||||
TEST_STUDENT3_USER_ID = "bcf94dba-53bc-474b-a22d-e4af39aa042b"
|
TEST_STUDENT3_USER_ID = "bcf94dba-53bc-474b-a22d-e4af39aa042b"
|
||||||
TEST_MENTOR1_USER_ID = "d1f5f5a9-5b0a-4e1a-9e1a-9e9b5b5e1b1b"
|
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_BERN_ID = -1
|
||||||
TEST_COURSE_SESSION_ZURICH_ID = -2
|
TEST_COURSE_SESSION_ZURICH_ID = -2
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ from vbv_lernwelt.core.constants import (
|
||||||
ADMIN_USER_ID,
|
ADMIN_USER_ID,
|
||||||
TEST_MENTOR1_USER_ID,
|
TEST_MENTOR1_USER_ID,
|
||||||
TEST_STUDENT1_USER_ID,
|
TEST_STUDENT1_USER_ID,
|
||||||
|
TEST_STUDENT1_VV_USER_ID,
|
||||||
TEST_STUDENT2_USER_ID,
|
TEST_STUDENT2_USER_ID,
|
||||||
TEST_STUDENT3_USER_ID,
|
TEST_STUDENT3_USER_ID,
|
||||||
TEST_SUPERVISOR1_USER_ID,
|
TEST_SUPERVISOR1_USER_ID,
|
||||||
|
|
@ -210,9 +211,10 @@ def create_default_users(default_password="test", set_avatar=False):
|
||||||
last_name="Expert3",
|
last_name="Expert3",
|
||||||
)
|
)
|
||||||
_create_student_user(
|
_create_student_user(
|
||||||
|
id=TEST_STUDENT1_VV_USER_ID,
|
||||||
email="student-vv@eiger-versicherungen.ch",
|
email="student-vv@eiger-versicherungen.ch",
|
||||||
first_name="Student",
|
first_name="Viktor",
|
||||||
last_name="VV",
|
last_name="Vollgas",
|
||||||
)
|
)
|
||||||
_create_student_user(
|
_create_student_user(
|
||||||
email="patrizia.huggel@eiger-versicherungen.ch",
|
email="patrizia.huggel@eiger-versicherungen.ch",
|
||||||
|
|
@ -364,10 +366,11 @@ def create_default_users(default_password="test", set_avatar=False):
|
||||||
_create_user(
|
_create_user(
|
||||||
_id=TEST_MENTOR1_USER_ID,
|
_id=TEST_MENTOR1_USER_ID,
|
||||||
email="test-mentor1@example.com",
|
email="test-mentor1@example.com",
|
||||||
first_name="[Mentor]",
|
first_name="Micheala",
|
||||||
last_name="Mentor",
|
last_name="Weber-Mentor",
|
||||||
password=default_password,
|
password=default_password,
|
||||||
language="de",
|
language="de",
|
||||||
|
avatar_image="uk1.patrizia.huggel.jpg",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,16 @@ from vbv_lernwelt.core.constants import (
|
||||||
TEST_COURSE_SESSION_BERN_ID,
|
TEST_COURSE_SESSION_BERN_ID,
|
||||||
TEST_MENTOR1_USER_ID,
|
TEST_MENTOR1_USER_ID,
|
||||||
TEST_STUDENT1_USER_ID,
|
TEST_STUDENT1_USER_ID,
|
||||||
|
TEST_STUDENT1_VV_USER_ID,
|
||||||
TEST_STUDENT2_USER_ID,
|
TEST_STUDENT2_USER_ID,
|
||||||
TEST_STUDENT3_USER_ID,
|
TEST_STUDENT3_USER_ID,
|
||||||
TEST_TRAINER1_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.consts import (
|
||||||
|
COURSE_TEST_ID,
|
||||||
|
COURSE_VERSICHERUNGSVERMITTLERIN_ID,
|
||||||
|
)
|
||||||
from vbv_lernwelt.course.creators.test_course import (
|
from vbv_lernwelt.course.creators.test_course import (
|
||||||
create_edoniq_test_result_data,
|
create_edoniq_test_result_data,
|
||||||
create_feedback_response_data,
|
create_feedback_response_data,
|
||||||
|
|
@ -39,6 +43,10 @@ from vbv_lernwelt.learnpath.models import (
|
||||||
LearningContentFeedbackVV,
|
LearningContentFeedbackVV,
|
||||||
)
|
)
|
||||||
from vbv_lernwelt.notify.models import Notification
|
from vbv_lernwelt.notify.models import Notification
|
||||||
|
from vbv_lernwelt.self_evaluation_feedback.models import (
|
||||||
|
CourseCompletionFeedback,
|
||||||
|
SelfEvaluationFeedback,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@click.command()
|
@click.command()
|
||||||
|
|
@ -107,7 +115,11 @@ def command(
|
||||||
FeedbackResponse.objects.all().delete()
|
FeedbackResponse.objects.all().delete()
|
||||||
CourseSessionAttendanceCourse.objects.all().update(attendance_user_list=[])
|
CourseSessionAttendanceCourse.objects.all().update(attendance_user_list=[])
|
||||||
|
|
||||||
|
SelfEvaluationFeedback.objects.all().delete()
|
||||||
|
CourseCompletionFeedback.objects.all().delete()
|
||||||
|
|
||||||
LearningMentor.objects.all().delete()
|
LearningMentor.objects.all().delete()
|
||||||
|
User.objects.all().update(organisation=Organisation.objects.first())
|
||||||
User.objects.all().update(language="de")
|
User.objects.all().update(language="de")
|
||||||
User.objects.all().update(additional_json_data={})
|
User.objects.all().update(additional_json_data={})
|
||||||
|
|
||||||
|
|
@ -331,16 +343,30 @@ def command(
|
||||||
attendance_course.save()
|
attendance_course.save()
|
||||||
|
|
||||||
if create_learning_mentor:
|
if create_learning_mentor:
|
||||||
print("Create learning mentor")
|
uk_mentor = LearningMentor.objects.create(
|
||||||
mentor = LearningMentor.objects.create(
|
|
||||||
course=Course.objects.get(id=COURSE_TEST_ID),
|
course=Course.objects.get(id=COURSE_TEST_ID),
|
||||||
mentor=User.objects.get(id=TEST_MENTOR1_USER_ID),
|
mentor=User.objects.get(id=TEST_MENTOR1_USER_ID),
|
||||||
)
|
)
|
||||||
course_session = CourseSession.objects.get(id=TEST_COURSE_SESSION_BERN_ID)
|
uk_mentor.participants.add(
|
||||||
csu = CourseSessionUser.objects.get(
|
CourseSessionUser.objects.get(
|
||||||
user__id=TEST_STUDENT1_USER_ID, course_session=course_session
|
user__id=TEST_STUDENT1_USER_ID,
|
||||||
|
course_session=CourseSession.objects.get(
|
||||||
|
id=TEST_COURSE_SESSION_BERN_ID
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
)
|
)
|
||||||
mentor.participants.add(csu)
|
|
||||||
|
|
||||||
course = Course.objects.get(id=COURSE_TEST_ID)
|
course = Course.objects.get(id=COURSE_TEST_ID)
|
||||||
course.enable_circle_documents = enable_circle_documents
|
course.enable_circle_documents = enable_circle_documents
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,336 @@
|
||||||
|
from datetime import datetime, time, timedelta
|
||||||
|
|
||||||
|
import djclick as click
|
||||||
|
import structlog
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from vbv_lernwelt.assignment.models import Assignment, AssignmentCompletion
|
||||||
|
from vbv_lernwelt.core.admin import User
|
||||||
|
from vbv_lernwelt.course.consts import (
|
||||||
|
COURSE_VERSICHERUNGSVERMITTLERIN_FR_ID,
|
||||||
|
COURSE_VERSICHERUNGSVERMITTLERIN_ID,
|
||||||
|
COURSE_VERSICHERUNGSVERMITTLERIN_IT_ID,
|
||||||
|
)
|
||||||
|
from vbv_lernwelt.course.models import (
|
||||||
|
Course,
|
||||||
|
CourseCompletion,
|
||||||
|
CourseSession,
|
||||||
|
CourseSessionUser,
|
||||||
|
)
|
||||||
|
from vbv_lernwelt.course_session.models import (
|
||||||
|
CourseSessionAssignment,
|
||||||
|
CourseSessionAttendanceCourse,
|
||||||
|
CourseSessionEdoniqTest,
|
||||||
|
)
|
||||||
|
from vbv_lernwelt.course_session_group.models import CourseSessionGroup
|
||||||
|
from vbv_lernwelt.feedback.models import FeedbackResponse
|
||||||
|
from vbv_lernwelt.learning_mentor.models import LearningMentor
|
||||||
|
from vbv_lernwelt.learnpath.models import Circle
|
||||||
|
from vbv_lernwelt.notify.models import Notification
|
||||||
|
|
||||||
|
logger = structlog.get_logger(__name__)
|
||||||
|
from vbv_lernwelt.importer.services import (
|
||||||
|
create_or_update_course_session,
|
||||||
|
get_uk_course,
|
||||||
|
LP_DATA,
|
||||||
|
TRANSLATIONS,
|
||||||
|
)
|
||||||
|
|
||||||
|
IT_VV_TEST_COURSE = "Iterativ VV Testkurs"
|
||||||
|
IT_UK_TEST_COURSE = "Iterativ üK Testkurs"
|
||||||
|
IT_UK_TEST_REGION = "Iterativ Region"
|
||||||
|
TIME_FORMAT = "%d.%m.%Y, %H:%M"
|
||||||
|
PASSWORD = "KqaDm3-x8zhCKHLWDV_oiqFrYWHg"
|
||||||
|
|
||||||
|
logger = structlog.get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@click.command()
|
||||||
|
def command():
|
||||||
|
create_or_update_uk()
|
||||||
|
create_or_update_vv()
|
||||||
|
|
||||||
|
|
||||||
|
def create_or_update_uk(language="de"):
|
||||||
|
uk_course = get_uk_course(language)
|
||||||
|
uk_circle_keys = [
|
||||||
|
"Kickoff",
|
||||||
|
"Basis",
|
||||||
|
"Fahrzeug",
|
||||||
|
"Haushalt Teil 1",
|
||||||
|
"Haushalt Teil 2",
|
||||||
|
]
|
||||||
|
|
||||||
|
data = create_uk_data(language)
|
||||||
|
create_or_update_course_session(
|
||||||
|
uk_course,
|
||||||
|
data,
|
||||||
|
language,
|
||||||
|
circle_keys=uk_circle_keys,
|
||||||
|
)
|
||||||
|
cs = CourseSession.objects.get(import_id=data["ID"])
|
||||||
|
|
||||||
|
members, trainer, regionenleiter = get_or_create_users_uk()
|
||||||
|
delete_cs_data(cs, members + [trainer, regionenleiter])
|
||||||
|
|
||||||
|
add_to_course_session(cs, members)
|
||||||
|
add_trainers_to_course_session(cs, [trainer], uk_circle_keys, language)
|
||||||
|
create_and_add_to_cs_group(cs.course, IT_UK_TEST_REGION, [cs], regionenleiter)
|
||||||
|
|
||||||
|
|
||||||
|
def create_or_update_vv(language="de"):
|
||||||
|
vv_course = get_vv_course(language)
|
||||||
|
|
||||||
|
cs, _created = CourseSession.objects.get_or_create(
|
||||||
|
course=vv_course, import_id=IT_VV_TEST_COURSE
|
||||||
|
)
|
||||||
|
cs.title = IT_VV_TEST_COURSE
|
||||||
|
cs.save()
|
||||||
|
|
||||||
|
create_or_update_assignment_course_session(cs)
|
||||||
|
members, member_with_mentor, mentor = get_or_create_users_vv()
|
||||||
|
delete_cs_data(cs, members + [member_with_mentor, mentor])
|
||||||
|
|
||||||
|
add_to_course_session(cs, members + [member_with_mentor])
|
||||||
|
add_mentor_to_course_session(cs, [(mentor, member_with_mentor)])
|
||||||
|
|
||||||
|
|
||||||
|
def delete_cs_data(cs: CourseSession, users: list[User]):
|
||||||
|
if cs:
|
||||||
|
CourseCompletion.objects.filter(course_session=cs).delete()
|
||||||
|
Notification.objects.filter(course_session=cs).delete()
|
||||||
|
AssignmentCompletion.objects.filter(course_session=cs).delete()
|
||||||
|
CourseSessionAttendanceCourse.objects.filter(course_session=cs).update(
|
||||||
|
attendance_user_list=[]
|
||||||
|
)
|
||||||
|
CourseSessionEdoniqTest.objects.filter(course_session=cs).delete()
|
||||||
|
CourseSessionUser.objects.filter(course_session=cs).delete()
|
||||||
|
learning_mentor_ids = (
|
||||||
|
LearningMentor.objects.filter(participants__course_session=cs)
|
||||||
|
.values_list("id", flat=True)
|
||||||
|
.distinct()
|
||||||
|
| LearningMentor.objects.filter(mentor__in=users)
|
||||||
|
.values_list("id", flat=True)
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
# cannot call delete on distinct objects
|
||||||
|
LearningMentor.objects.filter(id__in=list(learning_mentor_ids)).delete()
|
||||||
|
else:
|
||||||
|
logger.info("no_course_session_found", import_id=cs.import_id)
|
||||||
|
|
||||||
|
FeedbackResponse.objects.filter(feedback_user__in=users).delete()
|
||||||
|
|
||||||
|
|
||||||
|
def add_to_course_session(
|
||||||
|
course_session: CourseSession,
|
||||||
|
members: list[User],
|
||||||
|
role=CourseSessionUser.Role.MEMBER,
|
||||||
|
):
|
||||||
|
if course_session:
|
||||||
|
for user in members:
|
||||||
|
csu, _created = CourseSessionUser.objects.get_or_create(
|
||||||
|
course_session_id=course_session.id, user_id=user.id, role=role
|
||||||
|
)
|
||||||
|
csu.save()
|
||||||
|
|
||||||
|
|
||||||
|
def add_mentor_to_course_session(
|
||||||
|
course_session: CourseSession, mentor_mentee_pairs: list[tuple[User, User]]
|
||||||
|
):
|
||||||
|
for mentor, mentee in mentor_mentee_pairs:
|
||||||
|
lm = LearningMentor.objects.create(
|
||||||
|
course=course_session.course,
|
||||||
|
mentor=mentor,
|
||||||
|
)
|
||||||
|
lm.participants.add(
|
||||||
|
CourseSessionUser.objects.get(
|
||||||
|
user__id=mentee.id,
|
||||||
|
course_session=course_session,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def add_trainers_to_course_session(
|
||||||
|
course_session: CourseSession,
|
||||||
|
trainers: list[User],
|
||||||
|
circle_keys: list[str],
|
||||||
|
language,
|
||||||
|
):
|
||||||
|
add_to_course_session(course_session, trainers, CourseSessionUser.Role.EXPERT)
|
||||||
|
for user in trainers:
|
||||||
|
for circle_key in circle_keys:
|
||||||
|
circle_name = LP_DATA[circle_key][language]["title"]
|
||||||
|
circle = Circle.objects.filter(
|
||||||
|
slug=f"{course_session.course.slug}-lp-circle-{circle_name.lower()}"
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if course_session and circle:
|
||||||
|
csu = CourseSessionUser.objects.filter(
|
||||||
|
course_session_id=course_session.id, user_id=user.id
|
||||||
|
).first()
|
||||||
|
if csu:
|
||||||
|
csu.expert.add(circle)
|
||||||
|
csu.save()
|
||||||
|
|
||||||
|
|
||||||
|
def get_or_create_users_uk():
|
||||||
|
members = [
|
||||||
|
_create_or_update_user(
|
||||||
|
f"teilnehmer{n}.uk@iterativ.ch", "Teilnehmer üK", "Iterativ", PASSWORD, "de"
|
||||||
|
)
|
||||||
|
for n in range(1, 10)
|
||||||
|
]
|
||||||
|
trainer = _create_or_update_user(
|
||||||
|
"trainer1.uk@iterativ.ch", "Trainer üK", "Iterativ", PASSWORD, "de"
|
||||||
|
)
|
||||||
|
regionenleiter = _create_or_update_user(
|
||||||
|
"regionenleiter1.uk@iterativ.ch",
|
||||||
|
"Regionenleiter üK",
|
||||||
|
"Iterativ",
|
||||||
|
PASSWORD,
|
||||||
|
"de",
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
members,
|
||||||
|
trainer,
|
||||||
|
regionenleiter,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_or_create_users_vv():
|
||||||
|
members = [
|
||||||
|
_create_or_update_user(
|
||||||
|
f"teilnehmer{n}.vv@iterativ.ch", "Teilnehmer VV", "Iterativ", PASSWORD, "de"
|
||||||
|
)
|
||||||
|
for n in range(1, 10)
|
||||||
|
]
|
||||||
|
member_with_mentor = _create_or_update_user(
|
||||||
|
"teilnehmer1.vv.lb@iterativ.ch",
|
||||||
|
"Teilnehmer VV mit LB",
|
||||||
|
"Iterativ",
|
||||||
|
PASSWORD,
|
||||||
|
"de",
|
||||||
|
)
|
||||||
|
mentor = _create_or_update_user(
|
||||||
|
"lernbegleitung1.vv@iterativ.ch",
|
||||||
|
"Lernbegleitung VV",
|
||||||
|
"Iterativ",
|
||||||
|
PASSWORD,
|
||||||
|
"de",
|
||||||
|
)
|
||||||
|
return members, member_with_mentor, mentor
|
||||||
|
|
||||||
|
|
||||||
|
def _create_or_update_user(email, first_name, last_name, password, language):
|
||||||
|
try:
|
||||||
|
user = User.objects.get(email=email)
|
||||||
|
except User.DoesNotExist:
|
||||||
|
user = User(
|
||||||
|
email=email,
|
||||||
|
username=email,
|
||||||
|
)
|
||||||
|
|
||||||
|
user.email = email
|
||||||
|
user.first_name = first_name or user.first_name
|
||||||
|
user.last_name = last_name or user.last_name
|
||||||
|
user.username = email
|
||||||
|
user.language = language
|
||||||
|
user.set_password(password)
|
||||||
|
user.save()
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
def create_uk_data(language):
|
||||||
|
return {
|
||||||
|
"Klasse": IT_UK_TEST_COURSE,
|
||||||
|
"ID": IT_UK_TEST_COURSE,
|
||||||
|
"Generation": 2024,
|
||||||
|
"Region": "Bern",
|
||||||
|
"Sprache": language,
|
||||||
|
f"Kickoff {TRANSLATIONS[language]['start']}": timezone.make_aware(
|
||||||
|
datetime.combine((timezone.now() + timedelta(weeks=2)).date(), time(9, 0))
|
||||||
|
).strftime("%d.%m.%Y, %H:%M"),
|
||||||
|
f"Kickoff {TRANSLATIONS[language]['ende']}": timezone.make_aware(
|
||||||
|
datetime.combine((timezone.now() + timedelta(weeks=2)).date(), time(16, 0))
|
||||||
|
).strftime("%d.%m.%Y, %H:%M"),
|
||||||
|
f"Kickoff {TRANSLATIONS[language]['raum']}": "Raum 1",
|
||||||
|
f"Kickoff {TRANSLATIONS[language]['standort']}": "Bern",
|
||||||
|
f"Kickoff {TRANSLATIONS[language]['adresse']}": "Musterstrasse 1",
|
||||||
|
f"Basis {TRANSLATIONS[language]['start']}": timezone.make_aware(
|
||||||
|
datetime.combine((timezone.now() + timedelta(weeks=4)).date(), time(9, 0))
|
||||||
|
).strftime("%d.%m.%Y, %H:%M"),
|
||||||
|
f"Basis {TRANSLATIONS[language]['ende']}": timezone.make_aware(
|
||||||
|
datetime.combine((timezone.now() + timedelta(weeks=4)).date(), time(16, 0))
|
||||||
|
).strftime("%d.%m.%Y, %H:%M"),
|
||||||
|
f"Basis {TRANSLATIONS[language]['raum']}": "Raum 1",
|
||||||
|
f"Basis {TRANSLATIONS[language]['standort']}": "Bern",
|
||||||
|
f"Basis {TRANSLATIONS[language]['adresse']}": "Musterstrasse 1",
|
||||||
|
f"Fahrzeug {TRANSLATIONS[language]['start']}": timezone.make_aware(
|
||||||
|
datetime.combine((timezone.now() + timedelta(weeks=6)).date(), time(9, 0))
|
||||||
|
).strftime("%d.%m.%Y, %H:%M"),
|
||||||
|
f"Fahrzeug {TRANSLATIONS[language]['ende']}": timezone.make_aware(
|
||||||
|
datetime.combine((timezone.now() + timedelta(weeks=6)).date(), time(16, 0))
|
||||||
|
).strftime("%d.%m.%Y, %H:%M"),
|
||||||
|
f"Fahrzeug {TRANSLATIONS[language]['raum']}": "Raum 1",
|
||||||
|
f"Fahrzeug {TRANSLATIONS[language]['standort']}": "Bern",
|
||||||
|
f"Fahrzeug {TRANSLATIONS[language]['adresse']}": "Musterstrasse 1",
|
||||||
|
f"Haushalt Teil 1 {TRANSLATIONS[language]['start']}": timezone.make_aware(
|
||||||
|
datetime.combine((timezone.now() + timedelta(weeks=8)).date(), time(9, 0))
|
||||||
|
).strftime("%d.%m.%Y, %H:%M"),
|
||||||
|
f"Haushalt Teil 1 {TRANSLATIONS[language]['ende']}": timezone.make_aware(
|
||||||
|
datetime.combine((timezone.now() + timedelta(weeks=8)).date(), time(16, 0))
|
||||||
|
).strftime("%d.%m.%Y, %H:%M"),
|
||||||
|
f"Haushalt Teil 1 {TRANSLATIONS[language]['raum']}": "Raum 1",
|
||||||
|
f"Haushalt Teil 1 {TRANSLATIONS[language]['standort']}": "Bern",
|
||||||
|
f"Haushalt Teil 1 {TRANSLATIONS[language]['adresse']}": "Musterstrasse 1",
|
||||||
|
f"Haushalt Teil 2 {TRANSLATIONS[language]['start']}": timezone.make_aware(
|
||||||
|
datetime.combine((timezone.now() + timedelta(weeks=10)).date(), time(9, 0))
|
||||||
|
).strftime("%d.%m.%Y, %H:%M"),
|
||||||
|
f"Haushalt Teil 2 {TRANSLATIONS[language]['ende']}": timezone.make_aware(
|
||||||
|
datetime.combine((timezone.now() + timedelta(weeks=10)).date(), time(16, 0))
|
||||||
|
).strftime("%d.%m.%Y, %H:%M"),
|
||||||
|
f"Haushalt Teil 2 {TRANSLATIONS[language]['raum']}": "Raum 1",
|
||||||
|
f"Haushalt Teil 2 {TRANSLATIONS[language]['standort']}": "Bern",
|
||||||
|
f"Haushalt Teil 2 {TRANSLATIONS[language]['adresse']}": "Musterstrasse 1",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def create_and_add_to_cs_group(
|
||||||
|
course: Course, name: str, course_sessions: list[CourseSession], supervisor: User
|
||||||
|
):
|
||||||
|
region, _ = CourseSessionGroup.objects.get_or_create(
|
||||||
|
name=name,
|
||||||
|
course=course,
|
||||||
|
)
|
||||||
|
|
||||||
|
for cs in course_sessions:
|
||||||
|
region.course_session.add(cs)
|
||||||
|
|
||||||
|
region.supervisor.add(supervisor)
|
||||||
|
|
||||||
|
|
||||||
|
def get_vv_course(language: str) -> Course:
|
||||||
|
if language == "fr":
|
||||||
|
course_id = COURSE_VERSICHERUNGSVERMITTLERIN_FR_ID
|
||||||
|
elif language == "it":
|
||||||
|
course_id = COURSE_VERSICHERUNGSVERMITTLERIN_IT_ID
|
||||||
|
else:
|
||||||
|
course_id = COURSE_VERSICHERUNGSVERMITTLERIN_ID
|
||||||
|
|
||||||
|
return Course.objects.get(id=course_id)
|
||||||
|
|
||||||
|
|
||||||
|
def create_or_update_assignment_course_session(cs: CourseSession):
|
||||||
|
# not nice but works for now
|
||||||
|
for assignment in Assignment.objects.all():
|
||||||
|
if assignment.get_course().id == cs.course.id:
|
||||||
|
logger.debug(
|
||||||
|
"create_course_session_assigments",
|
||||||
|
assignment=assignment,
|
||||||
|
label="reset_test_courses",
|
||||||
|
)
|
||||||
|
for lca in assignment.learningcontentassignment_set.all():
|
||||||
|
_csa, _created = CourseSessionAssignment.objects.get_or_create(
|
||||||
|
course_session=cs,
|
||||||
|
learning_content=lca,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
# Generated by Django 3.2.20 on 2024-02-20 09:58
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("core", "0006_auto_20240125_0915"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="country",
|
||||||
|
options={
|
||||||
|
"ordering": ["order_id", "country_id"],
|
||||||
|
"verbose_name": "Country",
|
||||||
|
"verbose_name_plural": "Countries",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="country",
|
||||||
|
name="order_id",
|
||||||
|
field=models.FloatField(default=20),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -26,6 +26,7 @@ class Country(models.Model):
|
||||||
name_de = models.CharField(max_length=255)
|
name_de = models.CharField(max_length=255)
|
||||||
name_fr = models.CharField(max_length=255)
|
name_fr = models.CharField(max_length=255)
|
||||||
name_it = models.CharField(max_length=255)
|
name_it = models.CharField(max_length=255)
|
||||||
|
order_id = models.FloatField(default=20)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.name_de} ({self.country_id})"
|
return f"{self.name_de} ({self.country_id})"
|
||||||
|
|
@ -33,7 +34,7 @@ class Country(models.Model):
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Country"
|
verbose_name = "Country"
|
||||||
verbose_name_plural = "Countries"
|
verbose_name_plural = "Countries"
|
||||||
ordering = ["country_id"]
|
ordering = ["order_id", "country_id"]
|
||||||
|
|
||||||
|
|
||||||
class User(AbstractUser):
|
class User(AbstractUser):
|
||||||
|
|
|
||||||
|
|
@ -173,6 +173,10 @@ def cypress_reset_view(request):
|
||||||
request.data.get("create_attendance_days") == "true"
|
request.data.get("create_attendance_days") == "true"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
options["create_learning_mentor"] = (
|
||||||
|
request.data.get("create_learning_mentor") == "true"
|
||||||
|
)
|
||||||
|
|
||||||
call_command(
|
call_command(
|
||||||
"cypress_reset",
|
"cypress_reset",
|
||||||
**options,
|
**options,
|
||||||
|
|
@ -181,6 +185,17 @@ def cypress_reset_view(request):
|
||||||
return HttpResponseRedirect("/server/admin/")
|
return HttpResponseRedirect("/server/admin/")
|
||||||
|
|
||||||
|
|
||||||
|
@api_view(["POST"])
|
||||||
|
@authentication_classes((authentication.SessionAuthentication,))
|
||||||
|
@permission_classes((IsAdminUser,))
|
||||||
|
def iterativ_test_coursesessions_reset_view(request):
|
||||||
|
call_command(
|
||||||
|
"reset_iterativ_test_sessions",
|
||||||
|
)
|
||||||
|
|
||||||
|
return HttpResponseRedirect("/server/admin/")
|
||||||
|
|
||||||
|
|
||||||
@django_view_authentication_exempt
|
@django_view_authentication_exempt
|
||||||
def generate_web_component_icons(request):
|
def generate_web_component_icons(request):
|
||||||
svg_files = []
|
svg_files = []
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,8 @@ from vbv_lernwelt.learnpath.models import (
|
||||||
LearningContentAssignment,
|
LearningContentAssignment,
|
||||||
LearningContentEdoniqTest,
|
LearningContentEdoniqTest,
|
||||||
LearningPath,
|
LearningPath,
|
||||||
|
LearningUnit,
|
||||||
|
LearningUnitPerformanceFeedbackType,
|
||||||
)
|
)
|
||||||
from vbv_lernwelt.learnpath.tests.learning_path_factories import (
|
from vbv_lernwelt.learnpath.tests.learning_path_factories import (
|
||||||
CircleFactory,
|
CircleFactory,
|
||||||
|
|
@ -58,11 +60,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 = Course.objects.create(id=_id, title=title, category_name="Handlungsfeld")
|
||||||
|
|
||||||
course_page = CoursePageFactory(
|
course_page = CoursePageFactory(
|
||||||
title="Test Lehrgang",
|
title=course_page_title,
|
||||||
parent=get_wagtail_default_site().root_page,
|
parent=get_wagtail_default_site().root_page,
|
||||||
course=course,
|
course=course,
|
||||||
)
|
)
|
||||||
|
|
@ -268,10 +272,30 @@ def create_course_session_edoniq_test(
|
||||||
return cset
|
return cset
|
||||||
|
|
||||||
|
|
||||||
|
def create_learning_unit(
|
||||||
|
circle: Circle,
|
||||||
|
course: Course,
|
||||||
|
course_category_title: str = "Course Category",
|
||||||
|
feedback_user: LearningUnitPerformanceFeedbackType = LearningUnitPerformanceFeedbackType.NO_FEEDBACK,
|
||||||
|
) -> LearningUnit:
|
||||||
|
cat, _ = CourseCategory.objects.get_or_create(
|
||||||
|
course=course,
|
||||||
|
title=course_category_title,
|
||||||
|
)
|
||||||
|
|
||||||
|
return LearningUnitFactory(
|
||||||
|
title="Learning Unit",
|
||||||
|
parent=circle,
|
||||||
|
course_category=cat,
|
||||||
|
feedback_user=feedback_user.value,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def create_performance_criteria_page(
|
def create_performance_criteria_page(
|
||||||
course: Course,
|
course: Course,
|
||||||
course_page: CoursePage,
|
course_page: CoursePage,
|
||||||
circle: Circle,
|
circle: Circle,
|
||||||
|
learning_unit: LearningUnitFactory | LearningUnit | None = None,
|
||||||
) -> PerformanceCriteria:
|
) -> PerformanceCriteria:
|
||||||
competence_navi_page = CompetenceNaviPageFactory(
|
competence_navi_page = CompetenceNaviPageFactory(
|
||||||
title="Competence Navi",
|
title="Competence Navi",
|
||||||
|
|
@ -290,17 +314,14 @@ def create_performance_criteria_page(
|
||||||
items=[("item", "Action Competence Item")],
|
items=[("item", "Action Competence Item")],
|
||||||
)
|
)
|
||||||
|
|
||||||
cat, _ = CourseCategory.objects.get_or_create(
|
if not learning_unit:
|
||||||
course=course, title="Course Category"
|
learning_unit = create_learning_unit(circle=circle, course=course)
|
||||||
)
|
|
||||||
|
|
||||||
lu = LearningUnitFactory(title="Learning Unit", parent=circle, course_category=cat)
|
|
||||||
|
|
||||||
return PerformanceCriteriaFactory(
|
return PerformanceCriteriaFactory(
|
||||||
parent=action_competence,
|
parent=action_competence,
|
||||||
competence_id="X1.1",
|
competence_id="X1.1",
|
||||||
title="Performance Criteria",
|
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 = [
|
dependencies = [
|
||||||
("duedate", "0004_alter_duedate_start"),
|
("duedate", "0004_alter_duedate_start"),
|
||||||
("learnpath", "0008_add_edoniq_sequence_id"),
|
("learnpath", "0008_add_edoniq_sequence_id"),
|
||||||
|
("course_session", "0005_auto_20230825_1723"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
|
|
||||||
|
|
@ -209,10 +209,16 @@ def can_view_course_completions(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def can_complete_learning_content(user: User, course_session_id: int) -> bool:
|
||||||
|
return is_course_session_member(
|
||||||
|
user, course_session_id
|
||||||
|
) or is_course_session_expert(user, course_session_id)
|
||||||
|
|
||||||
|
|
||||||
def course_session_permissions(user: User, course_session_id: int) -> list[str]:
|
def course_session_permissions(user: User, course_session_id: int) -> list[str]:
|
||||||
return _action_list(
|
return _action_list(
|
||||||
{
|
{
|
||||||
"complete-learning-content": is_course_session_member(
|
"complete-learning-content": can_complete_learning_content(
|
||||||
user, course_session_id
|
user, course_session_id
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,12 +33,21 @@ class ActionTestCase(TestCase):
|
||||||
role=CourseSessionUser.Role.MEMBER,
|
role=CourseSessionUser.Role.MEMBER,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
trainer = create_user("trainer")
|
||||||
|
add_course_session_user(
|
||||||
|
self.course_session,
|
||||||
|
trainer,
|
||||||
|
role=CourseSessionUser.Role.EXPERT,
|
||||||
|
)
|
||||||
|
|
||||||
# WHEN
|
# WHEN
|
||||||
mentor_actions = course_session_permissions(lm, self.course_session.id)
|
mentor_actions = course_session_permissions(lm, self.course_session.id)
|
||||||
participant_actions = course_session_permissions(
|
participant_actions = course_session_permissions(
|
||||||
participant, self.course_session.id
|
participant, self.course_session.id
|
||||||
)
|
)
|
||||||
|
trainer_actions = course_session_permissions(trainer, self.course_session.id)
|
||||||
|
|
||||||
# THEN
|
# THEN
|
||||||
self.assertEqual(len(mentor_actions), 0)
|
self.assertEqual(len(mentor_actions), 0)
|
||||||
self.assertEqual(participant_actions, ["complete-learning-content"])
|
self.assertEqual(participant_actions, ["complete-learning-content"])
|
||||||
|
self.assertEqual(trainer_actions, ["complete-learning-content"])
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,10 @@ from vbv_lernwelt.core.models import User
|
||||||
from vbv_lernwelt.course.models import CourseSession
|
from vbv_lernwelt.course.models import CourseSession
|
||||||
from vbv_lernwelt.course_session.models import CourseSessionAssignment
|
from vbv_lernwelt.course_session.models import CourseSessionAssignment
|
||||||
from vbv_lernwelt.learning_mentor.entities import (
|
from vbv_lernwelt.learning_mentor.entities import (
|
||||||
CompletionStatus,
|
MentorAssignmentCompletion,
|
||||||
PraxisAssignmentCompletion,
|
MentorAssignmentStatus,
|
||||||
PraxisAssignmentStatus,
|
MentorAssignmentStatusType,
|
||||||
|
MentorCompletionStatus,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -21,7 +22,7 @@ def get_assignment_completions(
|
||||||
assignment: Assignment,
|
assignment: Assignment,
|
||||||
participants: List[User],
|
participants: List[User],
|
||||||
evaluation_user: User,
|
evaluation_user: User,
|
||||||
) -> List[PraxisAssignmentCompletion]:
|
) -> List[MentorAssignmentCompletion]:
|
||||||
evaluation_results = AssignmentCompletion.objects.filter(
|
evaluation_results = AssignmentCompletion.objects.filter(
|
||||||
assignment_user__in=participants,
|
assignment_user__in=participants,
|
||||||
course_session=course_session,
|
course_session=course_session,
|
||||||
|
|
@ -34,14 +35,14 @@ def get_assignment_completions(
|
||||||
completion_status = result["completion_status"]
|
completion_status = result["completion_status"]
|
||||||
|
|
||||||
if completion_status == AssignmentCompletionStatus.EVALUATION_SUBMITTED.value:
|
if completion_status == AssignmentCompletionStatus.EVALUATION_SUBMITTED.value:
|
||||||
status = CompletionStatus.EVALUATED
|
status = MentorCompletionStatus.EVALUATED
|
||||||
elif completion_status in [
|
elif completion_status in [
|
||||||
AssignmentCompletionStatus.SUBMITTED.value,
|
AssignmentCompletionStatus.SUBMITTED.value,
|
||||||
AssignmentCompletionStatus.EVALUATION_IN_PROGRESS.value,
|
AssignmentCompletionStatus.EVALUATION_IN_PROGRESS.value,
|
||||||
]:
|
]:
|
||||||
status = CompletionStatus.SUBMITTED
|
status = MentorCompletionStatus.SUBMITTED
|
||||||
else:
|
else:
|
||||||
status = CompletionStatus.UNKNOWN
|
status = MentorCompletionStatus.UNKNOWN
|
||||||
|
|
||||||
user_status_map[result["assignment_user"]] = (
|
user_status_map[result["assignment_user"]] = (
|
||||||
status,
|
status,
|
||||||
|
|
@ -49,25 +50,25 @@ def get_assignment_completions(
|
||||||
)
|
)
|
||||||
|
|
||||||
status_priority = {
|
status_priority = {
|
||||||
CompletionStatus.SUBMITTED: 1,
|
MentorCompletionStatus.SUBMITTED: 1,
|
||||||
CompletionStatus.EVALUATED: 2,
|
MentorCompletionStatus.EVALUATED: 2,
|
||||||
CompletionStatus.UNKNOWN: 3,
|
MentorCompletionStatus.UNKNOWN: 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
sorted_participants = sorted(
|
sorted_participants = sorted(
|
||||||
participants,
|
participants,
|
||||||
key=lambda u: (
|
key=lambda u: (
|
||||||
status_priority.get(
|
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],
|
user_status_map.get(u.id, ("", u.last_name))[1],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
return [
|
return [
|
||||||
PraxisAssignmentCompletion(
|
MentorAssignmentCompletion(
|
||||||
status=user_status_map.get(
|
status=user_status_map.get(
|
||||||
user.id, (CompletionStatus.UNKNOWN, user.last_name)
|
user.id, (MentorCompletionStatus.UNKNOWN, user.last_name)
|
||||||
)[0],
|
)[0],
|
||||||
user_id=user.id,
|
user_id=user.id,
|
||||||
last_name=user.last_name,
|
last_name=user.last_name,
|
||||||
|
|
@ -79,7 +80,7 @@ def get_assignment_completions(
|
||||||
|
|
||||||
def get_praxis_assignments(
|
def get_praxis_assignments(
|
||||||
course_session: CourseSession, participants: List[User], evaluation_user: User
|
course_session: CourseSession, participants: List[User], evaluation_user: User
|
||||||
) -> Tuple[List[PraxisAssignmentStatus], Set[int]]:
|
) -> Tuple[List[MentorAssignmentStatus], Set[int]]:
|
||||||
records = []
|
records = []
|
||||||
circle_ids = set()
|
circle_ids = set()
|
||||||
|
|
||||||
|
|
@ -105,19 +106,20 @@ def get_praxis_assignments(
|
||||||
[
|
[
|
||||||
completion
|
completion
|
||||||
for completion in completions
|
for completion in completions
|
||||||
if completion.status == CompletionStatus.SUBMITTED
|
if completion.status == MentorCompletionStatus.SUBMITTED
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
circle_id = learning_content.get_circle().id
|
circle_id = learning_content.get_circle().id
|
||||||
|
|
||||||
records.append(
|
records.append(
|
||||||
PraxisAssignmentStatus(
|
MentorAssignmentStatus(
|
||||||
id=course_session_assignment.id,
|
id=course_session_assignment.id,
|
||||||
title=learning_content.content_assignment.title,
|
title=learning_content.content_assignment.title,
|
||||||
circle_id=circle_id,
|
circle_id=circle_id,
|
||||||
pending_evaluations=submitted_count,
|
pending_evaluations=submitted_count,
|
||||||
completions=completions,
|
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
|
from typing import List
|
||||||
|
|
||||||
|
|
||||||
class CompletionStatus(str, Enum):
|
class MentorCompletionStatus(str, Enum):
|
||||||
UNKNOWN = "UNKNOWN"
|
UNKNOWN = "UNKNOWN"
|
||||||
SUBMITTED = "SUBMITTED"
|
SUBMITTED = "SUBMITTED"
|
||||||
EVALUATED = "EVALUATED"
|
EVALUATED = "EVALUATED"
|
||||||
|
|
||||||
|
|
||||||
|
class MentorAssignmentStatusType(str, Enum):
|
||||||
|
PRAXIS_ASSIGNMENT = "praxis_assignment"
|
||||||
|
SELF_EVALUATION_FEEDBACK = "self_evaluation_feedback"
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class PraxisAssignmentCompletion:
|
class MentorAssignmentCompletion:
|
||||||
status: CompletionStatus
|
status: MentorCompletionStatus
|
||||||
user_id: str
|
user_id: str
|
||||||
last_name: str
|
last_name: str
|
||||||
url: str
|
url: str
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class PraxisAssignmentStatus:
|
class MentorAssignmentStatus:
|
||||||
id: str
|
id: str
|
||||||
title: str
|
title: str
|
||||||
circle_id: str
|
circle_id: str
|
||||||
pending_evaluations: int
|
pending_evaluations: int
|
||||||
completions: List[PraxisAssignmentCompletion]
|
completions: List[MentorAssignmentCompletion]
|
||||||
|
type: MentorAssignmentStatusType
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
# Generated by Django 3.2.20 on 2024-02-12 09:25
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
("learning_mentor", "0004_alter_mentorinvitation_unique_together"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="learningmentor",
|
||||||
|
name="mentor",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -8,7 +8,7 @@ from vbv_lernwelt.course.models import CourseSessionUser
|
||||||
|
|
||||||
|
|
||||||
class LearningMentor(models.Model):
|
class LearningMentor(models.Model):
|
||||||
mentor = models.OneToOneField(User, on_delete=models.CASCADE)
|
mentor = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
course = models.ForeignKey("course.Course", on_delete=models.CASCADE)
|
course = models.ForeignKey("course.Course", on_delete=models.CASCADE)
|
||||||
|
|
||||||
participants = models.ManyToManyField(
|
participants = models.ManyToManyField(
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ from vbv_lernwelt.core.serializers import UserSerializer
|
||||||
from vbv_lernwelt.learning_mentor.models import LearningMentor, MentorInvitation
|
from vbv_lernwelt.learning_mentor.models import LearningMentor, MentorInvitation
|
||||||
|
|
||||||
|
|
||||||
class PraxisAssignmentCompletionSerializer(serializers.Serializer):
|
class MentorAssignmentCompletionSerializer(serializers.Serializer):
|
||||||
status = serializers.SerializerMethodField()
|
status = serializers.SerializerMethodField()
|
||||||
user_id = serializers.CharField()
|
user_id = serializers.CharField()
|
||||||
last_name = serializers.CharField()
|
last_name = serializers.CharField()
|
||||||
|
|
@ -15,13 +15,13 @@ class PraxisAssignmentCompletionSerializer(serializers.Serializer):
|
||||||
return obj.status.value
|
return obj.status.value
|
||||||
|
|
||||||
|
|
||||||
class PraxisAssignmentStatusSerializer(serializers.Serializer):
|
class MentorAssignmentStatusSerializer(serializers.Serializer):
|
||||||
id = serializers.CharField()
|
id = serializers.CharField()
|
||||||
title = serializers.CharField()
|
title = serializers.CharField()
|
||||||
circle_id = serializers.CharField()
|
circle_id = serializers.CharField()
|
||||||
pending_evaluations = serializers.IntegerField()
|
pending_evaluations = serializers.IntegerField()
|
||||||
completions = PraxisAssignmentCompletionSerializer(many=True)
|
completions = MentorAssignmentCompletionSerializer(many=True)
|
||||||
type = serializers.ReadOnlyField(default="praxis_assignment")
|
type = serializers.ReadOnlyField()
|
||||||
|
|
||||||
|
|
||||||
class InvitationSerializer(serializers.ModelSerializer):
|
class InvitationSerializer(serializers.ModelSerializer):
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ from vbv_lernwelt.learning_mentor.content.praxis_assignment import (
|
||||||
get_assignment_completions,
|
get_assignment_completions,
|
||||||
get_praxis_assignments,
|
get_praxis_assignments,
|
||||||
)
|
)
|
||||||
from vbv_lernwelt.learning_mentor.entities import CompletionStatus
|
from vbv_lernwelt.learning_mentor.entities import MentorCompletionStatus
|
||||||
|
|
||||||
|
|
||||||
class AttendanceServicesTestCase(TestCase):
|
class AttendanceServicesTestCase(TestCase):
|
||||||
|
|
@ -74,10 +74,10 @@ class AttendanceServicesTestCase(TestCase):
|
||||||
# THEN
|
# THEN
|
||||||
expected_order = ["Beta", "Alpha", "Gamma", "Kappa"]
|
expected_order = ["Beta", "Alpha", "Gamma", "Kappa"]
|
||||||
expected_statuses = {
|
expected_statuses = {
|
||||||
"Alpha": CompletionStatus.EVALUATED, # user1
|
"Alpha": MentorCompletionStatus.EVALUATED, # user1
|
||||||
"Beta": CompletionStatus.SUBMITTED, # user2
|
"Beta": MentorCompletionStatus.SUBMITTED, # user2
|
||||||
"Gamma": CompletionStatus.UNKNOWN, # user4 (no AssignmentCompletion)
|
"Gamma": MentorCompletionStatus.UNKNOWN, # user4 (no AssignmentCompletion)
|
||||||
"Kappa": CompletionStatus.UNKNOWN, # user3 (IN_PROGRESS should be PENDING)
|
"Kappa": MentorCompletionStatus.UNKNOWN, # user3 (IN_PROGRESS should be PENDING)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.assertEqual(len(results), len(participants))
|
self.assertEqual(len(results), len(participants))
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
@ -7,6 +9,7 @@ from vbv_lernwelt.assignment.models import (
|
||||||
AssignmentCompletionStatus,
|
AssignmentCompletionStatus,
|
||||||
AssignmentType,
|
AssignmentType,
|
||||||
)
|
)
|
||||||
|
from vbv_lernwelt.core.admin import User
|
||||||
from vbv_lernwelt.course.creators.test_utils import (
|
from vbv_lernwelt.course.creators.test_utils import (
|
||||||
add_course_session_user,
|
add_course_session_user,
|
||||||
create_assignment,
|
create_assignment,
|
||||||
|
|
@ -15,10 +18,22 @@ from vbv_lernwelt.course.creators.test_utils import (
|
||||||
create_course,
|
create_course,
|
||||||
create_course_session,
|
create_course_session,
|
||||||
create_course_session_assignment,
|
create_course_session_assignment,
|
||||||
|
create_learning_unit,
|
||||||
create_user,
|
create_user,
|
||||||
)
|
)
|
||||||
from vbv_lernwelt.course.models import CourseSessionUser
|
from vbv_lernwelt.course.models import CourseSessionUser
|
||||||
from vbv_lernwelt.learning_mentor.models import LearningMentor
|
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):
|
class LearningMentorAPITest(APITestCase):
|
||||||
|
|
@ -28,15 +43,6 @@ class LearningMentorAPITest(APITestCase):
|
||||||
|
|
||||||
self.circle, _ = create_circle(title="Circle", course_page=self.course_page)
|
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.mentor = create_user("mentor")
|
||||||
self.participant_1 = add_course_session_user(
|
self.participant_1 = add_course_session_user(
|
||||||
self.course_session,
|
self.course_session,
|
||||||
|
|
@ -109,7 +115,7 @@ class LearningMentorAPITest(APITestCase):
|
||||||
self.assertEqual(participant_1["first_name"], "Test")
|
self.assertEqual(participant_1["first_name"], "Test")
|
||||||
self.assertEqual(participant_1["last_name"], "Participant_1")
|
self.assertEqual(participant_1["last_name"], "Participant_1")
|
||||||
|
|
||||||
def test_api_praxis_assignments(self) -> None:
|
def test_api_self_evaluation_feedback(self) -> None:
|
||||||
# GIVEN
|
# GIVEN
|
||||||
participants = [self.participant_1, self.participant_2, self.participant_3]
|
participants = [self.participant_1, self.participant_2, self.participant_3]
|
||||||
self.client.force_login(self.mentor)
|
self.client.force_login(self.mentor)
|
||||||
|
|
@ -118,12 +124,104 @@ class LearningMentorAPITest(APITestCase):
|
||||||
mentor=self.mentor,
|
mentor=self.mentor,
|
||||||
course=self.course_session.course,
|
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)
|
mentor.participants.set(participants)
|
||||||
|
|
||||||
AssignmentCompletion.objects.create(
|
AssignmentCompletion.objects.create(
|
||||||
assignment_user=self.participant_1.user,
|
assignment_user=self.participant_1.user,
|
||||||
course_session=self.course_session,
|
course_session=self.course_session,
|
||||||
assignment=self.assignment,
|
assignment=assignment,
|
||||||
completion_status=AssignmentCompletionStatus.EVALUATION_SUBMITTED.value,
|
completion_status=AssignmentCompletionStatus.EVALUATION_SUBMITTED.value,
|
||||||
evaluation_user=self.mentor,
|
evaluation_user=self.mentor,
|
||||||
)
|
)
|
||||||
|
|
@ -131,7 +229,7 @@ class LearningMentorAPITest(APITestCase):
|
||||||
AssignmentCompletion.objects.create(
|
AssignmentCompletion.objects.create(
|
||||||
assignment_user=self.participant_3.user,
|
assignment_user=self.participant_3.user,
|
||||||
course_session=self.course_session,
|
course_session=self.course_session,
|
||||||
assignment=self.assignment,
|
assignment=assignment,
|
||||||
completion_status=AssignmentCompletionStatus.SUBMITTED.value,
|
completion_status=AssignmentCompletionStatus.SUBMITTED.value,
|
||||||
evaluation_user=self.mentor,
|
evaluation_user=self.mentor,
|
||||||
)
|
)
|
||||||
|
|
@ -232,3 +330,15 @@ class LearningMentorAPITest(APITestCase):
|
||||||
self.assertFalse(
|
self.assertFalse(
|
||||||
LearningMentor.objects.filter(participants=participant_cs_user).exists()
|
LearningMentor.objects.filter(participants=participant_cs_user).exists()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_mentor_multiple_courses(self) -> None:
|
||||||
|
# GIVEN
|
||||||
|
course_a, _ = create_course("Course A")
|
||||||
|
course_b, _ = create_course("Course B")
|
||||||
|
|
||||||
|
# WHEN
|
||||||
|
LearningMentor.objects.create(mentor=self.mentor, course=course_a)
|
||||||
|
LearningMentor.objects.create(mentor=self.mentor, course=course_b)
|
||||||
|
|
||||||
|
# THEN
|
||||||
|
self.assertEqual(LearningMentor.objects.count(), 2)
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
from vbv_lernwelt.learning_mentor.content.praxis_assignment import (
|
||||||
get_praxis_assignments,
|
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.models import LearningMentor, MentorInvitation
|
||||||
from vbv_lernwelt.learning_mentor.serializers import (
|
from vbv_lernwelt.learning_mentor.serializers import (
|
||||||
InvitationSerializer,
|
InvitationSerializer,
|
||||||
|
MentorAssignmentStatusSerializer,
|
||||||
MentorSerializer,
|
MentorSerializer,
|
||||||
PraxisAssignmentStatusSerializer,
|
|
||||||
)
|
)
|
||||||
from vbv_lernwelt.learnpath.models import Circle
|
from vbv_lernwelt.learnpath.models import Circle
|
||||||
from vbv_lernwelt.notify.email.email_services import EmailTemplate, send_email
|
from vbv_lernwelt.notify.email.email_services import EmailTemplate, send_email
|
||||||
|
|
@ -37,24 +40,40 @@ def mentor_summary(request, course_session_id: int):
|
||||||
assignments = []
|
assignments = []
|
||||||
circle_ids = set()
|
circle_ids = set()
|
||||||
|
|
||||||
praxis_assignments, _circle_ids = get_praxis_assignments(
|
praxis_assignments, praxis_assignments_circle_ids = get_praxis_assignments(
|
||||||
course_session=course_session, participants=users, evaluation_user=request.user
|
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(
|
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(
|
assignments.sort(
|
||||||
key=lambda x: (-x.get("pending_evaluations", 0), x.get("title", "").lower())
|
key=lambda x: (-x.get("pending_evaluations", 0), x.get("title", "").lower())
|
||||||
)
|
)
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
"participants": [UserSerializer(user).data for user in users],
|
"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,
|
"assignments": assignments,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,16 @@ from wagtail.rich_text import RichText
|
||||||
from wagtail_localize.models import LocaleSynchronization
|
from wagtail_localize.models import LocaleSynchronization
|
||||||
|
|
||||||
from vbv_lernwelt.assignment.models import Assignment
|
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.competence.models import ActionCompetence
|
||||||
from vbv_lernwelt.core.admin import User
|
from vbv_lernwelt.core.admin import User
|
||||||
from vbv_lernwelt.course.consts import COURSE_VERSICHERUNGSVERMITTLERIN_ID
|
from vbv_lernwelt.course.consts import COURSE_VERSICHERUNGSVERMITTLERIN_ID
|
||||||
from vbv_lernwelt.course.models import CourseCategory, CoursePage
|
from vbv_lernwelt.course.models import CourseCategory, CoursePage
|
||||||
|
from vbv_lernwelt.learnpath.models import LearningUnitPerformanceFeedbackType
|
||||||
from vbv_lernwelt.learnpath.tests.learning_path_factories import (
|
from vbv_lernwelt.learnpath.tests.learning_path_factories import (
|
||||||
CircleFactory,
|
CircleFactory,
|
||||||
LearningContentAssignmentFactory,
|
LearningContentAssignmentFactory,
|
||||||
|
|
@ -43,13 +48,13 @@ def create_vv_new_learning_path(
|
||||||
)
|
)
|
||||||
|
|
||||||
TopicFactory(title="Basis", is_visible=False, parent=lp)
|
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)
|
TopicFactory(title="Gewinnen von Kunden", parent=lp)
|
||||||
create_circle_gewinnen(lp)
|
create_circle_gewinnen(lp)
|
||||||
|
|
||||||
TopicFactory(title="Beraten und Betreuen von Kunden", parent=lp)
|
TopicFactory(title="Beraten und Betreuen von Kunden", parent=lp)
|
||||||
create_circle_fahrzeug(lp)
|
create_circle_fahrzeug(lp, course_page=course_page)
|
||||||
create_circle_haushalt(lp)
|
create_circle_haushalt(lp)
|
||||||
create_circle_rechtsstreitigkeiten(lp)
|
create_circle_rechtsstreitigkeiten(lp)
|
||||||
create_circle_reisen(lp)
|
create_circle_reisen(lp)
|
||||||
|
|
@ -103,7 +108,7 @@ def create_vv_pruefung_learning_path(
|
||||||
Page.objects.update(owner=user)
|
Page.objects.update(owner=user)
|
||||||
|
|
||||||
|
|
||||||
def create_circle_basis(lp, title="Basis"):
|
def create_circle_basis(lp, title="Basis", course_page=None):
|
||||||
circle = CircleFactory(
|
circle = CircleFactory(
|
||||||
title=title,
|
title=title,
|
||||||
parent=lp,
|
parent=lp,
|
||||||
|
|
@ -145,10 +150,42 @@ def create_circle_basis(lp, title="Basis"):
|
||||||
)
|
)
|
||||||
|
|
||||||
LearningSequenceFactory(title="Arbeitsalltag", parent=circle)
|
LearningSequenceFactory(title="Arbeitsalltag", parent=circle)
|
||||||
LearningUnitFactory(
|
lu = LearningUnitFactory(
|
||||||
title="Mein neuer Job, Arbeitstechnik, Soziale Medien, Datenschutz und Beratungspflichten",
|
title="Mein neuer Job, Arbeitstechnik, Soziale Medien, Datenschutz und Beratungspflichten",
|
||||||
|
feedback_user=LearningUnitPerformanceFeedbackType.MENTOR_FEEDBACK.name,
|
||||||
parent=circle,
|
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(
|
LearningContentPlaceholderFactory(
|
||||||
title="Mediathek",
|
title="Mediathek",
|
||||||
parent=circle,
|
parent=circle,
|
||||||
|
|
@ -303,7 +340,7 @@ def create_circle_gewinnen(lp, title="Gewinnen"):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def create_circle_fahrzeug(lp, title="Fahrzeug"):
|
def create_circle_fahrzeug(lp, title="Fahrzeug", course_page=None):
|
||||||
circle = CircleFactory(
|
circle = CircleFactory(
|
||||||
title=title,
|
title=title,
|
||||||
parent=lp,
|
parent=lp,
|
||||||
|
|
@ -367,7 +404,14 @@ def create_circle_fahrzeug(lp, title="Fahrzeug"):
|
||||||
)
|
)
|
||||||
|
|
||||||
LearningSequenceFactory(title="Transfer", parent=circle, icon="it-icon-ls-end")
|
LearningSequenceFactory(title="Transfer", parent=circle, icon="it-icon-ls-end")
|
||||||
LearningUnitFactory(title="Transfer", title_hidden=True, parent=circle)
|
|
||||||
|
lu_transfer = LearningUnitFactory(
|
||||||
|
title="Transfer",
|
||||||
|
title_hidden=True,
|
||||||
|
parent=circle,
|
||||||
|
feedback_user=LearningUnitPerformanceFeedbackType.MENTOR_FEEDBACK.name,
|
||||||
|
)
|
||||||
|
|
||||||
LearningContentPlaceholderFactory(
|
LearningContentPlaceholderFactory(
|
||||||
title="Praxisauftrag",
|
title="Praxisauftrag",
|
||||||
parent=circle,
|
parent=circle,
|
||||||
|
|
@ -392,6 +436,36 @@ def create_circle_fahrzeug(lp, title="Fahrzeug"):
|
||||||
parent=circle,
|
parent=circle,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
competence_profile_page = ActionCompetenceListPageFactory(
|
||||||
|
title="KompetenzNavi",
|
||||||
|
parent=course_page,
|
||||||
|
)
|
||||||
|
|
||||||
|
ace = ActionCompetenceFactory(
|
||||||
|
parent=competence_profile_page,
|
||||||
|
)
|
||||||
|
|
||||||
|
PerformanceCriteriaFactory(
|
||||||
|
parent=ace,
|
||||||
|
competence_id="VV-Transfer-A",
|
||||||
|
title="Ich setze das Gelernte in der Praxis um.",
|
||||||
|
learning_unit=lu_transfer,
|
||||||
|
)
|
||||||
|
|
||||||
|
PerformanceCriteriaFactory(
|
||||||
|
parent=ace,
|
||||||
|
competence_id="VV-Transfer-B",
|
||||||
|
title="Ich kenne den Unterschied zwischen einem Neuwagen und einem Occasionswagen.",
|
||||||
|
learning_unit=lu_transfer,
|
||||||
|
)
|
||||||
|
|
||||||
|
PerformanceCriteriaFactory(
|
||||||
|
parent=ace,
|
||||||
|
competence_id="VV-Transfer-C",
|
||||||
|
title="Ich kenne den Unterschied zwischen einem Leasing und einem Kauf.",
|
||||||
|
learning_unit=lu_transfer,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def create_circle_haushalt(lp, title="Haushalt"):
|
def create_circle_haushalt(lp, title="Haushalt"):
|
||||||
circle = CircleFactory(
|
circle = CircleFactory(
|
||||||
|
|
|
||||||
|
|
@ -234,7 +234,7 @@ class LearningUnitObjectType(DjangoObjectType):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = LearningUnit
|
model = LearningUnit
|
||||||
interfaces = (CoursePageInterface,)
|
interfaces = (CoursePageInterface,)
|
||||||
fields = ["evaluate_url", "title_hidden"]
|
fields = ["evaluate_url", "title_hidden", "feedback_user"]
|
||||||
|
|
||||||
def resolve_evaluate_url(self: LearningUnit, info, **kwargs):
|
def resolve_evaluate_url(self: LearningUnit, info, **kwargs):
|
||||||
return self.get_evaluate_url()
|
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
|
import re
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.text import slugify
|
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.fields import RichTextField, StreamField
|
||||||
from wagtail.models import Page
|
from wagtail.models import Page
|
||||||
|
|
||||||
|
|
@ -117,6 +119,13 @@ class Circle(CourseBasePage):
|
||||||
return f"{self.title}"
|
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):
|
class LearningSequence(CourseBasePage):
|
||||||
serialize_field_names = ["icon"]
|
serialize_field_names = ["icon"]
|
||||||
|
|
||||||
|
|
@ -169,10 +178,21 @@ class LearningUnit(CourseBasePage):
|
||||||
"course.CourseCategory", on_delete=models.SET_NULL, null=True, blank=True
|
"course.CourseCategory", on_delete=models.SET_NULL, null=True, blank=True
|
||||||
)
|
)
|
||||||
title_hidden = models.BooleanField(default=False)
|
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 + [
|
content_panels = Page.content_panels + [
|
||||||
FieldPanel("course_category"),
|
FieldPanel("course_category"),
|
||||||
FieldPanel("title_hidden"),
|
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:
|
class Meta:
|
||||||
|
|
@ -200,21 +220,29 @@ class LearningUnit(CourseBasePage):
|
||||||
)
|
)
|
||||||
super(LearningUnit, self).save(clean, user, log_action, **kwargs)
|
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 = re.compile(
|
||||||
r"^(?P<coursePart>.+?)-lp-circle-(?P<circlePart>.+?)-lu-(?P<luPart>.+?)$"
|
r"^(?P<coursePart>.+?)-lp-circle-(?P<circlePart>.+?)-lu-(?P<luPart>.+?)$"
|
||||||
)
|
)
|
||||||
m = r.match(self.slug)
|
m = r.match(self.slug)
|
||||||
|
|
||||||
if m is None:
|
if m is None:
|
||||||
return "ERROR: could not parse slug"
|
ValueError(f"Could not parse slug: {self.slug}")
|
||||||
return f"/course/{m.group('coursePart')}/learn/{m.group('circlePart')}#lu-{m.group('luPart')}"
|
|
||||||
|
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):
|
def get_evaluate_url(self):
|
||||||
r = re.compile(
|
course, circle, learning_unit = self.get_frontend_url_parts()
|
||||||
r"^(?P<coursePart>.+?)-lp-circle-(?P<circlePart>.+?)-lu-(?P<luPart>.+?)$"
|
return f"/course/{course}/learn/{circle}/evaluate/{learning_unit}"
|
||||||
)
|
|
||||||
m = r.match(self.slug)
|
|
||||||
return f"/course/{m.group('coursePart')}/learn/{m.group('circlePart')}/evaluate/{m.group('luPart')}"
|
|
||||||
|
|
||||||
def get_admin_display_title(self):
|
def get_admin_display_title(self):
|
||||||
return f"LE: {self.draft_title}"
|
return f"LE: {self.draft_title}"
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ class LearningUnitSerializer(
|
||||||
"course_category",
|
"course_category",
|
||||||
"children",
|
"children",
|
||||||
"title_hidden",
|
"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.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_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
|
from vbv_lernwelt.learnpath.models import LearningContentPlaceholder
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -31,3 +37,22 @@ class SaveSlugTestCase(TestCase):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
lc_fachcheck.slug, "test-lehrgang-lp-circle-reisen-lc-fachcheck-foobar"
|
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")
|
rendition.file.open("rb")
|
||||||
image_format = imghdr.what(rendition.file)
|
image_format = imghdr.what(rendition.file)
|
||||||
|
|
||||||
return StreamingHttpResponse(
|
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",
|
"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(
|
def send_email(
|
||||||
recipient_email: str,
|
recipient_email: str,
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,13 @@ class NotificationTrigger(models.TextChoices):
|
||||||
CASEWORK_EVALUATED = "CASEWORK_EVALUATED", _("Casework Evaluated")
|
CASEWORK_EVALUATED = "CASEWORK_EVALUATED", _("Casework Evaluated")
|
||||||
NEW_FEEDBACK = "NEW_FEEDBACK", _("New Feedback")
|
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):
|
class Notification(AbstractNotification):
|
||||||
# UUIDs are not supported by the notifications app...
|
# UUIDs are not supported by the notifications app...
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ from vbv_lernwelt.notify.models import (
|
||||||
NotificationCategory,
|
NotificationCategory,
|
||||||
NotificationTrigger,
|
NotificationTrigger,
|
||||||
)
|
)
|
||||||
|
from vbv_lernwelt.self_evaluation_feedback.models import SelfEvaluationFeedback
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from vbv_lernwelt.assignment.models import AssignmentCompletion
|
from vbv_lernwelt.assignment.models import AssignmentCompletion
|
||||||
|
|
@ -73,6 +74,74 @@ class NotificationService:
|
||||||
email_template=EmailTemplate.CASEWORK_SUBMITTED,
|
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
|
@classmethod
|
||||||
def send_assignment_evaluated_notification(
|
def send_assignment_evaluated_notification(
|
||||||
cls,
|
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,672 @@
|
||||||
|
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.learnpath.models import LearningUnitPerformanceFeedbackType
|
||||||
|
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( # noqa
|
||||||
|
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.assertFalse(feedback["feedback_submitted"])
|
||||||
|
self.assertEqual(feedback["circle_name"], self.circle.title) # noqa
|
||||||
|
|
||||||
|
provider_user = feedback["feedback_provider_user"]
|
||||||
|
self.assertEqual(provider_user["id"], str(self.mentor.id))
|
||||||
|
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_feedbacks_with_mixed_completion_statuses(self):
|
||||||
|
"""Case: CourseCompletion AND feedbacks with mixed completion statuses"""
|
||||||
|
|
||||||
|
# GIVEN
|
||||||
|
learning_unit = create_learning_unit(
|
||||||
|
course=self.course,
|
||||||
|
circle=self.circle,
|
||||||
|
feedback_user=LearningUnitPerformanceFeedbackType.MENTOR_FEEDBACK,
|
||||||
|
)
|
||||||
|
|
||||||
|
feedback = create_self_evaluation_feedback(
|
||||||
|
learning_unit=learning_unit,
|
||||||
|
feedback_requester_user=self.member,
|
||||||
|
feedback_provider_user=self.mentor,
|
||||||
|
)
|
||||||
|
|
||||||
|
feedback.feedback_submitted = True
|
||||||
|
feedback.save()
|
||||||
|
|
||||||
|
for status in [
|
||||||
|
CourseCompletionStatus.SUCCESS,
|
||||||
|
CourseCompletionStatus.FAIL,
|
||||||
|
CourseCompletionStatus.UNKNOWN,
|
||||||
|
]:
|
||||||
|
criteria_page = create_performance_criteria_page(
|
||||||
|
course=self.course,
|
||||||
|
course_page=self.course_page,
|
||||||
|
circle=self.circle,
|
||||||
|
learning_unit=learning_unit,
|
||||||
|
)
|
||||||
|
|
||||||
|
# self assessment
|
||||||
|
completion = mark_course_completion(
|
||||||
|
page=criteria_page,
|
||||||
|
user=self.member,
|
||||||
|
course_session=self.course_session,
|
||||||
|
completion_status=status.value,
|
||||||
|
)
|
||||||
|
|
||||||
|
# feedback assessment
|
||||||
|
CourseCompletionFeedback.objects.create(
|
||||||
|
feedback=feedback,
|
||||||
|
course_completion=completion,
|
||||||
|
feedback_assessment=status.value,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.client.force_login(self.member)
|
||||||
|
|
||||||
|
# WHEN
|
||||||
|
response = self.client.get(
|
||||||
|
reverse(
|
||||||
|
"get_self_evaluation_feedbacks_as_requester",
|
||||||
|
args=[self.course_session.id],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# THEN
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
result = response.data["results"][0]
|
||||||
|
|
||||||
|
self_assessment = result["self_assessment"]
|
||||||
|
self.assertEqual(self_assessment["counts"]["pass"], 1)
|
||||||
|
self.assertEqual(self_assessment["counts"]["fail"], 1)
|
||||||
|
self.assertEqual(self_assessment["counts"]["unknown"], 1)
|
||||||
|
|
||||||
|
feedback_assessment = result["feedback_assessment"]
|
||||||
|
self.assertEqual(feedback_assessment["counts"]["pass"], 1)
|
||||||
|
self.assertEqual(feedback_assessment["counts"]["fail"], 1)
|
||||||
|
self.assertEqual(feedback_assessment["counts"]["unknown"], 1)
|
||||||
|
|
||||||
|
self.assertTrue(feedback_assessment["submitted_by_provider"])
|
||||||
|
self.assertEqual(
|
||||||
|
feedback_assessment["provider_user"]["id"], str(self.mentor.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
aggregate = response.data["aggregates"]
|
||||||
|
self.assertEqual(aggregate["self_assessment"]["pass"], 1)
|
||||||
|
self.assertEqual(aggregate["self_assessment"]["fail"], 1)
|
||||||
|
self.assertEqual(aggregate["self_assessment"]["unknown"], 1)
|
||||||
|
self.assertEqual(aggregate["feedback_assessment"]["pass"], 1)
|
||||||
|
self.assertEqual(aggregate["feedback_assessment"]["fail"], 1)
|
||||||
|
self.assertEqual(aggregate["feedback_assessment"]["unknown"], 1)
|
||||||
|
|
||||||
|
def test_no_feedbacks_but_with_completion_status(self):
|
||||||
|
"""Case: CourseCompletion but NO feedback"""
|
||||||
|
|
||||||
|
# GIVEN
|
||||||
|
learning_unit_with_success_feedback = create_learning_unit(
|
||||||
|
course=self.course,
|
||||||
|
circle=self.circle,
|
||||||
|
feedback_user=LearningUnitPerformanceFeedbackType.MENTOR_FEEDBACK,
|
||||||
|
)
|
||||||
|
|
||||||
|
performance_criteria_page = create_performance_criteria_page(
|
||||||
|
course=self.course,
|
||||||
|
course_page=self.course_page,
|
||||||
|
circle=self.circle,
|
||||||
|
learning_unit=learning_unit_with_success_feedback,
|
||||||
|
)
|
||||||
|
|
||||||
|
# IMPORTANT: CourseCompletion but NO feedback!
|
||||||
|
|
||||||
|
mark_course_completion(
|
||||||
|
page=performance_criteria_page,
|
||||||
|
user=self.member,
|
||||||
|
course_session=self.course_session,
|
||||||
|
completion_status=CourseCompletionStatus.SUCCESS.value,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.client.force_login(self.member)
|
||||||
|
|
||||||
|
# WHEN
|
||||||
|
response = self.client.get(
|
||||||
|
reverse(
|
||||||
|
"get_self_evaluation_feedbacks_as_requester",
|
||||||
|
args=[self.course_session.id],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# THEN
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
result = response.data["results"][0]
|
||||||
|
counts = result["self_assessment"]["counts"]
|
||||||
|
self.assertEqual(counts["pass"], 1)
|
||||||
|
self.assertEqual(counts["fail"], 0)
|
||||||
|
self.assertEqual(counts["unknown"], 0)
|
||||||
|
|
||||||
|
def test_feedbacks_not_started(self):
|
||||||
|
"""Case: Learning unit with no completion status and no feedback"""
|
||||||
|
|
||||||
|
# GIVEN
|
||||||
|
learning_unit = create_learning_unit( # noqa
|
||||||
|
course=self.course,
|
||||||
|
circle=self.circle,
|
||||||
|
feedback_user=LearningUnitPerformanceFeedbackType.MENTOR_FEEDBACK,
|
||||||
|
)
|
||||||
|
|
||||||
|
create_performance_criteria_page(
|
||||||
|
course=self.course,
|
||||||
|
course_page=self.course_page,
|
||||||
|
circle=self.circle,
|
||||||
|
learning_unit=learning_unit,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.client.force_login(self.member)
|
||||||
|
|
||||||
|
# WHEN
|
||||||
|
response = self.client.get(
|
||||||
|
reverse(
|
||||||
|
"get_self_evaluation_feedbacks_as_requester",
|
||||||
|
args=[self.course_session.id],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# THEN
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
result = response.data["results"][0]
|
||||||
|
self.assertEqual(result["self_assessment"]["counts"]["pass"], 0)
|
||||||
|
self.assertEqual(result["self_assessment"]["counts"]["fail"], 0)
|
||||||
|
self.assertEqual(result["self_assessment"]["counts"]["unknown"], 1)
|
||||||
|
|
||||||
|
def test_feedbacks_metadata(self):
|
||||||
|
# GIVEN
|
||||||
|
learning_unit = create_learning_unit( # noqa
|
||||||
|
course=self.course,
|
||||||
|
circle=self.circle,
|
||||||
|
feedback_user=LearningUnitPerformanceFeedbackType.MENTOR_FEEDBACK,
|
||||||
|
)
|
||||||
|
|
||||||
|
create_performance_criteria_page(
|
||||||
|
course=self.course,
|
||||||
|
course_page=self.course_page,
|
||||||
|
circle=self.circle,
|
||||||
|
learning_unit=learning_unit,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.client.force_login(self.member)
|
||||||
|
|
||||||
|
# WHEN
|
||||||
|
response = self.client.get(
|
||||||
|
reverse(
|
||||||
|
"get_self_evaluation_feedbacks_as_requester",
|
||||||
|
args=[self.course_session.id],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# THEN
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
result = response.data["results"][0]
|
||||||
|
self.assertEqual(result["title"], learning_unit.title)
|
||||||
|
self.assertEqual(result["id"], learning_unit.id)
|
||||||
|
self.assertEqual(result["circle_id"], self.circle.id)
|
||||||
|
self.assertEqual(result["circle_title"], self.circle.title)
|
||||||
|
self.assertEqual(result["detail_url"], learning_unit.get_evaluate_url())
|
||||||
|
|
||||||
|
circles = response.data["circles"]
|
||||||
|
self.assertEqual(len(circles), 1)
|
||||||
|
self.assertEqual(circles[0]["id"], self.circle.id)
|
||||||
|
self.assertEqual(circles[0]["title"], self.circle.title)
|
||||||
|
|
||||||
|
def test_get_self_evaluation_feedback_as_provider(self):
|
||||||
|
"""Tests endpoint of feedback PROVIDER"""
|
||||||
|
|
||||||
|
# GIVEN
|
||||||
|
learning_unit = create_learning_unit( # noqa
|
||||||
|
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) # noqa
|
||||||
|
|
||||||
|
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
|
||||||
|
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,45 @@
|
||||||
|
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,
|
||||||
|
get_self_evaluation_feedbacks_as_requester,
|
||||||
|
release_provider_self_evaluation_feedback,
|
||||||
|
start_self_evaluation_feedback,
|
||||||
|
)
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
# /requester/* URLs -> For the user who requests feedback
|
||||||
|
path(
|
||||||
|
"requester/<signed_int:course_session_id>/feedbacks/summaries",
|
||||||
|
get_self_evaluation_feedbacks_as_requester,
|
||||||
|
name="get_self_evaluation_feedbacks_as_requester",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"requester/<int:learning_unit_id>/feedback/start",
|
||||||
|
start_self_evaluation_feedback,
|
||||||
|
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",
|
||||||
|
),
|
||||||
|
# /provider/* URLs -> For the user who is providing feedback
|
||||||
|
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,126 @@
|
||||||
|
from typing import NamedTuple
|
||||||
|
|
||||||
|
from django.db.models import Case, Count, IntegerField, Sum, Value, When
|
||||||
|
from django.db.models.functions import Coalesce
|
||||||
|
|
||||||
|
from vbv_lernwelt.core.admin import User
|
||||||
|
from vbv_lernwelt.course.models import CourseCompletion, CourseCompletionStatus
|
||||||
|
from vbv_lernwelt.learnpath.models import LearningUnit
|
||||||
|
from vbv_lernwelt.self_evaluation_feedback.models import (
|
||||||
|
CourseCompletionFeedback,
|
||||||
|
SelfEvaluationFeedback,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AssessmentCounts(NamedTuple):
|
||||||
|
pass_count: int
|
||||||
|
fail_count: int
|
||||||
|
unknown_count: int
|
||||||
|
|
||||||
|
@property
|
||||||
|
def total_count(self):
|
||||||
|
return self.pass_count + self.fail_count + self.unknown_count
|
||||||
|
|
||||||
|
|
||||||
|
def get_self_evaluation_feedback_counts(
|
||||||
|
feedback: SelfEvaluationFeedback,
|
||||||
|
):
|
||||||
|
course_completion_feedback = CourseCompletionFeedback.objects.filter(
|
||||||
|
feedback=feedback
|
||||||
|
).aggregate(
|
||||||
|
pass_count=Coalesce(
|
||||||
|
Sum(
|
||||||
|
Case(
|
||||||
|
When(
|
||||||
|
feedback_assessment=CourseCompletionStatus.SUCCESS.value,
|
||||||
|
then=Value(1),
|
||||||
|
),
|
||||||
|
output_field=IntegerField(),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
Value(0),
|
||||||
|
),
|
||||||
|
fail_count=Coalesce(
|
||||||
|
Sum(
|
||||||
|
Case(
|
||||||
|
When(
|
||||||
|
feedback_assessment=CourseCompletionStatus.FAIL.value,
|
||||||
|
then=Value(1),
|
||||||
|
),
|
||||||
|
output_field=IntegerField(),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
Value(0),
|
||||||
|
),
|
||||||
|
unknown_count=Coalesce(
|
||||||
|
Sum(
|
||||||
|
Case(
|
||||||
|
When(
|
||||||
|
feedback_assessment=CourseCompletionStatus.UNKNOWN.value,
|
||||||
|
then=Value(1),
|
||||||
|
),
|
||||||
|
output_field=IntegerField(),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
Value(0),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return AssessmentCounts(
|
||||||
|
pass_count=course_completion_feedback.get("pass_count", 0),
|
||||||
|
fail_count=course_completion_feedback.get("fail_count", 0),
|
||||||
|
unknown_count=course_completion_feedback.get("unknown_count", 0),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_self_assessment_counts(
|
||||||
|
learning_unit: LearningUnit, user: User
|
||||||
|
) -> AssessmentCounts:
|
||||||
|
performance_criteria = learning_unit.performancecriteria_set.all()
|
||||||
|
|
||||||
|
completion_counts = CourseCompletion.objects.filter(
|
||||||
|
page__in=performance_criteria, user=user
|
||||||
|
).aggregate(
|
||||||
|
pass_count=Count(
|
||||||
|
Case(
|
||||||
|
When(completion_status=CourseCompletionStatus.SUCCESS.value, then=1),
|
||||||
|
output_field=IntegerField(),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
fail_count=Count(
|
||||||
|
Case(
|
||||||
|
When(completion_status=CourseCompletionStatus.FAIL.value, then=1),
|
||||||
|
output_field=IntegerField(),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
unknown_count=Count(
|
||||||
|
Case(
|
||||||
|
When(completion_status=CourseCompletionStatus.UNKNOWN.value, then=1),
|
||||||
|
output_field=IntegerField(),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
pass_count = completion_counts.get("pass_count", 0)
|
||||||
|
fail_count = completion_counts.get("fail_count", 0)
|
||||||
|
unknown_count = completion_counts.get("unknown_count", 0)
|
||||||
|
|
||||||
|
# not yet completed performance criteria are unknown
|
||||||
|
if pass_count + fail_count + unknown_count < performance_criteria.count():
|
||||||
|
unknown_count += performance_criteria.count() - (
|
||||||
|
pass_count + fail_count + unknown_count
|
||||||
|
)
|
||||||
|
|
||||||
|
return AssessmentCounts(
|
||||||
|
pass_count=pass_count,
|
||||||
|
fail_count=fail_count,
|
||||||
|
unknown_count=unknown_count,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_aggregate(counts: [AssessmentCounts]):
|
||||||
|
return AssessmentCounts(
|
||||||
|
pass_count=sum(x.pass_count for x in counts),
|
||||||
|
fail_count=sum(x.fail_count for x in counts),
|
||||||
|
unknown_count=sum(x.unknown_count for x in counts),
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,258 @@
|
||||||
|
import structlog
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
from rest_framework.decorators import api_view, permission_classes
|
||||||
|
from rest_framework.exceptions import PermissionDenied
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
from vbv_lernwelt.core.models import User
|
||||||
|
from vbv_lernwelt.core.serializers import UserSerializer
|
||||||
|
from vbv_lernwelt.course.models import CourseCompletion, CourseSession
|
||||||
|
from vbv_lernwelt.learning_mentor.models import LearningMentor
|
||||||
|
from vbv_lernwelt.learnpath.models import (
|
||||||
|
Circle,
|
||||||
|
LearningUnit,
|
||||||
|
LearningUnitPerformanceFeedbackType,
|
||||||
|
)
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
from vbv_lernwelt.self_evaluation_feedback.utils import (
|
||||||
|
AssessmentCounts,
|
||||||
|
get_self_assessment_counts,
|
||||||
|
get_self_evaluation_feedback_counts,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger = structlog.get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@api_view(["POST"])
|
||||||
|
@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_feedbacks_as_requester(request, course_session_id: int):
|
||||||
|
course_session = get_object_or_404(CourseSession, id=course_session_id)
|
||||||
|
|
||||||
|
results = []
|
||||||
|
circle_ids = set()
|
||||||
|
|
||||||
|
all_self_assessment_counts = []
|
||||||
|
all_feedback_assessment_counts = []
|
||||||
|
|
||||||
|
for learning_unit in LearningUnit.objects.filter(
|
||||||
|
course_category__course=course_session.course,
|
||||||
|
):
|
||||||
|
# this is not a problem in real life, but in the test environment
|
||||||
|
# we have a lot of learning units without self assessment criteria
|
||||||
|
# -> just skip those learning units
|
||||||
|
if len(learning_unit.performancecriteria_set.all()) == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
circle = learning_unit.get_parent().specific
|
||||||
|
circle_ids.add(circle.id)
|
||||||
|
|
||||||
|
feedback = SelfEvaluationFeedback.objects.filter(
|
||||||
|
learning_unit=learning_unit,
|
||||||
|
feedback_requester_user=request.user,
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not feedback:
|
||||||
|
# no feedback given yet
|
||||||
|
feedback_assessment = None
|
||||||
|
else:
|
||||||
|
# feedback given
|
||||||
|
feedback_counts = get_self_evaluation_feedback_counts(feedback)
|
||||||
|
all_feedback_assessment_counts.append(feedback_counts)
|
||||||
|
|
||||||
|
feedback_assessment = {
|
||||||
|
"submitted_by_provider": feedback.feedback_submitted,
|
||||||
|
"provider_user": UserSerializer(feedback.feedback_provider_user).data,
|
||||||
|
"counts": {
|
||||||
|
"pass": feedback_counts.pass_count,
|
||||||
|
"fail": feedback_counts.fail_count,
|
||||||
|
"unknown": feedback_counts.unknown_count,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
self_assessment_counts = get_self_assessment_counts(learning_unit, request.user)
|
||||||
|
all_self_assessment_counts.append(self_assessment_counts)
|
||||||
|
|
||||||
|
results.append(
|
||||||
|
{
|
||||||
|
"id": learning_unit.id,
|
||||||
|
"title": learning_unit.title,
|
||||||
|
"detail_url": learning_unit.get_evaluate_url(),
|
||||||
|
"circle_id": circle.id,
|
||||||
|
"circle_title": circle.title,
|
||||||
|
"feedback_assessment": feedback_assessment,
|
||||||
|
"self_assessment": {
|
||||||
|
"counts": {
|
||||||
|
"pass": self_assessment_counts.pass_count,
|
||||||
|
"fail": self_assessment_counts.fail_count,
|
||||||
|
"unknown": self_assessment_counts.unknown_count,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
self_assessment_counts_aggregate = AssessmentCounts(
|
||||||
|
pass_count=sum(x.pass_count for x in all_self_assessment_counts),
|
||||||
|
fail_count=sum(x.fail_count for x in all_self_assessment_counts),
|
||||||
|
unknown_count=sum(x.unknown_count for x in all_self_assessment_counts),
|
||||||
|
)
|
||||||
|
received_feedback_counts_aggregate = AssessmentCounts(
|
||||||
|
pass_count=sum(x.pass_count for x in all_feedback_assessment_counts),
|
||||||
|
fail_count=sum(x.fail_count for x in all_feedback_assessment_counts),
|
||||||
|
unknown_count=sum(x.unknown_count for x in all_feedback_assessment_counts),
|
||||||
|
)
|
||||||
|
|
||||||
|
# pad the feedback counts with unknowns for the
|
||||||
|
# learning units where we have no feedback yet
|
||||||
|
feedback_assessment_counts_aggregate = AssessmentCounts(
|
||||||
|
pass_count=received_feedback_counts_aggregate.pass_count,
|
||||||
|
fail_count=received_feedback_counts_aggregate.fail_count,
|
||||||
|
unknown_count=self_assessment_counts_aggregate.total_count
|
||||||
|
- received_feedback_counts_aggregate.total_count
|
||||||
|
+ received_feedback_counts_aggregate.unknown_count,
|
||||||
|
)
|
||||||
|
|
||||||
|
# check if there are any learning units with mentor feedback
|
||||||
|
feedback_assessment_visible = (
|
||||||
|
LearningUnit.objects.filter(
|
||||||
|
feedback_user=LearningUnitPerformanceFeedbackType.MENTOR_FEEDBACK.value,
|
||||||
|
course_category__course=course_session.course,
|
||||||
|
).count()
|
||||||
|
> 0
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"results": results,
|
||||||
|
"circles": list(
|
||||||
|
Circle.objects.filter(id__in=circle_ids).values("id", "title")
|
||||||
|
),
|
||||||
|
"aggregates": {
|
||||||
|
"feedback_assessment_visible": feedback_assessment_visible,
|
||||||
|
"feedback_assessment": {
|
||||||
|
"pass": feedback_assessment_counts_aggregate.pass_count,
|
||||||
|
"fail": feedback_assessment_counts_aggregate.fail_count,
|
||||||
|
"unknown": feedback_assessment_counts_aggregate.unknown_count,
|
||||||
|
},
|
||||||
|
"self_assessment": {
|
||||||
|
"pass": self_assessment_counts_aggregate.pass_count,
|
||||||
|
"fail": self_assessment_counts_aggregate.fail_count,
|
||||||
|
"unknown": self_assessment_counts_aggregate.unknown_count,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@api_view(["GET"])
|
||||||
|
@permission_classes([IsAuthenticated])
|
||||||
|
def get_self_evaluation_feedback_as_requester(request, learning_unit_id):
|
||||||
|
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})
|
||||||
|
|
@ -43,10 +43,24 @@
|
||||||
<a href="{% url 'edoniq_export_students_and_trainers' %}" class="btn btn-primary">Teilnehmer
|
<a href="{% url 'edoniq_export_students_and_trainers' %}" class="btn btn-primary">Teilnehmer
|
||||||
und Trainer exportieren</a>
|
und Trainer exportieren</a>
|
||||||
|
|
||||||
|
<hr style="margin: 24px 0">
|
||||||
|
|
||||||
|
<form action="/api/core/resetiterativsessions/" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<p>Zurücksetzen der Iterativ Testdurchführungen (üK: "Iterativ üK Testkurs", VV: "Iterativ VV Testkurs")</p>
|
||||||
|
<button class="btn">Iterativ Testdurchführungen zurücksetzen</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
<hr style="margin: 24px 0">
|
<hr style="margin: 24px 0">
|
||||||
|
|
||||||
<form action="/api/core/cypressreset/" method="post">
|
<form action="/api/core/cypressreset/" method="post">
|
||||||
{% csrf_token %}
|
{% 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>
|
<label>
|
||||||
<input type="checkbox" name="create_assignment_completion" value="true">
|
<input type="checkbox" name="create_assignment_completion" value="true">
|
||||||
create_assignment_completion
|
create_assignment_completion
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue