changeset 8285:2bf0c4e7795e

fix: issue2551390 - Replace text input/calendar popup with native date input Docs, code and test changes for the changeover to a native date element. See issue for details.
author John Rouillard <rouilj@ieee.org>
date Sat, 18 Jan 2025 12:23:23 -0500
parents 92dad05379f9
children 6445e63bb423
files CHANGES.txt doc/customizing.txt doc/reference.txt doc/upgrading.txt doc/user_guide.txt roundup/cgi/templating.py roundup/date.py share/roundup/templates/classic/html/datecopy.js share/roundup/templates/classic/html/datecopy.min.js share/roundup/templates/classic/html/page.html share/roundup/templates/devel/html/datecopy.js share/roundup/templates/devel/html/datecopy.min.js share/roundup/templates/devel/html/page.html share/roundup/templates/jinja2/html/datecopy.js share/roundup/templates/jinja2/html/datecopy.min.js share/roundup/templates/jinja2/html/layout/page.html share/roundup/templates/minimal/html/datecopy.js share/roundup/templates/minimal/html/datecopy.min.js share/roundup/templates/minimal/html/page.html share/roundup/templates/responsive/html/datecopy.js share/roundup/templates/responsive/html/datecopy.min.js share/roundup/templates/responsive/html/page.html test/test_dates.py test/test_templating.py
diffstat 24 files changed, 1022 insertions(+), 14 deletions(-) [+]
line wrap: on
line diff
--- a/CHANGES.txt	Sat Jan 18 11:20:20 2025 -0500
+++ b/CHANGES.txt	Sat Jan 18 12:23:23 2025 -0500
@@ -77,6 +77,10 @@
 - issue2551391, partial fix for issue1513369. input fields were
   not getting id's assigned. Fixed automatic id assignment to
   input fields. Thinko in the code. (John Rouillard)
+- issue2551390 - Replace text input/calendar popup with native
+  date input. Also add double-click and exit keyboard handlers to
+  allow copy/paste/editing the text version of the date.  (John
+  Rouillard)
 
 Features:
 
--- a/doc/customizing.txt	Sat Jan 18 11:20:20 2025 -0500
+++ b/doc/customizing.txt	Sat Jan 18 12:23:23 2025 -0500
@@ -90,11 +90,11 @@
       <td tal:content="structure context/due_date/field" /> 
      </tr>
     
-   If you want to show only the date part of due_date then do this instead::
+   If you want to show the date and time for due_date then do this instead::
    
     <tr> 
      <th>Due Date</th> 
-     <td tal:content="structure python:context.due_date.field(format='%Y-%m-%d')" /> 
+     <td tal:content="structure context/due_date/field_time" /> 
     </tr>
 
 3. Add the property to the ``issue.index.html`` page::
--- a/doc/reference.txt	Sat Jan 18 11:20:20 2025 -0500
+++ b/doc/reference.txt	Sat Jan 18 12:23:23 2025 -0500
@@ -3070,17 +3070,28 @@
 	      tri-state yes/no/neither selection. This method may take some
 	      arguments:
 
-	      size
+	      size (default 30)
 		Sets the width in characters of the edit field
 
 	      format (Date properties only)
-		Sets the format of the date in the field - uses the same
-		format string argument as supplied to the ``pretty`` method
-		below.
+                Sets the format of the date in the field - uses the
+		same format string argument as supplied to the
+		``pretty`` method below. If you use this, it will
+		prevent the use of browser native date inputs. It is
+		useful if you want partial dates. For example using
+		``format="%Y-%m"`` with ``type="text"`` will display a
+		text edit box with the year and month part of your
+		date.
+
+              type (depends on property type)
+                Sets the type property of the input. To change a date
+                property field from a native date input to a text
+                input you would use ``type="text"``.
 
 	      popcal (Date properties only)
-		Include the JavaScript-based popup calendar for date
-		selection. Defaults to on.
+		Include a link to the JavaScript-based popup calendar
+		for date selection. Defaults to off/False since native
+		date inputs supply popup calendars.
 
 	      y_label, n_label, u_label (Boolean properties only)
 	        Set the labels for the true/false/undefined
@@ -3104,6 +3115,10 @@
 	      attribute without a value. This is useful for boolean
 	      properties like ``required``.
 
