List, filter and play movies

fayomihorace

Horace FAYOMI

Posted on December 30, 2022

List, filter and play movies

Django and Netflix

Hello, welcome to the third 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.


In the previous part, we set up the Django project with authentication. Now, we are going to add core Netflix features. We will add the ability to list, filter and play movies.

But to achieve that goal, let's populate our database with some fixtures data.

Django fixtures

What are fixtures and what is their purpose?
Sometimes, or even most of the time when you're building software, you need test data to see how your software behaves.
For the case of a web page or blog, you need fake posts, articles, and content, to see how your page is rendered. Or you may need a fake account to be able to log in a test your website easily. Fixtures are there for that. You might also hear it is named seeder or seed.
That is what we are going to do. Currently, our index.html renders static HTML, so static movie. But we want to render movies from our database. So for each movie listed in the index.html, we will create a Movie model instance of it.
We have already learned mainly two ways to do that in Django:

  • We can open the Django shell and start creating them one by one using:
Movie.objects.create(name="Blah Blah blah")
Enter fullscreen mode Exit fullscreen mode
  • Or can log in to Django Admin using a superuser account and create them using the graphical interface.

But no worries, Django provides a more convenient way to handle that instead of creating them one by one in two steps:

1- Create a fixture file

Now let's explain the content of that file.
A fixture should contain a list of JSON objects in this format:

{
    "model": "netflix.Movie",
    "pk": 1,
    "fields": {
        "name": "The Road trick",
        "description": "The Road trick description",
        "category": 1,
        "tags": [1, 2],
        "watch_count": 10,
        "preview_image": "preview_images/p1.PNG",
        "file": "movies/blank.mp4",
        "date_created": "2022-05-05 14:08:15.221580"
    }
}
Enter fullscreen mode Exit fullscreen mode

The example above is picked from our initial.json file.
Each object of the array has mainly 3 attributes:
- model: it's the name of the Django app (netflix) followed by a dot (.) and next the name of the item's model (Category).
- pk: it's the primary key of the model
- fields: it's a nested object that contains an object representation of the instance.
So that fixture will create an instance of Movie:

  • with 1 as the primary key,
  • with The Road trick as the name,
  • with 1 as the foreign key to Category model (that means it should exist a Category with pk=1 that is the category of that movie).
  • with "The Road trick description" as the description
  • with 2 tags (the first is the Tag model instance with pk=1, and the second tag has pk=2).
  • that has been watched 5 times
  • for which the preview_image is stored in the path static/preview_images/p1.PNG
  • for which the movie file itself is stored in the path static/movies/p1.PNG (supposed to be a video but we used an image for the sake of this course)
  • that has been created March 5th 2022 at 14h 08 min 15seconds.

2- Load the fixtures data

It's simple, open a terminal from the root of the project and run:

python manage.py loaddata netflix/fixtures/initial.json
Enter fullscreen mode Exit fullscreen mode

Important Note: When you create a fixture with foreign key (fk=5 for example), make sure the foreign key you are using is either an existing primary key for the Model you're referencing or another fixture that comes before it.

Moreover, if in the database, there is already a model instance with a primary key you define in your fixture, then Django will update that model instance instead of creating a new one.

3- Add the movie assets

In our fixtures, you'll notice that we have defined some files for the preview and movie files like preview_images/movie1.png or movies/mv1.mp4. Normally those files are supposed to be uploaded from the admin interface. But we can add them directly. They are available in the final source code of the project.
You can just clone or download the repo and then copy and replace your local folder django_netflix_clone/media with the source code folder.

Now if you log in to the admin interface and, and go to movies pages, you will see that our data is well displayed:
Image description

That is. Now let's go to the core features.

List movies

Currently, our index.html display static data. Let's remove them first.
To make it easy I've created the cleaned file for you available at https://github.com/fayomihorace/django-netflix-clone/blob/main/templates/netflix/index_clean.html.
Replace your current index.html file with it.

Now, our goal is to display movies stored in database in our index.html file in a way that each category will have
it own section.
Let's take a look at the current code that renders a section and its movies:

<section class="main-container">
      <div class="location" id="home">
          <h1 id="home">Action</h1>
          <div class="box">
            <a href="">
              <img src="https://github.com/carlosavilae/Netflix-Clone/blob/master/img/p1.PNG?raw=true" alt="">
            </a>  
          </div>
      </div>
...
Enter fullscreen mode Exit fullscreen mode

Here we have the section Action with one movie and it's rendered like this:
Image description
We had already seen that with Django Templates, we can send some variables to the HTML page and displays their values as we did to display the authenticated user.
We will do the same thing. Let's go:

1- modify the index_view so we can return the data we want to display:

# django_netflix_clone/netflix/views.py
from .models import Movie  # Add this line
...

def index_view(request):
    """Home page view."""
    # We define the list of categories we want to display
    categories_to_display = ['Action', 'Adventure']

    # We create a dictionary that map each category with the it movies
    for category_name in categories_to_display:
        movies = Movie.objects.filter(category__name=category_name)
        # we limit the number of movies to PAGE_SIZE_PER_CATEGORY = 20
        data[category_name] = movies[:PAGE_SIZE_PER_CATEGORY]

    # We return the response with the data
    return render(request, 'netflix/index.html', { 'data': data.items()})
Enter fullscreen mode Exit fullscreen mode

The code is explained. We have just retrieved movies for the two categories we defined in our fixtures.json (Action and Adventure).
The dictionary data should looks like:

{
    'Action': <QuerySet [<Movie: The Road trick>, ...]>,
    'Adventure': <QuerySet [<Movie: Lost in space>, ...]>
}
Enter fullscreen mode Exit fullscreen mode

Also, we returned data.items() instead of data because we need to send an iterable to the HTML page that we will loop to render the movies. But data is a dictionary and dict.items() return the iterable version.
We could then loop the data like this:

for key, value in dict.items():
    print(key, value)
Enter fullscreen mode Exit fullscreen mode

In our case:

for category, movies in data.items():
    print(category)  # 'Action' or 'Adventure'
    print(movies)    # list of `Movie` instances that match the category  
Enter fullscreen mode Exit fullscreen mode

2- Now let's render the data. Modify the HTML file by replacing this code:

<div class="location" id="home">
          <h1 id="home">Action</h1>
          <div class="box">
            <a href="">
              <img src="https://github.com/carlosavilae/Netflix-Clone/blob/master/img/p1.PNG?raw=true" alt="">
            </a>  
          </div>
      </div>

      <h1 id="myList">Adventure</h1>
      <div class="box">
        <a href="">
          <img src="https://github.com/carlosavilae/Netflix-Clone/blob/master/img/t1.PNG?raw=true" alt="">
        </a>            
      </div>
Enter fullscreen mode Exit fullscreen mode

by this:

<div class="location" id="home">
  {% for category, movies in data %}
    <h1>{{category}}</h1>
    <div class="box">
    {% for movie in movies %}
      <a href="" style="text-decoration: none; color: white">
        <img src="/media/{{movie.preview_image}}" alt="{{movie.name}}">
        <span>{{movie.name}}</span>
      </a>
    {% endfor %}
    </div>
  {% endfor %}
</div>
Enter fullscreen mode Exit fullscreen mode

This is similar to the normal loop in the python code I showed above.

  • we make a loop through the data {% for category, movies in data %} (iterable Dict) which give access to the category and the movies variables. Remember category contains the category name and movies is the list of movies matching that category.
  • we render the category variable at the place where the section name should be. <h1>{{category}}</h1>.
  • Now inside the first loop, so for each category, we make a second loop ({% for movie in movies %}) to go through the list of movies of that category and display the image with src="/media/{{movie.preview_image}}" and also the movie name with this part <span>{{movie.name}}</span>.

Filter movies

1- Create the search form
Inside netflix/forms.py add the following:

class SearchForm(forms.Form):
    """Search form class."""
    search_text = forms.CharField(
        label="",
        widget=forms.TextInput(attrs={'placeholder': 'Search'})
    )
Enter fullscreen mode Exit fullscreen mode

Note:

  • with label="" no label will be rendered as we just need the input.
  • widget=forms.TextInput(attrs={'placeholder': 'Search'}) allows to define the text in the input placeholder. You can customize the input as you want. Don't hesitate to check the documentation to read more about that customization: https://docs.djangoproject.com/en/4.1/ref/forms/widgets/

2- Send the form to index.html
Modify the netflix/views.py

# netflix/views.py
from .forms import SearchForm
...
def index_view(request):
    ...
    data = {}
    # We create a dictionary that map each category with the it movies
    for category_name in categories_to_display:
        movies = Movie.objects.filter(category__name=category_name)
        if request.method == 'POST':
            search_text = request.POST.get('search_text')
            movies = movies.filter(name__icontains=search_text)
        # we limit the number of movies to PAGE_SIZE_PER_CATEGORY = 20
        data[category_name] = movies[:PAGE_SIZE_PER_CATEGORY]

    search_form = SearchForm()
    # We return the response with the data
    return render(request, 'netflix/index.html', {
        'data': data.items(),
        'search_form': search_form
    })
Enter fullscreen mode Exit fullscreen mode

Note:

  • We use if request.method == 'POST': to check if the request is a post, and in that case, we override the query set to filter results using the search text (movies = movies.filter(name__icontains=search_text))

3- Replace the search icon by the form in index.html
Repace this:

<a href="#"><i class="fas fa-search sub-nav-logo"></i></a>
Enter fullscreen mode Exit fullscreen mode

by this:

<form action="/" method="POST">
    {% csrf_token %}
    {{ search_form.as_p }}
    <button type="submit">
      <i class="fas fa-search sub-nav-logo"></i>
    </button>
</form>
Enter fullscreen mode Exit fullscreen mode

It should looks like this:

Image description
Nice, you can now try to play with the search fiels to see that it works well.
Now, It's up to you to update the css style to change the rendering.

Play movies

1- Create a file named watch_movie.html and copy the code from the repo. It's located at /django_netflix_clone/templates/netflix/watch_movie.html.
The most important part of the code is this:

{% if movie %}
  <h3 style="padding: 15px">{{movie.name}}</h3>
  <video width="100%" height="70%" controls>
    <source src="/media/{{movie.file}}" type="video/mp4">
  </video>
{% else %}
  <strong style="font-size: 25px">Invalid url</strong>
{% endif %}
Enter fullscreen mode Exit fullscreen mode

Here, if the id sent is invalid we display Invalid url but if the id is good, the video player will be rendered.

2- Create a view to render the file:

# django_netflix_clone/netflix/views.py

def watch_movie_view(request):
    """Watch view."""
    # The primary key of the movie the user wants to watch is sent by GET parameters.
    # We retrieve that pk.
    movie_pk = request.GET.get('movie_pk')
    # We try to get from the database, the movie with the given pk 
    try:
        movie = Movie.objects.get(pk=movie_pk)
    except Movie.DoesNotExist:
        # if that movie doesn't exist, Movie.DoesNotExist exception is raised
        # and we then catch it and set the URL to None instead
        movie = None
    return render(request, 'netflix/watch_movie.html', {'movie': movie})
Enter fullscreen mode Exit fullscreen mode

The code is explained.
But note that the use of try/catch is a good practice related to Python itself, because there is a rule of thumb in software engineering that is: Never trust client. Here any user that could use our application is a client, and we cannot trust the client to send the primary key of an existing movie. So we should always make validation of client input, and check for edge cases that could raise exceptions or lead to weird behavior.

3- Add the route watch that will use watch_movie_view:

# django_netflix_clone/django_netflix_clone/urls.py

from netflix.views import watch_movie_view # Add this line
...
urlpatterns = [
    ...
    path('watch', watch_movie_view, name='watch_movie'), # Add this line
    ...
]
Enter fullscreen mode Exit fullscreen mode

4- Finally, we will make click on a movie item on index.html to redirect to the watch route. And we pass the movie_url as GET parameter ?movie_pk={{movie.pk}}.
Modify the href of the movie from:

<a href="" style="text-decoration: none; color: white">
Enter fullscreen mode Exit fullscreen mode

to

<a href="/watch?movie_pk={{movie.pk}}" style="text-decoration: none; color: white">
Enter fullscreen mode Exit fullscreen mode

And taddahh. Refresh the main page and try to click on a movie to see:

Image description

Note: You can use Django admin to modify the movie files with another real video so you can play with it using real video.

Congratulations for coming all the way here. We are almost done with this series.
In this article you've learned:

  • What are fixtures and how to create and use them to add initial data to your Django database
  • How to use for loops in Django templates
  • The rule of thumb is that A server should never trust data sent by a client and always do the validation and verifications before.

But we just scratched the surface of all Django is capable of. What remains next is to be able to write basic unit tests for our Django project and deploy it so everyone in the world can see the incredible work you did 😎.

💖 💪 🙅 🚩
fayomihorace
Horace FAYOMI

Posted on December 30, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

What was your win this week?
weeklyretro What was your win this week?

November 29, 2024

Where GitOps Meets ClickOps
devops Where GitOps Meets ClickOps

November 29, 2024

How to Use KitOps with MLflow
beginners How to Use KitOps with MLflow

November 29, 2024

Modern C++ for LeetCode 🧑‍💻🚀
leetcode Modern C++ for LeetCode 🧑‍💻🚀

November 29, 2024