Merged in feature/feedback-form-2022-12-29 (pull request #18)
Feature/feedback form 2022 12 29
This commit is contained in:
commit
6b343805a0
|
|
@ -0,0 +1,17 @@
|
||||||
|
import type { CodegenConfig } from "@graphql-codegen/cli";
|
||||||
|
const config: CodegenConfig = {
|
||||||
|
schema: "../server/schema.graphql",
|
||||||
|
documents: ["src/**/*.vue"],
|
||||||
|
ignoreNoDocuments: true,
|
||||||
|
generates: {
|
||||||
|
"./src/gql/": {
|
||||||
|
preset: "client",
|
||||||
|
config: {
|
||||||
|
useTypeImports: true,
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -5,6 +5,7 @@
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vue-tsc --noEmit && vite build && node versionize && cp ./dist/index.html ../server/vbv_lernwelt/templates/vue/index.html && cp -r ./dist/static/vue ../server/vbv_lernwelt/static/",
|
"build": "vue-tsc --noEmit && vite build && node versionize && cp ./dist/index.html ../server/vbv_lernwelt/templates/vue/index.html && cp -r ./dist/static/vue ../server/vbv_lernwelt/static/",
|
||||||
"build:tailwind": "tailwindcss -i tailwind.css -o ../server/vbv_lernwelt/static/css/tailwind.css --minify",
|
"build:tailwind": "tailwindcss -i tailwind.css -o ../server/vbv_lernwelt/static/css/tailwind.css --minify",
|
||||||
|
"codegen": "graphql-codegen",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"coverage": "vitest run --coverage",
|
"coverage": "vitest run --coverage",
|
||||||
"typecheck": "vue-tsc --noEmit -p tsconfig.app.json --composite false",
|
"typecheck": "vue-tsc --noEmit -p tsconfig.app.json --composite false",
|
||||||
|
|
@ -14,6 +15,7 @@
|
||||||
"tailwind": "tailwindcss -i tailwind.css -o ../server/vbv_lernwelt/static/css/tailwind.css --watch"
|
"tailwind": "tailwindcss -i tailwind.css -o ../server/vbv_lernwelt/static/css/tailwind.css --watch"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@headlessui/tailwindcss": "^0.1.2",
|
||||||
"@headlessui/vue": "^1.6.7",
|
"@headlessui/vue": "^1.6.7",
|
||||||
"@sentry/tracing": "^7.20.0",
|
"@sentry/tracing": "^7.20.0",
|
||||||
"@sentry/vue": "^7.20.0",
|
"@sentry/vue": "^7.20.0",
|
||||||
|
|
@ -28,6 +30,8 @@
|
||||||
"vue-router": "^4.1.5"
|
"vue-router": "^4.1.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@graphql-codegen/cli": "^2.13.12",
|
||||||
|
"@graphql-codegen/client-preset": "^1.1.4",
|
||||||
"@rollup/plugin-alias": "^3.1.9",
|
"@rollup/plugin-alias": "^3.1.9",
|
||||||
"@rushstack/eslint-patch": "^1.1.4",
|
"@rushstack/eslint-patch": "^1.1.4",
|
||||||
"@tailwindcss/forms": "^0.5.2",
|
"@tailwindcss/forms": "^0.5.2",
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import * as log from "loglevel";
|
import log from "loglevel";
|
||||||
|
|
||||||
import AppFooter from "@/components/AppFooter.vue";
|
import AppFooter from "@/components/AppFooter.vue";
|
||||||
import MainNavigationBar from "@/components/MainNavigationBar.vue";
|
import MainNavigationBar from "@/components/MainNavigationBar.vue";
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,217 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<p class="text-gray-800 mb-4">Schritt {{ stepNo + 1 }} von {{ MAX_STEPS }}</p>
|
||||||
|
<p class="mb-10 text-xl">
|
||||||
|
Patrizia Huggel, deine Trainerin, bittet dich, ihr Feedback zu geben.
|
||||||
|
<br />
|
||||||
|
Das ist freiwillig, würde ihr aber helfen, dein Lernerlebnis zu verbessern.
|
||||||
|
</p>
|
||||||
|
<ItRadioGroup
|
||||||
|
v-if="stepNo === 0"
|
||||||
|
v-model="wouldRecommend"
|
||||||
|
label="Würden Sie den Kurs weiterempfehlen?"
|
||||||
|
class="mb-8"
|
||||||
|
:items="YES_NO"
|
||||||
|
/>
|
||||||
|
<ItRadioGroup
|
||||||
|
v-if="stepNo === 1"
|
||||||
|
v-model="satisfaction"
|
||||||
|
label="Zufriedenheit insgesamt"
|
||||||
|
class="mb-8"
|
||||||
|
:items="RATINGS"
|
||||||
|
/>
|
||||||
|
<ItRadioGroup
|
||||||
|
v-if="stepNo === 2"
|
||||||
|
v-model="goalAttainment"
|
||||||
|
label="Zielerreichung insgesamt"
|
||||||
|
class="mb-8"
|
||||||
|
:items="RATINGS"
|
||||||
|
/>
|
||||||
|
<ItRadioGroup
|
||||||
|
v-if="stepNo === 3"
|
||||||
|
v-model="proficiency"
|
||||||
|
label="Wie beurteilen Sie Ihre Sicherheit bezüglichen den Themen nach dem Kurs?"
|
||||||
|
class="mb-8"
|
||||||
|
:items="PERCENTAGES"
|
||||||
|
/>
|
||||||
|
<ItRadioGroup
|
||||||
|
v-if="stepNo === 4"
|
||||||
|
v-model="receivedMaterials"
|
||||||
|
label="Haben Sie Vorbereitungsunterlagen (z.B. eLearning) erhalten?"
|
||||||
|
class="mb-8"
|
||||||
|
:items="YES_NO"
|
||||||
|
/>
|
||||||
|
<ItRadioGroup
|
||||||
|
v-if="stepNo === 4 && receivedMaterials"
|
||||||
|
v-model="materialsRating"
|
||||||
|
label="Falls ja: Wie beurteilen Sie die Vorbereitungsunterlagen (z.B.
|
||||||
|
eLearning)?"
|
||||||
|
class="mb-8"
|
||||||
|
:items="RATINGS"
|
||||||
|
/>
|
||||||
|
<ItRadioGroup
|
||||||
|
v-if="stepNo === 5"
|
||||||
|
v-model="instructorCompetence"
|
||||||
|
label="Der Kursleiter war themenstark, fachkompetent."
|
||||||
|
class="mb-8"
|
||||||
|
:items="RATINGS"
|
||||||
|
/>
|
||||||
|
<ItRadioGroup
|
||||||
|
v-if="stepNo === 6"
|
||||||
|
v-model="instructorRespect"
|
||||||
|
class="mb-8"
|
||||||
|
label="Fragen und Anregungen der Kursteilnehmenden wurden ernst
|
||||||
|
genommen u. aufgegriffen."
|
||||||
|
:items="RATINGS"
|
||||||
|
/>
|
||||||
|
<ItTextarea
|
||||||
|
v-if="stepNo === 7"
|
||||||
|
v-model="instructorOpenFeedback"
|
||||||
|
:label="instructorOpenFeedbackLabel"
|
||||||
|
class="mb-8"
|
||||||
|
/>
|
||||||
|
<ItTextarea
|
||||||
|
v-if="stepNo === 8"
|
||||||
|
v-model="courseNegativeFeedback"
|
||||||
|
:label="courseNegativeFeedbackLabel"
|
||||||
|
class="mb-8"
|
||||||
|
/>
|
||||||
|
<ItTextarea
|
||||||
|
v-if="stepNo === 9"
|
||||||
|
v-model="coursePositiveFeedback"
|
||||||
|
:label="coursePositiveFeedbackLabel"
|
||||||
|
class="mb-8"
|
||||||
|
/>
|
||||||
|
<button class="btn-blue" @click="sendFeedback">Senden und abschliessen</button>
|
||||||
|
<button class="btn-blue mr-4" :disabled="stepNo <= 0" @click="previousStep">
|
||||||
|
Zurück
|
||||||
|
</button>
|
||||||
|
<button class="btn-blue" :disabled="stepNo >= MAX_STEPS - 1" @click="nextStep">
|
||||||
|
Weiter
|
||||||
|
</button>
|
||||||
|
<hr class="mb-10 mt-10" />
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
PERCENTAGES,
|
||||||
|
RATINGS,
|
||||||
|
YES_NO,
|
||||||
|
} from "@/components/learningPath/feedback.constants";
|
||||||
|
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 { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||||
|
import type { LearningContent } from "@/types";
|
||||||
|
import { useMutation } from "@urql/vue";
|
||||||
|
import log from "loglevel";
|
||||||
|
import { onMounted, reactive, ref } from "vue";
|
||||||
|
|
||||||
|
const props = defineProps<{ page: LearningContent }>();
|
||||||
|
const courseSessionsStore = useCourseSessionsStore();
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
log.debug("Feedback mounted");
|
||||||
|
});
|
||||||
|
|
||||||
|
const stepNo = ref(0);
|
||||||
|
const MAX_STEPS = 10;
|
||||||
|
|
||||||
|
const instructorOpenFeedbackLabel = "Was ich dem Kursleiter sonst noch sagen wollte:";
|
||||||
|
const courseNegativeFeedbackLabel = "Wo sehen Sie Verbesserungspotenzial?";
|
||||||
|
const coursePositiveFeedbackLabel = "Was hat Ihnen besonders gut gefallen?";
|
||||||
|
|
||||||
|
const sendFeedbackMutation = graphql(`
|
||||||
|
mutation SendFeedbackMutation($input: SendFeedbackInput!) {
|
||||||
|
sendFeedback(input: $input) {
|
||||||
|
id
|
||||||
|
satisfaction
|
||||||
|
goalAttainment
|
||||||
|
proficiency
|
||||||
|
receivedMaterials
|
||||||
|
materialsRating
|
||||||
|
errors {
|
||||||
|
field
|
||||||
|
messages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
const { executeMutation } = useMutation(sendFeedbackMutation);
|
||||||
|
|
||||||
|
const satisfaction = ref(null);
|
||||||
|
const goalAttainment = ref(null);
|
||||||
|
const wouldRecommend = ref(null);
|
||||||
|
const proficiency = ref(null);
|
||||||
|
const receivedMaterials = ref(null);
|
||||||
|
const materialsRating = ref(null);
|
||||||
|
const instructorCompetence = ref(null);
|
||||||
|
const instructorRespect = ref(null);
|
||||||
|
const coursePositiveFeedback = ref("");
|
||||||
|
const courseNegativeFeedback = ref("");
|
||||||
|
const instructorOpenFeedback = ref("");
|
||||||
|
|
||||||
|
const mutationResult = ref<any>(null);
|
||||||
|
|
||||||
|
const previousStep = () => {
|
||||||
|
if (stepNo.value > 0) {
|
||||||
|
stepNo.value -= 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const nextStep = () => {
|
||||||
|
if (stepNo.value < MAX_STEPS - 1) {
|
||||||
|
stepNo.value += 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendFeedback = () => {
|
||||||
|
log.info("sending feedback");
|
||||||
|
const courseSession = courseSessionsStore.courseSessionForRoute;
|
||||||
|
if (!courseSession || !courseSession.id) {
|
||||||
|
log.error("no course session set");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const input: SendFeedbackInput = reactive({
|
||||||
|
materialsRating,
|
||||||
|
courseNegativeFeedback,
|
||||||
|
coursePositiveFeedback,
|
||||||
|
goalAttainment,
|
||||||
|
instructorCompetence,
|
||||||
|
instructorRespect,
|
||||||
|
instructorOpenFeedback,
|
||||||
|
satisfaction,
|
||||||
|
proficiency,
|
||||||
|
receivedMaterials,
|
||||||
|
wouldRecommend,
|
||||||
|
page: props.page.translation_key,
|
||||||
|
courseSession: courseSession.id,
|
||||||
|
});
|
||||||
|
const variables = reactive({
|
||||||
|
input,
|
||||||
|
});
|
||||||
|
log.debug(variables);
|
||||||
|
executeMutation(variables)
|
||||||
|
.then(({ data, error }) => {
|
||||||
|
log.debug(data);
|
||||||
|
log.error(error);
|
||||||
|
mutationResult.value = data;
|
||||||
|
})
|
||||||
|
.catch((e) => log.error(e));
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import * as log from "loglevel";
|
import log from "loglevel";
|
||||||
|
|
||||||
import IconLogout from "@/components/icons/IconLogout.vue";
|
import IconLogout from "@/components/icons/IconLogout.vue";
|
||||||
import IconSettings from "@/components/icons/IconSettings.vue";
|
import IconSettings from "@/components/icons/IconSettings.vue";
|
||||||
|
|
@ -78,7 +78,8 @@ function logout() {
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
log.debug("MainNavigationBar mounted");
|
log.debug("MainNavigationBar mounted");
|
||||||
if (userStore.loggedIn) {
|
if (userStore.loggedIn) {
|
||||||
courseSessionsStore.loadCourseSessionsData();
|
// fixme: only when i'm logged in? should this be handled in the store?
|
||||||
|
// courseSessionsStore.loadCourseSessionsData();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,16 @@ import { useCircleStore } from "@/stores/circle";
|
||||||
import type { LearningContent } from "@/types";
|
import type { LearningContent } from "@/types";
|
||||||
import * as log from "loglevel";
|
import * as log from "loglevel";
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
import { useRoute } from "vue-router";
|
|
||||||
|
import DescriptionBlock from "@/components/learningPath/blocks/DescriptionBlock.vue";
|
||||||
|
import DescriptionTextBlock from "@/components/learningPath/blocks/DescriptionTextBlock.vue";
|
||||||
|
import FeedbackBlock from "@/components/learningPath/blocks/FeedbackBlock.vue";
|
||||||
|
import IframeBlock from "@/components/learningPath/blocks/IframeBlock.vue";
|
||||||
|
import PlaceholderBlock from "@/components/learningPath/blocks/PlaceholderBlock.vue";
|
||||||
|
import VideoBlock from "@/components/learningPath/blocks/VideoBlock.vue";
|
||||||
|
|
||||||
log.debug("LearningContent.vue setup");
|
log.debug("LearningContent.vue setup");
|
||||||
|
|
||||||
const route = useRoute();
|
|
||||||
|
|
||||||
const circleStore = useCircleStore();
|
const circleStore = useCircleStore();
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
|
@ -23,71 +27,46 @@ const block = computed(() => {
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// can't use the type as component name, as some are reserved HTML components, e.g. video
|
||||||
|
const COMPONENTS: Record<string, any> = {
|
||||||
|
// todo: can we find a better type here than any? ^
|
||||||
|
placeholder: PlaceholderBlock,
|
||||||
|
video: VideoBlock,
|
||||||
|
assignment: DescriptionTextBlock,
|
||||||
|
resource: DescriptionTextBlock,
|
||||||
|
exercise: IframeBlock,
|
||||||
|
test: IframeBlock,
|
||||||
|
learningmodule: IframeBlock,
|
||||||
|
feedback: FeedbackBlock,
|
||||||
|
};
|
||||||
|
const DEFAULT_BLOCK = DescriptionBlock;
|
||||||
|
|
||||||
|
const component = computed(() => {
|
||||||
|
if (block.value) {
|
||||||
|
return COMPONENTS[block.value.type] || DEFAULT_BLOCK;
|
||||||
|
}
|
||||||
|
return DEFAULT_BLOCK;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="block">
|
<LearningContentContainer
|
||||||
<LearningContentContainer
|
v-if="block"
|
||||||
:title="learningContent.title"
|
:title="learningContent.title"
|
||||||
:next-button-text="$t('learningContent.completeAndContinue')"
|
:next-button-text="$t('learningContent.completeAndContinue')"
|
||||||
:exit-text="$t('general.backToCircle')"
|
:exit-text="$t('general.backToCircle')"
|
||||||
@exit="circleStore.closeLearningContent(props.learningContent)"
|
@exit="circleStore.closeLearningContent(props.learningContent)"
|
||||||
@next="circleStore.continueFromLearningContent(props.learningContent)"
|
@next="circleStore.continueFromLearningContent(props.learningContent)"
|
||||||
>
|
>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div
|
<component
|
||||||
v-if="
|
:is="component"
|
||||||
block.type === 'exercise' ||
|
:value="block.value"
|
||||||
block.type === 'test' ||
|
:content="learningContent"
|
||||||
block.type === 'learningmodule'
|
></component>
|
||||||
"
|
</div>
|
||||||
class="h-screen"
|
</LearningContentContainer>
|
||||||
>
|
|
||||||
<iframe width="100%" height="100%" scrolling="no" :src="block.value.url" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else class="container-medium">
|
|
||||||
<div v-if="block.type === 'video'">
|
|
||||||
<iframe
|
|
||||||
class="mt-8 w-full aspect-video"
|
|
||||||
:src="block.value.url"
|
|
||||||
:title="learningContent.title"
|
|
||||||
frameborder="0"
|
|
||||||
allow="accelerometer; encrypted-media; gyroscope; picture-in-picture"
|
|
||||||
allowfullscreen
|
|
||||||
></iframe>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else-if="block.type === 'media_library'" class="mt-4 lg:mt-12">
|
|
||||||
<h1>{{ learningContent.title }}</h1>
|
|
||||||
|
|
||||||
<p class="text-large my-4 lg:my-8">{{ block.value.description }}</p>
|
|
||||||
<router-link
|
|
||||||
:to="`${block.value.url}?back=${route.path}`"
|
|
||||||
class="button btn-primary"
|
|
||||||
>
|
|
||||||
Mediathek öffnen
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-else-if="block.type === 'resource' || block.type === 'assignment'"
|
|
||||||
class="mt-4 lg:mt-12"
|
|
||||||
>
|
|
||||||
<p class="text-large my-4">{{ block.value.description }}</p>
|
|
||||||
<div class="resource-text" v-html="block.value.text"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else-if="block.type === 'placeholder'" class="mt-4 lg:mt-12">
|
|
||||||
<p class="text-large my-4">{{ block.value.description }}</p>
|
|
||||||
<h1>{{ learningContent.title }}</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else class="text-large my-4">{{ block.value.description }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</LearningContentContainer>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,14 @@ import * as log from "loglevel";
|
||||||
|
|
||||||
log.debug("LeariningContentContainer.vue setup");
|
log.debug("LeariningContentContainer.vue setup");
|
||||||
|
|
||||||
const props = defineProps<{
|
defineProps<{
|
||||||
exitText: string;
|
exitText: string;
|
||||||
title: string;
|
title: string;
|
||||||
nextButtonText: string;
|
nextButtonText: string;
|
||||||
showBackButton?: boolean;
|
showBackButton?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits(["back", "next", "exit"]);
|
defineEmits(["back", "next", "exit"]);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
|
||||||
|
|
@ -126,9 +126,9 @@ const learningSequenceBorderClass = computed(() => {
|
||||||
</div>
|
</div>
|
||||||
<ItCheckbox
|
<ItCheckbox
|
||||||
v-else
|
v-else
|
||||||
:model-value="learningContent.completion_status === 'success'"
|
:checked="learningContent.completion_status === 'success'"
|
||||||
:on-toggle="() => toggleCompleted(learningContent)"
|
|
||||||
:data-cy="`${learningContent.slug}-checkbox`"
|
:data-cy="`${learningContent.slug}-checkbox`"
|
||||||
|
@toggle="toggleCompleted(learningContent)"
|
||||||
/>
|
/>
|
||||||
<div class="flex-auto pt-1 sm:pt-0">
|
<div class="flex-auto pt-1 sm:pt-0">
|
||||||
<span class="flex gap-4 items-center xl:h-10">
|
<span class="flex gap-4 items-center xl:h-10">
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import type { LearningUnit } from "@/types";
|
||||||
import * as log from "loglevel";
|
import * as log from "loglevel";
|
||||||
|
|
||||||
import LearningContentContainer from "@/components/learningPath/LearningContentContainer.vue";
|
import LearningContentContainer from "@/components/learningPath/LearningContentContainer.vue";
|
||||||
|
import { COMPLETION_FAILURE, COMPLETION_SUCCESS } from "@/constants";
|
||||||
import { computed, reactive } from "vue";
|
import { computed, reactive } from "vue";
|
||||||
|
|
||||||
log.debug("LearningContent.vue setup");
|
log.debug("LearningContent.vue setup");
|
||||||
|
|
@ -83,7 +84,7 @@ function handleBack() {
|
||||||
'border-2': currentQuestion.completion_status === 'success',
|
'border-2': currentQuestion.completion_status === 'success',
|
||||||
}"
|
}"
|
||||||
data-cy="success"
|
data-cy="success"
|
||||||
@click="circleStore.markCompletion(currentQuestion, 'success')"
|
@click="circleStore.markCompletion(currentQuestion, COMPLETION_SUCCESS)"
|
||||||
>
|
>
|
||||||
<it-icon-smiley-happy class="w-16 h-16 mr-4"></it-icon-smiley-happy>
|
<it-icon-smiley-happy class="w-16 h-16 mr-4"></it-icon-smiley-happy>
|
||||||
<span class="font-bold text-large">{{ $t("selfEvaluation.yes") }}.</span>
|
<span class="font-bold text-large">{{ $t("selfEvaluation.yes") }}.</span>
|
||||||
|
|
@ -91,8 +92,9 @@ function handleBack() {
|
||||||
<button
|
<button
|
||||||
class="flex-1 inline-flex items-center text-left p-4 border"
|
class="flex-1 inline-flex items-center text-left p-4 border"
|
||||||
:class="{
|
:class="{
|
||||||
'border-orange-500': currentQuestion.completion_status === 'fail',
|
'border-orange-500':
|
||||||
'border-2': currentQuestion.completion_status === 'fail',
|
currentQuestion.completion_status === COMPLETION_FAILURE,
|
||||||
|
'border-2': currentQuestion.completion_status === COMPLETION_FAILURE,
|
||||||
}"
|
}"
|
||||||
data-cy="fail"
|
data-cy="fail"
|
||||||
@click="circleStore.markCompletion(currentQuestion, 'fail')"
|
@click="circleStore.markCompletion(currentQuestion, 'fail')"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
<template>
|
||||||
|
<div class="container-medium">
|
||||||
|
<div class="mt-4 lg:mt-12">
|
||||||
|
<p class="text-large my-4">{{ value.description }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { LearningContent } from "@/types";
|
||||||
|
|
||||||
|
interface Value {
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
value: Value;
|
||||||
|
content: LearningContent;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
<template>
|
||||||
|
<div class="container-medium">
|
||||||
|
<div class="mt-4 lg:mt-12">
|
||||||
|
<p class="text-large my-4">{{ value.description }}</p>
|
||||||
|
<div class="resource-text" v-html="value.text"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { LearningContent } from "@/types";
|
||||||
|
|
||||||
|
interface Value {
|
||||||
|
description: string;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
value: Value;
|
||||||
|
content: LearningContent;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<template>
|
||||||
|
<div class="container-medium">
|
||||||
|
<div class="mt-4 lg:mt-12">
|
||||||
|
<FeedbackForm :page="content" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import FeedbackForm from "@/components/FeedbackForm.vue";
|
||||||
|
import type { LearningContent } from "@/types";
|
||||||
|
|
||||||
|
interface Value {
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
value: Value;
|
||||||
|
content: LearningContent;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
<template>
|
||||||
|
<div class="h-screen">
|
||||||
|
<iframe width="100%" height="100%" scrolling="no" :src="value.url" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { LearningContent } from "@/types";
|
||||||
|
|
||||||
|
interface Value {
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
value: Value;
|
||||||
|
content: LearningContent;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
<template>
|
||||||
|
<div class="container-medium">
|
||||||
|
<div class="mt-4 lg:mt-12">
|
||||||
|
<h1>{{ content.title }}</h1>
|
||||||
|
|
||||||
|
<p class="text-large my-4 lg:my-8">{{ value.description }}</p>
|
||||||
|
<router-link :to="`${value.url}?back=${route.path}`" class="button btn-primary">
|
||||||
|
Mediathek öffnen
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { LearningContent } from "@/types";
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
interface Value {
|
||||||
|
description: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
value: Value;
|
||||||
|
content: LearningContent;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<template>
|
||||||
|
<div class="container-medium">
|
||||||
|
<div class="mt-4 lg:mt-12">
|
||||||
|
<p class="text-large my-4">{{ value.description }}</p>
|
||||||
|
<h1>{{ content.title }}</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { LearningContent } from "@/types";
|
||||||
|
|
||||||
|
interface Value {
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
value: Value;
|
||||||
|
content: LearningContent;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
<template>
|
||||||
|
<div class="container-medium">
|
||||||
|
<iframe
|
||||||
|
class="mt-8 w-full aspect-video"
|
||||||
|
:src="value.url"
|
||||||
|
:title="content.title"
|
||||||
|
frameborder="0"
|
||||||
|
allow="accelerometer; encrypted-media; gyroscope; picture-in-picture"
|
||||||
|
allowfullscreen
|
||||||
|
></iframe>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { LearningContent } from "@/types";
|
||||||
|
|
||||||
|
interface Value {
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
value: Value;
|
||||||
|
content: LearningContent;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
import type { RadioItem } from "@/components/learningPath/feedback.types";
|
||||||
|
|
||||||
|
export const YES_NO: RadioItem<boolean>[] = [
|
||||||
|
{
|
||||||
|
name: "Ja",
|
||||||
|
value: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Nein",
|
||||||
|
value: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
export const RATINGS: RadioItem<number>[] = [
|
||||||
|
{
|
||||||
|
name: "sehr unzufrieden",
|
||||||
|
value: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unzufrieden",
|
||||||
|
value: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zufrieden",
|
||||||
|
value: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "sehr zufrieden",
|
||||||
|
value: 4,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
export const PERCENTAGES: RadioItem<number>[] = [
|
||||||
|
{
|
||||||
|
name: "20%",
|
||||||
|
value: 20,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "40%",
|
||||||
|
value: 40,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "60%",
|
||||||
|
value: 60,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "80%",
|
||||||
|
value: 80,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "100%",
|
||||||
|
value: 100,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
export interface RadioItem<T> {
|
||||||
|
value: T;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
@ -1,19 +1,34 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import log from "loglevel";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
modelValue?: boolean;
|
checked?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
onToggle?: () => void;
|
label?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
modelValue: false,
|
checked: false,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
onToggle: () => {
|
label: undefined,
|
||||||
// do nothing
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
defineEmits(["update:modelValue"]);
|
const emit = defineEmits(["toggle"]);
|
||||||
|
const toggle = () => {
|
||||||
|
emit("toggle");
|
||||||
|
};
|
||||||
|
const keydown = (e: KeyboardEvent) => {
|
||||||
|
log.debug("keydown", e.type, e.key);
|
||||||
|
if (e.key === " " && !props.disabled) {
|
||||||
|
e.preventDefault();
|
||||||
|
toggle();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const input = (e: Event) => {
|
||||||
|
const target = e.target as HTMLInputElement;
|
||||||
|
log.debug("input", e.type, target.checked, target.value);
|
||||||
|
emit("toggle");
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -22,18 +37,32 @@ defineEmits(["update:modelValue"]);
|
||||||
'opacity-50': disabled,
|
'opacity-50': disabled,
|
||||||
'cursor-not-allowed': disabled,
|
'cursor-not-allowed': disabled,
|
||||||
}"
|
}"
|
||||||
class="w-8 h-8 cursor-pointer"
|
class="cursor-pointer inline-flex"
|
||||||
@click="$emit('update:modelValue', !modelValue)"
|
|
||||||
>
|
>
|
||||||
<button
|
<label
|
||||||
v-if="modelValue"
|
class="block bg-contain disabled:opacity-50 cy-checkbox cy-checkbox-checked bg-no-repeat pl-8 h-8 flex items-center"
|
||||||
class="w-8 h-8 flex-none bg-contain bg-[url('/static/icons/icon-checkbox-checked.svg')] hover:bg-[url('/static/icons/icon-checkbox-checked-hover.svg')] disabled:opacity-50 cy-checkbox cy-checkbox-checked"
|
:class="
|
||||||
@click.stop="props.onToggle()"
|
checked
|
||||||
></button>
|
? 'bg-[url(/static/icons/icon-checkbox-checked.svg)] hover:bg-[url(/static/icons/icon-checkbox-checked-hover.svg)]'
|
||||||
<button
|
: 'bg-[url(/static/icons/icon-checkbox-unchecked.svg)] hover:bg-[url(/static/icons/icon-checkbox-unchecked-hover.svg)]'
|
||||||
v-else
|
"
|
||||||
class="w-8 h-8 flex-none bg-contain bg-[url('/static/icons/icon-checkbox-unchecked.svg')] hover:bg-[url('/static/icons/icon-checkbox-unchecked-hover.svg')] cy-checkbox cy-checkbox-unchecked"
|
tabindex="0"
|
||||||
@click.stop="props.onToggle()"
|
@keydown.stop="keydown"
|
||||||
></button>
|
>
|
||||||
|
<input
|
||||||
|
ref="checkbox"
|
||||||
|
:checked="checked"
|
||||||
|
:value="checked"
|
||||||
|
:disabled="disabled"
|
||||||
|
class="sr-only"
|
||||||
|
type="checkbox"
|
||||||
|
@keydown="keydown"
|
||||||
|
@input="input"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span v-if="label" class="ml-4">
|
||||||
|
{{ label }}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
:model-value="modelValue"
|
||||||
|
class="border p-12"
|
||||||
|
@update:modelValue="$emit('update:modelValue', $event)"
|
||||||
|
>
|
||||||
|
<h2 class="text-5xl mb-12 leading-normal font-bold block">{{ label }}</h2>
|
||||||
|
<div class="flex flex-col justify-start align-items-start justify-items-start">
|
||||||
|
<ItCheckbox
|
||||||
|
v-for="item in items"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.name"
|
||||||
|
class="mb-4"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { RadioItem } from "@/components/learningPath/feedback.types";
|
||||||
|
|
||||||
|
import ItCheckbox from "@/components/ui/ItCheckbox.vue";
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
modelValue: any;
|
||||||
|
items: RadioItem<any>[];
|
||||||
|
label: string;
|
||||||
|
}>();
|
||||||
|
defineEmits(["update:modelValue"]);
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
<template>
|
||||||
|
<RadioGroup
|
||||||
|
:model-value="modelValue"
|
||||||
|
class="border p-12"
|
||||||
|
@update:modelValue="$emit('update:modelValue', $event)"
|
||||||
|
>
|
||||||
|
<RadioGroupLabel class="text-5xl mb-12 leading-normal font-bold block">
|
||||||
|
{{ label }}
|
||||||
|
</RadioGroupLabel>
|
||||||
|
<div class="flex justify-between align-items-center justify-items-center space-x-6">
|
||||||
|
<RadioGroupOption
|
||||||
|
v-for="item in items"
|
||||||
|
:key="item.value"
|
||||||
|
as="template"
|
||||||
|
class="flex-1"
|
||||||
|
:value="item.value"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="py-10 text-xl flex-1 text-center cursor-pointer font-bold ui-checked:bg-sky-500 ui-not-checked:border hover:border-gray-500 hover:bg-gray-200"
|
||||||
|
>
|
||||||
|
<RadioGroupLabel as="span">
|
||||||
|
{{ item.name }}
|
||||||
|
</RadioGroupLabel>
|
||||||
|
</div>
|
||||||
|
</RadioGroupOption>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { RadioItem } from "@/components/learningPath/feedback.types";
|
||||||
|
|
||||||
|
import {
|
||||||
|
RadioGroup,
|
||||||
|
// RadioGroupDescription,
|
||||||
|
RadioGroupLabel,
|
||||||
|
RadioGroupOption,
|
||||||
|
} from "@headlessui/vue";
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
modelValue: any;
|
||||||
|
items: RadioItem<any>[];
|
||||||
|
label: string;
|
||||||
|
}>();
|
||||||
|
defineEmits(["update:modelValue"]);
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
<template>
|
||||||
|
<div class="border p-12">
|
||||||
|
<h2 class="text-5xl mb-12 leading-normal font-bold block">{{ label }}</h2>
|
||||||
|
<textarea
|
||||||
|
:value="modelValue"
|
||||||
|
class="w-full border-gray-500 h-40"
|
||||||
|
@input="onInput"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineProps<{
|
||||||
|
modelValue: string;
|
||||||
|
label: string;
|
||||||
|
}>();
|
||||||
|
const emit = defineEmits(["update:modelValue"]);
|
||||||
|
|
||||||
|
const onInput = (event: Event) => {
|
||||||
|
const target = event.target as HTMLInputElement;
|
||||||
|
emit("update:modelValue", target.value);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
import type { CourseCompletionStatus } from "@/types";
|
||||||
|
|
||||||
|
export const COMPLETION_SUCCESS: CourseCompletionStatus = "success";
|
||||||
|
export const COMPLETION_FAILURE: CourseCompletionStatus = "fail";
|
||||||
|
export const COMPLETION_UNKNOWN: CourseCompletionStatus = "unknown";
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
import type { TypedDocumentNode as DocumentNode } from "@graphql-typed-document-node/core";
|
||||||
|
|
||||||
|
export type FragmentType<TDocumentType extends DocumentNode<any, any>> =
|
||||||
|
TDocumentType extends DocumentNode<infer TType, any>
|
||||||
|
? TType extends { " $fragmentName"?: infer TKey }
|
||||||
|
? TKey extends string
|
||||||
|
? { " $fragmentRefs"?: { [key in TKey]: TType } }
|
||||||
|
: never
|
||||||
|
: never
|
||||||
|
: never;
|
||||||
|
|
||||||
|
// return non-nullable if `fragmentType` is non-nullable
|
||||||
|
export function useFragment<TType>(
|
||||||
|
_documentNode: DocumentNode<TType, any>,
|
||||||
|
fragmentType: FragmentType<DocumentNode<TType, any>>
|
||||||
|
): TType;
|
||||||
|
// return nullable if `fragmentType` is nullable
|
||||||
|
export function useFragment<TType>(
|
||||||
|
_documentNode: DocumentNode<TType, any>,
|
||||||
|
fragmentType: FragmentType<DocumentNode<TType, any>> | null | undefined
|
||||||
|
): TType | null | undefined;
|
||||||
|
// return array of non-nullable if `fragmentType` is array of non-nullable
|
||||||
|
export function useFragment<TType>(
|
||||||
|
_documentNode: DocumentNode<TType, any>,
|
||||||
|
fragmentType: ReadonlyArray<FragmentType<DocumentNode<TType, any>>>
|
||||||
|
): ReadonlyArray<TType>;
|
||||||
|
// return array of nullable if `fragmentType` is array of nullable
|
||||||
|
export function useFragment<TType>(
|
||||||
|
_documentNode: DocumentNode<TType, any>,
|
||||||
|
fragmentType: ReadonlyArray<FragmentType<DocumentNode<TType, any>>> | null | undefined
|
||||||
|
): ReadonlyArray<TType> | null | undefined;
|
||||||
|
export function useFragment<TType>(
|
||||||
|
_documentNode: DocumentNode<TType, any>,
|
||||||
|
fragmentType:
|
||||||
|
| FragmentType<DocumentNode<TType, any>>
|
||||||
|
| ReadonlyArray<FragmentType<DocumentNode<TType, any>>>
|
||||||
|
| null
|
||||||
|
| undefined
|
||||||
|
): TType | ReadonlyArray<TType> | null | undefined {
|
||||||
|
return fragmentType as any;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
/* eslint-disable */
|
||||||
|
import type { TypedDocumentNode as DocumentNode } from "@graphql-typed-document-node/core";
|
||||||
|
import * as types from "./graphql";
|
||||||
|
|
||||||
|
const documents = {
|
||||||
|
"\n mutation SendFeedbackMutation($input: SendFeedbackInput!) {\n sendFeedback(input: $input) {\n id\n satisfaction\n goalAttainment\n proficiency\n receivedMaterials\n materialsRating\n errors {\n field\n messages\n }\n }\n }\n":
|
||||||
|
types.SendFeedbackMutationDocument,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function graphql(
|
||||||
|
source: "\n mutation SendFeedbackMutation($input: SendFeedbackInput!) {\n sendFeedback(input: $input) {\n id\n satisfaction\n goalAttainment\n proficiency\n receivedMaterials\n materialsRating\n errors {\n field\n messages\n }\n }\n }\n"
|
||||||
|
): typeof documents["\n mutation SendFeedbackMutation($input: SendFeedbackInput!) {\n sendFeedback(input: $input) {\n id\n satisfaction\n goalAttainment\n proficiency\n receivedMaterials\n materialsRating\n errors {\n field\n messages\n }\n }\n }\n"];
|
||||||
|
|
||||||
|
export function graphql(source: string): unknown;
|
||||||
|
export function graphql(source: string) {
|
||||||
|
return (documents as any)[source] ?? {};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DocumentType<TDocumentNode extends DocumentNode<any, any>> =
|
||||||
|
TDocumentNode extends DocumentNode<infer TType, any> ? TType : never;
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from "./fragment-masking";
|
||||||
|
export * from "./gql";
|
||||||
|
|
@ -18,7 +18,6 @@ function employer() {
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
log.debug("DashboardPage mounted");
|
log.debug("DashboardPage mounted");
|
||||||
await courseSessionsStore.loadCourseSessionsData();
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,13 @@
|
||||||
import IconLogout from "@/components/icons/IconLogout.vue";
|
import IconLogout from "@/components/icons/IconLogout.vue";
|
||||||
import IconSettings from "@/components/icons/IconSettings.vue";
|
import IconSettings from "@/components/icons/IconSettings.vue";
|
||||||
import ItCheckbox from "@/components/ui/ItCheckbox.vue";
|
import ItCheckbox from "@/components/ui/ItCheckbox.vue";
|
||||||
|
import ItCheckboxGroup from "@/components/ui/ItCheckboxGroup.vue";
|
||||||
import ItDropdown from "@/components/ui/ItDropdown.vue";
|
import ItDropdown from "@/components/ui/ItDropdown.vue";
|
||||||
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
|
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
|
||||||
import { reactive } from "vue";
|
import ItRadioGroup from "@/components/ui/ItRadioGroup.vue";
|
||||||
|
import ItTextarea from "@/components/ui/ItTextarea.vue";
|
||||||
|
import logger from "loglevel";
|
||||||
|
import { reactive, ref } from "vue";
|
||||||
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
checkboxValue: true,
|
checkboxValue: true,
|
||||||
|
|
@ -68,8 +72,33 @@ function colorBgClass(color: string, value: number) {
|
||||||
return `bg-${color}-${value}`;
|
return `bg-${color}-${value}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const satisfactionValues = [
|
||||||
|
{ name: "Sehr unzufrieden", active: false, value: 1 },
|
||||||
|
{ name: "unzufrieden", active: true, value: 2 },
|
||||||
|
{ name: "zufrieden", active: false, value: 3 },
|
||||||
|
{ name: "Sehr zufrieden", active: false, value: 4 },
|
||||||
|
];
|
||||||
|
const satisfaction = ref(satisfactionValues[1].value);
|
||||||
|
const satisfactionText =
|
||||||
|
'Wie zufrieden bist du mit dem Kurs "Überbetriebliche Kurse" allgemein?';
|
||||||
|
|
||||||
|
const sourceLabel = "Wie bist du auf das Kursangebot aufmerksam geworden?";
|
||||||
|
const sourceValues = ref([]);
|
||||||
|
const sourceItems = [
|
||||||
|
{
|
||||||
|
name: "Internet",
|
||||||
|
value: "I",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "TV",
|
||||||
|
value: "T",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const textValue = ref("abc");
|
||||||
|
|
||||||
function log(data: any) {
|
function log(data: any) {
|
||||||
console.log(data);
|
logger.info(data);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -361,7 +390,11 @@ function log(data: any) {
|
||||||
|
|
||||||
<h2 class="mt-8 mb-8">Checkbox</h2>
|
<h2 class="mt-8 mb-8">Checkbox</h2>
|
||||||
|
|
||||||
<ItCheckbox v-model="state.checkboxValue" :disabled="false" class="">
|
<ItCheckbox
|
||||||
|
:checked="state.checkboxValue"
|
||||||
|
:disabled="false"
|
||||||
|
@toggle="state.checkboxValue = !state.checkboxValue"
|
||||||
|
>
|
||||||
Label
|
Label
|
||||||
</ItCheckbox>
|
</ItCheckbox>
|
||||||
|
|
||||||
|
|
@ -379,6 +412,18 @@ function log(data: any) {
|
||||||
Click Me
|
Click Me
|
||||||
</ItDropdown>
|
</ItDropdown>
|
||||||
</div>
|
</div>
|
||||||
|
<ItTextarea v-model="textValue" label="Hallo Velo" class="mb-8" />
|
||||||
|
<ItCheckboxGroup
|
||||||
|
v-model="sourceValues"
|
||||||
|
:label="sourceLabel"
|
||||||
|
:items="sourceItems"
|
||||||
|
class="mb-8"
|
||||||
|
/>
|
||||||
|
<ItRadioGroup
|
||||||
|
v-model="satisfaction"
|
||||||
|
:label="satisfactionText"
|
||||||
|
:items="satisfactionValues"
|
||||||
|
/>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import type { LearningPath } from "@/services/learningPath";
|
||||||
import { useCockpitStore } from "@/stores/cockpit";
|
import { useCockpitStore } from "@/stores/cockpit";
|
||||||
import { useCompetenceStore } from "@/stores/competence";
|
import { useCompetenceStore } from "@/stores/competence";
|
||||||
import { useLearningPathStore } from "@/stores/learningPath";
|
import { useLearningPathStore } from "@/stores/learningPath";
|
||||||
import * as log from "loglevel";
|
import log from "loglevel";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
courseSlug: string;
|
courseSlug: string;
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import CircleOverview from "@/components/learningPath/CircleOverview.vue";
|
||||||
import LearningSequence from "@/components/learningPath/LearningSequence.vue";
|
import LearningSequence from "@/components/learningPath/LearningSequence.vue";
|
||||||
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
|
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
|
||||||
import ItModal from "@/components/ui/ItModal.vue";
|
import ItModal from "@/components/ui/ItModal.vue";
|
||||||
import * as log from "loglevel";
|
import log from "loglevel";
|
||||||
import { computed, onMounted, reactive, ref } from "vue";
|
import { computed, onMounted, reactive, ref } from "vue";
|
||||||
|
|
||||||
import { useAppStore } from "@/stores/app";
|
import { useAppStore } from "@/stores/app";
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { useUserStore } from "@/stores/user";
|
import { useUserStore } from "@/stores/user";
|
||||||
import type { NavigationGuardWithThis, RouteLocationNormalized } from "vue-router";
|
import type { NavigationGuardWithThis, RouteLocationNormalized } from "vue-router";
|
||||||
|
|
||||||
export const updateLoggedIn: NavigationGuardWithThis<undefined> = async (_to) => {
|
export const updateLoggedIn: NavigationGuardWithThis<undefined> = async () => {
|
||||||
const loggedIn = getCookieValue("loginStatus") === "true";
|
const loggedIn = getCookieValue("loginStatus") === "true";
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
|
||||||
|
|
@ -11,10 +11,7 @@ export const updateLoggedIn: NavigationGuardWithThis<undefined> = async (_to) =>
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const redirectToLoginIfRequired: NavigationGuardWithThis<undefined> = (
|
export const redirectToLoginIfRequired: NavigationGuardWithThis<undefined> = (to) => {
|
||||||
to,
|
|
||||||
_from
|
|
||||||
) => {
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
if (loginRequired(to) && !userStore.loggedIn) {
|
if (loginRequired(to) && !userStore.loggedIn) {
|
||||||
return `/login?next=${to.fullPath}`;
|
return `/login?next=${to.fullPath}`;
|
||||||
|
|
|
||||||
|
|
@ -154,7 +154,7 @@ const router = createRouter({
|
||||||
router.beforeEach(updateLoggedIn);
|
router.beforeEach(updateLoggedIn);
|
||||||
router.beforeEach(redirectToLoginIfRequired);
|
router.beforeEach(redirectToLoginIfRequired);
|
||||||
|
|
||||||
router.afterEach((to, from) => {
|
router.afterEach(() => {
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
appStore.routingFinished = true;
|
appStore.routingFinished = true;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -29,12 +29,15 @@ export const useCockpitStore = defineStore({
|
||||||
actions: {
|
actions: {
|
||||||
async loadCourseSessionUsers(courseSlug: string, reload = false) {
|
async loadCourseSessionUsers(courseSlug: string, reload = false) {
|
||||||
log.debug("loadCockpitData called");
|
log.debug("loadCockpitData called");
|
||||||
const data = await itGetCached(`/api/course/sessions/${courseSlug}/users/`, {
|
const { users, cockpit_user: cockpitUser } = await itGetCached(
|
||||||
reload: reload,
|
`/api/course/sessions/${courseSlug}/users/`,
|
||||||
});
|
{
|
||||||
|
reload: reload,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
this.courseSessionUsers = data.users;
|
this.courseSessionUsers = users;
|
||||||
this.cockpitSessionUser = data.cockpit_user;
|
this.cockpitSessionUser = cockpitUser;
|
||||||
|
|
||||||
if (this.cockpitSessionUser && this.cockpitSessionUser.circles?.length > 0) {
|
if (this.cockpitSessionUser && this.cockpitSessionUser.circles?.length > 0) {
|
||||||
this.selectedCircles = [this.cockpitSessionUser.circles[0].translation_key];
|
this.selectedCircles = [this.cockpitSessionUser.circles[0].translation_key];
|
||||||
|
|
|
||||||
|
|
@ -4,53 +4,66 @@ import _ from "lodash";
|
||||||
import log from "loglevel";
|
import log from "loglevel";
|
||||||
|
|
||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
|
import { computed, ref } from "vue";
|
||||||
import { useRoute } from "vue-router";
|
import { useRoute } from "vue-router";
|
||||||
import { useUserStore } from "./user";
|
import { useUserStore } from "./user";
|
||||||
|
|
||||||
export type CourseSessionsStoreState = {
|
function loadCourseSessionsData(reload = false) {
|
||||||
courseSessions: CourseSession[] | undefined;
|
log.debug("loadCourseSessionsData called");
|
||||||
};
|
const courseSessions = ref<CourseSession[]>([]);
|
||||||
|
|
||||||
export const useCourseSessionsStore = defineStore({
|
async function loadAndUpdate() {
|
||||||
id: "courseSessions",
|
courseSessions.value = await itGetCached(`/api/course/sessions/`, {
|
||||||
state: () => {
|
reload: reload,
|
||||||
return {
|
});
|
||||||
courseSessions: undefined,
|
if (!courseSessions.value) {
|
||||||
} as CourseSessionsStoreState;
|
throw `No courseSessionData found for user`;
|
||||||
},
|
}
|
||||||
getters: {
|
}
|
||||||
courseSessionForRoute: (state) => {
|
|
||||||
const route = useRoute();
|
loadAndUpdate(); // this will be called asynchronously, but does not block
|
||||||
const routePath = decodeURI(route.path);
|
|
||||||
return state.courseSessions?.find((cs) => {
|
// returns the empty sessions array at first, then after loading populates the ref
|
||||||
return routePath.startsWith(cs.course_url);
|
return { courseSessions };
|
||||||
});
|
}
|
||||||
},
|
|
||||||
coursesFromCourseSessions: (state) => {
|
export const useCourseSessionsStore = defineStore("courseSessions", () => {
|
||||||
if (state.courseSessions) {
|
// using setup function seems cleaner, see https://pinia.vuejs.org/core-concepts/#setup-stores
|
||||||
return _.uniqBy(state.courseSessions, "course.id");
|
|
||||||
}
|
// this will become a state variable (like each other `ref()`
|
||||||
},
|
|
||||||
hasCockpit() {
|
// store should do own setup, we don't want to have each component initialize it
|
||||||
|
// that's why we call the load function in here
|
||||||
|
const { courseSessions } = loadCourseSessionsData();
|
||||||
|
// these will become getters
|
||||||
|
const coursesFromCourseSessions = computed(() =>
|
||||||
|
_.uniqBy(courseSessions.value, "course.id")
|
||||||
|
);
|
||||||
|
const courseSessionForRoute = computed(() => {
|
||||||
|
const route = useRoute();
|
||||||
|
const routePath = decodeURI(route.path);
|
||||||
|
|
||||||
|
return courseSessions.value.find((cs) => {
|
||||||
|
return routePath.startsWith(cs.course_url);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const hasCockpit = computed(() => {
|
||||||
|
if (courseSessionForRoute.value) {
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
return (
|
return (
|
||||||
this.courseSessionForRoute &&
|
courseSessionForRoute.value.experts.filter(
|
||||||
(this.courseSessionForRoute as unknown as CourseSession).experts.filter(
|
|
||||||
(expert) => expert.user_id === userStore.id
|
(expert) => expert.user_id === userStore.id
|
||||||
).length > 0
|
).length > 0
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
},
|
|
||||||
actions: {
|
return false;
|
||||||
async loadCourseSessionsData(reload = false) {
|
});
|
||||||
log.debug("loadCourseSessionsData called");
|
|
||||||
this.courseSessions = await itGetCached(`/api/course/sessions/`, {
|
return {
|
||||||
reload: reload,
|
courseSessions,
|
||||||
});
|
coursesFromCourseSessions,
|
||||||
if (!this.courseSessions) {
|
courseSessionForRoute,
|
||||||
throw `No courseSessionData found for user`;
|
hasCockpit,
|
||||||
}
|
};
|
||||||
return this.courseSessions;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
import * as log from "loglevel";
|
import log from "loglevel";
|
||||||
|
|
||||||
import { bustItGetCache, itGetCached, itPost } from "@/fetchHelpers";
|
import { bustItGetCache, itGetCached, itPost } from "@/fetchHelpers";
|
||||||
import { useAppStore } from "@/stores/app";
|
import { useAppStore } from "@/stores/app";
|
||||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
|
||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
|
|
||||||
const logoutRedirectUrl = import.meta.env.VITE_LOGOUT_REDIRECT;
|
const logoutRedirectUrl = import.meta.env.VITE_LOGOUT_REDIRECT;
|
||||||
|
|
@ -60,7 +59,7 @@ export const useUserStore = defineStore({
|
||||||
handleLogout() {
|
handleLogout() {
|
||||||
Object.assign(this, initialUserState);
|
Object.assign(this, initialUserState);
|
||||||
|
|
||||||
itPost("/api/core/logout/", {}).then((data) => {
|
itPost("/api/core/logout/", {}).then(() => {
|
||||||
let redirectUrl;
|
let redirectUrl;
|
||||||
|
|
||||||
if (logoutRedirectUrl !== "") {
|
if (logoutRedirectUrl !== "") {
|
||||||
|
|
@ -77,8 +76,9 @@ export const useUserStore = defineStore({
|
||||||
this.$state = data;
|
this.$state = data;
|
||||||
this.loggedIn = true;
|
this.loggedIn = true;
|
||||||
appStore.userLoaded = true;
|
appStore.userLoaded = true;
|
||||||
const courseSessionsStore = useCourseSessionsStore();
|
// todo: why?
|
||||||
await courseSessionsStore.loadCourseSessionsData();
|
// const courseSessionsStore = useCourseSessionsStore();
|
||||||
|
// await courseSessionsStore.loadCourseSessionsData();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -30,5 +30,5 @@ export function learningContentTypeData(t: LearningContentType): {
|
||||||
return { title: "In Umsetzung", icon: "it-icon-lc-document" };
|
return { title: "In Umsetzung", icon: "it-icon-lc-document" };
|
||||||
}
|
}
|
||||||
|
|
||||||
return assertUnreachable(t);
|
return assertUnreachable();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
export function assertUnreachable(x: never): never {
|
export function assertUnreachable(): never {
|
||||||
throw new Error("Didn't expect to get here");
|
throw new Error("Didn't expect to get here");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,5 +49,9 @@ module.exports = {
|
||||||
"bg-handlungsfelder-overview",
|
"bg-handlungsfelder-overview",
|
||||||
"bg-lernmedien-overview",
|
"bg-lernmedien-overview",
|
||||||
],
|
],
|
||||||
plugins: [require("@tailwindcss/typography"), require("@tailwindcss/forms")],
|
plugins: [
|
||||||
|
require("@tailwindcss/typography"),
|
||||||
|
require("@tailwindcss/forms"),
|
||||||
|
require("@headlessui/tailwindcss"),
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,7 @@ LOCAL_APPS = [
|
||||||
"vbv_lernwelt.learnpath",
|
"vbv_lernwelt.learnpath",
|
||||||
"vbv_lernwelt.competence",
|
"vbv_lernwelt.competence",
|
||||||
"vbv_lernwelt.media_library",
|
"vbv_lernwelt.media_library",
|
||||||
|
"vbv_lernwelt.feedback",
|
||||||
]
|
]
|
||||||
# https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
|
# https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
|
||||||
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS
|
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,8 @@ from vbv_lernwelt.course.views import (
|
||||||
request_course_completion,
|
request_course_completion,
|
||||||
request_course_completion_for_user,
|
request_course_completion_for_user,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from vbv_lernwelt.feedback.views import get_name
|
||||||
from wagtail import urls as wagtail_urls
|
from wagtail import urls as wagtail_urls
|
||||||
from wagtail.admin import urls as wagtailadmin_urls
|
from wagtail.admin import urls as wagtailadmin_urls
|
||||||
from wagtail.documents import urls as wagtaildocs_urls
|
from wagtail.documents import urls as wagtaildocs_urls
|
||||||
|
|
@ -82,6 +84,7 @@ urlpatterns = [
|
||||||
raise_example_error), ),
|
raise_example_error), ),
|
||||||
path("server/checkratelimit/", check_rate_limit),
|
path("server/checkratelimit/", check_rate_limit),
|
||||||
path("server/", include(grapple_urls)),
|
path("server/", include(grapple_urls)),
|
||||||
|
path(r"your-name/", get_name)
|
||||||
]
|
]
|
||||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
schema {
|
schema {
|
||||||
query: Query
|
query: Query
|
||||||
|
mutation: Mutation
|
||||||
}
|
}
|
||||||
|
|
||||||
type BlockQuoteBlock implements StreamFieldInterface {
|
type BlockQuoteBlock implements StreamFieldInterface {
|
||||||
|
|
@ -275,6 +276,11 @@ type EmbedBlock implements StreamFieldInterface {
|
||||||
rawEmbed: JSONString
|
rawEmbed: JSONString
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ErrorType {
|
||||||
|
field: String!
|
||||||
|
messages: [String!]!
|
||||||
|
}
|
||||||
|
|
||||||
type FloatBlock implements StreamFieldInterface {
|
type FloatBlock implements StreamFieldInterface {
|
||||||
id: String
|
id: String
|
||||||
blockType: String!
|
blockType: String!
|
||||||
|
|
@ -598,6 +604,10 @@ type MediaLibraryPage implements PageInterface {
|
||||||
ancestors(limit: PositiveInt, offset: PositiveInt, order: String, searchQuery: String, id: ID): [PageInterface!]!
|
ancestors(limit: PositiveInt, offset: PositiveInt, order: String, searchQuery: String, id: ID): [PageInterface!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Mutation {
|
||||||
|
sendFeedback(input: SendFeedbackInput!): SendFeedbackPayload
|
||||||
|
}
|
||||||
|
|
||||||
type Page implements PageInterface {
|
type Page implements PageInterface {
|
||||||
id: ID
|
id: ID
|
||||||
path: String!
|
path: String!
|
||||||
|
|
@ -779,6 +789,40 @@ type SecurityRequestResponseLog {
|
||||||
id: ID
|
id: ID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input SendFeedbackInput {
|
||||||
|
id: Int
|
||||||
|
page: String!
|
||||||
|
satisfaction: Int
|
||||||
|
goalAttainment: Int
|
||||||
|
proficiency: Int
|
||||||
|
receivedMaterials: Boolean
|
||||||
|
materialsRating: Int
|
||||||
|
instructorCompetence: Int
|
||||||
|
instructorRespect: Int
|
||||||
|
instructorOpenFeedback: String
|
||||||
|
wouldRecommend: Boolean
|
||||||
|
coursePositiveFeedback: String
|
||||||
|
courseNegativeFeedback: String
|
||||||
|
clientMutationId: String
|
||||||
|
}
|
||||||
|
|
||||||
|
type SendFeedbackPayload {
|
||||||
|
id: Int
|
||||||
|
satisfaction: Int
|
||||||
|
goalAttainment: Int
|
||||||
|
proficiency: Int
|
||||||
|
receivedMaterials: Boolean
|
||||||
|
materialsRating: Int
|
||||||
|
instructorCompetence: Int
|
||||||
|
instructorRespect: Int
|
||||||
|
instructorOpenFeedback: String
|
||||||
|
wouldRecommend: Boolean
|
||||||
|
coursePositiveFeedback: String
|
||||||
|
courseNegativeFeedback: String
|
||||||
|
errors: [ErrorType]
|
||||||
|
clientMutationId: String
|
||||||
|
}
|
||||||
|
|
||||||
type SiteObjectType {
|
type SiteObjectType {
|
||||||
id: ID!
|
id: ID!
|
||||||
hostname: String!
|
hostname: String!
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
from django import forms
|
||||||
|
|
||||||
|
from vbv_lernwelt.feedback.models import FeedbackResponse
|
||||||
|
|
||||||
|
|
||||||
|
class FeedbackForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = FeedbackResponse
|
||||||
|
fields = "__all__"
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
import graphene
|
||||||
|
from graphene_django.rest_framework.mutation import SerializerMutation
|
||||||
|
|
||||||
|
from vbv_lernwelt.feedback.serializers import FeedbackResponseSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class SendFeedback(SerializerMutation):
|
||||||
|
class Meta:
|
||||||
|
serializer_class = FeedbackResponseSerializer
|
||||||
|
model_operations = ["create"]
|
||||||
|
|
||||||
|
|
||||||
|
class Mutation(object):
|
||||||
|
send_feedback = SendFeedback.Field()
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
from graphene_django import DjangoObjectType
|
||||||
|
|
||||||
|
from vbv_lernwelt.feedback.models import Feedback
|
||||||
|
|
||||||
|
|
||||||
|
# class FeedbackType(DjangoObjectType):
|
||||||
|
# class Meta:
|
||||||
|
# model = Feedback
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
# Generated by Django 3.2.13 on 2022-12-27 14:58
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import vbv_lernwelt.feedback.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("learnpath", "0009_alter_learningcontent_contents"),
|
||||||
|
("course", "0007_coursesessionuser_role"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="FeedbackResponse",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"satisfaction",
|
||||||
|
vbv_lernwelt.feedback.models.FeedbackIntegerField(
|
||||||
|
null=True,
|
||||||
|
validators=[
|
||||||
|
django.core.validators.MinValueValidator(1),
|
||||||
|
django.core.validators.MaxValueValidator(4),
|
||||||
|
django.core.validators.MinValueValidator(1),
|
||||||
|
django.core.validators.MaxValueValidator(4),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"goal_attainment",
|
||||||
|
vbv_lernwelt.feedback.models.FeedbackIntegerField(
|
||||||
|
null=True,
|
||||||
|
validators=[
|
||||||
|
django.core.validators.MinValueValidator(1),
|
||||||
|
django.core.validators.MaxValueValidator(4),
|
||||||
|
django.core.validators.MinValueValidator(1),
|
||||||
|
django.core.validators.MaxValueValidator(4),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("proficiency", models.IntegerField(null=True)),
|
||||||
|
("received_materials", models.BooleanField(null=True)),
|
||||||
|
(
|
||||||
|
"materials_rating",
|
||||||
|
vbv_lernwelt.feedback.models.FeedbackIntegerField(
|
||||||
|
null=True,
|
||||||
|
validators=[
|
||||||
|
django.core.validators.MinValueValidator(1),
|
||||||
|
django.core.validators.MaxValueValidator(4),
|
||||||
|
django.core.validators.MinValueValidator(1),
|
||||||
|
django.core.validators.MaxValueValidator(4),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"instructor_competence",
|
||||||
|
vbv_lernwelt.feedback.models.FeedbackIntegerField(
|
||||||
|
null=True,
|
||||||
|
validators=[
|
||||||
|
django.core.validators.MinValueValidator(1),
|
||||||
|
django.core.validators.MaxValueValidator(4),
|
||||||
|
django.core.validators.MinValueValidator(1),
|
||||||
|
django.core.validators.MaxValueValidator(4),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"instructor_respect",
|
||||||
|
vbv_lernwelt.feedback.models.FeedbackIntegerField(
|
||||||
|
null=True,
|
||||||
|
validators=[
|
||||||
|
django.core.validators.MinValueValidator(1),
|
||||||
|
django.core.validators.MaxValueValidator(4),
|
||||||
|
django.core.validators.MinValueValidator(1),
|
||||||
|
django.core.validators.MaxValueValidator(4),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("instructor_open_feedback", models.TextField(blank=True)),
|
||||||
|
("would_recommend", models.BooleanField(null=True)),
|
||||||
|
("course_positive_feedback", models.TextField(blank=True)),
|
||||||
|
("course_negative_feedback", models.TextField(blank=True)),
|
||||||
|
(
|
||||||
|
"circle",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
to="learnpath.circle",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"course_session",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
to="course.coursesession",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
class FeedbackIntegerField(models.IntegerField):
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
validators = kwargs.pop("validators", [])
|
||||||
|
nullable = kwargs.pop("null", True)
|
||||||
|
super().__init__(
|
||||||
|
validators=validators + [MinValueValidator(1), MaxValueValidator(4)],
|
||||||
|
null=nullable,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class FeedbackResponse(models.Model):
|
||||||
|
class DiscoveredChoices(models.TextChoices):
|
||||||
|
INTERNET = "I", _("Internet")
|
||||||
|
LEAFLET = "L", _("Leaflet")
|
||||||
|
NEWSPAPER = "N", _("Newspaper")
|
||||||
|
RECOMMENDATION = "R", _("Personal recommendation")
|
||||||
|
EVENT = "E", _("Public event")
|
||||||
|
OTHER = "O", _("Other")
|
||||||
|
|
||||||
|
class RatingChoices(models.IntegerChoices):
|
||||||
|
ONE = 1, "1"
|
||||||
|
TWO = 2, "2"
|
||||||
|
THREE = 3, "3"
|
||||||
|
FOUR = 4, "4"
|
||||||
|
|
||||||
|
class PercentageChoices(models.IntegerChoices):
|
||||||
|
TWENTY = 20, "20%"
|
||||||
|
FOURTY = 40, "40%"
|
||||||
|
SIXTY = 60, "60%"
|
||||||
|
EIGHTY = 80, "80%"
|
||||||
|
HUNDRED = 100, "100%"
|
||||||
|
|
||||||
|
satisfaction = FeedbackIntegerField()
|
||||||
|
goal_attainment = FeedbackIntegerField()
|
||||||
|
proficiency = models.IntegerField(null=True)
|
||||||
|
received_materials = models.BooleanField(null=True)
|
||||||
|
materials_rating = FeedbackIntegerField()
|
||||||
|
instructor_competence = FeedbackIntegerField()
|
||||||
|
instructor_respect = FeedbackIntegerField()
|
||||||
|
instructor_open_feedback = models.TextField(blank=True)
|
||||||
|
would_recommend = models.BooleanField(null=True)
|
||||||
|
course_positive_feedback = models.TextField(blank=True)
|
||||||
|
course_negative_feedback = models.TextField(blank=True)
|
||||||
|
|
||||||
|
circle = models.ForeignKey("learnpath.Circle", models.PROTECT)
|
||||||
|
course_session = models.ForeignKey("course.CourseSession", models.PROTECT)
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
import structlog
|
||||||
|
from rest_framework import serializers
|
||||||
|
from wagtail.models import Page
|
||||||
|
|
||||||
|
from vbv_lernwelt.course.models import CourseSession
|
||||||
|
from vbv_lernwelt.feedback.models import FeedbackResponse
|
||||||
|
|
||||||
|
logger = structlog.get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class FeedbackResponseSerializer(serializers.ModelSerializer):
|
||||||
|
page = serializers.CharField(write_only=True)
|
||||||
|
course_session = serializers.CharField(write_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = FeedbackResponse
|
||||||
|
exclude = ["circle"]
|
||||||
|
# extra_kwargs = {"course", {"read_only": True}}
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
logger.info("creating feedback")
|
||||||
|
page_key = validated_data.pop("page")
|
||||||
|
course_session_id = validated_data.pop("course_session")
|
||||||
|
|
||||||
|
learning_content = Page.objects.get(
|
||||||
|
translation_key=page_key, locale__language_code="de-CH"
|
||||||
|
)
|
||||||
|
circle = learning_content.get_parent().specific
|
||||||
|
course_session = CourseSession.objects.get(id=course_session_id)
|
||||||
|
return FeedbackResponse.objects.create(
|
||||||
|
**validated_data, circle=circle, course_session=course_session
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
from django.http import HttpResponseRedirect
|
||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
from .forms import FeedbackForm
|
||||||
|
|
||||||
|
|
||||||
|
def get_name(request):
|
||||||
|
if request.method == "POST":
|
||||||
|
form = FeedbackForm(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
return HttpResponseRedirect("/thanks/")
|
||||||
|
else:
|
||||||
|
form = FeedbackForm()
|
||||||
|
|
||||||
|
return render(request, "feedback/name.html", {"form": form})
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
from wagtail.core import hooks
|
||||||
|
|
||||||
|
from .graphql.mutations import Mutation
|
||||||
|
|
||||||
|
|
||||||
|
@hooks.register("register_schema_mutation")
|
||||||
|
def register_feedback_mutations(mutation_mixins):
|
||||||
|
mutation_mixins.append(Mutation)
|
||||||
|
|
@ -0,0 +1,126 @@
|
||||||
|
# Generated by Django 3.2.13 on 2022-12-08 09:55
|
||||||
|
|
||||||
|
import wagtail.blocks
|
||||||
|
import wagtail.fields
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("learnpath", "0008_alter_learningcontent_contents"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="learningcontent",
|
||||||
|
name="contents",
|
||||||
|
field=wagtail.fields.StreamField(
|
||||||
|
[
|
||||||
|
(
|
||||||
|
"video",
|
||||||
|
wagtail.blocks.StructBlock(
|
||||||
|
[
|
||||||
|
("description", wagtail.blocks.TextBlock()),
|
||||||
|
("url", wagtail.blocks.TextBlock()),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"resource",
|
||||||
|
wagtail.blocks.StructBlock(
|
||||||
|
[
|
||||||
|
("description", wagtail.blocks.TextBlock()),
|
||||||
|
("url", wagtail.blocks.TextBlock()),
|
||||||
|
("text", wagtail.blocks.RichTextBlock(required=False)),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"exercise",
|
||||||
|
wagtail.blocks.StructBlock(
|
||||||
|
[
|
||||||
|
("description", wagtail.blocks.TextBlock()),
|
||||||
|
("url", wagtail.blocks.TextBlock()),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"learningmodule",
|
||||||
|
wagtail.blocks.StructBlock(
|
||||||
|
[
|
||||||
|
("description", wagtail.blocks.TextBlock()),
|
||||||
|
("url", wagtail.blocks.TextBlock()),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"online_training",
|
||||||
|
wagtail.blocks.StructBlock(
|
||||||
|
[
|
||||||
|
("description", wagtail.blocks.TextBlock()),
|
||||||
|
("url", wagtail.blocks.TextBlock()),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"media_library",
|
||||||
|
wagtail.blocks.StructBlock(
|
||||||
|
[
|
||||||
|
("description", wagtail.blocks.TextBlock()),
|
||||||
|
("url", wagtail.blocks.TextBlock()),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"document",
|
||||||
|
wagtail.blocks.StructBlock(
|
||||||
|
[
|
||||||
|
("description", wagtail.blocks.TextBlock()),
|
||||||
|
("url", wagtail.blocks.TextBlock()),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"test",
|
||||||
|
wagtail.blocks.StructBlock(
|
||||||
|
[
|
||||||
|
("description", wagtail.blocks.TextBlock()),
|
||||||
|
("url", wagtail.blocks.TextBlock()),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"book",
|
||||||
|
wagtail.blocks.StructBlock(
|
||||||
|
[
|
||||||
|
("description", wagtail.blocks.TextBlock()),
|
||||||
|
("url", wagtail.blocks.TextBlock()),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"assignment",
|
||||||
|
wagtail.blocks.StructBlock(
|
||||||
|
[
|
||||||
|
("description", wagtail.blocks.TextBlock()),
|
||||||
|
("url", wagtail.blocks.TextBlock()),
|
||||||
|
("text", wagtail.blocks.RichTextBlock(required=False)),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"placeholder",
|
||||||
|
wagtail.blocks.StructBlock(
|
||||||
|
[
|
||||||
|
("description", wagtail.blocks.TextBlock()),
|
||||||
|
("url", wagtail.blocks.TextBlock()),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("feedback", wagtail.blocks.StructBlock([])),
|
||||||
|
],
|
||||||
|
use_json_field=None,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -14,6 +14,7 @@ from vbv_lernwelt.learnpath.models_learning_unit_content import (
|
||||||
BookBlock,
|
BookBlock,
|
||||||
DocumentBlock,
|
DocumentBlock,
|
||||||
ExerciseBlock,
|
ExerciseBlock,
|
||||||
|
FeedbackBlock,
|
||||||
LearningModuleBlock,
|
LearningModuleBlock,
|
||||||
MediaLibraryBlock,
|
MediaLibraryBlock,
|
||||||
OnlineTrainingBlock,
|
OnlineTrainingBlock,
|
||||||
|
|
@ -267,6 +268,7 @@ class LearningContent(CourseBasePage):
|
||||||
("book", BookBlock()),
|
("book", BookBlock()),
|
||||||
("assignment", AssignmentBlock()),
|
("assignment", AssignmentBlock()),
|
||||||
("placeholder", PlaceholderBlock()),
|
("placeholder", PlaceholderBlock()),
|
||||||
|
("feedback", FeedbackBlock()),
|
||||||
]
|
]
|
||||||
|
|
||||||
contents = StreamField(
|
contents = StreamField(
|
||||||
|
|
|
||||||
|
|
@ -90,3 +90,8 @@ class PlaceholderBlock(blocks.StructBlock):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
icon = "media"
|
icon = "media"
|
||||||
|
|
||||||
|
|
||||||
|
class FeedbackBlock(blocks.StructBlock):
|
||||||
|
class Meta:
|
||||||
|
icon = "media"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<form action="/your-name/" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form }}
|
||||||
|
<input type="submit" value="submit" />
|
||||||
|
</form>
|
||||||
Loading…
Reference in New Issue