Fixed #29654 -- Made text truncation an ellipsis character instead of three dots.

Thanks Sudhanshu Mishra for the initial patch and Tim Graham for the review.
This commit is contained in:
Claude Paroz 2018-08-21 15:28:51 +02:00
parent 939dcff24f
commit 201017df30
14 changed files with 65 additions and 60 deletions

View File

@ -193,7 +193,7 @@ class ForeignKeyRawIdWidget(forms.TextInput):
except NoReverseMatch:
url = '' # Admin not registered for target model.
return Truncator(obj).words(14, truncate='...'), url
return Truncator(obj).words(14), url
class ManyToManyRawIdWidget(ForeignKeyRawIdWidget):

View File

@ -280,7 +280,7 @@ def truncatewords(value, arg):
length = int(arg)
except ValueError: # Invalid literal for int().
return value # Fail silently.
return Truncator(value).words(length, truncate=' ...')
return Truncator(value).words(length, truncate=' ')
@register.filter(is_safe=True)
@ -294,7 +294,7 @@ def truncatewords_html(value, arg):
length = int(arg)
except ValueError: # invalid literal for int()
return value # Fail silently.
return Truncator(value).words(length, html=True, truncate=' ...')
return Truncator(value).words(length, html=True, truncate=' ')
@register.filter(is_safe=False)

View File

@ -245,7 +245,7 @@ def urlize(text, trim_url_limit=None, nofollow=False, autoescape=False):
leading punctuation (opening parens) and it'll still do the right thing.
If trim_url_limit is not None, truncate the URLs in the link text longer
than this limit to trim_url_limit-3 characters and append an ellipsis.
than this limit to trim_url_limit - 1 characters and append an ellipsis.
If nofollow is True, give the links a rel="nofollow" attribute.
@ -256,7 +256,7 @@ def urlize(text, trim_url_limit=None, nofollow=False, autoescape=False):
def trim_url(x, limit=trim_url_limit):
if limit is None or len(x) <= limit:
return x
return '%s...' % x[:max(0, limit - 3)]
return '%s' % x[:max(0, limit - 1)]
def unescape(text, trail):
"""

View File

@ -64,7 +64,7 @@ class Truncator(SimpleLazyObject):
if truncate is None:
truncate = pgettext(
'String to return when truncating text',
'%(truncated_text)s...')
'%(truncated_text)s')
if '%(truncated_text)s' in truncate:
return truncate % {'truncated_text': text}
# The truncation text didn't contain the %(truncated_text)s string
@ -81,8 +81,7 @@ class Truncator(SimpleLazyObject):
of characters.
`truncate` specifies what should be used to notify that the string has
been truncated, defaulting to a translatable string of an ellipsis
(...).
been truncated, defaulting to a translatable string of an ellipsis.
"""
self._setup()
length = int(num)
@ -123,7 +122,7 @@ class Truncator(SimpleLazyObject):
"""
Truncate a string after a certain number of words. `truncate` specifies
what should be used to notify that the string has been truncated,
defaulting to ellipsis (...).
defaulting to ellipsis.
"""
self._setup()
length = int(num)

View File

