Fixed #25135 -- Deprecated the contrib.admin allow_tags attribute.

Thanks Jaap Roes for the idea and initial patch.
This commit is contained in:
Ola Sitarska 2015-09-08 20:46:26 +01:00 committed by Tim Graham
parent 1bbca7961c
commit f2f8972def
9 changed files with 96 additions and 30 deletions

View File

@ -14,7 +14,9 @@ from django.db.models.fields.related import ManyToManyRel
from django.forms.utils import flatatt from django.forms.utils import flatatt
from django.template.defaultfilters import capfirst, linebreaksbr from django.template.defaultfilters import capfirst, linebreaksbr
from django.utils import six from django.utils import six
from django.utils.deprecation import RemovedInDjango110Warning from django.utils.deprecation import (
RemovedInDjango20Warning, RemovedInDjango110Warning,
)
from django.utils.encoding import force_text, smart_text from django.utils.encoding import force_text, smart_text
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.html import conditional_escape, format_html from django.utils.html import conditional_escape, format_html
@ -198,11 +200,20 @@ class AdminReadonlyField(object):
if boolean: if boolean:
result_repr = _boolean_icon(value) result_repr = _boolean_icon(value)
else: else:
result_repr = smart_text(value) if hasattr(value, "__html__"):
if getattr(attr, "allow_tags", False): result_repr = value
result_repr = mark_safe(result_repr)
else: else:
result_repr = linebreaksbr(result_repr) result_repr = smart_text(value)
if getattr(attr, "allow_tags", False):
warnings.warn(
"Deprecated allow_tags attribute used on %s. "
"Use django.utils.safestring.format_html(), "
"format_html_join(), or mark_safe() instead." % attr,
RemovedInDjango20Warning
)
result_repr = mark_safe(value)
else:
result_repr = linebreaksbr(result_repr)
else: else:
if isinstance(f.remote_field, ManyToManyRel) and value is not None: if isinstance(f.remote_field, ManyToManyRel) and value is not None:
result_repr = ", ".join(map(six.text_type, value.all())) result_repr = ", ".join(map(six.text_type, value.all()))

View File

@ -753,7 +753,6 @@ class ModelAdmin(BaseModelAdmin):
""" """
return helpers.checkbox.render(helpers.ACTION_CHECKBOX_NAME, force_text(obj.pk)) return helpers.checkbox.render(helpers.ACTION_CHECKBOX_NAME, force_text(obj.pk))
action_checkbox.short_description = mark_safe('<input type="checkbox" id="action-toggle" />') action_checkbox.short_description = mark_safe('<input type="checkbox" id="action-toggle" />')
action_checkbox.allow_tags = True
def get_actions(self, request): def get_actions(self, request):
""" """

View File

@ -1,6 +1,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import datetime import datetime
import warnings
from django.contrib.admin.templatetags.admin_static import static from django.contrib.admin.templatetags.admin_static import static
from django.contrib.admin.templatetags.admin_urls import add_preserved_filters from django.contrib.admin.templatetags.admin_urls import add_preserved_filters
@ -16,6 +17,7 @@ from django.db import models
from django.template import Library from django.template import Library
from django.template.loader import get_template from django.template.loader import get_template
from django.utils import formats from django.utils import formats
from django.utils.deprecation import RemovedInDjango20Warning
from django.utils.encoding import force_text from django.utils.encoding import force_text
from django.utils.html import escapejs, format_html from django.utils.html import escapejs, format_html
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
@ -207,12 +209,14 @@ def items_for_result(cl, result, form):
row_classes = ['action-checkbox'] row_classes = ['action-checkbox']
allow_tags = getattr(attr, 'allow_tags', False) allow_tags = getattr(attr, 'allow_tags', False)
boolean = getattr(attr, 'boolean', False) boolean = getattr(attr, 'boolean', False)
if boolean or not value:
allow_tags = True
result_repr = display_for_value(value, empty_value_display, boolean) result_repr = display_for_value(value, empty_value_display, boolean)
# Strip HTML tags in the resulting text, except if the
# function has an "allow_tags" attribute set to True.
if allow_tags: if allow_tags:
warnings.warn(
"Deprecated allow_tags attribute used on field {}. "
"Use django.utils.safestring.format_html(), "
"format_html_join(), or mark_safe() instead.".format(field_name),
RemovedInDjango20Warning
)
result_repr = mark_safe(result_repr) result_repr = mark_safe(result_repr)
if isinstance(value, (datetime.date, datetime.time)): if isinstance(value, (datetime.date, datetime.time)):
row_classes.append('nowrap') row_classes.append('nowrap')

