Replace dueDate code

This commit is contained in:
Daniel Egger 2024-04-19 15:57:47 +02:00
parent 41b0d3ae4d
commit 8cb00b0976
19 changed files with 98 additions and 442 deletions

View File

@ -1,50 +0,0 @@
<script setup lang="ts">
import { useCurrentCourseSession, useDashboardPersons } from "@/composables";
import DueDateSingle from "@/components/dueDates/DueDateSingle.vue";
import { computed } from "vue";
import { useExpertCockpitStore } from "@/stores/expertCockpit";
import _ from "lodash";
import LoadingSpinner from "@/components/ui/LoadingSpinner.vue";
const expertCockpitStore = useExpertCockpitStore();
const courseSession = useCurrentCourseSession();
const courseSessionId = courseSession.value.id;
const { loading: loadingDates, currentDueDates } = useDashboardPersons();
const circleDates = computed(() => {
const courseSessionId = courseSession.value.id;
const circleId = expertCockpitStore.currentCircle?.id ?? "0";
const dueDates = currentDueDates.value.filter((dueDate) => {
return (
dueDate.course_session_id === courseSessionId && dueDate.circle?.id === circleId
);
});
return _.take(dueDates, 3);
});
</script>
<template>
<div v-if="loadingDates" class="m-8 flex justify-center">
<LoadingSpinner />
</div>
<div v-else class="flex flex-col space-y-2">
<h3 class="heading-3">{{ $t("Nächste Termine") }}</h3>
<div
v-for="dueDate in circleDates"
:key="dueDate.id"
class="border-t border-gray-500 pt-2"
>
<DueDateSingle :due-date="dueDate"></DueDateSingle>
</div>
<div v-if="circleDates.length === 0">{{ $t("dueDates.noDueDatesAvailable") }}</div>
</div>
<router-link
class="btn-secondary mt-4"
:to="`/dashboard/due-dates?session=${courseSessionId}`"
>
{{ $t("a.Alle Termine anzeigen") }}
</router-link>
</template>

View File

@ -0,0 +1,61 @@
<script lang="ts" setup>
import { useDashboardPersons } from "@/composables";
import { computed } from "vue";
import _ from "lodash";
import LoadingSpinner from "@/components/ui/LoadingSpinner.vue";
import DueDateSingle from "@/components/dueDates/DueDateSingle.vue";
const props = withDefaults(
defineProps<{
courseSessionId: string;
circleId?: string;
maxCount?: number;
showAllButton?: boolean;
}>(),
{
maxCount: 3,
circleId: undefined,
showAllButton: false,
}
);
const { loading, currentDueDates } = useDashboardPersons();
const filteredDueDates = computed(() => {
let dueDates = currentDueDates.value.filter(
(dueDate) => dueDate.course_session_id === props.courseSessionId
);
if (props.circleId) {
dueDates = dueDates.filter((dueDate) => dueDate.circle?.id === props.circleId);
}
return _.take(dueDates, props.maxCount);
});
</script>
<template>
<div v-if="loading" class="m-8 flex justify-center">
<LoadingSpinner />
</div>
<div v-else class="flex flex-col space-y-2">
<h3 class="heading-3">{{ $t("Nächste Termine") }}</h3>
<div
v-for="dueDate in filteredDueDates"
:key="dueDate.id"
class="border-t border-gray-500 pt-2"
>
<DueDateSingle :due-date="dueDate"></DueDateSingle>
</div>
<div v-if="filteredDueDates.length === 0">
{{ $t("dueDates.noDueDatesAvailable") }}
</div>
</div>
<router-link
v-if="showAllButton"
class="btn-secondary mt-4"
:to="`/dashboard/due-dates?session=${courseSessionId}`"
>
{{ $t("a.Alle Termine anzeigen") }}
</router-link>
</template>

View File

