feat: cockpit type / navigation
TODO dashboard -> cockpit/mentor (temporary) TODO dashboard -> cockpit/expert
This commit is contained in:
parent
d9cb334404
commit
742801bf22
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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,
|
||||
};
|
||||
});
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")),
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue