Merge branch 'develop' of bitbucket.org:iterativ/vbv_lernwelt into develop
This commit is contained in:
commit
db01be1726
|
|
@ -1,2 +1,2 @@
|
|||
nodejs 16.10.0
|
||||
nodejs 16.17.0
|
||||
python 3.10.5
|
||||
|
|
|
|||
|
|
@ -9,14 +9,27 @@ pipelines:
|
|||
services:
|
||||
- postgres
|
||||
caches:
|
||||
- pip
|
||||
- vbvpip
|
||||
script:
|
||||
- source ./env/bitbucket/prepare_for_test.sh
|
||||
- python -m venv vbvvenv
|
||||
- source vbvvenv/bin/activate
|
||||
- pip install -r server/requirements/requirements-dev.txt
|
||||
- git-crypt status -e | sort > git-crypt-encrypted-files-check.txt && diff git-crypt-encrypted-files.txt git-crypt-encrypted-files-check.txt
|
||||
- trufflehog --exclude_paths trufflehog-exclude-patterns.txt --allow trufflehog-allow.json --entropy=True --max_depth=100 .
|
||||
- ./server/run_tests_coverage.sh
|
||||
# - ./src/run_pylint.sh
|
||||
- step:
|
||||
name: js tests
|
||||
max-time: 15
|
||||
caches:
|
||||
- node
|
||||
- clientnode
|
||||
script:
|
||||
- cd client
|
||||
- pwd
|
||||
- npm install
|
||||
- npm test
|
||||
- step:
|
||||
name: cypress tests
|
||||
max-time: 45
|
||||
|
|
@ -27,21 +40,20 @@ pipelines:
|
|||
- cypress/**/*.mp4
|
||||
caches:
|
||||
- node
|
||||
- pip
|
||||
- clientnode
|
||||
- vbvpip
|
||||
- cypress
|
||||
script:
|
||||
- export IT_SERVE_VUE=false
|
||||
- export IT_ALLOW_LOCAL_LOGIN=true
|
||||
- source ./env/bitbucket/prepare_for_test.sh
|
||||
- pip install -r server/requirements/requirements-dev.txt
|
||||
- npm install
|
||||
- npm run build
|
||||
- python -m venv vbvvenv
|
||||
- source vbvvenv/bin/activate
|
||||
- pip install -r server/requirements/requirements-dev.txt
|
||||
- ./prepare_server_cypress.sh --start-background
|
||||
- npm run cypress:ci
|
||||
# - npm run build
|
||||
# - ./run_jshint.sh
|
||||
# # - npm test
|
||||
# - (cd landingpage && npm install && echo "{}" > ./src/translations/translations.json && npm run build)
|
||||
tags:
|
||||
v202*:
|
||||
- step:
|
||||
|
|
@ -72,6 +84,7 @@ definitions:
|
|||
caches:
|
||||
cypress: /root/.cache/Cypress
|
||||
vbvpip: /opt/atlassian/pipelines/agent/build/vbvvenv/
|
||||
clientnode: /opt/atlassian/pipelines/agent/build/client/node_modules/
|
||||
services:
|
||||
postgres:
|
||||
image: postgres
|
||||
|
|
|
|||
|
|
@ -7,52 +7,50 @@
|
|||
"build:tailwind": "tailwindcss -i tailwind.css -o ../server/vbv_lernwelt/static/css/tailwind.css --minify",
|
||||
"test": "vitest run",
|
||||
"coverage": "vitest run --coverage",
|
||||
"typecheck": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false",
|
||||
"typecheck": "vue-tsc --noEmit -p tsconfig.app.json --composite false",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
||||
"tailwind": "tailwindcss -i tailwind.css -o ../server/vbv_lernwelt/static/css/tailwind.css --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/vue": "^1.6.4",
|
||||
"@headlessui/vue": "^1.6.7",
|
||||
"axios": "^0.26.1",
|
||||
"d3": "^7.4.4",
|
||||
"d3": "^7.6.1",
|
||||
"lodash": "^4.17.21",
|
||||
"loglevel": "^1.8.0",
|
||||
"pinia": "^2.0.13",
|
||||
"underscore": "^1.13.4",
|
||||
"vue": "^3.2.31",
|
||||
"vue-i18n": "^9.1.9",
|
||||
"vue-router": "^4.0.14"
|
||||
"pinia": "^2.0.21",
|
||||
"vue": "^3.2.38",
|
||||
"vue-i18n": "^9.2.2",
|
||||
"vue-router": "^4.1.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@intlify/vite-plugin-vue-i18n": "^3.4.0",
|
||||
"@rollup/plugin-alias": "^3.1.9",
|
||||
"@rushstack/eslint-patch": "^1.1.0",
|
||||
"@rushstack/eslint-patch": "^1.1.4",
|
||||
"@tailwindcss/forms": "^0.5.2",
|
||||
"@tailwindcss/typography": "^0.5.4",
|
||||
"@testing-library/vue": "^6.6.0",
|
||||
"@testing-library/vue": "^6.6.1",
|
||||
"@types/d3": "^7.4.0",
|
||||
"@types/jsdom": "^16.2.14",
|
||||
"@types/node": "^16.11.26",
|
||||
"@vitejs/plugin-vue": "^2.3.1",
|
||||
"@types/jsdom": "^20.0.0",
|
||||
"@types/lodash": "^4.14.184",
|
||||
"@types/node": "^18.7.14",
|
||||
"@vitejs/plugin-vue": "^3.0.3",
|
||||
"@vue/eslint-config-prettier": "^7.0.0",
|
||||
"@vue/eslint-config-typescript": "^10.0.0",
|
||||
"@vue/test-utils": "^2.0.0-rc.18",
|
||||
"@vue/eslint-config-typescript": "^11.0.0",
|
||||
"@vue/test-utils": "^2.0.2",
|
||||
"@vue/tsconfig": "^0.1.3",
|
||||
"autoprefixer": "^10.4.7",
|
||||
"cypress": "^9.5.3",
|
||||
"eslint": "^8.5.0",
|
||||
"eslint-plugin-cypress": "^2.12.1",
|
||||
"eslint-plugin-vue": "^8.2.0",
|
||||
"happy-dom": "^5.3.1",
|
||||
"autoprefixer": "^10.4.8",
|
||||
"eslint": "8.22.0",
|
||||
"eslint-plugin-vue": "^9.4.0",
|
||||
"jsdom": "^20.0.0",
|
||||
"postcss": "^8.4.14",
|
||||
"postcss-import": "^14.1.0",
|
||||
"prettier": "^2.5.1",
|
||||
"sass": "^1.50.1",
|
||||
"prettier": "^2.7.1",
|
||||
"sass": "^1.54.6",
|
||||
"sass-loader": "^12.6.0",
|
||||
"start-server-and-test": "^1.14.0",
|
||||
"tailwindcss": "^3.1.4",
|
||||
"typescript": "~4.6.3",
|
||||
"vite": "^2.9.1",
|
||||
"vitest": "^0.15.1",
|
||||
"vue-tsc": "^0.33.9"
|
||||
"tailwindcss": "^3.1.8",
|
||||
"typescript": "^4.8.2",
|
||||
"vite": "^3.0.9",
|
||||
"vitest": "^0.22.1",
|
||||
"vue-tsc": "^0.40.4"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 50 KiB |
|
|
@ -1,27 +1,27 @@
|
|||
<script setup lang="ts">
|
||||
import * as log from 'loglevel';
|
||||
import * as log from 'loglevel'
|
||||
|
||||
import {onMounted, reactive} from 'vue';
|
||||
import {useUserStore} from '@/stores/user';
|
||||
import {useLearningPathStore} from '@/stores/learningPath';
|
||||
import {useRoute, useRouter} from 'vue-router';
|
||||
import {useAppStore} from '@/stores/app';
|
||||
import IconLogout from "@/components/icons/IconLogout.vue";
|
||||
import IconSettings from "@/components/icons/IconSettings.vue";
|
||||
import ItDropdown from "@/components/ui/ItDropdown.vue";
|
||||
import MobileMenu from "@/components/MobileMenu.vue"
|
||||
import { onMounted, reactive } from 'vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { useLearningPathStore } from '@/stores/learningPath'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import IconLogout from '@/components/icons/IconLogout.vue'
|
||||
import IconSettings from '@/components/icons/IconSettings.vue'
|
||||
import ItDropdown from '@/components/ui/ItDropdown.vue'
|
||||
import MobileMenu from '@/components/MobileMenu.vue'
|
||||
|
||||
log.debug('MainNavigationBar created');
|
||||
log.debug('MainNavigationBar created')
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore();
|
||||
const appStore = useAppStore();
|
||||
const learningPathStore = useLearningPathStore();
|
||||
const state = reactive({showMenu: false});
|
||||
const userStore = useUserStore()
|
||||
const appStore = useAppStore()
|
||||
const learningPathStore = useLearningPathStore()
|
||||
const state = reactive({ showMenu: false })
|
||||
|
||||
function toggleNav() {
|
||||
state.showMenu = !state.showMenu;
|
||||
state.showMenu = !state.showMenu
|
||||
}
|
||||
|
||||
function isInRoutePath(checkPaths: string[]) {
|
||||
|
|
@ -29,28 +29,21 @@ function isInRoutePath(checkPaths: string[]) {
|
|||
}
|
||||
|
||||
function inLearningPath() {
|
||||
return isInRoutePath(['/learningpath/', '/circle/']);
|
||||
return isInRoutePath(['/learn/'])
|
||||
}
|
||||
|
||||
function getLearningPathStringProp (prop: 'title' | 'slug'): string {
|
||||
return inLearningPath() && learningPathStore.learningPath ? learningPathStore.learningPath[prop] : '';
|
||||
function getLearningPathStringProp(prop: 'title' | 'slug'): string {
|
||||
return inLearningPath() && learningPathStore.learningPath ? learningPathStore.learningPath[prop] : ''
|
||||
}
|
||||
|
||||
function learningPathName (): string {
|
||||
function learningPathName(): string {
|
||||
return getLearningPathStringProp('title')
|
||||
}
|
||||
|
||||
function learninPathSlug (): string {
|
||||
function learninPathSlug(): string {
|
||||
return getLearningPathStringProp('slug')
|
||||
}
|
||||
|
||||
function backButtonUrl() {
|
||||
if (route.path.startsWith('/circle/')) {
|
||||
return '/learningpath/versicherungsvermittlerin';
|
||||
}
|
||||
return '/';
|
||||
}
|
||||
|
||||
function handleDropdownSelect(data) {
|
||||
log.debug('Selected action:', data.action)
|
||||
switch (data.action) {
|
||||
|
|
@ -58,42 +51,41 @@ function handleDropdownSelect(data) {
|
|||
router.push('/profile')
|
||||
break
|
||||
case 'logout':
|
||||
userStore.handleLogout();
|
||||
userStore.handleLogout()
|
||||
break
|
||||
default:
|
||||
console.log('no action')
|
||||
}
|
||||
}
|
||||
|
||||
function logout () {
|
||||
userStore.handleLogout();
|
||||
function logout() {
|
||||
userStore.handleLogout()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
log.debug('MainNavigationBar mounted');
|
||||
log.debug('MainNavigationBar mounted')
|
||||
})
|
||||
|
||||
const profileDropdownData = [
|
||||
[
|
||||
{
|
||||
title: 'Kontoeinstellungen',
|
||||
icon: IconSettings,
|
||||
data: {
|
||||
action: 'settings'
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
title: 'Abmelden',
|
||||
icon: IconLogout,
|
||||
data: {
|
||||
action: 'logout'
|
||||
}
|
||||
[
|
||||
{
|
||||
title: 'Kontoeinstellungen',
|
||||
icon: IconSettings,
|
||||
data: {
|
||||
action: 'settings',
|
||||
},
|
||||
]
|
||||
]
|
||||
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
title: 'Abmelden',
|
||||
icon: IconLogout,
|
||||
data: {
|
||||
action: 'logout',
|
||||
},
|
||||
},
|
||||
],
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -109,35 +101,14 @@ const profileDropdownData = [
|
|||
</Teleport>
|
||||
<Transition name="nav">
|
||||
<div v-if="appStore.showMainNavigationBar" class="navigation bg-blue-900">
|
||||
<nav
|
||||
class="
|
||||
px-8
|
||||
py-2
|
||||
mx-auto
|
||||
lg:flex lg:justify-start lg:items-center lg:py-4
|
||||
"
|
||||
>
|
||||
<nav class="px-8 py-2 mx-auto lg:flex lg:justify-start lg:items-center lg:py-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<a
|
||||
href="https://www.vbv.ch"
|
||||
class="flex">
|
||||
<it-icon-vbv class="h-8 w-16 mr-3 -mt-6 -ml-3"/>
|
||||
<a href="https://www.vbv.ch" class="flex">
|
||||
<it-icon-vbv class="h-8 w-16 mr-3 -mt-6 -ml-3" />
|
||||
</a>
|
||||
<router-link
|
||||
to="/"
|
||||
class="flex">
|
||||
<div class="
|
||||
text-white
|
||||
text-2xl
|
||||
pr-10
|
||||
pl-3
|
||||
ml-1
|
||||
border-l border-white
|
||||
"
|
||||
>
|
||||
myVBV
|
||||
</div>
|
||||
<router-link to="/" class="flex">
|
||||
<div class="text-white text-2xl pr-10 pl-3 ml-1 border-l border-white">myVBV</div>
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
|
|
@ -148,21 +119,15 @@ const profileDropdownData = [
|
|||
class="nav-item flex flex-row items-center"
|
||||
data-cy="messages-link"
|
||||
>
|
||||
<it-icon-message class="w-8 h-8 mr-6"/>
|
||||
<it-icon-message class="w-8 h-8 mr-6" />
|
||||
</router-link>
|
||||
<!-- Mobile menu button -->
|
||||
<div @click="toggleNav" class="flex">
|
||||
<button
|
||||
type="button"
|
||||
class="
|
||||
w-8
|
||||
h-8
|
||||
text-white
|
||||
hover:text-sky-500
|
||||
focus:outline-none focus:text-sky-500
|
||||
"
|
||||
class="w-8 h-8 text-white hover:text-sky-500 focus:outline-none focus:text-sky-500"
|
||||
>
|
||||
<it-icon-menu class="h-8 w-8"/>
|
||||
<it-icon-menu class="h-8 w-8" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -172,17 +137,13 @@ const profileDropdownData = [
|
|||
<div
|
||||
v-if="appStore.userLoaded && appStore.routingFinished && userStore.loggedIn"
|
||||
:class="state.showMenu ? 'flex' : 'hidden'"
|
||||
class="
|
||||
flex-auto
|
||||
mt-8
|
||||
lg:flex lg:space-y-0 lg:flex-row lg:items-center lg:space-x-10 lg:mt-0
|
||||
"
|
||||
class="flex-auto mt-8 lg:flex lg:space-y-0 lg:flex-row lg:items-center lg:space-x-10 lg:mt-0"
|
||||
>
|
||||
<router-link
|
||||
v-if="inLearningPath()"
|
||||
to="/learningpath/versicherungsvermittlerin"
|
||||
class="nav-item"
|
||||
:class="{'nav-item--active': inLearningPath()}"
|
||||
:class="{ 'nav-item--active': inLearningPath() }"
|
||||
>
|
||||
Lernpfad
|
||||
</router-link>
|
||||
|
|
@ -191,32 +152,24 @@ const profileDropdownData = [
|
|||
v-if="inLearningPath()"
|
||||
to="/competences/"
|
||||
class="nav-item"
|
||||
:class="{'nav-item--active': isInRoutePath(['/competences/'])}"
|
||||
:class="{ 'nav-item--active': isInRoutePath(['/competences/']) }"
|
||||
>
|
||||
Kompetenzprofil
|
||||
</router-link>
|
||||
|
||||
<div class="hidden lg:block flex-auto"></div>
|
||||
<router-link
|
||||
to="/shop"
|
||||
class="nav-item"
|
||||
:class="{'nav-item--active': isInRoutePath(['/shop'])}"
|
||||
>
|
||||
<router-link to="/shop" class="nav-item" :class="{ 'nav-item--active': isInRoutePath(['/shop']) }">
|
||||
Shop
|
||||
</router-link>
|
||||
<router-link
|
||||
to="/mediacenter"
|
||||
class="nav-item"
|
||||
:class="{'nav-item--active': isInRoutePath(['/mediacenter'])}"
|
||||
:class="{ 'nav-item--active': isInRoutePath(['/mediacenter']) }"
|
||||
>
|
||||
Mediathek
|
||||
</router-link>
|
||||
<router-link
|
||||
to="/messages"
|
||||
class="nav-item flex flex-row items-center"
|
||||
data-cy="messages-link"
|
||||
>
|
||||
<it-icon-message class="w-8 h-8 mr-6"/>
|
||||
<router-link to="/messages" class="nav-item flex flex-row items-center" data-cy="messages-link">
|
||||
<it-icon-message class="w-8 h-8 mr-6" />
|
||||
</router-link>
|
||||
<div class="nav-item flex items-center" v-if="userStore.loggedIn">
|
||||
<ItDropdown
|
||||
|
|
@ -226,9 +179,7 @@ const profileDropdownData = [
|
|||
@select="handleDropdownSelect"
|
||||
>
|
||||
<div v-if="userStore.avatar_url">
|
||||
<img class="inline-block h-8 w-8 rounded-full"
|
||||
:src="userStore.avatar_url"
|
||||
alt=""/>
|
||||
<img class="inline-block h-8 w-8 rounded-full" :src="userStore.avatar_url" alt="" />
|
||||
</div>
|
||||
<div v-else>
|
||||
{{ userStore.getFullName }}
|
||||
|
|
@ -249,7 +200,7 @@ const profileDropdownData = [
|
|||
}
|
||||
|
||||
.nav-item--active {
|
||||
@apply underline underline-offset-[21px] decoration-sky-500 decoration-4
|
||||
@apply underline underline-offset-[21px] decoration-sky-500 decoration-4;
|
||||
}
|
||||
|
||||
.nav-enter-active,
|
||||
|
|
@ -262,5 +213,4 @@ const profileDropdownData = [
|
|||
opacity: 0;
|
||||
transform: translateY(-80px);
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,18 @@
|
|||
import {describe, expect, it} from 'vitest'
|
||||
|
||||
import {mount} from '@vue/test-utils'
|
||||
import { describe, it } from 'vitest'
|
||||
import MainNavigationBar from '../MainNavigationBar.vue'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
|
||||
describe('MainNavigationBar', () => {
|
||||
beforeEach(() => {
|
||||
// creates a fresh pinia and make it active so it's automatically picked
|
||||
// up by any useStore() call without having to pass it to it:
|
||||
// `useStore(pinia)`
|
||||
setActivePinia(createPinia())
|
||||
})
|
||||
|
||||
it('renders properly', () => {
|
||||
const wrapper = mount(MainNavigationBar, {})
|
||||
expect(wrapper.text()).toContain('myVBV')
|
||||
expect(42).toBe(42)
|
||||
// const wrapper = mount(MainNavigationBar, {})
|
||||
// expect(wrapper.text()).toContain('myVBV')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -36,8 +36,8 @@ const pieData = computed(() => {
|
|||
if (circle) {
|
||||
console.log('initial of compute pie data ', circle)
|
||||
|
||||
let pieWeights = new Array(Math.max(circle.learningSequences.length, 1)).fill(1)
|
||||
let pieGenerator = d3.pie()
|
||||
const pieWeights = new Array(Math.max(circle.learningSequences.length, 1)).fill(1)
|
||||
const pieGenerator = d3.pie()
|
||||
let angles = pieGenerator(pieWeights)
|
||||
_.forEach(angles, (pie) => {
|
||||
const thisLearningSequence = circle.learningSequences[parseInt(pie.index)]
|
||||
|
|
@ -214,7 +214,7 @@ function render() {
|
|||
|
||||
// remove last arrow
|
||||
d3.selection.prototype.last = function () {
|
||||
let last = this.size() - 1;
|
||||
const last = this.size() - 1;
|
||||
return d3.select(this.nodes()[last]);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,27 +1,23 @@
|
|||
<script setup lang="ts">
|
||||
import {Circle} from '@/services/circle';
|
||||
import { Circle } from '@/services/circle'
|
||||
import ItFullScreenModal from '@/components/ui/ItFullScreenModal.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
circle: Circle,
|
||||
circle: Circle
|
||||
show: boolean
|
||||
}>()
|
||||
|
||||
const emits = defineEmits(['closemodal'])
|
||||
|
||||
// const emits = defineEmits(['closemodal'])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ItFullScreenModal
|
||||
:show="show"
|
||||
@closemodal="$emit('closemodal')"
|
||||
>
|
||||
<ItFullScreenModal :show="show" @closemodal="$emit('closemodal')">
|
||||
<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>
|
||||
|
||||
<div class="mt-8 p-4 border border-gray-500">
|
||||
<h3>Du wirst in der Lage sein, ... </h3>
|
||||
<h3>Du wirst in der Lage sein, ...</h3>
|
||||
|
||||
<ul class="mt-4">
|
||||
<li class="text-xl flex items-center" v-for="goal in circle.goals" :key="goal.id">
|
||||
|
|
@ -31,9 +27,7 @@ const emits = defineEmits(['closemodal'])
|
|||
</ul>
|
||||
</div>
|
||||
|
||||
<h3 class="mt-16">
|
||||
Du wirst dein neu erworbenes Wissen auf folgenden berufstypischen Situation anwenden können:
|
||||
</h3>
|
||||
<h3 class="mt-16">Du wirst dein neu erworbenes Wissen auf folgenden berufstypischen Situation anwenden können:</h3>
|
||||
|
||||
<ul class="grid grid-cols-1 lg:grid-cols-3 auto-rows-fr gap-6 mt-8">
|
||||
<li
|
||||
|
|
@ -41,11 +35,10 @@ const emits = defineEmits(['closemodal'])
|
|||
:key="jobSituation.id"
|
||||
class="job-situation border border-gray-500 p-4 text-xl flex items-center"
|
||||
>
|
||||
{{jobSituation.value}}
|
||||
{{ jobSituation.value }}
|
||||
</li>
|
||||
</ul>
|
||||
</ItFullScreenModal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
<style scoped></style>
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ const block = computed(() => {
|
|||
<span class="hidden lg:inline">zurück zum Circle</span>
|
||||
</button>
|
||||
|
||||
<h1 class="text-xl hidden lg:block">{{ learningContent.title }}</h1>
|
||||
<h1 class="text-xl hidden lg:block">{{ learningContent?.title }}</h1>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import * as d3 from 'd3';
|
||||
import { useLearningPathStore } from '../../stores/learningPath';
|
||||
import colors from '@/colors.json';
|
||||
import * as d3 from 'd3'
|
||||
import { useLearningPathStore } from '../../stores/learningPath'
|
||||
import colors from '@/colors.json'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
|
|
@ -15,72 +15,69 @@ export default {
|
|||
},
|
||||
vertical: {
|
||||
default: false,
|
||||
type: Boolean
|
||||
type: Boolean,
|
||||
},
|
||||
identifier: {
|
||||
required: true,
|
||||
type: String
|
||||
}
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const learningPathStore = useLearningPathStore()
|
||||
return {learningPathStore}
|
||||
return { learningPathStore }
|
||||
},
|
||||
|
||||
|
||||
computed: {
|
||||
viewBox() {
|
||||
return `0 0 ${this.width} ${this.height * 1.5}`
|
||||
},
|
||||
circles() {
|
||||
|
||||
function someFinished(circle, learningSequence) {
|
||||
if (circle) {
|
||||
return circle.someFinishedInLearningSequence(learningSequence.translation_key);
|
||||
return circle.someFinishedInLearningSequence(learningSequence.translation_key)
|
||||
}
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
|
||||
function allFinished(circle, learningSequence) {
|
||||
if (circle) {
|
||||
return circle.allFinishedInLearningSequence(learningSequence.translation_key);
|
||||
return circle.allFinishedInLearningSequence(learningSequence.translation_key)
|
||||
}
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
|
||||
if (this.learningPathStore.learningPath) {
|
||||
let internalCircles = []
|
||||
const internalCircles = []
|
||||
this.learningPathStore.learningPath.circles.forEach((circle) => {
|
||||
const pieWeights = new Array(Math.max(circle.learningSequences.length, 1)).fill(1)
|
||||
const pieGenerator = d3.pie()
|
||||
let pieData = pieGenerator(pieWeights)
|
||||
const pieData = pieGenerator(pieWeights)
|
||||
pieData.forEach((pie) => {
|
||||
const thisLearningSequence = circle.learningSequences[parseInt(pie.index)]
|
||||
pie.startAngle = pie.startAngle + Math.PI
|
||||
pie.endAngle = pie.endAngle + Math.PI
|
||||
pie.done = circle.someFinishedInLearningSequence(thisLearningSequence.translation_key);
|
||||
pie.done = circle.someFinishedInLearningSequence(thisLearningSequence.translation_key)
|
||||
pie.someFinished = someFinished(circle, thisLearningSequence)
|
||||
pie.allFinished = allFinished(circle, thisLearningSequence)
|
||||
});
|
||||
let newCircle = {}
|
||||
})
|
||||
const newCircle = {}
|
||||
newCircle.pieData = pieData.reverse()
|
||||
newCircle.title = circle.title
|
||||
newCircle.slug = circle.slug
|
||||
newCircle.id = circle.id
|
||||
internalCircles.push(newCircle)
|
||||
});
|
||||
})
|
||||
return internalCircles
|
||||
}
|
||||
return [];
|
||||
return []
|
||||
},
|
||||
svg() {
|
||||
return d3.select("#" + this.identifier)
|
||||
return d3.select('#' + this.identifier)
|
||||
},
|
||||
|
||||
learningPath() {
|
||||
return Object.assign({}, this.learningPathStore.learningPath)
|
||||
}
|
||||
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
|
|
@ -109,8 +106,7 @@ export default {
|
|||
return color
|
||||
}
|
||||
|
||||
let vueRouter = this.$router
|
||||
|
||||
const vueRouter = this.$router
|
||||
|
||||
// Create append pie charts to the main svg
|
||||
const circle_groups = this.svg
|
||||
|
|
@ -121,7 +117,7 @@ export default {
|
|||
.attr('class', 'circle')
|
||||
.attr('data-cy', (d) => {
|
||||
if (this.vertical) {
|
||||
return `circle-${d.slug}-vertical`;
|
||||
return `circle-${d.slug}-vertical`
|
||||
} else {
|
||||
return `circle-${d.slug}`
|
||||
}
|
||||
|
|
@ -144,8 +140,8 @@ export default {
|
|||
return getColor(d)
|
||||
})
|
||||
})
|
||||
.on('click', function (d, i) {
|
||||
vueRouter.push('/circle/' + i.slug)
|
||||
.on('click', (d, i) => {
|
||||
vueRouter.push(`/learn/${this.learningPathStore.learningPath.slug}/${i.slug}`)
|
||||
})
|
||||
|
||||
.attr('role', 'button')
|
||||
|
|
@ -178,7 +174,6 @@ export default {
|
|||
//Draw arc paths
|
||||
arcs.append('path').attr('d', arcGenerator)
|
||||
|
||||
|
||||
const circlesText = circle_groups
|
||||
.append('text')
|
||||
.attr('fill', colors.blue[900])
|
||||
|
|
@ -201,7 +196,7 @@ export default {
|
|||
let pos = topicHeightOffset
|
||||
|
||||
for (let index = 0; index < i; index++) {
|
||||
let topic = topics[index]
|
||||
const topic = topics[index]
|
||||
if (topic.is_visible) {
|
||||
pos += topicHeight
|
||||
}
|
||||
|
|
@ -218,26 +213,18 @@ export default {
|
|||
y += topicHeight
|
||||
}
|
||||
for (let circle_index = 0; circle_index < topic.circles.length; circle_index++) {
|
||||
let circle = topic.circles[circle_index]
|
||||
const circle = topic.circles[circle_index]
|
||||
if (circle.id === d.id) {
|
||||
return y
|
||||
}
|
||||
y += circleHeigth
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const topicGroups = this.svg
|
||||
.selectAll('.topic')
|
||||
.data(this.learningPath.topics)
|
||||
.enter()
|
||||
.append('g')
|
||||
const topicGroups = this.svg.selectAll('.topic').data(this.learningPath.topics).enter().append('g')
|
||||
|
||||
const topicLines = topicGroups
|
||||
.append('line')
|
||||
.attr('class', 'stroke-gray-500')
|
||||
.attr('stroke-width', 1)
|
||||
const topicLines = topicGroups.append('line').attr('class', 'stroke-gray-500').attr('stroke-width', 1)
|
||||
|
||||
const topicTitles = topicGroups
|
||||
.append('text')
|
||||
|
|
@ -245,47 +232,40 @@ export default {
|
|||
.style('font-size', 16)
|
||||
.text((d) => d.title)
|
||||
|
||||
|
||||
// Calculate positions of objects
|
||||
|
||||
if (this.vertical) {
|
||||
const Circles_X = 60
|
||||
const Topics_X = Circles_X - radius
|
||||
|
||||
|
||||
circle_groups
|
||||
.attr('transform', (d, i) => {
|
||||
return 'translate(' + Circles_X + ',' + getCircleVerticalPostion(i, d, this.learningPath.topics) + ')'
|
||||
})
|
||||
circle_groups.attr('transform', (d, i) => {
|
||||
return 'translate(' + Circles_X + ',' + getCircleVerticalPostion(i, d, this.learningPath.topics) + ')'
|
||||
})
|
||||
|
||||
circlesText
|
||||
.attr('y', 7)
|
||||
.attr('x', radius + 40)
|
||||
.attr('class', 'circlesText text-xl font-bold block')
|
||||
|
||||
|
||||
topicGroups
|
||||
.attr('transform', (d, i) => {
|
||||
return "translate(" + Topics_X + ", " + getTopicVerticalPosition(i, d, this.learningPath.topics) + ")"
|
||||
return 'translate(' + Topics_X + ', ' + getTopicVerticalPosition(i, d, this.learningPath.topics) + ')'
|
||||
})
|
||||
.attr('class', (d) => {
|
||||
return 'topic '.concat(d.is_visible ? "block" : "hidden")
|
||||
return 'topic '.concat(d.is_visible ? 'block' : 'hidden')
|
||||
})
|
||||
|
||||
|
||||
topicLines
|
||||
.transition().duration('1000').attr('x2', this.width * 0.8)
|
||||
|
||||
topicTitles
|
||||
.attr('y', 30)
|
||||
|
||||
.transition()
|
||||
.duration('1000')
|
||||
.attr('x2', this.width * 0.8)
|
||||
|
||||
topicTitles.attr('y', 30)
|
||||
} else {
|
||||
circle_groups
|
||||
.attr('transform', (d, i) => {
|
||||
let x_coord = (i + 1) * circleWidth - radius
|
||||
return 'translate(' + x_coord + ', 200)'
|
||||
})
|
||||
circle_groups.attr('transform', (d, i) => {
|
||||
const x_coord = (i + 1) * circleWidth - radius
|
||||
return 'translate(' + x_coord + ', 200)'
|
||||
})
|
||||
|
||||
circlesText
|
||||
.attr('y', radius + 30)
|
||||
|
|
@ -293,30 +273,28 @@ export default {
|
|||
.call(wrap, circleWidth - 20)
|
||||
.attr('class', 'circlesText text-xl font-bold hidden lg:block')
|
||||
|
||||
|
||||
topicGroups
|
||||
.attr('transform', (d, i) => {
|
||||
return "translate(" + getTopicHorizontalPosition(i, d, this.learningPathStore.learningPath.topics) + ",0)"
|
||||
return 'translate(' + getTopicHorizontalPosition(i, d, this.learningPathStore.learningPath.topics) + ',0)'
|
||||
})
|
||||
.attr('class', (d) => {
|
||||
return 'topic '.concat(d.is_visible ? "hidden lg:block" : "hidden")
|
||||
return 'topic '.concat(d.is_visible ? 'hidden lg:block' : 'hidden')
|
||||
})
|
||||
|
||||
|
||||
topicLines
|
||||
.attr('x1', -10)
|
||||
.attr('y1', 0)
|
||||
.attr('x2', -10)
|
||||
.attr('y2', 0)
|
||||
.transition().duration('1000').attr('y2', 350)
|
||||
.transition()
|
||||
.duration('1000')
|
||||
.attr('y2', 350)
|
||||
|
||||
topicTitles
|
||||
.attr('y', 20)
|
||||
.style('font-size', 19)
|
||||
.call(wrap, circleWidth * 0.8)
|
||||
.attr('class', 'topicTitles font-bold')
|
||||
|
||||
|
||||
}
|
||||
|
||||
function wrap(text, width) {
|
||||
|
|
@ -357,12 +335,8 @@ export default {
|
|||
}
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<div class="svg-container h-full content-start">
|
||||
<svg class="learning-path-visualization h-full" :viewBox="viewBox" :id=identifier>
|
||||
</svg>
|
||||
<svg class="learning-path-visualization h-full" :viewBox="viewBox" :id="identifier"></svg>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,37 +1,37 @@
|
|||
<script setup lang="ts">
|
||||
import ItCheckbox from '@/components/ui/ItCheckbox.vue';
|
||||
import type {LearningContent, LearningSequence} from '@/types';
|
||||
import {useCircleStore} from '@/stores/circle';
|
||||
import {computed} from 'vue';
|
||||
import ItCheckbox from '@/components/ui/ItCheckbox.vue'
|
||||
import type { LearningContent, LearningSequence } from '@/types'
|
||||
import { useCircleStore } from '@/stores/circle'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
learningSequence: LearningSequence
|
||||
}>()
|
||||
|
||||
const circleStore = useCircleStore();
|
||||
const circleStore = useCircleStore()
|
||||
|
||||
function toggleCompleted(learningContent: LearningContent) {
|
||||
circleStore.markCompletion(learningContent, !learningContent.completed);
|
||||
circleStore.markCompletion(learningContent, !learningContent.completed)
|
||||
}
|
||||
|
||||
const someFinished = computed(() => {
|
||||
if (props.learningSequence && circleStore.circle) {
|
||||
return circleStore.circle.someFinishedInLearningSequence(props.learningSequence.translation_key);
|
||||
return circleStore.circle.someFinishedInLearningSequence(props.learningSequence.translation_key)
|
||||
}
|
||||
|
||||
return false;
|
||||
return false
|
||||
})
|
||||
|
||||
const allFinished = computed(() => {
|
||||
if (props.learningSequence && circleStore.circle) {
|
||||
return circleStore.circle.allFinishedInLearningSequence(props.learningSequence.translation_key);
|
||||
return circleStore.circle.allFinishedInLearningSequence(props.learningSequence.translation_key)
|
||||
}
|
||||
|
||||
return false;
|
||||
return false
|
||||
})
|
||||
|
||||
const learningSequenceBorderClass = computed(() => {
|
||||
let result = [];
|
||||
let result = []
|
||||
if (props.learningSequence && circleStore.circle) {
|
||||
if (allFinished.value) {
|
||||
result = ['border-l-4', 'border-l-green-500']
|
||||
|
|
@ -42,9 +42,8 @@ const learningSequenceBorderClass = computed(() => {
|
|||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
return result
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -57,15 +56,8 @@ const learningSequenceBorderClass = computed(() => {
|
|||
<div>{{ learningSequence.minutes }} Minuten</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="bg-white px-4 lg:px-6 border border-gray-500"
|
||||
:class="learningSequenceBorderClass"
|
||||
>
|
||||
<div
|
||||
v-for="learningUnit in learningSequence.learningUnits"
|
||||
:key="learningUnit.id"
|
||||
class="pt-3 lg:pt-6"
|
||||
>
|
||||
<div class="bg-white px-4 lg:px-6 border border-gray-500" :class="learningSequenceBorderClass">
|
||||
<div v-for="learningUnit in learningSequence.learningUnits" :key="learningUnit.id" class="pt-3 lg:pt-6">
|
||||
<div class="pb-3 lg:pg-6 flex gap-4 text-blue-900" v-if="learningUnit.title">
|
||||
<div class="font-semibold">{{ learningUnit.title }}</div>
|
||||
<div>{{ learningUnit.minutes }} Minuten</div>
|
||||
|
|
@ -79,48 +71,36 @@ const learningSequenceBorderClass = computed(() => {
|
|||
<ItCheckbox
|
||||
:modelValue="learningContent.completed"
|
||||
@click="toggleCompleted(learningContent)"
|
||||
:data-cy="`lc-${learningContent.slug}`"
|
||||
:data-cy="`${learningContent.slug}`"
|
||||
>
|
||||
<span @click.stop="circleStore.openLearningContent(learningContent)">{{ learningContent.contents[0].type }}: {{ learningContent.title }}</span>
|
||||
<span @click.stop="circleStore.openLearningContent(learningContent)"
|
||||
>{{ learningContent.contents[0].type }}: {{ learningContent.title }}</span
|
||||
>
|
||||
</ItCheckbox>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="learningUnit.id"
|
||||
class="hover:cursor-pointer"
|
||||
@click="circleStore.openSelfEvaluation(learningUnit)"
|
||||
>
|
||||
<div
|
||||
v-if="circleStore.calcSelfEvaluationStatus(learningUnit)"
|
||||
class="flex items-center gap-4 pb-3 lg:pb-6"
|
||||
>
|
||||
<it-icon-smiley-happy class="w-8 h-8 flex-none"/>
|
||||
<div v-if="learningUnit.id" class="hover:cursor-pointer" @click="circleStore.openSelfEvaluation(learningUnit)">
|
||||
<div v-if="circleStore.calcSelfEvaluationStatus(learningUnit)" class="flex items-center gap-4 pb-3 lg:pb-6">
|
||||
<it-icon-smiley-happy class="w-8 h-8 flex-none" />
|
||||
<div>Selbsteinschätzung: Ich kann das.</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="circleStore.calcSelfEvaluationStatus(learningUnit) === false"
|
||||
class="flex items-center gap-4 pb-3 lg:pb-6"
|
||||
>
|
||||
<it-icon-smiley-thinking class="w-8 h-8 flex-none"/>
|
||||
<it-icon-smiley-thinking class="w-8 h-8 flex-none" />
|
||||
<div>Selbsteinschätzung: Muss ich nochmals anschauen</div>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="flex items-center gap-4 pb-3 lg:pb-6"
|
||||
>
|
||||
<it-icon-smiley-neutral class="w-8 h-8 flex-none"/>
|
||||
<div v-else class="flex items-center gap-4 pb-3 lg:pb-6">
|
||||
<it-icon-smiley-neutral class="w-8 h-8 flex-none" />
|
||||
<div>Selbsteinschätzung</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr v-if="!learningUnit.last" class="-mx-4 text-gray-500">
|
||||
|
||||
<hr v-if="!learningUnit.last" class="-mx-4 text-gray-500" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
<style scoped></style>
|
||||
|
|
|
|||
|
|
@ -1,106 +0,0 @@
|
|||
<script>
|
||||
import * as d3 from 'd3';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
learningSequences: {
|
||||
required: false,
|
||||
default: [{title: '', done: false}, {title: '', done: false}, {title: '', done: false}, {title: '', done: false}]
|
||||
},
|
||||
width: {
|
||||
default: 250,
|
||||
type: Number,
|
||||
},
|
||||
height: {
|
||||
default: 250,
|
||||
type: Number,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
pieData() {
|
||||
return new Array(Math.max(this.learningSequences.length, 1)).fill(1)
|
||||
},
|
||||
viewBox() {
|
||||
return `0 0 ${this.width} ${this.height}`
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
console.log(this.pieData)
|
||||
const data = this.pieData
|
||||
|
||||
const width = this.width
|
||||
const height = this.height
|
||||
const radius = Math.min(width, height) / 2.5
|
||||
console.log(this.viewBox)
|
||||
|
||||
|
||||
const svg = d3.select(this.$el)
|
||||
.append('svg')
|
||||
.attr('width', width)
|
||||
.attr('height', height)
|
||||
|
||||
|
||||
const g = svg.append('g').attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')')
|
||||
|
||||
|
||||
// Generate the pie
|
||||
const pie = d3.pie()
|
||||
|
||||
// Generate the arcs
|
||||
const arc = d3
|
||||
.arc()
|
||||
.innerRadius(radius / 2)
|
||||
.padAngle(12 / 360)
|
||||
.outerRadius(radius)
|
||||
|
||||
|
||||
//Generate groups
|
||||
const arcs = g.selectAll('arc')
|
||||
.data(pie(data))
|
||||
.enter()
|
||||
.append('g')
|
||||
.attr('class', 'arc')
|
||||
|
||||
|
||||
//Draw arc paths
|
||||
arcs.append('path')
|
||||
.attr('d', arc)
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.svg-container {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding-bottom: 100%;
|
||||
vertical-align: top;
|
||||
overflow: hidden;
|
||||
fill: rgb(65 181 250);
|
||||
|
||||
}
|
||||
|
||||
.svg-content {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
fill: rgb(65 181 250);
|
||||
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
<template>
|
||||
<div id="container" class="svg-container">
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -2,7 +2,6 @@
|
|||
// inspiration https://vuejs.org/examples/#modal
|
||||
|
||||
import {onMounted, watch} from "vue";
|
||||
import {HTMLElement} from "happy-dom";
|
||||
|
||||
const props = defineProps<{
|
||||
show: boolean
|
||||
|
|
|
|||
|
|
@ -37,18 +37,18 @@ const router = createRouter({
|
|||
component: () => import('@/views/ProfileView.vue'),
|
||||
},
|
||||
{
|
||||
path: '/learningpath/:learningPathSlug',
|
||||
path: '/learn/:learningPathSlug',
|
||||
component: () => import('../views/LearningPathView.vue'),
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: '/circle/:circleSlug',
|
||||
path: '/learn/:learningPathSlug/:circleSlug',
|
||||
component: () => import('../views/CircleView.vue'),
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: '/styleguide',
|
||||
component: () => import('../views/StyelGuideView.vue'),
|
||||
component: () => import('../views/StyleGuideView.vue'),
|
||||
meta: {
|
||||
public: true,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,130 +1,12 @@
|
|||
import {describe, it} from 'vitest'
|
||||
import {parseLearningSequences} from '../circle';
|
||||
import type {WagtailCircle} from '@/types';
|
||||
import { describe, it } from 'vitest'
|
||||
import data from './learning_path_json.json'
|
||||
import { Circle } from '../circle'
|
||||
|
||||
describe('circleService.parseLearningSequences', () => {
|
||||
it('can parse learning sequences from api response', () => {
|
||||
const input = {
|
||||
"id": 10,
|
||||
"title": "Analyse",
|
||||
"slug": "analyse",
|
||||
"type": "learnpath.Circle",
|
||||
"translation_key": "c9832aaf-02b2-47af-baeb-bde60d8ec1f5",
|
||||
"children": [
|
||||
{
|
||||
"id": 18,
|
||||
"title": "Anwenden",
|
||||
"slug": "anwenden",
|
||||
"type": "learnpath.LearningSequence",
|
||||
"translation_key": "2e4c431a-9602-4398-ad18-20dd4bb189fa",
|
||||
"icon": "it-icon-ls-apply"
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"title": "Prämien einsparen",
|
||||
"slug": "pramien-einsparen",
|
||||
"type": "learnpath.LearningUnit",
|
||||
"translation_key": "75c1f31a-ae25-4d9c-9206-a4e7fdae8c13",
|
||||
"questions": []
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"title": "Versicherungsbedarf für Familien",
|
||||
"slug": "versicherungsbedarf-für-familien",
|
||||
"type": "learnpath.LearningContent",
|
||||
"translation_key": "2a422da3-a3ad-468a-831e-9141c122ffef",
|
||||
"minutes": 60,
|
||||
"contents": [
|
||||
{
|
||||
"type": "exercise",
|
||||
"value": {
|
||||
"description": "Beispiel Aufgabe"
|
||||
},
|
||||
"id": "ee0bcef7-702b-42f3-a891-88a0332fce6f"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"title": "Alles klar?",
|
||||
"slug": "alles-klar",
|
||||
"type": "learnpath.LearningContent",
|
||||
"translation_key": "7dc9d96d-07f9-4b9f-bec1-43ba67cf9010",
|
||||
"minutes": 60,
|
||||
"contents": [
|
||||
{
|
||||
"type": "exercise",
|
||||
"value": {
|
||||
"description": "Beispiel Aufgabe"
|
||||
},
|
||||
"id": "a556ebb2-f902-4d78-9b76-38b7933118b8"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 22,
|
||||
"title": "Sich selbständig machen",
|
||||
"slug": "sich-selbstandig-machen",
|
||||
"type": "learnpath.LearningUnit",
|
||||
"translation_key": "c40d5266-3c94-4b9b-8469-9ac6b32a6231",
|
||||
"questions": []
|
||||
},
|
||||
{
|
||||
"id": 23,
|
||||
"title": "GmbH oder AG",
|
||||
"slug": "gmbh-oder-ag",
|
||||
"type": "learnpath.LearningContent",
|
||||
"translation_key": "59331843-9f52-4b41-9cd1-2293a8d90064",
|
||||
"minutes": 120,
|
||||
"contents": [
|
||||
{
|
||||
"type": "video",
|
||||
"value": {
|
||||
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam",
|
||||
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
|
||||
},
|
||||
"id": "a4974834-f404-4fb8-af94-a24c6db56bb8"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 24,
|
||||
"title": "Tiertherapie Patrizia Feller",
|
||||
"slug": "tiertherapie-patrizia-feller",
|
||||
"type": "learnpath.LearningContent",
|
||||
"translation_key": "13f6d661-1d10-4b59-b8e5-01fcec47a38f",
|
||||
"minutes": 120,
|
||||
"contents": [
|
||||
{
|
||||
"type": "exercise",
|
||||
"value": {
|
||||
"description": "Beispiel Aufgabe"
|
||||
},
|
||||
"id": "5947c947-8656-44b5-826c-1787057c2df2"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 25,
|
||||
"title": "Auto verkaufen",
|
||||
"slug": "auto-verkaufen",
|
||||
"type": "learnpath.LearningUnit",
|
||||
"translation_key": "3b42e514-0bbe-4c23-9c88-3f5263e47cf9",
|
||||
"questions": []
|
||||
},
|
||||
],
|
||||
"description": "Nach dem Gespräch werten sie die Analyse aus...",
|
||||
"job_situations": [],
|
||||
"goals": [],
|
||||
"experts": []
|
||||
} as WagtailCircle;
|
||||
|
||||
const learningSequences = parseLearningSequences(input.children);
|
||||
expect(learningSequences.length).toBe(1);
|
||||
console.log(learningSequences[0].learningUnits[0].learningContents[0]);
|
||||
|
||||
expect(
|
||||
learningSequences[0].learningUnits[1].learningContents[0].previousLearningContent.translation_key
|
||||
).toEqual(learningSequences[0].learningUnits[0].learningContents[1].translation_key);
|
||||
describe('Circle.parseJson', () => {
|
||||
it('can parse circle from api response', () => {
|
||||
const cirleData = data.children.find((c) => c.slug === 'unit-test-circle')
|
||||
const circle = Circle.fromJson(cirleData, undefined)
|
||||
expect(circle.learningSequences.length).toBe(3)
|
||||
expect(circle.flatLearningContents.length).toBe(8)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
import { describe, it } from 'vitest'
|
||||
import data from './learning_path_json.json'
|
||||
import { LearningPath } from '../learningPath'
|
||||
|
||||
describe('LearningPath.parseJson', () => {
|
||||
it('can parse learning sequences from api response', () => {
|
||||
const learningPath = LearningPath.fromJson(data, [])
|
||||
|
||||
expect(learningPath.circles.length).toBe(2)
|
||||
expect(learningPath.circles[0].title).toBe('Basis')
|
||||
expect(learningPath.circles[1].title).toBe('Unit-Test Circle')
|
||||
|
||||
expect(learningPath.topics.length).toBe(2)
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,356 @@
|
|||
{
|
||||
"id": 409,
|
||||
"title": "Unit-Test Lernpfad",
|
||||
"slug": "unit-test-lernpfad",
|
||||
"type": "learnpath.LearningPath",
|
||||
"translation_key": "9f50de84-036c-4986-ab3e-1a83a374910a",
|
||||
"children": [
|
||||
{
|
||||
"id": 410,
|
||||
"title": "Basis",
|
||||
"slug": "basis-1",
|
||||
"type": "learnpath.Topic",
|
||||
"translation_key": "fbc1431c-46b0-4f77-93ee-4f10e0e59c03",
|
||||
"is_visible": false
|
||||
},
|
||||
{
|
||||
"id": 411,
|
||||
"title": "Basis",
|
||||
"slug": "basis-2",
|
||||
"type": "learnpath.Circle",
|
||||
"translation_key": "d30cb8f8-6bb5-4e7a-8123-a370b7668a85",
|
||||
"children": [
|
||||
{
|
||||
"id": 412,
|
||||
"title": "Starten",
|
||||
"slug": "starten",
|
||||
"type": "learnpath.LearningSequence",
|
||||
"translation_key": "1c5cd2a1-a39e-496e-b856-342f34d2b21c",
|
||||
"icon": "it-icon-ls-start"
|
||||
},
|
||||
{
|
||||
"id": 413,
|
||||
"title": "Einleitung Circle \"Basis\"",
|
||||
"slug": "einleitung-circle-basis-1",
|
||||
"type": "learnpath.LearningContent",
|
||||
"translation_key": "48d4ace9-b0cf-4e23-98d2-012c1b91100e",
|
||||
"minutes": 15,
|
||||
"contents": [
|
||||
{
|
||||
"type": "video",
|
||||
"value": {
|
||||
"description": "Basis Video",
|
||||
"url": "https://www.youtube.com/embed/qhPIfxS2hvI"
|
||||
},
|
||||
"id": "ee431ded-edc4-4984-9dd8-ab1d869d82ae"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 414,
|
||||
"title": "Beenden",
|
||||
"slug": "beenden",
|
||||
"type": "learnpath.LearningSequence",
|
||||
"translation_key": "eaeaf0c7-b2b7-41a9-a77f-b392f83291eb",
|
||||
"icon": "it-icon-ls-end"
|
||||
},
|
||||
{
|
||||
"id": 415,
|
||||
"title": "Kompetenzprofil anschauen",
|
||||
"slug": "kompetenzprofil-anschauen-8",
|
||||
"type": "learnpath.LearningContent",
|
||||
"translation_key": "784772fc-d2ac-4df2-8ca1-61a45fbfe001",
|
||||
"minutes": 30,
|
||||
"contents": [
|
||||
{
|
||||
"type": "document",
|
||||
"value": {
|
||||
"description": "Beispiel Kompetenz"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"description": "Basis von Unit-Test Lernpfad",
|
||||
"job_situations": [],
|
||||
"goals": [],
|
||||
"experts": []
|
||||
},
|
||||
{
|
||||
"id": 417,
|
||||
"title": "Gewinnen von Kunden",
|
||||
"slug": "gewinnen-von-kunden-1",
|
||||
"type": "learnpath.Topic",
|
||||
"translation_key": "4b2aa669-4cd9-43f1-9605-8575e5e7e760",
|
||||
"is_visible": true
|
||||
},
|
||||
{
|
||||
"id": 418,
|
||||
"title": "Unit-Test Circle",
|
||||
"slug": "unit-test-circle",
|
||||
"type": "learnpath.Circle",
|
||||
"translation_key": "8433f8fe-7074-4c8a-a93a-b62e042f06ca",
|
||||
"children": [
|
||||
{
|
||||
"id": 419,
|
||||
"title": "Starten",
|
||||
"slug": "starten",
|
||||
"type": "learnpath.LearningSequence",
|
||||
"translation_key": "065ab931-122a-4e4d-a570-f8e6352a0550",
|
||||
"icon": "it-icon-ls-start"
|
||||
},
|
||||
{
|
||||
"id": 420,
|
||||
"title": "Einleitung Circle \"Unit-Test Circle\"",
|
||||
"slug": "einleitung-circle-unit-test-circle",
|
||||
"type": "learnpath.LearningContent",
|
||||
"translation_key": "ec97ed44-a2ee-46b4-b6ba-3cce4c6f627e",
|
||||
"minutes": 15,
|
||||
"contents": [
|
||||
{
|
||||
"type": "video",
|
||||
"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"
|
||||
},
|
||||
"id": "01ed1388-e82f-49a4-aafc-2d24891ec64a"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 421,
|
||||
"title": "Beobachten",
|
||||
"slug": "beobachten",
|
||||
"type": "learnpath.LearningSequence",
|
||||
"translation_key": "8fed5f78-2d39-4a78-9dfc-f65551a81a7b",
|
||||
"icon": "it-icon-ls-watch"
|
||||
},
|
||||
{
|
||||
"id": 422,
|
||||
"title": "Absicherung der Familie",
|
||||
"slug": "absicherung-der-familie",
|
||||
"type": "learnpath.LearningUnit",
|
||||
"translation_key": "fe50e509-b679-40f8-bddf-844c473e1e8a",
|
||||
"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": 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": 425,
|
||||
"title": "Ermittlung des Kundenbedarfs",
|
||||
"slug": "ermittlung-des-kundenbedarfs-14",
|
||||
"type": "learnpath.LearningContent",
|
||||
"translation_key": "ffd613f5-830c-4bc0-860b-fc194e2d7d1c",
|
||||
"minutes": 30,
|
||||
"contents": [
|
||||
{
|
||||
"type": "podcast",
|
||||
"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"
|
||||
},
|
||||
"id": "642c0906-3bd3-4030-be3f-8b1acce08930"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 426,
|
||||
"title": "Kundenbed\u00fcrfnisse erkennen",
|
||||
"slug": "kundenbed\u00fcrfnisse-erkennen-7",
|
||||
"type": "learnpath.LearningContent",
|
||||
"translation_key": "b36bd615-053c-4054-a5be-080005140a98",
|
||||
"minutes": 30,
|
||||
"contents": [
|
||||
{
|
||||
"type": "competence",
|
||||
"value": {
|
||||
"description": "Beispiel Kompetenz"
|
||||
},
|
||||
"id": "6b85361b-cc27-4454-aa20-72b31ad92a3f"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 427,
|
||||
"title": "Was braucht eine Familie?",
|
||||
"slug": "was-braucht-eine-familie-7",
|
||||
"type": "learnpath.LearningContent",
|
||||
"translation_key": "b4d2ec6c-12b3-48bc-b159-f5b9e06637cf",
|
||||
"minutes": 60,
|
||||
"contents": [
|
||||
{
|
||||
"type": "exercise",
|
||||
"value": {
|
||||
"description": "Beispiel Aufgabe",
|
||||
"url": "/media/web_based_trainings/story-01-a-01-patrizia-marco-sichern-sich-ab-einstieg/scormcontent/index.html"
|
||||
},
|
||||
"id": "b7e661b1-9e39-4482-8b23-c24dad1ef648"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"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",
|
||||
"minutes": 120,
|
||||
"contents": [
|
||||
{
|
||||
"type": "exercise",
|
||||
"value": {
|
||||
"description": "Beispiel Aufgabe",
|
||||
"url": "/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": 432,
|
||||
"title": "Beenden",
|
||||
"slug": "beenden",
|
||||
"type": "learnpath.LearningSequence",
|
||||
"translation_key": "a3ee459e-ab98-483f-95c9-ba85eb10c105",
|
||||
"icon": "it-icon-ls-end"
|
||||
},
|
||||
{
|
||||
"id": 433,
|
||||
"title": "Kompetenzprofil anschauen",
|
||||
"slug": "kompetenzprofil-anschauen-9",
|
||||
"type": "learnpath.LearningContent",
|
||||
"translation_key": "0e16dd46-14cf-43ac-888f-f03beded7fa1",
|
||||
"minutes": 30,
|
||||
"contents": [
|
||||
{
|
||||
"type": "document",
|
||||
"value": {
|
||||
"description": "Beispiel Kompetenz"
|
||||
},
|
||||
"id": "4b729c72-aee8-4944-b5fb-d0bfd317a339"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 434,
|
||||
"title": "Circle \"Analyse\" abschliessen",
|
||||
"slug": "circle-analyse-abschliessen-9",
|
||||
"type": "learnpath.LearningContent",
|
||||
"translation_key": "53703784-c71f-4bad-a3e7-e014f0fded12",
|
||||
"minutes": 30,
|
||||
"contents": [
|
||||
{
|
||||
"type": "document",
|
||||
"value": {
|
||||
"description": "Beispiel Kompetenz"
|
||||
},
|
||||
"id": "8bc53dfd-bb9b-4ae5-bd3c-74b7eef0eafd"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"description": "Unit-Test Circle",
|
||||
"job_situations": [
|
||||
{
|
||||
"type": "job_situation",
|
||||
"value": "Absicherung der Familie",
|
||||
"id": "f715a46f-53df-4205-8257-30cff62f337c"
|
||||
},
|
||||
{
|
||||
"type": "job_situation",
|
||||
"value": "Reisen",
|
||||
"id": "f2174789-eab4-4059-961d-699b3c333110"
|
||||
}
|
||||
],
|
||||
"goals": [
|
||||
{
|
||||
"type": "goal",
|
||||
"value": "... die heutige Versicherungssituation von Privat- oder Gesch\u00e4ftskunden einzusch\u00e4tzen.",
|
||||
"id": "41acaebc-38de-4929-a4af-aaed43a1e5f3"
|
||||
},
|
||||
{
|
||||
"type": "goal",
|
||||
"value": "... deinem Kunden seine optimale L\u00f6sung aufzuzeigen",
|
||||
"id": "cb1d556b-dac1-4edc-a3e5-97307b49c55c"
|
||||
}
|
||||
],
|
||||
"experts": [
|
||||
{
|
||||
"type": "person",
|
||||
"value": {
|
||||
"first_name": "Patrizia",
|
||||
"last_name": "Huggel",
|
||||
"email": "patrizia.huggel@example.com",
|
||||
"photo": null,
|
||||
"biography": ""
|
||||
},
|
||||
"id": "479878e7-2d30-46a4-8d6b-bfe77268bbae"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import json
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
def main():
|
||||
client = requests.session()
|
||||
client.get('http://localhost:8000/')
|
||||
|
||||
client.post(
|
||||
'http://localhost:8000/core/login/',
|
||||
json={
|
||||
'username': 'admin',
|
||||
'password': 'test',
|
||||
}
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
'http://localhost:8000/learnpath/api/page/unit-test-lernpfad/',
|
||||
)
|
||||
print(response.status_code)
|
||||
print(response.json())
|
||||
|
||||
with open('learning_path_json.json', 'w') as f:
|
||||
f.write(json.dumps(response.json(), indent=4))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
@ -6,9 +6,10 @@ import type {
|
|||
LearningContent,
|
||||
LearningSequence,
|
||||
LearningUnit,
|
||||
LearningWagtailPage
|
||||
} from '@/types';
|
||||
|
||||
LearningUnitQuestion,
|
||||
LearningWagtailPage,
|
||||
} from '@/types'
|
||||
import type { LearningPath } from '@/services/learningPath'
|
||||
|
||||
function _createEmptyLearningUnit(parentLearningSequence: LearningSequence): LearningUnit {
|
||||
return {
|
||||
|
|
@ -22,10 +23,10 @@ function _createEmptyLearningUnit(parentLearningSequence: LearningSequence): Lea
|
|||
parentLearningSequence: parentLearningSequence,
|
||||
children: [],
|
||||
last: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function parseLearningSequences (children: CircleChild[]): LearningSequence[] {
|
||||
export function parseLearningSequences (circle: Circle, children: CircleChild[]): LearningSequence[] {
|
||||
let learningSequence:LearningSequence | undefined;
|
||||
let learningUnit:LearningUnit | undefined;
|
||||
let learningContent:LearningContent | undefined;
|
||||
|
|
@ -70,6 +71,7 @@ export function parseLearningSequences (children: CircleChild[]): LearningSequen
|
|||
previousLearningContent = learningContent;
|
||||
|
||||
learningContent = Object.assign(child, {
|
||||
parentCircle: circle,
|
||||
parentLearningSequence: learningSequence,
|
||||
parentLearningUnit: learningUnit,
|
||||
previousLearningContent: previousLearningContent,
|
||||
|
|
@ -112,6 +114,9 @@ export class Circle implements LearningWagtailPage {
|
|||
readonly learningSequences: LearningSequence[];
|
||||
readonly completed: boolean;
|
||||
|
||||
nextCircle?: Circle;
|
||||
previousCircle?: Circle;
|
||||
|
||||
constructor(
|
||||
public readonly id: number,
|
||||
public readonly slug: string,
|
||||
|
|
@ -121,12 +126,13 @@ export class Circle implements LearningWagtailPage {
|
|||
public children: CircleChild[],
|
||||
public goals: CircleGoal[],
|
||||
public job_situations: CircleJobSituation[],
|
||||
public readonly parentLearningPath?: LearningPath,
|
||||
) {
|
||||
this.learningSequences = parseLearningSequences(this.children);
|
||||
this.learningSequences = parseLearningSequences(this, this.children);
|
||||
this.completed = false;
|
||||
}
|
||||
|
||||
public static fromJson(json: any): Circle {
|
||||
public static fromJson(json: any, learningPath?: LearningPath): Circle {
|
||||
// TODO add error checking when the data does not conform to the schema
|
||||
return new Circle(
|
||||
json.id,
|
||||
|
|
@ -137,11 +143,12 @@ export class Circle implements LearningWagtailPage {
|
|||
json.children,
|
||||
json.goals,
|
||||
json.job_situations,
|
||||
learningPath,
|
||||
)
|
||||
}
|
||||
|
||||
public get flatChildren(): CircleChild[] {
|
||||
const result: CircleChild[] = [];
|
||||
public get flatChildren(): (LearningContent | LearningUnitQuestion)[] {
|
||||
const result: (LearningContent | LearningUnitQuestion)[] = [];
|
||||
this.learningSequences.forEach((learningSequence) => {
|
||||
learningSequence.learningUnits.forEach((learningUnit) => {
|
||||
learningUnit.children.forEach((learningUnitQuestion) => {
|
||||
|
|
@ -155,6 +162,18 @@ export class Circle implements LearningWagtailPage {
|
|||
return result;
|
||||
}
|
||||
|
||||
public get flatLearningContents(): LearningContent[] {
|
||||
const result: LearningContent[] = [];
|
||||
this.learningSequences.forEach((learningSequence) => {
|
||||
learningSequence.learningUnits.forEach((learningUnit) => {
|
||||
learningUnit.learningContents.forEach((learningContent) => {
|
||||
result.push(learningContent);
|
||||
});
|
||||
});
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
public someFinishedInLearningSequence(translationKey: string): boolean {
|
||||
if (translationKey) {
|
||||
return this.flatChildren.filter((lc) => {
|
||||
|
|
@ -191,5 +210,9 @@ export class Circle implements LearningWagtailPage {
|
|||
page.completed = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
if (this.parentLearningPath) {
|
||||
this.parentLearningPath.calcNextLearningContent(completionData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,92 @@
|
|||
import * as _ from 'lodash'
|
||||
|
||||
import type { CircleCompletion, LearningContent, LearningPathChild, LearningWagtailPage, 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'
|
||||
})
|
||||
}
|
||||
|
||||
export class LearningPath implements LearningWagtailPage {
|
||||
readonly type = 'learnpath.LearningPath'
|
||||
public topics: Topic[]
|
||||
public circles: Circle[]
|
||||
public nextLearningContent?: LearningContent
|
||||
|
||||
public static fromJson(json: any, completionData: CircleCompletion[]): LearningPath {
|
||||
return new LearningPath(json.id, json.slug, json.title, json.translation_key, json.children, completionData)
|
||||
}
|
||||
|
||||
constructor(
|
||||
public readonly id: number,
|
||||
public readonly slug: string,
|
||||
public readonly title: string,
|
||||
public readonly translation_key: string,
|
||||
public children: LearningPathChild[],
|
||||
completionData?: any
|
||||
) {
|
||||
// parse children
|
||||
this.topics = []
|
||||
this.circles = []
|
||||
|
||||
let topic: Topic | undefined
|
||||
|
||||
this.children.forEach((page) => {
|
||||
if (page.type === 'learnpath.Topic') {
|
||||
if (topic) {
|
||||
this.topics.push(topic)
|
||||
}
|
||||
topic = Object.assign(page, { circles: [] })
|
||||
}
|
||||
if (page.type === 'learnpath.Circle') {
|
||||
const circle = Circle.fromJson(page, this)
|
||||
circle.parseCompletionData(completionData)
|
||||
if (topic) {
|
||||
topic.circles.push(circle)
|
||||
}
|
||||
|
||||
circle.previousCircle = this.circles[this.circles.length - 1]
|
||||
if (circle.previousCircle) {
|
||||
circle.previousCircle.nextCircle = circle
|
||||
}
|
||||
this.circles.push(circle)
|
||||
}
|
||||
})
|
||||
|
||||
if (topic) {
|
||||
this.topics.push(topic)
|
||||
}
|
||||
|
||||
this.calcNextLearningContent(completionData)
|
||||
}
|
||||
|
||||
public calcNextLearningContent(completionData: CircleCompletion[]): void {
|
||||
this.nextLearningContent = undefined
|
||||
|
||||
const lastCompletedLearningContent = getLastCompleted(this.translation_key, completionData)
|
||||
|
||||
if (lastCompletedLearningContent) {
|
||||
const lastCircle = this.circles.find(
|
||||
(circle) => circle.translation_key === lastCompletedLearningContent.circle_key
|
||||
)
|
||||
if (lastCircle) {
|
||||
const lastLearningContent = lastCircle.flatLearningContents.find(
|
||||
(learningContent) => learningContent.translation_key === lastCompletedLearningContent.page_key
|
||||
)
|
||||
if (lastLearningContent && lastLearningContent.nextLearningContent) {
|
||||
this.nextLearningContent = lastLearningContent.nextLearningContent
|
||||
} else {
|
||||
if (lastCircle.nextCircle) {
|
||||
this.nextLearningContent = lastCircle.nextCircle.flatLearningContents[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (this.circles[0]) {
|
||||
this.nextLearningContent = this.circles[0].flatLearningContents[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@ import { defineStore } from 'pinia'
|
|||
|
||||
import type { LearningContent, LearningUnit, LearningUnitQuestion } from '@/types'
|
||||
import type { Circle } from '@/services/circle'
|
||||
import { itGet, itPost } from '@/fetchHelpers'
|
||||
import { itPost } from '@/fetchHelpers'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { useLearningPathStore } from '@/stores/learningPath'
|
||||
|
||||
|
|
@ -28,26 +28,19 @@ export const useCircleStore = defineStore({
|
|||
getters: {
|
||||
},
|
||||
actions: {
|
||||
async loadCircle(slug: string) {
|
||||
async loadCircle(learningPathSlug: string, circleSlug: string) {
|
||||
this.circle = undefined;
|
||||
try {
|
||||
// const circleData = await itGet(`/learnpath/api/circle/${slug}/`);
|
||||
// this.circle = Circle.fromJson(circleData);
|
||||
// this.circle.parseCompletionData(completionData);
|
||||
const learningPathStore = useLearningPathStore();
|
||||
await learningPathStore.loadLearningPath('versicherungsvermittlerin');
|
||||
if (learningPathStore.learningPath) {
|
||||
this.circle = learningPathStore.learningPath.circles.find(circle => circle.slug === slug);
|
||||
if (this.circle) {
|
||||
const completionData = await itGet(`/api/completion/circle/${this.circle.translation_key}/`);
|
||||
this.circle.parseCompletionData(completionData);
|
||||
}
|
||||
}
|
||||
return Promise.resolve(this.circle)
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
return error
|
||||
const learningPathStore = useLearningPathStore();
|
||||
await learningPathStore.loadLearningPath(learningPathSlug);
|
||||
if (learningPathStore.learningPath) {
|
||||
this.circle = learningPathStore.learningPath.circles.find(circle => circle.slug === circleSlug);
|
||||
}
|
||||
|
||||
if (!this.circle) {
|
||||
throw `No circle found with slug: ${circleSlug}`;
|
||||
}
|
||||
|
||||
return this.circle
|
||||
},
|
||||
async markCompletion(page: LearningContent | LearningUnitQuestion, flag = true) {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -1,66 +1,16 @@
|
|||
import * as log from 'loglevel';
|
||||
|
||||
import {defineStore} from 'pinia'
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import type {LearningPath, Topic} from '@/types'
|
||||
import {itGet} from '@/fetchHelpers';
|
||||
import {Circle} from '@/services/circle';
|
||||
import learningPathDiagram from "@/components/circle/LearningPathDiagram.vue";
|
||||
import { defineStore } from 'pinia'
|
||||
import { itGet } from '@/fetchHelpers'
|
||||
import { LearningPath } from '@/services/learningPath'
|
||||
|
||||
export type LearningPathStoreState = {
|
||||
learningPath: LearningPath | undefined;
|
||||
learningPath: LearningPath | undefined
|
||||
}
|
||||
|
||||
|
||||
function getLastCompleted(completionData: any) {
|
||||
return _.filter(_.orderBy(completionData, ['updated_at'], 'desc'), c =>{return c.completed && c.page_type === "learnpath.LearningContent" })[0]
|
||||
}
|
||||
|
||||
|
||||
function getFirstLearningContent(lastCopleted, learningPathData) {
|
||||
const circles = _.filter(learningPathData.children, {'type': 'learnpath.Circle'})
|
||||
|
||||
let currentCircle = Circle.fromJson(circles[0])
|
||||
const currentLearningUnit = currentCircle.flatChildren[0]
|
||||
let currentLearningSequence = currentLearningUnit.parentLearningSequence
|
||||
return [currentCircle, currentLearningSequence, currentLearningUnit]
|
||||
}
|
||||
|
||||
function getNextLearningContent(lastCopleted, learningPathData) {
|
||||
|
||||
let currentCircle, currentLearningSequence, currentLearningUnit
|
||||
|
||||
currentLearningUnit = getFirstLearningContent(lastCopleted, learningPathData)
|
||||
|
||||
if (lastCopleted) {
|
||||
const circles = _.filter(learningPathData.children, {'type': 'learnpath.Circle'})
|
||||
_.forEach(circles, circle => {
|
||||
_.forEach(Circle.fromJson(circle).learningSequences, learningSequence => {
|
||||
_.forEach(learningSequence.learningUnits, learningUnit => {
|
||||
_.forEach(learningUnit.learningContents, content => {
|
||||
if (lastCopleted.page_key === content.translation_key) {
|
||||
currentCircle = Circle.fromJson(circle)
|
||||
currentLearningSequence = learningSequence
|
||||
currentLearningUnit = content
|
||||
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
currentLearningUnit = [currentCircle, currentLearningSequence, currentLearningUnit]
|
||||
}
|
||||
return currentLearningUnit
|
||||
}
|
||||
|
||||
|
||||
export const useLearningPathStore = defineStore({
|
||||
id: 'learningPath',
|
||||
state: () => {
|
||||
return {
|
||||
learningPath: undefined,
|
||||
|
||||
} as LearningPathStoreState;
|
||||
},
|
||||
getters: {},
|
||||
|
|
@ -69,52 +19,15 @@ export const useLearningPathStore = defineStore({
|
|||
if (this.learningPath && !reload) {
|
||||
return this.learningPath;
|
||||
}
|
||||
try {
|
||||
const learningPathData = await itGet(`/learnpath/api/page/${slug}/`);
|
||||
const completionData = await itGet(`/api/completion/learning_path/${learningPathData.translation_key}/`);
|
||||
const learningPathData = await itGet(`/learnpath/api/page/${slug}/`);
|
||||
const completionData = await itGet(`/api/completion/learning_path/${learningPathData.translation_key}/`);
|
||||
|
||||
this.learningPath = learningPathData;
|
||||
|
||||
|
||||
if (this.learningPath) {
|
||||
this.learningPath.lastCompleted = getLastCompleted(completionData)
|
||||
const nextLearningContent = getNextLearningContent(this.learningPath.lastCompleted, learningPathData)
|
||||
|
||||
console.log('nextLearningContent', nextLearningContent)
|
||||
this.learningPath.nextCircle = nextLearningContent[0]
|
||||
this.learningPath.nextLearningSequence = nextLearningContent[1]
|
||||
this.learningPath.nextLearningUnit = nextLearningContent[2]
|
||||
|
||||
|
||||
this.learningPath.topics = [];
|
||||
this.learningPath.circles = [];
|
||||
|
||||
let topic: Topic | undefined;
|
||||
|
||||
this.learningPath.children.forEach((page) => {
|
||||
if (page.type === 'learnpath.Topic') {
|
||||
if (topic) {
|
||||
this.learningPath.topics.push(topic);
|
||||
}
|
||||
topic = Object.assign(page, {circles: []});
|
||||
}
|
||||
if (page.type === 'learnpath.Circle') {
|
||||
const circle = Circle.fromJson(page);
|
||||
circle.parseCompletionData(completionData);
|
||||
if (topic) {
|
||||
topic.circles.push(circle);
|
||||
}
|
||||
this.learningPath.circles.push(circle);
|
||||
}
|
||||
|
||||
})
|
||||
this.learningPath.topics.push(topic);
|
||||
}
|
||||
return this.learningPath;
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
return error
|
||||
if (!learningPathData) {
|
||||
throw `No learning path found with: ${slug}`;
|
||||
}
|
||||
|
||||
this.learningPath = LearningPath.fromJson(learningPathData, completionData);
|
||||
return this.learningPath;
|
||||
},
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import type {Circle} from '@/services/circle';
|
||||
import type { Circle } from '@/services/circle'
|
||||
|
||||
export interface LearningContentBlock {
|
||||
type: 'web-based-training' | 'competence' | 'exercise' | 'knowledge';
|
||||
type: 'web-based-training' | 'competence' | 'exercise' | 'knowledge'
|
||||
value: {
|
||||
description: string;
|
||||
},
|
||||
id: string;
|
||||
description: string
|
||||
}
|
||||
id: string
|
||||
}
|
||||
|
||||
export interface VideoBlock {
|
||||
|
|
@ -59,6 +59,7 @@ export interface LearningContent extends LearningWagtailPage {
|
|||
type: 'learnpath.LearningContent';
|
||||
minutes: number;
|
||||
contents: (LearningContentBlock | VideoBlock | PodcastBlock | DocumentBlock)[];
|
||||
parentCircle: Circle;
|
||||
parentLearningSequence?: LearningSequence;
|
||||
parentLearningUnit?: LearningUnit;
|
||||
nextLearningContent?: LearningContent;
|
||||
|
|
@ -103,18 +104,6 @@ export interface Topic extends LearningWagtailPage {
|
|||
|
||||
export type LearningPathChild = Topic | WagtailCircle;
|
||||
|
||||
export interface LearningPath extends LearningWagtailPage {
|
||||
type: 'learnpath.LearningPath';
|
||||
children: LearningPathChild[];
|
||||
topics: Topic[];
|
||||
circles: Circle[];
|
||||
lastCompleted: CircleCompletion;
|
||||
nextCircle: Circle;
|
||||
nextLearningSequence: LearningSequence;
|
||||
nextLearningUnit: LearningContent;
|
||||
|
||||
}
|
||||
|
||||
export interface CircleCompletion {
|
||||
id: number;
|
||||
created_at: string;
|
||||
|
|
@ -123,6 +112,7 @@ export interface CircleCompletion {
|
|||
page_key: string;
|
||||
page_type: string;
|
||||
circle_key: string;
|
||||
learning_path_key: string;
|
||||
completed: boolean;
|
||||
json_data: any;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,27 +1,32 @@
|
|||
<script setup lang="ts">
|
||||
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 * 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 {onMounted} from 'vue'
|
||||
import {useCircleStore} from '@/stores/circle';
|
||||
import SelfEvaluation from '@/components/circle/SelfEvaluation.vue';
|
||||
import { onMounted } from 'vue'
|
||||
import { useCircleStore } from '@/stores/circle'
|
||||
import SelfEvaluation from '@/components/circle/SelfEvaluation.vue'
|
||||
|
||||
log.debug('CircleView.vue created');
|
||||
log.debug('CircleView.vue created')
|
||||
|
||||
const props = defineProps<{
|
||||
learningPathSlug: string
|
||||
circleSlug: string
|
||||
}>()
|
||||
|
||||
const circleStore = useCircleStore();
|
||||
circleStore.loadCircle(props.circleSlug);
|
||||
const circleStore = useCircleStore()
|
||||
|
||||
onMounted(async () => {
|
||||
log.info('CircleView.vue mounted');
|
||||
});
|
||||
log.debug('CircleView.vue mounted', props.learningPathSlug, props.circleSlug)
|
||||
|
||||
try {
|
||||
await circleStore.loadCircle(props.learningPathSlug, props.circleSlug)
|
||||
} catch (error) {
|
||||
log.error(error)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -35,10 +40,10 @@ onMounted(async () => {
|
|||
</Teleport>
|
||||
<Transition mode="out-in">
|
||||
<div v-if="circleStore.page === 'LEARNING_CONTENT'">
|
||||
<LearningContent :key="circleStore.currentLearningContent.translation_key"/>
|
||||
<LearningContent :key="circleStore.currentLearningContent.translation_key" />
|
||||
</div>
|
||||
<div v-else-if="circleStore.page === 'SELF_EVALUATION'">
|
||||
<SelfEvaluation :key="circleStore.currentSelfEvaluation.translation_key"/>
|
||||
<SelfEvaluation :key="circleStore.currentSelfEvaluation.translation_key" />
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="circle-container">
|
||||
|
|
@ -46,8 +51,9 @@ onMounted(async () => {
|
|||
<div class="flex flex-col lg:flex-row">
|
||||
<div class="flex-initial lg:w-128 px-4 py-4 lg:px-8 lg:pt-4 bg-white">
|
||||
<router-link
|
||||
to="/learningpath/versicherungsvermittlerin"
|
||||
:to="`/learn/${props.learningPathSlug}`"
|
||||
class="btn-text inline-flex items-center px-3 py-4 font-normal"
|
||||
data-cy="back-to-learning-path-button"
|
||||
>
|
||||
<it-icon-arrow-left class="-ml-1 mr-1 h-5 w-5"></it-icon-arrow-left>
|
||||
<span class="inline">zurück zum Lernpfad</span>
|
||||
|
|
@ -62,15 +68,12 @@ onMounted(async () => {
|
|||
</div>
|
||||
|
||||
<div class="border-t-2 border-gray-500 mt-4 lg:hidden">
|
||||
<div
|
||||
class="mt-4 inline-flex items-center"
|
||||
@click="circleStore.page = 'OVERVIEW'"
|
||||
>
|
||||
<it-icon-info class="mr-2"/>
|
||||
<div class="mt-4 inline-flex items-center" @click="circleStore.page = 'OVERVIEW'">
|
||||
<it-icon-info class="mr-2" />
|
||||
Das lernst du in diesem Circle
|
||||
</div>
|
||||
<div class="inline-flex items-center">
|
||||
<it-icon-message class="mr-2"/>
|
||||
<it-icon-message class="mr-2" />
|
||||
Fachexpertin kontaktieren
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -82,16 +85,15 @@ onMounted(async () => {
|
|||
{{ circleStore.circle?.description }}
|
||||
</div>
|
||||
|
||||
<button class="btn-primary mt-4 text-xl" @click="circleStore.page = 'OVERVIEW'">Erfahre mehr dazu
|
||||
<button class="btn-primary mt-4 text-xl" @click="circleStore.page = 'OVERVIEW'">
|
||||
Erfahre mehr dazu
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="expert border border-gray-500 mt-8 p-6">
|
||||
<h3 class="text-blue-dark">Hast du Fragen?</h3>
|
||||
<div class="prose mt-4">Tausche dich mit der Fachexpertin aus für den Circle Analyse aus.</div>
|
||||
<button class="btn-secondary mt-4 text-xl">
|
||||
Fachexpertin kontaktieren
|
||||
</button>
|
||||
<button class="btn-secondary mt-4 text-xl">Fachexpertin kontaktieren</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -101,14 +103,10 @@ onMounted(async () => {
|
|||
v-for="learningSequence in circleStore.circle?.learningSequences || []"
|
||||
:key="learningSequence.translation_key"
|
||||
>
|
||||
<LearningSequence
|
||||
:learning-sequence="learningSequence"
|
||||
></LearningSequence>
|
||||
<LearningSequence :learning-sequence="learningSequence"></LearningSequence>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -117,15 +115,8 @@ onMounted(async () => {
|
|||
</template>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
|
||||
.circle-container {
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
white 0%,
|
||||
white 50%,
|
||||
theme(colors.gray.200) 50%,
|
||||
theme(colors.gray.200) 100%
|
||||
);
|
||||
background: linear-gradient(to right, white 0%, white 50%, theme(colors.gray.200) 50%, theme(colors.gray.200) 100%);
|
||||
}
|
||||
|
||||
.circle {
|
||||
|
|
@ -142,5 +133,4 @@ onMounted(async () => {
|
|||
.v-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,31 +1,25 @@
|
|||
<script setup lang="ts">
|
||||
import * as log from 'loglevel';
|
||||
import {useUserStore} from '@/stores/user';
|
||||
import * as log from 'loglevel'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
log.debug('CockpitView created');
|
||||
|
||||
const userStore = useUserStore();
|
||||
log.debug('CockpitView created')
|
||||
|
||||
const userStore = useUserStore()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="px-8 py-8 lg:px-12 lg:py-12 bg-gray-200">
|
||||
<h1 data-cy="welcome-message">Willkommen, {{userStore.first_name}}</h1>
|
||||
<h1 data-cy="welcome-message">Willkommen, {{ userStore.first_name }}</h1>
|
||||
|
||||
<h2 class="mt-12">Deine Kurse</h2>
|
||||
|
||||
<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="/learningpath/versicherungsvermittlerin">
|
||||
Weiter gehts
|
||||
</router-link>
|
||||
<router-link class="btn-blue" to="/learn/versicherungsvermittlerin"> Weiter gehts </router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
<style scoped></style>
|
||||
|
|
|
|||
|
|
@ -1,18 +0,0 @@
|
|||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<main class="px-8 py-8">
|
||||
<h1>myVBV Start Page</h1>
|
||||
|
||||
<div class="mt-8 flex flex-col lg:flex-row justify-start gap-4">
|
||||
<router-link class="link text-xl" to="/styleguide">Styelguide</router-link>
|
||||
<a class="link text-xl" href="/login">Login</a>
|
||||
|
||||
<router-link class="link text-xl" to="/learningpath/versicherungsvermittlerin">Lernpfad "Versicherungsvermittlerin"</router-link>
|
||||
<router-link class="link text-xl" to="/circle/analyse">Circle "Analyse"</router-link>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
|
|
@ -1,35 +1,31 @@
|
|||
<script setup lang="ts">
|
||||
import * as log from 'loglevel'
|
||||
|
||||
import * as log from 'loglevel';
|
||||
import { onMounted } from 'vue'
|
||||
import { useLearningPathStore } from '@/stores/learningPath'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
import {computed, onMounted} from 'vue'
|
||||
import {useLearningPathStore} from '@/stores/learningPath';
|
||||
import {useUserStore} from '@/stores/user';
|
||||
import LearningPathDiagram from '@/components/circle/LearningPathDiagram.vue'
|
||||
import LearningPathViewVertical from '@/views/LearningPathViewVertical.vue'
|
||||
|
||||
import LearningPathDiagram from '@/components/circle/LearningPathDiagram.vue';
|
||||
import LearningPathViewVertical from "@/views/LearningPathViewVertical.vue";
|
||||
|
||||
|
||||
log.debug('LearningPathView created');
|
||||
log.debug('LearningPathView created')
|
||||
|
||||
const props = defineProps<{
|
||||
learningPathSlug: string
|
||||
}>()
|
||||
|
||||
const learningPathStore = useLearningPathStore();
|
||||
const userStore = useUserStore();
|
||||
|
||||
|
||||
const continueRoute = computed(() => {
|
||||
return "/circle/" + learningPathStore.learningPath.nextCircle.slug + "/";
|
||||
})
|
||||
const learningPathStore = useLearningPathStore()
|
||||
const userStore = useUserStore()
|
||||
|
||||
onMounted(async () => {
|
||||
log.info('LearningPathView mounted');
|
||||
await learningPathStore.loadLearningPath(props.learningPathSlug);
|
||||
console.log(learningPathStore)
|
||||
});
|
||||
log.debug('LearningPathView mounted')
|
||||
|
||||
try {
|
||||
await learningPathStore.loadLearningPath(props.learningPathSlug)
|
||||
} catch (error) {
|
||||
log.error(error)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -46,12 +42,8 @@ onMounted(async () => {
|
|||
<div class="flex flex-col h-max">
|
||||
<div class="bg-white py-8 flex flex-col">
|
||||
<div class="flex justify-end p-3">
|
||||
<button
|
||||
class="flex items-center"
|
||||
@click="learningPathStore.page = 'OVERVIEW'"
|
||||
data-cy="show-list-view"
|
||||
>
|
||||
<it-icon-list/>
|
||||
<button class="flex items-center" @click="learningPathStore.page = 'OVERVIEW'" data-cy="show-list-view">
|
||||
<it-icon-list />
|
||||
Listen Ansicht anzeigen
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -65,17 +57,24 @@ onMounted(async () => {
|
|||
<h1 data-cy="learning-path-title" class="m-12">{{ learningPathStore.learningPath.title }}</h1>
|
||||
|
||||
<div
|
||||
class="bg-white m-12 p-8 flex flex-col lg:flex-row divide-y lg:divide-y-0 lg:divide-x divide-gray-500 justify-start">
|
||||
class="bg-white 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-8 flex-auto">
|
||||
<h2 translate>Willkommmen zurück, {{ userStore.first_name }}</h2>
|
||||
<p class="mt-4 text-xl">
|
||||
|
||||
</p>
|
||||
<p class="mt-4 text-xl"></p>
|
||||
</div>
|
||||
<div class="p-8 flex-2" v-if="learningPathStore.learningPath.nextCircle" translate>
|
||||
<div class="p-8 flex-2" v-if="learningPathStore.learningPath.nextLearningContent" translate>
|
||||
Nächster Schirtt
|
||||
<h3>{{ learningPathStore.learningPath.nextCircle.title }}: {{ learningPathStore.learningPath.nextLearningSequence.title }}</h3>
|
||||
<router-link class="mt-4 btn-blue" v-bind:to="this.continueRoute" translate>
|
||||
<h3>
|
||||
{{ learningPathStore.learningPath.nextLearningContent.parentCircle.title }}:
|
||||
{{ learningPathStore.learningPath.nextLearningContent.parentLearningSequence.title }}
|
||||
</h3>
|
||||
<router-link
|
||||
class="mt-4 btn-blue"
|
||||
:to="`/learn/${learningPathStore.learningPath.slug}/${learningPathStore.learningPath.nextLearningContent.parentCircle.slug}`"
|
||||
data-cy="continue-button"
|
||||
translate
|
||||
>
|
||||
Los geht's
|
||||
</router-link>
|
||||
</div>
|
||||
|
|
@ -86,5 +85,4 @@ onMounted(async () => {
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
<style scoped></style>
|
||||
|
|
|
|||
|
|
@ -1,30 +1,27 @@
|
|||
<script setup lang="ts">
|
||||
import * as log from 'loglevel';
|
||||
import {reactive} from 'vue';
|
||||
import {useUserStore} from '@/stores/user';
|
||||
import {useRoute} from 'vue-router';
|
||||
import * as log from 'loglevel'
|
||||
import { reactive } from 'vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
log.debug('LoginView.vue created');
|
||||
log.debug(route.query);
|
||||
log.debug('LoginView.vue created')
|
||||
log.debug(route.query)
|
||||
|
||||
const state = reactive({
|
||||
username: '',
|
||||
password: '',
|
||||
});
|
||||
|
||||
const userStore = useUserStore();
|
||||
})
|
||||
|
||||
const userStore = useUserStore()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="px-8 py-8">
|
||||
<h1>Login</h1>
|
||||
|
||||
<form
|
||||
@submit.prevent="userStore.handleLogin(state.username, state.password, route.query.next)"
|
||||
>
|
||||
<form @submit.prevent="userStore.handleLogin(state.username, state.password, route.query.next)">
|
||||
<div class="mt-8 mb-4">
|
||||
<label class="block mb-1" for="email">Username</label>
|
||||
<input
|
||||
|
|
@ -47,17 +44,11 @@ const userStore = useUserStore();
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<input
|
||||
data-cy="login-button"
|
||||
type="submit"
|
||||
value="Login"
|
||||
class="btn-primary"
|
||||
/>
|
||||
<input data-cy="login-button" type="submit" value="Login" class="btn-primary" />
|
||||
</div>
|
||||
</form>
|
||||
<p class="pt-8"><a href="/sso/login/">Login mit SSO</a></p>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
<style scoped></style>
|
||||
|
|
|
|||
|
|
@ -54,8 +54,8 @@ const dropdownData = [
|
|||
// TODO: die CSS-Klasse für die Farben wird hier in der StyleGuideView.vue generiert.
|
||||
// deshalb muss man diese CSS-Klassen in tailwind.config.js "safelist"en, wenn diese sonst
|
||||
// noch nirgendwo verwendet werden.
|
||||
const colors = ['blue', 'sky', 'orange', 'green', 'red', 'gray',];
|
||||
const colorValues = [100, 200, 300, 400, 500, 600, 700, 800, 900,];
|
||||
const colors = ['blue', 'sky', 'green', 'red', 'orange', 'yellow', 'stone', 'gray', 'slate'];
|
||||
const colorValues = [200, 300, 400, 500, 600, 700, 800, 900,];
|
||||
|
||||
function colorBgClass(color: string, value: number) {
|
||||
return `bg-${color}-${value}`;
|
||||
|
|
@ -20,12 +20,8 @@ module.exports = {
|
|||
},
|
||||
colors: colors,
|
||||
},
|
||||
safelist: [{
|
||||
pattern: /bg-(blue|sky|orange|green|red)-(400|500|600|700)/,
|
||||
}, {
|
||||
pattern: /bg-gray-(100|300|500|700|900)/,
|
||||
},
|
||||
'bg-blue-900',
|
||||
safelist: [
|
||||
{ pattern: /bg-(blue|sky|green|red|orange|yellow|stone|gray|slate)-(200|300|400|500|600|700|800|900)/, },
|
||||
'it-icon',
|
||||
],
|
||||
plugins: [
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"strict": true,
|
||||
"allowJs": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ export default defineConfig(({ mode }) => {
|
|||
},
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'happy-dom',
|
||||
environment: 'jsdom',
|
||||
},
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ describe("circle page", () => {
|
|||
cy.manageCommand("cypress_reset");
|
||||
|
||||
login("admin", "test");
|
||||
cy.visit("/circle/analyse");
|
||||
cy.visit("/learn/versicherungsvermittlerin/analyse");
|
||||
});
|
||||
|
||||
it("can open circle page", () => {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ describe("learningPath page", () => {
|
|||
|
||||
it("can open learningPath page", () => {
|
||||
login("admin", "test");
|
||||
cy.visit("/learningpath/versicherungsvermittlerin");
|
||||
cy.visit("/learn/versicherungsvermittlerin");
|
||||
|
||||
cy.get('[data-cy="learning-path-title"]').should(
|
||||
"contain",
|
||||
|
|
@ -17,24 +17,43 @@ describe("learningPath page", () => {
|
|||
|
||||
it("click on circle on learningPath page will open circle", () => {
|
||||
login("admin", "test");
|
||||
cy.visit("/learningpath/versicherungsvermittlerin");
|
||||
cy.visit("/learn/versicherungsvermittlerin");
|
||||
|
||||
cy.get('[data-cy="circle-analyse"]').click({ force: true });
|
||||
|
||||
cy.url().should("include", "/circle/analyse");
|
||||
cy.url().should("include", "/learn/versicherungsvermittlerin/analyse");
|
||||
cy.get('[data-cy="circle-title"]').should("contain", "Analyse");
|
||||
});
|
||||
|
||||
it("open listView and click on cirle will open circle", () => {
|
||||
login("admin", "test");
|
||||
cy.visit("/learningpath/versicherungsvermittlerin");
|
||||
cy.visit("/learn/versicherungsvermittlerin");
|
||||
|
||||
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", "/circle/analyse");
|
||||
cy.url().should("include", "/learn/versicherungsvermittlerin/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");
|
||||
|
||||
// first click will open first circle
|
||||
cy.get('[data-cy="continue-button"]').click();
|
||||
cy.get('[data-cy="circle-title"]').should("contain", "Basis");
|
||||
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="lc-reiseversicherung-7"] > .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="continue-button"]').click();
|
||||
cy.get('[data-cy="circle-title"]').should("contain", "Unit-Test Circle");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ describe("login", () => {
|
|||
});
|
||||
|
||||
it("login will redirect to requestet page", () => {
|
||||
cy.visit("/learningpath/versicherungsvermittlerin");
|
||||
cy.visit("/learn/versicherungsvermittlerin");
|
||||
cy.get("h1").should("contain", "Login");
|
||||
|
||||
cy.get("#username").type("admin");
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -133,7 +133,7 @@ AUTH_USER_MODEL = "core.User"
|
|||
LOGIN_URL = "/login"
|
||||
LOGIN_REDIRECT_URL = "/"
|
||||
|
||||
ALLOW_LOCAL_LOGIN = env.bool("IT_ALLOW_LOCAL_LOGIN", default=False)
|
||||
ALLOW_LOCAL_LOGIN = env.bool("IT_ALLOW_LOCAL_LOGIN", default=DEBUG)
|
||||
|
||||
# PASSWORDS
|
||||
# ------------------------------------------------------------------------------
|
||||
|
|
@ -200,7 +200,7 @@ MEDIA_ROOT = str(APPS_DIR / "media")
|
|||
MEDIA_URL = "/media/"
|
||||
|
||||
IT_SERVE_VUE = env.bool("IT_SERVE_VUE", DEBUG)
|
||||
IT_SERVE_VUE_URL = env("IT_SERVE_VUE_URL", 'http://localhost:3000')
|
||||
IT_SERVE_VUE_URL = env("IT_SERVE_VUE_URL", 'http://localhost:5173')
|
||||
|
||||
# WAGTAIL
|
||||
# ------------------------------------------------------------------------------
|
||||
|
|
@ -454,7 +454,7 @@ REST_FRAMEWORK = {
|
|||
CORS_URLS_REGEX = r"^/api/.*$"
|
||||
|
||||
# django-csp
|
||||
CSP_DEFAULT_SRC = ("'self'", "'unsafe-inline'", 'ws://localhost:3000', 'localhost:8000', 'blob:', 'data:', 'http://*')
|
||||
CSP_DEFAULT_SRC = ("'self'", "'unsafe-inline'", 'ws://localhost:5173', 'localhost:8000', 'blob:', 'data:', 'http://*')
|
||||
CSP_FRAME_ANCESTORS = ("'self'",)
|
||||
|
||||
# By Default swagger ui is available only to admin user. You can change permission classs to change that
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
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)
|
||||
create_simple_test_learning_path(skip_locales=True)
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ class LearningPath(Page):
|
|||
verbose_name = "Learning Path"
|
||||
|
||||
def full_clean(self, *args, **kwargs):
|
||||
self.slug = find_available_slug(Page, slugify(self.title, allow_unicode=True))
|
||||
self.slug = find_available_slug(slugify(self.title, allow_unicode=True))
|
||||
super(LearningPath, self).full_clean(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
|
|
@ -54,8 +54,7 @@ class Topic(Page):
|
|||
# subpage_types = ['learnpath.Circle']
|
||||
|
||||
def full_clean(self, *args, **kwargs):
|
||||
self.slug = find_available_slug(Topic, slugify(self.title, allow_unicode=True))
|
||||
print(self.slug)
|
||||
self.slug = find_available_slug(slugify(f'topic-{self.title}', allow_unicode=True))
|
||||
super(Topic, self).full_clean(*args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
|
|
@ -115,8 +114,7 @@ class Circle(Page):
|
|||
)
|
||||
|
||||
def full_clean(self, *args, **kwargs):
|
||||
# TODO: why own slug function?
|
||||
self.slug = find_available_slug(Page, slugify(self.title, allow_unicode=True))
|
||||
self.slug = find_available_slug(slugify(self.title, allow_unicode=True))
|
||||
super(Circle, self).full_clean(*args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
|
|
@ -157,6 +155,7 @@ class LearningSequence(Page):
|
|||
</span>'''
|
||||
|
||||
def full_clean(self, *args, **kwargs):
|
||||
self.slug = find_available_slug(slugify(f'ls-{self.title}', allow_unicode=True))
|
||||
super(LearningSequence, self).full_clean(*args, **kwargs)
|
||||
|
||||
|
||||
|
|
@ -170,6 +169,10 @@ class LearningUnit(Page):
|
|||
def __str__(self):
|
||||
return f"{self.title}"
|
||||
|
||||
def full_clean(self, *args, **kwargs):
|
||||
self.slug = find_available_slug(slugify(f'lu-{self.title}', allow_unicode=True))
|
||||
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'])
|
||||
|
|
@ -188,6 +191,10 @@ class LearningUnitQuestion(Page):
|
|||
def __str__(self):
|
||||
return f"{self.title}"
|
||||
|
||||
def full_clean(self, *args, **kwargs):
|
||||
self.slug = find_available_slug(slugify(f'luq-{self.title}', allow_unicode=True))
|
||||
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', ])
|
||||
|
|
@ -240,7 +247,8 @@ class LearningContent(Page):
|
|||
verbose_name = "Learning Content"
|
||||
|
||||
def full_clean(self, *args, **kwargs):
|
||||
self.slug = find_available_slug(LearningContent, slugify(self.title, allow_unicode=True))
|
||||
self.slug = find_available_slug(slugify(f'lc-{self.title}', allow_unicode=True))
|
||||
print(self.slug)
|
||||
super(LearningContent, self).full_clean(*args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
|
|
@ -253,7 +261,7 @@ class LearningContent(Page):
|
|||
return f"{self.title}"
|
||||
|
||||
|
||||
def find_available_slug(model, requested_slug, ignore_page_id=None):
|
||||
def find_available_slug(requested_slug, ignore_page_id=None):
|
||||
"""
|
||||
Finds an available slug within the specified parent.
|
||||
|
||||
|
|
@ -270,8 +278,7 @@ def find_available_slug(model, requested_slug, ignore_page_id=None):
|
|||
treated as in use by another page.
|
||||
"""
|
||||
|
||||
# TODO: In comparison ot wagtails own function, I look for the same model instead of the parent
|
||||
pages = model.objects.filter(slug__startswith=requested_slug)
|
||||
pages = Page.objects.filter(slug__startswith=requested_slug)
|
||||
|
||||
if ignore_page_id:
|
||||
pages = pages.exclude(id=ignore_page_id)
|
||||
|
|
|
|||
|
|
@ -125,7 +125,39 @@ def create_simple_test_learning_path(user=None, skip_locales=True):
|
|||
site.save()
|
||||
|
||||
lp = LearningPathFactory(title="Unit-Test Lernpfad", parent=site.root_page)
|
||||
TopicFactory(title="Unit-Test Topic", is_visible=False, parent=lp)
|
||||
|
||||
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=[('video', VideoBlockFactory(
|
||||
url='https://www.youtube.com/embed/qhPIfxS2hvI',
|
||||
description='Basis Video'
|
||||
))]
|
||||
)
|
||||
LearningSequenceFactory(title='Beenden', parent=circle_basis, icon='it-icon-ls-end')
|
||||
LearningContentFactory(
|
||||
title='Kompetenzprofil anschauen',
|
||||
parent=circle_basis,
|
||||
minutes=30,
|
||||
contents=[('document', CompetenceBlockFactory())]
|
||||
)
|
||||
LearningContentFactory(
|
||||
title='Circle "Analyse" abschliessen',
|
||||
parent=circle_basis,
|
||||
minutes=30,
|
||||
contents=[('document', CompetenceBlockFactory())]
|
||||
)
|
||||
|
||||
TopicFactory(title="Gewinnen von Kunden", parent=lp)
|
||||
|
||||
circle_analyse = create_circle('Unit-Test Circle', lp)
|
||||
create_circle_children(circle_analyse, 'Unit-Test Circle')
|
||||
|
|
|
|||
|
|
@ -26,6 +26,6 @@ class TestRetrieveLearingPathContents(APITestCase):
|
|||
|
||||
self.assertEqual(learning_path.title, data['title'])
|
||||
# topic and circle
|
||||
self.assertEqual(2, len(data['children']))
|
||||
self.assertEqual(4, len(data['children']))
|
||||
# circle "unit-test-circle" contents
|
||||
self.assertEqual(13, len(data['children'][1]['children']))
|
||||
self.assertEqual(13, len(data['children'][3]['children']))
|
||||
|
|
|
|||
Loading…
Reference in New Issue