Django custom widget displaying escaped html
Question: I wrote a widget for Django forms in order to have a bootstrap3 multiple
checkbox directly in the template in order to reuse it in the future.

So I wrote an app `prettyforms` which contains all basic files (`__init__.py`,
`views.py` ...) and created a forms.py where I wrote the custom widget.

**the main form for the view item/forms.py is such as**

    
    
    from django import forms
    from django.utils.translation import ugettext as _
    from mysite.item.models import Item, ItemCategory
    
    from mysite.prettyforms import forms as prettyforms
    
    class CreateItemForm(forms.ModelForm):
    
        class Meta:
            model = Item
            fields = (
                'categories',
            )
            widgets = {
                'categories': prettyforms.PrettyCheckboxSelectMultiple(),
            }
    
        def __init__(self, *args, **kwargs):
            self.request = kwargs.pop('request', None)
            super(CreateItemForm, self).__init__(*args, **kwargs)
    
            categories = ItemCategory.objects.all()
            categories_choices = [ [category.pk, category.name] for category in categories ]
    
            self.fields['categories'].choices = categories_choices
            self.fields['categories'].error_messages = {
                'required': _('At least one category must be selected')
            }
    

**and the file containing the widget prettyforms/forms.py is such as:**

    
    
    # -*- coding: utf-8 -*-
    
    from django.forms.widgets import (
        ChoiceInput, SelectMultiple, RendererMixin,
        CheckboxChoiceInput, ChoiceFieldRenderer
    )
    from django.utils.encoding import (
        force_str, force_text, python_2_unicode_compatible,
    )
    from django.utils.safestring import mark_safe
    from django.forms.utils import flatatt
    from django.utils.html import format_html, html_safe
    
    @html_safe
    @python_2_unicode_compatible
    class PrettyChoiceInput(ChoiceInput):
    
        def __str__(self):
            return self.render()
    
        def render(self, name=None, value=None, attrs=None, choices=()):
            # NOTE: not sure if we need to compute this as we don't use it
            if self.id_for_label:
                label_for = format_html(' for="{}"', self.id_for_label)
            else:
                label_for = ''
            attrs = dict(self.attrs, **attrs) if attrs else self.attrs
    
            # TODO: create CSS for btn-checkbox for convention
            return format_html(
                '<label class="btn btn-primary">{} {}</label>',
                self.tag(attrs),
                self.choice_label
            )
    
        def tag(self, attrs=None):
            attrs = attrs or self.attrs
            final_attrs = dict(attrs, type=self.input_type, name=self.name, value=self.choice_value)
            if self.is_checked():
                final_attrs['checked'] = 'checked'
            # added autocomplete off
            return format_html('<input{} autocomplete="off" />', flatatt(final_attrs))
    
    
    class PrettyCheckboxChoiceInput(PrettyChoiceInput):
        input_type = 'checkbox'
    
        def __init__(self, *args, **kwargs):
            super(PrettyCheckboxChoiceInput, self).__init__(*args, **kwargs)
            self.value = set(force_text(v) for v in self.value)
    
        def is_checked(self):
            return self.choice_value in self.value
    
    
    @html_safe
    @python_2_unicode_compatible
    class PrettyChoiceFieldRenderer(ChoiceFieldRenderer):
        outer_html = '<div class="btn-group" data-toggle="buttons"{id_attr}>{content}</div>'
        inner_html = '{choice_value}{sub_widgets}'
    
        def __getitem__(self, idx):
            choice = self.choices[idx]  # Let the IndexError propagate
            return self.choice_input_class(self.name, self.value, self.attrs.copy(), choice, idx)
    
        def __str__(self):
            return self.render()
    
        def render(self):
            """
            Outputs a <ul> for this set of choice fields.
            If an id was given to the field, it is applied to the <ul> (each
            item in the list will get an id of `$id_$i`).
            """
            id_ = self.attrs.get('id', None)
            output = []
            for i, choice in enumerate(self.choices):
                choice_value, choice_label = choice
                if isinstance(choice_label, (tuple, list)):
                    attrs_plus = self.attrs.copy()
                    if id_:
                        attrs_plus['id'] += '_{}'.format(i)
                    sub_ul_renderer = ChoiceFieldRenderer(name=self.name,
                                                          value=self.value,
                                                          attrs=attrs_plus,
                                                          choices=choice_label)
                    sub_ul_renderer.choice_input_class = self.choice_input_class
                    output.append(format_html(self.inner_html, choice_value=choice_value,
                                              sub_widgets=sub_ul_renderer.render()))
                else:
                    w = self.choice_input_class(self.name, self.value,
                                                self.attrs.copy(), choice, i)
                    output.append(format_html(self.inner_html,
                                              choice_value=force_text(w), sub_widgets=''))
            return format_html(self.outer_html,
                               id_attr=format_html(' id="{}"', id_) if id_ else '',
                               content=mark_safe('\n'.join(output)))
    
    
    class PrettyCheckboxFieldRenderer(PrettyChoiceFieldRenderer):
        choice_input_class = PrettyCheckboxChoiceInput
    
    
    class PrettyCheckboxSelectMultiple(RendererMixin, SelectMultiple):
        renderer = PrettyCheckboxFieldRenderer
        _empty_value = []
    

