Django Admin - Specific user (admin) content

28,514

Solution 1

First, the cautionary warning: The Django admin design philosophy is that any user with access to the admin (is_staff==True) is a trusted user, e.g. an employee, hence the "staff" designation to even gain access to the admin. While you can customize the admin to restrict areas, allowing anyone not within your organization access to your admin is considered risky, and Django makes no guarantees about any sort of security at that point.

Now, if you still want to proceed, you can restrict most everything but the shops right off the bat by simply not assigning those privileges to the user. You'll have to give all the shop owners rights to edit any of the shop models they'll need access to, but everything else should be left off their permissions list.

Then, for each model that needs to be limited to the owner's eyes only, you'll need to add a field to store the "owner", or user allowed access to it. You can do this with the save_model method on ModelAdmin, which has access to the request object:

class MyModelAdmin(admin.ModelAdmin):
    def save_model(self, request, obj, form, change):
        obj.user = request.user
        super(MyModelAdmin, self).save_model(request, obj, form, change)

Then you'll also need to limit the ModelAdmin's queryset to only those items own by the current user:

class MyModelAdmin(admin.ModelAdmin):
    def get_queryset(self, request):
        qs = super(MyModelAdmin, self).get_queryset(request)
        if request.user.is_superuser:
            return qs
        return qs.filter(owner=request.user)

However, that will only limit what gets listed, the user could still play with the URL to access other objects they don't have access to, so you'll need to override each of the ModelAdmin's vulnerable views to redirect if the user is not the owner:

from django.http import HttpResponseRedirect
from django.core.urlresolvers import reverse

class MyModelAdmin(admin.ModelAdmin):
    def change_view(self, request, object_id, form_url='', extra_context=None):
        if not self.queryset(request).filter(id=object_id).exists():
            return HttpResponseRedirect(reverse('admin:myapp_mymodel_changelist'))

        return super(MyModelAdmin, self).change_view(request, object_id, form_url, extra_context)

    def delete_view(self, request, object_id, extra_context=None):
        if not self.queryset(request).filter(id=object_id).exists():
            return HttpResponseRedirect(reverse('admin:myapp_mymodel_changelist'))

        return super(MyModelAdmin, self).delete_view(request, object_id, extra_context)

    def history_view(self, request, object_id, extra_context=None):
        if not self.queryset(request).filter(id=object_id).exists():
            return HttpResponseRedirect(reverse('admin:myapp_mymodel_changelist'))

        return super(MyModelAdmin, self).history_view(request, object_id, extra_context)

UPDATE 06/05/12

Thanks @christophe31 for pointing out that since the ModelAdmin's queryset is already limited by user, you can just use self.queryset() in the change, delete and history views. This nicely abstracts away the model classname making the code less fragile. I've also changed to using filter and exists instead of a try...except block with get. It's more streamlined that way, and actually results in a simpler query, as well.

Solution 2

I'm just posting this here since the top comment is no longer the most up to date answer. I'm using Django 1.9, I'm not sure when this is change took place.

For example, you have different Venues and different users associated with each Venue, the model will look something like this:

class Venue(models.Model):
    user = models.ForeignKey(User)
    venue_name = models.CharField(max_length=255)
    area = models.CharField(max_length=255)

Now, staff status for the user must be true if he allowed to log in through the django admin panel.

The admin.py looks something like:

class FilterUserAdmin(admin.ModelAdmin): 
    def save_model(self, request, obj, form, change):
        if getattr(obj, 'user', None) is None:  
            obj.user = request.user
        obj.save()
    def get_queryset(self, request):
        qs = super(FilterUserAdmin, self).queryset(request)
        if request.user.is_superuser:
            return qs
        return qs.filter(user=request.user)
    def has_change_permission(self, request, obj=None):
        if not obj:
            return True 
        return obj.user == request.user or request.user.is_superuser


@admin.register(Venue)
class VenueAdmin(admin.ModelAdmin):
    pass

