ModelForm checkbox store as DateTime

I have a model, Task, with a “completed” field. It is a datetime field, so if it’s null/None then the task isn’t complete yet. (Previously, it was a Boolean field, but now I need the datetime information. I’ve migrated the development database successfully.)

class Task(models.Model):
    ...
    completed = models.DateTimeField(
        null=True, blank=True, default=False
    )
    ...

My aim is that a user can check the box and submit the form with a post request and the current datetime is stored in the database.

The view is a generic class-based view (Update or Create):

class TaskUpdateView(UpdateView):
    model = Task
    queryset = Task.objects.prefetch_related(
        Prefetch("notes", queryset=Note.objects.select_related("author").order_by("-created"))
        )
    form_class = TaskForm
    template_name = "tasks/task_detail.html"
    success_url = reverse_lazy("tasks:list")

I’m struggling with the form for this field. It’s a ModelForm, but with a checkbox widget for this field defined in the form’s Meta class.

class TaskForm(ModelForm):
    ...
    class Meta:
        model = Task
        fields = ["completed", "text", "private", "deadline", "scheduled", "deleted"]
        widgets = {
            "completed": forms.CheckboxInput(attrs={
                "class": "checkbox",
                }
            ),
        ...
        }

Unsurprisingly, trying to use this form, results in an attribute error - 'bool' object has no attribute 'strip', as Django attempts to clean the POST data submitted through the form and discovers it’s not quite as expected.

