[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.
--- 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')