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

Merge pull request #711 from pawl/inlist_filter3

add 'in list' and 'empty' filters, allow changing to filters with options, add js versions, fix bootstrap3+select
parents ec0f1054 5fb50407
Custom filter with SQLAlchemy backend example.
To run this example:
1. Clone the repository::
git clone https://github.com/mrjoes/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/custom-filter/requirements.txt'
4. Run the application::
python examples/custom-filter/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.ext.sqlalchemy import SQLAlchemy
from flask.ext.admin.contrib import sqla
from flask.ext.admin import expose, Admin
# required for creating custom filters
from flask.ext.admin.contrib.sqla.filters import BaseSQLAFilter, FilterEqual
# 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['DATABASE_FILE'] = 'sample_db.sqlite'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + app.config['DATABASE_FILE']
app.config['SQLALCHEMY_ECHO'] = True
db = SQLAlchemy(app)
# Create model
class User(db.Model):
def __init__(self, first_name, last_name, username, email):
self.first_name = first_name
self.last_name = last_name
self.username = username
self.email = email
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)
# Required for administrative interface. For python 3 please use __str__ instead.
def __unicode__(self):
return self.username
# Create custom filter class
class FilterLastNameBrown(BaseSQLAFilter):
def apply(self, query, value):
if value == '1':
return query.filter(self.column == "Brown")
else:
return query.filter(self.column != "Brown")
def operation(self):
return 'is Brown'
# Add custom filter and standard FilterEqual to ModelView
class UserAdmin(sqla.ModelView):
# each filter in the list is a filter operation (equals, not equals, etc)
# filters with the same name will appear as operations under the same filter
column_filters = [
FilterEqual(User.last_name, 'Last Name'),
FilterLastNameBrown(User.last_name, 'Last Name', options=(('1', 'Yes'),('0', 'No')))
]
admin = Admin(app, template_mode="bootstrap3")
admin.add_view(UserAdmin(User, db.session))
def build_sample_db():
db.drop_all()
db.create_all()
user_obj1 = User("Paul", "Brown", "pbrown", "paul@gmail.com")
user_obj2 = User("Luke", "Brown", "lbrown", "luke@gmail.com")
user_obj3 = User("Serge", "Koval", "skoval", "serge@gmail.com")
db.session.add_all([user_obj1, user_obj2, user_obj3])
db.session.commit()
if __name__ == '__main__':
build_sample_db()
app.run(port=5000)
\ No newline at end of file
Flask
Flask-Admin
Flask-SQLAlchemy
......@@ -5,7 +5,7 @@ import datetime
from flask.ext.admin.babel import lazy_gettext
from flask.ext.admin.model import filters
from flask.ext.admin.contrib.sqla import tools
from sqlalchemy.sql import not_
from sqlalchemy.sql import not_, or_
class BaseSQLAFilter(filters.BaseFilter):
"""
......@@ -80,6 +80,40 @@ class FilterSmaller(BaseSQLAFilter):
return lazy_gettext('smaller than')
class FilterEmpty(BaseSQLAFilter, filters.BaseBooleanFilter):
def apply(self, query, value):
if value == '1':
return query.filter(self.column == None)
else:
return query.filter(self.column != None)
def operation(self):
return lazy_gettext('empty')
class FilterInList(BaseSQLAFilter):
def __init__(self, column, name, options=None, data_type=None):
super(FilterInList, self).__init__(column, name, options, data_type='select2-tags')
def clean(self, value):
return [v.strip() for v in value.split(',') if v.strip()]
def apply(self, query, value):
return query.filter(self.column.in_(value))
def operation(self):
return lazy_gettext('in list')
class FilterNotInList(FilterInList):
def apply(self, query, value):
# NOT IN can exclude NULL values, so "or_ == None" needed to be added
return query.filter(or_(~self.column.in_(value), self.column == None))
def operation(self):
return lazy_gettext('not in list')
# Customized type filters
class BooleanEqualFilter(FilterEqual, filters.BaseBooleanFilter):
pass
......@@ -88,7 +122,7 @@ class BooleanEqualFilter(FilterEqual, filters.BaseBooleanFilter):
class BooleanNotEqualFilter(FilterNotEqual, filters.BaseBooleanFilter):
pass
class DateEqualFilter(FilterEqual, filters.BaseDateFilter):
pass
......@@ -106,6 +140,9 @@ class DateSmallerFilter(FilterSmaller, filters.BaseDateFilter):
class DateBetweenFilter(BaseSQLAFilter):
def __init__(self, column, name, options=None, data_type=None):
super(DateBetweenFilter, self).__init__(column, name, options, data_type='daterangepicker')
def clean(self, value):
return [datetime.datetime.strptime(range, '%Y-%m-%d') for range in value.split(' to ')]
......@@ -156,6 +193,9 @@ class DateTimeSmallerFilter(FilterSmaller, filters.BaseDateTimeFilter):
class DateTimeBetweenFilter(BaseSQLAFilter):
def __init__(self, column, name, options=None, data_type=None):
super(DateTimeBetweenFilter, self).__init__(column, name, options, data_type='datetimerangepicker')
def clean(self, value):
return [datetime.datetime.strptime(range, '%Y-%m-%d %H:%M:%S') for range in value.split(' to ')]
......@@ -175,7 +215,7 @@ class DateTimeBetweenFilter(BaseSQLAFilter):
return False
except ValueError:
return False
class DateTimeNotBetweenFilter(DateTimeBetweenFilter):
def apply(self, query, value):
......@@ -188,21 +228,24 @@ class DateTimeNotBetweenFilter(DateTimeBetweenFilter):
class TimeEqualFilter(FilterEqual, filters.BaseTimeFilter):
pass
class TimeNotEqualFilter(FilterNotEqual, filters.BaseTimeFilter):
pass
class TimeGreaterFilter(FilterGreater, filters.BaseTimeFilter):
pass
class TimeSmallerFilter(FilterSmaller, filters.BaseTimeFilter):
pass
class TimeBetweenFilter(BaseSQLAFilter):
def __init__(self, column, name, options=None, data_type=None):
super(TimeBetweenFilter, self).__init__(column, name, options, data_type='timerangepicker')
def clean(self, value):
timetuples = [time.strptime(range, '%H:%M:%S')
for range in value.split(' to ')]
......@@ -210,7 +253,7 @@ class TimeBetweenFilter(BaseSQLAFilter):
timetuple.tm_min,
timetuple.tm_sec)
for timetuple in timetuples]
def apply(self, query, value):
start, end = value
return query.filter(self.column.between(start, end))
......@@ -242,55 +285,51 @@ class TimeNotBetweenFilter(TimeBetweenFilter):
# Base SQLA filter field converter
class FilterConverter(filters.BaseFilterConverter):
strings = (FilterEqual, FilterNotEqual, FilterLike, FilterNotLike)
numeric = (FilterEqual, FilterNotEqual, FilterGreater, FilterSmaller)
strings = (FilterEqual, FilterNotEqual, FilterLike, FilterNotLike, FilterEmpty, FilterInList, FilterNotInList)
numeric = (FilterEqual, FilterNotEqual, FilterGreater, FilterSmaller, FilterEmpty, FilterInList, FilterNotInList)
bool = (BooleanEqualFilter, BooleanNotEqualFilter)
enum = (FilterEqual, FilterNotEqual)
enum = (FilterEqual, FilterNotEqual, FilterEmpty, FilterInList, FilterNotInList)
date_filters = (DateEqualFilter, DateNotEqualFilter, DateGreaterFilter, DateSmallerFilter,
DateBetweenFilter, DateNotBetweenFilter, FilterEmpty)
datetime_filters = (DateTimeEqualFilter, DateTimeNotEqualFilter, DateTimeGreaterFilter,
DateTimeSmallerFilter, DateTimeBetweenFilter, DateTimeNotBetweenFilter,
FilterEmpty)
time_filters = (TimeEqualFilter, TimeNotEqualFilter, TimeGreaterFilter, TimeSmallerFilter,
TimeBetweenFilter, TimeNotBetweenFilter, FilterEmpty)
def convert(self, type_name, column, name, **kwargs):
if type_name.lower() in self.converters:
return self.converters[type_name.lower()](column, name, **kwargs)
return None
@filters.convert('string', 'unicode', 'text', 'unicodetext', 'varchar')
@filters.convert('string', 'char', 'unicode', 'varchar', 'tinytext',
'text', 'mediumtext', 'longtext', 'unicodetext',
'nchar', 'nvarchar', 'ntext')
def conv_string(self, column, name, **kwargs):
return [f(column, name, **kwargs) for f in self.strings]
@filters.convert('boolean', 'tinyint')
def conv_bool(self, column, name, **kwargs):
return [f(column, name, **kwargs) for f in self.bool]
@filters.convert('integer', 'smallinteger', 'numeric', 'float', 'biginteger')
@filters.convert('int', 'integer', 'smallinteger', 'smallint', 'numeric',
'float', 'real', 'biginteger', 'bigint', 'decimal',
'double_precision', 'double', 'mediumint')
def conv_int(self, column, name, **kwargs):
return [f(column, name, **kwargs) for f in self.numeric]
@filters.convert('date')
def conv_date(self, column, name, **kwargs):
return [DateEqualFilter(column, name),
DateNotEqualFilter(column, name),
DateGreaterFilter(column, name),
DateSmallerFilter(column, name),
DateBetweenFilter(column, name, data_type='daterangepicker'),
DateNotBetweenFilter(column, name, data_type='daterangepicker')]
@filters.convert('datetime')
return [f(column, name, **kwargs) for f in self.date_filters]
@filters.convert('datetime', 'datetime2', 'timestamp', 'smalldatetime')
def conv_datetime(self, column, name, **kwargs):
return [DateTimeEqualFilter(column, name),
DateTimeNotEqualFilter(column, name),
DateTimeGreaterFilter(column, name),
DateTimeSmallerFilter(column, name),
DateTimeBetweenFilter(column, name, data_type='datetimerangepicker'),
DateTimeNotBetweenFilter(column, name, data_type='datetimerangepicker')]
return [f(column, name, **kwargs) for f in self.datetime_filters]
@filters.convert('time')
def conv_time(self, column, name, **kwargs):
return [TimeEqualFilter(column, name),
TimeNotEqualFilter(column, name),
TimeGreaterFilter(column, name),
TimeSmallerFilter(column, name),
TimeBetweenFilter(column, name, data_type='timerangepicker'),
TimeNotBetweenFilter(column, name, data_type='timerangepicker')]
return [f(column, name, **kwargs) for f in self.time_filters]
@filters.convert('enum')
def conv_enum(self, column, name, options=None, **kwargs):
if not options:
......
......@@ -93,7 +93,7 @@ class Select2Field(fields.SelectField):
"""
`Select2 <https://github.com/ivaynberg/select2>`_ styled select widget.
You must include select2.js, form.js and select2 stylesheet for it to
You must include select2.js, form-x.x.x.js and select2 stylesheet for it to
work.
"""
widget = admin_widgets.Select2Widget()
......@@ -141,7 +141,7 @@ class Select2Field(fields.SelectField):
class Select2TagsField(fields.StringField):
"""`Select2 <http://ivaynberg.github.com/select2/#tags>`_ styled text field.
You must include select2.js, form.js and select2 stylesheet for it to work.
You must include select2.js, form-x.x.x.js and select2 stylesheet for it to work.
"""
widget = admin_widgets.Select2TagsWidget()
......
......@@ -15,7 +15,7 @@ class Select2Widget(widgets.Select):
"""
`Select2 <https://github.com/ivaynberg/select2>`_ styled select widget.
You must include select2.js, form.js and select2 stylesheet for it to
You must include select2.js, form-x.x.x.js and select2 stylesheet for it to
work.
"""
def __call__(self, field, **kwargs):
......@@ -30,7 +30,7 @@ class Select2Widget(widgets.Select):
class Select2TagsWidget(widgets.TextInput):
"""`Select2 <http://ivaynberg.github.com/select2/#tags>`_ styled text widget.
You must include select2.js, form.js and select2 stylesheet for it to work.
You must include select2.js, form-x.x.x.js and select2 stylesheet for it to work.
"""
def __call__(self, field, **kwargs):
kwargs.setdefault('data-role', u'select2')
......@@ -43,7 +43,7 @@ class DatePickerWidget(widgets.TextInput):
"""
Date picker widget.
You must include bootstrap-datepicker.js and form.js for styling to work.
You must include bootstrap-datepicker.js and form-x.x.x.js for styling to work.
"""
def __call__(self, field, **kwargs):
kwargs.setdefault('data-role', u'datepicker')
......@@ -57,7 +57,7 @@ class DateTimePickerWidget(widgets.TextInput):
"""
Datetime picker widget.
You must include bootstrap-datepicker.js and form.js for styling to work.
You must include bootstrap-datepicker.js and form-x.x.x.js for styling to work.
"""
def __call__(self, field, **kwargs):
kwargs.setdefault('data-role', u'datetimepicker')
......@@ -69,7 +69,7 @@ class TimePickerWidget(widgets.TextInput):
"""
Date picker widget.
You must include bootstrap-datepicker.js and form.js for styling to work.
You must include bootstrap-datepicker.js and form-x.x.x.js for styling to work.
"""
def __call__(self, field, **kwargs):
kwargs.setdefault('data-role', u'timepicker')
......
......@@ -112,7 +112,7 @@ class BaseDateFilter(BaseFilter):
super(BaseDateFilter, self).__init__(name,
options,
data_type='datepicker')
def clean(self, value):
return datetime.datetime.strptime(value, '%Y-%m-%d').date()
......@@ -139,7 +139,7 @@ class BaseTimeFilter(BaseFilter):
super(BaseTimeFilter, self).__init__(name,
options,
data_type='timepicker')
def clean(self, value):
# time filters will not work in SQLite + SQLAlchemy if value not converted to time
timetuple = time.strptime(value, '%H:%M:%S')
......
......@@ -60,14 +60,6 @@ table.flters tr td:nth-child(2){
width: 60%;
}
.filters input, .filters div.select2-container {
margin-bottom: 0px;
}
.filters div.select2-container {
width: 100% !important;
}
.filters a.remove-filter {
margin-bottom: 0;
display: block;
......@@ -91,9 +83,13 @@ table.flters tr td:nth-child(2){
opacity: 0.4;
}
.filters .filter-op > a {
height: 28px;
line-height: 28px;
/* filters */
.filters .filter-op {
width: 130px;
}
.filters .filter-val {
width: 220px;
}
/* Inline forms */
......@@ -139,26 +135,6 @@ td>span.glyphicon {
padding-left: 35%;
}
/* Patch Select2 */
.select2-results li {
min-height: 24px !important;
}
.select2-container.form-control {
height: auto !important;
padding: 0px;
border-width: 0px;
}
form .select2-choice {
height: 30px !important;
}
.select2-search-choice {
height: 24px !important;
margin-top: 4px !important;
}
/* link style */
a.btn-cancel {
border-radius: 4px;
......
......@@ -259,6 +259,39 @@
$el.select2(opts);
return true;
case 'select2-tags':
// get tags from element
if ($el.attr('data-tags')) {
var tags = JSON.parse($el.attr('data-tags'));
} else {
var tags = [];
}
// default to a comma for separating list items
// allows using spaces as a token separator
if ($el.attr('data-token-separators')) {
var tokenSeparators = JSON.parse($el.attr('data-tags'));
} else {
var tokenSeparators = [','];
}
var opts = {
width: 'resolve',
tags: tags,
tokenSeparators: tokenSeparators,
formatNoMatches: function() {
return 'Enter comma separated values';
}
};
$el.select2(opts);
// submit on ENTER
$el.parent().find('input.select2-input').on('keyup', function(e) {
if(e.keyCode === 13)
$(this).closest('form').submit();
});
return true;
case 'select2-ajax':
processAjaxWidget($el, name);
return true;
......
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -226,4 +226,4 @@
<glyph unicode="&#xe199;" d="M100 200h400v-155l-75 -45h350l-75 45v155h400l-270 300h170l-270 300h170l-300 333l-300 -333h170l-270 -300h170z" />
<glyph unicode="&#xe200;" d="M121 700q0 -53 28.5 -97t75.5 -65q-4 -16 -4 -38q0 -74 52.5 -126.5t126.5 -52.5q56 0 100 30v-306l-75 -45h350l-75 45v306q46 -30 100 -30q74 0 126.5 52.5t52.5 126.5q0 24 -9 55q50 32 79.5 83t29.5 112q0 90 -61.5 155.5t-150.5 71.5q-26 89 -99.5 145.5 t-167.5 56.5q-116 0 -197.5 -81.5t-81.5 -197.5q0 -4 1 -11.5t1 -11.5q-14 2 -23 2q-74 0 -126.5 -52.5t-52.5 -126.5z" />
</font>
</defs></svg>
\ No newline at end of file
</defs></svg>
\ No newline at end of file
This diff is collapsed.
......@@ -24,7 +24,7 @@
{% macro script(message, actions, actions_confirmation) %}
{% if actions %}
<script src="{{ admin_static.url(filename='admin/js/actions.js') }}"></script>
<script src="{{ admin_static.url(filename='admin/js/actions-1.0.0.js') }}"></script>
<script language="javascript">
var modelActions = new AdminModelActions({{ message|tojson|safe }}, {{ actions_confirmation|tojson|safe }});
</script>
......
......@@ -189,5 +189,5 @@
<script src="{{ admin_static.url(filename='vendor/leaflet/leaflet.draw.js') }}"></script>
{% endif %}
<script src="{{ admin_static.url(filename='vendor/bootstrap-daterangepicker/daterangepicker.js') }}"></script>
<script src="{{ admin_static.url(filename='admin/js/form.js') }}"></script>
<script src="{{ admin_static.url(filename='admin/js/form-1.0.0.js') }}"></script>
{% endmacro %}
......@@ -145,7 +145,7 @@
{% block tail %}
{{ super() }}
{{ lib.form_js() }}
<script src="{{ admin_static.url(filename='admin/js/filters.js') }}"></script>
<script src="{{ admin_static.url(filename='admin/js/filters-1.0.0.js') }}"></script>
{{ actionlib.script(_gettext('Please select at least one record.'),
actions,
......
......@@ -21,7 +21,7 @@
{% block tail %}
{{ super() }}
<script src="{{ admin_static.url(filename='admin/js/rediscli.js') }}"></script>
<script src="{{ admin_static.url(filename='admin/js/rediscli-1.0.0.js') }}"></script>
<script language="javascript">
$(function() {
var redisCli = new RedisCli({{ get_url('.execute_view')|tojson }});
......
......@@ -24,7 +24,7 @@
{% macro script(message, actions, actions_confirmation) %}
{% if actions %}
<script src="{{ admin_static.url(filename='admin/js/actions.js') }}"></script>
<script src="{{ admin_static.url(filename='admin/js/actions-1.0.0.js') }}"></script>
<script language="javascript">
var modelActions = new AdminModelActions({{ message|tojson|safe }}, {{ actions_confirmation|tojson|safe }});
</script>
......
......@@ -166,6 +166,7 @@
{% macro form_css() %}
<link href="{{ admin_static.url(filename='vendor/select2/select2.css') }}" rel="stylesheet">
<link href="{{ admin_static.url(filename='vendor/select2/select2-bootstrap3.css') }}" rel="stylesheet">
<link href="{{ admin_static.url(filename='vendor/bootstrap-daterangepicker/daterangepicker-bs3.css') }}" rel="stylesheet">
{% if config.MAPBOX_MAP_ID %}
<link href="{{ admin_static.url(filename='vendor/leaflet/leaflet.css') }}" rel="stylesheet">
......@@ -182,5 +183,5 @@
<script src="{{ admin_static.url(filename='vendor/leaflet/leaflet.draw.js') }}"></script>
{% endif %}
<script src="{{ admin_static.url(filename='vendor/bootstrap-daterangepicker/daterangepicker.js') }}"></script>
<script src="{{ admin_static.url(filename='admin/js/form.js') }}"></script>
<script src="{{ admin_static.url(filename='admin/js/form-1.0.0.js') }}"></script>
{% endmacro %}
......@@ -144,7 +144,7 @@
{% block tail %}
{{ super() }}
<script src="{{ admin_static.url(filename='admin/js/filters.js') }}"></script>
<script src="{{ admin_static.url(filename='admin/js/filters-1.0.0.js') }}"></script>
{{ lib.form_js() }}
{{ actionlib.script(_gettext('Please select at least one record.'),
......
......@@ -21,7 +21,7 @@
{% block tail %}
{{ super() }}
<script src="{{ admin_static.url(filename='admin/js/rediscli.js') }}"></script>
<script src="{{ admin_static.url(filename='admin/js/rediscli-1.0.0.js') }}"></script>
<script language="javascript">
$(function() {
var redisCli = new RedisCli({{ admin_view.get_url('.execute_view')|tojson }});
......
This diff is collapsed.
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