Merged in feature/improve-course-session-loading-take2 (pull request #218)

Feature/improve course session loading take2
This commit is contained in:
Daniel Egger 2023-10-10 15:20:15 +00:00
commit ea8ab0adcb
94 changed files with 1770 additions and 1296 deletions

View File

@ -17,7 +17,7 @@ const assignmentType = t(props.dueDate.assignment_type_translation_key);
const courseSessionsStore = useCourseSessionsStore(); const courseSessionsStore = useCourseSessionsStore();
const courseSession = courseSessionsStore.allCourseSessions.find( const courseSession = courseSessionsStore.allCourseSessions.find(
(cs: CourseSession) => cs.id === props.dueDate.course_session (cs: CourseSession) => cs.id === props.dueDate.course_session_id
); );
if (!courseSession) { if (!courseSession) {
@ -28,10 +28,10 @@ const isExpert = courseSessionsStore.hasCockpit(courseSession);
const url = isExpert ? props.dueDate.url_expert : props.dueDate.url; const url = isExpert ? props.dueDate.url_expert : props.dueDate.url;
const courseSessionTitle = computed(() => { const courseSessionTitle = computed(() => {
if (props.dueDate.course_session) { if (props.dueDate.course_session_id) {
return ( return (
courseSessionsStore.getCourseSessionById(props.dueDate.course_session)?.title ?? courseSessionsStore.getCourseSessionById(props.dueDate.course_session_id)
"" ?.title ?? ""
); );
} }
return ""; return "";

View File

@ -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,
},
];
};

View File

@ -37,7 +37,7 @@ import { onMounted, ref, watch } from "vue";
import type { Circle } from "@/services/circle"; import type { Circle } from "@/services/circle";
interface FeedbackSummary { interface FeedbackSummary {
circle_id: number; circle_id: string;
count: number; count: number;
} }
@ -65,7 +65,7 @@ function makeSummary(
const props = defineProps<{ const props = defineProps<{
selctedCircles: string[]; selctedCircles: string[];
circles: Circle[]; circles: Circle[];
courseSessionId: number; courseSessionId: string;
url: string; url: string;
}>(); }>();

View File

@ -23,11 +23,11 @@ type Story = StoryObj<typeof AccountMenuContent>;
const courseSessions = [ const courseSessions = [
{ {
id: 1, id: "1",
title: "Bern 2023 a", title: "Bern 2023 a",
}, },
{ {
id: 2, id: "2",
title: "Zürich 2023 a", title: "Zürich 2023 a",
}, },
]; ];

View File

@ -6,7 +6,7 @@ import type { CourseSession } from "@/types";
const props = defineProps<{ const props = defineProps<{
courseSessions: CourseSession[]; courseSessions: CourseSession[];
user: UserState; user: UserState;
selectedCourseSession?: number; selectedCourseSession?: string;
}>(); }>();
const emit = defineEmits(["selectCourseSession", "logout"]); const emit = defineEmits(["selectCourseSession", "logout"]);

View File

@ -2,7 +2,7 @@
import { useTranslation } from "i18next-vue"; import { useTranslation } from "i18next-vue";
import { useRouteLookups } from "@/utils/route"; import { useRouteLookups } from "@/utils/route";
import { useCurrentCourseSession } from "@/composables"; import { useCurrentCourseSession } from "@/composables";
import { getCompetenceBaseUrl } from "@/utils/utils"; import { getCompetenceNaviUrl, getLearningPathUrl } from "@/utils/utils";
const { inCompetenceProfile, inLearningPath } = useRouteLookups(); const { inCompetenceProfile, inLearningPath } = useRouteLookups();
const courseSession = useCurrentCourseSession(); const courseSession = useCurrentCourseSession();
@ -24,7 +24,7 @@ const { t } = useTranslation();
<div class="flex space-x-8"> <div class="flex space-x-8">
<router-link <router-link
data-cy="preview-learn-path-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"
:class="{ 'preview-nav-item--active': inLearningPath() }" :class="{ 'preview-nav-item--active': inLearningPath() }"
> >
@ -33,7 +33,7 @@ const { t } = useTranslation();
<router-link <router-link
data-cy="preview-competence-profile-link" data-cy="preview-competence-profile-link"
:to="getCompetenceBaseUrl(courseSession)" :to="getCompetenceNaviUrl(courseSession.course.slug)"
class="preview-nav-item" class="preview-nav-item"
:class="{ 'preview-nav-item--active': inCompetenceProfile() }" :class="{ 'preview-nav-item--active': inCompetenceProfile() }"
> >

View File

@ -36,7 +36,7 @@ export interface Item {
export interface Props { export interface Props {
items: CourseSession[]; items: CourseSession[];
selected?: number; selected?: string;
} }
const props = defineProps<Props>(); const props = defineProps<Props>();

View File

@ -15,7 +15,12 @@ import { breakpointsTailwind, useBreakpoints } from "@vueuse/core";
import { computed, onMounted, reactive } from "vue"; import { computed, onMounted, reactive } from "vue";
import { useTranslation } from "i18next-vue"; import { useTranslation } from "i18next-vue";
import CoursePreviewBar from "@/components/header/CoursePreviewBar.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"); log.debug("MainNavigationBar created");
@ -71,7 +76,9 @@ onMounted(() => {
v-if="userStore.loggedIn" v-if="userStore.loggedIn"
:show="state.showMobileNavigationMenu" :show="state.showMobileNavigationMenu"
:course-session="courseSessionsStore.currentCourseSession" :course-session="courseSessionsStore.currentCourseSession"
:media-url="courseSessionsStore.currentCourseSession?.media_library_url" :media-url="
getMediaCenterUrl(courseSessionsStore.currentCourseSession?.course?.slug)
"
:user="userStore" :user="userStore"
@closemodal="state.showMobileNavigationMenu = false" @closemodal="state.showMobileNavigationMenu = false"
@logout="userStore.handleLogout()" @logout="userStore.handleLogout()"
@ -129,7 +136,11 @@ onMounted(() => {
<template v-if="courseSessionsStore.currentCourseSessionHasCockpit"> <template v-if="courseSessionsStore.currentCourseSessionHasCockpit">
<router-link <router-link
data-cy="navigation-cockpit-link" data-cy="navigation-cockpit-link"
:to="`${courseSessionsStore.currentCourseSession.course_url}/cockpit`" :to="
getCockpitUrl(
courseSessionsStore.currentCourseSession.course.slug
)
"
class="nav-item" class="nav-item"
:class="{ 'nav-item--active': inCockpit() }" :class="{ 'nav-item--active': inCockpit() }"
> >
@ -138,7 +149,11 @@ onMounted(() => {
<router-link <router-link
data-cy="navigation-preview-link" data-cy="navigation-preview-link"
:to="courseSessionsStore.currentCourseSession.learning_path_url" :to="
getLearningPathUrl(
courseSessionsStore.currentCourseSession.course.slug
)
"
target="_blank" target="_blank"
class="nav-item" class="nav-item"
> >
@ -151,7 +166,11 @@ onMounted(() => {
<template v-else> <template v-else>
<router-link <router-link
data-cy="navigation-learning-path-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"
:class="{ 'nav-item--active': inLearningPath() }" :class="{ 'nav-item--active': inLearningPath() }"
> >
@ -161,7 +180,9 @@ onMounted(() => {
<router-link <router-link
data-cy="navigation-competence-profile-link" data-cy="navigation-competence-profile-link"
:to=" :to="
getCompetenceBaseUrl(courseSessionsStore.currentCourseSession) getCompetenceNaviUrl(
courseSessionsStore.currentCourseSession.course.slug
)
" "
class="nav-item" class="nav-item"
:class="{ 'nav-item--active': inCompetenceProfile() }" :class="{ 'nav-item--active': inCompetenceProfile() }"
@ -176,7 +197,11 @@ onMounted(() => {
<div class="flex items-stretch justify-start space-x-8"> <div class="flex items-stretch justify-start space-x-8">
<router-link <router-link
v-if="inCourse() && courseSessionsStore.currentCourseSession" v-if="inCourse() && courseSessionsStore.currentCourseSession"
:to="courseSessionsStore.currentCourseSession.media_library_url" :to="
getMediaCenterUrl(
courseSessionsStore.currentCourseSession.course.slug
)
"
data-cy="medialibrary-link" data-cy="medialibrary-link"
class="nav-item-no-mobile" class="nav-item-no-mobile"
:class="{ 'nav-item--active': inMediaLibrary() }" :class="{ 'nav-item--active': inMediaLibrary() }"

View File

@ -4,7 +4,12 @@ import { useCourseSessionsStore } from "@/stores/courseSessions";
import type { UserState } from "@/stores/user"; import type { UserState } from "@/stores/user";
import type { CourseSession } from "@/types"; import type { CourseSession } from "@/types";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { getCompetenceBaseUrl } from "@/utils/utils"; import {
getCockpitUrl,
getCompetenceNaviUrl,
getLearningPathUrl,
getMediaCenterUrl,
} from "@/utils/utils";
const router = useRouter(); const router = useRouter();
@ -60,7 +65,7 @@ const courseSessionsStore = useCourseSessionsStore();
<li class="mb-6"> <li class="mb-6">
<button <button
data-cy="navigation-mobile-cockpit-link" data-cy="navigation-mobile-cockpit-link"
@click="clickLink(`${courseSession.course_url}/cockpit`)" @click="clickLink(getCockpitUrl(courseSession.course.slug))"
> >
{{ $t("cockpit.title") }} {{ $t("cockpit.title") }}
</button> </button>
@ -68,7 +73,7 @@ const courseSessionsStore = useCourseSessionsStore();
<li class="mb-6"> <li class="mb-6">
<button <button
data-cy="navigation-mobile-preview-link" data-cy="navigation-mobile-preview-link"
@click="clickLink(courseSession.learning_path_url)" @click="clickLink(getLearningPathUrl(courseSession.course.slug))"
> >
{{ $t("a.VorschauTeilnehmer") }} {{ $t("a.VorschauTeilnehmer") }}
</button> </button>
@ -78,7 +83,7 @@ const courseSessionsStore = useCourseSessionsStore();
<li class="mb-6"> <li class="mb-6">
<button <button
data-cy="navigation-mobile-learning-path-link" data-cy="navigation-mobile-learning-path-link"
@click="clickLink(courseSession.learning_path_url)" @click="clickLink(getLearningPathUrl(courseSession.course.slug))"
> >
{{ $t("general.learningPath") }} {{ $t("general.learningPath") }}
</button> </button>
@ -86,7 +91,7 @@ const courseSessionsStore = useCourseSessionsStore();
<li class="mb-6"> <li class="mb-6">
<button <button
data-cy="navigation-mobile-competence-profile-link" data-cy="navigation-mobile-competence-profile-link"
@click="clickLink(getCompetenceBaseUrl(courseSession))" @click="clickLink(getCompetenceNaviUrl(courseSession.course.slug))"
> >
{{ $t("competences.title") }} {{ $t("competences.title") }}
</button> </button>
@ -95,7 +100,7 @@ const courseSessionsStore = useCourseSessionsStore();
<li class="mb-6"> <li class="mb-6">
<button <button
data-cy="medialibrary-link" data-cy="medialibrary-link"
@click="clickLink(`${courseSession?.media_library_url}`)" @click="clickLink(getMediaCenterUrl(courseSession.course.slug))"
> >
{{ $t("a.Mediathek") }} {{ $t("a.Mediathek") }}
</button> </button>

View File

@ -10,19 +10,22 @@ export interface Props {
diagramType?: DiagramType; diagramType?: DiagramType;
learningPath: LearningPath; learningPath: LearningPath;
// set to undefined (default) to show all circles // set to undefined (default) to show all circles
showCircleTranslationKeys?: string[]; showCircleSlugs?: string[];
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
diagramType: "horizontal", diagramType: "horizontal",
showCircleTranslationKeys: undefined, showCircleSlugs: undefined,
}); });
const circles = computed(() => const circles = computed(() => {
props.learningPath.circles.filter( if (props.showCircleSlugs?.length) {
(c) => props.showCircleTranslationKeys?.includes(c.translation_key) ?? true return props.learningPath.circles.filter(
) (c) => props.showCircleSlugs?.includes(c.slug) ?? true
); );
}
return props.learningPath.circles;
});
const wrapperClasses = computed(() => { const wrapperClasses = computed(() => {
let classes = "flex my-5"; let classes = "flex my-5";

View File

@ -20,7 +20,7 @@ const emit = defineEmits<{
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
modelValue: () => { modelValue: () => {
return { return {
id: -1, id: "-1",
name: "", name: "",
}; };
}, },

View File

@ -1,8 +1,12 @@
import { useCourseSessionsStore } from "@/stores/courseSessions"; 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 log from "loglevel";
import type { ComputedRef } from "vue"; import type { ComputedRef } from "vue";
import { computed } from "vue"; import { computed, ref, watchEffect } from "vue";
export function useCurrentCourseSession() { export function useCurrentCourseSession() {
/** /**
@ -28,3 +32,94 @@ export function useCurrentCourseSession() {
); );
return result; 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,
};
}

View File

@ -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 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 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 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 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, "\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. * 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. * 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"]; 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. * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/ */

File diff suppressed because one or more lines are too long

View File

@ -8,11 +8,12 @@ type Query {
learning_content_learning_module: LearningContentLearningModuleObjectType learning_content_learning_module: LearningContentLearningModuleObjectType
learning_content_placeholder: LearningContentPlaceholderObjectType learning_content_placeholder: LearningContentPlaceholderObjectType
learning_content_rich_text: LearningContentRichTextObjectType learning_content_rich_text: LearningContentRichTextObjectType
learning_content_test: LearningContentTestObjectType learning_content_test: LearningContentEdoniqTestObjectType
learning_content_video: LearningContentVideoObjectType learning_content_video: LearningContentVideoObjectType
learning_content_document_list: LearningContentDocumentListObjectType learning_content_document_list: LearningContentDocumentListObjectType
course_session_attendance_course(id: ID!, assignment_user_id: ID): CourseSessionAttendanceCourseType course_session_attendance_course(id: ID!, assignment_user_id: ID): CourseSessionAttendanceCourseObjectType
course(id: Int): CourseObjectType course(id: ID): CourseObjectType
course_session(id: ID): CourseSessionObjectType
competence_certificate(id: ID, slug: String): CompetenceCertificateObjectType competence_certificate(id: ID, slug: String): CompetenceCertificateObjectType
competence_certificate_list(id: ID, slug: String, course_id: ID, course_slug: String): CompetenceCertificateListObjectType competence_certificate_list(id: ID, slug: String, course_id: ID, course_slug: String): CompetenceCertificateListObjectType
assignment(id: ID, slug: String): AssignmentObjectType assignment(id: ID, slug: String): AssignmentObjectType
@ -307,12 +308,167 @@ type AssignmentCompletionObjectType {
edoniq_extended_time_flag: Boolean! edoniq_extended_time_flag: Boolean!
assignment_user: UserType! assignment_user: UserType!
assignment: AssignmentObjectType! assignment: AssignmentObjectType!
course_session: CourseSessionObjectType!
completion_status: AssignmentAssignmentCompletionCompletionStatusChoices! completion_status: AssignmentAssignmentCompletionCompletionStatusChoices!
completion_data: GenericScalar completion_data: GenericScalar
additional_json_data: JSONString! additional_json_data: JSONString!
learning_content_page_id: ID 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.""" """An enumeration."""
enum AssignmentAssignmentCompletionCompletionStatusChoices { enum AssignmentAssignmentCompletionCompletionStatusChoices {
"""IN_PROGRESS""" """IN_PROGRESS"""
@ -361,21 +517,6 @@ enum LearnpathLearningContentAssignmentAssignmentTypeChoices {
EDONIQ_TEST 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 { type LearningContentFeedbackObjectType implements LearningContentInterface {
id: ID id: ID
title: String title: String
@ -436,22 +577,6 @@ type LearningContentRichTextObjectType implements LearningContentInterface {
content: String 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 { type LearningContentVideoObjectType implements LearningContentInterface {
id: ID id: ID
title: String title: String
@ -482,32 +607,6 @@ type LearningContentDocumentListObjectType implements LearningContentInterface {
content: String 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 { type CompetenceCertificateListObjectType implements CoursePageInterface {
id: ID id: ID
path: String! path: String!
@ -577,7 +676,7 @@ type ErrorType {
} }
type AttendanceCourseUserMutation { type AttendanceCourseUserMutation {
course_session_attendance_course: CourseSessionAttendanceCourseType course_session_attendance_course: CourseSessionAttendanceCourseObjectType
} }
input AttendanceUserInputType { input AttendanceUserInputType {

View File

@ -6,17 +6,25 @@ export const AssignmentCompletionStatus = "AssignmentCompletionStatus";
export const AssignmentObjectType = "AssignmentObjectType"; export const AssignmentObjectType = "AssignmentObjectType";
export const AttendanceCourseUserMutation = "AttendanceCourseUserMutation"; export const AttendanceCourseUserMutation = "AttendanceCourseUserMutation";
export const AttendanceUserInputType = "AttendanceUserInputType"; export const AttendanceUserInputType = "AttendanceUserInputType";
export const AttendanceUserObjectType = "AttendanceUserObjectType";
export const AttendanceUserStatus = "AttendanceUserStatus"; export const AttendanceUserStatus = "AttendanceUserStatus";
export const AttendanceUserType = "AttendanceUserType";
export const Boolean = "Boolean"; export const Boolean = "Boolean";
export const CircleDocumentObjectType = "CircleDocumentObjectType";
export const CircleObjectType = "CircleObjectType"; export const CircleObjectType = "CircleObjectType";
export const CompetenceCertificateListObjectType = "CompetenceCertificateListObjectType"; export const CompetenceCertificateListObjectType = "CompetenceCertificateListObjectType";
export const CompetenceCertificateObjectType = "CompetenceCertificateObjectType"; export const CompetenceCertificateObjectType = "CompetenceCertificateObjectType";
export const CoreUserLanguageChoices = "CoreUserLanguageChoices"; export const CoreUserLanguageChoices = "CoreUserLanguageChoices";
export const CourseObjectType = "CourseObjectType"; export const CourseObjectType = "CourseObjectType";
export const CoursePageInterface = "CoursePageInterface"; 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 DateTime = "DateTime";
export const DueDateObjectType = "DueDateObjectType";
export const ErrorType = "ErrorType"; export const ErrorType = "ErrorType";
export const FeedbackResponseObjectType = "FeedbackResponseObjectType"; export const FeedbackResponseObjectType = "FeedbackResponseObjectType";
export const Float = "Float"; export const Float = "Float";
@ -28,13 +36,13 @@ export const JSONString = "JSONString";
export const LearningContentAssignmentObjectType = "LearningContentAssignmentObjectType"; export const LearningContentAssignmentObjectType = "LearningContentAssignmentObjectType";
export const LearningContentAttendanceCourseObjectType = "LearningContentAttendanceCourseObjectType"; export const LearningContentAttendanceCourseObjectType = "LearningContentAttendanceCourseObjectType";
export const LearningContentDocumentListObjectType = "LearningContentDocumentListObjectType"; export const LearningContentDocumentListObjectType = "LearningContentDocumentListObjectType";
export const LearningContentEdoniqTestObjectType = "LearningContentEdoniqTestObjectType";
export const LearningContentFeedbackObjectType = "LearningContentFeedbackObjectType"; export const LearningContentFeedbackObjectType = "LearningContentFeedbackObjectType";
export const LearningContentInterface = "LearningContentInterface"; export const LearningContentInterface = "LearningContentInterface";
export const LearningContentLearningModuleObjectType = "LearningContentLearningModuleObjectType"; export const LearningContentLearningModuleObjectType = "LearningContentLearningModuleObjectType";
export const LearningContentMediaLibraryObjectType = "LearningContentMediaLibraryObjectType"; export const LearningContentMediaLibraryObjectType = "LearningContentMediaLibraryObjectType";
export const LearningContentPlaceholderObjectType = "LearningContentPlaceholderObjectType"; export const LearningContentPlaceholderObjectType = "LearningContentPlaceholderObjectType";
export const LearningContentRichTextObjectType = "LearningContentRichTextObjectType"; export const LearningContentRichTextObjectType = "LearningContentRichTextObjectType";
export const LearningContentTestObjectType = "LearningContentTestObjectType";
export const LearningContentVideoObjectType = "LearningContentVideoObjectType"; export const LearningContentVideoObjectType = "LearningContentVideoObjectType";
export const LearningPathObjectType = "LearningPathObjectType"; export const LearningPathObjectType = "LearningPathObjectType";
export const LearningSequenceObjectType = "LearningSequenceObjectType"; export const LearningSequenceObjectType = "LearningSequenceObjectType";

View File

@ -14,7 +14,7 @@ export const graphqlClient = new Client({
cacheExchange({ cacheExchange({
schema: schema, schema: schema,
keys: { keys: {
AttendanceUserType: (data) => data?.user_id?.toString() ?? null, AttendanceUserObjectType: (data) => data?.user_id?.toString() ?? null,
}, },
updates: { updates: {
Mutation: { Mutation: {

View File

@ -76,7 +76,7 @@ export const ASSIGNMENT_COMPLETION_QUERY = graphql(`
`); `);
export const COURSE_QUERY = graphql(` export const COURSE_QUERY = graphql(`
query courseQuery($courseId: Int!) { query courseQuery($courseId: ID!) {
course(id: $courseId) { course(id: $courseId) {
id id
slug 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
}
}
}
}
}
`);

View File

@ -9,12 +9,12 @@ import DueDatesList from "@/components/dueDates/DueDatesList.vue";
const { t } = useTranslation(); const { t } = useTranslation();
const UNFILTERED = Number.MAX_SAFE_INTEGER; const UNFILTERED = Number.MAX_SAFE_INTEGER.toString();
const courseSessionsStore = useCourseSessionsStore(); const courseSessionsStore = useCourseSessionsStore();
const learningPathStore = useLearningPathStore(); const learningPathStore = useLearningPathStore();
type Item = { type Item = {
id: number; id: string;
name: string; name: string;
}; };
@ -101,14 +101,14 @@ const appointments = computed(() => {
const isMatchingSession = (dueDate: DueDate) => const isMatchingSession = (dueDate: DueDate) =>
selectedSession.value.id === UNFILTERED || selectedSession.value.id === UNFILTERED ||
dueDate.course_session === selectedSession.value.id; dueDate.course_session_id === selectedSession.value.id;
const isMatchingCircle = (dueDate: DueDate) => const isMatchingCircle = (dueDate: DueDate) =>
selectedCircle.value.id === UNFILTERED || selectedCircle.value.id === UNFILTERED ||
dueDate.circle?.id === selectedCircle.value.id; dueDate.circle?.id === selectedCircle.value.id;
const isMatchingCourse = (dueDate: DueDate) => 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 numAppointmentsToShow = ref(7);
const canLoadMore = computed(() => { const canLoadMore = computed(() => {

View File

@ -6,6 +6,7 @@ import { useUserStore } from "@/stores/user";
import type { CourseSession } from "@/types"; import type { CourseSession } from "@/types";
import log from "loglevel"; import log from "loglevel";
import { computed, onMounted } from "vue"; import { computed, onMounted } from "vue";
import { getCockpitUrl, getLearningPathUrl } from "@/utils/utils";
log.debug("DashboardPage created"); log.debug("DashboardPage created");
@ -20,9 +21,9 @@ const allDueDates = courseSessionsStore.allDueDates();
const getNextStepLink = (courseSession: CourseSession) => { const getNextStepLink = (courseSession: CourseSession) => {
return computed(() => { return computed(() => {
if (courseSessionsStore.hasCockpit(courseSession)) { if (courseSessionsStore.hasCockpit(courseSession)) {
return courseSession.cockpit_url; return getCockpitUrl(courseSession.course.slug);
} }
return courseSession.learning_path_url; return getLearningPathUrl(courseSession.course.slug);
}); });
}; };
</script> </script>

View File

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { useCurrentCourseSession } from "@/composables"; import { useCourseSessionDetailQuery } from "@/composables";
import { useCockpitStore } from "@/stores/cockpit"; import { useCockpitStore } from "@/stores/cockpit";
import { useCompetenceStore } from "@/stores/competence"; import { useCompetenceStore } from "@/stores/competence";
import { useLearningPathStore } from "@/stores/learningPath"; import { useLearningPathStore } from "@/stores/learningPath";
@ -16,13 +16,15 @@ const props = defineProps<{
const cockpitStore = useCockpitStore(); const cockpitStore = useCockpitStore();
const competenceStore = useCompetenceStore(); const competenceStore = useCompetenceStore();
const learningPathStore = useLearningPathStore(); const learningPathStore = useLearningPathStore();
const courseSession = useCurrentCourseSession();
const courseSessionDetailResult = useCourseSessionDetailQuery();
onMounted(async () => { onMounted(async () => {
log.debug("CockpitParentPage mounted", props.courseSlug); log.debug("CockpitParentPage mounted", props.courseSlug);
try { try {
const members = await cockpitStore.loadCourseSessionMembers(courseSession.value.id); await courseSessionDetailResult.waitForData();
const members = courseSessionDetailResult.filterMembers();
members.forEach((csu) => { members.forEach((csu) => {
competenceStore.loadCompetenceProfilePage( competenceStore.loadCompetenceProfilePage(
props.courseSlug + "-competencenavi-competences", props.courseSlug + "-competencenavi-competences",
@ -35,7 +37,10 @@ onMounted(async () => {
props.courseSlug + "-lp", props.courseSlug + "-lp",
useUserStore().id useUserStore().id
); );
await cockpitStore.loadCircles(props.courseSlug, courseSession.value.id); await cockpitStore.loadCircles(
props.courseSlug,
courseSessionDetailResult.findCurrentUser()
);
} catch (error) { } catch (error) {
log.error(error); log.error(error);
} }

View File

@ -1,8 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import CirclePage from "@/pages/learningPath/circlePage/CirclePage.vue"; import CirclePage from "@/pages/learningPath/circlePage/CirclePage.vue";
import { useCockpitStore } from "@/stores/cockpit";
import * as log from "loglevel"; import * as log from "loglevel";
import { computed, onMounted } from "vue"; import { computed, onMounted } from "vue";
import { useCourseSessionDetailQuery } from "@/composables";
const props = defineProps<{ const props = defineProps<{
userId: string; userId: string;
@ -12,14 +12,14 @@ const props = defineProps<{
log.debug("CockpitUserCirclePage created", props.userId, props.circleSlug); log.debug("CockpitUserCirclePage created", props.userId, props.circleSlug);
const cockpitStore = useCockpitStore();
onMounted(async () => { onMounted(async () => {
log.debug("CockpitUserCirclePage mounted"); log.debug("CockpitUserCirclePage mounted");
}); });
const { findUser } = useCourseSessionDetailQuery();
const user = computed(() => { const user = computed(() => {
return cockpitStore.courseSessionMembers?.find((csu) => csu.user_id === props.userId); return findUser(props.userId);
}); });
</script> </script>

View File

@ -1,5 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
import { useCockpitStore } from "@/stores/cockpit";
import { useCompetenceStore } from "@/stores/competence"; import { useCompetenceStore } from "@/stores/competence";
import { useLearningPathStore } from "@/stores/learningPath"; import { useLearningPathStore } from "@/stores/learningPath";
import * as log from "loglevel"; import * as log from "loglevel";
@ -7,6 +6,7 @@ import { computed, onMounted } from "vue";
import CompetenceDetail from "@/pages/competence/ActionCompetenceDetail.vue"; import CompetenceDetail from "@/pages/competence/ActionCompetenceDetail.vue";
import LearningPathPathView from "@/pages/learningPath/learningPathPage/LearningPathPathView.vue"; import LearningPathPathView from "@/pages/learningPath/learningPathPage/LearningPathPathView.vue";
import { useCourseSessionDetailQuery } from "@/composables";
const props = defineProps<{ const props = defineProps<{
userId: string; userId: string;
@ -15,7 +15,6 @@ const props = defineProps<{
log.debug("CockpitUserProfilePage created", props.userId); log.debug("CockpitUserProfilePage created", props.userId);
const cockpitStore = useCockpitStore();
const competenceStore = useCompetenceStore(); const competenceStore = useCompetenceStore();
const learningPathStore = useLearningPathStore(); const learningPathStore = useLearningPathStore();
@ -27,8 +26,10 @@ const learningPath = computed(() => {
return learningPathStore.learningPathForUser(props.courseSlug, props.userId); return learningPathStore.learningPathForUser(props.courseSlug, props.userId);
}); });
const { findUser } = useCourseSessionDetailQuery();
const user = computed(() => { const user = computed(() => {
return cockpitStore.courseSessionMembers?.find((csu) => csu.user_id === props.userId); return findUser(props.userId);
}); });
function setActiveClasses(isActive: boolean) { function setActiveClasses(isActive: boolean) {

View File

@ -1,20 +1,15 @@
<script setup lang="ts"> <script setup lang="ts">
import { useCurrentCourseSession } from "@/composables"; import { useCourseSessionDetailQuery, useCurrentCourseSession } from "@/composables";
import { ASSIGNMENT_COMPLETION_QUERY } from "@/graphql/queries"; import { ASSIGNMENT_COMPLETION_QUERY } from "@/graphql/queries";
import EvaluationContainer from "@/pages/cockpit/assignmentEvaluationPage/EvaluationContainer.vue"; import EvaluationContainer from "@/pages/cockpit/assignmentEvaluationPage/EvaluationContainer.vue";
import AssignmentSubmissionResponses from "@/pages/learningPath/learningContentPage/assignment/AssignmentSubmissionResponses.vue"; import AssignmentSubmissionResponses from "@/pages/learningPath/learningContentPage/assignment/AssignmentSubmissionResponses.vue";
import type { import type { Assignment, AssignmentCompletion } from "@/types";
Assignment,
AssignmentCompletion,
CourseSessionAssignment,
CourseSessionUser,
} from "@/types";
import { useQuery } from "@urql/vue"; import { useQuery } from "@urql/vue";
import log from "loglevel"; import log from "loglevel";
import { computed, onMounted, reactive } from "vue"; import { computed, onMounted } from "vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { getPreviousRoute } from "@/router/history"; import { getPreviousRoute } from "@/router/history";
import { getAssignmentTypeTitle } from "@/utils/utils"; import { getAssignmentTypeTitle } from "../../../utils/utils";
const props = defineProps<{ const props = defineProps<{
courseSlug: string; courseSlug: string;
@ -24,16 +19,6 @@ const props = defineProps<{
log.debug("AssignmentEvaluationPage created", props.assignmentId, props.userId); 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 courseSession = useCurrentCourseSession();
const router = useRouter(); const router = useRouter();
@ -41,7 +26,7 @@ const router = useRouter();
const queryResult = useQuery({ const queryResult = useQuery({
query: ASSIGNMENT_COMPLETION_QUERY, query: ASSIGNMENT_COMPLETION_QUERY,
variables: { variables: {
courseSessionId: courseSession.value.id.toString(), courseSessionId: courseSession.value.id,
assignmentId: props.assignmentId, assignmentId: props.assignmentId,
assignmentUserId: props.userId, assignmentUserId: props.userId,
}, },
@ -49,12 +34,11 @@ const queryResult = useQuery({
onMounted(async () => { onMounted(async () => {
log.debug("AssignmentView mounted", props.assignmentId, props.userId); 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(); const previousRoute = getPreviousRoute();
function close() { function close() {
@ -103,10 +87,7 @@ const assignment = computed(
<it-icon-close></it-icon-close> <it-icon-close></it-icon-close>
</button> </button>
</header> </header>
<div <div v-if="assignment && assignmentCompletion && assignmentUser" class="relative">
v-if="assignment && assignmentCompletion && state.assignmentUser"
class="relative"
>
<div class="md:h-content flex flex-col md:flex-row"> <div class="md:h-content flex flex-col md:flex-row">
<div <div
class="bg-white md:h-full md:overflow-y-auto" class="bg-white md:h-full md:overflow-y-auto"
@ -118,12 +99,12 @@ const assignment = computed(
<div class="my-6 flex items-center"> <div class="my-6 flex items-center">
<img <img
:src="state.assignmentUser?.avatar_url" :src="assignmentUser?.avatar_url"
class="mr-4 h-11 w-11 rounded-full" class="mr-4 h-11 w-11 rounded-full"
/> />
<div class="font-bold"> <div class="font-bold">
{{ state.assignmentUser?.first_name }} {{ assignmentUser?.first_name }}
{{ state.assignmentUser?.last_name }} {{ assignmentUser?.last_name }}
</div> </div>
</div> </div>
<AssignmentSubmissionResponses <AssignmentSubmissionResponses
@ -139,7 +120,7 @@ const assignment = computed(
> >
<EvaluationContainer <EvaluationContainer
:assignment-completion="assignmentCompletion" :assignment-completion="assignmentCompletion"
:assignment-user="state.assignmentUser" :assignment-user="assignmentUser"
:assignment="assignment" :assignment="assignment"
@close="close()" @close="close()"
></EvaluationContainer> ></EvaluationContainer>

View File

@ -2,7 +2,6 @@
import EvaluationIntro from "@/pages/cockpit/assignmentEvaluationPage/EvaluationIntro.vue"; import EvaluationIntro from "@/pages/cockpit/assignmentEvaluationPage/EvaluationIntro.vue";
import EvaluationSummary from "@/pages/cockpit/assignmentEvaluationPage/EvaluationSummary.vue"; import EvaluationSummary from "@/pages/cockpit/assignmentEvaluationPage/EvaluationSummary.vue";
import EvaluationTask from "@/pages/cockpit/assignmentEvaluationPage/EvaluationTask.vue"; import EvaluationTask from "@/pages/cockpit/assignmentEvaluationPage/EvaluationTask.vue";
import { findAssignmentDetail } from "@/services/assignmentService";
import type { import type {
Assignment, Assignment,
AssignmentCompletion, AssignmentCompletion,
@ -14,6 +13,7 @@ import dayjs from "dayjs";
import { findIndex } from "lodash"; import { findIndex } from "lodash";
import * as log from "loglevel"; import * as log from "loglevel";
import { computed, onMounted } from "vue"; import { computed, onMounted } from "vue";
import { useCourseSessionDetailQuery } from "@/composables";
const props = defineProps<{ const props = defineProps<{
assignmentUser: CourseSessionUser; assignmentUser: CourseSessionUser;
@ -58,10 +58,14 @@ function editTask(task: AssignmentEvaluationTask) {
stepIndex.value = taskIndex + 1; 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(() => const dueDate = computed(() =>
dayjs(assignmentDetail.value?.evaluation_deadline_start) dayjs(assignmentDetail.value?.evaluation_deadline.start)
); );
const inEvaluationTask = computed( const inEvaluationTask = computed(

View File

@ -27,9 +27,9 @@ async function startEvaluation() {
log.debug("startEvaluation"); log.debug("startEvaluation");
if (props.assignmentCompletion.completion_status !== "EVALUATION_SUBMITTED") { if (props.assignmentCompletion.completion_status !== "EVALUATION_SUBMITTED") {
upsertAssignmentCompletionMutation.executeMutation({ upsertAssignmentCompletionMutation.executeMutation({
assignmentId: props.assignment.id.toString(), assignmentId: props.assignment.id,
courseSessionId: courseSession.value.id.toString(), courseSessionId: courseSession.value.id,
assignmentUserId: props.assignmentUser.user_id.toString(), assignmentUserId: props.assignmentUser.user_id,
completionStatus: "EVALUATION_IN_PROGRESS", completionStatus: "EVALUATION_IN_PROGRESS",
completionDataString: JSON.stringify({}), completionDataString: JSON.stringify({}),
// next line used for urql // next line used for urql

View File

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import ItSuccessAlert from "@/components/ui/ItSuccessAlert.vue"; 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 { UPSERT_ASSIGNMENT_COMPLETION_MUTATION } from "@/graphql/mutations";
import { import {
maxAssignmentPoints, maxAssignmentPoints,
@ -41,9 +41,9 @@ const upsertAssignmentCompletionMutation = useMutation(
async function submitEvaluation() { async function submitEvaluation() {
upsertAssignmentCompletionMutation.executeMutation({ upsertAssignmentCompletionMutation.executeMutation({
assignmentId: props.assignment.id.toString(), assignmentId: props.assignment.id,
courseSessionId: courseSession.value.id.toString(), courseSessionId: courseSession.value.id,
assignmentUserId: props.assignmentUser.user_id.toString(), assignmentUserId: props.assignmentUser.user_id,
completionStatus: "EVALUATION_SUBMITTED", completionStatus: "EVALUATION_SUBMITTED",
completionDataString: JSON.stringify({}), completionDataString: JSON.stringify({}),
evaluationPoints: userPoints.value, evaluationPoints: userPoints.value,
@ -77,13 +77,13 @@ const userPoints = computed(() =>
userAssignmentPoints(props.assignment, props.assignmentCompletion) userAssignmentPoints(props.assignment, props.assignmentCompletion)
); );
const courseSessionDetailResult = useCourseSessionDetailQuery();
const evaluationUser = computed(() => { const evaluationUser = computed(() => {
if (props.assignmentCompletion.evaluation_user) { if (props.assignmentCompletion.evaluation_user) {
return (courseSession.value.users ?? []).find( return courseSessionDetailResult.findUser(
(user) => user.user_id === props.assignmentCompletion.evaluation_user props.assignmentCompletion.evaluation_user
) as CourseSessionUser; );
} }
return undefined; return undefined;
}); });
</script> </script>

View File

@ -66,9 +66,9 @@ async function evaluateAssignmentCompletion(completionData: AssignmentCompletion
log.debug("evaluateAssignmentCompletion", completionData); log.debug("evaluateAssignmentCompletion", completionData);
upsertAssignmentCompletionMutation.executeMutation({ upsertAssignmentCompletionMutation.executeMutation({
assignmentId: props.assignment.id.toString(), assignmentId: props.assignment.id,
courseSessionId: courseSession.value.id.toString(), courseSessionId: courseSession.value.id,
assignmentUserId: props.assignmentUser.user_id.toString(), assignmentUserId: props.assignmentUser.user_id,
completionStatus: "EVALUATION_IN_PROGRESS", completionStatus: "EVALUATION_IN_PROGRESS",
completionDataString: JSON.stringify(completionData), completionDataString: JSON.stringify(completionData),
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment

View File

@ -2,20 +2,17 @@
import ItPersonRow from "@/components/ui/ItPersonRow.vue"; import ItPersonRow from "@/components/ui/ItPersonRow.vue";
import type { StatusCount } from "@/components/ui/ItProgress.vue"; import type { StatusCount } from "@/components/ui/ItProgress.vue";
import type { GradedUser } from "@/services/assignmentService"; import type { GradedUser } from "@/services/assignmentService";
import { import { loadAssignmentCompletionStatusData } from "@/services/assignmentService";
findAssignmentDetail,
loadAssignmentCompletionStatusData,
} from "@/services/assignmentService";
import { useCockpitStore } from "@/stores/cockpit";
import type { import type {
CourseSession, CourseSession,
CourseSessionUser, CourseSessionUser,
LearningContentAssignment, LearningContentAssignment,
} from "@/types"; } from "@/types";
import dayjs from "dayjs";
import log from "loglevel"; import log from "loglevel";
import { computed, onMounted, reactive } from "vue"; import { computed, onMounted, reactive } from "vue";
import AssignmentSubmissionProgress from "@/pages/cockpit/cockpitPage/AssignmentSubmissionProgress.vue"; import AssignmentSubmissionProgress from "@/pages/cockpit/cockpitPage/AssignmentSubmissionProgress.vue";
import { useCourseSessionDetailQuery } from "@/composables";
import { formatDueDate } from "../../../components/dueDates/dueDatesUtils";
const props = defineProps<{ const props = defineProps<{
courseSession: CourseSession; courseSession: CourseSession;
@ -27,7 +24,7 @@ log.debug(
props.learningContentAssignment.content_assignment_id props.learningContentAssignment.content_assignment_id
); );
const cockpitStore = useCockpitStore(); const courseSessionDetailResult = useCourseSessionDetailQuery();
const state = reactive({ const state = reactive({
progressStatusCount: {} as StatusCount, progressStatusCount: {} as StatusCount,
@ -35,6 +32,10 @@ const state = reactive({
assignmentSubmittedUsers: [] as CourseSessionUser[], assignmentSubmittedUsers: [] as CourseSessionUser[],
}); });
const assignmentDetail = computed(() => {
return courseSessionDetailResult.findAssignment(props.learningContentAssignment.id);
});
onMounted(async () => { onMounted(async () => {
const { gradedUsers, assignmentSubmittedUsers } = const { gradedUsers, assignmentSubmittedUsers } =
await loadAssignmentCompletionStatusData( await loadAssignmentCompletionStatusData(
@ -45,10 +46,6 @@ onMounted(async () => {
state.gradedUsers = gradedUsers; state.gradedUsers = gradedUsers;
state.assignmentSubmittedUsers = assignmentSubmittedUsers; state.assignmentSubmittedUsers = assignmentSubmittedUsers;
}); });
const assignmentDetail = computed(() =>
findAssignmentDetail(props.learningContentAssignment.content_assignment_id)
);
</script> </script>
<template> <template>
@ -60,16 +57,14 @@ const assignmentDetail = computed(() =>
Circle «{{ learningContentAssignment.parentCircle.title }}» Circle «{{ learningContentAssignment.parentCircle.title }}»
</div> </div>
<div v-if="assignmentDetail"> <div v-if="assignmentDetail">
<span> <span v-if="assignmentDetail.submission_deadline?.start">
{{ $t("Abgabetermin Ergebnisse:") }} {{ $t("Abgabetermin Ergebnisse:") }}
{{ dayjs(assignmentDetail.submission_deadline_start).format("DD.MM.YYYY") }} {{ formatDueDate(assignmentDetail.submission_deadline.start) }}
</span> </span>
<template v-if="assignmentDetail.evaluation_deadline_start"> <template v-if="assignmentDetail.evaluation_deadline?.start">
<br /> <br />
<span v-if="assignmentDetail.evaluation_deadline_start"> {{ $t("Freigabetermin Bewertungen:") }}
{{ $t("Freigabetermin Bewertungen:") }} {{ formatDueDate(assignmentDetail.evaluation_deadline.start) }}
{{ dayjs(assignmentDetail.evaluation_deadline_start).format("DD.MM.YYYY") }}
</span>
</template> </template>
</div> </div>
<div v-else> <div v-else>
@ -85,11 +80,11 @@ const assignmentDetail = computed(() =>
/> />
</div> </div>
<div v-if="cockpitStore.courseSessionMembers?.length" class="mt-6"> <div v-if="courseSessionDetailResult.filterMembers().length" class="mt-6">
<ul> <ul>
<ItPersonRow <ItPersonRow
v-for="csu in cockpitStore.courseSessionMembers" v-for="csu in courseSessionDetailResult.filterMembers()"
:key="csu.user_id + csu.session_title" :key="csu.user_id"
:name="`${csu.first_name} ${csu.last_name}`" :name="`${csu.first_name} ${csu.last_name}`"
:avatar-url="csu.avatar_url" :avatar-url="csu.avatar_url"
:data-cy="csu.last_name" :data-cy="csu.last_name"

View File

@ -25,7 +25,7 @@ onMounted(async () => {
const learningContentAssignment = computed(() => { const learningContentAssignment = computed(() => {
return calcLearningContentAssignments( return calcLearningContentAssignments(
learningPathStore.learningPathForUser(courseSession.value.course.slug, userStore.id) learningPathStore.learningPathForUser(courseSession.value.course.slug, userStore.id)
).filter((lc) => lc.id.toString() === props.assignmentId)[0]; ).filter((lc) => lc.id === props.assignmentId)[0];
}); });
</script> </script>

View File

@ -2,10 +2,9 @@
import ItCheckbox from "@/components/ui/ItCheckbox.vue"; import ItCheckbox from "@/components/ui/ItCheckbox.vue";
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue"; import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
import ItPersonRow from "@/components/ui/ItPersonRow.vue"; import ItPersonRow from "@/components/ui/ItPersonRow.vue";
import { useCurrentCourseSession } from "@/composables"; import { useCourseSessionDetailQuery } from "@/composables";
import type { AttendanceUserStatus } from "@/gql/graphql"; import type { AttendanceUserStatus } from "@/gql/graphql";
import { ATTENDANCE_CHECK_MUTATION } from "@/graphql/mutations"; import { ATTENDANCE_CHECK_MUTATION } from "@/graphql/mutations";
import { useCockpitStore } from "@/stores/cockpit";
import type { DropdownSelectable } from "@/types"; import type { DropdownSelectable } from "@/types";
import { useMutation } from "@urql/vue"; import { useMutation } from "@urql/vue";
import dayjs from "dayjs"; import dayjs from "dayjs";
@ -16,12 +15,15 @@ import { ATTENDANCE_CHECK_QUERY } from "@/graphql/queries";
import { graphqlClient } from "@/graphql/client"; import { graphqlClient } from "@/graphql/client";
const { t } = useTranslation(); const { t } = useTranslation();
const cockpitStore = useCockpitStore();
const courseSession = useCurrentCourseSession();
const attendanceMutation = useMutation(ATTENDANCE_CHECK_MUTATION); const attendanceMutation = useMutation(ATTENDANCE_CHECK_MUTATION);
const courseSessionDetailResult = useCourseSessionDetailQuery();
const attendanceCourses = computed(() => { const attendanceCourses = computed(() => {
return courseSession.value.attendance_courses; return courseSessionDetailResult.courseSessionDetail.value?.attendance_courses ?? [];
});
const courseSessionDetail = computed(() => {
return courseSessionDetailResult.courseSessionDetail.value;
}); });
const presenceCoursesDropdownOptions = computed(() => { const presenceCoursesDropdownOptions = computed(() => {
@ -29,9 +31,9 @@ const presenceCoursesDropdownOptions = computed(() => {
(attendanceCourse) => (attendanceCourse) =>
({ ({
id: attendanceCourse.id, id: attendanceCourse.id,
name: `${t("Präsenzkurs")} ${attendanceCourse.circle_title} ${dayjs( name: `${t("Präsenzkurs")} ${
attendanceCourse.start attendanceCourse.learning_content.circle.title
).format("DD.MM.YYYY")}`, } ${dayjs(attendanceCourse.due_date.start).format("DD.MM.YYYY")}`,
} as DropdownSelectable) } as DropdownSelectable)
); );
}); });
@ -43,6 +45,16 @@ const state = reactive({
attendanceSaved: false, attendanceSaved: false,
}); });
watch(
attendanceCourses,
(newVal) => {
if (newVal && newVal.length > 0) {
state.attendanceCourseSelected = presenceCoursesDropdownOptions.value[0];
}
},
{ immediate: true }
);
function resetState() { function resetState() {
state.userPresence = new Map<string, boolean>(); state.userPresence = new Map<string, boolean>();
state.disclaimerConfirmed = false; state.disclaimerConfirmed = false;
@ -76,23 +88,25 @@ const onSubmit = async () => {
const loadAttendanceData = async () => { const loadAttendanceData = async () => {
resetState(); resetState();
// with changing variables `useQuery` does not seem to work correctly // with changing variables `useQuery` does not seem to work correctly
const res = await graphqlClient.query( if (state.attendanceCourseSelected) {
ATTENDANCE_CHECK_QUERY, const res = await graphqlClient.query(
{ ATTENDANCE_CHECK_QUERY,
courseSessionId: state.attendanceCourseSelected.id.toString(), {
}, courseSessionId: state.attendanceCourseSelected.id.toString(),
{ },
requestPolicy: "network-only", {
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); log.debug("attendanceCourseSelected changed", state.attendanceCourseSelected);
loadAttendanceData(); loadAttendanceData();
} },
{ immediate: true }
); );
</script> </script>
<template> <template>
<div class="bg-gray-200"> <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"> <nav class="py-4 pb-4">
<router-link <router-link
class="btn-text inline-flex items-center pl-0" 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 /> <it-icon-arrow-left />
<span>{{ $t("general.back") }}</span> <span>{{ $t("general.back") }}</span>
</router-link> </router-link>
</nav> </nav>
<div class="pb-4 text-xl font-bold">{{ $t("Anwesenheit Präsenzkurse") }}</div> <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"> <section v-if="attendanceCourses.length && state.attendanceCourseSelected">
<div <div class="flex flex-row justify-between bg-white p-6">
v-for="(csu, index) in cockpitStore.courseSessionMembers" <ItDropdownSelect
:key="csu.user_id + csu.session_title" v-model="state.attendanceCourseSelected"
> :items="presenceCoursesDropdownOptions ?? []"
<ItPersonRow ></ItDropdownSelect>
:name="`${csu.first_name} ${csu.last_name}`" <div v-if="!state.attendanceSaved" class="flex flex-row items-center">
:avatar-url="csu.avatar_url" <ItCheckbox
:class="0 === index ? 'border-none' : ''" :checkbox-item="{
> value: true,
<template #leading> checked: state.disclaimerConfirmed,
<ItCheckbox }"
:disabled="state.attendanceSaved" @toggle="state.disclaimerConfirmed = !state.disclaimerConfirmed"
:checkbox-item="{ ></ItCheckbox>
value: true, <p class="w-64 pr-4 text-sm">
checked: state.userPresence.get(csu.user_id.toString()) as boolean, {{
}" $t(
@toggle=" "Ich will die Anwesenheit der untenstehenden Personen definitiv bestätigen."
state.userPresence.set( )
csu.user_id.toString(), }}
!state.userPresence.get(csu.user_id.toString()) </p>
) <button
" class="btn-primary"
></ItCheckbox> :disabled="!state.disclaimerConfirmed"
</template> @click="onSubmit"
</ItPersonRow> >
{{ $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>
<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>
</div> </div>
</template> </template>

View File

@ -25,7 +25,7 @@ const state = reactive({
statusByUser: [] as { statusByUser: [] as {
userStatus: AssignmentCompletionStatus; userStatus: AssignmentCompletionStatus;
progressStatus: StatusCountKey; progressStatus: StatusCountKey;
userId: number; userId: string;
}[], }[],
submissionProgressStatusCount: {} as StatusCount, submissionProgressStatusCount: {} as StatusCount,
gradingProgressStatusCount: {} as StatusCount, gradingProgressStatusCount: {} as StatusCount,

View File

@ -10,9 +10,7 @@ const courseSession = useCurrentCourseSession();
const circleDates = computed(() => { const circleDates = computed(() => {
const dueDates = courseSession.value.due_dates.filter((dueDate) => { const dueDates = courseSession.value.due_dates.filter((dueDate) => {
if (!cockpitStore.currentCircle) return false; if (!cockpitStore.currentCircle) return false;
return ( return cockpitStore.currentCircle.id == dueDate?.circle?.id;
cockpitStore.currentCircle.translation_key == dueDate?.circle?.translation_key
);
}); });
return dueDates.slice(0, 4); return dueDates.slice(0, 4);
}); });

View File

@ -3,14 +3,13 @@ import LearningPathDiagram from "@/components/learningPath/LearningPathDiagram.v
import ItPersonRow from "@/components/ui/ItPersonRow.vue"; import ItPersonRow from "@/components/ui/ItPersonRow.vue";
import type { LearningPath } from "@/services/learningPath"; import type { LearningPath } from "@/services/learningPath";
import { useCurrentCourseSession } from "@/composables"; import { useCourseSessionDetailQuery, useCurrentCourseSession } from "@/composables";
import SubmissionsOverview from "@/pages/cockpit/cockpitPage/SubmissionsOverview.vue"; import SubmissionsOverview from "@/pages/cockpit/cockpitPage/SubmissionsOverview.vue";
import { useCockpitStore } from "@/stores/cockpit"; import { useCockpitStore } from "@/stores/cockpit";
import { useCompetenceStore } from "@/stores/competence"; import { useCompetenceStore } from "@/stores/competence";
import { useLearningPathStore } from "@/stores/learningPath"; import { useLearningPathStore } from "@/stores/learningPath";
import log from "loglevel"; import log from "loglevel";
import CockpitDates from "@/pages/cockpit/cockpitPage/CockpitDates.vue"; import CockpitDates from "@/pages/cockpit/cockpitPage/CockpitDates.vue";
import { useCourseSessionsStore } from "@/stores/courseSessions";
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue"; import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
const props = defineProps<{ const props = defineProps<{
@ -23,7 +22,7 @@ const cockpitStore = useCockpitStore();
const competenceStore = useCompetenceStore(); const competenceStore = useCompetenceStore();
const learningPathStore = useLearningPathStore(); const learningPathStore = useLearningPathStore();
const courseSession = useCurrentCourseSession(); const courseSession = useCurrentCourseSession();
const courseSessionsStore = useCourseSessionsStore(); const courseSessionDetailResult = useCourseSessionDetailQuery();
function userCountStatusForCircle(userId: string) { function userCountStatusForCircle(userId: string) {
if (!cockpitStore.currentCircle) return { FAIL: 0, SUCCESS: 0, UNKNOWN: 0 }; if (!cockpitStore.currentCircle) return { FAIL: 0, SUCCESS: 0, UNKNOWN: 0 };
@ -37,175 +36,178 @@ function userCountStatusForCircle(userId: string) {
<template> <template>
<div class="bg-gray-200"> <div class="bg-gray-200">
<div v-if="cockpitStore.currentCircle" class="container-large"> <div v-if="cockpitStore.circles?.length">
<div class="mb-9 flex flex-col lg:flex-row lg:items-center lg:justify-between"> <div v-if="cockpitStore.currentCircle" class="container-large">
<h1>Cockpit</h1> <div class="mb-9 flex flex-col lg:flex-row lg:items-center lg:justify-between">
<ItDropdownSelect <h1>Cockpit</h1>
:model-value="cockpitStore.selectedCircle" <ItDropdownSelect
class="mt-4 w-full lg:mt-0 lg:w-96" :model-value="cockpitStore.currentCircle"
:items="cockpitStore.circles" class="mt-4 w-full lg:mt-0 lg:w-96"
@update:model-value="cockpitStore.setCurrentCourseCircleFromEvent" :items="cockpitStore.circles"
></ItDropdownSelect> @update:model-value="cockpitStore.setCurrentCourseCircleFromEvent"
</div> ></ItDropdownSelect>
<!-- Status --> </div>
<div class="mb-4 gap-4 lg:grid lg:grid-cols-3 lg:grid-rows-none"> <!-- Status -->
<div class="my-4 flex flex-col justify-between bg-white p-6 lg:my-0"> <div class="mb-4 gap-4 lg:grid lg:grid-cols-3 lg:grid-rows-none">
<div> <div class="my-4 flex flex-col justify-between bg-white p-6 lg:my-0">
<h3 class="heading-3 mb-4 flex items-center gap-2"> <div>
{{ $t("Trainerunterlagen") }} <h3 class="heading-3 mb-4 flex items-center gap-2">
</h3> {{ $t("Trainerunterlagen") }}
<div class="mb-4"> </h3>
{{ $t("cockpit.trainerFilesText") }} <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> </div>
<div> <div class="my-4 flex flex-col justify-between bg-white p-6 lg:my-0">
<a <div>
href="https://vbvbern.sharepoint.com/sites/myVBV-AFA_K-CI" <h3 class="heading-3 mb-4 flex items-center gap-2">
class="btn-secondary min-w-min" {{ $t("a.Unterlagen für Teilnehmenden") }}
target="_blank" </h3>
> <div class="mb-4">
{{ $t("MS Teams öffnen") }} {{ $t("a.Stelle deinen Lernenden zusätzliche Inhalte zur Verfügung.") }}
</a> </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> </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"> <div class="mb-4 bg-white p-6">
<CockpitDates></CockpitDates> <CockpitDates></CockpitDates>
</div> </div>
<SubmissionsOverview <SubmissionsOverview
:course-session="courseSession" :course-session="courseSession"
:selected-circle="cockpitStore.currentCircle.id" :selected-circle="cockpitStore.currentCircle.id"
></SubmissionsOverview> ></SubmissionsOverview>
<div class="pt-4"> <div class="pt-4">
<!-- progress --> <!-- progress -->
<div v-if="cockpitStore.courseSessionMembers" class="bg-white p-6"> <div
<h1 class="heading-3 mb-5">{{ $t("cockpit.progress") }}</h1> v-if="courseSessionDetailResult.filterMembers().length > 0"
<ul> class="bg-white p-6"
<ItPersonRow >
v-for="csu in cockpitStore.courseSessionMembers" <h1 class="heading-3 mb-5">{{ $t("cockpit.progress") }}</h1>
:key="csu.user_id + csu.session_title" <ul>
:name="`${csu.first_name} ${csu.last_name}`" <ItPersonRow
:avatar-url="csu.avatar_url" v-for="csu in courseSessionDetailResult.filterMembers()"
> :key="csu.user_id"
<template #center> :name="`${csu.first_name} ${csu.last_name}`"
<div :avatar-url="csu.avatar_url"
class="mt-2 flex w-full flex-col items-center justify-between lg:mt-0 lg:flex-row" >
> <template #center>
<LearningPathDiagram <div
v-if=" class="mt-2 flex w-full flex-col items-center justify-between lg:mt-0 lg:flex-row"
learningPathStore.learningPathForUser( >
props.courseSlug, <LearningPathDiagram
csu.user_id v-if="
) learningPathStore.learningPathForUser(
" props.courseSlug,
:learning-path=" csu.user_id
)
"
:learning-path="
learningPathStore.learningPathForUser( learningPathStore.learningPathForUser(
props.courseSlug, props.courseSlug,
csu.user_id csu.user_id
) as LearningPath ) as LearningPath
" "
:show-circle-translation-keys="[ :show-circle-slugs="[cockpitStore.currentCircle.slug]"
cockpitStore.currentCircle.translation_key, diagram-type="singleSmall"
]" class="mr-4"
diagram-type="singleSmall" ></LearningPathDiagram>
class="mr-4" <p class="lg:min-w-[150px]">
></LearningPathDiagram> {{ cockpitStore.currentCircle.title }}
<p class="lg:min-w-[150px]"> </p>
{{ cockpitStore.currentCircle.title }} <div class="ml-4 flex flex-row items-center">
</p> <div class="mr-6 flex flex-row items-center">
<div class="ml-4 flex flex-row items-center"> <it-icon-smiley-thinking
<div class="mr-6 flex flex-row items-center"> class="mr-2 inline-block h-8 w-8"
<it-icon-smiley-thinking ></it-icon-smiley-thinking>
class="mr-2 inline-block h-8 w-8" <p class="text-bold inline-block w-6">
></it-icon-smiley-thinking> {{ userCountStatusForCircle(csu.user_id).FAIL }}
<p class="text-bold inline-block w-6"> </p>
{{ userCountStatusForCircle(csu.user_id).FAIL }} </div>
</p> <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>
<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>
</div> </template>
</template> <template #link>
<template #link> <router-link
<router-link :to="`/course/${props.courseSlug}/cockpit/profile/${csu.user_id}`"
:to="`/course/${props.courseSlug}/cockpit/profile/${csu.user_id}`" class="link w-full lg:text-right"
class="link w-full lg:text-right" >
> {{ $t("general.profileLink") }}
{{ $t("general.profileLink") }} </router-link>
</router-link> </template>
</template> </ItPersonRow>
</ItPersonRow> </ul>
</ul> </div>
</div> </div>
</div> </div>
</div> <div v-else class="container-large mt-4">
<div v-else class="container-large mt-4"> <span class="text-lg text-orange-600">
<span class="text-lg text-orange-600"> {{ $t("a.Kein Circle verfügbar oder ausgewählt.") }}
{{ $t("a.Kein Circle verfügbar oder ausgewählt.") }} </span>
</span> </div>
</div> </div>
</div> </div>
</template> </template>

View File

@ -4,21 +4,20 @@ import log from "loglevel";
import { computed, onMounted, ref } from "vue"; import { computed, onMounted, ref } from "vue";
import ItProgress from "@/components/ui/ItProgress.vue"; import ItProgress from "@/components/ui/ItProgress.vue";
import { itGet } from "@/fetchHelpers"; import { itGet } from "@/fetchHelpers";
import { useCockpitStore } from "@/stores/cockpit"; import { useCourseSessionDetailQuery } from "@/composables";
const props = defineProps<{ const props = defineProps<{
courseSession: CourseSession; courseSession: CourseSession;
circleId: number; circleId: string;
}>(); }>();
log.debug("FeedbackSubmissionProgress created"); log.debug("FeedbackSubmissionProgress created");
const cockpitStore = useCockpitStore(); const courseSessionDetailResult = useCourseSessionDetailQuery();
const completeFeedbacks = ref(0); const completeFeedbacks = ref(0);
const numFeedbacks = computed(() => { const numFeedbacks = computed(() => {
return cockpitStore.courseSessionMembers?.length ?? 0; return courseSessionDetailResult.filterMembers().length;
}); });
onMounted(async () => { onMounted(async () => {

View File

@ -1,7 +1,6 @@
2 2
<script setup lang="ts"> <script setup lang="ts">
import AssignmentSubmissionProgress from "@/pages/cockpit/cockpitPage/AssignmentSubmissionProgress.vue"; import AssignmentSubmissionProgress from "@/pages/cockpit/cockpitPage/AssignmentSubmissionProgress.vue";
import { useCockpitStore } from "@/stores/cockpit";
import { useLearningPathStore } from "@/stores/learningPath"; import { useLearningPathStore } from "@/stores/learningPath";
import { useUserStore } from "@/stores/user"; import { useUserStore } from "@/stores/user";
import type { import type {
@ -14,9 +13,10 @@ import { computed } from "vue";
import { useTranslation } from "i18next-vue"; import { useTranslation } from "i18next-vue";
import FeedbackSubmissionProgress from "@/pages/cockpit/cockpitPage/FeedbackSubmissionProgress.vue"; import FeedbackSubmissionProgress from "@/pages/cockpit/cockpitPage/FeedbackSubmissionProgress.vue";
import { learningContentTypeData } from "@/utils/typeMaps"; import { learningContentTypeData } from "@/utils/typeMaps";
import { useCourseSessionDetailQuery } from "@/composables";
interface Submittable { interface Submittable {
id: number; id: string;
circleName: string; circleName: string;
frontendUrl: string; frontendUrl: string;
title: string; title: string;
@ -27,14 +27,15 @@ interface Submittable {
const props = defineProps<{ const props = defineProps<{
courseSession: CourseSession; courseSession: CourseSession;
selectedCircle: number; selectedCircle: string;
}>(); }>();
log.debug("SubmissionsOverview created", props.courseSession.id); log.debug("SubmissionsOverview created", props.courseSession.id);
const userStore = useUserStore(); const userStore = useUserStore();
const cockpitStore = useCockpitStore();
const learningPathStore = useLearningPathStore(); const learningPathStore = useLearningPathStore();
const courseSessionDetailResult = useCourseSessionDetailQuery();
const { t } = useTranslation(); const { t } = useTranslation();
const submittables = computed(() => { const submittables = computed(() => {
@ -128,7 +129,10 @@ const getIconName = (lc: LearningContent) => {
<template> <template>
<div class="bg-white px-6 py-2"> <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 <div
v-for="submittable in submittables" v-for="submittable in submittables"
:key="submittable.id" :key="submittable.id"

View File

@ -4,34 +4,80 @@ import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
import { useCockpitStore } from "@/stores/cockpit"; import { useCockpitStore } from "@/stores/cockpit";
import ItModal from "@/components/ui/ItModal.vue"; import ItModal from "@/components/ui/ItModal.vue";
import DocumentUploadForm from "@/pages/cockpit/documentPage/DocumentUploadForm.vue"; import DocumentUploadForm from "@/pages/cockpit/documentPage/DocumentUploadForm.vue";
import { computed, ref, watch } from "vue"; import { computed, onMounted, ref, watch } from "vue";
import { useCircleStore } from "@/stores/circle";
import { useTranslation } from "i18next-vue"; import { useTranslation } from "i18next-vue";
import type { CircleDocument, DocumentUploadData } from "@/types"; import type { CircleDocument, DocumentUploadData } from "@/types";
import dialog from "@/utils/confirm-dialog"; import dialog from "@/utils/confirm-dialog";
import log from "loglevel"; import log from "loglevel";
import { uploadCircleDocument } from "@/services/files"; import {
deleteCircleDocument,
fetchCourseSessionDocuments,
uploadCircleDocument,
} from "@/services/files";
import { useCourseSessionsStore } from "@/stores/courseSessions"; import { useCourseSessionsStore } from "@/stores/courseSessions";
import DocumentListItem from "@/components/circle/DocumentListItem.vue"; import DocumentListItem from "@/components/circle/DocumentListItem.vue";
import { useCircleStore } from "@/stores/circle";
const cockpitStore = useCockpitStore(); const cockpitStore = useCockpitStore();
const courseSession = useCurrentCourseSession(); const courseSession = useCurrentCourseSession();
const circleStore = useCircleStore();
const courseSessionsStore = useCourseSessionsStore(); const courseSessionsStore = useCourseSessionsStore();
const circleStore = useCircleStore();
const { t } = useTranslation(); const { t } = useTranslation();
const showUploadModal = ref(false); const showUploadModal = ref(false);
const showUploadErrorMessage = ref(false); const showUploadErrorMessage = ref(false);
const isUploading = 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(() => const dropdownLearningSequences = computed(() =>
circleStore.circle?.learningSequences.map((sequence) => ({ circleStore.circle?.learningSequences.map((sequence) => ({
id: sequence.id, 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 deleteDocument = async (doc: CircleDocument) => {
const options = { const options = {
title: t("circlePage.documents.deleteModalTitle"), title: t("circlePage.documents.deleteModalTitle"),
@ -39,7 +85,10 @@ const deleteDocument = async (doc: CircleDocument) => {
}; };
try { try {
await dialog.confirm(options); 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) { } catch (e) {
log.debug("rejected"); log.debug("rejected");
} }
@ -53,11 +102,13 @@ async function uploadDocument(data: DocumentUploadData) {
if (!courseSessionsStore.currentCourseSession) { if (!courseSessionsStore.currentCourseSession) {
throw new Error("No course session found"); throw new Error("No course session found");
} }
const newDocument = await uploadCircleDocument( await uploadCircleDocument(
data, data,
courseSessionsStore.currentCourseSession.id courseSessionsStore.currentCourseSession.id,
courseSessionDocumentsUrl
); );
courseSessionsStore.addDocument(newDocument); await fetchDocuments();
showUploadModal.value = false; showUploadModal.value = false;
isUploading.value = false; isUploading.value = false;
} catch (error) { } 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"> <div class="mb-9 flex flex-col lg:flex-row lg:items-center lg:justify-between">
<h2>{{ t("a.Unterlagen für Teilnehmenden") }}</h2> <h2>{{ t("a.Unterlagen für Teilnehmenden") }}</h2>
<ItDropdownSelect <ItDropdownSelect
:model-value="cockpitStore.selectedCircle" :model-value="cockpitStore.currentCircle"
class="mt-4 w-full lg:mt-0 lg:w-96" class="mt-4 w-full lg:mt-0 lg:w-96"
:items="cockpitStore.circles" :items="cockpitStore.circles"
@update:model-value="cockpitStore.setCurrentCourseCircleFromEvent" @update:model-value="cockpitStore.setCurrentCourseCircleFromEvent"
@ -96,23 +147,15 @@ async function uploadDocument(data: DocumentUploadData) {
{{ t("circlePage.documents.action") }} {{ t("circlePage.documents.action") }}
</button> </button>
<ul <ul v-if="circleDocuments.length" class="mt-8 border-t border-t-gray-500">
v-if="courseSessionsStore.circleDocuments.length" <DocumentListItem
class="mt-8 border-t border-t-gray-500" v-for="doc of circleDocuments"
> :key="doc.url"
<template :subtitle="doc.learning_sequence.title"
v-for="learningSequence of courseSessionsStore.circleDocuments" :can-delete="true"
:key="learningSequence.id" :doc="doc"
> @delete="deleteDocument(doc)"
<DocumentListItem />
v-for="doc of learningSequence.documents"
:key="doc.url"
:subtitle="learningSequence.title"
:can-delete="courseSessionsStore.canUploadCircleDocuments"
:doc="doc"
@delete="deleteDocument(doc)"
/>
</template>
</ul> </ul>
</div> </div>
<ItModal v-model="showUploadModal"> <ItModal v-model="showUploadModal">

View File

@ -26,7 +26,7 @@ const formData = reactive<DocumentUploadData>({
file: null, file: null,
name: "", name: "",
learningSequence: { learningSequence: {
id: -1, id: "-1",
name: t("circlePage.documents.chooseSequence"), name: t("circlePage.documents.chooseSequence"),
}, },
}); });
@ -56,7 +56,7 @@ function submitForm() {
function validateForm() { function validateForm() {
formErrors.file = formData.file === null; formErrors.file = formData.file === null;
formErrors.learningSequence = formData.learningSequence.id === -1; formErrors.learningSequence = formData.learningSequence.id === "-1";
formErrors.name = formData.name === ""; formErrors.name = formData.name === "";
for (const [, value] of Object.entries(formErrors)) { for (const [, value] of Object.entries(formErrors)) {

View File

@ -20,7 +20,7 @@ const certificatesQuery = useQuery({
query: COMPETENCE_NAVI_CERTIFICATE_QUERY, query: COMPETENCE_NAVI_CERTIFICATE_QUERY,
variables: { variables: {
courseSlug: props.courseSlug, courseSlug: props.courseSlug,
courseSessionId: courseSession.value.id.toString(), courseSessionId: courseSession.value.id,
}, },
}); });

View File

@ -23,7 +23,7 @@ const certificatesQuery = useQuery({
query: COMPETENCE_NAVI_CERTIFICATE_QUERY, query: COMPETENCE_NAVI_CERTIFICATE_QUERY,
variables: { variables: {
courseSlug: props.courseSlug, courseSlug: props.courseSlug,
courseSessionId: courseSession.value.id.toString(), courseSessionId: courseSession.value.id,
}, },
}); });

View File

@ -27,7 +27,7 @@ const certificatesQuery = useQuery({
query: COMPETENCE_NAVI_CERTIFICATE_QUERY, query: COMPETENCE_NAVI_CERTIFICATE_QUERY,
variables: { variables: {
courseSlug: props.courseSlug, courseSlug: props.courseSlug,
courseSessionId: courseSession.value.id.toString(), courseSessionId: courseSession.value.id,
}, },
}); });

View File

@ -1,6 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { useCircleStore } from "@/stores/circle"; import { useCircleStore } from "@/stores/circle";
import { useCourseSessionsStore } from "@/stores/courseSessions";
import type { CourseSessionUser } from "@/types"; import type { CourseSessionUser } from "@/types";
import { humanizeDuration } from "@/utils/humanizeDuration"; import { humanizeDuration } from "@/utils/humanizeDuration";
import sumBy from "lodash/sumBy"; import sumBy from "lodash/sumBy";
@ -11,6 +10,7 @@ import CircleDiagram from "./CircleDiagram.vue";
import CircleOverview from "./CircleOverview.vue"; import CircleOverview from "./CircleOverview.vue";
import DocumentSection from "./DocumentSection.vue"; import DocumentSection from "./DocumentSection.vue";
import LearningSequence from "./LearningSequence.vue"; import LearningSequence from "./LearningSequence.vue";
import { useCourseSessionDetailQuery } from "@/composables";
export interface Props { export interface Props {
courseSlug: string; courseSlug: string;
@ -20,7 +20,8 @@ export interface Props {
} }
const route = useRoute(); const route = useRoute();
const courseSessionsStore = useCourseSessionsStore(); const courseSessionDetailResult = useCourseSessionDetailQuery();
const circleStore = useCircleStore();
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
readonly: false, readonly: false,
@ -29,7 +30,12 @@ const props = withDefaults(defineProps<Props>(), {
log.debug("CirclePage created", props.readonly, props.profileUser); 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(() => { const duration = computed(() => {
if (circleStore.circle) { if (circleStore.circle) {
@ -190,10 +196,7 @@ onMounted(async () => {
}) })
}} }}
</div> </div>
<div <div v-for="expert in circleExperts" :key="expert.user_id">
v-for="expert in courseSessionsStore.circleExperts"
:key="expert.user_id"
>
<div class="mb-2 mt-2 flex flex-row items-center"> <div class="mb-2 mt-2 flex flex-row items-center">
<img <img
class="mr-2 h-[45px] rounded-full" class="mr-2 h-[45px] rounded-full"
@ -205,8 +208,8 @@ onMounted(async () => {
</div> </div>
</div> </div>
<a <a
v-if="courseSessionsStore.circleExperts.length > 0" v-if="circleExperts.length > 0"
:href="'mailto:' + courseSessionsStore.circleExperts[0].email" :href="'mailto:' + circleExperts[0].email"
class="btn-secondary mt-4 text-xl" class="btn-secondary mt-4 text-xl"
> >
{{ $t("circlePage.contactExpertButton") }} {{ $t("circlePage.contactExpertButton") }}

View File

@ -8,28 +8,46 @@
{{ $t("circlePage.documents.userDescription") }} {{ $t("circlePage.documents.userDescription") }}
</div> </div>
</div> </div>
<ul <ul v-if="circleDocuments.length" class="mt-8 border-t border-t-gray-500">
v-if="courseSessionsStore.circleDocuments.length" <DocumentListItem
class="mt-8 border-t border-t-gray-500" v-for="doc of circleDocuments"
> :key="doc.url"
<template :subtitle="doc.learning_sequence.title"
v-for="learningSequence of courseSessionsStore.circleDocuments" :doc="doc"
:key="learningSequence.id" />
>
<DocumentListItem
v-for="doc of learningSequence.documents"
:key="doc.url"
:subtitle="learningSequence.title"
:doc="doc"
/>
</template>
</ul> </ul>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useCourseSessionsStore } from "@/stores/courseSessions";
import DocumentListItem from "@/components/circle/DocumentListItem.vue"; 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> </script>

View File

@ -2,19 +2,19 @@
import DateEmbedding from "@/components/dueDates/DateEmbedding.vue"; import DateEmbedding from "@/components/dueDates/DateEmbedding.vue";
import type { Assignment } from "@/types"; import type { Assignment } from "@/types";
import { useRouteQuery } from "@vueuse/router"; import { useRouteQuery } from "@vueuse/router";
import type { Dayjs } from "dayjs";
import log from "loglevel"; import log from "loglevel";
import dayjs from "dayjs";
interface Props { interface Props {
assignment: Assignment; assignment: Assignment;
dueDate?: Dayjs; submissionDeadlineStart?: string;
} }
const props = withDefaults(defineProps<Props>(), { 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"); const step = useRouteQuery("step");
</script> </script>
@ -40,9 +40,10 @@ const step = useRouteQuery("step");
</ul> </ul>
<h3 class="mb-4 mt-8">{{ $t("assignment.dueDateSubmission") }}</h3> <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") }} {{ $t("assignment.dueDateIntroduction") }}
<DateEmbedding :single-date="dueDate"></DateEmbedding> <DateEmbedding :single-date="dayjs(submissionDeadlineStart)"></DateEmbedding>
</p> </p>
<p v-else class="text-large"> <p v-else class="text-large">
{{ $t("assignment.dueDateNotSet") }} {{ $t("assignment.dueDateNotSet") }}

View File

@ -3,34 +3,36 @@ import DateEmbedding from "@/components/dueDates/DateEmbedding.vue";
import ItButton from "@/components/ui/ItButton.vue"; import ItButton from "@/components/ui/ItButton.vue";
import ItCheckbox from "@/components/ui/ItCheckbox.vue"; import ItCheckbox from "@/components/ui/ItCheckbox.vue";
import ItSuccessAlert from "@/components/ui/ItSuccessAlert.vue"; import ItSuccessAlert from "@/components/ui/ItSuccessAlert.vue";
import { useCurrentCourseSession } from "@/composables"; import { useCourseSessionDetailQuery, useCurrentCourseSession } from "@/composables";
import { bustItGetCache } from "@/fetchHelpers"; import { bustItGetCache } from "@/fetchHelpers";
import { UPSERT_ASSIGNMENT_COMPLETION_MUTATION } from "@/graphql/mutations"; import { UPSERT_ASSIGNMENT_COMPLETION_MUTATION } from "@/graphql/mutations";
import AssignmentSubmissionResponses from "@/pages/learningPath/learningContentPage/assignment/AssignmentSubmissionResponses.vue"; import AssignmentSubmissionResponses from "@/pages/learningPath/learningContentPage/assignment/AssignmentSubmissionResponses.vue";
import { useCourseSessionsStore } from "@/stores/courseSessions";
import { useUserStore } from "@/stores/user"; import { useUserStore } from "@/stores/user";
import type { Assignment, AssignmentCompletion, AssignmentTask } from "@/types"; import type { Assignment, AssignmentCompletion, AssignmentTask } from "@/types";
import { useMutation } from "@urql/vue"; import { useMutation } from "@urql/vue";
import type { Dayjs } from "dayjs";
import log from "loglevel"; import log from "loglevel";
import { computed, reactive } from "vue"; import { computed, reactive } from "vue";
import { useTranslation } from "i18next-vue"; import { useTranslation } from "i18next-vue";
import eventBus from "@/utils/eventBus"; import eventBus from "@/utils/eventBus";
import dayjs from "dayjs";
import { useCircleStore } from "@/stores/circle";
const props = defineProps<{ const props = defineProps<{
assignment: Assignment; assignment: Assignment;
learningContentId: number; learningContentId: string;
assignmentCompletion?: AssignmentCompletion; assignmentCompletion?: AssignmentCompletion;
courseSessionId: number; courseSessionId: string;
dueDate: Dayjs; submissionDeadlineStart?: string;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: "editTask", task: AssignmentTask): void; (e: "editTask", task: AssignmentTask): void;
}>(); }>();
const courseSessionsStore = useCourseSessionsStore();
const courseSession = useCurrentCourseSession(); const courseSession = useCurrentCourseSession();
const courseSessionDetailResult = useCourseSessionDetailQuery();
const circleStore = useCircleStore();
const { t } = useTranslation(); const { t } = useTranslation();
const state = reactive({ const state = reactive({
@ -38,8 +40,15 @@ const state = reactive({
confirmPerson: false, confirmPerson: false,
}); });
const circleExperts = computed(() => {
if (circleStore.circle) {
return courseSessionDetailResult.filterCircleExperts(circleStore.circle.slug);
}
return [];
});
const circleExpert = computed(() => { const circleExpert = computed(() => {
return courseSessionsStore.circleExperts[0]; return circleExperts.value[0];
}); });
const circleExpertName = computed(() => { const circleExpertName = computed(() => {
@ -74,9 +83,9 @@ const onEditTask = (task: AssignmentTask) => {
const onSubmit = async () => { const onSubmit = async () => {
try { try {
await upsertAssignmentCompletionMutation.executeMutation({ await upsertAssignmentCompletionMutation.executeMutation({
assignmentId: props.assignment.id.toString(), assignmentId: props.assignment.id,
courseSessionId: courseSession.value.id.toString(), courseSessionId: courseSession.value.id,
learningContentId: props.learningContentId.toString(), learningContentId: props.learningContentId,
completionDataString: JSON.stringify({}), completionDataString: JSON.stringify({}),
completionStatus: "SUBMITTED", completionStatus: "SUBMITTED",
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
@ -138,9 +147,11 @@ const onSubmit = async () => {
{{ $t("assignment.showAssessmentDocument") }} {{ $t("assignment.showAssessmentDocument") }}
</a> </a>
</div> </div>
<p v-if="isCasework" class="pt-6"> <p v-if="isCasework && props.submissionDeadlineStart" class="pt-6">
{{ $t("assignment.dueDateSubmission") }} {{ $t("assignment.dueDateSubmission") }}
<DateEmbedding :single-date="dueDate"></DateEmbedding> <DateEmbedding
:single-date="dayjs(props.submissionDeadlineStart)"
></DateEmbedding>
</p> </p>
<ItButton <ItButton
class="mt-6" class="mt-6"

View File

@ -16,8 +16,8 @@ import log from "loglevel";
import { computed, reactive } from "vue"; import { computed, reactive } from "vue";
const props = defineProps<{ const props = defineProps<{
assignmentId: number; assignmentId: string;
learningContentId: number; learningContentId: string;
task: AssignmentTask; task: AssignmentTask;
assignmentCompletion?: AssignmentCompletion; assignmentCompletion?: AssignmentCompletion;
}>(); }>();
@ -33,9 +33,9 @@ const upsertAssignmentCompletionMutation = useMutation(
async function upsertAssignmentCompletion(completion_data: AssignmentCompletionData) { async function upsertAssignmentCompletion(completion_data: AssignmentCompletionData) {
try { try {
await upsertAssignmentCompletionMutation.executeMutation({ await upsertAssignmentCompletionMutation.executeMutation({
assignmentId: props.assignmentId.toString(), assignmentId: props.assignmentId,
courseSessionId: courseSession.value.id.toString(), courseSessionId: courseSession.value.id,
learningContentId: props.learningContentId.toString(), learningContentId: props.learningContentId,
completionDataString: JSON.stringify(completion_data), completionDataString: JSON.stringify(completion_data),
completionStatus: "IN_PROGRESS", completionStatus: "IN_PROGRESS",
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment

View File

@ -1,26 +1,22 @@
<script setup lang="ts"> <script setup lang="ts">
import { useCurrentCourseSession } from "@/composables"; import { useCourseSessionDetailQuery, useCurrentCourseSession } from "@/composables";
import { UPSERT_ASSIGNMENT_COMPLETION_MUTATION } from "@/graphql/mutations"; import { UPSERT_ASSIGNMENT_COMPLETION_MUTATION } from "@/graphql/mutations";
import { ASSIGNMENT_COMPLETION_QUERY } from "@/graphql/queries"; import { ASSIGNMENT_COMPLETION_QUERY } from "@/graphql/queries";
import AssignmentIntroductionView from "@/pages/learningPath/learningContentPage/assignment/AssignmentIntroductionView.vue"; import AssignmentIntroductionView from "@/pages/learningPath/learningContentPage/assignment/AssignmentIntroductionView.vue";
import AssignmentSubmissionView from "@/pages/learningPath/learningContentPage/assignment/AssignmentSubmissionView.vue"; import AssignmentSubmissionView from "@/pages/learningPath/learningContentPage/assignment/AssignmentSubmissionView.vue";
import AssignmentTaskView from "@/pages/learningPath/learningContentPage/assignment/AssignmentTaskView.vue"; import AssignmentTaskView from "@/pages/learningPath/learningContentPage/assignment/AssignmentTaskView.vue";
import LearningContentMultiLayout from "@/pages/learningPath/learningContentPage/layouts/LearningContentMultiLayout.vue"; import LearningContentMultiLayout from "@/pages/learningPath/learningContentPage/layouts/LearningContentMultiLayout.vue";
import { useCourseSessionsStore } from "@/stores/courseSessions";
import { useUserStore } from "@/stores/user"; import { useUserStore } from "@/stores/user";
import type { import type {
Assignment, Assignment,
AssignmentCompletion, AssignmentCompletion,
AssignmentTask, AssignmentTask,
CourseSessionAssignment,
CourseSessionUser,
LearningContentAssignment, LearningContentAssignment,
} from "@/types"; } from "@/types";
import { useMutation, useQuery } from "@urql/vue"; import { useMutation, useQuery } from "@urql/vue";
import { useRouteQuery } from "@vueuse/router"; import { useRouteQuery } from "@vueuse/router";
import dayjs from "dayjs";
import * as log from "loglevel"; 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 { useTranslation } from "i18next-vue";
import { bustItGetCache } from "@/fetchHelpers"; import { bustItGetCache } from "@/fetchHelpers";
import { learningContentTypeData } from "@/utils/typeMaps"; import { learningContentTypeData } from "@/utils/typeMaps";
@ -30,14 +26,6 @@ const { t } = useTranslation();
const courseSession = useCurrentCourseSession(); const courseSession = useCurrentCourseSession();
const userStore = useUserStore(); const userStore = useUserStore();
interface State {
courseSessionAssignment: CourseSessionAssignment | undefined;
}
const state: State = reactive({
courseSessionAssignment: undefined,
});
const props = defineProps<{ const props = defineProps<{
learningContent: LearningContentAssignment; learningContent: LearningContentAssignment;
}>(); }>();
@ -45,17 +33,24 @@ const props = defineProps<{
const queryResult = useQuery({ const queryResult = useQuery({
query: ASSIGNMENT_COMPLETION_QUERY, query: ASSIGNMENT_COMPLETION_QUERY,
variables: { variables: {
courseSessionId: courseSession.value.id.toString(), courseSessionId: courseSession.value.id,
assignmentId: props.learningContent.content_assignment_id.toString(), assignmentId: props.learningContent.content_assignment_id,
learningContentId: props.learningContent.id.toString(), learningContentId: props.learningContent.id,
}, },
pause: true, pause: true,
}); });
const courseSessionDetailResult = useCourseSessionDetailQuery();
const upsertAssignmentCompletionMutation = useMutation( const upsertAssignmentCompletionMutation = useMutation(
UPSERT_ASSIGNMENT_COMPLETION_MUTATION 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 // 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 // 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 // it fails with version 10.2.0. I have a reminder to check out the situation
@ -104,10 +99,6 @@ onMounted(async () => {
props.learningContent props.learningContent
); );
state.courseSessionAssignment = useCourseSessionsStore().findCourseSessionAssignment(
props.learningContent.id
);
// create initial `AssignmentCompletion` first, so that it exists and we don't // create initial `AssignmentCompletion` first, so that it exists and we don't
// have reactivity problem accessing it. // have reactivity problem accessing it.
await initUpsertAssignmentCompletion(); await initUpsertAssignmentCompletion();
@ -120,9 +111,7 @@ const numPages = computed(() => {
}); });
const showPreviousButton = computed(() => stepIndex.value != 0); const showPreviousButton = computed(() => stepIndex.value != 0);
const showNextButton = computed(() => stepIndex.value + 1 < numPages.value); const showNextButton = computed(() => stepIndex.value + 1 < numPages.value);
const dueDate = computed(() =>
dayjs(state.courseSessionAssignment?.submission_deadline_start)
);
const currentTask = computed(() => { const currentTask = computed(() => {
if (stepIndex.value > 0 && stepIndex.value <= numTasks.value) { if (stepIndex.value > 0 && stepIndex.value <= numTasks.value) {
return assignment.value?.tasks[stepIndex.value - 1]; return assignment.value?.tasks[stepIndex.value - 1];
@ -133,9 +122,9 @@ const currentTask = computed(() => {
const initUpsertAssignmentCompletion = async () => { const initUpsertAssignmentCompletion = async () => {
try { try {
await upsertAssignmentCompletionMutation.executeMutation({ await upsertAssignmentCompletionMutation.executeMutation({
assignmentId: props.learningContent.content_assignment_id.toString(), assignmentId: props.learningContent.content_assignment_id,
courseSessionId: courseSession.value.id.toString(), courseSessionId: courseSession.value.id,
learningContentId: props.learningContent.id.toString(), learningContentId: props.learningContent.id,
completionDataString: JSON.stringify({}), completionDataString: JSON.stringify({}),
completionStatus: "IN_PROGRESS", completionStatus: "IN_PROGRESS",
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
@ -194,9 +183,9 @@ const subTitle = computed(() => {
}); });
const assignmentUser = computed(() => { const assignmentUser = computed(() => {
return courseSession.value.users.find( return (courseSessionDetailResult.courseSessionDetail.value?.users ?? []).find(
(user) => user.user_id === userStore.id (u) => u.user_id === userStore.id
) as CourseSessionUser; );
}); });
</script> </script>
@ -204,7 +193,7 @@ const assignmentUser = computed(() => {
<div v-if="queryResult.fetching.value"></div> <div v-if="queryResult.fetching.value"></div>
<div v-else-if="queryResult.error.value">{{ queryResult.error.value }}</div> <div v-else-if="queryResult.error.value">{{ queryResult.error.value }}</div>
<div v-else> <div v-else>
<div v-if="assignment && assignmentCompletion"> <div v-if="assignment && assignmentCompletion && assignmentUser">
<div class="flex flex-col lg:flex-row"> <div class="flex flex-col lg:flex-row">
<div <div
v-if="assignmentCompletion?.completion_status === 'EVALUATION_SUBMITTED'" v-if="assignmentCompletion?.completion_status === 'EVALUATION_SUBMITTED'"
@ -239,8 +228,8 @@ const assignmentUser = computed(() => {
<div> <div>
<AssignmentIntroductionView <AssignmentIntroductionView
v-if="stepIndex === 0" v-if="stepIndex === 0"
:due-date="dueDate"
:assignment="assignment" :assignment="assignment"
:submission-deadline-start="submissionDeadline?.start"
></AssignmentIntroductionView> ></AssignmentIntroductionView>
<AssignmentTaskView <AssignmentTaskView
v-else-if="currentTask" v-else-if="currentTask"
@ -251,11 +240,11 @@ const assignmentUser = computed(() => {
></AssignmentTaskView> ></AssignmentTaskView>
<AssignmentSubmissionView <AssignmentSubmissionView
v-else-if="stepIndex + 1 === numPages" v-else-if="stepIndex + 1 === numPages"
:due-date="dueDate"
:assignment="assignment" :assignment="assignment"
:assignment-completion="assignmentCompletion" :assignment-completion="assignmentCompletion"
:learning-content-id="props.learningContent.id" :learning-content-id="props.learningContent.id"
:course-session-id="courseSession.id" :course-session-id="courseSession.id"
:submission-deadline-start="submissionDeadline?.start"
@edit-task="jumpToTask($event)" @edit-task="jumpToTask($event)"
></AssignmentSubmissionView> ></AssignmentSubmissionView>
</div> </div>

View File

@ -3,7 +3,12 @@
<it-icon-calendar-light class="w-[60px] grid-in-icon" /> <it-icon-calendar-light class="w-[60px] grid-in-icon" />
<h2 class="text-large font-bold grid-in-title">{{ $t("a.Datum") }}</h2> <h2 class="text-large font-bold grid-in-title">{{ $t("a.Datum") }}</h2>
<p class="grid-in-value"> <p class="grid-in-value">
{{ formatDueDate(props.attendanceCourse.start, props.attendanceCourse.end) }} {{
formatDueDate(
props.attendanceCourse.due_date.start,
props.attendanceCourse.due_date.end
)
}}
</p> </p>
</div> </div>
<div class="mb-12 grid grid-cols-icon-card gap-x-4 grid-areas-icon-card"> <div class="mb-12 grid grid-cols-icon-card gap-x-4 grid-areas-icon-card">
@ -24,8 +29,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { formatDueDate } from "@/components/dueDates/dueDatesUtils"; import { formatDueDate } from "@/components/dueDates/dueDatesUtils";
import type { CourseSessionAttendanceCourse } from "@/types"; import type { CourseSessionAttendanceCourse } from "@/types";
import dayjs from "dayjs";
import LocalizedFormat from "dayjs/plugin/localizedFormat";
import { computed } from "vue"; import { computed } from "vue";
export interface Props { export interface Props {
@ -34,7 +37,6 @@ export interface Props {
const props = defineProps<Props>(); const props = defineProps<Props>();
dayjs.extend(LocalizedFormat);
const location = computed(() => props.attendanceCourse.location); const location = computed(() => props.attendanceCourse.location);
const trainer = computed(() => props.attendanceCourse.trainer); const trainer = computed(() => props.attendanceCourse.trainer);
</script> </script>

View File

@ -1,18 +1,18 @@
<script setup lang="ts"> <script setup lang="ts">
import AttendanceCourse from "@/pages/learningPath/learningContentPage/attendanceCourse/AttendanceCourse.vue"; import AttendanceCourse from "@/pages/learningPath/learningContentPage/attendanceCourse/AttendanceCourse.vue";
import { useCourseSessionsStore } from "@/stores/courseSessions";
import type { LearningContentAttendanceCourse } from "@/types"; import type { LearningContentAttendanceCourse } from "@/types";
import { computed } from "vue"; import { computed } from "vue";
import LearningContentSimpleLayout from "../layouts/LearningContentSimpleLayout.vue"; import LearningContentSimpleLayout from "../layouts/LearningContentSimpleLayout.vue";
import { useCourseSessionDetailQuery } from "@/composables";
const courseSessionsStore = useCourseSessionsStore();
const props = defineProps<{ const props = defineProps<{
content: LearningContentAttendanceCourse; content: LearningContentAttendanceCourse;
}>(); }>();
const courseSessionDetailResult = useCourseSessionDetailQuery();
const courseSessionAttendanceCourse = computed(() => { const courseSessionAttendanceCourse = computed(() => {
return courseSessionsStore.findAttendanceCourse(props.content.id); return courseSessionDetailResult.findAttendanceCourse(props.content.id);
}); });
</script> </script>

View File

@ -8,8 +8,7 @@ import * as log from "loglevel";
import { itPost } from "@/fetchHelpers"; import { itPost } from "@/fetchHelpers";
import { useQuery } from "@urql/vue"; import { useQuery } from "@urql/vue";
import { ASSIGNMENT_COMPLETION_QUERY } from "@/graphql/queries"; import { ASSIGNMENT_COMPLETION_QUERY } from "@/graphql/queries";
import { useCurrentCourseSession } from "@/composables"; import { useCourseSessionDetailQuery, useCurrentCourseSession } from "@/composables";
import { useCourseSessionsStore } from "@/stores/courseSessions";
import dayjs from "dayjs"; import dayjs from "dayjs";
import ItSuccessAlert from "@/components/ui/ItSuccessAlert.vue"; import ItSuccessAlert from "@/components/ui/ItSuccessAlert.vue";
import { import {
@ -24,18 +23,18 @@ const props = defineProps<{
}>(); }>();
const courseSession = useCurrentCourseSession(); const courseSession = useCurrentCourseSession();
const courseSessionsStore = useCourseSessionsStore(); const courseSessionDetailResult = useCourseSessionDetailQuery();
const courseSessionEdoniqTest = computed(() => { const courseSessionEdoniqTest = computed(() => {
return courseSessionsStore.findCourseSessionEdoniqTest(props.content.id); return courseSessionDetailResult.findEdoniqTest(props.content.id);
}); });
const queryResult = useQuery({ const queryResult = useQuery({
query: ASSIGNMENT_COMPLETION_QUERY, query: ASSIGNMENT_COMPLETION_QUERY,
variables: { variables: {
courseSessionId: courseSession.value.id.toString(), courseSessionId: courseSession.value.id,
assignmentId: props.content.content_assignment_id.toString(), assignmentId: props.content.content_assignment_id,
learningContentId: props.content.id.toString(), learningContentId: props.content.id,
}, },
}); });
@ -53,7 +52,7 @@ const extendedTimeTest = ref(false);
const deadlineInPast = computed(() => { const deadlineInPast = computed(() => {
// with 16 minutes buffer // with 16 minutes buffer
return dayjs(courseSessionEdoniqTest.value?.deadline_start) return dayjs(courseSessionEdoniqTest.value?.deadline.start)
.add(16, "minute") .add(16, "minute")
.isBefore(dayjs()); .isBefore(dayjs());
}); });
@ -90,7 +89,7 @@ async function startTest() {
<p class="mt-2 text-lg"> <p class="mt-2 text-lg">
{{ {{
$t("edoniqTest.submitDateDescription", { $t("edoniqTest.submitDateDescription", {
x: formatDueDate(courseSessionEdoniqTest.deadline_start), x: formatDueDate(courseSessionEdoniqTest.deadline.start),
}) })
}} }}
</p> </p>
@ -158,7 +157,7 @@ async function startTest() {
</div> </div>
<div v-else> <div v-else>
{{ $t("a.Abgabetermin") }}: {{ $t("a.Abgabetermin") }}:
{{ getDateString(dayjs(courseSessionEdoniqTest?.deadline_start)) }} {{ getDateString(dayjs(courseSessionEdoniqTest?.deadline.start)) }}
</div> </div>
</div> </div>
</div> </div>

View File

@ -10,21 +10,21 @@ import {
} from "@/pages/learningPath/learningContentPage/feedback/feedback.constants"; } from "@/pages/learningPath/learningContentPage/feedback/feedback.constants";
import LearningContentMultiLayout from "@/pages/learningPath/learningContentPage/layouts/LearningContentMultiLayout.vue"; import LearningContentMultiLayout from "@/pages/learningPath/learningContentPage/layouts/LearningContentMultiLayout.vue";
import { useCircleStore } from "@/stores/circle"; import { useCircleStore } from "@/stores/circle";
import { useCourseSessionsStore } from "@/stores/courseSessions";
import type { LearningContentFeedback } from "@/types"; import type { LearningContentFeedback } from "@/types";
import { useMutation } from "@urql/vue"; import { useMutation } from "@urql/vue";
import { useRouteQuery } from "@vueuse/router"; import { useRouteQuery } from "@vueuse/router";
import log from "loglevel"; import log from "loglevel";
import { computed, onMounted, reactive, ref } from "vue"; import { computed, onMounted, reactive, ref } from "vue";
import { useTranslation } from "i18next-vue"; import { useTranslation } from "i18next-vue";
import { useCurrentCourseSession } from "@/composables"; import { useCourseSessionDetailQuery, useCurrentCourseSession } from "@/composables";
const props = defineProps<{ const props = defineProps<{
content: LearningContentFeedback; content: LearningContentFeedback;
}>(); }>();
const courseSessionsStore = useCourseSessionsStore();
const courseSession = useCurrentCourseSession(); const courseSession = useCurrentCourseSession();
const circleStore = useCircleStore(); const circleStore = useCircleStore();
const courseSessionDetailResult = useCourseSessionDetailQuery();
const { t } = useTranslation(); const { t } = useTranslation();
const stepNo = useRouteQuery("step", "0", { transform: Number, mode: "push" }); const stepNo = useRouteQuery("step", "0", { transform: Number, mode: "push" });
@ -33,6 +33,13 @@ const title = computed(
() => `«${circleStore.circle?.title}»: ${t("feedback.areYouSatisfied")}` () => `«${circleStore.circle?.title}»: ${t("feedback.areYouSatisfied")}`
); );
const circleExperts = computed(() => {
if (circleStore.circle) {
return courseSessionDetailResult.filterCircleExperts(circleStore.circle.slug);
}
return [];
});
const stepLabels = [ const stepLabels = [
t("general.introduction"), t("general.introduction"),
t("feedback.satisfactionLabel"), t("feedback.satisfactionLabel"),
@ -185,8 +192,8 @@ function hasStepValidInput(stepNumber: number) {
function mutateFeedback(data: FeedbackData, submit = false) { function mutateFeedback(data: FeedbackData, submit = false) {
log.debug("mutate feedback", feedbackData); log.debug("mutate feedback", feedbackData);
return executeMutation({ return executeMutation({
courseSessionId: courseSession.value.id.toString(), courseSessionId: courseSession.value.id,
learningContentId: props.content.id.toString(), learningContentId: props.content.id,
data: data, data: data,
submitted: submit, submitted: submit,
}) })
@ -244,7 +251,7 @@ onMounted(async () => {
<p v-if="stepNo === 0" class="mt-10"> <p v-if="stepNo === 0" class="mt-10">
{{ {{
$t("feedback.intro", { $t("feedback.intro", {
name: `${courseSessionsStore.circleExperts[0]?.first_name} ${courseSessionsStore.circleExperts[0]?.last_name}`, name: `${circleExperts[0]?.first_name} ${circleExperts[0]?.last_name}`,
}) })
}} }}
</p> </p>
@ -265,10 +272,10 @@ onMounted(async () => {
</div> </div>
<FeedbackCompletition <FeedbackCompletition
v-if="stepNo === 11" v-if="stepNo === 11"
:avatar-url="courseSessionsStore.circleExperts[0].avatar_url" :avatar-url="circleExperts[0].avatar_url"
:title=" :title="
$t('feedback.completionTitle', { $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')" :description="$t('feedback.completionDescription')"

View File

@ -11,6 +11,7 @@ import eventBus from "@/utils/eventBus";
import { useRouteQuery } from "@vueuse/router"; import { useRouteQuery } from "@vueuse/router";
import { computed, onUnmounted } from "vue"; import { computed, onUnmounted } from "vue";
import { getPreviousRoute } from "@/router/history"; import { getPreviousRoute } from "@/router/history";
import { getCompetenceNaviUrl } from "@/utils/utils";
log.debug("LearningContent.vue setup"); log.debug("LearningContent.vue setup");
@ -127,7 +128,7 @@ onUnmounted(() => {
<div class="mt-6 lg:mt-12"> <div class="mt-6 lg:mt-12">
{{ $t("selfEvaluation.progressText") }} {{ $t("selfEvaluation.progressText") }}
<router-link <router-link
:to="courseSession.competence_url" :to="getCompetenceNaviUrl(courseSession.course.slug)"
class="text-primary-500 underline" class="text-primary-500 underline"
> >
{{ $t("selfEvaluation.progressLink") }} {{ $t("selfEvaluation.progressLink") }}

View File

@ -1,9 +1,6 @@
import { useCourseSessionDetailQuery } from "@/composables";
import { itGet } from "@/fetchHelpers"; import { itGet } from "@/fetchHelpers";
import type { LearningPath } from "@/services/learningPath"; 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 { import type {
Assignment, Assignment,
AssignmentCompletion, AssignmentCompletion,
@ -31,23 +28,21 @@ export function calcLearningContentAssignments(learningPath?: LearningPath) {
} }
export async function loadAssignmentCompletionStatusData( export async function loadAssignmentCompletionStatusData(
assignmentId: number, assignmentId: string,
courseSessionId: number, courseSessionId: string,
learningContentId: number learningContentId: string
) { ) {
const cockpitStore = useCockpitStore(); const courseSessionDetailResult = useCourseSessionDetailQuery();
const assignmentCompletionData = (await itGet( const assignmentCompletionData = (await itGet(
`/api/assignment/${assignmentId}/${courseSessionId}/status/` `/api/assignment/${assignmentId}/${courseSessionId}/status/`
)) as UserAssignmentCompletionStatus[]; )) as UserAssignmentCompletionStatus[];
const courseSessionUsers = await cockpitStore.loadCourseSessionMembers( const members = courseSessionDetailResult.filterMembers();
courseSessionId
);
const gradedUsers: GradedUser[] = []; const gradedUsers: GradedUser[] = [];
const assignmentSubmittedUsers: CourseSessionUser[] = []; const assignmentSubmittedUsers: CourseSessionUser[] = [];
for (const csu of courseSessionUsers) { for (const csu of members) {
const userAssignmentStatus = assignmentCompletionData.find( const userAssignmentStatus = assignmentCompletionData.find(
(s) => (s) =>
s.assignment_user_id === csu.user_id && s.assignment_user_id === csu.user_id &&
@ -70,34 +65,10 @@ export async function loadAssignmentCompletionStatusData(
return { return {
assignmentSubmittedUsers: assignmentSubmittedUsers, assignmentSubmittedUsers: assignmentSubmittedUsers,
gradedUsers: gradedUsers, 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) { export function maxAssignmentPoints(assignment: Assignment) {
return sum(assignment.evaluation_tasks.map((task) => task.value.max_points)); return sum(assignment.evaluation_tasks.map((task) => task.value.max_points));
} }

View File

@ -121,7 +121,7 @@ export class Circle implements WagtailCircle {
previousCircle?: Circle; previousCircle?: Circle;
constructor( constructor(
public readonly id: number, public readonly id: string,
public readonly slug: string, public readonly slug: string,
public readonly title: string, public readonly title: string,
public readonly translation_key: string, public readonly translation_key: string,

View File

@ -1,13 +1,13 @@
import { itDelete, itFetch, itPost } from "@/fetchHelpers"; import { bustItGetCache, itDelete, itFetch, itGetCached, itPost } from "@/fetchHelpers";
import { getCookieValue } from "@/router/guards"; import { getCookieValue } from "@/router/guards";
import type { CircleDocument, DocumentUploadData } from "@/types"; import type { DocumentUploadData } from "@/types";
type FileData = { type FileData = {
fields: Record<string, string>; fields: Record<string, string>;
url: string; url: string;
}; };
async function startFileUpload(fileData: DocumentUploadData, courseSessionId: number) { async function startFileUpload(fileData: DocumentUploadData, courseSessionId: string) {
if (fileData === null || fileData.file === null) { if (fileData === null || fileData.file === null) {
return null; return null;
} }
@ -73,8 +73,9 @@ function handleUpload(url: string, options: RequestInit) {
export async function uploadCircleDocument( export async function uploadCircleDocument(
data: DocumentUploadData, data: DocumentUploadData,
courseSessionId: number courseSessionId: string,
): Promise<CircleDocument> { bustCacheUrlKey = ""
) {
if (data.file === null) { if (data.file === null) {
throw new Error("No file selected"); throw new Error("No file selected");
} }
@ -82,22 +83,25 @@ export async function uploadCircleDocument(
const startData = await startFileUpload(data, courseSessionId); const startData = await startFileUpload(data, courseSessionId);
await uploadFile(startData, data.file); 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, file_id: startData.file_id,
}); });
const newDocument: CircleDocument = { if (bustCacheUrlKey) {
id: startData.id, bustItGetCache(bustCacheUrlKey);
name: data.name, }
file_name: data.file.name,
url: response.url,
course_session: courseSessionId,
learning_sequence: data.learningSequence.id,
};
return Promise.resolve(newDocument); return response;
} }
export async function deleteCircleDocument(documentId: string) { export async function deleteCircleDocument(documentId: string, bustCacheUrlKey = "") {
return itDelete(`/api/core/document/${documentId}/`); 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}/`);
} }

View File

@ -57,7 +57,7 @@ export class LearningPath implements WagtailLearningPath {
} }
constructor( constructor(
public readonly id: number, public readonly id: string,
public readonly slug: string, public readonly slug: string,
public readonly title: string, public readonly title: string,
public readonly translation_key: string, public readonly translation_key: string,

View File

@ -35,11 +35,11 @@ describe("CourseSession Store", () => {
}; };
courseSessions = [ courseSessions = [
{ {
id: 1, id: "1",
created_at: "2021-05-11T10:00:00.000000Z", created_at: "2021-05-11T10:00:00.000000Z",
updated_at: "2023-05-11T10:00:00.000000Z", updated_at: "2023-05-11T10:00:00.000000Z",
course: { course: {
id: 1, id: "1",
title: "Test Course", title: "Test Course",
category_name: "Test Category", category_name: "Test Category",
slug: "test-course", slug: "test-course",

View File

@ -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 { useLearningPathStore } from "@/stores/learningPath";
import { useUserStore } from "@/stores/user"; import { useUserStore } from "@/stores/user";
import type { CircleLight, CourseSessionUser, ExpertSessionUser } from "@/types";
import log from "loglevel";
import { defineStore } from "pinia"; import { defineStore } from "pinia";
type CircleCockpit = CircleLight & {
name: string;
};
export type CockpitStoreState = { export type CockpitStoreState = {
courseSessionMembers: CourseSessionUser[] | undefined; courseSessionMembers: CourseSessionUser[] | undefined;
circles: { circles: CircleCockpit[] | undefined;
id: string; currentCircle: CircleCockpit | undefined;
name: string;
}[];
currentCourseSlug: string | undefined; currentCourseSlug: string | undefined;
}; };
@ -22,77 +21,54 @@ export const useCockpitStore = defineStore({
return { return {
courseSessionMembers: undefined, courseSessionMembers: undefined,
circles: [], circles: [],
currentCircle: undefined,
currentCourseSlug: undefined, currentCourseSlug: undefined,
} as CockpitStoreState; } as CockpitStoreState;
}, },
actions: { actions: {
async loadCircles(courseSlug: string, courseSessionId: number) { async loadCircles(
log.debug("loadCircles called", courseSlug, courseSessionId); courseSlug: string,
currentCourseSessionUser: CourseSessionUser | undefined
) {
log.debug("loadCircles called", courseSlug);
this.currentCourseSlug = 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 { return {
id: c.slug, id: c.id,
slug: c.slug,
title: c.title,
name: c.title, name: c.title,
}; } as const;
}); });
if (this.circles.length > 0) { if (this.circles?.length) {
await this.setCurrentCourseCircle(this.circles[0].id); await this.setCurrentCourseCircle(this.circles[0].slug);
} }
}, },
async setCurrentCourseCircle(circleSlug: string) { async setCurrentCourseCircle(circleSlug: string) {
if (!this.currentCourseSlug) { this.currentCircle = this.circles?.find((c) => c.slug === circleSlug);
throw new Error("currentCourseSlug is undefined");
}
const circleStore = useCircleStore();
await circleStore.loadCircle(this.currentCourseSlug, circleSlug);
}, },
async setCurrentCourseCircleFromEvent(event: { id: string }) { async setCurrentCourseCircleFromEvent(event: CircleLight) {
await this.setCurrentCourseCircle(event.id); await this.setCurrentCourseCircle(event.slug);
},
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 function courseCircles(courseSlug: string, courseSessionId: number) { async function courseCircles(
courseSlug: string,
currentCourseSessionUser: CourseSessionUser | undefined
) {
const userStore = useUserStore(); const userStore = useUserStore();
const userId = userStore.id; const userId = userStore.id;
const users = (await itGetCached(`/api/course/sessions/${courseSessionId}/users/`, { if (currentCourseSessionUser && currentCourseSessionUser.role === "EXPERT") {
reload: false, const expert = currentCourseSessionUser as ExpertSessionUser;
})) 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;
return expert.circles; return expert.circles;
} }

View File

@ -8,7 +8,6 @@ import type {
CompetenceProfilePage, CompetenceProfilePage,
PerformanceCriteria, PerformanceCriteria,
} from "@/types"; } from "@/types";
import i18next from "i18next";
import _ from "lodash"; import _ from "lodash";
import cloneDeep from "lodash/cloneDeep"; import cloneDeep from "lodash/cloneDeep";
import groupBy from "lodash/groupBy"; import groupBy from "lodash/groupBy";
@ -17,9 +16,6 @@ import { defineStore } from "pinia";
export type CompetenceStoreState = { export type CompetenceStoreState = {
competenceProfilePages: Map<string, CompetenceProfilePage>; competenceProfilePages: Map<string, CompetenceProfilePage>;
selectedCircle: { id: string; name: string };
availableCircles: { id: string; name: string }[];
circles: CircleLight[]; circles: CircleLight[];
}; };
@ -28,8 +24,6 @@ export const useCompetenceStore = defineStore({
state: () => { state: () => {
return { return {
competenceProfilePages: new Map<string, CompetenceProfilePage>(), competenceProfilePages: new Map<string, CompetenceProfilePage>(),
selectedCircle: { id: "all", name: `Circle: ${i18next.t("Alle")}` },
availableCircles: [],
circles: [], circles: [],
} as CompetenceStoreState; } as CompetenceStoreState;
}, },
@ -52,13 +46,7 @@ export const useCompetenceStore = defineStore({
}; };
}, },
criteriaByCompetence(competence: CompetencePage) { criteriaByCompetence(competence: CompetencePage) {
return competence.children.filter((criteria) => { return competence.children;
if (this.selectedCircle.id != "all") {
return criteria.circle.translation_key === this.selectedCircle.id;
}
return competence.children;
});
}, },
competenceProfilePage(userId: string | undefined = undefined) { competenceProfilePage(userId: string | undefined = undefined) {
if (!userId) { if (!userId) {
@ -70,7 +58,7 @@ export const useCompetenceStore = defineStore({
}, },
flatPerformanceCriteria( flatPerformanceCriteria(
userId: string | undefined = undefined, userId: string | undefined = undefined,
circleId: number | undefined = undefined circleId: string | undefined = undefined
) { ) {
if (!userId) { if (!userId) {
const userStore = useUserStore(); const userStore = useUserStore();
@ -88,12 +76,6 @@ export const useCompetenceStore = defineStore({
["asc"] ["asc"]
); );
if (this.selectedCircle.id !== "all") {
criteria = criteria.filter(
(c) => c.circle.translation_key === this.selectedCircle.id
);
}
if (circleId) { if (circleId) {
criteria = criteria.filter((c) => circleId === c.circle.id); criteria = criteria.filter((c) => circleId === c.circle.id);
} }
@ -116,12 +98,7 @@ export const useCompetenceStore = defineStore({
if (competenceProfilePage?.children.length) { if (competenceProfilePage?.children.length) {
return _.orderBy( return _.orderBy(
competenceProfilePage.children.filter((competence) => { competenceProfilePage.children.filter((competence) => {
let criteria = competence.children; const criteria = competence.children;
if (this.selectedCircle.id != "all") {
criteria = criteria.filter((criteria) => {
return criteria.circle.translation_key === this.selectedCircle.id;
});
}
return criteria.length > 0; return criteria.length > 0;
}), }),
["competence_id"], ["competence_id"],
@ -159,10 +136,6 @@ export const useCompetenceStore = defineStore({
this.competenceProfilePages.set(userId, cloneDeep(competenceProfilePage)); this.competenceProfilePages.set(userId, cloneDeep(competenceProfilePage));
this.circles = competenceProfilePage.circles; 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); await this.parseCompletionData(userId);

View File

@ -12,7 +12,7 @@ export const useCompletionStore = defineStore({
getters: {}, getters: {},
actions: { actions: {
async loadCourseSessionCompletionData( async loadCourseSessionCompletionData(
courseSessionId: number, courseSessionId: string,
userId: string, userId: string,
reload = false reload = false
) { ) {
@ -31,7 +31,7 @@ export const useCompletionStore = defineStore({
async markPage( async markPage(
page: BaseCourseWagtailPage, page: BaseCourseWagtailPage,
userId: string | undefined = undefined, userId: string | undefined = undefined,
courseSessionId: number | undefined = undefined courseSessionId: string | undefined = undefined
) { ) {
if (!courseSessionId) { if (!courseSessionId) {
const courseSessionsStore = useCourseSessionsStore(); const courseSessionsStore = useCourseSessionsStore();

View File

@ -1,15 +1,5 @@
import { itGetCached, itPost } from "@/fetchHelpers"; import { itGetCached } from "@/fetchHelpers";
import { deleteCircleDocument } from "@/services/files"; import type { CourseSession, DueDate } from "@/types";
import type {
CircleDocument,
CourseSession,
CourseSessionAssignment,
CourseSessionAttendanceCourse,
CourseSessionEdoniqTest,
CourseSessionUser,
DueDate,
ExpertSessionUser,
} from "@/types";
import eventBus from "@/utils/eventBus"; import eventBus from "@/utils/eventBus";
import { useRouteLookups } from "@/utils/route"; import { useRouteLookups } from "@/utils/route";
import { useLocalStorage } from "@vueuse/core"; import { useLocalStorage } from "@vueuse/core";
@ -18,7 +8,6 @@ import uniqBy from "lodash/uniqBy";
import log from "loglevel"; import log from "loglevel";
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import { computed, ref } from "vue"; import { computed, ref } from "vue";
import { useCircleStore } from "./circle";
import { useUserStore } from "./user"; import { useUserStore } from "./user";
const SELECTED_COURSE_SESSIONS_KEY = "selectedCourseSessionMap"; const SELECTED_COURSE_SESSIONS_KEY = "selectedCourseSessionMap";
@ -39,10 +28,6 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
// TODO: refactor after implementing of Klassenkonzept // TODO: refactor after implementing of Klassenkonzept
await Promise.all( await Promise.all(
allCourseSessions.value.map(async (cs) => { 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); sortDueDates(cs.due_dates);
}) })
); );
@ -57,7 +42,7 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
const selectedCourseSessionMap = useLocalStorage( const selectedCourseSessionMap = useLocalStorage(
SELECTED_COURSE_SESSIONS_KEY, SELECTED_COURSE_SESSIONS_KEY,
new Map<string, number>() new Map<string, string>()
); );
const _currentCourseSlug = ref(""); const _currentCourseSlug = ref("");
@ -99,15 +84,15 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
eventBus.emit("switchedCourseSession", courseSession.id); eventBus.emit("switchedCourseSession", courseSession.id);
} }
function getCourseSessionById(courseSessionId: number | string) { function getCourseSessionById(courseSessionId: string) {
return allCourseSessions.value.find((cs) => { 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) => { const courseSession = allCourseSessions.value.find((cs) => {
return courseSessionId.toString() === cs.id.toString(); return courseSessionId === cs.id;
}); });
if (courseSession) { if (courseSession) {
_switchCourseSession(courseSession); _switchCourseSession(courseSession);
@ -161,64 +146,14 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
return Boolean(isCourseExpert && (inLearningPath() || inCompetenceProfile())); return Boolean(isCourseExpert && (inLearningPath() || inCompetenceProfile()));
}); });
const circleExperts = computed(() => { function hasCockpit(courseSession: CourseSession) {
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(() => {
const userStore = useUserStore(); const userStore = useUserStore();
return ( return (
circleExperts.value.filter((expert) => expert.user_id === userStore.id).length > 0 userStore.course_session_experts.includes(courseSession.id) ||
);
});
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.is_superuser userStore.is_superuser
); );
} }
function addDocument(document: CircleDocument) {
currentCourseSession.value?.documents.push(document);
}
function allDueDates() { function allDueDates() {
const allDueDatesReturn: DueDate[] = []; 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 { return {
uniqueCourseSessionsByCourse, uniqueCourseSessionsByCourse,
allCurrentCourseSessions, allCurrentCourseSessions,
@ -302,15 +187,6 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
hasCockpit, hasCockpit,
hasCourseSessionPreview, hasCourseSessionPreview,
currentCourseSessionHasCockpit, currentCourseSessionHasCockpit,
canUploadCircleDocuments,
circleDocuments,
circleExperts,
addDocument,
startUpload,
removeDocument,
findAttendanceCourse,
findCourseSessionAssignment,
findCourseSessionEdoniqTest,
allDueDates, allDueDates,
// use `useCurrentCourseSession` whenever possible // use `useCurrentCourseSession` whenever possible

View File

@ -26,7 +26,7 @@ export type UserState = {
username: string; username: string;
avatar_url: string; avatar_url: string;
is_superuser: boolean; is_superuser: boolean;
course_session_experts: number[]; course_session_experts: string[];
loggedIn: boolean; loggedIn: boolean;
language: AvailableLanguages; language: AvailableLanguages;
}; };

View File

@ -7,7 +7,7 @@ export type LoginMethod = "local" | "sso";
export type CourseCompletionStatus = "UNKNOWN" | "FAIL" | "SUCCESS"; export type CourseCompletionStatus = "UNKNOWN" | "FAIL" | "SUCCESS";
export interface BaseCourseWagtailPage { export interface BaseCourseWagtailPage {
readonly id: number; readonly id: string;
readonly title: string; readonly title: string;
readonly slug: string; readonly slug: string;
readonly content_type: string; readonly content_type: string;
@ -18,9 +18,9 @@ export interface BaseCourseWagtailPage {
} }
export interface CircleLight { export interface CircleLight {
readonly id: number; readonly id: string;
readonly slug: string;
readonly title: string; readonly title: string;
readonly translation_key: string;
} }
export type LearningContent = export type LearningContent =
@ -52,10 +52,10 @@ export interface LearningContentInterface extends BaseCourseWagtailPage {
export interface LearningContentAssignment extends LearningContentInterface { export interface LearningContentAssignment extends LearningContentInterface {
readonly content_type: "learnpath.LearningContentAssignment"; readonly content_type: "learnpath.LearningContentAssignment";
readonly content_assignment_id: number; readonly content_assignment_id: string;
readonly assignment_type: AssignmentType; readonly assignment_type: AssignmentType;
readonly competence_certificate?: { readonly competence_certificate?: {
id: number; id: string;
title: string; title: string;
slug: string; slug: string;
content_type: string; content_type: string;
@ -104,9 +104,9 @@ export interface LearningContentEdoniqTest extends LearningContentInterface {
readonly content_type: "learnpath.LearningContentEdoniqTest"; readonly content_type: "learnpath.LearningContentEdoniqTest";
readonly checkbox_text: string; readonly checkbox_text: string;
readonly has_extended_time_test: boolean; readonly has_extended_time_test: boolean;
readonly content_assignment_id: number; readonly content_assignment_id: string;
readonly competence_certificate?: { readonly competence_certificate?: {
id: number; id: string;
title: string; title: string;
slug: string; slug: string;
content_type: string; content_type: string;
@ -191,22 +191,22 @@ export interface CourseCompletion {
created_at: string; created_at: string;
updated_at: string; updated_at: string;
readonly user: number; readonly user: number;
readonly page_id: number; readonly page_id: string;
readonly page_type: string; readonly page_type: string;
readonly course_session_id: number; readonly course_session_id: string;
completion_status: CourseCompletionStatus; completion_status: CourseCompletionStatus;
additional_json_data: unknown; additional_json_data: unknown;
} }
export interface Course { export interface Course {
id: number; id: string;
title: string; title: string;
category_name: string; category_name: string;
slug: string; slug: string;
} }
export interface CourseCategory { export interface CourseCategory {
id: number; id: string;
name: string; name: string;
general: boolean; general: boolean;
} }
@ -381,7 +381,7 @@ export interface Assignment extends BaseCourseWagtailPage {
readonly evaluation_tasks: AssignmentEvaluationTask[]; readonly evaluation_tasks: AssignmentEvaluationTask[];
readonly max_points: number; readonly max_points: number;
readonly competence_certificate?: { readonly competence_certificate?: {
id: number; id: string;
title: string; title: string;
slug: string; slug: string;
content_type: string; content_type: string;
@ -452,7 +452,7 @@ export interface CircleExpert {
user_email: string; user_email: string;
user_first_name: string; user_first_name: string;
user_last_name: string; user_last_name: string;
circle_id: number; circle_id: string;
circle_slug: string; circle_slug: string;
circle_translation_key: string; circle_translation_key: string;
} }
@ -462,89 +462,112 @@ export interface CircleDocument {
name: string; name: string;
file_name: string; file_name: string;
url: string; url: string;
course_session: number; learning_sequence: {
learning_sequence: number; id: string;
title: string;
circle: CircleLight;
};
} }
export interface CourseSessionAttendanceCourse { export interface CourseSessionAttendanceCourse {
id: number; id: string;
course_session_id: number; due_date: SimpleDueDate;
learning_content_id: number;
start: string;
end: string;
location: string; location: string;
trainer: string; trainer: string;
due_date_id: number;
circle_title: string; circle_title: string;
learning_content: {
id: string;
title: string;
circle: CircleLight;
};
} }
export interface CourseSessionAssignment { export interface CourseSessionAssignment {
id: number; id: string;
course_session_id: number; submission_deadline: SimpleDueDate;
learning_content_id: number; evaluation_deadline: SimpleDueDate;
submission_deadline_id: number; learning_content: {
submission_deadline_start: string; id: string;
evaluation_deadline_id: number; content_assignment: {
evaluation_deadline_start: string; id: string;
title: string;
assignment_type: AssignmentType;
};
};
} }
export interface CourseSessionEdoniqTest { export interface CourseSessionEdoniqTest {
id: number; id: number;
course_session_id: number; course_session_id: string;
learning_content_id: number; deadline: SimpleDueDate;
deadline_id: number; learning_content: {
deadline_start: string; id: string;
content_assignment: {
id: string;
title: string;
assignment_type: AssignmentType;
};
};
} }
export interface CourseSession { export interface CourseSession {
id: number; id: string;
created_at: string; created_at: string;
updated_at: string; updated_at: string;
course: Course; course: Course;
title: string; title: string;
start_date: string; start_date: string;
end_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[]; due_dates: DueDate[];
} }
export type Role = "MEMBER" | "EXPERT" | "TUTOR"; export type Role = "MEMBER" | "EXPERT" | "TUTOR";
export interface CourseSessionUser { export interface CourseSessionUser {
session_title: string;
user_id: string; user_id: string;
first_name: string; first_name: string;
last_name: string; last_name: string;
email: string; email: string;
avatar_url: string; avatar_url: string;
role: Role; role: Role;
circles: {
id: string;
title: string;
slug: string;
translation_key: string;
}[];
} }
export interface ExpertSessionUser extends CourseSessionUser { export interface ExpertSessionUser extends CourseSessionUser {
role: "EXPERT"; role: "EXPERT";
circles: { circles: {
id: number; id: string;
title: string; title: string;
slug: string; slug: string;
translation_key: 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 // document upload
export interface DocumentUploadData { export interface DocumentUploadData {
file: File | null; file: File | null;
name: string; name: string;
learningSequence: { learningSequence: {
id: number; id: string;
name: string; name: string;
}; };
} }
@ -614,8 +637,8 @@ export interface AssignmentCompletion {
} }
export type UpsertUserAssignmentCompletion = { export type UpsertUserAssignmentCompletion = {
assignment_id: number; assignment_id: string;
course_session_id: number; course_session_id: string;
completion_status: AssignmentCompletionStatus; completion_status: AssignmentCompletionStatus;
completion_data: AssignmentCompletionData; completion_data: AssignmentCompletionData;
}; };
@ -632,20 +655,22 @@ export interface UserAssignmentCompletionStatus {
assignment_user_id: string; assignment_user_id: string;
completion_status: AssignmentCompletionStatus; completion_status: AssignmentCompletionStatus;
evaluation_points: number | null; evaluation_points: number | null;
learning_content_page_id: number; learning_content_page_id: string;
} }
export type DueDate = { export type SimpleDueDate = {
id: number; id: string;
start: string; start: string;
end: string; end?: string;
};
export type DueDate = SimpleDueDate & {
title: string; title: string;
assignment_type_translation_key: string; assignment_type_translation_key: string;
date_type_translation_key: string; date_type_translation_key: string;
subtitle: string; subtitle: string;
url: string; url: string;
url_expert: string; url_expert: string;
course_session: number | null; course_session_id: string;
page: number | null;
circle: CircleLight | null; circle: CircleLight | null;
}; };

View File

@ -3,7 +3,7 @@ import mitt from "mitt";
export type MittEvents = { export type MittEvents = {
// event needed so that the App components do re-render // event needed so that the App components do re-render
// and reload the current course session // and reload the current course session
switchedCourseSession: number; switchedCourseSession: string;
finishedLearningContent: boolean; finishedLearningContent: boolean;
}; };

View File

@ -1,16 +1,35 @@
import type { AssignmentType, CourseSession } from "@/types"; import type { AssignmentType } from "@/types";
import { useTranslation } from "i18next-vue"; import { useTranslation } from "i18next-vue";
export function assertUnreachable(msg: string): never { export function assertUnreachable(msg: string): never {
throw new Error("Didn't expect to get here, " + msg); throw new Error("Didn't expect to get here, " + msg);
} }
export function getCompetenceBaseUrl(courseSession: CourseSession): string { function createCourseUrl(courseSlug: string | undefined, specificSub: string): string {
return courseSession.competence_url.replace( if (!courseSlug) {
// TODO: remove the `competence_url` with url to Navi... return "/";
"/competences", }
""
); 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 { export function getAssignmentTypeTitle(assignmentType: AssignmentType): string {

View File

@ -1,10 +1,16 @@
import {checkNavigationLink, EXPERT_LOGIN, login, PARTICIPANT_LOGIN, visitCoursePage} from "./helpers"; import {
checkNavigationLink,
const openMobileNavigation = () => { EXPERT_LOGIN,
cy.get('[data-cy="navigation-mobile-menu-button"]').click(); login,
} PARTICIPANT_LOGIN,
visitCoursePage,
} from "./helpers";
describe("navigation.cy.js", () => { describe("navigation.cy.js", () => {
const openMobileNavigation = () => {
cy.get('[data-cy="navigation-mobile-menu-button"]').click();
};
beforeEach(() => { beforeEach(() => {
cy.manageCommand("cypress_reset"); cy.manageCommand("cypress_reset");
}); });
@ -24,7 +30,9 @@ describe("navigation.cy.js", () => {
checkNavigationLink("navigation-cockpit-link", "cockpit"); checkNavigationLink("navigation-cockpit-link", "cockpit");
checkNavigationLink("navigation-preview-link", "learn"); 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"); 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", () => { it("should have correct expert navigation", () => {
cy.get('[data-cy="navigation-mobile-cockpit-link"]').should("exist"); 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-preview-link"]').should("exist");
cy.get('[data-cy="navigation-mobile-competence-profile-link"]').should("not.exist"); cy.get('[data-cy="navigation-mobile-competence-profile-link"]').should(
cy.get('[data-cy="navigation-mobile-learning-path-link"]').should("not.exist"); "not.exist"
}) );
}) cy.get('[data-cy="navigation-mobile-learning-path-link"]').should(
"not.exist"
);
});
});
describe("Participant", () => { describe("Participant", () => {
beforeEach(() => { beforeEach(() => {
@ -73,12 +85,19 @@ describe("navigation.cy.js", () => {
}); });
it("should have correct navigation", () => { it("should have correct navigation", () => {
cy.get('[data-cy="navigation-mobile-cockpit-link"]').should("not.exist"); cy.get('[data-cy="navigation-mobile-cockpit-link"]').should(
cy.get('[data-cy="navigation-mobile-preview-link"]').should("not.exist"); "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-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"
);
}); });
}); });
}) });
}); });

View File

@ -1,9 +1,14 @@
import {checkNavigationLink, login, PARTICIPANT_LOGIN, visitCoursePage} from "./helpers"; import {
checkNavigationLink,
login,
PARTICIPANT_LOGIN,
visitCoursePage,
} from "./helpers";
describe("preview.cy.js", () => { describe("preview.cy.js", () => {
beforeEach(() => { beforeEach(() => {
cy.manageCommand("cypress_reset"); cy.manageCommand("cypress_reset");
}) });
describe("Expert / Trainer", () => { describe("Expert / Trainer", () => {
beforeEach(() => { beforeEach(() => {
@ -41,5 +46,5 @@ describe("preview.cy.js", () => {
visitCoursePage("competence"); visitCoursePage("competence");
cy.get('[data-cy="course-preview-bar"]').should("not.exist"); cy.get('[data-cy="course-preview-bar"]').should("not.exist");
}); });
}) });
}); });

View File

@ -31,12 +31,12 @@ from vbv_lernwelt.course.views import (
document_direct_upload, document_direct_upload,
document_upload_finish, document_upload_finish,
document_upload_start, document_upload_start,
get_course_session_users,
get_course_sessions, get_course_sessions,
mark_course_completion_view, mark_course_completion_view,
request_course_completion, request_course_completion,
request_course_completion_for_user, request_course_completion_for_user,
) )
from vbv_lernwelt.course_session.views import get_course_session_documents
from vbv_lernwelt.edoniq_test.views import ( from vbv_lernwelt.edoniq_test.views import (
export_students, export_students,
export_students_and_trainers, export_students_and_trainers,
@ -90,6 +90,10 @@ urlpatterns = [
path('server/documents/', include(wagtaildocs_urls)), path('server/documents/', include(wagtaildocs_urls)),
path('server/pages/', include(wagtail_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 # user management
path("sso/", include("vbv_lernwelt.sso.urls")), path("sso/", include("vbv_lernwelt.sso.urls")),
re_path(r'api/core/me/$', me_user_view, name='me_user_view'), 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, re_path(r"api/notify/email_notification_settings/$", email_notification_settings,
name='email_notification_settings'), name='email_notification_settings'),
# core
re_path(r"server/core/icons/$", generate_web_component_icons,
name="generate_web_component_icons"),
# course # course
path(r"api/course/sessions/", get_course_sessions, name="get_course_sessions"), path(r"api/course/sessions/", get_course_sessions, name="get_course_sessions"),
path(r"api/course/sessions/<signed_int:course_session_id>/users/", # path(r"api/course/sessions/<signed_int:course_session_id>/users/",
get_course_session_users, # get_course_session_users,
name="get_course_session_users"), # name="get_course_session_users"),
path(r"api/course/page/<slug_or_id>/", course_page_api_view, path(r"api/course/page/<slug_or_id>/", course_page_api_view,
name="course_page_api_view"), name="course_page_api_view"),
path(r"api/course/completion/mark/", mark_course_completion_view, path(r"api/course/completion/mark/", mark_course_completion_view,
@ -139,6 +139,9 @@ urlpatterns = [
name='file_upload_finish'), name='file_upload_finish'),
path(r"api/core/document/local/<str:file_id>/", document_direct_upload, path(r"api/core/document/local/<str:file_id>/", document_direct_upload,
name='file_upload_local'), name='file_upload_local'),
path(r'api/core/document/list/<str:course_session_id>/',
get_course_session_documents,
name='get_course_session_documents'),
# feedback # feedback
path(r'api/core/feedback/<str:course_session_id>/summary/', path(r'api/core/feedback/<str:course_session_id>/summary/',

View File

@ -24,6 +24,12 @@ def request_assignment_completion_status(request, assignment_id, course_session_
"evaluation_passed", "evaluation_passed",
"learning_content_page_id", "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() raise PermissionDenied()

View File

@ -40,4 +40,4 @@ class UserSerializer(serializers.ModelSerializer):
role=CourseSessionUser.Role.EXPERT, user=obj 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]

View File

@ -2,6 +2,7 @@ import json
import re import re
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from rest_framework import serializers
from rest_framework.throttling import UserRateThrottle from rest_framework.throttling import UserRateThrottle
@ -41,6 +42,11 @@ def get_django_content_type(obj):
return obj._meta.app_label + "." + type(obj).__name__ 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): def pretty_print_json(json_string):
try: try:
parsed = json_string parsed = json_string

View File

@ -1,15 +1,22 @@
import graphene import graphene
from vbv_lernwelt.course.graphql.types import CourseObjectType from vbv_lernwelt.course.graphql.types import CourseObjectType, CourseSessionObjectType
from vbv_lernwelt.course.models import Course from vbv_lernwelt.course.models import Course, CourseSession
from vbv_lernwelt.course.permissions import has_course_access from vbv_lernwelt.course.permissions import has_course_access
class CourseQuery(graphene.ObjectType): 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): def resolve_course(root, info, id):
course = Course.objects.get(pk=id) course = Course.objects.get(pk=id)
if has_course_access(info.context.user, course): if has_course_access(info.context.user, course):
return course return course
raise PermissionError("You do not have access to this 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")

View File

@ -2,12 +2,30 @@ from typing import Type
import graphene import graphene
import structlog import structlog
from graphene import ObjectType
from graphene_django import DjangoObjectType from graphene_django import DjangoObjectType
from graphql import GraphQLError from graphql import GraphQLError
from rest_framework.exceptions import PermissionDenied 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.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 from vbv_lernwelt.learnpath.graphql.types import LearningPathObjectType
logger = structlog.get_logger(__name__) logger = structlog.get_logger(__name__)
@ -74,3 +92,113 @@ class CourseObjectType(DjangoObjectType):
def resolve_learning_path(self, info): def resolve_learning_path(self, info):
return self.get_learning_path() 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()

View File

@ -294,21 +294,6 @@ class CourseSessionUser(models.Model):
] ]
ordering = ["user__last_name", "user__first_name", "user__email"] 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): class CircleDocument(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) 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 "learnpath.LearningSequence", on_delete=models.CASCADE
) )
def get_circle(self):
return self.learning_sequence.get_circle()
@property @property
def url(self) -> str: def url(self) -> str:
return self.file.url return self.file.url

View File

@ -4,9 +4,12 @@ from vbv_lernwelt.core.serializer_helpers import (
get_it_serializer_class, get_it_serializer_class,
ItWagtailBaseSerializer, ItWagtailBaseSerializer,
) )
from vbv_lernwelt.core.utils import StringIDField
class CourseBaseSerializer(ItWagtailBaseSerializer): class CourseBaseSerializer(ItWagtailBaseSerializer):
id = StringIDField()
content_assignment_id = StringIDField()
course = SerializerMethodField() course = SerializerMethodField()
course_category = SerializerMethodField() course_category = SerializerMethodField()
circles = SerializerMethodField() circles = SerializerMethodField()

View File

@ -1,5 +1,6 @@
from rest_framework import serializers from rest_framework import serializers
from vbv_lernwelt.core.utils import StringIDField
from vbv_lernwelt.course.models import ( from vbv_lernwelt.course.models import (
CircleDocument, CircleDocument,
Course, Course,
@ -7,21 +8,13 @@ from vbv_lernwelt.course.models import (
CourseCompletion, CourseCompletion,
CourseSession, 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.models import DueDate
from vbv_lernwelt.duedate.serializers import DueDateSerializer from vbv_lernwelt.duedate.serializers import DueDateSerializer
class CourseSerializer(serializers.ModelSerializer): class CourseSerializer(serializers.ModelSerializer):
id = StringIDField()
class Meta: class Meta:
model = Course model = Course
fields = ["id", "title", "category_name", "slug"] fields = ["id", "title", "category_name", "slug"]
@ -38,6 +31,9 @@ class CourseCategorySerializer(serializers.ModelSerializer):
class CourseCompletionSerializer(serializers.ModelSerializer): class CourseCompletionSerializer(serializers.ModelSerializer):
page_id = StringIDField()
course_session_id = StringIDField()
class Meta: class Meta:
model = CourseCompletion model = CourseCompletion
fields = [ fields = [
@ -54,56 +50,17 @@ class CourseCompletionSerializer(serializers.ModelSerializer):
class CourseSessionSerializer(serializers.ModelSerializer): class CourseSessionSerializer(serializers.ModelSerializer):
id = StringIDField()
course = serializers.SerializerMethodField() course = serializers.SerializerMethodField()
course_url = 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()
due_dates = serializers.SerializerMethodField() due_dates = serializers.SerializerMethodField()
def get_course(self, obj): def get_course(self, obj):
return CourseSerializer(obj.course).data return CourseSerializer(obj.course).data
def get_course_url(self, obj): # def get_course_url(self, obj):
return obj.course.get_course_url() # 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_due_dates(self, obj): def get_due_dates(self, obj):
due_dates = DueDate.objects.filter(course_session=obj) due_dates = DueDate.objects.filter(course_session=obj)
@ -119,21 +76,13 @@ class CourseSessionSerializer(serializers.ModelSerializer):
"title", "title",
"start_date", "start_date",
"end_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", "due_dates",
] ]
class CircleDocumentSerializer(serializers.ModelSerializer): class CircleDocumentSerializer(serializers.ModelSerializer):
learning_sequence = serializers.SerializerMethodField()
class Meta: class Meta:
model = CircleDocument model = CircleDocument
fields = [ fields = [
@ -145,6 +94,20 @@ class CircleDocumentSerializer(serializers.ModelSerializer):
"learning_sequence", "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): class DocumentUploadStartInputSerializer(serializers.Serializer):
file_name = serializers.CharField() file_name = serializers.CharField()

View File

@ -48,7 +48,7 @@ class CourseCompletionApiTestCase(APITestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(len(response_json), 1) 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( self.assertEqual(
response_json[0]["page_type"], "learnpath.LearningContentPlaceholder" response_json[0]["page_type"], "learnpath.LearningContentPlaceholder"
) )
@ -67,7 +67,7 @@ class CourseCompletionApiTestCase(APITestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(len(response_json), 1) 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( self.assertEqual(
response_json[0]["page_type"], "learnpath.LearningContentPlaceholder" response_json[0]["page_type"], "learnpath.LearningContentPlaceholder"
) )
@ -86,7 +86,7 @@ class CourseCompletionApiTestCase(APITestCase):
response_json = response.json() response_json = response.json()
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(len(response_json), 1) 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( self.assertEqual(
response_json[0]["page_type"], "learnpath.LearningContentPlaceholder" response_json[0]["page_type"], "learnpath.LearningContentPlaceholder"
) )

View File

@ -40,7 +40,7 @@ class CourseCompletionApiTestCase(APITestCase):
self.assertEqual(len(response.json()), 1) self.assertEqual(len(response.json()), 1)
print(json.dumps(response.json(), indent=4)) 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): def test_api_superUser_canAccessEveryCourseSession(self):
self.client.login(username="admin", password="test") self.client.login(username="admin", password="test")
@ -50,4 +50,4 @@ class CourseCompletionApiTestCase(APITestCase):
self.assertEqual(len(response.json()), 1) self.assertEqual(len(response.json()), 1)
print(json.dumps(response.json(), indent=4)) 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))

View File

@ -6,7 +6,6 @@ from rest_framework.response import Response
from wagtail.models import Page from wagtail.models import Page
from vbv_lernwelt.core.utils import get_django_content_type from vbv_lernwelt.core.utils import get_django_content_type
from vbv_lernwelt.course.models import ( from vbv_lernwelt.course.models import (
CircleDocument, CircleDocument,
CourseCompletion, CourseCompletion,
@ -135,7 +134,9 @@ def mark_course_completion_view(request):
@api_view(["GET"]) @api_view(["GET"])
def get_course_sessions(request): def get_course_sessions(request):
try: try:
course_sessions = course_sessions_for_user_qs(request.user) course_sessions = course_sessions_for_user_qs(request.user).prefetch_related(
"course"
)
return Response( return Response(
status=200, data=CourseSessionSerializer(course_sessions, many=True).data 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) 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"]) @api_view(["POST"])
def document_upload_start(request): def document_upload_start(request):
serializer = DocumentUploadStartInputSerializer(data=request.data) serializer = DocumentUploadStartInputSerializer(data=request.data)

View File

@ -3,7 +3,9 @@ import structlog
from rest_framework.exceptions import PermissionDenied from rest_framework.exceptions import PermissionDenied
from vbv_lernwelt.course.permissions import has_course_access 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.models import CourseSessionAttendanceCourse
from vbv_lernwelt.course_session.services.attendance import ( from vbv_lernwelt.course_session.services.attendance import (
AttendanceUserStatus, AttendanceUserStatus,
@ -21,7 +23,9 @@ class AttendanceUserInputType(graphene.InputObjectType):
class AttendanceCourseUserMutation(graphene.Mutation): class AttendanceCourseUserMutation(graphene.Mutation):
course_session_attendance_course = graphene.Field(CourseSessionAttendanceCourseType) course_session_attendance_course = graphene.Field(
CourseSessionAttendanceCourseObjectType
)
class Input: class Input:
id = graphene.ID(required=True) id = graphene.ID(required=True)

View File

@ -3,13 +3,15 @@ from rest_framework.exceptions import PermissionDenied
from vbv_lernwelt.course.models import CourseSession from vbv_lernwelt.course.models import CourseSession
from vbv_lernwelt.course.permissions import has_course_access, is_course_session_expert 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 from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse
class CourseSessionQuery(object): class CourseSessionQuery(object):
course_session_attendance_course = graphene.Field( course_session_attendance_course = graphene.Field(
CourseSessionAttendanceCourseType, CourseSessionAttendanceCourseObjectType,
id=graphene.ID(required=True), id=graphene.ID(required=True),
assignment_user_id=graphene.ID(required=False), assignment_user_id=graphene.ID(required=False),
) )

View File

@ -1,11 +1,22 @@
import graphene import graphene
from graphene_django import DjangoObjectType 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.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) user_id = graphene.UUID(required=True)
status = graphene.Field( status = graphene.Field(
graphene.Enum.from_enum(AttendanceUserStatus), required=True graphene.Enum.from_enum(AttendanceUserStatus), required=True
@ -15,35 +26,61 @@ class AttendanceUserType(graphene.ObjectType):
email = graphene.String() email = graphene.String()
class CourseSessionAttendanceCourseType(DjangoObjectType): class CourseSessionAttendanceCourseObjectType(DjangoObjectType):
course_session_id = graphene.ID(source="course_session_id") course_session_id = graphene.ID(source="course_session_id")
learning_content_id = graphene.ID(source="learning_content_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( 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: class Meta:
model = CourseSessionAttendanceCourse model = CourseSessionAttendanceCourse
fields = ( fields = (
"id", "id",
"course_session_id", "course_session_id",
"learning_content_id", "learning_content",
"due_date_id",
"location", "location",
"trainer", "trainer",
"start", "due_date",
"end",
) )
def resolve_start(self, info): def resolve_attendance_user_list(self, info):
if self.due_date is None: if is_course_session_expert(info.context.user, self.course_session_id):
return None return self.attendance_user_list
return self.due_date.start return []
def resolve_end(self, info):
if self.due_date is None: class CourseSessionAssignmentObjectType(DjangoObjectType):
return None course_session_id = graphene.ID(source="course_session_id")
return self.due_date.end 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",
)

View File

@ -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

View File

@ -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
)

View File

@ -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",
)

View File

@ -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),
),
]

View File

@ -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)]

View File

@ -60,6 +60,7 @@ class DueDate(models.Model):
) )
page = models.ForeignKey(Page, on_delete=models.SET_NULL, null=True, blank=True) page = models.ForeignKey(Page, on_delete=models.SET_NULL, null=True, blank=True)
circle_data = models.JSONField(default=dict, blank=True)
class Meta: class Meta:
ordering = ["start", "end"] ordering = ["start", "end"]
@ -80,6 +81,21 @@ class DueDate(models.Model):
return result 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 @property
def display_subtitle(self): def display_subtitle(self):
result = "" result = ""

View File

@ -1,14 +1,18 @@
from rest_framework import serializers from rest_framework import serializers
from vbv_lernwelt.core.utils import StringIDField
from vbv_lernwelt.duedate.models import DueDate from vbv_lernwelt.duedate.models import DueDate
class DueDateSerializer(serializers.ModelSerializer): class DueDateSerializer(serializers.ModelSerializer):
id = StringIDField()
course_session_id = serializers.SerializerMethodField()
circle = serializers.SerializerMethodField() circle = serializers.SerializerMethodField()
class Meta: class Meta:
model = DueDate model = DueDate
fields = [ fields = [
"id",
"start", "start",
"end", "end",
"manual_override_fields", "manual_override_fields",
@ -18,18 +22,18 @@ class DueDateSerializer(serializers.ModelSerializer):
"subtitle", "subtitle",
"url", "url",
"url_expert", "url_expert",
"course_session", "course_session_id",
"page",
"circle", "circle",
] ]
def get_circle(self, obj): def get_course_session_id(self, obj):
circle = obj.get_circle() return str(obj.course_session.id)
if circle: def get_circle(self, obj):
if obj.circle_data:
return { return {
"id": circle.id, "id": obj.circle_data.get("id"),
"title": circle.title, "title": obj.circle_data.get("title"),
"translation_key": circle.translation_key, "slug": obj.circle_data.get("slug"),
} }
return None return None

View File

@ -6,12 +6,12 @@ from vbv_lernwelt.learnpath.graphql.types import (
LearningContentAssignmentObjectType, LearningContentAssignmentObjectType,
LearningContentAttendanceCourseObjectType, LearningContentAttendanceCourseObjectType,
LearningContentDocumentListObjectType, LearningContentDocumentListObjectType,
LearningContentEdoniqTestObjectType,
LearningContentFeedbackObjectType, LearningContentFeedbackObjectType,
LearningContentLearningModuleObjectType, LearningContentLearningModuleObjectType,
LearningContentMediaLibraryObjectType, LearningContentMediaLibraryObjectType,
LearningContentPlaceholderObjectType, LearningContentPlaceholderObjectType,
LearningContentRichTextObjectType, LearningContentRichTextObjectType,
LearningContentTestObjectType,
LearningContentVideoObjectType, LearningContentVideoObjectType,
LearningPathObjectType, LearningPathObjectType,
) )
@ -58,7 +58,7 @@ class LearningPathQuery:
) )
learning_content_placeholder = graphene.Field(LearningContentPlaceholderObjectType) learning_content_placeholder = graphene.Field(LearningContentPlaceholderObjectType)
learning_content_rich_text = graphene.Field(LearningContentRichTextObjectType) 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_video = graphene.Field(LearningContentVideoObjectType)
learning_content_document_list = graphene.Field( learning_content_document_list = graphene.Field(
LearningContentDocumentListObjectType LearningContentDocumentListObjectType

View File

@ -47,7 +47,7 @@ class LearningContentInterface(CoursePageInterface):
elif isinstance(instance, LearningContentRichText): elif isinstance(instance, LearningContentRichText):
return LearningContentRichTextObjectType return LearningContentRichTextObjectType
elif isinstance(instance, LearningContentEdoniqTest): elif isinstance(instance, LearningContentEdoniqTest):
return LearningContentTestObjectType return LearningContentEdoniqTestObjectType
elif isinstance(instance, LearningContentVideo): elif isinstance(instance, LearningContentVideo):
return LearningContentVideoObjectType return LearningContentVideoObjectType
elif isinstance(instance, LearningContentDocumentList): elif isinstance(instance, LearningContentDocumentList):
@ -102,7 +102,7 @@ class LearningContentMediaLibraryObjectType(DjangoObjectType):
fields = [] fields = []
class LearningContentTestObjectType(DjangoObjectType): class LearningContentEdoniqTestObjectType(DjangoObjectType):
class Meta: class Meta:
model = LearningContentEdoniqTest model = LearningContentEdoniqTest
interfaces = (LearningContentInterface,) interfaces = (LearningContentInterface,)

View File

@ -64,7 +64,7 @@ class LearningContentEdoniqTestSerializer(
try: try:
cert = obj.content_assignment.competence_certificate cert = obj.content_assignment.competence_certificate
return { return {
"id": cert.id, "id": str(cert.id),
"title": cert.title, "title": cert.title,
"slug": cert.slug, "slug": cert.slug,
"content_type": get_django_content_type(cert), "content_type": get_django_content_type(cert),
@ -89,7 +89,7 @@ class LearningContentAssignmentSerializer(
try: try:
cert = obj.content_assignment.competence_certificate cert = obj.content_assignment.competence_certificate
return { return {
"id": cert.id, "id": str(cert.id),
"title": cert.title, "title": cert.title,
"slug": cert.slug, "slug": cert.slug,
"content_type": get_django_content_type(cert), "content_type": get_django_content_type(cert),

View File

@ -6,7 +6,10 @@ from django.db import migrations
def init_user_notification_emails(apps=None, schema_editor=None): def init_user_notification_emails(apps=None, schema_editor=None):
User = apps.get_model("core", "User") User = apps.get_model("core", "User")
for u in User.objects.all(): 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() u.save()