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

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";
interface FeedbackSummary {
circle_id: number;
circle_id: string;
count: number;
}
@ -65,7 +65,7 @@ function makeSummary(
const props = defineProps<{
selctedCircles: string[];
circles: Circle[];
courseSessionId: number;
courseSessionId: string;
url: string;
}>();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,12 @@
import { useCourseSessionsStore } from "@/stores/courseSessions";
import type { CourseSession } from "@/types";
import type { CourseSession, CourseSessionDetail } from "@/types";
import { useQuery } from "@urql/vue";
import { COURSE_SESSION_DETAIL_QUERY } from "@/graphql/queries";
import { useUserStore } from "@/stores/user";
import log from "loglevel";
import type { ComputedRef } from "vue";
import { computed } from "vue";
import { computed, ref, watchEffect } from "vue";
export function useCurrentCourseSession() {
/**
@ -28,3 +32,94 @@ export function useCurrentCourseSession() {
);
return result;
}
export function useCourseSessionDetailQuery(courSessionId?: string) {
if (!courSessionId) {
courSessionId = useCurrentCourseSession().value.id;
}
const queryResult = useQuery({
query: COURSE_SESSION_DETAIL_QUERY,
variables: {
courseSessionId: courSessionId,
},
});
const courseSessionDetail = computed(() => {
return queryResult.data.value?.course_session as CourseSessionDetail | undefined;
});
function findAssignmentByAssignmentId(assignmentId: string) {
return (courseSessionDetail.value?.assignments ?? []).find((a) => {
return a.learning_content?.content_assignment?.id === assignmentId;
});
}
function findAssignment(learningContentId: string) {
return (courseSessionDetail.value?.assignments ?? []).find((a) => {
return a.learning_content.id === learningContentId;
});
}
function findEdoniqTest(learningContentId: string) {
return (courseSessionDetail.value?.edoniq_tests ?? []).find((e) => {
return e.learning_content.id === learningContentId;
});
}
function findAttendanceCourse(learningContentId: string) {
return (courseSessionDetail.value?.attendance_courses ?? []).find((e) => {
return e.learning_content.id === learningContentId;
});
}
function findUser(userId: string) {
return (courseSessionDetail.value?.users ?? []).find((u) => {
return u.user_id === userId;
});
}
function findCurrentUser() {
const userStore = useUserStore();
const userId = userStore.id;
return findUser(userId);
}
function filterMembers() {
return (courseSessionDetail.value?.users ?? []).filter((u) => {
return u.role === "MEMBER";
});
}
function filterCircleExperts(circleSlug: string) {
return (courseSessionDetail.value?.users ?? []).filter((u) => {
return u.role === "EXPERT" && u.circles.map((c) => c.slug).includes(circleSlug);
});
}
const dataLoaded = ref(false);
function waitForData() {
return new Promise((resolve) => {
watchEffect(() => {
if (queryResult.data.value) {
dataLoaded.value = true;
resolve(queryResult.data.value);
}
});
});
}
return {
...queryResult,
courseSessionDetail,
waitForData,
findAssignmentByAssignmentId,
findAssignment,
findEdoniqTest,
findAttendanceCourse,
findUser,
findCurrentUser,
filterMembers,
filterCircleExperts,
};
}

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 query attendanceCheckQuery($courseSessionId: ID!) {\n course_session_attendance_course(id: $courseSessionId) {\n id\n attendance_user_list {\n user_id\n status\n }\n }\n }\n": types.AttendanceCheckQueryDocument,
"\n query assignmentCompletionQuery(\n $assignmentId: ID!\n $courseSessionId: ID!\n $learningContentId: ID\n $assignmentUserId: UUID\n ) {\n assignment(id: $assignmentId) {\n assignment_type\n needs_expert_evaluation\n max_points\n content_type\n effort_required\n evaluation_description\n evaluation_document_url\n evaluation_tasks\n id\n intro_text\n performance_objectives\n slug\n tasks\n title\n translation_key\n competence_certificate {\n ...CoursePageFields\n }\n }\n assignment_completion(\n assignment_id: $assignmentId\n course_session_id: $courseSessionId\n assignment_user_id: $assignmentUserId\n learning_content_page_id: $learningContentId\n ) {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_user {\n id\n }\n assignment_user {\n id\n }\n evaluation_points\n evaluation_max_points\n evaluation_passed\n edoniq_extended_time_flag\n completion_data\n }\n }\n": types.AssignmentCompletionQueryDocument,
"\n query courseQuery($courseId: Int!) {\n course(id: $courseId) {\n id\n slug\n title\n category_name\n learning_path {\n id\n }\n }\n }\n": types.CourseQueryDocument,
"\n query courseQuery($courseId: ID!) {\n course(id: $courseId) {\n id\n slug\n title\n category_name\n learning_path {\n id\n }\n }\n }\n": types.CourseQueryDocument,
"\n query competenceCertificateQuery($courseSlug: String!, $courseSessionId: ID!) {\n competence_certificate_list(course_slug: $courseSlug) {\n ...CoursePageFields\n competence_certificates {\n ...CoursePageFields\n assignments {\n ...CoursePageFields\n assignment_type\n max_points\n completion(course_session_id: $courseSessionId) {\n id\n completion_status\n submitted_at\n evaluation_points\n evaluation_max_points\n evaluation_passed\n }\n learning_content {\n title\n id\n slug\n content_type\n frontend_url\n circle {\n ...CoursePageFields\n }\n }\n }\n }\n }\n }\n": types.CompetenceCertificateQueryDocument,
"\n query courseSessionDetail($courseSessionId: ID!) {\n course_session(id: $courseSessionId) {\n id\n title\n course {\n id\n title\n slug\n }\n users {\n id\n user_id\n first_name\n last_name\n email\n avatar_url\n role\n circles {\n id\n title\n slug\n }\n }\n attendance_courses {\n id\n location\n trainer\n due_date {\n id\n start\n end\n }\n learning_content_id\n learning_content {\n id\n title\n circle {\n id\n title\n slug\n }\n }\n }\n assignments {\n id\n submission_deadline {\n id\n start\n }\n evaluation_deadline {\n id\n start\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n edoniq_tests {\n id\n deadline {\n id\n start\n end\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n }\n }\n": types.CourseSessionDetailDocument,
"\n mutation SendFeedbackMutation(\n $courseSessionId: ID!\n $learningContentId: ID!\n $data: GenericScalar!\n $submitted: Boolean\n ) {\n send_feedback(\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n data: $data\n submitted: $submitted\n ) {\n feedback_response {\n id\n data\n submitted\n }\n errors {\n field\n messages\n }\n }\n }\n": types.SendFeedbackMutationDocument,
};
@ -60,11 +61,15 @@ export function graphql(source: "\n query assignmentCompletionQuery(\n $assi
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query courseQuery($courseId: Int!) {\n course(id: $courseId) {\n id\n slug\n title\n category_name\n learning_path {\n id\n }\n }\n }\n"): (typeof documents)["\n query courseQuery($courseId: Int!) {\n course(id: $courseId) {\n id\n slug\n title\n category_name\n learning_path {\n id\n }\n }\n }\n"];
export function graphql(source: "\n query courseQuery($courseId: ID!) {\n course(id: $courseId) {\n id\n slug\n title\n category_name\n learning_path {\n id\n }\n }\n }\n"): (typeof documents)["\n query courseQuery($courseId: ID!) {\n course(id: $courseId) {\n id\n slug\n title\n category_name\n learning_path {\n id\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query competenceCertificateQuery($courseSlug: String!, $courseSessionId: ID!) {\n competence_certificate_list(course_slug: $courseSlug) {\n ...CoursePageFields\n competence_certificates {\n ...CoursePageFields\n assignments {\n ...CoursePageFields\n assignment_type\n max_points\n completion(course_session_id: $courseSessionId) {\n id\n completion_status\n submitted_at\n evaluation_points\n evaluation_max_points\n evaluation_passed\n }\n learning_content {\n title\n id\n slug\n content_type\n frontend_url\n circle {\n ...CoursePageFields\n }\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query competenceCertificateQuery($courseSlug: String!, $courseSessionId: ID!) {\n competence_certificate_list(course_slug: $courseSlug) {\n ...CoursePageFields\n competence_certificates {\n ...CoursePageFields\n assignments {\n ...CoursePageFields\n assignment_type\n max_points\n completion(course_session_id: $courseSessionId) {\n id\n completion_status\n submitted_at\n evaluation_points\n evaluation_max_points\n evaluation_passed\n }\n learning_content {\n title\n id\n slug\n content_type\n frontend_url\n circle {\n ...CoursePageFields\n }\n }\n }\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query courseSessionDetail($courseSessionId: ID!) {\n course_session(id: $courseSessionId) {\n id\n title\n course {\n id\n title\n slug\n }\n users {\n id\n user_id\n first_name\n last_name\n email\n avatar_url\n role\n circles {\n id\n title\n slug\n }\n }\n attendance_courses {\n id\n location\n trainer\n due_date {\n id\n start\n end\n }\n learning_content_id\n learning_content {\n id\n title\n circle {\n id\n title\n slug\n }\n }\n }\n assignments {\n id\n submission_deadline {\n id\n start\n }\n evaluation_deadline {\n id\n start\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n edoniq_tests {\n id\n deadline {\n id\n start\n end\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query courseSessionDetail($courseSessionId: ID!) {\n course_session(id: $courseSessionId) {\n id\n title\n course {\n id\n title\n slug\n }\n users {\n id\n user_id\n first_name\n last_name\n email\n avatar_url\n role\n circles {\n id\n title\n slug\n }\n }\n attendance_courses {\n id\n location\n trainer\n due_date {\n id\n start\n end\n }\n learning_content_id\n learning_content {\n id\n title\n circle {\n id\n title\n slug\n }\n }\n }\n assignments {\n id\n submission_deadline {\n id\n start\n }\n evaluation_deadline {\n id\n start\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n edoniq_tests {\n id\n deadline {\n id\n start\n end\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/

File diff suppressed because one or more lines are too long

View File

@ -8,11 +8,12 @@ type Query {
learning_content_learning_module: LearningContentLearningModuleObjectType
learning_content_placeholder: LearningContentPlaceholderObjectType
learning_content_rich_text: LearningContentRichTextObjectType
learning_content_test: LearningContentTestObjectType
learning_content_test: LearningContentEdoniqTestObjectType
learning_content_video: LearningContentVideoObjectType
learning_content_document_list: LearningContentDocumentListObjectType
course_session_attendance_course(id: ID!, assignment_user_id: ID): CourseSessionAttendanceCourseType
course(id: Int): CourseObjectType
course_session_attendance_course(id: ID!, assignment_user_id: ID): CourseSessionAttendanceCourseObjectType
course(id: ID): CourseObjectType
course_session(id: ID): CourseSessionObjectType
competence_certificate(id: ID, slug: String): CompetenceCertificateObjectType
competence_certificate_list(id: ID, slug: String, course_id: ID, course_slug: String): CompetenceCertificateListObjectType
assignment(id: ID, slug: String): AssignmentObjectType
@ -307,12 +308,167 @@ type AssignmentCompletionObjectType {
edoniq_extended_time_flag: Boolean!
assignment_user: UserType!
assignment: AssignmentObjectType!
course_session: CourseSessionObjectType!
completion_status: AssignmentAssignmentCompletionCompletionStatusChoices!
completion_data: GenericScalar
additional_json_data: JSONString!
learning_content_page_id: ID
}
type CourseSessionObjectType {
id: ID!
created_at: DateTime!
updated_at: DateTime!
course: CourseObjectType!
title: String!
start_date: Date
end_date: Date
attendance_courses: [CourseSessionAttendanceCourseObjectType]
assignments: [CourseSessionAssignmentObjectType]
edoniq_tests: [CourseSessionEdoniqTestObjectType]
documents: [CircleDocumentObjectType]
users: [CourseSessionUserObjectsType]
}
"""
The `Date` scalar type represents a Date
value as specified by
[iso8601](https://en.wikipedia.org/wiki/ISO_8601).
"""
scalar Date
type CourseSessionAttendanceCourseObjectType {
id: ID!
learning_content: LearningContentAttendanceCourseObjectType
due_date: DueDateObjectType
location: String!
trainer: String!
course_session_id: ID
learning_content_id: ID
attendance_user_list: [AttendanceUserObjectType]
}
type LearningContentAttendanceCourseObjectType implements LearningContentInterface {
id: ID
title: String
slug: String
content_type: String
live: Boolean
translation_key: String
frontend_url: String
circle: CircleObjectType
course: CourseObjectType
minutes: Int
description: String
content: String
}
type DueDateObjectType {
id: ID!
"""Startdatum ist Pflicht"""
start: DateTime
"""Enddatum ist optional"""
end: DateTime
"""Nur aktivieren, wenn man die Felder manuell überschreiben will"""
manual_override_fields: Boolean!
"""Title wird standarmässig vom LearningContent übernommen"""
title: String!
"""Translation Key aus dem Frontend"""
assignment_type_translation_key: String!
"""Translation Key aus dem Frontend"""
date_type_translation_key: String!
"""
Überschreibt den Untertitel bei `assignment_type_translation_key` und `date_type_translation_key`
"""
subtitle: String!
"""
URL wird vom LearningContent übernommen (sichtbar für Member/Teilnehmer)
"""
url: String!
course_session: CourseSessionObjectType!
}
type AttendanceUserObjectType {
user_id: UUID!
status: AttendanceUserStatus!
first_name: String
last_name: String
email: String
}
"""An enumeration."""
enum AttendanceUserStatus {
PRESENT
ABSENT
}
type CourseSessionAssignmentObjectType {
id: ID!
learning_content: LearningContentAssignmentObjectType
submission_deadline: DueDateObjectType
evaluation_deadline: DueDateObjectType
course_session_id: ID
learning_content_id: ID
}
type CourseSessionEdoniqTestObjectType {
id: ID!
learning_content: LearningContentEdoniqTestObjectType
deadline: DueDateObjectType
course_session_id: ID
learning_content_id: ID
}
type LearningContentEdoniqTestObjectType implements LearningContentInterface {
content_assignment: AssignmentObjectType
id: ID
title: String
slug: String
content_type: String
live: Boolean
translation_key: String
frontend_url: String
circle: CircleObjectType
course: CourseObjectType
minutes: Int
description: String
content: String
}
type CircleDocumentObjectType {
id: UUID!
name: String!
course_session: CourseSessionObjectType!
learning_sequence: LearningSequenceObjectType!
file_name: String
url: String
}
type CourseSessionUserObjectsType {
id: UUID!
role: String
user_id: UUID
first_name: String
last_name: String
email: String
avatar_url: String
circles: [CourseSessionUserExpertCircleType]
}
type CourseSessionUserExpertCircleType {
id: ID
title: String
slug: String
}
"""An enumeration."""
enum AssignmentAssignmentCompletionCompletionStatusChoices {
"""IN_PROGRESS"""
@ -361,21 +517,6 @@ enum LearnpathLearningContentAssignmentAssignmentTypeChoices {
EDONIQ_TEST
}
type LearningContentAttendanceCourseObjectType implements LearningContentInterface {
id: ID
title: String
slug: String
content_type: String
live: Boolean
translation_key: String
frontend_url: String
circle: CircleObjectType
course: CourseObjectType
minutes: Int
description: String
content: String
}
type LearningContentFeedbackObjectType implements LearningContentInterface {
id: ID
title: String
@ -436,22 +577,6 @@ type LearningContentRichTextObjectType implements LearningContentInterface {
content: String
}
type LearningContentTestObjectType implements LearningContentInterface {
content_assignment: AssignmentObjectType
id: ID
title: String
slug: String
content_type: String
live: Boolean
translation_key: String
frontend_url: String
circle: CircleObjectType
course: CourseObjectType
minutes: Int
description: String
content: String
}
type LearningContentVideoObjectType implements LearningContentInterface {
id: ID
title: String
@ -482,32 +607,6 @@ type LearningContentDocumentListObjectType implements LearningContentInterface {
content: String
}
type CourseSessionAttendanceCourseType {
id: ID!
location: String!
trainer: String!
course_session_id: ID
learning_content_id: ID
due_date_id: ID
end: DateTime
start: DateTime
attendance_user_list: [AttendanceUserType]
}
type AttendanceUserType {
user_id: UUID!
status: AttendanceUserStatus!
first_name: String
last_name: String
email: String
}
"""An enumeration."""
enum AttendanceUserStatus {
PRESENT
ABSENT
}
type CompetenceCertificateListObjectType implements CoursePageInterface {
id: ID
path: String!
@ -577,7 +676,7 @@ type ErrorType {
}
type AttendanceCourseUserMutation {
course_session_attendance_course: CourseSessionAttendanceCourseType
course_session_attendance_course: CourseSessionAttendanceCourseObjectType
}
input AttendanceUserInputType {

View File

@ -6,17 +6,25 @@ export const AssignmentCompletionStatus = "AssignmentCompletionStatus";
export const AssignmentObjectType = "AssignmentObjectType";
export const AttendanceCourseUserMutation = "AttendanceCourseUserMutation";
export const AttendanceUserInputType = "AttendanceUserInputType";
export const AttendanceUserObjectType = "AttendanceUserObjectType";
export const AttendanceUserStatus = "AttendanceUserStatus";
export const AttendanceUserType = "AttendanceUserType";
export const Boolean = "Boolean";
export const CircleDocumentObjectType = "CircleDocumentObjectType";
export const CircleObjectType = "CircleObjectType";
export const CompetenceCertificateListObjectType = "CompetenceCertificateListObjectType";
export const CompetenceCertificateObjectType = "CompetenceCertificateObjectType";
export const CoreUserLanguageChoices = "CoreUserLanguageChoices";
export const CourseObjectType = "CourseObjectType";
export const CoursePageInterface = "CoursePageInterface";
export const CourseSessionAttendanceCourseType = "CourseSessionAttendanceCourseType";
export const CourseSessionAssignmentObjectType = "CourseSessionAssignmentObjectType";
export const CourseSessionAttendanceCourseObjectType = "CourseSessionAttendanceCourseObjectType";
export const CourseSessionEdoniqTestObjectType = "CourseSessionEdoniqTestObjectType";
export const CourseSessionObjectType = "CourseSessionObjectType";
export const CourseSessionUserExpertCircleType = "CourseSessionUserExpertCircleType";
export const CourseSessionUserObjectsType = "CourseSessionUserObjectsType";
export const Date = "Date";
export const DateTime = "DateTime";
export const DueDateObjectType = "DueDateObjectType";
export const ErrorType = "ErrorType";
export const FeedbackResponseObjectType = "FeedbackResponseObjectType";
export const Float = "Float";
@ -28,13 +36,13 @@ export const JSONString = "JSONString";
export const LearningContentAssignmentObjectType = "LearningContentAssignmentObjectType";
export const LearningContentAttendanceCourseObjectType = "LearningContentAttendanceCourseObjectType";
export const LearningContentDocumentListObjectType = "LearningContentDocumentListObjectType";
export const LearningContentEdoniqTestObjectType = "LearningContentEdoniqTestObjectType";
export const LearningContentFeedbackObjectType = "LearningContentFeedbackObjectType";
export const LearningContentInterface = "LearningContentInterface";
export const LearningContentLearningModuleObjectType = "LearningContentLearningModuleObjectType";
export const LearningContentMediaLibraryObjectType = "LearningContentMediaLibraryObjectType";
export const LearningContentPlaceholderObjectType = "LearningContentPlaceholderObjectType";
export const LearningContentRichTextObjectType = "LearningContentRichTextObjectType";
export const LearningContentTestObjectType = "LearningContentTestObjectType";
export const LearningContentVideoObjectType = "LearningContentVideoObjectType";
export const LearningPathObjectType = "LearningPathObjectType";
export const LearningSequenceObjectType = "LearningSequenceObjectType";

View File

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

View File

@ -76,7 +76,7 @@ export const ASSIGNMENT_COMPLETION_QUERY = graphql(`
`);
export const COURSE_QUERY = graphql(`
query courseQuery($courseId: Int!) {
query courseQuery($courseId: ID!) {
course(id: $courseId) {
id
slug
@ -122,3 +122,88 @@ export const COMPETENCE_NAVI_CERTIFICATE_QUERY = graphql(`
}
}
`);
export const COURSE_SESSION_DETAIL_QUERY = graphql(`
query courseSessionDetail($courseSessionId: ID!) {
course_session(id: $courseSessionId) {
id
title
course {
id
title
slug
}
users {
id
user_id
first_name
last_name
email
avatar_url
role
circles {
id
title
slug
}
}
attendance_courses {
id
location
trainer
due_date {
id
start
end
}
learning_content_id
learning_content {
id
title
circle {
id
title
slug
}
}
}
assignments {
id
submission_deadline {
id
start
}
evaluation_deadline {
id
start
}
learning_content {
id
title
content_assignment {
id
title
assignment_type
}
}
}
edoniq_tests {
id
deadline {
id
start
end
}
learning_content {
id
title
content_assignment {
id
title
assignment_type
}
}
}
}
}
`);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,28 +8,46 @@
{{ $t("circlePage.documents.userDescription") }}
</div>
</div>
<ul
v-if="courseSessionsStore.circleDocuments.length"
class="mt-8 border-t border-t-gray-500"
>
<template
v-for="learningSequence of courseSessionsStore.circleDocuments"
:key="learningSequence.id"
>
<DocumentListItem
v-for="doc of learningSequence.documents"
:key="doc.url"
:subtitle="learningSequence.title"
:doc="doc"
/>
</template>
<ul v-if="circleDocuments.length" class="mt-8 border-t border-t-gray-500">
<DocumentListItem
v-for="doc of circleDocuments"
:key="doc.url"
:subtitle="doc.learning_sequence.title"
:doc="doc"
/>
</ul>
</div>
</template>
<script setup lang="ts">
import { useCourseSessionsStore } from "@/stores/courseSessions";
import DocumentListItem from "@/components/circle/DocumentListItem.vue";
import { useCurrentCourseSession } from "@/composables";
import { computed, onMounted, ref } from "vue";
import { useCircleStore } from "@/stores/circle";
import type { CircleDocument } from "@/types";
import { fetchCourseSessionDocuments } from "@/services/files";
const courseSessionsStore = useCourseSessionsStore();
const courseSession = useCurrentCourseSession();
const circleStore = useCircleStore();
const circleDocumentsResultData = ref<CircleDocument[]>([]);
async function fetchDocuments() {
const result = await fetchCourseSessionDocuments(courseSession.value?.id);
if (result.length > 0) {
circleDocumentsResultData.value = result;
} else {
circleDocumentsResultData.value = [];
}
}
const circleDocuments = computed(() => {
return circleDocumentsResultData.value.filter(
(d) => d.learning_sequence.circle.slug === circleStore.circle?.slug
);
});
onMounted(async () => {
await fetchDocuments();
});
</script>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,9 +1,6 @@
import { useCourseSessionDetailQuery } from "@/composables";
import { itGet } from "@/fetchHelpers";
import type { LearningPath } from "@/services/learningPath";
import { useCockpitStore } from "@/stores/cockpit";
import { useCourseSessionsStore } from "@/stores/courseSessions";
import { useLearningPathStore } from "@/stores/learningPath";
import { useUserStore } from "@/stores/user";
import type {
Assignment,
AssignmentCompletion,
@ -31,23 +28,21 @@ export function calcLearningContentAssignments(learningPath?: LearningPath) {
}
export async function loadAssignmentCompletionStatusData(
assignmentId: number,
courseSessionId: number,
learningContentId: number
assignmentId: string,
courseSessionId: string,
learningContentId: string
) {
const cockpitStore = useCockpitStore();
const courseSessionDetailResult = useCourseSessionDetailQuery();
const assignmentCompletionData = (await itGet(
`/api/assignment/${assignmentId}/${courseSessionId}/status/`
)) as UserAssignmentCompletionStatus[];
const courseSessionUsers = await cockpitStore.loadCourseSessionMembers(
courseSessionId
);
const members = courseSessionDetailResult.filterMembers();
const gradedUsers: GradedUser[] = [];
const assignmentSubmittedUsers: CourseSessionUser[] = [];
for (const csu of courseSessionUsers) {
for (const csu of members) {
const userAssignmentStatus = assignmentCompletionData.find(
(s) =>
s.assignment_user_id === csu.user_id &&
@ -70,34 +65,10 @@ export async function loadAssignmentCompletionStatusData(
return {
assignmentSubmittedUsers: assignmentSubmittedUsers,
gradedUsers: gradedUsers,
total: courseSessionUsers.length,
total: members.length,
};
}
export function findAssignmentDetail(assignmentId: number) {
const learningPathStore = useLearningPathStore();
const userStore = useUserStore();
const courseSessionsStore = useCourseSessionsStore();
// TODO: filter by selected circle
if (!courseSessionsStore.currentCourseSession) {
return undefined;
}
const learningContents = calcLearningContentAssignments(
learningPathStore.learningPathForUser(
courseSessionsStore.currentCourseSession.course.slug,
userStore.id
)
);
const learningContent = learningContents.find(
(lc) => lc.content_assignment_id === assignmentId
);
return courseSessionsStore.findCourseSessionAssignment(learningContent?.id);
}
export function maxAssignmentPoints(assignment: Assignment) {
return sum(assignment.evaluation_tasks.map((task) => task.value.max_points));
}

View File

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

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

View File

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

View File

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

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

View File

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

View File

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

View File

@ -1,15 +1,5 @@
import { itGetCached, itPost } from "@/fetchHelpers";
import { deleteCircleDocument } from "@/services/files";
import type {
CircleDocument,
CourseSession,
CourseSessionAssignment,
CourseSessionAttendanceCourse,
CourseSessionEdoniqTest,
CourseSessionUser,
DueDate,
ExpertSessionUser,
} from "@/types";
import { itGetCached } from "@/fetchHelpers";
import type { CourseSession, DueDate } from "@/types";
import eventBus from "@/utils/eventBus";
import { useRouteLookups } from "@/utils/route";
import { useLocalStorage } from "@vueuse/core";
@ -18,7 +8,6 @@ import uniqBy from "lodash/uniqBy";
import log from "loglevel";
import { defineStore } from "pinia";
import { computed, ref } from "vue";
import { useCircleStore } from "./circle";
import { useUserStore } from "./user";
const SELECTED_COURSE_SESSIONS_KEY = "selectedCourseSessionMap";
@ -39,10 +28,6 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
// TODO: refactor after implementing of Klassenkonzept
await Promise.all(
allCourseSessions.value.map(async (cs) => {
const users = (await itGetCached(`/api/course/sessions/${cs.id}/users/`, {
reload: reload,
})) as CourseSessionUser[];
cs.users = users;
sortDueDates(cs.due_dates);
})
);
@ -57,7 +42,7 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
const selectedCourseSessionMap = useLocalStorage(
SELECTED_COURSE_SESSIONS_KEY,
new Map<string, number>()
new Map<string, string>()
);
const _currentCourseSlug = ref("");
@ -99,15 +84,15 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
eventBus.emit("switchedCourseSession", courseSession.id);
}
function getCourseSessionById(courseSessionId: number | string) {
function getCourseSessionById(courseSessionId: string) {
return allCourseSessions.value.find((cs) => {
return courseSessionId.toString() === cs.id.toString();
return courseSessionId === cs.id;
});
}
function switchCourseSessionById(courseSessionId: number | string) {
function switchCourseSessionById(courseSessionId: string) {
const courseSession = allCourseSessions.value.find((cs) => {
return courseSessionId.toString() === cs.id.toString();
return courseSessionId === cs.id;
});
if (courseSession) {
_switchCourseSession(courseSession);
@ -161,64 +146,14 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
return Boolean(isCourseExpert && (inLearningPath() || inCompetenceProfile()));
});
const circleExperts = computed(() => {
const circleStore = useCircleStore();
const circleTranslationKey = circleStore.circle?.translation_key;
if (currentCourseSession.value && circleTranslationKey) {
return currentCourseSession.value.users.filter((u) => {
if (u.role === "EXPERT") {
return (u as ExpertSessionUser).circles
.map((c) => c.translation_key)
.includes(circleTranslationKey);
}
return false;
}) as ExpertSessionUser[];
}
return [];
});
const canUploadCircleDocuments = computed(() => {
function hasCockpit(courseSession: CourseSession) {
const userStore = useUserStore();
return (
circleExperts.value.filter((expert) => expert.user_id === userStore.id).length > 0
);
});
const circleDocuments = computed(() => {
const circleStore = useCircleStore();
return (
circleStore.circle?.learningSequences
.map((ls) => ({ id: ls.id, title: ls.title, documents: [] }))
.map((ls: { id: number; title: string; documents: CircleDocument[] }) => {
if (currentCourseSession.value === undefined) {
return ls;
}
for (const document of currentCourseSession.value.documents) {
if (document.learning_sequence === ls.id) {
ls.documents.push(document);
}
}
return ls;
})
.filter((ls) => ls.documents.length > 0) || []
);
});
function hasCockpit(couseSession: CourseSession) {
const userStore = useUserStore();
return (
userStore.course_session_experts.includes(couseSession.id) ||
userStore.course_session_experts.includes(courseSession.id) ||
userStore.is_superuser
);
}
function addDocument(document: CircleDocument) {
currentCourseSession.value?.documents.push(document);
}
function allDueDates() {
const allDueDatesReturn: DueDate[] = [];
@ -243,56 +178,6 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
});
}
async function startUpload() {
log.debug("loadCourseSessionsData called");
allCourseSessions.value = await itPost(`/api/core/file/start`, {
file_type: "image/png",
file_name: "test.png",
});
}
async function removeDocument(documentId: string) {
await deleteCircleDocument(documentId);
if (currentCourseSession.value === undefined) {
return;
}
currentCourseSession.value.documents = currentCourseSession.value?.documents.filter(
(d) => d.id !== documentId
);
}
function findAttendanceCourse(
contentId: number
): CourseSessionAttendanceCourse | undefined {
if (currentCourseSession.value) {
return currentCourseSession.value.attendance_courses.find(
(attendanceCourse) => attendanceCourse.learning_content_id === contentId
);
}
}
function findCourseSessionAssignment(
contentId?: number
): CourseSessionAssignment | undefined {
if (contentId && currentCourseSession.value) {
return currentCourseSession.value.assignments.find(
(a) => a.learning_content_id === contentId
);
}
}
function findCourseSessionEdoniqTest(
contentId?: number
): CourseSessionEdoniqTest | undefined {
if (contentId && currentCourseSession.value) {
return currentCourseSession.value.edoniq_tests.find(
(a) => a.learning_content_id === contentId
);
}
}
return {
uniqueCourseSessionsByCourse,
allCurrentCourseSessions,
@ -302,15 +187,6 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
hasCockpit,
hasCourseSessionPreview,
currentCourseSessionHasCockpit,
canUploadCircleDocuments,
circleDocuments,
circleExperts,
addDocument,
startUpload,
removeDocument,
findAttendanceCourse,
findCourseSessionAssignment,
findCourseSessionEdoniqTest,
allDueDates,
// use `useCurrentCourseSession` whenever possible

View File

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

View File

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

View File

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

View File

@ -1,16 +1,35 @@
import type { AssignmentType, CourseSession } from "@/types";
import type { AssignmentType } from "@/types";
import { useTranslation } from "i18next-vue";
export function assertUnreachable(msg: string): never {
throw new Error("Didn't expect to get here, " + msg);
}
export function getCompetenceBaseUrl(courseSession: CourseSession): string {
return courseSession.competence_url.replace(
// TODO: remove the `competence_url` with url to Navi...
"/competences",
""
);
function createCourseUrl(courseSlug: string | undefined, specificSub: string): string {
if (!courseSlug) {
return "/";
}
if (["learn", "media", "competence", "cockpit"].includes(specificSub)) {
return `/course/${courseSlug}/${specificSub}`;
}
return `/course/${courseSlug}`;
}
export function getCompetenceNaviUrl(courseSlug: string | undefined): string {
return createCourseUrl(courseSlug, "competence");
}
export function getMediaCenterUrl(courseSlug: string | undefined): string {
return createCourseUrl(courseSlug, "media");
}
export function getLearningPathUrl(courseSlug: string | undefined): string {
return createCourseUrl(courseSlug, "learn");
}
export function getCockpitUrl(courseSlug: string | undefined): string {
return createCourseUrl(courseSlug, "cockpit");
}
export function getAssignmentTypeTitle(assignmentType: AssignmentType): string {

View File

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

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", () => {
beforeEach(() => {
cy.manageCommand("cypress_reset");
})
});
describe("Expert / Trainer", () => {
beforeEach(() => {
@ -41,5 +46,5 @@ describe("preview.cy.js", () => {
visitCoursePage("competence");
cy.get('[data-cy="course-preview-bar"]').should("not.exist");
});
})
});
});

View File

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

View File

@ -24,6 +24,12 @@ def request_assignment_completion_status(request, assignment_id, course_session_
"evaluation_passed",
"learning_content_page_id",
)
return Response(status=200, data=qs)
# Convert the learning_content_page_id to a string
data = list(qs) # Evaluate the queryset
for item in data:
item["learning_content_page_id"] = str(item["learning_content_page_id"])
return Response(status=200, data=data)
raise PermissionDenied()

View File

@ -40,4 +40,4 @@ class UserSerializer(serializers.ModelSerializer):
role=CourseSessionUser.Role.EXPERT, user=obj
)
return [csu.course_session.id for csu in qs]
return [str(csu.course_session.id) for csu in qs]

View File

@ -2,6 +2,7 @@ import json
import re
from django.utils.safestring import mark_safe
from rest_framework import serializers
from rest_framework.throttling import UserRateThrottle
@ -41,6 +42,11 @@ def get_django_content_type(obj):
return obj._meta.app_label + "." + type(obj).__name__
class StringIDField(serializers.Field):
def to_representation(self, value):
return str(value)
def pretty_print_json(json_string):
try:
parsed = json_string

View File

@ -1,15 +1,22 @@
import graphene
from vbv_lernwelt.course.graphql.types import CourseObjectType
from vbv_lernwelt.course.models import Course
from vbv_lernwelt.course.graphql.types import CourseObjectType, CourseSessionObjectType
from vbv_lernwelt.course.models import Course, CourseSession
from vbv_lernwelt.course.permissions import has_course_access
class CourseQuery(graphene.ObjectType):
course = graphene.Field(CourseObjectType, id=graphene.Int())
course = graphene.Field(CourseObjectType, id=graphene.ID())
course_session = graphene.Field(CourseSessionObjectType, id=graphene.ID())
def resolve_course(root, info, id):
course = Course.objects.get(pk=id)
if has_course_access(info.context.user, course):
return course
raise PermissionError("You do not have access to this course")
def resolve_course_session(root, info, id):
course_session = CourseSession.objects.get(pk=id)
if has_course_access(info.context.user, course_session.course):
return course_session
raise PermissionError("You do not have access to this course session")

View File

@ -2,12 +2,30 @@ from typing import Type
import graphene
import structlog
from graphene import ObjectType
from graphene_django import DjangoObjectType
from graphql import GraphQLError
from rest_framework.exceptions import PermissionDenied
from vbv_lernwelt.course.models import Course, CourseBasePage, CoursePage
from vbv_lernwelt.course.models import (
CircleDocument,
Course,
CourseBasePage,
CoursePage,
CourseSession,
CourseSessionUser,
)
from vbv_lernwelt.course.permissions import has_course_access
from vbv_lernwelt.course_session.graphql.types import (
CourseSessionAssignmentObjectType,
CourseSessionAttendanceCourseObjectType,
CourseSessionEdoniqTestObjectType,
)
from vbv_lernwelt.course_session.models import (
CourseSessionAssignment,
CourseSessionAttendanceCourse,
CourseSessionEdoniqTest,
)
from vbv_lernwelt.learnpath.graphql.types import LearningPathObjectType
logger = structlog.get_logger(__name__)
@ -74,3 +92,113 @@ class CourseObjectType(DjangoObjectType):
def resolve_learning_path(self, info):
return self.get_learning_path()
class CourseSessionUserExpertCircleType(ObjectType):
id = graphene.ID()
title = graphene.String()
slug = graphene.String()
class CourseSessionUserObjectsType(DjangoObjectType):
user_id = graphene.UUID()
first_name = graphene.String()
last_name = graphene.String()
email = graphene.String()
avatar_url = graphene.String()
role = graphene.String()
circles = graphene.List(CourseSessionUserExpertCircleType)
class Meta:
model = CourseSessionUser
fields = (
"id",
"user_id",
"first_name",
"last_name",
"email",
"avatar_url",
"role",
)
def resolve_user_id(self, info):
return self.user.id
def resolve_first_name(self, info):
return self.user.first_name
def resolve_last_name(self, info):
return self.user.last_name
def resolve_email(self, info):
return self.user.email
def resolve_avatar_url(self, info):
return self.user.avatar_url
def resolve_role(self, info):
return self.role
def resolve_circles(self, info):
return self.expert.all().values("id", "title", "slug", "translation_key")
class CircleDocumentObjectType(DjangoObjectType):
file_name = graphene.String()
url = graphene.String()
class Meta:
model = CircleDocument
fields = [
"id",
"name",
"file_name",
"url",
"course_session",
"learning_sequence",
]
def resolve_file_name(self, info):
return self.file_name
def resolve_url(self, info):
return self.url
class CourseSessionObjectType(DjangoObjectType):
attendance_courses = graphene.List(CourseSessionAttendanceCourseObjectType)
assignments = graphene.List(CourseSessionAssignmentObjectType)
edoniq_tests = graphene.List(CourseSessionEdoniqTestObjectType)
documents = graphene.List(CircleDocumentObjectType)
users = graphene.List(CourseSessionUserObjectsType)
class Meta:
model = CourseSession
fields = (
"id",
"created_at",
"updated_at",
"course",
"title",
"start_date",
"end_date",
"users",
)
def resolve_attendance_courses(self, info):
return CourseSessionAttendanceCourse.objects.filter(course_session=self)
def resolve_assignments(self, info):
return CourseSessionAssignment.objects.filter(course_session=self)
def resolve_edoniq_tests(self, info):
return CourseSessionEdoniqTest.objects.filter(course_session=self)
def resolve_documents(self, info):
return CircleDocument.objects.filter(
course_session=self, file__upload_finished_at__isnull=False
)
def resolve_users(self, info):
return CourseSessionUser.objects.filter(course_session_id=self.id).distinct()

View File

@ -294,21 +294,6 @@ class CourseSessionUser(models.Model):
]
ordering = ["user__last_name", "user__first_name", "user__email"]
def to_dict(self):
return {
"session_id": self.course_session.id,
"session_title": self.course_session.title,
"user_id": self.user.id,
"first_name": self.user.first_name,
"last_name": self.user.last_name,
"email": self.user.email,
"avatar_url": self.user.avatar_url,
"role": self.role,
"circles": self.expert.all().values(
"id", "title", "slug", "translation_key"
),
}
class CircleDocument(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
@ -322,6 +307,9 @@ class CircleDocument(models.Model):
"learnpath.LearningSequence", on_delete=models.CASCADE
)
def get_circle(self):
return self.learning_sequence.get_circle()
@property
def url(self) -> str:
return self.file.url

View File

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

View File

@ -1,5 +1,6 @@
from rest_framework import serializers
from vbv_lernwelt.core.utils import StringIDField
from vbv_lernwelt.course.models import (
CircleDocument,
Course,
@ -7,21 +8,13 @@ from vbv_lernwelt.course.models import (
CourseCompletion,
CourseSession,
)
from vbv_lernwelt.course_session.models import (
CourseSessionAssignment,
CourseSessionAttendanceCourse,
CourseSessionEdoniqTest,
)
from vbv_lernwelt.course_session.serializers import (
CourseSessionAssignmentSerializer,
CourseSessionAttendanceCourseSerializer,
CourseSessionEdoniqTestSerializer,
)
from vbv_lernwelt.duedate.models import DueDate
from vbv_lernwelt.duedate.serializers import DueDateSerializer
class CourseSerializer(serializers.ModelSerializer):
id = StringIDField()
class Meta:
model = Course
fields = ["id", "title", "category_name", "slug"]
@ -38,6 +31,9 @@ class CourseCategorySerializer(serializers.ModelSerializer):
class CourseCompletionSerializer(serializers.ModelSerializer):
page_id = StringIDField()
course_session_id = StringIDField()
class Meta:
model = CourseCompletion
fields = [
@ -54,56 +50,17 @@ class CourseCompletionSerializer(serializers.ModelSerializer):
class CourseSessionSerializer(serializers.ModelSerializer):
id = StringIDField()
course = serializers.SerializerMethodField()
course_url = serializers.SerializerMethodField()
learning_path_url = serializers.SerializerMethodField()
cockpit_url = serializers.SerializerMethodField()
competence_url = serializers.SerializerMethodField()
media_library_url = serializers.SerializerMethodField()
documents = serializers.SerializerMethodField()
attendance_courses = serializers.SerializerMethodField()
assignments = serializers.SerializerMethodField()
edoniq_tests = serializers.SerializerMethodField()
# course_url = serializers.SerializerMethodField()
due_dates = serializers.SerializerMethodField()
def get_course(self, obj):
return CourseSerializer(obj.course).data
def get_course_url(self, obj):
return obj.course.get_course_url()
def get_learning_path_url(self, obj):
return obj.course.get_learning_path_url()
def get_cockpit_url(self, obj):
return obj.course.get_cockpit_url()
def get_media_library_url(self, obj):
return obj.course.get_media_library_url()
def get_competence_url(self, obj):
return obj.course.get_competence_url()
def get_documents(self, obj):
documents = CircleDocument.objects.filter(
course_session=obj, file__upload_finished_at__isnull=False
)
return CircleDocumentSerializer(documents, many=True).data
def get_attendance_courses(self, obj):
return CourseSessionAttendanceCourseSerializer(
CourseSessionAttendanceCourse.objects.filter(course_session=obj), many=True
).data
def get_assignments(self, obj):
return CourseSessionAssignmentSerializer(
CourseSessionAssignment.objects.filter(course_session=obj), many=True
).data
def get_edoniq_tests(self, obj):
return CourseSessionEdoniqTestSerializer(
CourseSessionEdoniqTest.objects.filter(course_session=obj), many=True
).data
# def get_course_url(self, obj):
# return obj.course.get_course_url()
def get_due_dates(self, obj):
due_dates = DueDate.objects.filter(course_session=obj)
@ -119,21 +76,13 @@ class CourseSessionSerializer(serializers.ModelSerializer):
"title",
"start_date",
"end_date",
"additional_json_data",
"attendance_courses",
"assignments",
"edoniq_tests",
"learning_path_url",
"cockpit_url",
"competence_url",
"media_library_url",
"course_url",
"documents",
"due_dates",
]
class CircleDocumentSerializer(serializers.ModelSerializer):
learning_sequence = serializers.SerializerMethodField()
class Meta:
model = CircleDocument
fields = [
@ -145,6 +94,20 @@ class CircleDocumentSerializer(serializers.ModelSerializer):
"learning_sequence",
]
def get_learning_sequence(self, obj):
ls = obj.learning_sequence
circle = ls.get_circle()
return {
"title": ls.title,
"id": str(ls.id),
"slug": ls.slug,
"circle": {
"title": circle.title,
"id": str(circle.id),
"slug": circle.slug,
},
}
class DocumentUploadStartInputSerializer(serializers.Serializer):
file_name = serializers.CharField()

View File

@ -48,7 +48,7 @@ class CourseCompletionApiTestCase(APITestCase):
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response_json), 1)
self.assertEqual(response_json[0]["page_id"], learning_content.id)
self.assertEqual(response_json[0]["page_id"], str(learning_content.id))
self.assertEqual(
response_json[0]["page_type"], "learnpath.LearningContentPlaceholder"
)
@ -67,7 +67,7 @@ class CourseCompletionApiTestCase(APITestCase):
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response_json), 1)
self.assertEqual(response_json[0]["page_id"], learning_content.id)
self.assertEqual(response_json[0]["page_id"], str(learning_content.id))
self.assertEqual(
response_json[0]["page_type"], "learnpath.LearningContentPlaceholder"
)
@ -86,7 +86,7 @@ class CourseCompletionApiTestCase(APITestCase):
response_json = response.json()
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response_json), 1)
self.assertEqual(response_json[0]["page_id"], learning_content.id)
self.assertEqual(response_json[0]["page_id"], str(learning_content.id))
self.assertEqual(
response_json[0]["page_type"], "learnpath.LearningContentPlaceholder"
)

View File

@ -40,7 +40,7 @@ class CourseCompletionApiTestCase(APITestCase):
self.assertEqual(len(response.json()), 1)
print(json.dumps(response.json(), indent=4))
self.assertEqual(response.json()[0]["id"], self.course_session.id)
self.assertEqual(response.json()[0]["id"], str(self.course_session.id))
def test_api_superUser_canAccessEveryCourseSession(self):
self.client.login(username="admin", password="test")
@ -50,4 +50,4 @@ class CourseCompletionApiTestCase(APITestCase):
self.assertEqual(len(response.json()), 1)
print(json.dumps(response.json(), indent=4))
self.assertEqual(response.json()[0]["id"], self.course_session.id)
self.assertEqual(response.json()[0]["id"], str(self.course_session.id))

View File

@ -6,7 +6,6 @@ from rest_framework.response import Response
from wagtail.models import Page
from vbv_lernwelt.core.utils import get_django_content_type
from vbv_lernwelt.course.models import (
CircleDocument,
CourseCompletion,
@ -135,7 +134,9 @@ def mark_course_completion_view(request):
@api_view(["GET"])
def get_course_sessions(request):
try:
course_sessions = course_sessions_for_user_qs(request.user)
course_sessions = course_sessions_for_user_qs(request.user).prefetch_related(
"course"
)
return Response(
status=200, data=CourseSessionSerializer(course_sessions, many=True).data
)
@ -146,20 +147,6 @@ def get_course_sessions(request):
return Response({"error": str(e)}, status=404)
@api_view(["GET"])
def get_course_session_users(request, course_session_id):
try:
qs = CourseSessionUser.objects.filter(course_session_id=course_session_id)
user_data = [csu.to_dict() for csu in qs]
return Response(status=200, data=user_data)
except PermissionDenied as e:
raise e
except Exception as e:
logger.error(e)
return Response({"error": str(e)}, status=404)
@api_view(["POST"])
def document_upload_start(request):
serializer = DocumentUploadStartInputSerializer(data=request.data)

View File

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

View File

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

View File

@ -1,11 +1,22 @@
import graphene
from graphene_django import DjangoObjectType
from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse
from vbv_lernwelt.course.permissions import is_course_session_expert
from vbv_lernwelt.course_session.models import (
CourseSessionAssignment,
CourseSessionAttendanceCourse,
CourseSessionEdoniqTest,
)
from vbv_lernwelt.course_session.services.attendance import AttendanceUserStatus
from vbv_lernwelt.duedate.graphql.types import DueDateObjectType
from vbv_lernwelt.learnpath.graphql.types import (
LearningContentAssignmentObjectType,
LearningContentAttendanceCourseObjectType,
LearningContentEdoniqTestObjectType,
)
class AttendanceUserType(graphene.ObjectType):
class AttendanceUserObjectType(graphene.ObjectType):
user_id = graphene.UUID(required=True)
status = graphene.Field(
graphene.Enum.from_enum(AttendanceUserStatus), required=True
@ -15,35 +26,61 @@ class AttendanceUserType(graphene.ObjectType):
email = graphene.String()
class CourseSessionAttendanceCourseType(DjangoObjectType):
class CourseSessionAttendanceCourseObjectType(DjangoObjectType):
course_session_id = graphene.ID(source="course_session_id")
learning_content_id = graphene.ID(source="learning_content_id")
due_date_id = graphene.ID(source="due_date_id")
end = graphene.DateTime()
start = graphene.DateTime()
attendance_user_list = graphene.List(
AttendanceUserType, source="attendance_user_list"
AttendanceUserObjectType, source="attendance_user_list"
)
due_date = graphene.Field(DueDateObjectType)
learning_content = graphene.Field(LearningContentAttendanceCourseObjectType)
class Meta:
model = CourseSessionAttendanceCourse
fields = (
"id",
"course_session_id",
"learning_content_id",
"due_date_id",
"learning_content",
"location",
"trainer",
"start",
"end",
"due_date",
)
def resolve_start(self, info):
if self.due_date is None:
return None
return self.due_date.start
def resolve_attendance_user_list(self, info):
if is_course_session_expert(info.context.user, self.course_session_id):
return self.attendance_user_list
return []
def resolve_end(self, info):
if self.due_date is None:
return None
return self.due_date.end
class CourseSessionAssignmentObjectType(DjangoObjectType):
course_session_id = graphene.ID(source="course_session_id")
learning_content_id = graphene.ID(source="learning_content_id")
submission_deadline = graphene.Field(DueDateObjectType)
evaluation_deadline = graphene.Field(DueDateObjectType)
learning_content = graphene.Field(LearningContentAssignmentObjectType)
class Meta:
model = CourseSessionAssignment
fields = (
"id",
"course_session_id",
"learning_content",
"submission_deadline",
"evaluation_deadline",
)
class CourseSessionEdoniqTestObjectType(DjangoObjectType):
course_session_id = graphene.ID(source="course_session_id")
learning_content_id = graphene.ID(source="learning_content_id")
deadline = graphene.Field(DueDateObjectType)
learning_content = graphene.Field(LearningContentEdoniqTestObjectType)
class Meta:
model = CourseSessionEdoniqTest
fields = (
"id",
"course_session_id",
"learning_content",
"deadline",
)

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)
circle_data = models.JSONField(default=dict, blank=True)
class Meta:
ordering = ["start", "end"]
@ -80,6 +81,21 @@ class DueDate(models.Model):
return result
def save(self, *args, **kwargs):
try:
circle = self.get_circle()
# prefetch circle data, because loading it dynamically is too slow
if circle:
self.circle_data = {
"id": str(circle.id),
"title": circle.title,
"slug": circle.slug,
}
except Exception:
# noop
pass
super().save(*args, **kwargs)
@property
def display_subtitle(self):
result = ""

View File

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

View File

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

View File

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

View File

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

View File

@ -6,7 +6,10 @@ from django.db import migrations
def init_user_notification_emails(apps=None, schema_editor=None):
User = apps.get_model("core", "User")
for u in User.objects.all():
u.additional_json_data["email_notification_categories"] = ["NOTIFICATION"]
u.additional_json_data["email_notification_categories"] = [
"INFORMATION",
"USER_INTERACTION",
]
u.save()