Merge branch 'develop' of bitbucket.org:iterativ/vbv_lernwelt into develop

# Conflicts:
#	client/src/views/LearningPathView.vue
This commit is contained in:
Lorenz Padberg 2022-08-30 14:53:20 +02:00
commit dd18cf895b
76 changed files with 743 additions and 1230 deletions

View File

@ -9,7 +9,7 @@ pipelines:
services: services:
- postgres - postgres
caches: caches:
- vbvpip - pip
script: script:
- source ./env/bitbucket/prepare_for_test.sh - source ./env/bitbucket/prepare_for_test.sh
- pip install -r server/requirements/requirements-dev.txt - pip install -r server/requirements/requirements-dev.txt
@ -27,14 +27,15 @@ pipelines:
- cypress/**/*.mp4 - cypress/**/*.mp4
caches: caches:
- node - node
- vbvpip - pip
- cypress - cypress
script: script:
- export IT_SERVE_VUE=false
- export IT_ALLOW_LOCAL_LOGIN=true
- source ./env/bitbucket/prepare_for_test.sh - source ./env/bitbucket/prepare_for_test.sh
- python -m venv vbvvenv
- source vbvvenv/bin/activate
- pip install -r server/requirements/requirements-dev.txt - pip install -r server/requirements/requirements-dev.txt
- npm install - npm install
- npm run build
- ./prepare_server_cypress.sh --start-background - ./prepare_server_cypress.sh --start-background
- npm run cypress:ci - npm run cypress:ci
# - npm run build # - npm run build
@ -52,20 +53,20 @@ pipelines:
deployment: prod deployment: prod
trigger: manual trigger: manual
script: script:
- ./deploy.sh --commit "$BITBUCKET_COMMIT" --token "$DEPLOY_TOKEN" --url https://myservicecrm.swisscom.ch/deploy-iesc-bKVAkQguPDVi - ./deploy.sh --commit "$BITBUCKET_COMMIT" --token "$DEPLOY_TOKEN" --url https://myservicecrm.swisscom.ch/deploy-iesc-xxx
custom: custom:
deploy-preprod: deploy-preprod:
- step: - step:
name: Deploy to PREPROD name: Deploy to PREPROD
deployment: preprod deployment: preprod
script: script:
- ./deploy.sh --commit "$BITBUCKET_COMMIT" --token "$DEPLOY_TOKEN" --url https://preprod.myservicecrm.ch/deploy-iesc-bKVAkQguPDVi - ./deploy.sh --commit "$BITBUCKET_COMMIT" --token "$DEPLOY_TOKEN" --url https://preprod.myservicecrm.ch/deploy-iesc-xxx
deploy-api: deploy-api:
- step: - step:
name: Deploy to API name: Deploy to API
deployment: api deployment: api
script: script:
- ./deploy.sh --commit "$BITBUCKET_COMMIT" --token "$DEPLOY_TOKEN" --url https://api.myservicecrm.ch/deploy-iesc-bKVAkQguPDVi - ./deploy.sh --commit "$BITBUCKET_COMMIT" --token "$DEPLOY_TOKEN" --url https://api.myservicecrm.ch/deploy-iesc-xxx
definitions: definitions:
caches: caches:

96
client/src/colors.json Normal file
View File

@ -0,0 +1,96 @@
{
"transparent": "transparent",
"current": "currentColor",
"white": "#ffffff",
"black": "#0A0A0A",
"blue": {
"200": "#D2E0FA",
"300": "#B4CCFA",
"400": "#7FACFA",
"500": "#558AED",
"600": "#3D6DCC",
"700": "#2957A6",
"800": "#153D7A",
"900": "#00224D"
},
"sky": {
"200": "#CCECFF",
"300": "#A1DCFF",
"400": "#72CAFF",
"500": "#41B5FA",
"600": "#2198DF",
"700": "#007AC3",
"800": "#13609C",
"900": "#0D4169"
},
"green": {
"200": "#C4F5D9",
"300": "#A4EBC2",
"400": "#78DEA3",
"500": "#54CE8B",
"600": "#5BB782",
"700": "#419B73",
"800": "#3E8261",
"900": "#2C5C49"
},
"red": {
"200": "#F5DCD7",
"300": "#F2C1B8",
"400": "#F09D8D",
"500": "#EF7D68",
"600": "#DD6751",
"700": "#C65540",
"800": "#A14737",
"900": "#803729"
},
"orange": {
"200": "#FFDECC",
"300": "#FFC3A1",
"400": "#FFA776",
"500": "#FE955A",
"600": "#E68B4E",
"700": "#CC7B3D",
"800": "#A66635",
"900": "#7D4E2A"
},
"yellow": {
"200": "#FAEBC8",
"300": "#FADD98",
"400": "#FAC852",
"500": "#FBBA20",
"600": "#E3A81D",
"700": "#C7912C",
"800": "#AB7B22",
"900": "#876422"
},
"stone": {
"200": "#F2F1EB",
"300": "#E5E3DA",
"400": "#CBC9BE",
"500": "#B8B6AC",
"600": "#ABA99F",
"700": "#959388",
"800": "#7B7A71",
"900": "#616059"
},
"gray": {
"200": "#EDF2F6",
"300": "#E0E5EC",
"400": "#C5D2DA",
"500": "#B1C1CA",
"600": "#9EADB5",
"700": "#87949D",
"800": "#6F787E",
"900": "#585F63"
},
"slate": {
"200": "#E6F0FA",
"300": "#D7E6F5",
"400": "#C8DDF0",
"500": "#AFC8DF",
"600": "#97B3CD",
"700": "#7D95AC",
"800": "#62788C",
"900": "#4D5E6E"
}
}

View File

