Author: lukeplant
Date: 2010-02-09 09:02:39 -0600 (Tue, 09 Feb 2010)
New Revision: 12399

Modified:
   django/trunk/django/contrib/admin/options.py
   django/trunk/django/contrib/auth/admin.py
   django/trunk/django/contrib/auth/decorators.py
   django/trunk/django/contrib/formtools/wizard.py
   django/trunk/django/utils/decorators.py
   django/trunk/django/views/decorators/cache.py
   django/trunk/docs/releases/1.2.txt
   django/trunk/tests/modeltests/test_client/views.py
   django/trunk/tests/regressiontests/decorators/tests.py
Log:
Fixed #12804 - regression with decorating admin views.

This is a BACKWARDS INCOMPATIBLE change, because it removes the flawed
'auto_adapt_to_methods' decorator, and replaces it with 'method_decorator'
which must be applied manually when necessary, as described in the 1.2
release notes.

For users of 1.1 and 1.0, this affects the decorators:

 * login_required
 * permission_required
 * user_passes_test

For those following trunk, this also affects:

 * csrf_protect
 * anything created with decorator_from_middleware 

If a decorator does not depend on the signature of the function it is
supposed to decorate (for example if it only does post-processing of the
result), it will not be affected.
 



Modified: django/trunk/django/contrib/admin/options.py
===================================================================
--- django/trunk/django/contrib/admin/options.py        2010-02-07 15:55:40 UTC 
(rev 12398)
+++ django/trunk/django/contrib/admin/options.py        2010-02-09 15:02:39 UTC 
(rev 12399)
@@ -13,6 +13,7 @@
 from django.db.models.fields import BLANK_CHOICE_DASH
 from django.http import Http404, HttpResponse, HttpResponseRedirect
 from django.shortcuts import get_object_or_404, render_to_response
+from django.utils.decorators import method_decorator
 from django.utils.datastructures import SortedDict
 from django.utils.functional import update_wrapper
 from django.utils.html import escape
@@ -53,6 +54,7 @@
     models.FileField:       {'widget': widgets.AdminFileWidget},
 }
 
+csrf_protect_m = method_decorator(csrf_protect)
 
 class BaseModelAdmin(object):
     """Functionality common to both ModelAdmin and InlineAdmin."""
@@ -754,7 +756,7 @@
             msg = _("No action selected.")
             self.message_user(request, msg)
 
-    @csrf_protect
+    @csrf_protect_m
     @transaction.commit_on_success
     def add_view(self, request, form_url='', extra_context=None):
         "The 'add' admin view for this model."
@@ -844,7 +846,7 @@
         context.update(extra_context or {})
         return self.render_change_form(request, context, form_url=form_url, 
add=True)
 
-    @csrf_protect
+    @csrf_protect_m
     @transaction.commit_on_success
     def change_view(self, request, object_id, extra_context=None):
         "The 'change' admin view for this model."
@@ -936,7 +938,7 @@
         context.update(extra_context or {})
         return self.render_change_form(request, context, change=True, obj=obj)
 
-    @csrf_protect
+    @csrf_protect_m
     def changelist_view(self, request, extra_context=None):
         "The 'change list' admin view for this model."
         from django.contrib.admin.views.main import ERROR_FLAG
@@ -1057,7 +1059,7 @@
             'admin/change_list.html'
         ], context, context_instance=context_instance)
 
-    @csrf_protect
+    @csrf_protect_m
     def delete_view(self, request, object_id, extra_context=None):
         "The 'delete' admin view for this model."
         opts = self.model._meta

Modified: django/trunk/django/contrib/auth/admin.py
===================================================================
--- django/trunk/django/contrib/auth/admin.py   2010-02-07 15:55:40 UTC (rev 
12398)
+++ django/trunk/django/contrib/auth/admin.py   2010-02-09 15:02:39 UTC (rev 
12399)
@@ -10,9 +10,12 @@
 from django.shortcuts import render_to_response, get_object_or_404
 from django.template import RequestContext
 from django.utils.html import escape
