Skip to content

Commit 0e632cd

Browse files
Merge branch 'master' into inline-boolean
2 parents d0ee1f9 + e4d83a9 commit 0e632cd

File tree

31 files changed

+299
-104
lines changed

31 files changed

+299
-104
lines changed

README.rst

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -43,23 +43,23 @@ on the existing examples, and submit a *pull-request*.
4343

4444
To run the examples in your local environment::
4545

46-
1. Clone the repository::
46+
1. Clone the repository::
4747

48-
git clone https://github.com/flask-admin/flask-admin.git
49-
cd flask-admin
48+
git clone https://github.com/flask-admin/flask-admin.git
49+
cd flask-admin
5050

51-
2. Create and activate a virtual environment::
51+
2. Create and activate a virtual environment::
5252

53-
virtualenv env -p python3
54-
source env/bin/activate
53+
virtualenv env -p python3
54+
source env/bin/activate
5555

56-
3. Install requirements::
56+
3. Install requirements::
5757

58-
pip install -r 'examples/sqla/requirements.txt'
58+
pip install -r 'examples/sqla/requirements.txt'
5959

60-
4. Run the application::
60+
4. Run the application::
6161

62-
python examples/sqla/app.py
62+
python examples/sqla/app.py
6363

6464
Documentation
6565
-------------

doc/api/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ API
1515
mod_actions
1616

1717
mod_contrib_sqla
18+
mod_contrib_sqla_fields
1819
mod_contrib_mongoengine
1920
mod_contrib_mongoengine_fields
2021
mod_contrib_peewee

doc/api/mod_contrib_sqla_fields.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
``flask_admin.contrib.sqla.fields``
2+
===================================
3+
4+
.. automodule:: flask_admin.contrib.sqla.fields
5+
6+
.. autoclass:: QuerySelectField
7+
:members:
8+
9+
.. autoclass:: QuerySelectMultipleField
10+
:members:
11+
12+
.. autoclass:: CheckboxListField
13+
:members:

doc/changelog.rst

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
Changelog
22
=========
33

4-
next release
4+
1.5.3
55
-----
66

7+
* Fixed XSS vulnerability
78
* Support nested categories in the navbar menu
89
* Fix display of inline x-editable boolean fields on list view
910
* SQLAlchemy
1011
* sort on multiple columns with `column_default_sort`
1112
* sort on related models in `column_sortable_list`
13+
* show searchable fields in search input's placeholder text
1214
* fix: inline model forms can now also be used for models with multiple primary keys
1315
* support for using mapped `column_property`
1416
* Upgrade Leaflet and Leaflet.draw plugins, used for geoalchemy integration
@@ -19,6 +21,7 @@ next release
1921
* handle special characters in filename
2022
* fix a bug with listing directories on Windows
2123
* avoid raising an exception when unknown sort parameter is encountered
24+
* WTForms 3 support
2225

2326
1.5.2
2427
-----

doc/introduction.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@ The first step is to initialize an empty admin interface for your Flask app::
1616
from flask import Flask
1717
from flask_admin import Admin
1818

19+
app = Flask(__name__)
20+
1921
# set optional bootswatch theme
2022
app.config['FLASK_ADMIN_SWATCH'] = 'cerulean'
2123

22-
app = Flask(__name__)
23-
2424
admin = Admin(app, name='microblog', template_mode='bootstrap3')
2525
# Add administrative views here
2626

examples/sqla/app.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -202,10 +202,16 @@ class PostAdmin(sqla.ModelView):
202202
column_labels = dict(title='Post Title') # Rename 'title' column in list view
203203
column_searchable_list = [
204204
'title',
205-
User.first_name,
206-
User.last_name,
207205
'tags.name',
206+
'user.first_name',
207+
'user.last_name',
208208
]
209+
column_labels = {
210+
'title': 'Title',
211+
'tags.name': 'tags',
212+
'user.first_name': 'user\'s first name',
213+
'user.last_name': 'last name',
214+
}
209215
column_filters = [
210216
'user',
211217
'title',

flask_admin/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__version__ = '1.5.2'
1+
__version__ = '1.5.3'
22
__author__ = 'Flask-Admin team'
33
__email__ = '[email protected]'
44

flask_admin/_backwards.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@
88
import sys
99
import warnings
1010

11+
try:
12+
from wtforms.widgets import HTMLString as Markup
13+
except ImportError:
14+
from markupsafe import Markup # noqa: F401
15+
1116

1217
def get_property(obj, name, old_name, default=None):
1318
"""

flask_admin/contrib/mongoengine/view.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,8 @@ class MyModelView(BaseModelView):
140140

141141
allowed_search_types = (mongoengine.StringField,
142142
mongoengine.URLField,
143-
mongoengine.EmailField)
143+
mongoengine.EmailField,
144+
mongoengine.ReferenceField)
144145
"""
145146
List of allowed search field types.
146147
"""
@@ -466,7 +467,12 @@ def _search(self, query, search_term):
466467
criteria = None
467468

468469
for field in self._search_fields:
469-
flt = {'%s__%s' % (field.name, op): term}
470+
if type(field) == mongoengine.ReferenceField:
471+
import re
472+
regex = re.compile('.*%s.*' % term)
473+
else:
474+
regex = term
475+
flt = {'%s__%s' % (field.name, op): regex}
470476
q = mongoengine.Q(**flt)
471477

472478
if criteria is None:

flask_admin/contrib/mongoengine/widgets.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
from wtforms.widgets import HTMLString, html_params
1+
from wtforms.widgets import html_params
22

33
from jinja2 import escape
44

55
from mongoengine.fields import GridFSProxy, ImageGridFsProxy
66

7+
from flask_admin._backwards import Markup
78
from flask_admin.helpers import get_url
89
from . import helpers
910

@@ -31,10 +32,10 @@ def __call__(self, field, **kwargs):
3132
'marker': '_%s-delete' % field.name
3233
}
3334

34-
return HTMLString('%s<input %s>' % (placeholder,
35-
html_params(name=field.name,
36-
type='file',
37-
**kwargs)))
35+
return Markup('%s<input %s>' % (placeholder,
36+
html_params(name=field.name,
37+
type='file',
38+
**kwargs)))
3839

3940

4041
class MongoImageInput(object):
@@ -48,7 +49,6 @@ class MongoImageInput(object):
4849

4950
def __call__(self, field, **kwargs):
5051
kwargs.setdefault('id', field.id)
51-
5252
placeholder = ''
5353
if field.data and isinstance(field.data, ImageGridFsProxy):
5454
args = helpers.make_thumb_args(field.data)
@@ -57,7 +57,7 @@ def __call__(self, field, **kwargs):
5757
'marker': '_%s-delete' % field.name
5858
}
5959

60-
return HTMLString('%s<input %s>' % (placeholder,
61-
html_params(name=field.name,
62-
type='file',
63-
**kwargs)))
60+
return Markup('%s<input %s>' % (placeholder,
61+
html_params(name=field.name,
62+
type='file',
63+
**kwargs)))

flask_admin/contrib/sqla/fields.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
from .tools import get_primary_key
1515
from flask_admin._compat import text_type, string_types, iteritems
16+
from flask_admin.contrib.sqla.widgets import CheckboxListInput
1617
from flask_admin.form import FormOpts, BaseForm, Select2Widget
1718
from flask_admin.model.fields import InlineFieldList, InlineModelFormField
1819
from flask_admin.babel import lazy_gettext
@@ -181,6 +182,30 @@ def pre_validate(self, form):
181182
raise ValidationError(self.gettext(u'Not a valid choice'))
182183

183184

185+
class CheckboxListField(QuerySelectMultipleField):
186+
"""
187+
Alternative field for many-to-many relationships.
188+
189+
Can be used instead of `QuerySelectMultipleField`.
190+
Appears as the list of checkboxes.
191+
Example::
192+
193+
class MyView(ModelView):
194+
form_columns = (
195+
'languages',
196+
)
197+
form_args = {
198+
'languages': {
199+
'query_factory': Language.query,
200+
},
201+
}
202+
form_overrides = {
203+
'languages': CheckboxListField,
204+
}
205+
"""
206+
widget = CheckboxListInput()
207+
208+
184209
class HstoreForm(BaseForm):
185210
""" Form used in InlineFormField/InlineHstoreList for HSTORE columns """
186211
key = StringField(lazy_gettext('Key'))

flask_admin/contrib/sqla/view.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -590,10 +590,10 @@ class MyModelView(BaseModelView):
590590
column_labels = dict(name='Name', last_name='Last Name')
591591
column_searchable_list = ('name', 'last_name')
592592
593-
placeholder is: "Search: Name, Last Name"
593+
placeholder is: "Name, Last Name"
594594
"""
595595
if not self.column_searchable_list:
596-
return 'Search'
596+
return None
597597

598598
placeholders = []
599599

@@ -605,7 +605,7 @@ class MyModelView(BaseModelView):
605605
placeholders.append(
606606
self.column_labels.get(searchable, searchable))
607607

608-
return 'Search: %s' % u', '.join(placeholders)
608+
return u', '.join(placeholders)
609609

610610
def scaffold_filters(self, name):
611611
"""
@@ -824,15 +824,17 @@ def get_query(self):
824824
"""
825825
Return a query for the model type.
826826
827-
If you override this method, don't forget to override `get_count_query` as well.
828-
829827
This method can be used to set a "persistent filter" on an index_view.
830828
831829
Example::
832830
833831
class MyView(ModelView):
834832
def get_query(self):
835833
return super(MyView, self).get_query().filter(User.username == current_user.username)
834+
835+
836+
If you override this method, don't forget to also override `get_count_query`, for displaying the correct
837+
item count in the list view, and `get_one`, which is used when retrieving records for the edit view.
836838
"""
837839
return self.session.query(self.model)
838840

@@ -1073,6 +1075,14 @@ def get_one(self, id):
10731075
"""
10741076
Return a single model by its id.
10751077
1078+
Example::
1079+
1080+
def get_one(self, id):
1081+
query = self.get_query()
1082+
return query.filter(self.model.id == id).one()
1083+
1084+
Also see `get_query` for how to filter the list view.
1085+
10761086
:param id:
10771087
Model id
10781088
"""

flask_admin/contrib/sqla/widgets.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from wtforms.widgets.core import escape
2+
3+
from flask_admin._backwards import Markup
4+
5+
6+
class CheckboxListInput:
7+
"""
8+
Alternative widget for many-to-many relationships.
9+
10+
Appears as the list of checkboxes.
11+
"""
12+
template = (
13+
'<div class="checkbox">'
14+
' <label>'
15+
' <input id="%(id)s" name="%(name)s" value="%(id)s" '
16+
'type="checkbox"%(selected)s>%(label)s'
17+
' </label>'
18+
'</div>'
19+
)
20+
21+
def __call__(self, field, **kwargs):
22+
items = []
23+
for val, label, selected in field.iter_choices():
24+
args = {
25+
'id': val,
26+
'name': field.name,
27+
'label': escape(label),
28+
'selected': ' checked' if selected else '',
29+
}
30+
items.append(self.template % args)
31+
return Markup(''.join(items))

flask_admin/form/upload.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from werkzeug.datastructures import FileStorage
66

77
from wtforms import ValidationError, fields
8-
from wtforms.widgets import HTMLString, html_params
8+
from wtforms.widgets import html_params
99

1010
try:
1111
from wtforms.fields.core import _unset_value as unset_value
@@ -15,6 +15,7 @@
1515
from flask_admin.babel import gettext
1616
from flask_admin.helpers import get_url
1717

18+
from flask_admin._backwards import Markup
1819
from flask_admin._compat import string_types, urljoin
1920

2021

@@ -59,7 +60,7 @@ def __call__(self, field, **kwargs):
5960
else:
6061
value = field.data or ''
6162

62-
return HTMLString(template % {
63+
return Markup(template % {
6364
'text': html_params(type='text',
6465
readonly='readonly',
6566
value=value,
@@ -108,7 +109,7 @@ def __call__(self, field, **kwargs):
108109
else:
109110
template = self.empty_template
110111

111-
return HTMLString(template % args)
112+
return Markup(template % args)
112113

113114
def get_url(self, field):
114115
if field.thumbnail_size:

0 commit comments

Comments
 (0)