Merged in feature/VBV-563-feedback-vv (pull request #242)

Feature/VBV-563 feedback vv

Approved-by: Daniel Egger
This commit is contained in:
Christian Cueni 2023-12-11 07:30:25 +00:00
commit 987159a531
36 changed files with 1554 additions and 531 deletions

View File

@ -24,7 +24,7 @@ const documents = {
"\n query dashboardConfig {\n dashboard_config {\n id\n slug\n name\n dashboard_type\n }\n }\n": types.DashboardConfigDocument, "\n query dashboardConfig {\n dashboard_config {\n id\n slug\n name\n dashboard_type\n }\n }\n": types.DashboardConfigDocument,
"\n query dashboardProgress($courseId: ID!) {\n course_progress(course_id: $courseId) {\n _id\n course_id\n session_to_continue_id\n competence {\n _id\n total_count\n success_count\n fail_count\n }\n assignment {\n _id\n total_count\n points_max_count\n points_achieved_count\n }\n }\n }\n": types.DashboardProgressDocument, "\n query dashboardProgress($courseId: ID!) {\n course_progress(course_id: $courseId) {\n _id\n course_id\n session_to_continue_id\n competence {\n _id\n total_count\n success_count\n fail_count\n }\n assignment {\n _id\n total_count\n points_max_count\n points_achieved_count\n }\n }\n }\n": types.DashboardProgressDocument,
"\n query courseStatistics($courseId: ID!) {\n course_statistics(course_id: $courseId) {\n _id\n course_id\n course_title\n course_slug\n course_session_properties {\n _id\n sessions {\n id\n name\n }\n generations\n circles {\n id\n name\n }\n }\n course_session_selection_ids\n course_session_selection_metrics {\n _id\n session_count\n participant_count\n expert_count\n }\n attendance_day_presences {\n _id\n records {\n _id\n course_session_id\n generation\n circle_id\n due_date\n participants_present\n participants_total\n details_url\n }\n summary {\n _id\n days_completed\n participants_present\n }\n }\n feedback_responses {\n _id\n records {\n _id\n course_session_id\n generation\n circle_id\n experts\n satisfaction_average\n satisfaction_max\n details_url\n }\n summary {\n _id\n satisfaction_average\n satisfaction_max\n total_responses\n }\n }\n assignments {\n _id\n summary {\n _id\n completed_count\n average_passed\n }\n records {\n _id\n course_session_id\n course_session_assignment_id\n circle_id\n generation\n assignment_title\n assignment_type_translation_key\n details_url\n deadline\n metrics {\n _id\n passed_count\n failed_count\n unranked_count\n ranking_completed\n average_passed\n }\n }\n }\n competences {\n _id\n summary {\n _id\n success_total\n fail_total\n }\n records {\n _id\n course_session_id\n generation\n circle_id\n title\n success_count\n fail_count\n details_url\n }\n }\n }\n }\n": types.CourseStatisticsDocument, "\n query courseStatistics($courseId: ID!) {\n course_statistics(course_id: $courseId) {\n _id\n course_id\n course_title\n course_slug\n course_session_properties {\n _id\n sessions {\n id\n name\n }\n generations\n circles {\n id\n name\n }\n }\n course_session_selection_ids\n course_session_selection_metrics {\n _id\n session_count\n participant_count\n expert_count\n }\n attendance_day_presences {\n _id\n records {\n _id\n course_session_id\n generation\n circle_id\n due_date\n participants_present\n participants_total\n details_url\n }\n summary {\n _id\n days_completed\n participants_present\n }\n }\n feedback_responses {\n _id\n records {\n _id\n course_session_id\n generation\n circle_id\n experts\n satisfaction_average\n satisfaction_max\n details_url\n }\n summary {\n _id\n satisfaction_average\n satisfaction_max\n total_responses\n }\n }\n assignments {\n _id\n summary {\n _id\n completed_count\n average_passed\n }\n records {\n _id\n course_session_id\n course_session_assignment_id\n circle_id\n generation\n assignment_title\n assignment_type_translation_key\n details_url\n deadline\n metrics {\n _id\n passed_count\n failed_count\n unranked_count\n ranking_completed\n average_passed\n }\n }\n }\n competences {\n _id\n summary {\n _id\n success_total\n fail_total\n }\n records {\n _id\n course_session_id\n generation\n circle_id\n title\n success_count\n fail_count\n details_url\n }\n }\n }\n }\n": types.CourseStatisticsDocument,
"\n mutation SendFeedbackMutation(\n $courseSessionId: ID!\n $learningContentId: ID!\n $data: GenericScalar!\n $submitted: Boolean\n ) {\n send_feedback(\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n data: $data\n submitted: $submitted\n ) {\n feedback_response {\n id\n data\n submitted\n }\n errors {\n field\n messages\n }\n }\n }\n": types.SendFeedbackMutationDocument, "\n mutation SendFeedbackMutation(\n $courseSessionId: ID!\n $learningContentId: ID!\n $learningContentType: String!\n $data: GenericScalar!\n $submitted: Boolean\n ) {\n send_feedback(\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n learning_content_type: $learningContentType\n data: $data\n submitted: $submitted\n ) {\n feedback_response {\n id\n data\n submitted\n }\n errors {\n field\n messages\n }\n }\n }\n": types.SendFeedbackMutationDocument,
}; };
/** /**
@ -88,7 +88,7 @@ export function graphql(source: "\n query courseStatistics($courseId: ID!) {\n
/** /**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/ */
export function graphql(source: "\n mutation SendFeedbackMutation(\n $courseSessionId: ID!\n $learningContentId: ID!\n $data: GenericScalar!\n $submitted: Boolean\n ) {\n send_feedback(\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n data: $data\n submitted: $submitted\n ) {\n feedback_response {\n id\n data\n submitted\n }\n errors {\n field\n messages\n }\n }\n }\n"): (typeof documents)["\n mutation SendFeedbackMutation(\n $courseSessionId: ID!\n $learningContentId: ID!\n $data: GenericScalar!\n $submitted: Boolean\n ) {\n send_feedback(\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n data: $data\n submitted: $submitted\n ) {\n feedback_response {\n id\n data\n submitted\n }\n errors {\n field\n messages\n }\n }\n }\n"]; export function graphql(source: "\n mutation SendFeedbackMutation(\n $courseSessionId: ID!\n $learningContentId: ID!\n $learningContentType: String!\n $data: GenericScalar!\n $submitted: Boolean\n ) {\n send_feedback(\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n learning_content_type: $learningContentType\n data: $data\n submitted: $submitted\n ) {\n feedback_response {\n id\n data\n submitted\n }\n errors {\n field\n messages\n }\n }\n }\n"): (typeof documents)["\n mutation SendFeedbackMutation(\n $courseSessionId: ID!\n $learningContentId: ID!\n $learningContentType: String!\n $data: GenericScalar!\n $submitted: Boolean\n ) {\n send_feedback(\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n learning_content_type: $learningContentType\n data: $data\n submitted: $submitted\n ) {\n feedback_response {\n id\n data\n submitted\n }\n errors {\n field\n messages\n }\n }\n }\n"];
export function graphql(source: string) { export function graphql(source: string) {
return (documents as any)[source] ?? {}; return (documents as any)[source] ?? {};

File diff suppressed because one or more lines are too long

View File

@ -9,7 +9,8 @@ type Query {
learning_content_media_library: LearningContentMediaLibraryObjectType learning_content_media_library: LearningContentMediaLibraryObjectType
learning_content_assignment: LearningContentAssignmentObjectType learning_content_assignment: LearningContentAssignmentObjectType
learning_content_attendance_course: LearningContentAttendanceCourseObjectType learning_content_attendance_course: LearningContentAttendanceCourseObjectType
learning_content_feedback: LearningContentFeedbackObjectType learning_content_feedback_uk: LearningContentFeedbackUKObjectType
learning_content_feedback_vv: LearningContentFeedbackVVObjectType
learning_content_learning_module: LearningContentLearningModuleObjectType learning_content_learning_module: LearningContentLearningModuleObjectType
learning_content_knowledge_assessment: LearningContentKnowledgeAssessmentObjectType learning_content_knowledge_assessment: LearningContentKnowledgeAssessmentObjectType
learning_content_placeholder: LearningContentPlaceholderObjectType learning_content_placeholder: LearningContentPlaceholderObjectType
@ -718,7 +719,23 @@ type LearningContentMediaLibraryObjectType implements CoursePageInterface & Lear
circle: CircleLightObjectType circle: CircleLightObjectType
} }
type LearningContentFeedbackObjectType implements CoursePageInterface & LearningContentInterface { type LearningContentFeedbackUKObjectType implements CoursePageInterface & LearningContentInterface {
id: ID!
title: String!
slug: String!
content_type: String!
live: Boolean!
translation_key: String!
frontend_url: String!
course: CourseObjectType
minutes: Int
description: String!
content_url: String!
can_user_self_toggle_course_completion: Boolean!
circle: CircleLightObjectType
}
type LearningContentFeedbackVVObjectType implements CoursePageInterface & LearningContentInterface {
id: ID! id: ID!
title: String! title: String!
slug: String! slug: String!
@ -844,7 +861,7 @@ type CompetenceCertificateListObjectType implements CoursePageInterface {
} }
type Mutation { type Mutation {
send_feedback(course_session_id: ID!, data: GenericScalar, learning_content_page_id: ID!, submitted: Boolean = false): SendFeedbackMutation send_feedback(course_session_id: ID!, data: GenericScalar, learning_content_page_id: ID!, learning_content_type: String!, submitted: Boolean = false): SendFeedbackMutation
update_course_session_attendance_course_users(attendance_user_list: [AttendanceUserInputType]!, id: ID!): AttendanceCourseUserMutation update_course_session_attendance_course_users(attendance_user_list: [AttendanceUserInputType]!, id: ID!): AttendanceCourseUserMutation
upsert_assignment_completion(assignment_id: ID!, assignment_user_id: UUID, completion_data_string: String, completion_status: AssignmentCompletionStatus, course_session_id: ID!, evaluation_passed: Boolean, evaluation_points: Float, initialize_completion: Boolean, learning_content_page_id: ID): AssignmentCompletionMutation upsert_assignment_completion(assignment_id: ID!, assignment_user_id: UUID, completion_data_string: String, completion_status: AssignmentCompletionStatus, course_session_id: ID!, evaluation_passed: Boolean, evaluation_points: Float, initialize_completion: Boolean, learning_content_page_id: ID): AssignmentCompletionMutation
} }

View File

@ -55,7 +55,8 @@ export const LearningContentAssignmentObjectType = "LearningContentAssignmentObj
export const LearningContentAttendanceCourseObjectType = "LearningContentAttendanceCourseObjectType"; export const LearningContentAttendanceCourseObjectType = "LearningContentAttendanceCourseObjectType";
export const LearningContentDocumentListObjectType = "LearningContentDocumentListObjectType"; export const LearningContentDocumentListObjectType = "LearningContentDocumentListObjectType";
export const LearningContentEdoniqTestObjectType = "LearningContentEdoniqTestObjectType"; export const LearningContentEdoniqTestObjectType = "LearningContentEdoniqTestObjectType";
export const LearningContentFeedbackObjectType = "LearningContentFeedbackObjectType"; export const LearningContentFeedbackUKObjectType = "LearningContentFeedbackUKObjectType";
export const LearningContentFeedbackVVObjectType = "LearningContentFeedbackVVObjectType";
export const LearningContentInterface = "LearningContentInterface"; export const LearningContentInterface = "LearningContentInterface";
export const LearningContentKnowledgeAssessmentObjectType = "LearningContentKnowledgeAssessmentObjectType"; export const LearningContentKnowledgeAssessmentObjectType = "LearningContentKnowledgeAssessmentObjectType";
export const LearningContentLearningModuleObjectType = "LearningContentLearningModuleObjectType"; export const LearningContentLearningModuleObjectType = "LearningContentLearningModuleObjectType";

View File

@ -18,70 +18,24 @@
</span> </span>
{{ $t("feedback.feedbackPageInfo") }} {{ $t("feedback.feedbackPageInfo") }}
</p> </p>
<ol v-if="feedbackData.amount > 0"> <FeedbackPageVV v-if="feedbackType === 'vv'" :feedback-data="feedbackData" />
<li <FeedbackPageUK
v-for="(question, i) in orderedQuestions" v-else-if="feedbackType === 'uk'"
:key="i" :feedback-data="feedbackData"
:data-cy="`question-${i + 1}`" />
>
<RatingScale
v-if="ratingKeys.includes(question.key)"
class="mb-8 bg-white"
:ratings="feedbackData.questions[question.key]"
:title="`${$t('feedback.questionTitle')} ${i + 1}`"
:text="question.question"
/>
<VerticalBarChart
v-else-if="verticalChartKyes.includes(question.key)"
class="mb-8 bg-white"
:title="`${$t('feedback.questionTitle')} ${i + 1}`"
:ratings="feedbackData.questions[question.key]"
:text="question.question"
:ratio="0.2"
/>
<OpenFeedback
v-else-if="
openKeys.includes(question.key) && feedbackData.questions[question.key]
"
class="mb-8 bg-white"
:title="`${$t('feedback.questionTitle')} ${i + 1}`"
:text="question.question"
:answers="feedbackData.questions[question.key].filter((a: string) => a !== '')"
></OpenFeedback>
<HorizontalBarChart
v-else-if="
horizontalChartKeys.includes(question.key) &&
feedbackData.questions[question.key]
"
class="mb-8 bg-white"
:title="`${$t('feedback.questionTitle')} ${i}`"
:text="question.question"
:items="feedbackData.questions[question.key].map((a: string) => `${a}%`)"
/>
</li>
</ol>
</main> </main>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import HorizontalBarChart from "@/components/ui/HorizontalBarChart.vue";
import OpenFeedback from "@/components/ui/OpenFeedback.vue";
import RatingScale from "@/components/ui/RatingScale.vue";
import VerticalBarChart from "@/components/ui/VerticalBarChart.vue";
import { useCurrentCourseSession } from "@/composables"; import { useCurrentCourseSession } from "@/composables";
import { itGet } from "@/fetchHelpers"; import { itGet } from "@/fetchHelpers";
import * as log from "loglevel"; import * as log from "loglevel";
import { onMounted, ref } from "vue"; import { onMounted, ref } from "vue";
import { useTranslation } from "i18next-vue"; import type { FeedbackData, FeedbackType } from "@/types";
import FeedbackPageVV from "@/pages/cockpit/FeedbackPageVV.vue";
interface FeedbackData { import FeedbackPageUK from "@/pages/cockpit/FeedbackPageUK.vue";
amount: number;
questions: {
[key: string]: any;
};
}
const props = defineProps<{ const props = defineProps<{
courseSlug: string; courseSlug: string;
@ -91,72 +45,21 @@ const props = defineProps<{
log.debug("FeedbackPage created", props.circleId); log.debug("FeedbackPage created", props.circleId);
const courseSession = useCurrentCourseSession(); const courseSession = useCurrentCourseSession();
const { t } = useTranslation();
const orderedQuestions = [
{
key: "satisfaction",
question: t("feedback.satisfactionLabel"),
},
{
key: "goal_attainment",
question: t("feedback.goalAttainmentLabel"),
},
{
key: "proficiency",
question: t("feedback.proficiencyLabel"),
},
{
key: "preparation_task_clarity",
question: t("feedback.preparationTaskClarityLabel"),
},
{
key: "instructor_competence",
question: t("feedback.instructorCompetenceLabel"),
},
{
key: "instructor_respect",
question: t("feedback.instructorRespectLabel"),
},
{
key: "instructor_open_feedback",
question: t("feedback.instructorOpenFeedbackLabel"),
},
{
key: "would_recommend",
question: t("feedback.recommendLabel"),
},
{
key: "course_negative_feedback",
question: t("feedback.courseNegativeFeedbackLabel"),
},
{
key: "course_positive_feedback",
question: t("feedback.coursePositiveFeedbackLabel"),
},
];
const ratingKeys = [
"satisfaction",
"goal_attainment",
"instructor_competence",
"instructor_respect",
];
const verticalChartKyes = ["preparation_task_clarity", "would_recommend"];
const horizontalChartKeys = ["proficiency"];
const openKeys = [
"course_negative_feedback",
"course_positive_feedback",
"instructor_open_feedback",
];
const feedbackData = ref<FeedbackData | undefined>(undefined); const feedbackData = ref<FeedbackData | undefined>(undefined);
const feedbackType = ref<FeedbackType | undefined>(undefined);
onMounted(async () => { onMounted(async () => {
log.debug("FeedbackPage mounted"); log.debug("FeedbackPage mounted");
feedbackData.value = await itGet( feedbackData.value = await itGet(
`/api/core/feedback/${courseSession.value.id}/${props.circleId}` `/api/core/feedback/${courseSession.value.id}/${props.circleId}`
); );
log.debug("FeedbackPage feedbackData", feedbackData.value);
if (
feedbackData.value &&
["uk", "vv"].includes(feedbackData.value?.feedbackType ?? "")
) {
feedbackType.value = feedbackData.value.feedbackType;
}
}); });
</script> </script>

View File

@ -0,0 +1,84 @@
<template>
<FeedbackResults
:ordered-questions="orderedQuestions"
:feedback-data="feedbackData"
:rating-keys="ratingKeys"
:vertical-chart-keys="verticalChartKeys"
:horizontal-chart-keys="horizontalChartKeys"
:open-keys="openKeys"
/>
</template>
<script setup lang="ts">
import FeedbackResults from "@/pages/cockpit/FeedbackResults.vue";
import type { FeedbackData } from "@/types";
import * as log from "loglevel";
import { useTranslation } from "i18next-vue";
defineProps<{
feedbackData: FeedbackData;
}>();
log.debug("FeedbackPageUK created");
const { t } = useTranslation();
const orderedQuestions = [
{
key: "satisfaction",
question: t("feedback.satisfactionLabel"),
},
{
key: "goal_attainment",
question: t("feedback.goalAttainmentLabel"),
},
{
key: "proficiency",
question: t("feedback.proficiencyLabel"),
},
{
key: "preparation_task_clarity",
question: t("feedback.preparationTaskClarityLabel"),
},
{
key: "instructor_competence",
question: t("feedback.instructorCompetenceLabel"),
},
{
key: "instructor_respect",
question: t("feedback.instructorRespectLabel"),
},
{
key: "instructor_open_feedback",
question: t("feedback.instructorOpenFeedbackLabel"),
},
{
key: "would_recommend",
question: t("feedback.recommendLabel"),
},
{
key: "course_negative_feedback",
question: t("feedback.courseNegativeFeedbackLabel"),
},
{
key: "course_positive_feedback",
question: t("feedback.coursePositiveFeedbackLabel"),
},
];
const ratingKeys = [
"satisfaction",
"goal_attainment",
"instructor_competence",
"instructor_respect",
];
const verticalChartKeys = ["preparation_task_clarity", "would_recommend"];
const horizontalChartKeys = ["proficiency"];
const openKeys = [
"course_negative_feedback",
"course_positive_feedback",
"instructor_open_feedback",
];
</script>
<style scoped></style>

View File

@ -0,0 +1,63 @@
<template>
<FeedbackResults
:ordered-questions="orderedQuestions"
:feedback-data="feedbackData"
:rating-keys="ratingKeys"
:vertical-chart-keys="verticalChartKeys"
:horizontal-chart-keys="horizontalChartKeys"
:open-keys="openKeys"
/>
</template>
<script setup lang="ts">
import FeedbackResults from "@/pages/cockpit/FeedbackResults.vue";
import type { FeedbackData } from "@/types";
import * as log from "loglevel";
import { useTranslation } from "i18next-vue";
defineProps<{
feedbackData: FeedbackData;
}>();
log.debug("FeedbackPageVV created");
const { t } = useTranslation();
const orderedQuestions = [
{
key: "satisfaction",
question: t("feedback.satisfactionLabel"),
},
{
key: "goal_attainment",
question: t("feedback.goalAttainmentLabel"),
},
{
key: "proficiency",
question: t("feedback.proficiencyLabelVV"),
},
{
key: "preparation_task_clarity",
question: t("feedback.praxisAssignmentClarity"),
},
{
key: "would_recommend",
question: t("feedback.recommendLabelVV"),
},
{
key: "course_negative_feedback",
question: t("feedback.courseNegativeFeedbackLabel"),
},
{
key: "course_positive_feedback",
question: t("feedback.coursePositiveFeedbackLabel"),
},
];
const ratingKeys = ["satisfaction", "goal_attainment"];
const verticalChartKeys = ["preparation_task_clarity", "would_recommend"];
const horizontalChartKeys = ["proficiency"];
const openKeys = ["course_negative_feedback", "course_positive_feedback"];
</script>
<style scoped></style>

View File

@ -0,0 +1,77 @@
<template>
<ol v-if="feedbackData.amount > 0">
<li
v-for="(question, i) in orderedQuestions"
:key="i"
:data-cy="`question-${i + 1}`"
>
<RatingScale
v-if="ratingKeys.includes(question.key)"
class="mb-8 bg-white"
:ratings="feedbackData.questions[question.key]"
:title="`${$t('feedback.questionTitle')} ${i + 1}`"
:text="question.question"
/>
<VerticalBarChart
v-else-if="verticalChartKeys.includes(question.key)"
class="mb-8 bg-white"
:title="`${$t('feedback.questionTitle')} ${i + 1}`"
:ratings="feedbackData.questions[question.key]"
:text="question.question"
:ratio="0.2"
/>
<OpenFeedback
v-else-if="
openKeys.includes(question.key) && feedbackData.questions[question.key]
"
class="mb-8 bg-white"
:title="`${$t('feedback.questionTitle')} ${i + 1}`"
:text="question.question"
:answers="feedbackData.questions[question.key].filter((a: string) => a !== '')"
></OpenFeedback>
<HorizontalBarChart
v-else-if="
horizontalChartKeys.includes(question.key) &&
feedbackData.questions[question.key]
"
class="mb-8 bg-white"
:title="`${$t('feedback.questionTitle')} ${i + 1}`"
:text="question.question"
:items="feedbackData.questions[question.key].map((a: string) => `${a}%`)"
/>
</li>
</ol>
</template>
<script setup lang="ts">
import HorizontalBarChart from "@/components/ui/HorizontalBarChart.vue";
import OpenFeedback from "@/components/ui/OpenFeedback.vue";
import RatingScale from "@/components/ui/RatingScale.vue";
import VerticalBarChart from "@/components/ui/VerticalBarChart.vue";
import type { FeedbackData } from "@/types";
import * as log from "loglevel";
interface Props {
orderedQuestions?: {
key: string;
question: string;
}[];
feedbackData: FeedbackData;
ratingKeys?: string[];
verticalChartKeys?: string[];
horizontalChartKeys?: string[];
openKeys?: string[];
}
withDefaults(defineProps<Props>(), {
orderedQuestions: () => [],
ratingKeys: () => [],
verticalChartKeys: () => [],
horizontalChartKeys: () => [],
openKeys: () => [],
});
log.debug("FeedbackBasePage created");
</script>
<style scoped></style>

View File

@ -52,7 +52,8 @@ const submittables = computed(() => {
const learningContents = circleFlatLearningContents(circle).filter( const learningContents = circleFlatLearningContents(circle).filter(
(lc) => (lc) =>
lc.content_type === "learnpath.LearningContentAssignment" || lc.content_type === "learnpath.LearningContentAssignment" ||
lc.content_type === "learnpath.LearningContentFeedback" || lc.content_type === "learnpath.LearningContentFeedbackUK" ||
lc.content_type === "learnpath.LearningContentFeedbackVV" ||
lc.content_type === "learnpath.LearningContentEdoniqTest" lc.content_type === "learnpath.LearningContentEdoniqTest"
); );
@ -72,7 +73,10 @@ const submittables = computed(() => {
}); });
const isFeedback = (lc: LearningContent) => { const isFeedback = (lc: LearningContent) => {
return lc.content_type === "learnpath.LearningContentFeedback"; return (
lc.content_type === "learnpath.LearningContentFeedbackUK" ||
lc.content_type === "learnpath.LearningContentFeedbackVV"
);
}; };
const isAssignment = (lc: LearningContent) => { const isAssignment = (lc: LearningContent) => {

View File

@ -14,12 +14,13 @@ import type { Component } from "vue";
import { computed, onUnmounted } from "vue"; import { computed, onUnmounted } from "vue";
import AssignmentBlock from "./blocks/AssignmentBlock.vue"; import AssignmentBlock from "./blocks/AssignmentBlock.vue";
import AttendanceCourseBlock from "./blocks/AttendanceCourseBlock.vue"; import AttendanceCourseBlock from "./blocks/AttendanceCourseBlock.vue";
import FeedbackBlock from "./feedback/FeedbackBlock.vue";
import IframeBlock from "./blocks/IframeBlock.vue"; import IframeBlock from "./blocks/IframeBlock.vue";
import MediaLibraryBlock from "./blocks/MediaLibraryBlock.vue"; import MediaLibraryBlock from "./blocks/MediaLibraryBlock.vue";
import PlaceholderBlock from "./blocks/PlaceholderBlock.vue"; import PlaceholderBlock from "./blocks/PlaceholderBlock.vue";
import RichTextBlock from "./blocks/RichTextBlock.vue"; import RichTextBlock from "./blocks/RichTextBlock.vue";
import VideoBlock from "./blocks/VideoBlock.vue"; import VideoBlock from "./blocks/VideoBlock.vue";
import FeedbackBlockUK from "./feedback/FeedbackBlockUK.vue";
import FeedbackBlockVV from "./feedback/FeedbackBlockVV.vue";
import { getPreviousRoute } from "@/router/history"; import { getPreviousRoute } from "@/router/history";
import { stringifyParse } from "@/utils/utils"; import { stringifyParse } from "@/utils/utils";
import { useCourseDataWithCompletion } from "@/composables"; import { useCourseDataWithCompletion } from "@/composables";
@ -42,7 +43,8 @@ const COMPONENTS: Record<LearningContentContentType, Component> = {
"learnpath.LearningContentAssignment": AssignmentBlock, "learnpath.LearningContentAssignment": AssignmentBlock,
"learnpath.LearningContentAttendanceCourse": AttendanceCourseBlock, "learnpath.LearningContentAttendanceCourse": AttendanceCourseBlock,
"learnpath.LearningContentDocumentList": DocumentListBlock, "learnpath.LearningContentDocumentList": DocumentListBlock,
"learnpath.LearningContentFeedback": FeedbackBlock, "learnpath.LearningContentFeedbackUK": FeedbackBlockUK,
"learnpath.LearningContentFeedbackVV": FeedbackBlockVV,
"learnpath.LearningContentLearningModule": IframeBlock, "learnpath.LearningContentLearningModule": IframeBlock,
"learnpath.LearningContentKnowledgeAssessment": IframeBlock, "learnpath.LearningContentKnowledgeAssessment": IframeBlock,
"learnpath.LearningContentMediaLibrary": MediaLibraryBlock, "learnpath.LearningContentMediaLibrary": MediaLibraryBlock,

View File

@ -1,35 +1,32 @@
<script setup lang="ts"> <script setup lang="ts">
import ItRadioGroup from "@/components/ui/ItRadioGroup.vue";
import ItTextarea from "@/components/ui/ItTextarea.vue";
import { graphql } from "@/gql"; import { graphql } from "@/gql";
import FeedbackCompletition from "@/pages/learningPath/learningContentPage/feedback/FeedbackCompletition.vue"; import FeedbackCompletition from "@/pages/learningPath/learningContentPage/feedback/FeedbackCompletition.vue";
import {
PERCENTAGES,
RATINGS,
YES_NO,
} from "@/pages/learningPath/learningContentPage/feedback/feedback.constants";
import LearningContentMultiLayout from "@/pages/learningPath/learningContentPage/layouts/LearningContentMultiLayout.vue"; import LearningContentMultiLayout from "@/pages/learningPath/learningContentPage/layouts/LearningContentMultiLayout.vue";
import type { LearningContentFeedback } from "@/types"; import type { LearningContentFeedbackUK, LearningContentFeedbackVV } from "@/types";
import { useMutation } from "@urql/vue"; import { useMutation } from "@urql/vue";
import { useRouteQuery } from "@vueuse/router"; import { useRouteQuery } from "@vueuse/router";
import log from "loglevel"; import log from "loglevel";
import { computed, onMounted, reactive, ref } from "vue"; import { computed, onMounted, reactive, ref } from "vue";
import { useTranslation } from "i18next-vue";
import { useCourseSessionDetailQuery, useCurrentCourseSession } from "@/composables"; import { useCourseSessionDetailQuery, useCurrentCourseSession } from "@/composables";
import { bustItGetCache } from "@/fetchHelpers";
import { useUserStore } from "@/stores/user";
const props = defineProps<{ const props = defineProps<{
content: LearningContentFeedback; content: LearningContentFeedbackVV | LearningContentFeedbackUK;
stepLabels: string[];
questionData: any[];
introduction: string;
title: string;
completionTitle: string;
completionDescription: string;
showAvatar: boolean;
}>(); }>();
const courseSession = useCurrentCourseSession(); const courseSession = useCurrentCourseSession();
const courseSessionDetailResult = useCourseSessionDetailQuery(); const courseSessionDetailResult = useCourseSessionDetailQuery();
const { t } = useTranslation();
const stepNo = useRouteQuery("step", "0", { transform: Number, mode: "push" }); const stepNo = useRouteQuery("step", "0", { transform: Number, mode: "push" });
const title = computed( const title = computed(() => `«${props.content.circle?.title}»: ${props.title}`);
() => `«${props.content.circle?.title}»: ${t("feedback.areYouSatisfied")}`
);
const circleExperts = computed(() => { const circleExperts = computed(() => {
if (props.content?.circle?.slug) { if (props.content?.circle?.slug) {
@ -38,34 +35,29 @@ const circleExperts = computed(() => {
return []; return [];
}); });
const stepLabels = [ const localStepLabels = ref(props.stepLabels);
t("general.introduction"), const localQuestionData = ref(props.questionData);
t("feedback.satisfactionLabel"), const feedbackData: FeedbackData = reactive(feedbackDataFactory());
t("feedback.goalAttainmentLabel"),
t("feedback.proficiencyLabel"),
t("feedback.preparationTaskClarityLabel"),
t("feedback.instructorCompetenceLabel"),
t("feedback.instructorRespectLabel"),
t("feedback.instructorOpenFeedbackLabel"),
t("feedback.recommendLabel"),
t("feedback.coursePositiveFeedbackLabel"),
t("feedback.courseNegativeFeedbackLabel"),
t("general.submission"),
];
const numSteps = stepLabels.length; const numSteps = computed(() => localStepLabels.value.length);
const textQuestionKeys = computed(() =>
props.questionData.filter((item) => isTextNode(item)).map((item) => item.modelKey)
);
const avatarUrl = computed(() => circleExperts.value[0]?.avatar_url);
// noinspection GraphQLUnresolvedReference -> mute IntelliJ warning // noinspection GraphQLUnresolvedReference -> mute IntelliJ warning
const sendFeedbackMutation = graphql(` const sendFeedbackMutation = graphql(`
mutation SendFeedbackMutation( mutation SendFeedbackMutation(
$courseSessionId: ID! $courseSessionId: ID!
$learningContentId: ID! $learningContentId: ID!
$learningContentType: String!
$data: GenericScalar! $data: GenericScalar!
$submitted: Boolean $submitted: Boolean
) { ) {
send_feedback( send_feedback(
course_session_id: $courseSessionId course_session_id: $courseSessionId
learning_content_page_id: $learningContentId learning_content_page_id: $learningContentId
learning_content_type: $learningContentType
data: $data data: $data
submitted: $submitted submitted: $submitted
) { ) {
@ -90,69 +82,6 @@ interface FeedbackData {
[key: string]: number | string | null; [key: string]: number | string | null;
} }
const feedbackData: FeedbackData = reactive({
satisfaction: null,
goal_attainment: null,
proficiency: null,
preparation_task_clarity: null,
instructor_competence: null,
instructor_respect: null,
instructor_open_feedback: "",
would_recommend: null,
course_positive_feedback: "",
course_negative_feedback: "",
});
const questionData = [
{
modelKey: "satisfaction",
items: RATINGS,
component: ItRadioGroup,
},
{
modelKey: "goal_attainment",
items: RATINGS,
component: ItRadioGroup,
},
{
modelKey: "proficiency",
items: PERCENTAGES,
component: ItRadioGroup,
},
{
modelKey: "preparation_task_clarity",
items: YES_NO,
component: ItRadioGroup,
},
{
modelKey: "instructor_competence",
items: RATINGS,
component: ItRadioGroup,
},
{
modelKey: "instructor_respect",
items: RATINGS,
component: ItRadioGroup,
},
{
modelKey: "instructor_open_feedback",
component: ItTextarea,
},
{
modelKey: "would_recommend",
items: YES_NO,
component: ItRadioGroup,
},
{
modelKey: "course_positive_feedback",
component: ItTextarea,
},
{
modelKey: "course_negative_feedback",
component: ItTextarea,
},
];
const previousStep = () => { const previousStep = () => {
if (stepNo.value > 0) { if (stepNo.value > 0) {
stepNo.value -= 1; stepNo.value -= 1;
@ -160,23 +89,17 @@ const previousStep = () => {
}; };
const nextStep = () => { const nextStep = () => {
if (stepNo.value < numSteps && hasStepValidInput(stepNo.value)) { if (stepNo.value < numSteps.value && hasStepValidInput(stepNo.value)) {
stepNo.value += 1; stepNo.value += 1;
} }
log.debug(`next step ${stepNo.value} of ${numSteps}`); log.debug(`next step ${stepNo.value} of ${numSteps.value}`);
mutateFeedback(feedbackData); mutateFeedback(feedbackData);
}; };
function hasStepValidInput(stepNumber: number) { function hasStepValidInput(stepNumber: number) {
const question = questionData[stepNumber - 1]; const question = localQuestionData.value[stepNumber - 1];
if (question) { if (question) {
if ( if (textQuestionKeys.value.includes(question.modelKey)) {
[
"instructor_open_feedback",
"course_negative_feedback",
"course_positive_feedback",
].includes(question.modelKey)
) {
// text response questions need to have a "truthy" value (not "" or null) // text response questions need to have a "truthy" value (not "" or null)
return feedbackData[question.modelKey]; return feedbackData[question.modelKey];
} else { } else {
@ -192,6 +115,7 @@ function mutateFeedback(data: FeedbackData, submit = false) {
return executeMutation({ return executeMutation({
courseSessionId: courseSession.value.id, courseSessionId: courseSession.value.id,
learningContentId: props.content.id, learningContentId: props.content.id,
learningContentType: props.content.content_type,
data: data, data: data,
submitted: submit, submitted: submit,
}) })
@ -199,29 +123,40 @@ function mutateFeedback(data: FeedbackData, submit = false) {
log.debug("feedback mutation result", result); log.debug("feedback mutation result", result);
if (result.data?.send_feedback?.feedback_response?.data) { if (result.data?.send_feedback?.feedback_response?.data) {
const responseData = result.data.send_feedback.feedback_response.data; const responseData = result.data.send_feedback.feedback_response.data;
if (!responseData.instructor_open_feedback) { textQuestionKeys.value.map((key) => {
responseData.instructor_open_feedback = ""; if (!responseData[key]) {
} responseData[key] = "";
if (!responseData.course_negative_feedback) { }
responseData.course_negative_feedback = ""; });
}
if (!responseData.course_positive_feedback) {
responseData.course_positive_feedback = "";
}
Object.assign(feedbackData, responseData); Object.assign(feedbackData, responseData);
log.debug("feedback data", feedbackData); log.debug("feedback data", feedbackData);
feedbackSubmitted.value = feedbackSubmitted.value =
result.data?.send_feedback?.feedback_response?.submitted || false; result.data?.send_feedback?.feedback_response?.submitted || false;
} }
bustItGetCache(
`/api/course/completion/${courseSession.value.id}/${useUserStore().id}/`
);
}) })
.catch((e) => log.error(e)); .catch((e) => log.error(e));
} }
function feedbackDataFactory() {
const data: FeedbackData = {};
localQuestionData.value.map((item) => {
data[item.modelKey] = isTextNode(item) ? "" : null;
});
return data;
}
function isTextNode(item: any) {
return item.component.props.placeholder;
}
onMounted(async () => { onMounted(async () => {
log.debug("Feedback mounted"); log.debug("Feedback mounted");
await mutateFeedback({}); await mutateFeedback({});
if (feedbackSubmitted.value) { if (feedbackSubmitted.value) {
stepNo.value = numSteps - 1; stepNo.value = numSteps.value - 1;
} }
}); });
</script> </script>
@ -246,22 +181,22 @@ onMounted(async () => {
@next="nextStep()" @next="nextStep()"
> >
<div> <div>
<p v-if="stepNo === 0" class="mt-10"> <p v-if="stepNo === 0" class="mt-10" data-cy="introduction">
{{ {{ introduction }}
$t("feedback.intro", {
name: `${circleExperts[0]?.first_name} ${circleExperts[0]?.last_name}`,
})
}}
</p> </p>
<p v-if="stepNo > 0 && stepNo + 1 < numSteps" class="pb-2"> <p
{{ stepLabels[stepNo] }} v-if="stepNo > 0 && stepNo + 1 < numSteps"
class="pb-2"
:data-cy="`question-${stepNo}`"
>
{{ localStepLabels[stepNo] }}
</p> </p>
<div v-for="(question, index) in questionData" :key="index"> <div v-for="(question, index) in questionData" :key="index">
<!-- eslint-disable --> <!-- eslint-disable -->
<!-- eslint does not like the dynamic v-model... --> <!-- eslint does not like the dynamic v-model... -->
<component <component
:is="question.component" :is="question.component"
v-if="index + 1 === stepNo" v-if="index + 1 === stepNo && feedbackData != undefined"
v-model="feedbackData[question.modelKey] as any" v-model="feedbackData[question.modelKey] as any"
:items="question['items']" :items="question['items']"
:cy-key="question.modelKey" :cy-key="question.modelKey"
@ -269,14 +204,11 @@ onMounted(async () => {
<!-- eslint-enable --> <!-- eslint-enable -->
</div> </div>
<FeedbackCompletition <FeedbackCompletition
v-if="stepNo === 11" v-if="stepNo === numSteps - 1"
:avatar-url="circleExperts[0].avatar_url" :avatar-url="avatarUrl"
:title=" :show-avatar="showAvatar"
$t('feedback.completionTitle', { :title="completionTitle"
name: `${circleExperts[0].first_name} ${circleExperts[0].last_name}`, :description="completionDescription"
})
"
:description="$t('feedback.completionDescription')"
:feedback-sent="feedbackSubmitted" :feedback-sent="feedbackSubmitted"
@send-feedback="mutateFeedback(feedbackData, true)" @send-feedback="mutateFeedback(feedbackData, true)"
/> />

View File

@ -0,0 +1,114 @@
<script setup lang="ts">
import ItRadioGroup from "@/components/ui/ItRadioGroup.vue";
import ItTextarea from "@/components/ui/ItTextarea.vue";
import {
PERCENTAGES,
RATINGS,
YES_NO,
} from "@/pages/learningPath/learningContentPage/feedback/feedback.constants";
import type { LearningContentFeedbackUK, LearningContentFeedbackVV } from "@/types";
import { computed } from "vue";
import { useTranslation } from "i18next-vue";
import { useCourseSessionDetailQuery } from "@/composables";
import FeedbackBase from "@/pages/learningPath/learningContentPage/feedback/FeedbackBase.vue";
const props = defineProps<{
content: LearningContentFeedbackVV | LearningContentFeedbackUK;
}>();
const courseSessionDetailResult = useCourseSessionDetailQuery();
const { t } = useTranslation();
const circleExperts = computed(() => {
if (props.content?.circle?.slug) {
return courseSessionDetailResult.filterCircleExperts(props.content.circle.slug);
}
return [];
});
const stepLabels = [
t("general.introduction"),
t("feedback.satisfactionLabel"),
t("feedback.goalAttainmentLabel"),
t("feedback.proficiencyLabel"),
t("feedback.preparationTaskClarityLabel"),
t("feedback.instructorCompetenceLabel"),
t("feedback.instructorRespectLabel"),
t("feedback.instructorOpenFeedbackLabel"),
t("feedback.recommendLabel"),
t("feedback.coursePositiveFeedbackLabel"),
t("feedback.courseNegativeFeedbackLabel"),
t("general.submission"),
];
const questionData = [
{
modelKey: "satisfaction",
items: RATINGS,
component: ItRadioGroup,
},
{
modelKey: "goal_attainment",
items: RATINGS,
component: ItRadioGroup,
},
{
modelKey: "proficiency",
items: PERCENTAGES,
component: ItRadioGroup,
},
{
modelKey: "preparation_task_clarity",
items: YES_NO,
component: ItRadioGroup,
},
{
modelKey: "instructor_competence",
items: RATINGS,
component: ItRadioGroup,
},
{
modelKey: "instructor_respect",
items: RATINGS,
component: ItRadioGroup,
},
{
modelKey: "instructor_open_feedback",
component: ItTextarea,
},
{
modelKey: "would_recommend",
items: YES_NO,
component: ItRadioGroup,
},
{
modelKey: "course_positive_feedback",
component: ItTextarea,
},
{
modelKey: "course_negative_feedback",
component: ItTextarea,
},
];
</script>
<template>
<FeedbackBase
:step-labels="stepLabels"
:question-data="questionData"
:content="props.content"
:introduction="
$t('feedback.intro', {
name: `${circleExperts[0]?.first_name} ${circleExperts[0]?.last_name}`,
})
"
:title="$t('feedback.areYouSatisfied')"
:completion-title="
$t('feedback.completionTitle', {
name: `${circleExperts[0].first_name} ${circleExperts[0].last_name}`,
})
"
:completion-description="$t('feedback.completionDescription')"
:show-avatar="true"
/>
</template>

View File

@ -0,0 +1,79 @@
<script setup lang="ts">
import ItRadioGroup from "@/components/ui/ItRadioGroup.vue";
import ItTextarea from "@/components/ui/ItTextarea.vue";
import {
PERCENTAGES,
RATINGS,
YES_NO,
} from "@/pages/learningPath/learningContentPage/feedback/feedback.constants";
import type { LearningContentFeedbackUK, LearningContentFeedbackVV } from "@/types";
import FeedbackBase from "@/pages/learningPath/learningContentPage/feedback/FeedbackBase.vue";
import { useTranslation } from "i18next-vue";
const props = defineProps<{
content: LearningContentFeedbackVV | LearningContentFeedbackUK;
}>();
const { t } = useTranslation();
const stepLabels = [
t("general.introduction"),
t("feedback.satisfactionLabel"),
t("feedback.goalAttainmentLabel"),
t("feedback.proficiencyLabelVV"),
t("feedback.praxisAssignmentClarity"),
t("feedback.recommendLabelVV"),
t("feedback.coursePositiveFeedbackLabel"),
t("feedback.courseNegativeFeedbackLabel"),
t("general.submission"),
];
const questionData = [
{
modelKey: "satisfaction",
items: RATINGS,
component: ItRadioGroup,
},
{
modelKey: "goal_attainment",
items: RATINGS,
component: ItRadioGroup,
},
{
modelKey: "proficiency",
items: PERCENTAGES,
component: ItRadioGroup,
},
{
modelKey: "preparation_task_clarity",
items: YES_NO,
component: ItRadioGroup,
},
{
modelKey: "would_recommend",
items: YES_NO,
component: ItRadioGroup,
},
{
modelKey: "course_positive_feedback",
component: ItTextarea,
},
{
modelKey: "course_negative_feedback",
component: ItTextarea,
},
];
</script>
<template>
<FeedbackBase
:step-labels="stepLabels"
:question-data="questionData"
:content="props.content"
:introduction="$t('a.feedback.introductionVV')"
:title="$t('Feedback')"
:completion-title="$t('feedback.completionDescriptionVV')"
:completion-description="$t('feedback.completionDescriptionVV')"
:show-avatar="false"
/>
</template>

View File

@ -4,7 +4,11 @@
<div <div
class="b-0 flex flex-col lg:flex-row lg:items-center lg:border lg:border-gray-400 lg:p-8" class="b-0 flex flex-col lg:flex-row lg:items-center lg:border lg:border-gray-400 lg:p-8"
> >
<img :src="avatarUrl" class="mb-6 h-16 w-16 rounded-full lg:mr-12" /> <img
v-if="showAvatar"
:src="avatarUrl"
class="mb-6 h-16 w-16 rounded-full lg:mr-12"
/>
<h2 class="mb-8 block lg:hidden">{{ title }}</h2> <h2 class="mb-8 block lg:hidden">{{ title }}</h2>
<div> <div>
<p class="mb-6">{{ description }}</p> <p class="mb-6">{{ description }}</p>
@ -33,6 +37,7 @@ interface Props {
title?: string; title?: string;
description?: string; description?: string;
feedbackSent?: boolean; feedbackSent?: boolean;
showAvatar?: boolean;
} }
withDefaults(defineProps<Props>(), { withDefaults(defineProps<Props>(), {
@ -40,6 +45,7 @@ withDefaults(defineProps<Props>(), {
title: "", title: "",
description: "", description: "",
feedbackSent: false, feedbackSent: false,
showAvatar: true,
}); });
defineEmits(["sendFeedback"]); defineEmits(["sendFeedback"]);

View File

@ -11,7 +11,8 @@ import type {
LearningContentAttendanceCourseObjectType, LearningContentAttendanceCourseObjectType,
LearningContentDocumentListObjectType, LearningContentDocumentListObjectType,
LearningContentEdoniqTestObjectType, LearningContentEdoniqTestObjectType,
LearningContentFeedbackObjectType, LearningContentFeedbackUkObjectType,
LearningContentFeedbackVvObjectType,
LearningContentKnowledgeAssessmentObjectType, LearningContentKnowledgeAssessmentObjectType,
LearningContentLearningModuleObjectType, LearningContentLearningModuleObjectType,
LearningContentMediaLibraryObjectType, LearningContentMediaLibraryObjectType,
@ -68,8 +69,12 @@ export type LearningContentEdoniqTest = LearningContentEdoniqTestObjectType & {
readonly content_type: "learnpath.LearningContentEdoniqTest"; readonly content_type: "learnpath.LearningContentEdoniqTest";
}; };
export type LearningContentFeedback = LearningContentFeedbackObjectType & { export type LearningContentFeedbackVV = LearningContentFeedbackVvObjectType & {
readonly content_type: "learnpath.LearningContentFeedback"; readonly content_type: "learnpath.LearningContentFeedbackVV";
};
export type LearningContentFeedbackUK = LearningContentFeedbackUkObjectType & {
readonly content_type: "learnpath.LearningContentFeedbackUK";
}; };
export type LearningContentLearningModule = LearningContentLearningModuleObjectType & { export type LearningContentLearningModule = LearningContentLearningModuleObjectType & {
@ -102,7 +107,8 @@ export type LearningContent =
| LearningContentAttendanceCourse | LearningContentAttendanceCourse
| LearningContentDocumentList | LearningContentDocumentList
| LearningContentEdoniqTest | LearningContentEdoniqTest
| LearningContentFeedback | LearningContentFeedbackUK
| LearningContentFeedbackVV
| LearningContentLearningModule | LearningContentLearningModule
| LearningContentKnowledgeAssessment | LearningContentKnowledgeAssessment
| LearningContentMediaLibrary | LearningContentMediaLibrary
@ -560,3 +566,13 @@ export type DueDate = SimpleDueDate & {
course_session_id: string; course_session_id: string;
circle: CircleLight | null; circle: CircleLight | null;
}; };
export type FeedbackType = "uk" | "vv";
export interface FeedbackData {
amount: number;
questions: {
[key: string]: any;
};
feedbackType: FeedbackType;
}

View File

@ -46,7 +46,8 @@ export function learningContentTypeData(
return { title: t("learningContentTypes.test"), icon: "it-icon-lc-test" }; return { title: t("learningContentTypes.test"), icon: "it-icon-lc-test" };
case "learnpath.LearningContentRichText": case "learnpath.LearningContentRichText":
return { title: t("learningContentTypes.text"), icon: "it-icon-lc-resource" }; return { title: t("learningContentTypes.text"), icon: "it-icon-lc-resource" };
case "learnpath.LearningContentFeedback": case "learnpath.LearningContentFeedbackUK":
case "learnpath.LearningContentFeedbackVV":
return { title: t("learningContentTypes.feedback"), icon: "it-icon-lc-feedback" }; return { title: t("learningContentTypes.feedback"), icon: "it-icon-lc-feedback" };
case "learnpath.LearningContentPlaceholder": case "learnpath.LearningContentPlaceholder":
return { return {

View File

@ -74,7 +74,7 @@ describe("dashboardSupervisor.cy.js", () => {
describe("feedback summary box", () => { describe("feedback summary box", () => {
it("contains correct numbers", () => { it("contains correct numbers", () => {
getDashboardStatistics("feedback.average").should("have.text", "3.3"); getDashboardStatistics("feedback.average").should("have.text", "3.3");
getDashboardStatistics("feedback.count").should("have.text", "3"); getDashboardStatistics("feedback.count").should("have.text", "6");
}); });
it("contains correct details link", () => { it("contains correct details link", () => {
clickOnDetailsLink("feedback"); clickOnDetailsLink("feedback");

View File

@ -5,153 +5,359 @@ describe("feedbackStudent.cy.js", () => {
beforeEach(() => { beforeEach(() => {
cy.manageCommand("cypress_reset"); cy.manageCommand("cypress_reset");
login("test-student1@example.com", "test"); login("test-student1@example.com", "test");
cy.visit("/course/test-lehrgang/learn/fahrzeug/feedback");
}); });
it("can open feedback page", () => { describe("Feedback UK", () => {
cy.testLearningContentTitle("Kursfeedback"); beforeEach(() => {
cy.testLearningContentSubtitle("Feedback"); cy.visit("/course/test-lehrgang/learn/fahrzeug/feedback");
});
it("can open feedback page", () => {
cy.testLearningContentTitle("Kursfeedback");
cy.testLearningContentSubtitle("Feedback");
});
it("can create feedback by giving answers to all steps", () => {
// initial wait for step 0 (or none with step==0) is required for pipelines
cy.url().should((url) => {
expect(url).to.match(/\/fahrzeug\/feedback(\?step=0)?$/);
});
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// fill feedback form
// step 1
cy.url().should("include", "step=1");
cy.get('[data-cy="question-1"]').should(
"contain",
"Zufriedenheit insgesamt"
);
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="radio-4"]').click();
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 2
cy.url().should("include", "step=2");
cy.get('[data-cy="question-2"]').should(
"contain",
"Zielerreichung insgesamt"
);
cy.get('[data-cy="next-step"]').should("be.disabled");
// the system should store after every step -> check stored data
cy.loadFeedbackResponse("feedback_user_id", TEST_STUDENT1_USER_ID).then(
(ac) => {
expect(ac.submitted).to.be.false;
expect(ac.data.satisfaction).to.equal(4);
expect(ac.data.instructor_competence).to.equal(null);
}
);
cy.get('[data-cy="radio-3"]').click();
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 3
cy.url().should("include", "step=3");
cy.get('[data-cy="question-3"]').should(
"contain",
"Wie beurteilst du deine Sicherheit bezüglichen den Themen nach dem Kurs?"
);
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="radio-80"]').click();
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 4
cy.url().should("include", "step=4");
cy.get('[data-cy="question-4"]').should(
"contain",
"Waren die Vorbereitungsaufträge klar und verständlich?"
);
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="radio-false"]').click();
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 5
cy.url().should("include", "step=5");
cy.get('[data-cy="question-5"]').should(
"contain",
"Wie beurteilst du die Themensicherheit und Fachkompetenz des Kursleiters/der Kursleiterin?"
);
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="radio-2"]').click();
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 6
cy.url().should("include", "step=6");
cy.get('[data-cy="question-6"]').should(
"contain",
"Wurden Fragen und Anregungen der Kursteilnehmenden ernst genommen und aufgegriffen?"
);
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="radio-1"]').click();
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 7
cy.url().should("include", "step=7");
cy.get('[data-cy="question-7"]').should(
"contain",
"Was möchtest du dem Kursleiter/der Kursleiterin sonst noch sagen?"
);
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="it-textarea-instructor_open_feedback"]').type(
"Der Kursleiter ist eigentlich ganz nett."
);
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 8
cy.url().should("include", "step=8");
cy.get('[data-cy="question-8"]').should(
"contain",
"Würdest du den Kurs weiterempfehlen?"
);
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="radio-true"]').click();
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 9
cy.url().should("include", "step=9");
cy.get('[data-cy="question-9"]').should(
"contain",
"Was hat dir besonders gut gefallen?"
);
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="it-textarea-course_positive_feedback"]').type(
"Ich bin zufrieden mit den meisten Dingen."
);
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 10
cy.url().should("include", "step=10");
cy.get('[data-cy="question-10"]').should(
"contain",
"Wo siehst du Verbesserungspotential?"
);
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="it-textarea-course_negative_feedback"]').type(
"Ich bin unzufrieden mit einigen Sachen."
);
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
cy.url().should("include", "step=11");
cy.get('[data-cy="sendFeedbackButton"]').click();
cy.get('[data-cy="complete-and-continue"]').click({ force: true });
// marked complete in circle
cy.url().should((url) => {
expect(url).to.match(/\/fahrzeug#lu-transfer?$/);
});
cy.reload();
cy.get(
'[data-cy="test-lehrgang-lp-circle-fahrzeug-lc-feedback-checkbox"]'
).should("have.class", "cy-checked");
// reopening page should get directly to last step
cy.visit("/course/test-lehrgang/learn/fahrzeug/feedback");
cy.url().should("include", "step=11");
// check stored data
cy.loadFeedbackResponse("feedback_user_id", TEST_STUDENT1_USER_ID).then(
(ac) => {
expect(ac.submitted).to.be.true;
expect(ac.data).to.deep.equal({
course_negative_feedback: "Ich bin unzufrieden mit einigen Sachen.",
course_positive_feedback:
"Ich bin zufrieden mit den meisten Dingen.",
goal_attainment: 3,
instructor_competence: 2,
instructor_open_feedback:
"Der Kursleiter ist eigentlich ganz nett.",
instructor_respect: 1,
preparation_task_clarity: false,
proficiency: 80,
satisfaction: 4,
would_recommend: true,
feedback_type: "uk",
});
}
);
});
}); });
it("can create feedback by giving answers to all steps", () => { describe("Feedback VV", () => {
// initial wait for step 0 (or none with step==0) is required for pipelines beforeEach(() => {
cy.url().should((url) => { cy.visit("/course/test-lehrgang/learn/reisen/feedback");
expect(url).to.match(/\/fahrzeug\/feedback(\?step=0)?$/);
}); });
cy.wait(200); it("can open feedback page", () => {
cy.learningContentMultiLayoutNextStep(); cy.testLearningContentTitle("Feedback");
cy.wait(200); cy.testLearningContentSubtitle("Feedback");
// fill feedback form
// step 1
cy.url().should("include", "step=1");
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="radio-4"]').click();
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 2
cy.url().should("include", "step=2");
cy.get('[data-cy="next-step"]').should("be.disabled");
// the system should store after every step -> check stored data
cy.loadFeedbackResponse("feedback_user_id", TEST_STUDENT1_USER_ID).then(
(ac) => {
expect(ac.submitted).to.be.false;
expect(ac.data.satisfaction).to.equal(4);
expect(ac.data.instructor_competence).to.equal(null);
}
);
cy.get('[data-cy="radio-3"]').click();
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 3
cy.url().should("include", "step=3");
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="radio-80"]').click();
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 4
cy.url().should("include", "step=4");
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="radio-false"]').click();
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 5
cy.url().should("include", "step=5");
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="radio-2"]').click();
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 6
cy.url().should("include", "step=6");
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="radio-1"]').click();
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 7
cy.url().should("include", "step=7");
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="it-textarea-instructor_open_feedback"]').type(
"Der Kursleiter ist eigentlich ganz nett."
);
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 8
cy.url().should("include", "step=8");
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="radio-true"]').click();
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 9
cy.url().should("include", "step=9");
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="it-textarea-course_positive_feedback"]').type(
"Ich bin zufrieden mit den meisten Dingen."
);
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 10
cy.url().should("include", "step=10");
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="it-textarea-course_negative_feedback"]').type(
"Ich bin unzufrieden mit einigen Sachen."
);
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
cy.url().should("include", "step=11");
cy.get('[data-cy="sendFeedbackButton"]').click();
cy.get('[data-cy="complete-and-continue"]').click({ force: true });
// marked complete in circle
cy.url().should((url) => {
expect(url).to.match(/\/fahrzeug#lu-transfer?$/);
}); });
cy.reload();
cy.get(
'[data-cy="test-lehrgang-lp-circle-fahrzeug-lc-feedback-checkbox"]'
).should("have.class", "cy-checked");
// reopening page should get directly to last step it("can create feedback by giving answers to all steps", () => {
cy.visit("/course/test-lehrgang/learn/fahrzeug/feedback"); // initial wait for step 0 (or none with step==0) is required for pipelines
cy.url().should("include", "step=11"); cy.url().should((url) => {
expect(url).to.match(/\/reisen\/feedback(\?step=0)?$/);
});
cy.get('[data-cy="introduction"]').contains(
"Wir bitten dich um dein Feedback. Es hilft uns, damit wir deine Lernerlebnisse verbessern können."
);
// check stored data cy.wait(200);
cy.loadFeedbackResponse("feedback_user_id", TEST_STUDENT1_USER_ID).then( cy.learningContentMultiLayoutNextStep();
(ac) => { cy.wait(200);
expect(ac.submitted).to.be.true;
expect(ac.data).to.deep.equal({ // fill feedback form
course_negative_feedback: "Ich bin unzufrieden mit einigen Sachen.", // step 1
course_positive_feedback: "Ich bin zufrieden mit den meisten Dingen.", cy.url().should("include", "step=1");
goal_attainment: 3, cy.get('[data-cy="question-1"]').should(
instructor_competence: 2, "contain",
instructor_open_feedback: "Der Kursleiter ist eigentlich ganz nett.", "Zufriedenheit insgesamt"
instructor_respect: 1, );
preparation_task_clarity: false, cy.get('[data-cy="next-step"]').should("be.disabled");
proficiency: 80, cy.get('[data-cy="radio-4"]').click();
satisfaction: 4, cy.wait(200);
would_recommend: true, cy.learningContentMultiLayoutNextStep();
}); cy.wait(200);
}
); // step 2
cy.url().should("include", "step=2");
cy.get('[data-cy="question-2"]').should(
"contain",
"Zielerreichung insgesamt"
);
cy.get('[data-cy="next-step"]').should("be.disabled");
// the system should store after every step -> check stored data
cy.loadFeedbackResponse("feedback_user_id", TEST_STUDENT1_USER_ID).then(
(ac) => {
expect(ac.submitted).to.be.false;
expect(ac.data.satisfaction).to.equal(4);
expect(ac.data.course_positive_feedback).to.equal(null);
}
);
cy.get('[data-cy="radio-3"]').click();
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 3
cy.url().should("include", "step=3");
cy.get('[data-cy="question-3"]').should(
"contain",
"Wie beurteilst du deine Sicherheit bezüglichen den Themen nach dem Circle?"
);
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="radio-80"]').click();
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 4
cy.url().should("include", "step=4");
cy.get('[data-cy="question-4"]').should(
"contain",
"Waren die Praxisaufträge klar und verständlich?"
);
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="radio-false"]').click();
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 5
cy.url().should("include", "step=5");
cy.get('[data-cy="question-5"]').should(
"contain",
"Würdest du den Circle weiterempfehlen?"
);
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="radio-false"]').click();
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 6
cy.url().should("include", "step=6");
cy.get('[data-cy="question-6"]').should(
"contain",
"Was hat dir besonders gut gefallen?"
);
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="it-textarea-course_positive_feedback"]').type(
"Der Circle ist eigentlich ganz nett."
);
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 7
cy.url().should("include", "step=7");
cy.get('[data-cy="question-7"]').should(
"contain",
"Wo siehst du Verbesserungspotential?"
);
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="it-textarea-course_negative_feedback"]').type(
"Ich bin unzufrieden mit einigen Sachen."
);
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
cy.url().should("include", "step=8");
cy.get('[data-cy="sendFeedbackButton"]').click();
cy.get('[data-cy="complete-and-continue"]').click({ force: true });
// marked complete in circle
cy.url().should((url) => {
expect(url).to.match(/\/reisen#lu-transfer-reflexion-feedback?$/);
});
cy.reload();
cy.get(
'[data-cy="test-lehrgang-lp-circle-reisen-lc-feedback-checkbox"]'
).should("have.class", "cy-checked");
// reopening page should get directly to last step
cy.visit("/course/test-lehrgang/learn/reisen/feedback");
cy.url().should("include", "step=8");
// check stored data
cy.loadFeedbackResponse("feedback_user_id", TEST_STUDENT1_USER_ID).then(
(ac) => {
expect(ac.submitted).to.be.true;
expect(ac.data).to.deep.equal({
course_negative_feedback: "Ich bin unzufrieden mit einigen Sachen.",
course_positive_feedback: "Der Circle ist eigentlich ganz nett.",
goal_attainment: 3,
preparation_task_clarity: false,
proficiency: 80,
satisfaction: 4,
would_recommend: false,
feedback_type: "vv",
});
}
);
});
}); });
}); });

View File

@ -16,77 +16,216 @@ describe("feedbackTrainer.cy.js", () => {
cy.get('[data-cy="feedback-data-amount"]').should("contain", "0"); cy.get('[data-cy="feedback-data-amount"]').should("contain", "0");
}); });
it("can open feedback results page with results", () => { describe("FeedbackUK", function () {
cy.manageCommand("cypress_reset --create-feedback-responses"); it("can open feedback results page with results", () => {
login("test-trainer1@example.com", "test"); cy.manageCommand("cypress_reset --create-feedback-responses");
cy.visit("/course/test-lehrgang/cockpit"); login("test-trainer1@example.com", "test");
cy.get( cy.visit("/course/test-lehrgang/cockpit");
'[data-cy="show-feedback-btn-test-lehrgang-lp-circle-fahrzeug-lc-feedback"]' cy.get(
).click(); '[data-cy="show-feedback-btn-test-lehrgang-lp-circle-fahrzeug-lc-feedback"]'
).click();
cy.get('[data-cy="feedback-data-amount"]').should("contain", "3"); cy.get('[data-cy="feedback-data-amount"]').should("contain", "3");
cy.get('[data-cy="question-1"]') // check titles of questions
.find('[data-cy="rating-scale-average"]') cy.get('[data-cy="question-1"]').should(
.should("contain", "3.3"); "contain",
"Zufriedenheit insgesamt"
);
cy.get('[data-cy="question-2"]').should(
"contain",
"Zielerreichung insgesamt"
);
cy.get('[data-cy="question-3"]').should(
"contain",
"Wie beurteilst du deine Sicherheit bezüglichen den Themen nach dem Kurs?"
);
cy.get('[data-cy="question-4"]').should(
"contain",
"Waren die Vorbereitungsaufträge klar und verständlich?"
);
cy.get('[data-cy="question-5"]').should(
"contain",
"Wie beurteilst du die Themensicherheit und Fachkompetenz des Kursleiters/der Kursleiterin?"
);
cy.get('[data-cy="question-6"]').should(
"contain",
"Wurden Fragen und Anregungen der Kursteilnehmenden ernst genommen und aufgegriffen?"
);
cy.get('[data-cy="question-7"]').should(
"contain",
"Was möchtest du dem Kursleiter/der Kursleiterin sonst noch sagen?"
);
cy.get('[data-cy="question-8"]').should(
"contain",
"Würdest du den Kurs weiterempfehlen?"
);
cy.get('[data-cy="question-9"]').should(
"contain",
"Wo siehst du Verbesserungspotential?"
);
cy.get('[data-cy="question-10"]').should(
"contain",
"Was hat dir besonders gut gefallen?"
);
cy.get('[data-cy="question-2"]') cy.get('[data-cy="question-1"]')
.find('[data-cy="rating-scale-average"]') .find('[data-cy="rating-scale-average"]')
.should("contain", "3.0"); .should("contain", "3.3");
cy.get('[data-cy="question-3"]') cy.get('[data-cy="question-2"]')
.find('[data-cy="percentage-value-40%"]') .find('[data-cy="rating-scale-average"]')
.should("contain", "33.3"); .should("contain", "3.0");
cy.get('[data-cy="question-3"]')
.find('[data-cy="percentage-value-80%"]')
.should("contain", "33.3");
cy.get('[data-cy="question-3"]')
.find('[data-cy="percentage-value-100%"]')
.should("contain", "33.3");
cy.get('[data-cy="question-4"]') cy.get('[data-cy="question-3"]')
.find('[data-cy="popover-yes"]') .find('[data-cy="percentage-value-40%"]')
.click() .should("contain", "33.3");
.find('[data-cy="num-yes"]') cy.get('[data-cy="question-3"]')
.should("contain", "3"); .find('[data-cy="percentage-value-80%"]')
cy.get('[data-cy="question-4"]') .should("contain", "33.3");
.find('[data-cy="popover-no"]') cy.get('[data-cy="question-3"]')
.click() .find('[data-cy="percentage-value-100%"]')
.find('[data-cy="num-no"]') .should("contain", "33.3");
.should("contain", "0");
cy.get('[data-cy="question-5"]') cy.get('[data-cy="question-4"]')
.find('[data-cy="rating-scale-average"]') .find('[data-cy="popover-yes"]')
.should("contain", "2.7"); .click()
.find('[data-cy="num-yes"]')
.should("contain", "3");
cy.get('[data-cy="question-4"]')
.find('[data-cy="popover-no"]')
.click()
.find('[data-cy="num-no"]')
.should("contain", "0");
cy.get('[data-cy="question-6"]') cy.get('[data-cy="question-5"]')
.find('[data-cy="rating-scale-average"]') .find('[data-cy="rating-scale-average"]')
.should("contain", "3.0"); .should("contain", "2.7");
cy.get('[data-cy="question-7"]') cy.get('[data-cy="question-6"]')
.should("contain", "Super Kurs!") .find('[data-cy="rating-scale-average"]')
.should("contain", "Super, bin begeistert") .should("contain", "3.0");
.should("contain", "Ok, entspricht den Erwartungen");
cy.get('[data-cy="question-8"]') cy.get('[data-cy="question-7"]')
.find('[data-cy="popover-yes"]') .should("contain", "Super Kurs!")
.click() .should("contain", "Super, bin begeistert")
.find('[data-cy="num-yes"]') .should("contain", "Ok, entspricht den Erwartungen");
.should("contain", "2");
cy.get('[data-cy="question-8"]')
.find('[data-cy="popover-no"]')
.click()
.find('[data-cy="num-no"]')
.should("contain", "1");
cy.get('[data-cy="question-9"]') cy.get('[data-cy="question-8"]')
.should("contain", "Nichts Schlechtes") .find('[data-cy="popover-yes"]')
.should("contain", "Es wäre praktisch, Zugang zu einer FAQ zu haben.") .click()
.should("contain", "Mehr Videos wären schön."); .find('[data-cy="num-yes"]')
.should("contain", "2");
cy.get('[data-cy="question-8"]')
.find('[data-cy="popover-no"]')
.click()
.find('[data-cy="num-no"]')
.should("contain", "1");
cy.get('[data-cy="question-10"]') cy.get('[data-cy="question-9"]')
.should("contain", "Nur Gutes.") .should("contain", "Nichts Schlechtes")
.should("contain", "Das Beispiel mit der Katze fand ich sehr gut") .should("contain", "Es wäre praktisch, Zugang zu einer FAQ zu haben.")
.should("contain", "Die Präsentation war super"); .should("contain", "Mehr Videos wären schön.");
cy.get('[data-cy="question-10"]')
.should("contain", "Nur Gutes.")
.should("contain", "Das Beispiel mit der Katze fand ich sehr gut")
.should("contain", "Die Präsentation war super");
});
});
describe("FeedbackVV", function () {
it("can open feedback results page with results", () => {
cy.manageCommand("cypress_reset --create-feedback-responses");
login("test-trainer1@example.com", "test");
cy.visit("/course/test-lehrgang/cockpit");
cy.get('[data-cy="dropdown-select"]').click();
cy.get('[data-cy="dropdown-select-option-Reisen"]').click();
cy.get(
'[data-cy="show-feedback-btn-test-lehrgang-lp-circle-reisen-lc-feedback"]'
).click();
cy.get('[data-cy="feedback-data-amount"]').should("contain", "3");
// check titles of questions
cy.get('[data-cy="question-1"]').should(
"contain",
"Zufriedenheit insgesamt"
);
cy.get('[data-cy="question-2"]').should(
"contain",
"Zielerreichung insgesamt"
);
cy.get('[data-cy="question-3"]').should(
"contain",
"Wie beurteilst du deine Sicherheit bezüglichen den Themen nach dem Circle?"
);
cy.get('[data-cy="question-4"]').should(
"contain",
"Waren die Praxisaufträge klar und verständlich?"
);
cy.get('[data-cy="question-5"]').should(
"contain",
"Würdest du den Circle weiterempfehlen?"
);
cy.get('[data-cy="question-6"]').should(
"contain",
"Wo siehst du Verbesserungspotential?"
);
cy.get('[data-cy="question-7"]').should(
"contain",
"Was hat dir besonders gut gefallen?"
);
cy.get('[data-cy="question-1"]')
.find('[data-cy="rating-scale-average"]')
.should("contain", "3.3");
cy.get('[data-cy="question-2"]')
.find('[data-cy="rating-scale-average"]')
.should("contain", "3.0");
cy.get('[data-cy="question-3"]')
.find('[data-cy="percentage-value-40%"]')
.should("contain", "33.3");
cy.get('[data-cy="question-3"]')
.find('[data-cy="percentage-value-80%"]')
.should("contain", "33.3");
cy.get('[data-cy="question-3"]')
.find('[data-cy="percentage-value-100%"]')
.should("contain", "33.3");
cy.get('[data-cy="question-4"]')
.find('[data-cy="popover-yes"]')
.click()
.find('[data-cy="num-yes"]')
.should("contain", "3");
cy.get('[data-cy="question-4"]')
.find('[data-cy="popover-no"]')
.click()
.find('[data-cy="num-no"]')
.should("contain", "0");
cy.get('[data-cy="question-5"]')
.find('[data-cy="popover-yes"]')
.click()
.find('[data-cy="num-yes"]')
.should("contain", "2");
cy.get('[data-cy="question-5"]')
.find('[data-cy="popover-no"]')
.click()
.find('[data-cy="num-no"]')
.should("contain", "1");
cy.get('[data-cy="question-6"]')
.should("contain", "Nichts Schlechtes")
.should("contain", "Es wäre praktisch, Zugang zu einer FAQ zu haben.")
.should("contain", "Mehr Videos wären schön.");
cy.get('[data-cy="question-7"]')
.should("contain", "Nur Gutes.")
.should("contain", "Das Beispiel mit der Katze fand ich sehr gut")
.should("contain", "Die Präsentation war super");
});
}); });
}); });

View File

@ -32,7 +32,8 @@ from vbv_lernwelt.course_session.services.attendance import AttendanceUserStatus
from vbv_lernwelt.feedback.models import FeedbackResponse from vbv_lernwelt.feedback.models import FeedbackResponse
from vbv_lernwelt.learnpath.models import ( from vbv_lernwelt.learnpath.models import (
LearningContentAttendanceCourse, LearningContentAttendanceCourse,
LearningContentFeedback, LearningContentFeedbackUK,
LearningContentFeedbackVV,
) )
from vbv_lernwelt.notify.models import Notification from vbv_lernwelt.notify.models import Notification
@ -155,7 +156,9 @@ def command(
if create_feedback_responses: if create_feedback_responses:
print("create_feedback_responses") print("create_feedback_responses")
course_session = CourseSession.objects.get(id=TEST_COURSE_SESSION_BERN_ID) course_session = CourseSession.objects.get(id=TEST_COURSE_SESSION_BERN_ID)
learning_content_feedback_page = LearningContentFeedback.objects.get(
# feedback fahrzeug
learning_content_feedback_page = LearningContentFeedbackUK.objects.get(
slug="test-lehrgang-lp-circle-fahrzeug-lc-feedback" slug="test-lehrgang-lp-circle-fahrzeug-lc-feedback"
) )
create_feedback_response_data( create_feedback_response_data(
@ -174,6 +177,7 @@ def command(
"would_recommend": True, "would_recommend": True,
"course_negative_feedback": "Nichts Schlechtes", "course_negative_feedback": "Nichts Schlechtes",
"course_positive_feedback": "Nur Gutes.", "course_positive_feedback": "Nur Gutes.",
"feedback_type": "uk",
}, },
) )
@ -193,6 +197,7 @@ def command(
"would_recommend": True, "would_recommend": True,
"course_negative_feedback": "Es wäre praktisch, Zugang zu einer FAQ zu haben.", "course_negative_feedback": "Es wäre praktisch, Zugang zu einer FAQ zu haben.",
"course_positive_feedback": "Das Beispiel mit der Katze fand ich sehr gut veranschaulicht!", "course_positive_feedback": "Das Beispiel mit der Katze fand ich sehr gut veranschaulicht!",
"feedback_type": "uk",
}, },
) )
@ -212,6 +217,62 @@ def command(
"would_recommend": False, "would_recommend": False,
"course_negative_feedback": "Mehr Videos wären schön.", "course_negative_feedback": "Mehr Videos wären schön.",
"course_positive_feedback": "Die Präsentation war super", "course_positive_feedback": "Die Präsentation war super",
"feedback_type": "uk",
},
)
# feedback reisen
learning_content_feedback_page = LearningContentFeedbackVV.objects.get(
slug="test-lehrgang-lp-circle-reisen-lc-feedback"
)
create_feedback_response_data(
feedback_user=User.objects.get(id=TEST_STUDENT1_USER_ID),
course_session=course_session,
learning_content_feedback_page=learning_content_feedback_page,
submitted=True,
feedback_data={
"satisfaction": 4,
"goal_attainment": 3,
"proficiency": 80,
"preparation_task_clarity": True,
"would_recommend": True,
"course_negative_feedback": "Nichts Schlechtes",
"course_positive_feedback": "Nur Gutes.",
"feedback_type": "vv",
},
)
create_feedback_response_data(
feedback_user=User.objects.get(id=TEST_STUDENT2_USER_ID),
course_session=course_session,
learning_content_feedback_page=learning_content_feedback_page,
submitted=True,
feedback_data={
"satisfaction": 4,
"goal_attainment": 4,
"proficiency": 100,
"preparation_task_clarity": True,
"would_recommend": True,
"course_negative_feedback": "Es wäre praktisch, Zugang zu einer FAQ zu haben.",
"course_positive_feedback": "Das Beispiel mit der Katze fand ich sehr gut veranschaulicht!",
"feedback_type": "vv",
},
)
create_feedback_response_data(
feedback_user=User.objects.get(id=TEST_STUDENT3_USER_ID),
course_session=course_session,
learning_content_feedback_page=learning_content_feedback_page,
submitted=True,
feedback_data={
"satisfaction": 2,
"goal_attainment": 2,
"proficiency": 40,
"preparation_task_clarity": True,
"would_recommend": False,
"course_negative_feedback": "Mehr Videos wären schön.",
"course_positive_feedback": "Die Präsentation war super",
"feedback_type": "vv",
}, },
) )