Traceback (most recent call last):
  File "/Users/xxxx/lib/python3.12/site-packages/django/core/handlers/exception.py", line 55, in inner
    response = get_response(request)
               ^^^^^^^^^^^^^^^^^^^^^
  File "/Users/xxxx/lib/python3.12/site-packages/django/core/handlers/base.py", line 197, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/xxxx/lib/python3.12/site-packages/django/views/generic/base.py", line 104, in view
    return self.dispatch(request, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/xxxx/lib/python3.12/site-packages/django/views/generic/base.py", line 143, in dispatch
    return handler(request, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/xxxx/lib/python3.12/site-packages/django/views/generic/edit.py", line 182, in post
    return super().post(request, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/xxxx/lib/python3.12/site-packages/django/views/generic/edit.py", line 150, in post
    if form.is_valid():
       ^^^^^^^^^^^^^^^
  File "/Users/xxxx/lib/python3.12/site-packages/django/forms/forms.py", line 197, in is_valid
    return self.is_bound and not self.errors
                                 ^^^^^^^^^^^
  File "/Users/xxxx/lib/python3.12/site-packages/django/forms/forms.py", line 192, in errors
    self.full_clean()
    ^^^^^^^^^^^^^^^^^
  File "/Users/xxxx/lib/python3.12/site-packages/django/forms/forms.py", line 325, in full_clean
    self._clean_fields()
    ^^^^^^^^^^^^^^^^^^^^
  File "/Users/xxxx/lib/python3.12/site-packages/django/forms/forms.py", line 333, in _clean_fields
    self.cleaned_data[name] = field._clean_bound_field(bf)
                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/xxxx/lib/python3.12/site-packages/django/forms/fields.py", line 266, in _clean_bound_field
    return self.clean(value)
           ^^^^^^^^^^^^^^^^^
  File "/Users/xxxx/lib/python3.12/site-packages/django/forms/fields.py", line 204, in clean
    value = self.to_python(value)
            ^^^^^^^^^^^^^^^^^^^^^
  File "/Users/xxxx/lib/python3.12/site-packages/django/forms/fields.py", line 550, in to_python
    result = parse_datetime(value.strip())
                            ^^^^^^^^^^^

Exception Type: AttributeError at /tasks/new/
Exception Value: 'bool' object has no attribute 'strip'

I have read through the form submission and validation process and how it is adapted for ModelForms.

With the helpful guidance I’ve received previously, I’ve delved into Classy CBV and I have tried overriding the clean_completed method in the form (which I’ve done for another field, successfully), I’ve tried overriding the post method and the form_valid method in the view; and I’ve tried overriding the clean method in the model. None has stopped the attribute error being raised in the same way as the traceback above.

What am I missing or any suggestions?

I’m wondering if I need to bite the bullet and separate the form from the model (ie not use a ModelForm), so that I can override the fields and the data cleaning?

I’m also toying with adding a boolean field back into the model and adding a whole separate datetime field, which would be a hidden field, and using the clean_completed method in the form to update the datetime field when the Boolean field changes to trueemphasized text.

Many thanks in advance
David

You can also exclude the completed field from the ModelForm and create a separate form for it, then programmatically handle the completed field using the submitted value.

I think replacing a DateField with a boolean field should not work since the latter cannot process data required by the former; although I can’t seem to find published documentation to back me up on this, but there a pull request creating a reference for ModelForm.Meta options, (see docs on the field_classes attr in that PR, which seems related)

Thanks @cliff688

I looked at excluding the completed field, but I had assumed if I had two forms for each instance of a task then they would be submitted independently. So, the user could only make changes to “complete” a task or to change other values in each form. I’m aiming to have one, seamless form.

I have just updated the last paragraph of my question, because I don’t think I described the Boolean field and datetime field idea very clearly. I will read through the discussion on that PR in more detail though!

How about this:

In [7]: class TaskForm(ModelForm):
    ...:     completed = forms.BooleanField()
    ...:     class Meta:
    ...:         model = Task
    ...:         exclude = ["completed"]

You do not need to create an additional form.

A Django ModelForm is a Form, with additional functionality. The additional functionality includes the ability to create form fields from model fields.
This means you still have the ability to create fields in your ModelForm in the exact same way you create fields for a regular form.

What does that mean here?

  • Remove the completed field from the fields list.
  • Create a form field in your model form, perhaps named is_complete.
  • Override the form_valid method
    • save with commit=False
    • update the object’s completed field based upon the value of form.cleaned_data['is_complete']
    • save the updated object
    • redirect to the “success” url.

Thanks @KenWhitesell and @cliff688 for your help.

So the form, view and model work, so I am 80% of the way there, I think, with the following code. This also handles the conversion back from the value stored in the model into the initial value in the form. Provided in case helpful to others in future.

In forms.py:

class TaskForm(forms.ModelForm):

    is_complete = forms.BooleanField(required=False)

    def __init__(self, *args, **kwargs):
        self.user = kwargs.pop("user", None)
        super().__init__(*args, **kwargs)
        if self.instance.completed is None:
            self.fields["is_complete"].initial = False
        else:
            self.fields["is_complete"].initial = True
...
   class Meta:
        model = Task
        fields = ["is_complete", "text", "private", "deadline", "scheduled", "deleted"]
        widgets = {
            "is_complete": forms.CheckboxInput(attrs={
                "class": "checkbox",
                }
            ),
...

In views.py:


class TaskUpdateView(UpdateView):

    model = Task
    queryset = Task.objects.prefetch_related(
        Prefetch("notes", queryset=Note.objects.select_related("author").order_by("-created"))
        )
    form_class = TaskForm
    template_name = "tasks/task_detail.html"
    success_url = reverse_lazy("tasks:list")

    def form_valid(self, form):
        form.save(commit=False)
        if form.cleaned_data["is_complete"] == True:
            form.instance.completed = timezone.now()
        else: 
            form.instance.completed = None
        form.save()
        return HttpResponseRedirect(self.get_success_url())

The last 20% is:

  • 5% me working out why the completed/is_complete checkbox that is rendered in the HTML doesn’t have the same styling as the other checkbox in the same form; and
  • 15% me writing the tests …

Thanks again

This doesn’t apply to manually created fields. The widgets setting in Meta only applies to the model fields being automatically created by the ModelForm class.

You need to apply those attributes directly to the is_complete field definition.

1 Like