Improve course session loading

This commit is contained in:
Daniel Egger 2023-10-06 09:16:28 +02:00
parent bb50cc60e9
commit 778dde12d7
40 changed files with 809 additions and 398 deletions

View File

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

View File

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

View File

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

View File

@ -1,5 +1,9 @@
import { useCourseSessionsStore } from "@/stores/courseSessions"; import { useCourseSessionsStore } from "@/stores/courseSessions";
import type { CourseSession } from "@/types"; import type { CourseSession, CourseSessionDetail } from "@/types";
import { useQuery } from "@urql/vue";
import { COURSE_SESSION_DETAIL_QUERY } from "@/graphql/queries";
import { useUserStore } from "@/stores/user";
import log from "loglevel"; import log from "loglevel";
import type { ComputedRef } from "vue"; import type { ComputedRef } from "vue";
import { computed } from "vue"; import { computed } from "vue";
@ -28,3 +32,52 @@ export function useCurrentCourseSession() {
); );
return result; return result;
} }
export function useCourseSessionDetailQuery(courSessionId?: string | number) {
if (!courSessionId) {
courSessionId = useCurrentCourseSession().value.id;
}
const queryResult = useQuery({
query: COURSE_SESSION_DETAIL_QUERY,
variables: {
courseSessionId: courSessionId.toString(),
},
});
const courseSessionDetail = computed(() => {
return queryResult.data.value?.course_session as CourseSessionDetail | undefined;
});
function findAssignmentDetail(assignmentId: string) {
return (courseSessionDetail.value?.assignments ?? []).find((a) => {
return a.learning_content?.content_assignment?.id === assignmentId;
});
}
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";
});
}
return {
...queryResult,
courseSessionDetail,
findAssignmentDetail,
findUser,
findCurrentUser,
filterMembers,
};
}

View File

