Wellfire Interactive // Expertise for established Django SaaS applications

Where should that go, Django forms edition (This Old Pony #43)

A few years back I counseled a local startup that was trying to extend their web app. Even with a small user base they were experiencing technical challenges, and since we knew the founders I offered to help. What I saw was the worst PHP spaghetti code[0] I have ever laid eyes on. Miles upon miles of M, V, and C all wrapped up together. In the end we helped them with some simpler advice - move the DB to a different server, lock your cron jobs! - but as for the application code itself…. ¯_(ツ)_/¯

Separation of concerns in an application is one of the most basic ways of making software easier to work with. It makes testing easier, feature additions simpler, and goes a long way toward aiding comprehension. But where you draw the line is subjective.

This week I’d like to examine how to approach this with forms in Django.
 

The role of forms

Let’s start by defining our terms. A “form” in a Django app usually means any class that inherits from Django’s forms.Form class. So far so good. And what about the role that this class plays? Well that’s easy, too. The role - the purpose - of a form is to render an HTML form from a Python definition and to serialize data between HTTP requests and the rendered form and to validate submitted data against specified boundaries.

That’s the one role of a Django form….

Okay, so the role of forms isn’t so cut and dry, nor is to so, well, separated. I’m going to suggest here knocking out the first given purpose. In the absence of a special purpose rendering library, like Django Crispy Forms - which provides specific rendering classes - we’ve avoided using Django forms’ builtin rendering for a very long time.

That leaves serialization and validation. For our purposes here these are closely enough related[1] that we can call these a compound purpose or role: encapsulating submitted HTTP data. #
Form, view, or model? Now what happens when you want to do something with this data?

Let’s say you have a simple model with a few text fields and an association to a user. If you add a creation view, using Django’s CreateView, for example, where do you attach the user to the instance?[2]

You could assume a choice field on the form, but if we presume this much is something the user should not control then this is a bad idea. We could instead define a custom form class with the desired fields and then, without saving our data from the form instance (as a Django ModelForm is wont to do), attach the current user to the newly created instance by using the request. We could do that in the view or in a model method.

So which is it?

In this case, none of the above. In this case, we’d create a small form class that takes a user as an additional initialization argument, saving this to the instance, and then use this value in the form (presuming a ModelForm) save method.

If you’re following along you’re probably wondering how this fits into the role of a form. After all, interacting with the database is not the role of a form. That would be encapsulating submitted data - serializing and validating.

However the ModelForm gives us a little convenience: name the model class which it describes and, given the data, it will save a new instance of that model class.

That explains what is available. But why insist on keeping this extra data in the form class? Because the purpose of the role is to encapsulate the data for an instance, and while some of that data is sent over the wire, the rest is available from the request session. Adding it to the form makes it available for validation and for producing either a data dictionary with the necessary information to save a new instance or a new instance itself.

Additionally, it’s an excellent idea to keep as much business logic (which here can mean “anything that actually changes the database”) at least directly out of your views. As much as possible, as often as possible. The typical alternative here would be to use the form to validate the data, then in the view itself extract the cleaned data and create a new instance. That makes the view responsible for assigning data though, which is something we want to avoid.
 

What else can forms do?

Models, managers, and querysets are first things I’d reach for when looking to break down functionality in a Django app. But often it makes sense to attach functionality to your form classes, even if only to serve as an interface.

E.g. it makes more sense to have a “send” method on a contact form than to pull the data out of the form’s cleaned data and then redirect in a view. Even if all the form method does is send the data to a task function and delegate creating and sending an email, the form is the right layer for that to happen.

Hopefully this gives you a little to chew on this week. Last week’s letter never made it out due to some combination of killer bees, mind control rays, and a nasty virus(!).

Validly yours,
Ben

[0] The problem here is not “PHP is a bad language” rather “PHP lets you do things you would never want to do otherwise” like combine database access, data processing, raw queries, and pseudo-templated HTML in one file.
[1] Certainly it makes little sense to consider invalid data, e.g. the word “foo” where an integer is expected, to be properly serialized.
[2] Thanks to reader and startup oriented Django developer David, for framing this question (https://www.lessboring.com/)

Learn from more articles like this how to make the most out of your existing Django site.