feat: mentor cockpit self evaluation feedback

This commit is contained in:
Livio Bieri 2024-01-25 18:32:46 +01:00
parent 654ccb0d47
commit 864a00107e
9 changed files with 301 additions and 52 deletions

View File

@ -0,0 +1,23 @@
<script setup lang="ts">
import AssignmentItem from "@/components/cockpit/mentor/AssignmentItem.vue";
import type { RouteLocationRaw } from "vue-router";
defineProps<{
taskTitle: string;
circleTitle: string;
pendingTasks: number;
taskLink: RouteLocationRaw;
}>();
</script>
<template>
<AssignmentItem
:task-title="`${$t('a.Selbsteinschätzung')}: ${taskTitle}`"
:circle-title="circleTitle"
:pending-tasks="pendingTasks"
:task-link="taskLink"
:pending-tasks-label="$t('a.Selbsteinschätzung geteilt')"
:task-link-pending-label="$t('a.Fremdeinschätzung vornehmen')"
:task-link-label="$t('a.Selbsteinschätzung anzeigen')"
/>
</template>

View File

@ -6,6 +6,7 @@ import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
import { computed, type Ref, ref } from "vue"; import { computed, type Ref, ref } from "vue";
import PraxisAssignmentItem from "@/components/cockpit/mentor/PraxisAssignmentItem.vue"; import PraxisAssignmentItem from "@/components/cockpit/mentor/PraxisAssignmentItem.vue";
import { useTranslation } from "i18next-vue"; import { useTranslation } from "i18next-vue";
import SelfAssignmentFeedbackAssignmentItem from "@/components/cockpit/mentor/SelfAssignmentFeedbackAssignmentItem.vue";
const { t } = useTranslation(); const { t } = useTranslation();
const courseSession = useCurrentCourseSession(); const courseSession = useCurrentCourseSession();
@ -80,6 +81,16 @@ const filteredAssignments: Ref<PraxisAssignment[]> = computed(() => {
}" }"
:task-title="item.title" :task-title="item.title"
/> />
<SelfAssignmentFeedbackAssignmentItem
v-else-if="item.type === 'self_evaluation_feedback'"
:circle-title="mentorCockpitStore.getCircleTitleById(item.circle_id)"
:pending-tasks="item.pending_evaluations"
:task-link="{
name: 'mentorCockpitPraxisAssignments',
params: { praxisAssignmentId: item.id },
}"
:task-title="item.title"
/>
</template> </template>
</div> </div>
</template> </template>

View File

