Peter Percival Patterson’s Pet Pig Porky

π! I can rattle off 36 significant figures on demand, but how accurate do you really need to be? The JPL uses 15 digits for interplanetary navigation. What is needed, though?

Let’s start with a 1 m diameter circle, and add successive digits to the approximation. How many digits before you’re being absurd? I’ll give a definition of absurd, too: a difference in the circumference of less than the Planck length, approximately \(\LARGE{1.616229(38)\times10^{-35} {\rm m}}\)

  • For 1 m, the answer is fairly obvious: 36 significant figures
  • For d, 43 significant figures are needed
  • For the distance from the sun to Voyager 1, ~141 AU at time of writing, just 49 significant figures
  • For the Solar System, out to the distance where we believe the Oort cloud ends, ~100000 AU (i.e., the edge of the solar system), 53 significant figures
  • For the size of our observable universe, roughly 93000000000 light years, 63 is enough

What if the Bohr radius (approximate size of a hydrogen atom) was our standard, at \(\LARGE{5.2917721067(12)\times10^{-11}{\rm m}}\)? For the size of the observable universe, we only need 36 figures. Handily, that’s what I know! How much would one need for a circle the size of the orbit of Neptune? 23 digits.

So how accurate is JPL if they were measuring the circumference of a circle at the orbit of Neptune? They could be off by as much as a centimeter! Which, assuming appropriate feedback systems, isn’t going to be anything near significant.

How many do we know? 22459157718361 digits. Why? As George Leigh-Mallory put it, “Because it’s there”.

Who Are You

There seems to be a dearth of articles on setting up full-fledged “automatic” user registration systems for Django 2.0. Yes, there’s documentation, but there’s a lot to wade through, and it’s far from a step-by-step guide, which is what this aspires to be.

The problem for developing many web applications using Django is that adding a user requires logging in through the admin interface to do so. For smaller groups, say, 20 or so, this might be manageable, though annoying. If you develop for deployment to a wide audience, say the entire English speaking world, you don’t want people to email you requests all the time to be added so they can do whatever in your app.

I’ll be assuming that you have a passing familiarity with Django, and have made it through the tutorial and polls app. I’m also assuming that this is among the first things you’re setting up, and you won’t mind blowing away any data you have, because that’s the simplest way to do things in Step 3.

Step 1: Settings

You can customize these as your needs dictate, but here’s a bunch more things to add to your mysite.settings:

LOGIN_REDIRECT_URL = '/'
LOGIN_URL = '/accounts/login/'
LOGOUT_REDIRECT_URL = '/'
## Possible email backends. The uncommented one, also the default, is the one we want to use
##   by the end, but the others can be used in development. I find console the simplest for
##   that.
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
## Console
#EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
## File
#EMAIL_BACKEND = 'django.core.mail.backends.file.EmailBackend'
#EMAIL_FILE_PATH =  '/tmp/app-messages'
## In Memory (django.core.mail.outbox is a list of EmailMessage instances)
#EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend'
## Dummy (does nothing--same as /dev/null)
#EMAIL_BACKEND = 'django.core.mail.backends.dummy.EmailBackend'

## All options below are the default values. Unless you run your own SMTP server on localhost
##   with no authentication required (not recommended), you'll probably want to tweak these.
EMAIL_HOST = 'localhost'
EMAIL_PORT = 25
EMAIL_HOST_USER = '' #If this is the empty string, no authentication will be attempted
EMAIL_HOST_PASSWORD = ''
## The next two options are mutually exclusive.
EMAIL_USE_TLS = False
EMAIL_USE_SSL = False
EMAIL_TIMEOUT = None
EMAIL_SSL_KEYFILE = None #If EMAIL_USE_TLS or EMAIL_USE_SSL are True, specify the path to a
                         #  PEM formatted private key.
EMAIL_SSL_CERTFILE = None #If EMAIL_USE_TLS or EMAIL_USE_SSL are True, specify the path to a
                          #  PEM formatted certificate chain
EMAIL_SUBJECT_PREFIX = '[Django] '
EMAIL_USE_LOCALTIME = False #The default here will timestamp your message with UTC.

Step 2: Some Templates

The next thing we want to do is make a bunch of templates. It’s a tad backward from how Django is normally developed, but trust me. As always, customize as you wish.

In your templates directory, create a “registration” subdirectory. All these templates will live in there.

{% extends "base.html" %}

{% block content %}

{% if form.errors %}
<p>Your username and password didn't match. Please try again.</p>
{% endif %}

{% if next %}
    {% if user.is_authenticated %}
    <p>Your account doesn't have access to this page. To proceed,
    please login with an account that has access.</p>
    {% else %}
    <p>Please login to see this page.</p>
    {% endif %}
{% endif %}

<form method="post">
{% csrf_token %}
<table>
<tr>
    <td>{{ form.username.label_tag }}</td>
    <td>{{ form.username }}</td>
</tr>
<tr>
    <td>{{ form.password.label_tag }}</td>
    <td>{{ form.password }}</td>
</tr>
</table>

<input type="submit" value="login" />
<input type="hidden" name="next" value="{{ next }}" />
</form>

{# Assumes you setup the password_reset view in your URLconf #}
<p><a href="{% url 'password_reset' %}">Lost password?</a></p>

{% endblock %}
{% extends "base.html" %}

{% block content %}

<form method="post">
{% csrf_token %}
<table>
<tr>
    <td>{{ form.email.label_tag }}</td>
    <td>{{ form.email }}</td>
</tr>
</table>

<input type="submit" value="reset" />
<input type="hidden" name="next" value="{{ next }}" />
</form>

{% endblock %}
Someone asked for password reset for email {{ email }}. Follow the link below:
{{ protocol}}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %}
[Django] Password Reset
{% extends "base.html" %}

{% block content %}
<p>You should receieve an email shortly (if an account exists with the e-mail you've
entered) with instructions to reset your password. If you don't receive the email, make sure
you entered the email you registered with, and check your spam folder.</p>
{% endblock content %}
{% extends "base.html" %}

{% block content %}

<form method="post">
{% csrf_token %}
<table>
<tr>
    <td>{{ form.new_password1.label_tag }}</td>
    <td>{{ form.new_password1 }}</td>
</tr>
<tr>
    <td>{{ form.new_password2.label_tag }}</td>
    <td>{{ form.new_password2 }}</td>
</tr>
</table>

<input type="submit" value="reset" />
<input type="hidden" name="next" value="{{ next }}" />
</form>

{% endblock %}
{% extends "base.html" %}

{% block content %}

<p>Your password has been reset. <a href="{% url 'login' %}">Log in?</a></p>

{% endblock %}

Play around with the workflow regarding resetting passwords with your superuser, and make sure all these parts play together. If you want to try the same thing with a “regular” user (there won’t be a visible difference unless you make one in the templates), add one through the admin interface.

Step 3: Altering the User model

Personally, I prefer websites that ask me to log in using my email address. On the wider Internet, it’s guaranteed to be unique so I don’t have to come up with a username like james53401, and I won’t have to remember if I used james53401 on this website or james2557 instead. Yes, I have a password manager that does most of that heavy lifting, but that’s still something that annoys me.

Anyway, the first thing to do would be change the user model to accept an email address instead of a username for login. Easier said than done, though. We get to create a brand new model! You can create an app specifically for the user (./manage.py startapp testuser, which I will assume below) or add the model to an existing app, that’s up to you. Either way, the app will need to be added to the INSTALLED_APPS list in mysite.settings. Also, you’ll need to add this to mysite.settings:

AUTH_USER_MODEL='testuser.MyUser'

Then,

from django.db import models
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin
import datetime

class MyUserManager(BaseUserManager):
    def create_user(self, email, password=None):
        if not email:
            raise ValueError('Users must have an email address')
        user = self.model(
            email=self.normalize_email(email),
        )
        user.set_password(password)
        user.save(using=self._db)
        return user
    def create_superuser(self, email, password):
        user = self.create_user(
            email,
            password=password,
        )
        user.is_admin = True
        user.save(using=self._db)
        return user
