Add previous-/next- attributes to LearningContents
This commit is contained in:
parent
b893dcbcc8
commit
2c36ea9242
|
|
@ -1,11 +1,25 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import * as log from 'loglevel';
|
||||||
|
|
||||||
import type {Circle, LearningContent} from '@/types';
|
import type {Circle, LearningContent} from '@/types';
|
||||||
|
import {computed} from 'vue';
|
||||||
|
import {useCircleStore} from '@/stores/circle';
|
||||||
|
|
||||||
|
log.debug('LearningContent.vue setup');
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
circleData: Circle,
|
circleData: Circle,
|
||||||
learningContent: LearningContent,
|
learningContent: LearningContent,
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const block = computed(() => {
|
||||||
|
if(props.learningContent) {
|
||||||
|
return props.learningContent.contents[0];
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const circleStore = useCircleStore();
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -21,7 +35,7 @@ const props = defineProps<{
|
||||||
<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"
|
||||||
@click="$emit('close')"
|
@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>
|
||||||
<span class="">zurück zum Circle</span>
|
<span class="">zurück zum Circle</span>
|
||||||
|
|
@ -29,21 +43,36 @@ const props = defineProps<{
|
||||||
|
|
||||||
<h1 class="text-xl">{{ learningContent.title }}</h1>
|
<h1 class="text-xl">{{ learningContent.title }}</h1>
|
||||||
|
|
||||||
<button type="button" class="btn-blue">Abschliessen und weiter</button>
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn-blue"
|
||||||
|
@click="circleStore.continueToNextLearningContent()"
|
||||||
|
>
|
||||||
|
Abschliessen und weiter
|
||||||
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
|
||||||
<div class="mx-auto max-w-5xl px-8 py-4">
|
<div class="mx-auto max-w-5xl px-8 py-4">
|
||||||
<p>{{ learningContent.contents[0].value.description }}</p>
|
<p>{{ block.value.description }}</p>
|
||||||
|
|
||||||
|
<div v-if="block.type === 'video'">
|
||||||
|
<iframe
|
||||||
|
class="mt-8 w-full aspect-video"
|
||||||
|
:src="block.value.url"
|
||||||
|
:title="learningContent.title"
|
||||||
|
frameborder="0"
|
||||||
|
allow="accelerometer; encrypted-media; gyroscope; picture-in-picture"
|
||||||
|
allowfullscreen>
|
||||||
|
</iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="block.type === 'podcast'"
|
||||||
|
>
|
||||||
|
<iframe width="100%" height="300" scrolling="no" frameborder="no" allow="" :src="block.value.url"></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
<iframe
|
|
||||||
class="mt-8 w-full aspect-video"
|
|
||||||
:src="learningContent.contents[0].value.url"
|
|
||||||
:title="learningContent.title"
|
|
||||||
frameborder="0"
|
|
||||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
|
||||||
allowfullscreen>
|
|
||||||
</iframe>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
import ItCheckbox from '@/components/ui/ItCheckbox.vue';
|
import ItCheckbox from '@/components/ui/ItCheckbox.vue';
|
||||||
import type {LearningContent, LearningSequence} from '@/types';
|
import type {LearningContent, LearningSequence} from '@/types';
|
||||||
import {useCircleStore} from '@/stores/circle';
|
import {useCircleStore} from '@/stores/circle';
|
||||||
|
import {computed} from 'vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
learningSequence: LearningSequence
|
learningSequence: LearningSequence
|
||||||
|
|
@ -17,6 +18,16 @@ function toggleCompleted(learningContent: LearningContent) {
|
||||||
circleStore.toggleCompleted(learningContent, !learningContent.completed);
|
circleStore.toggleCompleted(learningContent, !learningContent.completed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const someFinished = computed(() => {
|
||||||
|
if (props.learningSequence) {
|
||||||
|
return circleStore.flatLearningContents.filter((lc) => {
|
||||||
|
return lc.completed && lc.parentLearningSequence?.translation_key === props.learningSequence.translation_key;
|
||||||
|
}).length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -29,7 +40,13 @@ function toggleCompleted(learningContent: LearningContent) {
|
||||||
<div>{{ learningSequence.minutes }} Minuten</div>
|
<div>{{ learningSequence.minutes }} Minuten</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-white px-4 border border-gray-500 border-l-4 border-l-sky-500">
|
<div
|
||||||
|
class="bg-white px-4 border border-gray-500 border-l-4"
|
||||||
|
:class="{
|
||||||
|
'border-l-sky-500': someFinished,
|
||||||
|
'border-l-gray-500': !someFinished,
|
||||||
|
}"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-for="learningUnit in learningSequence.learningUnits"
|
v-for="learningUnit in learningSequence.learningUnits"
|
||||||
class="py-3"
|
class="py-3"
|
||||||
|
|
@ -47,7 +64,7 @@ function toggleCompleted(learningContent: LearningContent) {
|
||||||
:modelValue="learningContent.completed"
|
:modelValue="learningContent.completed"
|
||||||
@click="toggleCompleted(learningContent)"
|
@click="toggleCompleted(learningContent)"
|
||||||
>
|
>
|
||||||
<span @click.stop="$emit('clickContent', learningContent)">{{ learningContent.contents[0].type }}: {{ learningContent.title }}</span>
|
<span @click.stop="circleStore.openLearningContent(learningContent)">{{ learningContent.contents[0].type }}: {{ learningContent.title }}</span>
|
||||||
</ItCheckbox>
|
</ItCheckbox>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -120,7 +120,11 @@ describe('circleService.parseLearningSequences', () => {
|
||||||
} as WagtailCircle;
|
} as WagtailCircle;
|
||||||
|
|
||||||
const learningSequences = parseLearningSequences(input.children);
|
const learningSequences = parseLearningSequences(input.children);
|
||||||
|
expect(learningSequences.length).toBe(1);
|
||||||
|
console.log(learningSequences[0].learningUnits[0].learningContents[0]);
|
||||||
|
|
||||||
expect(learningSequences.length).toBe(4);
|
expect(
|
||||||
|
learningSequences[0].learningUnits[1].learningContents[0].previousLearningContent.translation_key
|
||||||
|
).toEqual(learningSequences[0].learningUnits[0].learningContents[1].translation_key);
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import type {CircleChild, LearningSequence, LearningUnit} from '@/types';
|
import type {CircleChild, LearningContent, LearningSequence, LearningUnit} from '@/types';
|
||||||
|
|
||||||
|
|
||||||
function createEmptyLearningUnit(): LearningUnit {
|
function createEmptyLearningUnit(): LearningUnit {
|
||||||
return {
|
return {
|
||||||
id: -1,
|
id: 0,
|
||||||
title: '',
|
title: '',
|
||||||
slug: '',
|
slug: '',
|
||||||
translation_key: '',
|
translation_key: '',
|
||||||
|
|
@ -15,12 +15,13 @@ function createEmptyLearningUnit(): LearningUnit {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseLearningSequences (children: CircleChild[]): LearningSequence[] {
|
export function parseLearningSequences (children: CircleChild[]): LearningSequence[] {
|
||||||
let learningSequence:LearningSequence|null = null;
|
let learningSequence:LearningSequence | null = null;
|
||||||
let learningUnit:LearningUnit|null = null;
|
let learningUnit:LearningUnit | null = null;
|
||||||
|
let learningContent:LearningContent | null = null;
|
||||||
|
let previousLearningContent: LearningContent | null = null;
|
||||||
const result:LearningSequence[] = [];
|
const result:LearningSequence[] = [];
|
||||||
|
|
||||||
children.forEach((child) => {
|
children.forEach((child) => {
|
||||||
// FIXME add error detection if the data does not conform to expectations
|
|
||||||
if (child.type === 'learnpath.LearningSequence') {
|
if (child.type === 'learnpath.LearningSequence') {
|
||||||
if (learningSequence) {
|
if (learningSequence) {
|
||||||
if (learningUnit) {
|
if (learningUnit) {
|
||||||
|
|
@ -46,6 +47,17 @@ export function parseLearningSequences (children: CircleChild[]): LearningSequen
|
||||||
if (learningUnit === null) {
|
if (learningUnit === null) {
|
||||||
throw new Error('LearningContent found before LearningUnit');
|
throw new Error('LearningContent found before LearningUnit');
|
||||||
}
|
}
|
||||||
|
previousLearningContent = learningContent;
|
||||||
|
|
||||||
|
learningContent = Object.assign(child, {
|
||||||
|
parentLearningSequence: learningSequence,
|
||||||
|
parentLearningUnit: learningUnit,
|
||||||
|
previousLearningContent: previousLearningContent,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (previousLearningContent) {
|
||||||
|
previousLearningContent.nextLearningContent = learningContent;
|
||||||
|
}
|
||||||
|
|
||||||
learningUnit.learningContents.push(child);
|
learningUnit.learningContents.push(child);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import * as log from 'loglevel';
|
||||||
|
|
||||||
import {defineStore} from 'pinia'
|
import {defineStore} from 'pinia'
|
||||||
|
|
||||||
import type {Circle, LearningContent} from '@/types'
|
import type {Circle, LearningContent} from '@/types'
|
||||||
|
|
@ -8,6 +10,7 @@ export type CircleStoreState = {
|
||||||
circleData: Circle;
|
circleData: Circle;
|
||||||
completionData: any;
|
completionData: any;
|
||||||
currentLearningContent: LearningContent | undefined;
|
currentLearningContent: LearningContent | undefined;
|
||||||
|
page: 'INDEX' | 'OVERVIEW' | 'LEARNING_CONTENT',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useCircleStore = defineStore({
|
export const useCircleStore = defineStore({
|
||||||
|
|
@ -17,9 +20,22 @@ export const useCircleStore = defineStore({
|
||||||
circleData: {},
|
circleData: {},
|
||||||
completionData: {},
|
completionData: {},
|
||||||
currentLearningContent: undefined,
|
currentLearningContent: undefined,
|
||||||
|
page: 'INDEX',
|
||||||
} as CircleStoreState;
|
} as CircleStoreState;
|
||||||
},
|
},
|
||||||
getters: {},
|
getters: {
|
||||||
|
flatLearningContents: (state) => {
|
||||||
|
const result:LearningContent[] = [];
|
||||||
|
state.circleData.learningSequences.forEach((learningSequence) => {
|
||||||
|
learningSequence.learningUnits.forEach((learningUnit) => {
|
||||||
|
learningUnit.learningContents.forEach((learningContent) => {
|
||||||
|
result.push(learningContent);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
},
|
||||||
actions: {
|
actions: {
|
||||||
async loadCircle(slug: string) {
|
async loadCircle(slug: string) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -28,11 +44,8 @@ export const useCircleStore = defineStore({
|
||||||
this.circleData.learningSequences = parseLearningSequences(this.circleData.children);
|
this.circleData.learningSequences = parseLearningSequences(this.circleData.children);
|
||||||
this.completionData = await itGet(`/api/completion/circle/${this.circleData.translation_key}/`);
|
this.completionData = await itGet(`/api/completion/circle/${this.circleData.translation_key}/`);
|
||||||
this.parseCompletionData();
|
this.parseCompletionData();
|
||||||
|
|
||||||
// TODO set currentLearningContent for testing
|
|
||||||
this.setCurrentLearningContent(this.circleData.learningSequences[0].learningUnits[0].learningContents[0]);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(error);
|
log.error(error);
|
||||||
return error
|
return error
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -45,23 +58,34 @@ export const useCircleStore = defineStore({
|
||||||
});
|
});
|
||||||
this.parseCompletionData();
|
this.parseCompletionData();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(error);
|
log.error(error);
|
||||||
return error
|
return error
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
parseCompletionData() {
|
parseCompletionData() {
|
||||||
this.circleData.learningSequences.forEach((learningSequence) => {
|
this.flatLearningContents.forEach((learningContent) => {
|
||||||
learningSequence.learningUnits.forEach((learningUnit) => {
|
learningContent.completed = this.completionData.completed_learning_contents.findIndex((e) => {
|
||||||
learningUnit.learningContents.forEach((learningContent) => {
|
return e.learning_content_key === learningContent.translation_key && e.completed;
|
||||||
learningContent.completed = this.completionData.completed_learning_contents.findIndex((e) => {
|
}) >= 0;
|
||||||
return e.learning_content_key === learningContent.translation_key && e.completed;
|
|
||||||
}) >= 0;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
setCurrentLearningContent(learningContent: LearningContent) {
|
openLearningContent(learningContent: LearningContent) {
|
||||||
this.currentLearningContent = learningContent;
|
this.currentLearningContent = learningContent;
|
||||||
|
this.page = 'LEARNING_CONTENT';
|
||||||
|
},
|
||||||
|
closeLearningContent() {
|
||||||
|
this.currentLearningContent = undefined;
|
||||||
|
this.page = 'INDEX';
|
||||||
|
},
|
||||||
|
continueToNextLearningContent() {
|
||||||
|
if (this.currentLearningContent) {
|
||||||
|
this.toggleCompleted(this.currentLearningContent, true);
|
||||||
|
if (this.currentLearningContent.nextLearningContent) {
|
||||||
|
this.openLearningContent(this.currentLearningContent.nextLearningContent);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.error('currentLearningContent is undefined');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,34 @@
|
||||||
export interface LearningContentBlock {
|
export interface LearningContentBlock {
|
||||||
type: 'video' | 'web-based-training' | 'podcast' | 'competence' | 'exercise' | 'document' | 'knowledge';
|
type: 'web-based-training' | 'competence' | 'exercise' | 'knowledge';
|
||||||
value: {
|
value: {
|
||||||
description: string;
|
description: string;
|
||||||
url?: string;
|
},
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VideoBlock {
|
||||||
|
type: 'video';
|
||||||
|
value: {
|
||||||
|
description: string;
|
||||||
|
url: string;
|
||||||
|
},
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PodcastBlock {
|
||||||
|
type: 'podcast';
|
||||||
|
value: {
|
||||||
|
description: string;
|
||||||
|
url: string;
|
||||||
|
},
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DocumentBlock {
|
||||||
|
type: 'document';
|
||||||
|
value: {
|
||||||
|
description: string;
|
||||||
|
url: string;
|
||||||
},
|
},
|
||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
@ -29,8 +55,12 @@ export interface LearningWagtailPage {
|
||||||
export interface LearningContent extends LearningWagtailPage {
|
export interface LearningContent extends LearningWagtailPage {
|
||||||
type: 'learnpath.LearningContent';
|
type: 'learnpath.LearningContent';
|
||||||
minutes: number;
|
minutes: number;
|
||||||
contents: LearningContentBlock[];
|
contents: (LearningContentBlock | VideoBlock | PodcastBlock | DocumentBlock)[];
|
||||||
completed?: boolean;
|
completed?: boolean;
|
||||||
|
parentLearningSequence?: LearningSequence;
|
||||||
|
parentLearningUnit?: LearningUnit;
|
||||||
|
nextLearningContent?: LearningContent;
|
||||||
|
previousLearningContent?: LearningContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LearningUnit extends LearningWagtailPage {
|
export interface LearningUnit extends LearningWagtailPage {
|
||||||
|
|
|
||||||
|
|
@ -6,48 +6,32 @@ import LearningSequence from '@/components/circle/LearningSequence.vue';
|
||||||
import CircleOverview from '@/components/circle/CircleOverview.vue';
|
import CircleOverview from '@/components/circle/CircleOverview.vue';
|
||||||
import LearningContent from '@/components/circle/LearningContent.vue';
|
import LearningContent from '@/components/circle/LearningContent.vue';
|
||||||
|
|
||||||
import {onMounted, reactive} from 'vue'
|
import {onMounted} from 'vue'
|
||||||
import {useCircleStore} from '@/stores/circle';
|
import {useCircleStore} from '@/stores/circle';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
circleSlug: string
|
circleSlug: string
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
|
||||||
interface State {
|
|
||||||
page: 'INDEX' | 'OVERVIEW' | 'LEARNING_CONTENT',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const circleStore = useCircleStore();
|
const circleStore = useCircleStore();
|
||||||
|
|
||||||
const state:State = reactive({
|
|
||||||
page: 'LEARNING_CONTENT',
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
log.info('CircleView.vue mounted');
|
log.info('CircleView.vue mounted');
|
||||||
await circleStore.loadCircle(props.circleSlug);
|
await circleStore.loadCircle(props.circleSlug);
|
||||||
});
|
});
|
||||||
|
|
||||||
function openLearningContent(learningContent: LearningContent) {
|
|
||||||
log.info('openLearningContent', learningContent);
|
|
||||||
circleStore.setCurrentLearningContent(learningContent);
|
|
||||||
state.page = 'LEARNING_CONTENT';
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Transition>
|
<Transition>
|
||||||
<div v-if="state.page === 'OVERVIEW'">
|
<div v-if="circleStore.page === 'OVERVIEW'">
|
||||||
<CircleOverview :circle-data="circleStore.circleData" @close="state.page = 'INDEX'"/>
|
<CircleOverview :circle-data="circleStore.circleData" @close="circleStore.page = 'INDEX'"/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="state.page === 'LEARNING_CONTENT'">
|
<div v-else-if="circleStore.page === 'LEARNING_CONTENT'">
|
||||||
<LearningContent
|
<LearningContent
|
||||||
:circle-data="circleStore.circleData"
|
:circle-data="circleStore.circleData"
|
||||||
:learning-content="circleStore.currentLearningContent"
|
:learning-content="circleStore.currentLearningContent"
|
||||||
@close="state.page = 'INDEX'"
|
:key="circleStore.currentLearningContent.translation_key"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
|
|
@ -70,7 +54,7 @@ function openLearningContent(learningContent: LearningContent) {
|
||||||
{{ circleStore.circleData.description }}
|
{{ circleStore.circleData.description }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="btn-primary mt-4" @click="state.showOverview = true">Erfahre mehr dazu</button>
|
<button class="btn-primary mt-4" @click="circleStore.page = 'OVERVIEW'">Erfahre mehr dazu</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="expert border border-gray-500 mt-8 p-6">
|
<div class="expert border border-gray-500 mt-8 p-6">
|
||||||
|
|
@ -90,7 +74,6 @@ function openLearningContent(learningContent: LearningContent) {
|
||||||
<LearningSequence
|
<LearningSequence
|
||||||
:learning-sequence="learningSequence"
|
:learning-sequence="learningSequence"
|
||||||
:completion-data="circleStore.completionData"
|
:completion-data="circleStore.completionData"
|
||||||
@click-content="openLearningContent"
|
|
||||||
></LearningSequence>
|
></LearningSequence>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,7 @@ class Circle(Page):
|
||||||
], use_json_field=True)
|
], use_json_field=True)
|
||||||
|
|
||||||
parent_page_types = ['learnpath.LearningPath']
|
parent_page_types = ['learnpath.LearningPath']
|
||||||
subpage_types = ['learnpath.LearningSequence', 'learnpath.LearningUnit']
|
subpage_types = ['learnpath.LearningSequence', 'learnpath.LearningUnit', 'learnpath.LearningContent']
|
||||||
|
|
||||||
content_panels = Page.content_panels + [
|
content_panels = Page.content_panels + [
|
||||||
FieldPanel('description'),
|
FieldPanel('description'),
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,10 @@ Fachspezialisten bei.
|
||||||
title='Ermittlung des Kundenbedarfs',
|
title='Ermittlung des Kundenbedarfs',
|
||||||
parent=circe_analyse,
|
parent=circe_analyse,
|
||||||
minutes=30,
|
minutes=30,
|
||||||
contents=[('podcast', PodcastBlockFactory())]
|
contents=[('podcast', PodcastBlockFactory(
|
||||||
|
description='Die Ermittlung des Kundenbedarfs muss in einem eingehenden Gespräch herausgefunden werden. Höre dazu auch diesen Podcast an.',
|
||||||
|
url='https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/325190984&color=%23ff5500&auto_play=false&hide_related=false&show_comments=true&show_user=true&show_reposts=false&show_teaser=true&visual=true',
|
||||||
|
))]
|
||||||
)
|
)
|
||||||
LearningContentFactory(
|
LearningContentFactory(
|
||||||
title='Kundenbedürfnisse erkennen',
|
title='Kundenbedürfnisse erkennen',
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue