Validating Forms within the Django Admin

Sept. 19, 2018


Django provides a very functional admin console for managing data belonging to an application's models. The Django admin provides forms for changing, adding, and deleting data objects and it is extremely easy to configure. It does this by reading metadata from a Django applicaiton's models to provide a quick, model-centric interface where trusted users can manage content.

I was asked earlier this year to write an application that had a very short suspense. I decided to implement the models first and leverage the the Django Admin so users could get up and running while I worked on the custom views and templates. However, users kept asking for new features and constraints and I kept finding ways within the application's admin.py to accommodate user requests. Before I knew it the users were satisfied with the changes and the application was complete...with the only user interface being the Django admin site!

I know the Django documentation clearly states the admin site is not intended to be used as the entire front end for a site. But it's not my fault the Django admin is so functional with lots of capabilities! And besides, the Django tagline is The web framework for perfectionists with deadlines. I may not be a perfectionist but I certainly have deadlines!

Form Validation

The purpose of the applicaiton was to provide a customer relationship management (CRM) "Lite" solution to an internal sales team to serve as a bridge until the team can implement SalesForce. There were many requirements with one of the requirements to capture notes whenever a sales rep had interactions with a customer organization. Each "note" is entered by a user (sales rep) and includes references to a client contact and a customer organization. You can probably guess each of these is a model; sales reps use the default Django User model, and the application implements custom Contact and Organization models. The objective of this blog is to demonstrate how the Django admin can be used to enforce referential integrity. Specifically we want to ensure that the specified Contact actually belongs to the customer Organization for which the Note object is created.

Note Model

Below is the Python code for the 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.

Below is an example of a valid note in the Django admin:

But look what happens if we try to change the note where Yogi is still the contact but the Organization is changed to Frog's Leap:

You can see the Note object was not saved which is good because we want to prevent the creation of Note objects where the Contact does not belong to the selected Organization.

Below is the code in admin.py that makes all this happen:

class NoteForm(forms.ModelForm):
    class Meta:
        model = Note
        fields = "__all__"

    # The clean method gets invoked before the NoteAdmin's save_model method
    def clean(self):
        try:
            contact = self.cleaned_data['contact']
            org = self.cleaned_data['organization']
            contact_org = Contact.objects.get(pk=contact.pk).organization
            if contact_org == org:
            return self.cleaned_data
            else:
                raise ValidationError()
        except:
            msg = "{0} {1} does not belong to {2}".format(contact.first_name, contact.last_name,
                                                                                  org.organization_name)
            raise ValidationError(msg)

class NoteAdmin(admin.ModelAdmin):
    model = Note
    form = NoteForm

    # A superuser can create a note for anyone, but a sales rep can only create a note for themselves
    def save_model(self, request, obj, form, change):
        if obj.user == request.user or request.user.is_superuser:
            try:
                super().save_model(request, obj, form, change)
            except:
                pass

    fieldsets = [
        (None, {'fields': ['note', 'user', 'contact', 'organization']}),
    ]

    list_display = ('user', 'contact', 'organization', 'created', 'modified', 'note')

We create a NoteForm class which is used by the NoteAdmin class in order to validate that the assigned Contact belongs to the assigned Organization. Otherwise, a Note could be created for a Contact that doesn't belong to the specified Organization. This is done in the clean method which is automatically invoked when a Note is attempted to be saved. In fact, the clean method is invoked before the NoteAdmin save_model is even invoked. If you familiar with Django forms then the concept of "cleaned_data" should be familiar to you. If not, I recommend you read the Django documentation regarding the use of Forms.

The check itself is very simple. Data is stored in the self.cleaned_data and mapped to the model fields of the applicable model which is Note in this case. Recall that the Note model consists of four attributes (note, user, contact, and organization)*[]: We pull the contact and organization from the passed data in self.cleaned_data which gives us object references to the Contact and Organization. We then retrieve the specified Contact's Organization from the Contact object (contact_org = Contact.objects.get(pk=contact.pk).organization) and compare it to the Organization selected in the Note. If they are the same we return self.cleaned_data and the NoteAdmin's save_model is invoked. If the organizations don't match a validation is raised, an error message is constructed, and the Note object is not saved.

We don't have to write a custom save_model method within the Django admin because Django provides a save_model method. However, we override the method to ensure the user creating a Note is either the user (sales rep) selected in the Note object or is a superuser authorized to create Note objects on behalf of other users.

This post covers a fairly basic example of how to perform validation within the Django admin site. Hopefully, it also gives you a taste for the capabilities and flexibility of the Django's admin functionality which is surprisingly versatile and flexible.

Comment Enter a new comment: