[forms] Fix edition of TZDatetime attributes
authorFlorent Cayré <florent.cayre@logilab.fr>
Wed, 04 May 2016 17:07:41 +0200
changeset 11240 1694e6e9ff94
parent 11239 19cacea03fde
child 11241 a2091fa8cb2c
[forms] Fix edition of TZDatetime attributes Parse the submitted web form values into a timezone-aware datetime (by default, UTC) when such an attribute is edited. This is done using an automatically-selected (by `web.formfield.guess_field`) field named `TZDatetimeField`. Closes #12666598.
cubicweb/web/formfields.py
cubicweb/web/test/data/schema.py
cubicweb/web/test/unittest_form.py
cubicweb/web/test/unittest_magicsearch.py
cubicweb/web/test/unittest_views_editforms.py
--- a/cubicweb/web/formfields.py	Mon May 02 17:14:43 2016 +0200
+++ b/cubicweb/web/formfields.py	Wed May 04 17:07:41 2016 +0200
@@ -42,6 +42,7 @@
 .. autoclass:: cubicweb.web.formfields.BooleanField()
 .. autoclass:: cubicweb.web.formfields.DateField()
 .. autoclass:: cubicweb.web.formfields.DateTimeField()
+.. autoclass:: cubicweb.web.formfields.TZDatetimeField()
 .. autoclass:: cubicweb.web.formfields.TimeField()
 .. autoclass:: cubicweb.web.formfields.TimeIntervalField()
 
@@ -65,6 +66,8 @@
 
 from datetime import datetime, timedelta
 
+import pytz
+
 from six import PY2, text_type, string_types
 
 from logilab.mtconverter import xml_escape
@@ -1015,6 +1018,18 @@
     etype = 'Datetime'
 
 
+class TZDatetimeField(DateTimeField):
+    """ Use this field to edit a timezone-aware datetime (`TZDatetime` yams
+    type). Note the posted values are interpreted as UTC, so you may need to
+    convert them client-side, using some javascript in the corresponding widget.
+    """
+
+    def _ensure_correctly_typed(self, form, value):
+        tz_naive = super(TZDatetimeField, self)._ensure_correctly_typed(
+            form, value)
+        return tz_naive.replace(tzinfo=pytz.utc)
+
+
 class TimeField(DateField):
     """Use this field to edit time (`Time` yams type).
 
@@ -1267,7 +1282,7 @@
 
     'Date':       DateField,
     'Datetime':   DateTimeField,
-    'TZDatetime': DateTimeField,
+    'TZDatetime': TZDatetimeField,
     'Time':       TimeField,
     'TZTime':     TimeField,
     'Interval':   TimeIntervalField,
--- a/cubicweb/web/test/data/schema.py	Mon May 02 17:14:43 2016 +0200
+++ b/cubicweb/web/test/data/schema.py	Wed May 04 17:07:41 2016 +0200
@@ -17,7 +17,7 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 
 from yams.buildobjs import (EntityType, RelationDefinition, SubjectRelation,
-                            String, Int, Datetime, Boolean, Float)
+                            String, Int, Datetime, Boolean, Float, TZDatetime)
 from yams.constraints import IntervalBoundConstraint
 
 from cubicweb import _
@@ -58,6 +58,7 @@
     tel    = Int()
     fax    = Int()
     datenaiss = Datetime()
+    tzdatenaiss = TZDatetime()
     test   = Boolean()
     description = String()
     salary = Float()
--- a/cubicweb/web/test/unittest_form.py	Mon May 02 17:14:43 2016 +0200
+++ b/cubicweb/web/test/unittest_form.py	Wed May 04 17:07:41 2016 +0200
@@ -17,6 +17,8 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 
 import time
+from datetime import datetime
+import pytz
 
 from xml.etree.ElementTree import fromstring
 from lxml import html
@@ -30,7 +32,8 @@
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.web.formfields import (IntField, StringField, RichTextField,
                                      PasswordField, DateTimeField,
-                                     FileField, EditableFileField)
+                                     FileField, EditableFileField,
+                                     TZDatetimeField)
 from cubicweb.web.formwidgets import PasswordInput, Input, DateTimePicker
 from cubicweb.web.views.forms import EntityFieldsForm, FieldsForm
 from cubicweb.web.views.workflow import ChangeStateForm
@@ -260,6 +263,31 @@
 <p><b>You can either submit a new file using the browse button above, or choose to remove already uploaded file by checking the "detach attached file" check-box, or edit file content online with the widget below.</b></p>
 <textarea cols="80" name="data-subject:%(eid)s" onkeyup="autogrow(this)" rows="3" tabindex="4">new widgets system</textarea>''' % {'eid': file.eid})
 
+    def _modified_tzdatenaiss(self, eid, datestr, timestr):
+        ctx = {'tzdatenaiss-subjectdate:%d' % eid: datestr,
+               'tzdatenaiss-subjecttime:%d' % eid: timestr}
+        with self.admin_access.web_request(**ctx) as req:
+            form = EntityFieldsForm(req, None, entity=req.entity_from_eid(eid))
+            field = TZDatetimeField(name='tzdatenaiss', eidparam=True,
+                                    role='subject')
+            form.append_field(field)
+            form.build_context({})
+            return field.has_been_modified(form)
+
+    def test_tzdatetimefield(self):
+        """ Comparison of the tz-aware database-stored value and the posted data
+        should not crash, and the posted data should be considered UTC """
+        tzd = datetime.now(pytz.utc).replace(second=0, microsecond=0)
+        datestr, timestr = tzd.strftime('%Y/%m/%d %H:%M').split()
+        with self.admin_access.web_request() as req:
+            eid = req.create_entity('Personne', nom=u'Flo', tzdatenaiss=tzd).eid
+            req.cnx.commit()
+
+        modified = self._modified_tzdatenaiss(eid, datestr, timestr)
+        self.assertFalse(modified)
+
+        modified = self._modified_tzdatenaiss(eid, '2016/05/04', '15:07')
+        self.assertTrue(modified)
 
     def test_passwordfield(self):
         class PFForm(EntityFieldsForm):
--- a/cubicweb/web/test/unittest_magicsearch.py	Mon May 02 17:14:43 2016 +0200
+++ b/cubicweb/web/test/unittest_magicsearch.py	Wed May 04 17:07:41 2016 +0200
@@ -293,6 +293,7 @@
                               'Any X WHERE X is Personne, X test A',
                               'Any X WHERE X is Personne, X titre A',
                               'Any X WHERE X is Personne, X travaille A',
+                              'Any X WHERE X is Personne, X tzdatenaiss A',
                               'Any X WHERE X is Personne, X web A',
                               ],
                              self.suggestions('Any X WHERE X is Personne, X '))
@@ -300,6 +301,7 @@
                               'Any X WHERE X is Personne, X test A',
                               'Any X WHERE X is Personne, X titre A',
                               'Any X WHERE X is Personne, X travaille A',
+                              'Any X WHERE X is Personne, X tzdatenaiss A',
                               ],
                              self.suggestions('Any X WHERE X is Personne, X t'))
         # try completion on selected
@@ -307,6 +309,7 @@
                               'Any X WHERE X is Personne, Y is Societe, X test A',
                               'Any X WHERE X is Personne, Y is Societe, X titre A',
                               'Any X WHERE X is Personne, Y is Societe, X travaille Y',
+                              'Any X WHERE X is Personne, Y is Societe, X tzdatenaiss A',
                               ],
                              self.suggestions('Any X WHERE X is Personne, Y is Societe, X t'))
         # invalid relation should not break
--- a/cubicweb/web/test/unittest_views_editforms.py	Mon May 02 17:14:43 2016 +0200
+++ b/cubicweb/web/test/unittest_views_editforms.py	Wed May 04 17:07:41 2016 +0200
@@ -121,6 +121,7 @@
                                    ('tel', 'subject'),
                                    ('fax', 'subject'),
                                    ('datenaiss', 'subject'),
+                                   ('tzdatenaiss', 'subject'),
                                    ('test', 'subject'),
                                    ('description', 'subject'),
                                    ('salary', 'subject'),