Using DateTimeField only for Date, Validation expects a tuple, how do I provide it?

I have a model representing a concert. A concert has a date, there usually is a time for when the doors open and a time for when the bands start and optional a time when it’s all over.

date = m.DateTimeField()
time_open_doors = m.DateTimeField(blank=True, null=True)
time_start = m.DateTimeField(null=True)
time_end = m.DateTimeField(blank=True, null=True)

In my form, I want the field for date to only take a date, no time. During validation I want to add time (0:00). With all the other fields, I want the user to only enter the time. Then during validation, time_open_doors will use the date from date, time_start and time_end will do a little calculation to see if the time is still on the same day or the next.
Replacing the widgets accordingly was no problem. But during validation I get an error: “Enter a list of values.” Now I understand that the standard widget is a multiwidget with a for date and one for time. So the raw_data should be holding a tuple of strings. It would be easy to add a second string, but I don’t know where to do it.
Am I even looking in the right place or should I have my own field which inherits from DateTimeField and do this on the field level?

Hi,

This might not be what you were looking for, but I’ll share how I tried approaching this problem in case it helps you.

For the model, I actually thought it would make sense to skip the date field.

# concerts/models.py
from django.db import models


class Concert(models.Model):
    name = models.CharField(max_length=100)
    time_open_doors = models.DateTimeField(blank=True, null=True)
    time_start = models.DateTimeField(null=True)
    time_end = models.DateTimeField(blank=True, null=True)

This is how I specified the form

# concerts/forms.py
from concerts.models import Concert
from django import forms
from pytz import common_timezones

TZ_CHOICES = [(x, x) for x in common_timezones]

class ConcertForm(forms.Form):
    name = forms.CharField(max_length=100, min_length=3, required=True, strip=True)
    date = forms.DateField(label='Date on which doors open (or concert starts, if no doors open time is set)', required=True)
    timezone = forms.ChoiceField(label='Timezone of concert locale', choices=TZ_CHOICES)
    time_open_doors = forms.TimeField(label='Time when doors open', required=False)
    time_start = forms.TimeField(label='Time when concert starts', required=True)
    time_end = forms.TimeField(label='Time when concert ends', required=False)

If I understood you correctly, you actually wanted the user to be able to input a concert start time or a ‘doors open’ time. To make things work properly here you’d have to set both fields’s required attribute to False and override the is_valid method, I think, and add an additional check that either one is filled out)

As you can see, although the date never gets a field on its own in the database, the user is asked to input the date separately from the involved times. I also specified that it’s the date when the earliest time occurs, since it’s possible that the concert goes on through the night. I’m assuming though that no concerts will go on for >24 hours, which might be unrealistic for some really hardcore bands.

I’m including a time zone choice, as this is important unless the site only handles very local concerts and it’s safe to assume that all concert-holders and -goers will be in the same time zone.

For views I did this:

# concerts/views.py
from django.urls import reverse_lazy
from django.views.generic import FormView, ListView
from datetime import datetime, timedelta
import pytz

from .forms import ConcertForm
from .models import Concert

day_delta = timedelta(days=1)

class ConcertFormView(FormView):
    form_class = ConcertForm
    template_name = 'concerts/concert-create.html'
    success_url = reverse_lazy('home')

    def form_valid(self, form):
        name = form.cleaned_data.get('name')
        timezone = form.cleaned_data.get('timezone')
        date = form.cleaned_data.get('date')

        time_open_doors =form.cleaned_data.get('time_open_doors')
        if time_open_doors:
            datetime_open_doors = datetime.combine(date, time_open_doors)
            datetime_open_doors = pytz.timezone(timezone).localize(datetime_open_doors)

        time_start =form.cleaned_data.get('time_start')
        datetime_start = datetime.combine(date, time_start)
        if time_open_doors and time_start < time_open_doors:
            datetime_start = datetime_start + day_delta
        datetime_start = pytz.timezone(timezone).localize(datetime_start)

        time_end =form.cleaned_data.get('time_end')
        datetime_end = datetime.combine(date, time_end)
        if datetime_end and (time_open_doors and time_end < time_open_doors) or\
            (time_open_doors and time_open_doors < time_start):
            datetime_end = datetime_end + day_delta
        datetime_end = pytz.timezone(timezone).localize(datetime_end)

        print(datetime_start)
        save_data = {'name': name, 'time_start': datetime_start,
                     'time_end': datetime_end}
        if time_open_doors:
            save_data['time_open_doors'] = datetime_open_doors
        c = Concert(**save_data)
        c.save()
        return super().form_valid(form)

class ConcertListView(ListView):
    model = Concert
    template_name = 'concerts/home.html'
    context_object_name = 'concerts'

So I override the form_valid method of the FormView, and make sure that if the user has input e. g . times “22:00”, “23:00”, “03:00”, then the date for the final datetime is shifted forward by one day. The specified timezone is also added. There ought to be cleaner solutions, but maybe it helps you get going. Note that you will want to think about how timezones should be handled on your site. Do you want the concert times to always be displayed in the time zone of the area where the concert is held? Or do you want them to be displayed (e. g. if there are live streams/online concerts) in the concert-goers time zone?

Hope this helps :slight_smile:

1 Like

Thanks for sharing your code, this is very helpful. You’re right, it’s not quite what I wanted but it points me to a simpler solution for what I want to do.

What I omitted in my original post was that Concert is inheriting date from the Base Class Event. I have a template for displaying events on a month by weeks basis that I want to use. Also there will be other events, for instance a practice session, which is a non-public event but would be displayed on the internal calendar. For easy ordering amongst all events I put the date in the base class. Also I want all the timestamps to be datetime objects so that I can do easy calculus within a single event or between events without having to cast the times into datetimes.

The easier solution that was inspired by your post is this: I will keep the model fields in the background as datetime objects. However I will exclude them from the form and add form fields for the concert date with a DateField and the indvidual times with TimeField. That way I don’t have to worry about individual field validation at all. I just have to add a clean method for the form that casts the times into datetimes and vice versa back into the form.

I will definitely take a closer look at the code regarding timezones. I’m still a bit shaky in that area. As to your question: I think I will display the times in both the local timezone for the user and the local timezone for the website.

1 Like