@ -1,11 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import * as log from 'loglevel'; import * as log from 'loglevel';
import { onMounted, reactive} from 'vue'; import {onMounted, reactive} from 'vue';
import { useUserStore } from '@/stores/user'; import {useUserStore} from '@/stores/user';
import { useLearningPathStore } from '@/stores/learningPath'; import {useLearningPathStore} from '@/stores/learningPath';
import { useRoute, useRouter } from 'vue-router'; import {useRoute, useRouter} from 'vue-router';
import { useAppStore } from '@/stores/app'; import {useAppStore} from '@/stores/app';
import IconLogout from "@/components/icons/IconLogout.vue"; import IconLogout from "@/components/icons/IconLogout.vue";
import IconSettings from "@/components/icons/IconSettings.vue"; import IconSettings from "@/components/icons/IconSettings.vue";
import ItDropdown from "@/components/ui/ItDropdown.vue"; import ItDropdown from "@/components/ui/ItDropdown.vue";
@ -146,6 +146,7 @@ const profileDropdownData = [
v-if="userStore.loggedIn" v-if="userStore.loggedIn"
to="/messages" to="/messages"
class="nav-item flex flex-row items-center" class="nav-item flex flex-row items-center"
data-cy="messages-link"
> >
<it-icon-message class="w-8 h-8 mr-6"/> <it-icon-message class="w-8 h-8 mr-6"/>
</router-link> </router-link>
@ -213,6 +214,7 @@ const profileDropdownData = [
<router-link <router-link
to="/messages" to="/messages"
class="nav-item flex flex-row items-center" class="nav-item flex flex-row items-center"
data-cy="messages-link"
> >
<it-icon-message class="w-8 h-8 mr-6"/> <it-icon-message class="w-8 h-8 mr-6"/>
</router-link> </router-link>

View File

@ -5,6 +5,8 @@ import * as _ from 'lodash'
import {useCircleStore} from '@/stores/circle'; import {useCircleStore} from '@/stores/circle';
import * as log from 'loglevel'; import * as log from 'loglevel';
import colors from "@/colors.json";
const circleStore = useCircleStore(); const circleStore = useCircleStore();
function someFinished(learningSequence) { function someFinished(learningSequence) {
@ -22,7 +24,7 @@ function allFinished(learningSequence) {
} }
onMounted(async () => { onMounted(async () => {
log.info('CircleDiagram mounted'); log.debug('CircleDiagram mounted');
render(); render();
}); });
@ -55,17 +57,6 @@ const pieData = computed(() => {
return {} return {}
}) })
const blue900 = '#00224D',
blue700 = '#1A5197',
gray100 = '#EDF2F6',
gray300 = '#E0E5EC',
gray500 = '#B1C1CA',
sky400 = '#72CAFF',
sky500 = '#41B5FA',
green500 = '#54CE8B',
green400 = '#78DEA3'
const width = 450 const width = 450
const height = 450 const height = 450
const radius = Math.min(width, height) / 2.4 const radius = Math.min(width, height) / 2.4
@ -89,29 +80,29 @@ function render() {
.attr("d", "M -1 0 l 10 0 M 0 -1 l 0 10") .attr("d", "M -1 0 l 10 0 M 0 -1 l 0 10")
.attr('transform', 'rotate(-90, 10, 0)') .attr('transform', 'rotate(-90, 10, 0)')
.attr('stroke-width', arrowStrokeWidth) .attr('stroke-width', arrowStrokeWidth)
.attr('stroke', gray500) .attr('stroke', colors.gray[500])
const g = svg.append('g').attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')') const g = svg.append('g').attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')')
function getColor(d) { function getColor(d) {
let color = gray300 let color = colors.gray[300];
if (d.someFinished) { if (d.someFinished) {
color = sky500 color = colors.sky[500];
} }
if (d.allFinished) { if (d.allFinished) {
color = green500 color = colors.green[500];
} }
return color return color
} }
function getHoverColor(d) { function getHoverColor(d) {
let color = gray100 let color = colors.gray[200];
if (d.someFinished) { if (d.someFinished) {
color = sky400 color = colors.sky[400];
} }
if (d.allFinished) { if (d.allFinished) {
color = green400 color = colors.green[400];
} }
return color return color
} }
@ -131,7 +122,7 @@ function render() {
const learningSequences = g.selectAll('.learningSegmentArc').data(pieData.value).enter().append('g') const learningSequences = g.selectAll('.learningSegmentArc').data(pieData.value).enter().append('g')
.attr('class', 'learningSegmentArc') .attr('class', 'learningSegmentArc')
.attr('role', 'button') .attr('role', 'button')
.attr('fill', gray300) .attr('fill', colors.gray[300])
learningSequences learningSequences
@ -172,7 +163,7 @@ function render() {
const learningSequenceText = learningSequences const learningSequenceText = learningSequences
.append('text') .append('text')
.attr('fill', blue900) .attr('fill', colors.blue[900])
.style('font-size', 15) .style('font-size', 15)
.text((d) => { .text((d) => {
return d.title return d.title
@ -231,7 +222,7 @@ function render() {
all_arows.last().remove() all_arows.last().remove()
//Draw arrow paths //Draw arrow paths
arrows.append('path').attr('fill', gray500).attr('d', arrow) arrows.append('path').attr('fill', colors.gray[500]).attr('d', arrow)
return svg return svg
} }
@ -243,7 +234,7 @@ function render() {
<pre hidden>{{ pieData }}</pre> <pre hidden>{{ pieData }}</pre>
<pre hidden>{{ render() }}</pre> <pre hidden>{{ render() }}</pre>
<svg class="circle-visualization h-full"> <svg class="circle-visualization h-full">
<circle v-if="!circleStore.circle" :cx="width / 2" :cy="height / 2" :r="radius" :color="gray300"/> <circle v-if="!circleStore.circle" :cx="width / 2" :cy="height / 2" :r="radius" :color="colors.gray[300]"/>
<circle v-if="!circleStore.circle" :cx="width / 2" :cy="height / 2" :r="radius / 2.5" color="white"/> <circle v-if="!circleStore.circle" :cx="width / 2" :cy="height / 2" :r="radius / 2.5" color="white"/>
</svg> </svg>
</div> </div>

View File

@ -30,6 +30,7 @@ const block = computed(() => {
<button <button
type="button" type="button"
class="btn-text inline-flex items-center px-3 py-2 font-normal" class="btn-text inline-flex items-center px-3 py-2 font-normal"
data-cy="close-learnng-content"
@click="circleStore.closeLearningContent()" @click="circleStore.closeLearningContent()"
> >
<it-icon-arrow-left class="-ml-1 mr-1 h-5 w-5"></it-icon-arrow-left> <it-icon-arrow-left class="-ml-1 mr-1 h-5 w-5"></it-icon-arrow-left>
@ -41,6 +42,7 @@ const block = computed(() => {
<button <button
type="button" type="button"
class="btn-blue" class="btn-blue"
data-cy="complete-and-continue"
@click="circleStore.continueFromLearningContent()" @click="circleStore.continueFromLearningContent()"
> >
Abschliessen und weiter Abschliessen und weiter

View File

@ -1,7 +1,7 @@
<script> <script>
import * as d3 from 'd3'; import * as d3 from 'd3';
import {useLearningPathStore} from '../../stores/learningPath'; import { useLearningPathStore } from '../../stores/learningPath';
import colors from '@/colors.json';
export default { export default {
props: { props: {
@ -86,35 +86,25 @@ export default {
mounted() { mounted() {
const circleWidth = this.vertical ? 60 : 200 const circleWidth = this.vertical ? 60 : 200
const radius = (circleWidth * 0.8) / 2 const radius = (circleWidth * 0.8) / 2
const blue900 = '#00224D',
blue700 = '#1A5197',
gray100 = '#EDF2F6',
gray300 = '#E0E5EC',
gray500 = '#B1C1CA',
sky400 = '#72CAFF',
sky500 = '#41B5FA',
green500 = '#54CE8B',
green400 = '#78DEA3'
function getColor(d) { function getColor(d) {
let color = gray300 let color = colors.gray[300]
if (d.someFinished) { if (d.someFinished) {
color = sky500 color = colors.sky[500]
} }
if (d.allFinished) { if (d.allFinished) {
color = green500 color = colors.green[500]
} }
return color return color
} }
function getHoverColor(d) { function getHoverColor(d) {
let color = gray100 let color = colors.gray[200]
if (d.someFinished) { if (d.someFinished) {
color = sky400 color = colors.sky[400]
} }
if (d.allFinished) { if (d.allFinished) {
color = green400 color = colors.green[400]
} }
return color return color
} }
@ -129,6 +119,13 @@ export default {
.enter() .enter()
.append('g') .append('g')
.attr('class', 'circle') .attr('class', 'circle')
.attr('data-cy', (d) => {
if (this.vertical) {
return `circle-${d.slug}-vertical`;
} else {
return `circle-${d.slug}`
}
})
.on('mouseover', function (d, i) { .on('mouseover', function (d, i) {
d3.select(this) d3.select(this)
.selectAll('.learningSegmentArc') .selectAll('.learningSegmentArc')
@ -169,7 +166,7 @@ export default {
.enter() .enter()
.append('g') .append('g')
.attr('class', 'learningSegmentArc') .attr('class', 'learningSegmentArc')
.attr('fill', gray300) .attr('fill', colors.gray[300])
arcs arcs
.transition() .transition()
@ -184,7 +181,7 @@ export default {
const circlesText = circle_groups const circlesText = circle_groups
.append('text') .append('text')
.attr('fill', blue900) .attr('fill', colors.blue[900])
.style('font-size', 19) .style('font-size', 19)
.text((d) => d.title) .text((d) => d.title)
@ -244,7 +241,7 @@ export default {
const topicTitles = topicGroups const topicTitles = topicGroups
.append('text') .append('text')
.attr('fill', blue900) .attr('fill', colors.blue[900])
.style('font-size', 16) .style('font-size', 16)
.text((d) => d.title) .text((d) => d.title)

View File

@ -30,44 +30,56 @@ const allFinished = computed(() => {
return false; return false;
}) })
const learningSequenceBorderClass = computed(() => {
let result = [];
if (props.learningSequence && circleStore.circle) {
if (allFinished.value) {
result = ['border-l-4', 'border-l-green-500']
} else if (someFinished.value) {
result = ['border-l-4', 'border-l-sky-500']
} else {
result = ['border-l-gray-500']
}
}
return result;
});
</script> </script>
<template> <template>
<div class="mb-8 learning-sequence" :id="learningSequence.translation_key"> <div class="mb-8 learning-sequence" :id="learningSequence.translation_key">
<div class="flex items-center gap-4 mb-2"> <div class="flex items-center gap-4 mb-2 text-blue-900">
<component :is="learningSequence.icon" /> <component :is="learningSequence.icon" />
<h3 class="text-xl"> <h3 class="text-xl font-semibold">
{{ learningSequence.title }} {{ learningSequence.title }}
</h3> </h3>
<div>{{ learningSequence.minutes }} Minuten</div> <div>{{ learningSequence.minutes }} Minuten</div>
</div> </div>
<div <div
class="bg-white px-4 border border-gray-500 border-l-4" class="bg-white px-4 lg:px-6 border border-gray-500"
:class="{ :class="learningSequenceBorderClass"
'border-l-green-500': allFinished,
'border-l-sky-500': someFinished,
'border-l-gray-500': !someFinished,
}"
> >
<div <div
v-for="learningUnit in learningSequence.learningUnits" v-for="learningUnit in learningSequence.learningUnits"
:key="learningUnit.id" :key="learningUnit.id"
class="pt-3" class="pt-3 lg:pt-6"
> >
<div class="pb-3 flex gap-4" v-if="learningUnit.title"> <div class="pb-3 lg:pg-6 flex gap-4 text-blue-900" v-if="learningUnit.title">
<div class="font-bold">{{ learningUnit.title }}</div> <div class="font-semibold">{{ learningUnit.title }}</div>
<div>{{ learningUnit.minutes }} Minuten</div> <div>{{ learningUnit.minutes }} Minuten</div>
</div> </div>
<div <div
v-for="learningContent in learningUnit.learningContents" v-for="learningContent in learningUnit.learningContents"
:key="learningContent.id" :key="learningContent.id"
class="flex items-center gap-4 pb-3" class="flex items-center gap-4 pb-3 lg:pb-6"
> >
<ItCheckbox <ItCheckbox
:modelValue="learningContent.completed" :modelValue="learningContent.completed"
@click="toggleCompleted(learningContent)" @click="toggleCompleted(learningContent)"
:data-cy="`lc-${learningContent.slug}`"
> >
<span @click.stop="circleStore.openLearningContent(learningContent)">{{ learningContent.contents[0].type }}: {{ learningContent.title }}</span> <span @click.stop="circleStore.openLearningContent(learningContent)">{{ learningContent.contents[0].type }}: {{ learningContent.title }}</span>
</ItCheckbox> </ItCheckbox>
@ -80,24 +92,24 @@ const allFinished = computed(() => {
> >
<div <div
v-if="circleStore.calcSelfEvaluationStatus(learningUnit)" v-if="circleStore.calcSelfEvaluationStatus(learningUnit)"
class="flex items-center gap-4 pb-3" class="flex items-center gap-4 pb-3 lg:pb-6"
> >
<it-icon-smiley-happy/> <it-icon-smiley-happy class="w-8 h-8 flex-none"/>
<span>Selbsteinschätzung: Ich kann das.</span> <div>Selbsteinschätzung: Ich kann das.</div>
</div> </div>
<div <div
v-else-if="circleStore.calcSelfEvaluationStatus(learningUnit) === false" v-else-if="circleStore.calcSelfEvaluationStatus(learningUnit) === false"
class="flex items-center gap-4 pb-3" class="flex items-center gap-4 pb-3 lg:pb-6"
> >
<it-icon-smiley-thinking/> <it-icon-smiley-thinking class="w-8 h-8 flex-none"/>
<span>Selbsteinschätzung: Muss ich nochmals anschauen</span> <div>Selbsteinschätzung: Muss ich nochmals anschauen</div>
</div> </div>
<div <div
v-else v-else
class="flex items-center gap-4 pb-3" class="flex items-center gap-4 pb-3 lg:pb-6"
> >
<it-icon-smiley-neutral/> <it-icon-smiley-neutral class="w-8 h-8 flex-none"/>
<span>Selbsteinschätzung</span> <div>Selbsteinschätzung</div>
</div> </div>
</div> </div>

View File

@ -1,10 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>$Title$</title>
</head>
<body>
$END$
</body>
</html>

View File

@ -30,6 +30,8 @@ defineEmits(['update:modelValue'])
bg-[url('/static/icons/icon-checkbox-checked.svg')] bg-[url('/static/icons/icon-checkbox-checked.svg')]
hover:bg-[url('/static/icons/icon-checkbox-checked-hover.svg')] hover:bg-[url('/static/icons/icon-checkbox-checked-hover.svg')]
disabled:opacity-50 disabled:opacity-50
cy-checkbox
cy-checkbox-checked
" "
></div> ></div>
<div <div
@ -40,9 +42,13 @@ defineEmits(['update:modelValue'])
bg-contain bg-contain
bg-[url('/static/icons/icon-checkbox-unchecked.svg')] bg-[url('/static/icons/icon-checkbox-unchecked.svg')]
hover:bg-[url('/static/icons/icon-checkbox-unchecked-hover.svg')] hover:bg-[url('/static/icons/icon-checkbox-unchecked-hover.svg')]
cy-checkbox
cy-checkbox-unchecked
" "
></div> ></div>
<div class="flex-auto pl-4"><slot></slot></div> <div class="flex-auto pl-4">
<slot></slot>
</div>
</div> </div>
</template> </template>

View File

@ -33,6 +33,7 @@ const closeModal = () => {
<Transition mode="in-out"> <Transition mode="in-out">
<div <div
v-if="show" v-if="show"
data-cy="full-screen-modal"
class="px-4 py-16 lg:px-16 lg:py-24 fixed top-0 overflow-y-scroll bg-white h-full w-full"> class="px-4 py-16 lg:px-16 lg:py-24 fixed top-0 overflow-y-scroll bg-white h-full w-full">
<button <button
type="button" type="button"

View File

@ -1,13 +1,21 @@
import {createApp} from 'vue' import { createApp } from 'vue'
import {createPinia} from 'pinia' import { createPinia } from 'pinia'
import * as log from 'loglevel'
import {setupI18n} from './i18n' // import {setupI18n} from './i18n'
import App from './App.vue' import App from './App.vue'
import router from './router' import router from './router'
import '../tailwind.css' import '../tailwind.css'
const i18n = setupI18n() if (window.location.href.indexOf('localhost') >= 0) {
log.setLevel('trace')
} else {
log.setLevel('warn')
}
// const i18n = setupI18n()
const app = createApp(App) const app = createApp(App)
// todo: define lang setup // todo: define lang setup
@ -15,6 +23,6 @@ const app = createApp(App)
app.use(createPinia()) app.use(createPinia())
app.use(router) app.use(router)
app.use(i18n) // app.use(i18n)
app.mount('#app') app.mount('#app')

View File

@ -1,18 +1,18 @@
import * as log from 'loglevel'; import * as log from 'loglevel'
import {defineStore} from 'pinia' import { defineStore } from 'pinia'
import type {LearningContent, LearningUnit, LearningUnitQuestion} from '@/types' import type { LearningContent, LearningUnit, LearningUnitQuestion } from '@/types'
import type {Circle} from '@/services/circle' import type { Circle } from '@/services/circle'
import {itGet, itPost} from '@/fetchHelpers'; import { itGet, itPost } from '@/fetchHelpers'
import {useAppStore} from '@/stores/app'; import { useAppStore } from '@/stores/app'
import {useLearningPathStore} from '@/stores/learningPath'; import { useLearningPathStore } from '@/stores/learningPath'
export type CircleStoreState = { export type CircleStoreState = {
circle: Circle | undefined; circle: Circle | undefined
currentLearningContent: LearningContent | undefined; currentLearningContent: LearningContent | undefined
currentSelfEvaluation: LearningUnit | undefined; currentSelfEvaluation: LearningUnit | undefined
page: 'INDEX' | 'OVERVIEW' | 'LEARNING_CONTENT' | 'SELF_EVALUATION'; page: 'INDEX' | 'OVERVIEW' | 'LEARNING_CONTENT' | 'SELF_EVALUATION'
} }
export const useCircleStore = defineStore({ export const useCircleStore = defineStore({
@ -115,7 +115,14 @@ export const useCircleStore = defineStore({
// go to self evaluation // go to self evaluation
this.openSelfEvaluation(currentParent); this.openSelfEvaluation(currentParent);
} else if (this.currentLearningContent.nextLearningContent) { } else if (this.currentLearningContent.nextLearningContent) {
this.openLearningContent(this.currentLearningContent.nextLearningContent); if (
this.currentLearningContent.parentLearningSequence &&
this.currentLearningContent.parentLearningSequence.id === nextLearningContent?.parentLearningSequence?.id
) {
this.openLearningContent(this.currentLearningContent.nextLearningContent);
} else {
this.closeLearningContent();
}
} else { } else {
this.closeLearningContent(); this.closeLearningContent();
} }
@ -126,8 +133,13 @@ export const useCircleStore = defineStore({
continueFromSelfEvaluation() { continueFromSelfEvaluation() {
if (this.currentSelfEvaluation) { if (this.currentSelfEvaluation) {
const nextContent = this.currentSelfEvaluation.learningContents[this.currentSelfEvaluation.learningContents.length - 1].nextLearningContent; const nextContent = this.currentSelfEvaluation.learningContents[this.currentSelfEvaluation.learningContents.length - 1].nextLearningContent;
if (nextContent) { if (nextContent) {
this.openLearningContent(nextContent); if (this.currentSelfEvaluation?.parentLearningSequence?.id === nextContent?.parentLearningSequence?.id) {
this.openLearningContent(nextContent);
} else {
this.closeSelfEvaluation();
}
} else { } else {
this.closeSelfEvaluation(); this.closeSelfEvaluation();
} }

View File

@ -33,87 +33,105 @@ onMounted(async () => {
@closemodal="circleStore.page = 'INDEX'" @closemodal="circleStore.page = 'INDEX'"
/> />
</Teleport> </Teleport>
<Transition mode="out-in"> <Transition mode="out-in">
<div v-if="circleStore.page === 'LEARNING_CONTENT'"> <div v-if="circleStore.page === 'LEARNING_CONTENT'">
<LearningContent :key="circleStore.currentLearningContent.translation_key"/> <LearningContent :key="circleStore.currentLearningContent.translation_key"/>
</div> </div>
<div v-else-if="circleStore.page === 'SELF_EVALUATION'"> <div v-else-if="circleStore.page === 'SELF_EVALUATION'">
<SelfEvaluation :key="circleStore.currentSelfEvaluation.translation_key"/> <SelfEvaluation :key="circleStore.currentSelfEvaluation.translation_key"/>
</div> </div>
<div v-else> <div v-else>
<div class="circle"> <div class="circle-container">
<div class="flex flex-col lg:flex-row"> <div class="circle">
<div class="flex-initial lg:w-128 px-4 py-4 lg:px-8 lg:pt-4"> <div class="flex flex-col lg:flex-row">
<router-link <div class="flex-initial lg:w-128 px-4 py-4 lg:px-8 lg:pt-4 bg-white">
to="/learningpath/versicherungsvermittlerin" <router-link
class="btn-text inline-flex items-center px-3 py-4 font-normal" to="/learningpath/versicherungsvermittlerin"
> class="btn-text inline-flex items-center px-3 py-4 font-normal"
<it-icon-arrow-left class="-ml-1 mr-1 h-5 w-5"></it-icon-arrow-left> >
<span class="inline">zurück zum Lernpfad</span> <it-icon-arrow-left class="-ml-1 mr-1 h-5 w-5"></it-icon-arrow-left>
</router-link> <span class="inline">zurück zum Lernpfad</span>
</router-link>
<h1 class="text-blue-dark text-7xl"> <h1 class="text-blue-dark text-7xl" data-cy="circle-title">
{{ circleStore.circle?.title }} {{ circleStore.circle?.title }}
</h1> </h1>
<div class="w-full mt-8"> <div class="w-full mt-8">
<CircleDiagram></CircleDiagram> <CircleDiagram></CircleDiagram>
</div>
<div class="border-t-2 border-gray-500 mt-4 lg:hidden">
<div
class="mt-4 inline-flex items-center"
@click="circleStore.page = 'OVERVIEW'"
>
<it-icon-info class="mr-2"/>
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.circle?.description }}
</div> </div>
<button class="btn-primary mt-4" @click="circleStore.page = 'OVERVIEW'">Erfahre mehr dazu</button> <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.circle?.description }}
</div>
<button class="btn-primary mt-4 text-xl" @click="circleStore.page = 'OVERVIEW'">Erfahre mehr dazu
</button>
</div>
<div class="expert border border-gray-500 mt-8 p-6">
<h3 class="text-blue-dark">Hast du Fragen?</h3>
<div class="prose mt-4">Tausche dich mit der Fachexpertin aus für den Circle Analyse aus.</div>
<button class="btn-secondary mt-4 text-xl">
Fachexpertin kontaktieren
</button>
</div>
</div>
</div> </div>
<div class="expert border border-gray-500 mt-8 p-6"> <div class="flex-auto bg-gray-200 px-4 py-8 lg:px-24">
<h3 class="text-blue-dark">Hast du Fragen?</h3> <div
<div class="prose mt-4">Tausche dich mit der Fachexpertin aus für den Circle Analyse aus.</div> v-for="learningSequence in circleStore.circle?.learningSequences || []"
<button class="btn-secondary mt-4"> :key="learningSequence.translation_key"
Fachexpertin kontaktieren >
</button> <LearningSequence
</div> :learning-sequence="learningSequence"
</div> ></LearningSequence>
</div> </div>
<div class="flex-auto bg-gray-200 px-4 py-8 lg:px-24"> </div>
<div
v-for="learningSequence in circleStore.circle?.learningSequences || []"
:key="learningSequence.translation_key"
>
<LearningSequence
:learning-sequence="learningSequence"
></LearningSequence>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </Transition>
</Transition>
</div> </div>
</template> </template>
<style scoped> <style lang="postcss" scoped>
.circle-container {
background: linear-gradient(
to right,
white 0%,
white 50%,
theme(colors.gray.200) 50%,
theme(colors.gray.200) 100%
);
}
.circle {
max-width: 1440px;
margin: 0 auto;
}
.v-enter-active, .v-enter-active,
.v-leave-active { .v-leave-active {

View File

@ -10,7 +10,7 @@ const userStore = useUserStore();
<template> <template>
<main class="px-8 py-8 lg:px-12 lg:py-12 bg-gray-200"> <main class="px-8 py-8 lg:px-12 lg:py-12 bg-gray-200">
<h1>Willkommen, {{userStore.first_name}}</h1> <h1 data-cy="welcome-message">Willkommen, {{userStore.first_name}}</h1>
<h2 class="mt-12">Deine Kurse</h2> <h2 class="mt-12">Deine Kurse</h2>

View File

@ -46,16 +46,23 @@ onMounted(async () => {
<div class="flex flex-col h-max"> <div class="flex flex-col h-max">
<div class="bg-white py-8 flex flex-col"> <div class="bg-white py-8 flex flex-col">
<div class="flex justify-end p-3"> <div class="flex justify-end p-3">
<button class="flex items-center" @click="learningPathStore.page = 'OVERVIEW'"> <button
class="flex items-center"
@click="learningPathStore.page = 'OVERVIEW'"
data-cy="show-list-view"
>
<it-icon-list/> <it-icon-list/>
Listen Ansicht anzeigen Listen Ansicht anzeigen
</button> </button>
</div> </div>
<LearningPathDiagram class="max-w-[1680px] w-full" identifier="mainVisualization" <LearningPathDiagram
v-bind:vertical="false"></LearningPathDiagram> class="max-w-[1680px] w-full"
identifier="mainVisualization"
v-bind:vertical="false"
></LearningPathDiagram>
</div> </div>
<h1 class="m-12">{{ learningPathStore.learningPath.title }}</h1> <h1 data-cy="learning-path-title" class="m-12">{{ learningPathStore.learningPath.title }}</h1>
<div <div
class="bg-white m-12 p-8 flex flex-col lg:flex-row divide-y lg:divide-y-0 lg:divide-x divide-gray-500 justify-start"> class="bg-white m-12 p-8 flex flex-col lg:flex-row divide-y lg:divide-y-0 lg:divide-x divide-gray-500 justify-start">
@ -74,11 +81,7 @@ onMounted(async () => {
</div> </div>
</div> </div>
<div class="topic"></div> <div class="topic"></div>
</div> </div>
</div> </div>
</div> </div>
</template> </template>

View File

@ -1,14 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import * as log from 'loglevel'; import * as log from 'loglevel';
import {onMounted} from 'vue'
import {useLearningPathStore} from '@/stores/learningPath'; import {useLearningPathStore} from '@/stores/learningPath';
import {useUserStore} from '@/stores/user'; import {useUserStore} from '@/stores/user';
import LearningPathDiagram from '@/components/circle/LearningPathDiagram.vue'; import LearningPathDiagram from '@/components/circle/LearningPathDiagram.vue';
import ItFullScreenModal from '@/components/ui/ItFullScreenModal.vue'; import ItFullScreenModal from '@/components/ui/ItFullScreenModal.vue';
import {Circle} from "@/services/circle";
log.debug('LearningPathView created'); log.debug('LearningPathView created');

View File

@ -48,6 +48,7 @@ const userStore = useUserStore();
<div> <div>
<input <input
data-cy="login-button"
type="submit" type="submit"
value="Login" value="Login"
class="btn-primary" class="btn-primary"

View File

@ -1,4 +1,4 @@
const colors = require('tailwindcss/colors') const colors = require('./src/colors.json');
module.exports = { module.exports = {
content: [ content: [
@ -18,38 +18,7 @@ module.exports = {
backgroundImage: { backgroundImage: {
} }
}, },
colors: { colors: colors,
transparent: 'transparent',
current: 'currentColor',
'white': '#ffffff',
'black': '#0A0A0A',
'blue': {
700: '#2957A6',
900: '#00224D',
},
'sky': {
400: '#72CAFF',
500: '#41B5FA',
},
'orange': {
500: '#FE955A',
600: '#E68B4E',
},
'green': {
500: '#54CE8B',
600: '#5BB782',
},
'red': {
500: '#EF7D68',
},
'gray': {
200: '#EDF2F6',
300: '#E0E5EC',
500: '#B1C1CA',
700: '#6F787E',
900: '#585F63',
},
}
}, },
safelist: [{ safelist: [{
pattern: /bg-(blue|sky|orange|green|red)-(400|500|600|700)/, pattern: /bg-(blue|sky|orange|green|red)-(400|500|600|700)/,

View File

@ -63,28 +63,28 @@ svg {
} }
.btn-primary { .btn-primary {
@apply font-bold py-2 px-4 align-middle inline-block @apply font-semibold py-2 px-4 align-middle inline-block
bg-blue-900 text-white border-2 border-blue-900 bg-blue-900 text-white border-2 border-blue-900
hover:bg-blue-700 hover:border-blue-700 hover:bg-blue-700 hover:border-blue-700
disabled:opacity-50 disabled:cursor-not-allowed disabled:opacity-50 disabled:cursor-not-allowed
} }
.btn-secondary { .btn-secondary {
@apply font-bold py-2 px-4 align-middle inline-block @apply font-semibold py-2 px-4 align-middle inline-block
bg-white text-blue-900 border-2 border-blue-900 bg-white text-blue-900 border-2 border-blue-900
hover:bg-gray-200 hover:bg-gray-200
disabled:opacity-50 disabled:cursor-not-allowed disabled:opacity-50 disabled:cursor-not-allowed
} }
.btn-blue { .btn-blue {
@apply font-bold py-2 px-4 align-middle inline-block @apply font-semibold py-2 px-4 align-middle inline-block
bg-sky-500 text-blue-900 border-2 border-sky-500 bg-sky-500 text-blue-900 border-2 border-sky-500
hover:bg-sky-400 hover:border-sky-400 hover:bg-sky-400 hover:border-sky-400
disabled:opacity-50 disabled:cursor-not-allowed disabled:opacity-50 disabled:cursor-not-allowed
} }
.btn-text { .btn-text {
@apply font-bold py-2 px-4 align-middle inline-block @apply font-semibold py-2 px-4 align-middle inline-block
hover:text-gray-700 hover:text-gray-700
disabled:opacity-50 disabled:cursor-not-allowed disabled:opacity-50 disabled:cursor-not-allowed
} }

26
cypress.config.js Normal file
View File

@ -0,0 +1,26 @@
const { defineConfig } = require('cypress')
module.exports = defineConfig({
watchForFileChanges: false,
video: false,
viewportWidth: 1280,
viewportHeight: 720,
retries: {
runMode: 1,
openMode: 0,
},
reporter: 'junit',
reporterOptions: {
mochaFile: 'cypress/test-reports/cypress-results-[hash].xml',
toConsole: true,
},
e2e: {
// experimentalSessionAndOrigin: true,
// We've imported your old cypress plugins here.
// You may want to clean this up later by importing these.
setupNodeEvents(on, config) {
return require('./cypress/plugins/index.js')(on, config)
},
baseUrl: 'http://localhost:8001',
},
})

View File

@ -1,14 +0,0 @@
{
"baseUrl": "http://localhost:8001",
"watchForFileChanges": false,
"video": false,
"retries": {
"runMode": 1,
"openMode": 0
},
"reporter": "junit",
"reporterOptions": {
"mochaFile": "cypress/test-reports/cypress-results-[hash].xml",
"toConsole": true
}
}

53
cypress/e2e/circle.cy.js Normal file
View File

@ -0,0 +1,53 @@
import { login } from "./helpers";
describe("circle page", () => {
beforeEach(() => {
cy.manageCommand("cypress_reset");
login("admin", "test");
cy.visit("/circle/analyse");
});
it("can open circle page", () => {
cy.get('[data-cy="circle-title"]').should("contain", "Analyse");
});
it("can toggle learning content", () => {
cy.get('[data-cy="circle-title"]').should("contain", "Analyse");
cy.get(
'[data-cy="lc-ermittlung-des-kundenbedarfs-4"] > .cy-checkbox'
).click();
cy.get(
'[data-cy="lc-ermittlung-des-kundenbedarfs-4"] > .cy-checkbox-checked'
).should("have.class", "cy-checkbox-checked");
// completion data should still be there after reload
cy.reload();
cy.get(
'[data-cy="lc-ermittlung-des-kundenbedarfs-4"] > .cy-checkbox-checked'
).should("have.class", "cy-checkbox-checked");
});
it("can open learning contents and complete them by continuing", () => {
cy.get('[data-cy="lc-ermittlung-des-kundenbedarfs-4"]').click();
cy.get('[data-cy="complete-and-continue"]').click();
cy.get('[data-cy="complete-and-continue"]').click();
cy.get('[data-cy="complete-and-continue"]').click();
cy.get('[data-cy="close-learnng-content"]').click();
cy.get(
'[data-cy="lc-ermittlung-des-kundenbedarfs-4"] > .cy-checkbox-checked'
).should("have.class", "cy-checkbox-checked");
cy.get('[data-cy="lc-kundenanalyse"] > .cy-checkbox-checked').should(
"have.class",
"cy-checkbox-checked"
);
cy.get(
'[data-cy="lc-kundenbedürfnisse-erkennen-2"] > .cy-checkbox-checked'
).should("have.class", "cy-checkbox-checked");
});
});

7
cypress/e2e/helpers.js Normal file
View File

@ -0,0 +1,7 @@
export const login = (username, password) => {
cy.request({
method: 'POST',
url: '/core/login/',
body: { username, password },
})
}

View File

@ -0,0 +1,40 @@
import { login } from "./helpers";
describe("learningPath page", () => {
beforeEach(() => {
cy.manageCommand("cypress_reset");
});
it("can open learningPath page", () => {
login("admin", "test");
cy.visit("/learningpath/versicherungsvermittlerin");
cy.get('[data-cy="learning-path-title"]').should(
"contain",
"Versicherungsvermittler/in"
);
});
it("click on circle on learningPath page will open circle", () => {
login("admin", "test");
cy.visit("/learningpath/versicherungsvermittlerin");
cy.get('[data-cy="circle-analyse"]').click({ force: true });
cy.url().should("include", "/circle/analyse");
cy.get('[data-cy="circle-title"]').should("contain", "Analyse");
});
it("open listView and click on cirle will open circle", () => {
login("admin", "test");
cy.visit("/learningpath/versicherungsvermittlerin");
cy.get('[data-cy="show-list-view"]').click();
cy.get('[data-cy="full-screen-modal"]').should("be.visible");
cy.get('[data-cy="circle-analyse-vertical"]').click({ force: true });
cy.url().should("include", "/circle/analyse");
cy.get('[data-cy="circle-title"]').should("contain", "Analyse");
});
});

47
cypress/e2e/login.cy.js Normal file
View File

@ -0,0 +1,47 @@
import { login } from "./helpers";
describe("login", () => {
beforeEach(() => {
cy.manageCommand("cypress_reset");
});
it("can login to app with username/password", () => {
cy.visit("/");
cy.get("#username").type("admin");
cy.get("#password").type("test");
cy.get('[data-cy="login-button"]').click();
cy.request("/api/core/me").its("status").should("eq", 200);
cy.get('[data-cy="welcome-message"]').should(
"contain",
"Willkommen, Peter"
);
});
it("can login with helper function", () => {
login("admin", "test");
cy.visit("/");
cy.request("/api/core/me").its("status").should("eq", 200);
cy.get('[data-cy="welcome-message"]').should(
"contain",
"Willkommen, Peter"
);
});
it("login will redirect to requestet page", () => {
cy.visit("/learningpath/versicherungsvermittlerin");
cy.get("h1").should("contain", "Login");
cy.get("#username").type("admin");
cy.get("#password").type("test");
cy.get('[data-cy="login-button"]').click();
cy.get('[data-cy="learning-path-title"]').should(
"contain",
"Versicherungsvermittler"
);
});
});

View File

@ -1,148 +0,0 @@
/// <reference types="cypress" />
// Welcome to Cypress!
//
// This spec file contains a variety of sample tests
// for a todo list app that are designed to demonstrate
// the power of writing tests in Cypress.
//
// To learn more about how Cypress works and
// what makes it such an awesome testing tool,
// please read our getting started guide:
// https://on.cypress.io/introduction-to-cypress
describe('example to-do app', () => {
beforeEach(() => {
cy.manageCommand('cypress_reset');
cy.visit('/todo/');
cy.get("#username").type("cypress@example.com");
cy.get("#password").type("test");
cy.get('[data-cy="submit"]').click();
})
it.skip('can access simple todo page', () => {
cy.get('[data-cy="simple-list-title"]').should('contain', 'Todos');
});
// it('displays two todo items by default', () => {
// // We use the `cy.get()` command to get all elements that match the selector.
// // Then, we use `should` to assert that there are two matched items,
// // which are the two default items.
// cy.get('.todo-list li').should('have.length', 2)
//
// // We can go even further and check that the default todos each contain
// // the correct text. We use the `first` and `last` functions
// // to get just the first and last matched elements individually,
// // and then perform an assertion with `should`.
// cy.get('.todo-list li').first().should('have.text', 'Pay electric bill')
// cy.get('.todo-list li').last().should('have.text', 'Walk the dog')
// })
// it('can add new todo items', () => {
// // We'll store our item text in a variable so we can reuse it
// const newItem = 'Feed the cat'
//
// // Let's get the input element and use the `type` command to
// // input our new list item. After typing the content of our item,
// // we need to type the enter key as well in order to submit the input.
// // This input has a data-test attribute so we'll use that to select the
// // element in accordance with best practices:
// // https://on.cypress.io/selecting-elements
// cy.get('[data-test=new-todo]').type(`${newItem}{enter}`)
//
// // Now that we've typed our new item, let's check that it actually was added to the list.
// // Since it's the newest item, it should exist as the last element in the list.
// // In addition, with the two default items, we should have a total of 3 elements in the list.
// // Since assertions yield the element that was asserted on,
// // we can chain both of these assertions together into a single statement.
// cy.get('.todo-list li')
// .should('have.length', 3)
// .last()
// .should('have.text', newItem)
// })
// it('can check off an item as completed', () => {
// // In addition to using the `get` command to get an element by selector,
// // we can also use the `contains` command to get an element by its contents.
// // However, this will yield the <label>, which is lowest-level element that contains the text.
// // In order to check the item, we'll find the <input> element for this <label>
// // by traversing up the dom to the parent element. From there, we can `find`
// // the child checkbox <input> element and use the `check` command to check it.
// cy.contains('Pay electric bill')
// .parent()
// .find('input[type=checkbox]')
// .check()
//
// // Now that we've checked the button, we can go ahead and make sure
// // that the list element is now marked as completed.
// // Again we'll use `contains` to find the <label> element and then use the `parents` command
// // to traverse multiple levels up the dom until we find the corresponding <li> element.
// // Once we get that element, we can assert that it has the completed class.
// cy.contains('Pay electric bill')
// .parents('li')
// .should('have.class', 'completed')
// })
//
// context('with a checked task', () => {
// beforeEach(() => {
// // We'll take the command we used above to check off an element
// // Since we want to perform multiple tests that start with checking
// // one element, we put it in the beforeEach hook
// // so that it runs at the start of every test.
// cy.contains('Pay electric bill')
// .parent()
// .find('input[type=checkbox]')
// .check()
// })
//
// it('can filter for uncompleted tasks', () => {
// // We'll click on the "active" button in order to
// // display only incomplete items
// cy.contains('Active').click()
//
// // After filtering, we can assert that there is only the one
// // incomplete item in the list.
// cy.get('.todo-list li')
// .should('have.length', 1)
// .first()
// .should('have.text', 'Walk the dog')
//
// // For good measure, let's also assert that the task we checked off
// // does not exist on the page.
// cy.contains('Pay electric bill').should('not.exist')
// })
//
// it('can filter for completed tasks', () => {
// // We can perform similar steps as the test above to ensure
// // that only completed tasks are shown
// cy.contains('Completed').click()
//
// cy.get('.todo-list li')
// .should('have.length', 1)
// .first()
// .should('have.text', 'Pay electric bill')
//
// cy.contains('Walk the dog').should('not.exist')
// })
//
// it('can delete all completed tasks', () => {
// // First, let's click the "Clear completed" button
// // `contains` is actually serving two purposes here.
// // First, it's ensuring that the button exists within the dom.
// // This button only appears when at least one task is checked
// // so this command is implicitly verifying that it does exist.
// // Second, it selects the button so we can click it.
// cy.contains('Clear completed').click()
//
// // Then we can make sure that there is only one element
// // in the list and our element does not exist
// cy.get('.todo-list li')
// .should('have.length', 1)
// .should('not.have.text', 'Pay electric bill')
//
// // Finally, make sure that the clear button no longer exists.
// cy.contains('Clear completed').should('not.exist')
// })
// })
})

View File

@ -53,9 +53,16 @@
const _ = Cypress._; const _ = Cypress._;
Cypress.Commands.add('manageCommand', (command, preCommand = '') => { Cypress.Commands.add('manageCommand', (command, preCommand = '') => {
const execCommand = `${preCommand} python3 server/manage.py ${command} --settings=config.settings.test_cypress`; const execCommand = `${preCommand} python server/manage.py ${command} --settings=config.settings.test_cypress`;
console.log(execCommand); console.log(execCommand);
return cy.exec(execCommand); return cy.exec(execCommand, { failOnNonZeroExit: false }).then(result => {
if(result.code) {
throw new Error(`Execution of "${command}" failed
Exit code: ${result.code}
Stdout:\n${result.stdout}
Stderr:\n${result.stderr}`);
}
});
}); });
Cypress.Commands.add('manageShellCommand', (command) => { Cypress.Commands.add('manageShellCommand', (command) => {

View File

@ -3,7 +3,7 @@
# push new version to Docker Hub # push new version to Docker Hub
# > docker push iterativ/vbv-lernwelt-bitbucket # > docker push iterativ/vbv-lernwelt-bitbucket
# run locally with directory mounted # run locally with directory mounted
# > docker run -v $(dirname "$(pwd)"):/src -it iterativ/vbv-lernwelt-bitbucket /bin/bash # > docker run -v "$(pwd)":/src -it iterativ/vbv-lernwelt-bitbucket /bin/bash
FROM python:3.10-bullseye FROM python:3.10-bullseye
MAINTAINER Daniel Egger <daniel.egger@iterativ.ch> MAINTAINER Daniel Egger <daniel.egger@iterativ.ch>

View File

@ -5,13 +5,9 @@
"build": "npm install --prefix client && npm run build --prefix client && npm run build:tailwind --prefix client", "build": "npm install --prefix client && npm run build --prefix client && npm run build:tailwind --prefix client",
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"cypress:open": "cypress open", "cypress:open": "cypress open",
"cypress:run": "cypress run", "cypress:ci": "cypress run"
"cypress:ci": "cypress run --config baseUrl=http://localhost:8001",
"cypress:ci:open": "cypress open --config baseUrl=http://localhost:8001"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/forms": "^0.5.2", "cypress": "^10.6.0"
"@tailwindcss/typography": "^0.5.2",
"cypress": "^9.4.1"
} }
} }

View File

@ -103,7 +103,6 @@ THIRD_PARTY_APPS = [
LOCAL_APPS = [ LOCAL_APPS = [
"vbv_lernwelt.core", "vbv_lernwelt.core",
"vbv_lernwelt.simpletodo",
"vbv_lernwelt.sso", "vbv_lernwelt.sso",
"vbv_lernwelt.learnpath", "vbv_lernwelt.learnpath",
"vbv_lernwelt.completion", "vbv_lernwelt.completion",

View File

@ -4,7 +4,6 @@ import os
os.environ['IT_APP_ENVIRONMENT'] = 'development' os.environ['IT_APP_ENVIRONMENT'] = 'development'
from .base import * # noqa from .base import * # noqa
from .base import env
# https://docs.djangoproject.com/en/dev/ref/settings/#test-runner # https://docs.djangoproject.com/en/dev/ref/settings/#test-runner
TEST_RUNNER = "django.test.runner.DiscoverRunner" TEST_RUNNER = "django.test.runner.DiscoverRunner"

View File

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

View File

@ -34,7 +34,6 @@ urlpatterns = [
path('admin/raise_error/', user_passes_test(lambda u: u.is_superuser, login_url='/login/')(raise_example_error), ), path('admin/raise_error/', user_passes_test(lambda u: u.is_superuser, login_url='/login/')(raise_example_error), ),
path(settings.ADMIN_URL, admin.site.urls), path(settings.ADMIN_URL, admin.site.urls),
path("checkratelimit/", check_rate_limit), path("checkratelimit/", check_rate_limit),
path("todo/", include("vbv_lernwelt.simpletodo.urls")),
path("sso/", include("vbv_lernwelt.sso.urls")), path("sso/", include("vbv_lernwelt.sso.urls")),
path('cms/', include(wagtailadmin_urls)), path('cms/', include(wagtailadmin_urls)),
path('documents/', include(wagtaildocs_urls)), path('documents/', include(wagtaildocs_urls)),
@ -42,18 +41,14 @@ urlpatterns = [
path('learnpath/', include("vbv_lernwelt.learnpath.urls")), path('learnpath/', include("vbv_lernwelt.learnpath.urls")),
path('api/completion/', include("vbv_lernwelt.completion.urls")), path('api/completion/', include("vbv_lernwelt.completion.urls")),
re_path(r'api/core/me/$', me_user_view, name='me_user_view'), re_path(r'api/core/me/$', me_user_view, name='me_user_view'),
re_path(r'core/login/$', django_view_authentication_exempt(vue_login), name='vue_login'),
re_path(r'core/logout/$', vue_logout, name='vue_logout'), re_path(r'core/logout/$', vue_logout, name='vue_logout'),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
if settings.DEBUG: if settings.DEBUG:
# Static file serving when using Gunicorn + Uvicorn for local web socket development # Static file serving when using Gunicorn + Uvicorn for local web socket development
urlpatterns += staticfiles_urlpatterns() urlpatterns += staticfiles_urlpatterns()
if settings.ALLOW_LOCAL_LOGIN:
urlpatterns += [
re_path(r'core/login/$', django_view_authentication_exempt(vue_login), name='vue_login'),
]
# API URLS # API URLS
urlpatterns += [ urlpatterns += [
# API base url # API base url
@ -113,4 +108,3 @@ if settings.DEBUG:
# serve everything else via the vue app # serve everything else via the vue app
urlpatterns += [re_path(r'^.*$', vue_home, name='home')] urlpatterns += [re_path(r'^.*$', vue_home, name='home')]

View File

@ -14,8 +14,8 @@ class CircleCompletion(models.Model):
# Page can either be a LearningContent or a LearningUnitQuestion for now # Page can either be a LearningContent or a LearningUnitQuestion for now
page_key = models.UUIDField() page_key = models.UUIDField()
page_type = models.CharField(max_length=255, default='', blank=True) page_type = models.CharField(max_length=255, default='', blank=True)
circle_key = models.UUIDField() circle_key = models.UUIDField(blank=True, default='')
learning_path_key = models.UUIDField() learning_path_key = models.UUIDField(blank=True, default='')
completed = models.BooleanField(default=False) completed = models.BooleanField(default=False)
json_data = models.JSONField(default=dict, blank=True) json_data = models.JSONField(default=dict, blank=True)

View File

@ -4,25 +4,21 @@ from rest_framework.test import APITestCase
from vbv_lernwelt.core.create_default_users import create_default_users from vbv_lernwelt.core.create_default_users import create_default_users
from vbv_lernwelt.core.models import User from vbv_lernwelt.core.models import User
from vbv_lernwelt.core.tests.helpers import create_locales_for_wagtail
from vbv_lernwelt.learnpath.models import LearningContent from vbv_lernwelt.learnpath.models import LearningContent
from vbv_lernwelt.learnpath.tests.create_default_learning_path import create_default_learning_path from vbv_lernwelt.learnpath.tests.create_simple_test_learning_path import create_simple_test_learning_path
from vbv_lernwelt.learnpath.tests.test_create_default_learning_path import create_locales_for_wagtail
class CompletionApiTestCase(APITestCase): class CompletionApiTestCase(APITestCase):
@classmethod def setUp(self) -> None:
def setUpClass(cls) -> None:
super(CompletionApiTestCase, cls).setUpClass()
create_locales_for_wagtail() create_locales_for_wagtail()
create_default_users() create_default_users()
create_default_learning_path() create_simple_test_learning_path()
def setUp(self) -> None:
self.user = User.objects.get(username='student') self.user = User.objects.get(username='student')
self.client.login(username='student', password='test') self.client.login(username='student', password='test')
def test_completeLearningContent_works(self): def test_completeLearningContent_works(self):
learning_content = LearningContent.objects.get(title='Einleitung Circle "Anlayse"') learning_content = LearningContent.objects.get(title='Einleitung Circle "Unit-Test Circle"')
learning_content_key = str(learning_content.translation_key) learning_content_key = str(learning_content.translation_key)
circle_key = str(learning_content.get_parent().translation_key) circle_key = str(learning_content.get_parent().translation_key)

View File

@ -1,13 +1,17 @@
import djclick as click import djclick as click
from vbv_lernwelt.learnpath.tests.create_default_learning_path import create_default_learning_path, \ from vbv_lernwelt.completion.models import CircleCompletion
from vbv_lernwelt.learnpath.create_default_learning_path import create_default_learning_path, \
delete_default_learning_path delete_default_learning_path
@click.command() @click.command()
@click.option("--customer_language", default="de") @click.option('--reset-learning-path', default=False)
def command(customer_language): def command(reset_learning_path):
print("cypress reset data") print("cypress reset data")
delete_default_learning_path() if reset_learning_path:
create_default_learning_path(skip_locales=True) delete_default_learning_path()
create_default_learning_path(skip_locales=True)
CircleCompletion.objects.all().delete()

View File

@ -0,0 +1,7 @@
from django.conf import settings
from wagtail.models import Locale
def create_locales_for_wagtail():
for language in settings.WAGTAIL_CONTENT_LANGUAGES:
Locale.objects.get_or_create(language_code=language[0])

View File

@ -1,16 +0,0 @@
from django.test import TestCase
from vbv_lernwelt.core.models import User
from vbv_lernwelt.core.tests.factories import UserFactory
from vbv_lernwelt.simpletodo.models import SimpleList
class TestUserCreation(TestCase):
def test_create_user(self):
User(last_name='Sepp').save()
def test_simple(self):
# create_default_learning_path()
self.user = UserFactory()
SimpleList.objects.get_or_create(title='Default', user=self.user)
self.assertTrue(True)

View File

@ -48,20 +48,23 @@ def vue_home(request):
@api_view(['POST']) @api_view(['POST'])
@ensure_csrf_cookie @ensure_csrf_cookie
def vue_login(request): def vue_login(request):
try: if settings.ALLOW_LOCAL_LOGIN:
username = request.data.get('username') try:
password = request.data.get('password') username = request.data.get('username')
if username and password: password = request.data.get('password')
user = authenticate(request, username=username, password=password) if username and password:
if user: user = authenticate(request, username=username, password=password)
login(request, user) if user:
logger.debug('login successful', username=username, email=user.email, label='login') login(request, user)
return Response(UserSerializer(user).data) logger.debug('login successful', username=username, email=user.email, label='login')
except Exception as e: return Response(UserSerializer(user).data)
logger.exception(e) except Exception as e:
logger.exception(e)
logger.debug('login failed', username=username, label='login') logger.debug('login failed', username=username, label='login')
return Response({'success': False}, status=401) return Response({'success': False}, status=401)
else:
return Response({'success': False, 'message': 'ALLOW_LOCAL_LOGIN=false'}, status=403)
@api_view(['GET']) @api_view(['GET'])

View File

@ -1,8 +1,9 @@
import djclick as click import djclick as click
from vbv_lernwelt.learnpath.tests.create_default_learning_path import create_default_learning_path from vbv_lernwelt.learnpath.create_default_learning_path import create_default_learning_path
@click.command() @click.command()
def command(): def command():
create_default_learning_path(skip_locales=True) create_default_learning_path(skip_locales=True)
# create_simple_test_learning_path(skip_locales=True)

View File

@ -1,6 +1,6 @@
import djclick as click import djclick as click
from vbv_lernwelt.learnpath.tests.create_default_learning_path import delete_default_learning_path from vbv_lernwelt.learnpath.create_default_learning_path import delete_default_learning_path
@click.command() @click.command()

View File

@ -1,13 +1,12 @@
# Create your models here. from django.db import models
from django.utils.text import slugify from django.utils.text import slugify
from wagtail import blocks from wagtail import blocks
from wagtail.admin.panels import FieldPanel, StreamFieldPanel
from wagtail.blocks import StreamBlock from wagtail.blocks import StreamBlock
from wagtail.fields import StreamField from wagtail.fields import StreamField
from wagtail.images.blocks import ImageChooserBlock from wagtail.images.blocks import ImageChooserBlock
from wagtail.models import Page, Orderable from wagtail.models import Page, Orderable
from vbv_lernwelt.learnpath.models_competences import *
from vbv_lernwelt.learnpath.models_learning_unit_content import WebBasedTrainingBlock, VideoBlock, PodcastBlock, \ from vbv_lernwelt.learnpath.models_learning_unit_content import WebBasedTrainingBlock, VideoBlock, PodcastBlock, \
CompetenceBlock, ExerciseBlock, DocumentBlock, KnowledgeBlock CompetenceBlock, ExerciseBlock, DocumentBlock, KnowledgeBlock
from vbv_lernwelt.learnpath.serializer_helpers import get_it_serializer_class from vbv_lernwelt.learnpath.serializer_helpers import get_it_serializer_class

View File

@ -1,66 +0,0 @@
from django.db import models
from wagtail.models import Page, Orderable
from modelcluster.fields import ParentalKey
from wagtail.admin.panels import FieldPanel, StreamFieldPanel, InlinePanel
class CompetencePage(Page):
"""This is the page where the competences and Fullfillment criterias are manged
For one Learning Path"""
content_panels = Page.content_panels + [
InlinePanel('competences', label="Competences"),
]
subpage_types = ['learnpath.Circle']
parent_page_types = ['learnpath.LearningPath']
class Meta:
verbose_name = "Learning Path"
def __str__(self):
return f"{self.title}"
class Competence(Orderable):
""" In VBV Terms this is a "Handlungskompetenz"""
category_short = models.CharField(max_length=3, default='')
name = models.CharField(max_length=2048)
competence_page = ParentalKey('learnpath.CompetencePage',
null=True,
blank=True,
on_delete=models.CASCADE,
related_name='competences',
)
def get_short_info(self):
return f"{self.category_short}{self.sort_order}"
def __str__(self):
return f"{self.get_short_info()}: {self.name}"
class Meta:
verbose_name = "Competence"
class FullfillmentCriteria(Orderable):
""" VBV Term Leistungskriterium"""
name = models.CharField(max_length=2048)
competence = models.ForeignKey(Competence, on_delete=models.CASCADE, null=True)
def get_short_info(self):
return f"{self.competence.get_short_info()}.{self.sort_order}"
def __str__(self):
return f"{self.get_short_info()}: {self.name}"
class Meta:
verbose_name = "Fullfillment Criteria"

View File

@ -1,78 +0,0 @@
{
"competences": [
{
"name": "Weiterempfehlung für Neukunden generieren",
"category_short": "A",
"fullfillment_criteria": [
{
"name": "bestehende Kunden so zu beraten, dass sie von diesen weiterempfohlen werden"
},
{
"name": "geeignete Personen wie z.B. Garagisten, Architekten, Treuhänder auf die Vermittlung/Zusammenarbeit anzusprechen"
},
{
"name": "verschiedene Datenquellen wie Internet, Telefonbuch, Handelszeitung, Baugesuche etc. gezielt für die Gewinnung von Neukunden zu benützen"
},
{
"name": "ein beliebiges Gespräch resp. einen bestehenden Kontakt in die Richtung «Versicherung» zu lenken"
},
{
"name": "das Thema Risiko und Sicherheit in einem Gespräch gezielt und auf die Situation des jeweiligen Gesprächspartners bezogen einfliessen zu lassen"
},
{
"name": "im täglichen Kontakt potentielle Kundinnen und Kunden zu erkennen"
}
]
},
{
"name": "Kundengespräche vereinbaren",
"category_short": "A",
"fullfillment_criteria": [
{
"name": "je nach (Neu-) Kunde Form und Ort für das Gespräch festzulegen"
},
{
"name": "sich intern und extern die nötigen Informationen über den (Neu-) Kunde zu beschaffen"
},
{
"name": "die Terminierung auf ein bestimmtes Thema wie z.B. Rechtsschutz, Vorsorge, Krankenversicherung etc. auszurichten"
},
{
"name": "für das zu führende Gespräch eine Agenda zu erstellen"
},
{
"name": "für das zu führende Gespräch geeignete Hilfsmittel und Unterlagen zusammenzustellen"
}, {
"name": "eine Kaltakquise durchzuführen und auf mögliche Einwände reagieren zu können"
}
]
},
{
"name": "Auftritt in den sozialen Medien zeitgemäss halten",
"category_short": "A",
"fullfillment_criteria": [ {
"name": "in Zusammenarbeit mit den IT-Spezialisten und der Marketingabteilung die Inhalte für den zu realisierenden Medienauftritt zielgruppengerecht festzulegen"
},
{
"name": "für die verschiedenen Kundensegmente die passenden sozialen Medien zu definieren"
},
{
"name": "die Inhalte compliant zu halten"
}
]
},
{
"name": "Kundendaten erfassen",
"category_short": "A",
"fullfillment_criteria": []
},
{
"name": "Wünsche, Ziele und Bedürfnisse der Kunden im Gespräch ermitteln",
"category_short": "B",
"fullfillment_criteria": []
}
]
}

View File

@ -1,30 +0,0 @@
import factory
import wagtail_factories
from vbv_lernwelt.learnpath.models_competences import Competence, FullfillmentCriteria, CompetencePage
from vbv_lernwelt.learnpath.tests.learning_path_factories import LearningPathFactory
class CompetencePageFactory(wagtail_factories.PageFactory):
# learning_path = factory.SubFactory(LearningPathFactory)
class Meta:
model = CompetencePage
class CompetenceFactory(factory.django.DjangoModelFactory):
category_short = 'A'
name = "Weiterempfehung für neukunden generieren"
competence_page = factory.SubFactory(CompetencePageFactory)
class Meta:
model = Competence
class FullfilmentCriteriaFactory(factory.django.DjangoModelFactory):
name = 'Bestehende Kunden so zu beraten, dass sie von diesen weiterempfohlen werden'
competence = factory.SubFactory(CompetenceFactory)
class Meta:
model = FullfillmentCriteria

View File

@ -1,23 +0,0 @@
import json
import os.path
from vbv_lernwelt.learnpath.tests.competences_factories import CompetenceFactory, FullfilmentCriteriaFactory
competences_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'competences.json')
def create_default_competences(competences_json=competences_file):
with open(competences_json) as f:
competences_json = json.load(f)
for index, compentence in enumerate(competences_json['competences']):
competence_model = CompetenceFactory(name=compentence['name'], category_short=compentence['category_short'], sort_order=index)
print(competence_model)
for criteria_index, criteria in enumerate(compentence['fullfillment_criteria']):
criteria_model = FullfilmentCriteriaFactory(name=criteria['name'], competence=competence_model, sort_order=criteria_index)
print(criteria_model)

View File

@ -0,0 +1,149 @@
import wagtail_factories
from django.conf import settings
from wagtail.models import Site, Page
from vbv_lernwelt.core.admin import User
from vbv_lernwelt.learnpath.tests.learning_path_factories import LearningPathFactory, TopicFactory, CircleFactory, \
LearningSequenceFactory, LearningContentFactory, VideoBlockFactory, PodcastBlockFactory, CompetenceBlockFactory, \
ExerciseBlockFactory, LearningUnitFactory, LearningUnitQuestionFactory
def create_circle(title, learning_path):
return CircleFactory(
title=title,
parent=learning_path,
description="Unit-Test Circle",
job_situations=[
('job_situation', 'Absicherung der Familie'),
('job_situation', 'Reisen'),
],
goals=[
('goal', '... die heutige Versicherungssituation von Privat- oder Geschäftskunden einzuschätzen.'),
('goal', '... deinem Kunden seine optimale Lösung aufzuzeigen'),
],
experts=[
('person', {'last_name': 'Huggel', 'first_name': 'Patrizia', 'email': 'patrizia.huggel@example.com'}),
]
)
def create_circle_children(circle, title):
LearningSequenceFactory(title='Starten', parent=circle, icon='it-icon-ls-start')
LearningContentFactory(
title=f'Einleitung Circle "{title}"',
parent=circle,
minutes=15,
contents=[('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=circle, icon='it-icon-ls-watch')
lu = LearningUnitFactory(
title='Absicherung der Familie',
parent=circle,
)
LearningUnitQuestionFactory(
title="Ich bin in der Lage, mit geeigneten Fragestellungen die Deckung von Versicherungen zu erfassen.",
parent=lu
)
LearningUnitQuestionFactory(
title="Zweite passende Frage zu 'Absicherung der Familie'",
parent=lu
)
LearningContentFactory(
title='Ermittlung des Kundenbedarfs',
parent=circle,
minutes=30,
contents=[('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',
parent=circle,
minutes=30,
contents=[('competence', CompetenceBlockFactory())]
)
LearningContentFactory(
title='Was braucht eine Familie?',
parent=circle,
minutes=60,
contents=[('exercise', ExerciseBlockFactory(url='/media/web_based_trainings/story-01-a-01-patrizia-marco-sichern-sich-ab-einstieg/scormcontent/index.html'
))]
)
lu = LearningUnitFactory(title='Reisen', parent=circle)
LearningUnitQuestionFactory(
title='Passende Frage zu "Reisen"',
parent=lu
)
LearningContentFactory(
title='Reiseversicherung',
parent=circle,
minutes=240,
contents=[('competence', CompetenceBlockFactory())]
)
LearningContentFactory(
title='Sorgenfrei reisen',
parent=circle,
minutes=120,
contents=[('exercise', ExerciseBlockFactory(
url='/media/web_based_trainings/story-06-a-01-emma-und-ayla-campen-durch-amerika-einstieg/scormcontent/index.html'))]
)
LearningSequenceFactory(title='Beenden', parent=circle, icon='it-icon-ls-end')
LearningContentFactory(
title='Kompetenzprofil anschauen',
parent=circle,
minutes=30,
contents=[('document', CompetenceBlockFactory())]
)
LearningContentFactory(
title='Circle "Analyse" abschliessen',
parent=circle,
minutes=30,
contents=[('document', CompetenceBlockFactory())]
)
def create_simple_test_learning_path(user=None, skip_locales=True):
if user is None:
user = User.objects.get(username='info@iterativ.ch')
site = Site.objects.filter(is_default_site=True).first()
if not site:
site = wagtail_factories.SiteFactory(is_default_site=True)
if settings.APP_ENVIRONMENT == 'development':
site.port = 8000
site.save()
lp = LearningPathFactory(title="Unit-Test Lernpfad", parent=site.root_page)
TopicFactory(title="Unit-Test Topic", is_visible=False, parent=lp)
circle_analyse = create_circle('Unit-Test Circle', lp)
create_circle_children(circle_analyse, 'Unit-Test Circle')
# locales
# if not skip_locales:
# locale_de = Locale.objects.get(language_code='de-CH')
# locale_fr, _ = Locale.objects.get_or_create(language_code='fr-CH')
# LocaleSynchronization.objects.get_or_create(
# locale_id=locale_fr.id,
# sync_from_id=locale_de.id
# )
# locale_it, _ = Locale.objects.get_or_create(language_code='it-CH')
# LocaleSynchronization.objects.get_or_create(
# locale_id=locale_it.id,
# sync_from_id=locale_de.id
# )
# call_command('sync_locale_trees')
# all pages belong to 'admin' by default
Page.objects.update(owner=user)

View File

@ -1,20 +0,0 @@
GET http://localhost:8000/graphql/
Accept: application/json
###
{
page(id: 8) {
children {
__typename
id
title
children {
__typename
id
title
}
}
}
}

View File

@ -1,17 +0,0 @@
{
page(id: 8) {
children {
__typename
id
title
children {
__typename
id
title
}
}
}
}

View File

@ -2,44 +2,30 @@ from rest_framework.test import APITestCase
from vbv_lernwelt.core.admin import User from vbv_lernwelt.core.admin import User
from vbv_lernwelt.core.create_default_users import create_default_users from vbv_lernwelt.core.create_default_users import create_default_users
from vbv_lernwelt.learnpath.models import LearningPath, Circle from vbv_lernwelt.core.tests.helpers import create_locales_for_wagtail
from vbv_lernwelt.learnpath.tests.create_default_learning_path import create_default_learning_path from vbv_lernwelt.learnpath.models import LearningPath
from vbv_lernwelt.learnpath.tests.test_create_default_learning_path import create_locales_for_wagtail from vbv_lernwelt.learnpath.tests.create_simple_test_learning_path import create_simple_test_learning_path
class TestRetrieveLearingPathContents(APITestCase): class TestRetrieveLearingPathContents(APITestCase):
@classmethod def setUp(self) -> None:
def setUpClass(cls) -> None:
super(TestRetrieveLearingPathContents, cls).setUpClass()
create_locales_for_wagtail() create_locales_for_wagtail()
create_default_users() create_default_users()
create_default_learning_path() create_simple_test_learning_path()
def setUp(self) -> None: self.user = User.objects.get(username='student')
qs = LearningPath.objects.filter(title="Versicherungsvermittler/in") self.client.login(username='student', password='test')
self.credentials = {
'username': 'admin',
'password': 'admin'}
user = User.objects.get(username='admin') def test_get_learnpathPage(self):
self.client.force_authenticate(user=user) learning_path = LearningPath.objects.get(slug='unit-test-lernpfad')
self.client.post('/login/', self.credentials, follow=True) response = self.client.get('/learnpath/api/page/unit-test-lernpfad/')
print(response)
def test_get_circle(self):
circle = Circle.objects.get(slug='analyse')
response = self.client.get(f'/wagtailapi/v2/pages/{circle.id}/')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['title'], 'Analyse') data = response.json()
# print(data)
def test_get_circle_has_learning_sequences(self): self.assertEqual(learning_path.title, data['title'])
circle = Circle.objects.get(slug='analyse') # topic and circle
response = self.client.get(f'/wagtailapi/v2/pages/{circle.id}/') self.assertEqual(2, len(data['children']))
self.assertTrue(len(response.data['learning_sequences']) > 2) # circle "unit-test-circle" contents
self.assertEqual(response.data['learning_sequences'][0]['title'], 'Starten') self.assertEqual(13, len(data['children'][1]['children']))
def test_get_circle_has_learning_sequences_learningpackages(self):
circle = Circle.objects.get(slug='analyse')
response = self.client.get(f'/wagtailapi/v2/pages/{circle.id}/')
self.assertTrue(len(response.data['learning_sequences'][0]['learnging_packages']) > 1)
self.assertTrue(response.data['learning_sequences'][0]['learnging_packages'][0]['title'])

View File

@ -1,18 +0,0 @@
from django.test import TestCase
from vbv_lernwelt.learnpath.models_competences import Competence, FullfillmentCriteria
from vbv_lernwelt.learnpath.tests.competences_factories import CompetencePageFactory, CompetenceFactory, \
FullfilmentCriteriaFactory
class TestCompetencesFactories(TestCase):
def test_create_competences_page(self):
CompetencePageFactory()
def test_create_competence(self):
CompetenceFactory(name='Boogie Woogie')
self.assertEqual(Competence.objects.filter(name='Boogie Woogie').count(), 1)
def test_create_fullfillment_criteria(self):
FullfilmentCriteriaFactory(name='shuffle like ...')
self.assertEqual(FullfillmentCriteria.objects.filter(name='shuffle like ...').count(), 1)

View File

@ -1,12 +0,0 @@
from django.test import TestCase
from vbv_lernwelt.learnpath.tests.create_default_competences import create_default_competences
class TestCreateDefaultCompetences(TestCase):
def test_create_default_competeneces(self):
create_default_competences()

View File

@ -1,23 +0,0 @@
from django.conf import settings
from django.test import TestCase
from wagtail.models import Locale
from vbv_lernwelt.core.create_default_users import create_default_users
from vbv_lernwelt.learnpath.models import LearningPath
from vbv_lernwelt.learnpath.tests.create_default_learning_path import create_default_learning_path
class TestCreateDefaultLearningPaths(TestCase):
def setUp(self) -> None:
create_default_users()
create_locales_for_wagtail()
def test_create_learning_path(self):
create_default_learning_path()
qs = LearningPath.objects.filter(title="Versicherungsvermittler/in")
self.assertTrue(qs.exists())
def create_locales_for_wagtail():
for language in settings.WAGTAIL_CONTENT_LANGUAGES:
Locale.objects.get_or_create(language_code=language[0])

View File

@ -1,9 +0,0 @@
# -*- coding: utf-8 -*-
#
# Iterativ GmbH
# http://www.iterativ.ch/
#
# Copyright (c) 2015 Iterativ GmbH. All rights reserved.
#
# Created on 2022-03-29
# @author: lorenz.padberg@iterativ.ch

View File

@ -2,6 +2,7 @@
import glob import glob
from pathlib import Path from pathlib import Path
import structlog
from django.conf import settings from django.conf import settings
from django.shortcuts import render from django.shortcuts import render
from django.views.decorators.cache import cache_page from django.views.decorators.cache import cache_page
@ -11,13 +12,19 @@ from wagtail.models import Page
from vbv_lernwelt.core.middleware.auth import django_view_authentication_exempt from vbv_lernwelt.core.middleware.auth import django_view_authentication_exempt
logger = structlog.get_logger(__name__)
@api_view(['GET']) @api_view(['GET'])
@cache_page(60 * 60 * 8, cache="learning_path_cache") @cache_page(60 * 60 * 8, cache="learning_path_cache")
def page_api_view(request, slug): def page_api_view(request, slug):
page = Page.objects.get(slug=slug, locale__language_code='de-CH') try:
serializer = page.specific.get_serializer_class()(page.specific) page = Page.objects.get(slug=slug, locale__language_code='de-CH')
return Response(serializer.data) serializer = page.specific.get_serializer_class()(page.specific)
return Response(serializer.data)
except Exception as e:
logger.error(e)
return Response({"error": str(e)}, status=404)
@django_view_authentication_exempt @django_view_authentication_exempt

View File

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

@ -1,9 +1,8 @@
from django.conf import settings
from django.test import TestCase from django.test import TestCase
from wagtail.core.models import Collection from wagtail.core.models import Collection
from wagtail.models import Locale
from vbv_lernwelt.core.create_default_users import create_default_users from vbv_lernwelt.core.create_default_users import create_default_users
from vbv_lernwelt.core.tests.helpers import create_locales_for_wagtail
from vbv_lernwelt.media_library.create_default_documents import create_default_collections, create_default_documents from vbv_lernwelt.media_library.create_default_documents import create_default_collections, create_default_documents
from vbv_lernwelt.media_library.models import LibraryDocument from vbv_lernwelt.media_library.models import LibraryDocument
@ -24,8 +23,3 @@ class TestCreateDefaultDocuments(TestCase):
create_default_documents() create_default_documents()
qs = LibraryDocument.objects.all() qs = LibraryDocument.objects.all()
self.assertTrue(qs.exists()) self.assertTrue(qs.exists())
def create_locales_for_wagtail():
for language in settings.WAGTAIL_CONTENT_LANGUAGES:
Locale.objects.get_or_create(language_code=language[0])

View File

@ -1,39 +0,0 @@
# Register your models here.
from django.contrib import admin
from .models import SimpleTask, SimpleList
@admin.register(SimpleList)
class SimpleListAdmin(admin.ModelAdmin):
list_display = [
"title",
"user",
"created",
]
list_filter = [
"user",
]
search_fields = [
"title",
]
@admin.register(SimpleTask)
class SimpleTaskAdmin(admin.ModelAdmin):
date_hierarchy = "deadline"
list_display = [
"title",
"deadline",
"created",
"list",
"done",
]
list_filter = [
"list",
"done",
]
search_fields = [
"title",
"text",
]

View File

@ -1,6 +0,0 @@
from django.apps import AppConfig
class SimpletodoConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "vbv_lernwelt.simpletodo"

View File

@ -1,105 +0,0 @@
# Generated by Django 3.2.12 on 2022-02-03 20:37
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import model_utils.fields
import uuid
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="SimpleList",
fields=[
(
"created",
model_utils.fields.AutoCreatedField(
default=django.utils.timezone.now,
editable=False,
verbose_name="created",
),
),
(
"modified",
model_utils.fields.AutoLastModifiedField(
default=django.utils.timezone.now,
editable=False,
verbose_name="modified",
),
),
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("title", models.CharField(max_length=255)),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"abstract": False,
},
),
migrations.CreateModel(
name="SimpleTask",
fields=[
(
"created",
model_utils.fields.AutoCreatedField(
default=django.utils.timezone.now,
editable=False,
verbose_name="created",
),
),
(
"modified",
model_utils.fields.AutoLastModifiedField(
default=django.utils.timezone.now,
editable=False,
verbose_name="modified",
),
),
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("title", models.CharField(max_length=255)),
("text", models.TextField(blank=True, default="")),
("done", models.BooleanField(default=False)),
("deadline", models.DateTimeField(blank=True, null=True)),
(
"list",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="simpletodo.simplelist",
),
),
],
options={
"abstract": False,
},
),
]

View File

@ -1,24 +0,0 @@
import uuid
from django.conf import settings
from django.db import models
from model_utils.models import TimeStampedModel
class SimpleList(TimeStampedModel):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
title = models.CharField(max_length=255)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
def __str__(self):
return f"{self.title} ({self.user})"
class SimpleTask(TimeStampedModel):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
title = models.CharField(max_length=255)
text = models.TextField(blank=True, default="")
done = models.BooleanField(default=False)
deadline = models.DateTimeField(blank=True, null=True)
list = models.ForeignKey(SimpleList, on_delete=models.CASCADE)

View File

@ -1,41 +0,0 @@
import structlog
from rest_framework import serializers
from rest_framework.serializers import ModelSerializer
from vbv_lernwelt.simpletodo.models import SimpleTask, SimpleList
logger = structlog.get_logger(__name__)
class SimpleTaskSerializer(ModelSerializer):
list_title = serializers.CharField(max_length=100)
class Meta:
model = SimpleTask
fields = [
"id",
"title",
"text",
"done",
"deadline",
"list_title",
]
def create(self, validated_data):
user = validated_data.pop("user", None)
if user is None:
raise serializers.ValidationError("User is required")
list_title = validated_data.pop("list_title")
simple_list, _ = SimpleList.objects.get_or_create(title=list_title, user=user)
validated_data["list"] = simple_list
logger.debug(
"Creating task",
label="simpletodo",
dt={"s1": 3, "s2": 4},
title=validated_data.get("title"),
list_title=list_title,
)
return super().create(validated_data)

View File

@ -1,17 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div class="container mx-auto">
{% for list in simple_lists %}
{% include "simpletodo/partials/simple_list.html" with list=list%}
{% endfor %}
</div>
<it-icon-ls-start class="text-orange-500"></it-icon-ls-start>
<it-icon-arrow-up></it-icon-arrow-up>
<it-icon-arrow-down></it-icon-arrow-down>
{% endblock %}

View File

@ -1,21 +0,0 @@
<form class="flex mt-4" action="/todo/api/tasks/" method="POST">
{% csrf_token %}
<input class="shadow appearance-none border rounded w-full py-2 px-3 mr-4 text-gray-darker"
type="text"
name="title"
maxlength="100"
required
placeholder="Add Todo"
>
<input type="hidden" name="list_title" value="{{ list.title }}">
<input
type="submit"
value="Add"
hx-post="/todo/api/tasks/"
hx-trigger="submit"
hx-target="#parent-div"
hx-swap="outerHTML"
class="flex-no-shrink p-2 border-2 rounded text-blue-500 border-blue-500 hover:text-white hover:bg-blue-500"
>
</form>

View File

@ -1,18 +0,0 @@
<div class="h-100 w-full flex items-center justify-center bg-blue-100 font-sans">
<div class="bg-white rounded shadow p-6 m-4 w-full lg:w-3/5 md:w-3/4">
<div class="mb-4">
<h2
class="text-gray-darkest"
data-cy="simple-list-title"
>
{{ list.title }}
</h2>
{% include "simpletodo/partials/add_task_form.html" with task=task %}
</div>
{% for task in list.simpletask_set.all %}
{% include "simpletodo/partials/task.html" with task=task %}
{% endfor %}
</div>
</div>

View File

@ -1,35 +0,0 @@
<div class="task">
<div class="flex mb-4 items-center">
{% if task.done %}
<p class="flex-auto text-blue-500 line-through">
{% else %}
<p class="flex-auto text-blue-900">
{% endif %}
{{ task.title }}
</p>
<button
hx-post="/todo/api/tasks/{{ task.id }}/toggle_done/"
hx-swap="outerHTML swap:0.5s"
hx-target="closest .task"
{% if task.done %}
class="flex-no-shrink p-2 ml-4 mr-2 border-2 rounded hover:text-white text-gray-500 border-gray-500 hover:bg-gray-500"
{% else %}
class="flex-no-shrink p-2 ml-4 mr-2 border-2 rounded hover:text-white text-green-500 border-green-500 hover:bg-green-500"
{% endif %}
>
{% if task.done %}
Not Done
{% else %}
Done
{% endif %}
</button>
<button
hx-delete="/todo/api/tasks/{{ task.id }}/"
hx-swap="outerHTML swap:0.5s"
hx-target="closest .task"
class="flex-no-shrink p-2 ml-2 border-2 rounded text-red-500 border-red-500 hover:text-white hover:bg-red-500">
Remove
</button>
</div>
</div>

View File

@ -1,26 +0,0 @@
from django.test import TestCase
from vbv_lernwelt.core.tests.factories import UserFactory
from vbv_lernwelt.simpletodo.models import SimpleTask
from vbv_lernwelt.simpletodo.serializers import SimpleTaskSerializer
class SimpleTaskSerializerTestCase(TestCase):
def setUp(self) -> None:
self.user = UserFactory()
def test_serializer(self):
serializer = SimpleTaskSerializer(
data={
"title": "Test",
"list_title": "Todos",
}
)
serializer.is_valid(raise_exception=True)
serializer.save(user=self.user)
task = SimpleTask.objects.first()
self.assertEqual(task.title, "Test")
self.assertEqual(task.list.title, "Todos")

View File

@ -1,13 +0,0 @@
from django.conf.urls import url, include
from django.urls import path
from rest_framework.routers import DefaultRouter
from . import views
router = DefaultRouter()
router.register(r"tasks", views.SimpleTaskViewSet, basename="tasks")
urlpatterns = [
path("", views.index, name="index"),
url(r"^api/", include(router.urls)),
]

View File

@ -1,92 +0,0 @@
from django.http import HttpResponse
from django.shortcuts import redirect, render
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.renderers import TemplateHTMLRenderer, JSONRenderer
from rest_framework.response import Response
from vbv_lernwelt.simpletodo.models import SimpleList, SimpleTask
from vbv_lernwelt.simpletodo.serializers import SimpleTaskSerializer
def index(request):
simple_lists = SimpleList.objects.filter(user=request.user)
if simple_lists.count() == 0:
simple_lists = [SimpleList.objects.create(user=request.user, title="Todos")]
return render(request, "simpletodo/index.html", {"simple_lists": simple_lists})
class SimpleTaskViewSet(viewsets.ModelViewSet):
serializer_class = SimpleTaskSerializer
renderer_classes = [TemplateHTMLRenderer, JSONRenderer]
def get_queryset(self):
user = self.request.user
return SimpleTask.objects.filter(list__user=user)
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
if request.accepted_renderer.format == "html":
serializer.is_valid(raise_exception=True)
else:
serializer.is_valid(raise_exception=True)
serializer.save(user=request.user)
if request.accepted_renderer.format == "html":
return redirect("/todo/")
headers = self.get_success_headers(serializer.data)
return Response(
serializer.data, status=status.HTTP_201_CREATED, headers=headers
)
def destroy(self, request, *args, **kwargs):
instance = self.get_object()
instance.delete()
if request.htmx:
return HttpResponse(status=200, content="")
return Response(status=status.HTTP_204_NO_CONTENT)
@action(
detail=True,
methods=[
"post",
],
)
def toggle_done(self, request, pk=None):
task = self.get_object()
task.done = not task.done
task.save()
if request.htmx:
return render(request, "simpletodo/partials/task.html", {"task": task})
return Response(self.get_serializer(task), status=status.HTTP_200_OK)
#
# def get_category_from_request(self, request):
# cat_name = request.query_params.get('cat_name')
# category_obj = None
#
# if cat_name:
# category_obj = VideoCategory.objects.filter(category__iexact=cat_name).first()
# if not category_obj:
# category_obj = VideoCategory.objects.first()
#
# return category_obj
#
# @action(detail=False, methods=['get'])
# def form(self, request):
# category_obj = self.get_category_from_request(request)
# return Response(template_name='videos/partials/video_form.html', data={'category': category_obj})
#
# @action(detail=False, methods=['get'])
# def cancel(self, request):
# category_obj = self.get_category_from_request(request)
# return Response(template_name='videos/partials/show_add_form.html', data={'category': category_obj})

View File

@ -1,4 +1,8 @@
server/requirements/ server/requirements/
env_secrets/ env_secrets/
env/docker_local.env env/docker_local.env
server/vbv_lernwelt/media/
server/vbv_lernwelt/simpletodo/
supabase.md
scripts/supabase/init.sql
.envs/ .envs/