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

View File

@ -10,7 +10,7 @@ const props = defineProps<{
<template>
<ItRow>
<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>
</template>
<template #center>

View File

@ -1,11 +1,11 @@
<template>
<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>
</div>
<div class="flex-1 flex items-center">
<div class="flex flex-1 items-center">
<slot name="center"></slot>
</div>
<div class="flex items-center md:w-1/4">
@ -14,7 +14,6 @@
</li>
</template>
<script setup lang="ts">
</script>
<script setup lang="ts"></script>
<style lang="scss" scoped></style>

View File

@ -1,14 +1,14 @@
<template>
<QuestionSummary :title="props.title" :text="props.text">
<div class="inline-grid grid-cols-[140px_auto] grid-rows-2 gap-x-2">
<h4 class="text-xl font-bold">Durchschnitt:</h4>
<div class="mb-6 inline-grid grid-cols-[140px_auto] grid-rows-2 gap-x-2">
<h4 class="text-xl font-bold">{{ $t("feedback.average") }}:</h4>
<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"
>
{{ props.rating }}
{{ rating }}
</span>
<h5 class="text-base">{{ props.answers }} Antworten</h5>
<h5 class="text-base">{{ answers }} {{ $t("feedback.answers") }}</h5>
</div>
<div
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
v-for="legend in legends"
v-for="(legend, i) of legends"
:key="legend.index"
:class="{
'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"
>
<div
:class="[{ hidden: legend.index === 3 || legend.index === 2 }, 'md:block']"
>
{{ legend.index }}
</div>
<Popover class="relative">
<PopoverButton class="focus:outline-none">
<div
:class="[
{ hidden: legend.index === 3 || legend.index === 2 },
'md:block',
]"
>
{{ legend.index }}
</div>
<p :class="[{ hidden: legend.index === 3 || legend.index === 2 }, 'md:block']">
{{ legend.label }}
</p>
<p
:class="[
{ 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>
</QuestionSummary>
@ -53,7 +72,11 @@
<script setup lang="ts">
import QuestionSummary from "@/components/ui/QuestionSummary.vue";
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/vue";
import { computed } from "vue";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
type RGB = [number, number, number];
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 legends = [
{ index: 1, label: "Sehr unzufrieden" },
{ index: 2, label: "Unzufrieden" },
{ index: 3, label: "Zufrieden" },
{ index: 4, label: "Sehr zufrieden" },
{ index: 1, label: t("feedback.veryUnhappy") },
{ index: 2, label: t("feedback.unhappy") },
{ index: 3, label: t("feedback.happy") },
{ index: 4, label: t("feedback.veryHappy") },
];
const props = defineProps<{
rating: number;
answers: number;
ratings: number[];
title: 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(() => {
return props.rating % 1;
return rating.value % 1;
});
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(() => {

View File

@ -134,7 +134,16 @@
"feedbackSent": "Dein Feedback wurde abgeschickt",
"circleFeedback": "Feedback zum Circle",
"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": {
"yes": "Ja",

View File

@ -29,7 +29,8 @@ const app = createApp(App);
Sentry.init({
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",
tracesSampleRate: 0.0,
enabled:

View File

@ -450,64 +450,12 @@ function log(data: any) {
/>
<div>
<RatingScale
:rating="1.1"
: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"
:ratings="[3, 4, 3, 4]"
:answers="19"
title="Frage 3"
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="2" />
<VerticalBarChart title="Frage X" text="Fragentext" :ratio="0" />
<HorizontalBarChart title="Frage X" text="Fragentext" :items="barChartItems" />
</div>
</main>

View File

@ -1,17 +1,17 @@
<script setup lang="ts">
import FeedbackSummary from "@/components/feedback/feedbackSummary.vue";
import LearningPathDiagram from "@/components/learningPath/LearningPathDiagram.vue";
import ItPersonRow from "@/components/ui/ItPersonRow.vue";
import ItProgress from "@/components/ui/ItProgress.vue";
import FeedbackSummary from "@/components/feedback/feedbackSummary.vue";
import type { LearningPath } from "@/services/learningPath";
import { useCockpitStore } from "@/stores/cockpit";
import { useCompetenceStore } from "@/stores/competence";
import { useCourseSessionsStore } from "@/stores/courseSessions";
import { useLearningPathStore } from "@/stores/learningPath";
import { useUserStore } from "@/stores/user";
import log from "loglevel";
import { computed } from "vue";
import { useCourseSessionsStore } from "@/stores/courseSessions";
const props = defineProps<{
courseSlug: string;
@ -95,8 +95,8 @@ function setActiveClasses(translationKey: string) {
</ul>
</div>
<!-- Status -->
<div class="grid gap-4 grid-rows-2 lg:grid-rows-none lg:grid-cols-2 mb-4">
<div class="px-6 py-5 bg-white">
<div class="mb-4 grid grid-rows-2 gap-4 lg:grid-cols-2 lg:grid-rows-none">
<div class="bg-white px-6 py-5">
<h1
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 class="bg-white px-6 py-5">
<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) }}
</h1>
@ -120,12 +120,15 @@ function setActiveClasses(translationKey: string) {
</div>
</div>
<!-- Feedback -->
<FeedbackSummary
:selcted-circles="cockpitStore.selectedCircles || []"
:circles="learningPathStore.learningPathForUser(props.courseSlug, userStore.id)?.circles || []"
:course-id="courseSessionStore.courseSessionForRoute?.course.id"
:url="courseSessionStore.courseSessionForRoute.course_url"
></FeedbackSummary>
<FeedbackSummary
:selcted-circles="cockpitStore.selectedCircles || []"
:circles="
learningPathStore.learningPathForUser(props.courseSlug, userStore.id)
?.circles || []
"
:course-id="courseSessionStore.courseSessionForRoute?.course.id"
:url="courseSessionStore.courseSessionForRoute.course_url"
></FeedbackSummary>
<div>
<!-- progress -->
<div v-if="cockpitStore.courseSessionUsers?.length" class="bg-white p-6">
@ -139,7 +142,7 @@ function setActiveClasses(translationKey: string) {
>
<template #center>
<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">
<LearningPathDiagram
v-if="
@ -197,7 +200,7 @@ function setActiveClasses(translationKey: string) {
<template #link>
<router-link
:to="`/course/${props.courseSlug}/cockpit/profile/${csu.user_id}`"
class="underline w-full text-right"
class="w-full text-right underline"
>
{{ $t("general.profileLink") }}
</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"),
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):
def test_can_receive_feedback(self):
expected = {
feedback_data = {
"satisfaction": [1, 4, 2],
"goal_attainment": [2, 4, 3],
"proficiency": [20, 60, 80],
"received_materials": [True, False, True],
"materials_rating": [3, 2, 1],
"instructor_competence": [1, 2, 3],
"instructor_respect": [40, 80, 100],
"instructor_open_feedback": ["super", "ok", "naja"],
"would_recommend": [False, True, False],
"course_positive_feedback": ["Bla", "Katze", "Hund"],
@ -158,24 +159,30 @@ class FeedbackDetailApiTestCase(FeedbackApiBaseTestCase):
FeedbackFactory(
circle=circle,
course_session=csu.course_session,
satisfaction=expected["satisfaction"][i],
goal_attainment=expected["goal_attainment"][i],
proficiency=expected["proficiency"][i],
received_materials=expected["received_materials"][i],
materials_rating=expected["materials_rating"][i],
instructor_competence=expected["instructor_competence"][i],
instructor_open_feedback=expected["instructor_open_feedback"][i],
would_recommend=expected["would_recommend"][i],
course_positive_feedback=expected["course_positive_feedback"][i],
course_negative_feedback=expected["course_negative_feedback"][i],
satisfaction=feedback_data["satisfaction"][i],
goal_attainment=feedback_data["goal_attainment"][i],
proficiency=feedback_data["proficiency"][i],
received_materials=feedback_data["received_materials"][i],
materials_rating=feedback_data["materials_rating"][i],
instructor_competence=feedback_data["instructor_competence"][i],
instructor_open_feedback=feedback_data["instructor_open_feedback"][i],
would_recommend=feedback_data["would_recommend"][i],
course_positive_feedback=feedback_data["course_positive_feedback"][i],
course_negative_feedback=feedback_data["course_negative_feedback"][i],
).save()
response = self.client.get(
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.data, expected)
self.assertDictEqual(response.data, expected)
def test_cannot_receive_feedback_from_other_circle(self):
csu = CourseSessionUser.objects.get(
@ -192,7 +199,7 @@ class FeedbackDetailApiTestCase(FeedbackApiBaseTestCase):
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data, {})
self.assertEqual(response.data, {"amount": 0, "questions": {}})
def test_student_cannot_receive_feedback(self):
self.client.login(username="student", password="test")
@ -208,4 +215,4 @@ class FeedbackDetailApiTestCase(FeedbackApiBaseTestCase):
)
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",
"materials_rating",
"instructor_competence",
"instructor_respect",
"instructor_open_feedback",
"would_recommend",
"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
feedback_data = {}
feedback_data = {
"amount": len(feedbacks),
"questions": {}
}
if len(feedbacks) == 0:
if feedback_data["amount"] == 0:
return Response(status=200, data=feedback_data)
for field in FEEDBACK_FIELDS:
feedback_data[field] = []
feedback_data["questions"][field] = []
for feedback in feedbacks:
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)