View File

@ -70,7 +70,8 @@ from vbv_lernwelt.learnpath.tests.learning_path_factories import (
LearningContentAssignmentFactory, LearningContentAssignmentFactory,
LearningContentAttendanceCourseFactory, LearningContentAttendanceCourseFactory,
LearningContentEdoniqTestFactory, LearningContentEdoniqTestFactory,
LearningContentFeedbackFactory, LearningContentFeedbackUKFactory,
LearningContentFeedbackVVFactory,
LearningContentKnowledgeAssessmentFactory, LearningContentKnowledgeAssessmentFactory,
LearningContentLearningModuleFactory, LearningContentLearningModuleFactory,
LearningContentMediaLibraryFactory, LearningContentMediaLibraryFactory,
@ -371,6 +372,7 @@ def create_feedback_response_data(
"would_recommend": True, "would_recommend": True,
"course_negative_feedback": "Nichts Schlechtes", "course_negative_feedback": "Nichts Schlechtes",
"course_positive_feedback": "Nur Gutes.", "course_positive_feedback": "Nur Gutes.",
"feedback_type": "uk",
} }
return update_feedback_response( return update_feedback_response(
@ -549,7 +551,8 @@ damit du erfolgreich mit deinem Lernpfad (durch-)starten kannst.
slug__startswith="test-lehrgang-assignment-überprüfen-einer-motorfahrzeugs" slug__startswith="test-lehrgang-assignment-überprüfen-einer-motorfahrzeugs"
), ),
) )
LearningContentFeedbackFactory( LearningContentFeedbackUKFactory(
title="Feedback",
parent=circle, parent=circle,
) )
@ -633,7 +636,8 @@ def create_test_circle_reisen(lp):
title="Reflexion", title="Reflexion",
parent=parent, parent=parent,
) )
LearningContentFeedbackFactory( LearningContentFeedbackVVFactory(
title="Feedback",
parent=parent, parent=parent,
) )

View File

@ -22,7 +22,7 @@ from vbv_lernwelt.learnpath.tests.learning_path_factories import (
LearningContentAttendanceCourseFactory, LearningContentAttendanceCourseFactory,
LearningContentDocumentListFactory, LearningContentDocumentListFactory,
LearningContentEdoniqTestFactory, LearningContentEdoniqTestFactory,
LearningContentFeedbackFactory, LearningContentFeedbackUKFactory,
LearningContentMediaLibraryFactory, LearningContentMediaLibraryFactory,
LearningContentPlaceholderFactory, LearningContentPlaceholderFactory,
LearningPathFactory, LearningPathFactory,
@ -260,7 +260,7 @@ damit du erfolgreich mit deinem Lernpfad (durch-)starten kannst.
title="Unterlagen für den Unterricht", title="Unterlagen für den Unterricht",
parent=circle, parent=circle,
) )
LearningContentFeedbackFactory( LearningContentFeedbackUKFactory(
parent=circle, parent=circle,
) )
LearningSequenceFactory(title="Transfer", parent=circle, icon="it-icon-ls-end") LearningSequenceFactory(title="Transfer", parent=circle, icon="it-icon-ls-end")
@ -370,7 +370,7 @@ In diesem Circle erfährst du wie die überbetrieblichen Kurse aufgebaut sind. Z
# test_url="https://exam.vbv-afa.ch/e-tutor/v4/user/course/pre_course_object?aid=1689096897473,2147466097", # test_url="https://exam.vbv-afa.ch/e-tutor/v4/user/course/pre_course_object?aid=1689096897473,2147466097",
# ) # )
LearningUnitFactory(title="Feedback", title_hidden=True, parent=circle) LearningUnitFactory(title="Feedback", title_hidden=True, parent=circle)
LearningContentFeedbackFactory( LearningContentFeedbackUKFactory(
parent=circle, parent=circle,
) )
LearningSequenceFactory(title="Transfer", parent=circle, icon="it-icon-ls-end") LearningSequenceFactory(title="Transfer", parent=circle, icon="it-icon-ls-end")
@ -485,7 +485,7 @@ Dans ce cercle, tu apprendras comment les cours interentreprises sont structuré
test_url="https://exam.vbv-afa.ch/e-tutor/v4/user/course/pre_course_object?aid=1689096897473,2147466097", test_url="https://exam.vbv-afa.ch/e-tutor/v4/user/course/pre_course_object?aid=1689096897473,2147466097",
) )
LearningUnitFactory(title="Feedback", title_hidden=True, parent=circle) LearningUnitFactory(title="Feedback", title_hidden=True, parent=circle)
LearningContentFeedbackFactory( LearningContentFeedbackUKFactory(
parent=circle, parent=circle,
) )
LearningSequenceFactory(title="Transfert", parent=circle, icon="it-icon-ls-end") LearningSequenceFactory(title="Transfert", parent=circle, icon="it-icon-ls-end")
@ -600,7 +600,7 @@ In questo Circle imparerai come sono strutturati i corsi interaziendali. Imparer
test_url="https://exam.vbv-afa.ch/e-tutor/v4/user/course/pre_course_object?aid=1689096897473,2147466097", test_url="https://exam.vbv-afa.ch/e-tutor/v4/user/course/pre_course_object?aid=1689096897473,2147466097",
) )
LearningUnitFactory(title="Feedback", title_hidden=True, parent=circle) LearningUnitFactory(title="Feedback", title_hidden=True, parent=circle)
LearningContentFeedbackFactory( LearningContentFeedbackUKFactory(
parent=circle, parent=circle,
) )
LearningSequenceFactory(title="Trasferimento", parent=circle, icon="it-icon-ls-end") LearningSequenceFactory(title="Trasferimento", parent=circle, icon="it-icon-ls-end")
@ -705,7 +705,7 @@ In diesem Circle lernst du die wichtigsten Grundlagen bezüglich Versicherungswi
], ],
) )
LearningUnitFactory(title="Feedback", title_hidden=True, parent=circle) LearningUnitFactory(title="Feedback", title_hidden=True, parent=circle)
LearningContentFeedbackFactory( LearningContentFeedbackUKFactory(
parent=circle, parent=circle,
) )
LearningSequenceFactory(title="Transfer", parent=circle, icon="it-icon-ls-end") LearningSequenceFactory(title="Transfer", parent=circle, icon="it-icon-ls-end")
@ -815,7 +815,7 @@ Dans ce cercle, tu apprends les bases les plus importantes en matière d'assuran
], ],
) )
LearningUnitFactory(title="Feedback", title_hidden=True, parent=circle) LearningUnitFactory(title="Feedback", title_hidden=True, parent=circle)
LearningContentFeedbackFactory( LearningContentFeedbackUKFactory(
parent=circle, parent=circle,
) )
LearningSequenceFactory(title="Transfert", parent=circle, icon="it-icon-ls-end") LearningSequenceFactory(title="Transfert", parent=circle, icon="it-icon-ls-end")
@ -924,7 +924,7 @@ In questo Circle imparerai le basi più importanti del settore assicurativo e de
], ],
) )
LearningUnitFactory(title="Feedback", title_hidden=True, parent=circle) LearningUnitFactory(title="Feedback", title_hidden=True, parent=circle)
LearningContentFeedbackFactory( LearningContentFeedbackUKFactory(
parent=circle, parent=circle,
) )
LearningSequenceFactory(title="Trasferimento", parent=circle, icon="it-icon-ls-end") LearningSequenceFactory(title="Trasferimento", parent=circle, icon="it-icon-ls-end")
@ -1064,7 +1064,7 @@ def create_uk_circle_fahrzeug(lp, title="Fahrzeug"):
], ],
) )
LearningUnitFactory(title="Feedback", title_hidden=True, parent=circle) LearningUnitFactory(title="Feedback", title_hidden=True, parent=circle)
LearningContentFeedbackFactory( LearningContentFeedbackUKFactory(
parent=circle, parent=circle,
) )
@ -1198,7 +1198,7 @@ def create_uk_fr_circle_fahrzeug(lp, title="Véhicule"):
], ],
) )
LearningUnitFactory(title="Feedback", title_hidden=True, parent=circle) LearningUnitFactory(title="Feedback", title_hidden=True, parent=circle)
LearningContentFeedbackFactory( LearningContentFeedbackUKFactory(
parent=circle, parent=circle,
) )
@ -1336,7 +1336,7 @@ def create_uk_it_circle_fahrzeug(lp, title="Veicolo"):
], ],
) )
LearningUnitFactory(title="Feedback", title_hidden=True, parent=circle) LearningUnitFactory(title="Feedback", title_hidden=True, parent=circle)
LearningContentFeedbackFactory( LearningContentFeedbackUKFactory(
parent=circle, parent=circle,
) )

View File

@ -10,7 +10,8 @@ from vbv_lernwelt.learnpath.graphql.types import (
LearningContentAttendanceCourseObjectType, LearningContentAttendanceCourseObjectType,
LearningContentDocumentListObjectType, LearningContentDocumentListObjectType,
LearningContentEdoniqTestObjectType, LearningContentEdoniqTestObjectType,
LearningContentFeedbackObjectType, LearningContentFeedbackUKObjectType,
LearningContentFeedbackVVObjectType,
LearningContentKnowledgeAssessmentObjectType, LearningContentKnowledgeAssessmentObjectType,
LearningContentLearningModuleObjectType, LearningContentLearningModuleObjectType,
LearningContentMediaLibraryObjectType, LearningContentMediaLibraryObjectType,
@ -50,7 +51,8 @@ class CourseQuery(graphene.ObjectType):
learning_content_attendance_course = graphene.Field( learning_content_attendance_course = graphene.Field(
LearningContentAttendanceCourseObjectType LearningContentAttendanceCourseObjectType
) )
learning_content_feedback = graphene.Field(LearningContentFeedbackObjectType) learning_content_feedback_uk = graphene.Field(LearningContentFeedbackUKObjectType)
learning_content_feedback_vv = graphene.Field(LearningContentFeedbackVVObjectType)
learning_content_learning_module = graphene.Field( learning_content_learning_module = graphene.Field(
LearningContentLearningModuleObjectType LearningContentLearningModuleObjectType
) )

View File

@ -34,6 +34,7 @@ class FeedbackResponseFactory(DjangoModelFactory):
"Das Beispiel mit der Katze fand ich sehr gut veranschaulicht!", "Das Beispiel mit der Katze fand ich sehr gut veranschaulicht!",
] ]
), ),
"feedback_type": FuzzyChoice(["uk", "vv"]),
} }
) )

View File

@ -7,10 +7,16 @@ from vbv_lernwelt.course.models import CourseSession
from vbv_lernwelt.feedback.graphql.types import ( from vbv_lernwelt.feedback.graphql.types import (
FeedbackResponseObjectType as FeedbackResponseType, FeedbackResponseObjectType as FeedbackResponseType,
) )
from vbv_lernwelt.feedback.serializers import CourseFeedbackSerializer from vbv_lernwelt.feedback.serializers import (
CourseFeedbackSerializerUK,
CourseFeedbackSerializerVV,
)
from vbv_lernwelt.feedback.services import update_feedback_response from vbv_lernwelt.feedback.services import update_feedback_response
from vbv_lernwelt.iam.permissions import has_course_session_access from vbv_lernwelt.iam.permissions import has_course_session_access
from vbv_lernwelt.learnpath.models import LearningContentFeedback from vbv_lernwelt.learnpath.models import (
LearningContentFeedbackUK,
LearningContentFeedbackVV,
)
logger = structlog.get_logger(__name__) logger = structlog.get_logger(__name__)
@ -25,6 +31,7 @@ class SendFeedbackMutation(graphene.Mutation):
class Arguments: class Arguments:
course_session_id = graphene.ID(required=True) course_session_id = graphene.ID(required=True)
learning_content_page_id = graphene.ID(required=True) learning_content_page_id = graphene.ID(required=True)
learning_content_type = graphene.String(required=True)
data = GenericScalar() data = GenericScalar()
submitted = graphene.Boolean(required=False, default_value=False) submitted = graphene.Boolean(required=False, default_value=False)
@ -35,11 +42,29 @@ class SendFeedbackMutation(graphene.Mutation):
info, info,
course_session_id, course_session_id,
learning_content_page_id, learning_content_page_id,
learning_content_type,
data, data,
submitted, submitted,
): ):
feedback_user_id = info.context.user.id feedback_user_id = info.context.user.id
learning_content = LearningContentFeedback.objects.get(
if learning_content_type == "learnpath.LearningContentFeedbackVV":
learningContentFeedbackModel = LearningContentFeedbackVV
serializerClass = CourseFeedbackSerializerVV
data["feedback_type"] = "vv"
elif learning_content_type == "learnpath.LearningContentFeedbackUK":
learningContentFeedbackModel = LearningContentFeedbackUK
serializerClass = CourseFeedbackSerializerUK
data["feedback_type"] = "uk"
else:
errors = [
ErrorType(
field="learningContentType", messages="Invalid learningContentType"
)
]
return SendFeedbackMutation(errors=errors)
learning_content = learningContentFeedbackModel.objects.get(
id=learning_content_page_id id=learning_content_page_id
) )
circle = learning_content.get_circle() circle = learning_content.get_circle()
@ -65,7 +90,7 @@ class SendFeedbackMutation(graphene.Mutation):
course_session_id=course_session_id, course_session_id=course_session_id,
) )
serializer = CourseFeedbackSerializer(data=data) serializer = serializerClass(data=data)
if not serializer.is_valid(): if not serializer.is_valid():
logger.error( logger.error(

View File

@ -0,0 +1,22 @@
# Generated by Django 3.2.20 on 2023-12-07 14:01
from django.db import migrations
def add_field_to_json(apps, _schema_editor):
FeedbackResponse = apps.get_model("feedback", "FeedbackResponse")
for instance in FeedbackResponse.objects.all():
if instance.data is None:
instance.data = {}
instance.data["feedback_type"] = "uk" # Set the default value
instance.save()
class Migration(migrations.Migration):
dependencies = [
("feedback", "0006_auto_20230922_1131"),
]
operations = [
migrations.RunPython(add_field_to_json),
]

View File

@ -5,6 +5,11 @@ from vbv_lernwelt.feedback.models import FeedbackResponse
logger = structlog.get_logger(__name__) logger = structlog.get_logger(__name__)
FEEDBACK_TYPES = (
("uk", "Feedback UK"),
("vv", "Feedback VV"),
)
class FeedbackIntegerField(serializers.IntegerField): class FeedbackIntegerField(serializers.IntegerField):
def __init__(self, **kwargs): def __init__(self, **kwargs):
@ -13,7 +18,8 @@ class FeedbackIntegerField(serializers.IntegerField):
) )
class CourseFeedbackSerializer(serializers.Serializer): class CourseFeedbackSerializerUK(serializers.Serializer):
feedback_type = serializers.ChoiceField(choices=FEEDBACK_TYPES)
satisfaction = FeedbackIntegerField() satisfaction = FeedbackIntegerField()
goal_attainment = FeedbackIntegerField() goal_attainment = FeedbackIntegerField()
proficiency = serializers.IntegerField(required=False, allow_null=True) proficiency = serializers.IntegerField(required=False, allow_null=True)
@ -33,6 +39,22 @@ class CourseFeedbackSerializer(serializers.Serializer):
) )
class CourseFeedbackSerializerVV(serializers.Serializer):
feedback_type = serializers.ChoiceField(choices=FEEDBACK_TYPES)
satisfaction = FeedbackIntegerField()
goal_attainment = FeedbackIntegerField()
proficiency = serializers.IntegerField(required=False, allow_null=True)
preparation_task_clarity = serializers.BooleanField(required=False, allow_null=True)
materials_rating = FeedbackIntegerField()
would_recommend = serializers.BooleanField(required=False, allow_null=True)
course_positive_feedback = serializers.CharField(
required=False, allow_null=True, allow_blank=True
)
course_negative_feedback = serializers.CharField(
required=False, allow_null=True, allow_blank=True
)
class CypressFeedbackResponseSerializer(serializers.ModelSerializer): class CypressFeedbackResponseSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = FeedbackResponse model = FeedbackResponse

View File

@ -1,10 +1,15 @@
from typing import Union
import structlog import structlog
from vbv_lernwelt.core.models import User from vbv_lernwelt.core.models import User
from vbv_lernwelt.course.models import CourseCompletionStatus, CourseSession from vbv_lernwelt.course.models import CourseCompletionStatus, CourseSession
from vbv_lernwelt.course.services import mark_course_completion from vbv_lernwelt.course.services import mark_course_completion
from vbv_lernwelt.feedback.models import FeedbackResponse from vbv_lernwelt.feedback.models import FeedbackResponse
from vbv_lernwelt.learnpath.models import LearningContentFeedback from vbv_lernwelt.learnpath.models import (
LearningContentFeedbackUK,
LearningContentFeedbackVV,
)
logger = structlog.get_logger(__name__) logger = structlog.get_logger(__name__)
@ -12,7 +17,9 @@ logger = structlog.get_logger(__name__)
def update_feedback_response( def update_feedback_response(
feedback_user: User, feedback_user: User,
course_session: CourseSession, course_session: CourseSession,
learning_content_feedback_page: LearningContentFeedback, learning_content_feedback_page: Union[
LearningContentFeedbackUK, LearningContentFeedbackVV
],
submitted: bool, submitted: bool,
validated_data: dict, validated_data: dict,
): ):
@ -26,18 +33,7 @@ def update_feedback_response(
original_data = feedback_response.data original_data = feedback_response.data
updated_data = validated_data updated_data = validated_data
initial_data = { initial_data = initial_data_for_feedback_page(learning_content_feedback_page)
"satisfaction": None,
"goal_attainment": None,
"proficiency": None,
"preparation_task_clarity": None,
"instructor_competence": None,
"instructor_respect": None,
"instructor_open_feedback": "",
"would_recommend": None,
"course_negative_feedback": "",
"course_positive_feedback": "",
}
merged_data = initial_data | { merged_data = initial_data | {
key: updated_data[key] key: updated_data[key]
@ -71,3 +67,36 @@ def update_feedback_response(
) )
return feedback_response return feedback_response
def initial_data_for_feedback_page(
learning_content_feedback_page: Union[
LearningContentFeedbackUK, LearningContentFeedbackVV
]
):
if hasattr(learning_content_feedback_page, "learningcontentfeedbackuk"):
return {
"satisfaction": None,
"goal_attainment": None,
"proficiency": None,
"preparation_task_clarity": None,
"instructor_competence": None,
"instructor_respect": None,
"instructor_open_feedback": "",
"would_recommend": None,
"course_negative_feedback": "",
"course_positive_feedback": "",
"feedback_type": "uk",
}
if hasattr(learning_content_feedback_page, "learningcontentfeedbackvv"):
return {
"satisfaction": None,
"goal_attainment": None,
"proficiency": None,
"preparation_task_clarity": None,
"would_recommend": None,
"course_negative_feedback": "",
"course_positive_feedback": "",
"feedback_type": "vv",
}
return {}

View File

@ -114,6 +114,7 @@ class FeedbackRestApiTestCase(FeedbackBaseTestCase):
"course_negative_feedback": self.feedback_data[ "course_negative_feedback": self.feedback_data[
"course_negative_feedback" "course_negative_feedback"
][i], ][i],
"feedback_type": "uk",
}, },
feedback_user=self.feedback_users[i], feedback_user=self.feedback_users[i],
submitted=True, submitted=True,
@ -129,6 +130,7 @@ class FeedbackRestApiTestCase(FeedbackBaseTestCase):
expected = { expected = {
"amount": 3, "amount": 3,
"questions": self.feedback_data, "questions": self.feedback_data,
"feedbackType": "uk",
} }
print(response.data) print(response.data)

View File

