diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..b3bd934 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,37 @@ +# Python +__pycache__/ +*.py[cod] +*.so +.Python +*.egg-info/ +.installed.cfg +*.egg + +# Django +*.log +db.sqlite3 +media/ + +# Environment +.env +.venv +env/ +venv/ +ENV/ +.envs/ + +# Dev +.git/ +.gitignore +README.md +.flake8 +.pre-commit-config.yaml +docker-compose*.yml + +# IDE +.idea/ +.vscode/ + +# System +.DS_Store +Thumbs.db diff --git a/.envs/dev/.django b/.envs/dev/.django new file mode 100644 index 0000000..ca9c262 --- /dev/null +++ b/.envs/dev/.django @@ -0,0 +1,18 @@ +# ============================================================================= +# GENERAL CONFIGURATION +# ============================================================================= +# Docker and development settings +USE_DOCKER=yes +IPYTHONDIR=/app/.ipython + +# ============================================================================= +# REDIS CONFIGURATION +# ============================================================================= +# Redis connection URL for caching and message broker +REDIS_URL=redis://redis:6379/0 + +# ============================================================================= +# DJANGO CONFIGURATION +# ============================================================================= +# Core Django settings +DJANGO_ADMIN_URL=admin/ diff --git a/.envs/dev/.postgres b/.envs/dev/.postgres new file mode 100644 index 0000000..0dc7306 --- /dev/null +++ b/.envs/dev/.postgres @@ -0,0 +1,7 @@ +# PostgreSQL +# ------------------------------------------------------------------------------ +POSTGRES_HOST=postgres +POSTGRES_PORT=5432 +POSTGRES_DB=telelog +POSTGRES_USER=telelog_user +POSTGRES_PASSWORD=qwe123 diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..2fe1f26 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,85 @@ +# Config for Dependabot updates. See Documentation here: +# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + # Update GitHub actions in workflows + - package-ecosystem: 'github-actions' + directory: '/' + # Every weekday + schedule: + interval: 'daily' + + # Enable version updates for Docker + # We need to specify each Dockerfile in a separate entry because Dependabot doesn't + # support wildcards or recursively checking subdirectories. Check this issue for updates: + # https://github.com/dependabot/dependabot-core/issues/2178 + - package-ecosystem: 'docker' + # Look for a `Dockerfile` in the `compose/local/django` directory + directory: 'compose/local/django/' + # Every weekday + schedule: + interval: 'daily' + # Ignore minor version updates (3.10 -> 3.11) but update patch versions + ignore: + - dependency-name: '*' + update-types: + - 'version-update:semver-major' + - 'version-update:semver-minor' + + - package-ecosystem: 'docker' + # Look for a `Dockerfile` in the `compose/local/node` directory + directory: 'compose/local/node/' + # Every weekday + schedule: + interval: 'daily' + + - package-ecosystem: 'docker' + # Look for a `Dockerfile` in the `compose/production/aws` directory + directory: 'compose/production/aws/' + # Every weekday + schedule: + interval: 'daily' + + - package-ecosystem: 'docker' + # Look for a `Dockerfile` in the `compose/production/django` directory + directory: 'compose/production/django/' + # Every weekday + schedule: + interval: 'daily' + # Ignore minor version updates (3.10 -> 3.11) but update patch versions + ignore: + - dependency-name: '*' + update-types: + - 'version-update:semver-major' + - 'version-update:semver-minor' + + - package-ecosystem: 'docker' + # Look for a `Dockerfile` in the `compose/production/postgres` directory + directory: 'compose/production/postgres/' + # Every weekday + schedule: + interval: 'daily' + + - package-ecosystem: 'docker' + # Look for a `Dockerfile` in the `compose/production/traefik` directory + directory: 'compose/production/traefik/' + # Every weekday + schedule: + interval: 'daily' + + - package-ecosystem: 'docker' + # Look for a `Dockerfile` in the `compose/production/nginx` directory + directory: 'compose/production/nginx/' + # Every weekday + schedule: + interval: 'daily' + + # Enable version updates for Python/Pip - Production + - package-ecosystem: 'pip' + # Look for a `requirements.txt` in the `root` directory + # also 'setup.cfg', 'runtime.txt' and 'requirements/*.txt' + directory: '/' + # Every weekday + schedule: + interval: 'daily' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3891665 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,56 @@ +name: CI + +# Enable Buildkit and let compose use it to speed up image building +env: + DOCKER_BUILDKIT: 1 + COMPOSE_DOCKER_CLI_BUILD: 1 + +on: + pull_request: + branches: [ 'master', 'main' ] + paths-ignore: [ 'docs/**' ] + + push: + branches: [ 'master', 'main' ] + paths-ignore: [ 'docs/**' ] + +concurrency: + group: ${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + linter: + runs-on: ubuntu-latest + steps: + - name: Checkout Code Repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Run pre-commit + uses: pre-commit/action@v3.0.1 + + # With no caching at all the entire ci process takes 3m to complete! + pytest: + runs-on: ubuntu-latest + + steps: + - name: Checkout Code Repository + uses: actions/checkout@v4 + + - name: Build the Stack + run: docker compose -f docker-compose.dev.yml build django + + - name: Check DB Migrations + run: docker compose -f docker-compose.dev.yml run --rm django python manage.py makemigrations --check + + - name: Run DB Migrations + run: docker compose -f docker-compose.dev.yml run --rm django python manage.py migrate + + - name: Run Django Tests + run: docker compose -f docker-compose.dev.yml run django pytest + + - name: Tear down the Stack + run: docker compose -f docker-compose.dev.yml down diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7ddfb40 --- /dev/null +++ b/.gitignore @@ -0,0 +1,49 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Django +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal +media/ +static/ + +# Environment +.env +.venv +env/ +venv/ +ENV/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# Docker +docker-compose.override.yml + +# System +.DS_Store +Thumbs.db diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..a567ed9 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,49 @@ +exclude: '^docs/|/migrations/|devcontainer.json' +default_stages: [pre-commit] + +default_language_version: + python: python3.12 + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-json + - id: check-toml + - id: check-xml + - id: check-yaml + - id: debug-statements + - id: check-builtin-literals + - id: check-case-conflict + - id: check-docstring-first + - id: detect-private-key + + - repo: https://github.com/adamchainz/django-upgrade + rev: '1.22.1' + hooks: + - id: django-upgrade + args: ['--target-version', '5.1'] + + # Run the Ruff linter. + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.8.1 + hooks: + # Linter + - id: ruff + args: [--fix, --exit-non-zero-on-fix] + # Formatter + - id: ruff-format + + - repo: https://github.com/Riverside-Healthcare/djLint + rev: v1.36.1 + hooks: + - id: djlint-reformat-django + - id: djlint-django + +# sets up .pre-commit-ci.yaml to ensure pre-commit dependencies stay up to date +ci: + autoupdate_schedule: weekly + skip: [] + submodules: false diff --git a/README.md b/README.md new file mode 100644 index 0000000..7d1185a --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +# TeleLog - Telegram Authentication for Django + +Django application for secure authentication via Telegram bot. + +## Environment Setup + +1. Update environment files: +``` +.envs/ +├── dev/ +├──── .django # Django core settings +└──── .postgres # Database configuration +``` + +**`.django` configuration:** +- `DJANGO_ADMIN_URL`: Admin panel URL +- `APP_VERSION`: Application version +- `REDIS_URL`: Redis connection URL + +**`.postgres` configuration:** +- Database credentials (host, port, name) +- Default user/password +- Connection settings + +## Quick Start +```bash +docker compose -f docker-compose.dev.yml up -d +``` + +## Stack +- Python 3.12 +- Django 5 +- PostgreSQL 16 +- Redis 6 + +## License +MIT diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..8bb35ed --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,41 @@ +volumes: + telelog_dev_postgres_data: {} + telelog_dev_postgres_data_backups: {} + telelog_dev_redis_data: {} + +services: + django: &django + build: + context: . + dockerfile: ./docker/dev/django/Dockerfile + image: telelog_dev_django + container_name: telelog_dev_django + depends_on: + - postgres + - redis + volumes: + - .:/app:z + env_file: + - ./.envs/dev/.django + - ./.envs/dev/.postgres + ports: + - '8000:8000' + command: /start + + postgres: + build: + context: . + dockerfile: ./docker/dev/postgres/Dockerfile + image: telelog_dev_postgres + container_name: telelog_dev_postgres + volumes: + - telelog_dev_postgres_data:/var/lib/postgresql/data + - telelog_dev_postgres_data_backups:/backups + env_file: + - ./.envs/dev/.postgres + + redis: + image: docker.io/redis:6 + container_name: telelog_dev_redis + volumes: + - telelog_dev_redis_data:/data diff --git a/docker/dev/django/Dockerfile b/docker/dev/django/Dockerfile new file mode 100644 index 0000000..816310e --- /dev/null +++ b/docker/dev/django/Dockerfile @@ -0,0 +1,41 @@ +# define an alias for the specific python version used in this file. +FROM docker.io/python:3.12.7-slim-bookworm + +ARG BUILD_ENVIRONMENT=dev +ARG APP_HOME=/app + +ENV PYTHONUNBUFFERED=1 +ENV PYTHONDONTWRITEBYTECODE=1 +ENV BUILD_ENV=${BUILD_ENVIRONMENT} + +WORKDIR ${APP_HOME} + +ENV PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 + +RUN apt-get update && apt-get install -y \ + build-essential \ + libpq-dev \ + git \ + curl \ + wait-for-it \ + && rm -rf /var/lib/apt/lists/* + +RUN pip install -U uv +ENV UV_PROJECT_ENVIRONMENT="/usr/local/" + +COPY pyproject.toml . +RUN uv sync --extra ${BUILD_ENV} + +COPY ./docker/dev/django/entrypoint /entrypoint +RUN sed -i 's/\r$//g' /entrypoint +RUN chmod +x /entrypoint + +COPY ./docker/dev/django/start /start +RUN sed -i 's/\r$//g' /start +RUN chmod +x /start + +# copy application code to WORKDIR +COPY . ${APP_HOME} + +ENTRYPOINT ["/entrypoint"] diff --git a/docker/dev/django/entrypoint b/docker/dev/django/entrypoint new file mode 100644 index 0000000..fe9a013 --- /dev/null +++ b/docker/dev/django/entrypoint @@ -0,0 +1,17 @@ +#!/bin/bash + +set -o errexit +set -o pipefail +set -o nounset + +if [ -z "${POSTGRES_USER}" ]; then + base_postgres_image_default_user='postgres' + export POSTGRES_USER="${base_postgres_image_default_user}" +fi +export DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}" + +wait-for-it "${POSTGRES_HOST}:${POSTGRES_PORT}" -t 30 + +>&2 echo 'PostgreSQL is available' + +exec "$@" diff --git a/docker/dev/django/start b/docker/dev/django/start new file mode 100644 index 0000000..d6cde1f --- /dev/null +++ b/docker/dev/django/start @@ -0,0 +1,9 @@ +#!/bin/bash + +set -o errexit +set -o pipefail +set -o nounset + + +python src/manage.py migrate +exec python src/manage.py runserver_plus 0.0.0.0:8000 diff --git a/docker/dev/postgres/Dockerfile b/docker/dev/postgres/Dockerfile new file mode 100644 index 0000000..7a7a9f1 --- /dev/null +++ b/docker/dev/postgres/Dockerfile @@ -0,0 +1,6 @@ +FROM docker.io/postgres:16 + +COPY ./docker/dev/postgres/maintenance /usr/local/bin/maintenance +RUN chmod +x /usr/local/bin/maintenance/* +RUN mv /usr/local/bin/maintenance/* /usr/local/bin \ + && rmdir /usr/local/bin/maintenance diff --git a/docker/dev/postgres/maintenance/_sourced/constants.sh b/docker/dev/postgres/maintenance/_sourced/constants.sh new file mode 100644 index 0000000..6ca4f0c --- /dev/null +++ b/docker/dev/postgres/maintenance/_sourced/constants.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + + +BACKUP_DIR_PATH='/backups' +BACKUP_FILE_PREFIX='backup' diff --git a/docker/dev/postgres/maintenance/_sourced/countdown.sh b/docker/dev/postgres/maintenance/_sourced/countdown.sh new file mode 100644 index 0000000..e6cbfb6 --- /dev/null +++ b/docker/dev/postgres/maintenance/_sourced/countdown.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + + +countdown() { + declare desc="A simple countdown. Source: https://superuser.com/a/611582" + local seconds="${1}" + local d=$(($(date +%s) + "${seconds}")) + while [ "$d" -ge `date +%s` ]; do + echo -ne "$(date -u --date @$(($d - `date +%s`)) +%H:%M:%S)\r"; + sleep 0.1 + done +} diff --git a/docker/dev/postgres/maintenance/_sourced/messages.sh b/docker/dev/postgres/maintenance/_sourced/messages.sh new file mode 100644 index 0000000..f6be756 --- /dev/null +++ b/docker/dev/postgres/maintenance/_sourced/messages.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash + + +message_newline() { + echo +} + +message_debug() +{ + echo -e "DEBUG: ${@}" +} + +message_welcome() +{ + echo -e "\e[1m${@}\e[0m" +} + +message_warning() +{ + echo -e "\e[33mWARNING\e[0m: ${@}" +} + +message_error() +{ + echo -e "\e[31mERROR\e[0m: ${@}" +} + +message_info() +{ + echo -e "\e[37mINFO\e[0m: ${@}" +} + +message_suggestion() +{ + echo -e "\e[33mSUGGESTION\e[0m: ${@}" +} + +message_success() +{ + echo -e "\e[32mSUCCESS\e[0m: ${@}" +} diff --git a/docker/dev/postgres/maintenance/_sourced/yes_no.sh b/docker/dev/postgres/maintenance/_sourced/yes_no.sh new file mode 100644 index 0000000..fd9cae1 --- /dev/null +++ b/docker/dev/postgres/maintenance/_sourced/yes_no.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + + +yes_no() { + declare desc="Prompt for confirmation. \$\"\{1\}\": confirmation message." + local arg1="${1}" + + local response= + read -r -p "${arg1} (y/[n])? " response + if [[ "${response}" =~ ^[Yy]$ ]] + then + exit 0 + else + exit 1 + fi +} diff --git a/docker/dev/postgres/maintenance/backup b/docker/dev/postgres/maintenance/backup new file mode 100644 index 0000000..f72304c --- /dev/null +++ b/docker/dev/postgres/maintenance/backup @@ -0,0 +1,38 @@ +#!/usr/bin/env bash + + +### Create a database backup. +### +### Usage: +### $ docker compose -f .yml (exec |run --rm) postgres backup + + +set -o errexit +set -o pipefail +set -o nounset + + +working_dir="$(dirname ${0})" +source "${working_dir}/_sourced/constants.sh" +source "${working_dir}/_sourced/messages.sh" + + +message_welcome "Backing up the '${POSTGRES_DB}' database..." + + +if [[ "${POSTGRES_USER}" == "postgres" ]]; then + message_error "Backing up as 'postgres' user is not supported. Assign 'POSTGRES_USER' env with another one and try again." + exit 1 +fi + +export PGHOST="${POSTGRES_HOST}" +export PGPORT="${POSTGRES_PORT}" +export PGUSER="${POSTGRES_USER}" +export PGPASSWORD="${POSTGRES_PASSWORD}" +export PGDATABASE="${POSTGRES_DB}" + +backup_filename="${BACKUP_FILE_PREFIX}_$(date +'%Y_%m_%dT%H_%M_%S').sql.gz" +pg_dump | gzip > "${BACKUP_DIR_PATH}/${backup_filename}" + + +message_success "'${POSTGRES_DB}' database backup '${backup_filename}' has been created and placed in '${BACKUP_DIR_PATH}'." diff --git a/docker/dev/postgres/maintenance/backups b/docker/dev/postgres/maintenance/backups new file mode 100644 index 0000000..a18937d --- /dev/null +++ b/docker/dev/postgres/maintenance/backups @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + + +### View backups. +### +### Usage: +### $ docker compose -f .yml (exec |run --rm) postgres backups + + +set -o errexit +set -o pipefail +set -o nounset + + +working_dir="$(dirname ${0})" +source "${working_dir}/_sourced/constants.sh" +source "${working_dir}/_sourced/messages.sh" + + +message_welcome "These are the backups you have got:" + +ls -lht "${BACKUP_DIR_PATH}" diff --git a/docker/dev/postgres/maintenance/restore b/docker/dev/postgres/maintenance/restore new file mode 100644 index 0000000..c68f17d --- /dev/null +++ b/docker/dev/postgres/maintenance/restore @@ -0,0 +1,55 @@ +#!/usr/bin/env bash + + +### Restore database from a backup. +### +### Parameters: +### <1> filename of an existing backup. +### +### Usage: +### $ docker compose -f .yml (exec |run --rm) postgres restore <1> + + +set -o errexit +set -o pipefail +set -o nounset + + +working_dir="$(dirname ${0})" +source "${working_dir}/_sourced/constants.sh" +source "${working_dir}/_sourced/messages.sh" + + +if [[ -z ${1+x} ]]; then + message_error "Backup filename is not specified yet it is a required parameter. Make sure you provide one and try again." + exit 1 +fi +backup_filename="${BACKUP_DIR_PATH}/${1}" +if [[ ! -f "${backup_filename}" ]]; then + message_error "No backup with the specified filename found. Check out the 'backups' maintenance script output to see if there is one and try again." + exit 1 +fi + +message_welcome "Restoring the '${POSTGRES_DB}' database from the '${backup_filename}' backup..." + +if [[ "${POSTGRES_USER}" == "postgres" ]]; then + message_error "Restoring as 'postgres' user is not supported. Assign 'POSTGRES_USER' env with another one and try again." + exit 1 +fi + +export PGHOST="${POSTGRES_HOST}" +export PGPORT="${POSTGRES_PORT}" +export PGUSER="${POSTGRES_USER}" +export PGPASSWORD="${POSTGRES_PASSWORD}" +export PGDATABASE="${POSTGRES_DB}" + +message_info "Dropping the database..." +dropdb "${PGDATABASE}" + +message_info "Creating a new database..." +createdb --owner="${POSTGRES_USER}" + +message_info "Applying the backup to the new database..." +gunzip -c "${backup_filename}" | psql "${POSTGRES_DB}" + +message_success "The '${POSTGRES_DB}' database has been restored from the '${backup_filename}' backup." diff --git a/docker/dev/postgres/maintenance/rmbackup b/docker/dev/postgres/maintenance/rmbackup new file mode 100644 index 0000000..fdfd20e --- /dev/null +++ b/docker/dev/postgres/maintenance/rmbackup @@ -0,0 +1,36 @@ +#!/usr/bin/env bash + +### Remove a database backup. +### +### Parameters: +### <1> filename of a backup to remove. +### +### Usage: +### $ docker-compose -f .yml (exec |run --rm) postgres rmbackup <1> + + +set -o errexit +set -o pipefail +set -o nounset + + +working_dir="$(dirname ${0})" +source "${working_dir}/_sourced/constants.sh" +source "${working_dir}/_sourced/messages.sh" + + +if [[ -z ${1+x} ]]; then + message_error "Backup filename is not specified yet it is a required parameter. Make sure you provide one and try again." + exit 1 +fi +backup_filename="${BACKUP_DIR_PATH}/${1}" +if [[ ! -f "${backup_filename}" ]]; then + message_error "No backup with the specified filename found. Check out the 'backups' maintenance script output to see if there is one and try again." + exit 1 +fi + +message_welcome "Removing the '${backup_filename}' backup file..." + +rm -r "${backup_filename}" + +message_success "The '${backup_filename}' database backup has been removed." diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..1b201f4 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,52 @@ +[project] +name = "telelog" +version = "0.1.0" +description = "Telegram Authentication System for Django" +readme = "README.md" +license = { text = "MIT" } +authors = [ + { name = "Denis 🦊 (TheFoxKD)", email = "krishtopadenis@gmail.com" } +] +requires-python = ">=3.12" +dependencies = [ + "django-environ>=0.11.2", + "django>=5.1.3", + "psycopg>=3.2.3", + "python-dotenv>=1.0.1", + "python-telegram-bot>=21.8", + "whitenoise>=6.8.2", + "django-redis>=5.4.0", +] + +classifiers = [ + "Development Status :: 3 - Alpha", + "Framework :: Django", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.12", + "Topic :: Internet :: WWW/HTTP :: Dynamic Content", + "Topic :: Security :: Authentication" +] +keywords = ["django", "telegram", "authentication", "oauth", "bot"] + + +[project.optional-dependencies] +dev = [ + "pytest-django>=4.9.0", + "pytest>=8.3.4", + "ruff>=0.8.1", + "watchdog>=6.0.0", +] +prod = [ + "gunicorn>=23.0.0", +] + +[tool.ruff] +target-version = "py312" + +[dependency-groups] +dev = [ + "django-extensions>=3.2.3", + "werkzeug>=3.1.3", +] diff --git a/src/config/__init__.py b/src/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/config/asgi.py b/src/config/asgi.py new file mode 100644 index 0000000..e54db29 --- /dev/null +++ b/src/config/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for config project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.dev") + +application = get_asgi_application() diff --git a/src/config/settings/__init__.py b/src/config/settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/config/settings/base.py b/src/config/settings/base.py new file mode 100644 index 0000000..398a87a --- /dev/null +++ b/src/config/settings/base.py @@ -0,0 +1,203 @@ +# ruff: noqa: ERA001, E501 +"""Base settings to build other settings files upon.""" + +from pathlib import Path + +import environ + +BASE_DIR = Path(__file__).resolve(strict=True).parent.parent.parent +APPS_DIR = BASE_DIR / "src" +env = environ.Env() + +# GENERAL +# ------------------------------------------------------------------------------ +# https://docs.djangoproject.com/en/dev/ref/settings/#debug +DEBUG = env.bool("DJANGO_DEBUG", False) +# Local time zone. Choices are +# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name +# though not all of them may be available with every OS. +# In Windows, this must be set to your system time zone. +TIME_ZONE = "Europe/Moscow" +# https://docs.djangoproject.com/en/dev/ref/settings/#language-code +LANGUAGE_CODE = "ru-RU" +# https://docs.djangoproject.com/en/dev/ref/settings/#site-id +SITE_ID = 1 +# https://docs.djangoproject.com/en/dev/ref/settings/#use-i18n +USE_I18N = True +# https://docs.djangoproject.com/en/dev/ref/settings/#use-tz +USE_TZ = True + +# DATABASES +# ------------------------------------------------------------------------------ +# https://docs.djangoproject.com/en/dev/ref/settings/#databases +DATABASES = {"default": env.db("DATABASE_URL")} +DATABASES["default"]["ATOMIC_REQUESTS"] = True +# https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-DEFAULT_AUTO_FIELD +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +# URLS +# ------------------------------------------------------------------------------ +# https://docs.djangoproject.com/en/dev/ref/settings/#root-urlconf +ROOT_URLCONF = "config.urls" +# https://docs.djangoproject.com/en/dev/ref/settings/#wsgi-application +WSGI_APPLICATION = "config.wsgi.application" + +# APPS +# ------------------------------------------------------------------------------ +DJANGO_APPS = [ + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.sites", + "django.contrib.messages", + "django.contrib.staticfiles", + # "django.contrib.humanize", # Handy template tags + "django.contrib.admin", + "django.forms", +] +LOCAL_APPS = [ + "core", + "telelog", +] +# https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps +INSTALLED_APPS = DJANGO_APPS + LOCAL_APPS + +# AUTHENTICATION +# ------------------------------------------------------------------------------ +# https://docs.djangoproject.com/en/dev/ref/settings/#authentication-backends +AUTHENTICATION_BACKENDS = ["django.contrib.auth.backends.ModelBackend"] +# https://docs.djangoproject.com/en/dev/ref/settings/#auth-user-model +# AUTH_USER_MODEL = "users.User" + +# PASSWORDS +# ------------------------------------------------------------------------------ +# https://docs.djangoproject.com/en/dev/ref/settings/#password-hashers +PASSWORD_HASHERS = [ + # https://docs.djangoproject.com/en/dev/topics/auth/passwords/#using-argon2-with-django + "django.contrib.auth.hashers.Argon2PasswordHasher", + "django.contrib.auth.hashers.PBKDF2PasswordHasher", + "django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher", + "django.contrib.auth.hashers.BCryptSHA256PasswordHasher", +] +# https://docs.djangoproject.com/en/dev/ref/settings/#auth-password-validators +AUTH_PASSWORD_VALIDATORS = [ + { + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", + }, + {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, + {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, + {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, +] + +# MIDDLEWARE +# ------------------------------------------------------------------------------ +# https://docs.djangoproject.com/en/dev/ref/settings/#middleware +MIDDLEWARE = [ + "django.middleware.security.SecurityMiddleware", + "whitenoise.middleware.WhiteNoiseMiddleware", + "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", +] + +# STATIC +# ------------------------------------------------------------------------------ +# https://docs.djangoproject.com/en/dev/ref/settings/#static-root +STATIC_ROOT = str(BASE_DIR / "staticfiles") +# https://docs.djangoproject.com/en/dev/ref/settings/#static-url +STATIC_URL = "/static/" +# https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#std:setting-STATICFILES_DIRS +STATICFILES_DIRS = [str(APPS_DIR / "static")] +# https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#staticfiles-finders +STATICFILES_FINDERS = [ + "django.contrib.staticfiles.finders.FileSystemFinder", + "django.contrib.staticfiles.finders.AppDirectoriesFinder", +] + +# MEDIA +# ------------------------------------------------------------------------------ +# https://docs.djangoproject.com/en/dev/ref/settings/#media-root +MEDIA_ROOT = str(APPS_DIR / "media") +# https://docs.djangoproject.com/en/dev/ref/settings/#media-url +MEDIA_URL = "/media/" + +# TEMPLATES +# ------------------------------------------------------------------------------ +# https://docs.djangoproject.com/en/dev/ref/settings/#templates +TEMPLATES = [ + { + # https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-TEMPLATES-BACKEND + "BACKEND": "django.template.backends.django.DjangoTemplates", + # https://docs.djangoproject.com/en/dev/ref/settings/#dirs + "DIRS": [str(APPS_DIR / "templates")], + # https://docs.djangoproject.com/en/dev/ref/settings/#app-dirs + "APP_DIRS": True, + "OPTIONS": { + # https://docs.djangoproject.com/en/dev/ref/settings/#template-context-processors + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.template.context_processors.i18n", + "django.template.context_processors.media", + "django.template.context_processors.static", + "django.template.context_processors.tz", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] + +# FIXTURES +# ------------------------------------------------------------------------------ +# https://docs.djangoproject.com/en/dev/ref/settings/#fixture-dirs +FIXTURE_DIRS = (str(APPS_DIR / "fixtures"),) + + +# EMAIL +# ------------------------------------------------------------------------------ +# https://docs.djangoproject.com/en/dev/ref/settings/#email-backend +EMAIL_BACKEND = env( + "DJANGO_EMAIL_BACKEND", + default="django.core.mail.backends.smtp.EmailBackend", +) +# https://docs.djangoproject.com/en/dev/ref/settings/#email-timeout +EMAIL_TIMEOUT = 5 + +# ADMIN +# ------------------------------------------------------------------------------ +# Django Admin URL. +ADMIN_URL = "admin/" +# https://docs.djangoproject.com/en/dev/ref/settings/#admins +# TODO: add ADMINS +ADMINS = [] +# https://docs.djangoproject.com/en/dev/ref/settings/#managers +MANAGERS = ADMINS + +# LOGGING +# ------------------------------------------------------------------------------ +# https://docs.djangoproject.com/en/dev/ref/settings/#logging +# See https://docs.djangoproject.com/en/dev/topics/logging for +# more details on how to customize your logging configuration. +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "verbose": { + "format": "%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s", + }, + }, + "handlers": { + "console": { + "level": "DEBUG", + "class": "logging.StreamHandler", + "formatter": "verbose", + }, + }, + "root": {"level": "INFO", "handlers": ["console"]}, +} + +REDIS_URL = env("REDIS_URL", default="redis://redis:6379/0") diff --git a/src/config/settings/dev.py b/src/config/settings/dev.py new file mode 100644 index 0000000..73c0d0b --- /dev/null +++ b/src/config/settings/dev.py @@ -0,0 +1,49 @@ +# ruff: noqa: E501 +from .base import * # noqa: F403 +from .base import INSTALLED_APPS, REDIS_URL, env + +# GENERAL +# ------------------------------------------------------------------------------ +# https://docs.djangoproject.com/en/dev/ref/settings/#debug +DEBUG = True +# https://docs.djangoproject.com/en/dev/ref/settings/#secret-key +SECRET_KEY = env( + "DJANGO_SECRET_KEY", + default="fake-secret-key", +) +# https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts +ALLOWED_HOSTS = ["localhost", "0.0.0.0", "127.0.0.1"] # noqa: S104 + +# CACHES +# ------------------------------------------------------------------------------ +# https://docs.djangoproject.com/en/dev/ref/settings/#caches +CACHES = { + "default": { + "BACKEND": "django_redis.cache.RedisCache", + "LOCATION": REDIS_URL, + "OPTIONS": { + "CLIENT_CLASS": "django_redis.client.DefaultClient", + # Mimicking memcache behavior. + # https://github.com/jazzband/django-redis#memcached-exceptions-behavior + "IGNORE_EXCEPTIONS": True, + }, + }, +} + +# EMAIL +# ------------------------------------------------------------------------------ +# https://docs.djangoproject.com/en/dev/ref/settings/#email-backend +EMAIL_BACKEND = env( + "DJANGO_EMAIL_BACKEND", + default="django.core.mail.backends.console.EmailBackend", +) + +# WhiteNoise +# ------------------------------------------------------------------------------ +# http://whitenoise.evans.io/en/latest/django.html#using-whitenoise-in-development +INSTALLED_APPS = ["whitenoise.runserver_nostatic", *INSTALLED_APPS] + +# django-extensions +# ------------------------------------------------------------------------------ +# https://django-extensions.readthedocs.io/en/latest/installation_instructions.html#configuration +INSTALLED_APPS += ["django_extensions"] diff --git a/src/config/urls.py b/src/config/urls.py new file mode 100644 index 0000000..912824e --- /dev/null +++ b/src/config/urls.py @@ -0,0 +1,23 @@ +""" +URL configuration for config project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/5.1/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" + +from django.contrib import admin +from django.urls import path + +urlpatterns = [ + path("admin/", admin.site.urls), +] diff --git a/src/config/wsgi.py b/src/config/wsgi.py new file mode 100644 index 0000000..d30ba76 --- /dev/null +++ b/src/config/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for config project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.1/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.dev") + +application = get_wsgi_application() diff --git a/src/core/__init__.py b/src/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/manage.py b/src/manage.py new file mode 100755 index 0000000..bf28fde --- /dev/null +++ b/src/manage.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python +# ruff: noqa +import os +import sys +from pathlib import Path + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.dev") + + try: + from django.core.management import execute_from_command_line + except ImportError: + # The above import may fail for some other reason. Ensure that the + # issue is really that Django is missing to avoid masking other + # exceptions on Python 2. + try: + import django + except ImportError: + 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?" + ) + + raise + + current_path = Path(__file__).parent.resolve() + sys.path.append(str(current_path / "src")) + + execute_from_command_line(sys.argv) diff --git a/src/telelog/__init__.py b/src/telelog/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/telelog/migrations/__init__.py b/src/telelog/migrations/__init__.py new file mode 100644 index 0000000..e69de29