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

This commit is contained in:
François Freitag 2018-09-19 10:53:05 +02:00 committed by Tim Graham
parent 19e863a844
commit e671337e8b
7 changed files with 85 additions and 14 deletions

View File

@ -62,9 +62,12 @@ class View:
self = cls(**initkwargs) self = cls(**initkwargs)
if hasattr(self, 'get') and not hasattr(self, 'head'): if hasattr(self, 'get') and not hasattr(self, 'head'):
self.head = self.get self.head = self.get
self.request = request self.setup(request, *args, **kwargs)
self.args = args if not hasattr(self, 'request'):
self.kwargs = kwargs 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) return self.dispatch(request, *args, **kwargs)
view.view_class = cls view.view_class = cls
view.view_initkwargs = initkwargs view.view_initkwargs = initkwargs
@ -77,6 +80,12 @@ class View:
update_wrapper(view, cls.dispatch, assigned=()) update_wrapper(view, cls.dispatch, assigned=())
return view 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): def dispatch(self, request, *args, **kwargs):
# Try to dispatch to the right method; if a method doesn't exist, # 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 # defer to the error handler. Also defer to the error handler if the

View File

@ -24,6 +24,7 @@ MRO is an acronym for Method Resolution Order.
**Method Flowchart** **Method Flowchart**
#. :meth:`setup()`
#. :meth:`dispatch()` #. :meth:`dispatch()`
#. :meth:`http_method_not_allowed()` #. :meth:`http_method_not_allowed()`
#. :meth:`options()` #. :meth:`options()`
@ -70,11 +71,22 @@ MRO is an acronym for Method Resolution Order.
attributes. attributes.
When the view is called during the request/response cycle, the When the view is called during the request/response cycle, the
:class:`~django.http.HttpRequest` is assigned to the view's ``request`` :meth:`setup` method assigns the :class:`~django.http.HttpRequest` to
attribute. Any positional and/or keyword arguments :ref:`captured from the view's ``request`` attribute, and any positional and/or keyword
the URL pattern <how-django-processes-a-request>` are assigned to the arguments :ref:`captured from the URL pattern
``args`` and ``kwargs`` attributes, respectively. Then :meth:`dispatch` <how-django-processes-a-request>` to the ``args`` and ``kwargs``
is called. 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) .. method:: dispatch(request, *args, **kwargs)
@ -123,6 +135,7 @@ MRO is an acronym for Method Resolution Order.
**Method Flowchart** **Method Flowchart**
#. :meth:`~django.views.generic.base.View.setup()`
#. :meth:`~django.views.generic.base.View.dispatch()` #. :meth:`~django.views.generic.base.View.dispatch()`
#. :meth:`~django.views.generic.base.View.http_method_not_allowed()` #. :meth:`~django.views.generic.base.View.http_method_not_allowed()`
#. :meth:`~django.views.generic.base.ContextMixin.get_context_data()` #. :meth:`~django.views.generic.base.ContextMixin.get_context_data()`
@ -184,6 +197,7 @@ MRO is an acronym for Method Resolution Order.
**Method Flowchart** **Method Flowchart**
#. :meth:`~django.views.generic.base.View.setup()`
#. :meth:`~django.views.generic.base.View.dispatch()` #. :meth:`~django.views.generic.base.View.dispatch()`
#. :meth:`~django.views.generic.base.View.http_method_not_allowed()` #. :meth:`~django.views.generic.base.View.http_method_not_allowed()`
#. :meth:`get_redirect_url()` #. :meth:`get_redirect_url()`

View File

