From a00e8d4e4204bfb730ba3eac2ee11ad83c8ec91b Mon Sep 17 00:00:00 2001
From: Russell Keith-Magee <russell@keith-magee.com>
Date: Mon, 3 Jan 2011 13:15:58 +0000
Subject: [PATCH] Fixed #14878 -- Clarified the way verbose_name_plural is used
 in generic list views as a context variable. Thanks to diegueus9 for the
 report.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@15133 bcc190cf-cafb-0310-a4f2-bffc1f526a37
---
 django/views/generic/detail.py                |  5 ++--
 django/views/generic/list.py                  |  6 ++++-
 docs/ref/class-based-views.txt                |  2 +-
 docs/topics/class-based-views.txt             | 23 +++++++++++++------
 tests/regressiontests/generic_views/detail.py |  9 +++++++-
 .../fixtures/generic-views-test-data.json     |  7 ++++++
 tests/regressiontests/generic_views/list.py   | 12 +++++++++-
 tests/regressiontests/generic_views/models.py |  2 ++
 tests/regressiontests/generic_views/urls.py   |  3 +++
 tests/regressiontests/generic_views/views.py  |  5 ++++
 10 files changed, 61 insertions(+), 13 deletions(-)

diff --git a/django/views/generic/detail.py b/django/views/generic/detail.py
index e75dd77409..3fc21c1f49 100644
--- a/django/views/generic/detail.py
+++ b/django/views/generic/detail.py
@@ -2,6 +2,7 @@ import re
 
 from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
 from django.http import Http404
+from django.utils.encoding import smart_str
 from django.views.generic.base import TemplateResponseMixin, View
 
 
@@ -79,8 +80,8 @@ class SingleObjectMixin(object):
         if self.context_object_name:
             return self.context_object_name
         elif hasattr(obj, '_meta'):
-            return re.sub('[^a-zA-Z0-9]+', '_',
-                    obj._meta.verbose_name.lower())
+            return smart_str(re.sub('[^a-zA-Z0-9]+', '_',
+                    obj._meta.verbose_name.lower()))
         else:
             return None
 
diff --git a/django/views/generic/list.py b/django/views/generic/list.py
index 22135720ed..cd7f9f6ae8 100644
--- a/django/views/generic/list.py
+++ b/django/views/generic/list.py
@@ -1,9 +1,12 @@
+import re
+
 from django.core.paginator import Paginator, InvalidPage
 from django.core.exceptions import ImproperlyConfigured
 from django.http import Http404
 from django.utils.encoding import smart_str
 from django.views.generic.base import TemplateResponseMixin, View
 
+
 class MultipleObjectMixin(object):
     allow_empty = True
     queryset = None
@@ -76,7 +79,8 @@ class MultipleObjectMixin(object):
         if self.context_object_name:
             return self.context_object_name
         elif hasattr(object_list, 'model'):
-            return smart_str(object_list.model._meta.verbose_name_plural)
+            return smart_str(re.sub('[^a-zA-Z0-9]+', '_',
+                    object_list.model._meta.verbose_name_plural.lower()))
         else:
             return None
 
diff --git a/docs/ref/class-based-views.txt b/docs/ref/class-based-views.txt
index e8255a4381..8350fd22b2 100644
--- a/docs/ref/class-based-views.txt
+++ b/docs/ref/class-based-views.txt
@@ -428,7 +428,7 @@ FormMixin
     .. method:: get_form_kwargs()
 
         Build the keyword arguments requried to instanciate an the form.
-        
+
         The ``initial`` argument is set to :meth:`.get_initial`. If the
         request is a ``POST`` or ``PUT``, the request data (``request.POST``
         and ``request.FILES``) will also be provided.
diff --git a/docs/topics/class-based-views.txt b/docs/topics/class-based-views.txt
index 877323bcdd..516a3150ed 100644
--- a/docs/topics/class-based-views.txt
+++ b/docs/topics/class-based-views.txt
@@ -206,14 +206,23 @@ their attributes or methods.
 Making "friendly" template contexts
 -----------------------------------
 
-You might have noticed that our sample publisher list template stores all the
-publishers in a variable named ``object_list``. While this works just fine, it
-isn't all that "friendly" to template authors: they have to "just know" that
-they're dealing with publishers here. A more obvious name for that variable
-would be ``publisher_list``.
+You might have noticed that our sample publisher list template stores
+all the publishers in a variable named ``object_list``. While this
+works just fine, it isn't all that "friendly" to template authors:
+they have to "just know" that they're dealing with publishers here.
 
-We can change the name of that variable easily with the ``context_object_name``
-attribute - here, we'll override it in the URLconf, since it's a simple change:
+Well, if you're dealing with a Django object, this is already done for
+you. When you are dealing with an object or queryset, Django is able
+to populate the context using the verbose name (or the plural verbose
+name, in the case of a list of objects) of the object being displayed.
+This is provided in addition to the default ``object_list`` entry, but
+contains exactly the same data.
+
+If the verbose name (or plural verbose name) still isn't a good match,
+you can manually set the name of the context variable. The
+``context_object_name`` attribute on a generic view specifies the
+context variable to use. In this example, we'll override it in the
+URLconf, since it's a simple change:
 
 .. parsed-literal::
 
diff --git a/tests/regressiontests/generic_views/detail.py b/tests/regressiontests/generic_views/detail.py
index 91cacf65ba..c26ca67bd1 100644
--- a/tests/regressiontests/generic_views/detail.py
+++ b/tests/regressiontests/generic_views/detail.py
@@ -1,7 +1,7 @@
 from django.core.exceptions import ImproperlyConfigured
 from django.test import TestCase
 
-from regressiontests.generic_views.models import Author, Page
+from regressiontests.generic_views.models import Artist, Author, Page
 
 
 class DetailViewTest(TestCase):
@@ -28,6 +28,13 @@ class DetailViewTest(TestCase):
         self.assertEqual(res.context['author'], Author.objects.get(slug='scott-rosenberg'))
         self.assertTemplateUsed(res, 'generic_views/author_detail.html')
 
+    def test_verbose_name(self):
+        res = self.client.get('/detail/artist/1/')
+        self.assertEqual(res.status_code, 200)
+        self.assertEqual(res.context['object'], Artist.objects.get(pk=1))
+        self.assertEqual(res.context['professional_artist'], Artist.objects.get(pk=1))
+        self.assertTemplateUsed(res, 'generic_views/artist_detail.html')
+
     def test_template_name(self):
         res = self.client.get('/detail/author/1/template_name/')
         self.assertEqual(res.status_code, 200)
diff --git a/tests/regressiontests/generic_views/fixtures/generic-views-test-data.json b/tests/regressiontests/generic_views/fixtures/generic-views-test-data.json
index 8ecfe5e1e2..dfffbbf8d8 100644
--- a/tests/regressiontests/generic_views/fixtures/generic-views-test-data.json
+++ b/tests/regressiontests/generic_views/fixtures/generic-views-test-data.json
@@ -1,4 +1,11 @@
 [
+ {
+   "model": "generic_views.artist",
+   "pk": 1,
+   "fields": {
+     "name": "Rene Magritte"
+   }
+ },
  {
    "model": "generic_views.author",
    "pk": 1,
diff --git a/tests/regressiontests/generic_views/list.py b/tests/regressiontests/generic_views/list.py
index 6ed28170a7..7414c62d3a 100644
--- a/tests/regressiontests/generic_views/list.py
+++ b/tests/regressiontests/generic_views/list.py
@@ -1,7 +1,7 @@
 from django.core.exceptions import ImproperlyConfigured
 from django.test import TestCase
 
-from regressiontests.generic_views.models import Author
+from regressiontests.generic_views.models import Author, Artist
 from regressiontests.generic_views.views import CustomPaginator
 
 class ListViewTests(TestCase):
@@ -106,6 +106,16 @@ class ListViewTests(TestCase):
         self.assertEqual(res.status_code, 200)
         self.assertEqual(len(res.context['object_list']), 1)
 
+    def test_verbose_name(self):
+        res = self.client.get('/list/artists/')
+        self.assertEqual(res.status_code, 200)
+        self.assertTemplateUsed(res, 'generic_views/list.html')
+        self.assertEqual(list(res.context['object_list']), list(Artist.objects.all()))
+        self.assertIs(res.context['professional_artists'], res.context['object_list'])
+        self.assertIsNone(res.context['paginator'])
+        self.assertIsNone(res.context['page_obj'])
+        self.assertFalse(res.context['is_paginated'])
+
     def test_allow_empty_false(self):
         res = self.client.get('/list/authors/notempty/')
         self.assertEqual(res.status_code, 200)
diff --git a/tests/regressiontests/generic_views/models.py b/tests/regressiontests/generic_views/models.py
index 5a8577d0f6..5445e24cee 100644
--- a/tests/regressiontests/generic_views/models.py
+++ b/tests/regressiontests/generic_views/models.py
@@ -5,6 +5,8 @@ class Artist(models.Model):
 
     class Meta:
         ordering = ['name']
+        verbose_name = 'professional artist'
+        verbose_name_plural = 'professional artists'
 
     def __unicode__(self):
         return self.name
diff --git a/tests/regressiontests/generic_views/urls.py b/tests/regressiontests/generic_views/urls.py
index 037da42d9a..bd997e7e6d 100644
--- a/tests/regressiontests/generic_views/urls.py
+++ b/tests/regressiontests/generic_views/urls.py
@@ -100,6 +100,9 @@ urlpatterns = patterns('',
         views.DictList.as_view()),
     (r'^list/dict/paginated/$',
         views.DictList.as_view(paginate_by=1)),
+    url(r'^list/artists/$',
+        views.ArtistList.as_view(),
+        name="artists_list"),
     url(r'^list/authors/$',
         views.AuthorList.as_view(),
         name="authors_list"),
diff --git a/tests/regressiontests/generic_views/views.py b/tests/regressiontests/generic_views/views.py
index e3a3c40694..e9be7c7ca9 100644
--- a/tests/regressiontests/generic_views/views.py
+++ b/tests/regressiontests/generic_views/views.py
@@ -47,6 +47,11 @@ class DictList(generic.ListView):
     template_name = 'generic_views/list.html'
 
 
+class ArtistList(generic.ListView):
+    template_name = 'generic_views/list.html'
+    queryset = Artist.objects.all()
+
+
 class AuthorList(generic.ListView):
     queryset = Author.objects.all()