A common pattern in websites is when a few pages are protected and require a login to be accessed. The
@login_required decorator often comes in handy for these situations. But, another pattern which is quite common is when most of the site is protected, with just a few exceptions of pages that remain public (e.g. frontpage, registration page, etc.). In that case, it can be quite tedious to decorate all of the views with
@login_required, and it can be easy to forget to decorate some of them.
So, I came up with a simple system which by default protects every view and then lets you explicitly tell which views should be public. This makes things both easier and less error-prone.
The core of that system is contained in the following middleware code:
import re from django.conf import settings from django.contrib.auth.decorators import login_required from path.to.your.decorators import PublicView class LoginRequiredMiddleware(object): def __init__(self): self.public_patterns =  self.public_views =  if hasattr(settings, 'PUBLIC_VIEWS'): for view_path in settings.PUBLIC_VIEWS: view = self.get_view(view_path) self.public_views.append(view) if hasattr(settings, 'PUBLIC_PATHS'): for public_path in settings.PUBLIC_PATHS: self.public_patterns.append(re.compile(public_path)) def get_view(self, view_path): i = view_path.rfind('.') module_path, view_name = view_path[:i], view_path[i+1:] module = __import__(module_path, globals(), locals(), [view_name]) return getattr(module, view_name) def matches_public_view(self, view): if self.public_views: for public_view in self.public_views: if view == public_view: return True return False def matches_public_path(self, path): if self.public_patterns: for pattern in self.public_patterns: if pattern.match(path) is not None: return True return False def process_view(self, request, view_func, view_args, view_kwargs): if request.user.is_authenticated() or isinstance(view_func, PublicView) or self.matches_public_path(request.path) or self.matches_public_view(view_func): return None else: return login_required(view_func)(request, *view_args, **view_kwargs)
To install this middleware, simply copy and paste the above code anywhere in your project, for example in a file called
middleware.py. Then, update the
MIDDLEWARE_CLASSES setting in your project’s settings file:
MIDDLEWARE_CLASSES = ( ... 'path.to.your.middleware.LoginRequiredMiddleware', )
You’ll notice that, at the top of the middleware code above, there is an import of the
PublicView class. You need to update that import path after having copied/pasted the following snippet anywhere in your project, for example in a
try: from functools import update_wrapper except ImportError: from django.utils.functional import update_wrapper # Python 2.3, 2.4 fallback. from django.contrib.auth.decorators import _CheckLogin def login_not_required(view_func): """ Decorator which marks the given view as public (no login required). """ return PublicView(view_func) class PublicView(object): """ Forces a view to be public (no login required). """ def __init__(self, view_func): if isinstance(view_func, _CheckLogin): self.view_func = view_func.view_func else: self.view_func = view_func update_wrapper(self, view_func) def __get__(self, obj, cls=None): view_func = self.view_func.__get__(obj, cls) return _PublicView(view_func) def __call__(self, request, *args, **kwargs): return self.view_func(request, *args, **kwargs)
The above code contains a new decorator (
@login_not_required) which will be explained in detail in a moment.
Declaring public views
At this point, all of your views will require you to log in, including the login page itself. So, we now need to specify the few views that should be public. There are three different ways at your disposal: using a special decorator, listing the public views, or listing the public URL paths.
Using a Decorator
Thanks to the new
@login_not_required you can explicitly force a view to be public. Here’s an example:
from path.to.your.decorators import login_not_required @login_not_required def frontpage(request): ...
In this case, the
frontpage view will be properly displayed even if you’re not logged in.
Listing public views
If you don’t have direct access to modify a view’s code (e.g., it’s in a third-party application), you still can force that view to be public by adding it to the new
PUBLIC_VIEWS setting in your settings file. Here’s an example if you’re using the
django.contrib.auth system and the django-registration application:
PUBLIC_VIEWS = [ 'django.contrib.auth.views.login', 'django.contrib.auth.views.password_reset_done', 'django.contrib.auth.views.password_reset', 'django.contrib.auth.views.password_reset_confirm', 'django.contrib.auth.views.password_reset_complete', 'registration.views.register', 'registration.views.activate',]
Listing URL public paths
The third and last way is to directly specify the URL paths (as regular expressions) for the pages you want to be public. This can be useful, for example, if a page is rendered by a generic view. It is also useful if you are serving your media files statically via Django (only recommended in development mode). For that, you need to add the
PUBLIC_PATHS setting in your settings file. Here’s an example:
PUBLIC_PATHS = [ '^%s' % MEDIA_URL, '^/accounts/register/complete/$', # Uses the 'direct_to_template' generic view ]
That’s it! By using this technique your site will be protected effectively and it will be easy to maintain. I hope it helps! Any comment or remark is very welcome ;)