Ticket #14512: ticket14512_draft.diff
File ticket14512_draft.diff, 15.0 KB (added by , 14 years ago) |
---|
-
django/utils/decorators.py
diff --git a/django/utils/decorators.py b/django/utils/decorators.py index d75d36f..58be3e8 100644
a b def method_decorator(decorator): 39 39 return _dec 40 40 41 41 42 def view_decorator(fdec, subclass=False): 43 """ 44 Change a function decorator into a view decorator. 45 46 This is a simplest approach possible. `as_view()` is replaced, so 47 that it applies the given decorator before returning. 48 49 In this approach, decorators are always put on top - that means it's not 50 possible to have functions called in this order: 51 52 B.dispatch, login_required, A.dispatch 53 54 NOTE: By default this modifies the class it's called upon, so *MUST NOT* do: 55 56 TemplateView = view_decorator(login_required)(TemplateView) 57 58 Because it will modify the TemplateView class. Instead create a fresh 59 class first and apply the decorator there. A shortcut for this is 60 specifying the `subclass` argument. This is potentially dangerous. Consider: 61 62 @view_decorator(login_required, subclass=True) 63 class MyView(View): 64 65 def get_context_data(self): 66 data = super(MyView, self).get_context_data() 67 data["foo"] = "bar" 68 return data 69 70 This looks like a normal Python code, but there is a hidden infinite 71 recursion, because of how `super()` works in Python 2.x; By the time 72 `get_context_data()` is invoked, MyView refers to a subclass created in 73 the decorator. super() looks at the next class in the MRO of MyView, 74 which is the original MyView class we created, so it contains the 75 `get_context_data()` method. Which is exactly the method that was just 76 called. BOOM! 77 """ 78 79 from django.views.generic.base import classonlymethod 80 def decorator(cls): 81 if subclass: 82 cls = type("%sWithDecorator(%s)" % (cls.__name__, fdec.__name__), (cls,), {}) 83 @wraps(cls.as_view.__func__) 84 def as_view(current, **initkwargs): 85 return fdec(super(cls, current).as_view(**initkwargs)) 86 cls.as_view = classonlymethod(as_view) 87 return cls 88 return decorator 89 90 42 91 def decorator_from_middleware_with_args(middleware_class): 43 92 """ 44 93 Like decorator_from_middleware, but returns a function -
django/views/generic/base.py
diff --git a/django/views/generic/base.py b/django/views/generic/base.py index 19d0415..c1f4dd0 100644
a b from django.template import RequestContext, loader 5 5 from django.utils.translation import ugettext_lazy as _ 6 6 from django.utils.functional import update_wrapper 7 7 from django.utils.log import getLogger 8 from itertools import chain 8 9 9 10 logger = getLogger('django.request') 10 11 … … class View(object): 21 22 """ 22 23 23 24 http_method_names = ['get', 'post', 'put', 'delete', 'head', 'options', 'trace'] 25 decorators = () 24 26 25 27 def __init__(self, **kwargs): 26 28 """ … … class View(object): 32 34 for key, value in kwargs.iteritems(): 33 35 setattr(self, key, value) 34 36 37 @classmethod 38 def get_decorators(cls): 39 decorators = [] 40 # decorators in the subclass should be applied after the ones in base 41 for klass in reversed(cls.__mro__): 42 # decorators defined in the class should be applied in 43 # the reverse order they're specified 44 decorators.extend(reversed(klass.__dict__.get('decorators', ()))) 45 return decorators 46 35 47 @classonlymethod 36 48 def as_view(cls, **initkwargs): 37 49 """ … … class View(object): 57 69 # and possible attributes set by decorators 58 70 # like csrf_exempt from dispatch 59 71 update_wrapper(view, cls.dispatch, assigned=()) 72 73 # apply all defined decorators 74 for decorator in cls.get_decorators(): 75 view = decorator(view) 76 60 77 return view 61 78 62 79 def dispatch(self, request, *args, **kwargs): -
docs/topics/class-based-views.txt
diff --git a/docs/topics/class-based-views.txt b/docs/topics/class-based-views.txt index 821ded6..fe6c109 100644
a b Because of the way that Python resolves method overloading, the local 537 537 :func:`render_to_response()` implementation will override the 538 538 versions provided by :class:`JSONResponseMixin` and 539 539 :class:`~django.views.generic.detail.SingleObjectTemplateResponseMixin`. 540 541 Decorating 542 =========== 543 544 .. highlightlang:: python 545 546 While class-based views and mixins introduce a whole new way of extending 547 view behaviour in a reusable way, you also use existing decorators. 548 549 Simple decoration 550 ------------------ 551 552 The simplest way of decorating class-based views is to decorate the result 553 of the :meth:`~django.views.generic.base.View.as_view` method. This 554 is most useful in the URLconf:: 555 556 from django.views.generic import TemplateView 557 from django.contrib.auth.decorators import login_required 558 559 urlpatterns = patterns('', 560 (r'^about/',login_required(TemplateView.as_view(template_name="secret.html"))), 561 ) 562 563 The downside of this approach is that you can't define a class-based view with 564 the decorator always applied to it. 565 566 Overriding ``as_view`` class method 567 ------------------------------------ 568 569 If you want your view to have a decorator applied and be able to subclass 570 it later, you need to override one of it's methods. The most straightforward 571 solution is to override :meth:`~django.views.generic.base.View.as_view`:: 572 573 from django.views.generic import TemplateView, classonlymethod 574 575 class ProtectedView(TemplateView): 576 577 @classonlymethod 578 def as_view(cls, **initkwargs): 579 return login_required(super(ProtectedView, cls).as_view(**initkwargs)) 580 581 This will make the ``ProtectedView`` and any of it's subclasses always apply the 582 decorator. You don't need to worry about ``classonlymethod`` decorator. 583 It's not required for your view to work, but prevents from invoking 584 ``as_view()`` on the view's instance by accident. 585 586 587 .. note:: 588 589 This way of decorating (as the previous one) always applies the decorators 590 at creation time, before any code in `dispatch()` has any chance to run. 591 592 Class decorator 593 -------------------- 594 595 Python 2.6 introduces new syntax that allows you to decorate classes. Django 596 provides such a decorator for class-based views. The example above can 597 be simplified to:: 598 599 from django.utils.decorators import view_decorator 600 601 @view_decorator(login_required) 602 class ProtectedView(TemplateView) 603 pass 604 605 .. note:: 606 607 The decorator modifies the class it was given, so it is **not safe** to decorate 608 generic views this way without creating a subclass. 609 610 If you are absolutly sure you know how super() and MRO works in Python, you 611 can pass ``subclass=True`` to the decorator and it will create a sublass 612 for you. You have been warned! -
tests/regressiontests/generic_views/base.py
diff --git a/tests/regressiontests/generic_views/base.py b/tests/regressiontests/generic_views/base.py index a1da986..1702d5b 100644
a b from django.test import TestCase, RequestFactory 6 6 from django.utils import simplejson 7 7 from django.views.generic import View, TemplateView, RedirectView 8 8 9 def prepend_string(text): 10 "Decorator that prepends strings to responses" 11 from django.utils.functional import wraps 12 def decorator(view): 13 @wraps(view) 14 def decorated_view(request, *args, **kwargs): 15 return text + ':' + view(request, *args, **kwargs) 16 return decorated_view 17 return decorator 9 18 10 19 class SimpleView(View): 11 20 """ … … class ViewTest(unittest.TestCase): 149 158 """ 150 159 self.assertTrue(DecoratedDispatchView.as_view().is_decorated) 151 160 161 def test_defining_decorators(self): 162 """ 163 Test an alternate method of decoration. 164 """ 165 class Base(View): 166 def get(self, request, *args, **kwargs): 167 return "Base" 168 169 class B(Base): 170 decorators = (prepend_string('B'),) 171 172 class AB(B): 173 decorators = (prepend_string('A'),) 174 175 class CD(Base): 176 # decorators are applied in reverse order 177 decorators = (prepend_string('C'), prepend_string('D')) 178 179 class ABCD(AB, CD): 180 pass 181 182 self.assertEqual(Base.as_view()(self.rf.get('/')), "Base") 183 self.assertEqual(B.as_view()(self.rf.get('/')), "B:Base") 184 self.assertEqual(AB.as_view()(self.rf.get('/')), "A:B:Base") 185 self.assertEqual(CD.as_view()(self.rf.get('/')), "C:D:Base") 186 self.assertEqual(ABCD.as_view()(self.rf.get('/')), "A:B:C:D:Base") 152 187 153 188 class TemplateViewTest(TestCase): 154 189 urls = 'regressiontests.generic_views.urls' -
tests/regressiontests/utils/decorators.py
diff --git a/tests/regressiontests/utils/decorators.py b/tests/regressiontests/utils/decorators.py index ca9214f..74a29da 100644
a b 1 from django.test import TestCase 1 from django.test import TestCase, RequestFactory 2 from django.utils import unittest 3 from django.utils.decorators import method_decorator, wraps, view_decorator 4 from django.views.generic import View 5 2 6 3 7 class DecoratorFromMiddlewareTests(TestCase): 4 8 """ … … class DecoratorFromMiddlewareTests(TestCase): 17 21 Test a middleware that implements process_view, operating on a callable class. 18 22 """ 19 23 self.client.get('/utils/class_xview/') 24 25 26 def simple_dec(func): 27 """ 28 Simple decotator for testing method_decorator and view_decorator. 29 It assumes a request argument and one extra argument. Appends 30 string "decorator:" to the result. It also sets `is_decorated` attribute 31 on the wrappper. 32 """ 33 def wrapper(request, arg): 34 return "decorator:" + func(request, arg) 35 wrapper = wraps(func)(wrapper) 36 wrapper.is_decorated = True 37 return wrapper 38 39 40 class MethodDecoratorTests(TestCase): 41 """ 42 Tests for method_decorator. 43 """ 44 45 def test_method_decorator(self): 46 simple_dec_m = method_decorator(simple_dec) 47 48 class Test(object): 49 @simple_dec_m 50 def say(self, request, arg): 51 return arg 52 53 self.assertEqual("decorator:hello", Test().say(None, "hello")) 54 self.assertTrue(getattr(Test().say, "is_decorated", False), 55 "Method decorator didn't preserve attributes.") 56 57 58 class ClassBasedViewDecorationTests(TestCase): 59 rf = RequestFactory() 60 61 def test_decorate_view(self): 62 class TextView(View): 63 "Docstring" 64 def get(self, request, text): 65 return "get:" + text 66 def post(self, request, text): 67 return "post:" + text 68 TextView = view_decorator(simple_dec)(TextView) 69 70 self.assertTrue(getattr(TextView.as_view(), "is_decorated", False), 71 "Class based view decorator didn't preserve attributes.") 72 self.assertEqual(TextView.as_view().__doc__, "Docstring", 73 "Class based view decorator didn't preserve docstring.") 74 self.assertEqual(TextView.as_view()(self.rf.get('/'), "hello"), "decorator:get:hello") 75 self.assertEqual(TextView.as_view()(self.rf.post('/'), "hello"), "decorator:post:hello") 76 77 def test_super_calls(self): 78 class TextView(View): 79 def dispatch(self, request, text): 80 return "view1:" + text 81 82 # NOTE: it's important for this test, that the definition 83 # and decoration of the class happens in the *same scope*. 84 class ViewWithSuper(TextView): 85 86 def __init__(self, **initargs): 87 self.recursion_count = 0 88 super(ViewWithSuper, self).__init__(**initargs) 89 90 def dispatch(self, *args, **kwargs): 91 self.recursion_count += 1 92 if self.recursion_count > 10: 93 raise Exception("Decoration caused recursive super() calls.") 94 return "view2:" + super(ViewWithSuper, self).dispatch(*args, **kwargs) 95 ViewWithSuper = view_decorator(simple_dec)(ViewWithSuper) 96 97 self.assertEqual(ViewWithSuper.as_view()(self.rf.get('/'), "A"), "decorator:view2:view1:A") 98 99 @unittest.expectedFailure 100 def test_super_calls_with_subclassing(self): 101 class TextView(View): 102 def dispatch(self, request, text): 103 return "view1:" + text 104 105 # NOTE: it's important for this test, that the definition 106 # and decoration of the class happens in the *same scope*. 107 class ViewWithSuper(TextView): 108 109 def __init__(self, **initargs): 110 self.recursion_count = 0 111 super(ViewWithSuper, self).__init__(**initargs) 112 113 def dispatch(self, *args, **kwargs): 114 self.recursion_count += 1 115 if self.recursion_count > 10: 116 raise RuntimeError("Decoration caused recursive super() calls.") 117 return "view2:" + super(ViewWithSuper, self).dispatch(*args, **kwargs) 118 ViewWithSuper = view_decorator(simple_dec, subclassing=True)(ViewWithSuper) 119 120 self.assertEqual(ViewWithSuper.as_view()(self.rf.get('/'), "A"), "decorator:view2:view1:A") 121 122 def test_subclassing_decorated(self): 123 """ 124 Test that decorators are always pushed to front. 125 """ 126 class TextView(View): 127 def dispatch(self, request, text): 128 return "view1:" + text 129 TextView = view_decorator(simple_dec)(TextView) 130 131 class SubView(TextView): 132 def dispatch(self, *args, **kwargs): 133 return "view2:" + super(SubView, self).dispatch(*args, **kwargs) 134 135 self.assertEqual(SubView.as_view()(self.rf.get('/'), "A"), "decorator:view2:view1:A") 136 137 @unittest.expectedFailure 138 def test_base_unmodified(self): 139 class TextView(View): 140 attr = "OK" 141 def dispatch(self, request, text): 142 return "view1:" + text 143 DecoratedView = view_decorator(simple_dec)(TextView) 144 self.assertEqual(DecoratedView.as_view()(self.rf.get('/'), "A"), "decorator:view1:A") 145 self.assertEqual(TextView.as_view()(self.rf.get('/'), "A"), "view1:A") 146 self.assertFalse(DecoratedView is TextView) 147 self.assertEqual(DecoratedView.mro(), [DecoratedView, TextView, View, object]) 148 149 def test_base_unmodified_with_subclassing(self): 150 class TextView(View): 151 attr = "OK" 152 def dispatch(self, request, text): 153 return "view1:" + text 154 DecoratedView = view_decorator(simple_dec, subclass=True)(TextView) 155 156 self.assertEqual(DecoratedView.as_view()(self.rf.get('/'), "A"), "decorator:view1:A") 157 self.assertEqual(TextView.as_view()(self.rf.get('/'), "A"), "view1:A") 158 self.assertFalse(DecoratedView is TextView) 159 self.assertEqual(DecoratedView.mro(), [DecoratedView, TextView, View, object])