Commit e0f6dace authored by PJ Janse van Rensburg's avatar PJ Janse van Rensburg

Merge branch 'sqla-sort-multiple' into nested-categories

parents 704d8d9b 70f3d336
......@@ -4,7 +4,7 @@ Changelog
next release
-----
* Sort on multiple columns with `column_default_sort`
* SQLA: Sort on multiple columns with `column_default_sort` and related models in `column_sortable_list`
* Upgrade Leaflet and Leaflet.draw plugins, used for geoalchemy integration
* Specify `minimum_input_length` for ajax widget
* SQLAlchemy fix that lets you use inline model forms where models have multiple primary keys
......
......@@ -28,11 +28,10 @@ class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
first_name = db.Column(db.String(100))
last_name = db.Column(db.String(100))
username = db.Column(db.String(80), unique=True)
email = db.Column(db.String(120), unique=True)
def __str__(self):
return self.username
return "{}, {}".format(self.last_name, self.first_name)
# Create M2M table
......@@ -46,7 +45,7 @@ class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(120))
text = db.Column(db.Text, nullable=False)
date = db.Column(db.DateTime)
date = db.Column(db.Date)
user_id = db.Column(db.Integer(), db.ForeignKey(User.id))
user = db.relationship(User, backref='posts')
......@@ -54,7 +53,7 @@ class Post(db.Model):
tags = db.relationship('Tag', secondary=post_tags_table)
def __str__(self):
return self.title
return "{}".format(self.title)
class Tag(db.Model):
......@@ -62,7 +61,7 @@ class Tag(db.Model):
name = db.Column(db.Unicode(64))
def __str__(self):
return self.name
return "{}".format(self.name)
class UserInfo(db.Model):
......@@ -75,7 +74,7 @@ class UserInfo(db.Model):
user = db.relationship(User, backref='info')
def __str__(self):
return '%s - %s' % (self.key, self.value)
return "{} - {}".format(self.key, self.value)
class Tree(db.Model):
......@@ -85,7 +84,7 @@ class Tree(db.Model):
parent = db.relationship('Tree', remote_side=[id], backref='children')
def __str__(self):
return self.name
return "{}".format(self.name)
# Flask views
......@@ -96,28 +95,39 @@ def index():
# Customized User model admin
class UserAdmin(sqla.ModelView):
column_list = [
'id',
'last_name',
'first_name',
'email',
]
column_default_sort = [('last_name', False), ('first_name', False)] # sort on multiple columns
inline_models = (UserInfo,)
# Customized Post model admin
class PostAdmin(sqla.ModelView):
# Visible columns in the list view
column_exclude_list = ['text']
# List of columns that can be sorted. For 'user' column, use User.username as
# a column.
column_sortable_list = ('title', ('user', 'user.username'), 'date')
# Rename 'title' columns to 'Post Title' in list view
column_labels = dict(title='Post Title')
column_searchable_list = ('title', User.username, 'tags.name')
column_filters = ('user',
'title',
'date',
'tags',
filters.FilterLike(Post.title, 'Fixed Title', options=(('test1', 'Test 1'), ('test2', 'Test 2'))))
column_default_sort = ('date', True)
column_sortable_list = [
'title',
'date',
('user', ('user.last_name', 'user.first_name')), # sort on multiple columns
]
column_labels = dict(title='Post Title') # Rename 'title' column in list view
column_searchable_list = [
'title',
User.first_name,
User.last_name,
'tags.name',
]
column_filters = [
'user',
'title',
'date',
'tags',
filters.FilterLike(Post.title, 'Fixed Title', options=(('test1', 'Test 1'), ('test2', 'Test 2'))),
]
# Pass arguments to WTForms. In this case, change label for text field to
# be 'Big Text' and add required() validator.
......@@ -127,11 +137,11 @@ class PostAdmin(sqla.ModelView):
form_ajax_refs = {
'user': {
'fields': (User.username, User.email)
'fields': (User.first_name, User.last_name)
},
'tags': {
'fields': (Tag.name,),
'minimum_input_length': 0,
'minimum_input_length': 0, # show suggestions, even before any user input
'placeholder': 'Please select',
'page_size': 5,
},
......@@ -174,8 +184,8 @@ def build_sample_db():
'Riley', 'William', 'James', 'Geoffrey', 'Lisa', 'Benjamin', 'Stacey', 'Lucy'
]
last_names = [
'Brown', 'Smith', 'Patel', 'Jones', 'Williams', 'Johnson', 'Taylor', 'Thomas',
'Roberts', 'Khan', 'Lewis', 'Jackson', 'Clarke', 'James', 'Phillips', 'Wilson',
'Brown', 'Brown', 'Patel', 'Jones', 'Williams', 'Johnson', 'Taylor', 'Thomas',
'Roberts', 'Khan', 'Clarke', 'Clarke', 'Clarke', 'James', 'Phillips', 'Wilson',
'Ali', 'Mason', 'Mitchell', 'Rose', 'Davis', 'Davies', 'Rodriguez', 'Cox', 'Alexander'
]
......@@ -183,9 +193,8 @@ def build_sample_db():
for i in range(len(first_names)):
user = User()
user.first_name = first_names[i]
user.username = first_names[i].lower()
user.last_name = last_names[i]
user.email = user.username + "@example.com"
user.email = first_names[i].lower() + "@example.com"
user_list.append(user)
db.session.add(user)
......
......@@ -484,13 +484,21 @@ class ModelView(BaseModelView):
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, "
......@@ -836,15 +844,9 @@ class ModelView(BaseModelView):
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
......@@ -860,7 +862,11 @@ class ModelView(BaseModelView):
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()
for sort_field, sort_joins, sort_desc in order:
......
......@@ -382,6 +382,12 @@ class BaseModelView(BaseView, ActionsMixin):
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::
......
......@@ -1732,13 +1732,17 @@ def test_complex_sort():
app, db, admin = setup()
M1, M2 = create_models(db)
m1 = M1('b')
m1 = M1(test1='c', test2='x')
db.session.add(m1)
db.session.add(M2('c', model1=m1))
m2 = M1('a')
m2 = M1(test1='b', test2='x')
db.session.add(m2)
db.session.add(M2('c', model1=m2))
db.session.add(M2('b', model1=m2))
m3 = M1(test1='a', test2='y')
db.session.add(m3)
db.session.add(M2('a', model1=m3))
db.session.commit()
......@@ -1750,9 +1754,30 @@ def test_complex_sort():
client = app.test_client()
rv = client.get('/admin/model2/?sort=1')
rv = client.get('/admin/model2/?sort=0')
eq_(rv.status_code, 200)
_, data = view.get_list(0, 'model1.test1', False, None, None)
eq_(data[0].model1.test1, 'a')
eq_(data[1].model1.test1, 'b')
eq_(data[2].model1.test1, 'c')
# test sorting on multiple columns in related model
view2 = CustomModelView(M2, db.session,
column_list=['string_field', 'model1'],
column_sortable_list=[('model1', ('model1.test2', 'model1.test1'))], endpoint="m1_2")
admin.add_view(view2)
rv = client.get('/admin/m1_2/?sort=0')
eq_(rv.status_code, 200)
_, data = view2.get_list(0, 'model1', False, None, None)
eq_(data[0].model1.test1, 'b')
eq_(data[1].model1.test1, 'c')
eq_(data[2].model1.test1, 'a')
@raises(Exception)
def test_complex_sort_exception():
......
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