显示转义的 Django 自定义小部件 html

Django custom widget displaying escaped html

我为 Django 表单编写了一个小部件,以便在模板中直接拥有一个 bootstrap3 多个复选框,以便将来重用它。

所以我编写了一个包含所有基本文件(__init__.pyviews.py ...)的应用程序 prettyforms 并创建了一个 forms.py 我在其中编写了自定义小部件.

视图item/forms.py的主要形式如

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')
        }

包含widgetprettyforms/forms.py的文件如:

# -*- 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 = []

类 中的大部分文本及其方法是从 widgets.py

复制和粘贴的

在模板中我简单地输入了

{{ form.as_p }}

问题是所有字段都在 html 中呈现,但 'categories' 字段被转义了。检查 html 结果只有 PrettyChoiceFieldRenderer 中的 inner_html 被转义,而 outer_html 没有被转义,这意味着 html 没有被模板系统转义但之前是小部件渲染器。但我不知道在哪里。如果有人能发现不正确的地方,这就是我寻求帮助的地方。

这是'categories'字段的html

如您所见,outer_html 未转义,但 inner_html

<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>

提前致谢

@html_safe 装饰器将 __str____html__ 的输出标记为安全。它不会为您的渲染方法做任何事情。您想要 mark_safe 您的 return 字符串。

from django.utils.safestring import mark_safe

return mark_safe(format_html(self.outer_html,
                           id_attr=format_html(' id="{}"', id_) if id_ else '',
                           content=mark_safe('\n'.join(output))))

好的,我解决了这个问题。我删除了

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

所以它给出

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

Django在https://github.com/django/django/blob/master/django/forms/widgets.py#L728

中有force_text一定是有原因的