DEV Community

Horace FAYOMI
Horace FAYOMI

Posted on • Edited on

Add authentication features.

Django and Netflix

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.

Netflix Clone by Carlos Avila

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.

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
]
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode

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:

Image description

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 inside templates: 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 the index.html file. That adds the static tag that could be used to embed links from static files. And then use that static 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 on register button should redirect the user to the route register which will render the registration page and in the same way clicking on the login button will redirect the user to route login that should render the login page.

  • Add the register_view and login_view inside the netflix/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')
Enter fullscreen mode Exit fullscreen mode
  • 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
]
Enter fullscreen mode Exit fullscreen mode
  • create register.html and login.html files inside templates/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:
{% 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>
Enter fullscreen mode Exit fullscreen mode
  • 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>
Enter fullscreen mode Exit fullscreen mode

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 create forms.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)
Enter fullscreen mode Exit fullscreen mode
  • 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())
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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:
Image description

  • 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())
Enter fullscreen mode Exit fullscreen mode
  • 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>
Enter fullscreen mode Exit fullscreen mode

Refresh the login page.
It should look like this now:

Image description

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())
Enter fullscreen mode Exit fullscreen mode

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 to login.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 line password = forms.CharField(label="Password", widget=forms.PasswordInput) into password = forms.CharField(label="Password", widget=forms.PasswordInput(render_value=True), min_length=6, max_length=20). (You'll notice we have added render_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 the RegistrationForm 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
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode


. 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)
            }
        )
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Note that we have just added:

{% if wrong_credentials %}
    <p style="margin-top: 60px">Invalid credentials.</p>
{% endif %}
Enter fullscreen mode Exit fullscreen mode

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 and register buttons. Modify the line
<a href="/register">Register</a><a href="/login">Login</a> 
Enter fullscreen mode Exit fullscreen mode


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 %}
Enter fullscreen mode Exit fullscreen mode

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:

Image description

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>
Enter fullscreen mode Exit fullscreen mode

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('/')
Enter fullscreen mode Exit fullscreen mode
  • 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
]
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
paulomelgaco profile image
Paulo Melgaço

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.