Commit ba750147 authored by Paul Brown's avatar Paul Brown

add 'in list' and 'empty' filters, allow changing to filters with opts, add...

add 'in list' and 'empty' filters, allow changing to filters with opts, add versions to js filenames

fix between filters
parent ec0f1054
......@@ -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
......@@ -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 ')]
......@@ -203,6 +243,9 @@ class TimeSmallerFilter(FilterSmaller, filters.BaseTimeFilter):
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 ')]
......@@ -242,17 +285,26 @@ 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]
......@@ -260,39 +312,28 @@ class FilterConverter(filters.BaseFilterConverter):
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):
# set all operations to select2
kwargs['data_type'] = "select2"
if not options:
options = [
(v, v)
......
......@@ -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')
......
......@@ -98,7 +98,7 @@ class BaseBooleanFilter(BaseFilter):
super(BaseBooleanFilter, self).__init__(name,
(('1', lazy_gettext(u'Yes')),
('0', lazy_gettext(u'No'))),
data_type)
data_type="select2")
def validate(self, value):
return value in ('0', '1')
......
......@@ -19,14 +19,6 @@ var AdminFilters = function(element, filtersElement, filterGroups, activeFilters
return result;
}
function changeOperation() {
var $row = $(this).closest('tr');
var $el = $('.filter-val:input', $row);
var count = getCount($el.attr('name'));
$el.attr('name', 'flt' + count + '_' + $(this).val());
$('button', $root).show();
}
function removeFilter() {
$(this).closest('tr').remove();
if($('.filters tr').length == 0) {
......@@ -40,7 +32,73 @@ var AdminFilters = function(element, filtersElement, filterGroups, activeFilters
return false;
}
function addFilter(name, subfilters, selected, filterValue) {
// triggered when the filter operation (equals, not equals, etc) is changed
function changeOperation(subfilters, $el, filter, $select) {
// get the filter_group subfilter based on the index of the selected option
var selectedFilter = subfilters[$select.select2('data').element[0].index];
var $inputContainer = $el.find('td').last();
// recreate and style the input field (turn into date range or select2 if necessary)
var $field = createFilterInput($inputContainer, null, selectedFilter);
styleFilterInput(selectedFilter, $field);
$('button', $root).show();
}
// generate HTML for filter input - allows changing filter input type to one with options or tags
function createFilterInput(inputContainer, filterValue, filter) {
if (filter.type == "select2") {
var $field = $('<select class="filter-val" />').attr('name', makeName(filter.arg));
$(filter.options).each(function() {
// for active filter inputs with options, add "selected" if there is a matching active filter
if (filterValue && (filterValue == this[0])) {
$field.append($('<option/>')
.val(this[0]).text(this[1]).attr('selected', true));
} else {
$field.append($('<option/>')
.val(this[0]).text(this[1]));
}
});
} else if (filter.type == "select2-tags") {
var $field = $('<input type="hidden" class="filter-val form-control" />').attr('name', makeName(filter.arg));
$field.val(filterValue);
} else {
var $field = $('<input type="text" class="filter-val form-control" />').attr('name', makeName(filter.arg));
$field.val(filterValue);
}
inputContainer.replaceWith($('<td/>').append($field));
return $field;
}
// add styling to input field, accommodates filters that change the input field's HTML
function styleFilterInput(filter, field) {
if (filter.type) {
if ((filter.type == "datepicker") || (filter.type == "daterangepicker")) {
field.attr('data-date-format', "YYYY-MM-DD");
} else if ((filter.type == "datetimepicker") || (filter.type == "datetimerangepicker")) {
field.attr('data-date-format', "YYYY-MM-DD HH:mm:ss");
} else if ((filter.type == "timepicker") || (filter.type == "timerangepicker")) {
field.attr('data-date-format', "HH:mm:ss");
} else if (filter.type == "select2-tags") {
var options = [];
if (filter.options) {
filter.options.forEach(function(option) {
options.push({id:option[0], text:option[1]});
});
// save tag options as json on data attribute
field.attr('data-tags', JSON.stringify(options));
}
}
faForm.applyStyle(field, filter.type);
}
return field;
}
function addFilter(name, subfilters, selectedIndex, filterValue) {
var $el = $('<tr />').appendTo($container);
// Filter list
......@@ -54,16 +112,15 @@ var AdminFilters = function(element, filtersElement, filterGroups, activeFilters
)
);
// Filter type
var $select = $('<select class="filter-op" />')
.change(changeOperation);
// Filter operation <select> (equal, not equal, etc)
var $select = $('<select class="filter-op" />');
// if one of the subfilters are selected, use that subfilter to create the input field
var filter_selection = 0;
var filterSelection = 0;
$.each(subfilters, function( subfilterIndex, subfilter ) {
if (this.arg == selected) {
if (this.arg == selectedIndex) {
$select.append($('<option/>').attr('value', subfilter.arg).attr('selected', true).text(subfilter.operation));
filter_selection = subfilterIndex;
filterSelection = subfilterIndex;
} else {
$select.append($('<option/>').attr('value', subfilter.arg).text(subfilter.operation));
}
......@@ -73,90 +130,43 @@ var AdminFilters = function(element, filtersElement, filterGroups, activeFilters
$('<td/>').append($select)
);
// on change, get the subfilter based on the index of the added element, then modify the input field (turn into date range if necessary)
// select2 for filter-op (equal, not equal, etc)
$select.select2({width: 'resolve'}).on("change", function(e) {
styleFilterInput(subfilters[e.added.element[0].index], $el.find('input').last());
});
// add styling to input field, accommodates filters that change the type of the field
function styleFilterInput(filter, field) {
if (filter.type) {
field.attr('data-role', filter.type);
if ((filter.type == "datepicker") || (filter.type == "daterangepicker")) {
field.attr('data-date-format', "YYYY-MM-DD");
}
else if ((filter.type == "datetimepicker") || (filter.type == "datetimerangepicker")) {
field.attr('data-date-format', "YYYY-MM-DD HH:mm:ss");
}
else if ((filter.type == "timepicker") || (filter.type == "timerangepicker")) {
field.attr('data-date-format', "HH:mm:ss");
}
faForm.applyStyle(field, filter.type);
}
}
// initial filter creation
filter = subfilters[filter_selection];
var $field;
if (filter.options) {
$field = $('<select class="filter-val" />')
.attr('name', makeName(filter.arg));
$(filter.options).each(function() {
// for active fields, add "selected" to matching value
if (filterValue && (filterValue == this[0])) {
$field.append($('<option/>')
.val(this[0]).text(this[1]).attr('selected', true));
} else {
$field.append($('<option/>')
.val(this[0]).text(this[1]));
}
changeOperation(subfilters, $el, filter, $select);
});
$el.append($('<td/>').append($field));
$field.select2({width: 'resolve'});
} else
{
$field = $('<input type="text" class="filter-val form-control" />')
.attr('name', makeName(filter.arg));
$el.append($('<td/>').append($field));
}
// get filter option from filter_group, only for new filter creation
var filter = subfilters[filterSelection];
var $inputContainer = $('<td/>').appendTo($el);
styleFilterInput(filter, $field);
var $newFilterField = createFilterInput($inputContainer, filterValue, filter);
var $styledFilterField = styleFilterInput(filter, $newFilterField);
return $field;
return $styledFilterField;
}
// Add Filter Button, new filter
$('a.filter', filtersElement).click(function() {
var name = ($(this).text().trim !== undefined ? $(this).text().trim() : $(this).text().replace(/^\s+|\s+$/g,''));
addFilter(name, filterGroups[name], false, false);
addFilter(name, filterGroups[name], false, null);
$('button', $root).show();
//return false;
});
if(activeFilters.length > 0){
$('button', $root).show();
}
// add active filters on page load
// on page load - add active filters
$.each(activeFilters, function( activeIndex, activeFilter ) {
var idx = activeFilter[0],
name = activeFilter[1],
filterValue = activeFilter[2];
$field = addFilter(name, filterGroups[name], idx, filterValue);
// set value of newly created field
$field.val(filterValue);
var $activeField = addFilter(name, filterGroups[name], idx, filterValue);
});
$('.filter-op', $root).change(changeOperation);
$('.filter-val', $root).change(function() {
// show "Apply Filter" button when filter input is changed
$('.filter-val', $root).on('input change', function() {
$('button', $root).show();
});
$('.remove-filter', $root).click(removeFilter);
$('.filter-val', $root).not('.select2-container').each(function() {
......
......@@ -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;
......
......@@ -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>
......
......@@ -182,5 +182,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