class MyUser(PermissionsMixin,AbstractBaseUser):
    email=models.EmailField(unique=True)
    is_active = models.BooleanField(default=True)
    is_admin = models.BooleanField(default=False)
    USERNAME_FIELD='email'
    objects=MyUserManager()
    def __str__(self):
        return self.email
    def is_staff(self):
        return self.is_admin
    def is_superuser(self):
        return self.is_admin
from django import forms
from django.contrib import admin
from django.contrib.auth.models import Group
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.forms import ReadOnlyPasswordHashField

from testuser.models import MyUser

class UserCreationForm(forms.ModelForm):
    password1 = forms.CharField(label='Password', widget=forms.PasswordInput)
    password2 = forms.CharField(label='Password confirmation', widget=forms.PasswordInput)
    class Meta:
        model = MyUser
        fields = ('email',)
    def clean_password2(self):
        password1 = self.cleaned_data.get("password1")
        password2 = self.cleaned_data.get("password2")
        if password1 and password2 and password1 != password2:
            raise forms.ValidationError("Passwords don't match")
        return password2
    def save(self, commit=True):
        user = super().save(commit=False)
        user.set_password(self.cleaned_data["password1"])
        if commit:
            user.save()
        return user
class UserChangeForm(forms.ModelForm):
    #this prevents anyone from accidentally screwing up the password hash
    password = ReadOnlyPasswordHashField()
    class Meta:
        model = MyUser
        fields = ('email', 'password', 'is_active', 'is_admin')
    def clean_password(self):
        #this prevents anyone from accidentally screwing up the password hash
        return self.initial["password"]
class UserAdmin(BaseUserAdmin):
    form = UserChangeForm
    add_form = UserCreationForm
    list_display = ('email', 'is_admin')
    list_filter = ('is_admin',)
    fieldsets = (
        (None, {'fields': ('email', 'password')}),
        ('Permissions', {'fields': ('is_admin',)}),
    )
    add_fieldsets = (
        (None, {
            'classes': ('wide',),
            'fields': ('email', 'password1', 'password2')}
        ),
    )
    search_fields = ('email',)
    ordering = ('email',)
    filter_horizontal = ()

# Now register the new UserAdmin...
admin.site.register(MyUser, UserAdmin)

Now, we delete everything. If you’re using a MySQL, PostgreSQL, etc. type database, drop all tables. If you’re using sqlite3, delete the database file. Delete all migrations folders from the apps. We “get” to start over.

Now run the following:

./manage.py makemigrations testuser
./manage.py migrate

If you run your server now and go to the login page, there’s still no way to create a user. Let’s fix that.

Step 4: Adding Registration

Add the following to the appropriate views.py:

from django.views.generic.edit import CreateView
from django.urls import reverse_lazy
from .admin import UserCreationForm

class UserCreationView(CreateView):
    form_class=UserCreationForm
    template_name='registration/register.html'
    success_url=reverse_lazy('login')

And this to your urls.py:

urlpatterns = [
    path('accounts/register/',views.UserCreationView.as_view(),name='register'),
]

Nearly there!

Step 5: Verifying the Email Address

I’ve also found in my long years trawling the Internet that verifying email addresses is a necessity. I’ve wound up on a mailing list for a travel agency in South Africa, a Botox clinic in Chicago, a Mazda dealership in Texas, and a home remodelling firm in Denver, associated with people named Johann, Joni, Jacci, and others. Instagram was nice enough to send me an e-mail a few dozen times when a Jessica attempted to use my email address to sign up for an account (to the point where I signed up for one just to stop the emails). But to avoid being a bad apple, let’s add in a way to verify the person signed up for an account intentionally, and entered the right email address.

The way we’ll do this is to not allow the user to set a password until they have verified the email address. This sounds a lot like resetting a password, right? So we’ll take advantage of that.

First thing, change UserCreationForm to the following:

class UserCreationForm(forms.ModelForm):
    class Meta:
        model = MyUser
        fields = ('email',)
    def save(self, commit=True):
        user = super().save(commit=False)
        user.set_password(MyUser.objects.make_random_password(48))
        if commit:
            user.save()
        return user

At the moment, it’ll automatically pop the user to the login form, where they can click “Forgot password?” and get a link that way, but we’ll try to streamline that.