Most of the text in the classes and its methods is copied and pasted from
[widgets.py](https://github.com/django/django/blob/master/django/forms/widgets.py)

In the templates I simply put

    
    
    {{ form.as_p }}
    

And the problem is that all fields are rendered in html but the 'categories'
field is escaped. Inspecting the html it results that only the `inner_html`
from `PrettyChoiceFieldRenderer` is escaped while the `outer_html` is not
which means that the html is not escaped by the template system but by the
widget renderer before. But I don't know where. That's where I am asking for
help, if anyone can spot something incorrect.

**This is the html of the 'categories' field**

_as you can see the`outer_html` is not escaped but the `inner_html` is_

    
    
    <p>
        <label for="id_categories_0">Categories:</label>
        <div class="btn-group" data-toggle="buttons" id="id_categories">
    
            &lt;label class=&quot;btn btn-primary&quot;&gt;&lt;input id=&quot;id_categories_0&quot; name=&quot;categories&quot; type=&quot;checkbox&quot; value=&quot;1&quot; autocomplete=&quot;off&quot; /&gt; electronics&lt;/label&gt;&lt;label class=&quot;btn
            btn-primary&quot;&gt;&lt;input id=&quot;id_categories_1&quot; name=&quot;categories&quot; type=&quot;checkbox&quot; value=&quot;2&quot; autocomplete=&quot;off&quot; /&gt; bedroom and productivity&lt;/label&gt; &lt;label class=&quot;btn btn-primary&quot;&gt;&lt;input
            id=&quot;id_categories_2&quot; name=&quot;categories&quot; type=&quot;checkbox&quot; value=&quot;3&quot; autocomplete=&quot;off&quot; /&gt; phone&lt;/label&gt; &lt;label class=&quot;btn btn-primary&quot;&gt;&lt;input id=&quot;id_categories_3&quot;
            name=&quot;categories&quot; type=&quot;checkbox&quot; value=&quot;4&quot; autocomplete=&quot;off&quot; /&gt; office&lt;/label&gt; &lt;label class=&quot;btn btn-primary&quot;&gt;&lt;input id=&quot;id_categories_4&quot; name=&quot;categories&quot;
            type=&quot;checkbox&quot; value=&quot;6&quot; autocomplete=&quot;off&quot; /&gt; Kitchen&lt;/label&gt;
    
        </div>
    </p>
    

Thank you in advance


Answer: Alright, I fixed the issue. I removed the `force_text` in

    
    
    output.append(format_html(
                               self.inner_html,
                               choice_value=force_text(w),
                               sub_widgets=''
    ))
    

so it gives

    
    
    output.append(format_html(
                               self.inner_html,
                               choice_value=w,
                               sub_widgets=''
    ))
    

There must be a reason why Django has `force_text` in
<https://github.com/django/django/blob/master/django/forms/widgets.py#L728>

