feat: learning mentor mgmt UI
This commit is contained in:
parent
e2c32b7fb6
commit
6bd913307c
|
|
@ -18,6 +18,7 @@ import CoursePreviewBar from "@/components/header/CoursePreviewBar.vue";
|
||||||
import {
|
import {
|
||||||
getCockpitUrl,
|
getCockpitUrl,
|
||||||
getCompetenceNaviUrl,
|
getCompetenceNaviUrl,
|
||||||
|
getLearningMentorManagementUrl,
|
||||||
getLearningPathUrl,
|
getLearningPathUrl,
|
||||||
getMediaCenterUrl,
|
getMediaCenterUrl,
|
||||||
} from "@/utils/utils";
|
} from "@/utils/utils";
|
||||||
|
|
@ -31,6 +32,7 @@ const notificationsStore = useNotificationsStore();
|
||||||
const {
|
const {
|
||||||
inCockpit,
|
inCockpit,
|
||||||
inCompetenceProfile,
|
inCompetenceProfile,
|
||||||
|
inLearningMentor,
|
||||||
inCourse,
|
inCourse,
|
||||||
inLearningPath,
|
inLearningPath,
|
||||||
inMediaLibrary,
|
inMediaLibrary,
|
||||||
|
|
@ -189,6 +191,19 @@ onMounted(() => {
|
||||||
>
|
>
|
||||||
{{ t("competences.title") }}
|
{{ t("competences.title") }}
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
|
<router-link
|
||||||
|
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>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useFetch } from "@vueuse/core";
|
||||||
|
import { useCurrentCourseSession } from "@/composables";
|
||||||
|
import ItModal from "@/components/ui/ItModal.vue";
|
||||||
|
import { computed, ref } from "vue";
|
||||||
|
|
||||||
|
const courseSession = useCurrentCourseSession();
|
||||||
|
|
||||||
|
const showInvitationModal = ref(false);
|
||||||
|
|
||||||
|
const { data: mentors } = useFetch(
|
||||||
|
`/api/mentor/${courseSession.value.course.id}/mentors`
|
||||||
|
).json();
|
||||||
|
|
||||||
|
const { data: invitations } = useFetch(
|
||||||
|
`/api/mentor/${courseSession.value.course.id}/invitations`
|
||||||
|
).json();
|
||||||
|
|
||||||
|
const hasMentors = computed(() => {
|
||||||
|
return (
|
||||||
|
(mentors.value && mentors.value.length > 0) ||
|
||||||
|
(invitations.value && invitations.value.length > 0)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
</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-row justify-between gap-4 border-b py-4"
|
||||||
|
>
|
||||||
|
{{ 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>
|
||||||
|
<button class="underline">
|
||||||
|
{{ $t("a.Entfernen") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="mentor in mentors"
|
||||||
|
:key="mentor.id"
|
||||||
|
class="flex flex-col justify-between gap-4 border-b py-4"
|
||||||
|
>
|
||||||
|
{{ mentor.name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="!hasMentors" class="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>hallo</template>
|
||||||
|
</ItModal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -121,6 +121,11 @@ const router = createRouter({
|
||||||
import("../pages/learningPath/learningContentPage/LearningContentPage.vue"),
|
import("../pages/learningPath/learningContentPage/LearningContentPage.vue"),
|
||||||
props: true,
|
props: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/course/:courseSlug/mentor",
|
||||||
|
component: () => import("@/pages/learningMentor/MentorManagementPage.vue"),
|
||||||
|
props: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/course/:courseSlug/cockpit",
|
path: "/course/:courseSlug/cockpit",
|
||||||
props: true,
|
props: true,
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,11 @@ export function useRouteLookups() {
|
||||||
return regex.test(route.path);
|
return regex.test(route.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function inLearningMentor() {
|
||||||
|
const regex = new RegExp("/course/[^/]+/mentor");
|
||||||
|
return regex.test(route.path);
|
||||||
|
}
|
||||||
|
|
||||||
function inMediaLibrary() {
|
function inMediaLibrary() {
|
||||||
const regex = new RegExp("/course/[^/]+/media");
|
const regex = new RegExp("/course/[^/]+/media");
|
||||||
return regex.test(route.path);
|
return regex.test(route.path);
|
||||||
|
|
@ -37,6 +42,7 @@ export function useRouteLookups() {
|
||||||
inCockpit,
|
inCockpit,
|
||||||
inLearningPath,
|
inLearningPath,
|
||||||
inCompetenceProfile,
|
inCompetenceProfile,
|
||||||
|
inLearningMentor,
|
||||||
inCourse,
|
inCourse,
|
||||||
inAppointments: inAppointments,
|
inAppointments: inAppointments,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ function createCourseUrl(courseSlug: string | undefined, specificSub: string): s
|
||||||
return "/";
|
return "/";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (["learn", "media", "competence", "cockpit"].includes(specificSub)) {
|
if (["learn", "media", "competence", "cockpit", "mentor"].includes(specificSub)) {
|
||||||
return `/course/${courseSlug}/${specificSub}`;
|
return `/course/${courseSlug}/${specificSub}`;
|
||||||
}
|
}
|
||||||
return `/course/${courseSlug}`;
|
return `/course/${courseSlug}`;
|
||||||
|
|
@ -36,6 +36,10 @@ export function getCockpitUrl(courseSlug: string | undefined): string {
|
||||||
return createCourseUrl(courseSlug, "cockpit");
|
return createCourseUrl(courseSlug, "cockpit");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getLearningMentorManagementUrl(courseSlug: string | undefined): string {
|
||||||
|
return createCourseUrl(courseSlug, "mentor");
|
||||||
|
}
|
||||||
|
|
||||||
export function getAssignmentTypeTitle(assignmentType: AssignmentType): string {
|
export function getAssignmentTypeTitle(assignmentType: AssignmentType): string {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
from vbv_lernwelt.core.models import User
|
from vbv_lernwelt.core.models import User
|
||||||
from vbv_lernwelt.course.models import Course, CourseSession, CourseSessionUser
|
from vbv_lernwelt.course.models import Course, CourseSession, CourseSessionUser
|
||||||
from vbv_lernwelt.course_session_group.models import CourseSessionGroup
|
from vbv_lernwelt.course_session_group.models import CourseSessionGroup
|
||||||
|
from vbv_lernwelt.learning_mentor.models import LearningMentor
|
||||||
from vbv_lernwelt.learnpath.models import LearningSequence
|
from vbv_lernwelt.learnpath.models import LearningSequence
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -137,8 +138,15 @@ def can_view_course_session(user: User, course_session: CourseSession) -> bool:
|
||||||
|
|
||||||
|
|
||||||
def has_role_in_course(user: User, course: Course) -> bool:
|
def has_role_in_course(user: User, course: Course) -> bool:
|
||||||
"""
|
if CourseSessionUser.objects.filter(
|
||||||
Test for regio leiter, member, trainer...
|
course_session__course=course, user=user
|
||||||
"""
|
).exists():
|
||||||
...
|
return True
|
||||||
return True
|
|
||||||
|
if LearningMentor.objects.filter(course=course, mentor=user).exists():
|
||||||
|
return True
|
||||||
|
|
||||||
|
if CourseSessionGroup.objects.filter(course=course, supervisor=user).exists():
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from vbv_lernwelt.course.creators.test_utils import (
|
||||||
|
add_course_session_user,
|
||||||
|
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.iam.permissions import has_role_in_course
|
||||||
|
from vbv_lernwelt.learning_mentor.models import LearningMentor
|
||||||
|
|
||||||
|
|
||||||
|
class AnimalTestCase(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.course, _ = create_course("Test Course")
|
||||||
|
self.course_session = create_course_session(
|
||||||
|
course=self.course, title="Test Session"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.user = create_user("user")
|
||||||
|
|
||||||
|
def test_has_role_regional(self):
|
||||||
|
# GIVEN
|
||||||
|
csg = CourseSessionGroup.objects.create(name="Test Group", course=self.course)
|
||||||
|
csg.supervisor.add(self.user)
|
||||||
|
|
||||||
|
# WHEN
|
||||||
|
has_role = has_role_in_course(user=self.user, course=self.course)
|
||||||
|
|
||||||
|
# THEN
|
||||||
|
self.assertTrue(has_role)
|
||||||
|
|
||||||
|
def test_has_role_course_session(self):
|
||||||
|
# GIVEN
|
||||||
|
add_course_session_user(
|
||||||
|
self.course_session,
|
||||||
|
self.user,
|
||||||
|
role=CourseSessionUser.Role.MEMBER,
|
||||||
|
)
|
||||||
|
|
||||||
|
# WHEN
|
||||||
|
has_role = has_role_in_course(user=self.user, course=self.course)
|
||||||
|
|
||||||
|
# THEN
|
||||||
|
self.assertTrue(has_role)
|
||||||
|
|
||||||
|
def test_has_role_mentor(self):
|
||||||
|
# GIVEN
|
||||||
|
LearningMentor.objects.create(
|
||||||
|
mentor=self.user,
|
||||||
|
course=self.course,
|
||||||
|
)
|
||||||
|
|
||||||
|
# WHEN
|
||||||
|
has_role = has_role_in_course(user=self.user, course=self.course)
|
||||||
|
|
||||||
|
# THEN
|
||||||
|
self.assertTrue(has_role)
|
||||||
|
|
||||||
|
def test_no_role(self):
|
||||||
|
# GIVEN
|
||||||
|
other_course, _ = create_course("Other Test Course")
|
||||||
|
other_course_session = create_course_session(
|
||||||
|
course=other_course, title="Other Test Session"
|
||||||
|
)
|
||||||
|
add_course_session_user(
|
||||||
|
other_course_session,
|
||||||
|
self.user,
|
||||||
|
role=CourseSessionUser.Role.MEMBER,
|
||||||
|
)
|
||||||
|
|
||||||
|
# WHEN
|
||||||
|
has_role = has_role_in_course(user=self.user, course=self.course)
|
||||||
|
|
||||||
|
# THEN
|
||||||
|
self.assertFalse(has_role)
|
||||||
|
|
@ -11,7 +11,7 @@ from vbv_lernwelt.course.creators.test_utils import (
|
||||||
create_user,
|
create_user,
|
||||||
)
|
)
|
||||||
from vbv_lernwelt.course.models import CourseSessionUser
|
from vbv_lernwelt.course.models import CourseSessionUser
|
||||||
from vbv_lernwelt.learning_mentor.models import MentorInvitation
|
from vbv_lernwelt.learning_mentor.models import LearningMentor, MentorInvitation
|
||||||
from vbv_lernwelt.notify.email.email_services import EmailTemplate
|
from vbv_lernwelt.notify.email.email_services import EmailTemplate
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -129,17 +129,107 @@ class LearningMentorInvitationTest(APITestCase):
|
||||||
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
|
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
|
||||||
self.assertFalse(MentorInvitation.objects.filter(id=invitation.id).exists())
|
self.assertFalse(MentorInvitation.objects.filter(id=invitation.id).exists())
|
||||||
|
|
||||||
def test_accept_invitation(self) -> None:
|
def test_accept_invitation_invalid_course(self) -> None:
|
||||||
# GIVEN
|
# GIVEN
|
||||||
|
other_course, _ = create_course("Other Test Course")
|
||||||
|
other_course_session = create_course_session(
|
||||||
|
course=other_course, title="Other Test Session"
|
||||||
|
)
|
||||||
|
participant_cs_user = add_course_session_user(
|
||||||
|
self.course_session,
|
||||||
|
self.participant,
|
||||||
|
role=CourseSessionUser.Role.MEMBER,
|
||||||
|
)
|
||||||
|
|
||||||
invitee = create_user("invitee")
|
invitee = create_user("invitee")
|
||||||
|
invitation = MentorInvitation.objects.create(
|
||||||
|
participant=participant_cs_user, email=invitee.email
|
||||||
|
)
|
||||||
self.client.force_login(invitee)
|
self.client.force_login(invitee)
|
||||||
|
|
||||||
accept_url = reverse(
|
accept_url = reverse(
|
||||||
"create_invitation", kwargs={"course_session_id": self.course_session.id}
|
"accept_invitation", kwargs={"course_session_id": other_course_session.id}
|
||||||
)
|
)
|
||||||
|
|
||||||
# WHEN
|
# WHEN
|
||||||
response = self.client.get(accept_url)
|
response = self.client.post(accept_url, data={"invitation_id": invitation.id})
|
||||||
|
|
||||||
# THEN
|
# THEN
|
||||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertEqual(
|
||||||
|
response.data,
|
||||||
|
{
|
||||||
|
"message": "Invalid invitation",
|
||||||
|
"code": "invalidInvitation",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
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,
|
||||||
|
role=CourseSessionUser.Role.MEMBER,
|
||||||
|
)
|
||||||
|
|
||||||
|
invitee = create_user("invitee")
|
||||||
|
invitation = MentorInvitation.objects.create(
|
||||||
|
participant=participant_cs_user, email=invitee.email
|
||||||
|
)
|
||||||
|
|
||||||
|
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_200_OK)
|
||||||
|
self.assertFalse(MentorInvitation.objects.filter(id=invitation.id).exists())
|
||||||
|
self.assertTrue(
|
||||||
|
LearningMentor.objects.filter(
|
||||||
|
mentor=invitee, course=self.course, participants=participant_cs_user
|
||||||
|
).exists()
|
||||||
|
)
|
||||||
|
self.assertEqual(response.data["id"], str(self.participant.id))
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ from rest_framework.response import Response
|
||||||
|
|
||||||
from vbv_lernwelt.core.serializers import UserSerializer
|
from vbv_lernwelt.core.serializers import UserSerializer
|
||||||
from vbv_lernwelt.course.models import CourseSession, CourseSessionUser
|
from vbv_lernwelt.course.models import CourseSession, CourseSessionUser
|
||||||
from vbv_lernwelt.iam.permissions import is_course_session_member
|
from vbv_lernwelt.iam.permissions import has_role_in_course, is_course_session_member
|
||||||
from vbv_lernwelt.learning_mentor.content.praxis_assignment import (
|
from vbv_lernwelt.learning_mentor.content.praxis_assignment import (
|
||||||
get_praxis_assignments,
|
get_praxis_assignments,
|
||||||
)
|
)
|
||||||
|
|
@ -160,13 +160,31 @@ def accept_invitation(request, course_session_id: int):
|
||||||
|
|
||||||
if invitation.participant.course_session != course_session:
|
if invitation.participant.course_session != course_session:
|
||||||
return Response(
|
return Response(
|
||||||
data={"message": "Invalid invitation"}, status=status.HTTP_400_BAD_REQUEST
|
data={"message": "Invalid invitation", "code": "invalidInvitation"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
mentor, _ = LearningMentor.objects.get_or_create(
|
if LearningMentor.objects.filter(
|
||||||
mentor=request.user, course=course_session.course
|
mentor=request.user, course=course_session.course
|
||||||
)
|
).exists():
|
||||||
|
mentor = LearningMentor.objects.get(
|
||||||
|
mentor=request.user, course=course_session.course
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if has_role_in_course(request.user, course_session.course):
|
||||||
|
return Response(
|
||||||
|
data={
|
||||||
|
"message": "User already has a role in this course",
|
||||||
|
"code": "existingRole",
|
||||||
|
},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
mentor = LearningMentor.objects.create(
|
||||||
|
mentor=request.user, course=course_session.course
|
||||||
|
)
|
||||||
|
|
||||||
mentor.participants.add(invitation.participant)
|
mentor.participants.add(invitation.participant)
|
||||||
|
invitation.delete()
|
||||||
|
|
||||||
return Response({})
|
return Response(UserSerializer(invitation.participant.user).data)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue