├── .gitignore ├── LICENSE ├── README.md ├── ToDo.md ├── djangoflix.code-workspace ├── pyvenv.cfg ├── requirements.txt └── src ├── categories ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── tests.py ├── urls.py └── views.py ├── db.json ├── djangoflix ├── __init__.py ├── asgi.py ├── db │ ├── __init__.py │ ├── models.py │ ├── receivers.py │ └── utils.py ├── settings.py ├── urls.py └── wsgi.py ├── manage.py ├── playlists ├── __init__.py ├── admin.py ├── apps.py ├── fixtures │ └── projects.json ├── migrations │ ├── 0001_initial.py │ ├── 0002_playlist_video.py │ ├── 0003_auto_20210315_2357.py │ ├── 0004_alter_playlist_video.py │ ├── 0005_remove_playlist_videos.py │ ├── 0006_auto_20210316_2355.py │ ├── 0007_auto_20210317_0037.py │ ├── 0008_tvshowproxy_tvshowseasonproxy.py │ ├── 0009_playlist_type.py │ ├── 0010_alter_playlist_parent.py │ ├── 0011_movieproxy.py │ ├── 0012_alter_movieproxy_options.py │ ├── 0013_playlist_category.py │ ├── 0014_alter_playlist_category.py │ ├── 0015_auto_20210323_2242.py │ ├── 0016_alter_playlistrelated_related.py │ └── __init__.py ├── mixins.py ├── models.py ├── reference.md ├── shell.md ├── test_movies.py ├── test_playlists.py ├── test_tv_shows.py ├── test_views.py └── views.py ├── ratings ├── __init__.py ├── admin.py ├── apps.py ├── forms.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── templates │ └── ratings │ │ └── rating.html ├── templatetags │ ├── __init__.py │ └── rating.py ├── tests.py └── views.py ├── tags ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_taggeditem_object_id.py │ └── __init__.py ├── models.py ├── reference.md ├── tests.py ├── urls.py └── views.py ├── templates ├── base.html ├── categories │ └── category_list.html ├── playlist_list.html ├── playlists │ ├── cards │ │ ├── movie.html │ │ └── show.html │ ├── featured_list.html │ ├── movie_detail.html │ ├── playlist_detail.html │ ├── season_detail.html │ └── tvshow_detail.html ├── tags │ └── tag_list.html └── videos │ └── embed.html └── videos ├── __init__.py ├── admin.py ├── apps.py ├── migrations ├── 0001_initial.py ├── 0002_rename_title_video_name.py ├── 0003_rename_name_video_title.py ├── 0004_videoproxy.py ├── 0005_alter_videoproxy_options.py ├── 0006_video_active.py ├── 0007_videoallproxy.py ├── 0008_auto_20210315_1921.py ├── 0009_video_state.py ├── 0010_video_publish_timestamp.py ├── 0011_auto_20210315_2015.py ├── 0012_alter_video_video_id.py └── __init__.py ├── models.py ├── tests.py └── views.py /.gitignore: -------------------------------------------------------------------------------- 1 | parse.py 2 | 3 | bin/ 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | pip-wheel-metadata/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # IPython 84 | profile_default/ 85 | ipython_config.py 86 | 87 | # pyenv 88 | .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Coding For Entrepreneurs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![DjangoFlix Logo](https://static.codingforentrepreneurs.com/media/projects/djangoflix/images/share/DjangoFlix_Share.jpg)](https://www.codingforentrepreneurs.com/projects/djangoflix) 2 | 3 | # DjangoFlix 4 | 5 | Create a netflix-like service using Django, React.js, & More. 6 | 7 | 8 | ## Lectures 9 | 1 - Welcome - *No code* 10 | 11 | 2 - Walkthrough - *No code* 12 | 13 | [3 - Setup Project & Workspace](../../tree/ffe83c8c0b46004dcd06e73d156a7f9c6d502375/) 14 | 15 | [4 - Craft To Do List](../../tree/3b1050dcca788553104987b6577d95c82c419432/) 16 | 17 | [5 - django-admin startproject djangoflix](../../tree/41632758252e5f696fc968845ecd883f0a00f10f/) 18 | 19 | [6 - Videos App](../../tree/bb59baa8a0e9d64812ca6d53db3ad6be1be8152f/) 20 | 21 | 7 - Our First Migration & Superuser - *No code* 22 | 23 | [8 - Installing our Model](../../tree/c115c9ca56da01bb91f5b6e98d9632b5a4847456/) 24 | 25 | [9 - Your First TestCase](../../tree/6beee7e5e38001690c16c7abc74559558435287e/) 26 | 27 | [10 - Breaking Tests & Migrations Basics](../../tree/9e82da997669e9939a860ba17dc9ee491dc9d7b9/) 28 | 29 | [11 - Video Model to Django Admin](../../tree/0d163b23d0042a3166378a03aa78337958d5b68d/) 30 | 31 | [12 - Django Admin Model Proxy](../../tree/e1fb3e2e50e9728caaec1c853cb7d27c86176ea8/) 32 | 33 | [13 - Customize the Django Admin](../../tree/b9886f9387029a56e1518d135a89b811c485713b/) 34 | 35 | [14 - CharField Choices for Publishing Videos](../../tree/5942ba3a386ddc879a3d9980213668db057ff871/) 36 | 37 | [15 - Test Publish State Options](../../tree/b29eb022d5514d3f45374eb890c474d4129ae733/) 38 | 39 | [16 - Slug, Timestamp, & Updated Fields](../../tree/4d7fb54f962a94f01443af92653d7b359bddddcb/) 40 | 41 | [17 - Custom Managers & QuerySets](../../tree/d1f739b7aa1816af3a8aecb662c66657ac0300df/) 42 | 43 | [18 - Using Django Signals](../../tree/1fc8916024ef134c44ad444f5acc473ccfc5c142/) 44 | 45 | [19 - Playlists & Foreign Keys](../../tree/abdb39b823d10a681af4d808409af5572c717d49/) 46 | 47 | [20 - Understanding Foreign Keys with the Admin](../../tree/8e288ab79b14beaaa74692344c587dc48e5eb07e/) 48 | 49 | [21 - Django-Managed Python Shell to explore Foreign Keys](../../tree/593f8e3e16c2db45932b3ef79010719a2f18b22a/) 50 | 51 | [22 - Related Names & ManyToManyField](../../tree/52063e1efc9418af52fac47c89c8f9c5598c3ef5/) 52 | 53 | [23 - More on ManyToMany Fields](../../tree/eb1b591a7bab118255fd2046061fd7d59abc3714/) 54 | 55 | [24 - Ordering ManyToMany with Through Model](../../tree/6390c7b02d9d38da59d47f191ec7898c8086338a/) 56 | 57 | [25 - Tabular Inline in the Admin for ManyToMany](../../tree/7c9c42c7fca97b367854c3c01cf2637e8b27c200/) 58 | 59 | [26 - Updated Tests for Through Model](../../tree/609bea1c07a4da36f6ec732c368b439ad0d10235/) 60 | 61 | [27 - Playlists of Playlists](../../tree/b352f03a81dcc0661a8e633167da54096f4024d2/) 62 | 63 | [28 - TV Show Playlist Proxies for Admin](../../tree/f14e196931873894e74c7c023f49d31e2d6b0cde/) 64 | 65 | [29 - Playlist Type Choices and Why](../../tree/3a17b3a95955bfc251ef8554145e4a89f3bbf28d/) 66 | 67 | [30 - Saving Playlist Type via Proxy](../../tree/27f325b4d9f5f63c8c5a61bdd6339eaa2cf69567/) 68 | 69 | [31 - Categories](../../tree/78cbe8bc117993a52444714e76298e5549de668d/) 70 | 71 | [32 - ContentTypes & Generic Foreign Keys](../../tree/946ac5618acf6d04bd47b3814852c4c12f16b65b/) 72 | 73 | [33 - Reverse Relationship for Generic Foreign Keys](../../tree/046d59009506d6a3eedb725bc82357fa80149e76/) 74 | 75 | [34 - Testing Generic Foreign Keys](../../tree/b1d53892a256793532d61975c2953f9450c82055/) 76 | 77 | [35 - User Ratings Model](../../tree/ae3589181ee5895292f37a9ba339c5131496d770/) 78 | 79 | [36 - Testing User Ratings](../../tree/0f2032a9767120978d3e466806b4f15f566fe51d/) 80 | 81 | [37 - QuerySet Aggregation for Average Rating](../../tree/f4c544826a072c04b9a93e9929e327cfd130a360/) 82 | 83 | [38 - Templates & Base Template](../../tree/6856915f19bfa099475f2543e48e341a3502b99b/) 84 | 85 | [39 - Movies & TV Show List Views](../../tree/503f315fe2de5495b73a7c587bcc04017a396d10/) 86 | 87 | [40 -List View Template](../../tree/adef547e044318eb3a363aad2ea061bbc5148d8f/) 88 | 89 | [41 - Proxy Model Tests](../../tree/72de41fe2cc08d9e3c4abe27e8a675727a2fd17c/) 90 | 91 | [42 - URL Routing](../../tree/5d6001c992bc3d83cbcd9ceb3db7071d62f26582/) 92 | 93 | [43 - Detail Views](../../tree/5e3ab2a98e75871b29bb781c78041400b2311c2e/) 94 | 95 | [43 - Detail Views](../../tree/3984513914776facbc3f15c94c49dc04cd15e9fb/) 96 | 97 | [44 - Get Object Exception Handling](../../tree/967aebc1dddeea389892c9b3d57033491ff52d66/) 98 | 99 | [45 - Unique Slug Utility & Signal Receiver](../../tree/77a85cab8c2ec0e2d8f4522f0057200ada11b3a1/) 100 | 101 | [46 - Instance Methods for Videos](../../tree/bc8722e78a614c0b015b540901fc04c76290e00b/) 102 | 103 | [47 - Video Embeds in Templates](../../tree/17d549f67f840b20b7033343d73864d807f3e192/) 104 | 105 | [48 - Related Playlists Field](../../tree/337fafe0c4215d277d756ccb26dc8a1eb531910f/) 106 | 107 | [49 - Home View & Get Absolute Url](../../tree/90321925029cf06ae5cf7c36584848389edcede2/) 108 | 109 | [50 - Category Views](../../tree/eb2e32371ce7e16dbcd3315abae8a7b6c18e03e9/) 110 | 111 | [51 - Tagged Item Views](../../tree/f945fa5b689bc9e12e182f5e7905c66027fec7be/) 112 | 113 | [52 - Search View](../../tree/04dd4eefb91709461cffdc00f23f92767b690c98/) 114 | 115 | [53 - Test Views](../../tree/6fd1414db6b7aee41b8d3243101e8c18d857a116/) 116 | 117 | [54 - Inclusion Template Tag for Ratings](../../tree/601f358845b8be343b0f3e055d435fcaad3eb654/) 118 | 119 | [55 - Ratings Form](../../tree/0f34ed0a437315918d52f2ed1344c3bdf532f65e/) -------------------------------------------------------------------------------- /ToDo.md: -------------------------------------------------------------------------------- 1 | - Videos 2 | - Long Videos (movies) 3 | - Short Videos 4 | - Playlists -> List of movies 5 | - Seasons -> Playlists of Playlists 6 | - Categories 7 | - Action, Comedy 8 | - Tags 9 | - Action, Comedy, Drama, Action-Comedy, Live Studio Audience, 10 | - User Ratings 11 | - 1-5 Star Rating 12 | - Prepares our system for Machine Learning 13 | - Search 14 | - Title 15 | - Descriptions 16 | - Tags 17 | 18 | 19 | ## Future 20 | - User Registration 21 | - JavaScript UI / Frontend Library 22 | - React, Vue, Etc, 23 | - Bootstrap, Tailwind CSS 24 | - Django Rest Framework for API 25 | - Recommender 26 | - Collaborative Filtering with Machine Learning 27 | - Elasticsearch 28 | - Video Analytics & Watch History 29 | - Video hosts API 30 | - Vimeo, YouTube, Wistia, 31 | - Open Source Option (ffmpeg) 32 | - Nginx, stream via ffmpeg 33 | - API-Driven Video Details 34 | - Cast Members (actors, directors) 35 | - Release Year 36 | - Payments Integration 37 | - Stripe / Braintree / Crypot -------------------------------------------------------------------------------- /djangoflix.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "settings": {} 8 | } -------------------------------------------------------------------------------- /pyvenv.cfg: -------------------------------------------------------------------------------- 1 | home = /Library/Frameworks/Python.framework/Versions/3.9/bin 2 | include-system-site-packages = false 3 | version = 3.9.2 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | django>=3.2,<3.3 2 | -------------------------------------------------------------------------------- /src/categories/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/DjangoFlix/18c2c97c40ee03c317dbdb4baab0d61c60d08271/src/categories/__init__.py -------------------------------------------------------------------------------- /src/categories/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | from .models import Category 5 | 6 | admin.site.register(Category) -------------------------------------------------------------------------------- /src/categories/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CategoriesConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'categories' 7 | -------------------------------------------------------------------------------- /src/categories/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2b1 on 2021-03-17 21:26 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Category', 16 | fields=[ 17 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('title', models.CharField(max_length=220)), 19 | ('slug', models.SlugField(blank=True, null=True)), 20 | ('active', models.BooleanField(default=True)), 21 | ('timestamp', models.DateTimeField(auto_now_add=True)), 22 | ('updated', models.DateTimeField(auto_now=True)), 23 | ], 24 | options={ 25 | 'verbose_name': 'Category', 26 | 'verbose_name_plural': 'Categories', 27 | }, 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /src/categories/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/DjangoFlix/18c2c97c40ee03c317dbdb4baab0d61c60d08271/src/categories/migrations/__init__.py -------------------------------------------------------------------------------- /src/categories/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.contenttypes.fields import GenericRelation 2 | 3 | from django.db import models 4 | from django.db.models.signals import pre_save 5 | 6 | from djangoflix.db.receivers import unique_slugify_pre_save 7 | 8 | from tags.models import TaggedItem 9 | 10 | # Create your models here. 11 | class Category(models.Model): 12 | title = models.CharField(max_length=220) 13 | slug = models.SlugField(blank=True, null=True) 14 | active = models.BooleanField(default=True) 15 | timestamp = models.DateTimeField(auto_now_add=True) 16 | updated = models.DateTimeField(auto_now=True) 17 | tags = GenericRelation(TaggedItem, related_query_name='category') 18 | 19 | def get_absolute_url(self): 20 | return f"/category/{self.slug}/" 21 | 22 | 23 | def __str__(self): 24 | return self.title 25 | 26 | class Meta: 27 | verbose_name = 'Category' 28 | verbose_name_plural = 'Categories' 29 | 30 | 31 | 32 | pre_save.connect(unique_slugify_pre_save, sender=Category) -------------------------------------------------------------------------------- /src/categories/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from playlists.models import Playlist 4 | from .models import Category 5 | 6 | class CategoryTestCase(TestCase): 7 | def setUp(self): 8 | cat_a = Category.objects.create(title='Action') 9 | cat_b = Category.objects.create(title='Comedy', active=False) 10 | self.play_a = Playlist.objects.create(title='This is my title', category=cat_a) 11 | self.cat_a = cat_a 12 | self.cat_b = cat_b 13 | 14 | def test_is_active(self): 15 | self.assertTrue(self.cat_a.active) 16 | 17 | def test_not_is_active(self): 18 | self.assertFalse(self.cat_b.active) 19 | 20 | def test_related_playlist(self): 21 | qs = self.cat_a.playlists.all() 22 | self.assertEqual(qs.count(), 1) -------------------------------------------------------------------------------- /src/categories/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from .views import CategoryListView, CategoryDetailView 4 | 5 | 6 | urlpatterns = [ 7 | path('/', CategoryDetailView.as_view()), 8 | path('', CategoryListView.as_view()) 9 | ] -------------------------------------------------------------------------------- /src/categories/views.py: -------------------------------------------------------------------------------- 1 | from django.http import Http404 2 | from django.db.models import Count 3 | from django.views.generic import ListView, DetailView 4 | from django.shortcuts import render 5 | 6 | from playlists.mixins import PlaylistMixin 7 | from playlists.models import Playlist 8 | 9 | 10 | from .models import Category 11 | 12 | class CategoryListView(ListView): 13 | queryset = Category.objects.all().filter(active=True).annotate(pl_count=Count('playlists')).filter(pl_count__gt=0) 14 | 15 | 16 | class CategoryDetailView(PlaylistMixin, ListView): 17 | """ 18 | Another list view for Playlist 19 | """ 20 | def get_context_data(self): 21 | context = super().get_context_data() 22 | try: 23 | obj = Category.objects.get(slug=self.kwargs.get('slug')) 24 | except Category.DoesNotExist: 25 | raise Http404 26 | except Category.MultipleObjectsReturned: 27 | raise Http404 28 | except: 29 | obj = None 30 | context['object'] = obj 31 | if obj is not None: 32 | context['title'] = obj.title 33 | return context 34 | 35 | def get_queryset(self): 36 | slug = self.kwargs.get('slug') 37 | return Playlist.objects.filter(category__slug=slug).movie_or_show() 38 | 39 | -------------------------------------------------------------------------------- /src/db.json: -------------------------------------------------------------------------------- 1 | [{"model": "videos.video", "pk": 1, "fields": {"title": "Avengers Trailer", "description": "", "slug": "avengers-trailer", "video_id": "TcMBFSGVi1c", "active": true, "timestamp": "2021-03-23T22:19:48.708Z", "updated": "2021-03-23T22:19:48.708Z", "state": "PU", "publish_timestamp": "2021-03-23T22:19:48.707Z"}}, {"model": "videos.video", "pk": 2, "fields": {"title": "Parks & Rec Trailer", "description": "", "slug": "parks-rec-trailer", "video_id": "eey-wOyTOJs", "active": true, "timestamp": "2021-03-23T22:20:31.060Z", "updated": "2021-03-23T22:20:31.060Z", "state": "PU", "publish_timestamp": "2021-03-23T22:20:31.060Z"}}, {"model": "videos.video", "pk": 3, "fields": {"title": "The Office Trailer", "description": "", "slug": "the-office-trailer", "video_id": "gO8N3L_aERg", "active": true, "timestamp": "2021-03-23T22:20:56.215Z", "updated": "2021-03-23T22:20:56.215Z", "state": "PU", "publish_timestamp": "2021-03-23T22:20:56.215Z"}}, {"model": "playlists.playlist", "pk": 1, "fields": {"parent": null, "category": null, "order": 1, "title": "Avengers", "type": "MOV", "description": "", "slug": "avengers", "video": 1, "active": true, "timestamp": "2021-03-23T22:21:10.168Z", "updated": "2021-03-23T22:21:10.168Z", "state": "PU", "publish_timestamp": "2021-03-23T22:21:10.167Z"}}, {"model": "playlists.playlist", "pk": 2, "fields": {"parent": null, "category": null, "order": 1, "title": "The Office", "type": "TVS", "description": "", "slug": "the-office", "video": null, "active": true, "timestamp": "2021-03-23T22:21:41.835Z", "updated": "2021-03-23T22:22:12.169Z", "state": "PU", "publish_timestamp": "2021-03-23T22:21:41.834Z"}}, {"model": "playlists.playlist", "pk": 3, "fields": {"parent": 2, "category": null, "order": 1, "title": "Season 1", "type": "SEA", "description": "", "slug": "season-1", "video": null, "active": true, "timestamp": "2021-03-23T22:21:41.839Z", "updated": "2021-03-23T22:22:43.533Z", "state": "PU", "publish_timestamp": "2021-03-23T22:22:12Z"}}, {"model": "playlists.playlist", "pk": 4, "fields": {"parent": 2, "category": null, "order": 2, "title": "Season 2", "type": "SEA", "description": "", "slug": "season-2", "video": null, "active": true, "timestamp": "2021-03-23T22:21:41.840Z", "updated": "2021-03-23T22:22:56.769Z", "state": "PU", "publish_timestamp": "2021-03-23T22:22:12Z"}}, {"model": "playlists.playlist", "pk": 5, "fields": {"parent": null, "category": null, "order": 1, "title": "Parks & Rec", "type": "TVS", "description": "", "slug": "parks-rec", "video": null, "active": true, "timestamp": "2021-03-23T22:22:06.370Z", "updated": "2021-03-23T22:22:06.370Z", "state": "PU", "publish_timestamp": "2021-03-23T22:22:06.369Z"}}, {"model": "playlists.playlist", "pk": 6, "fields": {"parent": 5, "category": null, "order": 1, "title": "Season 1", "type": "SEA", "description": "", "slug": "season-1", "video": null, "active": true, "timestamp": "2021-03-23T22:22:06.373Z", "updated": "2021-03-23T22:23:14.212Z", "state": "PU", "publish_timestamp": "2021-03-23T22:22:06Z"}}, {"model": "playlists.playlist", "pk": 7, "fields": {"parent": 5, "category": null, "order": 2, "title": "Season 2", "type": "SEA", "description": "", "slug": "season-2", "video": null, "active": true, "timestamp": "2021-03-23T22:22:06.374Z", "updated": "2021-03-23T22:23:27.521Z", "state": "PU", "publish_timestamp": "2021-03-23T22:22:06Z"}}, {"model": "playlists.playlistitem", "pk": 1, "fields": {"playlist": 3, "video": 1, "order": 1, "timestamp": "2021-03-23T22:22:43.546Z"}}, {"model": "playlists.playlistitem", "pk": 2, "fields": {"playlist": 3, "video": 2, "order": 2, "timestamp": "2021-03-23T22:22:43.547Z"}}, {"model": "playlists.playlistitem", "pk": 3, "fields": {"playlist": 3, "video": 3, "order": 3, "timestamp": "2021-03-23T22:22:43.547Z"}}, {"model": "playlists.playlistitem", "pk": 4, "fields": {"playlist": 4, "video": 1, "order": 1, "timestamp": "2021-03-23T22:22:56.770Z"}}, {"model": "playlists.playlistitem", "pk": 5, "fields": {"playlist": 4, "video": 2, "order": 2, "timestamp": "2021-03-23T22:22:56.770Z"}}, {"model": "playlists.playlistitem", "pk": 6, "fields": {"playlist": 4, "video": 3, "order": 3, "timestamp": "2021-03-23T22:22:56.770Z"}}, {"model": "playlists.playlistitem", "pk": 7, "fields": {"playlist": 6, "video": 1, "order": 1, "timestamp": "2021-03-23T22:23:14.213Z"}}, {"model": "playlists.playlistitem", "pk": 8, "fields": {"playlist": 6, "video": 2, "order": 2, "timestamp": "2021-03-23T22:23:14.214Z"}}, {"model": "playlists.playlistitem", "pk": 9, "fields": {"playlist": 6, "video": 3, "order": 3, "timestamp": "2021-03-23T22:23:14.214Z"}}, {"model": "playlists.playlistitem", "pk": 10, "fields": {"playlist": 7, "video": 1, "order": 1, "timestamp": "2021-03-23T22:23:27.522Z"}}, {"model": "playlists.playlistitem", "pk": 11, "fields": {"playlist": 7, "video": 2, "order": 2, "timestamp": "2021-03-23T22:23:27.523Z"}}, {"model": "playlists.playlistitem", "pk": 12, "fields": {"playlist": 7, "video": 3, "order": 3, "timestamp": "2021-03-23T22:23:27.523Z"}}, {"model": "auth.user", "pk": 1, "fields": {"password": "pbkdf2_sha256$260000$NIAgO1c0RHHQ6YqAIgsFhf$Y/sHdPgxlBQsBLoF5L1/hu0AK3xXi9Fci53H+/iXUIk=", "last_login": "2021-03-23T22:19:16.520Z", "is_superuser": true, "username": "cfe", "first_name": "", "last_name": "", "email": "", "is_staff": true, "is_active": true, "date_joined": "2021-03-23T22:18:54.511Z", "groups": [], "user_permissions": []}}] -------------------------------------------------------------------------------- /src/djangoflix/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/DjangoFlix/18c2c97c40ee03c317dbdb4baab0d61c60d08271/src/djangoflix/__init__.py -------------------------------------------------------------------------------- /src/djangoflix/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for djangoflix 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/dev/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', 'djangoflix.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /src/djangoflix/db/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/DjangoFlix/18c2c97c40ee03c317dbdb4baab0d61c60d08271/src/djangoflix/db/__init__.py -------------------------------------------------------------------------------- /src/djangoflix/db/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | class PublishStateOptions(models.TextChoices): 4 | # CONSTANT = DB_VALUE, USER_DISPLAY_VA 5 | PUBLISH = 'PU', 'Publish' 6 | DRAFT = 'DR', 'Draft' 7 | # UNLISTED = 'UN', 'Unlisted' 8 | # Private = 'PR', 'Private' 9 | -------------------------------------------------------------------------------- /src/djangoflix/db/receivers.py: -------------------------------------------------------------------------------- 1 | from django.utils import timezone 2 | from django.utils.text import slugify 3 | # Create your models here. 4 | 5 | from .models import PublishStateOptions 6 | from .utils import get_unique_slug 7 | 8 | def publish_state_pre_save(sender, instance, *args, **kwargs): 9 | is_publish = instance.state == PublishStateOptions.PUBLISH 10 | is_draft = instance.state == PublishStateOptions.DRAFT 11 | if is_publish and instance.publish_timestamp is None: 12 | instance.publish_timestamp = timezone.now() 13 | elif is_draft: 14 | instance.publish_timestamp = None 15 | 16 | def slugify_pre_save(sender, instance, *args, **kwargs): 17 | title = instance.title 18 | slug = instance.slug 19 | if slug is None: 20 | instance.slug = slugify(title) 21 | 22 | def unique_slugify_pre_save(sender, instance, *args, **kwargs): 23 | title = instance.title 24 | slug = instance.slug 25 | if slug is None: 26 | instance.slug = get_unique_slug(instance, size=5) -------------------------------------------------------------------------------- /src/djangoflix/db/utils.py: -------------------------------------------------------------------------------- 1 | import random 2 | import string 3 | from django.utils.text import slugify 4 | 5 | 6 | def get_random_string(size=4, chars=string.ascii_lowercase + string.digits): 7 | return "".join([random.choice(chars) for _ in range(size)]) 8 | 9 | 10 | def get_unique_slug(instance, new_slug=None, size=10, max_size=30): 11 | title = instance.title 12 | if new_slug is None: 13 | """ 14 | Default 15 | """ 16 | slug = slugify(title) 17 | else: 18 | """ 19 | Recursive 20 | """ 21 | slug = new_slug 22 | slug = slug[:max_size] 23 | Klass = instance.__class__ # Playlist, Category 24 | parent = None 25 | try: 26 | parent = instance.parent 27 | except: 28 | pass 29 | if parent is not None: 30 | qs = Klass.objects.filter(parent=parent, slug=slug) # smaller 31 | else: 32 | qs = Klass.objects.filter(slug=slug) # larger 33 | if qs.exists(): 34 | new_slug = slugify(title) + get_random_string(size=size) 35 | return get_unique_slug(instance, new_slug=new_slug) 36 | return slug -------------------------------------------------------------------------------- /src/djangoflix/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for djangoflix project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.2b1. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/dev/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/dev/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/dev/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'django-insecure-59yg#hnq&=ypexkbk$dv!h0$j^+s-j_a(5@%&6(_p7-&o-wah3' 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 | "categories", 41 | "playlists", 42 | "ratings", 43 | "tags", 44 | "videos", 45 | ] 46 | 47 | MIDDLEWARE = [ 48 | 'django.middleware.security.SecurityMiddleware', 49 | 'django.contrib.sessions.middleware.SessionMiddleware', 50 | 'django.middleware.common.CommonMiddleware', 51 | 'django.middleware.csrf.CsrfViewMiddleware', 52 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 53 | 'django.contrib.messages.middleware.MessageMiddleware', 54 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 55 | ] 56 | 57 | ROOT_URLCONF = 'djangoflix.urls' 58 | 59 | TEMPLATES = [ 60 | { 61 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 62 | 'DIRS': [BASE_DIR / "templates", os.path.join(BASE_DIR, "templates")], 63 | 'APP_DIRS': True, 64 | 'OPTIONS': { 65 | 'context_processors': [ 66 | 'django.template.context_processors.debug', 67 | 'django.template.context_processors.request', 68 | 'django.contrib.auth.context_processors.auth', 69 | 'django.contrib.messages.context_processors.messages', 70 | ], 71 | }, 72 | }, 73 | ] 74 | 75 | WSGI_APPLICATION = 'djangoflix.wsgi.application' 76 | 77 | 78 | # Database 79 | # https://docs.djangoproject.com/en/dev/ref/settings/#databases 80 | 81 | DATABASES = { 82 | 'default': { 83 | 'ENGINE': 'django.db.backends.sqlite3', 84 | 'NAME': BASE_DIR / 'db.sqlite3', 85 | } 86 | } 87 | 88 | 89 | # Password validation 90 | # https://docs.djangoproject.com/en/dev/ref/settings/#auth-password-validators 91 | 92 | AUTH_PASSWORD_VALIDATORS = [ 93 | { 94 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 95 | }, 96 | { 97 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 98 | }, 99 | { 100 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 101 | }, 102 | { 103 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 104 | }, 105 | ] 106 | 107 | 108 | # Internationalization 109 | # https://docs.djangoproject.com/en/dev/topics/i18n/ 110 | 111 | LANGUAGE_CODE = 'en-us' 112 | 113 | TIME_ZONE = 'UTC' 114 | 115 | USE_I18N = True 116 | 117 | USE_L10N = True 118 | 119 | USE_TZ = True 120 | 121 | 122 | # Static files (CSS, JavaScript, Images) 123 | # https://docs.djangoproject.com/en/dev/howto/static-files/ 124 | 125 | STATIC_URL = '/static/' 126 | 127 | # Default primary key field type 128 | # https://docs.djangoproject.com/en/dev/ref/settings/#default-auto-field 129 | 130 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 131 | -------------------------------------------------------------------------------- /src/djangoflix/urls.py: -------------------------------------------------------------------------------- 1 | """djangoflix URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/dev/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, include 18 | from playlists.views import ( 19 | MovieListView, 20 | MovieDetailView, 21 | PlaylistDetailView, 22 | SearchView, 23 | TVShowListView, 24 | TVShowDetailView, 25 | TVShowSeasonDetailView, 26 | FeaturedPlaylistListView 27 | ) 28 | 29 | from ratings.views import rate_object_view 30 | 31 | ''' 32 | str - everything but / 33 | int - 0 and up 34 | slug -> this-is-a-slug-1 35 | uuid - import uuid; uuid.uuid4() 36 | path -> abc/bac/asdfads/ 37 | 38 | 39 | ''' 40 | 41 | 42 | urlpatterns = [ 43 | path('', FeaturedPlaylistListView.as_view()), 44 | path('admin/', admin.site.urls), 45 | path('category/', include('categories.urls')), 46 | path('categories/', include('categories.urls')), 47 | path('movies//', MovieDetailView.as_view()), 48 | path('movies/', MovieListView.as_view()), 49 | path('media//', PlaylistDetailView.as_view()), 50 | path('search/', SearchView.as_view()), 51 | path('shows//seasons//', TVShowSeasonDetailView.as_view()), 52 | path('shows//seasons/', TVShowDetailView.as_view()), 53 | path('shows//', TVShowDetailView.as_view()), 54 | path('shows/', TVShowListView.as_view()), 55 | path('tags/', include('tags.urls')), 56 | path('object-rate/', rate_object_view) 57 | ] 58 | -------------------------------------------------------------------------------- /src/djangoflix/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for djangoflix 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/dev/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', 'djangoflix.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /src/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', 'djangoflix.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 | -------------------------------------------------------------------------------- /src/playlists/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/DjangoFlix/18c2c97c40ee03c317dbdb4baab0d61c60d08271/src/playlists/__init__.py -------------------------------------------------------------------------------- /src/playlists/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from tags.admin import TaggedItemInline 4 | 5 | from .models import MovieProxy, TVShowProxy, TVShowSeasonProxy, Playlist, PlaylistItem, PlaylistRelated 6 | 7 | 8 | class MovieProxyAdmin(admin.ModelAdmin): 9 | inlines = [TaggedItemInline] 10 | list_display = ['title'] 11 | fields = ['title', 'description', 'state', 'category', 'video', 'slug'] 12 | class Meta: 13 | model = MovieProxy 14 | 15 | def get_queryset(self, request): 16 | return MovieProxy.objects.all() 17 | 18 | admin.site.register(MovieProxy, MovieProxyAdmin) 19 | 20 | 21 | class SeasonEpisodeInline(admin.TabularInline): 22 | model = PlaylistItem 23 | extra = 0 24 | 25 | class TVShowSeasonProxyAdmin(admin.ModelAdmin): 26 | inlines = [TaggedItemInline, SeasonEpisodeInline] 27 | list_display = ['title', 'parent'] 28 | class Meta: 29 | model = TVShowSeasonProxy 30 | 31 | def get_queryset(self, request): 32 | return TVShowSeasonProxy.objects.all() 33 | 34 | admin.site.register(TVShowSeasonProxy, TVShowSeasonProxyAdmin) 35 | 36 | 37 | class TVShowSeasonProxyInline(admin.TabularInline): 38 | model = TVShowSeasonProxy 39 | extra = 0 40 | fields = ['order', 'title', 'state'] 41 | 42 | class TVShowProxyAdmin(admin.ModelAdmin): 43 | inlines = [TaggedItemInline, TVShowSeasonProxyInline] 44 | list_display = ['title'] 45 | fields = ['title', 'description', 'state', 'category', 'video', 'slug'] 46 | class Meta: 47 | model = TVShowProxy 48 | 49 | def get_queryset(self, request): 50 | return TVShowProxy.objects.all() 51 | 52 | admin.site.register(TVShowProxy, TVShowProxyAdmin) 53 | 54 | 55 | class PlaylistRelatedInline(admin.TabularInline): 56 | model = PlaylistRelated 57 | fk_name = 'playlist' 58 | extra = 0 59 | 60 | class PlaylistItemInline(admin.TabularInline): 61 | model = PlaylistItem 62 | extra = 0 63 | 64 | class PlaylistAdmin(admin.ModelAdmin): 65 | inlines = [PlaylistRelatedInline, PlaylistItemInline, TaggedItemInline] 66 | fields = [ 67 | 'title', 68 | 'description', 69 | 'slug', 70 | 'state', 71 | 'active' 72 | ] 73 | class Meta: 74 | model = Playlist 75 | 76 | 77 | def get_queryset(self, request): 78 | return Playlist.objects.filter(type=Playlist.PlaylistTypeChoices.PLAYLIST) 79 | 80 | admin.site.register(Playlist, PlaylistAdmin) -------------------------------------------------------------------------------- /src/playlists/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class PlaylistsConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'playlists' 7 | -------------------------------------------------------------------------------- /src/playlists/fixtures/projects.json: -------------------------------------------------------------------------------- 1 | [{"model": "categories.category", "pk": 1, "fields": {"title": "Comedy", "slug": "comedy", "active": true, "timestamp": "2021-03-24T17:37:48.043Z", "updated": "2021-03-24T17:37:48.043Z"}}, {"model": "categories.category", "pk": 2, "fields": {"title": "Action", "slug": "action", "active": true, "timestamp": "2021-03-24T17:44:23.032Z", "updated": "2021-03-24T17:44:23.032Z"}}, {"model": "categories.category", "pk": 4, "fields": {"title": "Drama", "slug": "drama", "active": true, "timestamp": "2021-03-24T17:47:22.009Z", "updated": "2021-03-24T17:47:22.009Z"}}, {"model": "playlists.playlist", "pk": 1, "fields": {"parent": null, "category": 2, "order": 1, "title": "Avengers", "type": "MOV", "description": "", "slug": "avengers", "video": 1, "active": true, "timestamp": "2021-03-23T22:21:10.168Z", "updated": "2021-03-24T18:01:20.357Z", "state": "PU", "publish_timestamp": "2021-03-23T22:21:10.167Z"}}, {"model": "playlists.playlist", "pk": 2, "fields": {"parent": null, "category": 1, "order": 1, "title": "The Office", "type": "TVS", "description": "", "slug": "the-office", "video": null, "active": true, "timestamp": "2021-03-23T22:21:41.835Z", "updated": "2021-03-24T18:21:18.290Z", "state": "PU", "publish_timestamp": "2021-03-23T22:21:41.834Z"}}, {"model": "playlists.playlist", "pk": 3, "fields": {"parent": 2, "category": null, "order": 1, "title": "Season 1", "type": "SEA", "description": "", "slug": "season-1", "video": null, "active": true, "timestamp": "2021-03-23T22:21:41.839Z", "updated": "2021-03-23T22:22:43.533Z", "state": "PU", "publish_timestamp": "2021-03-23T22:22:12Z"}}, {"model": "playlists.playlist", "pk": 4, "fields": {"parent": 2, "category": null, "order": 2, "title": "Season 2", "type": "SEA", "description": "", "slug": "season-2", "video": null, "active": true, "timestamp": "2021-03-23T22:21:41.840Z", "updated": "2021-03-23T22:22:56.769Z", "state": "PU", "publish_timestamp": "2021-03-23T22:22:12Z"}}, {"model": "playlists.playlist", "pk": 5, "fields": {"parent": null, "category": null, "order": 1, "title": "Parks & Rec", "type": "TVS", "description": "", "slug": "parks-rec", "video": null, "active": true, "timestamp": "2021-03-23T22:22:06.370Z", "updated": "2021-03-24T18:22:58.191Z", "state": "PU", "publish_timestamp": "2021-03-23T22:22:06.369Z"}}, {"model": "playlists.playlist", "pk": 6, "fields": {"parent": 5, "category": null, "order": 1, "title": "Season 1", "type": "SEA", "description": "", "slug": "season-1", "video": null, "active": true, "timestamp": "2021-03-23T22:22:06.373Z", "updated": "2021-03-23T22:23:14.212Z", "state": "PU", "publish_timestamp": "2021-03-23T22:22:06Z"}}, {"model": "playlists.playlist", "pk": 7, "fields": {"parent": 5, "category": null, "order": 2, "title": "Season 2", "type": "SEA", "description": "", "slug": "season-2", "video": null, "active": true, "timestamp": "2021-03-23T22:22:06.374Z", "updated": "2021-03-23T22:23:27.521Z", "state": "PU", "publish_timestamp": "2021-03-23T22:22:06Z"}}, {"model": "playlists.playlist", "pk": 8, "fields": {"parent": null, "category": null, "order": 1, "title": "Featured Playlist 1", "type": "PLY", "description": "", "slug": "featured-playlist-1", "video": null, "active": true, "timestamp": "2021-03-23T22:45:30.400Z", "updated": "2021-03-23T22:50:48.541Z", "state": "PU", "publish_timestamp": "2021-03-23T22:45:30Z"}}, {"model": "playlists.playlist", "pk": 9, "fields": {"parent": null, "category": null, "order": 1, "title": "TV Shows", "type": "PLY", "description": "", "slug": "tv-shows", "video": null, "active": true, "timestamp": "2021-03-23T23:04:17.677Z", "updated": "2021-03-23T23:04:17.677Z", "state": "PU", "publish_timestamp": "2021-03-23T23:04:17.676Z"}}, {"model": "playlists.playlist", "pk": 10, "fields": {"parent": null, "category": 1, "order": 1, "title": "Ant Man", "type": "MOV", "description": "", "slug": "ant-man", "video": null, "active": true, "timestamp": "2021-03-24T17:38:07.296Z", "updated": "2021-03-24T17:59:12.489Z", "state": "PU", "publish_timestamp": "2021-03-24T17:38:07.296Z"}}, {"model": "playlists.playlist", "pk": 11, "fields": {"parent": 5, "category": 1, "order": 1, "title": "Season 4", "type": "SEA", "description": "", "slug": "season-4", "video": null, "active": true, "timestamp": "2021-03-24T17:38:38.400Z", "updated": "2021-03-24T17:38:38.400Z", "state": "PU", "publish_timestamp": "2021-03-24T17:38:38.399Z"}}, {"model": "playlists.playlist", "pk": 12, "fields": {"parent": null, "category": 4, "order": 1, "title": "30 Days of Python", "type": "MOV", "description": "", "slug": "30-days-of-python", "video": null, "active": true, "timestamp": "2021-03-24T21:25:56.094Z", "updated": "2021-03-24T21:25:56.094Z", "state": "PU", "publish_timestamp": "2021-03-24T21:25:56.093Z"}}, {"model": "playlists.playlistitem", "pk": 1, "fields": {"playlist": 3, "video": 1, "order": 1, "timestamp": "2021-03-23T22:22:43.546Z"}}, {"model": "playlists.playlistitem", "pk": 2, "fields": {"playlist": 3, "video": 2, "order": 2, "timestamp": "2021-03-23T22:22:43.547Z"}}, {"model": "playlists.playlistitem", "pk": 3, "fields": {"playlist": 3, "video": 3, "order": 3, "timestamp": "2021-03-23T22:22:43.547Z"}}, {"model": "playlists.playlistitem", "pk": 4, "fields": {"playlist": 4, "video": 1, "order": 1, "timestamp": "2021-03-23T22:22:56.770Z"}}, {"model": "playlists.playlistitem", "pk": 5, "fields": {"playlist": 4, "video": 2, "order": 2, "timestamp": "2021-03-23T22:22:56.770Z"}}, {"model": "playlists.playlistitem", "pk": 6, "fields": {"playlist": 4, "video": 3, "order": 3, "timestamp": "2021-03-23T22:22:56.770Z"}}, {"model": "playlists.playlistitem", "pk": 7, "fields": {"playlist": 6, "video": 1, "order": 1, "timestamp": "2021-03-23T22:23:14.213Z"}}, {"model": "playlists.playlistitem", "pk": 8, "fields": {"playlist": 6, "video": 2, "order": 2, "timestamp": "2021-03-23T22:23:14.214Z"}}, {"model": "playlists.playlistitem", "pk": 9, "fields": {"playlist": 6, "video": 3, "order": 3, "timestamp": "2021-03-23T22:23:14.214Z"}}, {"model": "playlists.playlistitem", "pk": 10, "fields": {"playlist": 7, "video": 1, "order": 1, "timestamp": "2021-03-23T22:23:27.522Z"}}, {"model": "playlists.playlistitem", "pk": 11, "fields": {"playlist": 7, "video": 2, "order": 2, "timestamp": "2021-03-23T22:23:27.523Z"}}, {"model": "playlists.playlistitem", "pk": 12, "fields": {"playlist": 7, "video": 3, "order": 3, "timestamp": "2021-03-23T22:23:27.523Z"}}, {"model": "playlists.playlistrelated", "pk": 1, "fields": {"playlist": 8, "related": 1, "order": 1, "timestamp": "2021-03-23T22:50:48.542Z"}}, {"model": "playlists.playlistrelated", "pk": 2, "fields": {"playlist": 8, "related": 2, "order": 2, "timestamp": "2021-03-23T22:50:48.542Z"}}, {"model": "playlists.playlistrelated", "pk": 3, "fields": {"playlist": 8, "related": 5, "order": 3, "timestamp": "2021-03-23T22:50:48.543Z"}}, {"model": "playlists.playlistrelated", "pk": 4, "fields": {"playlist": 9, "related": 2, "order": 1, "timestamp": "2021-03-23T23:04:17.678Z"}}, {"model": "playlists.playlistrelated", "pk": 5, "fields": {"playlist": 9, "related": 5, "order": 1, "timestamp": "2021-03-23T23:04:17.678Z"}}, {"model": "videos.video", "pk": 1, "fields": {"title": "Avengers Trailer", "description": "", "slug": "avengers-trailer", "video_id": "TcMBFSGVi1c", "active": true, "timestamp": "2021-03-23T22:19:48.708Z", "updated": "2021-03-23T22:19:48.708Z", "state": "PU", "publish_timestamp": "2021-03-23T22:19:48.707Z"}}, {"model": "videos.video", "pk": 2, "fields": {"title": "Parks & Rec Trailer", "description": "", "slug": "parks-rec-trailer", "video_id": "eey-wOyTOJs", "active": true, "timestamp": "2021-03-23T22:20:31.060Z", "updated": "2021-03-23T22:20:31.060Z", "state": "PU", "publish_timestamp": "2021-03-23T22:20:31.060Z"}}, {"model": "videos.video", "pk": 3, "fields": {"title": "The Office Trailer", "description": "", "slug": "the-office-trailer", "video_id": "gO8N3L_aERg", "active": true, "timestamp": "2021-03-23T22:20:56.215Z", "updated": "2021-03-23T22:20:56.215Z", "state": "PU", "publish_timestamp": "2021-03-23T22:20:56.215Z"}}, {"model": "tags.taggeditem", "pk": 1, "fields": {"tag": "comedy", "content_type": 8, "object_id": 10}}, {"model": "tags.taggeditem", "pk": 2, "fields": {"tag": "action", "content_type": 8, "object_id": 10}}, {"model": "tags.taggeditem", "pk": 3, "fields": {"tag": "mcu", "content_type": 8, "object_id": 10}}, {"model": "tags.taggeditem", "pk": 4, "fields": {"tag": "paul-rudd", "content_type": 8, "object_id": 10}}, {"model": "tags.taggeditem", "pk": 5, "fields": {"tag": "action", "content_type": 8, "object_id": 1}}, {"model": "tags.taggeditem", "pk": 6, "fields": {"tag": "sci-fi", "content_type": 8, "object_id": 1}}, {"model": "tags.taggeditem", "pk": 7, "fields": {"tag": "comedy", "content_type": 8, "object_id": 2}}, {"model": "tags.taggeditem", "pk": 8, "fields": {"tag": "sitcom", "content_type": 8, "object_id": 2}}, {"model": "tags.taggeditem", "pk": 9, "fields": {"tag": "paul-rudd", "content_type": 8, "object_id": 5}}] -------------------------------------------------------------------------------- /src/playlists/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2b1 on 2021-03-15 23:14 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Playlist', 16 | fields=[ 17 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('title', models.CharField(max_length=220)), 19 | ('description', models.TextField(blank=True, null=True)), 20 | ('slug', models.SlugField(blank=True, null=True)), 21 | ('active', models.BooleanField(default=True)), 22 | ('timestamp', models.DateTimeField(auto_now_add=True)), 23 | ('updated', models.DateTimeField(auto_now=True)), 24 | ('state', models.CharField(choices=[('PU', 'Publish'), ('DR', 'Draft')], default='DR', max_length=2)), 25 | ('publish_timestamp', models.DateTimeField(blank=True, null=True)), 26 | ], 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /src/playlists/migrations/0002_playlist_video.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2b1 on 2021-03-15 23:16 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('videos', '0012_alter_video_video_id'), 11 | ('playlists', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='playlist', 17 | name='video', 18 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='videos.video'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /src/playlists/migrations/0003_auto_20210315_2357.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2b1 on 2021-03-15 23:57 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('videos', '0012_alter_video_video_id'), 11 | ('playlists', '0002_playlist_video'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='playlist', 17 | name='videos', 18 | field=models.ManyToManyField(blank=True, related_name='playlist_item', to='videos.Video'), 19 | ), 20 | migrations.AlterField( 21 | model_name='playlist', 22 | name='video', 23 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='videos.video'), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /src/playlists/migrations/0004_alter_playlist_video.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2b1 on 2021-03-16 00:01 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('videos', '0012_alter_video_video_id'), 11 | ('playlists', '0003_auto_20210315_2357'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='playlist', 17 | name='video', 18 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='playlist_featured', to='videos.video'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /src/playlists/migrations/0005_remove_playlist_videos.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2b1 on 2021-03-16 23:50 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('playlists', '0004_alter_playlist_video'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='playlist', 15 | name='videos', 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /src/playlists/migrations/0006_auto_20210316_2355.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2b1 on 2021-03-16 23:55 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('videos', '0012_alter_video_video_id'), 11 | ('playlists', '0005_remove_playlist_videos'), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='PlaylistItem', 17 | fields=[ 18 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('order', models.IntegerField(default=1)), 20 | ('timestamp', models.DateTimeField(auto_now_add=True)), 21 | ('playlist', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='playlists.playlist')), 22 | ('video', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='videos.video')), 23 | ], 24 | options={ 25 | 'ordering': ['order', '-timestamp'], 26 | }, 27 | ), 28 | migrations.AddField( 29 | model_name='playlist', 30 | name='videos', 31 | field=models.ManyToManyField(blank=True, related_name='playlist_item', through='playlists.PlaylistItem', to='videos.Video'), 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /src/playlists/migrations/0007_auto_20210317_0037.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2b1 on 2021-03-17 00:37 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('playlists', '0006_auto_20210316_2355'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='playlist', 16 | name='order', 17 | field=models.IntegerField(default=1), 18 | ), 19 | migrations.AddField( 20 | model_name='playlist', 21 | name='parent', 22 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='playlists.playlist'), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /src/playlists/migrations/0008_tvshowproxy_tvshowseasonproxy.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2b1 on 2021-03-17 18:53 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('playlists', '0007_auto_20210317_0037'), 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='TVShowProxy', 15 | fields=[ 16 | ], 17 | options={ 18 | 'verbose_name': 'TV Show', 19 | 'verbose_name_plural': 'TV Shows', 20 | 'proxy': True, 21 | 'indexes': [], 22 | 'constraints': [], 23 | }, 24 | bases=('playlists.playlist',), 25 | ), 26 | migrations.CreateModel( 27 | name='TVShowSeasonProxy', 28 | fields=[ 29 | ], 30 | options={ 31 | 'verbose_name': 'Season', 32 | 'verbose_name_plural': 'Seasons', 33 | 'proxy': True, 34 | 'indexes': [], 35 | 'constraints': [], 36 | }, 37 | bases=('playlists.playlist',), 38 | ), 39 | ] 40 | -------------------------------------------------------------------------------- /src/playlists/migrations/0009_playlist_type.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2b1 on 2021-03-17 19:31 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('playlists', '0008_tvshowproxy_tvshowseasonproxy'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='playlist', 15 | name='type', 16 | field=models.CharField(choices=[('MOV', 'Movie'), ('TVS', 'TV Show'), ('SEA', 'Season'), ('PLY', 'Playlist')], default='PLY', max_length=3), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /src/playlists/migrations/0010_alter_playlist_parent.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2b1 on 2021-03-17 19:33 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('playlists', '0009_playlist_type'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='playlist', 16 | name='parent', 17 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='playlists.playlist'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /src/playlists/migrations/0011_movieproxy.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2b1 on 2021-03-17 21:07 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('playlists', '0010_alter_playlist_parent'), 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='MovieProxy', 15 | fields=[ 16 | ], 17 | options={ 18 | 'verbose_name': 'TV Show', 19 | 'verbose_name_plural': 'TV Shows', 20 | 'proxy': True, 21 | 'indexes': [], 22 | 'constraints': [], 23 | }, 24 | bases=('playlists.playlist',), 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /src/playlists/migrations/0012_alter_movieproxy_options.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2b1 on 2021-03-17 21:08 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('playlists', '0011_movieproxy'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='movieproxy', 15 | options={'verbose_name': 'Movie', 'verbose_name_plural': 'Movies'}, 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /src/playlists/migrations/0013_playlist_category.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2b1 on 2021-03-17 21:29 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('categories', '0001_initial'), 11 | ('playlists', '0012_alter_movieproxy_options'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='playlist', 17 | name='category', 18 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='categories.category'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /src/playlists/migrations/0014_alter_playlist_category.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2b1 on 2021-03-17 21:35 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('categories', '0001_initial'), 11 | ('playlists', '0013_playlist_category'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='playlist', 17 | name='category', 18 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='playlists', to='categories.category'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /src/playlists/migrations/0015_auto_20210323_2242.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2b1 on 2021-03-23 22:42 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('playlists', '0014_alter_playlist_category'), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='PlaylistRelated', 16 | fields=[ 17 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('order', models.IntegerField(default=1)), 19 | ('timestamp', models.DateTimeField(auto_now_add=True)), 20 | ('playlist', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='playlists.playlist')), 21 | ('related', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='related_item', to='playlists.playlist')), 22 | ], 23 | ), 24 | migrations.AddField( 25 | model_name='playlist', 26 | name='related', 27 | field=models.ManyToManyField(blank=True, related_name='_playlists_playlist_related_+', through='playlists.PlaylistRelated', to='playlists.Playlist'), 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /src/playlists/migrations/0016_alter_playlistrelated_related.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2b1 on 2021-03-23 22:46 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | import playlists.models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('playlists', '0015_auto_20210323_2242'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='playlistrelated', 17 | name='related', 18 | field=models.ForeignKey(limit_choices_to=playlists.models.pr_limit_choices_to, on_delete=django.db.models.deletion.CASCADE, related_name='related_item', to='playlists.playlist'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /src/playlists/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/DjangoFlix/18c2c97c40ee03c317dbdb4baab0d61c60d08271/src/playlists/migrations/__init__.py -------------------------------------------------------------------------------- /src/playlists/mixins.py: -------------------------------------------------------------------------------- 1 | class PlaylistMixin(): 2 | template_name = 'playlist_list.html' 3 | title = None 4 | def get_context_data(self, *args, **kwargs): 5 | context = super().get_context_data( *args, **kwargs) 6 | if self.title is not None: 7 | context['title'] = self.title 8 | return context 9 | 10 | def get_queryset(self): 11 | return super().get_queryset().published() -------------------------------------------------------------------------------- /src/playlists/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.contenttypes.fields import GenericRelation 2 | from django.db import models 3 | from django.db.models import Avg, Max, Min, Q 4 | from django.db.models.signals import pre_save 5 | from django.utils import timezone 6 | from django.utils.text import slugify 7 | # Create your models here. 8 | from djangoflix.db.models import PublishStateOptions 9 | from djangoflix.db.receivers import publish_state_pre_save, unique_slugify_pre_save 10 | 11 | from categories.models import Category 12 | from ratings.models import Rating 13 | from tags.models import TaggedItem 14 | from videos.models import Video 15 | 16 | 17 | class PlaylistQuerySet(models.QuerySet): 18 | def published(self): 19 | now = timezone.now() 20 | return self.filter( 21 | state=PublishStateOptions.PUBLISH, 22 | publish_timestamp__lte= now 23 | ) 24 | def search(self, query=None): 25 | if query is None: 26 | return self.none() 27 | return self.filter( 28 | Q(title__icontains=query) | 29 | Q(description__icontains=query) | 30 | Q(category__title__icontains=query) | 31 | Q(category__slug__icontains=query) | 32 | Q(tags__tag__icontains=query) 33 | ).distinct() 34 | 35 | def movie_or_show(self): 36 | return self.filter( 37 | Q(type=Playlist.PlaylistTypeChoices.MOVIE) | 38 | Q(type=Playlist.PlaylistTypeChoices.SHOW) 39 | ) 40 | 41 | class PlaylistManager(models.Manager): 42 | def get_queryset(self): 43 | return PlaylistQuerySet(self.model, using=self._db) 44 | 45 | def published(self): 46 | return self.get_queryset().published() 47 | 48 | def featured_playlists(self): 49 | return self.get_queryset().filter(type=Playlist.PlaylistTypeChoices.PLAYLIST) 50 | 51 | 52 | 53 | class Playlist(models.Model): 54 | class PlaylistTypeChoices(models.TextChoices): 55 | MOVIE = "MOV", "Movie" 56 | SHOW = 'TVS', "TV Show" 57 | SEASON = 'SEA', "Season" 58 | PLAYLIST = 'PLY', "Playlist" 59 | parent = models.ForeignKey("self", blank=True, null=True, on_delete=models.SET_NULL) 60 | related = models.ManyToManyField("self", blank=True, related_name='related', through='PlaylistRelated') 61 | category = models.ForeignKey(Category, related_name='playlists', blank=True, null=True, on_delete=models.SET_NULL) 62 | order = models.IntegerField(default=1) 63 | title = models.CharField(max_length=220) 64 | type = models.CharField(max_length=3, choices=PlaylistTypeChoices.choices, default=PlaylistTypeChoices.PLAYLIST) 65 | description = models.TextField(blank=True, null=True) 66 | slug = models.SlugField(blank=True, null=True) 67 | video = models.ForeignKey(Video, related_name='playlist_featured', blank=True, null=True, on_delete=models.SET_NULL) # one video per playlist 68 | videos = models.ManyToManyField(Video, related_name='playlist_item', blank=True, through='PlaylistItem') 69 | active = models.BooleanField(default=True) 70 | timestamp = models.DateTimeField(auto_now_add=True) 71 | updated = models.DateTimeField(auto_now=True) 72 | state = models.CharField(max_length=2, choices=PublishStateOptions.choices, default=PublishStateOptions.DRAFT) 73 | publish_timestamp = models.DateTimeField(auto_now_add=False, auto_now=False, blank=True, null=True) 74 | tags = GenericRelation(TaggedItem, related_query_name='playlist') 75 | ratings = GenericRelation(Rating, related_query_name='playlist') 76 | objects = PlaylistManager() 77 | 78 | def __str__(self): 79 | return self.title 80 | 81 | def get_related_items(self): 82 | return self.playlistrelated_set.all() 83 | 84 | def get_absolute_url(self): 85 | if self.is_movie: 86 | return f"/movies/{self.slug}/" 87 | if self.is_show: 88 | return f"/shows/{self.slug}/" 89 | if self.is_season and self.parent is not None: 90 | return f"/shows/{self.parent.slug}/seasons/{self.slug}/" 91 | return f"/playlists/{self.slug}/" 92 | 93 | @property 94 | def is_season(self): 95 | return self.type == self.PlaylistTypeChoices.SEASON 96 | 97 | @property 98 | def is_movie(self): 99 | return self.type == self.PlaylistTypeChoices.MOVIE 100 | 101 | @property 102 | def is_show(self): 103 | return self.type == self.PlaylistTypeChoices.SHOW 104 | 105 | def get_rating_avg(self): 106 | return Playlist.objects.filter(id=self.id).aggregate(Avg("ratings__value")) 107 | 108 | def get_rating_spread(self): 109 | return Playlist.objects.filter(id=self.id).aggregate(max=Max("ratings__value"), min=Min("ratings__value")) 110 | 111 | def get_short_display(self): 112 | return "" 113 | 114 | def get_video_id(self): 115 | """ 116 | get main video id to render video for users 117 | """ 118 | if self.video is None: 119 | return None 120 | return self.video.get_video_id() 121 | 122 | def get_clips(self): 123 | """ 124 | get clips to render clips for users 125 | """ 126 | return self.playlistitem_set.all().published() 127 | 128 | @property 129 | def is_published(self): 130 | return self.active 131 | 132 | 133 | 134 | 135 | 136 | 137 | class MovieProxyManager(PlaylistManager): 138 | def all(self): 139 | return self.get_queryset().filter(type=Playlist.PlaylistTypeChoices.MOVIE) 140 | 141 | 142 | class MovieProxy(Playlist): 143 | 144 | objects = MovieProxyManager() 145 | 146 | def get_movie_id(self): 147 | """ 148 | get movie id to render movie for users 149 | """ 150 | return self.get_video_id() 151 | 152 | class Meta: 153 | verbose_name = 'Movie' 154 | verbose_name_plural = 'Movies' 155 | proxy = True 156 | 157 | def save(self, *args, **kwargs): 158 | self.type = Playlist.PlaylistTypeChoices.MOVIE 159 | super().save(*args, **kwargs) 160 | 161 | 162 | 163 | class TVShowProxyManager(PlaylistManager): 164 | def all(self): 165 | return self.get_queryset().filter(parent__isnull=True, type=Playlist.PlaylistTypeChoices.SHOW) 166 | 167 | class TVShowProxy(Playlist): 168 | 169 | objects = TVShowProxyManager() 170 | 171 | class Meta: 172 | verbose_name = 'TV Show' 173 | verbose_name_plural = 'TV Shows' 174 | proxy = True 175 | 176 | def save(self, *args, **kwargs): 177 | self.type = Playlist.PlaylistTypeChoices.SHOW 178 | super().save(*args, **kwargs) 179 | 180 | @property 181 | def seasons(self): 182 | return self.playlist_set.published() 183 | 184 | def get_short_display(self): 185 | return f"{self.seasons.count()} Seasons" 186 | 187 | 188 | 189 | 190 | 191 | class TVShowSeasonProxyManager(PlaylistManager): 192 | def all(self): 193 | return self.get_queryset().filter(parent__isnull=False, type=Playlist.PlaylistTypeChoices.SEASON) 194 | 195 | class TVShowSeasonProxy(Playlist): 196 | 197 | objects = TVShowSeasonProxyManager() 198 | 199 | class Meta: 200 | verbose_name = 'Season' 201 | verbose_name_plural = 'Seasons' 202 | proxy = True 203 | 204 | def save(self, *args, **kwargs): 205 | self.type = Playlist.PlaylistTypeChoices.SEASON 206 | super().save(*args, **kwargs) 207 | 208 | def get_season_trailer(self): 209 | """ 210 | get episodes to render for users 211 | """ 212 | return self.get_video_id() 213 | 214 | def get_episodes(self): 215 | """ 216 | get episodes to render for users 217 | """ 218 | qs = self.playlistitem_set.all().published() 219 | print(qs) 220 | return qs 221 | 222 | 223 | 224 | 225 | class PlaylistItemQuerySet(models.QuerySet): 226 | def published(self): 227 | now = timezone.now() 228 | return self.filter( 229 | playlist__state=PublishStateOptions.PUBLISH, 230 | playlist__publish_timestamp__lte= now, 231 | video__state=PublishStateOptions.PUBLISH, 232 | video__publish_timestamp__lte= now 233 | ) 234 | 235 | class PlaylistItemManager(models.Manager): 236 | def get_queryset(self): 237 | return PlaylistItemQuerySet(self.model, using=self._db) 238 | 239 | def published(self): 240 | return self.get_queryset().published() 241 | 242 | 243 | class PlaylistItem(models.Model): 244 | playlist = models.ForeignKey(Playlist, on_delete=models.CASCADE) 245 | video = models.ForeignKey(Video, on_delete=models.CASCADE) 246 | order = models.IntegerField(default=1) 247 | timestamp = models.DateTimeField(auto_now_add=True) 248 | 249 | objects = PlaylistItemManager() 250 | 251 | class Meta: 252 | ordering = ['order', '-timestamp'] 253 | 254 | 255 | def pr_limit_choices_to(): 256 | return Q(type=Playlist.PlaylistTypeChoices.MOVIE) | Q(type=Playlist.PlaylistTypeChoices.SHOW) 257 | 258 | class PlaylistRelated(models.Model): 259 | playlist = models.ForeignKey(Playlist, on_delete=models.CASCADE) 260 | related = models.ForeignKey(Playlist, on_delete=models.CASCADE, related_name='related_item', limit_choices_to=pr_limit_choices_to) 261 | order = models.IntegerField(default=1) 262 | timestamp = models.DateTimeField(auto_now_add=True) 263 | 264 | 265 | 266 | 267 | 268 | pre_save.connect(publish_state_pre_save, sender=TVShowProxy) 269 | pre_save.connect(unique_slugify_pre_save, sender=TVShowProxy) 270 | 271 | pre_save.connect(publish_state_pre_save, sender=TVShowSeasonProxy) 272 | pre_save.connect(unique_slugify_pre_save, sender=TVShowSeasonProxy) 273 | 274 | pre_save.connect(publish_state_pre_save, sender=MovieProxy) 275 | pre_save.connect(unique_slugify_pre_save, sender=MovieProxy) 276 | 277 | pre_save.connect(publish_state_pre_save, sender=Playlist) 278 | pre_save.connect(unique_slugify_pre_save, sender=Playlist) -------------------------------------------------------------------------------- /src/playlists/reference.md: -------------------------------------------------------------------------------- 1 | ```python 2 | 3 | 4 | class CourseGrade(): 5 | student -> FK(student) 6 | course -> FK(course) 7 | 8 | 9 | class CourseAttendance(): 10 | student -> FK(student) 11 | course -> FK(course) 12 | datetime -> DateTime 13 | 14 | 15 | class Course(): 16 | students -> M2M( ) 17 | # course_obj.coursegrade_set.all() 18 | # course_obj.courseattendence_set.all(0) 19 | 20 | class Parent(): 21 | name 22 | # parent_obj.student_set.all() 23 | 24 | class Student(): 25 | mother = FK(parent, related_name='mother') 26 | father = FK(parent, related_name='father') 27 | # student.course_set.all() 28 | # student.coursegrade_set.all() 29 | # student.father 30 | # student.mother 31 | ``` 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | ``` 49 | playlist_a = Playlist.objects.first() 50 | 51 | ``` 52 | ## Add to ManyToMany 53 | ```python 54 | video_a = Video.objects.first() 55 | playlist_a.videos.add(video_a) 56 | ``` 57 | 58 | ## Remove from ManyToMany 59 | ```python 60 | video_a = Video.objects.first() 61 | playlist_a.videos.remove(video_a) 62 | ``` 63 | 64 | 65 | ## Set (or reset) ManyToMany 66 | ```python 67 | video_qs = Video.objects.all() 68 | playlist_a.videos.set(video_qs) 69 | ``` 70 | 71 | 72 | ## Clear ManyToMany 73 | ```python 74 | playlist_a.videos.clear() 75 | ``` 76 | 77 | ## Queryset from ManyToMany 78 | ```python 79 | playlist_a.videos.all() 80 | ``` 81 | 82 | 83 | 84 | 85 | 86 | ## Playlist of Playlists 87 | 88 | 89 | ```python 90 | 91 | from playlists.models import Playlist 92 | 93 | the_office = Playlist.objects.create(title='The Office Series') 94 | # featured video / videos / 95 | 96 | season_1 = Playlist.objects.create(title='The Office Series Season 1', parent=the_office, order=1) 97 | # featured video / videos / 98 | 99 | season_2 = Playlist.objects.create(title='The Office Series Season 2', parent=the_office, order=2) 100 | # featured video / videos / 101 | 102 | season_3 = Playlist.objects.create(title='The Office Series Season 3', parent=the_office, order=3) 103 | # featured video / videos / 104 | 105 | shows = Playlist.objects.filter(parent__isnull=True) 106 | show = Playlist.objects.get(id=1) 107 | # seasons = Playlist.objects.filter(parent=show) 108 | ``` -------------------------------------------------------------------------------- /src/playlists/shell.md: -------------------------------------------------------------------------------- 1 | ```python 2 | from videos.models import Video 3 | from playlists.models import Playlist 4 | 5 | video_a = Video.objects.create(title='My title', video_id='abc123') 6 | 7 | print(video_a) 8 | 9 | print(dir(video_a)) 10 | 11 | playlist_a = Playlist.objects.create(title='This is my title', video=video_a) 12 | 13 | print(dir(playlist_a)) 14 | 15 | print(playlist_a.video_id) 16 | 17 | print(video_a.id) 18 | ``` 19 | 20 | ```python 21 | playlist_a.video = None 22 | playlist_a.save() 23 | print(playlist_a.video_id) 24 | print(video_a.playlist_set.all()) 25 | ``` 26 | 27 | ```python 28 | playlist_a.video = video_a 29 | playlist_a.save() 30 | print(video_a.playlist_set.all()) 31 | print(playlist_a.id) 32 | ``` 33 | 34 | 35 | ```python 36 | print(video_a.playlist_set.all().published()) 37 | 38 | print(Playlist.objects.all().published()) 39 | 40 | print(Playlist.objects.filter(video=video_a).published()) 41 | ``` -------------------------------------------------------------------------------- /src/playlists/test_movies.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from django.utils import timezone 4 | from django.utils.text import slugify 5 | 6 | from djangoflix.db.models import PublishStateOptions 7 | 8 | from videos.models import Video 9 | from .models import MovieProxy 10 | 11 | class MovieProxyTestCase(TestCase): 12 | def create_videos(self): 13 | video_a = Video.objects.create(title='My title', video_id='abc123') 14 | video_b = Video.objects.create(title='My title', video_id='abc1233') 15 | video_c = Video.objects.create(title='My title', video_id='abc1234') 16 | self.video_a = video_a 17 | self.video_b = video_b 18 | self.video_c = video_c 19 | self.video_qs = Video.objects.all() 20 | 21 | def setUp(self): 22 | self.create_videos() 23 | self.movie_title = 'This is my title' 24 | self.movie_a = MovieProxy.objects.create(title=self.movie_title, video=self.video_a) 25 | self.movie_a_dup = MovieProxy.objects.create(title=self.movie_title, video=self.video_a) 26 | movie_b = MovieProxy.objects.create(title='This is my title', state=PublishStateOptions.PUBLISH, video=self.video_a) 27 | self.published_item_count = 1 28 | movie_b.videos.set(self.video_qs) 29 | movie_b.save() 30 | self.movie_b = movie_b 31 | 32 | def test_movie_video(self): 33 | self.assertEqual(self.movie_a.video, self.video_a) 34 | 35 | def test_movie_clip_items(self): 36 | count = self.movie_b.videos.all().count() 37 | self.assertEqual(count, 3) 38 | 39 | def test_movie_slug_unique(self): 40 | self.assertNotEqual(self.movie_a_dup.slug, self.movie_a.slug) 41 | 42 | def test_slug_field(self): 43 | title = self.movie_title 44 | test_slug = slugify(title) 45 | self.assertEqual(test_slug, self.movie_a.slug) 46 | 47 | def test_valid_title(self): 48 | title= self.movie_title 49 | qs = MovieProxy.objects.filter(title=title) 50 | self.assertTrue(qs.exists()) 51 | 52 | def test_draft_case(self): 53 | qs = MovieProxy.objects.filter(state=PublishStateOptions.DRAFT) 54 | self.assertEqual(qs.count(), 2) 55 | 56 | def test_publish_manager(self): 57 | published_qs = MovieProxy.objects.all().published() 58 | published_qs_2 = MovieProxy.objects.published() 59 | self.assertTrue(published_qs.exists()) 60 | self.assertEqual(published_qs.count(), published_qs_2.count()) 61 | self.assertEqual(published_qs.count(), self.published_item_count) 62 | -------------------------------------------------------------------------------- /src/playlists/test_playlists.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from django.utils import timezone 4 | from django.utils.text import slugify 5 | 6 | from djangoflix.db.models import PublishStateOptions 7 | 8 | from videos.models import Video 9 | from .models import Playlist 10 | 11 | class PlaylistModelTestCase(TestCase): 12 | def create_show_with_seasons(self): 13 | the_office = Playlist.objects.create(title='The Office Series') 14 | season_1 = Playlist.objects.create(title='The Office Series Season 1', parent=the_office, order=1) 15 | season_2 = Playlist.objects.create(title='The Office Series Season 2', parent=the_office, order=2) 16 | season_3 = Playlist.objects.create(title='The Office Series Season 3', parent=the_office, order=3) 17 | self.show = the_office 18 | 19 | def create_videos(self): 20 | video_a = Video.objects.create(title='My title', video_id='abc123') 21 | video_b = Video.objects.create(title='My title', video_id='abc1233') 22 | video_c = Video.objects.create(title='My title', video_id='abc1234') 23 | self.video_a = video_a 24 | self.video_b = video_b 25 | self.video_c = video_c 26 | self.video_qs = Video.objects.all() 27 | 28 | 29 | def setUp(self): 30 | self.create_videos() 31 | self.create_show_with_seasons() 32 | self.obj_a = Playlist.objects.create(title='This is my title', video=self.video_a) 33 | obj_b = Playlist.objects.create(title='This is my title', state=PublishStateOptions.PUBLISH, video=self.video_a) 34 | # obj_b.videos.set([self.video_a, self.video_b, self.video_c]) 35 | obj_b.videos.set(self.video_qs) 36 | obj_b.save() 37 | self.obj_b = obj_b 38 | 39 | def test_show_has_seasons(self): 40 | seasons = self.show.playlist_set.all() 41 | self.assertTrue(seasons.exists()) 42 | self.assertEqual(seasons.count(), 3) 43 | 44 | def test_playlist_video(self): 45 | self.assertEqual(self.obj_a.video, self.video_a) 46 | 47 | def test_playlist_video_items(self): 48 | count = self.obj_b.videos.all().count() 49 | self.assertEqual(count, 3) 50 | 51 | def test_playlist_video_through_model(self): 52 | v_qs = sorted(list(self.video_qs.values_list('id'))) 53 | video_qs = sorted(list(self.obj_b.videos.all().values_list('id'))) 54 | playlist_item_qs = sorted(list(self.obj_b.playlistitem_set.all().values_list('video'))) 55 | self.assertEqual(v_qs, video_qs, playlist_item_qs) 56 | 57 | 58 | def test_video_playlist_ids_propery(self): 59 | ids = self.obj_a.video.get_playlist_ids() 60 | acutal_ids = list(Playlist.objects.filter(video=self.video_a).values_list('id', flat=True)) 61 | self.assertEqual(ids, acutal_ids) 62 | 63 | def test_video_playlist(self): 64 | qs = self.video_a.playlist_featured.all() 65 | self.assertEqual(qs.count(), 2) 66 | 67 | def test_slug_field(self): 68 | title = self.obj_a.title 69 | test_slug = slugify(title) 70 | self.assertEqual(test_slug, self.obj_a.slug) 71 | 72 | def test_valid_title(self): 73 | title='This is my title' 74 | qs = Playlist.objects.filter(title=title) 75 | self.assertTrue(qs.exists()) 76 | 77 | def test_created_count(self): 78 | qs = Playlist.objects.all() 79 | self.assertEqual(qs.count(), 6) 80 | 81 | def test_draft_case(self): 82 | qs = Playlist.objects.filter(state=PublishStateOptions.DRAFT) 83 | self.assertEqual(qs.count(), 5) 84 | 85 | def test_publish_case(self): 86 | qs = Playlist.objects.filter(state=PublishStateOptions.PUBLISH) 87 | now = timezone.now() 88 | published_qs = Playlist.objects.filter( 89 | state=PublishStateOptions.PUBLISH, 90 | publish_timestamp__lte= now 91 | ) 92 | self.assertTrue(published_qs.exists()) 93 | 94 | def test_publish_manager(self): 95 | published_qs = Playlist.objects.all().published() 96 | published_qs_2 = Playlist.objects.published() 97 | self.assertTrue(published_qs.exists()) 98 | self.assertEqual(published_qs.count(), published_qs_2.count()) 99 | -------------------------------------------------------------------------------- /src/playlists/test_tv_shows.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from django.utils import timezone 4 | from django.utils.text import slugify 5 | 6 | from djangoflix.db.models import PublishStateOptions 7 | 8 | from videos.models import Video 9 | from .models import TVShowProxy, TVShowSeasonProxy 10 | 11 | class TVShowProxyModelTestCase(TestCase): 12 | def create_show_with_seasons(self): 13 | the_office = TVShowProxy.objects.create(title='The Office Series') 14 | self.season_1 = TVShowSeasonProxy.objects.create(title='The Office Series Season 1', state=PublishStateOptions.PUBLISH, parent=the_office, order=1) 15 | season_2 = TVShowSeasonProxy.objects.create(title='The Office Series Season 2', parent=the_office, order=2) 16 | season_3 = TVShowSeasonProxy.objects.create(title='The Office Series Season 3', parent=the_office, order=3) 17 | season_4 = TVShowSeasonProxy.objects.create(title='The Office Series Season 4', parent=the_office, order=4) 18 | self.season_11 = TVShowSeasonProxy.objects.create(title='The Office Series Season 1', parent=the_office, order=4) 19 | self.show = the_office 20 | 21 | def create_videos(self): 22 | video_a = Video.objects.create(title='My title', video_id='abc123') 23 | video_b = Video.objects.create(title='My title', video_id='abc1233') 24 | video_c = Video.objects.create(title='My title', video_id='abc1234') 25 | self.video_a = video_a 26 | self.video_b = video_b 27 | self.video_c = video_c 28 | self.video_qs = Video.objects.all() 29 | 30 | 31 | def setUp(self): 32 | self.create_videos() 33 | self.create_show_with_seasons() 34 | self.obj_a = TVShowProxy.objects.create(title='This is my title', video=self.video_a) 35 | obj_b = TVShowProxy.objects.create(title='This is my title', state=PublishStateOptions.PUBLISH, video=self.video_a) 36 | # obj_b.videos.set([self.video_a, self.video_b, self.video_c]) 37 | obj_b.videos.set(self.video_qs) 38 | obj_b.save() 39 | self.obj_b = obj_b 40 | 41 | def test_show_has_seasons(self): 42 | seasons = self.show.playlist_set.all() 43 | self.assertTrue(seasons.exists()) 44 | self.assertEqual(seasons.count(), 5) 45 | 46 | def test_season_slug_unique(self): 47 | self.assertNotEqual(self.season_1.slug, self.season_11.slug) 48 | 49 | def test_playlist_video(self): 50 | self.assertEqual(self.obj_a.video, self.video_a) 51 | 52 | def test_playlist_video_items(self): 53 | count = self.obj_b.videos.all().count() 54 | self.assertEqual(count, 3) 55 | 56 | def test_playlist_video_through_model(self): 57 | v_qs = sorted(list(self.video_qs.values_list('id'))) 58 | video_qs = sorted(list(self.obj_b.videos.all().values_list('id'))) 59 | playlist_item_qs = sorted(list(self.obj_b.playlistitem_set.all().values_list('video'))) 60 | self.assertEqual(v_qs, video_qs, playlist_item_qs) 61 | 62 | 63 | def test_video_playlist_ids_propery(self): 64 | ids = self.obj_a.video.get_playlist_ids() 65 | acutal_ids = list(TVShowProxy.objects.all().filter(video=self.video_a).values_list('id', flat=True)) 66 | self.assertEqual(ids, acutal_ids) 67 | 68 | def test_video_playlist(self): 69 | qs = self.video_a.playlist_featured.all() 70 | self.assertEqual(qs.count(), 2) 71 | 72 | def test_slug_field(self): 73 | title = self.obj_a.title 74 | test_slug = slugify(title) 75 | self.assertEqual(test_slug, self.obj_a.slug) 76 | 77 | def test_valid_title(self): 78 | title='This is my title' 79 | qs = TVShowProxy.objects.all().filter(title=title) 80 | self.assertTrue(qs.exists()) 81 | 82 | def test_tv_shows_created_count(self): 83 | qs = TVShowProxy.objects.all() 84 | self.assertEqual(qs.count(), 3) 85 | 86 | def test_seasons_created_count(self): 87 | qs = TVShowSeasonProxy.objects.all() 88 | self.assertEqual(qs.count(), 5) 89 | 90 | def test_tv_show_draft_case(self): 91 | qs = TVShowProxy.objects.all().filter(state=PublishStateOptions.DRAFT) 92 | self.assertEqual(qs.count(), 2) 93 | 94 | def test_seasons_draft_case(self): 95 | qs = TVShowSeasonProxy.objects.all().filter(state=PublishStateOptions.DRAFT) 96 | self.assertEqual(qs.count(), 4) 97 | 98 | def test_publish_case(self): 99 | qs = TVShowProxy.objects.all().filter(state=PublishStateOptions.PUBLISH) 100 | now = timezone.now() 101 | published_qs = TVShowProxy.objects.all().filter( 102 | state=PublishStateOptions.PUBLISH, 103 | publish_timestamp__lte= now 104 | ) 105 | self.assertTrue(published_qs.exists()) 106 | 107 | def test_publish_manager(self): 108 | published_qs = TVShowProxy.objects.all().published() 109 | published_qs_2 = TVShowProxy.objects.all().published() 110 | self.assertTrue(published_qs.exists()) 111 | self.assertEqual(published_qs.count(), published_qs_2.count()) 112 | -------------------------------------------------------------------------------- /src/playlists/test_views.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.test import TestCase 3 | 4 | from django.utils import timezone 5 | from django.utils.text import slugify 6 | 7 | from djangoflix.db.models import PublishStateOptions 8 | 9 | from videos.models import Video 10 | from .models import Playlist, TVShowProxy, MovieProxy 11 | 12 | class PlaylistViewTestCase(TestCase): 13 | fixtures = ['projects'] 14 | 15 | def test_movie_count(self): 16 | qs = MovieProxy.objects.all() 17 | self.assertEqual(qs.count(), 3) 18 | 19 | def test_shows_count(self): 20 | qs = TVShowProxy.objects.all() 21 | self.assertEqual(qs.count(), 2) 22 | 23 | def test_show_detail_view(self): 24 | show = TVShowProxy.objects.all().published().first() 25 | url = show.get_absolute_url() 26 | self.assertIsNotNone(url) 27 | response = self.client.get(url) 28 | self.assertEqual(response.status_code, 200) # 200 29 | self.assertContains(response, f"{show.title}") 30 | context = response.context 31 | obj = context['object'] 32 | self.assertEqual(obj.id, show.id) 33 | 34 | def test_show_detail_redirect_view(self): 35 | show = TVShowProxy.objects.all().published().first() 36 | url = f"/shows/{show.slug}" 37 | response = self.client.get(url, follow=True) 38 | self.assertEqual(response.status_code, 200) 39 | 40 | def test_show_list_view(self): 41 | shows_qs = TVShowProxy.objects.all().published() 42 | response = self.client.get("/shows/") 43 | self.assertEqual(response.status_code, 200) # 200 44 | context = response.context 45 | r_qs = context['object_list'] 46 | self.assertQuerysetEqual(shows_qs.order_by('-timestamp'), r_qs.order_by("-timestamp")) 47 | 48 | def test_movie_detail_view(self): 49 | movie = MovieProxy.objects.all().published().first() 50 | url = movie.get_absolute_url() 51 | self.assertIsNotNone(url) 52 | response = self.client.get(url) 53 | self.assertEqual(response.status_code, 200) # 200 54 | self.assertContains(response, f"{movie.title}") 55 | context = response.context 56 | obj = context['object'] 57 | self.assertEqual(obj.id, movie.id) 58 | 59 | def test_movie_detail_redirect_view(self): 60 | movie = MovieProxy.objects.all().published().first() 61 | url = f"/movies/{movie.slug}" 62 | response = self.client.get(url, follow=True) 63 | self.assertEqual(response.status_code, 200) # 200 64 | 65 | def test_movie_list_view(self): 66 | movies_qs = MovieProxy.objects.all().published() 67 | response = self.client.get("/movies/") 68 | self.assertEqual(response.status_code, 200) # 200 69 | context = response.context 70 | r_qs = context['object_list'] 71 | self.assertQuerysetEqual(movies_qs.order_by('-timestamp'), r_qs.order_by("-timestamp")) 72 | 73 | def test_search_none_view(self): 74 | query = None 75 | response = self.client.get("/search/") 76 | ply_qs = Playlist.objects.none() 77 | self.assertEqual(response.status_code, 200) # 200 78 | context = response.context 79 | r_qs = context['object_list'] 80 | self.assertQuerysetEqual(ply_qs.order_by('-timestamp'), r_qs.order_by("-timestamp")) 81 | self.assertContains(response, 'Perform a search') 82 | 83 | def test_search_results_view(self): 84 | query = "Action" 85 | response = self.client.get(f"/search/?q={query}") 86 | ply_qs = Playlist.objects.all().search(query=query) 87 | self.assertEqual(response.status_code, 200) # 200 88 | context = response.context 89 | r_qs = context['object_list'] 90 | self.assertQuerysetEqual(ply_qs.order_by('-timestamp'), r_qs.order_by("-timestamp")) 91 | self.assertContains(response, f"Searched for {query}") 92 | 93 | -------------------------------------------------------------------------------- /src/playlists/views.py: -------------------------------------------------------------------------------- 1 | from django.http import Http404 2 | from django.views.generic import ListView, DetailView 3 | from django.utils import timezone 4 | 5 | from djangoflix.db.models import PublishStateOptions 6 | 7 | 8 | from .mixins import PlaylistMixin 9 | from .models import Playlist, MovieProxy, TVShowProxy, TVShowSeasonProxy 10 | 11 | class SearchView(PlaylistMixin, ListView): 12 | def get_context_data(self): 13 | context = super().get_context_data() 14 | query = self.request.GET.get("q") 15 | if query is not None: 16 | context['title'] = f"Searched for {query}" 17 | else: 18 | context['title'] = 'Perform a search' 19 | return context 20 | 21 | def get_queryset(self): 22 | query = self.request.GET.get("q") # request.GET = {} 23 | return Playlist.objects.all().movie_or_show().search(query=query) 24 | 25 | 26 | 27 | class MovieListView(PlaylistMixin, ListView): 28 | queryset = MovieProxy.objects.all() 29 | title = "Movies" 30 | 31 | class MovieDetailView(PlaylistMixin, DetailView): 32 | template_name = 'playlists/movie_detail.html' 33 | queryset = MovieProxy.objects.all() 34 | 35 | class PlaylistDetailView(PlaylistMixin, DetailView): 36 | template_name = 'playlists/playlist_detail.html' 37 | queryset = Playlist.objects.all() 38 | 39 | class TVShowListView(PlaylistMixin, ListView): 40 | queryset = TVShowProxy.objects.all() 41 | title = "TV Shows" 42 | 43 | class TVShowDetailView(PlaylistMixin, DetailView): 44 | template_name = 'playlists/tvshow_detail.html' 45 | queryset = TVShowProxy.objects.all() 46 | 47 | 48 | class TVShowSeasonDetailView(PlaylistMixin, DetailView): 49 | template_name = 'playlists/season_detail.html' 50 | queryset = TVShowSeasonProxy.objects.all() 51 | 52 | def get_object(self): 53 | kwargs = self.kwargs 54 | show_slug = kwargs.get("showSlug") 55 | season_slug = kwargs.get("seasonSlug") 56 | now = timezone.now() 57 | try: 58 | obj = TVShowSeasonProxy.objects.get( 59 | state=PublishStateOptions.PUBLISH, 60 | publish_timestamp__lte=now, 61 | parent__slug__iexact=show_slug, 62 | slug__iexact=season_slug 63 | ) 64 | except TVShowSeasonProxy.MultipleObjectsReturned: 65 | qs = TVShowSeasonProxy.objects.filter( 66 | parent__slug__iexact=show_slug, 67 | slug__iexact=season_slug 68 | ).published() 69 | obj = qs.first() 70 | # log this 71 | except: 72 | raise Http404 73 | return obj 74 | 75 | 76 | # qs = self.get_queryset().filter(parent__slug__iexact=show_slug, slug__iexact=season_slug) 77 | # if not qs.count() == 1: 78 | # raise Http404 79 | # return qs.first() 80 | 81 | class FeaturedPlaylistListView(PlaylistMixin, ListView): 82 | template_name = 'playlists/featured_list.html' 83 | queryset = Playlist.objects.featured_playlists() 84 | title = "Featured" -------------------------------------------------------------------------------- /src/ratings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/DjangoFlix/18c2c97c40ee03c317dbdb4baab0d61c60d08271/src/ratings/__init__.py -------------------------------------------------------------------------------- /src/ratings/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /src/ratings/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class RatingsConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'ratings' 7 | -------------------------------------------------------------------------------- /src/ratings/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | 4 | from .models import RatingChoices 5 | 6 | class RatingForm(forms.Form): 7 | rating = forms.ChoiceField(label='Rate', choices=RatingChoices.choices) 8 | object_id = forms.IntegerField(widget=forms.HiddenInput) 9 | content_type_id = forms.IntegerField(widget=forms.HiddenInput) 10 | next = forms.CharField(widget=forms.HiddenInput) -------------------------------------------------------------------------------- /src/ratings/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2b1 on 2021-03-20 19:44 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ('contenttypes', '0002_remove_content_type_name'), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='Rating', 20 | fields=[ 21 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 22 | ('value', models.IntegerField(blank=True, choices=[(None, 'Unknown'), (1, 'One'), (2, 'Two'), (3, 'Three'), (4, 'Four'), (5, 'Five')], null=True)), 23 | ('object_id', models.PositiveIntegerField()), 24 | ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')), 25 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 26 | ], 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /src/ratings/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/DjangoFlix/18c2c97c40ee03c317dbdb4baab0d61c60d08271/src/ratings/migrations/__init__.py -------------------------------------------------------------------------------- /src/ratings/models.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.contrib.contenttypes.models import ContentType 3 | from django.contrib.contenttypes.fields import GenericForeignKey 4 | from django.db import models 5 | from django.db.models import Avg 6 | from django.db.models.signals import post_save 7 | 8 | User = settings.AUTH_USER_MODEL # "auth.User" 9 | 10 | class RatingChoices(models.IntegerChoices): 11 | ONE = 1 12 | TWO = 2 13 | THREE = 3 14 | FOUR = 4 15 | FIVE = 5 16 | __empty__ = 'Rate this' 17 | 18 | class RatingQuerySet(models.QuerySet): 19 | def rating(self): 20 | return self.aggregate(average=Avg("value"))['average'] 21 | 22 | 23 | class RatingManager(models.Manager): 24 | def get_queryset(self): 25 | return RatingQuerySet(self.model, using=self._db) 26 | 27 | 28 | class Rating(models.Model): 29 | user = models.ForeignKey(User, on_delete=models.CASCADE) 30 | value = models.IntegerField(null=True, blank=True, choices=RatingChoices.choices) 31 | content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) 32 | object_id = models.PositiveIntegerField() 33 | content_object = GenericForeignKey("content_type", "object_id") 34 | 35 | objects = RatingManager() 36 | 37 | 38 | def rating_post_save(sender, instance, created, *args, **kwargs): 39 | if created: 40 | # trigger new content_object calculation 41 | content_type = instance.content_type 42 | user = instance.user 43 | qs = Rating.objects.filter(user=user, content_type=content_type, object_id=instance.object_id).exclude(pk=instance.pk) 44 | if qs.exists(): 45 | qs.delete() 46 | 47 | 48 | post_save.connect(rating_post_save, sender=Rating) -------------------------------------------------------------------------------- /src/ratings/templates/ratings/rating.html: -------------------------------------------------------------------------------- 1 | Rating: {% if value %}{{ value }}{% else %}~{% endif %} 2 | {% if form %} 3 |
{% csrf_token %} 4 | {{ form.as_p }} 5 | 6 |
7 | {% endif %} -------------------------------------------------------------------------------- /src/ratings/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/DjangoFlix/18c2c97c40ee03c317dbdb4baab0d61c60d08271/src/ratings/templatetags/__init__.py -------------------------------------------------------------------------------- /src/ratings/templatetags/rating.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.contrib.contenttypes.models import ContentType 3 | 4 | from ratings.forms import RatingForm 5 | from ratings.models import Rating 6 | 7 | register = template.Library() 8 | 9 | @register.inclusion_tag('ratings/rating.html', takes_context=True) 10 | def rating(context, *args, **kwargs): 11 | ''' 12 | {% load rating %} 13 | {% rating %} 14 | ''' 15 | obj = kwargs.get("object") 16 | rating_only = kwargs.get("rating_only") 17 | request = context['request'] 18 | user = None 19 | if request.user.is_authenticated: 20 | user = request.user 21 | app_label = obj._meta.app_label # playlists 22 | model_name = obj._meta.model_name 23 | if app_label == "playlists": 24 | if model_name == 'movieproxy' or 'tvshowproxy': 25 | model_name = 'playlist' 26 | c_type = ContentType.objects.get(app_label=app_label,model=model_name) 27 | avg_rating = Rating.objects.filter(content_type=c_type, object_id=obj.id).rating() 28 | context = { 29 | 'value': avg_rating, 30 | 'form': None 31 | } 32 | 33 | display_form = False 34 | if user is not None: 35 | display_form = True 36 | if rating_only is True: 37 | display_form = False 38 | if display_form: 39 | context['form'] = RatingForm(initial={ 40 | "object_id": obj.id, 41 | "content_type_id": c_type.id, 42 | "next": request.path, 43 | }) 44 | return context -------------------------------------------------------------------------------- /src/ratings/tests.py: -------------------------------------------------------------------------------- 1 | import random 2 | from django.contrib.auth import get_user_model 3 | from django.db.models import Avg 4 | from django.test import TestCase 5 | 6 | 7 | from playlists.models import Playlist 8 | from .models import Rating, RatingChoices 9 | 10 | User = get_user_model() # User.objects.all() 11 | 12 | class RatingTestCase(TestCase): 13 | def create_playlists(self): 14 | items = [] 15 | self.playlist_count = random.randint(10, 500) 16 | for i in range(0, self.playlist_count): 17 | items.append(Playlist(title=f'Tv show {i}')) 18 | Playlist.objects.bulk_create(items) 19 | self.playlists = Playlist.objects.all() 20 | 21 | def create_users(self): 22 | items = [] 23 | self.user_count = random.randint(10, 500) 24 | for i in range(0, self.user_count): 25 | items.append(User(username=f'user_{i}')) 26 | User.objects.bulk_create(items) 27 | self.users = User.objects.all() 28 | 29 | def create_ratings(self): 30 | items = [] 31 | self.rating_totals = [] 32 | self.rating_count = 1_000 33 | for i in range(0, self.rating_count): 34 | user_obj = self.users.order_by("?").first() 35 | ply_obj = self.playlists.order_by("?").first() 36 | rating_val = random.choice(RatingChoices.choices)[0] 37 | if rating_val is not None: 38 | self.rating_totals.append(rating_val) 39 | items.append( 40 | Rating( 41 | user=user_obj, 42 | content_object=ply_obj, 43 | value=rating_val 44 | ) 45 | ) 46 | Rating.objects.bulk_create(items) 47 | self.ratings = Rating.objects.all() 48 | 49 | def setUp(self): 50 | self.create_users() 51 | self.create_playlists() 52 | self.create_ratings() 53 | 54 | def test_user_count(self): 55 | qs = User.objects.all() 56 | self.assertTrue(qs.exists()) 57 | self.assertEqual(qs.count(), self.user_count) 58 | self.assertEqual(self.users.count(), self.user_count) 59 | 60 | def test_playlist_count(self): 61 | qs = Playlist.objects.all() 62 | self.assertTrue(qs.exists()) 63 | self.assertEqual(qs.count(), self.playlist_count) 64 | self.assertEqual(self.playlists.count(), self.playlist_count) 65 | 66 | def test_rating_count(self): 67 | qs = Rating.objects.all() 68 | self.assertTrue(qs.exists()) 69 | self.assertEqual(qs.count(), self.rating_count) 70 | self.assertEqual(self.ratings.count(), self.rating_count) 71 | 72 | def test_rating_random_choices(self): 73 | value_set = set(Rating.objects.values_list('value', flat=True)) 74 | self.assertTrue(len(value_set) > 1) 75 | 76 | def test_rating_agg(self): 77 | db_avg = Rating.objects.aggregate(average=Avg('value'))['average'] 78 | self.assertIsNotNone(db_avg) 79 | self.assertTrue(db_avg > 0) 80 | total_sum = sum(self.rating_totals) 81 | passed_avg = total_sum / (len(self.rating_totals) * 1.0) 82 | # print(db_avg, passed_avg) 83 | self.assertEqual(passed_avg, db_avg) 84 | 85 | def test_rating_playlist_agg(self): 86 | item_1 = Playlist.objects.aggregate(average=Avg('ratings__value'))['average'] 87 | self.assertIsNotNone(item_1) 88 | self.assertTrue(item_1 > 0) -------------------------------------------------------------------------------- /src/ratings/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.contenttypes.models import ContentType 2 | from django.http import HttpResponseRedirect 3 | from django.shortcuts import render 4 | 5 | from .forms import RatingForm 6 | from .models import Rating 7 | 8 | def rate_object_view(request): 9 | if not request.user.is_authenticated: 10 | return HttpResponseRedirect('/') 11 | if request.method == "POST": 12 | form = RatingForm(request.POST) 13 | if form.is_valid(): 14 | object_id = form.cleaned_data.get('object_id') 15 | rating = form.cleaned_data.get('rating') 16 | content_type_id = form.cleaned_data.get('content_type_id') 17 | c_type = ContentType.objects.get_for_id(content_type_id) 18 | obj = Rating.objects.create( 19 | content_type=c_type, 20 | object_id=object_id, 21 | value=rating, 22 | user=request.user 23 | ) 24 | next_path = form.cleaned_data.get('next') # detail view 25 | return HttpResponseRedirect(next_path) 26 | return HttpResponseRedirect('/') -------------------------------------------------------------------------------- /src/tags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/DjangoFlix/18c2c97c40ee03c317dbdb4baab0d61c60d08271/src/tags/__init__.py -------------------------------------------------------------------------------- /src/tags/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib.contenttypes.admin import GenericTabularInline 2 | from django.contrib import admin 3 | 4 | from .models import TaggedItem 5 | 6 | 7 | class TaggedItemInline(GenericTabularInline): # admin.TabularInline 8 | model = TaggedItem 9 | extra = 0 10 | 11 | 12 | class TaggedItemAdmin(admin.ModelAdmin): 13 | fields = ['tag', 'content_type', 'object_id', 'content_object'] 14 | readonly_fields = ['content_object'] 15 | class Meta: 16 | model = TaggedItem 17 | 18 | 19 | admin.site.register(TaggedItem, TaggedItemAdmin) -------------------------------------------------------------------------------- /src/tags/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class TagsConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'tags' 7 | -------------------------------------------------------------------------------- /src/tags/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2b1 on 2021-03-18 01:14 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ('contenttypes', '0002_remove_content_type_name'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='TaggedItem', 18 | fields=[ 19 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('tag', models.SlugField()), 21 | ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')), 22 | ], 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /src/tags/migrations/0002_taggeditem_object_id.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2b1 on 2021-03-18 01:16 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('tags', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='taggeditem', 15 | name='object_id', 16 | field=models.PositiveIntegerField(default=1), 17 | preserve_default=False, 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /src/tags/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/DjangoFlix/18c2c97c40ee03c317dbdb4baab0d61c60d08271/src/tags/migrations/__init__.py -------------------------------------------------------------------------------- /src/tags/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.contenttypes.models import ContentType 2 | from django.contrib.contenttypes.fields import GenericForeignKey 3 | from django.db import models 4 | from django.db.models.signals import pre_save 5 | 6 | class TaggedItemManager(models.Manager): 7 | def unique_list(self): 8 | tags_set = set(self.get_queryset().values_list('tag', flat=True)) 9 | tags_list = sorted(list(tags_set)) 10 | return tags_list 11 | 12 | 13 | class TaggedItem(models.Model): 14 | tag = models.SlugField() 15 | content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) 16 | object_id = models.PositiveIntegerField() 17 | content_object = GenericForeignKey("content_type", "object_id") 18 | 19 | objects = TaggedItemManager() 20 | 21 | @property 22 | def slug(self): 23 | return self.tag 24 | 25 | # def get_related_object(self): 26 | # Klass = self.content_type.model_class() 27 | # return Klass.objects.get(id=self.object_id) 28 | 29 | 30 | def lowercase_tag_pre_save(sender, instance, *args, **kwargs): 31 | instance.tag = f"{instance.tag}".lower() 32 | 33 | 34 | pre_save.connect(lowercase_tag_pre_save, sender=TaggedItem) -------------------------------------------------------------------------------- /src/tags/reference.md: -------------------------------------------------------------------------------- 1 | ## Ways to import a model 2 | - [Docs](https://docs.djangoproject.com/en/dev/ref/contrib/contenttypes/) 3 | 4 | ### Standard 5 | ```python 6 | from playlists.models import Playlist 7 | ``` 8 | 9 | 10 | ### Via Content Type 11 | ```python 12 | from django.contrib.contenttypes.models import ContentType 13 | playlist_type = ContentType.objects.get(app_label='playlists', model='playlist') 14 | Playlist = playlist_type.model_class() 15 | ``` 16 | 17 | ### Via Apps 18 | ```python 19 | from django.apps import apps 20 | Playlist = apps.get_model(app_label='playlists', model_name='Playlist') 21 | ``` 22 | 23 | 24 | ## Ways to associate a generic foreign key 25 | 26 | ### With an model instance/object 27 | ```python 28 | ply_obj = Playlist.objects.first() 29 | TaggedItem.objects.create(content_object=ply_obj, tag='test-1') 30 | ``` 31 | 32 | ### With an model content type and object id 33 | ```python 34 | content_type = ContentType.objects.get_for_model(Playlist) 35 | TaggedItem.objects.create(content_type=content_type, object_id=1, tag='test-2') 36 | ``` 37 | 38 | 39 | ### Using GenericRelationField 40 | #### 41 | ```python 42 | ply_obj.tags.add(TaggedItem(tag='New tag'), bulk=False) 43 | ``` 44 | or 45 | ```python 46 | ply_obj.tags.create(tag='New tag too') 47 | ``` -------------------------------------------------------------------------------- /src/tags/tests.py: -------------------------------------------------------------------------------- 1 | from django.apps import apps 2 | from django.contrib.contenttypes.models import ContentType 3 | from django.test import TestCase 4 | from django.db.utils import IntegrityError 5 | # Create your tests here. 6 | 7 | from playlists.models import Playlist 8 | 9 | from .models import TaggedItem 10 | 11 | class TaggedItemTestCase(TestCase): 12 | def setUp(self): 13 | ply_title = "New title" 14 | self.ply_obj = Playlist.objects.create(title=ply_title) 15 | self.ply_obj2 = Playlist.objects.create(title=ply_title) 16 | self.ply_title = ply_title 17 | self.ply_obj.tags.add(TaggedItem(tag='new-tag'), bulk=False) 18 | self.ply_obj2.tags.add(TaggedItem(tag='new-tag'), bulk=False) 19 | 20 | def test_content_type_is_not_null(self): 21 | with self.assertRaises(IntegrityError): 22 | TaggedItem.objects.create(tag='my-new-tag') 23 | 24 | def test_create_via_content_type(self): 25 | c_type = ContentType.objects.get(app_label='playlists', model='playlist') 26 | # c_type.model_class() 27 | tag_a = TaggedItem.objects.create(content_type=c_type, object_id=1, tag='new-tag') 28 | self.assertIsNotNone(tag_a.pk) 29 | tag_a = TaggedItem.objects.create(content_type=c_type, object_id=100, tag='new-tag2') 30 | self.assertIsNotNone(tag_a.pk) 31 | 32 | def test_create_via_model_content_type(self): 33 | c_type = ContentType.objects.get_for_model(Playlist) 34 | tag_a = TaggedItem.objects.create(content_type=c_type, object_id=1, tag='new-tag') 35 | self.assertIsNotNone(tag_a.pk) 36 | 37 | def test_create_via_app_loader_content_type(self): 38 | PlaylistKlass = apps.get_model(app_label='playlists', model_name='Playlist') 39 | c_type = ContentType.objects.get_for_model(PlaylistKlass) 40 | tag_a = TaggedItem.objects.create(content_type=c_type, object_id=1, tag='new-tag') 41 | self.assertIsNotNone(tag_a.pk) 42 | 43 | def test_related_field(self): 44 | self.assertEqual(self.ply_obj.tags.count(), 1) 45 | 46 | def test_related_field_create(self): 47 | self.ply_obj.tags.create(tag='another-new-tag') 48 | self.assertEqual(self.ply_obj.tags.count(), 2) 49 | 50 | def test_related_field_query_name(self): 51 | qs = TaggedItem.objects.filter(playlist__title__iexact=self.ply_title) 52 | self.assertEqual(qs.count(), 2) 53 | 54 | def test_related_field_via_content_type(self): 55 | c_type = ContentType.objects.get_for_model(Playlist) 56 | tag_qs = TaggedItem.objects.filter(content_type=c_type, object_id=self.ply_obj.id) 57 | self.assertEqual(tag_qs.count(), 1) 58 | 59 | def test_direct_obj_creation(self): 60 | obj = self.ply_obj 61 | tag = TaggedItem.objects.create(content_object=obj, tag='another1') 62 | self.assertIsNotNone(tag.pk) -------------------------------------------------------------------------------- /src/tags/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from .views import TaggedItemListView, TaggedItemDetailView 4 | 5 | urlpatterns = [ 6 | path("", TaggedItemDetailView.as_view()), 7 | path('', TaggedItemListView.as_view()), 8 | ] -------------------------------------------------------------------------------- /src/tags/views.py: -------------------------------------------------------------------------------- 1 | from django.http import Http404 2 | from django.db.models import Count 3 | from django.views import View 4 | from django.views.generic import ListView, DetailView 5 | from django.shortcuts import render 6 | 7 | from playlists.mixins import PlaylistMixin 8 | from playlists.models import Playlist 9 | 10 | 11 | from .models import TaggedItem 12 | 13 | class TaggedItemListView(View): 14 | def get(self, request): 15 | tag_list = TaggedItem.objects.unique_list() 16 | context = { 17 | 'tag_list': tag_list 18 | } 19 | return render(request, 'tags/tag_list.html', context) 20 | 21 | 22 | 23 | class TaggedItemDetailView(PlaylistMixin, ListView): 24 | """ 25 | Another list view for Playlist 26 | """ 27 | def get_context_data(self): 28 | context = super().get_context_data() 29 | context['title'] = f"{self.kwargs.get('tag')}".title() 30 | return context 31 | 32 | def get_queryset(self): 33 | tag = self.kwargs.get('tag') 34 | return Playlist.objects.filter(tags__tag=tag).movie_or_show() 35 | 36 | -------------------------------------------------------------------------------- /src/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {% block head_title %}DjangoFlix{% endblock %} 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 |
15 | 16 | 17 | {% block content %} 18 | {% endblock content %} 19 | 20 | -------------------------------------------------------------------------------- /src/templates/categories/category_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block head_title %} 4 | Categories / 5 | {{ block.super }} 6 | {% endblock %} 7 | 8 | 9 | 10 | 11 | {% block content %} 12 | 13 |

Category

14 | 15 | 16 | {% for instance in object_list %} 17 | 20 | {% endfor %} 21 | 22 | {% endblock %} -------------------------------------------------------------------------------- /src/templates/playlist_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block head_title %} 4 | {% if title %} 5 | {{ title }} / 6 | {% endif %} 7 | {{ block.super }} 8 | {% endblock %} 9 | 10 | 11 | 12 | 13 | {% block content %} 14 | 15 | {% if title %} 16 |

{{ title }}

17 | {% endif %} 18 | 19 | 20 | {% for instance in object_list %} 21 |
22 |

{{ instance.title }}

23 |

{{ instance.description }}

24 | {% if instance.is_movie %} 25 | {% include 'playlists/cards/movie.html' with movie=instance %} 26 | {% elif instance.is_show %} 27 | {% include 'playlists/cards/show.html' with show=instance %} 28 | {% else %} 29 | {% endif %} 30 | 31 |
32 | {% endfor %} 33 | 34 | 35 | {% endblock %} -------------------------------------------------------------------------------- /src/templates/playlists/cards/movie.html: -------------------------------------------------------------------------------- 1 | {% load rating %} 2 | 8 | -------------------------------------------------------------------------------- /src/templates/playlists/cards/show.html: -------------------------------------------------------------------------------- 1 | {% load rating %} 2 | 8 | -------------------------------------------------------------------------------- /src/templates/playlists/featured_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block head_title %} 4 | {% if title %} 5 | {{ title }} / 6 | {% endif %} 7 | {{ block.super }} 8 | {% endblock %} 9 | 10 | 11 | {% block content %} 12 | 13 | {% for instance in object_list %} 14 |
15 |

{{ instance.title }}

16 |

{{ instance.description }}

17 | {% for item in instance.get_related_items %} 18 | {% if item.related.is_movie %} 19 | {% include 'playlists/cards/movie.html' with movie=item.related %} 20 | {% elif item.related.is_show %} 21 | {% include 'playlists/cards/show.html' with show=item.related %} 22 | {% else %} 23 | {% endif %} 24 | {% endfor %} 25 | 26 |
27 | {% endfor %} 28 | 29 | {% endblock %} -------------------------------------------------------------------------------- /src/templates/playlists/movie_detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load rating %} 3 | {% block head_title %} 4 | {{ object.title }} / 5 | {{ block.super }} 6 | {% endblock %} 7 | 8 | 9 | 10 | 11 | {% block content %} 12 | 13 |

{{ object.title }}

14 | 15 |
16 | {% rating object=object %} 17 |
18 | 19 |
20 | {{ object.slug }} {{ object.get_short_display }} 21 | 22 | {% include 'videos/embed.html' with video_id=object.get_video_id %} 23 | 24 |
25 | {% endblock %} -------------------------------------------------------------------------------- /src/templates/playlists/playlist_detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block head_title %} 4 | {{ object.title }} / 5 | {{ block.super }} 6 | {% endblock %} 7 | 8 | 9 | 10 | 11 | {% block content %} 12 | 13 |

{{ object.title }}

14 | 15 | {{ object.slug }} {{ object.get_short_display }} 16 | 17 | {% endblock %} -------------------------------------------------------------------------------- /src/templates/playlists/season_detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block head_title %} 4 | {{ object.title }} / 5 | {{ block.super }} 6 | {% endblock %} 7 | 8 | 9 | 10 | 11 | {% block content %} 12 | 13 |

{{ object.title }}

14 | 15 | {{ object.slug }} {{ object.get_short_display }} 16 | 17 | 18 | 19 |

Trailer

20 | {% include 'videos/embed.html' with video_id=object.get_video_id %} 21 | 22 |

Episodes

23 | {% for playlist_item in object.get_episodes %} 24 |
Episode {{ playlist_item.order }}
25 | {% include 'videos/embed.html' with video_id=playlist_item.video.get_video_id %} 26 | {% endfor %} 27 | {% endblock %} -------------------------------------------------------------------------------- /src/templates/playlists/tvshow_detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load rating %} 3 | {% block head_title %} 4 | {{ object.title }} / 5 | {{ block.super }} 6 | {% endblock %} 7 | 8 | 9 | 10 | 11 | {% block content %} 12 | 13 |

{{ object.title }}

14 |
15 | {% rating object=object %} 16 |
17 |
18 | {{ object.slug }} {{ object.get_short_display }} 19 | 20 | 21 |

