From 2512e980e484bd309662b344f68f34ab622e1225 Mon Sep 17 00:00:00 2001 From: Kieron Date: Thu, 9 Mar 2023 20:24:47 +1030 Subject: [PATCH 1/7] Add CRUD rest endpoints to Circles --- project/circles/serializers.py | 9 +++++++++ project/circles/views.py | 7 +++++++ project/core/urls.py | 6 ++++++ 3 files changed, 22 insertions(+) create mode 100644 project/circles/serializers.py diff --git a/project/circles/serializers.py b/project/circles/serializers.py new file mode 100644 index 0000000..4775e89 --- /dev/null +++ b/project/circles/serializers.py @@ -0,0 +1,9 @@ +from rest_framework import serializers + +from .models import Circle + + +class CircleSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = Circle + fields = ("id", "name", "photo") diff --git a/project/circles/views.py b/project/circles/views.py index efa3383..a3bcb1c 100644 --- a/project/circles/views.py +++ b/project/circles/views.py @@ -10,8 +10,10 @@ from django.views.generic.base import TemplateView from django.views.generic.detail import DetailView from django.views.generic.edit import CreateView, DeleteView, UpdateView +from rest_framework import viewsets from .models import Circle, Companion, JoinRequest +from .serializers import CircleSerializer class CompanionDeleteView(LoginRequiredMixin, UserPassesTestMixin, DeleteView): @@ -241,3 +243,8 @@ def get(self, request, circle_id, join_request_id, *args, **kwargs): join_request.delete() return redirect(circle) + + +class CircleViewSet(viewsets.ModelViewSet): + queryset = Circle.objects.all() + serializer_class = CircleSerializer diff --git a/project/core/urls.py b/project/core/urls.py index 8dcb31c..8dfb170 100644 --- a/project/core/urls.py +++ b/project/core/urls.py @@ -13,14 +13,19 @@ 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ +from circles import views from django.conf import settings from django.conf.urls.static import static from django.contrib import admin from django.urls import include, path from django.views.generic.base import TemplateView +from rest_framework.routers import DefaultRouter media_urlpatterns = static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) +router = DefaultRouter() +router.register(r"circles", views.CircleViewSet, basename="circle") + api_urlpatterns = [ path( "accounts/registration/", @@ -34,6 +39,7 @@ urlpatterns = [ path("api/v1/", include(api_urlpatterns)), + path("api/v1/", include(router.urls)), path("__debug__/", include("debug_toolbar.urls")), path("", TemplateView.as_view(template_name="home.html"), name="home"), path("admin/", admin.site.urls), From 1b65293106d413170a6b2ae882accaced5cdb766 Mon Sep 17 00:00:00 2001 From: Kieron Date: Thu, 16 Mar 2023 17:48:29 +1030 Subject: [PATCH 2/7] Restricts circles a user can see based on companion status --- project/circles/views.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/project/circles/views.py b/project/circles/views.py index a3bcb1c..f380fc1 100644 --- a/project/circles/views.py +++ b/project/circles/views.py @@ -11,6 +11,7 @@ from django.views.generic.detail import DetailView from django.views.generic.edit import CreateView, DeleteView, UpdateView from rest_framework import viewsets +from rest_framework.permissions import IsAuthenticated from .models import Circle, Companion, JoinRequest from .serializers import CircleSerializer @@ -245,6 +246,13 @@ def get(self, request, circle_id, join_request_id, *args, **kwargs): return redirect(circle) +# Only returns circles that user is a companion of class CircleViewSet(viewsets.ModelViewSet): - queryset = Circle.objects.all() serializer_class = CircleSerializer + permission_classes = [IsAuthenticated] + + def get_queryset(self): + user = self.request.user + circles = Circle.objects.filter(companions_through__user=user) + + return circles From 10c99710fbee2aa9ec58247465a59b7b3221a9a8 Mon Sep 17 00:00:00 2001 From: Kieron Date: Sat, 25 Mar 2023 16:30:08 +1030 Subject: [PATCH 3/7] Adds rest API end points for joining activities --- project/activities/serializers.py | 14 ++++++++ project/activities/urls.py | 8 ++++- project/activities/views.py | 53 ++++++++++++++++++++++++++++++- project/core/urls.py | 5 +++ 4 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 project/activities/serializers.py diff --git a/project/activities/serializers.py b/project/activities/serializers.py new file mode 100644 index 0000000..18541e3 --- /dev/null +++ b/project/activities/serializers.py @@ -0,0 +1,14 @@ +from rest_framework import serializers + +from circles.models import Circle +from .models import Activity + + +class ActivitySerializer(serializers.HyperlinkedModelSerializer): + circle = serializers.HyperlinkedRelatedField(view_name='circle-detail', read_only=True) + participants = serializers.HyperlinkedRelatedField(many=True, view_name='companion-detail', read_only=True) + organizers = serializers.HyperlinkedRelatedField(many=True, view_name='student-detail', read_only=True) + + class Meta: + model = Activity + fields = ('url', 'id', 'activity_type', 'activity_date', 'note', 'circle', 'participants', 'done', 'organizers') diff --git a/project/activities/urls.py b/project/activities/urls.py index 6e63589..6ec0a09 100644 --- a/project/activities/urls.py +++ b/project/activities/urls.py @@ -1,4 +1,5 @@ -from django.urls import path +from django.urls import include, path +from rest_framework import routers from .views import ( ActivityAddParticipantView, @@ -7,9 +8,14 @@ ActivityRemoveParticipantView, ActivitySetDoneView, ActivityUpdateView, + ActivityViewSet, ) +router = routers.SimpleRouter() +router.register(r'activities', ActivityViewSet) + urlpatterns = [ + path('', include(router.urls)), path( "create", ActivityCreateView.as_view(), diff --git a/project/activities/views.py b/project/activities/views.py index c4e61e4..522cf6a 100644 --- a/project/activities/views.py +++ b/project/activities/views.py @@ -1,12 +1,16 @@ -from circles.models import Circle +from circles.models import Circle, Companion from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin from django.http import HttpResponseRedirect from django.shortcuts import redirect from django.urls import reverse from django.views.generic import View +from rest_framework import viewsets, permissions, status +from rest_framework.decorators import action +from rest_framework.response import Response from .forms import ActivityModelForm from .models import Activity +from .serializers import ActivitySerializer class ActivityCreateView(UserPassesTestMixin, LoginRequiredMixin, View): @@ -219,3 +223,50 @@ def get(self, request, activity_id, *args, **kwargs): kwargs={"pk": self.activity.circle.id}, ) ) + + +class ActivityViewSet(viewsets.ModelViewSet): + queryset = Activity.objects.all() + serializer_class = ActivitySerializer + + @action(detail=True, methods=['post']) + def join_activity(self, request, pk=None): + """ Add current user to participants of an activity """ + activity = self.get_object() + user = request.user + + # Check user is Companion in Circle + try: + circle = Circle.objects.filter(activities=activity).first() + companion = Companion.objects.get(user=user, circle=circle) + except Companion.DoesNotExist: + return Response({'status': 'error', 'message': 'Sorry, you are not in this Circle'}) + + activity.participants.add(user) + activity.save() + + return Response({'status': 'success'}) + + @action(detail=True, methods=['post'], permission_classes=[permissions.IsAuthenticated]) + def assign_companion(self, request, pk=None): + """ + Assign a companion to an activity + """ + activity = self.get_object() + if not request.user.companion.is_organizer: + return Response({'error': 'You do not have permission to assign companions to this activity.'}, + status=status.HTTP_403_FORBIDDEN) + + companion_id = request.data.get('companion_id') + if not companion_id: + return Response({'error': 'Please provide a valid companion ID.'}, status=status.HTTP_400_BAD_REQUEST) + + try: + companion = activity.circle.companion.get(id=companion_id) + except Circle.DoesNotExist: + return Response({'error': 'Companion not found in Circle.'}, status=status.HTTP_400_BAD_REQUEST) + + activity.participants.add(companion) + activity.save() + + return Response({'status': 'success'}) diff --git a/project/core/urls.py b/project/core/urls.py index 8dfb170..f575f11 100644 --- a/project/core/urls.py +++ b/project/core/urls.py @@ -35,11 +35,16 @@ "accounts/", include("dj_rest_auth.urls"), ), + # path( + # "activities/", + # include("dj_rest_auth.urls"), + # ), ] urlpatterns = [ path("api/v1/", include(api_urlpatterns)), path("api/v1/", include(router.urls)), + path("api/v1/", include('activities.urls')), path("__debug__/", include("debug_toolbar.urls")), path("", TemplateView.as_view(template_name="home.html"), name="home"), path("admin/", admin.site.urls), From 339c770673bf72e02d9e30d482843a345026c5a7 Mon Sep 17 00:00:00 2001 From: Kieron Date: Sun, 26 Mar 2023 22:00:11 +1030 Subject: [PATCH 4/7] Allows orgnizer to assign participant with checks --- project/activities/views.py | 39 +++++++++++++++++++++++++------------ project/core/urls.py | 4 ---- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/project/activities/views.py b/project/activities/views.py index 522cf6a..fbae01f 100644 --- a/project/activities/views.py +++ b/project/activities/views.py @@ -9,7 +9,7 @@ from rest_framework.response import Response from .forms import ActivityModelForm -from .models import Activity +from .models import Activity, User from .serializers import ActivitySerializer @@ -234,14 +234,18 @@ def join_activity(self, request, pk=None): """ Add current user to participants of an activity """ activity = self.get_object() user = request.user + circle = activity.circle # Check user is Companion in Circle try: - circle = Circle.objects.filter(activities=activity).first() companion = Companion.objects.get(user=user, circle=circle) except Companion.DoesNotExist: return Response({'status': 'error', 'message': 'Sorry, you are not in this Circle'}) + # Check if companion is already a participant of the activity + if user in activity.participants.all(): + return Response({'status': 'error', 'message': 'You are already participating in this activity'}) + activity.participants.add(user) activity.save() @@ -253,20 +257,31 @@ def assign_companion(self, request, pk=None): Assign a companion to an activity """ activity = self.get_object() - if not request.user.companion.is_organizer: - return Response({'error': 'You do not have permission to assign companions to this activity.'}, - status=status.HTTP_403_FORBIDDEN) + user = request.user + circle = activity.circle + assignee_id = request.data.get('assignee_id') - companion_id = request.data.get('companion_id') - if not companion_id: - return Response({'error': 'Please provide a valid companion ID.'}, status=status.HTTP_400_BAD_REQUEST) + # Check current user is the Circle organizer + try: + assigner = Companion.objects.get(user__id=user.id, is_organizer=True, circle=circle) + except Companion.DoesNotExist: + return Response({'status': 'error', 'message': 'Sorry, you need to be the organizer of this Circle to add a' + ' participant'}) + # Check 'assignee' is in the Circle try: - companion = activity.circle.companion.get(id=companion_id) - except Circle.DoesNotExist: - return Response({'error': 'Companion not found in Circle.'}, status=status.HTTP_400_BAD_REQUEST) + assignee = Companion.objects.get(user__id=assignee_id, circle=circle) + except Companion.DoesNotExist: + return Response({'error': 'The participant you are trying to add is not in this Circle.'}, + status=status.HTTP_400_BAD_REQUEST) + + # Check 'assignee' is not already in the activity + assignee_email = User.objects.get(id=assignee_id) + if assignee_email in activity.participants.all(): + return Response({'status': 'error', 'message': 'This companion is already a participant of this activity'}) - activity.participants.add(companion) + # Assign Companion to activity + activity.participants.add(assignee_id) activity.save() return Response({'status': 'success'}) diff --git a/project/core/urls.py b/project/core/urls.py index f575f11..a34bc98 100644 --- a/project/core/urls.py +++ b/project/core/urls.py @@ -35,10 +35,6 @@ "accounts/", include("dj_rest_auth.urls"), ), - # path( - # "activities/", - # include("dj_rest_auth.urls"), - # ), ] urlpatterns = [ From 4a3acfde459bae32758757869f19b16c2ee21f1e Mon Sep 17 00:00:00 2001 From: Kieron Date: Mon, 27 Mar 2023 10:00:25 +1030 Subject: [PATCH 5/7] Fixes formatting --- project/activities/serializers.py | 26 ++++++++++++--- project/activities/urls.py | 4 +-- project/activities/views.py | 55 ++++++++++++++++++++++--------- project/core/urls.py | 2 +- 4 files changed, 64 insertions(+), 23 deletions(-) diff --git a/project/activities/serializers.py b/project/activities/serializers.py index 18541e3..25d7956 100644 --- a/project/activities/serializers.py +++ b/project/activities/serializers.py @@ -1,14 +1,30 @@ +from circles.models import Circle from rest_framework import serializers -from circles.models import Circle from .models import Activity class ActivitySerializer(serializers.HyperlinkedModelSerializer): - circle = serializers.HyperlinkedRelatedField(view_name='circle-detail', read_only=True) - participants = serializers.HyperlinkedRelatedField(many=True, view_name='companion-detail', read_only=True) - organizers = serializers.HyperlinkedRelatedField(many=True, view_name='student-detail', read_only=True) + circle = serializers.HyperlinkedRelatedField( + view_name="circle-detail", read_only=True + ) + participants = serializers.HyperlinkedRelatedField( + many=True, view_name="companion-detail", read_only=True + ) + organizers = serializers.HyperlinkedRelatedField( + many=True, view_name="student-detail", read_only=True + ) class Meta: model = Activity - fields = ('url', 'id', 'activity_type', 'activity_date', 'note', 'circle', 'participants', 'done', 'organizers') + fields = ( + "url", + "id", + "activity_type", + "activity_date", + "note", + "circle", + "participants", + "done", + "organizers", + ) diff --git a/project/activities/urls.py b/project/activities/urls.py index 6ec0a09..9e3afbc 100644 --- a/project/activities/urls.py +++ b/project/activities/urls.py @@ -12,10 +12,10 @@ ) router = routers.SimpleRouter() -router.register(r'activities', ActivityViewSet) +router.register(r"activities", ActivityViewSet) urlpatterns = [ - path('', include(router.urls)), + path("", include(router.urls)), path( "create", ActivityCreateView.as_view(), diff --git a/project/activities/views.py b/project/activities/views.py index fbae01f..3ca108a 100644 --- a/project/activities/views.py +++ b/project/activities/views.py @@ -4,7 +4,7 @@ from django.shortcuts import redirect from django.urls import reverse from django.views.generic import View -from rest_framework import viewsets, permissions, status +from rest_framework import permissions, status, viewsets from rest_framework.decorators import action from rest_framework.response import Response @@ -229,9 +229,9 @@ class ActivityViewSet(viewsets.ModelViewSet): queryset = Activity.objects.all() serializer_class = ActivitySerializer - @action(detail=True, methods=['post']) + @action(detail=True, methods=["post"]) def join_activity(self, request, pk=None): - """ Add current user to participants of an activity """ + """Add current user to participants of an activity""" activity = self.get_object() user = request.user circle = activity.circle @@ -240,18 +240,27 @@ def join_activity(self, request, pk=None): try: companion = Companion.objects.get(user=user, circle=circle) except Companion.DoesNotExist: - return Response({'status': 'error', 'message': 'Sorry, you are not in this Circle'}) + return Response( + {"status": "error", "message": "Sorry, you are not in this Circle"} + ) # Check if companion is already a participant of the activity if user in activity.participants.all(): - return Response({'status': 'error', 'message': 'You are already participating in this activity'}) + return Response( + { + "status": "error", + "message": "You are already participating in this activity", + } + ) activity.participants.add(user) activity.save() - return Response({'status': 'success'}) + return Response({"status": "success"}) - @action(detail=True, methods=['post'], permission_classes=[permissions.IsAuthenticated]) + @action( + detail=True, methods=["post"], permission_classes=[permissions.IsAuthenticated] + ) def assign_companion(self, request, pk=None): """ Assign a companion to an activity @@ -259,29 +268,45 @@ def assign_companion(self, request, pk=None): activity = self.get_object() user = request.user circle = activity.circle - assignee_id = request.data.get('assignee_id') + assignee_id = request.data.get("assignee_id") # Check current user is the Circle organizer try: - assigner = Companion.objects.get(user__id=user.id, is_organizer=True, circle=circle) + assigner = Companion.objects.get( + user__id=user.id, is_organizer=True, circle=circle + ) except Companion.DoesNotExist: - return Response({'status': 'error', 'message': 'Sorry, you need to be the organizer of this Circle to add a' - ' participant'}) + return Response( + { + "status": "error", + "message": "Sorry, you need to be the organizer of this Circle to add a" + " participant", + } + ) # Check 'assignee' is in the Circle try: assignee = Companion.objects.get(user__id=assignee_id, circle=circle) except Companion.DoesNotExist: - return Response({'error': 'The participant you are trying to add is not in this Circle.'}, - status=status.HTTP_400_BAD_REQUEST) + return Response( + { + "error": "The participant you are trying to add is not in this Circle." + }, + status=status.HTTP_400_BAD_REQUEST, + ) # Check 'assignee' is not already in the activity assignee_email = User.objects.get(id=assignee_id) if assignee_email in activity.participants.all(): - return Response({'status': 'error', 'message': 'This companion is already a participant of this activity'}) + return Response( + { + "status": "error", + "message": "This companion is already a participant of this activity", + } + ) # Assign Companion to activity activity.participants.add(assignee_id) activity.save() - return Response({'status': 'success'}) + return Response({"status": "success"}) diff --git a/project/core/urls.py b/project/core/urls.py index a34bc98..deece15 100644 --- a/project/core/urls.py +++ b/project/core/urls.py @@ -40,7 +40,7 @@ urlpatterns = [ path("api/v1/", include(api_urlpatterns)), path("api/v1/", include(router.urls)), - path("api/v1/", include('activities.urls')), + path("api/v1/", include("activities.urls")), path("__debug__/", include("debug_toolbar.urls")), path("", TemplateView.as_view(template_name="home.html"), name="home"), path("admin/", admin.site.urls), From 372e6c3a7425dec9e84f625b10e2f12466fb225c Mon Sep 17 00:00:00 2001 From: Kieron Date: Mon, 27 Mar 2023 10:19:09 +1030 Subject: [PATCH 6/7] Fixes more formatting --- project/activities/serializers.py | 1 - project/activities/views.py | 10 ++++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/project/activities/serializers.py b/project/activities/serializers.py index 25d7956..b5b3fe8 100644 --- a/project/activities/serializers.py +++ b/project/activities/serializers.py @@ -1,4 +1,3 @@ -from circles.models import Circle from rest_framework import serializers from .models import Activity diff --git a/project/activities/views.py b/project/activities/views.py index 3ca108a..bca6dd0 100644 --- a/project/activities/views.py +++ b/project/activities/views.py @@ -279,8 +279,8 @@ def assign_companion(self, request, pk=None): return Response( { "status": "error", - "message": "Sorry, you need to be the organizer of this Circle to add a" - " participant", + "message": "Sorry, you need to be the organizer of this Circle to " + "add a participant", } ) @@ -290,7 +290,8 @@ def assign_companion(self, request, pk=None): except Companion.DoesNotExist: return Response( { - "error": "The participant you are trying to add is not in this Circle." + "error": "The participant you are trying to add is not in this " + "Circle." }, status=status.HTTP_400_BAD_REQUEST, ) @@ -301,7 +302,8 @@ def assign_companion(self, request, pk=None): return Response( { "status": "error", - "message": "This companion is already a participant of this activity", + "message": "This companion is already a participant of this " + "activity", } ) From 738a2af4fa88675f77f8ab8ac2deb16217b7b4e0 Mon Sep 17 00:00:00 2001 From: Kieron Date: Mon, 27 Mar 2023 10:32:45 +1030 Subject: [PATCH 7/7] Fixes comment format --- project/activities/views.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/project/activities/views.py b/project/activities/views.py index bca6dd0..bf5e1a9 100644 --- a/project/activities/views.py +++ b/project/activities/views.py @@ -262,9 +262,7 @@ def join_activity(self, request, pk=None): detail=True, methods=["post"], permission_classes=[permissions.IsAuthenticated] ) def assign_companion(self, request, pk=None): - """ - Assign a companion to an activity - """ + # Assign a companion to an activity activity = self.get_object() user = request.user circle = activity.circle