How to use permission_required decorators on django class-based views
Asked Answered
C

13

188

I'm having a bit of trouble understanding how the new CBVs work. My question is this, I need to require login in all the views, and in some of them, specific permissions. In function-based views I do that with @permission_required() and the login_required attribute in the view, but I don't know how to do this on the new views. Is there some section in the django docs explaining this? I didn't found anything. What is wrong in my code?

I tried to use the @method_decorator but it replies "TypeError at /spaces/prueba/ _wrapped_view() takes at least 1 argument (0 given)"

Here is the code (GPL):

from django.utils.decorators import method_decorator
from django.contrib.auth.decorators import login_required, permission_required

class ViewSpaceIndex(DetailView):

    """
    Show the index page of a space. Get various extra contexts to get the
    information for that space.

    The get_object method searches in the user 'spaces' field if the current
    space is allowed, if not, he is redirected to a 'nor allowed' page. 
    """
    context_object_name = 'get_place'
    template_name = 'spaces/space_index.html'

    @method_decorator(login_required)
    def get_object(self):
        space_name = self.kwargs['space_name']

        for i in self.request.user.profile.spaces.all():
            if i.url == space_name:
                return get_object_or_404(Space, url = space_name)

        self.template_name = 'not_allowed.html'
        return get_object_or_404(Space, url = space_name)

    # Get extra context data
    def get_context_data(self, **kwargs):
        context = super(ViewSpaceIndex, self).get_context_data(**kwargs)
        place = get_object_or_404(Space, url=self.kwargs['space_name'])
        context['entities'] = Entity.objects.filter(space=place.id)
        context['documents'] = Document.objects.filter(space=place.id)
        context['proposals'] = Proposal.objects.filter(space=place.id).order_by('-pub_date')
        context['publication'] = Post.objects.filter(post_space=place.id).order_by('-post_pubdate')
        return context
Cretonne answered 20/5, 2011 at 8:1 Comment(0)
A
258

There are a few strategies listed in the CBV docs:

Decorate the view when you instantiate it in your urls.py (docs)

from django.contrib.auth.decorators import login_required

