Filtering Foreign Keys within Django Inline Classes

Jan. 28, 2019


Several months ago I posted a blog discussing how to perform form validation within the Django Admin (https://www.cloudstormllc.com/blog/19/validating-forms-within-the-django-admin). I discussed the use case thoroughly in the previous blog so I'm not going to go into detail again. In summary the use case was validating Notes by ensuring each Note for an Organization also includes a Contact who is an employee of the Organization. After all, it doesn't make sense to document a customer visit (via a "Note") and include a Contact who isn't associated with the Organization for which the Note is created. The previous blog demonstrated how a Note could be created from within the Note view of the Django Admin. From that view a user writes a Note and then selects the Organization and Contact before saving the Note. That requires validation after a User attempts to save a Note.

This may sound a little confusing so let's review the Django Note model:

class Note(models.Model):
    note = models.TextField(_('Call Note'), max_length=1000, blank=False, null=True)
    user = models.ForeignKey(User, related_name='user_notes', null=True)
    contact = models.ForeignKey(Contact, null=True)
    organization = models.ForeignKey(Organization, related_name='organization_notes',
                                                          null=True)

    def __str__(self):
        return self.note

The model itself is very straight forward. There is a TextField to represent the sales rep's notes and there are three foreign key relationships; 1) user represents a sales rep which is represented by Django's User model, 2) contact is a reference to a Contact object (which also has a foreign key relationship to an Organization), and 3) organization is a reference to a customer Organization. Note validation requires that a Note's Contact attribute also have a foreign key relationship to the Organization for which the Note is written. The previous post demonstrated how to do this using the NoteAdmin and a supporting form named NoteForm.

The validation works fine, but people are like electrons and prefer to take the path of least resistance. It didn't take long for users to begin requesting more efficient means of interacting with the system. The Organization model quickly turned out to be the center of gravity for the application. Users prefer to view an Organization and within the Organization view they want to see Contacts who belong to the Organization and all Notes written for the Organization. Django inlines to the rescue! However, Django inlines present a challenge because they don't follow the same path used by the NoteAdmin class. This is a problem for two reasons. The first reason is that all Contacts are present in the dropdown list when adding a new Note from the Note inline within the Organizaiton Admin view. However, the more serious issue is the fact that a Note can be added to the selected Organization using a Contact that doesn't belong to the Organization because the validation is bypassed.

Hmm...we have a problem. Our users demand the ability to use the Organization view as their primary interface, but that means there is a risk of the wrong Contact being assigned to an organizational Note. Also, no one likes selecting a Contact from the thousands stored in the system (there are between 3 - 20 Contacts per Organization). What do do? I got it! What if we can filter the list of Contacts available to select when adding a new Note from the inline within an Organization's view? After all, we already have a reference to an Organization if we are operating within its view. And we know a Contact has a foreign key relationship to an Organization which means that an Organization has a one to many relationship with Contacts. Now we are getting somewhere!

Organization Admin View Before

Below is an example of the Note inlines within the Organization view in the before we start working on our filtering:

Did you notice how the Note inline appears to show the note text twice? The reason that happens is because Django inline classes show the string representation, and from our Note model you can see that str is set to self.note which is the text of the Note. Let's make sure we deal with that as well because some Notes may be very long.

Organization Admin

Below is a snippet from the Organization Admin class:

class OrganizationAdmin(admin.ModelAdmin):
    model = Organization

    fieldsets = [
        (None, {'fields': ['organization_name', 'address1', 'address2', 'city', 'state', 'zipcode',
                           'region', 'country', 'website', 'customer_type', 'ab_number', 'is_customer',
                           'is_parent', 'parent']}),
    ]
    inlines = [ContactInLine, AddContactInLine, NoteInLine, AddNoteInLine]

    def get_form(self, request, obj=None, **kwargs):
        request._obj_ = obj
        return super(OrganizationAdmin, self).get_form(request, obj, **kwargs)

    class Media:
        css = {"all": ("css/hide_admin_original.css",)}

Note the inline definitions which includes AddNoteInLine. This is important because this is the inline where we will want to filter the Contacts for a given Organization. The other critical item is the get_form method. The request gets passed around and when an Organization is selected the obj attribute represents a reference to an Organization which we will need to filter the list of Contacts (each Contact has its own FK to an Organization). That's why we add the obj attribute to our request object (obj is not passed to the inline classes). Also, note the Media class. That's the code we need to hide the string representation of the Note inlines from appearing.

We are halfway there. The specified inlines are loaded and it is in the AddNoteInLine class where we need to filter and return only Contacts for the selected Organization (NoteInLine is a read only view of existing Notes).

Below is the code for the AddNoteInLine class:

class AddNoteInLine(admin.TabularInline):
    model = Note
    classes = ['collapse']

    def has_change_permission(self, request, obj=None):
        return False

    def has_add_permission(self, request, obj=None):
        return True

    def has_delete_permission(self, request, obj=None):
        return False

    extra = 0

    def formfield_for_foreignkey(self, db_field, request=None, **kwargs):

        field = super(AddNoteInLine, self).formfield_for_foreignkey(db_field, request, **kwargs)

        if db_field.name == 'contact':
            if request._obj_ is not None:
                field.queryset = field.queryset.filter(organization__exact=request._obj_)
            else:
                field.queryset = field.queryset.none()

        return field

The relevant code above is the formfield_for_foreignkey method. The formfield_for_foreignkey method on a ModelAdmin allows you to override the default formfield for a foreign keys field. Recall the get_form method invocation in the OrganizationAdmin class. When Django loads the inline classes it processes our custom formfield_for_foreignkey method because we have overridden the default which would have returned all Contacts. At this point we have the request object and db_field is a reference to the different attributes of an Organization. In this case we are only interested in Contacts that belong to an Organization (contact is the attribute in the Note model that represents a foreign key relationship). When the db_field.name matches the contact attribute we spring out trap! Recall we added an attribute names obj that is a reference to an Organization to our request object. We now use that to filter Contacts who belong to the selected Organization which, of course, is the Organization that was selected from within the Django Admin.

Organization Admin View After

When a user adds a new Note from the Organization Admin view the code above is triggered and only the current Organization's Contacts are available to be selected. The Note can be added and saved within the inline with the knowledge that the Note is valid meaning that the Contact belongs to the Organization. Below is how the filtered list will appear and note that the string representation no longer appears.

We covered some advanced features of the Django Admin interface, but hopefully this blog will be helpful if you have a need to filter foreign keys within a Django inline class. The tip about hiding the string representation of an inline class is also a very handy tip I stumbled across on Stack one day. Happy coding and feel free to enter a comment...we'd love to hear from you!

Comment Enter a new comment: