Filtering foreign key choices in newforms-admin
I decided it was time to learn something about the newforms-admin branch of Django, so I set out to try to write a couple of simple models for administering a mail server (something we always needed at GMTA, but I have never really liked any of the currently available options). Here is the models so far:
class Customer(models.Model):
name = models.CharField(max_length=100)
slug = models.SlugField()
def __str__(self):
return self.name
class UserProfile(models.Model):
user = models.ForeignKey(User, unique=True)
customer = models.ForeignKey(Customer)
class Domain(models.Model):
name = models.CharField(max_length=100)
customer = models.ForeignKey(Customer)
def __str__(self):
return self.name
def save(self):
# When a user adds a Domain, save the current customer.
if not self.id:
self.customer = get_current_user().customer_set.all()[0]
# Call the super save method
super(Domain, self).save()
class Mailbox(models.Model):
name = models.CharField(max_length=100)
domain = models.ForeignKey(Domain)
password = models.CharField(max_length=100)
def __str__(self):
return '%s@%s' % (self.name, self.domain)
class Meta:
verbose_name_plural = "mailboxes"
class Alias(models.Model):
name = models.CharField(max_length=100)
domain = models.ForeignKey(Domain)
destination = models.EmailField()
def __str__(self):
return '%s@%s -> %s' % (self.name, self.domain, self.destination)
class Meta:
verbose_name_plural = "aliases"
Model notes
There is a few gotchas in the above model code:
- Remember to add
AUTH_PROFILE_MODULEto your settings. See the Django Book on user profiles for details. - I use a thread locals hack to save the customer who owns a domain. See the Django wiki on Threadlocals and User for details.
- I am aware that this is a simplification (fx. a domain name should be unique...), but that's not the point with this article ;-)
The problems
There is a couple of issues we need to address:
- A
Usershould only be allowed to list/edit/delete the domains, mailboxes and alias of the Customer she is connected to. - When adding or editing a
MailboxorAlias, we only want theDomainobjects of the currentCustomeras well.
Handling permissions
Permissions is handled by overriding the queryset, has_add_permission, has_edit_permission and has_delete_permission on the ModelAdmin class, checking that the object in question is indeed connected to a Domain of the Customer. Here is an example of a Mailbox ModelAdmin class:
class MailboxOptions(admin.ModelAdmin):
list_display = ('name', 'domain')
ordering = ('domain', 'name')
fields = ('name', 'domain')
def queryset(self, request):
return Mailbox.objects.filter(
domain__customer=request.user.get_profile().customer
)
def has_module_permission(self, request):
return True
def has_add_permission(self, request):
return True
def has_change_permission(self, request, obj=None):
return not bool(obj) or \
obj.domain.customer == request.user.get_profile().customer
has_delete_permission = has_change_permission
I don't think there is any need to exhibit the other ModelAdmin derivees - they are pretty much alike.
Limiting the choices
When rendering a form field, the ModelAdmin class it's method formfield_for_dbfield which then delegates the work on to model field itself. The ForeignKey.formfield method accepts an extra keyword argument of the queryset that should be rendered as options. To our luck, all the way down, extra keyword arguments are passed with each call. So we just need a clever way of overriding the formfield_for_dbfield on the ModelAdmin class. I wrote an extra class:
class ForeignKeyFilter(object):
def formfield_for_dbfield(self, db_field, **kwargs):
# Only show the foreign key objects from the rendered filter.
filters = getattr(self, 'foreignkey_filters', None)
if filters and db_field.name in filters:
kwargs['queryset'] = filters[db_field.name](get_current_user())
return admin.ModelAdmin.formfield_for_dbfield(self, db_field, **kwargs)
Now every custom ModelAdmin class I create, need to inherit from this class as well. This makes it possible to set a property called foreignkey_filters which should be a dictionary with the field name as key and a callable as the value. The callable will be called with the current user as its first positional argument. Thus, when I implement it in the MailboxOptions class from before, it will now look like this:
class MailboxOptions(ForeignKeyFilter, admin.ModelAdmin):
list_display = ('name', 'domain')
ordering = ('domain', 'name')
fields = ('name', 'domain')
foreignkey_filters = {
'domain': lambda user: user.get_profile().customer.domain_set.all()
}
...
Conclusion
I have actually managed to implement something that could handle the goal of the Row Level Permission branch of Django. It's a bit hack'ish, but fun ;-)
Comments for "Filtering foreign key choices in newforms-admin"
Currently disabled.