Merged in feat/vv-read-only-komp-navi (pull request #302)
VV readonly profile w/ KompetenzNavi Approved-by: Daniel Egger
This commit is contained in:
commit
ade020614f
|
|
@ -1,6 +1,11 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useCircleStore } from "@/stores/circle";
|
import { useCircleStore } from "@/stores/circle";
|
||||||
import type { CircleType, LearningUnit } from "@/types";
|
import type {
|
||||||
|
CircleType,
|
||||||
|
CourseCompletionStatus,
|
||||||
|
LearningUnit,
|
||||||
|
LearningUnitPerformanceCriteria,
|
||||||
|
} from "@/types";
|
||||||
import * as log from "loglevel";
|
import * as log from "loglevel";
|
||||||
|
|
||||||
import { useCourseDataWithCompletion, useCurrentCourseSession } from "@/composables";
|
import { useCourseDataWithCompletion, useCurrentCourseSession } from "@/composables";
|
||||||
|
|
@ -12,6 +17,7 @@ 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";
|
import SelfEvaluationRequestFeedbackPage from "@/pages/learningPath/selfEvaluationPage/SelfEvaluationRequestFeedbackPage.vue";
|
||||||
|
import { useCockpitStore } from "@/stores/cockpit";
|
||||||
|
|
||||||
log.debug("LearningContent.vue setup");
|
log.debug("LearningContent.vue setup");
|
||||||
|
|
||||||
|
|
@ -24,6 +30,12 @@ const circleStore = useCircleStore();
|
||||||
const courseSession = useCurrentCourseSession();
|
const courseSession = useCurrentCourseSession();
|
||||||
const courseCompletionData = useCourseDataWithCompletion();
|
const courseCompletionData = useCourseDataWithCompletion();
|
||||||
|
|
||||||
|
const isReadOnly = computed(
|
||||||
|
// a hack: If we are a mentor or expert, we are in read only mode
|
||||||
|
// we might preview / view this but can't change anything (buttons are disabled)
|
||||||
|
() => useCockpitStore().hasExpertCockpitType || useCockpitStore().hasMentorCockpitType
|
||||||
|
);
|
||||||
|
|
||||||
const questions = computed(() => props.learningUnit?.performance_criteria ?? []);
|
const questions = computed(() => props.learningUnit?.performance_criteria ?? []);
|
||||||
const numPages = computed(() => {
|
const numPages = computed(() => {
|
||||||
if (learningUnitHasFeedbackPage.value) {
|
if (learningUnitHasFeedbackPage.value) {
|
||||||
|
|
@ -37,7 +49,8 @@ const questionIndex = useRouteQuery("step", "0", { transform: Number, mode: "pus
|
||||||
const previousRoute = getPreviousRoute();
|
const previousRoute = getPreviousRoute();
|
||||||
|
|
||||||
const learningUnitHasFeedbackPage = computed(
|
const learningUnitHasFeedbackPage = computed(
|
||||||
() => courseSession.value.course.configuration.enable_learning_mentor
|
() =>
|
||||||
|
courseSession.value.course.configuration.enable_learning_mentor && !isReadOnly.value
|
||||||
);
|
);
|
||||||
|
|
||||||
const currentQuestion = computed(() => questions.value[questionIndex.value]);
|
const currentQuestion = computed(() => questions.value[questionIndex.value]);
|
||||||
|
|
@ -57,7 +70,7 @@ function handleContinue() {
|
||||||
// not answering a question is allowed especially,
|
// not answering a question is allowed especially,
|
||||||
// nonetheless we want to still know this state in the backend!
|
// nonetheless we want to still know this state in the backend!
|
||||||
if (currentQuestion.value && currentQuestion.value.completion_status === "UNKNOWN") {
|
if (currentQuestion.value && currentQuestion.value.completion_status === "UNKNOWN") {
|
||||||
courseCompletionData.markCompletion(currentQuestion.value, "UNKNOWN");
|
markCompletion(currentQuestion.value, "UNKNOWN");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (questionIndex.value + 1 < numPages.value) {
|
if (questionIndex.value + 1 < numPages.value) {
|
||||||
|
|
@ -69,6 +82,19 @@ function handleContinue() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function markCompletion(
|
||||||
|
question: LearningUnitPerformanceCriteria,
|
||||||
|
status: CourseCompletionStatus
|
||||||
|
) {
|
||||||
|
if (isReadOnly.value) {
|
||||||
|
log.debug("We are in read only mode, so we do not mark the completion");
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
log.debug("markCompletion", question, status);
|
||||||
|
courseCompletionData.markCompletion(question, status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleBack() {
|
function handleBack() {
|
||||||
log.debug("handleBack");
|
log.debug("handleBack");
|
||||||
if (questionIndex.value > 0 && questionIndex.value < numPages.value) {
|
if (questionIndex.value > 0 && questionIndex.value < numPages.value) {
|
||||||
|
|
@ -105,7 +131,9 @@ onUnmounted(() => {
|
||||||
: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'"
|
:close-button-variant="
|
||||||
|
learningUnitHasFeedbackPage || isReadOnly ? 'close' : 'mark_as_done'
|
||||||
|
"
|
||||||
:end-badge-text="
|
:end-badge-text="
|
||||||
learningUnitHasFeedbackPage ? $t('general.submission') : undefined
|
learningUnitHasFeedbackPage ? $t('general.submission') : undefined
|
||||||
"
|
"
|
||||||
|
|
@ -122,13 +150,14 @@ onUnmounted(() => {
|
||||||
class="mt-4 flex flex-col justify-between gap-8 lg:mt-8 lg:flex-row lg:gap-12"
|
class="mt-4 flex flex-col justify-between gap-8 lg:mt-8 lg:flex-row lg:gap-12"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
|
:disabled="isReadOnly"
|
||||||
class="inline-flex flex-1 items-center border p-4 text-left"
|
class="inline-flex flex-1 items-center border p-4 text-left"
|
||||||
:class="{
|
:class="{
|
||||||
'border-green-500': currentQuestion.completion_status === 'SUCCESS',
|
'border-green-500': currentQuestion.completion_status === 'SUCCESS',
|
||||||
'border-2': currentQuestion.completion_status === 'SUCCESS',
|
'border-2': currentQuestion.completion_status === 'SUCCESS',
|
||||||
}"
|
}"
|
||||||
data-cy="success"
|
data-cy="success"
|
||||||
@click="courseCompletionData.markCompletion(currentQuestion, 'SUCCESS')"
|
@click="markCompletion(currentQuestion, 'SUCCESS')"
|
||||||
>
|
>
|
||||||
<it-icon-smiley-happy class="mr-4 h-16 w-16"></it-icon-smiley-happy>
|
<it-icon-smiley-happy class="mr-4 h-16 w-16"></it-icon-smiley-happy>
|
||||||
<span class="text-large font-bold">
|
<span class="text-large font-bold">
|
||||||
|
|
@ -136,13 +165,14 @@ onUnmounted(() => {
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
:disabled="isReadOnly"
|
||||||
class="inline-flex flex-1 items-center border p-4 text-left"
|
class="inline-flex flex-1 items-center border p-4 text-left"
|
||||||
:class="{
|
:class="{
|
||||||
'border-orange-500': currentQuestion.completion_status === 'FAIL',
|
'border-orange-500': currentQuestion.completion_status === 'FAIL',
|
||||||
'border-2': currentQuestion.completion_status === 'FAIL',
|
'border-2': currentQuestion.completion_status === 'FAIL',
|
||||||
}"
|
}"
|
||||||
data-cy="fail"
|
data-cy="fail"
|
||||||
@click="courseCompletionData.markCompletion(currentQuestion, 'FAIL')"
|
@click="markCompletion(currentQuestion, 'FAIL')"
|
||||||
>
|
>
|
||||||
<it-icon-smiley-thinking
|
<it-icon-smiley-thinking
|
||||||
class="mr-4 h-16 w-16"
|
class="mr-4 h-16 w-16"
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ const feedbackProviderName = computed(() => {
|
||||||
<b>{{ props.summary.title }}</b>
|
<b>{{ props.summary.title }}</b>
|
||||||
<span>Circle «{{ props.summary.circle_title }}»</span>
|
<span>Circle «{{ props.summary.circle_title }}»</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="underline">
|
<span class="pl-4 underline">
|
||||||
<router-link
|
<router-link
|
||||||
:to="props.summary.detail_url"
|
:to="props.summary.detail_url"
|
||||||
:data-cy="`self-eval-${summary.id}-detail-url`"
|
:data-cy="`self-eval-${summary.id}-detail-url`"
|
||||||
|
|
@ -47,21 +47,17 @@ const feedbackProviderName = computed(() => {
|
||||||
<div class="w-1/2">
|
<div class="w-1/2">
|
||||||
{{ $t("a.Deine Selbsteinschätzung") }}
|
{{ $t("a.Deine Selbsteinschätzung") }}
|
||||||
</div>
|
</div>
|
||||||
<div class="cell">
|
<div class="smily-row">
|
||||||
<SmileyCell
|
<SmileyCell
|
||||||
:count="props.summary.self_assessment.counts.pass"
|
:count="props.summary.self_assessment.counts.pass"
|
||||||
:cypress-identifier="`self-eval-${props.summary.id}-pass`"
|
:cypress-identifier="`self-eval-${props.summary.id}-pass`"
|
||||||
smiley="it-icon-smiley-happy"
|
smiley="it-icon-smiley-happy"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<div class="cell">
|
|
||||||
<SmileyCell
|
<SmileyCell
|
||||||
:count="props.summary.self_assessment.counts.fail"
|
:count="props.summary.self_assessment.counts.fail"
|
||||||
:cypress-identifier="`self-eval-${props.summary.id}-fail`"
|
:cypress-identifier="`self-eval-${props.summary.id}-fail`"
|
||||||
smiley="it-icon-smiley-thinking"
|
smiley="it-icon-smiley-thinking"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<div class="cell">
|
|
||||||
<SmileyCell
|
<SmileyCell
|
||||||
:count="props.summary.self_assessment.counts.unknown"
|
:count="props.summary.self_assessment.counts.unknown"
|
||||||
:cypress-identifier="`self-eval-${props.summary.id}-unknown`"
|
:cypress-identifier="`self-eval-${props.summary.id}-unknown`"
|
||||||
|
|
@ -82,19 +78,15 @@ const feedbackProviderName = computed(() => {
|
||||||
</span>
|
</span>
|
||||||
<img class="ml-2 h-7 w-7 rounded-full" :src="feedbackProviderAvatar" />
|
<img class="ml-2 h-7 w-7 rounded-full" :src="feedbackProviderAvatar" />
|
||||||
</div>
|
</div>
|
||||||
<div class="cell">
|
<div class="smily-row">
|
||||||
<SmileyCell
|
<SmileyCell
|
||||||
:count="props.summary.feedback_assessment?.counts.pass ?? 0"
|
:count="props.summary.feedback_assessment?.counts.pass ?? 0"
|
||||||
smiley="it-icon-smiley-happy"
|
smiley="it-icon-smiley-happy"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<div class="cell">
|
|
||||||
<SmileyCell
|
<SmileyCell
|
||||||
:count="props.summary.feedback_assessment?.counts.fail ?? 0"
|
:count="props.summary.feedback_assessment?.counts.fail ?? 0"
|
||||||
smiley="it-icon-smiley-thinking"
|
smiley="it-icon-smiley-thinking"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<div class="cell">
|
|
||||||
<SmileyCell
|
<SmileyCell
|
||||||
:count="props.summary.feedback_assessment?.counts.unknown ?? 0"
|
:count="props.summary.feedback_assessment?.counts.unknown ?? 0"
|
||||||
smiley="it-icon-smiley-neutral"
|
smiley="it-icon-smiley-neutral"
|
||||||
|
|
@ -107,7 +99,7 @@ const feedbackProviderName = computed(() => {
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="postcss" scoped>
|
<style lang="postcss" scoped>
|
||||||
.cell {
|
.smily-row {
|
||||||
@apply w-12;
|
@apply flex flex-1 justify-end pl-8 lg:justify-start;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
<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 props = defineProps<{
|
||||||
|
profileUserId: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const courseSession = useCurrentCourseSession();
|
||||||
|
const selfEvaluationFeedbackSummaries = useSelfEvaluationFeedbackSummaries(
|
||||||
|
courseSession.value.id,
|
||||||
|
props.profileUserId
|
||||||
|
);
|
||||||
|
|
||||||
|
const course = computed(() => courseSession.value.course);
|
||||||
|
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(() => {
|
||||||
|
if (course.value.configuration.enable_learning_mentor) {
|
||||||
|
return t("a.Selbst- und Fremdeinschätzungen");
|
||||||
|
} else {
|
||||||
|
return t("a.Selbsteinschätzungen");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="isLoaded">
|
||||||
|
<div>
|
||||||
|
<div class="flex flex-col items-center justify-between pb-4 md:flex-row">
|
||||||
|
<h2 class="py-4">{{ headerTitle }}</h2>
|
||||||
|
<ItDropdownSelect
|
||||||
|
v-model="selectedCircle"
|
||||||
|
class="text-bold w-full min-w-[18rem] border-2 border-gray-300 md:w-24"
|
||||||
|
: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>
|
||||||
|
|
@ -0,0 +1,131 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from "vue";
|
||||||
|
import { useSelfEvaluationFeedbackSummaries } from "@/services/selfEvaluationFeedback";
|
||||||
|
import { useCurrentCourseSession } from "@/composables";
|
||||||
|
|
||||||
|
const emit = defineEmits(["showAll"]);
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
profileUserId: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const courseSession = useCurrentCourseSession();
|
||||||
|
|
||||||
|
const selfEvaluationFeedbackSummaries = useSelfEvaluationFeedbackSummaries(
|
||||||
|
useCurrentCourseSession().value.id,
|
||||||
|
props.profileUserId
|
||||||
|
);
|
||||||
|
|
||||||
|
const selfAssessmentCounts = computed(
|
||||||
|
() => selfEvaluationFeedbackSummaries.aggregates.value?.self_assessment
|
||||||
|
);
|
||||||
|
|
||||||
|
const feedbackEvaluationCounts = computed(
|
||||||
|
() => selfEvaluationFeedbackSummaries.aggregates.value?.feedback_assessment
|
||||||
|
);
|
||||||
|
|
||||||
|
const isLoaded = computed(() => !selfEvaluationFeedbackSummaries.loading.value);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<template v-if="isLoaded">
|
||||||
|
<!-- Self Evaluation -->
|
||||||
|
<div class="bg-white px-8 py-4 lg:mb-8 lg:py-8">
|
||||||
|
<div class="mb-8">
|
||||||
|
<h3 class="mb-4 pb-4 lg:pb-0">
|
||||||
|
{{ $t("a.Selbsteinschätzungen") }}
|
||||||
|
</h3>
|
||||||
|
<ul
|
||||||
|
class="mb-6 flex flex-col lg:flex-row lg:items-center lg:justify-between lg:gap-8"
|
||||||
|
>
|
||||||
|
<li class="mb-4 inline-block flex-1 pb-4 lg:mb-0 lg:w-1/3 lg:pb-0">
|
||||||
|
<h5 class="mb-4 text-gray-700">«{{ $t("selfEvaluation.no") }}»</h5>
|
||||||
|
<div class="flex flex-row items-center">
|
||||||
|
<it-icon-smiley-thinking class="h-16 w-16"></it-icon-smiley-thinking>
|
||||||
|
<p
|
||||||
|
class="ml-4 inline-block text-7xl font-bold"
|
||||||
|
data-cy="self-evaluation-fail"
|
||||||
|
>
|
||||||
|
{{ selfAssessmentCounts?.fail }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="mb-4 inline-block flex-1 pb-4 lg:mb-0 lg:w-1/3 lg:pb-0">
|
||||||
|
<h5 class="mb-4 text-gray-700">«{{ $t("selfEvaluation.yes") }}»</h5>
|
||||||
|
<div class="flex flex-row items-center">
|
||||||
|
<it-icon-smiley-happy class="h-16 w-16"></it-icon-smiley-happy>
|
||||||
|
<p
|
||||||
|
class="ml-4 inline-block text-7xl font-bold"
|
||||||
|
data-cy="self-evaluation-success"
|
||||||
|
>
|
||||||
|
{{ selfAssessmentCounts?.pass }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="flex-1 pb-4 lg:mb-0 lg:w-1/3 lg:pb-0">
|
||||||
|
<h5 class="mb-4 text-gray-700">{{ $t("competences.notAssessed") }}</h5>
|
||||||
|
<div class="flex flex-row items-center">
|
||||||
|
<it-icon-smiley-neutral class="h-16 w-16"></it-icon-smiley-neutral>
|
||||||
|
<p
|
||||||
|
class="ml-4 inline-block text-7xl font-bold"
|
||||||
|
data-cy="self-evaluation-unknown"
|
||||||
|
>
|
||||||
|
{{ selfAssessmentCounts?.unknown }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<!-- Feedback Evaluation -->
|
||||||
|
<div
|
||||||
|
v-if="courseSession.course.configuration.enable_learning_mentor"
|
||||||
|
class="border-t pt-8"
|
||||||
|
>
|
||||||
|
<h3 class="mb-4 pb-4 lg:pb-0">
|
||||||
|
{{ $t("a.Fremdeinschätzungen") }}
|
||||||
|
</h3>
|
||||||
|
<ul
|
||||||
|
class="mb-6 flex flex-col lg:flex-row lg:items-center lg:justify-between lg:gap-8"
|
||||||
|
>
|
||||||
|
<li class="mb-4 inline-block flex-1 pb-4 lg:mb-0 lg:w-1/3 lg:pb-0">
|
||||||
|
<h5 class="mb-4 text-gray-700">«{{ $t("receivedEvaluation.no") }}»</h5>
|
||||||
|
<div class="flex flex-row items-center">
|
||||||
|
<it-icon-smiley-thinking class="h-16 w-16"></it-icon-smiley-thinking>
|
||||||
|
<p class="ml-4 inline-block text-7xl font-bold">
|
||||||
|
{{ feedbackEvaluationCounts?.fail }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="mb-4 inline-block flex-1 pb-4 lg:mb-0 lg:w-1/3 lg:pb-0">
|
||||||
|
<h5 class="mb-4 text-gray-700">«{{ $t("receivedEvaluation.yes") }}»</h5>
|
||||||
|
<div class="flex flex-row items-center">
|
||||||
|
<it-icon-smiley-happy class="h-16 w-16"></it-icon-smiley-happy>
|
||||||
|
<p class="ml-4 inline-block text-7xl font-bold">
|
||||||
|
{{ feedbackEvaluationCounts?.pass }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="flex-1 pb-4 lg:mb-0 lg:w-1/3 lg:pb-0">
|
||||||
|
<h5 class="mb-4 text-gray-700">{{ $t("competences.notAssessed") }}</h5>
|
||||||
|
<div class="flex flex-row items-center">
|
||||||
|
<it-icon-smiley-neutral class="h-16 w-16"></it-icon-smiley-neutral>
|
||||||
|
<p class="ml-4 inline-block text-7xl font-bold">
|
||||||
|
{{ feedbackEvaluationCounts?.unknown }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<!-- Show All (always)-->
|
||||||
|
<button
|
||||||
|
class="btn-text inline-flex items-center py-2 pl-0 pt-8"
|
||||||
|
@click="emit('showAll')"
|
||||||
|
>
|
||||||
|
<span>{{ $t("general.showAll") }}</span>
|
||||||
|
<it-icon-arrow-right></it-icon-arrow-right>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
|
|
@ -14,6 +14,7 @@ const { t } = useTranslation();
|
||||||
|
|
||||||
const pages = ref([
|
const pages = ref([
|
||||||
{ label: t("general.learningPath"), route: "cockpitProfileLearningPath" },
|
{ label: t("general.learningPath"), route: "cockpitProfileLearningPath" },
|
||||||
|
{ label: t("a.KompetenzNavi"), route: "cockpitProfileCompetence" },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const courseSession = useCurrentCourseSession();
|
const courseSession = useCurrentCourseSession();
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import CockpitProfileContent from "@/components/cockpit/profile/CockpitProfileContent.vue";
|
||||||
|
import { ref } from "vue";
|
||||||
|
import SelfEvaluationAndFeedbackList from "@/components/selfEvaluationFeedback/SelfEvaluationAndFeedbackList.vue";
|
||||||
|
import SelfEvaluationAndFeedbackOverview from "@/components/selfEvaluationFeedback/SelfEvaluationAndFeedbackOverview.vue";
|
||||||
|
import { useCurrentCourseSession } from "@/composables";
|
||||||
|
|
||||||
|
type SubMenuType = "OVERVIEW" | "DETAILS";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
userId: string;
|
||||||
|
courseSlug: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
interface SubMenuItem {
|
||||||
|
type: SubMenuType;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MENU_ENTRIES: SubMenuItem[] = [
|
||||||
|
{ type: "OVERVIEW", label: "a.Übersicht" },
|
||||||
|
{
|
||||||
|
type: "DETAILS",
|
||||||
|
label: useCurrentCourseSession().value.course.configuration.enable_learning_mentor
|
||||||
|
? "a.Selbst- und Fremdeinschätzungen"
|
||||||
|
: "a.Selbsteinschätzungen",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const active = ref<SubMenuItem>(MENU_ENTRIES[0]);
|
||||||
|
const selectDetails = () => {
|
||||||
|
active.value = MENU_ENTRIES[1];
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CockpitProfileContent>
|
||||||
|
<template #side>
|
||||||
|
<div v-for="(entry, index) in MENU_ENTRIES" :key="index" class="mb-2">
|
||||||
|
<button
|
||||||
|
class="flex w-full items-center space-x-2 p-2 pr-4 text-blue-900 hover:bg-gray-200 lg:pr-8"
|
||||||
|
:class="{ 'text-bold bg-gray-200': active.type === entry.type }"
|
||||||
|
@click="active = entry"
|
||||||
|
>
|
||||||
|
<span>{{ $t(entry.label) }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #main>
|
||||||
|
<div class="container-large">
|
||||||
|
<SelfEvaluationAndFeedbackOverview
|
||||||
|
v-if="active.type === 'OVERVIEW'"
|
||||||
|
:profile-user-id="props.userId"
|
||||||
|
@show-all="selectDetails"
|
||||||
|
/>
|
||||||
|
<SelfEvaluationAndFeedbackList
|
||||||
|
v-else-if="active.type === 'DETAILS'"
|
||||||
|
class="w-full"
|
||||||
|
:profile-user-id="props.userId"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</CockpitProfileContent>
|
||||||
|
</template>
|
||||||
|
|
@ -10,8 +10,10 @@ import {
|
||||||
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 SelfEvaluationAndFeedbackOverview from "@/components/selfEvaluationFeedback/SelfEvaluationAndFeedbackOverview.vue";
|
||||||
|
import { useUserStore } from "@/stores/user";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
courseSlug: string;
|
courseSlug: string;
|
||||||
|
|
@ -48,24 +50,11 @@ const userPointsEvaluatedAssignments = computed(() => {
|
||||||
return assignmentsUserPoints(allAssignments.value);
|
return assignmentsUserPoints(allAssignments.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
const selfEvaluationFeedbackSummaries = useSelfEvaluationFeedbackSummaries(
|
|
||||||
useCurrentCourseSession().value.id
|
|
||||||
);
|
|
||||||
|
|
||||||
const selfAssessmentCounts = computed(
|
|
||||||
() => selfEvaluationFeedbackSummaries.aggregates.value?.self_assessment
|
|
||||||
);
|
|
||||||
|
|
||||||
const feedbackEvaluationCounts = computed(
|
|
||||||
() => selfEvaluationFeedbackSummaries.aggregates.value?.feedback_assessment
|
|
||||||
);
|
|
||||||
|
|
||||||
const currentCourseSession = useCurrentCourseSession();
|
const currentCourseSession = useCurrentCourseSession();
|
||||||
|
|
||||||
const isLoaded = computed(
|
const isLoaded = computed(() => !certificatesQuery.fetching.value);
|
||||||
() =>
|
|
||||||
!selfEvaluationFeedbackSummaries.loading.value && !certificatesQuery.fetching.value
|
const router = useRouter();
|
||||||
);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -145,106 +134,14 @@ const isLoaded = computed(
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
<SelfEvaluationAndFeedbackOverview
|
||||||
<!-- Self-evaluation -->
|
:profile-user-id="useUserStore().id"
|
||||||
<section class="mb-4 bg-white px-8 py-4 lg:mb-8 lg:py-8">
|
@show-all="
|
||||||
<div class="mb-8">
|
() => {
|
||||||
<h3 class="mb-4 pb-4 lg:pb-0">
|
router.push({ name: 'selfEvaluationAndFeedback' });
|
||||||
{{ $t("a.Selbsteinschätzungen") }}
|
}
|
||||||
</h3>
|
"
|
||||||
<ul
|
/>
|
||||||
class="mb-6 flex flex-col lg:flex-row lg:items-center lg:justify-between lg:gap-8"
|
|
||||||
>
|
|
||||||
<li class="mb-4 inline-block flex-1 pb-4 lg:mb-0 lg:w-1/3 lg:pb-0">
|
|
||||||
<h5 class="mb-4 text-gray-700">«{{ $t("selfEvaluation.no") }}»</h5>
|
|
||||||
<div class="flex flex-row items-center">
|
|
||||||
<it-icon-smiley-thinking class="h-16 w-16"></it-icon-smiley-thinking>
|
|
||||||
<p
|
|
||||||
class="ml-4 inline-block text-7xl font-bold"
|
|
||||||
data-cy="self-evaluation-fail"
|
|
||||||
>
|
|
||||||
{{ selfAssessmentCounts?.fail }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li class="mb-4 inline-block flex-1 pb-4 lg:mb-0 lg:w-1/3 lg:pb-0">
|
|
||||||
<h5 class="mb-4 text-gray-700">«{{ $t("selfEvaluation.yes") }}»</h5>
|
|
||||||
<div class="flex flex-row items-center">
|
|
||||||
<it-icon-smiley-happy class="h-16 w-16"></it-icon-smiley-happy>
|
|
||||||
<p
|
|
||||||
class="ml-4 inline-block text-7xl font-bold"
|
|
||||||
data-cy="self-evaluation-success"
|
|
||||||
>
|
|
||||||
{{ selfAssessmentCounts?.pass }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li class="flex-1 pb-4 lg:mb-0 lg:w-1/3 lg:pb-0">
|
|
||||||
<h5 class="mb-4 text-gray-700">{{ $t("competences.notAssessed") }}</h5>
|
|
||||||
<div class="flex flex-row items-center">
|
|
||||||
<it-icon-smiley-neutral class="h-16 w-16"></it-icon-smiley-neutral>
|
|
||||||
<p
|
|
||||||
class="ml-4 inline-block text-7xl font-bold"
|
|
||||||
data-cy="self-evaluation-unknown"
|
|
||||||
>
|
|
||||||
{{ selfAssessmentCounts?.unknown }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Feedback evaluation -->
|
|
||||||
<div
|
|
||||||
v-if="courseSession.course.configuration.enable_learning_mentor"
|
|
||||||
class="mb-8 border-t pt-8"
|
|
||||||
>
|
|
||||||
<h3 class="mb-4 pb-4 lg:pb-0">
|
|
||||||
{{ $t("a.Fremdeinschätzungen") }}
|
|
||||||
</h3>
|
|
||||||
<ul
|
|
||||||
class="mb-6 flex flex-col lg:flex-row lg:items-center lg:justify-between lg:gap-8"
|
|
||||||
>
|
|
||||||
<li class="mb-4 inline-block flex-1 pb-4 lg:mb-0 lg:w-1/3 lg:pb-0">
|
|
||||||
<h5 class="mb-4 text-gray-700">«{{ $t("receivedEvaluation.no") }}»</h5>
|
|
||||||
<div class="flex flex-row items-center">
|
|
||||||
<it-icon-smiley-thinking class="h-16 w-16"></it-icon-smiley-thinking>
|
|
||||||
<p class="ml-4 inline-block text-7xl font-bold">
|
|
||||||
{{ feedbackEvaluationCounts?.fail }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li class="mb-4 inline-block flex-1 pb-4 lg:mb-0 lg:w-1/3 lg:pb-0">
|
|
||||||
<h5 class="mb-4 text-gray-700">«{{ $t("receivedEvaluation.yes") }}»</h5>
|
|
||||||
<div class="flex flex-row items-center">
|
|
||||||
<it-icon-smiley-happy class="h-16 w-16"></it-icon-smiley-happy>
|
|
||||||
<p class="ml-4 inline-block text-7xl font-bold">
|
|
||||||
{{ feedbackEvaluationCounts?.pass }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li class="flex-1 pb-4 lg:mb-0 lg:w-1/3 lg:pb-0">
|
|
||||||
<h5 class="mb-4 text-gray-700">{{ $t("competences.notAssessed") }}</h5>
|
|
||||||
<div class="flex flex-row items-center">
|
|
||||||
<it-icon-smiley-neutral class="h-16 w-16"></it-icon-smiley-neutral>
|
|
||||||
<p class="ml-4 inline-block text-7xl font-bold">
|
|
||||||
{{ feedbackEvaluationCounts?.unknown }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<router-link
|
|
||||||
:to="`/course/${props.courseSlug}/competence/self-evaluation-and-feedback`"
|
|
||||||
class="btn-text inline-flex items-center py-2 pl-0"
|
|
||||||
>
|
|
||||||
<span>{{ $t("general.showAll") }}</span>
|
|
||||||
<it-icon-arrow-right></it-icon-arrow-right>
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,69 +1,12 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useSelfEvaluationFeedbackSummaries } from "@/services/selfEvaluationFeedback";
|
import { useUserStore } from "@/stores/user";
|
||||||
import { useCurrentCourseSession } from "@/composables";
|
import SelfEvaluationAndFeedbackList from "@/components/selfEvaluationFeedback/SelfEvaluationAndFeedbackList.vue";
|
||||||
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(
|
const userId = useUserStore().id;
|
||||||
useCurrentCourseSession().value.id
|
|
||||||
);
|
|
||||||
|
|
||||||
const courseSession = useCurrentCourseSession();
|
|
||||||
const course = computed(() => courseSession.value.course);
|
|
||||||
|
|
||||||
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(() => {
|
|
||||||
if (course.value.configuration.enable_learning_mentor) {
|
|
||||||
return t("a.Selbst- und Fremdeinschätzungen");
|
|
||||||
} else {
|
|
||||||
return t("a.Selbsteinschätzungen");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="isLoaded">
|
<SelfEvaluationAndFeedbackList :profile-user-id="userId" class="container-large" />
|
||||||
<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>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,7 @@ const router = createRouter({
|
||||||
import("@/pages/competence/CompetenceCertificateDetailPage.vue"),
|
import("@/pages/competence/CompetenceCertificateDetailPage.vue"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
name: "selfEvaluationAndFeedback",
|
||||||
path: "self-evaluation-and-feedback",
|
path: "self-evaluation-and-feedback",
|
||||||
props: true,
|
props: true,
|
||||||
component: () =>
|
component: () =>
|
||||||
|
|
@ -257,6 +258,13 @@ const router = createRouter({
|
||||||
props: true,
|
props: true,
|
||||||
name: "cockpitProfileLearningPath",
|
name: "cockpitProfileLearningPath",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "competence",
|
||||||
|
component: () =>
|
||||||
|
import("@/pages/cockpit/profilePage/CompetenceProfilePage.vue"),
|
||||||
|
props: true,
|
||||||
|
name: "cockpitProfileCompetence",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -165,7 +165,8 @@ export function useSelfEvaluationFeedback(
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSelfEvaluationFeedbackSummaries(
|
export function useSelfEvaluationFeedbackSummaries(
|
||||||
courseSessionId: Ref<string> | string
|
courseSessionId: Ref<string> | string,
|
||||||
|
userId: Ref<string> | string
|
||||||
) {
|
) {
|
||||||
const summaries = ref<LearningUnitSummary[]>([]);
|
const summaries = ref<LearningUnitSummary[]>([]);
|
||||||
const aggregates = ref<FeedbackSummaryAggregates>();
|
const aggregates = ref<FeedbackSummaryAggregates>();
|
||||||
|
|
@ -175,7 +176,7 @@ export function useSelfEvaluationFeedbackSummaries(
|
||||||
|
|
||||||
const url = computed(
|
const url = computed(
|
||||||
() =>
|
() =>
|
||||||
`/api/self-evaluation-feedback/requester/${courseSessionId}/feedbacks/summaries`
|
`/api/self-evaluation-feedback/feedbacks/summaries/course-session/${courseSessionId}/user/${userId}`
|
||||||
);
|
);
|
||||||
|
|
||||||
const fetchFeedbackSummaries = async () => {
|
const fetchFeedbackSummaries = async () => {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { login } from "./helpers";
|
import {login} from "./helpers";
|
||||||
|
|
||||||
describe("circle.cy.js", () => {
|
describe("circle.cy.js", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|
@ -20,15 +20,21 @@ describe("circle.cy.js", () => {
|
||||||
"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();
|
// workaround for flaky test in CI:
|
||||||
cy.get("[data-cy=\"lc-title\"]").should(
|
// for some strange reason, locally we don't have to do this
|
||||||
|
// but in CI if we don't do this, the next click will not work...
|
||||||
|
// => it's not great but this workaround at least gives us a stable test
|
||||||
|
cy.visit("/course/test-lehrgang/learn/fahrzeug");
|
||||||
|
|
||||||
|
cy.get('[data-cy="ls-continue-button"]').click();
|
||||||
|
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(
|
||||||
|
|
@ -47,7 +53,7 @@ describe("circle.cy.js", () => {
|
||||||
"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();
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import uuid
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from rest_framework.decorators import api_view
|
from rest_framework.decorators import api_view
|
||||||
|
|
@ -18,6 +20,7 @@ from vbv_lernwelt.course_session_group.models import CourseSessionGroup
|
||||||
from vbv_lernwelt.files.models import UploadFile
|
from vbv_lernwelt.files.models import UploadFile
|
||||||
from vbv_lernwelt.files.services import FileDirectUploadService
|
from vbv_lernwelt.files.services import FileDirectUploadService
|
||||||
from vbv_lernwelt.iam.permissions import (
|
from vbv_lernwelt.iam.permissions import (
|
||||||
|
can_mark_course_completion,
|
||||||
can_view_course_completions,
|
can_view_course_completions,
|
||||||
course_sessions_for_user_qs,
|
course_sessions_for_user_qs,
|
||||||
has_course_access,
|
has_course_access,
|
||||||
|
|
@ -75,9 +78,13 @@ def request_course_completion(request, course_session_id):
|
||||||
|
|
||||||
|
|
||||||
@api_view(["GET"])
|
@api_view(["GET"])
|
||||||
def request_course_completion_for_user(request, course_session_id, user_id):
|
def request_course_completion_for_user(
|
||||||
|
request, course_session_id: int, user_id: uuid.UUID
|
||||||
|
):
|
||||||
if can_view_course_completions(
|
if can_view_course_completions(
|
||||||
user=request.user, course_session_id=course_session_id, target_user_id=user_id
|
user=request.user, # noqa
|
||||||
|
course_session_id=course_session_id,
|
||||||
|
target_user_id=str(user_id),
|
||||||
):
|
):
|
||||||
return _request_course_completion(course_session_id, user_id)
|
return _request_course_completion(course_session_id, user_id)
|
||||||
raise PermissionDenied()
|
raise PermissionDenied()
|
||||||
|
|
@ -91,7 +98,10 @@ def mark_course_completion_view(request):
|
||||||
course_session_id = request.data.get("course_session_id")
|
course_session_id = request.data.get("course_session_id")
|
||||||
page = Page.objects.get(id=page_id)
|
page = Page.objects.get(id=page_id)
|
||||||
|
|
||||||
if not has_course_access_by_page_request(request, page):
|
if not can_mark_course_completion(
|
||||||
|
user=request.user, # noqa
|
||||||
|
course_session_id=course_session_id,
|
||||||
|
):
|
||||||
raise PermissionDenied()
|
raise PermissionDenied()
|
||||||
|
|
||||||
mark_course_completion(
|
mark_course_completion(
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,10 @@ def has_course_access_by_page_request(request, obj):
|
||||||
return has_course_access(request.user, obj.specific.get_course().id)
|
return has_course_access(request.user, obj.specific.get_course().id)
|
||||||
|
|
||||||
|
|
||||||
|
def can_mark_course_completion(user: User, course_session_id: int) -> bool:
|
||||||
|
return is_course_session_member(user, course_session_id)
|
||||||
|
|
||||||
|
|
||||||
def has_course_access(user, course_id):
|
def has_course_access(user, course_id):
|
||||||
if user.is_superuser:
|
if user.is_superuser:
|
||||||
return True
|
return True
|
||||||
|
|
@ -209,7 +213,7 @@ def can_view_course_completions(
|
||||||
user: User, course_session_id: int, target_user_id: str
|
user: User, course_session_id: int, target_user_id: str
|
||||||
) -> bool:
|
) -> bool:
|
||||||
return (
|
return (
|
||||||
user.id == target_user_id
|
str(user.id) == target_user_id
|
||||||
or is_course_session_expert(user=user, course_session_id=course_session_id)
|
or is_course_session_expert(user=user, course_session_id=course_session_id)
|
||||||
or is_user_mentor(
|
or is_user_mentor(
|
||||||
mentor=user,
|
mentor=user,
|
||||||
|
|
|
||||||
|
|
@ -295,8 +295,8 @@ class SelfEvaluationFeedbackAPI(APITestCase):
|
||||||
# WHEN
|
# WHEN
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse(
|
reverse(
|
||||||
"get_self_evaluation_feedbacks_as_requester",
|
"get_course_session_user_feedback_summaries",
|
||||||
args=[self.course_session.id],
|
args=[self.course_session.id, self.member.id],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -358,8 +358,8 @@ class SelfEvaluationFeedbackAPI(APITestCase):
|
||||||
# WHEN
|
# WHEN
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse(
|
reverse(
|
||||||
"get_self_evaluation_feedbacks_as_requester",
|
"get_course_session_user_feedback_summaries",
|
||||||
args=[self.course_session.id],
|
args=[self.course_session.id, self.member.id],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -393,8 +393,8 @@ class SelfEvaluationFeedbackAPI(APITestCase):
|
||||||
# WHEN
|
# WHEN
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse(
|
reverse(
|
||||||
"get_self_evaluation_feedbacks_as_requester",
|
"get_course_session_user_feedback_summaries",
|
||||||
args=[self.course_session.id],
|
args=[self.course_session.id, self.member.id],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -406,6 +406,55 @@ class SelfEvaluationFeedbackAPI(APITestCase):
|
||||||
self.assertEqual(result["self_assessment"]["counts"]["fail"], 0)
|
self.assertEqual(result["self_assessment"]["counts"]["fail"], 0)
|
||||||
self.assertEqual(result["self_assessment"]["counts"]["unknown"], 1)
|
self.assertEqual(result["self_assessment"]["counts"]["unknown"], 1)
|
||||||
|
|
||||||
|
def test_permission_feedback_summaries(self):
|
||||||
|
# GIVEN
|
||||||
|
learning_unit = create_learning_unit( # noqa
|
||||||
|
course=self.course,
|
||||||
|
circle=self.circle,
|
||||||
|
)
|
||||||
|
|
||||||
|
create_performance_criteria_page(
|
||||||
|
course=self.course,
|
||||||
|
course_page=self.course_page,
|
||||||
|
circle=self.circle,
|
||||||
|
learning_unit=learning_unit,
|
||||||
|
)
|
||||||
|
|
||||||
|
expert = create_user("expert")
|
||||||
|
add_course_session_user(
|
||||||
|
course_session=self.course_session,
|
||||||
|
user=expert,
|
||||||
|
role=CourseSessionUser.Role.EXPERT,
|
||||||
|
)
|
||||||
|
|
||||||
|
fellow_member = create_user("fellow_member")
|
||||||
|
add_course_session_user(
|
||||||
|
course_session=self.course_session,
|
||||||
|
user=fellow_member,
|
||||||
|
role=CourseSessionUser.Role.MEMBER,
|
||||||
|
)
|
||||||
|
|
||||||
|
# WHEN / THEN
|
||||||
|
test_cases = [
|
||||||
|
# principle_user wants to access target_user
|
||||||
|
# -> expected_status_code
|
||||||
|
(self.member, self.member, 200),
|
||||||
|
(self.mentor, self.member, 200),
|
||||||
|
(expert, self.member, 200),
|
||||||
|
(fellow_member, self.member, 403),
|
||||||
|
]
|
||||||
|
|
||||||
|
for principle_user, target_user, expected_status_code in test_cases:
|
||||||
|
with self.subTest(principle_user=principle_user, target_user=target_user):
|
||||||
|
self.client.force_login(principle_user)
|
||||||
|
response = self.client.get(
|
||||||
|
reverse(
|
||||||
|
"get_course_session_user_feedback_summaries",
|
||||||
|
args=[self.course_session.id, target_user.id],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, expected_status_code)
|
||||||
|
|
||||||
def test_feedbacks_metadata(self):
|
def test_feedbacks_metadata(self):
|
||||||
# GIVEN
|
# GIVEN
|
||||||
learning_unit = create_learning_unit( # noqa
|
learning_unit = create_learning_unit( # noqa
|
||||||
|
|
@ -425,8 +474,8 @@ class SelfEvaluationFeedbackAPI(APITestCase):
|
||||||
# WHEN
|
# WHEN
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse(
|
reverse(
|
||||||
"get_self_evaluation_feedbacks_as_requester",
|
"get_course_session_user_feedback_summaries",
|
||||||
args=[self.course_session.id],
|
args=[self.course_session.id, self.member.id],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,20 +2,15 @@ from django.urls import path
|
||||||
|
|
||||||
from vbv_lernwelt.self_evaluation_feedback.views import (
|
from vbv_lernwelt.self_evaluation_feedback.views import (
|
||||||
add_provider_self_evaluation_feedback,
|
add_provider_self_evaluation_feedback,
|
||||||
|
get_course_session_user_feedback_summaries,
|
||||||
get_self_evaluation_feedback_as_provider,
|
get_self_evaluation_feedback_as_provider,
|
||||||
get_self_evaluation_feedback_as_requester,
|
get_self_evaluation_feedback_as_requester,
|
||||||
get_self_evaluation_feedbacks_as_requester,
|
|
||||||
release_provider_self_evaluation_feedback,
|
release_provider_self_evaluation_feedback,
|
||||||
start_self_evaluation_feedback,
|
start_self_evaluation_feedback,
|
||||||
)
|
)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# /requester/* URLs -> For the user who requests feedback
|
# /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(
|
path(
|
||||||
"requester/<int:learning_unit_id>/feedback/start",
|
"requester/<int:learning_unit_id>/feedback/start",
|
||||||
start_self_evaluation_feedback,
|
start_self_evaluation_feedback,
|
||||||
|
|
@ -42,4 +37,11 @@ urlpatterns = [
|
||||||
add_provider_self_evaluation_feedback,
|
add_provider_self_evaluation_feedback,
|
||||||
name="add_self_evaluation_feedback_assessment",
|
name="add_self_evaluation_feedback_assessment",
|
||||||
),
|
),
|
||||||
|
# route to get feedback summaries for a user in a course session
|
||||||
|
# used by different roles to retrieve feedback summaries for a user
|
||||||
|
path(
|
||||||
|
"feedbacks/summaries/course-session/<signed_int:course_session_id>/user/<uuid:user_id>",
|
||||||
|
get_course_session_user_feedback_summaries,
|
||||||
|
name="get_course_session_user_feedback_summaries",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import uuid
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from rest_framework.decorators import api_view, permission_classes
|
from rest_framework.decorators import api_view, permission_classes
|
||||||
|
|
@ -8,6 +10,7 @@ from rest_framework.response import Response
|
||||||
from vbv_lernwelt.core.models import User
|
from vbv_lernwelt.core.models import User
|
||||||
from vbv_lernwelt.core.serializers import UserSerializer
|
from vbv_lernwelt.core.serializers import UserSerializer
|
||||||
from vbv_lernwelt.course.models import CourseCompletion, CourseSession
|
from vbv_lernwelt.course.models import CourseCompletion, CourseSession
|
||||||
|
from vbv_lernwelt.iam.permissions import can_view_course_completions
|
||||||
from vbv_lernwelt.learning_mentor.models import LearningMentor
|
from vbv_lernwelt.learning_mentor.models import LearningMentor
|
||||||
from vbv_lernwelt.learnpath.models import Circle, LearningUnit
|
from vbv_lernwelt.learnpath.models import Circle, LearningUnit
|
||||||
from vbv_lernwelt.notify.services import NotificationService
|
from vbv_lernwelt.notify.services import NotificationService
|
||||||
|
|
@ -91,8 +94,18 @@ def get_self_evaluation_feedback_as_provider(request, learning_unit_id):
|
||||||
|
|
||||||
@api_view(["GET"])
|
@api_view(["GET"])
|
||||||
@permission_classes([IsAuthenticated])
|
@permission_classes([IsAuthenticated])
|
||||||
def get_self_evaluation_feedbacks_as_requester(request, course_session_id: int):
|
def get_course_session_user_feedback_summaries(
|
||||||
|
request, course_session_id: int, user_id: uuid.UUID
|
||||||
|
):
|
||||||
course_session = get_object_or_404(CourseSession, id=course_session_id)
|
course_session = get_object_or_404(CourseSession, id=course_session_id)
|
||||||
|
user_to_lookup = get_object_or_404(User, id=user_id)
|
||||||
|
|
||||||
|
if not can_view_course_completions(
|
||||||
|
user=request.user, # noqa
|
||||||
|
course_session_id=course_session_id,
|
||||||
|
target_user_id=str(user_id),
|
||||||
|
):
|
||||||
|
raise PermissionDenied()
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
circle_ids = set()
|
circle_ids = set()
|
||||||
|
|
@ -114,7 +127,7 @@ def get_self_evaluation_feedbacks_as_requester(request, course_session_id: int):
|
||||||
|
|
||||||
feedback = SelfEvaluationFeedback.objects.filter(
|
feedback = SelfEvaluationFeedback.objects.filter(
|
||||||
learning_unit=learning_unit,
|
learning_unit=learning_unit,
|
||||||
feedback_requester_user=request.user,
|
feedback_requester_user=user_to_lookup,
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
if not feedback:
|
if not feedback:
|
||||||
|
|
@ -135,7 +148,9 @@ def get_self_evaluation_feedbacks_as_requester(request, course_session_id: int):
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
self_assessment_counts = get_self_assessment_counts(learning_unit, request.user)
|
self_assessment_counts = get_self_assessment_counts(
|
||||||
|
learning_unit=learning_unit, user=user_to_lookup
|
||||||
|
)
|
||||||
all_self_assessment_counts.append(self_assessment_counts)
|
all_self_assessment_counts.append(self_assessment_counts)
|
||||||
|
|
||||||
results.append(
|
results.append(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue