Merged in feature/VBV-525-feedback-improvements (pull request #208)
Feature/VBV-525 feedback improvements Approved-by: Christian Cueni
This commit is contained in:
commit
05f111eada
|
|
@ -1,233 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
import ItRadioGroup from "@/components/ui/ItRadioGroup.vue";
|
||||
import ItTextarea from "@/components/ui/ItTextarea.vue";
|
||||
import { graphql } from "@/gql/";
|
||||
import type { SendFeedbackInput } from "@/gql/graphql";
|
||||
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 { useCircleStore } from "@/stores/circle";
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import type { LearningContentFeedback } from "@/types";
|
||||
import { useMutation } from "@urql/vue";
|
||||
import { useRouteQuery } from "@vueuse/router";
|
||||
import log from "loglevel";
|
||||
import { computed, onMounted, reactive, ref } from "vue";
|
||||
import { useTranslation } from "i18next-vue";
|
||||
|
||||
const props = defineProps<{ page: LearningContentFeedback }>();
|
||||
const courseSessionsStore = useCourseSessionsStore();
|
||||
const circleStore = useCircleStore();
|
||||
const { t } = useTranslation();
|
||||
|
||||
onMounted(async () => {
|
||||
log.debug("Feedback mounted");
|
||||
});
|
||||
|
||||
const stepNo = useRouteQuery("step", "0", { transform: Number, mode: "push" });
|
||||
|
||||
const title = computed(
|
||||
() => `«${circleStore.circle?.title}»: ${t("feedback.areYouSatisfied")}`
|
||||
);
|
||||
|
||||
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.courseNegativeFeedbackLabel"),
|
||||
t("feedback.coursePositiveFeedbackLabel"),
|
||||
t("general.submission"),
|
||||
];
|
||||
|
||||
const numSteps = stepLabels.length;
|
||||
|
||||
const sendFeedbackMutation = graphql(`
|
||||
mutation SendFeedbackMutation($input: SendFeedbackInput!) {
|
||||
send_feedback(input: $input) {
|
||||
feedback_response {
|
||||
id
|
||||
}
|
||||
errors {
|
||||
field
|
||||
messages
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
const { executeMutation } = useMutation(sendFeedbackMutation);
|
||||
|
||||
const satisfaction = ref(null);
|
||||
const goalAttainment = ref(null);
|
||||
const proficiency = ref(null);
|
||||
const preparationTaskClarity = ref(null);
|
||||
const instructorCompetence = ref(null);
|
||||
const instructorRespect = ref(null);
|
||||
const instructorOpenFeedback = ref("");
|
||||
const wouldRecommend = ref(null);
|
||||
const courseNegativeFeedback = ref("");
|
||||
const coursePositiveFeedback = ref("");
|
||||
|
||||
const mutationResult = ref<any>(null);
|
||||
|
||||
const previousStep = () => {
|
||||
if (stepNo.value > 0) {
|
||||
stepNo.value -= 1;
|
||||
}
|
||||
};
|
||||
const nextStep = () => {
|
||||
if (stepNo.value < numSteps) {
|
||||
stepNo.value += 1;
|
||||
}
|
||||
log.info(`next step ${stepNo.value} of ${numSteps}`);
|
||||
};
|
||||
|
||||
const sendFeedback = () => {
|
||||
log.info("sending feedback");
|
||||
const courseSession = courseSessionsStore.currentCourseSession;
|
||||
if (!courseSession || !courseSession.id) {
|
||||
log.error("no course session set");
|
||||
return;
|
||||
}
|
||||
const input: SendFeedbackInput = reactive({
|
||||
data: {
|
||||
preparation_task_clarity: preparationTaskClarity,
|
||||
course_negative_feedback: courseNegativeFeedback,
|
||||
course_positive_feedback: coursePositiveFeedback,
|
||||
goal_attainment: goalAttainment,
|
||||
instructor_competence: instructorCompetence,
|
||||
instructor_respect: instructorRespect,
|
||||
instructor_open_feedback: instructorOpenFeedback,
|
||||
satisfaction,
|
||||
proficiency,
|
||||
would_recommend: wouldRecommend,
|
||||
},
|
||||
page: props.page.id,
|
||||
course_session: courseSession.id,
|
||||
});
|
||||
const variables = reactive({
|
||||
input,
|
||||
});
|
||||
log.debug(variables);
|
||||
executeMutation(variables)
|
||||
.then(({ data }) => {
|
||||
log.debug(data);
|
||||
mutationResult.value = data;
|
||||
})
|
||||
.catch((e) => log.error(e));
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LearningContentMultiLayout
|
||||
:title="title"
|
||||
sub-title="Feedback"
|
||||
:learning-content="page"
|
||||
:show-start-button="stepNo === 0"
|
||||
:show-next-button="stepNo > 0 && stepNo + 1 < numSteps"
|
||||
:show-previous-button="stepNo > 0"
|
||||
:show-exit-button="stepNo + 1 === numSteps"
|
||||
:current-step="stepNo"
|
||||
:steps-count="numSteps"
|
||||
:start-badge-text="$t('general.introduction')"
|
||||
:end-badge-text="$t('general.submission')"
|
||||
:base-url="props.page.frontend_url"
|
||||
close-button-variant="close"
|
||||
@previous="previousStep()"
|
||||
@next="nextStep()"
|
||||
>
|
||||
<div>
|
||||
<p v-if="stepNo === 0" class="mt-10">
|
||||
{{
|
||||
$t("feedback.intro", {
|
||||
name: `${courseSessionsStore.circleExperts[0]?.first_name} ${courseSessionsStore.circleExperts[0]?.last_name}`,
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
<p v-if="stepNo > 0 && stepNo + 1 < numSteps" class="pb-2">
|
||||
{{ stepLabels[stepNo] }}
|
||||
</p>
|
||||
<ItRadioGroup
|
||||
v-if="stepNo === 1"
|
||||
v-model="satisfaction"
|
||||
class="mb-8"
|
||||
:items="RATINGS"
|
||||
/>
|
||||
<ItRadioGroup
|
||||
v-if="stepNo === 2"
|
||||
v-model="goalAttainment"
|
||||
class="mb-8"
|
||||
:items="RATINGS"
|
||||
/>
|
||||
<ItRadioGroup
|
||||
v-if="stepNo === 3"
|
||||
v-model="proficiency"
|
||||
class="mb-8"
|
||||
:items="PERCENTAGES"
|
||||
/>
|
||||
<ItRadioGroup
|
||||
v-if="stepNo === 4"
|
||||
v-model="preparationTaskClarity"
|
||||
class="mb-8"
|
||||
:items="YES_NO"
|
||||
/>
|
||||
<ItRadioGroup
|
||||
v-if="stepNo === 5"
|
||||
v-model="instructorCompetence"
|
||||
class="mb-8"
|
||||
:items="RATINGS"
|
||||
/>
|
||||
<ItRadioGroup
|
||||
v-if="stepNo === 6"
|
||||
v-model="instructorRespect"
|
||||
class="mb-8"
|
||||
:items="RATINGS"
|
||||
/>
|
||||
<ItTextarea v-if="stepNo === 7" v-model="instructorOpenFeedback" class="mb-8" />
|
||||
<ItRadioGroup
|
||||
v-if="stepNo === 8"
|
||||
v-model="wouldRecommend"
|
||||
class="mb-8"
|
||||
:items="YES_NO"
|
||||
/>
|
||||
<ItTextarea v-if="stepNo === 9" v-model="courseNegativeFeedback" class="mb-8" />
|
||||
<ItTextarea v-if="stepNo === 10" v-model="coursePositiveFeedback" class="mb-8" />
|
||||
<FeedbackCompletition
|
||||
v-if="stepNo === 11"
|
||||
:avatar-url="courseSessionsStore.circleExperts[0].avatar_url"
|
||||
:title="
|
||||
$t('feedback.completionTitle', {
|
||||
name: `${courseSessionsStore.circleExperts[0].first_name} ${courseSessionsStore.circleExperts[0].last_name}`,
|
||||
})
|
||||
"
|
||||
:description="$t('feedback.completionDescription')"
|
||||
:feedback-sent="mutationResult != null"
|
||||
@send-feedback="sendFeedback"
|
||||
/>
|
||||
</div>
|
||||
</LearningContentMultiLayout>
|
||||
<!--
|
||||
<pre>
|
||||
satisfaction {{ satisfaction }}
|
||||
goalAttainment {{ goalAttainment }}
|
||||
proficiency {{ proficiency }}
|
||||
receivedMaterials {{ receivedMaterials }}
|
||||
materialsRating {{ materialsRating }}
|
||||
instructorCompetence {{ instructorCompetence }}
|
||||
instructorRespect {{ instructorRespect }}
|
||||
instructorOpenFeedback {{ instructorOpenFeedback }}
|
||||
wouldRecommend {{ wouldRecommend }}
|
||||
coursePositiveFeedback {{ coursePositiveFeedback }}
|
||||
courseNegativeFeedback {{ courseNegativeFeedback }}
|
||||
mutationResult: {{ mutationResult }}
|
||||
</pre> -->
|
||||
</template>
|
||||
|
|
@ -26,7 +26,9 @@
|
|||
class="h-8 bg-sky-500"
|
||||
:style="{ width: `${percentage * 100 * 0.8}%` }"
|
||||
></div>
|
||||
<div class="text-sm">{{ (percentage * 100).toFixed(1) }}%</div>
|
||||
<div class="text-sm" :data-cy="`percentage-value-${label}`">
|
||||
{{ (percentage * 100).toFixed(1) }}%
|
||||
</div>
|
||||
</div>
|
||||
</QuestionSummary>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
as="template"
|
||||
class="flex-1"
|
||||
:value="item.value"
|
||||
:data-cy="`radio-${item.value}`"
|
||||
>
|
||||
<div
|
||||
class="flex-1 cursor-pointer py-10 text-center text-xl font-bold hover:border-gray-500 hover:bg-gray-200 ui-checked:bg-sky-500 ui-not-checked:border"
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
<template>
|
||||
<QuestionSummary :title="props.title" :text="props.text">
|
||||
<h5 class="mb-8 text-base">{{ answers.length }} {{ $t("feedback.answers") }}</h5>
|
||||
<h5 class="mb-8 text-base">
|
||||
{{ uniqueAnswers.length }} {{ $t("feedback.answers") }}
|
||||
</h5>
|
||||
<ol>
|
||||
<li v-for="answer of props.answers" :key="answer" class="mb-2 last:mb-0">
|
||||
<li v-for="answer of uniqueAnswers" :key="answer" class="mb-2 last:mb-0">
|
||||
<p>{{ answer }}</p>
|
||||
</li>
|
||||
</ol>
|
||||
|
|
@ -11,12 +13,17 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import QuestionSummary from "@/components/ui/QuestionSummary.vue";
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
answers: string[];
|
||||
title: string;
|
||||
text: string;
|
||||
}>();
|
||||
|
||||
const uniqueAnswers = computed(() => {
|
||||
return [...new Set(props.answers)];
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped></style>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
<span
|
||||
class="col-start-2 row-span-2 inline-flex h-9 w-11 items-center justify-center rounded text-xl font-bold"
|
||||
:style="ratingValueStyle"
|
||||
data-cy="rating-scale-average"
|
||||
>
|
||||
{{ rating.toFixed(1) }}
|
||||
</span>
|
||||
|
|
@ -75,6 +76,7 @@ import QuestionSummary from "@/components/ui/QuestionSummary.vue";
|
|||
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/vue";
|
||||
import { computed } from "vue";
|
||||
import { useTranslation } from "i18next-vue";
|
||||
import log from "loglevel";
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
|
@ -97,6 +99,8 @@ const props = defineProps<{
|
|||
text: string;
|
||||
}>();
|
||||
|
||||
log.debug("RatingScale created", props);
|
||||
|
||||
const rating = computed((): number => {
|
||||
const sum = props.ratings.reduce((a, b) => a + b, 0);
|
||||
return sum / props.ratings.length;
|
||||
|
|
@ -182,8 +186,6 @@ const gradientStyle = {
|
|||
const ratingValueStyle = {
|
||||
backgroundColor,
|
||||
};
|
||||
|
||||
console.log(props);
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
|
|
|
|||
|
|
@ -28,14 +28,14 @@
|
|||
:style="greenStyle"
|
||||
></div>
|
||||
<div class="self-center justify-self-center font-bold grid-in-left-label">
|
||||
<Popover class="relative">
|
||||
<Popover class="relative" data-cy="popover-no">
|
||||
<PopoverButton class="focus:outline-none">
|
||||
{{ $t("general.no") }}
|
||||
</PopoverButton>
|
||||
<PopoverPanel
|
||||
class="absolute top-[-200%] z-10 w-[120px] border border-gray-500 bg-white p-1 text-left text-sm font-normal"
|
||||
>
|
||||
<p>
|
||||
<p data-cy="num-no">
|
||||
{{
|
||||
`"${$t("general.no")}" ${numberOfRatings["no"]} ${$t(
|
||||
"feedback.answers"
|
||||
|
|
@ -46,14 +46,14 @@
|
|||
</Popover>
|
||||
</div>
|
||||
<div class="self-center justify-self-center font-bold grid-in-right-label">
|
||||
<Popover class="relative">
|
||||
<Popover class="relative" data-cy="popover-yes">
|
||||
<PopoverButton class="focus:outline-none">
|
||||
{{ $t("general.yes") }}
|
||||
</PopoverButton>
|
||||
<PopoverPanel
|
||||
class="absolute top-[-200%] z-10 w-[120px] border border-gray-500 bg-white p-1 text-left text-sm font-normal"
|
||||
>
|
||||
<p>
|
||||
<p data-cy="num-yes">
|
||||
{{
|
||||
`"${$t("general.yes")}" ${numberOfRatings["yes"]} ${$t(
|
||||
"feedback.answers"
|
||||
|
|
@ -71,6 +71,7 @@
|
|||
import QuestionSummary from "@/components/ui/QuestionSummary.vue";
|
||||
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/vue";
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
ratings: boolean[];
|
||||
title: string;
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-
|
|||
* Therefore it is highly recommended to use the babel or swc plugin for production.
|
||||
*/
|
||||
const documents = {
|
||||
"\n mutation SendFeedbackMutation($input: SendFeedbackInput!) {\n send_feedback(input: $input) {\n feedback_response {\n id\n }\n errors {\n field\n messages\n }\n }\n }\n": types.SendFeedbackMutationDocument,
|
||||
"\n mutation AttendanceCheckMutation(\n $attendanceCourseId: ID!\n $attendanceUserList: [AttendanceUserInputType]!\n ) {\n update_course_session_attendance_course_users(\n id: $attendanceCourseId\n attendance_user_list: $attendanceUserList\n ) {\n course_session_attendance_course {\n id\n attendance_user_list {\n user_id\n first_name\n last_name\n email\n status\n }\n }\n }\n }\n": types.AttendanceCheckMutationDocument,
|
||||
"\n mutation UpsertAssignmentCompletion(\n $assignmentId: ID!\n $courseSessionId: ID!\n $learningContentId: ID\n $assignmentUserId: UUID\n $completionStatus: AssignmentCompletionStatus!\n $completionDataString: String!\n $evaluationGrade: Float\n $evaluationPoints: Float\n $initializeCompletion: Boolean\n ) {\n upsert_assignment_completion(\n assignment_id: $assignmentId\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n assignment_user_id: $assignmentUserId\n completion_status: $completionStatus\n completion_data_string: $completionDataString\n evaluation_grade: $evaluationGrade\n evaluation_points: $evaluationPoints\n initialize_completion: $initializeCompletion\n ) {\n assignment_completion {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_grade\n evaluation_points\n completion_data\n }\n }\n }\n": types.UpsertAssignmentCompletionDocument,
|
||||
"\n fragment CoursePageFields on CoursePageInterface {\n title\n id\n slug\n content_type\n frontend_url\n }\n": types.CoursePageFieldsFragmentDoc,
|
||||
|
|
@ -21,6 +20,7 @@ const documents = {
|
|||
"\n query assignmentCompletionQuery(\n $assignmentId: ID!\n $courseSessionId: ID!\n $learningContentId: ID\n $assignmentUserId: UUID\n ) {\n assignment(id: $assignmentId) {\n assignment_type\n max_points\n content_type\n effort_required\n evaluation_description\n evaluation_document_url\n evaluation_tasks\n id\n intro_text\n performance_objectives\n slug\n tasks\n title\n translation_key\n competence_certificate {\n ...CoursePageFields\n }\n }\n assignment_completion(\n assignment_id: $assignmentId\n course_session_id: $courseSessionId\n assignment_user_id: $assignmentUserId\n learning_content_page_id: $learningContentId\n ) {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_user {\n id\n }\n assignment_user {\n id\n }\n evaluation_grade\n evaluation_points\n completion_data\n }\n }\n": types.AssignmentCompletionQueryDocument,
|
||||
"\n query courseQuery($courseId: Int!) {\n course(id: $courseId) {\n id\n slug\n title\n category_name\n learning_path {\n id\n }\n }\n }\n": types.CourseQueryDocument,
|
||||
"\n query competenceCertificateQuery($courseSlug: String!, $courseSessionId: ID!) {\n competence_certificate_list(course_slug: $courseSlug) {\n ...CoursePageFields\n competence_certificates {\n ...CoursePageFields\n assignments {\n ...CoursePageFields\n assignment_type\n max_points\n completion(course_session_id: $courseSessionId) {\n id\n completion_status\n submitted_at\n evaluation_points\n }\n learning_content {\n title\n id\n slug\n content_type\n frontend_url\n circle {\n ...CoursePageFields\n }\n }\n }\n }\n }\n }\n": types.CompetenceCertificateQueryDocument,
|
||||
"\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,
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -37,10 +37,6 @@ const documents = {
|
|||
*/
|
||||
export function graphql(source: string): unknown;
|
||||
|
||||
/**
|
||||
* 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($input: SendFeedbackInput!) {\n send_feedback(input: $input) {\n feedback_response {\n id\n }\n errors {\n field\n messages\n }\n }\n }\n"): (typeof documents)["\n mutation SendFeedbackMutation($input: SendFeedbackInput!) {\n send_feedback(input: $input) {\n feedback_response {\n id\n }\n errors {\n field\n messages\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
|
|
@ -69,6 +65,10 @@ export function graphql(source: "\n query courseQuery($courseId: Int!) {\n c
|
|||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n query competenceCertificateQuery($courseSlug: String!, $courseSessionId: ID!) {\n competence_certificate_list(course_slug: $courseSlug) {\n ...CoursePageFields\n competence_certificates {\n ...CoursePageFields\n assignments {\n ...CoursePageFields\n assignment_type\n max_points\n completion(course_session_id: $courseSessionId) {\n id\n completion_status\n submitted_at\n evaluation_points\n }\n learning_content {\n title\n id\n slug\n content_type\n frontend_url\n circle {\n ...CoursePageFields\n }\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query competenceCertificateQuery($courseSlug: String!, $courseSessionId: ID!) {\n competence_certificate_list(course_slug: $courseSlug) {\n ...CoursePageFields\n competence_certificates {\n ...CoursePageFields\n assignments {\n ...CoursePageFields\n assignment_type\n max_points\n completion(course_session_id: $courseSessionId) {\n id\n completion_status\n submitted_at\n evaluation_points\n }\n learning_content {\n title\n id\n slug\n content_type\n frontend_url\n circle {\n ...CoursePageFields\n }\n }\n }\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
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: string) {
|
||||
return (documents as any)[source] ?? {};
|
||||
|
|
|
|||
|
|
@ -270,13 +270,11 @@ export type ErrorType = {
|
|||
messages: Array<Scalars['String']['output']>;
|
||||
};
|
||||
|
||||
export type FeedbackResponse = Node & {
|
||||
__typename?: 'FeedbackResponse';
|
||||
circle: CircleObjectType;
|
||||
created_at: Scalars['DateTime']['output'];
|
||||
export type FeedbackResponseObjectType = {
|
||||
__typename?: 'FeedbackResponseObjectType';
|
||||
data?: Maybe<Scalars['GenericScalar']['output']>;
|
||||
/** The ID of the object */
|
||||
id: Scalars['ID']['output'];
|
||||
id: Scalars['UUID']['output'];
|
||||
submitted: Scalars['Boolean']['output'];
|
||||
};
|
||||
|
||||
export type LearningContentAssignmentObjectType = LearningContentInterface & {
|
||||
|
|
@ -537,14 +535,17 @@ export type LearnpathLearningContentAssignmentAssignmentTypeChoices =
|
|||
|
||||
export type Mutation = {
|
||||
__typename?: 'Mutation';
|
||||
send_feedback?: Maybe<SendFeedbackPayload>;
|
||||
send_feedback?: Maybe<SendFeedbackMutation>;
|
||||
update_course_session_attendance_course_users?: Maybe<AttendanceCourseUserMutation>;
|
||||
upsert_assignment_completion?: Maybe<AssignmentCompletionMutation>;
|
||||
};
|
||||
|
||||
|
||||
export type MutationSendFeedbackArgs = {
|
||||
input: SendFeedbackInput;
|
||||
course_session_id: Scalars['ID']['input'];
|
||||
data?: InputMaybe<Scalars['GenericScalar']['input']>;
|
||||
learning_content_page_id: Scalars['ID']['input'];
|
||||
submitted?: InputMaybe<Scalars['Boolean']['input']>;
|
||||
};
|
||||
|
||||
|
||||
|
|
@ -566,12 +567,6 @@ export type MutationUpsertAssignmentCompletionArgs = {
|
|||
learning_content_page_id?: InputMaybe<Scalars['ID']['input']>;
|
||||
};
|
||||
|
||||
/** An object with an ID */
|
||||
export type Node = {
|
||||
/** The ID of the object */
|
||||
id: Scalars['ID']['output'];
|
||||
};
|
||||
|
||||
export type Query = {
|
||||
__typename?: 'Query';
|
||||
assignment?: Maybe<AssignmentObjectType>;
|
||||
|
|
@ -647,19 +642,11 @@ export type QueryLearningPathArgs = {
|
|||
slug?: InputMaybe<Scalars['String']['input']>;
|
||||
};
|
||||
|
||||
export type SendFeedbackInput = {
|
||||
clientMutationId?: InputMaybe<Scalars['String']['input']>;
|
||||
course_session: Scalars['Int']['input'];
|
||||
data?: InputMaybe<Scalars['GenericScalar']['input']>;
|
||||
page: Scalars['Int']['input'];
|
||||
};
|
||||
|
||||
export type SendFeedbackPayload = {
|
||||
__typename?: 'SendFeedbackPayload';
|
||||
clientMutationId?: Maybe<Scalars['String']['output']>;
|
||||
export type SendFeedbackMutation = {
|
||||
__typename?: 'SendFeedbackMutation';
|
||||
/** May contain more than one error for same field. */
|
||||
errors?: Maybe<Array<Maybe<ErrorType>>>;
|
||||
feedback_response?: Maybe<FeedbackResponse>;
|
||||
feedback_response?: Maybe<FeedbackResponseObjectType>;
|
||||
};
|
||||
|
||||
export type TopicObjectType = CoursePageInterface & {
|
||||
|
|
@ -689,13 +676,6 @@ export type UserType = {
|
|||
username: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type SendFeedbackMutationMutationVariables = Exact<{
|
||||
input: SendFeedbackInput;
|
||||
}>;
|
||||
|
||||
|
||||
export type SendFeedbackMutationMutation = { __typename?: 'Mutation', send_feedback?: { __typename?: 'SendFeedbackPayload', feedback_response?: { __typename?: 'FeedbackResponse', id: string } | null, errors?: Array<{ __typename?: 'ErrorType', field: string, messages: Array<string> } | null> | null } | null };
|
||||
|
||||
export type AttendanceCheckMutationMutationVariables = Exact<{
|
||||
attendanceCourseId: Scalars['ID']['input'];
|
||||
attendanceUserList: Array<InputMaybe<AttendanceUserInputType>> | InputMaybe<AttendanceUserInputType>;
|
||||
|
|
@ -811,11 +791,21 @@ export type CompetenceCertificateQueryQuery = { __typename?: 'Query', competence
|
|||
& { ' $fragmentRefs'?: { 'CoursePageFieldsCompetenceCertificateListObjectTypeFragment': CoursePageFieldsCompetenceCertificateListObjectTypeFragment } }
|
||||
) | null };
|
||||
|
||||
export type SendFeedbackMutationMutationVariables = Exact<{
|
||||
courseSessionId: Scalars['ID']['input'];
|
||||
learningContentId: Scalars['ID']['input'];
|
||||
data: Scalars['GenericScalar']['input'];
|
||||
submitted?: InputMaybe<Scalars['Boolean']['input']>;
|
||||
}>;
|
||||
|
||||
|
||||
export type SendFeedbackMutationMutation = { __typename?: 'Mutation', send_feedback?: { __typename?: 'SendFeedbackMutation', feedback_response?: { __typename?: 'FeedbackResponseObjectType', id: any, data?: any | null, submitted: boolean } | null, errors?: Array<{ __typename?: 'ErrorType', field: string, messages: Array<string> } | null> | null } | null };
|
||||
|
||||
export const CoursePageFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CoursePageFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"CoursePageInterface"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"content_type"}},{"kind":"Field","name":{"kind":"Name","value":"frontend_url"}}]}}]} as unknown as DocumentNode<CoursePageFieldsFragment, unknown>;
|
||||
export const SendFeedbackMutationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SendFeedbackMutation"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SendFeedbackInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"send_feedback"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"feedback_response"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"errors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"field"}},{"kind":"Field","name":{"kind":"Name","value":"messages"}}]}}]}}]}}]} as unknown as DocumentNode<SendFeedbackMutationMutation, SendFeedbackMutationMutationVariables>;
|
||||
export const AttendanceCheckMutationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"AttendanceCheckMutation"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"attendanceCourseId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"attendanceUserList"}},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NamedType","name":{"kind":"Name","value":"AttendanceUserInputType"}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"update_course_session_attendance_course_users"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"attendanceCourseId"}}},{"kind":"Argument","name":{"kind":"Name","value":"attendance_user_list"},"value":{"kind":"Variable","name":{"kind":"Name","value":"attendanceUserList"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"course_session_attendance_course"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"attendance_user_list"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user_id"}},{"kind":"Field","name":{"kind":"Name","value":"first_name"}},{"kind":"Field","name":{"kind":"Name","value":"last_name"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"status"}}]}}]}}]}}]}}]} as unknown as DocumentNode<AttendanceCheckMutationMutation, AttendanceCheckMutationMutationVariables>;
|
||||
export const UpsertAssignmentCompletionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpsertAssignmentCompletion"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"assignmentId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"courseSessionId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"learningContentId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"assignmentUserId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"completionStatus"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"AssignmentCompletionStatus"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"completionDataString"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"evaluationGrade"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Float"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"evaluationPoints"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Float"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"initializeCompletion"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"upsert_assignment_completion"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"assignment_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"assignmentId"}}},{"kind":"Argument","name":{"kind":"Name","value":"course_session_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"courseSessionId"}}},{"kind":"Argument","name":{"kind":"Name","value":"learning_content_page_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"learningContentId"}}},{"kind":"Argument","name":{"kind":"Name","value":"assignment_user_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"assignmentUserId"}}},{"kind":"Argument","name":{"kind":"Name","value":"completion_status"},"value":{"kind":"Variable","name":{"kind":"Name","value":"completionStatus"}}},{"kind":"Argument","name":{"kind":"Name","value":"completion_data_string"},"value":{"kind":"Variable","name":{"kind":"Name","value":"completionDataString"}}},{"kind":"Argument","name":{"kind":"Name","value":"evaluation_grade"},"value":{"kind":"Variable","name":{"kind":"Name","value":"evaluationGrade"}}},{"kind":"Argument","name":{"kind":"Name","value":"evaluation_points"},"value":{"kind":"Variable","name":{"kind":"Name","value":"evaluationPoints"}}},{"kind":"Argument","name":{"kind":"Name","value":"initialize_completion"},"value":{"kind":"Variable","name":{"kind":"Name","value":"initializeCompletion"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"assignment_completion"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"completion_status"}},{"kind":"Field","name":{"kind":"Name","value":"submitted_at"}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_submitted_at"}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_grade"}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_points"}},{"kind":"Field","name":{"kind":"Name","value":"completion_data"}}]}}]}}]}}]} as unknown as DocumentNode<UpsertAssignmentCompletionMutation, UpsertAssignmentCompletionMutationVariables>;
|
||||
export const AttendanceCheckQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"attendanceCheckQuery"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"courseSessionId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"course_session_attendance_course"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"courseSessionId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"attendance_user_list"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user_id"}},{"kind":"Field","name":{"kind":"Name","value":"status"}}]}}]}}]}}]} as unknown as DocumentNode<AttendanceCheckQueryQuery, AttendanceCheckQueryQueryVariables>;
|
||||
export const AssignmentCompletionQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"assignmentCompletionQuery"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"assignmentId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"courseSessionId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"learningContentId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"assignmentUserId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"assignment"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"assignmentId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"assignment_type"}},{"kind":"Field","name":{"kind":"Name","value":"max_points"}},{"kind":"Field","name":{"kind":"Name","value":"content_type"}},{"kind":"Field","name":{"kind":"Name","value":"effort_required"}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_description"}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_document_url"}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_tasks"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"intro_text"}},{"kind":"Field","name":{"kind":"Name","value":"performance_objectives"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"tasks"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"translation_key"}},{"kind":"Field","name":{"kind":"Name","value":"competence_certificate"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CoursePageFields"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"assignment_completion"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"assignment_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"assignmentId"}}},{"kind":"Argument","name":{"kind":"Name","value":"course_session_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"courseSessionId"}}},{"kind":"Argument","name":{"kind":"Name","value":"assignment_user_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"assignmentUserId"}}},{"kind":"Argument","name":{"kind":"Name","value":"learning_content_page_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"learningContentId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"completion_status"}},{"kind":"Field","name":{"kind":"Name","value":"submitted_at"}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_submitted_at"}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"assignment_user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_grade"}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_points"}},{"kind":"Field","name":{"kind":"Name","value":"completion_data"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CoursePageFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"CoursePageInterface"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"content_type"}},{"kind":"Field","name":{"kind":"Name","value":"frontend_url"}}]}}]} as unknown as DocumentNode<AssignmentCompletionQueryQuery, AssignmentCompletionQueryQueryVariables>;
|
||||
export const CourseQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"courseQuery"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"courseId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"course"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"courseId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"category_name"}},{"kind":"Field","name":{"kind":"Name","value":"learning_path"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode<CourseQueryQuery, CourseQueryQueryVariables>;
|
||||
export const CompetenceCertificateQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"competenceCertificateQuery"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"courseSlug"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"courseSessionId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"competence_certificate_list"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"course_slug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"courseSlug"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CoursePageFields"}},{"kind":"Field","name":{"kind":"Name","value":"competence_certificates"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CoursePageFields"}},{"kind":"Field","name":{"kind":"Name","value":"assignments"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CoursePageFields"}},{"kind":"Field","name":{"kind":"Name","value":"assignment_type"}},{"kind":"Field","name":{"kind":"Name","value":"max_points"}},{"kind":"Field","name":{"kind":"Name","value":"completion"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"course_session_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"courseSessionId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"completion_status"}},{"kind":"Field","name":{"kind":"Name","value":"submitted_at"}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_points"}}]}},{"kind":"Field","name":{"kind":"Name","value":"learning_content"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"content_type"}},{"kind":"Field","name":{"kind":"Name","value":"frontend_url"}},{"kind":"Field","name":{"kind":"Name","value":"circle"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CoursePageFields"}}]}}]}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CoursePageFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"CoursePageInterface"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"content_type"}},{"kind":"Field","name":{"kind":"Name","value":"frontend_url"}}]}}]} as unknown as DocumentNode<CompetenceCertificateQueryQuery, CompetenceCertificateQueryQueryVariables>;
|
||||
export const SendFeedbackMutationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SendFeedbackMutation"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"courseSessionId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"learningContentId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"GenericScalar"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"submitted"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"send_feedback"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"course_session_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"courseSessionId"}}},{"kind":"Argument","name":{"kind":"Name","value":"learning_content_page_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"learningContentId"}}},{"kind":"Argument","name":{"kind":"Name","value":"data"},"value":{"kind":"Variable","name":{"kind":"Name","value":"data"}}},{"kind":"Argument","name":{"kind":"Name","value":"submitted"},"value":{"kind":"Variable","name":{"kind":"Name","value":"submitted"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"feedback_response"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"data"}},{"kind":"Field","name":{"kind":"Name","value":"submitted"}}]}},{"kind":"Field","name":{"kind":"Name","value":"errors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"field"}},{"kind":"Field","name":{"kind":"Name","value":"messages"}}]}}]}}]}}]} as unknown as DocumentNode<SendFeedbackMutationMutation, SendFeedbackMutationMutationVariables>;
|
||||
|
|
@ -546,31 +546,22 @@ type CompetenceCertificateListObjectType implements CoursePageInterface {
|
|||
}
|
||||
|
||||
type Mutation {
|
||||
send_feedback(input: SendFeedbackInput!): SendFeedbackPayload
|
||||
send_feedback(course_session_id: ID!, data: GenericScalar, learning_content_page_id: ID!, submitted: Boolean = false): SendFeedbackMutation
|
||||
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_grade: Float, evaluation_points: Float, initialize_completion: Boolean, learning_content_page_id: ID): AssignmentCompletionMutation
|
||||
}
|
||||
|
||||
type SendFeedbackPayload {
|
||||
feedback_response: FeedbackResponse
|
||||
type SendFeedbackMutation {
|
||||
feedback_response: FeedbackResponseObjectType
|
||||
|
||||
"""May contain more than one error for same field."""
|
||||
errors: [ErrorType]
|
||||
clientMutationId: String
|
||||
}
|
||||
|
||||
type FeedbackResponse implements Node {
|
||||
"""The ID of the object"""
|
||||
id: ID!
|
||||
type FeedbackResponseObjectType {
|
||||
id: UUID!
|
||||
data: GenericScalar
|
||||
created_at: DateTime!
|
||||
circle: CircleObjectType!
|
||||
}
|
||||
|
||||
"""An object with an ID"""
|
||||
interface Node {
|
||||
"""The ID of the object"""
|
||||
id: ID!
|
||||
submitted: Boolean!
|
||||
}
|
||||
|
||||
type ErrorType {
|
||||
|
|
@ -578,13 +569,6 @@ type ErrorType {
|
|||
messages: [String!]!
|
||||
}
|
||||
|
||||
input SendFeedbackInput {
|
||||
page: Int!
|
||||
course_session: Int!
|
||||
data: GenericScalar
|
||||
clientMutationId: String
|
||||
}
|
||||
|
||||
type AttendanceCourseUserMutation {
|
||||
course_session_attendance_course: CourseSessionAttendanceCourseType
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ export const CoursePageInterface = "CoursePageInterface";
|
|||
export const CourseSessionAttendanceCourseType = "CourseSessionAttendanceCourseType";
|
||||
export const DateTime = "DateTime";
|
||||
export const ErrorType = "ErrorType";
|
||||
export const FeedbackResponse = "FeedbackResponse";
|
||||
export const FeedbackResponseObjectType = "FeedbackResponseObjectType";
|
||||
export const Float = "Float";
|
||||
export const GenericScalar = "GenericScalar";
|
||||
export const ID = "ID";
|
||||
|
|
@ -41,10 +41,8 @@ export const LearningSequenceObjectType = "LearningSequenceObjectType";
|
|||
export const LearningUnitObjectType = "LearningUnitObjectType";
|
||||
export const LearnpathLearningContentAssignmentAssignmentTypeChoices = "LearnpathLearningContentAssignmentAssignmentTypeChoices";
|
||||
export const Mutation = "Mutation";
|
||||
export const Node = "Node";
|
||||
export const Query = "Query";
|
||||
export const SendFeedbackInput = "SendFeedbackInput";
|
||||
export const SendFeedbackPayload = "SendFeedbackPayload";
|
||||
export const SendFeedbackMutation = "SendFeedbackMutation";
|
||||
export const String = "String";
|
||||
export const TopicObjectType = "TopicObjectType";
|
||||
export const UUID = "UUID";
|
||||
|
|
|
|||
|
|
@ -10,14 +10,20 @@
|
|||
<span>{{ $t("general.back") }}</span>
|
||||
</router-link>
|
||||
</nav>
|
||||
<main>
|
||||
<main v-if="feedbackData">
|
||||
<h1 class="mb-2">{{ $t("feedback.feedbackPageTitle") }}</h1>
|
||||
<p class="mb-10">
|
||||
<span class="font-bold">{{ feedbackData.amount }}</span>
|
||||
<span class="font-bold" data-cy="feedback-data-amount">
|
||||
{{ feedbackData.amount }}
|
||||
</span>
|
||||
{{ $t("feedback.feedbackPageInfo") }}
|
||||
</p>
|
||||
<ol v-if="Object.keys(feedbackData).length > 0">
|
||||
<li v-for="(question, i) in orderedQuestions" :key="i">
|
||||
<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"
|
||||
|
|
@ -67,7 +73,7 @@ import VerticalBarChart from "@/components/ui/VerticalBarChart.vue";
|
|||
import { useCurrentCourseSession } from "@/composables";
|
||||
import { itGet } from "@/fetchHelpers";
|
||||
import * as log from "loglevel";
|
||||
import { onMounted, reactive } from "vue";
|
||||
import { onMounted, ref } from "vue";
|
||||
import { useTranslation } from "i18next-vue";
|
||||
|
||||
interface FeedbackData {
|
||||
|
|
@ -144,14 +150,13 @@ const openKeys = [
|
|||
"instructor_open_feedback",
|
||||
];
|
||||
|
||||
const feedbackData = reactive<FeedbackData>({ amount: 0, questions: {} });
|
||||
const feedbackData = ref<FeedbackData | undefined>(undefined);
|
||||
|
||||
onMounted(async () => {
|
||||
log.debug("FeedbackPage mounted");
|
||||
const data = await itGet(
|
||||
feedbackData.value = await itGet(
|
||||
`/api/core/feedback/${courseSession.value.id}/${props.circleId}`
|
||||
);
|
||||
Object.assign(feedbackData, data);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -163,7 +163,11 @@ const getIconName = (lc: LearningContent) => {
|
|||
<button class="btn-primary">
|
||||
<router-link
|
||||
:to="submittable.detailsLink"
|
||||
:data-cy="`show-details-btn-${submittable.content.slug}`"
|
||||
:data-cy="
|
||||
isFeedback(submittable.content)
|
||||
? `show-feedback-btn-${submittable.content.slug}`
|
||||
: `show-details-btn-${submittable.content.slug}`
|
||||
"
|
||||
>
|
||||
{{ submittable.showDetailsText }}
|
||||
</router-link>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import type { Component } from "vue";
|
|||
import { computed, onUnmounted } from "vue";
|
||||
import AssignmentBlock from "./blocks/AssignmentBlock.vue";
|
||||
import AttendanceCourseBlock from "./blocks/AttendanceCourseBlock.vue";
|
||||
import FeedbackBlock from "./blocks/FeedbackBlock.vue";
|
||||
import FeedbackBlock from "./feedback/FeedbackBlock.vue";
|
||||
import IframeBlock from "./blocks/IframeBlock.vue";
|
||||
import MediaLibraryBlock from "./blocks/MediaLibraryBlock.vue";
|
||||
import PlaceholderBlock from "./blocks/PlaceholderBlock.vue";
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
<template>
|
||||
<FeedbackForm :page="content" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import FeedbackForm from "@/components/FeedbackForm.vue";
|
||||
import type { LearningContentFeedback } from "@/types";
|
||||
|
||||
defineProps<{
|
||||
content: LearningContentFeedback;
|
||||
}>();
|
||||
</script>
|
||||
|
|
@ -0,0 +1,280 @@
|
|||
<script setup lang="ts">
|
||||
import ItRadioGroup from "@/components/ui/ItRadioGroup.vue";
|
||||
import ItTextarea from "@/components/ui/ItTextarea.vue";
|
||||
import { graphql } from "@/gql";
|
||||
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 { useCircleStore } from "@/stores/circle";
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import type { LearningContentFeedback } from "@/types";
|
||||
import { useMutation } from "@urql/vue";
|
||||
import { useRouteQuery } from "@vueuse/router";
|
||||
import log from "loglevel";
|
||||
import { computed, onMounted, reactive, ref } from "vue";
|
||||
import { useTranslation } from "i18next-vue";
|
||||
import { useCurrentCourseSession } from "@/composables";
|
||||
|
||||
const props = defineProps<{
|
||||
content: LearningContentFeedback;
|
||||
}>();
|
||||
const courseSessionsStore = useCourseSessionsStore();
|
||||
const courseSession = useCurrentCourseSession();
|
||||
const circleStore = useCircleStore();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const stepNo = useRouteQuery("step", "0", { transform: Number, mode: "push" });
|
||||
|
||||
const title = computed(
|
||||
() => `«${circleStore.circle?.title}»: ${t("feedback.areYouSatisfied")}`
|
||||
);
|
||||
|
||||
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.courseNegativeFeedbackLabel"),
|
||||
t("feedback.coursePositiveFeedbackLabel"),
|
||||
t("general.submission"),
|
||||
];
|
||||
|
||||
const numSteps = stepLabels.length;
|
||||
|
||||
// noinspection GraphQLUnresolvedReference -> mute IntelliJ warning
|
||||
const sendFeedbackMutation = graphql(`
|
||||
mutation SendFeedbackMutation(
|
||||
$courseSessionId: ID!
|
||||
$learningContentId: ID!
|
||||
$data: GenericScalar!
|
||||
$submitted: Boolean
|
||||
) {
|
||||
send_feedback(
|
||||
course_session_id: $courseSessionId
|
||||
learning_content_page_id: $learningContentId
|
||||
data: $data
|
||||
submitted: $submitted
|
||||
) {
|
||||
feedback_response {
|
||||
id
|
||||
data
|
||||
submitted
|
||||
}
|
||||
errors {
|
||||
field
|
||||
messages
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
const feedbackSubmitted = ref(false);
|
||||
|
||||
const { executeMutation } = useMutation(sendFeedbackMutation);
|
||||
|
||||
interface FeedbackData {
|
||||
[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_negative_feedback: "",
|
||||
course_positive_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_negative_feedback",
|
||||
component: ItTextarea,
|
||||
},
|
||||
{
|
||||
modelKey: "course_positive_feedback",
|
||||
component: ItTextarea,
|
||||
},
|
||||
];
|
||||
|
||||
const previousStep = () => {
|
||||
if (stepNo.value > 0) {
|
||||
stepNo.value -= 1;
|
||||
}
|
||||
};
|
||||
|
||||
const nextStep = () => {
|
||||
if (stepNo.value < numSteps && hasStepValidInput(stepNo.value)) {
|
||||
stepNo.value += 1;
|
||||
}
|
||||
log.debug(`next step ${stepNo.value} of ${numSteps}`);
|
||||
mutateFeedback(feedbackData);
|
||||
};
|
||||
|
||||
function hasStepValidInput(stepNumber: number) {
|
||||
const question = questionData[stepNumber - 1];
|
||||
if (question) {
|
||||
if (
|
||||
[
|
||||
"instructor_open_feedback",
|
||||
"course_negative_feedback",
|
||||
"course_positive_feedback",
|
||||
].includes(question.modelKey)
|
||||
) {
|
||||
// text response questions need to have a "truthy" value (not "" or null)
|
||||
return feedbackData[question.modelKey];
|
||||
} else {
|
||||
// other responses need to have data, can be `0` or `false`
|
||||
return feedbackData[question.modelKey] !== null;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function mutateFeedback(data: FeedbackData, submit = false) {
|
||||
log.debug("mutate feedback", feedbackData);
|
||||
return executeMutation({
|
||||
courseSessionId: courseSession.value.id.toString(),
|
||||
learningContentId: props.content.id.toString(),
|
||||
data: data,
|
||||
submitted: submit,
|
||||
})
|
||||
.then((result) => {
|
||||
log.debug("feedback mutation result", result);
|
||||
if (result.data?.send_feedback?.feedback_response?.data) {
|
||||
const responseData = result.data.send_feedback.feedback_response.data;
|
||||
if (!responseData.instructor_open_feedback) {
|
||||
responseData.instructor_open_feedback = "";
|
||||
}
|
||||
if (!responseData.course_negative_feedback) {
|
||||
responseData.course_negative_feedback = "";
|
||||
}
|
||||
if (!responseData.course_positive_feedback) {
|
||||
responseData.course_positive_feedback = "";
|
||||
}
|
||||
Object.assign(feedbackData, responseData);
|
||||
log.debug("feedback data", feedbackData);
|
||||
feedbackSubmitted.value =
|
||||
result.data?.send_feedback?.feedback_response?.submitted || false;
|
||||
}
|
||||
})
|
||||
.catch((e) => log.error(e));
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
log.debug("Feedback mounted");
|
||||
await mutateFeedback({});
|
||||
if (feedbackSubmitted.value) {
|
||||
stepNo.value = numSteps - 1;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LearningContentMultiLayout
|
||||
:title="title"
|
||||
sub-title="Feedback"
|
||||
:learning-content="content"
|
||||
:show-start-button="stepNo === 0"
|
||||
:show-next-button="stepNo > 0 && stepNo + 1 < numSteps"
|
||||
:disable-next-button="!hasStepValidInput(stepNo)"
|
||||
:show-previous-button="stepNo > 0 && !feedbackSubmitted"
|
||||
:show-exit-button="stepNo + 1 === numSteps"
|
||||
:current-step="stepNo"
|
||||
:steps-count="numSteps"
|
||||
:start-badge-text="$t('general.introduction')"
|
||||
:end-badge-text="$t('general.submission')"
|
||||
:base-url="props.content.frontend_url"
|
||||
close-button-variant="close"
|
||||
@previous="previousStep()"
|
||||
@next="nextStep()"
|
||||
>
|
||||
<div>
|
||||
<p v-if="stepNo === 0" class="mt-10">
|
||||
{{
|
||||
$t("feedback.intro", {
|
||||
name: `${courseSessionsStore.circleExperts[0]?.first_name} ${courseSessionsStore.circleExperts[0]?.last_name}`,
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
<p v-if="stepNo > 0 && stepNo + 1 < numSteps" class="pb-2">
|
||||
{{ stepLabels[stepNo] }}
|
||||
</p>
|
||||
<div v-for="(question, index) in questionData" :key="index">
|
||||
<!-- eslint-disable -->
|
||||
<!-- eslint does not like the dynamic v-model... -->
|
||||
<component
|
||||
:is="question.component"
|
||||
v-if="index + 1 === stepNo"
|
||||
v-model="feedbackData[question.modelKey] as any"
|
||||
:items="question['items']"
|
||||
:cy-key="question.modelKey"
|
||||
/>
|
||||
<!-- eslint-enable -->
|
||||
</div>
|
||||
<FeedbackCompletition
|
||||
v-if="stepNo === 11"
|
||||
:avatar-url="courseSessionsStore.circleExperts[0].avatar_url"
|
||||
:title="
|
||||
$t('feedback.completionTitle', {
|
||||
name: `${courseSessionsStore.circleExperts[0].first_name} ${courseSessionsStore.circleExperts[0].last_name}`,
|
||||
})
|
||||
"
|
||||
:description="$t('feedback.completionDescription')"
|
||||
:feedback-sent="feedbackSubmitted"
|
||||
@send-feedback="mutateFeedback(feedbackData, true)"
|
||||
/>
|
||||
</div>
|
||||
</LearningContentMultiLayout>
|
||||
</template>
|
||||
|
|
@ -1,14 +1,19 @@
|
|||
<template>
|
||||
<div>
|
||||
<h1 class="hidden lg:mb-12 lg:block">{{ title }}</h1>
|
||||
<h2 class="hidden lg:mb-12 lg:block">{{ title }}</h2>
|
||||
<div
|
||||
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" />
|
||||
<h1 class="mb-8 block lg:hidden">{{ title }}</h1>
|
||||
<h2 class="mb-8 block lg:hidden">{{ title }}</h2>
|
||||
<div>
|
||||
<p class="mb-6">{{ description }}</p>
|
||||
<button v-if="!feedbackSent" class="btn-primary" @click="$emit('sendFeedback')">
|
||||
<button
|
||||
v-if="!feedbackSent"
|
||||
class="btn-primary"
|
||||
data-cy="sendFeedbackButton"
|
||||
@click="$emit('sendFeedback')"
|
||||
>
|
||||
{{ $t("feedback.sendFeedback") }}
|
||||
</button>
|
||||
<p v-else class="flex items-center bg-green-200 px-6 py-4">
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ const props = defineProps<{
|
|||
showStartButton: boolean;
|
||||
showPreviousButton: boolean;
|
||||
showNextButton: boolean;
|
||||
disableNextButton: boolean;
|
||||
showExitButton: boolean;
|
||||
closingButtonVariant: ClosingButtonVariant;
|
||||
}>();
|
||||
|
|
@ -41,6 +42,7 @@ const closingButtonText = computed(() => {
|
|||
</button>
|
||||
<button
|
||||
v-if="props.showNextButton"
|
||||
:disabled="props.disableNextButton"
|
||||
class="btn-blue z-10 flex items-center"
|
||||
data-cy="next-step"
|
||||
@click="$emit('next')"
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ interface Props {
|
|||
showStartButton: boolean;
|
||||
showPreviousButton: boolean;
|
||||
showNextButton: boolean;
|
||||
disableNextButton?: boolean;
|
||||
showExitButton: boolean;
|
||||
currentStep: number;
|
||||
stepsCount: number;
|
||||
|
|
@ -37,6 +38,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|||
closeButtonVariant: "mark_as_done",
|
||||
baseUrl: undefined,
|
||||
stepQueryParam: undefined,
|
||||
disableNextButton: false,
|
||||
beforeExitCallback: async () => Promise.resolve(),
|
||||
});
|
||||
|
||||
|
|
@ -103,6 +105,7 @@ const emit = defineEmits(["previous", "next", "exit"]);
|
|||
:show-previous-button="props.showPreviousButton"
|
||||
:show-exit-button="props.showExitButton"
|
||||
:closing-button-variant="props.closeButtonVariant"
|
||||
:disable-next-button="props.disableNextButton"
|
||||
@previous="emit('previous')"
|
||||
@next="emit('next')"
|
||||
@start="emit('next')"
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ const closingButtonVariant = computed(() => {
|
|||
<slot></slot>
|
||||
<LearningContentFooter
|
||||
:show-next-button="false"
|
||||
:disable-next-button="false"
|
||||
:show-previous-button="false"
|
||||
:show-exit-button="true"
|
||||
:show-start-button="false"
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ const { cloudPlugin } = require("cypress-cloud/plugin");
|
|||
module.exports = defineConfig({
|
||||
projectId: "RVEZS1",
|
||||
watchForFileChanges: false,
|
||||
video: false,
|
||||
video: true,
|
||||
viewportWidth: 1280,
|
||||
viewportHeight: 720,
|
||||
retries: {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,156 @@
|
|||
import { TEST_STUDENT1_USER_ID } from "../../consts";
|
||||
import { login } from "../helpers";
|
||||
|
||||
describe("feedbackStudent.cy.js", () => {
|
||||
beforeEach(() => {
|
||||
cy.manageCommand("cypress_reset");
|
||||
login("test-student1@example.com", "test");
|
||||
cy.visit("/course/test-lehrgang/learn/fahrzeug/feedback");
|
||||
});
|
||||
|
||||
it("can open feedback page", () => {
|
||||
cy.testLearningContentTitle("Wie zufrieden bist du?");
|
||||
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="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_negative_feedback"]').type(
|
||||
"Ich bin unzufrieden mit einigen Sachen."
|
||||
);
|
||||
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_positive_feedback"]').type(
|
||||
"Ich bin zufrieden mit den meisten Dingen."
|
||||
);
|
||||
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");
|
||||
|
||||
// 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,
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
import { login } from "../helpers";
|
||||
|
||||
describe("feedbackTrainer.cy.js", () => {
|
||||
beforeEach(() => {
|
||||
cy.visit("/course/test-lehrgang/learn/fahrzeug/feedback");
|
||||
});
|
||||
|
||||
it("can open feedback results page with empty results", () => {
|
||||
cy.manageCommand("cypress_reset");
|
||||
login("test-trainer1@example.com", "test");
|
||||
cy.visit("/course/test-lehrgang/cockpit");
|
||||
cy.get(
|
||||
'[data-cy="show-feedback-btn-test-lehrgang-lp-circle-fahrzeug-lc-feedback"]'
|
||||
).click();
|
||||
|
||||
cy.get('[data-cy="feedback-data-amount"]').should("contain", "0");
|
||||
});
|
||||
|
||||
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="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="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="rating-scale-average"]')
|
||||
.should("contain", "2.7");
|
||||
|
||||
cy.get('[data-cy="question-6"]')
|
||||
.find('[data-cy="rating-scale-average"]')
|
||||
.should("contain", "3.0");
|
||||
|
||||
cy.get('[data-cy="question-7"]')
|
||||
.should("contain", "Super Kurs!")
|
||||
.should("contain", "Super, bin begeistert")
|
||||
.should("contain", "Ok, entspricht den Erwartungen");
|
||||
|
||||
cy.get('[data-cy="question-8"]')
|
||||
.find('[data-cy="popover-yes"]')
|
||||
.click()
|
||||
.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-9"]')
|
||||
.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-10"]')
|
||||
.should("contain", "Nur Gutes.")
|
||||
.should("contain", "Das Beispiel mit der Katze fand ich sehr gut")
|
||||
.should("contain", "Die Präsentation war super");
|
||||
});
|
||||
});
|
||||
|
|
@ -120,6 +120,7 @@ function loadObjectJson(
|
|||
`.replace(/(?:\r\n|\r|\n)/g, "");
|
||||
return cy.manageShellCommand(command).then((result) => {
|
||||
const objectJson = JSON.parse(result.stdout);
|
||||
// console.log(command);
|
||||
console.log(objectJson);
|
||||
return objectJson;
|
||||
});
|
||||
|
|
@ -135,6 +136,16 @@ Cypress.Commands.add("loadAssignmentCompletion", (key, value) => {
|
|||
);
|
||||
});
|
||||
|
||||
Cypress.Commands.add("loadFeedbackResponse", (key, value) => {
|
||||
return loadObjectJson(
|
||||
key,
|
||||
value,
|
||||
"vbv_lernwelt.feedback.models.FeedbackResponse",
|
||||
"vbv_lernwelt.feedback.serializers.CypressFeedbackResponseSerializer",
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
Cypress.Commands.add("makeSelfEvaluation", (answers) => {
|
||||
for (let i = 0; i < answers.length; i++) {
|
||||
const answer = answers[i];
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ ADMIN_USER_ID = "872efd96-3bd7-4a1e-a239-2d72cad9f604"
|
|||
TEST_TRAINER1_USER_ID = "b9e71f59-c44f-4290-b93a-9b3151e9a2fc"
|
||||
TEST_STUDENT1_USER_ID = "65c73ad0-6d53-43a9-a4a4-64143f27b03a"
|
||||
TEST_STUDENT2_USER_ID = "19c40d94-15cc-4198-aaad-ef707c4b0900"
|
||||
TEST_STUDENT3_USER_ID = "bcf94dba-53bc-474b-a22d-e4af39aa042b"
|
||||
|
||||
TEST_COURSE_SESSION_BERN_ID = -1
|
||||
TEST_COURSE_SESSION_ZURICH_ID = -2
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ from vbv_lernwelt.core.constants import (
|
|||
ADMIN_USER_ID,
|
||||
TEST_STUDENT1_USER_ID,
|
||||
TEST_STUDENT2_USER_ID,
|
||||
TEST_STUDENT3_USER_ID,
|
||||
TEST_TRAINER1_USER_ID,
|
||||
)
|
||||
from vbv_lernwelt.core.models import User
|
||||
|
|
@ -284,6 +285,13 @@ def create_default_users(user_model=User, group_model=Group, default_password=No
|
|||
last_name="Student2",
|
||||
avatar_url="/static/avatars/uk1.lina.egger.jpg",
|
||||
)
|
||||
_create_student_user(
|
||||
id=TEST_STUDENT3_USER_ID,
|
||||
email="test-student3@example.com",
|
||||
first_name="Test",
|
||||
last_name="Student3",
|
||||
avatar_url="/static/avatars/uk1.christian.koller.jpg",
|
||||
)
|
||||
_create_staff_user(
|
||||
email="matthias.wirth@vbv-afa.ch",
|
||||
first_name="Matthias",
|
||||
|
|
|
|||
|
|
@ -4,15 +4,20 @@ from vbv_lernwelt.assignment.models import Assignment, AssignmentCompletion
|
|||
from vbv_lernwelt.core.constants import (
|
||||
TEST_COURSE_SESSION_BERN_ID,
|
||||
TEST_STUDENT1_USER_ID,
|
||||
TEST_STUDENT2_USER_ID,
|
||||
TEST_STUDENT3_USER_ID,
|
||||
TEST_TRAINER1_USER_ID,
|
||||
)
|
||||
from vbv_lernwelt.core.models import User
|
||||
from vbv_lernwelt.course.creators.test_course import (
|
||||
create_edoniq_test_result_data,
|
||||
create_feedback_response_data,
|
||||
create_test_assignment_evaluation_data,
|
||||
create_test_assignment_submitted_data,
|
||||
)
|
||||
from vbv_lernwelt.course.models import CourseCompletion, CourseSession
|
||||
from vbv_lernwelt.feedback.models import FeedbackResponse
|
||||
from vbv_lernwelt.learnpath.models import LearningContentFeedback
|
||||
from vbv_lernwelt.notify.models import Notification
|
||||
|
||||
|
||||
|
|
@ -32,15 +37,22 @@ from vbv_lernwelt.notify.models import Notification
|
|||
default=False,
|
||||
help="will create edoniq result data for test-student1@example.com",
|
||||
)
|
||||
@click.option(
|
||||
"--create-feedback-responses/--no-create-feedback-responses",
|
||||
default=False,
|
||||
help="will create feedback response data",
|
||||
)
|
||||
def command(
|
||||
create_assignment_completion,
|
||||
create_assignment_evaluation,
|
||||
create_edoniq_test_results,
|
||||
create_feedback_responses,
|
||||
):
|
||||
print("cypress reset data")
|
||||
CourseCompletion.objects.all().delete()
|
||||
Notification.objects.all().delete()
|
||||
AssignmentCompletion.objects.all().delete()
|
||||
FeedbackResponse.objects.all().delete()
|
||||
User.objects.all().update(language="de")
|
||||
User.objects.all().update(additional_json_data={})
|
||||
|
||||
|
|
@ -74,3 +86,66 @@ def command(
|
|||
assignment_user=User.objects.get(id=TEST_STUDENT1_USER_ID),
|
||||
points=19,
|
||||
)
|
||||
|
||||
if create_feedback_responses:
|
||||
print("create_feedback_responses")
|
||||
course_session = CourseSession.objects.get(id=TEST_COURSE_SESSION_BERN_ID)
|
||||
learning_content_feedback_page = LearningContentFeedback.objects.get(
|
||||
slug="test-lehrgang-lp-circle-fahrzeug-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,
|
||||
"instructor_competence": 4,
|
||||
"instructor_respect": 4,
|
||||
"instructor_open_feedback": "Super Kurs!",
|
||||
"would_recommend": True,
|
||||
"course_negative_feedback": "Nichts Schlechtes",
|
||||
"course_positive_feedback": "Nur Gutes.",
|
||||
},
|
||||
)
|
||||
|
||||
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,
|
||||
"instructor_competence": 3,
|
||||
"instructor_respect": 3,
|
||||
"instructor_open_feedback": "Super, bin begeistert",
|
||||
"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!",
|
||||
},
|
||||
)
|
||||
|
||||
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,
|
||||
"instructor_competence": 1,
|
||||
"instructor_respect": 2,
|
||||
"instructor_open_feedback": "Ok, entspricht den Erwartungen",
|
||||
"would_recommend": False,
|
||||
"course_negative_feedback": "Mehr Videos wären schön.",
|
||||
"course_positive_feedback": "Die Präsentation war super",
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ from vbv_lernwelt.course_session.models import (
|
|||
CourseSessionAssignment,
|
||||
CourseSessionAttendanceCourse,
|
||||
)
|
||||
from vbv_lernwelt.feedback.services import update_feedback_response
|
||||
from vbv_lernwelt.learnpath.models import (
|
||||
Circle,
|
||||
LearningContentAssignment,
|
||||
|
|
@ -200,6 +201,12 @@ def create_test_course(include_uk=True, include_vv=True, with_sessions=False):
|
|||
user=student2,
|
||||
)
|
||||
|
||||
student3 = User.objects.get(email="test-student3@example.com")
|
||||
_csu = CourseSessionUser.objects.create(
|
||||
course_session=cs_bern,
|
||||
user=student3,
|
||||
)
|
||||
|
||||
return course
|
||||
|
||||
|
||||
|
|
@ -286,6 +293,36 @@ def create_edoniq_test_result_data(
|
|||
)
|
||||
|
||||
|
||||
def create_feedback_response_data(
|
||||
course_session,
|
||||
feedback_user,
|
||||
learning_content_feedback_page,
|
||||
submitted=True,
|
||||
feedback_data=None,
|
||||
):
|
||||
if feedback_data is None:
|
||||
feedback_data = {
|
||||
"satisfaction": 4,
|
||||
"goal_attainment": 3,
|
||||
"proficiency": 80,
|
||||
"preparation_task_clarity": True,
|
||||
"instructor_competence": 4,
|
||||
"instructor_respect": 4,
|
||||
"instructor_open_feedback": "Super Kurs!",
|
||||
"would_recommend": True,
|
||||
"course_negative_feedback": "Nichts Schlechtes",
|
||||
"course_positive_feedback": "Nur Gutes.",
|
||||
}
|
||||
|
||||
return update_feedback_response(
|
||||
feedback_user=feedback_user,
|
||||
course_session=course_session,
|
||||
learning_content_feedback_page=learning_content_feedback_page,
|
||||
submitted=submitted,
|
||||
validated_data=feedback_data,
|
||||
)
|
||||
|
||||
|
||||
def create_test_course_with_categories(apps=None, schema_editor=None):
|
||||
if apps is not None:
|
||||
Course = apps.get_model("course", "Course")
|
||||
|
|
|
|||
|
|
@ -86,7 +86,6 @@ from vbv_lernwelt.course_session.models import (
|
|||
CourseSessionAssignment,
|
||||
CourseSessionAttendanceCourse,
|
||||
)
|
||||
from vbv_lernwelt.feedback.creators.create_demo_feedback import create_feedback
|
||||
from vbv_lernwelt.importer.services import (
|
||||
import_course_sessions_from_excel,
|
||||
import_students_from_excel,
|
||||
|
|
@ -237,10 +236,11 @@ def create_versicherungsvermittlerin_course(
|
|||
circles = Circle.objects.filter(
|
||||
slug__startswith="versicherungsvermittler-in-lp"
|
||||
)
|
||||
for i, circle in enumerate(circles):
|
||||
expert = experts[i % len(experts)]
|
||||
expert.expert.add(circle)
|
||||
create_feedback(circle, cs, 3)
|
||||
|
||||
# for i, circle in enumerate(circles):
|
||||
# expert = experts[i % len(experts)]
|
||||
# expert.expert.add(circle)
|
||||
# create_feedback(circle, cs, 3)
|
||||
|
||||
for admin_email in ADMIN_EMAILS:
|
||||
CourseSessionUser.objects.create(
|
||||
|
|
@ -395,19 +395,20 @@ def create_course_uk_de_course_sessions():
|
|||
user=User.objects.get(username="patrick.muster@eiger-versicherungen.ch"),
|
||||
)
|
||||
|
||||
create_feedback(
|
||||
Circle.objects.get(slug="überbetriebliche-kurse-lp-circle-kickoff"),
|
||||
cs,
|
||||
3,
|
||||
)
|
||||
create_feedback(
|
||||
Circle.objects.get(slug="überbetriebliche-kurse-lp-circle-haushalt-teil-2"),
|
||||
cs,
|
||||
14,
|
||||
)
|
||||
create_feedback(
|
||||
Circle.objects.get(slug="überbetriebliche-kurse-lp-circle-basis"), cs, 4
|
||||
)
|
||||
# TODO: feedback must now contain a `feedback_user`
|
||||
# create_feedback(
|
||||
# Circle.objects.get(slug="überbetriebliche-kurse-lp-circle-kickoff"),
|
||||
# cs,
|
||||
# 3,
|
||||
# )
|
||||
# create_feedback(
|
||||
# Circle.objects.get(slug="überbetriebliche-kurse-lp-circle-haushalt-teil-2"),
|
||||
# cs,
|
||||
# 14,
|
||||
# )
|
||||
# create_feedback(
|
||||
# Circle.objects.get(slug="überbetriebliche-kurse-lp-circle-basis"), cs, 4
|
||||
# )
|
||||
|
||||
|
||||
def create_course_uk_fr():
|
||||
|
|
|
|||
|
|
@ -18,6 +18,18 @@ def has_course_access(user, course_id):
|
|||
return False
|
||||
|
||||
|
||||
def has_course_session_access(user, course_session_id: int):
|
||||
if user.is_superuser:
|
||||
return True
|
||||
|
||||
if CourseSessionUser.objects.filter(
|
||||
course_session_id=course_session_id, user=user
|
||||
).exists():
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def is_course_session_expert(user, course_session_id: int):
|
||||
if user.is_superuser:
|
||||
return True
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ class EdoniqUserExportTestCase(TestCase):
|
|||
|
||||
def test_fetch_course_session_users(self):
|
||||
users = fetch_course_session_users([COURSE_TEST_ID], excluded_domains=[])
|
||||
self.assertEqual(len(users), 2)
|
||||
self.assertEqual(len(users), 3)
|
||||
|
||||
def test_fetch_course_session_trainers(self):
|
||||
users = fetch_course_session_users(
|
||||
|
|
@ -51,11 +51,11 @@ class EdoniqUserExportTestCase(TestCase):
|
|||
users = fetch_course_session_users(
|
||||
[COURSE_TEST_ID], excluded_domains=["eiger-versicherungen.ch"]
|
||||
)
|
||||
self.assertEqual(len(users), 1)
|
||||
self.assertEqual(len(users), 2)
|
||||
|
||||
def test_export_students_and_trainers(self):
|
||||
users = fetch_course_session_all_users([COURSE_TEST_ID], excluded_domains=[])
|
||||
self.assertEqual(len(users), 3)
|
||||
self.assertEqual(len(users), 4)
|
||||
|
||||
def test_deduplicates_users(self):
|
||||
trainer1 = User.objects.get(email="test-trainer1@example.com")
|
||||
|
|
@ -67,7 +67,7 @@ class EdoniqUserExportTestCase(TestCase):
|
|||
user=trainer1,
|
||||
)
|
||||
users = fetch_course_session_all_users([COURSE_TEST_ID], excluded_domains=[])
|
||||
self.assertEqual(len(users), 3)
|
||||
self.assertEqual(len(users), 4)
|
||||
|
||||
def test_response_csv(self):
|
||||
users = fetch_course_session_users([COURSE_TEST_ID], excluded_domains=[])
|
||||
|
|
|
|||
|
|
@ -5,4 +5,5 @@ from vbv_lernwelt.learnpath.models import Circle
|
|||
|
||||
def create_feedback(circle: Circle, course_session: CourseSession, amount: int):
|
||||
for _i in range(amount):
|
||||
# FIXME needs `feedback_user` to work again
|
||||
FeedbackResponseFactory(circle=circle, course_session=course_session).save()
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ class FeedbackResponseFactory(DjangoModelFactory):
|
|||
[
|
||||
"Alles gut, manchmal etwas langfädig",
|
||||
"Super, bin begeistert",
|
||||
"Ok, enspricht den Erwartungen",
|
||||
"Ok, entspricht den Erwartungen",
|
||||
]
|
||||
),
|
||||
"would_recommend": FuzzyChoice([True, False]),
|
||||
|
|
|
|||
|
|
@ -1,55 +1,94 @@
|
|||
import graphene
|
||||
import structlog
|
||||
from graphene import ClientIDMutation, Field, Int, List
|
||||
from graphene.types.generic import GenericScalar
|
||||
from graphene_django.types import ErrorType
|
||||
|
||||
from vbv_lernwelt.course.models import CourseSession
|
||||
from vbv_lernwelt.feedback.graphql.types import FeedbackResponse as FeedbackResponseType
|
||||
from vbv_lernwelt.feedback.models import FeedbackResponse
|
||||
from vbv_lernwelt.course.permissions import has_course_session_access
|
||||
from vbv_lernwelt.feedback.graphql.types import (
|
||||
FeedbackResponseObjectType as FeedbackResponseType,
|
||||
)
|
||||
from vbv_lernwelt.feedback.serializers import CourseFeedbackSerializer
|
||||
from wagtail.models import Page
|
||||
from vbv_lernwelt.feedback.services import update_feedback_response
|
||||
from vbv_lernwelt.learnpath.models import LearningContentFeedback
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
# https://medium.com/open-graphql/jsonfield-models-in-graphene-django-308ae43d14ee
|
||||
class SendFeedback(ClientIDMutation):
|
||||
feedback_response = Field(FeedbackResponseType)
|
||||
errors = List(
|
||||
class SendFeedbackMutation(graphene.Mutation):
|
||||
feedback_response = graphene.Field(FeedbackResponseType)
|
||||
errors = graphene.List(
|
||||
ErrorType, description="May contain more than one error for same field."
|
||||
)
|
||||
|
||||
class Input:
|
||||
page = Int(required=True)
|
||||
course_session = Int(required=True)
|
||||
class Arguments:
|
||||
course_session_id = graphene.ID(required=True)
|
||||
learning_content_page_id = graphene.ID(required=True)
|
||||
data = GenericScalar()
|
||||
submitted = graphene.Boolean(required=False, default_value=False)
|
||||
|
||||
@classmethod
|
||||
def mutate_and_get_payload(cls, _, info, **input):
|
||||
page_id = input["page"]
|
||||
course_session_id = input["course_session"]
|
||||
logger.info("creating feedback")
|
||||
|
||||
learning_content = Page.objects.get(id=page_id)
|
||||
circle = learning_content.get_parent().specific
|
||||
def mutate(
|
||||
cls,
|
||||
root,
|
||||
info,
|
||||
course_session_id,
|
||||
learning_content_page_id,
|
||||
data,
|
||||
submitted,
|
||||
):
|
||||
feedback_user_id = info.context.user.id
|
||||
learning_content = LearningContentFeedback.objects.get(
|
||||
id=learning_content_page_id
|
||||
)
|
||||
circle = learning_content.get_circle()
|
||||
course_session = CourseSession.objects.get(id=course_session_id)
|
||||
data = input.get("data", {})
|
||||
|
||||
if not has_course_session_access(
|
||||
info.context.user,
|
||||
course_session.id,
|
||||
):
|
||||
return SendFeedbackMutation(
|
||||
errors=[
|
||||
ErrorType(
|
||||
field="send_feedback", messages=["Insufficient permissions"]
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"creating feedback",
|
||||
label="feedback",
|
||||
feedback_user_id=feedback_user_id,
|
||||
circle_title=circle.title,
|
||||
course_session_id=course_session_id,
|
||||
)
|
||||
|
||||
serializer = CourseFeedbackSerializer(data=data)
|
||||
|
||||
if not serializer.is_valid():
|
||||
logger.error(serializer.errors)
|
||||
return SendFeedback(errors=serializer.errors)
|
||||
logger.error(
|
||||
"creating feedback serializer invalid",
|
||||
error_list=serializer.errors,
|
||||
label="feedback",
|
||||
)
|
||||
errors = [
|
||||
ErrorType(field=field, messages=msgs)
|
||||
for field, msgs in serializer.errors.items()
|
||||
]
|
||||
return SendFeedbackMutation(errors=errors)
|
||||
|
||||
feedback_response = FeedbackResponse.objects.create(
|
||||
circle=circle,
|
||||
feedback_response = update_feedback_response(
|
||||
feedback_user=info.context.user,
|
||||
course_session=course_session,
|
||||
data=serializer.validated_data,
|
||||
learning_content_feedback_page=learning_content,
|
||||
submitted=submitted,
|
||||
validated_data=serializer.validated_data,
|
||||
)
|
||||
logger.info(feedback_response)
|
||||
|
||||
return SendFeedback(feedback_response=feedback_response)
|
||||
return SendFeedbackMutation(feedback_response=feedback_response)
|
||||
|
||||
|
||||
class FeedbackMutation(object):
|
||||
send_feedback = SendFeedback.Field()
|
||||
send_feedback = SendFeedbackMutation.Field()
|
||||
|
|
|
|||
|
|
@ -1,13 +1,16 @@
|
|||
from graphene.relay import Node
|
||||
from graphene.types.generic import GenericScalar
|
||||
from graphene_django import DjangoObjectType
|
||||
|
||||
from vbv_lernwelt.feedback.models import FeedbackResponse as FeedbackResponseModel
|
||||
|
||||
|
||||
class FeedbackResponse(DjangoObjectType):
|
||||
class FeedbackResponseObjectType(DjangoObjectType):
|
||||
data = GenericScalar()
|
||||
|
||||
class Meta:
|
||||
model = FeedbackResponseModel
|
||||
interfaces = (Node,)
|
||||
fields = [
|
||||
"id",
|
||||
"submitted",
|
||||
"data",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
# Generated by Django 3.2.20 on 2023-09-21 13:30
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
("feedback", "0003_alter_feedbackresponse_course_session"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="feedbackresponse",
|
||||
name="feedback_user",
|
||||
field=models.ForeignKey(
|
||||
default="872efd96-3bd7-4a1e-a239-2d72cad9f604",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="core.user",
|
||||
),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
# Generated by Django 3.2.20 on 2023-09-22 09:31
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("feedback", "0004_feedbackresponse_feedback_user"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="feedbackresponse",
|
||||
name="submitted",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="feedbackresponse",
|
||||
name="notification_sent",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="feedbackresponse",
|
||||
name="updated_at",
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
# Generated by Django 3.2.20 on 2023-09-22 09:31
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def set_feedback_submitted_true(apps, schema_editor):
|
||||
FeedbackResponse = apps.get_model("feedback", "FeedbackResponse")
|
||||
FeedbackResponse.objects.update(submitted=True)
|
||||
FeedbackResponse.objects.update(notification_sent=True)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("feedback", "0005_auto_20230922_1131"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(set_feedback_submitted_true),
|
||||
]
|
||||
|
|
@ -25,6 +25,7 @@ class FeedbackIntegerField(models.IntegerField):
|
|||
|
||||
class FeedbackResponse(models.Model):
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
feedback_user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
|
||||
class DiscoveredChoices(models.TextChoices):
|
||||
INTERNET = "I", _("Internet")
|
||||
|
|
@ -48,14 +49,10 @@ class FeedbackResponse(models.Model):
|
|||
HUNDRED = 100, "100%"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# with `id=UUIDField` it is always set...
|
||||
create_new = self._state.adding
|
||||
|
||||
super(FeedbackResponse, self).save(*args, **kwargs)
|
||||
|
||||
try:
|
||||
if create_new:
|
||||
# with `id=UUIDField` it is always set...
|
||||
if self.submitted and not self.notification_sent:
|
||||
course_session_users = CourseSessionUser.objects.filter(
|
||||
role="EXPERT",
|
||||
course_session=self.course_session,
|
||||
|
|
@ -66,6 +63,8 @@ class FeedbackResponse(models.Model):
|
|||
recipient=csu.user,
|
||||
feedback_response=self,
|
||||
)
|
||||
self.notification_sent = True
|
||||
self.save()
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"Failed to send feedback notification",
|
||||
|
|
@ -75,6 +74,9 @@ class FeedbackResponse(models.Model):
|
|||
|
||||
data = models.JSONField(default=dict)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
submitted = models.BooleanField(default=False)
|
||||
notification_sent = models.BooleanField(default=False)
|
||||
|
||||
circle = models.ForeignKey("learnpath.Circle", models.PROTECT)
|
||||
course_session = models.ForeignKey("course.CourseSession", models.CASCADE)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import structlog
|
||||
from rest_framework import serializers
|
||||
|
||||
from vbv_lernwelt.feedback.models import FeedbackResponse
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
|
|
@ -29,3 +31,9 @@ class CourseFeedbackSerializer(serializers.Serializer):
|
|||
course_negative_feedback = serializers.CharField(
|
||||
required=False, allow_null=True, allow_blank=True
|
||||
)
|
||||
|
||||
|
||||
class CypressFeedbackResponseSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = FeedbackResponse
|
||||
fields = "__all__"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
import structlog
|
||||
|
||||
from vbv_lernwelt.core.models import User
|
||||
from vbv_lernwelt.course.models import CourseCompletionStatus, CourseSession
|
||||
from vbv_lernwelt.course.services import mark_course_completion
|
||||
from vbv_lernwelt.feedback.models import FeedbackResponse
|
||||
from vbv_lernwelt.learnpath.models import LearningContentFeedback
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
def update_feedback_response(
|
||||
feedback_user: User,
|
||||
course_session: CourseSession,
|
||||
learning_content_feedback_page: LearningContentFeedback,
|
||||
submitted: bool,
|
||||
validated_data: dict,
|
||||
):
|
||||
circle = learning_content_feedback_page.get_circle()
|
||||
|
||||
feedback_response, _ = FeedbackResponse.objects.get_or_create(
|
||||
feedback_user_id=feedback_user.id,
|
||||
circle_id=circle.id,
|
||||
course_session=course_session,
|
||||
)
|
||||
|
||||
original_data = feedback_response.data
|
||||
updated_data = validated_data
|
||||
initial_data = {
|
||||
"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 | {
|
||||
key: updated_data[key]
|
||||
if updated_data.get(key, "") != ""
|
||||
else original_data.get(key)
|
||||
for key in initial_data.keys()
|
||||
}
|
||||
|
||||
feedback_response.data = merged_data
|
||||
|
||||
# save the response before completion mark,
|
||||
# because who knows what could happen in between...
|
||||
if submitted:
|
||||
feedback_response.submitted = submitted
|
||||
feedback_response.save()
|
||||
|
||||
if submitted:
|
||||
mark_course_completion(
|
||||
user=feedback_user,
|
||||
page=learning_content_feedback_page,
|
||||
course_session=course_session,
|
||||
completion_status=CourseCompletionStatus.SUCCESS.value,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"feedback successfully created",
|
||||
label="feedback",
|
||||
feedback_user_id=feedback_user.id,
|
||||
circle_title=circle.title,
|
||||
course_session_id=course_session.id,
|
||||
)
|
||||
|
||||
return feedback_response
|
||||
|
|
@ -2,7 +2,6 @@ from rest_framework.test import APITestCase
|
|||
|
||||
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, CourseSessionUser
|
||||
from vbv_lernwelt.feedback.factories import FeedbackResponseFactory
|
||||
|
|
@ -15,70 +14,35 @@ from vbv_lernwelt.notify.models import (
|
|||
)
|
||||
|
||||
|
||||
class FeedbackApiBaseTestCase(APITestCase):
|
||||
class FeedbackBaseTestCase(APITestCase):
|
||||
def setUp(self) -> None:
|
||||
create_default_users()
|
||||
create_test_course()
|
||||
|
||||
self.user = User.objects.get(username="student")
|
||||
self.expert = User.objects.get(
|
||||
username="patrizia.huggel@eiger-versicherungen.ch"
|
||||
)
|
||||
|
||||
self.course_session = CourseSession.objects.create(
|
||||
course_id=COURSE_TEST_ID,
|
||||
title="Test Lehrgang Session",
|
||||
)
|
||||
|
||||
csu = CourseSessionUser.objects.create(
|
||||
course_session=self.course_session,
|
||||
user=User.objects.get(username="patrizia.huggel@eiger-versicherungen.ch"),
|
||||
role=CourseSessionUser.Role.EXPERT,
|
||||
)
|
||||
csu.expert.add(Circle.objects.get(slug="test-lehrgang-lp-circle-fahrzeug"))
|
||||
|
||||
_csu = CourseSessionUser.objects.create(
|
||||
course_session=self.course_session,
|
||||
user=self.user,
|
||||
role=CourseSessionUser.Role.MEMBER,
|
||||
)
|
||||
|
||||
self.test_data = {
|
||||
"file_name": "test.pdf",
|
||||
"file_type": "application/pdf",
|
||||
"name": "Test",
|
||||
"course_session": self.course_session.id,
|
||||
}
|
||||
|
||||
self.client.login(
|
||||
username="patrizia.huggel@eiger-versicherungen.ch", password="myvbv1234"
|
||||
)
|
||||
create_test_course(include_vv=False, with_sessions=True)
|
||||
self.course_session = CourseSession.objects.get(title="Test Bern 2022 a")
|
||||
self.trainer = User.objects.get(username="test-trainer1@example.com")
|
||||
self.student = User.objects.get(username="test-student1@example.com")
|
||||
self.circle_basis = Circle.objects.get(slug="test-lehrgang-lp-circle-fahrzeug")
|
||||
|
||||
|
||||
class FeedbackSummaryApiTestCase(FeedbackApiBaseTestCase):
|
||||
def test_triggers_notification(self):
|
||||
expert = User.objects.get(username="patrizia.huggel@eiger-versicherungen.ch")
|
||||
csu = CourseSessionUser.objects.get(
|
||||
course_session=self.course_session,
|
||||
user=expert,
|
||||
role=CourseSessionUser.Role.EXPERT,
|
||||
)
|
||||
basis_circle = Circle.objects.get(slug="test-lehrgang-lp-circle-reisen")
|
||||
csu.expert.add(basis_circle)
|
||||
|
||||
class FeedbackNotificationTestCase(FeedbackBaseTestCase):
|
||||
def test_creating_submitted_feedback_triggers_notification(self):
|
||||
feedback = FeedbackResponse.objects.create(
|
||||
circle=basis_circle, course_session=csu.course_session
|
||||
circle=self.circle_basis,
|
||||
course_session=self.course_session,
|
||||
feedback_user=self.student,
|
||||
submitted=True,
|
||||
)
|
||||
|
||||
self.assertEqual(Notification.objects.count(), 1)
|
||||
notification = Notification.objects.first()
|
||||
self.assertEqual(notification.recipient, expert)
|
||||
self.assertEqual(notification.recipient, self.trainer)
|
||||
self.assertEqual(
|
||||
notification.verb, f"Feedback abgeschickt für Circle «{basis_circle.title}»"
|
||||
notification.verb,
|
||||
f"Feedback abgeschickt für Circle «{self.circle_basis.title}»",
|
||||
)
|
||||
self.assertEqual(
|
||||
notification.target_url,
|
||||
f"/course/{self.course_session.course.slug}/cockpit/feedback/{basis_circle.id}/",
|
||||
f"/course/{self.course_session.course.slug}/cockpit/feedback/{self.circle_basis.id}/",
|
||||
)
|
||||
self.assertEqual(
|
||||
notification.notification_category, NotificationCategory.INFORMATION
|
||||
|
|
@ -87,121 +51,22 @@ class FeedbackSummaryApiTestCase(FeedbackApiBaseTestCase):
|
|||
notification.notification_trigger, NotificationTrigger.NEW_FEEDBACK
|
||||
)
|
||||
self.assertEqual(notification.action_object, feedback)
|
||||
self.assertEqual(notification.course_session, csu.course_session)
|
||||
self.assertEqual(notification.course_session, self.course_session)
|
||||
|
||||
def test_triggers_notification_only_on_create(self):
|
||||
expert = User.objects.get(username="patrizia.huggel@eiger-versicherungen.ch")
|
||||
csu = CourseSessionUser.objects.get(
|
||||
def test_only_submitted_feedback_triggers_notification(self):
|
||||
feedback = FeedbackResponse.objects.create(
|
||||
circle=self.circle_basis,
|
||||
course_session=self.course_session,
|
||||
user=expert,
|
||||
role=CourseSessionUser.Role.EXPERT,
|
||||
)
|
||||
basis_circle = Circle.objects.get(slug="test-lehrgang-lp-circle-reisen")
|
||||
csu.expert.add(basis_circle)
|
||||
|
||||
feedback = FeedbackResponseFactory(
|
||||
circle=basis_circle, course_session=csu.course_session
|
||||
)
|
||||
feedback.save()
|
||||
|
||||
# Check that the initial notification was created and then deleted
|
||||
self.assertEqual(len(Notification.objects.all()), 1)
|
||||
Notification.objects.all().delete()
|
||||
self.assertEqual(len(Notification.objects.all()), 0)
|
||||
|
||||
# Check that an update of the feedback does not trigger a notification
|
||||
feedback.name = "Test2"
|
||||
feedback.save()
|
||||
self.assertEqual(len(Notification.objects.all()), 0)
|
||||
|
||||
def test_can_get_feedback_summary_for_circles(self):
|
||||
number_reisen_feedback = 5
|
||||
number_fahrzeug_feedback = 10
|
||||
|
||||
csu = CourseSessionUser.objects.get(
|
||||
course_session=self.course_session,
|
||||
user=User.objects.get(username="patrizia.huggel@eiger-versicherungen.ch"),
|
||||
role=CourseSessionUser.Role.EXPERT,
|
||||
)
|
||||
fahrzeug_circle = Circle.objects.get(slug="test-lehrgang-lp-circle-fahrzeug")
|
||||
reisen_circle = Circle.objects.get(slug="test-lehrgang-lp-circle-reisen")
|
||||
csu.expert.add(reisen_circle)
|
||||
|
||||
for i in range(number_reisen_feedback):
|
||||
FeedbackResponseFactory(
|
||||
circle=reisen_circle, course_session=csu.course_session
|
||||
).save()
|
||||
|
||||
for i in range(number_fahrzeug_feedback):
|
||||
FeedbackResponseFactory(
|
||||
circle=fahrzeug_circle, course_session=csu.course_session
|
||||
).save()
|
||||
|
||||
response = self.client.get(
|
||||
f"/api/core/feedback/{csu.course_session.id}/summary/"
|
||||
feedback_user=self.student,
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
expected = [
|
||||
{"circle_id": fahrzeug_circle.id, "count": number_fahrzeug_feedback},
|
||||
{"circle_id": reisen_circle.id, "count": number_reisen_feedback},
|
||||
]
|
||||
self.assertEqual(response.data, expected)
|
||||
|
||||
def test_can_only_see_feedback_from_own_circle(self):
|
||||
number_basis_feedback = 5
|
||||
number_analyse_feedback = 10
|
||||
|
||||
csu = CourseSessionUser.objects.get(
|
||||
course_session=self.course_session,
|
||||
user=User.objects.get(username="patrizia.huggel@eiger-versicherungen.ch"),
|
||||
role=CourseSessionUser.Role.EXPERT,
|
||||
)
|
||||
fahrzeug_circle = Circle.objects.get(slug="test-lehrgang-lp-circle-fahrzeug")
|
||||
reisen_circle = Circle.objects.get(slug="test-lehrgang-lp-circle-reisen")
|
||||
|
||||
for i in range(number_basis_feedback):
|
||||
FeedbackResponseFactory(
|
||||
circle=reisen_circle, course_session=csu.course_session
|
||||
).save()
|
||||
|
||||
for i in range(number_analyse_feedback):
|
||||
FeedbackResponseFactory(
|
||||
circle=fahrzeug_circle, course_session=csu.course_session
|
||||
).save()
|
||||
|
||||
response = self.client.get(
|
||||
f"/api/core/feedback/{csu.course_session.id}/summary/"
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
expected = [
|
||||
{"circle_id": fahrzeug_circle.id, "count": number_analyse_feedback},
|
||||
]
|
||||
self.assertEqual(response.data, expected)
|
||||
|
||||
def test_student_does_not_see_feedback(self):
|
||||
self.client.login(username="student", password="test")
|
||||
csu = CourseSessionUser.objects.get(
|
||||
course_session=self.course_session,
|
||||
user=self.user,
|
||||
)
|
||||
fahrzeug_circle = Circle.objects.get(slug="test-lehrgang-lp-circle-fahrzeug")
|
||||
FeedbackResponseFactory(
|
||||
circle=fahrzeug_circle, course_session=csu.course_session
|
||||
).save()
|
||||
|
||||
response = self.client.get(
|
||||
f"/api/core/feedback/{csu.course_session.id}/summary/"
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data, [])
|
||||
self.assertEqual(Notification.objects.count(), 0)
|
||||
|
||||
|
||||
class FeedbackDetailApiTestCase(FeedbackApiBaseTestCase):
|
||||
def test_can_receive_feedback(self):
|
||||
feedback_data = {
|
||||
class FeedbackRestApiTestCase(FeedbackBaseTestCase):
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
self.feedback_data = {
|
||||
"satisfaction": [1, 4, 2],
|
||||
"goal_attainment": [2, 4, 3],
|
||||
"proficiency": [20, 60, 80],
|
||||
|
|
@ -213,81 +78,83 @@ class FeedbackDetailApiTestCase(FeedbackApiBaseTestCase):
|
|||
"course_positive_feedback": ["Bla", "Katze", "Hund"],
|
||||
"course_negative_feedback": ["Maus", "Hase", "Fuchs"],
|
||||
}
|
||||
csu = CourseSessionUser.objects.get(
|
||||
course_session=self.course_session,
|
||||
user=User.objects.get(username="patrizia.huggel@eiger-versicherungen.ch"),
|
||||
role=CourseSessionUser.Role.EXPERT,
|
||||
)
|
||||
circle = Circle.objects.get(slug="test-lehrgang-lp-circle-fahrzeug")
|
||||
|
||||
self.students = [
|
||||
self.student,
|
||||
User.objects.get(username="test-student2@example.com"),
|
||||
User.objects.get(username="test-student3@example.com"),
|
||||
]
|
||||
|
||||
for i in range(3):
|
||||
FeedbackResponseFactory(
|
||||
circle=circle,
|
||||
course_session=csu.course_session,
|
||||
circle=self.circle_basis,
|
||||
course_session=self.course_session,
|
||||
data={
|
||||
"satisfaction": feedback_data["satisfaction"][i],
|
||||
"goal_attainment": feedback_data["goal_attainment"][i],
|
||||
"proficiency": feedback_data["proficiency"][i],
|
||||
"preparation_task_clarity": feedback_data[
|
||||
"satisfaction": self.feedback_data["satisfaction"][i],
|
||||
"goal_attainment": self.feedback_data["goal_attainment"][i],
|
||||
"proficiency": self.feedback_data["proficiency"][i],
|
||||
"preparation_task_clarity": self.feedback_data[
|
||||
"preparation_task_clarity"
|
||||
][i],
|
||||
"instructor_competence": feedback_data["instructor_competence"][i],
|
||||
"instructor_open_feedback": feedback_data[
|
||||
"instructor_competence": self.feedback_data[
|
||||
"instructor_competence"
|
||||
][i],
|
||||
"instructor_open_feedback": self.feedback_data[
|
||||
"instructor_open_feedback"
|
||||
][i],
|
||||
"would_recommend": feedback_data["would_recommend"][i],
|
||||
"instructor_respect": feedback_data["instructor_respect"][i],
|
||||
"course_positive_feedback": feedback_data[
|
||||
"would_recommend": self.feedback_data["would_recommend"][i],
|
||||
"instructor_respect": self.feedback_data["instructor_respect"][i],
|
||||
"course_positive_feedback": self.feedback_data[
|
||||
"course_positive_feedback"
|
||||
][i],
|
||||
"course_negative_feedback": feedback_data[
|
||||
"course_negative_feedback": self.feedback_data[
|
||||
"course_negative_feedback"
|
||||
][i],
|
||||
},
|
||||
).save()
|
||||
feedback_user=self.students[i],
|
||||
submitted=True,
|
||||
)
|
||||
|
||||
def test_detail_trainer_can_fetch_feedback(self):
|
||||
self.client.force_login(self.trainer)
|
||||
response = self.client.get(
|
||||
f"/api/core/feedback/{csu.course_session.id}/{circle.id}/"
|
||||
f"/api/core/feedback/{self.course_session.id}/{self.circle_basis.id}/"
|
||||
)
|
||||
self.maxDiff = None
|
||||
|
||||
expected = {
|
||||
"amount": 3,
|
||||
"questions": feedback_data,
|
||||
"questions": self.feedback_data,
|
||||
}
|
||||
print(response.data)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertDictEqual(response.data, expected)
|
||||
|
||||
def test_cannot_receive_feedback_from_other_circle(self):
|
||||
csu = CourseSessionUser.objects.get(
|
||||
course_session=self.course_session,
|
||||
user=User.objects.get(username="patrizia.huggel@eiger-versicherungen.ch"),
|
||||
role=CourseSessionUser.Role.EXPERT,
|
||||
)
|
||||
|
||||
circle = Circle.objects.get(slug="test-lehrgang-lp-circle-reisen")
|
||||
FeedbackResponseFactory(circle=circle, course_session=csu.course_session).save()
|
||||
|
||||
def test_summary_trainer_can_fetch_feedback(self):
|
||||
self.client.force_login(self.trainer)
|
||||
response = self.client.get(
|
||||
f"/api/core/feedback/{csu.course_session.id}/{circle.id}/"
|
||||
f"/api/core/feedback/{self.course_session.id}/summary/"
|
||||
)
|
||||
self.maxDiff = None
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data, {"amount": 0, "questions": {}})
|
||||
|
||||
def test_student_cannot_receive_feedback(self):
|
||||
self.client.login(username="student", password="test")
|
||||
csu = CourseSessionUser.objects.get(
|
||||
course_session=self.course_session,
|
||||
user=self.user,
|
||||
self.assertDictEqual(
|
||||
response.data[0], {"circle_id": self.circle_basis.id, "count": 3}
|
||||
)
|
||||
circle = Circle.objects.get(slug="test-lehrgang-lp-circle-fahrzeug")
|
||||
FeedbackResponseFactory(circle=circle, course_session=csu.course_session).save()
|
||||
|
||||
def test_detail_student_cannot_fetch_feedback(self):
|
||||
self.client.force_login(self.student)
|
||||
response = self.client.get(
|
||||
f"/api/core/feedback/{csu.course_session.id}/{circle.id}/"
|
||||
f"/api/core/feedback/{self.course_session.id}/{self.circle_basis.id}/"
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data, {"amount": 0, "questions": {}})
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_summary_student_cannot_fetch_feedback(self):
|
||||
self.client.force_login(self.student)
|
||||
response = self.client.get(
|
||||
f"/api/core/feedback/{self.course_session.id}/summary/"
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@ import itertools
|
|||
|
||||
import structlog
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework.response import Response
|
||||
|
||||
from vbv_lernwelt.course.permissions import is_course_session_expert
|
||||
from vbv_lernwelt.feedback.models import FeedbackResponse
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
|
@ -24,8 +26,12 @@ FEEDBACK_FIELDS = [
|
|||
|
||||
@api_view(["GET"])
|
||||
def get_expert_feedbacks_for_course(request, course_session_id):
|
||||
if not is_course_session_expert(request.user, course_session_id):
|
||||
raise PermissionDenied()
|
||||
|
||||
feedbacks = FeedbackResponse.objects.filter(
|
||||
course_session__id=course_session_id, circle__expert__user=request.user
|
||||
course_session__id=course_session_id,
|
||||
submitted=True,
|
||||
).order_by("circle_id")
|
||||
circle_count = []
|
||||
|
||||
|
|
@ -44,9 +50,12 @@ def get_expert_feedbacks_for_course(request, course_session_id):
|
|||
|
||||
@api_view(["GET"])
|
||||
def get_feedback_for_circle(request, course_session_id, circle_id):
|
||||
if not is_course_session_expert(request.user, course_session_id):
|
||||
raise PermissionDenied()
|
||||
|
||||
feedbacks = FeedbackResponse.objects.filter(
|
||||
course_session__id=course_session_id,
|
||||
circle__expert__user=request.user,
|
||||
submitted=True,
|
||||
circle_id=circle_id,
|
||||
).order_by("created_at")
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
# Generated by Django 3.2.20 on 2023-09-27 13:49
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("learnpath", "0005_alter_learningcontentedoniqtest_content_assignment"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="learningcontentfeedback",
|
||||
name="can_user_self_toggle_course_completion",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
|
|
@ -314,7 +314,7 @@ class LearningContentPlaceholder(LearningContent):
|
|||
class LearningContentFeedback(LearningContent):
|
||||
parent_page_types = ["learnpath.Circle"]
|
||||
subpage_types = []
|
||||
can_user_self_toggle_course_completion = models.BooleanField(default=True)
|
||||
can_user_self_toggle_course_completion = models.BooleanField(default=False)
|
||||
|
||||
|
||||
class LearningContentLearningModule(LearningContent):
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ class TestAttendanceCourseReminders(TestCase):
|
|||
|
||||
attendance_course_reminder_notification_job()
|
||||
|
||||
self.assertEquals(3, len(Notification.objects.all()))
|
||||
self.assertEquals(4, len(Notification.objects.all()))
|
||||
notification = Notification.objects.get(
|
||||
recipient__username="test-student1@example.com"
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in New Issue