(2019-10-08) Working with Dates and Times in the Forms (ISO 8601, Django 3.1)

Introduction

HTML5 comes with a bunch of new types for the input fields that are rendered as rich native widgets.

Browsers even restrict invalid values and validate the input immediately. Let’s explore how we could make use of them in Django forms.

We will be using an Exhibition model with models.DateField, models.TimeField, and models.DateTimeField:

exhibitions/models.py

# exhibitions/models.py
from django.db import models
from django.utils.translation import gettext_lazy as _

class Exhibition(models.Model):
    title = models.CharField(_("Title"), max_length=200)
    start = models.DateField(_("Start"))
    end = models.DateField(_("End"), blank=True, null=True)
    opening = models.TimeField(_("Opening every day"))
    closing = models.TimeField(_("Closing every day"))
    vernissage = models.DateTimeField(_("Vernissage"), blank=True, null=True)
    finissage = models.DateTimeField(_("Finissage"), blank=True, null=True)

    class Meta:
        verbose_name = _("Exhibition")
        verbose_name_plural = _("Exhibitions")

    def __str__(self):
        return self.title

exhibitions/forms.py

Here is a quick model form for the Exhibition model:

# exhibitions/forms.py
from django import forms
from .models import Exhibition

class ExhibitionForm(forms.ModelForm):
    class Meta:
        model = Exhibition
        fields = "__all__"

If we now open a Django shell and create an instance of the model form with some initial values, then print the form as HTML to the console, we will notice, that all date and time fields are rendered as <input type=”text” /> and the values for the dates are in a local format, not the ISO standard YYYY-MM-DD:

(venv)$ python manage.py shell
>>> from exhibitions.forms import ExhibitionForm
>>> from datetime import datetime, date, time
>>> form = ExhibitionForm(initial={
...     "start": date(2020, 1, 1),
...     "end": date(2020, 3, 31),
...     "opening": time(11, 0),
...     "closing": time(20, 0),
...     "vernissage": datetime(2019, 12, 27, 19, 0),
...     "finissage": datetime(2020, 4, 1, 19, 0),
>>> })
>>> print(form.as_p())
<p><label for="id_title">Title:</label> <input type="text" name="title" maxlength="200" required id="id_title"></p>
<p><label for="id_start">Start:</label> <input type="text" name="start" value="01.01.2020" required id="id_start"></p>
<p><label for="id_end">End:</label> <input type="text" name="end" value="31.03.2020" id="id_end"></p>
<p><label for="id_opening">Opening every day:</label> <input type="text" name="opening" value="11:00:00" required id="id_opening"></p>
<p><label for="id_closing">Closing every day:</label> <input type="text" name="closing" value="20:00:00" required id="id_closing"></p>
<p><label for="id_vernissage">Vernissage:</label> <input type="text" name="vernissage" value="27.12.2019 19:00:00" id="id_vernissage"></p>
<p><label for="id_finissage">Finissage:</label> <input type="text" name="finissage" value="01.04.2020 19:00:00" id="id_finissage"></p>

exhibitions/forms.py

Let’s modify the model form and customize the date and time inputs.

We will extend and use forms.DateInput, forms.TimeInput, and forms.DateTimeInput widgets. We want to show date inputs as <input type=”date” />, time inputs as <input type=”time” />, and date-time inputs as <input type=”datetime-local” />.

In addition, the format for the dates should be based on ISO standard.

 1 # exhibitions/forms.py
 2 from django import forms
 3 from .models import Exhibition
 4
 5
 6 class DateInput(forms.DateInput):
 7     input_type = "date"
 8
 9     def __init__(self, **kwargs):
10         kwargs["format"] = "%Y-%m-%d"
11         super().__init__(**kwargs)
12
13
14 class TimeInput(forms.TimeInput):
15     input_type = "time"
16
17
18 class DateTimeInput(forms.DateTimeInput):
19     input_type = "datetime-local"
20
21     def __init__(self, **kwargs):
22         kwargs["format"] = "%Y-%m-%dT%H:%M"
23         super().__init__(**kwargs)
24
25
26 class ExhibitionForm(forms.ModelForm):
27     class Meta:
28         model = Exhibition
29         fields = "__all__"
30
31     def __init__(self, *args, **kwargs):
32         super().__init__(*args, **kwargs)
33         self.fields["start"].widget = DateInput()
34         self.fields["end"].widget = DateInput()
35         self.fields["opening"].widget = TimeInput()
36         self.fields["closing"].widget = TimeInput()
37         self.fields["vernissage"].widget = DateTimeInput()
38         self.fields["vernissage"].input_formats = ["%Y-%m-%dT%H:%M", "%Y-%m-%d %H:%M"]
39         self.fields["finissage"].widget = DateTimeInput()
40         self.fields["finissage"].input_formats = ["%Y-%m-%dT%H:%M", "%Y-%m-%d %H:%M"]

Let’s see now in the Django shell if that worked as expected:

