Browse Source

Fixed #29750 -- Added View.setup() hook for class-based views.

François Freitag 6 years ago
parent
commit
e671337e8b

+ 12 - 3
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

+ 19 - 5
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 <how-django-processes-a-request>` 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
+        <how-django-processes-a-request>` 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()`

+ 16 - 0
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`

+ 2 - 0
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()`

+ 4 - 1
docs/releases/2.2.txt

@@ -166,7 +166,10 @@ Forms
 Generic Views
 ~~~~~~~~~~~~~
 
-* ...
+* The new :meth:`View.setup <django.views.generic.base.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
 ~~~~~~~~~~~~~~~~~~~~

+ 6 - 5
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

+ 26 - 0
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