Commit 4bff1946 authored by Luke Plant's avatar Luke Plant
Browse files

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.
 



git-svn-id: http://code.djangoproject.com/svn/django/trunk@12399 bcc190cf-cafb-0310-a4f2-bffc1f526a37
parent edb6d753
Loading
Loading
Loading
Loading
+6 −4
Original line number Diff line number Diff line
@@ -13,6 +13,7 @@ from django.db import models, transaction
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 @@ FORMFIELD_FOR_DBFIELD_DEFAULTS = {
    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 @@ class ModelAdmin(BaseModelAdmin):
            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 @@ class ModelAdmin(BaseModelAdmin):
        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 @@ class ModelAdmin(BaseModelAdmin):
        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 @@ class ModelAdmin(BaseModelAdmin):
            '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
+4 −1
Original line number Diff line number Diff line
@@ -10,9 +10,12 @@ from django.http import HttpResponseRedirect, Http404
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 @@ class UserAdmin(admin.ModelAdmin):
            (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
+4 −2
Original line number Diff line number Diff line
@@ -6,7 +6,7 @@ except ImportError:
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):
    """
@@ -26,7 +26,8 @@ def user_passes_test(test_func, login_url=None, redirect_field_name=REDIRECT_FIE
            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):
    """
@@ -41,6 +42,7 @@ def login_required(function=None, redirect_field_name=REDIRECT_FIELD_NAME):
        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
+3 −1
Original line number Diff line number Diff line
@@ -14,8 +14,10 @@ from django.template.context import RequestContext
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 @@ class FormWizard(object):
        # 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
+18 −36
Original line number Diff line number Diff line
@@ -7,44 +7,24 @@ except ImportError:
    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 @@ def decorator_from_middleware_with_args(middleware_class):
    """
    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 @@ def decorator_from_middleware(middleware_class):
    """
    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 @@ def make_middleware_decorator(middleware_class):
                        return result
                return response
            return wraps(view_func)(_wrapped_view)
        return auto_adapt_to_methods(_decorator)
        return _decorator
    return _make_decorator
Loading