From e2535554b1ebb98cd37723f9cff2673c7a2b275a Mon Sep 17 00:00:00 2001
From: Kazuhiko Shiozaki <kazuhiko@nexedi.com>
Date: Mon, 18 Sep 2017 11:24:23 +0200
Subject: [PATCH] Formulator: prepare DateTimeField's sub_form dynamically.

otherwise TALES in input_style does not work and changes in the original proxy field will not be reflected.
---
 product/ERP5Form/ProxyField.py       |   8 +-
 product/ERP5Form/tests/testFields.py |  60 +++++----------
 product/Formulator/Field.py          |   6 +-
 product/Formulator/StandardFields.py | 110 ++++++++++++++-------------
 product/Formulator/Widget.py         |   4 -
 product/Formulator/XMLToForm.py      |   5 --
 product/Formulator/tests/testForm.py |  27 +++++--
 7 files changed, 105 insertions(+), 115 deletions(-)

diff --git a/product/ERP5Form/ProxyField.py b/product/ERP5Form/ProxyField.py
index 961578200c..240baa58f8 100644
--- a/product/ERP5Form/ProxyField.py
+++ b/product/ERP5Form/ProxyField.py
@@ -493,10 +493,10 @@ class ProxyField(ZMIField):
       field = template_field
     return template_field
 
-  def _aq_dynamic(self, name):
-    if name == 'sub_form':
-      return self.getTemplateField().sub_form
-    return None
+  def _get_sub_form(self, field=None):
+    if field is None:
+      field = self
+    return self.getTemplateField()._get_sub_form(field=field)
 
   security.declareProtected('Access contents information',
                             'is_delegated')
diff --git a/product/ERP5Form/tests/testFields.py b/product/ERP5Form/tests/testFields.py
index 57933de1a7..05fb91ea1e 100644
--- a/product/ERP5Form/tests/testFields.py
+++ b/product/ERP5Form/tests/testFields.py
@@ -764,14 +764,31 @@ class TestProxyField(ERP5TypeTestCase):
                                 'my_date', 'Date', 'ProxyField')
     proxy_field.manage_edit_xmlrpc(dict(form_id='Base_viewProxyFieldLibrary',
                                         field_id='my_date',))
-    self.assertTrue(hasattr(proxy_field, 'sub_form'))
-    self.assertTrue(aq_base(proxy_field.sub_form) is
-                      aq_base(original_field.sub_form))
+    self.assertTrue(hasattr(proxy_field, '_get_sub_form'))
+    self.assertEqual(proxy_field._get_sub_form().render(),
+                     original_field._get_sub_form().render())
+
     # we can render
     proxy_field.render()
     # and validate
     self.container.Base_view.validate_all_to_request(self.portal.REQUEST)
 
+    # change style in the original field
+    original_field.manage_edit_xmlrpc(dict(input_style='number'))
+    self.assertTrue('type="number"' in original_field.render())
+    self.assertTrue('type="number"' in proxy_field.render())
+
+    # override style in the proxy field
+    original_field.manage_edit_xmlrpc(dict(input_style='text'))
+    proxy_field._surcharged_edit({'input_style': 'number'}, ['input_style'])
+    self.assertTrue('type="text"' in original_field.render())
+    self.assertTrue('type="number"' in proxy_field.render())
+
+    # unproxify the proxy field
+    self.container.Base_view.unProxifyField({'my_date': 'on'})
+    unproxified_field = self.container.Base_view.my_date
+    self.assertTrue('type="number"' in unproxified_field.render())
+
   def test_manage_edit_surcharged_xmlrpc(self):
     # manage_edit_surcharged_xmlrpc is a method to edit proxyfields
     # programmatically
@@ -942,7 +959,7 @@ class TestFieldValueCache(ERP5TypeTestCase):
     addField(DateTimeField('datetime_field'))
     form.datetime_field._p_oid = makeDummyOid()
     form.datetime_field._edit(dict(input_style='list'))
-    for i in form.datetime_field.sub_form.fields.values():
+    for i in form.datetime_field._get_sub_form().fields.values():
       i._p_oid = makeDummyOid()
 
   def test_method_field(self):
@@ -987,41 +1004,6 @@ class TestFieldValueCache(ERP5TypeTestCase):
     self.root.form.proxy_field_tales.get_value('title')
     self.assertEqual(True, cache_size == self._getCacheSize('ProxyField.get_value'))
 
-  def test_datetime_field(self):
-    field_value_cache.clear()
-
-    # make sure that boundmethod must not be cached.
-    year_field = self.root.form.datetime_field.sub_form.get_field('year', include_disabled=1)
-    self.assertEqual(True, type(year_field.overrides['items']) is BoundMethod)
-
-    cache_size = len(field_value_cache)
-    year_field.get_value('items')
-
-    # See Formulator/StandardFields.py(line:174)
-    # there are two get_value, start_datetime and end_datetime
-    cache_size += 2
-
-    # make sure that boundmethod is not cached(cache size does not change)
-    self.assertEqual(True, ('Form.get_value',
-                            self.root.form.datetime_field._p_oid,
-                            self.root.form.datetime_field._p_oid,
-                            'start_datetime'
-                            ) in field_value_cache)
-    self.assertEqual(True, ('Form.get_value',
-                            self.root.form.datetime_field._p_oid,
-                            self.root.form.datetime_field._p_oid,
-                            'end_datetime'
-                            ) in field_value_cache)
-    self.assertEqual(False, ('Form.get_value',
-                            year_field._p_oid,
-                            year_field._p_oid,
-                            'items'
-                            ) in field_value_cache)
-    self.assertEqual(cache_size, len(field_value_cache))
-
-    year_field.get_value('size')
-    year_field.get_value('default')
-    self.assertEqual(cache_size+2, len(field_value_cache))
 
 def makeDummyOid():
   import time, random
diff --git a/product/Formulator/Field.py b/product/Formulator/Field.py
index 546ca4a271..ab0f583e26 100644
--- a/product/Formulator/Field.py
+++ b/product/Formulator/Field.py
@@ -403,7 +403,7 @@ class Field:
       a form. Works like render() but for sub field.
           Added key parameter for ERP5 in order to be compatible with listbox/matrixbox
       """
-      return self.sub_form.get_field(id)._render_helper(
+      return self._get_sub_form().get_field(id)._render_helper(
           self.generate_subfield_key(id, key=key), value, REQUEST, render_prefix)
 
     security.declareProtected('View', 'render_sub_field_from_request')
@@ -411,7 +411,7 @@ class Field:
         """Convenience method; render the field widget from REQUEST
         (unvalidated data), or default if no raw data is found.
         """
-        return self.sub_form.get_field(id)._render_helper(
+        return self._get_sub_form().get_field(id)._render_helper(
             self.generate_subfield_key(id), None, REQUEST)
 
     security.declarePrivate('_validate_helper')
@@ -441,7 +441,7 @@ class Field:
     def validate_sub_field(self, id, REQUEST, key=None):
       """Validates a subfield (as part of field validation).
       """
-      return self.sub_form.get_field(id)._validate_helper(
+      return self._get_sub_form().get_field(id)._validate_helper(
       self.generate_subfield_key(id, validation=1, key=key), REQUEST)
 
     def PrincipiaSearchSource(self):
diff --git a/product/Formulator/StandardFields.py b/product/Formulator/StandardFields.py
index 10cda57915..b803087578 100644
--- a/product/Formulator/StandardFields.py
+++ b/product/Formulator/StandardFields.py
@@ -127,60 +127,19 @@ class DateTimeField(ZMIField):
   widget = Widget.DateTimeWidgetInstance
   validator = Validator.DateTimeValidatorInstance
 
-  def __init__(self, id, **kw):
-    # icky but necessary...
-    apply(ZMIField.__init__, (self, id), kw)
-
-    input_style = self.get_value('input_style')
+  def _get_sub_form(self, field=None):
+    if field is None:
+      field = self
+    input_style = field.get_value('input_style')
     if input_style == 'text':
-      self.sub_form = create_datetime_text_sub_form()
+      return create_datetime_text_sub_form(self)
     elif input_style == 'list':
-      self.sub_form = create_datetime_list_sub_form()
+      return create_datetime_list_sub_form(self)
     elif input_style == 'number':
-      self.sub_form = create_datetime_number_sub_form()
+      return create_datetime_number_sub_form(self)
     else:
       assert 0, "Unknown input_style '%s'" % input_style
 
-  def on_value_input_style_changed(self, value):
-    if value == 'text':
-      self.sub_form = create_datetime_text_sub_form()
-    elif value == 'list':
-      self.sub_form = create_datetime_list_sub_form()
-      year_field = self.sub_form.get_field('year', include_disabled=1)
-      year_field.overrides['items'] = BoundMethod(self,
-                                                  '_override_year_items')
-    elif value == 'number':
-      self.sub_form = create_datetime_number_sub_form()
-    else:
-      assert 0, "Unknown input_style."
-    self.on_value_css_class_changed(self.values['css_class'])
-
-  def on_value_timezone_style_changed(self, value):
-    if value:
-      input_style = self.get_value('input_style')
-      self.on_value_input_style_changed(input_style)
-
-  def on_value_css_class_changed(self, value):
-    for field in self.sub_form.get_fields():
-      field.values['css_class'] = value
-      field._p_changed = 1
-
-  def _override_year_items(self):
-    """The method gets called to get the right amount of years.
-    """
-    start_datetime = self.get_value('start_datetime')
-    end_datetime = self.get_value('end_datetime')
-    current_year = DateTime().year()
-    if start_datetime:
-      first_year = start_datetime.year()
-    else:
-      first_year = current_year
-    if end_datetime:
-      last_year = end_datetime.year() + 1
-    else:
-      last_year = first_year + 11
-    return create_items(first_year, last_year, digits=4)
-
   def _get_user_input_value(self, key, REQUEST):
     """
     Try to get a value of the field from the REQUEST
@@ -188,12 +147,14 @@ class DateTimeField(ZMIField):
     if REQUEST.form['subfield_%s_%s' % (key, 'year')]:
       return None
 
-def create_datetime_text_sub_form():
+def create_datetime_text_sub_form(field):
   sub_form = BasicForm()
+  css_class = field.get_value('css_class')
 
   year = IntegerField('year',
                       title="Year",
                       required=0,
+                      css_class=css_class,
                       display_width=4,
                       display_maxwidth=4,
                       max_length=4)
@@ -201,6 +162,7 @@ def create_datetime_text_sub_form():
   month = IntegerField('month',
                         title="Month",
                         required=0,
+                        css_class=css_class,
                         display_width=2,
                         display_maxwidth=2,
                         max_length=2)
@@ -208,6 +170,7 @@ def create_datetime_text_sub_form():
   day = IntegerField('day',
                       title="Day",
                       required=0,
+                      css_class=css_class,
                       display_width=2,
                       display_maxwidth=2,
                       max_length=2)
@@ -217,6 +180,7 @@ def create_datetime_text_sub_form():
   hour = IntegerField('hour',
                       title="Hour",
                       required=0,
+                      css_class=css_class,
                       display_width=2,
                       display_maxwidth=2,
                       max_length=2)
@@ -224,6 +188,7 @@ def create_datetime_text_sub_form():
   minute = IntegerField('minute',
                         title="Minute",
                         required=0,
+                        css_class=css_class,
                         display_width=2,
                         display_maxwidth=2,
                         max_length=2)
@@ -231,32 +196,48 @@ def create_datetime_text_sub_form():
   ampm = StringField('ampm',
                       title="am/pm",
                       required=0,
+                      css_class=css_class,
                       display_width=2,
                       display_maxwidth=2,
                       max_length=2)
   timezone = ListField('timezone',
                         title = "Timezone",
                         required = 0,
+                        css_class=css_class,
                         default = 'GMT',
                         items = Widget.gmt_timezones,
                         size = 1)
   sub_form.add_fields([hour, minute, ampm, timezone], "time")
   return sub_form
 
-def create_datetime_list_sub_form():
+def create_datetime_list_sub_form(field):
   """ Patch Products.Formulator.StandardFields so we can add timezone subfield """
   sub_form = BasicForm()
+  css_class = field.get_value('css_class')
+  start_datetime = field.get_value('start_datetime')
+  end_datetime = field.get_value('end_datetime')
+  current_year = DateTime().year()
+  if start_datetime:
+    first_year = start_datetime.year()
+  else:
+    first_year = current_year
+  if end_datetime:
+    last_year = end_datetime.year() + 1
+  else:
+    last_year = first_year + 11
 
   year = ListField('year',
                     title="Year",
                     required=0,
+                    css_class=css_class,
                     default="",
-                    items=create_items(2000, 2010, digits=4),
+                    items=create_items(first_year, last_year, digits=4),
                     size=1)
 
   month = ListField('month',
                     title="Month",
                     required=0,
+                    css_class=css_class,
                     default="",
                     items=create_items(1, 13, digits=2),
                     size=1)
