My Django ModelAdmin shows different forms in dependency on user permissions. I've solved it with ModelForm's (get_form, get_fieldsets), but what to do with inlines?
class NewsAdmin(admin.ModelAdmin):
form = NewsAdminForm
writing_form = NewsWritingForm
inlines = (LinkInline, FileInline)
ModelAdmin initializes them in ModelAdmin.__init__() before getting request object. The only way seems to use CSS to hide them.
You can use ModelAdmin get_inline_instances function.
Following code removes inlines from add_view:
from django.contrib import admin
from models import FooModel
class FooInline(admin.StackedInline):
model = FooModel
class MyModelAdmin(admin.ModelAdmin):
inlines = [FooInline]
def get_inline_instances(self, request, obj=None):
return obj and super(MyModelAdmin, self).get_inline_instances(request, obj) or []
ModelAdmin.__init__() takes the classes in ModelAdmin.inlines, instantiates them, and stores the results in ModelAdmin.inline_instances. You can set ModelAdmin.inline_instances to an empty list after __init__ to remove the inlines.
See line 243 of django/contrib/admin/options.py
I solved this by putting the logic to set self.inline_instances = [] in the get_readonly_fields(self, request, obj=None) method.
For example, if you wanted to show the inlines to superusers but not to anyone else:
def get_readonly_fields(self, request, obj=None):
if request.user.is_superuser:
return ()
else:
self.inline_instances = []
return ()
I used bskinner's trick to hide inlines when adding a new object, with two adjustments: First, I had to overwrite "inlines", not "inline_instances", and second, make sure you always return self.readonly_fields, to make sure you are preserving those when they are set.
def get_readonly_fields(self, request, obj=None):
if obj:
# edit mode... add fields here to make them read-only when editing
return self.readonly_fields
else:
self.inlines = []
return self.readonly_fields
Related
In the Django admin, I have an Inline, and I would like to filter the list of rows by the parent object.
I can override get_queryset(request) in my Inline, but I don't have access to the parent object.
This snippet is from Django's options.py:
def get_formset_kwargs(self, request, obj, inline, prefix):
formset_params = {
"instance": obj,
"prefix": prefix,
"queryset": inline.get_queryset(request),
}
This would by immediately solved, if Django would provide obj as an argument to inline.get_queryset().
How to implement a get_queryset() of an InlineModelAdmin instance, so that it has access to obj?
class ChildInline(admin.TabularInline):
...
def get_queryset(self, request):
???? how to get the parent object?
These lines of code are the implementation of how inline admin instance are instantiate in Django
def get_inline_instances(self, request, obj=None):
inline_instances = []
for inline_class in self.get_inlines(request, obj):
inline = inline_class(self.model, self.admin_site)
if request:
if not (
inline.has_view_or_change_permission(request, obj)
or inline.has_add_permission(request, obj)
or inline.has_delete_permission(request, obj)
):
continue
if not inline.has_add_permission(request, obj):
inline.max_num = 0
inline_instances.append(inline)
return inline_instances
as you can see there is no obj passed to the inline_class so normally you can't access the parent instance.
Override this function in your parent model's admin class and use assigned attribute 'parent_obj' in your get_queryset method will make it work.
# parent admin class
def get_inline_instances(self, request, obj=None):
inline_instances = super().get_inline_instances(request, obj)
for inline_instance in inline_instances:
inline_instance.parent_obj = obj
return inline_instances
# inline admin class
def get_queryset(self, request):
self.parent_obj # you can now access your parent obj
I reffered to https://stackoverflow.com/a/41065115/17524955 and provided one change:
replace args to kwargs which contain an object_id
def get_parent_obj_from_request(self, request):
resolved = resolve(request.path_info)
if resolved.kwargs.get('object_id'):
return self.parent_model.objects.get(pk=resolved.kwargs['object_id'])
return None
def get_queryset(self, request):
qs = super().get_queryset(request)
parent_obj = self.get_parent_obj_from_request(request)
I found this dirty hack. In my case Event is the parent-model:
class ChildInline(admin.TabularInline):
...
def get_queryset(self, request):
# dirty hack to get the parent-object (event).
# Please fix it and tell me,
# if you know a better way to get it.
event_id = request.path_info.split('/')[-2]
event = Event.objects.get(id=event_id)
...
If i see those questions, i understand, how many people works with django and completely don't understand it.
First.
In the Django admin, I have an Inline,
and I would like to filter the list of rows by the parent object.
You don't need to do it in Inline object. Inline is only the helper, who organize creation of InlineFormSet.
And on the first lines of __init__ of your InlineFormSet - it get parent_object and made "filter the list of rows by the parent object" for inline.queryset. (django.forms.models.py, row 904 in Django 4.07)
It means, if you want to filter inline.queryset by parent_id before, it has not any reason to do it twice.
Second.
Please avoid loops. for example - get_inline_instances is good to set parent_object. But not in form of #pakawinz answer. You can do it better:
def get_inline_instances(self, request, obj=None):
return ((instance, setattr(instance, 'parent_object', obj))[0] for instance in super().get_inline_instances(request, obj))
And you can do it much better:
def get_inlines(self, request, obj):
"""Hook for specifying custom inlines."""
return (type(inline.__name__, (inline,) {'parent_object': obj}) for inline in super().get_inlines(request, obj))
Second example add parent_object like an attribute to inline_class. it give you possibility to get parent_object in classmethods too.
Third
Your question already has the best possibility to give an inline a parent_object to use it in queryset
def get_formset_kwargs(self, request, obj, inline, prefix):
inline.parent_obj = obj
return super().get_formset_kwargs(request, obj, inline, prefix)
Four
#NackiE23 tells you a better way to get parent_object.
Please don't use path.split(), this is not works for add_view in ModelAdmin. in your case:
def get_queryset(self, request):
... # your staff
resolved = resolve(request.path_info) # this is a better way to get it.
if resolved.kwargs.get('object_id'):
event = resolved.func.__self__.get_object(request, resolved.kwargs.get('object_id'), to_field=None) # please check if you don't need to_field
else:
event = resolved.func.__self__.model()
Please check, how work your solution not only for change_view, also for add_view too.
Last
I work only with django.admin.contrib more than 7 years. You can find my Talks about django.admin.contrib on PyCon RU 2021, PyCon DE 2022, DjangoCon EU 2022 and, later, on DjangoCon US 2022.
With my experience I think, probably, you do something unnecessary in your code. But i am not sure, therefore i write you some solutions above.
Is there anyway to make the list_editable optional on a per object bases? For example the readonly fields attribute has this option, which doesn't affect the changelist_view.
class MyAdmin(admin.ModelAdmin):
readonly_fields = ('foo',)
def get_readonly_fields(self, request, obj=None):
fields = super(MyAdmin, self).get_readonly_fields(request, obj=obj)
if obj.status == 'CLOSED':
return fields + ('bar',)
return fields
The same can be achieved for list_display and some other attributes. It seems there isn't a method 'get_list_editable_fields'.
I want some of the rows to be immutable obviously, but other than raising a vulgar error doesn't seem to work. I didn't find any documentation about the attribute either
Would it somehow be possible to render the widget via a list_display getter?
class MyAdmin(admin.ModelAdmin):
list_display = ('get_bar',)
list_editable = ('get_bar',)
def get_bar(self, obj):
return widget or str(obj.bar) # ???
get_bar.allow_tags = True
update using Alasdair's feedback:
def get_changelist_formset(self, request, **kwargs):
"""
Returns a FormSet class for use on the changelist page if list_editable
is used.
"""
# I run through this code for each row in the changelist, but there's nothing in kwargs, so I don't know how to use the instance as a guide to which fields should be in list_editable?
defaults = {
"formfield_callback": partial(self.formfield_for_dbfield, request=request),
}
defaults.update(kwargs)
return modelformset_factory(
self.model, self.get_changelist_form(request), extra=0,
fields=self.list_editable, **defaults
)
As you say, there is no get_list_editable method.
Try overriding the get_changelist_formset method. I think you'll need to duplicate the entire method, and change the list of fields passed to modelformset_factory.
As said, there is not get_list_editable method in the ModelAdmin class, but it is possible to implement it easily (tested on django==2.1):
class MyAdminClass(admin.ModelAdmin):
def get_list_editable(self, request):
"""
get_list_editable method implementation,
django ModelAdmin doesn't provide it.
"""
dynamically_editable_fields = ('name', 'published', )
return dynamically_editable_fields
def get_changelist_instance(self, request):
"""
override admin method and list_editable property value
with values returned by our custom method implementation.
"""
self.list_editable = self.get_list_editable(request)
return super(MyAdminClass, self).get_changelist_instance(request)
Also, you could override the changelist_view and do something like that:
def changelist_view(self, request, extra_context=None):
resp = super(CustomModelAdmin, self).changelist_view(request, extra_context)
if something:
resp.context_data['cl'].formset = None
return resp
A little late but I found a way.
Override the get_changelist_instance()
def get_changelist_instance(self, request):
if request.user.is_superuser:
self.list_editable = ('state',) # replace state with list of fields you wish to be editable
else:
self.list_editable = ()
return super().get_changelist_instance(request)
It is better than overriding "get_changelist_formset" because get_changelist_formset() only runs if you have set list_editable to atleast one field. Link to Documentation
in the admin of a model I would like to allow the delete action only for some of the instances (my model has a DateTimeField and I would like to disable the delete action for instances which have this field set to the current month).
Anybody could help?
Thanks
EDIT
I tried the method proposed by Chris in his anser below, but obj is always None:
class UserProfileAdmin(admin.ModelAdmin):
def has_delete_permission(self, request, obj=None):
# obj is always None
return super(UserProfileAdmin, self).has_delete_permission(request, obj=obj)
class MyModelAdmin(admin.ModelAdmin):
...
def has_delete_permission(self, request, obj=None):
if obj is not None and \
obj.my_date_field.month == datetime.now().month and \
obj.my_date_field.year == datetime.now().year:
return False
return super(MyModelAdmin, self).has_delete_permission(request, obj=obj)
UPDATE:
It's not "always None", it's set to a specific object when a specific object can be ascertained. In the changelist, and particularly in your scenario when trying to bulk-delete from the changelist, it's set to None because no individual object can obviously be determined.
If you need to account for deletion from the changelist, you'll have to create your own delete action and replace the default Django version. Something like:
class MyModelAdmin(admin.ModelAdmin):
...
actions = ['limited_delete_selected']
# Need to remove the default delete_selected action
def get_actions(self, request):
actions = super(MyModelAdmin, self).get_actions(request)
if actions.has_key('delete_selected'):
del actions['delete_selected']
return actions
def limited_delete_selected(self, request, queryset):
# filter selected items to only those that are actually deletable
now = datetime.now()
queryset = queryset.exclude(date_field__month=now.month, date_field__year=now.year)
# call Django's delete_selected with limited queryset
from django.contrib.admin.actions import delete_selected
delete_selected(self, request, queryset)
limited_delete_selected.short_description = "Delete selected objects or whatever you want it to say"
You actually will need both the action and the original has_delete_permission since objects can be deleted individually on their change_form view.
Check out the docs around overriding built-in model methods: https://docs.djangoproject.com/en/1.3/topics/db/models/#overriding-model-methods
I want to have additional fields regarding value of one field. Therefor I build a custom admin form to add some new fields.
Related to the blogpost of jacobian 1 this is what I came up with:
class ProductAdminForm(forms.ModelForm):
class Meta:
model = Product
def __init__(self, *args, **kwargs):
super(ProductAdminForm, self).__init__(*args, **kwargs)
self.fields['foo'] = forms.IntegerField(label="foo")
class ProductAdmin(admin.ModelAdmin):
form = ProductAdminForm
admin.site.register(Product, ProductAdmin)
But the additional field 'foo' does not show up in the admin. If I add the field like this, all works fine but is not as dynamic as required, to add the fields regarding the value of another field of the model
class ProductAdminForm(forms.ModelForm):
foo = forms.IntegerField(label="foo")
class Meta:
model = Product
class ProductAdmin(admin.ModelAdmin):
form = ProductAdminForm
admin.site.register(Product, ProductAdmin)
So is there any initialize method that i have to trigger again to make the new field working? Or is there any other attempt?
Here is a solution to the problem. Thanks to koniiiik i tried to solve this by extending the *get_fieldsets* method
class ProductAdmin(admin.ModelAdmin):
def get_fieldsets(self, request, obj=None):
fieldsets = super(ProductAdmin, self).get_fieldsets(request, obj)
fieldsets[0][1]['fields'] += ['foo']
return fieldsets
If you use multiple fieldsets be sure to add the to the right fieldset by using the appropriate index.
The accepted answer above worked in older versions of django, and that's how I was doing it. This has now broken in later django versions (I am on 1.68 at the moment, but even that is old now).
The reason it is now broken is because any fields within fieldsets you return from ModelAdmin.get_fieldsets() are ultimately passed as the fields=parameter to modelform_factory(), which will give you an error because the fields on your list do not exist (and will not exist until your form is instantiated and its __init__ is called).
In order to fix this, we must override ModelAdmin.get_form() and supply a list of fields that does not include any extra fields that will be added later. The default behavior of get_form is to call get_fieldsets() for this information, and we must prevent that from happening:
# CHOOSE ONE
# newer versions of django use this
from django.contrib.admin.utils import flatten_fieldsets
# if above does not work, use this
from django.contrib.admin.util import flatten_fieldsets
class MyModelForm(ModelForm):
def __init__(self, *args, **kwargs):
super(MyModelForm, self).__init__(*args, **kwargs)
# add your dynamic fields here..
for fieldname in ('foo', 'bar', 'baz',):
self.fields[fieldname] = form.CharField()
class MyAdmin(ModelAdmin):
form = MyModelForm
fieldsets = [
# here you put the list of fieldsets you want displayed.. only
# including the ones that are not dynamic
]
def get_form(self, request, obj=None, **kwargs):
# By passing 'fields', we prevent ModelAdmin.get_form from
# looking up the fields itself by calling self.get_fieldsets()
# If you do not do this you will get an error from
# modelform_factory complaining about non-existent fields.
# use this line only for django before 1.9 (but after 1.5??)
kwargs['fields'] = flatten_fieldsets(self.declared_fieldsets)
# use this line only for django 1.9 and later
kwargs['fields'] = flatten_fieldsets(self.fieldsets)
return super(MyAdmin, self).get_form(request, obj, **kwargs)
def get_fieldsets(self, request, obj=None):
fieldsets = super(MyAdmin, self).get_fieldsets(request, obj)
newfieldsets = list(fieldsets)
fields = ['foo', 'bar', 'baz']
newfieldsets.append(['Dynamic Fields', { 'fields': fields }])
return newfieldsets
This works for adding dynamic fields in Django 1.9.3, using just a ModelAdmin class (no ModelForm) and by overriding get_fields. I don't know yet how robust it is:
class MyModelAdmin(admin.ModelAdmin):
fields = [('title','status', ), 'description', 'contact_person',]
exclude = ['material']
def get_fields(self, request, obj=None):
gf = super(MyModelAdmin, self).get_fields(request, obj)
new_dynamic_fields = [
('test1', forms.CharField()),
('test2', forms.ModelMultipleChoiceField(MyModel.objects.all(), widget=forms.CheckboxSelectMultiple)),
]
#without updating get_fields, the admin form will display w/o any new fields
#without updating base_fields or declared_fields, django will throw an error: django.core.exceptions.FieldError: Unknown field(s) (test) specified for MyModel. Check fields/fieldsets/exclude attributes of class MyModelAdmin.
for f in new_dynamic_fields:
#`gf.append(f[0])` results in multiple instances of the new fields
gf = gf + [f[0]]
#updating base_fields seems to have the same effect
self.form.declared_fields.update({f[0]:f[1]})
return gf
Maybe I am a bit late... However, I am using Django 3.0 and also wanted to dynamically ad some custom fields to the form, depending on the request.
I end up with a solution similar to the one described by #tehfink combined with #little_birdie.
However, just updating self.form.declared_fields as suggested didn't help. The result of this procedure is, that the list of custom fields defined in self.form.declared_fields always grows from request to request.
I solved this by initialising this dictionary first:
class ModelAdminGetCustomFieldsMixin(object):
def get_fields(self, request, obj=None):
fields = super().get_fields(request, obj=None)
self.form.declared_fields = {}
if obj:
for custom_attribute in custom_attribute_list:
self.form.declared_fields.update({custom_attribute.name: custom_attribute.field})
return fields
where custom_attribute.field is a form field instance.
Additionally, it was required to define a ModelForm, wherein during initialisation the custom fields have been added dynamically as well:
class SomeModelForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for custom_attribute in custom_attribute_list:
self.fields[custom_attribute.name] = custom_attribute.field
and use this ModelForm in the ModelAdmin.
Afterwards, the newly defined attributes can be used in, e.g., a fieldset.
While Jacob's post might work all right for regular ModelForms (even though it's more than a year and a half old), the admin is a somewhat different matter.
All the declarative way of defining models, forms ModelAdmins and whatnot makes heavy use of metaclasses and class introspection. Same with the admin – when you tell a ModelAdmin to use a specific form istead of creating a default one, it introspects the class. It gets the list of fields and other stuff from the class itself without instantiating it.
Your custom class, however, does not define the extra form field at class level, instead it dynamically adds one after it has been instantiated – that's too late for the ModelAdmin to recognize this change.
One way to go about your problem might be to subclass ModelAdmin and override its get_fieldsets method to actually instantiate the ModelForm class and get the list of fields from the instance instead of the class. You'll have to keep in mind, though, that this might be somewhat slower than the default implementation.
You can create dynamic fields and fieldset using the form meta class. Sample code is given below. Add the loop logic as per you requirements.
class CustomAdminFormMetaClass(ModelFormMetaclass):
"""
Metaclass for custom admin form with dynamic field
"""
def __new__(cls, name, bases, attrs):
for field in get_dynamic_fields: #add logic to get the fields
attrs[field] = forms.CharField(max_length=30) #add logic to the form field
return super(CustomAdminFormMetaClass, cls).__new__(cls, name, bases, attrs)
class CustomAdminForm(six.with_metaclass(CustomAdminFormMetaClass, forms.ModelForm)):
"""
Custom admin form
"""
class Meta:
model = ModelName
fields = "__all__"
class CustomAdmin(admin.ModelAdmin):
"""
Custom admin
"""
fieldsets = None
form = CustomAdminForm
def get_fieldsets(self, request, obj=None):
"""
Different fieldset for the admin form
"""
self.fieldsets = self.dynamic_fieldset(). #add logic to add the dynamic fieldset with fields
return super(CustomAdmin, self).get_fieldsets(request, obj)
def dynamic_fieldset(self):
"""
get the dynamic field sets
"""
fieldsets = []
for group in get_field_set_groups: #logic to get the field set group
fields = []
for field in get_group_fields: #logic to get the group fields
fields.append(field)
fieldset_values = {"fields": tuple(fields), "classes": ['collapse']}
fieldsets.append((group, fieldset_values))
fieldsets = tuple(fieldsets)
return fieldsets
Stephan's answer is elegant, but when I used in in dj1.6 it required the field to be a tuple.
The complete solution looked like this:
class ProductForm(ModelForm):
foo = CharField(label='foo')
class ProductAdmin(admin.ModelAdmin):
form = ProductForm
def get_fieldsets(self, request, obj=None):
fieldsets = super(ProductAdmin, self).get_fieldsets(request, obj)
fieldsets[0][1]['fields'] += ('foo', )
return fieldsets
not sure why that's not working, but could a possible workaround be to define the field statically (on the form) and then override it in the __init__?
I for a long time could not solve a problem with dynamic addition of fields.
The solution "little_birdie" really works. Thank you Birdie))
The only nuance is:
"Self.declared_fieldsets" should be replaced with "self.fieldsets".
#kwargs['fields'] = flatten_fieldsets(self.declared_fieldsets)
kwargs['fields'] = flatten_fieldsets(self.fieldsets)
I used version 1.10. Perhaps something has changed.
If someone finds an even simpler and elegant solution, show here.
Thanks to all )))
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?
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.
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
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?)
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