From e078d4b8e0a654151121f9a92e2719fc63c196af Mon Sep 17 00:00:00 2001 From: Jeffrey Finkelstein Date: Sat, 25 Mar 2017 00:50:33 -0400 Subject: [PATCH] Refresh SQLAlchemy object after creation in POST --- CHANGES | 4 ++++ flask_restless/views/resources.py | 13 +++++++++++ tests/test_creating.py | 33 ++++++++++++++++++++++++++- tests/test_updating.py | 37 ++++++++++++++++++++++++++++--- 4 files changed, 83 insertions(+), 4 deletions(-) diff --git a/CHANGES b/CHANGES index ce805c36..f000b893 100644 --- a/CHANGES +++ b/CHANGES @@ -48,6 +48,10 @@ Not yet released. - :issue:`599`: fixes `unicode` bug using :func:`!urlparse.urljoin` with the `future`_ library in resource serialization. - :issue:`625`: adds schema metadata to root endpoint. +- :issue:`630`: correctly respond with timezone-naive :class:`datetime` + attributes on :http:method:`post` requests when the database doesn't support + timezones, even if the request included a timezone-aware :class:`datetime` + attribute. .. _future: http://python-future.org/ diff --git a/flask_restless/views/resources.py b/flask_restless/views/resources.py index b259d232..60ba5cdb 100644 --- a/flask_restless/views/resources.py +++ b/flask_restless/views/resources.py @@ -478,6 +478,19 @@ def post(self): except self.validation_exceptions as exception: return self._handle_validation_exception(exception) only = self.sparse_fields.get(self.collection_name) + # Refresh the instance's attributes/relationships from the database. + # + # One place where we need this is when the request has a + # timezone-aware datetime attribute. In this case, the + # deserialized SQLAlchemy object actually seems to have the + # timezone-aware datetime attribute regardless of whether the + # underlying database has support for it. When added to the + # database however, the timezone is (silently) dropped if the + # database does not support it. By refreshing the object, we + # force the attribute to be reloaded from the database, thereby + # reloading a timezone-naive attribute into the SQLAlchemy + # object. + self.session.refresh(instance) # Get the dictionary representation of the new instance as it # appears in the database. try: diff --git a/tests/test_creating.py b/tests/test_creating.py index f8e6004f..bb49aa09 100644 --- a/tests/test_creating.py +++ b/tests/test_creating.py @@ -20,6 +20,8 @@ """ from __future__ import division +from datetime import timedelta +from datetime import timezone from datetime import datetime import dateutil @@ -77,7 +79,7 @@ class Person(self.Base): id = Column(Integer, primary_key=True) age = Column(Integer) name = Column(Unicode, unique=True) - birth_datetime = Column(DateTime, nullable=True) + birth_datetime = Column(DateTime(timezone=False), nullable=True) bedtime = Column(Time) hangtime = Column(Interval) articles = relationship('Article') @@ -885,6 +887,35 @@ def test_special_field_names(self): assert article['type'] == 'article' assert article['attributes']['type'] == u'fluff' + def test_timezone_aware_datetime(self): + """A timezone dropped by the DB should be reflected in the response. + + For more information, see GitHub issue #630. + + """ + # Request to create a person with a timezone-aware datetime attribute. + tz = timezone(timedelta(hours=-5)) + now = datetime.now(tz=tz) + data = { + 'data': { + 'type': 'person', + 'attributes': { + 'birth_datetime': now.isoformat() + } + } + } + response = self.app.post('/api/person', data=dumps(data)) + self.assertEqual(response.status_code, 201) + document = loads(response.data) + person = document['data'] + birth_datetime = person['attributes']['birth_datetime'] + birth_datetime = dateutil.parser.parse(birth_datetime) + # Our request had a timezone-aware attribute, but the database + # is timezone-naive so we expect that the database created a + # timezone-naive object. The returned resource should reflect + # that timezone-naive object. + self.assertEqual(birth_datetime, now.replace(tzinfo=None)) + class TestProcessors(ManagerTestBase): """Tests for pre- and postprocessors.""" diff --git a/tests/test_updating.py b/tests/test_updating.py index 542bba22..8fd3b4f7 100644 --- a/tests/test_updating.py +++ b/tests/test_updating.py @@ -21,7 +21,8 @@ from __future__ import division from datetime import datetime -from unittest2 import skip +from datetime import timedelta +from datetime import timezone try: from flask_sqlalchemy import SQLAlchemy @@ -37,7 +38,6 @@ from sqlalchemy import Integer from sqlalchemy import Time from sqlalchemy import Unicode -from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.orm import backref from sqlalchemy.orm import relationship @@ -82,7 +82,7 @@ class Person(self.Base): name = Column(Unicode, unique=True) bedtime = Column(Time) date_created = Column(Date) - birth_datetime = Column(DateTime) + birth_datetime = Column(DateTime(timezone=False)) def foo(self): return u'foo' @@ -961,6 +961,37 @@ def test_integer_id_error_message(self): check_sole_error(response, 409, ['"id" element', 'resource object', 'must be a JSON string']) + def test_timezone_aware_datetime(self): + """Test that timezone information is correctly dropped. + + For more information, see GitHub issue #630. + + """ + # Create a person with a timezone-naive datetime attribute. + now = datetime.now() + person = self.Person(id=1, birth_datetime=now) + self.session.add(person) + self.session.commit() + # Request to update the attribute with a timezone-aware datetime. + tz = timezone(timedelta(hours=-5)) + later = datetime.now(tz=tz) + data = { + 'data': { + 'id': '1', + 'type': 'person', + 'attributes': { + 'birth_datetime': later.isoformat() + } + } + } + response = self.app.patch('/api/person/1', data=dumps(data)) + self.assertEqual(response.status_code, 204) + # Our request had a timezone-aware attribute, but the database + # is timezone-naive so we expect that the database created a + # timezone-naive object. The returned resource should reflect + # that timezone-naive object. + self.assertEqual(person.birth_datetime, later.replace(tzinfo=None)) + class TestProcessors(ManagerTestBase): """Tests for pre- and postprocessors."""