@@ -264,6 +245,7 @@ def create_datetime_list_sub_form():
   day = ListField('day',
                   title="Day",
                   required=0,
+                  css_class=css_class,
                   default="",
                   items=create_items(1, 32, digits=2),
                   size=1)
@@ -274,6 +256,7 @@ def create_datetime_list_sub_form():
   hour = IntegerField('hour',
                       title="Hour",
                       required=0,
+                      css_class=css_class,
                       display_width=2,
                       display_maxwidth=2,
                       max_length=2)
@@ -281,6 +264,7 @@ def create_datetime_list_sub_form():
   minute = IntegerField('minute',
                         title="Minute",
                         required=0,
+                        css_class=css_class,
                         display_width=2,
                         display_maxwidth=2,
                         max_length=2)
@@ -288,6 +272,7 @@ def create_datetime_list_sub_form():
   ampm = ListField('ampm',
                     title="am/pm",
                     required=0,
+                    css_class=css_class,
                     default="am",
                     items=[("am","am"),
                           ("pm","pm")],
@@ -295,6 +280,7 @@ def create_datetime_list_sub_form():
   timezone = ListField('timezone',
                         title = "Timezone",
                         required = 0,
+                        css_class=css_class,
                         default = 'GMT',
                         items = Widget.gmt_timezones,
                         size = 1)
@@ -303,14 +289,26 @@ def create_datetime_list_sub_form():
   sub_form.add_fields([hour, minute, ampm, timezone], "time")
   return sub_form
 
-def create_datetime_number_sub_form():
+def create_datetime_number_sub_form(field):
   sub_form = BasicForm()
+  css_class = field.get_value('css_class')
+  start_datetime = field.get_value('start_datetime')
+  end_datetime = field.get_value('end_datetime')
+  if start_datetime:
+    first_year = start_datetime.year()
+  else:
+    first_year = 0
+  if end_datetime:
+    last_year = end_datetime.year()
+  else:
+    last_year = 9999
 
   year = IntegerField('year',
                       title="Year",
                       required=0,
                       input_type='number',
-                      extra='min="0" max="9999"',
+                      css_class=css_class,
+                      extra='min="%s" max="%s"' % (first_year, last_year),
                       display_width=4,
                       display_maxwidth=4,
                       max_length=4)
@@ -319,6 +317,7 @@ def create_datetime_number_sub_form():
                         title="Month",
                         required=0,
                         input_type='number',
+                        css_class=css_class,
                         extra='min="1" max="12"',
                         display_width=2,
                         display_maxwidth=2,
@@ -327,6 +326,7 @@ def create_datetime_number_sub_form():
   day = IntegerField('day',
                       title="Day",
                       required=0,
+                      css_class=css_class,
                       input_type='number',
                       extra='min="1" max="31"',
                       display_width=2,
@@ -338,6 +338,7 @@ def create_datetime_number_sub_form():
   hour = IntegerField('hour',
                       title="Hour",
                       required=0,
+                      css_class=css_class,
                       input_type='number',
                       extra='min="0" max="23"',
                       display_width=2,
@@ -347,6 +348,7 @@ def create_datetime_number_sub_form():
   minute = IntegerField('minute',
                         title="Minute",
                         required=0,
+                        css_class=css_class,
                         input_type='number',
                         extra='min="0" max="59"',
                         display_width=2,
@@ -356,12 +358,14 @@ def create_datetime_number_sub_form():
   ampm = StringField('ampm',
                       title="am/pm",
                       required=0,
+                      css_class=css_class,
                       display_width=2,
                       display_maxwidth=2,
                       max_length=2)
   timezone = ListField('timezone',
                         title = "Timezone",
                         required = 0,
+                        css_class=css_class,
                         default = 'GMT',
                         items = Widget.gmt_timezones,
                         size = 1)
diff --git a/product/Formulator/Widget.py b/product/Formulator/Widget.py
index 7e675ebe9e..1088dc427f 100644
--- a/product/Formulator/Widget.py
+++ b/product/Formulator/Widget.py
@@ -1455,10 +1455,6 @@ class DateTimeWidget(Widget):
   def render(self, field, key, value, REQUEST, render_prefix=None):
     use_ampm = field.get_value('ampm_time_style')
     use_timezone = field.get_value('timezone_style')
-    # FIXME: backwards compatibility hack:
-    if not hasattr(field, 'sub_form'):
-      from StandardFields import create_datetime_text_sub_form
-      field.sub_form = create_datetime_text_sub_form()
 
     # Is it still usefull to test the None value,
     # as DateTimeField should be considerer as the other field
diff --git a/product/Formulator/XMLToForm.py b/product/Formulator/XMLToForm.py
index 08d9303018..68d0c723cd 100644
--- a/product/Formulator/XMLToForm.py
+++ b/product/Formulator/XMLToForm.py
@@ -91,11 +91,6 @@ def XMLToForm(s, form, override_encoding=None):
                 else:
                     field.values[name] = encode(value.text, encoding)
 
-            # special hack for the DateTimeField
-            if field.meta_type=='DateTimeField':
-                field.on_value_input_style_changed(
-                    field.get_value('input_style'))
-
             # set tales
             tales = entry.first.tales
             for name in tales.getElementNames():
diff --git a/product/Formulator/tests/testForm.py b/product/Formulator/tests/testForm.py
index aa054a859d..efd8893721 100644
--- a/product/Formulator/tests/testForm.py
+++ b/product/Formulator/tests/testForm.py
@@ -1,5 +1,6 @@
 # -*- coding: utf-8 -*-
 import unittest, re
+from DateTime import DateTime
 import Zope2
 
 # XXX this does not work for zope2.x if x < 3
@@ -120,14 +121,10 @@ class FormTestCase(unittest.TestCase):
         self.form.manage_addProduct['Formulator']\
                  .manage_addField('date_time','Test Field','DateTimeField')
         field = self.form.date_time
-        sub_form = field.sub_form
-        if sub_form.has_field('timezone'):
-          del sub_form.fields['timezone']
-        #now timezone is not presented
-        self.assertFalse(self.form.date_time.sub_form.has_field('timezone'))
+        field._edit({'timezone_style': 0})
+        self.assertFalse('<select size="1" name="subfield_field_date_time_timezone" >' in field.render())
         field._edit({'timezone_style': 1})
-        #test if timezone's presented
-        self.assertTrue(self.form.date_time.sub_form.has_field('timezone'))
+        self.assertTrue('<select size="1" name="subfield_field_date_time_timezone" >' in field.render())
 
 
     def test_datetime_css_class_rendering(self):
@@ -177,6 +174,22 @@ class FormTestCase(unittest.TestCase):
         self.assertEqual(0, len(css_matches))
 
 
+    def test_datetime_number_style(self):
+        # test that a bug is fixed, which causing the css_class value
+        # not to be rendered
+        self.form.manage_addProduct['Formulator']\
+                 .manage_addField('date_time','Test Field','DateTimeField')
+        field = self.form.date_time
+        field._edit({'input_style': 'number'})
+        self.assertTrue('<input name="subfield_field_date_time_year" value="" maxlength="4" type="number" size="4" min="0" max="9999" />' in field.render())
+        field._edit({'start_datetime': DateTime('1900/01/01'), 'end_datetime': None})
+        self.assertTrue('<input name="subfield_field_date_time_year" value="" maxlength="4" type="number" size="4" min="1900" max="9999" />' in field.render())
+        field._edit({'start_datetime': None, 'end_datetime': DateTime('2099/12/31')})
+        self.assertTrue('<input name="subfield_field_date_time_year" value="" maxlength="4" type="number" size="4" min="0" max="2099" />' in field.render())
+        field._edit({'start_datetime': DateTime('1900/01/01'), 'end_datetime': DateTime('2099/12/31')})
+        self.assertTrue('<input name="subfield_field_date_time_year" value="" maxlength="4" type="number" size="4" min="1900" max="2099" />' in field.render())
+
+
 def test_suite():
     suite = unittest.TestSuite()
 
-- 
2.30.9