How to return timedelta objects from Forms?

Hi all! From looking at the Django docs I see that ChoiceField normalizes to a String and I wish to return a timedelta object instead. Therefore I am trying to use TypedChoiceField but failing to understand why I am getting the result I am getting. My forms.py code is:

# forms.py
from django import forms
from datetime import timedelta

class ExampleForm(forms.Form):

    OPTIONS = [
        (timedelta(minutes=5), '5 mins'),
        (timedelta(minutes=10), '10 mins'),
        (timedelta(minutes=30), '30 mins'),
        (timedelta(hours=1), '1 hour'),
        (timedelta(hours=3), '3 hours'),
        (timedelta(hours=6), '6 hours'),
        (timedelta(days=1), '1 day'),
        (timedelta(days=2), '2 day'),
        (timedelta(weeks=1), '1 week'),
    ]

    time = forms.TypedChoiceField(choices=OPTIONS, coerce=lambda str: timedelta)

and views.py code:

from django.shortcuts import render, redirect
from .forms import ExampleForm
from datetime import timedelta

def home(request):
    if request.method == 'GET':
        return render(request, 'test_app/home.html', {'form': ExampleForm()})
    else:
        form = ExampleForm(request.POST)
        if form.is_valid():

            t = form.cleaned_data['time']

            t2 = timedelta(hours=1)

            print(t, type(t))
            print(t2, type(t2))

            return redirect('home')

If I enter, say ‘1 hour’ into the form and submit, I would expect the result of both print statements to be exactly the same, namely: 1:00:00 <class 'datetime.timedelta'>. The second print statement indeed does print this but the first prints: <class 'datetime.timedelta'> <class 'type'>. This really confuses me… It seems like the TypedChoiceField did work in coercing the str into a timedelta object but for somereason that returned timedelta object is of type ‘type’ and the actual value cannot be read

You’ve got a couple different issues here.

  1. Remember that the OPTIONS list is what’s used to prepare the HTML values and display strings in the form rendered in the browser. This means that your timedelta values are rendered (in part) in the html like this:
<option value="6:00:00">6 hours</option>
<option value="1 day, 0:00:00">1 day</option>

What gets returned in your request.POST are then these strings: 6:00:00 or 1 day, 0:00:00. Those strings can’t be trivially converted back to a timedelta value.

Also, the format of your coerce function is wrong - at a minimum, you’re not passing the bound lambda variable to the function.

I’ve come up with the following that appears to work:

class ExampleForm(forms.Form):
    OPTIONS = [
        (timedelta(minutes=5).total_seconds(), '5 mins'),
        (timedelta(minutes=10).total_seconds(), '10 mins'),
        (timedelta(minutes=30).total_seconds(), '30 mins'),
        (timedelta(hours=1).total_seconds(), '1 hour'),
        (timedelta(hours=3).total_seconds(), '3 hours'),
        (timedelta(hours=6).total_seconds(), '6 hours'),
        (timedelta(days=1).total_seconds(), '1 day'),
        (timedelta(days=2).total_seconds(), '2 day'),
        (timedelta(weeks=1).total_seconds(), '1 week'),
    ]

    time = forms.TypedChoiceField(choices=OPTIONS, coerce=lambda dt: timedelta(seconds=float(dt)))

Ken

1 Like

Thanks Ken, this makes a lot of sense. I made a custom function as a workaround for converting the string back into a deltatime object like this:

def get_timedelta_from_str(s):
    ls = s.split(',')
    ls.reverse()
    d = datetime.strptime(ls[0].lstrip(), '%H:%M:%S')
    new_timedelta = timedelta(hours=d.hour, minutes=d.minute, seconds=d.second)
    if len(ls) == 2:
        days = ls[1].split()[0]
        new_timedelta += timedelta(days=int(days))
    return new_timedelta

But your solution is definitely more elegant and involves less code so I will refactor. I had overlooked the total_seconds() method in the docs and yes, I guess I misunderstood how to use timedelta in the context of coerce, I’m still very new to Django, it’s my first framework I’m learning.

Many thanks, Mike

Actually, I’ve learned that when I’m faced with a problem like this, my first thought is to check pypi.org or djangopackages.org to see if there’s already a library to handle it. Sure enough, https://pypi.org/project/pytimeparse/, https://pypi.org/project/timeparse-plus/, and https://pypi.org/project/parsedatetime/ were all found rather quickly.

Thanks for that tip! I’ll definitely start doing that more as well. I guess you have to really know quite accurately what you’re looking for in that case since my package search queries seem to have to match existing package names quite specifically