django-registration 3.0

Published: September 4, 2018. Filed under: Django, Python.

Today I’m pleased to announce the release of django-registration 3.0. This is a pretty big update, and one that’s been coming for a while, so I want to take a moment to go briefly through the changes (if you want the full version, you can check out the upgrade guide in the documentation).

This also marks the retirement of the 2.x release series of django-registration; 2.5.2 is on PyPI, and I intend for it to be the last 2.x release. The 2.x branch still exists in the repository, but I don’t intend to make any further commits to it, and will probably delete it at some point in the future.

A bit of history

I first started working on django-registration back in 2007. At the time, Django didn’t have a good open-source user-registration system. Ellington, the news CMS Django originated from, did have a user-registration system, and artifacts of it hung around in Django for a couple releases, but it was never open-sourced.

And I was working on a side project which needed a user-registration system, and also wanted to increase the number of examples of “reusable” Django applications in the wild. So it seemed the most natural thing in the world to write a user-registration app and open-source it.

The original codebase was… OK for its time. It supported only one approach to doing registration — two-step signup-then-activate — and used an extra model, called RegistrationProfile, to store an activation key for newly-created accounts. It emulated Django’s generic function-based views in sprouting a ton of arguments that let you customize the way it worked. Eventually, that got to be a burden, and the details were handed off to a “backend” class you’d write and pass to the view, which then turned into just being class-based views once Django had support for them with proper base classes.

The first major hurdle was when Django introduced its current support for swapping out the default user model in the auth system. Prior to that point, django-registration could simply assume you were using Django’s auth system and thus Django’s default User model, and all was well. This came at a time when I was a little bit burned out anyway from dealing with what was an increasingly complex codebase (due to all the features it had evolved to support), and led to me doing an Achilles-style “go off and sulk” for a while. Which in turn led to a couple of popular forks showing up which aimed to keep the codebase up-to-date with where Django was going.

The 2.x release series of django-registration did support custom user models, and did it in what I think was a fairly clean way. It documented what was expected of your user model, told you how to make it work if you could meet those expectations, and told you (politely) to go write your own custom subclasses of the views and forms if your user model was too different from what the default setup could support.

The 2.0 release also shipped with three different registration workflows: the model-based activation one that had existed from the beginning, a one-step workflow, and an activation workflow that used no server-side storage other than the inactive account itself, taking advantage of Django’s signing tools. Rather than needing to be stored server-side, the activation key was now just an HMAC-verified value.

And things were OK, for a while. But there are always problems, and two in particular had been growing.

One was that the old model-based activation workflow was really showing its age, and maintaining it (and dealing with conflicting expectations from people who wanted it to work in different obvious-to-them ways) was getting to be a chore. The documentation recommended that new installs use the shiny new HMAC-based activation system, but the model workflow had to stick around for backwards compatibility.

The other may seem a bit silly, but it was a real issue. Since the earliest days, django-registration had tried to help people get started quickly by providing a URLconf (later, one per supported workflow) which set up not only django-registration’s own views, but also the built-in Django auth views, so that things like login, logout and password reset would be wired up for you immediately. And it gave names to those URLs, which were different from the names Django itself ended up adopting for them. And, most difficult of all, Django switched to class-based views in django.contrib.auth a few releases ago, which require a completely different set of URL patterns. In order to work across multiple Django versions, django-registration had to do some careful work at runtime to figure out which auth views your version of Django had, and wire up the correct URL patterns for you.

On top of that, there were a lot of little bits of code that were just historical artifacts of decisions I’d made a decade ago, and which ended up locked in for backwards compatibility purposes. So a while back I copied the existing 2.x code into its own branch, put a warning in the README that master was going to be unstable for a while, and started working on 3.0, with the freedom of being able to make backwards incompatible changes and finally clean up the codebase.

The big changes

The top-level module installed by django-registration was always called registration. Now, it’s called django_registration. There are a couple of reasons for this:

This has also resulted in changes to default template names and URL patterns, which previously used “registration” as their prefix and now use “django_registration”.