@ -1,54 +0,0 @@
<script lang="ts" setup>
import DueDateSingle from "@/components/dueDates/DueDateSingle.vue";
import { computed } from "vue";
import type { DashboardDueDate } from "@/services/dashboard";
const props = defineProps<{
maxCount: number;
dueDates: DashboardDueDate[];
showTopBorder: boolean;
showBottomBorder: boolean;
showAllDueDatesLink: boolean;
showCourseSession: boolean;
}>();
const allDueDates = computed(() => {
return props.dueDates;
});
const dueDatesDisplayed = computed(() => {
return props.dueDates.slice(0, props.maxCount);
});
</script>
<template>
<div>
<ul :class="showBottomBorder ? '' : 'no-border-last'">
<li
v-for="dueDate in dueDatesDisplayed"
:key="dueDate.id"
class="cy-single-due-date"
:class="{ 'first:border-t': props.showTopBorder, 'border-b': true }"
>
<DueDateSingle
:due-date="dueDate"
:show-course-session="props.showCourseSession"
></DueDateSingle>
</li>
</ul>
<div v-if="allDueDates.length === 0">{{ $t("dueDates.noDueDatesAvailable") }}</div>
<div
v-if="showAllDueDatesLink && allDueDates.length > 0"
class="flex items-center pt-6"
>
<a href="/appointments">{{ $t("dueDates.showAllDueDates") }}</a>
<it-icon-arrow-right />
</div>
</div>
</template>
<style lang="postcss" scoped>
.no-border-last li:last-child {
border-bottom: none !important;
}
</style>

View File

@ -1,25 +0,0 @@
<template>
<div>
<DueDatesList
:due-dates="allDueDates"
:max-count="props.maxCount"
:show-top-border="props.showTopBorder"
show-all-due-dates-link
show-bottom-border
:show-course-session="false"
></DueDatesList>
</div>
</template>
<script lang="ts" setup>
import DueDatesList from "@/components/dueDates/DueDatesList.vue";
import { useCurrentCourseSession } from "@/composables";
const props = defineProps<{
maxCount: number;
showTopBorder: boolean;
}>();
const courseSession = useCurrentCourseSession();
const allDueDates = courseSession.value.due_dates;
</script>

View File

@ -60,9 +60,9 @@ const selectedCourseSessionTitle = computed(() => {
const appointmentsUrl = computed(() => { const appointmentsUrl = computed(() => {
const currentCourseSession = courseSessionsStore.currentCourseSession; const currentCourseSession = courseSessionsStore.currentCourseSession;
if (currentCourseSession) { if (currentCourseSession) {
return `/course/dashboard/due-dates`; return `/dashboard/due-dates?session=${currentCourseSession.id}`;
} else { } else {
return `/appointments`; return `/dashboard/due-dates`;
} }
}); });

View File

@ -1,179 +0,0 @@
<script setup lang="ts">
import { computed, onMounted, ref, watch } from "vue";
import { useCourseSessionsStore } from "@/stores/courseSessions";
import { useTranslation } from "i18next-vue";
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
import type { DueDate } from "@/types";
import DueDatesList from "@/components/dueDates/DueDatesList.vue";
import { useCourseData } from "@/composables";
const { t } = useTranslation();
const UNFILTERED = Number.MAX_SAFE_INTEGER.toString();
const courseSessionsStore = useCourseSessionsStore();
type Item = {
id: string;
name: string;
};
type CourseItem = Item & {
slug: string;
};
const courses: CourseItem[] = courseSessionsStore.uniqueCourseSessionsByCourse.map(
(cs) => ({
id: cs.course.id,
name: cs.course.title,
slug: cs.course.slug,
})
);
const selectedCourse = ref<CourseItem>(courses[0]);
const courseSessions = computed(() => {
return [
{
id: UNFILTERED,
name: t("a.AlleDurchführungen"),
},
...courseSessionsStore.allCourseSessions
.filter((cs) => cs.course.id === selectedCourse.value.id)
.map((cs) => ({ id: cs.id, name: cs.title })),
];
});
const selectedSession = ref<Item>(courseSessions.value[0]);
// pre-select course and session if we are in a course session
if (courseSessionsStore.currentCourseSession) {
const session = courseSessionsStore.currentCourseSession;
const { id: courseId, title: courseName, slug: courseSlug } = session.course;
selectedCourse.value = { id: courseId, name: courseName, slug: courseSlug };
const { id: sessionId, title: sessionName } = session;
selectedSession.value = { id: sessionId, name: sessionName };
}
const initialItemCircle: Item = {
id: UNFILTERED,
name: t("a.AlleCircle"),
};
const circles = ref<Item[]>([initialItemCircle]);
const selectedCircle = ref<Item>(circles.value[0]);
async function loadCircleValues() {
if (selectedCourse.value) {
const learningPathQuery = useCourseData(selectedCourse.value.slug);
await learningPathQuery.resultPromise;
circles.value = [
initialItemCircle,
...(learningPathQuery.circles.value ?? []).map((circle) => ({
id: circle.id,
name: circle.title,
})),
];
} else {
circles.value = [initialItemCircle];
}
selectedCircle.value = circles.value[0];
}
watch(selectedCourse, async () => {
selectedSession.value = courseSessions.value[0];
await loadCircleValues();
});
onMounted(async () => {
await loadCircleValues();
});
const appointments = computed(() => {
return courseSessionsStore
.allDueDates()
.filter(
(dueDate) =>
isMatchingCourse(dueDate) &&
isMatchingSession(dueDate) &&
isMatchingCircle(dueDate)
);
});
const isMatchingSession = (dueDate: DueDate) =>
selectedSession.value.id === UNFILTERED ||
dueDate.course_session_id === selectedSession.value.id;
const isMatchingCircle = (dueDate: DueDate) =>
selectedCircle.value.id === UNFILTERED ||
dueDate.circle?.id === selectedCircle.value.id;
const isMatchingCourse = (dueDate: DueDate) =>
courseSessions.value.map((cs) => cs.id).includes(dueDate.course_session_id);
const numAppointmentsToShow = ref(7);
const canLoadMore = computed(() => {
return numAppointmentsToShow.value < appointments.value.length;
});
async function loadAdditionalAppointments() {
numAppointmentsToShow.value *= 2;
}
</script>
<template>
<div class="bg-gray-200">
<div class="container-large px-8 py-8">
<header class="mb-6 flex flex-col lg:flex-row lg:items-center lg:justify-between">
<h1>{{ $t("a.AlleTermine") }}</h1>
<div>
<ItDropdownSelect
v-model="selectedCourse"
data-cy="appointments-course-select"
:items="courses"
></ItDropdownSelect>
</div>
</header>
<main>
<div class="flex flex-col space-y-2">
<div class="flex flex-col space-x-0 bg-white lg:flex-row lg:space-x-3">
<ItDropdownSelect
v-model="selectedSession"
data-cy="appointments-session-select"
:items="courseSessions"
borderless
></ItDropdownSelect>
<ItDropdownSelect
v-model="selectedCircle"
data-cy="appointments-circle-select"
:items="circles"
borderless
></ItDropdownSelect>
</div>
<div class="bg-white px-5">
<DueDatesList
:show-top-border="false"
:show-bottom-border="canLoadMore"
:due-dates="appointments"
:show-all-due-dates-link="false"
:max-count="numAppointmentsToShow"
data-cy="appointments-list"
:show-course-session="true"
/>
<button
v-if="canLoadMore"
class="py-4 underline"
data-cy="load-more-notifications"
@click="loadAdditionalAppointments()"
>
{{ $t("notifications.load_more") }}
</button>
</div>
</div>
</main>
</div>
</div>
</template>
<style lang="postcss" scoped>
.no-border-last li:last-child {
border-bottom: none !important;
}
</style>

View File

@ -41,8 +41,8 @@ const userStore = useUserStore();
class="bg-white p-4 lg:p-8" class="bg-white p-4 lg:p-8"
@submit.prevent=" @submit.prevent="
userStore.handleLogin( userStore.handleLogin(
state.username, state.username.trim(),
state.password, state.password.trim(),
route.query.next as string route.query.next as string
) )
" "

View File

@ -6,10 +6,10 @@ import { useCourseSessionDetailQuery, useCurrentCourseSession } from "@/composab
import SubmissionsOverview from "@/components/cockpit/SubmissionsOverview.vue"; import SubmissionsOverview from "@/components/cockpit/SubmissionsOverview.vue";
import { useExpertCockpitStore } from "@/stores/expertCockpit"; import { useExpertCockpitStore } from "@/stores/expertCockpit";
import log from "loglevel"; import log from "loglevel";
import CockpitDates from "@/components/cockpit/CockpitDates.vue";
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue"; import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
import UserStatusCount from "@/components/cockpit/UserStatusCount.vue"; import UserStatusCount from "@/components/cockpit/UserStatusCount.vue";
import { useExpertCockpitPageData } from "@/pages/cockpit/cockpitPage/composables"; import { useExpertCockpitPageData } from "@/pages/cockpit/cockpitPage/composables";
import CourseSessionDueDatesList from "@/components/dueDates/CourseSessionDueDatesList.vue";
const props = defineProps<{ const props = defineProps<{
courseSlug: string; courseSlug: string;
@ -105,7 +105,11 @@ const courseSessionDetailResult = useCourseSessionDetailQuery();
</div> </div>
<div class="mb-4 bg-white p-6"> <div class="mb-4 bg-white p-6">
<CockpitDates></CockpitDates> <CourseSessionDueDatesList
:course-session-id="courseSession.id"
:circle-id="expertCockpitStore.currentCircle.id"
:max-count="4"
></CourseSessionDueDatesList>
</div> </div>
<SubmissionsOverview <SubmissionsOverview
:course-session="courseSession" :course-session="courseSession"

View File

@ -82,6 +82,7 @@ const selectedSessionRouteQuery = useRouteQuery("session", UNFILTERED, {
const selectedSession = ref<DropboxItem>(courseSessions.value[0]); const selectedSession = ref<DropboxItem>(courseSessions.value[0]);
watch(selectedSession, () => { watch(selectedSession, () => {
// @ts-ignore
selectedSessionRouteQuery.value = selectedSession.value.id; selectedSessionRouteQuery.value = selectedSession.value.id;
}); });

View File

@ -1,6 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted } from "vue"; import { onMounted } from "vue";
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
import { useDashboardStore } from "@/stores/dashboard"; import { useDashboardStore } from "@/stores/dashboard";
import type { DashboardCourseConfigType } from "@/services/dashboard"; import type { DashboardCourseConfigType } from "@/services/dashboard";
import LoadingSpinner from "@/components/ui/LoadingSpinner.vue"; import LoadingSpinner from "@/components/ui/LoadingSpinner.vue";
@ -24,20 +23,11 @@ function newDashboardConfigForId(id: string): DashboardCourseConfigType | undefi
<LoadingSpinner /> <LoadingSpinner />
</div> </div>
<div <div
v-else-if="dashboardStore.currentDashboardConfig" v-else-if="dashboardStore.dashboardConfigsv2.length"
class="flex flex-col lg:flex-row" class="flex flex-col lg:flex-row"
> >
<main class="grow bg-gray-200 lg:order-2"> <main class="grow bg-gray-200 lg:order-2">
<div class="m-8"> <div class="m-8">
<div class="mb-10 flex items-center justify-between">
<h1 data-cy="dashboard-title">Dashboard</h1>
<ItDropdownSelect
:model-value="dashboardStore.currentDashboardConfig"
class="mt-4 w-full lg:mt-0 lg:w-96"
:items="dashboardStore.dashboardConfigs"
@update:model-value="dashboardStore.switchAndLoadDashboardConfig"
></ItDropdownSelect>
</div>
<!-- new way of dashboard --> <!-- new way of dashboard -->
<ul> <ul>
<li <li

View File

@ -1,5 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
import DueDatesShortList from "@/components/dueDates/DueDatesShortList.vue";
import LearningPathListView from "@/pages/learningPath/learningPathPage/LearningPathListView.vue"; import LearningPathListView from "@/pages/learningPath/learningPathPage/LearningPathListView.vue";
import LearningPathPathView from "@/pages/learningPath/learningPathPage/LearningPathPathView.vue"; import LearningPathPathView from "@/pages/learningPath/learningPathPage/LearningPathPathView.vue";
import CircleProgress from "@/pages/learningPath/learningPathPage/LearningPathProgress.vue"; import CircleProgress from "@/pages/learningPath/learningPathPage/LearningPathProgress.vue";
@ -8,7 +7,12 @@ import type { ViewType } from "@/pages/learningPath/learningPathPage/LearningPat
import LearningPathViewSwitch from "@/pages/learningPath/learningPathPage/LearningPathViewSwitch.vue"; import LearningPathViewSwitch from "@/pages/learningPath/learningPathPage/LearningPathViewSwitch.vue";
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core"; import { breakpointsTailwind, useBreakpoints } from "@vueuse/core";
import { computed, ref } from "vue"; import { computed, ref } from "vue";
import { useCourseCircleProgress, useCourseDataWithCompletion } from "@/composables"; import {
useCourseCircleProgress,
useCourseDataWithCompletion,
useCurrentCourseSession,
} from "@/composables";
import CourseSessionDueDatesList from "@/components/dueDates/CourseSessionDueDatesList.vue";
const props = defineProps<{ const props = defineProps<{
courseSlug: string; courseSlug: string;
@ -27,6 +31,8 @@ const lpQueryResult = useCourseDataWithCompletion(props.courseSlug);
const learningPath = computed(() => lpQueryResult.learningPath.value); const learningPath = computed(() => lpQueryResult.learningPath.value);
const course = computed(() => lpQueryResult.course.value); const course = computed(() => lpQueryResult.course.value);
const courseSession = useCurrentCourseSession();
const { inProgressCirclesCount, circlesCount } = useCourseCircleProgress( const { inProgressCirclesCount, circlesCount } = useCourseCircleProgress(
lpQueryResult.circles lpQueryResult.circles
); );
@ -60,10 +66,10 @@ const changeViewType = (viewType: ViewType) => {
<!-- Right --> <!-- Right -->
<div v-if="!useMobileLayout" class="flex-grow"> <div v-if="!useMobileLayout" class="flex-grow">
<div class="text-bold pb-3"> <CourseSessionDueDatesList
{{ $t("a.Nächste Termine") }} :course-session-id="courseSession.id"
</div> :max-count="2"
<DueDatesShortList :max-count="2" :show-top-border="true"></DueDatesShortList> ></CourseSessionDueDatesList>
</div> </div>
</div> </div>

View File

@ -7,18 +7,15 @@ import type {
} from "vue-router"; } from "vue-router";
const routeHistory: RouteLocationNormalized[] = []; const routeHistory: RouteLocationNormalized[] = [];
const MAX_HISTORY = 10; // for example, store the last 10 visited routes const MAX_HISTORY = 10;
let isFirstNavigation = true; let isFirstNavigation = true;
// Variable to track the type of the last navigation
let lastNavigationWasPush = false; let lastNavigationWasPush = false;
// Function to set the state
export function setLastNavigationWasPush(value: boolean) { export function setLastNavigationWasPush(value: boolean) {
lastNavigationWasPush = value; lastNavigationWasPush = value;
} }
// Function to get the current state
export function getLastNavigationWasPush() { export function getLastNavigationWasPush() {
return lastNavigationWasPush; return lastNavigationWasPush;
} }

View File

@ -324,14 +324,6 @@ const router = createRouter({
path: "/notifications", path: "/notifications",
component: () => import("@/pages/NotificationsPage.vue"), component: () => import("@/pages/NotificationsPage.vue"),
}, },
{
path: "/appointments",
component: () => import("@/pages/AppointmentsPage.vue"),
},
{
path: "/course/:courseSlug/appointments",
component: () => import("@/pages/AppointmentsPage.vue"),
},
{ {
path: "/onboarding/:courseType", path: "/onboarding/:courseType",
props: true, props: true,

View File

@ -1,9 +1,8 @@
import { itGetCached } from "@/fetchHelpers"; import { itGetCached } from "@/fetchHelpers";
import type { CourseSession, DueDate } from "@/types"; import type { CourseSession } 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";
import dayjs from "dayjs";
import uniqBy from "lodash/uniqBy"; import uniqBy from "lodash/uniqBy";
import log from "loglevel"; import log from "loglevel";
import { defineStore } from "pinia"; import { defineStore } from "pinia";
@ -25,13 +24,6 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
const userStore = useUserStore(); const userStore = useUserStore();
if (userStore.loggedIn) { if (userStore.loggedIn) {
// TODO: refactor after implementing of Klassenkonzept
await Promise.all(
allCourseSessions.value.map(async (cs) => {
sortDueDates(cs.due_dates);
})
);
if (!allCourseSessions.value) { if (!allCourseSessions.value) {
throw `No courseSessionData found for user`; throw `No courseSessionData found for user`;
} }
@ -137,37 +129,12 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
return Boolean(hasPreview && (inLearningPath() || inCompetenceProfile())); return Boolean(hasPreview && (inLearningPath() || inCompetenceProfile()));
}); });
function allDueDates() {
const allDueDatesReturn: DueDate[] = [];
allCourseSessions.value?.forEach((cs) => {
allDueDatesReturn.push(...cs.due_dates);
});
sortDueDates(allDueDatesReturn);
return allDueDatesReturn;
}
function sortDueDates(dueDates: DueDate[]) {
dueDates.sort((a, b) => {
const dateA = dayjs(a.start);
const dateB = dayjs(b.start);
if (!dateA.isValid() && !dateB.isValid()) return 0; // If both are invalid, they are equal
if (!dateA.isValid()) return 1; // If dateA is invalid, it goes after dateB
if (!dateB.isValid()) return -1; // If dateB is invalid, it goes after dateA
return dateA.diff(dateB); // sort by `start`
});
}
return { return {
uniqueCourseSessionsByCourse, uniqueCourseSessionsByCourse,
allCurrentCourseSessions, allCurrentCourseSessions,
getCourseSessionById, getCourseSessionById,
switchCourseSessionById, switchCourseSessionById,
isCourseSessionPreviewActive, isCourseSessionPreviewActive,
allDueDates,
// use `useCurrentCourseSession` whenever possible // use `useCurrentCourseSession` whenever possible
currentCourseSession, currentCourseSession,

View File

@ -62,16 +62,16 @@ export const useDashboardStore = defineStore("dashboard", () => {
dashboardConfigsv2.value = await fetchDashboardConfigv2(); dashboardConfigsv2.value = await fetchDashboardConfigv2();
console.log("got dashboard config v2: ", dashboardConfigsv2.value); console.log("got dashboard config v2: ", dashboardConfigsv2.value);
try { try {
if (!currentDashboardConfig.value) { // if (!currentDashboardConfig.value) {
await loadDashboardConfig(); // await loadDashboardConfig();
return; // return;
} // }
const { id, dashboard_type } = currentDashboardConfig.value; // const { id, dashboard_type } = currentDashboardConfig.value;
if (dashBoardDataCache[id]) { // if (dashBoardDataCache[id]) {
currentDashBoardData.value = dashBoardDataCache[id]; // currentDashBoardData.value = dashBoardDataCache[id];
return; // return;
} // }
// await loadDashboardData(dashboard_type, id); // // await loadDashboardData(dashboard_type, id);
} finally { } finally {
console.log("done loading dashboard details"); console.log("done loading dashboard details");
loading.value = false; loading.value = false;

View File

@ -452,7 +452,6 @@ export interface CourseSession {
title: string; title: string;
start_date: string; start_date: string;
end_date: string; end_date: string;
due_dates: DueDate[];
actions: string[]; actions: string[];
} }

View File

@ -1,4 +1,3 @@
from django.db.models import Q
from rest_framework import serializers from rest_framework import serializers
from vbv_lernwelt.core.utils import StringIDField from vbv_lernwelt.core.utils import StringIDField
@ -10,8 +9,6 @@ from vbv_lernwelt.course.models import (
CourseConfiguration, CourseConfiguration,
CourseSession, CourseSession,
) )
from vbv_lernwelt.duedate.models import DueDate
from vbv_lernwelt.duedate.serializers import DueDateSerializer
from vbv_lernwelt.iam.permissions import course_session_permissions from vbv_lernwelt.iam.permissions import course_session_permissions
@ -72,19 +69,12 @@ class CourseSessionSerializer(serializers.ModelSerializer):
id = StringIDField() id = StringIDField()
course = serializers.SerializerMethodField() course = serializers.SerializerMethodField()
due_dates = serializers.SerializerMethodField()
actions = serializers.SerializerMethodField() actions = serializers.SerializerMethodField()
user_roles = serializers.SerializerMethodField() user_roles = serializers.SerializerMethodField()
def get_course(self, obj): def get_course(self, obj):
return CourseSerializer(obj.course).data return CourseSerializer(obj.course).data
def get_due_dates(self, obj):
due_dates = DueDate.objects.filter(
Q(start__isnull=False) | Q(end__isnull=False), course_session_id=obj.id
)
return DueDateSerializer(due_dates, many=True).data
def get_user_roles(self, obj): def get_user_roles(self, obj):
if hasattr(obj, "roles"): if hasattr(obj, "roles"):
return list(obj.roles) return list(obj.roles)
@ -100,7 +90,6 @@ class CourseSessionSerializer(serializers.ModelSerializer):
"title", "title",
"start_date", "start_date",
"end_date", "end_date",
"due_dates",
"actions", "actions",
"user_roles", "user_roles",
] ]

View File

@ -1,7 +1,5 @@
import json import json
from datetime import datetime
from django.utils import timezone
from rest_framework.test import APITestCase from rest_framework.test import APITestCase
from vbv_lernwelt.core.create_default_users import create_default_users from vbv_lernwelt.core.create_default_users import create_default_users
@ -9,9 +7,6 @@ from vbv_lernwelt.core.models import User
from vbv_lernwelt.course.consts import COURSE_TEST_ID from vbv_lernwelt.course.consts import COURSE_TEST_ID
from vbv_lernwelt.course.creators.test_course import create_test_course from vbv_lernwelt.course.creators.test_course import create_test_course
from vbv_lernwelt.course.models import CourseSession, CourseSessionUser from vbv_lernwelt.course.models import CourseSession, CourseSessionUser
from vbv_lernwelt.course_session.models import CourseSessionAssignment
from vbv_lernwelt.duedate.factories import DueDateFactory
from vbv_lernwelt.learnpath.models import LearningContentAssignment
class CourseCompletionApiTestCase(APITestCase): class CourseCompletionApiTestCase(APITestCase):
@ -56,41 +51,3 @@ class CourseCompletionApiTestCase(APITestCase):
print(json.dumps(response.json(), indent=4)) print(json.dumps(response.json(), indent=4))
self.assertEqual(response.json()[0]["id"], str(self.course_session.id)) self.assertEqual(response.json()[0]["id"], str(self.course_session.id))
def test_api_hasNoDueDates(self):
self.client.login(username="admin", password="test")
response = self.client.get(f"/api/course/sessions/")
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()), 1)
print(json.dumps(response.json(), indent=4))
self.assertEqual(response.json()[0]["due_dates"], [])
def test_api_hasDueDates(self):
cs = CourseSession.objects.first()
due_date = DueDateFactory(
start=timezone.make_aware(datetime.now()),
course_session=cs,
title="Test Due Date",
)
csa = CourseSessionAssignment.objects.create(
course_session=cs,
learning_content=LearningContentAssignment.objects.get(
slug=f"test-lehrgang-lp-circle-reisen-lc-mein-kundenstamm"
),
)
csa.submission_deadline = due_date
csa.save()
self.client.login(username="admin", password="test")
response = self.client.get(f"/api/course/sessions/")
self.assertEqual(response.status_code, 200)
print(json.dumps(response.json(), indent=4))
self.assertEqual(len(response.json()), 1)
self.assertEqual(len(response.json()[0]["due_dates"]), 1)
self.assertEqual(response.json()[0]["due_dates"][0]["title"], "Test Due Date")

View File

@ -52,6 +52,7 @@ class DashboardQuery(graphene.ObjectType):
course_session_ids = set() course_session_ids = set()
# supervisors
for group in CourseSessionGroup.objects.filter(course=course): for group in CourseSessionGroup.objects.filter(course=course):
if can_view_course_session_group_statistics(user=user, group=group): if can_view_course_session_group_statistics(user=user, group=group):
course_session_ids.update( course_session_ids.update(