From 4c30df10b0321d83de433ed67f74f899f6409f4e Mon Sep 17 00:00:00 2001 From: Steve Yonkeu Date: Tue, 20 Aug 2024 16:26:13 +0100 Subject: [PATCH] Feat: updates to serializers --- apps/events/serializers/event_serializer.py | 1 + apps/events/views/event.py | 56 ++++++++++--------- apps/events/views/reservation.py | 16 +++++- apps/events/views/speaker.py | 11 +++- apps/users/pagination.py | 26 +++++++++ apps/users/paginators.py | 0 apps/users/serializers/general_serializers.py | 43 ++++++++++++-- apps/users/views/general_viewsets.py | 37 ++++++++++++ mixins/api_response_mixin.py | 46 ++++++++++++++- templates/docs/redoc.html | 3 +- utils/auth.py | 11 ++-- website_api/settings/extra.py | 2 +- 12 files changed, 208 insertions(+), 44 deletions(-) create mode 100644 apps/users/pagination.py delete mode 100644 apps/users/paginators.py create mode 100644 apps/users/views/general_viewsets.py diff --git a/apps/events/serializers/event_serializer.py b/apps/events/serializers/event_serializer.py index 8b3b8e7..8fea922 100644 --- a/apps/events/serializers/event_serializer.py +++ b/apps/events/serializers/event_serializer.py @@ -8,6 +8,7 @@ class EventSerializer(serializers.ModelSerializer): speakers = serializers.SerializerMethodField() + tags = serializers.ListField(child=serializers.CharField(), required=False) class Meta: model = Event diff --git a/apps/events/views/event.py b/apps/events/views/event.py index f87565b..d4d0943 100644 --- a/apps/events/views/event.py +++ b/apps/events/views/event.py @@ -20,10 +20,14 @@ class EventViewSet(ModelViewSet, APIResponseMixin): queryset = Event.objects.all().select_related('created_by', 'updated_by') authentication_classes = [OAuth2Authentication] - serializer_class = EventSerializer http_method_names = ["get", "post", "put", "delete"] parser_classes = [JSONParser] + def get_serializer_class(self): + if self.action in ["list"]: + return EventSerializer + return EventSerializer + def get_permissions(self): if self.action in ["list", "retrieve"]: permission_classes = [AllowAny] @@ -31,6 +35,28 @@ def get_permissions(self): permission_classes = [IsAuthenticated] return [permission() for permission in permission_classes] + @extend_schema( + summary="Get all events", + operation_id="get_events", + description="Get all events.", + responses={ + 200: OpenApiResponse( + response=EventSerializer(many=True), + description=_("List of events"), + ) + }, + tags=["Events"], + ) + def list(self, request, *args, **kwargs): + queryset = self.filter_queryset(self.get_queryset()) + return self.paginated_response( + request=request, + queryset=queryset, + serializer_class=EventSerializer, + message=_("List of events"), + status_code=status.HTTP_200_OK, + ) + @extend_schema( summary="Create an event", operation_id="create_event", @@ -38,7 +64,7 @@ def get_permissions(self): request=CreateEventInputSerializer, responses={ 201: OpenApiResponse( - response=EventSerializer(), + response=EventSerializer, description=_("Event created successfully") ) }, @@ -48,34 +74,10 @@ def create(self, request, *args, **kwargs): create_event_serializer = CreateEventInputSerializer(data=request.data) create_event_serializer.is_valid(raise_exception=True) event = create_event_serializer.save(created_by=request.user, updated_by=request.user) - response_serializer = EventSerializer(event) return self.success( message=_("Event created successfully"), + data=EventSerializer(event).data, status_code=status.HTTP_201_CREATED, - data=response_serializer.data, - ) - - @extend_schema( - summary="Get all events", - operation_id="get_events", - description="Get all events.", - responses={ - 200: OpenApiResponse( - response=EventSerializer(many=True), - description=_("List of events") - ) - }, - tags=["Events"], - ) - def list(self, request, *args, **kwargs): - events = self.get_queryset().select_related( - 'created_by', 'updated_by' - ) - serializer = EventSerializer(events, many=True) - return self.success( - message=_("List of events"), - status_code=status.HTTP_200_OK, - data=serializer.data, ) @extend_schema( diff --git a/apps/events/views/reservation.py b/apps/events/views/reservation.py index a4d1f09..bc3c9e4 100644 --- a/apps/events/views/reservation.py +++ b/apps/events/views/reservation.py @@ -19,10 +19,15 @@ class ReservationViewSet(ModelViewSet, APIResponseMixin): queryset = Reservation.objects.all() authentication_classes = [OAuth2Authentication] - serializer_class = ReservationSerializer http_method_names = ["get", "post", "put", "delete"] parser_classes = [JSONParser] + def get_serializer_class(self): + if self.action in ["list", "retrieve"]: + return ReservationSerializer + if self.action == "create": + return CreateReservationSerializer + def get_permissions(self): """ Instantiates and returns the list of permissions that this view requires. @@ -40,7 +45,14 @@ def get_permissions(self): tags=["Reservations"], ) def list(self, request, *args, **kwargs): - return super().list(request, *args, **kwargs) + reservations = self.get_queryset() + return self.paginated_response( + request=request, + queryset=reservations, + serializer_class=ReservationSerializer, + message=_("Reservations listed successfully"), + status_code=status.HTTP_200_OK, + ) @extend_schema( summary="Get reservation details", diff --git a/apps/events/views/speaker.py b/apps/events/views/speaker.py index 865d71d..25fe006 100644 --- a/apps/events/views/speaker.py +++ b/apps/events/views/speaker.py @@ -10,6 +10,7 @@ SpeakerSerializer, SpeakerWithLastUpdatedBySerializer, ) +from apps.users.serializers.general_serializers import PaginatedResponseSerializer from mixins.api_response_mixin import APIResponseMixin @@ -18,11 +19,15 @@ class SpeakerViewSet(ModelViewSet, APIResponseMixin): ViewSet for managing speakers. """ queryset = Speaker.objects.all() - serializer_class = SpeakerSerializer authentication_classes = [OAuth2Authentication] parser_classes = [JSONParser] http_method_names = ["get", "post", "put", "delete"] + def get_serializer_class(self): + if self.action in ["list", "retrieve"]: + return SpeakerSerializer + return SpeakerWithLastUpdatedBySerializer + def get_permissions(self): """ Instantiates and returns the list of permissions that this view requires. @@ -57,7 +62,9 @@ def create(self, request, *args, **kwargs): summary="List all speakers", operation_id="list_speakers", description="List all speakers.", - responses={200: SpeakerSerializer(many=True)}, + responses={ + status.HTTP_200_OK: PaginatedResponseSerializer(data_serializer_class=SpeakerSerializer), + } ) def list(self, request, *args, **kwargs): return super().list(request, *args, **kwargs) diff --git a/apps/users/pagination.py b/apps/users/pagination.py new file mode 100644 index 0000000..d1e5a28 --- /dev/null +++ b/apps/users/pagination.py @@ -0,0 +1,26 @@ +from rest_framework.pagination import PageNumberPagination +from rest_framework.response import Response + + +class CustomPagination(PageNumberPagination): + page_size_query_param = 'page_size' + page_query_param = 'page'.lower() + max_page_size = 100 + + def get_paginated_response(self, data): + return Response({ + 'status': True, + 'message': 'Data retrieved successfully', + 'status_code': 200, + 'page': self.page.number, + 'page_size': self.page.paginator.per_page, + 'total': self.page.paginator.count, + 'pagination': { + 'next': self.get_next_link(), + 'previous': self.get_previous_link(), + 'count': self.page.paginator.count, + 'current_page': self.page.number, + 'total_pages': self.page.paginator.num_pages + }, + 'data': data + }) diff --git a/apps/users/paginators.py b/apps/users/paginators.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/users/serializers/general_serializers.py b/apps/users/serializers/general_serializers.py index 8808998..071c9e0 100644 --- a/apps/users/serializers/general_serializers.py +++ b/apps/users/serializers/general_serializers.py @@ -4,16 +4,49 @@ User = get_user_model() +class PaginationSerializer(serializers.Serializer): + next = serializers.CharField() + previous = serializers.CharField() + count = serializers.IntegerField() + current_page = serializers.IntegerField() + total_pages = serializers.IntegerField() + + class SuccessResponseSerializer(serializers.Serializer): status = serializers.BooleanField(default=True) - message = serializers.CharField(max_length=255) - data = serializers.JSONField(required=False) + message = serializers.CharField(max_length=200) + status_code = serializers.IntegerField(default=200) + + def __init__(self, *args, **kwargs): + data_serializer_class = kwargs.pop('data_serializer_class', None) + many = kwargs.pop('many', False) + super().__init__(*args, **kwargs) + if data_serializer_class: + self.fields['data'] = data_serializer_class(many=many) + + +class PaginatedResponseSerializer(SuccessResponseSerializer): + next = serializers.CharField(required=False, allow_null=True) + previous = serializers.CharField(required=False, allow_null=True) + count = serializers.IntegerField() + page = serializers.IntegerField() + page_size = serializers.IntegerField() + total_pages = serializers.IntegerField() + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) class ErrorResponseSerializer(serializers.Serializer): - status = serializers.BooleanField(default=True) - message = serializers.CharField(max_length=255) - errors = serializers.JSONField(required=False) + status = serializers.BooleanField(default=False) + message = serializers.CharField(default="An error occurred") + status_code = serializers.IntegerField(default=400) + + def __init__(self, *args, **kwargs): + default_message = kwargs.pop('default_message', None) + super().__init__(*args, **kwargs) + if default_message: + self.fields['message'].default = default_message class UserSerializer(serializers.ModelSerializer): diff --git a/apps/users/views/general_viewsets.py b/apps/users/views/general_viewsets.py new file mode 100644 index 0000000..0276511 --- /dev/null +++ b/apps/users/views/general_viewsets.py @@ -0,0 +1,37 @@ +from rest_framework import status +from rest_framework.response import Response +from rest_framework.viewsets import ModelViewSet + + +class BaseModelViewSet(ModelViewSet): + """ + A base viewset that you can extend to add custom pagination handling. + """ + + def paginated_response( + self, queryset, request, + serializer_class, message="Success", + status_code=status.HTTP_200_OK, + ): + """ + Custom paginated response method. + """ + page = self.paginate_queryset(queryset) + serializer = serializer_class(page, many=True) + response_data = { + "status": True, + "message": message, + "status_code": status_code, + "page": self.paginator.page.number, + "page_size": self.paginator.page_size, + "total": self.paginator.page.paginator.count, + "pagination": { + "next": self.paginator.get_next_link(), + "previous": self.paginator.get_previous_link(), + "count": self.paginator.page.paginator.count, + "current_page": self.paginator.page.number, + "total_pages": self.paginator.page.paginator.num_pages, + }, + "results": serializer.data + } + return Response(response_data, status=status_code) diff --git a/mixins/api_response_mixin.py b/mixins/api_response_mixin.py index a2e54c0..0c383a0 100644 --- a/mixins/api_response_mixin.py +++ b/mixins/api_response_mixin.py @@ -1,12 +1,15 @@ from typing import Any, Optional, Union, List from rest_framework import status +from rest_framework.pagination import PageNumberPagination +from rest_framework.request import Request from rest_framework.response import Response class APIResponseMixin: """ - A mixin to standardize API responses, providing both success and error response methods. + A mixin to standardize API responses, providing both success and error response methods, + including pagination support. """ def success( @@ -46,3 +49,44 @@ def error( "errors": [errors] if isinstance(errors, str) else errors } return Response(response_data, status=status_code) + + def paginated_response( + self, request: Request, queryset: Any, serializer_class: Any, + message: str = "Success", page_size: int = 10, + status_code: int = status.HTTP_200_OK, + ) -> Response: + """ + Returns a paginated response with next and previous URLs. + + :param request: The DRF request object. + :param queryset: The queryset to paginate. + :param serializer_class: The serializer class to use for the data. + :param message: A string message describing the success. + :param page_size: Number of items per page. + :param status_code: HTTP status code, default is 200 OK. + :return: DRF Response object with standardized pagination format. + """ + paginator = PageNumberPagination() + paginator.page_size = page_size + + if not queryset.ordered: + queryset = queryset.order_by('created_at') + + paginated_queryset = paginator.paginate_queryset(queryset, request) + + data = serializer_class(paginated_queryset, many=True).data + + response_data = { + "status": True, + "message": message, + "data": data, + "status_code": status_code, + "pagination": { + "next": paginator.get_next_link(), + "previous": paginator.get_previous_link(), + "count": paginator.page.paginator.count, + "current_page": paginator.page.number, + "total_pages": paginator.page.paginator.num_pages + } + } + return Response(response_data, status=status.HTTP_200_OK) diff --git a/templates/docs/redoc.html b/templates/docs/redoc.html index d24de38..3181500 100644 --- a/templates/docs/redoc.html +++ b/templates/docs/redoc.html @@ -13,14 +13,13 @@ -
diff --git a/utils/auth.py b/utils/auth.py index 4073df0..1873958 100644 --- a/utils/auth.py +++ b/utils/auth.py @@ -6,10 +6,13 @@ def authenticate_user(self, data): - user = User.objects.get(Q(username=data['email_or_username']) | Q(email=data['email_or_username'])) - if user and user.check_password(data['password']): - return user - return None + try: + user = User.objects.get(Q(username=data['email_or_username']) | Q(email=data['email_or_username'])) + if user and user.check_password(data['password']): + return user + return None + except User.DoesNotExist: + raise ValueError("Authentication credentials invalid") class EmailOrUsernameBackend(BaseBackend): diff --git a/website_api/settings/extra.py b/website_api/settings/extra.py index b05992a..8de894e 100644 --- a/website_api/settings/extra.py +++ b/website_api/settings/extra.py @@ -16,7 +16,7 @@ "rest_framework.throttling.AnonRateThrottle", "rest_framework.throttling.UserRateThrottle", ], - "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", + "DEFAULT_PAGINATION_CLASS": "apps.users.pagination.CustomPagination", 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', "PAGE_SIZE": 100, "NON_FIELD_ERRORS_KEY": "message",