Compare commits
34 Commits
eecd62f5b2
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| a48df0f44d | |||
| 9230c35be6 | |||
| adada84e9a | |||
| 8cb0554696 | |||
| 947d9f740c | |||
| c84ae1334e | |||
| 1abc910621 | |||
| 5895714fd5 | |||
| 87086cdce9 | |||
| 54102f0d90 | |||
| 1c1e911518 | |||
| 75dbd6cadc | |||
| 232245d602 | |||
| 2e251d7444 | |||
| c862b72ac8 | |||
| 6a3eacbc4a | |||
| 6ae7bed38d | |||
| d5b65dd0b0 | |||
| b119c54473 | |||
| d4970c6aa4 | |||
| a573b80f09 | |||
| 5221094333 | |||
| 4851d50d7e | |||
| edd4e0ca41 | |||
| be466c451d | |||
| 061944850b | |||
| 9f7bdf952d | |||
| 94e34df89b | |||
| cdd49a2c68 | |||
| 87b1ca2382 | |||
| c3490e8a81 | |||
| e2efba4cc2 | |||
| 80a09c8907 | |||
| d0494c8370 |
+160
@@ -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/
|
||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
kind: pipeline
|
||||||
|
type: docker
|
||||||
|
name: default
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: test
|
||||||
|
image: python:3.10
|
||||||
|
commands:
|
||||||
|
- pip3 install poetry --quiet
|
||||||
|
- poetry install --no-root --quiet
|
||||||
|
- export SECRET_KEY=$(openssl rand -hex 32)
|
||||||
|
- export DATABASE_URL=sqlite://$(mktemp)/db.sqlite3
|
||||||
|
- poetry run ./manage.py test
|
||||||
|
environment:
|
||||||
|
ALLOWED_HOSTS: localhost,127.0.0.1
|
||||||
|
DEBUG: True
|
||||||
|
- name: deploy
|
||||||
|
image: caprover/cli-caprover:2.2.3
|
||||||
|
commands:
|
||||||
|
- caprover deploy
|
||||||
|
environment:
|
||||||
|
CAPROVER_URL:
|
||||||
|
from_secret: caprover_url
|
||||||
|
CAPROVER_APP:
|
||||||
|
from_secret: caprover_app
|
||||||
|
CAPROVER_APP_TOKEN:
|
||||||
|
from_secret: app_token
|
||||||
|
CAPROVER_BRANCH: main
|
||||||
|
when:
|
||||||
|
branch:
|
||||||
|
- main
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
repos:
|
||||||
|
- repo: local
|
||||||
|
hooks:
|
||||||
|
- id: poetry
|
||||||
|
name: poetry
|
||||||
|
language: system
|
||||||
|
entry: poetry check
|
||||||
|
files: '^(pyproject.toml|poetry.lock)$'
|
||||||
|
pass_filenames: false
|
||||||
|
# - id: black
|
||||||
|
# name: black
|
||||||
|
# language: system
|
||||||
|
# entry: poetry run black --target-version py39
|
||||||
|
# types_or: [python, pyi]
|
||||||
|
# require_serial: true
|
||||||
|
# - id: isort
|
||||||
|
# name: isort
|
||||||
|
# language: system
|
||||||
|
# entry: poetry run isort --profile black --python-version 39
|
||||||
|
# types_or: [cython, pyi, python]
|
||||||
|
# require_serial: true
|
||||||
|
# - id: flake8
|
||||||
|
# name: flake8
|
||||||
|
# language: system
|
||||||
|
# entry: poetry run flake8
|
||||||
|
# types_or: [python]
|
||||||
|
# files: "^.*\\.py$"
|
||||||
|
# - id: mypy
|
||||||
|
# name: mypy
|
||||||
|
# language: system
|
||||||
|
# entry: poetry run mypy --strict --python-version 3.9
|
||||||
|
# types_or: [python, pyi]
|
||||||
|
# files: "^.*\\.pyi?$"
|
||||||
|
- id: test
|
||||||
|
name: test
|
||||||
|
language: system
|
||||||
|
entry: poetry run ./manage.py test
|
||||||
|
types_or: [python]
|
||||||
|
files: "^.*\\.py$"
|
||||||
|
pass_filenames: false
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
FROM python:3.11
|
||||||
|
EXPOSE 8000
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY poetry.lock pyproject.toml ./
|
||||||
|
RUN apt update && apt install -y -qq \
|
||||||
|
python3-dev \
|
||||||
|
default-libmysqlclient-dev \
|
||||||
|
build-essential \
|
||||||
|
&& \
|
||||||
|
pip3 install \
|
||||||
|
poetry==1.3.0 \
|
||||||
|
&& \
|
||||||
|
poetry add mysqlclient && \
|
||||||
|
poetry install
|
||||||
|
|
||||||
|
COPY . /app
|
||||||
|
CMD ["bash", "/app/start.sh" ]
|
||||||
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": 2,
|
||||||
|
"dockerfilePath": "./Dockerfile"
|
||||||
|
}
|
||||||
@@ -18,6 +18,12 @@ environ.Env.read_env(os.path.join(BASE_DIR, '.env'))
|
|||||||
|
|
||||||
# SECURITY WARNING: keep the secret key used in production secret!
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
SECRET_KEY = env("SECRET_KEY")
|
SECRET_KEY = env("SECRET_KEY")
|
||||||
|
SESSION_COOKIE_SECURE = env.bool("SESSION_COOKIE_SECURE", False)
|
||||||
|
CSRF_COOKIE_SAMESITE = env("CSRF_COOKIE_SAMESITE", default="Lax")
|
||||||
|
CSRF_COOKIE_SECURE = env.bool("CSRF_COOKIE_SECURE", default=False)
|
||||||
|
CSRF_TRUSTED_ORIGINS = env.list("CSRF_TRUSTED_ORIGINS", default=[])
|
||||||
|
SECURE_PROXY_SSL_HEADER = env.tuple("SECURE_PROXY_SSL_HEADER", default=None)
|
||||||
|
USE_X_FORWARDED_HOST = env.bool("USE_X_FORWARDED_HOST", default=False)
|
||||||
|
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = env('DEBUG')
|
DEBUG = env('DEBUG')
|
||||||
@@ -44,6 +50,7 @@ REST_FRAMEWORK = {
|
|||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
'django.middleware.security.SecurityMiddleware',
|
'django.middleware.security.SecurityMiddleware',
|
||||||
|
"whitenoise.middleware.WhiteNoiseMiddleware",
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
'django.middleware.common.CommonMiddleware',
|
'django.middleware.common.CommonMiddleware',
|
||||||
'django.middleware.csrf.CsrfViewMiddleware',
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
@@ -120,6 +127,7 @@ USE_TZ = True
|
|||||||
STATICFILES_DIRS = [ BASE_DIR / "static"]
|
STATICFILES_DIRS = [ BASE_DIR / "static"]
|
||||||
STATIC_ROOT = "deploy/static/"
|
STATIC_ROOT = "deploy/static/"
|
||||||
STATIC_URL = 'static/'
|
STATIC_URL = 'static/'
|
||||||
|
STATICFILES_STORAGE = env("STATICFILES_STORAGE", default="whitenoise.storage.CompressedStaticFilesStorage")
|
||||||
|
|
||||||
# Default primary key field type
|
# Default primary key field type
|
||||||
# https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field
|
# https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field
|
||||||
|
|||||||
+1
-3
@@ -16,7 +16,6 @@ Including another URLconf
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import include, path, reverse
|
from django.urls import include, path, reverse
|
||||||
from django.views.generic.base import RedirectView
|
from django.views.generic.base import RedirectView
|
||||||
from rest_framework import routers, schemas
|
|
||||||
|
|
||||||
import auth.urls
|
import auth.urls
|
||||||
import watchlist.urls
|
import watchlist.urls
|
||||||
@@ -26,11 +25,10 @@ urlpatterns = [
|
|||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
path('auth/', include(auth.urls)),
|
path('auth/', include(auth.urls)),
|
||||||
path('watchlist/', include(watchlist.urls)),
|
path('watchlist/', include(watchlist.urls)),
|
||||||
# path('api/watchlist/', include(watchlist.urls.api_router.urls)),
|
|
||||||
# path('openapi', schemas.get_schema_view(
|
# path('openapi', schemas.get_schema_view(
|
||||||
# title="Movieclub",
|
# title="Movieclub",
|
||||||
# description="",
|
# description="",
|
||||||
# version="0.1.0"
|
# version="0.1.0"
|
||||||
# )),
|
# )),
|
||||||
# path('api-auth/', include('rest_framework.urls', namespace='rest_framework'))
|
path('api-auth/', include('rest_framework.urls', namespace='rest_framework'))
|
||||||
]
|
]
|
||||||
|
|||||||
Generated
+66
-1
@@ -69,6 +69,27 @@ files = [
|
|||||||
django = ">=3.0"
|
django = ">=3.0"
|
||||||
pytz = "*"
|
pytz = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gunicorn"
|
||||||
|
version = "20.1.0"
|
||||||
|
description = "WSGI HTTP Server for UNIX"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.5"
|
||||||
|
files = [
|
||||||
|
{file = "gunicorn-20.1.0-py3-none-any.whl", hash = "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e"},
|
||||||
|
{file = "gunicorn-20.1.0.tar.gz", hash = "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
setuptools = ">=3.0"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
eventlet = ["eventlet (>=0.24.1)"]
|
||||||
|
gevent = ["gevent (>=1.4.0)"]
|
||||||
|
setproctitle = ["setproctitle"]
|
||||||
|
tornado = ["tornado (>=0.2)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pytz"
|
name = "pytz"
|
||||||
version = "2022.7"
|
version = "2022.7"
|
||||||
@@ -131,6 +152,23 @@ files = [
|
|||||||
{file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"},
|
{file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "setuptools"
|
||||||
|
version = "66.0.0"
|
||||||
|
description = "Easily download, build, install, upgrade, and uninstall Python packages"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "setuptools-66.0.0-py3-none-any.whl", hash = "sha256:a78d01d1e2c175c474884671dde039962c9d74c7223db7369771fcf6e29ceeab"},
|
||||||
|
{file = "setuptools-66.0.0.tar.gz", hash = "sha256:bd6eb2d6722568de6d14b87c44a96fac54b2a45ff5e940e639979a3d1792adb6"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
|
||||||
|
testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
|
||||||
|
testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlparse"
|
name = "sqlparse"
|
||||||
version = "0.4.3"
|
version = "0.4.3"
|
||||||
@@ -143,6 +181,18 @@ files = [
|
|||||||
{file = "sqlparse-0.4.3.tar.gz", hash = "sha256:69ca804846bb114d2ec380e4360a8a340db83f0ccf3afceeb1404df028f57268"},
|
{file = "sqlparse-0.4.3.tar.gz", hash = "sha256:69ca804846bb114d2ec380e4360a8a340db83f0ccf3afceeb1404df028f57268"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tblib"
|
||||||
|
version = "1.7.0"
|
||||||
|
description = "Traceback serialization library."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||||
|
files = [
|
||||||
|
{file = "tblib-1.7.0-py2.py3-none-any.whl", hash = "sha256:289fa7359e580950e7d9743eab36b0691f0310fce64dee7d9c31065b8f723e23"},
|
||||||
|
{file = "tblib-1.7.0.tar.gz", hash = "sha256:059bd77306ea7b419d4f76016aef6d7027cc8a0785579b5aad198803435f882c"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tzdata"
|
name = "tzdata"
|
||||||
version = "2022.7"
|
version = "2022.7"
|
||||||
@@ -167,7 +217,22 @@ files = [
|
|||||||
{file = "uritemplate-4.1.1.tar.gz", hash = "sha256:4346edfc5c3b79f694bccd6d6099a322bbeb628dbf2cd86eea55a456ce5124f0"},
|
{file = "uritemplate-4.1.1.tar.gz", hash = "sha256:4346edfc5c3b79f694bccd6d6099a322bbeb628dbf2cd86eea55a456ce5124f0"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "whitenoise"
|
||||||
|
version = "6.3.0"
|
||||||
|
description = "Radically simplified static file serving for WSGI applications"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "whitenoise-6.3.0-py3-none-any.whl", hash = "sha256:cf8ecf56d86ba1c734fdb5ef6127312e39e92ad5947fef9033dc9e43ba2777d9"},
|
||||||
|
{file = "whitenoise-6.3.0.tar.gz", hash = "sha256:fe0af31504ab08faa1ec7fc02845432096e40cc1b27e6a7747263d7b30fb51fa"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
brotli = ["Brotli"]
|
||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.10"
|
python-versions = "^3.10"
|
||||||
content-hash = "e076467f7d3442399996abc0f612f37a80ba29600fa0d3130e68d525bdf97ef9"
|
content-hash = "ca22a0efd5d4acfb0a4a09e8567d0a8c90debca1203b7a80262919d8055366da"
|
||||||
|
|||||||
@@ -12,8 +12,13 @@ djangorestframework = "^3.14.0"
|
|||||||
uritemplate = "^4.1.1"
|
uritemplate = "^4.1.1"
|
||||||
pyyaml = "^6.0"
|
pyyaml = "^6.0"
|
||||||
django-environ = "^0.9.0"
|
django-environ = "^0.9.0"
|
||||||
|
gunicorn = "^20.1.0"
|
||||||
|
whitenoise = "^6.3.0"
|
||||||
|
|
||||||
|
|
||||||
|
[tool.poetry.group.dev.dependencies]
|
||||||
|
tblib = "^1.7.0"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core"]
|
requires = ["poetry-core"]
|
||||||
build-backend = "poetry.core.masonry.api"
|
build-backend = "poetry.core.masonry.api"
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
poetry run ./manage.py migrate
|
||||||
|
poetry run ./manage.py collectstatic -c --no-input
|
||||||
|
poetry run gunicorn movieclub.wsgi -b 0.0.0.0:8000
|
||||||
+3
-1
@@ -1,4 +1,6 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>Nothing here...</h1>
|
<div class="container-lg">
|
||||||
|
<h1>Nothing here...</h1>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
+1
-1
@@ -7,7 +7,7 @@
|
|||||||
<title>{% block title %}Movieclub{% endblock %}</title>
|
<title>{% block title %}Movieclub{% endblock %}</title>
|
||||||
<meta name='viewport' content='width=device-width, initial-scale=1'>
|
<meta name='viewport' content='width=device-width, initial-scale=1'>
|
||||||
<link rel='icon' type="image/webp" href="{% static 'favicon.webp' %}">
|
<link rel='icon' type="image/webp" href="{% static 'favicon.webp' %}">
|
||||||
<link rel='stylesheet' type='text/css' media='screen' href='{% static "style/main.css" %}'>
|
<!-- <link rel='stylesheet' type='text/css' media='screen' href='{% static "style/main.css" %}'> -->
|
||||||
<!-- Bootstrap -->
|
<!-- Bootstrap -->
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container-lg">
|
<div class="container-lg">
|
||||||
|
{% if error %}<div class="alert alert-warning">{{error}}</div>{% endif %}
|
||||||
<form action="{% url 'watchlist:delete' movie.id %}" method="post">
|
<form action="{% url 'watchlist:delete' movie.id %}" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="submit" value="Delete movie" class="btn btn-danger">
|
<input type="submit" value="Delete movie" class="btn btn-danger">
|
||||||
@@ -13,15 +14,43 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="form-label" for="suggested_by" name="suggested_by">Suggested by</label>
|
<label class="form-label" for="suggested_by" name="suggested_by">Suggested by</label>
|
||||||
<select class="form-select" id="suggested_by" name="suggested_by">
|
<div class="row">
|
||||||
{% if request.user.is_staff %}
|
<div class="col-auto">
|
||||||
{% for user in users %}
|
<select class="form-select" id="suggested_by" name="suggested_by">
|
||||||
<option value="{{ user.username }}" {% if user.username == movie.suggested_by.username %} selected {% endif %} >{{user.username}}</option>
|
{% if request.user.is_staff %}
|
||||||
{% endfor %}
|
{% for user in users %}
|
||||||
{% else %}
|
<option value="{{ user.username }}" {% if user.username == movie.suggested_by.username %} selected {% endif %} >{{user.username}}</option>
|
||||||
<option value="{{ movie.suggested_by.username }} disabled">{{movie.suggested_by.username}}</option>
|
{% endfor %}
|
||||||
{% endif %}
|
{% else %}
|
||||||
</select>
|
<option value="{{ movie.suggested_by.username }} disabled">{{movie.suggested_by.username}}</option>
|
||||||
|
{% endif %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="form-label" for="imdb_id">IMDB id</label>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-auto">
|
||||||
|
<input class="form-control" id="imdb_id" name="imdb_id" {% if movie.imdb_id %}value="{{movie.imdb_id}}"{%endif%}>
|
||||||
|
<div class="form-text">ID like <code>tt5580390</code> or whole URL.</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-3">
|
||||||
|
<a class="btn btn-secondary" href="https://www.imdb.com/find/?q={{movie.name}}" referrerpolicy="no-referrer" target="_blank">Search</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="form-label" for="csfd_id">ČSFD id</label>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-auto">
|
||||||
|
<input class="form-control" id="csfd_id" name="csfd_id" {% if movie.csfd_id %}value="{{movie.csfd_id}}"{%endif%}>
|
||||||
|
<div class="form-text">ID like <code>277495</code> or whole URL.</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-3">
|
||||||
|
<a class="btn btn-secondary" href="https://www.csfd.cz/hledat/?q={{movie.name}}" referrerpolicy="no-referrer" target="_blank">Search</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<input type="submit" value="Submit" class="btn btn-primary">
|
<input type="submit" value="Submit" class="btn btn-primary">
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,19 +1,23 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container-lg">
|
<div class="container-lg">
|
||||||
<h1>Watchlist</h1>
|
<h1>Watchlist</h1>
|
||||||
|
{% if object_list %}
|
||||||
<ul>
|
<ul>
|
||||||
{% for movie in object_list %}
|
{% for movie in object_list %}
|
||||||
<li><a href="{% url 'watchlist:detail' movie.id %}">{{movie.name}}</a> — {{movie.score}}</li>
|
<li>{% if movie.watched %}<i>{% endif %}<a href="{% url 'watchlist:detail' movie.id %}">{{movie.name}}</a>{% if movie.watched %}</i>{% endif %}{% if request.user.is_authenticated and movie not in voted_movies %}<sup>*</sup>{% endif %} — {{movie.score}}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<p>No movies yet.</p>
|
||||||
|
{% endif %}
|
||||||
{% if can_add_movie %}
|
{% if can_add_movie %}
|
||||||
<h2>Submit new movie</h2>
|
<h2>Submit new movie</h2>
|
||||||
<form action="{% url 'watchlist:submit' %}" method="post">
|
<form action="{% url 'watchlist:submit' %}" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<label class="form-label" for="name">Movie name</label>
|
<label class="form-label" for="name">Movie name</label>
|
||||||
<input class="form-control" type="text" id="name" name="name" required>
|
<input class="form-control" type="text" id="name" name="name" required>
|
||||||
<input class="btn btn-primary" type="submit" value="Sumbit">
|
<input class="btn btn-primary" type="submit" value="Submit">
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container-lg">
|
<div class="container-lg">
|
||||||
<h1 class="display-1">{{ movie.name }}</h1>
|
<h1 class="display-1">{{ movie.name }}</h1>
|
||||||
<p class="text-secondary">Suggested by: {{movie.suggested_by.username}}</p>
|
<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 %}
|
<div class="mb-2">
|
||||||
|
{% if request.user.is_staff or movie.suggested_by == request.user %}<a class="btn btn-sm btn-danger" href="{% url 'watchlist:edit' movie.id %}">Edit</a>{% endif %}
|
||||||
|
<a class="btn btn-sm btn-secondary" href="{% if movie.imdb_id %}https://www.imdb.com/title/{{movie.imdb_id}}/{%else%}https://www.imdb.com/find/?q={{movie.name}}{%endif%}" referrerpolicy="no-referrer" target="_blank">IMDB{% if movie.imdb_id == '' %}*{%endif%}</a>
|
||||||
|
<a class="btn btn-sm btn-secondary" href="{% if movie.csfd_id %}https://www.csfd.cz/film/{{movie.csfd_id}}{%else%}https://www.csfd.cz/hledat/?q={{movie.name}}{%endif%}" referrerpolicy="no-referrer" target="_blank">ČSFD{% if movie.csfd_id == '' %}*{%endif%}</a></div>
|
||||||
<h2>Votes</h2>
|
<h2>Votes</h2>
|
||||||
{% if votes|length == 0 %}
|
{% if votes|length == 0 %}
|
||||||
<p>Nobody voted yet, be first...</p>
|
<p>Nobody voted yet, be first...</p>
|
||||||
@@ -11,7 +14,7 @@
|
|||||||
<p>Total score: {{ movie.score }}, seen by: {{ movie.seen_score }}.
|
<p>Total score: {{ movie.score }}, seen by: {{ movie.seen_score }}.
|
||||||
<ul>
|
<ul>
|
||||||
{% for vote in votes %}
|
{% 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>
|
<li>{{vote.user.username}} {% if vote.seen %}(seen){% endif %} – {% if vote.vote == 1 %}👍{% elif vote.vote == 0 %}No opinion{% elif vote.vote == -1 %}👎{%endif%}{% if vote.comment != "" %} – {{vote.comment}}{% endif %}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -30,6 +33,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-check">
|
<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>
|
<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>
|
||||||
|
<div>
|
||||||
|
<label class="form-check-label" for="comment">Comment</label>
|
||||||
|
<input class="form-control" type="text" id="comment" name="comment" {% if user_vote and user_vote.comment %} value="{{user_vote.comment}}"{% endif %}>
|
||||||
</div>
|
</div>
|
||||||
<input class="btn btn-primary" type="submit" value="Submit">
|
<input class="btn btn-primary" type="submit" value="Submit">
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
+5
-1
@@ -3,7 +3,8 @@ from django.contrib import admin
|
|||||||
from . import models
|
from . import models
|
||||||
|
|
||||||
class MovieVoteInline(admin.StackedInline):
|
class MovieVoteInline(admin.StackedInline):
|
||||||
model = models.MovieVote()
|
model = models.MovieVote
|
||||||
|
extra = 0
|
||||||
|
|
||||||
class MovieAdmin(admin.ModelAdmin):
|
class MovieAdmin(admin.ModelAdmin):
|
||||||
fields = [
|
fields = [
|
||||||
@@ -11,6 +12,9 @@ class MovieAdmin(admin.ModelAdmin):
|
|||||||
]
|
]
|
||||||
readonly_fields = ("score",)
|
readonly_fields = ("score",)
|
||||||
list_display = ["name", "watched", "suggested_by", "score"]
|
list_display = ["name", "watched", "suggested_by", "score"]
|
||||||
|
inlines = [
|
||||||
|
MovieVoteInline
|
||||||
|
]
|
||||||
|
|
||||||
@admin.display(description="Score")
|
@admin.display(description="Score")
|
||||||
def score(self, instance):
|
def score(self, instance):
|
||||||
|
|||||||
+1
-1
@@ -6,4 +6,4 @@ class MovieEditForm(forms.ModelForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Movie
|
model = models.Movie
|
||||||
fields = ["name", "suggested_by", "watched"]
|
fields = ["name", "suggested_by", "watched", "imdb_id", "csfd_id"]
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 4.1.5 on 2023-01-24 20:32
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('watchlist', '0004_alter_movie_options'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='movievote',
|
||||||
|
name='comment',
|
||||||
|
field=models.TextField(null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 4.1.5 on 2023-02-10 12:54
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('watchlist', '0005_movievote_comment'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='movie',
|
||||||
|
name='csfd_id',
|
||||||
|
field=models.IntegerField(null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='movie',
|
||||||
|
name='imdb_id',
|
||||||
|
field=models.CharField(blank=True, max_length=32),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Generated by Django 4.1.5 on 2023-02-10 13:26
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import watchlist.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('watchlist', '0006_movie_csfd_id_movie_imdb_id'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='movie',
|
||||||
|
name='csfd_id',
|
||||||
|
field=models.CharField(blank=True, default='', max_length=32, validators=[watchlist.models.validate_imdb_id]),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='movie',
|
||||||
|
name='imdb_id',
|
||||||
|
field=models.CharField(blank=True, max_length=32, validators=[watchlist.models.validate_csfd_id]),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 4.1.5 on 2023-02-17 14:06
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('watchlist', '0007_alter_movie_csfd_id_alter_movie_imdb_id'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='movievote',
|
||||||
|
name='comment',
|
||||||
|
field=models.TextField(blank=True, default=''),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
]
|
||||||
+16
-2
@@ -1,8 +1,19 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
|
IMDB_ID_RE = re.compile(r'(?P<id>tt\d{7,})')
|
||||||
|
CSFD_ID_RE = re.compile(r'(?P<id>[1-9]\d*)')
|
||||||
|
|
||||||
|
def validate_imdb_id(v: str) -> bool:
|
||||||
|
return IMDB_ID_RE.match(v)
|
||||||
|
|
||||||
|
def validate_csfd_id(v: str) -> bool:
|
||||||
|
return CSFD_ID_RE.match(v)
|
||||||
|
|
||||||
class Movie(models.Model):
|
class Movie(models.Model):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -13,14 +24,16 @@ class Movie(models.Model):
|
|||||||
name = models.CharField(max_length=100)
|
name = models.CharField(max_length=100)
|
||||||
suggested_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
|
suggested_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
|
||||||
watched = models.BooleanField(default=False)
|
watched = models.BooleanField(default=False)
|
||||||
|
imdb_id = models.CharField(max_length=32, blank=True, validators=[validate_csfd_id])
|
||||||
|
csfd_id = models.CharField(max_length=32, blank=True, validators=[validate_imdb_id])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def score(self):
|
def score(self):
|
||||||
return reduce(lambda result,v: result+v.vote, self.movievote_set.all(), 0)
|
return reduce(lambda result,v: result+v.vote, self.movievote_set.filter(user__is_active=True).all(), 0)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def seen_score(self):
|
def seen_score(self):
|
||||||
return reduce(lambda result,v: result+int(v.seen), self.movievote_set.all(), 0)
|
return reduce(lambda result,v: result+int(v.seen), self.movievote_set.filter(user__is_active=True).all(), 0)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
@@ -36,6 +49,7 @@ class MovieVote(models.Model):
|
|||||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
vote = models.IntegerField(choices=Vote.choices, default=Vote.NOVOTE)
|
vote = models.IntegerField(choices=Vote.choices, default=Vote.NOVOTE)
|
||||||
seen = models.BooleanField(default=False, null=True)
|
seen = models.BooleanField(default=False, null=True)
|
||||||
|
comment = models.TextField(blank=True)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.user.username}'s vote for {self.movie.name}"
|
return f"{self.user.username}'s vote for {self.movie.name}"
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
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):
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
# Create your tests here.
|
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from watchlist.models import IMDB_ID_RE, CSFD_ID_RE
|
||||||
|
|
||||||
|
class IMDBIDTest(TestCase):
|
||||||
|
|
||||||
|
def test_valid_imdb_id(self):
|
||||||
|
valid = [
|
||||||
|
"tt0000000",
|
||||||
|
"tt1234567",
|
||||||
|
"tt4975722",
|
||||||
|
"tt3581920"
|
||||||
|
]
|
||||||
|
for testId in valid:
|
||||||
|
with self.subTest(testId=testId):
|
||||||
|
self.assertTrue(IMDB_ID_RE.match(testId))
|
||||||
|
|
||||||
|
def test_invalid_imdb_id(self):
|
||||||
|
invalid = [
|
||||||
|
"tt",
|
||||||
|
"tt1",
|
||||||
|
"1234567"
|
||||||
|
"tt123456",
|
||||||
|
"-tt1234567",
|
||||||
|
""
|
||||||
|
]
|
||||||
|
for testId in invalid:
|
||||||
|
with self.subTest(testId=testId):
|
||||||
|
self.assertFalse(IMDB_ID_RE.match(testId))
|
||||||
|
|
||||||
|
def test_url_extraction(self):
|
||||||
|
urls = [
|
||||||
|
("https://www.imdb.com/title/tt4925292/?ref_=hm_rvi_tt_i_1", "tt4925292"),
|
||||||
|
("https://www.imdb.com/title/tt5580390/", "tt5580390"),
|
||||||
|
]
|
||||||
|
for url, result in urls:
|
||||||
|
with self.subTest(url=url, result=result):
|
||||||
|
m = IMDB_ID_RE.search(url)
|
||||||
|
self.assertTrue(m)
|
||||||
|
self.assertEqual(m.group(), result)
|
||||||
|
|
||||||
|
class CSFDIDTest(TestCase):
|
||||||
|
|
||||||
|
def test_valid_csfd_id(self):
|
||||||
|
valid = [
|
||||||
|
"1",
|
||||||
|
"123",
|
||||||
|
"969361"
|
||||||
|
|
||||||
|
]
|
||||||
|
for testId in valid:
|
||||||
|
with self.subTest(testId=testId):
|
||||||
|
self.assertTrue(CSFD_ID_RE.match(testId))
|
||||||
|
|
||||||
|
def test_invalid_csfd_id(self):
|
||||||
|
invalid = [
|
||||||
|
"abc",
|
||||||
|
"-1",
|
||||||
|
"0",
|
||||||
|
"tt0000000",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
for testId in invalid:
|
||||||
|
with self.subTest(testId=testId):
|
||||||
|
self.assertFalse(CSFD_ID_RE.match(testId))
|
||||||
|
|
||||||
|
def test_url_extraction(self):
|
||||||
|
urls = [
|
||||||
|
("https://www.csfd.cz/film/1-predevsim-nikomu-neublizim/recenze/", "1"),
|
||||||
|
("https://www.csfd.cz/film/969361-velryba/prehled/", "969361"),
|
||||||
|
("https://www.csfd.cz/film/370706-daredevil/galerie/?page=20", "370706"),
|
||||||
|
("https://www.csfd.cz/film/370706", "370706")
|
||||||
|
]
|
||||||
|
for url, result in urls:
|
||||||
|
with self.subTest(url=url, result=result):
|
||||||
|
m = CSFD_ID_RE.search(url)
|
||||||
|
self.assertTrue(m)
|
||||||
|
self.assertEqual(m.group(), result)
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.contrib.auth.models import User, Permission
|
||||||
|
from django.contrib.auth.hashers import make_password
|
||||||
|
|
||||||
|
from watchlist.models import Movie, MovieVote
|
||||||
|
|
||||||
|
def create_user(name="user", is_staff=False):
|
||||||
|
user = User.objects.create(username=name, password=make_password("dummy"))
|
||||||
|
user.user_permissions.add(Permission.objects.get(codename="add_movie"))
|
||||||
|
user.save()
|
||||||
|
return user
|
||||||
|
|
||||||
|
def create_movie(name="Test movie", added_by="user"):
|
||||||
|
return Movie.objects.create(name=name, suggested_by=User.objects.filter(username=added_by).first(), watched=False)
|
||||||
|
|
||||||
|
class IndexViewTests(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.user = create_user("user")
|
||||||
|
|
||||||
|
def test_no_movies(self):
|
||||||
|
"""Tests the index page with no movies"""
|
||||||
|
response = self.client.get(reverse('watchlist:index'))
|
||||||
|
self.assertQuerysetEqual(response.context["object_list"], [])
|
||||||
|
self.assertContains(response, "No movies yet.")
|
||||||
|
self.assertNotContains(response, "Submit new movie")
|
||||||
|
|
||||||
|
def test_no_movies_logged_in(self):
|
||||||
|
"""Tests the index page with no movies, while the user is logged in."""
|
||||||
|
self.client.login(username="user", password="dummy")
|
||||||
|
response = self.client.get(reverse('watchlist:index'))
|
||||||
|
self.assertQuerysetEqual(response.context["object_list"], [])
|
||||||
|
self.assertContains(response, "No movies yet.")
|
||||||
|
self.assertContains(response, "Submit new movie")
|
||||||
|
|
||||||
|
def test_with_movie(self):
|
||||||
|
"""Tests that the index page shows a movie, hides it if it watched, but shows
|
||||||
|
it with proper filter"""
|
||||||
|
create_movie()
|
||||||
|
with self.subTest(watched=False):
|
||||||
|
response = self.client.get(reverse('watchlist:index'))
|
||||||
|
self.assertQuerysetEqual(response.context["object_list"], Movie.objects.all())
|
||||||
|
self.assertContains(response, "Test movie")
|
||||||
|
movie = Movie.objects.first()
|
||||||
|
movie.watched = True
|
||||||
|
movie.save()
|
||||||
|
with self.subTest(watched=True, filter=False):
|
||||||
|
response = self.client.get(reverse('watchlist:index'))
|
||||||
|
self.assertQuerysetEqual(response.context["object_list"], [])
|
||||||
|
self.assertNotContains(response, "Test movie")
|
||||||
|
with self.subTest(watched=True, filter=True):
|
||||||
|
response = self.client.get(reverse('watchlist:index',) + "?watched")
|
||||||
|
self.assertQuerysetEqual(response.context["object_list"], Movie.objects.all())
|
||||||
|
self.assertContains(response, "Test movie")
|
||||||
|
|
||||||
|
def test_unvoted(self):
|
||||||
|
"""Test that the index shows an asterisk with movies that the user hasn't voted on yet"""
|
||||||
|
create_movie()
|
||||||
|
self.client.login(username="user",password="dummy")
|
||||||
|
response = self.client.get(reverse('watchlist:index'))
|
||||||
|
self.assertContains(response, "Test movie</a><sup>*</sup>")
|
||||||
|
|
||||||
|
def test_voted(self):
|
||||||
|
"""Test that the index doesn't show an asterisk with movies the has voted on."""
|
||||||
|
movie = create_movie()
|
||||||
|
MovieVote.objects.create(movie=movie, user=self.user, vote=1)
|
||||||
|
self.client.login(username="user",password="dummy")
|
||||||
|
response = self.client.get(reverse('watchlist:index'))
|
||||||
|
self.assertContains(response, "Test movie</a>")
|
||||||
|
self.assertNotContains(response, "Test movie</a><sup>*</sup>")
|
||||||
|
|
||||||
|
|
||||||
|
def test_movie_sorting(self):
|
||||||
|
"""Test various methods of movie sorting"""
|
||||||
|
m1 = create_movie(name="ZZZ: A movie test")
|
||||||
|
m2 = create_movie(name="Test movie 2")
|
||||||
|
tests = [
|
||||||
|
('?sort=id', [m1,m2]),
|
||||||
|
('?sort=-id', [m2,m1]),
|
||||||
|
('?sort=name', [m2,m1]),
|
||||||
|
('?sort=-name', [m1,m2]),
|
||||||
|
]
|
||||||
|
for param, qs in tests:
|
||||||
|
with self.subTest(param=param, qs=qs):
|
||||||
|
response = self.client.get(reverse('watchlist:index') + param)
|
||||||
|
self.assertQuerysetEqual(response.context["object_list"], qs)
|
||||||
|
self.assertContains(response, m1.name)
|
||||||
|
self.assertContains(response, m2.name)
|
||||||
|
|
||||||
|
class MovieDetailViewTests(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.user = create_user("user")
|
||||||
|
|
||||||
|
def test_detail_logged_out(self):
|
||||||
|
m = create_movie()
|
||||||
|
response = self.client.get(reverse('watchlist:detail', args=(m.id,)))
|
||||||
|
self.assertContains(response, m.name)
|
||||||
|
self.assertEqual(response.context["movie"], m)
|
||||||
|
self.assertNotContains(response, "Edit")
|
||||||
|
|
||||||
|
def test_detail_logged_in(self):
|
||||||
|
m = create_movie()
|
||||||
|
self.client.login(username="user", password="dummy")
|
||||||
|
response = self.client.get(reverse('watchlist:detail', args=(m.id,)))
|
||||||
|
self.assertContains(response, m.name)
|
||||||
|
self.assertEqual(response.context["movie"], m)
|
||||||
|
self.assertContains(response, "Edit")
|
||||||
|
|
||||||
|
def test_no_movie(self):
|
||||||
|
response = self.client.get(reverse('watchlist:detail', args=(1,)))
|
||||||
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
|
class VoteTests(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.user = create_user("user")
|
||||||
|
|
||||||
|
def test_no_user_voting(self):
|
||||||
|
m = create_movie()
|
||||||
|
response = self.client.post(reverse('watchlist:vote', args=(m.id,)), data={"vote": 1, "seen": True})
|
||||||
|
self.assertRedirects(response, "/accounts/login/?next=" + response.request["PATH_INFO"], fetch_redirect_response=False)
|
||||||
|
|
||||||
|
def test_user_voting(self):
|
||||||
|
m = create_movie()
|
||||||
|
score = m.score
|
||||||
|
self.client.login(username="user", password="dummy")
|
||||||
|
for vote in [1, 0, -1]:
|
||||||
|
for seen in True, False:
|
||||||
|
with self.subTest(vote=vote, seen=seen):
|
||||||
|
response = self.client.post(reverse('watchlist:vote', args=(m.id,)), data={"vote": str(vote), "seen": True})
|
||||||
|
self.assertRedirects(response, reverse('watchlist:index'))
|
||||||
|
self.assertEqual(m.score, score + vote)
|
||||||
|
|
||||||
|
def test_invalid_votes(self):
|
||||||
|
m = create_movie()
|
||||||
|
score = m.score
|
||||||
|
self.client.login(username="user", password="dummy")
|
||||||
|
for vote in [2,3,4,-5,0.5,'abc','']:
|
||||||
|
with self.subTest(vote=vote):
|
||||||
|
response = self.client.post(reverse('watchlist:vote', args=(m.id,)), data={"vote": str(vote), "seen": True})
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
self.assertEqual(m.score, score)
|
||||||
|
with self.subTest(vote="empty"):
|
||||||
|
response = self.client.post(reverse('watchlist:vote', args=(m.id,)), data={"seen": True})
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
self.assertEqual(m.score, score)
|
||||||
|
|
||||||
|
def test_seen(self):
|
||||||
|
m = create_movie()
|
||||||
|
self.client.login(username="user", password="dummy")
|
||||||
|
for seen in (True, False) * 2:
|
||||||
|
with self.subTest(seen=seen):
|
||||||
|
data = {"vote": "0", "seen": "on"} if seen else {"vote": "0"}
|
||||||
|
response = self.client.post(reverse('watchlist:vote', args=(m.id,)), data=data)
|
||||||
|
self.assertRedirects(response, reverse('watchlist:index'))
|
||||||
|
mv = m.movievote_set.get(user=self.user)
|
||||||
|
self.assertEqual(mv.seen, seen)
|
||||||
|
|
||||||
|
|
||||||
|
def test_comment(self):
|
||||||
|
m = create_movie()
|
||||||
|
self.client.login(username="user", password="dummy")
|
||||||
|
for comment in ["aaa", "", "TEST"]:
|
||||||
|
with self.subTest(comment=comment):
|
||||||
|
response = self.client.post(reverse('watchlist:vote', args=(m.id,)), data={"vote": "0", "comment": comment})
|
||||||
|
mv = m.movievote_set.get(user=self.user)
|
||||||
|
self.assertEqual(mv.comment, comment)
|
||||||
|
with self.subTest(comment=None):
|
||||||
|
response = self.client.post(reverse('watchlist:vote', args=(m.id,)), data={"vote": "0"})
|
||||||
|
mv = m.movievote_set.get(user=self.user)
|
||||||
|
self.assertEqual(mv.comment, "")
|
||||||
|
|
||||||
|
|
||||||
@@ -1,10 +1,6 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
from rest_framework import routers
|
|
||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
api_router = routers.DefaultRouter()
|
|
||||||
api_router.register(r'movie', views.MovieViewSet)
|
|
||||||
|
|
||||||
app_name = "watchlist"
|
app_name = "watchlist"
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', views.IndexView.as_view(), name="index"),
|
path('', views.IndexView.as_view(), name="index"),
|
||||||
|
|||||||
+46
-23
@@ -1,3 +1,4 @@
|
|||||||
|
from django.db.models import Q
|
||||||
from django.http import HttpResponseRedirect, HttpResponseBadRequest, HttpResponseForbidden
|
from django.http import HttpResponseRedirect, HttpResponseBadRequest, HttpResponseForbidden
|
||||||
from django.views import generic
|
from django.views import generic
|
||||||
from django.views.decorators.http import require_http_methods, require_safe, require_POST
|
from django.views.decorators.http import require_http_methods, require_safe, require_POST
|
||||||
@@ -5,35 +6,30 @@ from django.urls import reverse
|
|||||||
from django.shortcuts import get_object_or_404, render
|
from django.shortcuts import get_object_or_404, render
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.contrib.auth.models import User
|
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)
|
|
||||||
|
|
||||||
|
from . import models
|
||||||
|
|
||||||
class IndexView(generic.ListView):
|
class IndexView(generic.ListView):
|
||||||
template_name = "watchlist/index.html"
|
template_name = "watchlist/index.html"
|
||||||
model = models.Movie
|
model = models.Movie
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return models.Movie.objects.order_by('id').all()
|
qs = models.Movie.objects
|
||||||
|
# Filter
|
||||||
|
if self.request.GET.get("watched", False) == False:
|
||||||
|
qs = qs.filter(watched=False)
|
||||||
|
# Sort
|
||||||
|
order = self.request.GET.get("sort", "score")
|
||||||
|
if order in ('id', '-id', 'name', '-name'):
|
||||||
|
qs = qs.order_by(order).all()
|
||||||
|
elif order == "score" or order == "-score":
|
||||||
|
qs = sorted(qs.all(), key=lambda x: x.score, reverse=True if order == "score" else False)
|
||||||
|
return qs
|
||||||
|
|
||||||
def get_context_data(self):
|
def get_context_data(self):
|
||||||
context = super().get_context_data()
|
context = super().get_context_data()
|
||||||
context['can_add_movie'] = self.request.user.has_perm("watchlist.add_movie")
|
context['can_add_movie'] = self.request.user.has_perm("watchlist.add_movie")
|
||||||
|
context['voted_movies'] = models.Movie.objects.filter(movievote__user=self.request.user).all() if self.request.user.is_authenticated else []
|
||||||
return context
|
return context
|
||||||
|
|
||||||
class DetailView(generic.DetailView):
|
class DetailView(generic.DetailView):
|
||||||
@@ -41,7 +37,7 @@ class DetailView(generic.DetailView):
|
|||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
votes = self.object.movievote_set.all()
|
votes = self.object.movievote_set.filter(user__is_active=True).all()
|
||||||
user_vote = None
|
user_vote = None
|
||||||
if self.request.user.is_authenticated:
|
if self.request.user.is_authenticated:
|
||||||
user_vote = votes.filter(user=self.request.user).first()
|
user_vote = votes.filter(user=self.request.user).first()
|
||||||
@@ -56,10 +52,15 @@ def vote(request, pk):
|
|||||||
user_vote = movie.movievote_set.filter(user=request.user).first()
|
user_vote = movie.movievote_set.filter(user=request.user).first()
|
||||||
if user_vote is None:
|
if user_vote is None:
|
||||||
user_vote = models.MovieVote(movie=movie, user=request.user)
|
user_vote = models.MovieVote(movie=movie, user=request.user)
|
||||||
|
if not request.POST.get("vote", None) in ("1", "0", "-1"):
|
||||||
|
return HttpResponseBadRequest("Invalid vote.")
|
||||||
user_vote.vote = request.POST['vote']
|
user_vote.vote = request.POST['vote']
|
||||||
user_vote.seen = request.POST.get('seen', False) == "on"
|
user_vote.seen = request.POST.get('seen', False) == "on"
|
||||||
|
comment = request.POST.get('comment', '').strip()
|
||||||
|
if comment != '' or user_vote.comment != "":
|
||||||
|
user_vote.comment = comment
|
||||||
user_vote.save()
|
user_vote.save()
|
||||||
return HttpResponseRedirect(reverse('watchlist:detail', args=(pk,)))
|
return HttpResponseRedirect(reverse('watchlist:index'))
|
||||||
|
|
||||||
class EditView(generic.DetailView):
|
class EditView(generic.DetailView):
|
||||||
model = models.Movie
|
model = models.Movie
|
||||||
@@ -70,7 +71,8 @@ class EditView(generic.DetailView):
|
|||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context["users"] = User.objects.all() if self.request.user.has_perm("watchlist.moderate_movies") else None
|
context["users"] = User.objects.filter(Q(is_active=True) | Q(id=self.object.suggested_by.id)).all() if self.request.user.has_perm("watchlist.moderate_movies") else None
|
||||||
|
context["error"] = self.request.GET.get("error", None)
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
@@ -84,7 +86,28 @@ class EditView(generic.DetailView):
|
|||||||
if not self.can_edit_movie(request):
|
if not self.can_edit_movie(request):
|
||||||
return HttpResponseForbidden("You cannot edit this object.")
|
return HttpResponseForbidden("You cannot edit this object.")
|
||||||
if "name" in request.POST:
|
if "name" in request.POST:
|
||||||
self.object.name = request.POST["name"]
|
name = request.POST["name"]
|
||||||
|
if name == '':
|
||||||
|
return HttpResponseRedirect(reverse('watchlist:edit', args=(kwargs["pk"],)) + "?error=Invalid%20name")
|
||||||
|
self.object.name = name
|
||||||
|
if "imdb_id" in request.POST:
|
||||||
|
if request.POST["imdb_id"] == '':
|
||||||
|
if self.object.imdb_id != '':
|
||||||
|
self.object.imdb_id = ''
|
||||||
|
else:
|
||||||
|
match = models.IMDB_ID_RE.search(request.POST["imdb_id"])
|
||||||
|
if not match:
|
||||||
|
return HttpResponseRedirect(reverse('watchlist:edit', args=(kwargs["pk"],)) + "?error=Invalid%20IMDB%20ID")
|
||||||
|
self.object.imdb_id = match.group()
|
||||||
|
if "csfd_id" in request.POST:
|
||||||
|
if request.POST["csfd_id"] == '':
|
||||||
|
if self.object.csfd_id != '':
|
||||||
|
self.object.csfd_id = ''
|
||||||
|
else:
|
||||||
|
match = models.CSFD_ID_RE.search(request.POST["csfd_id"])
|
||||||
|
if not match:
|
||||||
|
return HttpResponseRedirect(reverse('watchlist:edit', args=(kwargs["pk"],)) + "?error=Invalid%20CSFD%20ID")
|
||||||
|
self.object.csfd_id = match.group()
|
||||||
if "suggested_by" in request.POST:
|
if "suggested_by" in request.POST:
|
||||||
if request.user.has_perm('watchlist.moderate_movies'):
|
if request.user.has_perm('watchlist.moderate_movies'):
|
||||||
new_suggestor = User.objects.filter(username=request.POST["suggested_by"]).first()
|
new_suggestor = User.objects.filter(username=request.POST["suggested_by"]).first()
|
||||||
@@ -105,7 +128,7 @@ def submit(request):
|
|||||||
return HttpResponseForbidden("You can't add new movies.")
|
return HttpResponseForbidden("You can't add new movies.")
|
||||||
movie = models.Movie(name=request.POST["name"], suggested_by=request.user, watched=False)
|
movie = models.Movie(name=request.POST["name"], suggested_by=request.user, watched=False)
|
||||||
movie.save()
|
movie.save()
|
||||||
return HttpResponseRedirect(reverse("watchlist:index"))
|
return HttpResponseRedirect(reverse("watchlist:edit", args=(movie.id,)))
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@require_POST
|
@require_POST
|
||||||
|
|||||||
Reference in New Issue
Block a user