Read time: 5 minutes

A Django Administration interface for non staff users

A while back I had a Django application in which I needed registered users able to view, create, update and delete objects in my system. These objects were instances of only a subset of all the Django models.Model subclasses I had defined in the models.py file of my application.

You may find this problem very similar to what th Django Admin site solves for administrators: you register some models that are displayed, and then the admins can create/update/delete the objects in the system as needed. Cool, lets use the Django admin! We should only call admin.site.register() on those models we want users to be able to act on and we have a full CRUD interface over the models. However, there are some issues with this naive approach:

  • Only users flagged as staff members can login into the Django admin interface.
  • We would like to maintain the real administrator’s ability to act on many more models, not just those over which we want the mere mortal users to have CRUD capabilities. By not registering for the admin view, the real admin cannot create, view or delete content he might need to.

So, should we copy every template and replicate the logic of the Django Admin interface? No need. Fortunately, we can have multiple admin sites functioning at the same time.

The Django documentation explains that the default Admin site (django.contrib.admin.site) is actually just an instance of django.contrib.admin.sites.AdminSite. We can easily subclass AdminSite class to build new instances of the Django administration site, provided we give it a name different than “admin” when we instantiate it. So, having our own custom admin is as easy as having code like this in an admin.py file.

August 2018: Please note that this post was written for an older version of Django. Changes in the code might be necessary to adapt it to the latest versions and best practices.

from django.contrib import admin
from django.contrib.admin.sites import AdminSite

class UserAdmin(AdminSite):
    # Anything we wish to add or override

user_admin_site = UserAdmin(name='usersadmin')
# Run user_admin_site.register() for each model we wish to register
# for our admin interface for users

# Run admin.site.register() for each model we wish to register
# with the REAL django admin!

And in the urls.py:

from django.conf.urls import patterns, include, url
from django.contrib import admin
from myapp.admin import user_admin_site

admin.autodiscover()

urlpatterns = patterns('',
    url(r'^admin/', include(admin.site.urls)),
    url(r'^', include(user_admin_site.urls)),
    url(r'^', include('myapp.urls')),
)

Unfortunately a problem still persists, since only staff members are allowed to login into our new UserAdmin site. For this, we inspect the django/contrib/admin/sites.py file and find all the checks for is_staff. We find only one match in the has_permission() function of the AdminSite class itself. So in our subclass, we override the function removing the check:

class UserAdmin(AdminSite):

    def has_permission(self, request):
        """
        Removed check for is_staff.
        """
        return request.user.is_active

Done? Not yet. If you try it out you will see that non staff users still can’t login. Further inspection finds a login() function in the AdminSite class, and this chunk of code in it:

defaults = {
    'extra_context': context,
    'current_app': self.name,
    'authentication_form': self.login_form or AdminAuthenticationForm,
    'template_name': self.login_template or 'admin/login.html',
}
return login(request, **defaults)

So as we are not providing any custom login_form, it uses the default AdminAuthenticationForm which must contain the check for is_staff. Thanks to Django’s modular architecture it is very easy to just make our own login form and plug it in our UserAdmin. We just subclass AdminAuthenticationForm and provide our own clean() function, which works the same but avoids the check:

from django.contrib.auth.forms import AuthenticationForm

class UserAdminAuthenticationForm(AuthenticationForm):
    """
    Same as Django's AdminAuthenticationForm but allows to login
    any user who is not staff.
    """
    this_is_the_login_form = forms.BooleanField(widget=forms.HiddenInput,
                                initial=1,
                                error_messages={'required': ugettext_lazy(
                                "Please log in again, because your session has"
                                " expired.")})

    def clean(self):
        username = self.cleaned_data.get('username')
        password = self.cleaned_data.get('password')
        message = ERROR_MESSAGE

        if username and password:
            self.user_cache = authenticate(username=username,
            password=password)
            if self.user_cache is None:
                if u'@' in username:
                    # Mistakenly entered e-mail address instead of username?
                    # Look it up.
                    try:
                        user = User.objects.get(email=username)
                    except (User.DoesNotExist, User.MultipleObjectsReturned):
                        # Nothing to do here, moving along.
                        pass
                    else:
                        if user.check_password(password):
                            message = _("Your e-mail address is not your "
                                        "username."
                                        " Try '%s' instead.") % user.username
                raise forms.ValidationError(message)
            # Removed check for is_staff here!
            elif not self.user_cache.is_active:
                raise forms.ValidationError(message)
        self.check_for_test_cookie()
        return self.cleaned_data

And we plug it in our UserAdmin class:

class UserAdmin(AdminSite):

    login_form = UserAdminAuthenticationForm

    def has_permission(self, request):
        """
        Removed check for is_staff.
        """
        return request.user.is_active

(Note aside: with this knowledge we could even create a different admin site for Django’s superuser and regular staff members, just adding the is_superuser check to has_permission() ;))

Now we are done! We can login successfully into our brand new admin (or not so much) site for non staff users. There are a few gotchas, namely that some of the templates used by the admin site also contains an is_staff check in the template itself. For example in Django 1.3.1 line 26 of contrib/admin/templates/admin/base.html is:

{% if user.is_active and user.is_staff %}

so, for non-staff users, it won’t show the logout bar at the top.

To make it really usable we must copy these templates, edit them, and include them in our application. This is really easy to do, and the documentation shows how you can override the templates for our UserAdmin site only. This is more boring than the rest, so I’ll leave it as homework :-)


Like what you read?

Subscribe to our newsletter and get updates on Deep Learning, NLP, Computer Vision & Python.

No spam, ever. We'll never share your email address and you can opt out at any time.
Comments powered by Disqus

Get in touch

Do you have a project in mind?
We'd love to e-meet you!

Thanks for reaching out!

We'll reply as soon as possible.

And in the meantime?
Check out our blog to see what we're currently working on.