@ -2265,15 +2265,15 @@ If ``value`` is ``"my FIRST post"``, the output will be ``"My First Post"``.
-----------------
Truncates a string if it is longer than the specified number of characters.
Truncated strings will end with a translatable ellipsis sequence ("...").
Truncated strings will end with a translatable ellipsis character ("…").
**Argument:** Number of characters to truncate to
For example::
{{ value|truncatechars:9 }}
{{ value|truncatechars:7 }}
If ``value`` is ``"Joel is a slug"``, the output will be ``"Joel i..."``.
If ``value`` is ``"Joel is a slug"``, the output will be ``"Joel i"``.
.. templatefilter:: truncatechars_html
@ -2286,10 +2286,10 @@ are closed immediately after the truncation.
For example::
{{ value|truncatechars_html:9 }}
{{ value|truncatechars_html:7 }}
If ``value`` is ``"<p>Joel is a slug</p>"``, the output will be
``"<p>Joel i...</p>"``.
``"<p>Joel i</p>"``.
Newlines in the HTML content will be preserved.
@ -2306,7 +2306,7 @@ For example::
{{ value|truncatewords:2 }}
If ``value`` is ``"Joel is a slug"``, the output will be ``"Joel is ..."``.
If ``value`` is ``"Joel is a slug"``, the output will be ``"Joel is "``.
Newlines within the string will be removed.
@ -2327,7 +2327,7 @@ For example::
{{ value|truncatewords_html:2 }}
If ``value`` is ``"<p>Joel is a slug</p>"``, the output will be
``"<p>Joel is ...</p>"``.
``"<p>Joel is </p>"``.
Newlines in the HTML content will be preserved.
@ -2454,7 +2454,7 @@ For example::
If ``value`` is ``"Check out www.djangoproject.com"``, the output would be
``'Check out <a href="http://www.djangoproject.com"
rel="nofollow">www.djangopr...</a>'``.
rel="nofollow">www.djangoproj…</a>'``.
As with urlize_, this filter should only be applied to plain text.

View File

@ -273,6 +273,12 @@ Miscellaneous
* The return value of :func:`django.utils.text.slugify` is no longer marked as
HTML safe.
* The default truncation character used by the :tfilter:`urlizetrunc`,
:tfilter:`truncatechars`, :tfilter:`truncatechars_html`,
:tfilter:`truncatewords`, and :tfilter:`truncatewords_html` template filters
is now the real ellipsis character (``…``) instead of 3 dots. You may have to
adapt some test output comparisons.
.. _deprecated-features-2.2:
Features deprecated in 2.2

View File

@ -346,7 +346,7 @@ class MigrateTests(MigrationTestBase):
self.assertEqual(
'Planned operations:\n'
'migrations.0004_fourth\n'
' Raw SQL operation -> SELECT * FROM migrations_author W...\n',
' Raw SQL operation -> SELECT * FROM migrations_author WHE…\n',
out.getvalue()
)
# Migrate to the fourth migration.

View File

@ -5,10 +5,10 @@ from ..utils import setup
class TruncatecharsTests(SimpleTestCase):
@setup({'truncatechars01': '{{ a|truncatechars:5 }}'})
@setup({'truncatechars01': '{{ a|truncatechars:3 }}'})
def test_truncatechars01(self):
output = self.engine.render_to_string('truncatechars01', {'a': 'Testing, testing'})
self.assertEqual(output, 'Te...')
self.assertEqual(output, 'Te')
@setup({'truncatechars02': '{{ a|truncatechars:7 }}'})
def test_truncatechars02(self):

View File

@ -5,18 +5,18 @@ from django.test import SimpleTestCase
class FunctionTests(SimpleTestCase):
def test_truncate_zero(self):
self.assertEqual(truncatechars_html('<p>one <a href="#">two - three <br>four</a> five</p>', 0), '...')
self.assertEqual(truncatechars_html('<p>one <a href="#">two - three <br>four</a> five</p>', 0), '')
def test_truncate(self):
self.assertEqual(
truncatechars_html('<p>one <a href="#">two - three <br>four</a> five</p>', 6),
'<p>one...</p>',
truncatechars_html('<p>one <a href="#">two - three <br>four</a> five</p>', 4),
'<p>one</p>',
)
def test_truncate2(self):
self.assertEqual(
truncatechars_html('<p>one <a href="#">two - three <br>four</a> five</p>', 11),
'<p>one <a href="#">two ...</a></p>',
truncatechars_html('<p>one <a href="#">two - three <br>four</a> five</p>', 9),
'<p>one <a href="#">two </a></p>',
)
def test_truncate3(self):
@ -26,7 +26,7 @@ class FunctionTests(SimpleTestCase):
)
def test_truncate_unicode(self):
self.assertEqual(truncatechars_html('<b>\xc5ngstr\xf6m</b> was here', 5), '<b>\xc5n...</b>')
self.assertEqual(truncatechars_html('<b>\xc5ngstr\xf6m</b> was here', 3), '<b>\xc5n…</b>')
def test_truncate_something(self):
self.assertEqual(truncatechars_html('a<b>b</b>c', 3), 'a<b>b</b>c')

