diff --git a/client/src/components/learningPath/DocumentUploadForm.vue b/client/src/components/learningPath/DocumentUploadForm.vue index 966447ea..fdaa9631 100644 --- a/client/src/components/learningPath/DocumentUploadForm.vue +++ b/client/src/components/learningPath/DocumentUploadForm.vue @@ -84,7 +84,7 @@ function showFileInformation() { {{ $t("circlePage.documents.fileLabel") }}
- + {{ $t("circlePage.documents.modalAction") }}
@@ -100,7 +100,7 @@ function showFileInformation() { - +

{{ $t("circlePage.documents.modalNameInformation") }}

{{ $t("circlePage.documents.chooseName") }}

diff --git a/client/src/pages/cockpit/CockpitIndexPage.vue b/client/src/pages/cockpit/CockpitIndexPage.vue index b0cbd1a7..39d2514f 100644 --- a/client/src/pages/cockpit/CockpitIndexPage.vue +++ b/client/src/pages/cockpit/CockpitIndexPage.vue @@ -7,7 +7,9 @@ import type { LearningPath } from "@/services/learningPath"; import { useCockpitStore } from "@/stores/cockpit"; import { useCompetenceStore } from "@/stores/competence"; import { useLearningPathStore } from "@/stores/learningPath"; +import { useUserStore } from "@/stores/user"; import log from "loglevel"; +import { computed } from "vue"; const props = defineProps<{ courseSlug: string; @@ -15,6 +17,7 @@ const props = defineProps<{ log.debug("CockpitIndexPage created", props.courseSlug); +const userStore = useUserStore(); const cockpitStore = useCockpitStore(); const competenceStore = useCompetenceStore(); const learningPathStore = useLearningPathStore(); @@ -25,6 +28,33 @@ function userCountStatus(userId: number) { ); } +const circles = computed(() => { + const learningPathCircles = learningPathStore + .learningPathForUser(props.courseSlug, userStore.id) + ?.circles.map((c) => { + return { + id: c.id, + title: c.title, + slug: c.slug, + translation_key: c.translation_key, + }; + }); + + if (cockpitStore.cockpitSessionUser?.circles?.length) { + return cockpitStore.cockpitSessionUser.circles; + } else if (learningPathCircles) { + return learningPathCircles; + } else { + return []; + } +}); + +const selectedCirclesTitles = computed(() => { + return circles.value + .filter((c) => cockpitStore.selectedCircles.includes(c.translation_key)) + .map((c) => c.title); +}); + const data = { transferProgress: { fail: 0, @@ -47,7 +77,7 @@ function setActiveClasses(translationKey: string) {

{{ $t("general.circles") }}:

- + {{ title }}
diff --git a/client/src/pages/cockpit/CockpitParentPage.vue b/client/src/pages/cockpit/CockpitParentPage.vue index 2872bf01..1a6d2ea1 100644 --- a/client/src/pages/cockpit/CockpitParentPage.vue +++ b/client/src/pages/cockpit/CockpitParentPage.vue @@ -2,6 +2,7 @@ import { useCockpitStore } from "@/stores/cockpit"; import { useCompetenceStore } from "@/stores/competence"; import { useLearningPathStore } from "@/stores/learningPath"; +import { useUserStore } from "@/stores/user"; import * as log from "loglevel"; import { onMounted } from "vue"; @@ -28,6 +29,7 @@ onMounted(async () => { learningPathStore.loadLearningPath(props.courseSlug + "-lp", csu.user_id); }); + learningPathStore.loadLearningPath(props.courseSlug + "-lp", useUserStore().id); } catch (error) { log.error(error); } diff --git a/client/src/pages/learningPath/CirclePage.vue b/client/src/pages/learningPath/CirclePage.vue index c3ed4109..2eb43918 100644 --- a/client/src/pages/learningPath/CirclePage.vue +++ b/client/src/pages/learningPath/CirclePage.vue @@ -276,6 +276,20 @@ async function uploadDocument(data: DocumentUploadData) { }) }}
+
+
+ +

+ {{ expert.first_name }} {{ expert.last_name }} +

+
+
@@ -302,10 +316,10 @@ async function uploadDocument(data: DocumentUploadData) { diff --git a/client/src/services/__tests__/learning_path_json.json b/client/src/services/__tests__/learning_path_json.json index b003a4a6..6e7f41b4 100644 --- a/client/src/services/__tests__/learning_path_json.json +++ b/client/src/services/__tests__/learning_path_json.json @@ -1,59 +1,59 @@ { - "id": 362, + "id": 568, "title": "Test Lernpfad", "slug": "test-lehrgang-lp", "type": "learnpath.LearningPath", - "translation_key": "8a230aa1-075e-4ac1-a8d6-87642c4f33ba", - "frontend_url": "/learn/test-lehrgang-lp", + "translation_key": "25ddc136-af7d-4a74-8731-896281bfe20b", + "frontend_url": "/course/test-lehrgang/learn", "children": [ { - "id": 363, + "id": 569, "title": "Basis", "slug": "test-lehrgang-lp-topic-basis", "type": "learnpath.Topic", - "translation_key": "d6e14156-2fb9-4f1b-83ce-6879e364f9a2", + "translation_key": "8cb75aee-0349-41e2-9d2b-2938c2b04891", "frontend_url": "", "is_visible": false }, { - "id": 364, + "id": 570, "title": "Basis", "slug": "test-lehrgang-lp-circle-basis", "type": "learnpath.Circle", - "translation_key": "8034e867-4b05-4509-a9bc-99f9f3619e88", - "frontend_url": "/learn/test-lehrgang-lp/basis", + "translation_key": "aaa04c2b-03bf-470e-b414-bd8203db529d", + "frontend_url": "/course/test-lehrgang/learn/basis", "children": [ { - "id": 365, + "id": 571, "title": "Starten", "slug": "test-lehrgang-lp-circle-basis-ls-starten", "type": "learnpath.LearningSequence", - "translation_key": "868bc4cb-c5b5-423e-a890-433184cd06e0", - "frontend_url": "/learn/test-lehrgang-lp/basis#ls-starten", + "translation_key": "ce16116f-f4ed-4fc0-a95a-cc75ba88958d", + "frontend_url": "/course/test-lehrgang/learn/basis#ls-starten", "icon": "it-icon-ls-start" }, { - "id": 366, + "id": 572, "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", + "translation_key": "33863daf-a503-425d-a1a2-9a711f8256a5", + "frontend_url": "/course/test-lehrgang/learn/basis#lu-einf\u00fchrung", + "evaluate_url": "/course/test-lehrgang/learn/basis/evaluate/einf\u00fchrung", "course_category": { - "id": 14, + "id": 27, "title": "Allgemein", "general": true }, "children": [] }, { - "id": 367, + "id": 573, "title": "Einf\u00fchrung", "slug": "test-lehrgang-lp-circle-basis-lc-einf\u00fchrung", "type": "learnpath.LearningContent", - "translation_key": "d1d1b923-f597-4de7-ac44-d02c2f0a1a59", - "frontend_url": "/learn/test-lehrgang-lp/basis/einf\u00fchrung", + "translation_key": "ae19458c-9b08-4b9c-9e7c-62c3cce4a6cc", + "frontend_url": "/course/test-lehrgang/learn/basis/einf\u00fchrung", "minutes": 15, "contents": [ { @@ -62,41 +62,41 @@ "description": "Beispiel Dokument", "url": null }, - "id": "9f22d0b7-643a-4e97-816a-a41141befc95" + "id": "601ac5e1-b268-442e-b15b-26ead06bdf77" } ] }, { - "id": 368, + "id": 574, "title": "Beenden", "slug": "test-lehrgang-lp-circle-basis-ls-beenden", "type": "learnpath.LearningSequence", - "translation_key": "338208db-7c85-470e-872f-850e34747873", - "frontend_url": "/learn/test-lehrgang-lp/basis#ls-beenden", + "translation_key": "3c167825-a358-4c62-a632-9482e9fbfe5f", + "frontend_url": "/course/test-lehrgang/learn/basis#ls-beenden", "icon": "it-icon-ls-end" }, { - "id": 369, + "id": 575, "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", + "translation_key": "9a70a164-0468-4681-a6ed-a1b636db1ae6", + "frontend_url": "/course/test-lehrgang/learn/basis#lu-beenden", + "evaluate_url": "/course/test-lehrgang/learn/basis/evaluate/beenden", "course_category": { - "id": 14, + "id": 27, "title": "Allgemein", "general": true }, "children": [] }, { - "id": 370, + "id": 576, "title": "Jetzt kann es losgehen!", "slug": "test-lehrgang-lp-circle-basis-lc-jetzt-kann-es-losgehen", "type": "learnpath.LearningContent", - "translation_key": "6920bcac-597b-462a-9458-32aa5dc8d3f7", - "frontend_url": "/learn/test-lehrgang-lp/basis/jetzt-kann-es-losgehen", + "translation_key": "f251beda-a4b8-4a49-89a6-8c52d335b140", + "frontend_url": "/course/test-lehrgang/learn/basis/jetzt-kann-es-losgehen", "minutes": 30, "contents": [ { @@ -105,7 +105,7 @@ "description": "Beispiel Dokument", "url": null }, - "id": "1422a7c3-0a9a-4321-88a0-d82d0ed26ba2" + "id": "1075443f-e2e5-46eb-87c4-804f0227a858" } ] } @@ -116,17 +116,17 @@ { "type": "goal", "value": "... hier ein Beispieltext f\u00fcr ein Ziel 1", - "id": "38afbda4-7b7e-4f5c-88e8-c595c43e1659" + "id": "6c227ac1-42a7-4ca3-bde2-8531bf655481" }, { "type": "goal", "value": "... hier ein Beispieltext f\u00fcr ein Ziel 2", - "id": "4d00ac58-0499-4316-9af2-356c37dedc35" + "id": "7c466321-abe3-4705-bbb1-f286a8e5c800" }, { "type": "goal", "value": "... hier ein Beispieltext f\u00fcr ein Ziel 3", - "id": "945eb104-8cc1-45cd-a07a-a4d3ec4f39a3" + "id": "1e491838-62a9-4185-b321-f64ca7c99f36" } ], "job_situation_description": "Du triffst in diesem Circle auf die folgenden berufstypischen Handlungsfelder:", @@ -134,101 +134,88 @@ { "type": "job_situation", "value": "Job Situation 1", - "id": "02fb807a-0d07-4353-81ec-8b8b383954d7" + "id": "0d4a66de-523c-4659-9b4b-bb81cef28961" }, { "type": "job_situation", "value": "Job Situation 2", - "id": "371952f6-5871-4bf1-b423-d3dab7371001" + "id": "dc4d8157-1db4-4e64-bc0c-00a35159c32d" }, { "type": "job_situation", "value": "Job Situation 3", - "id": "116bfa7b-65e8-44a1-8c82-e8b05fd86a01" + "id": "a270baca-1077-4535-9a9f-5562af9854d8" }, { "type": "job_situation", "value": "Job Situation 4", - "id": "08baf7dd-8801-4af9-8af8-714989775ddb" + "id": "aa4b21f6-0bd3-40f3-8589-c43e4524f565" }, { "type": "job_situation", "value": "Job Situation 5", - "id": "93ade4b8-c4fb-4941-98c5-e58336fca4bb" + "id": "8618d1e7-c628-42ef-aac3-201b2b65393f" }, { "type": "job_situation", "value": "Job Situation 6", - "id": "1fac4ee4-6d86-4e9e-9fa4-a99c6659bc8b" + "id": "be7a2a59-2aef-4dd8-ad8d-17c4a46b064f" }, { "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": "a4b74338-abd7-4de8-82ab-d77664f6cbbf" } ] }, { - "id": 371, + "id": 577, "title": "Beraten der Kunden", "slug": "test-lehrgang-lp-topic-beraten-der-kunden", "type": "learnpath.Topic", - "translation_key": "728a2578-a22c-41df-9079-43a5318c5030", + "translation_key": "2ea6ea41-b839-4462-aea4-1796b0f9849d", "frontend_url": "", "is_visible": true }, { - "id": 372, + "id": 578, "title": "Analyse", "slug": "test-lehrgang-lp-circle-analyse", "type": "learnpath.Circle", - "translation_key": "e429adf5-dd5d-4699-b471-40c782fb507e", - "frontend_url": "/learn/test-lehrgang-lp/analyse", + "translation_key": "dbc95ecd-0fdf-4b0a-ba91-7eec88a420f3", + "frontend_url": "/course/test-lehrgang/learn/analyse", "children": [ { - "id": 373, + "id": 579, "title": "Starten", "slug": "test-lehrgang-lp-circle-analyse-ls-starten", "type": "learnpath.LearningSequence", - "translation_key": "40e977e0-3668-418d-b838-d3774a5cbe7d", - "frontend_url": "/learn/test-lehrgang-lp/analyse#ls-starten", + "translation_key": "9b10c6ec-e313-49fa-b09d-9639429254f9", + "frontend_url": "/course/test-lehrgang/learn/analyse#ls-starten", "icon": "it-icon-ls-start" }, { - "id": 374, + "id": 580, "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", + "translation_key": "f51ab860-bc23-4f59-bb88-0e8506ef42e5", + "frontend_url": "/course/test-lehrgang/learn/analyse#lu-einf\u00fchrung", + "evaluate_url": "/course/test-lehrgang/learn/analyse/evaluate/einf\u00fchrung", "course_category": { - "id": 14, + "id": 27, "title": "Allgemein", "general": true }, "children": [] }, { - "id": 375, + "id": 581, "title": "Einleitung Circle \"Analyse\"", "slug": "test-lehrgang-lp-circle-analyse-lc-einleitung-circle-analyse", "type": "learnpath.LearningContent", - "translation_key": "5e8d6478-6287-4658-94c5-ecbd5d624962", - "frontend_url": "/learn/test-lehrgang-lp/analyse/einleitung-circle-analyse", + "translation_key": "70363615-2a53-478d-82f7-0aa02744f559", + "frontend_url": "/course/test-lehrgang/learn/analyse/einleitung-circle-analyse", "minutes": 15, "contents": [ { @@ -237,60 +224,60 @@ "description": "Beispiel Dokument", "url": null }, - "id": "8b7f183e-1879-4391-953f-52d9a621f435" + "id": "174d883b-1bd0-4bee-872d-d08c2544dc3a" } ] }, { - "id": 376, + "id": 582, "title": "Beobachten", "slug": "test-lehrgang-lp-circle-analyse-ls-beobachten", "type": "learnpath.LearningSequence", - "translation_key": "35df96df-2e8d-4f16-aee1-8d72990f63a0", - "frontend_url": "/learn/test-lehrgang-lp/analyse#ls-beobachten", + "translation_key": "0a540c5a-25c8-42ab-8661-6c08c78d91c2", + "frontend_url": "/course/test-lehrgang/learn/analyse#ls-beobachten", "icon": "it-icon-ls-watch" }, { - "id": 377, + "id": 583, "title": "Fahrzeug", "slug": "test-lehrgang-lp-circle-analyse-lu-fahrzeug", "type": "learnpath.LearningUnit", - "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", + "translation_key": "f072e5b2-c033-4d7d-9dae-28e6b856087c", + "frontend_url": "/course/test-lehrgang/learn/analyse#lu-fahrzeug", + "evaluate_url": "/course/test-lehrgang/learn/analyse/evaluate/fahrzeug", "course_category": { - "id": 15, + "id": 28, "title": "Fahrzeug", "general": false }, "children": [ { - "id": 391, + "id": 597, "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": "3b714984-afdb-4456-9c01-a59064724929", + "translation_key": "7d76cdd3-57a0-4f82-bc33-fcc8a78b15e0", "frontend_url": "", "competence_id": "Y1.3" }, { - "id": 392, + "id": 598, "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": "c2850a27-60c5-471b-9fec-ba0baf152e91", + "translation_key": "5b3e1105-a12b-4d99-984c-28f01daeef54", "frontend_url": "", "competence_id": "Y2.1" } ] }, { - "id": 378, + "id": 584, "title": "Rafael Fasel wechselt sein Auto", "slug": "test-lehrgang-lp-circle-analyse-lc-rafael-fasel-wechselt-sein-auto", "type": "learnpath.LearningContent", - "translation_key": "b7779d45-adf4-41fc-a4a5-e95c732b2224", - "frontend_url": "/learn/test-lehrgang-lp/analyse/rafael-fasel-wechselt-sein-auto", + "translation_key": "ff254250-d010-4f57-9e57-987c343029a6", + "frontend_url": "/course/test-lehrgang/learn/analyse/rafael-fasel-wechselt-sein-auto", "minutes": 30, "contents": [ { @@ -299,17 +286,17 @@ "description": "In diesem Online-Training lernst du, wie du den Kundenbedarf ermittelst.", "url": "" }, - "id": "c79d34cb-0e7e-403d-a672-03d94cf6bdc7" + "id": "454fb0d8-c37d-4c8e-b15c-c50a32be9fd7" } ] }, { - "id": 379, + "id": 585, "title": "Fachcheck Fahrzeug", "slug": "test-lehrgang-lp-circle-analyse-lc-fachcheck-fahrzeug", "type": "learnpath.LearningContent", - "translation_key": "e395e05c-81bf-4bc6-98e8-3833bebb551c", - "frontend_url": "/learn/test-lehrgang-lp/analyse/fachcheck-fahrzeug", + "translation_key": "b0a95253-3a63-4d77-9ddc-961b711175a5", + "frontend_url": "/course/test-lehrgang/learn/analyse/fachcheck-fahrzeug", "minutes": 30, "contents": [ { @@ -318,42 +305,42 @@ "description": "Beispiel Test", "url": null }, - "id": "ac4c67bc-7de9-4e5c-a35e-e13f5766d6cc" + "id": "4b3e40c5-f5f9-4726-bc4c-0d914a029f83" } ] }, { - "id": 380, + "id": 586, "title": "Reisen", "slug": "test-lehrgang-lp-circle-analyse-lu-reisen", "type": "learnpath.LearningUnit", - "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", + "translation_key": "22bcb754-d5c5-413b-b63e-76e6406afef9", + "frontend_url": "/course/test-lehrgang/learn/analyse#lu-reisen", + "evaluate_url": "/course/test-lehrgang/learn/analyse/evaluate/reisen", "course_category": { - "id": 16, + "id": 29, "title": "Reisen", "general": false }, "children": [ { - "id": 393, + "id": 599, "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": "1df45a12-41f2-4ff5-8580-d5a7caf5dd56", + "translation_key": "6a3ce4ac-0d38-4a02-81fa-0979290142e1", "frontend_url": "", "competence_id": "Y1.3" } ] }, { - "id": 381, + "id": 587, "title": "Reiseversicherung", "slug": "test-lehrgang-lp-circle-analyse-lc-reiseversicherung", "type": "learnpath.LearningContent", - "translation_key": "bad7439a-8b0c-4877-8d6c-78f292be83d4", - "frontend_url": "/learn/test-lehrgang-lp/analyse/reiseversicherung", + "translation_key": "cb1c9c93-7f3b-42d6-9078-2d36655fdfd7", + "frontend_url": "/course/test-lehrgang/learn/analyse/reiseversicherung", "minutes": 240, "contents": [ { @@ -362,60 +349,60 @@ "description": "Beispiel \u00dcbung", "url": null }, - "id": "7e1ee533-7f75-495b-a2bc-8bbd2b1311c9" + "id": "b59bccb2-bcb2-48cc-8204-8df94723975b" } ] }, { - "id": 382, + "id": 588, "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": "27f9d8f3-209c-4d55-94d9-2e70fbfe163b", - "frontend_url": "/learn/test-lehrgang-lp/analyse/emma-und-ayla-campen-durch-amerika", + "translation_key": "fdf19afe-fe69-496d-a1d4-ecf18ed36b01", + "frontend_url": "/course/test-lehrgang/learn/analyse/emma-und-ayla-campen-durch-amerika", "minutes": 120, "contents": [ { - "type": "exercise", + "type": "learningmodule", "value": { - "description": "Beispiel \u00dcbung", + "description": "Beispiel Lernmodul", "url": "/static/media/web_based_trainings/story-06-a-01-emma-und-ayla-campen-durch-amerika-einstieg/scormcontent/index.html" }, - "id": "b08e1851-8583-4428-b1bc-402c7095130b" + "id": "c3a14f9f-3491-4695-9544-a3b2a2c3a82a" } ] }, { - "id": 383, + "id": 589, "title": "Beenden", "slug": "test-lehrgang-lp-circle-analyse-ls-beenden", "type": "learnpath.LearningSequence", - "translation_key": "68be244d-0e00-4700-834c-57b4db366fc1", - "frontend_url": "/learn/test-lehrgang-lp/analyse#ls-beenden", + "translation_key": "28ffad90-9333-452f-af18-108d4aff5ee4", + "frontend_url": "/course/test-lehrgang/learn/analyse#ls-beenden", "icon": "it-icon-ls-end" }, { - "id": 384, + "id": 590, "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", + "translation_key": "05a1cb34-e8d8-44e2-9635-5e33a852bfa2", + "frontend_url": "/course/test-lehrgang/learn/analyse#lu-beenden", + "evaluate_url": "/course/test-lehrgang/learn/analyse/evaluate/beenden", "course_category": { - "id": 14, + "id": 27, "title": "Allgemein", "general": true }, "children": [] }, { - "id": 385, + "id": 591, "title": "KompetenzNavi anschauen", "slug": "test-lehrgang-lp-circle-analyse-lc-kompetenznavi-anschauen", "type": "learnpath.LearningContent", - "translation_key": "8ee57ba5-e09e-4058-937a-b733ea72b969", - "frontend_url": "/learn/test-lehrgang-lp/analyse/kompetenznavi-anschauen", + "translation_key": "b90e7ddc-fcde-47aa-91e1-abd67ed96e66", + "frontend_url": "/course/test-lehrgang/learn/analyse/kompetenznavi-anschauen", "minutes": 30, "contents": [ { @@ -424,17 +411,17 @@ "description": "Beispiel Dokument", "url": null }, - "id": "3ef87e69-5e5c-415a-934c-ed47ad9fdd93" + "id": "a7faefea-4cd1-471a-aed1-c780d8236301" } ] }, { - "id": 386, + "id": 592, "title": "Circle \"Analyse\" abschliessen", "slug": "test-lehrgang-lp-circle-analyse-lc-circle-analyse-abschliessen", "type": "learnpath.LearningContent", - "translation_key": "90d9ab63-cc0f-492f-aad1-f7d448ee5b2c", - "frontend_url": "/learn/test-lehrgang-lp/analyse/circle-analyse-abschliessen", + "translation_key": "5c17be62-2da6-413c-82a4-7754c00ef18a", + "frontend_url": "/course/test-lehrgang/learn/analyse/circle-analyse-abschliessen", "minutes": 30, "contents": [ { @@ -443,7 +430,7 @@ "description": "Beispiel Dokument", "url": null }, - "id": "21415232-862b-488c-9987-4f4ee369a854" + "id": "c39488aa-2ce4-4bd7-9a39-f9ace70353bd" } ] } @@ -454,12 +441,12 @@ { "type": "goal", "value": "... die heutige Versicherungssituation von Privat- oder Gesch\u00e4ftskunden einzusch\u00e4tzen.", - "id": "d1f001fa-f7b8-41a3-90c7-632260ff7054" + "id": "c6d1b496-14e7-4570-ba05-c6f408f6ecfb" }, { "type": "goal", "value": "... deinem Kunden seine optimale L\u00f6sung aufzuzeigen", - "id": "8f73bb0f-e898-4961-ab28-dd34caca2c0b" + "id": "92e3ec0d-e90e-4d8e-a81a-43f0e56c504f" } ], "job_situation_description": "Du triffst in diesem Circle auf die folgenden berufstypischen Handlungsfelder:", @@ -467,25 +454,12 @@ { "type": "job_situation", "value": "Autoversicherung", - "id": "df46930b-2911-4161-a677-75b4b156dff3" + "id": "1a047ce6-8922-4bc7-b185-5b7d6d89c0f9" }, { "type": "job_situation", "value": "Autokauf", - "id": "17a6d252-e942-44cc-920f-015e38e727be" - } - ], - "experts": [ - { - "type": "person", - "value": { - "first_name": "Patrizia", - "last_name": "Huggel", - "email": "patrizia.huggel@eiger-versicherungen.ch", - "photo": null, - "biography": "" - }, - "id": "b0633305-5e74-43eb-93b8-ebbcfb1b17d1" + "id": "c1b5a26b-570a-4147-9338-753d87e67cf3" } ] } @@ -493,6 +467,7 @@ "course": { "id": -1, "title": "Test Lehrgang", - "category_name": "Handlungsfeld" + "category_name": "Handlungsfeld", + "slug": "test-lehrgang" } } diff --git a/client/src/services/__tests__/request_learning_path_json.py b/client/src/services/__tests__/request_learning_path_json.py index 87409bb5..8a069fd4 100644 --- a/client/src/services/__tests__/request_learning_path_json.py +++ b/client/src/services/__tests__/request_learning_path_json.py @@ -5,10 +5,10 @@ import requests def main(): client = requests.session() - client.get("http://localhost:8001/") + client.get("http://localhost:8000/") client.post( - "http://localhost:8001/api/core/login/", + "http://localhost:8000/api/core/login/", json={ "username": "admin", "password": "test", @@ -16,7 +16,7 @@ def main(): ) response = client.get( - "http://localhost:8001/api/course/page/test-lehrgang-lp/", + "http://localhost:8000/api/course/page/test-lehrgang-lp/", ) print(response.status_code) print(response.json()) diff --git a/client/src/services/files.ts b/client/src/services/files.ts index df7c9263..f1fc522b 100644 --- a/client/src/services/files.ts +++ b/client/src/services/files.ts @@ -36,7 +36,7 @@ function directUpload(fileData: FileData, file: File) { Accept: "application/json", } as HeadersInit; - let options = { + const options = { method: "POST", headers: headers, body: formData, diff --git a/client/src/stores/cockpit.ts b/client/src/stores/cockpit.ts index 9dc0339e..c7e0883c 100644 --- a/client/src/stores/cockpit.ts +++ b/client/src/stores/cockpit.ts @@ -2,6 +2,7 @@ import { itGetCached } from "@/fetchHelpers"; import type { CourseSessionUser, ExpertSessionUser } from "@/types"; import log from "loglevel"; +import { useUserStore } from "@/stores/user"; import { defineStore } from "pinia"; export type CockpitStoreState = { @@ -19,25 +20,21 @@ export const useCockpitStore = defineStore({ selectedCircles: [], } as CockpitStoreState; }, - getters: { - circles: (state) => state.cockpitSessionUser?.circles, - selectedCirclesTitles: (state) => - state.cockpitSessionUser?.circles - .filter((circle) => state.selectedCircles.indexOf(circle.translation_key) > -1) - .map((circle) => circle.title), - }, actions: { async loadCourseSessionUsers(courseSlug: string, reload = false) { log.debug("loadCockpitData called"); - const { users, cockpit_user: cockpitUser } = await itGetCached( - `/api/course/sessions/${courseSlug}/users/`, - { - reload: reload, - } - ); + const users = (await itGetCached(`/api/course/sessions/${courseSlug}/users/`, { + reload: reload, + })) as CourseSessionUser[]; - this.courseSessionUsers = users; - this.cockpitSessionUser = cockpitUser; + this.courseSessionUsers = users.filter((user) => user.role === "MEMBER"); + + const userStore = useUserStore(); + const currentUser = users.find((user) => user.user_id === userStore.id); + + if (currentUser && currentUser.role === "EXPERT") { + this.cockpitSessionUser = currentUser as ExpertSessionUser; + } if (this.cockpitSessionUser && this.cockpitSessionUser.circles?.length > 0) { this.selectedCircles = [this.cockpitSessionUser.circles[0].translation_key]; diff --git a/client/src/stores/courseSessions.ts b/client/src/stores/courseSessions.ts index 13fb111c..5f1f533a 100644 --- a/client/src/stores/courseSessions.ts +++ b/client/src/stores/courseSessions.ts @@ -1,9 +1,13 @@ import { itGetCached, itPost } from "@/fetchHelpers"; import { deleteCircleDocument } from "@/services/files"; -import type { CircleDocument, CircleExpert, CourseSession } from "@/types"; +import type { + CircleDocument, + CourseSession, + CourseSessionUser, + ExpertSessionUser, +} from "@/types"; import _ from "lodash"; import log from "loglevel"; - import { defineStore } from "pinia"; import { computed, ref } from "vue"; import { useRoute } from "vue-router"; @@ -29,6 +33,27 @@ function loadCourseSessionsData(reload = false) { reload: reload, }); + // TODO: refactor after implementing of Klassenkonzept + const uniqueCourses = _.uniqBy(courseSessions.value, "course.id"); + await Promise.all( + uniqueCourses.map(async (courseSession) => { + const users = (await itGetCached( + `/api/course/sessions/${courseSession.course.slug}/users/`, + { + reload: reload, + } + )) as CourseSessionUser[]; + courseSessions.value = courseSessions.value + .filter((cs) => { + return cs.course.slug === courseSession.course.slug; + }) + .map((cs) => { + cs.users = users; + return cs; + }); + }) + ); + const userStore = useUserStore(); if (!courseSessions.value && userStore.loggedIn) { throw `No courseSessionData found for user`; @@ -41,16 +66,6 @@ function loadCourseSessionsData(reload = false) { return { courseSessions }; } -function userExpertCircles( - userId: number, - courseSessionForRoute: CourseSession | undefined -): CircleExpert[] { - if (!courseSessionForRoute) { - return []; - } - return courseSessionForRoute.experts.filter((expert) => expert.user_id === userId); -} - export const useCourseSessionsStore = defineStore("courseSessions", () => { // using setup function seems cleaner, see https://pinia.vuejs.org/core-concepts/#setup-stores @@ -59,10 +74,13 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => { // store should do own setup, we don't want to have each component initialize it // that's why we call the load function in here const { courseSessions } = loadCourseSessionsData(); + // these will become getters const coursesFromCourseSessions = computed(() => + // TODO: refactor after implementing of Klassenkonzept _.uniqBy(courseSessions.value, "course.id") ); + const courseSessionForRoute = computed(() => { const route = useRoute(); const routePath = decodeURI(route.path); @@ -71,28 +89,40 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => { return routePath.startsWith(cs.course_url); }); }); + const hasCockpit = computed(() => { if (courseSessionForRoute.value) { const userStore = useUserStore(); return ( - courseSessionForRoute.value.experts.filter( - (expert) => expert.user_id === userStore.id - ).length > 0 + userStore.course_session_experts.includes(courseSessionForRoute.value.id) || + userStore.is_superuser ); } return false; }); + const circleExperts = computed(() => { + const circleStore = useCircleStore(); + const circleTranslationKey = circleStore.circle?.translation_key; + + if (courseSessionForRoute.value && circleTranslationKey) { + return courseSessionForRoute.value.users.filter((u) => { + if (u.role === "EXPERT") { + return (u as ExpertSessionUser).circles + .map((c) => c.translation_key) + .includes(circleTranslationKey); + } + return false; + }) as ExpertSessionUser[]; + } + return []; + }); + const canUploadCircleDocuments = computed(() => { const userStore = useUserStore(); - const circleStore = useCircleStore(); - const expertCircles = userExpertCircles(userStore.id, courseSessionForRoute.value); - return ( - expertCircles.filter( - (c) => c.circle_translation_key === circleStore.circle?.translation_key - ).length > 0 + circleExperts.value.filter((expert) => expert.user_id === userStore.id).length > 0 ); }); @@ -106,7 +136,7 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => { return ls; } - for (let document of courseSessionForRoute.value.documents) { + for (const document of courseSessionForRoute.value.documents) { if (document.learning_sequence === ls.id) { ls.documents.push(document); } @@ -146,6 +176,7 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => { hasCockpit, canUploadCircleDocuments, circleDocuments, + circleExperts, addDocument, startUpload, removeDocument, diff --git a/client/src/stores/user.ts b/client/src/stores/user.ts index f84c0abc..91514ca2 100644 --- a/client/src/stores/user.ts +++ b/client/src/stores/user.ts @@ -14,6 +14,8 @@ export type UserState = { email: string; username: string; avatar_url: string; + is_superuser: boolean; + course_session_experts: number[]; loggedIn: boolean; }; @@ -24,6 +26,8 @@ const initialUserState: UserState = { last_name: "", username: "", avatar_url: "", + is_superuser: false, + course_session_experts: [], loggedIn: false, }; @@ -76,9 +80,6 @@ export const useUserStore = defineStore({ this.$state = data; this.loggedIn = true; appStore.userLoaded = true; - // todo: why? - // const courseSessionsStore = useCourseSessionsStore(); - // await courseSessionsStore.loadCourseSessionsData(); }, }, }); diff --git a/client/src/types.ts b/client/src/types.ts index 742cdd4a..ccb4b107 100644 --- a/client/src/types.ts +++ b/client/src/types.ts @@ -341,8 +341,8 @@ export interface CourseSession { course_url: string; media_library_url: string; additional_json_data: unknown; - experts: CircleExpert[]; documents: CircleDocument[]; + users: CourseSessionUser[]; } export type Role = "MEMBER" | "EXPERT" | "TUTOR"; diff --git a/env_secrets/local_daniel.env b/env_secrets/local_daniel.env index 4127d960..1664c3b5 100644 Binary files a/env_secrets/local_daniel.env and b/env_secrets/local_daniel.env differ diff --git a/server/config/urls.py b/server/config/urls.py index de00dc75..8c779fe7 100644 --- a/server/config/urls.py +++ b/server/config/urls.py @@ -32,7 +32,6 @@ from vbv_lernwelt.course.views import ( request_course_completion, request_course_completion_for_user, ) - from vbv_lernwelt.feedback.views import get_name from wagtail import urls as wagtail_urls from wagtail.admin import urls as wagtailadmin_urls @@ -78,7 +77,7 @@ urlpatterns = [ name="mark_course_completion"), path(r"api/course/completion//", request_course_completion, name="request_course_completion"), - path(r"api/course/completion///", + path(r"api/course/completion///", request_course_completion_for_user, name="request_course_completion_for_user"), diff --git a/server/vbv_lernwelt/core/serializers.py b/server/vbv_lernwelt/core/serializers.py index f6dfe04b..0d6ef107 100644 --- a/server/vbv_lernwelt/core/serializers.py +++ b/server/vbv_lernwelt/core/serializers.py @@ -1,9 +1,12 @@ from rest_framework import serializers from vbv_lernwelt.core.models import User +from vbv_lernwelt.course.models import CourseSessionUser class UserSerializer(serializers.ModelSerializer): + course_session_experts = serializers.SerializerMethodField() + class Meta: model = User fields = [ @@ -13,4 +16,13 @@ class UserSerializer(serializers.ModelSerializer): "email", "username", "avatar_url", + "is_superuser", + "course_session_experts", ] + + def get_course_session_experts(self, obj): + qs = CourseSessionUser.objects.filter( + role=CourseSessionUser.Role.EXPERT, user=obj + ) + + return [csu.course_session.id for csu in qs] diff --git a/server/vbv_lernwelt/course/creators/test_course.py b/server/vbv_lernwelt/course/creators/test_course.py index 418f712f..26b1d2ae 100644 --- a/server/vbv_lernwelt/course/creators/test_course.py +++ b/server/vbv_lernwelt/course/creators/test_course.py @@ -132,16 +132,6 @@ def create_test_learning_path(user=None, skip_locales=True): ), ("goal", "... deinem Kunden seine optimale Lösung aufzuzeigen"), ], - experts=[ - ( - "person", - { - "last_name": "Huggel", - "first_name": "Patrizia", - "email": "patrizia.huggel@eiger-versicherungen.ch", - }, - ), - ], ) LearningSequenceFactory(title="Starten", parent=circle, icon="it-icon-ls-start") diff --git a/server/vbv_lernwelt/course/models.py b/server/vbv_lernwelt/course/models.py index f37503b3..94aad7b2 100644 --- a/server/vbv_lernwelt/course/models.py +++ b/server/vbv_lernwelt/course/models.py @@ -236,6 +236,9 @@ class CourseSessionUser(models.Model): "email": self.user.email, "avatar_url": self.user.avatar_url, "role": self.role, + "circles": self.expert.all().values( + "id", "title", "slug", "translation_key" + ), } diff --git a/server/vbv_lernwelt/course/permissions.py b/server/vbv_lernwelt/course/permissions.py index ce39431b..d409d453 100644 --- a/server/vbv_lernwelt/course/permissions.py +++ b/server/vbv_lernwelt/course/permissions.py @@ -3,15 +3,29 @@ from vbv_lernwelt.learnpath.models import LearningSequence def has_course_access_by_page_request(request, obj): - return has_course_access(request.user, obj.specific.get_course()) + return has_course_access(request.user, obj.specific.get_course().id) -def has_course_access(user, course): +def has_course_access(user, course_id): if user.is_superuser: return True if CourseSessionUser.objects.filter( - course_session__course_id=course.id, user=user + course_session__course_id=course_id, user=user + ).exists(): + return True + + return False + + +def is_course_expert(user, course_id: int): + if user.is_superuser: + return True + + if CourseSessionUser.objects.filter( + course_session__course_id=course_id, + user=user, + role=CourseSessionUser.Role.EXPERT, ).exists(): return True @@ -27,25 +41,23 @@ def course_sessions_for_user_qs(user): return course_sessions -def is_circle_expert(user, learning_sequence, course) -> bool: +def is_circle_expert(user, course_session_id: int, learning_sequence_id: int) -> bool: if user.is_superuser: return True try: - learning_sequence = LearningSequence.objects.get(id=learning_sequence) + learning_sequence = LearningSequence.objects.get(id=learning_sequence_id) except LearningSequence.DoesNotExist: return False circle_id = learning_sequence.get_parent().circle.id - try: - CourseSessionUser.objects.get( - course_session__id=course, - user_id=user.id, - role=CourseSessionUser.Role.EXPERT, - expert__id=circle_id, - ) - except CourseSessionUser.DoesNotExist: - return False + if CourseSessionUser.objects.filter( + course_session_id=course_session_id, + user=user, + role=CourseSessionUser.Role.EXPERT, + expert__id=circle_id, + ).exists(): + return True - return True + return False diff --git a/server/vbv_lernwelt/course/serializers.py b/server/vbv_lernwelt/course/serializers.py index 312f2c21..a9d64805 100644 --- a/server/vbv_lernwelt/course/serializers.py +++ b/server/vbv_lernwelt/course/serializers.py @@ -6,9 +6,7 @@ from vbv_lernwelt.course.models import ( CourseCategory, CourseCompletion, CourseSession, - CourseSessionUser, ) -from vbv_lernwelt.learnpath.models import Circle class CourseSerializer(serializers.ModelSerializer): @@ -50,7 +48,6 @@ class CourseSessionSerializer(serializers.ModelSerializer): learning_path_url = serializers.SerializerMethodField() competence_url = serializers.SerializerMethodField() media_library_url = serializers.SerializerMethodField() - experts = serializers.SerializerMethodField() documents = serializers.SerializerMethodField() def get_course(self, obj): @@ -68,26 +65,6 @@ class CourseSessionSerializer(serializers.ModelSerializer): def get_competence_url(self, obj): return obj.course.get_competence_url() - def get_experts(self, obj): - expert_relations = CourseSessionUser.objects.filter( - expert__in=Circle.objects.descendant_of(obj.course.coursepage) - ).distinct() - expert_result = [] - for er in expert_relations: - for circle in er.expert.all(): - expert_result.append( - { - "user_id": er.user.id, - "user_email": er.user.email, - "user_first_name": er.user.first_name, - "user_last_name": er.user.last_name, - "circle_id": circle.id, - "circle_slug": circle.slug, - "circle_translation_key": circle.translation_key, - } - ) - return expert_result - def get_documents(self, obj): documents = CircleDocument.objects.filter( course_session=obj, file__upload_finished_at__isnull=False @@ -109,7 +86,6 @@ class CourseSessionSerializer(serializers.ModelSerializer): "competence_url", "media_library_url", "course_url", - "experts", "documents", ] diff --git a/server/vbv_lernwelt/course/views.py b/server/vbv_lernwelt/course/views.py index 72626a01..9ca01fe9 100644 --- a/server/vbv_lernwelt/course/views.py +++ b/server/vbv_lernwelt/course/views.py @@ -13,8 +13,10 @@ from vbv_lernwelt.course.models import ( ) from vbv_lernwelt.course.permissions import ( course_sessions_for_user_qs, + has_course_access, has_course_access_by_page_request, is_circle_expert, + is_course_expert, ) from vbv_lernwelt.course.serializers import ( CourseCompletionSerializer, @@ -67,13 +69,16 @@ def _request_course_completion(course_id, user_id): @api_view(["GET"]) def request_course_completion(request, course_id): - return _request_course_completion(course_id, request.user.id) + if has_course_access(request.user, course_id): + return _request_course_completion(course_id, request.user.id) + raise PermissionDenied() @api_view(["GET"]) def request_course_completion_for_user(request, course_id, user_id): - # TODO: check permissions to access this users data - return _request_course_completion(course_id, user_id) + if request.user.id == user_id or is_course_expert(request.user, course_id): + return _request_course_completion(course_id, user_id) + raise PermissionDenied() @api_view(["POST"]) @@ -139,24 +144,9 @@ def get_course_session_users(request, course_slug): course__slug=course_slug ) qs = CourseSessionUser.objects.filter(course_session__in=course_sessions) - cockpit_user_csu = qs.filter(user_id=request.user.id) - if len(cockpit_user_csu) == 0: - return Response({"error": "User not found"}, status=404) - - user_data = [csu.to_dict() for csu in qs.exclude(user_id=request.user.id)] - - data = { - "cockpit_user": cockpit_user_csu[0].to_dict() - | { - "circles": cockpit_user_csu[0] - .expert.all() - .values("id", "title", "slug", "translation_key") - }, - "users": user_data, - } - - return Response(status=200, data=data) + user_data = [csu.to_dict() for csu in qs] + return Response(status=200, data=user_data) except PermissionDenied as e: raise e except Exception as e: @@ -171,8 +161,8 @@ def document_upload_start(request): if not is_circle_expert( request.user, - serializer.validated_data["learning_sequence"], serializer.validated_data["course_session"], + serializer.validated_data["learning_sequence"], ): raise PermissionDenied() @@ -227,7 +217,7 @@ def document_direct_upload(request, file_id): def document_delete(request, document_id): document = get_object_or_404(CircleDocument, id=document_id) if not is_circle_expert( - request.user, document.learning_sequence_id, document.course_session_id + request.user, document.course_session.id, document.learning_sequence.id ): raise PermissionDenied() diff --git a/server/vbv_lernwelt/learnpath/migrations/0010_remove_circle_experts.py b/server/vbv_lernwelt/learnpath/migrations/0010_remove_circle_experts.py new file mode 100644 index 00000000..e0dda869 --- /dev/null +++ b/server/vbv_lernwelt/learnpath/migrations/0010_remove_circle_experts.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.13 on 2023-01-11 09:31 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("learnpath", "0009_alter_learningcontent_contents"), + ] + + operations = [ + migrations.RemoveField( + model_name="circle", + name="experts", + ), + ] diff --git a/server/vbv_lernwelt/learnpath/models.py b/server/vbv_lernwelt/learnpath/models.py index 33e8fd2a..c3a2105d 100644 --- a/server/vbv_lernwelt/learnpath/models.py +++ b/server/vbv_lernwelt/learnpath/models.py @@ -4,7 +4,6 @@ from wagtail import blocks from wagtail.admin.panels import FieldPanel, StreamFieldPanel from wagtail.blocks import StreamBlock from wagtail.fields import StreamField -from wagtail.images.blocks import ImageChooserBlock from wagtail.models import Page from vbv_lernwelt.core.model_utils import find_available_slug @@ -73,17 +72,6 @@ class Topic(CourseBasePage): return f"{self.title}" -class PersonBlock(blocks.StructBlock): - first_name = blocks.CharBlock() - last_name = blocks.CharBlock() - email = blocks.EmailBlock() - photo = ImageChooserBlock(required=False) - biography = blocks.RichTextBlock(required=False) - - class Meta: - icon = "user" - - class Circle(CourseBasePage): parent_page_types = ["learnpath.LearningPath"] subpage_types = [ @@ -99,7 +87,6 @@ class Circle(CourseBasePage): "goals", "job_situation_description", "job_situations", - "experts", ] description = models.TextField(default="", blank=True) @@ -119,18 +106,11 @@ class Circle(CourseBasePage): ], use_json_field=True, ) - experts = StreamField( - [ - ("person", PersonBlock()), - ], - use_json_field=True, - ) content_panels = Page.content_panels + [ FieldPanel("description"), FieldPanel("goals"), FieldPanel("job_situations"), - FieldPanel("experts"), ] def get_frontend_url(self): diff --git a/server/vbv_lernwelt/learnpath/tests/learning_path_factories.py b/server/vbv_lernwelt/learnpath/tests/learning_path_factories.py index 17d188a6..a6297068 100644 --- a/server/vbv_lernwelt/learnpath/tests/learning_path_factories.py +++ b/server/vbv_lernwelt/learnpath/tests/learning_path_factories.py @@ -135,16 +135,6 @@ pretium quis, sem. Nulla consequat massa quis enim. Donec. goals = [ ("goal", f"... hier ein Beispieltext für ein Ziel {x + 1}") for x in range(3) ] - experts = [ - ( - "person", - { - "last_name": "Mustermann", - "first_name": "Patrizia", - "email": "patrizia.mustermann@example.com", - }, - ), - ] class Meta: model = Circle