From cd461aef51af00cae75c9a0459dd66d0d19b9ddb Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 11 Mar 2016 13:21:54 -0600 Subject: [PATCH] Improve grid filters for datetime fields. Hopefully this makes these filters more intuitive, by allowing user to provide a date value but interpret in a datetime context. --- tailbone/newgrids/alchemy.py | 6 +- tailbone/newgrids/filters.py | 119 +++++++++++++++++++++++++++++++++-- 2 files changed, 117 insertions(+), 8 deletions(-) diff --git a/tailbone/newgrids/alchemy.py b/tailbone/newgrids/alchemy.py index 0e31cb4e..6bb8355a 100644 --- a/tailbone/newgrids/alchemy.py +++ b/tailbone/newgrids/alchemy.py @@ -100,12 +100,14 @@ class AlchemyGrid(Grid): factory = filters.AlchemyNumericFilter elif isinstance(column.type, sa.Boolean): factory = filters.AlchemyBooleanFilter - elif isinstance(column.type, (sa.Date, sa.DateTime)): + elif isinstance(column.type, sa.Date): factory = filters.AlchemyDateFilter + elif isinstance(column.type, sa.DateTime): + factory = filters.AlchemyDateTimeFilter elif isinstance(column.type, GPCType): factory = filters.AlchemyGPCFilter factory = kwargs.pop('factory', factory) - return factory(key, column=column, **kwargs) + return factory(key, column=column, config=self.request.rattail_config, **kwargs) def iter_filters(self): """ diff --git a/tailbone/newgrids/filters.py b/tailbone/newgrids/filters.py index 06fd40a2..e5d375a8 100644 --- a/tailbone/newgrids/filters.py +++ b/tailbone/newgrids/filters.py @@ -26,6 +26,9 @@ Grid Filters from __future__ import unicode_literals, absolute_import +import datetime +import logging + import sqlalchemy as sa from edbob.util import prettify @@ -33,12 +36,16 @@ from edbob.util import prettify from rattail.gpc import GPC from rattail.util import OrderedDict from rattail.core import UNSPECIFIED +from rattail.time import localtime, make_utc from pyramid_simpleform import Form from pyramid_simpleform.renderers import FormRenderer from webhelpers.html import HTML, tags +log = logging.getLogger(__name__) + + class FilterValueRenderer(object): """ Base class for all filter renderers. @@ -350,6 +357,17 @@ class AlchemyDateFilter(AlchemyGridFilter): """ value_renderer_factory = DateValueRenderer + verb_labels = { + 'equal': "on", + 'not_equal': "not on", + 'greater_than': "after", + 'greater_equal': "on or after", + 'less_than': "before", + 'less_equal': "on or before", + 'is_null': "is null", + 'is_not_null': "is not null", + } + def default_verbs(self): """ Expose greater-than / less-than verbs in addition to core. @@ -357,17 +375,106 @@ class AlchemyDateFilter(AlchemyGridFilter): return ['equal', 'not_equal', 'greater_than', 'greater_equal', 'less_than', 'less_equal', 'is_null', 'is_not_null'] - def filter_is_true(self, query, value): + def make_date(self, value): """ - Filter data with an "is true" query (alias for "is not null"). + Convert user input to a proper ``datetime.date`` object. """ - return self.filter_is_not_null(query, value) + if value: + try: + dt = datetime.datetime.strptime(value, '%Y-%m-%d') + except ValueError: + log.warning("invalid date value: {}".format(value)) + else: + return dt.date() - def filter_is_false(self, query, value): + +class AlchemyDateTimeFilter(AlchemyDateFilter): + """ + SQLAlchemy filter for datetime values. + """ + + def filter_equal(self, query, value): """ - Filter data with an "is false" query (alias for "is null"). + Find all dateimes which fall on the given date. """ - return self.filter_is_null(query, value) + date = self.make_date(value) + if not date: + return query + + start = datetime.datetime.combine(date, datetime.time(0)) + start = make_utc(localtime(self.config, start)) + + stop = datetime.datetime.combine(date + datetime.timedelta(days=1), datetime.time(0)) + stop = make_utc(localtime(self.config, stop)) + + return query.filter(self.column >= start)\ + .filter(self.column < stop) + + def filter_not_equal(self, query, value): + """ + Find all dateimes which do *not* fall on the given date. + """ + date = self.make_date(value) + if not date: + return query + + start = datetime.datetime.combine(date, datetime.time(0)) + start = make_utc(localtime(self.config, start)) + + stop = datetime.datetime.combine(date + datetime.timedelta(days=1), datetime.time(0)) + stop = make_utc(localtime(self.config, stop)) + + return query.filter(sa.or_( + self.column < start, + self.column <= stop)) + + def filter_greater_than(self, query, value): + """ + Find all datetimes which fall after the given date. + """ + date = self.make_date(value) + if not date: + return query + + time = datetime.datetime.combine(date + datetime.timedelta(days=1), datetime.time(0)) + time = make_utc(localtime(self.config, time)) + return query.filter(self.column >= time) + + def filter_greater_equal(self, query, value): + """ + Find all datetimes which fall on or after the given date. + """ + date = self.make_date(value) + if not date: + return query + + time = datetime.datetime.combine(date, datetime.time(0)) + time = make_utc(localtime(self.config, time)) + return query.filter(self.column >= time) + + def filter_less_than(self, query, value): + """ + Find all datetimes which fall before the given date. + """ + date = self.make_date(value) + if not date: + return query + + time = datetime.datetime.combine(date, datetime.time(0)) + time = make_utc(localtime(self.config, time)) + return query.filter(self.column < time) + + def filter_less_equal(self, query, value): + """ + Find all datetimes which fall on or before the given date. + """ + date = self.make_date(value) + if not date: + return query + + time = datetime.datetime.combine(date + datetime.timedelta(days=1), datetime.time(0)) + time = make_utc(localtime(self.config, time)) + return query.filter(self.column < time) class AlchemyGPCFilter(AlchemyGridFilter):