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.
Installation
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 decorators.py
file:
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 ;)