├── netflix
├── __init__.py
├── migrations
│ ├── __init__.py
│ ├── 0002_alter_movie_date_created.py
│ ├── 0003_alter_movie_file_alter_movie_preview_image.py
│ └── 0001_initial.py
├── apps.py
├── admin.py
├── models.py
├── forms.py
├── views.py
├── fixtures
│ └── initial.json
└── tests.py
├── django_netflix_clone
├── __init__.py
├── asgi.py
├── wsgi.py
├── urls.py
└── settings.py
├── requirements.txt
├── db.sqlite3
├── .gitignore
├── media
├── movies
│ └── blank.mp4
└── preview_images
│ ├── p1.PNG
│ ├── p2.PNG
│ ├── p3.PNG
│ ├── p4.PNG
│ ├── p5.PNG
│ ├── p6.PNG
│ ├── r1.PNG
│ ├── r2.PNG
│ ├── r3.PNG
│ ├── r4.PNG
│ ├── r5.PNG
│ └── r6.PNG
├── static
└── netflix
│ ├── preview_images
│ └── movie1.png
│ └── style.css
├── manage.py
├── README.md
└── templates
└── netflix
├── register.html
├── login.html
├── watch_movie.html
├── index_clean.html
├── index.html
└── full_index_light.html
/netflix/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/django_netflix_clone/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/netflix/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | django==4.0.2
2 | Pillow==9.0.1
3 |
--------------------------------------------------------------------------------
/db.sqlite3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fayomihorace/django-netflix-clone/HEAD/db.sqlite3
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__/
2 | *__pycache__/
3 | *.py[cod]
4 | venv/
5 | build/
6 | develop-eggs/
7 | dist/
8 |
--------------------------------------------------------------------------------
/media/movies/blank.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fayomihorace/django-netflix-clone/HEAD/media/movies/blank.mp4
--------------------------------------------------------------------------------
/media/preview_images/p1.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fayomihorace/django-netflix-clone/HEAD/media/preview_images/p1.PNG
--------------------------------------------------------------------------------
/media/preview_images/p2.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fayomihorace/django-netflix-clone/HEAD/media/preview_images/p2.PNG
--------------------------------------------------------------------------------
/media/preview_images/p3.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fayomihorace/django-netflix-clone/HEAD/media/preview_images/p3.PNG
--------------------------------------------------------------------------------
/media/preview_images/p4.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fayomihorace/django-netflix-clone/HEAD/media/preview_images/p4.PNG
--------------------------------------------------------------------------------
/media/preview_images/p5.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fayomihorace/django-netflix-clone/HEAD/media/preview_images/p5.PNG
--------------------------------------------------------------------------------
/media/preview_images/p6.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fayomihorace/django-netflix-clone/HEAD/media/preview_images/p6.PNG
--------------------------------------------------------------------------------
/media/preview_images/r1.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fayomihorace/django-netflix-clone/HEAD/media/preview_images/r1.PNG
--------------------------------------------------------------------------------
/media/preview_images/r2.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fayomihorace/django-netflix-clone/HEAD/media/preview_images/r2.PNG
--------------------------------------------------------------------------------
/media/preview_images/r3.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fayomihorace/django-netflix-clone/HEAD/media/preview_images/r3.PNG
--------------------------------------------------------------------------------
/media/preview_images/r4.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fayomihorace/django-netflix-clone/HEAD/media/preview_images/r4.PNG
--------------------------------------------------------------------------------
/media/preview_images/r5.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fayomihorace/django-netflix-clone/HEAD/media/preview_images/r5.PNG
--------------------------------------------------------------------------------
/media/preview_images/r6.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fayomihorace/django-netflix-clone/HEAD/media/preview_images/r6.PNG
--------------------------------------------------------------------------------
/netflix/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class NetflixConfig(AppConfig):
5 | name = 'netflix'
6 |
--------------------------------------------------------------------------------
/static/netflix/preview_images/movie1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fayomihorace/django-netflix-clone/HEAD/static/netflix/preview_images/movie1.png
--------------------------------------------------------------------------------
/django_netflix_clone/asgi.py:
--------------------------------------------------------------------------------
1 | """
2 | ASGI config for django_netflix_clone project.
3 |
4 | It exposes the ASGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/
8 | """
9 |
10 | import os
11 |
12 | from django.core.asgi import get_asgi_application
13 |
14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_netflix_clone.settings')
15 |
16 | application = get_asgi_application()
17 |
--------------------------------------------------------------------------------
/django_netflix_clone/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for django_netflix_clone project.
3 |
4 | It exposes the WSGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/4.0/howto/deployment/wsgi/
8 | """
9 |
10 | import os
11 |
12 | from django.core.wsgi import get_wsgi_application
13 |
14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_netflix_clone.settings')
15 |
16 | application = get_wsgi_application()
17 |
--------------------------------------------------------------------------------
/netflix/migrations/0002_alter_movie_date_created.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.0.2 on 2022-02-27 02:41
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('netflix', '0001_initial'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name='movie',
15 | name='date_created',
16 | field=models.DateTimeField(auto_now_add=True),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/netflix/migrations/0003_alter_movie_file_alter_movie_preview_image.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.1.2 on 2022-11-05 13:05
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('netflix', '0002_alter_movie_date_created'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name='movie',
15 | name='file',
16 | field=models.FileField(upload_to='movies/'),
17 | ),
18 | migrations.AlterField(
19 | model_name='movie',
20 | name='preview_image',
21 | field=models.ImageField(upload_to='preview_images/'),
22 | ),
23 | ]
24 |
--------------------------------------------------------------------------------
/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """Django's command-line utility for administrative tasks."""
3 | import os
4 | import sys
5 |
6 |
7 | def main():
8 | """Run administrative tasks."""
9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_netflix_clone.settings')
10 | try:
11 | from django.core.management import execute_from_command_line
12 | except ImportError as exc:
13 | raise ImportError(
14 | "Couldn't import Django. Are you sure it's installed and "
15 | "available on your PYTHONPATH environment variable? Did you "
16 | "forget to activate a virtual environment?"
17 | ) from exc
18 | execute_from_command_line(sys.argv)
19 |
20 |
21 | if __name__ == '__main__':
22 | main()
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # django-netflix-clone
2 | A simple full-stack clone of Netflix website using Django.
3 |
4 | ## This project is the result of this [Dev.to](https://dev.to/fayomihorace/build-a-netflix-clone-with-django-part-1-complete-beginner-course-3al3) tutorial [Build a Netflix clone with Django (Complete beginner course)](https://dev.to/fayomihorace/build-a-netflix-clone-with-django-part-1-complete-beginner-course-3al3).
5 |
6 | ## Demo
7 | https://morning-tree-7095.fly.dev/
8 |
9 | If you're already familiar with Django, you don't need to follow the tutorial.
10 | Just follow these few step:
11 |
12 | ## Setup the project
13 | - `virtualenv venv` (make sure you have `virtualenv` installed)
14 | - `source venv/bin/activate`
15 | - `pip install -r requirements.txt`
16 | - `python manage.py migrate`
17 |
18 | ## Load fixtures (Optional)
19 | - `python manage.py loaddata netflix/fixtures/initial.json`
20 |
21 | ## Create superuser (Optional)
22 | - `python manage.py createsuperuser`
23 |
24 |
25 | ## Start the developement server
26 | - `python manage.py runserver`
27 |
28 | ## Run tests
29 | - `python manage.py test`
30 |
--------------------------------------------------------------------------------
/netflix/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from django.utils.html import format_html
3 |
4 | from netflix.models import Movie
5 | from netflix.models import Category
6 | from netflix.models import Tag
7 |
8 | admin.site.register(Category)
9 | admin.site.register(Tag)
10 |
11 |
12 | class MovieAdmin(admin.ModelAdmin):
13 |
14 | def preview(self, movie):
15 | """Render preview image as html image."""
16 |
17 | return format_html(
18 | f' '
19 | )
20 |
21 | def video(self, movie):
22 | """Render movie video as html image."""
23 |
24 | return format_html(
25 | f"""
26 |
27 |
28 | Your browser does not support the video tag.
29 | """
30 | )
31 |
32 | preview.short_description = 'Movie image'
33 | video.short_description = 'Movie video'
34 | list_display = ['name', 'preview', 'video', 'description']
35 |
36 | admin.site.register(Movie, MovieAdmin)
37 |
--------------------------------------------------------------------------------
/netflix/models.py:
--------------------------------------------------------------------------------
1 | from django.utils import timezone
2 | from django.db import models
3 | from django.contrib.auth.models import User
4 | CHARS_MAX_LENGTH: int = 150
5 |
6 |
7 | class Category(models.Model):
8 | """Category model class."""
9 |
10 | name = models.CharField(max_length=CHARS_MAX_LENGTH, blank=True)
11 | description = models.TextField(blank=True, null=True)
12 |
13 | def __str__(self):
14 | return self.name
15 |
16 |
17 | class Tag(models.Model):
18 | """Tag model class."""
19 |
20 | name = models.CharField(max_length=CHARS_MAX_LENGTH, blank=True)
21 | description = models.TextField(blank=True, null=True)
22 |
23 |
24 | def __str__(self):
25 | return self.name
26 |
27 |
28 | class Movie(models.Model):
29 | """Movie model class."""
30 |
31 | name = models.CharField(max_length=CHARS_MAX_LENGTH, blank=True)
32 | description = models.TextField(blank=True, null=True)
33 | category = models.ForeignKey(Category, on_delete=models.CASCADE)
34 | tags = models.ManyToManyField(Tag)
35 | watch_count = models.IntegerField(default=0)
36 | file = models.FileField(upload_to='movies/')
37 | preview_image = models.ImageField(upload_to='preview_images/')
38 | date_created = models.DateTimeField(auto_now_add=True)
39 |
40 | def __str__(self):
41 | return self.name
42 |
--------------------------------------------------------------------------------
/templates/netflix/register.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 |
3 |
4 |
5 |
6 | Netflix
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | Home
22 |
23 |
24 | Login
25 |
26 |
27 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/templates/netflix/login.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 |
3 |
4 |
5 |
6 | Netflix
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | Home
22 |
23 |
24 | Register
25 |
26 |
27 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/django_netflix_clone/urls.py:
--------------------------------------------------------------------------------
1 | """django_netflix_clone URL Configuration
2 |
3 | The `urlpatterns` list routes URLs to views. For more information please see:
4 | https://docs.djangoproject.com/en/4.0/topics/http/urls/
5 | Examples:
6 | Function views
7 | 1. Add an import: from my_app import views
8 | 2. Add a URL to urlpatterns: path('', views.home, name='home')
9 | Class-based views
10 | 1. Add an import: from other_app.views import Home
11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
12 | Including another URLconf
13 | 1. Import the include() function: from django.urls import include, path
14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
15 | """
16 | from django.contrib import admin
17 | from django.urls import path
18 | from django.conf import settings # Add this line
19 | from django.conf.urls.static import static # Add this line
20 |
21 | from netflix.views import index_view # Add this line
22 | from netflix.views import register_view # Add this line
23 | from netflix.views import login_view # Add this line
24 | from netflix.views import logout_view # Add this line
25 | from netflix.views import watch_movie_view
26 |
27 | urlpatterns = [
28 | path('admin/', admin.site.urls),
29 | path('', index_view, name='home'), # Add this line
30 | path('watch', watch_movie_view, name='watch_movie'), # Add this line
31 | path('register', register_view, name='register'), # Add this line
32 | path('login', login_view, name='login'), # Add this line
33 | path('logout', logout_view, name='logout'), # Add this line
34 | ]
35 | # Add the lines below
36 | urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
37 | urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
38 |
--------------------------------------------------------------------------------
/netflix/forms.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 | from django.contrib.auth.models import User
3 |
4 |
5 | class RegisterForm(forms.Form):
6 | """Registration form class."""
7 |
8 | firstname = forms.CharField(label="First name")
9 | lastname = forms.CharField(label="Last name")
10 | email = forms.EmailField(label="Email Address")
11 | password = forms.CharField(
12 | label="Password",
13 | widget=forms.PasswordInput(render_value=True),
14 | min_length=6,
15 | max_length=20
16 | )
17 | password_conf = forms.CharField(label="Password confirmation", widget=forms.PasswordInput)
18 |
19 | def clean(self):
20 | """Check if the form is validated."""
21 | # perform default validation of the form
22 | super(RegisterForm, self).clean()
23 |
24 | # extract user input data
25 | email = self.cleaned_data.get('email')
26 | password = self.cleaned_data.get('password')
27 | password_conf = self.cleaned_data.get('password_conf')
28 |
29 | # Check if the password match the password confirmation
30 | if password != password_conf:
31 | self._errors['password_conf'] = self.error_class([
32 | "wrong confirmation"
33 | ])
34 |
35 | # Check if the email used doen't already exist
36 | if User.objects.filter(username=email).exists():
37 | self._errors['email'] = self.error_class(['Email already exist'])
38 |
39 | # return any errors if found
40 | return self.cleaned_data
41 |
42 |
43 | class LoginForm(forms.Form):
44 | """Login form class."""
45 |
46 | email = forms.EmailField(label="Email Address")
47 | password = forms.CharField(label="Password", widget=forms.PasswordInput)
48 |
49 |
50 | class SearchForm(forms.Form):
51 | """Search form class."""
52 | search_text = forms.CharField(
53 | label="",
54 | widget=forms.TextInput(attrs={'placeholder': 'Search'})
55 | )
56 |
--------------------------------------------------------------------------------
/netflix/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.0.2 on 2022-02-27 02:21
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 | import django.utils.timezone
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | initial = True
11 |
12 | dependencies = [
13 | ]
14 |
15 | operations = [
16 | migrations.CreateModel(
17 | name='Category',
18 | fields=[
19 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
20 | ('name', models.CharField(blank=True, max_length=150)),
21 | ('description', models.TextField(blank=True, null=True)),
22 | ],
23 | ),
24 | migrations.CreateModel(
25 | name='Tag',
26 | fields=[
27 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
28 | ('name', models.CharField(blank=True, max_length=150)),
29 | ('description', models.TextField(blank=True, null=True)),
30 | ],
31 | ),
32 | migrations.CreateModel(
33 | name='Movie',
34 | fields=[
35 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
36 | ('name', models.CharField(blank=True, max_length=150)),
37 | ('description', models.TextField(blank=True, null=True)),
38 | ('watch_count', models.IntegerField(default=0)),
39 | ('file', models.FileField(upload_to='media/')),
40 | ('preview_image', models.ImageField(upload_to='media/')),
41 | ('date_created', models.DateTimeField(default=django.utils.timezone.now)),
42 | ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='netflix.category')),
43 | ('tags', models.ManyToManyField(to='netflix.Tag')),
44 | ],
45 | ),
46 | ]
47 |
--------------------------------------------------------------------------------
/templates/netflix/watch_movie.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 |
3 |
4 |
5 |
6 | Netflix
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
39 |
40 |
41 |
42 |
43 | {% if movie %}
44 | {{movie.name}}
45 |
46 |
47 |
48 | {% else %}
49 | Invalid url
50 | {% endif %}
51 |
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/templates/netflix/index_clean.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 |
3 |
4 |
5 |
6 | Netflix
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
39 |
40 |
41 |
42 |
43 |
51 |
52 | Adventure
53 |
58 |
59 |
60 |
61 |
85 |
86 |
87 |
88 |
92 |
93 |
94 |
95 |
--------------------------------------------------------------------------------
/templates/netflix/index.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 |
3 |
4 |
5 |
6 | Netflix
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
46 |
47 |
48 |
49 |
50 |
51 | {% for category, movies in data %}
52 |
{{category}}
53 |
61 | {% endfor %}
62 |
63 |
64 |
65 |
66 |
90 |
91 |
92 |
93 |
97 |
98 |
99 |
100 |
--------------------------------------------------------------------------------
/django_netflix_clone/settings.py:
--------------------------------------------------------------------------------
1 | """
2 | Django settings for django_netflix_clone project.
3 |
4 | Generated by 'django-admin startproject' using Django 4.0.2.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/4.0/topics/settings/
8 |
9 | For the full list of settings and their values, see
10 | https://docs.djangoproject.com/en/4.0/ref/settings/
11 | """
12 | import os
13 | from pathlib import Path
14 |
15 | # Build paths inside the project like this: BASE_DIR / 'subdir'.
16 | BASE_DIR = Path(__file__).resolve().parent.parent
17 |
18 |
19 | # Quick-start development settings - unsuitable for production
20 | # See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/
21 |
22 | # SECURITY WARNING: keep the secret key used in production secret!
23 | SECRET_KEY = 'django-insecure-m2rl*&!!c7&-_bmg59(kd)ll_-h8(qc3c=9&^_j4kz(=i)z5yc'
24 |
25 | # SECURITY WARNING: don't run with debug turned on in production!
26 | DEBUG = True
27 |
28 | ALLOWED_HOSTS = []
29 |
30 |
31 | # Application definition
32 |
33 | INSTALLED_APPS = [
34 | 'django.contrib.admin',
35 | 'django.contrib.auth',
36 | 'django.contrib.contenttypes',
37 | 'django.contrib.sessions',
38 | 'django.contrib.messages',
39 | 'django.contrib.staticfiles',
40 | 'netflix' # Add this line
41 | ]
42 |
43 | MIDDLEWARE = [
44 | 'django.middleware.security.SecurityMiddleware',
45 | 'django.contrib.sessions.middleware.SessionMiddleware',
46 | 'django.middleware.common.CommonMiddleware',
47 | 'django.middleware.csrf.CsrfViewMiddleware',
48 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
49 | 'django.contrib.messages.middleware.MessageMiddleware',
50 | 'django.middleware.clickjacking.XFrameOptionsMiddleware',
51 | ]
52 |
53 | ROOT_URLCONF = 'django_netflix_clone.urls'
54 |
55 | TEMPLATES = [
56 | {
57 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
58 | 'DIRS': [os.path.join(BASE_DIR, 'templates')],
59 | 'APP_DIRS': True,
60 | 'OPTIONS': {
61 | 'context_processors': [
62 | 'django.template.context_processors.debug',
63 | 'django.template.context_processors.request',
64 | 'django.contrib.auth.context_processors.auth',
65 | 'django.contrib.messages.context_processors.messages',
66 | ],
67 | },
68 | },
69 | ]
70 |
71 | WSGI_APPLICATION = 'django_netflix_clone.wsgi.application'
72 |
73 |
74 | # Database
75 | # https://docs.djangoproject.com/en/4.0/ref/settings/#databases
76 |
77 | DATABASES = {
78 | 'default': {
79 | 'ENGINE': 'django.db.backends.sqlite3',
80 | 'NAME': BASE_DIR / 'db.sqlite3',
81 | }
82 | }
83 |
84 |
85 | # Password validation
86 | # https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators
87 |
88 | AUTH_PASSWORD_VALIDATORS = [
89 | {
90 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
91 | },
92 | {
93 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
94 | },
95 | {
96 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
97 | },
98 | {
99 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
100 | },
101 | ]
102 |
103 |
104 | # Internationalization
105 | # https://docs.djangoproject.com/en/4.0/topics/i18n/
106 |
107 | LANGUAGE_CODE = 'en-us'
108 |
109 | TIME_ZONE = 'UTC'
110 |
111 | USE_I18N = True
112 |
113 | USE_TZ = True
114 |
115 |
116 | # Static files (CSS, JavaScript, Images)
117 | # https://docs.djangoproject.com/en/4.0/howto/static-files/
118 |
119 | STATIC_URL = 'static/'
120 | MEDIA_URL = 'media/'
121 | MEDIA_ROOT = os.path.join(BASE_DIR, 'media/')
122 | STATICFILES_DIRS = [
123 | os.path.join(BASE_DIR, "static"), # Add this line
124 | ]
125 | # Default primary key field type
126 | # https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field
127 |
128 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
129 |
--------------------------------------------------------------------------------
/netflix/views.py:
--------------------------------------------------------------------------------
1 | from importlib import invalidate_caches
2 | from django.shortcuts import render
3 | from django.http import HttpResponse, HttpResponseRedirect
4 | from django.contrib.auth.models import User
5 | from django.contrib.auth.hashers import make_password
6 | from django.contrib.auth import authenticate, login, logout
7 |
8 | from .forms import RegisterForm
9 | from .forms import LoginForm
10 | from .forms import SearchForm
11 | from .models import Movie
12 |
13 | PAGE_SIZE_PER_CATEGORY = 20
14 |
15 |
16 | def index_view(request):
17 | """Home page view."""
18 | # We define the list of categories we want to display
19 | categories_to_display = ['Action', 'Adventure']
20 |
21 | data = {}
22 | # We create a dictionary that map each category with the it movies
23 | for category_name in categories_to_display:
24 | movies = Movie.objects.filter(category__name=category_name)
25 | if request.method == 'POST':
26 | search_text = request.POST.get('search_text')
27 | movies = movies.filter(name__icontains=search_text)
28 | # we limit the number of movies to PAGE_SIZE_PER_CATEGORY = 20
29 | data[category_name] = movies[:PAGE_SIZE_PER_CATEGORY]
30 |
31 | search_form = SearchForm()
32 | # We return the response with the data
33 | return render(request, 'netflix/index.html', {
34 | 'data': data.items(),
35 | 'search_form': search_form
36 | })
37 |
38 |
39 | def watch_movie_view(request):
40 | """Watch view."""
41 | # The primary key of the movie the user want to watch is sent by GET parameters.
42 | # We retrieve that pk.
43 | movie_pk = request.GET.get('movie_pk')
44 | # We try to get from the database the movie with the given pk
45 | try:
46 | movie = Movie.objects.get(pk=movie_pk)
47 | except Movie.DoesNotExist:
48 | # if that movie doesn't exist, Movie.DoesNotExist exception is raised
49 | # and we then catch it and set the url to None instead
50 | movie = None
51 | return render(request, 'netflix/watch_movie.html', {'movie': movie})
52 |
53 |
54 | def register_view(request):
55 | """Registration view."""
56 | if request.method == 'GET':
57 | # executed to render the registration page
58 | register_form = RegisterForm()
59 | return render(request, 'netflix/register.html', locals())
60 | else:
61 | # executed on registration form submission
62 | register_form = RegisterForm(request.POST)
63 | if register_form.is_valid():
64 | User.objects.create(
65 | first_name=request.POST.get('firstname'),
66 | last_name=request.POST.get('lastname'),
67 | email=request.POST.get('email'),
68 | username=request.POST.get('email'),
69 | password=make_password(request.POST.get('password'))
70 | )
71 | return HttpResponseRedirect('/login')
72 | return render(request, 'netflix/register.html', locals())
73 |
74 |
75 | def login_view(request):
76 | """Login view."""
77 | if request.method == 'GET':
78 | # executed to render the login page
79 | login_form = LoginForm()
80 | return render(request, 'netflix/login.html', locals())
81 | else:
82 | # get user credentials input
83 | username = request.POST['email']
84 | password = request.POST['password']
85 | # If the email provided by user exists and match the
86 | # password he provided, then we authenticate him.
87 | user = authenticate(username=username, password=password)
88 | if user is not None:
89 | # if the credentials are good, we login the user
90 | login(request, user)
91 | # then we redirect him to home page
92 | return HttpResponseRedirect('/')
93 | # if the credentials are wrong, we redirect him to login and let him know
94 | return render(
95 | request,
96 | 'netflix/login.html',
97 | {
98 | 'wrong_credentials': True,
99 | 'login_form': LoginForm(request.POST)
100 | }
101 | )
102 |
103 | def logout_view(request):
104 | """Logout view."""
105 | # logout the request
106 | logout(request)
107 | # redirect user to home page
108 | return HttpResponseRedirect('/')
109 |
--------------------------------------------------------------------------------
/netflix/fixtures/initial.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "model": "netflix.Category",
4 | "pk": 1,
5 | "fields": { "name": "Action" }
6 | },
7 | {
8 | "model": "netflix.Category",
9 | "pk": 2,
10 | "fields": { "name": "Adventure" }
11 | },
12 | {
13 | "model": "netflix.Tag",
14 | "pk": 1,
15 | "fields": { "name": "Popular" }
16 | },
17 | {
18 | "model": "netflix.Tag",
19 | "pk": 2,
20 | "fields": { "name": "Trending now" }
21 | },
22 | {
23 | "model": "netflix.Movie",
24 | "pk": 1,
25 | "fields": {
26 | "name": "The Road trick",
27 | "description": "The Road trick description",
28 | "category": 1,
29 | "tags": [1, 2],
30 | "watch_count": 10,
31 | "preview_image": "preview_images/p1.PNG",
32 | "file": "movies/blank.mp4",
33 | "date_created": "2022-05-05 14:08:15.221580"
34 | }
35 | },
36 | {
37 | "model": "netflix.Movie",
38 | "pk": 2,
39 | "fields": {
40 | "name": "Wynonna",
41 | "description": "Wynonna",
42 | "category": 1,
43 | "tags": [1],
44 | "watch_count": 5,
45 | "preview_image": "preview_images/p2.PNG",
46 | "file": "movies/blank.mp4",
47 | "date_created": "2022-05-05 14:08:15.221580"
48 | }
49 | },
50 | {
51 | "model": "netflix.Movie",
52 | "pk": 3,
53 | "fields": {
54 | "name": "The Ballad of Hug SANCHEZ",
55 | "description": "The Ballad of Hug SANCHEZ",
56 | "category": 1,
57 | "tags": [2],
58 | "watch_count": 2,
59 | "preview_image": "preview_images/p3.PNG",
60 | "file": "movies/blank.mp4",
61 | "date_created": "2022-05-05 14:08:15.221580"
62 | }
63 | },
64 | {
65 | "model": "netflix.Movie",
66 | "pk": 4,
67 | "fields": {
68 | "name": "Grey's anatomy",
69 | "description": "Grey's anatomy",
70 | "category": 1,
71 | "tags": [],
72 | "watch_count": 1,
73 | "preview_image": "preview_images/p4.PNG",
74 | "file": "movies/blank.mp4",
75 | "date_created": "2022-05-05 14:08:15.221580"
76 | }
77 | },
78 | {
79 | "model": "netflix.Movie",
80 | "pk": 5,
81 | "fields": {
82 | "name": "Step Up 2",
83 | "description": "Step Up 2",
84 | "category": 1,
85 | "tags": [1, 2],
86 | "watch_count": 25,
87 | "preview_image": "preview_images/p5.PNG",
88 | "file": "movies/blank.mp4",
89 | "date_created": "2022-05-05 14:08:15.221580"
90 | }
91 | },
92 | {
93 | "model": "netflix.Movie",
94 | "pk": 6,
95 | "fields": {
96 | "name": "Liquid Science",
97 | "description": "Liquid Science",
98 | "category": 1,
99 | "tags": [1, 2],
100 | "watch_count": 11,
101 | "preview_image": "preview_images/p6.PNG",
102 | "file": "movies/blank.mp4",
103 | "date_created": "2022-05-05 14:08:15.221580"
104 | }
105 | },
106 | {
107 | "model": "netflix.Movie",
108 | "pk": 7,
109 | "fields": {
110 | "name": "Lost in space",
111 | "description": "Lost in space",
112 | "category": 2,
113 | "tags": [1],
114 | "watch_count": 5,
115 | "preview_image": "preview_images/r1.PNG",
116 | "file": "movies/blank.mp4",
117 | "date_created": "2022-05-05 14:08:15.221580"
118 | }
119 | },
120 | {
121 | "model": "netflix.Movie",
122 | "pk": 8,
123 | "fields": {
124 | "name": "Queen of the South",
125 | "description": "Queen of the South",
126 | "category": 2,
127 | "tags": [],
128 | "watch_count": 5,
129 | "preview_image": "preview_images/r2.PNG",
130 | "file": "movies/blank.mp4",
131 | "date_created": "2022-05-05 14:08:15.221580"
132 | }
133 | },
134 | {
135 | "model": "netflix.Movie",
136 | "pk": 9,
137 | "fields": {
138 | "name": "Undercover Boss",
139 | "description": "Undercover Boss",
140 | "category": 2,
141 | "tags": [1, 2],
142 | "watch_count": 5,
143 | "preview_image": "preview_images/r3.PNG",
144 | "file": "movies/blank.mp4",
145 | "date_created": "2022-05-05 14:08:15.221580"
146 | }
147 | },
148 | {
149 | "model": "netflix.Movie",
150 | "pk": 10,
151 | "fields": {
152 | "name": "Penny Dreadful",
153 | "description": "Penny Dreadful",
154 | "category": 2,
155 | "tags": [1],
156 | "watch_count": 5,
157 | "preview_image": "preview_images/r4.PNG",
158 | "file": "movies/blank.mp4",
159 | "date_created": "2022-05-05 14:08:15.221580"
160 | }
161 | },
162 | {
163 | "model": "netflix.Movie",
164 | "pk": 11,
165 | "fields": {
166 | "name": "Money heist",
167 | "description": "Money heist",
168 | "category": 2,
169 | "tags": [2],
170 | "watch_count": 5,
171 | "preview_image": "preview_images/r5.PNG",
172 | "file": "movies/blank.mp4",
173 | "date_created": "2022-05-05 14:08:15.221580"
174 | }
175 | },
176 | {
177 | "model": "netflix.Movie",
178 | "pk": 12,
179 | "fields": {
180 | "name": "The Week of",
181 | "description": "The Week of",
182 | "category": 2,
183 | "tags": [1],
184 | "watch_count": 5,
185 | "preview_image": "preview_images/r6.PNG",
186 | "file": "movies/blank.mp4",
187 | "date_created": "2022-05-05 14:08:15.221580"
188 | }
189 | }
190 | ]
191 |
--------------------------------------------------------------------------------
/static/netflix/style.css:
--------------------------------------------------------------------------------
1 | /* CSS VARIABLES */
2 | :root {
3 | --primary: #141414;
4 | --light: #F3F3F3;
5 | --dark: #686868;
6 | }
7 |
8 | html, body {
9 | width: 100vw;
10 | min-height: 100vh;
11 | margin: 0;
12 | padding: 0;
13 | background-color: var(--primary);
14 | color: var(--light);
15 | font-family: Arial, Helvetica, sans-serif;
16 | box-sizing: border-box;
17 | line-height: 1.4;
18 | }
19 |
20 | img {
21 | max-width: 100%;
22 | }
23 |
24 | h1 {
25 | padding-top: 60px;
26 | }
27 |
28 | .wrapper {
29 | margin: 0;
30 | padding: 0;
31 | }
32 |
33 | /* HEADER */
34 | header {
35 | padding: 20px 20px 0 20px;
36 | position: fixed;
37 | top: 0;
38 | display: grid;
39 | grid-gap:5px;
40 | grid-template-columns: 1fr 4fr 1fr;
41 | grid-template-areas:
42 | "nt mn mn sb . . . ";
43 | background-color: var(--primary);
44 | width: 100%;
45 | margin-bottom: 0px;
46 | }
47 |
48 | .netflixLogo {
49 | grid-area: nt;
50 | object-fit: cover;
51 | width: 100px;
52 | max-height: 100%;
53 |
54 | padding-left: 30px;
55 | padding-top: 0px;
56 | }
57 |
58 | .netflixLogo img {
59 | height: 35px;
60 | }
61 |
62 | #logo {
63 | color: #E50914;
64 | margin: 0;
65 | padding: 0;
66 | }
67 |
68 |
69 | .main-nav {
70 | grid-area: mn;
71 | padding: 0 30px 0 20px;
72 | }
73 |
74 | .main-nav a {
75 | color: var(--light);
76 | text-decoration: none;
77 | margin: 5px;
78 | }
79 |
80 | .main-nav a:hover {
81 | color: var(--dark);
82 | }
83 |
84 | .sub-nav {
85 | grid-area: sb;
86 | padding: 0 40px 0 40px;
87 | }
88 |
89 | .sub-nav a {
90 | color: var(--light);
91 | text-decoration: none;
92 | margin: 5px;
93 | }
94 |
95 | .sub-nav a:hover {
96 | color: var(--dark);
97 | }
98 |
99 |
100 | /* MAIN CONTIANER */
101 | .main-container {
102 | padding: 50px;
103 | }
104 |
105 | .box {
106 | display: grid;
107 | grid-gap: 20px;
108 | grid-template-columns: repeat(6, minmax(100px, 1fr));
109 | }
110 |
111 | .box a {
112 | transition: transform .3s;
113 | }
114 |
115 | .box a:hover {
116 | transition: transform .3s;
117 | -ms-transform: scale(1.4);
118 | -webkit-transform: scale(1.4);
119 | transform: scale(1.4);
120 | }
121 |
122 | .box img {
123 | border-radius: 2px;
124 | }
125 |
126 | /* LINKS */
127 | .link {
128 | padding: 50px;
129 | }
130 |
131 | .sub-links ul {
132 | list-style: none;
133 | padding: 0;
134 | display: grid;
135 | grid-gap: 20px;
136 | grid-template-columns: repeat(4, 1fr);
137 | }
138 |
139 | .sub-links a {
140 | color: var(--dark);
141 | text-decoration: none;
142 | }
143 |
144 | .sub-links a:hover {
145 | color: var(--dark);
146 | text-decoration: underline;
147 | }
148 |
149 | .logos a{
150 | padding: 10px;
151 | }
152 |
153 | .logo {
154 | color: var(--dark);
155 | }
156 |
157 |
158 | /* FOOTER */
159 | footer {
160 | padding: 20px;
161 | text-align: center;
162 | color: var(--dark);
163 | margin: 10px;
164 | }
165 |
166 | /* MEDIA QUERIES */
167 |
168 | @media(max-width: 900px) {
169 |
170 | header {
171 | display: grid;
172 | grid-gap: 20px;
173 | grid-template-columns: repeat(2, 1fr);
174 | grid-template-areas:
175 | "nt nt nt . . . sb . . . "
176 | "mn mn mn mn mn mn mn mn mn mn";
177 | }
178 |
179 | .box {
180 | display: grid;
181 | grid-gap: 20px;
182 | grid-template-columns: repeat(4, minmax(100px, 1fr));
183 | }
184 |
185 | }
186 |
187 | @media(max-width: 700px) {
188 |
189 | header {
190 | display: grid;
191 | grid-gap: 20px;
192 | grid-template-columns: repeat(2, 1fr);
193 | grid-template-areas:
194 | "nt nt nt . . . sb . . . "
195 | "mn mn mn mn mn mn mn mn mn mn";
196 | }
197 |
198 | .box {
199 | display: grid;
200 | grid-gap: 20px;
201 | grid-template-columns: repeat(3, minmax(100px, 1fr));
202 | }
203 |
204 | .sub-links ul {
205 | display: grid;
206 | grid-gap: 20px;
207 | grid-template-columns: repeat(3, 1fr);
208 | }
209 |
210 | }
211 |
212 | @media(max-width: 500px) {
213 |
214 | .wrapper {
215 | font-size: 15px;
216 | }
217 |
218 | header {
219 | margin: 0;
220 | padding: 20px 0 0 0;
221 | position: static;
222 | display: grid;
223 | grid-gap: 20px;
224 | grid-template-columns: repeat(1, 1fr);
225 | grid-template-areas:
226 | "nt"
227 | "mn"
228 | "sb";
229 | text-align: center;
230 | }
231 |
232 | .netflixLogo {
233 | max-width: 100%;
234 | margin: auto;
235 | padding-right: 20px;
236 | }
237 |
238 | .main-nav {
239 | display: grid;
240 | grid-gap: 0px;
241 | grid-template-columns: repeat(1, 1fr);
242 | text-align: center;
243 | }
244 |
245 | h1 {
246 | text-align: center;
247 | font-size: 18px;
248 | }
249 |
250 |
251 |
252 | .box {
253 | display: grid;
254 | grid-gap: 20px;
255 | grid-template-columns: repeat(1, 1fr);
256 | text-align: center;
257 | }
258 |
259 | .box a:hover {
260 | transition: transform .3s;
261 | -ms-transform: scale(1);
262 | -webkit-transform: scale(1);
263 | transform: scale(1.2);
264 | }
265 |
266 | .logos {
267 | display: grid;
268 | grid-gap: 20px;
269 | grid-template-columns: repeat(2, 1fr);
270 | text-align: center;
271 | }
272 |
273 | .sub-links ul {
274 | display: grid;
275 | grid-gap: 20px;
276 | grid-template-columns: repeat(1, 1fr);
277 | text-align: center;
278 | font-size: 15px;
279 | }
280 | }
--------------------------------------------------------------------------------
/templates/netflix/full_index_light.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 |
3 |
4 |
5 |
6 | Netflix
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
46 |
47 |
48 |
49 |
50 |
51 |
Action
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | Adventure
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
105 |
106 |
107 |
108 |
112 |
113 |
114 |
115 |
--------------------------------------------------------------------------------
/netflix/tests.py:
--------------------------------------------------------------------------------
1 | from http import HTTPStatus
2 | from copy import deepcopy
3 |
4 | from django.contrib.auth.models import User
5 | from django.test import TestCase
6 | from django.contrib import auth
7 |
8 | from netflix.models import Category, Movie
9 |
10 |
11 | TEST_DATA = {
12 | "firstname": "John",
13 | "lastname": "Joe",
14 | "email": "johnjoe@gmail.com",
15 | "password": "NetflixClone2022",
16 | "password_conf": "NetflixClone2022"
17 | }
18 |
19 |
20 | class IndexTests(TestCase):
21 |
22 | def setUp(self):
23 | self.category = Category.objects.create(name="Action")
24 | self.spider_man_movie = Movie.objects.create(
25 | name="Spider man",
26 | category=self.category
27 | )
28 | self.avatar_movie = Movie.objects.create(
29 | name="Avatar",
30 | category=self.category
31 | )
32 |
33 | def test_index_render_all_movies(self):
34 | response = self.client.get("/")
35 | # make sure index displays the two movies
36 | self.assertContains(response, self.spider_man_movie.name)
37 | self.assertContains(response, self.avatar_movie.name)
38 |
39 | def test_index_filter_movies(self):
40 | # make sure only `Avatar` movie is rendered when the search term is `ava`
41 | # This also asserts that the search is case insensitive as the real name
42 | # is `Avatar` with upper `A` and we search `ava`.
43 | response = self.client.post(
44 | "/",
45 | data={"search_text": "avat"}
46 | )
47 | # make sure index displays `Avatar` movie
48 | self.assertContains(response, self.avatar_movie.name)
49 | # Make sure index doesn't display `Spider man` movie
50 | self.assertNotContains(response, self.spider_man_movie.name)
51 |
52 |
53 | # Create your tests here.
54 | class RegisterTests(TestCase):
55 |
56 | def test_get_registration_page(self):
57 | # We call the `register` route using `GET`
58 | response = self.client.get("/register")
59 | # We make an assertion that no error is returned
60 | self.assertEqual(response.status_code, HTTPStatus.OK)
61 | # We assert that the returned page contains the button for registration
62 | # form
63 | self.assertContains(
64 | response,
65 | 'Register ',
66 | html=True
67 | )
68 |
69 | def test_registration_with_valid_data(self):
70 | # We make sure that there no user exists in the database before the registration
71 | self.assertEqual(User.objects.count(), 0)
72 |
73 | # We call the `register` route using `POST` to simulate form submission
74 | # with the good data in the setup
75 | self.client.post("/register", data=TEST_DATA)
76 |
77 | # We make sure that there is 1 user created in the database after the registration
78 | self.assertEqual(User.objects.count(), 1)
79 |
80 | # We make sure that new created user data are the same we used during registration
81 | new_user = User.objects.first()
82 | self.assertEqual(new_user.first_name, TEST_DATA['firstname'])
83 | self.assertEqual(new_user.last_name, TEST_DATA['lastname'])
84 | self.assertEqual(new_user.email, TEST_DATA['email'])
85 |
86 | def test_registration_with_empty_fields(self):
87 | # We make sure that there no user exists in the database before the registration
88 | self.assertEqual(User.objects.count(), 0)
89 |
90 | # We call the `register` route using `POST` to simulate form submission
91 | # with empty fields data
92 | response = self.client.post(
93 | "/register",
94 | data={
95 | "firstname": "",
96 | "lastname": "",
97 | "email": "",
98 | "password": "",
99 | "password_conf": ""
100 | }
101 | )
102 |
103 | # We make sure that there no user exists in the database after the registration
104 | # failure. That means no user has been created
105 | self.assertEqual(User.objects.count(), 0)
106 | # Make sure the required message is displayed
107 | self.assertContains(response, 'This field is required')
108 |
109 | def test_registration_with_wrong_password_confirmation(self):
110 | # We make sure that there no user exists in the database before the registration
111 | self.assertEqual(User.objects.count(), 0)
112 |
113 | # We call the `register` route using `POST` to simulate form submission
114 | # with wrong password confirmation good data in the setup
115 | # This time, to create the invalid dict, we create a copy of the good
116 | # data of the setup first.
117 | bad_data = deepcopy(TEST_DATA)
118 | bad_data['password_conf'] = "Wrong Password Confirmation"
119 | response = self.client.post(
120 | "/register",
121 | data=bad_data
122 | )
123 |
124 | # We make sure that there no user exists in the database after the registration
125 | # failure. That means no user has been created
126 | self.assertEqual(User.objects.count(), 0)
127 |
128 | # Make sure the wrong confirmation is displayed
129 | self.assertContains(response, 'wrong confirmation')
130 |
131 |
132 | class LoginTests(TestCase):
133 |
134 | def setUp(self):
135 | self.user = User.objects.create(username="johnjoe@gmail.com")
136 | self.user_password = "NetflixPassword2022"
137 | self.user.set_password(self.user_password)
138 | self.user.save()
139 |
140 | def test_login_with_invalid_credentials(self):
141 | self.assertFalse(auth.get_user(self.client).is_authenticated)
142 | response = self.client.post(
143 | "/login",
144 | data={"email": self.user.username, "password": "Wrong password"}
145 | )
146 | self.assertContains(response, 'Invalid credentials.')
147 | self.assertFalse(auth.get_user(self.client).is_authenticated)
148 |
149 | def test_login_with_good_credentials(self):
150 | self.assertFalse(auth.get_user(self.client).is_authenticated)
151 | self.client.post(
152 | "/login",
153 | data={"email": self.user.username, "password": self.user_password}
154 | )
155 | self.assertTrue(auth.get_user(self.client).is_authenticated)
156 |
--------------------------------------------------------------------------------