From 307ed35ee9e8575bd53dd1ae63d8ba02c2e69855 Mon Sep 17 00:00:00 2001 From: Corban-Lee Jones Date: Wed, 17 Jan 2024 19:33:28 +0000 Subject: [PATCH] API Tokens and Filters --- apps/api/urls.py | 2 + apps/api/views.py | 211 ++++++++++++++++++++++++++++++---------------- core/settings.py | 3 + requirements.txt | 2 + 4 files changed, 145 insertions(+), 73 deletions(-) diff --git a/apps/api/urls.py b/apps/api/urls.py index fb1b7d3..4c91e5d 100644 --- a/apps/api/urls.py +++ b/apps/api/urls.py @@ -1,12 +1,14 @@ # -*- encoding: utf-8 -*- from django.urls import path, include +from rest_framework.authtoken.views import obtain_auth_token from . import views urlpatterns = [ path("api-auth/", include("rest_framework.urls", namespace="rest_framework")), + path("api-token-auth/", obtain_auth_token), path("tickets/", views.TicketListApiView.as_view(), name="tickets"), path("filter-counts/", views.FilterCountApiView.as_view(), name="filter-counts"), diff --git a/apps/api/views.py b/apps/api/views.py index 60d8a8d..5ede093 100644 --- a/apps/api/views.py +++ b/apps/api/views.py @@ -2,8 +2,11 @@ import logging from typing import Any +from operator import or_, and_ +from functools import reduce -from django.db.models import Q +from django_filters import rest_framework as rest_filters +from django.db.models import Q, Count from django.http import HttpRequest from django.core.cache import cache from django.views.decorators.cache import cache_page @@ -11,11 +14,12 @@ from django.utils.decorators import method_decorator from django.core.exceptions import ValidationError from rest_framework.views import APIView from rest_framework.response import Response -from rest_framework import status, permissions +from rest_framework import status, permissions, filters, generics from rest_framework.pagination import PageNumberPagination +from rest_framework.authentication import SessionAuthentication, TokenAuthentication from apps.home.models import Ticket, TicketPriority, TicketTag -from apps.authentication.models import Department +from apps.authentication.models import Department, User from .serializers import ( TicketSerializer, TicketTagSerializer, TicketPrioritySerializer, UserSerializer @@ -37,101 +41,162 @@ class TicketPaginiation(PageNumberPagination): max_page_size = 100 -class TicketListApiView(APIView): +class TicketListApiView(generics.ListAPIView): + authentication_classes = [SessionAuthentication, TokenAuthentication] permission_classes = [permissions.IsAuthenticated] - pagination_class = TicketPaginiation - ALLOWED_FILTERS = ( - "uuid__in", "priority", "tags__in", "author__department", "search" - ) - MATCHER_MAP = { - "contains": lambda k, v: {k: v[0]}, - "in": lambda k, v: {k: v} - } + pagination_class = TicketPaginiation + serializer_class = TicketSerializer + + queryset = Ticket.objects.all() + + filter_backends = [filters.SearchFilter, rest_filters.DjangoFilterBackend, filters.OrderingFilter] + filterset_fields = ["priority", "tags", "author"] + search_fields = ["author__forename", "author__surname", "title", "description"] + ordering_fields = ["create_timestamp", "edit_timestamp"] + + def get_queryset(self): + tag_uuids = self.request.query_params.getlist("tags", []) + queryset = self.queryset + + log.debug("tag uuids %s", tag_uuids) + + for uuid in tag_uuids: + if not uuid: continue + log.debug("uuid %s", uuid) + queryset = queryset.filter(tags__uuid__in=[uuid]) + + return queryset + + + # ALLOWED_FILTERS = ( + # "uuid__in", "priority", "tags__in", "author__department", "search" + # ) + # MATCHER_MAP = { + # "contains": lambda k, v: {k: v[0]}, + # "in": lambda k, v: {k: v} + # } # @method_decorator(cache_page(60 * 5)) - def get(self, request: HttpRequest) -> Response: - """Returns a response containing tickets matching the given filters.""" + # def get(self, request: HttpRequest) -> Response: + # """Returns a response containing tickets matching the given filters.""" - try: - return self._get_tickets(request) + # try: - except KeyError as error: - return respond_error(error, status.HTTP_400_BAD_REQUEST) + # kwargs = self._parse_querystring(request) + # data = self._new_get_tickets(**kwargs) + # return Response(data, status=status.HTTP_200_OK) - except ValidationError as error: - return respond_error(error, status.HTTP_400_BAD_REQUEST) + # except KeyError as error: + # return respond_error(error, status.HTTP_400_BAD_REQUEST) - def post(self, request): - """Create a new ticket""" + # except ValidationError as error: + # return respond_error(error, status.HTTP_400_BAD_REQUEST) - serializer = TicketSerializer(data={ + # def post(self, request): + # """Create a new ticket""" - }) - if not serializer.is_valid(): - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + # serializer = TicketSerializer(data={ - serializer.save() - return Response(serializer.data, status=status.HTTP_201_CREATED) + # }) + # if not serializer.is_valid(): + # return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - def _get_tickets(self, request: HttpRequest) -> Response: - """Returns a response containing tickets matching the given filters. + # serializer.save() + # return Response(serializer.data, status=status.HTTP_201_CREATED) - Parameters - ---------- - request : HttpRequest - The request containing the filters. + # def _parse_querystring(self, request: HttpRequest) -> dict: + # """""" - Returns - ------- - Response - A response containing the tickets data. - Raises - ------ - KeyError - If any given filter key isn't in `self.ALLOWED_FILTERS`. - ValidationError - If any ValidationErrors are raised by Django while applying filters. - """ - queryset = Ticket.objects.all() - for key, values in request.GET.lists(): - key = key.removesuffix("[]") + # def _new_get_tickets(self, **filters) -> dict: + # """""" - if key not in self.ALLOWED_FILTERS: - raise KeyError(key) - if not key.endswith("__in"): - values = values[0] - if key == "search": - queryset = queryset.filter( - Q(**{"title__contains": values}) | - Q(**{"description__contains": values}) | - Q(**{"author__forename__contains": values}) | - Q(**{"author__surname__contains": values}) - ) - continue + # def _get_tickets(self, request: HttpRequest) -> dict: + # """Returns a response containing tickets matching the given filters. - if "all" in values: - continue + # Parameters + # ---------- + # request : HttpRequest + # The request containing the filters. - if not key.endswith("__in"): - queryset = queryset.filter(Q(**{key: values})) - continue + # Returns + # ------- + # Response + # A response containing the tickets data. - for value in values: - filter_kwargs = {key: [value]} - queryset = queryset.filter(Q(**filter_kwargs)) + # Raises + # ------ + # KeyError + # If any given filter key isn't in `self.ALLOWED_FILTERS`. + # ValidationError + # If any ValidationErrors are raised by Django while applying filters. + # """ - tickets = queryset.order_by("-edit_timestamp") - serializer = TicketSerializer(tickets, many=True) - response_data = serializer.data + # queryset = Ticket.objects.all() - log.debug("Query successful showing %s results", tickets.count()) - return Response(response_data, status=status.HTTP_200_OK) + # for key, values in request.GET.lists(): + # key = key.removesuffix("[]") + + # # format is not intended for this function + # if key == "format": + # continue + + # if key not in self.ALLOWED_FILTERS: + # raise KeyError(key) + + # if not key.endswith("__in"): + # values = values[0] + + # # search filter is a user text input + # if key == "search": + + # clean_value = values.split() + # log.debug("clean values %s", clean_value) + # or_filters = [ + # Q(**{"title__contains": clean_value}), + # Q(**{"description__contains": clean_value}), + # Q(**{"author__forename__contains": clean_value}), + # Q(**{"author__surname__contains": clean_value}) + # ] + + # name_values = values.split(" ") + # if len(name_values) == 2: + + # log.debug("looking for author %s %s", *name_values) + # users = User.objects.filter(Q(forename__contains=name_values[0]) | Q(surname__contains=name_values[1])) + # log.debug("found %s authors under that name", users.count()) + # for user in users: + # log.debug("%s %s", user.fullname.lower(), values.lower()) + # if user.fullname.lower() == values.lower(): + # log.debug("adding author to filter") + # or_filters.append(Q(**{"author": user})) + + # queryset = queryset.filter(reduce(or_, or_filters)) + # continue + + # if "all" in values: + # continue + + # # if it doesn't end with "__in", assume we are looking for an exact match + # if not key.endswith("__in"): + # queryset = queryset.filter(Q(**{key: values})) + # continue + + # # Chain the filters to create an AND query (Q() & Q() doesnt work?) + # for value in values: + # filter_kwargs = {key: [value]} + # queryset = queryset.filter(Q(**filter_kwargs)) + + # tickets = queryset.order_by("-edit_timestamp") + # serializer = TicketSerializer(tickets, many=True) + + # log.debug("Query successful showing %s results", tickets.count()) + # return serializer.data class FilterCountApiView(APIView): diff --git a/core/settings.py b/core/settings.py index a328f01..1960a21 100644 --- a/core/settings.py +++ b/core/settings.py @@ -15,6 +15,7 @@ env = environ.Env( # BASE_DIR = os.path.dirname(os.path.dirname(__file__)) BASE_DIR = Path(__file__).parent.parent CORE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +print(BASE_DIR, CORE_DIR) # Take environment variables from .env file environ.Env.read_env(BASE_DIR / ".env") @@ -42,6 +43,8 @@ INSTALLED_APPS = [ 'django.contrib.messages', 'django.contrib.staticfiles', 'rest_framework', + "rest_framework.authtoken", + "django_filters", 'apps.api', 'apps.home', 'apps.authentication' diff --git a/requirements.txt b/requirements.txt index 9fea12d..4f7db59 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,8 @@ bleach==6.1.0 dj-database-url==0.5.0 Django==3.2.16 django-environ==0.8.1 +django-filter==23.5 +django-rest-framework==0.1.0 djangorestframework==3.14.0 gunicorn==20.1.0 pillow==10.2.0