diff --git a/client/src/composables.ts b/client/src/composables.ts index 3b8730e2..f9f049e7 100644 --- a/client/src/composables.ts +++ b/client/src/composables.ts @@ -20,6 +20,7 @@ import type { CourseSession, CourseSessionDetail, LearningContentWithCompletion, + LearningMentor, LearningPathType, LearningUnitPerformanceCriteria, PerformanceCriteria, @@ -466,7 +467,7 @@ export function useFileUpload() { } export function useLearningMentors() { - const learningMentors = ref([]); + const learningMentors = ref([]); const currentCourseSessionId = useCurrentCourseSession().value.id; const fetchMentors = async () => { diff --git a/client/src/types.ts b/client/src/types.ts index 5cc139c5..19146357 100644 --- a/client/src/types.ts +++ b/client/src/types.ts @@ -456,6 +456,17 @@ export interface ExpertSessionUser extends CourseSessionUser { role: "EXPERT"; } +export interface Mentor { + id: number; + first_name: string; + last_name: string; +} + +export interface LearningMentor { + id: number; + mentor: Mentor; +} + export type CourseSessionDetail = CourseSessionObjectType; // document upload diff --git a/server/config/settings/base.py b/server/config/settings/base.py index b073746e..81090a32 100644 --- a/server/config/settings/base.py +++ b/server/config/settings/base.py @@ -133,6 +133,7 @@ LOCAL_APPS = [ "vbv_lernwelt.course_session_group", "vbv_lernwelt.shop", "vbv_lernwelt.learning_mentor", + "vbv_lernwelt.self_evaluation_feedback", ] # https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS diff --git a/server/config/urls.py b/server/config/urls.py index f7d27476..d244b960 100644 --- a/server/config/urls.py +++ b/server/config/urls.py @@ -133,6 +133,9 @@ urlpatterns = [ path("api/mentor//", include("vbv_lernwelt.learning_mentor.urls")), + # self evaluation feedback + path("api/self-evaluation-feedback/", include("vbv_lernwelt.self_evaluation_feedback.urls")), + # assignment path( r"api/assignment///status/", diff --git a/server/vbv_lernwelt/course/creators/test_utils.py b/server/vbv_lernwelt/course/creators/test_utils.py index 3a396754..51360fc6 100644 --- a/server/vbv_lernwelt/course/creators/test_utils.py +++ b/server/vbv_lernwelt/course/creators/test_utils.py @@ -268,10 +268,21 @@ def create_course_session_edoniq_test( return cset +def create_learning_unit(circle: Circle, course: Course): + cat, _ = CourseCategory.objects.get_or_create( + course=course, title="Course Category" + ) + + return LearningUnitFactory( + title="Learning Unit", parent=circle, course_category=cat + ) + + def create_performance_criteria_page( course: Course, course_page: CoursePage, circle: Circle, + learning_unit: LearningUnitFactory | None = None, ) -> PerformanceCriteria: competence_navi_page = CompetenceNaviPageFactory( title="Competence Navi", @@ -290,17 +301,14 @@ def create_performance_criteria_page( items=[("item", "Action Competence Item")], ) - cat, _ = CourseCategory.objects.get_or_create( - course=course, title="Course Category" - ) - - lu = LearningUnitFactory(title="Learning Unit", parent=circle, course_category=cat) + if not learning_unit: + learning_unit = create_learning_unit(circle=circle, course=course) return PerformanceCriteriaFactory( parent=action_competence, competence_id="X1.1", title="Performance Criteria", - learning_unit=lu, + learning_unit=learning_unit, ) diff --git a/server/vbv_lernwelt/course/models.py b/server/vbv_lernwelt/course/models.py index a6c8fd80..cbd52a55 100644 --- a/server/vbv_lernwelt/course/models.py +++ b/server/vbv_lernwelt/course/models.py @@ -329,25 +329,3 @@ class CircleDocument(models.Model): self.file.upload_finished_at = None self.file.save() return super().delete(*args, **kwargs) - - -# TODO: Model something like this: -# class LearningUnitCompletionFeedback(models.Model): -# assignment_user = models.ForeignKey(User, on_delete=models.CASCADE) -# feedback_user = models.ForeignKey(User, on_delete=models.CASCADE) -# feedback_submitted = models.BooleanField(default=False) -# learning_unit = models.ForeignKey( -# "learnpath.LearningUnit", on_delete=models.CASCADE -# ) -# -# -# class CourseCompletionFeedback(models.Model): -# learning_unit_completion_feedback = models.ForeignKey( -# LearningUnitCompletionFeedback, on_delete=models.CASCADE -# ) -# course_completion = models.ForeignKey(CourseCompletion, on_delete=models.CASCADE) -# feedback_status = models.CharField( -# max_length=255, -# choices=[(status, status.value) for status in CourseCompletionStatus], -# default=CourseCompletionStatus.UNKNOWN.value, -# ) diff --git a/server/vbv_lernwelt/self_evaluation_feedback/__init__.py b/server/vbv_lernwelt/self_evaluation_feedback/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/server/vbv_lernwelt/self_evaluation_feedback/admin.py b/server/vbv_lernwelt/self_evaluation_feedback/admin.py new file mode 100644 index 00000000..8c38f3f3 --- /dev/null +++ b/server/vbv_lernwelt/self_evaluation_feedback/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/server/vbv_lernwelt/self_evaluation_feedback/apps.py b/server/vbv_lernwelt/self_evaluation_feedback/apps.py new file mode 100644 index 00000000..d0cec9eb --- /dev/null +++ b/server/vbv_lernwelt/self_evaluation_feedback/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class SelfEvaluationFeedbackConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "vbv_lernwelt.self_evaluation_feedback" diff --git a/server/vbv_lernwelt/self_evaluation_feedback/migrations/0001_initial.py b/server/vbv_lernwelt/self_evaluation_feedback/migrations/0001_initial.py new file mode 100644 index 00000000..faa03afb --- /dev/null +++ b/server/vbv_lernwelt/self_evaluation_feedback/migrations/0001_initial.py @@ -0,0 +1,113 @@ +# Generated by Django 3.2.20 on 2024-01-21 18:42 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + +import vbv_lernwelt.course.models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + ("course", "0006_auto_20231221_1411"), + ("learnpath", "0014_alter_learningunit_feedback_user"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="SelfEvaluationFeedback", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("feedback_submitted", models.BooleanField(default=False)), + ( + "feedback_provider_user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="feedback_provider_user", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "feedback_requester_user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="feedback_requester_user", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "learning_unit", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="learnpath.learningunit", + ), + ), + ], + ), + migrations.CreateModel( + name="CourseCompletionFeedback", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "provider_evaluation_feedback", + models.CharField( + choices=[ + ( + vbv_lernwelt.course.models.CourseCompletionStatus[ + "SUCCESS" + ], + "SUCCESS", + ), + ( + vbv_lernwelt.course.models.CourseCompletionStatus[ + "FAIL" + ], + "FAIL", + ), + ( + vbv_lernwelt.course.models.CourseCompletionStatus[ + "UNKNOWN" + ], + "UNKNOWN", + ), + ], + default="UNKNOWN", + max_length=255, + ), + ), + ( + "feedback", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="self_evaluation_feedback.selfevaluationfeedback", + ), + ), + ( + "requester_evaluation", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="course.coursecompletion", + ), + ), + ], + ), + ] diff --git a/server/vbv_lernwelt/self_evaluation_feedback/migrations/__init__.py b/server/vbv_lernwelt/self_evaluation_feedback/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/server/vbv_lernwelt/self_evaluation_feedback/models.py b/server/vbv_lernwelt/self_evaluation_feedback/models.py new file mode 100644 index 00000000..cfc7477f --- /dev/null +++ b/server/vbv_lernwelt/self_evaluation_feedback/models.py @@ -0,0 +1,33 @@ +from django.db import models + +from vbv_lernwelt.core.admin import User +from vbv_lernwelt.course.models import CourseCompletion, CourseCompletionStatus + + +class SelfEvaluationFeedback(models.Model): + feedback_submitted = models.BooleanField(default=False) + + feedback_requester_user = models.ForeignKey( + User, on_delete=models.CASCADE, related_name="feedback_requester_user" + ) + + feedback_provider_user = models.ForeignKey( + User, on_delete=models.CASCADE, related_name="feedback_provider_user" + ) + + learning_unit = models.ForeignKey( + "learnpath.LearningUnit", on_delete=models.CASCADE + ) + + +class CourseCompletionFeedback(models.Model): + feedback = models.ForeignKey(SelfEvaluationFeedback, on_delete=models.CASCADE) + + # the course completion has to be evaluated by the feedback provider + requester_evaluation = models.ForeignKey(CourseCompletion, on_delete=models.CASCADE) + + provider_evaluation_feedback = models.CharField( + max_length=255, + choices=[(status, status.value) for status in CourseCompletionStatus], + default=CourseCompletionStatus.UNKNOWN.value, + ) diff --git a/server/vbv_lernwelt/self_evaluation_feedback/serializers.py b/server/vbv_lernwelt/self_evaluation_feedback/serializers.py new file mode 100644 index 00000000..2e1c7d72 --- /dev/null +++ b/server/vbv_lernwelt/self_evaluation_feedback/serializers.py @@ -0,0 +1,40 @@ +from typing import List + +from rest_framework import serializers + +from vbv_lernwelt.competence.models import PerformanceCriteria +from vbv_lernwelt.core.serializers import UserSerializer +from vbv_lernwelt.self_evaluation_feedback.models import SelfEvaluationFeedback + + +class SelfEvaluationFeedbackSerializer(serializers.ModelSerializer): + criteria = serializers.SerializerMethodField() + feedback_requester_user = UserSerializer(read_only=True) + feedback_provider_user = UserSerializer(read_only=True) + learning_unit_id = serializers.PrimaryKeyRelatedField( + read_only=True, source="learning_unit" + ) + + class Meta: + model = SelfEvaluationFeedback + fields = [ + "id", + "learning_unit_id", + "feedback_submitted", + "feedback_requester_user", + "feedback_provider_user", + "criteria", + ] + + def get_criteria(self, obj): + performance_criteria: List[ + PerformanceCriteria + ] = obj.learning_unit.performancecriteria_set.all() + + return [ + { + "id": criteria.id, + "title": criteria.title, + } + for criteria in performance_criteria + ] diff --git a/server/vbv_lernwelt/self_evaluation_feedback/tests/__init__.py b/server/vbv_lernwelt/self_evaluation_feedback/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/server/vbv_lernwelt/self_evaluation_feedback/tests/test_api.py b/server/vbv_lernwelt/self_evaluation_feedback/tests/test_api.py new file mode 100644 index 00000000..ce5febfc --- /dev/null +++ b/server/vbv_lernwelt/self_evaluation_feedback/tests/test_api.py @@ -0,0 +1,116 @@ +from django.urls import reverse +from rest_framework.test import APITestCase + +from vbv_lernwelt.course.creators.test_utils import ( + add_course_session_user, + create_circle, + create_course, + create_course_session, + create_learning_unit, + create_performance_criteria_page, + create_user, +) +from vbv_lernwelt.course.models import CourseSessionUser +from vbv_lernwelt.course.services import mark_course_completion +from vbv_lernwelt.learning_mentor.models import LearningMentor +from vbv_lernwelt.self_evaluation_feedback.models import SelfEvaluationFeedback + + +def create_self_evaluation_feedback( + learning_unit, feedback_requester_user, feedback_provider_user +): + return SelfEvaluationFeedback.objects.create( + learning_unit=learning_unit, + feedback_requester_user=feedback_requester_user, + feedback_provider_user=feedback_provider_user, + ) + + +class SelfEvaluationFeedbackAPI(APITestCase): + def setUp(self) -> None: + self.member = create_user("member") + self.mentor = create_user("mentor") + + self.course, self.course_page = create_course("Test Course") + self.course_session = create_course_session( + course=self.course, title="Test Bern 2022 a" + ) + + member_csu = add_course_session_user( + course_session=self.course_session, + user=self.member, + role=CourseSessionUser.Role.MEMBER, + ) + + self.circle, _ = create_circle( + title="Test Circle", course_page=self.course_page + ) + + learning_mentor = LearningMentor.objects.create( + mentor=self.mentor, + course=self.course_session.course, + ) + + learning_mentor.participants.add(member_csu) + + self.client.force_login(self.member) + + def test_start_self_evaluation_feedback(self): + # GIVEN + learning_unit = create_learning_unit(course=self.course, circle=self.circle) + + pc = create_performance_criteria_page( + course=self.course, + course_page=self.course_page, + circle=self.circle, + learning_unit=learning_unit, + ) + + mark_course_completion( + page=pc, + user=self.member, + course_session=self.course_session, + completion_status="SUCCESS", + ) + + # WHEN + response = self.client.post( + reverse("start_self_evaluation_feedback"), + { + "learning_unit_id": learning_unit.id, + "feedback_provider_user_id": self.mentor.id, + }, + ) + + # THEN + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data["success"], True) + + self.assertEqual( + SelfEvaluationFeedback.objects.count(), + 1, + ) + + # just get the first one + f = SelfEvaluationFeedback.objects.first() + + self.assertEqual(f.feedback_requester_user, self.member) + self.assertEqual(f.feedback_provider_user, self.mentor) + self.assertEqual(f.learning_unit, learning_unit) + + def test_start_self_evaluation_feedback_not_allowed_user(self): + # GIVEN + learning_unit = create_learning_unit(course=self.course, circle=self.circle) + not_a_mentor = create_user("not_a_mentor") + + # WHEN + response = self.client.post( + reverse("start_self_evaluation_feedback"), + { + "learning_unit_id": learning_unit.id, + "feedback_provider_user_id": not_a_mentor.id, + }, + ) + + # THEN + self.assertEqual(response.status_code, 403) diff --git a/server/vbv_lernwelt/self_evaluation_feedback/urls.py b/server/vbv_lernwelt/self_evaluation_feedback/urls.py new file mode 100644 index 00000000..ec1c26cf --- /dev/null +++ b/server/vbv_lernwelt/self_evaluation_feedback/urls.py @@ -0,0 +1,11 @@ +from django.urls import path + +from .views import start_self_evaluation_feedback + +urlpatterns = [ + path( + "start-feedback", + start_self_evaluation_feedback, + name="start_self_evaluation_feedback", + ), +] diff --git a/server/vbv_lernwelt/self_evaluation_feedback/views.py b/server/vbv_lernwelt/self_evaluation_feedback/views.py new file mode 100644 index 00000000..b6b5c7be --- /dev/null +++ b/server/vbv_lernwelt/self_evaluation_feedback/views.py @@ -0,0 +1,49 @@ +from django.shortcuts import get_object_or_404 +from rest_framework.decorators import api_view, permission_classes +from rest_framework.exceptions import PermissionDenied +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response + +from vbv_lernwelt.core.models import User +from vbv_lernwelt.learning_mentor.models import LearningMentor +from vbv_lernwelt.learnpath.models import LearningUnit +from vbv_lernwelt.self_evaluation_feedback.models import SelfEvaluationFeedback + + +@api_view(["POST"]) +@permission_classes([IsAuthenticated]) +def start_self_evaluation_feedback(request): + learning_unit_id = request.data.get("learning_unit_id") + feedback_provider_user_id = request.data.get("feedback_provider_user_id") + + learning_unit = get_object_or_404(LearningUnit, id=learning_unit_id) + feedback_provider_user = get_object_or_404(User, id=feedback_provider_user_id) + + if not LearningMentor.objects.filter( + course=learning_unit.get_course(), + mentor=feedback_provider_user, + participants__user=request.user, + ).exists(): + raise PermissionDenied() + + SelfEvaluationFeedback.objects.create( + feedback_requester_user=request.user, + feedback_provider_user=feedback_provider_user, + learning_unit=learning_unit, + ) + + # TODO: Create notification for feedback_provider_user + + return Response({"success": True}) + + +@api_view(["GET"]) +@permission_classes([IsAuthenticated]) +def list_self_evaluation_feedback(request): + feedbacks = SelfEvaluationFeedback.objects.filter( + feedback_provider_user=request.user + ) + + # TODO continue here + + return Response({"success": True})