Repository: flask-admin/flask-admin

Issue:
Error in filters with hybrid_property
Hi,

If i create a   @hybrid_property that return a text as follows

```
class TaxiDriver(User):

    @hybrid_property
    def full_name(self):
        return '%s %s' % (self.surname, self.second_surname)
```

If i try use  full_name as column_filter I get the next error when runserever

```
  File "manage.py", line 21, in <module>
    app = create_app(os.getenv('FLASK_CONFIG') or 'default')
  File "/home/jarenere/proyectos/dfa-ztaxi/ZgzTaxi/app/__init__.py", line 60, in create_app
    from .main import main as main_blueprint
  File "/home/jarenere/proyectos/dfa-ztaxi/ZgzTaxi/app/main/__init__.py", line 5, in <module>
    from . import views
  File "/home/jarenere/proyectos/dfa-ztaxi/ZgzTaxi/app/main/views.py", line 123, in <module>
    admin.add_view(DeviceAdmin(Device, db.session))
  File "/home/jarenere/proyectos/dfa-ztaxi/ZgzTaxi/venv/lib/python3.5/site-packages/flask_admin/contrib/sqla/view.py", line 318, in __init__
    menu_icon_value=menu_icon_value)
  File "/home/jarenere/proyectos/dfa-ztaxi/ZgzTaxi/venv/lib/python3.5/site-packages/flask_admin/model/base.py", line 771, in __init__
    self._refresh_cache()
  File "/home/jarenere/proyectos/dfa-ztaxi/ZgzTaxi/venv/lib/python3.5/site-packages/flask_admin/model/base.py", line 890, in _refresh_cache
    self._refresh_filters_cache()
  File "/home/jarenere/proyectos/dfa-ztaxi/ZgzTaxi/venv/lib/python3.5/site-packages/flask_admin/model/base.py", line 799, in _refresh_filters_cache
    self._filters = self.get_filters()
  File "/home/jarenere/proyectos/dfa-ztaxi/ZgzTaxi/venv/lib/python3.5/site-packages/flask_admin/model/base.py", line 1098, in get_filters
    flt = self.scaffold_filters(n)
  File "/home/jarenere/proyectos/dfa-ztaxi/ZgzTaxi/venv/lib/python3.5/site-packages/flask_admin/contrib/sqla/view.py", line 610, in scaffold_filters
    columns = tools.get_columns_for_field(attr)
  File "/home/jarenere/proyectos/dfa-ztaxi/ZgzTaxi/venv/lib/python3.5/site-packages/flask_admin/contrib/sqla/tools.py", line 118, in get_columns_for_field
    raise Exception('Invalid field %s: does not contains any columns.' % field)
Exception: Invalid field TaxiDriver.surname TaxiDriver.second_surname: does not contains any columns.

```

If i use

```
    @hybrid_property
    def full_name(self):
        return self.surname + ' '  + self.second_surname
```

It fail when i try used the filter

```
  File "/home/jarenere/proyectos/dfa-ztaxi/ZgzTaxi/venv/lib/python3.5/site-packages/flask/app.py", line 2000, in __call__
    return self.wsgi_app(environ, start_response)
  File "/home/jarenere/proyectos/dfa-ztaxi/ZgzTaxi/venv/lib/python3.5/site-packages/flask/app.py", line 1991, in wsgi_app
    response = self.make_response(self.handle_exception(e))
  File "/home/jarenere/proyectos/dfa-ztaxi/ZgzTaxi/venv/lib/python3.5/site-packages/flask/app.py", line 1567, in handle_exception
    reraise(exc_type, exc_value, tb)
  File "/home/jarenere/proyectos/dfa-ztaxi/ZgzTaxi/venv/lib/python3.5/site-packages/flask/_compat.py", line 33, in reraise
    raise value
  File "/home/jarenere/proyectos/dfa-ztaxi/ZgzTaxi/venv/lib/python3.5/site-packages/flask/app.py", line 1988, in wsgi_app
    response = self.full_dispatch_request()
  File "/home/jarenere/proyectos/dfa-ztaxi/ZgzTaxi/venv/lib/python3.5/site-packages/flask/app.py", line 1641, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/home/jarenere/proyectos/dfa-ztaxi/ZgzTaxi/venv/lib/python3.5/site-packages/flask/app.py", line 1544, in handle_user_exception
    reraise(exc_type, exc_value, tb)
  File "/home/jarenere/proyectos/dfa-ztaxi/ZgzTaxi/venv/lib/python3.5/site-packages/flask/_compat.py", line 33, in reraise
    raise value
  File "/home/jarenere/proyectos/dfa-ztaxi/ZgzTaxi/venv/lib/python3.5/site-packages/flask/app.py", line 1639, in full_dispatch_request
    rv = self.dispatch_request()
  File "/home/jarenere/proyectos/dfa-ztaxi/ZgzTaxi/venv/lib/python3.5/site-packages/flask/app.py", line 1625, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "/home/jarenere/proyectos/dfa-ztaxi/ZgzTaxi/venv/lib/python3.5/site-packages/flask_admin/base.py", line 69, in inner
    return self._run_view(f, *args, **kwargs)
  File "/home/jarenere/proyectos/dfa-ztaxi/ZgzTaxi/venv/lib/python3.5/site-packages/flask_admin/base.py", line 368, in _run_view
    return fn(self, *args, **kwargs)
  File "/home/jarenere/proyectos/dfa-ztaxi/ZgzTaxi/venv/lib/python3.5/site-packages/flask_admin/model/base.py", line 1818, in index_view
    view_args.search, view_args.filters)
  File "/home/jarenere/proyectos/dfa-ztaxi/ZgzTaxi/venv/lib/python3.5/site-packages/flask_admin/contrib/sqla/view.py", line 972, in get_list
    filters)
  File "/home/jarenere/proyectos/dfa-ztaxi/ZgzTaxi/venv/lib/python3.5/site-packages/flask_admin/contrib/sqla/view.py", line 890, in _apply_filters
    query = flt.apply(query, clean_value, alias)
  File "/home/jarenere/proyectos/dfa-ztaxi/ZgzTaxi/venv/lib/python3.5/site-packages/flask_admin/contrib/sqla/filters.py", line 55, in apply
    return query.filter(self.get_column(alias).ilike(stmt))
  File "/home/jarenere/proyectos/dfa-ztaxi/ZgzTaxi/venv/lib/python3.5/site-packages/flask_admin/contrib/sqla/filters.py", line 29, in get_column
    return self.column if alias is None else getattr(alias, self.column.key)
```



Discussion:
Hey,
1) How do you define the filter in the view? Using a string?
2) Which version of flask-admin are you using?

Hi, 

>  1) How do you define the filter in the view? Using a string?
> Yes, using a string

my environment

```
Flask-SQLAlchemy==2.1
Flask-Admin==1.4.2
Flask==0.11.1
SQLAlchemy==1.0.15
Python 3.5.2
```

I also tested with python 2.7 and nothing

Include a sample file, the hybrid_property is in `User`, the filter in `Post`

[sqla-test.tar.gz](https://github.com/flask-admin/flask-admin/files/473283/sqla-test.tar.gz)
[app.py.txt](https://github.com/flask-admin/flask-admin/files/473281/app.py.txt)

Ah yes, it seems like the system doesn't support remote hybrid property filters (trying to filter on the hybrid property in the "user" view works).
I'm trying to fix this, maybe you can help me :)

I got this example SQL for the filter:
`SELECT post.id AS post_id, post.title AS post_title, post.text AS post_text, post.date AS post_date, post.user_id AS post_user_id 
FROM "user", post LEFT OUTER JOIN "user" AS user_1 ON user_1.id = post.user_id 
WHERE lower(("user".first_name || " " || "user".last_name || " " || "user".username)) LIKE lower("%harry%")`

For some reason it selects all records. The WHERE clause is fine since I tested it separately, but I think there's a problem with the join... though I can't figure out what's wrong with it.

Hi, 
The SQL statement is wrong,  in the from, the user is out of place

``` sql
SELECT post.id AS post_id, post.title AS post_title, post.text AS post_text, post.date AS post_date, post.user_id AS post_user_id, user_1.username
FROM post 
LEFT OUTER JOIN "user" AS user_1 ON user_1.id = post.user_id 
WHERE lower((user_1.first_name || " " || user_1.last_name || " " || user_1.username)) LIKE lower("%harry%")
```

or  implicit join 

``` sql
SELECT post.id AS post_id, post.title AS post_title, post.text AS post_text, post.date AS post_date, post.user_id AS post_user_id, user_1.username
FROM post, "user"  AS user_1
WHERE user_1.id = post.user_id
AND lower((user_1.first_name || " " || user_1.last_name || " " || user_1.username)) LIKE lower("%harry%")
```


Resolving patch:
diff --git a/flask_admin/contrib/sqla/tools.py b/flask_admin/contrib/sqla/tools.py
--- a/flask_admin/contrib/sqla/tools.py
+++ b/flask_admin/contrib/sqla/tools.py
@@ -190,7 +190,12 @@ def get_hybrid_properties(model):
 
 
 def is_hybrid_property(model, attr_name):
-    return attr_name in get_hybrid_properties(model)
+    names = attr_name.split('.')
+    last_model = model
+    for i in range(len(names)-1):
+        last_model = getattr(last_model, names[i]).property.argument
+    last_name = names[-1]
+    return last_name in get_hybrid_properties(last_model)
 
 
 def is_relationship(attr):
@@ -199,9 +204,3 @@ def is_relationship(attr):
 
 def is_association_proxy(attr):
     return hasattr(attr, 'extension_type') and attr.extension_type == ASSOCIATION_PROXY
-
-
-def get_association_proxy_column_name(attr):
-    # TODO find a better way to get the name
-    name, = [key for key, value in inspect(attr.owning_class).all_orm_descriptors.items() if value is attr]
-    return name
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
@@ -2,6 +2,7 @@
 import warnings
 import inspect
 
+from speaklater import is_lazy_string, make_lazy_string
 from sqlalchemy.orm.attributes import InstrumentedAttribute
 from sqlalchemy.orm import joinedload, aliased
 from sqlalchemy.sql.expression import desc
@@ -617,6 +618,7 @@ def scaffold_filters(self, name):
             is_hybrid_property = tools.is_hybrid_property(self.model, name)
             if is_hybrid_property:
                 column = attr
+                column.key = name.split('.')[-1]
             else:
                 columns = tools.get_columns_for_field(attr)
 
@@ -636,7 +638,14 @@ def scaffold_filters(self, name):
                 if not isinstance(name, string_types):
                     visible_name = self.get_column_name(name.property.key)
                 else:
-                    visible_name = self.get_column_name(name)
+                    column_name = self.get_column_name(name)
+
+                    def prettify():
+                        return column_name.replace('.', ' / ')
+                    if is_lazy_string(column_name):
+                        visible_name = make_lazy_string(prettify)
+                    else:
+                        visible_name = prettify()
 
             type_name = type(column.type).__name__
 


Test patch: