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 {
|
||||
getCockpitUrl,
|
||||
getCompetenceNaviUrl,
|
||||
getLearningMentorManagementUrl,
|
||||
getLearningPathUrl,
|
||||
getMediaCenterUrl,
|
||||
} from "@/utils/utils";
|
||||
|
|
@ -31,6 +32,7 @@ const notificationsStore = useNotificationsStore();
|
|||
const {
|
||||
inCockpit,
|
||||
inCompetenceProfile,
|
||||
inLearningMentor,
|
||||
inCourse,
|
||||
inLearningPath,
|
||||
inMediaLibrary,
|
||||
|
|
@ -189,6 +191,19 @@ onMounted(() => {
|
|||
>
|
||||
{{ t("competences.title") }}
|
||||
</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>
|
||||
</div>
|
||||
</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"),
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "/course/:courseSlug/mentor",
|
||||
component: () => import("@/pages/learningMentor/MentorManagementPage.vue"),
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "/course/:courseSlug/cockpit",
|
||||
props: true,
|
||||
|
|
|
|||
|
|
@ -22,6 +22,11 @@ export function useRouteLookups() {
|
|||
return regex.test(route.path);
|
||||
}
|
||||
|
||||
function inLearningMentor() {
|
||||
const regex = new RegExp("/course/[^/]+/mentor");
|
||||
return regex.test(route.path);
|
||||
}
|
||||
|
||||
function inMediaLibrary() {
|
||||
const regex = new RegExp("/course/[^/]+/media");
|
||||
return regex.test(route.path);
|
||||
|
|
@ -37,6 +42,7 @@ export function useRouteLookups() {
|
|||
inCockpit,
|
||||
inLearningPath,
|
||||
inCompetenceProfile,
|
||||
inLearningMentor,
|
||||
inCourse,
|
||||
inAppointments: inAppointments,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ function createCourseUrl(courseSlug: string | undefined, specificSub: string): s
|
|||
return "/";
|
||||
}
|
||||
|
||||
if (["learn", "media", "competence", "cockpit"].includes(specificSub)) {
|
||||
if (["learn", "media", "competence", "cockpit", "mentor"].includes(specificSub)) {
|
||||
return `/course/${courseSlug}/${specificSub}`;
|
||||
}
|
||||
return `/course/${courseSlug}`;
|
||||
|
|
@ -36,6 +36,10 @@ export function getCockpitUrl(courseSlug: string | undefined): string {
|
|||
return createCourseUrl(courseSlug, "cockpit");
|
||||
}
|
||||
|
||||
export function getLearningMentorManagementUrl(courseSlug: string | undefined): string {
|
||||
return createCourseUrl(courseSlug, "mentor");
|
||||
}
|
||||
|
||||
export function getAssignmentTypeTitle(assignmentType: AssignmentType): string {
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from vbv_lernwelt.core.models import User
|
||||
from vbv_lernwelt.course.models import Course, CourseSession, CourseSessionUser
|
||||
from vbv_lernwelt.course_session_group.models import CourseSessionGroup
|
||||
from vbv_lernwelt.learning_mentor.models import LearningMentor
|
||||
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:
|
||||
"""
|
||||
Test for regio leiter, member, trainer...
|
||||
"""
|
||||
...
|
||||
return True
|
||||
if CourseSessionUser.objects.filter(
|
||||
course_session__course=course, user=user
|
||||
).exists():
|
||||
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,
|
||||
)
|
||||
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
|
||||
|
||||
|
||||
|
|
@ -129,17 +129,107 @@ class LearningMentorInvitationTest(APITestCase):
|
|||
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
|
||||
self.assertFalse(MentorInvitation.objects.filter(id=invitation.id).exists())
|
||||
|
||||
def test_accept_invitation(self) -> None:
|
||||
def test_accept_invitation_invalid_course(self) -> None:
|
||||
# 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")
|
||||
invitation = MentorInvitation.objects.create(
|
||||
participant=participant_cs_user, email=invitee.email
|
||||
)
|
||||
self.client.force_login(invitee)
|
||||
|
||||
accept_url = reverse(
|
||||
"create_invitation", kwargs={"course_session_id": self.course_session.id}
|
||||
"accept_invitation", kwargs={"course_session_id": other_course_session.id}
|
||||
)
|
||||
|
||||
# WHEN
|
||||
response = self.client.get(accept_url)
|
||||
response = self.client.post(accept_url, data={"invitation_id": invitation.id})
|
||||
|
||||
# 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.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 (
|
||||
get_praxis_assignments,
|
||||
)
|
||||
|
|
@ -160,13 +160,31 @@ def accept_invitation(request, course_session_id: int):
|
|||
|
||||
if invitation.participant.course_session != course_session:
|
||||
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
|
||||
)
|
||||
).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)
|
||||
invitation.delete()
|
||||
|
||||
return Response({})
|
||||
return Response(UserSerializer(invitation.participant.user).data)
|
||||
|
|
|
|||
Loading…
Reference in New Issue