diff --git a/flask_restless/helpers.py b/flask_restless/helpers.py index b81f9246..4a35e8a0 100644 --- a/flask_restless/helpers.py +++ b/flask_restless/helpers.py @@ -562,7 +562,11 @@ def strings_to_dates(model, dictionary): elif value in CURRENT_TIME_MARKERS: result[fieldname] = getattr(func, value.lower())() else: - result[fieldname] = parse_datetime(value) + fieldtype = get_field_type(model, fieldname) + if isinstance(fieldtype, Date): + result[fieldname] = parse_datetime(value).date() + else: + result[fieldname] = parse_datetime(value) elif (is_interval_field(model, fieldname) and value is not None and isinstance(value, int)): result[fieldname] = datetime.timedelta(seconds=value) diff --git a/flask_restless/views.py b/flask_restless/views.py index 4aaba9da..b1c5165f 100644 --- a/flask_restless/views.py +++ b/flask_restless/views.py @@ -43,6 +43,8 @@ from sqlalchemy.orm.exc import MultipleResultsFound from sqlalchemy.orm.exc import NoResultFound from sqlalchemy.orm.query import Query +from sqlalchemy.orm.attributes import InstrumentedAttribute +from sqlalchemy.ext.associationproxy import AssociationProxy from werkzeug.exceptions import BadRequest from werkzeug.exceptions import HTTPException from werkzeug.urls import url_quote_plus @@ -63,6 +65,7 @@ from .helpers import strings_to_dates from .helpers import to_dict from .helpers import upper_keys +from .helpers import get_related_association_proxy_model from .search import create_query from .search import search @@ -1139,6 +1142,31 @@ def _search(self): for preprocessor in self.preprocessors['GET_MANY']: preprocessor(search_params=search_params) + # resolve date-strings as required by the model + for param in search_params.get('filters', list()): + if 'name' in param and 'val' in param: + query_model = self.model + query_field = param['name'] + + if '__' in param['name']: + fieldname, relation = param['name'].split('__') + submodel = getattr(self.model, fieldname) + if isinstance(submodel, InstrumentedAttribute): + query_model = submodel.property.mapper.class_ + query_field = relation + elif isinstance(submodel, AssociationProxy): + query_model = get_related_association_proxy_model(submodel) + query_field = relation + try: + result = strings_to_dates( + query_model, + {query_field: param['val']} + ) + param['val'] = result.get(query_field) + except ValueError as exception: + current_app.logger.exception(str(exception)) + return dict(message='Unable to construct query'), 400 + # perform a filtered search try: result = search(self.session, self.model, search_params) diff --git a/tests/test_views.py b/tests/test_views.py index 3d461917..82a25ad4 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -1878,6 +1878,36 @@ def test_search2(self): assert resp.status_code == 400 assert loads(resp.data)['message'] == 'Multiple results found' + def test_search_dates(self): + """Test date parsing""" + # Lincoln has been allocated a birthday of 1900-01-02. + # We'll ask for dates in a variety of formats, including invalid ones. + search = { + 'single': True, + 'filters': [{'name': 'birth_date', 'op': 'eq'}] + } + + # 1900-01-02 + search['filters'][0]['val'] = '1900-01-02' + resp = self.app.search('/api/person', dumps(search)) + assert loads(resp.data)['name'] == u'Lincoln' + + # 2nd Jan 1900 + search['filters'][0]['val'] = '2nd Jan 1900' + resp = self.app.search('/api/person', dumps(search)) + assert loads(resp.data)['name'] == u'Lincoln' + + # Invalid Date + search['filters'][0]['val'] = 'REALLY-BAD-DATE' + resp = self.app.search('/api/person', dumps(search)) + assert resp.status_code == 400 + + # DateTime + # This will be cropped to a date, since birth_date is a Date column + search['filters'][0]['val'] = '2nd Jan 1900 14:35' + resp = self.app.search('/api/person', dumps(search)) + assert loads(resp.data)['name'] == u'Lincoln' + def test_search_boolean_formula(self): """Tests for Boolean formulas of filters in a search query.""" # This searches for people whose name is John, or people older than age