Repository: carltongibson/django-filter

Issue:
filter field title changes fields verbose_name 
When defining model fields verbose_name I want exactly use this as part of the generated filter label.

e.g. model field:

    dns = models.ManyToManyField(ReverseDnsEntry, blank=True, verbose_name='DNS')
    cidr = models.ForeignKey(Network, to_field='cidr', null=True, blank=True, verbose_name='CIDR')
    function = models.CharField(max_length="10", blank=True, verbose_name='Function')

Currently django filters creates a filter label like `Dns cidr function contains` instead of `DNS CIDR Function contains`. 

How can I change that behavior?


Discussion:
`Filter` takes a `label` kwarg. https://django-filter.readthedocs.io/en/develop/ref/filters.html#label
Sorry if i expressed myself not completely enough. 

I still want to rely on django-filters `generated filter labels` because they are really shining for my use case. 

I am missing a way to override `FILTERS_VERBOSE_LOOKUPS` and override the field name part with the fields `verbose_name`.
Well, the behaviour comes from [`django_filters.utils.verbose_field_name`](https://github.com/carltongibson/django-filter/blob/fd6c9a70ab2c97a5037e742affa5ea979ea597dc/django_filters/utils.py#L158-L184) so the first point of call is a failing test case showing the behaviour you'd like to see. 
That looks good in a shell but why is it titelized in the label?

	In [45]: utils.verbose_field_name(DnsEntry, 'dns__cidr__cidr')
	Out[45]: u'DNS CIDR CIDR'

>> Dns cidr cidr
Perhaps to note, as I am coming from Germany my expectation is that substantives are written in uppercase, also the app is missing i18n, yet ...
If you look at the code `label_for_filter` calls `django.forms.utils.pretty_name` which applies the capitalisation. 

The simplest way around that behaviour is to supply the `label` kwarg. It'd be an interesting question as to how to detect when to apply the capitalisation and when not. 

If you're looking to contribute to open source than PRs improving either this behaviour or the i18n of the labels here would be welcomed.
So perhaps pretty_name makes something in respect to Djangos language settings? I meant i18n of my app. If I have something related to django filters I'll post a PR.

Thanks for pointing that out and thanks for such a great extension!

Resolving patch:
diff --git a/django_filters/compat.py b/django_filters/compat.py
--- a/django_filters/compat.py
+++ b/django_filters/compat.py
@@ -5,6 +5,11 @@
 from django.conf import settings
 from django.utils.timezone import make_aware as make_aware_orig
 
+try:
+    from django.forms.utils import pretty_name
+except ImportError:  # Django 1.8
+    from django.forms.forms import pretty_name
+
 # django-crispy-forms is optional
 try:
     import crispy_forms
@@ -63,4 +68,4 @@ def make_aware(value, timezone, is_dst):
     if django.VERSION >= (1, 9):
         return make_aware_orig(value, timezone, is_dst)
     else:
-        return make_aware_orig(value, timezone)
\ No newline at end of file
+        return make_aware_orig(value, timezone)
diff --git a/django_filters/filters.py b/django_filters/filters.py
--- a/django_filters/filters.py
+++ b/django_filters/filters.py
@@ -13,13 +13,14 @@
 from django.utils.timezone import now
 from django.utils.translation import ugettext_lazy as _
 
+from .compat import pretty_name
 from .conf import settings
 from .constants import EMPTY_VALUES
 from .fields import (
     Lookup, LookupTypeField, BaseCSVField, BaseRangeField, RangeField,
     DateRangeField, DateTimeRangeField, TimeRangeField, IsoDateTimeField
 )
-from .utils import label_for_filter, pretty_name
+from .utils import label_for_filter
 
 
 __all__ = [
diff --git a/django_filters/utils.py b/django_filters/utils.py
--- a/django_filters/utils.py
+++ b/django_filters/utils.py
@@ -10,13 +10,9 @@
 from django.db.models.fields.related import RelatedField, ForeignObjectRel
 from django.utils import six, timezone
 from django.utils.encoding import force_text
+from django.utils.text import capfirst
 from django.utils.translation import ugettext as _
 
-try:
-    from django.forms.utils import pretty_name
-except ImportError:  # Django 1.8
-    from django.forms.forms import pretty_name
-
 from .compat import make_aware, remote_field, remote_model
 from .exceptions import FieldLookupError
 
@@ -177,7 +173,7 @@ def verbose_field_name(model, field_name):
     names = []
     for part in parts:
         if isinstance(part, ForeignObjectRel):
-            names.append(force_text(part.related_name))
+            names.append(part.related_name.replace('_', ' '))
         else:
             names.append(force_text(part.verbose_name))
 
@@ -229,6 +225,6 @@ def label_for_filter(model, field_name, lookup_expr, exclude=False):
         verbose_expression += [verbose_lookup_expr(lookup_expr)]
 
     verbose_expression = [force_text(part) for part in verbose_expression if part]
-    verbose_expression = pretty_name(' '.join(verbose_expression))
+    verbose_expression = capfirst(' '.join(verbose_expression))
 
     return verbose_expression


Test patch:
diff --git a/tests/models.py b/tests/models.py
--- a/tests/models.py
+++ b/tests/models.py
@@ -131,6 +131,8 @@ class NetworkSetting(models.Model):
     ip = models.GenericIPAddressField()
     mask = SubnetMaskField()
 
+    cidr = models.CharField(max_length=18, blank=True, verbose_name="CIDR")
+
 
 @python_2_unicode_compatible
 class Company(models.Model):
diff --git a/tests/test_filterset.py b/tests/test_filterset.py
--- a/tests/test_filterset.py
+++ b/tests/test_filterset.py
@@ -531,7 +531,7 @@ class Meta:
                     SubnetMaskField: {'filter_class': CharFilter}
                 }
 
-        self.assertEqual(list(F.base_filters.keys()), ['ip', 'mask'])
+        self.assertEqual(list(F.base_filters.keys()), ['ip', 'mask', 'cidr'])
 
     def test_custom_declared_field_no_warning(self):
         class F(FilterSet):
diff --git a/tests/test_utils.py b/tests/test_utils.py
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -20,6 +20,7 @@
 from .models import Book
 from .models import HiredWorker
 from .models import Business
+from .models import NetworkSetting
 
 
 class GetFieldPartsTests(TestCase):
@@ -226,6 +227,10 @@ def test_field_with_verbose_name(self):
         verbose_name = verbose_field_name(Article, 'name')
         self.assertEqual(verbose_name, 'title')
 
+    def test_field_all_caps(self):
+        verbose_name = verbose_field_name(NetworkSetting, 'cidr')
+        self.assertEqual(verbose_name, 'CIDR')
+
     def test_forwards_related_field(self):
         verbose_name = verbose_field_name(Article, 'author__username')
         self.assertEqual(verbose_name, 'author username')
@@ -234,6 +239,10 @@ def test_backwards_related_field(self):
         verbose_name = verbose_field_name(Book, 'lovers__first_name')
         self.assertEqual(verbose_name, 'lovers first name')
 
+    def test_backwards_related_field_multi_word(self):
+        verbose_name = verbose_field_name(User, 'manager_of')
+        self.assertEqual(verbose_name, 'manager of')
+
     def test_lazy_text(self):
         # sanity check
         field = User._meta.get_field('username')
@@ -287,6 +296,10 @@ def test_exact_lookup(self):
         label = label_for_filter(Article, 'name', 'exact')
         self.assertEqual(label, 'Title')
 
+    def test_field_all_caps(self):
+        label = label_for_filter(NetworkSetting, 'cidr', 'contains', exclude=True)
+        self.assertEqual(label, 'Exclude CIDR contains')
+
 
 class HandleTimezone(TestCase):