diff --git a/django/views/generic/base.py b/django/views/generic/base.py index fcdc7c785b..c32a58a349 100644 --- a/django/views/generic/base.py +++ b/django/views/generic/base.py @@ -8,6 +8,16 @@ from django.utils.decorators import classonlymethod logger = getLogger('django.request') +class ContextMixin(object): + """ + A default context mixin that passes the keyword arguments received by + get_context_data as the template context. + """ + + def get_context_data(self, **kwargs): + return kwargs + + class View(object): """ Intentionally simple parent class for all views. Only implements @@ -110,17 +120,13 @@ class TemplateResponseMixin(object): return [self.template_name] -class TemplateView(TemplateResponseMixin, View): +class TemplateView(TemplateResponseMixin, ContextMixin, View): """ - A view that renders a template. + A view that renders a template. This view is different from all the others + insofar as it also passes ``kwargs`` as ``params`` to the template context. """ - def get_context_data(self, **kwargs): - return { - 'params': kwargs - } - def get(self, request, *args, **kwargs): - context = self.get_context_data(**kwargs) + context = self.get_context_data(params=kwargs) return self.render_to_response(context) diff --git a/django/views/generic/dates.py b/django/views/generic/dates.py index 5f5f9591e9..e5c3e5bbe3 100644 --- a/django/views/generic/dates.py +++ b/django/views/generic/dates.py @@ -217,16 +217,6 @@ class BaseDateListView(MultipleObjectMixin, DateMixin, View): return date_list - def get_context_data(self, **kwargs): - """ - Get the context. Must return a Context (or subclass) instance. - """ - items = kwargs.pop('object_list') - context = super(BaseDateListView, self).get_context_data(object_list=items) - context.update(kwargs) - return context - - class BaseArchiveIndexView(BaseDateListView): """ Base class for archives of date-based items. diff --git a/django/views/generic/detail.py b/django/views/generic/detail.py index b9278bb1c2..a2adb15d47 100644 --- a/django/views/generic/detail.py +++ b/django/views/generic/detail.py @@ -2,10 +2,10 @@ from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist from django.http import Http404 from django.utils.encoding import smart_str from django.utils.translation import ugettext as _ -from django.views.generic.base import TemplateResponseMixin, View +from django.views.generic.base import TemplateResponseMixin, ContextMixin, View -class SingleObjectMixin(object): +class SingleObjectMixin(ContextMixin): """ Provides the ability to retrieve a single object for further manipulation. """ @@ -86,11 +86,12 @@ class SingleObjectMixin(object): return None def get_context_data(self, **kwargs): - context = kwargs + context = {} context_object_name = self.get_context_object_name(self.object) if context_object_name: context[context_object_name] = self.object - return context + context.update(kwargs) + return super(SingleObjectMixin, self).get_context_data(**context) class BaseDetailView(SingleObjectMixin, View): diff --git a/django/views/generic/edit.py b/django/views/generic/edit.py index d107e9a732..1f488cb880 100644 --- a/django/views/generic/edit.py +++ b/django/views/generic/edit.py @@ -1,12 +1,12 @@ from django.forms import models as model_forms from django.core.exceptions import ImproperlyConfigured from django.http import HttpResponseRedirect -from django.views.generic.base import TemplateResponseMixin, View +from django.views.generic.base import TemplateResponseMixin, ContextMixin, View from django.views.generic.detail import (SingleObjectMixin, SingleObjectTemplateResponseMixin, BaseDetailView) -class FormMixin(object): +class FormMixin(ContextMixin): """ A mixin that provides a way to show and handle a form in a request. """ @@ -45,9 +45,6 @@ class FormMixin(object): }) return kwargs - def get_context_data(self, **kwargs): - return kwargs - def get_success_url(self): if self.success_url: url = self.success_url @@ -113,13 +110,14 @@ class ModelFormMixin(FormMixin, SingleObjectMixin): return super(ModelFormMixin, self).form_valid(form) def get_context_data(self, **kwargs): - context = kwargs + context = {} if self.object: context['object'] = self.object context_object_name = self.get_context_object_name(self.object) if context_object_name: context[context_object_name] = self.object - return context + context.update(kwargs) + return super(ModelFormMixin, self).get_context_data(**context) class ProcessFormView(View): diff --git a/django/views/generic/list.py b/django/views/generic/list.py index 9797356529..d4664c34ef 100644 --- a/django/views/generic/list.py +++ b/django/views/generic/list.py @@ -3,10 +3,10 @@ from django.core.exceptions import ImproperlyConfigured from django.http import Http404 from django.utils.encoding import smart_str from django.utils.translation import ugettext as _ -from django.views.generic.base import TemplateResponseMixin, View +from django.views.generic.base import TemplateResponseMixin, ContextMixin, View -class MultipleObjectMixin(object): +class MultipleObjectMixin(ContextMixin): allow_empty = True queryset = None model = None @@ -103,10 +103,10 @@ class MultipleObjectMixin(object): 'is_paginated': False, 'object_list': queryset } - context.update(kwargs) if context_object_name is not None: context[context_object_name] = queryset - return context + context.update(kwargs) + return super(MultipleObjectMixin, self).get_context_data(**context) class BaseListView(MultipleObjectMixin, View): diff --git a/docs/topics/class-based-views.txt b/docs/topics/class-based-views.txt index 8059ed1df2..e1e2618478 100644 --- a/docs/topics/class-based-views.txt +++ b/docs/topics/class-based-views.txt @@ -270,6 +270,16 @@ more:: context['book_list'] = Book.objects.all() return context +.. note:: + + Generally, get_context_data will merge the context data of all parent classes + with those of the current class. To preserve this behavior in your own classes + where you want to alter the context, you should be sure to call + get_context_data on the super class. When no two classes try to define the same + key, this will give the expected results. However if any class attempts to + override a key after parent classes have set it (after the call to super), any + children of that class will also need to explictly set it after super if they + want to be sure to override all parents. Viewing subsets of objects -------------------------- diff --git a/tests/regressiontests/generic_views/base.py b/tests/regressiontests/generic_views/base.py index 6528dc6723..e18ed2a2a7 100644 --- a/tests/regressiontests/generic_views/base.py +++ b/tests/regressiontests/generic_views/base.py @@ -1,3 +1,5 @@ +from __future__ import absolute_import + import time from django.core.exceptions import ImproperlyConfigured @@ -6,6 +8,7 @@ from django.test import TestCase, RequestFactory from django.utils import unittest from django.views.generic import View, TemplateView, RedirectView +from . import views class SimpleView(View): """ @@ -331,3 +334,19 @@ class RedirectViewTest(unittest.TestCase): # we can't use self.rf.get because it always sets QUERY_STRING response = RedirectView.as_view(url='/bar/')(self.rf.request(PATH_INFO='/foo/')) self.assertEqual(response.status_code, 301) + + +class GetContextDataTest(unittest.TestCase): + + def test_get_context_data_super(self): + test_view = views.CustomContextView() + context = test_view.get_context_data(kwarg_test='kwarg_value') + + # the test_name key is inserted by the test classes parent + self.assertTrue('test_name' in context) + self.assertEqual(context['kwarg_test'], 'kwarg_value') + self.assertEqual(context['custom_key'], 'custom_value') + + # test that kwarg overrides values assigned higher up + context = test_view.get_context_data(test_name='test_value') + self.assertEqual(context['test_name'], 'test_value') diff --git a/tests/regressiontests/generic_views/tests.py b/tests/regressiontests/generic_views/tests.py index 72aab035a8..c985ad3309 100644 --- a/tests/regressiontests/generic_views/tests.py +++ b/tests/regressiontests/generic_views/tests.py @@ -1,6 +1,7 @@ from __future__ import absolute_import -from .base import ViewTest, TemplateViewTest, RedirectViewTest +from .base import (ViewTest, TemplateViewTest, RedirectViewTest, + GetContextDataTest) from .dates import (ArchiveIndexViewTests, YearArchiveViewTests, MonthArchiveViewTests, WeekArchiveViewTests, DayArchiveViewTests, DateDetailViewTests) diff --git a/tests/regressiontests/generic_views/views.py b/tests/regressiontests/generic_views/views.py index 5ff9cf0e65..1e70ba4b46 100644 --- a/tests/regressiontests/generic_views/views.py +++ b/tests/regressiontests/generic_views/views.py @@ -14,10 +14,9 @@ class CustomTemplateView(generic.TemplateView): template_name = 'generic_views/about.html' def get_context_data(self, **kwargs): - return { - 'params': kwargs, - 'key': 'value' - } + context = super(CustomTemplateView, self).get_context_data(**kwargs) + context.update({'key': 'value'}) + return context class ObjectDetail(generic.DetailView): @@ -184,3 +183,18 @@ class BookDetailGetObjectCustomQueryset(BookDetail): def get_object(self, queryset=None): return super(BookDetailGetObjectCustomQueryset,self).get_object( queryset=Book.objects.filter(pk=2)) + +class CustomContextView(generic.detail.SingleObjectMixin, generic.View): + model = Book + object = Book(name='dummy') + + def get_object(self): + return Book(name="dummy") + + def get_context_data(self, **kwargs): + context = {'custom_key': 'custom_value'} + context.update(kwargs) + return super(CustomContextView, self).get_context_data(**context) + + def get_context_object_name(self, obj): + return "test_name"