Refactor CirclePage

This commit is contained in:
Daniel Egger 2023-10-12 12:26:57 +02:00
parent 2eddb93be5
commit 627e4f6873
12 changed files with 373 additions and 471 deletions

View File

@ -1,9 +1,8 @@
import { COURSE_SESSION_DETAIL_QUERY, LEARNING_PATH_QUERY } from "@/graphql/queries";
import { useCourseSessionsStore } from "@/stores/courseSessions";
import type { CourseSession, CourseSessionDetail } from "@/types";
import { useQuery } from "@urql/vue";
import { COURSE_SESSION_DETAIL_QUERY } from "@/graphql/queries";
import { useUserStore } from "@/stores/user";
import type { CourseSession, CourseSessionDetail, LearningPathType } from "@/types";
import { useQuery } from "@urql/vue";
import log from "loglevel";
import type { ComputedRef } from "vue";
import { computed, ref, watchEffect } from "vue";
@ -123,3 +122,31 @@ export function useCourseSessionDetailQuery(courSessionId?: string) {
filterCircleExperts,
};
}
export function useLearningPathQuery(courseSlug: string) {
const queryResult = useQuery({
query: LEARNING_PATH_QUERY,
variables: {
slug: `${courseSlug}-lp`,
},
});
const learningPath = computed(() => {
return queryResult.data.value?.learning_path as LearningPathType | undefined;
});
const circles = computed(() => {
if (learningPath.value) {
return learningPath.value.topics.flatMap((t) => t.circles);
}
return undefined;
});
function findCircle(slug: string) {
return (circles.value ?? []).find((c) => {
return c.slug.endsWith(slug);
});
}
return { ...queryResult, learningPath, circles, findCircle };
}

View File

@ -20,7 +20,7 @@ const documents = {
"\n query assignmentCompletionQuery(\n $assignmentId: ID!\n $courseSessionId: ID!\n $learningContentId: ID\n $assignmentUserId: UUID\n ) {\n assignment(id: $assignmentId) {\n assignment_type\n needs_expert_evaluation\n max_points\n content_type\n effort_required\n evaluation_description\n evaluation_document_url\n evaluation_tasks\n id\n intro_text\n performance_objectives\n slug\n tasks\n title\n translation_key\n competence_certificate {\n ...CoursePageFields\n }\n }\n assignment_completion(\n assignment_id: $assignmentId\n course_session_id: $courseSessionId\n assignment_user_id: $assignmentUserId\n learning_content_page_id: $learningContentId\n ) {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_user {\n id\n }\n assignment_user {\n id\n }\n evaluation_points\n evaluation_max_points\n evaluation_passed\n edoniq_extended_time_flag\n completion_data\n task_completion_data\n }\n }\n": types.AssignmentCompletionQueryDocument,
"\n query competenceCertificateQuery($courseSlug: String!, $courseSessionId: ID!) {\n competence_certificate_list(course_slug: $courseSlug) {\n ...CoursePageFields\n competence_certificates {\n ...CoursePageFields\n assignments {\n ...CoursePageFields\n assignment_type\n max_points\n completion(course_session_id: $courseSessionId) {\n id\n completion_status\n submitted_at\n evaluation_points\n evaluation_max_points\n evaluation_passed\n }\n learning_content {\n ...CoursePageFields\n circle {\n ...CoursePageFields\n }\n }\n }\n }\n }\n }\n": types.CompetenceCertificateQueryDocument,
"\n query courseSessionDetail($courseSessionId: ID!) {\n course_session(id: $courseSessionId) {\n id\n title\n course {\n id\n title\n slug\n }\n users {\n id\n user_id\n first_name\n last_name\n email\n avatar_url\n role\n circles {\n id\n title\n slug\n }\n }\n attendance_courses {\n id\n location\n trainer\n due_date {\n id\n start\n end\n }\n learning_content_id\n learning_content {\n id\n title\n circle {\n id\n title\n slug\n }\n }\n }\n assignments {\n id\n submission_deadline {\n id\n start\n }\n evaluation_deadline {\n id\n start\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n edoniq_tests {\n id\n deadline {\n id\n start\n end\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n }\n }\n": types.CourseSessionDetailDocument,
"\n query learningPathQuery($slug: String!) {\n learning_path(slug: $slug) {\n ...CoursePageFields\n topics {\n is_visible\n ...CoursePageFields\n circles {\n ...CoursePageFields\n learning_sequences {\n icon\n ...CoursePageFields\n learning_units {\n evaluate_url\n ...CoursePageFields\n learning_contents {\n ...CoursePageFields\n ... on LearningContentAssignmentObjectType {\n assignment_type\n content_assignment {\n id\n }\n }\n }\n }\n }\n }\n }\n }\n }\n": types.LearningPathQueryDocument,
"\n query learningPathQuery($slug: String!) {\n learning_path(slug: $slug) {\n ...CoursePageFields\n topics {\n is_visible\n ...CoursePageFields\n circles {\n ...CoursePageFields\n learning_sequences {\n icon\n ...CoursePageFields\n learning_units {\n evaluate_url\n ...CoursePageFields\n learning_contents {\n ...CoursePageFields\n ... on LearningContentAssignmentObjectType {\n assignment_type\n content_assignment {\n id\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentEdoniqTestObjectType {\n checkbox_text\n competence_certificate {\n ...CoursePageFields\n }\n }\n }\n }\n }\n }\n }\n }\n }\n": types.LearningPathQueryDocument,
"\n mutation SendFeedbackMutation(\n $courseSessionId: ID!\n $learningContentId: ID!\n $data: GenericScalar!\n $submitted: Boolean\n ) {\n send_feedback(\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n data: $data\n submitted: $submitted\n ) {\n feedback_response {\n id\n data\n submitted\n }\n errors {\n field\n messages\n }\n }\n }\n": types.SendFeedbackMutationDocument,
};
@ -69,7 +69,7 @@ export function graphql(source: "\n query courseSessionDetail($courseSessionId:
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query learningPathQuery($slug: String!) {\n learning_path(slug: $slug) {\n ...CoursePageFields\n topics {\n is_visible\n ...CoursePageFields\n circles {\n ...CoursePageFields\n learning_sequences {\n icon\n ...CoursePageFields\n learning_units {\n evaluate_url\n ...CoursePageFields\n learning_contents {\n ...CoursePageFields\n ... on LearningContentAssignmentObjectType {\n assignment_type\n content_assignment {\n id\n }\n }\n }\n }\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query learningPathQuery($slug: String!) {\n learning_path(slug: $slug) {\n ...CoursePageFields\n topics {\n is_visible\n ...CoursePageFields\n circles {\n ...CoursePageFields\n learning_sequences {\n icon\n ...CoursePageFields\n learning_units {\n evaluate_url\n ...CoursePageFields\n learning_contents {\n ...CoursePageFields\n ... on LearningContentAssignmentObjectType {\n assignment_type\n content_assignment {\n id\n }\n }\n }\n }\n }\n }\n }\n }\n }\n"];
export function graphql(source: "\n query learningPathQuery($slug: String!) {\n learning_path(slug: $slug) {\n ...CoursePageFields\n topics {\n is_visible\n ...CoursePageFields\n circles {\n ...CoursePageFields\n learning_sequences {\n icon\n ...CoursePageFields\n learning_units {\n evaluate_url\n ...CoursePageFields\n learning_contents {\n ...CoursePageFields\n ... on LearningContentAssignmentObjectType {\n assignment_type\n content_assignment {\n id\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentEdoniqTestObjectType {\n checkbox_text\n competence_certificate {\n ...CoursePageFields\n }\n }\n }\n }\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query learningPathQuery($slug: String!) {\n learning_path(slug: $slug) {\n ...CoursePageFields\n topics {\n is_visible\n ...CoursePageFields\n circles {\n ...CoursePageFields\n learning_sequences {\n icon\n ...CoursePageFields\n learning_units {\n evaluate_url\n ...CoursePageFields\n learning_contents {\n ...CoursePageFields\n ... on LearningContentAssignmentObjectType {\n assignment_type\n content_assignment {\n id\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentEdoniqTestObjectType {\n checkbox_text\n competence_certificate {\n ...CoursePageFields\n }\n }\n }\n }\n }\n }\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/

File diff suppressed because one or more lines are too long

View File

@ -22,46 +22,15 @@ type Query {
type LearningPathObjectType implements CoursePageInterface {
id: ID!
path: String!
depth: Int!
numchild: Int!
translation_key: String!
live: Boolean!
has_unpublished_changes: Boolean!
first_published_at: DateTime
last_published_at: DateTime
go_live_at: DateTime
expire_at: DateTime
expired: Boolean!
locked: Boolean!
locked_at: DateTime
locked_by: UserObjectType
title: String!
draft_title: String!
slug: String!
content_type: String!
url_path: String!
owner: UserObjectType
"""
Der Titel der Seite, dargestellt in Suchmaschinen-Ergebnissen als die verlinkte Überschrift.
"""
seo_title: String!
"""
Ob ein Link zu dieser Seite in automatisch generierten Menüs auftaucht.
"""
show_in_menus: Boolean!
"""
Die informative Beschreibung, dargestellt in Suchmaschinen-Ergebnissen unter der Überschrift.
"""
search_description: String!
latest_revision_created_at: DateTime
live: Boolean!
translation_key: String!
frontend_url: String!
circle: CircleObjectType
course: CourseObjectType
topics: [TopicObjectType]
topics: [TopicObjectType!]!
}
interface CoursePageInterface {
@ -88,7 +57,7 @@ type CircleObjectType implements CoursePageInterface {
frontend_url: String!
circle: CircleObjectType
course: CourseObjectType
learning_sequences: [LearningSequenceObjectType]
learning_sequences: [LearningSequenceObjectType!]!
}
type CourseObjectType {
@ -109,10 +78,11 @@ type LearningSequenceObjectType implements CoursePageInterface {
frontend_url: String!
circle: CircleObjectType
course: CourseObjectType
learning_units: [LearningUnitObjectType]
learning_units: [LearningUnitObjectType!]!
}
type LearningUnitObjectType implements CoursePageInterface {
title_hidden: Boolean!
id: ID!
title: String!
slug: String!
@ -141,44 +111,6 @@ interface LearningContentInterface {
content: String
}
"""
The `DateTime` scalar type represents a DateTime
value as specified by
[iso8601](https://en.wikipedia.org/wiki/ISO_8601).
"""
scalar DateTime
type UserObjectType {
"""
Erforderlich. 150 Zeichen oder weniger. Nur Buchstaben, Ziffern und @/./+/-/_.
"""
username: String!
first_name: String!
last_name: String!
id: UUID!
avatar_url: String!
email: String!
language: CoreUserLanguageChoices!
}
"""
Leverages the internal Python implementation of UUID (uuid.UUID) to provide native UUID objects
in fields, resolvers and input.
"""
scalar UUID
"""An enumeration."""
enum CoreUserLanguageChoices {
"""Deutsch"""
DE
"""Français"""
FR
"""Italiano"""
IT
}
type TopicObjectType implements CoursePageInterface {
is_visible: Boolean!
id: ID!
@ -190,7 +122,7 @@ type TopicObjectType implements CoursePageInterface {
frontend_url: String!
circle: CircleObjectType
course: CourseObjectType
circles: [CircleObjectType]
circles: [CircleObjectType!]!
}
type LearningContentMediaLibraryObjectType implements CoursePageInterface & LearningContentInterface {
@ -223,6 +155,7 @@ type LearningContentAssignmentObjectType implements CoursePageInterface & Learni
minutes: Int
description: String
content: String
competence_certificate: CompetenceCertificateObjectType!
}
type AssignmentObjectType implements CoursePageInterface {
@ -316,6 +249,44 @@ type AssignmentCompletionObjectType {
learning_content_page_id: ID
}
"""
Leverages the internal Python implementation of UUID (uuid.UUID) to provide native UUID objects
in fields, resolvers and input.
"""
scalar UUID
"""
The `DateTime` scalar type represents a DateTime
value as specified by
[iso8601](https://en.wikipedia.org/wiki/ISO_8601).
"""
scalar DateTime
type UserObjectType {
"""
Erforderlich. 150 Zeichen oder weniger. Nur Buchstaben, Ziffern und @/./+/-/_.
"""
username: String!
first_name: String!
last_name: String!
id: UUID!
avatar_url: String!
email: String!
language: CoreUserLanguageChoices!
}
"""An enumeration."""
enum CoreUserLanguageChoices {
"""Deutsch"""
DE
"""Français"""
FR
"""Italiano"""
IT
}
type CourseSessionObjectType {
id: ID!
created_at: DateTime!
@ -428,6 +399,7 @@ type CourseSessionEdoniqTestObjectType {
}
type LearningContentEdoniqTestObjectType implements CoursePageInterface & LearningContentInterface {
checkbox_text: String!
content_assignment: AssignmentObjectType
id: ID!
title: String!
@ -441,6 +413,7 @@ type LearningContentEdoniqTestObjectType implements CoursePageInterface & Learni
minutes: Int
description: String
content: String
competence_certificate: CompetenceCertificateObjectType!
}
type CourseSessionUserObjectsType {
@ -612,42 +585,11 @@ type LearningContentDocumentListObjectType implements CoursePageInterface & Lear
type CompetenceCertificateListObjectType implements CoursePageInterface {
id: ID!
path: String!
depth: Int!
numchild: Int!
translation_key: String!
live: Boolean!
has_unpublished_changes: Boolean!
first_published_at: DateTime
last_published_at: DateTime
go_live_at: DateTime
expire_at: DateTime
expired: Boolean!
locked: Boolean!
locked_at: DateTime
locked_by: UserObjectType
title: String!
draft_title: String!
slug: String!
content_type: String!
url_path: String!
owner: UserObjectType
"""
Der Titel der Seite, dargestellt in Suchmaschinen-Ergebnissen als die verlinkte Überschrift.
"""
seo_title: String!
"""
Ob ein Link zu dieser Seite in automatisch generierten Menüs auftaucht.
"""
show_in_menus: Boolean!
"""
Die informative Beschreibung, dargestellt in Suchmaschinen-Ergebnissen unter der Überschrift.
"""
search_description: String!
latest_revision_created_at: DateTime
live: Boolean!
translation_key: String!
frontend_url: String!
circle: CircleObjectType
course: CourseObjectType

View File

@ -213,6 +213,15 @@ export const LEARNING_PATH_QUERY = graphql(`
content_assignment {
id
}
competence_certificate {
...CoursePageFields
}
}
... on LearningContentEdoniqTestObjectType {
checkbox_text
competence_certificate {
...CoursePageFields
}
}
}
}

View File

@ -1,16 +1,15 @@
<script setup lang="ts">
import { useCircleStore } from "@/stores/circle";
import type { CourseSessionUser } from "@/types";
import { humanizeDuration } from "@/utils/humanizeDuration";
import sumBy from "lodash/sumBy";
import log from "loglevel";
import { computed, onMounted } from "vue";
import { computed, watch } from "vue";
import { useRoute } from "vue-router";
import CircleDiagram from "./CircleDiagram.vue";
import CircleOverview from "./CircleOverview.vue";
import DocumentSection from "./DocumentSection.vue";
import LearningSequence from "./LearningSequence.vue";
import { useCourseSessionDetailQuery } from "@/composables";
import { useCourseSessionDetailQuery, useLearningPathQuery } from "@/composables";
import { stringifyParse } from "@/utils/utils";
import { useCircleStore } from "@/stores/circle";
import LearningSequence from "@/pages/learningPath/circlePage/LearningSequence.vue";
export interface Props {
courseSlug: string;
@ -28,60 +27,55 @@ const props = withDefaults(defineProps<Props>(), {
profileUser: undefined,
});
log.debug("CirclePage created", props.readonly, props.profileUser);
log.debug("CirclePage created", stringifyParse(props));
const circleExperts = computed(() => {
if (circleStore.circle) {
return courseSessionDetailResult.filterCircleExperts(circleStore.circle.slug);
if (circle.value) {
return courseSessionDetailResult.filterCircleExperts(circle.value.slug);
}
return [];
});
const duration = computed(() => {
if (circleStore.circle) {
const minutes = sumBy(circleStore.circle.learningSequences, "minutes");
return humanizeDuration(minutes);
}
// if (circleStore.circle) {
// const minutes = sumBy(circleStore.circle.learningSequences, "minutes");
// return humanizeDuration(minutes);
// }
return "";
});
const showDuration = computed(() => {
return (
circleStore.circle && sumBy(circleStore.circle.learningSequences, "minutes") > 0
);
// return (
// circleStore.circle && sumBy(circleStore.circle.learningSequences, "minutes") > 0
// );
return false;
});
onMounted(async () => {
log.debug(
"CirclePage mounted",
props.courseSlug,
props.circleSlug,
props.profileUser
);
const lpQueryResult = useLearningPathQuery(props.courseSlug);
const circle = computed(() => {
return lpQueryResult.findCircle(props.circleSlug);
});
watch(
() => circle.value,
() => {
if (circle.value) {
log.debug("circle loaded", circle);
try {
if (props.profileUser) {
await circleStore.loadCircle(
props.courseSlug,
props.circleSlug,
props.profileUser.user_id
);
} else {
await circleStore.loadCircle(props.courseSlug, props.circleSlug);
}
if (route.hash.startsWith("#ls-") || route.hash.startsWith("#lu-")) {
const slugEnd = route.hash.replace("#", "");
let wagtailPage = null;
if (circle.value) {
let wagtailPage = null;
if (slugEnd.startsWith("ls-")) {
wagtailPage = circleStore.circle?.learningSequences.find((ls) => {
wagtailPage = circle.value.learning_sequences.find((ls) => {
return ls.slug.endsWith(slugEnd);
});
} else if (slugEnd.startsWith("lu-")) {
const learningUnits = circleStore.circle?.learningSequences.flatMap(
(ls) => ls.learningUnits
const learningUnits = circle.value.learning_sequences.flatMap(
(ls) => ls.learning_units
);
if (learningUnits) {
wagtailPage = learningUnits.find((lu) => {
@ -95,14 +89,17 @@ onMounted(async () => {
?.scrollIntoView({ behavior: "smooth" });
}
}
}
} catch (error) {
log.error(error);
}
});
}
}
);
</script>
<template>
<div>
<div v-if="circle">
<Teleport to="body">
<CircleOverview
:circle="circleStore.circle"
@ -145,7 +142,7 @@ onMounted(async () => {
</router-link>
<h1 class="text-blue-dark text-4xl lg:text-6xl" data-cy="circle-title">
{{ circleStore.circle?.title }}
{{ circle?.title }}
</h1>
<div v-if="showDuration" class="mt-2">
@ -176,7 +173,7 @@ onMounted(async () => {
{{ $t("circlePage.circleContentBoxTitle") }}
</h3>
<div class="mt-4 leading-relaxed">
{{ circleStore.circle?.description }}
{{ circle?.description }}
</div>
<button
@ -192,7 +189,7 @@ onMounted(async () => {
<div class="mt-4 leading-relaxed">
{{
$t("circlePage.contactExpertDescription", {
circleName: circleStore.circle?.title,
circleName: circle?.title,
})
}}
</div>
@ -220,9 +217,8 @@ onMounted(async () => {
<ol class="flex-auto bg-gray-200 px-4 py-8 lg:px-24">
<li
v-for="learningSequence in circleStore.circle?.learningSequences ||
[]"
:key="learningSequence.translation_key"
v-for="learningSequence in circle?.learning_sequences ?? []"
:key="learningSequence.id"
>
<LearningSequence
:learning-sequence="learningSequence"
@ -239,15 +235,6 @@ onMounted(async () => {
</template>
<style lang="postcss" scoped>
.circle-container {
/*background: linear-gradient(to right, white 0%, white 50%, theme(colors.gray.200) 50%, theme(colors.gray.200) 100%);*/
}
.circle {
/*max-width: 1440px;*/
/*margin: 0 auto;*/
}
.v-enter-active,
.v-leave-active {
transition: opacity 0.3s ease;

View File

@ -1,17 +1,13 @@
<script setup lang="ts">
import ItCheckbox from "@/components/ui/ItCheckbox.vue";
import LearningContentBadge from "@/pages/learningPath/LearningContentTypeBadge.vue";
import { showIcon } from "@/pages/learningPath/circlePage/learningSequenceUtils";
import { useCircleStore } from "@/stores/circle";
import type {
CourseCompletionStatus,
LearningContent,
LearningContentAssignment,
LearningContentEdoniqTest,
LearningContentInterface,
LearningSequence,
} from "@/types";
import findLast from "lodash/findLast";
import { computed } from "vue";
import { humanizeDuration } from "../../../utils/humanizeDuration";
import {
@ -30,52 +26,52 @@ const props = withDefaults(defineProps<Props>(), {
const circleStore = useCircleStore();
function toggleCompleted(learningContent: LearningContentInterface) {
let completionStatus: CourseCompletionStatus = "SUCCESS";
if (learningContent.completion_status === "SUCCESS") {
completionStatus = "FAIL";
}
circleStore.markCompletion(learningContent, completionStatus);
function toggleCompleted(learningContent: LearningContent) {
// let completionStatus: CourseCompletionStatus = "SUCCESS";
// if (learningContent.completion_status === "SUCCESS") {
// completionStatus = "FAIL";
// }
// circleStore.markCompletion(learningContent, completionStatus);
}
const someFinished = computed(() => {
if (props.learningSequence && circleStore.circle) {
return circleStore.circle.someFinishedInLearningSequence(
props.learningSequence.translation_key
);
}
// if (props.learningSequence && circleStore.circle) {
// return circleStore.circle.someFinishedInLearningSequence(
// props.learningSequence.translation_key
// );
// }
return false;
});
const allFinished = computed(() => {
if (props.learningSequence && circleStore.circle) {
return circleStore.circle.allFinishedInLearningSequence(
props.learningSequence.translation_key
);
}
// if (props.learningSequence && circleStore.circle) {
// return circleStore.circle.allFinishedInLearningSequence(
// props.learningSequence.translation_key
// );
// }
return false;
});
const continueTranslationKeyTuple = computed(() => {
if (props.learningSequence && circleStore.circle) {
const lastFinished = findLast(
circleStore.circle.flatLearningContents,
(learningContent) => {
return learningContent.completion_status === "SUCCESS";
}
);
if (!lastFinished) {
// must be the first
return [circleStore.circle.flatLearningContents[0].translation_key, true];
}
if (lastFinished && lastFinished.nextLearningContent) {
return [lastFinished.nextLearningContent.translation_key, false];
}
}
// if (props.learningSequence && circleStore.circle) {
// const lastFinished = findLast(
// circleStore.circle.flatLearningContents,
// (learningContent) => {
// return learningContent.completion_status === "SUCCESS";
// }
// );
//
// if (!lastFinished) {
// // must be the first
// return [circleStore.circle.flatLearningContents[0].translation_key, true];
// }
//
// if (lastFinished && lastFinished.nextLearningContent) {
// return [lastFinished.nextLearningContent.translation_key, false];
// }
// }
return "";
});
@ -97,8 +93,8 @@ const learningSequenceBorderClass = computed(() => {
function belongsToCompetenceCertificate(lc: LearningContent) {
return (
(lc.content_type === "learnpath.LearningContentAssignment" ||
lc.content_type === "learnpath.LearningContentEdoniqTest") &&
(lc.__typename === "LearningContentAssignmentObjectType" ||
lc.__typename === "LearningContentEdoniqTestObjectType") &&
lc.competence_certificate?.frontend_url
);
}
@ -136,7 +132,7 @@ function checkboxIconUncheckedTailwindClass(lc: LearningContent) {
<ol class="border bg-white px-4 lg:px-6" :class="learningSequenceBorderClass">
<li
v-for="learningUnit in learningSequence.learningUnits"
v-for="learningUnit in learningSequence.learning_units"
:id="learningUnit.slug"
:key="learningUnit.id"
class="pt-3 lg:pt-6"
@ -154,7 +150,7 @@ function checkboxIconUncheckedTailwindClass(lc: LearningContent) {
</div>
<ol>
<li
v-for="learningContent in learningUnit.learningContents"
v-for="learningContent in learningUnit.learning_contents"
:key="learningContent.id"
>
<div class="pb-6">
@ -258,31 +254,31 @@ function checkboxIconUncheckedTailwindClass(lc: LearningContent) {
</li>
</ol>
<div
v-if="learningUnit.children.length"
:class="{ 'cursor-pointer': !props.readonly }"
:data-cy="`${learningUnit.slug}`"
@click="!props.readonly && circleStore.openSelfEvaluation(learningUnit)"
>
<div
v-if="circleStore.calcSelfEvaluationStatus(learningUnit) === 'SUCCESS'"
class="self-evaluation-success flex items-center gap-4 pb-6"
>
<it-icon-smiley-happy class="mr-4 h-8 w-8 flex-none" data-cy="success" />
<div>{{ $t("selfEvaluation.selfEvaluationYes") }}</div>
</div>
<div
v-else-if="circleStore.calcSelfEvaluationStatus(learningUnit) === 'FAIL'"
class="self-evaluation-fail flex items-center gap-4 pb-6"
>
<it-icon-smiley-thinking class="mr-4 h-8 w-8 flex-none" data-cy="fail" />
<div>{{ $t("selfEvaluation.selfEvaluationNo") }}</div>
</div>
<div v-else class="self-evaluation-unknown flex items-center gap-4 pb-6">
<it-icon-smiley-neutral class="mr-4 h-8 w-8 flex-none" data-cy="unknown" />
<div>{{ $t("a.Selbsteinschätzung") }}</div>
</div>
</div>
<!-- <div-->
<!-- v-if="learningUnit.children.length"-->
<!-- :class="{ 'cursor-pointer': !props.readonly }"-->
<!-- :data-cy="`${learningUnit.slug}`"-->
<!-- @click="!props.readonly && circleStore.openSelfEvaluation(learningUnit)"-->
<!-- >-->
<!-- <div-->
<!-- v-if="circleStore.calcSelfEvaluationStatus(learningUnit) === 'SUCCESS'"-->
<!-- class="self-evaluation-success flex items-center gap-4 pb-6"-->
<!-- >-->
<!-- <it-icon-smiley-happy class="mr-4 h-8 w-8 flex-none" data-cy="success" />-->
<!-- <div>{{ $t("selfEvaluation.selfEvaluationYes") }}</div>-->
<!-- </div>-->
<!-- <div-->
<!-- v-else-if="circleStore.calcSelfEvaluationStatus(learningUnit) === 'FAIL'"-->
<!-- class="self-evaluation-fail flex items-center gap-4 pb-6"-->
<!-- >-->
<!-- <it-icon-smiley-thinking class="mr-4 h-8 w-8 flex-none" data-cy="fail" />-->
<!-- <div>{{ $t("selfEvaluation.selfEvaluationNo") }}</div>-->
<!-- </div>-->
<!-- <div v-else class="self-evaluation-unknown flex items-center gap-4 pb-6">-->
<!-- <it-icon-smiley-neutral class="mr-4 h-8 w-8 flex-none" data-cy="unknown" />-->
<!-- <div>{{ $t("a.Selbsteinschätzung") }}</div>-->
<!-- </div>-->
<!-- </div>-->
<hr v-if="!learningUnit.last" class="-mx-4 text-gray-500" />
</li>

View File

@ -3,7 +3,7 @@ import LearningContentContainer from "@/pages/learningPath/learningContentPage/L
import DocumentListBlock from "@/pages/learningPath/learningContentPage/blocks/DocumentListBlock.vue";
import EdoniqTestBlock from "@/pages/learningPath/learningContentPage/blocks/EdoniqTestBlock.vue";
import { useCircleStore } from "@/stores/circle";
import type { LearningContent, LearningContentType } from "@/types";
import type { LearningContent, LearningContentContentType } from "@/types";
import eventBus from "@/utils/eventBus";
import log from "loglevel";
import type { Component } from "vue";
@ -29,7 +29,7 @@ log.debug("LearningContentParent setup", props.learningContent);
const previousRoute = getPreviousRoute();
// can't use the type as component name, as some are reserved HTML components, e.g. video
const COMPONENTS: Record<LearningContentType, Component> = {
const COMPONENTS: Record<LearningContentContentType, Component> = {
"learnpath.LearningContentAssignment": AssignmentBlock,
"learnpath.LearningContentAttendanceCourse": AttendanceCourseBlock,
"learnpath.LearningContentDocumentList": DocumentListBlock,

View File

@ -1,11 +1,25 @@
import type {
AssignmentCompletionObjectType,
AssignmentCompletionStatus as AssignmentCompletionStatusGenerated,
CircleObjectType,
CourseCourseSessionUserRoleChoices,
CourseSessionObjectType,
CourseSessionUserObjectsType,
LearningContentAssignmentObjectType,
LearningContentAttendanceCourseObjectType,
LearningContentDocumentListObjectType,
LearningContentEdoniqTestObjectType,
LearningContentFeedbackObjectType,
LearningContentLearningModuleObjectType,
LearningContentMediaLibraryObjectType,
LearningContentPlaceholderObjectType,
LearningContentRichTextObjectType,
LearningContentVideoObjectType,
LearningPathObjectType,
LearningSequenceObjectType,
LearningUnitObjectType,
TopicObjectType,
} from "@/gql/graphql";
import type { Circle } from "@/services/circle";
import type { Component } from "vue";
export type LoginMethod = "local" | "sso";
@ -29,101 +43,98 @@ export interface CircleLight {
readonly title: string;
}
// refine generated types
export type LearningContentAssignment = LearningContentAssignmentObjectType & {
readonly content_type: "learnpath.LearningContentAssignment";
};
export type LearningContentAttendanceCourse =
LearningContentAttendanceCourseObjectType & {
readonly content_type: "learnpath.LearningContentAttendanceCourse";
};
export type LearningContentDocumentList = LearningContentDocumentListObjectType & {
readonly content_type: "learnpath.LearningContentDocumentList";
};
export type LearningContentEdoniqTest = LearningContentEdoniqTestObjectType & {
readonly content_type: "learnpath.LearningContentEdoniqTest";
};
export type LearningContentFeedback = LearningContentFeedbackObjectType & {
readonly content_type: "learnpath.LearningContentFeedback";
};
export type LearningContentLearningModule = LearningContentLearningModuleObjectType & {
readonly content_type: "learnpath.LearningContentLearningModule";
};
export type LearningContentMediaLibrary = LearningContentMediaLibraryObjectType & {
readonly content_type: "learnpath.LearningContentMediaLibrary";
};
export type LearningContentPlaceholder = LearningContentPlaceholderObjectType & {
readonly content_type: "learnpath.LearningContentPlaceholder";
};
export type LearningContentRichText = LearningContentRichTextObjectType & {
readonly content_type: "learnpath.LearningContentRichText";
};
export type LearningContentVideo = LearningContentVideoObjectType & {
readonly content_type: "learnpath.LearningContentVideo";
};
export type LearningContent =
| LearningContentAssignment
| LearningContentAttendanceCourse
| LearningContentDocumentList
| LearningContentEdoniqTest
| LearningContentFeedback
| LearningContentLearningModule
| LearningContentMediaLibrary
| LearningContentPlaceholder
| LearningContentRichText
| LearningContentEdoniqTest
| LearningContentVideo;
export type LearningContentType = LearningContent["content_type"];
export type LearningContentContentType = LearningContent["content_type"];
export interface LearningContentInterface extends BaseCourseWagtailPage {
readonly content_type: LearningContentType;
readonly minutes: number;
readonly description: string;
readonly content_url: string;
readonly can_user_self_toggle_course_completion: boolean;
parentCircle: Circle;
parentLearningSequence?: LearningSequence;
parentLearningUnit?: LearningUnit;
nextLearningContent?: LearningContent;
previousLearningContent?: LearningContent;
}
export type LearningUnit = Omit<
LearningUnitObjectType,
"content_type" | "learning_contents"
> & {
content_type: "learnpath.LearningUnit";
learning_contents: LearningContent[];
};
export interface LearningContentAssignment extends LearningContentInterface {
readonly content_type: "learnpath.LearningContentAssignment";
readonly content_assignment_id: string;
readonly assignment_type: AssignmentType;
readonly competence_certificate?: {
id: string;
title: string;
slug: string;
content_type: string;
translation_key: string;
frontend_url: string;
} | null;
}
export type LearningSequence = Omit<
LearningSequenceObjectType,
"content_type" | "learning_units"
> & {
content_type: "learnpath.LearningSequence";
learning_units: LearningUnit[];
};
export interface LearningContentAttendanceCourse extends LearningContentInterface {
readonly content_type: "learnpath.LearningContentAttendanceCourse";
}
export type CircleType = Omit<
CircleObjectType,
"content_type" | "learning_sequences"
> & {
content_type: "learnpath.Circle";
learning_sequences: LearningSequence[];
};
export interface LearningContentDocument {
readonly type: "document";
readonly id: string;
readonly value: MediaLibraryContentBlockValue;
}
export type TopicType = Omit<TopicObjectType, "content_type" | "circles"> & {
content_type: "learnpath.Topic";
circles: CircleType[];
};
export interface LearningContentDocumentList extends LearningContentInterface {
readonly content_type: "learnpath.LearningContentDocumentList";
readonly documents: LearningContentDocument[];
}
export interface LearningContentFeedback extends LearningContentInterface {
readonly content_type: "learnpath.LearningContentFeedback";
}
export interface LearningContentLearningModule extends LearningContentInterface {
readonly content_type: "learnpath.LearningContentLearningModule";
}
export interface LearningContentMediaLibrary extends LearningContentInterface {
readonly content_type: "learnpath.LearningContentMediaLibrary";
}
export interface LearningContentPlaceholder extends LearningContentInterface {
readonly content_type: "learnpath.LearningContentPlaceholder";
}
export interface LearningContentRichText extends LearningContentInterface {
readonly content_type: "learnpath.LearningContentRichText";
readonly text: string;
}
export interface LearningContentEdoniqTest extends LearningContentInterface {
readonly content_type: "learnpath.LearningContentEdoniqTest";
readonly checkbox_text: string;
readonly has_extended_time_test: boolean;
readonly content_assignment_id: string;
readonly competence_certificate?: {
id: string;
title: string;
slug: string;
content_type: string;
translation_key: string;
frontend_url: string;
} | null;
}
export interface LearningContentVideo extends LearningContentInterface {
readonly content_type: "learnpath.LearningContentVideo";
}
export type LearningPathType = Omit<
LearningPathObjectType,
"content_type" | "topics"
> & {
content_type: "learnpath.LearningPath";
topics: TopicType[];
};
export interface LearningUnitPerformanceCriteria extends BaseCourseWagtailPage {
readonly content_type: "competence.PerformanceCriteria";
@ -132,66 +143,6 @@ export interface LearningUnitPerformanceCriteria extends BaseCourseWagtailPage {
parentLearningUnit?: LearningUnit;
}
export interface LearningUnit extends BaseCourseWagtailPage {
readonly content_type: "learnpath.LearningUnit";
readonly evaluate_url: string;
readonly course_category: CourseCategory;
readonly title_hidden: boolean;
children: LearningUnitPerformanceCriteria[];
// additional frontend fields
learningContents: LearningContent[];
minutes: number;
parentLearningSequence?: LearningSequence;
parentCircle?: Circle;
last?: boolean;
}
export interface LearningSequence extends BaseCourseWagtailPage {
readonly content_type: "learnpath.LearningSequence";
icon: string;
learningUnits: LearningUnit[];
minutes: number;
}
export type CircleChild = LearningContentInterface | LearningUnit | LearningSequence;
export interface WagtailLearningPath extends BaseCourseWagtailPage {
readonly content_type: "learnpath.LearningPath";
course: Course;
children: LearningPathChild[];
}
export interface CircleGoal {
type: "goal";
value: string;
id: string;
}
export interface CircleJobSituation {
type: "job_situation";
value: string;
id: string;
}
export interface WagtailCircle extends BaseCourseWagtailPage {
readonly content_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 Topic extends BaseCourseWagtailPage {
readonly content_type: "learnpath.Topic";
readonly is_visible: boolean;
circles: Circle[];
}
export type LearningPathChild = Topic | WagtailCircle;
export interface CourseCompletion {
readonly id: string;
created_at: string;

View File

@ -5,6 +5,10 @@ export function assertUnreachable(msg: string): never {
throw new Error("Didn't expect to get here, " + msg);
}
export function stringifyParse(obj: any): any {
return JSON.parse(JSON.stringify(obj));
}
function createCourseUrl(courseSlug: string | undefined, specificSub: string): string {
if (!courseSlug) {
return "/";

View File

@ -30,6 +30,7 @@ class CompetenceCertificateListObjectType(DjangoObjectType):
class Meta:
model = CompetenceCertificateList
interfaces = (CoursePageInterface,)
fields = []
def resolve_competence_certificates(self, info):
return CompetenceCertificate.objects.child_of(self)

View File

@ -124,15 +124,21 @@ class LearningContentMediaLibraryObjectType(DjangoObjectType):
class LearningContentEdoniqTestObjectType(DjangoObjectType):
competence_certificate = graphene.Field(
"vbv_lernwelt.competence.graphql.types.CompetenceCertificateObjectType",
)
class Meta:
model = LearningContentEdoniqTest
interfaces = (
CoursePageInterface,
LearningContentInterface,
)
fields = [
"content_assignment",
]
fields = ["content_assignment", "checkbox_text", "has_extended_time_test"]
@staticmethod
def resolve_competence_certificate(root: LearningContentEdoniqTest, info):
return root.content_assignment.competence_certificate
class LearningContentRichTextObjectType(DjangoObjectType):
@ -146,6 +152,10 @@ class LearningContentRichTextObjectType(DjangoObjectType):
class LearningContentAssignmentObjectType(DjangoObjectType):
competence_certificate = graphene.Field(
"vbv_lernwelt.competence.graphql.types.CompetenceCertificateObjectType",
)
class Meta:
model = LearningContentAssignment
interfaces = (
@ -157,6 +167,10 @@ class LearningContentAssignmentObjectType(DjangoObjectType):
"assignment_type",
]
@staticmethod
def resolve_competence_certificate(root: LearningContentAssignment, info):
return root.content_assignment.competence_certificate
class LearningContentDocumentListObjectType(DjangoObjectType):
class Meta:
@ -177,7 +191,7 @@ class LearningUnitObjectType(DjangoObjectType):
class Meta:
model = LearningUnit
interfaces = (CoursePageInterface,)
fields = ["evaluate_url"]
fields = ["evaluate_url", "title_hidden"]
@staticmethod
def resolve_evaluate_url(root: LearningUnit, info, **kwargs):
@ -207,7 +221,9 @@ class LearningUnitObjectType(DjangoObjectType):
class LearningSequenceObjectType(DjangoObjectType):
learning_units = graphene.List(LearningUnitObjectType)
learning_units = graphene.List(
graphene.NonNull(LearningUnitObjectType), required=True
)
class Meta:
model = LearningSequence
@ -236,7 +252,9 @@ class LearningSequenceObjectType(DjangoObjectType):
class CircleObjectType(DjangoObjectType):
learning_sequences = graphene.List(LearningSequenceObjectType)
learning_sequences = graphene.List(
graphene.NonNull(LearningSequenceObjectType), required=True
)
class Meta:
model = Circle
@ -273,7 +291,7 @@ class CircleObjectType(DjangoObjectType):
class TopicObjectType(DjangoObjectType):
circles = graphene.List(CircleObjectType)
circles = graphene.List(graphene.NonNull(CircleObjectType), required=True)
class Meta:
model = Topic
@ -305,11 +323,12 @@ class TopicObjectType(DjangoObjectType):
class LearningPathObjectType(DjangoObjectType):
topics = graphene.List(TopicObjectType)
topics = graphene.List(graphene.NonNull(TopicObjectType), required=True)
class Meta:
model = LearningPath
interfaces = (CoursePageInterface,)
fields = []
@staticmethod
def resolve_topics(root: LearningPath, info, **kwargs):