What is correct HTML input type or widget for DurationField?

I’m making a form that users can input duration of time into the fields
I set default values on these fields, and I set widget for these fields as NumberInput, because if I use HTML input type="time", the browser shows AM/PM without additional style or scripting, and this might lead users to confuse about entering duration.
So, I use NumberInput and additional clean_<field> to handle this
This don’t show default value on the input.
What I want to know is, which HTML input type or widget should I use to show users default value and make them not confused.

models.py

import datetime

from django.core.validators import RegexValidator
from django.db import models
from django.urls import reverse

# Create your models here.
...
class Branch(models.Model):
...
    lesson_time = models.DurationField(
        verbose_name="Lesson time",
        default=datetime.timedelta(minutes=110),
    )
    break_time = models.DurationField(
        verbose_name="Break time",
        default=datetime.timedelta(minutes=10),
    )
...

forms.py

import datetime

from django import forms
from django.forms import ModelForm

from branches.models import Branch, Hour


class BranchForm(ModelForm):
...
    def clean_lesson_time(self):
        time = self.data["lesson_time"]
        return datetime.timedelta(minutes=int(time))

    def clean_break_time(self):
        time = self.data["break_time"]
        return datetime.timedelta(minutes=int(time))
...
    class Meta:
        model = Branch
        fields = (
...
            "lesson_time",
            "break_time",
        )
...
            "lesson_time": forms.NumberInput(
                attrs={
                    "aria-describedby": "minute-unit",
                    "class": "form-control",
                }
            ),
            "break_time": forms.NumberInput(
                attrs={
                    "aria-describedby": "minute-unit",
                    "class": "form-control",
                }
            ),
        }

branch_form.html

<!DOCTYPE html>
{% extends "main/base.html" %}
{% load static %}
{% block content %}
    <form action="" method="POST" id="branch-form" class="container d-grid gap-3 justify-content-evenly align-items-center">
        {% csrf_token %}
        {{ form.non_field_errors }}        
...        
        <div class="row">
            <label class="col-3 col-form-label text-nowrap" for="{{ form.lesson_time.id_for_label }}">{{ form.lesson_time.label }}</label>
            <div class="col input-group">{{ form.lesson_time }}
                <span class="input-group-text">minutes</span>
            </div>
            {{ form.lesson_time.errors }}
        </div>
        
        <div class="row">
            <div class="col d-flex justify-content-end">
                <p>
                    &#8251; Enter length of lesson time in minutes.<br>
                    Default value is 110 minutes.
                </p>
            </div>
        </div>
        
        <div class="row">
            <label class="col-3 col-form-label text-nowrap" for="{{ form.break_time.id_for_label }}">{{ form.break_time.label }}</label>
            <div class="col input-group">{{ form.break_time }}<span class="input-group-text">Minutes</span></div>
            {{ form.break_time.errors }}
        </div>
        
        <div class="row">
            <div class="col d-flex justify-content-end">
                <p>
                    &#8251; Enter length of break time in minutes.<br>
                    Default value is 10 minutes.
                </p>
            </div>
        </div>
...
{% endblock %}

This is not django-related but I would have a DurationField set as a number input in the background, i.e, hidden via CSS. Or even as a hidden input.

The (most granular) unit of this field you can choose in an arbitrary way: could be seconds, hours, days or milliseconds.

Then I would enhance the lot with Javascript, i.e., purely client-side. Ideas:

  • split hidden field in several fields that can be interacted with. “Virtual fields” such as one for days, one for hours, etc. Similar to the deconstruct or to_python methods server-side.
  • do conversions client-side in real time. For example, if user enters 1 days and you hidden field is in seconds, add 246060 to the hidden field.

In that way you would ensure a simple number to reach your server as, say, a number of seconds.

OR

you could split you values in form.py, with optional fields taking care of each “unit”: days, hours, minutes, seconds, etc. Then transform in some clean methods server-side.

Up to you, there are probably a hundred different ways of doing that.

1 Like

Have you tried TimeInput as a widget type? I have not tried it myself as I had abandoned using DurationField (and used IntegerField instead)

https://docs.djangoproject.com/en/4.1/ref/forms/widgets/#timeinput

It could be worth experimenting with TIME_INPUT_FORMATS in settings.py also:
https://docs.djangoproject.com/en/4.1/ref/settings/#std-setting-TIME_INPUT_FORMATS

Well, I tried TimeInput with attrs = {"type": "time"}.
The result of that was the time with AM/PM

On Windows, the 12- or 24-hour format is determined by System Settings / Region / Date and time formats.

For example, with the 12-hour setting, this page, <input type="time"> - HTML: HyperText Markup Language | MDN, looks like this:

But if I change my system settings, it displays like this:

In other words, this appears to be something that is not under control of the HTML on the page.

(Note: Also read the text on the referenced page for more detailed information.)

I’d have to second @plopidou here - I think the only reasonable way to produce a platform and settings-independent widget for a duration would be to craft it yourself using a text input widget and some JavaScript.

may I ask - what duration format exactly do you want to work with? days/minutes/hours/seconds or just minutes? if this was just minutes (for input and output), you could really lower the complexity of everything

I want to show only minutes like 110 minutes for 1 hour 50 minutes.

I’ve already read the documentation from MDN.
So, is it mean there is no simple way to achieve what I want?

Correct. There is no HTML element or attribute to handle a duration field as a duration.

Can I assume that the only way to reflect default value in models.py is <input type="time">?

I don’t think I understand what you’re asking here.

What you see on a form on a page does not need to have any relationship to the data as it is stored in the model.

A DurationField in a model is stored in the database as an interval on PostgreSQL and Oracle and as a bigint everywhere else.

In the model objects themselves, it’s stored as a timedelta object, which is not a datetime field. And, unfortunately, there is no HTML representation of a “duration”.

The Django DurationField defaults to a text input that has to be parsed by the parse_duration function.

No, I think you’ve understood what I’m asking.
Because of my lack of knowledge about Django, I’m a bit confused and I just wanted to make it clear.
What I’m trying to make is if user just enter ‘110’, system recognize it as 110 minutes and store as timedelta(minutes=110).

First, all of your choices are going to involve some amount of writing code to do this. That format is not going to be directly handled by Django.

You have a couple different options

  • Create a custom field with a custom to_python method.

  • Translate the text field in JavaScript to a parse_duration compatible format before submitting the form.

  • Create a separate field for the Form entry and perform the conversion in either Form.clean or ModelForm.save.

… and I’m sure there are other options as well. You can add code pretty much anywhere along the line here to perform your conversion before the data gets saved to the database.

Thank you for your answer.
My current solution(using NumberInput for widget and handle with clean_<field> method) works well, so I can keep it.

If it helps, we did this before with an IntegerField and javascript to output minutes from days/hours/minutes (it outputs the minutes directly into the form field)

the JS code goes in static/js/script.js

function add_number() {

    const day_number = parseInt((document.getElementById("DayConversion").value)* 1440 || 0) ;  // 0 or get day value multiplied by minutes per day 
    const hour_number = parseInt(document.getElementById("HourConversion").value * 60 || 0);    // 0 or get hour value multiplied by minutes per hour
    const minute_number = parseInt(document.getElementById("MinuteConversion").value) || 0;     // 0 or get minutes value

    document.getElementById("id_downtime").value = day_number + hour_number + minute_number; // total value of minutes -> html element 
}

the html template contains this:

<div class="card-body">
                                                                <div class="row ">
                                                                        <div class="col-md-8 ">
                                                                                <div class="input-group">

                                                                                        <input type="number" class="form-control"  placeholder="DD" aria-label="Days" id="DayConversion"  oninput="add_number()">
                                                                                        <span class="input-group-text"> : </span>
                                                                                        <input type="number" class="form-control" placeholder="HH" aria-label="Hours" id="HourConversion"  oninput="add_number()">
                                                                                        <span class="input-group-text"> : </span>
                                                                                        <input type="number" class="form-control" placeholder="MM" aria-label="Minutes"  id="MinuteConversion"  oninput="add_number()">

                                                                                </div>
                                                                        </div>


                                                                        <div class="col-md-4 ">
                                                                                        {{ form.downtime}}
                                                                        </div>
                                                                </div>
                                                        </div>

and as for the widget for the form field (downtime), it was just NumberInput
"downtime" : forms.NumberInput(attrs={'class':'form-control', 'placeholder':'Total Downtime'}),

Okay, thanks.
I will look into it.