Add basic Feedback page

This commit is contained in:
Christian Cueni 2023-01-26 13:39:34 +01:00
parent 44ed154814
commit ec58ca176c
13 changed files with 324 additions and 160 deletions

View File

@ -1,35 +1,36 @@
<template> <template>
<div class="px-6 py-5 bg-white mb-4"> <div class="mb-4 bg-white px-6 py-5">
<h2 class="bg-feedback bg-no-repeat pl-[68px] heading-3 bg-60 leading-[60px] mb-4">{{ $t("general.feedback", 2) }}</h2> <h2 class="heading-3 mb-4 bg-feedback bg-60 bg-no-repeat pl-[68px] leading-[60px]">
{{ $t("general.feedback", 2) }}
</h2>
<ol> <ol>
<ItRow <ItRow v-for="feedbacks in feedbackSummary" :key="feedbacks.circle_id">
v-for="feedbacks in feedbackSummary" <template #firstRow>
:key="feedbacks.circle_id" <span class="text-bold">{{ $t("feedback.circleFeedback") }}</span>
> </template>
<template #firstRow><span class="text-bold">{{ $t("feedback.circleFeedback") }}</span></template> <template #center>
<template #center> <div class="flex w-full justify-between">
<div class="flex justify-between w-full"> <div>Circle: {{ feedbacks.circle.title }}</div>
<div>Circle: {{ feedbacks.circle.title }}</div> <div>{{ $t("feedback.sentByUsers", feedbacks.count) }}</div>
<div>{{ $t("feedback.sentByUsers", feedbacks.count) }}</div> </div>
</div> </template>
</template> <template #link>
<template #link> <router-link
<router-link :to="`${url}/cockpit/feedback/${feedbacks.circle_id}`"
:to="`${url}/cockpit/feedback/`" class="w-full text-right underline"
class="underline w-full text-right" >
> {{ $t("feedback.showDetails") }}
{{ $t("feedback.showDetails") }} </router-link>
</router-link> </template>
</template> </ItRow>
</ItRow>
</ol> </ol>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, ref, watch } from 'vue';
import ItRow from "@/components/ui/ItRow.vue"; import ItRow from "@/components/ui/ItRow.vue";
import { itGet } from "@/fetchHelpers"; import { itGet } from "@/fetchHelpers";
import { onMounted, ref, watch } from "vue";
import type { Circle } from "@/services/circle"; import type { Circle } from "@/services/circle";
@ -42,15 +43,20 @@ interface FeedbackDisplaySummary extends FeedbackSummary {
circle: Circle; circle: Circle;
} }
function makeSummary(feedbackData: FeedbackSummary[], circles: Circle[], selectedCircles: string[]) { function makeSummary(
const summary: FeedbackDisplaySummary[] = circles.filter(circle => selectedCircles.includes(circle.translation_key)) feedbackData: FeedbackSummary[],
circles: Circle[],
selectedCircles: string[]
) {
const summary: FeedbackDisplaySummary[] = circles
.filter((circle) => selectedCircles.includes(circle.translation_key))
.reduce((acc: FeedbackDisplaySummary[], circle) => { .reduce((acc: FeedbackDisplaySummary[], circle) => {
const circleFeedbacks = feedbackData const circleFeedbacks = feedbackData
.filter(data => data.circle_id === circle.id) .filter((data) => data.circle_id === circle.id)
.map(data => Object.assign({}, data, { circle })); .map((data) => Object.assign({}, data, { circle }));
return acc.concat(circleFeedbacks); return acc.concat(circleFeedbacks);
}, []); }, []);
return summary; return summary;
} }
@ -66,12 +72,28 @@ let feedbackData: FeedbackSummary[] = [];
onMounted(async () => { onMounted(async () => {
feedbackData = await itGet(`/api/core/feedback/${props.courseId}/summary`); feedbackData = await itGet(`/api/core/feedback/${props.courseId}/summary`);
feedbackSummary.value = makeSummary(feedbackData, props.circles, props.selctedCircles); feedbackSummary.value = makeSummary(
}) feedbackData,
props.circles,
props.selctedCircles
);
});
watch(() => props, () => { watch(
if (feedbackData.length > 0 && props.circles.length > 0 && props.selctedCircles.length > 0) { () => props,
feedbackSummary.value = makeSummary(feedbackData, props.circles, props.selctedCircles); () => {
} if (
}, { deep: true }); feedbackData.length > 0 &&
props.circles.length > 0 &&
props.selctedCircles.length > 0
) {
feedbackSummary.value = makeSummary(
feedbackData,
props.circles,
props.selctedCircles
);
}
},
{ deep: true }
);
</script> </script>