@ -2,17 +2,18 @@ from typing import List, Set, Tuple
from vbv_lernwelt.assignment.models import ( from vbv_lernwelt.assignment.models import (
Assignment, Assignment,
AssignmentCompletion,
AssignmentCompletionStatus, AssignmentCompletionStatus,
AssignmentType, AssignmentType,
AssignmentCompletion,
) )
from vbv_lernwelt.core.models import User from vbv_lernwelt.core.models import User
from vbv_lernwelt.course.models import CourseSession from vbv_lernwelt.course.models import CourseSession
from vbv_lernwelt.course_session.models import CourseSessionAssignment from vbv_lernwelt.course_session.models import CourseSessionAssignment
from vbv_lernwelt.learning_mentor.entities import ( from vbv_lernwelt.learning_mentor.entities import (
CompletionStatus, MentorCompletionStatus,
PraxisAssignmentCompletion, MentorAssignmentCompletion,
PraxisAssignmentStatus, MentorAssignmentStatus,
MentorAssignmentStatusType,
) )
@ -21,7 +22,7 @@ def get_assignment_completions(
assignment: Assignment, assignment: Assignment,
participants: List[User], participants: List[User],
evaluation_user: User, evaluation_user: User,
) -> List[PraxisAssignmentCompletion]: ) -> List[MentorAssignmentCompletion]:
evaluation_results = AssignmentCompletion.objects.filter( evaluation_results = AssignmentCompletion.objects.filter(
assignment_user__in=participants, assignment_user__in=participants,
course_session=course_session, course_session=course_session,
@ -34,14 +35,14 @@ def get_assignment_completions(
completion_status = result["completion_status"] completion_status = result["completion_status"]
if completion_status == AssignmentCompletionStatus.EVALUATION_SUBMITTED.value: if completion_status == AssignmentCompletionStatus.EVALUATION_SUBMITTED.value:
status = CompletionStatus.EVALUATED status = MentorCompletionStatus.EVALUATED
elif completion_status in [ elif completion_status in [
AssignmentCompletionStatus.SUBMITTED.value, AssignmentCompletionStatus.SUBMITTED.value,
AssignmentCompletionStatus.EVALUATION_IN_PROGRESS.value, AssignmentCompletionStatus.EVALUATION_IN_PROGRESS.value,
]: ]:
status = CompletionStatus.SUBMITTED status = MentorCompletionStatus.SUBMITTED
else: else:
status = CompletionStatus.UNKNOWN status = MentorCompletionStatus.UNKNOWN
user_status_map[result["assignment_user"]] = ( user_status_map[result["assignment_user"]] = (
status, status,
@ -49,25 +50,25 @@ def get_assignment_completions(
) )
status_priority = { status_priority = {
CompletionStatus.SUBMITTED: 1, MentorCompletionStatus.SUBMITTED: 1,
CompletionStatus.EVALUATED: 2, MentorCompletionStatus.EVALUATED: 2,
CompletionStatus.UNKNOWN: 3, MentorCompletionStatus.UNKNOWN: 3,
} }
sorted_participants = sorted( sorted_participants = sorted(
participants, participants,
key=lambda u: ( key=lambda u: (
status_priority.get( status_priority.get(
user_status_map.get(u.id, (CompletionStatus.UNKNOWN, ""))[0] user_status_map.get(u.id, (MentorCompletionStatus.UNKNOWN, ""))[0]
), ),
user_status_map.get(u.id, ("", u.last_name))[1], user_status_map.get(u.id, ("", u.last_name))[1],
), ),
) )
return [ return [
PraxisAssignmentCompletion( MentorAssignmentCompletion(
status=user_status_map.get( status=user_status_map.get(
user.id, (CompletionStatus.UNKNOWN, user.last_name) user.id, (MentorCompletionStatus.UNKNOWN, user.last_name)
)[0], )[0],
user_id=user.id, user_id=user.id,
last_name=user.last_name, last_name=user.last_name,
@ -78,7 +79,7 @@ def get_assignment_completions(
def get_praxis_assignments( def get_praxis_assignments(
course_session: CourseSession, participants: List[User], evaluation_user: User course_session: CourseSession, participants: List[User], evaluation_user: User
) -> Tuple[List[PraxisAssignmentStatus], Set[int]]: ) -> Tuple[List[MentorAssignmentStatus], Set[int]]:
records = [] records = []
circle_ids = set() circle_ids = set()
@ -104,19 +105,20 @@ def get_praxis_assignments(
[ [
completion completion
for completion in completions for completion in completions
if completion.status == CompletionStatus.SUBMITTED if completion.status == MentorCompletionStatus.SUBMITTED
] ]
) )
circle_id = learning_content.get_circle().id circle_id = learning_content.get_circle().id
records.append( records.append(
PraxisAssignmentStatus( MentorAssignmentStatus(
id=course_session_assignment.id, id=course_session_assignment.id,
title=learning_content.content_assignment.title, title=learning_content.content_assignment.title,
circle_id=circle_id, circle_id=circle_id,
pending_evaluations=submitted_count, pending_evaluations=submitted_count,
completions=completions, completions=completions,
type=MentorAssignmentStatusType.PRAXIS_ASSIGNMENT,
) )
) )

View File

@ -0,0 +1,99 @@
from typing import List, Set, Tuple
from django.db.models import Prefetch
from vbv_lernwelt.core.models import User
from vbv_lernwelt.learning_mentor.entities import (
MentorCompletionStatus,
MentorAssignmentStatus,
MentorAssignmentCompletion,
MentorAssignmentStatusType,
)
from vbv_lernwelt.self_evaluation_feedback.models import SelfEvaluationFeedback
def create_blank_completions_non_requesters(
completions: List[MentorAssignmentCompletion],
participants: List[User],
) -> List[MentorAssignmentCompletion]:
non_requester_completions = []
participants_ids = set([str(p.id) for p in participants])
completion_seen_user_ids = set([str(c.user_id) for c in completions])
user_by_id = {str(p.id): p for p in participants}
for non_requester_user_id in participants_ids - completion_seen_user_ids:
non_requester_user = user_by_id[non_requester_user_id]
non_requester_completions.append(
MentorAssignmentCompletion(
status=MentorCompletionStatus.UNKNOWN,
user_id=non_requester_user.id,
last_name=non_requester_user.last_name,
)
)
return non_requester_completions
def get_self_feedback_evaluation(
participants: List[User], evaluation_user: User
) -> Tuple[List[MentorAssignmentStatus], Set[int]]:
records: List[MentorAssignmentStatus] = []
circle_ids: Set[int] = set()
if not participants:
return records, circle_ids
feedbacks = SelfEvaluationFeedback.objects.prefetch_related(
Prefetch("learning_unit")
).filter(
feedback_requester_user__in=participants,
feedback_provider_user=evaluation_user,
)
feedback_by_learning_unit = {}
for feedback in feedbacks:
feedback_by_learning_unit.setdefault(feedback.learning_unit, []).append(
feedback
)
for learning_unit, feedbacks in feedback_by_learning_unit.items():
circle_id = learning_unit.get_circle().id
circle_ids.add(circle_id)
pending_evaluations = len([f for f in feedbacks if not f.feedback_submitted])
completions = [
MentorAssignmentCompletion(
# feedback_submitted as seen from the perspective of the evaluation user (feedback provider)
# means that the feedback has been evaluated by the feedback provider, hence the status is EVALUATED
status=MentorCompletionStatus.EVALUATED
if f.feedback_submitted
else MentorCompletionStatus.SUBMITTED,
user_id=f.feedback_requester_user.id,
last_name=f.feedback_requester_user.last_name,
)
for f in feedbacks
]
# requesting feedback is optional, so we need to add blank completions
# for those mentees who did not request a feedback
completions += create_blank_completions_non_requesters(
completions=completions,
participants=participants,
)
records.append(
MentorAssignmentStatus(
id=learning_unit.id,
title=learning_unit.title,
circle_id=circle_id,
pending_evaluations=pending_evaluations,
completions=completions,
type=MentorAssignmentStatusType.SELF_EVALUATION_FEEDBACK,
)
)
return records, circle_ids

View File

@ -3,23 +3,29 @@ from enum import Enum
from typing import List from typing import List
class CompletionStatus(str, Enum): class MentorCompletionStatus(str, Enum):
UNKNOWN = "UNKNOWN" UNKNOWN = "UNKNOWN"
SUBMITTED = "SUBMITTED" SUBMITTED = "SUBMITTED"
EVALUATED = "EVALUATED" EVALUATED = "EVALUATED"
class MentorAssignmentStatusType(str, Enum):
PRAXIS_ASSIGNMENT = "praxis_assignment"
SELF_EVALUATION_FEEDBACK = "self_evaluation_feedback"
@dataclass @dataclass
class PraxisAssignmentCompletion: class MentorAssignmentCompletion:
status: CompletionStatus status: MentorCompletionStatus
user_id: str user_id: str
last_name: str last_name: str
@dataclass @dataclass
class PraxisAssignmentStatus: class MentorAssignmentStatus:
id: str id: str
title: str title: str
circle_id: str circle_id: str
pending_evaluations: int pending_evaluations: int
completions: List[PraxisAssignmentCompletion] completions: List[MentorAssignmentCompletion]
type: MentorAssignmentStatusType

View File

@ -4,7 +4,7 @@ from vbv_lernwelt.core.serializers import UserSerializer
from vbv_lernwelt.learning_mentor.models import LearningMentor, MentorInvitation from vbv_lernwelt.learning_mentor.models import LearningMentor, MentorInvitation
class PraxisAssignmentCompletionSerializer(serializers.Serializer): class MentorAssignmentCompletionSerializer(serializers.Serializer):
status = serializers.SerializerMethodField() status = serializers.SerializerMethodField()
user_id = serializers.CharField() user_id = serializers.CharField()
last_name = serializers.CharField() last_name = serializers.CharField()
@ -14,13 +14,13 @@ class PraxisAssignmentCompletionSerializer(serializers.Serializer):
return obj.status.value return obj.status.value
class PraxisAssignmentStatusSerializer(serializers.Serializer): class MentorAssignmentStatusSerializer(serializers.Serializer):
id = serializers.CharField() id = serializers.CharField()
title = serializers.CharField() title = serializers.CharField()
circle_id = serializers.CharField() circle_id = serializers.CharField()
pending_evaluations = serializers.IntegerField() pending_evaluations = serializers.IntegerField()
completions = PraxisAssignmentCompletionSerializer(many=True) completions = MentorAssignmentCompletionSerializer(many=True)
type = serializers.ReadOnlyField(default="praxis_assignment") type = serializers.ReadOnlyField()
class InvitationSerializer(serializers.ModelSerializer): class InvitationSerializer(serializers.ModelSerializer):

View File

@ -18,7 +18,7 @@ from vbv_lernwelt.learning_mentor.content.praxis_assignment import (
get_assignment_completions, get_assignment_completions,
get_praxis_assignments, get_praxis_assignments,
) )
from vbv_lernwelt.learning_mentor.entities import CompletionStatus from vbv_lernwelt.learning_mentor.entities import MentorCompletionStatus
class AttendanceServicesTestCase(TestCase): class AttendanceServicesTestCase(TestCase):
@ -74,10 +74,10 @@ class AttendanceServicesTestCase(TestCase):
# THEN # THEN
expected_order = ["Beta", "Alpha", "Gamma", "Kappa"] expected_order = ["Beta", "Alpha", "Gamma", "Kappa"]
expected_statuses = { expected_statuses = {
"Alpha": CompletionStatus.EVALUATED, # user1 "Alpha": MentorCompletionStatus.EVALUATED, # user1
"Beta": CompletionStatus.SUBMITTED, # user2 "Beta": MentorCompletionStatus.SUBMITTED, # user2
"Gamma": CompletionStatus.UNKNOWN, # user4 (no AssignmentCompletion) "Gamma": MentorCompletionStatus.UNKNOWN, # user4 (no AssignmentCompletion)
"Kappa": CompletionStatus.UNKNOWN, # user3 (IN_PROGRESS should be PENDING) "Kappa": MentorCompletionStatus.UNKNOWN, # user3 (IN_PROGRESS should be PENDING)
} }
self.assertEqual(len(results), len(participants)) self.assertEqual(len(results), len(participants))

View File

@ -1,3 +1,5 @@
from typing import List, Optional, Dict
from django.urls import reverse from django.urls import reverse
from rest_framework import status from rest_framework import status
from rest_framework.test import APITestCase from rest_framework.test import APITestCase
@ -7,6 +9,7 @@ from vbv_lernwelt.assignment.models import (
AssignmentCompletionStatus, AssignmentCompletionStatus,
AssignmentType, AssignmentType,
) )
from vbv_lernwelt.core.admin import User
from vbv_lernwelt.course.creators.test_utils import ( from vbv_lernwelt.course.creators.test_utils import (
add_course_session_user, add_course_session_user,
create_assignment, create_assignment,
@ -16,9 +19,20 @@ from vbv_lernwelt.course.creators.test_utils import (
create_course_session, create_course_session,
create_course_session_assignment, create_course_session_assignment,
create_user, create_user,
create_learning_unit,
) )
from vbv_lernwelt.course.models import CourseSessionUser from vbv_lernwelt.course.models import CourseSessionUser
from vbv_lernwelt.learning_mentor.models import LearningMentor from vbv_lernwelt.learning_mentor.models import LearningMentor
from vbv_lernwelt.self_evaluation_feedback.models import SelfEvaluationFeedback
def get_completion_for_user(
completions: List[Dict[str, str]], user: User
) -> Optional[Dict[str, str]]:
for completion in completions:
if completion["user_id"] == str(user.id):
return completion
return None
class LearningMentorAPITest(APITestCase): class LearningMentorAPITest(APITestCase):
@ -28,15 +42,6 @@ class LearningMentorAPITest(APITestCase):
self.circle, _ = create_circle(title="Circle", course_page=self.course_page) self.circle, _ = create_circle(title="Circle", course_page=self.course_page)
self.assignment = create_assignment(
course=self.course, assignment_type=AssignmentType.PRAXIS_ASSIGNMENT
)
lca = create_assignment_learning_content(self.circle, self.assignment)
create_course_session_assignment(
course_session=self.course_session, learning_content_assignment=lca
)
self.mentor = create_user("mentor") self.mentor = create_user("mentor")
self.participant_1 = add_course_session_user( self.participant_1 = add_course_session_user(
self.course_session, self.course_session,
@ -109,7 +114,7 @@ class LearningMentorAPITest(APITestCase):
self.assertEqual(participant_1["first_name"], "Test") self.assertEqual(participant_1["first_name"], "Test")
self.assertEqual(participant_1["last_name"], "Participant_1") self.assertEqual(participant_1["last_name"], "Participant_1")
def test_api_praxis_assignments(self) -> None: def test_api_self_evaluation_feedback(self) -> None:
# GIVEN # GIVEN
participants = [self.participant_1, self.participant_2, self.participant_3] participants = [self.participant_1, self.participant_2, self.participant_3]
self.client.force_login(self.mentor) self.client.force_login(self.mentor)
@ -118,12 +123,97 @@ class LearningMentorAPITest(APITestCase):
mentor=self.mentor, mentor=self.mentor,
course=self.course_session.course, course=self.course_session.course,
) )
mentor.participants.set(participants)
learning_unit = create_learning_unit(
circle=self.circle,
course=self.course,
)
# 1: we already evaluated
SelfEvaluationFeedback.objects.create(
feedback_requester_user=self.participant_1.user,
feedback_provider_user=self.mentor,
learning_unit=learning_unit,
feedback_submitted=True,
)
# 2: we have not evaluated yet
SelfEvaluationFeedback.objects.create(
feedback_requester_user=self.participant_2.user,
feedback_provider_user=self.mentor,
learning_unit=learning_unit,
feedback_submitted=False,
)
# 3: did not request feedback
# ...
# WHEN
response = self.client.get(self.url)
# THEN
assignments = response.data["assignments"]
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
response.data["circles"],
[{"id": self.circle.id, "title": self.circle.title}],
)
self.assertEqual(len(assignments), 1)
assignment = assignments[0]
self.assertEqual(assignment["type"], "self_evaluation_feedback")
self.assertEqual(assignment["pending_evaluations"], 1)
completions = assignment["completions"]
self.assertEqual(
len(completions),
3,
)
completion_1 = get_completion_for_user(completions, self.participant_1.user)
self.assertEqual(completion_1["status"], "EVALUATED")
self.assertEqual(completion_1["last_name"], "Participant_1")
self.assertEqual(completion_1["user_id"], str(self.participant_1.user.id))
completion_2 = get_completion_for_user(completions, self.participant_2.user)
self.assertEqual(completion_2["status"], "SUBMITTED")
self.assertEqual(completion_2["last_name"], "Participant_2")
self.assertEqual(completion_2["user_id"], str(self.participant_2.user.id))
completion_3 = get_completion_for_user(completions, self.participant_3.user)
self.assertEqual(completion_3["status"], "UNKNOWN")
self.assertEqual(completion_3["last_name"], "Participant_3")
self.assertEqual(completion_3["user_id"], str(self.participant_3.user.id))
def test_api_praxis_assignments(self) -> None:
# GIVEN
self.client.force_login(self.mentor)
assignment = create_assignment(
course=self.course, assignment_type=AssignmentType.PRAXIS_ASSIGNMENT
)
lca = create_assignment_learning_content(self.circle, assignment)
create_course_session_assignment(
course_session=self.course_session, learning_content_assignment=lca
)
mentor = LearningMentor.objects.create(
mentor=self.mentor,
course=self.course_session.course,
)
participants = [self.participant_1, self.participant_2, self.participant_3]
mentor.participants.set(participants) mentor.participants.set(participants)
AssignmentCompletion.objects.create( AssignmentCompletion.objects.create(
assignment_user=self.participant_1.user, assignment_user=self.participant_1.user,
course_session=self.course_session, course_session=self.course_session,
assignment=self.assignment, assignment=assignment,
completion_status=AssignmentCompletionStatus.EVALUATION_SUBMITTED.value, completion_status=AssignmentCompletionStatus.EVALUATION_SUBMITTED.value,
evaluation_user=self.mentor, evaluation_user=self.mentor,
) )
@ -131,7 +221,7 @@ class LearningMentorAPITest(APITestCase):
AssignmentCompletion.objects.create( AssignmentCompletion.objects.create(
assignment_user=self.participant_3.user, assignment_user=self.participant_3.user,
course_session=self.course_session, course_session=self.course_session,
assignment=self.assignment, assignment=assignment,
completion_status=AssignmentCompletionStatus.SUBMITTED.value, completion_status=AssignmentCompletionStatus.SUBMITTED.value,
evaluation_user=self.mentor, evaluation_user=self.mentor,
) )

View File

@ -12,11 +12,14 @@ from vbv_lernwelt.iam.permissions import has_role_in_course, is_course_session_m
from vbv_lernwelt.learning_mentor.content.praxis_assignment import ( from vbv_lernwelt.learning_mentor.content.praxis_assignment import (
get_praxis_assignments, get_praxis_assignments,
) )
from vbv_lernwelt.learning_mentor.content.self_evaluation_feedback import (
get_self_feedback_evaluation,
)
from vbv_lernwelt.learning_mentor.models import LearningMentor, MentorInvitation from vbv_lernwelt.learning_mentor.models import LearningMentor, MentorInvitation
from vbv_lernwelt.learning_mentor.serializers import ( from vbv_lernwelt.learning_mentor.serializers import (
InvitationSerializer, InvitationSerializer,
MentorSerializer, MentorSerializer,
PraxisAssignmentStatusSerializer, MentorAssignmentStatusSerializer,
) )
from vbv_lernwelt.learnpath.models import Circle from vbv_lernwelt.learnpath.models import Circle
from vbv_lernwelt.notify.email.email_services import EmailTemplate, send_email from vbv_lernwelt.notify.email.email_services import EmailTemplate, send_email
@ -37,24 +40,39 @@ def mentor_summary(request, course_session_id: int):
assignments = [] assignments = []
circle_ids = set() circle_ids = set()
praxis_assignments, _circle_ids = get_praxis_assignments( praxis_assignments, praxis_assignments_circle_ids = get_praxis_assignments(
course_session=course_session, participants=users, evaluation_user=request.user course_session=course_session,
participants=users,
evaluation_user=request.user, # noqa
)
(
self_evaluation_feedbacks,
self_evaluation_feedback_circle_ids,
) = get_self_feedback_evaluation(
participants=users,
evaluation_user=request.user, # noqa
)
circle_ids.update(praxis_assignments_circle_ids)
circle_ids.update(self_evaluation_feedback_circle_ids)
assignments.extend(
MentorAssignmentStatusSerializer(praxis_assignments, many=True).data
) )
assignments.extend( assignments.extend(
PraxisAssignmentStatusSerializer(praxis_assignments, many=True).data MentorAssignmentStatusSerializer(self_evaluation_feedbacks, many=True).data
) )
circle_ids.update(_circle_ids)
circles = Circle.objects.filter(id__in=circle_ids).values("id", "title")
assignments.sort( assignments.sort(
key=lambda x: (-x.get("pending_evaluations", 0), x.get("title", "").lower()) key=lambda x: (-x.get("pending_evaluations", 0), x.get("title", "").lower())
) )
return Response( return Response(
{ {
"participants": [UserSerializer(user).data for user in users], "participants": [UserSerializer(user).data for user in users],
"circles": list(circles), "circles": list(
Circle.objects.filter(id__in=circle_ids).values("id", "title")
),
"assignments": assignments, "assignments": assignments,
} }
) )