View File

@ -265,6 +265,9 @@ details on these changes.
* The warning that :class:`~django.core.signing.Signer` issues when given an * The warning that :class:`~django.core.signing.Signer` issues when given an
invalid separator will become an exception. invalid separator will become an exception.
* Support for the ``allow_tags`` attribute on ``ModelAdmin`` methods will be
removed.
.. _deprecation-removed-in-1.9: .. _deprecation-removed-in-1.9:
1.9 1.9

View File

@ -583,11 +583,9 @@ subclass::
``False``. ``False``.
* If the string given is a method of the model, ``ModelAdmin`` or a * If the string given is a method of the model, ``ModelAdmin`` or a
callable, Django will HTML-escape the output by default. If you'd callable, Django will HTML-escape the output by default. To escape
rather not escape the output of the method, give the method an user input and allow your own unescaped tags, use
``allow_tags`` attribute whose value is ``True``. However, to avoid an :func:`~django.utils.html.format_html`.
XSS vulnerability, you should use :func:`~django.utils.html.format_html`
to escape user-provided inputs.
Here's a full example model:: Here's a full example model::
@ -606,11 +604,17 @@ subclass::
self.first_name, self.first_name,
self.last_name) self.last_name)
colored_name.allow_tags = True
class PersonAdmin(admin.ModelAdmin): class PersonAdmin(admin.ModelAdmin):
list_display = ('first_name', 'last_name', 'colored_name') list_display = ('first_name', 'last_name', 'colored_name')
.. deprecated:: 1.9
In older versions, you could add an ``allow_tags`` attribute to the
method to prevent auto-escaping. This attribute is deprecated as it's
safer to use :func:`~django.utils.html.format_html`,
:func:`~django.utils.html.format_html_join`, or
:func:`~django.utils.safestring.mark_safe` instead.
* If the value of a field is ``None``, an empty string, or an iterable * If the value of a field is ``None``, an empty string, or an iterable
without elements, Django will display ``-`` (a dash). You can override without elements, Django will display ``-`` (a dash). You can override
this with :attr:`AdminSite.empty_value_display`:: this with :attr:`AdminSite.empty_value_display`::
@ -688,7 +692,6 @@ subclass::
self.color_code, self.color_code,
self.first_name) self.first_name)
colored_first_name.allow_tags = True
colored_first_name.admin_order_field = 'first_name' colored_first_name.admin_order_field = 'first_name'
class PersonAdmin(admin.ModelAdmin): class PersonAdmin(admin.ModelAdmin):
@ -1095,12 +1098,10 @@ subclass::
mark_safe('<br/>'), mark_safe('<br/>'),
'{}', '{}',
((line,) for line in instance.get_full_address()), ((line,) for line in instance.get_full_address()),
) or "<span class='errors'>I can't determine this address.</span>" ) or mark_safe("<span class='errors'>I can't determine this address.</span>")
# short_description functions like a model field's verbose_name # short_description functions like a model field's verbose_name
address_report.short_description = "Address" address_report.short_description = "Address"
# in this example, we have used HTML tags in the output
address_report.allow_tags = True
.. attribute:: ModelAdmin.save_as .. attribute:: ModelAdmin.save_as

View File

@ -1212,6 +1212,12 @@ Miscellaneous
``SimpleTestCase.assertRaisesMessage()`` is deprecated. Pass the callable as ``SimpleTestCase.assertRaisesMessage()`` is deprecated. Pass the callable as
a positional argument instead. a positional argument instead.
* The ``allow_tags`` attribute on methods of ``ModelAdmin`` has been
deprecated. Use :func:`~django.utils.html.format_html`,
:func:`~django.utils.html.format_html_join`, or
:func:`~django.utils.safestring.mark_safe` when constructing the method's
return value instead.
.. removed-features-1.9: .. removed-features-1.9:
Features removed in 1.9 Features removed in 1.9

View File

