(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:
- Inheriting from
django.contrib.auth.models.AbstractUser
; - 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.
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.
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?
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:
- Use a function
def posts(request, **extra_params)
; - Use a
class Posts(...)
and register it in theurlpatterns
viaPosts.as_view()
- 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 Form
s 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
Comments
By email, at mathieu.bivert chez: