Repository: flask-admin/flask-admin

Issue:
Multi column sort with column_default_sort
Currently Flask-Admin doesn't support sorting on multiple columns. Could you implement this?

```
column_default_sort = [(Model1.colA, True), ('colB', False)]
```

and

```
column_default_sort = [Model1.colA, 'colB']
```



Discussion:
I have the same problem. 

I would love this. Perhaps reusing the JOIN logic that was added in https://github.com/flask-admin/flask-admin/commit/db3ee4b01944359f9839c0e3d99fdba640d4d25d.

+1

+1 

+1

+1

remove lines 84 and 110 in file [list.html](https://github.com/flask-admin/flask-admin/blob/master/flask_admin/templates/bootstrap3/admin/model/list.html) and add 
`{% set column = loop.index0 %}` right after "for" statement on line 85
+1
+1
+1
+1

Resolving patch:
diff --git a/flask_admin/contrib/sqla/view.py b/flask_admin/contrib/sqla/view.py
--- a/flask_admin/contrib/sqla/view.py
+++ b/flask_admin/contrib/sqla/view.py
@@ -482,13 +482,21 @@ def get_sortable_columns(self):
 
             for c in self.column_sortable_list:
                 if isinstance(c, tuple):
-                    column, path = tools.get_field_with_path(self.model, c[1])
-                    column_name = c[0]
+                    if isinstance(c[1], tuple):
+                        column, path = [], []
+                        for item in c[1]:
+                            column_item, path_item = tools.get_field_with_path(self.model, item)
+                            column.append(column_item)
+                            path.append(path_item)
+                        column_name = c[0]
+                    else:
+                        column, path = tools.get_field_with_path(self.model, c[1])
+                        column_name = c[0]
                 else:
                     column, path = tools.get_field_with_path(self.model, c)
                     column_name = text_type(c)
 
-                if path and hasattr(path[0], 'property'):
+                if path and (hasattr(path[0], 'property') or isinstance(path[0], list)):
                     self._sortable_joins[column_name] = path
                 elif path:
                     raise Exception("For sorting columns in a related table, "
@@ -828,15 +836,9 @@ def _order_by(self, query, joins, sort_joins, sort_field, sort_desc):
             column = sort_field if alias is None else getattr(alias, sort_field.key)
 
             if sort_desc:
-                if isinstance(column, tuple):
-                    query = query.order_by(*map(desc, column))
-                else:
-                    query = query.order_by(desc(column))
+                query = query.order_by(desc(column))
             else:
-                if isinstance(column, tuple):
-                    query = query.order_by(*column)
-                else:
-                    query = query.order_by(column)
+                query = query.order_by(column)
 
         return query, joins
 
@@ -844,11 +846,26 @@ def _get_default_order(self):
         order = super(ModelView, self)._get_default_order()
 
         if order is not None:
-            field, direction = order
+            if isinstance(order[1], bool):
+                field, direction = order
+
+                attr, joins = tools.get_field_with_path(self.model, field)
 
-            attr, joins = tools.get_field_with_path(self.model, field)
+                return attr, joins, direction
+            else:
+                attrs, joins, directions = [], [], []
+
+                for order_item in order:
+                    if isinstance(order_item, tuple):
+                        field, direction = order_item
+                    elif isinstance(order_item, string_types):
+                        field, direction = order_item, False
+                    attr, path = tools.get_field_with_path(self.model, field)
+                    attrs.append(attr)
+                    joins.append(path)
+                    directions.append(direction)
 
-            return attr, joins, direction
+                return attrs, joins, directions
 
         return None
 
@@ -858,14 +875,22 @@ def _apply_sorting(self, query, joins, sort_column, sort_desc):
                 sort_field = self._sortable_columns[sort_column]
                 sort_joins = self._sortable_joins.get(sort_column)
 
-                query, joins = self._order_by(query, joins, sort_joins, sort_field, sort_desc)
+                if isinstance(sort_field, list):
+                    for field_item, join_item in zip(sort_field, sort_joins):
+                        query, joins = self._order_by(query, joins, join_item, field_item, sort_desc)
+                else:
+                    query, joins = self._order_by(query, joins, sort_joins, sort_field, sort_desc)
         else:
             order = self._get_default_order()
 
             if order:
                 sort_field, sort_joins, sort_desc = order
 
-                query, joins = self._order_by(query, joins, sort_joins, sort_field, sort_desc)
+                if isinstance(sort_field, list):
+                    for sort_field_item, sort_joins_item, sort_desc_item in zip(sort_field, sort_joins, sort_desc):
+                        query, joins = self._order_by(query, joins, sort_joins_item, sort_field_item, sort_desc_item)
+                else:
+                    query, joins = self._order_by(query, joins, sort_joins, sort_field, sort_desc)
 
         return query, joins
 
diff --git a/flask_admin/model/base.py b/flask_admin/model/base.py
--- a/flask_admin/model/base.py
+++ b/flask_admin/model/base.py
@@ -360,6 +360,12 @@ class MyModelView(BaseModelView):
             class MyModelView(BaseModelView):
                 column_sortable_list = ('name', ('user', 'user.username'))
 
+        You can also specify multiple fields to be used while sorting::
+
+            class MyModelView(BaseModelView):
+                column_sortable_list = (
+                    'name', ('user', ('user.first_name', 'user.last_name')))
+
         When using SQLAlchemy models, model attributes can be used instead
         of strings::
 
@@ -381,6 +387,13 @@ class MyModelView(BaseModelView):
 
             class MyModelView(BaseModelView):
                 column_default_sort = ('user', True)
+
+        You can also sort by multiple columns. In following example,
+        items will be sorted by email ascending first, and then sorted by user
+        descending::
+
+            class MyModelView(BaseModelView):
+                column_default_sort = ('email', ('user', True))
     """
 
     column_searchable_list = ObsoleteAttr('column_searchable_list',


Test patch: