feat: cockpit type / navigation

TODO dashboard -> cockpit/mentor (temporary)
TODO dashboard -> cockpit/expert
This commit is contained in:
Livio Bieri 2024-03-08 15:43:06 +01:00
parent d9cb334404
commit 742801bf22
17 changed files with 246 additions and 541 deletions

View File

@ -24,8 +24,9 @@ if (!courseSession) {
throw new Error("Course session not found");
}
const isExpert = courseSessionsStore.hasCockpit(courseSession);
const url = isExpert ? props.dueDate.url_expert : props.dueDate.url;
const url = courseSession.actions.includes("expert-cockpit")
? props.dueDate.url_expert
: props.dueDate.url;
const courseSessionTitle = computed(() => {
if (props.dueDate.course_session_id) {

View File

@ -3,11 +3,9 @@ import { useTranslation } from "i18next-vue";
import { useRouteLookups } from "@/utils/route";
import { useCurrentCourseSession } from "@/composables";
import { getCompetenceNaviUrl, getLearningPathUrl } from "@/utils/utils";
import { useCockpitStore } from "@/stores/cockpit";
const { inCompetenceProfile, inLearningPath } = useRouteLookups();
const courseSession = useCurrentCourseSession();
const cockpit = useCockpitStore();
const { t } = useTranslation();
</script>
@ -34,7 +32,6 @@ const { t } = useTranslation();
</router-link>
<router-link
v-if="cockpit.hasExpertCockpitType"
data-cy="preview-competence-profile-link"
:to="getCompetenceNaviUrl(courseSession.course.slug)"
class="preview-nav-item"

View File

@ -22,7 +22,6 @@ import {
getLearningPathUrl,
getMediaCenterUrl,
} from "@/utils/utils";
import { useCockpitStore } from "@/stores/cockpit";
log.debug("MainNavigationBar created");
@ -70,47 +69,69 @@ onMounted(() => {
log.debug("MainNavigationBar mounted");
});
const hasMediaLibraryMenu = computed(() => {
if (useCockpitStore().hasMentorCockpitType) {
return false;
}
return inCourse() && Boolean(courseSessionsStore.currentCourseSession);
});
const hasLearningPathMenu = computed(() =>
Boolean(
courseSessionsStore.currentCourseSession?.actions.includes("learning-path") &&
inCourse()
)
);
const hasCockpitMenu = computed(() => {
return courseSessionsStore.currentCourseSessionHasCockpit;
});
const hasCompetenceNaviMenu = computed(() =>
Boolean(
courseSessionsStore.currentCourseSession?.actions.includes("competence-navi") &&
inCourse()
)
);
const hasPreviewMenu = computed(() => {
return (
useCockpitStore().hasExpertCockpitType || useCockpitStore().hasMentorCockpitType
);
});
const hasMediaLibraryMenu = computed(() =>
Boolean(
courseSessionsStore.currentCourseSession?.actions.includes("media-library") &&
inCourse()
)
);
const hasAppointmentsMenu = computed(() => {
if (useCockpitStore().hasMentorCockpitType) {
return false;
}
return userStore.loggedIn;
});
const hasCockpitMenu = computed(() =>
Boolean(courseSessionsStore.currentCourseSession?.actions.includes("expert-cockpit"))
);
const hasPreviewMenu = computed(() =>
Boolean(courseSessionsStore.currentCourseSession?.actions.includes("preview"))
);
const hasAppointmentsMenu = computed(() =>
Boolean(
courseSessionsStore.currentCourseSession?.actions.includes("appointments") &&
userStore.loggedIn
)
);
const hasNotificationsMenu = computed(() => {
return userStore.loggedIn;
});
const hasMentorManagementMenu = computed(() => {
if (courseSessionsStore.currentCourseSessionHasCockpit || !inCourse()) {
const hasLearningMentor = computed(() => {
if (!inCourse()) {
return false;
}
return (
courseSessionsStore.currentCourseSession?.course.configuration
.enable_learning_mentor ?? false
);
if (!courseSessionsStore.currentCourseSession) {
return false;
}
const courseSession = courseSessionsStore.currentCourseSession;
const course = courseSession.course;
if (!course.configuration.enable_learning_mentor) {
return false;
}
// FIXME: Use learning-mentor action instead of deprecated-mentor once we have moved everything from cockpit!
return courseSession.actions.includes("deprecated-mentor");
});
</script>
<template>
<CoursePreviewBar v-if="courseSessionsStore.hasCourseSessionPreview" />
<CoursePreviewBar v-if="courseSessionsStore.isCourseSessionPreviewActive" />
<div v-else>
<Teleport to="body">
<MobileMenu
@ -120,6 +141,11 @@ const hasMentorManagementMenu = computed(() => {
:has-media-library-menu="hasMediaLibraryMenu"
:has-cockpit-menu="hasCockpitMenu"
:has-preview-menu="hasPreviewMenu"
:has-learning-path-menu="hasLearningPathMenu"
:has-competence-navi-menu="hasCompetenceNaviMenu"
:has-learning-mentor="hasLearningMentor"
:has-notifications-menu="hasNotificationsMenu"
:has-appointments-menu="hasAppointmentsMenu"
:media-url="
getMediaCenterUrl(courseSessionsStore.currentCourseSession?.course?.slug)
"
@ -177,79 +203,77 @@ const hasMentorManagementMenu = computed(() => {
only relevant if there is a current course session -->
<template v-if="courseSessionsStore.currentCourseSession">
<div class="hidden space-x-8 lg:flex">
<template v-if="courseSessionsStore.currentCourseSessionHasCockpit">
<router-link
v-if="hasCockpitMenu"
data-cy="navigation-cockpit-link"
:to="
getCockpitUrl(
courseSessionsStore.currentCourseSession.course.slug
)
"
class="nav-item"
:class="{ 'nav-item--active': inCockpit() }"
>
{{ t("cockpit.title") }}
</router-link>
<router-link
v-if="hasCockpitMenu"
data-cy="navigation-cockpit-link"
:to="
getCockpitUrl(
courseSessionsStore.currentCourseSession.course.slug
)
"
class="nav-item"
:class="{ 'nav-item--active': inCockpit() }"
>
{{ t("cockpit.title") }}
</router-link>
<router-link
v-if="hasPreviewMenu"
data-cy="navigation-preview-link"
:to="
getLearningPathUrl(
courseSessionsStore.currentCourseSession.course.slug
)
"
target="_blank"
class="nav-item"
>
<div class="flex items-center">
<span>{{ t("a.VorschauTeilnehmer") }}</span>
<it-icon-external-link class="ml-2" />
</div>
</router-link>
</template>
<template v-else>
<router-link
data-cy="navigation-learning-path-link"
:to="
getLearningPathUrl(
courseSessionsStore.currentCourseSession.course.slug
)
"
class="nav-item"
:class="{ 'nav-item--active': inLearningPath() }"
>
{{ t("general.learningPath") }}
</router-link>
<router-link
v-if="hasPreviewMenu"
data-cy="navigation-preview-link"
:to="
getLearningPathUrl(
courseSessionsStore.currentCourseSession.course.slug
)
"
target="_blank"
class="nav-item"
>
<div class="flex items-center">
<span>{{ t("a.VorschauTeilnehmer") }}</span>
<it-icon-external-link class="ml-2" />
</div>
</router-link>
<router-link
v-if="hasLearningPathMenu"
data-cy="navigation-learning-path-link"
:to="
getLearningPathUrl(
courseSessionsStore.currentCourseSession.course.slug
)
"
class="nav-item"
:class="{ 'nav-item--active': inLearningPath() }"
>
{{ t("general.learningPath") }}
</router-link>
<router-link
data-cy="navigation-competence-profile-link"
:to="
getCompetenceNaviUrl(
courseSessionsStore.currentCourseSession.course.slug
)
"
class="nav-item"
:class="{ 'nav-item--active': inCompetenceProfile() }"
>
{{ t("competences.title") }}
</router-link>
<router-link
v-if="hasCompetenceNaviMenu"
data-cy="navigation-competence-profile-link"
:to="
getCompetenceNaviUrl(
courseSessionsStore.currentCourseSession.course.slug
)
"
class="nav-item"
:class="{ 'nav-item--active': inCompetenceProfile() }"
>
{{ t("competences.title") }}
</router-link>
<router-link
v-if="hasMentorManagementMenu"
data-cy="navigation-learning-mentor-link"
:to="
getLearningMentorManagementUrl(
courseSessionsStore.currentCourseSession.course.slug
)
"
class="nav-item"
:class="{ 'nav-item--active': inLearningMentor() }"
>
{{ t("a.Lernbegleitung") }}
</router-link>
</template>
<router-link
v-if="hasLearningMentor"
data-cy="navigation-learning-mentor-link"
:to="
getLearningMentorManagementUrl(
courseSessionsStore.currentCourseSession.course.slug
)
"
class="nav-item"
:class="{ 'nav-item--active': inLearningMentor() }"
>
{{ t("a.Lernbegleitung") }}
</router-link>
</div>
</template>
</div>

View File

@ -1,6 +1,5 @@
<script setup lang="ts">
import ItFullScreenModal from "@/components/ui/ItFullScreenModal.vue";
import { useCourseSessionsStore } from "@/stores/courseSessions";
import type { User } from "@/stores/user";
import type { CourseSession } from "@/types";
import { useRouter } from "vue-router";
@ -18,6 +17,11 @@ defineProps<{
hasMediaLibraryMenu: boolean;
hasPreviewMenu: boolean;
hasCockpitMenu: boolean;
hasLearningPathMenu: boolean;
hasCompetenceNaviMenu: boolean;
hasLearningMentor: boolean;
hasNotificationsMenu: boolean;
hasAppointmentsMenu: boolean;
courseSession: CourseSession | undefined;
mediaUrl?: string;
user: User | undefined;
@ -31,8 +35,6 @@ const clickLink = (to: string | undefined) => {
emit("closemodal");
}
};
const courseSessionsStore = useCourseSessionsStore();
</script>
<template>
@ -57,42 +59,38 @@ const courseSessionsStore = useCourseSessionsStore();
<div v-if="courseSession" class="mt-6 border-b">
<h4 class="text-sm text-gray-900">{{ courseSession.course.title }}</h4>
<ul class="mt-6">
<template v-if="courseSessionsStore.currentCourseSessionHasCockpit">
<li v-if="hasCockpitMenu" class="mb-6">
<button
data-cy="navigation-mobile-cockpit-link"
@click="clickLink(getCockpitUrl(courseSession.course.slug))"
>
{{ $t("cockpit.title") }}
</button>
</li>
<li v-if="hasPreviewMenu" class="mb-6">
<button
data-cy="navigation-mobile-preview-link"
@click="clickLink(getLearningPathUrl(courseSession.course.slug))"
>
{{ $t("a.VorschauTeilnehmer") }}
</button>
</li>
</template>
<template v-else>
<li class="mb-6">
<button
data-cy="navigation-mobile-learning-path-link"
@click="clickLink(getLearningPathUrl(courseSession.course.slug))"
>
{{ $t("general.learningPath") }}
</button>
</li>
<li class="mb-6">
<button
data-cy="navigation-mobile-competence-profile-link"
@click="clickLink(getCompetenceNaviUrl(courseSession.course.slug))"
>
{{ $t("competences.title") }}
</button>
</li>
</template>
<li v-if="hasCockpitMenu" class="mb-6">
<button
data-cy="navigation-mobile-cockpit-link"
@click="clickLink(getCockpitUrl(courseSession.course.slug))"
>
{{ $t("cockpit.title") }}
</button>
</li>
<li v-if="hasPreviewMenu" class="mb-6">
<button
data-cy="navigation-mobile-preview-link"
@click="clickLink(getLearningPathUrl(courseSession.course.slug))"
>
{{ $t("a.VorschauTeilnehmer") }}
</button>
</li>
<li v-if="hasLearningPathMenu" class="mb-6">
<button
data-cy="navigation-mobile-learning-path-link"
@click="clickLink(getLearningPathUrl(courseSession.course.slug))"
>
{{ $t("general.learningPath") }}
</button>
</li>
<li v-if="hasCompetenceNaviMenu" class="mb-6">
<button
data-cy="navigation-mobile-competence-profile-link"
@click="clickLink(getCompetenceNaviUrl(courseSession.course.slug))"
>
{{ $t("competences.title") }}
</button>
</li>
<li v-if="hasMediaLibraryMenu" class="mb-6">
<button
data-cy="medialibrary-link"

View File

@ -17,7 +17,6 @@ import { computed, onUnmounted } from "vue";
import { getPreviousRoute } from "@/router/history";
import { getCompetenceNaviUrl } from "@/utils/utils";
import SelfEvaluationRequestFeedbackPage from "@/pages/learningPath/selfEvaluationPage/SelfEvaluationRequestFeedbackPage.vue";
import { useCockpitStore } from "@/stores/cockpit";
log.debug("LearningContent.vue setup");
@ -30,11 +29,8 @@ const circleStore = useCircleStore();
const courseSession = useCurrentCourseSession();
const courseCompletionData = useCourseDataWithCompletion();
const isReadOnly = computed(
// a hack: If we are a mentor or expert, we are in read only mode
// we might preview / view this but can't change anything (buttons are disabled)
() => useCockpitStore().hasExpertCockpitType || useCockpitStore().hasMentorCockpitType
);
// if we have preview rights, we can only preview the learning content -> read only
const isReadOnly = computed(() => courseSession.value.actions.includes("preview"));
const questions = computed(() => props.learningUnit?.performance_criteria ?? []);
const numPages = computed(() => {

View File

@ -13,11 +13,11 @@ defineEmits(["exit"]);
<template>
<div>
<div class="absolute bottom-0 left-0 top-0 w-full bg-white">
<CoursePreviewBar v-if="courseSessionsStore.hasCourseSessionPreview" />
<CoursePreviewBar v-if="courseSessionsStore.isCourseSessionPreviewActive" />
<div
:class="{
'h-content': !courseSessionsStore.hasCourseSessionPreview,
'h-content-preview': courseSessionsStore.hasCourseSessionPreview,
'h-content': !courseSessionsStore.isCourseSessionPreviewActive,
'h-content-preview': courseSessionsStore.isCourseSessionPreviewActive,
}"
class="overflow-y-auto"
>

View File

@ -1,39 +0,0 @@
import { createPinia, setActivePinia } from "pinia";
import { beforeEach, describe, expect, vi } from "vitest";
import * as courseSessions from "../../stores/courseSessions";
import { expertRequired } from "../guards";
describe("Guards", () => {
afterEach(() => {
vi.restoreAllMocks();
});
beforeEach(() => {
// creates a fresh pinia and make it active so it's automatically picked
// up by any useStore() call without having to pass it to it:
// `useStore(pinia)`
setActivePinia(createPinia());
});
it("cannot route to cockpit", () => {
vi.spyOn(courseSessions, "useCourseSessionsStore").mockReturnValue({
currentCourseSessionHasCockpit: false,
});
const slug = "test";
expect(expertRequired({ params: { courseSlug: "test" } })).toEqual(
`/course/${slug}/learn`
);
});
it("can route to cockpit", () => {
vi.spyOn(courseSessions, "useCourseSessionsStore").mockReturnValue({
currentCourseSessionHasCockpit: true,
});
const to = {
params: {
courseSlug: "test",
},
};
expect(expertRequired(to)).toBe(true);
});
});

View File

@ -1,5 +1,4 @@
import { getLoginURLNext, shouldUseSSO } from "@/router/utils";
import { useCockpitStore } from "@/stores/cockpit";
import { useCourseSessionsStore } from "@/stores/courseSessions";
import { useUserStore } from "@/stores/user";
import type { NavigationGuard, RouteLocationNormalized } from "vue-router";
@ -52,16 +51,6 @@ const loginRequired = (to: RouteLocationNormalized) => {
return !to.meta?.public;
};
export const expertRequired: NavigationGuard = (to: RouteLocationNormalized) => {
const courseSessionsStore = useCourseSessionsStore();
if (courseSessionsStore.currentCourseSessionHasCockpit) {
return true;
} else {
const courseSlug = to.params.courseSlug as string;
return `/course/${courseSlug}/learn`;
}
};
export async function handleCurrentCourseSession(to: RouteLocationNormalized) {
// register after login hooks
const userStore = useUserStore();
@ -112,31 +101,6 @@ export async function handleCourseSessionAsQueryParam(to: RouteLocationNormalize
}
}
export async function handleCockpit(to: RouteLocationNormalized) {
const courseSessionsStore = useCourseSessionsStore();
const courseId = courseSessionsStore.currentCourseSession?.course.id || null;
const cockpitStore = useCockpitStore();
await cockpitStore.fetchCockpitType(courseId);
if (to.name === "cockpit") {
if (cockpitStore.hasExpertCockpitType) {
return { name: "expertCockpit", params: to.params };
} else if (cockpitStore.hasMentorCockpitType) {
return { name: "mentorCockpitOverview", params: to.params };
}
}
const cockpitType = to.meta?.cockpitType;
if (!cockpitType) {
return;
}
if (cockpitType !== cockpitStore.cockpitType) {
return "/";
}
}
export async function handleAcceptLearningMentorInvitation(
to: RouteLocationNormalized
) {

View File

@ -5,7 +5,6 @@ import UKStartPage from "@/pages/start/UKStartPage.vue";
import VVStartPage from "@/pages/start/VVStartPage.vue";
import {
handleAcceptLearningMentorInvitation,
handleCockpit,
handleCourseSessionAsQueryParam,
handleCurrentCourseSession,
redirectToLoginIfRequired,
@ -186,35 +185,23 @@ const router = createRouter({
component: () => import("@/pages/cockpit/cockpitPage/CockpitExpertPage.vue"),
props: true,
name: "expertCockpit",
meta: {
cockpitType: "expert",
},
},
{
path: "mentor",
component: () => import("@/pages/cockpit/cockpitPage/CockpitMentorPage.vue"),
name: "mentorCockpit",
meta: {
cockpitType: "mentor",
},
children: [
{
path: "",
component: () =>
import("@/pages/cockpit/cockpitPage/mentor/MentorOverviewPage.vue"),
name: "mentorCockpitOverview",
meta: {
cockpitType: "mentor",
},
},
{
path: "participants",
component: () =>
import("@/pages/cockpit/cockpitPage/mentor/MentorParticipantsPage.vue"),
name: "mentorCockpitParticipants",
meta: {
cockpitType: "mentor",
},
},
{
path: "self-evaluation-feedback/:learningUnitId",
@ -223,18 +210,12 @@ const router = createRouter({
"@/pages/cockpit/cockpitPage/mentor/SelfEvaluationFeedbackPage.vue"
),
name: "mentorSelfEvaluationFeedback",
meta: {
cockpitType: "mentor",
},
props: true,
},
{
path: "details",
component: () =>
import("@/pages/cockpit/cockpitPage/mentor/MentorDetailParentPage.vue"),
meta: {
cockpitType: "mentor",
},
children: [
{
path: "praxis-assignments/:praxisAssignmentId",
@ -243,9 +224,6 @@ const router = createRouter({
"@/pages/cockpit/cockpitPage/mentor/MentorPraxisAssignmentPage.vue"
),
name: "mentorCockpitPraxisAssignments",
meta: {
cockpitType: "mentor",
},
props: true,
},
{
@ -255,9 +233,6 @@ const router = createRouter({
"@/pages/cockpit/cockpitPage/mentor/MentorSelfEvaluationFeedbackAssignmentPage.vue"
),
name: "mentorCockpitSelfEvaluationFeedbackAssignments",
meta: {
cockpitType: "mentor",
},
props: true,
},
],
@ -428,8 +403,6 @@ router.beforeEach(redirectToLoginIfRequired);
router.beforeEach(handleCurrentCourseSession);
router.beforeEach(handleCourseSessionAsQueryParam);
router.beforeEach(handleCockpit);
router.beforeEach(addToHistory);
export default router;

View File

@ -1,95 +0,0 @@
import type { CourseSession } from "@/types";
import { createPinia, setActivePinia } from "pinia";
import { beforeEach, describe, expect, vi } from "vitest";
import { useCourseSessionsStore } from "../courseSessions";
import { useUserStore } from "../user";
let user = {};
let courseSessions: CourseSession[] = [];
describe("CourseSession Store", () => {
vi.mock("vue-router", () => ({
useRoute: () => ({
path: "/course/test-course/learn/",
}),
}));
vi.mock("@/fetchHelpers", () => {
const itGetCached = () => Promise.resolve([]);
const itPost = () => Promise.resolve([]);
return {
itGetCached,
itPost,
};
});
beforeEach(() => {
// creates a fresh pinia and make it active so it's automatically picked
// up by any useStore() call without having to pass it to it:
// `useStore(pinia)`
setActivePinia(createPinia());
user = {
is_superuser: false,
course_session_expert: [],
};
courseSessions = [
{
id: "1",
created_at: "2021-05-11T10:00:00.000000Z",
updated_at: "2023-05-11T10:00:00.000000Z",
course: {
id: "1",
title: "Test Course",
category_name: "Test Category",
slug: "test-course",
},
title: "Test Course Session",
start_date: "2022-05-11T10:00:00.000000Z",
end_date: "2023-05-11T10:00:00.000000Z",
learning_path_url: "/course/test-course/learn/",
competence_url: "/course/test-course/competence/",
course_url: "/course/test-course/",
media_library_url: "/course/test-course/media/",
attendance_courses: [],
additional_json_data: {},
documents: [],
},
];
});
it("normal user has no cockpit", () => {
const userStore = useUserStore();
userStore.$patch(user);
const courseSessionsStore = useCourseSessionsStore();
courseSessionsStore._currentCourseSlug = "test-course";
courseSessionsStore.allCourseSessions = courseSessions;
expect(courseSessionsStore.currentCourseSessionHasCockpit).toBeFalsy();
});
it("superuser has cockpit", () => {
const userStore = useUserStore();
userStore.$patch(Object.assign(user, { is_superuser: true }));
const courseSessionsStore = useCourseSessionsStore();
courseSessionsStore._currentCourseSlug = "test-course";
courseSessionsStore.allCourseSessions = courseSessions;
expect(courseSessionsStore.currentCourseSessionHasCockpit).toBeTruthy();
});
it("expert has cockpit", () => {
const userStore = useUserStore();
userStore.$patch(
Object.assign(user, { course_session_experts: [courseSessions[0].id] })
);
const courseSessionsStore = useCourseSessionsStore();
courseSessionsStore._currentCourseSlug = "test-course";
courseSessionsStore.allCourseSessions = courseSessions;
expect(courseSessionsStore.currentCourseSessionHasCockpit).toBeTruthy();
});
});

View File

@ -1,37 +0,0 @@
import { itGetCached } from "@/fetchHelpers";
import { defineStore } from "pinia";
import type { Ref } from "vue";
import { computed, ref } from "vue";
type CockpitType = "mentor" | "expert" | null;
export const useCockpitStore = defineStore("cockpit", () => {
const cockpitType: Ref<CockpitType> = ref(null);
const isLoading = ref(false);
const hasExpertCockpitType = computed(() => cockpitType.value === "expert");
const hasMentorCockpitType = computed(() => cockpitType.value === "mentor");
const hasNoCockpitType = computed(() => cockpitType.value === null);
async function fetchCockpitType(courseId: string | null) {
if (!courseId) {
cockpitType.value = null;
return;
}
isLoading.value = true;
const url = `/api/course/${courseId}/cockpit/`;
const response = await itGetCached(url);
cockpitType.value = response.type;
isLoading.value = false;
}
return {
fetchCockpitType,
hasExpertCockpitType,
hasMentorCockpitType,
hasNoCockpitType,
isLoading,
cockpitType,
};
});

View File

@ -1,5 +1,4 @@
import { itGetCached } from "@/fetchHelpers";
import { useCockpitStore } from "@/stores/cockpit";
import type { CourseSession, DueDate } from "@/types";
import eventBus from "@/utils/eventBus";
import { useRouteLookups } from "@/utils/route";
@ -133,31 +132,11 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
return allCourseSessionsForCourse(_currentCourseSlug.value);
});
const currentCourseSessionHasCockpit = computed(() => {
if (currentCourseSession.value) {
return hasCockpit(currentCourseSession.value);
}
return false;
const isCourseSessionPreviewActive = computed(() => {
const hasPreview = currentCourseSession.value?.actions.includes("preview");
return Boolean(hasPreview && (inLearningPath() || inCompetenceProfile()));
});
const hasCourseSessionPreview = computed(() => {
const isCourseExpert =
currentCourseSession.value && currentCourseSessionHasCockpit.value;
return Boolean(isCourseExpert && (inLearningPath() || inCompetenceProfile()));
});
function hasCockpit(courseSession: CourseSession) {
const userStore = useUserStore();
return (
useCockpitStore().hasMentorCockpitType ||
useCockpitStore().hasExpertCockpitType ||
// for legacy reasons: don't forget course session supervisors!
userStore.course_session_experts.includes(courseSession.id) ||
userStore.is_superuser
);
}
function allDueDates() {
const allDueDatesReturn: DueDate[] = [];
@ -185,12 +164,9 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
return {
uniqueCourseSessionsByCourse,
allCurrentCourseSessions,
courseSessionForCourse,
getCourseSessionById,
switchCourseSessionById,
hasCockpit,
hasCourseSessionPreview,
currentCourseSessionHasCockpit,
isCourseSessionPreviewActive,
allDueDates,
// use `useCurrentCourseSession` whenever possible

View File

@ -12,12 +12,7 @@ from django_ratelimit.exceptions import Ratelimited
from graphene_django.views import GraphQLView
from vbv_lernwelt.api.directory import list_entities
from vbv_lernwelt.api.user import (
get_cockpit_type,
get_profile,
me_user_view,
post_avatar,
)
from vbv_lernwelt.api.user import get_profile, me_user_view, post_avatar
from vbv_lernwelt.assignment.views import request_assignment_completion_status
from vbv_lernwelt.core.middleware.auth import django_view_authentication_exempt
from vbv_lernwelt.core.schema import schema
@ -136,9 +131,6 @@ urlpatterns = [
path(r"api/course/completion/<signed_int:course_session_id>/<uuid:user_id>/",
request_course_completion_for_user,
name="request_course_completion_for_user"),
path(r"api/course/<signed_int:course_id>/cockpit/",
get_cockpit_type,
name="get_cockpit_type"),
path("api/mentor/<signed_int:course_session_id>/", include("vbv_lernwelt.learning_mentor.urls")),

View File

@ -1,80 +0,0 @@
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from vbv_lernwelt.course.creators.test_utils import (
create_course,
create_course_session,
create_user,
)
from vbv_lernwelt.course.models import CourseSessionUser
from vbv_lernwelt.course_session_group.models import CourseSessionGroup
from vbv_lernwelt.learning_mentor.models import LearningMentor
class MeUserViewTest(APITestCase):
def setUp(self) -> None:
self.course, _ = create_course("Test Course")
self.user = create_user("tester")
self.url = reverse("get_cockpit_type", kwargs={"course_id": self.course.id})
def test_no_cockpit(self) -> None:
# GIVEN
self.client.force_login(self.user)
# WHEN
response = self.client.get(self.url)
# THEN
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEquals(response.data["type"], None)
def test_mentor_cockpit(self) -> None:
# GIVEN
self.client.force_login(self.user)
LearningMentor.objects.create(mentor=self.user, course=self.course)
# WHEN
response = self.client.get(self.url)
# THEN
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEquals(response.data["type"], "mentor")
def test_trainer_cockpit(self) -> None:
# GIVEN
self.client.force_login(self.user)
course_session = create_course_session(course=self.course, title="Test Session")
CourseSessionUser.objects.create(
user=self.user,
course_session=course_session,
role=CourseSessionUser.Role.EXPERT,
)
# WHEN
response = self.client.get(self.url)
# THEN
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEquals(response.data["type"], "expert")
def test_supervisor_cockpit(self):
# GIVEN
self.client.force_login(self.user)
course_session = create_course_session(course=self.course, title="Test Session")
csg = CourseSessionGroup.objects.create(
name="Test Group",
course=course_session.course,
)
csg.course_session.add(course_session)
csg.supervisor.add(self.user)
# WHEN
response = self.client.get(self.url)
# THEN
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEquals(response.data["type"], "expert")

View File

@ -5,10 +5,8 @@ from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from vbv_lernwelt.core.serializers import UserSerializer
from vbv_lernwelt.course.models import Course, CourseSessionUser
from vbv_lernwelt.course_session_group.models import CourseSessionGroup
from vbv_lernwelt.course.models import CourseSessionUser
from vbv_lernwelt.iam.permissions import can_view_profile
from vbv_lernwelt.learning_mentor.models import LearningMentor
from vbv_lernwelt.media_files.models import UserImage
@ -35,35 +33,6 @@ def me_user_view(request):
return Response(status=400)
@api_view(["GET"])
@permission_classes([IsAuthenticated])
def get_cockpit_type(request, course_id: int):
course = get_object_or_404(Course, id=course_id)
is_mentor = LearningMentor.objects.filter(
mentor=request.user, course=course
).exists()
is_expert = CourseSessionUser.objects.filter(
user=request.user,
course_session__course=course,
role=CourseSessionUser.Role.EXPERT,
).exists()
is_supervisor = CourseSessionGroup.objects.filter(
course_session__course=course, supervisor=request.user
).exists()
if is_mentor:
cockpit_type = "mentor"
elif is_expert or is_supervisor:
cockpit_type = "expert"
else:
cockpit_type = None
return Response({"type": cockpit_type})
@api_view(["GET"])
@permission_classes([IsAuthenticated])
def get_profile(request, course_session_id: int, user_id: str):

View File

@ -39,7 +39,52 @@ def has_course_session_access(user, course_session_id: int):
).exists()
def is_user_mentor(mentor: User, participant_user_id: str, course_session_id: int):
def has_course_session_preview(user, course_session_id: int):
if user.is_superuser:
return True
if is_course_session_member(user, course_session_id):
return False
return is_learning_mentor(user, course_session_id) or is_course_session_expert(
user, course_session_id
)
def has_expert_cockpit(user, course_session_id: int):
# FIXME: is_learning_mentor is just here WHILE we move cockpit -> learning-mentor THEN remove this!
return is_learning_mentor(user, course_session_id) or is_course_session_expert(
user, course_session_id
)
def has_media_library(user, course_session_id: int):
if user.is_superuser:
return True
if CourseSessionGroup.objects.filter(course_session=course_session_id).exists():
return True
return CourseSessionUser.objects.filter(
course_session=course_session_id,
user=user,
).exists()
def is_learning_mentor(mentor: User, course_session_id: int):
course_session = CourseSession.objects.get(id=course_session_id)
if course_session is None:
return False
return LearningMentor.objects.filter(
mentor=mentor, course_id=course_session.course_id
).exists()
def is_learning_mentor_for_user(
mentor: User, participant_user_id: str, course_session_id: int
):
csu = CourseSessionUser.objects.filter(
course_session_id=course_session_id, user_id=participant_user_id
).first()
@ -98,7 +143,7 @@ def can_evaluate_assignments(
role=CourseSessionUser.Role.EXPERT,
).exists()
is_mentor = is_user_mentor(
is_mentor = is_learning_mentor_for_user(
mentor=evaluation_user,
participant_user_id=assignment_user_id,
course_session_id=course_session_id,
@ -192,6 +237,16 @@ def can_view_course(user: User, course: Course) -> bool:
return False
def has_appointments(user: User, course_session_id: int) -> bool:
if user.is_superuser:
return True
if CourseSessionGroup.objects.filter(course_session=course_session_id).exists():
return True
return CourseSessionUser.objects.filter(course_session=course_session_id).exists()
def can_view_profile(user: User, profile_user: CourseSessionUser) -> bool:
if user.is_superuser:
return True
@ -199,7 +254,9 @@ def can_view_profile(user: User, profile_user: CourseSessionUser) -> bool:
if user == profile_user.user:
return True
if is_course_session_expert(user, profile_user.course_session.id) or is_user_mentor(
if is_course_session_expert(
user, profile_user.course_session.id
) or is_learning_mentor_for_user(
mentor=user,
participant_user_id=profile_user.user.id,
course_session_id=profile_user.course_session.id,
@ -215,7 +272,7 @@ def can_view_course_completions(
return (
str(user.id) == target_user_id
or is_course_session_expert(user=user, course_session_id=course_session_id)
or is_user_mentor(
or is_learning_mentor_for_user(
mentor=user,
participant_user_id=target_user_id,
course_session_id=course_session_id,
@ -232,6 +289,15 @@ def can_complete_learning_content(user: User, course_session_id: int) -> bool:
def course_session_permissions(user: User, course_session_id: int) -> list[str]:
return _action_list(
{
# FIXME: Just here WHILE we move cockpit -> learning-mentor THEN remove this!
"deprecated-mentor": is_course_session_member(user, course_session_id),
"learning-mentor": is_learning_mentor(user, course_session_id),
"preview": has_course_session_preview(user, course_session_id),
"media-library": has_media_library(user, course_session_id),
"appointments": has_appointments(user, course_session_id),
"expert-cockpit": has_expert_cockpit(user, course_session_id),
"learning-path": is_course_session_member(user, course_session_id),
"competence-navi": is_course_session_member(user, course_session_id),
"complete-learning-content": can_complete_learning_content(
user, course_session_id
),

View File

@ -8,7 +8,7 @@ from vbv_lernwelt.course.creators.test_utils import (
)
from vbv_lernwelt.course.models import CourseSessionUser
from vbv_lernwelt.course_session_group.models import CourseSessionGroup
from vbv_lernwelt.iam.permissions import has_role_in_course, is_user_mentor
from vbv_lernwelt.iam.permissions import has_role_in_course, is_learning_mentor_for_user
from vbv_lernwelt.learning_mentor.models import LearningMentor
@ -82,7 +82,7 @@ class RoleTestCase(TestCase):
learning_mentor.save()
# WHEN
is_mentor = is_user_mentor(
is_mentor = is_learning_mentor_for_user(
mentor=mentor,
participant_user_id=member.id,
course_session_id=course_session.id,
@ -106,7 +106,7 @@ class RoleTestCase(TestCase):
)
# WHEN
is_mentor = is_user_mentor(
is_mentor = is_learning_mentor_for_user(
mentor=wanna_be_mentor,
participant_user_id=member.id,
course_session_id=course_session.id,