< / >

This is a blog by about coding and web development.

Private by default

Posted on in

When most pages in a site require authentication, decorating all the views with @login_required can be annoying. You can reverse the default behavior by creating a custom middleware class:

1. middleware.py
import urllib
from django.conf import settings
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.http import HttpResponseRedirect

def allow_anonymous(view_func):
    view_func.allow_anonymous = True
    return view_func

class RequireLogin:
    def process_view(self, request, view_func, view_args, view_kwargs):
        if request.path != settings.LOGIN_URL and
            not request.user.is_authenticated() and
            not getattr(view_func, 'allow_anonymous', False):
            url = '%s?%s=%s' % (settings.LOGIN_URL, REDIRECT_FIELD_NAME,
                                urllib.quote(request.get_full_path()))
    return HttpResponseRedirect(url)

That’s an ugly block of code but it’s not too complex. allow_anonymous is a function decorator, like login_required. It just tags the function to tell the middleware that authentication isn’t required. The RequireLogin class verifies that the user is logged in. If not, and if the function is not decorated with allow_anonymous, it redirects to settings.LOGIN_URL.

So put that code in a file in your project – let’s say, yourproject/yourapp/middleware.py. Then open settings.py and add "yourproject.yourapp.middleware.RequireLogin" to MIDDLEWARE_CLASSES. This tells Django about your new middleware class, and RequireLogin.process_view will be called any time a view is about to be rendered.

Now you can use it in your view:

2. views.py
from yourproject.yourapp.middleware import allow_anonymous

def some_private_view(request):
    # won't be accessible unless user is logged in
    return HttpResponse('Hello, user!')

@allow_anonymous
def some_public_view(request):
    return HttpResponse('Hello, world!')

If all is working correctly, some_private_view should ask for a login, but some_public_view will allow viewing without it.

Update: I’ve updated the code to fix a bug when using django.contrib.auth.views.login for your login view. As you can’t mark this view function with @allow_anonymous, it would infinitely redirect back to it. Oops! Thanks for pointing it out, Phil.