@ -31,6 +31,7 @@ Simple generic views
* :meth:`~django.views.generic.base.View.dispatch` * :meth:`~django.views.generic.base.View.dispatch`
* ``head()`` * ``head()``
* :meth:`~django.views.generic.base.View.http_method_not_allowed` * :meth:`~django.views.generic.base.View.http_method_not_allowed`
* :meth:`~django.views.generic.base.View.setup`
``TemplateView`` ``TemplateView``
---------------- ----------------
@ -55,6 +56,7 @@ Simple generic views
* ``head()`` * ``head()``
* :meth:`~django.views.generic.base.View.http_method_not_allowed` * :meth:`~django.views.generic.base.View.http_method_not_allowed`
* :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` * :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response`
* :meth:`~django.views.generic.base.View.setup`
``RedirectView`` ``RedirectView``
---------------- ----------------
@ -80,6 +82,7 @@ Simple generic views
* ``options()`` * ``options()``
* ``post()`` * ``post()``
* ``put()`` * ``put()``
* :meth:`~django.views.generic.base.View.setup`
Detail Views Detail Views
============ ============
@ -116,6 +119,7 @@ Detail Views
* ``head()`` * ``head()``
* :meth:`~django.views.generic.base.View.http_method_not_allowed` * :meth:`~django.views.generic.base.View.http_method_not_allowed`
* :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` * :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response`
* :meth:`~django.views.generic.base.View.setup`
List Views List Views
========== ==========
@ -154,6 +158,7 @@ List Views
* :meth:`~django.views.generic.base.View.http_method_not_allowed` * :meth:`~django.views.generic.base.View.http_method_not_allowed`
* :meth:`~django.views.generic.list.MultipleObjectMixin.paginate_queryset` * :meth:`~django.views.generic.list.MultipleObjectMixin.paginate_queryset`
* :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` * :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response`
* :meth:`~django.views.generic.base.View.setup`
Editing views Editing views
============= =============
@ -189,6 +194,7 @@ Editing views
* :meth:`~django.views.generic.base.View.http_method_not_allowed` * :meth:`~django.views.generic.base.View.http_method_not_allowed`
* :meth:`~django.views.generic.edit.ProcessFormView.post` * :meth:`~django.views.generic.edit.ProcessFormView.post`
* :meth:`~django.views.generic.edit.ProcessFormView.put` * :meth:`~django.views.generic.edit.ProcessFormView.put`
* :meth:`~django.views.generic.base.View.setup`
``CreateView`` ``CreateView``
-------------- --------------
@ -233,6 +239,7 @@ Editing views
* :meth:`~django.views.generic.edit.ProcessFormView.post` * :meth:`~django.views.generic.edit.ProcessFormView.post`
* ``put()`` * ``put()``
* :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` * :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response`
* :meth:`~django.views.generic.base.View.setup`
``UpdateView`` ``UpdateView``
-------------- --------------
@ -277,6 +284,7 @@ Editing views
* :meth:`~django.views.generic.edit.ProcessFormView.post` * :meth:`~django.views.generic.edit.ProcessFormView.post`
* ``put()`` * ``put()``
* :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` * :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response`
* :meth:`~django.views.generic.base.View.setup`
``DeleteView`` ``DeleteView``
-------------- --------------
@ -313,6 +321,7 @@ Editing views
* :meth:`~django.views.generic.base.View.http_method_not_allowed` * :meth:`~django.views.generic.base.View.http_method_not_allowed`
* ``post()`` * ``post()``
* :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` * :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response`
* :meth:`~django.views.generic.base.View.setup`
Date-based views Date-based views
================ ================
@ -356,6 +365,7 @@ Date-based views
* :meth:`~django.views.generic.base.View.http_method_not_allowed` * :meth:`~django.views.generic.base.View.http_method_not_allowed`
* :meth:`~django.views.generic.list.MultipleObjectMixin.paginate_queryset` * :meth:`~django.views.generic.list.MultipleObjectMixin.paginate_queryset`
* :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` * :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response`
* :meth:`~django.views.generic.base.View.setup`
``YearArchiveView`` ``YearArchiveView``
------------------- -------------------
@ -399,6 +409,7 @@ Date-based views
* :meth:`~django.views.generic.base.View.http_method_not_allowed` * :meth:`~django.views.generic.base.View.http_method_not_allowed`
* :meth:`~django.views.generic.list.MultipleObjectMixin.paginate_queryset` * :meth:`~django.views.generic.list.MultipleObjectMixin.paginate_queryset`
* :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` * :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response`
* :meth:`~django.views.generic.base.View.setup`
``MonthArchiveView`` ``MonthArchiveView``
-------------------- --------------------
@ -445,6 +456,7 @@ Date-based views
* :meth:`~django.views.generic.base.View.http_method_not_allowed` * :meth:`~django.views.generic.base.View.http_method_not_allowed`
* :meth:`~django.views.generic.list.MultipleObjectMixin.paginate_queryset` * :meth:`~django.views.generic.list.MultipleObjectMixin.paginate_queryset`
* :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` * :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response`
* :meth:`~django.views.generic.base.View.setup`
``WeekArchiveView`` ``WeekArchiveView``
------------------- -------------------
@ -489,6 +501,7 @@ Date-based views
* :meth:`~django.views.generic.base.View.http_method_not_allowed` * :meth:`~django.views.generic.base.View.http_method_not_allowed`
* :meth:`~django.views.generic.list.MultipleObjectMixin.paginate_queryset` * :meth:`~django.views.generic.list.MultipleObjectMixin.paginate_queryset`
* :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` * :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response`
* :meth:`~django.views.generic.base.View.setup`
``DayArchiveView`` ``DayArchiveView``
------------------ ------------------
@ -539,6 +552,7 @@ Date-based views
* :meth:`~django.views.generic.base.View.http_method_not_allowed` * :meth:`~django.views.generic.base.View.http_method_not_allowed`
* :meth:`~django.views.generic.list.MultipleObjectMixin.paginate_queryset` * :meth:`~django.views.generic.list.MultipleObjectMixin.paginate_queryset`
* :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` * :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response`
* :meth:`~django.views.generic.base.View.setup`
``TodayArchiveView`` ``TodayArchiveView``
-------------------- --------------------
@ -589,6 +603,7 @@ Date-based views
* :meth:`~django.views.generic.base.View.http_method_not_allowed` * :meth:`~django.views.generic.base.View.http_method_not_allowed`
* :meth:`~django.views.generic.list.MultipleObjectMixin.paginate_queryset` * :meth:`~django.views.generic.list.MultipleObjectMixin.paginate_queryset`
* :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` * :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response`
* :meth:`~django.views.generic.base.View.setup`
``DateDetailView`` ``DateDetailView``
------------------ ------------------
@ -634,3 +649,4 @@ Date-based views
* ``head()`` * ``head()``
* :meth:`~django.views.generic.base.View.http_method_not_allowed` * :meth:`~django.views.generic.base.View.http_method_not_allowed`
* :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` * :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response`
* :meth:`~django.views.generic.base.View.setup`

