elias Published Feb. 20, 2023 · 4 min read

A Django template tag for displaying datetime ranges

A decorative hero image for this page.

A simple implementation of a custom Django template tag to render date ranges in your HTML templates.

HTML django templates

Something that comes up a lot when building web applications with Django is displaying a date range to the user. It might be the period during which a registration form is open or the duration of an event.

We sometimes use a handy custom template tag which optimizes how a date time range is displayed so that it doesn't look too verbose. Using it looks something like this:

{% load app_name %}
<p>
    Event time:
    {% datetimerange start end %}
</p>

Our goal was to handle these two cases:

  • If start and end are less than 24 hours apart, we want to display the date only once and communicate the range between the times.
  • Otherwise we need to display both dates and times.

For instance, if we set start to 17:00 February 18, 2023 and end to 02:00 February 19, 2023 we want to get something like this: 18/02/2023 17:00 - 02:00. If end was later, like 09:00 February 20, 2023, we'd like to get something like 18/02/2023 17:00 - 20/02/2023 09:00.

Additionally, we can also handle incomplete ranges where either start or end are None by writing Until %(datetime)s or From %(datetime)s, respectively. If both date times are None we can just return None in our template tag and use the builtin default filter to provide a default value as needed.

We can use the builtin date filter to handle datetime, date and time formatting. This filter takes either a format string or an alias from the Django configuration (see e.g. SHORT_DATE_FORMAT or TIME_FORMAT).

Implementing the datetimerange filter

There's a lot of work that Django has already done for us so we try to build on existing utilities as much as possible. Here's the code that you can put in <app_name>/templatetags/<app_name>.py (see the guide to creating custom template tags and filters to know more about how to structure this code).

from django import template
from django.template.defaultfilters import date as date_filter
from django.utils import timezone
from django.utils.html import format_html
from django.utils.translation import gettext as _

register = template.Library()


@register.simple_tag()
def datetimerangeformat(
    start: timezone.datetime,
    end: timezone.datetime,
    datetime_format: str = 'SHORT_DATETIME_FORMAT',
    date_format: str = 'SHORT_DATE_FORMAT',
    time_format: str = 'TIME_FORMAT'
):
    '''
    Displays the range between the two given datetimes.

    - If either `start` or `end` is `None`, then it displays *Until {end}* or
      *From {start}*.
    - If neither is `None` and they are less than 24 hours apart, it displays
      *{start.date} {start.time} - {end.time}.*
    - If both are `None` it returns `None`. Use the builtin `default` filter to
      provide a default value if necessary.

    Datetimes are localized to the current timezone. Datetimes, dates and times
    are formatted using the builtin `date` filter which is passed each of the
    keyword arguments to this tag appropriately.

    Translation of the *From* and *Until* words is included.
    '''
    tz = timezone.get_current_timezone()
    if start is not None:
        start = start.astimezone(tz)

    if end is not None:
        end = end.astimezone(tz)

    if start is None and end is None:
        return None

    if end is None:
        return _('From %(datetime)s') % {
            'datetime': date_filter(start, datetime_format)
        }
    if start is None:
        return _('Until %(datetime)s') % {
            'datetime': date_filter(end, datetime_format)
        }

    if end - start < timezone.timedelta(hours=24):
        inside = '%s %s - %s' % (
            date_filter(start, date_format),
            date_filter(start, time_format),
            date_filter(end, time_format)
        )
    else:
        inside = '%s - %s' % (
            date_filter(start, datetime_format),
            date_filter(end, datetime_format)
        )

    return format_html(
        '<span title="{} - {}">{}</span>',
        date_filter(start, datetime_format),
        date_filter(end, datetime_format),
        inside
    )

We've found it pretty useful lately and hope you do too!

Ready to bring your vision to life?

Get in touch