Code

Ticket #14512: ticket14512.diff

File ticket14512.diff, 13.8 KB (added by lrekucki, 3 years ago)

Cleaned up the patch a bit.

Line 
1diff --git a/django/utils/decorators.py b/django/utils/decorators.py
2index d75d36f..afb011e 100644
3--- a/django/utils/decorators.py
4+++ b/django/utils/decorators.py
5@@ -1,11 +1,15 @@
6 "Functions that help with dynamically creating decorators for views."
7 
8-import types
9 try:
10     from functools import wraps, update_wrapper, WRAPPER_ASSIGNMENTS
11 except ImportError:
12     from django.utils.functional import wraps, update_wrapper, WRAPPER_ASSIGNMENTS  # Python 2.4 fallback.
13 
14+class classonlymethod(classmethod):
15+    def __get__(self, instance, owner):
16+        if instance is not None:
17+            raise AttributeError("This method is available only on the view class.")
18+        return super(classonlymethod, self).__get__(instance, owner)
19 
20 def method_decorator(decorator):
21     """
22@@ -39,6 +43,53 @@ def method_decorator(decorator):
23     return _dec
24 
25 
26+def view_decorator(fdec, subclass=False):
27+    """
28+    Change a function decorator into a view decorator.
29+
30+    This is a simplest approach possible. `as_view()` is replaced, so
31+    that it applies the given decorator before returning.
32+
33+    In this approach, decorators are always put on top - that means it's not
34+    possible to have functions called in this order:
35+
36+       B.dispatch, login_required, A.dispatch
37+
38+    NOTE: By default this modifies the given class, so be careful when doing this:
39+
40+       TemplateView = view_decorator(login_required)(TemplateView)
41+
42+    Because it will modify the TemplateView class. Instead create a fresh
43+    class first and apply the decorator there. A shortcut for this is
44+    specifying the ``subclass`` argument. But this is also dangerous. Consider:
45+
46+        @view_decorator(login_required, subclass=True)
47+        class MyView(View):
48+
49+            def get_context_data(self):
50+                data = super(MyView, self).get_context_data()
51+                data["foo"] = "bar"
52+                return data
53+
54+    This looks like a normal Python code, but there is a hidden infinite
55+    recursion, because of how `super()` works in Python 2.x; By the time
56+    `get_context_data()` is invoked, MyView refers to a subclass created in
57+    the decorator. super() looks at the next class in the MRO of MyView,
58+    which is the original MyView class we created, so it contains the
59+    `get_context_data()` method. Which is exactly the method that was just
60+    called. BOOM!
61+    """
62+    def decorator(cls):
63+        if subclass:
64+            cls = type("%sWithDecorator(%s)" % (cls.__name__, fdec.__name__), (cls,), {})
65+        @wraps(cls.as_view)
66+        def as_view(current, **initkwargs):
67+            return fdec(super(cls, current).as_view(**initkwargs))
68+        cls.as_view = classonlymethod(as_view)
69+        return cls
70+    return decorator
71+
72+
73 def decorator_from_middleware_with_args(middleware_class):
74     """
75     Like decorator_from_middleware, but returns a function
76diff --git a/django/views/generic/base.py b/django/views/generic/base.py
77index 19d0415..0c9cd79 100644
78--- a/django/views/generic/base.py
79+++ b/django/views/generic/base.py
80@@ -1,19 +1,12 @@
81-import copy
82 from django import http
83 from django.core.exceptions import ImproperlyConfigured
84 from django.template import RequestContext, loader
85-from django.utils.translation import ugettext_lazy as _
86 from django.utils.functional import update_wrapper
87 from django.utils.log import getLogger
88+from django.utils.decorators import classonlymethod
89 
90 logger = getLogger('django.request')
91 
92-class classonlymethod(classmethod):
93-    def __get__(self, instance, owner):
94-        if instance is not None:
95-            raise AttributeError("This method is available only on the view class.")
96-        return super(classonlymethod, self).__get__(instance, owner)
97-
98 class View(object):
99     """
100     Intentionally simple parent class for all views. Only implements
101diff --git a/docs/topics/class-based-views.txt b/docs/topics/class-based-views.txt
102index f0e4910..56bc143 100644
103--- a/docs/topics/class-based-views.txt
104+++ b/docs/topics/class-based-views.txt
105@@ -537,3 +537,85 @@ Because of the way that Python resolves method overloading, the local
106 :func:`render_to_response()` implementation will override the
107 versions provided by :class:`JSONResponseMixin` and
108 :class:`~django.views.generic.detail.SingleObjectTemplateResponseMixin`.
109+
110+Decorating
111+===========
112+
113+.. highlightlang:: python
114+
115+While class-based views and mixins introduce a whole new way of extending
116+view behaviour in a reusable way, you also use existing decorators.
117+
118+
119+Decorating in URLconf
120+-----------------------
121+
122+The simplest way of decorating class-based views is to decorate the result
123+of the :meth:`~django.views.generic.base.View.as_view` method. This
124+is most useful in the URLconf::
125+
126+    from django.views.generic import TemplateView
127+    from django.contrib.auth.decorators import login_required
128+
129+    urlpatterns = patterns('',
130+        (r'^about/',login_required(TemplateView.as_view(template_name="secret.html"))),
131+    )
132+
133+The downside of this approach is that you can't define a class-based view with
134+the decorator always applied to it.
135+
136+
137+Overriding ``as_view`` class method
138+--------------------------------------
139+
140+If you want your view to have a decorator applied and be able to subclass
141+it later, you need to override one of it's methods. The most straightforward
142+solution is to override :meth:`~django.views.generic.base.View.as_view`::
143+
144+    from django.views.generic import TemplateView
145+    from django.utils.decorators import classonlymethod
146+
147+    class ProtectedView(TemplateView):
148+
149+        @classonlymethod
150+        def as_view(cls, **initkwargs):
151+            return login_required(super(ProtectedView, cls).as_view(**initkwargs))
152+
153+This will make the ``ProtectedView`` and any of it's subclasses always apply the
154+``login_required`` decorator. The  ``classonlymethod`` decorator is a variation
155+of the standard Python ``classmethod`` decorator that lets you invoke the
156+method directly from the class, but not from an instance.
157+
158+
159+Class decorators
160+-----------------
161+
162+Django provides also provides a way to turn function decorators into
163+class decorators::
164+
165+    from django.utils.decorators import view_decorator
166+
167+    class ProtectedView(TemplateView):
168+        pass
169+    ProtectedView = view_decorator(login_required)(ProtectedView)
170+
171+In Python 2.6 and above you can also use class decorator syntax for this::
172+
173+    from django.utils.decorators import view_decorator
174+
175+    @view_decorator(login_required)
176+    class ProtectedView(TempalateView):
177+        pass
178+
179+.. note::
180+
181+    The produced class decorator modifies the class it was given, so it is
182+    *not safe* to decorate generic views this way **without creating a subclass**.
183+
184+    If you are absolutely sure you know how super() and MRO works in Python, you
185+    can pass a ``subclass`` keyword to make the decorator create an inline
186+    subclass for you::
187+
188+        ProtectedView = view_decorator(login_required, subclass=True)(TemplateView)
189+
190+    You have been warned!
191diff --git a/tests/regressiontests/generic_views/base.py b/tests/regressiontests/generic_views/base.py
192index a1da986..d419acc 100644
193--- a/tests/regressiontests/generic_views/base.py
194+++ b/tests/regressiontests/generic_views/base.py
195@@ -22,6 +22,7 @@ class SimplePostView(SimpleView):
196 class CustomizableView(SimpleView):
197     parameter = {}
198 
199+
200 def decorator(view):
201     view.is_decorated = True
202     return view
203@@ -149,7 +150,6 @@ class ViewTest(unittest.TestCase):
204         """
205         self.assertTrue(DecoratedDispatchView.as_view().is_decorated)
206 
207-
208 class TemplateViewTest(TestCase):
209     urls = 'regressiontests.generic_views.urls'
210 
211diff --git a/tests/regressiontests/utils/decorators.py b/tests/regressiontests/utils/decorators.py
212index ca9214f..fddc32e 100644
213--- a/tests/regressiontests/utils/decorators.py
214+++ b/tests/regressiontests/utils/decorators.py
215@@ -1,4 +1,8 @@
216-from django.test import TestCase
217+from django.test import TestCase, RequestFactory
218+from django.utils import unittest
219+from django.utils.decorators import method_decorator, wraps, view_decorator
220+from django.views.generic import View
221+
222 
223 class DecoratorFromMiddlewareTests(TestCase):
224     """
225@@ -17,3 +21,139 @@ class DecoratorFromMiddlewareTests(TestCase):
226         Test a middleware that implements process_view, operating on a callable class.
227         """
228         self.client.get('/utils/class_xview/')
229+
230+
231+def simple_dec(func):
232+    """
233+    Simple decotator for testing method_decorator and view_decorator.
234+    It assumes a request argument and one extra argument.  Appends
235+    string "decorator:" to the result. It also sets `is_decorated` attribute
236+    on the wrappper.
237+    """
238+    def wrapper(request, arg):
239+        return "decorator:" + func(request, arg)
240+    wrapper = wraps(func)(wrapper)
241+    wrapper.is_decorated = True
242+    return wrapper
243+
244+
245+class MethodDecoratorTests(TestCase):
246+    """
247+    Tests for method_decorator.
248+    """
249+
250+    def test_method_decorator(self):
251+        simple_dec_m = method_decorator(simple_dec)
252+
253+        class Test(object):
254+            @simple_dec_m
255+            def say(self, request, arg):
256+                return arg
257+
258+        self.assertEqual("decorator:hello", Test().say(None, "hello"))
259+        self.assertTrue(getattr(Test().say, "is_decorated", False),
260+                "Method decorator didn't preserve attributes.")
261+
262+
263+class ClassBasedViewDecorationTests(TestCase):
264+    rf = RequestFactory()
265+
266+    def test_decorate_view(self):
267+        class TextView(View):
268+            "Docstring"
269+            def get(self, request, text):
270+                return "get:" + text
271+            def post(self, request, text):
272+                return "post:" + text
273+        TextView = view_decorator(simple_dec)(TextView)
274+
275+        self.assertTrue(getattr(TextView.as_view(), "is_decorated", False),
276+                    "Class based view decorator didn't preserve attributes.")
277+        self.assertEqual(TextView.as_view().__doc__, "Docstring",
278+                    "Class based view decorator didn't preserve docstring.")
279+        self.assertEqual(TextView.as_view()(self.rf.get('/'), "hello"), "decorator:get:hello")
280+        self.assertEqual(TextView.as_view()(self.rf.post('/'), "hello"), "decorator:post:hello")
281+
282+    def test_super_calls(self):
283+        class TextView(View):
284+            def dispatch(self, request, text):
285+                return "view1:" + text
286+
287+        # NOTE: it's important for this test, that the definition
288+        # and decoration of the class happens in the *same scope*.
289+        class ViewWithSuper(TextView):
290+
291+            def __init__(self, **initargs):
292+                self.recursion_count = 0
293+                super(ViewWithSuper, self).__init__(**initargs)
294+
295+            def dispatch(self, *args, **kwargs):
296+                self.recursion_count += 1
297+                if self.recursion_count > 10:
298+                    raise Exception("Decoration caused recursive super() calls.")
299+                return "view2:" + super(ViewWithSuper, self).dispatch(*args, **kwargs)
300+        ViewWithSuper = view_decorator(simple_dec)(ViewWithSuper)
301+
302+        self.assertEqual(ViewWithSuper.as_view()(self.rf.get('/'), "A"), "decorator:view2:view1:A")
303+
304+    @unittest.expectedFailure
305+    def test_super_calls_with_subclassing(self):
306+        class TextView(View):
307+            def dispatch(self, request, text):
308+                return "view1:" + text
309+
310+        # NOTE: it's important for this test, that the definition
311+        # and decoration of the class happens in the *same scope*.
312+        class ViewWithSuper(TextView):
313+
314+            def __init__(self, **initargs):
315+                self.recursion_count = 0
316+                super(ViewWithSuper, self).__init__(**initargs)
317+
318+            def dispatch(self, *args, **kwargs):
319+                self.recursion_count += 1
320+                if self.recursion_count > 10:
321+                    raise RuntimeError("Decoration caused recursive super() calls.")
322+                return "view2:" + super(ViewWithSuper, self).dispatch(*args, **kwargs)
323+        ViewWithSuper = view_decorator(simple_dec, subclass=True)(ViewWithSuper)
324+
325+        self.assertEqual(ViewWithSuper.as_view()(self.rf.get('/'), "A"), "decorator:view2:view1:A")
326+
327+    def test_subclassing_decorated(self):
328+        """
329+        Test that decorators are always pushed to front.
330+        """
331+        class TextView(View):
332+            def dispatch(self, request, text):
333+                return "view1:" + text
334+        TextView = view_decorator(simple_dec)(TextView)
335+
336+        class SubView(TextView):
337+            def dispatch(self, *args, **kwargs):
338+                return "view2:" + super(SubView, self).dispatch(*args, **kwargs)
339+
340+        self.assertEqual(SubView.as_view()(self.rf.get('/'), "A"), "decorator:view2:view1:A")
341+
342+    @unittest.expectedFailure
343+    def test_base_unmodified(self):
344+        class TextView(View):
345+            attr = "OK"
346+            def dispatch(self, request, text):
347+                return "view1:" + text
348+        DecoratedView = view_decorator(simple_dec)(TextView)
349+        self.assertEqual(DecoratedView.as_view()(self.rf.get('/'), "A"), "decorator:view1:A")
350+        self.assertEqual(TextView.as_view()(self.rf.get('/'), "A"), "view1:A")
351+        self.assertFalse(DecoratedView is TextView)
352+        self.assertEqual(DecoratedView.mro(), [DecoratedView, TextView, View, object])
353+
354+    def test_base_unmodified_with_subclassing(self):
355+        class TextView(View):
356+            attr = "OK"
357+            def dispatch(self, request, text):
358+                return "view1:" + text
359+        DecoratedView = view_decorator(simple_dec, subclass=True)(TextView)
360+
361+        self.assertEqual(DecoratedView.as_view()(self.rf.get('/'), "A"), "decorator:view1:A")
362+        self.assertEqual(TextView.as_view()(self.rf.get('/'), "A"), "view1:A")
363+        self.assertFalse(DecoratedView is TextView)
364+        self.assertEqual(DecoratedView.mro(), [DecoratedView, TextView, View, object])