Django: make ModelChoiceField evaluate queryset at run-time - django

I've overridden the default manager of my models in order to show only allowed items, according to the logged user (a sort of object-specific permission):
class User_manager(models.Manager):
def get_query_set(self):
""" Filter results according to logged user """
#Compose a filter dictionary with current user (stored in a middleware method)
user_filter = middleware.get_user_filter()
return super(User_manager, self).get_query_set().filter(**user_filter)
class Foo(models.Model):
objects = User_manager()
...
In this way, whenever I use Foo.objects, the current user is retrieved and a filter is applied to default queryset in order to show allowed records only.
Then, I have a model with a ForeignKey to Foo:
class Bar(models.Model):
foo = models.ForeignKey(Foo)
class BarForm(form.ModelForm):
class Meta:
model = Bar
When I compose BarForm I'm expecting to see only the filteres Foo instances but the filter is not applied. I think it is because the queryset is evaluated and cached on Django start-up, when no user is logged and no filter is applied.
Is there a method to make Django evalutate the ModelChoice queryset at run-time, without having to make it explicit in the form definition? (despite of all performance issues...)
EDIT
I've found where the queryset is evaluated (django\db\models\fields\related.py: 887):
def formfield(self, **kwargs):
db = kwargs.pop('using', None)
defaults = {
'form_class': forms.ModelChoiceField,
'queryset': self.rel.to._default_manager.using(db).complex_filter(self.rel.limit_choices_to),
'to_field_name': self.rel.field_name,
}
defaults.update(kwargs)
return super(ForeignKey, self).formfield(**defaults)
Any hint?

Had exactly this problem -- needed to populate select form with user objects from a group, but fun_vit's answer is incorrect (at least for django 1.5)
Firstly, you don't want to overwrite the field['somefield'].choices object -- it is a ModelChoiceIterator object, not a queryset. Secondly, a comment in django.forms.BaseForm warns you against overriding base_fields:
# The base_fields class attribute is the *class-wide* definition of
# fields. Because a particular *instance* of the class might want to
# alter self.fields, we create self.fields here by copying base_fields.
# Instances should always modify self.fields; they should not modify
# self.base_fields.
This worked for me (django 1.5):
class MyForm(ModelForm):
users = ModelMultipleChoiceField(queryset=User.objects.none())
def __init__(self, *args, **kwargs):
super(MyForm, self).__init__(*args,**kwargs)
site = Site.objects.get_current()
self.fields['users'].queryset = site.user_group.user_set.all()
class Meta:
model = MyModel

i use init of custom form:
class BT_Form(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(BT_Form, self).__init__(*args, **kwargs)
#prepare new values
cities = [(u'',u'------')] #default value
cities.extend([
(
c.pk,
c.__unicode__()
) for c in City.objects.filter(enabled=True).all()
])
self.fields['fly_from_city'].choices = cities #renew values

No way: I had to rewrite queryset definition (which is evaluated at startup)

Related

ModelChoiceField in forms.Form won't validate if queryset is overridden

I have a django ModelChoiceField that won't validate if I override the queryset.
class PersonalNote(forms.Form):
tile = ModelChoiceField(queryset=Tile.objects.none())
note = forms.CharField()
form = PersonalNote()
form.fields['tile'].queryset = Tile.objects.filter(section__xxx=yyy)
The form.is_valid() error is: "Select a valid choice. That choice is not one of the available choices".
If Tile.objects.none() is replaced with Tile.objects.all() it validates, but loads far too much data from the database. I've also tried:
class PersonalNote(forms.Form):
tile = ModelChoiceField(queryset=Tile.objects.none())
note = forms.CharField()
def __init__(self, *args, **kwargs):
yyy = kwargs.pop('yyy', None)
super(PersonalNote, self).__init__(*args, **kwargs)
if yyy:
self.fields['tile'].queryset = Tile.objects.filter(section__xxx=yyy)
What might be wrong here? Note the real application also overrides the label, but that does not seem to be a factor here:
class ModelChoiceField2(forms.ModelChoiceField):
def label_from_instance(self, obj):
assert isinstance(obj,Tile)
return obj.child_title()
After 2 hours I found the solution. Because you specified a queryset of none in the class definition, when you instantiate that PersonalNote(request.POST) to be validated it is referenceing a null query set
class PersonalNote(forms.Form):
tile = ModelChoiceField(queryset=Tile.objects.none())
note = forms.CharField()
To fix this, when you create your form based on a POST request be sure to overwrite your queryset AGAIN before you check is_valid()
def some_view_def(request):
form = PersonalNote(request.POST)
**form.fields['tile'].queryset = Tile.objects.filter(section__xxx=yyy)**
if form.is_valid():
#Do whatever it is
When you pass an empty queryset to ModelChoiceField you're saying that nothing will be valid for that field. Perhaps you could filter the queryset so there aren't too many options.
I also had this problem. The idea is to dynamically change the queryset of a ModelChoiceField based on a condition (in my case it was a filter made by another ModelChoiceField).
So, having the next model as example:
class FilterModel(models.Model):
name = models.CharField()
class FooModel(models.Model):
filter_field = models.ForeignKey(FilterModel)
name = models.CharField()
class MyModel(models.Model):
foo_field = models.ForeignKey(FooModel)
As you can see, MyModel has a foreign key with FooModel, but not with FilterModel. So, in order to filter the FooModel options, I added a new ModelChoiceField on my form:
class MyForm(forms.ModelForm):
class Meta:
model = MyModel
def __init__(self, *args, **kwargs):
# your code here
self.fields['my_filter_field'] = forms.ModelChoiceField(FilterModel, initial=my_filter_field_selected)
self.fields['my_filter_field'].queryset = FilterModel.objects.all()
Then, on your Front-End you can use Ajax to load the options of foo_field, based on the selected value of my_filter_field. At this point everyting should be working. But, when the form is loaded, it will bring all the posible options from FooModel. To avoid this, you need to dynamically change the queryset of foo_field.
On my form view, I passed a new argument to MyForm:
id_filter_field = request.POST.get('my_filter_field', None)
form = MyForm(data=request.POST, id_filter_field=id_filter_field)
Now, you can use that argument on MyForm to change the queryset:
class MyForm(forms.ModelForm):
# your code here
def __init__(self, *args, **kwargs):
self.id_filter_field = kwargs.pop('id_filter_field', None)
# your code here
if self.id_filter_field:
self.fields['foo_field'].queryset = FooModel.objects.filter(filter_field_id=self.id_filter_field)
else:
self.fields['foo_field'].queryset = FooModel.objects.none()

How to override the queryset giving the filters in list_filter?

Given the following models
class AnotherModel(models.Model):
n = models.IntegerField()
class MyModel(models.Model):
somefield = models.ForeignKey(AnotherModel)
and admin
class MyModelAdmin(admin.ModelAdmin):
list_filter = ('somefield',)
how can I filter the instances of AnotherModel to show only those with a given n value in my admin filter?
I need something like:
Filter
By somefield
all
[list of AnotherModel instances with given n]
See ModelAdmin.queryset and ModelAdmin.formfield_for_foreignkey. From the docs:
The queryset method on a ModelAdmin returns a QuerySet of all model instances that can be edited by the admin site. One use case for overriding this method is to show objects owned by the logged-in user:
class MyModelAdmin(admin.ModelAdmin):
def queryset(self, request):
qs = super(MyModelAdmin, self).queryset(request)
if request.user.is_superuser:
return qs
return qs.filter(author=request.user)
The formfield_for_foreignkey method on a ModelAdmin allows you to override the default formfield for a foreign keys field. For example, to return a subset of objects for this foreign key field based on the user:
class MyModelAdmin(admin.ModelAdmin):
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == "car":
kwargs["queryset"] = Car.objects.filter(owner=request.user)
return super(MyModelAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
This uses the HttpRequest instance to filter the Car foreign key field to only display the cars owned by the User instance.
[update]
Sorry, I failed to read the "filter" part. In Django >= 1.4 you can pass a subclass of django.contrib.admin.SimpleListFilter in the list_filter argument list, which you can use in order to override the lookups and queryset methods.
from datetime import date
from django.contrib import admin
from django.utils.translation import ugettext_lazy as _
class DecadeBornListFilter(admin.SimpleListFilter):
# Human-readable title which will be displayed in the
# right admin sidebar just above the filter options.
title = _('decade born')
# Parameter for the filter that will be used in the URL query.
parameter_name = 'decade'
def lookups(self, request, model_admin):
"""
Returns a list of tuples. The first element in each
tuple is the coded value for the option that will
appear in the URL query. The second element is the
human-readable name for the option that will appear
in the right sidebar.
"""
return (
('80s', _('in the eighties')),
('90s', _('in the nineties')),
)
def queryset(self, request, queryset):
"""
Returns the filtered queryset based on the value
provided in the query string and retrievable via
`self.value()`.
"""
# Compare the requested value (either '80s' or '90s')
# to decide how to filter the queryset.
if self.value() == '80s':
return queryset.filter(birthday__gte=date(1980, 1, 1),
birthday__lte=date(1989, 12, 31))
if self.value() == '90s':
return queryset.filter(birthday__gte=date(1990, 1, 1),
birthday__lte=date(1999, 12, 31))
class PersonAdmin(admin.ModelAdmin):
list_filter = (DecadeBornListFilter,)
Edit - this method has been pointed out to have issues, see below
You can do it like this:
Let's say you have a model called Animal, which has a ForeignKey field to a model called Species. In a particular admin list, you want to allow only certain species to be shown in the animals filter choices.
First, specify a custom ListFilter called SpeciesFilter in the Animal's ModelAdmin:
class AnimalAdmin(ModelAdmin):
list_filter = (('species', SpeciesFilter),)
Then define the SpeciesFilter:
from django.contrib.admin.filters import RelatedFieldListFilter
class SpeciesFilter(RelatedFieldListFilter):
def __init__(self, field, request, *args, **kwargs):
"""Get the species you want to limit it to.
This could be determined by the request,
But in this example we'll just specify an
arbitrary species"""
species = Species.objects.get(name='Tarantula')
#Limit the choices on the field
field.rel.limit_choices_to = {'species': species}
#Let the RelatedFieldListFilter do its magic
super(SpeciesFilter, self).__init__(field, request, *args, **kwargs)
That should do it.
I found another method similar to #seddonym, but doesn't mess with the caching. It is based on this Django code, but uses undocumented method field_choices, which can be subject to change in the future Django releases. The code for #seddonym's case would be:
from django.contrib.admin.filters import RelatedFieldListFilter
class SpeciesFilter(RelatedFieldListFilter):
def field_choices(self, field, request, model_admin):
return field.get_choices(include_blank=False, limit_choices_to={'name': 'Tarantula'})
Or in my case the working code is:
from django.contrib.admin.filters import RelatedFieldListFilter
class UnitFilter(RelatedFieldListFilter):
def field_choices(self, field, request, model_admin):
return field.get_choices(include_blank=False, limit_choices_to={'pk__in': request.user.administrated_units.all()})
I had to create my lookup fields from db table. I created custom filter class as below and displaying only related values to logged in user and filter accordingly:
class ShiftFilter_Org(admin.SimpleListFilter):
title = 'Organisation'
parameter_name = 'org'
def lookups(self, request, model_admin):
"""Get the organisations you want to limit"""
qs_org = Organisation.objects.filter(users=request.user)
list_org = []
for og in qs_org:
list_org.append(
(og.id, og.name)
)
return (
sorted(list_org, key=lambda tp:tp[1])
)
def queryset(self, request, queryset):
if self.value():
return queryset.filter(org=self.value())
For more visit Getting the most out of Django Admin filters
To use RelatedFieldListFilter like suggested in some answers, you need to pass a tuple to list_filter. Following the classes definition used in the original question, you would have:
class MyModelAdmin(admin.ModelAdmin):
class AnotherModelListFilter(admin.RelatedFieldListFilter):
def field_choices(self, field, request, model_admin):
ordering = self.field_admin_ordering(field, request, model_admin)
return field.get_choices(
include_blank=False,
ordering=ordering,
limit_choices_to=dict(n=<THE_VALUE_YOU_WANT>),
)
list_filter = (('somefield', AnotherModelListFilter),)

Dynamic fields in Django Admin

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

trying to get request.user, and then a query, in a form that overrides ModelChoiceField and is subclassed

I need to pass an instance variable (self.rank) to be used by a class variable (provider) (see the commented out line below).
Commented out, the code below works. But I'm pretty sure I shouldn't be trying to pass an instance variable up to a class variable anyway. So I'm dumbfounded as to how to accomplish my goal, which is to dynamically filter down my data in the ModelChoiceField.
As you can see, I already overrided ModelChoiceField so I could beautify the usernames. And I also subclassed my basic SwapForm because I have several other forms I'm using (not shown here).
Another way of saying what I need ... I want the value of request.user in my Form so I can then determine the rank of that user and then filter out my Users by rank to build a smaller ModelChoiceField (that looks good too). Note that in my views.py, I call the form using:
form = NewSwapForm(request.user)
or
form = NewSwapForm(request.user, request.POST)
In forms.py:
from myapp.swaps.models import Swaps
from django.contrib.auth.models import User
class UserModelChoiceField(forms.ModelChoiceField):
""" Override the ModelChoiceField to display friendlier name """
def label_from_instance(self, obj):
return "%s" % (obj.get_full_name())
class SwapForm(forms.ModelForm):
""" Basic form from Swaps model. See inherited models below. """
class Meta:
model = Swaps
class NewSwapForm(SwapForm):
# Using a custom argument 'user'
def __init__(self, user, *args, **kwargs):
super(NewSwapForm, self).__init__(*args, **kwargs)
self.rank = User.objects.get(id=user.id).firefighter_rank_set.get().rank
provider = UserModelChoiceField(User.objects.all().
order_by('last_name').
filter(firefighter__hirestatus='active')
### .filter(firefighter_rank__rank=self.rank) ###
)
class Meta(SwapForm.Meta):
model = Swaps
fields = ['provider', 'date_swapped', 'swap_shift']
Thanks!
You can't do it that way, because self doesn't exist at that point - and even if you could, that would be executed at define time, so the rank would be static for all instantiations of the form.
Instead, do it in __init__:
provider = UserModelChoiceField(User.objects.none())
def __init__(self, user, *args, **kwargs):
super(NewSwapForm, self).__init__(*args, **kwargs)
rank = User.objects.get(id=user.id).firefighter_rank_set.get().rank # ??
self.fields['provider'].queryset = User.objects.order_by('last_name').filter(
firefighter__hirestatus='active', firefighter_rank__rank=rank)
I've put a question mark next to the rank line, because rank_set.get() isn't valid... not sure what you meant there.

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

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