From 8d80fbb8f80ee4bcd58ea481a2a26fe73a437d47 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Thu, 30 Apr 2020 14:59:19 +0200
Subject: [PATCH 1/2] generic endpoint: define optional category for display
 (#17729)

---
 passerelle/base/models.py                     |  5 +++-
 .../passerelle/manage/service_view.html       | 16 +++++++-----
 passerelle/utils/api.py                       | 17 +++++++++++--
 tests/test_generic_endpoint.py                | 25 +++++++++++++++++++
 4 files changed, 54 insertions(+), 9 deletions(-)

diff --git a/passerelle/base/models.py b/passerelle/base/models.py
index f403e60b..bfc1962f 100644
--- a/passerelle/base/models.py
+++ b/passerelle/base/models.py
@@ -132,6 +132,9 @@ class BaseResource(models.Model):
     # permission descriptions
     _can_access_description = _('Access is limited to the following API users:')
 
+    # category ordering for display
+    _category_ordering = []
+
     class Meta:
         abstract = True
 
@@ -253,7 +256,7 @@ class BaseResource(models.Model):
                     endpoint_info = copy.copy(method.endpoint_info)
                     endpoint_info.http_method = http_method
                     endpoints.append(endpoint_info)
-        endpoints.sort(key=lambda x: (x.display_order or 99999999, x.name or '', x.pattern or ''))
+        endpoints.sort(key=lambda x: (x.display_category_order, x.display_category, x.display_order or 99999999, x.name or '', x.pattern or ''))
         if hasattr(self, 'queries'):
             self.append_custom_queries(endpoints)
         return endpoints
diff --git a/passerelle/templates/passerelle/manage/service_view.html b/passerelle/templates/passerelle/manage/service_view.html
index 136f2888..37a50306 100644
--- a/passerelle/templates/passerelle/manage/service_view.html
+++ b/passerelle/templates/passerelle/manage/service_view.html
@@ -62,13 +62,17 @@
   <div>
   {% if object.api_description %}<p>{{object.api_description|safe}}</p>{% endif %}
   {% block endpoints %}
-    <ul class="endpoints">
-    {% for endpoint in object.get_endpoints_infos %}
-      {% if endpoint.show %}
-        {% include "passerelle/manage/endpoint.html" with endpoint=endpoint %}
-      {% endif %}
+    {% regroup object.get_endpoints_infos by display_category as endpoints_groups %}
+    {% for endpoints in endpoints_groups %}
+      {% if endpoints.grouper %}<h4>{{ endpoints.grouper }}</h4>{% elif not forloop.first %}<h4>{% trans "Misc" %}</h4>{% endif %}
+      <ul class="endpoints">
+        {% for endpoint in endpoints.list %}
+          {% if endpoint.show %}
+            {% include "passerelle/manage/endpoint.html" with endpoint=endpoint %}
+          {% endif %}
+        {% endfor %}
+      </ul>
     {% endfor %}
-    </ul>
   {% endblock %}
   </div>
 </div>
diff --git a/passerelle/utils/api.py b/passerelle/utils/api.py
index e3218171..87e0a48b 100644
--- a/passerelle/utils/api.py
+++ b/passerelle/utils/api.py
@@ -23,7 +23,7 @@ from django.utils import six
 from django.utils.safestring import mark_safe
 
 # make APIError available from this module
-from .jsonresponse import APIError
+from .jsonresponse import APIError  # noqa
 
 
 class endpoint(object):
@@ -45,7 +45,8 @@ class endpoint(object):
                  post=None,
                  show=True,
                  show_undocumented_params=True,
-                 display_order=0):
+                 display_order=0,
+                 display_category=''):
         self.perm = perm
         self.methods = methods
         self.serializer_type = serializer_type
@@ -75,6 +76,7 @@ class endpoint(object):
         self.show = show
         self.show_undocumented_params = show_undocumented_params
         self.display_order = display_order
+        self.display_category = display_category
 
     def __call__(self, func):
         func.endpoint_info = self
@@ -83,6 +85,17 @@ class endpoint(object):
         self.func = func
         return func
 
+    @property
+    def display_category_order(self):
+        if not self.display_category:
+            # no category, put it at the end
+            return 99999999
+        # self.object is attached in BaseResource.get_endpoints_infos method
+        if self.display_category not in self.object._category_ordering:
+            # category without ordering, put it at the end, just before no category
+            return 99999998
+        return self.object._category_ordering.index(self.display_category)
+
     def get_example_params(self):
         return dict([(x, self.parameters[x]['example_value']) for x in self.parameters or {}
                      if x in self.parameters and 'example_value' in self.parameters[x]])
diff --git a/tests/test_generic_endpoint.py b/tests/test_generic_endpoint.py
index 29495b97..75a38429 100644
--- a/tests/test_generic_endpoint.py
+++ b/tests/test_generic_endpoint.py
@@ -752,9 +752,34 @@ class DummyConnectorWithOrdering(DummyConnectorBase):
         pass
 
 
+class DummyConnectorWithOrderingAndCategory(DummyConnectorBase):
+    _category_ordering = ['Foo', 'Bar']
+
+    @endpoint(display_category='Foo')
+    def a(self, request):
+        pass
+
+    @endpoint(display_category='Bar', display_order=2)
+    def b(self, request):
+        pass
+
+    @endpoint()
+    def c(self, request):
+        pass
+
+    @endpoint(display_category='Bar', display_order=1)
+    def d(self, request):
+        pass
+
+    @endpoint(display_category='Blah')
+    def e(self, request):
+        pass
+
+
 @pytest.mark.parametrize('connector_class, expected_ordering', [
     (DummyConnectorWithoutOrdering, ['a', 'b', 'caa', 'cbb']),
     (DummyConnectorWithOrdering, ['caa', 'cbb', 'a', 'b']),
+    (DummyConnectorWithOrderingAndCategory, ['a', 'd', 'b', 'e', 'c']),
 ])
 def test_generic_up_in_endpoints_ordering(db, app, connector_class, expected_ordering):
     connector = connector_class()
-- 
2.20.1