@ -12,9 +12,10 @@ from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.template import Context, Template from django.template import Context, Template
from django.test import TestCase, override_settings from django.test import TestCase, ignore_warnings, override_settings
from django.test.client import RequestFactory from django.test.client import RequestFactory
from django.utils import formats, six from django.utils import formats, six
from django.utils.deprecation import RemovedInDjango20Warning
from .admin import ( from .admin import (
BandAdmin, ChildAdmin, ChordsBandAdmin, ConcertAdmin, BandAdmin, ChildAdmin, ChordsBandAdmin, ConcertAdmin,
@ -168,7 +169,7 @@ class ChangeListTests(TestCase):
link = reverse('admin:admin_changelist_child_change', args=(new_child.id,)) link = reverse('admin:admin_changelist_child_change', args=(new_child.id,))
row_html = ( row_html = (
'<tbody><tr class="row1"><th class="field-name"><a href="%s">name</a></th>' '<tbody><tr class="row1"><th class="field-name"><a href="%s">name</a></th>'
'<td class="field-age_display">&dagger;</td><td class="field-age">-empty-</td></tr></tbody>' % link '<td class="field-age_display">&amp;dagger;</td><td class="field-age">-empty-</td></tr></tbody>' % link
) )
self.assertNotEqual(table_output.find(row_html), -1, self.assertNotEqual(table_output.find(row_html), -1,
'Failed to find expected row element: %s' % table_output) 'Failed to find expected row element: %s' % table_output)
@ -253,6 +254,38 @@ class ChangeListTests(TestCase):
m.list_filter, m.date_hierarchy, m.search_fields, m.list_filter, m.date_hierarchy, m.search_fields,
m.list_select_related, m.list_per_page, m.list_max_show_all, m.list_editable, m)) m.list_select_related, m.list_per_page, m.list_max_show_all, m.list_editable, m))
@ignore_warnings(category=RemovedInDjango20Warning)
def test_result_list_with_allow_tags(self):
"""
Test for deprecation of allow_tags attribute
"""
new_parent = Parent.objects.create(name='parent')
for i in range(2):
Child.objects.create(name='name %s' % i, parent=new_parent)
request = self.factory.get('/child/')
m = ChildAdmin(Child, custom_site)
def custom_method(self, obj=None):
return 'Unsafe html <br />'
custom_method.allow_tags = True
# Add custom method with allow_tags attribute
m.custom_method = custom_method
m.list_display = ['id', 'name', 'parent', 'custom_method']
cl = ChangeList(
request, Child, m.list_display, m.list_display_links,
m.list_filter, m.date_hierarchy, m.search_fields,
m.list_select_related, m.list_per_page, m.list_max_show_all, m.list_editable, m
)
FormSet = m.get_changelist_formset(request)
cl.formset = FormSet(queryset=cl.result_list)
template = Template('{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}')
context = Context({'cl': cl})
table_output = template.render(context)
custom_field_html = '<td class="field-custom_method">Unsafe html <br /></td>'
self.assertInHTML(custom_field_html, table_output)
def test_custom_paginator(self): def test_custom_paginator(self):
new_parent = Parent.objects.create(name='parent') new_parent = Parent.objects.create(name='parent')
for i in range(200): for i in range(200):

View File

