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,8 +75,7 @@ onMounted(() => {
log.debug("MainNavigationBar mounted");
});
const profileDropdownData: DropdownListItem[][] = [
[
const profileDropdownData: DropdownListItem[] = [
{
title: "Kontoeinstellungen",
icon: IconSettings as Component,
@ -85,8 +83,6 @@ const profileDropdownData: DropdownListItem[][] = [
action: "settings",
},
},
],
[
{
title: "Abmelden",
icon: IconLogout as Component,
@ -94,7 +90,6 @@ const profileDropdownData: DropdownListItem[][] = [
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,37 +55,63 @@ 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();
if (pieData.value) {
const arrowStrokeWidth = 2;
// Append marker as definition to the svg
svg
.attr("viewBox", `0 0 ${width} ${height}`)
@ -94,28 +134,6 @@ function render() {
.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;
}
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()
@ -141,6 +159,7 @@ function render() {
.transition()
.duration(200)
.attr("fill", (d) => {
// @ts-ignore
return getHoverColor(d);
});
})
@ -149,6 +168,7 @@ function render() {
.transition()
.duration(200)
.attr("fill", (d) => {
// @ts-ignore
return getColor(d);
});
})
@ -164,6 +184,7 @@ function render() {
return getColor(d);
});
// @ts-ignore
learningSequences.append("path").attr("d", wedgeGenerator);
const learningSequenceText = learningSequences
@ -174,7 +195,7 @@ function render() {
return d.title;
})
.attr("transform", function (d) {
let translate = wedgeGenerator.centroid(d);
let translate = wedgeGenerator.centroid(d as unknown as DefaultArcObject);
translate = [translate[0], translate[1] + 20];
return "translate(" + translate + ")";
})
@ -191,7 +212,7 @@ function render() {
.attr("width", iconWidth)
.attr("height", iconWidth)
.attr("transform", function (d) {
let translate = wedgeGenerator.centroid(d);
let translate = wedgeGenerator.centroid(d as unknown as DefaultArcObject);
translate = [translate[0] - iconWidth / 2, translate[1] - iconWidth];
return "translate(" + translate + ")";
})
@ -204,10 +225,10 @@ function render() {
.outerRadius(arrowRadius + arrowStrokeWidth)
.padAngle(20 / 360)
.startAngle((d) => {
return d.arrowStartAngle;
return (d as unknown as CirclePie).arrowStartAngle;
})
.endAngle((d) => {
return d.arrowEndAngle;
return (d as unknown as CirclePie).arrowEndAngle;
});
const arrows = g
@ -224,10 +245,13 @@ function render() {
};
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);
}
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,8 +33,7 @@ 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">
<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"
@ -47,7 +46,6 @@ const emit = defineEmits<{
</button>
</MenuItem>
</div>
</div>
</MenuItems>
</transition>
</Menu>

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,7 +27,6 @@ const state = reactive({
});
const dropdownData = [
[
{
title: "Option 1",
icon: IconLogout,
@ -35,13 +34,11 @@ const dropdownData = [
},
{
title: "Option 2",
icon: null,
icon: IconLogout,
data: {
test: 12,
},
},
],
[
{
title: "Option 3",
icon: IconSettings,
@ -49,7 +46,6 @@ const dropdownData = [
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,10 +52,12 @@ onMounted(async () => {
const learningUnits = circleStore.circle?.learningSequences.flatMap(
(ls) => ls.learningUnits
);
if (learningUnits) {
wagtailPage = learningUnits.find((lu) => {
return lu.slug.endsWith(slugEnd);
});
}
}
if (wagtailPage) {
document
.getElementById(wagtailPage.slug)

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"
},
{
"id": 379,
"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": 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,17 +86,23 @@ export const useCircleStore = defineStore({
return learningUnit;
},
async markCompletion(
page: LearningContent | LearningUnitPerformanceCriteria,
page:
| LearningContent
| LearningUnitPerformanceCriteria
| PerformanceCriteria
| undefined,
completion_status: CourseCompletionStatus = "success"
) {
const completionStore = useCompletionStore();
try {
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);
return 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) => {
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

@ -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}/")