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


Comments

  1. Iván González
    Iván González on 07/27/2012 4 a.m.
    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. abi
    abi on 07/28/2012 4:40 p.m.
    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. Cédric
    Cédric on 10/16/2012 7:55 p.m.
    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. Alex
    Alex on 01/26/2013 4:20 p.m.
    Thank you!!!
  5. Matteo
    Matteo on 02/01/2013 8:44 a.m.
    Really useful, thanks!
  6. Arun Mittal
    Arun Mittal on 02/16/2013 1:07 p.m.
    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!!!
  7. bino
    bino on 02/27/2013 midnight
    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-
  8. og
    og on 04/16/2013 9:40 a.m.
    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...
  9. Murali
    Murali on 05/26/2013 9:54 p.m.
    thanks. great write up
  10. R Bell
    R Bell on 08/24/2013 9:03 p.m.
    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?
  11. Nartuleg
    Nartuleg on 12/07/2013 11:20 p.m.
    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
  12. phoenix
    phoenix on 01/08/2014 3:33 p.m.
    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.
  13. phoenix
    phoenix on 01/08/2014 7:10 p.m.
    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')
  14. cheap animal onesies
    cheap animal onesies on 07/02/2014 2:05 a.m.
    Thanks for taking the time to discuss this,Are you searching for animal onesies costumes collection 2014? <a href="http://www.sale-pajama.com/animal-onesies/adult-animal-onesies.html">animal onesies</a>
  15. bandage dress
    bandage dress on 07/02/2014 2:08 a.m.
    Our main products are Bodycon dresses, mini bandage dresses.
  16. aeiykspy
    aeiykspy on 07/07/2014 10:06 a.m.
    She wantedto see the pictures, and when told <a href="http://mpaydayloansb1.info">payday loans</a> euqajytbvr <a href=http://mpaydayloansa1.info>payday loans</a> payday loans http://mpaydayloansa1.info and took her home to the kingspalace.
  17. resepyos
    resepyos on 07/12/2014 1:27 a.m.
    Thanks for a great tutorial. very useful for me
  18. Resep Kuliner Kreatif
    Resep Kuliner Kreatif on 07/12/2014 1:39 a.m.
    I've tried and it does really work. Thank you for help me!
  19. buy likes on instagram
    buy likes on instagram on 07/14/2014 8:33 a.m.
    Your approach to this post is unique and appreciating. I am writing an article for our research paper and this post has helped me. Thanks.
  20. http://www.bicara.co.id
    http://www.bicara.co.id on 07/25/2014 3:41 a.m.
    The next occasion Many of us study the web site, Enables wish who's doesnt fall short us with this in mind. Of course, I know it turned out our replacement for see, but Many of us essentially considered youd get whatever beneficial to communicate. Most Many of us pick up is usually many different whining regarding whatever that you might restoration when you werent additionally active searching for recognition.

Post your comment

:

:

(Optional):

:

(Optional):