24. Date- and DateTime Input¶
Modern browsers offer input fields with built-in date and date-time pickers, hence usually there is no need to use a special widget accepting datestamps. However, the built-in date and date-time pickers are impossible to style using CSS or other means. It therefore is impossible to attach context information to the dates and times.
The JavaScript ecosystem offers a huge set of libraries with alternative date- and time-pickers, for instance Flatpickr or Pikaday to name a few, written in pure JavaScript and without any framework. Using one of those would of course be an option. However, it then still is not possible to change the rendering context based on information only available on the server, for instance the vacancy of reservable time slots.
Moreover, JavaScript does not offer any functionality for non-trivial calendar arithmetic, so this has to be implemented by every date picker library.
24.1. Date Input Widget¶
Django by default uses the HTML field <input type="text" …>
to accept dates and datetime as
input. This presumably has historic reasons, because browsers started to support this field type
only from 2018 onwards. Since these fields adopt themselves to the browser’s locale setting, it is
possible to enter dates in different formats. For instance, in the Anglo Saxon area, dates are
formatted as mm/dd/yyyy
, whereas in Europe they are formatted as dd.mm.yyyy
. Japan uses
yyyy月mm日dd
as date format, but in web applications yyyy/mm/dd
is commonly used. This
means that the conversion from a string in potentially different formats, must be handled by the
server which usually does not know where the user is located. This input field furthermore offers a
date picker.
For this reason django-formset offers the widgets formset.widgets.DateInput
. This
widget renders a date field as
<input type="date" … />
and makes usage of the browser’s built-in date picker. The date format used by the input field then adopts itself to the browser’s current locale setting.
from django.forms import fields, forms
from formset.widgets import DateInput
class ArticleForm(forms.Form):
date = fields.DateField(
widget=DateInput,
)
django-formset also offers the widget formset.widgets.DateTimeInput
. This widget
renders as date-time field as
<input type="datetime-local" … />
and makes usage of the browser’s built-in date-time picker. The date format used by the input field then adopts itself to the browser’s current locale setting.
from django.forms import fields, forms
from formset.widgets import DateTimeInput
class PurchaseForm(forms.Form):
timestamp = fields.DateTimeField(
widget=DateTimeInput,
)
24.2. Date Picker Widget¶
In addition to the two native widgets formset.widgets.DatePicker
and
formset.widgets.DateTimePicker
mentioned before, django-formset offers widgets which
render the calendar part of the input field server-side, using Python’s built-in Calendar class.
This gives us finer control over the styling of the date picker, and offers the same user experience
across all browsers. They furthermore have a more appealing user interface which is consistent with
the date- and date-time range fields provided by django-formset.
In this example, we want to add a field to enter the publishing date of our blog. By using the named widgets instead of the default, this input field opens a calendar, whenever the user clicks on it. While technically possible, it is not recommended to interchange them on the same page or even application as this results in unexpected user experience.
from django.forms import fields, forms
from formset.widgets import DatePicker, DateTimePicker
class BlogForm(forms.Form):
publish_date = fields.DateField(widget=DatePicker)
# other fields
When paginating through calendar sheets, each sheet must be fetched from the server. Therefore the
view controlling our blog form must inherit from the special mixin class
formset.calendar.CalendarResponseMixin
. This class listens on the supplied endpoint and
responds with a HTML snippet of the next sheet.
from formset.calendar import CalendarResponseMixin
from formset.views import FormView
class BlogView(CalendarResponseMixin, FormView):
form_class = BlogForm
template_name = "form.html"
success_url = "/success"
- Mon
- Tue
- Wed
- Thu
- Fri
- Sat
- Sun
- 29
- 30
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 1
- 2
The date format used by the input field adopts itself to the browser’s current locale setting. This
means that in the Anglo Saxon area, dates are formatted as mm/dd/yyyy
, whereas in Europe they are
formatted as dd.mm.yyyy
. Japan uses yyyy/mm/dd
as date format. This setting can be
overridden by adding the attribute date-format
to the widget during instantiation, for instance
DatePicker(attrs={"date-format": "iso"})
.
24.3. Date Textbox Widget¶
If no popup calendar is desired, we can use the widget formset.widgets.DateTextBox
. This
widget is rendered as a simple text box, but still uses the same date format as the date picker
widget. This means that the date format adapts itself to the browser’s locale setting. This setting
can be overridden by adding the attribute date-format
to the widget during instantiation, for
instance DatePicker(attrs={"date-format": "iso"})
.
from formset.widgets import DateTextbox
class BirthdateForm(forms.Form):
birthdate = fields.DateField(widget=DateTextbox)
# other fields
When using this widget, there is no need for the view controlling our blog form to inherit from the
special mixin class formset.calendar.CalendarResponseMixin
, because no calendar sheets have
to be fetched from the server.
24.4. Date Calendar Widget¶
If we don’t want to offer an input field to enter a date, but instead a pageable calendar, then we
can use the widget formset.widgets.DateCalendar
. This widget is then rendered as a calendar
sheet but behaves just as any date input field.
from formset.widgets import DateCalendar
class AugurationForm(forms.Form):
auguration_date = fields.DateField(widget=DateCalendar)
# other fields
Clicking into the calendar’s title switches to the year view. Another click switches to the decade view. By clicking on the up button, we return to the previous calendar sheet. Clicking on the small calendar icon inside the title jumps to the current date. Clicking on a date selects it but does not close the calendar.
- Mon
- Tue
- Wed
- Thu
- Fri
- Sat
- Sun
- 29
- 30
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 1
- 2
When paginating through calendar sheets, each sheet must be fetched from the server. Therefore the
view controlling this form must inherit from the special mixin class
formset.calendar.CalendarResponseMixin
. This class listens on the supplied endpoint and
responds with a HTML snippet of the next sheet.
24.5. Date-Time Picker Widget¶
In our form, we want to add a field to enter the launch date and time. By using
formset.widgets.DateTimePicker
instead of the default widget, this input field opens a
calendar, whenever the user clicks on it. This calendar differs from the default HTML date picker,
which is rendered when using the widget formset.widgets.DateTimeInput
. While technically
possible, it is not recomended to interchange them on the same page or even application as this
results in unexpected user experience.
By clicking on a date inside the DateTimePicker
widget, a 24h view appears. Depending on the
chosen value for the step
attribute , the user can then select a certain time interval. The
step
attribute must be of Python type datetime.timedelta
and can have one of these values:
timedelta(minutes=5)
, timedelta(minutes=10)
, timedelta(minutes=15)
,
timedelta(minutes=20)
, timedelta(minutes=30)
, timedelta(hours=1)
,
timedelta(hours=2)
, timedelta(hours=3)
, timedelta(hours=4)
, timedelta(hours=6)
,
timedelta(hours=8)
and timedelta(hours=12)
. This defines the granularity of the timestamp
the user can select.
from datetime import timedelta
class LaunchForm(forms.Form):
start_datetime = fields.DateTimeField(
widget=DateTimePicker(attrs={'step': timedelta(minutes=5)})
)
- Mon
- Tue
- Wed
- Thu
- Fri
- Sat
- Sun
- 29
- 30
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 1
- 2
24.6. Date and Time Constraints¶
Both widgets formset.widgets.DatePicker
and formset.widgets.DateTimePicker
respect
the minimum and maximum values passed to the widget DatePicker
and DateTimePicker
. By
combining it with now
and timedelta
this becomes very useful, since it prevents users from
selecting dates too far in the past or in the future.
from django.utils.timezone import now
class AppointmentForm(forms.Form):
date = fields.DateField(
widget=DatePicker(attrs={
'min': now().isoformat(),
'max': (now() + timedelta(weeks=2)).isoformat(),
}),
)
This example disables all dates which lie in the past and are more than two weeks in the future.
- Mon
- Tue
- Wed
- Thu
- Fri
- Sat
- Sun
- 29
- 30
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 1
- 2
24.7. Applying Context to the Calendar¶
Apart from not having to integrate date arithmetics into the client-side part of this library, one of the big advantages of using a server side rendered calendar sheet is, that we are able to enrich the rendering context with additional data. Say that we want to show the phases of the moon for each date (this of course could also be done in JavaScript, but here it is used for simple demonstration purposes). Normally one would use some information stored in the database, for instance to display vacant or occupied rooms in a booking application. Or it can be useful to display extra information such as holidays.
from datetime import datetime
from decimal import Decimal
from math import floor
from formset.calendar import CalendarRenderer, ViewMode
class MoonCalendarRenderer(CalendarRenderer):
# Calculate lunar phase by Sean B. Palmer, inamidst.com
# http://en.wikipedia.org/wiki/Lunar_phase#Lunar_phase_calculation
phases = ["🌑", "🌒", "🌓", "🌔", "🌕", "🌖", "🌗", "🌘"]
def position(self, then):
diff = then - datetime(2001, 1, 1)
days = Decimal(diff.days) + Decimal(diff.seconds) / Decimal(86400)
lunations = Decimal('0.20439731') + days * Decimal('0.03386319269')
return lunations % Decimal(1)
def phase(self, pos):
index = pos * Decimal(8) + Decimal('0.5')
index = int(floor(index)) & 7
return self.phases[index]
def get_template_name(self, view_mode):
if view_mode == ViewMode.weeks:
return 'calendar/weeks-moon.html'
return super().get_template_name(view_mode)
def get_context_weeks(self):
context = super().get_context_weeks()
monthdays = []
for monthday in context['monthdays']:
phase = self.phase(self.position(datetime.fromisoformat(monthday[0])))
monthdays.append((*monthday, phase))
context['monthdays'] = monthdays
return context
class MoonForm(forms.Form):
date = fields.DateField(
label="Some Date",
widget=DateCalendar(calendar_renderer=MoonCalendarRenderer),
)
Since this view requires a modified renderer to add additional context, we must tell our special
mixin class formset.calendar.CalendarResponseMixin
to use that by passing it as
calendar_renderer_class
.
class MoonView(CalendarResponseMixin, FormView):
form_class = MoonForm
calendar_renderer_class = MoonCalendarRenderer
template_name = "form.html"
success_url = "/success"
- Mon
- Tue
- Wed
- Thu
- Fri
- Sat
- Sun
- 🌖 29
- 🌗 30
- 🌗 1
- 🌗 2
- 🌘 3
- 🌘 4
- 🌘 5
- 🌘 6
- 🌑 7
- 🌑 8
- 🌑 9
- 🌑 10
- 🌒 11
- 🌒 12
- 🌒 13
- 🌓 14
- 🌓 15
- 🌓 16
- 🌓 17
- 🌔 18
- 🌔 19
- 🌔 20
- 🌔 21
- 🌕 22
- 🌕 23
- 🌕 24
- 🌖 25
- 🌖 26
- 🌖 27
- 🌖 28
- 🌗 29
- 🌗 30
- 🌗 31
- 🌗 1
- 🌘 2