Merge branch 'feature/circle-vue' into develop
This commit is contained in:
commit
1e3c75d171
|
|
@ -47,6 +47,7 @@ cap.create_and_update_app(
|
|||
'IT_DJANGO_SECRET_KEY': env.str('IT_DJANGO_SECRET_KEY'),
|
||||
'IT_DJANGO_ADMIN_URL': env.str('IT_DJANGO_ADMIN_URL'),
|
||||
'IT_DJANGO_ALLOWED_HOSTS': env.str('IT_DJANGO_ALLOWED_HOSTS'),
|
||||
'IT_DJANGO_DEBUG': 'false',
|
||||
'IT_SENTRY_DSN': env.str('IT_SENTRY_DSN'),
|
||||
'IT_APP_ENVIRONMENT': 'caprover',
|
||||
'POSTGRES_HOST': 'srv-captain--vbv-lernwelt-postgres-db',
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
#!/bin/bash
|
||||
|
||||
# script should fail when any process returns non zero code
|
||||
set -ev
|
||||
|
||||
# create client
|
||||
npm run build
|
||||
|
||||
|
|
|
|||
|
|
@ -1,28 +1,18 @@
|
|||
/* 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-essential',
|
||||
'eslint:recommended',
|
||||
'@vue/eslint-config-typescript/recommended',
|
||||
// "@vue/eslint-config-prettier"
|
||||
],
|
||||
"env": {
|
||||
"vue/setup-compiler-macros": true
|
||||
'env': {
|
||||
'vue/setup-compiler-macros': true
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"cypress/integration/**.spec.{js,ts,jsx,tsx}"
|
||||
],
|
||||
"extends": [
|
||||
"plugin:cypress/recommended"
|
||||
]
|
||||
}
|
||||
],
|
||||
"rules": {
|
||||
"quotes": ["error", "single"]
|
||||
'rules': {
|
||||
'@typescript-eslint/no-unused-vars': ['warn'],
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,10 +4,8 @@
|
|||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc --noEmit && vite build && cp ./dist/index.html ../server/vbv_lernwelt/templates/vue/index.html && cp -r ./dist/static/vue ../server/vbv_lernwelt/static/",
|
||||
"preview": "vite preview --port 5050",
|
||||
"test:unit": "vitest --environment jsdom",
|
||||
"test:e2e": "start-server-and-test preview http://127.0.0.1:5050/ 'cypress open'",
|
||||
"test:e2e:ci": "start-server-and-test preview http://127.0.0.1:5050/ 'cypress run'",
|
||||
"test": "vitest run",
|
||||
"coverage": "vitest run --coverage",
|
||||
"typecheck": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
|
||||
},
|
||||
|
|
@ -26,6 +24,7 @@
|
|||
"@intlify/vite-plugin-vue-i18n": "^3.4.0",
|
||||
"@rollup/plugin-alias": "^3.1.9",
|
||||
"@rushstack/eslint-patch": "^1.1.0",
|
||||
"@testing-library/vue": "^6.6.0",
|
||||
"@types/jsdom": "^16.2.14",
|
||||
"@types/node": "^16.11.26",
|
||||
"@vitejs/plugin-vue": "^2.3.1",
|
||||
|
|
@ -38,7 +37,7 @@
|
|||
"eslint": "^8.5.0",
|
||||
"eslint-plugin-cypress": "^2.12.1",
|
||||
"eslint-plugin-vue": "^8.2.0",
|
||||
"jsdom": "^19.0.0",
|
||||
"happy-dom": "^5.3.1",
|
||||
"postcss": "^8.4.12",
|
||||
"postcss-import": "^14.1.0",
|
||||
"prettier": "^2.5.1",
|
||||
|
|
@ -47,7 +46,7 @@
|
|||
"start-server-and-test": "^1.14.0",
|
||||
"typescript": "~4.6.3",
|
||||
"vite": "^2.9.1",
|
||||
"vitest": "^0.8.1",
|
||||
"vitest": "^0.15.1",
|
||||
"vue-tsc": "^0.33.9"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
'postcss-import': {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
import { describe, it, expect } from 'vitest'
|
||||
import {describe, expect, it} from 'vitest'
|
||||
|
||||
import { mount } from '@vue/test-utils'
|
||||
import {mount} from '@vue/test-utils'
|
||||
import MainNavigationBar from '../MainNavigationBar.vue'
|
||||
|
||||
describe('MainNavigationBar', () => {
|
||||
it('renders properly', () => {
|
||||
const wrapper = mount(MainNavigationBar, { })
|
||||
expect(wrapper.text()).toContain('Ich bin ein myVBV Heade')
|
||||
const wrapper = mount(MainNavigationBar, {})
|
||||
expect(wrapper.text()).toContain('myVBV')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
<script setup lang="ts">
|
||||
import type {Circle} from '@/types';
|
||||
|
||||
const props = defineProps<{
|
||||
circleData: Circle
|
||||
}>()
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="circle-overview px-4 py-16 lg:px-16 lg:py-24 relative">
|
||||
<div
|
||||
class="w-8 h-8 absolute right-4 top-4 cursor-pointer"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
<it-icon-close></it-icon-close>
|
||||
</div>
|
||||
|
||||
|
||||
<h1 class="">Überblick: Circle "{{ circleData.title }}"</h1>
|
||||
|
||||
<p class="mt-8 text-xl">Hier zeigen wir dir, was du in diesem Circle lernen wirst.</p>
|
||||
|
||||
<div class="mt-8 p-4 border border-gray-500">
|
||||
<h3>Du wirst in der Lage sein, ... </h3>
|
||||
|
||||
<ul class="mt-4">
|
||||
<li class="text-xl flex items-center" v-for="goal in circleData.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>
|
||||
<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:
|
||||
</h3>
|
||||
|
||||
<ul class="grid grid-cols-1 lg:grid-cols-3 auto-rows-fr gap-6 mt-8">
|
||||
<li
|
||||
v-for="jobSituation in circleData.job_situations"
|
||||
:key="jobSituation.id"
|
||||
class="job-situation border border-gray-500 p-4 text-xl flex items-center"
|
||||
>
|
||||
{{jobSituation.value}}
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
<script setup lang="ts">
|
||||
import * as log from 'loglevel';
|
||||
import {computed} from 'vue';
|
||||
import {useCircleStore} from '@/stores/circle';
|
||||
|
||||
log.debug('LearningContent.vue setup');
|
||||
|
||||
const circleStore = useCircleStore();
|
||||
|
||||
const learningContent = computed(() => circleStore.currentLearningContent);
|
||||
|
||||
const block = computed(() => {
|
||||
if (learningContent.value) {
|
||||
return learningContent.value.contents[0];
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<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"
|
||||
@click="circleStore.closeLearningContent()"
|
||||
>
|
||||
<it-icon-arrow-left class="-ml-1 mr-1 h-5 w-5"></it-icon-arrow-left>
|
||||
<span class="hidden lg:inline">zurück zum Circle</span>
|
||||
</button>
|
||||
|
||||
<h1 class="text-xl hidden lg:block">{{ learningContent.title }}</h1>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn-blue"
|
||||
@click="circleStore.continueFromLearningContent()"
|
||||
>
|
||||
Abschliessen und weiter
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
|
||||
<div class="mx-auto max-w-5xl px-4 lg:px-8 py-4">
|
||||
<p>{{ block.value.description }}</p>
|
||||
|
||||
<div v-if="block.type === 'video'">
|
||||
<iframe
|
||||
class="mt-8 w-full aspect-video"
|
||||
:src="block.value.url"
|
||||
:title="learningContent.title"
|
||||
frameborder="0"
|
||||
allow="accelerometer; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen>
|
||||
</iframe>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="block.type === 'podcast'"
|
||||
>
|
||||
<iframe width="100%" height="300" scrolling="no" frameborder="no" allow="" :src="block.value.url"></iframe>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
|
|
@ -1,17 +1,28 @@
|
|||
<script setup lang="ts">
|
||||
|
||||
|
||||
import ItCheckbox from '@/components/ui/ItCheckbox.vue';
|
||||
import type {LearningContent, LearningSequence} from '@/types';
|
||||
import {useCircleStore} from '@/stores/circle';
|
||||
import {computed} from 'vue';
|
||||
|
||||
const props = defineProps(['learningSequence', 'completionData'])
|
||||
const props = defineProps<{
|
||||
learningSequence: LearningSequence
|
||||
}>()
|
||||
|
||||
const contentCompleted = (learningContent) => {
|
||||
if (props.completionData?.json_data?.completed_learning_contents) {
|
||||
return learningContent.translation_key in props.completionData.json_data.completed_learning_contents;
|
||||
const circleStore = useCircleStore();
|
||||
|
||||
function toggleCompleted(learningContent: LearningContent) {
|
||||
circleStore.markCompletion(learningContent, !learningContent.completed);
|
||||
}
|
||||
|
||||
const someFinished = computed(() => {
|
||||
if (props.learningSequence) {
|
||||
return circleStore.flatChildren.filter((lc) => {
|
||||
return lc.completed && lc.parentLearningSequence?.translation_key === props.learningSequence.translation_key;
|
||||
}).length > 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
|
|
@ -25,10 +36,17 @@ const contentCompleted = (learningContent) => {
|
|||
<div>{{ learningSequence.minutes }} Minuten</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white px-4 border border-gray-500 border-l-4 border-l-sky-500">
|
||||
<div
|
||||
class="bg-white px-4 border border-gray-500 border-l-4"
|
||||
:class="{
|
||||
'border-l-sky-500': someFinished,
|
||||
'border-l-gray-500': !someFinished,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-for="learningUnit in learningSequence.learningUnits"
|
||||
class="py-3"
|
||||
:key="learningUnit.id"
|
||||
class="pt-3"
|
||||
>
|
||||
<div class="pb-3 flex gap-4" v-if="learningUnit.title">
|
||||
<div class="font-bold">{{ learningUnit.title }}</div>
|
||||
|
|
@ -37,17 +55,46 @@ const contentCompleted = (learningContent) => {
|
|||
|
||||
<div
|
||||
v-for="learningContent in learningUnit.learningContents"
|
||||
:key="learningContent.id"
|
||||
class="flex items-center gap-4 pb-3"
|
||||
>
|
||||
<ItCheckbox
|
||||
:modelValue="contentCompleted(learningContent)"
|
||||
@click="$emit('toggleLearningContentCheckbox', learningContent)"
|
||||
:modelValue="learningContent.completed"
|
||||
@click="toggleCompleted(learningContent)"
|
||||
>
|
||||
{{ learningContent.contents[0].type }}: {{ learningContent.title }}
|
||||
<span @click.stop="circleStore.openLearningContent(learningContent)">{{ learningContent.contents[0].type }}: {{ learningContent.title }}</span>
|
||||
</ItCheckbox>
|
||||
</div>
|
||||
|
||||
<hr class="-mx-4 text-gray-500">
|
||||
<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"
|
||||
>
|
||||
<it-icon-smiley-happy/>
|
||||
<span>Selbsteinschätzung: Ich kann das.</span>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="circleStore.calcSelfEvaluationStatus(learningUnit) === false"
|
||||
class="flex items-center gap-4 pb-3"
|
||||
>
|
||||
<it-icon-smiley-thinking/>
|
||||
<span>Selbsteinschätzung: Muss ich nochmals anschauen</span>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="flex items-center gap-4 pb-3"
|
||||
>
|
||||
<it-icon-smiley-neutral/>
|
||||
<span>Selbsteinschätzung</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr v-if="!learningUnit.last" class="-mx-4 text-gray-500">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,114 @@
|
|||
<script setup lang="ts">
|
||||
import * as log from 'loglevel';
|
||||
import {computed, reactive} from 'vue';
|
||||
import {useCircleStore} from '@/stores/circle';
|
||||
|
||||
log.debug('LearningContent.vue setup');
|
||||
|
||||
const circleStore = useCircleStore();
|
||||
|
||||
const state = reactive({
|
||||
questionIndex: 0,
|
||||
});
|
||||
|
||||
const questions = computed(() => circleStore.currentSelfEvaluation!.children);
|
||||
const currentQuestion = computed(() => questions.value[state.questionIndex]);
|
||||
|
||||
function handleContinue() {
|
||||
log.debug('handleContinue');
|
||||
if (state.questionIndex + 1 < questions.value.length) {
|
||||
log.debug('increment questionIndex', state.questionIndex);
|
||||
state.questionIndex += 1;
|
||||
} else {
|
||||
log.debug('continue to next learning content');
|
||||
circleStore.continueFromSelfEvaluation();
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<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"
|
||||
@click="circleStore.closeSelfEvaluation()"
|
||||
>
|
||||
<it-icon-arrow-left class="-ml-1 mr-1 h-5 w-5"></it-icon-arrow-left>
|
||||
<span class="hidden lg:inline">zurück zum Circle</span>
|
||||
</button>
|
||||
|
||||
<h1 class="text-xl hidden lg:block">{{ circleStore.currentSelfEvaluation.title }}</h1>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn-blue"
|
||||
@click="handleContinue()"
|
||||
>
|
||||
Weiter
|
||||
</button>
|
||||
</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>
|
||||
|
||||
<p class="text-xl mt-4">
|
||||
Überprüfe, ob du in der Lernheinheit <span class="font-bold">"{{ circleStore.currentSelfEvaluation.title }}"</span> alles verstanden hast.<br>
|
||||
Lies die folgende Aussage und bewerte sie:
|
||||
</p>
|
||||
|
||||
<div class="mt-4 lg:mt-8 p-6 lg:p-12 border border-gray-500">
|
||||
<h2 class="heading-2">{{ currentQuestion.title }}</h2>
|
||||
|
||||
<div class="mt-4 lg:mt-8 flex flex-col lg:flex-row justify-between gap-6">
|
||||
<button
|
||||
@click="circleStore.markCompletion(currentQuestion, true)"
|
||||
class="flex-1 inline-flex items-center text-left p-4 border"
|
||||
:class="{
|
||||
'border-green-500': currentQuestion.completed,
|
||||
'border-2': currentQuestion.completed,
|
||||
'border-gray-500': !currentQuestion.completed,
|
||||
}"
|
||||
>
|
||||
<it-icon-smiley-happy class="w-16 h-16 mr-4"></it-icon-smiley-happy>
|
||||
<span class="font-bold text-xl">
|
||||
Ja, ich kann das.
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
@click="circleStore.markCompletion(currentQuestion, false)"
|
||||
class="flex-1 inline-flex items-center text-left p-4 border"
|
||||
:class="{
|
||||
'border-orange-500': currentQuestion.completed === false,
|
||||
'border-2': currentQuestion.completed === false,
|
||||
'border-gray-500': currentQuestion.completed === true || currentQuestion.completed === undefined,
|
||||
}"
|
||||
>
|
||||
<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>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
|
|
@ -1,13 +1,15 @@
|
|||
import { getCookieValue } from '@/router/guards';
|
||||
import {getCookieValue} from '@/router/guards';
|
||||
|
||||
class FetchError extends Error {
|
||||
constructor(response, message = 'HTTP error ' + response.status) {
|
||||
response: Response;
|
||||
|
||||
constructor(response: Response, message = 'HTTP error ' + response.status) {
|
||||
super(message);
|
||||
this.response = response;
|
||||
}
|
||||
}
|
||||
|
||||
export const itFetch = (url, options) => {
|
||||
export const itFetch = (url: RequestInfo, options: RequestInit) => {
|
||||
return fetch(url, options).then(response => {
|
||||
if (!response.ok) {
|
||||
throw new FetchError(response);
|
||||
|
|
@ -17,7 +19,11 @@ export const itFetch = (url, options) => {
|
|||
});
|
||||
};
|
||||
|
||||
export const itPost = (url, data, options) => {
|
||||
export const itPost = (
|
||||
url: RequestInfo,
|
||||
data: unknown,
|
||||
options: RequestInit = {},
|
||||
) => {
|
||||
options = Object.assign({}, options);
|
||||
|
||||
const headers = Object.assign({
|
||||
|
|
@ -35,6 +41,8 @@ export const itPost = (url, data, options) => {
|
|||
body: JSON.stringify(data)
|
||||
}, options);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
options.headers['X-CSRFToken'] = getCookieValue('csrftoken');
|
||||
|
||||
if (options.method === 'GET') {
|
||||
|
|
@ -48,6 +56,6 @@ export const itPost = (url, data, options) => {
|
|||
});
|
||||
};
|
||||
|
||||
export const itGet = (url) => {
|
||||
export const itGet = (url: RequestInfo) => {
|
||||
return itPost(url, {}, {method: 'GET'});
|
||||
};
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
import {describe, it} from 'vitest'
|
||||
import {parseLearningSequences} from '../circle';
|
||||
import type {WagtailCircle} from '@/types';
|
||||
|
||||
describe('circleService.parseLearningSequences', () => {
|
||||
it('can parse learning sequences from api response', () => {
|
||||
const input = {
|
||||
"id": 10,
|
||||
"title": "Analyse",
|
||||
"slug": "analyse",
|
||||
"type": "learnpath.Circle",
|
||||
"translation_key": "c9832aaf-02b2-47af-baeb-bde60d8ec1f5",
|
||||
"children": [
|
||||
{
|
||||
"id": 18,
|
||||
"title": "Anwenden",
|
||||
"slug": "anwenden",
|
||||
"type": "learnpath.LearningSequence",
|
||||
"translation_key": "2e4c431a-9602-4398-ad18-20dd4bb189fa",
|
||||
"icon": "it-icon-ls-apply"
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"title": "Prämien einsparen",
|
||||
"slug": "pramien-einsparen",
|
||||
"type": "learnpath.LearningUnit",
|
||||
"translation_key": "75c1f31a-ae25-4d9c-9206-a4e7fdae8c13",
|
||||
"questions": []
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"title": "Versicherungsbedarf für Familien",
|
||||
"slug": "versicherungsbedarf-für-familien",
|
||||
"type": "learnpath.LearningContent",
|
||||
"translation_key": "2a422da3-a3ad-468a-831e-9141c122ffef",
|
||||
"minutes": 60,
|
||||
"contents": [
|
||||
{
|
||||
"type": "exercise",
|
||||
"value": {
|
||||
"description": "Beispiel Aufgabe"
|
||||
},
|
||||
"id": "ee0bcef7-702b-42f3-a891-88a0332fce6f"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"title": "Alles klar?",
|
||||
"slug": "alles-klar",
|
||||
"type": "learnpath.LearningContent",
|
||||
"translation_key": "7dc9d96d-07f9-4b9f-bec1-43ba67cf9010",
|
||||
"minutes": 60,
|
||||
"contents": [
|
||||
{
|
||||
"type": "exercise",
|
||||
"value": {
|
||||
"description": "Beispiel Aufgabe"
|
||||
},
|
||||
"id": "a556ebb2-f902-4d78-9b76-38b7933118b8"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 22,
|
||||
"title": "Sich selbständig machen",
|
||||
"slug": "sich-selbstandig-machen",
|
||||
"type": "learnpath.LearningUnit",
|
||||
"translation_key": "c40d5266-3c94-4b9b-8469-9ac6b32a6231",
|
||||
"questions": []
|
||||
},
|
||||
{
|
||||
"id": 23,
|
||||
"title": "GmbH oder AG",
|
||||
"slug": "gmbh-oder-ag",
|
||||
"type": "learnpath.LearningContent",
|
||||
"translation_key": "59331843-9f52-4b41-9cd1-2293a8d90064",
|
||||
"minutes": 120,
|
||||
"contents": [
|
||||
{
|
||||
"type": "video",
|
||||
"value": {
|
||||
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam",
|
||||
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
|
||||
},
|
||||
"id": "a4974834-f404-4fb8-af94-a24c6db56bb8"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 24,
|
||||
"title": "Tiertherapie Patrizia Feller",
|
||||
"slug": "tiertherapie-patrizia-feller",
|
||||
"type": "learnpath.LearningContent",
|
||||
"translation_key": "13f6d661-1d10-4b59-b8e5-01fcec47a38f",
|
||||
"minutes": 120,
|
||||
"contents": [
|
||||
{
|
||||
"type": "exercise",
|
||||
"value": {
|
||||
"description": "Beispiel Aufgabe"
|
||||
},
|
||||
"id": "5947c947-8656-44b5-826c-1787057c2df2"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 25,
|
||||
"title": "Auto verkaufen",
|
||||
"slug": "auto-verkaufen",
|
||||
"type": "learnpath.LearningUnit",
|
||||
"translation_key": "3b42e514-0bbe-4c23-9c88-3f5263e47cf9",
|
||||
"questions": []
|
||||
},
|
||||
],
|
||||
"description": "Nach dem Gespräch werten sie die Analyse aus...",
|
||||
"job_situations": [],
|
||||
"goals": [],
|
||||
"experts": []
|
||||
} as WagtailCircle;
|
||||
|
||||
const learningSequences = parseLearningSequences(input.children);
|
||||
expect(learningSequences.length).toBe(1);
|
||||
console.log(learningSequences[0].learningUnits[0].learningContents[0]);
|
||||
|
||||
expect(
|
||||
learningSequences[0].learningUnits[1].learningContents[0].previousLearningContent.translation_key
|
||||
).toEqual(learningSequences[0].learningUnits[0].learningContents[1].translation_key);
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
import type {CircleChild, LearningContent, LearningSequence, LearningUnit} from '@/types';
|
||||
|
||||
|
||||
function createEmptyLearningUnit(parentLearningSequence: LearningSequence): LearningUnit {
|
||||
return {
|
||||
id: 0,
|
||||
title: '',
|
||||
slug: '',
|
||||
translation_key: '',
|
||||
type: 'learnpath.LearningUnit',
|
||||
learningContents: [],
|
||||
minutes: 0,
|
||||
parentLearningSequence: parentLearningSequence,
|
||||
children: [],
|
||||
last: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function parseLearningSequences (children: CircleChild[]): LearningSequence[] {
|
||||
let learningSequence:LearningSequence | undefined;
|
||||
let learningUnit:LearningUnit | undefined;
|
||||
let learningContent:LearningContent | undefined;
|
||||
let previousLearningContent: LearningContent | undefined;
|
||||
const result:LearningSequence[] = [];
|
||||
|
||||
children.forEach((child) => {
|
||||
if (child.type === 'learnpath.LearningSequence') {
|
||||
if (learningSequence) {
|
||||
if (learningUnit) {
|
||||
learningUnit.last = true;
|
||||
learningSequence.learningUnits.push(learningUnit);
|
||||
}
|
||||
result.push(learningSequence);
|
||||
}
|
||||
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') {
|
||||
if (!learningSequence) {
|
||||
throw new Error('LearningUnit found before LearningSequence');
|
||||
}
|
||||
|
||||
if (learningUnit && learningUnit.learningContents.length) {
|
||||
learningSequence.learningUnits.push(learningUnit);
|
||||
}
|
||||
|
||||
learningUnit = Object.assign(child, {
|
||||
learningContents: [],
|
||||
parentLearningSequence: learningSequence,
|
||||
children: child.children.map((c) => {
|
||||
c.parentLearningUnit = learningUnit;
|
||||
c.parentLearningSequence = learningSequence;
|
||||
return c;
|
||||
})
|
||||
});
|
||||
} else if (child.type === 'learnpath.LearningContent') {
|
||||
if (!learningUnit) {
|
||||
throw new Error('LearningContent found before LearningUnit');
|
||||
}
|
||||
previousLearningContent = learningContent;
|
||||
|
||||
learningContent = Object.assign(child, {
|
||||
parentLearningSequence: learningSequence,
|
||||
parentLearningUnit: learningUnit,
|
||||
previousLearningContent: previousLearningContent,
|
||||
});
|
||||
|
||||
if (previousLearningContent) {
|
||||
previousLearningContent.nextLearningContent = learningContent;
|
||||
}
|
||||
|
||||
learningUnit.learningContents.push(child);
|
||||
}
|
||||
});
|
||||
|
||||
if (learningUnit && learningSequence) {
|
||||
// TypeScript does not get it here...
|
||||
learningUnit.last = true;
|
||||
(learningSequence as LearningSequence).learningUnits.push(learningUnit);
|
||||
result.push(learningSequence);
|
||||
} else {
|
||||
throw new Error('Finished with LearningContent but there is no LearningSequence and LearningUnit');
|
||||
}
|
||||
|
||||
// sum minutes
|
||||
result.forEach((learningSequence) => {
|
||||
learningSequence.minutes = 0;
|
||||
learningSequence.learningUnits.forEach((learningUnit) => {
|
||||
learningUnit.minutes = 0;
|
||||
learningUnit.learningContents.forEach((learningContent) => {
|
||||
learningUnit.minutes += learningContent.minutes;
|
||||
});
|
||||
learningSequence.minutes += learningUnit.minutes;
|
||||
});
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
import * as log from 'loglevel';
|
||||
|
||||
import {defineStore} from 'pinia'
|
||||
|
||||
import type {Circle, CircleChild, CircleCompletion, LearningContent, LearningUnit, LearningUnitQuestion} from '@/types'
|
||||
import {itGet, itPost} from '@/fetchHelpers';
|
||||
import {parseLearningSequences} from '@/services/circle';
|
||||
|
||||
export type CircleStoreState = {
|
||||
circleData: Circle;
|
||||
completionData: CircleCompletion[];
|
||||
currentLearningContent: LearningContent | undefined;
|
||||
currentSelfEvaluation: LearningUnit | undefined;
|
||||
page: 'INDEX' | 'OVERVIEW' | 'LEARNING_CONTENT' | 'SELF_EVALUATION';
|
||||
}
|
||||
|
||||
export const useCircleStore = defineStore({
|
||||
id: 'circle',
|
||||
state: () => {
|
||||
return {
|
||||
circleData: {},
|
||||
completionData: {},
|
||||
currentLearningContent: undefined,
|
||||
currentSelfEvaluation: undefined,
|
||||
page: 'INDEX',
|
||||
} as CircleStoreState;
|
||||
},
|
||||
getters: {
|
||||
flatChildren: (state) => {
|
||||
const result:CircleChild[] = [];
|
||||
state.circleData.learningSequences.forEach((learningSequence) => {
|
||||
learningSequence.learningUnits.forEach((learningUnit) => {
|
||||
learningUnit.children.forEach((learningUnitQuestion) => {
|
||||
result.push(learningUnitQuestion);
|
||||
})
|
||||
learningUnit.learningContents.forEach((learningContent) => {
|
||||
result.push(learningContent);
|
||||
});
|
||||
});
|
||||
});
|
||||
return result;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
async loadCircle(slug: string) {
|
||||
try {
|
||||
this.circleData = await itGet(`/learnpath/api/circle/${slug}/`);
|
||||
this.circleData.learningSequences = parseLearningSequences(this.circleData.children);
|
||||
this.completionData = await itGet(`/api/completion/circle/${this.circleData.translation_key}/`);
|
||||
this.parseCompletionData();
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
return error
|
||||
}
|
||||
},
|
||||
async markCompletion(page: LearningContent | LearningUnitQuestion, flag = true) {
|
||||
try {
|
||||
page.completed = flag;
|
||||
this.completionData = await itPost('/api/completion/circle/mark/', {
|
||||
page_key: page.translation_key,
|
||||
completed: page.completed,
|
||||
});
|
||||
this.parseCompletionData();
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
return error
|
||||
}
|
||||
},
|
||||
parseCompletionData() {
|
||||
this.flatChildren.forEach((page) => {
|
||||
const pageIndex = this.completionData.findIndex((e) => {
|
||||
return e.page_key === page.translation_key;
|
||||
});
|
||||
if (pageIndex >= 0) {
|
||||
page.completed = this.completionData[pageIndex].completed;
|
||||
} else {
|
||||
page.completed = undefined;
|
||||
}
|
||||
});
|
||||
},
|
||||
openLearningContent(learningContent: LearningContent) {
|
||||
this.currentLearningContent = learningContent;
|
||||
this.page = 'LEARNING_CONTENT';
|
||||
},
|
||||
closeLearningContent() {
|
||||
this.currentLearningContent = undefined;
|
||||
this.page = 'INDEX';
|
||||
},
|
||||
openSelfEvaluation(learningUnit: LearningUnit) {
|
||||
this.page = 'SELF_EVALUATION';
|
||||
this.currentSelfEvaluation = learningUnit;
|
||||
},
|
||||
closeSelfEvaluation() {
|
||||
this.page = 'INDEX';
|
||||
this.currentSelfEvaluation = undefined;
|
||||
},
|
||||
calcSelfEvaluationStatus(learningUnit: LearningUnit) {
|
||||
if (learningUnit.children.length > 0) {
|
||||
if (learningUnit.children.every((q) => q.completed)) {
|
||||
return true;
|
||||
}
|
||||
if (learningUnit.children.some((q) => q.completed !== undefined)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
continueFromLearningContent() {
|
||||
if (this.currentLearningContent) {
|
||||
this.markCompletion(this.currentLearningContent, true);
|
||||
|
||||
const nextLearningContent = this.currentLearningContent.nextLearningContent;
|
||||
const currentParent = this.currentLearningContent.parentLearningUnit;
|
||||
const nextParent = nextLearningContent?.parentLearningUnit;
|
||||
|
||||
if (
|
||||
currentParent && currentParent.id &&
|
||||
currentParent.id !== nextParent?.id &&
|
||||
currentParent.children.length > 0
|
||||
) {
|
||||
// go to self evaluation
|
||||
this.openSelfEvaluation(currentParent);
|
||||
} else if (this.currentLearningContent.nextLearningContent) {
|
||||
this.openLearningContent(this.currentLearningContent.nextLearningContent);
|
||||
} else {
|
||||
this.closeLearningContent();
|
||||
}
|
||||
} else {
|
||||
log.error('currentLearningContent is undefined');
|
||||
}
|
||||
},
|
||||
continueFromSelfEvaluation() {
|
||||
if (this.currentSelfEvaluation) {
|
||||
const nextContent = this.currentSelfEvaluation.learningContents[this.currentSelfEvaluation.learningContents.length - 1].nextLearningContent;
|
||||
if (nextContent) {
|
||||
this.openLearningContent(nextContent);
|
||||
} else {
|
||||
this.closeSelfEvaluation();
|
||||
}
|
||||
} else {
|
||||
log.error('currentSelfEvaluation is undefined');
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
export interface LearningContentBlock {
|
||||
type: 'web-based-training' | 'competence' | 'exercise' | 'knowledge';
|
||||
value: {
|
||||
description: string;
|
||||
},
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface VideoBlock {
|
||||
type: 'video';
|
||||
value: {
|
||||
description: string;
|
||||
url: string;
|
||||
},
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface PodcastBlock {
|
||||
type: 'podcast';
|
||||
value: {
|
||||
description: string;
|
||||
url: string;
|
||||
},
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface DocumentBlock {
|
||||
type: 'document';
|
||||
value: {
|
||||
description: string;
|
||||
url: string;
|
||||
},
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface CircleGoal {
|
||||
type: 'goal';
|
||||
value: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface CircleJobSituation {
|
||||
type: 'job_situation';
|
||||
value: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface LearningWagtailPage {
|
||||
id: number;
|
||||
title: string;
|
||||
slug: string;
|
||||
translation_key: string;
|
||||
completed?: boolean;
|
||||
}
|
||||
|
||||
export interface LearningContent extends LearningWagtailPage {
|
||||
type: 'learnpath.LearningContent';
|
||||
minutes: number;
|
||||
contents: (LearningContentBlock | VideoBlock | PodcastBlock | DocumentBlock)[];
|
||||
parentLearningSequence?: LearningSequence;
|
||||
parentLearningUnit?: LearningUnit;
|
||||
nextLearningContent?: LearningContent;
|
||||
previousLearningContent?: LearningContent;
|
||||
}
|
||||
|
||||
export interface LearningUnitQuestion extends LearningWagtailPage {
|
||||
type: 'learnpath.LearningUnitQuestion';
|
||||
parentLearningSequence?: LearningSequence;
|
||||
parentLearningUnit?: LearningUnit;
|
||||
}
|
||||
|
||||
export interface LearningUnit extends LearningWagtailPage {
|
||||
type: 'learnpath.LearningUnit';
|
||||
learningContents: LearningContent[];
|
||||
minutes: number;
|
||||
parentLearningSequence?: LearningSequence;
|
||||
children: LearningUnitQuestion[];
|
||||
last?: boolean;
|
||||
}
|
||||
|
||||
export interface LearningSequence extends LearningWagtailPage {
|
||||
type: 'learnpath.LearningSequence';
|
||||
icon: string;
|
||||
learningUnits: LearningUnit[];
|
||||
minutes: number;
|
||||
}
|
||||
|
||||
export type CircleChild = LearningContent | LearningUnit | LearningSequence | LearningUnitQuestion;
|
||||
|
||||
export interface WagtailCircle extends LearningWagtailPage {
|
||||
type: 'learnpath.Circle';
|
||||
children: CircleChild[];
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface CircleCompletion {
|
||||
id: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
user: number;
|
||||
page_key: string;
|
||||
page_type: string;
|
||||
circle_key: string;
|
||||
completed: boolean;
|
||||
json_data: any;
|
||||
}
|
||||
|
||||
export interface Circle extends LearningWagtailPage {
|
||||
type: 'learnpath.Circle';
|
||||
children: CircleChild[];
|
||||
description: string;
|
||||
learningSequences: LearningSequence[];
|
||||
goals: CircleGoal[];
|
||||
job_situations: CircleJobSituation[];
|
||||
}
|
||||
|
|
@ -1,137 +1,119 @@
|
|||
<script>
|
||||
<script setup lang="ts">
|
||||
import * as log from 'loglevel';
|
||||
|
||||
import MainNavigationBar from '../components/MainNavigationBar.vue';
|
||||
import LearningSequence from '../components/circle/LearningSequence.vue';
|
||||
import { itGet, itPost } from '../fetchHelpers';
|
||||
import MainNavigationBar from '@/components/MainNavigationBar.vue';
|
||||
import LearningSequence from '@/components/circle/LearningSequence.vue';
|
||||
import CircleOverview from '@/components/circle/CircleOverview.vue';
|
||||
import LearningContent from '@/components/circle/LearningContent.vue';
|
||||
|
||||
export default {
|
||||
components: { LearningSequence, MainNavigationBar },
|
||||
props: ['circleSlug',],
|
||||
data() {
|
||||
return {
|
||||
count: 0,
|
||||
circleData: {},
|
||||
learningSequences: [],
|
||||
completionData: {},
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleLearningContentCheckbox(learningContent) {
|
||||
log.debug('toggleLearningContentCheckbox', learningContent);
|
||||
console.log(learningContent);
|
||||
import {onMounted} from 'vue'
|
||||
import {useCircleStore} from '@/stores/circle';
|
||||
import SelfEvaluation from '@/components/circle/SelfEvaluation.vue';
|
||||
|
||||
itPost('/api/completion/complete_learning_content/', {
|
||||
learning_content_key: learningContent.translation_key,
|
||||
}).then((data) => {
|
||||
this.completionData = data;
|
||||
});
|
||||
},
|
||||
createLearningSequences(circleData) {
|
||||
// aggregate wagtail data into LearningSequence > LearningUnit > LearningPackage hierarchy
|
||||
let learningSequence = null;
|
||||
let learningUnit = null;
|
||||
circleData.children.forEach((child) => {
|
||||
// FIXME add error detection if the data does not conform to expectations
|
||||
if(child.type === 'learnpath.LearningSequence') {
|
||||
if(learningSequence) {
|
||||
if(learningUnit) {
|
||||
learningSequence.learningUnits.push(learningUnit);
|
||||
}
|
||||
this.learningSequences.push(learningSequence);
|
||||
}
|
||||
learningSequence = Object.assign(child, { learningUnits: [] });
|
||||
learningUnit = { id: null, title: '', learningContents: [] };
|
||||
} else if(child.type === 'learnpath.LearningUnit') {
|
||||
if(learningUnit && learningUnit.learningContents.length) {
|
||||
learningSequence.learningUnits.push(learningUnit);
|
||||
}
|
||||
learningUnit = Object.assign(child, { learningContents: [] });
|
||||
} else {
|
||||
learningUnit.learningContents.push(child);
|
||||
}
|
||||
});
|
||||
const props = defineProps<{
|
||||
circleSlug: string
|
||||
}>()
|
||||
|
||||
if(learningUnit) {
|
||||
learningSequence.learningUnits.push(learningUnit);
|
||||
}
|
||||
this.learningSequences.push(learningSequence);
|
||||
const circleStore = useCircleStore();
|
||||
|
||||
// sum minutes
|
||||
this.learningSequences.forEach((learningSequence) => {
|
||||
learningSequence.minutes = 0;
|
||||
learningSequence.learningUnits.forEach((learningUnit) => {
|
||||
learningUnit.minutes = 0;
|
||||
learningUnit.learningContents.forEach((learningContent) => {
|
||||
learningUnit.minutes += learningContent.minutes;
|
||||
});
|
||||
learningSequence.minutes += learningUnit.minutes;
|
||||
});
|
||||
});
|
||||
onMounted(async () => {
|
||||
log.info('CircleView.vue mounted');
|
||||
await circleStore.loadCircle(props.circleSlug);
|
||||
});
|
||||
|
||||
log.debug(this.learningSequences);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
log.debug('CircleView mounted', this.circleSlug);
|
||||
itGet(`/learnpath/api/circle/${this.circleSlug}/`).then((data) => {
|
||||
this.circleData = data;
|
||||
this.createLearningSequences(data);
|
||||
|
||||
itGet(`/api/completion/circle/${this.circleData.translation_key}/`).then((completionData) => {
|
||||
this.completionData = completionData;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MainNavigationBar/>
|
||||
<Transition>
|
||||
<div v-if="circleStore.page === 'OVERVIEW'">
|
||||
<CircleOverview :circle-data="circleStore.circleData" @close="circleStore.page = 'INDEX'"/>
|
||||
</div>
|
||||
<div v-else-if="circleStore.page === 'LEARNING_CONTENT'">
|
||||
<LearningContent :key="circleStore.currentLearningContent.translation_key"/>
|
||||
</div>
|
||||
<div v-else-if="circleStore.page === 'SELF_EVALUATION'">
|
||||
<SelfEvaluation :key="circleStore.currentSelfEvaluation.translation_key"/>
|
||||
</div>
|
||||
<div v-else>
|
||||
<MainNavigationBar/>
|
||||
|
||||
<div class="circle">
|
||||
<div class="flex flex-col lg:flex-row">
|
||||
<div class="flex-initial lg:w-128 px-4 py-8 lg:px-8">
|
||||
<h1 class="text-blue-dark text-7xl">
|
||||
{{ circleData.title }}
|
||||
</h1>
|
||||
<div class="circle">
|
||||
<div class="flex flex-col lg:flex-row">
|
||||
<div class="flex-initial lg:w-128 px-4 py-4 lg:px-8 lg:py-8">
|
||||
<h1 class="text-blue-dark text-7xl">
|
||||
{{ circleStore.circleData.title }}
|
||||
</h1>
|
||||
|
||||
<div class="mt-8">
|
||||
<img src="@/assets/circle-analyse.svg" alt="">
|
||||
</div>
|
||||
<div class="mt-8">
|
||||
<img src="@/assets/circle-analyse.svg" alt="">
|
||||
</div>
|
||||
|
||||
<div class="outcome border border-gray-500 mt-8 p-6">
|
||||
<h3 class="text-blue-dark">Das lernst du in diesem Circle.</h3>
|
||||
<div class="prose mt-4">
|
||||
{{ circleData.description }}
|
||||
<div class="border-t-2 border-gray-500 mt-4 lg:hidden">
|
||||
<div
|
||||
class="mt-4 inline-flex items-center"
|
||||
@click="circleStore.page = 'OVERVIEW'"
|
||||
>
|
||||
<it-icon-info class="mr-2"/>
|
||||
Das lernst du in diesem Circle
|
||||
</div>
|
||||
<div class="inline-flex items-center">
|
||||
<it-icon-message class="mr-2"/>
|
||||
Fachexpertin kontaktieren
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hidden lg:block">
|
||||
<div class="block border border-gray-500 mt-8 p-6">
|
||||
<h3 class="text-blue-dark">Das lernst du in diesem Circle.</h3>
|
||||
<div class="prose mt-4">
|
||||
{{ circleStore.circleData.description }}
|
||||
</div>
|
||||
|
||||
<button class="btn-primary mt-4" @click="circleStore.page = 'OVERVIEW'">Erfahre mehr dazu</button>
|
||||
</div>
|
||||
|
||||
<div class="expert border border-gray-500 mt-8 p-6">
|
||||
<h3 class="text-blue-dark">Hast du Fragen?</h3>
|
||||
<div class="prose mt-4">Tausche dich mit der Fachexpertin aus für den Circle Analyse aus.</div>
|
||||
<button class="btn-secondary mt-4">
|
||||
Fachexpertin kontaktieren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn-primary mt-4">Erfahre mehr dazu</button>
|
||||
</div>
|
||||
<div class="flex-auto bg-gray-100 px-4 py-8 lg:px-24">
|
||||
<div
|
||||
v-for="learningSequence in circleStore.circleData.learningSequences"
|
||||
:key="learningSequence.translation_key"
|
||||
>
|
||||
<LearningSequence
|
||||
:learning-sequence="learningSequence"
|
||||
:completion-data="circleStore.completionData"
|
||||
></LearningSequence>
|
||||
</div>
|
||||
|
||||
<div class="expert border border-gray-500 mt-8 p-6">
|
||||
<h3 class="text-blue-dark">Hast du Fragen?</h3>
|
||||
<div class="prose mt-4">Tausche dich mit der Fachexpertin aus für den Circle Analyse aus.</div>
|
||||
<button class="btn-secondary mt-4">
|
||||
Fachexpertin kontaktieren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-auto bg-gray-100 px-4 py-8 lg:px-24">
|
||||
<div v-for="learningSequence in learningSequences">
|
||||
<LearningSequence
|
||||
:learning-sequence="learningSequence" @toggleLearningContentCheckbox="toggleLearningContentCheckbox"
|
||||
:completion-data="completionData"
|
||||
></LearningSequence>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.v-enter-active,
|
||||
.v-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.v-enter-from,
|
||||
.v-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.v-enter-active {
|
||||
transition-delay: 0.3s;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import MainNavigationBar from '@/components/MainNavigationBar.vue';</script>
|
|||
<div class="mt-8 flex flex-col lg:flex-row justify-start gap-4">
|
||||
<router-link class="link text-xl" to="/styleguide">Styelguide</router-link>
|
||||
<a class="link text-xl" href="/login/">Login</a>
|
||||
<router-link class="link text-xl" to="/learningpath/versicherungsvermittlerin">Lernpfad "Versicherungsvermittlerin" (Login benötigt)</router-link>
|
||||
<!-- <router-link class="link text-xl" to="/learningpath/versicherungsvermittlerin">Lernpfad "Versicherungsvermittlerin" (Login benötigt)</router-link>-->
|
||||
<router-link class="link text-xl" to="/circle/analyse">Circle "Analyse" (Login benötigt)</router-link>
|
||||
</div>
|
||||
</main>
|
||||
|
|
|
|||
|
|
@ -49,106 +49,106 @@ function colorBgClass(color: string, value: number) {
|
|||
<div class="mt-8 mb-8 flex flex-col gap-4 flex-wrap lg:flex-row">
|
||||
<div class="inline-flex flex-col">
|
||||
message
|
||||
<it-icon-message class="w-8 h-8" />
|
||||
<it-icon-message/>
|
||||
</div>
|
||||
|
||||
<div class="inline-flex flex-col">
|
||||
arrow-up
|
||||
<it-icon-arrow-up class="w-8 h-8" />
|
||||
<it-icon-arrow-up/>
|
||||
</div>
|
||||
|
||||
<div class="inline-flex flex-col">
|
||||
arrow-down
|
||||
<it-icon-arrow-down class="w-8 h-8" />
|
||||
<it-icon-arrow-down/>
|
||||
</div>
|
||||
|
||||
<div class="inline-flex flex-col">
|
||||
arrow-left
|
||||
<it-icon-arrow-left class="w-8 h-8" />
|
||||
<it-icon-arrow-left/>
|
||||
</div>
|
||||
|
||||
<div class="inline-flex flex-col">
|
||||
arrow-right
|
||||
<it-icon-arrow-right class="w-8 h-8" />
|
||||
<it-icon-arrow-right/>
|
||||
</div>
|
||||
|
||||
<div class="inline-flex flex-col">
|
||||
close
|
||||
<it-icon-close class="w-8 h-8" />
|
||||
<it-icon-close/>
|
||||
</div>
|
||||
|
||||
<div class="inline-flex flex-col">
|
||||
check
|
||||
<it-icon-check class="w-8 h-8" />
|
||||
<it-icon-check/>
|
||||
</div>
|
||||
|
||||
<div class="inline-flex flex-col">
|
||||
info
|
||||
<it-icon-info class="w-8 h-8" />
|
||||
<it-icon-info/>
|
||||
</div>
|
||||
|
||||
<div class="inline-flex flex-col">
|
||||
list
|
||||
<it-icon-list class="w-8 h-8" />
|
||||
<it-icon-list/>
|
||||
</div>
|
||||
|
||||
<div class="inline-flex flex-col">
|
||||
menu
|
||||
<it-icon-menu class="w-8 h-8" />
|
||||
<it-icon-menu/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 mb-8 flex flex-col gap-4 flex-wrap lg:flex-row">
|
||||
<div class="inline-flex flex-col">
|
||||
ls-apply
|
||||
<it-icon-ls-apply class="w-8 h-8" />
|
||||
<it-icon-ls-apply/>
|
||||
</div>
|
||||
|
||||
<div class="inline-flex flex-col">
|
||||
ls-watch
|
||||
<it-icon-ls-watch class="w-8 h-8" />
|
||||
<it-icon-ls-watch/>
|
||||
</div>
|
||||
|
||||
<div class="inline-flex flex-col">
|
||||
ls-test
|
||||
<it-icon-ls-test class="w-8 h-8" />
|
||||
<it-icon-ls-test/>
|
||||
</div>
|
||||
|
||||
<div class="inline-flex flex-col">
|
||||
ls-practice
|
||||
<it-icon-ls-practice class="w-8 h-8" />
|
||||
<it-icon-ls-practice/>
|
||||
</div>
|
||||
|
||||
<div class="inline-flex flex-col">
|
||||
ls-network
|
||||
<it-icon-ls-network class="w-8 h-8" />
|
||||
<it-icon-ls-network/>
|
||||
</div>
|
||||
|
||||
<div class="inline-flex flex-col">
|
||||
ls-start
|
||||
<it-icon-ls-start class="w-8 h-8" />
|
||||
<it-icon-ls-start/>
|
||||
</div>
|
||||
|
||||
<div class="inline-flex flex-col">
|
||||
ls-end
|
||||
<it-icon-ls-end class="w-8 h-8" />
|
||||
<it-icon-ls-end/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 mb-8 flex flex-col gap-4 flex-wrap lg:flex-row">
|
||||
<div class="inline-flex flex-col">
|
||||
smiley-happy
|
||||
<it-icon-smiley-happy class="w-8 h-8" />
|
||||
<it-icon-smiley-happy/>
|
||||
</div>
|
||||
|
||||
<div class="inline-flex flex-col">
|
||||
smiley-thinking
|
||||
<it-icon-smiley-thinking class="w-8 h-8" />
|
||||
<it-icon-smiley-thinking/>
|
||||
</div>
|
||||
|
||||
<div class="inline-flex flex-col">
|
||||
smiley-neutral
|
||||
<it-icon-smiley-neutral class="w-8 h-8" />
|
||||
<it-icon-smiley-neutral/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -214,6 +214,7 @@ function colorBgClass(color: string, value: number) {
|
|||
<button class="btn-primary">Primary</button>
|
||||
<button class="btn-secondary">Secondary</button>
|
||||
<button class="btn-blue">Blue</button>
|
||||
<button class="btn-text">Text</button>
|
||||
<a class="btn-primary inline-block" href="/">Primary Link</a>
|
||||
</div>
|
||||
|
||||
|
|
@ -223,9 +224,7 @@ function colorBgClass(color: string, value: number) {
|
|||
<button disabled class="btn-blue">Blue disabled</button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-4 flex-wrap lg:flex-row lg:w-128 content-center lg:justify-start mb-16">
|
||||
|
||||
|
||||
<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>
|
||||
|
|
@ -242,6 +241,13 @@ function colorBgClass(color: string, value: number) {
|
|||
Button text
|
||||
<it-icon-message class="ml-3 -mr-1 h-5 w-5"></it-icon-message>
|
||||
</button>
|
||||
|
||||
<button type="button"
|
||||
class="btn-text inline-flex items-center px-3 py-2">
|
||||
<it-icon-message class="-ml-1 mr-3 h-5 w-5"></it-icon-message>
|
||||
Button text
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
<h2 class="mt-8 mb-8">Dropdown (Work-in-progress)</h2>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
"exclude": ["src/**/__tests__/*"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"strict": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
|
|
|
|||
|
|
@ -4,6 +4,10 @@
|
|||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"lib": [],
|
||||
"types": ["node", "jsdom"]
|
||||
"types": [
|
||||
"node",
|
||||
"jsdom",
|
||||
"vitest/globals"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,17 +6,17 @@ import vue from '@vitejs/plugin-vue'
|
|||
import alias from '@rollup/plugin-alias'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig(({mode}) => {
|
||||
process.env = {...process.env, ...loadEnv(mode, process.cwd())};
|
||||
export default defineConfig(({ mode }) => {
|
||||
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-'),
|
||||
},
|
||||
},
|
||||
}),
|
||||
// vueI18n({
|
||||
// include: path.resolve(__dirname, './locales/**')
|
||||
|
|
@ -40,6 +40,10 @@ export default defineConfig(({mode}) => {
|
|||
},
|
||||
build: {
|
||||
assetsDir: 'static/vue',
|
||||
}
|
||||
},
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'happy-dom',
|
||||
},
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -4,8 +4,15 @@ set -o errexit
|
|||
set -o pipefail
|
||||
set -o nounset
|
||||
|
||||
# TODO remove after stabilisation
|
||||
python /app/manage.py reset_schema
|
||||
|
||||
python /app/manage.py collectstatic --noinput
|
||||
python /app/manage.py createcachetable
|
||||
python /app/manage.py migrate
|
||||
|
||||
# TODO remove after stabilisation
|
||||
python /app/manage.py create_default_users
|
||||
python /app/manage.py create_default_learning_path
|
||||
|
||||
/usr/local/bin/gunicorn config.asgi --bind 0.0.0.0:80 --chdir=/app -k uvicorn.workers.UvicornWorker
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Generated by Django 3.2.13 on 2022-06-08 13:52
|
||||
# Generated by Django 3.2.13 on 2022-06-22 16:53
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
|
@ -15,52 +15,21 @@ class Migration(migrations.Migration):
|
|||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='UserCircleCompletion',
|
||||
name='CircleCompletion',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('page_key', models.UUIDField()),
|
||||
('page_type', models.CharField(blank=True, default='', max_length=255)),
|
||||
('circle_key', models.UUIDField()),
|
||||
('json_data', models.JSONField(blank=True, default=dict)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='LearningUnitQuestionCompletion',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('question_key', models.UUIDField()),
|
||||
('circle_key', models.UUIDField()),
|
||||
('completed', models.BooleanField(default=True)),
|
||||
('json_data', models.JSONField(blank=True, default=dict)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='LearningContentCompletion',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('learning_content_key', models.UUIDField()),
|
||||
('circle_key', models.UUIDField()),
|
||||
('completed', models.BooleanField(default=True)),
|
||||
('completed', models.BooleanField(default=False)),
|
||||
('json_data', models.JSONField(blank=True, default=dict)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='usercirclecompletion',
|
||||
constraint=models.UniqueConstraint(fields=('user', 'circle_key'), name='unique_user_circle_completion'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='learningunitquestioncompletion',
|
||||
constraint=models.UniqueConstraint(fields=('user', 'question_key'), name='unique_user_question_key'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='learningcontentcompletion',
|
||||
constraint=models.UniqueConstraint(fields=('user', 'learning_content_key'), name='unique_user_learning_content_key'),
|
||||
model_name='circlecompletion',
|
||||
constraint=models.UniqueConstraint(fields=('user', 'page_key'), name='unique_user_page_key'),
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -4,56 +4,25 @@ from django.db.models import UniqueConstraint
|
|||
from vbv_lernwelt.core.models import User
|
||||
|
||||
|
||||
class LearningContentCompletion(models.Model):
|
||||
class CircleCompletion(models.Model):
|
||||
# Page can either be a LearningContent or a LearningUnitQuestion for now
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
learning_content_key = models.UUIDField()
|
||||
|
||||
# Page can either be a LearningContent or a LearningUnitQuestion for now
|
||||
page_key = models.UUIDField()
|
||||
page_type = models.CharField(max_length=255, default='', blank=True)
|
||||
circle_key = models.UUIDField()
|
||||
|
||||
completed = models.BooleanField(default=True)
|
||||
completed = models.BooleanField(default=False)
|
||||
json_data = models.JSONField(default=dict, blank=True)
|
||||
|
||||
class Meta:
|
||||
constraints = [
|
||||
UniqueConstraint(
|
||||
fields=['user', 'learning_content_key', ],
|
||||
name='unique_user_learning_content_key'
|
||||
fields=['user', 'page_key', ],
|
||||
name='unique_user_page_key'
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
class LearningUnitQuestionCompletion(models.Model):
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
question_key = models.UUIDField()
|
||||
circle_key = models.UUIDField()
|
||||
|
||||
completed = models.BooleanField(default=True)
|
||||
json_data = models.JSONField(default=dict, blank=True)
|
||||
|
||||
class Meta:
|
||||
constraints = [
|
||||
UniqueConstraint(
|
||||
fields=['user', 'question_key', ],
|
||||
name='unique_user_question_key'
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
class UserCircleCompletion(models.Model):
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
circle_key = models.UUIDField()
|
||||
|
||||
json_data = models.JSONField(default=dict, blank=True)
|
||||
|
||||
class Meta:
|
||||
constraints = [
|
||||
UniqueConstraint(fields=['user', 'circle_key'], name='unique_user_circle_completion')
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,15 +1,13 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from vbv_lernwelt.completion.models import UserCircleCompletion, LearningContentCompletion
|
||||
from vbv_lernwelt.completion.models import CircleCompletion
|
||||
|
||||
|
||||
class UserCircleCompletionSerializer(serializers.ModelSerializer):
|
||||
class CircleCompletionSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = UserCircleCompletion
|
||||
fields = ['id', 'created_at', 'updated_at', 'user', 'circle_key', 'json_data']
|
||||
model = CircleCompletion
|
||||
fields = [
|
||||
'id', 'created_at', 'updated_at', 'user', 'page_key', 'page_type', 'circle_key',
|
||||
'completed', 'json_data',
|
||||
]
|
||||
|
||||
|
||||
class LearningContentCompletionSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = LearningContentCompletion
|
||||
fields = ['id', 'created_at', 'updated_at', 'user', 'learning_content_key', 'circle_key', 'json_data']
|
||||
|
|
|
|||
|
|
@ -26,30 +26,28 @@ class CompletionApiTestCase(APITestCase):
|
|||
learning_content_key = str(learning_content.translation_key)
|
||||
circle_key = str(learning_content.get_parent().translation_key)
|
||||
|
||||
response = self.client.post(f'/api/completion/complete_learning_content/', {
|
||||
'learning_content_key': learning_content_key
|
||||
mark_url = f'/api/completion/circle/mark/'
|
||||
print(mark_url)
|
||||
|
||||
response = self.client.post(mark_url, {
|
||||
'page_key': learning_content_key,
|
||||
})
|
||||
response_json = response.json()
|
||||
print(json.dumps(response.json(), indent=2))
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response_json['circle_key'], circle_key)
|
||||
self.assertEqual(
|
||||
response_json['json_data']['completed_learning_contents'][learning_content_key]['learning_content_key'],
|
||||
learning_content_key
|
||||
)
|
||||
self.assertEqual(len(response_json), 1)
|
||||
self.assertEqual(response_json[0]['page_key'], learning_content_key)
|
||||
self.assertEqual(response_json[0]['circle_key'], circle_key)
|
||||
self.assertTrue(response_json[0]['completed'])
|
||||
|
||||
# test getting the circle data
|
||||
response = self.client.get(f'/api/completion/user_circle_completion/{circle_key}/')
|
||||
response = self.client.get(f'/api/completion/circle/{circle_key}/')
|
||||
response_json = response.json()
|
||||
print(json.dumps(response.json(), indent=2))
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response_json['circle_key'], circle_key)
|
||||
self.assertEqual(
|
||||
response_json['json_data']['completed_learning_contents'][learning_content_key]['learning_content_key'],
|
||||
learning_content_key
|
||||
)
|
||||
|
||||
|
||||
|
||||
self.assertEqual(len(response_json), 1)
|
||||
self.assertEqual(response_json[0]['page_key'], learning_content_key)
|
||||
self.assertEqual(response_json[0]['circle_key'], circle_key)
|
||||
self.assertTrue(response_json[0]['completed'])
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
from django.urls import path
|
||||
|
||||
from vbv_lernwelt.completion.views import complete_learning_content, request_user_circle_completion
|
||||
from vbv_lernwelt.completion.views import request_circle_completion, mark_circle_completion
|
||||
|
||||
urlpatterns = [
|
||||
path(r"circle/<uuid:circle_key>/", request_user_circle_completion, name="request_user_circle_completion"),
|
||||
path(r"complete_learning_content/", complete_learning_content, name="complete_learning_content"),
|
||||
path(r"circle/<uuid:circle_key>/", request_circle_completion, name="request_circle_completion"),
|
||||
path(r"circle/mark/", mark_circle_completion, name="mark_circle_completion"),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,54 +1,58 @@
|
|||
from datetime import datetime
|
||||
|
||||
import structlog
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
from wagtail.models import Page
|
||||
|
||||
from vbv_lernwelt.completion.models import LearningContentCompletion, UserCircleCompletion
|
||||
from vbv_lernwelt.completion.serializers import UserCircleCompletionSerializer
|
||||
from vbv_lernwelt.learnpath.models import LearningContent
|
||||
from vbv_lernwelt.completion.models import CircleCompletion
|
||||
from vbv_lernwelt.completion.serializers import CircleCompletionSerializer
|
||||
from vbv_lernwelt.learnpath.models import Circle
|
||||
from vbv_lernwelt.learnpath.utils import get_wagtail_type
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
@api_view(['GET'])
|
||||
def request_user_circle_completion(request, circle_key):
|
||||
ucc = UserCircleCompletion.objects.filter(
|
||||
user=request.user,
|
||||
circle_key=circle_key,
|
||||
)
|
||||
def request_circle_completion(request, circle_key):
|
||||
response_data = CircleCompletionSerializer(
|
||||
CircleCompletion.objects.filter(user=request.user, circle_key=circle_key),
|
||||
many=True,
|
||||
).data
|
||||
|
||||
if ucc.count() > 0:
|
||||
return Response(status=200, data=UserCircleCompletionSerializer(ucc.first()).data)
|
||||
else:
|
||||
return Response(status=200, data={})
|
||||
return Response(status=200, data=response_data)
|
||||
|
||||
|
||||
@api_view(['POST'])
|
||||
def complete_learning_content(request):
|
||||
learning_content_key = request.data.get('learning_content_key')
|
||||
learning_content = LearningContent.objects.get(translation_key=learning_content_key)
|
||||
def mark_circle_completion(request):
|
||||
page_key = request.data.get('page_key')
|
||||
completed = request.data.get('completed', True)
|
||||
|
||||
circle_key = learning_content.get_parent().translation_key
|
||||
page = Page.objects.get(translation_key=page_key)
|
||||
page_type = get_wagtail_type(page.specific)
|
||||
circle = Circle.objects.ancestor_of(page).first()
|
||||
|
||||
LearningContentCompletion.objects.get_or_create(
|
||||
cc, created = CircleCompletion.objects.get_or_create(
|
||||
user=request.user,
|
||||
learning_content_key=learning_content_key,
|
||||
circle_key=circle_key,
|
||||
page_key=page_key,
|
||||
circle_key=circle.translation_key,
|
||||
)
|
||||
cc.page_type = page_type
|
||||
cc.completed = completed
|
||||
cc.save()
|
||||
|
||||
ucc, created = UserCircleCompletion.objects.get_or_create(
|
||||
user=request.user,
|
||||
circle_key=circle_key,
|
||||
)
|
||||
ucc.save()
|
||||
response_data = CircleCompletionSerializer(
|
||||
CircleCompletion.objects.filter(user=request.user, circle_key=circle.translation_key),
|
||||
many=True,
|
||||
).data
|
||||
|
||||
logger.debug(
|
||||
'learning content completed',
|
||||
'page completed',
|
||||
label='completion_api',
|
||||
circle_key=circle_key,
|
||||
learning_content_key=learning_content_key,
|
||||
circle_key=circle.translation_key,
|
||||
circle_title=circle.title,
|
||||
page_key=page_key,
|
||||
page_type=page_type,
|
||||
page_title=page.title,
|
||||
user_id=request.user.id,
|
||||
)
|
||||
|
||||
return Response(status=200, data=UserCircleCompletionSerializer(ucc).data)
|
||||
return Response(status=200, data=response_data)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
import djclick as click
|
||||
from django.conf import settings
|
||||
from django.db import transaction, connection
|
||||
|
||||
|
||||
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',
|
||||
"COMMENT ON SCHEMA public IS 'standard public schema';",
|
||||
)
|
||||
|
||||
with transaction.atomic():
|
||||
with connection.cursor() as cursor:
|
||||
for sql in sql_list:
|
||||
cursor.execute(sql)
|
||||
|
||||
|
||||
@click.command()
|
||||
def command():
|
||||
user = settings.DATABASES['default']['USER']
|
||||
print(user)
|
||||
|
||||
reset_schema(db_config_user=user)
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
# Generated by Django 3.2.13 on 2022-06-14 08:51
|
||||
# Generated by Django 3.2.13 on 2022-06-22 15:48
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
|
@ -61,7 +61,7 @@ class Migration(migrations.Migration):
|
|||
('contents', wagtail.fields.StreamField([('video', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock()), ('url', wagtail.blocks.URLBlock())])), ('web_based_training', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock()), ('url', wagtail.blocks.URLBlock())])), ('podcast', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock()), ('url', wagtail.blocks.URLBlock())])), ('competence', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock())])), ('exercise', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock())])), ('document', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock())])), ('knowledge', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock())]))], use_json_field=None)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Learning Unit',
|
||||
'verbose_name': 'Learning Content',
|
||||
},
|
||||
bases=('wagtailcore.page',),
|
||||
),
|
||||
|
|
@ -90,13 +90,22 @@ class Migration(migrations.Migration):
|
|||
name='LearningUnit',
|
||||
fields=[
|
||||
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')),
|
||||
('questions', wagtail.fields.StreamField([('question', wagtail.blocks.CharBlock())], use_json_field=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Learning Unit',
|
||||
},
|
||||
bases=('wagtailcore.page',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='LearningUnitQuestion',
|
||||
fields=[
|
||||
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Learning Unit Question',
|
||||
},
|
||||
bases=('wagtailcore.page',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Topic',
|
||||
fields=[
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ class Circle(Page):
|
|||
], use_json_field=True)
|
||||
|
||||
parent_page_types = ['learnpath.LearningPath']
|
||||
subpage_types = ['learnpath.LearningSequence', 'learnpath.LearningUnit']
|
||||
subpage_types = ['learnpath.LearningSequence', 'learnpath.LearningUnit', 'learnpath.LearningContent']
|
||||
|
||||
content_panels = Page.content_panels + [
|
||||
FieldPanel('description'),
|
||||
|
|
@ -100,7 +100,13 @@ class Circle(Page):
|
|||
|
||||
@classmethod
|
||||
def get_serializer_class(cls):
|
||||
return get_it_serializer_class(cls, field_names=['id', 'title', 'slug', 'type', 'translation_key', 'learning_sequences'])
|
||||
return get_it_serializer_class(
|
||||
cls,
|
||||
field_names=[
|
||||
'id', 'title', 'slug', 'type', 'translation_key', 'learning_sequences', 'children',
|
||||
'description', 'job_situations', 'goals', 'experts',
|
||||
]
|
||||
)
|
||||
|
||||
def full_clean(self, *args, **kwargs):
|
||||
self.slug = find_available_slug(Circle, slugify(self.title, allow_unicode=True))
|
||||
|
|
@ -144,14 +150,6 @@ class LearningUnit(Page):
|
|||
parent_page_types = ['learnpath.Circle']
|
||||
subpage_types = []
|
||||
|
||||
questions = StreamField([
|
||||
('question', blocks.CharBlock()),
|
||||
], use_json_field=True)
|
||||
|
||||
content_panels = Page.content_panels + [
|
||||
FieldPanel('questions'),
|
||||
]
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Learning Unit"
|
||||
|
||||
|
|
@ -160,7 +158,22 @@ class LearningUnit(Page):
|
|||
|
||||
@classmethod
|
||||
def get_serializer_class(cls):
|
||||
return get_it_serializer_class(cls, field_names=['id', 'title', 'slug', 'type', 'translation_key', 'questions'])
|
||||
return get_it_serializer_class(cls, field_names=['id', 'title', 'slug', 'type', 'translation_key', 'children'])
|
||||
|
||||
|
||||
class LearningUnitQuestion(Page):
|
||||
parent_page_types = ['learnpath.LearningUnit']
|
||||
subpage_types = []
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Learning Unit Question"
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.title}"
|
||||
|
||||
@classmethod
|
||||
def get_serializer_class(cls):
|
||||
return get_it_serializer_class(cls, field_names=['id', 'title', 'slug', 'type', 'translation_key', ])
|
||||
|
||||
|
||||
class LearningContent(Page):
|
||||
|
|
@ -200,7 +213,7 @@ class LearningContent(Page):
|
|||
return display_title
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Learning Unit"
|
||||
verbose_name = "Learning Content"
|
||||
|
||||
def full_clean(self, *args, **kwargs):
|
||||
self.slug = find_available_slug(LearningContent, slugify(self.title, allow_unicode=True))
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
import wagtail.api.v2.serializers as wagtail_serializers
|
||||
from rest_framework.fields import SerializerMethodField
|
||||
|
||||
from vbv_lernwelt.learnpath.utils import get_wagtail_type
|
||||
|
||||
|
||||
def get_it_serializer_class(model, field_names):
|
||||
|
|
@ -7,9 +10,15 @@ def get_it_serializer_class(model, field_names):
|
|||
|
||||
class ItTypeField(wagtail_serializers.TypeField):
|
||||
def to_representation(self, obj):
|
||||
name = type(obj)._meta.app_label + '.' + type(obj).__name__
|
||||
name = get_wagtail_type(obj)
|
||||
return name
|
||||
|
||||
|
||||
class ItBaseSerializer(wagtail_serializers.BaseSerializer):
|
||||
type = ItTypeField(read_only=True)
|
||||
children = SerializerMethodField()
|
||||
|
||||
meta_fields = []
|
||||
|
||||
def get_children(self, obj):
|
||||
return [c.specific.get_serializer_class()(c.specific).data for c in obj.get_children()]
|
||||
|
|
|
|||
|
|
@ -4,25 +4,6 @@ from vbv_lernwelt.learnpath.models import Circle, LearningPath
|
|||
from vbv_lernwelt.learnpath.serializer_helpers import get_it_serializer_class
|
||||
|
||||
|
||||
class CircleSerializer(get_it_serializer_class(Circle, [])):
|
||||
children = serializers.SerializerMethodField()
|
||||
|
||||
meta_fields = []
|
||||
|
||||
def get_children(self, obj):
|
||||
return [c.specific.get_serializer_class()(c.specific).data for c in obj.get_children()]
|
||||
|
||||
def get_meta_label(self, obj):
|
||||
return obj._meta.label
|
||||
|
||||
class Meta:
|
||||
model = Circle
|
||||
fields = [
|
||||
'id', 'title', 'slug', 'type', 'translation_key',
|
||||
'children', 'description', 'job_situations', 'goals', 'experts',
|
||||
]
|
||||
|
||||
|
||||
class LearningPathSerializer(get_it_serializer_class(LearningPath, [])):
|
||||
children = serializers.SerializerMethodField()
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ from vbv_lernwelt.core.admin import User
|
|||
from vbv_lernwelt.learnpath.models import LearningPath, Topic, Circle, LearningSequence, LearningContent
|
||||
from vbv_lernwelt.learnpath.tests.learning_path_factories import LearningPathFactory, TopicFactory, CircleFactory, \
|
||||
LearningSequenceFactory, LearningContentFactory, VideoBlockFactory, PodcastBlockFactory, CompetenceBlockFactory, \
|
||||
ExerciseBlockFactory, DocumentBlockFactory, LearningUnitFactory
|
||||
ExerciseBlockFactory, DocumentBlockFactory, LearningUnitFactory, LearningUnitQuestionFactory
|
||||
|
||||
|
||||
def create_default_learning_path(user=None):
|
||||
|
|
@ -98,16 +98,34 @@ Fachspezialisten bei.
|
|||
title='Einleitung Circle "Anlayse"',
|
||||
parent=circe_analyse,
|
||||
minutes=15,
|
||||
contents=[('video', VideoBlockFactory())]
|
||||
contents=[('video', VideoBlockFactory(
|
||||
url='https://www.youtube.com/embed/qhPIfxS2hvI',
|
||||
description='In dieser Circle zeigt dir ein Fachexperte anhand von Kundensituationen, wie du erfolgreich'
|
||||
'den Kundenbedarf ermitteln, analysieren, priorisieren und anschliessend zusammenfassen kannst.'
|
||||
))]
|
||||
)
|
||||
|
||||
LearningSequenceFactory(title='Beobachten', parent=circe_analyse, icon='it-icon-ls-watch')
|
||||
LearningUnitFactory(title='Abischerung der Familie', parent=circe_analyse)
|
||||
lu = LearningUnitFactory(
|
||||
title='Absicherung der Familie',
|
||||
parent=circe_analyse,
|
||||
)
|
||||
LearningUnitQuestionFactory(
|
||||
title="Ich bin in der Lage, mit geeigneten Fragestellungen die Deckung von Versicherungen zu erfassen.",
|
||||
parent=lu
|
||||
)
|
||||
LearningUnitQuestionFactory(
|
||||
title="Zweite passende Frage zu 'Absicherung der Familie'",
|
||||
parent=lu
|
||||
)
|
||||
LearningContentFactory(
|
||||
title='Ermittlung des Kundenbedarfs',
|
||||
parent=circe_analyse,
|
||||
minutes=30,
|
||||
contents=[('podcast', PodcastBlockFactory())]
|
||||
contents=[('podcast', PodcastBlockFactory(
|
||||
description='Die Ermittlung des Kundenbedarfs muss in einem eingehenden Gespräch herausgefunden werden. Höre dazu auch diesen Podcast an.',
|
||||
url='https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/325190984&color=%23ff5500&auto_play=false&hide_related=false&show_comments=true&show_user=true&show_reposts=false&show_teaser=true&visual=true',
|
||||
))]
|
||||
)
|
||||
LearningContentFactory(
|
||||
title='Kundenbedürfnisse erkennen',
|
||||
|
|
@ -123,7 +141,11 @@ Fachspezialisten bei.
|
|||
)
|
||||
|
||||
LearningSequenceFactory(title='Anwenden', parent=circe_analyse, icon='it-icon-ls-apply')
|
||||
LearningUnitFactory(title='Prämien einsparen', parent=circe_analyse)
|
||||
lu = LearningUnitFactory(title='Prämien einsparen', parent=circe_analyse)
|
||||
LearningUnitQuestionFactory(
|
||||
title="Passende Frage zu Anwenden",
|
||||
parent=lu
|
||||
)
|
||||
LearningContentFactory(
|
||||
title='Versicherungsbedarf für Familien',
|
||||
parent=circe_analyse,
|
||||
|
|
@ -137,7 +159,11 @@ Fachspezialisten bei.
|
|||
contents=[('exercise', ExerciseBlockFactory())]
|
||||
)
|
||||
|
||||
LearningUnitFactory(title='Sich selbständig machen', parent=circe_analyse)
|
||||
lu = LearningUnitFactory(title='Sich selbständig machen', parent=circe_analyse)
|
||||
LearningUnitQuestionFactory(
|
||||
title="Passende Frage zu 'Sich selbständig machen'",
|
||||
parent=lu
|
||||
)
|
||||
LearningContentFactory(
|
||||
title='GmbH oder AG',
|
||||
parent=circe_analyse,
|
||||
|
|
@ -151,7 +177,11 @@ Fachspezialisten bei.
|
|||
contents=[('exercise', ExerciseBlockFactory())]
|
||||
)
|
||||
|
||||
LearningUnitFactory(title='Auto verkaufen', parent=circe_analyse)
|
||||
lu = LearningUnitFactory(title='Auto verkaufen', parent=circe_analyse)
|
||||
LearningUnitQuestionFactory(
|
||||
title='Passende Frage zu "Auto verkaufen"',
|
||||
parent=lu
|
||||
)
|
||||
LearningContentFactory(
|
||||
title='Motorfahrzeugversicherung',
|
||||
parent=circe_analyse,
|
||||
|
|
@ -177,8 +207,84 @@ Fachspezialisten bei.
|
|||
contents=[('exercise', ExerciseBlockFactory())]
|
||||
)
|
||||
|
||||
lu = LearningUnitFactory(title='Pensionierung', parent=circe_analyse)
|
||||
LearningUnitQuestionFactory(
|
||||
title='Passende Frage zu "Pensionierung"',
|
||||
parent=lu
|
||||
)
|
||||
LearningContentFactory(
|
||||
title='3-Säulen-Prinzip',
|
||||
parent=circe_analyse,
|
||||
minutes=240,
|
||||
contents=[('competence', CompetenceBlockFactory())]
|
||||
)
|
||||
LearningContentFactory(
|
||||
title='Altersvorsorge',
|
||||
parent=circe_analyse,
|
||||
minutes=240,
|
||||
contents=[('competence', CompetenceBlockFactory())]
|
||||
)
|
||||
LearningContentFactory(
|
||||
title='AHV',
|
||||
parent=circe_analyse,
|
||||
minutes=120,
|
||||
contents=[('document', DocumentBlockFactory())]
|
||||
)
|
||||
LearningContentFactory(
|
||||
title='Altersvorsorge planen',
|
||||
parent=circe_analyse,
|
||||
minutes=120,
|
||||
contents=[('exercise', ExerciseBlockFactory())]
|
||||
)
|
||||
|
||||
lu = LearningUnitFactory(title='Reisen', parent=circe_analyse)
|
||||
LearningUnitQuestionFactory(
|
||||
title='Passende Frage zu "Reisen"',
|
||||
parent=lu
|
||||
)
|
||||
LearningContentFactory(
|
||||
title='Reiseversicherung',
|
||||
parent=circe_analyse,
|
||||
minutes=240,
|
||||
contents=[('competence', CompetenceBlockFactory())]
|
||||
)
|
||||
LearningContentFactory(
|
||||
title='Sorgenfrei reisen',
|
||||
parent=circe_analyse,
|
||||
minutes=120,
|
||||
contents=[('exercise', ExerciseBlockFactory())]
|
||||
)
|
||||
|
||||
lu = LearningUnitFactory(title='Haushalt', parent=circe_analyse)
|
||||
LearningUnitQuestionFactory(
|
||||
title='Passende Frage zu "Haushalt"',
|
||||
parent=lu
|
||||
)
|
||||
LearningContentFactory(
|
||||
title='Privathaftpflicht',
|
||||
parent=circe_analyse,
|
||||
minutes=240,
|
||||
contents=[('competence', CompetenceBlockFactory())]
|
||||
)
|
||||
LearningContentFactory(
|
||||
title='Zusatzversicherung',
|
||||
parent=circe_analyse,
|
||||
minutes=120,
|
||||
contents=[('document', DocumentBlockFactory())]
|
||||
)
|
||||
LearningContentFactory(
|
||||
title='Einen eigenen Haushalt führen',
|
||||
parent=circe_analyse,
|
||||
minutes=120,
|
||||
contents=[('exercise', ExerciseBlockFactory())]
|
||||
)
|
||||
|
||||
LearningSequenceFactory(title='Üben', parent=circe_analyse, icon='it-icon-ls-practice')
|
||||
LearningUnitFactory(title='Kind zieht von zu Hause aus', parent=circe_analyse)
|
||||
lu = LearningUnitFactory(title='Kind zieht von zu Hause aus', parent=circe_analyse)
|
||||
LearningUnitQuestionFactory(
|
||||
title='Passende Frage zu "Kind zieht von zu Hause aus"',
|
||||
parent=lu
|
||||
)
|
||||
LearningContentFactory(
|
||||
title='Hausrat',
|
||||
parent=circe_analyse,
|
||||
|
|
@ -198,6 +304,43 @@ Fachspezialisten bei.
|
|||
contents=[('competence', CompetenceBlockFactory())]
|
||||
)
|
||||
|
||||
LearningSequenceFactory(title='Testen', parent=circe_analyse, icon='it-icon-ls-test')
|
||||
lu = LearningUnitFactory(title='Kind zieht von zu Hause aus "Testen"', parent=circe_analyse)
|
||||
LearningContentFactory(
|
||||
title='Das erwartet dich im Test',
|
||||
parent=circe_analyse,
|
||||
minutes=30,
|
||||
contents=[('document', CompetenceBlockFactory())]
|
||||
)
|
||||
LearningContentFactory(
|
||||
title='Test durchführen',
|
||||
parent=circe_analyse,
|
||||
minutes=30,
|
||||
contents=[('document', CompetenceBlockFactory())]
|
||||
)
|
||||
|
||||
LearningSequenceFactory(title='Vernetzen', parent=circe_analyse, icon='it-icon-ls-network')
|
||||
LearningContentFactory(
|
||||
title='Online Training',
|
||||
parent=circe_analyse,
|
||||
minutes=60,
|
||||
contents=[('document', CompetenceBlockFactory())]
|
||||
)
|
||||
|
||||
LearningSequenceFactory(title='Beenden', parent=circe_analyse, icon='it-icon-ls-end')
|
||||
LearningContentFactory(
|
||||
title='Kompetenzprofil anschauen',
|
||||
parent=circe_analyse,
|
||||
minutes=30,
|
||||
contents=[('document', CompetenceBlockFactory())]
|
||||
)
|
||||
LearningContentFactory(
|
||||
title='Circle "Analyse" abschliessen',
|
||||
parent=circe_analyse,
|
||||
minutes=30,
|
||||
contents=[('document', CompetenceBlockFactory())]
|
||||
)
|
||||
|
||||
# learning_unit = LearningUnitFactory.create(title='** Einstieg Video"', parent=circle_4)
|
||||
# video_url = "https://www.vbv.ch/fileadmin/vbv/Videos/Statements_Externe/Janos_M/Testimonial_Janos_Mischler_PositiveEffekte.mp4"
|
||||
# video_title = "Ausbildung ist pflicht"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import factory
|
||||
import wagtail_factories
|
||||
|
||||
from vbv_lernwelt.learnpath.models import LearningPath, Topic, Circle, LearningSequence, LearningContent, LearningUnit
|
||||
from vbv_lernwelt.learnpath.models import LearningPath, Topic, Circle, LearningSequence, LearningContent, LearningUnit, \
|
||||
LearningUnitQuestion
|
||||
from vbv_lernwelt.learnpath.models_learning_unit_content import VideoBlock, WebBasedTrainingBlock, PodcastBlock, \
|
||||
CompetenceBlock, ExerciseBlock, DocumentBlock, KnowledgeBlock
|
||||
|
||||
|
|
@ -42,8 +42,15 @@ class LearningUnitFactory(wagtail_factories.PageFactory):
|
|||
model = LearningUnit
|
||||
|
||||
|
||||
class LearningUnitQuestionFactory(wagtail_factories.PageFactory):
|
||||
title = 'Frage zu Lerneinheit'
|
||||
|
||||
class Meta:
|
||||
model = LearningUnitQuestion
|
||||
|
||||
|
||||
class LearningContentFactory(wagtail_factories.PageFactory):
|
||||
title = "Herzlich Willkommen"
|
||||
title = 'Lerninhalt'
|
||||
|
||||
class Meta:
|
||||
model = LearningContent
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
def get_wagtail_type(obj):
|
||||
return obj._meta.app_label + '.' + type(obj).__name__
|
||||
|
|
@ -8,16 +8,14 @@ from rest_framework.decorators import api_view
|
|||
from rest_framework.response import Response
|
||||
|
||||
from vbv_lernwelt.core.middleware.auth import django_view_authentication_exempt
|
||||
from vbv_lernwelt.learnpath.models import Circle
|
||||
from vbv_lernwelt.learnpath.serializers import CircleSerializer
|
||||
from vbv_lernwelt.learnpath.models import Circle, LearningPath
|
||||
from vbv_lernwelt.learnpath.serializers import CircleSerializer, LearningPathSerializer
|
||||
from vbv_lernwelt.learnpath.serializers import LearningPathSerializer
|
||||
|
||||
|
||||
@api_view(['GET'])
|
||||
def circle_view(request, slug):
|
||||
circle = Circle.objects.get(slug=slug)
|
||||
serializer = CircleSerializer(circle)
|
||||
serializer = Circle.get_serializer_class()(circle)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ example:
|
|||
|
||||
class icon_arrow_up extends HTMLElement {
|
||||
connectedCallback() {
|
||||
this.innerHTML = `<svg width="30" height="30" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
this.innerHTML = `<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.9383 10L23.7001 18.9614C23.916 19.2016 23.9084 19.5731 23.6828 19.8038C23.4573 20.0344 23.0941 20.0422 22.8593 19.8214L14.9383 11.72L7.01742 19.8214C6.86685 19.9754 6.6474 20.0356 6.44173 19.9792C6.23605 19.9228 6.0754 19.7585 6.02029 19.5482C5.96518 19.3378 6.02398 19.1134 6.17455 18.9594L14.9383 10Z"
|
||||
/>
|
||||
</svg>
|
||||
|
|
@ -20,7 +20,10 @@ customElements.define('it-icon-arrow-up', icon_arrow_up);
|
|||
|
||||
class {{ svg_icon.classname }} extends HTMLElement {
|
||||
connectedCallback() {
|
||||
this.innerHTML = `{{ svg_icon.content|safe }}`;
|
||||
this.classList.add('it-icon');
|
||||
this.innerHTML = `
|
||||
{{ svg_icon.content|safe }}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@ svg {
|
|||
}
|
||||
|
||||
@layer base {
|
||||
.it-icon {
|
||||
@apply w-8 h-8 inline-block
|
||||
}
|
||||
|
||||
h1 {
|
||||
@apply text-4xl md:text-5xl xl:text-7xl font-bold
|
||||
|
|
@ -67,5 +70,10 @@ svg {
|
|||
disabled:opacity-50 disabled:cursor-not-allowed
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
@apply font-bold py-2 px-4 align-middle inline-block
|
||||
hover:text-gray-700
|
||||
disabled:opacity-50 disabled:cursor-not-allowed
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue