Add code formatting with prettier and black

This commit is contained in:
Daniel Egger 2022-09-30 17:43:06 +02:00
parent 38c8d2120a
commit 827e7a0fc0
140 changed files with 4588 additions and 3050 deletions

3
.gitignore vendored
View File

@ -283,6 +283,9 @@ cypress/videos
cypress/screenshots
cypress/test-reports
git-crypt-encrypted-files-check.txt
/server/vbv_lernwelt/static/css/tailwind.css
/server/vbv_lernwelt/static/vue/
/server/vbv_lernwelt/templates/vue/index.html

View File

@ -1,6 +1,8 @@
# VBV Lernwelt
Project setup is based on [cookiecutter-django](https://github.com/cookiecutter/cookiecutter-django) project template.
Project setup is based
on [cookiecutter-django](https://github.com/cookiecutter/cookiecutter-django) project
template.
## Run for development
@ -28,7 +30,6 @@ export IT_APP_ENVIRONMENT=development
See `.env_secrets/local_daniel.env` for more possible environment variables.
Especially set correct values for `POSTGRES_*` and `DATABASE_URL`
### Server part
Install python dependencies:
@ -37,7 +38,8 @@ Install python dependencies:
pip install -r server/requirements/requirements-dev.txt
```
The "prepare_server.sh" script will create the database according to `POSTGRES_*` environment variables.
The "prepare_server.sh" script will create the database according to `POSTGRES_*`
environment variables.
It will also setup the tables for django and run the django development server.
```bash
@ -61,13 +63,32 @@ npm run dev
### General part
Cypress is installed for client and server, so there is this package.json on the project root directory
Cypress is installed for client and server, so there is this package.json on the project
root directory
```bash
# in project root directory
npm install
```
### Git hooks
```bash
# install git hooks
ln -s ../../git-pre-commit.sh .git/hooks/pre-commit
ln -s ../../git-pre-push.sh .git/hooks/pre-push
```
### Actions on Save
You can enable some useful "Actions on Save" in your JetBrains IDE:
Preferences -> Tools -> Actions on Save
* Reformat Code
* Optimize Imports
* Run eslint --fix
* Run prettier
## Deployment to CapRover

View File

@ -6,6 +6,19 @@ pipelines:
- step:
name: python tests
max-time: 15
services:
- postgres
caches:
- 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
- ./server/run_tests_coverage.sh
- step:
name: python linting
max-time: 15
services:
- postgres
caches:
@ -17,8 +30,7 @@ pipelines:
- 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
- ufmt check server
- step:
name: js tests
max-time: 15
@ -30,6 +42,18 @@ pipelines:
- pwd
- npm install
- npm test
- step:
name: js linting
max-time: 15
caches:
- node
- clientnode
script:
- cd client
- pwd
- npm install
- npm run prettier:check
- npm run lint
- step:
name: cypress tests
max-time: 45

View File

@ -1,18 +1,21 @@
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution');
require("@rushstack/eslint-patch/modern-module-resolution");
module.exports = {
'root': true,
'extends': [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-typescript/recommended',
// "@vue/eslint-config-prettier"
root: true,
extends: [
"plugin:vue/vue3-recommended",
"eslint:recommended",
"@vue/eslint-config-typescript/recommended",
"@vue/eslint-config-prettier",
],
'env': {
'vue/setup-compiler-macros': true
env: {
"vue/setup-compiler-macros": true,
},
'rules': {
'@typescript-eslint/no-unused-vars': ['warn'],
}
}
ignorePatterns: ["versionize.js", "tailwind.config.js", "postcss.config.js"],
rules: {
"@typescript-eslint/no-unused-vars": ["warn"],
"@typescript-eslint/ban-ts-comment": ["warn"],
"prefer-const": ["warn"],
},
};

2
client/.prettierignore Normal file
View File

@ -0,0 +1,2 @@
dist
node_modules

View File

@ -1,6 +1,7 @@
{
"semi": false,
"singleQuote": true,
"tabWidth": 2,
"printWidth": 120
"semi": true,
"singleQuote": false,
"tabWidth": 2,
"printWidth": 88,
"organizeImportsSkipDestructiveCodeActions": true
}

View File

@ -1,3 +1,6 @@
{
"recommendations": ["johnsoncodehk.volar", "johnsoncodehk.vscode-typescript-vue-plugin"]
"recommendations": [
"johnsoncodehk.volar",
"johnsoncodehk.vscode-typescript-vue-plugin"
]
}

View File

@ -11,4 +11,5 @@ VBV Frontend
`npm run dev`
## Vue layouts
[How layouts are implemented](https://itnext.io/vue-tricks-smart-layouts-for-vuejs-5c61a472b69b)

View File

@ -6,7 +6,10 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- workaround for vitejs bundling -> reference https:// -->
<link href="https://vbv-lernwelt.control.iterativ.ch/static/fonts/BuenosAires/stylesheet.css" rel="stylesheet">
<link
href="https://vbv-lernwelt.control.iterativ.ch/static/fonts/BuenosAires/stylesheet.css"
rel="stylesheet"
/>
<script defer src="/server/core/icons/"></script>
<!-- end workaround -->

View File

@ -9,6 +9,8 @@
"coverage": "vitest run --coverage",
"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",
"prettier": "prettier . --write",
"prettier:check": "prettier . --check",
"tailwind": "tailwindcss -i tailwind.css -o ../server/vbv_lernwelt/static/css/tailwind.css --watch"
},
"dependencies": {
@ -33,17 +35,20 @@
"@types/lodash": "^4.14.184",
"@types/node": "^18.7.14",
"@vitejs/plugin-vue": "^3.0.3",
"@volar/vue-typescript": "^0.40.13",
"@vue/eslint-config-prettier": "^7.0.0",
"@vue/eslint-config-typescript": "^11.0.0",
"@vue/test-utils": "^2.0.2",
"@vue/tsconfig": "^0.1.3",
"autoprefixer": "^10.4.8",
"eslint": "8.22.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-vue": "^9.4.0",
"jsdom": "^20.0.0",
"postcss": "^8.4.14",
"postcss-import": "^14.1.0",
"prettier": "^2.7.1",
"prettier-plugin-organize-imports": "^3.1.1",
"replace-in-file": "^6.3.5",
"sass": "^1.54.6",
"sass-loader": "^12.6.0",

View File

@ -3,4 +3,4 @@ module.exports = {
tailwindcss: {},
autoprefixer: {},
},
}
};

View File

@ -1,28 +1,26 @@
<template>
<div id="app" class="flex flex-col min-h-screen">
<MainNavigationBar class="flex-none" />
<RouterView class="flex-auto" v-slot="{ Component }">
<RouterView v-slot="{ Component }" class="flex-auto">
<Transition mode="out-in" name="app">
<component :is="Component"></component>
</Transition>
</RouterView>
<Footer class="flex-none" />
<AppFooter class="flex-none" />
</div>
</template>
<script setup lang="ts">
import * as log from 'loglevel';
import * as log from "loglevel";
import MainNavigationBar from '@/components/MainNavigationBar.vue';
import Footer from '@/components/Footer.vue';
import {onMounted} from 'vue';
import MainNavigationBar from "@/components/MainNavigationBar.vue";
import { onMounted } from "vue";
log.debug('App created');
log.debug("App created");
onMounted(() => {
log.debug('App mounted');
log.debug("App mounted");
});
</script>
<style lang="postcss" scoped>
@ -35,5 +33,4 @@ onMounted(() => {
.app-leave-to {
opacity: 0;
}
</style>

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import * as log from 'loglevel'
import * as log from "loglevel";
log.debug('Footer created')
log.debug("AppFooter created");
</script>
<template>

View File

@ -1,91 +1,93 @@
<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 IconLogout from "@/components/icons/IconLogout.vue";
import IconSettings from "@/components/icons/IconSettings.vue";
import MobileMenu from "@/components/MobileMenu.vue";
import ItDropdown from "@/components/ui/ItDropdown.vue";
import { useAppStore } from "@/stores/app";
import { useLearningPathStore } from "@/stores/learningPath";
import { useUserStore } from "@/stores/user";
import { onMounted, reactive } from "vue";
import { useRoute, useRouter } from "vue-router";
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 route = useRoute();
const router = useRouter();
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[]) {
return checkPaths.some((path) => route.path.startsWith(path))
return checkPaths.some((path) => route.path.startsWith(path));
}
function inLearningPath() {
return isInRoutePath(['/learn/'])
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 {
return getLearningPathStringProp('title')
return getLearningPathStringProp("title");
}
function learninPathSlug(): string {
return getLearningPathStringProp('slug')
return getLearningPathStringProp("slug");
}
function handleDropdownSelect(data) {
log.debug('Selected action:', data.action)
log.debug("Selected action:", data.action);
switch (data.action) {
case 'settings':
router.push('/profile')
break
case 'logout':
userStore.handleLogout()
break
case "settings":
router.push("/profile");
break;
case "logout":
userStore.handleLogout();
break;
default:
console.log('no action')
console.log("no action");
}
}
function logout() {
userStore.handleLogout()
userStore.handleLogout();
}
onMounted(() => {
log.debug('MainNavigationBar mounted')
})
log.debug("MainNavigationBar mounted");
});
const profileDropdownData = [
[
{
title: 'Kontoeinstellungen',
title: "Kontoeinstellungen",
icon: IconSettings,
data: {
action: 'settings',
action: "settings",
},
},
],
[
{
title: 'Abmelden',
title: "Abmelden",
icon: IconLogout,
data: {
action: 'logout',
action: "logout",
},
},
],
]
];
</script>
<template>
@ -108,7 +110,9 @@ const profileDropdownData = [
<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>
<div class="text-white text-2xl pr-10 pl-3 ml-1 border-l border-white">
myVBV
</div>
</router-link>
</div>
@ -122,7 +126,7 @@ const profileDropdownData = [
<it-icon-message class="w-8 h-8 mr-6" />
</router-link>
<!-- Mobile menu button -->
<div @click="toggleNav" class="flex">
<div class="flex" @click="toggleNav">
<button
type="button"
class="w-8 h-8 text-white hover:text-sky-500 focus:outline-none focus:text-sky-500"
@ -158,7 +162,11 @@ const profileDropdownData = [
</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
@ -168,10 +176,14 @@ const profileDropdownData = [
>
Mediathek
</router-link>
<router-link to="/messages" class="nav-item flex flex-row items-center" data-cy="messages-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>
<div class="nav-item flex items-center" v-if="userStore.loggedIn">
<div v-if="userStore.loggedIn" class="nav-item flex items-center">
<ItDropdown
:button-classes="[]"
:list-items="profileDropdownData"
@ -179,7 +191,11 @@ 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 }}

View File

@ -1,80 +1,80 @@
<script setup lang="ts">
import ItFullScreenModal from '@/components/ui/ItFullScreenModal.vue'
import IconLogout from '@/components/icons/IconLogout.vue'
import IconSettings from '@/components/icons/IconSettings.vue'
import {useRouter} from "vue-router";
import IconLogout from "@/components/icons/IconLogout.vue";
import IconSettings from "@/components/icons/IconSettings.vue";
import ItFullScreenModal from "@/components/ui/ItFullScreenModal.vue";
import { useRouter } from "vue-router";
const router = useRouter()
const router = useRouter();
const props = defineProps<{
show: boolean,
userStore: object,
learningPathName: string,
learningPathSlug: string
}>()
show: boolean;
userStore: object;
learningPathName: string;
learningPathSlug: string;
}>();
const emits = defineEmits(['closemodal'])
const emits = defineEmits(["closemodal"]);
const clickLink = (to: string) => {
router.push(to)
emits('closemodal')
}
router.push(to);
emits("closemodal");
};
</script>
<template>
<ItFullScreenModal
:show="show"
@closemodal="$emit('closemodal')"
>
<div>
<ItFullScreenModal :show="show" @closemodal="$emit('closemodal')">
<div>
<div
v-if="userStore.loggedIn"
class="border-b border-gray-500 -mx-4 px-8 pb-4">
<div class="-ml-4 flex">
<div
v-if="userStore.avatar_url"
>
<img class="inline-block h-16 w-16 rounded-full"
:src="userStore.avatar_url"
alt=""/>
</div>
<div class="ml-6">
<h3>{{userStore.first_name}} {{userStore.last_name}}</h3>
<button
@click="clickLink('/profile')"
class="mt-2 inline-block flex items-center">
<IconSettings class="inline-block" /><span class="ml-3">Kontoeinstellungen</span>
</button>
</div>
</div>
</div>
<div>
<div
class="mt-6 pb-6 border-b border-gray-500"
v-if="learningPathName">
<h4 class="text-gray-900 text-sm">Kurs: {{learningPathName}}</h4>
<ul class="mt-6">
<li><button @click="clickLink(`/learningpath/${learningPathSlug}`)">Lernpfad</button></li>
<li class="mt-6">Kompetenzprofil</li>
</ul>
<div v-if="userStore.loggedIn" class="border-b border-gray-500 -mx-4 px-8 pb-4">
<div class="-ml-4 flex">
<div v-if="userStore.avatar_url">
<img
class="inline-block h-16 w-16 rounded-full"
:src="userStore.avatar_url"
alt=""
/>
</div>
<div class="ml-6">
<h3>{{ userStore.first_name }} {{ userStore.last_name }}</h3>
<button
class="mt-2 inline-block flex items-center"
@click="clickLink('/profile')"
>
<IconSettings class="inline-block" /><span class="ml-3"
>Kontoeinstellungen</span
>
</button>
</div>
</div>
</div>
<div class="mt-6 pb-6 border-b border-gray-500">
<ul>
<li>Shop</li>
<li class="mt-6">Mediathek</li>
</ul>
<div>
<div v-if="learningPathName" class="mt-6 pb-6 border-b border-gray-500">
<h4 class="text-gray-900 text-sm">Kurs: {{ learningPathName }}</h4>
<ul class="mt-6">
<li>
<button @click="clickLink(`/learningpath/${learningPathSlug}`)">
Lernpfad
</button>
</li>
<li class="mt-6">Kompetenzprofil</li>
</ul>
</div>
<div class="mt-6 pb-6 border-b border-gray-500">
<ul>
<li>Shop</li>
<li class="mt-6">Mediathek</li>
</ul>
</div>
<button
v-if="userStore.loggedIn"
type="button"
class="mt-6 items-center flex"
@click="userStore.handleLogout()"
>
<IconLogout class="inline-block" /><span class="ml-1">Abmelden</span>
</button>
</div>
<button
v-if="userStore.loggedIn"
type="button"
@click="userStore.handleLogout()"
class="mt-6 items-center flex">
<IconLogout class="inline-block" /><span class="ml-1">Abmelden</span>
</button>
</div>
</div>
</div>
</ItFullScreenModal>
</template>

View File

@ -1,18 +1,17 @@
import { describe, it } from 'vitest'
import MainNavigationBar from '../MainNavigationBar.vue'
import { createPinia, setActivePinia } from 'pinia'
import { createPinia, setActivePinia } from "pinia";
import { describe, it } from "vitest";
describe('MainNavigationBar', () => {
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())
})
setActivePinia(createPinia());
});
it('renders properly', () => {
expect(42).toBe(42)
it("renders properly", () => {
expect(42).toBe(42);
// const wrapper = mount(MainNavigationBar, {})
// expect(wrapper.text()).toContain('myVBV')
})
})
});
});

View File

@ -1,9 +1,9 @@
<script setup lang="ts">
import { useCircleStore } from "@/stores/circle";
import * as d3 from "d3";
import {computed, onMounted} from "vue";
import * as _ from 'lodash'
import {useCircleStore} from '@/stores/circle';
import * as log from 'loglevel';
import * as _ from "lodash";
import * as log from "loglevel";
import { computed, onMounted } from "vue";
import colors from "@/colors.json";
@ -11,65 +11,71 @@ const circleStore = useCircleStore();
function someFinished(learningSequence) {
if (circleStore.circle) {
return circleStore.circle.someFinishedInLearningSequence(learningSequence.translation_key);
return circleStore.circle.someFinishedInLearningSequence(
learningSequence.translation_key
);
}
return false;
}
function allFinished(learningSequence) {
if (circleStore.circle) {
return circleStore.circle.allFinishedInLearningSequence(learningSequence.translation_key);
return circleStore.circle.allFinishedInLearningSequence(
learningSequence.translation_key
);
}
return false;
}
onMounted(async () => {
log.debug('CircleDiagram mounted');
log.debug("CircleDiagram mounted");
render();
});
const pieData = computed(() => {
const circle = circleStore.circle
console.log('initial of compute pie data ', circle)
const circle = circleStore.circle;
console.log("initial of compute pie data ", circle);
if (circle) {
console.log('initial of compute pie data ', circle)
console.log("initial of compute pie data ", circle);
const pieWeights = new Array(Math.max(circle.learningSequences.length, 1)).fill(1)
const pieGenerator = d3.pie()
let angles = pieGenerator(pieWeights)
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)]
pie.title = thisLearningSequence.title
pie.icon = thisLearningSequence.icon
pie.startAngle = pie.startAngle + Math.PI
pie.endAngle = pie.endAngle + Math.PI
pie.arrowStartAngle = pie.endAngle + (pie.startAngle - pie.endAngle) / 2
pie.arrowEndAngle = pie.startAngle + (pie.startAngle - pie.endAngle) / 2
pie.translation_key = thisLearningSequence.translation_key
pie.slug = thisLearningSequence.slug
pie.someFinished = someFinished(thisLearningSequence)
pie.allFinished = allFinished(thisLearningSequence)
})
angles = angles.reverse()
return angles
const thisLearningSequence = circle.learningSequences[parseInt(pie.index)];
pie.title = thisLearningSequence.title;
pie.icon = thisLearningSequence.icon;
pie.startAngle = pie.startAngle + Math.PI;
pie.endAngle = pie.endAngle + Math.PI;
pie.arrowStartAngle = pie.endAngle + (pie.startAngle - pie.endAngle) / 2;
pie.arrowEndAngle = pie.startAngle + (pie.startAngle - pie.endAngle) / 2;
pie.translation_key = thisLearningSequence.translation_key;
pie.slug = thisLearningSequence.slug;
pie.someFinished = someFinished(thisLearningSequence);
pie.allFinished = allFinished(thisLearningSequence);
});
angles = angles.reverse();
return angles;
}
return {}
})
return {};
});
const width = 450
const height = 450
const radius = Math.min(width, height) / 2.4
const width = 450;
const height = 450;
const radius = Math.min(width, height) / 2.4;
function render() {
const arrowStrokeWidth = 2
const arrowStrokeWidth = 2;
const svg = d3.select('.circle-visualization')
.attr('viewBox', `0 0 ${width} ${height}`)
const svg = d3
.select(".circle-visualization")
.attr("viewBox", `0 0 ${width} ${height}`);
// Append markter as definition to the svg
svg.append("svg:defs").append("svg:marker")
svg
.append("svg:defs")
.append("svg:marker")
.attr("id", "triangle")
.attr("refX", 11)
.attr("refY", 11)
@ -79,12 +85,13 @@ function render() {
.attr("orient", "auto")
.append("path")
.attr("d", "M -1 0 l 10 0 M 0 -1 l 0 10")
.attr('transform', 'rotate(-90, 10, 0)')
.attr('stroke-width', arrowStrokeWidth)
.attr('stroke', colors.gray[500])
const g = svg.append('g').attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')')
.attr("transform", "rotate(-90, 10, 0)")
.attr("stroke-width", arrowStrokeWidth)
.attr("stroke", colors.gray[500]);
const g = svg
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
function getColor(d) {
let color = colors.gray[300];
@ -94,7 +101,7 @@ function render() {
if (d.allFinished) {
color = colors.green[500];
}
return color
return color;
}
function getHoverColor(d) {
@ -105,7 +112,7 @@ function render() {
if (d.allFinished) {
color = colors.green[400];
}
return color
return color;
}
// Generate the pie diagram wede
@ -113,84 +120,80 @@ function render() {
.arc()
.innerRadius(radius / 2.5)
.padAngle(12 / 360)
.outerRadius(radius)
.outerRadius(radius);
// Generate the arrows
const arrowRadius = radius * 1.1
const learningSequences = g.selectAll('.learningSegmentArc').data(pieData.value).enter().append('g')
.attr('class', 'learningSegmentArc')
.attr('role', 'button')
.attr('fill', colors.gray[300])
const arrowRadius = radius * 1.1;
const learningSequences = g
.selectAll(".learningSegmentArc")
.data(pieData.value)
.enter()
.append("g")
.attr("class", "learningSegmentArc")
.attr("role", "button")
.attr("fill", colors.gray[300]);
learningSequences
.on('mouseover', function (d, i) {
.on("mouseover", function (d, i) {
d3.select(this)
.transition()
.duration('200')
.attr('fill', (d) => {
return getHoverColor(d)
})
.duration("200")
.attr("fill", (d) => {
return getHoverColor(d);
});
})
.on('mouseout', function (d, i) {
.on("mouseout", function (d, i) {
d3.select(this)
.transition()
.duration('200')
.attr('fill', (d) => {
return getColor(d)
})
.duration("200")
.attr("fill", (d) => {
return getColor(d);
});
})
.on('click', function (d, elm) {
console.log('clicked on ', d, elm)
document.getElementById(elm.slug)?.scrollIntoView({behavior: 'smooth'})
})
.on("click", function (d, elm) {
console.log("clicked on ", d, elm);
document.getElementById(elm.slug)?.scrollIntoView({ behavior: "smooth" });
});
learningSequences
.transition()
.duration(1)
.attr('fill', (d) => {
return getColor(d)
})
learningSequences.append('path').attr('d', wedgeGenerator)
.attr("fill", (d) => {
return getColor(d);
});
learningSequences.append("path").attr("d", wedgeGenerator);
const learningSequenceText = learningSequences
.append('text')
.attr('fill', colors.blue[900])
.style('font-size', '15px')
.append("text")
.attr("fill", colors.blue[900])
.style("font-size", "15px")
.text((d) => {
return d.title
return d.title;
})
.attr("transform", function (d) {
let translate = wedgeGenerator.centroid(d)
translate = [translate[0], translate[1] + 20]
let translate = wedgeGenerator.centroid(d);
translate = [translate[0], translate[1] + 20];
return "translate(" + translate + ")";
})
.attr('class', 'circlesText text-xl font-bold')
.style('text-anchor', 'middle')
.attr("class", "circlesText text-xl font-bold")
.style("text-anchor", "middle");
const iconWidth = 25
const iconWidth = 25;
const learningSequenceIcon = learningSequences.append("svg:image")
const learningSequenceIcon = learningSequences
.append("svg:image")
.attr("xlink:href", (d) => {
return "/static/icons/" + d.icon.replace("it-", "") + ".svg"
return "/static/icons/" + d.icon.replace("it-", "") + ".svg";
})
.attr("width", iconWidth)
.attr("height", iconWidth)
.attr("transform", function (d) {
let translate = wedgeGenerator.centroid(d)
translate = [translate[0] - iconWidth / 2, translate[1] - iconWidth]
let translate = wedgeGenerator.centroid(d);
translate = [translate[0] - iconWidth / 2, translate[1] - iconWidth];
return "translate(" + translate + ")";
})
});
// Create Arrows
const arrow = d3
@ -198,20 +201,19 @@ function render() {
.innerRadius(arrowRadius)
.outerRadius(arrowRadius + arrowStrokeWidth)
.padAngle(20 / 360)
.startAngle(d => {
return d.arrowStartAngle
.startAngle((d) => {
return d.arrowStartAngle;
})
.endAngle(d => {
return d.arrowEndAngle
})
.endAngle((d) => {
return d.arrowEndAngle;
});
const arrows = g
.selectAll('.arrow')
.selectAll(".arrow")
.data(pieData.value)
.join('g')
.attr('class', 'arrow')
.attr('marker-end', 'url(#triangle)')
.join("g")
.attr("class", "arrow")
.attr("marker-end", "url(#triangle)");
// remove last arrow
d3.selection.prototype.last = function () {
@ -219,24 +221,34 @@ function render() {
return d3.select(this.nodes()[last]);
};
const all_arows = g.selectAll('.arrow')
all_arows.last().remove()
const all_arows = g.selectAll(".arrow");
all_arows.last().remove();
//Draw arrow paths
arrows.append('path').attr('fill', colors.gray[500]).attr('d', arrow)
return svg
arrows.append("path").attr("fill", colors.gray[500]).attr("d", arrow);
return svg;
}
</script>
<template>
<div class="svg-container h-full content-center">
<pre hidden>{{ pieData }}</pre>
<pre hidden>{{ render() }}</pre>
<svg class="circle-visualization h-full">
<circle v-if="!circleStore.circle" :cx="width / 2" :cy="height / 2" :r="radius" :color="colors.gray[300]"/>
<circle v-if="!circleStore.circle" :cx="width / 2" :cy="height / 2" :r="radius / 2.5" color="white"/>
<circle
v-if="!circleStore.circle"
:cx="width / 2"
:cy="height / 2"
:r="radius"
:color="colors.gray[300]"
/>
<circle
v-if="!circleStore.circle"
:cx="width / 2"
:cy="height / 2"
:r="radius / 2.5"
color="white"
/>
</svg>
</div>
</template>

View File

@ -1,35 +1,44 @@
<script setup lang="ts">
import type { Circle } from '@/services/circle'
import ItFullScreenModal from '@/components/ui/ItFullScreenModal.vue'
import ItFullScreenModal from "@/components/ui/ItFullScreenModal.vue";
import type { Circle } from "@/services/circle";
const props = defineProps<{
circle: Circle | undefined
show: boolean
}>()
circle: Circle | undefined;
show: boolean;
}>();
// const emits = defineEmits(['closemodal'])
</script>
<template>
<ItFullScreenModal :show="show" @closemodal="$emit('closemodal')">
<div class="container-medium" v-if="circle">
<div v-if="circle" class="container-medium">
<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>
<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>
<ul class="mt-4">
<li class="text-xl flex items-center" v-for="goal in circle.goals" :key="goal.id">
<it-icon-check class="mt-4 hidden lg:block w-12 h-12 text-sky-500 flex-none"></it-icon-check>
<li
v-for="goal in circle.goals"
:key="goal.id"
class="text-xl flex items-center"
>
<it-icon-check
class="mt-4 hidden lg:block w-12 h-12 text-sky-500 flex-none"
></it-icon-check>
<div class="mt-4">{{ goal.value }}</div>
</li>
</ul>
</div>
<h3 class="mt-16">
Du wirst dein neu erworbenes Wissen auf folgenden berufstypischen Situation anwenden können:
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">

View File

@ -1,29 +1,31 @@
<script setup lang="ts">
import * as log from 'loglevel'
import { computed } from 'vue'
import type { LearningContent } from '@/types'
import { useCircleStore } from '@/stores/circle'
import { useCircleStore } from "@/stores/circle";
import type { LearningContent } from "@/types";
import * as log from "loglevel";
import { computed } from "vue";
log.debug('LearningContent.vue setup')
log.debug("LearningContent.vue setup");
const circleStore = useCircleStore()
const circleStore = useCircleStore();
const props = defineProps<{
learningContent: LearningContent
}>()
learningContent: LearningContent;
}>();
const block = computed(() => {
if (props.learningContent?.contents?.length) {
return props.learningContent.contents[0]
return props.learningContent.contents[0];
}
return undefined
})
return undefined;
});
</script>
<template>
<div v-if="block">
<nav class="px-4 lg:px-8 py-4 flex justify-between items-center border-b border-gray-500">
<nav
class="px-4 lg:px-8 py-4 flex justify-between items-center border-b border-gray-500"
>
<button
type="button"
class="btn-text inline-flex items-center px-3 py-2 font-normal"
@ -34,7 +36,9 @@ const block = computed(() => {
<span class="hidden lg:inline">zurück zum Circle</span>
</button>
<h1 class="text-xl hidden lg:block" data-cy="ln-title">{{ learningContent?.title }}</h1>
<h1 class="text-xl hidden lg:block" data-cy="ln-title">
{{ learningContent?.title }}
</h1>
<button
type="button"
@ -64,7 +68,6 @@ const block = computed(() => {
>
</iframe>
</div>
</div>
</div>
</template>

View File

@ -1,23 +1,48 @@
<script setup lang="ts">
import type { LearningContentType } from '@/types'
import { learningContentTypesToName } from '@/utils/typeMaps'
import type { LearningContentType } from "@/types";
import { learningContentTypesToName } from "@/utils/typeMaps";
const props = defineProps<{
learningContentType: LearningContentType
}>()
learningContentType: LearningContentType;
}>();
</script>
<template>
<div class="flex bg-gray-200 rounded-full px-2.5 py-0.5 gap-2 items-center w-min h-min">
<it-icon-lc-assignment class="w-6 h-6" v-if="props.learningContentType === 'assignment'" />
<it-icon-lc-exercise class="w-6 h-6" v-else-if="props.learningContentType === 'exercise'" />
<it-icon-lc-book class="w-6 h-6" v-else-if="props.learningContentType === 'book'" />
<it-icon-lc-video class="w-6 h-6" v-else-if="props.learningContentType === 'video'" />
<it-icon-lc-media-library class="w-6 h-6" v-else-if="props.learningContentType === 'media_library'" />
<it-icon-lc-test class="w-6 h-6" v-else-if="props.learningContentType === 'test'" />
<it-icon-lc-online-training class="w-6 h-6" v-else-if="props.learningContentType === 'online_training'" />
<it-icon-lc-resource class="w-6 h-6" v-else-if="props.learningContentType === 'resource'" />
<it-icon-lc-document class="w-6 h-6" v-else-if="props.learningContentType === 'document'" />
<p class="whitespace-nowrap">{{ learningContentTypesToName.get(props.learningContentType) }}</p>
<div
class="flex bg-gray-200 rounded-full px-2.5 py-0.5 gap-2 items-center w-min h-min"
>
<it-icon-lc-assignment
v-if="props.learningContentType === 'assignment'"
class="w-6 h-6"
/>
<it-icon-lc-exercise
v-else-if="props.learningContentType === 'exercise'"
class="w-6 h-6"
/>
<it-icon-lc-book v-else-if="props.learningContentType === 'book'" class="w-6 h-6" />
<it-icon-lc-video
v-else-if="props.learningContentType === 'video'"
class="w-6 h-6"
/>
<it-icon-lc-media-library
v-else-if="props.learningContentType === 'media_library'"
class="w-6 h-6"
/>
<it-icon-lc-test v-else-if="props.learningContentType === 'test'" class="w-6 h-6" />
<it-icon-lc-online-training
v-else-if="props.learningContentType === 'online_training'"
class="w-6 h-6"
/>
<it-icon-lc-resource
v-else-if="props.learningContentType === 'resource'"
class="w-6 h-6"
/>
<it-icon-lc-document
v-else-if="props.learningContentType === 'document'"
class="w-6 h-6"
/>
<p class="whitespace-nowrap">
{{ learningContentTypesToName.get(props.learningContentType) }}
</p>
</div>
</template>

View File

@ -1,8 +1,8 @@
<script>
import * as d3 from 'd3'
import { useLearningPathStore } from '@/stores/learningPath'
import colors from '@/colors.json'
import * as log from 'loglevel'
import colors from "@/colors.json";
import { useLearningPathStore } from "@/stores/learningPath";
import * as d3 from "d3";
import * as log from "loglevel";
export default {
props: {
@ -15,294 +15,334 @@ export default {
type: String,
},
},
setup() {
const learningPathStore = useLearningPathStore();
return { learningPathStore };
},
data() {
return {
width: 1640,
height: 384,
}
},
setup() {
const learningPathStore = useLearningPathStore()
return { learningPathStore }
};
},
computed: {
viewBox() {
return `0 0 ${this.width} ${this.height}`
return `0 0 ${this.width} ${this.height}`;
},
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) {
const 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()
const pieData = pieGenerator(pieWeights)
const pieWeights = new Array(
Math.max(circle.learningSequences.length, 1)
).fill(1);
const pieGenerator = d3.pie();
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.someFinished = someFinished(circle, thisLearningSequence)
pie.allFinished = allFinished(circle, thisLearningSequence)
})
const newCircle = {}
newCircle.pieData = pieData.reverse()
newCircle.title = circle.title
newCircle.slug = circle.slug.replace(`${circle.parentLearningPath.slug}-circle-`, '')
newCircle.id = circle.id
internalCircles.push(newCircle)
})
return internalCircles
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.someFinished = someFinished(circle, thisLearningSequence);
pie.allFinished = allFinished(circle, thisLearningSequence);
});
const newCircle = {};
newCircle.pieData = pieData.reverse();
newCircle.title = circle.title;
newCircle.slug = circle.slug.replace(
`${circle.parentLearningPath.slug}-circle-`,
""
);
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)
return Object.assign({}, this.learningPathStore.learningPath);
},
},
mounted() {
log.debug('LearningPathDiagram mounted')
log.debug("LearningPathDiagram mounted");
if (this.vertical) {
this.width = Math.min(960, window.innerWidth - 32)
this.height = 860
this.width = Math.min(960, window.innerWidth - 32);
this.height = 860;
}
const circleWidth = this.vertical ? 60 : 200
const radius = (circleWidth * 0.8) / 2
const circleWidth = this.vertical ? 60 : 200;
const radius = (circleWidth * 0.8) / 2;
function getColor(d) {
let color = colors.gray[300]
let color = colors.gray[300];
if (d.someFinished) {
color = colors.sky[500]
color = colors.sky[500];
}
if (d.allFinished) {
color = colors.green[500]
color = colors.green[500];
}
return color
return color;
}
function getHoverColor(d) {
let color = colors.gray[200]
let color = colors.gray[200];
if (d.someFinished) {
color = colors.sky[400]
color = colors.sky[400];
}
if (d.allFinished) {
color = colors.green[400]
color = colors.green[400];
}
return color
return color;
}
const vueRouter = this.$router
const vueRouter = this.$router;
// Create append pie charts to the main svg
const circle_groups = this.svg
.selectAll('.circle')
.selectAll(".circle")
.data(this.circles)
.enter()
.append('g')
.attr('class', 'circle')
.attr('data-cy', (d) => {
.append("g")
.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}`
return `circle-${d.slug}`;
}
})
.on('mouseover', function (d, i) {
.on("mouseover", function (d, i) {
d3.select(this)
.selectAll('.learningSegmentArc')
.selectAll(".learningSegmentArc")
.transition()
.duration(200)
.attr('fill', (d) => {
return getHoverColor(d)
})
.attr("fill", (d) => {
return getHoverColor(d);
});
})
.on('mouseout', function (d, i) {
.on("mouseout", function (d, i) {
d3.select(this)
.selectAll('.learningSegmentArc')
.selectAll(".learningSegmentArc")
.transition()
.duration(200)
.attr('fill', (d) => {
return getColor(d)
})
.attr("fill", (d) => {
return getColor(d);
});
})
.on('click', (d, i) => {
vueRouter.push(`/learn/${this.learningPathStore.learningPath.slug}/${i.slug}`)
.on("click", (d, i) => {
vueRouter.push(`/learn/${this.learningPathStore.learningPath.slug}/${i.slug}`);
})
.attr('role', 'button')
.attr("role", "button");
const arcGenerator = d3
.arc()
.innerRadius(radius / 2)
.padAngle(12 / 360)
.outerRadius(radius)
.outerRadius(radius);
//Generate groups
const arcs = this.svg
.selectAll('g')
.selectAll('.learningSegmentArc')
.selectAll("g")
.selectAll(".learningSegmentArc")
.data((d) => {
return d.pieData
return d.pieData;
})
.enter()
.append('g')
.attr('class', 'learningSegmentArc')
.attr('fill', colors.gray[300])
.append("g")
.attr("class", "learningSegmentArc")
.attr("fill", colors.gray[300]);
arcs
.transition()
.duration(1000)
.attr('fill', (d) => {
return getColor(d)
})
.attr("fill", (d) => {
return getColor(d);
});
//Draw arc paths
arcs.append('path').attr('d', arcGenerator)
arcs.append("path").attr("d", arcGenerator);
const circlesText = circle_groups
.append('text')
.attr('fill', colors.blue[900])
.style('font-size', '18px')
.style('overflow-wrap', 'break-word')
.append("text")
.attr("fill", colors.blue[900])
.style("font-size", "18px")
.style("overflow-wrap", "break-word")
.text((d) => {
if (!this.vertical) {
return d.title.replace('Prüfungsvorbereitung', 'Prüfungs- vorbereitung')
return d.title.replace("Prüfungsvorbereitung", "Prüfungs- vorbereitung");
}
return d.title
})
return d.title;
});
const topicHeightOffset = 20
const topicHeight = 50
const circleHeigth = circleWidth + 20
const topicHeightOffset = 20;
const topicHeight = 50;
const circleHeigth = circleWidth + 20;
function getTopicHorizontalPosition(i, d, topics) {
let x = 0
let x = 0;
for (let index = 0; index < i; index++) {
x += circleWidth * topics[index].circles.length
x += circleWidth * topics[index].circles.length;
}
return x + 30
return x + 30;
}
function getTopicVerticalPosition(i, d, topics) {
let pos = topicHeightOffset
let pos = topicHeightOffset;
for (let index = 0; index < i; index++) {
const topic = topics[index]
const topic = topics[index];
if (topic.is_visible) {
pos += topicHeight
pos += topicHeight;
}
pos += circleHeigth * topic.circles.length
pos += circleHeigth * topic.circles.length;
}
return pos + topicHeightOffset
return pos + topicHeightOffset;
}
function getCircleVerticalPostion(i, d, topics) {
let y = circleHeigth / 2 + topicHeightOffset + 10
let y = circleHeigth / 2 + topicHeightOffset + 10;
for (let topic_index = 0; topic_index < topics.length; topic_index++) {
const topic = topics[topic_index]
const topic = topics[topic_index];
if (topic.is_visible) {
y += topicHeight
y += topicHeight;
}
for (let circle_index = 0; circle_index < topic.circles.length; circle_index++) {
const circle = topic.circles[circle_index]
for (
let circle_index = 0;
circle_index < topic.circles.length;
circle_index++
) {
const circle = topic.circles[circle_index];
if (circle.id === d.id) {
return y
return y;
}
y += circleHeigth
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')
.attr('fill', colors.blue[900])
.style('font-size', '16px')
.text((d) => d.title)
.append("text")
.attr("fill", colors.blue[900])
.style("font-size", "16px")
.text((d) => d.title);
// Calculate positions of objects
if (this.vertical) {
const Circles_X = radius
const Topics_X = Circles_X - radius
const Circles_X = radius;
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')
.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) + ')'
})
.attr('class', (d) => {
return 'topic '.concat(d.is_visible ? 'block' : 'hidden')
.attr("transform", (d, i) => {
return (
"translate(" +
Topics_X +
", " +
getTopicVerticalPosition(i, d, this.learningPath.topics) +
")"
);
})
.attr("class", (d) => {
return "topic ".concat(d.is_visible ? "block" : "hidden");
});
topicLines.transition().duration('1000').attr('x2', this.width)
topicLines.transition().duration("1000").attr("x2", this.width);
topicTitles.attr('y', 30)
topicTitles.attr("y", 30);
} else {
circle_groups.attr('transform', (d, i) => {
const 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)
.style('text-anchor', 'middle')
.attr("y", radius + 30)
.style("text-anchor", "middle")
.call(wrap, circleWidth - 20)
.attr('class', 'circlesText text-xl font-bold hidden lg:block')
.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)'
})
.attr('class', (d) => {
return 'topic '.concat(d.is_visible ? 'hidden lg:block' : 'hidden')
.attr("transform", (d, i) => {
return (
"translate(" +
getTopicHorizontalPosition(
i,
d,
this.learningPathStore.learningPath.topics
) +
",0)"
);
})
.attr("class", (d) => {
return "topic ".concat(d.is_visible ? "hidden lg:block" : "hidden");
});
topicLines
.attr('x1', -10)
.attr('y1', 0)
.attr('x2', -10)
.attr('y2', 0)
.attr("x1", -10)
.attr("y1", 0)
.attr("x2", -10)
.attr("y2", 0)
.transition()
.duration('1000')
.attr('y2', 350)
.duration("1000")
.attr("y2", 350);
topicTitles
.attr('y', 20)
.style('font-size', '18px')
.attr("y", 20)
.style("font-size", "18px")
.call(wrap, circleWidth * 0.8)
.attr('class', 'topicTitles font-bold')
.attr("class", "topicTitles font-bold");
}
function wrap(text, width) {
@ -313,38 +353,42 @@ export default {
line = [],
lineNumber = 0,
lineHeight = 1.1, // ems
y = text.attr('y'),
y = text.attr("y"),
dy = 0, //parseFloat(text.attr('dy')),
tspan = text
.text(null)
.append('tspan')
.attr('x', 0)
.attr('y', y)
.attr('dy', dy + 'em')
.append("tspan")
.attr("x", 0)
.attr("y", y)
.attr("dy", dy + "em");
while ((word = words.pop())) {
line.push(word)
tspan.text(line.join(' '))
line.push(word);
tspan.text(line.join(" "));
if (tspan.node().getComputedTextLength() > width) {
line.pop()
tspan.text(line.join(' '))
line = [word]
line.pop();
tspan.text(line.join(" "));
line = [word];
tspan = text
.append('tspan')
.attr('x', 0)
.attr('y', y)
.attr('dy', ++lineNumber * lineHeight + dy + 'em')
.text(word)
.append("tspan")
.attr("x", 0)
.attr("y", y)
.attr("dy", ++lineNumber * lineHeight + dy + "em")
.text(word);
}
}
})
});
}
},
}
};
</script>
<template>
<div class="svg-container h-full content-start">
<svg class="learning-path-visualization h-full" :viewBox="viewBox" :id="identifier"></svg>
<svg
:id="identifier"
class="learning-path-visualization h-full"
:viewBox="viewBox"
></svg>
</div>
</template>

View File

@ -1,79 +1,90 @@
<script setup lang="ts">
import ItCheckbox from '@/components/ui/ItCheckbox.vue'
import type { CourseCompletionStatus, LearningContent, LearningSequence } from '@/types'
import { useCircleStore } from '@/stores/circle'
import { computed } from 'vue'
import _ from 'lodash'
import { humanizeDuration } from '@/utils/humanizeDuration'
import LearningContentBadge from './LearningContentTypeBadge.vue'
import ItCheckbox from "@/components/ui/ItCheckbox.vue";
import { useCircleStore } from "@/stores/circle";
import type {
CourseCompletionStatus,
LearningContent,
LearningSequence,
} from "@/types";
import { humanizeDuration } from "@/utils/humanizeDuration";
import _ from "lodash";
import { computed } from "vue";
import LearningContentBadge from "./LearningContentTypeBadge.vue";
const props = defineProps<{
learningSequence: LearningSequence
}>()
learningSequence: LearningSequence;
}>();
const circleStore = useCircleStore()
const circleStore = useCircleStore();
function toggleCompleted(learningContent: LearningContent) {
let completionStatus: CourseCompletionStatus = 'success'
if (learningContent.completion_status === 'success') {
completionStatus = 'fail'
let completionStatus: CourseCompletionStatus = "success";
if (learningContent.completion_status === "success") {
completionStatus = "fail";
}
circleStore.markCompletion(learningContent, completionStatus)
circleStore.markCompletion(learningContent, completionStatus);
}
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 continueTranslationKeyTuple = computed(() => {
if (props.learningSequence && circleStore.circle) {
const lastFinished = _.findLast(circleStore.circle.flatLearningContents, (learningContent) => {
return learningContent.completion_status === 'success'
})
const lastFinished = _.findLast(
circleStore.circle.flatLearningContents,
(learningContent) => {
return learningContent.completion_status === "success";
}
);
if (!lastFinished) {
// must be the first
return [circleStore.circle.flatLearningContents[0].translation_key, true]
return [circleStore.circle.flatLearningContents[0].translation_key, true];
}
if (lastFinished && lastFinished.nextLearningContent) {
return [lastFinished.nextLearningContent.translation_key, false]
return [lastFinished.nextLearningContent.translation_key, false];
}
}
return ''
})
return "";
});
const learningSequenceBorderClass = computed(() => {
let result: string[] = []
let result: string[] = [];
if (props.learningSequence && circleStore.circle) {
if (allFinished.value) {
result = ['border-l-4', 'border-l-green-500']
result = ["border-l-4", "border-l-green-500"];
} else if (someFinished.value) {
result = ['border-l-4', 'border-l-sky-500']
result = ["border-l-4", "border-l-sky-500"];
} else {
result = ['border-l-gray-500']
result = ["border-l-gray-500"];
}
}
return result
})
return result;
});
</script>
<template>
<div class="mb-8 learning-sequence" :id="learningSequence.slug">
<div :id="learningSequence.slug" class="mb-8 learning-sequence">
<div class="flex items-center gap-4 mb-2 text-blue-900">
<component :is="learningSequence.icon" />
<h3 class="text-xl font-semibold">
@ -82,11 +93,20 @@ const learningSequenceBorderClass = computed(() => {
<div>{{ humanizeDuration(learningSequence.minutes) }}</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="pb-3 lg:pg-6 flex gap-4 text-blue-900" v-if="learningUnit.title">
<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 v-if="learningUnit.title" class="pb-3 lg:pg-6 flex gap-4 text-blue-900">
<div class="font-semibold">{{ learningUnit.title }}</div>
<div class="whitespace-nowrap">{{ humanizeDuration(learningUnit.minutes) }}</div>
<div class="whitespace-nowrap">
{{ humanizeDuration(learningUnit.minutes) }}
</div>
</div>
<div
@ -95,39 +115,54 @@ const learningSequenceBorderClass = computed(() => {
class="flex gap-4 pb-3 lg:pb-6"
>
<ItCheckbox
:modelValue="learningContent.completion_status === 'success'"
:onToggle="() => toggleCompleted(learningContent)"
:model-value="learningContent.completion_status === 'success'"
:on-toggle="() => toggleCompleted(learningContent)"
:data-cy="`${learningContent.slug}`"
>
<span class="flex flex-wrap gap-4 items-center">
<div
@click.stop="circleStore.openLearningContent(learningContent)"
class="cursor-pointer w-full sm:w-auto"
@click.stop="circleStore.openLearningContent(learningContent)"
>
{{ learningContent.title }}
</div>
<div class="flex items-center gap-4 flex-col justify-between sm:flex-row sm:grow">
<div
class="flex items-center gap-4 flex-col justify-between sm:flex-row sm:grow"
>
<button
v-if="learningContent.translation_key === continueTranslationKeyTuple[0]"
v-if="
learningContent.translation_key === continueTranslationKeyTuple[0]
"
class="btn-blue order-1 sm:order-none"
data-cy="ls-continue-button"
@click.stop="circleStore.openLearningContent(learningContent)"
>
<span v-if="continueTranslationKeyTuple[1]" class="whitespace-nowrap">Los geht's </span>
<span v-if="continueTranslationKeyTuple[1]" class="whitespace-nowrap"
>Los geht's
</span>
<span v-else class="whitespace-nowrap"> Weiter geht's </span>
</button>
<div class="hidden sm:block"></div>
<div class="w-full sm:w-auto">
<LearningContentBadge :learningContentType="learningContent.contents[0].type" />
<LearningContentBadge
:learning-content-type="learningContent.contents[0].type"
/>
</div>
</div>
</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">
<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>

View File

@ -1,39 +1,41 @@
<script setup lang="ts">
import * as log from 'loglevel'
import { computed, reactive } from 'vue'
import { useCircleStore } from '@/stores/circle'
import type { LearningUnit } from '@/types'
import { useCircleStore } from "@/stores/circle";
import type { LearningUnit } from "@/types";
import * as log from "loglevel";
import { computed, reactive } from "vue";
log.debug('LearningContent.vue setup')
log.debug("LearningContent.vue setup");
const circleStore = useCircleStore()
const circleStore = useCircleStore();
const state = reactive({
questionIndex: 0,
})
});
const props = defineProps<{
learningUnit: LearningUnit
}>()
learningUnit: LearningUnit;
}>();
const questions = computed(() => props.learningUnit?.children)
const currentQuestion = computed(() => questions.value[state.questionIndex])
const questions = computed(() => props.learningUnit?.children);
const currentQuestion = computed(() => questions.value[state.questionIndex]);
function handleContinue() {
log.debug('handleContinue')
log.debug("handleContinue");
if (state.questionIndex + 1 < questions.value.length) {
log.debug('increment questionIndex', state.questionIndex)
state.questionIndex += 1
log.debug("increment questionIndex", state.questionIndex);
state.questionIndex += 1;
} else {
log.debug('continue to next learning content')
circleStore.continueFromSelfEvaluation()
log.debug("continue to next learning content");
circleStore.continueFromSelfEvaluation();
}
}
</script>
<template>
<div v-if="learningUnit">
<nav class="px-4 lg:px-8 py-4 flex justify-between items-center border-b border-gray-500">
<nav
class="px-4 lg:px-8 py-4 flex justify-between items-center border-b border-gray-500"
>
<button
type="button"
class="btn-text inline-flex items-center px-3 py-2 font-normal"
@ -49,11 +51,14 @@ function handleContinue() {
</nav>
<div class="mx-auto max-w-6xl px-4 lg:px-8 py-4">
<div class="mt-2 lg:mt-8 text-gray-700">Schritt {{ state.questionIndex + 1 }} von {{ questions.length }}</div>
<div class="mt-2 lg:mt-8 text-gray-700">
Schritt {{ state.questionIndex + 1 }} von {{ questions.length }}
</div>
<p class="text-xl mt-4">
Überprüfe, ob du in der Lernheinheit
<span class="font-bold">"{{ learningUnit.title }}"</span> alles verstanden hast.<br />
<span class="font-bold">"{{ learningUnit.title }}"</span> alles verstanden
hast.<br />
Lies die folgende Aussage und bewerte sie:
</p>
@ -62,32 +67,34 @@ function handleContinue() {
<div class="mt-4 lg:mt-8 flex flex-col lg:flex-row justify-between gap-6">
<button
@click="circleStore.markCompletion(currentQuestion, 'success')"
class="flex-1 inline-flex items-center text-left p-4 border"
:class="{
'border-green-500': currentQuestion.completion_status === 'success',
'border-2': currentQuestion.completion_status === 'success',
'border-gray-500': currentQuestion.completion_status !== 'success',
}"
@click="circleStore.markCompletion(currentQuestion, 'success')"
>
<it-icon-smiley-happy class="w-16 h-16 mr-4"></it-icon-smiley-happy>
<span class="font-bold text-xl"> Ja, ich kann das. </span>
</button>
<button
@click="circleStore.markCompletion(currentQuestion, 'fail')"
class="flex-1 inline-flex items-center text-left p-4 border"
:class="{
'border-orange-500': currentQuestion.completion_status === 'fail',
'border-2': currentQuestion.completion_status === 'fail',
'border-gray-500': currentQuestion.completion_status !== 'fail'
'border-gray-500': currentQuestion.completion_status !== 'fail',
}"
@click="circleStore.markCompletion(currentQuestion, 'fail')"
>
<it-icon-smiley-thinking class="w-16 h-16 mr-4"></it-icon-smiley-thinking>
<span class="font-bold text-xl"> Das muss ich nochmals anschauen. </span>
</button>
</div>
<div class="mt-6 lg:mt-12">Schau dein Fortschritt in deinem Kompetenzprofil: Kompetenzprofil öffnen</div>
<div class="mt-6 lg:mt-12">
Schau dein Fortschritt in deinem Kompetenzprofil: Kompetenzprofil öffnen
</div>
</div>
</div>
</div>

View File

@ -1,6 +1,18 @@
<template>
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M26.3292 15.7779C26.3292 15.7625 26.3474 15.7474 26.3536 15.7321C26.3619 15.71 26.3691 15.6877 26.3749 15.6649V15.6222C26.39 15.5448 26.39 15.465 26.3749 15.3876V15.3449C26.3691 15.3221 26.3619 15.2998 26.3536 15.2777C26.3536 15.2623 26.3383 15.2472 26.3292 15.2319C26.32 15.2132 26.3098 15.1949 26.2985 15.1771L26.262 15.1313L26.2315 15.0918L20.9236 9.44226C20.8147 9.31721 20.6597 9.24193 20.494 9.23364C20.3283 9.22556 20.1665 9.2851 20.0457 9.39888C19.9249 9.51244 19.8558 9.67046 19.8541 9.83634C19.8522 10.002 19.9181 10.1615 20.0363 10.2776L24.3779 14.8906H7.47836C7.1417 14.8906 6.86865 15.1634 6.86865 15.5003C6.86865 15.8371 7.14172 16.11 7.47836 16.11H24.3779L20.0363 20.7229C19.8213 20.9698 19.8398 21.3426 20.0782 21.5672C20.3169 21.7916 20.6898 21.7877 20.9236 21.5583L26.2315 15.918L26.2619 15.8784L26.2985 15.8327C26.3098 15.8149 26.32 15.7966 26.3291 15.7779L26.3292 15.7779Z" fill="#0A0A0A"/>
<path d="M6.65855 27H11.5884C12.5588 27 13.4894 26.6146 14.1755 25.9286C14.8616 25.2423 15.247 24.3119 15.247 23.3414V21.2926C15.247 20.9559 14.9741 20.6829 14.6372 20.6829C14.3006 20.6829 14.0275 20.956 14.0275 21.2926V23.3506C14.0275 23.9975 13.7706 24.6179 13.3132 25.0753C12.8558 25.5328 12.2354 25.7897 11.5885 25.7897H6.65861C6.01166 25.7897 5.39134 25.5328 4.93386 25.0753C4.47642 24.6179 4.21952 23.9975 4.21952 23.3506V7.65855C4.21952 7.01161 4.47642 6.39129 4.93386 5.93381C5.39131 5.47636 6.01166 5.21946 6.65861 5.21946H11.5885C12.2354 5.21946 12.8557 5.47637 13.3132 5.93381C13.7706 6.39125 14.0275 7.01161 14.0275 7.65855V9.7074C14.0275 10.0441 14.3006 10.3171 14.6372 10.3171C14.9741 10.3171 15.247 10.044 15.247 9.7074V7.65855C15.247 6.68817 14.8616 5.75774 14.1755 5.07143C13.4894 4.38535 12.5588 4 11.5884 4H6.65855C5.68817 4 4.75774 4.38535 4.07143 5.07143C3.38535 5.75768 3 6.68811 3 7.65855V23.3413C3 24.3117 3.38535 25.2422 4.07143 25.9285C4.75768 26.6145 5.68811 26.9999 6.65855 26.9999V27Z" fill="#0A0A0A"/>
<svg
width="30"
height="30"
viewBox="0 0 30 30"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M26.3292 15.7779C26.3292 15.7625 26.3474 15.7474 26.3536 15.7321C26.3619 15.71 26.3691 15.6877 26.3749 15.6649V15.6222C26.39 15.5448 26.39 15.465 26.3749 15.3876V15.3449C26.3691 15.3221 26.3619 15.2998 26.3536 15.2777C26.3536 15.2623 26.3383 15.2472 26.3292 15.2319C26.32 15.2132 26.3098 15.1949 26.2985 15.1771L26.262 15.1313L26.2315 15.0918L20.9236 9.44226C20.8147 9.31721 20.6597 9.24193 20.494 9.23364C20.3283 9.22556 20.1665 9.2851 20.0457 9.39888C19.9249 9.51244 19.8558 9.67046 19.8541 9.83634C19.8522 10.002 19.9181 10.1615 20.0363 10.2776L24.3779 14.8906H7.47836C7.1417 14.8906 6.86865 15.1634 6.86865 15.5003C6.86865 15.8371 7.14172 16.11 7.47836 16.11H24.3779L20.0363 20.7229C19.8213 20.9698 19.8398 21.3426 20.0782 21.5672C20.3169 21.7916 20.6898 21.7877 20.9236 21.5583L26.2315 15.918L26.2619 15.8784L26.2985 15.8327C26.3098 15.8149 26.32 15.7966 26.3291 15.7779L26.3292 15.7779Z"
fill="#0A0A0A"
/>
<path
d="M6.65855 27H11.5884C12.5588 27 13.4894 26.6146 14.1755 25.9286C14.8616 25.2423 15.247 24.3119 15.247 23.3414V21.2926C15.247 20.9559 14.9741 20.6829 14.6372 20.6829C14.3006 20.6829 14.0275 20.956 14.0275 21.2926V23.3506C14.0275 23.9975 13.7706 24.6179 13.3132 25.0753C12.8558 25.5328 12.2354 25.7897 11.5885 25.7897H6.65861C6.01166 25.7897 5.39134 25.5328 4.93386 25.0753C4.47642 24.6179 4.21952 23.9975 4.21952 23.3506V7.65855C4.21952 7.01161 4.47642 6.39129 4.93386 5.93381C5.39131 5.47636 6.01166 5.21946 6.65861 5.21946H11.5885C12.2354 5.21946 12.8557 5.47637 13.3132 5.93381C13.7706 6.39125 14.0275 7.01161 14.0275 7.65855V9.7074C14.0275 10.0441 14.3006 10.3171 14.6372 10.3171C14.9741 10.3171 15.247 10.044 15.247 9.7074V7.65855C15.247 6.68817 14.8616 5.75774 14.1755 5.07143C13.4894 4.38535 12.5588 4 11.5884 4H6.65855C5.68817 4 4.75774 4.38535 4.07143 5.07143C3.38535 5.75768 3 6.68811 3 7.65855V23.3413C3 24.3117 3.38535 25.2422 4.07143 25.9285C4.75768 26.6145 5.68811 26.9999 6.65855 26.9999V27Z"
fill="#0A0A0A"
/>
</svg>
</template>

File diff suppressed because one or more lines are too long

View File

@ -2,21 +2,27 @@
<svg viewBox="0 0 39 35" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M38.3507 28.111C38.3898 28.1686 38.4124 28.2358 38.4162 28.3053C38.4201 28.3748 38.4049 28.4441 38.3725 28.5057C38.34 28.5672 38.2914 28.6188 38.2319 28.655C38.1724 28.6911 38.1042 28.7104 38.0346 28.7108H36.3463C36.2839 28.7105 36.2224 28.6949 36.1673 28.6655C36.1122 28.636 36.0652 28.5936 36.0302 28.5419L32.0792 22.7066L28.1282 28.5419C28.0932 28.5936 28.0461 28.636 27.991 28.6654C27.9359 28.6949 27.8745 28.7104 27.812 28.7108H26.122C26.0523 28.7104 25.9841 28.6911 25.9246 28.655C25.8651 28.6188 25.8165 28.5672 25.7841 28.5057C25.7516 28.4441 25.7365 28.3748 25.7403 28.3053C25.7442 28.2358 25.7668 28.1686 25.8058 28.111L31.7648 19.312C31.8 19.26 31.8475 19.2175 31.9029 19.1881C31.9584 19.1586 32.0202 19.1432 32.083 19.1432C32.1458 19.1432 32.2076 19.1586 32.2631 19.1881C32.3186 19.2175 32.366 19.26 32.4012 19.312L38.3507 28.111Z"
fill="#007AC3"/>
fill="#007AC3"
/>
<path
d="M12.6181 28.111C12.6572 28.1686 12.6798 28.2358 12.6836 28.3053C12.6874 28.3748 12.6723 28.4441 12.6399 28.5057C12.6074 28.5672 12.5588 28.6188 12.4993 28.655C12.4398 28.6911 12.3716 28.7104 12.302 28.7108H10.6119C10.5495 28.7104 10.488 28.6949 10.4329 28.6654C10.3779 28.636 10.3308 28.5936 10.2958 28.5419L6.34521 22.7066L2.3942 28.5419C2.35919 28.5936 2.31211 28.636 2.25702 28.6654C2.20193 28.6949 2.1405 28.7104 2.07805 28.7108H0.383918C0.314304 28.7104 0.246102 28.6911 0.186595 28.655C0.127088 28.6188 0.0785123 28.5672 0.0460511 28.5057C0.01359 28.4441 -0.00153491 28.3748 0.00228956 28.3053C0.00611403 28.2358 0.0287451 28.1686 0.0677665 28.111L6.0268 19.312C6.06202 19.26 6.10943 19.2175 6.1649 19.1881C6.22037 19.1586 6.2822 19.1432 6.34498 19.1432C6.40777 19.1432 6.4696 19.1586 6.52506 19.1881C6.58053 19.2175 6.62795 19.26 6.66317 19.312L12.6181 28.111Z"
fill="white"/>
fill="white"
/>
<path
d="M0.0660575 7.0891C0.027036 7.03145 0.00440495 6.96429 0.000580477 6.89478C-0.003244 6.82527 0.0118844 6.75603 0.0443455 6.69444C0.0768067 6.63286 0.125382 6.58125 0.184889 6.54512C0.244396 6.50899 0.312599 6.48971 0.382213 6.48932H2.07047C2.13293 6.48967 2.19436 6.50523 2.24944 6.53466C2.30453 6.56409 2.35161 6.60651 2.38662 6.65824L6.33718 12.4935L10.2882 6.65824C10.3232 6.60651 10.3703 6.56409 10.4254 6.53466C10.4805 6.50523 10.5419 6.48967 10.6043 6.48932H12.2944C12.364 6.48971 12.4322 6.50899 12.4917 6.54512C12.5512 6.58125 12.5998 6.63286 12.6323 6.69444C12.6647 6.75603 12.6799 6.82527 12.676 6.89478C12.6722 6.96429 12.6496 7.03145 12.6106 7.0891L6.65966 15.8876C6.62451 15.9397 6.57711 15.9824 6.52164 16.0118C6.46616 16.0413 6.40429 16.0568 6.34147 16.0568C6.27865 16.0568 6.21678 16.0413 6.16131 16.0118C6.10583 15.9824 6.05844 15.9397 6.02328 15.8876L0.0660575 7.0891Z"
fill="#007AC3"/>
fill="#007AC3"
/>
<path
d="M25.7982 7.0891C25.7592 7.03145 25.7366 6.96429 25.7328 6.89478C25.7289 6.82527 25.7441 6.75603 25.7765 6.69444C25.809 6.63286 25.8576 6.58125 25.9171 6.54512C25.9766 6.50899 26.0448 6.48971 26.1144 6.48932H27.8026C27.8651 6.48963 27.9265 6.50517 27.9816 6.53461C28.0367 6.56405 28.0838 6.60648 28.1188 6.65824L32.0698 12.4935L36.0208 6.65824C36.0558 6.60651 36.1029 6.56409 36.158 6.53466C36.2131 6.50523 36.2745 6.48967 36.337 6.48932H38.027C38.0966 6.48971 38.1648 6.50899 38.2244 6.54512C38.2839 6.58125 38.3324 6.63286 38.3649 6.69444C38.3974 6.75603 38.4125 6.82527 38.4087 6.89478C38.4048 6.96429 38.3822 7.03145 38.3432 7.0891L32.3841 15.8881C32.349 15.9402 32.3016 15.9828 32.2461 16.0123C32.1906 16.0418 32.1288 16.0572 32.066 16.0572C32.0031 16.0572 31.9413 16.0418 31.8858 16.0123C31.8303 15.9828 31.7829 15.9402 31.7478 15.8881L25.7982 7.0891Z"
fill="white"/>
fill="white"
/>
<path
d="M24.2526 20.5202C23.1808 19.5633 21.7844 19.0514 20.3481 19.0889C17.1866 19.0889 15.1465 21.3851 15.1465 24.9395V34.1161C15.1466 34.218 15.1872 34.3156 15.2592 34.3876C15.3313 34.4596 15.429 34.5 15.5308 34.5H17.1035C17.2053 34.5 17.303 34.4596 17.3751 34.3876C17.4472 34.3156 17.4877 34.218 17.4878 34.1161V28.5739H22.0228C22.1247 28.5738 22.2223 28.5333 22.2943 28.4612C22.3663 28.3891 22.4067 28.2914 22.4067 28.1896V26.7895C22.4067 26.6876 22.3663 26.5899 22.2943 26.5179C22.2223 26.4458 22.1247 26.4052 22.0228 26.4051H17.4874V24.8722C17.4874 22.5431 18.5262 21.2591 20.4163 21.2591C21.3288 21.2362 22.2134 21.5752 22.8769 22.2021L23.0395 22.3471C23.0789 22.3821 23.125 22.4086 23.1751 22.4251C23.2252 22.4415 23.2782 22.4474 23.3307 22.4425C23.3831 22.4376 23.434 22.4219 23.4802 22.3965C23.5264 22.3711 23.5668 22.3364 23.5991 22.2947L24.4545 21.1859C24.5125 21.1105 24.5406 21.0163 24.5331 20.9214C24.5257 20.8265 24.4834 20.7378 24.4143 20.6724C24.3343 20.596 24.263 20.5283 24.2526 20.5202Z"
fill="white"/>
fill="white"
/>
<path
d="M19.2443 6.13249C19.2444 6.08179 19.2546 6.03161 19.2742 5.98484C19.2938 5.93808 19.3224 5.89566 19.3585 5.86001C19.3945 5.82437 19.4373 5.79622 19.4843 5.77717C19.5313 5.75813 19.5816 5.74857 19.6323 5.74904C19.7226 5.74904 19.7989 5.74904 19.8129 5.7522C21.1089 5.881 22.3057 6.50307 23.1558 7.4897C24.0059 8.47632 24.4442 9.75198 24.38 11.0527C24.2917 12.3636 23.7038 13.5907 22.7374 14.4808C21.7709 15.3709 20.4998 15.8562 19.186 15.8366H15.5277C15.4258 15.8365 15.3282 15.7959 15.2562 15.7238C15.1842 15.6518 15.1438 15.5541 15.1438 15.4522V0.884351C15.1438 0.782493 15.1842 0.6848 15.2562 0.612733C15.3282 0.540666 15.4258 0.50012 15.5277 0.5H17.0976C17.1996 0.5 17.2973 0.540494 17.3694 0.612574C17.4415 0.684654 17.482 0.782414 17.482 0.884351V13.2405C17.482 13.3425 17.5225 13.4402 17.5946 13.5123C17.6666 13.5844 17.7644 13.6249 17.8663 13.6249H19.2326C19.9527 13.6327 20.6501 13.3736 21.1905 12.8975C21.7308 12.4215 22.0758 11.7622 22.1588 11.0469C22.2259 10.2951 21.9924 9.5474 21.5092 8.96758C21.0261 8.38775 20.3328 8.0231 19.5812 7.95353C19.4866 7.94417 19.3988 7.89972 19.3353 7.82892C19.2718 7.75812 19.2371 7.66611 19.238 7.57099L19.2443 6.13249Z"
fill="white"/>
fill="white"
/>
</svg>
</template>

View File

@ -1,20 +1,20 @@
<script setup lang="ts">
import MediaLink from '@/components/mediaCenter/MediaLink.vue'
import MediaLink from "@/components/mediaCenter/MediaLink.vue";
export interface Props {
title: string
description: string
linkText: string
url: string
icon: string
openWindow?: boolean
title: string;
description: string;
linkText: string;
url: string;
icon: string;
openWindow?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
icon: '',
description: '',
icon: "",
description: "",
openWindow: false,
})
});
</script>
<template>

View File

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

View File

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

View File

@ -1,16 +1,19 @@
<script setup lang="ts">
interface Props {
modelValue?: boolean
disabled?: boolean
onToggle: () => void
modelValue?: boolean;
disabled?: boolean;
onToggle?: () => void;
}
const props = withDefaults(defineProps<Props>(), {
modelValue: false,
disabled: false,
})
onToggle: () => {
// do nothing
},
});
defineEmits(['update:modelValue'])
defineEmits(["update:modelValue"]);
</script>
<template>

View File

@ -1,27 +1,21 @@
<script setup lang="ts">
import {Menu, MenuButton, MenuItems, MenuItem} from '@headlessui/vue'
import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/vue";
const props = defineProps<{
buttonClasses: [string],
listItems: [
[object]
],
align: 'left' | 'right'
}>()
buttonClasses: [string];
listItems: [[object]];
align: "left" | "right";
}>();
const emit = defineEmits<{
(e: 'select', data: object): void
}>()
(e: "select", data: object): void;
}>();
</script>
<template>
<Menu as="div" class="relative inline-block text-left">
<div>
<MenuButton
:class="buttonClasses"
>
<MenuButton :class="buttonClasses">
<slot></slot>
</MenuButton>
</div>
@ -38,20 +32,17 @@ const emit = defineEmits<{
class="absolute mt-2 px-6 w-56 origin-top-right divide-y divide-gray-500 bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
:class="[align === 'left' ? 'left-0' : 'right-0']"
>
<div class="" v-for="section in listItems" :key="section">
<div class="px-1 py-1" v-for="item in section" :key="item">
<div v-for="section in listItems" :key="section" class="">
<div v-for="item in section" :key="item" class="px-1 py-1">
<MenuItem>
<button
@click="$emit('select', item.data)"
class="text-black group flex w-full items-center px-0 py-2 text-sm"
@click="$emit('select', item.data)"
>
<span class="inline-block pr-2">
<component
v-if="item.icon"
:is="item.icon"
></component>
<component :is="item.icon" v-if="item.icon"></component>
</span>
{{item.title}}
{{ item.title }}
</button>
</MenuItem>
</div>

View File

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

View File

@ -1,31 +1,31 @@
<script setup lang="ts">
// inspiration https://vuejs.org/examples/#modal
import {onMounted, watch} from "vue";
import { onMounted, watch } from "vue";
const props = defineProps<{
show: boolean
}>()
show: boolean;
}>();
const emits = defineEmits(['closemodal'])
const emits = defineEmits(["closemodal"]);
let appElement: HTMLElement | null = null;
watch(() => props.show,
(isShown) => isShown && appElement ? appElement.classList.add('no-scroll') : null
)
watch(
() => props.show,
(isShown) => (isShown && appElement ? appElement.classList.add("no-scroll") : null)
);
onMounted(() => {
appElement = document.getElementById('app');
})
appElement = document.getElementById("app");
});
const closeModal = () => {
if (appElement) {
appElement.classList.remove('no-scroll')
appElement.classList.remove("no-scroll");
}
emits('closemodal')
}
emits("closemodal");
};
</script>
<template>
@ -33,7 +33,8 @@ const closeModal = () => {
<div
v-if="show"
data-cy="full-screen-modal"
class="px-4 py-16 lg:px-16 lg:py-24 fixed top-0 overflow-y-scroll bg-white h-full w-full">
class="px-4 py-16 lg:px-16 lg:py-24 fixed top-0 overflow-y-scroll bg-white h-full w-full"
>
<button
type="button"
class="w-8 h-8 absolute right-4 top-4 cursor-pointer"

View File

@ -1,16 +1,16 @@
import {getCookieValue} from '@/router/guards';
import { getCookieValue } from "@/router/guards";
class FetchError extends Error {
response: Response;
constructor(response: Response, message = 'HTTP error ' + response.status) {
constructor(response: Response, message = "HTTP error " + response.status) {
super(message);
this.response = response;
}
}
export const itFetch = (url: RequestInfo, options: RequestInit) => {
return fetch(url, options).then(response => {
return fetch(url, options).then((response) => {
if (!response.ok) {
throw new FetchError(response);
}
@ -19,33 +19,35 @@ export const itFetch = (url: RequestInfo, options: RequestInit) => {
});
};
export const itPost = (
url: RequestInfo,
data: unknown,
options: RequestInit = {},
) => {
export const itPost = (url: RequestInfo, data: unknown, options: RequestInit = {}) => {
options = Object.assign({}, options);
const headers = Object.assign({
Accept: 'application/json',
'Content-Type': 'application/json;charset=UTF-8',
}, options?.headers);
const headers = Object.assign(
{
Accept: "application/json",
"Content-Type": "application/json;charset=UTF-8",
},
options?.headers
);
if (options?.headers) {
delete options.headers;
}
options = Object.assign({
method: 'POST',
headers: headers,
body: JSON.stringify(data)
}, options);
options = Object.assign(
{
method: "POST",
headers: headers,
body: JSON.stringify(data),
},
options
);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
options.headers['X-CSRFToken'] = getCookieValue('csrftoken');
options.headers["X-CSRFToken"] = getCookieValue("csrftoken");
if (options.method === 'GET') {
if (options.method === "GET") {
delete options.body;
}
@ -57,5 +59,5 @@ export const itPost = (
};
export const itGet = (url: RequestInfo) => {
return itPost(url, {}, {method: 'GET'});
return itPost(url, {}, { method: "GET" });
};

View File

@ -1,20 +1,20 @@
import { nextTick } from 'vue'
import { createI18n } from 'vue-i18n'
import { nextTick } from "vue";
import { createI18n } from "vue-i18n";
// https://vue-i18n.intlify.dev/guide/advanced/lazy.html
export const SUPPORT_LOCALES = ['de', 'fr', 'it']
export const SUPPORT_LOCALES = ["de", "fr", "it"];
export function setupI18n(options = { locale: 'de' }) {
const i18n = createI18n(options)
setI18nLanguage(i18n, options.locale)
return i18n
export function setupI18n(options = { locale: "de" }) {
const i18n = createI18n(options);
setI18nLanguage(i18n, options.locale);
return i18n;
}
export function setI18nLanguage(i18n: any, locale: string) {
if (i18n.mode === 'legacy') {
i18n.global.locale = locale
if (i18n.mode === "legacy") {
i18n.global.locale = locale;
} else {
i18n.global.locale.value = locale
i18n.global.locale.value = locale;
}
/**
* NOTE:
@ -23,17 +23,17 @@ export function setI18nLanguage(i18n: any, locale: string) {
*
* axios.defaults.headers.common['Accept-Language'] = locale
*/
document.querySelector('html').setAttribute('lang', locale)
document.querySelector("html").setAttribute("lang", locale);
}
export async function loadLocaleMessages(i18n: any, locale: any) {
// load locale messages with dynamic import
const messages = await import(
/* webpackChunkName: "locale-[request]" */ `./locales/${locale}.json`
)
);
// set locale and locale message
i18n.global.setLocaleMessage(locale, messages.default)
i18n.global.setLocaleMessage(locale, messages.default);
return nextTick()
return nextTick();
}

View File

@ -1,40 +1,38 @@
import { createApp, markRaw } from 'vue'
import { createPinia } from 'pinia'
import * as log from 'loglevel'
import * as log from "loglevel";
import { createPinia } from "pinia";
import { createApp, markRaw } from "vue";
// import {setupI18n} from './i18n'
import App from './App.vue'
import router from './router'
import App from "./App.vue";
import router from "./router";
import '../tailwind.css'
import type { Router } from 'vue-router'
import type { Router } from "vue-router";
import "../tailwind.css";
if (window.location.href.indexOf('localhost') >= 0) {
log.setLevel('trace')
if (window.location.href.indexOf("localhost") >= 0) {
log.setLevel("trace");
} else {
log.setLevel('warn')
log.setLevel("warn");
}
// const i18n = setupI18n()
const app = createApp(App)
const app = createApp(App);
// todo: define lang setup
// await loadLocaleMessages(i18n, 'de')
app.use(router)
app.use(router);
declare module 'pinia' {
declare module "pinia" {
export interface PiniaCustomProperties {
router: Router
router: Router;
}
}
const pinia = createPinia();
pinia.use(({ store }) => {
store.router = markRaw(router)
})
app.use(pinia)
store.router = markRaw(router);
});
app.use(pinia);
// app.use(i18n)
app.mount('#app')
app.mount("#app");

View File

@ -1,32 +1,37 @@
import type { NavigationGuardWithThis, RouteLocationNormalized } from 'vue-router'
import { useUserStore } from '@/stores/user'
import { useUserStore } from "@/stores/user";
import type { NavigationGuardWithThis, RouteLocationNormalized } from "vue-router";
export const updateLoggedIn: NavigationGuardWithThis<undefined> = (_to) => {
const loggedIn = getCookieValue('loginStatus') === 'true'
const userStore = useUserStore()
const loggedIn = getCookieValue("loginStatus") === "true";
const userStore = useUserStore();
userStore.$patch({ loggedIn })
userStore.$patch({ loggedIn });
if (loggedIn && !userStore.email) {
userStore.fetchUser()
userStore.fetchUser();
}
}
};
export const redirectToLoginIfRequired: NavigationGuardWithThis<undefined> = (to, _from) => {
const userStore = useUserStore()
if(loginRequired(to) && !userStore.loggedIn) {
return `/login?next=${to.fullPath}`
export const redirectToLoginIfRequired: NavigationGuardWithThis<undefined> = (
to,
_from
) => {
const userStore = useUserStore();
if (loginRequired(to) && !userStore.loggedIn) {
return `/login?next=${to.fullPath}`;
}
}
};
export const getCookieValue = (cookieName: string): string => {
// https://stackoverflow.com/questions/5639346/what-is-the-shortest-function-for-reading-a-cookie-by-name-in-javascript
const cookieValue = document.cookie.match('(^|[^;]+)\\s*' + cookieName + '\\s*=\\s*([^;]+)')
const cookieValue = document.cookie.match(
"(^|[^;]+)\\s*" + cookieName + "\\s*=\\s*([^;]+)"
);
if (!cookieValue) {
return ''
return "";
}
return cookieValue.pop() || '';
}
return cookieValue.pop() || "";
};
const loginRequired = (to: RouteLocationNormalized) => {
return !to.meta?.public
}
return !to.meta?.public;
};

View File

@ -1,14 +1,14 @@
import { createRouter, createWebHistory } from 'vue-router'
import CockpitView from '@/views/CockpitView.vue'
import LoginView from '@/views/LoginView.vue'
import { redirectToLoginIfRequired, updateLoggedIn } from '@/router/guards'
import { useAppStore } from '@/stores/app'
import { redirectToLoginIfRequired, updateLoggedIn } from "@/router/guards";
import { useAppStore } from "@/stores/app";
import CockpitView from "@/views/CockpitView.vue";
import LoginView from "@/views/LoginView.vue";
import { createRouter, createWebHistory } from "vue-router";
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/login',
path: "/login",
component: LoginView,
meta: {
// no login required -> so `public === true`
@ -16,86 +16,86 @@ const router = createRouter({
},
},
{
path: '/',
name: 'home',
path: "/",
name: "home",
component: CockpitView,
},
{
path: '/shop',
component: () => import('@/views/ShopView.vue'),
path: "/shop",
component: () => import("@/views/ShopView.vue"),
},
{
path: '/mediacenter/:mediaCenterPageSlug',
path: "/mediacenter/:mediaCenterPageSlug",
props: true,
component: () => import('@/views/MediaCenterView.vue'),
component: () => import("@/views/MediaCenterView.vue"),
children: [
{
path: 'overview',
component: () => import('@/views/MediaCenterMainView.vue'),
path: "overview",
component: () => import("@/views/MediaCenterMainView.vue"),
},
{
path: 'handlungsfelder/:mediaCategorySlug',
path: "handlungsfelder/:mediaCategorySlug",
props: true,
component: () => import('@/views/MediaCategoryDetailView.vue'),
component: () => import("@/views/MediaCategoryDetailView.vue"),
},
{
path: 'handlungsfelder',
component: () => import('@/views/MediaCenterCategoryOverview.vue'),
path: "handlungsfelder",
component: () => import("@/views/MediaCenterCategoryOverview.vue"),
},
{
path: 'handlungsfeldlist',
component: () => import('@/views/MediaList.vue'),
path: "handlungsfeldlist",
component: () => import("@/views/MediaList.vue"),
},
],
},
{
path: '/messages',
component: () => import('@/views/MessagesView.vue'),
path: "/messages",
component: () => import("@/views/MessagesView.vue"),
},
{
path: '/profile',
component: () => import('@/views/ProfileView.vue'),
path: "/profile",
component: () => import("@/views/ProfileView.vue"),
},
{
path: '/learn/:learningPathSlug',
component: () => import('../views/LearningPathView.vue'),
path: "/learn/:learningPathSlug",
component: () => import("../views/LearningPathView.vue"),
props: true,
},
{
path: '/learn/:learningPathSlug/:circleSlug',
component: () => import('../views/CircleView.vue'),
path: "/learn/:learningPathSlug/:circleSlug",
component: () => import("../views/CircleView.vue"),
props: true,
},
{
path: '/learn/:learningPathSlug/:circleSlug/evaluate/:learningUnitSlug',
component: () => import('../views/LearningUnitSelfEvaluationView.vue'),
path: "/learn/:learningPathSlug/:circleSlug/evaluate/:learningUnitSlug",
component: () => import("../views/LearningUnitSelfEvaluationView.vue"),
props: true,
},
{
path: '/learn/:learningPathSlug/:circleSlug/:contentSlug',
component: () => import('../views/LearningContentView.vue'),
path: "/learn/:learningPathSlug/:circleSlug/:contentSlug",
component: () => import("../views/LearningContentView.vue"),
props: true,
},
{
path: '/styleguide',
component: () => import('../views/StyleGuideView.vue'),
path: "/styleguide",
component: () => import("../views/StyleGuideView.vue"),
meta: {
public: true,
},
},
{
path: '/:pathMatch(.*)*',
component: () => import('../views/404View.vue'),
path: "/:pathMatch(.*)*",
component: () => import("../views/404View.vue"),
},
],
})
});
router.beforeEach(updateLoggedIn)
router.beforeEach(redirectToLoginIfRequired)
router.beforeEach(updateLoggedIn);
router.beforeEach(redirectToLoginIfRequired);
router.afterEach((to, from) => {
const appStore = useAppStore();
appStore.routingFinished = true;
});
export default router
export default router;

View File

@ -1,12 +1,14 @@
import { describe, it } from 'vitest'
import data from './learning_path_json.json'
import { Circle } from '../circle'
import { describe, it } from "vitest";
import { Circle } from "../circle";
import data from "./learning_path_json.json";
describe('Circle.parseJson', () => {
it('can parse circle from api response', () => {
const cirleData = data.children.find((c) => c.slug === 'test-lehrgang-lp-circle-analyse')
const circle = Circle.fromJson(cirleData, undefined)
expect(circle.learningSequences.length).toBe(3)
expect(circle.flatLearningContents.length).toBe(7)
})
})
describe("Circle.parseJson", () => {
it("can parse circle from api response", () => {
const cirleData = data.children.find(
(c) => c.slug === "test-lehrgang-lp-circle-analyse"
);
const circle = Circle.fromJson(cirleData, undefined);
expect(circle.learningSequences.length).toBe(3);
expect(circle.flatLearningContents.length).toBe(7);
});
});

View File

@ -1,15 +1,15 @@
import { describe, it } from 'vitest'
import data from './learning_path_json.json'
import { LearningPath } from '../learningPath'
import { describe, it } from "vitest";
import { LearningPath } from "../learningPath";
import data from "./learning_path_json.json";
describe('LearningPath.parseJson', () => {
it('can parse learning sequences from api response', () => {
const learningPath = LearningPath.fromJson(data, [])
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('Analyse')
expect(learningPath.circles.length).toBe(2);
expect(learningPath.circles[0].title).toBe("Basis");
expect(learningPath.circles[1].title).toBe("Analyse");
expect(learningPath.topics.length).toBe(2)
})
})
expect(learningPath.topics.length).toBe(2);
});
});

View File

@ -1,344 +1,344 @@
{
"id": 372,
"title": "Test Lernpfad",
"slug": "test-lehrgang-lp",
"type": "learnpath.LearningPath",
"translation_key": "42e559ca-970f-4a08-9e5e-63860585ee1e",
"children": [
"id": 372,
"title": "Test Lernpfad",
"slug": "test-lehrgang-lp",
"type": "learnpath.LearningPath",
"translation_key": "42e559ca-970f-4a08-9e5e-63860585ee1e",
"children": [
{
"id": 373,
"title": "Basis",
"slug": "test-lehrgang-lp-topic-basis",
"type": "learnpath.Topic",
"translation_key": "d68c1544-cf22-4a59-a81c-8cb977440cd0",
"is_visible": false
},
{
"id": 374,
"title": "Basis",
"slug": "test-lehrgang-lp-circle-basis",
"type": "learnpath.Circle",
"translation_key": "ec62a2af-6f74-4031-b971-c3287bbbc573",
"children": [
{
"id": 373,
"title": "Basis",
"slug": "test-lehrgang-lp-topic-basis",
"type": "learnpath.Topic",
"translation_key": "d68c1544-cf22-4a59-a81c-8cb977440cd0",
"is_visible": false
"id": 375,
"title": "Starten",
"slug": "test-lehrgang-lp-circle-basis-ls-starten",
"type": "learnpath.LearningSequence",
"translation_key": "c5fdada9-036d-4516-a50f-6656a1c6b009",
"icon": "it-icon-ls-start"
},
{
"id": 374,
"title": "Basis",
"slug": "test-lehrgang-lp-circle-basis",
"type": "learnpath.Circle",
"translation_key": "ec62a2af-6f74-4031-b971-c3287bbbc573",
"children": [
{
"id": 375,
"title": "Starten",
"slug": "test-lehrgang-lp-circle-basis-ls-starten",
"type": "learnpath.LearningSequence",
"translation_key": "c5fdada9-036d-4516-a50f-6656a1c6b009",
"icon": "it-icon-ls-start"
},
{
"id": 376,
"title": "Einf\u00fchrung",
"slug": "test-lehrgang-lp-circle-basis-lc-einf\u00fchrung",
"type": "learnpath.LearningContent",
"translation_key": "01de5131-28ce-4b1f-805f-8643384bfd6b",
"minutes": 15,
"contents": [
{
"type": "document",
"value": {
"description": "Beispiel Dokument",
"url": null
},
"id": "bd05f721-3e9d-4a11-8fe2-7c04e2365f52"
}
]
},
{
"id": 377,
"title": "Beenden",
"slug": "test-lehrgang-lp-circle-basis-ls-beenden",
"type": "learnpath.LearningSequence",
"translation_key": "128c0162-025f-41be-9842-60016a77cdbc",
"icon": "it-icon-ls-end"
},
{
"id": 378,
"title": "Jetzt kann es losgehen!",
"slug": "test-lehrgang-lp-circle-basis-lc-jetzt-kann-es-losgehen",
"type": "learnpath.LearningContent",
"translation_key": "271896b9-6082-4fd4-9d70-6093ec9cc6ea",
"minutes": 30,
"contents": [
{
"type": "document",
"value": {
"description": "Beispiel Dokument",
"url": null
},
"id": "204fc13b-a9ae-40de-8e09-f1e922c4fdd9"
}
]
}
],
"description": "Basis",
"job_situations": [],
"goals": [],
"experts": []
"id": 376,
"title": "Einf\u00fchrung",
"slug": "test-lehrgang-lp-circle-basis-lc-einf\u00fchrung",
"type": "learnpath.LearningContent",
"translation_key": "01de5131-28ce-4b1f-805f-8643384bfd6b",
"minutes": 15,
"contents": [
{
"type": "document",
"value": {
"description": "Beispiel Dokument",
"url": null
},
"id": "bd05f721-3e9d-4a11-8fe2-7c04e2365f52"
}
]
},
{
"id": 379,
"title": "Beraten der Kunden",
"slug": "test-lehrgang-lp-topic-beraten-der-kunden",
"type": "learnpath.Topic",
"translation_key": "91918780-75f8-4db3-8fb8-91b63f08b9b9",
"is_visible": true
"id": 377,
"title": "Beenden",
"slug": "test-lehrgang-lp-circle-basis-ls-beenden",
"type": "learnpath.LearningSequence",
"translation_key": "128c0162-025f-41be-9842-60016a77cdbc",
"icon": "it-icon-ls-end"
},
{
"id": 380,
"title": "Analyse",
"slug": "test-lehrgang-lp-circle-analyse",
"type": "learnpath.Circle",
"translation_key": "50f11be3-a56d-412d-be25-3d272fb5df40",
"children": [
{
"id": 381,
"title": "Starten",
"slug": "test-lehrgang-lp-circle-analyse-ls-starten",
"type": "learnpath.LearningSequence",
"translation_key": "07ac0eb9-3671-4b62-8053-1d0c43a1f0fb",
"icon": "it-icon-ls-start"
},
{
"id": 382,
"title": "Einleitung Circle \"Analyse\"",
"slug": "test-lehrgang-lp-circle-analyse-lc-einleitung-circle-analyse",
"type": "learnpath.LearningContent",
"translation_key": "00ed0ab2-fdb0-4ee6-a7d2-42a219b849a8",
"minutes": 15,
"contents": [
{
"type": "document",
"value": {
"description": "Beispiel Dokument",
"url": null
},
"id": "892a9a4a-8e1e-4f7e-8c35-9bf3bbe5371b"
}
]
},
{
"id": 383,
"title": "Beobachten",
"slug": "test-lehrgang-lp-circle-analyse-ls-beobachten",
"type": "learnpath.LearningSequence",
"translation_key": "4cb08bc2-d101-43cc-b006-8f2bbb1a0579",
"icon": "it-icon-ls-watch"
},
{
"id": 384,
"title": "Fahrzeug",
"slug": "test-lehrgang-lp-circle-analyse-lu-fahrzeug",
"type": "learnpath.LearningUnit",
"translation_key": "8f4afa40-c27e-48f7-a2d7-0e713479a55e",
"course_category": {
"id": 15,
"title": "Fahrzeug",
"general": false
},
"children": [
{
"id": 397,
"title": "Innerhalb des Handlungsfelds \u00abFahrzeug\u00bb bin ich f\u00e4hig, die Ziele und Pl\u00e4ne des Kunden zu ergr\u00fcnden (SOLL).",
"slug": "test-lehrgang-competence-crit-y13-fahrzeug",
"type": "competence.PerformanceCriteria",
"translation_key": "e9d49552-7d18-418a-94b6-ebb4ee6bf187",
"competence_id": "Y1.3"
},
{
"id": 398,
"title": "Innerhalb des Handlungsfelds \u00abFahrzeug\u00bb bin ich f\u00e4hig, die IST-Situation des Kunden mit der geeigneten Gespr\u00e4chs-/Fragetechnik zu erfassen.",
"slug": "test-lehrgang-competence-crit-y21-fahrzeug",
"type": "competence.PerformanceCriteria",
"translation_key": "5f257b35-c6ca-49e4-9401-a5d02d53926d",
"competence_id": "Y2.1"
}
]
},
{
"id": 385,
"title": "Rafael Fasel wechselt sein Auto",
"slug": "test-lehrgang-lp-circle-analyse-lc-rafael-fasel-wechselt-sein-auto",
"type": "learnpath.LearningContent",
"translation_key": "fda4f870-9307-414d-b07f-eea607a9afb7",
"minutes": 30,
"contents": [
{
"type": "online_training",
"value": {
"description": "In diesem Online-Training lernst du, wie du den Kundenbedarf ermittelst.",
"url": ""
},
"id": "700a0f64-0892-4fa5-9e08-3bd34e99edeb"
}
]
},
{
"id": 386,
"title": "Fachcheck Fahrzeug",
"slug": "test-lehrgang-lp-circle-analyse-lc-fachcheck-fahrzeug",
"type": "learnpath.LearningContent",
"translation_key": "dce0847f-4593-4bba-bd0c-a09c71eb0344",
"minutes": 30,
"contents": [
{
"type": "test",
"value": {
"description": "Beispiel Test",
"url": null
},
"id": "9f674aaa-ebf0-4a01-adcc-c0c46394fb10"
}
]
},
{
"id": 387,
"title": "Reisen",
"slug": "test-lehrgang-lp-circle-analyse-lu-reisen",
"type": "learnpath.LearningUnit",
"translation_key": "c3f6d33f-8dbc-4d88-9a81-3c602c4f9cc8",
"course_category": {
"id": 16,
"title": "Reisen",
"general": false
},
"children": [
{
"id": 399,
"title": "Innerhalb des Handlungsfelds \u00abReisen\u00bb bin ich f\u00e4hig, die Ziele und Pl\u00e4ne des Kunden zu ergr\u00fcnden (SOLL).",
"slug": "test-lehrgang-competence-crit-y13-reisen",
"type": "competence.PerformanceCriteria",
"translation_key": "1e488b69-8a3e-4acc-9547-48c103e0d038",
"competence_id": "Y1.3"
}
]
},
{
"id": 388,
"title": "Reiseversicherung",
"slug": "test-lehrgang-lp-circle-analyse-lc-reiseversicherung",
"type": "learnpath.LearningContent",
"translation_key": "ff513aae-efe1-4974-b67f-7a292b8aef86",
"minutes": 240,
"contents": [
{
"type": "exercise",
"value": {
"description": "Beispiel \u00dcbung",
"url": null
},
"id": "f35f213e-1a33-49fe-97c5-26e15161719f"
}
]
},
{
"id": 389,
"title": "Emma und Ayla campen durch Amerika",
"slug": "test-lehrgang-lp-circle-analyse-lc-emma-und-ayla-campen-durch-amerika",
"type": "learnpath.LearningContent",
"translation_key": "a77b0f9d-9a70-47bd-8e62-7580d70a4306",
"minutes": 120,
"contents": [
{
"type": "exercise",
"value": {
"description": "Beispiel \u00dcbung",
"url": "/static/media/web_based_trainings/story-06-a-01-emma-und-ayla-campen-durch-amerika-einstieg/scormcontent/index.html"
},
"id": "60f087ff-fa3a-4da2-820f-4fcdf449f70d"
}
]
},
{
"id": 390,
"title": "Beenden",
"slug": "test-lehrgang-lp-circle-analyse-ls-beenden",
"type": "learnpath.LearningSequence",
"translation_key": "06f1e998-b827-41cc-8129-d72d731719c1",
"icon": "it-icon-ls-end"
},
{
"id": 391,
"title": "Kompetenzprofil anschauen",
"slug": "test-lehrgang-lp-circle-analyse-lc-kompetenzprofil-anschauen",
"type": "learnpath.LearningContent",
"translation_key": "6cc47dc1-a74f-4cbf-afa6-23885891c82f",
"minutes": 30,
"contents": [
{
"type": "document",
"value": {
"description": "Beispiel Dokument",
"url": null
},
"id": "3f685055-4e3e-4ca9-93af-bac19236931d"
}
]
},
{
"id": 392,
"title": "Circle \"Analyse\" abschliessen",
"slug": "test-lehrgang-lp-circle-analyse-lc-circle-analyse-abschliessen",
"type": "learnpath.LearningContent",
"translation_key": "9b32e2cd-1368-4885-a79b-906b45ba04bc",
"minutes": 30,
"contents": [
{
"type": "document",
"value": {
"description": "Beispiel Dokument",
"url": null
},
"id": "650b7b15-b522-4df7-ac5b-6a654f12334f"
}
]
}
],
"description": "Unit-Test Circle",
"job_situations": [
{
"type": "job_situation",
"value": "Autoversicherung",
"id": "c5a6b365-0a18-47d5-b6e1-6cb8b8ec7d35"
},
{
"type": "job_situation",
"value": "Autokauf",
"id": "e969d2a2-b383-482c-a721-88552af086a6"
}
],
"goals": [
{
"type": "goal",
"value": "... die heutige Versicherungssituation von Privat- oder Gesch\u00e4ftskunden einzusch\u00e4tzen.",
"id": "d9ad8aed-d7d6-42c7-b6d4-65102c8ddf10"
},
{
"type": "goal",
"value": "... deinem Kunden seine optimale L\u00f6sung aufzuzeigen",
"id": "2506950c-45cb-474f-acb9-45e83e9ebe1b"
}
],
"experts": [
{
"type": "person",
"value": {
"first_name": "Patrizia",
"last_name": "Huggel",
"email": "patrizia.huggel@example.com",
"photo": null,
"biography": ""
},
"id": "b7b0ff2e-f840-4d74-99c1-c7a5ee6dc14e"
}
]
"id": 378,
"title": "Jetzt kann es losgehen!",
"slug": "test-lehrgang-lp-circle-basis-lc-jetzt-kann-es-losgehen",
"type": "learnpath.LearningContent",
"translation_key": "271896b9-6082-4fd4-9d70-6093ec9cc6ea",
"minutes": 30,
"contents": [
{
"type": "document",
"value": {
"description": "Beispiel Dokument",
"url": null
},
"id": "204fc13b-a9ae-40de-8e09-f1e922c4fdd9"
}
]
}
],
"course": {
"id": -1,
"title": "Test Lerngang",
"category_name": "Handlungsfeld"
],
"description": "Basis",
"job_situations": [],
"goals": [],
"experts": []
},
{
"id": 379,
"title": "Beraten der Kunden",
"slug": "test-lehrgang-lp-topic-beraten-der-kunden",
"type": "learnpath.Topic",
"translation_key": "91918780-75f8-4db3-8fb8-91b63f08b9b9",
"is_visible": true
},
{
"id": 380,
"title": "Analyse",
"slug": "test-lehrgang-lp-circle-analyse",
"type": "learnpath.Circle",
"translation_key": "50f11be3-a56d-412d-be25-3d272fb5df40",
"children": [
{
"id": 381,
"title": "Starten",
"slug": "test-lehrgang-lp-circle-analyse-ls-starten",
"type": "learnpath.LearningSequence",
"translation_key": "07ac0eb9-3671-4b62-8053-1d0c43a1f0fb",
"icon": "it-icon-ls-start"
},
{
"id": 382,
"title": "Einleitung Circle \"Analyse\"",
"slug": "test-lehrgang-lp-circle-analyse-lc-einleitung-circle-analyse",
"type": "learnpath.LearningContent",
"translation_key": "00ed0ab2-fdb0-4ee6-a7d2-42a219b849a8",
"minutes": 15,
"contents": [
{
"type": "document",
"value": {
"description": "Beispiel Dokument",
"url": null
},
"id": "892a9a4a-8e1e-4f7e-8c35-9bf3bbe5371b"
}
]
},
{
"id": 383,
"title": "Beobachten",
"slug": "test-lehrgang-lp-circle-analyse-ls-beobachten",
"type": "learnpath.LearningSequence",
"translation_key": "4cb08bc2-d101-43cc-b006-8f2bbb1a0579",
"icon": "it-icon-ls-watch"
},
{
"id": 384,
"title": "Fahrzeug",
"slug": "test-lehrgang-lp-circle-analyse-lu-fahrzeug",
"type": "learnpath.LearningUnit",
"translation_key": "8f4afa40-c27e-48f7-a2d7-0e713479a55e",
"course_category": {
"id": 15,
"title": "Fahrzeug",
"general": false
},
"children": [
{
"id": 397,
"title": "Innerhalb des Handlungsfelds \u00abFahrzeug\u00bb bin ich f\u00e4hig, die Ziele und Pl\u00e4ne des Kunden zu ergr\u00fcnden (SOLL).",
"slug": "test-lehrgang-competence-crit-y13-fahrzeug",
"type": "competence.PerformanceCriteria",
"translation_key": "e9d49552-7d18-418a-94b6-ebb4ee6bf187",
"competence_id": "Y1.3"
},
{
"id": 398,
"title": "Innerhalb des Handlungsfelds \u00abFahrzeug\u00bb bin ich f\u00e4hig, die IST-Situation des Kunden mit der geeigneten Gespr\u00e4chs-/Fragetechnik zu erfassen.",
"slug": "test-lehrgang-competence-crit-y21-fahrzeug",
"type": "competence.PerformanceCriteria",
"translation_key": "5f257b35-c6ca-49e4-9401-a5d02d53926d",
"competence_id": "Y2.1"
}
]
},
{
"id": 385,
"title": "Rafael Fasel wechselt sein Auto",
"slug": "test-lehrgang-lp-circle-analyse-lc-rafael-fasel-wechselt-sein-auto",
"type": "learnpath.LearningContent",
"translation_key": "fda4f870-9307-414d-b07f-eea607a9afb7",
"minutes": 30,
"contents": [
{
"type": "online_training",
"value": {
"description": "In diesem Online-Training lernst du, wie du den Kundenbedarf ermittelst.",
"url": ""
},
"id": "700a0f64-0892-4fa5-9e08-3bd34e99edeb"
}
]
},
{
"id": 386,
"title": "Fachcheck Fahrzeug",
"slug": "test-lehrgang-lp-circle-analyse-lc-fachcheck-fahrzeug",
"type": "learnpath.LearningContent",
"translation_key": "dce0847f-4593-4bba-bd0c-a09c71eb0344",
"minutes": 30,
"contents": [
{
"type": "test",
"value": {
"description": "Beispiel Test",
"url": null
},
"id": "9f674aaa-ebf0-4a01-adcc-c0c46394fb10"
}
]
},
{
"id": 387,
"title": "Reisen",
"slug": "test-lehrgang-lp-circle-analyse-lu-reisen",
"type": "learnpath.LearningUnit",
"translation_key": "c3f6d33f-8dbc-4d88-9a81-3c602c4f9cc8",
"course_category": {
"id": 16,
"title": "Reisen",
"general": false
},
"children": [
{
"id": 399,
"title": "Innerhalb des Handlungsfelds \u00abReisen\u00bb bin ich f\u00e4hig, die Ziele und Pl\u00e4ne des Kunden zu ergr\u00fcnden (SOLL).",
"slug": "test-lehrgang-competence-crit-y13-reisen",
"type": "competence.PerformanceCriteria",
"translation_key": "1e488b69-8a3e-4acc-9547-48c103e0d038",
"competence_id": "Y1.3"
}
]
},
{
"id": 388,
"title": "Reiseversicherung",
"slug": "test-lehrgang-lp-circle-analyse-lc-reiseversicherung",
"type": "learnpath.LearningContent",
"translation_key": "ff513aae-efe1-4974-b67f-7a292b8aef86",
"minutes": 240,
"contents": [
{
"type": "exercise",
"value": {
"description": "Beispiel \u00dcbung",
"url": null
},
"id": "f35f213e-1a33-49fe-97c5-26e15161719f"
}
]
},
{
"id": 389,
"title": "Emma und Ayla campen durch Amerika",
"slug": "test-lehrgang-lp-circle-analyse-lc-emma-und-ayla-campen-durch-amerika",
"type": "learnpath.LearningContent",
"translation_key": "a77b0f9d-9a70-47bd-8e62-7580d70a4306",
"minutes": 120,
"contents": [
{
"type": "exercise",
"value": {
"description": "Beispiel \u00dcbung",
"url": "/static/media/web_based_trainings/story-06-a-01-emma-und-ayla-campen-durch-amerika-einstieg/scormcontent/index.html"
},
"id": "60f087ff-fa3a-4da2-820f-4fcdf449f70d"
}
]
},
{
"id": 390,
"title": "Beenden",
"slug": "test-lehrgang-lp-circle-analyse-ls-beenden",
"type": "learnpath.LearningSequence",
"translation_key": "06f1e998-b827-41cc-8129-d72d731719c1",
"icon": "it-icon-ls-end"
},
{
"id": 391,
"title": "Kompetenzprofil anschauen",
"slug": "test-lehrgang-lp-circle-analyse-lc-kompetenzprofil-anschauen",
"type": "learnpath.LearningContent",
"translation_key": "6cc47dc1-a74f-4cbf-afa6-23885891c82f",
"minutes": 30,
"contents": [
{
"type": "document",
"value": {
"description": "Beispiel Dokument",
"url": null
},
"id": "3f685055-4e3e-4ca9-93af-bac19236931d"
}
]
},
{
"id": 392,
"title": "Circle \"Analyse\" abschliessen",
"slug": "test-lehrgang-lp-circle-analyse-lc-circle-analyse-abschliessen",
"type": "learnpath.LearningContent",
"translation_key": "9b32e2cd-1368-4885-a79b-906b45ba04bc",
"minutes": 30,
"contents": [
{
"type": "document",
"value": {
"description": "Beispiel Dokument",
"url": null
},
"id": "650b7b15-b522-4df7-ac5b-6a654f12334f"
}
]
}
],
"description": "Unit-Test Circle",
"job_situations": [
{
"type": "job_situation",
"value": "Autoversicherung",
"id": "c5a6b365-0a18-47d5-b6e1-6cb8b8ec7d35"
},
{
"type": "job_situation",
"value": "Autokauf",
"id": "e969d2a2-b383-482c-a721-88552af086a6"
}
],
"goals": [
{
"type": "goal",
"value": "... die heutige Versicherungssituation von Privat- oder Gesch\u00e4ftskunden einzusch\u00e4tzen.",
"id": "d9ad8aed-d7d6-42c7-b6d4-65102c8ddf10"
},
{
"type": "goal",
"value": "... deinem Kunden seine optimale L\u00f6sung aufzuzeigen",
"id": "2506950c-45cb-474f-acb9-45e83e9ebe1b"
}
],
"experts": [
{
"type": "person",
"value": {
"first_name": "Patrizia",
"last_name": "Huggel",
"email": "patrizia.huggel@example.com",
"photo": null,
"biography": ""
},
"id": "b7b0ff2e-f840-4d74-99c1-c7a5ee6dc14e"
}
]
}
}
],
"course": {
"id": -1,
"title": "Test Lerngang",
"category_name": "Handlungsfeld"
}
}

View File

@ -1,3 +1,4 @@
import type { LearningPath } from "@/services/learningPath";
import type {
CircleChild,
CircleGoal,
@ -9,34 +10,38 @@ import type {
LearningSequence,
LearningUnit,
LearningUnitQuestion,
} from '@/types'
import type { LearningPath } from '@/services/learningPath'
} from "@/types";
function _createEmptyLearningUnit(parentLearningSequence: LearningSequence): LearningUnit {
function _createEmptyLearningUnit(
parentLearningSequence: LearningSequence
): LearningUnit {
return {
id: 0,
title: '',
slug: '',
translation_key: '',
type: 'learnpath.LearningUnit',
title: "",
slug: "",
translation_key: "",
type: "learnpath.LearningUnit",
learningContents: [],
minutes: 0,
parentLearningSequence: parentLearningSequence,
children: [],
last: true,
completion_status: 'unknown',
}
completion_status: "unknown",
};
}
export function parseLearningSequences (circle: Circle, children: CircleChild[]): LearningSequence[] {
let learningSequence:LearningSequence | undefined;
let learningUnit:LearningUnit | undefined;
let learningContent:LearningContent | undefined;
export function parseLearningSequences(
circle: Circle,
children: CircleChild[]
): LearningSequence[] {
let learningSequence: LearningSequence | undefined;
let learningUnit: LearningUnit | undefined;
let learningContent: LearningContent | undefined;
let previousLearningContent: LearningContent | undefined;
const result:LearningSequence[] = [];
const result: LearningSequence[] = [];
children.forEach((child) => {
if (child.type === 'learnpath.LearningSequence') {
if (child.type === "learnpath.LearningSequence") {
if (learningSequence) {
if (learningUnit) {
learningUnit.last = true;
@ -44,13 +49,13 @@ export function parseLearningSequences (circle: Circle, children: CircleChild[])
}
result.push(learningSequence);
}
learningSequence = Object.assign(child, {learningUnits: []});
learningSequence = Object.assign(child, { learningUnits: [] });
// initialize empty learning unit if there will not come a learning unit next
learningUnit = _createEmptyLearningUnit(learningSequence);
} else if (child.type === 'learnpath.LearningUnit') {
} else if (child.type === "learnpath.LearningUnit") {
if (!learningSequence) {
throw new Error('LearningUnit found before LearningSequence');
throw new Error("LearningUnit found before LearningSequence");
}
if (learningUnit && learningUnit.learningContents.length) {
@ -64,11 +69,11 @@ export function parseLearningSequences (circle: Circle, children: CircleChild[])
c.parentLearningUnit = learningUnit;
c.parentLearningSequence = learningSequence;
return c;
})
}),
});
} else if (child.type === 'learnpath.LearningContent') {
} else if (child.type === "learnpath.LearningContent") {
if (!learningUnit) {
throw new Error('LearningContent found before LearningUnit');
throw new Error("LearningContent found before LearningUnit");
}
previousLearningContent = learningContent;
@ -93,7 +98,9 @@ export function parseLearningSequences (circle: Circle, children: CircleChild[])
(learningSequence as LearningSequence).learningUnits.push(learningUnit);
result.push(learningSequence);
} else {
throw new Error('Finished with LearningContent but there is no LearningSequence and LearningUnit');
throw new Error(
"Finished with LearningContent but there is no LearningSequence and LearningUnit"
);
}
// sum minutes
@ -112,9 +119,9 @@ export function parseLearningSequences (circle: Circle, children: CircleChild[])
}
export class Circle implements CourseWagtailPage {
readonly type = 'learnpath.Circle';
readonly type = "learnpath.Circle";
readonly learningSequences: LearningSequence[];
completion_status: CourseCompletionStatus = 'unknown'
completion_status: CourseCompletionStatus = "unknown";
nextCircle?: Circle;
previousCircle?: Circle;
@ -128,7 +135,7 @@ export class Circle implements CourseWagtailPage {
public children: CircleChild[],
public goals: CircleGoal[],
public job_situations: CircleJobSituation[],
public readonly parentLearningPath?: LearningPath,
public readonly parentLearningPath?: LearningPath
) {
this.learningSequences = parseLearningSequences(this, this.children);
}
@ -144,8 +151,8 @@ export class Circle implements CourseWagtailPage {
json.children,
json.goals,
json.job_situations,
learningPath,
)
learningPath
);
}
public get flatChildren(): (LearningContent | LearningUnitQuestion)[] {
@ -154,7 +161,7 @@ export class Circle implements CourseWagtailPage {
learningSequence.learningUnits.forEach((learningUnit) => {
learningUnit.children.forEach((learningUnitQuestion) => {
result.push(learningUnitQuestion);
})
});
learningUnit.learningContents.forEach((learningContent) => {
result.push(learningContent);
});
@ -187,9 +194,14 @@ export class Circle implements CourseWagtailPage {
public someFinishedInLearningSequence(translationKey: string): boolean {
if (translationKey) {
return this.flatChildren.filter((lc) => {
return lc.completion_status === 'success' && lc.parentLearningSequence?.translation_key === translationKey;
}).length > 0;
return (
this.flatChildren.filter((lc) => {
return (
lc.completion_status === "success" &&
lc.parentLearningSequence?.translation_key === translationKey
);
}).length > 0
);
}
return false;
@ -197,14 +209,17 @@ export class Circle implements CourseWagtailPage {
public allFinishedInLearningSequence(translationKey: string): boolean {
if (translationKey) {
const finishedContents = this.flatChildren.filter((lc) => {
return lc.completion_status === 'success' && lc.parentLearningSequence?.translation_key === translationKey;
const finishedContents = this.flatChildren.filter((lc) => {
return (
lc.completion_status === "success" &&
lc.parentLearningSequence?.translation_key === translationKey
);
}).length;
const totalContents = this.flatChildren.filter((lc) => {
const totalContents = this.flatChildren.filter((lc) => {
return lc.parentLearningSequence?.translation_key === translationKey;
}).length;
return finishedContents === totalContents
return finishedContents === totalContents;
}
return false;
@ -218,7 +233,7 @@ export class Circle implements CourseWagtailPage {
if (pageIndex >= 0) {
page.completion_status = completionData[pageIndex].completion_status;
} else {
page.completion_status = 'unknown';
page.completion_status = "unknown";
}
});
@ -228,7 +243,7 @@ export class Circle implements CourseWagtailPage {
}
public getUrl(): string {
const shortSlug = this.slug.replace(`${this.parentLearningPath?.slug}-circle-`, '')
const shortSlug = this.slug.replace(`${this.parentLearningPath?.slug}-circle-`, "");
return `/learn/${this.parentLearningPath?.slug}/${shortSlug}`;
}
}

View File

@ -1,5 +1,6 @@
import * as _ from 'lodash'
import * as _ from "lodash";
import { Circle } from "@/services/circle";
import type {
CourseCompletion,
CourseCompletionStatus,
@ -7,24 +8,37 @@ import type {
LearningContent,
LearningPathChild,
Topic,
} from '@/types'
import { Circle } from '@/services/circle'
} from "@/types";
function getLastCompleted(courseId: number, completionData: CourseCompletion[]) {
return _.orderBy(completionData, ['updated_at'], 'desc').find((c: CourseCompletion) => {
return c.completion_status === 'success' && c.course === courseId && c.page_type === 'learnpath.LearningContent'
})
return _.orderBy(completionData, ["updated_at"], "desc").find(
(c: CourseCompletion) => {
return (
c.completion_status === "success" &&
c.course === courseId &&
c.page_type === "learnpath.LearningContent"
);
}
);
}
export class LearningPath implements CourseWagtailPage {
readonly type = 'learnpath.LearningPath'
public topics: Topic[]
public circles: Circle[]
public nextLearningContent?: LearningContent
readonly completion_status: CourseCompletionStatus = 'unknown'
readonly type = "learnpath.LearningPath";
public topics: Topic[];
public circles: Circle[];
public nextLearningContent?: LearningContent;
readonly completion_status: CourseCompletionStatus = "unknown";
public static fromJson(json: any, completionData: CourseCompletion[]): LearningPath {
return new LearningPath(json.id, json.slug, json.title, json.translation_key, json.course.id, json.children, completionData)
return new LearningPath(
json.id,
json.slug,
json.title,
json.translation_key,
json.course.id,
json.children,
completionData
);
}
constructor(
@ -37,70 +51,75 @@ export class LearningPath implements CourseWagtailPage {
completionData?: CourseCompletion[]
) {
// parse children
this.topics = []
this.circles = []
this.topics = [];
this.circles = [];
let topic: Topic | undefined
let topic: Topic | undefined;
this.children.forEach((page) => {
if (page.type === 'learnpath.Topic') {
if (page.type === "learnpath.Topic") {
if (topic) {
this.topics.push(topic)
this.topics.push(topic);
}
topic = Object.assign(page, { circles: [] })
topic = Object.assign(page, { circles: [] });
}
if (page.type === 'learnpath.Circle') {
const circle = Circle.fromJson(page, this)
if (page.type === "learnpath.Circle") {
const circle = Circle.fromJson(page, this);
if (completionData) {
circle.parseCompletionData(completionData)
circle.parseCompletionData(completionData);
}
if (topic) {
topic.circles.push(circle)
topic.circles.push(circle);
}
circle.previousCircle = this.circles[this.circles.length - 1]
circle.previousCircle = this.circles[this.circles.length - 1];
if (circle.previousCircle) {
circle.previousCircle.nextCircle = circle
circle.previousCircle.nextCircle = circle;
}
this.circles.push(circle)
this.circles.push(circle);
}
})
});
if (topic) {
this.topics.push(topic)
this.topics.push(topic);
}
if (completionData) {
this.calcNextLearningContent(completionData)
this.calcNextLearningContent(completionData);
}
}
public calcNextLearningContent(completionData: CourseCompletion[]): void {
this.nextLearningContent = undefined
this.nextLearningContent = undefined;
const lastCompletedLearningContent = getLastCompleted(this.courseId, completionData)
const lastCompletedLearningContent = getLastCompleted(
this.courseId,
completionData
);
if (lastCompletedLearningContent) {
const lastCircle = this.circles.find(
(circle) => {
return circle.flatLearningContents.find((learningContent) => learningContent.translation_key === lastCompletedLearningContent.page_key)
}
)
const lastCircle = this.circles.find((circle) => {
return circle.flatLearningContents.find(
(learningContent) =>
learningContent.translation_key === lastCompletedLearningContent.page_key
);
});
if (lastCircle) {
const lastLearningContent = lastCircle.flatLearningContents.find(
(learningContent) => learningContent.translation_key === lastCompletedLearningContent.page_key
)
(learningContent) =>
learningContent.translation_key === lastCompletedLearningContent.page_key
);
if (lastLearningContent && lastLearningContent.nextLearningContent) {
this.nextLearningContent = lastLearningContent.nextLearningContent
this.nextLearningContent = lastLearningContent.nextLearningContent;
} else {
if (lastCircle.nextCircle) {
this.nextLearningContent = lastCircle.nextCircle.flatLearningContents[0]
this.nextLearningContent = lastCircle.nextCircle.flatLearningContents[0];
}
}
}
} else {
if (this.circles[0]) {
this.nextLearningContent = this.circles[0].flatLearningContents[0]
this.nextLearningContent = this.circles[0].flatLearningContents[0];
}
}
}

View File

@ -1,38 +1,37 @@
import { defineStore } from 'pinia'
import { defineStore } from "pinia";
export type AppState = {
userLoaded: boolean
routingFinished: boolean
showMainNavigationBar: boolean
}
userLoaded: boolean;
routingFinished: boolean;
showMainNavigationBar: boolean;
};
const showMainNavigationBarInitialState = () => {
let path = window.location.pathname;
// remove dangling slash
if (path.endsWith('/')) {
if (path.endsWith("/")) {
path = path.slice(0, -1);
}
const numberOfSlashes = (path.match(/\//g) || []).length;
// it should hide main navigation bar when on learning content page
if (path.startsWith('/learn/') && numberOfSlashes >= 4) {
return false
if (path.startsWith("/learn/") && numberOfSlashes >= 4) {
return false;
}
return true;
}
};
export const useAppStore = defineStore({
id: 'app',
state: () => ({
showMainNavigationBar: showMainNavigationBarInitialState(),
userLoaded: false,
routingFinished: false,
} as AppState),
getters: {
},
actions: {
}
})
id: "app",
state: () =>
({
showMainNavigationBar: showMainNavigationBarInitialState(),
userLoaded: false,
routingFinished: false,
} as AppState),
getters: {},
actions: {},
});

View File

@ -1,27 +1,31 @@
import * as log from 'loglevel'
import * as log from "loglevel";
import { defineStore } from 'pinia'
import { defineStore } from "pinia";
import type { CourseCompletionStatus, LearningContent, LearningUnit, LearningUnitQuestion } from '@/types'
import type { Circle } from '@/services/circle'
import { itPost } from '@/fetchHelpers'
import { useLearningPathStore } from '@/stores/learningPath'
import { itPost } from "@/fetchHelpers";
import type { Circle } from "@/services/circle";
import { useLearningPathStore } from "@/stores/learningPath";
import type {
CourseCompletionStatus,
LearningContent,
LearningUnit,
LearningUnitQuestion,
} from "@/types";
export type CircleStoreState = {
circle: Circle | undefined
page: 'INDEX' | 'OVERVIEW'
}
circle: Circle | undefined;
page: "INDEX" | "OVERVIEW";
};
export const useCircleStore = defineStore({
id: 'circle',
id: "circle",
state: () => {
return {
circle: undefined,
page: 'INDEX',
page: "INDEX",
} as CircleStoreState;
},
getters: {
},
getters: {},
actions: {
async loadCircle(learningPathSlug: string, circleSlug: string): Promise<Circle> {
this.circle = undefined;
@ -37,9 +41,13 @@ export const useCircleStore = defineStore({
throw `No circle found with slug: ${circleSlug}`;
}
return this.circle
return this.circle;
},
async loadLearningContent(learningPathSlug: string, circleSlug: string, learningContentSlug: string) {
async loadLearningContent(
learningPathSlug: string,
circleSlug: string,
learningContentSlug: string
) {
const circle = await this.loadCircle(learningPathSlug, circleSlug);
const result = circle.flatLearningContents.find((learningContent) => {
return learningContent.slug.endsWith(learningContentSlug);
@ -49,24 +57,31 @@ export const useCircleStore = defineStore({
throw `No learning content found with slug: ${learningContentSlug}`;
}
return result
return result;
},
async loadSelfEvaluation(learningPathSlug: string, circleSlug: string, learningUnitSlug: string) {
async loadSelfEvaluation(
learningPathSlug: string,
circleSlug: string,
learningUnitSlug: string
) {
const circle = await this.loadCircle(learningPathSlug, circleSlug);
const learningUnit = circle.flatLearningUnits.find((child) => {
return child.slug.endsWith(learningUnitSlug)
return child.slug.endsWith(learningUnitSlug);
});
if (!learningUnit) {
throw `No self evaluation found with slug: ${learningUnitSlug}`;
}
return learningUnit
return learningUnit;
},
async markCompletion(page: LearningContent | LearningUnitQuestion, completion_status:CourseCompletionStatus='success') {
async markCompletion(
page: LearningContent | LearningUnitQuestion,
completion_status: CourseCompletionStatus = "success"
) {
try {
page.completion_status = completion_status;
const completionData = await itPost('/api/course/completion/mark/', {
const completionData = await itPost("/api/course/completion/mark/", {
page_key: page.translation_key,
completion_status: page.completion_status,
});
@ -75,37 +90,37 @@ export const useCircleStore = defineStore({
}
} catch (error) {
log.error(error);
return error
return error;
}
},
openLearningContent(learningContent: LearningContent) {
const shortSlug = learningContent.slug.replace(`${this.circle?.slug}-lc-`, '');
const shortSlug = learningContent.slug.replace(`${this.circle?.slug}-lc-`, "");
this.router.push({
path: `${this.circle?.getUrl()}/${shortSlug}`,
});
},
closeLearningContent() {
this.router.push({
path: `${this.circle?.getUrl()}`
path: `${this.circle?.getUrl()}`,
});
},
openSelfEvaluation(learningUnit: LearningUnit) {
const shortSlug = learningUnit.slug.replace(`${this.circle?.slug}-lu-`, '');
const shortSlug = learningUnit.slug.replace(`${this.circle?.slug}-lu-`, "");
this.router.push({
path: `${this.circle?.getUrl()}/evaluate/${shortSlug}`,
});
},
closeSelfEvaluation() {
this.router.push({
path: `${this.circle?.getUrl()}`
path: `${this.circle?.getUrl()}`,
});
},
calcSelfEvaluationStatus(learningUnit: LearningUnit) {
if (learningUnit.children.length > 0) {
if (learningUnit.children.every((q) => q.completion_status === 'success')) {
if (learningUnit.children.every((q) => q.completion_status === "success")) {
return true;
}
if (learningUnit.children.some((q) => q.completion_status === 'fail')) {
if (learningUnit.children.some((q) => q.completion_status === "fail")) {
return false;
}
}
@ -113,14 +128,15 @@ export const useCircleStore = defineStore({
},
continueFromLearningContent(currentLearningContent: LearningContent) {
if (currentLearningContent) {
this.markCompletion(currentLearningContent, 'success');
this.markCompletion(currentLearningContent, "success");
const nextLearningContent = currentLearningContent.nextLearningContent;
const currentParent = currentLearningContent.parentLearningUnit;
const nextParent = nextLearningContent?.parentLearningUnit;
if (
currentParent && currentParent.id &&
currentParent &&
currentParent.id &&
currentParent.id !== nextParent?.id &&
currentParent.children.length > 0
) {
@ -130,7 +146,8 @@ export const useCircleStore = defineStore({
} else if (currentLearningContent.nextLearningContent) {
if (
currentLearningContent.parentLearningSequence &&
currentLearningContent.parentLearningSequence.id === nextLearningContent?.parentLearningSequence?.id
currentLearningContent.parentLearningSequence.id ===
nextLearningContent?.parentLearningSequence?.id
) {
this.openLearningContent(currentLearningContent.nextLearningContent);
} else {
@ -140,11 +157,11 @@ export const useCircleStore = defineStore({
this.closeLearningContent();
}
} else {
log.error('currentLearningContent is undefined');
log.error("currentLearningContent is undefined");
}
},
continueFromSelfEvaluation() {
this.closeSelfEvaluation()
this.closeSelfEvaluation();
// if (this.currentSelfEvaluation) {
// const nextContent = this.currentSelfEvaluation.learningContents[this.currentSelfEvaluation.learningContents.length - 1].nextLearningContent;
//
@ -160,6 +177,6 @@ export const useCircleStore = defineStore({
// } else {
// log.error('currentSelfEvaluation is undefined');
// }
}
}
})
},
},
});

View File

@ -1,18 +1,18 @@
import { defineStore } from 'pinia'
import { itGet } from '@/fetchHelpers'
import { LearningPath } from '@/services/learningPath'
import { itGet } from "@/fetchHelpers";
import { LearningPath } from "@/services/learningPath";
import { defineStore } from "pinia";
export type LearningPathStoreState = {
learningPath: LearningPath | undefined
page: 'INDEX' | 'OVERVIEW'
}
learningPath: LearningPath | undefined;
page: "INDEX" | "OVERVIEW";
};
export const useLearningPathStore = defineStore({
id: 'learningPath',
id: "learningPath",
state: () => {
return {
learningPath: undefined,
page: 'INDEX',
page: "INDEX",
} as LearningPathStoreState;
},
getters: {},
@ -22,7 +22,9 @@ export const useLearningPathStore = defineStore({
return this.learningPath;
}
const learningPathData = await itGet(`/api/course/page/${slug}/`);
const completionData = await itGet(`/api/course/completion/${learningPathData.course.id}/`);
const completionData = await itGet(
`/api/course/completion/${learningPathData.course.id}/`
);
if (!learningPathData) {
throw `No learning path found with: ${slug}`;
@ -31,5 +33,5 @@ export const useLearningPathStore = defineStore({
this.learningPath = LearningPath.fromJson(learningPathData, completionData);
return this.learningPath;
},
}
})
},
});

View File

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

View File

@ -1,70 +1,73 @@
import * as log from "loglevel";
import { defineStore } from "pinia";
import { itGet, itPost } from "@/fetchHelpers";
import { useAppStore } from "@/stores/app";
import { defineStore } from "pinia";
// typed state https://stackoverflow.com/questions/71012513/when-using-pinia-and-typescript-how-do-you-use-an-action-to-set-the-state
export type UserState = {
first_name: string,
last_name: string,
first_name: string;
last_name: string;
email: string;
username: string,
avatar_url: string,
username: string;
avatar_url: string;
loggedIn: boolean;
}
};
const initialUserState: UserState = {
email: '',
first_name: '',
last_name: '',
username: '',
avatar_url: '',
loggedIn: false
}
email: "",
first_name: "",
last_name: "",
username: "",
avatar_url: "",
loggedIn: false,
};
export const useUserStore = defineStore({
id: 'user',
state: () => (initialUserState as UserState),
id: "user",
state: () => initialUserState as UserState,
getters: {
getFullName(): string {
return `${this.first_name} ${this.last_name}`.trim();
},
},
actions: {
handleLogin(username: string, password: string, next='/') {
handleLogin(username: string, password: string, next = "/") {
if (username && password) {
itPost('/api/core/login/', {
itPost("/api/core/login/", {
username,
password,
}).then((data) => {
this.$state = data;
this.loggedIn = true;
log.debug(`redirect to ${next}`);
window.location.href = next;
}).catch(() => {
this.loggedIn = false;
alert('Login failed');
});
})
.then((data) => {
this.$state = data;
this.loggedIn = true;
log.debug(`redirect to ${next}`);
window.location.href = next;
})
.catch(() => {
this.loggedIn = false;
alert("Login failed");
});
}
},
handleLogout() {
itPost('/api/core/logout/', {})
.then(data => {
Object.assign(this, initialUserState);
window.location.href = '/';
})
itPost("/api/core/logout/", {}).then((data) => {
Object.assign(this, initialUserState);
window.location.href = "/";
});
},
fetchUser() {
const appStore = useAppStore();
itGet('/api/core/me/').then((data) => {
this.$state = data;
this.loggedIn = true;
appStore.userLoaded = true;
}).catch(() => {
this.loggedIn = false;
appStore.userLoaded = true;
})
}
}
})
itGet("/api/core/me/")
.then((data) => {
this.$state = data;
this.loggedIn = true;
appStore.userLoaded = true;
})
.catch(() => {
this.loggedIn = false;
appStore.userLoaded = true;
});
},
},
});

View File

@ -1,116 +1,115 @@
import type { Circle } from '@/services/circle'
import type { Circle } from "@/services/circle";
export type CourseCompletionStatus = 'unknown' | 'fail' | 'success'
export type CourseCompletionStatus = "unknown" | "fail" | "success";
export type LearningContentType =
| 'assignment'
| 'book'
| 'document'
| 'exercise'
| 'media_library'
| 'online_training'
| 'resource'
| 'test'
| 'video'
| "assignment"
| "book"
| "document"
| "exercise"
| "media_library"
| "online_training"
| "resource"
| "test"
| "video";
export interface LearningContentBlock {
type: LearningContentType
type: LearningContentType;
value: {
description: string
}
id: string
description: string;
};
id: string;
}
export interface AssignmentBlock {
type: 'assignment';
type: "assignment";
value: {
description: string;
url: string;
},
};
id: string;
}
export interface BookBlock {
type: 'book';
type: "book";
value: {
description: string;
url: string;
},
};
id: string;
}
export interface DocumentBlock {
type: 'document';
type: "document";
value: {
description: string;
url: string;
},
};
id: string;
}
export interface ExerciseBlock {
type: 'exercise';
value: {
description: string;
url: string;
},
id: string;
type: "exercise";
value: {
description: string;
url: string;
};
id: string;
}
export interface MediaLibraryBlock {
type: 'media_library';
value: {
description: string;
url: string;
},
id: string;
type: "media_library";
value: {
description: string;
url: string;
};
id: string;
}
export interface OnlineTrainingBlock {
type: 'online_training';
value: {
description: string;
url: string;
},
id: string;
type: "online_training";
value: {
description: string;
url: string;
};
id: string;
}
export interface ResourceBlock {
type: 'resource';
value: {
description: string;
url: string;
},
id: string;
type: "resource";
value: {
description: string;
url: string;
};
id: string;
}
export interface TestBlock {
type: 'test';
value: {
description: string;
url: string;
},
id: string;
type: "test";
value: {
description: string;
url: string;
};
id: string;
}
export interface VideoBlock {
type: 'video';
value: {
description: string;
url: string;
},
id: string;
type: "video";
value: {
description: string;
url: string;
};
id: string;
}
export interface CircleGoal {
type: 'goal';
type: "goal";
value: string;
id: string;
}
export interface CircleJobSituation {
type: 'job_situation';
type: "job_situation";
value: string;
id: string;
}
@ -124,9 +123,19 @@ export interface CourseWagtailPage {
}
export interface LearningContent extends CourseWagtailPage {
type: 'learnpath.LearningContent';
type: "learnpath.LearningContent";
minutes: number;
contents: (AssignmentBlock | BookBlock | DocumentBlock | ExerciseBlock | MediaLibraryBlock | OnlineTrainingBlock | ResourceBlock | TestBlock | VideoBlock)[];
contents: (
| AssignmentBlock
| BookBlock
| DocumentBlock
| ExerciseBlock
| MediaLibraryBlock
| OnlineTrainingBlock
| ResourceBlock
| TestBlock
| VideoBlock
)[];
parentCircle: Circle;
parentLearningSequence?: LearningSequence;
parentLearningUnit?: LearningUnit;
@ -135,13 +144,13 @@ export interface LearningContent extends CourseWagtailPage {
}
export interface LearningUnitQuestion extends CourseWagtailPage {
type: 'learnpath.LearningUnitQuestion';
type: "learnpath.LearningUnitQuestion";
parentLearningSequence?: LearningSequence;
parentLearningUnit?: LearningUnit;
}
export interface LearningUnit extends CourseWagtailPage {
type: 'learnpath.LearningUnit';
type: "learnpath.LearningUnit";
learningContents: LearningContent[];
minutes: number;
parentLearningSequence?: LearningSequence;
@ -150,22 +159,26 @@ export interface LearningUnit extends CourseWagtailPage {
}
export interface LearningSequence extends CourseWagtailPage {
type: 'learnpath.LearningSequence';
type: "learnpath.LearningSequence";
icon: string;
learningUnits: LearningUnit[];
minutes: number;
}
export type CircleChild = LearningContent | LearningUnit | LearningSequence | LearningUnitQuestion;
export type CircleChild =
| LearningContent
| LearningUnit
| LearningSequence
| LearningUnitQuestion;
export interface WagtailCircle extends CourseWagtailPage {
type: 'learnpath.Circle';
type: "learnpath.Circle";
children: CircleChild[];
description: string;
}
export interface Topic extends CourseWagtailPage {
type: 'learnpath.Topic';
type: "learnpath.Topic";
is_visible: boolean;
circles: Circle[];
}
@ -186,17 +199,16 @@ export interface CourseCompletion {
}
export interface CircleDiagramData {
index: number
title: string
icon: string
startAngle: number
endAngle: number
arrowStartAngle: number
arrowEndAngle: number
done: boolean
index: number;
title: string;
icon: string;
startAngle: number;
endAngle: number;
arrowStartAngle: number;
arrowEndAngle: number;
done: boolean;
}
export interface Course {
id: number;
name: string;
@ -223,7 +235,7 @@ export interface MediaLink {
description: string;
link_display_text: string;
url: string;
}
};
}
export interface MediaContentCollection {
@ -231,26 +243,26 @@ export interface MediaContentCollection {
value: {
title: string;
contents: (MediaDocument | MediaLink)[];
}
};
}
export interface MediaCategoryPage extends CourseWagtailPage {
type: 'media_library.MediaCategoryPage';
type: "media_library.MediaCategoryPage";
overview_icon: string;
introduction_text: string;
description_title: string;
description_text: string;
items: {
type: 'item';
type: "item";
value: string;
id: string;
}
};
course_category: CourseCategory;
body: MediaContentCollection[];
}
export interface MediaLibraryPage extends CourseWagtailPage {
type: 'media_library.MediaLibraryPage';
type: "media_library.MediaLibraryPage";
course: Course;
children: MediaCategoryPage[];
}

View File

@ -1,13 +1,13 @@
import { expect, test } from 'vitest'
import { humanizeDuration } from '../humanizeDuration'
import { expect, test } from "vitest";
import { humanizeDuration } from "../humanizeDuration";
test('format duration for humans', () => {
expect(humanizeDuration(1)).toBe('1 Minute')
expect(humanizeDuration(15)).toBe('15 Minuten')
expect(humanizeDuration(42)).toBe('45 Minuten')
expect(humanizeDuration(60)).toBe('1 Stunde')
expect(humanizeDuration(122)).toBe('2 Stunden')
expect(humanizeDuration(120)).toBe('2 Stunden')
expect(humanizeDuration(132)).toBe('2 Stunden 15 Minuten')
expect(humanizeDuration(632)).toBe('10 Stunden')
})
test("format duration for humans", () => {
expect(humanizeDuration(1)).toBe("1 Minute");
expect(humanizeDuration(15)).toBe("15 Minuten");
expect(humanizeDuration(42)).toBe("45 Minuten");
expect(humanizeDuration(60)).toBe("1 Stunde");
expect(humanizeDuration(122)).toBe("2 Stunden");
expect(humanizeDuration(120)).toBe("2 Stunden");
expect(humanizeDuration(132)).toBe("2 Stunden 15 Minuten");
expect(humanizeDuration(632)).toBe("10 Stunden");
});

View File

@ -1,29 +1,30 @@
function pluralize(text: string, count: number) {
if (count === 1) {
return text;
}
return text + 'n';
if (count === 1) {
return text;
}
return text + "n";
}
export function humanizeDuration(minutes: number) {
const hours = Math.floor(minutes / 60)
const remainingMinutes = minutes % 60
const hours = Math.floor(minutes / 60);
const remainingMinutes = minutes % 60;
if (hours === 0 && minutes < 16) {
return pluralize(`${remainingMinutes} Minute`, remainingMinutes)
}
if (hours === 0 && minutes < 16) {
return pluralize(`${remainingMinutes} Minute`, remainingMinutes);
}
// Remaining minutes are rounded to 15 mins
const roundToMinutes = 15
const roundedMinutes = Math.round((minutes % 60) / roundToMinutes) * roundToMinutes
// Remaining minutes are rounded to 15 mins
const roundToMinutes = 15;
const roundedMinutes = Math.round((minutes % 60) / roundToMinutes) * roundToMinutes;
const hoursString = hours > 0 ? pluralize(`${hours} Stunde`, hours) : ''
const hoursString = hours > 0 ? pluralize(`${hours} Stunde`, hours) : "";
const showMinutesUpToHours = 10
const minutesString = roundedMinutes > 0 && hours < showMinutesUpToHours
? pluralize(`${roundedMinutes} Minute`, roundedMinutes) : ''
const showMinutesUpToHours = 10;
const minutesString =
roundedMinutes > 0 && hours < showMinutesUpToHours
? pluralize(`${roundedMinutes} Minute`, roundedMinutes)
: "";
const delimiter = hoursString && minutesString ? ' ' : ''
return `${hoursString}${delimiter}${minutesString}`
}
const delimiter = hoursString && minutesString ? " " : "";
return `${hoursString}${delimiter}${minutesString}`;
}

View File

@ -1,13 +1,13 @@
import type { LearningContentType } from '@/types';
import type { LearningContentType } from "@/types";
export const learningContentTypesToName = new Map<LearningContentType, string>([
['assignment', 'Auftrag'],
['book', 'Buch'],
['document', 'Dokument'],
['exercise', 'Übung'],
['media_library', 'Mediathek'],
['online_training', 'Online-Training'],
['video', 'Video'],
['test', 'Test'],
['resource', 'Hilfsmittel'],
]);
["assignment", "Auftrag"],
["book", "Buch"],
["document", "Dokument"],
["exercise", "Übung"],
["media_library", "Mediathek"],
["online_training", "Online-Training"],
["video", "Video"],
["test", "Test"],
["resource", "Hilfsmittel"],
]);

View File

@ -1,18 +1,18 @@
<script setup lang="ts">
let url = document.location.href;
if (url.charAt(url.length - 1) !== '/') {
url += '/';
if (url.charAt(url.length - 1) !== "/") {
url += "/";
}
</script>
<template>
<main class="px-4 py-8">
<h1>404 - Not Found as Vue view...</h1>
<div class="text-xl mt-8">Add trailing slash for django view?</div>
<div class="mt-8 text-xl">Try this: <a class="link" :href="url">{{ url }}</a></div>
<div class="mt-8 text-xl">
Try this: <a class="link" :href="url">{{ url }}</a>
</div>
</main>
</template>
<style scoped>
</style>
<style scoped></style>

View File

@ -1,57 +1,59 @@
<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 CircleDiagram from "@/components/circle/CircleDiagram.vue";
import CircleOverview from "@/components/circle/CircleOverview.vue";
import LearningSequence from "@/components/circle/LearningSequence.vue";
import * as log from "loglevel";
import { computed, onMounted } from 'vue'
import { useCircleStore } from '@/stores/circle'
import { useAppStore } from '@/stores/app'
import { useRoute } from 'vue-router'
import _ from 'lodash'
import { humanizeDuration } from '@/utils/humanizeDuration'
import { useAppStore } from "@/stores/app";
import { useCircleStore } from "@/stores/circle";
import { humanizeDuration } from "@/utils/humanizeDuration";
import _ from "lodash";
import { computed, onMounted } from "vue";
import { useRoute } from "vue-router";
const route = useRoute()
const route = useRoute();
log.debug('CircleView.vue created', route)
log.debug("CircleView.vue created", route);
const props = defineProps<{
learningPathSlug: string
circleSlug: string
}>()
learningPathSlug: string;
circleSlug: string;
}>();
const appStore = useAppStore()
appStore.showMainNavigationBar = true
const appStore = useAppStore();
appStore.showMainNavigationBar = true;
const circleStore = useCircleStore()
const circleStore = useCircleStore();
const duration = computed(() => {
if (circleStore.circle) {
const minutes = _.sumBy(circleStore.circle.learningSequences, 'minutes')
return humanizeDuration(minutes)
const minutes = _.sumBy(circleStore.circle.learningSequences, "minutes");
return humanizeDuration(minutes);
}
return ''
})
return "";
});
onMounted(async () => {
log.debug('CircleView.vue mounted', props.learningPathSlug, props.circleSlug)
log.debug("CircleView.vue mounted", props.learningPathSlug, props.circleSlug);
try {
await circleStore.loadCircle(props.learningPathSlug, props.circleSlug)
await circleStore.loadCircle(props.learningPathSlug, props.circleSlug);
if (route.hash.startsWith('#ls-')) {
if (route.hash.startsWith("#ls-")) {
const hashLearningSequence = circleStore.circle?.learningSequences.find((ls) => {
return ls.slug.endsWith(route.hash.replace('#', ''))
})
return ls.slug.endsWith(route.hash.replace("#", ""));
});
if (hashLearningSequence) {
document.getElementById(hashLearningSequence.slug)?.scrollIntoView({ behavior: 'smooth' })
document
.getElementById(hashLearningSequence.slug)
?.scrollIntoView({ behavior: "smooth" });
}
}
} catch (error) {
log.error(error)
log.error(error);
}
})
});
</script>
<template>
@ -89,7 +91,10 @@ 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'">
<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>
@ -106,7 +111,10 @@ onMounted(async () => {
{{ circleStore.circle?.description }}
</div>
<button class="btn-primary mt-4 text-xl" @click="circleStore.page = 'OVERVIEW'">
<button
class="btn-primary mt-4 text-xl"
@click="circleStore.page = 'OVERVIEW'"
>
Erfahre mehr dazu
</button>
</div>
@ -116,17 +124,22 @@ onMounted(async () => {
<div class="leading-relaxed 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>
<div class="flex-auto bg-gray-200 px-4 py-8 lg:px-24">
<div
v-for="learningSequence in circleStore.circle?.learningSequences || []"
v-for="learningSequence in circleStore.circle?.learningSequences ||
[]"
:key="learningSequence.translation_key"
>
<LearningSequence :learning-sequence="learningSequence"></LearningSequence>
<LearningSequence
:learning-sequence="learningSequence"
></LearningSequence>
</div>
</div>
</div>

View File

@ -1,10 +1,10 @@
<script setup lang="ts">
import * as log from 'loglevel'
import { useUserStore } from '@/stores/user'
import { useUserStore } from "@/stores/user";
import * as log from "loglevel";
log.debug('CockpitView created')
log.debug("CockpitView created");
const userStore = useUserStore()
const userStore = useUserStore();
</script>
<template>
@ -16,7 +16,9 @@ const userStore = useUserStore()
<div class="mt-8 p-8 break-words bg-white max-w-xl">
<h3>Versicherungsvermittler/in</h3>
<div class="mt-4">
<router-link class="btn-blue" to="/learn/versicherungsvermittlerin-lp"> Weiter geht's </router-link>
<router-link class="btn-blue" to="/learn/versicherungsvermittlerin-lp">
Weiter geht's
</router-link>
</div>
</div>
</main>

View File

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

View File

@ -1,25 +1,25 @@
<script setup lang="ts">
import * as log from 'loglevel'
import { onMounted, reactive, watch } from 'vue'
import { useCircleStore } from '@/stores/circle'
import { useAppStore } from '@/stores/app'
import LearningContent from '@/components/circle/LearningContent.vue'
import type { LearningContent as LearningContentType } from '@/types'
import LearningContent from "@/components/circle/LearningContent.vue";
import { useAppStore } from "@/stores/app";
import { useCircleStore } from "@/stores/circle";
import type { LearningContent as LearningContentType } from "@/types";
import * as log from "loglevel";
import { onMounted, reactive, watch } from "vue";
log.debug('LearningContentView created')
log.debug("LearningContentView created");
const props = defineProps<{
learningPathSlug: string
circleSlug: string
contentSlug: string
}>()
learningPathSlug: string;
circleSlug: string;
contentSlug: string;
}>();
const state: { learningContent?: LearningContentType } = reactive({})
const state: { learningContent?: LearningContentType } = reactive({});
const appStore = useAppStore()
appStore.showMainNavigationBar = false
const appStore = useAppStore();
appStore.showMainNavigationBar = false;
const circleStore = useCircleStore()
const circleStore = useCircleStore();
const loadLearningContent = async () => {
try {
@ -27,33 +27,41 @@ const loadLearningContent = async () => {
props.learningPathSlug,
props.circleSlug,
props.contentSlug
)
);
} catch (error) {
log.error(error)
log.error(error);
}
}
};
watch(
() => props.contentSlug,
async () => {
log.debug(
'LearningContentView props.contentSlug changed',
"LearningContentView props.contentSlug changed",
props.learningPathSlug,
props.circleSlug,
props.contentSlug
)
await loadLearningContent()
);
await loadLearningContent();
}
)
);
onMounted(async () => {
log.debug('LearningContentView mounted', props.learningPathSlug, props.circleSlug, props.contentSlug)
await loadLearningContent()
})
log.debug(
"LearningContentView mounted",
props.learningPathSlug,
props.circleSlug,
props.contentSlug
);
await loadLearningContent();
});
</script>
<template>
<LearningContent v-if="state.learningContent" :learning-content="state.learningContent" />
<LearningContent
v-if="state.learningContent"
:learning-content="state.learningContent"
/>
</template>
<style lang="postcss" scoped></style>

View File

@ -1,55 +1,59 @@
<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 { useLearningPathStore } from "@/stores/learningPath";
import { useUserStore } from "@/stores/user";
import { onMounted } from "vue";
import LearningPathDiagram from '@/components/circle/LearningPathDiagram.vue'
import LearningPathViewVertical from '@/views/LearningPathViewVertical.vue'
import type { LearningPath } from '@/services/learningPath'
import LearningPathDiagram from "@/components/circle/LearningPathDiagram.vue";
import type { LearningPath } from "@/services/learningPath";
import LearningPathViewVertical from "@/views/LearningPathViewVertical.vue";
log.debug('LearningPathView created')
log.debug("LearningPathView created");
const props = defineProps<{
learningPathSlug: string
}>()
learningPathSlug: string;
}>();
const learningPathStore = useLearningPathStore()
const userStore = useUserStore()
const learningPathStore = useLearningPathStore();
const userStore = useUserStore();
onMounted(async () => {
log.debug('LearningPathView mounted')
log.debug("LearningPathView mounted");
try {
await learningPathStore.loadLearningPath(props.learningPathSlug)
await learningPathStore.loadLearningPath(props.learningPathSlug);
} catch (error) {
log.error(error)
log.error(error);
}
})
});
const createContinueUrl = (learningPath: LearningPath): [string, boolean] => {
if (learningPath.nextLearningContent) {
const circle = learningPath.nextLearningContent.parentCircle
const lsShortSlug = learningPath.nextLearningContent.parentLearningSequence?.slug.replace(`${circle.slug}-`, '')
const url = `/learn/${learningPath.slug}/${learningPath.nextLearningContent.parentCircle.slug}#${lsShortSlug}`
const circle = learningPath.nextLearningContent.parentCircle;
const lsShortSlug =
learningPath.nextLearningContent.parentLearningSequence?.slug.replace(
`${circle.slug}-`,
""
);
const url = `/learn/${learningPath.slug}/${learningPath.nextLearningContent.parentCircle.slug}#${lsShortSlug}`;
const isFirst =
learningPath.nextLearningContent.translation_key ===
learningPath.circles[0].flatLearningContents[0].translation_key
return [url, isFirst]
learningPath.circles[0].flatLearningContents[0].translation_key;
return [url, isFirst];
}
return ['', false]
}
return ["", false];
};
</script>
<template>
<div class="bg-gray-200" v-if="learningPathStore.learningPath">
<div v-if="learningPathStore.learningPath" class="bg-gray-200">
<Teleport to="body">
<LearningPathViewVertical
:show="learningPathStore.page === 'OVERVIEW'"
:learning-path-slug="props.learningPathSlug"
@closemodal="learningPathStore.page = 'INDEX'"
v-bind:learning-path-slug="props.learningPathSlug"
/>
</Teleport>
@ -57,7 +61,11 @@ const createContinueUrl = (learningPath: LearningPath): [string, boolean] => {
<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">
<button
class="flex items-center"
data-cy="show-list-view"
@click="learningPathStore.page = 'OVERVIEW'"
>
<it-icon-list />
Listenansicht anzeigen
</button>
@ -65,7 +73,7 @@ const createContinueUrl = (learningPath: LearningPath): [string, boolean] => {
<LearningPathDiagram
class="max-w-[1680px] w-full"
identifier="mainVisualization"
v-bind:vertical="false"
:vertical="false"
></LearningPathDiagram>
</div>
@ -80,11 +88,19 @@ const createContinueUrl = (learningPath: LearningPath): [string, boolean] => {
<h2>Willkommmen zurück, {{ userStore.first_name }}</h2>
<p class="mt-4 text-xl"></p>
</div>
<div class="p-4 lg:p-8 flex-2" v-if="learningPathStore.learningPath.nextLearningContent">
<div
v-if="learningPathStore.learningPath.nextLearningContent"
class="p-4 lg:p-8 flex-2"
>
Nächster Schritt
<h3>
{{ learningPathStore.learningPath.nextLearningContent.parentCircle.title }}:
{{ learningPathStore.learningPath.nextLearningContent.parentLearningSequence?.title }}
{{
learningPathStore.learningPath.nextLearningContent.parentCircle.title
}}:
{{
learningPathStore.learningPath.nextLearningContent
.parentLearningSequence?.title
}}
</h3>
<router-link
class="mt-4 btn-blue"
@ -92,7 +108,9 @@ const createContinueUrl = (learningPath: LearningPath): [string, boolean] => {
data-cy="lp-continue-button"
translate
>
<span v-if="createContinueUrl(learningPathStore.learningPath)[1]"> Los geht's </span>
<span v-if="createContinueUrl(learningPathStore.learningPath)[1]">
Los geht's
</span>
<span v-else>Weiter geht's</span>
</router-link>
</div>

View File

@ -1,33 +1,33 @@
<script setup lang="ts">
import * as log from 'loglevel'
import { useLearningPathStore } from '@/stores/learningPath'
import { useUserStore } from '@/stores/user'
import { useLearningPathStore } from "@/stores/learningPath";
import { useUserStore } from "@/stores/user";
import * as log from "loglevel";
import LearningPathDiagram from '@/components/circle/LearningPathDiagram.vue'
import ItFullScreenModal from '@/components/ui/ItFullScreenModal.vue'
import LearningPathDiagram from "@/components/circle/LearningPathDiagram.vue";
import ItFullScreenModal from "@/components/ui/ItFullScreenModal.vue";
log.debug('LearningPathView created')
log.debug("LearningPathView created");
const props = defineProps<{
learningPathSlug: string
show: boolean
}>()
learningPathSlug: string;
show: boolean;
}>();
const learningPathStore = useLearningPathStore()
const userStore = useUserStore()
const learningPathStore = useLearningPathStore();
const userStore = useUserStore();
const emits = defineEmits(['closemodal'])
const emits = defineEmits(["closemodal"]);
</script>
<template>
<ItFullScreenModal :show="show" @closemodal="$emit('closemodal')">
<div class="container-medium" v-if="learningPathStore.learningPath">
<div v-if="learningPathStore.learningPath" class="container-medium">
<h1>{{ learningPathStore.learningPath.title }}</h1>
<div class="learningpath flex flex-col">
<div class="flex flex-col h-max">
<LearningPathDiagram
class="w-full"
identifier="verticalVisualization"
v-bind:vertical="true"
:vertical="true"
></LearningPathDiagram>
</div>
</div>

View File

@ -1,41 +1,46 @@
<script setup lang="ts">
import * as log from 'loglevel'
import * as log from "loglevel";
import SelfEvaluation from '@/components/circle/SelfEvaluation.vue'
import SelfEvaluation from "@/components/circle/SelfEvaluation.vue";
import { onMounted, reactive } from 'vue'
import { useAppStore } from '@/stores/app'
import { useCircleStore } from '@/stores/circle'
import type { LearningUnit } from '@/types'
import { useAppStore } from "@/stores/app";
import { useCircleStore } from "@/stores/circle";
import type { LearningUnit } from "@/types";
import { onMounted, reactive } from "vue";
log.debug('LearningUnitSelfEvaluationView created')
log.debug("LearningUnitSelfEvaluationView created");
const props = defineProps<{
learningPathSlug: string
circleSlug: string
learningUnitSlug: string
}>()
learningPathSlug: string;
circleSlug: string;
learningUnitSlug: string;
}>();
const appStore = useAppStore()
appStore.showMainNavigationBar = false
const appStore = useAppStore();
appStore.showMainNavigationBar = false;
const circleStore = useCircleStore()
const circleStore = useCircleStore();
const state: { learningUnit?: LearningUnit } = reactive({})
const state: { learningUnit?: LearningUnit } = reactive({});
onMounted(async () => {
log.debug('LearningUnitSelfEvaluationView mounted', props.learningPathSlug, props.circleSlug, props.learningUnitSlug)
log.debug(
"LearningUnitSelfEvaluationView mounted",
props.learningPathSlug,
props.circleSlug,
props.learningUnitSlug
);
try {
state.learningUnit = await circleStore.loadSelfEvaluation(
props.learningPathSlug,
props.circleSlug,
props.learningUnitSlug
)
);
} catch (error) {
log.error(error)
log.error(error);
}
})
});
</script>
<template>

View File

@ -1,34 +1,38 @@
<script setup lang="ts">
import * as log from 'loglevel'
import { reactive } from 'vue'
import { useUserStore } from '@/stores/user'
import { useRoute } from 'vue-router'
import { useUserStore } from "@/stores/user";
import * as log from "loglevel";
import { reactive } from "vue";
import { useRoute } from "vue-router";
const route = useRoute()
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: '',
})
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
id="username"
v-model="state.username"
type="text"
name="username"
v-model="state.username"
class="py-2 px-3 border border-gray-500 mt-1 block w-96"
/>
</div>
@ -36,9 +40,9 @@ const userStore = useUserStore()
<label class="block mb-1" for="password">Password</label>
<input
id="password"
v-model="state.password"
type="password"
name="password"
v-model="state.password"
class="py-2 px-3 border border-gray-500 mt-1 block w-96"
/>
</div>

View File

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

View File

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

View File

@ -1,19 +1,19 @@
<script setup lang="ts">
import * as log from 'loglevel'
import OverviewCard from '@/components/mediaCenter/OverviewCard.vue'
import { ref, watch } from 'vue'
import { useMediaCenterStore } from '@/stores/mediaCenter'
import OverviewCard from "@/components/mediaCenter/OverviewCard.vue";
import { useMediaCenterStore } from "@/stores/mediaCenter";
import * as log from "loglevel";
import { ref, watch } from "vue";
log.debug('MediaMainView created')
log.debug("MediaMainView created");
const mediaStore = useMediaCenterStore()
const dropdownSelected = ref(mediaStore.selectedLearningPath)
const mediaStore = useMediaCenterStore();
const dropdownSelected = ref(mediaStore.selectedLearningPath);
watch(dropdownSelected, (newValue) =>
mediaStore.$patch({
selectedLearningPath: newValue,
})
)
);
</script>
<template>

View File

@ -1,25 +1,25 @@
<script setup lang="ts">
import * as log from 'loglevel'
import { onMounted } from 'vue'
import { useMediaCenterStore } from '@/stores/mediaCenter'
import { useMediaCenterStore } from "@/stores/mediaCenter";
import * as log from "loglevel";
import { onMounted } from "vue";
log.debug('MediaCenterView created')
log.debug("MediaCenterView created");
const props = defineProps<{
mediaCenterPageSlug: string
}>()
mediaCenterPageSlug: string;
}>();
const mediaCenterStore = useMediaCenterStore()
const mediaCenterStore = useMediaCenterStore();
onMounted(async () => {
log.debug('MediaCenterView mounted', props.mediaCenterPageSlug)
log.debug("MediaCenterView mounted", props.mediaCenterPageSlug);
try {
await mediaCenterStore.loadMediaCenterPage(props.mediaCenterPageSlug)
await mediaCenterStore.loadMediaCenterPage(props.mediaCenterPageSlug);
} catch (error) {
log.error(error)
log.error(error);
}
})
});
</script>
<template>
@ -30,7 +30,9 @@ onMounted(async () => {
<li class="ml-10">Handlungsfelder</li>
<li class="ml-10">Allgemeines zu Versicherungen</li>
<li class="ml-10">Lernmedien</li>
<li class="ml-10"><a href="https://www.vbv.ch/de/der-vbv/lernen-lehren/lexikon">Lexikon</a></li>
<li class="ml-10">
<a href="https://www.vbv.ch/de/der-vbv/lernen-lehren/lexikon">Lexikon</a>
</li>
</ul>
</nav>
<main class="px-8 py-8">

View File

@ -1,52 +1,52 @@
<script setup lang="ts">
import HandlungsfeldLayout from '@/views/HandlungsfeldLayout.vue'
import MediaLink from '@/components/mediaCenter/MediaLink.vue'
import MediaLink from "@/components/mediaCenter/MediaLink.vue";
import HandlungsfeldLayout from "@/views/HandlungsfeldLayout.vue";
const data = {
title: 'Fahrzeug: Lernmedien',
title: "Fahrzeug: Lernmedien",
items: [
{
title: 'Die Motorfahrzeughaftpflicht',
description: 'Buch «Sach- und Vermögensversicherungen» Kapitel 16',
iconUrl: '/static/icons/demo/icon-hf-book.png',
linkText: 'PDF anzeigen',
link: '/static/media/documents/01a_Motorfahrzeughaftpflicht.pdf',
title: "Die Motorfahrzeughaftpflicht",
description: "Buch «Sach- und Vermögensversicherungen» Kapitel 16",
iconUrl: "/static/icons/demo/icon-hf-book.png",
linkText: "PDF anzeigen",
link: "/static/media/documents/01a_Motorfahrzeughaftpflicht.pdf",
openWindow: true,
},
{
title: 'Die Motorfahrzeughaftpflicht',
iconUrl: '/static/icons/demo/icon-hf-book.png',
description: 'Buch «Sach- und Vermögensversicherungen» Kapitel 16',
linkText: 'PDF anzeigen',
link: '/static/media/documents/01a_Motorfahrzeughaftpflicht.pdf',
title: "Die Motorfahrzeughaftpflicht",
iconUrl: "/static/icons/demo/icon-hf-book.png",
description: "Buch «Sach- und Vermögensversicherungen» Kapitel 16",
linkText: "PDF anzeigen",
link: "/static/media/documents/01a_Motorfahrzeughaftpflicht.pdf",
openWindow: true,
},
{
title: 'Die Motorfahrzeughaftpflicht',
iconUrl: '/static/icons/demo/icon-hf-book.png',
description: 'Buch «Sach- und Vermögensversicherungen» Kapitel 16',
linkText: 'PDF anzeigen',
link: '/static/media/documents/01a_Motorfahrzeughaftpflicht.pdf',
title: "Die Motorfahrzeughaftpflicht",
iconUrl: "/static/icons/demo/icon-hf-book.png",
description: "Buch «Sach- und Vermögensversicherungen» Kapitel 16",
linkText: "PDF anzeigen",
link: "/static/media/documents/01a_Motorfahrzeughaftpflicht.pdf",
openWindow: true,
},
{
title: 'Die Motorfahrzeughaftpflicht',
iconUrl: '/static/icons/demo/icon-hf-book.png',
description: 'Buch «Sach- und Vermögensversicherungen» Kapitel 16',
linkText: 'PDF anzeigen',
link: '/static/media/documents/01a_Motorfahrzeughaftpflicht.pdf',
title: "Die Motorfahrzeughaftpflicht",
iconUrl: "/static/icons/demo/icon-hf-book.png",
description: "Buch «Sach- und Vermögensversicherungen» Kapitel 16",
linkText: "PDF anzeigen",
link: "/static/media/documents/01a_Motorfahrzeughaftpflicht.pdf",
openWindow: true,
},
{
title: 'Die Motorfahrzeughaftpflicht',
iconUrl: '/static/icons/demo/icon-hf-book.png',
description: 'Buch «Sach- und Vermögensversicherungen» Kapitel 16',
linkText: 'PDF anzeigen',
link: '/static/media/documents/01a_Motorfahrzeughaftpflicht.pdf',
title: "Die Motorfahrzeughaftpflicht",
iconUrl: "/static/icons/demo/icon-hf-book.png",
description: "Buch «Sach- und Vermögensversicherungen» Kapitel 16",
linkText: "PDF anzeigen",
link: "/static/media/documents/01a_Motorfahrzeughaftpflicht.pdf",
openWindow: true,
},
],
}
};
</script>
<template>
@ -58,7 +58,11 @@ const data = {
<template #body>
<section class="mb-20">
<ul class="border-t">
<li v-for="item in data.items" :key="item.link" class="flex items-center justify-between border-b py-4">
<li
v-for="item in data.items"
:key="item.link"
class="flex items-center justify-between border-b py-4"
>
<div class="flex items-center justify-between">
<div v-if="item.iconUrl">
<img class="mr-6 max-h-[70px]" :src="item.iconUrl" />
@ -69,7 +73,9 @@ const data = {
</div>
</div>
<div class="">
<media-link :to="item.link" :blank="item.openWindow" class="link">{{ item.linkText }}</media-link>
<media-link :to="item.link" :blank="item.openWindow" class="link">{{
item.linkText
}}</media-link>
</div>
</li>
</ul>

View File

@ -1,8 +1,7 @@
<script setup lang="ts">
import * as log from 'loglevel';
log.debug('MessagesView created');
import * as log from "loglevel";
log.debug("MessagesView created");
</script>
<template>
@ -11,5 +10,4 @@ log.debug('MessagesView created');
</main>
</template>
<style scoped>
</style>
<style scoped></style>

View File

@ -1,8 +1,7 @@
<script setup lang="ts">
import * as log from 'loglevel';
log.debug('ProfileView created');
import * as log from "loglevel";
log.debug("ProfileView created");
</script>
<template>
@ -11,5 +10,4 @@ log.debug('ProfileView created');
</main>
</template>
<style scoped>
</style>
<style scoped></style>

View File

@ -1,8 +1,7 @@
<script setup lang="ts">
import * as log from 'loglevel';
log.debug('ShopView created');
import * as log from "loglevel";
log.debug("ShopView created");
</script>
<template>
@ -11,5 +10,4 @@ log.debug('ShopView created');
</main>
</template>
<style scoped>
</style>
<style scoped></style>

View File

@ -1,40 +1,40 @@
<script setup lang="ts">
import { reactive } from 'vue'
import ItCheckbox from '@/components/ui/ItCheckbox.vue'
import ItDropdown from '@/components/ui/ItDropdown.vue'
import IconLogout from '@/components/icons/IconLogout.vue'
import IconSettings from '@/components/icons/IconSettings.vue'
import ItDropdownSelect from '@/components/ui/ItDropdownSelect.vue'
import IconLogout from "@/components/icons/IconLogout.vue";
import IconSettings from "@/components/icons/IconSettings.vue";
import ItCheckbox from "@/components/ui/ItCheckbox.vue";
import ItDropdown from "@/components/ui/ItDropdown.vue";
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
import { reactive } from "vue";
const state = reactive({
checkboxValue: true,
dropdownValues: [
{ id: 1, name: 'Wade Cooper' },
{ id: 2, name: 'Arlene Mccoy' },
{ id: 3, name: 'Devon Webb' },
{ id: 4, name: 'Tom Cook' },
{ id: 5, name: 'Tanya Fox' },
{ id: 6, name: 'Hellen Schmidt' },
{ id: 7, name: 'Caroline Schultz' },
{ id: 8, name: 'Mason Heaney' },
{ id: 9, name: 'Claudie Smitham' },
{ id: 10, name: 'Emil Schaefer' },
{ id: 1, name: "Wade Cooper" },
{ id: 2, name: "Arlene Mccoy" },
{ id: 3, name: "Devon Webb" },
{ id: 4, name: "Tom Cook" },
{ id: 5, name: "Tanya Fox" },
{ id: 6, name: "Hellen Schmidt" },
{ id: 7, name: "Caroline Schultz" },
{ id: 8, name: "Mason Heaney" },
{ id: 9, name: "Claudie Smitham" },
{ id: 10, name: "Emil Schaefer" },
],
dropdownSelected: {
id: -1,
name: 'Select a name',
name: "Select a name",
},
})
});
const dropdownData = [
[
{
title: 'Option 1',
title: "Option 1",
icon: IconLogout,
data: {},
},
{
title: 'Option 2',
title: "Option 2",
icon: null,
data: {
test: 12,
@ -43,27 +43,37 @@ const dropdownData = [
],
[
{
title: 'Option 3',
title: "Option 3",
icon: IconSettings,
data: {
amount: 34,
},
},
],
]
];
// 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', 'green', 'red', 'orange', 'yellow', 'stone', 'gray', 'slate']
const colorValues = [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}`
return `bg-${color}-${value}`;
}
function log(data: any) {
console.log(data)
console.log(data);
}
</script>
@ -76,8 +86,8 @@ function log(data: any) {
</div>
<p class="mt-8 text-xl">
The icons are defined as Web Components, so they can also be used in the backend. Use them like
&lt;it-icon-message/&gt;
The icons are defined as Web Components, so they can also be used in the backend.
Use them like &lt;it-icon-message/&gt;
</p>
<div class="mt-8 mb-8 flex flex-col gap-4 flex-wrap lg:flex-row">
@ -266,12 +276,17 @@ function log(data: any) {
<table class="text-gray-700">
<tr class="h-12 md:h-18 lg:h-24">
<td></td>
<td class="text-center" v-for="value in colorValues">{{ value }}</td>
<td v-for="value in colorValues" :key="value" class="text-center">
{{ value }}
</td>
</tr>
<tr v-for="color in colors" class="h-12 md:h-18 lg:h-24">
<tr v-for="color in colors" :key="color" class="h-12 md:h-18 lg:h-24">
<td>{{ color }}</td>
<td v-for="value in colorValues" class="px-2">
<div class="w-8 h-8 md:w-12 md:h-12 lg:w-16 lg:h-16 rounded-full" :class="[colorBgClass(color, value)]"></div>
<td v-for="value in colorValues" :key="value" class="px-2">
<div
class="w-8 h-8 md:w-12 md:h-12 lg:w-16 lg:h-16 rounded-full"
:class="[colorBgClass(color, value)]"
></div>
</td>
</tr>
</table>
@ -313,8 +328,13 @@ function log(data: any) {
<button disabled class="btn-blue">Blue disabled</button>
</div>
<div class="flex flex-col gap-4 flex-wrap lg:flex-row content-center lg:justify-start mb-16">
<button type="button" class="btn-primary inline-flex items-center p-3 rounded-full">
<div
class="flex flex-col gap-4 flex-wrap lg:flex-row content-center lg:justify-start mb-16"
>
<button
type="button"
class="btn-primary inline-flex items-center p-3 rounded-full"
>
<it-icon-message class="h-5 w-5"></it-icon-message>
</button>
@ -336,19 +356,26 @@ function log(data: any) {
<h2 class="mt-8 mb-8">Dropdown (Work-in-progress)</h2>
<ItDropdownSelect v-model="state.dropdownSelected" :items="state.dropdownValues"> </ItDropdownSelect>
<ItDropdownSelect v-model="state.dropdownSelected" :items="state.dropdownValues">
</ItDropdownSelect>
{{ state.dropdownSelected }}
<h2 class="mt-8 mb-8">Checkbox</h2>
<ItCheckbox :disabled="false" class="" v-model="state.checkboxValue">Label</ItCheckbox>
<ItCheckbox v-model="state.checkboxValue" :disabled="false" class=""
>Label</ItCheckbox
>
<ItCheckbox disabled class="mt-4">Disabled</ItCheckbox>
<h2 class="mt-8 mb-8">Dropdown</h2>
<div class="h-60">
<ItDropdown :button-classes="['btn-primary']" :list-items="dropdownData" :align="'left'" @select="log"
<ItDropdown
:button-classes="['btn-primary']"
:list-items="dropdownData"
:align="'left'"
@select="log"
>Click Me</ItDropdown
>
</div>

View File

@ -1,40 +1,40 @@
const colors = require('./src/colors.json');
const colors = require("./src/colors.json");
module.exports = {
content: [
'./index.html',
'./src/**/*.{vue,js,ts,jsx,tsx}',
"./index.html",
"./src/**/*.{vue,js,ts,jsx,tsx}",
// TODO: wenn man den server-pfad auch angibt wird Tailwind langsamer?! (Startzeit erhöht sich stark...)
// '../server/vbv_lernwelt/**/*.{html,js,py}',
],
theme: {
fontFamily: {
sans: ['Buenos Aires', 'sans-serif'],
sans: ["Buenos Aires", "sans-serif"],
},
extend: {
spacing: {
'128': '32rem',
128: "32rem",
},
maxWidth: {
'8xl': '88rem',
'9xl': '96rem',
"8xl": "88rem",
"9xl": "96rem",
},
backgroundImage: {
'handlungsfelder-overview': "url('/static/icons/icon-handlungsfelder-overview.svg')",
'lernmedien-overview': "url('/static/icons/icon-lernmedien-overview.svg')",
}
"handlungsfelder-overview":
"url('/static/icons/icon-handlungsfelder-overview.svg')",
"lernmedien-overview": "url('/static/icons/icon-lernmedien-overview.svg')",
},
},
colors: colors,
},
safelist: [
{ pattern: /bg-(blue|sky|green|red|orange|yellow|stone|gray|slate)-(200|300|400|500|600|700|800|900)/, },
'it-icon',
'bg-handlungsfelder-overview',
'bg-lernmedien-overview',
{
pattern:
/bg-(blue|sky|green|red|orange|yellow|stone|gray|slate)-(200|300|400|500|600|700|800|900)/,
},
"it-icon",
"bg-handlungsfelder-overview",
"bg-lernmedien-overview",
],
plugins: [
require('@tailwindcss/typography'),
require('@tailwindcss/forms'),
],
}
plugins: [require("@tailwindcss/typography"), require("@tailwindcss/forms")],
};

View File

@ -3,7 +3,7 @@
@tailwind utilities;
html {
@apply text-black
@apply text-black;
}
body {
@ -11,94 +11,93 @@ body {
}
svg {
@apply fill-current
@apply fill-current;
}
@layer base {
.it-icon {
@apply w-8 h-8 inline-block
@apply w-8 h-8 inline-block;
}
h1 {
@apply text-4xl md:text-5xl xl:text-7xl font-bold
@apply text-4xl md:text-5xl xl:text-7xl font-bold;
}
.heading-1 {
@apply text-4xl md:text-5xl xl:text-7xl font-bold
@apply text-4xl md:text-5xl xl:text-7xl font-bold;
}
h2 {
@apply text-2xl md:text-3xl xl:text-4xl font-bold
@apply text-2xl md:text-3xl xl:text-4xl font-bold;
}
.heading-2 {
@apply text-2xl md:text-3xl xl:text-4xl font-bold
@apply text-2xl md:text-3xl xl:text-4xl font-bold;
}
h3 {
@apply text-xl xl:text-2xl font-bold
@apply text-xl xl:text-2xl font-bold;
}
.heading-3 {
@apply text-xl xl:text-2xl font-bold
@apply text-xl xl:text-2xl font-bold;
}
.link {
@apply underline underline-offset-2
@apply underline underline-offset-2;
}
.link-large {
@apply text-lg underline xl:text-xl
@apply text-lg underline xl:text-xl;
}
.text-large {
@apply text-lg xl:text-xl
@apply text-lg xl:text-xl;
}
.text-bold {
@apply text-base font-bold
@apply text-base font-bold;
}
.container-medium {
@apply mx-auto max-w-5xl px-4 lg:px-8 py-4
@apply mx-auto max-w-5xl px-4 lg:px-8 py-4;
}
.container-large {
@apply mx-auto max-w-9xl px-4 lg:px-8 py-4
@apply mx-auto max-w-9xl px-4 lg:px-8 py-4;
}
}
@layer components {
.circle-title {
@apply text-9xl font-bold
@apply text-9xl font-bold;
}
.btn-primary {
@apply font-semibold py-2 px-4 align-middle inline-block
bg-blue-900 text-white border-2 border-blue-900
hover:bg-blue-700 hover:border-blue-700
disabled:opacity-50 disabled:cursor-not-allowed
disabled:opacity-50 disabled:cursor-not-allowed;
}
.btn-secondary {
@apply font-semibold py-2 px-4 align-middle inline-block
bg-white text-blue-900 border-2 border-blue-900
hover:bg-gray-200
disabled:opacity-50 disabled:cursor-not-allowed
disabled:opacity-50 disabled:cursor-not-allowed;
}
.btn-blue {
@apply font-semibold py-2 px-4 align-middle inline-block
bg-sky-500 text-blue-900 border-2 border-sky-500
hover:bg-sky-400 hover:border-sky-400
disabled:opacity-50 disabled:cursor-not-allowed
disabled:opacity-50 disabled:cursor-not-allowed;
}
.btn-text {
@apply font-semibold py-2 px-4 align-middle inline-block
hover:text-gray-700
disabled:opacity-50 disabled:cursor-not-allowed
disabled:opacity-50 disabled:cursor-not-allowed;
}
}
@ -108,4 +107,3 @@ svg {
overflow: hidden;
}
}

View File

@ -4,10 +4,6 @@
"compilerOptions": {
"composite": true,
"lib": [],
"types": [
"node",
"jsdom",
"vitest/globals"
]
"types": ["node", "jsdom", "vitest/globals"]
}
}

View File

@ -1,12 +1,13 @@
const replace = require("replace-in-file");
const gitHash = require('child_process')
const gitHash = require("child_process")
.execSync("git rev-parse --short HEAD")
.toString().trim()
.toString()
.trim();
const options = {
files: "dist/static/vue/*.js",
from: /VBV_VERSION_BUILD_NUMBER_VBV/g,
to: new Date().toISOString().replace("T", " ").substring(0, 19) + ' ' + gitHash,
to: new Date().toISOString().replace("T", " ").substring(0, 19) + " " + gitHash,
};
replace(options, (error, results) => {

View File

@ -1,20 +1,20 @@
import { fileURLToPath, URL } from "url";
import { defineConfig, loadEnv } from "vite";
import vue from "@vitejs/plugin-vue";
import { defineConfig, loadEnv } from "vite";
// import vueI18n from '@intlify/vite-plugin-vue-i18n'
import alias from "@rollup/plugin-alias";
// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
process.env = { ...process.env, ...loadEnv(mode, process.cwd()) }
process.env = { ...process.env, ...loadEnv(mode, process.cwd()) };
return {
plugins: [
vue({
template: {
compilerOptions: {
// treat all tags which start with '<it-' as custom elements
isCustomElement: (tag) => tag.startsWith('it-'),
isCustomElement: (tag) => tag.startsWith("it-"),
},
},
}),
@ -35,19 +35,19 @@ export default defineConfig(({ mode }) => {
],
server: {
port: 5173,
hmr: { port: 5173 }
hmr: { port: 5173 },
},
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
"@": fileURLToPath(new URL("./src", import.meta.url)),
},
},
build: {
assetsDir: 'static/vue',
assetsDir: "static/vue",
},
test: {
globals: true,
environment: 'jsdom',
environment: "jsdom",
},
}
})
};
});

10
format_code.sh Executable file
View File

@ -0,0 +1,10 @@
#!/bin/bash
# script should fail when any process returns non zero code
set -e
echo 'format client code'
npm run prettier
echo 'format python code'
ufmt format server

13
git-pre-commit.sh Executable file
View File

@ -0,0 +1,13 @@
#!/bin/bash
# script should fail when any process returns non zero code
set -e
echo 'prettier:check'
(cd client && npm run prettier:check)
echo 'lint'
(cd client && npm run lint)
echo 'python ufmt check'
ufmt check server

10
git-pre-push.sh Executable file
View File

@ -0,0 +1,10 @@
#!/bin/bash
# script should fail when any process returns non zero code
set -e
echo 'check git-crypt files diff'
git-crypt status -e | sort > git-crypt-encrypted-files-check.txt && diff git-crypt-encrypted-files.txt git-crypt-encrypted-files-check.txt
echo 'check for secrets with truffleHog'
trufflehog --exclude_paths trufflehog-exclude-patterns.txt --allow trufflehog-allow.json --max_depth=3 .

View File

@ -5,7 +5,8 @@
"build": "npm install --prefix client && npm run build --prefix client && npm run build:tailwind --prefix client",
"test": "echo \"Error: no test specified\" && exit 1",
"cypress:open": "cypress open",
"cypress:ci": "cypress run"
"cypress:ci": "cypress run",
"prettier": "npm run prettier --prefix client"
},
"devDependencies": {
"cypress": "^10.6.0"

View File

@ -78,26 +78,24 @@ THIRD_PARTY_APPS = [
"rest_framework.authtoken",
"corsheaders",
"drf_spectacular",
'wagtail.contrib.forms',
'wagtail.contrib.redirects',
'wagtail.contrib.styleguide',
'wagtail.embeds',
'wagtail.sites',
'wagtail.users',
'wagtail.snippets',
'wagtail.documents',
'wagtail.images',
'wagtail.search',
'wagtail.admin',
'wagtail',
"wagtail.contrib.forms",
"wagtail.contrib.redirects",
"wagtail.contrib.styleguide",
"wagtail.embeds",
"wagtail.sites",
"wagtail.users",
"wagtail.snippets",
"wagtail.documents",
"wagtail.images",
"wagtail.search",
"wagtail.admin",
"wagtail",
# 'wagtail.locales',
"wagtail_localize",
"wagtail_localize.locales",
'wagtail.api.v2',
'modelcluster',
'taggit',
"wagtail.api.v2",
"modelcluster",
"taggit",
]
LOCAL_APPS = [
@ -199,32 +197,32 @@ 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:5173')
IT_SERVE_VUE_URL = env("IT_SERVE_VUE_URL", "http://localhost:5173")
# WAGTAIL
# ------------------------------------------------------------------------------
WAGTAIL_SITE_NAME = 'VBV Lernwelt'
WAGTAIL_SITE_NAME = "VBV Lernwelt"
WAGTAIL_I18N_ENABLED = True
LANGUAGES = [
('en-US', "English (American)"),
('fr-CH', "Swiss French"),
('de-CH', "Swiss German"),
('it-CH', "Swiss Italian")
("en-US", "English (American)"),
("fr-CH", "Swiss French"),
("de-CH", "Swiss German"),
("it-CH", "Swiss Italian"),
]
WAGTAILDOCS_DOCUMENT_MODEL = 'media_library.LibraryDocument'
WAGTAILDOCS_DOCUMENT_MODEL = "media_library.LibraryDocument"
WAGTAIL_CONTENT_LANGUAGES = [
('fr-CH', "Swiss French"),
('de-CH', "Swiss German"),
('it-CH', "Swiss Italian")
("fr-CH", "Swiss French"),
("de-CH", "Swiss German"),
("it-CH", "Swiss Italian"),
]
WAGTAILSEARCH_BACKENDS = {
'default': {
'BACKEND': 'wagtail.search.backends.database',
"default": {
"BACKEND": "wagtail.search.backends.database",
}
}
@ -456,13 +454,13 @@ CORS_URLS_REGEX = r"^/api/.*$"
CSP_DEFAULT_SRC = [
"'self'",
"'unsafe-inline'",
'ws://localhost:5173',
'ws://127.0.0.1:5173',
'localhost:8000',
'localhost:8001',
'blob:',
'data:',
'http://*'
"ws://localhost:5173",
"ws://127.0.0.1:5173",
"localhost:8000",
"localhost:8001",
"blob:",
"data:",
"http://*",
]
CSP_FRAME_ANCESTORS = ("'self'",)
@ -498,7 +496,10 @@ ALLOWED_HOSTS = env.list(
# CACHES
CACHES = {
"default": {
"BACKEND": env("IT_DJANGO_CACHE_BACKEND", default="django.core.cache.backends.db.DatabaseCache"),
"BACKEND": env(
"IT_DJANGO_CACHE_BACKEND",
default="django.core.cache.backends.db.DatabaseCache",
),
"LOCATION": env("IT_DJANGO_CACHE_LOCATION", default="django_cache_table"),
},
}
@ -524,9 +525,7 @@ CACHES["api_page_cache"] = {
IT_OAUTH_TENANT_ID = env.str("IT_OAUTH_TENANT_ID", default=None)
if IT_OAUTH_TENANT_ID:
IT_OAUTH_AUTHORIZE_PARAMS = {
'tenant_id': IT_OAUTH_TENANT_ID
}
IT_OAUTH_AUTHORIZE_PARAMS = {"tenant_id": IT_OAUTH_TENANT_ID}
else:
IT_OAUTH_AUTHORIZE_PARAMS = {}
@ -538,14 +537,22 @@ OAUTH = {
# "authorize_url": env("IT_OAUTH_AUTHORIZE_URL", default="https://sso.test.b.lernetz.host/auth/realms/vbv/protocol/openid-connect/auth"),
"authorize_params": IT_OAUTH_AUTHORIZE_PARAMS,
"access_token_params": IT_OAUTH_AUTHORIZE_PARAMS,
"api_base_url": env("IT_OAUTH_API_BASE_URL", default="https://sso.test.b.lernetz.host/auth/realms/vbv/protocol/openid-connect/"),
"local_redirect_uri": env("IT_OAUTH_LOCAL_DIRECT_URI", default="http://localhost:8000/sso/callback/"),
"server_metadata_url": env("IT_OAUTH_SERVER_METADATA_URL", default="https://sso.test.b.lernetz.host/auth/realms/vbv/.well-known/openid-configuration"),
"api_base_url": env(
"IT_OAUTH_API_BASE_URL",
default="https://sso.test.b.lernetz.host/auth/realms/vbv/protocol/openid-connect/",
),
"local_redirect_uri": env(
"IT_OAUTH_LOCAL_DIRECT_URI", default="http://localhost:8000/sso/callback/"
),
"server_metadata_url": env(
"IT_OAUTH_SERVER_METADATA_URL",
default="https://sso.test.b.lernetz.host/auth/realms/vbv/.well-known/openid-configuration",
),
"client_kwargs": {
'scope': env("IT_OAUTH_SCOPE", default=''),
'token_endpoint_auth_method': 'client_secret_post',
'token_placement': 'body',
}
"scope": env("IT_OAUTH_SCOPE", default=""),
"token_endpoint_auth_method": "client_secret_post",
"token_placement": "body",
},
}
if APP_ENVIRONMENT == "development":
@ -555,7 +562,7 @@ if APP_ENVIRONMENT == "development":
# django-debug-toolbar
# ------------------------------------------------------------------------------
# https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#prerequisites
#INSTALLED_APPS += ["debug_toolbar"] # noqa F405
# INSTALLED_APPS += ["debug_toolbar"] # noqa F405
# https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#middleware
# MIDDLEWARE += ["debug_toolbar.middleware.DebugToolbarMiddleware"] # noqa F405
# https://django-debug-toolbar.readthedocs.io/en/latest/configuration.html#debug-toolbar-config
@ -582,7 +589,9 @@ if APP_ENVIRONMENT == "development":
# https://django-extensions.readthedocs.io/en/latest/installation_instructions.html#configuration
INSTALLED_APPS += ["django_extensions"] # noqa F405
if APP_ENVIRONMENT in ["production", "caprover"] or APP_ENVIRONMENT.startswith("caprover"):
if APP_ENVIRONMENT in ["production", "caprover"] or APP_ENVIRONMENT.startswith(
"caprover"
):
# SECURITY
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#secure-proxy-ssl-header
@ -632,7 +641,7 @@ if APP_ENVIRONMENT in ["production", "caprover"] or APP_ENVIRONMENT.startswith("
# ADMIN
# ------------------------------------------------------------------------------
# Django Admin URL regex.
ADMIN_URL = env("IT_DJANGO_ADMIN_URL", 'admin/')
ADMIN_URL = env("IT_DJANGO_ADMIN_URL", "admin/")
# Anymail
# ------------------------------------------------------------------------------

View File

@ -1,7 +1,7 @@
# pylint: disable=unused-wildcard-import,wildcard-import,wrong-import-position
import os
os.environ['IT_APP_ENVIRONMENT'] = 'development'
os.environ["IT_APP_ENVIRONMENT"] = "development"
from .base import * # noqa

View File

@ -1,14 +1,14 @@
# pylint: disable=unused-wildcard-import,wildcard-import,wrong-import-position
import os
os.environ['IT_APP_ENVIRONMENT'] = 'development'
os.environ["IT_APP_ENVIRONMENT"] = "development"
from .base import * # noqa
# GENERAL
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#secret-key
DATABASES['default']['NAME'] = 'vbv_lernwelt_cypress'
DATABASES["default"]["NAME"] = "vbv_lernwelt_cypress"
# EMAIL
# ------------------------------------------------------------------------------

View File

@ -6,16 +6,27 @@ from django.contrib.staticfiles.urls import staticfiles_urlpatterns
from django.urls import include, path, re_path
from django.views import defaults as default_views
from ratelimit.exceptions import Ratelimited
from wagtail import urls as wagtail_urls
from wagtail.admin import urls as wagtailadmin_urls
from wagtail.documents import urls as wagtaildocs_urls
from vbv_lernwelt.core.middleware.auth import django_view_authentication_exempt
from vbv_lernwelt.core.views import (
rate_limit_exceeded_view,
check_rate_limit,
cypress_reset_view,
generate_web_component_icons,
me_user_view,
permission_denied_view,
check_rate_limit, cypress_reset_view, vue_home, vue_login, me_user_view, vue_logout, generate_web_component_icons, )
from vbv_lernwelt.course.views import page_api_view, request_course_completion, mark_course_completion
rate_limit_exceeded_view,
vue_home,
vue_login,
vue_logout,
)
from vbv_lernwelt.course.views import (
mark_course_completion,
page_api_view,
request_course_completion,
)
from wagtail import urls as wagtail_urls
from wagtail.admin import urls as wagtailadmin_urls
from wagtail.documents import urls as wagtaildocs_urls
def raise_example_error(request):
@ -106,4 +117,4 @@ if settings.DEBUG:
# serve everything else via the vue app
urlpatterns += [re_path(r'^(?!.*(server/|api/|sso/)).*$', vue_home, name='home')]
urlpatterns += [re_path(r"^(?!.*(server/|api/|sso/)).*$", vue_home, name="home")]

View File

@ -1,4 +1,4 @@
from django.test import TestCase, override_settings
from django.test import override_settings, TestCase
class RateLimitTest(TestCase):

View File

@ -19,9 +19,10 @@ djangorestframework-stubs # https://github.com/typeddjango/djangorestframework-
flake8 # https://github.com/PyCQA/flake8
flake8-isort # https://github.com/gforcada/flake8-isort
coverage # https://github.com/nedbat/coveragepy
black # https://github.com/psf/black
black>=22.8.0 # https://github.com/psf/black
pylint-django # https://github.com/PyCQA/pylint-django
pre-commit # https://github.com/pre-commit/pre-commit
ufmt
# Django
# ------------------------------------------------------------------------------

View File

@ -28,14 +28,17 @@ attrs==21.4.0
# via
# jsonschema
# pytest
# usort
authlib==1.0.0
# via -r requirements.in
backcall==0.2.0
# via ipython
beautifulsoup4==4.9.3
# via wagtail
black==22.3.0
# via -r requirements-dev.in
black==22.8.0
# via
# -r requirements-dev.in
# ufmt
certifi==2021.10.8
# via
# requests
@ -52,7 +55,10 @@ click==8.1.1
# via
# black
# django-click
# moreorless
# pip-tools
# ufmt
# usort
# uvicorn
concurrent-log-handler==0.9.20
# via -r requirements.in
@ -86,7 +92,6 @@ django==3.2.13
# django-debug-toolbar
# django-extensions
# django-filter
# django-htmx
# django-model-utils
# django-modelcluster
# django-permissionedforms
@ -212,8 +217,14 @@ l18n==2021.3
# via wagtail
lazy-object-proxy==1.7.1
# via astroid
libcst==0.4.7
# via
# ufmt
# usort
markupsafe==2.1.1
# via jinja2
# via
# jinja2
# werkzeug
marshmallow==3.15.0
# via environs
matplotlib-inline==0.1.3
@ -222,6 +233,10 @@ mccabe==0.6.1
# via
# flake8
# pylint
moreorless==0.4.0
# via
# ufmt
# usort
mypy==0.942
# via
# -r requirements-dev.in
@ -231,6 +246,7 @@ mypy-extensions==0.4.3
# via
# black
# mypy
# typing-inspect
nodeenv==1.6.0
# via pre-commit
openpyxl==3.0.9
@ -244,7 +260,9 @@ packaging==21.3
parso==0.8.3
# via jedi
pathspec==0.9.0
# via black
# via
# black
# trailrunner
pep517==0.12.0
# via pip-tools
pexpect==4.8.0
@ -329,6 +347,7 @@ pytz==2022.1
pyyaml==6.0
# via
# drf-spectacular
# libcst
# pre-commit
# uvicorn
redis==4.2.1
@ -362,6 +381,8 @@ sqlparse==0.4.2
# django-debug-toolbar
stack-data==0.2.0
# via ipython
stdlibs==2022.6.8
# via usort
structlog==21.5.0
# via -r requirements.in
tablib[xls,xlsx]==3.2.1
@ -378,6 +399,7 @@ toml==0.10.2
# via
# ipdb
# pre-commit
# usort
tomli==2.0.1
# via
# black
@ -386,6 +408,12 @@ tomli==2.0.1
# pep517
# pylint
# pytest
tomlkit==0.11.5
# via ufmt
trailrunner==1.2.1
# via
# ufmt
# usort
traitlets==5.1.1
# via
# ipython
@ -403,8 +431,15 @@ typing-extensions==4.1.1
# django-stubs
# django-stubs-ext
# djangorestframework-stubs
# libcst
# mypy
# typing-inspect
# ufmt
# wagtail-localize
typing-inspect==0.8.0
# via libcst
ufmt==2.0.1
# via -r requirements-dev.in
uritemplate==4.1.1
# via
# coreapi
@ -413,6 +448,8 @@ urllib3==1.26.9
# via
# requests
# sentry-sdk
usort==1.0.5
# via ufmt
uvicorn[standard]==0.17.6
# via -r requirements.in
uvloop==0.16.0

View File

@ -2,5 +2,5 @@ from django.apps import AppConfig
class MediaLibraryConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'vbv_lernwelt.competence'
default_auto_field = "django.db.models.BigAutoField"
name = "vbv_lernwelt.competence"

View File

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

View File

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

View File

@ -1,9 +1,9 @@
# Generated by Django 3.2.13 on 2022-09-28 12:51
from django.db import migrations, models
import django.db.models.deletion
import wagtail.blocks
import wagtail.fields
from django.db import migrations, models
class Migration(migrations.Migration):
@ -11,41 +11,76 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
('wagtailcore', '0069_log_entry_jsonfield'),
("wagtailcore", "0069_log_entry_jsonfield"),
]
operations = [
migrations.CreateModel(
name='CompetencePage',
name="CompetencePage",
fields=[
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')),
('competence_id', models.TextField(default='A1')),
('items', wagtail.fields.StreamField([('item', wagtail.blocks.TextBlock())], use_json_field=True)),
(
"page_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="wagtailcore.page",
),
),
("competence_id", models.TextField(default="A1")),
(
"items",
wagtail.fields.StreamField(
[("item", wagtail.blocks.TextBlock())], use_json_field=True
),
),
],
options={
'abstract': False,
"abstract": False,
},
bases=('wagtailcore.page',),
bases=("wagtailcore.page",),
),
migrations.CreateModel(
name='CompetenceProfilePage',
name="CompetenceProfilePage",
fields=[
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')),
(
"page_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="wagtailcore.page",
),
),
],
options={
'abstract': False,
"abstract": False,
},
bases=('wagtailcore.page',),
bases=("wagtailcore.page",),
),
migrations.CreateModel(
name='PerformanceCriteria',
name="PerformanceCriteria",
fields=[
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')),
('competence_id', models.TextField(default='A1.1')),
(
"page_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="wagtailcore.page",
),
),
("competence_id", models.TextField(default="A1.1")),
],
options={
'abstract': False,
"abstract": False,
},
bases=('wagtailcore.page',),
bases=("wagtailcore.page",),
),
]

View File

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

View File

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

View File

@ -6,18 +6,37 @@ from vbv_lernwelt.learnpath.models import LearningUnit
from vbv_lernwelt.learnpath.serializer_helpers import get_it_serializer_class
class PerformanceCriteriaSerializer(get_it_serializer_class(PerformanceCriteria, [
'id', 'title', 'slug', 'type', 'translation_key',
'competence_id', 'learning_unit', 'circle', 'course_category',
])):
class PerformanceCriteriaSerializer(
get_it_serializer_class(
PerformanceCriteria,
[
"id",
"title",
"slug",
"type",
"translation_key",
"competence_id",
"learning_unit",
"circle",
"course_category",
],
)
):
learning_unit = serializers.SerializerMethodField()
circle = serializers.SerializerMethodField()
course_category = serializers.SerializerMethodField()
def get_learning_unit(self, obj):
learning_unit_serializer = get_it_serializer_class(LearningUnit, [
'id', 'title', 'slug', 'type', 'translation_key',
])
learning_unit_serializer = get_it_serializer_class(
LearningUnit,
[
"id",
"title",
"slug",
"type",
"translation_key",
],
)
return learning_unit_serializer(obj.learning_unit).data
def get_circle(self, obj):
@ -29,7 +48,17 @@ class PerformanceCriteriaSerializer(get_it_serializer_class(PerformanceCriteria,
return None
class PerformanceCriteriaLearningPathSerializer(get_it_serializer_class(PerformanceCriteria, [
'id', 'title', 'slug', 'type', 'translation_key', 'competence_id',
])):
class PerformanceCriteriaLearningPathSerializer(
get_it_serializer_class(
PerformanceCriteria,
[
"id",
"title",
"slug",
"type",
"translation_key",
"competence_id",
],
)
):
pass

View File

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

View File

@ -1,6 +1,5 @@
from django.contrib import admin
from django.contrib.auth import admin as auth_admin
from django.contrib.auth import get_user_model
from django.contrib.auth import admin as auth_admin, get_user_model
from django.utils.translation import gettext_lazy as _
User = get_user_model()

View File

@ -6,15 +6,21 @@ from vbv_lernwelt.core.models import User
def create_default_users(user_model=User, group_model=Group, default_password=None):
if default_password is None:
default_password = 'test'
default_password = "test"
admin_group, created = group_model.objects.get_or_create(name='admin_group')
_content_creator_grop, _created = group_model.objects.get_or_create(name='content_creator_grop')
student_group, created = group_model.objects.get_or_create(name='student_group')
admin_group, created = group_model.objects.get_or_create(name="admin_group")
_content_creator_grop, _created = group_model.objects.get_or_create(
name="content_creator_grop"
)
student_group, created = group_model.objects.get_or_create(name="student_group")
def _create_student_user(email, first_name, last_name, avatar_url='', password=default_password):
def _create_student_user(
email, first_name, last_name, avatar_url="", password=default_password
):
student_user, created = _get_or_create_user(
user_model=user_model, username=email, password=password,
user_model=user_model,
username=email,
password=password,
)
student_user.first_name = first_name
student_user.last_name = last_name
@ -22,7 +28,7 @@ def create_default_users(user_model=User, group_model=Group, default_password=No
student_user.groups.add(student_group)
student_user.save()
def _create_admin_user(email, first_name, last_name, avatar_url=''):
def _create_admin_user(email, first_name, last_name, avatar_url=""):
admin_user, created = _get_or_create_user(
user_model=user_model, username=email, password=default_password
)
@ -35,68 +41,68 @@ def create_default_users(user_model=User, group_model=Group, default_password=No
admin_user.save()
_create_admin_user(
email='info@iterativ.ch',
first_name='Info',
last_name='Iterativ',
avatar_url='/static/avatars/avatar_iterativ.png'
email="info@iterativ.ch",
first_name="Info",
last_name="Iterativ",
avatar_url="/static/avatars/avatar_iterativ.png",
)
_create_admin_user(
email='admin',
first_name='Peter',
last_name='Adminson',
avatar_url='/static/avatars/avatar_iterativ.png'
email="admin",
first_name="Peter",
last_name="Adminson",
avatar_url="/static/avatars/avatar_iterativ.png",
)
_create_student_user(
email='student',
first_name='Student',
last_name='Meier',
avatar_url='/static/avatars/avatar_iterativ.png'
email="student",
first_name="Student",
last_name="Meier",
avatar_url="/static/avatars/avatar_iterativ.png",
)
_create_student_user(
email='daniel.egger@iterativ.ch',
first_name='Daniel',
last_name='Egger',
avatar_url='/static/avatars/avatar_iterativ.png'
email="daniel.egger@iterativ.ch",
first_name="Daniel",
last_name="Egger",
avatar_url="/static/avatars/avatar_iterativ.png",
)
_create_student_user(
email='axel.manderbach@lernetz.ch',
first_name='Axel',
last_name='Manderbach',
avatar_url='/static/avatars/avatar_axel.jpg'
email="axel.manderbach@lernetz.ch",
first_name="Axel",
last_name="Manderbach",
avatar_url="/static/avatars/avatar_axel.jpg",
)
_create_student_user(
email='christoph.bosshard@vbv-afa.ch',
first_name='Christoph',
last_name='Bosshard',
avatar_url='/static/avatars/avatar_christoph.png',
password='myvbv1234'
email="christoph.bosshard@vbv-afa.ch",
first_name="Christoph",
last_name="Bosshard",
avatar_url="/static/avatars/avatar_christoph.png",
password="myvbv1234",
)
_create_student_user(
email='alexandra.vangelista@lernetz.ch',
first_name='Alexandra',
last_name='Vangelista',
avatar_url='/static/avatars/avatar_alexandra.png',
password='myvbv1234'
email="alexandra.vangelista@lernetz.ch",
first_name="Alexandra",
last_name="Vangelista",
avatar_url="/static/avatars/avatar_alexandra.png",
password="myvbv1234",
)
_create_student_user(
email='chantal.rosenberg@vbv-afa.ch',
first_name='Chantal',
last_name='Rosenberg',
avatar_url='/static/avatars/avatar_chantal.png',
password='myvbv1234'
email="chantal.rosenberg@vbv-afa.ch",
first_name="Chantal",
last_name="Rosenberg",
avatar_url="/static/avatars/avatar_chantal.png",
password="myvbv1234",
)
def _get_or_create_user(user_model, *args, **kwargs):
username = kwargs.get('username', None)
password = kwargs.get('password', None)
username = kwargs.get("username", None)
password = kwargs.get("password", None)
created = False
user = user_model.objects.filter(username=username).first()

View File

@ -1,15 +1,15 @@
import djclick as click
from django.conf import settings
from django.core.management import call_command
from django.db import transaction, connection
from django.db import connection, transaction
def reset_schema(db_config_user):
sql_list = (
'DROP SCHEMA public CASCADE',
f'CREATE SCHEMA public AUTHORIZATION {db_config_user}',
'GRANT ALL ON SCHEMA public TO postgres',
'GRANT ALL ON SCHEMA public TO public',
"DROP SCHEMA public CASCADE",
f"CREATE SCHEMA public AUTHORIZATION {db_config_user}",
"GRANT ALL ON SCHEMA public TO postgres",
"GRANT ALL ON SCHEMA public TO public",
"COMMENT ON SCHEMA public IS 'standard public schema';",
)
@ -21,11 +21,11 @@ def reset_schema(db_config_user):
@click.command()
def command():
user = settings.DATABASES['default']['USER']
user = settings.DATABASES["default"]["USER"]
print(user)
reset_schema(db_config_user=user)
call_command('createcachetable')
call_command('migrate')
call_command('create_default_users')
call_command('create_default_learning_path')
call_command("createcachetable")
call_command("migrate")
call_command("create_default_users")
call_command("create_default_learning_path")

View File

@ -3,13 +3,17 @@ from django.contrib.auth.models import AbstractUser
class UserManager(BaseUserManager):
def create_or_update_by_email(self, email: str, first_name: str, last_name: str, username: str) -> tuple[
AbstractUser, bool]:
def create_or_update_by_email(
self, email: str, first_name: str, last_name: str, username: str
) -> tuple[AbstractUser, bool]:
# create or sync user with OpenID Data
user, created = self.model.objects.get_or_create(email=email, defaults={
"first_name": first_name,
"last_name": last_name,
"username": username
})
user, created = self.model.objects.get_or_create(
email=email,
defaults={
"first_name": first_name,
"last_name": last_name,
"username": username,
},
)
return user, created

View File

@ -55,13 +55,15 @@ class UserLoggedInCookieMiddleWare(MiddlewareMixin):
If the user is not authenticated and the cookie remains, delete it
"""
cookie_name = 'loginStatus'
cookie_name = "loginStatus"
def process_response(self, request, response):
# if user and no cookie, set cookie
if request.user.is_authenticated and not request.COOKIES.get(self.cookie_name):
response.set_cookie(self.cookie_name, 'true')
elif not request.user.is_authenticated and request.COOKIES.get(self.cookie_name):
response.set_cookie(self.cookie_name, "true")
elif not request.user.is_authenticated and request.COOKIES.get(
self.cookie_name
):
# else if no user and cookie remove user cookie, logout
response.delete_cookie(self.cookie_name)
return response

View File

@ -2,8 +2,8 @@
import django.contrib.auth.models
import django.contrib.auth.validators
from django.db import migrations, models
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):

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