feat: learning mentor mgmt UI

This commit is contained in:
Reto Aebersold 2023-12-12 10:01:11 +01:00
parent e2c32b7fb6
commit 6bd913307c
10 changed files with 328 additions and 16 deletions

View File

@ -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>

View File

@ -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>

View File

@ -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,

View File

@ -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,
};

View File

@ -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();

View File

@ -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

View File

@ -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)

View File

@ -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))

View File

@ -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)