Merged in feature/improve-course-session-loading-take2 (pull request #218)
Feature/improve course session loading take2
This commit is contained in:
commit
ea8ab0adcb
|
|
@ -17,7 +17,7 @@ const assignmentType = t(props.dueDate.assignment_type_translation_key);
|
|||
|
||||
const courseSessionsStore = useCourseSessionsStore();
|
||||
const courseSession = courseSessionsStore.allCourseSessions.find(
|
||||
(cs: CourseSession) => cs.id === props.dueDate.course_session
|
||||
(cs: CourseSession) => cs.id === props.dueDate.course_session_id
|
||||
);
|
||||
|
||||
if (!courseSession) {
|
||||
|
|
@ -28,10 +28,10 @@ const isExpert = courseSessionsStore.hasCockpit(courseSession);
|
|||
const url = isExpert ? props.dueDate.url_expert : props.dueDate.url;
|
||||
|
||||
const courseSessionTitle = computed(() => {
|
||||
if (props.dueDate.course_session) {
|
||||
if (props.dueDate.course_session_id) {
|
||||
return (
|
||||
courseSessionsStore.getCourseSessionById(props.dueDate.course_session)?.title ??
|
||||
""
|
||||
courseSessionsStore.getCourseSessionById(props.dueDate.course_session_id)
|
||||
?.title ?? ""
|
||||
);
|
||||
}
|
||||
return "";
|
||||
|
|
|
|||
|
|
@ -1,60 +0,0 @@
|
|||
import dayjs from "dayjs";
|
||||
|
||||
export const dueDatesTestData = () => {
|
||||
return [
|
||||
{
|
||||
id: 1,
|
||||
start: dayjs("2023-06-14T15:00:00+02:00"),
|
||||
end: dayjs("2023-06-14T18:00:00+02:00"),
|
||||
title: "Präsenzkurs Kickoff",
|
||||
url: "/course/überbetriebliche-kurse/learn/kickoff/präsenzkurs-kickoff",
|
||||
course_session: 2,
|
||||
page: 383,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
start: dayjs("2023-06-15T15:00:00+02:00"),
|
||||
end: dayjs("2023-06-15T18:00:00+02:00"),
|
||||
title: "Präsenzkurs Basis",
|
||||
url: "/course/überbetriebliche-kurse/learn/basis/präsenzkurs-basis",
|
||||
course_session: 2,
|
||||
page: 397,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
start: dayjs("2023-06-16T15:00:00+02:00"),
|
||||
end: dayjs("2023-06-16T18:00:00+02:00"),
|
||||
title: "Präsenzkurs Fahrzeug",
|
||||
url: "/course/überbetriebliche-kurse/learn/fahrzeug/präsenzkurs-fahrzeug",
|
||||
course_session: 2,
|
||||
page: 413,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
start: dayjs("2023-06-16T15:00:00+02:00"),
|
||||
end: dayjs("2023-06-16T18:00:00+02:00"),
|
||||
title: "Präsenzkurs Flugzeuge",
|
||||
url: "/course/überbetriebliche-kurse/learn/fahrzeug/präsenzkurs-fahrzeug",
|
||||
course_session: 2,
|
||||
page: 413,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
start: dayjs("2023-07-16T11:00:00+02:00"),
|
||||
end: dayjs("2023-07-16T18:00:00+02:00"),
|
||||
title: "Präsenzkurs Motorräder",
|
||||
url: "/course/überbetriebliche-kurse/learn/fahrzeug/präsenzkurs-fahrzeug",
|
||||
course_session: 2,
|
||||
page: 413,
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
start: dayjs("2023-08-09T15:00:00+02:00"),
|
||||
end: dayjs("2023-08-09T19:00:00+02:00"),
|
||||
title: "Präsenzkurs Fahrräder",
|
||||
url: "/course/überbetriebliche-kurse/learn/fahrzeug/präsenzkurs-fahrzeug",
|
||||
course_session: 2,
|
||||
page: 413,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
|
@ -37,7 +37,7 @@ import { onMounted, ref, watch } from "vue";
|
|||
import type { Circle } from "@/services/circle";
|
||||
|
||||
interface FeedbackSummary {
|
||||
circle_id: number;
|
||||
circle_id: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
|
|
@ -65,7 +65,7 @@ function makeSummary(
|
|||
const props = defineProps<{
|
||||
selctedCircles: string[];
|
||||
circles: Circle[];
|
||||
courseSessionId: number;
|
||||
courseSessionId: string;
|
||||
url: string;
|
||||
}>();
|
||||
|
||||
|
|
|
|||
|
|
@ -23,11 +23,11 @@ type Story = StoryObj<typeof AccountMenuContent>;
|
|||
|
||||
const courseSessions = [
|
||||
{
|
||||
id: 1,
|
||||
id: "1",
|
||||
title: "Bern 2023 a",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
id: "2",
|
||||
title: "Zürich 2023 a",
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import type { CourseSession } from "@/types";
|
|||
const props = defineProps<{
|
||||
courseSessions: CourseSession[];
|
||||
user: UserState;
|
||||
selectedCourseSession?: number;
|
||||
selectedCourseSession?: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(["selectCourseSession", "logout"]);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import { useTranslation } from "i18next-vue";
|
||||
import { useRouteLookups } from "@/utils/route";
|
||||
import { useCurrentCourseSession } from "@/composables";
|
||||
import { getCompetenceBaseUrl } from "@/utils/utils";
|
||||
import { getCompetenceNaviUrl, getLearningPathUrl } from "@/utils/utils";
|
||||
|
||||
const { inCompetenceProfile, inLearningPath } = useRouteLookups();
|
||||
const courseSession = useCurrentCourseSession();
|
||||
|
|
@ -24,7 +24,7 @@ const { t } = useTranslation();
|
|||
<div class="flex space-x-8">
|
||||
<router-link
|
||||
data-cy="preview-learn-path-link"
|
||||
:to="courseSession.learning_path_url"
|
||||
:to="getLearningPathUrl(courseSession.course.slug)"
|
||||
class="preview-nav-item"
|
||||
:class="{ 'preview-nav-item--active': inLearningPath() }"
|
||||
>
|
||||
|
|
@ -33,7 +33,7 @@ const { t } = useTranslation();
|
|||
|
||||
<router-link
|
||||
data-cy="preview-competence-profile-link"
|
||||
:to="getCompetenceBaseUrl(courseSession)"
|
||||
:to="getCompetenceNaviUrl(courseSession.course.slug)"
|
||||
class="preview-nav-item"
|
||||
:class="{ 'preview-nav-item--active': inCompetenceProfile() }"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export interface Item {
|
|||
|
||||
export interface Props {
|
||||
items: CourseSession[];
|
||||
selected?: number;
|
||||
selected?: string;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
|
|
|||
|
|
@ -15,7 +15,12 @@ import { breakpointsTailwind, useBreakpoints } from "@vueuse/core";
|
|||
import { computed, onMounted, reactive } from "vue";
|
||||
import { useTranslation } from "i18next-vue";
|
||||
import CoursePreviewBar from "@/components/header/CoursePreviewBar.vue";
|
||||
import { getCompetenceBaseUrl } from "@/utils/utils";
|
||||
import {
|
||||
getCockpitUrl,
|
||||
getCompetenceNaviUrl,
|
||||
getLearningPathUrl,
|
||||
getMediaCenterUrl,
|
||||
} from "@/utils/utils";
|
||||
|
||||
log.debug("MainNavigationBar created");
|
||||
|
||||
|
|
@ -71,7 +76,9 @@ onMounted(() => {
|
|||
v-if="userStore.loggedIn"
|
||||
:show="state.showMobileNavigationMenu"
|
||||
:course-session="courseSessionsStore.currentCourseSession"
|
||||
:media-url="courseSessionsStore.currentCourseSession?.media_library_url"
|
||||
:media-url="
|
||||
getMediaCenterUrl(courseSessionsStore.currentCourseSession?.course?.slug)
|
||||
"
|
||||
:user="userStore"
|
||||
@closemodal="state.showMobileNavigationMenu = false"
|
||||
@logout="userStore.handleLogout()"
|
||||
|
|
@ -129,7 +136,11 @@ onMounted(() => {
|
|||
<template v-if="courseSessionsStore.currentCourseSessionHasCockpit">
|
||||
<router-link
|
||||
data-cy="navigation-cockpit-link"
|
||||
:to="`${courseSessionsStore.currentCourseSession.course_url}/cockpit`"
|
||||
:to="
|
||||
getCockpitUrl(
|
||||
courseSessionsStore.currentCourseSession.course.slug
|
||||
)
|
||||
"
|
||||
class="nav-item"
|
||||
:class="{ 'nav-item--active': inCockpit() }"
|
||||
>
|
||||
|
|
@ -138,7 +149,11 @@ onMounted(() => {
|
|||
|
||||
<router-link
|
||||
data-cy="navigation-preview-link"
|
||||
:to="courseSessionsStore.currentCourseSession.learning_path_url"
|
||||
:to="
|
||||
getLearningPathUrl(
|
||||
courseSessionsStore.currentCourseSession.course.slug
|
||||
)
|
||||
"
|
||||
target="_blank"
|
||||
class="nav-item"
|
||||
>
|
||||
|
|
@ -151,7 +166,11 @@ onMounted(() => {
|
|||
<template v-else>
|
||||
<router-link
|
||||
data-cy="navigation-learning-path-link"
|
||||
:to="courseSessionsStore.currentCourseSession.learning_path_url"
|
||||
:to="
|
||||
getLearningPathUrl(
|
||||
courseSessionsStore.currentCourseSession.course.slug
|
||||
)
|
||||
"
|
||||
class="nav-item"
|
||||
:class="{ 'nav-item--active': inLearningPath() }"
|
||||
>
|
||||
|
|
@ -161,7 +180,9 @@ onMounted(() => {
|
|||
<router-link
|
||||
data-cy="navigation-competence-profile-link"
|
||||
:to="
|
||||
getCompetenceBaseUrl(courseSessionsStore.currentCourseSession)
|
||||
getCompetenceNaviUrl(
|
||||
courseSessionsStore.currentCourseSession.course.slug
|
||||
)
|
||||
"
|
||||
class="nav-item"
|
||||
:class="{ 'nav-item--active': inCompetenceProfile() }"
|
||||
|
|
@ -176,7 +197,11 @@ onMounted(() => {
|
|||
<div class="flex items-stretch justify-start space-x-8">
|
||||
<router-link
|
||||
v-if="inCourse() && courseSessionsStore.currentCourseSession"
|
||||
:to="courseSessionsStore.currentCourseSession.media_library_url"
|
||||
:to="
|
||||
getMediaCenterUrl(
|
||||
courseSessionsStore.currentCourseSession.course.slug
|
||||
)
|
||||
"
|
||||
data-cy="medialibrary-link"
|
||||
class="nav-item-no-mobile"
|
||||
:class="{ 'nav-item--active': inMediaLibrary() }"
|
||||
|
|
|
|||
|
|
@ -4,7 +4,12 @@ import { useCourseSessionsStore } from "@/stores/courseSessions";
|
|||
import type { UserState } from "@/stores/user";
|
||||
import type { CourseSession } from "@/types";
|
||||
import { useRouter } from "vue-router";
|
||||
import { getCompetenceBaseUrl } from "@/utils/utils";
|
||||
import {
|
||||
getCockpitUrl,
|
||||
getCompetenceNaviUrl,
|
||||
getLearningPathUrl,
|
||||
getMediaCenterUrl,
|
||||
} from "@/utils/utils";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
|
|
@ -60,7 +65,7 @@ const courseSessionsStore = useCourseSessionsStore();
|
|||
<li class="mb-6">
|
||||
<button
|
||||
data-cy="navigation-mobile-cockpit-link"
|
||||
@click="clickLink(`${courseSession.course_url}/cockpit`)"
|
||||
@click="clickLink(getCockpitUrl(courseSession.course.slug))"
|
||||
>
|
||||
{{ $t("cockpit.title") }}
|
||||
</button>
|
||||
|
|
@ -68,7 +73,7 @@ const courseSessionsStore = useCourseSessionsStore();
|
|||
<li class="mb-6">
|
||||
<button
|
||||
data-cy="navigation-mobile-preview-link"
|
||||
@click="clickLink(courseSession.learning_path_url)"
|
||||
@click="clickLink(getLearningPathUrl(courseSession.course.slug))"
|
||||
>
|
||||
{{ $t("a.VorschauTeilnehmer") }}
|
||||
</button>
|
||||
|
|
@ -78,7 +83,7 @@ const courseSessionsStore = useCourseSessionsStore();
|
|||
<li class="mb-6">
|
||||
<button
|
||||
data-cy="navigation-mobile-learning-path-link"
|
||||
@click="clickLink(courseSession.learning_path_url)"
|
||||
@click="clickLink(getLearningPathUrl(courseSession.course.slug))"
|
||||
>
|
||||
{{ $t("general.learningPath") }}
|
||||
</button>
|
||||
|
|
@ -86,7 +91,7 @@ const courseSessionsStore = useCourseSessionsStore();
|
|||
<li class="mb-6">
|
||||
<button
|
||||
data-cy="navigation-mobile-competence-profile-link"
|
||||
@click="clickLink(getCompetenceBaseUrl(courseSession))"
|
||||
@click="clickLink(getCompetenceNaviUrl(courseSession.course.slug))"
|
||||
>
|
||||
{{ $t("competences.title") }}
|
||||
</button>
|
||||
|
|
@ -95,7 +100,7 @@ const courseSessionsStore = useCourseSessionsStore();
|
|||
<li class="mb-6">
|
||||
<button
|
||||
data-cy="medialibrary-link"
|
||||
@click="clickLink(`${courseSession?.media_library_url}`)"
|
||||
@click="clickLink(getMediaCenterUrl(courseSession.course.slug))"
|
||||
>
|
||||
{{ $t("a.Mediathek") }}
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -10,19 +10,22 @@ export interface Props {
|
|||
diagramType?: DiagramType;
|
||||
learningPath: LearningPath;
|
||||
// set to undefined (default) to show all circles
|
||||
showCircleTranslationKeys?: string[];
|
||||
showCircleSlugs?: string[];
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
diagramType: "horizontal",
|
||||
showCircleTranslationKeys: undefined,
|
||||
showCircleSlugs: undefined,
|
||||
});
|
||||
|
||||
const circles = computed(() =>
|
||||
props.learningPath.circles.filter(
|
||||
(c) => props.showCircleTranslationKeys?.includes(c.translation_key) ?? true
|
||||
)
|
||||
);
|
||||
const circles = computed(() => {
|
||||
if (props.showCircleSlugs?.length) {
|
||||
return props.learningPath.circles.filter(
|
||||
(c) => props.showCircleSlugs?.includes(c.slug) ?? true
|
||||
);
|
||||
}
|
||||
return props.learningPath.circles;
|
||||
});
|
||||
|
||||
const wrapperClasses = computed(() => {
|
||||
let classes = "flex my-5";
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ const emit = defineEmits<{
|
|||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: () => {
|
||||
return {
|
||||
id: -1,
|
||||
id: "-1",
|
||||
name: "",
|
||||
};
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import type { CourseSession } from "@/types";
|
||||
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 log from "loglevel";
|
||||
import type { ComputedRef } from "vue";
|
||||
import { computed } from "vue";
|
||||
import { computed, ref, watchEffect } from "vue";
|
||||
|
||||
export function useCurrentCourseSession() {
|
||||
/**
|
||||
|
|
@ -28,3 +32,94 @@ export function useCurrentCourseSession() {
|
|||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
export function useCourseSessionDetailQuery(courSessionId?: string) {
|
||||
if (!courSessionId) {
|
||||
courSessionId = useCurrentCourseSession().value.id;
|
||||
}
|
||||
const queryResult = useQuery({
|
||||
query: COURSE_SESSION_DETAIL_QUERY,
|
||||
variables: {
|
||||
courseSessionId: courSessionId,
|
||||
},
|
||||
});
|
||||
|
||||
const courseSessionDetail = computed(() => {
|
||||
return queryResult.data.value?.course_session as CourseSessionDetail | undefined;
|
||||
});
|
||||
|
||||
function findAssignmentByAssignmentId(assignmentId: string) {
|
||||
return (courseSessionDetail.value?.assignments ?? []).find((a) => {
|
||||
return a.learning_content?.content_assignment?.id === assignmentId;
|
||||
});
|
||||
}
|
||||
|
||||
function findAssignment(learningContentId: string) {
|
||||
return (courseSessionDetail.value?.assignments ?? []).find((a) => {
|
||||
return a.learning_content.id === learningContentId;
|
||||
});
|
||||
}
|
||||
|
||||
function findEdoniqTest(learningContentId: string) {
|
||||
return (courseSessionDetail.value?.edoniq_tests ?? []).find((e) => {
|
||||
return e.learning_content.id === learningContentId;
|
||||
});
|
||||
}
|
||||
|
||||
function findAttendanceCourse(learningContentId: string) {
|
||||
return (courseSessionDetail.value?.attendance_courses ?? []).find((e) => {
|
||||
return e.learning_content.id === learningContentId;
|
||||
});
|
||||
}
|
||||
|
||||
function findUser(userId: string) {
|
||||
return (courseSessionDetail.value?.users ?? []).find((u) => {
|
||||
return u.user_id === userId;
|
||||
});
|
||||
}
|
||||
|
||||
function findCurrentUser() {
|
||||
const userStore = useUserStore();
|
||||
const userId = userStore.id;
|
||||
return findUser(userId);
|
||||
}
|
||||
|
||||
function filterMembers() {
|
||||
return (courseSessionDetail.value?.users ?? []).filter((u) => {
|
||||
return u.role === "MEMBER";
|
||||
});
|
||||
}
|
||||
|
||||
function filterCircleExperts(circleSlug: string) {
|
||||
return (courseSessionDetail.value?.users ?? []).filter((u) => {
|
||||
return u.role === "EXPERT" && u.circles.map((c) => c.slug).includes(circleSlug);
|
||||
});
|
||||
}
|
||||
|
||||
const dataLoaded = ref(false);
|
||||
|
||||
function waitForData() {
|
||||
return new Promise((resolve) => {
|
||||
watchEffect(() => {
|
||||
if (queryResult.data.value) {
|
||||
dataLoaded.value = true;
|
||||
resolve(queryResult.data.value);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...queryResult,
|
||||
courseSessionDetail,
|
||||
waitForData,
|
||||
findAssignmentByAssignmentId,
|
||||
findAssignment,
|
||||
findEdoniqTest,
|
||||
findAttendanceCourse,
|
||||
findUser,
|
||||
findCurrentUser,
|
||||
filterMembers,
|
||||
filterCircleExperts,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,8 +18,9 @@ const documents = {
|
|||
"\n fragment CoursePageFields on CoursePageInterface {\n title\n id\n slug\n content_type\n frontend_url\n }\n": types.CoursePageFieldsFragmentDoc,
|
||||
"\n query attendanceCheckQuery($courseSessionId: ID!) {\n course_session_attendance_course(id: $courseSessionId) {\n id\n attendance_user_list {\n user_id\n status\n }\n }\n }\n": types.AttendanceCheckQueryDocument,
|
||||
"\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 }\n }\n": types.AssignmentCompletionQueryDocument,
|
||||
"\n query courseQuery($courseId: Int!) {\n course(id: $courseId) {\n id\n slug\n title\n category_name\n learning_path {\n id\n }\n }\n }\n": types.CourseQueryDocument,
|
||||
"\n query courseQuery($courseId: ID!) {\n course(id: $courseId) {\n id\n slug\n title\n category_name\n learning_path {\n id\n }\n }\n }\n": types.CourseQueryDocument,
|
||||
"\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 title\n id\n slug\n content_type\n frontend_url\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 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,
|
||||
};
|
||||
|
||||
|
|
@ -60,11 +61,15 @@ export function graphql(source: "\n query assignmentCompletionQuery(\n $assi
|
|||
/**
|
||||
* 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 courseQuery($courseId: Int!) {\n course(id: $courseId) {\n id\n slug\n title\n category_name\n learning_path {\n id\n }\n }\n }\n"): (typeof documents)["\n query courseQuery($courseId: Int!) {\n course(id: $courseId) {\n id\n slug\n title\n category_name\n learning_path {\n id\n }\n }\n }\n"];
|
||||
export function graphql(source: "\n query courseQuery($courseId: ID!) {\n course(id: $courseId) {\n id\n slug\n title\n category_name\n learning_path {\n id\n }\n }\n }\n"): (typeof documents)["\n query courseQuery($courseId: ID!) {\n course(id: $courseId) {\n id\n slug\n title\n category_name\n learning_path {\n id\n }\n }\n }\n"];
|
||||
/**
|
||||
* 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 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 title\n id\n slug\n content_type\n frontend_url\n circle {\n ...CoursePageFields\n }\n }\n }\n }\n }\n }\n"): (typeof documents)["\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 title\n id\n slug\n content_type\n frontend_url\n circle {\n ...CoursePageFields\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.
|
||||
*/
|
||||
export function graphql(source: "\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"): (typeof documents)["\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"];
|
||||
/**
|
||||
* 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
|
|
@ -8,11 +8,12 @@ type Query {
|
|||
learning_content_learning_module: LearningContentLearningModuleObjectType
|
||||
learning_content_placeholder: LearningContentPlaceholderObjectType
|
||||
learning_content_rich_text: LearningContentRichTextObjectType
|
||||
learning_content_test: LearningContentTestObjectType
|
||||
learning_content_test: LearningContentEdoniqTestObjectType
|
||||
learning_content_video: LearningContentVideoObjectType
|
||||
learning_content_document_list: LearningContentDocumentListObjectType
|
||||
course_session_attendance_course(id: ID!, assignment_user_id: ID): CourseSessionAttendanceCourseType
|
||||
course(id: Int): CourseObjectType
|
||||
course_session_attendance_course(id: ID!, assignment_user_id: ID): CourseSessionAttendanceCourseObjectType
|
||||
course(id: ID): CourseObjectType
|
||||
course_session(id: ID): CourseSessionObjectType
|
||||
competence_certificate(id: ID, slug: String): CompetenceCertificateObjectType
|
||||
competence_certificate_list(id: ID, slug: String, course_id: ID, course_slug: String): CompetenceCertificateListObjectType
|
||||
assignment(id: ID, slug: String): AssignmentObjectType
|
||||
|
|
@ -307,12 +308,167 @@ type AssignmentCompletionObjectType {
|
|||
edoniq_extended_time_flag: Boolean!
|
||||
assignment_user: UserType!
|
||||
assignment: AssignmentObjectType!
|
||||
course_session: CourseSessionObjectType!
|
||||
completion_status: AssignmentAssignmentCompletionCompletionStatusChoices!
|
||||
completion_data: GenericScalar
|
||||
additional_json_data: JSONString!
|
||||
learning_content_page_id: ID
|
||||
}
|
||||
|
||||
type CourseSessionObjectType {
|
||||
id: ID!
|
||||
created_at: DateTime!
|
||||
updated_at: DateTime!
|
||||
course: CourseObjectType!
|
||||
title: String!
|
||||
start_date: Date
|
||||
end_date: Date
|
||||
attendance_courses: [CourseSessionAttendanceCourseObjectType]
|
||||
assignments: [CourseSessionAssignmentObjectType]
|
||||
edoniq_tests: [CourseSessionEdoniqTestObjectType]
|
||||
documents: [CircleDocumentObjectType]
|
||||
users: [CourseSessionUserObjectsType]
|
||||
}
|
||||
|
||||
"""
|
||||
The `Date` scalar type represents a Date
|
||||
value as specified by
|
||||
[iso8601](https://en.wikipedia.org/wiki/ISO_8601).
|
||||
"""
|
||||
scalar Date
|
||||
|
||||
type CourseSessionAttendanceCourseObjectType {
|
||||
id: ID!
|
||||
learning_content: LearningContentAttendanceCourseObjectType
|
||||
due_date: DueDateObjectType
|
||||
location: String!
|
||||
trainer: String!
|
||||
course_session_id: ID
|
||||
learning_content_id: ID
|
||||
attendance_user_list: [AttendanceUserObjectType]
|
||||
}
|
||||
|
||||
type LearningContentAttendanceCourseObjectType implements LearningContentInterface {
|
||||
id: ID
|
||||
title: String
|
||||
slug: String
|
||||
content_type: String
|
||||
live: Boolean
|
||||
translation_key: String
|
||||
frontend_url: String
|
||||
circle: CircleObjectType
|
||||
course: CourseObjectType
|
||||
minutes: Int
|
||||
description: String
|
||||
content: String
|
||||
}
|
||||
|
||||
type DueDateObjectType {
|
||||
id: ID!
|
||||
|
||||
"""Startdatum ist Pflicht"""
|
||||
start: DateTime
|
||||
|
||||
"""Enddatum ist optional"""
|
||||
end: DateTime
|
||||
|
||||
"""Nur aktivieren, wenn man die Felder manuell überschreiben will"""
|
||||
manual_override_fields: Boolean!
|
||||
|
||||
"""Title wird standarmässig vom LearningContent übernommen"""
|
||||
title: String!
|
||||
|
||||
"""Translation Key aus dem Frontend"""
|
||||
assignment_type_translation_key: String!
|
||||
|
||||
"""Translation Key aus dem Frontend"""
|
||||
date_type_translation_key: String!
|
||||
|
||||
"""
|
||||
Überschreibt den Untertitel bei `assignment_type_translation_key` und `date_type_translation_key`
|
||||
"""
|
||||
subtitle: String!
|
||||
|
||||
"""
|
||||
URL wird vom LearningContent übernommen (sichtbar für Member/Teilnehmer)
|
||||
"""
|
||||
url: String!
|
||||
course_session: CourseSessionObjectType!
|
||||
}
|
||||
|
||||
type AttendanceUserObjectType {
|
||||
user_id: UUID!
|
||||
status: AttendanceUserStatus!
|
||||
first_name: String
|
||||
last_name: String
|
||||
email: String
|
||||
}
|
||||
|
||||
"""An enumeration."""
|
||||
enum AttendanceUserStatus {
|
||||
PRESENT
|
||||
ABSENT
|
||||
}
|
||||
|
||||
type CourseSessionAssignmentObjectType {
|
||||
id: ID!
|
||||
learning_content: LearningContentAssignmentObjectType
|
||||
submission_deadline: DueDateObjectType
|
||||
evaluation_deadline: DueDateObjectType
|
||||
course_session_id: ID
|
||||
learning_content_id: ID
|
||||
}
|
||||
|
||||
type CourseSessionEdoniqTestObjectType {
|
||||
id: ID!
|
||||
learning_content: LearningContentEdoniqTestObjectType
|
||||
deadline: DueDateObjectType
|
||||
course_session_id: ID
|
||||
learning_content_id: ID
|
||||
}
|
||||
|
||||
type LearningContentEdoniqTestObjectType implements LearningContentInterface {
|
||||
content_assignment: AssignmentObjectType
|
||||
id: ID
|
||||
title: String
|
||||
slug: String
|
||||
content_type: String
|
||||
live: Boolean
|
||||
translation_key: String
|
||||
frontend_url: String
|
||||
circle: CircleObjectType
|
||||
course: CourseObjectType
|
||||
minutes: Int
|
||||
description: String
|
||||
content: String
|
||||
}
|
||||
|
||||
type CircleDocumentObjectType {
|
||||
id: UUID!
|
||||
name: String!
|
||||
course_session: CourseSessionObjectType!
|
||||
learning_sequence: LearningSequenceObjectType!
|
||||
file_name: String
|
||||
url: String
|
||||
}
|
||||
|
||||
type CourseSessionUserObjectsType {
|
||||
id: UUID!
|
||||
role: String
|
||||
user_id: UUID
|
||||
first_name: String
|
||||
last_name: String
|
||||
email: String
|
||||
avatar_url: String
|
||||
circles: [CourseSessionUserExpertCircleType]
|
||||
}
|
||||
|
||||
type CourseSessionUserExpertCircleType {
|
||||
id: ID
|
||||
title: String
|
||||
slug: String
|
||||
}
|
||||
|
||||
"""An enumeration."""
|
||||
enum AssignmentAssignmentCompletionCompletionStatusChoices {
|
||||
"""IN_PROGRESS"""
|
||||
|
|
@ -361,21 +517,6 @@ enum LearnpathLearningContentAssignmentAssignmentTypeChoices {
|
|||
EDONIQ_TEST
|
||||
}
|
||||
|
||||
type LearningContentAttendanceCourseObjectType implements LearningContentInterface {
|
||||
id: ID
|
||||
title: String
|
||||
slug: String
|
||||
content_type: String
|
||||
live: Boolean
|
||||
translation_key: String
|
||||
frontend_url: String
|
||||
circle: CircleObjectType
|
||||
course: CourseObjectType
|
||||
minutes: Int
|
||||
description: String
|
||||
content: String
|
||||
}
|
||||
|
||||
type LearningContentFeedbackObjectType implements LearningContentInterface {
|
||||
id: ID
|
||||
title: String
|
||||
|
|
@ -436,22 +577,6 @@ type LearningContentRichTextObjectType implements LearningContentInterface {
|
|||
content: String
|
||||
}
|
||||
|
||||
type LearningContentTestObjectType implements LearningContentInterface {
|
||||
content_assignment: AssignmentObjectType
|
||||
id: ID
|
||||
title: String
|
||||
slug: String
|
||||
content_type: String
|
||||
live: Boolean
|
||||
translation_key: String
|
||||
frontend_url: String
|
||||
circle: CircleObjectType
|
||||
course: CourseObjectType
|
||||
minutes: Int
|
||||
description: String
|
||||
content: String
|
||||
}
|
||||
|
||||
type LearningContentVideoObjectType implements LearningContentInterface {
|
||||
id: ID
|
||||
title: String
|
||||
|
|
@ -482,32 +607,6 @@ type LearningContentDocumentListObjectType implements LearningContentInterface {
|
|||
content: String
|
||||
}
|
||||
|
||||
type CourseSessionAttendanceCourseType {
|
||||
id: ID!
|
||||
location: String!
|
||||
trainer: String!
|
||||
course_session_id: ID
|
||||
learning_content_id: ID
|
||||
due_date_id: ID
|
||||
end: DateTime
|
||||
start: DateTime
|
||||
attendance_user_list: [AttendanceUserType]
|
||||
}
|
||||
|
||||
type AttendanceUserType {
|
||||
user_id: UUID!
|
||||
status: AttendanceUserStatus!
|
||||
first_name: String
|
||||
last_name: String
|
||||
email: String
|
||||
}
|
||||
|
||||
"""An enumeration."""
|
||||
enum AttendanceUserStatus {
|
||||
PRESENT
|
||||
ABSENT
|
||||
}
|
||||
|
||||
type CompetenceCertificateListObjectType implements CoursePageInterface {
|
||||
id: ID
|
||||
path: String!
|
||||
|
|
@ -577,7 +676,7 @@ type ErrorType {
|
|||
}
|
||||
|
||||
type AttendanceCourseUserMutation {
|
||||
course_session_attendance_course: CourseSessionAttendanceCourseType
|
||||
course_session_attendance_course: CourseSessionAttendanceCourseObjectType
|
||||
}
|
||||
|
||||
input AttendanceUserInputType {
|
||||
|
|
|
|||
|
|
@ -6,17 +6,25 @@ export const AssignmentCompletionStatus = "AssignmentCompletionStatus";
|
|||
export const AssignmentObjectType = "AssignmentObjectType";
|
||||
export const AttendanceCourseUserMutation = "AttendanceCourseUserMutation";
|
||||
export const AttendanceUserInputType = "AttendanceUserInputType";
|
||||
export const AttendanceUserObjectType = "AttendanceUserObjectType";
|
||||
export const AttendanceUserStatus = "AttendanceUserStatus";
|
||||
export const AttendanceUserType = "AttendanceUserType";
|
||||
export const Boolean = "Boolean";
|
||||
export const CircleDocumentObjectType = "CircleDocumentObjectType";
|
||||
export const CircleObjectType = "CircleObjectType";
|
||||
export const CompetenceCertificateListObjectType = "CompetenceCertificateListObjectType";
|
||||
export const CompetenceCertificateObjectType = "CompetenceCertificateObjectType";
|
||||
export const CoreUserLanguageChoices = "CoreUserLanguageChoices";
|
||||
export const CourseObjectType = "CourseObjectType";
|
||||
export const CoursePageInterface = "CoursePageInterface";
|
||||
export const CourseSessionAttendanceCourseType = "CourseSessionAttendanceCourseType";
|
||||
export const CourseSessionAssignmentObjectType = "CourseSessionAssignmentObjectType";
|
||||
export const CourseSessionAttendanceCourseObjectType = "CourseSessionAttendanceCourseObjectType";
|
||||
export const CourseSessionEdoniqTestObjectType = "CourseSessionEdoniqTestObjectType";
|
||||
export const CourseSessionObjectType = "CourseSessionObjectType";
|
||||
export const CourseSessionUserExpertCircleType = "CourseSessionUserExpertCircleType";
|
||||
export const CourseSessionUserObjectsType = "CourseSessionUserObjectsType";
|
||||
export const Date = "Date";
|
||||
export const DateTime = "DateTime";
|
||||
export const DueDateObjectType = "DueDateObjectType";
|
||||
export const ErrorType = "ErrorType";
|
||||
export const FeedbackResponseObjectType = "FeedbackResponseObjectType";
|
||||
export const Float = "Float";
|
||||
|
|
@ -28,13 +36,13 @@ export const JSONString = "JSONString";
|
|||
export const LearningContentAssignmentObjectType = "LearningContentAssignmentObjectType";
|
||||
export const LearningContentAttendanceCourseObjectType = "LearningContentAttendanceCourseObjectType";
|
||||
export const LearningContentDocumentListObjectType = "LearningContentDocumentListObjectType";
|
||||
export const LearningContentEdoniqTestObjectType = "LearningContentEdoniqTestObjectType";
|
||||
export const LearningContentFeedbackObjectType = "LearningContentFeedbackObjectType";
|
||||
export const LearningContentInterface = "LearningContentInterface";
|
||||
export const LearningContentLearningModuleObjectType = "LearningContentLearningModuleObjectType";
|
||||
export const LearningContentMediaLibraryObjectType = "LearningContentMediaLibraryObjectType";
|
||||
export const LearningContentPlaceholderObjectType = "LearningContentPlaceholderObjectType";
|
||||
export const LearningContentRichTextObjectType = "LearningContentRichTextObjectType";
|
||||
export const LearningContentTestObjectType = "LearningContentTestObjectType";
|
||||
export const LearningContentVideoObjectType = "LearningContentVideoObjectType";
|
||||
export const LearningPathObjectType = "LearningPathObjectType";
|
||||
export const LearningSequenceObjectType = "LearningSequenceObjectType";
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ export const graphqlClient = new Client({
|
|||
cacheExchange({
|
||||
schema: schema,
|
||||
keys: {
|
||||
AttendanceUserType: (data) => data?.user_id?.toString() ?? null,
|
||||
AttendanceUserObjectType: (data) => data?.user_id?.toString() ?? null,
|
||||
},
|
||||
updates: {
|
||||
Mutation: {
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ export const ASSIGNMENT_COMPLETION_QUERY = graphql(`
|
|||
`);
|
||||
|
||||
export const COURSE_QUERY = graphql(`
|
||||
query courseQuery($courseId: Int!) {
|
||||
query courseQuery($courseId: ID!) {
|
||||
course(id: $courseId) {
|
||||
id
|
||||
slug
|
||||
|
|
@ -122,3 +122,88 @@ export const COMPETENCE_NAVI_CERTIFICATE_QUERY = graphql(`
|
|||
}
|
||||
}
|
||||
`);
|
||||
|
||||
export const COURSE_SESSION_DETAIL_QUERY = graphql(`
|
||||
query courseSessionDetail($courseSessionId: ID!) {
|
||||
course_session(id: $courseSessionId) {
|
||||
id
|
||||
title
|
||||
course {
|
||||
id
|
||||
title
|
||||
slug
|
||||
}
|
||||
users {
|
||||
id
|
||||
user_id
|
||||
first_name
|
||||
last_name
|
||||
email
|
||||
avatar_url
|
||||
role
|
||||
circles {
|
||||
id
|
||||
title
|
||||
slug
|
||||
}
|
||||
}
|
||||
attendance_courses {
|
||||
id
|
||||
location
|
||||
trainer
|
||||
due_date {
|
||||
id
|
||||
start
|
||||
end
|
||||
}
|
||||
learning_content_id
|
||||
learning_content {
|
||||
id
|
||||
title
|
||||
circle {
|
||||
id
|
||||
title
|
||||
slug
|
||||
}
|
||||
}
|
||||
}
|
||||
assignments {
|
||||
id
|
||||
submission_deadline {
|
||||
id
|
||||
start
|
||||
}
|
||||
evaluation_deadline {
|
||||
id
|
||||
start
|
||||
}
|
||||
learning_content {
|
||||
id
|
||||
title
|
||||
content_assignment {
|
||||
id
|
||||
title
|
||||
assignment_type
|
||||
}
|
||||
}
|
||||
}
|
||||
edoniq_tests {
|
||||
id
|
||||
deadline {
|
||||
id
|
||||
start
|
||||
end
|
||||
}
|
||||
learning_content {
|
||||
id
|
||||
title
|
||||
content_assignment {
|
||||
id
|
||||
title
|
||||
assignment_type
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
|
|
|||
|
|
@ -9,12 +9,12 @@ import DueDatesList from "@/components/dueDates/DueDatesList.vue";
|
|||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const UNFILTERED = Number.MAX_SAFE_INTEGER;
|
||||
const UNFILTERED = Number.MAX_SAFE_INTEGER.toString();
|
||||
const courseSessionsStore = useCourseSessionsStore();
|
||||
const learningPathStore = useLearningPathStore();
|
||||
|
||||
type Item = {
|
||||
id: number;
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
|
|
@ -101,14 +101,14 @@ const appointments = computed(() => {
|
|||
|
||||
const isMatchingSession = (dueDate: DueDate) =>
|
||||
selectedSession.value.id === UNFILTERED ||
|
||||
dueDate.course_session === selectedSession.value.id;
|
||||
dueDate.course_session_id === selectedSession.value.id;
|
||||
|
||||
const isMatchingCircle = (dueDate: DueDate) =>
|
||||
selectedCircle.value.id === UNFILTERED ||
|
||||
dueDate.circle?.id === selectedCircle.value.id;
|
||||
|
||||
const isMatchingCourse = (dueDate: DueDate) =>
|
||||
courseSessions.value.map((cs) => cs.id).includes(dueDate.course_session as number);
|
||||
courseSessions.value.map((cs) => cs.id).includes(dueDate.course_session_id);
|
||||
|
||||
const numAppointmentsToShow = ref(7);
|
||||
const canLoadMore = computed(() => {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { useUserStore } from "@/stores/user";
|
|||
import type { CourseSession } from "@/types";
|
||||
import log from "loglevel";
|
||||
import { computed, onMounted } from "vue";
|
||||
import { getCockpitUrl, getLearningPathUrl } from "@/utils/utils";
|
||||
|
||||
log.debug("DashboardPage created");
|
||||
|
||||
|
|
@ -20,9 +21,9 @@ const allDueDates = courseSessionsStore.allDueDates();
|
|||
const getNextStepLink = (courseSession: CourseSession) => {
|
||||
return computed(() => {
|
||||
if (courseSessionsStore.hasCockpit(courseSession)) {
|
||||
return courseSession.cockpit_url;
|
||||
return getCockpitUrl(courseSession.course.slug);
|
||||
}
|
||||
return courseSession.learning_path_url;
|
||||
return getLearningPathUrl(courseSession.course.slug);
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import { useCurrentCourseSession } from "@/composables";
|
||||
import { useCourseSessionDetailQuery } from "@/composables";
|
||||
import { useCockpitStore } from "@/stores/cockpit";
|
||||
import { useCompetenceStore } from "@/stores/competence";
|
||||
import { useLearningPathStore } from "@/stores/learningPath";
|
||||
|
|
@ -16,13 +16,15 @@ const props = defineProps<{
|
|||
const cockpitStore = useCockpitStore();
|
||||
const competenceStore = useCompetenceStore();
|
||||
const learningPathStore = useLearningPathStore();
|
||||
const courseSession = useCurrentCourseSession();
|
||||
|
||||
const courseSessionDetailResult = useCourseSessionDetailQuery();
|
||||
|
||||
onMounted(async () => {
|
||||
log.debug("CockpitParentPage mounted", props.courseSlug);
|
||||
|
||||
try {
|
||||
const members = await cockpitStore.loadCourseSessionMembers(courseSession.value.id);
|
||||
await courseSessionDetailResult.waitForData();
|
||||
const members = courseSessionDetailResult.filterMembers();
|
||||
members.forEach((csu) => {
|
||||
competenceStore.loadCompetenceProfilePage(
|
||||
props.courseSlug + "-competencenavi-competences",
|
||||
|
|
@ -35,7 +37,10 @@ onMounted(async () => {
|
|||
props.courseSlug + "-lp",
|
||||
useUserStore().id
|
||||
);
|
||||
await cockpitStore.loadCircles(props.courseSlug, courseSession.value.id);
|
||||
await cockpitStore.loadCircles(
|
||||
props.courseSlug,
|
||||
courseSessionDetailResult.findCurrentUser()
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
<script setup lang="ts">
|
||||
import CirclePage from "@/pages/learningPath/circlePage/CirclePage.vue";
|
||||
import { useCockpitStore } from "@/stores/cockpit";
|
||||
import * as log from "loglevel";
|
||||
import { computed, onMounted } from "vue";
|
||||
import { useCourseSessionDetailQuery } from "@/composables";
|
||||
|
||||
const props = defineProps<{
|
||||
userId: string;
|
||||
|
|
@ -12,14 +12,14 @@ const props = defineProps<{
|
|||
|
||||
log.debug("CockpitUserCirclePage created", props.userId, props.circleSlug);
|
||||
|
||||
const cockpitStore = useCockpitStore();
|
||||
|
||||
onMounted(async () => {
|
||||
log.debug("CockpitUserCirclePage mounted");
|
||||
});
|
||||
|
||||
const { findUser } = useCourseSessionDetailQuery();
|
||||
|
||||
const user = computed(() => {
|
||||
return cockpitStore.courseSessionMembers?.find((csu) => csu.user_id === props.userId);
|
||||
return findUser(props.userId);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
<script setup lang="ts">
|
||||
import { useCockpitStore } from "@/stores/cockpit";
|
||||
import { useCompetenceStore } from "@/stores/competence";
|
||||
import { useLearningPathStore } from "@/stores/learningPath";
|
||||
import * as log from "loglevel";
|
||||
|
|
@ -7,6 +6,7 @@ import { computed, onMounted } from "vue";
|
|||
|
||||
import CompetenceDetail from "@/pages/competence/ActionCompetenceDetail.vue";
|
||||
import LearningPathPathView from "@/pages/learningPath/learningPathPage/LearningPathPathView.vue";
|
||||
import { useCourseSessionDetailQuery } from "@/composables";
|
||||
|
||||
const props = defineProps<{
|
||||
userId: string;
|
||||
|
|
@ -15,7 +15,6 @@ const props = defineProps<{
|
|||
|
||||
log.debug("CockpitUserProfilePage created", props.userId);
|
||||
|
||||
const cockpitStore = useCockpitStore();
|
||||
const competenceStore = useCompetenceStore();
|
||||
const learningPathStore = useLearningPathStore();
|
||||
|
||||
|
|
@ -27,8 +26,10 @@ const learningPath = computed(() => {
|
|||
return learningPathStore.learningPathForUser(props.courseSlug, props.userId);
|
||||
});
|
||||
|
||||
const { findUser } = useCourseSessionDetailQuery();
|
||||
|
||||
const user = computed(() => {
|
||||
return cockpitStore.courseSessionMembers?.find((csu) => csu.user_id === props.userId);
|
||||
return findUser(props.userId);
|
||||
});
|
||||
|
||||
function setActiveClasses(isActive: boolean) {
|
||||
|
|
|
|||
|
|
@ -1,20 +1,15 @@
|
|||
<script setup lang="ts">
|
||||
import { useCurrentCourseSession } from "@/composables";
|
||||
import { useCourseSessionDetailQuery, useCurrentCourseSession } from "@/composables";
|
||||
import { ASSIGNMENT_COMPLETION_QUERY } from "@/graphql/queries";
|
||||
import EvaluationContainer from "@/pages/cockpit/assignmentEvaluationPage/EvaluationContainer.vue";
|
||||
import AssignmentSubmissionResponses from "@/pages/learningPath/learningContentPage/assignment/AssignmentSubmissionResponses.vue";
|
||||
import type {
|
||||
Assignment,
|
||||
AssignmentCompletion,
|
||||
CourseSessionAssignment,
|
||||
CourseSessionUser,
|
||||
} from "@/types";
|
||||
import type { Assignment, AssignmentCompletion } from "@/types";
|
||||
import { useQuery } from "@urql/vue";
|
||||
import log from "loglevel";
|
||||
import { computed, onMounted, reactive } from "vue";
|
||||
import { computed, onMounted } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { getPreviousRoute } from "@/router/history";
|
||||
import { getAssignmentTypeTitle } from "@/utils/utils";
|
||||
import { getAssignmentTypeTitle } from "../../../utils/utils";
|
||||
|
||||
const props = defineProps<{
|
||||
courseSlug: string;
|
||||
|
|
@ -24,16 +19,6 @@ const props = defineProps<{
|
|||
|
||||
log.debug("AssignmentEvaluationPage created", props.assignmentId, props.userId);
|
||||
|
||||
interface StateInterface {
|
||||
courseSessionAssignment: CourseSessionAssignment | undefined;
|
||||
assignmentUser: CourseSessionUser | undefined;
|
||||
}
|
||||
|
||||
const state: StateInterface = reactive({
|
||||
courseSessionAssignment: undefined,
|
||||
assignmentUser: undefined,
|
||||
});
|
||||
|
||||
const courseSession = useCurrentCourseSession();
|
||||
const router = useRouter();
|
||||
|
||||
|
|
@ -41,7 +26,7 @@ const router = useRouter();
|
|||
const queryResult = useQuery({
|
||||
query: ASSIGNMENT_COMPLETION_QUERY,
|
||||
variables: {
|
||||
courseSessionId: courseSession.value.id.toString(),
|
||||
courseSessionId: courseSession.value.id,
|
||||
assignmentId: props.assignmentId,
|
||||
assignmentUserId: props.userId,
|
||||
},
|
||||
|
|
@ -49,12 +34,11 @@ const queryResult = useQuery({
|
|||
|
||||
onMounted(async () => {
|
||||
log.debug("AssignmentView mounted", props.assignmentId, props.userId);
|
||||
|
||||
state.assignmentUser = courseSession.value.users.find(
|
||||
(user) => user.user_id === props.userId
|
||||
);
|
||||
});
|
||||
|
||||
const courseSessionDetailResult = useCourseSessionDetailQuery();
|
||||
const assignmentUser = computed(() => courseSessionDetailResult.findUser(props.userId));
|
||||
|
||||
const previousRoute = getPreviousRoute();
|
||||
|
||||
function close() {
|
||||
|
|
@ -103,10 +87,7 @@ const assignment = computed(
|
|||
<it-icon-close></it-icon-close>
|
||||
</button>
|
||||
</header>
|
||||
<div
|
||||
v-if="assignment && assignmentCompletion && state.assignmentUser"
|
||||
class="relative"
|
||||
>
|
||||
<div v-if="assignment && assignmentCompletion && assignmentUser" class="relative">
|
||||
<div class="md:h-content flex flex-col md:flex-row">
|
||||
<div
|
||||
class="bg-white md:h-full md:overflow-y-auto"
|
||||
|
|
@ -118,12 +99,12 @@ const assignment = computed(
|
|||
|
||||
<div class="my-6 flex items-center">
|
||||
<img
|
||||
:src="state.assignmentUser?.avatar_url"
|
||||
:src="assignmentUser?.avatar_url"
|
||||
class="mr-4 h-11 w-11 rounded-full"
|
||||
/>
|
||||
<div class="font-bold">
|
||||
{{ state.assignmentUser?.first_name }}
|
||||
{{ state.assignmentUser?.last_name }}
|
||||
{{ assignmentUser?.first_name }}
|
||||
{{ assignmentUser?.last_name }}
|
||||
</div>
|
||||
</div>
|
||||
<AssignmentSubmissionResponses
|
||||
|
|
@ -139,7 +120,7 @@ const assignment = computed(
|
|||
>
|
||||
<EvaluationContainer
|
||||
:assignment-completion="assignmentCompletion"
|
||||
:assignment-user="state.assignmentUser"
|
||||
:assignment-user="assignmentUser"
|
||||
:assignment="assignment"
|
||||
@close="close()"
|
||||
></EvaluationContainer>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
import EvaluationIntro from "@/pages/cockpit/assignmentEvaluationPage/EvaluationIntro.vue";
|
||||
import EvaluationSummary from "@/pages/cockpit/assignmentEvaluationPage/EvaluationSummary.vue";
|
||||
import EvaluationTask from "@/pages/cockpit/assignmentEvaluationPage/EvaluationTask.vue";
|
||||
import { findAssignmentDetail } from "@/services/assignmentService";
|
||||
import type {
|
||||
Assignment,
|
||||
AssignmentCompletion,
|
||||
|
|
@ -14,6 +13,7 @@ import dayjs from "dayjs";
|
|||
import { findIndex } from "lodash";
|
||||
import * as log from "loglevel";
|
||||
import { computed, onMounted } from "vue";
|
||||
import { useCourseSessionDetailQuery } from "@/composables";
|
||||
|
||||
const props = defineProps<{
|
||||
assignmentUser: CourseSessionUser;
|
||||
|
|
@ -58,10 +58,14 @@ function editTask(task: AssignmentEvaluationTask) {
|
|||
stepIndex.value = taskIndex + 1;
|
||||
}
|
||||
|
||||
const assignmentDetail = computed(() => findAssignmentDetail(props.assignment.id));
|
||||
const courseSessionDetailResult = useCourseSessionDetailQuery();
|
||||
|
||||
const assignmentDetail = computed(() => {
|
||||
return courseSessionDetailResult.findAssignmentByAssignmentId(props.assignment.id);
|
||||
});
|
||||
|
||||
const dueDate = computed(() =>
|
||||
dayjs(assignmentDetail.value?.evaluation_deadline_start)
|
||||
dayjs(assignmentDetail.value?.evaluation_deadline.start)
|
||||
);
|
||||
|
||||
const inEvaluationTask = computed(
|
||||
|
|
|
|||
|
|
@ -27,9 +27,9 @@ async function startEvaluation() {
|
|||
log.debug("startEvaluation");
|
||||
if (props.assignmentCompletion.completion_status !== "EVALUATION_SUBMITTED") {
|
||||
upsertAssignmentCompletionMutation.executeMutation({
|
||||
assignmentId: props.assignment.id.toString(),
|
||||
courseSessionId: courseSession.value.id.toString(),
|
||||
assignmentUserId: props.assignmentUser.user_id.toString(),
|
||||
assignmentId: props.assignment.id,
|
||||
courseSessionId: courseSession.value.id,
|
||||
assignmentUserId: props.assignmentUser.user_id,
|
||||
completionStatus: "EVALUATION_IN_PROGRESS",
|
||||
completionDataString: JSON.stringify({}),
|
||||
// next line used for urql
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import ItSuccessAlert from "@/components/ui/ItSuccessAlert.vue";
|
||||
import { useCurrentCourseSession } from "@/composables";
|
||||
import { useCourseSessionDetailQuery, useCurrentCourseSession } from "@/composables";
|
||||
import { UPSERT_ASSIGNMENT_COMPLETION_MUTATION } from "@/graphql/mutations";
|
||||
import {
|
||||
maxAssignmentPoints,
|
||||
|
|
@ -41,9 +41,9 @@ const upsertAssignmentCompletionMutation = useMutation(
|
|||
|
||||
async function submitEvaluation() {
|
||||
upsertAssignmentCompletionMutation.executeMutation({
|
||||
assignmentId: props.assignment.id.toString(),
|
||||
courseSessionId: courseSession.value.id.toString(),
|
||||
assignmentUserId: props.assignmentUser.user_id.toString(),
|
||||
assignmentId: props.assignment.id,
|
||||
courseSessionId: courseSession.value.id,
|
||||
assignmentUserId: props.assignmentUser.user_id,
|
||||
completionStatus: "EVALUATION_SUBMITTED",
|
||||
completionDataString: JSON.stringify({}),
|
||||
evaluationPoints: userPoints.value,
|
||||
|
|
@ -77,13 +77,13 @@ const userPoints = computed(() =>
|
|||
userAssignmentPoints(props.assignment, props.assignmentCompletion)
|
||||
);
|
||||
|
||||
const courseSessionDetailResult = useCourseSessionDetailQuery();
|
||||
const evaluationUser = computed(() => {
|
||||
if (props.assignmentCompletion.evaluation_user) {
|
||||
return (courseSession.value.users ?? []).find(
|
||||
(user) => user.user_id === props.assignmentCompletion.evaluation_user
|
||||
) as CourseSessionUser;
|
||||
return courseSessionDetailResult.findUser(
|
||||
props.assignmentCompletion.evaluation_user
|
||||
);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -66,9 +66,9 @@ async function evaluateAssignmentCompletion(completionData: AssignmentCompletion
|
|||
log.debug("evaluateAssignmentCompletion", completionData);
|
||||
|
||||
upsertAssignmentCompletionMutation.executeMutation({
|
||||
assignmentId: props.assignment.id.toString(),
|
||||
courseSessionId: courseSession.value.id.toString(),
|
||||
assignmentUserId: props.assignmentUser.user_id.toString(),
|
||||
assignmentId: props.assignment.id,
|
||||
courseSessionId: courseSession.value.id,
|
||||
assignmentUserId: props.assignmentUser.user_id,
|
||||
completionStatus: "EVALUATION_IN_PROGRESS",
|
||||
completionDataString: JSON.stringify(completionData),
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
|
|
|
|||
|
|
@ -2,20 +2,17 @@
|
|||
import ItPersonRow from "@/components/ui/ItPersonRow.vue";
|
||||
import type { StatusCount } from "@/components/ui/ItProgress.vue";
|
||||
import type { GradedUser } from "@/services/assignmentService";
|
||||
import {
|
||||
findAssignmentDetail,
|
||||
loadAssignmentCompletionStatusData,
|
||||
} from "@/services/assignmentService";
|
||||
import { useCockpitStore } from "@/stores/cockpit";
|
||||
import { loadAssignmentCompletionStatusData } from "@/services/assignmentService";
|
||||
import type {
|
||||
CourseSession,
|
||||
CourseSessionUser,
|
||||
LearningContentAssignment,
|
||||
} from "@/types";
|
||||
import dayjs from "dayjs";
|
||||
import log from "loglevel";
|
||||
import { computed, onMounted, reactive } from "vue";
|
||||
import AssignmentSubmissionProgress from "@/pages/cockpit/cockpitPage/AssignmentSubmissionProgress.vue";
|
||||
import { useCourseSessionDetailQuery } from "@/composables";
|
||||
import { formatDueDate } from "../../../components/dueDates/dueDatesUtils";
|
||||
|
||||
const props = defineProps<{
|
||||
courseSession: CourseSession;
|
||||
|
|
@ -27,7 +24,7 @@ log.debug(
|
|||
props.learningContentAssignment.content_assignment_id
|
||||
);
|
||||
|
||||
const cockpitStore = useCockpitStore();
|
||||
const courseSessionDetailResult = useCourseSessionDetailQuery();
|
||||
|
||||
const state = reactive({
|
||||
progressStatusCount: {} as StatusCount,
|
||||
|
|
@ -35,6 +32,10 @@ const state = reactive({
|
|||
assignmentSubmittedUsers: [] as CourseSessionUser[],
|
||||
});
|
||||
|
||||
const assignmentDetail = computed(() => {
|
||||
return courseSessionDetailResult.findAssignment(props.learningContentAssignment.id);
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
const { gradedUsers, assignmentSubmittedUsers } =
|
||||
await loadAssignmentCompletionStatusData(
|
||||
|
|
@ -45,10 +46,6 @@ onMounted(async () => {
|
|||
state.gradedUsers = gradedUsers;
|
||||
state.assignmentSubmittedUsers = assignmentSubmittedUsers;
|
||||
});
|
||||
|
||||
const assignmentDetail = computed(() =>
|
||||
findAssignmentDetail(props.learningContentAssignment.content_assignment_id)
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -60,16 +57,14 @@ const assignmentDetail = computed(() =>
|
|||
Circle «{{ learningContentAssignment.parentCircle.title }}»
|
||||
</div>
|
||||
<div v-if="assignmentDetail">
|
||||
<span>
|
||||
<span v-if="assignmentDetail.submission_deadline?.start">
|
||||
{{ $t("Abgabetermin Ergebnisse:") }}
|
||||
{{ dayjs(assignmentDetail.submission_deadline_start).format("DD.MM.YYYY") }}
|
||||
{{ formatDueDate(assignmentDetail.submission_deadline.start) }}
|
||||
</span>
|
||||
<template v-if="assignmentDetail.evaluation_deadline_start">
|
||||
<template v-if="assignmentDetail.evaluation_deadline?.start">
|
||||
<br />
|
||||
<span v-if="assignmentDetail.evaluation_deadline_start">
|
||||
{{ $t("Freigabetermin Bewertungen:") }}
|
||||
{{ dayjs(assignmentDetail.evaluation_deadline_start).format("DD.MM.YYYY") }}
|
||||
</span>
|
||||
{{ $t("Freigabetermin Bewertungen:") }}
|
||||
{{ formatDueDate(assignmentDetail.evaluation_deadline.start) }}
|
||||
</template>
|
||||
</div>
|
||||
<div v-else>
|
||||
|
|
@ -85,11 +80,11 @@ const assignmentDetail = computed(() =>
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="cockpitStore.courseSessionMembers?.length" class="mt-6">
|
||||
<div v-if="courseSessionDetailResult.filterMembers().length" class="mt-6">
|
||||
<ul>
|
||||
<ItPersonRow
|
||||
v-for="csu in cockpitStore.courseSessionMembers"
|
||||
:key="csu.user_id + csu.session_title"
|
||||
v-for="csu in courseSessionDetailResult.filterMembers()"
|
||||
:key="csu.user_id"
|
||||
:name="`${csu.first_name} ${csu.last_name}`"
|
||||
:avatar-url="csu.avatar_url"
|
||||
:data-cy="csu.last_name"
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ onMounted(async () => {
|
|||
const learningContentAssignment = computed(() => {
|
||||
return calcLearningContentAssignments(
|
||||
learningPathStore.learningPathForUser(courseSession.value.course.slug, userStore.id)
|
||||
).filter((lc) => lc.id.toString() === props.assignmentId)[0];
|
||||
).filter((lc) => lc.id === props.assignmentId)[0];
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -2,10 +2,9 @@
|
|||
import ItCheckbox from "@/components/ui/ItCheckbox.vue";
|
||||
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
|
||||
import ItPersonRow from "@/components/ui/ItPersonRow.vue";
|
||||
import { useCurrentCourseSession } from "@/composables";
|
||||
import { useCourseSessionDetailQuery } from "@/composables";
|
||||
import type { AttendanceUserStatus } from "@/gql/graphql";
|
||||
import { ATTENDANCE_CHECK_MUTATION } from "@/graphql/mutations";
|
||||
import { useCockpitStore } from "@/stores/cockpit";
|
||||
import type { DropdownSelectable } from "@/types";
|
||||
import { useMutation } from "@urql/vue";
|
||||
import dayjs from "dayjs";
|
||||
|
|
@ -16,12 +15,15 @@ import { ATTENDANCE_CHECK_QUERY } from "@/graphql/queries";
|
|||
import { graphqlClient } from "@/graphql/client";
|
||||
|
||||
const { t } = useTranslation();
|
||||
const cockpitStore = useCockpitStore();
|
||||
const courseSession = useCurrentCourseSession();
|
||||
const attendanceMutation = useMutation(ATTENDANCE_CHECK_MUTATION);
|
||||
const courseSessionDetailResult = useCourseSessionDetailQuery();
|
||||
|
||||
const attendanceCourses = computed(() => {
|
||||
return courseSession.value.attendance_courses;
|
||||
return courseSessionDetailResult.courseSessionDetail.value?.attendance_courses ?? [];
|
||||
});
|
||||
|
||||
const courseSessionDetail = computed(() => {
|
||||
return courseSessionDetailResult.courseSessionDetail.value;
|
||||
});
|
||||
|
||||
const presenceCoursesDropdownOptions = computed(() => {
|
||||
|
|
@ -29,9 +31,9 @@ const presenceCoursesDropdownOptions = computed(() => {
|
|||
(attendanceCourse) =>
|
||||
({
|
||||
id: attendanceCourse.id,
|
||||
name: `${t("Präsenzkurs")} ${attendanceCourse.circle_title} ${dayjs(
|
||||
attendanceCourse.start
|
||||
).format("DD.MM.YYYY")}`,
|
||||
name: `${t("Präsenzkurs")} ${
|
||||
attendanceCourse.learning_content.circle.title
|
||||
} ${dayjs(attendanceCourse.due_date.start).format("DD.MM.YYYY")}`,
|
||||
} as DropdownSelectable)
|
||||
);
|
||||
});
|
||||
|
|
@ -43,6 +45,16 @@ const state = reactive({
|
|||
attendanceSaved: false,
|
||||
});
|
||||
|
||||
watch(
|
||||
attendanceCourses,
|
||||
(newVal) => {
|
||||
if (newVal && newVal.length > 0) {
|
||||
state.attendanceCourseSelected = presenceCoursesDropdownOptions.value[0];
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
function resetState() {
|
||||
state.userPresence = new Map<string, boolean>();
|
||||
state.disclaimerConfirmed = false;
|
||||
|
|
@ -76,23 +88,25 @@ const onSubmit = async () => {
|
|||
const loadAttendanceData = async () => {
|
||||
resetState();
|
||||
// with changing variables `useQuery` does not seem to work correctly
|
||||
const res = await graphqlClient.query(
|
||||
ATTENDANCE_CHECK_QUERY,
|
||||
{
|
||||
courseSessionId: state.attendanceCourseSelected.id.toString(),
|
||||
},
|
||||
{
|
||||
requestPolicy: "network-only",
|
||||
if (state.attendanceCourseSelected) {
|
||||
const res = await graphqlClient.query(
|
||||
ATTENDANCE_CHECK_QUERY,
|
||||
{
|
||||
courseSessionId: state.attendanceCourseSelected.id.toString(),
|
||||
},
|
||||
{
|
||||
requestPolicy: "network-only",
|
||||
}
|
||||
);
|
||||
const attendanceUserList =
|
||||
res.data?.course_session_attendance_course?.attendance_user_list ?? [];
|
||||
for (const user of attendanceUserList) {
|
||||
if (!user) continue;
|
||||
state.userPresence.set(user.user_id, user.status === "PRESENT");
|
||||
}
|
||||
if (attendanceUserList.length !== 0) {
|
||||
state.attendanceSaved = true;
|
||||
}
|
||||
);
|
||||
const attendanceUserList =
|
||||
res.data?.course_session_attendance_course?.attendance_user_list ?? [];
|
||||
for (const user of attendanceUserList) {
|
||||
if (!user) continue;
|
||||
state.userPresence.set(user.user_id.toString(), user.status === "PRESENT");
|
||||
}
|
||||
if (attendanceUserList.length !== 0) {
|
||||
state.attendanceSaved = true;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -110,89 +124,93 @@ watch(
|
|||
() => {
|
||||
log.debug("attendanceCourseSelected changed", state.attendanceCourseSelected);
|
||||
loadAttendanceData();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-gray-200">
|
||||
<div v-if="courseSession" class="container-large">
|
||||
<div v-if="courseSessionDetail" class="container-large">
|
||||
<nav class="py-4 pb-4">
|
||||
<router-link
|
||||
class="btn-text inline-flex items-center pl-0"
|
||||
:to="`/course/${courseSession.course.slug}/cockpit`"
|
||||
:to="`/course/${courseSessionDetail.course.slug}/cockpit`"
|
||||
>
|
||||
<it-icon-arrow-left />
|
||||
<span>{{ $t("general.back") }}</span>
|
||||
</router-link>
|
||||
</nav>
|
||||
<div class="pb-4 text-xl font-bold">{{ $t("Anwesenheit Präsenzkurse") }}</div>
|
||||
<div class="flex flex-row justify-between bg-white p-6">
|
||||
<ItDropdownSelect
|
||||
v-model="state.attendanceCourseSelected"
|
||||
:items="presenceCoursesDropdownOptions ?? []"
|
||||
></ItDropdownSelect>
|
||||
<div v-if="!state.attendanceSaved" class="flex flex-row items-center">
|
||||
<ItCheckbox
|
||||
:checkbox-item="{
|
||||
value: true,
|
||||
checked: state.disclaimerConfirmed,
|
||||
}"
|
||||
@toggle="state.disclaimerConfirmed = !state.disclaimerConfirmed"
|
||||
></ItCheckbox>
|
||||
<p class="w-64 pr-4 text-sm">
|
||||
{{
|
||||
$t(
|
||||
"Ich will die Anwesenheit der untenstehenden Personen definitiv bestätigen."
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<button
|
||||
class="btn-primary"
|
||||
:disabled="!state.disclaimerConfirmed"
|
||||
@click="onSubmit"
|
||||
>
|
||||
{{ $t("Anwesenheit bestätigen") }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-else class="self-center">
|
||||
<p class="text-base">
|
||||
{{ $t("a.Die Anwesenheit wurde definitiv bestätigt") }}
|
||||
</p>
|
||||
<button class="btn-link link" @click="editAgain()">
|
||||
{{ $t("a.Erneut bearbeiten") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex flex-col bg-white p-6">
|
||||
<div
|
||||
v-for="(csu, index) in cockpitStore.courseSessionMembers"
|
||||
:key="csu.user_id + csu.session_title"
|
||||
>
|
||||
<ItPersonRow
|
||||
:name="`${csu.first_name} ${csu.last_name}`"
|
||||
:avatar-url="csu.avatar_url"
|
||||
:class="0 === index ? 'border-none' : ''"
|
||||
>
|
||||
<template #leading>
|
||||
<ItCheckbox
|
||||
:disabled="state.attendanceSaved"
|
||||
:checkbox-item="{
|
||||
value: true,
|
||||
checked: state.userPresence.get(csu.user_id.toString()) as boolean,
|
||||
}"
|
||||
@toggle="
|
||||
state.userPresence.set(
|
||||
csu.user_id.toString(),
|
||||
!state.userPresence.get(csu.user_id.toString())
|
||||
)
|
||||
"
|
||||
></ItCheckbox>
|
||||
</template>
|
||||
</ItPersonRow>
|
||||
<section v-if="attendanceCourses.length && state.attendanceCourseSelected">
|
||||
<div class="flex flex-row justify-between bg-white p-6">
|
||||
<ItDropdownSelect
|
||||
v-model="state.attendanceCourseSelected"
|
||||
:items="presenceCoursesDropdownOptions ?? []"
|
||||
></ItDropdownSelect>
|
||||
<div v-if="!state.attendanceSaved" class="flex flex-row items-center">
|
||||
<ItCheckbox
|
||||
:checkbox-item="{
|
||||
value: true,
|
||||
checked: state.disclaimerConfirmed,
|
||||
}"
|
||||
@toggle="state.disclaimerConfirmed = !state.disclaimerConfirmed"
|
||||
></ItCheckbox>
|
||||
<p class="w-64 pr-4 text-sm">
|
||||
{{
|
||||
$t(
|
||||
"Ich will die Anwesenheit der untenstehenden Personen definitiv bestätigen."
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<button
|
||||
class="btn-primary"
|
||||
:disabled="!state.disclaimerConfirmed"
|
||||
@click="onSubmit"
|
||||
>
|
||||
{{ $t("Anwesenheit bestätigen") }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-else class="self-center">
|
||||
<p class="text-base">
|
||||
{{ $t("a.Die Anwesenheit wurde definitiv bestätigt") }}
|
||||
</p>
|
||||
<button class="btn-link link" @click="editAgain()">
|
||||
{{ $t("a.Erneut bearbeiten") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex flex-col bg-white p-6">
|
||||
<div
|
||||
v-for="(csu, index) in courseSessionDetailResult.filterMembers()"
|
||||
:key="csu.user_id"
|
||||
>
|
||||
<ItPersonRow
|
||||
:name="`${csu.first_name} ${csu.last_name}`"
|
||||
:avatar-url="csu.avatar_url"
|
||||
:class="0 === index ? 'border-none' : ''"
|
||||
>
|
||||
<template #leading>
|
||||
<ItCheckbox
|
||||
:disabled="state.attendanceSaved"
|
||||
:checkbox-item="{
|
||||
value: true,
|
||||
checked: state.userPresence.get(csu.user_id) as boolean,
|
||||
}"
|
||||
@toggle="
|
||||
state.userPresence.set(
|
||||
csu.user_id,
|
||||
!state.userPresence.get(csu.user_id)
|
||||
)
|
||||
"
|
||||
></ItCheckbox>
|
||||
</template>
|
||||
</ItPersonRow>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ const state = reactive({
|
|||
statusByUser: [] as {
|
||||
userStatus: AssignmentCompletionStatus;
|
||||
progressStatus: StatusCountKey;
|
||||
userId: number;
|
||||
userId: string;
|
||||
}[],
|
||||
submissionProgressStatusCount: {} as StatusCount,
|
||||
gradingProgressStatusCount: {} as StatusCount,
|
||||
|
|
|
|||
|
|
@ -10,9 +10,7 @@ const courseSession = useCurrentCourseSession();
|
|||
const circleDates = computed(() => {
|
||||
const dueDates = courseSession.value.due_dates.filter((dueDate) => {
|
||||
if (!cockpitStore.currentCircle) return false;
|
||||
return (
|
||||
cockpitStore.currentCircle.translation_key == dueDate?.circle?.translation_key
|
||||
);
|
||||
return cockpitStore.currentCircle.id == dueDate?.circle?.id;
|
||||
});
|
||||
return dueDates.slice(0, 4);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,14 +3,13 @@ import LearningPathDiagram from "@/components/learningPath/LearningPathDiagram.v
|
|||
import ItPersonRow from "@/components/ui/ItPersonRow.vue";
|
||||
import type { LearningPath } from "@/services/learningPath";
|
||||
|
||||
import { useCurrentCourseSession } from "@/composables";
|
||||
import { useCourseSessionDetailQuery, useCurrentCourseSession } from "@/composables";
|
||||
import SubmissionsOverview from "@/pages/cockpit/cockpitPage/SubmissionsOverview.vue";
|
||||
import { useCockpitStore } from "@/stores/cockpit";
|
||||
import { useCompetenceStore } from "@/stores/competence";
|
||||
import { useLearningPathStore } from "@/stores/learningPath";
|
||||
import log from "loglevel";
|
||||
import CockpitDates from "@/pages/cockpit/cockpitPage/CockpitDates.vue";
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
|
|
@ -23,7 +22,7 @@ const cockpitStore = useCockpitStore();
|
|||
const competenceStore = useCompetenceStore();
|
||||
const learningPathStore = useLearningPathStore();
|
||||
const courseSession = useCurrentCourseSession();
|
||||
const courseSessionsStore = useCourseSessionsStore();
|
||||
const courseSessionDetailResult = useCourseSessionDetailQuery();
|
||||
|
||||
function userCountStatusForCircle(userId: string) {
|
||||
if (!cockpitStore.currentCircle) return { FAIL: 0, SUCCESS: 0, UNKNOWN: 0 };
|
||||
|
|
@ -37,175 +36,178 @@ function userCountStatusForCircle(userId: string) {
|
|||
|
||||
<template>
|
||||
<div class="bg-gray-200">
|
||||
<div v-if="cockpitStore.currentCircle" class="container-large">
|
||||
<div class="mb-9 flex flex-col lg:flex-row lg:items-center lg:justify-between">
|
||||
<h1>Cockpit</h1>
|
||||
<ItDropdownSelect
|
||||
:model-value="cockpitStore.selectedCircle"
|
||||
class="mt-4 w-full lg:mt-0 lg:w-96"
|
||||
:items="cockpitStore.circles"
|
||||
@update:model-value="cockpitStore.setCurrentCourseCircleFromEvent"
|
||||
></ItDropdownSelect>
|
||||
</div>
|
||||
<!-- Status -->
|
||||
<div class="mb-4 gap-4 lg:grid lg:grid-cols-3 lg:grid-rows-none">
|
||||
<div class="my-4 flex flex-col justify-between bg-white p-6 lg:my-0">
|
||||
<div>
|
||||
<h3 class="heading-3 mb-4 flex items-center gap-2">
|
||||
{{ $t("Trainerunterlagen") }}
|
||||
</h3>
|
||||
<div class="mb-4">
|
||||
{{ $t("cockpit.trainerFilesText") }}
|
||||
<div v-if="cockpitStore.circles?.length">
|
||||
<div v-if="cockpitStore.currentCircle" class="container-large">
|
||||
<div class="mb-9 flex flex-col lg:flex-row lg:items-center lg:justify-between">
|
||||
<h1>Cockpit</h1>
|
||||
<ItDropdownSelect
|
||||
:model-value="cockpitStore.currentCircle"
|
||||
class="mt-4 w-full lg:mt-0 lg:w-96"
|
||||
:items="cockpitStore.circles"
|
||||
@update:model-value="cockpitStore.setCurrentCourseCircleFromEvent"
|
||||
></ItDropdownSelect>
|
||||
</div>
|
||||
<!-- Status -->
|
||||
<div class="mb-4 gap-4 lg:grid lg:grid-cols-3 lg:grid-rows-none">
|
||||
<div class="my-4 flex flex-col justify-between bg-white p-6 lg:my-0">
|
||||
<div>
|
||||
<h3 class="heading-3 mb-4 flex items-center gap-2">
|
||||
{{ $t("Trainerunterlagen") }}
|
||||
</h3>
|
||||
<div class="mb-4">
|
||||
{{ $t("cockpit.trainerFilesText") }}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href="https://vbvbern.sharepoint.com/sites/myVBV-AFA_K-CI"
|
||||
class="btn-secondary min-w-min"
|
||||
target="_blank"
|
||||
>
|
||||
{{ $t("MS Teams öffnen") }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href="https://vbvbern.sharepoint.com/sites/myVBV-AFA_K-CI"
|
||||
class="btn-secondary min-w-min"
|
||||
target="_blank"
|
||||
>
|
||||
{{ $t("MS Teams öffnen") }}
|
||||
</a>
|
||||
<div class="my-4 flex flex-col justify-between bg-white p-6 lg:my-0">
|
||||
<div>
|
||||
<h3 class="heading-3 mb-4 flex items-center gap-2">
|
||||
{{ $t("a.Unterlagen für Teilnehmenden") }}
|
||||
</h3>
|
||||
<div class="mb-4">
|
||||
{{ $t("a.Stelle deinen Lernenden zusätzliche Inhalte zur Verfügung.") }}
|
||||
</div>
|
||||
<!-- <div-->
|
||||
<!-- v-if="courseSessionsStore.circleDocuments.length"-->
|
||||
<!-- class="mb-4 flex items-center gap-x-2"-->
|
||||
<!-- >-->
|
||||
<!-- <it-icon-document />-->
|
||||
<!-- {{ courseSessionsStore.circleDocuments.length }} {{ $t("a.Unterlagen") }}-->
|
||||
<!-- </div>-->
|
||||
</div>
|
||||
<div>
|
||||
<router-link
|
||||
:to="`/course/${props.courseSlug}/cockpit/documents`"
|
||||
class="btn-secondary min-w-min"
|
||||
>
|
||||
{{ $t("a.Zum Unterlagen-Upload") }}
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<div class="my-4 flex flex-col justify-between bg-white p-6 lg:my-0">
|
||||
<div>
|
||||
<h3 class="heading-3 mb-4 flex items-center gap-2">
|
||||
{{ $t("Anwesenheitskontrolle Präsenzkurse") }}
|
||||
</h3>
|
||||
<div class="mb-4">
|
||||
{{
|
||||
$t(
|
||||
"Hier überprüfst und bestätigst du die Anwesenheit deiner Teilnehmenden."
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<router-link
|
||||
:to="`/course/${props.courseSlug}/cockpit/attendance`"
|
||||
class="btn-secondary min-w-min"
|
||||
>
|
||||
{{ $t("Anwesenheit prüfen") }}
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="my-4 flex flex-col justify-between bg-white p-6 lg:my-0">
|
||||
<div>
|
||||
<h3 class="heading-3 mb-4 flex items-center gap-2">
|
||||
{{ $t("a.Unterlagen für Teilnehmenden") }}
|
||||
</h3>
|
||||
<div class="mb-4">
|
||||
{{ $t("a.Stelle deinen Lernenden zusätzliche Inhalte zur Verfügung.") }}
|
||||
</div>
|
||||
<div
|
||||
v-if="courseSessionsStore.circleDocuments.length"
|
||||
class="mb-4 flex items-center gap-x-2"
|
||||
>
|
||||
<it-icon-document />
|
||||
{{ courseSessionsStore.circleDocuments.length }} {{ $t("a.Unterlagen") }}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<router-link
|
||||
:to="`/course/${props.courseSlug}/cockpit/documents`"
|
||||
class="btn-secondary min-w-min"
|
||||
>
|
||||
{{ $t("a.Zum Unterlagen-Upload") }}
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<div class="my-4 flex flex-col justify-between bg-white p-6 lg:my-0">
|
||||
<div>
|
||||
<h3 class="heading-3 mb-4 flex items-center gap-2">
|
||||
{{ $t("Anwesenheitskontrolle Präsenzkurse") }}
|
||||
</h3>
|
||||
<div class="mb-4">
|
||||
{{
|
||||
$t(
|
||||
"Hier überprüfst und bestätigst du die Anwesenheit deiner Teilnehmenden."
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<router-link
|
||||
:to="`/course/${props.courseSlug}/cockpit/attendance`"
|
||||
class="btn-secondary min-w-min"
|
||||
>
|
||||
{{ $t("Anwesenheit prüfen") }}
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4 bg-white p-6">
|
||||
<CockpitDates></CockpitDates>
|
||||
</div>
|
||||
<SubmissionsOverview
|
||||
:course-session="courseSession"
|
||||
:selected-circle="cockpitStore.currentCircle.id"
|
||||
></SubmissionsOverview>
|
||||
<div class="pt-4">
|
||||
<!-- progress -->
|
||||
<div v-if="cockpitStore.courseSessionMembers" class="bg-white p-6">
|
||||
<h1 class="heading-3 mb-5">{{ $t("cockpit.progress") }}</h1>
|
||||
<ul>
|
||||
<ItPersonRow
|
||||
v-for="csu in cockpitStore.courseSessionMembers"
|
||||
:key="csu.user_id + csu.session_title"
|
||||
:name="`${csu.first_name} ${csu.last_name}`"
|
||||
:avatar-url="csu.avatar_url"
|
||||
>
|
||||
<template #center>
|
||||
<div
|
||||
class="mt-2 flex w-full flex-col items-center justify-between lg:mt-0 lg:flex-row"
|
||||
>
|
||||
<LearningPathDiagram
|
||||
v-if="
|
||||
learningPathStore.learningPathForUser(
|
||||
props.courseSlug,
|
||||
csu.user_id
|
||||
)
|
||||
"
|
||||
:learning-path="
|
||||
<div class="mb-4 bg-white p-6">
|
||||
<CockpitDates></CockpitDates>
|
||||
</div>
|
||||
<SubmissionsOverview
|
||||
:course-session="courseSession"
|
||||
:selected-circle="cockpitStore.currentCircle.id"
|
||||
></SubmissionsOverview>
|
||||
<div class="pt-4">
|
||||
<!-- progress -->
|
||||
<div
|
||||
v-if="courseSessionDetailResult.filterMembers().length > 0"
|
||||
class="bg-white p-6"
|
||||
>
|
||||
<h1 class="heading-3 mb-5">{{ $t("cockpit.progress") }}</h1>
|
||||
<ul>
|
||||
<ItPersonRow
|
||||
v-for="csu in courseSessionDetailResult.filterMembers()"
|
||||
:key="csu.user_id"
|
||||
:name="`${csu.first_name} ${csu.last_name}`"
|
||||
:avatar-url="csu.avatar_url"
|
||||
>
|
||||
<template #center>
|
||||
<div
|
||||
class="mt-2 flex w-full flex-col items-center justify-between lg:mt-0 lg:flex-row"
|
||||
>
|
||||
<LearningPathDiagram
|
||||
v-if="
|
||||
learningPathStore.learningPathForUser(
|
||||
props.courseSlug,
|
||||
csu.user_id
|
||||
)
|
||||
"
|
||||
:learning-path="
|
||||
learningPathStore.learningPathForUser(
|
||||
props.courseSlug,
|
||||
csu.user_id
|
||||
) as LearningPath
|
||||
"
|
||||
:show-circle-translation-keys="[
|
||||
cockpitStore.currentCircle.translation_key,
|
||||
]"
|
||||
diagram-type="singleSmall"
|
||||
class="mr-4"
|
||||
></LearningPathDiagram>
|
||||
<p class="lg:min-w-[150px]">
|
||||
{{ cockpitStore.currentCircle.title }}
|
||||
</p>
|
||||
<div class="ml-4 flex flex-row items-center">
|
||||
<div class="mr-6 flex flex-row items-center">
|
||||
<it-icon-smiley-thinking
|
||||
class="mr-2 inline-block h-8 w-8"
|
||||
></it-icon-smiley-thinking>
|
||||
<p class="text-bold inline-block w-6">
|
||||
{{ userCountStatusForCircle(csu.user_id).FAIL }}
|
||||
</p>
|
||||
:show-circle-slugs="[cockpitStore.currentCircle.slug]"
|
||||
diagram-type="singleSmall"
|
||||
class="mr-4"
|
||||
></LearningPathDiagram>
|
||||
<p class="lg:min-w-[150px]">
|
||||
{{ cockpitStore.currentCircle.title }}
|
||||
</p>
|
||||
<div class="ml-4 flex flex-row items-center">
|
||||
<div class="mr-6 flex flex-row items-center">
|
||||
<it-icon-smiley-thinking
|
||||
class="mr-2 inline-block h-8 w-8"
|
||||
></it-icon-smiley-thinking>
|
||||
<p class="text-bold inline-block w-6">
|
||||
{{ userCountStatusForCircle(csu.user_id).FAIL }}
|
||||
</p>
|
||||
</div>
|
||||
<li class="mr-6 flex flex-row items-center">
|
||||
<it-icon-smiley-happy
|
||||
class="mr-2 inline-block h-8 w-8"
|
||||
></it-icon-smiley-happy>
|
||||
<p class="text-bold inline-block w-6">
|
||||
{{ userCountStatusForCircle(csu.user_id).SUCCESS }}
|
||||
</p>
|
||||
</li>
|
||||
<li class="flex flex-row items-center">
|
||||
<it-icon-smiley-neutral
|
||||
class="mr-2 inline-block h-8 w-8"
|
||||
></it-icon-smiley-neutral>
|
||||
<p class="text-bold inline-block w-6">
|
||||
{{ userCountStatusForCircle(csu.user_id).UNKNOWN }}
|
||||
</p>
|
||||
</li>
|
||||
</div>
|
||||
<li class="mr-6 flex flex-row items-center">
|
||||
<it-icon-smiley-happy
|
||||
class="mr-2 inline-block h-8 w-8"
|
||||
></it-icon-smiley-happy>
|
||||
<p class="text-bold inline-block w-6">
|
||||
{{ userCountStatusForCircle(csu.user_id).SUCCESS }}
|
||||
</p>
|
||||
</li>
|
||||
<li class="flex flex-row items-center">
|
||||
<it-icon-smiley-neutral
|
||||
class="mr-2 inline-block h-8 w-8"
|
||||
></it-icon-smiley-neutral>
|
||||
<p class="text-bold inline-block w-6">
|
||||
{{ userCountStatusForCircle(csu.user_id).UNKNOWN }}
|
||||
</p>
|
||||
</li>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #link>
|
||||
<router-link
|
||||
:to="`/course/${props.courseSlug}/cockpit/profile/${csu.user_id}`"
|
||||
class="link w-full lg:text-right"
|
||||
>
|
||||
{{ $t("general.profileLink") }}
|
||||
</router-link>
|
||||
</template>
|
||||
</ItPersonRow>
|
||||
</ul>
|
||||
</template>
|
||||
<template #link>
|
||||
<router-link
|
||||
:to="`/course/${props.courseSlug}/cockpit/profile/${csu.user_id}`"
|
||||
class="link w-full lg:text-right"
|
||||
>
|
||||
{{ $t("general.profileLink") }}
|
||||
</router-link>
|
||||
</template>
|
||||
</ItPersonRow>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="container-large mt-4">
|
||||
<span class="text-lg text-orange-600">
|
||||
{{ $t("a.Kein Circle verfügbar oder ausgewählt.") }}
|
||||
</span>
|
||||
<div v-else class="container-large mt-4">
|
||||
<span class="text-lg text-orange-600">
|
||||
{{ $t("a.Kein Circle verfügbar oder ausgewählt.") }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -4,21 +4,20 @@ import log from "loglevel";
|
|||
import { computed, onMounted, ref } from "vue";
|
||||
import ItProgress from "@/components/ui/ItProgress.vue";
|
||||
import { itGet } from "@/fetchHelpers";
|
||||
import { useCockpitStore } from "@/stores/cockpit";
|
||||
import { useCourseSessionDetailQuery } from "@/composables";
|
||||
|
||||
const props = defineProps<{
|
||||
courseSession: CourseSession;
|
||||
circleId: number;
|
||||
circleId: string;
|
||||
}>();
|
||||
|
||||
log.debug("FeedbackSubmissionProgress created");
|
||||
|
||||
const cockpitStore = useCockpitStore();
|
||||
|
||||
const courseSessionDetailResult = useCourseSessionDetailQuery();
|
||||
const completeFeedbacks = ref(0);
|
||||
|
||||
const numFeedbacks = computed(() => {
|
||||
return cockpitStore.courseSessionMembers?.length ?? 0;
|
||||
return courseSessionDetailResult.filterMembers().length;
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
2
|
||||
<script setup lang="ts">
|
||||
import AssignmentSubmissionProgress from "@/pages/cockpit/cockpitPage/AssignmentSubmissionProgress.vue";
|
||||
import { useCockpitStore } from "@/stores/cockpit";
|
||||
import { useLearningPathStore } from "@/stores/learningPath";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import type {
|
||||
|
|
@ -14,9 +13,10 @@ import { computed } from "vue";
|
|||
import { useTranslation } from "i18next-vue";
|
||||
import FeedbackSubmissionProgress from "@/pages/cockpit/cockpitPage/FeedbackSubmissionProgress.vue";
|
||||
import { learningContentTypeData } from "@/utils/typeMaps";
|
||||
import { useCourseSessionDetailQuery } from "@/composables";
|
||||
|
||||
interface Submittable {
|
||||
id: number;
|
||||
id: string;
|
||||
circleName: string;
|
||||
frontendUrl: string;
|
||||
title: string;
|
||||
|
|
@ -27,14 +27,15 @@ interface Submittable {
|
|||
|
||||
const props = defineProps<{
|
||||
courseSession: CourseSession;
|
||||
selectedCircle: number;
|
||||
selectedCircle: string;
|
||||
}>();
|
||||
|
||||
log.debug("SubmissionsOverview created", props.courseSession.id);
|
||||
|
||||
const userStore = useUserStore();
|
||||
const cockpitStore = useCockpitStore();
|
||||
const learningPathStore = useLearningPathStore();
|
||||
const courseSessionDetailResult = useCourseSessionDetailQuery();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const submittables = computed(() => {
|
||||
|
|
@ -128,7 +129,10 @@ const getIconName = (lc: LearningContent) => {
|
|||
|
||||
<template>
|
||||
<div class="bg-white px-6 py-2">
|
||||
<div v-if="cockpitStore.courseSessionMembers" class="divide-y divide-gray-500">
|
||||
<div
|
||||
v-if="courseSessionDetailResult.filterMembers().length"
|
||||
class="divide-y divide-gray-500"
|
||||
>
|
||||
<div
|
||||
v-for="submittable in submittables"
|
||||
:key="submittable.id"
|
||||
|
|
|
|||
|
|
@ -4,34 +4,80 @@ import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
|
|||
import { useCockpitStore } from "@/stores/cockpit";
|
||||
import ItModal from "@/components/ui/ItModal.vue";
|
||||
import DocumentUploadForm from "@/pages/cockpit/documentPage/DocumentUploadForm.vue";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { useCircleStore } from "@/stores/circle";
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
import { useTranslation } from "i18next-vue";
|
||||
import type { CircleDocument, DocumentUploadData } from "@/types";
|
||||
import dialog from "@/utils/confirm-dialog";
|
||||
import log from "loglevel";
|
||||
import { uploadCircleDocument } from "@/services/files";
|
||||
import {
|
||||
deleteCircleDocument,
|
||||
fetchCourseSessionDocuments,
|
||||
uploadCircleDocument,
|
||||
} from "@/services/files";
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import DocumentListItem from "@/components/circle/DocumentListItem.vue";
|
||||
import { useCircleStore } from "@/stores/circle";
|
||||
|
||||
const cockpitStore = useCockpitStore();
|
||||
const courseSession = useCurrentCourseSession();
|
||||
const circleStore = useCircleStore();
|
||||
|
||||
const courseSessionsStore = useCourseSessionsStore();
|
||||
|
||||
const circleStore = useCircleStore();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const showUploadModal = ref(false);
|
||||
const showUploadErrorMessage = ref(false);
|
||||
const isUploading = ref(false);
|
||||
|
||||
const circleDocumentsResultData = ref<CircleDocument[]>([]);
|
||||
let courseSessionDocumentsUrl = "";
|
||||
|
||||
async function fetchDocuments() {
|
||||
const result = await fetchCourseSessionDocuments(courseSession.value?.id);
|
||||
if (result.length > 0) {
|
||||
circleDocumentsResultData.value = result;
|
||||
} else {
|
||||
circleDocumentsResultData.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
log.debug("DocumentPage mounted");
|
||||
if (courseSession.value?.id) {
|
||||
courseSessionDocumentsUrl = `/api/core/document/list/${courseSession.value?.id}/`;
|
||||
}
|
||||
|
||||
await fetchDocuments();
|
||||
});
|
||||
|
||||
watch(
|
||||
// workaround to load learning sequences when circle changes
|
||||
() => cockpitStore.currentCircle,
|
||||
async () => {
|
||||
if (cockpitStore.currentCircle) {
|
||||
await circleStore.loadCircle(
|
||||
courseSession.value?.course.slug,
|
||||
cockpitStore.currentCircle?.slug
|
||||
);
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const dropdownLearningSequences = computed(() =>
|
||||
circleStore.circle?.learningSequences.map((sequence) => ({
|
||||
id: sequence.id,
|
||||
name: sequence.title,
|
||||
name: `${sequence.title}`,
|
||||
}))
|
||||
);
|
||||
|
||||
const circleDocuments = computed(() => {
|
||||
return circleDocumentsResultData.value.filter(
|
||||
(d) => d.learning_sequence.circle.slug === cockpitStore.currentCircle?.slug
|
||||
);
|
||||
});
|
||||
|
||||
const deleteDocument = async (doc: CircleDocument) => {
|
||||
const options = {
|
||||
title: t("circlePage.documents.deleteModalTitle"),
|
||||
|
|
@ -39,7 +85,10 @@ const deleteDocument = async (doc: CircleDocument) => {
|
|||
};
|
||||
try {
|
||||
await dialog.confirm(options);
|
||||
courseSessionsStore.removeDocument(doc.id);
|
||||
await deleteCircleDocument(doc.id, courseSessionDocumentsUrl);
|
||||
circleDocumentsResultData.value = circleDocumentsResultData.value.filter(
|
||||
(d) => d.id !== doc.id
|
||||
);
|
||||
} catch (e) {
|
||||
log.debug("rejected");
|
||||
}
|
||||
|
|
@ -53,11 +102,13 @@ async function uploadDocument(data: DocumentUploadData) {
|
|||
if (!courseSessionsStore.currentCourseSession) {
|
||||
throw new Error("No course session found");
|
||||
}
|
||||
const newDocument = await uploadCircleDocument(
|
||||
await uploadCircleDocument(
|
||||
data,
|
||||
courseSessionsStore.currentCourseSession.id
|
||||
courseSessionsStore.currentCourseSession.id,
|
||||
courseSessionDocumentsUrl
|
||||
);
|
||||
courseSessionsStore.addDocument(newDocument);
|
||||
await fetchDocuments();
|
||||
|
||||
showUploadModal.value = false;
|
||||
isUploading.value = false;
|
||||
} catch (error) {
|
||||
|
|
@ -84,7 +135,7 @@ async function uploadDocument(data: DocumentUploadData) {
|
|||
<div class="mb-9 flex flex-col lg:flex-row lg:items-center lg:justify-between">
|
||||
<h2>{{ t("a.Unterlagen für Teilnehmenden") }}</h2>
|
||||
<ItDropdownSelect
|
||||
:model-value="cockpitStore.selectedCircle"
|
||||
:model-value="cockpitStore.currentCircle"
|
||||
class="mt-4 w-full lg:mt-0 lg:w-96"
|
||||
:items="cockpitStore.circles"
|
||||
@update:model-value="cockpitStore.setCurrentCourseCircleFromEvent"
|
||||
|
|
@ -96,23 +147,15 @@ async function uploadDocument(data: DocumentUploadData) {
|
|||
{{ t("circlePage.documents.action") }}
|
||||
</button>
|
||||
|
||||
<ul
|
||||
v-if="courseSessionsStore.circleDocuments.length"
|
||||
class="mt-8 border-t border-t-gray-500"
|
||||
>
|
||||
<template
|
||||
v-for="learningSequence of courseSessionsStore.circleDocuments"
|
||||
:key="learningSequence.id"
|
||||
>
|
||||
<DocumentListItem
|
||||
v-for="doc of learningSequence.documents"
|
||||
:key="doc.url"
|
||||
:subtitle="learningSequence.title"
|
||||
:can-delete="courseSessionsStore.canUploadCircleDocuments"
|
||||
:doc="doc"
|
||||
@delete="deleteDocument(doc)"
|
||||
/>
|
||||
</template>
|
||||
<ul v-if="circleDocuments.length" class="mt-8 border-t border-t-gray-500">
|
||||
<DocumentListItem
|
||||
v-for="doc of circleDocuments"
|
||||
:key="doc.url"
|
||||
:subtitle="doc.learning_sequence.title"
|
||||
:can-delete="true"
|
||||
:doc="doc"
|
||||
@delete="deleteDocument(doc)"
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
<ItModal v-model="showUploadModal">
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ const formData = reactive<DocumentUploadData>({
|
|||
file: null,
|
||||
name: "",
|
||||
learningSequence: {
|
||||
id: -1,
|
||||
id: "-1",
|
||||
name: t("circlePage.documents.chooseSequence"),
|
||||
},
|
||||
});
|
||||
|
|
@ -56,7 +56,7 @@ function submitForm() {
|
|||
|
||||
function validateForm() {
|
||||
formErrors.file = formData.file === null;
|
||||
formErrors.learningSequence = formData.learningSequence.id === -1;
|
||||
formErrors.learningSequence = formData.learningSequence.id === "-1";
|
||||
formErrors.name = formData.name === "";
|
||||
|
||||
for (const [, value] of Object.entries(formErrors)) {
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ const certificatesQuery = useQuery({
|
|||
query: COMPETENCE_NAVI_CERTIFICATE_QUERY,
|
||||
variables: {
|
||||
courseSlug: props.courseSlug,
|
||||
courseSessionId: courseSession.value.id.toString(),
|
||||
courseSessionId: courseSession.value.id,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ const certificatesQuery = useQuery({
|
|||
query: COMPETENCE_NAVI_CERTIFICATE_QUERY,
|
||||
variables: {
|
||||
courseSlug: props.courseSlug,
|
||||
courseSessionId: courseSession.value.id.toString(),
|
||||
courseSessionId: courseSession.value.id,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ const certificatesQuery = useQuery({
|
|||
query: COMPETENCE_NAVI_CERTIFICATE_QUERY,
|
||||
variables: {
|
||||
courseSlug: props.courseSlug,
|
||||
courseSessionId: courseSession.value.id.toString(),
|
||||
courseSessionId: courseSession.value.id,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import { useCircleStore } from "@/stores/circle";
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import type { CourseSessionUser } from "@/types";
|
||||
import { humanizeDuration } from "@/utils/humanizeDuration";
|
||||
import sumBy from "lodash/sumBy";
|
||||
|
|
@ -11,6 +10,7 @@ import CircleDiagram from "./CircleDiagram.vue";
|
|||
import CircleOverview from "./CircleOverview.vue";
|
||||
import DocumentSection from "./DocumentSection.vue";
|
||||
import LearningSequence from "./LearningSequence.vue";
|
||||
import { useCourseSessionDetailQuery } from "@/composables";
|
||||
|
||||
export interface Props {
|
||||
courseSlug: string;
|
||||
|
|
@ -20,7 +20,8 @@ export interface Props {
|
|||
}
|
||||
|
||||
const route = useRoute();
|
||||
const courseSessionsStore = useCourseSessionsStore();
|
||||
const courseSessionDetailResult = useCourseSessionDetailQuery();
|
||||
const circleStore = useCircleStore();
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
readonly: false,
|
||||
|
|
@ -29,7 +30,12 @@ const props = withDefaults(defineProps<Props>(), {
|
|||
|
||||
log.debug("CirclePage created", props.readonly, props.profileUser);
|
||||
|
||||
const circleStore = useCircleStore();
|
||||
const circleExperts = computed(() => {
|
||||
if (circleStore.circle) {
|
||||
return courseSessionDetailResult.filterCircleExperts(circleStore.circle.slug);
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
const duration = computed(() => {
|
||||
if (circleStore.circle) {
|
||||
|
|
@ -190,10 +196,7 @@ onMounted(async () => {
|
|||
})
|
||||
}}
|
||||
</div>
|
||||
<div
|
||||
v-for="expert in courseSessionsStore.circleExperts"
|
||||
:key="expert.user_id"
|
||||
>
|
||||
<div v-for="expert in circleExperts" :key="expert.user_id">
|
||||
<div class="mb-2 mt-2 flex flex-row items-center">
|
||||
<img
|
||||
class="mr-2 h-[45px] rounded-full"
|
||||
|
|
@ -205,8 +208,8 @@ onMounted(async () => {
|
|||
</div>
|
||||
</div>
|
||||
<a
|
||||
v-if="courseSessionsStore.circleExperts.length > 0"
|
||||
:href="'mailto:' + courseSessionsStore.circleExperts[0].email"
|
||||
v-if="circleExperts.length > 0"
|
||||
:href="'mailto:' + circleExperts[0].email"
|
||||
class="btn-secondary mt-4 text-xl"
|
||||
>
|
||||
{{ $t("circlePage.contactExpertButton") }}
|
||||
|
|
|
|||
|
|
@ -8,28 +8,46 @@
|
|||
{{ $t("circlePage.documents.userDescription") }}
|
||||
</div>
|
||||
</div>
|
||||
<ul
|
||||
v-if="courseSessionsStore.circleDocuments.length"
|
||||
class="mt-8 border-t border-t-gray-500"
|
||||
>
|
||||
<template
|
||||
v-for="learningSequence of courseSessionsStore.circleDocuments"
|
||||
:key="learningSequence.id"
|
||||
>
|
||||
<DocumentListItem
|
||||
v-for="doc of learningSequence.documents"
|
||||
:key="doc.url"
|
||||
:subtitle="learningSequence.title"
|
||||
:doc="doc"
|
||||
/>
|
||||
</template>
|
||||
<ul v-if="circleDocuments.length" class="mt-8 border-t border-t-gray-500">
|
||||
<DocumentListItem
|
||||
v-for="doc of circleDocuments"
|
||||
:key="doc.url"
|
||||
:subtitle="doc.learning_sequence.title"
|
||||
:doc="doc"
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import DocumentListItem from "@/components/circle/DocumentListItem.vue";
|
||||
import { useCurrentCourseSession } from "@/composables";
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { useCircleStore } from "@/stores/circle";
|
||||
import type { CircleDocument } from "@/types";
|
||||
import { fetchCourseSessionDocuments } from "@/services/files";
|
||||
|
||||
const courseSessionsStore = useCourseSessionsStore();
|
||||
const courseSession = useCurrentCourseSession();
|
||||
const circleStore = useCircleStore();
|
||||
|
||||
const circleDocumentsResultData = ref<CircleDocument[]>([]);
|
||||
|
||||
async function fetchDocuments() {
|
||||
const result = await fetchCourseSessionDocuments(courseSession.value?.id);
|
||||
if (result.length > 0) {
|
||||
circleDocumentsResultData.value = result;
|
||||
} else {
|
||||
circleDocumentsResultData.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
const circleDocuments = computed(() => {
|
||||
return circleDocumentsResultData.value.filter(
|
||||
(d) => d.learning_sequence.circle.slug === circleStore.circle?.slug
|
||||
);
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchDocuments();
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -2,19 +2,19 @@
|
|||
import DateEmbedding from "@/components/dueDates/DateEmbedding.vue";
|
||||
import type { Assignment } from "@/types";
|
||||
import { useRouteQuery } from "@vueuse/router";
|
||||
import type { Dayjs } from "dayjs";
|
||||
import log from "loglevel";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
interface Props {
|
||||
assignment: Assignment;
|
||||
dueDate?: Dayjs;
|
||||
submissionDeadlineStart?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
dueDate: undefined,
|
||||
submissionDeadlineStart: "",
|
||||
});
|
||||
|
||||
log.debug("AssignmentIntroductionView created", props.assignment, props.dueDate);
|
||||
log.debug("AssignmentIntroductionView created", props.assignment);
|
||||
|
||||
const step = useRouteQuery("step");
|
||||
</script>
|
||||
|
|
@ -40,9 +40,10 @@ const step = useRouteQuery("step");
|
|||
</ul>
|
||||
|
||||
<h3 class="mb-4 mt-8">{{ $t("assignment.dueDateSubmission") }}</h3>
|
||||
<p v-if="props.dueDate?.toString() === 'Invalid Date'" class="text-large">
|
||||
|
||||
<p v-if="submissionDeadlineStart" class="text-large">
|
||||
{{ $t("assignment.dueDateIntroduction") }}
|
||||
<DateEmbedding :single-date="dueDate"></DateEmbedding>
|
||||
<DateEmbedding :single-date="dayjs(submissionDeadlineStart)"></DateEmbedding>
|
||||
</p>
|
||||
<p v-else class="text-large">
|
||||
{{ $t("assignment.dueDateNotSet") }}
|
||||
|
|
|
|||
|
|
@ -3,34 +3,36 @@ import DateEmbedding from "@/components/dueDates/DateEmbedding.vue";
|
|||
import ItButton from "@/components/ui/ItButton.vue";
|
||||
import ItCheckbox from "@/components/ui/ItCheckbox.vue";
|
||||
import ItSuccessAlert from "@/components/ui/ItSuccessAlert.vue";
|
||||
import { useCurrentCourseSession } from "@/composables";
|
||||
import { useCourseSessionDetailQuery, useCurrentCourseSession } from "@/composables";
|
||||
import { bustItGetCache } from "@/fetchHelpers";
|
||||
import { UPSERT_ASSIGNMENT_COMPLETION_MUTATION } from "@/graphql/mutations";
|
||||
import AssignmentSubmissionResponses from "@/pages/learningPath/learningContentPage/assignment/AssignmentSubmissionResponses.vue";
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import type { Assignment, AssignmentCompletion, AssignmentTask } from "@/types";
|
||||
import { useMutation } from "@urql/vue";
|
||||
import type { Dayjs } from "dayjs";
|
||||
import log from "loglevel";
|
||||
import { computed, reactive } from "vue";
|
||||
import { useTranslation } from "i18next-vue";
|
||||
import eventBus from "@/utils/eventBus";
|
||||
import dayjs from "dayjs";
|
||||
import { useCircleStore } from "@/stores/circle";
|
||||
|
||||
const props = defineProps<{
|
||||
assignment: Assignment;
|
||||
learningContentId: number;
|
||||
learningContentId: string;
|
||||
assignmentCompletion?: AssignmentCompletion;
|
||||
courseSessionId: number;
|
||||
dueDate: Dayjs;
|
||||
courseSessionId: string;
|
||||
submissionDeadlineStart?: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "editTask", task: AssignmentTask): void;
|
||||
}>();
|
||||
|
||||
const courseSessionsStore = useCourseSessionsStore();
|
||||
const courseSession = useCurrentCourseSession();
|
||||
const courseSessionDetailResult = useCourseSessionDetailQuery();
|
||||
const circleStore = useCircleStore();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const state = reactive({
|
||||
|
|
@ -38,8 +40,15 @@ const state = reactive({
|
|||
confirmPerson: false,
|
||||
});
|
||||
|
||||
const circleExperts = computed(() => {
|
||||
if (circleStore.circle) {
|
||||
return courseSessionDetailResult.filterCircleExperts(circleStore.circle.slug);
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
const circleExpert = computed(() => {
|
||||
return courseSessionsStore.circleExperts[0];
|
||||
return circleExperts.value[0];
|
||||
});
|
||||
|
||||
const circleExpertName = computed(() => {
|
||||
|
|
@ -74,9 +83,9 @@ const onEditTask = (task: AssignmentTask) => {
|
|||
const onSubmit = async () => {
|
||||
try {
|
||||
await upsertAssignmentCompletionMutation.executeMutation({
|
||||
assignmentId: props.assignment.id.toString(),
|
||||
courseSessionId: courseSession.value.id.toString(),
|
||||
learningContentId: props.learningContentId.toString(),
|
||||
assignmentId: props.assignment.id,
|
||||
courseSessionId: courseSession.value.id,
|
||||
learningContentId: props.learningContentId,
|
||||
completionDataString: JSON.stringify({}),
|
||||
completionStatus: "SUBMITTED",
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
|
|
@ -138,9 +147,11 @@ const onSubmit = async () => {
|
|||
{{ $t("assignment.showAssessmentDocument") }}
|
||||
</a>
|
||||
</div>
|
||||
<p v-if="isCasework" class="pt-6">
|
||||
<p v-if="isCasework && props.submissionDeadlineStart" class="pt-6">
|
||||
{{ $t("assignment.dueDateSubmission") }}
|
||||
<DateEmbedding :single-date="dueDate"></DateEmbedding>
|
||||
<DateEmbedding
|
||||
:single-date="dayjs(props.submissionDeadlineStart)"
|
||||
></DateEmbedding>
|
||||
</p>
|
||||
<ItButton
|
||||
class="mt-6"
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@ import log from "loglevel";
|
|||
import { computed, reactive } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
assignmentId: number;
|
||||
learningContentId: number;
|
||||
assignmentId: string;
|
||||
learningContentId: string;
|
||||
task: AssignmentTask;
|
||||
assignmentCompletion?: AssignmentCompletion;
|
||||
}>();
|
||||
|
|
@ -33,9 +33,9 @@ const upsertAssignmentCompletionMutation = useMutation(
|
|||
async function upsertAssignmentCompletion(completion_data: AssignmentCompletionData) {
|
||||
try {
|
||||
await upsertAssignmentCompletionMutation.executeMutation({
|
||||
assignmentId: props.assignmentId.toString(),
|
||||
courseSessionId: courseSession.value.id.toString(),
|
||||
learningContentId: props.learningContentId.toString(),
|
||||
assignmentId: props.assignmentId,
|
||||
courseSessionId: courseSession.value.id,
|
||||
learningContentId: props.learningContentId,
|
||||
completionDataString: JSON.stringify(completion_data),
|
||||
completionStatus: "IN_PROGRESS",
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
|
|
|
|||
|
|
@ -1,26 +1,22 @@
|
|||
<script setup lang="ts">
|
||||
import { useCurrentCourseSession } from "@/composables";
|
||||
import { useCourseSessionDetailQuery, useCurrentCourseSession } from "@/composables";
|
||||
import { UPSERT_ASSIGNMENT_COMPLETION_MUTATION } from "@/graphql/mutations";
|
||||
import { ASSIGNMENT_COMPLETION_QUERY } from "@/graphql/queries";
|
||||
import AssignmentIntroductionView from "@/pages/learningPath/learningContentPage/assignment/AssignmentIntroductionView.vue";
|
||||
import AssignmentSubmissionView from "@/pages/learningPath/learningContentPage/assignment/AssignmentSubmissionView.vue";
|
||||
import AssignmentTaskView from "@/pages/learningPath/learningContentPage/assignment/AssignmentTaskView.vue";
|
||||
import LearningContentMultiLayout from "@/pages/learningPath/learningContentPage/layouts/LearningContentMultiLayout.vue";
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import type {
|
||||
Assignment,
|
||||
AssignmentCompletion,
|
||||
AssignmentTask,
|
||||
CourseSessionAssignment,
|
||||
CourseSessionUser,
|
||||
LearningContentAssignment,
|
||||
} from "@/types";
|
||||
import { useMutation, useQuery } from "@urql/vue";
|
||||
import { useRouteQuery } from "@vueuse/router";
|
||||
import dayjs from "dayjs";
|
||||
import * as log from "loglevel";
|
||||
import { computed, onMounted, reactive, ref, watchEffect } from "vue";
|
||||
import { computed, onMounted, ref, watchEffect } from "vue";
|
||||
import { useTranslation } from "i18next-vue";
|
||||
import { bustItGetCache } from "@/fetchHelpers";
|
||||
import { learningContentTypeData } from "@/utils/typeMaps";
|
||||
|
|
@ -30,14 +26,6 @@ const { t } = useTranslation();
|
|||
const courseSession = useCurrentCourseSession();
|
||||
const userStore = useUserStore();
|
||||
|
||||
interface State {
|
||||
courseSessionAssignment: CourseSessionAssignment | undefined;
|
||||
}
|
||||
|
||||
const state: State = reactive({
|
||||
courseSessionAssignment: undefined,
|
||||
});
|
||||
|
||||
const props = defineProps<{
|
||||
learningContent: LearningContentAssignment;
|
||||
}>();
|
||||
|
|
@ -45,17 +33,24 @@ const props = defineProps<{
|
|||
const queryResult = useQuery({
|
||||
query: ASSIGNMENT_COMPLETION_QUERY,
|
||||
variables: {
|
||||
courseSessionId: courseSession.value.id.toString(),
|
||||
assignmentId: props.learningContent.content_assignment_id.toString(),
|
||||
learningContentId: props.learningContent.id.toString(),
|
||||
courseSessionId: courseSession.value.id,
|
||||
assignmentId: props.learningContent.content_assignment_id,
|
||||
learningContentId: props.learningContent.id,
|
||||
},
|
||||
pause: true,
|
||||
});
|
||||
|
||||
const courseSessionDetailResult = useCourseSessionDetailQuery();
|
||||
|
||||
const upsertAssignmentCompletionMutation = useMutation(
|
||||
UPSERT_ASSIGNMENT_COMPLETION_MUTATION
|
||||
);
|
||||
|
||||
const submissionDeadline = computed(() => {
|
||||
return courseSessionDetailResult.findAssignment(props.learningContent.id)
|
||||
?.submission_deadline;
|
||||
});
|
||||
|
||||
// FIXME daniel: `useRouteQuery` from usevue is currently the reason that we have to
|
||||
// fix the version of @vueuse/router and @vueuse/core to 10.1.0
|
||||
// it fails with version 10.2.0. I have a reminder to check out the situation
|
||||
|
|
@ -104,10 +99,6 @@ onMounted(async () => {
|
|||
props.learningContent
|
||||
);
|
||||
|
||||
state.courseSessionAssignment = useCourseSessionsStore().findCourseSessionAssignment(
|
||||
props.learningContent.id
|
||||
);
|
||||
|
||||
// create initial `AssignmentCompletion` first, so that it exists and we don't
|
||||
// have reactivity problem accessing it.
|
||||
await initUpsertAssignmentCompletion();
|
||||
|
|
@ -120,9 +111,7 @@ const numPages = computed(() => {
|
|||
});
|
||||
const showPreviousButton = computed(() => stepIndex.value != 0);
|
||||
const showNextButton = computed(() => stepIndex.value + 1 < numPages.value);
|
||||
const dueDate = computed(() =>
|
||||
dayjs(state.courseSessionAssignment?.submission_deadline_start)
|
||||
);
|
||||
|
||||
const currentTask = computed(() => {
|
||||
if (stepIndex.value > 0 && stepIndex.value <= numTasks.value) {
|
||||
return assignment.value?.tasks[stepIndex.value - 1];
|
||||
|
|
@ -133,9 +122,9 @@ const currentTask = computed(() => {
|
|||
const initUpsertAssignmentCompletion = async () => {
|
||||
try {
|
||||
await upsertAssignmentCompletionMutation.executeMutation({
|
||||
assignmentId: props.learningContent.content_assignment_id.toString(),
|
||||
courseSessionId: courseSession.value.id.toString(),
|
||||
learningContentId: props.learningContent.id.toString(),
|
||||
assignmentId: props.learningContent.content_assignment_id,
|
||||
courseSessionId: courseSession.value.id,
|
||||
learningContentId: props.learningContent.id,
|
||||
completionDataString: JSON.stringify({}),
|
||||
completionStatus: "IN_PROGRESS",
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
|
|
@ -194,9 +183,9 @@ const subTitle = computed(() => {
|
|||
});
|
||||
|
||||
const assignmentUser = computed(() => {
|
||||
return courseSession.value.users.find(
|
||||
(user) => user.user_id === userStore.id
|
||||
) as CourseSessionUser;
|
||||
return (courseSessionDetailResult.courseSessionDetail.value?.users ?? []).find(
|
||||
(u) => u.user_id === userStore.id
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
@ -204,7 +193,7 @@ const assignmentUser = computed(() => {
|
|||
<div v-if="queryResult.fetching.value"></div>
|
||||
<div v-else-if="queryResult.error.value">{{ queryResult.error.value }}</div>
|
||||
<div v-else>
|
||||
<div v-if="assignment && assignmentCompletion">
|
||||
<div v-if="assignment && assignmentCompletion && assignmentUser">
|
||||
<div class="flex flex-col lg:flex-row">
|
||||
<div
|
||||
v-if="assignmentCompletion?.completion_status === 'EVALUATION_SUBMITTED'"
|
||||
|
|
@ -239,8 +228,8 @@ const assignmentUser = computed(() => {
|
|||
<div>
|
||||
<AssignmentIntroductionView
|
||||
v-if="stepIndex === 0"
|
||||
:due-date="dueDate"
|
||||
:assignment="assignment"
|
||||
:submission-deadline-start="submissionDeadline?.start"
|
||||
></AssignmentIntroductionView>
|
||||
<AssignmentTaskView
|
||||
v-else-if="currentTask"
|
||||
|
|
@ -251,11 +240,11 @@ const assignmentUser = computed(() => {
|
|||
></AssignmentTaskView>
|
||||
<AssignmentSubmissionView
|
||||
v-else-if="stepIndex + 1 === numPages"
|
||||
:due-date="dueDate"
|
||||
:assignment="assignment"
|
||||
:assignment-completion="assignmentCompletion"
|
||||
:learning-content-id="props.learningContent.id"
|
||||
:course-session-id="courseSession.id"
|
||||
:submission-deadline-start="submissionDeadline?.start"
|
||||
@edit-task="jumpToTask($event)"
|
||||
></AssignmentSubmissionView>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,12 @@
|
|||
<it-icon-calendar-light class="w-[60px] grid-in-icon" />
|
||||
<h2 class="text-large font-bold grid-in-title">{{ $t("a.Datum") }}</h2>
|
||||
<p class="grid-in-value">
|
||||
{{ formatDueDate(props.attendanceCourse.start, props.attendanceCourse.end) }}
|
||||
{{
|
||||
formatDueDate(
|
||||
props.attendanceCourse.due_date.start,
|
||||
props.attendanceCourse.due_date.end
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="mb-12 grid grid-cols-icon-card gap-x-4 grid-areas-icon-card">
|
||||
|
|
@ -24,8 +29,6 @@
|
|||
<script setup lang="ts">
|
||||
import { formatDueDate } from "@/components/dueDates/dueDatesUtils";
|
||||
import type { CourseSessionAttendanceCourse } from "@/types";
|
||||
import dayjs from "dayjs";
|
||||
import LocalizedFormat from "dayjs/plugin/localizedFormat";
|
||||
import { computed } from "vue";
|
||||
|
||||
export interface Props {
|
||||
|
|
@ -34,7 +37,6 @@ export interface Props {
|
|||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
dayjs.extend(LocalizedFormat);
|
||||
const location = computed(() => props.attendanceCourse.location);
|
||||
const trainer = computed(() => props.attendanceCourse.trainer);
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
<script setup lang="ts">
|
||||
import AttendanceCourse from "@/pages/learningPath/learningContentPage/attendanceCourse/AttendanceCourse.vue";
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import type { LearningContentAttendanceCourse } from "@/types";
|
||||
import { computed } from "vue";
|
||||
import LearningContentSimpleLayout from "../layouts/LearningContentSimpleLayout.vue";
|
||||
|
||||
const courseSessionsStore = useCourseSessionsStore();
|
||||
import { useCourseSessionDetailQuery } from "@/composables";
|
||||
|
||||
const props = defineProps<{
|
||||
content: LearningContentAttendanceCourse;
|
||||
}>();
|
||||
|
||||
const courseSessionDetailResult = useCourseSessionDetailQuery();
|
||||
|
||||
const courseSessionAttendanceCourse = computed(() => {
|
||||
return courseSessionsStore.findAttendanceCourse(props.content.id);
|
||||
return courseSessionDetailResult.findAttendanceCourse(props.content.id);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -8,8 +8,7 @@ import * as log from "loglevel";
|
|||
import { itPost } from "@/fetchHelpers";
|
||||
import { useQuery } from "@urql/vue";
|
||||
import { ASSIGNMENT_COMPLETION_QUERY } from "@/graphql/queries";
|
||||
import { useCurrentCourseSession } from "@/composables";
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import { useCourseSessionDetailQuery, useCurrentCourseSession } from "@/composables";
|
||||
import dayjs from "dayjs";
|
||||
import ItSuccessAlert from "@/components/ui/ItSuccessAlert.vue";
|
||||
import {
|
||||
|
|
@ -24,18 +23,18 @@ const props = defineProps<{
|
|||
}>();
|
||||
|
||||
const courseSession = useCurrentCourseSession();
|
||||
const courseSessionsStore = useCourseSessionsStore();
|
||||
const courseSessionDetailResult = useCourseSessionDetailQuery();
|
||||
|
||||
const courseSessionEdoniqTest = computed(() => {
|
||||
return courseSessionsStore.findCourseSessionEdoniqTest(props.content.id);
|
||||
return courseSessionDetailResult.findEdoniqTest(props.content.id);
|
||||
});
|
||||
|
||||
const queryResult = useQuery({
|
||||
query: ASSIGNMENT_COMPLETION_QUERY,
|
||||
variables: {
|
||||
courseSessionId: courseSession.value.id.toString(),
|
||||
assignmentId: props.content.content_assignment_id.toString(),
|
||||
learningContentId: props.content.id.toString(),
|
||||
courseSessionId: courseSession.value.id,
|
||||
assignmentId: props.content.content_assignment_id,
|
||||
learningContentId: props.content.id,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -53,7 +52,7 @@ const extendedTimeTest = ref(false);
|
|||
|
||||
const deadlineInPast = computed(() => {
|
||||
// with 16 minutes buffer
|
||||
return dayjs(courseSessionEdoniqTest.value?.deadline_start)
|
||||
return dayjs(courseSessionEdoniqTest.value?.deadline.start)
|
||||
.add(16, "minute")
|
||||
.isBefore(dayjs());
|
||||
});
|
||||
|
|
@ -90,7 +89,7 @@ async function startTest() {
|
|||
<p class="mt-2 text-lg">
|
||||
{{
|
||||
$t("edoniqTest.submitDateDescription", {
|
||||
x: formatDueDate(courseSessionEdoniqTest.deadline_start),
|
||||
x: formatDueDate(courseSessionEdoniqTest.deadline.start),
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
|
|
@ -158,7 +157,7 @@ async function startTest() {
|
|||
</div>
|
||||
<div v-else>
|
||||
{{ $t("a.Abgabetermin") }}:
|
||||
{{ getDateString(dayjs(courseSessionEdoniqTest?.deadline_start)) }}
|
||||
{{ getDateString(dayjs(courseSessionEdoniqTest?.deadline.start)) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -10,21 +10,21 @@ import {
|
|||
} from "@/pages/learningPath/learningContentPage/feedback/feedback.constants";
|
||||
import LearningContentMultiLayout from "@/pages/learningPath/learningContentPage/layouts/LearningContentMultiLayout.vue";
|
||||
import { useCircleStore } from "@/stores/circle";
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import type { LearningContentFeedback } from "@/types";
|
||||
import { useMutation } from "@urql/vue";
|
||||
import { useRouteQuery } from "@vueuse/router";
|
||||
import log from "loglevel";
|
||||
import { computed, onMounted, reactive, ref } from "vue";
|
||||
import { useTranslation } from "i18next-vue";
|
||||
import { useCurrentCourseSession } from "@/composables";
|
||||
import { useCourseSessionDetailQuery, useCurrentCourseSession } from "@/composables";
|
||||
|
||||
const props = defineProps<{
|
||||
content: LearningContentFeedback;
|
||||
}>();
|
||||
const courseSessionsStore = useCourseSessionsStore();
|
||||
const courseSession = useCurrentCourseSession();
|
||||
const circleStore = useCircleStore();
|
||||
const courseSessionDetailResult = useCourseSessionDetailQuery();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const stepNo = useRouteQuery("step", "0", { transform: Number, mode: "push" });
|
||||
|
|
@ -33,6 +33,13 @@ const title = computed(
|
|||
() => `«${circleStore.circle?.title}»: ${t("feedback.areYouSatisfied")}`
|
||||
);
|
||||
|
||||
const circleExperts = computed(() => {
|
||||
if (circleStore.circle) {
|
||||
return courseSessionDetailResult.filterCircleExperts(circleStore.circle.slug);
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
const stepLabels = [
|
||||
t("general.introduction"),
|
||||
t("feedback.satisfactionLabel"),
|
||||
|
|
@ -185,8 +192,8 @@ function hasStepValidInput(stepNumber: number) {
|
|||
function mutateFeedback(data: FeedbackData, submit = false) {
|
||||
log.debug("mutate feedback", feedbackData);
|
||||
return executeMutation({
|
||||
courseSessionId: courseSession.value.id.toString(),
|
||||
learningContentId: props.content.id.toString(),
|
||||
courseSessionId: courseSession.value.id,
|
||||
learningContentId: props.content.id,
|
||||
data: data,
|
||||
submitted: submit,
|
||||
})
|
||||
|
|
@ -244,7 +251,7 @@ onMounted(async () => {
|
|||
<p v-if="stepNo === 0" class="mt-10">
|
||||
{{
|
||||
$t("feedback.intro", {
|
||||
name: `${courseSessionsStore.circleExperts[0]?.first_name} ${courseSessionsStore.circleExperts[0]?.last_name}`,
|
||||
name: `${circleExperts[0]?.first_name} ${circleExperts[0]?.last_name}`,
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
|
|
@ -265,10 +272,10 @@ onMounted(async () => {
|
|||
</div>
|
||||
<FeedbackCompletition
|
||||
v-if="stepNo === 11"
|
||||
:avatar-url="courseSessionsStore.circleExperts[0].avatar_url"
|
||||
:avatar-url="circleExperts[0].avatar_url"
|
||||
:title="
|
||||
$t('feedback.completionTitle', {
|
||||
name: `${courseSessionsStore.circleExperts[0].first_name} ${courseSessionsStore.circleExperts[0].last_name}`,
|
||||
name: `${circleExperts[0].first_name} ${circleExperts[0].last_name}`,
|
||||
})
|
||||
"
|
||||
:description="$t('feedback.completionDescription')"
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import eventBus from "@/utils/eventBus";
|
|||
import { useRouteQuery } from "@vueuse/router";
|
||||
import { computed, onUnmounted } from "vue";
|
||||
import { getPreviousRoute } from "@/router/history";
|
||||
import { getCompetenceNaviUrl } from "@/utils/utils";
|
||||
|
||||
log.debug("LearningContent.vue setup");
|
||||
|
||||
|
|
@ -127,7 +128,7 @@ onUnmounted(() => {
|
|||
<div class="mt-6 lg:mt-12">
|
||||
{{ $t("selfEvaluation.progressText") }}
|
||||
<router-link
|
||||
:to="courseSession.competence_url"
|
||||
:to="getCompetenceNaviUrl(courseSession.course.slug)"
|
||||
class="text-primary-500 underline"
|
||||
>
|
||||
{{ $t("selfEvaluation.progressLink") }}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
import { useCourseSessionDetailQuery } from "@/composables";
|
||||
import { itGet } from "@/fetchHelpers";
|
||||
import type { LearningPath } from "@/services/learningPath";
|
||||
import { useCockpitStore } from "@/stores/cockpit";
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import { useLearningPathStore } from "@/stores/learningPath";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import type {
|
||||
Assignment,
|
||||
AssignmentCompletion,
|
||||
|
|
@ -31,23 +28,21 @@ export function calcLearningContentAssignments(learningPath?: LearningPath) {
|
|||
}
|
||||
|
||||
export async function loadAssignmentCompletionStatusData(
|
||||
assignmentId: number,
|
||||
courseSessionId: number,
|
||||
learningContentId: number
|
||||
assignmentId: string,
|
||||
courseSessionId: string,
|
||||
learningContentId: string
|
||||
) {
|
||||
const cockpitStore = useCockpitStore();
|
||||
const courseSessionDetailResult = useCourseSessionDetailQuery();
|
||||
|
||||
const assignmentCompletionData = (await itGet(
|
||||
`/api/assignment/${assignmentId}/${courseSessionId}/status/`
|
||||
)) as UserAssignmentCompletionStatus[];
|
||||
|
||||
const courseSessionUsers = await cockpitStore.loadCourseSessionMembers(
|
||||
courseSessionId
|
||||
);
|
||||
const members = courseSessionDetailResult.filterMembers();
|
||||
|
||||
const gradedUsers: GradedUser[] = [];
|
||||
const assignmentSubmittedUsers: CourseSessionUser[] = [];
|
||||
for (const csu of courseSessionUsers) {
|
||||
for (const csu of members) {
|
||||
const userAssignmentStatus = assignmentCompletionData.find(
|
||||
(s) =>
|
||||
s.assignment_user_id === csu.user_id &&
|
||||
|
|
@ -70,34 +65,10 @@ export async function loadAssignmentCompletionStatusData(
|
|||
return {
|
||||
assignmentSubmittedUsers: assignmentSubmittedUsers,
|
||||
gradedUsers: gradedUsers,
|
||||
total: courseSessionUsers.length,
|
||||
total: members.length,
|
||||
};
|
||||
}
|
||||
|
||||
export function findAssignmentDetail(assignmentId: number) {
|
||||
const learningPathStore = useLearningPathStore();
|
||||
const userStore = useUserStore();
|
||||
const courseSessionsStore = useCourseSessionsStore();
|
||||
|
||||
// TODO: filter by selected circle
|
||||
if (!courseSessionsStore.currentCourseSession) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const learningContents = calcLearningContentAssignments(
|
||||
learningPathStore.learningPathForUser(
|
||||
courseSessionsStore.currentCourseSession.course.slug,
|
||||
userStore.id
|
||||
)
|
||||
);
|
||||
|
||||
const learningContent = learningContents.find(
|
||||
(lc) => lc.content_assignment_id === assignmentId
|
||||
);
|
||||
|
||||
return courseSessionsStore.findCourseSessionAssignment(learningContent?.id);
|
||||
}
|
||||
|
||||
export function maxAssignmentPoints(assignment: Assignment) {
|
||||
return sum(assignment.evaluation_tasks.map((task) => task.value.max_points));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ export class Circle implements WagtailCircle {
|
|||
previousCircle?: Circle;
|
||||
|
||||
constructor(
|
||||
public readonly id: number,
|
||||
public readonly id: string,
|
||||
public readonly slug: string,
|
||||
public readonly title: string,
|
||||
public readonly translation_key: string,
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import { itDelete, itFetch, itPost } from "@/fetchHelpers";
|
||||
import { bustItGetCache, itDelete, itFetch, itGetCached, itPost } from "@/fetchHelpers";
|
||||
import { getCookieValue } from "@/router/guards";
|
||||
import type { CircleDocument, DocumentUploadData } from "@/types";
|
||||
import type { DocumentUploadData } from "@/types";
|
||||
|
||||
type FileData = {
|
||||
fields: Record<string, string>;
|
||||
url: string;
|
||||
};
|
||||
|
||||
async function startFileUpload(fileData: DocumentUploadData, courseSessionId: number) {
|
||||
async function startFileUpload(fileData: DocumentUploadData, courseSessionId: string) {
|
||||
if (fileData === null || fileData.file === null) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -73,8 +73,9 @@ function handleUpload(url: string, options: RequestInit) {
|
|||
|
||||
export async function uploadCircleDocument(
|
||||
data: DocumentUploadData,
|
||||
courseSessionId: number
|
||||
): Promise<CircleDocument> {
|
||||
courseSessionId: string,
|
||||
bustCacheUrlKey = ""
|
||||
) {
|
||||
if (data.file === null) {
|
||||
throw new Error("No file selected");
|
||||
}
|
||||
|
|
@ -82,22 +83,25 @@ export async function uploadCircleDocument(
|
|||
const startData = await startFileUpload(data, courseSessionId);
|
||||
|
||||
await uploadFile(startData, data.file);
|
||||
const response = await itPost(`/api/core/file/finish/`, {
|
||||
const response = itPost(`/api/core/file/finish/`, {
|
||||
file_id: startData.file_id,
|
||||
});
|
||||
|
||||
const newDocument: CircleDocument = {
|
||||
id: startData.id,
|
||||
name: data.name,
|
||||
file_name: data.file.name,
|
||||
url: response.url,
|
||||
course_session: courseSessionId,
|
||||
learning_sequence: data.learningSequence.id,
|
||||
};
|
||||
if (bustCacheUrlKey) {
|
||||
bustItGetCache(bustCacheUrlKey);
|
||||
}
|
||||
|
||||
return Promise.resolve(newDocument);
|
||||
return response;
|
||||
}
|
||||
|
||||
export async function deleteCircleDocument(documentId: string) {
|
||||
return itDelete(`/api/core/document/${documentId}/`);
|
||||
export async function deleteCircleDocument(documentId: string, bustCacheUrlKey = "") {
|
||||
const result = itDelete(`/api/core/document/${documentId}/`);
|
||||
if (bustCacheUrlKey) {
|
||||
bustItGetCache(bustCacheUrlKey);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function fetchCourseSessionDocuments(courseSessionId: string) {
|
||||
return itGetCached(`/api/core/document/list/${courseSessionId}/`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ export class LearningPath implements WagtailLearningPath {
|
|||
}
|
||||
|
||||
constructor(
|
||||
public readonly id: number,
|
||||
public readonly id: string,
|
||||
public readonly slug: string,
|
||||
public readonly title: string,
|
||||
public readonly translation_key: string,
|
||||
|
|
|
|||
|
|
@ -35,11 +35,11 @@ describe("CourseSession Store", () => {
|
|||
};
|
||||
courseSessions = [
|
||||
{
|
||||
id: 1,
|
||||
id: "1",
|
||||
created_at: "2021-05-11T10:00:00.000000Z",
|
||||
updated_at: "2023-05-11T10:00:00.000000Z",
|
||||
course: {
|
||||
id: 1,
|
||||
id: "1",
|
||||
title: "Test Course",
|
||||
category_name: "Test Category",
|
||||
slug: "test-course",
|
||||
|
|
|
|||
|
|
@ -1,18 +1,17 @@
|
|||
import { itGetCached } from "@/fetchHelpers";
|
||||
import type { CourseSessionUser, ExpertSessionUser } from "@/types";
|
||||
import log from "loglevel";
|
||||
|
||||
import { useCircleStore } from "@/stores/circle";
|
||||
import { useLearningPathStore } from "@/stores/learningPath";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import type { CircleLight, CourseSessionUser, ExpertSessionUser } from "@/types";
|
||||
import log from "loglevel";
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
type CircleCockpit = CircleLight & {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type CockpitStoreState = {
|
||||
courseSessionMembers: CourseSessionUser[] | undefined;
|
||||
circles: {
|
||||
id: string;
|
||||
name: string;
|
||||
}[];
|
||||
circles: CircleCockpit[] | undefined;
|
||||
currentCircle: CircleCockpit | undefined;
|
||||
currentCourseSlug: string | undefined;
|
||||
};
|
||||
|
||||
|
|
@ -22,77 +21,54 @@ export const useCockpitStore = defineStore({
|
|||
return {
|
||||
courseSessionMembers: undefined,
|
||||
circles: [],
|
||||
currentCircle: undefined,
|
||||
currentCourseSlug: undefined,
|
||||
} as CockpitStoreState;
|
||||
},
|
||||
actions: {
|
||||
async loadCircles(courseSlug: string, courseSessionId: number) {
|
||||
log.debug("loadCircles called", courseSlug, courseSessionId);
|
||||
async loadCircles(
|
||||
courseSlug: string,
|
||||
currentCourseSessionUser: CourseSessionUser | undefined
|
||||
) {
|
||||
log.debug("loadCircles called", courseSlug);
|
||||
this.currentCourseSlug = courseSlug;
|
||||
|
||||
const f = await courseCircles(this.currentCourseSlug, courseSessionId);
|
||||
const circles = await courseCircles(
|
||||
this.currentCourseSlug,
|
||||
currentCourseSessionUser
|
||||
);
|
||||
|
||||
this.circles = f.map((c) => {
|
||||
this.circles = circles.map((c) => {
|
||||
return {
|
||||
id: c.slug,
|
||||
id: c.id,
|
||||
slug: c.slug,
|
||||
title: c.title,
|
||||
name: c.title,
|
||||
};
|
||||
} as const;
|
||||
});
|
||||
|
||||
if (this.circles.length > 0) {
|
||||
await this.setCurrentCourseCircle(this.circles[0].id);
|
||||
if (this.circles?.length) {
|
||||
await this.setCurrentCourseCircle(this.circles[0].slug);
|
||||
}
|
||||
},
|
||||
async setCurrentCourseCircle(circleSlug: string) {
|
||||
if (!this.currentCourseSlug) {
|
||||
throw new Error("currentCourseSlug is undefined");
|
||||
}
|
||||
const circleStore = useCircleStore();
|
||||
await circleStore.loadCircle(this.currentCourseSlug, circleSlug);
|
||||
this.currentCircle = this.circles?.find((c) => c.slug === circleSlug);
|
||||
},
|
||||
async setCurrentCourseCircleFromEvent(event: { id: string }) {
|
||||
await this.setCurrentCourseCircle(event.id);
|
||||
},
|
||||
async loadCourseSessionMembers(courseSessionId: number, reload = false) {
|
||||
log.debug("loadCourseSessionMembers called");
|
||||
const users = (await itGetCached(
|
||||
`/api/course/sessions/${courseSessionId}/users/`,
|
||||
{
|
||||
reload: reload,
|
||||
}
|
||||
)) as CourseSessionUser[];
|
||||
|
||||
this.courseSessionMembers = users.filter((user) => user.role === "MEMBER");
|
||||
return this.courseSessionMembers;
|
||||
},
|
||||
},
|
||||
getters: {
|
||||
currentCircle: () => {
|
||||
const circleStore = useCircleStore();
|
||||
return circleStore.circle;
|
||||
},
|
||||
selectedCircle: () => {
|
||||
const circleStore = useCircleStore();
|
||||
return {
|
||||
id: circleStore.circle?.id || 0,
|
||||
name: circleStore.circle?.title || "",
|
||||
};
|
||||
async setCurrentCourseCircleFromEvent(event: CircleLight) {
|
||||
await this.setCurrentCourseCircle(event.slug);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
async function courseCircles(courseSlug: string, courseSessionId: number) {
|
||||
async function courseCircles(
|
||||
courseSlug: string,
|
||||
currentCourseSessionUser: CourseSessionUser | undefined
|
||||
) {
|
||||
const userStore = useUserStore();
|
||||
const userId = userStore.id;
|
||||
|
||||
const users = (await itGetCached(`/api/course/sessions/${courseSessionId}/users/`, {
|
||||
reload: false,
|
||||
})) as CourseSessionUser[];
|
||||
|
||||
// First check if current user is an expert for this course session
|
||||
const currentUser = users.find((user) => user.user_id === userId);
|
||||
if (currentUser && currentUser.role === "EXPERT") {
|
||||
const expert = currentUser as ExpertSessionUser;
|
||||
if (currentCourseSessionUser && currentCourseSessionUser.role === "EXPERT") {
|
||||
const expert = currentCourseSessionUser as ExpertSessionUser;
|
||||
return expert.circles;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import type {
|
|||
CompetenceProfilePage,
|
||||
PerformanceCriteria,
|
||||
} from "@/types";
|
||||
import i18next from "i18next";
|
||||
import _ from "lodash";
|
||||
import cloneDeep from "lodash/cloneDeep";
|
||||
import groupBy from "lodash/groupBy";
|
||||
|
|
@ -17,9 +16,6 @@ import { defineStore } from "pinia";
|
|||
|
||||
export type CompetenceStoreState = {
|
||||
competenceProfilePages: Map<string, CompetenceProfilePage>;
|
||||
|
||||
selectedCircle: { id: string; name: string };
|
||||
availableCircles: { id: string; name: string }[];
|
||||
circles: CircleLight[];
|
||||
};
|
||||
|
||||
|
|
@ -28,8 +24,6 @@ export const useCompetenceStore = defineStore({
|
|||
state: () => {
|
||||
return {
|
||||
competenceProfilePages: new Map<string, CompetenceProfilePage>(),
|
||||
selectedCircle: { id: "all", name: `Circle: ${i18next.t("Alle")}` },
|
||||
availableCircles: [],
|
||||
circles: [],
|
||||
} as CompetenceStoreState;
|
||||
},
|
||||
|
|
@ -52,13 +46,7 @@ export const useCompetenceStore = defineStore({
|
|||
};
|
||||
},
|
||||
criteriaByCompetence(competence: CompetencePage) {
|
||||
return competence.children.filter((criteria) => {
|
||||
if (this.selectedCircle.id != "all") {
|
||||
return criteria.circle.translation_key === this.selectedCircle.id;
|
||||
}
|
||||
|
||||
return competence.children;
|
||||
});
|
||||
return competence.children;
|
||||
},
|
||||
competenceProfilePage(userId: string | undefined = undefined) {
|
||||
if (!userId) {
|
||||
|
|
@ -70,7 +58,7 @@ export const useCompetenceStore = defineStore({
|
|||
},
|
||||
flatPerformanceCriteria(
|
||||
userId: string | undefined = undefined,
|
||||
circleId: number | undefined = undefined
|
||||
circleId: string | undefined = undefined
|
||||
) {
|
||||
if (!userId) {
|
||||
const userStore = useUserStore();
|
||||
|
|
@ -88,12 +76,6 @@ export const useCompetenceStore = defineStore({
|
|||
["asc"]
|
||||
);
|
||||
|
||||
if (this.selectedCircle.id !== "all") {
|
||||
criteria = criteria.filter(
|
||||
(c) => c.circle.translation_key === this.selectedCircle.id
|
||||
);
|
||||
}
|
||||
|
||||
if (circleId) {
|
||||
criteria = criteria.filter((c) => circleId === c.circle.id);
|
||||
}
|
||||
|
|
@ -116,12 +98,7 @@ export const useCompetenceStore = defineStore({
|
|||
if (competenceProfilePage?.children.length) {
|
||||
return _.orderBy(
|
||||
competenceProfilePage.children.filter((competence) => {
|
||||
let criteria = competence.children;
|
||||
if (this.selectedCircle.id != "all") {
|
||||
criteria = criteria.filter((criteria) => {
|
||||
return criteria.circle.translation_key === this.selectedCircle.id;
|
||||
});
|
||||
}
|
||||
const criteria = competence.children;
|
||||
return criteria.length > 0;
|
||||
}),
|
||||
["competence_id"],
|
||||
|
|
@ -159,10 +136,6 @@ export const useCompetenceStore = defineStore({
|
|||
this.competenceProfilePages.set(userId, cloneDeep(competenceProfilePage));
|
||||
|
||||
this.circles = competenceProfilePage.circles;
|
||||
const circles = competenceProfilePage.circles.map((c: CircleLight) => {
|
||||
return { id: c.translation_key, name: `Circle: ${c.title}` };
|
||||
});
|
||||
this.availableCircles = [{ id: "all", name: "Circle: Alle" }, ...circles];
|
||||
|
||||
await this.parseCompletionData(userId);
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ export const useCompletionStore = defineStore({
|
|||
getters: {},
|
||||
actions: {
|
||||
async loadCourseSessionCompletionData(
|
||||
courseSessionId: number,
|
||||
courseSessionId: string,
|
||||
userId: string,
|
||||
reload = false
|
||||
) {
|
||||
|
|
@ -31,7 +31,7 @@ export const useCompletionStore = defineStore({
|
|||
async markPage(
|
||||
page: BaseCourseWagtailPage,
|
||||
userId: string | undefined = undefined,
|
||||
courseSessionId: number | undefined = undefined
|
||||
courseSessionId: string | undefined = undefined
|
||||
) {
|
||||
if (!courseSessionId) {
|
||||
const courseSessionsStore = useCourseSessionsStore();
|
||||
|
|
|
|||
|
|
@ -1,15 +1,5 @@
|
|||
import { itGetCached, itPost } from "@/fetchHelpers";
|
||||
import { deleteCircleDocument } from "@/services/files";
|
||||
import type {
|
||||
CircleDocument,
|
||||
CourseSession,
|
||||
CourseSessionAssignment,
|
||||
CourseSessionAttendanceCourse,
|
||||
CourseSessionEdoniqTest,
|
||||
CourseSessionUser,
|
||||
DueDate,
|
||||
ExpertSessionUser,
|
||||
} from "@/types";
|
||||
import { itGetCached } from "@/fetchHelpers";
|
||||
import type { CourseSession, DueDate } from "@/types";
|
||||
import eventBus from "@/utils/eventBus";
|
||||
import { useRouteLookups } from "@/utils/route";
|
||||
import { useLocalStorage } from "@vueuse/core";
|
||||
|
|
@ -18,7 +8,6 @@ import uniqBy from "lodash/uniqBy";
|
|||
import log from "loglevel";
|
||||
import { defineStore } from "pinia";
|
||||
import { computed, ref } from "vue";
|
||||
import { useCircleStore } from "./circle";
|
||||
import { useUserStore } from "./user";
|
||||
|
||||
const SELECTED_COURSE_SESSIONS_KEY = "selectedCourseSessionMap";
|
||||
|
|
@ -39,10 +28,6 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
|
|||
// TODO: refactor after implementing of Klassenkonzept
|
||||
await Promise.all(
|
||||
allCourseSessions.value.map(async (cs) => {
|
||||
const users = (await itGetCached(`/api/course/sessions/${cs.id}/users/`, {
|
||||
reload: reload,
|
||||
})) as CourseSessionUser[];
|
||||
cs.users = users;
|
||||
sortDueDates(cs.due_dates);
|
||||
})
|
||||
);
|
||||
|
|
@ -57,7 +42,7 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
|
|||
|
||||
const selectedCourseSessionMap = useLocalStorage(
|
||||
SELECTED_COURSE_SESSIONS_KEY,
|
||||
new Map<string, number>()
|
||||
new Map<string, string>()
|
||||
);
|
||||
|
||||
const _currentCourseSlug = ref("");
|
||||
|
|
@ -99,15 +84,15 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
|
|||
eventBus.emit("switchedCourseSession", courseSession.id);
|
||||
}
|
||||
|
||||
function getCourseSessionById(courseSessionId: number | string) {
|
||||
function getCourseSessionById(courseSessionId: string) {
|
||||
return allCourseSessions.value.find((cs) => {
|
||||
return courseSessionId.toString() === cs.id.toString();
|
||||
return courseSessionId === cs.id;
|
||||
});
|
||||
}
|
||||
|
||||
function switchCourseSessionById(courseSessionId: number | string) {
|
||||
function switchCourseSessionById(courseSessionId: string) {
|
||||
const courseSession = allCourseSessions.value.find((cs) => {
|
||||
return courseSessionId.toString() === cs.id.toString();
|
||||
return courseSessionId === cs.id;
|
||||
});
|
||||
if (courseSession) {
|
||||
_switchCourseSession(courseSession);
|
||||
|
|
@ -161,64 +146,14 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
|
|||
return Boolean(isCourseExpert && (inLearningPath() || inCompetenceProfile()));
|
||||
});
|
||||
|
||||
const circleExperts = computed(() => {
|
||||
const circleStore = useCircleStore();
|
||||
const circleTranslationKey = circleStore.circle?.translation_key;
|
||||
|
||||
if (currentCourseSession.value && circleTranslationKey) {
|
||||
return currentCourseSession.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(() => {
|
||||
function hasCockpit(courseSession: CourseSession) {
|
||||
const userStore = useUserStore();
|
||||
return (
|
||||
circleExperts.value.filter((expert) => expert.user_id === userStore.id).length > 0
|
||||
);
|
||||
});
|
||||
|
||||
const circleDocuments = computed(() => {
|
||||
const circleStore = useCircleStore();
|
||||
|
||||
return (
|
||||
circleStore.circle?.learningSequences
|
||||
.map((ls) => ({ id: ls.id, title: ls.title, documents: [] }))
|
||||
.map((ls: { id: number; title: string; documents: CircleDocument[] }) => {
|
||||
if (currentCourseSession.value === undefined) {
|
||||
return ls;
|
||||
}
|
||||
|
||||
for (const document of currentCourseSession.value.documents) {
|
||||
if (document.learning_sequence === ls.id) {
|
||||
ls.documents.push(document);
|
||||
}
|
||||
}
|
||||
return ls;
|
||||
})
|
||||
.filter((ls) => ls.documents.length > 0) || []
|
||||
);
|
||||
});
|
||||
|
||||
function hasCockpit(couseSession: CourseSession) {
|
||||
const userStore = useUserStore();
|
||||
return (
|
||||
userStore.course_session_experts.includes(couseSession.id) ||
|
||||
userStore.course_session_experts.includes(courseSession.id) ||
|
||||
userStore.is_superuser
|
||||
);
|
||||
}
|
||||
|
||||
function addDocument(document: CircleDocument) {
|
||||
currentCourseSession.value?.documents.push(document);
|
||||
}
|
||||
|
||||
function allDueDates() {
|
||||
const allDueDatesReturn: DueDate[] = [];
|
||||
|
||||
|
|
@ -243,56 +178,6 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
|
|||
});
|
||||
}
|
||||
|
||||
async function startUpload() {
|
||||
log.debug("loadCourseSessionsData called");
|
||||
allCourseSessions.value = await itPost(`/api/core/file/start`, {
|
||||
file_type: "image/png",
|
||||
file_name: "test.png",
|
||||
});
|
||||
}
|
||||
|
||||
async function removeDocument(documentId: string) {
|
||||
await deleteCircleDocument(documentId);
|
||||
|
||||
if (currentCourseSession.value === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
currentCourseSession.value.documents = currentCourseSession.value?.documents.filter(
|
||||
(d) => d.id !== documentId
|
||||
);
|
||||
}
|
||||
|
||||
function findAttendanceCourse(
|
||||
contentId: number
|
||||
): CourseSessionAttendanceCourse | undefined {
|
||||
if (currentCourseSession.value) {
|
||||
return currentCourseSession.value.attendance_courses.find(
|
||||
(attendanceCourse) => attendanceCourse.learning_content_id === contentId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function findCourseSessionAssignment(
|
||||
contentId?: number
|
||||
): CourseSessionAssignment | undefined {
|
||||
if (contentId && currentCourseSession.value) {
|
||||
return currentCourseSession.value.assignments.find(
|
||||
(a) => a.learning_content_id === contentId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function findCourseSessionEdoniqTest(
|
||||
contentId?: number
|
||||
): CourseSessionEdoniqTest | undefined {
|
||||
if (contentId && currentCourseSession.value) {
|
||||
return currentCourseSession.value.edoniq_tests.find(
|
||||
(a) => a.learning_content_id === contentId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
uniqueCourseSessionsByCourse,
|
||||
allCurrentCourseSessions,
|
||||
|
|
@ -302,15 +187,6 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
|
|||
hasCockpit,
|
||||
hasCourseSessionPreview,
|
||||
currentCourseSessionHasCockpit,
|
||||
canUploadCircleDocuments,
|
||||
circleDocuments,
|
||||
circleExperts,
|
||||
addDocument,
|
||||
startUpload,
|
||||
removeDocument,
|
||||
findAttendanceCourse,
|
||||
findCourseSessionAssignment,
|
||||
findCourseSessionEdoniqTest,
|
||||
allDueDates,
|
||||
|
||||
// use `useCurrentCourseSession` whenever possible
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export type UserState = {
|
|||
username: string;
|
||||
avatar_url: string;
|
||||
is_superuser: boolean;
|
||||
course_session_experts: number[];
|
||||
course_session_experts: string[];
|
||||
loggedIn: boolean;
|
||||
language: AvailableLanguages;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ export type LoginMethod = "local" | "sso";
|
|||
export type CourseCompletionStatus = "UNKNOWN" | "FAIL" | "SUCCESS";
|
||||
|
||||
export interface BaseCourseWagtailPage {
|
||||
readonly id: number;
|
||||
readonly id: string;
|
||||
readonly title: string;
|
||||
readonly slug: string;
|
||||
readonly content_type: string;
|
||||
|
|
@ -18,9 +18,9 @@ export interface BaseCourseWagtailPage {
|
|||
}
|
||||
|
||||
export interface CircleLight {
|
||||
readonly id: number;
|
||||
readonly id: string;
|
||||
readonly slug: string;
|
||||
readonly title: string;
|
||||
readonly translation_key: string;
|
||||
}
|
||||
|
||||
export type LearningContent =
|
||||
|
|
@ -52,10 +52,10 @@ export interface LearningContentInterface extends BaseCourseWagtailPage {
|
|||
|
||||
export interface LearningContentAssignment extends LearningContentInterface {
|
||||
readonly content_type: "learnpath.LearningContentAssignment";
|
||||
readonly content_assignment_id: number;
|
||||
readonly content_assignment_id: string;
|
||||
readonly assignment_type: AssignmentType;
|
||||
readonly competence_certificate?: {
|
||||
id: number;
|
||||
id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
content_type: string;
|
||||
|
|
@ -104,9 +104,9 @@ export interface LearningContentEdoniqTest extends LearningContentInterface {
|
|||
readonly content_type: "learnpath.LearningContentEdoniqTest";
|
||||
readonly checkbox_text: string;
|
||||
readonly has_extended_time_test: boolean;
|
||||
readonly content_assignment_id: number;
|
||||
readonly content_assignment_id: string;
|
||||
readonly competence_certificate?: {
|
||||
id: number;
|
||||
id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
content_type: string;
|
||||
|
|
@ -191,22 +191,22 @@ export interface CourseCompletion {
|
|||
created_at: string;
|
||||
updated_at: string;
|
||||
readonly user: number;
|
||||
readonly page_id: number;
|
||||
readonly page_id: string;
|
||||
readonly page_type: string;
|
||||
readonly course_session_id: number;
|
||||
readonly course_session_id: string;
|
||||
completion_status: CourseCompletionStatus;
|
||||
additional_json_data: unknown;
|
||||
}
|
||||
|
||||
export interface Course {
|
||||
id: number;
|
||||
id: string;
|
||||
title: string;
|
||||
category_name: string;
|
||||
slug: string;
|
||||
}
|
||||
|
||||
export interface CourseCategory {
|
||||
id: number;
|
||||
id: string;
|
||||
name: string;
|
||||
general: boolean;
|
||||
}
|
||||
|
|
@ -381,7 +381,7 @@ export interface Assignment extends BaseCourseWagtailPage {
|
|||
readonly evaluation_tasks: AssignmentEvaluationTask[];
|
||||
readonly max_points: number;
|
||||
readonly competence_certificate?: {
|
||||
id: number;
|
||||
id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
content_type: string;
|
||||
|
|
@ -452,7 +452,7 @@ export interface CircleExpert {
|
|||
user_email: string;
|
||||
user_first_name: string;
|
||||
user_last_name: string;
|
||||
circle_id: number;
|
||||
circle_id: string;
|
||||
circle_slug: string;
|
||||
circle_translation_key: string;
|
||||
}
|
||||
|
|
@ -462,89 +462,112 @@ export interface CircleDocument {
|
|||
name: string;
|
||||
file_name: string;
|
||||
url: string;
|
||||
course_session: number;
|
||||
learning_sequence: number;
|
||||
learning_sequence: {
|
||||
id: string;
|
||||
title: string;
|
||||
circle: CircleLight;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CourseSessionAttendanceCourse {
|
||||
id: number;
|
||||
course_session_id: number;
|
||||
learning_content_id: number;
|
||||
start: string;
|
||||
end: string;
|
||||
id: string;
|
||||
due_date: SimpleDueDate;
|
||||
location: string;
|
||||
trainer: string;
|
||||
due_date_id: number;
|
||||
circle_title: string;
|
||||
learning_content: {
|
||||
id: string;
|
||||
title: string;
|
||||
circle: CircleLight;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CourseSessionAssignment {
|
||||
id: number;
|
||||
course_session_id: number;
|
||||
learning_content_id: number;
|
||||
submission_deadline_id: number;
|
||||
submission_deadline_start: string;
|
||||
evaluation_deadline_id: number;
|
||||
evaluation_deadline_start: string;
|
||||
id: string;
|
||||
submission_deadline: SimpleDueDate;
|
||||
evaluation_deadline: SimpleDueDate;
|
||||
learning_content: {
|
||||
id: string;
|
||||
content_assignment: {
|
||||
id: string;
|
||||
title: string;
|
||||
assignment_type: AssignmentType;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface CourseSessionEdoniqTest {
|
||||
id: number;
|
||||
course_session_id: number;
|
||||
learning_content_id: number;
|
||||
deadline_id: number;
|
||||
deadline_start: string;
|
||||
course_session_id: string;
|
||||
deadline: SimpleDueDate;
|
||||
learning_content: {
|
||||
id: string;
|
||||
content_assignment: {
|
||||
id: string;
|
||||
title: string;
|
||||
assignment_type: AssignmentType;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface CourseSession {
|
||||
id: number;
|
||||
id: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
course: Course;
|
||||
title: string;
|
||||
start_date: string;
|
||||
end_date: string;
|
||||
learning_path_url: string;
|
||||
cockpit_url: string;
|
||||
competence_url: string;
|
||||
course_url: string;
|
||||
media_library_url: string;
|
||||
attendance_courses: CourseSessionAttendanceCourse[];
|
||||
assignments: CourseSessionAssignment[];
|
||||
edoniq_tests: CourseSessionEdoniqTest[];
|
||||
documents: CircleDocument[];
|
||||
users: CourseSessionUser[];
|
||||
due_dates: DueDate[];
|
||||
}
|
||||
|
||||
export type Role = "MEMBER" | "EXPERT" | "TUTOR";
|
||||
|
||||
export interface CourseSessionUser {
|
||||
session_title: string;
|
||||
user_id: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
email: string;
|
||||
avatar_url: string;
|
||||
role: Role;
|
||||
circles: {
|
||||
id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
translation_key: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface ExpertSessionUser extends CourseSessionUser {
|
||||
role: "EXPERT";
|
||||
circles: {
|
||||
id: number;
|
||||
id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
translation_key: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface CourseSessionDetail {
|
||||
id: string;
|
||||
title: string;
|
||||
course: {
|
||||
id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
};
|
||||
assignments: CourseSessionAssignment[];
|
||||
attendance_courses: CourseSessionAttendanceCourse[];
|
||||
edoniq_tests: CourseSessionEdoniqTest[];
|
||||
users: CourseSessionUser[];
|
||||
}
|
||||
|
||||
// document upload
|
||||
export interface DocumentUploadData {
|
||||
file: File | null;
|
||||
name: string;
|
||||
learningSequence: {
|
||||
id: number;
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
|
@ -614,8 +637,8 @@ export interface AssignmentCompletion {
|
|||
}
|
||||
|
||||
export type UpsertUserAssignmentCompletion = {
|
||||
assignment_id: number;
|
||||
course_session_id: number;
|
||||
assignment_id: string;
|
||||
course_session_id: string;
|
||||
completion_status: AssignmentCompletionStatus;
|
||||
completion_data: AssignmentCompletionData;
|
||||
};
|
||||
|
|
@ -632,20 +655,22 @@ export interface UserAssignmentCompletionStatus {
|
|||
assignment_user_id: string;
|
||||
completion_status: AssignmentCompletionStatus;
|
||||
evaluation_points: number | null;
|
||||
learning_content_page_id: number;
|
||||
learning_content_page_id: string;
|
||||
}
|
||||
|
||||
export type DueDate = {
|
||||
id: number;
|
||||
export type SimpleDueDate = {
|
||||
id: string;
|
||||
start: string;
|
||||
end: string;
|
||||
end?: string;
|
||||
};
|
||||
|
||||
export type DueDate = SimpleDueDate & {
|
||||
title: string;
|
||||
assignment_type_translation_key: string;
|
||||
date_type_translation_key: string;
|
||||
subtitle: string;
|
||||
url: string;
|
||||
url_expert: string;
|
||||
course_session: number | null;
|
||||
page: number | null;
|
||||
course_session_id: string;
|
||||
circle: CircleLight | null;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import mitt from "mitt";
|
|||
export type MittEvents = {
|
||||
// event needed so that the App components do re-render
|
||||
// and reload the current course session
|
||||
switchedCourseSession: number;
|
||||
switchedCourseSession: string;
|
||||
|
||||
finishedLearningContent: boolean;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,16 +1,35 @@
|
|||
import type { AssignmentType, CourseSession } from "@/types";
|
||||
import type { AssignmentType } from "@/types";
|
||||
import { useTranslation } from "i18next-vue";
|
||||
|
||||
export function assertUnreachable(msg: string): never {
|
||||
throw new Error("Didn't expect to get here, " + msg);
|
||||
}
|
||||
|
||||
export function getCompetenceBaseUrl(courseSession: CourseSession): string {
|
||||
return courseSession.competence_url.replace(
|
||||
// TODO: remove the `competence_url` with url to Navi...
|
||||
"/competences",
|
||||
""
|
||||
);
|
||||
function createCourseUrl(courseSlug: string | undefined, specificSub: string): string {
|
||||
if (!courseSlug) {
|
||||
return "/";
|
||||
}
|
||||
|
||||
if (["learn", "media", "competence", "cockpit"].includes(specificSub)) {
|
||||
return `/course/${courseSlug}/${specificSub}`;
|
||||
}
|
||||
return `/course/${courseSlug}`;
|
||||
}
|
||||
|
||||
export function getCompetenceNaviUrl(courseSlug: string | undefined): string {
|
||||
return createCourseUrl(courseSlug, "competence");
|
||||
}
|
||||
|
||||
export function getMediaCenterUrl(courseSlug: string | undefined): string {
|
||||
return createCourseUrl(courseSlug, "media");
|
||||
}
|
||||
|
||||
export function getLearningPathUrl(courseSlug: string | undefined): string {
|
||||
return createCourseUrl(courseSlug, "learn");
|
||||
}
|
||||
|
||||
export function getCockpitUrl(courseSlug: string | undefined): string {
|
||||
return createCourseUrl(courseSlug, "cockpit");
|
||||
}
|
||||
|
||||
export function getAssignmentTypeTitle(assignmentType: AssignmentType): string {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,16 @@
|
|||
import {checkNavigationLink, EXPERT_LOGIN, login, PARTICIPANT_LOGIN, visitCoursePage} from "./helpers";
|
||||
|
||||
const openMobileNavigation = () => {
|
||||
cy.get('[data-cy="navigation-mobile-menu-button"]').click();
|
||||
}
|
||||
import {
|
||||
checkNavigationLink,
|
||||
EXPERT_LOGIN,
|
||||
login,
|
||||
PARTICIPANT_LOGIN,
|
||||
visitCoursePage,
|
||||
} from "./helpers";
|
||||
|
||||
describe("navigation.cy.js", () => {
|
||||
const openMobileNavigation = () => {
|
||||
cy.get('[data-cy="navigation-mobile-menu-button"]').click();
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
cy.manageCommand("cypress_reset");
|
||||
});
|
||||
|
|
@ -24,7 +30,9 @@ describe("navigation.cy.js", () => {
|
|||
checkNavigationLink("navigation-cockpit-link", "cockpit");
|
||||
checkNavigationLink("navigation-preview-link", "learn");
|
||||
|
||||
cy.get('[data-cy="navigation-competence-profile-link"]').should("not.exist");
|
||||
cy.get('[data-cy="navigation-competence-profile-link"]').should(
|
||||
"not.exist"
|
||||
);
|
||||
cy.get('[data-cy="navigation-learning-path-link"]').should("not.exist");
|
||||
});
|
||||
});
|
||||
|
|
@ -60,10 +68,14 @@ describe("navigation.cy.js", () => {
|
|||
it("should have correct expert navigation", () => {
|
||||
cy.get('[data-cy="navigation-mobile-cockpit-link"]').should("exist");
|
||||
cy.get('[data-cy="navigation-mobile-preview-link"]').should("exist");
|
||||
cy.get('[data-cy="navigation-mobile-competence-profile-link"]').should("not.exist");
|
||||
cy.get('[data-cy="navigation-mobile-learning-path-link"]').should("not.exist");
|
||||
})
|
||||
})
|
||||
cy.get('[data-cy="navigation-mobile-competence-profile-link"]').should(
|
||||
"not.exist"
|
||||
);
|
||||
cy.get('[data-cy="navigation-mobile-learning-path-link"]').should(
|
||||
"not.exist"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Participant", () => {
|
||||
beforeEach(() => {
|
||||
|
|
@ -73,12 +85,19 @@ describe("navigation.cy.js", () => {
|
|||
});
|
||||
|
||||
it("should have correct navigation", () => {
|
||||
cy.get('[data-cy="navigation-mobile-cockpit-link"]').should("not.exist");
|
||||
cy.get('[data-cy="navigation-mobile-preview-link"]').should("not.exist");
|
||||
cy.get('[data-cy="navigation-mobile-competence-profile-link"]').should("exist");
|
||||
cy.get('[data-cy="navigation-mobile-learning-path-link"]').should("exist");
|
||||
cy.get('[data-cy="navigation-mobile-cockpit-link"]').should(
|
||||
"not.exist"
|
||||
);
|
||||
cy.get('[data-cy="navigation-mobile-preview-link"]').should(
|
||||
"not.exist"
|
||||
);
|
||||
cy.get('[data-cy="navigation-mobile-competence-profile-link"]').should(
|
||||
"exist"
|
||||
);
|
||||
cy.get('[data-cy="navigation-mobile-learning-path-link"]').should(
|
||||
"exist"
|
||||
);
|
||||
});
|
||||
});
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,14 @@
|
|||
import {checkNavigationLink, login, PARTICIPANT_LOGIN, visitCoursePage} from "./helpers";
|
||||
import {
|
||||
checkNavigationLink,
|
||||
login,
|
||||
PARTICIPANT_LOGIN,
|
||||
visitCoursePage,
|
||||
} from "./helpers";
|
||||
|
||||
describe("preview.cy.js", () => {
|
||||
beforeEach(() => {
|
||||
cy.manageCommand("cypress_reset");
|
||||
})
|
||||
});
|
||||
|
||||
describe("Expert / Trainer", () => {
|
||||
beforeEach(() => {
|
||||
|
|
@ -41,5 +46,5 @@ describe("preview.cy.js", () => {
|
|||
visitCoursePage("competence");
|
||||
cy.get('[data-cy="course-preview-bar"]').should("not.exist");
|
||||
});
|
||||
})
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -31,12 +31,12 @@ from vbv_lernwelt.course.views import (
|
|||
document_direct_upload,
|
||||
document_upload_finish,
|
||||
document_upload_start,
|
||||
get_course_session_users,
|
||||
get_course_sessions,
|
||||
mark_course_completion_view,
|
||||
request_course_completion,
|
||||
request_course_completion_for_user,
|
||||
)
|
||||
from vbv_lernwelt.course_session.views import get_course_session_documents
|
||||
from vbv_lernwelt.edoniq_test.views import (
|
||||
export_students,
|
||||
export_students_and_trainers,
|
||||
|
|
@ -90,6 +90,10 @@ urlpatterns = [
|
|||
path('server/documents/', include(wagtaildocs_urls)),
|
||||
path('server/pages/', include(wagtail_urls)),
|
||||
|
||||
# core
|
||||
re_path(r"server/core/icons/$", generate_web_component_icons,
|
||||
name="generate_web_component_icons"),
|
||||
|
||||
# user management
|
||||
path("sso/", include("vbv_lernwelt.sso.urls")),
|
||||
re_path(r'api/core/me/$', me_user_view, name='me_user_view'),
|
||||
|
|
@ -104,15 +108,11 @@ urlpatterns = [
|
|||
re_path(r"api/notify/email_notification_settings/$", email_notification_settings,
|
||||
name='email_notification_settings'),
|
||||
|
||||
# core
|
||||
re_path(r"server/core/icons/$", generate_web_component_icons,
|
||||
name="generate_web_component_icons"),
|
||||
|
||||
# course
|
||||
path(r"api/course/sessions/", get_course_sessions, name="get_course_sessions"),
|
||||
path(r"api/course/sessions/<signed_int:course_session_id>/users/",
|
||||
get_course_session_users,
|
||||
name="get_course_session_users"),
|
||||
# path(r"api/course/sessions/<signed_int:course_session_id>/users/",
|
||||
# get_course_session_users,
|
||||
# name="get_course_session_users"),
|
||||
path(r"api/course/page/<slug_or_id>/", course_page_api_view,
|
||||
name="course_page_api_view"),
|
||||
path(r"api/course/completion/mark/", mark_course_completion_view,
|
||||
|
|
@ -139,6 +139,9 @@ urlpatterns = [
|
|||
name='file_upload_finish'),
|
||||
path(r"api/core/document/local/<str:file_id>/", document_direct_upload,
|
||||
name='file_upload_local'),
|
||||
path(r'api/core/document/list/<str:course_session_id>/',
|
||||
get_course_session_documents,
|
||||
name='get_course_session_documents'),
|
||||
|
||||
# feedback
|
||||
path(r'api/core/feedback/<str:course_session_id>/summary/',
|
||||
|
|
|
|||
|
|
@ -24,6 +24,12 @@ def request_assignment_completion_status(request, assignment_id, course_session_
|
|||
"evaluation_passed",
|
||||
"learning_content_page_id",
|
||||
)
|
||||
return Response(status=200, data=qs)
|
||||
|
||||
# Convert the learning_content_page_id to a string
|
||||
data = list(qs) # Evaluate the queryset
|
||||
for item in data:
|
||||
item["learning_content_page_id"] = str(item["learning_content_page_id"])
|
||||
|
||||
return Response(status=200, data=data)
|
||||
|
||||
raise PermissionDenied()
|
||||
|
|
|
|||
|
|
@ -40,4 +40,4 @@ class UserSerializer(serializers.ModelSerializer):
|
|||
role=CourseSessionUser.Role.EXPERT, user=obj
|
||||
)
|
||||
|
||||
return [csu.course_session.id for csu in qs]
|
||||
return [str(csu.course_session.id) for csu in qs]
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import json
|
|||
import re
|
||||
|
||||
from django.utils.safestring import mark_safe
|
||||
from rest_framework import serializers
|
||||
from rest_framework.throttling import UserRateThrottle
|
||||
|
||||
|
||||
|
|
@ -41,6 +42,11 @@ def get_django_content_type(obj):
|
|||
return obj._meta.app_label + "." + type(obj).__name__
|
||||
|
||||
|
||||
class StringIDField(serializers.Field):
|
||||
def to_representation(self, value):
|
||||
return str(value)
|
||||
|
||||
|
||||
def pretty_print_json(json_string):
|
||||
try:
|
||||
parsed = json_string
|
||||
|
|
|
|||
|
|
@ -1,15 +1,22 @@
|
|||
import graphene
|
||||
|
||||
from vbv_lernwelt.course.graphql.types import CourseObjectType
|
||||
from vbv_lernwelt.course.models import Course
|
||||
from vbv_lernwelt.course.graphql.types import CourseObjectType, CourseSessionObjectType
|
||||
from vbv_lernwelt.course.models import Course, CourseSession
|
||||
from vbv_lernwelt.course.permissions import has_course_access
|
||||
|
||||
|
||||
class CourseQuery(graphene.ObjectType):
|
||||
course = graphene.Field(CourseObjectType, id=graphene.Int())
|
||||
course = graphene.Field(CourseObjectType, id=graphene.ID())
|
||||
course_session = graphene.Field(CourseSessionObjectType, id=graphene.ID())
|
||||
|
||||
def resolve_course(root, info, id):
|
||||
course = Course.objects.get(pk=id)
|
||||
if has_course_access(info.context.user, course):
|
||||
return course
|
||||
raise PermissionError("You do not have access to this course")
|
||||
|
||||
def resolve_course_session(root, info, id):
|
||||
course_session = CourseSession.objects.get(pk=id)
|
||||
if has_course_access(info.context.user, course_session.course):
|
||||
return course_session
|
||||
raise PermissionError("You do not have access to this course session")
|
||||
|
|
|
|||
|
|
@ -2,12 +2,30 @@ from typing import Type
|
|||
|
||||
import graphene
|
||||
import structlog
|
||||
from graphene import ObjectType
|
||||
from graphene_django import DjangoObjectType
|
||||
from graphql import GraphQLError
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
|
||||
from vbv_lernwelt.course.models import Course, CourseBasePage, CoursePage
|
||||
from vbv_lernwelt.course.models import (
|
||||
CircleDocument,
|
||||
Course,
|
||||
CourseBasePage,
|
||||
CoursePage,
|
||||
CourseSession,
|
||||
CourseSessionUser,
|
||||
)
|
||||
from vbv_lernwelt.course.permissions import has_course_access
|
||||
from vbv_lernwelt.course_session.graphql.types import (
|
||||
CourseSessionAssignmentObjectType,
|
||||
CourseSessionAttendanceCourseObjectType,
|
||||
CourseSessionEdoniqTestObjectType,
|
||||
)
|
||||
from vbv_lernwelt.course_session.models import (
|
||||
CourseSessionAssignment,
|
||||
CourseSessionAttendanceCourse,
|
||||
CourseSessionEdoniqTest,
|
||||
)
|
||||
from vbv_lernwelt.learnpath.graphql.types import LearningPathObjectType
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
|
@ -74,3 +92,113 @@ class CourseObjectType(DjangoObjectType):
|
|||
|
||||
def resolve_learning_path(self, info):
|
||||
return self.get_learning_path()
|
||||
|
||||
|
||||
class CourseSessionUserExpertCircleType(ObjectType):
|
||||
id = graphene.ID()
|
||||
title = graphene.String()
|
||||
slug = graphene.String()
|
||||
|
||||
|
||||
class CourseSessionUserObjectsType(DjangoObjectType):
|
||||
user_id = graphene.UUID()
|
||||
first_name = graphene.String()
|
||||
last_name = graphene.String()
|
||||
email = graphene.String()
|
||||
avatar_url = graphene.String()
|
||||
role = graphene.String()
|
||||
circles = graphene.List(CourseSessionUserExpertCircleType)
|
||||
|
||||
class Meta:
|
||||
model = CourseSessionUser
|
||||
fields = (
|
||||
"id",
|
||||
"user_id",
|
||||
"first_name",
|
||||
"last_name",
|
||||
"email",
|
||||
"avatar_url",
|
||||
"role",
|
||||
)
|
||||
|
||||
def resolve_user_id(self, info):
|
||||
return self.user.id
|
||||
|
||||
def resolve_first_name(self, info):
|
||||
return self.user.first_name
|
||||
|
||||
def resolve_last_name(self, info):
|
||||
return self.user.last_name
|
||||
|
||||
def resolve_email(self, info):
|
||||
return self.user.email
|
||||
|
||||
def resolve_avatar_url(self, info):
|
||||
return self.user.avatar_url
|
||||
|
||||
def resolve_role(self, info):
|
||||
return self.role
|
||||
|
||||
def resolve_circles(self, info):
|
||||
return self.expert.all().values("id", "title", "slug", "translation_key")
|
||||
|
||||
|
||||
class CircleDocumentObjectType(DjangoObjectType):
|
||||
file_name = graphene.String()
|
||||
url = graphene.String()
|
||||
|
||||
class Meta:
|
||||
model = CircleDocument
|
||||
fields = [
|
||||
"id",
|
||||
"name",
|
||||
"file_name",
|
||||
"url",
|
||||
"course_session",
|
||||
"learning_sequence",
|
||||
]
|
||||
|
||||
def resolve_file_name(self, info):
|
||||
return self.file_name
|
||||
|
||||
def resolve_url(self, info):
|
||||
return self.url
|
||||
|
||||
|
||||
class CourseSessionObjectType(DjangoObjectType):
|
||||
attendance_courses = graphene.List(CourseSessionAttendanceCourseObjectType)
|
||||
assignments = graphene.List(CourseSessionAssignmentObjectType)
|
||||
edoniq_tests = graphene.List(CourseSessionEdoniqTestObjectType)
|
||||
documents = graphene.List(CircleDocumentObjectType)
|
||||
|
||||
users = graphene.List(CourseSessionUserObjectsType)
|
||||
|
||||
class Meta:
|
||||
model = CourseSession
|
||||
fields = (
|
||||
"id",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"course",
|
||||
"title",
|
||||
"start_date",
|
||||
"end_date",
|
||||
"users",
|
||||
)
|
||||
|
||||
def resolve_attendance_courses(self, info):
|
||||
return CourseSessionAttendanceCourse.objects.filter(course_session=self)
|
||||
|
||||
def resolve_assignments(self, info):
|
||||
return CourseSessionAssignment.objects.filter(course_session=self)
|
||||
|
||||
def resolve_edoniq_tests(self, info):
|
||||
return CourseSessionEdoniqTest.objects.filter(course_session=self)
|
||||
|
||||
def resolve_documents(self, info):
|
||||
return CircleDocument.objects.filter(
|
||||
course_session=self, file__upload_finished_at__isnull=False
|
||||
)
|
||||
|
||||
def resolve_users(self, info):
|
||||
return CourseSessionUser.objects.filter(course_session_id=self.id).distinct()
|
||||
|
|
|
|||
|
|
@ -294,21 +294,6 @@ class CourseSessionUser(models.Model):
|
|||
]
|
||||
ordering = ["user__last_name", "user__first_name", "user__email"]
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"session_id": self.course_session.id,
|
||||
"session_title": self.course_session.title,
|
||||
"user_id": self.user.id,
|
||||
"first_name": self.user.first_name,
|
||||
"last_name": self.user.last_name,
|
||||
"email": self.user.email,
|
||||
"avatar_url": self.user.avatar_url,
|
||||
"role": self.role,
|
||||
"circles": self.expert.all().values(
|
||||
"id", "title", "slug", "translation_key"
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class CircleDocument(models.Model):
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
|
|
@ -322,6 +307,9 @@ class CircleDocument(models.Model):
|
|||
"learnpath.LearningSequence", on_delete=models.CASCADE
|
||||
)
|
||||
|
||||
def get_circle(self):
|
||||
return self.learning_sequence.get_circle()
|
||||
|
||||
@property
|
||||
def url(self) -> str:
|
||||
return self.file.url
|
||||
|
|
|
|||
|
|
@ -4,9 +4,12 @@ from vbv_lernwelt.core.serializer_helpers import (
|
|||
get_it_serializer_class,
|
||||
ItWagtailBaseSerializer,
|
||||
)
|
||||
from vbv_lernwelt.core.utils import StringIDField
|
||||
|
||||
|
||||
class CourseBaseSerializer(ItWagtailBaseSerializer):
|
||||
id = StringIDField()
|
||||
content_assignment_id = StringIDField()
|
||||
course = SerializerMethodField()
|
||||
course_category = SerializerMethodField()
|
||||
circles = SerializerMethodField()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from vbv_lernwelt.core.utils import StringIDField
|
||||
from vbv_lernwelt.course.models import (
|
||||
CircleDocument,
|
||||
Course,
|
||||
|
|
@ -7,21 +8,13 @@ from vbv_lernwelt.course.models import (
|
|||
CourseCompletion,
|
||||
CourseSession,
|
||||
)
|
||||
from vbv_lernwelt.course_session.models import (
|
||||
CourseSessionAssignment,
|
||||
CourseSessionAttendanceCourse,
|
||||
CourseSessionEdoniqTest,
|
||||
)
|
||||
from vbv_lernwelt.course_session.serializers import (
|
||||
CourseSessionAssignmentSerializer,
|
||||
CourseSessionAttendanceCourseSerializer,
|
||||
CourseSessionEdoniqTestSerializer,
|
||||
)
|
||||
from vbv_lernwelt.duedate.models import DueDate
|
||||
from vbv_lernwelt.duedate.serializers import DueDateSerializer
|
||||
|
||||
|
||||
class CourseSerializer(serializers.ModelSerializer):
|
||||
id = StringIDField()
|
||||
|
||||
class Meta:
|
||||
model = Course
|
||||
fields = ["id", "title", "category_name", "slug"]
|
||||
|
|
@ -38,6 +31,9 @@ class CourseCategorySerializer(serializers.ModelSerializer):
|
|||
|
||||
|
||||
class CourseCompletionSerializer(serializers.ModelSerializer):
|
||||
page_id = StringIDField()
|
||||
course_session_id = StringIDField()
|
||||
|
||||
class Meta:
|
||||
model = CourseCompletion
|
||||
fields = [
|
||||
|
|
@ -54,56 +50,17 @@ class CourseCompletionSerializer(serializers.ModelSerializer):
|
|||
|
||||
|
||||
class CourseSessionSerializer(serializers.ModelSerializer):
|
||||
id = StringIDField()
|
||||
|
||||
course = serializers.SerializerMethodField()
|
||||
course_url = serializers.SerializerMethodField()
|
||||
learning_path_url = serializers.SerializerMethodField()
|
||||
cockpit_url = serializers.SerializerMethodField()
|
||||
competence_url = serializers.SerializerMethodField()
|
||||
media_library_url = serializers.SerializerMethodField()
|
||||
documents = serializers.SerializerMethodField()
|
||||
attendance_courses = serializers.SerializerMethodField()
|
||||
assignments = serializers.SerializerMethodField()
|
||||
edoniq_tests = serializers.SerializerMethodField()
|
||||
# course_url = serializers.SerializerMethodField()
|
||||
due_dates = serializers.SerializerMethodField()
|
||||
|
||||
def get_course(self, obj):
|
||||
return CourseSerializer(obj.course).data
|
||||
|
||||
def get_course_url(self, obj):
|
||||
return obj.course.get_course_url()
|
||||
|
||||
def get_learning_path_url(self, obj):
|
||||
return obj.course.get_learning_path_url()
|
||||
|
||||
def get_cockpit_url(self, obj):
|
||||
return obj.course.get_cockpit_url()
|
||||
|
||||
def get_media_library_url(self, obj):
|
||||
return obj.course.get_media_library_url()
|
||||
|
||||
def get_competence_url(self, obj):
|
||||
return obj.course.get_competence_url()
|
||||
|
||||
def get_documents(self, obj):
|
||||
documents = CircleDocument.objects.filter(
|
||||
course_session=obj, file__upload_finished_at__isnull=False
|
||||
)
|
||||
return CircleDocumentSerializer(documents, many=True).data
|
||||
|
||||
def get_attendance_courses(self, obj):
|
||||
return CourseSessionAttendanceCourseSerializer(
|
||||
CourseSessionAttendanceCourse.objects.filter(course_session=obj), many=True
|
||||
).data
|
||||
|
||||
def get_assignments(self, obj):
|
||||
return CourseSessionAssignmentSerializer(
|
||||
CourseSessionAssignment.objects.filter(course_session=obj), many=True
|
||||
).data
|
||||
|
||||
def get_edoniq_tests(self, obj):
|
||||
return CourseSessionEdoniqTestSerializer(
|
||||
CourseSessionEdoniqTest.objects.filter(course_session=obj), many=True
|
||||
).data
|
||||
# def get_course_url(self, obj):
|
||||
# return obj.course.get_course_url()
|
||||
|
||||
def get_due_dates(self, obj):
|
||||
due_dates = DueDate.objects.filter(course_session=obj)
|
||||
|
|
@ -119,21 +76,13 @@ class CourseSessionSerializer(serializers.ModelSerializer):
|
|||
"title",
|
||||
"start_date",
|
||||
"end_date",
|
||||
"additional_json_data",
|
||||
"attendance_courses",
|
||||
"assignments",
|
||||
"edoniq_tests",
|
||||
"learning_path_url",
|
||||
"cockpit_url",
|
||||
"competence_url",
|
||||
"media_library_url",
|
||||
"course_url",
|
||||
"documents",
|
||||
"due_dates",
|
||||
]
|
||||
|
||||
|
||||
class CircleDocumentSerializer(serializers.ModelSerializer):
|
||||
learning_sequence = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = CircleDocument
|
||||
fields = [
|
||||
|
|
@ -145,6 +94,20 @@ class CircleDocumentSerializer(serializers.ModelSerializer):
|
|||
"learning_sequence",
|
||||
]
|
||||
|
||||
def get_learning_sequence(self, obj):
|
||||
ls = obj.learning_sequence
|
||||
circle = ls.get_circle()
|
||||
return {
|
||||
"title": ls.title,
|
||||
"id": str(ls.id),
|
||||
"slug": ls.slug,
|
||||
"circle": {
|
||||
"title": circle.title,
|
||||
"id": str(circle.id),
|
||||
"slug": circle.slug,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class DocumentUploadStartInputSerializer(serializers.Serializer):
|
||||
file_name = serializers.CharField()
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ class CourseCompletionApiTestCase(APITestCase):
|
|||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(len(response_json), 1)
|
||||
self.assertEqual(response_json[0]["page_id"], learning_content.id)
|
||||
self.assertEqual(response_json[0]["page_id"], str(learning_content.id))
|
||||
self.assertEqual(
|
||||
response_json[0]["page_type"], "learnpath.LearningContentPlaceholder"
|
||||
)
|
||||
|
|
@ -67,7 +67,7 @@ class CourseCompletionApiTestCase(APITestCase):
|
|||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(len(response_json), 1)
|
||||
self.assertEqual(response_json[0]["page_id"], learning_content.id)
|
||||
self.assertEqual(response_json[0]["page_id"], str(learning_content.id))
|
||||
self.assertEqual(
|
||||
response_json[0]["page_type"], "learnpath.LearningContentPlaceholder"
|
||||
)
|
||||
|
|
@ -86,7 +86,7 @@ class CourseCompletionApiTestCase(APITestCase):
|
|||
response_json = response.json()
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(len(response_json), 1)
|
||||
self.assertEqual(response_json[0]["page_id"], learning_content.id)
|
||||
self.assertEqual(response_json[0]["page_id"], str(learning_content.id))
|
||||
self.assertEqual(
|
||||
response_json[0]["page_type"], "learnpath.LearningContentPlaceholder"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ class CourseCompletionApiTestCase(APITestCase):
|
|||
self.assertEqual(len(response.json()), 1)
|
||||
|
||||
print(json.dumps(response.json(), indent=4))
|
||||
self.assertEqual(response.json()[0]["id"], self.course_session.id)
|
||||
self.assertEqual(response.json()[0]["id"], str(self.course_session.id))
|
||||
|
||||
def test_api_superUser_canAccessEveryCourseSession(self):
|
||||
self.client.login(username="admin", password="test")
|
||||
|
|
@ -50,4 +50,4 @@ class CourseCompletionApiTestCase(APITestCase):
|
|||
self.assertEqual(len(response.json()), 1)
|
||||
|
||||
print(json.dumps(response.json(), indent=4))
|
||||
self.assertEqual(response.json()[0]["id"], self.course_session.id)
|
||||
self.assertEqual(response.json()[0]["id"], str(self.course_session.id))
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ from rest_framework.response import Response
|
|||
from wagtail.models import Page
|
||||
|
||||
from vbv_lernwelt.core.utils import get_django_content_type
|
||||
|
||||
from vbv_lernwelt.course.models import (
|
||||
CircleDocument,
|
||||
CourseCompletion,
|
||||
|
|
@ -135,7 +134,9 @@ def mark_course_completion_view(request):
|
|||
@api_view(["GET"])
|
||||
def get_course_sessions(request):
|
||||
try:
|
||||
course_sessions = course_sessions_for_user_qs(request.user)
|
||||
course_sessions = course_sessions_for_user_qs(request.user).prefetch_related(
|
||||
"course"
|
||||
)
|
||||
return Response(
|
||||
status=200, data=CourseSessionSerializer(course_sessions, many=True).data
|
||||
)
|
||||
|
|
@ -146,20 +147,6 @@ def get_course_sessions(request):
|
|||
return Response({"error": str(e)}, status=404)
|
||||
|
||||
|
||||
@api_view(["GET"])
|
||||
def get_course_session_users(request, course_session_id):
|
||||
try:
|
||||
qs = CourseSessionUser.objects.filter(course_session_id=course_session_id)
|
||||
|
||||
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:
|
||||
logger.error(e)
|
||||
return Response({"error": str(e)}, status=404)
|
||||
|
||||
|
||||
@api_view(["POST"])
|
||||
def document_upload_start(request):
|
||||
serializer = DocumentUploadStartInputSerializer(data=request.data)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@ import structlog
|
|||
from rest_framework.exceptions import PermissionDenied
|
||||
|
||||
from vbv_lernwelt.course.permissions import has_course_access
|
||||
from vbv_lernwelt.course_session.graphql.types import CourseSessionAttendanceCourseType
|
||||
from vbv_lernwelt.course_session.graphql.types import (
|
||||
CourseSessionAttendanceCourseObjectType,
|
||||
)
|
||||
from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse
|
||||
from vbv_lernwelt.course_session.services.attendance import (
|
||||
AttendanceUserStatus,
|
||||
|
|
@ -21,7 +23,9 @@ class AttendanceUserInputType(graphene.InputObjectType):
|
|||
|
||||
|
||||
class AttendanceCourseUserMutation(graphene.Mutation):
|
||||
course_session_attendance_course = graphene.Field(CourseSessionAttendanceCourseType)
|
||||
course_session_attendance_course = graphene.Field(
|
||||
CourseSessionAttendanceCourseObjectType
|
||||
)
|
||||
|
||||
class Input:
|
||||
id = graphene.ID(required=True)
|
||||
|
|
|
|||
|
|
@ -3,13 +3,15 @@ from rest_framework.exceptions import PermissionDenied
|
|||
|
||||
from vbv_lernwelt.course.models import CourseSession
|
||||
from vbv_lernwelt.course.permissions import has_course_access, is_course_session_expert
|
||||
from vbv_lernwelt.course_session.graphql.types import CourseSessionAttendanceCourseType
|
||||
from vbv_lernwelt.course_session.graphql.types import (
|
||||
CourseSessionAttendanceCourseObjectType,
|
||||
)
|
||||
from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse
|
||||
|
||||
|
||||
class CourseSessionQuery(object):
|
||||
course_session_attendance_course = graphene.Field(
|
||||
CourseSessionAttendanceCourseType,
|
||||
CourseSessionAttendanceCourseObjectType,
|
||||
id=graphene.ID(required=True),
|
||||
assignment_user_id=graphene.ID(required=False),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,22 @@
|
|||
import graphene
|
||||
from graphene_django import DjangoObjectType
|
||||
|
||||
from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse
|
||||
from vbv_lernwelt.course.permissions import is_course_session_expert
|
||||
from vbv_lernwelt.course_session.models import (
|
||||
CourseSessionAssignment,
|
||||
CourseSessionAttendanceCourse,
|
||||
CourseSessionEdoniqTest,
|
||||
)
|
||||
from vbv_lernwelt.course_session.services.attendance import AttendanceUserStatus
|
||||
from vbv_lernwelt.duedate.graphql.types import DueDateObjectType
|
||||
from vbv_lernwelt.learnpath.graphql.types import (
|
||||
LearningContentAssignmentObjectType,
|
||||
LearningContentAttendanceCourseObjectType,
|
||||
LearningContentEdoniqTestObjectType,
|
||||
)
|
||||
|
||||
|
||||
class AttendanceUserType(graphene.ObjectType):
|
||||
class AttendanceUserObjectType(graphene.ObjectType):
|
||||
user_id = graphene.UUID(required=True)
|
||||
status = graphene.Field(
|
||||
graphene.Enum.from_enum(AttendanceUserStatus), required=True
|
||||
|
|
@ -15,35 +26,61 @@ class AttendanceUserType(graphene.ObjectType):
|
|||
email = graphene.String()
|
||||
|
||||
|
||||
class CourseSessionAttendanceCourseType(DjangoObjectType):
|
||||
class CourseSessionAttendanceCourseObjectType(DjangoObjectType):
|
||||
course_session_id = graphene.ID(source="course_session_id")
|
||||
learning_content_id = graphene.ID(source="learning_content_id")
|
||||
due_date_id = graphene.ID(source="due_date_id")
|
||||
end = graphene.DateTime()
|
||||
start = graphene.DateTime()
|
||||
attendance_user_list = graphene.List(
|
||||
AttendanceUserType, source="attendance_user_list"
|
||||
AttendanceUserObjectType, source="attendance_user_list"
|
||||
)
|
||||
due_date = graphene.Field(DueDateObjectType)
|
||||
learning_content = graphene.Field(LearningContentAttendanceCourseObjectType)
|
||||
|
||||
class Meta:
|
||||
model = CourseSessionAttendanceCourse
|
||||
fields = (
|
||||
"id",
|
||||
"course_session_id",
|
||||
"learning_content_id",
|
||||
"due_date_id",
|
||||
"learning_content",
|
||||
"location",
|
||||
"trainer",
|
||||
"start",
|
||||
"end",
|
||||
"due_date",
|
||||
)
|
||||
|
||||
def resolve_start(self, info):
|
||||
if self.due_date is None:
|
||||
return None
|
||||
return self.due_date.start
|
||||
def resolve_attendance_user_list(self, info):
|
||||
if is_course_session_expert(info.context.user, self.course_session_id):
|
||||
return self.attendance_user_list
|
||||
return []
|
||||
|
||||
def resolve_end(self, info):
|
||||
if self.due_date is None:
|
||||
return None
|
||||
return self.due_date.end
|
||||
|
||||
class CourseSessionAssignmentObjectType(DjangoObjectType):
|
||||
course_session_id = graphene.ID(source="course_session_id")
|
||||
learning_content_id = graphene.ID(source="learning_content_id")
|
||||
submission_deadline = graphene.Field(DueDateObjectType)
|
||||
evaluation_deadline = graphene.Field(DueDateObjectType)
|
||||
learning_content = graphene.Field(LearningContentAssignmentObjectType)
|
||||
|
||||
class Meta:
|
||||
model = CourseSessionAssignment
|
||||
fields = (
|
||||
"id",
|
||||
"course_session_id",
|
||||
"learning_content",
|
||||
"submission_deadline",
|
||||
"evaluation_deadline",
|
||||
)
|
||||
|
||||
|
||||
class CourseSessionEdoniqTestObjectType(DjangoObjectType):
|
||||
course_session_id = graphene.ID(source="course_session_id")
|
||||
learning_content_id = graphene.ID(source="learning_content_id")
|
||||
deadline = graphene.Field(DueDateObjectType)
|
||||
learning_content = graphene.Field(LearningContentEdoniqTestObjectType)
|
||||
|
||||
class Meta:
|
||||
model = CourseSessionEdoniqTest
|
||||
fields = (
|
||||
"id",
|
||||
"course_session_id",
|
||||
"learning_content",
|
||||
"deadline",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,82 +1 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from vbv_lernwelt.course_session.models import (
|
||||
CourseSessionAssignment,
|
||||
CourseSessionAttendanceCourse,
|
||||
CourseSessionEdoniqTest,
|
||||
)
|
||||
|
||||
|
||||
class CourseSessionAttendanceCourseSerializer(serializers.ModelSerializer):
|
||||
start = serializers.SerializerMethodField()
|
||||
end = serializers.SerializerMethodField()
|
||||
circle_title = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = CourseSessionAttendanceCourse
|
||||
fields = [
|
||||
"id",
|
||||
"course_session",
|
||||
"learning_content_id",
|
||||
"due_date_id",
|
||||
"location",
|
||||
"trainer",
|
||||
"start",
|
||||
"end",
|
||||
"circle_title",
|
||||
]
|
||||
|
||||
def get_start(self, obj):
|
||||
return obj.due_date.start
|
||||
|
||||
def get_end(self, obj):
|
||||
return obj.due_date.end
|
||||
|
||||
def get_circle_title(self, obj):
|
||||
circle = obj.get_circle()
|
||||
if circle:
|
||||
return circle.title
|
||||
return ""
|
||||
|
||||
|
||||
class CourseSessionAssignmentSerializer(serializers.ModelSerializer):
|
||||
submission_deadline_start = serializers.SerializerMethodField()
|
||||
evaluation_deadline_start = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = CourseSessionAssignment
|
||||
fields = [
|
||||
"id",
|
||||
"course_session_id",
|
||||
"learning_content_id",
|
||||
"submission_deadline_id",
|
||||
"submission_deadline_start",
|
||||
"evaluation_deadline_id",
|
||||
"evaluation_deadline_start",
|
||||
]
|
||||
|
||||
def get_evaluation_deadline_start(self, obj):
|
||||
if obj.evaluation_deadline:
|
||||
return obj.evaluation_deadline.start
|
||||
|
||||
def get_submission_deadline_start(self, obj):
|
||||
if obj.submission_deadline:
|
||||
return obj.submission_deadline.start
|
||||
|
||||
|
||||
class CourseSessionEdoniqTestSerializer(serializers.ModelSerializer):
|
||||
deadline_start = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = CourseSessionEdoniqTest
|
||||
fields = [
|
||||
"id",
|
||||
"course_session_id",
|
||||
"learning_content_id",
|
||||
"deadline_id",
|
||||
"deadline_start",
|
||||
]
|
||||
|
||||
def get_deadline_start(self, obj):
|
||||
if obj.deadline:
|
||||
return obj.deadline.start
|
||||
|
|
|
|||
|
|
@ -1,3 +1,21 @@
|
|||
from django.shortcuts import render
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework.response import Response
|
||||
|
||||
# Create your views here.
|
||||
from vbv_lernwelt.course.models import CircleDocument
|
||||
from vbv_lernwelt.course.permissions import has_course_session_access
|
||||
from vbv_lernwelt.course.serializers import CircleDocumentSerializer
|
||||
|
||||
|
||||
@api_view(["GET"])
|
||||
def get_course_session_documents(request, course_session_id):
|
||||
if not has_course_session_access(request.user, course_session_id):
|
||||
raise PermissionDenied()
|
||||
|
||||
circle_documents = CircleDocument.objects.filter(
|
||||
course_session_id=course_session_id
|
||||
)
|
||||
|
||||
return Response(
|
||||
status=200, data=CircleDocumentSerializer(circle_documents, many=True).data
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
from graphene_django import DjangoObjectType
|
||||
|
||||
from vbv_lernwelt.duedate.models import DueDate
|
||||
|
||||
|
||||
class DueDateObjectType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = DueDate
|
||||
fields = (
|
||||
"id",
|
||||
"start",
|
||||
"end",
|
||||
"manual_override_fields",
|
||||
"title",
|
||||
"assignment_type_translation_key",
|
||||
"date_type_translation_key",
|
||||
"subtitle",
|
||||
"url",
|
||||
"course_session",
|
||||
"page",
|
||||
)
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
# Generated by Django 3.2.20 on 2023-10-10 13:05
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("duedate", "0005_auto_20230925_1648"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="duedate",
|
||||
name="circle_data",
|
||||
field=models.JSONField(blank=True, default=dict),
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
# Generated by Django 3.2.20 on 2023-10-10 13:05
|
||||
|
||||
from django.db import migrations
|
||||
from django.db.migrations import RunPython
|
||||
|
||||
|
||||
def trigger_due_date_save(apps, schema_editor):
|
||||
# need to load concrete model, so that wagtail page has `specific` instance method...
|
||||
from vbv_lernwelt.duedate.models import DueDate
|
||||
|
||||
for due_date in DueDate.objects.all():
|
||||
# trigger save to prefetch circle data
|
||||
due_date.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("duedate", "0006_duedate_circle_data"),
|
||||
]
|
||||
|
||||
operations = [RunPython(trigger_due_date_save)]
|
||||
|
|
@ -60,6 +60,7 @@ class DueDate(models.Model):
|
|||
)
|
||||
|
||||
page = models.ForeignKey(Page, on_delete=models.SET_NULL, null=True, blank=True)
|
||||
circle_data = models.JSONField(default=dict, blank=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ["start", "end"]
|
||||
|
|
@ -80,6 +81,21 @@ class DueDate(models.Model):
|
|||
|
||||
return result
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
try:
|
||||
circle = self.get_circle()
|
||||
# prefetch circle data, because loading it dynamically is too slow
|
||||
if circle:
|
||||
self.circle_data = {
|
||||
"id": str(circle.id),
|
||||
"title": circle.title,
|
||||
"slug": circle.slug,
|
||||
}
|
||||
except Exception:
|
||||
# noop
|
||||
pass
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def display_subtitle(self):
|
||||
result = ""
|
||||
|
|
|
|||
|
|
@ -1,14 +1,18 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from vbv_lernwelt.core.utils import StringIDField
|
||||
from vbv_lernwelt.duedate.models import DueDate
|
||||
|
||||
|
||||
class DueDateSerializer(serializers.ModelSerializer):
|
||||
id = StringIDField()
|
||||
course_session_id = serializers.SerializerMethodField()
|
||||
circle = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = DueDate
|
||||
fields = [
|
||||
"id",
|
||||
"start",
|
||||
"end",
|
||||
"manual_override_fields",
|
||||
|
|
@ -18,18 +22,18 @@ class DueDateSerializer(serializers.ModelSerializer):
|
|||
"subtitle",
|
||||
"url",
|
||||
"url_expert",
|
||||
"course_session",
|
||||
"page",
|
||||
"course_session_id",
|
||||
"circle",
|
||||
]
|
||||
|
||||
def get_circle(self, obj):
|
||||
circle = obj.get_circle()
|
||||
def get_course_session_id(self, obj):
|
||||
return str(obj.course_session.id)
|
||||
|
||||
if circle:
|
||||
def get_circle(self, obj):
|
||||
if obj.circle_data:
|
||||
return {
|
||||
"id": circle.id,
|
||||
"title": circle.title,
|
||||
"translation_key": circle.translation_key,
|
||||
"id": obj.circle_data.get("id"),
|
||||
"title": obj.circle_data.get("title"),
|
||||
"slug": obj.circle_data.get("slug"),
|
||||
}
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -6,12 +6,12 @@ from vbv_lernwelt.learnpath.graphql.types import (
|
|||
LearningContentAssignmentObjectType,
|
||||
LearningContentAttendanceCourseObjectType,
|
||||
LearningContentDocumentListObjectType,
|
||||
LearningContentEdoniqTestObjectType,
|
||||
LearningContentFeedbackObjectType,
|
||||
LearningContentLearningModuleObjectType,
|
||||
LearningContentMediaLibraryObjectType,
|
||||
LearningContentPlaceholderObjectType,
|
||||
LearningContentRichTextObjectType,
|
||||
LearningContentTestObjectType,
|
||||
LearningContentVideoObjectType,
|
||||
LearningPathObjectType,
|
||||
)
|
||||
|
|
@ -58,7 +58,7 @@ class LearningPathQuery:
|
|||
)
|
||||
learning_content_placeholder = graphene.Field(LearningContentPlaceholderObjectType)
|
||||
learning_content_rich_text = graphene.Field(LearningContentRichTextObjectType)
|
||||
learning_content_test = graphene.Field(LearningContentTestObjectType)
|
||||
learning_content_test = graphene.Field(LearningContentEdoniqTestObjectType)
|
||||
learning_content_video = graphene.Field(LearningContentVideoObjectType)
|
||||
learning_content_document_list = graphene.Field(
|
||||
LearningContentDocumentListObjectType
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ class LearningContentInterface(CoursePageInterface):
|
|||
elif isinstance(instance, LearningContentRichText):
|
||||
return LearningContentRichTextObjectType
|
||||
elif isinstance(instance, LearningContentEdoniqTest):
|
||||
return LearningContentTestObjectType
|
||||
return LearningContentEdoniqTestObjectType
|
||||
elif isinstance(instance, LearningContentVideo):
|
||||
return LearningContentVideoObjectType
|
||||
elif isinstance(instance, LearningContentDocumentList):
|
||||
|
|
@ -102,7 +102,7 @@ class LearningContentMediaLibraryObjectType(DjangoObjectType):
|
|||
fields = []
|
||||
|
||||
|
||||
class LearningContentTestObjectType(DjangoObjectType):
|
||||
class LearningContentEdoniqTestObjectType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = LearningContentEdoniqTest
|
||||
interfaces = (LearningContentInterface,)
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ class LearningContentEdoniqTestSerializer(
|
|||
try:
|
||||
cert = obj.content_assignment.competence_certificate
|
||||
return {
|
||||
"id": cert.id,
|
||||
"id": str(cert.id),
|
||||
"title": cert.title,
|
||||
"slug": cert.slug,
|
||||
"content_type": get_django_content_type(cert),
|
||||
|
|
@ -89,7 +89,7 @@ class LearningContentAssignmentSerializer(
|
|||
try:
|
||||
cert = obj.content_assignment.competence_certificate
|
||||
return {
|
||||
"id": cert.id,
|
||||
"id": str(cert.id),
|
||||
"title": cert.title,
|
||||
"slug": cert.slug,
|
||||
"content_type": get_django_content_type(cert),
|
||||
|
|
|
|||
|
|
@ -6,7 +6,10 @@ from django.db import migrations
|
|||
def init_user_notification_emails(apps=None, schema_editor=None):
|
||||
User = apps.get_model("core", "User")
|
||||
for u in User.objects.all():
|
||||
u.additional_json_data["email_notification_categories"] = ["NOTIFICATION"]
|
||||
u.additional_json_data["email_notification_categories"] = [
|
||||
"INFORMATION",
|
||||
"USER_INTERACTION",
|
||||
]
|
||||
u.save()
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue