First working version
This commit is contained in:
parent
0b7d16d3e0
commit
823470e2f2
160
.gitignore
vendored
Normal file
160
.gitignore
vendored
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
#poetry.lock
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
|
#pdm.lock
|
||||||
|
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||||
|
# in version control.
|
||||||
|
# https://pdm.fming.dev/#use-with-ide
|
||||||
|
.pdm.toml
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
#.idea/
|
0
auth/__init__.py
Normal file
0
auth/__init__.py
Normal file
3
auth/admin.py
Normal file
3
auth/admin.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
6
auth/apps.py
Normal file
6
auth/apps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class AuthConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'auth'
|
11
auth/forms.py
Normal file
11
auth/forms.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
from django import forms
|
||||||
|
|
||||||
|
class LoginForm(forms.Form):
|
||||||
|
|
||||||
|
|
||||||
|
username = forms.CharField(label="Username", max_length=100, required=True, strip=True,
|
||||||
|
widget=forms.TextInput(attrs={"class": "form-control"})
|
||||||
|
)
|
||||||
|
password = forms.CharField(label="Password", max_length=100, required=True,
|
||||||
|
widget=forms.PasswordInput(attrs={"class": "form-control"})
|
||||||
|
)
|
0
auth/migrations/__init__.py
Normal file
0
auth/migrations/__init__.py
Normal file
3
auth/tests.py
Normal file
3
auth/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
8
auth/urls.py
Normal file
8
auth/urls.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
from django.urls import path
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
app_name = "auth"
|
||||||
|
urlpatterns = [
|
||||||
|
path('login', views.LoginView.as_view(), name="login"),
|
||||||
|
path('logout', views.LogoutView.as_view(), name="logout"),
|
||||||
|
]
|
51
auth/views.py
Normal file
51
auth/views.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
from django.shortcuts import render
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.http import HttpResponseRedirect
|
||||||
|
from django.contrib.auth import authenticate, login, logout
|
||||||
|
from django.views import generic
|
||||||
|
|
||||||
|
from .forms import LoginForm
|
||||||
|
|
||||||
|
class LoginView(generic.TemplateView):
|
||||||
|
template_name = "auth/login.html"
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context["form"] = LoginForm()
|
||||||
|
if "next" in self.request.GET:
|
||||||
|
context["next"] = self.request.GET["next"]
|
||||||
|
return context
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
if request.user.is_authenticated:
|
||||||
|
return HttpResponseRedirect(request.GET.get("next","/"))
|
||||||
|
context = self.get_context_data(**kwargs)
|
||||||
|
return self.render_to_response(context)
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
if request.user.is_authenticated:
|
||||||
|
return HttpResponseRedirect(request.POST.get("next","/"))
|
||||||
|
form = LoginForm(request.POST)
|
||||||
|
if not form.is_valid():
|
||||||
|
return HttpResponseRedirect(reverse("auth:login"))
|
||||||
|
user = authenticate(request, username=form.cleaned_data["username"], password=form.cleaned_data["password"])
|
||||||
|
if user is not None:
|
||||||
|
login(request, user)
|
||||||
|
return HttpResponseRedirect(request.GET.get("next", "/"))
|
||||||
|
else:
|
||||||
|
return HttpResponseRedirect(reverse("auth:login"))
|
||||||
|
|
||||||
|
class LogoutView(generic.RedirectView):
|
||||||
|
|
||||||
|
def get_redirect_url(self):
|
||||||
|
return self.request.GET.get("next", "/")
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
logout(request)
|
||||||
|
return super().get(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def logout_view(request):
|
||||||
|
|
||||||
|
logout(request)
|
||||||
|
redirect_to = request.GET.get("redirect_to", "/")
|
||||||
|
return HttpResponseRedirect(redirect_to)
|
@ -1,36 +1,34 @@
|
|||||||
"""
|
"""
|
||||||
Django settings for movieclub project.
|
Django settings for movieclub project.
|
||||||
|
|
||||||
Generated by 'django-admin startproject' using Django 4.1.5.
|
|
||||||
|
|
||||||
For more information on this file, see
|
|
||||||
https://docs.djangoproject.com/en/4.1/topics/settings/
|
|
||||||
|
|
||||||
For the full list of settings and their values, see
|
|
||||||
https://docs.djangoproject.com/en/4.1/ref/settings/
|
|
||||||
"""
|
"""
|
||||||
|
import environ
|
||||||
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
env = environ.Env(
|
||||||
|
DEBUG=(bool, False),
|
||||||
|
LANGUAGE_CODE=(str, "en-us"),
|
||||||
|
TIME_ZONE=(str, "UTC")
|
||||||
|
)
|
||||||
|
|
||||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
environ.Env.read_env(os.path.join(BASE_DIR, '.env'))
|
||||||
# Quick-start development settings - unsuitable for production
|
|
||||||
# See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/
|
|
||||||
|
|
||||||
# SECURITY WARNING: keep the secret key used in production secret!
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
SECRET_KEY = 'django-insecure-dkxk-x5l6s(8tt89-gwq+u5)o-qovem0=@a^00=h=*3r+25%^g'
|
SECRET_KEY = env("SECRET_KEY")
|
||||||
|
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = True
|
DEBUG = env('DEBUG')
|
||||||
|
|
||||||
ALLOWED_HOSTS = []
|
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = env('ALLOWED_HOSTS')
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
|
'watchlist.apps.WatchlistConfig',
|
||||||
|
'rest_framework',
|
||||||
'django.contrib.admin',
|
'django.contrib.admin',
|
||||||
'django.contrib.auth',
|
'django.contrib.auth',
|
||||||
'django.contrib.contenttypes',
|
'django.contrib.contenttypes',
|
||||||
@ -39,6 +37,11 @@ INSTALLED_APPS = [
|
|||||||
'django.contrib.staticfiles',
|
'django.contrib.staticfiles',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
REST_FRAMEWORK = {
|
||||||
|
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
|
||||||
|
'PAGE_SIZE': 100
|
||||||
|
}
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
'django.middleware.security.SecurityMiddleware',
|
'django.middleware.security.SecurityMiddleware',
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
@ -54,7 +57,9 @@ ROOT_URLCONF = 'movieclub.urls'
|
|||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
{
|
{
|
||||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
'DIRS': [],
|
'DIRS': [
|
||||||
|
BASE_DIR / "templates"
|
||||||
|
],
|
||||||
'APP_DIRS': True,
|
'APP_DIRS': True,
|
||||||
'OPTIONS': {
|
'OPTIONS': {
|
||||||
'context_processors': [
|
'context_processors': [
|
||||||
@ -74,10 +79,7 @@ WSGI_APPLICATION = 'movieclub.wsgi.application'
|
|||||||
# https://docs.djangoproject.com/en/4.1/ref/settings/#databases
|
# https://docs.djangoproject.com/en/4.1/ref/settings/#databases
|
||||||
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': {
|
'default': env.db()
|
||||||
'ENGINE': 'django.db.backends.sqlite3',
|
|
||||||
'NAME': BASE_DIR / 'db.sqlite3',
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -105,7 +107,7 @@ AUTH_PASSWORD_VALIDATORS = [
|
|||||||
|
|
||||||
LANGUAGE_CODE = 'en-us'
|
LANGUAGE_CODE = 'en-us'
|
||||||
|
|
||||||
TIME_ZONE = 'UTC'
|
TIME_ZONE = 'Europe/Prague'
|
||||||
|
|
||||||
USE_I18N = True
|
USE_I18N = True
|
||||||
|
|
||||||
@ -115,6 +117,8 @@ USE_TZ = True
|
|||||||
# Static files (CSS, JavaScript, Images)
|
# Static files (CSS, JavaScript, Images)
|
||||||
# https://docs.djangoproject.com/en/4.1/howto/static-files/
|
# https://docs.djangoproject.com/en/4.1/howto/static-files/
|
||||||
|
|
||||||
|
STATICFILES_DIRS = [ BASE_DIR / "static"]
|
||||||
|
STATIC_ROOT = "deploy/static/"
|
||||||
STATIC_URL = 'static/'
|
STATIC_URL = 'static/'
|
||||||
|
|
||||||
# Default primary key field type
|
# Default primary key field type
|
||||||
|
@ -14,8 +14,23 @@ Including another URLconf
|
|||||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||||
"""
|
"""
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import path
|
from django.urls import include, path, reverse
|
||||||
|
from django.views.generic.base import RedirectView
|
||||||
|
from rest_framework import routers, schemas
|
||||||
|
|
||||||
|
import auth.urls
|
||||||
|
import watchlist.urls
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
path('', RedirectView.as_view(url="/watchlist/"), name="home"),
|
||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
|
path('auth/', include(auth.urls)),
|
||||||
|
path('watchlist/', include(watchlist.urls)),
|
||||||
|
# path('api/watchlist/', include(watchlist.urls.api_router.urls)),
|
||||||
|
# path('openapi', schemas.get_schema_view(
|
||||||
|
# title="Movieclub",
|
||||||
|
# description="",
|
||||||
|
# version="0.1.0"
|
||||||
|
# )),
|
||||||
|
# path('api-auth/', include('rest_framework.urls', namespace='rest_framework'))
|
||||||
]
|
]
|
||||||
|
109
poetry.lock
generated
109
poetry.lock
generated
@ -36,6 +36,101 @@ tzdata = {version = "*", markers = "sys_platform == \"win32\""}
|
|||||||
argon2 = ["argon2-cffi (>=19.1.0)"]
|
argon2 = ["argon2-cffi (>=19.1.0)"]
|
||||||
bcrypt = ["bcrypt"]
|
bcrypt = ["bcrypt"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "django-environ"
|
||||||
|
version = "0.9.0"
|
||||||
|
description = "A package that allows you to utilize 12factor inspired environment variables to configure your Django application."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.4,<4"
|
||||||
|
files = [
|
||||||
|
{file = "django-environ-0.9.0.tar.gz", hash = "sha256:bff5381533056328c9ac02f71790bd5bf1cea81b1beeb648f28b81c9e83e0a21"},
|
||||||
|
{file = "django_environ-0.9.0-py2.py3-none-any.whl", hash = "sha256:f21a5ef8cc603da1870bbf9a09b7e5577ab5f6da451b843dbcc721a7bca6b3d9"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
develop = ["coverage[toml] (>=5.0a4)", "furo (>=2021.8.17b43,<2021.9.0)", "pytest (>=4.6.11)", "sphinx (>=3.5.0)", "sphinx-notfound-page"]
|
||||||
|
docs = ["furo (>=2021.8.17b43,<2021.9.0)", "sphinx (>=3.5.0)", "sphinx-notfound-page"]
|
||||||
|
testing = ["coverage[toml] (>=5.0a4)", "pytest (>=4.6.11)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "djangorestframework"
|
||||||
|
version = "3.14.0"
|
||||||
|
description = "Web APIs for Django, made easy."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
files = [
|
||||||
|
{file = "djangorestframework-3.14.0-py3-none-any.whl", hash = "sha256:eb63f58c9f218e1a7d064d17a70751f528ed4e1d35547fdade9aaf4cd103fd08"},
|
||||||
|
{file = "djangorestframework-3.14.0.tar.gz", hash = "sha256:579a333e6256b09489cbe0a067e66abe55c6595d8926be6b99423786334350c8"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
django = ">=3.0"
|
||||||
|
pytz = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytz"
|
||||||
|
version = "2022.7"
|
||||||
|
description = "World timezone definitions, modern and historical"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
files = [
|
||||||
|
{file = "pytz-2022.7-py2.py3-none-any.whl", hash = "sha256:93007def75ae22f7cd991c84e02d434876818661f8df9ad5df9e950ff4e52cfd"},
|
||||||
|
{file = "pytz-2022.7.tar.gz", hash = "sha256:7ccfae7b4b2c067464a6733c6261673fdb8fd1be905460396b97a073e9fa683a"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyyaml"
|
||||||
|
version = "6.0"
|
||||||
|
description = "YAML parser and emitter for Python"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
files = [
|
||||||
|
{file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"},
|
||||||
|
{file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"},
|
||||||
|
{file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"},
|
||||||
|
{file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"},
|
||||||
|
{file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"},
|
||||||
|
{file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"},
|
||||||
|
{file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"},
|
||||||
|
{file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"},
|
||||||
|
{file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"},
|
||||||
|
{file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"},
|
||||||
|
{file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"},
|
||||||
|
{file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"},
|
||||||
|
{file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"},
|
||||||
|
{file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"},
|
||||||
|
{file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"},
|
||||||
|
{file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"},
|
||||||
|
{file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"},
|
||||||
|
{file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"},
|
||||||
|
{file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"},
|
||||||
|
{file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"},
|
||||||
|
{file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"},
|
||||||
|
{file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"},
|
||||||
|
{file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"},
|
||||||
|
{file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"},
|
||||||
|
{file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"},
|
||||||
|
{file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"},
|
||||||
|
{file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"},
|
||||||
|
{file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"},
|
||||||
|
{file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"},
|
||||||
|
{file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"},
|
||||||
|
{file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"},
|
||||||
|
{file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"},
|
||||||
|
{file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"},
|
||||||
|
{file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"},
|
||||||
|
{file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"},
|
||||||
|
{file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"},
|
||||||
|
{file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"},
|
||||||
|
{file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"},
|
||||||
|
{file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"},
|
||||||
|
{file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlparse"
|
name = "sqlparse"
|
||||||
version = "0.4.3"
|
version = "0.4.3"
|
||||||
@ -60,7 +155,19 @@ files = [
|
|||||||
{file = "tzdata-2022.7.tar.gz", hash = "sha256:fe5f866eddd8b96e9fcba978f8e503c909b19ea7efda11e52e39494bad3a7bfa"},
|
{file = "tzdata-2022.7.tar.gz", hash = "sha256:fe5f866eddd8b96e9fcba978f8e503c909b19ea7efda11e52e39494bad3a7bfa"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "uritemplate"
|
||||||
|
version = "4.1.1"
|
||||||
|
description = "Implementation of RFC 6570 URI Templates"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
files = [
|
||||||
|
{file = "uritemplate-4.1.1-py2.py3-none-any.whl", hash = "sha256:830c08b8d99bdd312ea4ead05994a38e8936266f84b9a7878232db50b044e02e"},
|
||||||
|
{file = "uritemplate-4.1.1.tar.gz", hash = "sha256:4346edfc5c3b79f694bccd6d6099a322bbeb628dbf2cd86eea55a456ce5124f0"},
|
||||||
|
]
|
||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.10"
|
python-versions = "^3.10"
|
||||||
content-hash = "8070a9888290f52ebd017534a68014a5724f63a94dfed4c513488df1f2c31d0f"
|
content-hash = "e076467f7d3442399996abc0f612f37a80ba29600fa0d3130e68d525bdf97ef9"
|
||||||
|
@ -8,6 +8,10 @@ readme = "README.md"
|
|||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = "^3.10"
|
python = "^3.10"
|
||||||
django = "^4.1.5"
|
django = "^4.1.5"
|
||||||
|
djangorestframework = "^3.14.0"
|
||||||
|
uritemplate = "^4.1.1"
|
||||||
|
pyyaml = "^6.0"
|
||||||
|
django-environ = "^0.9.0"
|
||||||
|
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
|
0
static/style/main.css
Normal file
0
static/style/main.css
Normal file
5
templates/403.html
Normal file
5
templates/403.html
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<h1>You can't access this resource.</h1>
|
||||||
|
<p>{{exception}}</p>
|
||||||
|
{% endblock %}
|
4
templates/404.html
Normal file
4
templates/404.html
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<h1>Nothing here...</h1>
|
||||||
|
{% endblock %}
|
6
templates/auth/form_template.html
Normal file
6
templates/auth/form_template.html
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{% for field in form %}
|
||||||
|
<div class="fieldWrapper">
|
||||||
|
{{ field.errors }}
|
||||||
|
{{ field.label_tag }} {{ field }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
1
templates/auth/label_tag.html
Normal file
1
templates/auth/label_tag.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
{% if use_tag %}<{{ tag }}{% include 'django/forms/attrs.html' %}>{{ label }}</{{ tag }}>{% else %}{{ label }}{% endif %}
|
18
templates/auth/login.html
Normal file
18
templates/auth/login.html
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="container container-sm">
|
||||||
|
<form action="{% url 'auth:login' %}" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label" for="username">Username:</label>
|
||||||
|
<input class="form-control" type="text" name="username" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label" for="password">Password:</label>
|
||||||
|
<input class="form-control" type="password" name="password" required>
|
||||||
|
</div>
|
||||||
|
<input type="submit" class="btn btn-primary" value="Submit">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
49
templates/base.html
Normal file
49
templates/base.html
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
{% load static %}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset='utf-8'>
|
||||||
|
<meta http-equiv='X-UA-Compatible' content='IE=edge'>
|
||||||
|
<title>{% block title %}Movieclub{% endblock %}</title>
|
||||||
|
<meta name='viewport' content='width=device-width, initial-scale=1'>
|
||||||
|
<link rel='stylesheet' type='text/css' media='screen' href='{% static "style/main.css" %}'>
|
||||||
|
<!-- Bootstrap -->
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{% block body %}
|
||||||
|
{% block menu %}
|
||||||
|
<nav class="navbar navbar-expand-lg bg-light">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<a class="navbar-brand" href="/">Movieclub</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbar-content">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="navbar-content">
|
||||||
|
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{% url 'watchlist:index' %}">Watchlist</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" style="text-decoration: line-through" href="#">Reviews</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item end-0">
|
||||||
|
{% if request.user.is_authenticated %}
|
||||||
|
<a class="nav-link" href="{% url 'auth:logout' %}">Logout</a>
|
||||||
|
{% else %}
|
||||||
|
<a class="nav-link" href="{% url 'auth:login' %}">Login</a>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<p>Welcome to out little movieclub.</p>
|
||||||
|
{% endblock %}
|
||||||
|
<!-- Bootstrap -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4" crossorigin="anonymous"></script>
|
||||||
|
{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
29
templates/watchlist/edit.html
Normal file
29
templates/watchlist/edit.html
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-lg">
|
||||||
|
<form action="{% url 'watchlist:delete' movie.id %}" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="submit" value="Delete movie" class="btn btn-danger">
|
||||||
|
</form>
|
||||||
|
<form action="{% url 'watchlist:edit' movie.id %}" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div>
|
||||||
|
<label class="form-label" for="name">Movie name</label>
|
||||||
|
<input class="form-control" id="name" name="name" value="{{movie.name}}">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="form-label" for="suggested_by" name="suggested_by">Suggested by</label>
|
||||||
|
<select class="form-select" id="suggested_by" name="suggested_by">
|
||||||
|
{% if request.user.is_staff %}
|
||||||
|
{% for user in users %}
|
||||||
|
<option value="{{ user.username }}" {% if user.username == movie.suggested_by.username %} selected {% endif %} >{{user.username}}</option>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<option value="{{ movie.suggested_by.username }} disabled">{{movie.suggested_by.username}}</option>
|
||||||
|
{% endif %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<input type="submit" value="Submit" class="btn btn-primary">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
20
templates/watchlist/index.html
Normal file
20
templates/watchlist/index.html
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-lg">
|
||||||
|
<h1>Watchlist</h1>
|
||||||
|
<ul>
|
||||||
|
{% for movie in object_list %}
|
||||||
|
<li><a href="{% url 'watchlist:detail' movie.id %}">{{movie.name}}</a> — {{movie.score}}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% if can_add_movie %}
|
||||||
|
<h2>Submit new movie</h2>
|
||||||
|
<form action="{% url 'watchlist:submit' %}" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<label class="form-label" for="name">Movie name</label>
|
||||||
|
<input class="form-control" type="text" id="name" name="name" required>
|
||||||
|
<input class="btn btn-primary" type="submit" value="Sumbit">
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
39
templates/watchlist/movie_detail.html
Normal file
39
templates/watchlist/movie_detail.html
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-lg">
|
||||||
|
<h1 class="display-1">{{ movie.name }}</h1>
|
||||||
|
<p class="text-secondary">Suggested by: {{movie.suggested_by.username}}</p>
|
||||||
|
{% if request.user.is_staff or movie.suggested_by == request.user %}<div class="mb-2"><a class="btn btn-sm btn-danger" href="{% url 'watchlist:edit' movie.id %}">Edit</a></div>{% endif %}
|
||||||
|
<h2>Votes</h2>
|
||||||
|
{% if votes|length == 0 %}
|
||||||
|
<p>Nobody voted yet, be first...</p>
|
||||||
|
{% else %}
|
||||||
|
<p>Total score: {{ movie.score }}, seen by: {{ movie.seen_score }}.
|
||||||
|
<ul>
|
||||||
|
{% for vote in votes %}
|
||||||
|
<li>{{vote.user.username}} {% if vote.seen %}(seen){% endif %} – {% if vote.vote == 1 %}👍{% elif vote.vote == 0 %}No opinion{% elif vote.vote == -1 %}👎{%endif%}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
{% if request.user.is_authenticated %}
|
||||||
|
<h2>Your opinion</h2>
|
||||||
|
<form action="{% url 'watchlist:vote' movie.id %}" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" name="vote" id="vote-upvote" value="1" {% if user_vote and user_vote.vote == 1 %} checked {% endif %}><label class="form-check-label" for="vote-upvote">Want to watch</label><br>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" name="vote" id="vote-novote" value="0" {% if user_vote is None or user_vote.vote == 0 %} checked {% endif %}><label class="form-check-label" for="vote-novote">No opinion</label><br>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" name="vote" id="vote-downvote" value="-1" {% if user_vote and user_vote.vote == -1 %} checked {% endif %}><label class="form-check-label" for="vote-downvote">Don't want to watch</label><br>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" id="seen" name="seen" {% if user_vote and user_vote.seen %} checked {%endif%}><label class="form-check-label" for="seen">I saw this movie already</label><br>
|
||||||
|
</div>
|
||||||
|
<input class="btn btn-primary" type="submit" value="Submit">
|
||||||
|
</form>
|
||||||
|
</h2>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
0
watchlist/__init__.py
Normal file
0
watchlist/__init__.py
Normal file
20
watchlist/admin.py
Normal file
20
watchlist/admin.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from . import models
|
||||||
|
|
||||||
|
class MovieVoteInline(admin.StackedInline):
|
||||||
|
model = models.MovieVote()
|
||||||
|
|
||||||
|
class MovieAdmin(admin.ModelAdmin):
|
||||||
|
fields = [
|
||||||
|
"name", "watched", "suggested_by", "score"
|
||||||
|
]
|
||||||
|
readonly_fields = ("score",)
|
||||||
|
list_display = ["name", "watched", "suggested_by", "score"]
|
||||||
|
|
||||||
|
@admin.display(description="Score")
|
||||||
|
def score(self, instance):
|
||||||
|
return instance.score
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(models.Movie, MovieAdmin)
|
6
watchlist/apps.py
Normal file
6
watchlist/apps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class WatchlistConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'watchlist'
|
9
watchlist/forms.py
Normal file
9
watchlist/forms.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
from django import forms
|
||||||
|
|
||||||
|
from . import models
|
||||||
|
|
||||||
|
class MovieEditForm(forms.ModelForm):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.Movie
|
||||||
|
fields = ["name", "suggested_by", "watched"]
|
34
watchlist/migrations/0001_initial.py
Normal file
34
watchlist/migrations/0001_initial.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# Generated by Django 4.1.5 on 2023-01-12 21:45
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Movie',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=100)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='MovieVote',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('vote', models.IntegerField(choices=[(1, 'Upvote'), (0, 'Novote'), (-1, 'Downvote')], default=0)),
|
||||||
|
('seen', models.BooleanField(default=False, null=True)),
|
||||||
|
('movie', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='watchlist.movie')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,26 @@
|
|||||||
|
# Generated by Django 4.1.5 on 2023-01-12 21:57
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('watchlist', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='movie',
|
||||||
|
name='suggested_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='movie',
|
||||||
|
name='watched',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,25 @@
|
|||||||
|
# Generated by Django 4.1.5 on 2023-01-15 15:57
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('watchlist', '0002_movie_suggested_by_movie_watched'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='movie',
|
||||||
|
options={'permissions': [('change_suggested_by', 'Can change who suggested movie'), ('change_watched', 'Can mark as watched')]},
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='movie',
|
||||||
|
name='suggested_by',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
]
|
17
watchlist/migrations/0004_alter_movie_options.py
Normal file
17
watchlist/migrations/0004_alter_movie_options.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 4.1.5 on 2023-01-15 16:10
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('watchlist', '0003_alter_movie_options_alter_movie_suggested_by'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='movie',
|
||||||
|
options={'permissions': [('moderate_movies', "Can edit other's movies and mark them as watched")]},
|
||||||
|
),
|
||||||
|
]
|
0
watchlist/migrations/__init__.py
Normal file
0
watchlist/migrations/__init__.py
Normal file
42
watchlist/models.py
Normal file
42
watchlist/models.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
from functools import reduce
|
||||||
|
from django.db import models
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
class Movie(models.Model):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
permissions = [
|
||||||
|
("moderate_movies", "Can edit other's movies and mark them as watched"),
|
||||||
|
]
|
||||||
|
|
||||||
|
name = models.CharField(max_length=100)
|
||||||
|
suggested_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
|
||||||
|
watched = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def score(self):
|
||||||
|
return reduce(lambda result,v: result+v.vote, self.movievote_set.all(), 0)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def seen_score(self):
|
||||||
|
return reduce(lambda result,v: result+int(v.seen), self.movievote_set.all(), 0)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
class MovieVote(models.Model):
|
||||||
|
|
||||||
|
class Vote(models.IntegerChoices):
|
||||||
|
UPVOTE = 1
|
||||||
|
NOVOTE = 0
|
||||||
|
DOWNVOTE = -1
|
||||||
|
|
||||||
|
movie = models.ForeignKey(Movie, on_delete=models.CASCADE)
|
||||||
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
|
vote = models.IntegerField(choices=Vote.choices, default=Vote.NOVOTE)
|
||||||
|
seen = models.BooleanField(default=False, null=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.user.username}'s vote for {self.movie.name}"
|
||||||
|
|
18
watchlist/serializers.py
Normal file
18
watchlist/serializers.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class MovieSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
|
||||||
|
suggested_by = serializers.ReadOnlyField(source="suggested_by.username")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
|
||||||
|
model = models.Movie
|
||||||
|
fields = ["url", "name", "watched", "suggested_by", "score"]
|
||||||
|
|
||||||
|
# class VoteSerializer(serializers.Serializer):
|
||||||
|
|
||||||
|
|
3
watchlist/tests.py
Normal file
3
watchlist/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
16
watchlist/urls.py
Normal file
16
watchlist/urls.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
from django.urls import path
|
||||||
|
from rest_framework import routers
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
api_router = routers.DefaultRouter()
|
||||||
|
api_router.register(r'movie', views.MovieViewSet)
|
||||||
|
|
||||||
|
app_name = "watchlist"
|
||||||
|
urlpatterns = [
|
||||||
|
path('', views.IndexView.as_view(), name="index"),
|
||||||
|
path('movie/<int:pk>', views.DetailView.as_view(), name="detail"),
|
||||||
|
path('movie/<int:pk>/vote', views.vote, name="vote"),
|
||||||
|
path('movie/<int:pk>/edit', views.EditView.as_view(), name="edit"),
|
||||||
|
path('movie/<int:pk>/delete', views.delete, name="delete"),
|
||||||
|
path('movie', views.submit, name="submit")
|
||||||
|
]
|
119
watchlist/views.py
Normal file
119
watchlist/views.py
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
from django.http import HttpResponseRedirect, HttpResponseBadRequest, HttpResponseForbidden
|
||||||
|
from django.views import generic
|
||||||
|
from django.views.decorators.http import require_http_methods, require_safe, require_POST
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.shortcuts import get_object_or_404, render
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from rest_framework import viewsets, permissions
|
||||||
|
from rest_framework.decorators import action
|
||||||
|
|
||||||
|
from . import serializers, models
|
||||||
|
|
||||||
|
class MovieViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = models.Movie.objects.order_by('id').all()
|
||||||
|
serializer_class = serializers.MovieSerializer
|
||||||
|
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
|
||||||
|
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
serializer.save(owner=self.request.user)
|
||||||
|
|
||||||
|
# @action(detail=True, methods=["POST"])
|
||||||
|
# def vote(self, request, pk=None):
|
||||||
|
# movie = self.get_object()
|
||||||
|
# vote = request.date.get("vote", 0)
|
||||||
|
|
||||||
|
|
||||||
|
class IndexView(generic.ListView):
|
||||||
|
template_name = "watchlist/index.html"
|
||||||
|
model = models.Movie
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return models.Movie.objects.order_by('id').all()
|
||||||
|
|
||||||
|
def get_context_data(self):
|
||||||
|
context = super().get_context_data()
|
||||||
|
context['can_add_movie'] = self.request.user.has_perm("watchlist.add_movie")
|
||||||
|
return context
|
||||||
|
|
||||||
|
class DetailView(generic.DetailView):
|
||||||
|
model = models.Movie
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
votes = self.object.movievote_set.all()
|
||||||
|
user_vote = None
|
||||||
|
if self.request.user.is_authenticated:
|
||||||
|
user_vote = votes.filter(user=self.request.user).first()
|
||||||
|
context["votes"] = votes
|
||||||
|
context["user_vote"] = user_vote
|
||||||
|
return context
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@require_POST
|
||||||
|
def vote(request, pk):
|
||||||
|
movie = get_object_or_404(models.Movie, pk=pk)
|
||||||
|
user_vote = movie.movievote_set.filter(user=request.user).first()
|
||||||
|
if user_vote is None:
|
||||||
|
user_vote = models.MovieVote(movie=movie, user=request.user)
|
||||||
|
user_vote.vote = request.POST['vote']
|
||||||
|
user_vote.seen = request.POST.get('seen', False) == "on"
|
||||||
|
user_vote.save()
|
||||||
|
return HttpResponseRedirect(reverse('watchlist:detail', args=(pk,)))
|
||||||
|
|
||||||
|
class EditView(generic.DetailView):
|
||||||
|
model = models.Movie
|
||||||
|
template_name = "watchlist/edit.html"
|
||||||
|
|
||||||
|
def can_edit_movie(self, request):
|
||||||
|
return request.user.has_perm('watchlist.moderate_movies') or request.user == self.object.suggested_by
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context["users"] = User.objects.all() if self.request.user.has_perm("watchlist.moderate_movies") else None
|
||||||
|
return context
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
self.object = self.get_object()
|
||||||
|
if not self.can_edit_movie(request):
|
||||||
|
return HttpResponseForbidden("You cannot edit this object.")
|
||||||
|
return super().get(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
self.object = self.get_object()
|
||||||
|
if not self.can_edit_movie(request):
|
||||||
|
return HttpResponseForbidden("You cannot edit this object.")
|
||||||
|
if "name" in request.POST:
|
||||||
|
self.object.name = request.POST["name"]
|
||||||
|
if "suggested_by" in request.POST:
|
||||||
|
if request.user.has_perm('watchlist.moderate_movies'):
|
||||||
|
new_suggestor = User.objects.filter(username=request.POST["suggested_by"]).first()
|
||||||
|
if new_suggestor is None:
|
||||||
|
return HttpResponseBadRequest("The new suggestor doesn't exist.")
|
||||||
|
self.object.suggested_by = new_suggestor
|
||||||
|
# else:
|
||||||
|
# if request.POST["suggested_by"] != self.object.suggested_by.username:
|
||||||
|
# return HttpResponseForbidden("You cannot change the suggested by field.")
|
||||||
|
self.object.save()
|
||||||
|
return HttpResponseRedirect(reverse('watchlist:detail', args=(kwargs["pk"],)))
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@require_POST
|
||||||
|
def submit(request):
|
||||||
|
if not request.user.has_perm("watchlist.add_movie"):
|
||||||
|
return HttpResponseForbidden("You can't add new movies.")
|
||||||
|
movie = models.Movie(name=request.POST["name"], suggested_by=request.user, watched=False)
|
||||||
|
movie.save()
|
||||||
|
return HttpResponseRedirect(reverse("watchlist:index"))
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@require_POST
|
||||||
|
def delete(request, pk):
|
||||||
|
movie = get_object_or_404(models.Movie, pk=pk)
|
||||||
|
if not (request.user.has_perm("watchlist.moderate_movies") or (
|
||||||
|
request.user.has_perm("watchlist.delete_movie") and request.user == movie.suggested_by
|
||||||
|
)):
|
||||||
|
return HttpResponseForbidden("You can't delete this movie")
|
||||||
|
movie.delete()
|
||||||
|
return HttpResponseRedirect(reverse("watchlist:index"))
|
Loading…
Reference in New Issue
Block a user