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 run prettier:check
- npm run lint
- npm run typecheck
- step:
name: cypress tests
max-time: 45

View File

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

View File

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

View File

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

View File

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

View File

@ -5,11 +5,14 @@ import * as _ from "lodash";
import * as log from "loglevel";
import { computed, onMounted } from "vue";
// @ts-ignore
import colors from "@/colors.json";
import type { LearningSequence } from "@/types";
import type { DefaultArcObject } from "d3";
const circleStore = useCircleStore();
function someFinished(learningSequence) {
function someFinished(learningSequence: LearningSequence) {
if (circleStore.circle) {
return circleStore.circle.someFinishedInLearningSequence(
learningSequence.translation_key
@ -18,7 +21,7 @@ function someFinished(learningSequence) {
return false;
}
function allFinished(learningSequence) {
function allFinished(learningSequence: LearningSequence) {
if (circleStore.circle) {
return circleStore.circle.allFinishedInLearningSequence(
learningSequence.translation_key
@ -32,6 +35,17 @@ onMounted(async () => {
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 circle = circleStore.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 pieGenerator = d3.pie();
let angles = pieGenerator(pieWeights);
_.forEach(angles, (pie) => {
const thisLearningSequence = circle.learningSequences[parseInt(pie.index)];
pie.title = thisLearningSequence.title;
pie.icon = thisLearningSequence.icon;
pie.startAngle = pie.startAngle + Math.PI;
pie.endAngle = pie.endAngle + Math.PI;
pie.arrowStartAngle = pie.endAngle + (pie.startAngle - pie.endAngle) / 2;
pie.arrowEndAngle = pie.startAngle + (pie.startAngle - pie.endAngle) / 2;
pie.translation_key = thisLearningSequence.translation_key;
pie.slug = thisLearningSequence.slug;
pie.someFinished = someFinished(thisLearningSequence);
pie.allFinished = allFinished(thisLearningSequence);
const angles = pieGenerator(pieWeights);
let result = angles.map((angle) => {
const thisLearningSequence = circle.learningSequences[angle.index];
return Object.assign(
{
startAngle: angle.startAngle + Math.PI,
endAngle: angle.endAngle + Math.PI,
..._.pick(thisLearningSequence, ["title", "icon", "translation_key", "slug"]),
arrowStartAngle: angle.endAngle + (angle.startAngle - angle.endAngle) / 2,
arrowEndAngle: angle.startAngle + (angle.startAngle - angle.endAngle) / 2,
someFinished: someFinished(thisLearningSequence),
allFinished: allFinished(thisLearningSequence),
},
angle
);
});
angles = angles.reverse();
return angles;
result = result.reverse();
return result as CirclePie[];
}
return {};
return undefined;
});
const width = 450;
const height = 450;
const radius = Math.min(width, height) / 2.4;
function render() {
const arrowStrokeWidth = 2;
function getColor(d: CirclePie) {
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");
// Clean svg before adding new stuff.
svg.selectAll("*").remove();
// Append marker as definition to the svg
svg
.attr("viewBox", `0 0 ${width} ${height}`)
.append("svg:defs")
.append("svg:marker")
.attr("id", "triangle")
.attr("refX", 11)
.attr("refY", 11)
.attr("markerWidth", 20)
.attr("markerHeight", 20)
.attr("markerUnits", "userSpaceOnUse")
.attr("orient", "auto")
.append("path")
.attr("d", "M -1 0 l 10 0 M 0 -1 l 0 10")
.attr("transform", "rotate(-90, 10, 0)")
.attr("stroke-width", arrowStrokeWidth)
.attr("stroke", colors.gray[500]);
if (pieData.value) {
const arrowStrokeWidth = 2;
// Append marker as definition to the svg
svg
.attr("viewBox", `0 0 ${width} ${height}`)
.append("svg:defs")
.append("svg:marker")
.attr("id", "triangle")
.attr("refX", 11)
.attr("refY", 11)
.attr("markerWidth", 20)
.attr("markerHeight", 20)
.attr("markerUnits", "userSpaceOnUse")
.attr("orient", "auto")
.append("path")
.attr("d", "M -1 0 l 10 0 M 0 -1 l 0 10")
.attr("transform", "rotate(-90, 10, 0)")
.attr("stroke-width", arrowStrokeWidth)
.attr("stroke", colors.gray[500]);
const g = svg
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
const g = svg
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
function getColor(d) {
let color = colors.gray[300];
if (d.someFinished) {
color = colors.sky[500];
}
if (d.allFinished) {
color = colors.green[500];
}
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) => {
// @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;
}
</script>

View File

@ -18,6 +18,7 @@ const emit = defineEmits(["back", "next"]);
<button
type="button"
class="btn-text inline-flex items-center px-3 py-2"
data-cy="close-learning-content"
@click="$emit('back')"
>
<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">
import type { LearningContentType } from "@/types";
import { learningContentTypesToName } from "@/utils/typeMaps";
import { learningContentTypeData } from "@/utils/typeMaps";
const props = defineProps<{
learningContentType: LearningContentType;
@ -11,39 +11,12 @@ const props = defineProps<{
<div
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
v-if="props.learningContentType === 'assignment'"
<component
:is="learningContentTypeData(props.learningContentType).icon"
class="w-6 h-6"
/>
<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" />
></component>
<p class="whitespace-nowrap">
{{ learningContentTypesToName.get(props.learningContentType) }}
{{ learningContentTypeData(props.learningContentType).title }}
</p>
</div>
</template>

View File

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

View File

@ -23,9 +23,9 @@ const props = withDefaults(defineProps<Props>(), {
<div>
<h4 class="mb-2 text-bold">{{ title }}</h4>
<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>
</media-link>
</MediaLink>
</div>
</div>
</template>

View File

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

View File

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

View File

@ -3,13 +3,13 @@ import type { DropdownListItem } from "@/types";
import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/vue";
const props = defineProps<{
buttonClasses: [string];
listItems: [[DropdownListItem]];
buttonClasses: [string] | [];
listItems: DropdownListItem[];
align: "left" | "right";
}>();
const emit = defineEmits<{
(e: "select", data: object): void;
(e: "select", data: any): void;
}>();
</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="[align === 'left' ? 'left-0' : 'right-0']"
>
<div v-for="section in listItems" :key="section" class="">
<div v-for="item in section" :key="item" class="px-1 py-1">
<MenuItem>
<button
class="text-black group flex w-full items-center px-0 py-2 text-sm"
@click="$emit('select', item.data)"
>
<span class="inline-block pr-2">
<component :is="item.icon" v-if="item.icon"></component>
</span>
{{ item.title }}
</button>
</MenuItem>
</div>
<div v-for="item in listItems" :key="item.title" class="px-1 py-1">
<MenuItem>
<button
class="text-black group flex w-full items-center px-0 py-2 text-sm"
@click="$emit('select', item.data)"
>
<span class="inline-block pr-2">
<component :is="item.icon" v-if="item.icon"></component>
</span>
{{ item.title }}
</button>
</MenuItem>
</div>
</MenuItems>
</transition>

View File

@ -28,11 +28,7 @@ export const itPost = (url: RequestInfo, data: unknown, options: RequestInit = {
"Content-Type": "application/json;charset=UTF-8",
},
options?.headers
);
if (options?.headers) {
delete options.headers;
}
) as HeadersInit;
options = Object.assign(
{
@ -43,7 +39,6 @@ export const itPost = (url: RequestInfo, data: unknown, options: RequestInit = {
options
);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
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
*/
document.querySelector("html").setAttribute("lang", locale);
document.querySelector("html")?.setAttribute("lang", locale);
}
export async function loadLocaleMessages(i18n: any, locale: any) {

View File

@ -25,7 +25,11 @@ const userStore = useUserStore();
<form
class="bg-white p-4 lg:p-8"
@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">

View File

@ -27,29 +27,25 @@ const state = reactive({
});
const dropdownData = [
[
{
title: "Option 1",
icon: IconLogout,
data: {},
{
title: "Option 1",
icon: IconLogout,
data: {},
},
{
title: "Option 2",
icon: IconLogout,
data: {
test: 12,
},
{
title: "Option 2",
icon: null,
data: {
test: 12,
},
},
{
title: "Option 3",
icon: IconSettings,
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.

View File

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

View File

@ -4,7 +4,8 @@ import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
import { useCompetenceStore } from "@/stores/competence";
import type { CourseCompletionStatus } from "@/types";
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");

View File

@ -21,7 +21,7 @@ let competencePage: CompetencePage | undefined;
const findCriteria = () => {
for (const page of competenceStore.competenceProfilePage
?.children as CompetencePage[]) {
for (let criteria of page.children) {
for (const criteria of page.children) {
if (criteria.slug === route.params["criteriaSlug"]) {
currentQuestion = criteria;
competencePage = page;
@ -63,7 +63,7 @@ findCriteria();
@back="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">
<h2 class="heading-2">
{{ currentQuestion.competence_id }} {{ currentQuestion.title }}

View File

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

View File

@ -2,6 +2,7 @@
import LinkCard from "@/components/mediaLibrary/LinkCard.vue";
import MediaLink from "@/components/mediaLibrary/MediaLink.vue";
import { useMediaLibraryStore } from "@/stores/mediaLibrary";
import type { MediaBlockType } from "@/types";
import * as log from "loglevel";
import { computed } from "vue";
import { useRoute } from "vue-router";
@ -33,28 +34,28 @@ const backLink = computed(() => {
const maxCardItems = 4;
const maxListItems = 6;
const displayAsCard = (itemType: string): boolean => {
const displayAsCard = (itemType: MediaBlockType): boolean => {
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;
};
}
const getMaxDisplayItems = (items: object[], maxItems: number) => {
function getMaxDisplayItems<T>(items: T[], maxItems: number) {
return items.slice(0, maxItems);
};
}
const getMaxDisplayItemsForType = (itemType: string, items: object[]) => {
function getMaxDisplayItemsForType<T>(itemType: MediaBlockType, items: T[]) {
return displayAsCard(itemType)
? getMaxDisplayItems(items, maxCardItems)
: getMaxDisplayItems(items, maxListItems);
};
}
const hasMoreItemsForType = (itemType: string, items: object[]) => {
function hasMoreItemsForType<T>(itemType: MediaBlockType, items: T[]) {
const maxItems = displayAsCard(itemType) ? maxCardItems : maxListItems;
return hasMoreItems(items, maxItems);
};
}
</script>
<template>
@ -65,7 +66,10 @@ const hasMoreItemsForType = (itemType: string, items: object[]) => {
<div class="bg-gray-200 pb-4 lg:pb-12">
<div class="container-large">
<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 />
<span>zurück</span>
</router-link>
@ -93,7 +97,7 @@ const hasMoreItemsForType = (itemType: string, items: object[]) => {
<ul>
<li
v-for="item in mediaCategory.items"
:key="item"
:key="item.id"
class="mb-2 flex items-center"
>
<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">
<h4 class="text-bold">{{ mediaItem.value.title }}</h4>
<media-link
<MediaLink
:blank="mediaItem.value.open_window"
:to="mediaItem.value.url"
class="link"
>
{{ mediaItem.value.link_display_text }}
</media-link>
</MediaLink>
</div>
</li>
</ul>

View File

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

View File

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

View File

@ -1,39 +1,59 @@
{
"id": 372,
"id": 362,
"title": "Test Lernpfad",
"slug": "test-lehrgang-lp",
"type": "learnpath.LearningPath",
"translation_key": "42e559ca-970f-4a08-9e5e-63860585ee1e",
"translation_key": "8a230aa1-075e-4ac1-a8d6-87642c4f33ba",
"frontend_url": "/learn/test-lehrgang-lp",
"children": [
{
"id": 373,
"id": 363,
"title": "Basis",
"slug": "test-lehrgang-lp-topic-basis",
"type": "learnpath.Topic",
"translation_key": "d68c1544-cf22-4a59-a81c-8cb977440cd0",
"translation_key": "d6e14156-2fb9-4f1b-83ce-6879e364f9a2",
"frontend_url": "",
"is_visible": false
},
{
"id": 374,
"id": 364,
"title": "Basis",
"slug": "test-lehrgang-lp-circle-basis",
"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": [
{
"id": 375,
"id": 365,
"title": "Starten",
"slug": "test-lehrgang-lp-circle-basis-ls-starten",
"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"
},
{
"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",
"slug": "test-lehrgang-lp-circle-basis-lc-einf\u00fchrung",
"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,
"contents": [
{
@ -42,24 +62,41 @@
"description": "Beispiel Dokument",
"url": null
},
"id": "bd05f721-3e9d-4a11-8fe2-7c04e2365f52"
"id": "9f22d0b7-643a-4e97-816a-a41141befc95"
}
]
},
{
"id": 377,
"id": 368,
"title": "Beenden",
"slug": "test-lehrgang-lp-circle-basis-ls-beenden",
"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"
},
{
"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!",
"slug": "test-lehrgang-lp-circle-basis-lc-jetzt-kann-es-losgehen",
"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,
"contents": [
{
@ -68,45 +105,130 @@
"description": "Beispiel Dokument",
"url": null
},
"id": "204fc13b-a9ae-40de-8e09-f1e922c4fdd9"
"id": "1422a7c3-0a9a-4321-88a0-d82d0ed26ba2"
}
]
}
],
"description": "Basis",
"job_situations": [],
"goals": [],
"experts": []
"goal_description": "('In diesem Circle baust du deine Handlungskompetenzen f\u00fcr diese Themen aus:',)",
"goals": [
{
"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",
"slug": "test-lehrgang-lp-topic-beraten-der-kunden",
"type": "learnpath.Topic",
"translation_key": "91918780-75f8-4db3-8fb8-91b63f08b9b9",
"translation_key": "728a2578-a22c-41df-9079-43a5318c5030",
"frontend_url": "",
"is_visible": true
},
{
"id": 380,
"id": 372,
"title": "Analyse",
"slug": "test-lehrgang-lp-circle-analyse",
"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": [
{
"id": 381,
"id": 373,
"title": "Starten",
"slug": "test-lehrgang-lp-circle-analyse-ls-starten",
"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"
},
{
"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\"",
"slug": "test-lehrgang-lp-circle-analyse-lc-einleitung-circle-analyse",
"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,
"contents": [
{
@ -115,24 +237,27 @@
"description": "Beispiel Dokument",
"url": null
},
"id": "892a9a4a-8e1e-4f7e-8c35-9bf3bbe5371b"
"id": "8b7f183e-1879-4391-953f-52d9a621f435"
}
]
},
{
"id": 383,
"id": 376,
"title": "Beobachten",
"slug": "test-lehrgang-lp-circle-analyse-ls-beobachten",
"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"
},
{
"id": 384,
"id": 377,
"title": "Fahrzeug",
"slug": "test-lehrgang-lp-circle-analyse-lu-fahrzeug",
"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": {
"id": 15,
"title": "Fahrzeug",
@ -140,29 +265,32 @@
},
"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).",
"slug": "test-lehrgang-competence-crit-y13-fahrzeug",
"type": "competence.PerformanceCriteria",
"translation_key": "e9d49552-7d18-418a-94b6-ebb4ee6bf187",
"translation_key": "3b714984-afdb-4456-9c01-a59064724929",
"frontend_url": "",
"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.",
"slug": "test-lehrgang-competence-crit-y21-fahrzeug",
"type": "competence.PerformanceCriteria",
"translation_key": "5f257b35-c6ca-49e4-9401-a5d02d53926d",
"translation_key": "c2850a27-60c5-471b-9fec-ba0baf152e91",
"frontend_url": "",
"competence_id": "Y2.1"
}
]
},
{
"id": 385,
"id": 378,
"title": "Rafael Fasel wechselt sein Auto",
"slug": "test-lehrgang-lp-circle-analyse-lc-rafael-fasel-wechselt-sein-auto",
"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,
"contents": [
{
@ -171,16 +299,17 @@
"description": "In diesem Online-Training lernst du, wie du den Kundenbedarf ermittelst.",
"url": ""
},
"id": "700a0f64-0892-4fa5-9e08-3bd34e99edeb"
"id": "c79d34cb-0e7e-403d-a672-03d94cf6bdc7"
}
]
},
{
"id": 386,
"id": 379,
"title": "Fachcheck Fahrzeug",
"slug": "test-lehrgang-lp-circle-analyse-lc-fachcheck-fahrzeug",
"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,
"contents": [
{
@ -189,16 +318,18 @@
"description": "Beispiel Test",
"url": null
},
"id": "9f674aaa-ebf0-4a01-adcc-c0c46394fb10"
"id": "ac4c67bc-7de9-4e5c-a35e-e13f5766d6cc"
}
]
},
{
"id": 387,
"id": 380,
"title": "Reisen",
"slug": "test-lehrgang-lp-circle-analyse-lu-reisen",
"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": {
"id": 16,
"title": "Reisen",
@ -206,21 +337,23 @@
},
"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).",
"slug": "test-lehrgang-competence-crit-y13-reisen",
"type": "competence.PerformanceCriteria",
"translation_key": "1e488b69-8a3e-4acc-9547-48c103e0d038",
"translation_key": "1df45a12-41f2-4ff5-8580-d5a7caf5dd56",
"frontend_url": "",
"competence_id": "Y1.3"
}
]
},
{
"id": 388,
"id": 381,
"title": "Reiseversicherung",
"slug": "test-lehrgang-lp-circle-analyse-lc-reiseversicherung",
"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,
"contents": [
{
@ -229,16 +362,17 @@
"description": "Beispiel \u00dcbung",
"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",
"slug": "test-lehrgang-lp-circle-analyse-lc-emma-und-ayla-campen-durch-amerika",
"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,
"contents": [
{
@ -247,24 +381,41 @@
"description": "Beispiel \u00dcbung",
"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",
"slug": "test-lehrgang-lp-circle-analyse-ls-beenden",
"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"
},
{
"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",
"slug": "test-lehrgang-lp-circle-analyse-lc-kompetenzprofil-anschauen",
"slug": "test-lehrgang-lp-circle-analyse-lc-kompetenznavi-anschauen",
"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,
"contents": [
{
@ -273,16 +424,17 @@
"description": "Beispiel Dokument",
"url": null
},
"id": "3f685055-4e3e-4ca9-93af-bac19236931d"
"id": "3ef87e69-5e5c-415a-934c-ed47ad9fdd93"
}
]
},
{
"id": 392,
"id": 386,
"title": "Circle \"Analyse\" abschliessen",
"slug": "test-lehrgang-lp-circle-analyse-lc-circle-analyse-abschliessen",
"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,
"contents": [
{
@ -291,34 +443,36 @@
"description": "Beispiel Dokument",
"url": null
},
"id": "650b7b15-b522-4df7-ac5b-6a654f12334f"
"id": "21415232-862b-488c-9987-4f4ee369a854"
}
]
}
],
"description": "Unit-Test Circle",
"job_situations": [
{
"type": "job_situation",
"value": "Autoversicherung",
"id": "c5a6b365-0a18-47d5-b6e1-6cb8b8ec7d35"
},
{
"type": "job_situation",
"value": "Autokauf",
"id": "e969d2a2-b383-482c-a721-88552af086a6"
}
],
"goal_description": "('In diesem Circle baust du deine Handlungskompetenzen f\u00fcr diese Themen aus:',)",
"goals": [
{
"type": "goal",
"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",
"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": [
@ -331,14 +485,14 @@
"photo": null,
"biography": ""
},
"id": "b7b0ff2e-f840-4d74-99c1-c7a5ee6dc14e"
"id": "b0633305-5e74-43eb-93b8-ebbcfb1b17d1"
}
]
}
],
"course": {
"id": -1,
"title": "Test Lerngang",
"title": "Test Lehrgang",
"category_name": "Handlungsfeld"
}
}

View File

@ -5,25 +5,25 @@ import requests
def main():
client = requests.session()
client.get('http://localhost:8000/')
client.get("http://localhost:8001/")
client.post(
'http://localhost:8000/api/core/login/',
"http://localhost:8001/api/core/login/",
json={
'username': 'admin',
'password': 'test',
}
"username": "admin",
"password": "test",
},
)
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.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))
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

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

View File

@ -2,12 +2,12 @@ import * as _ from "lodash";
import { Circle } from "@/services/circle";
import type {
Course,
CourseCompletion,
CourseCompletionStatus,
CourseWagtailPage,
LearningContent,
LearningPathChild,
Topic,
WagtailLearningPath,
} from "@/types";
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";
public topics: Topic[];
public circles: Circle[];
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(
json.id,
json.slug,
json.course.title,
json.translation_key,
json.frontend_url,
json.course.id,
json.course,
json.children,
completionData
);
@ -48,7 +50,7 @@ export class LearningPath implements CourseWagtailPage {
public readonly title: string,
public readonly translation_key: string,
public readonly frontend_url: string,
public readonly courseId: number,
public readonly course: Course,
public children: LearningPathChild[],
completionData?: CourseCompletion[]
) {
@ -95,7 +97,7 @@ export class LearningPath implements CourseWagtailPage {
this.nextLearningContent = undefined;
const lastCompletedLearningContent = getLastCompleted(
this.courseId,
this.course.id,
completionData
);

View File

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

View File

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

View File

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

View File

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

View File

@ -1,14 +1,32 @@
import type { LearningContentType } from "@/types";
import { assertUnreachable } from "@/utils/utils";
export const learningContentTypesToName = new Map<LearningContentType, string>([
["assignment", "Transferauftrag"],
["book", "Buch"],
["document", "Dokument"],
["exercise", "Übung"],
["media_library", "Mediathek"],
["online_training", "Online-Training"],
["video", "Video"],
["test", "Test"],
["resource", "Seite"],
["placeholder", "In Umsetzung"],
]);
export function learningContentTypeData(t: LearningContentType): {
title: string;
icon: string;
} {
switch (t) {
case "assignment":
return { title: "Transferauftrag", icon: "it-icon-lc-assignment" };
case "book":
return { title: "Buch", icon: "it-icon-lc-book" };
case "document":
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'
(cd client && npm run prettier:check)
echo 'lint'
(cd client && npm run lint)
echo 'lint and typecheck'
(cd client && npm run lint && npm run typecheck)
echo 'python ufmt check'
ufmt check server

View File

@ -185,8 +185,6 @@ gunicorn==20.1.0
# via -r requirements.in
h11==0.13.0
# via uvicorn
hiredis==2.0.0
# via -r requirements.in
html5lib==1.1
# via wagtail
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
whitenoise # https://github.com/evansd/whitenoise
redis # https://github.com/redis/redis-py
hiredis # https://github.com/redis/hiredis-py
uvicorn[standard] # https://github.com/encode/uvicorn
environs

View File

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

View File

@ -13,7 +13,7 @@ class CompetenceAPITestCase(APITestCase):
self.user = User.objects.get(username="student")
self.client.login(username="student", password="test")
def test_get_learnpathPage(self):
def test_get_compentence_page(self):
slug = "test-lehrgang-competence"
competence_profile = CompetenceProfilePage.objects.get(slug=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(
title="Starten", parent=circle_basis, icon="it-icon-ls-start"
)
LearningUnitFactory(title="Einführung", parent=circle_basis)
LearningContentFactory(
title="Einführung",
parent=circle_basis,
@ -103,6 +104,7 @@ def create_test_learning_path(user=None, skip_locales=True):
contents=[("document", DocumentBlockFactory())],
)
LearningSequenceFactory(title="Beenden", parent=circle_basis, icon="it-icon-ls-end")
LearningUnitFactory(title="Beenden", parent=circle_basis)
LearningContentFactory(
title="Jetzt kann es losgehen!",
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")
LearningUnitFactory(title="Einführung", parent=circle)
LearningContentFactory(
title=f'Einleitung Circle "Analyse"',
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")
LearningUnitFactory(title="Beenden", parent=circle)
LearningContentFactory(
title="KompetenzNavi anschauen",
parent=circle,

View File

@ -273,7 +273,7 @@ wichtige Grundlage für eine erfolgreiche Beziehung.
VideoBlockFactory(
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"
"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"
class PlaceholderBlock(blocks.StructBlock):
description = blocks.TextBlock()
url = blocks.TextBlock()
class Meta:
icon = "media"
class ExerciseBlock(blocks.StructBlock):
description = blocks.TextBlock()
url = blocks.TextBlock()
@ -82,3 +74,11 @@ class VideoBlock(blocks.StructBlock):
class Meta:
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.client.login(username="student", password="test")
def test_get_learnpathPage(self):
def test_get_learnpath_page(self):
slug = "test-lehrgang-lp"
learning_path = LearningPath.objects.get(slug=slug)
response = self.client.get(f"/api/course/page/{slug}/")
@ -25,4 +25,4 @@ class TestRetrieveLearingPathContents(APITestCase):
# topics and circles
self.assertEqual(4, len(data["children"]))
# 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.client.login(username="student", password="test")
def test_get_learnpathPage(self):
def test_get_media_library_page(self):
slug = "test-lehrgang-media"
media_library = MediaLibraryPage.objects.get(slug=slug)
response = self.client.get(f"/api/course/page/{slug}/")