Opened 10 days ago

Closed 9 days ago

#35988 closed Bug (fixed)

ErrorDict always uses default renderer

Reported by: Adam Johnson Owned by: Adam Johnson
Component: Forms Version: dev
Severity: Normal Keywords:
Cc: Triage Stage: Ready for checkin
Has patch: yes Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

When BaseForm.full_clean() instantiates ErrorDict, it doesn't pass it the renderer:

https://github.com/django/django/blob/1860a1afc9ac20750f932e8e0a94b32d096f2848/django/forms/forms.py#L319

Despite ErrorDict being ready to receive it and use it for rendering:

https://github.com/django/django/blob/1860a1afc9ac20750f932e8e0a94b32d096f2848/django/forms/utils.py#L124

Practically, this means customizations to the renderer are ignored when rendering the form errors using {{ errors }} in the template.

Building on top of the example and fix from #35987, I have a custom renderer that swaps out some templates:

from django import forms
from django.forms.renderers import TemplatesSetting
from django.forms.utils import ErrorList

from django.template.exceptions import TemplateDoesNotExist


class CustomRenderer(TemplatesSetting):
    def get_template(self, template_name):
        if template_name.startswith("django/forms/"):
            # Load our custom version from "custom/forms/" if it exists
            our_template = f"custom/{template_name.removeprefix('django/')}"
            try:
                return super().get_template(our_template)
            except TemplateDoesNotExist:
                pass
        return super().get_template(template_name)


class CustomErrorList(ErrorList):
    def copy(self):
        # Copying the fix from Django Ticket #35987
        copy = super().copy()
        copy.renderer = self.renderer
        return copy


class MyForm(forms.Form):
    default_renderer = CustomRenderer()

    def __init__(self, *args, error_class=CustomErrorList, **kwargs):
        super().__init__(*args, error_class=error_class, **kwargs)

The custom error list template uses some CSS utility classes from Tailwind, like text-red-600:

{% if errors %}<ul class="text-red-600">{% for field, error in errors %}<li>{{ field }}{{ error }}</li>{% endfor %}</ul>{% endif %}

But creating a form with a non-field error and rendering the error dict uses the default renderer and its template:

In [1]: from example.forms import MyForm
   ...:
   ...: form = MyForm({})
   ...: form.full_clean()
   ...: form.add_error(None, "Test error")

In [2]: form.errors.render()
Out[2]: '<ul class="errorlist"><li>__all__<ul class="text-red-600"><li>Test error</li></ul></li></ul>'

I need to override full_clean() to set the renderer:

class MyForm(forms.Form):
    ...
    
    def full_clean(self):
        super().full_clean()

        # Fix a bug in Django where self._errors = ErrorDict is not passed the
        # renderer argument when initialized.
        self._errors.renderer = self.renderer

Then form errors use my custom template:

In [1]: from example.forms import MyForm
   ...:
   ...: form = MyForm({})
   ...: form.full_clean()
   ...: form.add_error(None, "Test error")

In [2]: form.errors.render()
Out[2]: '<ul class="text-red-600"><li>__all__<ul class="text-red-600"><li>Test error</li></ul></li></ul>'

I think this has probably been an issue ever since a custom renderer became possible in #31026. The argument was added to ErrorDict but missed in BaseForm.full_clean(), the only place where the class is instantiated.

Change History (4)

comment:1 by Adam Johnson, 10 days ago

Has patch: set

comment:2 by Sarah Boyce, 10 days ago

Triage Stage: UnreviewedAccepted

comment:3 by Sarah Boyce, 10 days ago

Triage Stage: AcceptedReady for checkin

comment:4 by Sarah Boyce <42296566+sarahboyce@…>, 9 days ago

Resolution: fixed
Status: assignedclosed

In 02628c05:

Fixed #35988 -- Made BaseForm.full_clean() pass renderer to ErrorDict.

Note: See TracTickets for help on using tickets.
Back to Top