Fixed #29750 -- Added View.setup() hook for class-based views.
This commit is contained in:
parent
19e863a844
commit
e671337e8b
|
@ -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
|
||||||
|
|
|
@ -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()`
|
||||||
|
|
|
@ -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`
|
||||||
|
|
|
@ -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()`
|
||||||
|
|
|
@ -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
|
||||||
~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue