Hello, welcome to the second part of this tutorial-course in which you'll learn and understand the basics of Django by building a clone of the popular website Netflix.
We are going to add authentication features. Basically, we will allow users to login and register.
Let's get started.
Add website templates
For this project, we won't write our own Html or CSS files. Instead, we will use the work done by Carlos Avila available here: Netflix Clone. Thank to him.
If you open that codepen you'll see an HTML code and some CSS.
Create templates folder
Django recommends putting our Html files or templates inside a directory called templates
at the root of the project. Then inside that directory, we can create a folder for each app of the project to separate each app template. This is possible because of the line: 'BACKEND': 'django.template.backends.django.DjangoTemplates',
in the setting file.
In addition, modify line 58 of the settings file like this:
'DIRS': [os.path.join(BASE_DIR, 'templates')],
, to tell Django that he should look for templates inside a folder named templates
.
- create the
templates
folder at the root of the project:mkir templates
. - create a folder named
netflix
for our netflix app inside it:cd templates
mkir netflix
- create an index.html file inside it:
cd netflix
-
touch index.html
. I made a lighter version of the codepen html available here https://github.com/fayomihorace/django-netflix-clone/blob/main/templates/netflix/full_index_light.html. Click to open the link on a browser, copy the html and paste it into yourindex.html
file.
Good. Our homepage is created. Now we need to access it from the browser. And for that we need a route
.
Django URLs
Remember in part1 we saw that the urls.py
file contains all the routes of our project.
We only have one route to access the admin page.
Now we will add a route for the home page with this line:
Modify it like this:
from django.contrib import admin
from django.urls import path
from django.conf import settings
from django.conf.urls.static import static
from netflix.views import index_view # Add this line
urlpatterns = [
path('admin/', admin.site.urls),
path('', index_view, name='home'), # Add this line
]
We should have an error because the module index_view
imported by the line from netflix.views import index_view
doesn't yet exist.
It's the view that will be executed when someone will call the path named home
we have just added at line
path('', index_view, name='home')
. That means we should be able to access the homepage with our website host URL. Locally it's just http://localhost:8000
.
So we need to create index_view
that will render the homepage to requesters.
- open the
netflix/views.py
file and add this code:
def index_view(request, path):
"""Home page view."""
return render(request, 'index.html')
There we go.
- serve the project: 'python manage.py runserver'
- Open your browser, and go to
http://localhost:8000
You should see something like this:
That means we can access our homepage. But the render is not the right because we didn't yet add the styles.
That is what we will do now.
Static files
In Django, CSS, js, and images needed to render the website are called statics
that's why Django recommends placing them in a folder called statics
.
Let's go:
- First, create a directory called
static
at the root of the project. Django will look for static files there similar to how Django finds templates insidetemplates
:mkdir static
. - create a folder for
netflix
app similarly to templates:cd static
mkdir netflix
- create a file called
style.css
inside it:touch style.css
and copy the CSS part of the codepen into it.
We had already specified the statics files URL with line:
STATIC_URL = 'static/'
.
That means we should access them like this:
localhost:8000/static/style.css
- Now adds this
{% load static %}
at the beginning of theindex.html
file. That adds thestatic
tag that could be used to embed links from static files. And then use thatstatic
tag to import our style.css. - Adds this line
<link rel="stylesheet" href="{% static 'netflix/style.css' %}">
into the<head>
tag of the index.html file. - Refresh the home page.
There we go.
Registration and login templates
First, we will add a registration
and login
buttons to the homepage.
In the index.html file replace
<a href="#">Account</a>
by<a href="/register">Register</a><a href="/login">Login</a>
The clicks onregister
button should redirect the user to the routeregister
which will render the registration page and in the same way clicking on thelogin
button will redirect the user to routelogin
that should render thelogin
page.Add the
register_view
andlogin_view
inside thenetflix/views.py
file:
def register_view(request):
"""Registration view."""
return render(request, 'netflix/register.html')
def login_view(request):
"""Login view."""
return render(request, 'netflix/login.html')
- Modify
urls.py
file to add the registration and login routes:
...
from netflix.views import register_view # Add this line
from netflix.views import login_view # Add this line
urlpatterns = [
...
path('register', register_view, name='register'), # Add this line
path('login', login_view, name='login'), # Add this line
]
- create
register.html
andlogin.html
files insidetemplates/netlfix/
. We'll created a registration HTML inspired by the index file.- For
registration.html
file, You just have to copy and past the following html code:
- For
{% load static %}
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Netflix</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script defer src="https://use.fontawesome.com/releases/v5.1.0/js/all.js" crossorigin="anonymous"></script>
<script src="main.js"></script>
<link rel="stylesheet" href="{% static 'netflix/style.css' %}">
</head>
<body>
<div class="wrapper">
<!-- HEADER -->
<header>
<div class="netflixLogo">
<a id="logo" href="/"><img src="https://github.com/carlosavilae/Netflix-Clone/blob/master/img/logo.PNG?raw=true" alt="Logo Image"></a>
</div>
<nav class="main-nav">
<a href="/">Home</a>
</nav>
<nav class="sub-nav">
<a href="/login">Login</a>
</nav>
</header>
<section class="main-container" >
<div class="location" id="home">
<div class="box">
</div>
</div>
</section>
</div>
</body>
</html>
- For
login.html
file, You just have to copy and past the following html code:
{% load static %}
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Netflix</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script defer src="https://use.fontawesome.com/releases/v5.1.0/js/all.js" crossorigin="anonymous"></script>
<script src="main.js"></script>
<link rel="stylesheet" href="{% static 'netflix/style.css' %}">
</head>
<body>
<div class="wrapper">
<!-- HEADER -->
<header>
<div class="netflixLogo">
<a id="logo" href="/"><img src="https://github.com/carlosavilae/Netflix-Clone/blob/master/img/logo.PNG?raw=true" alt="Logo Image"></a>
</div>
<nav class="main-nav">
<a href="/">Home</a>
</nav>
<nav class="sub-nav">
<a href="/register">Register</a>
</nav>
</header>
<section class="main-container" >
<div class="location" id="home">
<div class="box">
</div>
</div>
</section>
</div>
</body>
</html>
Django Forms: Add registration and login forms
Django allows us to create and manage forms easily.
Let's create a file netlfix/forms.py
to put forms related to Netflix app:
- inside
netlfix
folder createforms.py
file:touch forms.py
- Add this code to it:
from django import forms
from django.contrib.auth.models import User
class RegisterForm(forms.Form):
"""Registration form class."""
firstname = forms.CharField(label="First name")
lastname = forms.CharField(label="Last name")
email = forms.EmailField(label="Email Address")
password = forms.CharField(label="Password", widget=forms.PasswordInput)
password_conf = forms.CharField(label="Password confirmation", widget=forms.PasswordInput)
class LoginForm(forms.Form):
"""Login form class."""
email = forms.EmailField(label="Email Address")
password = forms.CharField(label="Password", widget=forms.PasswordInput)
- Modify the
register_view
like this:
from .forms import RegisterForm
...
...
def register_view(request):
"""Registration view."""
register_form = RegisterForm()
return render(request, 'netflix/register.html', locals())
Note that the argument locals()
passed to render()
is to send all local variables of the view (register_view) to the HTML template (here register.html
). That means we can access our form in register.html
as a variable named register_form
. That feature is customizable.
- In
register.html
, modify the<div class="box">
tag like this:
<div class="box">
<form id="registerForm" action="/register" method="POST">
{% csrf_token %}
{{ register_form.as_p }}
<button type="submit">Register</button>
</form>
</div>
Important note: You will notice the syntax {{ register_form.as_p }}
and {% csrf_token %}
. Remember I said, the locals()
arguments send all the registration view local variables to the template the variable register_form
is sent to the HTML template (register.html
). Then to access those locals in register.html
we just need to wrap the variable name with {{ }}
as it's done for {{ register_form.as_p }}
. Now for {% csrf_token %}
it's a little bit different as it's not rendered as a variable but as a Tag
. If you want to know more, read this part of the documentation. And {% csrf_token %}
allows us to set the csrf_token
value of user session to prevent CSRF attacks.
Refresh the register page.
Django will automatically generate the HTML code of the form. Isn't it magic ^_^ ? And we can customize it, but not in this course.
It should look like this now:
- Modify the
login_view
like this:
...
from .forms import LoginForm # add this line
...
...
def login_view(request):
"""Login view."""
login_form = LoginForm()
return render(request, 'netflix/login.html', locals())
- In
login.html
, modify the<div class="box">
tag like this:
<div class="box">
<form id="loginForm" action="/login" method="POST">
{% csrf_token %}
{{ login_form.as_p }}
<button type="submit">Login</button>
</form>
</div>
Refresh the login page.
It should look like this now:
Handle the forms submission.
Registration form
Now, if a user fills in the form and submits it, he should be able to register.
If you look at the registration form HTML code, you'll notice that the form action
HTML property value is the /register
(the registration route in our url.py
).
We will modify the register_view
to handle the form submission. (But it's up to you to move the post inside another view).
- Modify the
register_view
like this:
from django.http import HttpResponseRedirect
from django.contrib.auth.models import User
from django.contrib.auth.hashers import make_password
...
...
def register_view(request):
"""Registration view."""
if request.method == 'GET':
# executed to render the registration page
register_form = RegisterForm()
return render(request, 'netflix/register.html', locals())
else:
# executed on registration form submission
register_form = RegisterForm(request.POST)
if register_form.is_valid():
User.objects.create(
first_name=request.POST.get('firstname'),
last_name=request.POST.get('lastname'),
email=request.POST.get('email'),
username=request.POST.get('email'),
password=make_password(request.POST.get('password'))
)
return HttpResponseRedirect('/login')
return render(request, 'netflix/register.html', locals())
Now our register_view
handle both the rendering of the registration page under condition if request.method == 'GET':
and also the submission of the registration form in the else
condition. And as I said it's up to you to handle them with two different views.
Let explains what we are doing in the else
:
-
form = RegisterForm(request.POST)
is the submission of the POST request -
if form.is_valid():
allows us to handle errors. We will come on it at the end of this part when we do the form validations. Basically, that line checks if our form is filled with good values. - If it's the case,
User.objects.create(
is executed to create the User model instance. - And
return HttpResponseRedirect('/login')
redirect the user tologin.page
- If the form is not valid, then the
else:
condition is executed instead - We then redirect the user to the register page and Django will automatically display validation errors to the user, so he could fix them and submit the form again. Again isn't that magic ^_^.
Registration Form validation
We might have some constraints for our data.
password mininum length is 6 and maxinum length is 20: inside
forms.py
, modify linepassword = forms.CharField(label="Password", widget=forms.PasswordInput)
intopassword = forms.CharField(label="Password", widget=forms.PasswordInput(render_value=True), min_length=6, max_length=20)
. (You'll notice we have addedrender_value=True
, it's to tells django to keep the value of the password when the form is returned back with errors).the password confirmation should match the password and the email shouldn't exist in the database. To handle those two validation constraints, we will override the default behavior of a Django Form class method
clean
. So modify theRegistrationForm
like this:
...
class RegisterForm(forms.Form):
"""Registration form class."""
....
# Add this methods
def clean(self):
"""Check if the form is validated."""
# call the default `clean` method to perform
# default validation of the form (max_lenght and min_length defined for password for instance)
super(RegisterForm, self).clean()
# extract user input data
email = self.cleaned_data.get('email')
password = self.cleaned_data.get('password')
password_conf = self.cleaned_data.get('password_conf')
# Check if the password match the password confirmation
if password != password_conf:
self._errors['password_conf'] = self.error_class([
"wrong confirmation"
])
# Check if the email used doen't already exist
if User.objects.filter(username=email).exists():
self._errors['email'] = self.error_class(['Email already exist'])
# return any errors if found
return self.cleaned_data
The method is documented enough to explain what it does.
And there we go.
Note: For password validators, you can use the settings AUTH_PASSWORD_VALIDATORS
, use default Django validators or create your own ones. But in this tutorial, we will use instead Django form. Let me know in the comments if you want a tutorial on how to build custom password validators.
Try to register a user with wrong input and then with good input.
After you're done. Login to django admin
with the superuser, and go to http://localhost:8000/admin/auth/user/
. You'll see the new user added. You can also test in django shell:
from django.contrib.auth.models import User
User.objects.all()
. You should see the new user.
Login form
Now, a registered user should be able to login.
If you look at the login form HTML code, you'll notice that the form action
HTML property value is the /login
(the login route in our url.py
).
We will modify the login_view
to handle the form submission.
- Modify the
login_view
like this:
...
from django.contrib.auth import authenticate, login # Add this line
from .forms import LoginForm # Add this line
...
...
def login_view(request):
"""Login view."""
if request.method == 'GET':
# executed to render the login page
login_form = LoginForm()
return render(request, 'netflix/login.html', locals())
else:
# get user credentials input
username = request.POST['email']
password = request.POST['password']
# If the email provided by user exists and match the
# password he provided, then we authenticate him.
user = authenticate(username=username, password=password)
if user is not None:
# if the credentials are good, we login the user
login(request, user)
# then we redirect him to home page
return HttpResponseRedirect('/')
# if the credentials are wrong, we redirect him to login and let him know
return render(
request,
'netflix/login.html',
{
'wrong_credentials': True,
'login_form': LoginForm(request.POST)
}
)
The method is documented enough.
- Modify the
login.html
file from this:
<form id="loginForm" action="/login" method="POST">
{% csrf_token %}
{{ login_form.as_p }}
<button type="submit">Login</button>
</form>
into this:
<form id="loginForm" action="/login" method="POST">
{% csrf_token %}
{% if wrong_credentials %}
<p style="margin-top: 60px">Invalid credentials.</p>
{% endif %}
{{ login_form.as_p }}
<button type="submit">Login</button>
</form>
Note that we have just added:
{% if wrong_credentials %}
<p style="margin-top: 60px">Invalid credentials.</p>
{% endif %}
Important note: That is called template conditional rendering
. Django allows us to render some HTML according to some conditions. So here we display the message Invalid credentials
if the view returns the variable wrong_credentials
with the value True
.
- Now when the user is authenticated and redirected to home page, we will render his username and a logout button instead of
login
andregister
buttons. Modify the line
<a href="/register">Register</a><a href="/login">Login</a>
of index.html
file into theses ones:
{% if request.user.is_authenticated %}
{{ request.user }}
<a href="/logout"><i class="fas fa-power-off sub-nav-logo"></i>Logout</a>
{% else %}
<a href="/register">Register</a><a href="/login">Login</a>
{% endif %}
Note: We used again conditional rendering here to achieve our goal with {% if request.user.is_authenticated %}
. But you might be asking that we didn't send that variable (request) to the HTML template
. And you're right. We are able to access the variable request
because it's a global (context) variable automatically sent by Django to allow us to get useful information about the current request. So if {{ request.user.is_authenticated }}
is True
, user is authenticated, and request.user
is the User model instance of the authenticated user. And we can then display the user email using request.user.username
or directly by request.user
. But if instead {{ request.user.is_authenticated }}
is False
, we display the registration and login buttons.
If you login with the right credentials, you should now be able to see the authenticated user email and a log out button like this:
Handle Logout
An authenticated user should be able to logout.
We already have a logout button on index.html:
<a href="/logout"><i class="fas fa-power-off sub-nav-logo"></i>Logout</a>
Note that the link point to logout
route that doesn't yet exist.
Let's add it.
- At the bottom of the
views.py
file, add the logout_view:
...
...
def logout_view(request):
"""Logout view."""
# logout the request
logout(request)
# redirect user to home page
return HttpResponseRedirect('/')
- Add the logout view into
urls.py
file:
...
...
from netflix.views import logout_view # Add this line
urlpatterns = [
...
...
path('logout', logout_view, name='logout'), # Add this line
]
Congratulations.
We are at the end of this second part.
Summary
In this tutorial you've learned
- how to manage Django template
- how to render variables inside the template
- how to conditionally render HTML inside the template
- how to manage Django static files
- how to create Django forms and handle validation
- how to access and use
request
variable inside Django template - how to handle basic authentication in Django (registration, login, and logout).
NB: The full source code is available here: https://github.com/fayomihorace/django-netflix-clone
If you face any blocker or error while following this tutorial, please drop a comment, I'll reply to help you as soon as possible. Excited to have you in part 3 of this tutorial-course.
Top comments (1)
Right at the beginning, when setting up the index_view, the code is:
def index_view (request,path):
""" Home page view """
return render(request, 'index.html')
But when I run it, it returns an error:
Exception Value:
index_view() missing 1 required positional argument: 'path'
Is there anything else to look for? I'm getting started in django.