Max Vallee
Max Vallee

Reputation: 468

Displaying datetime field as hours and minutes only (no date displayed) in ModelForm

I'm building a calendar on Django. When a user clicks a day on the calendar, he's redirected to a CreateEventForm where he can create an event happening on the day he clicked (selected_date). To save the user the hassle of having to enter the date he selected, I used get_form_kwargs() to send the selected_date to the form and prefill the start_time and end-time fields. In this way, he only needs to enter the hours and minutes for the start_time and end_time of the event.

My problem

Currently, the entire "%Y-%m-%dT%H:%M" format is displayed like so:

enter image description here

I would like to display only the %H:%M. But I want to make sure that the datetime object still contains the date; except that it wouldn't be displayed to the end user, like so:

enter image description here

I've tried to format the datetime object using form widgets, but it doesn't seem to work. When I format the field to '%H:%M', the field displays like so:

enter image description here

Forms

class EventForm(ModelForm):
  class Meta:
    model = Event
    widgets = {
      'start_time': DateInput(attrs={'type': 'datetime-local'}, format='%H:%M'),
      'end_time': DateInput(attrs={'type': 'datetime-local'}, format='%H:%M'),
    }
    fields = '__all__'

  def __init__(self, selected_date, *args, **kwargs):
    super(EventForm, self).__init__(*args, **kwargs)
    self.fields['start_time'].initial = selected_date
    self.fields['end_time'].initial = selected_date
    self.fields['start_time'].input_formats = ('%H:%M',)
    self.fields['end_time'].input_formats = ('%H:%M',)

Models

class Event(models.Model):
    title = models.CharField(max_length=200)
    description = models.TextField()
    start_time = models.DateTimeField()
    end_time = models.DateTimeField()

    def get_absolute_url(self):
        return reverse('cal:calendar')

Views

class EventCreateView(generic.CreateView):
    model = Event
    template_name = 'cal/event.html'
    form_class = EventForm

    def get_form_kwargs(self):
        kwargs = super(EventCreateView, self).get_form_kwargs()
        selected_date_str = self.request.GET.get('date', None)
        selected_date = datetime.strptime(selected_date_str, '%Y-%m-%d')
        kwargs.update({'selected_date': selected_date})
        return kwargs

class EventUpdateView(generic.UpdateView):
    model = Event
    template_name = 'cal/event.html'
    form_class = EventForm

Template

{% extends 'cal/base.html' %}
{% load crispy_forms_tags %}

{% block title %}
Event
{% endblock %}

{% block content %}
<div class="clearfix">
  <a class="btn btn-info left" href="{% url 'cal:calendar' %}"> Calendar </a>
</div>
<div class="container">

<form method="post">
  <div class="form-group">
      {% csrf_token %}
      {% if form.instance.pk %}
          <h3 class="center">{{ form.instance.start_time }}</h3>
      {% else %}
          <h3 class="center">{{selected_date}}</h3>
      {% endif %}

      <div class="col-4">{{ form.title | as_crispy_field }}</div>
      <div class="col-4">{{ form.description | as_crispy_field }}</div>
      <div class="col-4">{{ form.start_time | as_crispy_field }}</div>
      <div class="col-4">{{ form.end_time | as_crispy_field }}</div>

      {% if form.instance.pk %}
          <a href="{% url 'cal:event_delete' pk=event.pk %}" class="btn btn-danger"> Delete </a>
      {% endif %}
      <button type="submit" class="btn btn-info"> Submit </button>

</div>

</form>
{% endblock %}

Upvotes: 2

Views: 1809

Answers (1)

Brian Dant
Brian Dant

Reputation: 4149

  1. Override the clean method to handle creating datetime objects
  2. I'm pretty sure you need a DateTimeInput widget
  3. Add a hidden field on your form with the selected_date
class EventForm(ModelForm):
    start_time = forms.DateTimeField(required=True, input_formats=['%H:%M'], localize=True, widget=forms.DateTimeInput)
    end_time = forms.DateTimeField(required=True, input_formats=['%H:%M'], localize=True, widget=forms.DateTimeInput)
    selected_date = forms.DateField(required=True)

    class Meta:
        model = Event
        fields = ['title', 'description', 'start_time', 'end_time', 'selected_date']

    def __init__(self, selected_date, *args, **kwargs):
        super(EventForm, self).__init__(*args, **kwargs)
        self.fields['selected_date'].initial = selected_date

    def clean(self):

        end_time = self.cleaned_data['end_time']
        end_time = end_time.replace(year = self.cleaned_data['selected_date'].year)
        end_time = end_time.replace(month = self.cleaned_data['selected_date'].month)
        end_time = end_time.replace(day = self.cleaned_data['selected_date'].day)

        start_time = self.cleaned_data['start_time']
        start_time = start_time.replace(year = self.cleaned_data['selected_date'].year)
        start_time = start_time.replace(month = self.cleaned_data['selected_date'].month)
        start_time = start_time.replace(day = self.cleaned_data['selected_date'].day)

        return self.cleaned_data.update({'end_time': end_time, 'start_time': start_time})

To post that selected_date to your EventCreateView (which happens when the user submits the form from your template), add the following to your template:

<form>
...
    <input id="selected_date" type="hidden" name="selected_date" value="{{ selected_date }}">
...
</form>

In the same way that the title and description are automatically extracted from the request and saved to the form, the selected_date will be extracted by Django. This happens because the view you're using automatically gets the POST data and passes it to your form class. Thus, the value of selected_date from your hidden field will be available to you in your clean() method, where we then use it to create the full datetime objects and pass them back to the form framework, which will save them for you.

The More on hidden elements can be found here.

N.B.: This requires that you pass the selected_date back during your POST request, after the user has filled out the end_time and start_time. This is because Django's form framework will automatically fetch that value if the string matches. (If you need help with that, post your template code.)

My changes rely on the implementation details of Django's forms. The docs for the clean method state:

The form subclass’s clean() method can perform validation that requires access to multiple form fields. This is where you might put in checks such as “if field A is supplied, field B must contain a valid email address”. This method can return a completely different dictionary if it wishes, which will be used as the cleaned_data.

Upvotes: 3

Related Questions