Blog

Our blog for cool new technologies and random stuff.

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 the 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:

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 :-)

13 thoughts on “A Django Administration interface for non staff users

  1. Fantastic explanation!! If all the docs around about Admin topics would be as clear an straight as this, the Django community would grow faster.
    Thank you!!

  2. Hi thanks for the post am lost here can u give me an example of codes i should put in this class and this funtion pls thanks alot

    class UserAdmin(AdminSite):
    def has_permission(self, request):

  3. Hello, thanks for the post. I tried the same but am facing a problem. How did you solve this:

    I have the standard "autodiscovered" admin site at the URL "/admin" and this works fine.

    I wrote a custom AdminSite, exactly like your "UserAdmin" class, and made it accessible at the URL "/custom/admin". This custom admin shows only a subset of all the models. When I browse this second admin URL, I can see what I want, with only the subset, but all the links to the models (list, add, …) display the URL "/admin", not "/custom/admin".

    When one of your users browse to your custom admin and click on a model CRUD operation, isn't he redirected to the URL with "/admin" ?

  4. I was doing the same thing today, and was not able to login to my newly created site only for staff members. Your post will save be 2-3 hours of debugging…
    its fantastic!!!

  5. Hi …
    I Try your solution but have a problem

    —————-
    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.")})

    —————-
    Above lines generate Exceptions :
    Exception Type: NameError
    Exception Value: name 'forms' is not defined

    Kindly please give your enlighten to fix this problem

    Sincerely
    -bino-

  6. Thanks a lot for your post!

    When you say:
    And in the urls.py:
    .
    .
    .
    url(r'^', include('myapp.urls')),

    Where is the urls.py located? isn't it the same urls.py as in myapp.urls and therefore recursive?

    I must be missing something…

  7. Great post. However, when I use this approach while I see only the models registered with the custom admin all the CRUD links point to the default adminsite. I'm using django 1.5. Is there a fix/work around for this?

  8. cialis compatibility with other medication
    <a href=http://buycialis-us.com>cialis online
    </a> buy cialis online our users have posted a total of
    – cialis online

  9. Hello,

    class imported here:
    from django.contrib.auth.forms import AuthenticationForm
    does not have staff check,
    just use it in UserAdmin class the way you used your overwritten form.

    class UserAdmin(AdminSite):
    login_form = AuthenticationForm

    Writing your own UserAuthenticateForm is not necessary in this case.

  10. For those having problems with 'admin' prefix in urls – take a closer look at UserAdmin constructor – you have to give it a name in argument – otherwise it stays as admin and goes to urls…

    user_admin_site = UserAdmin('usersadmin')

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>