From 72f6de14370bae13365e53efb6bed98df53a1ca3 Mon Sep 17 00:00:00 2001 From: Jan Pieter Waagmeester Date: Mon, 12 Mar 2018 19:57:18 +0100 Subject: [PATCH] Only ever apply capitalisation to the first character of any verbose name, or do nothing to it. fixes #491 Doing it this way conforms to the django recommendations: https://docs.djangoproject.com/en/stable/topics/db/models/#verbose-field-names > The convention is not to capitalize the first letter of the verbose_name. > Django will automatically capitalize the first letter where it needs to. --- django_tables2/columns/base.py | 9 ++-- django_tables2/columns/booleancolumn.py | 7 ++- django_tables2/columns/datecolumn.py | 4 +- django_tables2/columns/datetimecolumn.py | 4 +- django_tables2/columns/emailcolumn.py | 4 +- django_tables2/columns/filecolumn.py | 5 +- django_tables2/columns/jsoncolumn.py | 5 +- django_tables2/columns/manytomanycolumn.py | 4 +- django_tables2/columns/timecolumn.py | 4 +- django_tables2/columns/urlcolumn.py | 4 +- django_tables2/templatetags/django_tables2.py | 30 ------------ django_tables2/utils.py | 11 +++++ tests/test_export.py | 38 +++++++-------- tests/test_models.py | 47 +++++++++---------- 14 files changed, 76 insertions(+), 100 deletions(-) diff --git a/django_tables2/columns/base.py b/django_tables2/columns/base.py index d07403d6..e922275f 100644 --- a/django_tables2/columns/base.py +++ b/django_tables2/columns/base.py @@ -7,8 +7,8 @@ from django.utils import six from django.utils.safestring import SafeData -from django_tables2.templatetags.django_tables2 import title -from django_tables2.utils import Accessor, AttributeDict, OrderBy, OrderByTuple, call_with_appropriate, computed_values +from django_tables2.utils import (Accessor, AttributeDict, OrderBy, OrderByTuple, call_with_appropriate, + computed_values, ucfirst) class Library(object): @@ -256,7 +256,8 @@ def from_field(cls, field): verbose_name = field.get_related_field().verbose_name else: verbose_name = getattr(field, 'verbose_name', field.name) - return cls(verbose_name=title(verbose_name)) + + return cls(verbose_name=ucfirst(verbose_name)) @six.python_2_unicode_compatible @@ -537,7 +538,7 @@ def verbose_name(self): if isinstance(name, SafeData): return name - return title(name) + return ucfirst(name) @property def visible(self): diff --git a/django_tables2/columns/booleancolumn.py b/django_tables2/columns/booleancolumn.py index b1d92d37..3ffd2327 100644 --- a/django_tables2/columns/booleancolumn.py +++ b/django_tables2/columns/booleancolumn.py @@ -5,8 +5,7 @@ from django.utils import six from django.utils.html import escape, format_html -from django_tables2.templatetags.django_tables2 import title -from django_tables2.utils import AttributeDict +from django_tables2.utils import AttributeDict, ucfirst from .base import Column, library @@ -70,8 +69,8 @@ def value(self, record, value, bound_column): @classmethod def from_field(cls, field): if isinstance(field, models.NullBooleanField): - return cls(verbose_name=title(field.verbose_name), null=True) + return cls(verbose_name=ucfirst(field.verbose_name), null=True) if isinstance(field, models.BooleanField): null = getattr(field, 'null', False) - return cls(verbose_name=title(field.verbose_name), null=null) + return cls(verbose_name=ucfirst(field.verbose_name), null=null) diff --git a/django_tables2/columns/datecolumn.py b/django_tables2/columns/datecolumn.py index c454b8df..8174d929 100644 --- a/django_tables2/columns/datecolumn.py +++ b/django_tables2/columns/datecolumn.py @@ -3,7 +3,7 @@ from django.db import models -from django_tables2.templatetags.django_tables2 import title +from django_tables2.utils import ucfirst from .base import library from .templatecolumn import TemplateColumn @@ -29,4 +29,4 @@ def __init__(self, format=None, short=True, *args, **kwargs): @classmethod def from_field(cls, field): if isinstance(field, models.DateField): - return cls(verbose_name=title(field.verbose_name)) + return cls(verbose_name=ucfirst(field.verbose_name)) diff --git a/django_tables2/columns/datetimecolumn.py b/django_tables2/columns/datetimecolumn.py index c9ed3b5f..2088dbb7 100644 --- a/django_tables2/columns/datetimecolumn.py +++ b/django_tables2/columns/datetimecolumn.py @@ -3,7 +3,7 @@ from django.db import models -from django_tables2.templatetags.django_tables2 import title +from django_tables2.utils import ucfirst from .base import library from .templatecolumn import TemplateColumn @@ -29,4 +29,4 @@ def __init__(self, format=None, short=True, *args, **kwargs): @classmethod def from_field(cls, field): if isinstance(field, models.DateTimeField): - return cls(verbose_name=title(field.verbose_name)) + return cls(verbose_name=ucfirst(field.verbose_name)) diff --git a/django_tables2/columns/emailcolumn.py b/django_tables2/columns/emailcolumn.py index fa457129..06cafcf8 100644 --- a/django_tables2/columns/emailcolumn.py +++ b/django_tables2/columns/emailcolumn.py @@ -3,7 +3,7 @@ from django.db import models -from django_tables2.templatetags.django_tables2 import title +from django_tables2.utils import ucfirst from .base import library from .linkcolumn import BaseLinkColumn @@ -45,4 +45,4 @@ def render(self, record, value): @classmethod def from_field(cls, field): if isinstance(field, models.EmailField): - return cls(verbose_name=title(field.verbose_name)) + return cls(verbose_name=ucfirst(field.verbose_name)) diff --git a/django_tables2/columns/filecolumn.py b/django_tables2/columns/filecolumn.py index 18e2c098..38831cd6 100644 --- a/django_tables2/columns/filecolumn.py +++ b/django_tables2/columns/filecolumn.py @@ -6,8 +6,7 @@ from django.db import models from django.utils.html import format_html -from django_tables2.templatetags.django_tables2 import title -from django_tables2.utils import AttributeDict +from django_tables2.utils import AttributeDict, ucfirst from .base import library from .linkcolumn import BaseLinkColumn @@ -85,4 +84,4 @@ def render(self, record, value): @classmethod def from_field(cls, field): if isinstance(field, models.FileField): - return cls(verbose_name=title(field.verbose_name)) + return cls(verbose_name=ucfirst(field.verbose_name)) diff --git a/django_tables2/columns/jsoncolumn.py b/django_tables2/columns/jsoncolumn.py index 1628e6b1..2c8faebd 100644 --- a/django_tables2/columns/jsoncolumn.py +++ b/django_tables2/columns/jsoncolumn.py @@ -5,8 +5,7 @@ from django.utils.html import format_html -from django_tables2.templatetags.django_tables2 import title -from django_tables2.utils import AttributeDict +from django_tables2.utils import AttributeDict, ucfirst from .base import library from .linkcolumn import BaseLinkColumn @@ -58,4 +57,4 @@ def render(self, record, value): def from_field(cls, field): if POSTGRES_AVAILABLE: if isinstance(field, JSONField) or isinstance(field, HStoreField): - return cls(verbose_name=title(field.verbose_name)) + return cls(verbose_name=ucfirst(field.verbose_name)) diff --git a/django_tables2/columns/manytomanycolumn.py b/django_tables2/columns/manytomanycolumn.py index de0ef509..24d5c7eb 100644 --- a/django_tables2/columns/manytomanycolumn.py +++ b/django_tables2/columns/manytomanycolumn.py @@ -5,7 +5,7 @@ from django.utils.encoding import force_text from django.utils.html import conditional_escape, mark_safe -from django_tables2.templatetags.django_tables2 import title +from django_tables2.utils import ucfirst from .base import Column, library @@ -82,4 +82,4 @@ def render(self, value): @classmethod def from_field(cls, field): if isinstance(field, models.ManyToManyField): - return cls(verbose_name=title(field.verbose_name)) + return cls(verbose_name=ucfirst(field.verbose_name)) diff --git a/django_tables2/columns/timecolumn.py b/django_tables2/columns/timecolumn.py index db1cbcbb..ea1cf3f5 100644 --- a/django_tables2/columns/timecolumn.py +++ b/django_tables2/columns/timecolumn.py @@ -4,7 +4,7 @@ from django.conf import settings from django.db import models -from django_tables2.templatetags.django_tables2 import title +from django_tables2.utils import ucfirst from .base import library from .templatecolumn import TemplateColumn @@ -30,4 +30,4 @@ def __init__(self, format=None, *args, **kwargs): @classmethod def from_field(cls, field): if isinstance(field, models.TimeField): - return cls(verbose_name=title(field.verbose_name)) + return cls(verbose_name=ucfirst(field.verbose_name)) diff --git a/django_tables2/columns/urlcolumn.py b/django_tables2/columns/urlcolumn.py index 7849a820..937b6314 100644 --- a/django_tables2/columns/urlcolumn.py +++ b/django_tables2/columns/urlcolumn.py @@ -3,7 +3,7 @@ from django.db import models -from django_tables2.templatetags.django_tables2 import title +from django_tables2.utils import ucfirst from .base import library from .linkcolumn import BaseLinkColumn @@ -34,4 +34,4 @@ def render(self, record, value): @classmethod def from_field(cls, field): if isinstance(field, models.URLField): - return cls(verbose_name=title(field.verbose_name)) + return cls(verbose_name=ucfirst(field.verbose_name)) diff --git a/django_tables2/templatetags/django_tables2.py b/django_tables2/templatetags/django_tables2.py index ceb6f3fb..e443ce1a 100644 --- a/django_tables2/templatetags/django_tables2.py +++ b/django_tables2/templatetags/django_tables2.py @@ -8,8 +8,6 @@ from django.conf import settings from django.core.exceptions import ImproperlyConfigured from django.template import Node, TemplateSyntaxError -from django.template.defaultfilters import title as old_title -from django.template.defaultfilters import stringfilter from django.template.loader import get_template, select_template from django.templatetags.l10n import register as l10n_register from django.utils import six @@ -211,34 +209,6 @@ class Meta: return RenderTableNode(table, template) -@register.filter -@stringfilter -def title(value): - ''' - A slightly better title template filter. - - Same as Django's builtin `~django.template.defaultfilters.title` filter, - but operates on individual words and leaves words unchanged if they already - have a capital letter or a digit. Actually Django's filter also skips - words with digits but only for latin letters (or at least not for - cyrillic ones). - ''' - return ' '.join([ - any([c.isupper() or c.isdigit() for c in w]) and w or old_title(w) - for w in value.split() - ]) - - -title.is_safe = True - -try: - from django.utils.functional import keep_lazy_text - title = keep_lazy_text(title) -except ImportError: - # to keep backward (Django < 1.10) compatibility - from django.utils.functional import lazy - title = lazy(title, six.text_type) - register.filter('localize', l10n_register.filters['localize']) register.filter('unlocalize', l10n_register.filters['unlocalize']) diff --git a/django_tables2/utils.py b/django_tables2/utils.py index 947b3555..3383d897 100644 --- a/django_tables2/utils.py +++ b/django_tables2/utils.py @@ -7,10 +7,21 @@ from django.db import models from django.db.models.fields import FieldDoesNotExist +from django.template.defaultfilters import stringfilter from django.utils import six +from django.utils.functional import keep_lazy_text from django.utils.html import format_html_join +@keep_lazy_text +@stringfilter +def ucfirst(s): + if not isinstance(s, six.string_types): + return '' + else: + return s[0].upper() + s[1:] + + class Sequence(list): ''' Represents a column sequence, e.g. ``('first_name', '...', 'last_name')`` diff --git a/tests/test_export.py b/tests/test_export.py index 3eba8026..ad8ff04b 100644 --- a/tests/test_export.py +++ b/tests/test_export.py @@ -31,11 +31,11 @@ CSV_SEP = '\r\n' EXPECTED_CSV = CSV_SEP.join( - ('First Name,Surname', ) + tuple(','.join(name) for name in NAMES) + ('First name,Surname', ) + tuple(','.join(name) for name in NAMES) ) + CSV_SEP EXPECTED_JSON = list([ - {'First Name': first_name, 'Surname': last_name} + {'First name': first_name, 'Surname': last_name} for first_name, last_name in NAMES ]) @@ -65,7 +65,7 @@ def test_None_values(self): exporter = TableExport('csv', table) expected = ( - 'First Name,Last Name', + 'First name,Last name', 'Yildiz,van der Kuil', 'Jan,' ) @@ -76,13 +76,13 @@ def test_null_values(self): class Table(tables.Table): first_name = tables.Column() - last_name = tables.Column(verbose_name='Last Name') + last_name = tables.Column(verbose_name='Last name') occupation = tables.Column(verbose_name='Occupation') table = Table(Person.objects.all()) exporter = TableExport('csv', table) expected = ( - 'First Name,Last Name,Occupation', + 'First name,Last name,Occupation', 'Jan,Coen,' ) self.assertEqual(exporter.export(), CSV_SEP.join(expected) + CSV_SEP) @@ -96,14 +96,14 @@ def setUp(self): def test_view_should_support_csv_export(self): response, view = View.as_view()(build_request('/?_export=csv')) - assert response.getvalue().decode('utf8') == EXPECTED_CSV + self.assertEqual(response.getvalue().decode('utf8'), EXPECTED_CSV) # should just render the normal table without the _export query response, view = View.as_view()(build_request('/')) html = response.render().rendered_content - assert 'Yildiz' in html - assert 'Lindy' not in html + self.assertIn('Yildiz', html) + self.assertNotIn('Lindy', html) def test_should_raise_error_for_unsupported_file_type(self): table = Table([]) @@ -113,7 +113,7 @@ def test_should_raise_error_for_unsupported_file_type(self): def test_should_support_json_export(self): response, view = View.as_view()(build_request('/?_export=json')) - assert json.loads(response.getvalue().decode('utf8')) == EXPECTED_JSON + self.assertEqual(json.loads(response.getvalue().decode('utf8')), EXPECTED_JSON) def test_should_support_custom_trigger_param(self): class View(DispatchHookMixin, ExportMixin, tables.SingleTableView): @@ -122,7 +122,7 @@ class View(DispatchHookMixin, ExportMixin, tables.SingleTableView): model = Person # required for ListView response, view = View.as_view()(build_request('/?export_to=json')) - assert json.loads(response.getvalue().decode('utf8')) == EXPECTED_JSON + self.assertEqual(json.loads(response.getvalue().decode('utf8')), EXPECTED_JSON) def test_should_support_custom_filename(self): class View(DispatchHookMixin, ExportMixin, tables.SingleTableView): @@ -131,7 +131,7 @@ class View(DispatchHookMixin, ExportMixin, tables.SingleTableView): model = Person # required for ListView response, view = View.as_view()(build_request('/?_export=json')) - assert response['Content-Disposition'] == 'attachment; filename="people.json"' + self.assertEqual(response['Content-Disposition'], 'attachment; filename="people.json"') def test_function_view(self): ''' @@ -151,14 +151,14 @@ def table_view(request): }) response = table_view(build_request('/?_export=csv')) - assert response.getvalue().decode('utf8') == EXPECTED_CSV + self.assertEqual(response.getvalue().decode('utf8'), EXPECTED_CSV) # must also support the normal html table. response = table_view(build_request('/')) html = response.content.decode('utf8') - assert 'Yildiz' in html - assert 'Lindy' not in html + self.assertIn('Yildiz', html) + self.assertNotIn('Lindy', html) class OccupationTable(tables.Table): @@ -187,9 +187,9 @@ def test_should_work_with_foreign_keys(self): response, view = OccupationView.as_view()(build_request('/?_export=xls')) data = response.content # binary data, so not possible to compare to an exact expectation - assert data.find('Vlaanderen'.encode()) - assert data.find('Ecoloog'.encode()) - assert data.find('Timmerman'.encode()) + self.assertTrue(data.find('Vlaanderen'.encode())) + self.assertTrue(data.find('Ecoloog'.encode())) + self.assertTrue(data.find('Timmerman'.encode())) def test_should_work_with_foreign_key_fields(self): class OccupationWithForeignKeyFieldsTable(tables.Table): @@ -208,11 +208,11 @@ class View(DispatchHookMixin, ExportMixin, tables.SingleTableView): data = response.getvalue().decode('utf8') expected_csv = '\r\n'.join(( - 'Name,Boolean,Region,First Name', + 'Name,Boolean,Region,First name', 'Timmerman,True,Vlaanderen,Richard', 'Ecoloog,False,Vlaanderen,Richard\r\n' )) - assert data == expected_csv + self.assertEqual(data, expected_csv) def test_should_allow_exclude_columns(self): class OccupationExcludingView(DispatchHookMixin, ExportMixin, tables.SingleTableView): diff --git a/tests/test_models.py b/tests/test_models.py index b2dd954c..3f06b95b 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -220,22 +220,22 @@ class PersonTable(tables.Table): table = PersonTable(Person.objects.all()) # Should be generated (capitalized column name) - assert 'First Name' == table.columns['first_name'].verbose_name - assert 'First Name' == table.columns['fn1'].verbose_name - assert 'First Name' == table.columns['fn2'].verbose_name - assert 'OVERRIDE' == table.columns['fn3'].verbose_name - assert 'override' == table.columns['fn4'].verbose_name + self.assertEqual('First name', table.columns['first_name'].verbose_name) + self.assertEqual('First name', table.columns['fn1'].verbose_name) + self.assertEqual('First name', table.columns['fn2'].verbose_name) + self.assertEqual('OVERRIDE', table.columns['fn3'].verbose_name) + self.assertEqual('override', table.columns['fn4'].verbose_name) # Should use the titlised model field's verbose_name - assert 'Surname' == table.columns['last_name'].verbose_name - assert 'Surname' == table.columns['ln1'].verbose_name - assert 'Surname' == table.columns['ln2'].verbose_name - assert 'OVERRIDE' == table.columns['ln3'].verbose_name - assert 'Name' == table.columns['region'].verbose_name - assert 'Name' == table.columns['r1'].verbose_name - assert 'Name' == table.columns['r2'].verbose_name - assert 'OVERRIDE' == table.columns['r3'].verbose_name - assert 'Translation Test' == table.columns['trans_test'].verbose_name - assert 'Translation Test Lazy' == table.columns['trans_test_lazy'].verbose_name + self.assertEqual('Surname', table.columns['last_name'].verbose_name) + self.assertEqual('Surname', table.columns['ln1'].verbose_name) + self.assertEqual('Surname', table.columns['ln2'].verbose_name) + self.assertEqual('OVERRIDE', table.columns['ln3'].verbose_name) + self.assertEqual('Name', table.columns['region'].verbose_name) + self.assertEqual('Name', table.columns['r1'].verbose_name) + self.assertEqual('Name', table.columns['r2'].verbose_name) + self.assertEqual('OVERRIDE', table.columns['r3'].verbose_name) + self.assertEqual('Translation test', table.columns['trans_test'].verbose_name) + self.assertEqual('Translation test lazy', table.columns['trans_test_lazy'].verbose_name) def test_using_Meta_model(self): # Now we'll try using a table with Meta.model @@ -247,22 +247,19 @@ class Meta: # Issue #16 table = PersonTable(Person.objects.all()) - assert 'Translation Test' == table.columns['trans_test'].verbose_name - assert 'Translation Test Lazy' == table.columns['trans_test_lazy'].verbose_name - assert 'Web Site' == table.columns['website'].verbose_name - assert 'Birthdate' == table.columns['birthdate'].verbose_name - assert 'OVERRIDE' == table.columns['first_name'].verbose_name - - # Verbose name should be lazy if it comes from the model field and - # the column was not declared explicitly + self.assertEqual('Translation test', table.columns['trans_test'].verbose_name) + self.assertEqual('Translation test lazy', table.columns['trans_test_lazy'].verbose_name) + self.assertEqual('Web site', table.columns['website'].verbose_name) + self.assertEqual('Birthdate', table.columns['birthdate'].verbose_name) + self.assertEqual('OVERRIDE', table.columns['first_name'].verbose_name) + class PersonTable(tables.Table): class Meta: model = Person table = PersonTable(Person.objects.all()) - assert type(table.columns['trans_test_lazy'].verbose_name) is not six.text_type with translation_override('ua'): - assert 'Тест Ленивого Перекладу' == table.columns['trans_test_lazy'].verbose_name + self.assertEqual('Тест ленивого перекладу', table.columns['trans_test_lazy'].verbose_name) def test_data_verbose_name(self): table = tables.Table(Person.objects.all())