diff --git a/client/src/components/learningPath/SelfEvaluation.vue b/client/src/components/learningPath/SelfEvaluation.vue index 6f5534a7..484727b9 100644 --- a/client/src/components/learningPath/SelfEvaluation.vue +++ b/client/src/components/learningPath/SelfEvaluation.vue @@ -1,6 +1,11 @@ + + + + diff --git a/client/src/components/selfEvaluationFeedback/SelfEvaluationAndFeedbackOverview.vue b/client/src/components/selfEvaluationFeedback/SelfEvaluationAndFeedbackOverview.vue new file mode 100644 index 00000000..fdf3d296 --- /dev/null +++ b/client/src/components/selfEvaluationFeedback/SelfEvaluationAndFeedbackOverview.vue @@ -0,0 +1,131 @@ + + + + + diff --git a/client/src/pages/cockpit/profilePage/CockpitUserProfilePage.vue b/client/src/pages/cockpit/profilePage/CockpitUserProfilePage.vue index e06f7f96..fe9c24a6 100644 --- a/client/src/pages/cockpit/profilePage/CockpitUserProfilePage.vue +++ b/client/src/pages/cockpit/profilePage/CockpitUserProfilePage.vue @@ -14,6 +14,7 @@ const { t } = useTranslation(); const pages = ref([ { label: t("general.learningPath"), route: "cockpitProfileLearningPath" }, + { label: t("a.KompetenzNavi"), route: "cockpitProfileCompetence" }, ]); const courseSession = useCurrentCourseSession(); diff --git a/client/src/pages/cockpit/profilePage/CompetenceProfilePage.vue b/client/src/pages/cockpit/profilePage/CompetenceProfilePage.vue new file mode 100644 index 00000000..b306ba5b --- /dev/null +++ b/client/src/pages/cockpit/profilePage/CompetenceProfilePage.vue @@ -0,0 +1,64 @@ + + + diff --git a/client/src/pages/competence/CompetenceIndexPage.vue b/client/src/pages/competence/CompetenceIndexPage.vue index 34c5cd74..90e898c4 100644 --- a/client/src/pages/competence/CompetenceIndexPage.vue +++ b/client/src/pages/competence/CompetenceIndexPage.vue @@ -10,8 +10,10 @@ import { assignmentsUserPoints, competenceCertificateProgressStatusCount, } from "@/pages/competence/utils"; -import { useSelfEvaluationFeedbackSummaries } from "@/services/selfEvaluationFeedback"; import ItProgress from "@/components/ui/ItProgress.vue"; +import SelfEvaluationAndFeedbackOverview from "@/components/selfEvaluationFeedback/SelfEvaluationAndFeedbackOverview.vue"; +import { useUserStore } from "@/stores/user"; +import { useRouter } from "vue-router"; const props = defineProps<{ courseSlug: string; @@ -48,24 +50,11 @@ const userPointsEvaluatedAssignments = computed(() => { return assignmentsUserPoints(allAssignments.value); }); -const selfEvaluationFeedbackSummaries = useSelfEvaluationFeedbackSummaries( - useCurrentCourseSession().value.id -); - -const selfAssessmentCounts = computed( - () => selfEvaluationFeedbackSummaries.aggregates.value?.self_assessment -); - -const feedbackEvaluationCounts = computed( - () => selfEvaluationFeedbackSummaries.aggregates.value?.feedback_assessment -); - const currentCourseSession = useCurrentCourseSession(); -const isLoaded = computed( - () => - !selfEvaluationFeedbackSummaries.loading.value && !certificatesQuery.fetching.value -); +const isLoaded = computed(() => !certificatesQuery.fetching.value); + +const router = useRouter(); diff --git a/client/src/pages/competence/SelfEvaluationAndFeedbackPage.vue b/client/src/pages/competence/SelfEvaluationAndFeedbackPage.vue index 9e649a4d..8d7b92d9 100644 --- a/client/src/pages/competence/SelfEvaluationAndFeedbackPage.vue +++ b/client/src/pages/competence/SelfEvaluationAndFeedbackPage.vue @@ -1,69 +1,12 @@ diff --git a/client/src/router/index.ts b/client/src/router/index.ts index 3a46207c..0e6c6b2d 100644 --- a/client/src/router/index.ts +++ b/client/src/router/index.ts @@ -105,6 +105,7 @@ const router = createRouter({ import("@/pages/competence/CompetenceCertificateDetailPage.vue"), }, { + name: "selfEvaluationAndFeedback", path: "self-evaluation-and-feedback", props: true, component: () => @@ -257,6 +258,13 @@ const router = createRouter({ props: true, name: "cockpitProfileLearningPath", }, + { + path: "competence", + component: () => + import("@/pages/cockpit/profilePage/CompetenceProfilePage.vue"), + props: true, + name: "cockpitProfileCompetence", + }, ], }, { diff --git a/client/src/services/selfEvaluationFeedback.ts b/client/src/services/selfEvaluationFeedback.ts index 7908ca43..c2f4b8d6 100644 --- a/client/src/services/selfEvaluationFeedback.ts +++ b/client/src/services/selfEvaluationFeedback.ts @@ -165,7 +165,8 @@ export function useSelfEvaluationFeedback( } export function useSelfEvaluationFeedbackSummaries( - courseSessionId: Ref | string + courseSessionId: Ref | string, + userId: Ref | string ) { const summaries = ref([]); const aggregates = ref(); @@ -175,7 +176,7 @@ export function useSelfEvaluationFeedbackSummaries( const url = computed( () => - `/api/self-evaluation-feedback/requester/${courseSessionId}/feedbacks/summaries` + `/api/self-evaluation-feedback/feedbacks/summaries/course-session/${courseSessionId}/user/${userId}` ); const fetchFeedbackSummaries = async () => { diff --git a/cypress/e2e/circle.cy.js b/cypress/e2e/circle.cy.js index 60380029..3daaf0e6 100644 --- a/cypress/e2e/circle.cy.js +++ b/cypress/e2e/circle.cy.js @@ -1,4 +1,4 @@ -import { login } from "./helpers"; +import {login} from "./helpers"; describe("circle.cy.js", () => { beforeEach(() => { @@ -20,15 +20,21 @@ describe("circle.cy.js", () => { "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=\"circle-title\"]").should("contain", "Fahrzeug"); - cy.get("[data-cy=\"ls-continue-button\"]").click(); - cy.get("[data-cy=\"lc-title\"]").should( + // workaround for flaky test in CI: + // for some strange reason, locally we don't have to do this + // but in CI if we don't do this, the next click will not work... + // => it's not great but this workaround at least gives us a stable test + cy.visit("/course/test-lehrgang/learn/fahrzeug"); + + cy.get('[data-cy="ls-continue-button"]').click(); + cy.get('[data-cy="lc-title"]').should( "contain", "Handlungsfeld «Fahrzeug»" ); - cy.get("[data-cy=\"complete-and-continue\"]").click({ force: true }); + cy.get("[data-cy=\"complete-and-continue\"]").click({force: true}); cy.get("[data-cy=\"circle-title\"]").should("contain", "Fahrzeug"); cy.get( @@ -47,7 +53,7 @@ describe("circle.cy.js", () => { "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(); diff --git a/server/vbv_lernwelt/course/views.py b/server/vbv_lernwelt/course/views.py index a5b8ce2b..80b7f238 100644 --- a/server/vbv_lernwelt/course/views.py +++ b/server/vbv_lernwelt/course/views.py @@ -1,3 +1,5 @@ +import uuid + import structlog from django.shortcuts import get_object_or_404 from rest_framework.decorators import api_view @@ -18,6 +20,7 @@ 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_mark_course_completion, can_view_course_completions, course_sessions_for_user_qs, has_course_access, @@ -75,9 +78,13 @@ def request_course_completion(request, course_session_id): @api_view(["GET"]) -def request_course_completion_for_user(request, course_session_id, user_id): +def request_course_completion_for_user( + request, course_session_id: int, user_id: uuid.UUID +): if can_view_course_completions( - user=request.user, course_session_id=course_session_id, target_user_id=user_id + user=request.user, # noqa + course_session_id=course_session_id, + target_user_id=str(user_id), ): return _request_course_completion(course_session_id, user_id) raise PermissionDenied() @@ -91,7 +98,10 @@ def mark_course_completion_view(request): course_session_id = request.data.get("course_session_id") page = Page.objects.get(id=page_id) - if not has_course_access_by_page_request(request, page): + if not can_mark_course_completion( + user=request.user, # noqa + course_session_id=course_session_id, + ): raise PermissionDenied() mark_course_completion( diff --git a/server/vbv_lernwelt/iam/permissions.py b/server/vbv_lernwelt/iam/permissions.py index 69323f8b..833fc197 100644 --- a/server/vbv_lernwelt/iam/permissions.py +++ b/server/vbv_lernwelt/iam/permissions.py @@ -9,6 +9,10 @@ def has_course_access_by_page_request(request, obj): return has_course_access(request.user, obj.specific.get_course().id) +def can_mark_course_completion(user: User, course_session_id: int) -> bool: + return is_course_session_member(user, course_session_id) + + def has_course_access(user, course_id): if user.is_superuser: return True @@ -209,7 +213,7 @@ def can_view_course_completions( user: User, course_session_id: int, target_user_id: str ) -> bool: return ( - user.id == target_user_id + str(user.id) == target_user_id or is_course_session_expert(user=user, course_session_id=course_session_id) or is_user_mentor( mentor=user, diff --git a/server/vbv_lernwelt/self_evaluation_feedback/tests/test_api.py b/server/vbv_lernwelt/self_evaluation_feedback/tests/test_api.py index 526df344..721d8fb3 100644 --- a/server/vbv_lernwelt/self_evaluation_feedback/tests/test_api.py +++ b/server/vbv_lernwelt/self_evaluation_feedback/tests/test_api.py @@ -295,8 +295,8 @@ class SelfEvaluationFeedbackAPI(APITestCase): # WHEN response = self.client.get( reverse( - "get_self_evaluation_feedbacks_as_requester", - args=[self.course_session.id], + "get_course_session_user_feedback_summaries", + args=[self.course_session.id, self.member.id], ) ) @@ -358,8 +358,8 @@ class SelfEvaluationFeedbackAPI(APITestCase): # WHEN response = self.client.get( reverse( - "get_self_evaluation_feedbacks_as_requester", - args=[self.course_session.id], + "get_course_session_user_feedback_summaries", + args=[self.course_session.id, self.member.id], ) ) @@ -393,8 +393,8 @@ class SelfEvaluationFeedbackAPI(APITestCase): # WHEN response = self.client.get( reverse( - "get_self_evaluation_feedbacks_as_requester", - args=[self.course_session.id], + "get_course_session_user_feedback_summaries", + args=[self.course_session.id, self.member.id], ) ) @@ -406,6 +406,55 @@ class SelfEvaluationFeedbackAPI(APITestCase): self.assertEqual(result["self_assessment"]["counts"]["fail"], 0) self.assertEqual(result["self_assessment"]["counts"]["unknown"], 1) + def test_permission_feedback_summaries(self): + # GIVEN + learning_unit = create_learning_unit( # noqa + course=self.course, + circle=self.circle, + ) + + create_performance_criteria_page( + course=self.course, + course_page=self.course_page, + circle=self.circle, + learning_unit=learning_unit, + ) + + expert = create_user("expert") + add_course_session_user( + course_session=self.course_session, + user=expert, + role=CourseSessionUser.Role.EXPERT, + ) + + fellow_member = create_user("fellow_member") + add_course_session_user( + course_session=self.course_session, + user=fellow_member, + role=CourseSessionUser.Role.MEMBER, + ) + + # WHEN / THEN + test_cases = [ + # principle_user wants to access target_user + # -> expected_status_code + (self.member, self.member, 200), + (self.mentor, self.member, 200), + (expert, self.member, 200), + (fellow_member, self.member, 403), + ] + + for principle_user, target_user, expected_status_code in test_cases: + with self.subTest(principle_user=principle_user, target_user=target_user): + self.client.force_login(principle_user) + response = self.client.get( + reverse( + "get_course_session_user_feedback_summaries", + args=[self.course_session.id, target_user.id], + ) + ) + self.assertEqual(response.status_code, expected_status_code) + def test_feedbacks_metadata(self): # GIVEN learning_unit = create_learning_unit( # noqa @@ -425,8 +474,8 @@ class SelfEvaluationFeedbackAPI(APITestCase): # WHEN response = self.client.get( reverse( - "get_self_evaluation_feedbacks_as_requester", - args=[self.course_session.id], + "get_course_session_user_feedback_summaries", + args=[self.course_session.id, self.member.id], ) ) diff --git a/server/vbv_lernwelt/self_evaluation_feedback/urls.py b/server/vbv_lernwelt/self_evaluation_feedback/urls.py index 856a802f..009aa5ad 100644 --- a/server/vbv_lernwelt/self_evaluation_feedback/urls.py +++ b/server/vbv_lernwelt/self_evaluation_feedback/urls.py @@ -2,20 +2,15 @@ from django.urls import path from vbv_lernwelt.self_evaluation_feedback.views import ( add_provider_self_evaluation_feedback, + get_course_session_user_feedback_summaries, get_self_evaluation_feedback_as_provider, get_self_evaluation_feedback_as_requester, - get_self_evaluation_feedbacks_as_requester, release_provider_self_evaluation_feedback, start_self_evaluation_feedback, ) urlpatterns = [ # /requester/* URLs -> For the user who requests feedback - path( - "requester//feedbacks/summaries", - get_self_evaluation_feedbacks_as_requester, - name="get_self_evaluation_feedbacks_as_requester", - ), path( "requester//feedback/start", start_self_evaluation_feedback, @@ -42,4 +37,11 @@ urlpatterns = [ add_provider_self_evaluation_feedback, name="add_self_evaluation_feedback_assessment", ), + # route to get feedback summaries for a user in a course session + # used by different roles to retrieve feedback summaries for a user + path( + "feedbacks/summaries/course-session//user/", + get_course_session_user_feedback_summaries, + name="get_course_session_user_feedback_summaries", + ), ] diff --git a/server/vbv_lernwelt/self_evaluation_feedback/views.py b/server/vbv_lernwelt/self_evaluation_feedback/views.py index 78dbf2b9..31e772f8 100644 --- a/server/vbv_lernwelt/self_evaluation_feedback/views.py +++ b/server/vbv_lernwelt/self_evaluation_feedback/views.py @@ -1,3 +1,5 @@ +import uuid + import structlog from django.shortcuts import get_object_or_404 from rest_framework.decorators import api_view, permission_classes @@ -8,6 +10,7 @@ from rest_framework.response import Response from vbv_lernwelt.core.models import User from vbv_lernwelt.core.serializers import UserSerializer from vbv_lernwelt.course.models import CourseCompletion, CourseSession +from vbv_lernwelt.iam.permissions import can_view_course_completions from vbv_lernwelt.learning_mentor.models import LearningMentor from vbv_lernwelt.learnpath.models import Circle, LearningUnit from vbv_lernwelt.notify.services import NotificationService @@ -91,8 +94,18 @@ def get_self_evaluation_feedback_as_provider(request, learning_unit_id): @api_view(["GET"]) @permission_classes([IsAuthenticated]) -def get_self_evaluation_feedbacks_as_requester(request, course_session_id: int): +def get_course_session_user_feedback_summaries( + request, course_session_id: int, user_id: uuid.UUID +): course_session = get_object_or_404(CourseSession, id=course_session_id) + user_to_lookup = get_object_or_404(User, id=user_id) + + if not can_view_course_completions( + user=request.user, # noqa + course_session_id=course_session_id, + target_user_id=str(user_id), + ): + raise PermissionDenied() results = [] circle_ids = set() @@ -114,7 +127,7 @@ def get_self_evaluation_feedbacks_as_requester(request, course_session_id: int): feedback = SelfEvaluationFeedback.objects.filter( learning_unit=learning_unit, - feedback_requester_user=request.user, + feedback_requester_user=user_to_lookup, ).first() if not feedback: @@ -135,7 +148,9 @@ def get_self_evaluation_feedbacks_as_requester(request, course_session_id: int): }, } - self_assessment_counts = get_self_assessment_counts(learning_unit, request.user) + self_assessment_counts = get_self_assessment_counts( + learning_unit=learning_unit, user=user_to_lookup + ) all_self_assessment_counts.append(self_assessment_counts) results.append(