#34532: Form.default_renderer is ignored in formsets.
----------------------------------+------------------------------------
     Reporter:  Ryan Burt         |                    Owner:  nobody
         Type:  Bug               |                   Status:  new
    Component:  Forms             |                  Version:  4.2
     Severity:  Normal            |               Resolution:
     Keywords:  default_renderer  |             Triage Stage:  Accepted
    Has patch:  0                 |      Needs documentation:  0
  Needs tests:  0                 |  Patch needs improvement:  0
Easy pickings:  0                 |                    UI/UX:  0
----------------------------------+------------------------------------

Comment (by Ryan Burt):

 Hi,

 Passing the renderer to {{{ modelformset_factory }}} does indeed work,
 though it needs to be instantiated object rather than passing the class.
 I think the concern is that in your scenario, setting {{{ default_renderer
 = CustomFormRenderer }}} is still unsupported as the code block that uses
 the default_renderer is inaccessible.
 This leaves a confusing mix of behaviour for the ModelForm class as the
 default_renderer will work when instantiating directly, but not when using
 {{{ formset_factory }}}. I agree with the hierarchical intent that lower
 level methods shouldn't override supplied variables from their parents.

 I think that altering the attrs in {{{ formset_factory }}} from:

 {{{
 attrs = {
         "form": form,
         "extra": extra,
         "can_order": can_order,
         "can_delete": can_delete,
         "can_delete_extra": can_delete_extra,
         "min_num": min_num,
         "max_num": max_num,
         "absolute_max": absolute_max,
         "validate_min": validate_min,
         "validate_max": validate_max,
         "renderer": renderer or get_default_renderer(),
     }

 }}}
 To:

 {{{
 attrs = {
         "form": form,
         "extra": extra,
         "can_order": can_order,
         "can_delete": can_delete,
         "can_delete_extra": can_delete_extra,
         "min_num": min_num,
         "max_num": max_num,
         "absolute_max": absolute_max,
         "validate_min": validate_min,
         "validate_max": validate_max,
         "renderer": renderer,
     }
 }}}

 Is a non breaking change, as the return is the very next line, which
 instantiates the Form class:
 {{{
 return type(form.__name__ + "FormSet", (formset,), attrs)
 }}}

 And the renderer variable is not used anywhere prior inside {{{
 BaseForm.__init__ }}} :

 {{{
     def __init__(
         self,
         data=None,
         files=None,
         auto_id="id_%s",
         prefix=None,
         initial=None,
         error_class=ErrorList,
         label_suffix=None,
         empty_permitted=False,
         field_order=None,
         use_required_attribute=None,
         renderer=None,
     ):
         self.is_bound = data is not None or files is not None
         self.data = MultiValueDict() if data is None else data
         self.files = MultiValueDict() if files is None else files
         self.auto_id = auto_id
         if prefix is not None:
             self.prefix = prefix
         self.initial = initial or {}
         self.error_class = error_class
         # Translators: This is the default suffix added to form field
 labels
         self.label_suffix = label_suffix if label_suffix is not None else
 _(":")
         self.empty_permitted = empty_permitted
         self._errors = None  # Stores the errors after clean() has been
 called.

         # The base_fields class attribute is the *class-wide* definition
 of
         # fields. Because a particular *instance* of the class might want
 to
         # alter self.fields, we create self.fields here by copying
 base_fields.
         # Instances should always modify self.fields; they should not
 modify
         # self.base_fields.
         self.fields = copy.deepcopy(self.base_fields)
         self._bound_fields_cache = {}
         self.order_fields(self.field_order if field_order is None else
 field_order)

         if use_required_attribute is not None:
             self.use_required_attribute = use_required_attribute

         if self.empty_permitted and self.use_required_attribute:
             raise ValueError(
                 "The empty_permitted and use_required_attribute arguments
 may "
                 "not both be True."
             )

         # Initialize form renderer. Use a global default if not specified
         # either as an argument or as self.default_renderer.
         if renderer is None:
             if self.default_renderer is None:
                 renderer = get_default_renderer()
             else:
                 renderer = self.default_renderer
                 if isinstance(self.default_renderer, type):
                     renderer = renderer()
         self.renderer = renderer
 }}}


 Essentially, if renderer is none when calling {{{ formset_factory }}}, {{{
 get_default_renderer() }}} is still called lower in the api if required,
 but still before {{{ self.renderer }}} is ever utilised in the form
 widgets. This improves your scenario as you could do something like:

 {{{
 class CustomFormRendererPrimary(TemplatesSetting):
     ...

 class CustomFormRendererSecondary(DjangoTemplates):
     ...

 class MyForm(Form):
     name = CharField()
     default_renderer = CustomFormRendererPrimary

 class CustomFormsetRenderer(Jinja2):
     ...
 if use_secondary:
     MyFormSet = formset_factory(MyForm,
 renderer=CustomFormRendererSecondary())
 else:
     MyFormSet = formset_factory(MyForm)
 }}}

 Adding the caveat to the docs that {{{ default_renderer }}} through {{{
 formset_factory }}} is unsupported and that passing the renderer directly
 is the supported method could suffice, as I'm not sure how many projects
 are affected by this and haven't figured out to pass renderer directly (or
 override {{{ __init__ }}} to set the renderer like I did originally).

-- 
Ticket URL: <https://code.djangoproject.com/ticket/34532#comment:7>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.

-- 
You received this message because you are subscribed to the Google Groups 
"Django updates" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To view this discussion on the web visit 
https://groups.google.com/d/msgid/django-updates/0107018831b7bb46-c7588f71-fa33-4174-987f-170bca5b7803-000000%40eu-central-1.amazonses.com.

Reply via email to