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 @@
+
+
+
+
+
SelfEvaluationsAndFeedback
+
+
+
+
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):