diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..29ed65f --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,48 @@ +name: Test application + +on: + push: + pull_request: + types: + - reopened + +jobs: + build: + runs-on: ubuntu-latest + strategy: + max-parallel: 4 + matrix: + python-version: ["3.11"] + poetry-version: ["1.6.1"] + + env: + DATABASE_URL: "sqlite:///db.sqlite3" + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Setup Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + + - name: Install Poetry ${{ matrix.poetry-version }} + uses: snok/install-poetry@v1 + with: + version: ${{ matrix.poetry-version }} + virtualenvs-create: false + installer-parallel: true + + - name: Install dependencies + run: | + poetry run pip install --no-deps --upgrade pip + poetry install --with dev + + - name: Run hooks + uses: pre-commit/action@v3.0.0 + + - name: Run tests + run: | + poetry run python manage.py test \ + --settings=server.settings.development diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 535f8cd..c443555 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -36,3 +36,6 @@ repos: rev: v1.5.1 hooks: - id: mypy + additional_dependencies: + - 'django-stubs>=4.2.4,<5.0' + - 'django-environ>=0.11.2,<1.0' diff --git a/README.md b/README.md index 883be91..a11421d 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -# 2023-2-Squad06 \ No newline at end of file +# 2023-2-Squad06 diff --git a/apps/templates/.keep b/apps/templates/.keep new file mode 100644 index 0000000..e69de29 diff --git a/bin/create-env b/bin/create-env new file mode 100755 index 0000000..f16ee6b --- /dev/null +++ b/bin/create-env @@ -0,0 +1,14 @@ +#!/bin/bash + +# This script creates a .env file in the config directory and generates a +# random secret key for the Django app automatically. + +read -r -d '' SECRET_KEY_CMD << EOM +from django.utils.crypto import get_random_string; +print(get_random_string(64)) +EOM + +SECRET_KEY=$(python3 -c "${SECRET_KEY_CMD}") +OUTPUT=$(sed "s/^\(DJANGO_SECRET_KEY=\).*/\1$SECRET_KEY/" config/.env.example) + +echo "$OUTPUT" > config/.env diff --git a/config/.env.example b/config/.env.example new file mode 100644 index 0000000..aabbc8e --- /dev/null +++ b/config/.env.example @@ -0,0 +1,17 @@ + +############ +# Django # +############ + +DJANGO_SECRET_KEY= + +################ +# PostgreSQL # +################ + +POSTGRES_HOST=postgres +POSTGRES_PORT=5432 + +POSTGRES_USER=virtualjudge +POSTGRES_PASSWORD=virtualjudge +POSTGRES_DB=virtualjudge diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..ceef93a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,42 @@ +services: + django: + image: virtual-judge:dev + container_name: django + build: + context: . + dockerfile: docker/django/Dockerfile + target: development-build + args: + - DJANGO_ENV=development + - UID=${UID:-1000} + - GID=${GID:-1000} + cache_from: + - "virtual-judge:dev" + - "virtual-judge:latest" + - "*" + volumes: + - .:/app + ports: + - 8000:8000 + networks: + - main + env_file: + - config/.env + command: python -Wd manage.py runserver 0.0.0.0:8000 + + postgres: + image: postgres:16.0-alpine + container_name: postgres + volumes: + - data:/var/lib/postgresql/data + networks: + - main + env_file: + - config/.env + restart: unless-stopped + +volumes: + data: + +networks: + main: diff --git a/docker/django/Dockerfile b/docker/django/Dockerfile new file mode 100644 index 0000000..c85ba76 --- /dev/null +++ b/docker/django/Dockerfile @@ -0,0 +1,102 @@ +# We use multi-stage builds to reduce the size of the image in production and +# to avoid installing unnecessary dependencies in the production image. +# See: https://docs.docker.com/develop/develop-images/multistage-build/ + +####################### +# Development stage # +####################### + +FROM python:3.11.5-slim-bullseye AS development-build + +ARG DJANGO_ENV \ + # This is needed to fix permissions of files created in the container, so + # that they are owned by the host user. + UID=1000 \ + GID=1000 + +ENV DJANGO_ENV=${DJANGO_ENV} \ + # Python: + PYTHONFAULTHANDLER=1 \ + PYTHONUNBUFFERED=1 \ + PYTHONHASHSEED=random \ + PYTHONDONTWRITEBYTECODE=1 \ + # Pip: + PIP_NO_CACHE_DIR=1 \ + PIP_DISABLE_PIP_VERSION_CHECK=1 \ + PIP_DEFAULT_TIMEOUT=100 \ + # Poetry: + POETRY_VERSION=1.6.1 \ + POETRY_NO_INTERACTION=1 \ + POETRY_VIRTUALENVS_CREATE=false \ + POETRY_CACHE_DIR='/var/cache/pypoetry' \ + POETRY_HOME='/usr/local' \ + # Tini + TINI_VERSION=v0.19.0 \ + # Dockerize: + DOCKERIZE_VERSION=v0.7.0 + +SHELL ["/bin/bash", "-eo", "pipefail", "-c"] + + +# Install system dependencies: +RUN apt-get update \ + && apt-get upgrade -y \ + && apt-get install -y --no-install-recommends \ + bash \ + brotli \ + build-essential \ + curl \ + gettext \ + git \ + libpq-dev \ + # Installing Poetry: + && curl -sSL 'https://install.python-poetry.org' | python - \ + && poetry --version \ + # Installing Dockerize: + && curl -sSLO "https://github.com/jwilder/dockerize/releases/download/${DOCKERIZE_VERSION}/dockerize-linux-amd64-${DOCKERIZE_VERSION}.tar.gz" \ + && tar -C /usr/local/bin -xzvf "dockerize-linux-amd64-${DOCKERIZE_VERSION}.tar.gz" \ + && rm "dockerize-linux-amd64-${DOCKERIZE_VERSION}.tar.gz" \ + && dockerize --version \ + # Installing Tini: + && dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')" \ + && curl -o /usr/local/bin/tini -sSLO "https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini-${dpkgArch}" \ + && chmod +x /usr/local/bin/tini \ + && tini --version \ + # Cleaning cache: + && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \ + && apt-get clean -y && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# We create a non-root user to run the application, so that we don't run the +# application as root. +RUN groupadd -g "${GID}" -r web \ + && useradd -d '/app' -g web -l -r -u "${UID}" web \ + && chown web:web -R '/app' + +COPY --chown=web:web ./poetry.lock ./pyproject.toml /app/ + +RUN --mount=type=cache,target="$POETRY_CACHE_DIR" echo "${DJANGO_ENV}" \ + && poetry version \ + && poetry run pip install --no-deps --upgrade pip \ + && poetry install \ + $(if [ "${DJANGO_ENV}" = 'production' ]; then echo '--only main'; fi) \ + --no-interaction --no-ansi + +COPY ./docker/django/entrypoint.sh /docker-entrypoint.sh + +RUN chmod +x '/docker-entrypoint.sh' + +USER web + +# We customize how our app is loaded with the custom entrypoint: +ENTRYPOINT ["tini", "--", "/docker-entrypoint.sh"] + + +###################### +# Production stage # +###################### + +FROM development AS production + +COPY --chown=web:web . /app diff --git a/docker/django/entrypoint.sh b/docker/django/entrypoint.sh new file mode 100755 index 0000000..0fecb0e --- /dev/null +++ b/docker/django/entrypoint.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +set -o errexit +set -o nounset +set -o pipefail + +readonly cmd="$*" + +: "${POSTGRES_HOST:=postgres}" +: "${POSTGRES_PORT:=5432}" +export DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}" + +# We need this line to make sure that this container is started after the one +# with PostgreSQL: +dockerize \ + -wait "tcp://${POSTGRES_HOST}:${POSTGRES_PORT}" \ + -timeout 90s + +>&2 echo 'PostgreSQL is up -- continuing...' + +exec $cmd diff --git a/docs/source/conf.py b/docs/source/conf.py index e0bb952..0fa1244 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,27 +1,3 @@ -""" -MIT License - -Copyright (c) 2023 MDS - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -""" - from dataclasses import asdict from typing import List diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..640f6b6 --- /dev/null +++ b/manage.py @@ -0,0 +1,21 @@ +import sys +from os import environ + + +def main() -> None: + environ.setdefault("DJANGO_SETTINGS_MODULE", "server.settings.development") + + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + + execute_from_command_line(sys.argv) + + +if __name__ == "__main__": + main() diff --git a/poetry.lock b/poetry.lock index 1548963..5858235 100644 --- a/poetry.lock +++ b/poetry.lock @@ -25,6 +25,20 @@ files = [ [package.extras] test = ["coverage", "mypy", "pexpect", "ruff", "wheel"] +[[package]] +name = "asgiref" +version = "3.7.2" +description = "ASGI specs, helper code, and adapters" +optional = false +python-versions = ">=3.7" +files = [ + {file = "asgiref-3.7.2-py3-none-any.whl", hash = "sha256:89b2ef2247e3b562a16eef663bc0e2e703ec6468e2fa8a5cd61cd449786d4f6e"}, + {file = "asgiref-3.7.2.tar.gz", hash = "sha256:9e0ce3aa93a819ba5b45120216b23878cf6e8525eb3848653452b4192b92afed"}, +] + +[package.extras] +tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] + [[package]] name = "babel" version = "2.12.1" @@ -275,6 +289,82 @@ files = [ {file = "distlib-0.3.7.tar.gz", hash = "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"}, ] +[[package]] +name = "django" +version = "4.2.5" +description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." +optional = false +python-versions = ">=3.8" +files = [ + {file = "Django-4.2.5-py3-none-any.whl", hash = "sha256:b6b2b5cae821077f137dc4dade696a1c2aa292f892eca28fa8d7bfdf2608ddd4"}, + {file = "Django-4.2.5.tar.gz", hash = "sha256:5e5c1c9548ffb7796b4a8a4782e9a2e5a3df3615259fc1bfd3ebc73b646146c1"}, +] + +[package.dependencies] +asgiref = ">=3.6.0,<4" +sqlparse = ">=0.3.1" +tzdata = {version = "*", markers = "sys_platform == \"win32\""} + +[package.extras] +argon2 = ["argon2-cffi (>=19.1.0)"] +bcrypt = ["bcrypt"] + +[[package]] +name = "django-environ" +version = "0.11.2" +description = "A package that allows you to utilize 12factor inspired environment variables to configure your Django application." +optional = false +python-versions = ">=3.6,<4" +files = [ + {file = "django-environ-0.11.2.tar.gz", hash = "sha256:f32a87aa0899894c27d4e1776fa6b477e8164ed7f6b3e410a62a6d72caaf64be"}, + {file = "django_environ-0.11.2-py2.py3-none-any.whl", hash = "sha256:0ff95ab4344bfeff693836aa978e6840abef2e2f1145adff7735892711590c05"}, +] + +[package.extras] +develop = ["coverage[toml] (>=5.0a4)", "furo (>=2021.8.17b43,<2021.9.dev0)", "pytest (>=4.6.11)", "sphinx (>=3.5.0)", "sphinx-notfound-page"] +docs = ["furo (>=2021.8.17b43,<2021.9.dev0)", "sphinx (>=3.5.0)", "sphinx-notfound-page"] +testing = ["coverage[toml] (>=5.0a4)", "pytest (>=4.6.11)"] + +[[package]] +name = "django-stubs" +version = "4.2.4" +description = "Mypy stubs for Django" +optional = false +python-versions = ">=3.8" +files = [ + {file = "django-stubs-4.2.4.tar.gz", hash = "sha256:7d4a132c381519815e865c27a89eca41bcbd06056832507224816a43d75c601c"}, + {file = "django_stubs-4.2.4-py3-none-any.whl", hash = "sha256:834b60fd81510cce6b56c1c6c28bec3c504a418bc90ff7d0063fabe8ab9a7868"}, +] + +[package.dependencies] +django = "*" +django-stubs-ext = ">=4.2.2" +mypy = [ + {version = ">=1.0.0", optional = true, markers = "extra != \"compatible-mypy\""}, + {version = "==1.5.*", optional = true, markers = "extra == \"compatible-mypy\""}, +] +types-pytz = "*" +types-PyYAML = "*" +typing-extensions = "*" + +[package.extras] +compatible-mypy = ["mypy (==1.5.*)"] + +[[package]] +name = "django-stubs-ext" +version = "4.2.2" +description = "Monkey-patching and extensions for django-stubs" +optional = false +python-versions = ">=3.8" +files = [ + {file = "django-stubs-ext-4.2.2.tar.gz", hash = "sha256:c69d1cc46f1c4c3b7894b685a5022c29b2a36c7cfb52e23762eaf357ebfc2c98"}, + {file = "django_stubs_ext-4.2.2-py3-none-any.whl", hash = "sha256:fdacc65a14d2d4b97334b58ff178a5853ec8c8c76cec406e417916ad67536ce4"}, +] + +[package.dependencies] +django = "*" +typing-extensions = "*" + [[package]] name = "docutils" version = "0.20.1" @@ -616,6 +706,26 @@ files = [ [package.dependencies] wcwidth = "*" +[[package]] +name = "psycopg2" +version = "2.9.7" +description = "psycopg2 - Python-PostgreSQL Database Adapter" +optional = false +python-versions = ">=3.6" +files = [ + {file = "psycopg2-2.9.7-cp310-cp310-win32.whl", hash = "sha256:1a6a2d609bce44f78af4556bea0c62a5e7f05c23e5ea9c599e07678995609084"}, + {file = "psycopg2-2.9.7-cp310-cp310-win_amd64.whl", hash = "sha256:b22ed9c66da2589a664e0f1ca2465c29b75aaab36fa209d4fb916025fb9119e5"}, + {file = "psycopg2-2.9.7-cp311-cp311-win32.whl", hash = "sha256:44d93a0109dfdf22fe399b419bcd7fa589d86895d3931b01fb321d74dadc68f1"}, + {file = "psycopg2-2.9.7-cp311-cp311-win_amd64.whl", hash = "sha256:91e81a8333a0037babfc9fe6d11e997a9d4dac0f38c43074886b0d9dead94fe9"}, + {file = "psycopg2-2.9.7-cp37-cp37m-win32.whl", hash = "sha256:d1210fcf99aae6f728812d1d2240afc1dc44b9e6cba526a06fb8134f969957c2"}, + {file = "psycopg2-2.9.7-cp37-cp37m-win_amd64.whl", hash = "sha256:e9b04cbef584310a1ac0f0d55bb623ca3244c87c51187645432e342de9ae81a8"}, + {file = "psycopg2-2.9.7-cp38-cp38-win32.whl", hash = "sha256:d5c5297e2fbc8068d4255f1e606bfc9291f06f91ec31b2a0d4c536210ac5c0a2"}, + {file = "psycopg2-2.9.7-cp38-cp38-win_amd64.whl", hash = "sha256:8275abf628c6dc7ec834ea63f6f3846bf33518907a2b9b693d41fd063767a866"}, + {file = "psycopg2-2.9.7-cp39-cp39-win32.whl", hash = "sha256:c7949770cafbd2f12cecc97dea410c514368908a103acf519f2a346134caa4d5"}, + {file = "psycopg2-2.9.7-cp39-cp39-win_amd64.whl", hash = "sha256:b6bd7d9d3a7a63faae6edf365f0ed0e9b0a1aaf1da3ca146e6b043fb3eb5d723"}, + {file = "psycopg2-2.9.7.tar.gz", hash = "sha256:f00cc35bd7119f1fed17b85bd1007855194dde2cbd8de01ab8ebb17487440ad8"}, +] + [[package]] name = "pycodestyle" version = "2.11.0" @@ -953,6 +1063,22 @@ Sphinx = ">=5" lint = ["docutils-stubs", "flake8", "mypy"] test = ["pytest"] +[[package]] +name = "sqlparse" +version = "0.4.4" +description = "A non-validating SQL parser." +optional = false +python-versions = ">=3.5" +files = [ + {file = "sqlparse-0.4.4-py3-none-any.whl", hash = "sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3"}, + {file = "sqlparse-0.4.4.tar.gz", hash = "sha256:d446183e84b8349fa3061f0fe7f06ca94ba65b426946ffebe6e3e8295332420c"}, +] + +[package.extras] +dev = ["build", "flake8"] +doc = ["sphinx"] +test = ["pytest", "pytest-cov"] + [[package]] name = "termcolor" version = "2.3.0" @@ -978,6 +1104,28 @@ files = [ {file = "tomlkit-0.12.1.tar.gz", hash = "sha256:38e1ff8edb991273ec9f6181244a6a391ac30e9f5098e7535640ea6be97a7c86"}, ] +[[package]] +name = "types-pytz" +version = "2023.3.1.0" +description = "Typing stubs for pytz" +optional = false +python-versions = "*" +files = [ + {file = "types-pytz-2023.3.1.0.tar.gz", hash = "sha256:8e7d2198cba44a72df7628887c90f68a568e1445f14db64631af50c3cab8c090"}, + {file = "types_pytz-2023.3.1.0-py3-none-any.whl", hash = "sha256:a660a38ed86d45970603e4f3b4877c7ba947668386a896fb5d9589c17e7b8407"}, +] + +[[package]] +name = "types-pyyaml" +version = "6.0.12.11" +description = "Typing stubs for PyYAML" +optional = false +python-versions = "*" +files = [ + {file = "types-PyYAML-6.0.12.11.tar.gz", hash = "sha256:7d340b19ca28cddfdba438ee638cd4084bde213e501a3978738543e27094775b"}, + {file = "types_PyYAML-6.0.12.11-py3-none-any.whl", hash = "sha256:a461508f3096d1d5810ec5ab95d7eeecb651f3a15b71959999988942063bf01d"}, +] + [[package]] name = "typing-extensions" version = "4.8.0" @@ -989,15 +1137,26 @@ files = [ {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, ] +[[package]] +name = "tzdata" +version = "2023.3" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +files = [ + {file = "tzdata-2023.3-py2.py3-none-any.whl", hash = "sha256:7e65763eef3120314099b6939b5546db7adce1e7d6f2e179e3df563c70511eda"}, + {file = "tzdata-2023.3.tar.gz", hash = "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a"}, +] + [[package]] name = "urllib3" -version = "2.0.4" +version = "2.0.5" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.7" files = [ - {file = "urllib3-2.0.4-py3-none-any.whl", hash = "sha256:de7df1803967d2c2a98e4b11bb7d6bd9210474c46e8a0401514e3a42a75ebde4"}, - {file = "urllib3-2.0.4.tar.gz", hash = "sha256:8d22f86aae8ef5e410d4f539fde9ce6b2113a001bb4d189e0aed70642d602b11"}, + {file = "urllib3-2.0.5-py3-none-any.whl", hash = "sha256:ef16afa8ba34a1f989db38e1dbbe0c302e4289a47856990d0682e374563ce35e"}, + {file = "urllib3-2.0.5.tar.gz", hash = "sha256:13abf37382ea2ce6fb744d4dad67838eec857c9f4f57009891805e0b5e123594"}, ] [package.extras] @@ -1055,4 +1214,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "6cb4a427f7db629e2f3901f57cf3ab467cc885f105e421d4508fd50199c5afcb" +content-hash = "967a300f6e56903859bf57de51046e3b4a61148fa292662d8da1b7e420727b3c" diff --git a/pyproject.toml b/pyproject.toml index 05796dc..d47c19c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,6 +8,9 @@ readme = "README.md" [tool.poetry.dependencies] python = "^3.11" +django = "^4.2.5" +psycopg2 = "^2.9.7" +django-environ = "^0.11.2" [tool.poetry.group.dev.dependencies] pre-commit = "^3.4.0" @@ -16,6 +19,7 @@ isort = "^5.12.0" commitizen = "^3.9.0" flake8 = "^6.1.0" mypy = "^1.5.1" +django-stubs = {extras = ["compatible-mypy"], version = "^4.2.4"} [tool.poetry.group.docs] optional = true @@ -25,6 +29,14 @@ sphinx = "^7.1.2" sphinxawesome-theme = {version = "^5.0.0b3", allow-prereleases = true} sphinx-design = "^0.5.0" +[tool.black] +color = true +line-length = 79 + +[tool.isort] +profile = "black" +line_length = 79 + [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" diff --git a/server/__init__.py b/server/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/server/asgi.py b/server/asgi.py new file mode 100644 index 0000000..9aa57ab --- /dev/null +++ b/server/asgi.py @@ -0,0 +1,7 @@ +from os import environ + +from django.core.asgi import get_asgi_application + +environ.setdefault("DJANGO_SETTINGS_MODULE", "server.settings.development") + +application = get_asgi_application() diff --git a/server/settings/__init__.py b/server/settings/__init__.py new file mode 100644 index 0000000..0ddd0b5 --- /dev/null +++ b/server/settings/__init__.py @@ -0,0 +1,13 @@ +from os.path import join +from pathlib import Path + +from environ import Env + +BASE_DIR = Path(__file__).parent.parent.parent +APPS_DIR = BASE_DIR / "apps" + +# Load environment variables from config/.env file. See +# https://django-environ.readthedocs.io/en/latest/ + +env = Env() +env.read_env(join(BASE_DIR, "config", ".env")) diff --git a/server/settings/base.py b/server/settings/base.py new file mode 100644 index 0000000..ac2e850 --- /dev/null +++ b/server/settings/base.py @@ -0,0 +1,120 @@ +from os.path import join +from typing import List + +from server.settings import APPS_DIR, BASE_DIR, env + +###################### +# General Settings # +###################### + +SECRET_KEY = env("DJANGO_SECRET_KEY", default="django-secret-key") + +DEBUG = env.bool("DJANGO_DEBUG", default=True) + +# Internationalization +# https://docs.djangoproject.com/en/4.1/topics/i18n/ + +LANGUAGE_CODE = "en-us" + +TIME_ZONE = "UTC" + +USE_I18N = True + +USE_TZ = True + +LOCALE_PATHS = [join(BASE_DIR, "locale")] + +# Default primary key field type +# https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +################# +# Middlewares # +################# + +MIDDLEWARE = [ + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", +] + +############### +# Databases # +############### + +DATABASES = {"default": env.db()} + +########## +# URLs # +########## + +ROOT_URLCONF = "server.urls" + +WSGI_APPLICATION = "server.wsgi.application" + +########## +# Apps # +########## + +DJANGO_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", +] + +THIRD_PARTY_APPS: List[str] = [] + +LOCAL_APPS: List[str] = [] + +INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS + +############### +# Passwords # +############### + +# The list of validators that are used to check the strength of user's +# passwords. See Password validation for more details. +# https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators + +BASE_PASSWORD_VALIDATOR = "django.contrib.auth.password_validation" + +AUTH_PASSWORD_VALIDATORS = [ + {"NAME": f"{BASE_PASSWORD_VALIDATOR}.UserAttributeSimilarityValidator"}, + {"NAME": f"{BASE_PASSWORD_VALIDATOR}.MinimumLengthValidator"}, + {"NAME": f"{BASE_PASSWORD_VALIDATOR}.CommonPasswordValidator"}, + {"NAME": f"{BASE_PASSWORD_VALIDATOR}.NumericPasswordValidator"}, +] + +############### +# Templates # +############### + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [APPS_DIR / "templates"], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] + +################## +# Static files # +################## + +STATIC_URL = "/static/" diff --git a/server/settings/development.py b/server/settings/development.py new file mode 100644 index 0000000..d0cc532 --- /dev/null +++ b/server/settings/development.py @@ -0,0 +1,14 @@ +from server.settings.base import * + +ALLOWED_HOSTS = [ + "localhost", + "0.0.0.0", + "127.0.0.1", +] + +# In development, we don't need a secure password hasher. We can use +# MD5 instead, this is because we don't need to worry about security +# in development. However, we should use a secure password hasher in +# production, like PBKDF2 or Argon2. + +PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"] diff --git a/server/settings/production.py b/server/settings/production.py new file mode 100644 index 0000000..9d52f5b --- /dev/null +++ b/server/settings/production.py @@ -0,0 +1,18 @@ +from server.settings import env +from server.settings.base import * + +###################### +# General Settings # +###################### + +DEBUG = False + +SECRET_KEY = env("DJANGO_SECRET_KEY") + +ALLOWED_HOSTS = env.list("DJANGO_ALLOWED_HOSTS") + +############### +# Databases # +############### + +DATABASES["default"]["CONN_MAX_AGE"] = env.int("CONN_MAX_AGE", default=60) diff --git a/server/urls.py b/server/urls.py new file mode 100644 index 0000000..083932c --- /dev/null +++ b/server/urls.py @@ -0,0 +1,6 @@ +from django.contrib import admin +from django.urls import path + +urlpatterns = [ + path("admin/", admin.site.urls), +] diff --git a/server/wsgi.py b/server/wsgi.py new file mode 100644 index 0000000..aa9ff36 --- /dev/null +++ b/server/wsgi.py @@ -0,0 +1,7 @@ +from os import environ + +from django.core.wsgi import get_wsgi_application + +environ.setdefault("DJANGO_SETTINGS_MODULE", "server.settings") + +application = get_wsgi_application() diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..6d5f68f --- /dev/null +++ b/setup.cfg @@ -0,0 +1,63 @@ +[flake8] +# Flake8 configuration: +# https://flake8.pycqa.org/en/latest/user/configuration.html +max-line-length = 79 +max-doc-length = 72 +indent-size = 4 +statistics = true +show-source = true +exclude = **/migrations/** +per-file-ignores = + server/settings/*.py: F401,F403,F405 + +# Pyflakes configuration: +# https://github.com/PyCQA/pyflakes +doctests = true + +# McCabe configuration: +# https://github.com/PyCQA/mccabe +max-complexity = 5 + +[coverage:run] +# coverage.py configuration: +# https://coverage.readthedocs.io/en/latest/ + +# These files can't be tested, so we exclude them from coverage. +# We also exclude the manage.py file, since it's not a part of the app. +omit = + server/asgi.py + server/wsgi.py + manage.py + +[coverage:report] +# coverage.py configuration: +# https://coverage.readthedocs.io/en/latest/ + +exclude_lines = + if TYPE_CHECKING: + +[mypy] +strict = true +pretty = true +disallow_any_unimported = true +disallow_any_decorated = true +disallow_any_generics = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_defs = true +disallow_incomplete_defs = true +check_untyped_defs = true +disallow_untyped_decorators = true +warn_redundant_casts = true +warn_unused_ignores = true +warn_return_any = true +warn_unreachable = true +show_error_context = true +show_column_numbers = true +show_error_codes = true + +plugins = + mypy_django_plugin.main, + +[mypy.plugins.django-stubs] +django_settings_module = "server.settings"