From 7f8cfcba2472a1c9ebe1646069d62d19c0c74bc8 Mon Sep 17 00:00:00 2001 From: Livio Bieri Date: Thu, 15 Feb 2024 21:49:26 +0100 Subject: [PATCH] feat: kompnavi api --- .../pages/competence/CompetenceParentPage.vue | 18 ++ .../SelfEvaluationAndFeedbackPage.vue | 9 + client/src/router/index.ts | 7 +- .../course/creators/test_utils.py | 5 +- .../tests/test_api.py | 211 +++++++++++++++++- .../self_evaluation_feedback/urls.py | 8 + .../self_evaluation_feedback/utils.py | 114 ++++++++++ .../self_evaluation_feedback/views.py | 73 +++++- 8 files changed, 434 insertions(+), 11 deletions(-) create mode 100644 client/src/pages/competence/SelfEvaluationAndFeedbackPage.vue create mode 100644 server/vbv_lernwelt/self_evaluation_feedback/utils.py diff --git a/client/src/pages/competence/CompetenceParentPage.vue b/client/src/pages/competence/CompetenceParentPage.vue index 0b96ec43..67044e21 100644 --- a/client/src/pages/competence/CompetenceParentPage.vue +++ b/client/src/pages/competence/CompetenceParentPage.vue @@ -27,6 +27,10 @@ function routeInActionCompetences() { return route.path.endsWith("/competences"); } +function routeInSelfEvaluationAndFeedback() { + return route.path.endsWith("/self-evaluation-and-feedback"); +} + onMounted(async () => { log.debug("CompetenceParentPage mounted", props.courseSlug); }); @@ -79,6 +83,20 @@ onMounted(async () => { +
  • + + {{ $t("a.Selbst- und Fremdeinschätzungen") }} + +
  • +
  • diff --git a/client/src/pages/competence/SelfEvaluationAndFeedbackPage.vue b/client/src/pages/competence/SelfEvaluationAndFeedbackPage.vue new file mode 100644 index 00000000..3a21ee36 --- /dev/null +++ b/client/src/pages/competence/SelfEvaluationAndFeedbackPage.vue @@ -0,0 +1,9 @@ + + + + + diff --git a/client/src/router/index.ts b/client/src/router/index.ts index 5ffa7819..109b624f 100644 --- a/client/src/router/index.ts +++ b/client/src/router/index.ts @@ -92,7 +92,6 @@ const router = createRouter({ props: true, component: () => import("@/pages/competence/CompetenceIndexPage.vue"), }, - { path: "certificates", props: true, @@ -110,6 +109,12 @@ const router = createRouter({ props: true, component: () => import("@/pages/competence/PerformanceCriteriaPage.vue"), }, + { + path: "self-evaluation-and-feedback", + props: true, + component: () => + import("@/pages/competence/SelfEvaluationAndFeedbackPage.vue"), + }, { path: "competences", props: true, diff --git a/server/vbv_lernwelt/course/creators/test_utils.py b/server/vbv_lernwelt/course/creators/test_utils.py index 3f945b97..ec22f19b 100644 --- a/server/vbv_lernwelt/course/creators/test_utils.py +++ b/server/vbv_lernwelt/course/creators/test_utils.py @@ -47,6 +47,7 @@ from vbv_lernwelt.learnpath.models import ( LearningContentEdoniqTest, LearningPath, LearningUnit, + LearningUnitPerformanceFeedbackType, ) from vbv_lernwelt.learnpath.tests.learning_path_factories import ( CircleFactory, @@ -275,6 +276,7 @@ def create_learning_unit( circle: Circle, course: Course, course_category_title: str = "Course Category", + feedback_user: LearningUnitPerformanceFeedbackType = LearningUnitPerformanceFeedbackType.NO_FEEDBACK, ) -> LearningUnit: cat, _ = CourseCategory.objects.get_or_create( course=course, @@ -285,6 +287,7 @@ def create_learning_unit( title="Learning Unit", parent=circle, course_category=cat, + feedback_user=feedback_user.value, ) @@ -292,7 +295,7 @@ def create_performance_criteria_page( course: Course, course_page: CoursePage, circle: Circle, - learning_unit: LearningUnitFactory | None = None, + learning_unit: LearningUnitFactory | LearningUnit | None = None, ) -> PerformanceCriteria: competence_navi_page = CompetenceNaviPageFactory( title="Competence Navi", 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 98291bbc..b13254b8 100644 --- a/server/vbv_lernwelt/self_evaluation_feedback/tests/test_api.py +++ b/server/vbv_lernwelt/self_evaluation_feedback/tests/test_api.py @@ -15,6 +15,7 @@ from vbv_lernwelt.course.creators.test_utils import ( from vbv_lernwelt.course.models import CourseCompletionStatus, CourseSessionUser from vbv_lernwelt.course.services import mark_course_completion from vbv_lernwelt.learning_mentor.models import LearningMentor +from vbv_lernwelt.learnpath.models import LearningUnitPerformanceFeedbackType from vbv_lernwelt.self_evaluation_feedback.models import ( CourseCompletionFeedback, SelfEvaluationFeedback, @@ -154,7 +155,9 @@ class SelfEvaluationFeedbackAPI(APITestCase): """Tests endpoint of feedback REQUESTER""" # GIVEN - learning_unit = create_learning_unit(course=self.course, circle=self.circle) + learning_unit = create_learning_unit( # noqa + course=self.course, circle=self.circle + ) performance_criteria_1 = create_performance_criteria_page( course=self.course, @@ -204,11 +207,11 @@ class SelfEvaluationFeedbackAPI(APITestCase): feedback = response.data self.assertEqual(feedback["learning_unit_id"], learning_unit.id) - self.assertEqual(feedback["feedback_submitted"], False) - self.assertEqual(feedback["circle_name"], self.circle.title) + self.assertFalse(feedback["feedback_submitted"]) + self.assertEqual(feedback["circle_name"], self.circle.title) # noqa provider_user = feedback["feedback_provider_user"] - self.assertEqual(provider_user["id"], str(self.mentor.id)) # noqa + self.assertEqual(provider_user["id"], str(self.mentor.id)) self.assertEqual(provider_user["first_name"], self.mentor.first_name) self.assertEqual(provider_user["last_name"], self.mentor.last_name) self.assertEqual(provider_user["avatar_url"], self.mentor.avatar_url) @@ -243,11 +246,205 @@ class SelfEvaluationFeedbackAPI(APITestCase): CourseCompletionStatus.UNKNOWN.value, ) + def test_feedbacks_with_mixed_completion_statuses(self): + """Case: CourseCompletion AND feedbacks with mixed completion statuses""" + + # GIVEN + learning_unit = create_learning_unit( + course=self.course, + circle=self.circle, + feedback_user=LearningUnitPerformanceFeedbackType.MENTOR_FEEDBACK, + ) + + feedback = create_self_evaluation_feedback( + learning_unit=learning_unit, + feedback_requester_user=self.member, + feedback_provider_user=self.mentor, + ) + + feedback.feedback_submitted = True + feedback.save() + + for status in [ + CourseCompletionStatus.SUCCESS, + CourseCompletionStatus.FAIL, + CourseCompletionStatus.UNKNOWN, + ]: + criteria_page = create_performance_criteria_page( + course=self.course, + course_page=self.course_page, + circle=self.circle, + learning_unit=learning_unit, + ) + + # self assessment + completion = mark_course_completion( + page=criteria_page, + user=self.member, + course_session=self.course_session, + completion_status=status.value, + ) + + # feedback assessment + CourseCompletionFeedback.objects.create( + feedback=feedback, + course_completion=completion, + feedback_assessment=status.value, + ) + + self.client.force_login(self.member) + + # WHEN + response = self.client.get( + reverse( + "get_self_evaluation_feedbacks_as_requester", + args=[self.course_session.id], + ) + ) + + # THEN + self.assertEqual(response.status_code, 200) + + result = response.data["results"][0] + + self_assessment = result["self_assessment"] + self.assertEqual(self_assessment["pass"], 1) + self.assertEqual(self_assessment["fail"], 1) + self.assertEqual(self_assessment["unknown"], 1) + + feedback_assessment = result["feedback_assessment"] + self.assertEqual(feedback_assessment["counts"]["pass"], 1) + self.assertEqual(feedback_assessment["counts"]["fail"], 1) + self.assertEqual(feedback_assessment["counts"]["unknown"], 1) + + self.assertTrue(feedback_assessment["submitted_by_provider"]) + self.assertEqual( + feedback_assessment["provider_user"]["id"], str(self.mentor.id) + ) + + def test_no_feedbacks_but_with_completion_status(self): + """Case: CourseCompletion but NO feedback""" + + # GIVEN + learning_unit_with_success_feedback = create_learning_unit( + course=self.course, + circle=self.circle, + feedback_user=LearningUnitPerformanceFeedbackType.MENTOR_FEEDBACK, + ) + + performance_criteria_page = create_performance_criteria_page( + course=self.course, + course_page=self.course_page, + circle=self.circle, + learning_unit=learning_unit_with_success_feedback, + ) + + # IMPORTANT: CourseCompletion but NO feedback! + + mark_course_completion( + page=performance_criteria_page, + user=self.member, + course_session=self.course_session, + completion_status=CourseCompletionStatus.SUCCESS.value, + ) + + self.client.force_login(self.member) + + # WHEN + response = self.client.get( + reverse( + "get_self_evaluation_feedbacks_as_requester", + args=[self.course_session.id], + ) + ) + + # THEN + self.assertEqual(response.status_code, 200) + + result = response.data["results"][0] + self.assertEqual(result["self_assessment"]["pass"], 1) + self.assertEqual(result["self_assessment"]["fail"], 0) + self.assertEqual(result["self_assessment"]["unknown"], 0) + + def test_feedbacks_not_started(self): + """Case: Learning unit with no completion status and no feedback""" + + # GIVEN + learning_unit = create_learning_unit( # noqa + course=self.course, + circle=self.circle, + feedback_user=LearningUnitPerformanceFeedbackType.MENTOR_FEEDBACK, + ) + + create_performance_criteria_page( + course=self.course, + course_page=self.course_page, + circle=self.circle, + learning_unit=learning_unit, + ) + + self.client.force_login(self.member) + + # WHEN + response = self.client.get( + reverse( + "get_self_evaluation_feedbacks_as_requester", + args=[self.course_session.id], + ) + ) + + # THEN + self.assertEqual(response.status_code, 200) + + result = response.data["results"][0] + self.assertEqual(result["self_assessment"]["pass"], 0) + self.assertEqual(result["self_assessment"]["fail"], 0) + self.assertEqual(result["self_assessment"]["unknown"], 1) + + def test_feedbacks_metadata(self): + # GIVEN + learning_unit = create_learning_unit( # noqa + course=self.course, + circle=self.circle, + feedback_user=LearningUnitPerformanceFeedbackType.MENTOR_FEEDBACK, + ) + + create_performance_criteria_page( + course=self.course, + course_page=self.course_page, + circle=self.circle, + learning_unit=learning_unit, + ) + + self.client.force_login(self.member) + + # WHEN + response = self.client.get( + reverse( + "get_self_evaluation_feedbacks_as_requester", + args=[self.course_session.id], + ) + ) + + # THEN + self.assertEqual(response.status_code, 200) + + result = response.data["results"][0] + self.assertEqual(result["title"], learning_unit.title) + self.assertEqual(result["id"], learning_unit.id) + + circles = response.data["circles"] + self.assertEqual(len(circles), 1) + self.assertEqual(circles[0]["id"], self.circle.id) + self.assertEqual(circles[0]["title"], self.circle.title) + def test_get_self_evaluation_feedback_as_provider(self): """Tests endpoint of feedback PROVIDER""" # GIVEN - learning_unit = create_learning_unit(course=self.course, circle=self.circle) + learning_unit = create_learning_unit( # noqa + course=self.course, circle=self.circle + ) performance_criteria_1 = create_performance_criteria_page( course=self.course, @@ -299,7 +496,7 @@ class SelfEvaluationFeedbackAPI(APITestCase): self.assertEqual(feedback["learning_unit_id"], learning_unit.id) self.assertEqual(feedback["title"], learning_unit.title) self.assertEqual(feedback["feedback_submitted"], False) - self.assertEqual(feedback["circle_name"], self.circle.title) + self.assertEqual(feedback["circle_name"], self.circle.title) # noqa provider_user = feedback["feedback_provider_user"] self.assertEqual(provider_user["id"], str(self.mentor.id)) # noqa @@ -404,7 +601,7 @@ class SelfEvaluationFeedbackAPI(APITestCase): self.client.force_login(self.mentor) # WHEN - response = self.client.put( + self.client.put( reverse( "release_self_evaluation_feedback", args=[self_evaluation_feedback.id] ), diff --git a/server/vbv_lernwelt/self_evaluation_feedback/urls.py b/server/vbv_lernwelt/self_evaluation_feedback/urls.py index 0b0514de..ab9e7c9b 100644 --- a/server/vbv_lernwelt/self_evaluation_feedback/urls.py +++ b/server/vbv_lernwelt/self_evaluation_feedback/urls.py @@ -4,11 +4,18 @@ from vbv_lernwelt.self_evaluation_feedback.views import ( add_provider_self_evaluation_feedback, 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", + get_self_evaluation_feedbacks_as_requester, + name="get_self_evaluation_feedbacks_as_requester", + ), path( "requester//feedback/start", start_self_evaluation_feedback, @@ -19,6 +26,7 @@ urlpatterns = [ get_self_evaluation_feedback_as_requester, name="get_self_evaluation_feedback_as_requester", ), + # /provider/* URLs -> For the user who is providing feedback path( "provider//feedback", get_self_evaluation_feedback_as_provider, diff --git a/server/vbv_lernwelt/self_evaluation_feedback/utils.py b/server/vbv_lernwelt/self_evaluation_feedback/utils.py new file mode 100644 index 00000000..1d3fd5fb --- /dev/null +++ b/server/vbv_lernwelt/self_evaluation_feedback/utils.py @@ -0,0 +1,114 @@ +from typing import NamedTuple + +from django.db.models import Case, Count, IntegerField, Sum, Value, When +from django.db.models.functions import Coalesce + +from vbv_lernwelt.core.admin import User +from vbv_lernwelt.course.models import CourseCompletion, CourseCompletionStatus +from vbv_lernwelt.learnpath.models import LearningUnit +from vbv_lernwelt.self_evaluation_feedback.models import ( + CourseCompletionFeedback, + SelfEvaluationFeedback, +) + + +class AssessmentCounts(NamedTuple): + pass_count: int + fail_count: int + unknown_count: int + + +def get_self_evaluation_feedback_counts( + feedback: SelfEvaluationFeedback, +): + course_completion_feedback = CourseCompletionFeedback.objects.filter( + feedback=feedback + ).aggregate( + pass_count=Coalesce( + Sum( + Case( + When( + feedback_assessment=CourseCompletionStatus.SUCCESS.value, + then=Value(1), + ), + output_field=IntegerField(), + ) + ), + Value(0), + ), + fail_count=Coalesce( + Sum( + Case( + When( + feedback_assessment=CourseCompletionStatus.FAIL.value, + then=Value(1), + ), + output_field=IntegerField(), + ) + ), + Value(0), + ), + unknown_count=Coalesce( + Sum( + Case( + When( + feedback_assessment=CourseCompletionStatus.UNKNOWN.value, + then=Value(1), + ), + output_field=IntegerField(), + ) + ), + Value(0), + ), + ) + + return AssessmentCounts( + pass_count=course_completion_feedback.get("pass_count", 0), + fail_count=course_completion_feedback.get("fail_count", 0), + unknown_count=course_completion_feedback.get("unknown_count", 0), + ) + + +def get_self_assessment_counts( + learning_unit: LearningUnit, user: User +) -> AssessmentCounts: + performance_criteria = learning_unit.performancecriteria_set.all() + + completion_counts = CourseCompletion.objects.filter( + page__in=performance_criteria, user=user + ).aggregate( + pass_count=Count( + Case( + When(completion_status=CourseCompletionStatus.SUCCESS.value, then=1), + output_field=IntegerField(), + ) + ), + fail_count=Count( + Case( + When(completion_status=CourseCompletionStatus.FAIL.value, then=1), + output_field=IntegerField(), + ) + ), + unknown_count=Count( + Case( + When(completion_status=CourseCompletionStatus.UNKNOWN.value, then=1), + output_field=IntegerField(), + ) + ), + ) + + pass_count = completion_counts.get("pass_count", 0) + fail_count = completion_counts.get("fail_count", 0) + unknown_count = completion_counts.get("unknown_count", 0) + + # not yet completed performance criteria are unknown + if pass_count + fail_count + unknown_count < performance_criteria.count(): + unknown_count += performance_criteria.count() - ( + pass_count + fail_count + unknown_count + ) + + return AssessmentCounts( + pass_count=pass_count, + fail_count=fail_count, + unknown_count=unknown_count, + ) diff --git a/server/vbv_lernwelt/self_evaluation_feedback/views.py b/server/vbv_lernwelt/self_evaluation_feedback/views.py index 2bb02376..24e2eb2e 100644 --- a/server/vbv_lernwelt/self_evaluation_feedback/views.py +++ b/server/vbv_lernwelt/self_evaluation_feedback/views.py @@ -5,9 +5,14 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from vbv_lernwelt.core.models import User -from vbv_lernwelt.course.models import CourseCompletion +from vbv_lernwelt.core.serializers import UserSerializer +from vbv_lernwelt.course.models import CourseCompletion, CourseSession from vbv_lernwelt.learning_mentor.models import LearningMentor -from vbv_lernwelt.learnpath.models import LearningUnit +from vbv_lernwelt.learnpath.models import ( + Circle, + LearningUnit, + LearningUnitPerformanceFeedbackType, +) from vbv_lernwelt.notify.services import NotificationService from vbv_lernwelt.self_evaluation_feedback.models import ( CourseCompletionFeedback, @@ -16,6 +21,10 @@ from vbv_lernwelt.self_evaluation_feedback.models import ( from vbv_lernwelt.self_evaluation_feedback.serializers import ( SelfEvaluationFeedbackSerializer, ) +from vbv_lernwelt.self_evaluation_feedback.utils import ( + get_self_assessment_counts, + get_self_evaluation_feedback_counts, +) @api_view(["POST"]) @@ -80,6 +89,66 @@ def get_self_evaluation_feedback_as_provider(request, learning_unit_id): return Response(SelfEvaluationFeedbackSerializer(feedback).data) +@api_view(["GET"]) +@permission_classes([IsAuthenticated]) +def get_self_evaluation_feedbacks_as_requester(request, course_session_id: int): + course_session = get_object_or_404(CourseSession, id=course_session_id) + + results = [] + circle_ids = set() + + for learning_unit in LearningUnit.objects.filter( + feedback_user=LearningUnitPerformanceFeedbackType.MENTOR_FEEDBACK.value, + course_category__course=course_session.course, + ): + circle = learning_unit.get_parent().specific + circle_ids.add(circle.id) + + feedback_assessment = None + + feedback = SelfEvaluationFeedback.objects.filter( + learning_unit=learning_unit, + feedback_requester_user=request.user, + ).first() + + if feedback: + feedback_counts = get_self_evaluation_feedback_counts(feedback) + + feedback_assessment = { + "submitted_by_provider": feedback.feedback_submitted, + "provider_user": UserSerializer(feedback.feedback_provider_user).data, + "counts": { + "pass": feedback_counts.pass_count, + "fail": feedback_counts.fail_count, + "unknown": feedback_counts.unknown_count, + }, + } + + self_assessment_counts = get_self_assessment_counts(learning_unit, request.user) + + results.append( + { + "id": learning_unit.id, + "title": learning_unit.title, + "feedback_assessment": feedback_assessment, + "self_assessment": { + "pass": self_assessment_counts.pass_count, + "fail": self_assessment_counts.fail_count, + "unknown": self_assessment_counts.unknown_count, + }, + } + ) + + return Response( + { + "results": results, + "circles": list( + Circle.objects.filter(id__in=circle_ids).values("id", "title") + ), + } + ) + + @api_view(["GET"]) @permission_classes([IsAuthenticated]) def get_self_evaluation_feedback_as_requester(request, learning_unit_id):