@ -8,11 +8,12 @@ type Query {
learning_content_learning_module: LearningContentLearningModuleObjectType learning_content_learning_module: LearningContentLearningModuleObjectType
learning_content_placeholder: LearningContentPlaceholderObjectType learning_content_placeholder: LearningContentPlaceholderObjectType
learning_content_rich_text: LearningContentRichTextObjectType learning_content_rich_text: LearningContentRichTextObjectType
learning_content_test: LearningContentTestObjectType learning_content_test: LearningContentEdoniqTestObjectType
learning_content_video: LearningContentVideoObjectType learning_content_video: LearningContentVideoObjectType
learning_content_document_list: LearningContentDocumentListObjectType learning_content_document_list: LearningContentDocumentListObjectType
course_session_attendance_course(id: ID!, assignment_user_id: ID): CourseSessionAttendanceCourseType course_session_attendance_course(id: ID!, assignment_user_id: ID): CourseSessionAttendanceCourseObjectType
course(id: Int): CourseObjectType course(id: ID): CourseObjectType
course_session(id: ID): CourseSessionObjectType
competence_certificate(id: ID, slug: String): CompetenceCertificateObjectType competence_certificate(id: ID, slug: String): CompetenceCertificateObjectType
competence_certificate_list(id: ID, slug: String, course_id: ID, course_slug: String): CompetenceCertificateListObjectType competence_certificate_list(id: ID, slug: String, course_id: ID, course_slug: String): CompetenceCertificateListObjectType
assignment(id: ID, slug: String): AssignmentObjectType assignment(id: ID, slug: String): AssignmentObjectType
@ -307,12 +308,155 @@ type AssignmentCompletionObjectType {
edoniq_extended_time_flag: Boolean! edoniq_extended_time_flag: Boolean!
assignment_user: UserType! assignment_user: UserType!
assignment: AssignmentObjectType! assignment: AssignmentObjectType!
course_session: CourseSessionObjectType!
completion_status: AssignmentAssignmentCompletionCompletionStatusChoices! completion_status: AssignmentAssignmentCompletionCompletionStatusChoices!
completion_data: GenericScalar completion_data: GenericScalar
additional_json_data: JSONString! additional_json_data: JSONString!
learning_content_page_id: ID learning_content_page_id: ID
} }
type CourseSessionObjectType {
id: ID!
created_at: DateTime!
updated_at: DateTime!
course: CourseObjectType!
title: String!
start_date: Date
end_date: Date
attendance_courses: [CourseSessionAttendanceCourseObjectType]
assignments: [CourseSessionAssignmentObjectType]
edoniq_tests: [CourseSessionEdoniqTestObjectType]
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"""
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
course_session_id: ID
learning_content_id: ID
deadline: DueDateObjectType
}
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 CourseSessionUserObjectsType {
id: UUID!
role: String
user_id: UUID
first_name: String
last_name: String
email: String
avatar_url: String
circles: [CourseSessionUserExpertCircleType]
}
type CourseSessionUserExpertCircleType {
id: ID
title: String
slug: String
}
"""An enumeration.""" """An enumeration."""
enum AssignmentAssignmentCompletionCompletionStatusChoices { enum AssignmentAssignmentCompletionCompletionStatusChoices {
"""IN_PROGRESS""" """IN_PROGRESS"""
@ -361,21 +505,6 @@ enum LearnpathLearningContentAssignmentAssignmentTypeChoices {
EDONIQ_TEST EDONIQ_TEST
} }
type LearningContentAttendanceCourseObjectType implements LearningContentInterface {
id: ID
title: String
slug: String
content_type: String
live: Boolean
translation_key: String
frontend_url: String
circle: CircleObjectType
course: CourseObjectType
minutes: Int
description: String
content: String
}
type LearningContentFeedbackObjectType implements LearningContentInterface { type LearningContentFeedbackObjectType implements LearningContentInterface {
id: ID id: ID
title: String title: String
@ -436,22 +565,6 @@ type LearningContentRichTextObjectType implements LearningContentInterface {
content: String content: String
} }
type LearningContentTestObjectType implements LearningContentInterface {
content_assignment: AssignmentObjectType
id: ID
title: String
slug: String
content_type: String
live: Boolean
translation_key: String
frontend_url: String
circle: CircleObjectType
course: CourseObjectType
minutes: Int
description: String
content: String
}
type LearningContentVideoObjectType implements LearningContentInterface { type LearningContentVideoObjectType implements LearningContentInterface {
id: ID id: ID
title: String title: String
@ -482,32 +595,6 @@ type LearningContentDocumentListObjectType implements LearningContentInterface {
content: String content: String
} }
type CourseSessionAttendanceCourseType {
id: ID!
location: String!
trainer: String!
course_session_id: ID
learning_content_id: ID
due_date_id: ID
end: DateTime
start: DateTime
attendance_user_list: [AttendanceUserType]
}
type AttendanceUserType {
user_id: UUID!
status: AttendanceUserStatus!
first_name: String
last_name: String
email: String
}
"""An enumeration."""
enum AttendanceUserStatus {
PRESENT
ABSENT
}
type CompetenceCertificateListObjectType implements CoursePageInterface { type CompetenceCertificateListObjectType implements CoursePageInterface {
id: ID id: ID
path: String! path: String!
@ -577,7 +664,7 @@ type ErrorType {
} }
type AttendanceCourseUserMutation { type AttendanceCourseUserMutation {
course_session_attendance_course: CourseSessionAttendanceCourseType course_session_attendance_course: CourseSessionAttendanceCourseObjectType
} }
input AttendanceUserInputType { input AttendanceUserInputType {

View File

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

View File

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

View File

@ -76,7 +76,7 @@ export const ASSIGNMENT_COMPLETION_QUERY = graphql(`
`); `);
export const COURSE_QUERY = graphql(` export const COURSE_QUERY = graphql(`
query courseQuery($courseId: Int!) { query courseQuery($courseId: ID!) {
course(id: $courseId) { course(id: $courseId) {
id id
slug slug
@ -122,3 +122,76 @@ 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
}
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
}
}
assignments {
id
submission_deadline {
id
start
}
evaluation_deadline {
id
start
}
learning_content {
id
content_assignment {
id
title
assignment_type
}
}
}
edoniq_tests {
id
deadline {
id
start
end
}
learning_content {
id
title
}
}
}
}
`);

View File

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

View File

@ -0,0 +1,22 @@
<script setup lang="ts">
import * as log from "loglevel";
import { onMounted } from "vue";
import { useCourseSessionDetailQuery } from "@/composables";
const props = defineProps<{
courseSlug: string;
}>();
onMounted(async () => {
log.debug("TestCourseSessionComposablePage mounted");
});
// const courseSession = useCurrentCourseSession();
const queryResult = useCourseSessionDetailQuery("-1");
</script>
<template>
<h1>Hello World</h1>
<pre>{{ queryResult.courseSessionDetail }}</pre>
</template>

View File

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

View File

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

View File

@ -7,6 +7,7 @@ import { computed, onMounted } from "vue";
import CompetenceDetail from "@/pages/competence/ActionCompetenceDetail.vue"; import CompetenceDetail from "@/pages/competence/ActionCompetenceDetail.vue";
import LearningPathPathView from "@/pages/learningPath/learningPathPage/LearningPathPathView.vue"; import LearningPathPathView from "@/pages/learningPath/learningPathPage/LearningPathPathView.vue";
import { useCourseSessionDetailQuery } from "@/composables";
const props = defineProps<{ const props = defineProps<{
userId: string; userId: string;
@ -27,8 +28,10 @@ const learningPath = computed(() => {
return learningPathStore.learningPathForUser(props.courseSlug, props.userId); return learningPathStore.learningPathForUser(props.courseSlug, props.userId);
}); });
const { findUser } = useCourseSessionDetailQuery();
const user = computed(() => { const user = computed(() => {
return cockpitStore.courseSessionMembers?.find((csu) => csu.user_id === props.userId); return findUser(props.userId);
}); });
function setActiveClasses(isActive: boolean) { function setActiveClasses(isActive: boolean) {

View File

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

View File

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

View File

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import ItSuccessAlert from "@/components/ui/ItSuccessAlert.vue"; import ItSuccessAlert from "@/components/ui/ItSuccessAlert.vue";
import { useCurrentCourseSession } from "@/composables"; import { useCourseSessionDetailQuery, useCurrentCourseSession } from "@/composables";
import { UPSERT_ASSIGNMENT_COMPLETION_MUTATION } from "@/graphql/mutations"; import { UPSERT_ASSIGNMENT_COMPLETION_MUTATION } from "@/graphql/mutations";
import { import {
maxAssignmentPoints, maxAssignmentPoints,
@ -77,13 +77,13 @@ const userPoints = computed(() =>
userAssignmentPoints(props.assignment, props.assignmentCompletion) userAssignmentPoints(props.assignment, props.assignmentCompletion)
); );
const courseSessionDetailResult = useCourseSessionDetailQuery();
const evaluationUser = computed(() => { const evaluationUser = computed(() => {
if (props.assignmentCompletion.evaluation_user) { if (props.assignmentCompletion.evaluation_user) {
return (courseSession.value.users ?? []).find( return courseSessionDetailResult.findUser(
(user) => user.user_id === props.assignmentCompletion.evaluation_user props.assignmentCompletion.evaluation_user
) as CourseSessionUser; );
} }
return undefined; return undefined;
}); });
</script> </script>

View File

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

View File

@ -2,10 +2,9 @@
import ItCheckbox from "@/components/ui/ItCheckbox.vue"; import ItCheckbox from "@/components/ui/ItCheckbox.vue";
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue"; import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
import ItPersonRow from "@/components/ui/ItPersonRow.vue"; import ItPersonRow from "@/components/ui/ItPersonRow.vue";
import { useCurrentCourseSession } from "@/composables"; import { useCourseSessionDetailQuery, useCurrentCourseSession } from "@/composables";
import type { AttendanceUserStatus } from "@/gql/graphql"; import type { AttendanceUserStatus } from "@/gql/graphql";
import { ATTENDANCE_CHECK_MUTATION } from "@/graphql/mutations"; import { ATTENDANCE_CHECK_MUTATION } from "@/graphql/mutations";
import { useCockpitStore } from "@/stores/cockpit";
import type { DropdownSelectable } from "@/types"; import type { DropdownSelectable } from "@/types";
import { useMutation } from "@urql/vue"; import { useMutation } from "@urql/vue";
import dayjs from "dayjs"; import dayjs from "dayjs";
@ -16,9 +15,9 @@ import { ATTENDANCE_CHECK_QUERY } from "@/graphql/queries";
import { graphqlClient } from "@/graphql/client"; import { graphqlClient } from "@/graphql/client";
const { t } = useTranslation(); const { t } = useTranslation();
const cockpitStore = useCockpitStore();
const courseSession = useCurrentCourseSession(); const courseSession = useCurrentCourseSession();
const attendanceMutation = useMutation(ATTENDANCE_CHECK_MUTATION); const attendanceMutation = useMutation(ATTENDANCE_CHECK_MUTATION);
const courseSessionDetailResult = useCourseSessionDetailQuery();
const attendanceCourses = computed(() => { const attendanceCourses = computed(() => {
return courseSession.value.attendance_courses; return courseSession.value.attendance_courses;
@ -167,8 +166,8 @@ watch(
<div class="mt-4 flex flex-col bg-white p-6"> <div class="mt-4 flex flex-col bg-white p-6">
<div <div
v-for="(csu, index) in cockpitStore.courseSessionMembers" v-for="(csu, index) in courseSessionDetailResult.filterMembers()"
:key="csu.user_id + csu.session_title" :key="csu.user_id"
> >
<ItPersonRow <ItPersonRow
:name="`${csu.first_name} ${csu.last_name}`" :name="`${csu.first_name} ${csu.last_name}`"

View File

@ -3,7 +3,7 @@ import LearningPathDiagram from "@/components/learningPath/LearningPathDiagram.v
import ItPersonRow from "@/components/ui/ItPersonRow.vue"; import ItPersonRow from "@/components/ui/ItPersonRow.vue";
import type { LearningPath } from "@/services/learningPath"; import type { LearningPath } from "@/services/learningPath";
import { useCurrentCourseSession } from "@/composables"; import { useCurrentCourseSession, useCourseSessionDetailQuery } from "@/composables";
import SubmissionsOverview from "@/pages/cockpit/cockpitPage/SubmissionsOverview.vue"; import SubmissionsOverview from "@/pages/cockpit/cockpitPage/SubmissionsOverview.vue";
import { useCockpitStore } from "@/stores/cockpit"; import { useCockpitStore } from "@/stores/cockpit";
import { useCompetenceStore } from "@/stores/competence"; import { useCompetenceStore } from "@/stores/competence";
@ -24,6 +24,7 @@ const competenceStore = useCompetenceStore();
const learningPathStore = useLearningPathStore(); const learningPathStore = useLearningPathStore();
const courseSession = useCurrentCourseSession(); const courseSession = useCurrentCourseSession();
const courseSessionsStore = useCourseSessionsStore(); const courseSessionsStore = useCourseSessionsStore();
const courseSessionDetailResult = useCourseSessionDetailQuery();
function userCountStatusForCircle(userId: string) { function userCountStatusForCircle(userId: string) {
if (!cockpitStore.currentCircle) return { FAIL: 0, SUCCESS: 0, UNKNOWN: 0 }; if (!cockpitStore.currentCircle) return { FAIL: 0, SUCCESS: 0, UNKNOWN: 0 };
@ -126,12 +127,15 @@ function userCountStatusForCircle(userId: string) {
></SubmissionsOverview> ></SubmissionsOverview>
<div class="pt-4"> <div class="pt-4">
<!-- progress --> <!-- progress -->
<div v-if="cockpitStore.courseSessionMembers" class="bg-white p-6"> <div
v-if="courseSessionDetailResult.filterMembers().length > 0"
class="bg-white p-6"
>
<h1 class="heading-3 mb-5">{{ $t("cockpit.progress") }}</h1> <h1 class="heading-3 mb-5">{{ $t("cockpit.progress") }}</h1>
<ul> <ul>
<ItPersonRow <ItPersonRow
v-for="csu in cockpitStore.courseSessionMembers" v-for="csu in courseSessionDetailResult.filterMembers()"
:key="csu.user_id + csu.session_title" :key="csu.user_id"
:name="`${csu.first_name} ${csu.last_name}`" :name="`${csu.first_name} ${csu.last_name}`"
:avatar-url="csu.avatar_url" :avatar-url="csu.avatar_url"
> >

View File

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

View File

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

View File

@ -98,6 +98,11 @@ const router = createRouter({
}, },
], ],
}, },
{
path: "/course/:courseSlug/test-composable",
component: () => import("../pages/TestCourseSessionComposablePage.vue"),
props: true,
},
{ {
path: "/course/:courseSlug/learn", path: "/course/:courseSlug/learn",
component: () => component: () =>

View File

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

View File

@ -5,6 +5,8 @@ import log from "loglevel";
import { useCircleStore } from "@/stores/circle"; import { useCircleStore } from "@/stores/circle";
import { useLearningPathStore } from "@/stores/learningPath"; import { useLearningPathStore } from "@/stores/learningPath";
import { useUserStore } from "@/stores/user"; import { useUserStore } from "@/stores/user";
import type { CourseSessionUser, ExpertSessionUser } from "@/types";
import log from "loglevel";
import { defineStore } from "pinia"; import { defineStore } from "pinia";
export type CockpitStoreState = { export type CockpitStoreState = {
@ -26,13 +28,16 @@ export const useCockpitStore = defineStore({
} as CockpitStoreState; } as CockpitStoreState;
}, },
actions: { actions: {
async loadCircles(courseSlug: string, courseSessionId: number) { async loadCircles(
courseSlug: string,
currentCourseSessionUser: CourseSessionUser | undefined
) {
log.debug("loadCircles called", courseSlug, courseSessionId); log.debug("loadCircles called", courseSlug, courseSessionId);
this.currentCourseSlug = courseSlug; this.currentCourseSlug = courseSlug;
const f = await courseCircles(this.currentCourseSlug, courseSessionId); const circles = await courseCircles(this.currentCourseSlug, currentCourseSessionUser);
this.circles = f.map((c) => { this.circles = circles.map((c) => {
return { return {
id: c.slug, id: c.slug,
name: c.title, name: c.title,
@ -53,18 +58,6 @@ export const useCockpitStore = defineStore({
async setCurrentCourseCircleFromEvent(event: { id: string }) { async setCurrentCourseCircleFromEvent(event: { id: string }) {
await this.setCurrentCourseCircle(event.id); 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: { getters: {
currentCircle: () => { currentCircle: () => {
@ -81,18 +74,15 @@ export const useCockpitStore = defineStore({
}, },
}); });
async function courseCircles(courseSlug: string, courseSessionId: number) { async function courseCircles(
courseSlug: string,
currentCourseSessionUser: CourseSessionUser | undefined
) {
const userStore = useUserStore(); const userStore = useUserStore();
const userId = userStore.id; const userId = userStore.id;
const users = (await itGetCached(`/api/course/sessions/${courseSessionId}/users/`, { if (currentCourseSessionUser && currentCourseSessionUser.role === "EXPERT") {
reload: false, const expert = currentCourseSessionUser as ExpertSessionUser;
})) as CourseSessionUser[];
// First check if current user is an expert for this course session
const currentUser = users.find((user) => user.user_id === userId);
if (currentUser && currentUser.role === "EXPERT") {
const expert = currentUser as ExpertSessionUser;
return expert.circles; return expert.circles;
} }

View File

@ -1,15 +1,6 @@
import { itGetCached, itPost } from "@/fetchHelpers"; import { itGetCached, itPost } from "@/fetchHelpers";
import { deleteCircleDocument } from "@/services/files"; import { deleteCircleDocument } from "@/services/files";
import type { import type { CircleDocument, CourseSession, DueDate } from "@/types";
CircleDocument,
CourseSession,
CourseSessionAssignment,
CourseSessionAttendanceCourse,
CourseSessionEdoniqTest,
CourseSessionUser,
DueDate,
ExpertSessionUser,
} from "@/types";
import eventBus from "@/utils/eventBus"; import eventBus from "@/utils/eventBus";
import { useRouteLookups } from "@/utils/route"; import { useRouteLookups } from "@/utils/route";
import { useLocalStorage } from "@vueuse/core"; import { useLocalStorage } from "@vueuse/core";
@ -18,7 +9,6 @@ import uniqBy from "lodash/uniqBy";
import log from "loglevel"; import log from "loglevel";
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import { computed, ref } from "vue"; import { computed, ref } from "vue";
import { useCircleStore } from "./circle";
import { useUserStore } from "./user"; import { useUserStore } from "./user";
const SELECTED_COURSE_SESSIONS_KEY = "selectedCourseSessionMap"; const SELECTED_COURSE_SESSIONS_KEY = "selectedCourseSessionMap";
@ -39,10 +29,6 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
// TODO: refactor after implementing of Klassenkonzept // TODO: refactor after implementing of Klassenkonzept
await Promise.all( await Promise.all(
allCourseSessions.value.map(async (cs) => { allCourseSessions.value.map(async (cs) => {
const users = (await itGetCached(`/api/course/sessions/${cs.id}/users/`, {
reload: reload,
})) as CourseSessionUser[];
cs.users = users;
sortDueDates(cs.due_dates); sortDueDates(cs.due_dates);
}) })
); );
@ -161,56 +147,56 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
return Boolean(isCourseExpert && (inLearningPath() || inCompetenceProfile())); return Boolean(isCourseExpert && (inLearningPath() || inCompetenceProfile()));
}); });
const circleExperts = computed(() => { // const circleExperts = computed(() => {
const circleStore = useCircleStore(); // const circleStore = useCircleStore();
const circleTranslationKey = circleStore.circle?.translation_key; // 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 [];
// });
if (currentCourseSession.value && circleTranslationKey) { // const canUploadCircleDocuments = computed(() => {
return currentCourseSession.value.users.filter((u) => { // const userStore = useUserStore();
if (u.role === "EXPERT") { // return (
return (u as ExpertSessionUser).circles // circleExperts.value.filter((expert) => expert.user_id === userStore.id).length > 0
.map((c) => c.translation_key) // );
.includes(circleTranslationKey); // });
}
return false;
}) as ExpertSessionUser[];
}
return [];
});
const canUploadCircleDocuments = computed(() => { // 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(courseSession: CourseSession) {
const userStore = useUserStore(); const userStore = useUserStore();
return ( return (
circleExperts.value.filter((expert) => expert.user_id === userStore.id).length > 0 userStore.course_session_experts.includes(courseSession.id) ||
);
});
const circleDocuments = computed(() => {
const circleStore = useCircleStore();
return (
circleStore.circle?.learningSequences
.map((ls) => ({ id: ls.id, title: ls.title, documents: [] }))
.map((ls: { id: number; title: string; documents: CircleDocument[] }) => {
if (currentCourseSession.value === undefined) {
return ls;
}
for (const document of currentCourseSession.value.documents) {
if (document.learning_sequence === ls.id) {
ls.documents.push(document);
}
}
return ls;
})
.filter((ls) => ls.documents.length > 0) || []
);
});
function hasCockpit(couseSession: CourseSession) {
const userStore = useUserStore();
return (
userStore.course_session_experts.includes(couseSession.id) ||
userStore.is_superuser userStore.is_superuser
); );
} }
@ -263,35 +249,35 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
); );
} }
function findAttendanceCourse( // function findAttendanceCourse(
contentId: number // contentId: number
): CourseSessionAttendanceCourse | undefined { // ): CourseSessionAttendanceCourse | undefined {
if (currentCourseSession.value) { // if (currentCourseSession.value) {
return currentCourseSession.value.attendance_courses.find( // return currentCourseSession.value.attendance_courses.find(
(attendanceCourse) => attendanceCourse.learning_content_id === contentId // (attendanceCourse) => attendanceCourse.learning_content_id === contentId
); // );
} // }
} // }
function findCourseSessionAssignment( // function findCourseSessionAssignment(
contentId?: number // contentId?: number
): CourseSessionAssignment | undefined { // ): CourseSessionAssignment | undefined {
if (contentId && currentCourseSession.value) { // if (contentId && currentCourseSession.value) {
return currentCourseSession.value.assignments.find( // return currentCourseSession.value.assignments.find(
(a) => a.learning_content_id === contentId // (a) => a.learning_content_id === contentId
); // );
} // }
} // }
//
function findCourseSessionEdoniqTest( // function findCourseSessionEdoniqTest(
contentId?: number // contentId?: number
): CourseSessionEdoniqTest | undefined { // ): CourseSessionEdoniqTest | undefined {
if (contentId && currentCourseSession.value) { // if (contentId && currentCourseSession.value) {
return currentCourseSession.value.edoniq_tests.find( // return currentCourseSession.value.edoniq_tests.find(
(a) => a.learning_content_id === contentId // (a) => a.learning_content_id === contentId
); // );
} // }
} // }
return { return {
uniqueCourseSessionsByCourse, uniqueCourseSessionsByCourse,
@ -302,15 +288,9 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
hasCockpit, hasCockpit,
hasCourseSessionPreview, hasCourseSessionPreview,
currentCourseSessionHasCockpit, currentCourseSessionHasCockpit,
canUploadCircleDocuments,
circleDocuments,
circleExperts,
addDocument, addDocument,
startUpload, startUpload,
removeDocument, removeDocument,
findAttendanceCourse,
findCourseSessionAssignment,
findCourseSessionEdoniqTest,
allDueDates, allDueDates,
// use `useCurrentCourseSession` whenever possible // use `useCurrentCourseSession` whenever possible

View File

@ -479,13 +479,23 @@ export interface CourseSessionAttendanceCourse {
} }
export interface CourseSessionAssignment { export interface CourseSessionAssignment {
id: number; id: string;
course_session_id: number; submission_deadline: {
learning_content_id: number; id: string;
submission_deadline_id: number; start: string;
submission_deadline_start: string; };
evaluation_deadline_id: number; evaluation_deadline: {
evaluation_deadline_start: string; id: string;
start: string;
};
learning_content: {
id: string;
content_assignment: {
id: string;
title: string;
assignment_type: AssignmentType;
};
};
} }
export interface CourseSessionEdoniqTest { export interface CourseSessionEdoniqTest {
@ -504,29 +514,34 @@ export interface CourseSession {
title: string; title: string;
start_date: string; start_date: string;
end_date: string; end_date: string;
learning_path_url: string; // learning_path_url: string;
cockpit_url: string; // cockpit_url: string;
competence_url: string; // competence_url: string;
course_url: string; course_url: string;
media_library_url: string; // media_library_url: string;
attendance_courses: CourseSessionAttendanceCourse[]; // attendance_courses: CourseSessionAttendanceCourse[];
assignments: CourseSessionAssignment[]; // assignments: CourseSessionAssignment[];
edoniq_tests: CourseSessionEdoniqTest[]; // edoniq_tests: CourseSessionEdoniqTest[];
documents: CircleDocument[]; // documents: CircleDocument[];
users: CourseSessionUser[]; // users: CourseSessionUser[];
due_dates: DueDate[]; due_dates: DueDate[];
} }
export type Role = "MEMBER" | "EXPERT" | "TUTOR"; export type Role = "MEMBER" | "EXPERT" | "TUTOR";
export interface CourseSessionUser { export interface CourseSessionUser {
session_title: string;
user_id: string; user_id: string;
first_name: string; first_name: string;
last_name: string; last_name: string;
email: string; email: string;
avatar_url: string; avatar_url: string;
role: Role; role: Role;
circles: {
id: number;
title: string;
slug: string;
translation_key: string;
}[];
} }
export interface ExpertSessionUser extends CourseSessionUser { export interface ExpertSessionUser extends CourseSessionUser {
@ -539,6 +554,17 @@ export interface ExpertSessionUser extends CourseSessionUser {
}[]; }[];
} }
export interface CourseSessionDetail {
id: string;
title: string;
course: {
id: string;
title: string;
};
assignments: CourseSessionAssignment[];
users: CourseSessionUser[];
}
// document upload // document upload
export interface DocumentUploadData { export interface DocumentUploadData {
file: File | null; file: File | null;

View File

@ -5,12 +5,30 @@ export function assertUnreachable(msg: string): never {
throw new Error("Didn't expect to get here, " + msg); throw new Error("Didn't expect to get here, " + msg);
} }
export function getCompetenceBaseUrl(courseSession: CourseSession): string { function createCourseUrl(
return courseSession.competence_url.replace( courseSession: CourseSession | undefined,
// TODO: remove the `competence_url` with url to Navi... specificSub: string
"/competences", ): string {
"" if (!courseSession) {
); return "";
}
if (["learn", "media", "competence"].includes(specificSub)) {
return `${courseSession.course_url}/${specificSub}`;
}
return courseSession.course_url;
}
export function getCompetenceNaviUrl(courseSession: CourseSession | undefined): string {
return createCourseUrl(courseSession, "competence");
}
export function getMediaCenterUrl(courseSession: CourseSession | undefined): string {
return createCourseUrl(courseSession, "media");
}
export function getLearningPathUrl(courseSession: CourseSession | undefined): string {
return createCourseUrl(courseSession, "learn");
} }
export function getAssignmentTypeTitle(assignmentType: AssignmentType): string { export function getAssignmentTypeTitle(assignmentType: AssignmentType): string {

View File

@ -110,9 +110,9 @@ urlpatterns = [
# course # course
path(r"api/course/sessions/", get_course_sessions, name="get_course_sessions"), path(r"api/course/sessions/", get_course_sessions, name="get_course_sessions"),
path(r"api/course/sessions/<signed_int:course_session_id>/users/", # path(r"api/course/sessions/<signed_int:course_session_id>/users/",
get_course_session_users, # get_course_session_users,
name="get_course_session_users"), # name="get_course_session_users"),
path(r"api/course/page/<slug_or_id>/", course_page_api_view, path(r"api/course/page/<slug_or_id>/", course_page_api_view,
name="course_page_api_view"), name="course_page_api_view"),
path(r"api/course/completion/mark/", mark_course_completion_view, path(r"api/course/completion/mark/", mark_course_completion_view,

View File

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

View File

@ -2,12 +2,24 @@ from typing import Type
import graphene import graphene
import structlog import structlog
from graphene import ObjectType
from graphene_django import DjangoObjectType from graphene_django import DjangoObjectType
from graphql import GraphQLError from graphql import GraphQLError
from rest_framework.exceptions import PermissionDenied from rest_framework.exceptions import PermissionDenied
from vbv_lernwelt.course.models import Course, CourseBasePage, CoursePage from vbv_lernwelt.course.models import Course, CourseBasePage, CoursePage, \
CourseSession, CourseSessionUser
from vbv_lernwelt.course.permissions import has_course_access from vbv_lernwelt.course.permissions import has_course_access
from vbv_lernwelt.course_session.graphql.types import (
CourseSessionAttendanceCourseObjectType,
CourseSessionAssignmentObjectType,
CourseSessionEdoniqTestObjectType,
)
from vbv_lernwelt.course_session.models import (
CourseSessionAttendanceCourse,
CourseSessionAssignment,
CourseSessionEdoniqTest,
)
from vbv_lernwelt.learnpath.graphql.types import LearningPathObjectType from vbv_lernwelt.learnpath.graphql.types import LearningPathObjectType
logger = structlog.get_logger(__name__) logger = structlog.get_logger(__name__)
@ -74,3 +86,89 @@ class CourseObjectType(DjangoObjectType):
def resolve_learning_path(self, info): def resolve_learning_path(self, info):
return self.get_learning_path() return self.get_learning_path()
class CourseSessionUserExpertCircleType(ObjectType):
id = graphene.ID()
title = graphene.String()
slug = graphene.String()
class CourseSessionUserObjectsType(DjangoObjectType):
user_id = graphene.UUID()
first_name = graphene.String()
last_name = graphene.String()
email = graphene.String()
avatar_url = graphene.String()
role = graphene.String()
circles = graphene.List(CourseSessionUserExpertCircleType)
class Meta:
model = CourseSessionUser
fields = (
"id",
"user_id",
"first_name",
"last_name",
"email",
"avatar_url",
"role",
)
def resolve_user_id(self, info):
return self.user.id
def resolve_first_name(self, info):
return self.user.first_name
def resolve_last_name(self, info):
return self.user.last_name
def resolve_email(self, info):
return self.user.email
def resolve_avatar_url(self, info):
return self.user.avatar_url
def resolve_role(self, info):
return self.role
def resolve_circles(self, info):
return self.expert.all().values(
"id", "title", "slug", "translation_key"
)
class CourseSessionObjectType(DjangoObjectType):
attendance_courses = graphene.List(CourseSessionAttendanceCourseObjectType)
assignments = graphene.List(CourseSessionAssignmentObjectType)
edoniq_tests = graphene.List(CourseSessionEdoniqTestObjectType)
users = graphene.List(CourseSessionUserObjectsType)
class Meta:
model = CourseSession
fields = (
"id",
"created_at",
"updated_at",
"course",
"title",
"start_date",
"end_date",
"attendance_courses",
"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_test(self, info):
return CourseSessionEdoniqTest.objects.filter(course_session=self)
def resolve_users(self, info):
return CourseSessionUser.objects.filter(
course_session_id=self.id
).distinct()

View File

@ -56,14 +56,15 @@ class CourseCompletionSerializer(serializers.ModelSerializer):
class CourseSessionSerializer(serializers.ModelSerializer): class CourseSessionSerializer(serializers.ModelSerializer):
course = serializers.SerializerMethodField() course = serializers.SerializerMethodField()
course_url = serializers.SerializerMethodField() course_url = serializers.SerializerMethodField()
learning_path_url = serializers.SerializerMethodField() # learning_path_url = serializers.SerializerMethodField()
cockpit_url = serializers.SerializerMethodField() # cockpit_url = serializers.SerializerMethodField()
competence_url = serializers.SerializerMethodField() # competence_url = serializers.SerializerMethodField()
media_library_url = serializers.SerializerMethodField() # media_library_url = serializers.SerializerMethodField()
documents = serializers.SerializerMethodField() # documents = serializers.SerializerMethodField()
attendance_courses = serializers.SerializerMethodField() # attendance_courses = serializers.SerializerMethodField()
assignments = serializers.SerializerMethodField() # assignments = serializers.SerializerMethodField()
edoniq_tests = serializers.SerializerMethodField() # edoniq_tests = serializers.SerializerMethodField()
due_dates = serializers.SerializerMethodField() due_dates = serializers.SerializerMethodField()
def get_course(self, obj): def get_course(self, obj):
@ -119,16 +120,16 @@ class CourseSessionSerializer(serializers.ModelSerializer):
"title", "title",
"start_date", "start_date",
"end_date", "end_date",
"additional_json_data", # "additional_json_data",
"attendance_courses", # "attendance_courses",
"assignments", # "assignments",
"edoniq_tests", # "edoniq_tests",
"learning_path_url", # "learning_path_url",
"cockpit_url", # "cockpit_url",
"competence_url", # "competence_url",
"media_library_url", # "media_library_url",
"course_url", "course_url",
"documents", # "documents",
"due_dates", "due_dates",
] ]

View File

@ -6,7 +6,6 @@ from rest_framework.response import Response
from wagtail.models import Page from wagtail.models import Page
from vbv_lernwelt.core.utils import get_django_content_type from vbv_lernwelt.core.utils import get_django_content_type
from vbv_lernwelt.course.models import ( from vbv_lernwelt.course.models import (
CircleDocument, CircleDocument,
CourseCompletion, CourseCompletion,
@ -135,7 +134,9 @@ def mark_course_completion_view(request):
@api_view(["GET"]) @api_view(["GET"])
def get_course_sessions(request): def get_course_sessions(request):
try: try:
course_sessions = course_sessions_for_user_qs(request.user) course_sessions = course_sessions_for_user_qs(request.user).prefetch_related(
"course"
)
return Response( return Response(
status=200, data=CourseSessionSerializer(course_sessions, many=True).data status=200, data=CourseSessionSerializer(course_sessions, many=True).data
) )
@ -149,7 +150,9 @@ def get_course_sessions(request):
@api_view(["GET"]) @api_view(["GET"])
def get_course_session_users(request, course_session_id): def get_course_session_users(request, course_session_id):
try: try:
qs = CourseSessionUser.objects.filter(course_session_id=course_session_id) qs = CourseSessionUser.objects.filter(
course_session_id=course_session_id
).distinct()
user_data = [csu.to_dict() for csu in qs] user_data = [csu.to_dict() for csu in qs]
return Response(status=200, data=user_data) return Response(status=200, data=user_data)

View File

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

View File

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

View File

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

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

@ -4,7 +4,7 @@ from vbv_lernwelt.duedate.models import DueDate
class DueDateSerializer(serializers.ModelSerializer): class DueDateSerializer(serializers.ModelSerializer):
circle = serializers.SerializerMethodField() # circle = serializers.SerializerMethodField()
class Meta: class Meta:
model = DueDate model = DueDate
@ -20,7 +20,7 @@ class DueDateSerializer(serializers.ModelSerializer):
"url_expert", "url_expert",
"course_session", "course_session",
"page", "page",
"circle", # "circle",
] ]
def get_circle(self, obj): def get_circle(self, obj):

View File

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

View File

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