Browse Source

fix #713: added paginated_response shortcut

Rafał Pitoń 8 years ago
parent
commit
00815df402

+ 24 - 0
docs/Shortcuts.md

@@ -25,6 +25,30 @@ def index(request, page=None):
 ```
 
 
+## `pagination_dict(page)`
+
+Utility function that returns JSON-serializable dict for `Page` object defining following keys:
+
+* `page` - The 1-based page number for current page.
+* `pages` - The total number of pages.
+* `count` - The total number of items on list.
+* `first` - None if this is first page, otherwhise `1`.
+* `previous` - None if this is first page, otherwhise number of previous page.
+* `next` - None if this is last page, otherwhise number of next page.
+* `last` - None if this is last page, otherwhise number of the last page.
+* `before` - Total number of items on previous pages.
+* `more` - Total number of items left to display on next pages.
+
+
+## `paginated_response(page, serializer=None, data=None, extra=None)`
+
+Shortcut function for returning paginated responses from API. Takes one required argument, the `Page` object, and following optional arguments:
+
+* `serializer` - Serializer to use. If its omited, no additional serialization step will be taken.
+* `data` - Data object to use. If its omited, `page.object_list` will be used by default.
+* `extra` - Dict with additional data to be included in response's JSON. Because dict in `extra` is added to response via `response_json.update(extra)`, this dict can be used for last minute overrides as well.
+
+
 ##### Note
 
 Giving `page` argument default value of 1 will make `paginate` function assume that first page was reached via link with explicit first page number and cause redirect loop.

+ 21 - 4
misago/core/shortcuts.py

@@ -3,6 +3,8 @@ import six
 from django.http import Http404
 from django.shortcuts import *  # noqa
 
+from rest_framework.response import Response
+
 
 def paginate(object_list, page, per_page, orphans=0,
              allow_empty_first_page=True,
@@ -41,13 +43,11 @@ def pagination_dict(page):
 
     if page.has_previous():
         pagination['first'] = 1
-        if page.previous_page_number() > 1:
-            pagination['previous'] = page.previous_page_number()
+        pagination['previous'] = page.previous_page_number()
 
     if page.has_next():
         pagination['last'] = page.paginator.num_pages
-        if page.next_page_number() <= page.paginator.num_pages:
-            pagination['next'] = page.next_page_number()
+        pagination['next'] = page.next_page_number()
 
     if page.start_index():
         pagination['before'] = page.start_index() - 1
@@ -56,6 +56,23 @@ def pagination_dict(page):
     return pagination
 
 
+def paginated_response(page, serializer=None, data=None, extra=None):
+    response_data = pagination_dict(page)
+
+    results = list(data or page.object_list)
+    if serializer:
+        results = serializer(results, many=True).data
+
+    response_data.update({
+        'results': results
+    })
+
+    if extra:
+        response_data.update(extra)
+
+    return Response(response_data)
+
+
 def validate_slug(model, slug):
     from .exceptions import OutdatedSlug
     if model.slug != slug:

+ 13 - 0
misago/core/testproject/serializers.py

@@ -0,0 +1,13 @@
+from rest_framework import serializers
+
+
+class MockSerializer(serializers.Serializer):
+    id = serializers.SerializerMethodField()
+
+    class Meta:
+        fields = (
+            'id',
+        )
+
+    def get_id(self, obj):
+        return obj * 2

+ 5 - 0
misago/core/testproject/urls.py

@@ -24,6 +24,11 @@ urlpatterns = [
     url(r'^forum/test-mail-users/$', views.test_mail_users, name='test-mail-users'),
     url(r'^forum/test-pagination/$', views.test_pagination, name='test-pagination'),
     url(r'^forum/test-pagination/(?P<page>[1-9][0-9]*)/$', views.test_pagination, name='test-pagination'),
+    url(r'^forum/test-paginated-response/$', views.test_paginated_response, name='test-paginated-response'),
+    url(r'^forum/test-paginated-response-data/$', views.test_paginated_response_data, name='test-paginated-response-data'),
+    url(r'^forum/test-paginated-response-serializer/$', views.test_paginated_response_serializer, name='test-paginated-response-serializer'),
+    url(r'^forum/test-paginated-response-data-serializer/$', views.test_paginated_response_data_serializer, name='test-paginated-response-data-serializer'),
+    url(r'^forum/test-paginated-response-data-extra/$', views.test_paginated_response_data_extra, name='test-paginated-response-data-extra'),
     url(r'^forum/test-valid-slug/(?P<slug>[a-z0-9\-]+)-(?P<pk>\d+)/$', views.validate_slug_view, name='validate-slug-view'),
     url(r'^forum/test-banned/$', views.raise_misago_banned, name='raise-misago-banned'),
     url(r'^forum/test-403/$', views.raise_misago_403, name='raise-misago-403'),

+ 55 - 1
misago/core/testproject/views.py

@@ -2,14 +2,17 @@ from django.contrib.auth import get_user_model
 from django.core.exceptions import PermissionDenied
 from django.http import Http404, HttpResponse
 
+from rest_framework.decorators import api_view
+
 from misago.users.models import Ban
 
 from .. import errorpages, mail
 from ..decorators import require_POST
 from ..exceptions import Banned
-from ..shortcuts import paginate, validate_slug
+from ..shortcuts import paginate, paginated_response, validate_slug
 from ..views import home_redirect
 from .models import Model
+from .serializers import MockSerializer
 
 
 def test_mail_user(request):
@@ -41,6 +44,57 @@ def test_pagination(request, page=None):
     return HttpResponse(",".join([str(x) for x in page.object_list]))
 
 
+@api_view()
+def test_paginated_response(request):
+    data = range(100)
+    page = paginate(data, 2, 10)
+
+    return paginated_response(page)
+
+
+@api_view()
+def test_paginated_response_data(request):
+    data = range(100)
+    page = paginate(data, 2, 10)
+
+    return paginated_response(page, data=['a', 'b', 'c', 'd', 'e'])
+
+
+@api_view()
+def test_paginated_response_serializer(request):
+    data = [0, 1, 2, 3]
+    page = paginate(data, 0, 10)
+
+    return paginated_response(page, serializer=MockSerializer)
+
+
+@api_view()
+def test_paginated_response_data_serializer(request):
+    data = [0, 1, 2, 3]
+    page = paginate(data, 0, 10)
+
+    return paginated_response(
+        page,
+        data=['a', 'b', 'c', 'd'],
+        serializer=MockSerializer
+    )
+
+
+@api_view()
+def test_paginated_response_data_extra(request):
+    data = [0, 1, 2, 3]
+    page = paginate(data, 0, 10)
+
+    return paginated_response(
+        page,
+        data=['a', 'b', 'c', 'd'],
+        extra={
+            'next': 'EXTRA',
+            'lorem': 'ipsum'
+        }
+    )
+
+
 def validate_slug_view(request, pk, slug):
     model = Model(int(pk), 'eric-the-fish')
     validate_slug(model, slug)

+ 100 - 1
misago/core/tests/test_shortcuts.py

@@ -54,7 +54,7 @@ class ValidateSlugTests(TestCase):
         self.assertEqual(response['Location'], valid_url)
 
 
-class GetIntOr404(TestCase):
+class GetIntOr404Tests(TestCase):
     def test_valid_inputs(self):
         """get_int_or_404 returns int for valid values"""
         VALID_VALUES = (
@@ -84,3 +84,102 @@ class GetIntOr404(TestCase):
         for value in INVALID_VALUES:
             with self.assertRaises(Http404):
                 get_int_or_404(value)
+
+
+@override_settings(ROOT_URLCONF='misago.core.testproject.urls')
+class PaginatedResponseTests(TestCase):
+    def test_page_response(self):
+        """utility returns response for only page arg"""
+        response = self.client.get(reverse('test-paginated-response'))
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(response.json(), {
+            'results': [i + 10 for i in range(10)],
+            'page': 2,
+            'pages': 10,
+            'count': 100,
+            'first': 1,
+            'previous': 1,
+            'next': 3,
+            'last': 10,
+            'before': 10,
+            'more': 80,
+        })
+
+    def test_explicit_data_response(self):
+        """utility returns response with explicit data"""
+        response = self.client.get(reverse('test-paginated-response-data'))
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(response.json(), {
+            'results': ['a', 'b', 'c', 'd', 'e'],
+            'page': 2,
+            'pages': 10,
+            'count': 100,
+            'first': 1,
+            'previous': 1,
+            'next': 3,
+            'last': 10,
+            'before': 10,
+            'more': 80,
+        })
+
+    def test_explicit_serializer_response(self):
+        """utility returns response with data serialized via serializer"""
+        response = self.client.get(reverse('test-paginated-response-serializer'))
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(response.json(), {
+            'results': [
+                {'id': 0},
+                {'id': 2},
+                {'id': 4},
+                {'id': 6},
+            ],
+            'page': 1,
+            'pages': 1,
+            'count': 4,
+            'first': None,
+            'previous': None,
+            'next': None,
+            'last': None,
+            'before': 0,
+            'more': 0,
+        })
+
+    def test_explicit_data_serializer_response(self):
+        """utility returns response with explicit data serialized via serializer"""
+        response = self.client.get(reverse('test-paginated-response-data-serializer'))
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(response.json(), {
+            'results': [
+                {'id': 'aa'},
+                {'id': 'bb'},
+                {'id': 'cc'},
+                {'id': 'dd'},
+            ],
+            'page': 1,
+            'pages': 1,
+            'count': 4,
+            'first': None,
+            'previous': None,
+            'next': None,
+            'last': None,
+            'before': 0,
+            'more': 0,
+        })
+
+    def test_explicit_data_extra_response(self):
+        """utility returns response with explicit data and extra"""
+        response = self.client.get(reverse('test-paginated-response-data-extra'))
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(response.json(), {
+            'results': ['a', 'b', 'c', 'd'],
+            'page': 1,
+            'pages': 1,
+            'count': 4,
+            'first': None,
+            'previous': None,
+            'next': 'EXTRA',
+            'last': None,
+            'before': 0,
+            'more': 0,
+            'lorem': 'ipsum'
+        })

+ 1 - 1
misago/templates/misago/threadslist/base.html

@@ -76,7 +76,7 @@
           {% if paginator.pages > 1 %}
             <nav>
               <ul class="pager">
-                {% if paginator.previous %}
+                {% if paginator.previous > 1 %}
                   <li class="previous">
                     <a href="{{ REQUEST_PATH }}?page={{ paginator.previous }}">
                       <span aria-hidden="true" class="material-icon">

+ 1 - 1
misago/templates/misago/userslists/rank.html

@@ -119,7 +119,7 @@
           <div class="pager-undercontent">
             <nav>
               <ul class="pager">
-                {% if paginator.previous %}
+                {% if paginator.previous > 1 %}
                 <li class="previous">
                   <a href="{% url 'misago:users-rank' slug=rank.slug page=paginator.previous %}">
                     <span aria-hidden="true" class="material-icon">

+ 3 - 9
misago/users/api/userendpoints/list.py

@@ -10,7 +10,7 @@ from rest_framework.response import Response
 from misago.conf import settings
 from misago.core.cache import cache
 from misago.core.shortcuts import (
-    get_int_or_404, get_object_or_404, paginate, pagination_dict)
+    get_int_or_404, get_object_or_404, paginate, paginated_response)
 
 from ...activepostersranking import get_active_posters_ranking
 from ...models import Rank
@@ -66,15 +66,9 @@ def generic(request):
 
     list_page = paginate(queryset, page, settings.MISAGO_USERS_PER_PAGE, 4)
 
-    users = list_page.object_list
-    make_users_status_aware(request.user, users)
+    make_users_status_aware(request.user, list_page.object_list)
 
-    data = pagination_dict(list_page)
-    data.update({
-        'results': UserSerializer(users, many=True).data
-    })
-
-    return Response(data)
+    return paginated_response(list_page, serializer=UserSerializer)
 
 
 LISTS = {