├── 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 | """ 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 | 20 | 23 | 26 |
27 |
28 |
29 |
30 |
31 | {% csrf_token %} 32 | {{ register_form.as_p }} 33 | 34 |
35 |
36 |
37 |
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 | 20 | 23 | 26 |
27 |
28 |
29 |
30 |
31 | {% csrf_token %} 32 | {% if wrong_credentials %} 33 |

Invalid credentials.

34 | {% endif %} 35 | {{ login_form.as_p }} 36 | 37 |
38 |
39 |
40 |
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 |
17 | 20 | 28 | 38 |
39 | 40 | 41 | 42 |
43 | {% if movie %} 44 |

{{movie.name}}

45 | 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 |
17 | 20 | 28 | 38 |
39 | 40 | 41 | 42 |
43 |
44 |

Action

45 |
46 | 47 | 48 | 49 |
50 |
51 | 52 |

Adventure

53 |
54 | 55 | 56 | 57 |
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 |
17 | 20 | 28 | 45 |
46 | 47 | 48 | 49 |
50 |
51 | {% for category, movies in data %} 52 |

{{category}}

53 |
54 | {% for movie in movies %} 55 | 56 | {{movie.name}} 57 | {{movie.name}} 58 | 59 | {% endfor %} 60 |
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 |
17 | 20 | 28 | 45 |
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 | '', 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 | --------------------------------------------------------------------------------