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 ...@@ -4,7 +4,7 @@ import inspect
from sqlalchemy.orm.attributes import InstrumentedAttribute from sqlalchemy.orm.attributes import InstrumentedAttribute
from sqlalchemy.orm import joinedload, aliased 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 import Boolean, Table, func, or_
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
...@@ -550,8 +550,11 @@ class ModelView(BaseModelView): ...@@ -550,8 +550,11 @@ class ModelView(BaseModelView):
if attr is None: if attr is None:
raise Exception('Failed to find field for filter: %s' % name) raise Exception('Failed to find field for filter: %s' % name)
# Figure out filters for related column # Figure out filters for related column, unless it's a hybrid_property
if hasattr(attr, 'property') and hasattr(attr.property, 'direction'): 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 = [] filters = []
for p in self._get_model_iterator(attr.property.mapper.class_): for p in self._get_model_iterator(attr.property.mapper.class_):
...@@ -620,7 +623,9 @@ class ModelView(BaseModelView): ...@@ -620,7 +623,9 @@ class ModelView(BaseModelView):
if isinstance(filter, sqla_filters.BaseSQLAFilter): if isinstance(filter, sqla_filters.BaseSQLAFilter):
column = filter.column 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] self._filter_joins[column] = [column.table]
return filter return filter
......
...@@ -8,6 +8,8 @@ from flask_admin._compat import iteritems ...@@ -8,6 +8,8 @@ from flask_admin._compat import iteritems
from flask_admin.contrib.sqla import ModelView, filters from flask_admin.contrib.sqla import ModelView, filters
from flask_babelex import Babel from flask_babelex import Babel
from sqlalchemy.ext.hybrid import hybrid_property
from . import setup from . import setup
from datetime import datetime, time, date from datetime import datetime, time, date
...@@ -1217,6 +1219,53 @@ def test_column_filters(): ...@@ -1217,6 +1219,53 @@ def test_column_filters():
ok_('test1_val_2' not in data) 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(): def test_url_args():
app, db, admin = setup() 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