@ -18,6 +18,7 @@ from django.core.mail import EmailMessage
from django.db import models from django.db import models
from django.forms.models import BaseModelFormSet from django.forms.models import BaseModelFormSet
from django.http import HttpResponse, StreamingHttpResponse from django.http import HttpResponse, StreamingHttpResponse
from django.utils.html import format_html
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.six import StringIO from django.utils.six import StringIO
@ -429,7 +430,8 @@ class PostAdmin(admin.ModelAdmin):
list_display = ['title', 'public'] list_display = ['title', 'public']
readonly_fields = ( readonly_fields = (
'posted', 'awesomeness_level', 'coolness', 'value', 'posted', 'awesomeness_level', 'coolness', 'value',
'multiline', 'multiline_html', lambda obj: "foo" 'multiline', 'multiline_html', lambda obj: "foo",
'multiline_html_allow_tags',
) )
inlines = [ inlines = [
@ -444,15 +446,17 @@ class PostAdmin(admin.ModelAdmin):
def value(self, instance): def value(self, instance):
return 1000 return 1000
value.short_description = 'Value in $US'
def multiline(self, instance): def multiline(self, instance):
return "Multiline\ntest\nstring" return "Multiline\ntest\nstring"
def multiline_html(self, instance): def multiline_html(self, instance):
return mark_safe("Multiline<br>\nhtml<br>\ncontent") return mark_safe("Multiline<br>\nhtml<br>\ncontent")
multiline_html.allow_tags = True
value.short_description = 'Value in $US' def multiline_html_allow_tags(self, instance):
return "Multiline<br>html<br>content<br>with allow tags"
multiline_html_allow_tags.allow_tags = True
class FieldOverridePostForm(forms.ModelForm): class FieldOverridePostForm(forms.ModelForm):
@ -574,8 +578,7 @@ class ComplexSortedPersonAdmin(admin.ModelAdmin):
ordering = ('name',) ordering = ('name',)
def colored_name(self, obj): def colored_name(self, obj):
return '<span style="color: #%s;">%s</span>' % ('ff00ff', obj.name) return format_html('<span style="color: #ff00ff;">{}</span>', obj.name)
colored_name.allow_tags = True
colored_name.admin_order_field = 'name' colored_name.admin_order_field = 'name'

View File

@ -27,13 +27,14 @@ from django.forms.utils import ErrorList
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.test import ( from django.test import (
SimpleTestCase, TestCase, modify_settings, override_settings, SimpleTestCase, TestCase, ignore_warnings, modify_settings,
skipUnlessDBFeature, override_settings, skipUnlessDBFeature,
) )
from django.test.utils import override_script_prefix, patch_logger from django.test.utils import override_script_prefix, patch_logger
from django.utils import formats, six, translation from django.utils import formats, six, translation
from django.utils._os import upath from django.utils._os import upath
from django.utils.cache import get_max_age from django.utils.cache import get_max_age
from django.utils.deprecation import RemovedInDjango20Warning
from django.utils.encoding import force_bytes, force_text, iri_to_uri from django.utils.encoding import force_bytes, force_text, iri_to_uri
from django.utils.html import escape from django.utils.html import escape
from django.utils.http import urlencode from django.utils.http import urlencode
@ -4681,6 +4682,7 @@ class ReadonlyTest(TestCase):
def setUp(self): def setUp(self):
self.client.login(username='super', password='secret') self.client.login(username='super', password='secret')
@ignore_warnings(category=RemovedInDjango20Warning) # for allow_tags deprecation
def test_readonly_get(self): def test_readonly_get(self):
response = self.client.get(reverse('admin:admin_views_post_add')) response = self.client.get(reverse('admin:admin_views_post_add'))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
@ -4699,6 +4701,8 @@ class ReadonlyTest(TestCase):
self.assertContains(response, "Multiline<br />test<br />string") self.assertContains(response, "Multiline<br />test<br />string")
self.assertContains(response, "<p>Multiline<br />html<br />content</p>", html=True) self.assertContains(response, "<p>Multiline<br />html<br />content</p>", html=True)
self.assertContains(response, "InlineMultiline<br />test<br />string") self.assertContains(response, "InlineMultiline<br />test<br />string")
# Remove only this last line when the deprecation completes.
self.assertContains(response, "<p>Multiline<br />html<br />content<br />with allow tags</p>", html=True)
self.assertContains(response, self.assertContains(response,
formats.localize(datetime.date.today() - datetime.timedelta(days=7))) formats.localize(datetime.date.today() - datetime.timedelta(days=7)))
@ -4768,6 +4772,7 @@ class ReadonlyTest(TestCase):
response = self.client.get(reverse('admin:admin_views_topping_add')) response = self.client.get(reverse('admin:admin_views_topping_add'))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
@ignore_warnings(category=RemovedInDjango20Warning) # for allow_tags deprecation
def test_readonly_field_overrides(self): def test_readonly_field_overrides(self):
""" """
Regression test for #22087 - ModelForm Meta overrides are ignored by Regression test for #22087 - ModelForm Meta overrides are ignored by
@ -5181,6 +5186,7 @@ class CSSTest(TestCase):
def setUp(self): def setUp(self):
self.client.login(username='super', password='secret') self.client.login(username='super', password='secret')
@ignore_warnings(category=RemovedInDjango20Warning) # for allow_tags deprecation
def test_field_prefix_css_classes(self): def test_field_prefix_css_classes(self):
""" """
Ensure that fields have a CSS class name with a 'field-' prefix. Ensure that fields have a CSS class name with a 'field-' prefix.