The old model-based activation workflow is gone. If you want two-step signup-then-activate, the HMAC-based workflow is still there, and is what you should use. The only vestige of the model-based version is a no-op database migration file that exists to make sure existing installs of django-registration can still safely run manage.py migrate (lingering references to removed models/apps can be tricky to get rid of).

And one of the biggest design mistakes in django-registration has finally been corrected: when a two-step workflow fails to activate an account, there’s now an official, clear way to indicate why and communicate it all the way through the view and into template rendering. Activation views now have an exception they can raise, with standardized messages and codes (like Django’s ValidationError) to explain what went wrong, and the built-in HMAC-based activation workflow provides an example of how to use that.

Other stuff

The upgrade guide, linked above, has the full details, but there are a few smaller changes I’m pretty happy with. There’s a new case-insensitive uniqueness validator, and a form class which will apply it to usernames. A lot of the historical backwards-compatibility shims — including some that literally go all the way back to 2007 — have finally been removed after raising deprecation warnings for years. The redirect handling is drastically simplified. You now have to wire up your own auth URLs (don’t worry, for most sites it’s a single call to include()), instead of hoping django-registration can correctly figure out what the right ones are and what they should be named. The current releases of Django — 1.11, 2.0 and 2.1 — are supported, on a variety of Python versions (including 3.7, which I test locally but can’t run in CI right now because Travis doesn’t have it yet).

The repository has been ported over to the layout I discussed back in June, and I couldn’t be happier with how that’s working out. The tox config file has borrowed a ton of useful tricks from Django’s own config, including spell-checking the documentation during builds.

There have also been some internal cleanups that hopefully shouldn’t affect any real-world deployments, but if I get bug reports I’ll add notes in the documentation. The biggest one is that django-registration no longer uses custom clean() methods in its forms to run additional validation; instead, it now attaches validators directly to the fields, and lets Django run them. This actually makes it easier to override/change django-registration’s default behavior, since now all you have to do is reach into the field validator lists and add/remove as desired.

The future

I hope the 3.0 release provides a solid foundation for continuing to work on django-registration for years to come. I know it’s not the most fashionable choice these days, since many sites are now mostly doing API backends with JS frontends, and using OAuth with external identity providers like Google and Facebook. But I think there is still a niche for something like django-registration, and there are two specific goals I want it to achieve.

The first goal is one that it’s always had: as much as possible, be a good example of how to do a reusable/distributable Django application. I gave a talk about this at the very first DjangoCon in 2008, and I’ve always felt that Django’s concept of an application was its secret weapon. I also have some more up-to-date thoughts on that, and plan to write more about the topic in the future, but for now django-registration represents the best I think I can do in terms of showing how to produce “pluggable” applications for distribution. This includes more than just code, by the way: the django-registration repository is something like ⅔ documentation by volume, and I’m still not happy with it.

The second goal is one that I’ve grown to care about over the years of maintaining django-registration: get the little things right. User registration is a surprisingly complex problem (as I’ve learned the hard way), and django-registration represents what I know about it, set down in code. It reserves a bunch of names in order to protect you from malicious or accidental collisions. It knows about and will do what it can to protect you from homograph attacks. It uses Django’s boring wrappers around Python’s boring standard library for most of the hard work, and especially for doing anything that even looks sort of like cryptography. It deliberately and gleefully violates RFCs by treating the local-part of an email address as case-insensitive, and if I thought I could get away with it, it would ignore dots and anything after a plus sign, too. In fact, if you just go read my rant about usernames from earlier this year, django-registration does everything in its power to adhere to what I think is the right way to do things (it doesn’t implement the tripartite identity pattern, because that would require replacing Django’s entire auth system, but maybe one day when I have the copious free time I’ll sit down and do just that).

There are a few specific things I’d like to add in future 3.x releases, though I don’t yet know when exactly I’ll get around to them or what they’ll look like. The two big ones are:

But those are considerations for the future. For now, I’ve got a few other apps that need at least a quick refresh release for Django 2.1, and I want to let django-registration 3.0 settle a bit before coming back to make any large additions to it.