Rendering a field's widget value in a template, with correct format

I’m rendering form fields manually in my Django templates. It works fine but I’ve hit a problem with a custom Field’s format parameter not being used when rendering a field’s value attribute.

To render an <input type="datetime-local"> I have this custom Input and Field:

from django import forms

class DateTimeLocalInput(forms.DateTimeInput):
    input_type = "datetime-local"

class DateTimeLocalField(forms.DateTimeField):
    input_formats = [
        "%Y-%m-%dT%H:%M:%S", "%Y-%m-%dT%H:%M:%S.%f", "%Y-%m-%dT%H:%M"
    ]
    widget = DateTimeLocalInput(format="%Y-%m-%dT%H:%M")

Then I use this in a model form, for a field DateTimeField model attribute like this:

class MyForm(forms.ModelForm):
    fieldname = DateTimeLocalField(label="My field")

In a template if I render a field by doing {{ form.fieldname }} then it outputs as I’d expect, using the format parameter in the last line, above. e.g.:

<input type="datetime-local" name="fieldname" value="2023-11-02T22:30" id="id_fieldname">

But if I render a field’s value attribute manually like this:

<input ... value="{{ form.fieldname.value|default_if_none:""|stringformat:'s' }}">

Then it comes out like this:

<input ... value="2023-11-02 22:30:35">

You can see that the Field’s format is ignored: the value is output with no T in the middle, and with the seconds included. This is a problem in Safari, which refuses to accept that as a valid format.

I can’t see how to get round this. In a widget’s context data, value is set as self.format_value(value) (seen here on GitHub), but I think that’s not happening when just using {{ form.fieldname.value }}.

why try difficult???
just set widget.

forms.py
class MyForm(forms.ModelForm):
  fieldname = forms.DatetimeField(
    widgets=(attrs={'type': 'datetime-local'})
  )
template
<form>
  {{ form }}
</form>
1 Like

Because I want more control over the HTML.

what conrol???

Can we accept that some people care about the exact HTML of their pages, and wish to render forms manually as described in the Django documentation, and focus on the question I asked? Thanks.

If you want to manually fill out the input field… I think the problem is that you entered the date format incorrectly. Try like this.

<input type="datetime-local" value="{{ form.datefield.value|date:'Y-m-d H:i:s' }}">

The format is correct. From MDN’s description of the datetime-local input:

One thing to note is that the displayed date and time formats differ from the actual value ; the displayed date and time are formatted according to the user’s locale as reported by their operating system, whereas the date/time value is always formatted YYYY-MM-DDThh:mm .

Oh, I’m sorry. There was something I misunderstood.

The format of year/month/day hour:minute can be applied as follows.

{% if form.fieldname.value %}
<input type="datetime-local" value="{{ form.fieldname.value|date:'Y-m-d\TH:i' }}">
{% else %}
<input type="datetime-local" value="{% now 'Y-m-d\TH:i' %}">
{% endif %}

Thanks. That becomes more difficult if you’re looping through all the fields - you have to detect the kind of field and change how to format the output accordingly. Not impossible, but it feels unnecessary when the format is already defined in the widget!

The docs say that form.fieldname.value can be used for this, so I’m wondering why it ignores the widget’s defined format. It feels like a bug (or incorrect docs).

Because the default format of datetime-local is %Y-%m-%d %H:%M:%s.

It will format according to the value you entered, but if you leave it blank, it will use the default format.
It’s not a bug.

if you use multiple date-hour-min fields, i recommend to you just set 0 second in clean_{field} method

But what’s the point of defining the format of that field’s widget if it gets ignored when rendering the field’s value?

if you use multiple date-hour-min fields, i recommend to you just set 0 second in clean_{field} method

0 seconds is set automatically. It’s not the input format that’s the issue, it’s the format of the rendered value in the template.

For now I’ve settled on this, which is a bit horrible but works for this one case:

<input type="{{ field.field.widget.input_type }}" value="{% if field.field.widget.format == '%Y-%m-%dT%H:%M' %}{{ field.value|default_if_none:''|date:'Y-m-d\TH:i' }}{% else %}{{ field.value|default_if_none:''|stringformat:'s' }}{% endif %}">

Putting that on separate lines, just for readability, it’s like this:

<input
  type="{{ field.field.widget.input_type }}"
  value="
    {% if field.field.widget.format == '%Y-%m-%dT%H:%M' %}
      {{ field.value|default_if_none:''|date:'Y-m-d\TH:i' }}
    {% else %}
      {{ field.value|default_if_none:''|stringformat:'s' }}
    {% endif %}
  "
>

I would create a templatetag filter.

   # myapp/templatetags/custom_filters.py
   from django import template
   import datetime

   register = template.Library()

   @register.filter
   def format_datetime(value, format="%Y-%m-%dT%H:%M"):
       if isinstance(value, datetime.datetime):
           return value.strftime(format)
       return value

and then in templates:

   {% load custom_filters %}

   <input type="datetime-local" name="fieldname" value="{{ form.fieldname.value|default_if_none:""|format_datetime }}" id="id_fieldname">
   

Good idea! I’ve made my filter more general so that I can use it for the value attribute of any <input> field. It will output the BoundField’s value, respecting its widget’s format, if any:

from datetime import date, datetime, time
from django import template

register = template.Library()

@register.filter
def format_value(field):
    widget = field.field.widget
    value = field.value()

    if type(value) in [date, datetime, time]:
        if hasattr(widget, "format") and widget.format is not None:
            format = widget.format
        elif type(value) is date:
            format = "%Y-%m-%d"
        elif type(value) is datetime:
            format = "%Y-%m-%dT%H:%M"
        elif type(value) is time:
            format = "%H:%M:%S"
        return value.strftime(format)
    elif value is None:
        return ""
    else:
        return str(value)

Use like this in a template:

<input ... value="{{ form.fieldname|format_value }}">

It’s possible that a widget’s format is used for fields that aren’t date/datetime/time, in which case this would need modification, but it’s good enough for me at the moment.

Thanks for the help!

1 Like