[server] improve TZDatetime support
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Tue, 22 Dec 2015 15:35:10 +0100
changeset 11034 75d752e6daf7
parent 11033 63d860a14a17
child 11035 0fb100e8385b
[server] improve TZDatetime support by depending on logilab-database 1.15.0 including https://www.logilab.org/ticket/1485893, we can now keep the tzinfo attribute on datetime objects for TZDatetime attributes, so one knows that this is a tz-aware datetime. To easily make it work with backends that have no tz support, we keep converting tz-aware datetime objects into utc naive datetime objects before sending them to the database.
__pkginfo__.py
cubicweb.spec
debian/control
hooks/metadata.py
hooks/test/unittest_hooks.py
server/sqlutils.py
server/test/unittest_postgres.py
server/test/unittest_querier.py
sobjects/cwxmlparser.py
sobjects/test/unittest_cwxmlparser.py
--- a/__pkginfo__.py	Fri Dec 18 09:23:23 2015 +0100
+++ b/__pkginfo__.py	Tue Dec 22 15:35:10 2015 +0100
@@ -49,8 +49,9 @@
     'lxml': '',
     # XXX graphviz
     # server dependencies
-    'logilab-database': '>= 1.13.0',
+    'logilab-database': '>= 1.14.0',
     'passlib': '',
+    'pytz': '',
     'Markdown': ''
     }
 
--- a/cubicweb.spec	Fri Dec 18 09:23:23 2015 +0100
+++ b/cubicweb.spec	Tue Dec 22 15:35:10 2015 +0100
@@ -25,11 +25,12 @@
 Requires:       %{python}-logilab-mtconverter >= 0.8.0
 Requires:       %{python}-rql >= 0.31.2
 Requires:       %{python}-yams >= 0.41.1
-Requires:       %{python}-logilab-database >= 1.13.0
+Requires:       %{python}-logilab-database >= 1.14.0
 Requires:       %{python}-passlib
 Requires:       %{python}-lxml
 Requires:       %{python}-twisted-web
 Requires:       %{python}-markdown
+Requires:       %{python}-tz
 # the schema view uses `dot'; at least on el5, png output requires graphviz-gd
 Requires:       graphviz-gd
 Requires:       gettext
--- a/debian/control	Fri Dec 18 09:23:23 2015 +0100
+++ b/debian/control	Tue Dec 22 15:35:10 2015 +0100
@@ -16,6 +16,7 @@
  python-unittest2 | python (>= 2.7),
  python-logilab-mtconverter,
  python-markdown,
+ python-tz,
  python-rql,
  python-yams (>= 0.41.1),
  python-lxml,
@@ -53,7 +54,7 @@
  ${python:Depends},
  cubicweb-common (= ${source:Version}),
  cubicweb-ctl (= ${source:Version}),
- python-logilab-database (>= 1.13.0),
+ python-logilab-database (>= 1.14.0),
  cubicweb-postgresql-support
  | cubicweb-mysql-support
  | python-pysqlite2,
--- a/hooks/metadata.py	Fri Dec 18 09:23:23 2015 +0100
+++ b/hooks/metadata.py	Tue Dec 22 15:35:10 2015 +0100
@@ -22,6 +22,8 @@
 from datetime import datetime
 from base64 import b64encode
 
+from pytz import utc
+
 from cubicweb.predicates import is_instance
 from cubicweb.server import hook
 from cubicweb.server.edition import EditedEntity
@@ -41,7 +43,7 @@
     events = ('before_add_entity',)
 
     def __call__(self):
-        timestamp = datetime.utcnow()
+        timestamp = datetime.now(utc)
         edited = self.entity.cw_edited
         if not edited.get('creation_date'):
             edited['creation_date'] = timestamp
@@ -64,7 +66,7 @@
         # XXX to be really clean, we should turn off modification_date update
         # explicitly on each command where we do not want that behaviour.
         if not self._cw.vreg.config.repairing:
-            self.entity.cw_edited.setdefault('modification_date', datetime.utcnow())
+            self.entity.cw_edited.setdefault('modification_date', datetime.now(utc))
 
 
 class SetCreatorOp(hook.DataOperationMixIn, hook.Operation):
--- a/hooks/test/unittest_hooks.py	Fri Dec 18 09:23:23 2015 +0100
+++ b/hooks/test/unittest_hooks.py	Tue Dec 22 15:35:10 2015 +0100
@@ -26,6 +26,7 @@
 
 from six import text_type
 
+from pytz import utc
 from cubicweb import ValidationError, AuthenticationError, BadConnectionId
 from cubicweb.devtools.testlib import CubicWebTC
 
@@ -115,7 +116,7 @@
 
     def test_metadata_creation_modification_date(self):
         with self.admin_access.repo_cnx() as cnx:
-            _now = datetime.utcnow()
+            _now = datetime.now(utc)
             entity = cnx.create_entity('Workflow', name=u'wf1')
             self.assertEqual((entity.creation_date - _now).seconds, 0)
             self.assertEqual((entity.modification_date - _now).seconds, 0)
--- a/server/sqlutils.py	Fri Dec 18 09:23:23 2015 +0100
+++ b/server/sqlutils.py	Tue Dec 22 15:35:10 2015 +0100
@@ -30,6 +30,8 @@
 from six import string_types, text_type
 from six.moves import filter
 
+from pytz import utc
+
 from logilab import database as db, common as lgc
 from logilab.common.shellutils import ProgressBar, DummyProgressBar
 from logilab.common.deprecation import deprecated
@@ -503,6 +505,8 @@
                                 row[cellindex] = strptime(value, '%Y-%m-%d %H:%M:%S')
                             except Exception:
                                 row[cellindex] = strptime(value, '%Y-%m-%d')
+                            if vtype == 'TZDatetime':
+                                row[cellindex] = row[cellindex].replace(tzinfo=utc)
                         if vtype == 'Time' and isinstance(value, text_type):
                             found_date = True
                             try:
--- a/server/test/unittest_postgres.py	Fri Dec 18 09:23:23 2015 +0100
+++ b/server/test/unittest_postgres.py	Tue Dec 22 15:35:10 2015 +0100
@@ -127,20 +127,22 @@
                                              'WHERE X has_text "cubicweb"').rows,
                                   [[c1.eid,], [c3.eid,], [c2.eid,]])
 
-
     def test_tz_datetime(self):
         with self.admin_access.repo_cnx() as cnx:
-            cnx.execute("INSERT Personne X: X nom 'bob', X tzdatenaiss %(date)s",
-                        {'date': datetime(1977, 6, 7, 2, 0, tzinfo=FixedOffset(1))})
+            bob = cnx.create_entity('Personne', nom=u'bob',
+                                   tzdatenaiss=datetime(1977, 6, 7, 2, 0, tzinfo=FixedOffset(1)))
             datenaiss = cnx.execute("Any XD WHERE X nom 'bob', X tzdatenaiss XD")[0][0]
-            self.assertEqual(datenaiss.tzinfo, None)
+            self.assertIsNotNone(datenaiss.tzinfo)
             self.assertEqual(datenaiss.utctimetuple()[:5], (1977, 6, 7, 1, 0))
             cnx.commit()
-            cnx.execute("INSERT Personne X: X nom 'boby', X tzdatenaiss %(date)s",
-                        {'date': datetime(1977, 6, 7, 2, 0)})
+            cnx.create_entity('Personne', nom=u'boby',
+                              tzdatenaiss=datetime(1977, 6, 7, 2, 0))
             datenaiss = cnx.execute("Any XD WHERE X nom 'boby', X tzdatenaiss XD")[0][0]
-            self.assertEqual(datenaiss.tzinfo, None)
+            self.assertIsNotNone(datenaiss.tzinfo)
             self.assertEqual(datenaiss.utctimetuple()[:5], (1977, 6, 7, 2, 0))
+            rset = cnx.execute("Any X WHERE X tzdatenaiss %(d)s",
+                               {'d': datetime(1977, 6, 7, 2, 0, tzinfo=FixedOffset(1))})
+            self.assertEqual(rset.rows, [[bob.eid]])
 
     def test_constraint_validationerror(self):
         with self.admin_access.repo_cnx() as cnx:
--- a/server/test/unittest_querier.py	Fri Dec 18 09:23:23 2015 +0100
+++ b/server/test/unittest_querier.py	Tue Dec 22 15:35:10 2015 +0100
@@ -21,6 +21,8 @@
 
 from datetime import date, datetime, timedelta, tzinfo
 
+import pytz
+
 from six import PY2, integer_types, binary_type, text_type
 from logilab.common.testlib import TestCase, unittest_main
 from rql import BadRQLQuery, RQLSyntaxError
@@ -855,6 +857,7 @@
         self.assertIsInstance(rset[0][0], datetime)
         rset = self.qexecute('Any MAX(D) WHERE X is Personne, X tzdatenaiss D')
         self.assertIsInstance(rset[0][0], datetime)
+        self.assertEqual(rset[0][0].tzinfo, pytz.utc)
 
     def test_today(self):
         self.qexecute("INSERT Tag X: X name 'bidule', X creation_date TODAY")
@@ -1398,7 +1401,7 @@
         self.qexecute("INSERT Personne X: X nom 'bob', X tzdatenaiss %(date)s",
                      {'date': datetime(1977, 6, 7, 2, 0, tzinfo=FixedOffset(1))})
         datenaiss = self.qexecute("Any XD WHERE X nom 'bob', X tzdatenaiss XD")[0][0]
-        self.assertEqual(datenaiss.tzinfo, None)
+        self.assertIsNotNone(datenaiss.tzinfo)
         self.assertEqual(datenaiss.utctimetuple()[:5], (1977, 6, 7, 1, 0))
 
     def test_tz_datetime_cache_nonregr(self):
--- a/sobjects/cwxmlparser.py	Fri Dec 18 09:23:23 2015 +0100
+++ b/sobjects/cwxmlparser.py	Tue Dec 22 15:35:10 2015 +0100
@@ -37,6 +37,7 @@
 from six import text_type
 from six.moves.urllib.parse import urlparse, urlunparse, parse_qs, urlencode
 
+import pytz
 from logilab.common.date import todate, totime
 from logilab.common.textutils import splitstrip, text_to_dict
 from logilab.common.decorators import classproperty
@@ -65,7 +66,11 @@
 # XXX handle timezone, though this will be enough as TZDatetime are
 # serialized without time zone by default (UTC time). See
 # cw.web.views.xmlrss.SERIALIZERS.
-DEFAULT_CONVERTERS['TZDatetime'] = convert_datetime
+def convert_tzdatetime(ustr):
+    date = convert_datetime(ustr)
+    date = date.replace(tzinfo=pytz.utc)
+    return date
+DEFAULT_CONVERTERS['TZDatetime'] = convert_tzdatetime
 def convert_time(ustr):
     return totime(datetime.strptime(ustr, '%H:%M:%S'))
 DEFAULT_CONVERTERS['Time'] = convert_time
--- a/sobjects/test/unittest_cwxmlparser.py	Fri Dec 18 09:23:23 2015 +0100
+++ b/sobjects/test/unittest_cwxmlparser.py	Tue Dec 22 15:35:10 2015 +0100
@@ -20,6 +20,7 @@
 
 from six.moves.urllib.parse import urlsplit, parse_qsl
 
+import pytz
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.sobjects.cwxmlparser import CWEntityXMLParser
 
@@ -215,8 +216,8 @@
 
         with self.admin_access.web_request() as req:
             user = req.execute('CWUser X WHERE X login "sthenault"').get_entity(0, 0)
-            self.assertEqual(user.creation_date, datetime(2010, 1, 22, 10, 27, 59))
-            self.assertEqual(user.modification_date, datetime(2011, 1, 25, 14, 14, 6))
+            self.assertEqual(user.creation_date, datetime(2010, 1, 22, 10, 27, 59, tzinfo=pytz.utc))
+            self.assertEqual(user.modification_date, datetime(2011, 1, 25, 14, 14, 6, tzinfo=pytz.utc))
             self.assertEqual(user.cwuri, 'http://pouet.org/5')
             self.assertEqual(user.cw_source[0].name, 'myfeed')
             self.assertEqual(user.absolute_url(), 'http://pouet.org/5')
@@ -300,8 +301,8 @@
         with self.repo.internal_cnx() as cnx:
             stats = dfsource.pull_data(cnx, force=True, raise_on_error=True)
             user = cnx.execute('CWUser X WHERE X login "sthenault"').get_entity(0, 0)
-            self.assertEqual(user.creation_date, datetime(2010, 1, 22, 10, 27, 59))
-            self.assertEqual(user.modification_date, datetime(2011, 1, 25, 14, 14, 6))
+            self.assertEqual(user.creation_date, datetime(2010, 1, 22, 10, 27, 59, tzinfo=pytz.utc))
+            self.assertEqual(user.modification_date, datetime(2011, 1, 25, 14, 14, 6, tzinfo=pytz.utc))
             self.assertEqual(user.cwuri, 'http://pouet.org/5')
             self.assertEqual(user.cw_source[0].name, 'myfeed')