diff --git a/server/config/urls.py b/server/config/urls.py index 87376ca5..9e3094c9 100644 --- a/server/config/urls.py +++ b/server/config/urls.py @@ -53,7 +53,6 @@ from vbv_lernwelt.importer.views import ( coursesessions_trainers_import, t2l_sync, ) -from vbv_lernwelt.learning_mentor.views import mentor_summary from vbv_lernwelt.notify.views import email_notification_settings from wagtail import urls as wagtail_urls from wagtail.admin import urls as wagtailadmin_urls @@ -126,9 +125,7 @@ urlpatterns = [ request_course_completion_for_user, name="request_course_completion_for_user"), - path(r"api/mentor//", - mentor_summary, - name="mentor_summary"), + path("api/mentor//", include("vbv_lernwelt.learning_mentor.urls")), # assignment path( diff --git a/server/vbv_lernwelt/iam/permissions.py b/server/vbv_lernwelt/iam/permissions.py index 7558d0a5..c82510fe 100644 --- a/server/vbv_lernwelt/iam/permissions.py +++ b/server/vbv_lernwelt/iam/permissions.py @@ -48,6 +48,17 @@ def is_course_session_expert(user, course_session_id: int): return is_supervisor or is_expert +def is_course_session_member(user, course_session_id: int | None = None): + if course_session_id is None: + return False + + return CourseSessionUser.objects.filter( + course_session_id=course_session_id, + user=user, + role=CourseSessionUser.Role.MEMBER, + ).exists() + + def can_evaluate_assignments(user, course_session_id: int): if user.is_superuser: return True diff --git a/server/vbv_lernwelt/learning_mentor/migrations/0002_mentorinvitation.py b/server/vbv_lernwelt/learning_mentor/migrations/0002_mentorinvitation.py index 5d0dc191..30c20c01 100644 --- a/server/vbv_lernwelt/learning_mentor/migrations/0002_mentorinvitation.py +++ b/server/vbv_lernwelt/learning_mentor/migrations/0002_mentorinvitation.py @@ -1,31 +1,55 @@ # Generated by Django 3.2.20 on 2023-12-07 13:46 -from django.db import migrations, models +import uuid + import django.db.models.deletion import django_extensions.db.fields -import uuid +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('course', '0005_course_enable_circle_documents'), - ('learning_mentor', '0001_initial'), + ("course", "0005_course_enable_circle_documents"), + ("learning_mentor", "0001_initial"), ] operations = [ migrations.CreateModel( - name='MentorInvitation', + name="MentorInvitation", fields=[ - ('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, verbose_name='created')), - ('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True, verbose_name='modified')), - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('email', models.EmailField(max_length=254)), - ('participant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='course.coursesessionuser')), + ( + "created", + django_extensions.db.fields.CreationDateTimeField( + auto_now_add=True, verbose_name="created" + ), + ), + ( + "modified", + django_extensions.db.fields.ModificationDateTimeField( + auto_now=True, verbose_name="modified" + ), + ), + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ("email", models.EmailField(max_length=254)), + ( + "participant", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="course.coursesessionuser", + ), + ), ], options={ - 'verbose_name': 'Mentor Invitation', - 'verbose_name_plural': 'Mentor Invitations', + "verbose_name": "Mentor Invitation", + "verbose_name_plural": "Mentor Invitations", }, ), ] diff --git a/server/vbv_lernwelt/learning_mentor/migrations/0003_auto_20231207_1448.py b/server/vbv_lernwelt/learning_mentor/migrations/0003_auto_20231207_1448.py index b14bbb59..b9de393d 100644 --- a/server/vbv_lernwelt/learning_mentor/migrations/0003_auto_20231207_1448.py +++ b/server/vbv_lernwelt/learning_mentor/migrations/0003_auto_20231207_1448.py @@ -4,18 +4,23 @@ from django.db import migrations class Migration(migrations.Migration): - dependencies = [ - ('learning_mentor', '0002_mentorinvitation'), + ("learning_mentor", "0002_mentorinvitation"), ] operations = [ migrations.AlterModelOptions( - name='learningmentor', - options={'verbose_name': 'Lernbegleiter', 'verbose_name_plural': 'Lernbegleiter'}, + name="learningmentor", + options={ + "verbose_name": "Lernbegleiter", + "verbose_name_plural": "Lernbegleiter", + }, ), migrations.AlterModelOptions( - name='mentorinvitation', - options={'verbose_name': 'Lernbegleiter Einladung', 'verbose_name_plural': 'Lernbegleiter Einladungen'}, + name="mentorinvitation", + options={ + "verbose_name": "Lernbegleiter Einladung", + "verbose_name_plural": "Lernbegleiter Einladungen", + }, ), ] diff --git a/server/vbv_lernwelt/learning_mentor/migrations/0004_alter_mentorinvitation_unique_together.py b/server/vbv_lernwelt/learning_mentor/migrations/0004_alter_mentorinvitation_unique_together.py new file mode 100644 index 00000000..8f8e2b30 --- /dev/null +++ b/server/vbv_lernwelt/learning_mentor/migrations/0004_alter_mentorinvitation_unique_together.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.20 on 2023-12-11 09:00 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("course", "0005_course_enable_circle_documents"), + ("learning_mentor", "0003_auto_20231207_1448"), + ] + + operations = [ + migrations.AlterUniqueTogether( + name="mentorinvitation", + unique_together={("email", "participant")}, + ), + ] diff --git a/server/vbv_lernwelt/learning_mentor/models.py b/server/vbv_lernwelt/learning_mentor/models.py index 07c31dc3..c6e8aa2e 100644 --- a/server/vbv_lernwelt/learning_mentor/models.py +++ b/server/vbv_lernwelt/learning_mentor/models.py @@ -33,9 +33,7 @@ class LearningMentor(models.Model): class MentorInvitation(TimeStampedModel): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) email = models.EmailField() - participant = models.ForeignKey( - CourseSessionUser, on_delete=models.CASCADE - ) + participant = models.ForeignKey(CourseSessionUser, on_delete=models.CASCADE) def __str__(self): return f"{self.email} ({self.participant})" @@ -43,3 +41,4 @@ class MentorInvitation(TimeStampedModel): class Meta: verbose_name = "Lernbegleiter Einladung" verbose_name_plural = "Lernbegleiter Einladungen" + unique_together = [["email", "participant"]] diff --git a/server/vbv_lernwelt/learning_mentor/tests/test_invitation.py b/server/vbv_lernwelt/learning_mentor/tests/test_invitation.py new file mode 100644 index 00000000..5eacddbe --- /dev/null +++ b/server/vbv_lernwelt/learning_mentor/tests/test_invitation.py @@ -0,0 +1,50 @@ +from django.urls import reverse +from rest_framework import status +from rest_framework.test import APITestCase + +from vbv_lernwelt.course.creators.test_utils import ( + add_course_session_user, + create_course, + create_course_session, + create_user, +) +from vbv_lernwelt.course.models import CourseSessionUser + + +class LearningMentorInvitationTest(APITestCase): + def setUp(self) -> None: + self.course, self.course_page = create_course("Test Course") + self.course_session = create_course_session(course=self.course, title="Test VV") + + self.participant = create_user("participant") + + def test_create_invitation_not_member(self) -> None: + # GIVEN + self.client.force_login(self.participant) + invite_url = reverse( + "create_invitation", kwargs={"course_session_id": self.course_session.id} + ) + + # WHEN + response = self.client.post(invite_url) + + # THEN + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_create_invitation(self) -> None: + # GIVEN + self.client.force_login(self.participant) + add_course_session_user( + self.course_session, + self.participant, + role=CourseSessionUser.Role.MEMBER, + ) + invite_url = reverse( + "create_invitation", kwargs={"course_session_id": self.course_session.id} + ) + + # WHEN + response = self.client.post(invite_url, data={"email": "test@example.com"}) + + # THEN + self.assertEqual(response.status_code, status.HTTP_200_OK) diff --git a/server/vbv_lernwelt/learning_mentor/urls.py b/server/vbv_lernwelt/learning_mentor/urls.py new file mode 100644 index 00000000..88e9cba3 --- /dev/null +++ b/server/vbv_lernwelt/learning_mentor/urls.py @@ -0,0 +1,8 @@ +from django.urls import path + +from . import views + +urlpatterns = [ + path("summary", views.mentor_summary, name="mentor_summary"), + path("invite", views.create_invitation, name="create_invitation"), +] diff --git a/server/vbv_lernwelt/learning_mentor/views.py b/server/vbv_lernwelt/learning_mentor/views.py index 8ea1393d..e3de9cad 100644 --- a/server/vbv_lernwelt/learning_mentor/views.py +++ b/server/vbv_lernwelt/learning_mentor/views.py @@ -1,15 +1,17 @@ from django.shortcuts import redirect +from rest_framework import permissions from rest_framework.decorators import api_view, permission_classes from rest_framework.generics import get_object_or_404 from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from vbv_lernwelt.core.serializers import UserSerializer -from vbv_lernwelt.course.models import CourseSession +from vbv_lernwelt.course.models import CourseSession, CourseSessionUser +from vbv_lernwelt.iam.permissions import is_course_session_member from vbv_lernwelt.learning_mentor.content.praxis_assignment import ( get_praxis_assignments, ) -from vbv_lernwelt.learning_mentor.models import LearningMentor +from vbv_lernwelt.learning_mentor.models import LearningMentor, MentorInvitation from vbv_lernwelt.learning_mentor.serializers import PraxisAssignmentStatusSerializer from vbv_lernwelt.learnpath.models import Circle @@ -41,6 +43,13 @@ def mentor_summary(request, course_session_id: int): ) +class CourseSessionMember(permissions.BasePermission): + def has_permission(self, request, view): + return is_course_session_member( + request.user, view.kwargs.get("course_session_id") + ) + + @api_view(["GET"]) @permission_classes([IsAuthenticated]) def list_invitations(request, course_session_id: int): @@ -48,12 +57,23 @@ def list_invitations(request, course_session_id: int): @api_view(["POST"]) -@permission_classes([IsAuthenticated]) +@permission_classes([IsAuthenticated, CourseSessionMember]) def create_invitation(request, course_session_id: int): - # Validate request - # Create invitation - # Send email - pass + course_session = get_object_or_404(CourseSession, id=course_session_id) + course_session_user = get_object_or_404( + CourseSessionUser, user=request.user, course_session=course_session + ) + + email = request.data.get("email") + if not email: + return Response({"error": "email is required"}, status=400) + + invitation = MentorInvitation.objects.get_or_create( + email=email, + participant=course_session_user, + ) + + return Response({}) @api_view(["GET"])