You may want to inform yourself about human rights in China.

On a ๐•-Like Service Part. 1: User Handling (Django Tutorial)

date: 2024-08-22
update: 2024-08-23

(WIP)

We propose a step-by-step implementation of a ๐•-like service, with a specification essentially based on CS50’s Project 4’s. We’ll focus in this article on custom user handling.

As for the previous project, the code is available on github.

Setting up

# Create a directory and get in
% mkdir django-network
% cd django-network

# I'd recommend to version things with git(1). You can version
# things after every exercices for example.
#
# If you're not familiar with git(1), it's definitely the occasion
# to learn how to use it.
% git init

[...]

# Remove some noise
% echo '*__pycache__*' > .gitignore

# Create our project
% django-admin startproject networksite

# Move in and create our app (remember, a project is
# a collection of apps).
% cd networksite/
% python manage.py startapp network

# Move back to our original directory and version things.
% cd -
% git add .gitignore networksite/
% git commit -m 'creating initial project & app'

[...]

Introduction to user management

The previous service was purposefully essentially anonymous; let’s now see how to deal properly with users.

The good news is that Django comes with a fairly complete and sophisticated way to handle users by default: this is the same machinery that is used “internally” for its admin interface.

Note: The bad news is, you’re basically delegating user-handling to someone else, and that user-handling isn’t trivial. There may be bugs, and because of Django’s popularity, there’s a strong incentive to find and exploit flaws. There’s a middle-ground that we’re trying to adopt below. Not necessarily out of security concerns (this is a toy project) but because it’ll provide us with an opportunity to dig deeper in Django’s code.

The User model

If we think about it for a minute, users will probably need to be registered in the database (eh). This means that we must have a model for them. And Django does indeed provides us a model: django.contrib.auth.models.User. As you can see from the surrounding documentation, the django.contrib.auth module is well-furbished.

The typical recommendation is to always create a new User model extending what is provided by django.contrib.auth: the cost is negligible, and will provide valuable flexibility: from the outset, it’s not always clear how we’ll want to equip our Users. In our case, we will want add it an extra field later on.

Then, there are two ways to extend the default model:

  1. Inheriting from django.contrib.auth.models.AbstractUser;
  2. Inheriting from django.contrib.auth.models.AbstractBaseUser.

The former essentially is the battery-included thing: you grab everything provided by django. The latter is more frugal, and requires some extra-wiring so as to be able to still leverage django’s added value. It’s also more powerful, and more interesting to tinker with, as it’ll require to get a bit deeper.

Note: The django documentation is very clear: changing the User model mid-project is costly and error-prone. So make sure you’re set correctly as you start.

For curiosity – we won’t be using this – setting up the former is as simple as:

# network/models.py
from django.contrib.auth.models import AbstractUser

class User(AbstractUser):
	pass

Exercise: Look for the code of the django.contrib.auth.models.User class.

Solution:[+]

Note: As-is this is still not enough, as we’ll need to tell django about our new User model; more on that later.

The second option – that’s the one we’ll use – is a bit more interesting.

Again, we need to inherit from a class, and we’ll furthermore wire a few things to leverage some of django’s default behaviors. The extra CAPITALIZED_FIELDS below are a first step in that wiring: they tell django which field corresponds to the username, and which one to the email.

The USERNAME_FIELD is used by django for login, so for example, were we to set USERNAME_FIELD = email, the login would require us to input the email and not the username. We could also inherit from an existing database that we’d want to plug into Django, but which would relies on different conventions than Django’s default.

# network/models.py
from django.contrib.auth.models import AbstractBaseUser
from django.db.models           import EmailField

class User(AbstractBaseUser):
	# password is the only field provided by AbstractBaseUser; we don't
	# need to specify it.

	username        = ...
	email           = ...

	USERNAME_FIELD  = "username"
	EMAIL_FIELD     = "email"
	REQUIRED_FIELDS = ...

Exercise: Look for the code of the class AbstractBaseUser. Copy/paste the definition of its email, username and REQUIRED_FIELDS fields to our local User model.

Solution:[+]

You may now enable this new auth model in the networksite/settings.py:

AUTH_USER_MODEL    = "network.User"

And then create your migrations and run then:

