Code

Ticket #14512: ticket14512.2.diff

File ticket14512.2.diff, 7.5 KB (added by roalddevries, 3 years ago)

patch based on release 1.3.1

Line 
1Index: docs/topics/class-based-views.txt
2===================================================================
3--- docs/topics/class-based-views.txt   (revision 16790)
4+++ docs/topics/class-based-views.txt   (working copy)
5@@ -588,32 +588,27 @@
6 --------------------
7 
8 To decorate every instance of a class-based view, you need to decorate
9-the class definition itself. To do this you apply the decorator to the
10-:meth:`~django.views.generic.base.View.dispatch` method of the class.
11+the class definition itself. Django provides a way to turn function
12+based view decorators into class based view decorators::
13 
14-A method on a class isn't quite the same as a standalone function, so
15-you can't just apply a function decorator to the method -- you need to
16-transform it into a method decorator first. The ``method_decorator``
17-decorator transforms a function decorator into a method decorator so
18-that it can be used on an instance method. For example::
19-
20     from django.contrib.auth.decorators import login_required
21-    from django.utils.decorators import method_decorator
22+    from django.utils.decorators import view_decorator
23     from django.views.generic import TemplateView
24 
25-    class ProtectedView(TemplateView):
26+    class ProtectedView(TemplateView):
27         template_name = 'secret.html'
28+    ProtectedView = view_decorator(login_required)(ProtectedView)
29 
30-        @method_decorator(login_required)
31-        def dispatch(self, *args, **kwargs):
32-            return super(ProtectedView, self).dispatch(*args, **kwargs)
33+In Python 2.6 and above you can also use class decorator syntax for this::
34 
35-In this example, every instance of ``ProtectedView`` will have
36-login protection.
37+    from django.contrib.auth.decorators import login_required
38+    from django.utils.decorators import view_decorator
39+    from django.views.generic import TemplateView
40 
41-.. note::
42+    @view_decorator(login_required)
43+    class ProtectedView(TemplateView):
44+        template_name = 'secret.html'
45+    ProtectedView = view_decorator(login_required)(ProtectedView)
46 
47-    ``method_decorator`` passes ``*args`` and ``**kwargs``
48-    as parameters to the decorated method on the class. If your method
49-    does not accept a compatible set of parameters it will raise a
50-    ``TypeError`` exception.
51\ No newline at end of file
52+In these examples, every instance of ``ProtectedView`` will have
53+login protection.
54Index: tests/regressiontests/utils/decorators.py
55===================================================================
56--- tests/regressiontests/utils/decorators.py   (revision 16790)
57+++ tests/regressiontests/utils/decorators.py   (working copy)
58@@ -3,7 +3,8 @@
59 from django.template import Template, Context
60 from django.template.response import TemplateResponse
61 from django.test import TestCase, RequestFactory
62-from django.utils.decorators import decorator_from_middleware
63+from django.utils.decorators import decorator_from_middleware, wraps, view_decorator
64+from django.views.generic import View
65 
66 
67 xview_dec = decorator_from_middleware(XViewMiddleware)
68@@ -104,3 +105,73 @@
69         self.assertTrue(getattr(request, 'process_response_reached', False))
70         # Check that process_response saw the rendered content
71         self.assertEqual(request.process_response_content, "Hello world")
72+
73+
74+def simple_dec(func):
75+    """
76+    Simple decorator for testing view_decorator. It assumes a request
77+    argument and one extra argument. Prepends string "decorator:" to the
78+    result.
79+    """
80+    def wrapper(request, arg):
81+        return "decorator:" + func(request, arg)
82+    wrapper = wraps(func)(wrapper)
83+    wrapper.is_decorated = True
84+    return wrapper
85+
86+
87+class ClassBasedViewDecorationTests(TestCase):
88+    rf = RequestFactory()
89+
90+    def setUp(self):
91+        class TextView(View):
92+            attr = "OK"
93+            def __init__(self, *args, **kwargs):
94+                super(TextView, self).__init__(*args, **kwargs)
95+            def dispatch(self, request, text):
96+                return "view1:" + text
97+        self.TextView = TextView
98+
99+    def test_decorate_view(self):
100+        class TextView(View):
101+            def get(self, request, text):
102+                return "get:" + text
103+            def post(self, request, text):
104+                return "post:" + text
105+        TextView = view_decorator(simple_dec)(TextView)
106+
107+        self.assertTrue(getattr(TextView.as_view(), "is_decorated", False),
108+                    "Class based view decorator didn't preserve attributes.")
109+        self.assertEqual(TextView.as_view()(self.rf.get('/'), "hello"), "decorator:get:hello")
110+        self.assertEqual(TextView.as_view()(self.rf.post('/'), "hello"), "decorator:post:hello")
111+
112+    def test_super_calls(self):
113+        # NOTE: it's important for this test, that the definition
114+        # and decoration of the class happens in the *same scope*.
115+        class ViewWithSuper(self.TextView):
116+            def __init__(self, **initargs):
117+                self.recursion_count = 0
118+                super(ViewWithSuper, self).__init__(**initargs)
119+
120+            def dispatch(self, *args, **kwargs):
121+                self.recursion_count += 1
122+                if self.recursion_count > 10:
123+                    raise Exception("Decoration caused recursive super() calls.")
124+                return "view2:" + super(ViewWithSuper, self).dispatch(*args, **kwargs)
125+        ViewWithSuper = view_decorator(simple_dec)(ViewWithSuper)
126+
127+        self.assertEqual(ViewWithSuper.as_view()(self.rf.get('/'), "A"), "decorator:view2:view1:A")
128+
129+    def test_base_unmodified(self):
130+        DecoratedView = view_decorator(simple_dec)(self.TextView)
131+
132+        # since a super(TestView) is called in the __init__ method of the following
133+        # assertion, and the DecorationView instance is no instance of TestView,
134+        # a TypeError is raised. This is a Python subtlety, and therefore not the
135+        # concern of view_decorator.
136+        self.assertRaises(TypeError, DecoratedView.as_view(), (self.rf.get('/'), "A"))
137+
138+        self.assertEqual(self.TextView.as_view()(self.rf.get('/'), "A"), "view1:A")
139+        self.assertFalse(DecoratedView is self.TextView)
140+        self.assertEqual(DecoratedView.mro(), [DecoratedView] + self.TextView.mro()[1:])
141+
142Index: django/utils/decorators.py
143===================================================================
144--- django/utils/decorators.py  (revision 16790)
145+++ django/utils/decorators.py  (working copy)
146@@ -43,6 +43,38 @@
147     return _dec
148 
149 
150+class view_decorator(object):
151+    '''
152+    Used to convert a function based view decorator into a class based
153+    view decorator. Use it like::
154+
155+        class ProtectedView(View):
156+            pass
157+        ProtectedView = view_decorator(login_required)(ProtectedView)
158+
159+    or::
160+
161+        @view_decorator(login_required)
162+        class ProtectedView(View):
163+            pass
164+    '''
165+    def __init__(self, decorator):
166+        super(view_decorator, self).__init__()
167+        self.decorator = decorator
168+
169+    def __call__(self, cbv):
170+        '''
171+        The result is a new class with the same base classes as the
172+        original class, and the original class is left unchanged.
173+        '''
174+        decorated_cbv = type(cbv.__name__, cbv.__bases__, dict(cbv.__dict__))
175+        @classonlymethod
176+        def as_view(cls, decorator=self.decorator, original=decorated_cbv.as_view):
177+            return decorator(original())
178+        decorated_cbv.as_view = as_view
179+        return decorated_cbv
180+
181+
182 def decorator_from_middleware_with_args(middleware_class):
183     """
184     Like decorator_from_middleware, but returns a function