From 6a985ce60797b46a83559af95059c943cd3d8f41 Mon Sep 17 00:00:00 2001 From: Reto Aebersold Date: Mon, 15 Jan 2024 11:33:45 +0100 Subject: [PATCH 1/7] feat: cockpit user profile --- .../cockpit/profile/CockpitProfileContent.vue | 10 ++ .../pages/cockpit/CockpitUserProfilePage.vue | 116 ------------------ .../profilePage/CockpitUserProfilePage.vue | 74 +++++++++++ .../profilePage/LearningPathProfilePage.vue | 72 +++++++++++ client/src/router/index.ts | 12 +- server/config/urls.py | 3 +- server/vbv_lernwelt/api/user.py | 15 ++- server/vbv_lernwelt/iam/permissions.py | 26 +++- server/vbv_lernwelt/iam/tests/test_experts.py | 82 +++++++++++++ 9 files changed, 288 insertions(+), 122 deletions(-) create mode 100644 client/src/components/cockpit/profile/CockpitProfileContent.vue delete mode 100644 client/src/pages/cockpit/CockpitUserProfilePage.vue create mode 100644 client/src/pages/cockpit/profilePage/CockpitUserProfilePage.vue create mode 100644 client/src/pages/cockpit/profilePage/LearningPathProfilePage.vue create mode 100644 server/vbv_lernwelt/iam/tests/test_experts.py diff --git a/client/src/components/cockpit/profile/CockpitProfileContent.vue b/client/src/components/cockpit/profile/CockpitProfileContent.vue new file mode 100644 index 00000000..77eb78d1 --- /dev/null +++ b/client/src/components/cockpit/profile/CockpitProfileContent.vue @@ -0,0 +1,10 @@ + diff --git a/client/src/pages/cockpit/CockpitUserProfilePage.vue b/client/src/pages/cockpit/CockpitUserProfilePage.vue deleted file mode 100644 index 88df5bd4..00000000 --- a/client/src/pages/cockpit/CockpitUserProfilePage.vue +++ /dev/null @@ -1,116 +0,0 @@ - - - - - diff --git a/client/src/pages/cockpit/profilePage/CockpitUserProfilePage.vue b/client/src/pages/cockpit/profilePage/CockpitUserProfilePage.vue new file mode 100644 index 00000000..e06f7f96 --- /dev/null +++ b/client/src/pages/cockpit/profilePage/CockpitUserProfilePage.vue @@ -0,0 +1,74 @@ + + + diff --git a/client/src/pages/cockpit/profilePage/LearningPathProfilePage.vue b/client/src/pages/cockpit/profilePage/LearningPathProfilePage.vue new file mode 100644 index 00000000..c4bbfb9b --- /dev/null +++ b/client/src/pages/cockpit/profilePage/LearningPathProfilePage.vue @@ -0,0 +1,72 @@ + + + diff --git a/client/src/router/index.ts b/client/src/router/index.ts index 2e0deef5..44eeb299 100644 --- a/client/src/router/index.ts +++ b/client/src/router/index.ts @@ -221,9 +221,19 @@ const router = createRouter({ }, { path: "profile/:userId", - component: () => import("@/pages/cockpit/CockpitUserProfilePage.vue"), + component: () => + import("@/pages/cockpit/profilePage/CockpitUserProfilePage.vue"), props: true, name: "cockpitUserProfile", + children: [ + { + path: "learning-path", + component: () => + import("@/pages/cockpit/profilePage/LearningPathProfilePage.vue"), + props: true, + name: "cockpitProfileLearningPath", + }, + ], }, { path: "profile/:userId/:circleSlug", diff --git a/server/config/urls.py b/server/config/urls.py index f7d27476..0634c233 100644 --- a/server/config/urls.py +++ b/server/config/urls.py @@ -12,7 +12,7 @@ from django_ratelimit.exceptions import Ratelimited from graphene_django.views import GraphQLView from vbv_lernwelt.api.directory import list_entities -from vbv_lernwelt.api.user import get_cockpit_type, me_user_view +from vbv_lernwelt.api.user import get_cockpit_type, get_profile, me_user_view from vbv_lernwelt.assignment.views import request_assignment_completion_status from vbv_lernwelt.core.middleware.auth import django_view_authentication_exempt from vbv_lernwelt.core.schema import schema @@ -100,6 +100,7 @@ urlpatterns = [ path("sso/", include("vbv_lernwelt.sso.urls")), re_path(r'api/core/me/$', me_user_view, name='me_user_view'), re_path(r'api/core/entities/$', list_entities, name='list_entities'), + path(r'api/core/profile//', get_profile, name='get_profile_view'), re_path(r'api/core/login/$', django_view_authentication_exempt(vue_login), name='vue_login'), diff --git a/server/vbv_lernwelt/api/user.py b/server/vbv_lernwelt/api/user.py index 2ff2b3d3..9aeecaf3 100644 --- a/server/vbv_lernwelt/api/user.py +++ b/server/vbv_lernwelt/api/user.py @@ -5,9 +5,9 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from vbv_lernwelt.core.serializers import UserSerializer - from vbv_lernwelt.course.models import Course, CourseSessionUser from vbv_lernwelt.course_session_group.models import CourseSessionGroup +from vbv_lernwelt.iam.permissions import can_view_profile from vbv_lernwelt.learning_mentor.models import LearningMentor @@ -59,3 +59,16 @@ def get_cockpit_type(request, course_id: int): cockpit_type = None return Response({"type": cockpit_type}) + + +@api_view(["GET"]) +@permission_classes([IsAuthenticated]) +def get_profile(request, course_session_id: int, user_id: str): + course_session_user = get_object_or_404( + CourseSessionUser, course_session_id=course_session_id, user_id=user_id + ) + + if not can_view_profile(request.user, course_session_user): + return Response(status=403) + + return Response(UserSerializer(course_session_user.user).data) diff --git a/server/vbv_lernwelt/iam/permissions.py b/server/vbv_lernwelt/iam/permissions.py index e0b0fd92..c243b62c 100644 --- a/server/vbv_lernwelt/iam/permissions.py +++ b/server/vbv_lernwelt/iam/permissions.py @@ -36,17 +36,24 @@ def is_course_session_expert(user, course_session_id: int): if user.is_superuser: return True + course_session = CourseSession.objects.get(id=course_session_id) + is_supervisor = CourseSessionGroup.objects.filter( - supervisor=user, course_session__id=course_session_id + supervisor=user, course_session=course_session ).exists() is_expert = CourseSessionUser.objects.filter( - course_session_id=course_session_id, + course_session=course_session, user=user, role=CourseSessionUser.Role.EXPERT, ).exists() - return is_supervisor or is_expert + is_learning_mentor = LearningMentor.objects.filter( + mentor=user, + course=course_session.course, + ).exists() + + return is_supervisor or is_expert or is_learning_mentor def is_course_session_member(user, course_session_id: int | None = None): @@ -150,3 +157,16 @@ def has_role_in_course(user: User, course: Course) -> bool: return True return False + + +def can_view_profile(user: User, profile_user: CourseSessionUser) -> bool: + if user.is_superuser: + return True + + if user == profile_user.user: + return True + + if is_course_session_expert(user, profile_user.course_session_id): + return True + + return False diff --git a/server/vbv_lernwelt/iam/tests/test_experts.py b/server/vbv_lernwelt/iam/tests/test_experts.py new file mode 100644 index 00000000..f0032656 --- /dev/null +++ b/server/vbv_lernwelt/iam/tests/test_experts.py @@ -0,0 +1,82 @@ +from django.test import TestCase + +from vbv_lernwelt.course.creators.test_utils import ( + 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 is_course_session_expert +from vbv_lernwelt.learning_mentor.models import LearningMentor + + +class ExpertTestCase(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_member(self): + # GIVEN + csu = CourseSessionUser.objects.create( + course_session=self.course_session, + user=self.user, + role=CourseSessionUser.Role.MEMBER, + ) + + # WHEN + is_expert = is_course_session_expert( + user=csu.user, course_session_id=self.course_session.id + ) + + # THEN + self.assertFalse(is_expert) + + def test_supervisor(self): + # GIVEN + csg = CourseSessionGroup.objects.create(name="Test Group", course=self.course) + csg.course_session.add(self.course_session) + csg.supervisor.add(self.user) + + # WHEN + is_expert = is_course_session_expert( + user=self.user, course_session_id=self.course_session.id + ) + + # THEN + self.assertTrue(is_expert) + + def test_learning_mentor(self): + # GIVEN + lm = LearningMentor.objects.create( + mentor=self.user, + course=self.course, + ) + + # WHEN + is_expert = is_course_session_expert( + user=lm.mentor, course_session_id=self.course_session.id + ) + + # THEN + self.assertTrue(is_expert) + + def test_expert(self): + # GIVEN + csu = CourseSessionUser.objects.create( + course_session=self.course_session, + user=self.user, + role=CourseSessionUser.Role.EXPERT, + ) + + # WHEN + is_expert = is_course_session_expert( + user=csu.user, course_session_id=self.course_session.id + ) + + # THEN + self.assertTrue(is_expert) From a8c09651db16e43a3dce682fd7d5f55529b8edad Mon Sep 17 00:00:00 2001 From: Reto Aebersold Date: Mon, 15 Jan 2024 14:39:17 +0100 Subject: [PATCH 2/7] chore: hide profile links and add profile test --- .../profilePage/LearningPathProfilePage.vue | 1 + .../circlePage/LearningSequence.vue | 7 +-- server/vbv_lernwelt/api/tests/test_profile.py | 60 +++++++++++++++++++ 3 files changed, 63 insertions(+), 5 deletions(-) create mode 100644 server/vbv_lernwelt/api/tests/test_profile.py diff --git a/client/src/pages/cockpit/profilePage/LearningPathProfilePage.vue b/client/src/pages/cockpit/profilePage/LearningPathProfilePage.vue index c4bbfb9b..c094e729 100644 --- a/client/src/pages/cockpit/profilePage/LearningPathProfilePage.vue +++ b/client/src/pages/cockpit/profilePage/LearningPathProfilePage.vue @@ -64,6 +64,7 @@ watch(lpQueryResult.learningPath, () => { :circle="selectedCircle" :learning-sequence="learningSequence" readonly + hide-links > diff --git a/client/src/pages/learningPath/circlePage/LearningSequence.vue b/client/src/pages/learningPath/circlePage/LearningSequence.vue index 2f52f837..2457c591 100644 --- a/client/src/pages/learningPath/circlePage/LearningSequence.vue +++ b/client/src/pages/learningPath/circlePage/LearningSequence.vue @@ -32,6 +32,7 @@ type Props = { learningSequence: LearningSequence; circle: CircleType; readonly?: boolean; + hideLinks?: boolean; }; const props = withDefaults(defineProps(), { @@ -229,7 +230,7 @@ function checkboxIconUncheckedTailwindClass(lc: LearningContent) {
{{ @@ -298,11 +299,7 @@ function checkboxIconUncheckedTailwindClass(lc: LearningContent) {
{{ $t("a.Selbsteinschätzung") }}
- - - - diff --git a/server/vbv_lernwelt/api/tests/test_profile.py b/server/vbv_lernwelt/api/tests/test_profile.py new file mode 100644 index 00000000..4c73b50e --- /dev/null +++ b/server/vbv_lernwelt/api/tests/test_profile.py @@ -0,0 +1,60 @@ +from django.urls import reverse +from rest_framework import status +from rest_framework.test import APITestCase + +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 + + +class ProfileViewTest(APITestCase): + def setUp(self) -> None: + self.course, _ = create_course("Test Course") + self.course_session = create_course_session( + course=self.course, title="Test Session" + ) + + self.user = create_user("user") + add_course_session_user( + self.course_session, + self.user, + role=CourseSessionUser.Role.MEMBER, + ) + + self.client.force_login(self.user) + + def test_user_profile(self) -> None: + # GIVEN + url = reverse( + "get_profile_view", + kwargs={ + "course_session_id": self.course_session.id, + "user_id": self.user.id, + }, + ) + + # WHEN + response = self.client.get(url) + + # THEN + self.assertEqual(response.status_code, status.HTTP_200_OK) + profile = response.data + self.assertEqual( + profile, + { + "id": str(self.user.id), + "first_name": self.user.first_name, + "last_name": self.user.last_name, + "email": self.user.email, + "username": self.user.username, + "avatar_url": "/static/avatars/myvbv-default-avatar.png", + "organisation": None, + "is_superuser": False, + "course_session_experts": [], + "language": "de", + }, + ) From f7aa5a35ca7ae2cad5cf5c8a6ba6a697000de961 Mon Sep 17 00:00:00 2001 From: Reto Aebersold Date: Mon, 15 Jan 2024 15:48:30 +0100 Subject: [PATCH 3/7] fix: wait for completion before nav --- .../learningContentPage/LearningContentParent.vue | 4 ++-- client/src/stores/circle.ts | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/client/src/pages/learningPath/learningContentPage/LearningContentParent.vue b/client/src/pages/learningPath/learningContentPage/LearningContentParent.vue index 012f966f..e33113fd 100644 --- a/client/src/pages/learningPath/learningContentPage/LearningContentParent.vue +++ b/client/src/pages/learningPath/learningContentPage/LearningContentParent.vue @@ -64,8 +64,8 @@ function handleFinishedLearningContent() { props.learningContent, props.circle, previousRoute, - (lc: LearningContentWithCompletion) => { - courseCompletionData.markCompletion(lc, "SUCCESS"); + async (lc: LearningContentWithCompletion) => { + await courseCompletionData.markCompletion(lc, "SUCCESS"); } ); } diff --git a/client/src/stores/circle.ts b/client/src/stores/circle.ts index 1bebc5db..5756c775 100644 --- a/client/src/stores/circle.ts +++ b/client/src/stores/circle.ts @@ -73,16 +73,18 @@ export const useCircleStore = defineStore({ }); } }, - continueFromLearningContent( + async continueFromLearningContent( currentLearningContent: LearningContentWithCompletion, circle: CircleType, returnRoute?: RouteLocationNormalized, - markCompletionFn?: (learningContent: LearningContentWithCompletion) => void + markCompletionFn?: ( + learningContent: LearningContentWithCompletion + ) => Promise ) { if (currentLearningContent) { if (currentLearningContent.can_user_self_toggle_course_completion) { if (markCompletionFn) { - markCompletionFn(currentLearningContent); + await markCompletionFn(currentLearningContent); } } this.closeLearningContent(currentLearningContent, circle, returnRoute); From 57f80ea1092ce50f91a7aff3b09d52693ef04cc3 Mon Sep 17 00:00:00 2001 From: Reto Aebersold Date: Thu, 18 Jan 2024 08:33:08 +0100 Subject: [PATCH 4/7] chore: format --- server/config/urls.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/server/config/urls.py b/server/config/urls.py index 1008ba73..56b9ad07 100644 --- a/server/config/urls.py +++ b/server/config/urls.py @@ -12,7 +12,12 @@ from django_ratelimit.exceptions import Ratelimited from graphene_django.views import GraphQLView from vbv_lernwelt.api.directory import list_entities -from vbv_lernwelt.api.user import get_cockpit_type, get_profile, me_user_view, post_avatar +from vbv_lernwelt.api.user import ( + get_cockpit_type, + get_profile, + me_user_view, + post_avatar, +) from vbv_lernwelt.assignment.views import request_assignment_completion_status from vbv_lernwelt.core.middleware.auth import django_view_authentication_exempt from vbv_lernwelt.core.schema import schema From 55d418602f98f0919aee7906f884f967ae689f4f Mon Sep 17 00:00:00 2001 From: Reto Aebersold Date: Tue, 23 Jan 2024 09:05:42 +0100 Subject: [PATCH 5/7] feat: vv user profile --- .../cockpitPage/mentor/MentorParticipants.vue | 9 ++++++--- server/vbv_lernwelt/course/views.py | 11 +++++++++-- server/vbv_lernwelt/iam/permissions.py | 13 ++++++------- server/vbv_lernwelt/iam/tests/test_experts.py | 16 ---------------- 4 files changed, 21 insertions(+), 28 deletions(-) diff --git a/client/src/pages/cockpit/cockpitPage/mentor/MentorParticipants.vue b/client/src/pages/cockpit/cockpitPage/mentor/MentorParticipants.vue index c8fd6d15..5356d283 100644 --- a/client/src/pages/cockpit/cockpitPage/mentor/MentorParticipants.vue +++ b/client/src/pages/cockpit/cockpitPage/mentor/MentorParticipants.vue @@ -27,9 +27,12 @@ const { summary } = useMentorCockpit(courseSession.value.id); {{ participant.email }} - - - + + {{ $t("cockpit.profileLink") }} + diff --git a/server/vbv_lernwelt/course/views.py b/server/vbv_lernwelt/course/views.py index 85b67714..d6e8afe6 100644 --- a/server/vbv_lernwelt/course/views.py +++ b/server/vbv_lernwelt/course/views.py @@ -23,6 +23,7 @@ from vbv_lernwelt.iam.permissions import ( has_course_access_by_page_request, is_circle_expert, is_course_session_expert, + is_user_mentor, ) from vbv_lernwelt.learning_mentor.models import LearningMentor @@ -76,8 +77,14 @@ def request_course_completion(request, course_session_id): @api_view(["GET"]) def request_course_completion_for_user(request, course_session_id, user_id): - if request.user.id == user_id or is_course_session_expert( - request.user, course_session_id + if ( + request.user.id == user_id + or is_course_session_expert(request.user, course_session_id) + or is_user_mentor( + mentor=request.user, + participant_user_id=user_id, + course_session_id=course_session_id, + ) ): return _request_course_completion(course_session_id, user_id) raise PermissionDenied() diff --git a/server/vbv_lernwelt/iam/permissions.py b/server/vbv_lernwelt/iam/permissions.py index 02a7e0a3..5a1d7cec 100644 --- a/server/vbv_lernwelt/iam/permissions.py +++ b/server/vbv_lernwelt/iam/permissions.py @@ -64,12 +64,7 @@ def is_course_session_expert(user, course_session_id: int): role=CourseSessionUser.Role.EXPERT, ).exists() - is_learning_mentor = LearningMentor.objects.filter( - mentor=user, - course=course_session.course, - ).exists() - - return is_supervisor or is_expert or is_learning_mentor + return is_supervisor or is_expert def is_course_session_member(user, course_session_id: int | None = None): @@ -190,7 +185,11 @@ def can_view_profile(user: User, profile_user: CourseSessionUser) -> bool: if user == profile_user.user: return True - if is_course_session_expert(user, profile_user.course_session_id): + if is_course_session_expert(user, profile_user.course_session.id) or is_user_mentor( + mentor=user, + participant_user_id=profile_user.user.id, + course_session_id=profile_user.course_session.id, + ): return True return False diff --git a/server/vbv_lernwelt/iam/tests/test_experts.py b/server/vbv_lernwelt/iam/tests/test_experts.py index f0032656..973bb368 100644 --- a/server/vbv_lernwelt/iam/tests/test_experts.py +++ b/server/vbv_lernwelt/iam/tests/test_experts.py @@ -8,7 +8,6 @@ from vbv_lernwelt.course.creators.test_utils import ( from vbv_lernwelt.course.models import CourseSessionUser from vbv_lernwelt.course_session_group.models import CourseSessionGroup from vbv_lernwelt.iam.permissions import is_course_session_expert -from vbv_lernwelt.learning_mentor.models import LearningMentor class ExpertTestCase(TestCase): @@ -50,21 +49,6 @@ class ExpertTestCase(TestCase): # THEN self.assertTrue(is_expert) - def test_learning_mentor(self): - # GIVEN - lm = LearningMentor.objects.create( - mentor=self.user, - course=self.course, - ) - - # WHEN - is_expert = is_course_session_expert( - user=lm.mentor, course_session_id=self.course_session.id - ) - - # THEN - self.assertTrue(is_expert) - def test_expert(self): # GIVEN csu = CourseSessionUser.objects.create( From 6362d85aecbcb854bc22623fad62964578d0ce74 Mon Sep 17 00:00:00 2001 From: Reto Aebersold Date: Tue, 23 Jan 2024 09:50:14 +0100 Subject: [PATCH 6/7] fix: force click next button --- cypress/e2e/circle.cy.js | 74 ++++++++++++++++++++-------------------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/cypress/e2e/circle.cy.js b/cypress/e2e/circle.cy.js index 98d41b73..de1185a1 100644 --- a/cypress/e2e/circle.cy.js +++ b/cypress/e2e/circle.cy.js @@ -9,71 +9,71 @@ describe("circle.cy.js", () => { }); it("can open circle page", () => { - cy.get('[data-cy="circle-title"]').should("contain", "Fahrzeug"); + cy.get("[data-cy=\"circle-title\"]").should("contain", "Fahrzeug"); }); it("can toggle learning content", () => { - cy.get('[data-cy="circle-title"]').should("contain", "Fahrzeug"); + cy.get("[data-cy=\"circle-title\"]").should("contain", "Fahrzeug"); cy.get( - '[data-cy="test-lehrgang-lp-circle-fahrzeug-lc-handlungsfeld-fahrzeug-checkbox"]' + "[data-cy=\"test-lehrgang-lp-circle-fahrzeug-lc-handlungsfeld-fahrzeug-checkbox\"]" ).should("have.class", "cy-unchecked"); cy.get( - '[data-cy="test-lehrgang-lp-circle-fahrzeug-lc-handlungsfeld-fahrzeug-checkbox"]' + "[data-cy=\"test-lehrgang-lp-circle-fahrzeug-lc-handlungsfeld-fahrzeug-checkbox\"]" ).click(); cy.get( - '[data-cy="test-lehrgang-lp-circle-fahrzeug-lc-handlungsfeld-fahrzeug-checkbox"]' + "[data-cy=\"test-lehrgang-lp-circle-fahrzeug-lc-handlungsfeld-fahrzeug-checkbox\"]" ).should("have.class", "cy-checked"); // completion data should still be there after reload cy.reload(); cy.get( - '[data-cy="test-lehrgang-lp-circle-fahrzeug-lc-handlungsfeld-fahrzeug-checkbox"]' + "[data-cy=\"test-lehrgang-lp-circle-fahrzeug-lc-handlungsfeld-fahrzeug-checkbox\"]" ).should("have.class", "cy-checked"); }); it("can open learning contents and complete them by continuing", () => { cy.get( - '[data-cy="test-lehrgang-lp-circle-fahrzeug-lc-verschaffe-dir-einen-überblick"]' + "[data-cy=\"test-lehrgang-lp-circle-fahrzeug-lc-verschaffe-dir-einen-überblick\"]" ).click(); - cy.get('[data-cy="lc-title"]').should( + cy.get("[data-cy=\"lc-title\"]").should( "contain", "Verschaffe dir einen Überblick" ); - cy.get('[data-cy="complete-and-continue"]').click({ force: true }); - cy.get('[data-cy="circle-title"]').should("contain", "Fahrzeug"); + cy.get("[data-cy=\"complete-and-continue\"]").click({ force: true }); + cy.get("[data-cy=\"circle-title\"]").should("contain", "Fahrzeug"); - cy.get('[data-cy="ls-continue-button"]').click(); - cy.get('[data-cy="lc-title"]').should( + cy.get("[data-cy=\"ls-continue-button\"]").click({ force: true }); + cy.get("[data-cy=\"lc-title\"]").should( "contain", "Handlungsfeld «Fahrzeug»" ); - cy.get('[data-cy="complete-and-continue"]').click({ force: true }); - cy.get('[data-cy="circle-title"]').should("contain", "Fahrzeug"); + cy.get("[data-cy=\"complete-and-continue\"]").click({ force: true }); + cy.get("[data-cy=\"circle-title\"]").should("contain", "Fahrzeug"); cy.get( - '[data-cy="test-lehrgang-lp-circle-fahrzeug-lc-verschaffe-dir-einen-überblick-checkbox"]' + "[data-cy=\"test-lehrgang-lp-circle-fahrzeug-lc-verschaffe-dir-einen-überblick-checkbox\"]" ).should("have.class", "cy-checked"); cy.get( - '[data-cy="test-lehrgang-lp-circle-fahrzeug-lc-handlungsfeld-fahrzeug-checkbox"]' + "[data-cy=\"test-lehrgang-lp-circle-fahrzeug-lc-handlungsfeld-fahrzeug-checkbox\"]" ).should("have.class", "cy-checked"); }); it("continue button works", () => { - cy.get('[data-cy="ls-continue-button"]').should("contain", "Los geht's"); - cy.get('[data-cy="ls-continue-button"]').click(); + cy.get("[data-cy=\"ls-continue-button\"]").should("contain", "Los geht's"); + cy.get("[data-cy=\"ls-continue-button\"]").click(); - cy.get('[data-cy="lc-title"]').should( + cy.get("[data-cy=\"lc-title\"]").should( "contain", "Verschaffe dir einen Überblick" ); - cy.get('[data-cy="complete-and-continue"]').click({ force: true }); + cy.get("[data-cy=\"complete-and-continue\"]").click({ force: true }); - cy.get('[data-cy="ls-continue-button"]').should("contain", "Weiter geht's"); - cy.get('[data-cy="ls-continue-button"]').click(); - cy.get('[data-cy="lc-title"]').should( + cy.get("[data-cy=\"ls-continue-button\"]").should("contain", "Weiter geht's"); + cy.get("[data-cy=\"ls-continue-button\"]").click(); + cy.get("[data-cy=\"lc-title\"]").should( "contain", "Handlungsfeld «Fahrzeug»" ); @@ -81,43 +81,43 @@ describe("circle.cy.js", () => { it("can open learning content by url", () => { cy.visit("/course/test-lehrgang/learn/fahrzeug/handlungsfeld-fahrzeug"); - cy.get('[data-cy="lc-title"]').should( + cy.get("[data-cy=\"lc-title\"]").should( "contain", "Handlungsfeld «Fahrzeug»" ); - cy.get('[data-cy="close-learning-content"]').click(); - cy.get('[data-cy="circle-title"]').should("contain", "Fahrzeug"); + cy.get("[data-cy=\"close-learning-content\"]").click(); + cy.get("[data-cy=\"circle-title\"]").should("contain", "Fahrzeug"); }); it("checks number of sequences and contents", () => { - cy.get('[data-cy="lp-learning-sequence"]').should("have.length", 3); - cy.get('[data-cy="lp-learning-sequence"]') + cy.get("[data-cy=\"lp-learning-sequence\"]").should("have.length", 3); + cy.get("[data-cy=\"lp-learning-sequence\"]") .first() .should("contain", "Vorbereitung"); - cy.get('[data-cy="lp-learning-sequence"]') + cy.get("[data-cy=\"lp-learning-sequence\"]") .eq(1) .should("contain", "Training"); - cy.get('[data-cy="lp-learning-sequence"]') + cy.get("[data-cy=\"lp-learning-sequence\"]") .last() .should("contain", "Transfer"); - cy.get('[data-cy="lp-learning-content"]').should("have.length", 10); - cy.get('[data-cy="lp-learning-content"]') + cy.get("[data-cy=\"lp-learning-content\"]").should("have.length", 10); + cy.get("[data-cy=\"lp-learning-content\"]") .first() .should("contain", "Verschaffe dir einen Überblick"); - cy.get('[data-cy="lp-learning-content"]') + cy.get("[data-cy=\"lp-learning-content\"]") .eq(4) .should("contain", "Präsenzkurs Fahrzeug"); - cy.get('[data-cy="lp-learning-content"]') + cy.get("[data-cy=\"lp-learning-content\"]") .eq(7) .should("contain", "Reflexion"); - cy.get('[data-cy="lp-learning-content"]') + cy.get("[data-cy=\"lp-learning-content\"]") .last() .should("contain", "Feedback"); cy.visit("/course/test-lehrgang/learn/reisen"); - cy.get('[data-cy="lp-learning-sequence"]').should("have.length", 3); - cy.get('[data-cy="lp-learning-content"]').should("have.length", 9); + cy.get("[data-cy=\"lp-learning-sequence\"]").should("have.length", 3); + cy.get("[data-cy=\"lp-learning-content\"]").should("have.length", 9); }); }); From 07b3a4e9d5edde9f52531b5d18db182201d33f78 Mon Sep 17 00:00:00 2001 From: Reto Aebersold Date: Tue, 23 Jan 2024 10:01:22 +0100 Subject: [PATCH 7/7] feat: add proper permission --- server/vbv_lernwelt/course/views.py | 13 +++---------- server/vbv_lernwelt/iam/permissions.py | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/server/vbv_lernwelt/course/views.py b/server/vbv_lernwelt/course/views.py index d6e8afe6..6c2db3ee 100644 --- a/server/vbv_lernwelt/course/views.py +++ b/server/vbv_lernwelt/course/views.py @@ -18,12 +18,11 @@ from vbv_lernwelt.course_session_group.models import CourseSessionGroup from vbv_lernwelt.files.models import UploadFile from vbv_lernwelt.files.services import FileDirectUploadService from vbv_lernwelt.iam.permissions import ( + can_view_course_completions, course_sessions_for_user_qs, has_course_access, has_course_access_by_page_request, is_circle_expert, - is_course_session_expert, - is_user_mentor, ) from vbv_lernwelt.learning_mentor.models import LearningMentor @@ -77,14 +76,8 @@ def request_course_completion(request, course_session_id): @api_view(["GET"]) def request_course_completion_for_user(request, course_session_id, user_id): - if ( - request.user.id == user_id - or is_course_session_expert(request.user, course_session_id) - or is_user_mentor( - mentor=request.user, - participant_user_id=user_id, - course_session_id=course_session_id, - ) + if can_view_course_completions( + user=request.user, course_session_id=course_session_id, target_user_id=user_id ): return _request_course_completion(course_session_id, user_id) raise PermissionDenied() diff --git a/server/vbv_lernwelt/iam/permissions.py b/server/vbv_lernwelt/iam/permissions.py index 5a1d7cec..110be65f 100644 --- a/server/vbv_lernwelt/iam/permissions.py +++ b/server/vbv_lernwelt/iam/permissions.py @@ -193,3 +193,17 @@ def can_view_profile(user: User, profile_user: CourseSessionUser) -> bool: return True return False + + +def can_view_course_completions( + user: User, course_session_id: int, target_user_id: str +) -> bool: + return ( + user.id == target_user_id + or is_course_session_expert(user=user, course_session_id=course_session_id) + or is_user_mentor( + mentor=user, + participant_user_id=target_user_id, + course_session_id=course_session_id, + ) + )