View File

@ -14,25 +14,25 @@ class TruncatewordsTests(SimpleTestCase):
output = self.engine.render_to_string(
'truncatewords01', {'a': 'alpha & bravo', 'b': mark_safe('alpha &amp; bravo')}
)
self.assertEqual(output, 'alpha & ... alpha &amp; ...')
self.assertEqual(output, 'alpha & … alpha &amp; …')
@setup({'truncatewords02': '{{ a|truncatewords:"2" }} {{ b|truncatewords:"2"}}'})
def test_truncatewords02(self):
output = self.engine.render_to_string(
'truncatewords02', {'a': 'alpha & bravo', 'b': mark_safe('alpha &amp; bravo')}
)
self.assertEqual(output, 'alpha &amp; ... alpha &amp; ...')
self.assertEqual(output, 'alpha &amp; … alpha &amp; …')
class FunctionTests(SimpleTestCase):
def test_truncate(self):
self.assertEqual(truncatewords('A sentence with a few words in it', 1), 'A ...')
self.assertEqual(truncatewords('A sentence with a few words in it', 1), 'A ')
def test_truncate2(self):
self.assertEqual(
truncatewords('A sentence with a few words in it', 5),
'A sentence with a few ...',
'A sentence with a few ',
)
def test_overtruncate(self):

View File

@ -10,13 +10,13 @@ class FunctionTests(SimpleTestCase):
def test_truncate(self):
self.assertEqual(
truncatewords_html('<p>one <a href="#">two - three <br>four</a> five</p>', 2),
'<p>one <a href="#">two ...</a></p>',
'<p>one <a href="#">two </a></p>',
)
def test_truncate2(self):
self.assertEqual(
truncatewords_html('<p>one <a href="#">two - three <br>four</a> five</p>', 4),
'<p>one <a href="#">two - three <br>four ...</a></p>',
'<p>one <a href="#">two - three <br>four </a></p>',
)
def test_truncate3(self):
@ -32,12 +32,12 @@ class FunctionTests(SimpleTestCase):
)
def test_truncate_unicode(self):
self.assertEqual(truncatewords_html('\xc5ngstr\xf6m was here', 1), '\xc5ngstr\xf6m ...')
self.assertEqual(truncatewords_html('\xc5ngstr\xf6m was here', 1), '\xc5ngstr\xf6m ')
def test_truncate_complex(self):
self.assertEqual(
truncatewords_html('<i>Buenos d&iacute;as! &#x00bf;C&oacute;mo est&aacute;?</i>', 3),
'<i>Buenos d&iacute;as! &#x00bf;C&oacute;mo ...</i>',
'<i>Buenos d&iacute;as! &#x00bf;C&oacute;mo </i>',
)
def test_invalid_arg(self):

View File

@ -20,8 +20,8 @@ class UrlizetruncTests(SimpleTestCase):
)
self.assertEqual(
output,
'"Unsafe" <a href="http://example.com/x=&amp;y=" rel="nofollow">http:...</a> '
'&quot;Safe&quot; <a href="http://example.com?x=&amp;y=" rel="nofollow">http:...</a>'
'"Unsafe" <a href="http://example.com/x=&amp;y=" rel="nofollow">http://…</a> '
'&quot;Safe&quot; <a href="http://example.com?x=&amp;y=" rel="nofollow">http://…</a>'
)
@setup({'urlizetrunc02': '{{ a|urlizetrunc:"8" }} {{ b|urlizetrunc:"8" }}'})
@ -35,8 +35,8 @@ class UrlizetruncTests(SimpleTestCase):
)
self.assertEqual(
output,
'&quot;Unsafe&quot; <a href="http://example.com/x=&amp;y=" rel="nofollow">http:...</a> '
'&quot;Safe&quot; <a href="http://example.com?x=&amp;y=" rel="nofollow">http:...</a>'
'&quot;Unsafe&quot; <a href="http://example.com/x=&amp;y=" rel="nofollow">http://…</a> '
'&quot;Safe&quot; <a href="http://example.com?x=&amp;y=" rel="nofollow">http://…</a>'
)
@ -55,13 +55,13 @@ class FunctionTests(SimpleTestCase):
self.assertEqual(
urlizetrunc(uri, 30),
'<a href="http://31characteruri.com/test/" rel="nofollow">'
'http://31characteruri.com/t...</a>',
'http://31characteruri.com/tes…</a>',
)
self.assertEqual(
urlizetrunc(uri, 2),
urlizetrunc(uri, 1),
'<a href="http://31characteruri.com/test/"'
' rel="nofollow">...</a>',
' rel="nofollow"></a>',
)
def test_overtruncate(self):
@ -74,7 +74,7 @@ class FunctionTests(SimpleTestCase):
self.assertEqual(
urlizetrunc('http://www.google.co.uk/search?hl=en&q=some+long+url&btnG=Search&meta=', 20),
'<a href="http://www.google.co.uk/search?hl=en&amp;q=some+long+url&amp;btnG=Search&amp;'
'meta=" rel="nofollow">http://www.google...</a>',
'meta=" rel="nofollow">http://www.google.c…</a>',
)
def test_non_string_input(self):
@ -89,5 +89,5 @@ class FunctionTests(SimpleTestCase):
def test_autoescape_off(self):
self.assertEqual(
urlizetrunc('foo<a href=" google.com ">bar</a>buz', 9, autoescape=False),
'foo<a href=" <a href="http://google.com" rel="nofollow">google...</a> ">bar</a>buz',
'foo<a href=" <a href="http://google.com" rel="nofollow">google.c…</a> ">bar</a>buz',
)

View File

