diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..68bc17f --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/auth/__init__.py b/auth/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/auth/admin.py b/auth/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/auth/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/auth/apps.py b/auth/apps.py new file mode 100644 index 0000000..836fe02 --- /dev/null +++ b/auth/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class AuthConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'auth' diff --git a/auth/forms.py b/auth/forms.py new file mode 100644 index 0000000..93c29f8 --- /dev/null +++ b/auth/forms.py @@ -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"}) + ) \ No newline at end of file diff --git a/auth/migrations/__init__.py b/auth/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/auth/tests.py b/auth/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/auth/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/auth/urls.py b/auth/urls.py new file mode 100644 index 0000000..48f2e2d --- /dev/null +++ b/auth/urls.py @@ -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"), +] \ No newline at end of file diff --git a/auth/views.py b/auth/views.py new file mode 100644 index 0000000..a91d5a0 --- /dev/null +++ b/auth/views.py @@ -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) \ No newline at end of file diff --git a/movieclub/settings.py b/movieclub/settings.py index b999bc7..f9e17c5 100644 --- a/movieclub/settings.py +++ b/movieclub/settings.py @@ -1,36 +1,34 @@ """ 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 +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'. BASE_DIR = Path(__file__).resolve().parent.parent - -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/ +environ.Env.read_env(os.path.join(BASE_DIR, '.env')) # 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! -DEBUG = True - -ALLOWED_HOSTS = [] +DEBUG = env('DEBUG') +ALLOWED_HOSTS = env('ALLOWED_HOSTS') # Application definition INSTALLED_APPS = [ + 'watchlist.apps.WatchlistConfig', + 'rest_framework', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', @@ -39,6 +37,11 @@ INSTALLED_APPS = [ 'django.contrib.staticfiles', ] +REST_FRAMEWORK = { + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', + 'PAGE_SIZE': 100 +} + MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', @@ -54,7 +57,9 @@ ROOT_URLCONF = 'movieclub.urls' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], + 'DIRS': [ + BASE_DIR / "templates" + ], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ @@ -74,10 +79,7 @@ WSGI_APPLICATION = 'movieclub.wsgi.application' # https://docs.djangoproject.com/en/4.1/ref/settings/#databases DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': BASE_DIR / 'db.sqlite3', - } + 'default': env.db() } @@ -105,7 +107,7 @@ AUTH_PASSWORD_VALIDATORS = [ LANGUAGE_CODE = 'en-us' -TIME_ZONE = 'UTC' +TIME_ZONE = 'Europe/Prague' USE_I18N = True @@ -115,6 +117,8 @@ USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/4.1/howto/static-files/ +STATICFILES_DIRS = [ BASE_DIR / "static"] +STATIC_ROOT = "deploy/static/" STATIC_URL = 'static/' # Default primary key field type diff --git a/movieclub/urls.py b/movieclub/urls.py index 7bc79f4..c8d845a 100644 --- a/movieclub/urls.py +++ b/movieclub/urls.py @@ -14,8 +14,23 @@ Including another URLconf 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ 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 = [ + path('', RedirectView.as_view(url="/watchlist/"), name="home"), 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')) ] diff --git a/poetry.lock b/poetry.lock index be6212c..3d41469 100644 --- a/poetry.lock +++ b/poetry.lock @@ -36,6 +36,101 @@ tzdata = {version = "*", markers = "sys_platform == \"win32\""} argon2 = ["argon2-cffi (>=19.1.0)"] 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]] name = "sqlparse" version = "0.4.3" @@ -60,7 +155,19 @@ files = [ {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] lock-version = "2.0" python-versions = "^3.10" -content-hash = "8070a9888290f52ebd017534a68014a5724f63a94dfed4c513488df1f2c31d0f" +content-hash = "e076467f7d3442399996abc0f612f37a80ba29600fa0d3130e68d525bdf97ef9" diff --git a/pyproject.toml b/pyproject.toml index b957e7c..73e2701 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,6 +8,10 @@ readme = "README.md" [tool.poetry.dependencies] python = "^3.10" django = "^4.1.5" +djangorestframework = "^3.14.0" +uritemplate = "^4.1.1" +pyyaml = "^6.0" +django-environ = "^0.9.0" [build-system] diff --git a/static/style/main.css b/static/style/main.css new file mode 100644 index 0000000..e69de29 diff --git a/templates/403.html b/templates/403.html new file mode 100644 index 0000000..d50e789 --- /dev/null +++ b/templates/403.html @@ -0,0 +1,5 @@ +{% extends "base.html" %} +{% block content %} +

You can't access this resource.

+

{{exception}}

+{% endblock %} \ No newline at end of file diff --git a/templates/404.html b/templates/404.html new file mode 100644 index 0000000..eccc3b3 --- /dev/null +++ b/templates/404.html @@ -0,0 +1,4 @@ +{% extends "base.html" %} +{% block content %} +

Nothing here...

+{% endblock %} \ No newline at end of file diff --git a/templates/auth/form_template.html b/templates/auth/form_template.html new file mode 100644 index 0000000..bd45cd7 --- /dev/null +++ b/templates/auth/form_template.html @@ -0,0 +1,6 @@ +{% for field in form %} +
+ {{ field.errors }} + {{ field.label_tag }} {{ field }} +
+{% endfor %} \ No newline at end of file diff --git a/templates/auth/label_tag.html b/templates/auth/label_tag.html new file mode 100644 index 0000000..3e7c15e --- /dev/null +++ b/templates/auth/label_tag.html @@ -0,0 +1 @@ +{% if use_tag %}<{{ tag }}{% include 'django/forms/attrs.html' %}>{{ label }}{% else %}{{ label }}{% endif %} \ No newline at end of file diff --git a/templates/auth/login.html b/templates/auth/login.html new file mode 100644 index 0000000..57ab81b --- /dev/null +++ b/templates/auth/login.html @@ -0,0 +1,18 @@ +{% extends 'base.html' %} +{% block content %} +
+
+ {% csrf_token %} +
+ + +
+
+ + +
+ +
+
+ +{% endblock %} \ No newline at end of file diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..66bae0f --- /dev/null +++ b/templates/base.html @@ -0,0 +1,49 @@ +{% load static %} + + + + + + {% block title %}Movieclub{% endblock %} + + + + + + + {% block body %} + {% block menu %} + + {% endblock %} + {% block content %} +

Welcome to out little movieclub.

+ {% endblock %} + + + {% endblock %} + + \ No newline at end of file diff --git a/templates/watchlist/edit.html b/templates/watchlist/edit.html new file mode 100644 index 0000000..188d50f --- /dev/null +++ b/templates/watchlist/edit.html @@ -0,0 +1,29 @@ +{% extends 'base.html' %} +{% block content %} +
+
+ {% csrf_token %} + +
+
+ {% csrf_token %} +
+ + +
+
+ + +
+ +
+
+{% endblock %} \ No newline at end of file diff --git a/templates/watchlist/index.html b/templates/watchlist/index.html new file mode 100644 index 0000000..2b360d5 --- /dev/null +++ b/templates/watchlist/index.html @@ -0,0 +1,20 @@ +{% extends 'base.html' %} +{% block content %} +
+

Watchlist

+ + {% if can_add_movie %} +

Submit new movie

+
+ {% csrf_token %} + + + +
+ {% endif %} +
+{% endblock %} \ No newline at end of file diff --git a/templates/watchlist/movie_detail.html b/templates/watchlist/movie_detail.html new file mode 100644 index 0000000..a45a083 --- /dev/null +++ b/templates/watchlist/movie_detail.html @@ -0,0 +1,39 @@ +{% extends 'base.html' %} +{% block content %} +
+

{{ movie.name }}

+

Suggested by: {{movie.suggested_by.username}}

+ {% if request.user.is_staff or movie.suggested_by == request.user %}
Edit
{% endif %} +

Votes

+ {% if votes|length == 0 %} +

Nobody voted yet, be first...

+ {% else %} +

Total score: {{ movie.score }}, seen by: {{ movie.seen_score }}. +

+ {% endif %} + {% if request.user.is_authenticated %} +

Your opinion

+
+ {% csrf_token %} +
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + {% endif %} +
+{% endblock %} \ No newline at end of file diff --git a/watchlist/__init__.py b/watchlist/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/watchlist/admin.py b/watchlist/admin.py new file mode 100644 index 0000000..96309e0 --- /dev/null +++ b/watchlist/admin.py @@ -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) diff --git a/watchlist/apps.py b/watchlist/apps.py new file mode 100644 index 0000000..085ad64 --- /dev/null +++ b/watchlist/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class WatchlistConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'watchlist' diff --git a/watchlist/forms.py b/watchlist/forms.py new file mode 100644 index 0000000..0d7a43e --- /dev/null +++ b/watchlist/forms.py @@ -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"] \ No newline at end of file diff --git a/watchlist/migrations/0001_initial.py b/watchlist/migrations/0001_initial.py new file mode 100644 index 0000000..2f59ece --- /dev/null +++ b/watchlist/migrations/0001_initial.py @@ -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)), + ], + ), + ] diff --git a/watchlist/migrations/0002_movie_suggested_by_movie_watched.py b/watchlist/migrations/0002_movie_suggested_by_movie_watched.py new file mode 100644 index 0000000..a8800d8 --- /dev/null +++ b/watchlist/migrations/0002_movie_suggested_by_movie_watched.py @@ -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), + ), + ] diff --git a/watchlist/migrations/0003_alter_movie_options_alter_movie_suggested_by.py b/watchlist/migrations/0003_alter_movie_options_alter_movie_suggested_by.py new file mode 100644 index 0000000..9ce1f98 --- /dev/null +++ b/watchlist/migrations/0003_alter_movie_options_alter_movie_suggested_by.py @@ -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), + ), + ] diff --git a/watchlist/migrations/0004_alter_movie_options.py b/watchlist/migrations/0004_alter_movie_options.py new file mode 100644 index 0000000..0b22418 --- /dev/null +++ b/watchlist/migrations/0004_alter_movie_options.py @@ -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")]}, + ), + ] diff --git a/watchlist/migrations/__init__.py b/watchlist/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/watchlist/models.py b/watchlist/models.py new file mode 100644 index 0000000..6c24abd --- /dev/null +++ b/watchlist/models.py @@ -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}" + diff --git a/watchlist/serializers.py b/watchlist/serializers.py new file mode 100644 index 0000000..2d1a409 --- /dev/null +++ b/watchlist/serializers.py @@ -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): + + \ No newline at end of file diff --git a/watchlist/tests.py b/watchlist/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/watchlist/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/watchlist/urls.py b/watchlist/urls.py new file mode 100644 index 0000000..cf3aec8 --- /dev/null +++ b/watchlist/urls.py @@ -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/', views.DetailView.as_view(), name="detail"), + path('movie//vote', views.vote, name="vote"), + path('movie//edit', views.EditView.as_view(), name="edit"), + path('movie//delete', views.delete, name="delete"), + path('movie', views.submit, name="submit") +] \ No newline at end of file diff --git a/watchlist/views.py b/watchlist/views.py new file mode 100644 index 0000000..8cd4794 --- /dev/null +++ b/watchlist/views.py @@ -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"))