Reputation: 723
In Django 1.5.1 I setup a SAAS system for internal use, where each department gets their own set of data.
To do that, I use ModelAdmin.queryset (https://docs.djangoproject.com/en/1.5/ref/contrib/admin/#django.contrib.admin.ModelAdmin.queryset) to limit all data to only the records that belong to the department of the currently logged in user.
This works perfect for the main Admin functions (summary tables etc.). But anything I put on "ModelAdmin.list_filter" shows ALL values - obviously using the base query set and not the one I defined in ModelAdmin.queryset.
I can see how one can define a custom ModelAdmin.list_filter query here: https://docs.djangoproject.com/en/1.5/ref/contrib/admin/#django.contrib.admin.ModelAdmin.list_filter that lets you also define a custom filter manager, which can include a custom queryset.
But this is PER FILTER FIELD!
That seems like an awful lot of work for something that should be automatic once you add ModelAdmin.queryset!
Is there a simpler way I'm missing here, or maybe this is something I should ticket?
Thanks in advance,
Rich.
Peter, thanks for the kick in my pants. Turns out my understanding of my own problem was wrong!
================= Example from my code ================
class Company(models.Model):
"""Company model."""
accountid = models.CharField(_('Account Number'), max_length=20, null=False, blank=True, help_text="Link to old system and records")
name = models.CharField(_('name'), max_length=200, unique=True, help_text=_("Enter full company name, no abbreviations. Duplicates are not allowed."))
nickname = models.CharField(_('nickname'), max_length=50, blank=True, null=True, help_text=_("Enter one or more keywords or abbreviations seperated by a space to make searching easier"))
slug = AutoSlugField(_('slug'), max_length=50, unique=True, blank=False, populate_from=('name', ))
site = models.ForeignKey(Site, default="0", blank=False, null=False, editable=False, help_text="Indicate which site this record belongs to. ")
class CompanyAdmin(admin.ModelAdmin):
list_filter = ('type', 'lead_quality', )
def queryset(self, request):
qs = super(PersonAdmin, self).queryset(request)
if request.user.is_superuser:
self.message_user(request, "Warning: This is the ADMINISTRATOR view!!", 'warning')
return qs
return qs.filter(site__id=request.session['site'].id)
def save_model(self, request, obj, form, change):
if change:
if obj.site.id != request.session['site'].id:
logger.debug("Contacts.Person.save_model: replacing site (%s) with (%s) " % (repr(obj.site), repr(request.session['site'])) )
else:
logger.debug("Contacts.Person.save_model: setting site (%s)" % (repr(request.session['site'])) )
obj.site = request.session['site']
obj.save()
class Person(models.Model):
"""Person model."""
first_name = models.CharField(_('first name'), max_length=100)
last_name = models.CharField(_('last name'), max_length=200)
slug = AutoSlugField(_('slug'), max_length=50, unique=True, blank=False, populate_from=('first_name', 'last_name'))
company = models.ForeignKey(Company, blank=True, null=True, help_text=_("If this person is associated with a Company, indicate which one here.") )
site = models.ForeignKey(Site, default="0", blank=False, null=False, editable=False, help_text="Indicate which site this record belongs to. ")
class PersonAdmin(admin.ModelAdmin):
list_filter = ('company',)
def queryset(self, request):
qs = super(PersonAdmin, self).queryset(request)
if request.user.is_superuser:
self.message_user(request, "Warning: This is the ADMINISTRATOR view!!", 'warning')
return qs
return qs.filter(site__id=request.session['site'].id)
def save_model(self, request, obj, form, change):
if change:
if obj.site.id != request.session['site'].id:
logger.debug("Contacts.Person.save_model: replacing site (%s) with (%s) " % (repr(obj.site), repr(request.session['site'])) )
else:
logger.debug("Contacts.Person.save_model: setting site (%s)" % (repr(request.session['site'])) )
obj.site = request.session['site']
obj.save()
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name in ('company',):
kwargs["queryset"] = Company.objects.get_query_set().filter(site__id=request.session['site'].id)
return super(PersonAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
So when a User views the summary screen for People, and they click on the Filter for Company, they see choices belonging to other company divisions.
If they click on one belonging to another division they get an empty list since the PersonAdmin cannot actually access that record.
Upvotes: 3
Views: 2368
Reputation: 37319
Is there any particular reason you expect the related fields in list_filter
to care about the queryset on the admin class? Such filters do not generally remove values that don't have a related object present.
If you don't want that behavior, you can write a single filter class that limits the choices however you like as a subset of django.contrib.admin.filter.RelatedFieldListFilter
and use that instead, either overriding its choices
method (the queryset as you defined it on the model admin will be available as cl.root_query_set
within that method) or overriding its __init__
method to create self.lookup_choices
differently - perhaps based on the request. I see no reason you should have to keep redefining the class - one definition should work with as many related fields as you like.
Here's a simple one that should only include items in the filter if the admin's filtered queryset has at least one object for the filter value:
class RelatedFieldRestrictedListFilter(RelatedFieldListFilter):
def choices(self, cl):
from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
yield {
'selected': self.lookup_val is None and not self.lookup_val_isnull,
'query_string': cl.get_query_string({},
[self.lookup_kwarg, self.lookup_kwarg_isnull]),
'display': _('All'),
}
for pk_val, val in self.lookup_choices:
if cl.root_query_set.filter(**{self.lookup_kwarg: pk_val}).exists():
yield {
'selected': self.lookup_val == smart_unicode(pk_val),
'query_string': cl.get_query_string({
self.lookup_kwarg: pk_val,
}, [self.lookup_kwarg_isnull]),
'display': val,
}
if (isinstance(self.field, models.related.RelatedObject)
and self.field.field.null or hasattr(self.field, 'rel')
and self.field.null):
yield {
'selected': bool(self.lookup_val_isnull),
'query_string': cl.get_query_string({
self.lookup_kwarg_isnull: 'True',
}, [self.lookup_kwarg]),
'display': EMPTY_CHANGELIST_VALUE,
}
This does a separate query against the DB for each possible value in the list_filter
field, so it's a bit inefficient - if that turns into a problem you should customize __init__
instead.
Followup:
Ok, so the correct filter choices depend on the request session. That means you need to establish them in the __init__
method of a subclass. Your example shows a related field, so I'll use RelatedFieldListFilter
again - I'm honestly not sure the concept of a restricted queryset makes sense for any other kind of filter. To do that, the lazy method (shorter to write, less efficient) would be to call the superclass's __init__
, then change self.lookup_choices
. The less lazy method would be to entirely override __init__
.
The lazy method is something like this:
from django.utils.encoding import smart_unicode
class RelatedFieldRestrictedListFilter(RelatedFieldListFilter):
def __init__(self, field, request, params, model, model_admin, field_path):
super(RelatedFieldRestrictedListFilter, self).__init__(field, request, params, model, model_admin, field_path)
if 'site' in request.session:
self.lookup_choices = [(instance.pk, smart_unicode(instance) for instance in model.objects.filter(site=request.session['site'])]
else:
# something else here
The less lazy method would involve copying in the base code from the superclass's __init__
method and replacing self.lookup_choices = field.get_choices(include_blank=False)
line with the above.
Note that I'm allowing for the possibility that the session may not have a site
- you should think about what you want to happen if that's the case. And maybe not bother changing lookup_choices
if the user is a superuser.
Upvotes: 3