Merge pull request #1565 from garrypolley/#19988-ordinal-html-i18n-fix

fixes #19988, allow html in ordinal for humanize
This commit is contained in:
Alex Gaynor 2013-09-07 11:13:55 -07:00
commit 907f19eba7
2 changed files with 28 additions and 6 deletions

View File

@ -8,11 +8,13 @@ from django.conf import settings
from django.template import defaultfilters from django.template import defaultfilters
from django.utils.encoding import force_text from django.utils.encoding import force_text
from django.utils.formats import number_format from django.utils.formats import number_format
from django.utils.safestring import mark_safe
from django.utils.translation import pgettext, ungettext, ugettext as _ from django.utils.translation import pgettext, ungettext, ugettext as _
from django.utils.timezone import is_aware, utc from django.utils.timezone import is_aware, utc
register = template.Library() register = template.Library()
@register.filter(is_safe=True) @register.filter(is_safe=True)
def ordinal(value): def ordinal(value):
""" """
@ -24,9 +26,11 @@ def ordinal(value):
except (TypeError, ValueError): except (TypeError, ValueError):
return value return value
suffixes = (_('th'), _('st'), _('nd'), _('rd'), _('th'), _('th'), _('th'), _('th'), _('th'), _('th')) suffixes = (_('th'), _('st'), _('nd'), _('rd'), _('th'), _('th'), _('th'), _('th'), _('th'), _('th'))
if value % 100 in (11, 12, 13): # special case if value % 100 in (11, 12, 13): # special case
return "%d%s" % (value, suffixes[0]) return mark_safe("%d%s" % (value, suffixes[0]))
return "%d%s" % (value, suffixes[value % 10]) # Mark value safe so i18n does not break with <sup> or <sub> see #19988
return mark_safe("%d%s" % (value, suffixes[value % 10]))
@register.filter(is_safe=True) @register.filter(is_safe=True)
def intcomma(value, use_l10n=True): def intcomma(value, use_l10n=True):
@ -97,6 +101,7 @@ intword_converters = (
)), )),
) )
@register.filter(is_safe=False) @register.filter(is_safe=False)
def intword(value): def intword(value):
""" """
@ -130,6 +135,7 @@ def intword(value):
return _check_for_i18n(new_value, *converters(new_value)) return _check_for_i18n(new_value, *converters(new_value))
return value return value
@register.filter(is_safe=True) @register.filter(is_safe=True)
def apnumber(value): def apnumber(value):
""" """
@ -144,6 +150,7 @@ def apnumber(value):
return value return value
return (_('one'), _('two'), _('three'), _('four'), _('five'), _('six'), _('seven'), _('eight'), _('nine'))[value-1] return (_('one'), _('two'), _('three'), _('four'), _('five'), _('six'), _('seven'), _('eight'), _('nine'))[value-1]
# Perform the comparison in the default time zone when USE_TZ = True # Perform the comparison in the default time zone when USE_TZ = True
# (unless a specific time zone has been applied with the |timezone filter). # (unless a specific time zone has been applied with the |timezone filter).
@register.filter(expects_localtime=True) @register.filter(expects_localtime=True)
@ -172,6 +179,7 @@ def naturalday(value, arg=None):
return _('yesterday') return _('yesterday')
return defaultfilters.date(value, arg) return defaultfilters.date(value, arg)
# This filter doesn't require expects_localtime=True because it deals properly # This filter doesn't require expects_localtime=True because it deals properly
# with both naive and aware datetimes. Therefore avoid the cost of conversion. # with both naive and aware datetimes. Therefore avoid the cost of conversion.
@register.filter @register.filter
@ -180,7 +188,7 @@ def naturaltime(value):
For date and time values shows how many seconds, minutes or hours ago For date and time values shows how many seconds, minutes or hours ago
compared to current timestamp returns representing string. compared to current timestamp returns representing string.
""" """
if not isinstance(value, date): # datetime is a subclass of date if not isinstance(value, date): # datetime is a subclass of date
return value return value
now = datetime.now(utc if is_aware(value) else None) now = datetime.now(utc if is_aware(value) else None)

View File

@ -28,6 +28,7 @@ from i18n import TransRealMixin
now = datetime.datetime(2012, 3, 9, 22, 30) now = datetime.datetime(2012, 3, 9, 22, 30)
class MockDateTime(datetime.datetime): class MockDateTime(datetime.datetime):
@classmethod @classmethod
def now(self, tz=None): def now(self, tz=None):
@ -40,11 +41,11 @@ class MockDateTime(datetime.datetime):
class HumanizeTests(TransRealMixin, TestCase): class HumanizeTests(TransRealMixin, TestCase):
def humanize_tester(self, test_list, result_list, method): def humanize_tester(self, test_list, result_list, method, normalize_result_func=escape):
for test_content, result in zip(test_list, result_list): for test_content, result in zip(test_list, result_list):
t = Template('{%% load humanize %%}{{ test_content|%s }}' % method) t = Template('{%% load humanize %%}{{ test_content|%s }}' % method)
rendered = t.render(Context(locals())).strip() rendered = t.render(Context(locals())).strip()
self.assertEqual(rendered, escape(result), self.assertEqual(rendered, normalize_result_func(result),
msg="%s test failed, produced '%s', should've produced '%s'" % (method, rendered, result)) msg="%s test failed, produced '%s', should've produced '%s'" % (method, rendered, result))
def test_ordinal(self): def test_ordinal(self):
@ -58,6 +59,19 @@ class HumanizeTests(TransRealMixin, TestCase):
with translation.override('en'): with translation.override('en'):
self.humanize_tester(test_list, result_list, 'ordinal') self.humanize_tester(test_list, result_list, 'ordinal')
def test_i18n_html_ordinal(self):
"""Allow html in output on i18n strings"""
test_list = ('1', '2', '3', '4', '11', '12',
'13', '101', '102', '103', '111',
'something else', None)
result_list = ('1<sup>er</sup>', '2<sup>e</sup>', '3<sup>e</sup>', '4<sup>e</sup>',
'11<sup>e</sup>', '12<sup>e</sup>', '13<sup>e</sup>', '101<sup>er</sup>',
'102<sup>e</sup>', '103<sup>e</sup>', '111<sup>e</sup>', 'something else',
'None')
with translation.override('fr-fr'):
self.humanize_tester(test_list, result_list, 'ordinal', lambda x: x)
def test_intcomma(self): def test_intcomma(self):
test_list = (100, 1000, 10123, 10311, 1000000, 1234567.25, test_list = (100, 1000, 10123, 10311, 1000000, 1234567.25,
'100', '1000', '10123', '10311', '1000000', '1234567.1234567', Decimal('1234567.1234567'), '100', '1000', '10123', '10311', '1000000', '1234567.1234567', Decimal('1234567.1234567'),