% python manage.py makemigrations && python manage.py migrate

Exercise: Try to create an user from manage.py shell; why aren’t we using the admin/ API? What is happening? Eventually, try to create a user via manage.py createsuperuser. Have a look at the code of the UserManager; do you understand what it does? Can you fix the issue?

Tip:[+]
Solution:[+]

Generic views

Introduction

The existence of some default “generic” views has been demonstrated in Django tutorial:

Those views can handle some of the work, and can be parametrized by settings some class fields. Generally speaking, when implementing views, we have at least the following options:

  1. Use a function def posts(request, **extra_params);
  2. Use a class Posts(...) and register it in the urlpatterns via Posts.as_view()
  3. Use a class inheriting from a Django’s default class, e.g. class Posts(generic.ListView).

It’s sometimes unclear which one we may prefer. The former is perhaps the less magical / most explicit, but the second/third provides (so they say) better re-usability, and the third one can provide a fair amount of work.

Login/Logout

In the case of the user handling, the django.contrib.auth.views provides us with a bunch of essentially ready-made views such as those two:

When I say that those views are essentially ready-made, I mean that while the views can be registered as in our urlpatterns:

from django.contrib.auth.views import LoginView, LogoutView

app_name = 'network'

urlpatterns = [
	[...]

	path('login/', LoginView.as_view(...), name = 'login'),
	path('logout/', LogoutView.as_view(), name='logout'),
]

Django still doesn’t provides us with ready-made templates, and some view parameters and other settings basically must be set to get something usable.

Let’s see how to set those up for a login/logout.

Logout

Starting with logout, as it’s the simplest: we just need to register the view as done earlier:

from django.contrib.auth.views import LogoutView

app_name = 'network'

urlpatterns = [
	[...]

	path('logout/', LogoutView.as_view(), name='logout'),
]

By default, upon logout, a systematic redirection is performed. The destination of this redirection can be either configured by a view parameter

	path('logout/', LogoutView.as_view(next_page = 'network:home'), name='logout'),

Which defaults to the LOGOUT_REDIRECT_URL setting. Meaning, we can alternatively – and this seems to be the preferred way – configure this redirection by setting in networksite/settings.py e.g.

LOGOUT_REDIRECT_URL = 'network:home'

Login

The login is a bit more sophisticated. We now need to specify some additional parameters. In particular, we’ll want to use a template. The default is registration/login.html; we can set it to network/login.html. We will also want to systematically redirect to the home page on login, or to the page described by the next field of the login’s form.

All of this but the default redirection can be performed as such when registering the view in urlpatterns:

from django.contrib.auth.views import LoginView

app_name = 'network'

urlpatterns = [
	[...]

	path('login/', LoginView.as_view(
			template_name='network/login.html',
			redirect_authenticated_user=True,
			# This is the default; made explicit
			redirect_field_name='next',
		), name = 'login'),

	[...]
]

The redirection can be performed via next_page, as we did for the logout, or via the LOGIN_REDIRECT_URL setting, again as we did for logout:

LOGIN_REDIRECT_URL = 'network:home'

It only remains to to fill our template. I’ll assume you can cook a base template network/main.html on your own.

{% extends "network/main.html" %}

{% block content %}

<form method="post" action="{% url 'network:login' %}">
	{% csrf_token %}
	{{ form.as_p }}
	<input type="hidden" name="next" value="{{ next }}">
	<input type="submit" value="login">
</form>
{% endblock %}

Let’s spend a few lines discussing what’s happening above. The LoginView provides the template with a ready-made Form. We’ll expand on Forms later, but for now, you can think of it as a convenient blackbox: Django is able to compute the HTML corrresponding to the fields of your User model needed for authentication, and to deal with authenticating them.

The form when used as such even provides a default error handling. Errors – if any – are added in a list with a class="errorlist".

And so there we go: we have implemented a login/logout, mostly be relying on Django’s features, despite our frugal User model.

Signin, Signout

Things are a little bit more difficult for the signin, as Django doesn’t provide a default view for user registration. But it provides a Form tailored for user creation, django.contrib.auth.forms.UserCreationForm. TODO

Password change/reset

TODO


In the series:


Comments

By email, at mathieu.bivert chez:

email