View File

@ -10,7 +10,7 @@ const props = defineProps<{
<template> <template>
<ItRow> <ItRow>
<template #firstRow> <template #firstRow>
<img class="h-[45px] rounded-full mr-2" :src="avatarUrl" /> <img class="mr-2 h-[45px] rounded-full" :src="avatarUrl" />
<p class="text-bold lg:leading-[45px]">{{ name }}</p> <p class="text-bold lg:leading-[45px]">{{ name }}</p>
</template> </template>
<template #center> <template #center>

View File

@ -1,11 +1,11 @@
<template> <template>
<li <li
class="py-4 leading-[45px] border-t border-gray-500 flex flex-col lg:flex-row justify-between" class="flex flex-col justify-between border-t border-gray-500 py-4 leading-[45px] lg:flex-row"
> >
<div class="flex flex-row md:w-1/4 items-center"> <div class="flex flex-row items-center md:w-1/4">
<slot name="firstRow"></slot> <slot name="firstRow"></slot>
</div> </div>
<div class="flex-1 flex items-center"> <div class="flex flex-1 items-center">
<slot name="center"></slot> <slot name="center"></slot>
</div> </div>
<div class="flex items-center md:w-1/4"> <div class="flex items-center md:w-1/4">
@ -14,7 +14,6 @@
</li> </li>
</template> </template>
<script setup lang="ts"> <script setup lang="ts"></script>
</script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

View File

@ -1,14 +1,14 @@
<template> <template>
<QuestionSummary :title="props.title" :text="props.text"> <QuestionSummary :title="props.title" :text="props.text">
<div class="inline-grid grid-cols-[140px_auto] grid-rows-2 gap-x-2"> <div class="mb-6 inline-grid grid-cols-[140px_auto] grid-rows-2 gap-x-2">
<h4 class="text-xl font-bold">Durchschnitt:</h4> <h4 class="text-xl font-bold">{{ $t("feedback.average") }}:</h4>
<span <span
class="col-start-2 row-span-2 inline-flex h-9 w-11 items-center justify-center rounded text-xl" class="col-start-2 row-span-2 inline-flex h-9 w-11 items-center justify-center rounded text-xl font-bold"
:style="ratingValueStyle" :style="ratingValueStyle"
> >
{{ props.rating }} {{ rating }}
</span> </span>
<h5 class="text-base">{{ props.answers }} Antworten</h5> <h5 class="text-base">{{ answers }} {{ $t("feedback.answers") }}</h5>
</div> </div>
<div <div
class="relative grid grid-cols-3 pt-3 grid-areas-rating-scale-slim md:grid-cols-8 md:grid-areas-rating-scale" class="relative grid grid-cols-3 pt-3 grid-areas-rating-scale-slim md:grid-cols-8 md:grid-areas-rating-scale"
@ -25,7 +25,7 @@
></div> ></div>
</div> </div>
<div <div
v-for="legend in legends" v-for="(legend, i) of legends"
:key="legend.index" :key="legend.index"
:class="{ :class="{
'items-start grid-in-fst before:left-0 md:before:left-1/2': legend.index == 1, 'items-start grid-in-fst before:left-0 md:before:left-1/2': legend.index == 1,
@ -37,15 +37,34 @@
}" }"
class="legend relative inline-flex flex-col pt-4 before:absolute before:top-0 before:h-4 before:w-px before:bg-gray-500 md:items-center" class="legend relative inline-flex flex-col pt-4 before:absolute before:top-0 before:h-4 before:w-px before:bg-gray-500 md:items-center"
> >
<div <Popover class="relative">
:class="[{ hidden: legend.index === 3 || legend.index === 2 }, 'md:block']" <PopoverButton class="focus:outline-none">
> <div
{{ legend.index }} :class="[
</div> { hidden: legend.index === 3 || legend.index === 2 },
'md:block',
]"
>
{{ legend.index }}
</div>
<p :class="[{ hidden: legend.index === 3 || legend.index === 2 }, 'md:block']"> <p
{{ legend.label }} :class="[
</p> { hidden: legend.index === 3 || legend.index === 2 },
'md:block',
]"
>
{{ legend.label }}
</p>
</PopoverButton>
<PopoverPanel
class="absolute top-[-150%] z-10 w-[120px] border border-gray-500 bg-white p-1 text-left text-sm"
>
<p>
{{ `"${legend.label}" ${numberOfRatings[i]} ${$t("feedback.answers")}` }}
</p>
</PopoverPanel>
</Popover>
</div> </div>
</div> </div>
</QuestionSummary> </QuestionSummary>
@ -53,7 +72,11 @@
<script setup lang="ts"> <script setup lang="ts">
import QuestionSummary from "@/components/ui/QuestionSummary.vue"; import QuestionSummary from "@/components/ui/QuestionSummary.vue";
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/vue";
import { computed } from "vue"; import { computed } from "vue";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
type RGB = [number, number, number]; type RGB = [number, number, number];
const red: RGB = [221, 103, 81]; // red-600 const red: RGB = [221, 103, 81]; // red-600
@ -62,25 +85,37 @@ const lightGreen: RGB = [120, 222, 163]; // green-500
const darkGreen: RGB = [91, 183, 130]; // green-600 const darkGreen: RGB = [91, 183, 130]; // green-600
const legends = [ const legends = [
{ index: 1, label: "Sehr unzufrieden" }, { index: 1, label: t("feedback.veryUnhappy") },
{ index: 2, label: "Unzufrieden" }, { index: 2, label: t("feedback.unhappy") },
{ index: 3, label: "Zufrieden" }, { index: 3, label: t("feedback.happy") },
{ index: 4, label: "Sehr zufrieden" }, { index: 4, label: t("feedback.veryHappy") },
]; ];
const props = defineProps<{ const props = defineProps<{
rating: number; ratings: number[];
answers: number;
title: string; title: string;
text: string; text: string;
}>(); }>();
const rating = computed((): number => {
const sum = props.ratings.reduce((a, b) => a + b, 0);
return sum / props.ratings.length;
});
const weight = computed(() => { const weight = computed(() => {
return props.rating % 1; return rating.value % 1;
}); });
const scale = computed(() => { const scale = computed(() => {
return Math.floor(props.rating); return Math.floor(rating.value);
});
const answers = computed(() => props.ratings.length);
const numberOfRatings = computed(() => {
return legends.map(
(legend) => props.ratings.filter((rating) => rating === legend.index).length
);
}); });
const colors = computed(() => { const colors = computed(() => {

View File

@ -134,7 +134,16 @@
"feedbackSent": "Dein Feedback wurde abgeschickt", "feedbackSent": "Dein Feedback wurde abgeschickt",
"circleFeedback": "Feedback zum Circle", "circleFeedback": "Feedback zum Circle",
"showDetails": "Details anzeigen", "showDetails": "Details anzeigen",
"sentByUsers": "Von {count} Teilnehmern ausgefüllt" "sentByUsers": "Von {count} Teilnehmern ausgefüllt",
"feedbackPageTitle": "Feedback zum Lehrgang",
"feedbackPageInfo": "Teilnehmer haben das Feedback ausgefüllt",
"questionTitle": "Frage",
"veryUnhappy": "Sehr unzufrieden",
"unhappy": "Unzufrieden",
"happy": "Zufrieden",
"veryHappy": "Sehr zufrieden",
"average": "Durchschnitt",
"answers": "Antworten"
}, },
"constants": { "constants": {
"yes": "Ja", "yes": "Ja",

View File

@ -29,7 +29,8 @@ const app = createApp(App);
Sentry.init({ Sentry.init({
app, app,
environment: import.meta.env.VITE_SENTRY_ENV || "http://localhost:8000/server/graphql/", environment:
import.meta.env.VITE_SENTRY_ENV || "http://localhost:8000/server/graphql/",
dsn: "https://2df6096a4fd94bd6b4802124d10e4b8d@o8544.ingest.sentry.io/4504157846372352", dsn: "https://2df6096a4fd94bd6b4802124d10e4b8d@o8544.ingest.sentry.io/4504157846372352",
tracesSampleRate: 0.0, tracesSampleRate: 0.0,
enabled: enabled:

View File

@ -450,64 +450,12 @@ function log(data: any) {
/> />
<div> <div>
<RatingScale <RatingScale
:rating="1.1" :ratings="[3, 4, 3, 4]"
:answers="19"
title="Frage 1"
text="Wie zufrieden bist du mit dem Kurs “Überbetriebliche Kurse” im Allgemeinen?"
/>
<RatingScale
:rating="2"
:answers="17"
title="Frage 2"
text="Wie zufrieden bist du mit dem Kurs “Überbetriebliche Kurse” im Allgemeinen?"
/>
<RatingScale
:rating="2.5"
:answers="19" :answers="19"
title="Frage 3" title="Frage 3"
text="Wie zufrieden bist du mit dem Kurs “Überbetriebliche Kurse” im Allgemeinen?" text="Wie zufrieden bist du mit dem Kurs “Überbetriebliche Kurse” im Allgemeinen?"
/> />
<RatingScale
:rating="3.9"
:answers="18"
title="Frage 4"
text="Wie zufrieden bist du mit dem Kurs “Überbetriebliche Kurse” im Allgemeinen?"
/>
<RatingScale
:rating="4"
:answers="20"
title="Frage 5"
text="Wie zufrieden bist du mit dem Kurs “Überbetriebliche Kurse” im Allgemeinen?"
/>
<RatingScale
:rating="1"
:answers="20"
title="Frage 6"
text="Wie zufrieden bist du mit dem Kurs “Überbetriebliche Kurse” im Allgemeinen?"
/>
<RatingScale
:rating="2"
:answers="20"
title="Frage 7"
text="Wie zufrieden bist du mit dem Kurs “Überbetriebliche Kurse” im Allgemeinen?"
/>
<RatingScale
:rating="3"
:answers="20"
title="Frage 8"
text="Wie zufrieden bist du mit dem Kurs “Überbetriebliche Kurse” im Allgemeinen?"
/>
<RatingScale
:rating="4"
:answers="20"
title="Frage 9"
text="Wie zufrieden bist du mit dem Kurs “Überbetriebliche Kurse” im Allgemeinen?"
/>
<VerticalBarChart title="Frage X" text="Fragentext" :ratio="0.5" />
<VerticalBarChart title="Frage X" text="Fragentext" :ratio="0.8" />
<VerticalBarChart title="Frage X" text="Fragentext" :ratio="0.2" /> <VerticalBarChart title="Frage X" text="Fragentext" :ratio="0.2" />
<VerticalBarChart title="Frage X" text="Fragentext" :ratio="2" />
<VerticalBarChart title="Frage X" text="Fragentext" :ratio="0" />
<HorizontalBarChart title="Frage X" text="Fragentext" :items="barChartItems" /> <HorizontalBarChart title="Frage X" text="Fragentext" :items="barChartItems" />
</div> </div>
</main> </main>

View File

@ -1,17 +1,17 @@
<script setup lang="ts"> <script setup lang="ts">
import FeedbackSummary from "@/components/feedback/feedbackSummary.vue";
import LearningPathDiagram from "@/components/learningPath/LearningPathDiagram.vue"; import LearningPathDiagram from "@/components/learningPath/LearningPathDiagram.vue";
import ItPersonRow from "@/components/ui/ItPersonRow.vue"; import ItPersonRow from "@/components/ui/ItPersonRow.vue";
import ItProgress from "@/components/ui/ItProgress.vue"; import ItProgress from "@/components/ui/ItProgress.vue";
import FeedbackSummary from "@/components/feedback/feedbackSummary.vue";
import type { LearningPath } from "@/services/learningPath"; import type { LearningPath } from "@/services/learningPath";
import { useCockpitStore } from "@/stores/cockpit"; import { useCockpitStore } from "@/stores/cockpit";
import { useCompetenceStore } from "@/stores/competence"; import { useCompetenceStore } from "@/stores/competence";
import { useCourseSessionsStore } from "@/stores/courseSessions";
import { useLearningPathStore } from "@/stores/learningPath"; import { useLearningPathStore } from "@/stores/learningPath";
import { useUserStore } from "@/stores/user"; import { useUserStore } from "@/stores/user";
import log from "loglevel"; import log from "loglevel";
import { computed } from "vue"; import { computed } from "vue";
import { useCourseSessionsStore } from "@/stores/courseSessions";
const props = defineProps<{ const props = defineProps<{
courseSlug: string; courseSlug: string;
@ -95,8 +95,8 @@ function setActiveClasses(translationKey: string) {
</ul> </ul>
</div> </div>
<!-- Status --> <!-- Status -->
<div class="grid gap-4 grid-rows-2 lg:grid-rows-none lg:grid-cols-2 mb-4"> <div class="mb-4 grid grid-rows-2 gap-4 lg:grid-cols-2 lg:grid-rows-none">
<div class="px-6 py-5 bg-white"> <div class="bg-white px-6 py-5">
<h1 <h1
class="heading-3 mb-4 bg-assignment bg-60 bg-no-repeat pl-[68px] leading-[60px]" class="heading-3 mb-4 bg-assignment bg-60 bg-no-repeat pl-[68px] leading-[60px]"
> >
@ -109,7 +109,7 @@ function setActiveClasses(translationKey: string) {
</div> </div>
<div class="bg-white px-6 py-5"> <div class="bg-white px-6 py-5">
<h1 <h1
class="bg-test bg-no-repeat pl-[68px] heading-3 bg-60 leading-[60px] mb-4" class="heading-3 mb-4 bg-test bg-60 bg-no-repeat pl-[68px] leading-[60px]"
> >
{{ $t("general.examResult", 2) }} {{ $t("general.examResult", 2) }}
</h1> </h1>
@ -120,12 +120,15 @@ function setActiveClasses(translationKey: string) {
</div> </div>
</div> </div>
<!-- Feedback --> <!-- Feedback -->
<FeedbackSummary <FeedbackSummary
:selcted-circles="cockpitStore.selectedCircles || []" :selcted-circles="cockpitStore.selectedCircles || []"
:circles="learningPathStore.learningPathForUser(props.courseSlug, userStore.id)?.circles || []" :circles="
:course-id="courseSessionStore.courseSessionForRoute?.course.id" learningPathStore.learningPathForUser(props.courseSlug, userStore.id)
:url="courseSessionStore.courseSessionForRoute.course_url" ?.circles || []
></FeedbackSummary> "
:course-id="courseSessionStore.courseSessionForRoute?.course.id"
:url="courseSessionStore.courseSessionForRoute.course_url"
></FeedbackSummary>
<div> <div>
<!-- progress --> <!-- progress -->
<div v-if="cockpitStore.courseSessionUsers?.length" class="bg-white p-6"> <div v-if="cockpitStore.courseSessionUsers?.length" class="bg-white p-6">
@ -139,7 +142,7 @@ function setActiveClasses(translationKey: string) {
> >
<template #center> <template #center>
<div class="flex flex-row items-center justify-between"> <div class="flex flex-row items-center justify-between">
<div class="flex flex-row items-center gap-4 mr-6"> <div class="mr-6 flex flex-row items-center gap-4">
<div class="h-12"> <div class="h-12">
<LearningPathDiagram <LearningPathDiagram
v-if=" v-if="
@ -197,7 +200,7 @@ function setActiveClasses(translationKey: string) {
<template #link> <template #link>
<router-link <router-link
:to="`/course/${props.courseSlug}/cockpit/profile/${csu.user_id}`" :to="`/course/${props.courseSlug}/cockpit/profile/${csu.user_id}`"
class="underline w-full text-right" class="w-full text-right underline"
> >
{{ $t("general.profileLink") }} {{ $t("general.profileLink") }}
</router-link> </router-link>

View File

@ -0,0 +1,140 @@
<template>
<div class="bg-gray-200">
<div class="container-large">
<nav class="py-4 pb-4">
<router-link
class="btn-text inline-flex items-center pl-0"
:to="`/course/${props.courseSlug}/cockpit`"
>
<it-icon-arrow-left />
<span>{{ $t("general.back") }}</span>
</router-link>
</nav>
<main>
<h1 class="mb-2">{{ $t("feedback.feedbackPageTitle") }}</h1>
<p class="mb-10">
<span class="font-bold">{{ feedbackData.amount }}</span>
{{ $t("feedback.feedbackPageInfo") }}
</p>
<ol v-if="Object.keys(feedbackData).length > 0">
<li v-for="(question, i) in orderedQuestions" class="mb-8 bg-white p-8">
<RatingScale
v-if="ratingKeys.includes(question.key)"
:ratings="feedbackData.questions[question.key]"
:title="`${$t('feedback.questionTitle')} ${i}`"
:text="question.question"
/>
<VerticalBarChart
v-else-if="verticalChartKyes.includes(question.key)"
:title="`${$t('feedback.questionTitle')} ${i}`"
:text="question.question"
:ratio="0.2"
/>
<!-- HorizontalBarChart
v-else
:title="`${$t('feedback.questionTitle')} ${i}`"
:text="question.question"
:items="barChartItems" /-->
</li>
</ol>
</main>
</div>
</div>
</template>
<script setup lang="ts">
import HorizontalBarChart from "@/components/ui/HorizontalBarChart.vue";
import RatingScale from "@/components/ui/RatingScale.vue";
import VerticalBarChart from "@/components/ui/VerticalBarChart.vue";
import { itGet } from "@/fetchHelpers";
import { useCourseSessionsStore } from "@/stores/courseSessions";
import * as log from "loglevel";
import { onMounted, reactive } from "vue";
import { useI18n } from "vue-i18n";
const props = defineProps<{
courseSlug: string;
circleId: string;
}>();
log.debug("FeedbackPage created", props.circleId);
const courseSessionStore = useCourseSessionsStore();
const { t } = useI18n();
const orderedQuestions = [
{
key: "would_recommend",
question: t("feedback.recommendLabel"),
},
{
key: "satisfaction",
question: t("feedback.satisfactionLabel"),
},
{
key: "goal_attainment",
question: t("feedback.goalAttainmentLabel"),
},
{
key: "proficieny",
question: t("feedback.proficiencyLabel"),
},
{
key: "received_materials",
question: t("feedback.receivedMaterialsLabel"),
},
{
key: "materials_rating",
question: t("feedback.materialsRatingLabel"),
},
{
key: "instructor_competence",
question: t("feedback.instructorCompetenceLabel"),
},
{
key: "instructor_respect",
question: t("feedback.instructorRespectLabel"),
},
{
key: "instructor_open_feedback",
question: t("feedback.instructorOpenFeedbackLabel"),
},
{
key: "course_negative_feedback",
question: t("feedback.courseNegativeFeedbackLabel"),
},
{
key: "course_positive_feedback",
question: t("feedback.coursePositiveFeedbackLabel"),
},
];
const ratingKeys = [
"satisfaction",
"goal_attainment",
"materials_rating",
"instructor_competence",
"instructor_respect",
];
const verticalChartKyes = ["received_materials", "would_recommend"];
const feedbackData = reactive({});
onMounted(async () => {
log.debug("FeedbackPage mounted");
const data = await itGet(
`/api/core/feedback/${courseSessionStore.courseSessionForRoute?.course.id}/${props.circleId}`
);
Object.assign(feedbackData, data);
});
function calculateAverage(ratings: number[]): number {
return ratings.reduce((a, b) => a + b, 0) / ratings.length;
}
function getNumberOfAnswers(ratings: number[]): number {
return ratings.filter((r) => typeof r !== "string" || r !== "").length;
}
</script>
<style scoped></style>

View File

@ -119,6 +119,11 @@ const router = createRouter({
component: () => import("@/pages/cockpit/CockpitUserCirclePage.vue"), component: () => import("@/pages/cockpit/CockpitUserCirclePage.vue"),
props: true, props: true,
}, },
{
path: "feedback/:circleId",
component: () => import("@/pages/cockpit/FeedbackPage.vue"),
props: true,
},
], ],
}, },
{ {

View File

@ -1,9 +0,0 @@
from django import forms
from vbv_lernwelt.feedback.models import FeedbackResponse
class FeedbackForm(forms.ModelForm):
class Meta:
model = FeedbackResponse
fields = "__all__"

View File

@ -135,13 +135,14 @@ class FeedbackSummaryApiTestCase(FeedbackApiBaseTestCase):
class FeedbackDetailApiTestCase(FeedbackApiBaseTestCase): class FeedbackDetailApiTestCase(FeedbackApiBaseTestCase):
def test_can_receive_feedback(self): def test_can_receive_feedback(self):
expected = { feedback_data = {
"satisfaction": [1, 4, 2], "satisfaction": [1, 4, 2],
"goal_attainment": [2, 4, 3], "goal_attainment": [2, 4, 3],
"proficiency": [20, 60, 80], "proficiency": [20, 60, 80],
"received_materials": [True, False, True], "received_materials": [True, False, True],
"materials_rating": [3, 2, 1], "materials_rating": [3, 2, 1],
"instructor_competence": [1, 2, 3], "instructor_competence": [1, 2, 3],
"instructor_respect": [40, 80, 100],
"instructor_open_feedback": ["super", "ok", "naja"], "instructor_open_feedback": ["super", "ok", "naja"],
"would_recommend": [False, True, False], "would_recommend": [False, True, False],
"course_positive_feedback": ["Bla", "Katze", "Hund"], "course_positive_feedback": ["Bla", "Katze", "Hund"],
@ -158,24 +159,30 @@ class FeedbackDetailApiTestCase(FeedbackApiBaseTestCase):
FeedbackFactory( FeedbackFactory(
circle=circle, circle=circle,
course_session=csu.course_session, course_session=csu.course_session,
satisfaction=expected["satisfaction"][i], satisfaction=feedback_data["satisfaction"][i],
goal_attainment=expected["goal_attainment"][i], goal_attainment=feedback_data["goal_attainment"][i],
proficiency=expected["proficiency"][i], proficiency=feedback_data["proficiency"][i],
received_materials=expected["received_materials"][i], received_materials=feedback_data["received_materials"][i],
materials_rating=expected["materials_rating"][i], materials_rating=feedback_data["materials_rating"][i],
instructor_competence=expected["instructor_competence"][i], instructor_competence=feedback_data["instructor_competence"][i],
instructor_open_feedback=expected["instructor_open_feedback"][i], instructor_open_feedback=feedback_data["instructor_open_feedback"][i],
would_recommend=expected["would_recommend"][i], would_recommend=feedback_data["would_recommend"][i],
course_positive_feedback=expected["course_positive_feedback"][i], course_positive_feedback=feedback_data["course_positive_feedback"][i],
course_negative_feedback=expected["course_negative_feedback"][i], course_negative_feedback=feedback_data["course_negative_feedback"][i],
).save() ).save()
response = self.client.get( response = self.client.get(
f"/api/core/feedback/{csu.course_session.course.id}/{circle.id}/" f"/api/core/feedback/{csu.course_session.course.id}/{circle.id}/"
) )
self.maxDiff = None
expected = {
"amount": 3,
"questions": feedback_data,
}
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.data, expected) self.assertDictEqual(response.data, expected)
def test_cannot_receive_feedback_from_other_circle(self): def test_cannot_receive_feedback_from_other_circle(self):
csu = CourseSessionUser.objects.get( csu = CourseSessionUser.objects.get(
@ -192,7 +199,7 @@ class FeedbackDetailApiTestCase(FeedbackApiBaseTestCase):
) )
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.data, {}) self.assertEqual(response.data, {"amount": 0, "questions": {}})
def test_student_cannot_receive_feedback(self): def test_student_cannot_receive_feedback(self):
self.client.login(username="student", password="test") self.client.login(username="student", password="test")
@ -208,4 +215,4 @@ class FeedbackDetailApiTestCase(FeedbackApiBaseTestCase):
) )
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.data, {}) self.assertEqual(response.data, {"amount": 0, "questions": {}})

View File

@ -15,6 +15,7 @@ FEEDBACK_FIELDS = [
"received_materials", "received_materials",
"materials_rating", "materials_rating",
"instructor_competence", "instructor_competence",
"instructor_respect",
"instructor_open_feedback", "instructor_open_feedback",
"would_recommend", "would_recommend",
"course_positive_feedback", "course_positive_feedback",
@ -51,16 +52,19 @@ def get_feedback_for_circle(request, course_id, circle_id):
) )
# I guess this is ok for the üK case # I guess this is ok for the üK case
feedback_data = {} feedback_data = {
"amount": len(feedbacks),
"questions": {}
}
if len(feedbacks) == 0: if feedback_data["amount"] == 0:
return Response(status=200, data=feedback_data) return Response(status=200, data=feedback_data)
for field in FEEDBACK_FIELDS: for field in FEEDBACK_FIELDS:
feedback_data[field] = [] feedback_data["questions"][field] = []
for feedback in feedbacks: for feedback in feedbacks:
for field in FEEDBACK_FIELDS: for field in FEEDBACK_FIELDS:
feedback_data[field].append(getattr(feedback, field)) feedback_data["questions"][field].append(getattr(feedback, field))
return Response(status=200, data=feedback_data) return Response(status=200, data=feedback_data)