Merge branch 'feature/refactor-typescript' into develop

This commit is contained in:
Daniel Egger 2022-10-21 19:03:39 +02:00
commit 4635435ce3
44 changed files with 793 additions and 673 deletions

View File

@ -54,6 +54,7 @@ pipelines:
- npm install - npm install
- npm run prettier:check - npm run prettier:check
- npm run lint - npm run lint
- npm run typecheck
- step: - step:
name: cypress tests name: cypress tests
max-time: 45 max-time: 45

View File

@ -15,7 +15,6 @@
}, },
"dependencies": { "dependencies": {
"@headlessui/vue": "^1.6.7", "@headlessui/vue": "^1.6.7",
"axios": "^0.26.1",
"d3": "^7.6.1", "d3": "^7.6.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"loglevel": "^1.8.0", "loglevel": "^1.8.0",

View File

@ -55,7 +55,6 @@ function learninPathSlug(): string {
} }
function handleDropdownSelect(data: DropdownData) { function handleDropdownSelect(data: DropdownData) {
log.debug("Selected action:", data.action);
switch (data.action) { switch (data.action) {
case "settings": case "settings":
router.push("/profile"); router.push("/profile");
@ -76,25 +75,21 @@ onMounted(() => {
log.debug("MainNavigationBar mounted"); log.debug("MainNavigationBar mounted");
}); });
const profileDropdownData: DropdownListItem[][] = [ const profileDropdownData: DropdownListItem[] = [
[ {
{ title: "Kontoeinstellungen",
title: "Kontoeinstellungen", icon: IconSettings as Component,
icon: IconSettings as Component, data: {
data: { action: "settings",
action: "settings",
},
}, },
], },
[ {
{ title: "Abmelden",
title: "Abmelden", icon: IconLogout as Component,
icon: IconLogout as Component, data: {
data: { action: "logout",
action: "logout",
},
}, },
], },
]; ];
</script> </script>
@ -102,7 +97,6 @@ const profileDropdownData: DropdownListItem[][] = [
<div> <div>
<Teleport to="body"> <Teleport to="body">
<MobileMenu <MobileMenu
:user-store="userStore"
:show="state.showMenu" :show="state.showMenu"
:learning-path-slug="learninPathSlug()" :learning-path-slug="learninPathSlug()"
:learning-path-name="learningPathName()" :learning-path-name="learningPathName()"

View File

@ -2,13 +2,14 @@
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 ItFullScreenModal from "@/components/ui/ItFullScreenModal.vue"; import ItFullScreenModal from "@/components/ui/ItFullScreenModal.vue";
import { useUserStore } from "@/stores/user";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
const router = useRouter(); const router = useRouter();
const userStore = useUserStore();
const props = defineProps<{ const props = defineProps<{
show: boolean; show: boolean;
userStore: object;
learningPathName: string; learningPathName: string;
learningPathSlug: string; learningPathSlug: string;
}>(); }>();

View File

@ -8,7 +8,6 @@ interface Props {
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
criteria: undefined,
showState: false, showState: false,
}); });

View File

@ -5,11 +5,14 @@ import * as _ from "lodash";
import * as log from "loglevel"; import * as log from "loglevel";
import { computed, onMounted } from "vue"; import { computed, onMounted } from "vue";
// @ts-ignore
import colors from "@/colors.json"; import colors from "@/colors.json";
import type { LearningSequence } from "@/types";
import type { DefaultArcObject } from "d3";
const circleStore = useCircleStore(); const circleStore = useCircleStore();
function someFinished(learningSequence) { function someFinished(learningSequence: LearningSequence) {
if (circleStore.circle) { if (circleStore.circle) {
return circleStore.circle.someFinishedInLearningSequence( return circleStore.circle.someFinishedInLearningSequence(
learningSequence.translation_key learningSequence.translation_key
@ -18,7 +21,7 @@ function someFinished(learningSequence) {
return false; return false;
} }
function allFinished(learningSequence) { function allFinished(learningSequence: LearningSequence) {
if (circleStore.circle) { if (circleStore.circle) {
return circleStore.circle.allFinishedInLearningSequence( return circleStore.circle.allFinishedInLearningSequence(
learningSequence.translation_key learningSequence.translation_key
@ -32,6 +35,17 @@ onMounted(async () => {
render(); render();
}); });
interface CirclePie extends d3.PieArcDatum<number> {
title: string;
icon: string;
slug: string;
translation_key: string;
arrowStartAngle: number;
arrowEndAngle: number;
someFinished: boolean;
allFinished: boolean;
}
const pieData = computed(() => { const pieData = computed(() => {
const circle = circleStore.circle; const circle = circleStore.circle;
console.log("initial of compute pie data ", circle); console.log("initial of compute pie data ", circle);
@ -41,193 +55,203 @@ const pieData = computed(() => {
const pieWeights = new Array(Math.max(circle.learningSequences.length, 1)).fill(1); const pieWeights = new Array(Math.max(circle.learningSequences.length, 1)).fill(1);
const pieGenerator = d3.pie(); const pieGenerator = d3.pie();
let angles = pieGenerator(pieWeights); const angles = pieGenerator(pieWeights);
_.forEach(angles, (pie) => { let result = angles.map((angle) => {
const thisLearningSequence = circle.learningSequences[parseInt(pie.index)]; const thisLearningSequence = circle.learningSequences[angle.index];
pie.title = thisLearningSequence.title;
pie.icon = thisLearningSequence.icon; return Object.assign(
pie.startAngle = pie.startAngle + Math.PI; {
pie.endAngle = pie.endAngle + Math.PI; startAngle: angle.startAngle + Math.PI,
pie.arrowStartAngle = pie.endAngle + (pie.startAngle - pie.endAngle) / 2; endAngle: angle.endAngle + Math.PI,
pie.arrowEndAngle = pie.startAngle + (pie.startAngle - pie.endAngle) / 2; ..._.pick(thisLearningSequence, ["title", "icon", "translation_key", "slug"]),
pie.translation_key = thisLearningSequence.translation_key; arrowStartAngle: angle.endAngle + (angle.startAngle - angle.endAngle) / 2,
pie.slug = thisLearningSequence.slug; arrowEndAngle: angle.startAngle + (angle.startAngle - angle.endAngle) / 2,
pie.someFinished = someFinished(thisLearningSequence); someFinished: someFinished(thisLearningSequence),
pie.allFinished = allFinished(thisLearningSequence); allFinished: allFinished(thisLearningSequence),
},
angle
);
}); });
angles = angles.reverse(); result = result.reverse();
return angles; return result as CirclePie[];
} }
return {};
return undefined;
}); });
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;
function render() { function getColor(d: CirclePie) {
const arrowStrokeWidth = 2; let color = colors.gray[300];
if (d.someFinished) {
color = colors.sky[500];
}
if (d.allFinished) {
color = colors.green[500];
}
return color;
}
function getHoverColor(d: CirclePie) {
let color = colors.gray[200];
if (d.someFinished) {
color = colors.sky[400];
}
if (d.allFinished) {
color = colors.green[400];
}
return color;
}
function render() {
const svg = d3.select(".circle-visualization"); const svg = d3.select(".circle-visualization");
// Clean svg before adding new stuff. // Clean svg before adding new stuff.
svg.selectAll("*").remove(); svg.selectAll("*").remove();
// Append marker as definition to the svg if (pieData.value) {
svg const arrowStrokeWidth = 2;
.attr("viewBox", `0 0 ${width} ${height}`) // Append marker as definition to the svg
.append("svg:defs") svg
.append("svg:marker") .attr("viewBox", `0 0 ${width} ${height}`)
.attr("id", "triangle") .append("svg:defs")
.attr("refX", 11) .append("svg:marker")
.attr("refY", 11) .attr("id", "triangle")
.attr("markerWidth", 20) .attr("refX", 11)
.attr("markerHeight", 20) .attr("refY", 11)
.attr("markerUnits", "userSpaceOnUse") .attr("markerWidth", 20)
.attr("orient", "auto") .attr("markerHeight", 20)
.append("path") .attr("markerUnits", "userSpaceOnUse")
.attr("d", "M -1 0 l 10 0 M 0 -1 l 0 10") .attr("orient", "auto")
.attr("transform", "rotate(-90, 10, 0)") .append("path")
.attr("stroke-width", arrowStrokeWidth) .attr("d", "M -1 0 l 10 0 M 0 -1 l 0 10")
.attr("stroke", colors.gray[500]); .attr("transform", "rotate(-90, 10, 0)")
.attr("stroke-width", arrowStrokeWidth)
.attr("stroke", colors.gray[500]);
const g = svg const g = svg
.append("g") .append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")"); .attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
function getColor(d) { // Generate the pie diagram wede
let color = colors.gray[300]; const wedgeGenerator = d3
if (d.someFinished) { .arc()
color = colors.sky[500]; .innerRadius(radius / 2.5)
} .padAngle(12 / 360)
if (d.allFinished) { .outerRadius(radius);
color = colors.green[500];
} // Generate the arrows
return color; const arrowRadius = radius * 1.1;
const learningSequences = g
.selectAll(".learningSegmentArc")
.data(pieData.value)
.enter()
.append("g")
.attr("class", "learningSegmentArc")
.attr("role", "button")
.attr("fill", colors.gray[300]);
learningSequences
.on("mouseover", function (d, i) {
d3.select(this)
.transition()
.duration(200)
.attr("fill", (d) => {
// @ts-ignore
return getHoverColor(d);
});
})
.on("mouseout", function (d, i) {
d3.select(this)
.transition()
.duration(200)
.attr("fill", (d) => {
// @ts-ignore
return getColor(d);
});
})
.on("click", function (d, elm) {
console.log("clicked on ", d, elm);
document.getElementById(elm.slug)?.scrollIntoView({ behavior: "smooth" });
});
learningSequences
.transition()
.duration(1)
.attr("fill", (d) => {
return getColor(d);
});
// @ts-ignore
learningSequences.append("path").attr("d", wedgeGenerator);
const learningSequenceText = learningSequences
.append("text")
.attr("fill", colors.blue[900])
.style("font-size", "15px")
.text((d) => {
return d.title;
})
.attr("transform", function (d) {
let translate = wedgeGenerator.centroid(d as unknown as DefaultArcObject);
translate = [translate[0], translate[1] + 20];
return "translate(" + translate + ")";
})
.attr("class", "circlesText text-large font-bold")
.style("text-anchor", "middle");
const iconWidth = 25;
const learningSequenceIcon = learningSequences
.append("svg:image")
.attr("xlink:href", (d) => {
return "/static/icons/" + d.icon.replace("it-", "") + ".svg";
})
.attr("width", iconWidth)
.attr("height", iconWidth)
.attr("transform", function (d) {
let translate = wedgeGenerator.centroid(d as unknown as DefaultArcObject);
translate = [translate[0] - iconWidth / 2, translate[1] - iconWidth];
return "translate(" + translate + ")";
})
.attr("class", "filter-blue-900");
// Create Arrows
const arrow = d3
.arc()
.innerRadius(arrowRadius)
.outerRadius(arrowRadius + arrowStrokeWidth)
.padAngle(20 / 360)
.startAngle((d) => {
return (d as unknown as CirclePie).arrowStartAngle;
})
.endAngle((d) => {
return (d as unknown as CirclePie).arrowEndAngle;
});
const arrows = g
.selectAll(".arrow")
.data(pieData.value)
.join("g")
.attr("class", "arrow")
.attr("marker-end", "url(#triangle)");
// remove last arrow
d3.selection.prototype.last = function () {
const last = this.size() - 1;
return d3.select(this.nodes()[last]);
};
const all_arows = g.selectAll(".arrow");
// @ts-ignore
all_arows.last().remove();
//Draw arrow paths
// @ts-ignore
arrows.append("path").attr("fill", colors.gray[500]).attr("d", arrow);
} }
function getHoverColor(d) {
let color = colors.gray[200];
if (d.someFinished) {
color = colors.sky[400];
}
if (d.allFinished) {
color = colors.green[400];
}
return color;
}
// Generate the pie diagram wede
const wedgeGenerator = d3
.arc()
.innerRadius(radius / 2.5)
.padAngle(12 / 360)
.outerRadius(radius);
// Generate the arrows
const arrowRadius = radius * 1.1;
const learningSequences = g
.selectAll(".learningSegmentArc")
.data(pieData.value)
.enter()
.append("g")
.attr("class", "learningSegmentArc")
.attr("role", "button")
.attr("fill", colors.gray[300]);
learningSequences
.on("mouseover", function (d, i) {
d3.select(this)
.transition()
.duration(200)
.attr("fill", (d) => {
return getHoverColor(d);
});
})
.on("mouseout", function (d, i) {
d3.select(this)
.transition()
.duration(200)
.attr("fill", (d) => {
return getColor(d);
});
})
.on("click", function (d, elm) {
console.log("clicked on ", d, elm);
document.getElementById(elm.slug)?.scrollIntoView({ behavior: "smooth" });
});
learningSequences
.transition()
.duration(1)
.attr("fill", (d) => {
return getColor(d);
});
learningSequences.append("path").attr("d", wedgeGenerator);
const learningSequenceText = learningSequences
.append("text")
.attr("fill", colors.blue[900])
.style("font-size", "15px")
.text((d) => {
return d.title;
})
.attr("transform", function (d) {
let translate = wedgeGenerator.centroid(d);
translate = [translate[0], translate[1] + 20];
return "translate(" + translate + ")";
})
.attr("class", "circlesText text-large font-bold")
.style("text-anchor", "middle");
const iconWidth = 25;
const learningSequenceIcon = learningSequences
.append("svg:image")
.attr("xlink:href", (d) => {
return "/static/icons/" + d.icon.replace("it-", "") + ".svg";
})
.attr("width", iconWidth)
.attr("height", iconWidth)
.attr("transform", function (d) {
let translate = wedgeGenerator.centroid(d);
translate = [translate[0] - iconWidth / 2, translate[1] - iconWidth];
return "translate(" + translate + ")";
})
.attr("class", "filter-blue-900");
// Create Arrows
const arrow = d3
.arc()
.innerRadius(arrowRadius)
.outerRadius(arrowRadius + arrowStrokeWidth)
.padAngle(20 / 360)
.startAngle((d) => {
return d.arrowStartAngle;
})
.endAngle((d) => {
return d.arrowEndAngle;
});
const arrows = g
.selectAll(".arrow")
.data(pieData.value)
.join("g")
.attr("class", "arrow")
.attr("marker-end", "url(#triangle)");
// remove last arrow
d3.selection.prototype.last = function () {
const last = this.size() - 1;
return d3.select(this.nodes()[last]);
};
const all_arows = g.selectAll(".arrow");
all_arows.last().remove();
//Draw arrow paths
arrows.append("path").attr("fill", colors.gray[500]).attr("d", arrow);
return svg; return svg;
} }
</script> </script>

View File

@ -18,6 +18,7 @@ const emit = defineEmits(["back", "next"]);
<button <button
type="button" type="button"
class="btn-text inline-flex items-center px-3 py-2" class="btn-text inline-flex items-center px-3 py-2"
data-cy="close-learning-content"
@click="$emit('back')" @click="$emit('back')"
> >
<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>

View File

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { LearningContentType } from "@/types"; import type { LearningContentType } from "@/types";
import { learningContentTypesToName } from "@/utils/typeMaps"; import { learningContentTypeData } from "@/utils/typeMaps";
const props = defineProps<{ const props = defineProps<{
learningContentType: LearningContentType; learningContentType: LearningContentType;
@ -11,39 +11,12 @@ const props = defineProps<{
<div <div
class="flex bg-gray-200 rounded-full px-2.5 py-0.5 gap-2 items-center w-min h-min" class="flex bg-gray-200 rounded-full px-2.5 py-0.5 gap-2 items-center w-min h-min"
> >
<it-icon-lc-assignment <component
v-if="props.learningContentType === 'assignment'" :is="learningContentTypeData(props.learningContentType).icon"
class="w-6 h-6" class="w-6 h-6"
/> ></component>
<it-icon-lc-exercise
v-else-if="props.learningContentType === 'exercise'"
class="w-6 h-6"
/>
<it-icon-lc-book v-else-if="props.learningContentType === 'book'" class="w-6 h-6" />
<it-icon-lc-video
v-else-if="props.learningContentType === 'video'"
class="w-6 h-6"
/>
<it-icon-lc-media-library
v-else-if="props.learningContentType === 'media_library'"
class="w-6 h-6"
/>
<it-icon-lc-test v-else-if="props.learningContentType === 'test'" class="w-6 h-6" />
<it-icon-lc-online-training
v-else-if="props.learningContentType === 'online_training'"
class="w-6 h-6"
/>
<it-icon-lc-resource
v-else-if="props.learningContentType === 'resource'"
class="w-6 h-6"
/>
<it-icon-lc-resource
v-else-if="props.learningContentType === 'document'"
class="w-6 h-6"
/>
<it-icon-lc-document v-else class="w-6 h-6" />
<p class="whitespace-nowrap"> <p class="whitespace-nowrap">
{{ learningContentTypesToName.get(props.learningContentType) }} {{ learningContentTypeData(props.learningContentType).title }}
</p> </p>
</div> </div>
</template> </template>

View File

@ -121,8 +121,8 @@ const learningSequenceBorderClass = computed(() => {
<span class="flex gap-4 items-center xl:h-10"> <span class="flex gap-4 items-center xl:h-10">
<button <button
class="cursor-pointer w-full sm:w-auto text-left" class="cursor-pointer w-full sm:w-auto text-left"
@click.stop="circleStore.openLearningContent(learningContent)"
:data-cy="`${learningContent.slug}`" :data-cy="`${learningContent.slug}`"
@click.stop="circleStore.openLearningContent(learningContent)"
> >
{{ learningContent.title }} {{ learningContent.title }}
</button> </button>

View File

@ -23,9 +23,9 @@ const props = withDefaults(defineProps<Props>(), {
<div> <div>
<h4 class="mb-2 text-bold">{{ title }}</h4> <h4 class="mb-2 text-bold">{{ title }}</h4>
<p class="mb-2">{{ description }}</p> <p class="mb-2">{{ description }}</p>
<media-link :to="url" :blank="openWindow" class="link"> <MediaLink :to="url" :blank="openWindow" class="link">
<span class="inline">{{ linkText }}</span> <span class="inline">{{ linkText }}</span>
</media-link> </MediaLink>
</div> </div>
</div> </div>
</template> </template>

View File

@ -6,7 +6,9 @@ import { computed } from "vue";
import { RouterLink } from "vue-router"; import { RouterLink } from "vue-router";
const props = defineProps({ const props = defineProps({
...RouterLink.props, // @ts-ignore // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
...RouterLink.props,
blank: { blank: {
type: Boolean, type: Boolean,
default: false, default: false,

View File

@ -22,8 +22,8 @@ defineEmits(["update:modelValue"]);
'opacity-50': disabled, 'opacity-50': disabled,
'cursor-not-allowed': disabled, 'cursor-not-allowed': disabled,
}" }"
@click="$emit('update:modelValue', !modelValue)"
class="w-8 h-8 cursor-pointer" class="w-8 h-8 cursor-pointer"
@click="$emit('update:modelValue', !modelValue)"
> >
<button <button
v-if="modelValue" v-if="modelValue"

View File

@ -3,13 +3,13 @@ import type { DropdownListItem } from "@/types";
import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/vue"; import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/vue";
const props = defineProps<{ const props = defineProps<{
buttonClasses: [string]; buttonClasses: [string] | [];
listItems: [[DropdownListItem]]; listItems: DropdownListItem[];
align: "left" | "right"; align: "left" | "right";
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: "select", data: object): void; (e: "select", data: any): void;
}>(); }>();
</script> </script>
@ -33,20 +33,18 @@ const emit = defineEmits<{
class="absolute mt-2 px-6 w-56 w-max-full origin-top-right divide-y divide-gray-500 bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none" class="absolute mt-2 px-6 w-56 w-max-full origin-top-right divide-y divide-gray-500 bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
:class="[align === 'left' ? 'left-0' : 'right-0']" :class="[align === 'left' ? 'left-0' : 'right-0']"
> >
<div v-for="section in listItems" :key="section" class=""> <div v-for="item in listItems" :key="item.title" class="px-1 py-1">
<div v-for="item in section" :key="item" class="px-1 py-1"> <MenuItem>
<MenuItem> <button
<button class="text-black group flex w-full items-center px-0 py-2 text-sm"
class="text-black group flex w-full items-center px-0 py-2 text-sm" @click="$emit('select', item.data)"
@click="$emit('select', item.data)" >
> <span class="inline-block pr-2">
<span class="inline-block pr-2"> <component :is="item.icon" v-if="item.icon"></component>
<component :is="item.icon" v-if="item.icon"></component> </span>
</span> {{ item.title }}
{{ item.title }} </button>
</button> </MenuItem>
</MenuItem>
</div>
</div> </div>
</MenuItems> </MenuItems>
</transition> </transition>

View File

@ -28,11 +28,7 @@ export const itPost = (url: RequestInfo, data: unknown, options: RequestInit = {
"Content-Type": "application/json;charset=UTF-8", "Content-Type": "application/json;charset=UTF-8",
}, },
options?.headers options?.headers
); ) as HeadersInit;
if (options?.headers) {
delete options.headers;
}
options = Object.assign( options = Object.assign(
{ {
@ -43,7 +39,6 @@ export const itPost = (url: RequestInfo, data: unknown, options: RequestInit = {
options options
); );
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
options.headers["X-CSRFToken"] = getCookieValue("csrftoken"); options.headers["X-CSRFToken"] = getCookieValue("csrftoken");

View File

@ -23,7 +23,7 @@ export function setI18nLanguage(i18n: any, locale: string) {
* *
* axios.defaults.headers.common['Accept-Language'] = locale * axios.defaults.headers.common['Accept-Language'] = locale
*/ */
document.querySelector("html").setAttribute("lang", locale); document.querySelector("html")?.setAttribute("lang", locale);
} }
export async function loadLocaleMessages(i18n: any, locale: any) { export async function loadLocaleMessages(i18n: any, locale: any) {

View File

@ -25,7 +25,11 @@ const userStore = useUserStore();
<form <form
class="bg-white p-4 lg:p-8" class="bg-white p-4 lg:p-8"
@submit.prevent=" @submit.prevent="
userStore.handleLogin(state.username, state.password, route.query.next) userStore.handleLogin(
state.username,
state.password,
route.query.next as string
)
" "
> >
<div class="mb-4"> <div class="mb-4">

View File

@ -27,29 +27,25 @@ const state = reactive({
}); });
const dropdownData = [ const dropdownData = [
[ {
{ title: "Option 1",
title: "Option 1", icon: IconLogout,
icon: IconLogout, data: {},
data: {}, },
{
title: "Option 2",
icon: IconLogout,
data: {
test: 12,
}, },
{ },
title: "Option 2", {
icon: null, title: "Option 3",
data: { icon: IconSettings,
test: 12, data: {
}, amount: 34,
}, },
], },
[
{
title: "Option 3",
icon: IconSettings,
data: {
amount: 34,
},
},
],
]; ];
// TODO: die CSS-Klasse für die Farben wird hier in der StyleGuideView.vue generiert. // TODO: die CSS-Klasse für die Farben wird hier in der StyleGuideView.vue generiert.

View File

@ -13,6 +13,7 @@ const competenceStore = useCompetenceStore();
<div class="container-large"> <div class="container-large">
<nav class="py-4 lg:pb-8"> <nav class="py-4 lg:pb-8">
<router-link <router-link
v-if="competenceStore.competenceProfilePage"
class="btn-text inline-flex items-center pl-0" class="btn-text inline-flex items-center pl-0"
:to="competenceStore.competenceProfilePage?.frontend_url" :to="competenceStore.competenceProfilePage?.frontend_url"
> >

View File

@ -4,7 +4,8 @@ import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
import { useCompetenceStore } from "@/stores/competence"; import { useCompetenceStore } from "@/stores/competence";
import type { CourseCompletionStatus } from "@/types"; import type { CourseCompletionStatus } from "@/types";
import * as log from "loglevel"; import * as log from "loglevel";
import { computed, Ref, ref } from "vue"; import type { Ref } from "vue";
import { computed, ref } from "vue";
log.debug("CompetencesMainView created"); log.debug("CompetencesMainView created");

View File

@ -21,7 +21,7 @@ let competencePage: CompetencePage | undefined;
const findCriteria = () => { const findCriteria = () => {
for (const page of competenceStore.competenceProfilePage for (const page of competenceStore.competenceProfilePage
?.children as CompetencePage[]) { ?.children as CompetencePage[]) {
for (let criteria of page.children) { for (const criteria of page.children) {
if (criteria.slug === route.params["criteriaSlug"]) { if (criteria.slug === route.params["criteriaSlug"]) {
currentQuestion = criteria; currentQuestion = criteria;
competencePage = page; competencePage = page;
@ -63,7 +63,7 @@ findCriteria();
@back="router.back()" @back="router.back()"
@next="router.back()" @next="router.back()"
> >
<div class="container-medium"> <div v-if="currentQuestion" class="container-medium">
<div class="mt-4 lg:mt-8 p-6 lg:p-12 border"> <div class="mt-4 lg:mt-8 p-6 lg:p-12 border">
<h2 class="heading-2"> <h2 class="heading-2">
{{ currentQuestion.competence_id }} {{ currentQuestion.title }} {{ currentQuestion.competence_id }} {{ currentQuestion.title }}

View File

@ -52,9 +52,11 @@ onMounted(async () => {
const learningUnits = circleStore.circle?.learningSequences.flatMap( const learningUnits = circleStore.circle?.learningSequences.flatMap(
(ls) => ls.learningUnits (ls) => ls.learningUnits
); );
wagtailPage = learningUnits.find((lu) => { if (learningUnits) {
return lu.slug.endsWith(slugEnd); wagtailPage = learningUnits.find((lu) => {
}); return lu.slug.endsWith(slugEnd);
});
}
} }
if (wagtailPage) { if (wagtailPage) {
document document

View File

@ -2,6 +2,7 @@
import LinkCard from "@/components/mediaLibrary/LinkCard.vue"; import LinkCard from "@/components/mediaLibrary/LinkCard.vue";
import MediaLink from "@/components/mediaLibrary/MediaLink.vue"; import MediaLink from "@/components/mediaLibrary/MediaLink.vue";
import { useMediaLibraryStore } from "@/stores/mediaLibrary"; import { useMediaLibraryStore } from "@/stores/mediaLibrary";
import type { MediaBlockType } from "@/types";
import * as log from "loglevel"; import * as log from "loglevel";
import { computed } from "vue"; import { computed } from "vue";
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
@ -33,28 +34,28 @@ const backLink = computed(() => {
const maxCardItems = 4; const maxCardItems = 4;
const maxListItems = 6; const maxListItems = 6;
const displayAsCard = (itemType: string): boolean => { const displayAsCard = (itemType: MediaBlockType): boolean => {
return itemType === "learn_media" || itemType === "relative_link"; return itemType === "learn_media" || itemType === "relative_link";
}; };
const hasMoreItems = (items: object[], maxItems: number): boolean => { function hasMoreItems<T>(items: T[], maxItems: number): boolean {
return items.length > maxItems; return items.length > maxItems;
}; }
const getMaxDisplayItems = (items: object[], maxItems: number) => { function getMaxDisplayItems<T>(items: T[], maxItems: number) {
return items.slice(0, maxItems); return items.slice(0, maxItems);
}; }
const getMaxDisplayItemsForType = (itemType: string, items: object[]) => { function getMaxDisplayItemsForType<T>(itemType: MediaBlockType, items: T[]) {
return displayAsCard(itemType) return displayAsCard(itemType)
? getMaxDisplayItems(items, maxCardItems) ? getMaxDisplayItems(items, maxCardItems)
: getMaxDisplayItems(items, maxListItems); : getMaxDisplayItems(items, maxListItems);
}; }
const hasMoreItemsForType = (itemType: string, items: object[]) => { function hasMoreItemsForType<T>(itemType: MediaBlockType, items: T[]) {
const maxItems = displayAsCard(itemType) ? maxCardItems : maxListItems; const maxItems = displayAsCard(itemType) ? maxCardItems : maxListItems;
return hasMoreItems(items, maxItems); return hasMoreItems(items, maxItems);
}; }
</script> </script>
<template> <template>
@ -65,7 +66,10 @@ const hasMoreItemsForType = (itemType: string, items: object[]) => {
<div class="bg-gray-200 pb-4 lg:pb-12"> <div class="bg-gray-200 pb-4 lg:pb-12">
<div class="container-large"> <div class="container-large">
<nav class="py-4 lg:pb-8"> <nav class="py-4 lg:pb-8">
<router-link class="btn-text inline-flex items-center pl-0" :to="backLink"> <router-link
class="btn-text inline-flex items-center pl-0"
:to="(backLink as string)"
>
<it-icon-arrow-left /> <it-icon-arrow-left />
<span>zurück</span> <span>zurück</span>
</router-link> </router-link>
@ -93,7 +97,7 @@ const hasMoreItemsForType = (itemType: string, items: object[]) => {
<ul> <ul>
<li <li
v-for="item in mediaCategory.items" v-for="item in mediaCategory.items"
:key="item" :key="item.id"
class="mb-2 flex items-center" class="mb-2 flex items-center"
> >
<it-icon-check class="h-8 w-8 text-sky-500 mr-4 flex-none"></it-icon-check> <it-icon-check class="h-8 w-8 text-sky-500 mr-4 flex-none"></it-icon-check>
@ -141,13 +145,13 @@ const hasMoreItemsForType = (itemType: string, items: object[]) => {
/> />
<div v-else class="flex items-center justify-between border-b py-4"> <div v-else class="flex items-center justify-between border-b py-4">
<h4 class="text-bold">{{ mediaItem.value.title }}</h4> <h4 class="text-bold">{{ mediaItem.value.title }}</h4>
<media-link <MediaLink
:blank="mediaItem.value.open_window" :blank="mediaItem.value.open_window"
:to="mediaItem.value.url" :to="mediaItem.value.url"
class="link" class="link"
> >
{{ mediaItem.value.link_display_text }} {{ mediaItem.value.link_display_text }}
</media-link> </MediaLink>
</div> </div>
</li> </li>
</ul> </ul>

View File

@ -26,7 +26,6 @@ const mediaList = computed(() => {
return contentCollection.value.contents[0].type === "learn_media"; return contentCollection.value.contents[0].type === "learn_media";
} }
}); });
console.log(learnMediaCollection);
return learnMediaCollection?.value; return learnMediaCollection?.value;
} }
return undefined; return undefined;
@ -35,7 +34,7 @@ const mediaList = computed(() => {
<template> <template>
<div <div
v-if="mediaCategory" v-if="mediaCategory && mediaStore.mediaLibraryPage && mediaList"
class="fixed top-0 overflow-y-scroll bg-white h-full w-full" class="fixed top-0 overflow-y-scroll bg-white h-full w-full"
> >
<div class="bg-gray-200"> <div class="bg-gray-200">

View File

@ -1,3 +1,4 @@
import type { WagtailCircle } from "@/types";
import { describe, it } from "vitest"; import { describe, it } from "vitest";
import { Circle } from "../circle"; import { Circle } from "../circle";
import data from "./learning_path_json.json"; import data from "./learning_path_json.json";
@ -6,7 +7,7 @@ describe("Circle.parseJson", () => {
it("can parse circle from api response", () => { it("can parse circle from api response", () => {
const cirleData = data.children.find( const cirleData = data.children.find(
(c) => c.slug === "test-lehrgang-lp-circle-analyse" (c) => c.slug === "test-lehrgang-lp-circle-analyse"
); ) as unknown as WagtailCircle;
const circle = Circle.fromJson(cirleData, undefined); const circle = Circle.fromJson(cirleData, undefined);
expect(circle.learningSequences.length).toBe(3); expect(circle.learningSequences.length).toBe(3);
expect(circle.flatLearningContents.length).toBe(7); expect(circle.flatLearningContents.length).toBe(7);

View File

@ -1,39 +1,59 @@
{ {
"id": 372, "id": 362,
"title": "Test Lernpfad", "title": "Test Lernpfad",
"slug": "test-lehrgang-lp", "slug": "test-lehrgang-lp",
"type": "learnpath.LearningPath", "type": "learnpath.LearningPath",
"translation_key": "42e559ca-970f-4a08-9e5e-63860585ee1e", "translation_key": "8a230aa1-075e-4ac1-a8d6-87642c4f33ba",
"frontend_url": "/learn/test-lehrgang-lp",
"children": [ "children": [
{ {
"id": 373, "id": 363,
"title": "Basis", "title": "Basis",
"slug": "test-lehrgang-lp-topic-basis", "slug": "test-lehrgang-lp-topic-basis",
"type": "learnpath.Topic", "type": "learnpath.Topic",
"translation_key": "d68c1544-cf22-4a59-a81c-8cb977440cd0", "translation_key": "d6e14156-2fb9-4f1b-83ce-6879e364f9a2",
"frontend_url": "",
"is_visible": false "is_visible": false
}, },
{ {
"id": 374, "id": 364,
"title": "Basis", "title": "Basis",
"slug": "test-lehrgang-lp-circle-basis", "slug": "test-lehrgang-lp-circle-basis",
"type": "learnpath.Circle", "type": "learnpath.Circle",
"translation_key": "ec62a2af-6f74-4031-b971-c3287bbbc573", "translation_key": "8034e867-4b05-4509-a9bc-99f9f3619e88",
"frontend_url": "/learn/test-lehrgang-lp/basis",
"children": [ "children": [
{ {
"id": 375, "id": 365,
"title": "Starten", "title": "Starten",
"slug": "test-lehrgang-lp-circle-basis-ls-starten", "slug": "test-lehrgang-lp-circle-basis-ls-starten",
"type": "learnpath.LearningSequence", "type": "learnpath.LearningSequence",
"translation_key": "c5fdada9-036d-4516-a50f-6656a1c6b009", "translation_key": "868bc4cb-c5b5-423e-a890-433184cd06e0",
"frontend_url": "/learn/test-lehrgang-lp/basis#ls-starten",
"icon": "it-icon-ls-start" "icon": "it-icon-ls-start"
}, },
{ {
"id": 376, "id": 366,
"title": "Einf\u00fchrung",
"slug": "test-lehrgang-lp-circle-basis-lu-einf\u00fchrung",
"type": "learnpath.LearningUnit",
"translation_key": "6b0a4794-9861-4ea4-b422-99261a4347a6",
"frontend_url": "/learn/test-lehrgang-lp/basis#lu-einf\u00fchrung",
"evaluate_url": "/learn/test-lehrgang-lp/basis/evaluate/einf\u00fchrung",
"course_category": {
"id": 14,
"title": "Allgemein",
"general": true
},
"children": []
},
{
"id": 367,
"title": "Einf\u00fchrung", "title": "Einf\u00fchrung",
"slug": "test-lehrgang-lp-circle-basis-lc-einf\u00fchrung", "slug": "test-lehrgang-lp-circle-basis-lc-einf\u00fchrung",
"type": "learnpath.LearningContent", "type": "learnpath.LearningContent",
"translation_key": "01de5131-28ce-4b1f-805f-8643384bfd6b", "translation_key": "d1d1b923-f597-4de7-ac44-d02c2f0a1a59",
"frontend_url": "/learn/test-lehrgang-lp/basis/einf\u00fchrung",
"minutes": 15, "minutes": 15,
"contents": [ "contents": [
{ {
@ -42,24 +62,41 @@
"description": "Beispiel Dokument", "description": "Beispiel Dokument",
"url": null "url": null
}, },
"id": "bd05f721-3e9d-4a11-8fe2-7c04e2365f52" "id": "9f22d0b7-643a-4e97-816a-a41141befc95"
} }
] ]
}, },
{ {
"id": 377, "id": 368,
"title": "Beenden", "title": "Beenden",
"slug": "test-lehrgang-lp-circle-basis-ls-beenden", "slug": "test-lehrgang-lp-circle-basis-ls-beenden",
"type": "learnpath.LearningSequence", "type": "learnpath.LearningSequence",
"translation_key": "128c0162-025f-41be-9842-60016a77cdbc", "translation_key": "338208db-7c85-470e-872f-850e34747873",
"frontend_url": "/learn/test-lehrgang-lp/basis#ls-beenden",
"icon": "it-icon-ls-end" "icon": "it-icon-ls-end"
}, },
{ {
"id": 378, "id": 369,
"title": "Beenden",
"slug": "test-lehrgang-lp-circle-basis-lu-beenden",
"type": "learnpath.LearningUnit",
"translation_key": "c14b63d6-3144-41fa-8a3c-2eada6ddd5ea",
"frontend_url": "/learn/test-lehrgang-lp/basis#lu-beenden",
"evaluate_url": "/learn/test-lehrgang-lp/basis/evaluate/beenden",
"course_category": {
"id": 14,
"title": "Allgemein",
"general": true
},
"children": []
},
{
"id": 370,
"title": "Jetzt kann es losgehen!", "title": "Jetzt kann es losgehen!",
"slug": "test-lehrgang-lp-circle-basis-lc-jetzt-kann-es-losgehen", "slug": "test-lehrgang-lp-circle-basis-lc-jetzt-kann-es-losgehen",
"type": "learnpath.LearningContent", "type": "learnpath.LearningContent",
"translation_key": "271896b9-6082-4fd4-9d70-6093ec9cc6ea", "translation_key": "6920bcac-597b-462a-9458-32aa5dc8d3f7",
"frontend_url": "/learn/test-lehrgang-lp/basis/jetzt-kann-es-losgehen",
"minutes": 30, "minutes": 30,
"contents": [ "contents": [
{ {
@ -68,45 +105,130 @@
"description": "Beispiel Dokument", "description": "Beispiel Dokument",
"url": null "url": null
}, },
"id": "204fc13b-a9ae-40de-8e09-f1e922c4fdd9" "id": "1422a7c3-0a9a-4321-88a0-d82d0ed26ba2"
} }
] ]
} }
], ],
"description": "Basis", "description": "Basis",
"job_situations": [], "goal_description": "('In diesem Circle baust du deine Handlungskompetenzen f\u00fcr diese Themen aus:',)",
"goals": [], "goals": [
"experts": [] {
"type": "goal",
"value": "... hier ein Beispieltext f\u00fcr ein Ziel 1",
"id": "38afbda4-7b7e-4f5c-88e8-c595c43e1659"
},
{
"type": "goal",
"value": "... hier ein Beispieltext f\u00fcr ein Ziel 2",
"id": "4d00ac58-0499-4316-9af2-356c37dedc35"
},
{
"type": "goal",
"value": "... hier ein Beispieltext f\u00fcr ein Ziel 3",
"id": "945eb104-8cc1-45cd-a07a-a4d3ec4f39a3"
}
],
"job_situation_description": "Du triffst in diesem Circle auf die folgenden berufstypischen Handlungsfelder:",
"job_situations": [
{
"type": "job_situation",
"value": "Job Situation 1",
"id": "02fb807a-0d07-4353-81ec-8b8b383954d7"
},
{
"type": "job_situation",
"value": "Job Situation 2",
"id": "371952f6-5871-4bf1-b423-d3dab7371001"
},
{
"type": "job_situation",
"value": "Job Situation 3",
"id": "116bfa7b-65e8-44a1-8c82-e8b05fd86a01"
},
{
"type": "job_situation",
"value": "Job Situation 4",
"id": "08baf7dd-8801-4af9-8af8-714989775ddb"
},
{
"type": "job_situation",
"value": "Job Situation 5",
"id": "93ade4b8-c4fb-4941-98c5-e58336fca4bb"
},
{
"type": "job_situation",
"value": "Job Situation 6",
"id": "1fac4ee4-6d86-4e9e-9fa4-a99c6659bc8b"
},
{
"type": "job_situation",
"value": "Job Situation 7",
"id": "06d1e273-dec8-4a0b-ae2c-2baeb7a19ec7"
}
],
"experts": [
{
"type": "person",
"value": {
"first_name": "Patrizia",
"last_name": "Mustermann",
"email": "patrizia.mustermann@example.com",
"photo": null,
"biography": ""
},
"id": "83490f33-da54-4548-baac-af75ea36651e"
}
]
}, },
{ {
"id": 379, "id": 371,
"title": "Beraten der Kunden", "title": "Beraten der Kunden",
"slug": "test-lehrgang-lp-topic-beraten-der-kunden", "slug": "test-lehrgang-lp-topic-beraten-der-kunden",
"type": "learnpath.Topic", "type": "learnpath.Topic",
"translation_key": "91918780-75f8-4db3-8fb8-91b63f08b9b9", "translation_key": "728a2578-a22c-41df-9079-43a5318c5030",
"frontend_url": "",
"is_visible": true "is_visible": true
}, },
{ {
"id": 380, "id": 372,
"title": "Analyse", "title": "Analyse",
"slug": "test-lehrgang-lp-circle-analyse", "slug": "test-lehrgang-lp-circle-analyse",
"type": "learnpath.Circle", "type": "learnpath.Circle",
"translation_key": "50f11be3-a56d-412d-be25-3d272fb5df40", "translation_key": "e429adf5-dd5d-4699-b471-40c782fb507e",
"frontend_url": "/learn/test-lehrgang-lp/analyse",
"children": [ "children": [
{ {
"id": 381, "id": 373,
"title": "Starten", "title": "Starten",
"slug": "test-lehrgang-lp-circle-analyse-ls-starten", "slug": "test-lehrgang-lp-circle-analyse-ls-starten",
"type": "learnpath.LearningSequence", "type": "learnpath.LearningSequence",
"translation_key": "07ac0eb9-3671-4b62-8053-1d0c43a1f0fb", "translation_key": "40e977e0-3668-418d-b838-d3774a5cbe7d",
"frontend_url": "/learn/test-lehrgang-lp/analyse#ls-starten",
"icon": "it-icon-ls-start" "icon": "it-icon-ls-start"
}, },
{ {
"id": 382, "id": 374,
"title": "Einf\u00fchrung",
"slug": "test-lehrgang-lp-circle-analyse-lu-einf\u00fchrung",
"type": "learnpath.LearningUnit",
"translation_key": "badfd186-26c1-433e-90ad-8cac52eb599f",
"frontend_url": "/learn/test-lehrgang-lp/analyse#lu-einf\u00fchrung",
"evaluate_url": "/learn/test-lehrgang-lp/analyse/evaluate/einf\u00fchrung",
"course_category": {
"id": 14,
"title": "Allgemein",
"general": true
},
"children": []
},
{
"id": 375,
"title": "Einleitung Circle \"Analyse\"", "title": "Einleitung Circle \"Analyse\"",
"slug": "test-lehrgang-lp-circle-analyse-lc-einleitung-circle-analyse", "slug": "test-lehrgang-lp-circle-analyse-lc-einleitung-circle-analyse",
"type": "learnpath.LearningContent", "type": "learnpath.LearningContent",
"translation_key": "00ed0ab2-fdb0-4ee6-a7d2-42a219b849a8", "translation_key": "5e8d6478-6287-4658-94c5-ecbd5d624962",
"frontend_url": "/learn/test-lehrgang-lp/analyse/einleitung-circle-analyse",
"minutes": 15, "minutes": 15,
"contents": [ "contents": [
{ {
@ -115,24 +237,27 @@
"description": "Beispiel Dokument", "description": "Beispiel Dokument",
"url": null "url": null
}, },
"id": "892a9a4a-8e1e-4f7e-8c35-9bf3bbe5371b" "id": "8b7f183e-1879-4391-953f-52d9a621f435"
} }
] ]
}, },
{ {
"id": 383, "id": 376,
"title": "Beobachten", "title": "Beobachten",
"slug": "test-lehrgang-lp-circle-analyse-ls-beobachten", "slug": "test-lehrgang-lp-circle-analyse-ls-beobachten",
"type": "learnpath.LearningSequence", "type": "learnpath.LearningSequence",
"translation_key": "4cb08bc2-d101-43cc-b006-8f2bbb1a0579", "translation_key": "35df96df-2e8d-4f16-aee1-8d72990f63a0",
"frontend_url": "/learn/test-lehrgang-lp/analyse#ls-beobachten",
"icon": "it-icon-ls-watch" "icon": "it-icon-ls-watch"
}, },
{ {
"id": 384, "id": 377,
"title": "Fahrzeug", "title": "Fahrzeug",
"slug": "test-lehrgang-lp-circle-analyse-lu-fahrzeug", "slug": "test-lehrgang-lp-circle-analyse-lu-fahrzeug",
"type": "learnpath.LearningUnit", "type": "learnpath.LearningUnit",
"translation_key": "8f4afa40-c27e-48f7-a2d7-0e713479a55e", "translation_key": "405d42e4-ee10-4453-8e5f-82e49bb4d597",
"frontend_url": "/learn/test-lehrgang-lp/analyse#lu-fahrzeug",
"evaluate_url": "/learn/test-lehrgang-lp/analyse/evaluate/fahrzeug",
"course_category": { "course_category": {
"id": 15, "id": 15,
"title": "Fahrzeug", "title": "Fahrzeug",
@ -140,29 +265,32 @@
}, },
"children": [ "children": [
{ {
"id": 397, "id": 391,
"title": "Innerhalb des Handlungsfelds \u00abFahrzeug\u00bb bin ich f\u00e4hig, die Ziele und Pl\u00e4ne des Kunden zu ergr\u00fcnden (SOLL).", "title": "Innerhalb des Handlungsfelds \u00abFahrzeug\u00bb bin ich f\u00e4hig, die Ziele und Pl\u00e4ne des Kunden zu ergr\u00fcnden (SOLL).",
"slug": "test-lehrgang-competence-crit-y13-fahrzeug", "slug": "test-lehrgang-competence-crit-y13-fahrzeug",
"type": "competence.PerformanceCriteria", "type": "competence.PerformanceCriteria",
"translation_key": "e9d49552-7d18-418a-94b6-ebb4ee6bf187", "translation_key": "3b714984-afdb-4456-9c01-a59064724929",
"frontend_url": "",
"competence_id": "Y1.3" "competence_id": "Y1.3"
}, },
{ {
"id": 398, "id": 392,
"title": "Innerhalb des Handlungsfelds \u00abFahrzeug\u00bb bin ich f\u00e4hig, die IST-Situation des Kunden mit der geeigneten Gespr\u00e4chs-/Fragetechnik zu erfassen.", "title": "Innerhalb des Handlungsfelds \u00abFahrzeug\u00bb bin ich f\u00e4hig, die IST-Situation des Kunden mit der geeigneten Gespr\u00e4chs-/Fragetechnik zu erfassen.",
"slug": "test-lehrgang-competence-crit-y21-fahrzeug", "slug": "test-lehrgang-competence-crit-y21-fahrzeug",
"type": "competence.PerformanceCriteria", "type": "competence.PerformanceCriteria",
"translation_key": "5f257b35-c6ca-49e4-9401-a5d02d53926d", "translation_key": "c2850a27-60c5-471b-9fec-ba0baf152e91",
"frontend_url": "",
"competence_id": "Y2.1" "competence_id": "Y2.1"
} }
] ]
}, },
{ {
"id": 385, "id": 378,
"title": "Rafael Fasel wechselt sein Auto", "title": "Rafael Fasel wechselt sein Auto",
"slug": "test-lehrgang-lp-circle-analyse-lc-rafael-fasel-wechselt-sein-auto", "slug": "test-lehrgang-lp-circle-analyse-lc-rafael-fasel-wechselt-sein-auto",
"type": "learnpath.LearningContent", "type": "learnpath.LearningContent",
"translation_key": "fda4f870-9307-414d-b07f-eea607a9afb7", "translation_key": "b7779d45-adf4-41fc-a4a5-e95c732b2224",
"frontend_url": "/learn/test-lehrgang-lp/analyse/rafael-fasel-wechselt-sein-auto",
"minutes": 30, "minutes": 30,
"contents": [ "contents": [
{ {
@ -171,16 +299,17 @@
"description": "In diesem Online-Training lernst du, wie du den Kundenbedarf ermittelst.", "description": "In diesem Online-Training lernst du, wie du den Kundenbedarf ermittelst.",
"url": "" "url": ""
}, },
"id": "700a0f64-0892-4fa5-9e08-3bd34e99edeb" "id": "c79d34cb-0e7e-403d-a672-03d94cf6bdc7"
} }
] ]
}, },
{ {
"id": 386, "id": 379,
"title": "Fachcheck Fahrzeug", "title": "Fachcheck Fahrzeug",
"slug": "test-lehrgang-lp-circle-analyse-lc-fachcheck-fahrzeug", "slug": "test-lehrgang-lp-circle-analyse-lc-fachcheck-fahrzeug",
"type": "learnpath.LearningContent", "type": "learnpath.LearningContent",
"translation_key": "dce0847f-4593-4bba-bd0c-a09c71eb0344", "translation_key": "e395e05c-81bf-4bc6-98e8-3833bebb551c",
"frontend_url": "/learn/test-lehrgang-lp/analyse/fachcheck-fahrzeug",
"minutes": 30, "minutes": 30,
"contents": [ "contents": [
{ {
@ -189,16 +318,18 @@
"description": "Beispiel Test", "description": "Beispiel Test",
"url": null "url": null
}, },
"id": "9f674aaa-ebf0-4a01-adcc-c0c46394fb10" "id": "ac4c67bc-7de9-4e5c-a35e-e13f5766d6cc"
} }
] ]
}, },
{ {
"id": 387, "id": 380,
"title": "Reisen", "title": "Reisen",
"slug": "test-lehrgang-lp-circle-analyse-lu-reisen", "slug": "test-lehrgang-lp-circle-analyse-lu-reisen",
"type": "learnpath.LearningUnit", "type": "learnpath.LearningUnit",
"translation_key": "c3f6d33f-8dbc-4d88-9a81-3c602c4f9cc8", "translation_key": "d0c956cc-3c86-4e08-9990-ed4e85d03219",
"frontend_url": "/learn/test-lehrgang-lp/analyse#lu-reisen",
"evaluate_url": "/learn/test-lehrgang-lp/analyse/evaluate/reisen",
"course_category": { "course_category": {
"id": 16, "id": 16,
"title": "Reisen", "title": "Reisen",
@ -206,21 +337,23 @@
}, },
"children": [ "children": [
{ {
"id": 399, "id": 393,
"title": "Innerhalb des Handlungsfelds \u00abReisen\u00bb bin ich f\u00e4hig, die Ziele und Pl\u00e4ne des Kunden zu ergr\u00fcnden (SOLL).", "title": "Innerhalb des Handlungsfelds \u00abReisen\u00bb bin ich f\u00e4hig, die Ziele und Pl\u00e4ne des Kunden zu ergr\u00fcnden (SOLL).",
"slug": "test-lehrgang-competence-crit-y13-reisen", "slug": "test-lehrgang-competence-crit-y13-reisen",
"type": "competence.PerformanceCriteria", "type": "competence.PerformanceCriteria",
"translation_key": "1e488b69-8a3e-4acc-9547-48c103e0d038", "translation_key": "1df45a12-41f2-4ff5-8580-d5a7caf5dd56",
"frontend_url": "",
"competence_id": "Y1.3" "competence_id": "Y1.3"
} }
] ]
}, },
{ {
"id": 388, "id": 381,
"title": "Reiseversicherung", "title": "Reiseversicherung",
"slug": "test-lehrgang-lp-circle-analyse-lc-reiseversicherung", "slug": "test-lehrgang-lp-circle-analyse-lc-reiseversicherung",
"type": "learnpath.LearningContent", "type": "learnpath.LearningContent",
"translation_key": "ff513aae-efe1-4974-b67f-7a292b8aef86", "translation_key": "bad7439a-8b0c-4877-8d6c-78f292be83d4",
"frontend_url": "/learn/test-lehrgang-lp/analyse/reiseversicherung",
"minutes": 240, "minutes": 240,
"contents": [ "contents": [
{ {
@ -229,16 +362,17 @@
"description": "Beispiel \u00dcbung", "description": "Beispiel \u00dcbung",
"url": null "url": null
}, },
"id": "f35f213e-1a33-49fe-97c5-26e15161719f" "id": "7e1ee533-7f75-495b-a2bc-8bbd2b1311c9"
} }
] ]
}, },
{ {
"id": 389, "id": 382,
"title": "Emma und Ayla campen durch Amerika", "title": "Emma und Ayla campen durch Amerika",
"slug": "test-lehrgang-lp-circle-analyse-lc-emma-und-ayla-campen-durch-amerika", "slug": "test-lehrgang-lp-circle-analyse-lc-emma-und-ayla-campen-durch-amerika",
"type": "learnpath.LearningContent", "type": "learnpath.LearningContent",
"translation_key": "a77b0f9d-9a70-47bd-8e62-7580d70a4306", "translation_key": "27f9d8f3-209c-4d55-94d9-2e70fbfe163b",
"frontend_url": "/learn/test-lehrgang-lp/analyse/emma-und-ayla-campen-durch-amerika",
"minutes": 120, "minutes": 120,
"contents": [ "contents": [
{ {
@ -247,24 +381,41 @@
"description": "Beispiel \u00dcbung", "description": "Beispiel \u00dcbung",
"url": "/static/media/web_based_trainings/story-06-a-01-emma-und-ayla-campen-durch-amerika-einstieg/scormcontent/index.html" "url": "/static/media/web_based_trainings/story-06-a-01-emma-und-ayla-campen-durch-amerika-einstieg/scormcontent/index.html"
}, },
"id": "60f087ff-fa3a-4da2-820f-4fcdf449f70d" "id": "b08e1851-8583-4428-b1bc-402c7095130b"
} }
] ]
}, },
{ {
"id": 390, "id": 383,
"title": "Beenden", "title": "Beenden",
"slug": "test-lehrgang-lp-circle-analyse-ls-beenden", "slug": "test-lehrgang-lp-circle-analyse-ls-beenden",
"type": "learnpath.LearningSequence", "type": "learnpath.LearningSequence",
"translation_key": "06f1e998-b827-41cc-8129-d72d731719c1", "translation_key": "68be244d-0e00-4700-834c-57b4db366fc1",
"frontend_url": "/learn/test-lehrgang-lp/analyse#ls-beenden",
"icon": "it-icon-ls-end" "icon": "it-icon-ls-end"
}, },
{ {
"id": 391, "id": 384,
"title": "Beenden",
"slug": "test-lehrgang-lp-circle-analyse-lu-beenden",
"type": "learnpath.LearningUnit",
"translation_key": "d594db87-ad78-491b-bf1b-410adfa3a0ba",
"frontend_url": "/learn/test-lehrgang-lp/analyse#lu-beenden",
"evaluate_url": "/learn/test-lehrgang-lp/analyse/evaluate/beenden",
"course_category": {
"id": 14,
"title": "Allgemein",
"general": true
},
"children": []
},
{
"id": 385,
"title": "KompetenzNavi anschauen", "title": "KompetenzNavi anschauen",
"slug": "test-lehrgang-lp-circle-analyse-lc-kompetenzprofil-anschauen", "slug": "test-lehrgang-lp-circle-analyse-lc-kompetenznavi-anschauen",
"type": "learnpath.LearningContent", "type": "learnpath.LearningContent",
"translation_key": "6cc47dc1-a74f-4cbf-afa6-23885891c82f", "translation_key": "8ee57ba5-e09e-4058-937a-b733ea72b969",
"frontend_url": "/learn/test-lehrgang-lp/analyse/kompetenznavi-anschauen",
"minutes": 30, "minutes": 30,
"contents": [ "contents": [
{ {
@ -273,16 +424,17 @@
"description": "Beispiel Dokument", "description": "Beispiel Dokument",
"url": null "url": null
}, },
"id": "3f685055-4e3e-4ca9-93af-bac19236931d" "id": "3ef87e69-5e5c-415a-934c-ed47ad9fdd93"
} }
] ]
}, },
{ {
"id": 392, "id": 386,
"title": "Circle \"Analyse\" abschliessen", "title": "Circle \"Analyse\" abschliessen",
"slug": "test-lehrgang-lp-circle-analyse-lc-circle-analyse-abschliessen", "slug": "test-lehrgang-lp-circle-analyse-lc-circle-analyse-abschliessen",
"type": "learnpath.LearningContent", "type": "learnpath.LearningContent",
"translation_key": "9b32e2cd-1368-4885-a79b-906b45ba04bc", "translation_key": "90d9ab63-cc0f-492f-aad1-f7d448ee5b2c",
"frontend_url": "/learn/test-lehrgang-lp/analyse/circle-analyse-abschliessen",
"minutes": 30, "minutes": 30,
"contents": [ "contents": [
{ {
@ -291,34 +443,36 @@
"description": "Beispiel Dokument", "description": "Beispiel Dokument",
"url": null "url": null
}, },
"id": "650b7b15-b522-4df7-ac5b-6a654f12334f" "id": "21415232-862b-488c-9987-4f4ee369a854"
} }
] ]
} }
], ],
"description": "Unit-Test Circle", "description": "Unit-Test Circle",
"job_situations": [ "goal_description": "('In diesem Circle baust du deine Handlungskompetenzen f\u00fcr diese Themen aus:',)",
{
"type": "job_situation",
"value": "Autoversicherung",
"id": "c5a6b365-0a18-47d5-b6e1-6cb8b8ec7d35"
},
{
"type": "job_situation",
"value": "Autokauf",
"id": "e969d2a2-b383-482c-a721-88552af086a6"
}
],
"goals": [ "goals": [
{ {
"type": "goal", "type": "goal",
"value": "... die heutige Versicherungssituation von Privat- oder Gesch\u00e4ftskunden einzusch\u00e4tzen.", "value": "... die heutige Versicherungssituation von Privat- oder Gesch\u00e4ftskunden einzusch\u00e4tzen.",
"id": "d9ad8aed-d7d6-42c7-b6d4-65102c8ddf10" "id": "d1f001fa-f7b8-41a3-90c7-632260ff7054"
}, },
{ {
"type": "goal", "type": "goal",
"value": "... deinem Kunden seine optimale L\u00f6sung aufzuzeigen", "value": "... deinem Kunden seine optimale L\u00f6sung aufzuzeigen",
"id": "2506950c-45cb-474f-acb9-45e83e9ebe1b" "id": "8f73bb0f-e898-4961-ab28-dd34caca2c0b"
}
],
"job_situation_description": "Du triffst in diesem Circle auf die folgenden berufstypischen Handlungsfelder:",
"job_situations": [
{
"type": "job_situation",
"value": "Autoversicherung",
"id": "df46930b-2911-4161-a677-75b4b156dff3"
},
{
"type": "job_situation",
"value": "Autokauf",
"id": "17a6d252-e942-44cc-920f-015e38e727be"
} }
], ],
"experts": [ "experts": [
@ -331,14 +485,14 @@
"photo": null, "photo": null,
"biography": "" "biography": ""
}, },
"id": "b7b0ff2e-f840-4d74-99c1-c7a5ee6dc14e" "id": "b0633305-5e74-43eb-93b8-ebbcfb1b17d1"
} }
] ]
} }
], ],
"course": { "course": {
"id": -1, "id": -1,
"title": "Test Lerngang", "title": "Test Lehrgang",
"category_name": "Handlungsfeld" "category_name": "Handlungsfeld"
} }
} }

View File

@ -5,25 +5,25 @@ import requests
def main(): def main():
client = requests.session() client = requests.session()
client.get('http://localhost:8000/') client.get("http://localhost:8001/")
client.post( client.post(
'http://localhost:8000/api/core/login/', "http://localhost:8001/api/core/login/",
json={ json={
'username': 'admin', "username": "admin",
'password': 'test', "password": "test",
} },
) )
response = client.get( response = client.get(
'http://localhost:8000/api/course/page/test-lehrgang-lp/', "http://localhost:8001/api/course/page/test-lehrgang-lp/",
) )
print(response.status_code) print(response.status_code)
print(response.json()) print(response.json())
with open('learning_path_json.json', 'w') as f: with open("learning_path_json.json", "w") as f:
f.write(json.dumps(response.json(), indent=4)) f.write(json.dumps(response.json(), indent=4))
if __name__ == '__main__': if __name__ == "__main__":
main() main()

View File

@ -4,34 +4,13 @@ import type {
CircleGoal, CircleGoal,
CircleJobSituation, CircleJobSituation,
CourseCompletion, CourseCompletion,
CourseCompletionStatus,
CourseWagtailPage,
LearningContent, LearningContent,
LearningSequence, LearningSequence,
LearningUnit, LearningUnit,
LearningUnitPerformanceCriteria, LearningUnitPerformanceCriteria,
WagtailCircle,
} from "@/types"; } from "@/types";
function _createEmptyLearningUnit(
parentLearningSequence: LearningSequence
): LearningUnit {
return {
id: 0,
title: "",
slug: "",
translation_key: "",
type: "learnpath.LearningUnit",
frontend_url: "",
learningContents: [],
minutes: 0,
parentLearningSequence: parentLearningSequence,
children: [],
last: true,
completion_status: "unknown",
evaluate_url: "",
};
}
export function parseLearningSequences( export function parseLearningSequences(
circle: Circle, circle: Circle,
children: CircleChild[] children: CircleChild[]
@ -47,23 +26,15 @@ export function parseLearningSequences(
if (learningSequence) { if (learningSequence) {
if (learningUnit) { if (learningUnit) {
learningUnit.last = true; learningUnit.last = true;
learningSequence.learningUnits.push(learningUnit);
} }
result.push(learningSequence);
} }
learningSequence = Object.assign(child, { learningUnits: [] }); learningSequence = Object.assign(child, { learningUnits: [] });
result.push(learningSequence);
// initialize empty learning unit if there will not come a learning unit next
learningUnit = _createEmptyLearningUnit(learningSequence);
} else if (child.type === "learnpath.LearningUnit") { } else if (child.type === "learnpath.LearningUnit") {
if (!learningSequence) { if (!learningSequence) {
throw new Error("LearningUnit found before LearningSequence"); throw new Error("LearningUnit found before LearningSequence");
} }
if (learningUnit && learningUnit.learningContents.length) {
learningSequence.learningUnits.push(learningUnit);
}
learningUnit = Object.assign(child, { learningUnit = Object.assign(child, {
learningContents: [], learningContents: [],
parentLearningSequence: learningSequence, parentLearningSequence: learningSequence,
@ -74,9 +45,10 @@ export function parseLearningSequences(
return c; return c;
}), }),
}); });
learningSequence.learningUnits.push(learningUnit);
} else if (child.type === "learnpath.LearningContent") { } else if (child.type === "learnpath.LearningContent") {
if (!learningUnit) { if (!learningUnit) {
throw new Error("LearningContent found before LearningUnit"); throw new Error(`LearningContent found before LearningUnit ${child.slug}`);
} }
previousLearningContent = learningContent; previousLearningContent = learningContent;
@ -92,14 +64,13 @@ export function parseLearningSequences(
} }
learningUnit.learningContents.push(child); learningUnit.learningContents.push(child);
} else {
throw new Error("Unknown CircleChild found...");
} }
}); });
if (learningUnit && learningSequence) { if (learningUnit) {
// TypeScript does not get it here...
learningUnit.last = true; learningUnit.last = true;
(learningSequence as LearningSequence).learningUnits.push(learningUnit);
result.push(learningSequence);
} else { } else {
throw new Error( throw new Error(
"Finished with LearningContent but there is no LearningSequence and LearningUnit" "Finished with LearningContent but there is no LearningSequence and LearningUnit"
@ -121,10 +92,9 @@ export function parseLearningSequences(
return result; return result;
} }
export class Circle implements CourseWagtailPage { export class Circle implements WagtailCircle {
readonly type = "learnpath.Circle"; readonly type = "learnpath.Circle";
readonly learningSequences: LearningSequence[]; readonly learningSequences: LearningSequence[];
completion_status: CourseCompletionStatus = "unknown";
nextCircle?: Circle; nextCircle?: Circle;
previousCircle?: Circle; previousCircle?: Circle;
@ -136,30 +106,33 @@ export class Circle implements CourseWagtailPage {
public readonly translation_key: string, public readonly translation_key: string,
public readonly frontend_url: string, public readonly frontend_url: string,
public readonly description: string, public readonly description: string,
public children: CircleChild[], public readonly children: CircleChild[],
public goal_description: string, public readonly goal_description: string,
public goals: CircleGoal[], public readonly goals: CircleGoal[],
public job_situation_description: string, public readonly job_situation_description: string,
public job_situations: CircleJobSituation[], public readonly job_situations: CircleJobSituation[],
public readonly parentLearningPath?: LearningPath public readonly parentLearningPath?: LearningPath
) { ) {
this.learningSequences = parseLearningSequences(this, this.children); this.learningSequences = parseLearningSequences(this, this.children);
} }
public static fromJson(json: any, learningPath?: LearningPath): Circle { public static fromJson(
wagtailCircle: WagtailCircle,
learningPath?: LearningPath
): Circle {
// TODO add error checking when the data does not conform to the schema // TODO add error checking when the data does not conform to the schema
return new Circle( return new Circle(
json.id, wagtailCircle.id,
json.slug, wagtailCircle.slug,
json.title, wagtailCircle.title,
json.translation_key, wagtailCircle.translation_key,
json.frontend_url, wagtailCircle.frontend_url,
json.description, wagtailCircle.description,
json.children, wagtailCircle.children,
json.goal_description, wagtailCircle.goal_description,
json.goals, wagtailCircle.goals,
json.job_situation_description, wagtailCircle.job_situation_description,
json.job_situations, wagtailCircle.job_situations,
learningPath learningPath
); );
} }

View File

@ -2,12 +2,12 @@ import * as _ from "lodash";
import { Circle } from "@/services/circle"; import { Circle } from "@/services/circle";
import type { import type {
Course,
CourseCompletion, CourseCompletion,
CourseCompletionStatus,
CourseWagtailPage,
LearningContent, LearningContent,
LearningPathChild, LearningPathChild,
Topic, Topic,
WagtailLearningPath,
} from "@/types"; } from "@/types";
function getLastCompleted(courseId: number, completionData: CourseCompletion[]) { function getLastCompleted(courseId: number, completionData: CourseCompletion[]) {
@ -22,21 +22,23 @@ function getLastCompleted(courseId: number, completionData: CourseCompletion[])
); );
} }
export class LearningPath implements CourseWagtailPage { export class LearningPath implements WagtailLearningPath {
readonly type = "learnpath.LearningPath"; readonly type = "learnpath.LearningPath";
public topics: Topic[]; public topics: Topic[];
public circles: Circle[]; public circles: Circle[];
public nextLearningContent?: LearningContent; public nextLearningContent?: LearningContent;
readonly completion_status: CourseCompletionStatus = "unknown";
public static fromJson(json: any, completionData: CourseCompletion[]): LearningPath { public static fromJson(
json: WagtailLearningPath,
completionData: CourseCompletion[]
): LearningPath {
return new LearningPath( return new LearningPath(
json.id, json.id,
json.slug, json.slug,
json.course.title, json.course.title,
json.translation_key, json.translation_key,
json.frontend_url, json.frontend_url,
json.course.id, json.course,
json.children, json.children,
completionData completionData
); );
@ -48,7 +50,7 @@ export class LearningPath implements CourseWagtailPage {
public readonly title: string, public readonly title: string,
public readonly translation_key: string, public readonly translation_key: string,
public readonly frontend_url: string, public readonly frontend_url: string,
public readonly courseId: number, public readonly course: Course,
public children: LearningPathChild[], public children: LearningPathChild[],
completionData?: CourseCompletion[] completionData?: CourseCompletion[]
) { ) {
@ -95,7 +97,7 @@ export class LearningPath implements CourseWagtailPage {
this.nextLearningContent = undefined; this.nextLearningContent = undefined;
const lastCompletedLearningContent = getLastCompleted( const lastCompletedLearningContent = getLastCompleted(
this.courseId, this.course.id,
completionData completionData
); );

View File

@ -8,6 +8,7 @@ import type {
LearningContent, LearningContent,
LearningUnit, LearningUnit,
LearningUnitPerformanceCriteria, LearningUnitPerformanceCriteria,
PerformanceCriteria,
} from "@/types"; } from "@/types";
import { defineStore } from "pinia"; import { defineStore } from "pinia";
@ -85,16 +86,22 @@ export const useCircleStore = defineStore({
return learningUnit; return learningUnit;
}, },
async markCompletion( async markCompletion(
page: LearningContent | LearningUnitPerformanceCriteria, page:
| LearningContent
| LearningUnitPerformanceCriteria
| PerformanceCriteria
| undefined,
completion_status: CourseCompletionStatus = "success" completion_status: CourseCompletionStatus = "success"
) { ) {
const completionStore = useCompletionStore(); const completionStore = useCompletionStore();
try { try {
page.completion_status = completion_status; if (page) {
const completionData = await completionStore.markPage(page); page.completion_status = completion_status;
if (this.circle) { const completionData = await completionStore.markPage(page);
this.circle.parseCompletionData(completionData); if (this.circle) {
this.circle.parseCompletionData(completionData);
}
} }
} catch (error) { } catch (error) {
log.error(error); log.error(error);

View File

@ -1,9 +1,9 @@
import { itGet } from "@/fetchHelpers"; import { itGet } from "@/fetchHelpers";
import { useCompletionStore } from "@/stores/completion"; import { useCompletionStore } from "@/stores/completion";
import type { import type {
BaseCourseWagtailPage,
CompetencePage, CompetencePage,
CompetenceProfilePage, CompetenceProfilePage,
CourseWagtailPage,
PerformanceCriteria, PerformanceCriteria,
} from "@/types"; } from "@/types";
import _ from "lodash"; import _ from "lodash";
@ -103,9 +103,11 @@ export const useCompetenceStore = defineStore({
this.competenceProfilePage = competenceProfilePageData; this.competenceProfilePage = competenceProfilePageData;
const circles = competenceProfilePageData.circles.map((c: CourseWagtailPage) => { const circles = competenceProfilePageData.circles.map(
return { id: c.translation_key, name: `Circle: ${c.title}` }; (c: BaseCourseWagtailPage) => {
}); return { id: c.translation_key, name: `Circle: ${c.title}` };
}
);
this.availableCircles = [{ id: "all", name: "Circle: Alle" }, ...circles]; this.availableCircles = [{ id: "all", name: "Circle: Alle" }, ...circles];
await this.parseCompletionData(); await this.parseCompletionData();

View File

@ -1,5 +1,5 @@
import { itGet, itPost } from "@/fetchHelpers"; import { itGet, itPost } from "@/fetchHelpers";
import type { CourseCompletion, CourseWagtailPage } from "@/types"; import type { BaseCourseWagtailPage, CourseCompletion } from "@/types";
import { defineStore } from "pinia"; import { defineStore } from "pinia";
export type CompletionStoreState = { export type CompletionStoreState = {
@ -28,7 +28,7 @@ export const useCompletionStore = defineStore({
this.completionData = completionData; this.completionData = completionData;
return this.completionData || []; return this.completionData || [];
}, },
async markPage(page: CourseWagtailPage) { async markPage(page: BaseCourseWagtailPage) {
const completionData = await itPost("/api/course/completion/mark/", { const completionData = await itPost("/api/course/completion/mark/", {
page_key: page.translation_key, page_key: page.translation_key,
completion_status: page.completion_status, completion_status: page.completion_status,

View File

@ -3,105 +3,132 @@ import type { Component } from "vue";
export type CourseCompletionStatus = "unknown" | "fail" | "success"; export type CourseCompletionStatus = "unknown" | "fail" | "success";
export type LearningContentType = export interface BaseCourseWagtailPage {
| "assignment" readonly id: number;
| "book" readonly title: string;
| "document" readonly slug: string;
| "exercise" readonly type: string;
| "media_library" readonly translation_key: string;
| "online_training" readonly frontend_url: string;
| "resource" completion_status?: CourseCompletionStatus;
| "test" completion_status_updated_at?: string;
| "video"
| "placeholder";
export interface LearningContentBlock {
type: LearningContentType;
value: {
description: string;
};
id: string;
} }
export interface AssignmentBlock { export interface CircleLight {
type: "assignment"; readonly id: number;
value: { readonly title: string;
description: string; readonly translation_key: string;
url: string;
};
id: string;
} }
export interface BookBlock { export interface BaseLearningContentBlock {
type: "book"; readonly type: string;
value: { readonly id: string;
readonly value: {
description: string; description: string;
url: string; url: string;
text?: string;
}; };
id: string;
} }
export interface DocumentBlock { export interface AssignmentBlock extends BaseLearningContentBlock {
type: "document"; readonly type: "assignment";
value: {
description: string;
url: string;
};
id: string;
} }
export interface ExerciseBlock { export interface BookBlock extends BaseLearningContentBlock {
type: "exercise"; readonly type: "book";
value: {
description: string;
url: string;
};
id: string;
} }
export interface MediaLibraryBlock { export interface DocumentBlock extends BaseLearningContentBlock {
type: "media_library"; readonly type: "document";
value: {
description: string;
url: string;
};
id: string;
} }
export interface OnlineTrainingBlock { export interface ExerciseBlock extends BaseLearningContentBlock {
type: "online_training"; readonly type: "exercise";
value: {
description: string;
url: string;
};
id: string;
} }
export interface ResourceBlock { export interface MediaLibraryBlock extends BaseLearningContentBlock {
type: "resource"; readonly type: "media_library";
value: {
description: string;
url: string;
};
id: string;
} }
export interface TestBlock { export interface OnlineTrainingBlock extends BaseLearningContentBlock {
type: "test"; readonly type: "online_training";
value: {
description: string;
url: string;
};
id: string;
} }
export interface VideoBlock { export interface ResourceBlock extends BaseLearningContentBlock {
type: "video"; readonly type: "resource";
value: { }
description: string;
url: string; export interface TestBlock extends BaseLearningContentBlock {
}; readonly type: "test";
id: string; }
export interface VideoBlock extends BaseLearningContentBlock {
readonly type: "video";
}
export interface PlaceholderBlock extends BaseLearningContentBlock {
readonly type: "placeholder";
}
export type LearningContentBlock =
| AssignmentBlock
| BookBlock
| DocumentBlock
| ExerciseBlock
| MediaLibraryBlock
| OnlineTrainingBlock
| ResourceBlock
| TestBlock
| VideoBlock
| PlaceholderBlock;
export type LearningContentType = LearningContentBlock["type"];
export interface LearningContent extends BaseCourseWagtailPage {
readonly type: "learnpath.LearningContent";
minutes: number;
contents: LearningContentBlock[];
parentCircle: Circle;
parentLearningSequence?: LearningSequence;
parentLearningUnit?: LearningUnit;
nextLearningContent?: LearningContent;
previousLearningContent?: LearningContent;
}
export interface LearningUnitPerformanceCriteria extends BaseCourseWagtailPage {
readonly type: "competence.PerformanceCriteria";
readonly competence_id: string;
parentLearningSequence?: LearningSequence;
parentLearningUnit?: LearningUnit;
}
export interface LearningUnit extends BaseCourseWagtailPage {
readonly type: "learnpath.LearningUnit";
readonly evaluate_url: string;
readonly course_category: CourseCategory;
children: LearningUnitPerformanceCriteria[];
// additional frontend fields
learningContents: LearningContent[];
minutes: number;
parentLearningSequence?: LearningSequence;
parentCircle?: Circle;
last?: boolean;
}
export interface LearningSequence extends BaseCourseWagtailPage {
readonly type: "learnpath.LearningSequence";
icon: string;
learningUnits: LearningUnit[];
minutes: number;
}
export type CircleChild = LearningContent | LearningUnit | LearningSequence;
export interface WagtailLearningPath extends BaseCourseWagtailPage {
readonly type: "learnpath.LearningPath";
course: Course;
children: LearningPathChild[];
} }
export interface CircleGoal { export interface CircleGoal {
@ -116,83 +143,19 @@ export interface CircleJobSituation {
id: string; id: string;
} }
export interface CourseWagtailPage { export interface WagtailCircle extends BaseCourseWagtailPage {
readonly id: number; readonly type: "learnpath.Circle";
readonly title: string; readonly description: string;
readonly slug: string; readonly goal_description: string;
readonly translation_key: string; readonly goals: CircleGoal[];
readonly frontend_url: string; readonly job_situation_description: string;
completion_status: CourseCompletionStatus; readonly job_situations: CircleJobSituation[];
completion_status_updated_at?: string; readonly children: CircleChild[];
} }
export interface CircleLight { export interface Topic extends BaseCourseWagtailPage {
readonly id: number; readonly type: "learnpath.Topic";
readonly title: string; readonly is_visible: boolean;
readonly slug: string;
readonly translation_key: string;
}
export interface LearningContent extends CourseWagtailPage {
type: "learnpath.LearningContent";
minutes: number;
contents: (
| AssignmentBlock
| BookBlock
| DocumentBlock
| ExerciseBlock
| MediaLibraryBlock
| OnlineTrainingBlock
| ResourceBlock
| TestBlock
| VideoBlock
)[];
parentCircle: Circle;
parentLearningSequence?: LearningSequence;
parentLearningUnit?: LearningUnit;
nextLearningContent?: LearningContent;
previousLearningContent?: LearningContent;
}
export interface LearningUnitPerformanceCriteria extends CourseWagtailPage {
type: "competence.PerformanceCriteria";
parentLearningSequence?: LearningSequence;
parentLearningUnit?: LearningUnit;
}
export interface LearningUnit extends CourseWagtailPage {
type: "learnpath.LearningUnit";
learningContents: LearningContent[];
evaluate_url: string;
minutes: number;
parentLearningSequence?: LearningSequence;
parentCircle?: Circle;
children: LearningUnitPerformanceCriteria[];
last?: boolean;
}
export interface LearningSequence extends CourseWagtailPage {
type: "learnpath.LearningSequence";
icon: string;
learningUnits: LearningUnit[];
minutes: number;
}
export type CircleChild =
| LearningContent
| LearningUnit
| LearningSequence
| LearningUnitPerformanceCriteria;
export interface WagtailCircle extends CourseWagtailPage {
type: "learnpath.Circle";
children: CircleChild[];
description: string;
}
export interface Topic extends CourseWagtailPage {
type: "learnpath.Topic";
is_visible: boolean;
circles: Circle[]; circles: Circle[];
} }
@ -208,7 +171,7 @@ export interface CourseCompletion {
page_slug: string; page_slug: string;
course: number; course: number;
completion_status: CourseCompletionStatus; completion_status: CourseCompletionStatus;
additional_json_data: any; additional_json_data: unknown;
} }
export interface CircleDiagramData { export interface CircleDiagramData {
@ -224,7 +187,7 @@ export interface CircleDiagramData {
export interface Course { export interface Course {
id: number; id: number;
name: string; title: string;
category_name: string; category_name: string;
} }
@ -267,20 +230,24 @@ export interface RelativeLinkBlock {
value: MediaLibraryContentBlockValue; value: MediaLibraryContentBlockValue;
} }
export type MediaBlock =
| LearnMediaBlock
| ExternalLinkBlock
| InternalLinkBlock
| RelativeLinkBlock;
export type MediaBlockType = MediaBlock["type"];
export interface MediaContentCollection { export interface MediaContentCollection {
type: "content_collection"; type: "content_collection";
value: { value: {
title: string; title: string;
contents: ( description: string;
| LearnMediaBlock contents: MediaBlock[];
| ExternalLinkBlock
| InternalLinkBlock
| RelativeLinkBlock
)[];
}; };
} }
export interface MediaCategoryPage extends CourseWagtailPage { export interface MediaCategoryPage extends BaseCourseWagtailPage {
type: "media_library.MediaCategoryPage"; type: "media_library.MediaCategoryPage";
overview_icon: string; overview_icon: string;
introduction_text: string; introduction_text: string;
@ -290,36 +257,36 @@ export interface MediaCategoryPage extends CourseWagtailPage {
type: "item"; type: "item";
value: string; value: string;
id: string; id: string;
}; }[];
course_category: CourseCategory; course_category: CourseCategory;
body: MediaContentCollection[]; body: MediaContentCollection[];
} }
export interface MediaLibraryPage extends CourseWagtailPage { export interface MediaLibraryPage extends BaseCourseWagtailPage {
type: "media_library.MediaLibraryPage"; readonly type: "media_library.MediaLibraryPage";
course: Course; readonly course: Course;
children: MediaCategoryPage[]; readonly children: MediaCategoryPage[];
} }
export interface PerformanceCriteria extends CourseWagtailPage { export interface PerformanceCriteria extends BaseCourseWagtailPage {
type: "competence.PerformanceCriteria"; readonly type: "competence.PerformanceCriteria";
competence_id: string; readonly competence_id: string;
circle: CircleLight; readonly circle: CircleLight;
course_category: CourseCategory; readonly course_category: CourseCategory;
learning_unit: CourseWagtailPage; readonly learning_unit: BaseCourseWagtailPage;
} }
export interface CompetencePage extends CourseWagtailPage { export interface CompetencePage extends BaseCourseWagtailPage {
type: "competence.CompetencePage"; readonly type: "competence.CompetencePage";
competence_id: string; readonly competence_id: string;
children: PerformanceCriteria[]; readonly children: PerformanceCriteria[];
} }
export interface CompetenceProfilePage extends CourseWagtailPage { export interface CompetenceProfilePage extends BaseCourseWagtailPage {
type: "competence.CompetenceProfilePage"; readonly type: "competence.CompetenceProfilePage";
course: Course; readonly course: Course;
circles: CircleLight[]; readonly circles: CircleLight[];
children: CompetencePage[]; readonly children: CompetencePage[];
} }
// dropdown // dropdown

View File

@ -1,14 +1,32 @@
import type { LearningContentType } from "@/types"; import type { LearningContentType } from "@/types";
import { assertUnreachable } from "@/utils/utils";
export const learningContentTypesToName = new Map<LearningContentType, string>([ export function learningContentTypeData(t: LearningContentType): {
["assignment", "Transferauftrag"], title: string;
["book", "Buch"], icon: string;
["document", "Dokument"], } {
["exercise", "Übung"], switch (t) {
["media_library", "Mediathek"], case "assignment":
["online_training", "Online-Training"], return { title: "Transferauftrag", icon: "it-icon-lc-assignment" };
["video", "Video"], case "book":
["test", "Test"], return { title: "Buch", icon: "it-icon-lc-book" };
["resource", "Seite"], case "document":
["placeholder", "In Umsetzung"], return { title: "Dokument", icon: "it-icon-lc-document" };
]); case "exercise":
return { title: "Übung", icon: "it-icon-lc-exercise" };
case "media_library":
return { title: "Mediathek", icon: "it-icon-lc-media-library" };
case "online_training":
return { title: "Online-Training", icon: "it-icon-lc-online-training" };
case "video":
return { title: "Video", icon: "it-icon-lc-video" };
case "test":
return { title: "Test", icon: "it-icon-lc-test" };
case "resource":
return { title: "Ressource", icon: "it-icon-lc-resource" };
case "placeholder":
return { title: "In Umsetzung", icon: "it-icon-lc-document" };
}
return assertUnreachable(t);
}

View File

@ -0,0 +1,3 @@
export function assertUnreachable(x: never): never {
throw new Error("Didn't expect to get here");
}

View File

@ -6,8 +6,8 @@ set -e
echo 'prettier:check' echo 'prettier:check'
(cd client && npm run prettier:check) (cd client && npm run prettier:check)
echo 'lint' echo 'lint and typecheck'
(cd client && npm run lint) (cd client && npm run lint && npm run typecheck)
echo 'python ufmt check' echo 'python ufmt check'
ufmt check server ufmt check server

View File

@ -185,8 +185,6 @@ gunicorn==20.1.0
# via -r requirements.in # via -r requirements.in
h11==0.13.0 h11==0.13.0
# via uvicorn # via uvicorn
hiredis==2.0.0
# via -r requirements.in
html5lib==1.1 html5lib==1.1
# via wagtail # via wagtail
httptools==0.4.0 httptools==0.4.0

View File

@ -5,7 +5,6 @@ Pillow # https://github.com/python-pillow/Pillow
argon2-cffi # https://github.com/hynek/argon2_cffi argon2-cffi # https://github.com/hynek/argon2_cffi
whitenoise # https://github.com/evansd/whitenoise whitenoise # https://github.com/evansd/whitenoise
redis # https://github.com/redis/redis-py redis # https://github.com/redis/redis-py
hiredis # https://github.com/redis/hiredis-py
uvicorn[standard] # https://github.com/encode/uvicorn uvicorn[standard] # https://github.com/encode/uvicorn
environs environs

View File

@ -106,8 +106,6 @@ gunicorn==20.1.0
# via -r requirements.in # via -r requirements.in
h11==0.13.0 h11==0.13.0
# via uvicorn # via uvicorn
hiredis==2.0.0
# via -r requirements.in
html5lib==1.1 html5lib==1.1
# via wagtail # via wagtail
httptools==0.4.0 httptools==0.4.0

View File

@ -13,7 +13,7 @@ class CompetenceAPITestCase(APITestCase):
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_get_learnpathPage(self): def test_get_compentence_page(self):
slug = "test-lehrgang-competence" slug = "test-lehrgang-competence"
competence_profile = CompetenceProfilePage.objects.get(slug=slug) competence_profile = CompetenceProfilePage.objects.get(slug=slug)
response = self.client.get(f"/api/course/page/{slug}/") response = self.client.get(f"/api/course/page/{slug}/")

View File

@ -96,6 +96,7 @@ def create_test_learning_path(user=None, skip_locales=True):
LearningSequenceFactory( LearningSequenceFactory(
title="Starten", parent=circle_basis, icon="it-icon-ls-start" title="Starten", parent=circle_basis, icon="it-icon-ls-start"
) )
LearningUnitFactory(title="Einführung", parent=circle_basis)
LearningContentFactory( LearningContentFactory(
title="Einführung", title="Einführung",
parent=circle_basis, parent=circle_basis,
@ -103,6 +104,7 @@ def create_test_learning_path(user=None, skip_locales=True):
contents=[("document", DocumentBlockFactory())], contents=[("document", DocumentBlockFactory())],
) )
LearningSequenceFactory(title="Beenden", parent=circle_basis, icon="it-icon-ls-end") LearningSequenceFactory(title="Beenden", parent=circle_basis, icon="it-icon-ls-end")
LearningUnitFactory(title="Beenden", parent=circle_basis)
LearningContentFactory( LearningContentFactory(
title="Jetzt kann es losgehen!", title="Jetzt kann es losgehen!",
parent=circle_basis, parent=circle_basis,
@ -140,6 +142,7 @@ def create_test_learning_path(user=None, skip_locales=True):
) )
LearningSequenceFactory(title="Starten", parent=circle, icon="it-icon-ls-start") LearningSequenceFactory(title="Starten", parent=circle, icon="it-icon-ls-start")
LearningUnitFactory(title="Einführung", parent=circle)
LearningContentFactory( LearningContentFactory(
title=f'Einleitung Circle "Analyse"', title=f'Einleitung Circle "Analyse"',
parent=circle, parent=circle,
@ -204,6 +207,7 @@ def create_test_learning_path(user=None, skip_locales=True):
) )
LearningSequenceFactory(title="Beenden", parent=circle, icon="it-icon-ls-end") LearningSequenceFactory(title="Beenden", parent=circle, icon="it-icon-ls-end")
LearningUnitFactory(title="Beenden", parent=circle)
LearningContentFactory( LearningContentFactory(
title="KompetenzNavi anschauen", title="KompetenzNavi anschauen",
parent=circle, parent=circle,

View File

@ -273,7 +273,7 @@ wichtige Grundlage für eine erfolgreiche Beziehung.
VideoBlockFactory( VideoBlockFactory(
url="https://onedrive.live.com/embed?cid=26E4A934B79DCE5E&resid=26E4A934B79DCE5E%2153350&authkey=AId6i7z_X8l2fHw", url="https://onedrive.live.com/embed?cid=26E4A934B79DCE5E&resid=26E4A934B79DCE5E%2153350&authkey=AId6i7z_X8l2fHw",
description="In dieser Circle zeigt dir ein Fachexperte anhand von Kundensituationen, wie du erfolgreich" description="In dieser Circle zeigt dir ein Fachexperte anhand von Kundensituationen, wie du erfolgreich"
"den Kundenbedarf ermitteln, analysieren, priorisieren und anschliessend zusammenfassen kannst.", "den Kundenbedarf ermitteln, analysieren, priorisieren und anschliessend zusammenfassen kannst.",
), ),
) )
], ],

View File

@ -26,14 +26,6 @@ class DocumentBlock(blocks.StructBlock):
icon = "media" icon = "media"
class PlaceholderBlock(blocks.StructBlock):
description = blocks.TextBlock()
url = blocks.TextBlock()
class Meta:
icon = "media"
class ExerciseBlock(blocks.StructBlock): class ExerciseBlock(blocks.StructBlock):
description = blocks.TextBlock() description = blocks.TextBlock()
url = blocks.TextBlock() url = blocks.TextBlock()
@ -82,3 +74,11 @@ class VideoBlock(blocks.StructBlock):
class Meta: class Meta:
icon = "media" icon = "media"
class PlaceholderBlock(blocks.StructBlock):
description = blocks.TextBlock()
url = blocks.TextBlock()
class Meta:
icon = "media"

View File

@ -13,7 +13,7 @@ class TestRetrieveLearingPathContents(APITestCase):
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_get_learnpathPage(self): def test_get_learnpath_page(self):
slug = "test-lehrgang-lp" slug = "test-lehrgang-lp"
learning_path = LearningPath.objects.get(slug=slug) learning_path = LearningPath.objects.get(slug=slug)
response = self.client.get(f"/api/course/page/{slug}/") response = self.client.get(f"/api/course/page/{slug}/")
@ -25,4 +25,4 @@ class TestRetrieveLearingPathContents(APITestCase):
# topics and circles # topics and circles
self.assertEqual(4, len(data["children"])) self.assertEqual(4, len(data["children"]))
# circle "analyse" contents # circle "analyse" contents
self.assertEqual(12, len(data["children"][3]["children"])) self.assertEqual(14, len(data["children"][3]["children"]))

View File

@ -13,7 +13,7 @@ class MediaLibraryAPITestCase(APITestCase):
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_get_learnpathPage(self): def test_get_media_library_page(self):
slug = "test-lehrgang-media" slug = "test-lehrgang-media"
media_library = MediaLibraryPage.objects.get(slug=slug) media_library = MediaLibraryPage.objects.get(slug=slug)
response = self.client.get(f"/api/course/page/{slug}/") response = self.client.get(f"/api/course/page/{slug}/")