List, filter and play movies
Horace FAYOMI
Posted on December 30, 2022
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")
- 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
- Inside
Netflix
folder, create a folder namedfixtures
. - then inside that folder create a fixture file. Let's name it
initial.json
. Note that it's a.json
file. I have already done the work for you. Just click on this link https://github.com/fayomihorace/django-netflix-clone/blob/main/netflix/fixtures/initial.json and copy-paste the content into yourinitial.json
.
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"
}
}
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 toCategory
model (that means it should exist aCategory
withpk=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 pathstatic/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
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:
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>
...
Here we have the section Action
with one movie and it's rendered like this:
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()})
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>, ...]>
}
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)
In our case:
for category, movies in data.items():
print(category) # 'Action' or 'Adventure'
print(movies) # list of `Movie` instances that match the category
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>
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>
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 thecategory
and themovies
variables. Remembercategory
contains the category name andmovies
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 withsrc="/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'})
)
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
})
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>
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>
It should looks like this:
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 %}
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})
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
...
]
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">
to
<a href="/watch?movie_pk={{movie.pk}}" style="text-decoration: none; color: white">
And taddahh. Refresh the main page and try to click on a movie to see:
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 😎.
Posted on December 30, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.