From 4ed534758cb6a11df9f49baddecca5a6cdda9311 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Thu, 15 Aug 2019 06:48:33 +0100 Subject: [PATCH] Fixed #19878 -- Deprecated TemplateView passing URL kwargs into context. --- django/views/generic/base.py | 30 +++++++++++++-- docs/internals/deprecation.txt | 3 ++ docs/ref/class-based-views/base.txt | 12 ++++-- docs/releases/3.1.txt | 4 ++ tests/generic_views/test_base.py | 59 +++++++++++++++++++---------- tests/generic_views/urls.py | 5 ++- 6 files changed, 84 insertions(+), 29 deletions(-) diff --git a/django/views/generic/base.py b/django/views/generic/base.py index 3dd957d8f8..ea5baca08d 100644 --- a/django/views/generic/base.py +++ b/django/views/generic/base.py @@ -1,4 +1,5 @@ import logging +import warnings from functools import update_wrapper from django.core.exceptions import ImproperlyConfigured @@ -9,6 +10,8 @@ from django.http import ( from django.template.response import TemplateResponse from django.urls import reverse from django.utils.decorators import classonlymethod +from django.utils.deprecation import RemovedInDjango40Warning +from django.utils.functional import SimpleLazyObject logger = logging.getLogger('django.request') @@ -152,14 +155,33 @@ class TemplateResponseMixin: class TemplateView(TemplateResponseMixin, ContextMixin, View): - """ - Render a template. Pass keyword arguments from the URLconf to the context. - """ + """Render a template.""" def get(self, request, *args, **kwargs): - context = self.get_context_data(**kwargs) + # RemovedInDjango40Warning: when the deprecation ends, replace with: + # context = self.get_context_data() + context_kwargs = _wrap_url_kwargs_with_deprecation_warning(kwargs) + context = self.get_context_data(**context_kwargs) return self.render_to_response(context) +# RemovedInDjango40Warning +def _wrap_url_kwargs_with_deprecation_warning(url_kwargs): + context_kwargs = {} + for key, value in url_kwargs.items(): + # Bind into function closure. + @SimpleLazyObject + def access_value(key=key, value=value): + warnings.warn( + 'TemplateView passing URL kwargs to the context is ' + 'deprecated. Reference %s in your template through ' + 'view.kwargs instead.' % key, + RemovedInDjango40Warning, stacklevel=2, + ) + return value + context_kwargs[key] = access_value + return context_kwargs + + class RedirectView(View): """Provide a redirect on any GET request.""" permanent = False diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index a4262af43b..7fea512750 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -66,6 +66,9 @@ details on these changes. * The ``list`` message for ``ModelMultipleChoiceField`` will be removed. +* ``django.views.generic.TemplateView`` will no longer pass URL kwargs directly + to the ``context``. + See the :ref:`Django 3.1 release notes ` for more details on these changes. diff --git a/docs/ref/class-based-views/base.txt b/docs/ref/class-based-views/base.txt index 7906c56846..de45ae3a5e 100644 --- a/docs/ref/class-based-views/base.txt +++ b/docs/ref/class-based-views/base.txt @@ -117,8 +117,7 @@ MRO is an acronym for Method Resolution Order. .. class:: django.views.generic.base.TemplateView - Renders a given template, with the context containing parameters captured - in the URL. + Renders a given template. **Ancestors (MRO)** @@ -162,12 +161,17 @@ MRO is an acronym for Method Resolution Order. **Context** - * Populated (through :class:`~django.views.generic.base.ContextMixin`) with - the keyword arguments captured from the URL pattern that served the view. + * Populated (through :class:`~django.views.generic.base.ContextMixin`). * You can also add context using the :attr:`~django.views.generic.base.ContextMixin.extra_context` keyword argument for :meth:`~django.views.generic.base.View.as_view`. + .. deprecated:: 3.1 + + Starting in Django 4.0, the keyword arguments captured from the URL + pattern won't be passed to the context. Reference them with + ``view.kwargs`` instead. + ``RedirectView`` ================ diff --git a/docs/releases/3.1.txt b/docs/releases/3.1.txt index 44ba489a44..d9c0a5db11 100644 --- a/docs/releases/3.1.txt +++ b/docs/releases/3.1.txt @@ -640,6 +640,10 @@ Miscellaneous * The ``list`` message for :class:`~django.forms.ModelMultipleChoiceField` is deprecated in favor of ``invalid_list``. +* The passing of URL kwargs directly to the context by + :class:`~django.views.generic.base.TemplateView` is deprecated. Reference + them in the template with ``view.kwargs`` instead. + .. _removed-features-3.1: Features removed in 3.1 diff --git a/tests/generic_views/test_base.py b/tests/generic_views/test_base.py index 7aaea3ffa0..d498d23a68 100644 --- a/tests/generic_views/test_base.py +++ b/tests/generic_views/test_base.py @@ -2,9 +2,12 @@ import time from django.core.exceptions import ImproperlyConfigured from django.http import HttpResponse -from django.test import RequestFactory, SimpleTestCase, override_settings +from django.test import ( + RequestFactory, SimpleTestCase, ignore_warnings, override_settings, +) from django.test.utils import require_jinja2 from django.urls import resolve +from django.utils.deprecation import RemovedInDjango40Warning from django.views.generic import RedirectView, TemplateView, View from . import views @@ -347,25 +350,6 @@ class TemplateViewTest(SimpleTestCase): view = TemplateView.as_view(template_name='generic_views/using.html', template_engine='jinja2') self.assertEqual(view(request).render().content, b'Jinja2\n') - def test_template_params(self): - """ - A generic template view passes kwargs as context. - """ - response = self.client.get('/template/simple/bar/') - self.assertEqual(response.status_code, 200) - self.assertEqual(response.context['foo'], 'bar') - self.assertIsInstance(response.context['view'], View) - - def test_extra_template_params(self): - """ - A template view can be customized to return extra context. - """ - response = self.client.get('/template/custom/bar/') - self.assertEqual(response.status_code, 200) - self.assertEqual(response.context['foo'], 'bar') - self.assertEqual(response.context['key'], 'value') - self.assertIsInstance(response.context['view'], View) - def test_cached_views(self): """ A template view can be cached @@ -584,3 +568,38 @@ class SingleObjectTemplateResponseMixinTest(SimpleTestCase): ) with self.assertRaisesMessage(ImproperlyConfigured, msg): view.get_template_names() + + +@override_settings(ROOT_URLCONF='generic_views.urls') +class DeprecationTests(SimpleTestCase): + @ignore_warnings(category=RemovedInDjango40Warning) + def test_template_params(self): + """A generic template view passes kwargs as context.""" + response = self.client.get('/template/simple/bar/') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context['foo'], 'bar') + self.assertIsInstance(response.context['view'], View) + + @ignore_warnings(category=RemovedInDjango40Warning) + def test_extra_template_params(self): + """A template view can be customized to return extra context.""" + response = self.client.get('/template/custom/bar1/bar2/') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context['foo1'], 'bar1') + self.assertEqual(response.context['foo2'], 'bar2') + self.assertEqual(response.context['key'], 'value') + self.assertIsInstance(response.context['view'], View) + + def test_template_params_warning(self): + response = self.client.get('/template/custom/bar1/bar2/') + self.assertEqual(response.status_code, 200) + msg = ( + 'TemplateView passing URL kwargs to the context is deprecated. ' + 'Reference %s in your template through view.kwargs instead.' + ) + with self.assertRaisesMessage(RemovedInDjango40Warning, msg % 'foo1'): + str(response.context['foo1']) + with self.assertRaisesMessage(RemovedInDjango40Warning, msg % 'foo2'): + str(response.context['foo2']) + self.assertEqual(response.context['key'], 'value') + self.assertIsInstance(response.context['view'], View) diff --git a/tests/generic_views/urls.py b/tests/generic_views/urls.py index 5295bff08d..d547c5be4a 100644 --- a/tests/generic_views/urls.py +++ b/tests/generic_views/urls.py @@ -12,7 +12,10 @@ urlpatterns = [ path('template/no_template/', TemplateView.as_view()), path('template/login_required/', login_required(TemplateView.as_view())), path('template/simple//', TemplateView.as_view(template_name='generic_views/about.html')), - path('template/custom//', views.CustomTemplateView.as_view(template_name='generic_views/about.html')), + path( + 'template/custom///', + views.CustomTemplateView.as_view(template_name='generic_views/about.html'), + ), path( 'template/content_type/', TemplateView.as_view(template_name='generic_views/robots.txt', content_type='text/plain'),