@ -0,0 +1,91 @@
import json
from graphene_django.utils.testing import GraphQLTestCase
from vbv_lernwelt.core.create_default_users import create_default_users
from vbv_lernwelt.core.models import User
from vbv_lernwelt.course.consts import COURSE_TEST_ID
from vbv_lernwelt.course.creators.test_course import create_test_course
from vbv_lernwelt.course.models import CourseSession
from vbv_lernwelt.feedback.models import FeedbackResponse
from vbv_lernwelt.learnpath.models import LearningContentFeedbackUK
class FeedbackMutationTestCase(GraphQLTestCase):
GRAPHQL_URL = "/server/graphql/"
def setUp(self):
create_default_users()
create_test_course(include_vv=False, with_sessions=True)
self.course_session = CourseSession.objects.get(title="Test Bern 2022 a")
self.learning_content_feedback_page = LearningContentFeedbackUK.objects.get(
slug="test-lehrgang-lp-circle-fahrzeug-lc-feedback"
)
self.student = User.objects.get(username="test-student1@example.com")
self.client.force_login(self.student)
def test_creates_response(self):
data = {
"course_negative_feedback": "schlecht",
"course_positive_feedback": "gut",
"feedback_type": "uk",
"goal_attainment": 3,
"preparation_task_clarity": False,
"proficiency": 100,
"satisfaction": 3,
"would_recommend": False,
"instructor_competence": None,
"instructor_respect": None,
"instructor_open_feedback": None,
}
response = self.query(
f"""
mutation {{
send_feedback(
course_session_id: "{COURSE_TEST_ID}"
learning_content_page_id: "{self.learning_content_feedback_page.id}"
learning_content_type: "learnpath.LearningContentFeedbackUK"
data: {{
course_negative_feedback: "{data['course_negative_feedback']}",
course_positive_feedback: "{data['course_positive_feedback']}",
feedback_type: null,
goal_attainment: {data['goal_attainment']},
preparation_task_clarity: {str(data['preparation_task_clarity']).lower()},
proficiency: {data['proficiency']},
satisfaction: {data['satisfaction']},
would_recommend: {str(data['would_recommend']).lower()},
instructor_competence: null,
instructor_respect: null,
instructor_open_feedback: null,
}},
submitted: false
) {{
feedback_response {{
id
data
submitted
__typename
}}
errors {{
field
messages
__typename
}}
__typename
}}
}}
"""
)
content = json.loads(response.content)
self.assertResponseNoErrors(response)
self.assertDictEqual(
content["data"]["send_feedback"]["feedback_response"]["data"], data
)
feedback = FeedbackResponse.objects.first()
self.assertEqual(feedback.data, data)
self.assertEqual(feedback.submitted, False)
self.assertEqual(feedback.feedback_user, self.student)

View File

@ -61,12 +61,13 @@ def get_feedback_for_circle(request, course_session_id, circle_id):
feedback_user__in=feedback_users(course_session_id), feedback_user__in=feedback_users(course_session_id),
).order_by("created_at") ).order_by("created_at")
# I guess this is ok for the üK case feedback_data = {"amount": len(feedbacks), "questions": {}, "feedbackType": None}
feedback_data = {"amount": len(feedbacks), "questions": {}}
if feedback_data["amount"] == 0: if feedback_data["amount"] == 0:
return Response(status=200, data=feedback_data) return Response(status=200, data=feedback_data)
feedback_data["feedbackType"] = feedbacks[0].data.get("feedback_type", None)
for field in FEEDBACK_FIELDS: for field in FEEDBACK_FIELDS:
feedback_data["questions"][field] = [] feedback_data["questions"][field] = []

View File

@ -13,7 +13,7 @@ from vbv_lernwelt.course.models import CourseCategory, CoursePage
from vbv_lernwelt.learnpath.tests.learning_path_factories import ( from vbv_lernwelt.learnpath.tests.learning_path_factories import (
CircleFactory, CircleFactory,
LearningContentAssignmentFactory, LearningContentAssignmentFactory,
LearningContentFeedbackFactory, LearningContentFeedbackVVFactory,
LearningContentLearningModuleFactory, LearningContentLearningModuleFactory,
LearningContentMediaLibraryFactory, LearningContentMediaLibraryFactory,
LearningContentPlaceholderFactory, LearningContentPlaceholderFactory,
@ -201,7 +201,7 @@ def create_circle_basis(lp, title="Basis"):
slug__startswith=f"versicherungsvermittler-in-assignment-reflexion" slug__startswith=f"versicherungsvermittler-in-assignment-reflexion"
), ),
), ),
LearningContentFeedbackFactory( LearningContentFeedbackVVFactory(
parent=circle, parent=circle,
) )
@ -278,7 +278,7 @@ def create_circle_gewinnen(lp, title="Gewinnen"):
slug__startswith=f"{course_slug}-assignment-reflexion" slug__startswith=f"{course_slug}-assignment-reflexion"
), ),
), ),
LearningContentFeedbackFactory( LearningContentFeedbackVVFactory(
parent=circle, parent=circle,
) )
@ -368,7 +368,7 @@ def create_circle_fahrzeug(lp, title="Fahrzeug"):
# slug__startswith=f"{circle.get_course().slug}-assignment-reflexion" # slug__startswith=f"{circle.get_course().slug}-assignment-reflexion"
# ), # ),
# ), # ),
LearningContentFeedbackFactory( LearningContentFeedbackVVFactory(
parent=circle, parent=circle,
) )
@ -554,7 +554,7 @@ def create_circle_reisen(lp, title="Reisen"):
slug__startswith=f"{circle.get_course().slug}-assignment-reflexion" slug__startswith=f"{circle.get_course().slug}-assignment-reflexion"
), ),
), ),
LearningContentFeedbackFactory( LearningContentFeedbackVVFactory(
parent=circle, parent=circle,
) )
@ -647,7 +647,7 @@ def create_circle_einkommenssicherung(lp, title="Einkommenssicherung"):
slug__startswith=f"{circle.get_course().slug}-assignment-reflexion" slug__startswith=f"{circle.get_course().slug}-assignment-reflexion"
), ),
), ),
LearningContentFeedbackFactory( LearningContentFeedbackVVFactory(
parent=circle, parent=circle,
) )
@ -700,7 +700,7 @@ def create_circle_wohneigentum(lp, title="Wohneigentum"):
slug__startswith=f"{circle.get_course().slug}-assignment-reflexion" slug__startswith=f"{circle.get_course().slug}-assignment-reflexion"
), ),
), ),
LearningContentFeedbackFactory( LearningContentFeedbackVVFactory(
parent=circle, parent=circle,
) )
@ -782,7 +782,7 @@ def create_circle_pensionierung(lp, title="Pensionierung"):
slug__startswith=f"{circle.get_course().slug}-assignment-reflexion" slug__startswith=f"{circle.get_course().slug}-assignment-reflexion"
), ),
), ),
LearningContentFeedbackFactory( LearningContentFeedbackVVFactory(
parent=circle, parent=circle,
) )
@ -839,7 +839,7 @@ def create_circle_erben(lp, title="Erben/Vererben"):
slug__startswith=f"{circle.get_course().slug}-assignment-reflexion" slug__startswith=f"{circle.get_course().slug}-assignment-reflexion"
), ),
), ),
LearningContentFeedbackFactory( LearningContentFeedbackVVFactory(
parent=circle, parent=circle,
) )
@ -929,7 +929,7 @@ def create_circle_gesundheit(lp, title="Gesundheit"):
slug__startswith=f"{circle.get_course().slug}-assignment-reflexion" slug__startswith=f"{circle.get_course().slug}-assignment-reflexion"
), ),
), ),
LearningContentFeedbackFactory( LearningContentFeedbackVVFactory(
parent=circle, parent=circle,
) )
@ -1352,7 +1352,7 @@ def create_learning_sequence_transfer(parent, title, lc_praxis_title=None):
slug__startswith=f"versicherungsvermittler-in-assignment-reflexion" slug__startswith=f"versicherungsvermittler-in-assignment-reflexion"
), ),
), ),
LearningContentFeedbackFactory( LearningContentFeedbackVVFactory(
parent=parent, parent=parent,
) )

View File

@ -10,7 +10,8 @@ from vbv_lernwelt.learnpath.models import (
LearningContentAttendanceCourse, LearningContentAttendanceCourse,
LearningContentDocumentList, LearningContentDocumentList,
LearningContentEdoniqTest, LearningContentEdoniqTest,
LearningContentFeedback, LearningContentFeedbackUK,
LearningContentFeedbackVV,
LearningContentKnowledgeAssessment, LearningContentKnowledgeAssessment,
LearningContentLearningModule, LearningContentLearningModule,
LearningContentMediaLibrary, LearningContentMediaLibrary,
@ -49,8 +50,10 @@ class LearningContentInterface(CoursePageInterface):
return LearningContentAssignmentObjectType return LearningContentAssignmentObjectType
elif isinstance(instance, LearningContentAttendanceCourse): elif isinstance(instance, LearningContentAttendanceCourse):
return LearningContentAttendanceCourseObjectType return LearningContentAttendanceCourseObjectType
elif isinstance(instance, LearningContentFeedback): elif isinstance(instance, LearningContentFeedbackUK):
return LearningContentFeedbackObjectType return LearningContentFeedbackUKObjectType
elif isinstance(instance, LearningContentFeedbackVV):
return LearningContentFeedbackVVObjectType
elif isinstance(instance, LearningContentLearningModule): elif isinstance(instance, LearningContentLearningModule):
return LearningContentLearningModuleObjectType return LearningContentLearningModuleObjectType
elif isinstance(instance, LearningContentKnowledgeAssessment): elif isinstance(instance, LearningContentKnowledgeAssessment):
@ -105,9 +108,19 @@ class LearningContentPlaceholderObjectType(DjangoObjectType):
fields = [] fields = []
class LearningContentFeedbackObjectType(DjangoObjectType): class LearningContentFeedbackUKObjectType(DjangoObjectType):
class Meta: class Meta:
model = LearningContentFeedback model = LearningContentFeedbackUK
interfaces = (
CoursePageInterface,
LearningContentInterface,
)
fields = []
class LearningContentFeedbackVVObjectType(DjangoObjectType):
class Meta:
model = LearningContentFeedbackVV
interfaces = ( interfaces = (
CoursePageInterface, CoursePageInterface,
LearningContentInterface, LearningContentInterface,

View File

@ -0,0 +1,60 @@
# Generated by Django 3.2.20 on 2023-11-29 07:27
import django.db.models.deletion
import wagtail.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("wagtailcore", "0089_log_entry_data_json_null_to_object"),
("learnpath", "0011_learningcontentknowledgeassessment"),
]
operations = [
migrations.RenameModel("LearningContentFeedback", "LearningContentFeedbackUK"),
migrations.CreateModel(
name="LearningContentFeedbackVV",
fields=[
(
"page_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="wagtailcore.page",
),
),
("minutes", models.PositiveIntegerField(default=15)),
("description", wagtail.fields.RichTextField(blank=True)),
("content_url", models.TextField(blank=True)),
("has_course_completion_status", models.BooleanField(default=True)),
(
"can_user_self_toggle_course_completion",
models.BooleanField(default=False),
),
],
options={
"abstract": False,
},
bases=("wagtailcore.page",),
),
migrations.AlterField(
model_name="learningcontentassignment",
name="assignment_type",
field=models.CharField(
choices=[
("VOLUNTARY_CASEWORK", "VOLUNTARY_CASEWORK"),
("MANDATORY_CASEWORK", "MANDATORY_CASEWORK"),
("PREP_ASSIGNMENT", "PREP_ASSIGNMENT"),
("REFLECTION", "REFLECTION"),
("CONDITION_ACCEPTANCE", "CONDITION_ACCEPTANCE"),
("EDONIQ_TEST", "EDONIQ_TEST"),
],
default="MANDATORY_CASEWORK",
max_length=50,
),
),
]

View File

@ -72,7 +72,8 @@ class Circle(CourseBasePage):
"learnpath.LearningUnit", "learnpath.LearningUnit",
"learnpath.LearningContentAssignment", "learnpath.LearningContentAssignment",
"learnpath.LearningContentAttendanceCourse", "learnpath.LearningContentAttendanceCourse",
"learnpath.LearningContentFeedback", "learnpath.LearningContentFeedbackUK",
"learnpath.LearningContentFeedbackVV",
"learnpath.LearningContentLearningModule", "learnpath.LearningContentLearningModule",
"learnpath.LearningContentKnowledgeAssessment", "learnpath.LearningContentKnowledgeAssessment",
"learnpath.LearningContentMediaLibrary", "learnpath.LearningContentMediaLibrary",
@ -318,7 +319,13 @@ class LearningContentPlaceholder(LearningContent):
can_user_self_toggle_course_completion = models.BooleanField(default=True) can_user_self_toggle_course_completion = models.BooleanField(default=True)
class LearningContentFeedback(LearningContent): class LearningContentFeedbackUK(LearningContent):
parent_page_types = ["learnpath.Circle"]
subpage_types = []
can_user_self_toggle_course_completion = models.BooleanField(default=False)
class LearningContentFeedbackVV(LearningContent):
parent_page_types = ["learnpath.Circle"] parent_page_types = ["learnpath.Circle"]
subpage_types = [] subpage_types = []
can_user_self_toggle_course_completion = models.BooleanField(default=False) can_user_self_toggle_course_completion = models.BooleanField(default=False)

View File

@ -7,7 +7,8 @@ from vbv_lernwelt.learnpath.models import (
LearningContentAttendanceCourse, LearningContentAttendanceCourse,
LearningContentDocumentList, LearningContentDocumentList,
LearningContentEdoniqTest, LearningContentEdoniqTest,
LearningContentFeedback, LearningContentFeedbackUK,
LearningContentFeedbackVV,
LearningContentKnowledgeAssessment, LearningContentKnowledgeAssessment,
LearningContentLearningModule, LearningContentLearningModule,
LearningContentMediaLibrary, LearningContentMediaLibrary,
@ -120,14 +121,24 @@ class LearningContentPlaceholderFactory(wagtail_factories.PageFactory):
model = LearningContentPlaceholder model = LearningContentPlaceholder
class LearningContentFeedbackFactory(wagtail_factories.PageFactory): class LearningContentFeedbackVVFactory(wagtail_factories.PageFactory):
title = "Feedback" title = "FeedbackVV"
minutes = 0 minutes = 0
content_url = "" content_url = ""
description = RichText("") description = RichText("")
class Meta: class Meta:
model = LearningContentFeedback model = LearningContentFeedbackVV
class LearningContentFeedbackUKFactory(wagtail_factories.PageFactory):
title = "FeedbackUK"
minutes = 0
content_url = ""
description = RichText("")
class Meta:
model = LearningContentFeedbackUK
class LearningContentLearningModuleFactory(wagtail_factories.PageFactory): class LearningContentLearningModuleFactory(wagtail_factories.PageFactory):