Commit dd66d0ee authored by Serge S. Koval's avatar Serge S. Koval

Merge pull request #962 from pawl/fix-hybrid-properties

Fix SQLAlchemy hybrid_property support, add example and test
parents 7585da1d 38c7894b
Example of how to use (and filter on) a hybrid_property with the SQLAlchemy backend.
Hybrid properties allow you to treat calculations (for example: first_name + last_name)
like any other database column.
To run this example:
1. Clone the repository::
git clone https://github.com/flask-admin/flask-admin.git
cd flask-admin
2. Create and activate a virtual environment::
virtualenv env
source env/bin/activate
3. Install requirements::
pip install -r 'examples/sqla-hybrid_property/requirements.txt'
4. Run the application::
python examples/sqla-hybrid_property/app.py
The first time you run this example, a sample sqlite database gets populated automatically. To suppress this behaviour,
comment the following lines in app.py:::
if not os.path.exists(database_path):
build_sample_db()
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.ext.hybrid import hybrid_property
import flask_admin as admin
from flask_admin.contrib import sqla
from flask_admin.contrib.sqla.filters import IntGreaterFilter
# Create application
app = Flask(__name__)
# Create dummy secrey key so we can use sessions
app.config['SECRET_KEY'] = '123456790'
# Create in-memory database
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///sample_db_2.sqlite'
app.config['SQLALCHEMY_ECHO'] = True
db = SQLAlchemy(app)
# Flask views
@app.route('/')
def index():
return '<a href="/admin/">Click me to get to Admin!</a>'
class Screen(db.Model):
__tablename__ = 'screen'
id = db.Column(db.Integer, primary_key=True)
width = db.Column(db.Integer, nullable=False)
height = db.Column(db.Integer, nullable=False)
@hybrid_property
def number_of_pixels(self):
return self.width * self.height
class ScreenAdmin(sqla.ModelView):
''' Flask-admin can not automatically find a hybrid_property yet. You will
need to manually define the column in list_view/filters/sorting/etc.'''
list_columns = ['id', 'width', 'height', 'number_of_pixels']
column_sortable_list = ['id', 'width', 'height', 'number_of_pixels']
# make sure the type of your filter matches your hybrid_property
column_filters = [IntGreaterFilter(Screen.number_of_pixels,
'Number of Pixels')]
# Create admin
admin = admin.Admin(app, name='Example: SQLAlchemy2', template_mode='bootstrap3')
admin.add_view(ScreenAdmin(Screen, db.session))
if __name__ == '__main__':
# Create DB
db.create_all()
# Start app
app.run(debug=True)
Flask
Flask-Admin
Flask-SQLAlchemy
......@@ -4,7 +4,7 @@ import inspect
from sqlalchemy.orm.attributes import InstrumentedAttribute
from sqlalchemy.orm import joinedload, aliased
from sqlalchemy.sql.expression import desc
from sqlalchemy.sql.expression import desc, ColumnElement
from sqlalchemy import Boolean, Table, func, or_
from sqlalchemy.exc import IntegrityError
......@@ -550,8 +550,11 @@ class ModelView(BaseModelView):
if attr is None:
raise Exception('Failed to find field for filter: %s' % name)
# Figure out filters for related column
if hasattr(attr, 'property') and hasattr(attr.property, 'direction'):
# Figure out filters for related column, unless it's a hybrid_property
if isinstance(attr, ColumnElement):
warnings.warn(('Unable to scaffold the filter for %s, scaffolding '
'for hybrid_property is not supported yet.') % name)
elif hasattr(attr, 'property') and hasattr(attr.property, 'direction'):
filters = []
for p in self._get_model_iterator(attr.property.mapper.class_):
......@@ -620,7 +623,9 @@ class ModelView(BaseModelView):
if isinstance(filter, sqla_filters.BaseSQLAFilter):
column = filter.column
if self._need_join(column.table):
# hybrid_property joins are not supported yet
if (isinstance(column, InstrumentedAttribute) and
self._need_join(column.table)):
self._filter_joins[column] = [column.table]
return filter
......
......@@ -8,6 +8,8 @@ from flask_admin._compat import iteritems
from flask_admin.contrib.sqla import ModelView, filters
from flask_babelex import Babel
from sqlalchemy.ext.hybrid import hybrid_property
from . import setup
from datetime import datetime, time, date
......@@ -1217,6 +1219,53 @@ def test_column_filters():
ok_('test1_val_2' not in data)
def test_hybrid_property():
app, db, admin = setup()
class Model1(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String)
width = db.Column(db.Integer)
height = db.Column(db.Integer)
@hybrid_property
def number_of_pixels(self):
return self.width * self.height
db.create_all()
db.session.add(Model1(id=1, name="test_row_1", width=25, height=25))
db.session.add(Model1(id=2, name="test_row_2", width=10, height=10))
db.session.commit()
client = app.test_client()
view = CustomModelView(
Model1, db.session,
column_default_sort='number_of_pixels',
column_filters = [filters.IntGreaterFilter(Model1.number_of_pixels,
'Number of Pixels')]
)
admin.add_view(view)
# filters - hybrid_property integer - greater
rv = client.get('/admin/model1/?flt0_0=600')
eq_(rv.status_code, 200)
data = rv.data.decode('utf-8')
ok_('test_row_1' in data)
ok_('test_row_2' not in data)
# sorting
rv = client.get('/admin/model1/?sort=0')
eq_(rv.status_code, 200)
_, data = view.get_list(0, None, None, None, None)
eq_(len(data), 2)
eq_(data[0].name, 'test_row_2')
eq_(data[1].name, 'test_row_1')
def test_url_args():
app, db, admin = setup()
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment