Repository: flask-admin/flask-admin

Issue:
SQLA column_sortable_list with a related table's column object
I understand this one is a bit of an edge case, but I figured I'd make an issue for it anyway.

If your ModelView is for `Model1`, sorting won't work if you use a related model's SQLA column object in `column_sortable_list`. For example: `column_sortable_list=[Model2.column1]`

Here's a unit test that demonstrates the problem: https://github.com/flask-admin/flask-admin/compare/master...pawl:fix_sortable_model_relations?expand=1#diff-527d8cf625fb5fc58b22130d4ded2201R1726

Fixing this one might require changing flask-admin to store column objects (instead of just the names) in `_sortable_columns`, `_list_columns`, `column_exclude_list`, etc. It also works fine now if you use the string `'<relation name>.<column name>'`. So, I'm not sure if it's worth fixing.



Discussion:

Resolving patch:
diff --git a/flask_admin/contrib/sqla/form.py b/flask_admin/contrib/sqla/form.py
--- a/flask_admin/contrib/sqla/form.py
+++ b/flask_admin/contrib/sqla/form.py
@@ -9,7 +9,7 @@
 from flask_admin.model.fields import AjaxSelectField, AjaxSelectMultipleField
 from flask_admin.model.helpers import prettify_name
 from flask_admin._backwards import get_property
-from flask_admin._compat import iteritems, text_type
+from flask_admin._compat import iteritems
 
 from .validators import Unique
 from .fields import (QuerySelectField, QuerySelectMultipleField,
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
@@ -475,10 +475,15 @@ def get_sortable_columns(self):
                     column, path = tools.get_field_with_path(self.model, c)
                     column_name = c
 
-                if path:
+                if path and hasattr(path[0], 'property'):
                     # column is in another table, use full path as column_name
                     column_name = text_type(c)
                     self._sortable_joins[column_name] = path
+                elif path:
+                    raise Exception("For sorting columns in a related table, "
+                                    "column_sortable_list requires a string "
+                                    "like '<relation name>.<column name>'. "
+                                    "Failed on: {0}".format(c))
                 else:
                     # column is in same table, use only model attribute name
                     column_name = column.key


Test patch:
diff --git a/flask_admin/tests/sqla/test_basic.py b/flask_admin/tests/sqla/test_basic.py
--- a/flask_admin/tests/sqla/test_basic.py
+++ b/flask_admin/tests/sqla/test_basic.py
@@ -271,21 +271,6 @@ def test_complex_list_columns():
     data = rv.data.decode('utf-8')
     ok_('model1_val1' in data)
 
-    # TODO: Allow providing a list of related models
-    """
-    # test column_list with a list of models on a relation
-    view2 = CustomModelView(M2, db.session, endpoint='model2_2',
-                            column_list=[M1.test1])
-    admin.add_view(view2)
-
-    client = app.test_client()
-
-    rv = client.get('/admin/model2_2/')
-    eq_(rv.status_code, 200)
-    data = rv.data.decode('utf-8')
-    ok_('model1_val1' in data)
-    """
-
 
 def test_exclude_columns():
     app, db, admin = setup()
@@ -1712,31 +1697,23 @@ def test_complex_sort():
     rv = client.get('/admin/model2/?sort=1')
     eq_(rv.status_code, 200)
 
-    # test sorting on relation object - M2.string_field
-    view2 = CustomModelView(M1, db.session,
-                            column_list=['model2.string_field'],
-                            column_sortable_list=[M2.string_field])
-    admin.add_view(view2)
-
-    client = app.test_client()
 
-    rv = client.get('/admin/model1/?sort=1')
-    eq_(rv.status_code, 200)
-    data = rv.data.decode('utf-8')
-    ok_('Sort by' in data)
+@raises(Exception)
+def test_complex_sort_exception():
+    app, db, admin = setup()
+    M1, M2 = create_models(db)
 
-    # test sorting on relations with model in column_list
-    view3 = CustomModelView(M1, db.session, endpoint="model1_2",
-                            column_list=[M2.string_field],
-                            column_sortable_list=[M2.string_field])
-    admin.add_view(view3)
+    # test column_sortable_list on a related table's column object
+    view = CustomModelView(M2, db.session, endpoint="model2_3",
+                           column_sortable_list=[M1.test1])
+    admin.add_view(view)
 
-    client = app.test_client()
+    sort_column = view._get_column_by_idx(0)[0]
+    _, data = view.get_list(0, sort_column, False, None, None)
 
-    rv = client.get('/admin/model1_2/?sort=1')
-    eq_(rv.status_code, 200)
-    data = rv.data.decode('utf-8')
-    ok_('Sort by' in data)
+    eq_(len(data), 2)
+    eq_(data[0].model1.test1, 'a')
+    eq_(data[1].model1.test1, 'b')
 
 
 def test_default_complex_sort():
@@ -1762,6 +1739,17 @@ def test_default_complex_sort():
     eq_(data[0].model1.test1, 'a')
     eq_(data[1].model1.test1, 'b')
 
+    # test column_default_sort on a related table's column object
+    view2 = CustomModelView(M2, db.session, endpoint="model2_2",
+                            column_default_sort=(M1.test1, False))
+    admin.add_view(view2)
+
+    _, data = view2.get_list(0, None, None, None, None)
+
+    eq_(len(data), 2)
+    eq_(data[0].model1.test1, 'a')
+    eq_(data[1].model1.test1, 'b')
+
 
 def test_extra_fields():
     app, db, admin = setup()