Using the Todo app from the midterm exam, add the ability for each user to have his own todo list.
The steps are:
- Remove the upstream URL from the Git repository.
- Add “users” to the Todo application.
- 2.1 Add Django’s built-in authentication. Create a login template.
- 2.2 Modify todo index view to show the authenticated user. Add links to login and logout.
- 2.3 For other views, require login to access
- Modify the domain model so a user owns a collection of “todo”.
- 3.1 Modify models and run migrations.
- 3.2 Review the “todo” form. Modify it needed.
- Modify the code to use the domain model.
1. Remove the git upstream URL
So you don’t accidentally “push” to the exam repository (penalty if you do).
git remote remove origin
2.1 Add Django’s built-in authentication
This part is described step-by-step in either of these docs:
-
MDN’s Django Tutorial: User authentication part 8 of their excellent Django tutorial!
Add one or two local users so you can test login and logout.
Create one or two users. Two users will be helpful so you can verify that each user has his own set of “todo” items.
Create a user using the Django shell (manage.py shell
).
Note that the method to create a user is create_user
not the
usual create
method.
from django.contrib.auth.models import User
# choose your own username, email, and password
user = User.objects.create_user(
'username',
email='email@some.domain',
password='password')
# set other User attributes (at least first name)
user.first_name = "Harry"
user.last_name = "Hacker"
user.save()
Test: when you finish this you should be able to
- login at http://localhost:8000/accounts/login/ and be redirected to Todo index
- logout at http://localhost:8000/accounts/logout/ and be redirected to a logout page.
2.2 Update Views and Templates to Use Authentication
In the todo index page,
- if user is authenticated then show his name in the heading line, and show list of todo
- also add a “Logout” link so user can logout
- if not authenticated, don’t show todo.
Template variables you can use are:
{{user.first_name}}
{{user.last_name}}
{% if user.is_authenticated %}
show his todo list with his name in heading line
{% else %}
ask him to login
{% endif %}
In a view, to get the URL for the login (or logout) page use
<a href="{% url 'login' %}">Login</a>
To perform an authentication test inside a view:
def someview(request, ...):
user = request.user
if user.is_authenticated:
do something
else:
do something_else
2.3 Require login to access the ‘add’ and ‘done’ views
A user must be authenticated to add a todo or change status of a todo.
Use decorators on the view methods to require login.
(Wasn’t that easy?)
Evaluation: As an unauthenticated user
- If you navigate to /todo/ it shows a message that you need to login (with hyperlink)
- If you navigate to /todo/add/ it redirects to the login page
- If you navigate to /todo/id/done/ it redirects to the login page
3. Revise Domain Model & Views
We want each user to have his own list of Todo. Draw a UML diagram for this.
We need to
- update the Todo model and run a migration
- (maybe) update the TodoForm in
forms.py
- modify the add view to set the user after the form data is submitted
- modify the index view so it only shows the todos for the logged in user
- modify the ‘done’ view so user can only mark his own Todo as ‘done’
3.1 Update the Todo model.
This is just like Question - Choice in KU Polls.
The User class is in django.contrib.auth.models
.
from django.contrib.auth.models import User
class Todo(models.Model):
description = models.CharField(...)
done = models.BooleanField(...)
user = models.ForeignKey(User, null=True, blank=True,
on_delete=models.CASCADE)
The options null=True
and blank=True
allow Todo items in the database
even if the user or empty or null. This is in case there are some old
todo in the database (before you added users), so those old todos won’t
violate the constraints on the database table.
Create a migration and perform the migration.
3.2 Update TodoForm
in forms.py
.
You should check the form after modifying the related model class.
In this form, the user can set only the todo description
. The Todo app automatically
sets done=False
and sets the user.
In forms.py
:
class TodoForm(forms.ModelForm):
class Meta:
model = Todo
fields = ['description'] # the fields to show in form
exclude = ['done', 'user']
# custom labels for input fields
labels = {
'description': "Description of todo",
}
3.3 Modify add_todo
so the current user owns the Todo
This method is invoked when a user submits the TodoForm. The method sets the user attribute of the todo and the done flag.
We need some code to insert the current user into the todo object’s user attribute before saving it to the database.
@login_required
def add_todo(request):
"""Add a new todo. Should be invoked via POST method."""
if request.method == 'POST':
form = TodoForm(request.POST)
if form.is_valid():
# create a todo from the form data so we can set
# the user who owns this todo and then save the todo
todo = form.save(commit=False)
todo.user = request.user
todo.done = False
todo.save()
messages.success(request,
f"Added \"{request.POST['description']}\"" )
return redirect('todo:index')
else:
... # same as original code
3.4 In the todo_index
view, display only todo owned by the current user
Filter the todos to that the todo_list
only contains todo owned by the current user, using request.user
.
You only need a small change to the code. Try to do it yourself.
In the index
view in views.py
you have:
todo_list = Todo.objects.filter(done=False)
you should also filter for todo.user is the current user.
The current user is request.user
so you can write:
todo_list = Todo.objects.filter(done=False).filter(user=request.user)
or add an explanatory variable if its not clear:
this_user = request.user
todo_list = Todo.objects.filter(done=False).filter(user=this_user)
There is another way to get the current user’s Todo that may be more efficient.
The relationship User-Todo is 1-to-many, just like Question-Choice
in the polls application. In the polls application we saw that
Django adds a choice_set
attribute to Question. So if you want
all the choices for one question you can write:
question.choice_set.all()
Can you apply that to User-Todo? Instead of all()
use a filter
to select done=False
.
3.5 Modify the “done” view tp verify user owns the todo
In the done_todo
view, the user must be authenticated and he must be the owner of the todo he is trying to modify.
#todo: require login to access this view
def done_todo(request, todo_id: int):
"""Mark a todo as done, then redirect back to the index page."""
try:
todo = Todo.objects.get(id=todo_id)
except ...
# same as original code
# the user must own this todo to modify it
if todo.user == request.user:
todo.done = True
todo.save()
messages.success(request, f"Todo {todo.id} marked as done")
else:
messages.error(request, f"Todo {todo.id} doesn't belong to you")
...
This code uses the Django Messages framework to pass a message to a page template. Messages is much easier than adding a message to the context.
User - Todo Model and ER Diagram
Forms and Form Processing
The Todo app uses a Form to handle HTML form input for a todo.
The MDN Django Tutorial Part 9: Working with Forms explains how to use forms. They have a diagram of the flow in processing a form: