Commit 9606f209 authored by Paul Brown's avatar Paul Brown

add date/datetime/time range filters, add filter tests, combine jinja2 logic...

add date/datetime/time range filters, add filter tests, combine jinja2 logic for active filters (from models/layout.html) with filters.js
parent b59dcbdf
...@@ -5,7 +5,7 @@ import datetime ...@@ -5,7 +5,7 @@ import datetime
from flask.ext.admin.babel import lazy_gettext from flask.ext.admin.babel import lazy_gettext
from flask.ext.admin.model import filters from flask.ext.admin.model import filters
from flask.ext.admin.contrib.sqla import tools from flask.ext.admin.contrib.sqla import tools
from sqlalchemy.sql import not_
class BaseSQLAFilter(filters.BaseFilter): class BaseSQLAFilter(filters.BaseFilter):
""" """
...@@ -90,43 +90,156 @@ class BooleanNotEqualFilter(FilterNotEqual, filters.BaseBooleanFilter): ...@@ -90,43 +90,156 @@ class BooleanNotEqualFilter(FilterNotEqual, filters.BaseBooleanFilter):
class DateEqualFilter(FilterEqual, filters.BaseDateFilter): class DateEqualFilter(FilterEqual, filters.BaseDateFilter):
pass
class DateNotEqualFilter(FilterNotEqual, filters.BaseDateFilter):
pass
class DateGreaterFilter(FilterGreater, filters.BaseDateFilter):
pass
class DateSmallerFilter(FilterSmaller, filters.BaseDateFilter):
pass
class DateBetweenFilter(BaseSQLAFilter):
def clean(self, value): def clean(self, value):
return datetime.datetime.strptime(value, '%Y-%m-%d').date() return [datetime.datetime.strptime(range, '%Y-%m-%d') for range in value.split(' to ')]
def apply(self, query, value):
start, end = value
return query.filter(self.column.between(start, end))
def operation(self):
return lazy_gettext('between')
def validate(self, value): def validate(self, value):
try: try:
self.clean(value) value = [datetime.datetime.strptime(range, '%Y-%m-%d') for range in value.split(' to ')]
return True # if " to " is missing, fail validation
# sqlalchemy's .between() will not work if end date is before start date
if (len(value) == 2) and (value[0] <= value[1]):
return True
else:
return False
except ValueError: except ValueError:
return False return False
class DateNotBetweenFilter(DateBetweenFilter):
def apply(self, query, value):
start, end = value
# ~between() isn't possible until sqlalchemy 1.0.0
return query.filter(not_(self.column.between(start, end)))
def operation(self):
return lazy_gettext('not between')
class DateTimeEqualFilter(FilterEqual, filters.BaseDateTimeFilter): class DateTimeEqualFilter(FilterEqual, filters.BaseDateTimeFilter):
def clean(self, value): pass
return datetime.datetime.strptime(value, '%Y-%m-%d %H:%M:%S')
class DateTimeNotEqualFilter(FilterNotEqual, filters.BaseDateTimeFilter):
pass
class DateTimeGreaterFilter(FilterGreater, filters.BaseDateTimeFilter):
pass
class DateTimeSmallerFilter(FilterSmaller, filters.BaseDateTimeFilter):
pass
class DateTimeBetweenFilter(BaseSQLAFilter):
def clean(self, value):
return [datetime.datetime.strptime(range, '%Y-%m-%d %H:%M:%S') for range in value.split(' to ')]
def apply(self, query, value):
start, end = value
return query.filter(self.column.between(start, end))
def operation(self):
return lazy_gettext('between')
def validate(self, value): def validate(self, value):
try: try:
self.clean(value) value = [datetime.datetime.strptime(range, '%Y-%m-%d %H:%M:%S') for range in value.split(' to ')]
return True if (len(value) == 2) and (value[0] <= value[1]):
return True
else:
return False
except ValueError: except ValueError:
return False return False
class DateTimeNotBetweenFilter(DateTimeBetweenFilter):
def apply(self, query, value):
start, end = value
return query.filter(not_(self.column.between(start, end)))
def operation(self):
return lazy_gettext('not between')
class TimeEqualFilter(FilterEqual, filters.BaseTimeFilter): 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 clean(self, value): def clean(self, value):
timetuple = time.strptime(value, '%H:%M:%S') timetuples = [time.strptime(range, '%H:%M:%S')
return datetime.time(timetuple.tm_hour, for range in value.split(' to ')]
timetuple.tm_min, return [datetime.time(timetuple.tm_hour,
timetuple.tm_sec) 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))
def operation(self):
return lazy_gettext('between')
def validate(self, value): def validate(self, value):
try: try:
self.clean(value) timetuples = [time.strptime(range, '%H:%M:%S')
return True for range in value.split(' to ')]
if (len(timetuples) == 2) and (timetuples[0] <= timetuples[1]):
return True
else:
return False
except ValueError: except ValueError:
raise
return False return False
class TimeNotBetweenFilter(TimeBetweenFilter):
def apply(self, query, value):
start, end = value
return query.filter(not_(self.column.between(start, end)))
def operation(self):
return lazy_gettext('not between')
# Base SQLA filter field converter # Base SQLA filter field converter
class FilterConverter(filters.BaseFilterConverter): class FilterConverter(filters.BaseFilterConverter):
strings = (FilterEqual, FilterNotEqual, FilterLike, FilterNotLike) strings = (FilterEqual, FilterNotEqual, FilterLike, FilterNotLike)
...@@ -137,7 +250,6 @@ class FilterConverter(filters.BaseFilterConverter): ...@@ -137,7 +250,6 @@ class FilterConverter(filters.BaseFilterConverter):
def convert(self, type_name, column, name, **kwargs): def convert(self, type_name, column, name, **kwargs):
if type_name.lower() in self.converters: if type_name.lower() in self.converters:
return self.converters[type_name.lower()](column, name, **kwargs) return self.converters[type_name.lower()](column, name, **kwargs)
return None return None
@filters.convert('string', 'unicode', 'text', 'unicodetext', 'varchar') @filters.convert('string', 'unicode', 'text', 'unicodetext', 'varchar')
...@@ -155,23 +267,29 @@ class FilterConverter(filters.BaseFilterConverter): ...@@ -155,23 +267,29 @@ class FilterConverter(filters.BaseFilterConverter):
@filters.convert('date') @filters.convert('date')
def conv_date(self, column, name, **kwargs): def conv_date(self, column, name, **kwargs):
return [DateEqualFilter(column, name), return [DateEqualFilter(column, name),
FilterNotEqual(column, name, data_type='datepicker', **kwargs), DateNotEqualFilter(column, name),
FilterGreater(column, name, data_type='datepicker', **kwargs), DateGreaterFilter(column, name),
FilterSmaller(column, name, data_type='datepicker', **kwargs)] DateSmallerFilter(column, name),
DateBetweenFilter(column, name, data_type='daterangepicker'),
DateNotBetweenFilter(column, name, data_type='daterangepicker')]
@filters.convert('datetime') @filters.convert('datetime')
def conv_datetime(self, column, name, **kwargs): def conv_datetime(self, column, name, **kwargs):
return [DateTimeEqualFilter(column, name), return [DateTimeEqualFilter(column, name),
FilterNotEqual(column, name, data_type='datetimepicker', **kwargs), DateTimeNotEqualFilter(column, name),
FilterGreater(column, name, data_type='datetimepicker', **kwargs), DateTimeGreaterFilter(column, name),
FilterSmaller(column, name, data_type='datetimepicker', **kwargs)] DateTimeSmallerFilter(column, name),
DateTimeBetweenFilter(column, name, data_type='datetimerangepicker'),
DateTimeNotBetweenFilter(column, name, data_type='datetimerangepicker')]
@filters.convert('time') @filters.convert('time')
def conv_time(self, column, name, **kwargs): def conv_time(self, column, name, **kwargs):
return [TimeEqualFilter(column, name), return [TimeEqualFilter(column, name),
FilterNotEqual(column, name, data_type='timepicker', **kwargs), TimeNotEqualFilter(column, name),
FilterGreater(column, name, data_type='timepicker', **kwargs), TimeGreaterFilter(column, name),
FilterSmaller(column, name, data_type='timepicker', **kwargs)] TimeSmallerFilter(column, name),
TimeBetweenFilter(column, name, data_type='timerangepicker'),
TimeNotBetweenFilter(column, name, data_type='timerangepicker')]
@filters.convert('enum') @filters.convert('enum')
def conv_enum(self, column, name, options=None, **kwargs): def conv_enum(self, column, name, options=None, **kwargs):
......
...@@ -768,7 +768,7 @@ class ModelView(BaseModelView): ...@@ -768,7 +768,7 @@ class ModelView(BaseModelView):
# Apply filters # Apply filters
if filters and self._filters: if filters and self._filters:
for idx, value in filters: for idx, flt_name, value in filters:
flt = self._filters[idx] flt = self._filters[idx]
# Figure out joins # Figure out joins
...@@ -782,9 +782,9 @@ class ModelView(BaseModelView): ...@@ -782,9 +782,9 @@ class ModelView(BaseModelView):
count_query = count_query.join(table) count_query = count_query.join(table)
joins.add(table.name) joins.add(table.name)
# Apply filter # turn into python format with .clean() and apply filter
query = flt.apply(query, value) query = flt.apply(query, flt.clean(value))
count_query = flt.apply(count_query, value) count_query = flt.apply(count_query, flt.clean(value))
# Calculate number of rows # Calculate number of rows
count = count_query.scalar() count = count_query.scalar()
......
...@@ -1109,11 +1109,11 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -1109,11 +1109,11 @@ class BaseModelView(BaseView, ActionsMixin):
if key in self._filter_args: if key in self._filter_args:
idx, flt = self._filter_args[key] idx, flt = self._filter_args[key]
value = request.args[n] value = request.args[n]
if flt.validate(value): if flt.validate(value):
filters.append((pos, (idx, flt.clean(value)))) filters.append((pos, (idx, flt.name, value)))
else: else:
flash(gettext('Invalid Filter Value: %(value)s', value=value)) flash(gettext('Invalid Filter Value: %(value)s', value=value))
...@@ -1151,7 +1151,7 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -1151,7 +1151,7 @@ class BaseModelView(BaseView, ActionsMixin):
if view_args.filters: if view_args.filters:
for i, pair in enumerate(view_args.filters): for i, pair in enumerate(view_args.filters):
idx, value = pair idx, flt_name, value = pair
key = 'flt%d_%s' % (i, self.get_filter_arg(idx, self._filters[idx])) key = 'flt%d_%s' % (i, self.get_filter_arg(idx, self._filters[idx]))
kwargs[key] = value kwargs[key] = value
...@@ -1419,4 +1419,4 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -1419,4 +1419,4 @@ class BaseModelView(BaseView, ActionsMixin):
abort(404) abort(404)
data = [loader.format(m) for m in loader.get_list(query, offset, limit)] data = [loader.format(m) for m in loader.get_list(query, offset, limit)]
return Response(json.dumps(data), mimetype='application/json') return Response(json.dumps(data), mimetype='application/json')
\ No newline at end of file
import time
import datetime
from flask.ext.admin._compat import text_type from flask.ext.admin._compat import text_type
from flask.ext.admin.babel import lazy_gettext from flask.ext.admin.babel import lazy_gettext
...@@ -45,15 +48,20 @@ class BaseFilter(object): ...@@ -45,15 +48,20 @@ class BaseFilter(object):
Validate value. Validate value.
If value is valid, returns `True` and `False` otherwise. If value is valid, returns `True` and `False` otherwise.
:param value: :param value:
Value to validate Value to validate
""" """
return True # useful for filters with date conversions, see if conversion in clean() raises ValueError
try:
self.clean(value)
return True
except ValueError:
return False
def clean(self, value): def clean(self, value):
""" """
Parse value into python format. Parse value into python format. Occurs before .apply()
:param value: :param value:
Value to parse Value to parse
...@@ -105,9 +113,8 @@ class BaseDateFilter(BaseFilter): ...@@ -105,9 +113,8 @@ class BaseDateFilter(BaseFilter):
options, options,
data_type='datepicker') data_type='datepicker')
def validate(self, value): def clean(self, value):
# TODO: Validation return datetime.datetime.strptime(value, '%Y-%m-%d').date()
return True
class BaseDateTimeFilter(BaseFilter): class BaseDateTimeFilter(BaseFilter):
...@@ -118,10 +125,10 @@ class BaseDateTimeFilter(BaseFilter): ...@@ -118,10 +125,10 @@ class BaseDateTimeFilter(BaseFilter):
super(BaseDateTimeFilter, self).__init__(name, super(BaseDateTimeFilter, self).__init__(name,
options, options,
data_type='datetimepicker') data_type='datetimepicker')
def validate(self, value): def clean(self, value):
# TODO: Validation # datetime filters will not work in SQLite + SQLAlchemy if value not converted to datetime
return True return datetime.datetime.strptime(value, '%Y-%m-%d %H:%M:%S')
class BaseTimeFilter(BaseFilter): class BaseTimeFilter(BaseFilter):
...@@ -133,10 +140,13 @@ class BaseTimeFilter(BaseFilter): ...@@ -133,10 +140,13 @@ class BaseTimeFilter(BaseFilter):
options, options,
data_type='timepicker') data_type='timepicker')
def validate(self, value): def clean(self, value):
# TODO: Validation # time filters will not work in SQLite + SQLAlchemy if value not converted to time
return True timetuple = time.strptime(value, '%H:%M:%S')
return datetime.time(timetuple.tm_hour,
timetuple.tm_min,
timetuple.tm_sec)
def convert(*args): def convert(*args):
""" """
......
var AdminFilters = function(element, filtersElement, filterGroups) { var AdminFilters = function(element, filtersElement, filterGroups, activeFilters) {
var $root = $(element); var $root = $(element);
var $container = $('.filters', $root); var $container = $('.filters', $root);
var lastCount = 0; var lastCount = 0;
...@@ -40,7 +40,7 @@ var AdminFilters = function(element, filtersElement, filterGroups) { ...@@ -40,7 +40,7 @@ var AdminFilters = function(element, filtersElement, filterGroups) {
return false; return false;
} }
function addFilter(name, subfilters) { function addFilter(name, subfilters, selected, filterValue) {
var $el = $('<tr />').appendTo($container); var $el = $('<tr />').appendTo($container);
// Filter list // Filter list
...@@ -58,19 +58,45 @@ var AdminFilters = function(element, filtersElement, filterGroups) { ...@@ -58,19 +58,45 @@ var AdminFilters = function(element, filtersElement, filterGroups) {
var $select = $('<select class="filter-op" />') var $select = $('<select class="filter-op" />')
.change(changeOperation); .change(changeOperation);
$(subfilters).each(function() { // if one of the subfilters are selected, use that subfilter to create the input field
$select.append($('<option/>').attr('value', this.arg).text(this.operation)); var filter_selection = 0;
$.each(subfilters, function( subfilterIndex, subfilter ) {
if (this.arg == selected) {
$select.append($('<option/>').attr('value', subfilter.arg).attr('selected', true).text(subfilter.operation));
filter_selection = subfilterIndex;
} else {
$select.append($('<option/>').attr('value', subfilter.arg).text(subfilter.operation));
}
}); });
$el.append( $el.append(
$('<td/>').append($select) $('<td/>').append($select)
); );
$select.select2({width: 'resolve'}); // on change, get the subfilter based on the index of the added element, then modify the input field (turn into date range if necessary)
$select.select2({width: 'resolve'}).on("change", function(e) {
// Input styleFilterInput(subfilters[e.added.element[0].index], $el.find('input').last());
var filter = subfilters[0]; });
// 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; var $field;
if (filter.options) { if (filter.options) {
...@@ -78,8 +104,14 @@ var AdminFilters = function(element, filtersElement, filterGroups) { ...@@ -78,8 +104,14 @@ var AdminFilters = function(element, filtersElement, filterGroups) {
.attr('name', makeName(filter.arg)); .attr('name', makeName(filter.arg));
$(filter.options).each(function() { $(filter.options).each(function() {
$field.append($('<option/>') // for active fields, add "selected" to matching value
.val(this[0]).text(this[1])); 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]));
}
}); });
$el.append($('<td/>').append($field)); $el.append($('<td/>').append($field));
...@@ -90,31 +122,36 @@ var AdminFilters = function(element, filtersElement, filterGroups) { ...@@ -90,31 +122,36 @@ var AdminFilters = function(element, filtersElement, filterGroups) {
.attr('name', makeName(filter.arg)); .attr('name', makeName(filter.arg));
$el.append($('<td/>').append($field)); $el.append($('<td/>').append($field));
} }
if (filter.type) { styleFilterInput(filter, $field);
$field.attr('data-role', filter.type);
if (filter.type == "datepicker") { return $field;
$field.attr('data-date-format', "YYYY-MM-DD");
}
else if (filter.type == "datetimepicker") {
$field.attr('data-date-format', "YYYY-MM-DD HH:mm:ss");
}
else if (filter.type == "timepicker") {
$field.attr('data-date-format', "HH:mm:ss");
}
faForm.applyStyle($field, filter.type);
}
} }
$('a.filter', filtersElement).click(function() { $('a.filter', filtersElement).click(function() {
var name = ($(this).text().trim !== undefined ? $(this).text().trim() : $(this).text().replace(/^\s+|\s+$/g,'')); var name = ($(this).text().trim !== undefined ? $(this).text().trim() : $(this).text().replace(/^\s+|\s+$/g,''));
addFilter(name, filterGroups[name]); addFilter(name, filterGroups[name], false, false);
$('button', $root).show(); $('button', $root).show();
//return false; //return false;
}); });
if(activeFilters.length > 0){
$('button', $root).show();
}
// add active filters on page load
$.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);
});
$('.filter-op', $root).change(changeOperation); $('.filter-op', $root).change(changeOperation);
$('.filter-val', $root).change(function() { $('.filter-val', $root).change(function() {
...@@ -122,7 +159,7 @@ var AdminFilters = function(element, filtersElement, filterGroups) { ...@@ -122,7 +159,7 @@ var AdminFilters = function(element, filtersElement, filterGroups) {
}); });
$('.remove-filter', $root).click(removeFilter); $('.remove-filter', $root).click(removeFilter);
$('.filter-val', $root).each(function() { $('.filter-val', $root).not('.select2-container').each(function() {
var count = getCount($(this).attr('name')); var count = getCount($(this).attr('name'));
if (count > lastCount) if (count > lastCount)
lastCount = count; lastCount = count;
......
...@@ -107,49 +107,95 @@ ...@@ -107,49 +107,95 @@
return true; return true;
case 'datepicker': case 'datepicker':
$el.daterangepicker({ $el.daterangepicker({
// TODO: Have separate converters for bs2 and bs3
// Bootstrap 2 option
// Bootstrap 3 option
timePicker: false, timePicker: false,
showDropdowns: true, showDropdowns: true,
singleDatePicker: true, singleDatePicker: true,
format: $el.attr('data-date-format') format: $el.attr('data-date-format')
}, },
function(start, end) { function(start, end) {
$('.filter-val').trigger("change"); $('.filter-val').trigger("change");
}); });
return true;
case 'daterangepicker':
$el.daterangepicker({
timePicker: false,
showDropdowns: true,
separator: ' to ',
format: $el.attr('data-date-format')
},
function(start, end) {
$('.filter-val').trigger("change");
});
return true; return true;
case 'datetimepicker': case 'datetimepicker':
$el.daterangepicker({ $el.daterangepicker({
timePicker: true, timePicker: true,
showDropdowns: true, showDropdowns: true,
singleDatePicker: true, singleDatePicker: true,
timePickerIncrement: 1, timePickerIncrement: 1,
timePicker12Hour: false, timePicker12Hour: false,
format: $el.attr('data-date-format') format: $el.attr('data-date-format')
}, },
function(start, end) { function(start, end) {
$('.filter-val').trigger("change"); $('.filter-val').trigger("change");
}); });
return true;
case 'datetimerangepicker':
$el.daterangepicker({
timePicker: true,
showDropdowns: true,
timePickerIncrement: 1,
timePicker12Hour: false,
separator: ' to ',
format: $el.attr('data-date-format')
},
function(start, end) {
$('.filter-val').trigger("change");
});
return true; return true;
case 'timepicker': case 'timepicker':
$el.daterangepicker({ $el.daterangepicker({
// Bootstrap 2 option // Bootstrap 2 option
timePicker: true, timePicker: true,
showDropdowns: true, showDropdowns: true,
format: $el.attr('data-date-format'), format: $el.attr('data-date-format'),
timePicker12Hour: false, timePicker12Hour: false,
timePickerIncrement: 1, timePickerIncrement: 1,
singleDatePicker: true singleDatePicker: true
},
function(start, end) {
$('.filter-val').trigger("change");
});
// hack to hide calendar to create a time-only picker
$el.data('daterangepicker').container.find('.calendar-date').hide();
$el.on('showCalendar.daterangepicker', function (event, data) {
var $container = data.container;
$container.find('.calendar-date').remove();
});
return true;
case 'timerangepicker':
$el.daterangepicker({
// Bootstrap 2 option
timePicker: true,
showDropdowns: true,
format: $el.attr('data-date-format'),
timePicker12Hour: false,
separator: ' to ',
timePickerIncrement: 1
}, },
function(start, end) { function(start, end) {
$('.filter-val').trigger("change"); $('.filter-val').trigger("change");
}); });
$el.data('daterangepicker').container.find('.calendar-date').hide(); // hack - hide calendar + range inputs
$el.on('showCalendar.daterangepicker', function (event, data) { $el.data('daterangepicker').container.find('.calendar-date').hide();
var $container = data.container; $el.data('daterangepicker').container.find('.daterangepicker_start_input').hide();
$container.find('.calendar-date').remove(); $el.data('daterangepicker').container.find('.daterangepicker_end_input').hide();
}); // hack - add TO between time inputs
$el.data('daterangepicker').container.find('.left').before($('<div style="float: right; margin-top: 20px; padding-left: 5px; padding-right: 5px;"> to </span>'));
$el.on('showCalendar.daterangepicker', function (event, data) {
var $container = data.container;
$container.find('.calendar-date').remove();
});
return true; return true;
} }
}; };
...@@ -183,7 +229,7 @@ ...@@ -183,7 +229,7 @@
prefix = id + '-' + idx; prefix = id + '-' + idx;
} }
// Get tempalate // Get template
var $template = $($el.find('> .inline-field-template').text()); var $template = $($el.find('> .inline-field-template').text());
// Set form ID // Set form ID
......
...@@ -20,38 +20,7 @@ ...@@ -20,38 +20,7 @@
{% endif %} {% endif %}
</div> </div>
<table class="filters"> <table class="filters"></table>
{%- for n, values in enumerate(active_filters) -%}
<tr>
{% set idx, value = values %}
{% set filter = filters[idx] %}
{% set filter_arg = admin_view.get_filter_arg(idx, filter) %}
<td>
<a href="javascript:void(0)" class="btn remove-filter" title="{{ _gettext('Remove Filter') }}">
<span class="close-icon">&times;</span>&nbsp;{{ filter.name }}
</a>
</td>
<td>
<select class="filter-op" data-role="select2">
{% for op in filter_groups[filter.name] %}
<option value="{{ op['arg'] }}"{% if idx == op['index'] %} selected="selected"{% endif %}>{{ op['operation'] }}</option>
{% endfor %}
</select>
</td>
<td>
{%- if filter.options -%}
<select name="flt{{n}}_{{ filter_arg }}" class="filter-val" data-role="select2">
{%- for d in filter.options %}
<option value="{{ d[0] }}"{% if value == d[0] %} selected{% endif %}>{{ d[1] }}</option>
{%- endfor %}
</select>
{%- else -%}
<input name="flt{{n}}_{{ filter_arg }}" type="text" value="{{ value or '' }}" class="filter-val"{% if filter.data_type %} data-role="{{ filter.data_type }}"{% endif %} {% if filter.data_type == "datepicker" %}data-date-format="YYYY-MM-DD"{% elif filter.data_type == "datetimepicker" %}data-date-format="YYYY-MM-DD HH:mm:ss"{% elif filter.data_type == "timepicker" %}data-date-format="HH:mm:ss"{% endif %}></input>
{%- endif -%}
</td>
</tr>
{%- endfor -%}
</table>
</form> </form>
<div class="clearfix"></div> <div class="clearfix"></div>
{% endmacro %} {% endmacro %}
...@@ -59,21 +28,21 @@ ...@@ -59,21 +28,21 @@
{% macro search_form(input_class="span2") %} {% macro search_form(input_class="span2") %}
<form method="GET" action="{{ return_url }}" class="search-form"> <form method="GET" action="{{ return_url }}" class="search-form">
{% if sort_column is not none %} {% if sort_column is not none %}
<input type="hidden" name="sort" value="{{ sort_column }}"></input> <input type="hidden" name="sort" value="{{ sort_column }}">
{% endif %} {% endif %}
{% if sort_desc %} {% if sort_desc %}
<input type="hidden" name="desc" value="{{ sort_desc }}"></input> <input type="hidden" name="desc" value="{{ sort_desc }}">
{% endif %} {% 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('Search') }}"></input> <input type="text" name="search" value="{{ search }}" class="{{ input_class }}" placeholder="{{ _gettext('Search') }}">
<a href="{{ clear_search_url }}" class="clear add-on"> <a href="{{ clear_search_url }}" class="clear add-on">
<i class="icon-remove"></i> <i class="icon-remove"></i>
</a> </a>
</div> </div>
{% else %} {% else %}
<div> <div>
<input type="text" name="search" value="" class="{{ input_class }}" placeholder="{{ _gettext('Search') }}"></input> <input type="text" name="search" value="" class="{{ input_class }}" placeholder="{{ _gettext('Search') }}">
</div> </div>
{% endif %} {% endif %}
</form> </form>
......
...@@ -160,8 +160,9 @@ ...@@ -160,8 +160,9 @@
{% if filter_groups %} {% if filter_groups %}
var filter = new AdminFilters( var filter = new AdminFilters(
'#filter_form', '.field-filters', '#filter_form', '.field-filters',
{{ filter_groups|tojson|safe }} {{ filter_groups|tojson|safe }},
); {{ active_filters|tojson|safe }}
);
{% endif %} {% endif %}
})(jQuery); })(jQuery);
</script> </script>
......
...@@ -20,38 +20,7 @@ ...@@ -20,38 +20,7 @@
{% endif %} {% endif %}
</div> </div>
<table class="filters"> <table class="filters"></table>
{%- for n, values in enumerate(active_filters) -%}
<tr>
{% set idx, value = values %}
{% set filter = filters[idx] %}
{% set filter_arg = admin_view.get_filter_arg(idx, filter) %}
<td>
<a href="javascript:void(0)" class="btn btn-filter remove-filter" title="{{ _gettext('Remove Filter') }}">
<span class="close-icon">&times;</span>&nbsp;{{ filter.name }}
</a>
</td>
<td>
<select class="filter-op" data-role="select2">
{% for op in filter_groups[filter.name] %}
<option value="{{ op['arg'] }}"{% if idx == op['index'] %} selected="selected"{% endif %}>{{ op['operation'] }}</option>
{% endfor %}
</select>
</td>
<td>
{%- if filter.options -%}
<select name="flt{{n}}_{{ filter_arg }}" class="filter-val" data-role="select2">
{%- for d in filter.options %}
<option value="{{ d[0] }}"{% if value == d[0] %} selected{% endif %}>{{ d[1] }}</option>
{%- endfor %}
</select>
{%- else -%}
<input name="flt{{n}}_{{ filter_arg }}" type="text" value="{{ value or '' }}" class="filter-val form-control"{% if filter.data_type %} data-role="{{ filter.data_type }}"{% endif %} {% if filter.data_type == "datepicker" %}data-date-format="YYYY-MM-DD"{% elif filter.data_type == "datetimepicker" %}data-date-format="YYYY-MM-DD HH:mm:ss"{% elif filter.data_type == "timepicker" %}data-date-format="HH:mm:ss"{% endif %}></input>
{%- endif -%}
</td>
</tr>
{%- endfor -%}
</table>
</form> </form>
<div class="clearfix"></div> <div class="clearfix"></div>
{% endmacro %} {% endmacro %}
...@@ -59,21 +28,21 @@ ...@@ -59,21 +28,21 @@
{% macro search_form(input_class="span2") %} {% macro search_form(input_class="span2") %}
<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">
{% if sort_column is not none %} {% if sort_column is not none %}
<input type="hidden" name="sort" value="{{ sort_column }}"></input> <input type="hidden" name="sort" value="{{ sort_column }}">
{% endif %} {% endif %}
{% if sort_desc %} {% if sort_desc %}
<input type="hidden" name="desc" value="{{ sort_desc }}"></input> <input type="hidden" name="desc" value="{{ sort_desc }}">
{% endif %} {% endif %}
{% if search %} {% if search %}
<div class="input-append form-group"> <div class="input-append form-group">
<input type="text" name="search" value="{{ search }}" class="{{ input_class }} form-control" placeholder="{{ _gettext('Search') }}"></input> <input type="text" name="search" value="{{ search }}" class="{{ input_class }} form-control" placeholder="{{ _gettext('Search') }}">
<a href="{{ clear_search_url }}" class="clear add-on"> <a href="{{ clear_search_url }}" class="clear add-on">
<span class="glyphicon glyphicon-remove"></span> <span class="glyphicon glyphicon-remove"></span>
</a> </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('Search') }}"></input> <input type="text" name="search" value="" class="{{ input_class }} form-control" placeholder="{{ _gettext('Search') }}">
</div> </div>
{% endif %} {% endif %}
</form> </form>
......
...@@ -160,7 +160,8 @@ ...@@ -160,7 +160,8 @@
{% if filter_groups %} {% if filter_groups %}
var filter = new AdminFilters( var filter = new AdminFilters(
'#filter_form', '.field-filters', '#filter_form', '.field-filters',
{{ filter_groups|tojson|safe }} {{ filter_groups|tojson|safe }},
{{ active_filters|tojson|safe }}
); );
{% endif %} {% endif %}
})(jQuery); })(jQuery);
......
...@@ -454,23 +454,29 @@ def test_column_filters(): ...@@ -454,23 +454,29 @@ def test_column_filters():
(0, 'equals'), (0, 'equals'),
(1, 'not equal'), (1, 'not equal'),
(2, 'greater than'), (2, 'greater than'),
(3, 'smaller than') (3, 'smaller than'),
(4, 'between'),
(5, 'not between')
]) ])
eq_([(f['index'], f['operation']) for f in view._filter_groups[u'Datetime Field']], eq_([(f['index'], f['operation']) for f in view._filter_groups[u'Datetime Field']],
[ [
(4, 'equals'), (6, 'equals'),
(5, 'not equal'), (7, 'not equal'),
(6, 'greater than'), (8, 'greater than'),
(7, 'smaller than') (9, 'smaller than'),
(10, 'between'),
(11, 'not between')
]) ])
eq_([(f['index'], f['operation']) for f in view._filter_groups[u'Time Field']], eq_([(f['index'], f['operation']) for f in view._filter_groups[u'Time Field']],
[ [
(8, 'equals'), (12, 'equals'),
(9, 'not equal'), (13, 'not equal'),
(10, 'greater than'), (14, 'greater than'),
(11, 'smaller than') (15, 'smaller than'),
(16, 'between'),
(17, 'not between')
]) ])
# date - equals # date - equals
...@@ -481,19 +487,124 @@ def test_column_filters(): ...@@ -481,19 +487,124 @@ def test_column_filters():
ok_('date_obj2' not in data) ok_('date_obj2' not in data)
# datetime - equals # datetime - equals
rv = client.get('/admin/_datetime/?flt0_4=2014-04-03+01%3A09%3A00') rv = client.get('/admin/_datetime/?flt0_6=2014-04-03+01%3A09%3A00')
eq_(rv.status_code, 200) eq_(rv.status_code, 200)
data = rv.data.decode('utf-8') data = rv.data.decode('utf-8')
ok_('datetime_obj1' in data) ok_('datetime_obj1' in data)
ok_('datetime_obj2' not in data) ok_('datetime_obj2' not in data)
# time - equals # time - equals
rv = client.get('/admin/_datetime/?flt0_8=11%3A10%3A09') rv = client.get('/admin/_datetime/?flt0_12=11%3A10%3A09')
eq_(rv.status_code, 200)
data = rv.data.decode('utf-8')
ok_('time_obj1' in data)
ok_('time_obj2' not in data)
# date - not equal
rv = client.get('/admin/_datetime/?flt0_1=2014-11-17')
eq_(rv.status_code, 200)
data = rv.data.decode('utf-8')
ok_('date_obj1' not in data)
ok_('date_obj2' in data)
# datetime - not equal
rv = client.get('/admin/_datetime/?flt0_7=2014-04-03+01%3A09%3A00')
eq_(rv.status_code, 200)
data = rv.data.decode('utf-8')
ok_('datetime_obj1' not in data)
ok_('datetime_obj2' in data)
# time - not equal
rv = client.get('/admin/_datetime/?flt0_13=11%3A10%3A09')
eq_(rv.status_code, 200)
data = rv.data.decode('utf-8')
ok_('time_obj1' not in data)
ok_('time_obj2' in data)
# date - greater
rv = client.get('/admin/_datetime/?flt0_2=2014-11-16')
eq_(rv.status_code, 200)
data = rv.data.decode('utf-8')
ok_('date_obj1' in data)
ok_('date_obj2' not in data)
# datetime - greater
rv = client.get('/admin/_datetime/?flt0_8=2014-04-03+01%3A08%3A00')
eq_(rv.status_code, 200)
data = rv.data.decode('utf-8')
ok_('datetime_obj1' in data)
ok_('datetime_obj2' not in data)
# time - greater
rv = client.get('/admin/_datetime/?flt0_14=11%3A09%3A09')
eq_(rv.status_code, 200)
data = rv.data.decode('utf-8')
ok_('time_obj1' in data)
ok_('time_obj2' not in data)
# date - smaller
rv = client.get('/admin/_datetime/?flt0_3=2014-11-16')
eq_(rv.status_code, 200)
data = rv.data.decode('utf-8')
ok_('date_obj1' not in data)
ok_('date_obj2' in data)
# datetime - smaller
rv = client.get('/admin/_datetime/?flt0_9=2014-04-03+01%3A08%3A00')
eq_(rv.status_code, 200)
data = rv.data.decode('utf-8')
ok_('datetime_obj1' not in data)
ok_('datetime_obj2' in data)
# time - smaller
rv = client.get('/admin/_datetime/?flt0_15=11%3A09%3A09')
eq_(rv.status_code, 200)
data = rv.data.decode('utf-8')
ok_('time_obj1' not in data)
ok_('time_obj2' in data)
# date - between
rv = client.get('/admin/_datetime/?flt0_4=2014-11-13+to+2014-11-20')
eq_(rv.status_code, 200)
data = rv.data.decode('utf-8')
ok_('date_obj1' in data)
ok_('date_obj2' not in data)
# datetime - between
rv = client.get('/admin/_datetime/?flt0_10=2014-04-02+00%3A00%3A00+to+2014-11-20+23%3A59%3A59')
eq_(rv.status_code, 200)
data = rv.data.decode('utf-8')
ok_('datetime_obj1' in data)
ok_('datetime_obj2' not in data)
# time - between
rv = client.get('/admin/_datetime/?flt0_16=10%3A40%3A00+to+11%3A50%3A59')
eq_(rv.status_code, 200) eq_(rv.status_code, 200)
data = rv.data.decode('utf-8') data = rv.data.decode('utf-8')
ok_('time_obj1' in data) ok_('time_obj1' in data)
ok_('time_obj2' not in data) ok_('time_obj2' not in data)
# date - not between
rv = client.get('/admin/_datetime/?flt0_5=2014-11-13+to+2014-11-20')
eq_(rv.status_code, 200)
data = rv.data.decode('utf-8')
ok_('date_obj1' not in data)
ok_('date_obj2' in data)
# datetime - not between
rv = client.get('/admin/_datetime/?flt0_11=2014-04-02+00%3A00%3A00+to+2014-11-20+23%3A59%3A59')
eq_(rv.status_code, 200)
data = rv.data.decode('utf-8')
ok_('datetime_obj1' not in data)
ok_('datetime_obj2' in data)
# time - not between
rv = client.get('/admin/_datetime/?flt0_17=10%3A40%3A00+to+11%3A50%3A59')
eq_(rv.status_code, 200)
data = rv.data.decode('utf-8')
ok_('time_obj1' not in data)
ok_('time_obj2' in data)
def test_url_args(): def test_url_args():
app, db, admin = setup() app, db, admin = setup()
......
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