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

View File

@ -4,7 +4,11 @@ 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 {
getCompetenceNaviUrl,
getLearningPathUrl,
getMediaCenterUrl,
} from "@/utils/utils";
const router = useRouter();
@ -68,7 +72,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))"
>
{{ $t("a.VorschauTeilnehmer") }}
</button>
@ -78,7 +82,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))"
>
{{ $t("general.learningPath") }}
</button>
@ -86,7 +90,7 @@ const courseSessionsStore = useCourseSessionsStore();
<li class="mb-6">
<button
data-cy="navigation-mobile-competence-profile-link"
@click="clickLink(getCompetenceBaseUrl(courseSession))"
@click="clickLink(getCompetenceNaviUrl(courseSession))"
>
{{ $t("competences.title") }}
</button>
@ -95,7 +99,7 @@ const courseSessionsStore = useCourseSessionsStore();
<li class="mb-6">
<button
data-cy="medialibrary-link"
@click="clickLink(`${courseSession?.media_library_url}`)"
@click="clickLink(getMediaCenterUrl(courseSession))"
>
{{ $t("a.Mediathek") }}
</button>

View File

@ -1,5 +1,9 @@
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";
@ -28,3 +32,52 @@ export function useCurrentCourseSession() {
);
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_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,155 @@ 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]
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."""
enum AssignmentAssignmentCompletionCompletionStatusChoices {
"""IN_PROGRESS"""
@ -361,21 +505,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 +565,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 +595,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 +664,7 @@ type ErrorType {
}
type AttendanceCourseUserMutation {
course_session_attendance_course: CourseSessionAttendanceCourseType
course_session_attendance_course: CourseSessionAttendanceCourseObjectType
}
input AttendanceUserInputType {

View File

@ -6,8 +6,8 @@ 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 CircleObjectType = "CircleObjectType";
export const CompetenceCertificateListObjectType = "CompetenceCertificateListObjectType";
@ -15,8 +15,15 @@ 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 +35,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,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 log from "loglevel";
import { computed, onMounted } from "vue";
import { 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 `${courseSession.course_url}/cockpit`;
}
return courseSession.learning_path_url;
return getLearningPathUrl(courseSession);
});
};
</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">
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,14 @@ 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);
const members = courseSessionDetailResult.filterMembers();
members.forEach((csu) => {
competenceStore.loadCompetenceProfilePage(
props.courseSlug + "-competencenavi-competences",
@ -35,7 +36,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

@ -7,6 +7,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;
@ -27,8 +28,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 { useCurrentCourseSession, useCourseSessionDetailQuery } 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();
@ -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.findAssignmentDetail(props.assignment.id.toString());
});
const dueDate = computed(() =>
dayjs(assignmentDetail.value?.evaluation_deadline_start)
dayjs(assignmentDetail.value?.evaluation_deadline.start)
);
const inEvaluationTask = computed(

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

@ -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,12 @@ const state = reactive({
assignmentSubmittedUsers: [] as CourseSessionUser[],
});
const assignmentDetail = computed(() => {
return courseSessionDetailResult.findAssignmentDetail(
props.learningContentAssignment.content_assignment_id.toString()
);
});
onMounted(async () => {
const { gradedUsers, assignmentSubmittedUsers } =
await loadAssignmentCompletionStatusData(
@ -45,10 +48,6 @@ onMounted(async () => {
state.gradedUsers = gradedUsers;
state.assignmentSubmittedUsers = assignmentSubmittedUsers;
});
const assignmentDetail = computed(() =>
findAssignmentDetail(props.learningContentAssignment.content_assignment_id)
);
</script>
<template>
@ -62,14 +61,12 @@ const assignmentDetail = computed(() =>
<div v-if="assignmentDetail">
<span>
{{ $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 +82,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

@ -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, useCurrentCourseSession } 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,9 +15,9 @@ 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;
@ -167,8 +166,8 @@ watch(
<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"
v-for="(csu, index) in courseSessionDetailResult.filterMembers()"
:key="csu.user_id"
>
<ItPersonRow
: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 type { LearningPath } from "@/services/learningPath";
import { useCurrentCourseSession } from "@/composables";
import { useCurrentCourseSession, useCourseSessionDetailQuery } from "@/composables";
import SubmissionsOverview from "@/pages/cockpit/cockpitPage/SubmissionsOverview.vue";
import { useCockpitStore } from "@/stores/cockpit";
import { useCompetenceStore } from "@/stores/competence";
@ -24,6 +24,7 @@ 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 };
@ -126,12 +127,15 @@ function userCountStatusForCircle(userId: string) {
></SubmissionsOverview>
<div class="pt-4">
<!-- 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>
<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"
>

View File

@ -4,7 +4,7 @@ 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;
@ -13,12 +13,11 @@ const props = defineProps<{
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,6 +13,7 @@ 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;
@ -33,8 +33,9 @@ const props = defineProps<{
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

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

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,
@ -35,19 +32,17 @@ export async function loadAssignmentCompletionStatusData(
courseSessionId: number,
learningContentId: number
) {
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

@ -5,6 +5,8 @@ import log from "loglevel";
import { useCircleStore } from "@/stores/circle";
import { useLearningPathStore } from "@/stores/learningPath";
import { useUserStore } from "@/stores/user";
import type { CourseSessionUser, ExpertSessionUser } from "@/types";
import log from "loglevel";
import { defineStore } from "pinia";
export type CockpitStoreState = {
@ -26,13 +28,16 @@ export const useCockpitStore = defineStore({
} as CockpitStoreState;
},
actions: {
async loadCircles(courseSlug: string, courseSessionId: number) {
async loadCircles(
courseSlug: string,
currentCourseSessionUser: CourseSessionUser | undefined
) {
log.debug("loadCircles called", courseSlug, courseSessionId);
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,
name: c.title,
@ -53,18 +58,6 @@ export const useCockpitStore = defineStore({
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: () => {
@ -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 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

@ -1,15 +1,6 @@
import { itGetCached, itPost } from "@/fetchHelpers";
import { deleteCircleDocument } from "@/services/files";
import type {
CircleDocument,
CourseSession,
CourseSessionAssignment,
CourseSessionAttendanceCourse,
CourseSessionEdoniqTest,
CourseSessionUser,
DueDate,
ExpertSessionUser,
} from "@/types";
import type { CircleDocument, CourseSession, DueDate } from "@/types";
import eventBus from "@/utils/eventBus";
import { useRouteLookups } from "@/utils/route";
import { useLocalStorage } from "@vueuse/core";
@ -18,7 +9,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 +29,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);
})
);
@ -161,56 +147,56 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
return Boolean(isCourseExpert && (inLearningPath() || inCompetenceProfile()));
});
const circleExperts = computed(() => {
const circleStore = useCircleStore();
const circleTranslationKey = circleStore.circle?.translation_key;
// 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 [];
// });
if (currentCourseSession.value && circleTranslationKey) {
return currentCourseSession.value.users.filter((u) => {
if (u.role === "EXPERT") {
return (u as ExpertSessionUser).circles
.map((c) => c.translation_key)
.includes(circleTranslationKey);
}
return false;
}) as ExpertSessionUser[];
}
return [];
});
// const canUploadCircleDocuments = computed(() => {
// const userStore = useUserStore();
// return (
// circleExperts.value.filter((expert) => expert.user_id === userStore.id).length > 0
// );
// });
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();
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
);
}
@ -263,35 +249,35 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
);
}
function findAttendanceCourse(
contentId: number
): CourseSessionAttendanceCourse | undefined {
if (currentCourseSession.value) {
return currentCourseSession.value.attendance_courses.find(
(attendanceCourse) => attendanceCourse.learning_content_id === contentId
);
}
}
// 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
);
}
}
// 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,
@ -302,15 +288,9 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
hasCockpit,
hasCourseSessionPreview,
currentCourseSessionHasCockpit,
canUploadCircleDocuments,
circleDocuments,
circleExperts,
addDocument,
startUpload,
removeDocument,
findAttendanceCourse,
findCourseSessionAssignment,
findCourseSessionEdoniqTest,
allDueDates,
// use `useCurrentCourseSession` whenever possible

View File

@ -479,13 +479,23 @@ export interface CourseSessionAttendanceCourse {
}
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: {
id: string;
start: string;
};
evaluation_deadline: {
id: string;
start: string;
};
learning_content: {
id: string;
content_assignment: {
id: string;
title: string;
assignment_type: AssignmentType;
};
};
}
export interface CourseSessionEdoniqTest {
@ -504,29 +514,34 @@ export interface CourseSession {
title: string;
start_date: string;
end_date: string;
learning_path_url: string;
cockpit_url: string;
competence_url: 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[];
// 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: number;
title: string;
slug: string;
translation_key: string;
}[];
}
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
export interface DocumentUploadData {
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);
}
export function getCompetenceBaseUrl(courseSession: CourseSession): string {
return courseSession.competence_url.replace(
// TODO: remove the `competence_url` with url to Navi...
"/competences",
""
);
function createCourseUrl(
courseSession: CourseSession | undefined,
specificSub: string
): 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 {

View File

@ -110,9 +110,9 @@ urlpatterns = [
# 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,

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,24 @@ 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 Course, CourseBasePage, CoursePage, \
CourseSession, CourseSessionUser
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
logger = structlog.get_logger(__name__)
@ -74,3 +86,89 @@ 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 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):
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()
# learning_path_url = serializers.SerializerMethodField()
# cockpit_url = serializers.SerializerMethodField()
# competence_url = serializers.SerializerMethodField()
# media_library_url = serializers.SerializerMethodField()
# documents = serializers.SerializerMethodField()
# attendance_courses = serializers.SerializerMethodField()
# assignments = serializers.SerializerMethodField()
# edoniq_tests = serializers.SerializerMethodField()
due_dates = serializers.SerializerMethodField()
def get_course(self, obj):
@ -119,16 +120,16 @@ 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",
# "additional_json_data",
# "attendance_courses",
# "assignments",
# "edoniq_tests",
# "learning_path_url",
# "cockpit_url",
# "competence_url",
# "media_library_url",
"course_url",
"documents",
# "documents",
"due_dates",
]

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
)
@ -149,7 +150,9 @@ def get_course_sessions(request):
@api_view(["GET"])
def get_course_session_users(request, course_session_id):
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]
return Response(status=200, data=user_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,21 @@
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 (
CourseSessionAttendanceCourse,
CourseSessionAssignment,
)
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,15 +25,14 @@ 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
@ -31,19 +40,49 @@ class CourseSessionAttendanceCourseType(DjangoObjectType):
"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_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):
circle = serializers.SerializerMethodField()
# circle = serializers.SerializerMethodField()
class Meta:
model = DueDate
@ -20,7 +20,7 @@ class DueDateSerializer(serializers.ModelSerializer):
"url_expert",
"course_session",
"page",
"circle",
# "circle",
]
def get_circle(self, obj):

View File

@ -11,7 +11,7 @@ from vbv_lernwelt.learnpath.graphql.types import (
LearningContentMediaLibraryObjectType,
LearningContentPlaceholderObjectType,
LearningContentRichTextObjectType,
LearningContentTestObjectType,
LearningContentEdoniqTestObjectType,
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,)