diff --git a/tailbone/newgrids/alchemy.py b/tailbone/newgrids/alchemy.py index 40fc3ba5..5c4e249e 100644 --- a/tailbone/newgrids/alchemy.py +++ b/tailbone/newgrids/alchemy.py @@ -93,6 +93,8 @@ class AlchemyGrid(Grid): factory = filters.AlchemyNumericFilter elif isinstance(column.type, sa.Boolean): factory = filters.AlchemyBooleanFilter + elif isinstance(column.type, (sa.Date, sa.DateTime)): + factory = filters.AlchemyDateFilter return factory(key, column=column, **kwargs) def iter_filters(self): diff --git a/tailbone/newgrids/filters.py b/tailbone/newgrids/filters.py index 9d05d7a6..5c14acc1 100644 --- a/tailbone/newgrids/filters.py +++ b/tailbone/newgrids/filters.py @@ -38,29 +38,41 @@ from pyramid_simpleform.renderers import FormRenderer from webhelpers.html import HTML, tags -class FilterRenderer(object): +class FilterValueRenderer(object): """ Base class for all filter renderers. """ + def __init__(self, filter=None): + self.filter = filter + + @property + def name(self): + return self.filter.key + def render(self, value=None, **kwargs): """ Render the filter input element(s) as HTML. Default implementation uses a simple text input. """ - name = self.filter.key - return tags.text(name, value=value, id='filter.{0}.value'.format(name)) + return tags.text(self.name, value=value, **kwargs) -class DefaultRenderer(FilterRenderer): +class DefaultValueRenderer(FilterValueRenderer): """ Default / fallback renderer. """ -class NumericRenderer(FilterRenderer): +class NumericValueRenderer(FilterValueRenderer): """ - Input renderer for numeric fields. + Input renderer for numeric values. + """ + + +class DateValueRenderer(FilterValueRenderer): + """ + Input renderer for date values. """ @@ -69,7 +81,7 @@ class GridFilter(object): Represents a filter available to a grid. This is used to construct the 'filters' section when rendering the index page template. """ - verbmap = { + verb_labels = { 'is_any': "is any", 'equal': "equal to", 'not_equal': "not equal to", @@ -85,13 +97,20 @@ class GridFilter(object): 'does_not_contain': "does not contain", } - def __init__(self, key, label=None, verbs=None, renderer=None, + valueless_verbs = ['is_any', 'is_null', 'is_not_null', 'is_true', 'is_false'] + + value_renderer_factory = DefaultValueRenderer + + def __init__(self, key, label=None, verbs=None, value_renderer=None, default_active=False, default_verb=None, default_value=None): self.key = key self.label = label or prettify(key) self.verbs = verbs or self.get_default_verbs() - self.renderer = renderer or DefaultRenderer() - self.renderer.filter = self + if value_renderer is not None: + value_renderer.filter = self + self.value_renderer = value_renderer + else: + self.value_renderer = self.value_renderer_factory(self) self.default_active = default_active self.default_verb = default_verb self.default_value = default_value @@ -132,9 +151,14 @@ class GridFilter(object): """ return data - def render(self, **kwargs): - kwargs['filter'] = self - return self.renderer.render(**kwargs) + def render_value(self, value=UNSPECIFIED, **kwargs): + """ + Render the HTML needed to expose the filter's value for user input. + """ + if value is UNSPECIFIED: + value = self.value + kwargs['filtr'] = self + return self.value_renderer.render(value=value, **kwargs) class AlchemyGridFilter(GridFilter): @@ -284,6 +308,25 @@ class AlchemyBooleanFilter(AlchemyGridFilter): return query.filter(self.column == False) +class AlchemyDateFilter(AlchemyGridFilter): + """ + Date filter for SQLAlchemy. + """ + value_renderer_factory = DateValueRenderer + + def filter_is_true(self, query, value): + """ + Filter data with an "is true" query (alias for "is not null"). + """ + return self.filter_is_not_null(query, value) + + def filter_is_false(self, query, value): + """ + Filter data with an "is false" query (alias for "is null"). + """ + return self.filter_is_null(query, value) + + class GridFilterSet(OrderedDict): """ Collection class for :class:`GridFilter` instances. @@ -337,13 +380,18 @@ class GridFiltersFormRenderer(FormRenderer): """ Render the verb selection dropdown for the given filter. """ - options = [(v, filtr.verbmap.get(v, "unknown verb '{0}'".format(v))) + options = [(v, filtr.verb_labels.get(v, "unknown verb '{0}'".format(v))) for v in filtr.verbs] - return self.select('{0}.verb'.format(filtr.key), options, class_='verb') + hide_values = [v for v in filtr.valueless_verbs + if v in filtr.verbs] + return self.select('{0}.verb'.format(filtr.key), options, **{ + 'class_': 'verb', + 'data-hide-value-for': ' '.join(hide_values)}) - def filter_value(self, filtr): + def filter_value(self, filtr, **kwargs): """ Render the value input element(s) for the filter. """ - # TODO: This surely needs some work..? - return HTML.tag('div', class_='value', c=filtr.render(value=self.value(filtr.key))) + style = 'display: none;' if filtr.verb in filtr.valueless_verbs else None + return HTML.tag('div', class_='value', style=style, + c=filtr.render_value(**kwargs)) diff --git a/tailbone/static/js/jquery.ui.tailbone.js b/tailbone/static/js/jquery.ui.tailbone.js index 521920fa..0542fe82 100644 --- a/tailbone/static/js/jquery.ui.tailbone.js +++ b/tailbone/static/js/jquery.ui.tailbone.js @@ -213,6 +213,8 @@ _create: function() { + var that = this; + // Track down some important elements. this.checkbox = this.element.find('input[name$="-active"]'); this.label = this.element.find('label'); @@ -229,8 +231,24 @@ icons: {primary: 'ui-icon-blank'} }); - // Enhance some more stuff. - this.inputs.find('.verb').selectmenu({width: '15em'}); + // Enhance verb dropdown as selectmenu. + this.verb_select = this.inputs.find('.verb'); + this.valueless_verbs = {}; + $.each(this.verb_select.data('hide-value-for').split(' '), function(index, value) { + that.valueless_verbs[value] = true; + }); + this.verb_select.selectmenu({ + width: '15em', + change: function(event, ui) { + if (ui.item.value in that.valueless_verbs) { + that.inputs.find('.value').hide(); + } else { + that.inputs.find('.value').show(); + that.focus(); + that.select(); + } + } + }); // Listen for button click, to keep checkbox in sync. this._on(this.activebutton, { @@ -289,6 +307,10 @@ this.inputs.find('.value input').focus(); }, + select: function() { + this.inputs.find('.value input').select(); + }, + value: function() { return this.inputs.find('.value input').val(); },