+from django.utils.decorators import method_decorator
 from django.utils.translation import ugettext, ugettext_lazy as _
 from django.views.decorators.csrf import csrf_protect
 
+csrf_protect_m = method_decorator(csrf_protect)
+
 class GroupAdmin(admin.ModelAdmin):
     search_fields = ('name',)
     ordering = ('name',)
@@ -76,7 +79,7 @@
             (r'^(\d+)/password/$', 
self.admin_site.admin_view(self.user_change_password))
         ) + super(UserAdmin, self).get_urls()
 
-    @csrf_protect
+    @csrf_protect_m
     @transaction.commit_on_success
     def add_view(self, request, form_url='', extra_context=None):
         # It's an error for a user to have add permission but NOT change

Modified: django/trunk/django/contrib/auth/decorators.py
===================================================================
--- django/trunk/django/contrib/auth/decorators.py      2010-02-07 15:55:40 UTC 
(rev 12398)
+++ django/trunk/django/contrib/auth/decorators.py      2010-02-09 15:02:39 UTC 
(rev 12399)
@@ -6,8 +6,8 @@
 from django.contrib.auth import REDIRECT_FIELD_NAME
 from django.http import HttpResponseRedirect
 from django.utils.http import urlquote
-from django.utils.decorators import auto_adapt_to_methods
 
+
 def user_passes_test(test_func, login_url=None, 
redirect_field_name=REDIRECT_FIELD_NAME):
     """
     Decorator for views that checks that the user passes the given test,
@@ -26,8 +26,9 @@
             tup = login_url, redirect_field_name, path
             return HttpResponseRedirect('%s?%s=%s' % tup)
         return wraps(view_func)(_wrapped_view)
-    return auto_adapt_to_methods(decorator)
+    return decorator
 
+
 def login_required(function=None, redirect_field_name=REDIRECT_FIELD_NAME):
     """
     Decorator for views that checks that the user is logged in, redirecting
@@ -41,6 +42,7 @@
         return actual_decorator(function)
     return actual_decorator
 
+
 def permission_required(perm, login_url=None):
     """
     Decorator for views that checks whether a user has a particular permission

Modified: django/trunk/django/contrib/formtools/wizard.py
===================================================================
--- django/trunk/django/contrib/formtools/wizard.py     2010-02-07 15:55:40 UTC 
(rev 12398)
+++ django/trunk/django/contrib/formtools/wizard.py     2010-02-09 15:02:39 UTC 
(rev 12399)
@@ -14,8 +14,10 @@
 from django.utils.hashcompat import md5_constructor
 from django.utils.translation import ugettext_lazy as _
 from django.contrib.formtools.utils import security_hash
+from django.utils.decorators import method_decorator
 from django.views.decorators.csrf import csrf_protect
 
+
 class FormWizard(object):
     # Dictionary of extra template context variables.
     extra_context = {}
@@ -45,7 +47,7 @@
         # hook methods might alter self.form_list.
         return len(self.form_list)
 
-    @csrf_protect
+    @method_decorator(csrf_protect)
     def __call__(self, request, *args, **kwargs):
         """
         Main method that does all the hard work, conforming to the Django view

Modified: django/trunk/django/utils/decorators.py
===================================================================
--- django/trunk/django/utils/decorators.py     2010-02-07 15:55:40 UTC (rev 
12398)
+++ django/trunk/django/utils/decorators.py     2010-02-09 15:02:39 UTC (rev 
12399)
@@ -7,44 +7,24 @@
     from django.utils.functional import wraps, update_wrapper  # Python 2.3, 
2.4 fallback.
 
 
-# Licence for MethodDecoratorAdaptor and auto_adapt_to_methods
-#
-# This code is taken from stackoverflow.com [1], the code being supplied by
-# users 'Ants Aasma' [2] and 'Silent Ghost' [3] with modifications.  It is
-# legally included here under the terms of the Creative Commons
-# Attribution-Share Alike 2.5 Generic Licence [4]
-#
-# [1] 
http://stackoverflow.com/questions/1288498/using-the-same-decorator-with-arguments-with-functions-and-methods
-# [2] http://stackoverflow.com/users/107366/ants-aasma
-# [3] http://stackoverflow.com/users/12855/silentghost
-# [4] http://creativecommons.org/licenses/by-sa/2.5/
-
-class MethodDecoratorAdaptor(object):
+def method_decorator(decorator):
     """
-    Generic way of creating decorators that adapt to being
-    used on methods
+    Converts a function decorator into a method decorator
     """
-    def __init__(self, decorator, func):
-        update_wrapper(self, func)
-        # NB: update the __dict__ first, *then* set
-        # our own .func and .decorator, in case 'func' is actually
-        # another MethodDecoratorAdaptor object, which has its
-        # 'func' and 'decorator' attributes in its own __dict__
-        self.decorator = decorator
-        self.func = func
-    def __call__(self, *args, **kwargs):
-        return self.decorator(self.func)(*args, **kwargs)
-    def __get__(self, instance, owner):
-        return self.decorator(self.func.__get__(instance, owner))
+    def _dec(func):
+        def _wrapper(self, *args, **kwargs):
+            def bound_func(*args2, **kwargs2):
+                return func(self, *args2, **kwargs2)
+            # bound_func has the signature that 'decorator' expects i.e.  no
+            # 'self' argument, but it is a closure over self so it can call
+            # 'func' correctly.
+            return decorator(bound_func)(*args, **kwargs)
+        return wraps(func)(_wrapper)
+    update_wrapper(_dec, decorator)
+    # Change the name to aid debugging.
+    _dec.__name__ = 'method_dec(%s)' % decorator.__name__
+    return _dec
 
-def auto_adapt_to_methods(decorator):
-    """
-    Takes a decorator function, and returns a decorator-like callable that can
-    be used on methods as well as functions.
-    """
-    def adapt(func):
-        return MethodDecoratorAdaptor(decorator, func)
-    return wraps(decorator)(adapt)
 
 def decorator_from_middleware_with_args(middleware_class):
     """
@@ -61,6 +41,7 @@
     """
     return make_middleware_decorator(middleware_class)
 
+
 def decorator_from_middleware(middleware_class):
     """
     Given a middleware class (not an instance), returns a view decorator. This
@@ -69,6 +50,7 @@
     """
     return make_middleware_decorator(middleware_class)()
 
+
 def make_middleware_decorator(middleware_class):
     def _make_decorator(*m_args, **m_kwargs):
         middleware = middleware_class(*m_args, **m_kwargs)
@@ -96,5 +78,5 @@
                         return result
                 return response
             return wraps(view_func)(_wrapped_view)
-        return auto_adapt_to_methods(_decorator)
+        return _decorator
     return _make_decorator

Modified: django/trunk/django/views/decorators/cache.py
===================================================================
--- django/trunk/django/views/decorators/cache.py       2010-02-07 15:55:40 UTC 
(rev 12398)
+++ django/trunk/django/views/decorators/cache.py       2010-02-09 15:02:39 UTC 
(rev 12399)
@@ -16,10 +16,11 @@
 except ImportError:
     from django.utils.functional import wraps  # Python 2.3, 2.4 fallback.
 
-from django.utils.decorators import decorator_from_middleware_with_args, 
auto_adapt_to_methods
+from django.utils.decorators import decorator_from_middleware_with_args
 from django.utils.cache import patch_cache_control, add_never_cache_headers
 from django.middleware.cache import CacheMiddleware
 
+
 def cache_page(*args, **kwargs):
     # We need backwards compatibility with code which spells it this way:
     #   def my_view(): pass
@@ -48,18 +49,16 @@
     else:
         return 
decorator_from_middleware_with_args(CacheMiddleware)(cache_timeout=args[0], 
key_prefix=key_prefix)
 
-def cache_control(**kwargs):
 
+def cache_control(**kwargs):
     def _cache_controller(viewfunc):
-
         def _cache_controlled(request, *args, **kw):
             response = viewfunc(request, *args, **kw)
             patch_cache_control(response, **kwargs)
             return response
-
         return wraps(viewfunc)(_cache_controlled)
+    return _cache_controller
 
-    return auto_adapt_to_methods(_cache_controller)
 
 def never_cache(view_func):
     """
@@ -71,4 +70,3 @@
         add_never_cache_headers(response)
         return response
     return wraps(view_func)(_wrapped_view_func)
-never_cache = auto_adapt_to_methods(never_cache)

Modified: django/trunk/docs/releases/1.2.txt
===================================================================
--- django/trunk/docs/releases/1.2.txt  2010-02-07 15:55:40 UTC (rev 12398)
+++ django/trunk/docs/releases/1.2.txt  2010-02-09 15:02:39 UTC (rev 12399)
@@ -251,6 +251,55 @@
 cookies and have javascript code that parses and manipulates cookie values
 client-side.
 
+``user_passes_test``, ``login_required`` and ``permission_required``
+--------------------------------------------------------------------
+
+``django.contrib.auth.decorators`` provides the decorators ``login_required``,
+``permission_required`` and ``user_passes_test``. Previously it was possible to
+use these decorators both on functions (where the first argument is 'request')
+and on methods (where the first argument is 'self', and the second argument is
+'request'). However, we have found that the trick which enabled this is
+flawed. It only works in limited circumstances, and produces errors that are
+very difficult to debug when it does not work.
+
+For this reason, the 'auto adapt' behaviour has been removed, and if you are
+using these decorators on methods, you will need to manually apply
+:func:`django.utils.decorators.method_decorator` to convert the decorator to 
one
+that works with methods. You would change code from this::
+
+    class MyClass(object):
+
+        @login_required
+        def my_view(self, request):
+            pass
+
+to this::
+
+    from django.utils.decorators import method_decorator
+
+    class MyClass(object):
+
+        @method_decorator(login_required)
+        def my_view(self, request):
+            pass
+
+or::
+
+    from django.utils.decorators import method_decorator
+
+    login_required_m = method_decorator(login_required)
+
+    class MyClass(object):
+
+        @login_required_m
+        def my_view(self, request):
+            pass
+
+For those following trunk, this change also applies to other decorators
+introduced since 1.1, including ``csrf_protect``, ``cache_control`` and 
anything
+created using ``decorator_from_middleware``.
+
+
 .. _deprecated-features-1.2:
 
 Features deprecated in 1.2

Modified: django/trunk/tests/modeltests/test_client/views.py
===================================================================
--- django/trunk/tests/modeltests/test_client/views.py  2010-02-07 15:55:40 UTC 
(rev 12398)
+++ django/trunk/tests/modeltests/test_client/views.py  2010-02-09 15:02:39 UTC 
(rev 12399)
@@ -7,6 +7,7 @@
 from django.forms.forms import Form
 from django.forms import fields
 from django.shortcuts import render_to_response
+from django.utils.decorators import method_decorator
 
 def get_view(request):
     "A simple view that expects a GET request, and returns a rendered template"
@@ -147,14 +148,15 @@
 permission_protected_view = 
permission_required('modeltests.test_perm')(permission_protected_view)
 
 class _ViewManager(object):
+    @method_decorator(login_required)
     def login_protected_view(self, request):
         t = Template('This is a login protected test using a method. '
                      'Username is {{ user.username }}.',
                      name='Login Method Template')
         c = Context({'user': request.user})
         return HttpResponse(t.render(c))
-    login_protected_view = login_required(login_protected_view)
 
+    @method_decorator(permission_required('modeltests.test_perm'))
     def permission_protected_view(self, request):
         t = Template('This is a permission protected test using a method. '
                      'Username is {{ user.username }}. '
@@ -162,7 +164,6 @@
                      name='Permissions Template')
         c = Context({'user': request.user})
         return HttpResponse(t.render(c))
-    permission_protected_view = 
permission_required('modeltests.test_perm')(permission_protected_view)
 
 _view_manager = _ViewManager()
 login_protected_method_view = _view_manager.login_protected_view

Modified: django/trunk/tests/regressiontests/decorators/tests.py
===================================================================
--- django/trunk/tests/regressiontests/decorators/tests.py      2010-02-07 
15:55:40 UTC (rev 12398)
+++ django/trunk/tests/regressiontests/decorators/tests.py      2010-02-09 
15:02:39 UTC (rev 12399)
@@ -10,7 +10,7 @@
 from django.views.decorators.http import require_http_methods, require_GET, 
require_POST
 from django.views.decorators.vary import vary_on_headers, vary_on_cookie
 from django.views.decorators.cache import cache_page, never_cache, 
cache_control
-from django.utils.decorators import auto_adapt_to_methods
+from django.utils.decorators import method_decorator
 from django.contrib.auth.decorators import login_required, 
permission_required, user_passes_test
 from django.contrib.admin.views.decorators import staff_member_required
 
@@ -47,6 +47,7 @@
 fully_decorated = allow_lazy(fully_decorated)
 fully_decorated = lazy(fully_decorated)
 
+
 class DecoratorsTest(TestCase):
 
     def test_attributes(self):
@@ -112,42 +113,25 @@
         my_view_cached2 = cache_page(my_view, 123, key_prefix="test")
         self.assertEqual(my_view_cached2(HttpRequest()), "response")
 
-class MethodDecoratorAdapterTests(TestCase):
-    def test_auto_adapt_to_methods(self):
-        """
-        Test that auto_adapt_to_methods actually works.
-        """
-        # Need 2 decorators with auto_adapt_to_methods,
-        # to check it plays nicely with composing itself.
 
-        def my_decorator(func):
-            def wrapped(*args, **kwargs):
-                # need to ensure that the first arg isn't 'self'
-                self.assertEqual(args[0], "test")
-                return "my_decorator:" + func(*args, **kwargs)
-            wrapped.my_decorator_custom_attribute = True
-            return wraps(func)(wrapped)
-        my_decorator = auto_adapt_to_methods(my_decorator)
+# For testing method_decorator, a decorator that assumes a single argument.
+# We will get type arguments if there is a mismatch in the number of arguments.
+def simple_dec(func):
+    def wrapper(arg):
+        return func("test:" + arg)
+    return wraps(func)(wrapper)
 
-        def my_decorator2(func):
-            def wrapped(*args, **kwargs):
-                # need to ensure that the first arg isn't 'self'
-                self.assertEqual(args[0], "test")
-                return "my_decorator2:" + func(*args, **kwargs)
-            wrapped.my_decorator2_custom_attribute = True
-            return wraps(func)(wrapped)
-        my_decorator2 = auto_adapt_to_methods(my_decorator2)
+simple_dec_m = method_decorator(simple_dec)
 
-        class MyClass(object):
-            def my_method(self, *args, **kwargs):
-                return "my_method:%r %r" % (args, kwargs)
-            my_method = my_decorator2(my_decorator(my_method))
 
-        obj = MyClass()
-        self.assertEqual(obj.my_method("test", 123, name='foo'),
-                         "my_decorator2:my_decorator:my_method:('test', 123) 
{'name': 'foo'}")
-        self.assertEqual(obj.my_method.__name__, 'my_method')
-        self.assertEqual(getattr(obj.my_method, 
'my_decorator_custom_attribute', False),
-                         True)
-        self.assertEqual(getattr(obj.my_method, 
'my_decorator2_custom_attribute', False),
-                         True)
+class MethodDecoratorTests(TestCase):
+    """
+    Tests for method_decorator
+    """
+    def test_method_decorator(self):
+        class Test(object):
+            @simple_dec_m
+            def say(self, arg):
+                return arg
+
+        self.assertEqual("test:hello", Test().say("hello"))

-- 
You received this message because you are subscribed to the Google Groups 
"Django updates" group.
To post to this group, send email to [email protected].
To unsubscribe from this group, send email to 
[email protected].
For more options, visit this group at 
http://groups.google.com/group/django-updates?hl=en.

Reply via email to