@ -168,7 +168,7 @@ class FilterSyntaxTests(SimpleTestCase):
Numbers as filter arguments should work
"""
output = self.engine.render_to_string('filter-syntax19', {"var": "hello world"})
self.assertEqual(output, "hello ...")
self.assertEqual(output, "hello ")
@setup({'filter-syntax20': '{{ ""|default_if_none:"was none" }}'})
def test_filter_syntax20(self):

View File

@ -56,22 +56,22 @@ class TestUtilsText(SimpleTestCase):
def test_truncate_chars(self):
truncator = text.Truncator('The quick brown fox jumped over the lazy dog.')
self.assertEqual('The quick brown fox jumped over the lazy dog.', truncator.chars(100)),
self.assertEqual('The quick brown fox ...', truncator.chars(23)),
self.assertEqual('The quick brown fox ', truncator.chars(21)),
self.assertEqual('The quick brown fo.....', truncator.chars(23, '.....')),
nfc = text.Truncator('o\xfco\xfco\xfco\xfc')
nfd = text.Truncator('ou\u0308ou\u0308ou\u0308ou\u0308')
self.assertEqual('oüoüoüoü', nfc.chars(8))
self.assertEqual('oüoüoüoü', nfd.chars(8))
self.assertEqual('...', nfc.chars(5))
self.assertEqual('...', nfd.chars(5))
self.assertEqual('', nfc.chars(3))
self.assertEqual('', nfd.chars(3))
# Ensure the final length is calculated correctly when there are
# combining characters with no precomposed form, and that combining
# characters are not split up.
truncator = text.Truncator('-B\u030AB\u030A----8')
self.assertEqual('-B\u030A...', truncator.chars(5))
self.assertEqual('-B\u030AB\u030A-...', truncator.chars(7))
self.assertEqual('-B\u030A', truncator.chars(3))
self.assertEqual('-B\u030AB\u030A-', truncator.chars(5))
self.assertEqual('-B\u030AB\u030A----8', truncator.chars(8))
# Ensure the length of the end text is correctly calculated when it
@ -82,18 +82,18 @@ class TestUtilsText(SimpleTestCase):
# Make a best effort to shorten to the desired length, but requesting
# a length shorter than the ellipsis shouldn't break
self.assertEqual('...', text.Truncator('asdf').chars(1))
self.assertEqual('', text.Truncator('asdf').chars(0))
# lazy strings are handled correctly
self.assertEqual(text.Truncator(lazystr('The quick brown fox')).chars(12), 'The quick...')
self.assertEqual(text.Truncator(lazystr('The quick brown fox')).chars(10), 'The quick…')
def test_truncate_words(self):
truncator = text.Truncator('The quick brown fox jumped over the lazy dog.')
self.assertEqual('The quick brown fox jumped over the lazy dog.', truncator.words(10))
self.assertEqual('The quick brown fox...', truncator.words(4))
self.assertEqual('The quick brown fox', truncator.words(4))
self.assertEqual('The quick brown fox[snip]', truncator.words(4, '[snip]'))
# lazy strings are handled correctly
truncator = text.Truncator(lazystr('The quick brown fox jumped over the lazy dog.'))
self.assertEqual('The quick brown fox...', truncator.words(4))
self.assertEqual('The quick brown fox', truncator.words(4))
def test_truncate_html_words(self):
truncator = text.Truncator(
@ -104,7 +104,7 @@ class TestUtilsText(SimpleTestCase):
truncator.words(10, html=True)
)
self.assertEqual(
'<p id="par"><strong><em>The quick brown fox...</em></strong></p>',
'<p id="par"><strong><em>The quick brown fox</em></strong></p>',
truncator.words(4, html=True)
)
self.assertEqual(
@ -121,21 +121,21 @@ class TestUtilsText(SimpleTestCase):
'<p>The quick <a href="xyz.html"\n id="mylink">brown fox</a> jumped over the lazy dog.</p>'
)
self.assertEqual(
'<p>The quick <a href="xyz.html"\n id="mylink">brown...</a></p>',
truncator.words(3, '...', html=True)
'<p>The quick <a href="xyz.html"\n id="mylink">brown</a></p>',
truncator.words(3, html=True)
)
# Test self-closing tags
truncator = text.Truncator('<br/>The <hr />quick brown fox jumped over the lazy dog.')
self.assertEqual('<br/>The <hr />quick brown...', truncator.words(3, '...', html=True))
self.assertEqual('<br/>The <hr />quick brown', truncator.words(3, html=True))
truncator = text.Truncator('<br>The <hr/>quick <em>brown fox</em> jumped over the lazy dog.')
self.assertEqual('<br>The <hr/>quick <em>brown...</em>', truncator.words(3, '...', html=True))
self.assertEqual('<br>The <hr/>quick <em>brown</em>', truncator.words(3, html=True))
# Test html entities
truncator = text.Truncator('<i>Buenos d&iacute;as! &#x00bf;C&oacute;mo est&aacute;?</i>')
self.assertEqual('<i>Buenos d&iacute;as! &#x00bf;C&oacute;mo...</i>', truncator.words(3, '...', html=True))
self.assertEqual('<i>Buenos d&iacute;as! &#x00bf;C&oacute;mo</i>', truncator.words(3, html=True))
truncator = text.Truncator('<p>I &lt;3 python, what about you?</p>')
self.assertEqual('<p>I &lt;3 python...</p>', truncator.words(3, '...', html=True))
self.assertEqual('<p>I &lt;3 python</p>', truncator.words(3, html=True))
re_tag_catastrophic_test = ('</a' + '\t' * 50000) + '//>'
truncator = text.Truncator(re_tag_catastrophic_test)