function name has changed from queryset to get_queryset.

EDIT: I wanted to extend my answer. There's another way to return filtered objects without using the queryset function. I do want to emphasise that I don't know if this method is more efficient or less efficient.

An alternative implementation for the get_queryset method is as follows:

def get_queryset(self, request):
    if request.user.is_superuser:
        return Venue.objects.all()
    else:
        return Venue.objects.filter(user=request.user)

Furthermore, we can also filter content is the relationships are more deeper.

class VenueDetails(models.Model):
    venue = models.ForeignKey(Venue)
    details = models.TextField()

Now, if I want to filter this model which has Venue as foreignkey but does not have User, my query changes as follows:

def get_queryset(self, request):
    if request.user.is_superuser:
        return VenueDetails.objects.all()
    else:
        return VenueDetails.objects.filter(venue__user=request.user)

Django ORM allows us to access different kinds of relationships which can be as deep as we want via '__'

Here's a link to the offical docs for the above.

Share:
28,514
Thiago Belem
Author by

Thiago Belem

I have proficiency in creating applications using Ruby on Rails & ReactJS. Using Agile methodologies (XP & Kanban), and keeping my code 100% covered via TDD and BDD.

Updated on July 09, 2022

Comments

  • Thiago Belem
    Thiago Belem almost 2 years

    I'm starting to organize a new project and let's say i'll have a few models like products and catalogs.

    I will allow my clients (not visitors, only specific clients) to login on Django Admin site to create, edit and delete their own catalogs.

    Lets say I create a model called "Shop", create every shop (name, address, logo, contact info and etc.) and create an admin user binded to that shop.

    Now I want this new admin (who's not a site admin, but a shop admin -- probably an user group) to see and edit only the catalogs linked with his shop.

    Is that possible?

    Should I do this inside the Django Admin or should I create a new "shop admin" app?

  • christophe31
    christophe31 about 12 years
    I took a look to the django admin code, it use the queryset method's queryset to object, so the second part of your code is not mandatory. (I've looked on django 1.4 source code)
  • Chris Pratt
    Chris Pratt about 12 years
    @christophe31: This was written prior even to Django 1.3, so it's possible something has changed. However, I am not aware of anything that would negate the need to still limit the change, delete and history views. Please provide some specifics.
  • christophe31
    christophe31 about 12 years
    for change and delete view, when they get the object they use: "self.queryset().get(pk=pk)" so it will return an error if user can't view the item.
  • jfmatt
    jfmatt over 11 years
    @ChrisPratt: This answer is great, but with one problem (as of November 2012). The default change_view takes four arguments, and extra_context is the optional fourth. By passing it on the way you do, you stomp on the form_url parameter, which decides the form action on the admin page. The solution is to either capture and pass on the form url as well or, to be more robust/futureproof, use extra_context=extra_context.
  • Ekin Ertaç
    Ekin Ertaç almost 9 years
    queryset method was changed on newer versions of Django (idk which version) to get_queryset
  • S_alj
    S_alj almost 7 years
    Thanks for taking the time to update this. It was very useful to me, I can confirm it works on Django 1.9 (at least).
  • NateB80
    NateB80 almost 7 years
    Great answer chatuur, one comment. You don't want to use your alternate get_queryset function because it will ignore any admin filters when returning the results. It is always best to call super in a function you override so any functionality provided elsewhere in django (like filters) is not lost.
  • djvg
    djvg almost 6 years
    @ChrisPratt Thanks for the warning. Does that still apply for django 2.0+? If so, what would be the best alternative to setting is_staff=True for a "non-employee" and then restricting permissions? Would it be acceptable to have a second admin site as suggested e.g. here?
  • Saturnix
    Saturnix over 2 years
    "the user could still play with the URL to access other objects they don't have access to" - I don't think that's the case anymore. I just tried with Django 3.0.8 and I get a 404 error if I try to view items that are excluded from one particular user through the use of get_queryset. The same URL viewed through a superuser returns the expected result.