Trailer

22 | {% include 'videos/embed.html' with video_id=object.get_video_id %} 23 | 24 | 25 | 26 | {% for season in object.seasons %} 27 | 28 |
  • {{ forloop.counter }} - {{ season.title }}
  • 29 | {% endfor %} 30 | 31 |
    32 | 33 | {% endblock %} 34 | 35 | -------------------------------------------------------------------------------- /src/templates/tags/tag_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block head_title %} 4 | Tags / 5 | {{ block.super }} 6 | {% endblock %} 7 | 8 | 9 | 10 | 11 | {% block content %} 12 | 13 |

    Tags

    14 | 15 | 16 | {% for tag in tag_list %} 17 |
    18 |

    {{ tag|title }}

    19 |
    20 | {% endfor %} 21 | 22 | {% endblock %} -------------------------------------------------------------------------------- /src/templates/videos/embed.html: -------------------------------------------------------------------------------- 1 | {% if video_id %} 2 | 3 | {% else %} 4 |

    Video is coming soon

    5 | {% endif %} -------------------------------------------------------------------------------- /src/videos/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/DjangoFlix/18c2c97c40ee03c317dbdb4baab0d61c60d08271/src/videos/__init__.py -------------------------------------------------------------------------------- /src/videos/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | from .models import VideoAllProxy, VideoPublishedProxy 5 | 6 | class VideoAllAdmin(admin.ModelAdmin): 7 | list_display = ['title', 'id', 'state', 'video_id', 'is_published', 'get_playlist_ids'] 8 | search_fields = ['title'] 9 | list_filter = ['state', 'active'] 10 | readonly_fields = ['id', 'is_published', 'publish_timestamp', 'get_playlist_ids'] 11 | class Meta: 12 | model = VideoAllProxy 13 | 14 | # def published(self, obj, *args, **kwargs): 15 | # return obj.active 16 | 17 | admin.site.register(VideoAllProxy, VideoAllAdmin) 18 | 19 | 20 | class VideoPublishedProxyAdmin(admin.ModelAdmin): 21 | list_display = ['title', 'video_id'] 22 | search_fields = ['title'] 23 | class Meta: 24 | model = VideoPublishedProxy 25 | 26 | def get_queryset(self, request): 27 | return VideoPublishedProxy.objects.filter(active=True) 28 | 29 | 30 | admin.site.register(VideoPublishedProxy, VideoPublishedProxyAdmin) -------------------------------------------------------------------------------- /src/videos/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class VideosConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'videos' 7 | -------------------------------------------------------------------------------- /src/videos/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2b1 on 2021-03-14 20:28 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Video', 16 | fields=[ 17 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('title', models.CharField(max_length=220)), 19 | ('description', models.TextField(blank=True, null=True)), 20 | ('slug', models.SlugField(blank=True, null=True)), 21 | ('video_id', models.CharField(max_length=220)), 22 | ], 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /src/videos/migrations/0002_rename_title_video_name.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2b1 on 2021-03-14 20:34 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('videos', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RenameField( 14 | model_name='video', 15 | old_name='title', 16 | new_name='name', 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /src/videos/migrations/0003_rename_name_video_title.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2b1 on 2021-03-14 20:35 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('videos', '0002_rename_title_video_name'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RenameField( 14 | model_name='video', 15 | old_name='name', 16 | new_name='title', 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /src/videos/migrations/0004_videoproxy.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2b1 on 2021-03-15 19:12 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('videos', '0003_rename_name_video_title'), 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='VideoProxy', 15 | fields=[ 16 | ], 17 | options={ 18 | 'proxy': True, 19 | 'indexes': [], 20 | 'constraints': [], 21 | }, 22 | bases=('videos.video',), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /src/videos/migrations/0005_alter_videoproxy_options.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2b1 on 2021-03-15 19:13 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('videos', '0004_videoproxy'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='videoproxy', 15 | options={'verbose_name': 'Published Video', 'verbose_name_plural': 'Published Videos'}, 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /src/videos/migrations/0006_video_active.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2b1 on 2021-03-15 19:17 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('videos', '0005_alter_videoproxy_options'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='video', 15 | name='active', 16 | field=models.BooleanField(default=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /src/videos/migrations/0007_videoallproxy.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2b1 on 2021-03-15 19:19 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('videos', '0006_video_active'), 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='VideoAllProxy', 15 | fields=[ 16 | ], 17 | options={ 18 | 'verbose_name': 'All Video', 19 | 'verbose_name_plural': 'All Videos', 20 | 'proxy': True, 21 | 'indexes': [], 22 | 'constraints': [], 23 | }, 24 | bases=('videos.video',), 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /src/videos/migrations/0008_auto_20210315_1921.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2b1 on 2021-03-15 19:21 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('videos', '0007_videoallproxy'), 10 | ] 11 | 12 | operations = [ 13 | migrations.DeleteModel( 14 | name='VideoProxy', 15 | ), 16 | migrations.CreateModel( 17 | name='VideoPublishedProxy', 18 | fields=[ 19 | ], 20 | options={ 21 | 'verbose_name': 'Published Video', 22 | 'verbose_name_plural': 'Published Videos', 23 | 'proxy': True, 24 | 'indexes': [], 25 | 'constraints': [], 26 | }, 27 | bases=('videos.video',), 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /src/videos/migrations/0009_video_state.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2b1 on 2021-03-15 19:59 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('videos', '0008_auto_20210315_1921'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='video', 15 | name='state', 16 | field=models.CharField(choices=[('PU', 'Publish'), ('DR', 'Draft')], default='DR', max_length=2), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /src/videos/migrations/0010_video_publish_timestamp.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2b1 on 2021-03-15 20:04 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('videos', '0009_video_state'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='video', 15 | name='publish_timestamp', 16 | field=models.DateTimeField(blank=True, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /src/videos/migrations/0011_auto_20210315_2015.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2b1 on 2021-03-15 20:15 2 | 3 | from django.db import migrations, models 4 | import django.utils.timezone 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('videos', '0010_video_publish_timestamp'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='video', 16 | name='timestamp', 17 | field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), 18 | preserve_default=False, 19 | ), 20 | migrations.AddField( 21 | model_name='video', 22 | name='updated', 23 | field=models.DateTimeField(auto_now=True), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /src/videos/migrations/0012_alter_video_video_id.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2b1 on 2021-03-15 20:16 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('videos', '0011_auto_20210315_2015'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='video', 15 | name='video_id', 16 | field=models.CharField(max_length=220, unique=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /src/videos/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/DjangoFlix/18c2c97c40ee03c317dbdb4baab0d61c60d08271/src/videos/migrations/__init__.py -------------------------------------------------------------------------------- /src/videos/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.db.models.signals import pre_save 3 | from django.utils import timezone 4 | from django.utils.text import slugify 5 | # Create your models here. 6 | from djangoflix.db.models import PublishStateOptions 7 | from djangoflix.db.receivers import publish_state_pre_save, slugify_pre_save 8 | 9 | 10 | class VideoQuerySet(models.QuerySet): 11 | def published(self): 12 | now = timezone.now() 13 | return self.filter( 14 | state=PublishStateOptions.PUBLISH, 15 | publish_timestamp__lte= now 16 | ) 17 | 18 | class VideoManager(models.Manager): 19 | def get_queryset(self): 20 | return VideoQuerySet(self.model, using=self._db) 21 | 22 | def published(self): 23 | return self.get_queryset().published() 24 | 25 | class Video(models.Model): 26 | title = models.CharField(max_length=220) 27 | description = models.TextField(blank=True, null=True) 28 | slug = models.SlugField(blank=True, null=True) # 'this-is-my-video' 29 | video_id = models.CharField(max_length=220, unique=True) 30 | active = models.BooleanField(default=True) 31 | timestamp = models.DateTimeField(auto_now_add=True) 32 | updated = models.DateTimeField(auto_now=True) 33 | state = models.CharField(max_length=2, choices=PublishStateOptions.choices, default=PublishStateOptions.DRAFT) 34 | publish_timestamp = models.DateTimeField(auto_now_add=False, auto_now=False, blank=True, null=True) 35 | 36 | objects = VideoManager() 37 | 38 | def get_video_id(self): 39 | if not self.is_published: 40 | return None 41 | return self.video_id 42 | 43 | @property 44 | def is_published(self): 45 | if self.active is False: 46 | return False 47 | state = self.state 48 | if state != PublishStateOptions.PUBLISH: 49 | return False 50 | pub_timestamp = self.publish_timestamp 51 | if pub_timestamp is None: 52 | return False 53 | now = timezone.now() 54 | return pub_timestamp <= now 55 | 56 | def get_playlist_ids(self): 57 | # self._set.all() 58 | return list(self.playlist_featured.all().values_list('id', flat=True)) 59 | 60 | class VideoAllProxy(Video): 61 | class Meta: 62 | proxy = True 63 | verbose_name = 'All Video' 64 | verbose_name_plural = 'All Videos' 65 | 66 | 67 | class VideoPublishedProxy(Video): 68 | class Meta: 69 | proxy = True 70 | verbose_name = 'Published Video' 71 | verbose_name_plural = 'Published Videos' 72 | 73 | 74 | pre_save.connect(publish_state_pre_save, sender=Video) 75 | pre_save.connect(slugify_pre_save, sender=Video) 76 | 77 | 78 | pre_save.connect(publish_state_pre_save, sender=VideoAllProxy) 79 | pre_save.connect(slugify_pre_save, sender=VideoAllProxy) 80 | 81 | 82 | pre_save.connect(publish_state_pre_save, sender=VideoPublishedProxy) 83 | pre_save.connect(slugify_pre_save, sender=VideoPublishedProxy) 84 | 85 | -------------------------------------------------------------------------------- /src/videos/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from django.utils import timezone 3 | from django.utils.text import slugify 4 | 5 | from djangoflix.db.models import PublishStateOptions 6 | 7 | from .models import Video 8 | 9 | class VideoModelTestCase(TestCase): 10 | def setUp(self): 11 | self.obj_a = Video.objects.create(title='This is my title', video_id='abc') 12 | self.obj_b = Video.objects.create(title='This is my title', state=PublishStateOptions.PUBLISH, video_id='abasdfdsc') 13 | 14 | def test_slug_field(self): 15 | title = self.obj_a.title 16 | test_slug = slugify(title) 17 | self.assertEqual(test_slug, self.obj_a.slug) 18 | 19 | def test_valid_title(self): 20 | title='This is my title' 21 | qs = Video.objects.filter(title=title) 22 | self.assertTrue(qs.exists()) 23 | 24 | def test_created_count(self): 25 | qs = Video.objects.all() 26 | self.assertEqual(qs.count(), 2) 27 | 28 | def test_draft_case(self): 29 | qs = Video.objects.filter(state=PublishStateOptions.DRAFT) 30 | self.assertEqual(qs.count(), 1) 31 | 32 | def test_draft_case(self): 33 | obj = Video.objects.filter(state=PublishStateOptions.DRAFT).first() 34 | self.assertFalse(obj.is_published) 35 | 36 | def test_publish_case(self): 37 | qs = Video.objects.filter(state=PublishStateOptions.PUBLISH) 38 | now = timezone.now() 39 | published_qs = Video.objects.filter( 40 | state=PublishStateOptions.PUBLISH, 41 | publish_timestamp__lte= now 42 | ) 43 | self.assertTrue(published_qs.exists()) 44 | 45 | def test_publish_case(self): 46 | obj = Video.objects.filter(state=PublishStateOptions.PUBLISH).first() 47 | self.assertTrue(obj.is_published) 48 | 49 | def test_publish_manager(self): 50 | published_qs = Video.objects.all().published() 51 | published_qs_2 = Video.objects.published() 52 | self.assertTrue(published_qs.exists()) 53 | self.assertEqual(published_qs.count(), published_qs_2.count()) 54 | -------------------------------------------------------------------------------- /src/videos/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | --------------------------------------------------------------------------------