(venv)$ python manage.py shell
>>> from exhibitions.forms import ExhibitionForm
>>> from datetime import datetime, date, time
>>> form = ExhibitionForm(initial={
...     "start": date(2020, 1, 1),
...     "end": date(2020, 3, 31),
...     "opening": time(11, 0),
...     "closing": time(20, 0),
...     "vernissage": datetime(2019, 12, 27, 19, 0),
...     "finissage": datetime(2020, 4, 1, 19, 0),
>>> })
>>> print(form.as_p())
<p><label for="id_title">Title:</label> <input type="text" name="title" maxlength="200" required id="id_title"></p>
<p><label for="id_start">Start:</label> <input type="date" name="start" value="2020-01-01" required id="id_start"></p>
<p><label for="id_end">End:</label> <input type="date" name="end" value="2020-03-31" id="id_end"></p>
<p><label for="id_opening">Opening every day:</label> <input type="time" name="opening" value="11:00:00" required id="id_opening"></p>
<p><label for="id_closing">Closing every day:</label> <input type="time" name="closing" value="20:00:00" required id="id_closing"></p>
<p><label for="id_vernissage">Vernissage:</label> <input type="datetime-local" name="vernissage" value="2019-12-27T19:00" id="id_vernissage"></p>
<p><label for="id_finissage">Finissage:</label> <input type="datetime-local" name="finissage" value="2020-04-01T19:00" id="id_finissage"></p>

The same way you can also create widgets for other HTML5 input types: color, email, month, number, range, tel, url, week, and alike.

Happy coding!

https://github.com/django/django/pull/11893 , Fixed django#11385 – Made forms.DateTimeField accept ISO 8601 date input

class DateTimeFieldTest(SimpleTestCase):
@@ -39,6 +40,25 @@ def test_datetimefield_clean_defaults(self):
        with self.assertRaisesMessage(ValidationError, "'Enter a valid date/time.'"):
            f.clean('2006-10-25 4:30 p.m.')

    def test_datetimefield_iso8601(self):
        f = DateTimeField()
        self.assertEqual(
            datetime(2014, 9, 23, 22, 34, 41, 614804),
            f.clean('2014-09-23T22:34:41.614804')
        )
        self.assertEqual(datetime(2014, 9, 23, 22, 34, 41), f.clean('2014-09-23T22:34:41'))
        self.assertEqual(datetime(2014, 9, 23, 22, 34), f.clean('2014-09-23T22:34'))
        self.assertEqual(
            datetime(2014, 9, 23, 22, 34, tzinfo=utc),
            f.clean('2014-09-23T22:34Z')
        )
        self.assertEqual(
            datetime(2014, 9, 23, 22, 34, tzinfo=timezone(timedelta(hours=7))),
            f.clean('2014-09-23T22:34+07:00')
        )
        with self.assertRaisesMessage(ValidationError, "'Enter a valid date/time.'"):
            f.clean('2014-09-23T28:23')

raw.githubusercontent.com/claudep/django/2e7807bcbc78762b241b5899f787b1de13156ee6/tests/forms_tests/field_tests/test_datetimefield.py

from datetime import date, datetime, timedelta, timezone

from django.forms import DateTimeField, ValidationError
from django.test import SimpleTestCase
from django.utils.timezone import utc


class DateTimeFieldTest(SimpleTestCase):

    def test_datetimefield_clean_defaults(self):
        f = DateTimeField()
        self.assertEqual(datetime(2006, 10, 25, 0, 0), f.clean(date(2006, 10, 25)))
        self.assertEqual(datetime(2006, 10, 25, 14, 30), f.clean(datetime(2006, 10, 25, 14, 30)))
        self.assertEqual(
            datetime(2006, 10, 25, 14, 30, 59),
            f.clean(datetime(2006, 10, 25, 14, 30, 59))
        )
        self.assertEqual(
            datetime(2006, 10, 25, 14, 30, 59, 200),
            f.clean(datetime(2006, 10, 25, 14, 30, 59, 200))
        )
        self.assertEqual(datetime(2006, 10, 25, 14, 30, 45, 200), f.clean('2006-10-25 14:30:45.000200'))
        self.assertEqual(datetime(2006, 10, 25, 14, 30, 45, 200), f.clean('2006-10-25 14:30:45.0002'))
        self.assertEqual(datetime(2006, 10, 25, 14, 30, 45), f.clean('2006-10-25 14:30:45'))
        self.assertEqual(datetime(2006, 10, 25, 14, 30), f.clean('2006-10-25 14:30:00'))
        self.assertEqual(datetime(2006, 10, 25, 14, 30), f.clean('2006-10-25 14:30'))
        self.assertEqual(datetime(2006, 10, 25, 0, 0), f.clean('2006-10-25'))
        self.assertEqual(datetime(2006, 10, 25, 14, 30, 45, 200), f.clean('10/25/2006 14:30:45.000200'))
        self.assertEqual(datetime(2006, 10, 25, 14, 30, 45), f.clean('10/25/2006 14:30:45'))
        self.assertEqual(datetime(2006, 10, 25, 14, 30), f.clean('10/25/2006 14:30:00'))
        self.assertEqual(datetime(2006, 10, 25, 14, 30), f.clean('10/25/2006 14:30'))
        self.assertEqual(datetime(2006, 10, 25, 0, 0), f.clean('10/25/2006'))
        self.assertEqual(datetime(2006, 10, 25, 14, 30, 45, 200), f.clean('10/25/06 14:30:45.000200'))
        self.assertEqual(datetime(2006, 10, 25, 14, 30, 45), f.clean('10/25/06 14:30:45'))
        self.assertEqual(datetime(2006, 10, 25, 14, 30), f.clean('10/25/06 14:30:00'))
        self.assertEqual(datetime(2006, 10, 25, 14, 30), f.clean('10/25/06 14:30'))
        self.assertEqual(datetime(2006, 10, 25, 0, 0), f.clean('10/25/06'))
        with self.assertRaisesMessage(ValidationError, "'Enter a valid date/time.'"):
            f.clean('hello')
        with self.assertRaisesMessage(ValidationError, "'Enter a valid date/time.'"):
            f.clean('2006-10-25 4:30 p.m.')

    def test_datetimefield_iso8601(self):
        f = DateTimeField()
        self.assertEqual(
            datetime(2014, 9, 23, 22, 34, 41, 614804),
            f.clean('2014-09-23T22:34:41.614804')
        )
        self.assertEqual(datetime(2014, 9, 23, 22, 34, 41), f.clean('2014-09-23T22:34:41'))
        self.assertEqual(datetime(2014, 9, 23, 22, 34), f.clean('2014-09-23T22:34'))
        self.assertEqual(
            datetime(2014, 9, 23, 22, 34, tzinfo=utc),
            f.clean('2014-09-23T22:34Z')
        )
        self.assertEqual(
            datetime(2014, 9, 23, 22, 34, tzinfo=timezone(timedelta(hours=7))),
            f.clean('2014-09-23T22:34+07:00')
        )
        with self.assertRaisesMessage(ValidationError, "'Enter a valid date/time.'"):
            f.clean('2014-09-23T28:23')

    def test_datetimefield_clean_input_formats(self):
        f = DateTimeField(input_formats=['%Y %m %d %I:%M %p'])
        self.assertEqual(datetime(2006, 10, 25, 0, 0), f.clean(date(2006, 10, 25)))
        self.assertEqual(datetime(2006, 10, 25, 14, 30), f.clean(datetime(2006, 10, 25, 14, 30)))
        self.assertEqual(
            datetime(2006, 10, 25, 14, 30, 59),
            f.clean(datetime(2006, 10, 25, 14, 30, 59))
        )
        self.assertEqual(
            datetime(2006, 10, 25, 14, 30, 59, 200),
            f.clean(datetime(2006, 10, 25, 14, 30, 59, 200))
        )
        self.assertEqual(datetime(2006, 10, 25, 14, 30), f.clean('2006 10 25 2:30 PM'))
        # ISO-like formats are always accepted, even when not in input_formats.
        self.assertEqual(datetime(2006, 10, 25, 14, 30, 45), f.clean('2006-10-25 14:30:45'))
        with self.assertRaisesMessage(ValidationError, "'Enter a valid date/time.'"):
            f.clean('2006.10.25 14:30:45')

        f = DateTimeField(input_formats=['%Y.%m.%d %H:%M:%S.%f'])
        self.assertEqual(
            datetime(2006, 10, 25, 14, 30, 45, 200),
            f.clean('2006.10.25 14:30:45.0002')
        )

    def test_datetimefield_not_required(self):
        f = DateTimeField(required=False)
        self.assertIsNone(f.clean(None))
        self.assertEqual('None', repr(f.clean(None)))
        self.assertIsNone(f.clean(''))
        self.assertEqual('None', repr(f.clean('')))

    def test_datetimefield_whitespace_stripping(self):
        f = DateTimeField()
        # Test whitespace stripping behavior (#5714)
        self.assertEqual(datetime(2006, 10, 25, 14, 30, 45), f.clean(' 2006-10-25   14:30:45 '))
        self.assertEqual(datetime(2006, 10, 25, 0, 0), f.clean(' 2006-10-25 '))
        self.assertEqual(datetime(2006, 10, 25, 14, 30, 45), f.clean(' 10/25/2006 14:30:45 '))
        self.assertEqual(datetime(2006, 10, 25, 14, 30), f.clean(' 10/25/2006 14:30 '))
        self.assertEqual(datetime(2006, 10, 25, 0, 0), f.clean(' 10/25/2006 '))
        self.assertEqual(datetime(2006, 10, 25, 14, 30, 45), f.clean(' 10/25/06 14:30:45 '))
        self.assertEqual(datetime(2006, 10, 25, 0, 0), f.clean(' 10/25/06 '))
        with self.assertRaisesMessage(ValidationError, "'Enter a valid date/time.'"):
            f.clean('   ')

    def test_datetimefield_changed(self):
        format = '%Y %m %d %I:%M %p'
        f = DateTimeField(input_formats=[format])
        d = datetime(2006, 9, 17, 14, 30, 0)
        self.assertFalse(f.has_changed(d, '2006 09 17 2:30 PM'))