View File

@ -25,6 +25,7 @@ many projects they are typically the most commonly used views.
**Method Flowchart** **Method Flowchart**
#. :meth:`~django.views.generic.base.View.setup()`
#. :meth:`~django.views.generic.base.View.dispatch()` #. :meth:`~django.views.generic.base.View.dispatch()`
#. :meth:`~django.views.generic.base.View.http_method_not_allowed()` #. :meth:`~django.views.generic.base.View.http_method_not_allowed()`
#. :meth:`~django.views.generic.base.TemplateResponseMixin.get_template_names()` #. :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** **Method Flowchart**
#. :meth:`~django.views.generic.base.View.setup()`
#. :meth:`~django.views.generic.base.View.dispatch()` #. :meth:`~django.views.generic.base.View.dispatch()`
#. :meth:`~django.views.generic.base.View.http_method_not_allowed()` #. :meth:`~django.views.generic.base.View.http_method_not_allowed()`
#. :meth:`~django.views.generic.base.TemplateResponseMixin.get_template_names()` #. :meth:`~django.views.generic.base.TemplateResponseMixin.get_template_names()`

View File

@ -166,7 +166,10 @@ Forms
Generic Views 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 Internationalization
~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~

View File

@ -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 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 :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 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 associated pattern. The function creates an instance of the class, calls
:meth:`~django.views.generic.base.View.dispatch` method. ``dispatch`` looks at :meth:`~django.views.generic.base.View.setup` to initialize its attributes, and
the request to determine whether it is a ``GET``, ``POST``, etc, and relays the then calls its :meth:`~django.views.generic.base.View.dispatch` method.
request to a matching method if one is defined, or raises ``dispatch`` looks at the request to determine whether it is a ``GET``,
:class:`~django.http.HttpResponseNotAllowed` if not:: ``POST``, etc, and relays the request to a matching method if one is defined,
or raises :class:`~django.http.HttpResponseNotAllowed` if not::
# urls.py # urls.py
from django.urls import path from django.urls import path

View File

@ -233,6 +233,32 @@ class ViewTest(SimpleTestCase):
self.assertNotIn(attribute, dir(bare_view)) self.assertNotIn(attribute, dir(bare_view))
self.assertIn(attribute, dir(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): def test_direct_instantiation(self):
""" """
It should be possible to use the view by directly instantiating it It should be possible to use the view by directly instantiating it