Merge branch 'feature/datenmodell' into develop

This commit is contained in:
Daniel Egger 2022-09-30 15:22:47 +02:00
commit 38c8d2120a
112 changed files with 2857 additions and 1155 deletions

View File

@ -162,7 +162,7 @@ const profileDropdownData = [
Shop
</router-link>
<router-link
to="/mediacenter"
to="/mediacenter/versicherungsvermittlerin-media/overview"
class="nav-item"
:class="{ 'nav-item--active': isInRoutePath(['/mediacenter']) }"
>

View File

@ -1,9 +1,9 @@
<script setup lang="ts">
import { Circle } from '@/services/circle'
import type { Circle } from '@/services/circle'
import ItFullScreenModal from '@/components/ui/ItFullScreenModal.vue'
const props = defineProps<{
circle: Circle
circle: Circle | undefined
show: boolean
}>()
@ -12,7 +12,7 @@ const props = defineProps<{
<template>
<ItFullScreenModal :show="show" @closemodal="$emit('closemodal')">
<div class="container-medium">
<div class="container-medium" v-if="circle">
<h1 class="">Überblick: Circle "{{ circle.title }}"</h1>
<p class="mt-8 text-xl">Hier zeigen wir dir, was du in diesem Circle lernen wirst.</p>

View File

@ -65,9 +65,6 @@ const block = computed(() => {
</iframe>
</div>
<div v-if="block.type === 'podcast'">
<iframe width="100%" height="300" scrolling="no" frameborder="no" allow="" :src="block.value.url"></iframe>
</div>
</div>
</div>
</template>

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import ItCheckbox from '@/components/ui/ItCheckbox.vue'
import type { LearningContent, LearningSequence } from '@/types'
import type { CourseCompletionStatus, LearningContent, LearningSequence } from '@/types'
import { useCircleStore } from '@/stores/circle'
import { computed } from 'vue'
import _ from 'lodash'
@ -14,7 +14,11 @@ const props = defineProps<{
const circleStore = useCircleStore()
function toggleCompleted(learningContent: LearningContent) {
circleStore.markCompletion(learningContent, !learningContent.completed)
let completionStatus: CourseCompletionStatus = 'success'
if (learningContent.completion_status === 'success') {
completionStatus = 'fail'
}
circleStore.markCompletion(learningContent, completionStatus)
}
const someFinished = computed(() => {
@ -36,7 +40,7 @@ const allFinished = computed(() => {
const continueTranslationKeyTuple = computed(() => {
if (props.learningSequence && circleStore.circle) {
const lastFinished = _.findLast(circleStore.circle.flatLearningContents, (learningContent) => {
return learningContent.completed
return learningContent.completion_status === 'success'
})
if (!lastFinished) {
@ -91,7 +95,7 @@ const learningSequenceBorderClass = computed(() => {
class="flex gap-4 pb-3 lg:pb-6"
>
<ItCheckbox
:modelValue="learningContent.completed"
:modelValue="learningContent.completion_status === 'success'"
:onToggle="() => toggleCompleted(learningContent)"
:data-cy="`${learningContent.slug}`"
>

View File

@ -2,7 +2,7 @@
import * as log from 'loglevel'
import { computed, reactive } from 'vue'
import { useCircleStore } from '@/stores/circle'
import { LearningUnit } from '@/types'
import type { LearningUnit } from '@/types'
log.debug('LearningContent.vue setup')
@ -62,24 +62,24 @@ function handleContinue() {
<div class="mt-4 lg:mt-8 flex flex-col lg:flex-row justify-between gap-6">
<button
@click="circleStore.markCompletion(currentQuestion, true)"
@click="circleStore.markCompletion(currentQuestion, 'success')"
class="flex-1 inline-flex items-center text-left p-4 border"
:class="{
'border-green-500': currentQuestion.completed,
'border-2': currentQuestion.completed,
'border-gray-500': !currentQuestion.completed,
'border-green-500': currentQuestion.completion_status === 'success',
'border-2': currentQuestion.completion_status === 'success',
'border-gray-500': currentQuestion.completion_status !== 'success',
}"
>
<it-icon-smiley-happy class="w-16 h-16 mr-4"></it-icon-smiley-happy>
<span class="font-bold text-xl"> Ja, ich kann das. </span>
</button>
<button
@click="circleStore.markCompletion(currentQuestion, false)"
@click="circleStore.markCompletion(currentQuestion, 'fail')"
class="flex-1 inline-flex items-center text-left p-4 border"
:class="{
'border-orange-500': currentQuestion.completed === false,
'border-2': currentQuestion.completed === false,
'border-gray-500': currentQuestion.completed === true || currentQuestion.completed === undefined,
'border-orange-500': currentQuestion.completion_status === 'fail',
'border-2': currentQuestion.completion_status === 'fail',
'border-gray-500': currentQuestion.completion_status !== 'fail'
}"
>
<it-icon-smiley-thinking class="w-16 h-16 mr-4"></it-icon-smiley-thinking>

View File

@ -0,0 +1,33 @@
<script setup lang="ts">
import MediaLink from '@/components/mediaCenter/MediaLink.vue'
export interface Props {
title: string
description: string
linkText: string
url: string
icon: string
openWindow?: boolean
}
const props = withDefaults(defineProps<Props>(), {
icon: '',
description: '',
openWindow: false,
})
</script>
<template>
<div class="border-gray-500 border flex p-4 items-center">
<img class="mr-6" :src="icon" />
<div>
<h4 class="mb-2 text-bold">{{ title }}</h4>
<p class="mb-2">{{ description }}</p>
<media-link :to="url" :blank="openWindow" class="link">
<span class="inline">{{ linkText }}</span>
</media-link>
</div>
</div>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,28 @@
<script setup lang="ts">
// https://router.vuejs.org/guide/advanced/extending-router-link.html
// https://vueschool.io/articles/vuejs-tutorials/extending-vue-router-links-in-vue-3/
import { RouterLink } from 'vue-router'
import { computed } from 'vue'
const props = defineProps({
...RouterLink.props, // @ts-ignore
blank: {
type: Boolean,
default: false,
},
})
const isExternalLink = computed(() => typeof props.to === 'string' && props.to.startsWith('http'))
</script>
<template>
<div>
<a v-if="isExternalLink" :target="props.blank ? '_blank' : '_self'" rel="noopener" :href="props.to">
<slot />
</a>
<router-link v-else :target="props.blank ? '_blank' : '_self'" rel="noopener" v-bind="props">
<slot />
</router-link>
</div>
</template>

View File

@ -0,0 +1,39 @@
<script setup lang="ts">
export interface Props {
title: string,
description: string,
call2Action: string,
link: string,
icon: string
}
const props = withDefaults(defineProps<Props>(), {
icon: ''
})
</script>
<template>
<div class="bg-white p-8 flex justify-between">
<div>
<h3 class="mb-4">{{title}}</h3>
<p class="mb-4">{{description}}</p>
<router-link
:to="link"
class="inline-flex items-center font-normal"
>
<span class="inline">{{call2Action}}</span>
<it-icon-arrow-right class="ml-1 h-5 w-5"></it-icon-arrow-right>
</router-link>
</div>
<div
v-if="icon"
:class="[`bg-${icon}`]"
class="bg-contain bg-no-repeat bg-right w-2/6 -mr-8">
</div>
</div>
</template>
<style lang="scss" scoped>
</style>

View File

@ -1,6 +1,5 @@
<script setup lang="ts">
import {reactive} from 'vue'
import {Menu, MenuButton, MenuItems, MenuItem} from '@headlessui/vue'
const props = defineProps<{

View File

@ -0,0 +1,78 @@
<script setup lang="ts">
import { watch, onMounted, reactive, defineEmits, computed } from 'vue'
import {Listbox, ListboxButton, ListboxOption, ListboxOptions} from '@headlessui/vue';
export interface DropdownSelectable {
id: number|string,
name: string
}
// https://stackoverflow.com/questions/64775876/vue-3-pass-reactive-object-to-component-with-two-way-binding
export interface Props {
modelValue: {
id: string|number
name: string
},
items: DropdownSelectable[]
}
const emit = defineEmits<{
(e: 'update:modelValue', data: object): void
}>()
const props = withDefaults(defineProps<Props>(), {
modelValue: {
id: -1,
name: ''
},
items: [],
})
const dropdownSelected = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val),
})
</script>
<template>
<Listbox as="div" v-model="dropdownSelected">
<div class="mt-1 relative w-96">
<ListboxButton
class="bg-white relative w-full border border-gray-500 pl-5 pr-10 py-3 text-left cursor-default font-bold">
<span class="block truncate">{{ dropdownSelected.name }}</span>
<span class="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
<it-icon-arrow-down class="h-5 w-5" aria-hidden="true"/>
</span>
</ListboxButton>
<transition leave-active-class="transition ease-in duration-100" leave-from-class="opacity-100"
leave-to-class="opacity-0">
<ListboxOptions
class="absolute z-10 mt-1 w-full bg-white shadow-lg max-h-60 py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm">
<ListboxOption
as="template"
v-for="item in items"
:key="item.id"
:value="item"
v-slot="{ active, selected }">
<li
:class="[active ? 'text-white bg-blue-900' : 'text-black', 'cursor-default select-none relative py-2 pl-3 pr-9']">
<span :class="[dropdownSelected ? 'font-semibold' : 'font-normal', 'block truncate']">
{{ item.name }}
</span>
<span v-if="dropdownSelected"
class="text-blue-900 absolute inset-y-0 right-0 flex items-center pr-4">
<it-icon-check
v-if="selected"
class="h-5 w-5"
aria-hidden="true"/>
</span>
</li>
</ListboxOption>
</ListboxOptions>
</transition>
</div>
</Listbox>
</template>

View File

@ -1,5 +1,5 @@
import { nextTick } from 'vue'
import { createI18n } from 'vue-i18n/index'
import { createI18n } from 'vue-i18n'
// https://vue-i18n.intlify.dev/guide/advanced/lazy.html
export const SUPPORT_LOCALES = ['de', 'fr', 'it']

View File

@ -25,8 +25,28 @@ const router = createRouter({
component: () => import('@/views/ShopView.vue'),
},
{
path: '/mediacenter',
component: () => import('@/views/MediaView.vue'),
path: '/mediacenter/:mediaCenterPageSlug',
props: true,
component: () => import('@/views/MediaCenterView.vue'),
children: [
{
path: 'overview',
component: () => import('@/views/MediaCenterMainView.vue'),
},
{
path: 'handlungsfelder/:mediaCategorySlug',
props: true,
component: () => import('@/views/MediaCategoryDetailView.vue'),
},
{
path: 'handlungsfelder',
component: () => import('@/views/MediaCenterCategoryOverview.vue'),
},
{
path: 'handlungsfeldlist',
component: () => import('@/views/MediaList.vue'),
},
],
},
{
path: '/messages',

View File

@ -4,9 +4,9 @@ import { Circle } from '../circle'
describe('Circle.parseJson', () => {
it('can parse circle from api response', () => {
const cirleData = data.children.find((c) => c.slug === 'unit-test-circle')
const cirleData = data.children.find((c) => c.slug === 'test-lehrgang-lp-circle-analyse')
const circle = Circle.fromJson(cirleData, undefined)
expect(circle.learningSequences.length).toBe(3)
expect(circle.flatLearningContents.length).toBe(8)
expect(circle.flatLearningContents.length).toBe(7)
})
})

View File

@ -8,7 +8,7 @@ describe('LearningPath.parseJson', () => {
expect(learningPath.circles.length).toBe(2)
expect(learningPath.circles[0].title).toBe('Basis')
expect(learningPath.circles[1].title).toBe('Unit-Test Circle')
expect(learningPath.circles[1].title).toBe('Analyse')
expect(learningPath.topics.length).toBe(2)
})

View File

@ -1,314 +1,297 @@
{
"id": 409,
"title": "Unit-Test Lernpfad",
"slug": "unit-test-lernpfad",
"id": 372,
"title": "Test Lernpfad",
"slug": "test-lehrgang-lp",
"type": "learnpath.LearningPath",
"translation_key": "9f50de84-036c-4986-ab3e-1a83a374910a",
"translation_key": "42e559ca-970f-4a08-9e5e-63860585ee1e",
"children": [
{
"id": 410,
"id": 373,
"title": "Basis",
"slug": "basis-1",
"slug": "test-lehrgang-lp-topic-basis",
"type": "learnpath.Topic",
"translation_key": "fbc1431c-46b0-4f77-93ee-4f10e0e59c03",
"translation_key": "d68c1544-cf22-4a59-a81c-8cb977440cd0",
"is_visible": false
},
{
"id": 411,
"id": 374,
"title": "Basis",
"slug": "basis-2",
"slug": "test-lehrgang-lp-circle-basis",
"type": "learnpath.Circle",
"translation_key": "d30cb8f8-6bb5-4e7a-8123-a370b7668a85",
"translation_key": "ec62a2af-6f74-4031-b971-c3287bbbc573",
"children": [
{
"id": 412,
"id": 375,
"title": "Starten",
"slug": "starten",
"slug": "test-lehrgang-lp-circle-basis-ls-starten",
"type": "learnpath.LearningSequence",
"translation_key": "1c5cd2a1-a39e-496e-b856-342f34d2b21c",
"translation_key": "c5fdada9-036d-4516-a50f-6656a1c6b009",
"icon": "it-icon-ls-start"
},
{
"id": 413,
"title": "Einleitung Circle \"Basis\"",
"slug": "einleitung-circle-basis-1",
"id": 376,
"title": "Einf\u00fchrung",
"slug": "test-lehrgang-lp-circle-basis-lc-einf\u00fchrung",
"type": "learnpath.LearningContent",
"translation_key": "48d4ace9-b0cf-4e23-98d2-012c1b91100e",
"translation_key": "01de5131-28ce-4b1f-805f-8643384bfd6b",
"minutes": 15,
"contents": [
{
"type": "video",
"type": "document",
"value": {
"description": "Basis Video",
"url": "https://www.youtube.com/embed/qhPIfxS2hvI"
"description": "Beispiel Dokument",
"url": null
},
"id": "ee431ded-edc4-4984-9dd8-ab1d869d82ae"
"id": "bd05f721-3e9d-4a11-8fe2-7c04e2365f52"
}
]
},
{
"id": 414,
"id": 377,
"title": "Beenden",
"slug": "beenden",
"slug": "test-lehrgang-lp-circle-basis-ls-beenden",
"type": "learnpath.LearningSequence",
"translation_key": "eaeaf0c7-b2b7-41a9-a77f-b392f83291eb",
"translation_key": "128c0162-025f-41be-9842-60016a77cdbc",
"icon": "it-icon-ls-end"
},
{
"id": 415,
"title": "Kompetenzprofil anschauen",
"slug": "kompetenzprofil-anschauen-8",
"id": 378,
"title": "Jetzt kann es losgehen!",
"slug": "test-lehrgang-lp-circle-basis-lc-jetzt-kann-es-losgehen",
"type": "learnpath.LearningContent",
"translation_key": "784772fc-d2ac-4df2-8ca1-61a45fbfe001",
"translation_key": "271896b9-6082-4fd4-9d70-6093ec9cc6ea",
"minutes": 30,
"contents": [
{
"type": "document",
"value": {
"description": "Beispiel Kompetenz"
"description": "Beispiel Dokument",
"url": null
},
"id": "09acb23d-cb20-4d0f-963b-61db9ac0b037"
}
]
},
{
"id": 416,
"title": "Circle \"Analyse\" abschliessen",
"slug": "circle-analyse-abschliessen-8",
"type": "learnpath.LearningContent",
"translation_key": "e1bf9081-cf6b-4426-a16d-8213aba9795e",
"minutes": 30,
"contents": [
{
"type": "document",
"value": {
"description": "Beispiel Kompetenz"
},
"id": "fa835da9-6238-40fb-a718-2d21d420926f"
"id": "204fc13b-a9ae-40de-8e09-f1e922c4fdd9"
}
]
}
],
"description": "Basis von Unit-Test Lernpfad",
"description": "Basis",
"job_situations": [],
"goals": [],
"experts": []
},
{
"id": 417,
"title": "Gewinnen von Kunden",
"slug": "gewinnen-von-kunden-1",
"id": 379,
"title": "Beraten der Kunden",
"slug": "test-lehrgang-lp-topic-beraten-der-kunden",
"type": "learnpath.Topic",
"translation_key": "4b2aa669-4cd9-43f1-9605-8575e5e7e760",
"translation_key": "91918780-75f8-4db3-8fb8-91b63f08b9b9",
"is_visible": true
},
{
"id": 418,
"title": "Unit-Test Circle",
"slug": "unit-test-circle",
"id": 380,
"title": "Analyse",
"slug": "test-lehrgang-lp-circle-analyse",
"type": "learnpath.Circle",
"translation_key": "8433f8fe-7074-4c8a-a93a-b62e042f06ca",
"translation_key": "50f11be3-a56d-412d-be25-3d272fb5df40",
"children": [
{
"id": 419,
"id": 381,
"title": "Starten",
"slug": "starten",
"slug": "test-lehrgang-lp-circle-analyse-ls-starten",
"type": "learnpath.LearningSequence",
"translation_key": "065ab931-122a-4e4d-a570-f8e6352a0550",
"translation_key": "07ac0eb9-3671-4b62-8053-1d0c43a1f0fb",
"icon": "it-icon-ls-start"
},
{
"id": 420,
"title": "Einleitung Circle \"Unit-Test Circle\"",
"slug": "einleitung-circle-unit-test-circle",
"id": 382,
"title": "Einleitung Circle \"Analyse\"",
"slug": "test-lehrgang-lp-circle-analyse-lc-einleitung-circle-analyse",
"type": "learnpath.LearningContent",
"translation_key": "ec97ed44-a2ee-46b4-b6ba-3cce4c6f627e",
"translation_key": "00ed0ab2-fdb0-4ee6-a7d2-42a219b849a8",
"minutes": 15,
"contents": [
{
"type": "video",
"type": "document",
"value": {
"description": "In dieser Circle zeigt dir ein Fachexperte anhand von Kundensituationen, wie du erfolgreichden Kundenbedarf ermitteln, analysieren, priorisieren und anschliessend zusammenfassen kannst.",
"url": "https://www.youtube.com/embed/qhPIfxS2hvI"
"description": "Beispiel Dokument",
"url": null
},
"id": "01ed1388-e82f-49a4-aafc-2d24891ec64a"
"id": "892a9a4a-8e1e-4f7e-8c35-9bf3bbe5371b"
}
]
},
{
"id": 421,
"id": 383,
"title": "Beobachten",
"slug": "beobachten",
"slug": "test-lehrgang-lp-circle-analyse-ls-beobachten",
"type": "learnpath.LearningSequence",
"translation_key": "8fed5f78-2d39-4a78-9dfc-f65551a81a7b",
"translation_key": "4cb08bc2-d101-43cc-b006-8f2bbb1a0579",
"icon": "it-icon-ls-watch"
},
{
"id": 422,
"title": "Absicherung der Familie",
"slug": "absicherung-der-familie",
"id": 384,
"title": "Fahrzeug",
"slug": "test-lehrgang-lp-circle-analyse-lu-fahrzeug",
"type": "learnpath.LearningUnit",
"translation_key": "fe50e509-b679-40f8-bddf-844c473e1e8a",
"translation_key": "8f4afa40-c27e-48f7-a2d7-0e713479a55e",
"course_category": {
"id": 15,
"title": "Fahrzeug",
"general": false
},
"children": [
{
"id": 423,
"title": "Ich bin in der Lage, mit geeigneten Fragestellungen die Deckung von Versicherungen zu erfassen.",
"slug": "ich-bin-in-der-lage-mit-geeigneten-fragestellungen-die-deckung-von-versicherungen-zu-erfassen",
"type": "learnpath.LearningUnitQuestion",
"translation_key": "7a1631e9-56b2-48fd-b9ff-1eafba9f96da"
"id": 397,
"title": "Innerhalb des Handlungsfelds \u00abFahrzeug\u00bb bin ich f\u00e4hig, die Ziele und Pl\u00e4ne des Kunden zu ergr\u00fcnden (SOLL).",
"slug": "test-lehrgang-competence-crit-y13-fahrzeug",
"type": "competence.PerformanceCriteria",
"translation_key": "e9d49552-7d18-418a-94b6-ebb4ee6bf187",
"competence_id": "Y1.3"
},
{
"id": 424,
"title": "Zweite passende Frage zu 'Absicherung der Familie'",
"slug": "zweite-passende-frage-zu-absicherung-der-familie",
"type": "learnpath.LearningUnitQuestion",
"translation_key": "f5aea045-f428-4b06-8b51-1857626250a8"
"id": 398,
"title": "Innerhalb des Handlungsfelds \u00abFahrzeug\u00bb bin ich f\u00e4hig, die IST-Situation des Kunden mit der geeigneten Gespr\u00e4chs-/Fragetechnik zu erfassen.",
"slug": "test-lehrgang-competence-crit-y21-fahrzeug",
"type": "competence.PerformanceCriteria",
"translation_key": "5f257b35-c6ca-49e4-9401-a5d02d53926d",
"competence_id": "Y2.1"
}
]
},
{
"id": 425,
"title": "Ermittlung des Kundenbedarfs",
"slug": "ermittlung-des-kundenbedarfs-14",
"id": 385,
"title": "Rafael Fasel wechselt sein Auto",
"slug": "test-lehrgang-lp-circle-analyse-lc-rafael-fasel-wechselt-sein-auto",
"type": "learnpath.LearningContent",
"translation_key": "ffd613f5-830c-4bc0-860b-fc194e2d7d1c",
"translation_key": "fda4f870-9307-414d-b07f-eea607a9afb7",
"minutes": 30,
"contents": [
{
"type": "podcast",
"type": "online_training",
"value": {
"description": "Die Ermittlung des Kundenbedarfs muss in einem eingehenden Gespr\u00e4ch herausgefunden werden. H\u00f6re dazu auch diesen Podcast an.",
"url": "https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/325190984&color=%23ff5500&auto_play=false&hide_related=false&show_comments=true&show_user=true&show_reposts=false&show_teaser=true&visual=true"
"description": "In diesem Online-Training lernst du, wie du den Kundenbedarf ermittelst.",
"url": ""
},
"id": "642c0906-3bd3-4030-be3f-8b1acce08930"
"id": "700a0f64-0892-4fa5-9e08-3bd34e99edeb"
}
]
},
{
"id": 426,
"title": "Kundenbed\u00fcrfnisse erkennen",
"slug": "kundenbed\u00fcrfnisse-erkennen-7",
"id": 386,
"title": "Fachcheck Fahrzeug",
"slug": "test-lehrgang-lp-circle-analyse-lc-fachcheck-fahrzeug",
"type": "learnpath.LearningContent",
"translation_key": "b36bd615-053c-4054-a5be-080005140a98",
"translation_key": "dce0847f-4593-4bba-bd0c-a09c71eb0344",
"minutes": 30,
"contents": [
{
"type": "competence",
"type": "test",
"value": {
"description": "Beispiel Kompetenz"
"description": "Beispiel Test",
"url": null
},
"id": "6b85361b-cc27-4454-aa20-72b31ad92a3f"
"id": "9f674aaa-ebf0-4a01-adcc-c0c46394fb10"
}
]
},
{
"id": 427,
"title": "Was braucht eine Familie?",
"slug": "was-braucht-eine-familie-7",
"id": 387,
"title": "Reisen",
"slug": "test-lehrgang-lp-circle-analyse-lu-reisen",
"type": "learnpath.LearningUnit",
"translation_key": "c3f6d33f-8dbc-4d88-9a81-3c602c4f9cc8",
"course_category": {
"id": 16,
"title": "Reisen",
"general": false
},
"children": [
{
"id": 399,
"title": "Innerhalb des Handlungsfelds \u00abReisen\u00bb bin ich f\u00e4hig, die Ziele und Pl\u00e4ne des Kunden zu ergr\u00fcnden (SOLL).",
"slug": "test-lehrgang-competence-crit-y13-reisen",
"type": "competence.PerformanceCriteria",
"translation_key": "1e488b69-8a3e-4acc-9547-48c103e0d038",
"competence_id": "Y1.3"
}
]
},
{
"id": 388,
"title": "Reiseversicherung",
"slug": "test-lehrgang-lp-circle-analyse-lc-reiseversicherung",
"type": "learnpath.LearningContent",
"translation_key": "b4d2ec6c-12b3-48bc-b159-f5b9e06637cf",
"minutes": 60,
"translation_key": "ff513aae-efe1-4974-b67f-7a292b8aef86",
"minutes": 240,
"contents": [
{
"type": "exercise",
"value": {
"description": "Beispiel Aufgabe",
"url": "/static/media/web_based_trainings/story-01-a-01-patrizia-marco-sichern-sich-ab-einstieg/scormcontent/index.html"
"description": "Beispiel \u00dcbung",
"url": null
},
"id": "b7e661b1-9e39-4482-8b23-c24dad1ef648"
"id": "f35f213e-1a33-49fe-97c5-26e15161719f"
}
]
},
{
"id": 428,
"title": "Reisen",
"slug": "reisen",
"type": "learnpath.LearningUnit",
"translation_key": "07a52671-a50e-46fc-b685-947aadb3e4d4",
"children": [
{
"id": 429,
"title": "Passende Frage zu \"Reisen\"",
"slug": "passende-frage-zu-reisen",
"type": "learnpath.LearningUnitQuestion",
"translation_key": "00491fd6-f1f5-4a52-b13d-0197bc875296"
}
]
},
{
"id": 430,
"title": "Reiseversicherung",
"slug": "reiseversicherung-7",
"id": 389,
"title": "Emma und Ayla campen durch Amerika",
"slug": "test-lehrgang-lp-circle-analyse-lc-emma-und-ayla-campen-durch-amerika",
"type": "learnpath.LearningContent",
"translation_key": "08dacac1-1853-4e07-8a6d-b7e2ee610398",
"minutes": 240,
"contents": [
{
"type": "competence",
"value": {
"description": "Beispiel Kompetenz"
},
"id": "6532e206-8737-45d9-9c2a-3ad44c372449"
}
]
},
{
"id": 431,
"title": "Sorgenfrei reisen",
"slug": "sorgenfrei-reisen-7",
"type": "learnpath.LearningContent",
"translation_key": "d8e9ec02-cae6-4494-91e0-707591456afb",
"translation_key": "a77b0f9d-9a70-47bd-8e62-7580d70a4306",
"minutes": 120,
"contents": [
{
"type": "exercise",
"value": {
"description": "Beispiel Aufgabe",
"description": "Beispiel \u00dcbung",
"url": "/static/media/web_based_trainings/story-06-a-01-emma-und-ayla-campen-durch-amerika-einstieg/scormcontent/index.html"
},
"id": "87333833-ad07-4c86-a846-46232668e8e1"
"id": "60f087ff-fa3a-4da2-820f-4fcdf449f70d"
}
]
},
{
"id": 432,
"id": 390,
"title": "Beenden",
"slug": "beenden",
"slug": "test-lehrgang-lp-circle-analyse-ls-beenden",
"type": "learnpath.LearningSequence",
"translation_key": "a3ee459e-ab98-483f-95c9-ba85eb10c105",
"translation_key": "06f1e998-b827-41cc-8129-d72d731719c1",
"icon": "it-icon-ls-end"
},
{
"id": 433,
"id": 391,
"title": "Kompetenzprofil anschauen",
"slug": "kompetenzprofil-anschauen-9",
"slug": "test-lehrgang-lp-circle-analyse-lc-kompetenzprofil-anschauen",
"type": "learnpath.LearningContent",
"translation_key": "0e16dd46-14cf-43ac-888f-f03beded7fa1",
"translation_key": "6cc47dc1-a74f-4cbf-afa6-23885891c82f",
"minutes": 30,
"contents": [
{
"type": "document",
"value": {
"description": "Beispiel Kompetenz"
"description": "Beispiel Dokument",
"url": null
},
"id": "4b729c72-aee8-4944-b5fb-d0bfd317a339"
"id": "3f685055-4e3e-4ca9-93af-bac19236931d"
}
]
},
{
"id": 434,
"id": 392,
"title": "Circle \"Analyse\" abschliessen",
"slug": "circle-analyse-abschliessen-9",
"slug": "test-lehrgang-lp-circle-analyse-lc-circle-analyse-abschliessen",
"type": "learnpath.LearningContent",
"translation_key": "53703784-c71f-4bad-a3e7-e014f0fded12",
"translation_key": "9b32e2cd-1368-4885-a79b-906b45ba04bc",
"minutes": 30,
"contents": [
{
"type": "document",
"value": {
"description": "Beispiel Kompetenz"
"description": "Beispiel Dokument",
"url": null
},
"id": "8bc53dfd-bb9b-4ae5-bd3c-74b7eef0eafd"
"id": "650b7b15-b522-4df7-ac5b-6a654f12334f"
}
]
}
@ -317,25 +300,25 @@
"job_situations": [
{
"type": "job_situation",
"value": "Absicherung der Familie",
"id": "f715a46f-53df-4205-8257-30cff62f337c"
"value": "Autoversicherung",
"id": "c5a6b365-0a18-47d5-b6e1-6cb8b8ec7d35"
},
{
"type": "job_situation",
"value": "Reisen",
"id": "f2174789-eab4-4059-961d-699b3c333110"
"value": "Autokauf",
"id": "e969d2a2-b383-482c-a721-88552af086a6"
}
],
"goals": [
{
"type": "goal",
"value": "... die heutige Versicherungssituation von Privat- oder Gesch\u00e4ftskunden einzusch\u00e4tzen.",
"id": "41acaebc-38de-4929-a4af-aaed43a1e5f3"
"id": "d9ad8aed-d7d6-42c7-b6d4-65102c8ddf10"
},
{
"type": "goal",
"value": "... deinem Kunden seine optimale L\u00f6sung aufzuzeigen",
"id": "cb1d556b-dac1-4edc-a3e5-97307b49c55c"
"id": "2506950c-45cb-474f-acb9-45e83e9ebe1b"
}
],
"experts": [
@ -348,9 +331,14 @@
"photo": null,
"biography": ""
},
"id": "479878e7-2d30-46a4-8d6b-bfe77268bbae"
"id": "b7b0ff2e-f840-4d74-99c1-c7a5ee6dc14e"
}
]
}
]
}
],
"course": {
"id": -1,
"title": "Test Lerngang",
"category_name": "Handlungsfeld"
}
}

View File

@ -16,7 +16,7 @@ def main():
)
response = client.get(
'http://localhost:8000/api/learnpath/page/unit-test-lernpfad/',
'http://localhost:8000/api/course/page/test-lehrgang-lp/',
)
print(response.status_code)
print(response.json())

View File

@ -1,13 +1,14 @@
import type {
CircleChild,
CircleCompletion,
CircleGoal,
CircleJobSituation,
CourseCompletion,
CourseCompletionStatus,
CourseWagtailPage,
LearningContent,
LearningSequence,
LearningUnit,
LearningUnitQuestion,
LearningWagtailPage,
} from '@/types'
import type { LearningPath } from '@/services/learningPath'
@ -23,6 +24,7 @@ function _createEmptyLearningUnit(parentLearningSequence: LearningSequence): Lea
parentLearningSequence: parentLearningSequence,
children: [],
last: true,
completion_status: 'unknown',
}
}
@ -109,10 +111,10 @@ export function parseLearningSequences (circle: Circle, children: CircleChild[])
return result;
}
export class Circle implements LearningWagtailPage {
export class Circle implements CourseWagtailPage {
readonly type = 'learnpath.Circle';
readonly learningSequences: LearningSequence[];
readonly completed: boolean;
completion_status: CourseCompletionStatus = 'unknown'
nextCircle?: Circle;
previousCircle?: Circle;
@ -129,7 +131,6 @@ export class Circle implements LearningWagtailPage {
public readonly parentLearningPath?: LearningPath,
) {
this.learningSequences = parseLearningSequences(this, this.children);
this.completed = false;
}
public static fromJson(json: any, learningPath?: LearningPath): Circle {
@ -187,7 +188,7 @@ export class Circle implements LearningWagtailPage {
public someFinishedInLearningSequence(translationKey: string): boolean {
if (translationKey) {
return this.flatChildren.filter((lc) => {
return lc.completed && lc.parentLearningSequence?.translation_key === translationKey;
return lc.completion_status === 'success' && lc.parentLearningSequence?.translation_key === translationKey;
}).length > 0;
}
@ -197,7 +198,7 @@ export class Circle implements LearningWagtailPage {
public allFinishedInLearningSequence(translationKey: string): boolean {
if (translationKey) {
const finishedContents = this.flatChildren.filter((lc) => {
return lc.completed && lc.parentLearningSequence?.translation_key === translationKey;
return lc.completion_status === 'success' && lc.parentLearningSequence?.translation_key === translationKey;
}).length;
const totalContents = this.flatChildren.filter((lc) => {
@ -209,15 +210,15 @@ export class Circle implements LearningWagtailPage {
return false;
}
public parseCompletionData(completionData: CircleCompletion[]) {
public parseCompletionData(completionData: CourseCompletion[]) {
this.flatChildren.forEach((page) => {
const pageIndex = completionData.findIndex((e) => {
return e.page_key === page.translation_key;
});
if (pageIndex >= 0) {
page.completed = completionData[pageIndex].completed;
page.completion_status = completionData[pageIndex].completion_status;
} else {
page.completed = undefined;
page.completion_status = 'unknown';
}
});

View File

@ -1,22 +1,30 @@
import * as _ from 'lodash'
import type { CircleCompletion, LearningContent, LearningPathChild, LearningWagtailPage, Topic } from '@/types'
import type {
CourseCompletion,
CourseCompletionStatus,
CourseWagtailPage,
LearningContent,
LearningPathChild,
Topic,
} from '@/types'
import { Circle } from '@/services/circle'
function getLastCompleted(learningPathKey: string, completionData: CircleCompletion[]) {
return _.orderBy(completionData, ['updated_at'], 'desc').find((c: CircleCompletion) => {
return c.completed && c.learning_path_key === learningPathKey && c.page_type === 'learnpath.LearningContent'
function getLastCompleted(courseId: number, completionData: CourseCompletion[]) {
return _.orderBy(completionData, ['updated_at'], 'desc').find((c: CourseCompletion) => {
return c.completion_status === 'success' && c.course === courseId && c.page_type === 'learnpath.LearningContent'
})
}
export class LearningPath implements LearningWagtailPage {
export class LearningPath implements CourseWagtailPage {
readonly type = 'learnpath.LearningPath'
public topics: Topic[]
public circles: Circle[]
public nextLearningContent?: LearningContent
readonly completion_status: CourseCompletionStatus = 'unknown'
public static fromJson(json: any, completionData: CircleCompletion[]): LearningPath {
return new LearningPath(json.id, json.slug, json.title, json.translation_key, json.children, completionData)
public static fromJson(json: any, completionData: CourseCompletion[]): LearningPath {
return new LearningPath(json.id, json.slug, json.title, json.translation_key, json.course.id, json.children, completionData)
}
constructor(
@ -24,8 +32,9 @@ export class LearningPath implements LearningWagtailPage {
public readonly slug: string,
public readonly title: string,
public readonly translation_key: string,
public readonly courseId: number,
public children: LearningPathChild[],
completionData?: any
completionData?: CourseCompletion[]
) {
// parse children
this.topics = []
@ -42,7 +51,9 @@ export class LearningPath implements LearningWagtailPage {
}
if (page.type === 'learnpath.Circle') {
const circle = Circle.fromJson(page, this)
circle.parseCompletionData(completionData)
if (completionData) {
circle.parseCompletionData(completionData)
}
if (topic) {
topic.circles.push(circle)
}
@ -59,17 +70,21 @@ export class LearningPath implements LearningWagtailPage {
this.topics.push(topic)
}
this.calcNextLearningContent(completionData)
if (completionData) {
this.calcNextLearningContent(completionData)
}
}
public calcNextLearningContent(completionData: CircleCompletion[]): void {
public calcNextLearningContent(completionData: CourseCompletion[]): void {
this.nextLearningContent = undefined
const lastCompletedLearningContent = getLastCompleted(this.translation_key, completionData)
const lastCompletedLearningContent = getLastCompleted(this.courseId, completionData)
if (lastCompletedLearningContent) {
const lastCircle = this.circles.find(
(circle) => circle.translation_key === lastCompletedLearningContent.circle_key
(circle) => {
return circle.flatLearningContents.find((learningContent) => learningContent.translation_key === lastCompletedLearningContent.page_key)
}
)
if (lastCircle) {
const lastLearningContent = lastCircle.flatLearningContents.find(

View File

@ -2,13 +2,14 @@ import * as log from 'loglevel'
import { defineStore } from 'pinia'
import type { LearningContent, LearningUnit, LearningUnitQuestion } from '@/types'
import type { CourseCompletionStatus, LearningContent, LearningUnit, LearningUnitQuestion } from '@/types'
import type { Circle } from '@/services/circle'
import { itPost } from '@/fetchHelpers'
import { useLearningPathStore } from '@/stores/learningPath'
export type CircleStoreState = {
circle: Circle | undefined
page: 'INDEX' | 'OVERVIEW'
}
export const useCircleStore = defineStore({
@ -16,6 +17,7 @@ export const useCircleStore = defineStore({
state: () => {
return {
circle: undefined,
page: 'INDEX',
} as CircleStoreState;
},
getters: {
@ -61,12 +63,12 @@ export const useCircleStore = defineStore({
return learningUnit
},
async markCompletion(page: LearningContent | LearningUnitQuestion, flag = true) {
async markCompletion(page: LearningContent | LearningUnitQuestion, completion_status:CourseCompletionStatus='success') {
try {
page.completed = flag;
const completionData = await itPost('/api/completion/circle/mark/', {
page.completion_status = completion_status;
const completionData = await itPost('/api/course/completion/mark/', {
page_key: page.translation_key,
completed: page.completed,
completion_status: page.completion_status,
});
if (this.circle) {
this.circle.parseCompletionData(completionData);
@ -100,10 +102,10 @@ export const useCircleStore = defineStore({
},
calcSelfEvaluationStatus(learningUnit: LearningUnit) {
if (learningUnit.children.length > 0) {
if (learningUnit.children.every((q) => q.completed)) {
if (learningUnit.children.every((q) => q.completion_status === 'success')) {
return true;
}
if (learningUnit.children.some((q) => q.completed !== undefined)) {
if (learningUnit.children.some((q) => q.completion_status === 'fail')) {
return false;
}
}
@ -111,7 +113,7 @@ export const useCircleStore = defineStore({
},
continueFromLearningContent(currentLearningContent: LearningContent) {
if (currentLearningContent) {
this.markCompletion(currentLearningContent, true);
this.markCompletion(currentLearningContent, 'success');
const nextLearningContent = currentLearningContent.nextLearningContent;
const currentParent = currentLearningContent.parentLearningUnit;

View File

@ -4,6 +4,7 @@ import { LearningPath } from '@/services/learningPath'
export type LearningPathStoreState = {
learningPath: LearningPath | undefined
page: 'INDEX' | 'OVERVIEW'
}
export const useLearningPathStore = defineStore({
@ -11,6 +12,7 @@ export const useLearningPathStore = defineStore({
state: () => {
return {
learningPath: undefined,
page: 'INDEX',
} as LearningPathStoreState;
},
getters: {},
@ -19,8 +21,8 @@ export const useLearningPathStore = defineStore({
if (this.learningPath && !reload) {
return this.learningPath;
}
const learningPathData = await itGet(`/api/learnpath/page/${slug}/`);
const completionData = await itGet(`/api/completion/learning_path/${learningPathData.translation_key}/`);
const learningPathData = await itGet(`/api/course/page/${slug}/`);
const completionData = await itGet(`/api/course/completion/${learningPathData.course.id}/`);
if (!learningPathData) {
throw `No learning path found with: ${slug}`;

View File

@ -0,0 +1,39 @@
import { defineStore } from 'pinia'
import { itGet } from '@/fetchHelpers'
import type { MediaLibraryPage } from '@/types'
export type MediaCenterStoreState = {
mediaCenterPage: MediaLibraryPage | undefined
selectedLearningPath: { id: number; name: string }
availableLearningPaths: { id: number; name: string }[]
}
export const useMediaCenterStore = defineStore({
id: 'mediaCenter',
state: () => {
return {
mediaCenterPage: undefined,
selectedLearningPath: { id: 1, name: 'Alle Lehrgänge' },
availableLearningPaths: [
{ id: 1, name: 'Alle Lehrgänge' },
{ id: 2, name: 'Versicherungsvermittler/in' },
],
} as MediaCenterStoreState
},
getters: {},
actions: {
async loadMediaCenterPage(slug: string, reload = false) {
if (this.mediaCenterPage && !reload) {
return this.mediaCenterPage
}
const mediaCenterPageData = await itGet(`/api/course/page/${slug}/`)
if (!mediaCenterPageData) {
throw `No mediaCenterPageData found with: ${slug}`
}
this.mediaCenterPage = mediaCenterPageData
return this.mediaCenterPage
},
},
})

View File

@ -1,8 +1,17 @@
import type { Circle } from '@/services/circle'
export type LearningContentType = 'assignment' | 'book' | 'document' |
'exercise' | 'media_library' | 'online_training' |
'resource' | 'test' | 'video';
export type CourseCompletionStatus = 'unknown' | 'fail' | 'success'
export type LearningContentType =
| 'assignment'
| 'book'
| 'document'
| 'exercise'
| 'media_library'
| 'online_training'
| 'resource'
| 'test'
| 'video'
export interface LearningContentBlock {
type: LearningContentType
@ -106,15 +115,15 @@ export interface CircleJobSituation {
id: string;
}
export interface LearningWagtailPage {
export interface CourseWagtailPage {
readonly id: number;
readonly title: string;
readonly slug: string;
readonly translation_key: string;
completed?: boolean;
completion_status: CourseCompletionStatus;
}
export interface LearningContent extends LearningWagtailPage {
export interface LearningContent extends CourseWagtailPage {
type: 'learnpath.LearningContent';
minutes: number;
contents: (AssignmentBlock | BookBlock | DocumentBlock | ExerciseBlock | MediaLibraryBlock | OnlineTrainingBlock | ResourceBlock | TestBlock | VideoBlock)[];
@ -125,13 +134,13 @@ export interface LearningContent extends LearningWagtailPage {
previousLearningContent?: LearningContent;
}
export interface LearningUnitQuestion extends LearningWagtailPage {
export interface LearningUnitQuestion extends CourseWagtailPage {
type: 'learnpath.LearningUnitQuestion';
parentLearningSequence?: LearningSequence;
parentLearningUnit?: LearningUnit;
}
export interface LearningUnit extends LearningWagtailPage {
export interface LearningUnit extends CourseWagtailPage {
type: 'learnpath.LearningUnit';
learningContents: LearningContent[];
minutes: number;
@ -140,7 +149,7 @@ export interface LearningUnit extends LearningWagtailPage {
last?: boolean;
}
export interface LearningSequence extends LearningWagtailPage {
export interface LearningSequence extends CourseWagtailPage {
type: 'learnpath.LearningSequence';
icon: string;
learningUnits: LearningUnit[];
@ -149,13 +158,13 @@ export interface LearningSequence extends LearningWagtailPage {
export type CircleChild = LearningContent | LearningUnit | LearningSequence | LearningUnitQuestion;
export interface WagtailCircle extends LearningWagtailPage {
export interface WagtailCircle extends CourseWagtailPage {
type: 'learnpath.Circle';
children: CircleChild[];
description: string;
}
export interface Topic extends LearningWagtailPage {
export interface Topic extends CourseWagtailPage {
type: 'learnpath.Topic';
is_visible: boolean;
circles: Circle[];
@ -163,17 +172,17 @@ export interface Topic extends LearningWagtailPage {
export type LearningPathChild = Topic | WagtailCircle;
export interface CircleCompletion {
export interface CourseCompletion {
id: number;
created_at: string;
updated_at: string;
user: number;
page_key: string;
page_type: string;
circle_key: string;
learning_path_key: string;
completed: boolean;
json_data: any;
page_slug: string;
course: number;
completion_status: CourseCompletionStatus;
additional_json_data: any;
}
export interface CircleDiagramData {
@ -186,3 +195,62 @@ export interface CircleDiagramData {
arrowEndAngle: number
done: boolean
}
export interface Course {
id: number;
name: string;
category_name: string;
}
export interface CourseCategory {
id: number;
name: string;
general: boolean;
}
export interface MediaDocument {
type: "Documents";
value: number;
id: string;
}
export interface MediaLink {
type: "Links";
id: string;
value: {
title: string;
description: string;
link_display_text: string;
url: string;
}
}
export interface MediaContentCollection {
type: "content_collection";
value: {
title: string;
contents: (MediaDocument | MediaLink)[];
}
}
export interface MediaCategoryPage extends CourseWagtailPage {
type: 'media_library.MediaCategoryPage';
overview_icon: string;
introduction_text: string;
description_title: string;
description_text: string;
items: {
type: 'item';
value: string;
id: string;
}
course_category: CourseCategory;
body: MediaContentCollection[];
}
export interface MediaLibraryPage extends CourseWagtailPage {
type: 'media_library.MediaLibraryPage';
course: Course;
children: MediaCategoryPage[];
}

View File

@ -3,7 +3,6 @@ import * as log from 'loglevel'
import LearningSequence from '@/components/circle/LearningSequence.vue'
import CircleOverview from '@/components/circle/CircleOverview.vue'
import CircleDiagram from '@/components/circle/CircleDiagram.vue'
import LearningContent from '@/components/circle/LearningContent.vue'
import { computed, onMounted } from 'vue'
import { useCircleStore } from '@/stores/circle'
@ -65,10 +64,7 @@ onMounted(async () => {
/>
</Teleport>
<Transition mode="out-in">
<div v-if="circleStore.page === 'LEARNING_CONTENT'">
<LearningContent :key="circleStore.currentLearningContent.translation_key" />
</div>
<div v-else>
<div>
<div class="circle-container bg-gray-200">
<div class="circle max-w-9xl">
<div class="flex flex-col lg:flex-row">

View File

@ -16,7 +16,7 @@ const userStore = useUserStore()
<div class="mt-8 p-8 break-words bg-white max-w-xl">
<h3>Versicherungsvermittler/in</h3>
<div class="mt-4">
<router-link class="btn-blue" to="/learn/versicherungsvermittlerin"> Weiter geht's </router-link>
<router-link class="btn-blue" to="/learn/versicherungsvermittlerin-lp"> Weiter geht's </router-link>
</div>
</div>
</main>

View File

@ -0,0 +1,31 @@
<script setup lang="ts">
import { useRouter } from 'vue-router';
const router = useRouter()
</script>
<template>
<Teleport to="body">
<div class="px-16 fixed top-0 overflow-y-scroll bg-white h-full w-full">
<div class="-mx-16 pt-4 pb-24 px-16 mb-20 bg-gray-200">
<nav>
<a
class="block my-9 cursor-pointer flex items-center"
@click="router.go(-1)"><it-icon-arrow-left /><span>zurück</span></a>
</nav>
<slot name="header"></slot>
</div>
<slot name="body"></slot>
</div>
</Teleport>
</template>
<style scoped>
.it-icon-hf {
color: blue
}
.it-icon-hf > * {
@apply m-auto;
}
</style>

View File

@ -4,7 +4,7 @@ import { onMounted, reactive, watch } from 'vue'
import { useCircleStore } from '@/stores/circle'
import { useAppStore } from '@/stores/app'
import LearningContent from '@/components/circle/LearningContent.vue'
import { LearningContent as LearningContentType } from '@/types'
import type { LearningContent as LearningContentType } from '@/types'
log.debug('LearningContentView created')

View File

@ -7,7 +7,7 @@ import { useUserStore } from '@/stores/user'
import LearningPathDiagram from '@/components/circle/LearningPathDiagram.vue'
import LearningPathViewVertical from '@/views/LearningPathViewVertical.vue'
import { LearningPath } from '@/services/learningPath'
import type { LearningPath } from '@/services/learningPath'
log.debug('LearningPathView created')
@ -28,7 +28,7 @@ onMounted(async () => {
}
})
const createContinueUrl = (learningPath: LearningPath) => {
const createContinueUrl = (learningPath: LearningPath): [string, boolean] => {
if (learningPath.nextLearningContent) {
const circle = learningPath.nextLearningContent.parentCircle
const lsShortSlug = learningPath.nextLearningContent.parentLearningSequence?.slug.replace(`${circle.slug}-`, '')
@ -77,14 +77,14 @@ const createContinueUrl = (learningPath: LearningPath) => {
class="bg-white m-6 lg:m-12 p-8 flex flex-col lg:flex-row divide-y lg:divide-y-0 lg:divide-x divide-gray-500 justify-start"
>
<div class="p-4 lg:p-8 flex-auto">
<h2 translate>Willkommmen zurück, {{ userStore.first_name }}</h2>
<h2>Willkommmen zurück, {{ userStore.first_name }}</h2>
<p class="mt-4 text-xl"></p>
</div>
<div class="p-4 lg:p-8 flex-2" v-if="learningPathStore.learningPath.nextLearningContent" translate>
<div class="p-4 lg:p-8 flex-2" v-if="learningPathStore.learningPath.nextLearningContent">
Nächster Schritt
<h3>
{{ learningPathStore.learningPath.nextLearningContent.parentCircle.title }}:
{{ learningPathStore.learningPath.nextLearningContent.parentLearningSequence.title }}
{{ learningPathStore.learningPath.nextLearningContent.parentLearningSequence?.title }}
</h3>
<router-link
class="mt-4 btn-blue"

View File

@ -0,0 +1,263 @@
<script setup lang="ts">
import * as log from 'loglevel'
import LinkCard from '@/components/mediaCenter/LinkCard.vue'
import HandlungsfeldLayout from '@/views/HandlungsfeldLayout.vue'
import MediaLink from '@/components/mediaCenter/MediaLink.vue'
import { useMediaCenterStore } from '@/stores/mediaCenter'
import { computed } from 'vue'
const field = {
title: 'Fahrzeug',
description:
'Das Auto ist für viele der grösste Stolz! Es birgt aber auch ein grosses Gefahrenpotenzial. Dabei geht es bei den heutigen Fahrzeugpreisen und Reparaturkosten rasch um namhafte Summen, die der Fahrzeugbesitzer und die Fahrzeugbesitzerin in einem grösseren Schadenfall oft nur schwer selbst aufbringen kann.',
icon: '/static/icons/demo/icon-hf-fahrzeug-big.svg',
summary: {
text: 'In diesem berufstypischem Handlungsfeld lernst du alles rund um Motorfahrzeugversicherungen, wie man sein Auto optimal schützen kann, wie du vorgehst bei einem Fahrzeugwechsel, welche Aspekte du bei einer Offerte beachten musst und wie du dem Kunden die Lösung präsentierst.',
items: ['Motorfahrzeughaftpflichtversicherung', 'Motorfahrzeugkaskoversicherung', 'Insassenunfallversicherung'],
},
items: [
{
title: 'Lernmedien',
type: 'learnmedia',
moreLink: '',
items: [
{
title: 'Die Motorfahrzeughaftpflicht',
description: 'Buch «Sach- und Vermögensversicherungen» Kapitel 16',
iconUrl: '/static/icons/demo/icon-hf-book.png',
linkText: 'PDF anzeigen',
link: '/static/media/documents/01a_Motorfahrzeughaftpflicht.pdf',
openWindow: true,
},
{
title: 'Die Motorfahrzeughaftpflicht',
iconUrl: '/static/icons/demo/icon-hf-book.png',
description: 'Buch «Sach- und Vermögensversicherungen» Kapitel 16',
linkText: 'PDF anzeigen',
link: '/static/media/documents/01a_Motorfahrzeughaftpflicht.pdf',
openWindow: true,
},
{
title: 'Die Motorfahrzeughaftpflicht',
iconUrl: '/static/icons/demo/icon-hf-book.png',
description: 'Buch «Sach- und Vermögensversicherungen» Kapitel 16',
linkText: 'PDF anzeigen',
link: '/static/media/documents/01a_Motorfahrzeughaftpflicht.pdf',
openWindow: true,
},
{
title: 'Die Motorfahrzeughaftpflicht',
iconUrl: '/static/icons/demo/icon-hf-book.png',
description: 'Buch «Sach- und Vermögensversicherungen» Kapitel 16',
linkText: 'PDF anzeigen',
link: '/static/media/documents/01a_Motorfahrzeughaftpflicht.pdf',
openWindow: true,
},
{
title: 'Die Motorfahrzeughaftpflicht',
iconUrl: '/static/icons/demo/icon-hf-book.png',
description: 'Buch «Sach- und Vermögensversicherungen» Kapitel 16',
linkText: 'PDF anzeigen',
link: '/static/media/documents/01a_Motorfahrzeughaftpflicht.pdf',
openWindow: true,
},
],
},
{
title: 'Links',
type: 'externalLinks',
moreLink: '',
items: [
{
title: 'Nationales Versicherungsbüro',
iconUrl: '',
description: '',
linkText: 'Link öffnen',
link: 'https://www.nbi-ngf.ch/h',
openWindow: true,
},
{
title: 'Adressen der Strassenverkehrsämter',
iconUrl: '',
description: '',
linkText: 'Link öffnen',
link: 'https://asa.ch/strassenverkehrsaemter/adressen/',
openWindow: true,
},
],
},
{
title: 'Verankerung im Lernpfad',
type: 'internalLinks',
moreLink: '',
items: [
{
title: 'Circle: Einstieg Lernsequenz: Anwenden',
iconUrl: '',
description: '',
linkText: 'Lerineinheit anzeigen',
link: 'http://localhost:8000/learn/versicherungsvermittlerin/versicherungsvermittlerin-circle-analyse',
openWindow: false,
},
{
title: 'Circle: Einstieg Lernsequenz: Anwenden',
iconUrl: '',
description: '',
linkText: 'Lerineinheit anzeigen',
link: 'http://localhost:8000/learn/versicherungsvermittlerin/versicherungsvermittlerin-circle-analyse',
openWindow: false,
},
{
title: 'Circle: Einstieg Lernsequenz: Anwenden',
iconUrl: '',
description: '',
linkText: 'Lerineinheit anzeigen',
link: 'http://localhost:8000/learn/versicherungsvermittlerin/versicherungsvermittlerin-circle-analyse',
openWindow: false,
},
{
title: 'Circle: Einstieg Lernsequenz: Anwenden',
iconUrl: '',
description: '',
linkText: 'Lerineinheit anzeigen',
link: 'http://localhost:8000/learn/versicherungsvermittlerin/versicherungsvermittlerin-circle-analyse',
openWindow: false,
},
],
},
{
title: 'Querverweise',
type: 'realtiveLinks',
moreLink: '',
items: [
{
title: 'Rechtsstreigkeiten',
iconUrl: '/static/icons/demo/icon-hf-einkommenssicherung.svg',
description: 'Lernmedium: Verkehrsrechtsschutz Buch «Sach- und Vermögensversicherungen/Kapitel 12.3»',
linkText: 'Handlungsfeldanzeigen',
link: 'http://localhost:8000/mediacenter/handlungsfeld',
openWindow: false,
},
{
title: 'Rechtsstreigkeiten',
iconUrl: '/static/icons/demo/icon-hf-einkommenssicherung.svg',
description: 'Lernmedium: Verkehrsrechtsschutz Buch «Sach- und Vermögensversicherungen/Kapitel 12.3»',
linkText: 'Handlungsfeldanzeigen',
link: 'http://localhost:8000/mediacenter/handlungsfeld',
openWindow: false,
},
],
},
],
}
const props = defineProps<{
mediaCategorySlug: string
}>()
log.debug('MediaCategoryDetailView created', props.mediaCategorySlug)
const mediaStore = useMediaCenterStore()
const mediaCategory = computed(() => {
return mediaStore.mediaCenterPage?.children.find((category) => category.slug === props.mediaCategorySlug)
})
const maxCardItems = 4
const maxListItems = 6
const displayAsCard = (itemType: string): boolean => {
return itemType === 'learnmedia' || itemType === 'realtiveLinks'
}
const hasMoreItems = (items: object[], maxItems: number): boolean => {
return items.length > maxItems
}
const getMaxDisplayItems = (items: object[], maxItems: number) => {
return items.slice(0, maxItems)
}
const getMaxDisplayItemsForType = (itemType: string, items: object[]) => {
return displayAsCard(itemType) ? getMaxDisplayItems(items, maxCardItems) : getMaxDisplayItems(items, maxListItems)
}
const hasMoreItemsForType = (itemType: string, items: object[]) => {
const maxItems = displayAsCard(itemType) ? maxCardItems : maxListItems
return hasMoreItems(items, maxItems)
}
</script>
<template>
<Teleport to="body" v-if="mediaStore.mediaCenterPage && mediaCategory">
<HandlungsfeldLayout>
<template #header>
<div class="flex justify-between">
<div class="w-5/12">
<h3 class="font-normal text-large mb-3">Handlungsfeld</h3>
<h1 class="mb-4 lg:mb-8">{{ mediaCategory.title }}</h1>
<p>{{ mediaCategory.introduction_text }}</p>
</div>
<img class="w-5/12" :src="field.icon" />
</div>
</template>
<template #body>
<section class="mb-20">
<h2 class="mb-4">{{ mediaCategory.description_title }}</h2>
<p class="mb-4 lg:w-2/3">{{ mediaCategory.description_text }}</p>
<ul>
<li v-for="item in mediaCategory.items" :key="item" class="mb-2 h-10 leading-10 flex items-center">
<span class="text-sky-500 bg-[url('/static/icons/icon-check.svg')] bg-no-repeat h-10 w-10 mr-2"></span>
{{ item.value }}
</li>
</ul>
</section>
<section class="mb-20" v-for="item in field.items" :key="item.title">
<h2 class="mb-4">{{ item.title }}</h2>
<ul
:class="{
'grid gap-4 grid-cols-1 lg:grid-cols-2': displayAsCard(item.type),
'border-t': !displayAsCard(item.type),
'mb-6': hasMoreItemsForType(item.type, item.items),
}"
>
<li v-for="subItem in getMaxDisplayItemsForType(item.type, item.items)" :key="subItem.link">
<LinkCard
v-if="displayAsCard(item.type)"
:title="subItem.title"
:icon="subItem.iconUrl"
:description="subItem.description"
:url="subItem.link"
:link-text="subItem.linkText"
:open-window="subItem.openWindow"
/>
<div v-else class="flex items-center justify-between border-b py-4">
<h4 class="text-bold">{{ subItem.title }}</h4>
<media-link :blank="subItem.openWindow" :to="subItem.link" class="link">{{
subItem.linkText
}}</media-link>
</div>
</li>
</ul>
<router-link
v-if="hasMoreItemsForType(item.type, item.items)"
to="/mediacenter/handlungsfeldlist"
class="flex items-center"
>
<span>Alle anschauen</span>
<it-icon-arrow-right></it-icon-arrow-right>
</router-link>
</section>
</template>
</HandlungsfeldLayout>
</Teleport>
</template>
<style scoped>
.it-icon-hf {
color: blue;
}
.it-icon-hf > * {
@apply m-auto;
}
</style>

View File

@ -0,0 +1,92 @@
<script setup lang="ts">
import * as log from 'loglevel'
import { ref, watch } from 'vue'
import { useMediaCenterStore } from '@/stores/mediaCenter'
log.debug('HandlungsfelderOverview created')
const fields = [
{
name: 'Fahrzeug',
icon: 'icon-hf-fahrzeug',
},
{
name: 'Reisen',
icon: 'icon-hf-reisen',
},
{
name: 'Einkommenssicherung',
icon: 'icon-hf-einkommenssicherung',
},
{
name: 'Gesundheit',
icon: 'icon-hf-fahrzeug',
},
{
name: 'Haushalt',
icon: 'icon-hf-reisen',
},
{
name: 'Sparen',
icon: 'icon-hf-einkommenssicherung',
},
{
name: 'Pensionierung',
icon: 'icon-hf-fahrzeug',
},
{
name: 'KMU',
icon: 'icon-hf-reisen',
},
{
name: 'Wohneigentum',
icon: 'icon-hf-einkommenssicherung',
},
{
name: 'Rechtsstreitigkeiten',
icon: 'icon-hf-fahrzeug',
},
{
name: 'Erben / Vererben',
icon: 'icon-hf-reisen',
},
{
name: 'Selbstständigkeit',
icon: 'icon-hf-einkommenssicherung',
},
]
const mediaStore = useMediaCenterStore()
const dropdownSelected = ref(mediaStore.selectedLearningPath)
watch(dropdownSelected, (newValue) =>
mediaStore.$patch({
selectedLearningPath: newValue,
})
)
</script>
<template>
<div class="mx-auto max-w-5xl">
<div class="flex flex-col lg:flex-row items-center justify-between mb-10">
<h1>Handlungsfelder</h1>
<!-- <ItDropdownSelect v-model="dropdownSelected" :items="mediaStore.availableLearningPaths"></ItDropdownSelect>-->
</div>
<div v-if="mediaStore.mediaCenterPage">
<ul class="grid gap-5 grid-cols-1 lg:grid-cols-4">
<li class="bg-white p-4" v-for="cat in mediaStore.mediaCenterPage.children" :key="cat.id">
<router-link :to="`/mediacenter/${mediaStore.mediaCenterPage.slug}/handlungsfelder/${cat.slug}`">
<img class="m-auto" :src="`/static/icons/demo/${cat.overview_icon}.svg`" />
<h3 class="text-base text-center">{{ cat.title }}</h3>
</router-link>
</li>
</ul>
</div>
</div>
</template>
<style scoped>
.it-icon-hf > * {
@apply m-auto;
}
</style>

View File

@ -0,0 +1,50 @@
<script setup lang="ts">
import * as log from 'loglevel'
import OverviewCard from '@/components/mediaCenter/OverviewCard.vue'
import { ref, watch } from 'vue'
import { useMediaCenterStore } from '@/stores/mediaCenter'
log.debug('MediaMainView created')
const mediaStore = useMediaCenterStore()
const dropdownSelected = ref(mediaStore.selectedLearningPath)
watch(dropdownSelected, (newValue) =>
mediaStore.$patch({
selectedLearningPath: newValue,
})
)
</script>
<template>
<div class="mx-auto max-w-5xl">
<div class="flex flex-col lg:flex-row items-center justify-between mb-10">
<h1>Mediathek</h1>
<!-- <ItDropdownSelect-->
<!-- v-model="dropdownSelected"-->
<!-- :items="mediaStore.availableLearningPaths"></ItDropdownSelect>-->
</div>
<OverviewCard
v-if="mediaStore.mediaCenterPage"
title="Handlungsfelder"
call2-action="Anschauen"
:link="`/mediacenter/${mediaStore.mediaCenterPage.slug}/handlungsfelder`"
description="Finde alle Ressourcen der Handlungsfelder wie Lernmedien, Links und andere nützliche Informationen."
icon="handlungsfelder-overview"
class="mb-6"
>
</OverviewCard>
<OverviewCard
v-if="mediaStore.mediaCenterPage"
title="Lernmedien"
call2-action="Anschauen"
:link="`/mediacenter/${mediaStore.mediaCenterPage.slug}/lernmedien`"
description="Finde eine vollständige Liste der Bücher und anderen Medien, auf die im Kurs verwiesen wird."
icon="lernmedien-overview"
class="mb-6"
>
</OverviewCard>
</div>
</template>
<style scoped></style>

View File

@ -0,0 +1,42 @@
<script setup lang="ts">
import * as log from 'loglevel'
import { onMounted } from 'vue'
import { useMediaCenterStore } from '@/stores/mediaCenter'
log.debug('MediaCenterView created')
const props = defineProps<{
mediaCenterPageSlug: string
}>()
const mediaCenterStore = useMediaCenterStore()
onMounted(async () => {
log.debug('MediaCenterView mounted', props.mediaCenterPageSlug)
try {
await mediaCenterStore.loadMediaCenterPage(props.mediaCenterPageSlug)
} catch (error) {
log.error(error)
}
})
</script>
<template>
<div class="bg-gray-200">
<nav class="h-12 px-6 py-2 border-b border-gray-500 bg-white">
<ul class="flex">
<li>Übersicht</li>
<li class="ml-10">Handlungsfelder</li>
<li class="ml-10">Allgemeines zu Versicherungen</li>
<li class="ml-10">Lernmedien</li>
<li class="ml-10"><a href="https://www.vbv.ch/de/der-vbv/lernen-lehren/lexikon">Lexikon</a></li>
</ul>
</nav>
<main class="px-8 py-8">
<router-view></router-view>
</main>
</div>
</template>
<style scoped></style>

View File

@ -0,0 +1,89 @@
<script setup lang="ts">
import HandlungsfeldLayout from '@/views/HandlungsfeldLayout.vue'
import MediaLink from '@/components/mediaCenter/MediaLink.vue'
const data = {
title: 'Fahrzeug: Lernmedien',
items: [
{
title: 'Die Motorfahrzeughaftpflicht',
description: 'Buch «Sach- und Vermögensversicherungen» Kapitel 16',
iconUrl: '/static/icons/demo/icon-hf-book.png',
linkText: 'PDF anzeigen',
link: '/static/media/documents/01a_Motorfahrzeughaftpflicht.pdf',
openWindow: true,
},
{
title: 'Die Motorfahrzeughaftpflicht',
iconUrl: '/static/icons/demo/icon-hf-book.png',
description: 'Buch «Sach- und Vermögensversicherungen» Kapitel 16',
linkText: 'PDF anzeigen',
link: '/static/media/documents/01a_Motorfahrzeughaftpflicht.pdf',
openWindow: true,
},
{
title: 'Die Motorfahrzeughaftpflicht',
iconUrl: '/static/icons/demo/icon-hf-book.png',
description: 'Buch «Sach- und Vermögensversicherungen» Kapitel 16',
linkText: 'PDF anzeigen',
link: '/static/media/documents/01a_Motorfahrzeughaftpflicht.pdf',
openWindow: true,
},
{
title: 'Die Motorfahrzeughaftpflicht',
iconUrl: '/static/icons/demo/icon-hf-book.png',
description: 'Buch «Sach- und Vermögensversicherungen» Kapitel 16',
linkText: 'PDF anzeigen',
link: '/static/media/documents/01a_Motorfahrzeughaftpflicht.pdf',
openWindow: true,
},
{
title: 'Die Motorfahrzeughaftpflicht',
iconUrl: '/static/icons/demo/icon-hf-book.png',
description: 'Buch «Sach- und Vermögensversicherungen» Kapitel 16',
linkText: 'PDF anzeigen',
link: '/static/media/documents/01a_Motorfahrzeughaftpflicht.pdf',
openWindow: true,
},
],
}
</script>
<template>
<Teleport to="body">
<HandlungsfeldLayout>
<template #header>
<h1 class="mb-4">{{ data.title }}</h1>
</template>
<template #body>
<section class="mb-20">
<ul class="border-t">
<li v-for="item in data.items" :key="item.link" class="flex items-center justify-between border-b py-4">
<div class="flex items-center justify-between">
<div v-if="item.iconUrl">
<img class="mr-6 max-h-[70px]" :src="item.iconUrl" />
</div>
<div>
<h4 class="text-bold">{{ item.title }}</h4>
<p v-if="item.description" class="mb-2">{{ item.description }}</p>
</div>
</div>
<div class="">
<media-link :to="item.link" :blank="item.openWindow" class="link">{{ item.linkText }}</media-link>
</div>
</li>
</ul>
</section>
</template>
</HandlungsfeldLayout>
</Teleport>
</template>
<style scoped>
.it-icon-hf {
color: blue;
}
.it-icon-hf > * {
@apply m-auto;
}
</style>

View File

@ -1,15 +0,0 @@
<script setup lang="ts">
import * as log from 'loglevel';
log.debug('ShopView created');
</script>
<template>
<main class="px-8 py-8">
<h1>Mediathek</h1>
</main>
</template>
<style scoped>
</style>

View File

@ -1,10 +1,10 @@
<script setup lang="ts">
import { reactive } from 'vue'
import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/vue'
import ItCheckbox from '@/components/ui/ItCheckbox.vue'
import ItDropdown from '@/components/ui/ItDropdown.vue'
import IconLogout from '@/components/icons/IconLogout.vue'
import IconSettings from '@/components/icons/IconSettings.vue'
import ItDropdownSelect from '@/components/ui/ItDropdownSelect.vue'
const state = reactive({
checkboxValue: true,
@ -20,7 +20,10 @@ const state = reactive({
{ id: 9, name: 'Claudie Smitham' },
{ id: 10, name: 'Emil Schaefer' },
],
dropdownSelected: { id: 8 },
dropdownSelected: {
id: -1,
name: 'Select a name',
},
})
const dropdownData = [
@ -211,7 +214,6 @@ function log(data: any) {
lc-video
<it-icon-lc-video />
</div>
</div>
<div class="mt-8 mb-8 flex flex-col gap-4 flex-wrap lg:flex-row">
@ -334,57 +336,8 @@ function log(data: any) {
<h2 class="mt-8 mb-8">Dropdown (Work-in-progress)</h2>
<Listbox as="div" v-model="state.dropdownSelected">
<div class="mt-1 relative w-128">
<ListboxButton
class="bg-white relative w-full border border-gray-500 pl-5 pr-10 py-3 text-left cursor-default font-bold"
>
<span class="block truncate">{{ state.dropdownSelected.name }}</span>
<span class="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
<it-icon-arrow-down class="h-5 w-5" aria-hidden="true" />
</span>
</ListboxButton>
<transition
leave-active-class="transition ease-in duration-100"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<ListboxOptions
class="absolute z-10 mt-1 w-full bg-white shadow-lg max-h-60 py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm"
>
<ListboxOption
as="template"
v-for="person in state.dropdownValues"
:key="person.id"
:value="person"
v-slot="{ active, selected }"
>
<li
:class="[
active ? 'text-white bg-blue-900' : 'text-black',
'cursor-default select-none relative py-2 pl-3 pr-9',
]"
>
<span :class="[state.dropdownSelected ? 'font-semibold' : 'font-normal', 'block truncate']">
{{ person.name }}
</span>
<span
v-if="state.dropdownSelected"
:class="[
active ? 'text-white' : 'text-blue-900',
'absolute inset-y-0 right-0 flex items-center pr-4',
]"
>
<it-icon-check class="h-5 w-5" aria-hidden="true" />
</span>
</li>
</ListboxOption>
</ListboxOptions>
</transition>
</div>
</Listbox>
<ItDropdownSelect v-model="state.dropdownSelected" :items="state.dropdownValues"> </ItDropdownSelect>
{{ state.dropdownSelected }}
<h2 class="mt-8 mb-8">Checkbox</h2>

View File

@ -20,6 +20,8 @@ module.exports = {
'9xl': '96rem',
},
backgroundImage: {
'handlungsfelder-overview': "url('/static/icons/icon-handlungsfelder-overview.svg')",
'lernmedien-overview': "url('/static/icons/icon-lernmedien-overview.svg')",
}
},
colors: colors,
@ -27,6 +29,9 @@ module.exports = {
safelist: [
{ pattern: /bg-(blue|sky|green|red|orange|yellow|stone|gray|slate)-(200|300|400|500|600|700|800|900)/, },
'it-icon',
'bg-handlungsfelder-overview',
'bg-lernmedien-overview',
],
plugins: [
require('@tailwindcss/typography'),

View File

@ -5,62 +5,49 @@ describe("circle page", () => {
cy.manageCommand("cypress_reset");
login("admin", "test");
cy.visit("/learn/unit-test-lernpfad/unit-test-circle");
cy.visit("/learn/test-lehrgang-lp/analyse");
});
it("can open circle page", () => {
cy.get('[data-cy="circle-title"]').should("contain", "Unit-Test Circle");
cy.get('[data-cy="circle-title"]').should("contain", "Analyse");
});
it("can toggle learning content", () => {
cy.get('[data-cy="circle-title"]').should("contain", "Unit-Test Circle");
cy.get('[data-cy="circle-title"]').should("contain", "Analyse");
cy.get(
'[data-cy="unit-test-lernpfad-circle-unit-test-circle-lc-ermittlung-des-kundenbedarfs"] > .cy-checkbox'
'[data-cy="test-lehrgang-lp-circle-analyse-lc-einleitung-circle-analyse"] > .cy-checkbox'
).click();
cy.get(
'[data-cy="unit-test-lernpfad-circle-unit-test-circle-lc-ermittlung-des-kundenbedarfs"] > .cy-checkbox-checked'
'[data-cy="test-lehrgang-lp-circle-analyse-lc-einleitung-circle-analyse"] > .cy-checkbox-checked'
).should("have.class", "cy-checkbox-checked");
// completion data should still be there after reload
cy.reload();
cy.get(
'[data-cy="unit-test-lernpfad-circle-unit-test-circle-lc-ermittlung-des-kundenbedarfs"] > .cy-checkbox-checked'
'[data-cy="test-lehrgang-lp-circle-analyse-lc-einleitung-circle-analyse"] > .cy-checkbox-checked'
).should("have.class", "cy-checkbox-checked");
});
it("can open learning contents and complete them by continuing", () => {
cy.get(
'[data-cy="unit-test-lernpfad-circle-unit-test-circle-lc-ermittlung-des-kundenbedarfs"]'
'[data-cy="test-lehrgang-lp-circle-analyse-lc-rafael-fasel-wechselt-sein-auto"]'
).click();
cy.get('[data-cy="ln-title"]').should(
"contain",
"Ermittlung des Kundenbedarfs"
"Rafael Fasel wechselt sein Auto"
);
cy.get('[data-cy="complete-and-continue"]').click();
cy.get('[data-cy="ln-title"]').should(
"contain",
"Kundenbedürfnisse erkennen"
);
cy.get('[data-cy="complete-and-continue"]').click();
cy.get('[data-cy="ln-title"]').should(
"contain",
"Was braucht eine Familie"
);
cy.get('[data-cy="ln-title"]').should("contain", "Fachcheck Fahrzeug");
cy.get('[data-cy="complete-and-continue"]').click();
cy.get(
'[data-cy="unit-test-lernpfad-circle-unit-test-circle-lc-ermittlung-des-kundenbedarfs"] > .cy-checkbox-checked'
'[data-cy="test-lehrgang-lp-circle-analyse-lc-rafael-fasel-wechselt-sein-auto"] > .cy-checkbox-checked'
).should("have.class", "cy-checkbox-checked");
cy.get(
'[data-cy="unit-test-lernpfad-circle-unit-test-circle-lc-kundenbedürfnisse-erkennen"] > .cy-checkbox-checked'
).should("have.class", "cy-checkbox-checked");
cy.get(
'[data-cy="unit-test-lernpfad-circle-unit-test-circle-lc-was-braucht-eine-familie"] > .cy-checkbox-checked'
'[data-cy="test-lehrgang-lp-circle-analyse-lc-fachcheck-fahrzeug"] > .cy-checkbox-checked'
).should("have.class", "cy-checkbox-checked");
});
@ -70,7 +57,7 @@ describe("circle page", () => {
cy.get('[data-cy="ln-title"]').should(
"contain",
'Einleitung Circle "Unit-Test Circle"'
'Einleitung Circle "Analyse"'
);
cy.get('[data-cy="complete-and-continue"]').click();
@ -78,17 +65,15 @@ describe("circle page", () => {
cy.get('[data-cy="ls-continue-button"]').click();
cy.get('[data-cy="ln-title"]').should(
"contain",
"Ermittlung des Kundenbedarfs"
"Rafael Fasel wechselt sein Auto"
);
});
it("can open learning content by url", () => {
cy.visit(
"/learn/unit-test-lernpfad/unit-test-circle/ermittlung-des-kundenbedarfs"
);
cy.get('[data-cy="ln-title"]').should(
"contain",
"Ermittlung des Kundenbedarfs"
);
cy.visit("/learn/test-lehrgang-lp/analyse/reiseversicherung");
cy.get('[data-cy="ln-title"]').should("contain", "Reiseversicherung");
cy.get('[data-cy="close-learning-content"]').click();
cy.get('[data-cy="circle-title"]').should("contain", "Analyse");
});
});

View File

@ -7,7 +7,7 @@ describe("learningPath page", () => {
it("can open learningPath page", () => {
login("admin", "test");
cy.visit("/learn/versicherungsvermittlerin");
cy.visit("/learn/versicherungsvermittlerin-lp");
cy.get('[data-cy="learning-path-title"]').should(
"contain",
@ -17,30 +17,30 @@ describe("learningPath page", () => {
it("click on circle on learningPath page will open circle", () => {
login("admin", "test");
cy.visit("/learn/versicherungsvermittlerin");
cy.visit("/learn/test-lehrgang-lp");
cy.get('[data-cy="circle-analyse"]').click({ force: true });
cy.url().should("include", "/learn/versicherungsvermittlerin/analyse");
cy.url().should("include", "/learn/test-lehrgang-lp/analyse");
cy.get('[data-cy="circle-title"]').should("contain", "Analyse");
});
it("open listView and click on circle will open circle", () => {
login("admin", "test");
cy.visit("/learn/versicherungsvermittlerin");
cy.visit("/learn/test-lehrgang-lp");
cy.get('[data-cy="show-list-view"]').click();
cy.get('[data-cy="full-screen-modal"]').should("be.visible");
cy.get('[data-cy="circle-analyse-vertical"]').click({ force: true });
cy.url().should("include", "/learn/versicherungsvermittlerin/analyse");
cy.url().should("include", "/learn/test-lehrgang-lp/analyse");
cy.get('[data-cy="circle-title"]').should("contain", "Analyse");
});
it("weiter gehts button will open next circle", () => {
login("admin", "test");
cy.visit("/learn/unit-test-lernpfad");
cy.visit("/learn/test-lehrgang-lp");
// first click will open first circle
cy.get('[data-cy="lp-continue-button"]').should("contain", "Los geht's");
@ -49,15 +49,15 @@ describe("learningPath page", () => {
cy.get('[data-cy="back-to-learning-path-button"]').click();
// mark a learning content in second circle
cy.get('[data-cy="circle-unit-test-circle"]').click({ force: true });
cy.get('[data-cy="circle-analyse"]').click({ force: true });
cy.get(
'[data-cy="unit-test-lernpfad-circle-unit-test-circle-lc-reiseversicherung"] > .cy-checkbox'
'[data-cy="test-lehrgang-lp-circle-analyse-lc-fachcheck-fahrzeug"] > .cy-checkbox'
).click();
cy.get('[data-cy="back-to-learning-path-button"]').click();
// click on continue should go to unit-test-circle
cy.get('[data-cy="lp-continue-button"]').should("contain", "Weiter geht's");
cy.get('[data-cy="lp-continue-button"]').click();
cy.get('[data-cy="circle-title"]').should("contain", "Unit-Test Circle");
cy.get('[data-cy="circle-title"]').should("contain", "Analyse");
});
});

View File

@ -31,7 +31,7 @@ describe("login", () => {
});
it("login will redirect to requestet page", () => {
cy.visit("/learn/versicherungsvermittlerin");
cy.visit("/learn/versicherungsvermittlerin-lp");
cy.get("h1").should("contain", "Login");
cy.get("#username").type("admin");

View File

@ -62,8 +62,7 @@ if [ "$SKIP_SETUP" = false ]; then
python3 server/manage.py createcachetable --settings="$DJANGO_SETTINGS_MODULE"
python3 server/manage.py migrate --settings="$DJANGO_SETTINGS_MODULE"
python3 server/manage.py create_default_users --settings="$DJANGO_SETTINGS_MODULE"
python3 server/manage.py create_default_learning_path --settings="$DJANGO_SETTINGS_MODULE"
python3 server/manage.py create_default_media_library --settings="$DJANGO_SETTINGS_MODULE"
python3 server/manage.py create_default_courses --settings="$DJANGO_SETTINGS_MODULE"
# make django translations
(cd server && python3 manage.py compilemessages --settings="$DJANGO_SETTINGS_MODULE")

View File

@ -103,8 +103,9 @@ THIRD_PARTY_APPS = [
LOCAL_APPS = [
"vbv_lernwelt.core",
"vbv_lernwelt.sso",
"vbv_lernwelt.course",
"vbv_lernwelt.learnpath",
"vbv_lernwelt.completion",
"vbv_lernwelt.competence",
"vbv_lernwelt.media_library",
]
# https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
@ -514,7 +515,7 @@ if "django_redis.cache.RedisCache" in env("IT_DJANGO_CACHE_BACKEND", default="")
},
}
CACHES["learning_path_cache"] = {
CACHES["api_page_cache"] = {
"BACKEND": "django.core.cache.backends.db.DatabaseCache",
"LOCATION": "django_cache_learning_path",
}

View File

@ -10,14 +10,12 @@ from wagtail import urls as wagtail_urls
from wagtail.admin import urls as wagtailadmin_urls
from wagtail.documents import urls as wagtaildocs_urls
from vbv_lernwelt.completion.views import request_learning_path_completion, request_circle_completion, \
mark_circle_completion
from vbv_lernwelt.core.middleware.auth import django_view_authentication_exempt
from vbv_lernwelt.core.views import (
rate_limit_exceeded_view,
permission_denied_view,
check_rate_limit, cypress_reset_view, vue_home, vue_login, me_user_view, vue_logout, generate_web_component_icons, )
from vbv_lernwelt.learnpath.views import page_api_view
from vbv_lernwelt.course.views import page_api_view, request_course_completion, mark_course_completion
def raise_example_error(request):
@ -47,13 +45,10 @@ urlpatterns = [
# core
re_path(r"server/core/icons/$", generate_web_component_icons, name="generate_web_component_icons"),
# learnpath
path(r"api/learnpath/page/<slug:slug>/", page_api_view, name="page_api_view"),
# completion
path(r"api/completion/circle/<uuid:circle_key>/", request_circle_completion, name="request_circle_completion"),
path(r"api/completion/learning_path/<uuid:learning_path_key>/", request_learning_path_completion, name="request_learning_path_completion"),
path(r"api/completion/circle/mark/", mark_circle_completion, name="mark_circle_completion"),
# course
path(r"api/course/page/<slug:slug>/", page_api_view, name="page_api_view"),
path(r"api/course/completion/mark/", mark_course_completion, name="mark_course_completion"),
path(r"api/course/completion/<course_id>/", request_course_completion, name="request_course_completion"),
# testing and debug
path('server/raise_error/', user_passes_test(lambda u: u.is_superuser, login_url='/login/')(raise_example_error), ),

View File

@ -1,6 +1,6 @@
from django.apps import AppConfig
class CompletionConfig(AppConfig):
class MediaLibraryConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'vbv_lernwelt.completion'
name = 'vbv_lernwelt.competence'

View File

@ -0,0 +1,145 @@
from vbv_lernwelt.competence.factories import CompetenceProfilePageFactory, PerformanceCriteriaFactory, \
CompetencePageFactory
from vbv_lernwelt.competence.models import CompetencePage
from vbv_lernwelt.course.consts import COURSE_VERSICHERUNGSVERMITTLERIN_ID
from vbv_lernwelt.course.models import CoursePage, Course
from vbv_lernwelt.learnpath.models import LearningUnit
def create_default_competence_profile():
course = Course.objects.get(id=COURSE_VERSICHERUNGSVERMITTLERIN_ID)
course_page = CoursePage.objects.get(course_id=COURSE_VERSICHERUNGSVERMITTLERIN_ID)
competence_profile_page = CompetenceProfilePageFactory(
title='Kompetenzprofil',
parent=course_page,
)
competences = [{
'competence_id': 'A1',
'title': 'Weiterempfehlung für Neukunden generieren',
'items': [
'Verhandlungsgeschick',
'Überzeugtes Auftreten',
],
}, {
'competence_id': 'A2',
'title': 'Kundengespräche vereinbaren',
'items': [
'Gesprächsführung / Fragetechniken',
'Selbstorganisation',
'Arbeitstechniken',
'Psychologische Kenntnisse / Kommunikations-psychologie',
],
}, {
'competence_id': 'A3',
'title': 'Auftritt in den sozialen Medien zeitgemäss halten',
'items': [
'Gesetzliche und Compliance-Anforderungen der Versicherer',
'Datenschutzgesetz',
'Kommunikation in den sozialen Medien',
]
}, {
'competence_id': 'A4',
'title': 'Kundendaten erfassen',
'items': []
}, {
'competence_id': 'B1',
'title': 'Wünsche, Ziele und Bedürfnisse der Kunden im Gespräch ermitteln',
'items': [
'Gesprächsführung',
'Fragetechniken',
'Kundenpsychologie',
]
}, {
'competence_id': 'B2',
'title': 'Analyse des Kundenbedarfs und des Kundenbedürfnisses durchführen',
'items': [
'Fragetechniken',
'Visuelle Hilfsmittel / Visualisierungstechniken',
]
}, {
'competence_id': 'B3',
'title': 'Individuelle Lösungsvorschläge erarbeiten',
'items': [
'Fundierte Produktekenntnisse',
'Regulatorische Vorschriften',
]
}, {
'competence_id': 'B4',
'title': 'Lösungsvorschläge präsentieren und umsetzen',
'items': [
'Verhandlungsstrategien',
'Fundierte Produktkenntnisse',
'Visuelle Hilfsmittel / Visualisierungstechniken',
]
}, {
'competence_id': 'C1',
'title': 'Cross- und Upselling; bestehende fremdverwaltete Versicherungspolicen prüfen und in das Portfolio aufnehmen',
'items': [
'Produktkenntnisse',
'Gesprächsführung',
'Kommunikation',
'Fragetechnik',
'Verhandlungsgeschick',
'Vertragsrecht',
'Regulatorische Vorgaben',
'UVG, BVG, KVG, VVG',
]
}, {
'competence_id': 'C2',
'title': 'Änderungswünsche entgegennehmen und bestehende Verträge anpassen',
'items': [
'Produktkenntnisse',
'Gesprächsführung',
'Kommunikation',
'Fragetechnik',
'Verhandlungsgeschick',
'Vertragsrecht',
'Regulatorische Vorgaben',
'UVG, BVG, KVG, VVG',
]
}, {
'competence_id': 'C3',
'title': 'Kunden im Schadenfall unterstützen',
'items': []
}, {
'competence_id': 'C4',
'title': 'Bestehende Kunden pflegen',
'items': []
}, {
'competence_id': 'C5',
'title': 'Versicherungsanträge nachbearbeiten',
'items': []
}]
for c in competences:
CompetencePageFactory(
parent=competence_profile_page,
competence_id=c['competence_id'],
title=c['title'],
items=[
('item', i) for i in c['items']
]
)
PerformanceCriteriaFactory(
parent=CompetencePage.objects.get(competence_id='B1'),
competence_id='B1.3',
title='Innerhalb des Handlungsfelds «Fahrzeug» bin ich fähig, die Ziele und Pläne des Kunden zu ergründen (SOLL).',
learning_unit=LearningUnit.objects.get(slug='versicherungsvermittlerin-lp-circle-analyse-lu-fahrzeug'),
)
PerformanceCriteriaFactory(
parent=CompetencePage.objects.get(competence_id='B2'),
competence_id='B2.1',
title='Innerhalb des Handlungsfelds «Fahrzeug» bin ich fähig, die IST-Situation des Kunden mit der geeigneten Gesprächs-/Fragetechnik zu erfassen.',
learning_unit=LearningUnit.objects.get(slug='versicherungsvermittlerin-lp-circle-analyse-lu-fahrzeug'),
)
PerformanceCriteriaFactory(
parent=CompetencePage.objects.get(competence_id='B2'),
competence_id='B2.2',
title='Innerhalb des Handlungsfelds «Fahrzeug» bin ich fähig, die Risiken aufzuzeigen.',
learning_unit=LearningUnit.objects.get(slug='versicherungsvermittlerin-lp-circle-analyse-lu-fahrzeug'),
)

View File

@ -0,0 +1,26 @@
import wagtail_factories
from vbv_lernwelt.competence.models import CompetenceProfilePage, PerformanceCriteria, CompetencePage
class CompetenceProfilePageFactory(wagtail_factories.PageFactory):
title = 'Kompetenzprofil'
class Meta:
model = CompetenceProfilePage
class CompetencePageFactory(wagtail_factories.PageFactory):
competence_id = 'A1'
title = 'Weiterempfehlung für Neukunden generieren'
class Meta:
model = CompetencePage
class PerformanceCriteriaFactory(wagtail_factories.PageFactory):
competence_id = 'A1.1'
title = 'Bestehende Kunden so zu beraten, dass sie von diesen weiterempfohlen werden'
class Meta:
model = PerformanceCriteria

View File

@ -0,0 +1,9 @@
# -*- coding: utf-8 -*-
#
# Iterativ GmbH
# http://www.iterativ.ch/
#
# Copyright (c) 2015 Iterativ GmbH. All rights reserved.
#
# Created on 2022-08-18
# @author: lorenz.padberg@iterativ.ch

View File

@ -0,0 +1,9 @@
# -*- coding: utf-8 -*-
#
# Iterativ GmbH
# http://www.iterativ.ch/
#
# Copyright (c) 2015 Iterativ GmbH. All rights reserved.
#
# Created on 2022-08-18
# @author: lorenz.padberg@iterativ.ch

View File

@ -1,22 +1,34 @@
# Generated by Django 3.2.13 on 2022-08-18 12:14
# Generated by Django 3.2.13 on 2022-09-28 12:51
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import wagtail.blocks
import wagtail.fields
class Migration(migrations.Migration):
initial = True
dependencies = [
('wagtailcore', '0069_log_entry_jsonfield'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('taggit', '0004_alter_taggeditem_content_type_alter_taggeditem_tag'),
('media_library', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Category',
name='CompetencePage',
fields=[
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')),
('competence_id', models.TextField(default='A1')),
('items', wagtail.fields.StreamField([('item', wagtail.blocks.TextBlock())], use_json_field=True)),
],
options={
'abstract': False,
},
bases=('wagtailcore.page',),
),
migrations.CreateModel(
name='CompetenceProfilePage',
fields=[
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')),
],
@ -26,17 +38,14 @@ class Migration(migrations.Migration):
bases=('wagtailcore.page',),
),
migrations.CreateModel(
name='TopCategory',
name='PerformanceCriteria',
fields=[
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')),
('competence_id', models.TextField(default='A1.1')),
],
options={
'abstract': False,
},
bases=('wagtailcore.page',),
),
migrations.RenameModel(
old_name='CustomDocument',
new_name='LibraryDocument',
),
]

View File

@ -0,0 +1,22 @@
# Generated by Django 3.2.13 on 2022-09-28 12:51
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('learnpath', '0001_initial'),
('competence', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='performancecriteria',
name='learning_unit',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='learnpath.learningunit'),
),
]

View File

@ -0,0 +1,95 @@
from django.db import models
from django.utils.text import slugify
from wagtail import blocks
from wagtail.admin.panels import FieldPanel
from wagtail.fields import StreamField
from wagtail.models import Page
from vbv_lernwelt.core.model_utils import find_available_slug
from vbv_lernwelt.learnpath.serializer_helpers import get_it_serializer_class
class CompetenceProfilePage(Page):
parent_page_types = ['course.CoursePage']
subpage_types = ['competence.CompetencePage']
content_panels = [
FieldPanel('title', classname="full title"),
]
def full_clean(self, *args, **kwargs):
self.slug = find_available_slug(slugify(f"{self.get_parent().slug}-competence", allow_unicode=True))
super(CompetenceProfilePage, self).full_clean(*args, **kwargs)
@classmethod
def get_serializer_class(cls):
return get_it_serializer_class(
cls, [
'id', 'title', 'slug', 'type', 'translation_key',
'course',
'children',
]
)
class CompetencePage(Page):
parent_page_types = ['competence.CompetenceProfilePage']
subpage_types = ['competence.PerformanceCriteria']
competence_id = models.TextField(default='A1')
items = StreamField([
('item', blocks.TextBlock()),
], use_json_field=True)
content_panels = [
FieldPanel('title'),
FieldPanel('competence_id'),
]
def full_clean(self, *args, **kwargs):
self.slug = find_available_slug(slugify(f"{self.get_parent().slug}-competence-{self.competence_id}", allow_unicode=True))
super(CompetencePage, self).full_clean(*args, **kwargs)
@classmethod
def get_serializer_class(cls):
return get_it_serializer_class(
cls, [
'id', 'title', 'slug', 'type', 'translation_key',
'children',
]
)
class PerformanceCriteria(Page):
parent_page_types = ['competence.CompetenceProfilePage']
competence_id = models.TextField(default='A1.1')
learning_unit = models.ForeignKey(
'learnpath.LearningUnit',
null=True,
blank=True,
on_delete=models.SET_NULL,
)
content_panels = [
FieldPanel('title'),
FieldPanel('competence_id'),
FieldPanel('learning_unit'),
]
def full_clean(self, *args, **kwargs):
profile_parent = self.get_ancestors().exact_type(CompetenceProfilePage).last()
if self.learning_unit and self.learning_unit.course_category:
self.slug = find_available_slug(slugify(f"{profile_parent.slug}-crit-{self.competence_id}-{self.learning_unit.course_category.title}", allow_unicode=True))
else:
self.slug = find_available_slug(slugify(f"{profile_parent.slug}-crit-{self.competence_id}", allow_unicode=True))
super(PerformanceCriteria, self).full_clean(*args, **kwargs)
@classmethod
def get_serializer_class(cls):
from vbv_lernwelt.competence.serializers import PerformanceCriteriaSerializer
return PerformanceCriteriaSerializer
def get_admin_display_title(self):
if self.learning_unit and self.learning_unit.course_category:
return f'{self.competence_id} ({self.learning_unit.course_category.title}) {self.draft_title[:30]}'
else:
return f'{self.competence_id} {self.draft_title[:30]}'

View File

@ -0,0 +1,35 @@
from rest_framework import serializers
from vbv_lernwelt.competence.models import PerformanceCriteria
from vbv_lernwelt.course.serializers import CourseCategorySerializer
from vbv_lernwelt.learnpath.models import LearningUnit
from vbv_lernwelt.learnpath.serializer_helpers import get_it_serializer_class
class PerformanceCriteriaSerializer(get_it_serializer_class(PerformanceCriteria, [
'id', 'title', 'slug', 'type', 'translation_key',
'competence_id', 'learning_unit', 'circle', 'course_category',
])):
learning_unit = serializers.SerializerMethodField()
circle = serializers.SerializerMethodField()
course_category = serializers.SerializerMethodField()
def get_learning_unit(self, obj):
learning_unit_serializer = get_it_serializer_class(LearningUnit, [
'id', 'title', 'slug', 'type', 'translation_key',
])
return learning_unit_serializer(obj.learning_unit).data
def get_circle(self, obj):
return obj.learning_unit.get_parent().specific.title
def get_course_category(self, obj):
if obj.learning_unit:
return CourseCategorySerializer(obj.learning_unit.course_category).data
return None
class PerformanceCriteriaLearningPathSerializer(get_it_serializer_class(PerformanceCriteria, [
'id', 'title', 'slug', 'type', 'translation_key', 'competence_id',
])):
pass

View File

@ -0,0 +1,9 @@
# -*- coding: utf-8 -*-
#
# Iterativ GmbH
# http://www.iterativ.ch/
#
# Copyright (c) 2015 Iterativ GmbH. All rights reserved.
#
# Created on 2022-08-16
# @author: lorenz.padberg@iterativ.ch

View File

@ -0,0 +1,25 @@
from rest_framework.test import APITestCase
from vbv_lernwelt.competence.models import CompetenceProfilePage
from vbv_lernwelt.core.admin import User
from vbv_lernwelt.core.create_default_users import create_default_users
from vbv_lernwelt.course.creators.test_course import create_test_course
class CompetenceAPITestCase(APITestCase):
def setUp(self) -> None:
create_default_users()
create_test_course()
self.user = User.objects.get(username='student')
self.client.login(username='student', password='test')
def test_get_learnpathPage(self):
slug = 'test-lehrgang-competence'
competence_profile = CompetenceProfilePage.objects.get(slug=slug)
response = self.client.get(f'/api/course/page/{slug}/')
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertEqual(competence_profile.title, data['title'])
self.assertEqual('Innerhalb des Handlungsfelds «Fahrzeug» bin ich fähig, die Ziele und Pläne des Kunden zu ergründen (SOLL).', data['children'][1]['children'][0]['title'])

View File

@ -1,36 +0,0 @@
# Generated by Django 3.2.13 on 2022-07-04 09:58
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='CircleCompletion',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('page_key', models.UUIDField()),
('page_type', models.CharField(blank=True, default='', max_length=255)),
('circle_key', models.UUIDField()),
('learning_path_key', models.UUIDField()),
('completed', models.BooleanField(default=False)),
('json_data', models.JSONField(blank=True, default=dict)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.AddConstraint(
model_name='circlecompletion',
constraint=models.UniqueConstraint(fields=('user', 'page_key'), name='unique_user_page_key'),
),
]

View File

@ -1,29 +0,0 @@
from django.db import models
from django.db.models import UniqueConstraint
from vbv_lernwelt.core.models import User
class CircleCompletion(models.Model):
# Page can either be a LearningContent or a LearningUnitQuestion for now
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
user = models.ForeignKey(User, on_delete=models.CASCADE)
# Page can either be a LearningContent or a LearningUnitQuestion for now
page_key = models.UUIDField()
page_type = models.CharField(max_length=255, default='', blank=True)
circle_key = models.UUIDField(blank=True, default='')
learning_path_key = models.UUIDField(blank=True, default='')
completed = models.BooleanField(default=False)
json_data = models.JSONField(default=dict, blank=True)
class Meta:
constraints = [
UniqueConstraint(
fields=['user', 'page_key', ],
name='unique_user_page_key'
)
]

View File

@ -1,13 +0,0 @@
from rest_framework import serializers
from vbv_lernwelt.completion.models import CircleCompletion
class CircleCompletionSerializer(serializers.ModelSerializer):
class Meta:
model = CircleCompletion
fields = [
'id', 'created_at', 'updated_at', 'user', 'page_key', 'page_type', 'circle_key',
'learning_path_key', 'completed', 'json_data',
]

View File

@ -1,49 +0,0 @@
import json
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.core.tests.helpers import create_locales_for_wagtail
from vbv_lernwelt.learnpath.models import LearningContent
from vbv_lernwelt.learnpath.tests.create_simple_test_learning_path import create_simple_test_learning_path
class CompletionApiTestCase(APITestCase):
def setUp(self) -> None:
create_locales_for_wagtail()
create_default_users()
create_simple_test_learning_path()
self.user = User.objects.get(username='student')
self.client.login(username='student', password='test')
def test_completeLearningContent_works(self):
learning_content = LearningContent.objects.get(title='Einleitung Circle "Unit-Test Circle"')
learning_content_key = str(learning_content.translation_key)
circle_key = str(learning_content.get_parent().translation_key)
mark_url = f'/api/completion/circle/mark/'
print(mark_url)
response = self.client.post(mark_url, {
'page_key': learning_content_key,
})
response_json = response.json()
print(json.dumps(response.json(), indent=2))
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response_json), 1)
self.assertEqual(response_json[0]['page_key'], learning_content_key)
self.assertEqual(response_json[0]['circle_key'], circle_key)
self.assertTrue(response_json[0]['completed'])
# test getting the circle data
response = self.client.get(f'/api/completion/circle/{circle_key}/')
response_json = response.json()
print(json.dumps(response.json(), indent=2))
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response_json), 1)
self.assertEqual(response_json[0]['page_key'], learning_content_key)
self.assertEqual(response_json[0]['circle_key'], circle_key)
self.assertTrue(response_json[0]['completed'])

View File

@ -1,70 +0,0 @@
import structlog
from rest_framework.decorators import api_view
from rest_framework.response import Response
from wagtail.models import Page
from vbv_lernwelt.completion.models import CircleCompletion
from vbv_lernwelt.completion.serializers import CircleCompletionSerializer
from vbv_lernwelt.learnpath.models import Circle, LearningPath
from vbv_lernwelt.learnpath.utils import get_wagtail_type
logger = structlog.get_logger(__name__)
@api_view(['GET'])
def request_circle_completion(request, circle_key):
response_data = CircleCompletionSerializer(
CircleCompletion.objects.filter(user=request.user, circle_key=circle_key),
many=True,
).data
return Response(status=200, data=response_data)
@api_view(['GET'])
def request_learning_path_completion(request, learning_path_key):
response_data = CircleCompletionSerializer(
CircleCompletion.objects.filter(user=request.user, learning_path_key=learning_path_key),
many=True,
).data
return Response(status=200, data=response_data)
@api_view(['POST'])
def mark_circle_completion(request):
page_key = request.data.get('page_key')
completed = request.data.get('completed', True)
page = Page.objects.get(translation_key=page_key, locale__language_code='de-CH')
page_type = get_wagtail_type(page.specific)
circle = Circle.objects.ancestor_of(page).first()
learning_path = LearningPath.objects.ancestor_of(page).first()
cc, created = CircleCompletion.objects.get_or_create(
user=request.user,
page_key=page_key,
circle_key=circle.translation_key,
learning_path_key=learning_path.translation_key,
)
cc.page_type = page_type
cc.completed = completed
cc.save()
response_data = CircleCompletionSerializer(
CircleCompletion.objects.filter(user=request.user, circle_key=circle.translation_key),
many=True,
).data
logger.debug(
'page completed',
label='completion_api',
circle_key=circle.translation_key,
circle_title=circle.title,
page_key=page_key,
page_type=page_type,
page_title=page.title,
user_id=request.user.id,
)
return Response(status=200, data=response_data)

View File

@ -1,17 +1,10 @@
import djclick as click
from vbv_lernwelt.completion.models import CircleCompletion
from vbv_lernwelt.learnpath.create_default_learning_path import create_default_learning_path, \
delete_default_learning_path
from vbv_lernwelt.course.models import CourseCompletion
@click.command()
@click.option('--reset-learning-path', default=False)
def command(reset_learning_path):
def command():
print("cypress reset data")
if reset_learning_path:
delete_default_learning_path()
create_default_learning_path(skip_locales=True)
CircleCompletion.objects.all().delete()
CourseCompletion.objects.all().delete()

View File

@ -0,0 +1,34 @@
from wagtail.models import Page
def find_available_slug(requested_slug, ignore_page_id=None):
"""
Finds an available slug within the specified parent.
If the requested slug is not available, this adds a number on the end, for example:
- 'requested-slug'
- 'requested-slug-1'
- 'requested-slug-2'
And so on, until an available slug is found.
The `ignore_page_id` keyword argument is useful for when you are updating a page,
you can pass the page being updated here so the page's current slug is not
treated as in use by another page.
"""
pages = Page.objects.filter(slug__startswith=requested_slug)
if ignore_page_id:
pages = pages.exclude(id=ignore_page_id)
existing_slugs = set(pages.values_list("slug", flat=True))
slug = requested_slug
number = 1
while slug in existing_slugs:
slug = requested_slug + "-" + str(number)
number += 1
return slug

View File

View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

View File

@ -0,0 +1,13 @@
from django.apps import AppConfig
class CourseConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'vbv_lernwelt.course'
def ready(self):
try:
# pylint: disable=unused-import,import-outside-toplevel
import vbv_lernwelt.course.signals # noqa F401
except ImportError:
pass

View File

@ -0,0 +1,2 @@
COURSE_TEST_ID = -1
COURSE_VERSICHERUNGSVERMITTLERIN_ID = -2

View File

@ -0,0 +1,290 @@
import json
import wagtail_factories
from django.conf import settings
from wagtail.models import Site
from vbv_lernwelt.competence.factories import CompetenceProfilePageFactory, CompetencePageFactory, \
PerformanceCriteriaFactory
from vbv_lernwelt.competence.models import CompetencePage
from vbv_lernwelt.core.tests.helpers import create_locales_for_wagtail
from vbv_lernwelt.course.consts import COURSE_TEST_ID
from vbv_lernwelt.course.factories import CoursePageFactory
from vbv_lernwelt.course.models import CoursePage, CourseCategory, Course
from vbv_lernwelt.learnpath.models import LearningUnit
from vbv_lernwelt.learnpath.tests.learning_path_factories import CircleFactory, LearningSequenceFactory, \
LearningContentFactory, DocumentBlockFactory, LearningUnitFactory, TestBlockFactory, ExerciseBlockFactory, \
LearningPathFactory, TopicFactory, OnlineTrainingBlockFactory
from vbv_lernwelt.media_library.tests.media_library_factories import MediaLibraryPageFactory, \
create_document_collection, create_link_collection, create_media_content_link, LinkBlockFactory, \
MediaCategoryPageFactory
def create_test_course():
create_locales_for_wagtail()
create_test_course_with_categories()
create_test_learning_path()
create_test_competence_profile()
create_test_media_library()
def create_test_course_with_categories(apps=None, schema_editor=None):
if apps is not None:
Course = apps.get_model('course', 'Course')
CourseCategory = apps.get_model('course', 'CourseCategory')
else:
# pylint: disable=import-outside-toplevel
from vbv_lernwelt.course.models import Course, CourseCategory
course, _ = Course.objects.get_or_create(
id=COURSE_TEST_ID,
title='Test Lehrgang',
category_name='Handlungsfeld'
)
CourseCategory.objects.get_or_create(course=course, title='Allgemein', general=True)
for cat in [
'Fahrzeug', 'Reisen',
]:
CourseCategory.objects.get_or_create(course=course, title=cat)
# create default course page
site = Site.objects.filter(is_default_site=True).first()
if not site:
site = wagtail_factories.SiteFactory(is_default_site=True)
if settings.APP_ENVIRONMENT == 'development':
site.port = 8000
site.save()
course_page = CoursePageFactory(
title="Test Lehrgang",
parent=site.root_page,
course=course,
)
def create_test_learning_path(user=None, skip_locales=True):
course_page = CoursePage.objects.get(course_id=COURSE_TEST_ID)
lp = LearningPathFactory(
title="Test Lernpfad", parent=course_page
)
TopicFactory(title="Basis", is_visible=False, parent=lp)
circle_basis = CircleFactory(
title="Basis",
parent=lp,
description="Basis",
)
LearningSequenceFactory(title='Starten', parent=circle_basis, icon='it-icon-ls-start')
LearningContentFactory(
title='Einführung',
parent=circle_basis,
minutes=15,
contents=[('document', DocumentBlockFactory())]
)
LearningSequenceFactory(title='Beenden', parent=circle_basis, icon='it-icon-ls-end')
LearningContentFactory(
title='Jetzt kann es losgehen!',
parent=circle_basis,
minutes=30,
contents=[('document', DocumentBlockFactory())]
)
TopicFactory(title="Beraten der Kunden", parent=lp)
circle = CircleFactory(
title="Analyse",
parent=lp,
description="Unit-Test Circle",
job_situations=[
('job_situation', 'Autoversicherung'),
('job_situation', 'Autokauf'),
],
goals=[
('goal', '... die heutige Versicherungssituation von Privat- oder Geschäftskunden einzuschätzen.'),
('goal', '... deinem Kunden seine optimale Lösung aufzuzeigen'),
],
experts=[
('person', {'last_name': 'Huggel', 'first_name': 'Patrizia', 'email': 'patrizia.huggel@example.com'}),
]
)
LearningSequenceFactory(title='Starten', parent=circle, icon='it-icon-ls-start')
LearningContentFactory(
title=f'Einleitung Circle "Analyse"',
parent=circle,
minutes=15,
contents=[('document', DocumentBlockFactory())]
)
LearningSequenceFactory(title='Beobachten', parent=circle, icon='it-icon-ls-watch')
lu = LearningUnitFactory(
title='Fahrzeug',
parent=circle,
course_category=CourseCategory.objects.get(course_id=COURSE_TEST_ID, title='Fahrzeug'),
)
LearningContentFactory(
title='Rafael Fasel wechselt sein Auto',
parent=circle,
minutes=30,
contents=[('online_training', OnlineTrainingBlockFactory(
description='In diesem Online-Training lernst du, wie du den Kundenbedarf ermittelst.',
url='',
))]
)
LearningContentFactory(
title='Fachcheck Fahrzeug',
parent=circle,
minutes=30,
contents=[('test', TestBlockFactory())]
)
lu = LearningUnitFactory(
title='Reisen',
parent=circle,
course_category=CourseCategory.objects.get(course_id=COURSE_TEST_ID, title='Reisen'),
)
LearningContentFactory(
title='Reiseversicherung',
parent=circle,
minutes=240,
contents=[('exercise', ExerciseBlockFactory())]
)
LearningContentFactory(
title='Emma und Ayla campen durch Amerika',
parent=circle,
minutes=120,
contents=[('exercise', ExerciseBlockFactory(
url='/static/media/web_based_trainings/story-06-a-01-emma-und-ayla-campen-durch-amerika-einstieg/scormcontent/index.html'))]
)
LearningSequenceFactory(title='Beenden', parent=circle, icon='it-icon-ls-end')
LearningContentFactory(
title='Kompetenzprofil anschauen',
parent=circle,
minutes=30,
contents=[('document', DocumentBlockFactory())]
)
LearningContentFactory(
title='Circle "Analyse" abschliessen',
parent=circle,
minutes=30,
contents=[('document', DocumentBlockFactory())]
)
def create_test_competence_profile():
course = Course.objects.get(id=COURSE_TEST_ID)
course_page = CoursePage.objects.get(course_id=COURSE_TEST_ID)
competence_profile_page = CompetenceProfilePageFactory(
title='Kompetenzprofil',
parent=course_page,
)
competences = [{
'competence_id': 'X1',
'title': 'Weiterempfehlung für Neukunden generieren',
'items': [
'Verhandlungsgeschick',
'Überzeugtes Auftreten',
],
}, {
'competence_id': 'Y1',
'title': 'Wünsche, Ziele und Bedürfnisse der Kunden im Gespräch ermitteln',
'items': [
'Gesprächsführung',
'Fragetechniken',
'Kundenpsychologie',
]
}, {
'competence_id': 'Y2',
'title': 'Analyse des Kundenbedarfs und des Kundenbedürfnisses durchführen',
'items': [
'Fragetechniken',
'Visuelle Hilfsmittel / Visualisierungstechniken',
]
}]
for c in competences:
CompetencePageFactory(
parent=competence_profile_page,
competence_id=c['competence_id'],
title=c['title'],
items=[
('item', i) for i in c['items']
]
)
PerformanceCriteriaFactory(
parent=CompetencePage.objects.get(competence_id='Y1'),
competence_id='Y1.3',
title='Innerhalb des Handlungsfelds «Fahrzeug» bin ich fähig, die Ziele und Pläne des Kunden zu ergründen (SOLL).',
learning_unit=LearningUnit.objects.get(slug='test-lehrgang-lp-circle-analyse-lu-fahrzeug'),
)
PerformanceCriteriaFactory(
parent=CompetencePage.objects.get(competence_id='Y2'),
competence_id='Y2.1',
title='Innerhalb des Handlungsfelds «Fahrzeug» bin ich fähig, die IST-Situation des Kunden mit der geeigneten Gesprächs-/Fragetechnik zu erfassen.',
learning_unit=LearningUnit.objects.get(slug='test-lehrgang-lp-circle-analyse-lu-fahrzeug'),
)
PerformanceCriteriaFactory(
parent=CompetencePage.objects.get(competence_id='Y1'),
competence_id='Y1.3',
title='Innerhalb des Handlungsfelds «Reisen» bin ich fähig, die Ziele und Pläne des Kunden zu ergründen (SOLL).',
learning_unit=LearningUnit.objects.get(slug='test-lehrgang-lp-circle-analyse-lu-reisen'),
)
def create_test_media_library():
course = Course.objects.get(id=COURSE_TEST_ID)
course_page = CoursePage.objects.get(course_id=COURSE_TEST_ID)
media_lib_page = MediaLibraryPageFactory(
title='Mediathek',
parent=course_page,
)
icons = ['icon-hf-fahrzeug', 'icon-hf-reisen', 'icon-hf-einkommenssicherung', ]
for idx, cat in enumerate(course.coursecategory_set.all()):
overview_icon = icons[(idx + 2) % len(icons)]
introduction_text = '''
Das Auto ist für viele der grösste Stolz! Es birgt aber auch ein grosses Gefahrenpotenzial.
Dabei geht es bei den heutigen Fahrzeugpreisen und Reparaturkosten rasch um namhafte Summen,
die der Fahrzeugbesitzer und die Fahrzeugbesitzerin in einem grösseren Schadenfall oft nur schwer selbst aufbringen kann.'''.strip()
description_title = 'Das erwartet dich in diesem Handlungsfeld'
description_text = '''
In diesem berufstypischem Handlungsfeld lernst du alles rund um Motorfahrzeugversicherungen,
wie man sein Auto optimal schützen kann, wie du vorgehst bei einem Fahrzeugwechsel,
welche Aspekte du bei einer Offerte beachten musst und wie du dem Kunden die Lösung präsentierst.'''.strip()
items = [
('item', 'Motorfahrzeughaftpflichtversicherung'),
('item', 'Motorfahrzeugkaskoversicherung'),
('item', 'Insassenunfallversicherung'),
]
body_data = json.dumps([
create_document_collection(),
create_link_collection(
links_dict=[
create_media_content_link(
LinkBlockFactory(title='Nationales Versicherungsbüro', url='https://www.vbv.ch/')),
create_media_content_link(
LinkBlockFactory(title='Adressen der Strassenverkehrsämter', url='https://www.vbv.ch/')),
]
)
])
media_category = MediaCategoryPageFactory(
overview_icon=overview_icon,
title=cat.title,
course_category=cat,
parent=media_lib_page,
introduction_text=introduction_text,
description_title=description_title,
description_text=description_text,
items=items,
body=body_data,
)

View File

@ -0,0 +1,45 @@
import wagtail_factories
from django.conf import settings
from wagtail.models import Site
from vbv_lernwelt.course.consts import COURSE_VERSICHERUNGSVERMITTLERIN_ID
from vbv_lernwelt.course.factories import CoursePageFactory
def create_versicherungsvermittlerin_with_categories(apps=None, schema_editor=None):
if apps is not None:
Course = apps.get_model('course', 'Course')
CourseCategory = apps.get_model('course', 'CourseCategory')
else:
# pylint: disable=import-outside-toplevel
from vbv_lernwelt.course.models import Course, CourseCategory
course, _ = Course.objects.get_or_create(
id=COURSE_VERSICHERUNGSVERMITTLERIN_ID,
title='Versicherungsvermittler/in',
category_name='Handlungsfeld'
)
CourseCategory.objects.get_or_create(course=course, title='Allgemein', general=True)
for cat in [
'Fahrzeug', 'Reisen', 'Einkommenssicherung', 'Gesundheit', 'Haushalt', 'Sparen',
'Pensionierung', 'KMU', 'Wohneigentum', 'Rechtsstreitigkeiten', 'Erben / Vererben',
'Selbständigkeit',
]:
CourseCategory.objects.get_or_create(course=course, title=cat)
# create default course page
site = Site.objects.filter(is_default_site=True).first()
if not site:
site = wagtail_factories.SiteFactory(is_default_site=True)
if settings.APP_ENVIRONMENT == 'development':
site.port = 8000
site.save()
course_page = CoursePageFactory(
title="Versicherungsvermittler/in",
parent=site.root_page,
course=course,
)

View File

@ -0,0 +1,19 @@
import wagtail_factories
from factory.django import DjangoModelFactory
from vbv_lernwelt.course.models import CoursePage, Course
class CourseFactory(DjangoModelFactory):
class Meta:
model = Course
title = 'Versicherungsvermittler/in'
category_name = 'Handlungsfeld'
class CoursePageFactory(wagtail_factories.PageFactory):
title = "Versicherungsvermittler/in"
class Meta:
model = CoursePage

View File

@ -0,0 +1,9 @@
# -*- coding: utf-8 -*-
#
# Iterativ GmbH
# http://www.iterativ.ch/
#
# Copyright (c) 2015 Iterativ GmbH. All rights reserved.
#
# Created on 2022-03-31
# @author: lorenz.padberg@iterativ.ch

View File

@ -0,0 +1,25 @@
import djclick as click
from vbv_lernwelt.competence.create_default_competence_profile import create_default_competence_profile
from vbv_lernwelt.course.creators.test_course import create_test_course
from vbv_lernwelt.course.creators.versicherungsvermittlerin import create_versicherungsvermittlerin_with_categories
from vbv_lernwelt.learnpath.create_default_learning_path import create_default_learning_path
from vbv_lernwelt.media_library.create_default_documents import create_default_collections, create_default_documents
from vbv_lernwelt.media_library.create_default_media_library import create_default_media_library
@click.command()
def command():
create_versicherungsvermittlerin_with_categories()
create_default_learning_path()
create_default_competence_profile()
# media library
create_default_collections()
create_default_documents()
create_default_media_library()
# test course
create_test_course()

View File

@ -0,0 +1,68 @@
# Generated by Django 3.2.13 on 2022-09-28 12:51
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('wagtailcore', '0069_log_entry_jsonfield'),
]
operations = [
migrations.CreateModel(
name='Course',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=255, verbose_name='Titel')),
('category_name', models.CharField(default='Kategorie', max_length=255, verbose_name='Kategorie-Name')),
],
options={
'verbose_name': 'Lehrgang',
},
),
migrations.CreateModel(
name='CoursePage',
fields=[
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')),
('course', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='course.course')),
],
options={
'verbose_name': 'Lehrgang-Seite',
},
bases=('wagtailcore.page',),
),
migrations.CreateModel(
name='CourseCompletion',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('page_key', models.UUIDField()),
('page_type', models.CharField(blank=True, default='', max_length=255)),
('page_slug', models.CharField(blank=True, default='', max_length=255)),
('completion_status', models.CharField(choices=[('unknown', 'unknown'), ('success', 'success'), ('fail', 'fail')], default='unknown', max_length=255)),
('additional_json_data', models.JSONField(default=dict)),
('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='course.course')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='CourseCategory',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(blank=True, max_length=255, verbose_name='Titel')),
('general', models.BooleanField(default=False, verbose_name='Allgemein')),
('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='course.course')),
],
),
migrations.AddConstraint(
model_name='coursecompletion',
constraint=models.UniqueConstraint(fields=('user', 'page_key'), name='course_completion_unique_user_page_key'),
),
]

View File

@ -0,0 +1,78 @@
from django.db import models
from django.db.models import UniqueConstraint
from django.utils.text import slugify
from django.utils.translation import gettext_lazy as _
from wagtail.models import Page
from vbv_lernwelt.core.model_utils import find_available_slug
from vbv_lernwelt.core.models import User
class Course(models.Model):
title = models.CharField(_('Titel'), max_length=255)
category_name = models.CharField(_('Kategorie-Name'), max_length=255, default='Kategorie')
class Meta:
verbose_name = _("Lehrgang")
def __str__(self):
return f"{self.title}"
class CourseCategory(models.Model):
# Die Handlungsfelder im "Versicherungsvermittler/in"
title = models.CharField(_('Titel'), max_length=255, blank=True)
course = models.ForeignKey('course.Course', on_delete=models.CASCADE)
general = models.BooleanField(_('Allgemein'), default=False)
def __str__(self):
return f"{self.course} / {self.title}"
class CoursePage(Page):
content_panels = Page.content_panels
subpage_types = ['learnpath.LearningPath', 'media_library.MediaLibraryPage']
course = models.ForeignKey('course.Course', on_delete=models.PROTECT)
class Meta:
verbose_name = _("Lehrgang-Seite")
def full_clean(self, *args, **kwargs):
self.slug = find_available_slug(slugify(self.title, allow_unicode=True))
super(CoursePage, self).full_clean(*args, **kwargs)
def __str__(self):
return f"{self.title}"
class CourseCompletion(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
user = models.ForeignKey(User, on_delete=models.CASCADE)
# page can logically be a LearningContent or a PerformanceCriteria for now
page_key = models.UUIDField()
page_type = models.CharField(max_length=255, default='', blank=True)
page_slug = models.CharField(max_length=255, default='', blank=True)
course = models.ForeignKey('course.Course', on_delete=models.CASCADE)
completion_status = models.CharField(
max_length=255,
choices=[
('unknown', 'unknown'),
('success', 'success'),
('fail', 'fail'),
],
default='unknown',
)
additional_json_data = models.JSONField(default=dict)
class Meta:
constraints = [
UniqueConstraint(
fields=['user', 'page_key', ],
name='course_completion_unique_user_page_key'
)
]

View File

@ -0,0 +1,25 @@
from rest_framework import serializers
from vbv_lernwelt.course.models import CourseCategory, Course, CourseCompletion
class CourseSerializer(serializers.ModelSerializer):
class Meta:
model = Course
fields = ['id', 'title', 'category_name']
class CourseCategorySerializer(serializers.ModelSerializer):
class Meta:
model = CourseCategory
fields = ['id', 'title', 'general',]
class CourseCompletionSerializer(serializers.ModelSerializer):
class Meta:
model = CourseCompletion
fields = [
'id', 'created_at', 'updated_at', 'user',
'page_key', 'page_type', 'page_slug',
'course', 'completion_status', 'additional_json_data',
]

View File

@ -0,0 +1,16 @@
import structlog
from django.core.cache import caches
from django.db.models.signals import post_delete, post_save
from wagtail.models import Page
logger = structlog.get_logger(__name__)
def invalidate_api_page_cache(sender, **kwargs):
logger.debug('invalidate api_page_cache', label='api_page_cache')
caches['api_page_cache'].clear()
for subclass in Page.__subclasses__():
post_save.connect(invalidate_api_page_cache, subclass)
post_delete.connect(invalidate_api_page_cache, subclass)

View File

@ -0,0 +1,65 @@
import json
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 CourseCompletion
from vbv_lernwelt.learnpath.models import LearningContent
class CourseCompletionApiTestCase(APITestCase):
def setUp(self) -> None:
create_default_users()
create_test_course()
self.user = User.objects.get(username='student')
self.client.login(username='student', password='test')
def test_completeLearningContent_works(self):
learning_content = LearningContent.objects.get(title='Fachcheck Fahrzeug')
learning_content_key = str(learning_content.translation_key)
mark_url = f'/api/course/completion/mark/'
response = self.client.post(mark_url, {
'page_key': learning_content_key,
})
response_json = response.json()
print(json.dumps(response.json(), indent=2))
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response_json), 1)
self.assertEqual(response_json[0]['page_key'], learning_content_key)
self.assertEqual(response_json[0]['completion_status'], 'success')
db_entry = CourseCompletion.objects.get(user=self.user, course_id=COURSE_TEST_ID, page_key=learning_content_key)
self.assertEqual(db_entry.completion_status, 'success')
# test getting the circle data
response = self.client.get(f'/api/course/completion/{COURSE_TEST_ID}/')
print(response.status_code)
response_json = response.json()
print(json.dumps(response.json(), indent=2))
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response_json), 1)
self.assertEqual(response_json[0]['page_key'], learning_content_key)
self.assertTrue(response_json[0]['completion_status'], 'success')
# test with "fail"
response = self.client.post(mark_url, {
'page_key': learning_content_key,
'completion_status': 'fail',
})
response_json = response.json()
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response_json), 1)
self.assertEqual(response_json[0]['page_key'], learning_content_key)
self.assertEqual(response_json[0]['completion_status'], 'fail')
db_entry = CourseCompletion.objects.get(user=self.user, course_id=COURSE_TEST_ID, page_key=learning_content_key)
self.assertEqual(db_entry.completion_status, 'fail')

View File

@ -0,0 +1,71 @@
import structlog
from rest_framework.decorators import api_view
from rest_framework.response import Response
from wagtail.models import Page
from vbv_lernwelt.course.models import CourseCompletion, CoursePage
from vbv_lernwelt.course.serializers import CourseCompletionSerializer
from vbv_lernwelt.learnpath.utils import get_wagtail_type
logger = structlog.get_logger(__name__)
@api_view(['GET'])
# @cache_page(60 * 60 * 8, cache="api_page_cache")
def page_api_view(request, slug):
try:
page = Page.objects.get(slug=slug, locale__language_code='de-CH')
serializer = page.specific.get_serializer_class()(page.specific)
return Response(serializer.data)
except Exception as e:
logger.error(e)
return Response({"error": str(e)}, status=404)
@api_view(['GET'])
def request_course_completion(request, course_id):
response_data = CourseCompletionSerializer(
CourseCompletion.objects.filter(user=request.user, course_id=course_id),
many=True,
).data
return Response(status=200, data=response_data)
@api_view(['POST'])
def mark_course_completion(request):
page_key = request.data.get('page_key')
completion_status = request.data.get('completion_status', 'success')
page = Page.objects.get(translation_key=page_key, locale__language_code='de-CH')
page_type = get_wagtail_type(page.specific)
course = CoursePage.objects.ancestor_of(page).first().specific.course
cc, created = CourseCompletion.objects.get_or_create(
user=request.user,
page_key=page_key,
course_id=course.id,
)
cc.page_slug = page.slug
cc.page_type = page_type
cc.completion_status = completion_status
cc.save()
response_data = CourseCompletionSerializer(
CourseCompletion.objects.filter(user=request.user, course_id=course.id),
many=True,
).data
logger.debug(
'mark_course_completion successful',
label='completion_api',
page_key=page_key,
page_type=page_type,
page_slug=page.slug,
page_title=page.title,
user_id=request.user.id,
course_id=course.id,
completion_status=completion_status,
)
return Response(status=200, data=response_data)

View File

@ -5,12 +5,12 @@ from wagtail.models import Site, Page, Locale
from wagtail_localize.models import LocaleSynchronization
from vbv_lernwelt.core.admin import User
from vbv_lernwelt.learnpath.models import LearningPath, Topic, Circle, LearningSequence, LearningContent, LearningUnit, \
LearningUnitQuestion
from vbv_lernwelt.course.consts import COURSE_VERSICHERUNGSVERMITTLERIN_ID
from vbv_lernwelt.course.models import CoursePage, CourseCategory
from vbv_lernwelt.learnpath.tests.learning_path_factories import LearningPathFactory, TopicFactory, CircleFactory, \
LearningSequenceFactory, LearningContentFactory, VideoBlockFactory, ResourceBlockFactory, \
ExerciseBlockFactory, DocumentBlockFactory, LearningUnitFactory, LearningUnitQuestionFactory, \
AssignmentBlockFactory, BookBlockFactory, MediaLibraryBlockFactory, OnlineTrainingBlockFactory, TestBlockFactory
ExerciseBlockFactory, DocumentBlockFactory, LearningUnitFactory, AssignmentBlockFactory, BookBlockFactory, \
MediaLibraryBlockFactory, OnlineTrainingBlockFactory, TestBlockFactory
def create_circle(title, learning_path):
@ -61,14 +61,7 @@ def create_circle_children(circle, title):
lu = LearningUnitFactory(
title='Absicherung der Familie',
parent=circle,
)
LearningUnitQuestionFactory(
title="Ich bin in der Lage, mit geeigneten Fragestellungen die Deckung von Versicherungen zu erfassen.",
parent=lu
)
LearningUnitQuestionFactory(
title="Zweite passende Frage zu 'Absicherung der Familie'",
parent=lu
course_category=CourseCategory.objects.get(course_id=COURSE_VERSICHERUNGSVERMITTLERIN_ID, title='Einkommenssicherung')
)
LearningContentFactory(
title='Ermittlung des Kundenbedarfs',
@ -120,10 +113,9 @@ def create_circle_children(circle, title):
)
LearningSequenceFactory(title='Anwenden', parent=circle, icon='it-icon-ls-apply')
lu = LearningUnitFactory(title='Prämien einsparen', parent=circle)
LearningUnitQuestionFactory(
title="Passende Frage zu Anwenden",
parent=lu
lu = LearningUnitFactory(
title='Prämien einsparen',
parent=circle,
)
LearningContentFactory(
title='Versicherungsbedarf für Familien',
@ -138,10 +130,10 @@ def create_circle_children(circle, title):
contents=[('exercise', ExerciseBlockFactory())]
)
lu = LearningUnitFactory(title='Sich selbständig machen', parent=circle)
LearningUnitQuestionFactory(
title="Passende Frage zu 'Sich selbständig machen'",
parent=lu
lu = LearningUnitFactory(
title='Sich selbständig machen',
parent=circle,
course_category=CourseCategory.objects.get(course_id=COURSE_VERSICHERUNGSVERMITTLERIN_ID, title='Selbständigkeit')
)
LearningContentFactory(
title='GmbH oder AG',
@ -156,10 +148,10 @@ def create_circle_children(circle, title):
contents=[('exercise', ExerciseBlockFactory())]
)
lu = LearningUnitFactory(title='Auto verkaufen', parent=circle)
LearningUnitQuestionFactory(
title='Passende Frage zu "Auto verkaufen"',
parent=lu
lu = LearningUnitFactory(
title='Auto verkaufen',
parent=circle,
course_category=CourseCategory.objects.get(course_id=COURSE_VERSICHERUNGSVERMITTLERIN_ID, title='Fahrzeug')
)
LearningContentFactory(
title='Motorfahrzeugversicherung',
@ -186,10 +178,10 @@ def create_circle_children(circle, title):
contents=[('exercise', ExerciseBlockFactory(url='/static/media/web_based_trainings/training-04-a-01-rafael-fasel-wechselt-sein-auto-einstieg/scormcontent/index.html'))]
)
lu = LearningUnitFactory(title='Pensionierung', parent=circle)
LearningUnitQuestionFactory(
title='Passende Frage zu "Pensionierung"',
parent=lu
lu = LearningUnitFactory(
title='Pensionierung',
parent=circle,
course_category=CourseCategory.objects.get(course_id=COURSE_VERSICHERUNGSVERMITTLERIN_ID, title='Pensionierung')
)
LearningContentFactory(
title='3-Säulen-Prinzip',
@ -216,10 +208,10 @@ def create_circle_children(circle, title):
contents=[('exercise', ExerciseBlockFactory())]
)
lu = LearningUnitFactory(title='Reisen', parent=circle)
LearningUnitQuestionFactory(
title='Passende Frage zu "Reisen"',
parent=lu
lu = LearningUnitFactory(
title='Reisen',
parent=circle,
course_category=CourseCategory.objects.get(course_id=COURSE_VERSICHERUNGSVERMITTLERIN_ID, title='Reisen')
)
LearningContentFactory(
title='Reiseversicherung',
@ -235,10 +227,10 @@ def create_circle_children(circle, title):
url='/static/media/web_based_trainings/story-06-a-01-emma-und-ayla-campen-durch-amerika-einstieg/scormcontent/index.html'))]
)
lu = LearningUnitFactory(title='Haushalt', parent=circle)
LearningUnitQuestionFactory(
title='Passende Frage zu "Haushalt"',
parent=lu
lu = LearningUnitFactory(
title='Haushalt',
parent=circle,
course_category=CourseCategory.objects.get(course_id=COURSE_VERSICHERUNGSVERMITTLERIN_ID, title='Haushalt')
)
LearningContentFactory(
title='Privathaftpflicht',
@ -260,10 +252,10 @@ def create_circle_children(circle, title):
)
LearningSequenceFactory(title='Üben', parent=circle, icon='it-icon-ls-practice')
lu = LearningUnitFactory(title='Kind zieht von zu Hause aus', parent=circle)
LearningUnitQuestionFactory(
title='Passende Frage zu "Kind zieht von zu Hause aus"',
parent=lu
lu = LearningUnitFactory(
title='Kind zieht von zu Hause aus',
parent=circle,
course_category=CourseCategory.objects.get(course_id=COURSE_VERSICHERUNGSVERMITTLERIN_ID, title='Einkommenssicherung')
)
LearningContentFactory(
title='Hausrat',
@ -285,10 +277,10 @@ def create_circle_children(circle, title):
)
LearningSequenceFactory(title='Testen', parent=circle, icon='it-icon-ls-test')
lu = LearningUnitFactory(title='Kind zieht von zu Hause aus "Testen"', parent=circle)
LearningUnitQuestionFactory(
title='Passende Frage zu "Kind zieht von zu Hause aus"',
parent=lu
lu = LearningUnitFactory(
title='Kind zieht von zu Hause aus "Testen"',
parent=circle,
course_category=CourseCategory.objects.get(course_id=COURSE_VERSICHERUNGSVERMITTLERIN_ID, title='Einkommenssicherung')
)
LearningContentFactory(
title='Das erwartet dich im Test',
@ -341,7 +333,11 @@ def create_default_learning_path(user=None, skip_locales=True):
# create_default_competences()
lp = LearningPathFactory(title="Versicherungsvermittler/in", parent=site.root_page)
course_page = CoursePage.objects.get(course_id=COURSE_VERSICHERUNGSVERMITTLERIN_ID)
lp = LearningPathFactory(
title="Versicherungsvermittler/in",
parent=course_page,
)
TopicFactory(title="Basis", is_visible=False, parent=lp)
@ -446,13 +442,3 @@ Neukundinnen und -kunden.""",
# all pages belong to 'admin' by default
Page.objects.update(owner=user)
def delete_default_learning_path():
LearningContent.objects.all().delete()
LearningUnitQuestion.objects.all().delete()
LearningUnit.objects.all().delete()
LearningSequence.objects.all().delete()
Circle.objects.all().delete()
Topic.objects.all().delete()
LearningPath.objects.all().delete()

View File

@ -1,10 +0,0 @@
import djclick as click
from vbv_lernwelt.learnpath.create_default_learning_path import create_default_learning_path
from vbv_lernwelt.learnpath.tests.create_simple_test_learning_path import create_simple_test_learning_path
@click.command()
def command():
create_default_learning_path(skip_locales=True)
create_simple_test_learning_path(skip_locales=True)

View File

@ -1,8 +0,0 @@
import djclick as click
from vbv_lernwelt.learnpath.create_default_learning_path import delete_default_learning_path
@click.command()
def command():
delete_default_learning_path()

View File

@ -1,8 +1,7 @@
# Generated by Django 3.2.13 on 2022-06-22 15:48
# Generated by Django 3.2.13 on 2022-09-28 12:51
from django.db import migrations, models
import django.db.models.deletion
import modelcluster.fields
import wagtail.blocks
import wagtail.fields
import wagtail.images.blocks
@ -13,6 +12,7 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
('course', '0001_initial'),
('wagtailcore', '0069_log_entry_jsonfield'),
]
@ -31,34 +31,12 @@ class Migration(migrations.Migration):
},
bases=('wagtailcore.page',),
),
migrations.CreateModel(
name='Competence',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('sort_order', models.IntegerField(blank=True, editable=False, null=True)),
('category_short', models.CharField(default='', max_length=3)),
('name', models.CharField(max_length=2048)),
],
options={
'verbose_name': 'Competence',
},
),
migrations.CreateModel(
name='CompetencePage',
fields=[
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')),
],
options={
'verbose_name': 'Learning Path',
},
bases=('wagtailcore.page',),
),
migrations.CreateModel(
name='LearningContent',
fields=[
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')),
('minutes', models.PositiveIntegerField(default=15)),
('contents', wagtail.fields.StreamField([('video', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock()), ('url', wagtail.blocks.URLBlock())])), ('web_based_training', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock()), ('url', wagtail.blocks.URLBlock())])), ('podcast', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock()), ('url', wagtail.blocks.URLBlock())])), ('competence', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock())])), ('exercise', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock())])), ('document', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock())])), ('knowledge', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock())]))], use_json_field=None)),
('contents', wagtail.fields.StreamField([('video', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock()), ('url', wagtail.blocks.URLBlock())])), ('resource', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock()), ('url', wagtail.blocks.URLBlock())])), ('exercise', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock()), ('url', wagtail.blocks.URLBlock())])), ('online_training', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock()), ('url', wagtail.blocks.URLBlock())])), ('media_library', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock()), ('url', wagtail.blocks.URLBlock())])), ('document', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock()), ('url', wagtail.blocks.URLBlock())])), ('test', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock()), ('url', wagtail.blocks.URLBlock())])), ('book', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock()), ('url', wagtail.blocks.URLBlock())])), ('assignment', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock()), ('url', wagtail.blocks.URLBlock())]))], use_json_field=None)),
],
options={
'verbose_name': 'Learning Content',
@ -86,26 +64,6 @@ class Migration(migrations.Migration):
},
bases=('wagtailcore.page',),
),
migrations.CreateModel(
name='LearningUnit',
fields=[
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')),
],
options={
'verbose_name': 'Learning Unit',
},
bases=('wagtailcore.page',),
),
migrations.CreateModel(
name='LearningUnitQuestion',
fields=[
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')),
],
options={
'verbose_name': 'Learning Unit Question',
},
bases=('wagtailcore.page',),
),
migrations.CreateModel(
name='Topic',
fields=[
@ -118,20 +76,14 @@ class Migration(migrations.Migration):
bases=('wagtailcore.page',),
),
migrations.CreateModel(
name='FullfillmentCriteria',
name='LearningUnit',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('sort_order', models.IntegerField(blank=True, editable=False, null=True)),
('name', models.CharField(max_length=2048)),
('competence', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='learnpath.competence')),
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')),
('course_category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='course.coursecategory')),
],
options={
'verbose_name': 'Fullfillment Criteria',
'verbose_name': 'Learning Unit',
},
),
migrations.AddField(
model_name='competence',
name='competence_page',
field=modelcluster.fields.ParentalKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='competences', to='learnpath.competencepage'),
bases=('wagtailcore.page',),
),
]

View File

@ -1,20 +0,0 @@
# Generated by Django 3.2.13 on 2022-08-24 14:47
from django.db import migrations
import wagtail.blocks
import wagtail.fields
class Migration(migrations.Migration):
dependencies = [
('learnpath', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='learningcontent',
name='contents',
field=wagtail.fields.StreamField([('video', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock()), ('url', wagtail.blocks.URLBlock())])), ('web_based_training', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock()), ('url', wagtail.blocks.URLBlock())])), ('podcast', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock()), ('url', wagtail.blocks.URLBlock())])), ('competence', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock())])), ('exercise', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock()), ('url', wagtail.blocks.URLBlock())])), ('document', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock())])), ('knowledge', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock())]))], use_json_field=None),
),
]

View File

@ -7,6 +7,8 @@ from wagtail.fields import StreamField
from wagtail.images.blocks import ImageChooserBlock
from wagtail.models import Page
from vbv_lernwelt.core.model_utils import find_available_slug
from vbv_lernwelt.course.models import CoursePage
from vbv_lernwelt.learnpath.models_learning_unit_content import VideoBlock, \
ExerciseBlock, DocumentBlock, AssignmentBlock, BookBlock, MediaLibraryBlock, \
OnlineTrainingBlock, ResourceBlock, TestBlock
@ -14,16 +16,15 @@ from vbv_lernwelt.learnpath.serializer_helpers import get_it_serializer_class
class LearningPath(Page):
# PageChooserPanel('related_page', 'demo.PublisherPage'),
content_panels = Page.content_panels
subpage_types = ['learnpath.Circle', 'learnpath.Topic']
parent_page_types = ['course.CoursePage']
class Meta:
verbose_name = "Learning Path"
def full_clean(self, *args, **kwargs):
self.slug = find_available_slug(slugify(self.title, allow_unicode=True))
self.slug = find_available_slug(slugify(f"{self.get_parent().slug}-lp", allow_unicode=True))
super(LearningPath, self).full_clean(*args, **kwargs)
def __str__(self):
@ -32,7 +33,7 @@ class LearningPath(Page):
@classmethod
def get_serializer_class(cls):
return get_it_serializer_class(
cls, ['id', 'title', 'slug', 'type', 'translation_key', 'children']
cls, ['id', 'title', 'slug', 'type', 'translation_key', 'children', 'course']
)
@ -163,6 +164,11 @@ class LearningSequence(Page):
class LearningUnit(Page):
parent_page_types = ['learnpath.Circle']
subpage_types = []
course_category = models.ForeignKey('course.CourseCategory', on_delete=models.SET_NULL, null=True, blank=True)
content_panels = Page.content_panels + [
FieldPanel('course_category'),
]
class Meta:
verbose_name = "Learning Unit"
@ -171,36 +177,29 @@ class LearningUnit(Page):
return f"{self.title}"
def full_clean(self, *args, **kwargs):
self.slug = find_slug_with_parent_prefix(self, 'lu')
course = None
course_parent_page = self.get_ancestors().exact_type(CoursePage).last()
if course_parent_page:
course = course_parent_page.specific.course
if self.course_category is None and course:
self.course_category = course.coursecategory_set.filter(general=True).first()
if self.course_category.general:
self.slug = find_slug_with_parent_prefix(self, 'lu')
else:
self.slug = find_slug_with_parent_prefix(self, 'lu', self.course_category.title)
super(LearningUnit, self).full_clean(*args, **kwargs)
@classmethod
def get_serializer_class(cls):
return get_it_serializer_class(cls, field_names=['id', 'title', 'slug', 'type', 'translation_key', 'children'])
from vbv_lernwelt.learnpath.serializers import LearningUnitSerializer
return LearningUnitSerializer
def get_admin_display_title_html(self):
return f'<span style="font-weight: 700; font-size: 20px;">{self.draft_title}</span>'
class LearningUnitQuestion(Page):
parent_page_types = ['learnpath.LearningUnit']
subpage_types = []
class Meta:
verbose_name = "Learning Unit Question"
def __str__(self):
return f"{self.title}"
def full_clean(self, *args, **kwargs):
self.slug = find_slug_with_parent_prefix(self, 'luq')
super(LearningUnitQuestion, self).full_clean(*args, **kwargs)
@classmethod
def get_serializer_class(cls):
return get_it_serializer_class(cls, field_names=['id', 'title', 'slug', 'type', 'translation_key', ])
class LearningContent(Page):
parent_page_types = ['learnpath.Circle']
subpage_types = []
@ -263,44 +262,14 @@ class LearningContent(Page):
return f"{self.title}"
def find_slug_with_parent_prefix(page, type_prefix):
def find_slug_with_parent_prefix(page, type_prefix, slug_postfix=None):
parent_slug = page.get_ancestors().exact_type(LearningPath, Circle).last().slug
if parent_slug:
slug_prefix = f"{parent_slug}-{type_prefix}"
else:
slug_prefix = type_prefix
return find_available_slug(slugify(f'{slug_prefix}-{page.title}', allow_unicode=True))
if slug_postfix is None:
slug_postfix = page.title
def find_available_slug(requested_slug, ignore_page_id=None):
"""
Finds an available slug within the specified parent.
If the requested slug is not available, this adds a number on the end, for example:
- 'requested-slug'
- 'requested-slug-1'
- 'requested-slug-2'
And so on, until an available slug is found.
The `ignore_page_id` keyword argument is useful for when you are updating a page,
you can pass the page being updated here so the page's current slug is not
treated as in use by another page.
"""
pages = Page.objects.filter(slug__startswith=requested_slug)
if ignore_page_id:
pages = pages.exclude(id=ignore_page_id)
existing_slugs = set(pages.values_list("slug", flat=True))
slug = requested_slug
number = 1
while slug in existing_slugs:
slug = requested_slug + "-" + str(number)
number += 1
return slug
return find_available_slug(slugify(f'{slug_prefix}-{slug_postfix}', allow_unicode=True))

View File

@ -1,6 +1,8 @@
import wagtail.api.v2.serializers as wagtail_serializers
from rest_framework.fields import SerializerMethodField
from vbv_lernwelt.course.models import CoursePage
from vbv_lernwelt.course.serializers import CourseCategorySerializer, CourseSerializer
from vbv_lernwelt.learnpath.utils import get_wagtail_type
@ -17,6 +19,8 @@ class ItTypeField(wagtail_serializers.TypeField):
class ItBaseSerializer(wagtail_serializers.BaseSerializer):
type = ItTypeField(read_only=True)
children = SerializerMethodField()
course = SerializerMethodField()
course_category = CourseCategorySerializer(read_only=True)
meta_fields = []
@ -30,6 +34,15 @@ class ItBaseSerializer(wagtail_serializers.BaseSerializer):
children = _get_children(self.descendants, obj)
return [c.specific.get_serializer_class()(c.specific, descendants=self.descendants).data for c in children]
def get_course(self, obj):
if hasattr(obj, 'course'):
return CourseSerializer(obj.course).data
else:
course_parent_page = obj.get_ancestors().exact_type(CoursePage).last()
if course_parent_page:
return CourseSerializer(course_parent_page.specific.course).data
return ''
def _get_descendants(pages, obj):
return [c for c in pages if c.path.startswith(obj.path) and c.depth >= obj.depth]

View File

@ -0,0 +1,14 @@
from vbv_lernwelt.competence.serializers import PerformanceCriteriaLearningPathSerializer
from vbv_lernwelt.learnpath.models import LearningUnit
from vbv_lernwelt.learnpath.serializer_helpers import get_it_serializer_class
class LearningUnitSerializer(get_it_serializer_class(LearningUnit, [
'id', 'title', 'slug', 'type', 'translation_key',
'course_category', 'children',
])):
def get_children(self, obj):
return [
PerformanceCriteriaLearningPathSerializer(child).data
for child in obj.performancecriteria_set.all()
]

View File

@ -1,16 +0,0 @@
import structlog
from django.core.cache import caches
from django.db.models.signals import post_delete, post_save
from wagtail.models import Page
logger = structlog.get_logger(__name__)
def invalidate_learning_path_cache(sender, **kwargs):
logger.debug('invalidate learning_path_cache', label='learning_path_cache')
caches['learning_path_cache'].clear()
for subclass in Page.__subclasses__():
post_save.connect(invalidate_learning_path_cache, subclass)
post_delete.connect(invalidate_learning_path_cache, subclass)

View File

@ -1,174 +0,0 @@
import wagtail_factories
from django.conf import settings
from wagtail.models import Site, Page
from vbv_lernwelt.core.admin import User
from vbv_lernwelt.learnpath.tests.learning_path_factories import LearningPathFactory, TopicFactory, CircleFactory, \
LearningSequenceFactory, LearningContentFactory, \
ExerciseBlockFactory, LearningUnitFactory, LearningUnitQuestionFactory, \
DocumentBlockFactory, TestBlockFactory, OnlineTrainingBlockFactory
def create_circle(title, learning_path):
return CircleFactory(
title=title,
parent=learning_path,
description="Unit-Test Circle",
job_situations=[
('job_situation', 'Absicherung der Familie'),
('job_situation', 'Reisen'),
],
goals=[
('goal', '... die heutige Versicherungssituation von Privat- oder Geschäftskunden einzuschätzen.'),
('goal', '... deinem Kunden seine optimale Lösung aufzuzeigen'),
],
experts=[
('person', {'last_name': 'Huggel', 'first_name': 'Patrizia', 'email': 'patrizia.huggel@example.com'}),
]
)
def create_circle_children(circle, title):
LearningSequenceFactory(title='Starten', parent=circle, icon='it-icon-ls-start')
LearningContentFactory(
title=f'Einleitung Circle "{title}"',
parent=circle,
minutes=15,
contents=[('document', DocumentBlockFactory())]
)
LearningSequenceFactory(title='Beobachten', parent=circle, icon='it-icon-ls-watch')
lu = LearningUnitFactory(
title='Absicherung der Familie',
parent=circle,
)
LearningUnitQuestionFactory(
title="Ich bin in der Lage, mit geeigneten Fragestellungen die Deckung von Versicherungen zu erfassen.",
parent=lu
)
LearningUnitQuestionFactory(
title="Zweite passende Frage zu 'Absicherung der Familie'",
parent=lu
)
LearningContentFactory(
title='Ermittlung des Kundenbedarfs',
parent=circle,
minutes=30,
contents=[('online_training', OnlineTrainingBlockFactory(
description='In diesem Online-Training lernst du, wie du den Kundenbedarf ermittelst.',
url='',
))]
)
LearningContentFactory(
title='Kundenbedürfnisse erkennen',
parent=circle,
minutes=30,
contents=[('test', TestBlockFactory())]
)
LearningContentFactory(
title='Was braucht eine Familie?',
parent=circle,
minutes=60,
contents=[('exercise', ExerciseBlockFactory(url='/static/media/web_based_trainings/story-01-a-01-patrizia-marco-sichern-sich-ab-einstieg/scormcontent/index.html'
))]
)
lu = LearningUnitFactory(title='Reisen', parent=circle)
LearningUnitQuestionFactory(
title='Passende Frage zu "Reisen"',
parent=lu
)
LearningContentFactory(
title='Reiseversicherung',
parent=circle,
minutes=240,
contents=[('exercise', ExerciseBlockFactory())]
)
LearningContentFactory(
title='Sorgenfrei reisen',
parent=circle,
minutes=120,
contents=[('exercise', ExerciseBlockFactory(
url='/static/media/web_based_trainings/story-06-a-01-emma-und-ayla-campen-durch-amerika-einstieg/scormcontent/index.html'))]
)
LearningSequenceFactory(title='Beenden', parent=circle, icon='it-icon-ls-end')
LearningContentFactory(
title='Kompetenzprofil anschauen',
parent=circle,
minutes=30,
contents=[('document', DocumentBlockFactory())]
)
LearningContentFactory(
title='Circle "Analyse" abschliessen',
parent=circle,
minutes=30,
contents=[('document', DocumentBlockFactory())]
)
def create_simple_test_learning_path(user=None, skip_locales=True):
if user is None:
user = User.objects.get(username='info@iterativ.ch')
site = Site.objects.filter(is_default_site=True).first()
if not site:
site = wagtail_factories.SiteFactory(is_default_site=True)
if settings.APP_ENVIRONMENT == 'development':
site.port = 8000
site.save()
lp = LearningPathFactory(title="Unit-Test Lernpfad", parent=site.root_page)
TopicFactory(title="Basis", is_visible=False, parent=lp)
circle_basis = CircleFactory(
title="Basis",
parent=lp,
description="Basis von Unit-Test Lernpfad",
)
LearningSequenceFactory(title='Starten', parent=circle_basis, icon='it-icon-ls-start')
LearningContentFactory(
title='Einleitung Circle "Basis"',
parent=circle_basis,
minutes=15,
contents=[('document', DocumentBlockFactory())]
)
LearningSequenceFactory(title='Beenden', parent=circle_basis, icon='it-icon-ls-end')
LearningContentFactory(
title='Kompetenzprofil anschauen',
parent=circle_basis,
minutes=30,
contents=[('document', DocumentBlockFactory())]
)
LearningContentFactory(
title='Circle "Analyse" abschliessen',
parent=circle_basis,
minutes=30,
contents=[('document', DocumentBlockFactory())]
)
TopicFactory(title="Gewinnen von Kunden", parent=lp)
circle_analyse = create_circle('Unit-Test Circle', lp)
create_circle_children(circle_analyse, 'Unit-Test Circle')
# locales
# if not skip_locales:
# locale_de = Locale.objects.get(language_code='de-CH')
# locale_fr, _ = Locale.objects.get_or_create(language_code='fr-CH')
# LocaleSynchronization.objects.get_or_create(
# locale_id=locale_fr.id,
# sync_from_id=locale_de.id
# )
# locale_it, _ = Locale.objects.get_or_create(language_code='it-CH')
# LocaleSynchronization.objects.get_or_create(
# locale_id=locale_it.id,
# sync_from_id=locale_de.id
# )
# call_command('sync_locale_trees')
# all pages belong to 'admin' by default
Page.objects.update(owner=user)

View File

@ -1,7 +1,6 @@
import wagtail_factories
from vbv_lernwelt.learnpath.models import LearningPath, Topic, Circle, LearningSequence, LearningContent, LearningUnit, \
LearningUnitQuestion
from vbv_lernwelt.learnpath.models import LearningPath, Topic, Circle, LearningSequence, LearningContent, LearningUnit
from vbv_lernwelt.learnpath.models_learning_unit_content import VideoBlock, OnlineTrainingBlock, \
ExerciseBlock, DocumentBlock, ResourceBlock, TestBlock, BookBlock, MediaLibraryBlock, AssignmentBlock
@ -42,12 +41,6 @@ class LearningUnitFactory(wagtail_factories.PageFactory):
model = LearningUnit
class LearningUnitQuestionFactory(wagtail_factories.PageFactory):
title = 'Frage zu Lerneinheit'
class Meta:
model = LearningUnitQuestion
class LearningContentFactory(wagtail_factories.PageFactory):
title = 'Lerninhalt'
@ -62,30 +55,35 @@ class VideoBlockFactory(wagtail_factories.StructBlockFactory):
class Meta:
model = VideoBlock
class AssignmentBlockFactory(wagtail_factories.StructBlockFactory):
description = "Beispiel Auftrag"
class Meta:
model = AssignmentBlock
class BookBlockFactory(wagtail_factories.StructBlockFactory):
description = "Beispiel Buch"
class Meta:
model = BookBlock
class DocumentBlockFactory(wagtail_factories.StructBlockFactory):
description = "Beispiel Dokument"
class Meta:
model = DocumentBlock
class ExerciseBlockFactory(wagtail_factories.StructBlockFactory):
description = "Beispiel Übung"
class Meta:
model = ExerciseBlock
class OnlineTrainingBlockFactory(wagtail_factories.StructBlockFactory):
url = "/static/media/web_based_trainings/rise_cmi5_test_export/scormcontent/index.html"
description = "Beispiel Rise Modul"
@ -93,20 +91,23 @@ class OnlineTrainingBlockFactory(wagtail_factories.StructBlockFactory):
class Meta:
model = OnlineTrainingBlock
class TestBlockFactory(wagtail_factories.StructBlockFactory):
description = "Beispiel Test"
class Meta:
model = TestBlock
class ResourceBlockFactory(wagtail_factories.StructBlockFactory):
description = "Beispiel Hilfsmittel"
class Meta:
model = ResourceBlock
class MediaLibraryBlockFactory(wagtail_factories.StructBlockFactory):
description = "Beispiel Mediathekeninhalt"
class Meta:
model = MediaLibraryBlock
model = MediaLibraryBlock

View File

@ -2,30 +2,27 @@ from rest_framework.test import APITestCase
from vbv_lernwelt.core.admin import User
from vbv_lernwelt.core.create_default_users import create_default_users
from vbv_lernwelt.core.tests.helpers import create_locales_for_wagtail
from vbv_lernwelt.course.creators.test_course import create_test_course
from vbv_lernwelt.learnpath.models import LearningPath
from vbv_lernwelt.learnpath.tests.create_simple_test_learning_path import create_simple_test_learning_path
class TestRetrieveLearingPathContents(APITestCase):
def setUp(self) -> None:
create_locales_for_wagtail()
create_default_users()
create_simple_test_learning_path()
create_test_course()
self.user = User.objects.get(username='student')
self.client.login(username='student', password='test')
def test_get_learnpathPage(self):
learning_path = LearningPath.objects.get(slug='unit-test-lernpfad')
response = self.client.get('/api/learnpath/page/unit-test-lernpfad/')
print(response)
slug = 'test-lehrgang-lp'
learning_path = LearningPath.objects.get(slug=slug)
response = self.client.get(f'/api/course/page/{slug}/')
self.assertEqual(response.status_code, 200)
data = response.json()
# print(data)
self.assertEqual(learning_path.title, data['title'])
# topic and circle
# topics and circles
self.assertEqual(4, len(data['children']))
# circle "unit-test-circle" contents
self.assertEqual(13, len(data['children'][3]['children']))
# circle "analyse" contents
self.assertEqual(12, len(data['children'][3]['children']))

View File

@ -1,23 +0,0 @@
# Create your views here.
import structlog
from django.views.decorators.cache import cache_page
from rest_framework.decorators import api_view
from rest_framework.response import Response
from wagtail.models import Page
logger = structlog.get_logger(__name__)
@api_view(['GET'])
@cache_page(60 * 60 * 8, cache="learning_path_cache")
def page_api_view(request, slug):
try:
page = Page.objects.get(slug=slug, locale__language_code='de-CH')
serializer = page.specific.get_serializer_class()(page.specific)
return Response(serializer.data)
except Exception as e:
logger.error(e)
return Response({"error": str(e)}, status=404)

View File

@ -0,0 +1,61 @@
from django.db import models
from wagtail import blocks
from wagtail.admin.panels import FieldPanel
from wagtail.documents.blocks import DocumentChooserBlock
from wagtail.snippets.models import register_snippet
@register_snippet
class MediaLibraryContent(models.Model):
title = models.TextField()
description = models.TextField()
link_display_text = models.CharField(max_length=255)
# TODO: Revisions only work with wagtail 4.0, can not migrate since wagtail localize is not ready yet.
# _revisions = GenericRelation("wagtailcore.Revision", related_query_name="media_library_content")
panels = [
FieldPanel('title'),
FieldPanel('description'),
FieldPanel('link_display_text'),
]
@property
def revisions(self):
return self._revisions
class AnchorBlock(blocks.PageChooserBlock):
"""
Verankerung im Lernpfad. Link to a Learning Content.
"""
page_type = 'learnpath.LearningUnit'
class LinkBlock(blocks.StructBlock):
title = blocks.TextBlock(blank=False, null=False)
description = blocks.TextBlock(default='')
link_display_text = blocks.CharBlock(max_length=255, default='Link öffnen')
url = blocks.URLBlock()
class CrossReferenceBlock(blocks.StructBlock):
title = models.TextField(blank=False, null=False)
description = blocks.TextBlock(default='')
link_display_text = blocks.CharBlock(max_length=255, default='Link öffnen')
category = blocks.PageChooserBlock(page_type='media_library.MediaCategoryPage')
class MediaContentCollection(blocks.StructBlock):
"""
Lernmedien, Links, Querverweise, Verankerung
"""
title = blocks.TextBlock()
contents = blocks.StreamBlock([
('Links', LinkBlock()),
('Documents', DocumentChooserBlock()),
('Ankers', AnchorBlock()),
('CrossReference', CrossReferenceBlock())
])
class Meta:
icon = 'link'

View File

@ -3,6 +3,7 @@ import os
import factory
from wagtail.core.models import Collection
from vbv_lernwelt.course.models import Course
from vbv_lernwelt.media_library.models import LibraryDocument
from vbv_lernwelt.media_library.tests.media_library_factories import LibraryDocumentFactory
@ -11,15 +12,11 @@ def create_default_collections():
c = Collection.objects.all().delete()
root, created = Collection.objects.get_or_create(name='Root', depth=0)
versicherungsvermittler = root.add_child(name='Versicherungsvermittler/in')
handlungsfelder = versicherungsvermittler.add_child(name='Handlungsfelder')
handlungsfelder_names = ['Fahrzeug', 'Reisen', 'Einkommensicherung', 'Gesundheit', 'Haushalt', 'Sparen',
'Pensionierung', 'KMU', 'Wohneigentum', 'Rechtsstreitigkeiten', 'Erben / Vererben',
'Selbständigkeit']
for handlungsfeld in handlungsfelder_names:
versicherungsvermittler = handlungsfelder.add_child(name=handlungsfeld)
for course in Course.objects.all():
course_collection = root.add_child(name=course.title)
for cat in course.coursecategory_set.all():
cat_collection = course_collection.add_child(name=cat.title)
def create_default_documents():
@ -33,7 +30,7 @@ def create_default_documents():
document = LibraryDocumentFactory(
title='V1 C25 ZGB CH',
display_text='Schweizerisches Zivilgesetzbuch',
description='Ein wundervolles Dokument, Bachblüten für Leseratten und metaphysisches Wolbefinden für Handyvekäufer.',
description='Ein wundervolles Dokument, Bachblüten für Leseratten und metaphysisches Wohlbefinden für Handyvekäufer.',
link_display_text='Dokument laden',
file=factory.django.FileField(from_path=os.path.join(path, filename), filename=filename),
collection=collection
@ -43,10 +40,8 @@ def create_default_documents():
document = LibraryDocumentFactory(
title='V1 C25 ',
display_text='Pdf showcase ',
description='Ein wundervolles Dokument, Bachblüten für Leseratten und metaphysisches Wolbefinden für Handyvekäufer.',
description='Ein wundervolles Dokument, Bachblüten für Leseratten und metaphysisches Wohlbefinden für Handyvekäufer.',
link_display_text='Dokument laden',
file=factory.django.FileField(from_path=os.path.join(path, filename), filename=filename),
collection=collection
)
pass

View File

@ -0,0 +1,54 @@
import json
from vbv_lernwelt.course.consts import COURSE_VERSICHERUNGSVERMITTLERIN_ID
from vbv_lernwelt.course.models import CoursePage, Course
from vbv_lernwelt.media_library.tests.media_library_factories import MediaLibraryPageFactory, MediaCategoryPageFactory, \
create_media_content_link, LinkBlockFactory, create_link_collection, create_document_collection
def create_default_media_library():
course = Course.objects.get(id=COURSE_VERSICHERUNGSVERMITTLERIN_ID)
course_page = CoursePage.objects.get(course_id=COURSE_VERSICHERUNGSVERMITTLERIN_ID)
media_lib_page = MediaLibraryPageFactory(
title='Mediathek',
parent=course_page,
)
icons = ['icon-hf-fahrzeug', 'icon-hf-reisen', 'icon-hf-einkommenssicherung', ]
for idx, cat in enumerate(course.coursecategory_set.all()):
overview_icon = icons[(idx + 2) % len(icons)]
introduction_text = '''
Das Auto ist für viele der grösste Stolz! Es birgt aber auch ein grosses Gefahrenpotenzial.
Dabei geht es bei den heutigen Fahrzeugpreisen und Reparaturkosten rasch um namhafte Summen,
die der Fahrzeugbesitzer und die Fahrzeugbesitzerin in einem grösseren Schadenfall oft nur schwer selbst aufbringen kann.'''.strip()
description_title = 'Das erwartet dich in diesem Handlungsfeld'
description_text = '''
In diesem berufstypischem Handlungsfeld lernst du alles rund um Motorfahrzeugversicherungen,
wie man sein Auto optimal schützen kann, wie du vorgehst bei einem Fahrzeugwechsel,
welche Aspekte du bei einer Offerte beachten musst und wie du dem Kunden die Lösung präsentierst.'''.strip()
items = [
('item', 'Motorfahrzeughaftpflichtversicherung'),
('item', 'Motorfahrzeugkaskoversicherung'),
('item', 'Insassenunfallversicherung'),
]
body_data = json.dumps([
create_document_collection(),
create_link_collection(
links_dict=[
create_media_content_link(LinkBlockFactory(title='Nationales Versicherungsbüro', url='https://www.vbv.ch/')),
create_media_content_link(LinkBlockFactory(title='Adressen der Strassenverkehrsämter', url='https://www.vbv.ch/')),
]
)
])
media_category = MediaCategoryPageFactory(
overview_icon=overview_icon,
title=cat.title,
course_category=cat,
parent=media_lib_page,
introduction_text=introduction_text,
description_title=description_title,
description_text=description_text,
items=items,
body=body_data,
)

View File

@ -1,10 +0,0 @@
import djclick as click
from vbv_lernwelt.media_library.create_default_documents import create_default_collections, create_default_documents
@click.command()
def command():
create_default_collections()
create_default_documents()

View File

@ -1,9 +1,13 @@
# Generated by Django 3.2.13 on 2022-08-16 08:35
# Generated by Django 3.2.13 on 2022-09-28 12:51
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import taggit.managers
import vbv_lernwelt.media_library.content_blocks
import wagtail.blocks
import wagtail.documents.blocks
import wagtail.fields
import wagtail.models.collections
import wagtail.search.index
@ -13,6 +17,7 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
('course', '0001_initial'),
('wagtailcore', '0069_log_entry_jsonfield'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('taggit', '0004_alter_taggeditem_content_type_alter_taggeditem_tag'),
@ -20,7 +25,43 @@ class Migration(migrations.Migration):
operations = [
migrations.CreateModel(
name='CustomDocument',
name='MediaLibraryContent',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.TextField()),
('description', models.TextField()),
('link_display_text', models.CharField(max_length=255)),
],
),
migrations.CreateModel(
name='MediaLibraryPage',
fields=[
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')),
],
options={
'abstract': False,
},
bases=('wagtailcore.page',),
),
migrations.CreateModel(
name='MediaCategoryPage',
fields=[
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')),
('introduction_text', models.TextField(default='')),
('description_title', models.TextField(default='Das erwartet dich in diesem Handlungsfeld')),
('description_text', models.TextField(default='')),
('items', wagtail.fields.StreamField([('item', wagtail.blocks.TextBlock())], use_json_field=True)),
('overview_icon', models.CharField(default='icon-hf-fahrzeug', max_length=255)),
('body', wagtail.fields.StreamField([('content_collection', wagtail.blocks.StructBlock([('title', wagtail.blocks.TextBlock()), ('contents', wagtail.blocks.StreamBlock([('Links', wagtail.blocks.StructBlock([('title', wagtail.blocks.TextBlock(blank=False, null=False)), ('description', wagtail.blocks.TextBlock(default='')), ('link_display_text', wagtail.blocks.CharBlock(default='Link öffnen', max_length=255)), ('url', wagtail.blocks.URLBlock())])), ('Documents', wagtail.documents.blocks.DocumentChooserBlock()), ('Ankers', vbv_lernwelt.media_library.content_blocks.AnchorBlock()), ('CrossReference', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock(default='')), ('link_display_text', wagtail.blocks.CharBlock(default='Link öffnen', max_length=255)), ('category', wagtail.blocks.PageChooserBlock(page_type=['media_library.MediaCategoryPage']))]))]))]))], null=True, use_json_field=True)),
('course_category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='course.coursecategory')),
],
options={
'abstract': False,
},
bases=('wagtailcore.page',),
),
migrations.CreateModel(
name='LibraryDocument',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=255, verbose_name='title')),

Some files were not shown because too many files have changed in this diff Show More