Ticket #14512: ticket14512.4.diff

File ticket14512.4.diff, 8.5 KB (added by roalddevries, 4 years ago)

Same patch as previous, but now complete

  • docs/topics/class-based-views.txt

     
    588588--------------------
    589589
    590590To decorate every instance of a class-based view, you need to decorate
    591 the class definition itself. To do this you apply the decorator to the
    592 :meth:`~django.views.generic.base.View.dispatch` method of the class.
     591the class definition itself. Django provides a way to turn function
     592based view decorators into class based view decorators::
    593593
    594 A method on a class isn't quite the same as a standalone function, so
    595 you can't just apply a function decorator to the method -- you need to
    596 transform it into a method decorator first. The ``method_decorator``
    597 decorator transforms a function decorator into a method decorator so
    598 that it can be used on an instance method. For example::
    599 
    600594    from django.contrib.auth.decorators import login_required
    601     from django.utils.decorators import method_decorator
     595    from django.utils.decorators import view_decorator
    602596    from django.views.generic import TemplateView
    603597
    604     class ProtectedView(TemplateView):
     598    class ProtectedView(TemplateView): 
    605599        template_name = 'secret.html'
     600    ProtectedView = view_decorator(login_required)(ProtectedView)
    606601
    607         @method_decorator(login_required)
    608         def dispatch(self, *args, **kwargs):
    609             return super(ProtectedView, self).dispatch(*args, **kwargs)
     602In Python 2.6 and above you can also use class decorator syntax for this::
    610603
    611 In this example, every instance of ``ProtectedView`` will have
    612 login protection.
     604    from django.contrib.auth.decorators import login_required
     605    from django.utils.decorators import view_decorator
     606    from django.views.generic import TemplateView
    613607
    614 .. note::
     608    @view_decorator(login_required)
     609    class ProtectedView(TemplateView):
     610        template_name = 'secret.html'
    615611
    616     ``method_decorator`` passes ``*args`` and ``**kwargs``
    617     as parameters to the decorated method on the class. If your method
    618     does not accept a compatible set of parameters it will raise a
    619     ``TypeError`` exception.
    620  No newline at end of file
     612In these examples, every instance of ``ProtectedView`` will have
     613login protection.
  • tests/regressiontests/utils/decorators.py

     
    33from django.template import Template, Context
    44from django.template.response import TemplateResponse
    55from django.test import TestCase, RequestFactory
    6 from django.utils.decorators import decorator_from_middleware
     6from django.utils.decorators import decorator_from_middleware, wraps, view_decorator
     7from django.views.generic import View
    78
    89
    910xview_dec = decorator_from_middleware(XViewMiddleware)
     
    104105        self.assertTrue(getattr(request, 'process_response_reached', False))
    105106        # Check that process_response saw the rendered content
    106107        self.assertEqual(request.process_response_content, "Hello world")
     108
     109
     110def simple_dec(func):
     111    """
     112    Simple decorator for testing view_decorator. It assumes a request
     113    argument and one extra argument. Prepends string "decorator:" to the
     114    result.
     115    """
     116    def wrapper(request, arg, func=func):
     117        return "decorator:" + func(request, arg)
     118    wrapper = wraps(func)(wrapper)
     119    wrapper.is_decorated = True
     120    return wrapper
     121
     122
     123class TextView(View):
     124    def __init__(self, *args, **kwargs):
     125        super(TextView, self).__init__(*args, **kwargs)
     126    def get(self, request, text):
     127        return "get1:" + text
     128    def post(self, request, text):
     129        return "post1:" + text
     130TextView = view_decorator(simple_dec)(TextView)
     131
     132
     133class BaseTextView(View):
     134    def __init__(self, *args, **kwargs):
     135        super(BaseTextView, self).__init__(*args, **kwargs)
     136
     137    def get(self, request, text):
     138        return "get1:" + text
     139
     140
     141class ExtendedTextView(BaseTextView):
     142    def __init__(self, **initargs):
     143        self.recursion_count = 0
     144        super(ExtendedTextView, self).__init__(**initargs)
     145
     146    def get(self, *args, **kwargs):
     147        self.recursion_count += 1
     148        if self.recursion_count > 10:
     149            raise Exception("Decoration caused recursive super() calls.")
     150        return "get2:" + super(ExtendedTextView, self).get(*args, **kwargs)
     151ExtendedTextView = view_decorator(simple_dec)(ExtendedTextView)
     152
     153
     154class ClassBasedViewDecorationTests(TestCase):
     155    rf = RequestFactory()
     156
     157    def test_decorate_view(self):
     158        self.assertTrue(getattr(TextView.as_view(), "is_decorated", False),
     159                        "Class based view decorator didn't preserve attributes.")
     160        self.assertEqual(TextView.as_view()(self.rf.get('/'), "hello"),
     161                         "decorator:get1:hello")
     162        self.assertEqual(TextView.as_view()(self.rf.post('/'), "hello"),
     163                         "decorator:post1:hello")
     164
     165    def test_decorate_derived_view(self):
     166        # NOTE: it's important for this test, that the definition
     167        # and decoration of the class happens in the *same scope*.
     168        view = ExtendedTextView.as_view()
     169        result = view(self.rf.get('/'), "A")
     170        self.assertEqual(result, "decorator:get2:get1:A")
     171
     172    def test_base_unmodified(self):
     173        DecoratedView = view_decorator(simple_dec)(BaseTextView)
     174
     175        # since a super(TestView) is called in the __init__ method of the
     176        # following assertion, and the DecorationView instance is no instance of
     177        # TestView, a TypeError is raised. This is a Python subtlety, and
     178        # therefore not the concern of view_decorator.
     179        self.assertRaises(TypeError, DecoratedView.as_view(), (self.rf.get('/'), "A"))
     180
     181        self.assertEqual(BaseTextView.as_view()(self.rf.get('/'), "A"), "get1:A")
     182        self.assertFalse(DecoratedView is BaseTextView)
     183        self.assertEqual(DecoratedView.mro(), [DecoratedView] + BaseTextView.mro()[1:])
     184
  • django/utils/decorators.py

     
    4343    return _dec
    4444
    4545
     46class as_view_wrapper(object):
     47    'An instance of this class should be decorated with classonlymethod'
     48    def __init__(self, decorator, original):
     49        super(as_view_wrapper, self).__init__()
     50        self.decorator = decorator
     51        self.original = original
     52
     53    def __call__(self, *args, **kwargs):
     54        view = self.original(*args, **kwargs)
     55        return self.decorator(view)
     56
     57
     58class view_decorator(object):
     59    '''
     60    Used to convert a function based view decorator into a class based
     61    view decorator. Use it like::
     62
     63        class ProtectedView(View):
     64            pass
     65        ProtectedView = view_decorator(login_required)(ProtectedView)
     66
     67    or::
     68
     69        @view_decorator(login_required)
     70        class ProtectedView(View):
     71            pass
     72    '''
     73    def __init__(self, decorator):
     74        super(view_decorator, self).__init__()
     75        self.decorator = decorator
     76
     77    def __call__(self, view):
     78        '''
     79        The result is a new class with the same base classes as the
     80        original class, and the original class is left unchanged.
     81        '''
     82        # first make a (shallow) copy of the original view
     83        decorated_view = type(view.__name__, view.__bases__, dict(view.__dict__))
     84
     85        # the original classonlymethod `as_view` is associated with the class
     86        # it is defined in, so we first need to get the original function
     87        # definition back
     88        original_as_view_function = decorated_view.as_view.__func__
     89
     90        # make sure the decorated `as_view` classonlymethod uses the right
     91        # decorator and original `as_view` function; we can't rely on the
     92        # dynamic resolution that python uses, so we have to associate them
     93        # with the callable object (see
     94        # docs.python.org/tutorial/classes.html#python-scopes-and-namespaces)
     95        as_view = as_view_wrapper(self.decorator, original_as_view_function)
     96
     97        # now, the decorated `as_view` classonlymethod can replace the
     98        # original one
     99        decorated_view.as_view = classonlymethod(as_view)
     100        return decorated_view
     101
     102
    46103def decorator_from_middleware_with_args(middleware_class):
    47104    """
    48105    Like decorator_from_middleware, but returns a function
Back to Top