[Django]-How to limit fields in django-admin depending on user?

21👍

I think there is a more easy way to do that:

Guest we have the same problem of Blog-Post

blog/models.py:

Class Blog(models.Model):
     ...
     #fields like autor, title, stuff..
     ...

class Post(models.Model):
     ...
     #fields like blog, title, stuff..
     ...
     approved = models.BooleanField(default=False)
     approved_by = models.ForeignKey(User) 
     class Meta:
         permissions = (
             ("can_approve_post", "Can approve post"),
         )

And the magic is in the admin:

blog/admin.py:

...
from django.views.decorators.csrf import csrf_protect
...
def has_approval_permission(request, obj=None):
     if request.user.has_perm('blog.can_approve_post'):
         return True
     return False

Class PostAdmin(admin.ModelAdmin):
     @csrf_protect
     def changelist_view(self, request, extra_context=None):
         if not has_approval_permission(request):
             self.list_display = [...] # list of fields to show if user can't approve the post
             self.editable = [...]
         else:
             self.list_display = [...] # list of fields to show if user can approve the post
         return super(PostAdmin, self).changelist_view(request, extra_context)
     def get_form(self, request, obj=None, **kwargs):
         if not has_approval_permission(request, obj):
             self.fields = [...] # same thing
         else:
             self.fields = ['approved']
         return super(PostAdmin, self).get_form(request, obj, **kwargs)

In this way you can use the api of custom permission in django, and you can override the methods for save the model or get the queryset if you have to. In the methid has_approval_permission you can define the logic of when the user can or can’t to do something.

7👍

Starting Django 1.7, you can now use the get_fields hook which makes it so much simpler to implement conditional fields.

class MyModelAdmin(admin.ModelAdmin):
    ...

    def get_fields(self, request, obj=None):
        fields = super(MyModelAdmin, self).get_fields(request, obj)
        if request.user.is_superuser:
            fields += ('approve',)

        return fields

2👍

I have a system kind of like this on a project that I’m just finishing up. There will be a lot of work to put this together, but here are some of the components that I had to make my system work:

  • You need a way to define an Editor and a Supervisor. The three ways this could be done are 1.) by having an M2M field that defines the Supervisor [and assuming that everyone else with permission to read/write is an Editor], 2.) make 2 new User models that inherit from User [probably more work than necessary] or 3.) use the django.auth ability to have a UserProfile class. Method #1 is probably the most reasonable.

  • Once you can identify what type the user is, you need a way to generically enforce the authorization you’re looking for. I think the best route here is probably a generic admin model.

  • Lastly you’ll need some type of “parent” model that will hold the permissions for whatever needs to be moderated. For example, if you had a Blog model and BlogPost model (assuming multiple blogs within the same site), then Blog is the parent model (it can hold the permissions of who approves what). However, if you have a single blog and there is no parent model for BlogPost, we’ll need some place to store the permissions. I’ve found the ContentType works out well here.

Here’s some ideas in code (untested and more conceptual than actual).

Make a new app called ‘moderated’ which will hold our generic stuff.

moderated.models.py

class ModeratedModelParent(models.Model):
    """Class to govern rules for a given model"""
    content_type = models.OneToOneField(ContentType)
    can_approve = models.ManyToManyField(User)

class ModeratedModel(models.Model):
    """Class to implement a model that is moderated by a supervisor"""
    is_approved = models.BooleanField(default=False)

    def get_parent_instance(self):
        """
        If the model already has a parent, override to return the parent's type
        For example, for a BlogPost model it could return self.parent_blog
        """

        # Get self's ContentType then return ModeratedModelParent for that type
        self_content_type = ContentType.objects.get_for_model(self)
        try:            
            return ModeratedModelParent.objects.get(content_type=self_content_type)
        except:
            # Create it if it doesn't already exist...
            return ModeratedModelParent.objects.create(content_type=self_content_type).save()

    class Meta:
        abstract = True

So now we should have a generic, re-usable bit of code that we can identify the permission for a given model (which we’ll identify the model by it’s Content Type).

Next, we can implement our policies in the admin, again through a generic model:

moderated.admin.py

class ModeratedModelAdmin(admin.ModelAdmin):

    # Save our request object for later
    def __call__(self, request, url):
        self.request = request
        return super(ModeratedModelAdmin, self).__call__(request, url)

    # Adjust our 'is_approved' widget based on the parent permissions
    def formfield_for_dbfield(self, db_field, **kwargs):
        if db_field.name == 'is_approved':
            if not self.request.user in self.get_parent_instance().can_approve.all():
                kwargs['widget'] = forms.CheckboxInput(attrs={ 'disabled':'disabled' })

    # Enforce our "unapproved" policy on saves
    def save_model(self, *args, **kwargs):
        if not self.request.user in self.get_parent_instance().can_approve.all():
            self.is_approved = False
        return super(ModeratedModelAdmin, self).save_model(*args, **kwargs)

Once these are setup and working, we can re-use them across many models as I’ve found once you add structured permissions for something like this, you easily want it for many other things.

Say for instance you have a news model, you would simply need to make it inherit off of the model we just made and you’re good.

# in your app's models.py
class NewsItem(ModeratedModel):
    title = models.CharField(max_length=200)
    text = models.TextField()


# in your app's admin.py
class NewsItemAdmin(ModeratedModelAdmin):
    pass

admin.site.register(NewsItem, NewsItemAdmin)

I’m sure I made some code errors and mistakes in there, but hopefully this can give you some ideas to act as a launching pad for whatever you decide to implement.

The last thing you have to do, which I’ll leave up to you, is to implement filtering for the is_approved items. (ie. you don’t want un-approved items being listed on the news section, right?)

2👍

The problem using the approach outlined by @diegueus9 is that the ModelAdmin acts liked a singleton and is not instanced for each request. This means that each request is modifying the same ModelAdmin object that is being accessed by other requests, which isn’t ideal. Below is the proposed solutions by @diegueus9:

# For example, get_form() modifies the single PostAdmin's fields on each request
...
class PostAdmin(ModelAdmin):
    def get_form(self, request, obj=None, **kwargs):
        if not has_approval_permission(request, obj):
            self.fields = [...] # list of fields to show if user can't approve the post
        else:
            self.fields = ['approved', ...] # add 'approved' to the list of fields if the user can approve the post
...

An alternative approach would be to pass fields as a keyword arg to the parent’s get_form() method like so:

...
from django.contrib.admin.util import flatten_fieldsets

class PostAdmin(ModelAdmin):
    def get_form(self, request, obj=None, **kwargs):
        if has_approval_permission(request, obj):
            fields = ['approved']
            if self.declared_fieldsets:
                fields += flatten_fieldsets(self.declared_fieldsets)

            # Update the keyword args as needed to allow the parent to build 
            # and return the ModelForm instance you require for the user given their perms
            kwargs.update({'fields': fields})
        return super(PostAdmin, self).get_form(request, obj=None, **kwargs)
...

This way, you are not modifying the PostAdmin singleton on every request; you are simply passing the appropriate keyword args needed to build and return the ModelForm from the parent.

It is probably worth looking at the get_form() method on the base ModelAdmin for more info: https://code.djangoproject.com/browser/django/trunk/django/contrib/admin/options.py#L431

Leave a comment