Merged in feature/VBV-525-feedback-improvements (pull request #208)

Feature/VBV-525 feedback improvements

Approved-by: Christian Cueni
This commit is contained in:
Daniel Egger 2023-09-28 06:48:39 +00:00
commit 05f111eada
45 changed files with 1122 additions and 599 deletions

View File

@ -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>

View File

@ -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>

View File

@ -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"

View File

@ -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>

View File

@ -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>

View File

@ -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;

View File

@ -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] ?? {};

View File

@ -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>;

View File

@ -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
}

View File

@ -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";

View File

@ -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>

View File

@ -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>

View File

@ -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";

View File

@ -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>

View File

@ -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>

View File

@ -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">

View File

@ -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')"

View File

@ -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')"

View File

@ -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"

View File

@ -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: {

View File

@ -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,
});
}
);
});
});

View File

@ -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");
});
});

View File

@ -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];

View File

@ -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

View File

@ -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",

View File

@ -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",
},
)

View File

@ -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")

View File

@ -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():

View File

@ -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

View File

@ -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=[])

View File

@ -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()

View File

@ -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]),

View File

@ -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)
feedback_response = FeedbackResponse.objects.create(
circle=circle,
course_session=course_session,
data=serializer.validated_data,
logger.error(
"creating feedback serializer invalid",
error_list=serializer.errors,
label="feedback",
)
logger.info(feedback_response)
errors = [
ErrorType(field=field, messages=msgs)
for field, msgs in serializer.errors.items()
]
return SendFeedbackMutation(errors=errors)
return SendFeedback(feedback_response=feedback_response)
feedback_response = update_feedback_response(
feedback_user=info.context.user,
course_session=course_session,
learning_content_feedback_page=learning_content,
submitted=submitted,
validated_data=serializer.validated_data,
)
return SendFeedbackMutation(feedback_response=feedback_response)
class FeedbackMutation(object):
send_feedback = SendFeedback.Field()
send_feedback = SendFeedbackMutation.Field()

View File

@ -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",
]

View File

@ -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,
),
]

View File

@ -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),
),
]

View File

@ -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),
]

View File

@ -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)

View File

@ -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__"

View File

@ -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

View File

@ -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)

View File

@ -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")

View File

@ -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),
),
]

View File

@ -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):

View File

@ -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"
)