diff --git a/client/src/components/header/MainNavigationBar.vue b/client/src/components/header/MainNavigationBar.vue index 836f8f40..8234380e 100644 --- a/client/src/components/header/MainNavigationBar.vue +++ b/client/src/components/header/MainNavigationBar.vue @@ -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") }} + + + {{ t("a.Lernbegleitung") }} + diff --git a/client/src/pages/learningMentor/MentorManagementPage.vue b/client/src/pages/learningMentor/MentorManagementPage.vue new file mode 100644 index 00000000..e444cf55 --- /dev/null +++ b/client/src/pages/learningMentor/MentorManagementPage.vue @@ -0,0 +1,88 @@ + + + diff --git a/client/src/router/index.ts b/client/src/router/index.ts index a9bb8728..9a07c64d 100644 --- a/client/src/router/index.ts +++ b/client/src/router/index.ts @@ -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, diff --git a/client/src/utils/route.ts b/client/src/utils/route.ts index 8f2a9b69..1ae3c477 100644 --- a/client/src/utils/route.ts +++ b/client/src/utils/route.ts @@ -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, }; diff --git a/client/src/utils/utils.ts b/client/src/utils/utils.ts index f95ddc5a..75164a9d 100644 --- a/client/src/utils/utils.ts +++ b/client/src/utils/utils.ts @@ -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(); diff --git a/server/vbv_lernwelt/iam/permissions.py b/server/vbv_lernwelt/iam/permissions.py index c82510fe..e0b0fd92 100644 --- a/server/vbv_lernwelt/iam/permissions.py +++ b/server/vbv_lernwelt/iam/permissions.py @@ -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 diff --git a/server/vbv_lernwelt/iam/tests/__init__.py b/server/vbv_lernwelt/iam/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/server/vbv_lernwelt/iam/tests/test_roles.py b/server/vbv_lernwelt/iam/tests/test_roles.py new file mode 100644 index 00000000..088d145e --- /dev/null +++ b/server/vbv_lernwelt/iam/tests/test_roles.py @@ -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) diff --git a/server/vbv_lernwelt/learning_mentor/tests/test_invitation.py b/server/vbv_lernwelt/learning_mentor/tests/test_invitation.py index 34558583..182787f9 100644 --- a/server/vbv_lernwelt/learning_mentor/tests/test_invitation.py +++ b/server/vbv_lernwelt/learning_mentor/tests/test_invitation.py @@ -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)) diff --git a/server/vbv_lernwelt/learning_mentor/views.py b/server/vbv_lernwelt/learning_mentor/views.py index 7c9c58b2..3ee73690 100644 --- a/server/vbv_lernwelt/learning_mentor/views.py +++ b/server/vbv_lernwelt/learning_mentor/views.py @@ -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)