Merge branch 'develop' of bitbucket.org:iterativ/vbv_lernwelt into develop
# Conflicts: # client/src/views/LearningPathView.vue
This commit is contained in:
commit
dd18cf895b
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>$Title$</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
$END$
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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')
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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)/,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
},
|
||||||
|
})
|
||||||
14
cypress.json
14
cypress.json
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
export const login = (username, password) => {
|
||||||
|
cy.request({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/core/login/',
|
||||||
|
body: { username, password },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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')
|
|
||||||
// })
|
|
||||||
// })
|
|
||||||
})
|
|
||||||
|
|
@ -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) => {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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')]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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])
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -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'])
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"
|
|
||||||
|
|
@ -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": []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
GET http://localhost:8000/graphql/
|
|
||||||
Accept: application/json
|
|
||||||
|
|
||||||
###
|
|
||||||
{
|
|
||||||
page(id: 8) {
|
|
||||||
|
|
||||||
children {
|
|
||||||
__typename
|
|
||||||
id
|
|
||||||
title
|
|
||||||
children {
|
|
||||||
__typename
|
|
||||||
|
|
||||||
id
|
|
||||||
title
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
{
|
|
||||||
page(id: 8) {
|
|
||||||
|
|
||||||
children {
|
|
||||||
__typename
|
|
||||||
id
|
|
||||||
title
|
|
||||||
children {
|
|
||||||
__typename
|
|
||||||
|
|
||||||
id
|
|
||||||
title
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -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'])
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -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()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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])
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
# Create your tests here.
|
|
||||||
|
|
@ -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])
|
|
||||||
|
|
|
||||||
|
|
@ -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",
|
|
||||||
]
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class SimpletodoConfig(AppConfig):
|
|
||||||
default_auto_field = "django.db.models.BigAutoField"
|
|
||||||
name = "vbv_lernwelt.simpletodo"
|
|
||||||
|
|
@ -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,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -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 %}
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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")
|
|
||||||
|
|
@ -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)),
|
|
||||||
]
|
|
||||||
|
|
@ -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})
|
|
||||||
|
|
@ -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/
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue