Fixed #20246 -- Added non-breaking spaces between values an units

This commit is contained in:
Emil Stenström 2013-05-18 13:58:45 +02:00 committed by Claude Paroz
parent caf56ad174
commit 7d77e9786a
9 changed files with 155 additions and 130 deletions

View File

@ -536,6 +536,7 @@ answer newbie questions, and generally made Django that much better:
starrynight <cmorgh@gmail.com> starrynight <cmorgh@gmail.com>
Vasiliy Stavenko <stavenko@gmail.com> Vasiliy Stavenko <stavenko@gmail.com>
Thomas Steinacher <http://www.eggdrop.ch/> Thomas Steinacher <http://www.eggdrop.ch/>
Emil Stenström <em@kth.se>
Johan C. Stöver <johan@nilling.nl> Johan C. Stöver <johan@nilling.nl>
Nowell Strite <http://nowell.strite.org/> Nowell Strite <http://nowell.strite.org/>
Thomas Stromberg <tstromberg@google.com> Thomas Stromberg <tstromberg@google.com>

View File

@ -194,17 +194,20 @@ def naturaltime(value):
return _('now') return _('now')
elif delta.seconds < 60: elif delta.seconds < 60:
return ungettext( return ungettext(
'a second ago', '%(count)s seconds ago', delta.seconds # Translators: \\u00a0 is non-breaking space
'a second ago', '%(count)s\u00a0seconds ago', delta.seconds
) % {'count': delta.seconds} ) % {'count': delta.seconds}
elif delta.seconds // 60 < 60: elif delta.seconds // 60 < 60:
count = delta.seconds // 60 count = delta.seconds // 60
return ungettext( return ungettext(
'a minute ago', '%(count)s minutes ago', count # Translators: \\u00a0 is non-breaking space
'a minute ago', '%(count)s\u00a0minutes ago', count
) % {'count': count} ) % {'count': count}
else: else:
count = delta.seconds // 60 // 60 count = delta.seconds // 60 // 60
return ungettext( return ungettext(
'an hour ago', '%(count)s hours ago', count # Translators: \\u00a0 is non-breaking space
'an hour ago', '%(count)s\u00a0hours ago', count
) % {'count': count} ) % {'count': count}
else: else:
delta = value - now delta = value - now
@ -216,15 +219,18 @@ def naturaltime(value):
return _('now') return _('now')
elif delta.seconds < 60: elif delta.seconds < 60:
return ungettext( return ungettext(
'a second from now', '%(count)s seconds from now', delta.seconds # Translators: \\u00a0 is non-breaking space
'a second from now', '%(count)s\u00a0seconds from now', delta.seconds
) % {'count': delta.seconds} ) % {'count': delta.seconds}
elif delta.seconds // 60 < 60: elif delta.seconds // 60 < 60:
count = delta.seconds // 60 count = delta.seconds // 60
return ungettext( return ungettext(
'a minute from now', '%(count)s minutes from now', count # Translators: \\u00a0 is non-breaking space
'a minute from now', '%(count)s\u00a0minutes from now', count
) % {'count': count} ) % {'count': count}
else: else:
count = delta.seconds // 60 // 60 count = delta.seconds // 60 // 60
return ungettext( return ungettext(
'an hour from now', '%(count)s hours from now', count # Translators: \\u00a0 is non-breaking space
'an hour from now', '%(count)s\u00a0hours from now', count
) % {'count': count} ) % {'count': count}

View File

@ -195,22 +195,22 @@ class HumanizeTests(TestCase):
result_list = [ result_list = [
'now', 'now',
'a second ago', 'a second ago',
'30 seconds ago', '30\xa0seconds ago',
'a minute ago', 'a minute ago',
'2 minutes ago', '2\xa0minutes ago',
'an hour ago', 'an hour ago',
'23 hours ago', '23\xa0hours ago',
'1 day ago', '1\xa0day ago',
'1 year, 4 months ago', '1\xa0year, 4\xa0months ago',
'a second from now', 'a second from now',
'30 seconds from now', '30\xa0seconds from now',
'a minute from now', 'a minute from now',
'2 minutes from now', '2\xa0minutes from now',
'an hour from now', 'an hour from now',
'23 hours from now', '23\xa0hours from now',
'1 day from now', '1\xa0day from now',
'2 days, 6 hours from now', '2\xa0days, 6\xa0hours from now',
'1 year, 4 months from now', '1\xa0year, 4\xa0months from now',
'now', 'now',
'now', 'now',
] ]
@ -218,8 +218,8 @@ class HumanizeTests(TestCase):
# date in naive arithmetic is only 2 days and 5 hours after in # date in naive arithmetic is only 2 days and 5 hours after in
# aware arithmetic. # aware arithmetic.
result_list_with_tz_support = result_list[:] result_list_with_tz_support = result_list[:]
assert result_list_with_tz_support[-4] == '2 days, 6 hours from now' assert result_list_with_tz_support[-4] == '2\xa0days, 6\xa0hours from now'
result_list_with_tz_support[-4] == '2 days, 5 hours from now' result_list_with_tz_support[-4] == '2\xa0days, 5\xa0hours from now'
orig_humanize_datetime, humanize.datetime = humanize.datetime, MockDateTime orig_humanize_datetime, humanize.datetime = humanize.datetime, MockDateTime
try: try:

View File

@ -14,7 +14,7 @@ from django.utils import formats
from django.utils.dateformat import format, time_format from django.utils.dateformat import format, time_format
from django.utils.encoding import force_text, iri_to_uri from django.utils.encoding import force_text, iri_to_uri
from django.utils.html import (conditional_escape, escapejs, fix_ampersands, from django.utils.html import (conditional_escape, escapejs, fix_ampersands,
escape, urlize as urlize_impl, linebreaks, strip_tags) escape, urlize as urlize_impl, linebreaks, strip_tags, avoid_wrapping)
from django.utils.http import urlquote from django.utils.http import urlquote
from django.utils.text import Truncator, wrap, phone2numeric from django.utils.text import Truncator, wrap, phone2numeric
from django.utils.safestring import mark_safe, SafeData, mark_for_escaping from django.utils.safestring import mark_safe, SafeData, mark_for_escaping
@ -810,7 +810,8 @@ def filesizeformat(bytes):
try: try:
bytes = float(bytes) bytes = float(bytes)
except (TypeError,ValueError,UnicodeDecodeError): except (TypeError,ValueError,UnicodeDecodeError):
return ungettext("%(size)d byte", "%(size)d bytes", 0) % {'size': 0} value = ungettext("%(size)d byte", "%(size)d bytes", 0) % {'size': 0}
return avoid_wrapping(value)
filesize_number_format = lambda value: formats.number_format(round(value, 1), 1) filesize_number_format = lambda value: formats.number_format(round(value, 1), 1)
@ -821,16 +822,19 @@ def filesizeformat(bytes):
PB = 1<<50 PB = 1<<50
if bytes < KB: if bytes < KB:
return ungettext("%(size)d byte", "%(size)d bytes", bytes) % {'size': bytes} value = ungettext("%(size)d byte", "%(size)d bytes", bytes) % {'size': bytes}
if bytes < MB: elif bytes < MB:
return ugettext("%s KB") % filesize_number_format(bytes / KB) value = ugettext("%s KB") % filesize_number_format(bytes / KB)
if bytes < GB: elif bytes < GB:
return ugettext("%s MB") % filesize_number_format(bytes / MB) value = ugettext("%s MB") % filesize_number_format(bytes / MB)
if bytes < TB: elif bytes < TB:
return ugettext("%s GB") % filesize_number_format(bytes / GB) value = ugettext("%s GB") % filesize_number_format(bytes / GB)
if bytes < PB: elif bytes < PB:
return ugettext("%s TB") % filesize_number_format(bytes / TB) value = ugettext("%s TB") % filesize_number_format(bytes / TB)
return ugettext("%s PB") % filesize_number_format(bytes / PB) else:
value = ugettext("%s PB") % filesize_number_format(bytes / PB)
return avoid_wrapping(value)
@register.filter(is_safe=False) @register.filter(is_safe=False)
def pluralize(value, arg='s'): def pluralize(value, arg='s'):

View File

@ -281,3 +281,10 @@ def clean_html(text):
text = trailing_empty_content_re.sub('', text) text = trailing_empty_content_re.sub('', text)
return text return text
clean_html = allow_lazy(clean_html, six.text_type) clean_html = allow_lazy(clean_html, six.text_type)
def avoid_wrapping(value):
"""
Avoid text wrapping in the middle of a phrase by adding non-breaking
spaces where there previously were normal spaces.
"""
return value.replace(" ", "\xa0")

View File

@ -2,6 +2,7 @@ from __future__ import unicode_literals
import datetime import datetime
from django.utils.html import avoid_wrapping
from django.utils.timezone import is_aware, utc from django.utils.timezone import is_aware, utc
from django.utils.translation import ugettext, ungettext_lazy from django.utils.translation import ugettext, ungettext_lazy
@ -40,18 +41,18 @@ def timesince(d, now=None, reversed=False):
since = delta.days * 24 * 60 * 60 + delta.seconds since = delta.days * 24 * 60 * 60 + delta.seconds
if since <= 0: if since <= 0:
# d is in the future compared to now, stop processing. # d is in the future compared to now, stop processing.
return ugettext('0 minutes') return avoid_wrapping(ugettext('0 minutes'))
for i, (seconds, name) in enumerate(chunks): for i, (seconds, name) in enumerate(chunks):
count = since // seconds count = since // seconds
if count != 0: if count != 0:
break break
result = name % count result = avoid_wrapping(name % count)
if i + 1 < len(chunks): if i + 1 < len(chunks):
# Now get the second item # Now get the second item
seconds2, name2 = chunks[i + 1] seconds2, name2 = chunks[i + 1]
count2 = (since - (seconds * count)) // seconds2 count2 = (since - (seconds * count)) // seconds2
if count2 != 0: if count2 != 0:
result += ugettext(', ') + name2 % count2 result += ugettext(', ') + avoid_wrapping(name2 % count2)
return result return result
def timeuntil(d, now=None): def timeuntil(d, now=None):

View File

@ -527,24 +527,26 @@ class DefaultFiltersTests(TestCase):
def test_timesince(self): def test_timesince(self):
# real testing is done in timesince.py, where we can provide our own 'now' # real testing is done in timesince.py, where we can provide our own 'now'
# NOTE: \xa0 avoids wrapping between value and unit
self.assertEqual( self.assertEqual(
timesince_filter(datetime.datetime.now() - datetime.timedelta(1)), timesince_filter(datetime.datetime.now() - datetime.timedelta(1)),
'1 day') '1\xa0day')
self.assertEqual( self.assertEqual(
timesince_filter(datetime.datetime(2005, 12, 29), timesince_filter(datetime.datetime(2005, 12, 29),
datetime.datetime(2005, 12, 30)), datetime.datetime(2005, 12, 30)),
'1 day') '1\xa0day')
def test_timeuntil(self): def test_timeuntil(self):
# NOTE: \xa0 avoids wrapping between value and unit
self.assertEqual( self.assertEqual(
timeuntil_filter(datetime.datetime.now() + datetime.timedelta(1, 1)), timeuntil_filter(datetime.datetime.now() + datetime.timedelta(1, 1)),
'1 day') '1\xa0day')
self.assertEqual( self.assertEqual(
timeuntil_filter(datetime.datetime(2005, 12, 30), timeuntil_filter(datetime.datetime(2005, 12, 30),
datetime.datetime(2005, 12, 29)), datetime.datetime(2005, 12, 29)),
'1 day') '1\xa0day')
def test_default(self): def test_default(self):
self.assertEqual(default("val", "default"), 'val') self.assertEqual(default("val", "default"), 'val')
@ -574,43 +576,45 @@ class DefaultFiltersTests(TestCase):
'get out of town') 'get out of town')
def test_filesizeformat(self): def test_filesizeformat(self):
self.assertEqual(filesizeformat(1023), '1023 bytes') # NOTE: \xa0 avoids wrapping between value and unit
self.assertEqual(filesizeformat(1024), '1.0 KB') self.assertEqual(filesizeformat(1023), '1023\xa0bytes')
self.assertEqual(filesizeformat(10*1024), '10.0 KB') self.assertEqual(filesizeformat(1024), '1.0\xa0KB')
self.assertEqual(filesizeformat(1024*1024-1), '1024.0 KB') self.assertEqual(filesizeformat(10*1024), '10.0\xa0KB')
self.assertEqual(filesizeformat(1024*1024), '1.0 MB') self.assertEqual(filesizeformat(1024*1024-1), '1024.0\xa0KB')
self.assertEqual(filesizeformat(1024*1024*50), '50.0 MB') self.assertEqual(filesizeformat(1024*1024), '1.0\xa0MB')
self.assertEqual(filesizeformat(1024*1024*1024-1), '1024.0 MB') self.assertEqual(filesizeformat(1024*1024*50), '50.0\xa0MB')
self.assertEqual(filesizeformat(1024*1024*1024), '1.0 GB') self.assertEqual(filesizeformat(1024*1024*1024-1), '1024.0\xa0MB')
self.assertEqual(filesizeformat(1024*1024*1024*1024), '1.0 TB') self.assertEqual(filesizeformat(1024*1024*1024), '1.0\xa0GB')
self.assertEqual(filesizeformat(1024*1024*1024*1024*1024), '1.0 PB') self.assertEqual(filesizeformat(1024*1024*1024*1024), '1.0\xa0TB')
self.assertEqual(filesizeformat(1024*1024*1024*1024*1024), '1.0\xa0PB')
self.assertEqual(filesizeformat(1024*1024*1024*1024*1024*2000), self.assertEqual(filesizeformat(1024*1024*1024*1024*1024*2000),
'2000.0 PB') '2000.0\xa0PB')
self.assertEqual(filesizeformat(complex(1,-1)), '0 bytes') self.assertEqual(filesizeformat(complex(1,-1)), '0\xa0bytes')
self.assertEqual(filesizeformat(""), '0 bytes') self.assertEqual(filesizeformat(""), '0\xa0bytes')
self.assertEqual(filesizeformat("\N{GREEK SMALL LETTER ALPHA}"), self.assertEqual(filesizeformat("\N{GREEK SMALL LETTER ALPHA}"),
'0 bytes') '0\xa0bytes')
def test_localized_filesizeformat(self): def test_localized_filesizeformat(self):
# NOTE: \xa0 avoids wrapping between value and unit
with self.settings(USE_L10N=True): with self.settings(USE_L10N=True):
with translation.override('de', deactivate=True): with translation.override('de', deactivate=True):
self.assertEqual(filesizeformat(1023), '1023 Bytes') self.assertEqual(filesizeformat(1023), '1023\xa0Bytes')
self.assertEqual(filesizeformat(1024), '1,0 KB') self.assertEqual(filesizeformat(1024), '1,0\xa0KB')
self.assertEqual(filesizeformat(10*1024), '10,0 KB') self.assertEqual(filesizeformat(10*1024), '10,0\xa0KB')
self.assertEqual(filesizeformat(1024*1024-1), '1024,0 KB') self.assertEqual(filesizeformat(1024*1024-1), '1024,0\xa0KB')
self.assertEqual(filesizeformat(1024*1024), '1,0 MB') self.assertEqual(filesizeformat(1024*1024), '1,0\xa0MB')
self.assertEqual(filesizeformat(1024*1024*50), '50,0 MB') self.assertEqual(filesizeformat(1024*1024*50), '50,0\xa0MB')
self.assertEqual(filesizeformat(1024*1024*1024-1), '1024,0 MB') self.assertEqual(filesizeformat(1024*1024*1024-1), '1024,0\xa0MB')
self.assertEqual(filesizeformat(1024*1024*1024), '1,0 GB') self.assertEqual(filesizeformat(1024*1024*1024), '1,0\xa0GB')
self.assertEqual(filesizeformat(1024*1024*1024*1024), '1,0 TB') self.assertEqual(filesizeformat(1024*1024*1024*1024), '1,0\xa0TB')
self.assertEqual(filesizeformat(1024*1024*1024*1024*1024), self.assertEqual(filesizeformat(1024*1024*1024*1024*1024),
'1,0 PB') '1,0\xa0PB')
self.assertEqual(filesizeformat(1024*1024*1024*1024*1024*2000), self.assertEqual(filesizeformat(1024*1024*1024*1024*1024*2000),
'2000,0 PB') '2000,0\xa0PB')
self.assertEqual(filesizeformat(complex(1,-1)), '0 Bytes') self.assertEqual(filesizeformat(complex(1,-1)), '0\xa0Bytes')
self.assertEqual(filesizeformat(""), '0 Bytes') self.assertEqual(filesizeformat(""), '0\xa0Bytes')
self.assertEqual(filesizeformat("\N{GREEK SMALL LETTER ALPHA}"), self.assertEqual(filesizeformat("\N{GREEK SMALL LETTER ALPHA}"),
'0 Bytes') '0\xa0Bytes')
def test_pluralize(self): def test_pluralize(self):
self.assertEqual(pluralize(1), '') self.assertEqual(pluralize(1), '')

View File

@ -35,59 +35,60 @@ def get_filter_tests():
now_tz_i = datetime.now(FixedOffset((3 * 60) + 15)) # imaginary time zone now_tz_i = datetime.now(FixedOffset((3 * 60) + 15)) # imaginary time zone
today = date.today() today = date.today()
# NOTE: \xa0 avoids wrapping between value and unit
return { return {
# Default compare with datetime.now() # Default compare with datetime.now()
'filter-timesince01' : ('{{ a|timesince }}', {'a': datetime.now() + timedelta(minutes=-1, seconds = -10)}, '1 minute'), 'filter-timesince01' : ('{{ a|timesince }}', {'a': datetime.now() + timedelta(minutes=-1, seconds = -10)}, '1\xa0minute'),
'filter-timesince02' : ('{{ a|timesince }}', {'a': datetime.now() - timedelta(days=1, minutes = 1)}, '1 day'), 'filter-timesince02' : ('{{ a|timesince }}', {'a': datetime.now() - timedelta(days=1, minutes = 1)}, '1\xa0day'),
'filter-timesince03' : ('{{ a|timesince }}', {'a': datetime.now() - timedelta(hours=1, minutes=25, seconds = 10)}, '1 hour, 25 minutes'), 'filter-timesince03' : ('{{ a|timesince }}', {'a': datetime.now() - timedelta(hours=1, minutes=25, seconds = 10)}, '1\xa0hour, 25\xa0minutes'),
# Compare to a given parameter # Compare to a given parameter
'filter-timesince04' : ('{{ a|timesince:b }}', {'a':now - timedelta(days=2), 'b':now - timedelta(days=1)}, '1 day'), 'filter-timesince04' : ('{{ a|timesince:b }}', {'a':now - timedelta(days=2), 'b':now - timedelta(days=1)}, '1\xa0day'),
'filter-timesince05' : ('{{ a|timesince:b }}', {'a':now - timedelta(days=2, minutes=1), 'b':now - timedelta(days=2)}, '1 minute'), 'filter-timesince05' : ('{{ a|timesince:b }}', {'a':now - timedelta(days=2, minutes=1), 'b':now - timedelta(days=2)}, '1\xa0minute'),
# Check that timezone is respected # Check that timezone is respected
'filter-timesince06' : ('{{ a|timesince:b }}', {'a':now_tz - timedelta(hours=8), 'b':now_tz}, '8 hours'), 'filter-timesince06' : ('{{ a|timesince:b }}', {'a':now_tz - timedelta(hours=8), 'b':now_tz}, '8\xa0hours'),
# Regression for #7443 # Regression for #7443
'filter-timesince07': ('{{ earlier|timesince }}', { 'earlier': now - timedelta(days=7) }, '1 week'), 'filter-timesince07': ('{{ earlier|timesince }}', { 'earlier': now - timedelta(days=7) }, '1\xa0week'),
'filter-timesince08': ('{{ earlier|timesince:now }}', { 'now': now, 'earlier': now - timedelta(days=7) }, '1 week'), 'filter-timesince08': ('{{ earlier|timesince:now }}', { 'now': now, 'earlier': now - timedelta(days=7) }, '1\xa0week'),
'filter-timesince09': ('{{ later|timesince }}', { 'later': now + timedelta(days=7) }, '0 minutes'), 'filter-timesince09': ('{{ later|timesince }}', { 'later': now + timedelta(days=7) }, '0\xa0minutes'),
'filter-timesince10': ('{{ later|timesince:now }}', { 'now': now, 'later': now + timedelta(days=7) }, '0 minutes'), 'filter-timesince10': ('{{ later|timesince:now }}', { 'now': now, 'later': now + timedelta(days=7) }, '0\xa0minutes'),
# Ensures that differing timezones are calculated correctly # Ensures that differing timezones are calculated correctly
'filter-timesince11' : ('{{ a|timesince }}', {'a': now}, '0 minutes'), 'filter-timesince11' : ('{{ a|timesince }}', {'a': now}, '0\xa0minutes'),
'filter-timesince12' : ('{{ a|timesince }}', {'a': now_tz}, '0 minutes'), 'filter-timesince12' : ('{{ a|timesince }}', {'a': now_tz}, '0\xa0minutes'),
'filter-timesince13' : ('{{ a|timesince }}', {'a': now_tz_i}, '0 minutes'), 'filter-timesince13' : ('{{ a|timesince }}', {'a': now_tz_i}, '0\xa0minutes'),
'filter-timesince14' : ('{{ a|timesince:b }}', {'a': now_tz, 'b': now_tz_i}, '0 minutes'), 'filter-timesince14' : ('{{ a|timesince:b }}', {'a': now_tz, 'b': now_tz_i}, '0\xa0minutes'),
'filter-timesince15' : ('{{ a|timesince:b }}', {'a': now, 'b': now_tz_i}, ''), 'filter-timesince15' : ('{{ a|timesince:b }}', {'a': now, 'b': now_tz_i}, ''),
'filter-timesince16' : ('{{ a|timesince:b }}', {'a': now_tz_i, 'b': now}, ''), 'filter-timesince16' : ('{{ a|timesince:b }}', {'a': now_tz_i, 'b': now}, ''),
# Regression for #9065 (two date objects). # Regression for #9065 (two date objects).
'filter-timesince17' : ('{{ a|timesince:b }}', {'a': today, 'b': today}, '0 minutes'), 'filter-timesince17' : ('{{ a|timesince:b }}', {'a': today, 'b': today}, '0\xa0minutes'),
'filter-timesince18' : ('{{ a|timesince:b }}', {'a': today, 'b': today + timedelta(hours=24)}, '1 day'), 'filter-timesince18' : ('{{ a|timesince:b }}', {'a': today, 'b': today + timedelta(hours=24)}, '1\xa0day'),
# Default compare with datetime.now() # Default compare with datetime.now()
'filter-timeuntil01' : ('{{ a|timeuntil }}', {'a':datetime.now() + timedelta(minutes=2, seconds = 10)}, '2 minutes'), 'filter-timeuntil01' : ('{{ a|timeuntil }}', {'a':datetime.now() + timedelta(minutes=2, seconds = 10)}, '2\xa0minutes'),
'filter-timeuntil02' : ('{{ a|timeuntil }}', {'a':(datetime.now() + timedelta(days=1, seconds = 10))}, '1 day'), 'filter-timeuntil02' : ('{{ a|timeuntil }}', {'a':(datetime.now() + timedelta(days=1, seconds = 10))}, '1\xa0day'),
'filter-timeuntil03' : ('{{ a|timeuntil }}', {'a':(datetime.now() + timedelta(hours=8, minutes=10, seconds = 10))}, '8 hours, 10 minutes'), 'filter-timeuntil03' : ('{{ a|timeuntil }}', {'a':(datetime.now() + timedelta(hours=8, minutes=10, seconds = 10))}, '8\xa0hours, 10\xa0minutes'),
# Compare to a given parameter # Compare to a given parameter
'filter-timeuntil04' : ('{{ a|timeuntil:b }}', {'a':now - timedelta(days=1), 'b':now - timedelta(days=2)}, '1 day'), 'filter-timeuntil04' : ('{{ a|timeuntil:b }}', {'a':now - timedelta(days=1), 'b':now - timedelta(days=2)}, '1\xa0day'),
'filter-timeuntil05' : ('{{ a|timeuntil:b }}', {'a':now - timedelta(days=2), 'b':now - timedelta(days=2, minutes=1)}, '1 minute'), 'filter-timeuntil05' : ('{{ a|timeuntil:b }}', {'a':now - timedelta(days=2), 'b':now - timedelta(days=2, minutes=1)}, '1\xa0minute'),
# Regression for #7443 # Regression for #7443
'filter-timeuntil06': ('{{ earlier|timeuntil }}', { 'earlier': now - timedelta(days=7) }, '0 minutes'), 'filter-timeuntil06': ('{{ earlier|timeuntil }}', { 'earlier': now - timedelta(days=7) }, '0\xa0minutes'),
'filter-timeuntil07': ('{{ earlier|timeuntil:now }}', { 'now': now, 'earlier': now - timedelta(days=7) }, '0 minutes'), 'filter-timeuntil07': ('{{ earlier|timeuntil:now }}', { 'now': now, 'earlier': now - timedelta(days=7) }, '0\xa0minutes'),
'filter-timeuntil08': ('{{ later|timeuntil }}', { 'later': now + timedelta(days=7, hours=1) }, '1 week'), 'filter-timeuntil08': ('{{ later|timeuntil }}', { 'later': now + timedelta(days=7, hours=1) }, '1\xa0week'),
'filter-timeuntil09': ('{{ later|timeuntil:now }}', { 'now': now, 'later': now + timedelta(days=7) }, '1 week'), 'filter-timeuntil09': ('{{ later|timeuntil:now }}', { 'now': now, 'later': now + timedelta(days=7) }, '1\xa0week'),
# Ensures that differing timezones are calculated correctly # Ensures that differing timezones are calculated correctly
'filter-timeuntil10' : ('{{ a|timeuntil }}', {'a': now_tz_i}, '0 minutes'), 'filter-timeuntil10' : ('{{ a|timeuntil }}', {'a': now_tz_i}, '0\xa0minutes'),
'filter-timeuntil11' : ('{{ a|timeuntil:b }}', {'a': now_tz_i, 'b': now_tz}, '0 minutes'), 'filter-timeuntil11' : ('{{ a|timeuntil:b }}', {'a': now_tz_i, 'b': now_tz}, '0\xa0minutes'),
# Regression for #9065 (two date objects). # Regression for #9065 (two date objects).
'filter-timeuntil12' : ('{{ a|timeuntil:b }}', {'a': today, 'b': today}, '0 minutes'), 'filter-timeuntil12' : ('{{ a|timeuntil:b }}', {'a': today, 'b': today}, '0\xa0minutes'),
'filter-timeuntil13' : ('{{ a|timeuntil:b }}', {'a': today, 'b': today - timedelta(hours=24)}, '1 day'), 'filter-timeuntil13' : ('{{ a|timeuntil:b }}', {'a': today, 'b': today - timedelta(hours=24)}, '1\xa0day'),
'filter-addslash01': ("{% autoescape off %}{{ a|addslashes }} {{ b|addslashes }}{% endautoescape %}", {"a": "<a>'", "b": mark_safe("<a>'")}, r"<a>\' <a>\'"), 'filter-addslash01': ("{% autoescape off %}{{ a|addslashes }} {{ b|addslashes }}{% endautoescape %}", {"a": "<a>'", "b": mark_safe("<a>'")}, r"<a>\' <a>\'"),
'filter-addslash02': ("{{ a|addslashes }} {{ b|addslashes }}", {"a": "<a>'", "b": mark_safe("<a>'")}, r"&lt;a&gt;\&#39; <a>\'"), 'filter-addslash02': ("{{ a|addslashes }} {{ b|addslashes }}", {"a": "<a>'", "b": mark_safe("<a>'")}, r"&lt;a&gt;\&#39; <a>\'"),

View File

@ -21,32 +21,33 @@ class TimesinceTests(unittest.TestCase):
def test_equal_datetimes(self): def test_equal_datetimes(self):
""" equal datetimes. """ """ equal datetimes. """
self.assertEqual(timesince(self.t, self.t), '0 minutes') # NOTE: \xa0 avoids wrapping between value and unit
self.assertEqual(timesince(self.t, self.t), '0\xa0minutes')
def test_ignore_microseconds_and_seconds(self): def test_ignore_microseconds_and_seconds(self):
""" Microseconds and seconds are ignored. """ """ Microseconds and seconds are ignored. """
self.assertEqual(timesince(self.t, self.t+self.onemicrosecond), self.assertEqual(timesince(self.t, self.t+self.onemicrosecond),
'0 minutes') '0\xa0minutes')
self.assertEqual(timesince(self.t, self.t+self.onesecond), self.assertEqual(timesince(self.t, self.t+self.onesecond),
'0 minutes') '0\xa0minutes')
def test_other_units(self): def test_other_units(self):
""" Test other units. """ """ Test other units. """
self.assertEqual(timesince(self.t, self.t+self.oneminute), self.assertEqual(timesince(self.t, self.t+self.oneminute),
'1 minute') '1\xa0minute')
self.assertEqual(timesince(self.t, self.t+self.onehour), '1 hour') self.assertEqual(timesince(self.t, self.t+self.onehour), '1\xa0hour')
self.assertEqual(timesince(self.t, self.t+self.oneday), '1 day') self.assertEqual(timesince(self.t, self.t+self.oneday), '1\xa0day')
self.assertEqual(timesince(self.t, self.t+self.oneweek), '1 week') self.assertEqual(timesince(self.t, self.t+self.oneweek), '1\xa0week')
self.assertEqual(timesince(self.t, self.t+self.onemonth), self.assertEqual(timesince(self.t, self.t+self.onemonth),
'1 month') '1\xa0month')
self.assertEqual(timesince(self.t, self.t+self.oneyear), '1 year') self.assertEqual(timesince(self.t, self.t+self.oneyear), '1\xa0year')
def test_multiple_units(self): def test_multiple_units(self):
""" Test multiple units. """ """ Test multiple units. """
self.assertEqual(timesince(self.t, self.assertEqual(timesince(self.t,
self.t+2*self.oneday+6*self.onehour), '2 days, 6 hours') self.t+2*self.oneday+6*self.onehour), '2\xa0days, 6\xa0hours')
self.assertEqual(timesince(self.t, self.assertEqual(timesince(self.t,
self.t+2*self.oneweek+2*self.oneday), '2 weeks, 2 days') self.t+2*self.oneweek+2*self.oneday), '2\xa0weeks, 2\xa0days')
def test_display_first_unit(self): def test_display_first_unit(self):
""" """
@ -55,10 +56,10 @@ class TimesinceTests(unittest.TestCase):
""" """
self.assertEqual(timesince(self.t, self.assertEqual(timesince(self.t,
self.t+2*self.oneweek+3*self.onehour+4*self.oneminute), self.t+2*self.oneweek+3*self.onehour+4*self.oneminute),
'2 weeks') '2\xa0weeks')
self.assertEqual(timesince(self.t, self.assertEqual(timesince(self.t,
self.t+4*self.oneday+5*self.oneminute), '4 days') self.t+4*self.oneday+5*self.oneminute), '4\xa0days')
def test_display_second_before_first(self): def test_display_second_before_first(self):
""" """
@ -66,30 +67,30 @@ class TimesinceTests(unittest.TestCase):
get 0 minutes. get 0 minutes.
""" """
self.assertEqual(timesince(self.t, self.t-self.onemicrosecond), self.assertEqual(timesince(self.t, self.t-self.onemicrosecond),
'0 minutes') '0\xa0minutes')
self.assertEqual(timesince(self.t, self.t-self.onesecond), self.assertEqual(timesince(self.t, self.t-self.onesecond),
'0 minutes') '0\xa0minutes')
self.assertEqual(timesince(self.t, self.t-self.oneminute), self.assertEqual(timesince(self.t, self.t-self.oneminute),
'0 minutes') '0\xa0minutes')
self.assertEqual(timesince(self.t, self.t-self.onehour), self.assertEqual(timesince(self.t, self.t-self.onehour),
'0 minutes') '0\xa0minutes')
self.assertEqual(timesince(self.t, self.t-self.oneday), self.assertEqual(timesince(self.t, self.t-self.oneday),
'0 minutes') '0\xa0minutes')
self.assertEqual(timesince(self.t, self.t-self.oneweek), self.assertEqual(timesince(self.t, self.t-self.oneweek),
'0 minutes') '0\xa0minutes')
self.assertEqual(timesince(self.t, self.t-self.onemonth), self.assertEqual(timesince(self.t, self.t-self.onemonth),
'0 minutes') '0\xa0minutes')
self.assertEqual(timesince(self.t, self.t-self.oneyear), self.assertEqual(timesince(self.t, self.t-self.oneyear),
'0 minutes') '0\xa0minutes')
self.assertEqual(timesince(self.t, self.assertEqual(timesince(self.t,
self.t-2*self.oneday-6*self.onehour), '0 minutes') self.t-2*self.oneday-6*self.onehour), '0\xa0minutes')
self.assertEqual(timesince(self.t, self.assertEqual(timesince(self.t,
self.t-2*self.oneweek-2*self.oneday), '0 minutes') self.t-2*self.oneweek-2*self.oneday), '0\xa0minutes')
self.assertEqual(timesince(self.t, self.assertEqual(timesince(self.t,
self.t-2*self.oneweek-3*self.onehour-4*self.oneminute), self.t-2*self.oneweek-3*self.onehour-4*self.oneminute),
'0 minutes') '0\xa0minutes')
self.assertEqual(timesince(self.t, self.assertEqual(timesince(self.t,
self.t-4*self.oneday-5*self.oneminute), '0 minutes') self.t-4*self.oneday-5*self.oneminute), '0\xa0minutes')
def test_different_timezones(self): def test_different_timezones(self):
""" When using two different timezones. """ """ When using two different timezones. """
@ -97,28 +98,28 @@ class TimesinceTests(unittest.TestCase):
now_tz = datetime.datetime.now(LocalTimezone(now)) now_tz = datetime.datetime.now(LocalTimezone(now))
now_tz_i = datetime.datetime.now(FixedOffset((3 * 60) + 15)) now_tz_i = datetime.datetime.now(FixedOffset((3 * 60) + 15))
self.assertEqual(timesince(now), '0 minutes') self.assertEqual(timesince(now), '0\xa0minutes')
self.assertEqual(timesince(now_tz), '0 minutes') self.assertEqual(timesince(now_tz), '0\xa0minutes')
self.assertEqual(timeuntil(now_tz, now_tz_i), '0 minutes') self.assertEqual(timeuntil(now_tz, now_tz_i), '0\xa0minutes')
def test_date_objects(self): def test_date_objects(self):
""" Both timesince and timeuntil should work on date objects (#17937). """ """ Both timesince and timeuntil should work on date objects (#17937). """
today = datetime.date.today() today = datetime.date.today()
self.assertEqual(timesince(today + self.oneday), '0 minutes') self.assertEqual(timesince(today + self.oneday), '0\xa0minutes')
self.assertEqual(timeuntil(today - self.oneday), '0 minutes') self.assertEqual(timeuntil(today - self.oneday), '0\xa0minutes')
def test_both_date_objects(self): def test_both_date_objects(self):
""" Timesince should work with both date objects (#9672) """ """ Timesince should work with both date objects (#9672) """
today = datetime.date.today() today = datetime.date.today()
self.assertEqual(timeuntil(today + self.oneday, today), '1 day') self.assertEqual(timeuntil(today + self.oneday, today), '1\xa0day')
self.assertEqual(timeuntil(today - self.oneday, today), '0 minutes') self.assertEqual(timeuntil(today - self.oneday, today), '0\xa0minutes')
self.assertEqual(timeuntil(today + self.oneweek, today), '1 week') self.assertEqual(timeuntil(today + self.oneweek, today), '1\xa0week')
def test_naive_datetime_with_tzinfo_attribute(self): def test_naive_datetime_with_tzinfo_attribute(self):
class naive(datetime.tzinfo): class naive(datetime.tzinfo):
def utcoffset(self, dt): def utcoffset(self, dt):
return None return None
future = datetime.datetime(2080, 1, 1, tzinfo=naive()) future = datetime.datetime(2080, 1, 1, tzinfo=naive())
self.assertEqual(timesince(future), '0 minutes') self.assertEqual(timesince(future), '0\xa0minutes')
past = datetime.datetime(1980, 1, 1, tzinfo=naive()) past = datetime.datetime(1980, 1, 1, tzinfo=naive())
self.assertEqual(timeuntil(past), '0 minutes') self.assertEqual(timeuntil(past), '0\xa0minutes')