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 PraxisAssignmentItem from "@/components/cockpit/mentor/PraxisAssignmentItem.vue";
import { useTranslation } from "i18next-vue";
import SelfAssignmentFeedbackAssignmentItem from "@/components/cockpit/mentor/SelfAssignmentFeedbackAssignmentItem.vue";
const { t } = useTranslation();
const courseSession = useCurrentCourseSession();
@ -80,6 +81,16 @@ const filteredAssignments: Ref<PraxisAssignment[]> = computed(() => {
}"
: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>
</div>
</template>

View File

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

View File

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

View File

@ -1,3 +1,5 @@
from typing import List, Optional, Dict
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
@ -7,6 +9,7 @@ from vbv_lernwelt.assignment.models import (
AssignmentCompletionStatus,
AssignmentType,
)
from vbv_lernwelt.core.admin import User
from vbv_lernwelt.course.creators.test_utils import (
add_course_session_user,
create_assignment,
@ -16,9 +19,20 @@ from vbv_lernwelt.course.creators.test_utils import (
create_course_session,
create_course_session_assignment,
create_user,
create_learning_unit,
)
from vbv_lernwelt.course.models import CourseSessionUser
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):
@ -28,15 +42,6 @@ class LearningMentorAPITest(APITestCase):
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.participant_1 = add_course_session_user(
self.course_session,
@ -109,7 +114,7 @@ class LearningMentorAPITest(APITestCase):
self.assertEqual(participant_1["first_name"], "Test")
self.assertEqual(participant_1["last_name"], "Participant_1")
def test_api_praxis_assignments(self) -> None:
def test_api_self_evaluation_feedback(self) -> None:
# GIVEN
participants = [self.participant_1, self.participant_2, self.participant_3]
self.client.force_login(self.mentor)
@ -118,12 +123,97 @@ class LearningMentorAPITest(APITestCase):
mentor=self.mentor,
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)
AssignmentCompletion.objects.create(
assignment_user=self.participant_1.user,
course_session=self.course_session,
assignment=self.assignment,
assignment=assignment,
completion_status=AssignmentCompletionStatus.EVALUATION_SUBMITTED.value,
evaluation_user=self.mentor,
)
@ -131,7 +221,7 @@ class LearningMentorAPITest(APITestCase):
AssignmentCompletion.objects.create(
assignment_user=self.participant_3.user,
course_session=self.course_session,
assignment=self.assignment,
assignment=assignment,
completion_status=AssignmentCompletionStatus.SUBMITTED.value,
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 (
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.serializers import (
InvitationSerializer,
MentorSerializer,
PraxisAssignmentStatusSerializer,
MentorAssignmentStatusSerializer,
)
from vbv_lernwelt.learnpath.models import Circle
from vbv_lernwelt.notify.email.email_services import EmailTemplate, send_email
@ -37,24 +40,39 @@ def mentor_summary(request, course_session_id: int):
assignments = []
circle_ids = set()
praxis_assignments, _circle_ids = get_praxis_assignments(
course_session=course_session, participants=users, evaluation_user=request.user
praxis_assignments, praxis_assignments_circle_ids = get_praxis_assignments(
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(
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(
key=lambda x: (-x.get("pending_evaluations", 0), x.get("title", "").lower())
)
return Response(
{
"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,
}
)