From e671337e8b35427b379303cbf6808855f04083e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Wed, 19 Sep 2018 10:53:05 +0200 Subject: [PATCH] Fixed #29750 -- Added View.setup() hook for class-based views. --- django/views/generic/base.py | 15 ++++++++--- docs/ref/class-based-views/base.txt | 24 +++++++++++++---- .../ref/class-based-views/flattened-index.txt | 16 ++++++++++++ .../ref/class-based-views/generic-display.txt | 2 ++ docs/releases/2.2.txt | 5 +++- docs/topics/class-based-views/intro.txt | 11 ++++---- tests/generic_views/test_base.py | 26 +++++++++++++++++++ 7 files changed, 85 insertions(+), 14 deletions(-) diff --git a/django/views/generic/base.py b/django/views/generic/base.py index cea077c6d1..5ed4f18ee1 100644 --- a/django/views/generic/base.py +++ b/django/views/generic/base.py @@ -62,9 +62,12 @@ class View: self = cls(**initkwargs) if hasattr(self, 'get') and not hasattr(self, 'head'): self.head = self.get - self.request = request - self.args = args - self.kwargs = kwargs + self.setup(request, *args, **kwargs) + if not hasattr(self, 'request'): + raise AttributeError( + "%s instance has no 'request' attribute. Did you override " + "setup() and forget to call super()?" % cls.__name__ + ) return self.dispatch(request, *args, **kwargs) view.view_class = cls view.view_initkwargs = initkwargs @@ -77,6 +80,12 @@ class View: update_wrapper(view, cls.dispatch, assigned=()) return view + def setup(self, request, *args, **kwargs): + """Initialize attributes shared by all view methods.""" + self.request = request + self.args = args + self.kwargs = kwargs + def dispatch(self, request, *args, **kwargs): # Try to dispatch to the right method; if a method doesn't exist, # defer to the error handler. Also defer to the error handler if the diff --git a/docs/ref/class-based-views/base.txt b/docs/ref/class-based-views/base.txt index 3b753a1a3d..85e3c8bd4b 100644 --- a/docs/ref/class-based-views/base.txt +++ b/docs/ref/class-based-views/base.txt @@ -24,6 +24,7 @@ MRO is an acronym for Method Resolution Order. **Method Flowchart** + #. :meth:`setup()` #. :meth:`dispatch()` #. :meth:`http_method_not_allowed()` #. :meth:`options()` @@ -70,11 +71,22 @@ MRO is an acronym for Method Resolution Order. attributes. When the view is called during the request/response cycle, the - :class:`~django.http.HttpRequest` is assigned to the view's ``request`` - attribute. Any positional and/or keyword arguments :ref:`captured from - the URL pattern ` are assigned to the - ``args`` and ``kwargs`` attributes, respectively. Then :meth:`dispatch` - is called. + :meth:`setup` method assigns the :class:`~django.http.HttpRequest` to + the view's ``request`` attribute, and any positional and/or keyword + arguments :ref:`captured from the URL pattern + ` to the ``args`` and ``kwargs`` + attributes, respectively. Then :meth:`dispatch` is called. + + .. method:: setup(request, *args, **kwargs) + + .. versionadded:: 2.2 + + Initializes view instance attributes: ``self.request``, ``self.args``, + and ``self.kwargs`` prior to :meth:`dispatch`. + + Overriding this method allows mixins to setup instance attributes for + reuse in child classes. When overriding this method, you must call + ``super()``. .. method:: dispatch(request, *args, **kwargs) @@ -123,6 +135,7 @@ MRO is an acronym for Method Resolution Order. **Method Flowchart** + #. :meth:`~django.views.generic.base.View.setup()` #. :meth:`~django.views.generic.base.View.dispatch()` #. :meth:`~django.views.generic.base.View.http_method_not_allowed()` #. :meth:`~django.views.generic.base.ContextMixin.get_context_data()` @@ -184,6 +197,7 @@ MRO is an acronym for Method Resolution Order. **Method Flowchart** + #. :meth:`~django.views.generic.base.View.setup()` #. :meth:`~django.views.generic.base.View.dispatch()` #. :meth:`~django.views.generic.base.View.http_method_not_allowed()` #. :meth:`get_redirect_url()` diff --git a/docs/ref/class-based-views/flattened-index.txt b/docs/ref/class-based-views/flattened-index.txt index 148e95a6af..6e85e0b3b8 100644 --- a/docs/ref/class-based-views/flattened-index.txt +++ b/docs/ref/class-based-views/flattened-index.txt @@ -31,6 +31,7 @@ Simple generic views * :meth:`~django.views.generic.base.View.dispatch` * ``head()`` * :meth:`~django.views.generic.base.View.http_method_not_allowed` +* :meth:`~django.views.generic.base.View.setup` ``TemplateView`` ---------------- @@ -55,6 +56,7 @@ Simple generic views * ``head()`` * :meth:`~django.views.generic.base.View.http_method_not_allowed` * :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` +* :meth:`~django.views.generic.base.View.setup` ``RedirectView`` ---------------- @@ -80,6 +82,7 @@ Simple generic views * ``options()`` * ``post()`` * ``put()`` +* :meth:`~django.views.generic.base.View.setup` Detail Views ============ @@ -116,6 +119,7 @@ Detail Views * ``head()`` * :meth:`~django.views.generic.base.View.http_method_not_allowed` * :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` +* :meth:`~django.views.generic.base.View.setup` List Views ========== @@ -154,6 +158,7 @@ List Views * :meth:`~django.views.generic.base.View.http_method_not_allowed` * :meth:`~django.views.generic.list.MultipleObjectMixin.paginate_queryset` * :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` +* :meth:`~django.views.generic.base.View.setup` Editing views ============= @@ -189,6 +194,7 @@ Editing views * :meth:`~django.views.generic.base.View.http_method_not_allowed` * :meth:`~django.views.generic.edit.ProcessFormView.post` * :meth:`~django.views.generic.edit.ProcessFormView.put` +* :meth:`~django.views.generic.base.View.setup` ``CreateView`` -------------- @@ -233,6 +239,7 @@ Editing views * :meth:`~django.views.generic.edit.ProcessFormView.post` * ``put()`` * :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` +* :meth:`~django.views.generic.base.View.setup` ``UpdateView`` -------------- @@ -277,6 +284,7 @@ Editing views * :meth:`~django.views.generic.edit.ProcessFormView.post` * ``put()`` * :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` +* :meth:`~django.views.generic.base.View.setup` ``DeleteView`` -------------- @@ -313,6 +321,7 @@ Editing views * :meth:`~django.views.generic.base.View.http_method_not_allowed` * ``post()`` * :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` +* :meth:`~django.views.generic.base.View.setup` Date-based views ================ @@ -356,6 +365,7 @@ Date-based views * :meth:`~django.views.generic.base.View.http_method_not_allowed` * :meth:`~django.views.generic.list.MultipleObjectMixin.paginate_queryset` * :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` +* :meth:`~django.views.generic.base.View.setup` ``YearArchiveView`` ------------------- @@ -399,6 +409,7 @@ Date-based views * :meth:`~django.views.generic.base.View.http_method_not_allowed` * :meth:`~django.views.generic.list.MultipleObjectMixin.paginate_queryset` * :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` +* :meth:`~django.views.generic.base.View.setup` ``MonthArchiveView`` -------------------- @@ -445,6 +456,7 @@ Date-based views * :meth:`~django.views.generic.base.View.http_method_not_allowed` * :meth:`~django.views.generic.list.MultipleObjectMixin.paginate_queryset` * :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` +* :meth:`~django.views.generic.base.View.setup` ``WeekArchiveView`` ------------------- @@ -489,6 +501,7 @@ Date-based views * :meth:`~django.views.generic.base.View.http_method_not_allowed` * :meth:`~django.views.generic.list.MultipleObjectMixin.paginate_queryset` * :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` +* :meth:`~django.views.generic.base.View.setup` ``DayArchiveView`` ------------------ @@ -539,6 +552,7 @@ Date-based views * :meth:`~django.views.generic.base.View.http_method_not_allowed` * :meth:`~django.views.generic.list.MultipleObjectMixin.paginate_queryset` * :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` +* :meth:`~django.views.generic.base.View.setup` ``TodayArchiveView`` -------------------- @@ -589,6 +603,7 @@ Date-based views * :meth:`~django.views.generic.base.View.http_method_not_allowed` * :meth:`~django.views.generic.list.MultipleObjectMixin.paginate_queryset` * :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` +* :meth:`~django.views.generic.base.View.setup` ``DateDetailView`` ------------------ @@ -634,3 +649,4 @@ Date-based views * ``head()`` * :meth:`~django.views.generic.base.View.http_method_not_allowed` * :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` +* :meth:`~django.views.generic.base.View.setup` diff --git a/docs/ref/class-based-views/generic-display.txt b/docs/ref/class-based-views/generic-display.txt index e7b2163818..ac1e4c39ca 100644 --- a/docs/ref/class-based-views/generic-display.txt +++ b/docs/ref/class-based-views/generic-display.txt @@ -25,6 +25,7 @@ many projects they are typically the most commonly used views. **Method Flowchart** + #. :meth:`~django.views.generic.base.View.setup()` #. :meth:`~django.views.generic.base.View.dispatch()` #. :meth:`~django.views.generic.base.View.http_method_not_allowed()` #. :meth:`~django.views.generic.base.TemplateResponseMixin.get_template_names()` @@ -95,6 +96,7 @@ many projects they are typically the most commonly used views. **Method Flowchart** + #. :meth:`~django.views.generic.base.View.setup()` #. :meth:`~django.views.generic.base.View.dispatch()` #. :meth:`~django.views.generic.base.View.http_method_not_allowed()` #. :meth:`~django.views.generic.base.TemplateResponseMixin.get_template_names()` diff --git a/docs/releases/2.2.txt b/docs/releases/2.2.txt index 21786d6d63..b144eb6234 100644 --- a/docs/releases/2.2.txt +++ b/docs/releases/2.2.txt @@ -166,7 +166,10 @@ Forms Generic Views ~~~~~~~~~~~~~ -* ... +* The new :meth:`View.setup ` hook + initializes view attributes before calling + :meth:`~django.views.generic.base.View.dispatch`. It allows mixins to setup + instance attributes for reuse in child classes. Internationalization ~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/topics/class-based-views/intro.txt b/docs/topics/class-based-views/intro.txt index f4a5f5ac24..8399b56041 100644 --- a/docs/topics/class-based-views/intro.txt +++ b/docs/topics/class-based-views/intro.txt @@ -82,11 +82,12 @@ Because Django's URL resolver expects to send the request and associated arguments to a callable function, not a class, class-based views have an :meth:`~django.views.generic.base.View.as_view` class method which returns a function that can be called when a request arrives for a URL matching the -associated pattern. The function creates an instance of the class and calls its -:meth:`~django.views.generic.base.View.dispatch` method. ``dispatch`` looks at -the request to determine whether it is a ``GET``, ``POST``, etc, and relays the -request to a matching method if one is defined, or raises -:class:`~django.http.HttpResponseNotAllowed` if not:: +associated pattern. The function creates an instance of the class, calls +:meth:`~django.views.generic.base.View.setup` to initialize its attributes, and +then calls its :meth:`~django.views.generic.base.View.dispatch` method. +``dispatch`` looks at the request to determine whether it is a ``GET``, +``POST``, etc, and relays the request to a matching method if one is defined, +or raises :class:`~django.http.HttpResponseNotAllowed` if not:: # urls.py from django.urls import path diff --git a/tests/generic_views/test_base.py b/tests/generic_views/test_base.py index d3e4510e6b..da12b1bbe8 100644 --- a/tests/generic_views/test_base.py +++ b/tests/generic_views/test_base.py @@ -233,6 +233,32 @@ class ViewTest(SimpleTestCase): self.assertNotIn(attribute, dir(bare_view)) self.assertIn(attribute, dir(view)) + def test_overridden_setup(self): + class SetAttributeMixin: + def setup(self, request, *args, **kwargs): + self.attr = True + super().setup(request, *args, **kwargs) + + class CheckSetupView(SetAttributeMixin, SimpleView): + def dispatch(self, request, *args, **kwargs): + assert hasattr(self, 'attr') + return super().dispatch(request, *args, **kwargs) + + response = CheckSetupView.as_view()(self.rf.get('/')) + self.assertEqual(response.status_code, 200) + + def test_not_calling_parent_setup_error(self): + class TestView(View): + def setup(self, request, *args, **kwargs): + pass # Not calling supre().setup() + + msg = ( + "TestView instance has no 'request' attribute. Did you override " + "setup() and forget to call super()?" + ) + with self.assertRaisesMessage(AttributeError, msg): + TestView.as_view()(self.rf.get('/')) + def test_direct_instantiation(self): """ It should be possible to use the view by directly instantiating it