views:

265

answers:

2

I suppose similar problem would have been discussed here, but I couldn't find it.

Let's suppose I have an Editor and a Supervisor. I want the Editor to be able to add new content (eg. a news post) but before publication it has to be acknowledged by Supervisor.

When Editor lists all items, I want to set some fields on the models (like an 'ack' field) as read-only (so he could know what had been ack'ed and what's still waiting approval) but the Supervisor should be able to change everything (list_editable would be perfect)

What are the possible solutions to this problem?

+1  A: 

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

T. Stone
+3  A: 

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.

diegueus9
you probably meant self.exclude = ['approved']in get_form() and there's also little glitch in changelist_view();)Thanks, this looks great and combined with bits from T.Stone's answer, it's exactly what I've been looking for :)
minder