diff --git a/caprover_create_app.py b/caprover_create_app.py index 2306907f..6d776d48 100644 --- a/caprover_create_app.py +++ b/caprover_create_app.py @@ -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', diff --git a/caprover_deploy.sh b/caprover_deploy.sh index aa996df9..f259c6d8 100755 --- a/caprover_deploy.sh +++ b/caprover_deploy.sh @@ -1,5 +1,8 @@ #!/bin/bash +# script should fail when any process returns non zero code +set -ev + # create client npm run build diff --git a/client/.eslintrc.cjs b/client/.eslintrc.cjs index f29db5b6..581abdaf 100644 --- a/client/.eslintrc.cjs +++ b/client/.eslintrc.cjs @@ -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'], } } diff --git a/client/package.json b/client/package.json index 8d703d42..ec5b46fc 100644 --- a/client/package.json +++ b/client/package.json @@ -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" } } diff --git a/client/postcss.config.js b/client/postcss.config.js deleted file mode 100644 index bb637173..00000000 --- a/client/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - plugins: { - 'postcss-import': {}, - autoprefixer: {}, - }, -} diff --git a/client/src/components/__tests__/MainNavigationBar.spec.ts b/client/src/components/__tests__/MainNavigationBar.spec.ts index 0050ea5e..4b81a1b7 100644 --- a/client/src/components/__tests__/MainNavigationBar.spec.ts +++ b/client/src/components/__tests__/MainNavigationBar.spec.ts @@ -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') }) }) diff --git a/client/src/components/circle/CircleOverview.vue b/client/src/components/circle/CircleOverview.vue new file mode 100644 index 00000000..7a9b446a --- /dev/null +++ b/client/src/components/circle/CircleOverview.vue @@ -0,0 +1,53 @@ + + + + + diff --git a/client/src/components/circle/LearningContent.vue b/client/src/components/circle/LearningContent.vue new file mode 100644 index 00000000..8871e809 --- /dev/null +++ b/client/src/components/circle/LearningContent.vue @@ -0,0 +1,78 @@ + + + + + diff --git a/client/src/components/circle/LearningSequence.vue b/client/src/components/circle/LearningSequence.vue index c5993e26..91c2b5c5 100644 --- a/client/src/components/circle/LearningSequence.vue +++ b/client/src/components/circle/LearningSequence.vue @@ -1,17 +1,28 @@ @@ -25,10 +36,17 @@ const contentCompleted = (learningContent) => {
{{ learningSequence.minutes }} Minuten
-
+
{{ learningUnit.title }}
@@ -37,17 +55,46 @@ const contentCompleted = (learningContent) => {
- {{ learningContent.contents[0].type }}: {{ learningContent.title }} + {{ learningContent.contents[0].type }}: {{ learningContent.title }}
-
+
+
+ + Selbsteinschätzung: Ich kann das. +
+
+ + Selbsteinschätzung: Muss ich nochmals anschauen +
+
+ + Selbsteinschätzung +
+
+ +
diff --git a/client/src/components/circle/SelfEvaluation.vue b/client/src/components/circle/SelfEvaluation.vue new file mode 100644 index 00000000..ceeece63 --- /dev/null +++ b/client/src/components/circle/SelfEvaluation.vue @@ -0,0 +1,114 @@ + + + + + diff --git a/client/src/fetchHelpers.js b/client/src/fetchHelpers.ts similarity index 67% rename from client/src/fetchHelpers.js rename to client/src/fetchHelpers.ts index bc397aa1..23fa5b33 100644 --- a/client/src/fetchHelpers.js +++ b/client/src/fetchHelpers.ts @@ -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'}); }; diff --git a/client/src/services/__tests__/circle.spec.ts b/client/src/services/__tests__/circle.spec.ts new file mode 100644 index 00000000..31c4536c --- /dev/null +++ b/client/src/services/__tests__/circle.spec.ts @@ -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); + }) +}) diff --git a/client/src/services/circle.ts b/client/src/services/circle.ts new file mode 100644 index 00000000..9c0178ce --- /dev/null +++ b/client/src/services/circle.ts @@ -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; +} + diff --git a/client/src/stores/circle.ts b/client/src/stores/circle.ts new file mode 100644 index 00000000..557c297c --- /dev/null +++ b/client/src/stores/circle.ts @@ -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'); + } + } + } +}) diff --git a/client/src/types.ts b/client/src/types.ts new file mode 100644 index 00000000..265f9974 --- /dev/null +++ b/client/src/types.ts @@ -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[]; +} diff --git a/client/src/views/CircleView.vue b/client/src/views/CircleView.vue index babf7a2a..b6c049e1 100644 --- a/client/src/views/CircleView.vue +++ b/client/src/views/CircleView.vue @@ -1,137 +1,119 @@ - diff --git a/client/src/views/HomeView.vue b/client/src/views/HomeView.vue index 4686655a..de8b39f2 100644 --- a/client/src/views/HomeView.vue +++ b/client/src/views/HomeView.vue @@ -10,7 +10,7 @@ import MainNavigationBar from '@/components/MainNavigationBar.vue';
Styelguide Login - Lernpfad "Versicherungsvermittlerin" (Login benötigt) + Circle "Analyse" (Login benötigt)
diff --git a/client/src/views/StyelGuideView.vue b/client/src/views/StyelGuideView.vue index 2a0a814b..8fb83e88 100644 --- a/client/src/views/StyelGuideView.vue +++ b/client/src/views/StyelGuideView.vue @@ -49,106 +49,106 @@ function colorBgClass(color: string, value: number) {
message - +
arrow-up - +
arrow-down - +
arrow-left - +
arrow-right - +
close - +
check - +
info - +
list - +
menu - +
ls-apply - +
ls-watch - +
ls-test - +
ls-practice - +
ls-network - +
ls-start - +
ls-end - +
smiley-happy - +
smiley-thinking - +
smiley-neutral - +
@@ -214,6 +214,7 @@ function colorBgClass(color: string, value: number) { + Primary Link
@@ -223,9 +224,7 @@ function colorBgClass(color: string, value: number) {
-
- - +
+ + +

Dropdown (Work-in-progress)

diff --git a/client/tsconfig.app.json b/client/tsconfig.app.json index cdbea1d7..98e2e534 100644 --- a/client/tsconfig.app.json +++ b/client/tsconfig.app.json @@ -4,6 +4,7 @@ "exclude": ["src/**/__tests__/*"], "compilerOptions": { "composite": true, + "strict": true, "baseUrl": ".", "paths": { "@/*": ["./src/*"] diff --git a/client/tsconfig.vitest.json b/client/tsconfig.vitest.json index d080d611..ee2193e4 100644 --- a/client/tsconfig.vitest.json +++ b/client/tsconfig.vitest.json @@ -4,6 +4,10 @@ "compilerOptions": { "composite": true, "lib": [], - "types": ["node", "jsdom"] + "types": [ + "node", + "jsdom", + "vitest/globals" + ] } } diff --git a/client/vite.config.ts b/client/vite.config.ts index 6a68449a..8fc64adf 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -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 ' 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', + }, } }) diff --git a/compose/django/docker_start.sh b/compose/django/docker_start.sh index 9c06873f..05e535bd 100644 --- a/compose/django/docker_start.sh +++ b/compose/django/docker_start.sh @@ -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 diff --git a/server/vbv_lernwelt/completion/migrations/0001_initial.py b/server/vbv_lernwelt/completion/migrations/0001_initial.py index 31c21594..eab576fb 100644 --- a/server/vbv_lernwelt/completion/migrations/0001_initial.py +++ b/server/vbv_lernwelt/completion/migrations/0001_initial.py @@ -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'), ), ] diff --git a/server/vbv_lernwelt/completion/models.py b/server/vbv_lernwelt/completion/models.py index 306a2004..d46c28bf 100644 --- a/server/vbv_lernwelt/completion/models.py +++ b/server/vbv_lernwelt/completion/models.py @@ -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') - ] diff --git a/server/vbv_lernwelt/completion/serializers.py b/server/vbv_lernwelt/completion/serializers.py index eaa5a4af..92d5bef4 100644 --- a/server/vbv_lernwelt/completion/serializers.py +++ b/server/vbv_lernwelt/completion/serializers.py @@ -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'] diff --git a/server/vbv_lernwelt/completion/tests/test_api.py b/server/vbv_lernwelt/completion/tests/test_api.py index e257c11b..6d21296e 100644 --- a/server/vbv_lernwelt/completion/tests/test_api.py +++ b/server/vbv_lernwelt/completion/tests/test_api.py @@ -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']) diff --git a/server/vbv_lernwelt/completion/urls.py b/server/vbv_lernwelt/completion/urls.py index 51d373c5..3530697b 100644 --- a/server/vbv_lernwelt/completion/urls.py +++ b/server/vbv_lernwelt/completion/urls.py @@ -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//", request_user_circle_completion, name="request_user_circle_completion"), - path(r"complete_learning_content/", complete_learning_content, name="complete_learning_content"), + path(r"circle//", request_circle_completion, name="request_circle_completion"), + path(r"circle/mark/", mark_circle_completion, name="mark_circle_completion"), ] diff --git a/server/vbv_lernwelt/completion/views.py b/server/vbv_lernwelt/completion/views.py index 98f7b708..da7479dc 100644 --- a/server/vbv_lernwelt/completion/views.py +++ b/server/vbv_lernwelt/completion/views.py @@ -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) diff --git a/server/vbv_lernwelt/core/management/commands/reset_schema.py b/server/vbv_lernwelt/core/management/commands/reset_schema.py new file mode 100644 index 00000000..d4faa57d --- /dev/null +++ b/server/vbv_lernwelt/core/management/commands/reset_schema.py @@ -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) diff --git a/server/vbv_lernwelt/learnpath/migrations/0001_initial.py b/server/vbv_lernwelt/learnpath/migrations/0001_initial.py index 2cfc21af..04760b7b 100644 --- a/server/vbv_lernwelt/learnpath/migrations/0001_initial.py +++ b/server/vbv_lernwelt/learnpath/migrations/0001_initial.py @@ -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=[ diff --git a/server/vbv_lernwelt/learnpath/models.py b/server/vbv_lernwelt/learnpath/models.py index fdc5896b..5f057775 100644 --- a/server/vbv_lernwelt/learnpath/models.py +++ b/server/vbv_lernwelt/learnpath/models.py @@ -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)) diff --git a/server/vbv_lernwelt/learnpath/serializer_helpers.py b/server/vbv_lernwelt/learnpath/serializer_helpers.py index 81bd41f9..bfaeb48b 100644 --- a/server/vbv_lernwelt/learnpath/serializer_helpers.py +++ b/server/vbv_lernwelt/learnpath/serializer_helpers.py @@ -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()] diff --git a/server/vbv_lernwelt/learnpath/serializers.py b/server/vbv_lernwelt/learnpath/serializers.py index 8bae0dba..8e868ee3 100644 --- a/server/vbv_lernwelt/learnpath/serializers.py +++ b/server/vbv_lernwelt/learnpath/serializers.py @@ -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() diff --git a/server/vbv_lernwelt/learnpath/tests/create_default_learning_path.py b/server/vbv_lernwelt/learnpath/tests/create_default_learning_path.py index c57591e3..28b3737e 100644 --- a/server/vbv_lernwelt/learnpath/tests/create_default_learning_path.py +++ b/server/vbv_lernwelt/learnpath/tests/create_default_learning_path.py @@ -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" diff --git a/server/vbv_lernwelt/learnpath/tests/learning_path_factories.py b/server/vbv_lernwelt/learnpath/tests/learning_path_factories.py index 9518358f..260153e3 100644 --- a/server/vbv_lernwelt/learnpath/tests/learning_path_factories.py +++ b/server/vbv_lernwelt/learnpath/tests/learning_path_factories.py @@ -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 diff --git a/server/vbv_lernwelt/learnpath/utils.py b/server/vbv_lernwelt/learnpath/utils.py new file mode 100644 index 00000000..a4e77eee --- /dev/null +++ b/server/vbv_lernwelt/learnpath/utils.py @@ -0,0 +1,2 @@ +def get_wagtail_type(obj): + return obj._meta.app_label + '.' + type(obj).__name__ diff --git a/server/vbv_lernwelt/learnpath/views.py b/server/vbv_lernwelt/learnpath/views.py index 066d43c9..3aa6b4b8 100644 --- a/server/vbv_lernwelt/learnpath/views.py +++ b/server/vbv_lernwelt/learnpath/views.py @@ -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) diff --git a/server/vbv_lernwelt/static/icons/icon-ls-start.svg b/server/vbv_lernwelt/static/icons/icon-ls-start.svg index f554f302..5f982338 100644 --- a/server/vbv_lernwelt/static/icons/icon-ls-start.svg +++ b/server/vbv_lernwelt/static/icons/icon-ls-start.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/server/vbv_lernwelt/templates/learnpath/icons.html b/server/vbv_lernwelt/templates/learnpath/icons.html index f03cc06b..7381fde4 100644 --- a/server/vbv_lernwelt/templates/learnpath/icons.html +++ b/server/vbv_lernwelt/templates/learnpath/icons.html @@ -4,7 +4,7 @@ example: class icon_arrow_up extends HTMLElement { connectedCallback() { - this.innerHTML = ` + this.innerHTML = ` @@ -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 }} + `; } } diff --git a/tailwind/input.css b/tailwind/input.css index 4f68bab9..8f5c57af 100644 --- a/tailwind/input.css +++ b/tailwind/input.css @@ -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 + } }