Repository: getredash/redash

Issue:
Add server-side sorting for dashboard list
<!--
#####################################################################
#
# Need support? USE THE FORUM! https://discuss.redash.io/c/support.
#
# Don't have steps to reproduce and actually not sure it's a bug?
# Use the forum! https://discuss.redash.io/c/support.
#
#####################################################################

**Got an idea for a new feature?** Check if it isn't on the roadmap already: http://bit.ly/redash-roadmap and start a new discussion in the features category: https://discuss.redash.io/c/feature-requests 🌟.

Found a bug? Please fill out the sections below... thank you 👍
-->

### Issue Summary

The dashboard list view doesn't currently support server side sorting like the new query list view.

### Steps to Reproduce

1. Open the dashboard list view and try to sort
2. Find the table column headers not able to sort

Any other info e.g. Why do you consider this to be a bug? What did you expect to happen instead?

### Technical details:

* Redash Version: Redash master (bc15c0b6d1f4fc79e10b6920ad6117f118a8c820)
* Browser/OS: Firefox/macOS
* How did you install Redash: Local development



Discussion:

Resolving patch:
diff --git a/redash/handlers/dashboards.py b/redash/handlers/dashboards.py
--- a/redash/handlers/dashboards.py
+++ b/redash/handlers/dashboards.py
@@ -1,11 +1,11 @@
-from itertools import chain
-
 from flask import request, url_for
-from funcy import distinct, project, take
+from funcy import project, rpartial
 
 from flask_restful import abort
-from redash import models, serializers, settings
-from redash.handlers.base import BaseResource, get_object_or_404, paginate, filter_by_tags
+from redash import models, serializers
+from redash.handlers.base import (BaseResource, get_object_or_404, paginate,
+                                  filter_by_tags,
+                                  order_results as _order_results)
 from redash.serializers import serialize_dashboard
 from redash.permissions import (can_modify, require_admin_or_owner,
                                 require_object_modify_permission,
@@ -13,24 +13,61 @@
 from sqlalchemy.orm.exc import StaleDataError
 
 
+# Ordering map for relationships
+order_map = {
+    'name': 'lowercase_name',
+    '-name': '-lowercase_name',
+    'created_at': 'created_at',
+    '-created_at': '-created_at',
+}
+
+order_results = rpartial(_order_results, '-created_at', order_map)
+
+
 class DashboardListResource(BaseResource):
     @require_permission('list_dashboards')
     def get(self):
         """
         Lists all accessible dashboards.
+
+        :qparam number page_size: Number of queries to return per page
+        :qparam number page: Page number to retrieve
+        :qparam number order: Name of column to order by
+        :qparam number q: Full text search term
+
+        Responds with an array of :ref:`dashboard <dashboard-response-label>`
+        objects.
         """
         search_term = request.args.get('q')
 
         if search_term:
-            results = models.Dashboard.search(self.current_org, self.current_user.group_ids, self.current_user.id, search_term)
+            results = models.Dashboard.search(
+                self.current_org,
+                self.current_user.group_ids,
+                self.current_user.id,
+                search_term,
+            )
         else:
-            results = models.Dashboard.all(self.current_org, self.current_user.group_ids, self.current_user.id)
+            results = models.Dashboard.all(
+                self.current_org,
+                self.current_user.group_ids,
+                self.current_user.id,
+            )
 
         results = filter_by_tags(results, models.Dashboard.tags)
 
+        # order results according to passed order parameter
+        ordered_results = order_results(results)
+
         page = request.args.get('page', 1, type=int)
         page_size = request.args.get('page_size', 25, type=int)
-        response = paginate(results, page, page_size, serialize_dashboard)
+
+        response = paginate(
+            ordered_results,
+            page=page,
+            page_size=page_size,
+            serializer=serialize_dashboard,
+        )
 
         return response
 
@@ -120,7 +157,7 @@ def post(self, dashboard_slug):
 
         require_object_modify_permission(dashboard, self.current_user)
 
-        updates = project(dashboard_properties, ('name', 'layout', 'version', 'tags', 
+        updates = project(dashboard_properties, ('name', 'layout', 'version', 'tags',
                                                  'is_draft', 'dashboard_filters_enabled'))
 
         # SQLAlchemy handles the case where a concurrent transaction beats us
diff --git a/redash/models.py b/redash/models.py
--- a/redash/models.py
+++ b/redash/models.py
@@ -1405,6 +1405,16 @@ def favorites(cls, user, base_query=None):
     def get_by_slug_and_org(cls, slug, org):
         return cls.query.filter(cls.slug == slug, cls.org == org).one()
 
+    @hybrid_property
+    def lowercase_name(self):
+        "Optional property useful for sorting purposes."
+        return self.name.lower()
+
+    @lowercase_name.expression
+    def lowercase_name(cls):
+        "The SQLAlchemy expression for the property above."
+        return func.lower(cls.name)
+
     def __unicode__(self):
         return u"%s=%s" % (self.id, self.name)
 


Test patch: