Commit 0e632cd8 authored by PJ Janse van Rensburg's avatar PJ Janse van Rensburg

Merge branch 'master' into inline-boolean

parents d0ee1f9e e4d83a91
...@@ -43,23 +43,23 @@ on the existing examples, and submit a *pull-request*. ...@@ -43,23 +43,23 @@ on the existing examples, and submit a *pull-request*.
To run the examples in your local environment:: To run the examples in your local environment::
1. Clone the repository:: 1. Clone the repository::
git clone https://github.com/flask-admin/flask-admin.git git clone https://github.com/flask-admin/flask-admin.git
cd flask-admin cd flask-admin
2. Create and activate a virtual environment:: 2. Create and activate a virtual environment::
virtualenv env -p python3 virtualenv env -p python3
source env/bin/activate source env/bin/activate
3. Install requirements:: 3. Install requirements::
pip install -r 'examples/sqla/requirements.txt' pip install -r 'examples/sqla/requirements.txt'
4. Run the application:: 4. Run the application::
python examples/sqla/app.py python examples/sqla/app.py
Documentation Documentation
------------- -------------
......
...@@ -15,6 +15,7 @@ API ...@@ -15,6 +15,7 @@ API
mod_actions mod_actions
mod_contrib_sqla mod_contrib_sqla
mod_contrib_sqla_fields
mod_contrib_mongoengine mod_contrib_mongoengine
mod_contrib_mongoengine_fields mod_contrib_mongoengine_fields
mod_contrib_peewee mod_contrib_peewee
......
``flask_admin.contrib.sqla.fields``
===================================
.. automodule:: flask_admin.contrib.sqla.fields
.. autoclass:: QuerySelectField
:members:
.. autoclass:: QuerySelectMultipleField
:members:
.. autoclass:: CheckboxListField
:members:
Changelog Changelog
========= =========
next release 1.5.3
----- -----
* Fixed XSS vulnerability
* Support nested categories in the navbar menu * Support nested categories in the navbar menu
* Fix display of inline x-editable boolean fields on list view * Fix display of inline x-editable boolean fields on list view
* SQLAlchemy * SQLAlchemy
* sort on multiple columns with `column_default_sort` * sort on multiple columns with `column_default_sort`
* sort on related models in `column_sortable_list` * sort on related models in `column_sortable_list`
* show searchable fields in search input's placeholder text
* fix: inline model forms can now also be used for models with multiple primary keys * fix: inline model forms can now also be used for models with multiple primary keys
* support for using mapped `column_property` * support for using mapped `column_property`
* Upgrade Leaflet and Leaflet.draw plugins, used for geoalchemy integration * Upgrade Leaflet and Leaflet.draw plugins, used for geoalchemy integration
...@@ -19,6 +21,7 @@ next release ...@@ -19,6 +21,7 @@ next release
* handle special characters in filename * handle special characters in filename
* fix a bug with listing directories on Windows * fix a bug with listing directories on Windows
* avoid raising an exception when unknown sort parameter is encountered * avoid raising an exception when unknown sort parameter is encountered
* WTForms 3 support
1.5.2 1.5.2
----- -----
......
...@@ -16,11 +16,11 @@ The first step is to initialize an empty admin interface for your Flask app:: ...@@ -16,11 +16,11 @@ The first step is to initialize an empty admin interface for your Flask app::
from flask import Flask from flask import Flask
from flask_admin import Admin from flask_admin import Admin
app = Flask(__name__)
# set optional bootswatch theme # set optional bootswatch theme
app.config['FLASK_ADMIN_SWATCH'] = 'cerulean' app.config['FLASK_ADMIN_SWATCH'] = 'cerulean'
app = Flask(__name__)
admin = Admin(app, name='microblog', template_mode='bootstrap3') admin = Admin(app, name='microblog', template_mode='bootstrap3')
# Add administrative views here # Add administrative views here
......
...@@ -202,10 +202,16 @@ class PostAdmin(sqla.ModelView): ...@@ -202,10 +202,16 @@ class PostAdmin(sqla.ModelView):
column_labels = dict(title='Post Title') # Rename 'title' column in list view column_labels = dict(title='Post Title') # Rename 'title' column in list view
column_searchable_list = [ column_searchable_list = [
'title', 'title',
User.first_name,
User.last_name,
'tags.name', 'tags.name',
'user.first_name',
'user.last_name',
] ]
column_labels = {
'title': 'Title',
'tags.name': 'tags',
'user.first_name': 'user\'s first name',
'user.last_name': 'last name',
}
column_filters = [ column_filters = [
'user', 'user',
'title', 'title',
......
__version__ = '1.5.2' __version__ = '1.5.3'
__author__ = 'Flask-Admin team' __author__ = 'Flask-Admin team'
__email__ = 'serge.koval+github@gmail.com' __email__ = 'serge.koval+github@gmail.com'
......
...@@ -8,6 +8,11 @@ ...@@ -8,6 +8,11 @@
import sys import sys
import warnings import warnings
try:
from wtforms.widgets import HTMLString as Markup
except ImportError:
from markupsafe import Markup # noqa: F401
def get_property(obj, name, old_name, default=None): def get_property(obj, name, old_name, default=None):
""" """
......
...@@ -140,7 +140,8 @@ class ModelView(BaseModelView): ...@@ -140,7 +140,8 @@ class ModelView(BaseModelView):
allowed_search_types = (mongoengine.StringField, allowed_search_types = (mongoengine.StringField,
mongoengine.URLField, mongoengine.URLField,
mongoengine.EmailField) mongoengine.EmailField,
mongoengine.ReferenceField)
""" """
List of allowed search field types. List of allowed search field types.
""" """
...@@ -466,7 +467,12 @@ class ModelView(BaseModelView): ...@@ -466,7 +467,12 @@ class ModelView(BaseModelView):
criteria = None criteria = None
for field in self._search_fields: for field in self._search_fields:
flt = {'%s__%s' % (field.name, op): term} if type(field) == mongoengine.ReferenceField:
import re
regex = re.compile('.*%s.*' % term)
else:
regex = term
flt = {'%s__%s' % (field.name, op): regex}
q = mongoengine.Q(**flt) q = mongoengine.Q(**flt)
if criteria is None: if criteria is None:
......
from wtforms.widgets import HTMLString, html_params from wtforms.widgets import html_params
from jinja2 import escape from jinja2 import escape
from mongoengine.fields import GridFSProxy, ImageGridFsProxy from mongoengine.fields import GridFSProxy, ImageGridFsProxy
from flask_admin._backwards import Markup
from flask_admin.helpers import get_url from flask_admin.helpers import get_url
from . import helpers from . import helpers
...@@ -31,10 +32,10 @@ class MongoFileInput(object): ...@@ -31,10 +32,10 @@ class MongoFileInput(object):
'marker': '_%s-delete' % field.name 'marker': '_%s-delete' % field.name
} }
return HTMLString('%s<input %s>' % (placeholder, return Markup('%s<input %s>' % (placeholder,
html_params(name=field.name, html_params(name=field.name,
type='file', type='file',
**kwargs))) **kwargs)))
class MongoImageInput(object): class MongoImageInput(object):
...@@ -48,7 +49,6 @@ class MongoImageInput(object): ...@@ -48,7 +49,6 @@ class MongoImageInput(object):
def __call__(self, field, **kwargs): def __call__(self, field, **kwargs):
kwargs.setdefault('id', field.id) kwargs.setdefault('id', field.id)
placeholder = '' placeholder = ''
if field.data and isinstance(field.data, ImageGridFsProxy): if field.data and isinstance(field.data, ImageGridFsProxy):
args = helpers.make_thumb_args(field.data) args = helpers.make_thumb_args(field.data)
...@@ -57,7 +57,7 @@ class MongoImageInput(object): ...@@ -57,7 +57,7 @@ class MongoImageInput(object):
'marker': '_%s-delete' % field.name 'marker': '_%s-delete' % field.name
} }
return HTMLString('%s<input %s>' % (placeholder, return Markup('%s<input %s>' % (placeholder,
html_params(name=field.name, html_params(name=field.name,
type='file', type='file',
**kwargs))) **kwargs)))
...@@ -13,6 +13,7 @@ except ImportError: ...@@ -13,6 +13,7 @@ except ImportError:
from .tools import get_primary_key from .tools import get_primary_key
from flask_admin._compat import text_type, string_types, iteritems from flask_admin._compat import text_type, string_types, iteritems
from flask_admin.contrib.sqla.widgets import CheckboxListInput
from flask_admin.form import FormOpts, BaseForm, Select2Widget from flask_admin.form import FormOpts, BaseForm, Select2Widget
from flask_admin.model.fields import InlineFieldList, InlineModelFormField from flask_admin.model.fields import InlineFieldList, InlineModelFormField
from flask_admin.babel import lazy_gettext from flask_admin.babel import lazy_gettext
...@@ -181,6 +182,30 @@ class QuerySelectMultipleField(QuerySelectField): ...@@ -181,6 +182,30 @@ class QuerySelectMultipleField(QuerySelectField):
raise ValidationError(self.gettext(u'Not a valid choice')) raise ValidationError(self.gettext(u'Not a valid choice'))
class CheckboxListField(QuerySelectMultipleField):
"""
Alternative field for many-to-many relationships.
Can be used instead of `QuerySelectMultipleField`.
Appears as the list of checkboxes.
Example::
class MyView(ModelView):
form_columns = (
'languages',
)
form_args = {
'languages': {
'query_factory': Language.query,
},
}
form_overrides = {
'languages': CheckboxListField,
}
"""
widget = CheckboxListInput()
class HstoreForm(BaseForm): class HstoreForm(BaseForm):
""" Form used in InlineFormField/InlineHstoreList for HSTORE columns """ """ Form used in InlineFormField/InlineHstoreList for HSTORE columns """
key = StringField(lazy_gettext('Key')) key = StringField(lazy_gettext('Key'))
......
...@@ -590,10 +590,10 @@ class ModelView(BaseModelView): ...@@ -590,10 +590,10 @@ class ModelView(BaseModelView):
column_labels = dict(name='Name', last_name='Last Name') column_labels = dict(name='Name', last_name='Last Name')
column_searchable_list = ('name', 'last_name') column_searchable_list = ('name', 'last_name')
placeholder is: "Search: Name, Last Name" placeholder is: "Name, Last Name"
""" """
if not self.column_searchable_list: if not self.column_searchable_list:
return 'Search' return None
placeholders = [] placeholders = []
...@@ -605,7 +605,7 @@ class ModelView(BaseModelView): ...@@ -605,7 +605,7 @@ class ModelView(BaseModelView):
placeholders.append( placeholders.append(
self.column_labels.get(searchable, searchable)) self.column_labels.get(searchable, searchable))
return 'Search: %s' % u', '.join(placeholders) return u', '.join(placeholders)
def scaffold_filters(self, name): def scaffold_filters(self, name):
""" """
...@@ -824,8 +824,6 @@ class ModelView(BaseModelView): ...@@ -824,8 +824,6 @@ class ModelView(BaseModelView):
""" """
Return a query for the model type. Return a query for the model type.
If you override this method, don't forget to override `get_count_query` as well.
This method can be used to set a "persistent filter" on an index_view. This method can be used to set a "persistent filter" on an index_view.
Example:: Example::
...@@ -833,6 +831,10 @@ class ModelView(BaseModelView): ...@@ -833,6 +831,10 @@ class ModelView(BaseModelView):
class MyView(ModelView): class MyView(ModelView):
def get_query(self): def get_query(self):
return super(MyView, self).get_query().filter(User.username == current_user.username) return super(MyView, self).get_query().filter(User.username == current_user.username)
If you override this method, don't forget to also override `get_count_query`, for displaying the correct
item count in the list view, and `get_one`, which is used when retrieving records for the edit view.
""" """
return self.session.query(self.model) return self.session.query(self.model)
...@@ -1073,6 +1075,14 @@ class ModelView(BaseModelView): ...@@ -1073,6 +1075,14 @@ class ModelView(BaseModelView):
""" """
Return a single model by its id. Return a single model by its id.
Example::
def get_one(self, id):
query = self.get_query()
return query.filter(self.model.id == id).one()
Also see `get_query` for how to filter the list view.
:param id: :param id:
Model id Model id
""" """
......
from wtforms.widgets.core import escape
from flask_admin._backwards import Markup
class CheckboxListInput:
"""
Alternative widget for many-to-many relationships.
Appears as the list of checkboxes.
"""
template = (
'<div class="checkbox">'
' <label>'
' <input id="%(id)s" name="%(name)s" value="%(id)s" '
'type="checkbox"%(selected)s>%(label)s'
' </label>'
'</div>'
)
def __call__(self, field, **kwargs):
items = []
for val, label, selected in field.iter_choices():
args = {
'id': val,
'name': field.name,
'label': escape(label),
'selected': ' checked' if selected else '',
}
items.append(self.template % args)
return Markup(''.join(items))
...@@ -5,7 +5,7 @@ from werkzeug import secure_filename ...@@ -5,7 +5,7 @@ from werkzeug import secure_filename
from werkzeug.datastructures import FileStorage from werkzeug.datastructures import FileStorage
from wtforms import ValidationError, fields from wtforms import ValidationError, fields
from wtforms.widgets import HTMLString, html_params from wtforms.widgets import html_params
try: try:
from wtforms.fields.core import _unset_value as unset_value from wtforms.fields.core import _unset_value as unset_value
...@@ -15,6 +15,7 @@ except ImportError: ...@@ -15,6 +15,7 @@ except ImportError:
from flask_admin.babel import gettext from flask_admin.babel import gettext
from flask_admin.helpers import get_url from flask_admin.helpers import get_url
from flask_admin._backwards import Markup
from flask_admin._compat import string_types, urljoin from flask_admin._compat import string_types, urljoin
...@@ -59,7 +60,7 @@ class FileUploadInput(object): ...@@ -59,7 +60,7 @@ class FileUploadInput(object):
else: else:
value = field.data or '' value = field.data or ''
return HTMLString(template % { return Markup(template % {
'text': html_params(type='text', 'text': html_params(type='text',
readonly='readonly', readonly='readonly',
value=value, value=value,
...@@ -108,7 +109,7 @@ class ImageUploadInput(object): ...@@ -108,7 +109,7 @@ class ImageUploadInput(object):
else: else:
template = self.empty_template template = self.empty_template
return HTMLString(template % args) return Markup(template % args)
def get_url(self, field): def get_url(self, field):
if field.thumbnail_size: if field.thumbnail_size:
......
from re import sub from re import sub, compile
from jinja2 import contextfunction from jinja2 import contextfunction
from flask import g, request, url_for, flash from flask import g, request, url_for, flash
from wtforms.validators import DataRequired, InputRequired from wtforms.validators import DataRequired, InputRequired
...@@ -9,6 +9,8 @@ from ._compat import string_types ...@@ -9,6 +9,8 @@ from ._compat import string_types
VALID_SCHEMES = ['http', 'https'] VALID_SCHEMES = ['http', 'https']
_substitute_whitespace = compile(r'[\s\x00-\x08\x0B\x0C\x0E-\x19]+').sub
_fix_multiple_slashes = compile(r'(^([^/]+:)?//)/*').sub
def set_current_view(view): def set_current_view(view):
...@@ -131,8 +133,18 @@ def prettify_class_name(name): ...@@ -131,8 +133,18 @@ def prettify_class_name(name):
def is_safe_url(target): def is_safe_url(target):
# prevent urls like "\\www.google.com"
# some browser will change \\ to // (eg: Chrome)
# refs https://stackoverflow.com/questions/10438008
target = target.replace('\\', '/')
# handle cases like "j a v a s c r i p t:"
target = _substitute_whitespace('', target)
# Chrome and FireFox "fix" more than two slashes into two after protocol
target = _fix_multiple_slashes(lambda m: m.group(1), target, 1)
# prevent urls starting with "javascript:" # prevent urls starting with "javascript:"
target = target.strip()
target_info = urlparse(target) target_info = urlparse(target)
target_scheme = target_info.scheme target_scheme = target_info.scheme
if target_scheme and target_scheme not in VALID_SCHEMES: if target_scheme and target_scheme not in VALID_SCHEMES:
......
...@@ -1109,9 +1109,9 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -1109,9 +1109,9 @@ class BaseModelView(BaseView, ActionsMixin):
def search_placeholder(self): def search_placeholder(self):
""" """
Return search placeholder. Return search placeholder text.
""" """
return 'Search' return None
# Filter helpers # Filter helpers
def scaffold_filters(self, name): def scaffold_filters(self, name):
......
from flask import json from flask import json
from jinja2 import escape from jinja2 import escape
from wtforms.widgets import HTMLString, html_params from wtforms.widgets import html_params
from flask_admin._backwards import Markup
from flask_admin._compat import as_unicode, text_type from flask_admin._compat import as_unicode, text_type
from flask_admin.babel import gettext from flask_admin.babel import gettext
from flask_admin.helpers import get_url from flask_admin.helpers import get_url
...@@ -64,7 +65,7 @@ class AjaxSelect2Widget(object): ...@@ -64,7 +65,7 @@ class AjaxSelect2Widget(object):
minimum_input_length = int(field.loader.options.get('minimum_input_length', 1)) minimum_input_length = int(field.loader.options.get('minimum_input_length', 1))
kwargs.setdefault('data-minimum-input-length', minimum_input_length) kwargs.setdefault('data-minimum-input-length', minimum_input_length)
return HTMLString('<input %s>' % html_params(name=field.name, **kwargs)) return Markup('<input %s>' % html_params(name=field.name, **kwargs))
class XEditableWidget(object): class XEditableWidget(object):
...@@ -93,7 +94,7 @@ class XEditableWidget(object): ...@@ -93,7 +94,7 @@ class XEditableWidget(object):
kwargs = self.get_kwargs(field, kwargs) kwargs = self.get_kwargs(field, kwargs)
return HTMLString( return Markup(
'<a %s>%s</a>' % (html_params(**kwargs), '<a %s>%s</a>' % (html_params(**kwargs),
escape(display_value)) escape(display_value))
) )
......
...@@ -143,3 +143,23 @@ table.filters tr td { ...@@ -143,3 +143,23 @@ table.filters tr td {
.editable-input .select2-container { .editable-input .select2-container {
min-width: 220px; min-width: 220px;
} }
[placeholder]{
text-overflow:ellipsis;
}
::-webkit-input-placeholder { /* Chrome/Opera/Safari */
text-overflow:ellipsis;
}
::-moz-placeholder { /* Firefox 19+ */
text-overflow:ellipsis;
}
:-ms-input-placeholder { /* IE 10+ */
text-overflow:ellipsis;
}
:-moz-placeholder { /* Firefox 18- */
text-overflow:ellipsis;
}
...@@ -108,3 +108,23 @@ body.modal-open { ...@@ -108,3 +108,23 @@ body.modal-open {
{ {
overflow-x: auto; overflow-x: auto;
} }
[placeholder]{
text-overflow:ellipsis;
}
::-webkit-input-placeholder { /* Chrome/Opera/Safari */
text-overflow:ellipsis;
}
::-moz-placeholder { /* Firefox 19+ */
text-overflow:ellipsis;
}
:-ms-input-placeholder { /* IE 10+ */
text-overflow:ellipsis;
}
:-moz-placeholder { /* Firefox 18- */
text-overflow:ellipsis;
}
(function() {
window.faHelpers = {
// A simple confirm() wrapper
safeConfirm: function(msg) {
try {
return confirm(msg) ? true : false;
} catch (e) {
return false;
}
}
};
})();
...@@ -84,7 +84,11 @@ ...@@ -84,7 +84,11 @@
{% if name != '..' and admin_view.can_delete_dirs %} {% if name != '..' and admin_view.can_delete_dirs %}
<form class="icon" method="POST" action="{{ get_url('.delete') }}"> <form class="icon" method="POST" action="{{ get_url('.delete') }}">
{{ delete_form.path(value=path) }} {{ delete_form.path(value=path) }}
{{ delete_form.csrf_token }} {% if delete_form.csrf_token %}
{{ delete_form.csrf_token }}
{% elif csrf_token %}
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
{% endif %}
<button onclick="return confirm('{{ _gettext('Are you sure you want to delete \\\'%(name)s\\\' recursively?', name=name) }}')"> <button onclick="return confirm('{{ _gettext('Are you sure you want to delete \\\'%(name)s\\\' recursively?', name=name) }}')">
<i class="fa fa-times icon-remove"></i> <i class="fa fa-times icon-remove"></i>
</button> </button>
...@@ -93,7 +97,11 @@ ...@@ -93,7 +97,11 @@
{% else %} {% else %}
<form class="icon" method="POST" action="{{ get_url('.delete') }}"> <form class="icon" method="POST" action="{{ get_url('.delete') }}">
{{ delete_form.path(value=path) }} {{ delete_form.path(value=path) }}
{{ delete_form.csrf_token }} {% if delete_form.csrf_token %}
{{ delete_form.csrf_token }}
{% elif csrf_token %}
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
{% endif %}
<button onclick="return confirm('{{ _gettext('Are you sure you want to delete \\\'%(name)s\\\'?', name=name) }}')"> <button onclick="return confirm('{{ _gettext('Are you sure you want to delete \\\'%(name)s\\\'?', name=name) }}')">
<i class="fa fa-times icon-remove"></i> <i class="fa fa-times icon-remove"></i>
</button> </button>
......
...@@ -58,7 +58,7 @@ ...@@ -58,7 +58,7 @@
<div class="clearfix"></div> <div class="clearfix"></div>
{% endmacro %} {% endmacro %}
{% macro search_form(input_class="span2") %} {% macro search_form(input_class=None) %}
<form method="GET" action="{{ return_url }}" class="search-form"> <form method="GET" action="{{ return_url }}" class="search-form">
{% for flt_name, flt_value in filter_args.items() %} {% for flt_name, flt_value in filter_args.items() %}
<input type="hidden" name="{{ flt_name }}" value="{{ flt_value }}"> <input type="hidden" name="{{ flt_name }}" value="{{ flt_value }}">
...@@ -72,17 +72,17 @@ ...@@ -72,17 +72,17 @@
{% if sort_desc %} {% if sort_desc %}
<input type="hidden" name="desc" value="{{ sort_desc }}"> <input type="hidden" name="desc" value="{{ sort_desc }}">
{% endif %} {% endif %}
{%- set full_search_placeholder = _gettext('Search') %}
{%- if search_placeholder %}{% set full_search_placeholder = [full_search_placeholder, search_placeholder] | join(": ") %}{% endif %}
{% if search %} {% if search %}
<div class="input-append"> <div class="input-append">
<input type="text" name="search" value="{{ search }}" class="{{ input_class }}" placeholder="{{ _gettext('%(placeholder)s', placeholder=search_placeholder) }}"> <input type="search" name="search" class="input-xlarge{% if input_class %} {{ input_class }}{% endif %}" value="{{ search }}" placeholder="{{ full_search_placeholder }}">
<a href="{{ clear_search_url }}" class="clear add-on"> <a href="{{ clear_search_url }}" class="clear add-on">
<i class="fa fa-times icon-remove"></i> <i class="fa fa-times icon-remove"></i>
</a> </a>
</div> </div>
{% else %} {% else %}
<div> <input type="search" name="search" class="input-xlarge{% if input_class %} {{ input_class }}{% endif %}" value="" placeholder="{{ full_search_placeholder }}">
<input type="text" name="search" value="" class="{{ input_class }}" placeholder="{{ _gettext('%(placeholder)s', placeholder=search_placeholder) }}">
</div>
{% endif %} {% endif %}
</form> </form>
{% endmacro %} {% endmacro %}
......
...@@ -185,8 +185,8 @@ ...@@ -185,8 +185,8 @@
<div id="active-filters-data" style="display:none;">{{ active_filters|tojson|safe }}</div> <div id="active-filters-data" style="display:none;">{{ active_filters|tojson|safe }}</div>
{% endif %} {% endif %}
<script src="{{ admin_static.url(filename='admin/js/filters.js', v='1.0.0') }}"></script>
{{ lib.form_js() }} {{ lib.form_js() }}
<script src="{{ admin_static.url(filename='admin/js/filters.js', v='1.0.0') }}"></script>
{{ actionlib.script(_gettext('Please select at least one record.'), {{ actionlib.script(_gettext('Please select at least one record.'),
actions, actions,
......
...@@ -84,6 +84,7 @@ ...@@ -84,6 +84,7 @@
<script src="{{ admin_static.url(filename='bootstrap/bootstrap3/js/bootstrap.min.js', v='3.3.5') }}" type="text/javascript"></script> <script src="{{ admin_static.url(filename='bootstrap/bootstrap3/js/bootstrap.min.js', v='3.3.5') }}" type="text/javascript"></script>
<script src="{{ admin_static.url(filename='vendor/moment.min.js', v='2.9.0') }}" type="text/javascript"></script> <script src="{{ admin_static.url(filename='vendor/moment.min.js', v='2.9.0') }}" type="text/javascript"></script>
<script src="{{ admin_static.url(filename='vendor/select2/select2.min.js', v='3.5.2') }}" type="text/javascript"></script> <script src="{{ admin_static.url(filename='vendor/select2/select2.min.js', v='3.5.2') }}" type="text/javascript"></script>
<script src="{{ admin_static.url(filename='admin/js/helpers.js', v='1.0.0') }}" type="text/javascript"></script>
{% if admin_view.extra_js %} {% if admin_view.extra_js %}
{% for js_url in admin_view.extra_js %} {% for js_url in admin_view.extra_js %}
<script src="{{ js_url }}" type="text/javascript"></script> <script src="{{ js_url }}" type="text/javascript"></script>
......
...@@ -84,7 +84,11 @@ ...@@ -84,7 +84,11 @@
{% if name != '..' and admin_view.can_delete_dirs %} {% if name != '..' and admin_view.can_delete_dirs %}
<form class="icon" method="POST" action="{{ get_url('.delete') }}"> <form class="icon" method="POST" action="{{ get_url('.delete') }}">
{{ delete_form.path(value=path) }} {{ delete_form.path(value=path) }}
{{ delete_form.csrf_token }} {% if delete_form.csrf_token %}
{{ delete_form.csrf_token }}
{% elif csrf_token %}
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
{% endif %}
<button onclick="return confirm('{{ _gettext('Are you sure you want to delete \\\'%(name)s\\\' recursively?', name=name) }}')"> <button onclick="return confirm('{{ _gettext('Are you sure you want to delete \\\'%(name)s\\\' recursively?', name=name) }}')">
<i class="fa fa-times glyphicon glyphicon-remove"></i> <i class="fa fa-times glyphicon glyphicon-remove"></i>
</button> </button>
...@@ -93,7 +97,11 @@ ...@@ -93,7 +97,11 @@
{% else %} {% else %}
<form class="icon" method="POST" action="{{ get_url('.delete') }}"> <form class="icon" method="POST" action="{{ get_url('.delete') }}">
{{ delete_form.path(value=path) }} {{ delete_form.path(value=path) }}
{{ delete_form.csrf_token }} {% if delete_form.csrf_token %}
{{ delete_form.csrf_token }}
{% elif csrf_token %}
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
{% endif %}
<button onclick="return confirm('{{ _gettext('Are you sure you want to delete \\\'%(name)s\\\'?', name=name) }}')"> <button onclick="return confirm('{{ _gettext('Are you sure you want to delete \\\'%(name)s\\\'?', name=name) }}')">
<i class="fa fa-trash glyphicon glyphicon-trash"></i> <i class="fa fa-trash glyphicon glyphicon-trash"></i>
</button> </button>
......
...@@ -58,7 +58,7 @@ ...@@ -58,7 +58,7 @@
<div class="clearfix"></div> <div class="clearfix"></div>
{% endmacro %} {% endmacro %}
{% macro search_form(input_class="col-md-2") %} {% macro search_form(input_class=None) %}
<form method="GET" action="{{ return_url }}" class="navbar-form navbar-left" role="search"> <form method="GET" action="{{ return_url }}" class="navbar-form navbar-left" role="search">
{% for flt_name, flt_value in filter_args.items() %} {% for flt_name, flt_value in filter_args.items() %}
<input type="hidden" name="{{ flt_name }}" value="{{ flt_value }}"> <input type="hidden" name="{{ flt_name }}" value="{{ flt_value }}">
...@@ -72,14 +72,18 @@ ...@@ -72,14 +72,18 @@
{% if sort_desc %} {% if sort_desc %}
<input type="hidden" name="desc" value="{{ sort_desc }}"> <input type="hidden" name="desc" value="{{ sort_desc }}">
{% endif %} {% endif %}
{%- set full_search_placeholder = _gettext('Search') %}
{%- set max_size = config.get('FLASK_ADMIN_SEARCH_SIZE_MAX', 100) %}
{%- if search_placeholder %}{% set full_search_placeholder = [full_search_placeholder, search_placeholder] | join(": ") %}{% endif %}
{%- set input_size = [[full_search_placeholder | length, 30] | max, max_size] | min %}
{% if search %} {% if search %}
<div class="input-group"> <div class="input-group">
<input type="text" name="search" value="{{ search }}" class="{{ input_class }} form-control" placeholder="{{ _gettext('%(placeholder)s', placeholder=search_placeholder) }}"> <input type="search" name="search" value="{{ search }}" class="form-control{% if input_class %} {{ input_class }}{% endif %}" size="{{ input_size }}" placeholder="{{ full_search_placeholder }}">
<a href="{{ clear_search_url }}" class="input-group-addon clear"><span class="fa fa-times glyphicon glyphicon-remove"></span></a> <a href="{{ clear_search_url }}" class="input-group-addon clear"><span class="fa fa-times glyphicon glyphicon-remove"></span></a>
</div> </div>
{% else %} {% else %}
<div class="form-group"> <div class="form-group">
<input type="text" name="search" value="" class="{{ input_class }} form-control" placeholder="{{ _gettext('%(placeholder)s', placeholder=search_placeholder) }}"> <input type="search" name="search" value="" class="form-control{% if input_class %} {{ input_class }}{% endif %}" size="{{ input_size }}" placeholder="{{ full_search_placeholder }}">
</div> </div>
{% endif %} {% endif %}
</form> </form>
......
...@@ -186,8 +186,8 @@ ...@@ -186,8 +186,8 @@
<div id="active-filters-data" style="display:none;">{{ active_filters|tojson|safe }}</div> <div id="active-filters-data" style="display:none;">{{ active_filters|tojson|safe }}</div>
{% endif %} {% endif %}
<script src="{{ admin_static.url(filename='admin/js/filters.js', v='1.0.0') }}"></script>
{{ lib.form_js() }} {{ lib.form_js() }}
<script src="{{ admin_static.url(filename='admin/js/filters.js', v='1.0.0') }}"></script>
{{ actionlib.script(_gettext('Please select at least one record.'), {{ actionlib.script(_gettext('Please select at least one record.'),
actions, actions,
......
...@@ -31,7 +31,7 @@ ...@@ -31,7 +31,7 @@
{% elif csrf_token %} {% elif csrf_token %}
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
{% endif %} {% endif %}
<button onclick="return safeConfirm('{{ _gettext('Are you sure you want to delete this record?') }}');" title="{{ _gettext('Delete record') }}"> <button onclick="return faHelpers.safeConfirm('{{ _gettext('Are you sure you want to delete this record?') }}');" title="{{ _gettext('Delete record') }}">
<span class="fa fa-trash glyphicon glyphicon-trash"></span> <span class="fa fa-trash glyphicon glyphicon-trash"></span>
</button> </button>
</form> </form>
......
...@@ -11,7 +11,14 @@ def test_is_safe_url(): ...@@ -11,7 +11,14 @@ def test_is_safe_url():
assert helpers.is_safe_url('https://127.0.0.1/admin/car/') assert helpers.is_safe_url('https://127.0.0.1/admin/car/')
assert helpers.is_safe_url('/admin/car/') assert helpers.is_safe_url('/admin/car/')
assert helpers.is_safe_url('admin/car/') assert helpers.is_safe_url('admin/car/')
assert helpers.is_safe_url('http////www.google.com')
assert not helpers.is_safe_url('http://127.0.0.2/admin/car/') assert not helpers.is_safe_url('http://127.0.0.2/admin/car/')
assert not helpers.is_safe_url(' javascript:alert(document.domain)') assert not helpers.is_safe_url(' javascript:alert(document.domain)')
assert not helpers.is_safe_url('javascript:alert(document.domain)') assert not helpers.is_safe_url('javascript:alert(document.domain)')
assert not helpers.is_safe_url('javascrip\nt:alert(document.domain)')
assert not helpers.is_safe_url(r'\\www.google.com')
assert not helpers.is_safe_url(r'\\/www.google.com')
assert not helpers.is_safe_url('/////www.google.com')
assert not helpers.is_safe_url('http:///www.google.com')
assert not helpers.is_safe_url('https:////www.google.com')
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