In the documentation of Django for “permissions” it says:

Permissions are set globally per type of object, not per specific object instance. It is possible to say “Mary may change news stories,” but it’s not currently possible to say “Mary may change news stories, but only the ones she created herself” or “Mary may only change news stories that have a certain status or publication date.

Well, actually, something like that is what we want to do; in our case we want Mary to change the Museums and Buildings of the city she is a “is_staff” member for (i.e. allowed to enter Admin and have certain permissions to edit/add/delete).

Let’s sum up the boundary conditions we have for our new application on the Admin side of thing.

  1. We have an application with many models.
  2. We have many clients.
  3. We want that the “staff” personal of the clients can edit and add new objects (but not delete them).
  4. We certainly do not want one client to see, edit or add object of other clients.
  5. We want all the clients in the same database (i.e. one database per client is not an option).

An example to explain usually works best; see below.

For the cities of Utrecht (these days famous for the start of the Tour the France), London and Paris (both just famous), we have models for Museum(s) and Building(s). Two models are the representation of condition number 1 above (an application with many models).

The Models.py:

from django.contrib.auth.models import AbstractUser
from django.db import models


class Museum(models.Model):
    client = models.ForeignKey('auth.Group')
    museum_nm = models.CharField(max_length=60, null=False)

    def __str__(self):
        return '{0}, {1}'.format(self.client, self.museum_nm)


class Building(models.Model):
    client = models.ForeignKey('auth.Group')
    building_nm = models.CharField(max_length=60, null=False)
    height = models.IntegerField(null=True)

    def __str__(self):
        return '{0}, {1}, {2}'.format(self.client, self.building_nm, self.height)

On to the Admin.py. We introduce the class “GetClientData”. The mixin class GetClientData limits the objects seen in the list view to the objects which belong to the city of the staff member.

from django.contrib import admin
from .models import Museum, Building
from .forms import MuseumForm, BuildingForm


class GetClientData:
    """
    Limit apparatus / building list view to the objects that belong to
    the request's user group(s) (= client(s)). Superuser sees all.
    Thank you: http://reinout.vanrees.org/weblog/2011/09/30/django-admin-filtering.html
    """
    def get_queryset(self, request):
        qs = super(GetClientData, self).get_queryset(request)
        return qs if request.user.is_superuser else \
                        qs.filter(client__in=request.user.groups.all())


class MuseumAdmin(GetClientData, admin.ModelAdmin):
    form = MuseumForm
admin.site.register(Museum, MuseumAdmin)


class BuildingAdmin(GetClientData, admin.ModelAdmin):
    form = BuildingForm
admin.site.register(Building, BuildingAdmin)

Ok, so now our staff members only sees the list of her/his city.

Below we see the screen shot for the Paris_user for the listview:

And for the superuser:

But what if the Paris_user clicks on one of the objects to edit it? What they will see in the edit screen is a dropdown list of the cities containing ALL the cities. This is not what we want. We want to limit the dropdown box for the edit and add views to the city of the staff member (except for the superuser, (s)he should see a dropdown box with all the cities).

Back to the forms.py and let’s limit the dropdown box. In order to do this we must know our user. There is an excellent package I have used multiple times to good effect to ALWAYS know who the owner of the session is: cuser. Get it from PyPI. This is middleware so you have to do just a little more than just installing it; you have to make 2 modifications to your settings.py.

Once you have installed it modify forms.py as follows:

from django.forms import ModelForm
from .models import Museum, Building
from django.contrib.auth.models import Group
from cuser.middleware import CuserMiddleware

class GroupDropDown:
    """
    Limit the dropdown box for the change / add screens to the city (=groups)
    the user belongs to.
    Thank you: 
     https://groups.google.com/forum/?fromgroups=#!topic/django-users/s9a0J6fKgWs
    """
    def __init__(self, *args, **kwargs):
        super(GroupDropDown, self).__init__(*args, **kwargs)
        # gets the logged in user, even if there is no request
        user = CuserMiddleware.get_user()

        # transform a many-2-many query to a list of ids
        gps = [x.id for x in user.groups.all()]

        # to which groups does (s)he belong? superuser belongs to all.
        groups = Group.objects.filter(id__in=gps) \
                 if not user.is_superuser else Group.objects.all()

        group_choices = []

        if groups is None:
            # this is not good if this happens... a staff member without a city
            group_choices.append(('', '---------'))

        for group in groups:
            group_choices.append((group.id, group.name))
        self.widget = self.fields['client'].widget
        self.widget.choices = group_choices


class MuseumForm(GroupDropDown, ModelForm):
    class Meta:
        model = Museum
        fields = '__all__'


class BuildingForm(GroupDropDown, ModelForm):
    class Meta:
        model = Building
        fields = '__all__'

The GroupDropDown is again made as a mixin to avoid duplicating code for each and every class.

Et voila, as the French say, this has done the trick.

The dropdown box in the edit view for the Paris_user:

 

The dropdown box for the superuser:

I hope this is useful,

Greetings,

Maarten Zaanen

Thank you Alexander Kojevnikov for “hilite.me” for formatting the Python code to nice looking HTML. I used Style Native.

Below the code for fill_data_initial.py

# in Django console at the prompt run:  execfile('fill_data_initial.py')

# Import general Python stuff

# import Django stuff
import django
django.setup()  # in Pycharm, needs to be done, your miles may vary !!
from django.contrib.auth.models import Group, User, Permission
from django.db.models import Q

# Import own stuff
from tteesstt.models import Museum, Building


def ins_group(nm):
    k = Group()
    k.name = nm
    k.save()
    # can not add more than 1 with the .add statement, never mind, 
    # this is not the issue in this blog
    k.permissions.add(Permission.objects.get(codename='add_museum'))
    k.permissions.add(Permission.objects.get(codename='change_museum'))
    k.permissions.add(Permission.objects.get(codename='add_building'))
    k.permissions.add(Permission.objects.get(codename='change_building'))
    return
Group.objects.all().delete()
ins_group('Paris')
ins_group('London')
ins_group('Utrecht')
print('OK Groups')


def ins_user(us_nm, us_email, us_passw, fn, ln, gr_nm):
    """
    create users for accounts, not superusers, they are created seperately.
    """
    us1 = User.objects.create_user(us_nm, us_email, us_passw)
    us1.first_name = fn
    us1.last_name = ln
    us1.is_staff = True
    us1.save()
    g = Group.objects.get(name=gr_nm)
    g.user_set.add(us1)
    return

# User.objects.filter(~Q(username='a')).delete()  # do not delete superuser 'a' created manually
User.objects.all().delete()  # also delete superuser

# make superuser
User.objects.create_superuser(username='a', password='a', email='a@a.nl')

# make staff users
ins_user('Paris_user', 'P@a.nl', 'P', 'Jean', 'Lafitte', 'Paris')
ins_user('London_user', 'L@a.nl', 'L', 'Mary', 'Scott', 'London')
ins_user('Utrecht_user', 'U@a.nl', 'U', 'Maarten', 'Zaanen', 'Utrecht')
print('OK Created users')


def ins_museum(cl, nm):
    g = Museum()
    g.client = Group.objects.get(name=cl)
    g.museum_nm = nm
    g.save()
    return

Museum.objects.all().delete()
ins_museum('Paris', 'Centre Georges Pompidou')
ins_museum('Paris', 'Musée d''Orsay')
ins_museum('London', 'Science Museum')
ins_museum('London', 'The British Museum')
ins_museum('Utrecht', 'Museum Speelklok')
ins_museum('Utrecht', 'Spoorwegmuseum')
print('OK Museum')


def ins_building(cl, nm, ht):
    g = Building()
    g.client = Group.objects.get(name=cl)
    g.building_nm = nm
    g.height = ht
    g.save()
    return

Building.objects.all().delete()
ins_building('Paris', 'Eiffel Tower', 324)
ins_building('Paris', 'Montparnasse Tower', 220)
ins_building('London', 'Big Ben', 96)
ins_building('London', 'London Eye', 135)
ins_building('Utrecht', 'de Dom', 111)
ins_building('Utrecht', 'Rabo', 105)
print('OK Buildings')