From cf43a4351ece30dfd4f4c7fbdce07db3363fb97a Mon Sep 17 00:00:00 2001
From: mpeltriaux <michel.peltriaux@sgdnord.rlp.de>
Date: Wed, 16 Feb 2022 11:38:24 +0100
Subject: [PATCH] #118 API pagination

* adds pagination and related parameters to GET apis
* updates api GET test
---
 api/tests/v1/get/test_api_get.py   |  7 ++++-
 api/utils/serializer/serializer.py | 12 +++++--
 api/views/v1/views.py              | 17 ++++++----
 api/views/views.py                 | 50 ++++++++++++++++++++++++++----
 4 files changed, 71 insertions(+), 15 deletions(-)

diff --git a/api/tests/v1/get/test_api_get.py b/api/tests/v1/get/test_api_get.py
index 38af864b..3c464542 100644
--- a/api/tests/v1/get/test_api_get.py
+++ b/api/tests/v1/get/test_api_get.py
@@ -36,7 +36,12 @@ class APIV1GetTestCase(BaseAPIV1TestCase):
         """
         response = self._run_get_request(url)
         content = json.loads(response.content)
-        geojson = content[str(obj.id)]
+        self.assertIn("rpp", content)
+        self.assertIn("p", content)
+        self.assertIn("next", content)
+        self.assertIn("results", content)
+        paginated_content = content["results"]
+        geojson = paginated_content[str(obj.id)]
         self.assertEqual(response.status_code, 200, msg=response.content)
         return geojson
 
diff --git a/api/utils/serializer/serializer.py b/api/utils/serializer/serializer.py
index 443e10c5..8d618e44 100644
--- a/api/utils/serializer/serializer.py
+++ b/api/utils/serializer/serializer.py
@@ -10,6 +10,7 @@ from abc import abstractmethod
 
 from django.contrib.gis import geos
 from django.contrib.gis.geos import GEOSGeometry
+from django.core.paginator import Paginator
 
 from konova.utils.message_templates import DATA_UNSHARED
 
@@ -19,6 +20,10 @@ class AbstractModelAPISerializer:
     lookup = None
     properties_data = None
 
+    rpp = None
+    page_number = None
+    paginator = None
+
     class Meta:
         abstract = True
 
@@ -80,9 +85,12 @@ class AbstractModelAPISerializer:
         Returns:
             serialized_data (dict)
         """
-        entries = self.model.objects.filter(**self.lookup)
+        entries = self.model.objects.filter(**self.lookup).order_by("id")
+        self.paginator = Paginator(entries, self.rpp)
+        requested_entries = self.paginator.page(self.page_number)
+
         serialized_data = {}
-        for entry in entries:
+        for entry in requested_entries.object_list:
             serialized_data[str(entry.id)] = self._model_to_geo_json(entry)
         return serialized_data
 
diff --git a/api/views/v1/views.py b/api/views/v1/views.py
index 7789680f..8da5d49e 100644
--- a/api/views/v1/views.py
+++ b/api/views/v1/views.py
@@ -21,7 +21,6 @@ class AbstractAPIViewV1(AbstractAPIView):
     """ Holds general serialization functions for API v1
 
     """
-    serializer = None
 
     def __init__(self, *args, **kwargs):
         self.lookup = {
@@ -45,11 +44,17 @@ class AbstractAPIViewV1(AbstractAPIView):
             response (JsonResponse)
         """
         try:
+            self.rpp = int(request.GET.get("rpp", self.rpp))
+            self.page_number = int(request.GET.get("p", self.page_number))
+
+            self.serializer.rpp = self.rpp
+            self.serializer.page_number = self.page_number
+
             self.serializer.prepare_lookup(id, self.user)
             data = self.serializer.fetch_and_serialize()
         except Exception as e:
-            return self.return_error_response(e, 500)
-        return JsonResponse(data)
+            return self._return_error_response(e, 500)
+        return self._return_response(request, data)
 
     def post(self, request: HttpRequest):
         """ Handles the POST request
@@ -67,7 +72,7 @@ class AbstractAPIViewV1(AbstractAPIView):
             body = json.loads(body)
             created_id = self.serializer.create_model_from_json(body, self.user)
         except Exception as e:
-            return self.return_error_response(e, 500)
+            return self._return_error_response(e, 500)
         return JsonResponse({"id": created_id})
 
     def put(self, request: HttpRequest, id=None):
@@ -87,7 +92,7 @@ class AbstractAPIViewV1(AbstractAPIView):
             body = json.loads(body)
             updated_id = self.serializer.update_model_from_json(id, body, self.user)
         except Exception as e:
-            return self.return_error_response(e, 500)
+            return self._return_error_response(e, 500)
         return JsonResponse({"id": updated_id})
 
     def delete(self, request: HttpRequest, id=None):
@@ -104,7 +109,7 @@ class AbstractAPIViewV1(AbstractAPIView):
         try:
             success = self.serializer.delete_entry(id, self.user)
         except Exception as e:
-            return self.return_error_response(e, 500)
+            return self._return_error_response(e, 500)
         return JsonResponse(
             {
                 "success": success,
diff --git a/api/views/views.py b/api/views/views.py
index fb4f6df1..35c54fee 100644
--- a/api/views/views.py
+++ b/api/views/views.py
@@ -31,10 +31,22 @@ class AbstractAPIView(View):
 
     """
     user = None
+    serializer = None
+    rpp = 5  # Results per page default
+    page_number = 1  # Page number default
 
     class Meta:
         abstract = True
 
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.response_body_base = {
+            "rpp": None,
+            "p": None,
+            "next": None,
+            "results": None
+        }
+
     @csrf_exempt
     def dispatch(self, request, *args, **kwargs):
         try:
@@ -42,13 +54,14 @@ class AbstractAPIView(View):
             ksp_token = request.headers.get(KSP_TOKEN_HEADER_IDENTIFIER, None)
             ksp_user = request.headers.get(KSP_USER_HEADER_IDENTIFIER, None)
             self.user = APIUserToken.get_user_from_token(ksp_token, ksp_user)
+            request.user = self.user
             if not self.user.is_default_user():
                 raise PermissionError("Default permissions required")
         except PermissionError as e:
-            return self.return_error_response(e, 403)
+            return self._return_error_response(e, 403)
         return super().dispatch(request, *args, **kwargs)
 
-    def return_error_response(self, error, status_code=500):
+    def _return_error_response(self, error, status_code=500):
         """ Returns an error as JsonReponse
 
         Args:
@@ -68,6 +81,31 @@ class AbstractAPIView(View):
             status=status_code
         )
 
+    def _return_response(self, request: HttpRequest, data):
+        """ Returns all important data into a response object
+
+        Args:
+            request (HttpRequest): The incoming request
+            data (dict): The serialized data
+
+        Returns:
+            response (JsonResponse): The response to be returned
+        """
+        response = self.response_body_base
+        next_page = self.page_number + 1
+        next_page = next_page if next_page in self.serializer.paginator.page_range else None
+        if next_page is not None:
+            next_url = request.build_absolute_uri(
+                request.path + f"?rpp={self.rpp}&p={next_page}"
+            )
+        else:
+            next_url = None
+        response["rpp"] = self.rpp
+        response["p"] = self.page_number
+        response["next"] = next_url
+        response["results"] = data
+        return JsonResponse(response)
+
 
 class InterventionCheckAPIView(AbstractAPIView):
 
@@ -82,14 +120,14 @@ class InterventionCheckAPIView(AbstractAPIView):
             response (JsonResponse)
         """
         if not self.user.is_zb_user():
-            return self.return_error_response("Permission not granted", 403)
+            return self._return_error_response("Permission not granted", 403)
         try:
             obj = Intervention.objects.get(
                 id=id,
                 users__in=[self.user]
             )
         except Exception as e:
-            return self.return_error_response(e)
+            return self._return_error_response(e)
 
         all_valid, check_details = self.run_quality_checks(obj)
 
@@ -161,7 +199,7 @@ class AbstractModelShareAPIView(AbstractAPIView):
         try:
             users = self._get_shared_users_of_object(id)
         except Exception as e:
-            return self.return_error_response(e)
+            return self._return_error_response(e)
 
         data = {
             "users": [
@@ -185,7 +223,7 @@ class AbstractModelShareAPIView(AbstractAPIView):
         try:
             success = self._process_put_body(request.body, id)
         except Exception as e:
-            return self.return_error_response(e)
+            return self._return_error_response(e)
         data = {
             "success": success,
         }