From 8cff12fc102e97a3941ee4a05625d8426150f4d5 Mon Sep 17 00:00:00 2001 From: Reto Aebersold Date: Thu, 8 Feb 2024 10:04:17 +0100 Subject: [PATCH] feat: add course session permissions --- .../components/header/CoursePreviewBar.vue | 3 ++ .../components/header/MainNavigationBar.vue | 4 +- .../learningPath/circlePage/CirclePage.vue | 7 ++- client/src/types.ts | 1 + server/vbv_lernwelt/course/serializers.py | 14 ++++-- server/vbv_lernwelt/course/views.py | 5 ++- server/vbv_lernwelt/iam/permissions.py | 14 ++++++ server/vbv_lernwelt/iam/tests/test_actions.py | 44 +++++++++++++++++++ 8 files changed, 85 insertions(+), 7 deletions(-) create mode 100644 server/vbv_lernwelt/iam/tests/test_actions.py diff --git a/client/src/components/header/CoursePreviewBar.vue b/client/src/components/header/CoursePreviewBar.vue index c2bf4259..979fce70 100644 --- a/client/src/components/header/CoursePreviewBar.vue +++ b/client/src/components/header/CoursePreviewBar.vue @@ -3,9 +3,11 @@ 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(); @@ -32,6 +34,7 @@ const { t } = useTranslation(); { }); const hasPreviewMenu = computed(() => { - return useCockpitStore().hasExpertCockpitType; + return ( + useCockpitStore().hasExpertCockpitType || useCockpitStore().hasMentorCockpitType + ); }); const hasAppointmentsMenu = computed(() => { diff --git a/client/src/pages/learningPath/circlePage/CirclePage.vue b/client/src/pages/learningPath/circlePage/CirclePage.vue index 274f23dd..b52f4565 100644 --- a/client/src/pages/learningPath/circlePage/CirclePage.vue +++ b/client/src/pages/learningPath/circlePage/CirclePage.vue @@ -52,6 +52,11 @@ const circleExperts = computed(() => { return []; }); +const learningContentReadonly = computed(() => { + const actions = courseSession.value.actions; + return props.readonly || !actions.includes("complete-learning-content"); +}); + const duration = computed(() => { // if (circleStore.circle) { // const minutes = sumBy(circleStore.circle.learningSequences, "minutes"); @@ -293,7 +298,7 @@ watch( :course-slug="props.courseSlug" :circle="circle" :learning-sequence="learningSequence" - :readonly="props.readonly" + :readonly="learningContentReadonly" > diff --git a/client/src/types.ts b/client/src/types.ts index 5cc139c5..519df057 100644 --- a/client/src/types.ts +++ b/client/src/types.ts @@ -448,6 +448,7 @@ export interface CourseSession { start_date: string; end_date: string; due_dates: DueDate[]; + actions: string[]; } export type CourseSessionUser = CourseSessionUserObjectsType; diff --git a/server/vbv_lernwelt/course/serializers.py b/server/vbv_lernwelt/course/serializers.py index 5f9e8cf9..cb246c3b 100644 --- a/server/vbv_lernwelt/course/serializers.py +++ b/server/vbv_lernwelt/course/serializers.py @@ -11,6 +11,7 @@ from vbv_lernwelt.course.models import ( ) from vbv_lernwelt.duedate.models import DueDate from vbv_lernwelt.duedate.serializers import DueDateSerializer +from vbv_lernwelt.iam.permissions import course_session_permissions class CourseSerializer(serializers.ModelSerializer): @@ -54,15 +55,12 @@ class CourseSessionSerializer(serializers.ModelSerializer): id = StringIDField() course = serializers.SerializerMethodField() - # course_url = serializers.SerializerMethodField() due_dates = serializers.SerializerMethodField() + actions = serializers.SerializerMethodField() def get_course(self, obj): return CourseSerializer(obj.course).data - # def get_course_url(self, obj): - # return obj.course.get_course_url() - def get_due_dates(self, obj): due_dates = DueDate.objects.filter( Q(start__isnull=False) | Q(end__isnull=False), course_session=obj @@ -80,7 +78,15 @@ class CourseSessionSerializer(serializers.ModelSerializer): "start_date", "end_date", "due_dates", + "actions", ] + read_only_fields = ["actions"] + + def get_actions(self, obj): + user = self.context.get("user") + if user: + return course_session_permissions(user, obj.id) + return [] class CircleDocumentSerializer(serializers.ModelSerializer): diff --git a/server/vbv_lernwelt/course/views.py b/server/vbv_lernwelt/course/views.py index 6c2db3ee..a5b8ce2b 100644 --- a/server/vbv_lernwelt/course/views.py +++ b/server/vbv_lernwelt/course/views.py @@ -157,7 +157,10 @@ def get_course_sessions(request): ).distinct() return Response( - status=200, data=CourseSessionSerializer(all_to_serialize, many=True).data + status=200, + data=CourseSessionSerializer( + all_to_serialize, many=True, context={"user": request.user} + ).data, ) except PermissionDenied as e: raise e diff --git a/server/vbv_lernwelt/iam/permissions.py b/server/vbv_lernwelt/iam/permissions.py index 110be65f..ecbdc2f7 100644 --- a/server/vbv_lernwelt/iam/permissions.py +++ b/server/vbv_lernwelt/iam/permissions.py @@ -207,3 +207,17 @@ def can_view_course_completions( course_session_id=course_session_id, ) ) + + +def course_session_permissions(user: User, course_session_id: int) -> list[str]: + return _action_list( + { + "complete-learning-content": is_course_session_member( + user, course_session_id + ), + } + ) + + +def _action_list(actions: dict[str, bool]): + return [action for action, allowed in actions.items() if allowed] diff --git a/server/vbv_lernwelt/iam/tests/test_actions.py b/server/vbv_lernwelt/iam/tests/test_actions.py new file mode 100644 index 00000000..2a87cf82 --- /dev/null +++ b/server/vbv_lernwelt/iam/tests/test_actions.py @@ -0,0 +1,44 @@ +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.iam.permissions import course_session_permissions +from vbv_lernwelt.learning_mentor.models import LearningMentor + + +class ActionTestCase(TestCase): + def setUp(self): + self.course, _ = create_course("Test Course") + self.course_session = create_course_session( + course=self.course, title="Test Session" + ) + + def test_course_session_permissions(self): + # GIVEN + lm = create_user("mentor") + LearningMentor.objects.create( + mentor=lm, + course=self.course, + ) + + participant = create_user("participant") + add_course_session_user( + self.course_session, + participant, + role=CourseSessionUser.Role.MEMBER, + ) + + # WHEN + mentor_actions = course_session_permissions(lm, self.course_session.id) + participant_actions = course_session_permissions( + participant, self.course_session.id + ) + + # THEN + self.assertEqual(len(mentor_actions), 0) + self.assertEqual(participant_actions, ["complete-learning-content"])