Merge branch 'feature/datenmodell' into develop
This commit is contained in:
commit
38c8d2120a
|
|
@ -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']) }"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}`"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
|
||||
import {reactive} from 'vue'
|
||||
import {Menu, MenuButton, MenuItems, MenuItem} from '@headlessui/vue'
|
||||
|
||||
const props = defineProps<{
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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']
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
if (completionData) {
|
||||
circle.parseCompletionData(completionData)
|
||||
}
|
||||
if (topic) {
|
||||
topic.circles.push(circle)
|
||||
}
|
||||
|
|
@ -59,17 +70,21 @@ export class LearningPath implements LearningWagtailPage {
|
|||
this.topics.push(topic)
|
||||
}
|
||||
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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}`;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
@ -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[];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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')
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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), ),
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
@ -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'),
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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',
|
||||
),
|
||||
]
|
||||
|
|
@ -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'),
|
||||
),
|
||||
]
|
||||
|
|
@ -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]}'
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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'])
|
||||
|
|
@ -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'),
|
||||
),
|
||||
]
|
||||
|
|
@ -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'
|
||||
)
|
||||
]
|
||||
|
|
@ -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',
|
||||
]
|
||||
|
||||
|
|
@ -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'])
|
||||
|
|
@ -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)
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
COURSE_TEST_ID = -1
|
||||
COURSE_VERSICHERUNGSVERMITTLERIN_ID = -2
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
|
|
@ -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'),
|
||||
),
|
||||
]
|
||||
|
|
@ -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'
|
||||
)
|
||||
]
|
||||
|
|
@ -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',
|
||||
]
|
||||
|
|
@ -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)
|
||||
|
|
@ -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')
|
||||
|
||||
|
|
@ -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)
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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()
|
||||
|
|
@ -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',),
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
]
|
||||
|
|
@ -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):
|
||||
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))
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
]
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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,18 +91,21 @@ 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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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']))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
@ -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'
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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()
|
||||
|
||||
|
|
@ -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
Loading…
Reference in New Issue