urlpatterns = [
    path('view/',login_required(ViewSpaceIndex.as_view(..)),
    ...
]

The decorator is applied on a per-instance basis, so you can add it or remove it in different urls.py routes as needed.

Decorate your class so every instance of your view is wrapped (docs)

There's two ways to do this:

  1. Apply method_decorator to your CBV dispatch method e.g.,

     from django.utils.decorators import method_decorator
     from django.contrib.auth.decorators import login_required
    
     @method_decorator(login_required, name='dispatch')
     class ViewSpaceIndex(TemplateView):
         template_name = 'secret.html'
    

If you're using Django < 1.9 (which you shouldn't, it's no longer supported) you can't use method_decorator on the class, so you have to override the dispatch method manually:

    from django.contrib.auth.decorators import login_required

    class ViewSpaceIndex(TemplateView):

        @method_decorator(login_required)
        def dispatch(self, *args, **kwargs):
            return super(ViewSpaceIndex, self).dispatch(*args, **kwargs)
  1. Use a mixin like django.contrib.auth.mixins.LoginRequiredMixin outlined well in the other answers here:

     from django.contrib.auth.mixins import LoginRequiredMixin
    
     class MyView(LoginRequiredMixin, View):
    
         login_url = '/login/'
         redirect_field_name = 'redirect_to'
    

Make sure you place the mixin class first in the inheritance list (so Python's Method Resolution Order algorithm picks the Right Thing).

The reason you're getting a TypeError is explained in the docs:

Note: method_decorator passes *args and **kwargs as parameters to the decorated method on the class. If your method does not accept a compatible set of parameters it will raise a TypeError exception.

Abolish answered 20/5, 2011 at 8:42 Comment(5)
Mentioned here in latest docs docs.djangoproject.com/en/dev/topics/class-based-views/introAdrastus
how to append message to it?Ezzell
For those who didn't understand (like I did, at first) - the 'dispatch' method should be added to the ViewSpaceIndex classMeagan
Is there any reason to favour one of these methods over the other?Lavonnelaw
@Lavonnelaw I think it boils down to personal preference and maintaining codebase consistency within your team / organization. I personally tend towards the mixin approach if I'm building class based views though.Abolish
P
120

Here is my approach, I create a mixin that is protected (this is kept in my mixin library):

from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator

class LoginRequiredMixin(object):
    @method_decorator(login_required)
    def dispatch(self, request, *args, **kwargs):
        return super(LoginRequiredMixin, self).dispatch(request, *args, **kwargs)

Whenever you want a view to be protected you just add the appropriate mixin:

class SomeProtectedViewView(LoginRequiredMixin, TemplateView):
    template_name = 'index.html'

Just make sure that your mixin is first.

Update: I posted this in way back in 2011, starting with version 1.9 Django now includes this and other useful mixins (AccessMixin, PermissionRequiredMixin, UserPassesTestMixin) as standard!

Patsis answered 23/6, 2011 at 13:51 Comment(6)
is it possible to have multiple of this kind of mixins? It didn't work for me and I do not think it makes sense it would have.Ailsun
Yes it should be possible to have several mixins since each mixin does a call to super that picks the next class in accordance with the MROSigh
I think that this is an elegant solution; I don't like having a mixture of decorators in my urls.py and mixins in views.py. This is a way to wrap decorators that would move all of that logic to the view.Journeywork
django-braces has this (and more) mixins - a very useful package to installLearn
Just a note for people in full retard mode like me: make sure that you are not logged in when testing login_required functionality...Micheal
As per your update, here's the link to the docs for AccessMixin: docs.djangoproject.com/en/1.9/topics/auth/default/…Incuse
S
51

Here's an alternative using class based decorators:

from django.utils.decorators import method_decorator

def class_view_decorator(function_decorator):
    """Convert a function based decorator into a class based decorator usable
    on class based Views.

    Can't subclass the `View` as it breaks inheritance (super in particular),
    so we monkey-patch instead.
    """

    def simple_decorator(View):
        View.dispatch = method_decorator(function_decorator)(View.dispatch)
        return View

    return simple_decorator

This can then be used simply like this:

@class_view_decorator(login_required)
class MyView(View):
    # this view now decorated
Schlieren answered 8/12, 2011 at 10:13 Comment(4)
You can use this to chain view decorators, nicely! +1Ailsun
This is so great it should be considered for inclusion upstream IMO.Rhone
I love this! Im wondering is it at all possible to pass args/kwargs down from the class_view_decorator to the function_decorator??? It would be great if the login_decorator could say conditionally match request.METHOD so it only applies for say post?Institutionalize
The args/kwargs should be easily achievable by using class_view_decorator(my_decorator(*args, **kwargs)). As for conditional method matching - you could modify the class_view_decorator to apply itself to View.get or View.post instead of View.dispatch.Schlieren
C
23

For those of you who use Django >= 1.9, it's already included in django.contrib.auth.mixins as AccessMixin, LoginRequiredMixin, PermissionRequiredMixin and UserPassesTestMixin.

So to apply LoginRequired to CBV(e.g. DetailView):

from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic.detail import DetailView


class ViewSpaceIndex(LoginRequiredMixin, DetailView):
    model = Space
    template_name = 'spaces/space_index.html'
    login_url = '/login/'
    redirect_field_name = 'redirect_to'

It's also good to keep in mind GCBV Mixin order: Mixins must go on the left side, and the base view class must go in the right side. If the order is different you can get broken and unpredictable results.

Carolincarolina answered 2/3, 2016 at 11:40 Comment(1)
This is the best answer in 2019. Also, great point about mixin order.Iand
A
14

I realise this thread is a bit dated, but here's my two cents anyway.

with the following code:

from django.utils.decorators import method_decorator
from inspect import isfunction

class _cbv_decorate(object):
    def __init__(self, dec):
        self.dec = method_decorator(dec)

    def __call__(self, obj):
        obj.dispatch = self.dec(obj.dispatch)
        return obj

def patch_view_decorator(dec):
    def _conditional(view):
        if isfunction(view):
            return dec(view)

        return _cbv_decorate(dec)(view)

    return _conditional

we now have a way to patch a decorator, so it will become multifunctional. This effectively means that when applied to a regular view decorator, like so:

login_required = patch_view_decorator(login_required)

this decorator will still work when used the way it was originally intended:

@login_required
def foo(request):
    return HttpResponse('bar')

but will also work properly when used like so:

@login_required
class FooView(DetailView):
    model = Foo

This seems to work fine in several cases i've recently come across, including this real-world example:

@patch_view_decorator
def ajax_view(view):
    def _inner(request, *args, **kwargs):
        if request.is_ajax():
            return view(request, *args, **kwargs)
        else:
            raise Http404

    return _inner

The ajax_view function is written to modify a (function based) view, so that it raises a 404 error whenever this view is visited by a non ajax call. By simply applying the patch function as a decorator, this decorator is all set to work in class based views as well

Antrim answered 17/7, 2012 at 15:30 Comment(0)
F
5

Use Django Braces. It provides a lot of useful mixins that is easily available. It has beautiful docs. Try it out.

You can even create your custom mixins.

http://django-braces.readthedocs.org/en/v1.4.0/

Example Code:

from django.views.generic import TemplateView

from braces.views import LoginRequiredMixin


class SomeSecretView(LoginRequiredMixin, TemplateView):
    template_name = "path/to/template.html"

    #optional
    login_url = "/signup/"
    redirect_field_name = "hollaback"
    raise_exception = True

    def get(self, request):
        return self.render_to_response({})
Forrer answered 19/4, 2014 at 0:10 Comment(0)
K
4

If it's a site where the majority of pages requires the user to be logged in, you can use a middleware to force login on all views except some who are especially marked.

Pre Django 1.10 middleware.py:

from django.contrib.auth.decorators import login_required
from django.conf import settings

EXEMPT_URL_PREFIXES = getattr(settings, 'LOGIN_EXEMPT_URL_PREFIXES', ())

class LoginRequiredMiddleware(object):
    def process_view(self, request, view_func, view_args, view_kwargs):
        path = request.path
        for exempt_url_prefix in EXEMPT_URL_PREFIXES:
            if path.startswith(exempt_url_prefix):
                return None
        is_login_required = getattr(view_func, 'login_required', True)
        if not is_login_required:
            return None
        return login_required(view_func)(request, *view_args, **view_kwargs) 

views.py:

def public(request, *args, **kwargs):
    ...
public.login_required = False

class PublicView(View):
    ...
public_view = PublicView.as_view()
public_view.login_required = False

Third-party views you don't want to wrap can be made excempt in the settings:

settings.py:

LOGIN_EXEMPT_URL_PREFIXES = ('/login/', '/reset_password/')
Karsten answered 15/9, 2013 at 13:26 Comment(0)
D
4

In my code I have written this adapter to adapt member functions to a non-member function:

from functools import wraps


def method_decorator_adaptor(adapt_to, *decorator_args, **decorator_kwargs):
    def decorator_outer(func):
        @wraps(func)
        def decorator(self, *args, **kwargs):
            @adapt_to(*decorator_args, **decorator_kwargs)
            def adaptor(*args, **kwargs):
                return func(self, *args, **kwargs)
            return adaptor(*args, **kwargs)
        return decorator
    return decorator_outer

You can simply use it like this:

from django.http import HttpResponse
from django.views.generic import View
from django.contrib.auth.decorators import permission_required
from some.where import method_decorator_adaptor


class MyView(View):
    @method_decorator_adaptor(permission_required, 'someapp.somepermission')
    def get(self, request):
        # <view logic>
        return HttpResponse('result')
Downbow answered 30/11, 2014 at 2:29 Comment(1)
It would be nice that this was a built-in on Django (just like method_decorator is). It seems a nice and readable way of achieving this.Teamwork
D
4

It has been a while now and now Django has changed so much.

Check here for how to decorate a class-based view.

https://docs.djangoproject.com/en/2.2/topics/class-based-views/intro/#decorating-the-class

The documentation did not include an example of "decorators that takes any argument". But decorators that take arguments are like this:

def mydec(arg1):
    def decorator(func):
         def decorated(*args, **kwargs):
             return func(*args, **kwargs) + arg1
         return decorated
    return deocrator

so if we are to use mydec as a "normal" decorator without arguments, we can do this:

mydecorator = mydec(10)

@mydecorator
def myfunc():
    return 5

So similarly, to use permission_required with method_decorator

we can do:

@method_decorator(permission_required("polls.can_vote"), name="dispatch")
class MyView:
    def get(self, request):
        # ...
Downbow answered 1/10, 2019 at 5:39 Comment(1)
I think it is the easiest way to achieve permissions in class base views by checking other permissions that the classic @login_required... Also you can pass more than one custom permission as the following @permission_required(['polls.can_vote', 'polls.change_vote']) .Wirehaired
C
1

I've made that fix based on Josh's solution

class LoginRequiredMixin(object):

    @method_decorator(login_required)
    def dispatch(self, *args, **kwargs):
        return super(LoginRequiredMixin, self).dispatch(*args, **kwargs)

Sample usage:

class EventsListView(LoginRequiredMixin, ListView):

    template_name = "events/list_events.html"
    model = Event
Cerous answered 4/12, 2015 at 10:39 Comment(0)
B
1

This is super easy with django > 1.9 coming with support for PermissionRequiredMixin and LoginRequiredMixin

Just import from the auth

views.py

from django.contrib.auth.mixins import LoginRequiredMixin

class YourListView(LoginRequiredMixin, Views):
    pass

For more details read Authorization in django

Bronson answered 12/4, 2018 at 8:51 Comment(0)
S
0

If you are doing a project which requires variety of permission tests, you can inherit this class.

from django.contrib.auth.decorators import login_required
from django.contrib.auth.decorators import user_passes_test
from django.views.generic import View
from django.utils.decorators import method_decorator



class UserPassesTest(View):

    '''
    Abstract base class for all views which require permission check.
    '''


    requires_login = True
    requires_superuser = False
    login_url = '/login/'

    permission_checker = None
    # Pass your custom decorator to the 'permission_checker'
    # If you have a custom permission test


    @method_decorator(self.get_permission())
    def dispatch(self, *args, **kwargs):
        return super(UserPassesTest, self).dispatch(*args, **kwargs)


    def get_permission(self):

        '''
        Returns the decorator for permission check
        '''

        if self.permission_checker:
            return self.permission_checker

        if requires_superuser and not self.requires_login:
            raise RuntimeError((
                'You have assigned requires_login as False'
                'and requires_superuser as True.'
                "  Don't do that!"
            ))

        elif requires_login and not requires_superuser:
            return login_required(login_url=self.login_url)

        elif requires_superuser:
            return user_passes_test(lambda u:u.is_superuser,
                                    login_url=self.login_url)

        else:
            return user_passes_test(lambda u:True)
Southwards answered 28/7, 2015 at 12:24 Comment(0)
H
0

Here the solution for permission_required decorator:

class CustomerDetailView(generics.GenericAPIView):

@method_decorator(permission_required('app_name.permission_codename', raise_exception=True))
    def post(self, request):
        # code...
        return True
Heiskell answered 9/12, 2019 at 7:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.