Merged in feat/geteilter-bereich (pull request #304)

Feat: Teilnehmer kann auch Mentor sein

Approved-by: Daniel Egger
This commit is contained in:
Livio Bieri 2024-03-28 07:27:15 +00:00 committed by Christian Cueni
commit 24aa5d9b8c
104 changed files with 1688 additions and 1338 deletions

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import EvaluationIntro from "@/pages/cockpit/assignmentEvaluationPage/EvaluationIntro.vue";
import EvaluationSummary from "@/pages/cockpit/assignmentEvaluationPage/EvaluationSummary.vue";
import EvaluationTask from "@/pages/cockpit/assignmentEvaluationPage/EvaluationTask.vue";
import EvaluationIntro from "@/components/assignment/evaluation/EvaluationIntro.vue";
import EvaluationSummary from "@/components/assignment/evaluation/EvaluationSummary.vue";
import EvaluationTask from "@/components/assignment/evaluation/EvaluationTask.vue";
import type {
Assignment,
AssignmentCompletion,

View File

@ -30,12 +30,10 @@ const courseSession = useCurrentCourseSession();
const task = computed(() => props.assignment.evaluation_tasks[props.taskIndex]);
const expertData = computed(() => {
const data = (props.assignmentCompletion?.completion_data?.[task.value.id]
?.expert_data ?? {
return (props.assignmentCompletion?.completion_data?.[task.value.id]?.expert_data ?? {
points: 0,
text: "",
}) as ExpertData;
return data;
});
const text = computed(() => {

View File

@ -1,6 +1,6 @@
2
<script setup lang="ts">
import AssignmentSubmissionProgress from "@/components/cockpit/AssignmentSubmissionProgress.vue";
import AssignmentSubmissionProgress from "@/components/assignment/AssignmentSubmissionProgress.vue";
import type {
CourseSession,
LearningContent,
@ -10,7 +10,7 @@ import type {
import log from "loglevel";
import { computed } from "vue";
import { useTranslation } from "i18next-vue";
import FeedbackSubmissionProgress from "@/components/cockpit/mentor/FeedbackSubmissionProgress.vue";
import FeedbackSubmissionProgress from "@/components/selfEvaluationFeedback/FeedbackSubmissionProgress.vue";
import { learningContentTypeData } from "@/utils/typeMaps";
import {
useCourseDataWithCompletion,

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

@ -18,11 +18,10 @@ import CoursePreviewBar from "@/components/header/CoursePreviewBar.vue";
import {
getCockpitUrl,
getCompetenceNaviUrl,
getLearningMentorManagementUrl,
getLearningMentorUrl,
getLearningPathUrl,
getMediaCenterUrl,
} from "@/utils/utils";
import { useCockpitStore } from "@/stores/cockpit";
log.debug("MainNavigationBar created");
@ -70,47 +69,62 @@ 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;
return courseSession.actions.includes("learning-mentor");
});
</script>
<template>
<CoursePreviewBar v-if="courseSessionsStore.hasCourseSessionPreview" />
<CoursePreviewBar v-if="courseSessionsStore.isCourseSessionPreviewActive" />
<div v-else>
<Teleport to="body">
<MobileMenu
@ -120,6 +134,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 +196,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="
getLearningMentorUrl(
courseSessionsStore.currentCourseSession.course.slug
)
"
class="nav-item"
:class="{ 'nav-item--active': inLearningMentor() }"
>
{{ t("a.Lernbegleitung") }}
</router-link>
</div>
</template>
</div>

View File

@ -1,12 +1,12 @@
<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";
import {
getCockpitUrl,
getCompetenceNaviUrl,
getLearningMentorUrl,
getLearningPathUrl,
getMediaCenterUrl,
} from "@/utils/utils";
@ -18,6 +18,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 +36,6 @@ const clickLink = (to: string | undefined) => {
emit("closemodal");
}
};
const courseSessionsStore = useCourseSessionsStore();
</script>
<template>
@ -57,42 +60,47 @@ 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="hasLearningMentor" class="mb-6">
<button
data-cy="navigation-mobile-mentor-link"
@click="clickLink(getLearningMentorUrl(courseSession.course.slug))"
>
{{ $t("a.Lernbegleitung") }}
</button>
</li>
<li v-if="hasMediaLibraryMenu" class="mb-6">
<button
data-cy="medialibrary-link"

View File

@ -0,0 +1,77 @@
<script setup lang="ts">
import { useLearningMentees } from "@/services/learningMentees";
import { useCurrentCourseSession } from "@/composables";
import { useCSRFFetch } from "@/fetchHelpers";
const courseSession = useCurrentCourseSession();
const { summary, fetchData } = useLearningMentees(courseSession.value.id);
const removeMyMentee = async (menteeId: string) => {
await useCSRFFetch(
`/api/mentor/${courseSession.value.id}/mentors/${summary.value?.mentor_id}/remove/${menteeId}`
).delete();
fetchData();
};
</script>
<template>
<div v-if="summary">
<template v-if="summary.participants.length > 0">
<h2 class="heading-2 py-6">{{ $t("a.Personen, die du begleitest") }}</h2>
<div class="bg-white px-4 py-2">
<div
v-for="participant in summary.participants"
:key="participant.id"
data-cy="lm-my-mentee-list-item"
class="flex flex-col items-start justify-between gap-4 border-b py-2 last:border-b-0 md:flex-row md:items-center md:gap-16"
>
<div class="flex items-center space-x-2">
<img
:alt="participant.last_name"
class="h-11 w-11 rounded-full"
:src="
participant.avatar_url || '/static/avatars/myvbv-default-avatar.png'
"
/>
<div>
<div class="text-bold">
{{ participant.first_name }}
{{ participant.last_name }}
</div>
{{ participant.email }}
</div>
</div>
<div class="space-x-5">
<router-link
data-cy="lm-my-mentee-profile"
:to="{
name: 'profileLearningPath',
params: {
userId: participant.id,
courseSlug: courseSession.course.slug,
},
}"
class="underline"
>
{{ $t("cockpit.profileLink") }}
</router-link>
<button
class="underline"
data-cy="lm-my-mentee-remove"
@click="removeMyMentee(participant.id)"
>
{{ $t("a.Entfernen") }}
</button>
</div>
</div>
</div>
</template>
<div v-else>
<h2 class="heading-2 py-6">{{ $t("a.Personen, die du begleitest") }}</h2>
<div class="flex items-center bg-white px-4 py-2">
<it-icon-info class="it-icon mr-2 h-6 w-6" />
{{ $t("a.Aktuell begleitest du niemanden als Lernbegleitung.") }}
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,172 @@
<script setup lang="ts">
import { useCurrentCourseSession } from "@/composables";
import ItModal from "@/components/ui/ItModal.vue";
import { computed, ref } from "vue";
import { useCSRFFetch } from "@/fetchHelpers";
import { useUserStore } from "@/stores/user";
const courseSession = useCurrentCourseSession();
const showInvitationModal = ref(false);
const inviteeEmail = ref("");
const {
execute: refreshMentors,
data: mentors,
isFetching: isFetchingMentors,
} = useCSRFFetch(`/api/mentor/${courseSession.value.id}/mentors`).json();
const {
execute: refreshInvitations,
data: invitations,
isFetching: isFetchingInvitations,
} = useCSRFFetch(`/api/mentor/${courseSession.value.id}/invitations`).json();
const isLoading = computed(
() => isFetchingMentors.value || isFetchingInvitations.value
);
const hasMentors = computed(() => {
return (
(mentors.value && mentors.value.length > 0) ||
(invitations.value && invitations.value.length > 0)
);
});
const validEmail = computed(() => {
const isSelfInvitation = Boolean(inviteeEmail.value === useUserStore().email);
if (isSelfInvitation) {
return false;
}
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
return emailRegex.test(inviteeEmail.value);
});
const removeInvitation = async (invitationId: string) => {
await useCSRFFetch(
`/api/mentor/${courseSession.value.id}/invitations/${invitationId}/delete`
).delete();
await refreshInvitations();
};
const userStore = useUserStore();
const removeMyMentor = async (mentorId: string) => {
await useCSRFFetch(
`/api/mentor/${courseSession.value.id}/mentors/${mentorId}/remove/${userStore.id}`
).delete();
await refreshMentors();
};
const inviteMentor = async () => {
await useCSRFFetch(`/api/mentor/${courseSession.value.id}/invitations/create`).post({
email: inviteeEmail.value,
});
await refreshInvitations();
showInvitationModal.value = false;
inviteeEmail.value = "";
};
</script>
<template>
<div v-if="!isLoading" class="bg-gray-200">
<div class="flex flex-row items-center justify-between py-6">
<h2 class="heading-2">{{ $t("a.Meine Lernbegleitung") }}</h2>
<div>
<button
class="btn-secondary flex items-center"
@click="showInvitationModal = true"
>
<it-icon-add class="it-icon mr-2 h-6 w-6" />
{{ $t("a.Neue Lernbegleitung einladen") }}
</button>
</div>
</div>
<div class="bg-white px-4 py-2">
<main>
<div>
<div
v-for="invitation in invitations"
:key="invitation.id"
class="flex flex-col justify-between gap-4 border-b py-2 last:border-b-0 md:flex-row md:gap-16"
>
<div class="flex flex-col md:flex-grow md:flex-row">
<div class="flex items-center space-x-2">
<img
:alt="invitation.email"
class="h-11 w-11 rounded-full"
:src="'/static/avatars/myvbv-default-avatar.png'"
/>
<span class="text-bold">{{ invitation.email }}</span>
</div>
<div class="flex items-center pl-8">
<it-icon-info class="it-icon mr-1 h-6 w-6" />
{{ $t("a.Die Einladung wurde noch nicht angenommen.") }}
</div>
</div>
<button class="underline" @click="removeInvitation(invitation.id)">
{{ $t("a.Entfernen") }}
</button>
</div>
<div
v-for="learningMentor in mentors"
:key="learningMentor.id"
data-cy="lm-my-mentor-list-item"
class="flex flex-col justify-between gap-4 border-b py-2 last:border-b-0 md:flex-row md:gap-16"
>
<div class="flex items-center space-x-2">
<img
:alt="learningMentor.mentor.last_name"
class="h-11 w-11 rounded-full"
:src="learningMentor.mentor.avatar_url"
/>
<div>
<div class="text-bold">
{{ learningMentor.mentor.first_name }}
{{ learningMentor.mentor.last_name }}
</div>
{{ learningMentor.mentor.email }}
</div>
</div>
<button
class="underline"
data-cy="lm-my-mentor-remove"
@click="removeMyMentor(learningMentor.id)"
>
{{ $t("a.Entfernen") }}
</button>
</div>
</div>
<div v-if="!hasMentors">
<div class="j mx-1 my-3 flex w-fit items-center space-x-1 bg-sky-200 p-4">
<it-icon-info class="it-icon mr-2 h-6 w-6 text-sky-700" />
<span>
{{
$t("a.Aktuell hast du noch keine Person als Lernbegleitung eingeladen.")
}}
</span>
</div>
</div>
</main>
</div>
<ItModal v-model="showInvitationModal">
<template #title>{{ $t("a.Neue Lernbegleitung einladen") }}</template>
<template #body>
<div class="flex flex-col">
<label for="mentor-email">{{ $t("a.E-Mail Adresse") }}</label>
<input id="mentor-email" v-model="inviteeEmail" type="email" />
<button
:disabled="!validEmail"
class="btn-primary mt-8"
@click="inviteMentor()"
>
{{ $t("a.Einladung abschicken") }}
</button>
</div>
</template>
</ItModal>
</div>
</template>

View File

@ -17,7 +17,7 @@ const currentCourseSession = useCurrentCourseSession();
</div>
<router-link
:to="{
name: 'learningMentorManagement',
name: 'mentorsAndParticipants',
params: { courseSlug: currentCourseSession.course.slug },
}"
class="btn-blue px-4 py-2 font-bold"

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import AssignmentItem from "@/components/cockpit/mentor/AssignmentItem.vue";
import AssignmentItem from "@/components/learningMentor/AssignmentItem.vue";
import type { RouteLocationRaw } from "vue-router";
defineProps<{
@ -16,8 +16,8 @@ defineProps<{
:circle-title="circleTitle"
:pending-tasks="pendingTasks"
:task-link="taskLink"
:pending-tasks-label="$t('a.Ergebnisse abgegeben')"
:task-link-pending-label="$t('a.Ergebnisse bewerten')"
:pending-tasks-label="$t('a.Feedback freigegeben')"
:task-link-pending-label="$t('a.Feedback geben')"
:task-link-label="$t('a.Praxisaufträge anschauen')"
/>
</template>

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import AssignmentItem from "@/components/cockpit/mentor/AssignmentItem.vue";
import AssignmentItem from "@/components/learningMentor/AssignmentItem.vue";
import type { RouteLocationRaw } from "vue-router";
defineProps<{

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

@ -12,8 +12,8 @@ import { useMutation } from "@urql/vue";
import { UPSERT_ASSIGNMENT_COMPLETION_MUTATION } from "@/graphql/mutations";
import type { Assignment } from "@/types";
import DateEmbedding from "@/components/dueDates/DateEmbedding.vue";
import { useLearningMentors } from "@/composables";
import NoMentorInformationPanel from "@/components/mentor/NoMentorInformationPanel.vue";
import { useMyLearningMentors } from "@/composables";
import NoMentorInformationPanel from "@/components/learningMentor/NoMentorInformationPanel.vue";
import SampleSolution from "@/components/assignment/SampleSolution.vue";
const props = defineProps<{
@ -29,7 +29,7 @@ const upsertAssignmentCompletionMutation = useMutation(
UPSERT_ASSIGNMENT_COMPLETION_MUTATION
);
const learningMentors = useLearningMentors().learningMentors;
const learningMentors = useMyLearningMentors().learningMentors;
const selectedLearningMentor = ref();
const onSubmit = async () => {

View File

@ -466,7 +466,7 @@ export function useFileUpload() {
return { upload, error, loading, fileInfo };
}
export function useLearningMentors() {
export function useMyLearningMentors() {
const learningMentors = ref<LearningMentor[]>([]);
const currentCourseSessionId = useCurrentCourseSession().value.id;
const loading = ref(false);

View File

@ -464,6 +464,7 @@ export type DashboardConfigType = {
};
export type DashboardType =
| 'MENTOR_DASHBOARD'
| 'PROGRESS_DASHBOARD'
| 'SIMPLE_DASHBOARD'
| 'STATISTICS_DASHBOARD';

View File

@ -207,6 +207,7 @@ enum DashboardType {
STATISTICS_DASHBOARD
PROGRESS_DASHBOARD
SIMPLE_DASHBOARD
MENTOR_DASHBOARD
}
type CourseConfigurationObjectType {

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import { useCurrentCourseSession } from "@/composables";
import { ASSIGNMENT_COMPLETION_QUERY } from "@/graphql/queries";
import EvaluationContainer from "@/pages/cockpit/assignmentEvaluationPage/EvaluationContainer.vue";
import EvaluationContainer from "@/components/assignment/evaluation/EvaluationContainer.vue";
import AssignmentSubmissionResponses from "@/pages/learningPath/learningContentPage/assignment/AssignmentSubmissionResponses.vue";
import type { Assignment, AssignmentCompletion, CourseSessionUser } from "@/types";
import { useQuery } from "@urql/vue";
@ -9,7 +9,7 @@ import log from "loglevel";
import { computed, onMounted } from "vue";
import { useRouter } from "vue-router";
import { getPreviousRoute } from "@/router/history";
import { getAssignmentTypeTitle } from "../../../utils/utils";
import { getAssignmentTypeTitle } from "../../utils/utils";
const props = defineProps<{
courseSlug: string;
@ -42,9 +42,15 @@ function close() {
if (previousRoute) {
router.push(previousRoute);
} else {
router.push({
path: `/course/${props.courseSlug}/cockpit`,
});
if (assignment.value?.assignment_type === "PRAXIS_ASSIGNMENT") {
router.push({
path: `/course/${props.courseSlug}/learning-mentor`,
});
} else {
router.push({
path: `/course/${props.courseSlug}/cockpit`,
});
}
}
}

View File

@ -11,7 +11,7 @@ import type {
} from "@/types";
import log from "loglevel";
import { computed, onMounted, reactive } from "vue";
import AssignmentSubmissionProgress from "@/components/cockpit/AssignmentSubmissionProgress.vue";
import AssignmentSubmissionProgress from "@/components/assignment/AssignmentSubmissionProgress.vue";
import { useCourseSessionDetailQuery } from "@/composables";
import { formatDueDate } from "../../../components/dueDates/dueDatesUtils";
import { stringifyParse } from "@/utils/utils";
@ -171,7 +171,7 @@ function findUserPointsHtml(userId: string) {
props.learningContent.content_type !==
'learnpath.LearningContentEdoniqTest'
"
:to="`/course/${props.courseSession.course.slug}/cockpit/assignment/${learningContent.content_assignment.id}/${csu.user_id}`"
:to="`/course/${props.courseSession.course.slug}/assignment-evaluation/${learningContent.content_assignment.id}/${csu.user_id}`"
class="link lg:w-full lg:text-right"
data-cy="show-results"
>

View File

@ -148,7 +148,10 @@ const courseSessionDetailResult = useCourseSessionDetailQuery();
</template>
<template #link>
<router-link
:to="`/course/${props.courseSlug}/cockpit/profile/${csu.user_id}`"
:to="{
name: 'profileLearningPath',
params: { userId: csu.user_id, courseSlug: props.courseSlug },
}"
class="link w-full lg:text-right"
>
{{ $t("general.profileLink") }}

View File

@ -1,38 +0,0 @@
<script setup lang="ts">
import { useRoute } from "vue-router";
const route = useRoute();
</script>
<template>
<div class="bg-gray-200">
<nav class="border-b bg-white px-4 lg:px-8">
<ul class="flex flex-col lg:flex-row">
<li
class="border-t-2 border-t-transparent"
:class="{
'border-b-2 border-b-blue-900':
route.name === 'mentorCockpitOverview' || route.name === 'mentorCockpit',
}"
>
<router-link :to="{ name: 'mentorCockpitOverview' }" class="block py-3">
{{ $t("a.Übersicht") }}
</router-link>
</li>
<li
class="border-t-2 border-t-transparent lg:ml-12"
:class="{
'border-b-2 border-b-blue-900': route.name === 'mentorCockpitParticipants',
}"
>
<router-link :to="{ name: 'mentorCockpitParticipants' }" class="block py-3">
{{ $t("a.Teilnehmer") }}
</router-link>
</li>
</ul>
</nav>
<main class="container-large">
<router-view></router-view>
</main>
</div>
</template>

View File

@ -1,96 +0,0 @@
<script setup lang="ts">
import type { Assignment } from "@/services/mentorCockpit";
import { useMentorCockpit } from "@/services/mentorCockpit";
import { useCurrentCourseSession } from "@/composables";
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
import { computed, type Ref, ref } from "vue";
import PraxisAssignmentItem from "@/components/cockpit/mentor/PraxisAssignmentItem.vue";
import { useTranslation } from "i18next-vue";
import SelfAssignmentFeedbackAssignmentItem from "@/components/cockpit/mentor/SelfAssignmentFeedbackAssignmentItem.vue";
const { t } = useTranslation();
const courseSession = useCurrentCourseSession();
const mentorCockpitStore = useMentorCockpit(courseSession.value.id);
const summary = mentorCockpitStore.summary;
const statusFilterValue = ref({ name: t("Alle"), id: "_all" });
const statusFilter = ref([
{ name: t("Alle"), id: "_all" },
{ name: t("a.Zu erledigen"), id: "todo" },
]);
const circleFilterValue = ref({ name: t("a.AlleCircle"), id: "_all" });
const circleFilter = computed(() => {
if (!summary.value) return [];
return [
{ name: t("a.AlleCircle"), id: "_all" },
...summary.value.circles.map((circle) => ({
name: `Circle: ${circle.title}`,
id: circle.id,
})),
];
});
const filteredAssignments: Ref<Assignment[]> = computed(() => {
if (!summary.value) return [];
let filtered = summary.value.assignments;
if (statusFilterValue.value.id !== "_all") {
filtered = filtered.filter((item) => item.pending_evaluations > 0);
}
if (circleFilterValue.value.id !== "_all") {
filtered = filtered.filter(
(item) => item.circle_id === String(circleFilterValue.value.id)
);
}
return filtered;
});
</script>
<template>
<div v-if="summary" class="bg-white">
<div class="flex flex-col space-x-2 lg:flex-row">
<ItDropdownSelect
v-model="statusFilterValue"
class="min-w-[10rem]"
:items="statusFilter"
borderless
></ItDropdownSelect>
<ItDropdownSelect
v-model="circleFilterValue"
class="min-w-[18rem]"
:items="circleFilter"
borderless
></ItDropdownSelect>
</div>
<template v-for="item in filteredAssignments" :key="item.id">
<PraxisAssignmentItem
v-if="item.type === 'praxis_assignment'"
:circle-title="mentorCockpitStore.getCircleTitleById(item.circle_id)"
:pending-tasks="item.pending_evaluations"
:task-link="{
name: 'mentorCockpitPraxisAssignments',
params: { praxisAssignmentId: item.id },
}"
:task-title="item.title"
/>
<SelfAssignmentFeedbackAssignmentItem
v-else-if="item.type === 'self_evaluation_feedback'"
:circle-title="mentorCockpitStore.getCircleTitleById(item.circle_id)"
:pending-tasks="item.pending_evaluations"
:task-link="{
name: 'mentorCockpitSelfEvaluationFeedbackAssignments',
params: { learningUnitId: item.id },
}"
:task-title="item.title"
/>
</template>
</div>
</template>

View File

@ -1,38 +0,0 @@
<script setup lang="ts">
import { useMentorCockpit } from "@/services/mentorCockpit";
import { useCurrentCourseSession } from "@/composables";
const courseSession = useCurrentCourseSession();
const { summary } = useMentorCockpit(courseSession.value.id);
</script>
<template>
<div v-if="summary" class="bg-white px-4 py-2">
<div
v-for="participant in summary.participants"
:key="participant.id"
class="flex flex-col items-start justify-between gap-4 border-b py-2 last:border-b-0 md:flex-row md:items-center md:gap-16"
>
<div class="flex items-center space-x-2">
<img
:alt="participant.last_name"
class="h-11 w-11 rounded-full"
:src="participant.avatar_url || '/static/avatars/myvbv-default-avatar.png'"
/>
<div>
<div class="text-bold">
{{ participant.first_name }}
{{ participant.last_name }}
</div>
{{ participant.email }}
</div>
</div>
<router-link
:to="{ name: 'cockpitUserProfile', params: { userId: participant.id } }"
class="underline"
>
{{ $t("cockpit.profileLink") }}
</router-link>
</div>
</div>
</template>

View File

@ -1,11 +0,0 @@
<script setup lang="ts"></script>
<template>
<div class="bg-gray-200">
<main>
<div>
<router-view></router-view>
</div>
</main>
</div>
</template>

View File

@ -11,6 +11,7 @@ import SimpleCoursePage from "@/pages/dashboard/SimpleCoursePage.vue";
import LoadingSpinner from "@/components/ui/LoadingSpinner.vue";
import CourseDetailDates from "@/components/dashboard/CourseDetailDates.vue";
import NoCourseSession from "@/components/dashboard/NoCourseSession.vue";
import MentorPage from "@/pages/dashboard/MentorPage.vue";
const dashboardStore = useDashboardStore();
@ -23,6 +24,7 @@ const boards: Record<DashboardType, DashboardPage> = {
PROGRESS_DASHBOARD: { main: ProgressPage, aside: SimpleDates },
SIMPLE_DASHBOARD: { main: SimpleCoursePage, aside: SimpleDates },
STATISTICS_DASHBOARD: { main: StatisticPage, aside: CourseDetailDates },
MENTOR_DASHBOARD: { main: MentorPage, aside: SimpleDates },
};
onMounted(dashboardStore.loadDashboardDetails);

View File

@ -0,0 +1,25 @@
<script setup lang="ts">
import { getLearningMentorUrl } from "@/utils/utils";
import { useDashboardStore } from "@/stores/dashboard";
const dashboardStore = useDashboardStore();
</script>
<template>
<div v-if="dashboardStore.currentDashboardConfig">
<div class="mb-14">
<div class="h-full bg-white p-6">
<h3 class="mb-4">{{ dashboardStore.currentDashboardConfig.name }}</h3>
<div>
<router-link
class="btn-blue"
data-cy="lm-dashboard-link"
:to="getLearningMentorUrl(dashboardStore.currentDashboardConfig.slug)"
>
{{ $t("a.Übersicht anschauen") }}
</router-link>
</div>
</div>
</div>
</div>
</template>

View File

@ -74,7 +74,7 @@ const showCompetenceCertificates = computed(() => {
<router-link
class="btn-blue"
:to="getLearningPathUrl(dashboardStore.currentDashboardConfig.slug)"
:data-cy="`continue-course-${dashboardStore.currentDashboardConfig.id}`"
data-cy="progress-dashboard-continue-course-link"
>
{{ $t("general.nextStep") }}
</router-link>

View File

@ -14,7 +14,6 @@ const dashboardStore = useDashboardStore();
<router-link
class="btn-blue"
:to="getCockpitUrl(dashboardStore.currentDashboardConfig.slug)"
:data-cy="`continue-course-${dashboardStore.currentDashboardConfig.id}`"
>
{{ $t("a.Cockpit anschauen") }}
</router-link>

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import { useCSRFFetch } from "@/fetchHelpers";
import { getCockpitUrl } from "@/utils/utils";
import { getLearningMentorUrl } from "@/utils/utils";
const props = defineProps<{
courseId: string;
@ -56,8 +56,8 @@ const { data, error } = useCSRFFetch(
</template>
</i18next>
<div class="mt-4">
<a class="underline" :href="getCockpitUrl(data.course_slug)">
{{ $t("a.Cockpit anschauen") }}
<a class="underline" :href="getLearningMentorUrl(data.course_slug)">
{{ $t("a.Übersicht anschauen") }}
</a>
</div>
</template>

View File

@ -1,148 +0,0 @@
<script setup lang="ts">
import { useCurrentCourseSession } from "@/composables";
import ItModal from "@/components/ui/ItModal.vue";
import { computed, ref } from "vue";
import { useCSRFFetch } from "@/fetchHelpers";
const courseSession = useCurrentCourseSession();
const showInvitationModal = ref(false);
const inviteeEmail = ref("");
const { execute: refreshMentors, data: mentors } = useCSRFFetch(
`/api/mentor/${courseSession.value.id}/mentors`
).json();
const { execute: refreshInvitations, data: invitations } = useCSRFFetch(
`/api/mentor/${courseSession.value.id}/invitations`
).json();
const hasMentors = computed(() => {
return (
(mentors.value && mentors.value.length > 0) ||
(invitations.value && invitations.value.length > 0)
);
});
const validEmail = computed(() => {
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
return emailRegex.test(inviteeEmail.value);
});
const removeInvitation = async (invitationId: string) => {
await useCSRFFetch(
`/api/mentor/${courseSession.value.id}/invitations/${invitationId}/delete`
).delete();
await refreshInvitations();
};
const removeMentor = async (mentorId: string) => {
await useCSRFFetch(
`/api/mentor/${courseSession.value.id}/mentors/${mentorId}/leave`
).delete();
await refreshMentors();
};
const inviteMentor = async () => {
await useCSRFFetch(`/api/mentor/${courseSession.value.id}/invitations/create`).post({
email: inviteeEmail.value,
});
await refreshInvitations();
showInvitationModal.value = false;
inviteeEmail.value = "";
};
</script>
<template>
<div class="bg-gray-200">
<div class="container-large">
<header class="mb-8 mt-12">
<h1 class="mb-8">{{ $t("a.Lernbegleitung") }}</h1>
<p>
{{
$t(
"a.Hier kannst du Personen einladen, damit sie deine Lernbegleitung werden. Zudem siehst du jederzeit eine Übersicht aller Personen, die du bereits als Lernbegleitung hinzugefügt hast."
)
}}
</p>
</header>
<main>
<div class="bg-white p-6">
<div class="mb-8">
<button
class="btn-secondary flex items-center"
@click="showInvitationModal = true"
>
<it-icon-add class="it-icon mr-2 h-6 w-6" />
{{ $t("a.Neue Lernbegleitung einladen") }}
</button>
</div>
<div class="border-t">
<div
v-for="invitation in invitations"
:key="invitation.id"
class="flex flex-col justify-between gap-4 border-b py-4 md:flex-row md:gap-16"
>
<div class="flex flex-col justify-between md:flex-grow md:flex-row">
{{ invitation.email }}
<div class="flex items-center">
<it-icon-info class="it-icon mr-2 h-6 w-6" />
{{ $t("a.Die Einladung wurde noch nicht angenommen.") }}
</div>
</div>
<button class="underline" @click="removeInvitation(invitation.id)">
{{ $t("a.Entfernen") }}
</button>
</div>
<div
v-for="learningMentor in mentors"
:key="learningMentor.id"
class="flex flex-col justify-between gap-4 border-b py-4 md:flex-row md:gap-16"
>
<div class="flex items-center space-x-2">
<img
:alt="learningMentor.mentor.last_name"
class="h-11 w-11 rounded-full"
:src="learningMentor.mentor.avatar_url"
/>
<div>
<div class="text-bold">
{{ learningMentor.mentor.first_name }}
{{ learningMentor.mentor.last_name }}
</div>
{{ learningMentor.mentor.email }}
</div>
</div>
<button class="underline" @click="removeMentor(learningMentor.id)">
{{ $t("a.Entfernen") }}
</button>
</div>
</div>
<div v-if="!hasMentors" class="mt-8 flex items-center">
<it-icon-info class="it-icon mr-2 h-6 w-6" />
{{
$t("a.Aktuell hast du noch keine Person als Lernbegleitung eingeladen.")
}}
</div>
</div>
</main>
</div>
<ItModal v-model="showInvitationModal">
<template #title>{{ $t("a.Neue Lernbegleitung einladen") }}</template>
<template #body>
<div class="flex flex-col">
<label for="mentor-email">{{ $t("a.E-Mail Adresse") }}</label>
<input id="mentor-email" v-model="inviteeEmail" type="email" />
<button
:disabled="!validEmail"
class="btn-primary mt-8"
@click="inviteMentor()"
>
{{ $t("a.Einladung abschicken") }}
</button>
</div>
</template>
</ItModal>
</div>
</template>

View File

@ -2,7 +2,7 @@
<template>
<router-link
:to="{ name: 'mentorCockpitOverview' }"
:to="{ name: 'learningMentorOverview' }"
class="btn-text mb-4 inline-flex items-center pl-0"
>
<it-icon-arrow-left class="it-icon"></it-icon-arrow-left>

View File

@ -0,0 +1,65 @@
<script setup lang="ts">
import { useRoute, useRouter } from "vue-router";
import { useCurrentCourseSession } from "@/composables";
import { onMounted } from "vue";
const route = useRoute();
const router = useRouter();
const courseSession = useCurrentCourseSession();
// if just a course session member -> hide navigation
// and automatically redirect to mentorsAndParticipants
const isMentoring = courseSession.value.actions.includes(
"learning-mentor::guide-members"
);
onMounted(() => {
if (!isMentoring) {
router.push({ name: "mentorsAndParticipants" });
}
});
</script>
<template>
<div class="bg-gray-200">
<nav
v-if="isMentoring"
class="border-b bg-white px-4 lg:px-8"
data-cy="lm-main-navigation"
>
<ul class="flex flex-col lg:flex-row">
<li
class="border-t-2 border-t-transparent"
:class="{
'border-b-2 border-b-blue-900': route.name
?.toString()
.startsWith('learningMentor'),
}"
>
<router-link
data-cy="lm-overview-navigation-link"
:to="{ name: 'learningMentorOverview' }"
class="block py-3"
>
{{ $t("a.Übersicht") }}
</router-link>
</li>
<li
data-cy="lm-mentees-navigation-link"
class="border-t-2 border-t-transparent lg:ml-12"
:class="{
'border-b-2 border-b-blue-900': route.name === 'mentorsAndParticipants',
}"
>
<router-link :to="{ name: 'mentorsAndParticipants' }" class="block py-3">
{{ $t("a.Personen") }}
</router-link>
</li>
</ul>
</nav>
<main class="container-large">
<router-view></router-view>
</main>
</div>
</template>

View File

@ -0,0 +1,59 @@
<script setup lang="ts">
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
import PraxisAssignmentItem from "@/components/learningMentor/PraxisAssignmentItem.vue";
import SelfAssignmentFeedbackAssignmentItem from "@/components/learningMentor/SelfAssignmentFeedbackAssignmentItem.vue";
import { useAssignmentTodoListStore } from "@/stores/learningMentor/assignmentTodoList";
const assignmentTodoListStore = useAssignmentTodoListStore();
</script>
<template>
<h2 class="heading-2 mb-6 mt-6">{{ $t("a.Das wurde mit dir geteilt") }}</h2>
<div class="flex flex-col bg-white pb-5 pt-1">
<div class="flex flex-col lg:flex-row lg:space-x-2">
<ItDropdownSelect
v-model="assignmentTodoListStore.selectedStatus"
class="min-w-[10rem]"
:items="assignmentTodoListStore.statusOptions"
borderless
></ItDropdownSelect>
<ItDropdownSelect
v-model="assignmentTodoListStore.selectedCircle"
class="min-w-[18rem]"
:items="assignmentTodoListStore.circleOptions"
borderless
></ItDropdownSelect>
</div>
<template v-if="assignmentTodoListStore.filteredAssignments.length > 0">
<template
v-for="item in assignmentTodoListStore.filteredAssignments"
:key="item.id"
>
<PraxisAssignmentItem
v-if="item.type === 'praxis_assignment'"
:circle-title="item.circle_name"
:pending-tasks="item.pending_evaluations"
:task-link="{
name: 'learningMentorPraxisAssignments',
params: { praxisAssignmentId: item.id },
}"
:task-title="item.title"
/>
<SelfAssignmentFeedbackAssignmentItem
v-else-if="item.type === 'self_evaluation_feedback'"
:circle-title="item.circle_name"
:pending-tasks="item.pending_evaluations"
:task-link="{
name: 'learningMentorSelfEvaluationFeedbackAssignments',
params: { learningUnitId: item.id },
}"
:task-title="item.title"
/>
</template>
</template>
<div v-else class="mx-5 flex flex-row items-center bg-green-200 pl-2">
<it-icon-check class="it-icon"></it-icon-check>
<p class="py-4 text-base">{{ $t("a.Du hast alles erledigt.") }}</p>
</div>
</div>
</template>

View File

@ -0,0 +1,29 @@
<script setup lang="ts">
import { useCurrentCourseSession } from "@/composables";
import MyMentors from "@/components/learningMentor/MyMentors.vue";
import MyMentees from "@/components/learningMentor/MyMentees.vue";
import { computed } from "vue";
const courseSession = useCurrentCourseSession();
const isMyMentorsVisible = computed(() =>
courseSession.value.actions.includes("learning-mentor::edit-mentors")
);
const isMyMenteesVisible = computed(() =>
courseSession.value.actions.includes("learning-mentor::guide-members")
);
</script>
<template>
<div class="container-large space-y-20">
<template v-if="isMyMenteesVisible || isMyMentorsVisible">
<div v-if="isMyMentorsVisible">
<MyMentors data-cy="lm-my-mentors" />
</div>
<div v-if="isMyMenteesVisible">
<MyMentees data-cy="lm-my-mentees" />
</div>
</template>
</div>
</template>

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import type { Assignment, Participant } from "@/services/mentorCockpit";
import { useMentorCockpit } from "@/services/mentorCockpit";
import type { Assignment, Participant } from "@/services/learningMentees";
import { useLearningMentees } from "@/services/learningMentees";
import { computed, onMounted, type Ref } from "vue";
import { useCurrentCourseSession } from "@/composables";
import log from "loglevel";
@ -10,10 +10,10 @@ const props = defineProps<{
}>();
const courseSession = useCurrentCourseSession();
const mentorCockpitStore = useMentorCockpit(courseSession.value.id);
const participants = computed(() => mentorCockpitStore.summary.value?.participants);
const learningMentees = useLearningMentees(courseSession.value.id);
const participants = computed(() => learningMentees.summary.value?.participants);
const praxisAssignment: Ref<Assignment | null> = computed(() =>
mentorCockpitStore.getAssignmentById(props.praxisAssignmentId)
learningMentees.getAssignmentById(props.praxisAssignmentId)
);
const getParticipantById = (id: string): Participant | null => {
@ -22,7 +22,7 @@ const getParticipantById = (id: string): Participant | null => {
onMounted(() => {
log.debug("MentorPraxisAssignment mounted");
mentorCockpitStore.fetchData();
learningMentees.fetchData();
});
</script>
@ -30,9 +30,7 @@ onMounted(() => {
<div v-if="praxisAssignment">
<div class="p-6">
<h2 class="mb-2">{{ $t("a.Praxisauftrag") }}: {{ praxisAssignment.title }}</h2>
<span class="text-gray-800">
Circle «{{ mentorCockpitStore.getCircleTitleById(praxisAssignment.circle_id) }}»
</span>
<span class="text-gray-800">Circle «{{ praxisAssignment.circle_name }}»</span>
<template v-if="praxisAssignment.pending_evaluations > 0">
<div class="flex flex-row items-center space-x-2 pt-4">
<div
@ -92,7 +90,7 @@ onMounted(() => {
<it-icon-check class="h-5 w-5"></it-icon-check>
</span>
</div>
<span>{{ $t("a.Bewertung freigeben") }}</span>
<span>{{ $t("a.Ergebnisse abgegeben") }}</span>
</template>
</div>
<!-- Right -->
@ -102,14 +100,14 @@ onMounted(() => {
class="btn-primary"
:to="item.url"
>
{{ $t("a.Ergebnis bewerten") }}
{{ $t("a.Feedback geben") }}
</router-link>
<router-link
v-else-if="item.status == 'EVALUATED'"
class="underline"
:to="item.url"
>
{{ $t("a.Bewertung ansehen") }}
{{ $t("a.Feedback ansehen") }}
</router-link>
</div>
</div>

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import type { Assignment, Participant } from "@/services/mentorCockpit";
import { useMentorCockpit } from "@/services/mentorCockpit";
import type { Assignment, Participant } from "@/services/learningMentees";
import { useLearningMentees } from "@/services/learningMentees";
import { computed, type Ref } from "vue";
import { useCurrentCourseSession } from "@/composables";
@ -9,15 +9,15 @@ const props = defineProps<{
}>();
const courseSession = useCurrentCourseSession();
const mentorCockpitStore = useMentorCockpit(courseSession.value.id);
const learningMentees = useLearningMentees(courseSession.value.id);
const selfEvaluationFeedback: Ref<Assignment | null> = computed(() =>
mentorCockpitStore.getAssignmentById(props.learningUnitId)
learningMentees.getAssignmentById(props.learningUnitId)
);
const getParticipantById = (id: string): Participant | null => {
if (mentorCockpitStore.summary.value?.participants) {
const found = mentorCockpitStore.summary.value.participants.find(
if (learningMentees.summary.value?.participants) {
const found = learningMentees.summary.value.participants.find(
(item) => item.id === id
);
return found || null;
@ -33,9 +33,7 @@ const getParticipantById = (id: string): Participant | null => {
{{ $t("a.Selbsteinschätzung") }}: {{ selfEvaluationFeedback.title }}
</h2>
<span class="text-gray-800">
Circle «{{
mentorCockpitStore.getCircleTitleById(selfEvaluationFeedback.circle_id)
}}»
Circle «{{ selfEvaluationFeedback.circle_name }}»
</span>
<template v-if="selfEvaluationFeedback.pending_evaluations > 0">
<div class="flex flex-row items-center space-x-2 pt-4">

View File

@ -93,7 +93,7 @@ const handleContinue = () => {
const clickExit = () => {
console.log("clickExit");
router.push({
name: "mentorCockpitSelfEvaluationFeedbackAssignments",
name: "learningMentorSelfEvaluationFeedbackAssignments",
params: {
learningUnitId: props.learningUnitId,
},

View File

@ -172,7 +172,10 @@ watch(
<div>
<router-link
class="link"
:to="`/course/${courseSlug}/cockpit/profile/${profileUser.user_id}`"
:to="{
name: 'profileLearningPath',
params: { userId: profileUser.user_id, courseSlug },
}"
>
{{ $t("general.profileLink") }}
</router-link>

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

@ -19,7 +19,7 @@ import * as log from "loglevel";
import { computed, onMounted, ref, watchEffect } from "vue";
import { useTranslation } from "i18next-vue";
import { learningContentTypeData } from "@/utils/typeMaps";
import EvaluationSummary from "@/pages/cockpit/assignmentEvaluationPage/EvaluationSummary.vue";
import EvaluationSummary from "@/components/assignment/evaluation/EvaluationSummary.vue";
import { bustItGetCache } from "@/fetchHelpers";
const { t } = useTranslation();

View File

@ -1,9 +1,9 @@
<script setup lang="ts">
import type { LearningUnit, LearningUnitPerformanceCriteria } from "@/types";
import { useLearningMentors } from "@/composables";
import { useMyLearningMentors } from "@/composables";
import { computed, ref } from "vue";
import ItButton from "@/components/ui/ItButton.vue";
import NoMentorInformationPanel from "@/components/mentor/NoMentorInformationPanel.vue";
import NoMentorInformationPanel from "@/components/learningMentor/NoMentorInformationPanel.vue";
import { useSelfEvaluationFeedback } from "@/services/selfEvaluationFeedback";
import FeedbackRequestedInformationPanel from "@/components/selfEvaluationFeedback/FeedbackRequestedInformationPanel.vue";
import FeedbackReceived from "@/components/selfEvaluationFeedback/FeedbackReceived.vue";
@ -24,7 +24,7 @@ const isStoredFeedbackLoading = computed(() => selfEvaluationFeedback.loading.va
const feedbackProvider = computed(() => storedFeedback.value?.feedback_provider_user);
// if no feedback is stored "current session" state management (mentor selection etc.)
const learningMentors = useLearningMentors();
const learningMentors = useMyLearningMentors();
const isMentorsLoading = computed(() => learningMentors.loading.value);
const mentors = computed(() => {

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import CockpitProfileContent from "@/components/cockpit/profile/CockpitProfileContent.vue";
import CockpitProfileContent from "@/components/userProfile/UserProfileContent.vue";
import { ref } from "vue";
import SelfEvaluationAndFeedbackList from "@/components/selfEvaluationFeedback/SelfEvaluationAndFeedbackList.vue";
import SelfEvaluationAndFeedbackOverview from "@/components/selfEvaluationFeedback/SelfEvaluationAndFeedbackOverview.vue";

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import { calculateCircleSectorData } from "@/pages/learningPath/learningPathPage/utils";
import { useCourseDataWithCompletion } from "@/composables";
import CockpitProfileContent from "@/components/cockpit/profile/CockpitProfileContent.vue";
import UserProfileContent from "@/components/userProfile/UserProfileContent.vue";
import LearningPathCircle from "@/pages/learningPath/learningPathPage/LearningPathCircle.vue";
import LearningSequence from "@/pages/learningPath/circlePage/LearningSequence.vue";
import { ref, watch } from "vue";
@ -28,7 +28,7 @@ watch(lpQueryResult.learningPath, () => {
</script>
<template>
<CockpitProfileContent>
<UserProfileContent>
<template #side>
<div
v-for="topic in lpQueryResult.learningPath?.value?.topics ?? []"
@ -69,5 +69,5 @@ watch(lpQueryResult.learningPath, () => {
</li>
</ol>
</template>
</CockpitProfileContent>
</UserProfileContent>
</template>

View File

@ -13,8 +13,8 @@ const props = defineProps<{
const { t } = useTranslation();
const pages = ref([
{ label: t("general.learningPath"), route: "cockpitProfileLearningPath" },
{ label: t("a.KompetenzNavi"), route: "cockpitProfileCompetence" },
{ label: t("general.learningPath"), route: "profileLearningPath" },
{ label: t("a.KompetenzNavi"), route: "profileCompetence" },
]);
const courseSession = useCurrentCourseSession();
@ -40,14 +40,6 @@ onMounted(() => {
<div v-if="user" class="flex flex-col bg-gray-200">
<div class="relative border-b bg-white shadow-md">
<div class="container-large pb-0">
<router-link
class="btn-text inline-flex items-center pl-0"
:to="`/course/${props.courseSlug}/cockpit`"
>
<it-icon-arrow-left />
<span>{{ $t("general.back") }}</span>
</router-link>
<div class="mb-12 mt-2 flex items-center">
<img class="mr-8 h-48 w-48 rounded-full" :src="user.avatar_url" />
<div class="flex flex-col">

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,
@ -141,12 +140,6 @@ const router = createRouter({
import("../pages/learningPath/learningContentPage/LearningContentPage.vue"),
props: true,
},
{
path: "/course/:courseSlug/mentor",
component: () => import("@/pages/learningMentor/MentorManagementPage.vue"),
props: true,
name: "learningMentorManagement",
},
{
path: "/lernbegleitung/:courseId/invitation/:invitationId",
component: () => import("@/pages/learningMentor/InvitationAcceptPage.vue"),
@ -157,115 +150,89 @@ const router = createRouter({
public: true,
},
},
{
path: "/course/:courseSlug/profile/:userId",
component: () => import("@/pages/userProfile/UserProfilePage.vue"),
props: true,
children: [
{
path: "learning-path",
component: () => import("@/pages/userProfile/LearningPathProfilePage.vue"),
props: true,
name: "profileLearningPath",
},
{
path: "competence",
component: () => import("@/pages/userProfile/CompetenceProfilePage.vue"),
props: true,
name: "profileCompetence",
},
],
},
{
path: "/course/:courseSlug/learning-mentor",
component: () => import("@/pages/learningMentor/mentor/MentorIndexPage.vue"),
props: true,
name: "learningMentor",
children: [
{
path: "",
component: () =>
import("@/pages/learningMentor/mentor/MentorOverviewPage.vue"),
name: "learningMentorOverview",
},
{
path: "participants",
component: () =>
import("@/pages/learningMentor/mentor/MentorParticipantsPage.vue"),
name: "mentorsAndParticipants",
},
{
path: "self-evaluation-feedback/:learningUnitId",
component: () =>
import("@/pages/learningMentor/mentor/SelfEvaluationFeedbackPage.vue"),
name: "mentorSelfEvaluationFeedback",
props: true,
},
{
path: "details",
component: () =>
import("@/pages/learningMentor/mentor/MentorDetailParentPage.vue"),
children: [
{
path: "praxis-assignments/:praxisAssignmentId",
component: () =>
import("@/pages/learningMentor/mentor/MentorPraxisAssignmentPage.vue"),
name: "learningMentorPraxisAssignments",
props: true,
},
{
path: "self-evaluation-feedback-assignments/:learningUnitId",
component: () =>
import(
"@/pages/learningMentor/mentor/MentorSelfEvaluationFeedbackAssignmentPage.vue"
),
name: "learningMentorSelfEvaluationFeedbackAssignments",
props: true,
},
],
},
],
},
{
path: "/course/:courseSlug/assignment-evaluation/:assignmentId/:userId",
component: () =>
import("@/pages/assignmentEvaluation/AssignmentEvaluationPage.vue"),
props: true,
},
{
path: "/course/:courseSlug/cockpit",
name: "cockpit",
children: [
{
path: "expert",
path: "",
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",
component: () =>
import(
"@/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",
component: () =>
import(
"@/pages/cockpit/cockpitPage/mentor/MentorPraxisAssignmentPage.vue"
),
name: "mentorCockpitPraxisAssignments",
meta: {
cockpitType: "mentor",
},
props: true,
},
{
path: "self-evaluation-feedback-assignments/:learningUnitId",
component: () =>
import(
"@/pages/cockpit/cockpitPage/mentor/MentorSelfEvaluationFeedbackAssignmentPage.vue"
),
name: "mentorCockpitSelfEvaluationFeedbackAssignments",
meta: {
cockpitType: "mentor",
},
props: true,
},
],
},
],
},
{
path: "profile/:userId",
component: () =>
import("@/pages/cockpit/profilePage/CockpitUserProfilePage.vue"),
props: true,
name: "cockpitUserProfile",
children: [
{
path: "learning-path",
component: () =>
import("@/pages/cockpit/profilePage/LearningPathProfilePage.vue"),
props: true,
name: "cockpitProfileLearningPath",
},
{
path: "competence",
component: () =>
import("@/pages/cockpit/profilePage/CompetenceProfilePage.vue"),
props: true,
name: "cockpitProfileCompetence",
},
],
},
{
path: "profile/:userId/:circleSlug",
@ -283,14 +250,6 @@ const router = createRouter({
import("@/pages/cockpit/assignmentsPage/AssignmentsPage.vue"),
props: true,
},
{
path: "assignment/:assignmentId/:userId",
component: () =>
import(
"@/pages/cockpit/assignmentEvaluationPage/AssignmentEvaluationPage.vue"
),
props: true,
},
{
path: "attendance",
component: () =>
@ -431,8 +390,6 @@ router.beforeEach(redirectToLoginIfRequired);
router.beforeEach(handleCurrentCourseSession);
router.beforeEach(handleCourseSessionAsQueryParam);
router.beforeEach(handleCockpit);
router.beforeEach(addToHistory);
export default router;

View File

@ -34,32 +34,26 @@ export interface Assignment {
id: string;
title: string;
circle_id: string;
circle_name: string;
pending_evaluations: number;
completions: Completion[];
type: string;
}
interface Summary {
export interface Summary {
mentor_id: string;
participants: Participant[];
circles: Circle[];
assignments: Assignment[];
}
export const useMentorCockpit = (
export const useLearningMentees = (
courseSessionId: string | Ref<string> | (() => string)
) => {
const isLoading = ref(false);
const summary: Ref<Summary | null> = ref(null);
const error = ref(null);
const getCircleTitleById = (id: string): string => {
if (summary.value?.circles) {
const circle = summary.value.circles.find((circle) => String(circle.id) === id);
return circle ? circle.title : "";
}
return "";
};
const getAssignmentById = (id: string): Assignment | null => {
if (summary.value?.assignments) {
const found = summary.value.assignments.find(
@ -92,7 +86,6 @@ export const useMentorCockpit = (
isLoading,
summary,
error,
getCircleTitleById,
fetchData,
getAssignmentById,
};

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";
@ -59,7 +58,7 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
function selectedCourseSessionForCourse(courseSlug: string) {
// Wir wollen pro Kurs wissen, welche Durchführung der User zuletzt ausgewählt hat.
// Die letzte Durchführung wird im localStorage via `selectedCoruseSessionMap`
// Die letzte Durchführung wird im localStorage via `selectedCourseSessionMap`
// gespeichert und hier geladen.
// Wenn noch keine Durchführung ausgewählt wurde, wird die erste Durchführung
// in `courseSessionForCourse` zurückgegeben.
@ -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

@ -0,0 +1,64 @@
import { useCurrentCourseSession } from "@/composables";
import { useLearningMentees } from "@/services/learningMentees";
import { useTranslation } from "i18next-vue";
import { defineStore } from "pinia";
import { computed, ref } from "vue";
interface FilterOption {
name: string;
id: string | number;
}
export const useAssignmentTodoListStore = defineStore("filters", () => {
const { t } = useTranslation();
const courseSession = useCurrentCourseSession();
const learningMentees = useLearningMentees(courseSession.value.id);
const summary = computed(() => learningMentees.summary.value);
const statusOptions = ref<FilterOption[]>([
{ name: t("Alle"), id: "_all" },
{ name: t("a.Zu erledigen"), id: "_todo" },
]);
const selectedStatus = ref<FilterOption>(statusOptions.value[1]);
const allCircles = { name: t("a.AlleCircle"), id: "_all" };
const circleOptions = computed(() => {
if (!summary.value) return [allCircles];
return [
allCircles,
...summary.value.circles.map((circle) => ({
name: `Circle: ${circle.title}`,
id: circle.id,
})),
];
});
const selectedCircle = ref<FilterOption>(circleOptions.value[0]);
const filteredAssignments = computed(() => {
if (!summary.value) return [];
let filtered = summary.value.assignments;
if (selectedStatus.value.id !== "_all") {
filtered = filtered.filter((item) => item.pending_evaluations > 0);
}
if (selectedCircle.value.id !== "_all") {
filtered = filtered.filter(
(item) => item.circle_id === String(selectedCircle.value.id)
);
}
return filtered;
});
return {
selectedStatus,
statusOptions,
selectedCircle,
circleOptions,
filteredAssignments,
};
});

View File

@ -8,32 +8,32 @@ export function useRouteLookups() {
}
function inCockpit() {
const regex = new RegExp("/course/[^/]+/cockpit");
const regex = new RegExp("/course/[^/]+/cockpit($|/)");
return regex.test(route.path);
}
function inLearningPath() {
const regex = new RegExp("/course/[^/]+/learn");
const regex = new RegExp("/course/[^/]+/learn($|/)");
return regex.test(route.path);
}
function inCompetenceProfile() {
const regex = new RegExp("/course/[^/]+/competence");
const regex = new RegExp("/course/[^/]+/competence($|/)");
return regex.test(route.path);
}
function inLearningMentor() {
const regex = new RegExp("/course/[^/]+/mentor");
const regex = new RegExp("/course/[^/]+/learning-mentor($|/)");
return regex.test(route.path);
}
function inMediaLibrary() {
const regex = new RegExp("/course/[^/]+/media");
const regex = new RegExp("/course/[^/]+/media($|/)");
return regex.test(route.path);
}
function inAppointments() {
const regex = new RegExp("/(?:[^/]+/)?appointments");
const regex = new RegExp("/(?:[^/]+/)?appointments($|/)");
return regex.test(route.path);
}

View File

@ -14,10 +14,7 @@ function createCourseUrl(courseSlug: string | undefined, specificSub: string): s
return "/";
}
if (["learn", "media", "competence", "cockpit", "mentor"].includes(specificSub)) {
return `/course/${courseSlug}/${specificSub}`;
}
return `/course/${courseSlug}`;
return `/course/${courseSlug}/${specificSub}`;
}
export function getCompetenceNaviUrl(courseSlug: string | undefined): string {
@ -32,12 +29,12 @@ export function getLearningPathUrl(courseSlug: string | undefined): string {
return createCourseUrl(courseSlug, "learn");
}
export function getCockpitUrl(courseSlug: string | undefined): string {
return createCourseUrl(courseSlug, "cockpit");
export function getLearningMentorUrl(courseSlug: string | undefined): string {
return createCourseUrl(courseSlug, "learning-mentor");
}
export function getLearningMentorManagementUrl(courseSlug: string | undefined): string {
return createCourseUrl(courseSlug, "mentor");
export function getCockpitUrl(courseSlug: string | undefined): string {
return createCourseUrl(courseSlug, "cockpit");
}
export function getAssignmentTypeTitle(assignmentType: AssignmentType): string {

View File

@ -1,5 +1,5 @@
import { TEST_TRAINER1_USER_ID } from "../../consts";
import { login } from "../helpers";
import {TEST_TRAINER1_USER_ID} from "../../consts";
import {EXPERT_COCKPIT_URL, login} from "../helpers";
describe("assignmentTrainer.cy.js", () => {
beforeEach(() => {
@ -9,10 +9,8 @@ describe("assignmentTrainer.cy.js", () => {
describe("Casework", () => {
it("can open cockpit assignment page and open user assignment", () => {
cy.visit("/course/test-lehrgang/cockpit");
cy.get(
'[data-cy="show-details-btn-test-lehrgang-lp-circle-fahrzeug-lc-überprüfen-einer-motorfahrzeug-versicherungspolice"]'
).click();
cy.visit(EXPERT_COCKPIT_URL);
cy.get('[data-cy="show-details-btn-test-lehrgang-lp-circle-fahrzeug-lc-überprüfen-einer-motorfahrzeug-versicherungspolice"]').click();
cy.get('[data-cy="Student1"]').should("contain", "Ergebnisse abgegeben");
cy.get('[data-cy="Student1"]').find('[data-cy="show-results"]').click();
@ -22,24 +20,16 @@ describe("assignmentTrainer.cy.js", () => {
});
it("can start evaluation and store evaluation results", () => {
cy.visit("/course/test-lehrgang/cockpit");
cy.get(
'[data-cy="show-details-btn-test-lehrgang-lp-circle-fahrzeug-lc-überprüfen-einer-motorfahrzeug-versicherungspolice"]'
).click();
cy.visit(EXPERT_COCKPIT_URL)
cy.get('[data-cy="show-details-btn-test-lehrgang-lp-circle-fahrzeug-lc-überprüfen-einer-motorfahrzeug-versicherungspolice"]').click();
cy.get('[data-cy="Student1"]').find('[data-cy="show-results"]').click();
cy.get('[data-cy="title"]').should("contain", "Bewertung");
cy.get('[data-cy="evaluation-duedate"]').should("exist");
cy.get('[data-cy="instruction"]').should(
"contain",
"Die Gesamtpunktzahl und die daraus resultierende Note wird auf Grund des hinterlegeten Beurteilungsinstrument berechnet."
);
cy.get('[data-cy="instruction"]').should("contain", "Die Gesamtpunktzahl und die daraus resultierende Note wird auf Grund des hinterlegeten Beurteilungsinstrument berechnet.");
cy.get('[data-cy="start-evaluation"]').click();
cy.get('[data-cy="evaluation-task"]').should(
"contain",
"Beurteilungskriterium 1 / 5"
);
cy.get('[data-cy="evaluation-task"]').should("contain", "Beurteilungskriterium 1 / 5");
// without text input the button should be disabled
cy.get('[data-cy="next-step"]').should("be.disabled");
@ -51,10 +41,7 @@ describe("assignmentTrainer.cy.js", () => {
cy.wait(500);
cy.get('[data-cy="next-step"]').click();
cy.get('[data-cy="evaluation-task"]').should(
"contain",
"Beurteilungskriterium 2 / 5"
);
cy.get('[data-cy="evaluation-task"]').should("contain", "Beurteilungskriterium 2 / 5");
cy.get('[data-cy="subtask-2"]').click();
cy.get('[data-cy="reason-text"]').type("Nicht so gut");
cy.wait(500);
@ -84,36 +71,28 @@ describe("assignmentTrainer.cy.js", () => {
cy.wait(500);
// load AssignmentCompletion from DB and check
cy.loadAssignmentCompletion(
"evaluation_user_id",
TEST_TRAINER1_USER_ID
).then((ac) => {
cy.loadAssignmentCompletion("evaluation_user_id", TEST_TRAINER1_USER_ID).then((ac) => {
expect(ac.completion_status).to.equal("EVALUATION_IN_PROGRESS");
expect(JSON.stringify(ac.completion_data)).to.include("Nicht so gut");
expect(Cypress._.values(ac.completion_data)).to.deep.include({
expert_data: { points: 2, text: "Nicht so gut" },
expert_data: {points: 2, text: "Nicht so gut"},
});
expect(Cypress._.values(ac.completion_data)).to.deep.include({
expert_data: { points: 4, text: "Gut gemacht!" },
expert_data: {points: 4, text: "Gut gemacht!"},
});
});
});
it("can make complete evaluation", () => {
cy.visit("/course/test-lehrgang/cockpit");
cy.get(
'[data-cy="show-details-btn-test-lehrgang-lp-circle-fahrzeug-lc-überprüfen-einer-motorfahrzeug-versicherungspolice"]'
).click();
cy.visit(EXPERT_COCKPIT_URL)
cy.get('[data-cy="show-details-btn-test-lehrgang-lp-circle-fahrzeug-lc-überprüfen-einer-motorfahrzeug-versicherungspolice"]').click();
cy.get('[data-cy="Student1"]').find('[data-cy="show-results"]').click();
cy.get('[data-cy="start-evaluation"]').click();
// step 1
cy.get('[data-cy="evaluation-task"]').should(
"contain",
"Beurteilungskriterium 1 / 5"
);
cy.get('[data-cy="evaluation-task"]').should("contain", "Beurteilungskriterium 1 / 5");
cy.get('[data-cy="subtask-6"]').click();
cy.get('[data-cy="reason-text"]').type("Begründung Schritt 1");
// wait for debounce
@ -121,40 +100,28 @@ describe("assignmentTrainer.cy.js", () => {
cy.get('[data-cy="next-step"]').click();
// step 2
cy.get('[data-cy="evaluation-task"]').should(
"contain",
"Beurteilungskriterium 2 / 5"
);
cy.get('[data-cy="evaluation-task"]').should("contain", "Beurteilungskriterium 2 / 5");
cy.get('[data-cy="subtask-4"]').click();
cy.get('[data-cy="reason-text"]').type("Begründung Schritt 2");
cy.wait(500);
cy.get('[data-cy="next-step"]').click();
// step 3
cy.get('[data-cy="evaluation-task"]').should(
"contain",
"Beurteilungskriterium 3 / 5"
);
cy.get('[data-cy="evaluation-task"]').should("contain", "Beurteilungskriterium 3 / 5");
cy.get('[data-cy="subtask-2"]').click();
cy.get('[data-cy="reason-text"]').type("Begründung Schritt 3");
cy.wait(500);
cy.get('[data-cy="next-step"]').click();
// step 4
cy.get('[data-cy="evaluation-task"]').should(
"contain",
"Beurteilungskriterium 4 / 5"
);
cy.get('[data-cy="evaluation-task"]').should("contain", "Beurteilungskriterium 4 / 5");
cy.get('[data-cy="subtask-3"]').click();
cy.get('[data-cy="reason-text"]').type("Begründung Schritt 4");
cy.wait(500);
cy.get('[data-cy="next-step"]').click();
// step 5
cy.get('[data-cy="evaluation-task"]').should(
"contain",
"Beurteilungskriterium 5 / 5"
);
cy.get('[data-cy="evaluation-task"]').should("contain", "Beurteilungskriterium 5 / 5");
cy.get('[data-cy="subtask-2"]').click();
cy.get('[data-cy="reason-text"]').type("Begründung Schritt 5");
cy.wait(500);
@ -166,17 +133,12 @@ describe("assignmentTrainer.cy.js", () => {
cy.get('[data-cy="total-points"]').should("contain", "24");
cy.get('[data-cy="submit-evaluation"]').click();
cy.get('[data-cy="result-section"]').should(
"contain",
"Deine Bewertung für Test Student1 wurde freigegeben"
);
cy.get('[data-cy="result-section"]').should("contain", "Deine Bewertung für Test Student1 wurde freigegeben");
// going back to cockpit should show points for student
cy.visit("/course/test-lehrgang/cockpit");
cy.visit(EXPERT_COCKPIT_URL);
cy.reload();
cy.get(
'[data-cy="show-details-btn-test-lehrgang-lp-circle-fahrzeug-lc-überprüfen-einer-motorfahrzeug-versicherungspolice"]'
).click();
cy.get('[data-cy="show-details-btn-test-lehrgang-lp-circle-fahrzeug-lc-überprüfen-einer-motorfahrzeug-versicherungspolice"]').click();
cy.get('[data-cy="Student1"]').should("contain", "Bewertung freigegeben");
cy.get('[data-cy="Student1"]').should("contain", "17 von 24 Punkte");
@ -186,10 +148,7 @@ describe("assignmentTrainer.cy.js", () => {
cy.url().should("include", "step=6");
// load AssignmentCompletion from DB and check
cy.loadAssignmentCompletion(
"evaluation_user_id",
TEST_TRAINER1_USER_ID
).then((ac) => {
cy.loadAssignmentCompletion("evaluation_user_id", TEST_TRAINER1_USER_ID).then((ac) => {
expect(ac.completion_status).to.equal("EVALUATION_SUBMITTED");
expect(ac.evaluation_points).to.equal(17);
expect(ac.evaluation_max_points).to.equal(24);
@ -207,21 +166,16 @@ describe("assignmentTrainer.cy.js", () => {
//Todo: Move tests to Lernbegleitung once it is implemented
describe("Praxis Assignment", () => {
it("can start evaluation and store evaluation results", () => {
cy.visit("/course/test-lehrgang/cockpit");
cy.visit(EXPERT_COCKPIT_URL);
cy.get('[data-cy="dropdown-select"]').click();
cy.get('[data-cy="dropdown-select-option-Reisen"]').click();
cy.get(
'[data-cy="show-details-btn-test-lehrgang-lp-circle-reisen-lc-mein-kundenstamm"]'
).click();
cy.get('[data-cy="show-details-btn-test-lehrgang-lp-circle-reisen-lc-mein-kundenstamm"]').click();
cy.get('[data-cy="Student1"]').find('[data-cy="show-results"]').click();
cy.get('[data-cy="title"]').should("contain", "Feedback");
cy.get('[data-cy="evaluation-duedate]"').should("not.exist");
cy.get('[data-cy="instruction"]').should(
"contain",
"Bitte unterstütze Test Student1 und gib Feedback zum Auftrag."
);
cy.get('[data-cy="instruction"]').should("contain", "Bitte unterstütze Test Student1 und gib Feedback zum Auftrag.");
cy.get('[data-cy="start-evaluation"]').click();
cy.get('[data-cy="evaluation-task"]').should("contain", "Feedback 1 / 5");
@ -239,29 +193,24 @@ describe("assignmentTrainer.cy.js", () => {
cy.wait(1000);
// load AssignmentCompletion from DB and check
cy.loadAssignmentCompletion(
"evaluation_user_id",
TEST_TRAINER1_USER_ID
).then((ac) => {
cy.loadAssignmentCompletion("evaluation_user_id", TEST_TRAINER1_USER_ID).then((ac) => {
console.log(ac.completion_status);
expect(ac.completion_status).to.equal("EVALUATION_IN_PROGRESS");
expect(JSON.stringify(ac.completion_data)).to.include("Nicht so gut");
expect(Cypress._.values(ac.completion_data)).to.deep.include({
expert_data: { points: 0, text: "Nicht so gut" },
expert_data: {points: 0, text: "Nicht so gut"},
});
expect(Cypress._.values(ac.completion_data)).to.deep.include({
expert_data: { points: 0, text: "Gut gemacht!" },
expert_data: {points: 0, text: "Gut gemacht!"},
});
});
});
it("can make complete evaluation", () => {
cy.visit("/course/test-lehrgang/cockpit");
cy.visit(EXPERT_COCKPIT_URL);
cy.get('[data-cy="dropdown-select"]').click();
cy.get('[data-cy="dropdown-select-option-Reisen"]').click();
cy.get(
'[data-cy="show-details-btn-test-lehrgang-lp-circle-reisen-lc-mein-kundenstamm"]'
).click();
cy.get('[data-cy="show-details-btn-test-lehrgang-lp-circle-reisen-lc-mein-kundenstamm"]').click();
cy.get('[data-cy="Student1"]').find('[data-cy="show-results"]').click();
@ -303,19 +252,14 @@ describe("assignmentTrainer.cy.js", () => {
cy.get('[data-cy="total-points"]').should("not.exist");
cy.get('[data-cy="submit-evaluation"]').click();
cy.get('[data-cy="result-section"]').should(
"contain",
"Dein Feedback für Test Student1 wurde freigegeben"
);
cy.get('[data-cy="result-section"]').should("contain", "Dein Feedback für Test Student1 wurde freigegeben");
// going back to cockpit should show points for student
cy.visit("/course/test-lehrgang/cockpit");
cy.visit(EXPERT_COCKPIT_URL);
cy.reload();
cy.get('[data-cy="dropdown-select"]').click();
cy.get('[data-cy="dropdown-select-option-Reisen"]').click();
cy.get(
'[data-cy="show-details-btn-test-lehrgang-lp-circle-reisen-lc-mein-kundenstamm"]'
).click();
cy.get('[data-cy="show-details-btn-test-lehrgang-lp-circle-reisen-lc-mein-kundenstamm"]').click();
cy.get('[data-cy="Student1"]').should("contain", "Feedback freigegeben");
cy.get('[data-cy="Student1"]').should("not.contain", "Punkte");
@ -325,10 +269,7 @@ describe("assignmentTrainer.cy.js", () => {
cy.url().should("include", "step=6");
// load AssignmentCompletion from DB and check
cy.loadAssignmentCompletion(
"evaluation_user_id",
TEST_TRAINER1_USER_ID
).then((ac) => {
cy.loadAssignmentCompletion("evaluation_user_id", TEST_TRAINER1_USER_ID).then((ac) => {
expect(ac.completion_status).to.equal("EVALUATION_SUBMITTED");
expect(ac.evaluation_max_points).to.equal(0);
const completionString = JSON.stringify(ac.completion_data);

View File

@ -1,4 +1,4 @@
import { login } from "../helpers";
import {EXPERT_COCKPIT_URL, login} from "../helpers";
describe("feedbackTrainer.cy.js", () => {
beforeEach(() => {
@ -8,7 +8,7 @@ describe("feedbackTrainer.cy.js", () => {
it("can open feedback results page with empty results", () => {
cy.manageCommand("cypress_reset");
login("test-trainer1@example.com", "test");
cy.visit("/course/test-lehrgang/cockpit");
cy.visit(EXPERT_COCKPIT_URL);
cy.get(
'[data-cy="show-feedback-btn-test-lehrgang-lp-circle-fahrzeug-lc-feedback"]'
).click();
@ -20,7 +20,7 @@ describe("feedbackTrainer.cy.js", () => {
it("can open feedback results page with results", () => {
cy.manageCommand("cypress_reset --create-feedback-responses");
login("test-trainer1@example.com", "test");
cy.visit("/course/test-lehrgang/cockpit");
cy.visit(EXPERT_COCKPIT_URL);
cy.get(
'[data-cy="show-feedback-btn-test-lehrgang-lp-circle-fahrzeug-lc-feedback"]'
).click();
@ -138,7 +138,7 @@ describe("feedbackTrainer.cy.js", () => {
it("can open feedback results page with results", () => {
cy.manageCommand("cypress_reset --create-feedback-responses");
login("test-trainer1@example.com", "test");
cy.visit("/course/test-lehrgang/cockpit");
cy.visit(EXPERT_COCKPIT_URL);
cy.get('[data-cy="dropdown-select"]').click();
cy.get('[data-cy="dropdown-select-option-Reisen"]').click();
cy.get(

View File

@ -1,13 +1,15 @@
export const EXPERT_COCKPIT_URL = "course/test-lehrgang/cockpit"
export const login = (username, password) => {
cy.request({
method: "POST", url: "/api/core/login/", body: {username, password},
});
cy.request({
method: "POST", url: "/api/core/login/", body: {username, password},
});
};
export const logout = () => {
cy.request({
method: "POST", url: "/api/core/logout/",
});
cy.request({
method: "POST", url: "/api/core/logout/",
});
};
export const BASE_URL = "/course/test-lehrgang";
@ -15,9 +17,9 @@ export const EXPERT_LOGIN = ["test-trainer1@example.com", "test"];
export const PARTICIPANT_LOGIN = ["test-student1@example.com", "test"];
export const visitCoursePage = (subPath) => {
cy.visit(`${BASE_URL}/${subPath}`);
cy.visit(`${BASE_URL}/${subPath}`);
}
export const checkNavigationLink = (dataCy, expectedLink) => {
cy.get(`[data-cy="${dataCy}"]`).should('have.attr', 'href').and('eq', `${BASE_URL}/${expectedLink}`);
cy.get(`[data-cy="${dataCy}"]`).should('have.attr', 'href').and('eq', `${BASE_URL}/${expectedLink}`);
}

View File

@ -0,0 +1,6 @@
Since the learning mentor behaves differently depending on the user's role, the learning mentor is tested in the
following roles:
- [x] As a learning mentor (no other roles). -> `justMentor.cy.js`
- [x] As a course session user (member, no other roles). -> `justMember.cy.js`
- [x] As a course session user (member) and a learning mentor. `mentorAndMember.cy.js`

View File

@ -0,0 +1,23 @@
export const MENTOR_OVERVIEW_URL = "/course/versicherungsvermittler-in/learning-mentor";
export const MENTOR_MENTEES_URL = "/course/versicherungsvermittler-in/learning-mentor/participants";
export const MENTOR_DASHBOARD_LINK = "[data-cy=lm-dashboard-link]";
export const MEMBER_DASHBOARD_LINK = "[data-cy=progress-dashboard-continue-course-link]";
export const MENTOR_MAIN_NAVIGATION = "[data-cy=lm-main-navigation]";
export const MENTOR_OVERVIEW_NAVIGATION_LINK = "[data-cy=lm-overview-navigation-link]";
export const MENTOR_MENTEES_NAVIGATION_LINK = "[data-cy=lm-mentees-navigation-link]";
// /participants
export const MENTOR_MY_MENTEES = "[data-cy=lm-my-mentees]";
export const MENTOR_MY_MENTORS = "[data-cy=lm-my-mentors]";
export const MENTOR_MENTEE_LIST_ITEM = "[data-cy=lm-my-mentee-list-item]";
export const MENTOR_MENTEE_REMOVE = "[data-cy=lm-my-mentee-remove]";
export const MENTOR_MENTEE_PROFILE = "[data-cy=lm-my-mentee-profile]";
export const MENTEE_MENTOR_LIST_ITEM = "[data-cy=lm-my-mentor-list-item]";
export const MENTEE_MENTOR_REMOVE = "[data-cy=lm-my-mentor-remove]";

View File

@ -0,0 +1,58 @@
import {login} from "../../helpers";
import {
MEMBER_DASHBOARD_LINK,
MENTEE_MENTOR_LIST_ITEM,
MENTEE_MENTOR_REMOVE,
MENTOR_MENTEES_NAVIGATION_LINK,
MENTOR_MENTEES_URL,
MENTOR_MY_MENTEES,
MENTOR_MY_MENTORS,
MENTOR_OVERVIEW_NAVIGATION_LINK,
MENTOR_OVERVIEW_URL
} from "../constants";
describe("memberOnly.cy.js", () => {
beforeEach(() => {
cy.manageCommand("cypress_reset --create-learning-mentor");
login("student-vv@eiger-versicherungen.ch", "test");
});
it("shows the correct dashboard", () => {
cy.visit("/");
cy.get(MEMBER_DASHBOARD_LINK).should("exist");
});
it("shows NO mentees navigation link", () => {
cy.visit(MENTOR_OVERVIEW_URL);
cy.get(MENTOR_MENTEES_NAVIGATION_LINK).should("not.exist");
})
it("shows NO overview navigation link", () => {
cy.visit(MENTOR_OVERVIEW_URL);
cy.get(MENTOR_OVERVIEW_NAVIGATION_LINK).should("not.exist");
})
it("shows NO mentees", () => {
cy.visit(MENTOR_MENTEES_URL);
cy.get(MENTOR_MY_MENTEES).should("not.exist");
});
it("shows my mentors", () => {
cy.visit(MENTOR_MENTEES_URL);
cy.get(MENTOR_MY_MENTORS).should("exist");
});
it("can remove a mentor", () => {
// given
const mentor = "Micheala Weber-Mentor";
// when
cy.visit(MENTOR_MENTEES_URL);
cy.contains(MENTEE_MENTOR_LIST_ITEM, mentor)
.find(MENTEE_MENTOR_REMOVE)
.click();
// then
cy.contains(MENTOR_MY_MENTORS, mentor).should("not.exist");
})
});

View File

@ -0,0 +1,112 @@
import {login} from "../../helpers";
import {
MEMBER_DASHBOARD_LINK,
MENTEE_MENTOR_LIST_ITEM,
MENTEE_MENTOR_REMOVE,
MENTOR_MAIN_NAVIGATION,
MENTOR_MENTEE_LIST_ITEM,
MENTOR_MENTEE_PROFILE,
MENTOR_MENTEE_REMOVE,
MENTOR_MENTEES_NAVIGATION_LINK,
MENTOR_MENTEES_URL,
MENTOR_MY_MENTEES,
MENTOR_MY_MENTORS,
MENTOR_OVERVIEW_NAVIGATION_LINK,
MENTOR_OVERVIEW_URL
} from "../constants";
describe("mentorAndMember.cy.js", () => {
beforeEach(() => {
cy.manageCommand("cypress_reset --create-learning-mentor");
login("test-student-and-mentor2@example.com", "test");
});
it("shows the correct dashboard", () => {
cy.visit("/");
cy.get(MEMBER_DASHBOARD_LINK).should("exist");
});
it("shows the learning mentor navigation", () => {
cy.visit(MENTOR_OVERVIEW_URL);
cy.get(MENTOR_MAIN_NAVIGATION).should("exist");
});
it("shows the mentees navigation link", () => {
cy.visit(MENTOR_OVERVIEW_URL);
cy.get(MENTOR_MENTEES_NAVIGATION_LINK).click();
cy.url().should("include", MENTOR_MENTEES_URL);
})
it("shows the overview navigation link", () => {
cy.visit(MENTOR_OVERVIEW_URL);
cy.get(MENTOR_OVERVIEW_NAVIGATION_LINK).click();
cy.url().should("include", MENTOR_OVERVIEW_URL);
})
it("shows my mentees", () => {
cy.visit(MENTOR_MENTEES_URL);
cy.get(MENTOR_MY_MENTEES).should("exist");
});
it("shows my mentors", () => {
cy.visit(MENTOR_MENTEES_URL);
cy.get(MENTOR_MY_MENTORS).should("exist");
});
it("shows the correct mentees", () => {
cy.visit(MENTOR_MENTEES_URL);
cy.get(MENTOR_MY_MENTEES).should("contain", "Viktor Vollgas");
});
it("shows the profile of a mentee", () => {
// given
const mentee = "Viktor Vollgas";
// when
cy.visit(MENTOR_MENTEES_URL);
cy.contains(MENTOR_MENTEE_LIST_ITEM, mentee)
.find(MENTOR_MENTEE_PROFILE)
.click();
// then
const expectedMenteeProfileUrl = "/course/versicherungsvermittler-in/profile/5ff59857-8de5-415e-a387-4449f9a0337a"
cy.url().should("include", expectedMenteeProfileUrl);
cy.contains(mentee).should("exist");
})
it("can remove a mentee", () => {
// given
const mentee = "Viktor Vollgas";
// when
cy.visit(MENTOR_MENTEES_URL);
cy.contains(MENTOR_MENTEE_LIST_ITEM, mentee)
.find(MENTOR_MENTEE_REMOVE)
.click();
// then
cy.contains(MENTOR_MENTEE_LIST_ITEM, mentee).should("not.exist");
cy.contains("Aktuell begleitest du niemanden als Lernbegleitung").should("exist");
})
it("shows the correct mentors", () => {
const mentor = "Micheala Weber-Mentor";
cy.visit(MENTOR_MENTEES_URL);
cy.get(MENTOR_MY_MENTORS).should("contain", mentor);
})
it("can remove a mentor", () => {
// given
const mentor = "Micheala Weber-Mentor";
// when
cy.visit(MENTOR_MENTEES_URL);
cy.contains(MENTEE_MENTOR_LIST_ITEM, mentor)
.find(MENTEE_MENTOR_REMOVE)
.click();
// then
cy.contains(MENTOR_MY_MENTORS, mentor).should("not.exist");
cy.contains("Aktuell hast du noch keine Person als Lernbegleitung eingeladen").should("exist");
})
});

View File

@ -0,0 +1,91 @@
import {login} from "../../helpers";
import {
MENTOR_DASHBOARD_LINK,
MENTOR_MAIN_NAVIGATION,
MENTOR_MENTEE_LIST_ITEM,
MENTOR_MENTEE_PROFILE,
MENTOR_MENTEE_REMOVE,
MENTOR_MENTEES_NAVIGATION_LINK,
MENTOR_MENTEES_URL,
MENTOR_MY_MENTEES,
MENTOR_MY_MENTORS,
MENTOR_OVERVIEW_NAVIGATION_LINK,
MENTOR_OVERVIEW_URL
} from "../constants";
describe("mentorOnly.cy.js", () => {
beforeEach(() => {
cy.manageCommand("cypress_reset --create-learning-mentor");
login("test-mentor1@example.com", "test");
});
it("shows the correct dashboard", () => {
cy.visit("/");
cy.get(MENTOR_DASHBOARD_LINK).click();
cy.url().should("include", MENTOR_OVERVIEW_URL);
});
it("shows the learning mentor navigation", () => {
cy.visit(MENTOR_OVERVIEW_URL);
cy.get(MENTOR_MAIN_NAVIGATION).should("exist");
});
it("shows the mentees navigation link", () => {
cy.visit(MENTOR_OVERVIEW_URL);
cy.get(MENTOR_MENTEES_NAVIGATION_LINK).click();
cy.url().should("include", MENTOR_MENTEES_URL);
})
it("shows the overview navigation link", () => {
cy.visit(MENTOR_OVERVIEW_URL);
cy.get(MENTOR_OVERVIEW_NAVIGATION_LINK).click();
cy.url().should("include", MENTOR_OVERVIEW_URL);
})
it("shows my mentees", () => {
cy.visit(MENTOR_MENTEES_URL);
cy.get(MENTOR_MY_MENTEES).should("exist");
});
it("shows no mentors", () => {
cy.visit(MENTOR_MENTEES_URL);
cy.get(MENTOR_MY_MENTORS).should("not.exist");
});
it("shows the correct mentees", () => {
cy.visit(MENTOR_MENTEES_URL);
cy.get(MENTOR_MY_MENTEES).should("contain", "Robert Student-plus-Mentor");
cy.get(MENTOR_MY_MENTEES).should("contain", "Viktor Vollgas");
});
it("shows the profile of a mentee", () => {
// given
const mentee = "Viktor Vollgas";
// when
cy.visit(MENTOR_MENTEES_URL);
cy.contains(MENTOR_MENTEE_LIST_ITEM, mentee)
.find(MENTOR_MENTEE_PROFILE)
.click();
// then
const expectedMenteeProfileUrl = "/course/versicherungsvermittler-in/profile/5ff59857-8de5-415e-a387-4449f9a0337a"
cy.url().should("include", expectedMenteeProfileUrl);
cy.contains(mentee).should("exist");
})
it("can remove a mentee", () => {
// given
const mentee = "Viktor Vollgas";
// when
cy.visit(MENTOR_MENTEES_URL);
cy.contains(MENTOR_MENTEE_LIST_ITEM, mentee)
.find(MENTOR_MENTEE_REMOVE)
.click();
// then
cy.contains(MENTOR_MENTEE_LIST_ITEM, mentee).should("not.exist");
cy.contains(MENTOR_MENTEE_LIST_ITEM, "Robert Student-plus-Mentor").should("exist")
})
});

View File

@ -1,4 +1,4 @@
import { login } from "./helpers";
import {EXPERT_COCKPIT_URL, login} from "./helpers";
describe("settings.cy.js", () => {
beforeEach(() => {
@ -14,7 +14,7 @@ describe("settings.cy.js", () => {
it("trainer can see circle documents", () => {
login("test-trainer1@example.com", "test");
cy.visit("/course/test-lehrgang/cockpit");
cy.visit(EXPERT_COCKPIT_URL);
cy.get('[data-cy="circle-documents"]').should("exist");
});
});
@ -33,7 +33,7 @@ describe("settings.cy.js", () => {
it("trainer cannot see circle documents", () => {
login("test-trainer1@example.com", "test");
cy.visit("/course/test-lehrgang/cockpit");
cy.visit(EXPERT_COCKPIT_URL);
cy.get('[data-cy="circle-documents"]').should("not.exist");
});
});

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
@ -123,9 +118,6 @@ urlpatterns = [
# course
path(r"api/course/sessions/", get_course_sessions, name="get_course_sessions"),
# path(r"api/course/sessions/<signed_int:course_session_id>/users/",
# get_course_session_users,
# name="get_course_session_users"),
path(r"api/course/page/<slug_or_id>/", course_page_api_view,
name="course_page_api_view"),
path(r"api/course/completion/mark/", mark_course_completion_view,
@ -136,9 +128,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

@ -377,11 +377,8 @@ class AssignmentCompletion(models.Model):
]
def get_assignment_evaluation_frontend_url(self):
"""
Used by the expert to evaluate the assignment
Example: /course/überbetriebliche-kurse/cockpit/assignment/371/18
"""
return f"{self.course_session.course.get_cockpit_url()}/assignment/{self.assignment.id}/{self.assignment_user.id}"
"""Used by the expert to evaluate the assignment"""
return f"{self.course_session.course.get_course_url()}/assignment-evaluation/{self.assignment.id}/{self.assignment_user.id}"
@property
def task_completion_data(self):

View File

@ -58,7 +58,6 @@ class AttendanceCourseUserMutationTestCase(GraphQLTestCase):
file.save()
file_id = str(file.id)
file_url = file.url
completion_data_string = json.dumps(
{
@ -223,7 +222,7 @@ class AttendanceCourseUserMutationTestCase(GraphQLTestCase):
self.course_session,
)
self.assertTrue(
f"/course/test-lehrgang/cockpit/assignment" in notification.target_url
f"/course/test-lehrgang/assignment-evaluation" in notification.target_url
)
# second submit will fail

View File

@ -26,6 +26,7 @@ TEST_STUDENT2_USER_ID = "19c40d94-15cc-4198-aaad-ef707c4b0900"
TEST_STUDENT3_USER_ID = "bcf94dba-53bc-474b-a22d-e4af39aa042b"
TEST_MENTOR1_USER_ID = "d1f5f5a9-5b0a-4e1a-9e1a-9e9b5b5e1b1b"
TEST_STUDENT1_VV_USER_ID = "5ff59857-8de5-415e-a387-4449f9a0337a"
TEST_STUDENT2_VV_AND_VV_MENTOR_USER_ID = "7e8ebf0b-e6e2-4022-88f4-6e663ba0a9db"
TEST_COURSE_SESSION_BERN_ID = -1
TEST_COURSE_SESSION_ZURICH_ID = -2

View File

@ -15,6 +15,7 @@ from vbv_lernwelt.core.constants import (
TEST_STUDENT1_USER_ID,
TEST_STUDENT1_VV_USER_ID,
TEST_STUDENT2_USER_ID,
TEST_STUDENT2_VV_AND_VV_MENTOR_USER_ID,
TEST_STUDENT3_USER_ID,
TEST_SUPERVISOR1_USER_ID,
TEST_TRAINER1_USER_ID,
@ -372,6 +373,14 @@ def create_default_users(default_password="test", set_avatar=False):
language="de",
avatar_image="uk1.patrizia.huggel.jpg",
)
_create_student_user(
id=TEST_STUDENT2_VV_AND_VV_MENTOR_USER_ID,
email="test-student-and-mentor2@example.com",
first_name="Robert",
last_name="Student-plus-Mentor",
password=default_password,
language="de",
)
def _get_or_create_user(user_model, *args, **kwargs):

View File

@ -11,6 +11,7 @@ from vbv_lernwelt.core.constants import (
TEST_STUDENT1_USER_ID,
TEST_STUDENT1_VV_USER_ID,
TEST_STUDENT2_USER_ID,
TEST_STUDENT2_VV_AND_VV_MENTOR_USER_ID,
TEST_STUDENT3_USER_ID,
TEST_TRAINER1_USER_ID,
)
@ -343,31 +344,51 @@ def command(
attendance_course.save()
if create_learning_mentor:
cs_bern = CourseSession.objects.get(id=TEST_COURSE_SESSION_BERN_ID)
uk_mentor = LearningMentor.objects.create(
course=Course.objects.get(id=COURSE_TEST_ID),
mentor=User.objects.get(id=TEST_MENTOR1_USER_ID),
course_session=cs_bern,
)
uk_mentor.participants.add(
CourseSessionUser.objects.get(
user__id=TEST_STUDENT1_USER_ID,
course_session=CourseSession.objects.get(
id=TEST_COURSE_SESSION_BERN_ID
),
course_session=cs_bern,
)
)
vv_course = Course.objects.get(id=COURSE_VERSICHERUNGSVERMITTLERIN_ID)
vv_course_session = CourseSession.objects.get(course=vv_course)
vv_mentor = LearningMentor.objects.create(
course=vv_course,
mentor=User.objects.get(id=TEST_MENTOR1_USER_ID),
course_session=vv_course_session,
)
vv_mentor.participants.add(
CourseSessionUser.objects.get(
user__id=TEST_STUDENT1_VV_USER_ID, course_session=vv_course_session
)
)
vv_mentor.participants.add(
CourseSessionUser.objects.get(
user__id=TEST_STUDENT2_VV_AND_VV_MENTOR_USER_ID,
course_session=vv_course_session,
)
)
vv_student_and_mentor = LearningMentor.objects.create(
mentor=User.objects.get(id=TEST_STUDENT2_VV_AND_VV_MENTOR_USER_ID),
course_session=vv_course_session,
)
vv_student_and_mentor.participants.add(
CourseSessionUser.objects.get(
user__id=TEST_STUDENT1_VV_USER_ID,
course_session=vv_course_session,
)
)
course = Course.objects.get(id=COURSE_TEST_ID)
course.configuration.enable_circle_documents = enable_circle_documents
course.configuration.save()

View File

@ -139,7 +139,7 @@ def add_mentor_to_course_session(
):
for mentor, mentee in mentor_mentee_pairs:
lm = LearningMentor.objects.create(
course=course_session.course,
course_session=course_session,
mentor=mentor,
)
lm.participants.add(

View File

@ -101,9 +101,11 @@ def create_course_session(
def add_learning_mentor(
course: Course, mentor: User, mentee: CourseSessionUser
course_session: CourseSession, mentor: User, mentee: CourseSessionUser
) -> LearningMentor:
learning_mentor = LearningMentor.objects.create(course=course, mentor=mentor)
learning_mentor = LearningMentor.objects.create(
course_session=course_session, mentor=mentor
)
learning_mentor.participants.add(mentee)
return learning_mentor

View File

@ -45,7 +45,10 @@ from vbv_lernwelt.competence.create_vv_new_competence_profile import (
create_vv_new_competence_profile,
)
from vbv_lernwelt.competence.models import PerformanceCriteria
from vbv_lernwelt.core.constants import TEST_MENTOR1_USER_ID
from vbv_lernwelt.core.constants import (
TEST_MENTOR1_USER_ID,
TEST_STUDENT2_VV_AND_VV_MENTOR_USER_ID,
)
from vbv_lernwelt.core.create_default_users import default_users
from vbv_lernwelt.core.models import User
from vbv_lernwelt.course.consts import (
@ -234,7 +237,7 @@ def create_versicherungsvermittlerin_course(
user=User.objects.get(username=user_data["email"]),
)
csu = CourseSessionUser.objects.create(
student_1_csu = CourseSessionUser.objects.create(
course_session=cs,
user=User.objects.get(username="student-vv@eiger-versicherungen.ch"),
)
@ -257,12 +260,29 @@ def create_versicherungsvermittlerin_course(
role=CourseSessionUser.Role.EXPERT,
)
lemme = LearningMentor.objects.create(
mentor=User.objects.get(id=TEST_MENTOR1_USER_ID),
course=cs.course,
mentor_and_student_2_learning_csu = CourseSessionUser.objects.create(
course_session=cs,
user=User.objects.get(id=TEST_STUDENT2_VV_AND_VV_MENTOR_USER_ID),
role=CourseSessionUser.Role.MEMBER,
)
lemme.participants.add(csu)
# TEST_MENTOR1_USER_ID is only mentor
just_mentor = LearningMentor.objects.create(
mentor=User.objects.get(id=TEST_MENTOR1_USER_ID),
course_session=cs,
)
just_mentor.participants.add(student_1_csu)
just_mentor.participants.add(mentor_and_student_2_learning_csu)
# TEST_STUDENT2_VV_AND_VV_MENTOR_USER_ID is both student and mentor
mentor_and_student_learning_mentor = LearningMentor.objects.create(
mentor=User.objects.get(id=TEST_STUDENT2_VV_AND_VV_MENTOR_USER_ID),
course_session=cs,
)
mentor_and_student_learning_mentor.participants.add(student_1_csu)
for admin_email in ADMIN_EMAILS:
CourseSessionUser.objects.create(

View File

@ -35,7 +35,7 @@ class CourseCompletionApiTestCase(APITestCase):
self.assertEqual(len(response.json()), 0)
def test_api_courseSession_withCourseSessionUser(self):
csu = CourseSessionUser.objects.create(
CourseSessionUser.objects.create(
course_session=self.course_session,
user=self.user,
)

View File

@ -155,8 +155,8 @@ def get_course_sessions(request):
# enrich with mentor course sessions
mentor_course_sessions = CourseSession.objects.filter(
course__in=LearningMentor.objects.filter(mentor=request.user).values_list(
"course", flat=True
id__in=LearningMentor.objects.filter(mentor=request.user).values_list(
"course_session", flat=True
)
).prefetch_related("course")

View File

@ -83,11 +83,16 @@ class DashboardQuery(graphene.ObjectType):
statistics_dashboard_course_ids,
) = get_user_statistics_dashboards(user=user)
course_session_dashboards = get_user_course_session_dashboards(
(
course_session_dashboards,
course_session_dashboard_course_ids,
) = get_user_course_session_dashboards(
user=user, exclude_course_ids=statistics_dashboard_course_ids
)
learning_mentor_dashboards = get_learning_mentor_dashboards(user=user)
learning_mentor_dashboards, _ = get_learning_mentor_dashboards(
user=user, exclude_course_ids=course_session_dashboard_course_ids
)
return (
statistic_dashboards
@ -163,13 +168,13 @@ class DashboardQuery(graphene.ObjectType):
def get_user_statistics_dashboards(user: User) -> Tuple[List[Dict[str, str]], Set[int]]:
course_index = set()
course_ids = set()
dashboards = []
for group in CourseSessionGroup.objects.all():
if can_view_course_session_group_statistics(user=user, group=group):
course = group.course
course_index.add(course)
course_ids.add(course)
dashboards.append(
{
"id": str(course.id),
@ -180,31 +185,38 @@ def get_user_statistics_dashboards(user: User) -> Tuple[List[Dict[str, str]], Se
}
)
return dashboards, course_index
return dashboards, course_ids
def get_learning_mentor_dashboards(user: User) -> List[Dict[str, str]]:
learning_mentor = LearningMentor.objects.filter(mentor=user)
def get_learning_mentor_dashboards(
user: User, exclude_course_ids: Set[int]
) -> Tuple[List[Dict[str, str]], Set[int]]:
learning_mentor = LearningMentor.objects.filter(mentor=user).exclude(
course_session__course__id__in=exclude_course_ids
)
dashboards = []
course_ids = set()
for mentor in learning_mentor:
course = mentor.course
course = mentor.course_session.course
course_ids.add(course.id)
dashboards.append(
{
"id": str(course.id),
"name": course.title,
"slug": course.slug,
"dashboard_type": DashboardType.SIMPLE_DASHBOARD,
"dashboard_type": DashboardType.MENTOR_DASHBOARD,
"course_configuration": course.configuration,
}
)
return dashboards
return dashboards, course_ids
def get_user_course_session_dashboards(
user: User, exclude_course_ids: Set[int]
) -> List[Dict[str, str]]:
) -> Tuple[List[Dict[str, str]], Set[int]]:
"""
Edge case: what do we show to users with access to multiple
sessions of a course, but with varying permissions?
@ -212,6 +224,7 @@ def get_user_course_session_dashboards(
"""
dashboards = []
course_ids = set()
course_sessions = CourseSession.objects.exclude(course__in=exclude_course_ids)
roles_by_course: Dict[Course, Set[DashboardType]] = {}
@ -237,6 +250,8 @@ def get_user_course_session_dashboards(
# fallback: just go with simple list dashboard
resolved_dashboard_type = DashboardType.SIMPLE_DASHBOARD
course_ids.add(course.id)
dashboards.append(
{
"id": str(course.id),
@ -247,4 +262,4 @@ def get_user_course_session_dashboards(
}
)
return dashboards
return dashboards, course_ids

View File

@ -53,6 +53,7 @@ class DashboardType(Enum):
STATISTICS_DASHBOARD = "StatisticsDashboard"
PROGRESS_DASHBOARD = "ProgressDashboard"
SIMPLE_DASHBOARD = "SimpleDashboard"
MENTOR_DASHBOARD = "MentorDashboard"
class DashboardConfigType(graphene.ObjectType):

View File

@ -268,7 +268,7 @@ class DashboardTestCase(GraphQLTestCase):
role=CourseSessionUser.Role.MEMBER,
)
add_learning_mentor(course=course_1, mentor=mentor, mentee=csu)
add_learning_mentor(course_session=cs_1, mentor=mentor, mentee=csu)
self.client.force_login(mentor)
@ -291,7 +291,55 @@ class DashboardTestCase(GraphQLTestCase):
self.assertEqual(len(response.json()["data"]["dashboard_config"]), 1)
self.assertEqual(
response.json()["data"]["dashboard_config"][0]["dashboard_type"],
"SIMPLE_DASHBOARD",
"MENTOR_DASHBOARD",
)
def test_mentor_and_member_mixed(self):
# GIVEN
course, _ = create_course("Test Course")
cs = create_course_session(course=course, title="Test Course Session 1")
# in same course session
mentor_and_member = create_user("mentor_and_member")
mentee = add_course_session_user(
course_session=cs,
user=create_user("mentee"),
role=CourseSessionUser.Role.MEMBER,
)
add_course_session_user(
course_session=cs,
user=mentor_and_member,
role=CourseSessionUser.Role.MEMBER,
)
add_learning_mentor(course_session=cs, mentor=mentor_and_member, mentee=mentee)
# WHEN
self.client.force_login(mentor_and_member)
query = """query {
dashboard_config {
id
name
slug
dashboard_type
}
}
"""
response = self.query(query)
# THEN
self.assertResponseNoErrors(response)
# given mentor + member -> should see the PROGRESS_DASHBOARD,
# not the MENTOR_DASHBOARD which is only for "pure" mentors.
self.assertEqual(len(response.json()["data"]["dashboard_config"]), 1)
self.assertEqual(
response.json()["data"]["dashboard_config"][0]["dashboard_type"],
"PROGRESS_DASHBOARD",
)
def test_course_statistics_deny_not_allowed_user(self):

View File

@ -22,7 +22,9 @@ def has_course_access(user, course_id):
).exists():
return True
if LearningMentor.objects.filter(course_id=course_id, mentor=user).exists():
if LearningMentor.objects.filter(
course_session__course_id=course_id, mentor=user
).exists():
return True
return CourseSessionUser.objects.filter(
@ -39,7 +41,45 @@ 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_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_session=course_session
).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()
@ -48,7 +88,7 @@ def is_user_mentor(mentor: User, participant_user_id: str, course_session_id: in
return False
return LearningMentor.objects.filter(
course_id=csu.course_session.course_id, mentor=mentor, participants=csu
course_session_id=course_session_id, mentor=mentor, participants=csu
).exists()
@ -98,7 +138,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,
@ -173,7 +213,9 @@ def has_role_in_course(user: User, course: Course) -> bool:
).exists():
return True
if LearningMentor.objects.filter(course=course, mentor=user).exists():
if LearningMentor.objects.filter(
course_session__course=course, mentor=user
).exists():
return True
if CourseSessionGroup.objects.filter(course=course, supervisor=user).exists():
@ -192,6 +234,50 @@ 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 has_learning_mentor(user: User, course_session_id: int) -> bool:
course_session = CourseSession.objects.get(id=course_session_id)
if course_session is None:
return False
if not course_session.course.configuration.enable_learning_mentor:
return False
if is_learning_mentor(user, course_session_id):
return True
if is_course_session_member(user, course_session_id):
return True
return False
def can_edit_mentors(user: User, course_session_id: int) -> bool:
if not has_learning_mentor(user, course_session_id):
return False
# limit further, since has_learning_mentor is too broad
return is_course_session_member(user, course_session_id)
def can_guide_members(user: User, course_session_id: int) -> bool:
if not has_learning_mentor(user, course_session_id):
return False
# limit further, since has_learning_mentor is too broad
return is_learning_mentor(user, course_session_id)
def can_view_profile(user: User, profile_user: CourseSessionUser) -> bool:
if user.is_superuser:
return True
@ -199,7 +285,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 +303,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 +320,17 @@ 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(
{
"learning-mentor": has_learning_mentor(user, course_session_id),
"learning-mentor::edit-mentors": can_edit_mentors(user, course_session_id),
"learning-mentor::guide-members": can_guide_members(
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": is_course_session_expert(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

@ -21,10 +21,7 @@ class ActionTestCase(TestCase):
def test_course_session_permissions(self):
# GIVEN
lm = create_user("mentor")
LearningMentor.objects.create(
mentor=lm,
course=self.course,
)
LearningMentor.objects.create(mentor=lm, course_session=self.course_session)
participant = create_user("participant")
add_course_session_user(
@ -48,6 +45,34 @@ class ActionTestCase(TestCase):
trainer_actions = course_session_permissions(trainer, self.course_session.id)
# THEN
self.assertEqual(len(mentor_actions), 0)
self.assertEqual(participant_actions, ["complete-learning-content"])
self.assertEqual(trainer_actions, ["complete-learning-content"])
self.assertEqual(
mentor_actions,
[
"learning-mentor",
"learning-mentor::guide-members",
"preview",
"appointments",
],
)
self.assertEqual(
participant_actions,
[
"learning-mentor",
"learning-mentor::edit-mentors",
"media-library",
"appointments",
"learning-path",
"competence-navi",
"complete-learning-content",
],
)
self.assertEqual(
trainer_actions,
[
"preview",
"media-library",
"appointments",
"expert-cockpit",
"complete-learning-content",
],
)

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
@ -50,7 +50,7 @@ class RoleTestCase(TestCase):
# GIVEN
LearningMentor.objects.create(
mentor=self.user,
course=self.course,
course_session=self.course_session,
)
# WHEN
@ -75,14 +75,14 @@ class RoleTestCase(TestCase):
learning_mentor = LearningMentor.objects.create(
mentor=mentor,
course=course,
course_session=course_session,
)
learning_mentor.participants.add(member_csu)
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,

View File

@ -10,7 +10,7 @@ class LearningMentorAdmin(admin.ModelAdmin):
participant_count.short_description = "Participants"
list_display = ["mentor", "course", "participant_count"]
list_display = ["mentor", "course_session", "participant_count"]
search_fields = ["mentor__email"]

View File

@ -72,7 +72,7 @@ def get_assignment_completions(
)[0],
user_id=user.id,
last_name=user.last_name,
url=f"/course/{course_session.course.slug}/cockpit/assignment/{assignment.id}/{user.id}",
url=f"/course/{course_session.course.slug}/assignment-evaluation/{assignment.id}/{user.id}",
)
for user in sorted_participants
]
@ -110,19 +110,19 @@ def get_praxis_assignments(
]
)
circle_id = learning_content.get_circle().id
circle = learning_content.get_circle()
circle_ids.add(circle.id)
records.append(
MentorAssignmentStatus(
id=course_session_assignment.id,
title=learning_content.content_assignment.title,
circle_id=circle_id,
circle_id=circle.id,
circle_name=circle.title,
pending_evaluations=submitted_count,
completions=completions,
type=MentorAssignmentStatusType.PRAXIS_ASSIGNMENT,
)
)
circle_ids.add(circle_id)
return records, circle_ids

View File

@ -64,9 +64,6 @@ def get_self_feedback_evaluation(
feedback_provider_user=evaluation_user,
)
circle_id = learning_unit.get_circle().id
circle_ids.add(circle_id)
pending_evaluations = len([f for f in feedbacks if not f.feedback_submitted])
completions = [
@ -90,11 +87,15 @@ def get_self_feedback_evaluation(
participants=participants,
)
circle = learning_unit.get_circle()
circle_ids.add(circle.id)
records.append(
MentorAssignmentStatus(
id=learning_unit.id,
title=learning_unit.title,
circle_id=circle_id,
circle_id=circle.id,
circle_name=circle.title,
pending_evaluations=pending_evaluations,
completions=completions,
type=MentorAssignmentStatusType.SELF_EVALUATION_FEEDBACK,

View File

@ -27,6 +27,7 @@ class MentorAssignmentStatus:
id: str
title: str
circle_id: str
circle_name: str
pending_evaluations: int
completions: List[MentorAssignmentCompletion]
type: MentorAssignmentStatusType

View File

@ -0,0 +1,57 @@
# Generated by Django 3.2.20 on 2024-03-19 09:58
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
def reverse_migrate_course_session_to_course(apps, schema_editor):
LearningMentor = apps.get_model("learning_mentor", "LearningMentor")
for lm in LearningMentor.objects.all():
lm.course = lm.course_session.course
lm.save()
def migrate_course_to_course_session(apps, schema_editor):
LearningMentor = apps.get_model("learning_mentor", "LearningMentor")
CourseSession = apps.get_model("course", "CourseSession")
for lm in LearningMentor.objects.all():
# first is fine for VV there is only one course session per course
course_session = CourseSession.objects.filter(course=lm.course).first()
lm.course_session = course_session
lm.save()
class Migration(migrations.Migration):
dependencies = [
("course", "0007_auto_20240226_1553"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("learning_mentor", "0005_alter_learningmentor_mentor"),
]
operations = [
migrations.AddField(
model_name="learningmentor",
name="course_session",
field=models.ForeignKey(
# this is a dummy value, it will be replaced by the migration
default=-1,
on_delete=django.db.models.deletion.CASCADE,
to="course.coursesession",
),
preserve_default=False,
),
migrations.RunPython(
code=migrate_course_to_course_session,
reverse_code=reverse_migrate_course_session_to_course,
),
migrations.AlterUniqueTogether(
name="learningmentor",
unique_together={("mentor", "course_session")},
),
migrations.RemoveField(
model_name="learningmentor",
name="course",
),
]

View File

@ -9,7 +9,7 @@ from vbv_lernwelt.course.models import CourseSessionUser
class LearningMentor(models.Model):
mentor = models.ForeignKey(User, on_delete=models.CASCADE)
course = models.ForeignKey("course.Course", on_delete=models.CASCADE)
course_session = models.ForeignKey("course.CourseSession", on_delete=models.CASCADE)
participants = models.ManyToManyField(
CourseSessionUser,
@ -18,12 +18,12 @@ class LearningMentor(models.Model):
)
class Meta:
unique_together = [["mentor", "course"]]
unique_together = [["mentor", "course_session"]]
verbose_name = "Lernbegleiter"
verbose_name_plural = "Lernbegleiter"
def __str__(self):
return f"{self.mentor} ({self.course.title})"
return f"{self.mentor} ({self.course_session.title})"
@property
def course_sessions(self):

View File

@ -19,6 +19,7 @@ class MentorAssignmentStatusSerializer(serializers.Serializer):
id = serializers.CharField()
title = serializers.CharField()
circle_id = serializers.CharField()
circle_name = serializers.CharField()
pending_evaluations = serializers.IntegerField()
completions = MentorAssignmentCompletionSerializer(many=True)
type = serializers.ReadOnlyField()

View File

@ -10,7 +10,9 @@ def validate_student(sender, instance, action, reverse, model, pk_set, **kwargs)
if action == "pre_add":
participants = model.objects.filter(pk__in=pk_set)
for participant in participants:
if participant.course_session.course != instance.course:
if participant.course_session != instance.course_session:
raise ValidationError(
"Participant (CourseSessionUser) does not match the course for this mentor."
)
if participant.user == instance.mentor:
raise ValidationError("You cannot mentor yourself.")

View File

@ -86,7 +86,7 @@ class AttendanceServicesTestCase(TestCase):
self.assertEqual(result.status, expected_statuses[result.last_name])
self.assertEqual(
result.url,
f"/course/test-lehrgang/cockpit/assignment/{self.assignment.id}/{result.user_id}",
f"/course/test-lehrgang/assignment-evaluation/{self.assignment.id}/{result.user_id}",
)
def test_praxis_assignment_status(self):
@ -108,5 +108,6 @@ class AttendanceServicesTestCase(TestCase):
assignment = assignments[0]
self.assertEqual(assignment.pending_evaluations, 1)
self.assertEqual(assignment.title, "Dummy Assignment (PRAXIS_ASSIGNMENT)")
self.assertEqual(assignment.circle_name, "Circle")
self.assertEqual(assignment.circle_id, self.circle.id)
self.assertEqual(list(circle_ids)[0], self.circle.id)

View File

@ -35,6 +35,34 @@ class LearningMentorInvitationTest(APITestCase):
# THEN
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
@patch("vbv_lernwelt.learning_mentor.views.send_email")
def test_create_denies_self_invitation(self, mock_send_mail) -> None:
# GIVEN
self.client.force_login(self.participant)
add_course_session_user(
self.course_session,
self.participant,
role=CourseSessionUser.Role.MEMBER,
)
invite_url = reverse(
"create_invitation", kwargs={"course_session_id": self.course_session.id}
)
# WHEN
email = self.participant.email
response = self.client.post(invite_url, data={"email": email})
# THEN
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(
response.data,
{
"message": "You cannot invite yourself",
},
)
@patch("vbv_lernwelt.learning_mentor.views.send_email")
def test_create_invitation(self, mock_send_mail) -> None:
# GIVEN
@ -164,49 +192,11 @@ class LearningMentorInvitationTest(APITestCase):
},
)
def test_accept_invitation_role_collision(self) -> None:
# GIVEN
participant_cs_user = add_course_session_user(
self.course_session,
self.participant,
role=CourseSessionUser.Role.MEMBER,
)
invitee = create_user("invitee")
invitation = MentorInvitation.objects.create(
participant=participant_cs_user, email=invitee.email
)
# Make invitee a trainer
add_course_session_user(
self.course_session,
invitee,
role=CourseSessionUser.Role.EXPERT,
)
self.client.force_login(invitee)
accept_url = reverse(
"accept_invitation", kwargs={"course_session_id": self.course_session.id}
)
# WHEN
response = self.client.post(accept_url, data={"invitation_id": invitation.id})
# THEN
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(
response.data,
{
"message": "User already has a role in this course",
"code": "existingRole",
},
)
def test_accept_invitation(self) -> None:
# GIVEN
participant_cs_user = add_course_session_user(
self.course_session,
self.participant,
course_session=self.course_session,
user=self.participant,
role=CourseSessionUser.Role.MEMBER,
)
@ -229,7 +219,9 @@ class LearningMentorInvitationTest(APITestCase):
self.assertFalse(MentorInvitation.objects.filter(id=invitation.id).exists())
self.assertTrue(
LearningMentor.objects.filter(
mentor=invitee, course=self.course, participants=participant_cs_user
mentor=invitee,
course_session=self.course_session,
participants=participant_cs_user,
).exists()
)

View File

@ -77,7 +77,7 @@ class LearningMentorAPITest(APITestCase):
# GIVEN
self.client.force_login(self.mentor)
LearningMentor.objects.create(
mentor=self.mentor, course=self.course_session.course
mentor=self.mentor, course_session=self.course_session
)
# WHEN
@ -93,8 +93,7 @@ class LearningMentorAPITest(APITestCase):
participants = [self.participant_1, self.participant_2, self.participant_3]
self.client.force_login(self.mentor)
mentor = LearningMentor.objects.create(
mentor=self.mentor,
course=self.course_session.course,
mentor=self.mentor, course_session=self.course_session
)
mentor.participants.set(participants)
@ -114,14 +113,18 @@ class LearningMentorAPITest(APITestCase):
self.assertEqual(participant_1["first_name"], "Test")
self.assertEqual(participant_1["last_name"], "Participant_1")
self.assertEqual(
response.data["mentor_id"],
mentor.id,
)
def test_api_self_evaluation_feedback(self) -> None:
# GIVEN
participants = [self.participant_1, self.participant_2, self.participant_3]
self.client.force_login(self.mentor)
mentor = LearningMentor.objects.create(
mentor=self.mentor,
course=self.course_session.course,
mentor=self.mentor, course_session=self.course_session
)
mentor.participants.set(participants)
@ -204,7 +207,7 @@ class LearningMentorAPITest(APITestCase):
mentor = LearningMentor.objects.create(
mentor=self.mentor,
course=self.course_session.course,
course_session=self.course_session,
)
participants = [self.participant_1, self.participant_2, self.participant_3]
@ -265,8 +268,7 @@ class LearningMentorAPITest(APITestCase):
)
learning_mentor = LearningMentor.objects.create(
mentor=self.mentor,
course=self.course_session.course,
mentor=self.mentor, course_session=self.course_session
)
learning_mentor.participants.add(participant_cs_user)
@ -288,7 +290,41 @@ class LearningMentorAPITest(APITestCase):
self.assertEqual(mentor_user["email"], self.mentor.email)
self.assertEqual(mentor_user["id"], str(self.mentor.id))
def test_remove_user_mentor(self) -> None:
def test_remove_participant_as_mentor(self) -> None:
# GIVEN
self.client.force_login(self.mentor)
participant_cs_user = add_course_session_user(
self.course_session,
create_user("participant"),
role=CourseSessionUser.Role.MEMBER,
)
learning_mentor = LearningMentor.objects.create(
mentor=self.mentor, course_session=self.course_session
)
learning_mentor.participants.add(participant_cs_user)
remove_url = reverse(
"remove_participant_from_mentor",
kwargs={
"course_session_id": self.course_session.id,
"mentor_id": learning_mentor.id,
"participant_user_id": participant_cs_user.user.id,
},
)
# WHEN
response = self.client.delete(remove_url)
# THEN
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
self.assertFalse(
LearningMentor.objects.filter(participants=participant_cs_user).exists()
)
def test_remove_myself_from_mentor(self) -> None:
# GIVEN
participant = create_user("participant")
self.client.force_login(participant)
@ -300,22 +336,22 @@ class LearningMentorAPITest(APITestCase):
)
learning_mentor = LearningMentor.objects.create(
mentor=self.mentor,
course=self.course_session.course,
mentor=self.mentor, course_session=self.course_session
)
learning_mentor.participants.add(participant_cs_user)
remove_self_url = reverse(
"remove_self_from_mentor",
remove_url = reverse(
"remove_participant_from_mentor",
kwargs={
"course_session_id": self.course_session.id,
"mentor_id": learning_mentor.id,
"participant_user_id": participant_cs_user.user.id,
},
)
# WHEN
response = self.client.delete(remove_self_url)
response = self.client.delete(remove_url)
# THEN
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
@ -326,11 +362,19 @@ class LearningMentorAPITest(APITestCase):
def test_mentor_multiple_courses(self) -> None:
# GIVEN
course_a, _ = create_course("Course A")
course_session_a = create_course_session(course=course_a, title="Test A")
course_b, _ = create_course("Course B")
course_session_b = create_course_session(course=course_b, title="Test B")
# WHEN
LearningMentor.objects.create(mentor=self.mentor, course=course_a)
LearningMentor.objects.create(mentor=self.mentor, course=course_b)
LearningMentor.objects.create(
mentor=self.mentor, course_session=course_session_a
)
LearningMentor.objects.create(
mentor=self.mentor, course_session=course_session_b
)
# THEN
self.assertEqual(LearningMentor.objects.count(), 2)

View File

@ -6,9 +6,9 @@ urlpatterns = [
path("summary", views.mentor_summary, name="mentor_summary"),
path("mentors", views.list_user_mentors, name="list_user_mentors"),
path(
"mentors/<int:mentor_id>/leave",
views.remove_self_from_mentor,
name="remove_self_from_mentor",
"mentors/<int:mentor_id>/remove/<uuid:participant_user_id>",
views.remove_participant_from_mentor,
name="remove_participant_from_mentor",
),
path("invitations", views.list_invitations, name="list_invitations"),
path("invitations/create", views.create_invitation, name="create_invitation"),

Some files were not shown because too many files have changed in this diff Show More