+  field_time  (Date properties only)
+              Create a browser native input for editing date and time.
+              The field method creates an input for editing
+              month/day/year (without time).
 
   rst         only on String properties - render the value of the property
 	      as ReStructuredText (requires the :ref:`Docutils
@@ -3164,7 +3179,11 @@
 	      is returned if the value is ``None`` otherwise it
 	      is converted to a string.
 
-  popcal      Generate a link to a popup calendar which may be used to
+  popcal      This is deprecated with Roundup 2.5 which uses the
+              native HTML5 date input. The native date input
+              includes a calendar popup on modern broswers.
+
+              Generate a link to a popup calendar which may be used to
 	      edit the date field, for example::
 
 		<span tal:replace="structure context/due/popcal" />
@@ -4027,6 +4046,17 @@
 comma-separated value content (i.e. something to load into a
 spreadsheet or database).
 
+CSS for the web interface
+-------------------------
+
+The web interface can be completely redesigned by the admin, However
+some parts of Roundup use classes or set attributes that can be
+selected by css to change the look of the element.
+
+The ``datecopy.js`` module used to allow editing a date value with a
+text input assigns the ``mode_textdate`` class to the input when it is
+in text mode. The class is removed when it is not in text mode.
+
 
 8-bit character set support in Web interface
 --------------------------------------------
--- a/doc/upgrading.txt	Sat Jan 18 11:20:20 2025 -0500
+++ b/doc/upgrading.txt	Sat Jan 18 12:23:23 2025 -0500
@@ -224,6 +224,148 @@
 
 .. _defusedxml: https://pypi.org/project/defusedxml/
 
+Use native date inputs (optional)
+---------------------------------
+
+Roundup now uses native date or datetime-local inputs for Date()
+properties. These inputs take the place of the text input and
+calendar popup from earlier Roundup versions. Modern browsers
+come with a built-in calendar for date selection, so the
+``(cal)`` calendar link is no longer needed. These native inputs
+show the date based on the browser's locale and translate terms
+into the local language.
+
+If you do nothing, simple uses of the field() method will
+generate date inputs to allow selection of a date.  Input fields
+for Date() properties will not have the ``(cal)`` link
+anymore. Complex uses will not be upgraded and will operate like
+earlier Roundup versions.
+
+To upgrade all date properties, there are four changes to make:
+
+  1. Replace ``field`` calls with ``field_time`` where needed.
+
+  2. Remove the format argument from field() calls on Date()
+     properties.
+
+  3. Remove popcal() calls.
+
+  4. Include datecopy.js in page.html.
+
+Use field_time() where needed
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The format used by ``field`` does not include hours, minutes or
+seconds. If your users need to enter times, you should change
+these calls to use ``field_time``. The arguments are the same as
+for field.
+
+Remove format argument
+~~~~~~~~~~~~~~~~~~~~~~
+
+Speaking of arguments, avoid setting the date format if you want
+to use native date inputs. The date value needs a specific format
+for date or datetime-local inputs. If you include the `format`
+argument in the `field` method, it should be removed.
+
+The `field` method uses the format ``%Y-%m-%d``.  The
+``field_time`` method uses the format ``%Y-%m-%dT%H:%M:%S``. If
+you use these exact formats, Roundup will accept them and use a
+native date input.
+
+.. highlight:: text
+
+If you use an format that doesn't match, you will see a text
+input and a logged warning message like::
+
+  Format '%Y-%m' prevents use of modern date input.
+  Remove format from field() call in template test.item.
+  Using text input.
+
+.. highlight:: default
+
+The log message will appear if your logging level is set to
+WARNING or lower. (Refer to your tracker's :ref:`config.ini
+logging section <config-ini-section-logging>` for details on
+logging levels.)
+
+If you want to use a text input for a specific date format, you
+can add ``type="text"`` to the field() argument list to suppress
+the warning. By default using a format argument will show the
+popup calendar link. You can disable the link by setting
+``popcal=False`` in the field() call. If you have::
+
+  tal:content="structure python:context.duedate.field(
+               placeholder='YYYY-MM, format='%Y-%m')"
+
+changing it to::
+
+  tal:content="structure python:context.duedate.field(
+               type='text',
+               placeholder='YYYY-MM, format='%Y-%m',
+               popcal=False)"
+
+will generate the input as in Roundup 2.4 or earlier without a
+popcal link.
+
+If you are using a path expression like::
+
+  tal:content="context/duedate/field"
+
+change it to::
+
+  tal:content="structure python:context.duedate.field(
+               type='text')"
+
+to get the input from before Roundup 2.5 with a popcal link.
+
+Remove popcal
+~~~~~~~~~~~~~
+
+If you use the ``popcal()`` method directly in your templates, you
+can remove them. The browser's native date selection calendar can
+be used instead.
+
+Add copy/paste/edit on double-click using datecopy.js
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+There is no way to copy/paste using a native datetime-local or
+date input. With the datecopy.js file installed, double-clicking
+on the input turns it into a normal text input with the ability
+to copy, paste, or manually edit the date.
+
+To set this up, take either ``datecopy.js`` or the smaller
+version, ``datecopy.min.js``, from the ``html`` folder of the
+classic tracker template. Put the file in the ``html`` folder of
+your tracker home.
+
+After you install the datecopy file, you can add the script
+directly to a page using::
+
+  <script tal:attributes="nonce request/client/client_nonce"
+          tal:content="structure python:utils.readfile('datecopy.min.js')">
+  </script>
+
+or get the file in a separate download using a regular script
+tag::
+
+    <script type="text/javascript" src="@@file/datecopy.js">
+    </script>
+
+You can place these at the end of ``page.html`` just before the
+close body ``</body>`` tag. This is the method used in the
+classic template. This forces the file to be run for every page
+even those that don't have any date inputs. However, it is cached
+after the first download.
+
+Alternatively you can inline or link to it using a script tag
+only on pages that will have a date input. For example
+``issue.item.html``.
+
+There is no support for activating text mode using the
+keyboard. Tablet/touch support is mixed. Chrome supports
+double-tap to activate text mode input. Firefox does not.
+
 Change in REST response for invalid CORS requests (info)
 --------------------------------------------------------
 
--- a/doc/user_guide.txt	Sat Jan 18 11:20:20 2025 -0500
+++ b/doc/user_guide.txt	Sat Jan 18 12:23:23 2025 -0500
@@ -177,6 +177,14 @@
 Date properties
 ~~~~~~~~~~~~~~~
 
+Date properties are usually shown using a native HTML date
+element. This provides a calendar button for choosing the
+date. The date is shown in the normal format for your location.
+
+Native date inputs do not allow the use of partial forms as
+defined below.  For this reason, you may edit a date/time
+stamp directly.
+
 Date-and-time stamps are specified with the date in
 international standard format (``yyyy-mm-dd``) joined to the time
 (``hh:mm:ss``) by a period ``.``.  Dates in this form can be easily
@@ -191,7 +199,7 @@
 interpreted in the user's local time zone. The Date constructor takes
 care of these conversions. In the following examples, suppose that
 ``yyyy`` is the current year, ``mm`` is the current month, and ``dd`` is
-the current day of the month.
+the current day of the month and the local timezone is GMT-5.
 
 -   "2000-04-17" means <Date 2000-04-17.00:00:00>
 -   "01-25" means <Date yyyy-01-25.00:00:00>
@@ -202,6 +210,30 @@
 -   "8:47:11" means <Date yyyy-mm-dd.13:47:11>
 -   the special date "." means "right now"
 
+The native date input doesn't allow copy or paste. Roundup
+enhances the native date field. If you double-click on a native
+date field, it changes to a text input mode with the date already
+selected. You can use control-C to copy the date or control-V to
+paste into the field.  Double-clicking also lets you add seconds
+in a date-time value if you need to.
+
+It will switch back to a date input and save the value when:
+
+- you move to another field using the mouse or the Tab key.
+- you press enter/return (press return again if you want to
+  submit the form).
+
+If you press Escape, it will restore the original value and
+change back to a date input.
+
+When using native date elements in text input mode, the date
+looks like a full data with ``T`` replacing ``.``. If the ``T``
+is missing, the native date elements will not recognize the value
+as a date.
+
+There is no support for activating text mode using the
+keyboard. Tablet/touch support is mixed. Chrome supports
+double-tap to activate text mode input. Firefox does not.
 
 When searching, a plain date entered as a search field will match that date
 exactly in the database.  We may also accept ranges of dates. You can
--- a/roundup/cgi/templating.py	Sat Jan 18 11:20:20 2025 -0500
+++ b/roundup/cgi/templating.py	Sat Jan 18 12:23:23 2025 -0500
@@ -2252,7 +2252,16 @@
         return DateHTMLProperty(self._client, self._classname, self._nodeid,
                                 self._prop, self._formname, ret)
 
-    def field(self, size=30, default=None, format=_marker, popcal=True,
+
+    def field_time(self, size=30, default=None, format=_marker, popcal=None,
+                   **kwargs):
+
+        kwargs.setdefault("type", "datetime-local")
+        field = self.field(size=size, default=default, format=format,
+                           popcal=popcal, **kwargs)
+        return field
+
+    def field(self, size=30, default=None, format=_marker, popcal=None,
               **kwargs):
         """Render a form edit field for the property
 
@@ -2269,6 +2278,47 @@
             else:
                 return self.pretty(format)
 
+        kwargs.setdefault("type", "date")
+
+        if kwargs["type"] in ["date", "datetime-local"]:
+            acceptable_formats = {
+                "date": "%Y-%m-%d",
+                "datetime-local": "%Y-%m-%dT%H:%M:%S"
+            }
+
+            if format is not self._marker:  # user set format
+                if format != acceptable_formats[kwargs["type"]]:
+                    # format is incompatible with date type
+                    kwargs['type'] = "text"
+                    if popcal is not False:
+                        popcal = True
+                    logger.warning(self._(
+                        "Format '%(format)s' prevents use of modern "
+                        "date input. Remove format from field() call in "
+                        "template %(class)s.%(template)s. "
+                        "Using text input.") % {
+                            "format": format,
+                            "class": self._client.classname,
+                            "template": self._client.template
+                        })
+
+                    """
+                    raise ValueError(self._(
+                        "When using input type of '%(field_type)s', the "
+                        "format must not be set, or must be "
+                        "'%(format_string)s' to match RFC3339 date "
+                        "or date-time. Current format is '%(format)s'.") % {
+                            "field_type": kwargs["type"],
+                            "format_string":
+                            acceptable_formats[kwargs["type"]],
+                            "format": format,
+                      })"""
+            else:
+                # https://developer.mozilla.org/en-US/docs/Web/HTML/Date_and_time_formats#local_date_and_time_strings
+                # match date-time format in
+                # https://www.rfc-editor.org/rfc/rfc3339
+                format = acceptable_formats[kwargs['type']]
+
         value = self._value
 
         if value is None:
--- a/roundup/date.py	Sat Jan 18 11:20:20 2025 -0500
+++ b/roundup/date.py	Sat Jan 18 12:23:23 2025 -0500
@@ -46,7 +46,7 @@
 date_re = re.compile(r'''^
     ((?P<y>\d\d\d\d)([/-](?P<m>\d\d?)([/-](?P<d>\d\d?))?)? # yyyy[-mm[-dd]]
     |(?P<a>\d\d?)[/-](?P<b>\d\d?))?              # or mm-dd
-    (?P<n>\.)?                                   # .
+    (?P<n>[.T])?                                 # . or T
     (((?P<H>\d?\d):(?P<M>\d\d))?(:(?P<S>\d\d?(\.\d+)?))?)?  # hh:mm:ss
     (?:(?P<tz>\s?[+-]\d{4})|(?P<o>[\d\smywd\-+]+))? # time-zone offset, offset
 $''', re.VERBOSE)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/share/roundup/templates/classic/html/datecopy.js	Sat Jan 18 12:23:23 2025 -0500
@@ -0,0 +1,116 @@
+/* Capture double-click on a date element. Turn element in text element
+   and select date for copying. Return to date element saving change on
+   focusout or Enter. Return to date element with original value on Escape.
+   Derived from
+   https://stackoverflow.com/questions/49981660/enable-copy-paste-on-html5-date-field
+*/
+
+/* TODO: keyboard support should be added to allow entering text mode. */
+
+// use iife to encapsulate handleModeExitKeys
+(function () {
+  "use strict";
+
+  // Define named function so it can be added/removed as event handler
+  //   in different scopes of the code
+  function handleModeExitKeys (event) {
+    if (event.key !== "Escape" && event.key !== "Enter") return;
+    event.preventDefault();
+    if (event.key === "Escape") {
+	event.target.value = event.target.original_value;
+    }
+    let focusout = new Event("focusout");
+    event.target.dispatchEvent(focusout);
+  }
+
+
+  document.querySelector("body").addEventListener("dblclick", (evt) => {
+    if (evt.target.tagName !== "INPUT") return;
+
+    if (! ["date", "datetime-local"].includes(
+	evt.target.attributes.type.value.toLowerCase())) return;
+
+    // we have a date type input
+    let target = evt.target;
+    let original_type = target.attributes.type.value;
+    target.type = "text";
+
+    target.original_value = target.value;
+
+    // allow admin to set CSS to change input
+    target.classList.add("mode_textdate");
+    // After changing input type with JS .select() won't
+    // work as usual
+    // Needs timeout fn() to make it work
+    setTimeout(() => {
+	target.select();
+    });
+
+    // register the focusout event to reset the input back
+    // to a date input field. Once it triggers the handler
+    // is deleted to be added on the next doubleclick.
+    // This also should end the closure of original_type.
+    target.addEventListener("focusout", () => {
+	target.type = original_type;
+	delete event.target.original_value;
+
+	target.classList.remove("mode_textdate");
+
+	target.removeEventListener("keydown", handleModeExitKeys);
+    }, {once: true});
+
+    // called on every keypress including editing the field,
+    // so can not be set with "once" like "focusout".
+    target.addEventListener("keydown", handleModeExitKeys);
+  });
+})()
+
+/* Some failed experiments that I would have liked to have work */
+/* With the date element focused, type ^c or ^v to copy/paste
+   evt.target always seems to be inconsistent. Sometimes I get the
+   input but usually I get the body.
+
+   I can find the date element using document.activeElement, but
+   this seems like a kludge.
+ */
+/*
+body.addEventListener("copy", (evt) => {
+    // target = document.activeElement;
+    target = evt.target;
+    if (target.tagName != "INPUT") {
+	//alert("copy received non-date"  + target.tagName);
+	return;
+    }
+
+    if (! ["date", "datetime-local"].includes(
+	    target.attributes.type.value)) {
+	//alert("copy received non-date");
+	return;
+	}
+
+    evt.clipboardData.setData("text/plain",
+			      target.value);
+    // default behaviour is to copy any selected text
+    // overwriting what we set
+    evt.preventDefault();
+    //alert("copy received date");
+})
+
+body.addEventListener("paste", (evt) => {
+    if (evt.target.tagName != "INPUT") {
+	//alert("paste received non-date");
+	return;
+    }
+
+    if (! ["date", "datetime-local"].includes(
+	    evt.target.attributes.type.value)) {
+	//alert("paste received non-date");
+	return;
+	}
+
+    data = evt.clipboardData.getData("text/plain");
+    evt.preventDefault();
+    evt.target.value = data;
+    //alert("paste received date " + data);
+})
+*/
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/share/roundup/templates/classic/html/datecopy.min.js	Sat Jan 18 12:23:23 2025 -0500
@@ -0,0 +1,1 @@
+(function(){"use strict";function handleModeExitKeys(event){if(event.key!=="Escape"&&event.key!=="Enter")return;event.preventDefault();if(event.key==="Escape"){event.target.value=event.target.original_value;};let focusout=new Event("focusout");event.target.dispatchEvent(focusout);};document.querySelector("body").addEventListener("dblclick",(evt)=>{if(evt.target.tagName!=="INPUT")return;if(!["date","datetime-local"].includes(evt.target.attributes.type.value.toLowerCase()))return;let target=evt.target;let original_type=target.attributes.type.value;target.type="text";target.original_value=target.value;target.classList.add("mode_textdate");setTimeout(()=>{target.select();});target.addEventListener("focusout",()=>{target.type=original_type;delete event.target.original_value;target.classList.remove("mode_textdate");target.removeEventListener("keydown",handleModeExitKeys);},{once:true});target.addEventListener("keydown",handleModeExitKeys);});})()
\ No newline at end of file
--- a/share/roundup/templates/classic/html/page.html	Sat Jan 18 11:20:20 2025 -0500
+++ b/share/roundup/templates/classic/html/page.html	Sat Jan 18 12:23:23 2025 -0500
@@ -200,6 +200,7 @@
 <pre tal:condition="request/form/debug | nothing" tal:content="request">
 </pre>
 
+<script type="text/javascript" src='@@file/datecopy.min.js'></script>
 </body>
 </html>
 </tal:block>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/share/roundup/templates/devel/html/datecopy.js	Sat Jan 18 12:23:23 2025 -0500
@@ -0,0 +1,116 @@
+/* Capture double-click on a date element. Turn element in text element
+   and select date for copying. Return to date element saving change on
+   focusout or Enter. Return to date element with original value on Escape.
+   Derived from
+   https://stackoverflow.com/questions/49981660/enable-copy-paste-on-html5-date-field
+*/
+
+/* TODO: keyboard support should be added to allow entering text mode. */
+
+// use iife to encapsulate handleModeExitKeys
+(function () {
+  "use strict";
+
+  // Define named function so it can be added/removed as event handler
+  //   in different scopes of the code
+  function handleModeExitKeys (event) {
+    if (event.key !== "Escape" && event.key !== "Enter") return;
+    event.preventDefault();
+    if (event.key === "Escape") {
+	event.target.value = event.target.original_value;
+    }
+    let focusout = new Event("focusout");
+    event.target.dispatchEvent(focusout);
+  }
+
+
+  document.querySelector("body").addEventListener("dblclick", (evt) => {
+    if (evt.target.tagName !== "INPUT") return;
+
+    if (! ["date", "datetime-local"].includes(
+	evt.target.attributes.type.value.toLowerCase())) return;
+
+    // we have a date type input
+    let target = evt.target;
+    let original_type = target.attributes.type.value;
+    target.type = "text";
+
+    target.original_value = target.value;
+
+    // allow admin to set CSS to change input
+    target.classList.add("mode_textdate");
+    // After changing input type with JS .select() won't
+    // work as usual
+    // Needs timeout fn() to make it work
+    setTimeout(() => {
+	target.select();
+    });
+
+    // register the focusout event to reset the input back
+    // to a date input field. Once it triggers the handler
+    // is deleted to be added on the next doubleclick.
+    // This also should end the closure of original_type.
+    target.addEventListener("focusout", () => {
+	target.type = original_type;
+	delete event.target.original_value;
+
+	target.classList.remove("mode_textdate");
+
+	target.removeEventListener("keydown", handleModeExitKeys);
+    }, {once: true});
+
+    // called on every keypress including editing the field,
+    // so can not be set with "once" like "focusout".
+    target.addEventListener("keydown", handleModeExitKeys);
+  });
+})()
+
+/* Some failed experiments that I would have liked to have work */
+/* With the date element focused, type ^c or ^v to copy/paste
+   evt.target always seems to be inconsistent. Sometimes I get the
+   input but usually I get the body.
+
+   I can find the date element using document.activeElement, but
+   this seems like a kludge.
+ */
+/*
+body.addEventListener("copy", (evt) => {
+    // target = document.activeElement;
+    target = evt.target;
+    if (target.tagName != "INPUT") {
+	//alert("copy received non-date"  + target.tagName);
+	return;
+    }
+
+    if (! ["date", "datetime-local"].includes(
+	    target.attributes.type.value)) {
+	//alert("copy received non-date");
+	return;
+	}
+
+    evt.clipboardData.setData("text/plain",
+			      target.value);
+    // default behaviour is to copy any selected text
+    // overwriting what we set
+    evt.preventDefault();
+    //alert("copy received date");
+})
+
+body.addEventListener("paste", (evt) => {
+    if (evt.target.tagName != "INPUT") {
+	//alert("paste received non-date");
+	return;
+    }
+
+    if (! ["date", "datetime-local"].includes(
+	    evt.target.attributes.type.value)) {
+	//alert("paste received non-date");
+	return;
+	}
+
+    data = evt.clipboardData.getData("text/plain");
+    evt.preventDefault();
+    evt.target.value = data;
+    //alert("paste received date " + data);
+})
+*/
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/share/roundup/templates/devel/html/datecopy.min.js	Sat Jan 18 12:23:23 2025 -0500
@@ -0,0 +1,1 @@
+(function(){"use strict";function handleModeExitKeys(event){if(event.key!=="Escape"&&event.key!=="Enter")return;event.preventDefault();if(event.key==="Escape"){event.target.value=event.target.original_value;};let focusout=new Event("focusout");event.target.dispatchEvent(focusout);};document.querySelector("body").addEventListener("dblclick",(evt)=>{if(evt.target.tagName!=="INPUT")return;if(!["date","datetime-local"].includes(evt.target.attributes.type.value.toLowerCase()))return;let target=evt.target;let original_type=target.attributes.type.value;target.type="text";target.original_value=target.value;target.classList.add("mode_textdate");setTimeout(()=>{target.select();});target.addEventListener("focusout",()=>{target.type=original_type;delete event.target.original_value;target.classList.remove("mode_textdate");target.removeEventListener("keydown",handleModeExitKeys);},{once:true});target.addEventListener("keydown",handleModeExitKeys);});})()
\ No newline at end of file
--- a/share/roundup/templates/devel/html/page.html	Sat Jan 18 11:20:20 2025 -0500
+++ b/share/roundup/templates/devel/html/page.html	Sat Jan 18 12:23:23 2025 -0500
@@ -263,6 +263,7 @@
 <!-- hhmts end -->
   </div> <!-- footer -->
   <pre tal:condition="request/form/deissue | nothing" tal:content="request"></pre>
+  <script type="text/javascript" src='@@file/datecopy.min.js'></script>
  </body>
 </html>
 </tal:block>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/share/roundup/templates/jinja2/html/datecopy.js	Sat Jan 18 12:23:23 2025 -0500
@@ -0,0 +1,116 @@
+/* Capture double-click on a date element. Turn element in text element
+   and select date for copying. Return to date element saving change on
+   focusout or Enter. Return to date element with original value on Escape.
+   Derived from
+   https://stackoverflow.com/questions/49981660/enable-copy-paste-on-html5-date-field
+*/
+
+/* TODO: keyboard support should be added to allow entering text mode. */
+
+// use iife to encapsulate handleModeExitKeys
+(function () {
+  "use strict";
+
+  // Define named function so it can be added/removed as event handler
+  //   in different scopes of the code
+  function handleModeExitKeys (event) {
+    if (event.key !== "Escape" && event.key !== "Enter") return;
+    event.preventDefault();
+    if (event.key === "Escape") {
+	event.target.value = event.target.original_value;
+    }
+    let focusout = new Event("focusout");
+    event.target.dispatchEvent(focusout);
+  }
+
+
+  document.querySelector("body").addEventListener("dblclick", (evt) => {
+    if (evt.target.tagName !== "INPUT") return;
+
+    if (! ["date", "datetime-local"].includes(
+	evt.target.attributes.type.value.toLowerCase())) return;
+
+    // we have a date type input
+    let target = evt.target;
+    let original_type = target.attributes.type.value;
+    target.type = "text";
+
+    target.original_value = target.value;
+
+    // allow admin to set CSS to change input
+    target.classList.add("mode_textdate");
+    // After changing input type with JS .select() won't
+    // work as usual
+    // Needs timeout fn() to make it work
+    setTimeout(() => {
+	target.select();
+    });
+
+    // register the focusout event to reset the input back
+    // to a date input field. Once it triggers the handler
+    // is deleted to be added on the next doubleclick.
+    // This also should end the closure of original_type.
+    target.addEventListener("focusout", () => {
+	target.type = original_type;
+	delete event.target.original_value;
+
+	target.classList.remove("mode_textdate");
+
+	target.removeEventListener("keydown", handleModeExitKeys);
+    }, {once: true});
+
+    // called on every keypress including editing the field,
+    // so can not be set with "once" like "focusout".
+    target.addEventListener("keydown", handleModeExitKeys);
+  });
+})()
+
+/* Some failed experiments that I would have liked to have work */
+/* With the date element focused, type ^c or ^v to copy/paste
+   evt.target always seems to be inconsistent. Sometimes I get the
+   input but usually I get the body.
+
+   I can find the date element using document.activeElement, but
+   this seems like a kludge.
+ */
+/*
+body.addEventListener("copy", (evt) => {
+    // target = document.activeElement;
+    target = evt.target;
+    if (target.tagName != "INPUT") {
+	//alert("copy received non-date"  + target.tagName);
+	return;
+    }
+
+    if (! ["date", "datetime-local"].includes(
+	    target.attributes.type.value)) {
+	//alert("copy received non-date");
+	return;
+	}
+
+    evt.clipboardData.setData("text/plain",
+			      target.value);
+    // default behaviour is to copy any selected text
+    // overwriting what we set
+    evt.preventDefault();
+    //alert("copy received date");
+})
+
+body.addEventListener("paste", (evt) => {
+    if (evt.target.tagName != "INPUT") {
+	//alert("paste received non-date");
+	return;
+    }
+
+    if (! ["date", "datetime-local"].includes(
+	    evt.target.attributes.type.value)) {
+	//alert("paste received non-date");
+	return;
+	}
+
+    data = evt.clipboardData.getData("text/plain");
+    evt.preventDefault();
+    evt.target.value = data;
+    //alert("paste received date " + data);
+})
+*/
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/share/roundup/templates/jinja2/html/datecopy.min.js	Sat Jan 18 12:23:23 2025 -0500
@@ -0,0 +1,1 @@
+(function(){"use strict";function handleModeExitKeys(event){if(event.key!=="Escape"&&event.key!=="Enter")return;event.preventDefault();if(event.key==="Escape"){event.target.value=event.target.original_value;};let focusout=new Event("focusout");event.target.dispatchEvent(focusout);};document.querySelector("body").addEventListener("dblclick",(evt)=>{if(evt.target.tagName!=="INPUT")return;if(!["date","datetime-local"].includes(evt.target.attributes.type.value.toLowerCase()))return;let target=evt.target;let original_type=target.attributes.type.value;target.type="text";target.original_value=target.value;target.classList.add("mode_textdate");setTimeout(()=>{target.select();});target.addEventListener("focusout",()=>{target.type=original_type;delete event.target.original_value;target.classList.remove("mode_textdate");target.removeEventListener("keydown",handleModeExitKeys);},{once:true});target.addEventListener("keydown",handleModeExitKeys);});})()
\ No newline at end of file
--- a/share/roundup/templates/jinja2/html/layout/page.html	Sat Jan 18 11:20:20 2025 -0500
+++ b/share/roundup/templates/jinja2/html/layout/page.html	Sat Jan 18 12:23:23 2025 -0500
@@ -68,6 +68,7 @@
 
     <script src='@@file/jquery-1.9.0.min.js'></script>
     <script src='@@file/bootstrap.min.js'></script>
+    <script src='@@file/datecopy.min.js'></script>
     {% block extrajs %} {% endblock %}
   </body>
 </html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/share/roundup/templates/minimal/html/datecopy.js	Sat Jan 18 12:23:23 2025 -0500
@@ -0,0 +1,116 @@
+/* Capture double-click on a date element. Turn element in text element
+   and select date for copying. Return to date element saving change on
+   focusout or Enter. Return to date element with original value on Escape.
+   Derived from
+   https://stackoverflow.com/questions/49981660/enable-copy-paste-on-html5-date-field
+*/
+
+/* TODO: keyboard support should be added to allow entering text mode. */
+
+// use iife to encapsulate handleModeExitKeys
+(function () {
+  "use strict";
+
+  // Define named function so it can be added/removed as event handler
+  //   in different scopes of the code
+  function handleModeExitKeys (event) {
+    if (event.key !== "Escape" && event.key !== "Enter") return;
+    event.preventDefault();
+    if (event.key === "Escape") {
+	event.target.value = event.target.original_value;
+    }
+    let focusout = new Event("focusout");
+    event.target.dispatchEvent(focusout);
+  }
+
+
+  document.querySelector("body").addEventListener("dblclick", (evt) => {
+    if (evt.target.tagName !== "INPUT") return;
+
+    if (! ["date", "datetime-local"].includes(
+	evt.target.attributes.type.value.toLowerCase())) return;
+
+    // we have a date type input
+    let target = evt.target;
+    let original_type = target.attributes.type.value;
+    target.type = "text";
+
+    target.original_value = target.value;
+
+    // allow admin to set CSS to change input
+    target.classList.add("mode_textdate");
+    // After changing input type with JS .select() won't
+    // work as usual
+    // Needs timeout fn() to make it work
+    setTimeout(() => {
+	target.select();
+    });
+
+    // register the focusout event to reset the input back
+    // to a date input field. Once it triggers the handler
+    // is deleted to be added on the next doubleclick.
+    // This also should end the closure of original_type.
+    target.addEventListener("focusout", () => {
+	target.type = original_type;
+	delete event.target.original_value;
+
+	target.classList.remove("mode_textdate");
+
+	target.removeEventListener("keydown", handleModeExitKeys);
+    }, {once: true});
+
+    // called on every keypress including editing the field,
+    // so can not be set with "once" like "focusout".
+    target.addEventListener("keydown", handleModeExitKeys);
+  });
+})()
+
+/* Some failed experiments that I would have liked to have work */
+/* With the date element focused, type ^c or ^v to copy/paste
+   evt.target always seems to be inconsistent. Sometimes I get the
+   input but usually I get the body.
+
+   I can find the date element using document.activeElement, but
+   this seems like a kludge.
+ */
+/*
+body.addEventListener("copy", (evt) => {
+    // target = document.activeElement;
+    target = evt.target;
+    if (target.tagName != "INPUT") {
+	//alert("copy received non-date"  + target.tagName);
+	return;
+    }
+
+    if (! ["date", "datetime-local"].includes(
+	    target.attributes.type.value)) {
+	//alert("copy received non-date");
+	return;
+	}
+
+    evt.clipboardData.setData("text/plain",
+			      target.value);
+    // default behaviour is to copy any selected text
+    // overwriting what we set
+    evt.preventDefault();
+    //alert("copy received date");
+})
+
+body.addEventListener("paste", (evt) => {
+    if (evt.target.tagName != "INPUT") {
+	//alert("paste received non-date");
+	return;
+    }
+
+    if (! ["date", "datetime-local"].includes(
+	    evt.target.attributes.type.value)) {
+	//alert("paste received non-date");
+	return;
+	}
+
+    data = evt.clipboardData.getData("text/plain");
+    evt.preventDefault();
+    evt.target.value = data;
+    //alert("paste received date " + data);
+})
+*/
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/share/roundup/templates/minimal/html/datecopy.min.js	Sat Jan 18 12:23:23 2025 -0500
@@ -0,0 +1,1 @@
+(function(){"use strict";function handleModeExitKeys(event){if(event.key!=="Escape"&&event.key!=="Enter")return;event.preventDefault();if(event.key==="Escape"){event.target.value=event.target.original_value;};let focusout=new Event("focusout");event.target.dispatchEvent(focusout);};document.querySelector("body").addEventListener("dblclick",(evt)=>{if(evt.target.tagName!=="INPUT")return;if(!["date","datetime-local"].includes(evt.target.attributes.type.value.toLowerCase()))return;let target=evt.target;let original_type=target.attributes.type.value;target.type="text";target.original_value=target.value;target.classList.add("mode_textdate");setTimeout(()=>{target.select();});target.addEventListener("focusout",()=>{target.type=original_type;delete event.target.original_value;target.classList.remove("mode_textdate");target.removeEventListener("keydown",handleModeExitKeys);},{once:true});target.addEventListener("keydown",handleModeExitKeys);});})()
\ No newline at end of file
--- a/share/roundup/templates/minimal/html/page.html	Sat Jan 18 11:20:20 2025 -0500
+++ b/share/roundup/templates/minimal/html/page.html	Sat Jan 18 12:23:23 2025 -0500
@@ -186,6 +186,7 @@
 <pre tal:condition="request/form/debug | nothing" tal:content="request">
 </pre>
 
+<script type="text/javascript" src='@@file/datecopy.min.js'></script>
 </body>
 </html>
 </tal:block>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/share/roundup/templates/responsive/html/datecopy.js	Sat Jan 18 12:23:23 2025 -0500
@@ -0,0 +1,116 @@
+/* Capture double-click on a date element. Turn element in text element
+   and select date for copying. Return to date element saving change on
+   focusout or Enter. Return to date element with original value on Escape.
+   Derived from
+   https://stackoverflow.com/questions/49981660/enable-copy-paste-on-html5-date-field
+*/
+
+/* TODO: keyboard support should be added to allow entering text mode. */
+
+// use iife to encapsulate handleModeExitKeys
+(function () {
+  "use strict";
+
+  // Define named function so it can be added/removed as event handler
+  //   in different scopes of the code
+  function handleModeExitKeys (event) {
+    if (event.key !== "Escape" && event.key !== "Enter") return;
+    event.preventDefault();
+    if (event.key === "Escape") {
+	event.target.value = event.target.original_value;
+    }
+    let focusout = new Event("focusout");
+    event.target.dispatchEvent(focusout);
+  }
+
+
+  document.querySelector("body").addEventListener("dblclick", (evt) => {
+    if (evt.target.tagName !== "INPUT") return;
+
+    if (! ["date", "datetime-local"].includes(
+	evt.target.attributes.type.value.toLowerCase())) return;
+
+    // we have a date type input
+    let target = evt.target;
+    let original_type = target.attributes.type.value;
+    target.type = "text";
+
+    target.original_value = target.value;
+
+    // allow admin to set CSS to change input
+    target.classList.add("mode_textdate");
+    // After changing input type with JS .select() won't
+    // work as usual
+    // Needs timeout fn() to make it work
+    setTimeout(() => {
+	target.select();
+    });
+
+    // register the focusout event to reset the input back
+    // to a date input field. Once it triggers the handler
+    // is deleted to be added on the next doubleclick.
+    // This also should end the closure of original_type.
+    target.addEventListener("focusout", () => {
+	target.type = original_type;
+	delete event.target.original_value;
+
+	target.classList.remove("mode_textdate");
+
+	target.removeEventListener("keydown", handleModeExitKeys);
+    }, {once: true});
+
+    // called on every keypress including editing the field,
+    // so can not be set with "once" like "focusout".
+    target.addEventListener("keydown", handleModeExitKeys);
+  });
+})()
+
+/* Some failed experiments that I would have liked to have work */
+/* With the date element focused, type ^c or ^v to copy/paste
+   evt.target always seems to be inconsistent. Sometimes I get the
+   input but usually I get the body.
+
+   I can find the date element using document.activeElement, but
+   this seems like a kludge.
+ */
+/*
+body.addEventListener("copy", (evt) => {
+    // target = document.activeElement;
+    target = evt.target;
+    if (target.tagName != "INPUT") {
+	//alert("copy received non-date"  + target.tagName);
+	return;
+    }
+
+    if (! ["date", "datetime-local"].includes(
+	    target.attributes.type.value)) {
+	//alert("copy received non-date");
+	return;
+	}
+
+    evt.clipboardData.setData("text/plain",
+			      target.value);
+    // default behaviour is to copy any selected text
+    // overwriting what we set
+    evt.preventDefault();
+    //alert("copy received date");
+})
+
+body.addEventListener("paste", (evt) => {
+    if (evt.target.tagName != "INPUT") {
+	//alert("paste received non-date");
+	return;
+    }
+
+    if (! ["date", "datetime-local"].includes(
+	    evt.target.attributes.type.value)) {
+	//alert("paste received non-date");
+	return;
+	}
+
+    data = evt.clipboardData.getData("text/plain");
+    evt.preventDefault();
+    evt.target.value = data;
+    //alert("paste received date " + data);
+})
+*/
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/share/roundup/templates/responsive/html/datecopy.min.js	Sat Jan 18 12:23:23 2025 -0500
@@ -0,0 +1,1 @@
+(function(){"use strict";function handleModeExitKeys(event){if(event.key!=="Escape"&&event.key!=="Enter")return;event.preventDefault();if(event.key==="Escape"){event.target.value=event.target.original_value;};let focusout=new Event("focusout");event.target.dispatchEvent(focusout);};document.querySelector("body").addEventListener("dblclick",(evt)=>{if(evt.target.tagName!=="INPUT")return;if(!["date","datetime-local"].includes(evt.target.attributes.type.value.toLowerCase()))return;let target=evt.target;let original_type=target.attributes.type.value;target.type="text";target.original_value=target.value;target.classList.add("mode_textdate");setTimeout(()=>{target.select();});target.addEventListener("focusout",()=>{target.type=original_type;delete event.target.original_value;target.classList.remove("mode_textdate");target.removeEventListener("keydown",handleModeExitKeys);},{once:true});target.addEventListener("keydown",handleModeExitKeys);});})()
\ No newline at end of file
--- a/share/roundup/templates/responsive/html/page.html	Sat Jan 18 11:20:20 2025 -0500
+++ b/share/roundup/templates/responsive/html/page.html	Sat Jan 18 12:23:23 2025 -0500
@@ -278,6 +278,7 @@
     </footer>
   </div> <!-- container -->
   <pre tal:condition="request/form/deissue | nothing" tal:content="request"></pre>
+  <script type="text/javascript" src='@@file/datecopy.min.js'></script>
  </body>
 </html>
 </tal:block>
--- a/test/test_dates.py	Sat Jan 18 11:20:20 2025 -0500
+++ b/test/test_dates.py	Sat Jan 18 12:23:23 2025 -0500
@@ -483,6 +483,11 @@
         toomuch = datetime.MAXYEAR + 1
         self.assertRaises(ValueError, Date, (toomuch, 1, 1, 0, 0, 0, 0, 1, -1))
 
+    def testRfc3339Form(self):
+        ae = self.assertEqual
+        d = Date('2003-11-01T00:00:00')
+        self.assertEqual(str(d), '2003-11-01.00:00:00')
+
     def testSimpleTZ(self):
         ae = self.assertEqual
         # local to utc
--- a/test/test_templating.py	Sat Jan 18 11:20:20 2025 -0500
+++ b/test/test_templating.py	Sat Jan 18 12:23:23 2025 -0500
@@ -5,6 +5,7 @@
 from roundup.anypy.cgi_ import FieldStorage, MiniFieldStorage
 from roundup.cgi.templating import *
 from roundup.cgi.ZTUtils.Iterator import Iterator
+from roundup.test import memorydb
 from .test_actions import MockNull, true
 from .html_norm import NormalizingHtmlParser
 
@@ -716,7 +717,11 @@
     def setUp(self):
         self.form = FieldStorage()
         self.client = MockNull()
-        self.client.db = db = MockDatabase()
+        self.client.db = db = memorydb.create('admin')
+        db.tx_Source = "web"
+
+        db.issue.addprop(tx_Source=hyperdb.String())
+
         db.security.hasPermission = lambda *args, **kw: True
         self.client.form = self.form
 
@@ -725,8 +730,102 @@
         self.client.session_api = MockNull(_sid="1234567890")
         self.client.db.getuid = lambda : 10
 
+    @pytest.fixture(autouse=True)
+    def inject_fixtures(self, caplog):
+        self._caplog = caplog
+
 class DateHTMLPropertyTestCase(HTMLPropertyTestClass):
 
+    def setUp(self):
+        super(DateHTMLPropertyTestCase, self).setUp()
+
+        db = self.client.db
+        db.issue.addprop(deadline=hyperdb.Date())
+
+        self.test_datestring = "2021-01-01.11:22:10"
+
+        self.client.db.issue.create(title="title",
+                                    deadline=date.Date(self.test_datestring))
+        self.client.db.getUserTimezone = lambda: "2"
+
+    def tearDown(self):
+        self.client.db.close()
+        memorydb.db_nuke('')
+
+    def test_DateHTMLWithDate(self):
+        """Test methods when DateHTMLProperty._value is a hyperdb.Date()
+        """
+        test_datestring = self.test_datestring
+        test_Date = self.client.db.issue.get("1", 'deadline')
+        test_hyperdbDate = self.client.db.issue.getprops("1")['deadline']
+
+        self.client.classname = "issue"
+        self.client.template = "item"
+
+        # client, classname, nodeid, prop, name, value,
+        #    anonymous=0, offset=None
+        d = DateHTMLProperty(self.client, 'issue', '1', test_hyperdbDate,
+                             'deadline', test_Date)
+        self.assertIsInstance(d._value, date.Date)
+        self.assertEqual(d.pretty(), " 1 January 2021")
+        self.assertEqual(d.pretty(format="%Y-%m"), "2021-01")
+        self.assertEqual(d.plain(), "2021-01-01.13:22:10")
+        self.assertEqual(d.local("-4").plain(), "2021-01-01.07:22:10")
+        input_expected = """<input id="issue1@deadline" name="issue1@deadline" size="30" type="date" value="2021-01-01">"""
+        self.assertEqual(d.field(), input_expected)
+        self.assertEqual(d.field_time(type="date"), input_expected)
+
+        input_expected = """<input id="issue1@deadline" name="issue1@deadline" size="30" type="datetime-local" value="2021-01-01T13:22:10">"""
+        self.assertEqual(d.field(type="datetime-local"), input_expected)
+        self.assertEqual(d.field_time(), input_expected)
+
+        input_expected = """<input id="issue1@deadline" name="issue1@deadline" size="30" type="text" value="2021-01-01.13:22:10">"""
+        self.assertEqual(d.field(type="text"), input_expected)
+        self.assertEqual(d.field_time(type="text"), input_expected)
+
+        # test with format
+        input_expected = """<input id="issue1@deadline" name="issue1@deadline" size="30" type="text" value="2021-01"><a class="classhelp" data-calurl="issue?@template=calendar&amp;amp;property=deadline&amp;amp;form=itemSynopsis&amp;date=2021-01-01.11:22:10" data-height="200" data-width="300" href="javascript:help_window(\'issue?@template=calendar&amp;property=deadline&amp;form=itemSynopsis&date=2021-01-01.11:22:10\', 300, 200)">(cal)</a>"""
+
+        self._caplog.clear()
+        with self._caplog.at_level(logging.WARNING,
+                                   logger="roundup"):
+            input = d.field(format="%Y-%m")
+        self.assertEqual(input_expected, input)
+
+        # name used for logging
+        log_expected = """Format '%Y-%m' prevents use of modern date input. Remove format from field() call in template issue.item. Using text input."""
+        self.assertEqual(self._caplog.record_tuples[0][2], log_expected)
+        # severity ERROR = 40
+        self.assertEqual(self._caplog.record_tuples[0][1], 30,
+                        msg="logging level != 30 (WARNING)")
+
+        # test with format and popcal=None
+        input_expected = """<input id="issue1@deadline" name="issue1@deadline" size="30" type="text" value="2021-01">"""
+
+        self._caplog.clear()
+        with self._caplog.at_level(logging.WARNING,
+                                   logger="roundup"):
+            input = d.field(format="%Y-%m", popcal=False)
+        self.assertEqual(input_expected, input)
+
+        # name used for logging
+        log_expected = """Format '%Y-%m' prevents use of modern date input. Remove format from field() call in template issue.item. Using text input."""
+        self.assertEqual(self._caplog.record_tuples[0][2], log_expected)
+        # severity ERROR = 40
+        self.assertEqual(self._caplog.record_tuples[0][1], 30,
+                        msg="logging level != 30 (WARNING)")
+
+        # test with format, type=text and popcal=None
+        input_expected = """<input id="issue1@deadline" name="issue1@deadline" size="30" type="text" value="2021-01">"""
+
+        self._caplog.clear()
+        with self._caplog.at_level(logging.WARNING,
+                                   logger="roundup"):
+            input = d.field(type="text", format="%Y-%m", popcal=False )
+        self.assertEqual(input_expected, input)
+
+        self.assertEqual(self._caplog.records, [])
+
     def test_DateHTMLWithText(self):
         """Test methods when DateHTMLProperty._value is a string
            rather than a hyperdb.Date()
@@ -741,6 +840,9 @@
             ( test = MockNull(getprops = lambda : test_date)
             )
 
+        self.client.classname = "test"
+        self.client.template = "item"
+
         # client, classname, nodeid, prop, name, value,
         #    anonymous=0, offset=None
         d = DateHTMLProperty(self.client, 'test', '1', self.client._props,
@@ -748,9 +850,62 @@
         self.assertIs(type(d._value), str)
         self.assertEqual(d.pretty(), "2021-01-01 11:22:10")
         self.assertEqual(d.plain(), "2021-01-01 11:22:10")
-        input = """<input id="test1@test" name="test1@test" size="30" type="text" value="2021-01-01 11:22:10"><a class="classhelp" data-calurl="test?@template=calendar&amp;amp;property=test&amp;amp;form=itemSynopsis&amp;date=2021-01-01 11:22:10" data-height="200" data-width="300" href="javascript:help_window('test?@template=calendar&amp;property=test&amp;form=itemSynopsis&date=2021-01-01 11:22:10', 300, 200)">(cal)</a>"""
+        input = """<input id="test1@test" name="test1@test" size="30" type="date" value="2021-01-01 11:22:10">"""
         self.assertEqual(d.field(), input)
 
+
+        input_expected = """<input id="test1@test" name="test1@test" size="40" type="date" value="2021-01-01 11:22:10">"""
+        self.assertEqual(d.field(size=40), input_expected)
+
+        input_expected = """<input id="test1@test" name="test1@test" size="30" type="text" value="2021-01-01 11:22:10"><a class="classhelp" data-calurl="test?@template=calendar&amp;amp;property=test&amp;amp;form=itemSynopsis&amp;date=2021-01-01 11:22:10" data-height="200" data-width="300" href="javascript:help_window(\'test?@template=calendar&amp;property=test&amp;form=itemSynopsis&date=2021-01-01 11:22:10\', 300, 200)">(cal)</a>"""
+        with self._caplog.at_level(logging.WARNING,
+                                   logger="roundup"):
+            input = d.field(format="%Y-%m")
+        self.assertEqual(input_expected, input)
+
+        # name used for logging
+        log_expected = """Format '%Y-%m' prevents use of modern date input. Remove format from field() call in template test.item. Using text input."""
+        self.assertEqual(self._caplog.record_tuples[0][2], log_expected)
+        # severity ERROR = 40
+        self.assertEqual(self._caplog.record_tuples[0][1], 30,
+                        msg="logging level != 30 (WARNING)")
+
+        """with self.assertRaises(ValueError) as e:
+            d.field(format="%Y-%m")
+        self.assertIn("'%Y-%m'", e.exception.args[0])
+        self.assertIn("'date'", e.exception.args[0])"""
+
+
+        # format matches rfc format, so this should pass
+        result = d.field(format="%Y-%m-%d")
+        input_expected = """<input id="test1@test" name="test1@test" size="30" type="date" value="2021-01-01 11:22:10">"""
+        self.assertEqual(result, input_expected)
+
+        input_expected = """<input id="test1@test" name="test1@test" size="30" type="text" value="2021-01-01 11:22:10"><a class="classhelp" data-calurl="test?@template=calendar&amp;amp;property=test&amp;amp;form=itemSynopsis&amp;date=2021-01-01 11:22:10" data-height="200" data-width="300" href="javascript:help_window(\'test?@template=calendar&amp;property=test&amp;form=itemSynopsis&date=2021-01-01 11:22:10\', 300, 200)">(cal)</a>"""
+        with self._caplog.at_level(logging.WARNING,
+                                   logger="roundup"):
+            input = d.field(format="%Y-%m", type="datetime-local")
+        self.assertEqual(input_expected, input)
+
+        # name used for logging
+        log_expected = """Format '%Y-%m' prevents use of modern date input. Remove format from field() call in template test.item. Using text input."""
+        self.assertEqual(self._caplog.record_tuples[0][2], log_expected)
+        # severity ERROR = 40
+        self.assertEqual(self._caplog.record_tuples[0][1], 30,
+                        msg="logging level != 30 (WARNING)")
+
+        """
+        with self.assertRaises(ValueError) as e:
+            d.field(format="%Y-%m", type="datetime-local")
+        self.assertIn("'%Y-%m'", e.exception.args[0])
+        self.assertIn("'datetime-local'", e.exception.args[0])
+        """
+
+        # format matches rfc, so this should pass
+        result = d.field(type="datetime-local", format="%Y-%m-%dT%H:%M:%S")
+        input_expected = """<input id="test1@test" name="test1@test" size="30" type="datetime-local" value="2021-01-01 11:22:10">"""
+        self.assertEqual(result, input_expected)
+
 # common markdown test cases
 class MarkdownTests:
     def mangleMarkdown2(self, s):

Roundup Issue Tracker: http://roundup-tracker.org/