├── .dockerignore ├── .editorconfig ├── .gitattributes ├── .github └── workflows │ ├── codecov.yml │ ├── docs.yml │ ├── main.yml │ ├── release.yml │ ├── ssh-deploy.yml │ └── tests.yml ├── .gitignore ├── .gitlab-ci.yml ├── LICENSE ├── README.md ├── docker-compose.yml ├── docker ├── dev │ ├── env │ │ ├── .db.env │ │ ├── .email.env │ │ └── .env │ ├── redis │ │ ├── redis.conf │ │ └── redis.conf.example │ └── web │ │ ├── Dockerfile │ │ └── entrypoints │ │ ├── entrypoint.sh │ │ └── test.sh ├── modules │ ├── autoheal.yml │ ├── gitlab.runner.yml │ ├── jaeger.yml │ ├── mailpit.yml │ ├── pg_backup.yml │ ├── rabbitmq.yml │ └── runner │ │ └── config.toml └── prod │ ├── db │ └── pg.conf │ ├── env │ ├── .data.env │ ├── .db.env │ ├── .env │ └── .gunicorn.env │ ├── nginx │ ├── certbot.conf │ ├── conf.d │ │ ├── default.conf │ │ └── proxy.conf │ └── nginx.conf │ ├── redis │ └── redis.conf │ └── web │ ├── Dockerfile │ └── entrypoint.sh ├── docs ├── IntegerMultipleChoiceField.md ├── Makefile ├── make.bat ├── readme.md ├── requirements.txt └── source │ ├── changelog.rst │ ├── cicd │ ├── github.rst │ └── gitlab.rst │ ├── conf.py │ ├── environ │ └── project_env.rst │ ├── index.rst │ ├── intro.rst │ ├── modules │ ├── mailhog.rst │ └── nginx.rst │ ├── quick_start.rst │ ├── spelling_wordlist.txt │ └── utils │ ├── decorators │ ├── except_shell.rst │ └── index.rst │ └── utils │ ├── find_dict_in_list.rst │ └── index.rst ├── prod.certbot.yml ├── prod.yml └── web ├── api ├── __init__.py ├── urls.py └── v1 │ ├── __init__.py │ └── urls.py ├── gunicorn.conf.py ├── main ├── __init__.py ├── admin.py ├── apps.py ├── decorators.py ├── factory.py ├── filters.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── wait_for_db.py ├── managers.py ├── middleware.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_set_superuser.py │ └── __init__.py ├── models.py ├── pagination.py ├── static │ └── favicon.ico ├── tasks.py ├── tests │ ├── __init__.py │ ├── test_managements.py │ ├── test_managers.py │ ├── test_middlewares.py │ ├── test_tasks.py │ ├── tests_decorators.py │ └── tests_utils.py ├── urls.py ├── utils.py └── views.py ├── manage.py ├── pyproject.toml ├── setup.cfg ├── src ├── __init__.py ├── additional_settings │ ├── __init__.py │ ├── celery_settings.py │ └── smtp_settings.py ├── asgi.py ├── celery.py ├── requirements │ ├── base.txt │ ├── local.txt │ └── production.txt ├── settings.py ├── settings_dev.py ├── settings_prod.py ├── urls.py └── wsgi.py └── templates ├── 403.html ├── 404.html ├── 500.html ├── admin └── base_site.html └── rest_framework └── login.html /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .cache 3 | .ash_history 4 | .coverage 5 | .vscode 6 | .idea 7 | htmlcov 8 | venv 9 | __pycache__ 10 | backup 11 | node_modules 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Check http://editorconfig.org for more information 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = 2 10 | trim_trailing_whitespace = true 11 | 12 | [*.{py,pyi}] 13 | indent_size = 4 14 | 15 | [Makefile] 16 | indent_style = tab 17 | 18 | [*.md] 19 | trim_trailing_whitespace = false 20 | 21 | [*.rst] 22 | indent_size = 3 23 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior, in case people don't have core.autocrlf set. 2 | * text=auto 3 | 4 | # Declare .sh files that will always have LF line endings on checkout. 5 | *.sh text eol=lf -------------------------------------------------------------------------------- /.github/workflows/codecov.yml: -------------------------------------------------------------------------------- 1 | name: Coverage 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | coverage: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: "Run coverage.py" 13 | run: | 14 | docker-compose build 15 | docker-compose run --entrypoint="" -u root web coverage run manage.py test 16 | docker-compose run --entrypoint="" -u root web coverage xml 17 | - name: "Upload coverage to Codecov" 18 | uses: codecov/codecov-action@v1 19 | with: 20 | fail_ci_if_error: true 21 | token: ${{ secrets.CODECOV_TOKEN }} 22 | directory: ./web/ 23 | 24 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Documentation 2 | on: 3 | push: 4 | 5 | jobs: 6 | run: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Acquire sources 10 | uses: actions/checkout@v2 11 | - name: Setup Python 12 | uses: actions/setup-python@v2 13 | with: 14 | python-version: 3.9 15 | architecture: x64 16 | 17 | - name: Apply caching of dependencies 18 | uses: actions/cache@v2 19 | with: 20 | path: ~/.cache/pip 21 | key: pip-${{ hashFiles('**/requirements-*.txt') }} 22 | 23 | - name: Install dependencies 24 | run: pip install -r docs/requirements.txt 25 | 26 | - name: doc8 style checks 27 | run: doc8 docs/source/ --config web/pyproject.toml 28 | 29 | - name: Generate documentation 30 | run: make -C docs html 31 | 32 | - name: Spelling 33 | run: make -C docs spelling 34 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Docker Image CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | workflow_dispatch: 7 | 8 | env: 9 | DOCKER_IMAGE_REPO: bandirom/django-template 10 | 11 | jobs: 12 | tests: 13 | uses: ./.github/workflows/tests.yml 14 | secrets: inherit 15 | push: 16 | runs-on: ubuntu-latest 17 | needs: [tests] 18 | steps: 19 | - name: Checkout Repo 20 | uses: actions/checkout@v4 21 | - name: Set up Docker Buildx 22 | uses: docker/setup-buildx-action@v3 23 | - name: Login to Docker Hub 24 | uses: docker/login-action@v3 25 | with: 26 | username: ${{ secrets.DOCKER_HUB_USERNAME }} 27 | password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} 28 | - name: Extract metadata (tags, labels) for Docker 29 | id: meta 30 | uses: docker/metadata-action@v5 31 | with: 32 | images: ${{ env.DOCKER_IMAGE_REPO }} 33 | tags: | 34 | type=sha,format=short,prefix= 35 | - name: Build and push 36 | uses: docker/build-push-action@v5 37 | with: 38 | push: ${{ github.event_name != 'pull_request' }} 39 | context: . 40 | tags: | 41 | ${{ steps.meta.outputs.tags }} 42 | ${{ env.DOCKER_IMAGE_REPO }}:latest 43 | labels: ${{ steps.meta.outputs.labels }} 44 | file: docker/prod/web/Dockerfile 45 | cache-from: type=registry,ref=${{ env.DOCKER_IMAGE_REPO }}:latest 46 | cache-to: type=inline 47 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: "Release CI/CD" 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | # release: 8 | # types: [ published ] 9 | 10 | 11 | env: 12 | DOCKER_REPOSITORY: "bandirom/django-template" 13 | 14 | 15 | jobs: 16 | tests: 17 | uses: ./.github/workflows/tests.yml 18 | build: 19 | name: "Release" 20 | runs-on: ubuntu-latest 21 | needs: [tests] 22 | steps: 23 | - name: Checkout Repo 24 | uses: actions/checkout@v4 25 | - name: Login to Docker Hub 26 | uses: docker/login-action@v3 27 | with: 28 | username: ${{ secrets.DOCKER_HUB_USERNAME }} 29 | password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} 30 | - name: Extract metadata (tags, labels) for Docker 31 | id: meta 32 | uses: docker/metadata-action@v5 33 | with: 34 | images: ${{ env.DOCKER_REPOSITORY }} 35 | - name: Set up Docker Buildx 36 | uses: docker/setup-buildx-action@v3 37 | - name: Build and push 38 | uses: docker/build-push-action@v5 39 | with: 40 | context: . 41 | file: docker/prod/web/Dockerfile 42 | push: ${{ github.event_name != 'pull_request' }} 43 | tags: ${{ steps.meta.outputs.tags }} 44 | labels: ${{ steps.meta.outputs.labels }} 45 | deploy: 46 | name: "Deploy" 47 | runs-on: ubuntu-latest 48 | needs: [ build ] 49 | steps: 50 | - name: Checkout Repo 51 | uses: actions/checkout@v4 52 | - name: Login to Docker Hub 53 | uses: docker/login-action@v3 54 | with: 55 | username: ${{ secrets.DOCKER_HUB_USERNAME }} 56 | password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} 57 | - name: Extract metadata (tags, labels) for Docker 58 | id: meta 59 | uses: docker/metadata-action@v5 60 | with: 61 | images: ${{ env.DOCKER_REPOSITORY }} 62 | - name: deploy 63 | run: | 64 | echo "Tags: $DOCKER_REPOSITORY:$DOCKER_METADATA_OUTPUT_VERSION" 65 | docker pull "$DOCKER_REPOSITORY:$DOCKER_METADATA_OUTPUT_VERSION" 66 | -------------------------------------------------------------------------------- /.github/workflows/ssh-deploy.yml: -------------------------------------------------------------------------------- 1 | name: Main 2 | 3 | env: 4 | APP_NAME: "Template" 5 | PROJECT_PATH: "/home/ubuntu/template/" 6 | 7 | on: 8 | # Allows you to run this workflow manually from the Actions tab 9 | workflow_dispatch: 10 | 11 | jobs: 12 | deploy-ssh: 13 | runs-on: ubuntu-latest 14 | needs: ['testing'] 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v2 18 | - name: Install SSH Key 19 | uses: shimataro/ssh-key-action@v2 20 | with: 21 | key: ${{ secrets.AWS_PEM }} 22 | known_hosts: 'empty' 23 | - name: Adding Known Hosts 24 | run: ssh-keyscan -H ${{ secrets.AWS_HOST }} >> ~/.ssh/known_hosts 25 | 26 | - name: Deploy with rsync 27 | run: rsync -az . ${{ secrets.AWS_USER }}@${{ secrets.AWS_HOST }}:${{ env.PROJECT_PATH }} 28 | 29 | - name: Run build in the server 30 | uses: garygrossgarten/github-action-ssh@release 31 | with: 32 | command: | 33 | cd ${{ env.PROJECT_PATH }} 34 | docker-compose -f prod.yml -f prod.dev.yml up -d --build 35 | host: ${{ secrets.AWS_HOST }} 36 | username: ${{ secrets.AWS_USER }} 37 | privateKey: ${{ secrets.AWS_PEM}} 38 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | env: 4 | COVERAGE_THRESHOLD: 95 5 | DOCKER_IMAGE_REPO: bandirom/django-template 6 | 7 | on: 8 | workflow_call: 9 | secrets: 10 | DOCKER_HUB_USERNAME: 11 | required: true 12 | DOCKER_HUB_ACCESS_TOKEN: 13 | required: true 14 | push: 15 | branches-ignore: 16 | - master 17 | tags-ignore: 18 | - v* 19 | 20 | concurrency: 21 | group: ${{ github.workflow }}-${{ github.ref }} 22 | cancel-in-progress: true 23 | 24 | jobs: 25 | build: 26 | name: "Build and Push Dev Image" 27 | runs-on: ubuntu-latest 28 | steps: 29 | - name: Checkout Repo 30 | uses: actions/checkout@v4 31 | - name: Set environment docker tag 32 | id: set-docker-tag 33 | run: | 34 | BRANCH_NAME=$(echo "${GITHUB_REF#refs/heads/}") 35 | echo "DOCKER_TAG=${BRANCH_NAME}" >> $GITHUB_ENV 36 | - name: Set environment docker image 37 | run: echo "DOCKER_IMAGE=${{ env.DOCKER_IMAGE_REPO }}:${{ env.DOCKER_TAG }}" >> $GITHUB_ENV 38 | - name: Set up Docker Buildx 39 | uses: docker/setup-buildx-action@v3 40 | - name: Login to Docker Hub 41 | uses: docker/login-action@v3 42 | with: 43 | username: ${{ secrets.DOCKER_HUB_USERNAME }} 44 | password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} 45 | - name: Build and Push Dev Image with Cache 46 | uses: docker/build-push-action@v6 47 | with: 48 | context: . 49 | push: true 50 | tags: ${{ env.DOCKER_IMAGE }} 51 | file: docker/dev/web/Dockerfile 52 | cache-from: type=registry,ref=${{ env.DOCKER_IMAGE }} 53 | cache-to: type=inline 54 | 55 | test: 56 | name: "Run Tests" 57 | runs-on: ubuntu-latest 58 | needs: [ build ] 59 | env: 60 | SECRET_KEY: dummy 61 | POSTGRES_USER: postgres 62 | POSTGRES_PASSWORD: postgres 63 | POSTGRES_DB: postgres 64 | POSTGRES_HOST: postgres 65 | POSTGRES_PORT: 5432 66 | steps: 67 | - name: Set environment docker tag 68 | run: | 69 | BRANCH_NAME=$(echo "${GITHUB_REF#refs/heads/}") 70 | echo "DOCKER_TAG=${BRANCH_NAME}" >> $GITHUB_ENV 71 | - name: Set environment docker image 72 | run: echo "DOCKER_IMAGE=${{ env.DOCKER_IMAGE_REPO }}:${{ env.DOCKER_TAG }}" >> $GITHUB_ENV 73 | - name: Pull Docker Image 74 | run: docker pull ${{ env.DOCKER_IMAGE }} 75 | - name: Migration Check 76 | run: docker run --entrypoint="" ${{ env.DOCKER_IMAGE }} python manage.py makemigrations --check 77 | - name: Run Tests 78 | run: docker run --entrypoint="" ${{ env.DOCKER_IMAGE }} pytest 79 | services: 80 | postgres: 81 | image: postgres:15.1-alpine 82 | ports: 83 | - 5432:5432 84 | options: >- 85 | --health-cmd pg_isready 86 | --health-interval 10s 87 | --health-timeout 5s 88 | --health-retries 5 89 | env: 90 | POSTGRES_PASSWORD: postgres 91 | 92 | test_isort: 93 | runs-on: ubuntu-latest 94 | steps: 95 | - name: CheckOut Repo 96 | uses: actions/checkout@v4 97 | - name: Run isort 98 | run: | 99 | pip install isort 100 | cd web 101 | isort . --check 102 | 103 | test_black: 104 | runs-on: ubuntu-latest 105 | steps: 106 | - name: CheckOut Repo 107 | uses: actions/checkout@v4 108 | - name: Run black 109 | id: tests 110 | run: | 111 | pip install black 112 | cd web 113 | black . --check 114 | 115 | test_flake8: 116 | runs-on: ubuntu-latest 117 | steps: 118 | - name: CheckOut Repo 119 | uses: actions/checkout@v4 120 | - name: Run Flake8 121 | run: | 122 | pip install flake8 123 | cd web 124 | flake8 . 125 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | web/static/ 3 | Pip* 4 | Pipfile* 5 | __pycache__ 6 | .idea 7 | .vscode 8 | celerybeat* 9 | .local 10 | *.log 11 | *.log.* 12 | .ash_history 13 | .bash_history 14 | .python_history 15 | .coverage 16 | .cache 17 | web/media/* 18 | htmlcov 19 | venv 20 | backup 21 | .crt 22 | .key 23 | build 24 | *.mo 25 | coverage.xml 26 | .runner_system_id 27 | db.sqlite3 28 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | 2 | stages: 3 | - test 4 | - build 5 | 6 | variables: 7 | # use overlays driver for improved performance 8 | DOCKER_DRIVER: overlay2 9 | DOCKER_BUILDKIT: 1 10 | DOCKER_REGISTRY: "" 11 | DOCKER_TAG: $CI_COMMIT_REF_SLUG 12 | 13 | COVERAGE_THRESHOLD: 95 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Nazarii Romanchenko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![GitHub](https://img.shields.io/github/license/bandirom/DjangoTemplateWithDocker?style=plastic) 2 | ![Codecov](https://img.shields.io/codecov/c/gh/bandirom/DjangoTemplateWithDocker?style=plastic) 3 | [![Documentation Status](https://readthedocs.org/projects/djangotemplatewithdocker/badge/?version=latest)](https://djangotemplatewithdocker.readthedocs.io/en/latest/?badge=latest) 4 | [![Docker Image CI](https://github.com/bandirom/DjangoTemplateWithDocker/actions/workflows/main.yml/badge.svg?branch=master)](https://github.com/bandirom/DjangoTemplateWithDocker/actions/workflows/main.yml) 5 | 6 | # Django template in docker with docker-compose 7 | 8 | ### Features of the template: 9 | 10 | #### Project features: 11 | * Docker/Docker-compose environment 12 | * Environment variables 13 | * Separated settings for Dev and Prod django version 14 | * Docker configuration for nginx for 80 and/or 443 ports (dev/stage/prod) (Let's Encrypt certbot) 15 | * Celery worker 16 | * Redis service for caching using socket. Also message broker for queue 17 | * RabbitMQ configuration 18 | * ASGI support 19 | * Linters integration (flake8, black, isort) 20 | * Swagger in Django Admin Panel 21 | * Ready for deploy by one click 22 | * Separated configuration for dev and prod (requirements and settings) 23 | * CI/CD: GitHub Actions 24 | * Redefined default User model (main.models.py) 25 | * Mailpit, Jaeger, RabbitMQ integrations 26 | * Multi-stage build for prod versions 27 | * PostgreSql Backup 28 | 29 | ### How to use: 30 | 31 | #### Clone the repo or click "Use this template" button: 32 | 33 | ```shell 34 | git clone https://github.com/bandirom/django-template.git ./project_name 35 | ``` 36 | 37 | 38 | #### Before running add your superuser email/password and project name in docker/prod/env/.data.env file 39 | 40 | ```dotenv 41 | SUPERUSER_EMAIL=example@email.com 42 | SUPERUSER_PASSWORD=secretp@ssword 43 | PROJECT_TITLE=MyProject 44 | ``` 45 | 46 | #### Run the local develop server: 47 | 48 | ```shell 49 | docker-compose up -d --build 50 | docker-compose logs -f 51 | ``` 52 | 53 | ##### Server will run on 8000 port. You can get access to server by browser [http://localhost:8000](http://localhost:8000) 54 | 55 | Run django commands through exec: 56 | ```shell 57 | docker-compose exec web python manage.py makemigrations 58 | 59 | docker-compose exec web python manage.py shell 60 | ``` 61 | 62 | Get access to the container 63 | ```shell 64 | docker-compose exec web sh 65 | ``` 66 | 67 | ##### For run mail smtp for local development you can use Mailpit service 68 | 69 | * Run Mailpit 70 | ```shell 71 | docker-compose -f docker/modules/mailpit.yml up -d 72 | ``` 73 | 74 | Don't forget to set SMTP mail backend in settings 75 | 76 | ```dotenv 77 | # docker/dev/env/.email.env 78 | EMAIL_HOST= 79 | ``` 80 | 81 | **Where ``:** 82 | * `host.docker.internal` for Window and macOS 83 | * `172.17.0.1` for Linux OS 84 | 85 | --- 86 | 87 | ### Production environment 88 | 89 | If your server under LoadBalancer or nginx with SSL/TLS certificate you can run `prod.yml` configuration 90 | 91 | ```shell 92 | docker-compose -f prod.yml up -d --build 93 | ``` 94 | 95 | #### For set https connection you should have a domain name 96 | **In prod.certbot.yml:** 97 | 98 | Change the envs: 99 | CERTBOT_EMAIL: your real email 100 | ENVSUBST_VARS: list of variables which set in nginx.conf files 101 | APP: value of the variable from list ENVSUBST_VARS 102 | 103 | To set https for 2 and more nginx servers: 104 | 105 | ```dotenv 106 | ENVSUBST_VARS: API 107 | API: api.your-domain.com 108 | ``` 109 | 110 | Run command: 111 | ```shell 112 | docker-compose -f prod.yml -f prod.certbot.yml up -d --build 113 | ``` 114 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | x-variables: &variables 2 | ENV_STAGE: local 3 | 4 | services: 5 | web: 6 | build: 7 | context: . 8 | dockerfile: docker/dev/web/Dockerfile 9 | volumes: 10 | - ./web/:/usr/src/web/:cached 11 | - postgres_socket:/postgres_socket 12 | ports: 13 | - "8000:8000" 14 | environment: 15 | <<: *variables 16 | env_file: 17 | - docker/dev/env/.env 18 | - docker/dev/env/.db.env 19 | - docker/dev/env/.email.env 20 | - docker/prod/env/.data.env 21 | depends_on: [db, redis] 22 | restart: unless-stopped 23 | networks: 24 | - microservice_network 25 | - separated_network 26 | extra_hosts: 27 | - "gateway-host:172.17.0.1" # Linux OS get access from docker container to localhost 28 | # host.docker.internal - For docker in Windows and macOS. No other action is required. 29 | # If You need to connect to Postgresql in localhost, just use host.docker.internal instead of localhost 30 | healthcheck: 31 | test: curl --fail -s http://localhost:8000$$HEALTH_CHECK_URL || exit 1 32 | interval: 1m30s 33 | timeout: 3s 34 | retries: 3 35 | db: 36 | image: postgres:15.1-alpine 37 | restart: unless-stopped 38 | volumes: 39 | - postgres_data:/var/lib/postgresql/data/ 40 | - postgres_socket:/var/run/postgresql/ 41 | env_file: 42 | - docker/dev/env/.db.env 43 | networks: 44 | - separated_network 45 | healthcheck: 46 | test: [ "CMD-SHELL", "pg_isready -U $$POSTGRES_USER" ] 47 | interval: 50s 48 | timeout: 5s 49 | retries: 5 50 | redis: 51 | image: redis:7.0.8-alpine 52 | restart: unless-stopped 53 | volumes: 54 | - redis_data:/data 55 | networks: 56 | - separated_network 57 | healthcheck: 58 | test: [ "CMD", "redis-cli","ping" ] 59 | interval: 1m20s 60 | timeout: 5s 61 | retries: 3 62 | celery: 63 | build: 64 | context: . 65 | dockerfile: docker/dev/web/Dockerfile 66 | entrypoint: "" 67 | command: celery -A src worker --beat -l info 68 | volumes: 69 | - ./web/:/usr/src/web/:cached 70 | - postgres_socket:/postgres_socket 71 | environment: 72 | <<: *variables 73 | env_file: 74 | - docker/dev/env/.env 75 | - docker/dev/env/.db.env 76 | - docker/dev/env/.email.env 77 | - docker/prod/env/.data.env 78 | depends_on: [redis] 79 | restart: unless-stopped 80 | networks: 81 | - separated_network 82 | extra_hosts: 83 | - "gateway-host:172.17.0.1" 84 | 85 | volumes: 86 | postgres_data: 87 | postgres_socket: 88 | redis_data: 89 | 90 | networks: 91 | microservice_network: 92 | driver: bridge 93 | name: local_microservice_network 94 | separated_network: 95 | driver: bridge 96 | -------------------------------------------------------------------------------- /docker/dev/env/.db.env: -------------------------------------------------------------------------------- 1 | POSTGRES_HOST=/postgres_socket 2 | POSTGRES_PORT=5432 3 | POSTGRES_DB=develop 4 | POSTGRES_USER=develop 5 | POSTGRES_PASSWORD=develop 6 | -------------------------------------------------------------------------------- /docker/dev/env/.email.env: -------------------------------------------------------------------------------- 1 | EMAIL_HOST=gateway-host 2 | EMAIL_PORT=1025 3 | EMAIL_USE_TLS=0 4 | EMAIL_USE_SSL=0 5 | DEFAULT_FROM_EMAIL="Localhost " 6 | EMAIL_HOST_USER 7 | EMAIL_HOST_PASSWORD 8 | -------------------------------------------------------------------------------- /docker/dev/env/.env: -------------------------------------------------------------------------------- 1 | SECRET_KEY='g)g$9zy$=!2#^*%o^=s21ev@o-q-iszijbw%-54n%+n=z8*p+n' 2 | DEBUG=1 3 | DJANGO_ALLOWED_HOSTS=localhost,* 4 | 5 | REDIS_URL=redis://redis:6379 6 | CELERY_BROKER_URL=${REDIS_URL}/2 7 | CELERY_RESULT_BACKEND=${REDIS_URL}/2 8 | HEALTH_CHECK_URL=/application/health/ 9 | 10 | ENABLE_SILK=0 11 | ENABLE_DEBUG_TOOLBAR=0 12 | ENABLE_SENTRY=0 13 | -------------------------------------------------------------------------------- /docker/dev/redis/redis.conf: -------------------------------------------------------------------------------- 1 | bind 0.0.0.0 2 | port 6379 3 | unixsocket /redis_socket/redis-server.sock 4 | unixsocketperm 777 5 | -------------------------------------------------------------------------------- /docker/dev/redis/redis.conf.example: -------------------------------------------------------------------------------- 1 | # Redis configuration file example 2 | 3 | # Note on units: when memory size is needed, it is possible to specifiy 4 | # it in the usual form of 1k 5GB 4M and so forth: 5 | # 6 | # 1k => 1000 bytes 7 | # 1kb => 1024 bytes 8 | # 1m => 1000000 bytes 9 | # 1mb => 1024*1024 bytes 10 | # 1g => 1000000000 bytes 11 | # 1gb => 1024*1024*1024 bytes 12 | # 13 | # units are case insensitive so 1GB 1Gb 1gB are all the same. 14 | 15 | # By default Redis does not run as a daemon. Use 'yes' if you need it. 16 | # Note that Redis will write a pid file in /usr/local/var/run/redis.pid when daemonized. 17 | daemonize yes 18 | 19 | # When running daemonized, Redis writes a pid file in /usr/local/var/run/redis.pid by 20 | # default. You can specify a custom pid file location here. 21 | pidfile /usr/local/var/run/redis6380.pid 22 | 23 | # Accept connections on the specified port, default is 6379. 24 | # If port 0 is specified Redis will not listen on a TCP socket. 25 | port 6380 26 | 27 | # If you want you can bind a single interface, if the bind option is not 28 | # specified all the interfaces will listen for incoming connections. 29 | # 30 | # bind 127.0.0.1 31 | 32 | # Specify the path for the unix socket that will be used to listen for 33 | # incoming connections. There is no default, so Redis will not listen 34 | # on a unix socket when not specified. 35 | # 36 | # unixsocket /tmp/redis.sock 37 | # unixsocketperm 755 38 | 39 | # Close the connection after a client is idle for N seconds (0 to disable) 40 | timeout 0 41 | 42 | # Set server verbosity to 'debug' 43 | # it can be one of: 44 | # debug (a lot of information, useful for development/testing) 45 | # verbose (many rarely useful info, but not a mess like the debug level) 46 | # notice (moderately verbose, what you want in production probably) 47 | # warning (only very important / critical messages are logged) 48 | loglevel verbose 49 | 50 | # Specify the log file name. Also 'stdout' can be used to force 51 | # Redis to log on the standard output. Note that if you use standard 52 | # output for logging but daemonize, logs will be sent to /dev/null 53 | logfile stdout 54 | 55 | # To enable logging to the system logger, just set 'syslog-enabled' to yes, 56 | # and optionally update the other syslog parameters to suit your needs. 57 | # syslog-enabled no 58 | 59 | # Specify the syslog identity. 60 | # syslog-ident redis 61 | 62 | # Specify the syslog facility. Must be USER or between LOCAL0-LOCAL7. 63 | # syslog-facility local0 64 | 65 | # Set the number of databases. The default database is DB 0, you can select 66 | # a different one on a per-connection basis using SELECT where 67 | # dbid is a number between 0 and 'databases'-1 68 | databases 16 69 | 70 | ################################ SNAPSHOTTING ################################# 71 | # 72 | # Save the DB on disk: 73 | # 74 | # save 75 | # 76 | # Will save the DB if both the given number of seconds and the given 77 | # number of write operations against the DB occurred. 78 | # 79 | # In the example below the behaviour will be to save: 80 | # after 900 sec (15 min) if at least 1 key changed 81 | # after 300 sec (5 min) if at least 10 keys changed 82 | # after 60 sec if at least 10000 keys changed 83 | # 84 | # Note: you can disable saving at all commenting all the "save" lines. 85 | 86 | save 900 1 87 | save 300 10 88 | save 60 10000 89 | 90 | # Compress string objects using LZF when dump .rdb databases? 91 | # For default that's set to 'yes' as it's almost always a win. 92 | # If you want to save some CPU in the saving child set it to 'no' but 93 | # the dataset will likely be bigger if you have compressible values or keys. 94 | rdbcompression yes 95 | 96 | # The filename where to dump the DB 97 | dbfilename dump6380.rdb 98 | 99 | # The working directory. 100 | # 101 | # The DB will be written inside this directory, with the filename specified 102 | # above using the 'dbfilename' configuration directive. 103 | # 104 | # Also the Append Only File will be created inside this directory. 105 | # 106 | # Note that you must specify a directory here, not a file name. 107 | dir /usr/local/var/db/redis/ 108 | 109 | ################################# REPLICATION ################################# 110 | 111 | # Master-Slave replication. Use slaveof to make a Redis instance a copy of 112 | # another Redis server. Note that the configuration is local to the slave 113 | # so for example it is possible to configure the slave to save the DB with a 114 | # different interval, or to listen to another port, and so on. 115 | # 116 | # slaveof 117 | 118 | # If the master is password protected (using the "requirepass" configuration 119 | # directive below) it is possible to tell the slave to authenticate before 120 | # starting the replication synchronization process, otherwise the master will 121 | # refuse the slave request. 122 | # 123 | # masterauth 124 | 125 | # When a slave lost the connection with the master, or when the replication 126 | # is still in progress, the slave can act in two different ways: 127 | # 128 | # 1) if slave-serve-stale-data is set to 'yes' (the default) the slave will 129 | # still reply to client requests, possibly with out of data data, or the 130 | # data set may just be empty if this is the first synchronization. 131 | # 132 | # 2) if slave-serve-stale data is set to 'no' the slave will reply with 133 | # an error "SYNC with master in progress" to all the kind of commands 134 | # but to INFO and SLAVEOF. 135 | # 136 | slave-serve-stale-data yes 137 | 138 | # Slaves send PINGs to server in a predefined interval. It's possible to change 139 | # this interval with the repl_ping_slave_period option. The default value is 10 140 | # seconds. 141 | # 142 | # repl-ping-slave-period 10 143 | 144 | # The following option sets a timeout for both Bulk transfer I/O timeout and 145 | # master data or ping response timeout. The default value is 60 seconds. 146 | # 147 | # It is important to make sure that this value is greater than the value 148 | # specified for repl-ping-slave-period otherwise a timeout will be detected 149 | # every time there is low traffic between the master and the slave. 150 | # 151 | # repl-timeout 60 152 | 153 | ################################## SECURITY ################################### 154 | 155 | # Require clients to issue AUTH before processing any other 156 | # commands. This might be useful in environments in which you do not trust 157 | # others with access to the host running redis-server. 158 | # 159 | # This should stay commented out for backward compatibility and because most 160 | # people do not need auth (e.g. they run their own servers). 161 | # 162 | # Warning: since Redis is pretty fast an outside user can try up to 163 | # 150k passwords per second against a good box. This means that you should 164 | # use a very strong password otherwise it will be very easy to break. 165 | # 166 | # requirepass foobared 167 | 168 | # Command renaming. 169 | # 170 | # It is possilbe to change the name of dangerous commands in a shared 171 | # environment. For instance the CONFIG command may be renamed into something 172 | # of hard to guess so that it will be still available for internal-use 173 | # tools but not available for general clients. 174 | # 175 | # Example: 176 | # 177 | # rename-command CONFIG b840fc02d524045429941cc15f59e41cb7be6c52 178 | # 179 | # It is also possilbe to completely kill a command renaming it into 180 | # an empty string: 181 | # 182 | # rename-command CONFIG "" 183 | 184 | ################################### LIMITS #################################### 185 | 186 | # Set the max number of connected clients at the same time. By default there 187 | # is no limit, and it's up to the number of file descriptors the Redis process 188 | # is able to open. The special value '0' means no limits. 189 | # Once the limit is reached Redis will close all the new connections sending 190 | # an error 'max number of clients reached'. 191 | # 192 | # maxclients 128 193 | 194 | # Don't use more memory than the specified amount of bytes. 195 | # When the memory limit is reached Redis will try to remove keys with an 196 | # EXPIRE set. It will try to start freeing keys that are going to expire 197 | # in little time and preserve keys with a longer time to live. 198 | # Redis will also try to remove objects from free lists if possible. 199 | # 200 | # If all this fails, Redis will start to reply with errors to commands 201 | # that will use more memory, like SET, LPUSH, and so on, and will continue 202 | # to reply to most read-only commands like GET. 203 | # 204 | # WARNING: maxmemory can be a good idea mainly if you want to use Redis as a 205 | # 'state' server or cache, not as a real DB. When Redis is used as a real 206 | # database the memory usage will grow over the weeks, it will be obvious if 207 | # it is going to use too much memory in the long run, and you'll have the time 208 | # to upgrade. With maxmemory after the limit is reached you'll start to get 209 | # errors for write operations, and this may even lead to DB inconsistency. 210 | # 211 | # maxmemory 212 | 213 | # MAXMEMORY POLICY: how Redis will select what to remove when maxmemory 214 | # is reached? You can select among five behavior: 215 | # 216 | # volatile-lru -> remove the key with an expire set using an LRU algorithm 217 | # allkeys-lru -> remove any key accordingly to the LRU algorithm 218 | # volatile-random -> remove a random key with an expire set 219 | # allkeys->random -> remove a random key, any key 220 | # volatile-ttl -> remove the key with the nearest expire time (minor TTL) 221 | # noeviction -> don't expire at all, just return an error on write operations 222 | # 223 | # Note: with all the kind of policies, Redis will return an error on write 224 | # operations, when there are not suitable keys for eviction. 225 | # 226 | # At the date of writing this commands are: set setnx setex append 227 | # incr decr rpush lpush rpushx lpushx linsert lset rpoplpush sadd 228 | # sinter sinterstore sunion sunionstore sdiff sdiffstore zadd zincrby 229 | # zunionstore zinterstore hset hsetnx hmset hincrby incrby decrby 230 | # getset mset msetnx exec sort 231 | # 232 | # The default is: 233 | # 234 | # maxmemory-policy volatile-lru 235 | 236 | # LRU and minimal TTL algorithms are not precise algorithms but approximated 237 | # algorithms (in order to save memory), so you can select as well the sample 238 | # size to check. For instance for default Redis will check three keys and 239 | # pick the one that was used less recently, you can change the sample size 240 | # using the following configuration directive. 241 | # 242 | # maxmemory-samples 3 243 | 244 | ############################## APPEND ONLY MODE ############################### 245 | 246 | # By default Redis asynchronously dumps the dataset on disk. If you can live 247 | # with the idea that the latest records will be lost if something like a crash 248 | # happens this is the preferred way to run Redis. If instead you care a lot 249 | # about your data and don't want to that a single record can get lost you should 250 | # enable the append only mode: when this mode is enabled Redis will append 251 | # every write operation received in the file appendonly.aof. This file will 252 | # be read on startup in order to rebuild the full dataset in memory. 253 | # 254 | # Note that you can have both the async dumps and the append only file if you 255 | # like (you have to comment the "save" statements above to disable the dumps). 256 | # Still if append only mode is enabled Redis will load the data from the 257 | # log file at startup ignoring the dump.rdb file. 258 | # 259 | # IMPORTANT: Check the BGREWRITEAOF to check how to rewrite the append 260 | # log file in background when it gets too big. 261 | 262 | appendonly no 263 | 264 | # The name of the append only file (default: "appendonly.aof") 265 | # appendfilename appendonly.aof 266 | 267 | # The fsync() call tells the Operating System to actually write data on disk 268 | # instead to wait for more data in the output buffer. Some OS will really flush 269 | # data on disk, some other OS will just try to do it ASAP. 270 | # 271 | # Redis supports three different modes: 272 | # 273 | # no: don't fsync, just let the OS flush the data when it wants. Faster. 274 | # always: fsync after every write to the append only log . Slow, Safest. 275 | # everysec: fsync only if one second passed since the last fsync. Compromise. 276 | # 277 | # The default is "everysec" that's usually the right compromise between 278 | # speed and data safety. It's up to you to understand if you can relax this to 279 | # "no" that will will let the operating system flush the output buffer when 280 | # it wants, for better performances (but if you can live with the idea of 281 | # some data loss consider the default persistence mode that's snapshotting), 282 | # or on the contrary, use "always" that's very slow but a bit safer than 283 | # everysec. 284 | # 285 | # If unsure, use "everysec". 286 | 287 | # appendfsync always 288 | appendfsync everysec 289 | # appendfsync no 290 | 291 | # When the AOF fsync policy is set to always or everysec, and a background 292 | # saving process (a background save or AOF log background rewriting) is 293 | # performing a lot of I/O against the disk, in some Linux configurations 294 | # Redis may block too long on the fsync() call. Note that there is no fix for 295 | # this currently, as even performing fsync in a different thread will block 296 | # our synchronous write(2) call. 297 | # 298 | # In order to mitigate this problem it's possible to use the following option 299 | # that will prevent fsync() from being called in the main process while a 300 | # BGSAVE or BGREWRITEAOF is in progress. 301 | # 302 | # This means that while another child is saving the durability of Redis is 303 | # the same as "appendfsync none", that in pratical terms means that it is 304 | # possible to lost up to 30 seconds of log in the worst scenario (with the 305 | # default Linux settings). 306 | # 307 | # If you have latency problems turn this to "yes". Otherwise leave it as 308 | # "no" that is the safest pick from the point of view of durability. 309 | no-appendfsync-on-rewrite no 310 | 311 | # Automatic rewrite of the append only file. 312 | # Redis is able to automatically rewrite the log file implicitly calling 313 | # BGREWRITEAOF when the AOF log size will growth by the specified percentage. 314 | # 315 | # This is how it works: Redis remembers the size of the AOF file after the 316 | # latest rewrite (or if no rewrite happened since the restart, the size of 317 | # the AOF at startup is used). 318 | # 319 | # This base size is compared to the current size. If the current size is 320 | # bigger than the specified percentage, the rewrite is triggered. Also 321 | # you need to specify a minimal size for the AOF file to be rewritten, this 322 | # is useful to avoid rewriting the AOF file even if the percentage increase 323 | # is reached but it is still pretty small. 324 | # 325 | # Specify a precentage of zero in order to disable the automatic AOF 326 | # rewrite feature. 327 | 328 | auto-aof-rewrite-percentage 100 329 | auto-aof-rewrite-min-size 64mb 330 | 331 | ################################## SLOW LOG ################################### 332 | 333 | # The Redis Slow Log is a system to log queries that exceeded a specified 334 | # execution time. The execution time does not include the I/O operations 335 | # like talking with the client, sending the reply and so forth, 336 | # but just the time needed to actually execute the command (this is the only 337 | # stage of command execution where the thread is blocked and can not serve 338 | # other requests in the meantime). 339 | # 340 | # You can configure the slow log with two parameters: one tells Redis 341 | # what is the execution time, in microseconds, to exceed in order for the 342 | # command to get logged, and the other parameter is the length of the 343 | # slow log. When a new command is logged the oldest one is removed from the 344 | # queue of logged commands. 345 | 346 | # The following time is expressed in microseconds, so 1000000 is equivalent 347 | # to one second. Note that a negative number disables the slow log, while 348 | # a value of zero forces the logging of every command. 349 | slowlog-log-slower-than 10000 350 | 351 | # There is no limit to this length. Just be aware that it will consume memory. 352 | # You can reclaim memory used by the slow log with SLOWLOG RESET. 353 | slowlog-max-len 1024 354 | 355 | ################################ VIRTUAL MEMORY ############################### 356 | 357 | ### WARNING! Virtual Memory is deprecated in Redis 2.4 358 | ### The use of Virtual Memory is strongly discouraged. 359 | 360 | # Virtual Memory allows Redis to work with datasets bigger than the actual 361 | # amount of RAM needed to hold the whole dataset in memory. 362 | # In order to do so very used keys are taken in memory while the other keys 363 | # are swapped into a swap file, similarly to what operating systems do 364 | # with memory pages. 365 | # 366 | # To enable VM just set 'vm-enabled' to yes, and set the following three 367 | # VM parameters accordingly to your needs. 368 | 369 | vm-enabled no 370 | # vm-enabled yes 371 | 372 | # This is the path of the Redis swap file. As you can guess, swap files 373 | # can't be shared by different Redis instances, so make sure to use a swap 374 | # file for every redis process you are running. Redis will complain if the 375 | # swap file is already in use. 376 | # 377 | # The best kind of storage for the Redis swap file (that's accessed at random) 378 | # is a Solid State Disk (SSD). 379 | # 380 | # *** WARNING *** if you are using a shared hosting the default of putting 381 | # the swap file under /tmp is not secure. Create a dir with access granted 382 | # only to Redis user and configure Redis to create the swap file there. 383 | vm-swap-file /tmp/redis.swap 384 | 385 | # vm-max-memory configures the VM to use at max the specified amount of 386 | # RAM. Everything that deos not fit will be swapped on disk *if* possible, that 387 | # is, if there is still enough contiguous space in the swap file. 388 | # 389 | # With vm-max-memory 0 the system will swap everything it can. Not a good 390 | # default, just specify the max amount of RAM you can in bytes, but it's 391 | # better to leave some margin. For instance specify an amount of RAM 392 | # that's more or less between 60 and 80% of your free RAM. 393 | vm-max-memory 0 394 | 395 | # Redis swap files is split into pages. An object can be saved using multiple 396 | # contiguous pages, but pages can't be shared between different objects. 397 | # So if your page is too big, small objects swapped out on disk will waste 398 | # a lot of space. If you page is too small, there is less space in the swap 399 | # file (assuming you configured the same number of total swap file pages). 400 | # 401 | # If you use a lot of small objects, use a page size of 64 or 32 bytes. 402 | # If you use a lot of big objects, use a bigger page size. 403 | # If unsure, use the default :) 404 | vm-page-size 32 405 | 406 | # Number of total memory pages in the swap file. 407 | # Given that the page table (a bitmap of free/used pages) is taken in memory, 408 | # every 8 pages on disk will consume 1 byte of RAM. 409 | # 410 | # The total swap size is vm-page-size * vm-pages 411 | # 412 | # With the default of 32-bytes memory pages and 134217728 pages Redis will 413 | # use a 4 GB swap file, that will use 16 MB of RAM for the page table. 414 | # 415 | # It's better to use the smallest acceptable value for your application, 416 | # but the default is large in order to work in most conditions. 417 | vm-pages 134217728 418 | 419 | # Max number of VM I/O threads running at the same time. 420 | # This threads are used to read/write data from/to swap file, since they 421 | # also encode and decode objects from disk to memory or the reverse, a bigger 422 | # number of threads can help with big objects even if they can't help with 423 | # I/O itself as the physical device may not be able to couple with many 424 | # reads/writes operations at the same time. 425 | # 426 | # The special value of 0 turn off threaded I/O and enables the blocking 427 | # Virtual Memory implementation. 428 | vm-max-threads 4 429 | 430 | ############################### ADVANCED CONFIG ############################### 431 | 432 | # Hashes are encoded in a special way (much more memory efficient) when they 433 | # have at max a given numer of elements, and the biggest element does not 434 | # exceed a given threshold. You can configure this limits with the following 435 | # configuration directives. 436 | hash-max-zipmap-entries 512 437 | hash-max-zipmap-value 64 438 | 439 | # Similarly to hashes, small lists are also encoded in a special way in order 440 | # to save a lot of space. The special representation is only used when 441 | # you are under the following limits: 442 | list-max-ziplist-entries 512 443 | list-max-ziplist-value 64 444 | 445 | # Sets have a special encoding in just one case: when a set is composed 446 | # of just strings that happens to be integers in radix 10 in the range 447 | # of 64 bit signed integers. 448 | # The following configuration setting sets the limit in the size of the 449 | # set in order to use this special memory saving encoding. 450 | set-max-intset-entries 512 451 | 452 | # Similarly to hashes and lists, sorted sets are also specially encoded in 453 | # order to save a lot of space. This encoding is only used when the length and 454 | # elements of a sorted set are below the following limits: 455 | zset-max-ziplist-entries 128 456 | zset-max-ziplist-value 64 457 | 458 | # Active rehashing uses 1 millisecond every 100 milliseconds of CPU time in 459 | # order to help rehashing the main Redis hash table (the one mapping top-level 460 | # keys to values). The hash table implementation redis uses (see dict.c) 461 | # performs a lazy rehashing: the more operation you run into an hash table 462 | # that is rhashing, the more rehashing "steps" are performed, so if the 463 | # server is idle the rehashing is never complete and some more memory is used 464 | # by the hash table. 465 | # 466 | # The default is to use this millisecond 10 times every second in order to 467 | # active rehashing the main dictionaries, freeing memory when possible. 468 | # 469 | # If unsure: 470 | # use "activerehashing no" if you have hard latency requirements and it is 471 | # not a good thing in your environment that Redis can reply form time to time 472 | # to queries with 2 milliseconds delay. 473 | # 474 | # use "activerehashing yes" if you don't have such hard requirements but 475 | # want to free memory asap when possible. 476 | activerehashing yes 477 | 478 | ################################## INCLUDES ################################### 479 | 480 | # Include one or more other config files here. This is useful if you 481 | # have a standard template that goes to all redis server but also need 482 | # to customize a few per-server settings. Include files can include 483 | # other files, so use this wisely. 484 | # 485 | # include /path/to/local.conf 486 | # include /path/to/other.conf 487 | -------------------------------------------------------------------------------- /docker/dev/web/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.12-alpine 2 | 3 | ENV PYTHONDONTWRITEBYTECODE=1 \ 4 | PYTHONUNBUFFERED=1 \ 5 | TZ=Europe/Kiev \ 6 | LANG=C.UTF-8 \ 7 | APP_HOME=/usr/src/web \ 8 | DJANGO_SETTINGS_MODULE=src.settings_dev 9 | 10 | 11 | WORKDIR $APP_HOME 12 | 13 | ARG GID=1000 14 | ARG UID=1000 15 | ARG USER=ubuntu 16 | 17 | RUN apk add --update --no-cache curl postgresql-dev gcc python3-dev musl-dev openssl libffi-dev openssl-dev build-base \ 18 | # install Pillow dependencies 19 | jpeg-dev zlib-dev freetype-dev lcms2-dev openjpeg-dev tiff-dev tk-dev tcl-dev harfbuzz-dev fribidi-dev && \ 20 | pip install --upgrade pip setuptools && \ 21 | addgroup -g $GID -S $USER && \ 22 | adduser -S $USER -G $USER --uid "$UID" 23 | 24 | COPY ./web/setup.cfg ./web/pyproject.toml $APP_HOME/ 25 | COPY ./web/src/requirements ./src/requirements 26 | RUN pip install -e .[local] 27 | 28 | COPY --chown=$USER:$USER ./docker/dev/web/entrypoints / 29 | COPY --chown=$USER:$USER ./web $APP_HOME 30 | 31 | RUN chmod +x /*.sh && \ 32 | mkdir -p $APP_HOME/static && \ 33 | chown -R $USER:$USER $APP_HOME 34 | 35 | ENTRYPOINT ["/entrypoint.sh"] 36 | 37 | CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"] 38 | 39 | EXPOSE 8000 40 | 41 | USER $USER 42 | -------------------------------------------------------------------------------- /docker/dev/web/entrypoints/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | python manage.py wait_for_db 4 | 5 | python manage.py makemigrations 6 | python manage.py migrate 7 | 8 | exec "$@" 9 | -------------------------------------------------------------------------------- /docker/dev/web/entrypoints/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | coverage erase 4 | coverage run manage.py test || exit 5 | coverage report --fail-under="$COVERAGE_THRESHOLD" || exit 6 | coverage xml 7 | -------------------------------------------------------------------------------- /docker/modules/autoheal.yml: -------------------------------------------------------------------------------- 1 | services: 2 | autoheal: 3 | restart: always 4 | image: willfarrell/autoheal 5 | environment: 6 | AUTOHEAL_CONTAINER_LABEL: all 7 | volumes: 8 | - /var/run/docker.sock:/var/run/docker.sock 9 | -------------------------------------------------------------------------------- /docker/modules/gitlab.runner.yml: -------------------------------------------------------------------------------- 1 | services: 2 | runner: 3 | container_name: gitlab-runner 4 | image: gitlab/gitlab-runner:latest 5 | volumes: 6 | - ./runner/:/etc/gitlab-runner 7 | - /var/run/docker.sock:/var/run/docker.sock 8 | restart: unless-stopped 9 | -------------------------------------------------------------------------------- /docker/modules/jaeger.yml: -------------------------------------------------------------------------------- 1 | # OpenTracing helps developers trace a request in a complicated distributed systems 2 | # to figure out bottlenecks and slow components in the request path 3 | # which need optimizations, fixes or deeper debugging 4 | # For using GUI: http://localhost:16686 5 | 6 | services: 7 | jaeger: 8 | image: jaegertracing/all-in-one 9 | ports: 10 | - "5775:5775/udp" 11 | - "6831:6831/udp" 12 | - "6832:6832/udp" 13 | - "5778:5778" 14 | - "16686:16686" 15 | - "14268:14268" 16 | - "9411:9411" 17 | restart: unless-stopped 18 | -------------------------------------------------------------------------------- /docker/modules/mailpit.yml: -------------------------------------------------------------------------------- 1 | # SMTP Server for mail testing 2 | 3 | services: 4 | mailpit: 5 | image: axllent/mailpit 6 | restart: unless-stopped 7 | ports: 8 | - target: 1025 9 | published: 1025 10 | protocol: tcp 11 | mode: host 12 | - target: 8025 13 | published: 8025 14 | protocol: tcp 15 | mode: host 16 | -------------------------------------------------------------------------------- /docker/modules/pg_backup.yml: -------------------------------------------------------------------------------- 1 | services: 2 | pgbackups: 3 | image: prodrigestivill/postgres-backup-local 4 | restart: always 5 | volumes: 6 | - ../../backup:/backups 7 | env_file: 8 | - ../prod/env/.db.env 9 | environment: 10 | POSTGRES_EXTRA_OPTS: "-Z9 --schema=public --blobs" 11 | SCHEDULE: @daily # @every 0h30m00s # @daily 12 | BACKUP_KEEP_DAYS: 7 13 | BACKUP_KEEP_WEEKS: 4 14 | BACKUP_KEEP_MONTHS: 6 15 | HEALTHCHECK_PORT: 81 16 | networks: 17 | - separated_network 18 | 19 | networks: 20 | separated_network: 21 | driver: bridge 22 | -------------------------------------------------------------------------------- /docker/modules/rabbitmq.yml: -------------------------------------------------------------------------------- 1 | # Message Broker RabbitMQ for Celery queues 2 | # In celery: 3 | # CELERY_BROKER_URL=pyamqp://${RABBITMQ_DEFAULT_USER}:${RABBITMQ_DEFAULT_PASS}@rabbitmq/${RABBITMQ_DEFAULT_VHOST} 4 | # CELERY_RESULT_BACKEND=rpc://${RABBITMQ_DEFAULT_USER}:${RABBITMQ_DEFAULT_PASS}@rabbitmq/${RABBITMQ_DEFAULT_VHOST} 5 | 6 | services: 7 | rabbitmq: 8 | image: rabbitmq:3-management 9 | ports: 10 | - "5672:5672" 11 | - "15672:15672" 12 | volumes: 13 | - rabbitmq:/var/lib/rabbitmq 14 | environment: 15 | RABBITMQ_DEFAULT_USER: admin 16 | RABBITMQ_DEFAULT_PASS: password 17 | RABBITMQ_DEFAULT_VHOST: host 18 | networks: 19 | - queue_network 20 | restart: always 21 | healthcheck: 22 | test: rabbitmq-diagnostics -q status 23 | interval: 10s 24 | timeout: 30s 25 | retries: 3 26 | 27 | volumes: 28 | rabbitmq: 29 | 30 | networks: 31 | queue_network: 32 | driver: bridge 33 | -------------------------------------------------------------------------------- /docker/modules/runner/config.toml: -------------------------------------------------------------------------------- 1 | concurrent = 1 2 | check_interval = 0 3 | shutdown_timeout = 0 4 | 5 | [session_server] 6 | session_timeout = 1800 7 | -------------------------------------------------------------------------------- /docker/prod/db/pg.conf: -------------------------------------------------------------------------------- 1 | listen_addresses = '0.0.0.0' 2 | unix_socket_group = '' # (change requires restart) 3 | unix_socket_permissions = 0777 4 | -------------------------------------------------------------------------------- /docker/prod/env/.data.env: -------------------------------------------------------------------------------- 1 | # your superuser email 2 | SUPERUSER_EMAIL=test@test.com 3 | # your superuser password 4 | SUPERUSER_PASSWORD=tester26 5 | # project title 6 | PROJECT_TITLE=Template 7 | 8 | # PostgreSQL engine 9 | SQL_ENGINE=django.db.backends.postgresql 10 | 11 | # If You use https://sentry.io set your DSN URL here. Example SENTRY_DSN=https://eb3f1... 12 | SENTRY_DSN 13 | -------------------------------------------------------------------------------- /docker/prod/env/.db.env: -------------------------------------------------------------------------------- 1 | POSTGRES_HOST=/postgres_socket 2 | POSTGRES_PORT=5432 3 | POSTGRES_DB=develop 4 | POSTGRES_USER=develop 5 | POSTGRES_PASSWORD=develop 6 | -------------------------------------------------------------------------------- /docker/prod/env/.env: -------------------------------------------------------------------------------- 1 | SECRET_KEY=22+sg%5+##a=^-$o58(1q9(^r@cjl-p0r3m^x9@-#i=1qcs2y12asdwqz2341casdf%zx 2 | DEBUG=0 3 | DJANGO_ALLOWED_HOSTS=localhost,* 4 | USE_HTTPS=1 5 | 6 | REDIS_URL=redis://redis:6379 7 | REDIS_UNIX_SOCKET_PATH=/redis_socket/redis-server.sock 8 | REDIS_SOCKET=unix:///redis_socket/redis-server.sock?db=1 9 | CELERY_BROKER_URL=redis+socket:///redis_socket/redis-server.sock?db=2 10 | CELERY_RESULT_BACKEND=redis+socket:///redis_socket/redis-server.sock?db=2 11 | HEALTH_CHECK_URL=/application/health/ 12 | 13 | ENABLE_SENTRY=0 14 | 15 | EMAIL_HOST 16 | EMAIL_PORT 17 | EMAIL_USE_TLS=0 18 | EMAIL_USE_SSL=0 19 | DEFAULT_FROM_EMAIL 20 | EMAIL_HOST_USER 21 | EMAIL_HOST_PASSWORD 22 | -------------------------------------------------------------------------------- /docker/prod/env/.gunicorn.env: -------------------------------------------------------------------------------- 1 | GUNICORN_WORKERS=3 2 | GUNICORN_THREADS=1 3 | GUNICORN_RELOAD=0 4 | GUNICORN_TIMEOUT=30 5 | GUNICORN_KEEP_ALIVE=2 6 | -------------------------------------------------------------------------------- /docker/prod/nginx/certbot.conf: -------------------------------------------------------------------------------- 1 | gzip on; 2 | gzip_min_length 200; 3 | gzip_comp_level 3; 4 | gzip_disable "msie6"; 5 | gzip_types 6 | text/plain 7 | text/css 8 | text/javascript 9 | text/xml 10 | application/javascript 11 | application/x-javascript 12 | application/json 13 | application/xml; 14 | 15 | server_tokens off; 16 | 17 | upstream src { 18 | server localhost:8000; 19 | } 20 | 21 | server { 22 | listen 443 ssl http2; 23 | server_name ${APP}; 24 | ssl_certificate /etc/letsencrypt/live/${APP}/fullchain.pem; 25 | ssl_certificate_key /etc/letsencrypt/live/${APP}/privkey.pem; 26 | 27 | location / { 28 | proxy_pass http://src; 29 | include /etc/nginx/user.conf.d/proxy.conf; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /docker/prod/nginx/conf.d/default.conf: -------------------------------------------------------------------------------- 1 | server_tokens off; 2 | 3 | upstream src { 4 | server unix:/gunicorn_socket/gunicorn.sock fail_timeout=0; 5 | } 6 | 7 | server { 8 | 9 | listen 8000; 10 | access_log off; 11 | error_log /var/log/nginx/error.log warn; 12 | 13 | include /etc/nginx/conf.d/proxy.conf; 14 | 15 | gzip on; 16 | gzip_min_length 200; # bytes 17 | gzip_comp_level 3; # if > 5 = significant impact on the system 18 | gzip_disable "msie6"; 19 | gzip_types 20 | text/plain 21 | text/css 22 | text/javascript 23 | text/xml 24 | application/javascript 25 | application/x-javascript 26 | application/json 27 | application/xml; 28 | 29 | location / { 30 | proxy_pass http://src; 31 | } 32 | 33 | location /swagger/ { 34 | proxy_pass http://src; 35 | } 36 | 37 | location /static/ { 38 | alias /web/static/; 39 | } 40 | 41 | location /media/ { 42 | alias /web/media/; 43 | } 44 | 45 | location = /favicon.ico { 46 | log_not_found off; 47 | alias /web/static/favicon.ico; 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /docker/prod/nginx/conf.d/proxy.conf: -------------------------------------------------------------------------------- 1 | proxy_http_version 1.1; 2 | proxy_set_header Upgrade $http_upgrade; 3 | proxy_set_header Connection "upgrade"; 4 | proxy_redirect off; 5 | 6 | proxy_set_header Host $http_host; 7 | proxy_set_header Referer $http_referer; 8 | 9 | proxy_cache_bypass $http_upgrade; 10 | proxy_set_header X-Real-IP $remote_addr; 11 | proxy_set_header X-NginX-Proxy true; 12 | 13 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 14 | proxy_set_header X-Forwarded-Host $server_name; 15 | proxy_set_header X-Forwarded-Proto https; 16 | proxy_set_header X-Forwarded-Referrer $http_referer; 17 | -------------------------------------------------------------------------------- /docker/prod/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | worker_processes auto; 2 | 3 | error_log /var/log/nginx/error.log warn; 4 | 5 | 6 | events { 7 | worker_connections 1024; 8 | multi_accept on; 9 | use epoll; 10 | } 11 | 12 | 13 | http { 14 | include /etc/nginx/mime.types; 15 | default_type application/octet-stream; 16 | charset utf-8; 17 | 18 | # Configure buffer sizes 19 | client_max_body_size 10m; 20 | client_body_buffer_size 16k; 21 | client_header_buffer_size 1k; 22 | large_client_header_buffers 2 1k; 23 | 24 | sendfile on; 25 | 26 | keepalive_timeout 65; 27 | 28 | include /etc/nginx/conf.d/*.conf; 29 | } 30 | -------------------------------------------------------------------------------- /docker/prod/redis/redis.conf: -------------------------------------------------------------------------------- 1 | bind 0.0.0.0 2 | port 6379 3 | unixsocket /redis_socket/redis-server.sock 4 | unixsocketperm 777 5 | -------------------------------------------------------------------------------- /docker/prod/web/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.12-alpine as builder 2 | ARG WHEELS_PATH=/etc/wheels 3 | 4 | RUN apk add --update --no-cache --virtual .build-deps \ 5 | build-base postgresql-dev gcc python3-dev musl-dev openssl libffi-dev openssl-dev \ 6 | # install Pillow dependencies 7 | jpeg-dev zlib-dev freetype-dev lcms2-dev openjpeg-dev tiff-dev tk-dev tcl-dev harfbuzz-dev fribidi-dev 8 | 9 | COPY ./web/setup.cfg ./web/pyproject.toml ./ 10 | COPY ./web/src/requirements ./src/requirements 11 | 12 | RUN pip wheel --wheel-dir=$WHEELS_PATH .[production] 13 | 14 | 15 | FROM python:3.12-alpine 16 | 17 | ENV PYTHONDONTWRITEBYTECODE=1 \ 18 | PYTHONUNBUFFERED=1 \ 19 | TZ=Europe/Kiev \ 20 | LANG=C.UTF-8 \ 21 | APP_HOME=/web \ 22 | DJANGO_SETTINGS_MODULE=src.settings_prod 23 | 24 | ARG GID=1000 25 | ARG UID=1000 26 | ARG USER=ubuntu 27 | ARG WHEELS_PATH=/etc/wheels 28 | 29 | RUN apk add --update --no-cache --virtual .build-deps postgresql-dev curl nginx && \ 30 | addgroup -g $GID -S $USER && \ 31 | adduser -S $USER -G $USER --uid "$UID" && \ 32 | mkdir -p /gunicorn_socket /redis_socket && \ 33 | chmod -R 777 /gunicorn_socket /redis_socket && \ 34 | chown -R $USER:$USER /gunicorn_socket 35 | 36 | WORKDIR $APP_HOME 37 | 38 | COPY --from=builder $WHEELS_PATH $WHEELS_PATH 39 | COPY ./web/setup.cfg ./web/pyproject.toml ./ 40 | COPY ./web/src/requirements ./src/requirements 41 | RUN pip install --upgrade pip setuptools && \ 42 | pip install --no-build-isolation --no-index --find-links=$WHEELS_PATH --editable .[production] && \ 43 | rm -rf $WHEELS_PATH 44 | 45 | COPY --chown=$USER:$USER ./docker/prod/web/entrypoint.sh / 46 | COPY ./docker/prod/nginx/conf.d /etc/nginx/conf.d 47 | COPY ./docker/prod/nginx/nginx.conf /etc/nginx/nginx.conf 48 | COPY ./web $APP_HOME 49 | 50 | RUN chmod +x /*.sh && \ 51 | mkdir -p media && \ 52 | python manage.py collectstatic --no-input && \ 53 | chown -R $USER:$USER $APP_HOME media && \ 54 | chown -R $USER:$USER /etc/nginx /var/lib/nginx/ /var/log /run/nginx/ 55 | 56 | ENTRYPOINT ["/entrypoint.sh"] 57 | 58 | CMD ["gunicorn", "src.asgi:application"] 59 | 60 | EXPOSE 8000 61 | 62 | USER $USER 63 | -------------------------------------------------------------------------------- /docker/prod/web/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | python manage.py wait_for_db 4 | 5 | python manage.py check --deploy 6 | 7 | python manage.py migrate 8 | 9 | nginx -g 'daemon on;' 10 | 11 | exec "$@" 12 | -------------------------------------------------------------------------------- /docs/IntegerMultipleChoiceField.md: -------------------------------------------------------------------------------- 1 | IntegerMultipleChoiceField(forms.MultipleChoiceField) 2 | ============================== 3 | 4 | Integer Form representation of `MultipleChoiceField` using `models.JsonField()` 5 | 6 | ##### Example: 7 | ```python 8 | 9 | # models.py 10 | from django.db import models 11 | 12 | class YourModel(models.Model): 13 | some_choice_field = models.JSONField(default=list, blank=True) 14 | 15 | # choices.py 16 | from django.db.models import IntegerChoices 17 | 18 | class YourModelChoice(IntegerChoices): 19 | ONE = (1, 'One') 20 | TWO = (2, 'Two') 21 | Three = (3, 'Three') 22 | 23 | 24 | # forms.py 25 | from django import forms 26 | from django.contrib.admin.widgets import FilteredSelectMultiple 27 | 28 | class YourModelForm(forms.ModelForm): 29 | some_choice_field = IntegerMultipleChoiceField( 30 | choices=YourModelChoice.choices, 31 | widget=FilteredSelectMultiple('FieldName', False), 32 | required=False 33 | ) 34 | 35 | class Meta: 36 | model = YourModel 37 | fields = ('id', 'some_choice_field') 38 | 39 | # admin.py 40 | from django.contrib import admin 41 | 42 | @admin.register(YourModel) 43 | class YourModelAdmin(admin.ModelAdmin): 44 | form = YourModelForm 45 | 46 | 47 | ``` 48 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.https://www.sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/readme.md: -------------------------------------------------------------------------------- 1 | * Install dependencies 2 | ```shell 3 | pipenv install -r docs/requirements.txt --dev 4 | ``` 5 | 6 | * doc8 style checks 7 | ```shell 8 | doc8 docs/source/ --config web/pyproject.toml 9 | ``` 10 | 11 | * Generate documentation 12 | ```shell 13 | make -C docs html 14 | ``` 15 | 16 | * Check spelling 17 | ```shell 18 | make -C docs spelling 19 | ``` 20 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | Sphinx==4.3.2 2 | doc8==0.10.1 3 | numpydoc==1.1.0 4 | setuptools-scm==6.3.2 5 | sphinx_rtd_theme==1.0.0 6 | sphinxcontrib-spelling==7.3.1 7 | toml==0.10.2 8 | -------------------------------------------------------------------------------- /docs/source/changelog.rst: -------------------------------------------------------------------------------- 1 | ######### 2 | ChangeLog 3 | ######### 4 | 5 | .. _changelog-head: 6 | 7 | .. _changelog-1.0.0: 8 | 9 | ***** 10 | 1.0.0 11 | ***** 12 | 13 | * docker and docker-compose support. 14 | -------------------------------------------------------------------------------- /docs/source/cicd/github.rst: -------------------------------------------------------------------------------- 1 | 2 | GitHub Actions 3 | ============== 4 | 5 | What is it? 6 | ----------- 7 | 8 | Hello 9 | -------------------------------------------------------------------------------- /docs/source/cicd/gitlab.rst: -------------------------------------------------------------------------------- 1 | 2 | GitLab 3 | ====== 4 | 5 | What is it? 6 | ----------- 7 | 8 | Hello 9 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | # import os 14 | # import sys 15 | # sys.path.insert(0, os.path.abspath('.')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = "Django template" 21 | copyright = "2021, bandirom" 22 | author = "bandirom" 23 | 24 | 25 | # -- General configuration --------------------------------------------------- 26 | 27 | # Add any Sphinx extension module names here, as strings. They can be 28 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 29 | # ones. 30 | extensions = [] 31 | 32 | # Add any paths that contain templates here, relative to this directory. 33 | templates_path = ["_templates"] 34 | 35 | # List of patterns, relative to source directory, that match files and 36 | # directories to ignore when looking for source files. 37 | # This pattern also affects html_static_path and html_extra_path. 38 | exclude_patterns = [] 39 | 40 | 41 | # -- Options for HTML output ------------------------------------------------- 42 | 43 | # The theme to use for HTML and HTML Help pages. See the documentation for 44 | # a list of builtin themes. 45 | # 46 | html_theme = "sphinx_rtd_theme" # alabaster 47 | 48 | # Add any paths that contain custom static files (such as style sheets) here, 49 | # relative to this directory. They are copied after the builtin static files, 50 | # so a file named "default.css" will overwrite the builtin "default.css". 51 | html_static_path = ["_static"] 52 | -------------------------------------------------------------------------------- /docs/source/environ/project_env.rst: -------------------------------------------------------------------------------- 1 | .. code-block:: yaml 2 | 3 | x-variables: &variables 4 | ENV_STAGE: dev 5 | USE_HTTPS: 0 6 | 7 | services: 8 | web: 9 | ports: 10 | - "9000:8000" 11 | environment: 12 | <<: *variables 13 | celery: 14 | environment: 15 | <<: *variables 16 | 17 | networks: 18 | microservice_network: 19 | name: dev_microservice_network 20 | 21 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. Django template documentation master file, created by 2 | sphinx-quickstart on Sat Nov 13 16:16:05 2021. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to Django template's documentation! 7 | =========================================== 8 | 9 | .. include:: intro.rst 10 | 11 | .. toctree:: 12 | :maxdepth: 1 13 | :caption: Quick start 14 | 15 | quick_start 16 | changelog 17 | 18 | .. toctree:: 19 | :maxdepth: 1 20 | :caption: Modules 21 | 22 | MailHog 23 | Nginx Certbot 24 | 25 | .. toctree:: 26 | :maxdepth: 1 27 | :caption: CI/CD 28 | 29 | GitHub 30 | GitLab 31 | 32 | .. toctree:: 33 | :maxdepth: 1 34 | :caption: Utils 35 | 36 | Decorators 37 | Utils 38 | 39 | 40 | 41 | Indices and tables 42 | ================== 43 | 44 | * :ref:`genindex` 45 | * :ref:`modindex` 46 | * :ref:`search` 47 | -------------------------------------------------------------------------------- /docs/source/intro.rst: -------------------------------------------------------------------------------- 1 | **django-template** is a project template based on |django_link| and |drf_link| with |docker_link|. 2 | Everything what you will need for develop included here. 3 | 4 | 5 minutes and you will be ready for develop new projects! 5 | 6 | **Features**: 7 | 8 | #. Based on Python 3.9 (will be updated to 3.10 soon) 9 | #. Docker-compose environment 10 | #. |celery_link| integration 11 | #. |postgres_link| and |redis_link| integrations 12 | #. Nginx and support SSL (TLS) certificates for https connections (Support http2 protocol) 13 | #. CI/CD integrations for Github and Gitlab repositories. 14 | #. Swagger "out-of-box" for OpenApi 15 | 16 | And a lot of another features wait for you! 17 | 18 | 19 | .. |django_link| raw:: html 20 | 21 | Django 22 | 23 | .. |drf_link| raw:: html 24 | 25 | Django Rest Framework 26 | 27 | .. |docker_link| raw:: html 28 | 29 | Docker 30 | 31 | .. |celery_link| raw:: html 32 | 33 | Celery 34 | 35 | .. |postgres_link| raw:: html 36 | 37 | Postgres 38 | 39 | .. |redis_link| raw:: html 40 | 41 | Redis 42 | 43 | -------------------------------------------------------------------------------- /docs/source/modules/mailhog.rst: -------------------------------------------------------------------------------- 1 | MailHog 2 | ======= 3 | MailHog 4 | 5 | What is it? 6 | ----------- 7 | 8 | Here 9 | -------------------------------------------------------------------------------- /docs/source/modules/nginx.rst: -------------------------------------------------------------------------------- 1 | Nginx 2 | ===== 3 | 4 | Info about nginx 5 | 6 | Certbot 7 | ------- 8 | 9 | Here 10 | -------------------------------------------------------------------------------- /docs/source/quick_start.rst: -------------------------------------------------------------------------------- 1 | Quick start! 2 | ============ 3 | 4 | This guide helps to install and run new projects. 5 | 6 | Requirements 7 | ------------ 8 | * Docker with docker-compose 9 | 10 | Usage 11 | ----- 12 | 13 | 14 | # Clone the repository 15 | 16 | .. code-block:: console 17 | 18 | $ git clone https://github.com/bandirom/django-template.git ./project_name 19 | 20 | Before start let's set up superuser email and password (not username) 21 | 22 | Open the project in your favorite IDE and edit :command:`docker/prod/env/.data.env` file. 23 | 24 | Set up variables for superuser:: 25 | 26 | SUPERUSER_EMAIL=example@email.com 27 | SUPERUSER_PASSWORD=secretpassword 28 | PROJECT_TITLE=MyProject 29 | 30 | Run the local project with command: 31 | 32 | .. code-block:: console 33 | 34 | $ docker-compose up -d --build 35 | 36 | .. NOTE:: 37 | 38 | You can run project without :command:`-d (detach)` flag, if you don't need to run server everytime 39 | 40 | .. NOTE:: 41 | 42 | Project will bind 8000 port on your machine. 43 | If you wanna change it, you can do it in :command:`docker-compose.yml` in service :command:`web` 44 | 45 | Let's check the logs of containers (Only if you use (-d) flag): 46 | 47 | .. code-block:: console 48 | 49 | $ docker-compose logs -f 50 | 51 | And visit `http://localhost:8000 `_. 52 | -------------------------------------------------------------------------------- /docs/source/spelling_wordlist.txt: -------------------------------------------------------------------------------- 1 | certbot 2 | gitlab 3 | github 4 | nginx 5 | http 6 | https 7 | utils 8 | integrations 9 | -------------------------------------------------------------------------------- /docs/source/utils/decorators/except_shell.rst: -------------------------------------------------------------------------------- 1 | 2 | Decorator 'except_shell' 3 | -------------------------------------------------------------------------------- /docs/source/utils/decorators/index.rst: -------------------------------------------------------------------------------- 1 | Decorators 2 | ========== 3 | 4 | Except shell 5 | ------------ 6 | 7 | .. include:: except_shell.rst 8 | -------------------------------------------------------------------------------- /docs/source/utils/utils/find_dict_in_list.rst: -------------------------------------------------------------------------------- 1 | 2 | 3 | do something 4 | -------------------------------------------------------------------------------- /docs/source/utils/utils/index.rst: -------------------------------------------------------------------------------- 1 | Utils 2 | ===== 3 | 4 | Find dict in list 5 | ----------------- 6 | 7 | .. include:: find_dict_in_list.rst 8 | 9 | Another utils 10 | ------------- 11 | 12 | here another include 13 | -------------------------------------------------------------------------------- /prod.certbot.yml: -------------------------------------------------------------------------------- 1 | services: 2 | nginx: 3 | image: staticfloat/nginx-certbot 4 | network_mode: host 5 | restart: 'always' 6 | container_name: nginx 7 | volumes: 8 | - letsencrypt:/etc/letsencrypt 9 | - ./docker/prod/nginx/certbot.conf:/etc/nginx/user.conf.d/nginx_template.conf:ro 10 | - ./docker/prod/nginx/conf.d/proxy.conf:/etc/nginx/user.conf.d/proxy.conf:ro 11 | environment: 12 | CERTBOT_EMAIL: your@email.com 13 | # variable names are space-separated 14 | ENVSUBST_VARS: APP 15 | APP: your.domain.com 16 | 17 | 18 | volumes: 19 | letsencrypt: 20 | -------------------------------------------------------------------------------- /prod.yml: -------------------------------------------------------------------------------- 1 | x-variables: &variables 2 | ENV_STAGE: dev 3 | USE_HTTPS: 0 4 | 5 | services: 6 | web: 7 | image: bandirom/django-template:${DOCKER_TAG:-latest} 8 | volumes: 9 | - redis_socket:/redis_socket 10 | - postgres_socket:/postgres_socket 11 | - media_files:/web/media/ 12 | environment: 13 | <<: *variables 14 | ports: 15 | - "8000:8000" 16 | env_file: 17 | - docker/prod/env/.env 18 | - docker/prod/env/.db.env 19 | - docker/prod/env/.gunicorn.env 20 | - docker/prod/env/.data.env 21 | depends_on: [db, redis] 22 | restart: always 23 | networks: 24 | - separated_network 25 | healthcheck: 26 | test: curl --fail -s http://localhost:8000$$HEALTH_CHECK_URL || exit 1 27 | interval: 1m30s 28 | timeout: 3s 29 | retries: 3 30 | extra_hosts: 31 | - "gateway-host:172.17.0.1" 32 | celery: 33 | image: bandirom/django-template:${DOCKER_TAG:-latest} 34 | entrypoint: "" 35 | command: celery -A src worker --beat -l info 36 | env_file: 37 | - docker/prod/env/.env 38 | - docker/prod/env/.db.env 39 | - docker/prod/env/.data.env 40 | environment: 41 | <<: *variables 42 | depends_on: [redis] 43 | restart: always 44 | volumes: 45 | - redis_socket:/redis_socket 46 | - postgres_socket:/postgres_socket 47 | networks: 48 | - separated_network 49 | extra_hosts: 50 | - "gateway-host:172.17.0.1" 51 | db: 52 | image: postgres:15.1-alpine 53 | command: ["-c", "config_file=/etc/postgresql/postgresql.conf"] 54 | volumes: 55 | - ./docker/prod/db/pg.conf:/etc/postgresql/postgresql.conf 56 | - postgres_data:/var/lib/postgresql/data/ 57 | - postgres_socket:/var/run/postgresql/ 58 | env_file: 59 | - docker/prod/env/.db.env 60 | restart: always 61 | networks: 62 | - separated_network 63 | healthcheck: 64 | test: [ "CMD-SHELL", "pg_isready -U $$POSTGRES_USER" ] 65 | interval: 10s 66 | timeout: 5s 67 | retries: 5 68 | redis: 69 | image: redis:7.0.8-alpine 70 | restart: always 71 | command: ["/var/lib/redis/redis.conf"] 72 | volumes: 73 | - ./docker/prod/redis/redis.conf:/var/lib/redis/redis.conf 74 | - redis_data:/data 75 | - redis_socket:/redis_socket 76 | networks: 77 | - separated_network 78 | healthcheck: 79 | test: [ "CMD", "redis-cli","ping" ] 80 | interval: 1m20s 81 | timeout: 5s 82 | retries: 3 83 | 84 | volumes: 85 | postgres_data: 86 | redis_data: 87 | redis_socket: 88 | postgres_socket: 89 | media_files: 90 | 91 | networks: 92 | separated_network: 93 | driver: bridge 94 | -------------------------------------------------------------------------------- /web/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bandirom/django-template/fa36ea9234870b2c05e6c8da7a7c7144a5889622/web/api/__init__.py -------------------------------------------------------------------------------- /web/api/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, path 2 | 3 | app_name = 'api' 4 | 5 | urlpatterns = [ 6 | path('v1/', include('api.v1.urls')), 7 | ] 8 | -------------------------------------------------------------------------------- /web/api/v1/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bandirom/django-template/fa36ea9234870b2c05e6c8da7a7c7144a5889622/web/api/v1/__init__.py -------------------------------------------------------------------------------- /web/api/v1/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, path # noqa: F401 2 | 3 | app_name = 'v1' 4 | 5 | urlpatterns = [] 6 | -------------------------------------------------------------------------------- /web/gunicorn.conf.py: -------------------------------------------------------------------------------- 1 | from multiprocessing import cpu_count 2 | from os import environ 3 | 4 | bind: list = ['unix:/gunicorn_socket/gunicorn.sock'] 5 | 6 | workers: int = int(environ.get('GUNICORN_WORKERS', cpu_count() * 2 + 1)) 7 | 8 | threads: int = int(environ.get('GUNICORN_THREADS', 1)) 9 | 10 | worker_class: str = 'uvicorn.workers.UvicornWorker' 11 | 12 | loglevel: str = 'info' 13 | 14 | reload: bool = bool(environ.get('GUNICORN_RELOAD', 0)) 15 | 16 | # Reload gunicorn worker if request count > max_requests 17 | max_requests: int = 1000 18 | max_requests_jitter: int = 200 19 | 20 | user: int = 1000 21 | group: int = 1000 22 | 23 | timeout: int = int(environ.get('GUNICORN_TIMEOUT', 30)) 24 | 25 | keepalive: int = int(environ.get('GUNICORN_KEEP_ALIVE', 2)) 26 | -------------------------------------------------------------------------------- /web/main/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bandirom/django-template/fa36ea9234870b2c05e6c8da7a7c7144a5889622/web/main/__init__.py -------------------------------------------------------------------------------- /web/main/admin.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.contrib import admin 3 | from django.contrib.auth import get_user_model 4 | from django.contrib.auth.admin import UserAdmin 5 | from django.contrib.auth.models import Group 6 | from django.utils.translation import gettext_lazy as _ 7 | 8 | User = get_user_model() 9 | 10 | 11 | @admin.register(User) 12 | class CustomUserAdmin(UserAdmin): 13 | ordering = ('-id',) 14 | list_display = ('email', 'full_name', 'is_active') 15 | search_fields = ('first_name', 'last_name', 'email') 16 | 17 | fieldsets = ( 18 | (_('Personal info'), {'fields': ('id', 'first_name', 'last_name', 'email')}), 19 | (_('Secrets'), {'fields': ('password',)}), 20 | ( 21 | _('Permissions'), 22 | { 23 | 'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions'), 24 | }, 25 | ), 26 | (_('Important dates'), {'fields': ('last_login', 'date_joined')}), 27 | ) 28 | add_fieldsets = ( 29 | ( 30 | None, 31 | { 32 | 'classes': ('wide',), 33 | 'fields': ('email', 'password1', 'password2'), 34 | }, 35 | ), 36 | ) 37 | readonly_fields = ('id',) 38 | 39 | 40 | title = settings.PROJECT_TITLE 41 | 42 | admin.site.site_title = title 43 | admin.site.site_header = title 44 | admin.site.site_url = '/' 45 | admin.site.index_title = title 46 | 47 | admin.site.unregister(Group) 48 | -------------------------------------------------------------------------------- /web/main/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class MainConfig(AppConfig): 5 | name = 'main' 6 | -------------------------------------------------------------------------------- /web/main/decorators.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from functools import wraps 3 | from timeit import default_timer 4 | from typing import Any, Callable, Iterable, Literal, TypeVar, Union 5 | 6 | from celery.exceptions import TimeoutError 7 | from django.core.cache import cache 8 | from kombu.exceptions import OperationalError 9 | from requests.exceptions import RequestException 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | RT = TypeVar('RT') 14 | 15 | 16 | def cached_result( 17 | cache_key: str, timeout: int = 300, version: Union[int, str] = 1 18 | ) -> Callable[[Callable[..., RT]], Callable[..., RT]]: 19 | def decorator(function: Callable[..., RT]) -> Callable[..., RT]: 20 | @wraps(function) 21 | def wrapper(*args: Any, **kwargs: Any) -> RT: 22 | key = cache.make_key(cache_key, version) 23 | if key in cache: 24 | return cache.get(key) 25 | result = function(*args, **kwargs) 26 | cache.set(key, result, timeout=timeout) 27 | return result 28 | 29 | return wrapper 30 | 31 | return decorator 32 | 33 | 34 | def execution_time(stdout: Literal['console', 'tuple'] = 'console') -> Callable[[Callable[..., RT]], Callable[..., RT]]: 35 | """ 36 | :param stdout: 'console' or 'tuple' 37 | """ 38 | 39 | def decorator(func: Callable[..., RT]) -> Callable[..., RT]: 40 | @wraps(func) 41 | def delta_time(*args: Any, **kwargs: Any) -> RT | tuple[RT, float]: 42 | t1 = default_timer() 43 | data = func(*args, **kwargs) 44 | delta = default_timer() - t1 45 | if stdout == "console": 46 | logger.debug(f"Function: {func.__name__}, Run time: {delta}") 47 | logger.debug(f"Returned data: {data}, Type: {type(data)}") 48 | logger.debug("############ SEPARATING ############") 49 | elif stdout == "tuple": 50 | return data, delta 51 | return data 52 | 53 | return delta_time 54 | 55 | return decorator 56 | 57 | 58 | def except_shell( 59 | errors: Iterable = (Exception,), default_value: Any = None 60 | ) -> Callable[[Callable[..., RT]], Callable[..., RT]]: 61 | def decorator(func: Callable[..., RT]) -> Callable[..., RT]: 62 | @wraps(func) 63 | def wrapper(*args: Any, **kwargs: Any) -> RT: 64 | try: 65 | return func(*args, **kwargs) 66 | except errors as e: 67 | logging.error(e) 68 | return default_value 69 | 70 | return wrapper 71 | 72 | return decorator 73 | 74 | 75 | request_shell = except_shell((RequestException,)) 76 | celery_shell = except_shell((OperationalError, TimeoutError)) 77 | -------------------------------------------------------------------------------- /web/main/factory.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from factory import PostGenerationMethodCall 3 | from factory.django import DjangoModelFactory 4 | 5 | User = get_user_model() 6 | 7 | 8 | class UserFactory(DjangoModelFactory): 9 | password = PostGenerationMethodCall('set_password', 'secret') 10 | 11 | class Meta: 12 | model = User 13 | django_get_or_create = ('email',) 14 | -------------------------------------------------------------------------------- /web/main/filters.py: -------------------------------------------------------------------------------- 1 | from django_filters import rest_framework as filters 2 | 3 | 4 | class ListCharFilter(filters.BaseInFilter, filters.CharFilter): 5 | """Filter for django-filter lib 6 | ListCharFilter return: list[str] 7 | filters.CharFilter return: str 8 | """ 9 | 10 | pass 11 | -------------------------------------------------------------------------------- /web/main/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bandirom/django-template/fa36ea9234870b2c05e6c8da7a7c7144a5889622/web/main/management/__init__.py -------------------------------------------------------------------------------- /web/main/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bandirom/django-template/fa36ea9234870b2c05e6c8da7a7c7144a5889622/web/main/management/commands/__init__.py -------------------------------------------------------------------------------- /web/main/management/commands/wait_for_db.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | 3 | from django.core.management.base import BaseCommand 4 | from django.db import connection 5 | from django.db.utils import OperationalError 6 | 7 | 8 | class Command(BaseCommand): 9 | """Django command that waits for database to be available""" 10 | 11 | def handle(self, *args, **options): 12 | """Handle the command""" 13 | self.stdout.write('Waiting for database...') 14 | db_conn = None 15 | while not db_conn: 16 | try: 17 | connection.ensure_connection() 18 | db_conn = True 19 | except OperationalError as e: # pragma: no cover 20 | self.stdout.write(f'Database unavailable, waiting 1 second... {e}') 21 | sleep(1) 22 | 23 | self.stdout.write(self.style.SUCCESS('Database available!')) 24 | -------------------------------------------------------------------------------- /web/main/managers.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING, Any 2 | 3 | from django.contrib.auth.base_user import BaseUserManager 4 | 5 | if TYPE_CHECKING: 6 | from .models import UserType 7 | 8 | 9 | class UserManager(BaseUserManager): 10 | """ 11 | Custom user model manager where email is the unique identifiers 12 | for authentication instead of usernames. 13 | """ 14 | 15 | def create_user(self, email: str, password: str, **extra_fields: Any) -> 'UserType': 16 | """ 17 | Create and save a User with the given email and password. 18 | """ 19 | _email: str = self.normalize_email(email) 20 | user = self.model(email=_email, **extra_fields) 21 | user.set_password(password) 22 | user.save() 23 | return user 24 | 25 | def create_superuser(self, email: str, password: str, **extra_fields: Any) -> 'UserType': 26 | """ 27 | Create and save a SuperUser with the given email and password. 28 | """ 29 | extra_fields['is_staff'] = True 30 | extra_fields['is_superuser'] = True 31 | extra_fields['is_active'] = True 32 | return self.create_user(email, password, **extra_fields) 33 | -------------------------------------------------------------------------------- /web/main/middleware.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING, Optional 2 | from zoneinfo import ZoneInfo 3 | 4 | from django.conf import settings 5 | from django.http import HttpResponse 6 | from django.utils import timezone 7 | from django.utils.deprecation import MiddlewareMixin 8 | 9 | if TYPE_CHECKING: 10 | from django.http import HttpRequest 11 | 12 | 13 | class HealthCheckMiddleware(MiddlewareMixin): 14 | def process_request(self, request: 'HttpRequest') -> Optional[HttpResponse]: 15 | if request.META['PATH_INFO'] == settings.HEALTH_CHECK_URL: 16 | return HttpResponse('pong') 17 | 18 | 19 | class TimezoneMiddleware: 20 | def __init__(self, get_response) -> None: 21 | self.get_response = get_response 22 | 23 | def __call__(self, request: 'HttpRequest'): 24 | if tzname := request.COOKIES.get(getattr(settings, 'TIMEZONE_COOKIE_NAME', 'timezone')): 25 | timezone.activate(ZoneInfo(tzname)) 26 | else: 27 | timezone.deactivate() 28 | return self.get_response(request) 29 | -------------------------------------------------------------------------------- /web/main/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.5 on 2021-01-23 10:34 2 | 3 | from django.db import migrations, models 4 | import django.utils.timezone 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ('auth', '0012_alter_user_first_name_max_length'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='User', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('password', models.CharField(max_length=128, verbose_name='password')), 21 | ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), 22 | ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), 23 | ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), 24 | ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), 25 | ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), 26 | ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), 27 | ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), 28 | ('email', models.EmailField(max_length=254, unique=True, verbose_name='Email address')), 29 | ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), 30 | ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), 31 | ], 32 | options={ 33 | 'verbose_name': 'User', 34 | 'verbose_name_plural': 'Users', 35 | }, 36 | ), 37 | ] 38 | -------------------------------------------------------------------------------- /web/main/migrations/0002_set_superuser.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.1 on 2020-09-21 08:30 2 | import os 3 | 4 | from django.db import migrations 5 | from django.contrib.auth.hashers import make_password 6 | 7 | 8 | def set_superuser(apps, schema_editor): 9 | if (email := os.getenv('SUPERUSER_EMAIL')) and (password := os.getenv('SUPERUSER_PASSWORD')): 10 | user_obj = apps.get_model('main', 'User') 11 | user = user_obj( 12 | email=email, 13 | first_name='Super', 14 | last_name='Admin', 15 | is_staff=True, 16 | is_active=True, 17 | is_superuser=True, 18 | password=make_password(password) 19 | ) 20 | user.save() 21 | 22 | 23 | class Migration(migrations.Migration): 24 | dependencies = [ 25 | ('main', '0001_initial'), 26 | ] 27 | 28 | operations = [ 29 | migrations.RunPython(set_superuser, migrations.RunPython.noop), 30 | ] 31 | -------------------------------------------------------------------------------- /web/main/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bandirom/django-template/fa36ea9234870b2c05e6c8da7a7c7144a5889622/web/main/migrations/__init__.py -------------------------------------------------------------------------------- /web/main/models.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar 2 | 3 | from django.contrib.auth.models import AbstractUser 4 | from django.db import models 5 | from django.utils.translation import gettext_lazy as _ 6 | 7 | from .managers import UserManager 8 | 9 | UserType = TypeVar('UserType', bound='User') 10 | 11 | 12 | class User(AbstractUser): 13 | username = None # type: ignore 14 | email = models.EmailField(_('Email address'), unique=True) 15 | 16 | USERNAME_FIELD: str = 'email' 17 | REQUIRED_FIELDS: list[str] = [] 18 | 19 | objects = UserManager() # type: ignore 20 | 21 | class Meta: 22 | verbose_name = _('User') 23 | verbose_name_plural = _('Users') 24 | 25 | def __str__(self) -> str: 26 | return self.email 27 | 28 | @property 29 | def full_name(self) -> str: 30 | return super().get_full_name() 31 | -------------------------------------------------------------------------------- /web/main/pagination.py: -------------------------------------------------------------------------------- 1 | from rest_framework.pagination import PageNumberPagination 2 | 3 | 4 | class BasePageNumberPagination(PageNumberPagination): 5 | page_size: int = 10 6 | page_query_param: str = 'page' 7 | max_page_size: int = 100 8 | page_size_query_param: str = 'page_size' 9 | -------------------------------------------------------------------------------- /web/main/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bandirom/django-template/fa36ea9234870b2c05e6c8da7a7c7144a5889622/web/main/static/favicon.ico -------------------------------------------------------------------------------- /web/main/tasks.py: -------------------------------------------------------------------------------- 1 | from smtplib import SMTPRecipientsRefused 2 | from typing import Any, Optional 3 | 4 | from django.core.mail import EmailMultiAlternatives 5 | from django.template import loader 6 | from django.utils.html import strip_tags 7 | from django.utils.translation import activate 8 | 9 | from src import celery_app as app 10 | 11 | 12 | class SendingEmailTaskArgs(app.Task): 13 | autoretry_for = (SMTPRecipientsRefused, ConnectionRefusedError) 14 | retry_kwargs = {'max_retries': 5} 15 | retry_backoff = 5 16 | retry_jitter = True 17 | 18 | 19 | @app.task(name='email.send_information_email', base=SendingEmailTaskArgs) 20 | def send_information_email( 21 | *, 22 | subject: str, 23 | template_name: str, 24 | context: dict, 25 | to_email: list[str] | str, 26 | letter_language: str = 'en', 27 | **kwargs: Optional[Any], 28 | ) -> bool: 29 | """ 30 | :param subject: email subject 31 | :param template_name: template path to email template 32 | :param context: data what will be passed into email 33 | :param to_email: receiver email(s) 34 | :param letter_language: translate letter to selected lang 35 | :param kwargs: from_email, bcc, cc, reply_to and file_path params 36 | """ 37 | activate(letter_language) 38 | _to_email: list[str] = [to_email] if isinstance(to_email, str) else to_email 39 | html_email: str = loader.render_to_string(template_name, context) 40 | 41 | email_message = EmailMultiAlternatives( 42 | subject=subject, 43 | body=strip_tags(html_email), 44 | to=_to_email, 45 | from_email=kwargs.get('from_email'), 46 | bcc=kwargs.get('bcc'), 47 | cc=kwargs.get('cc'), 48 | reply_to=kwargs.get('reply_to'), 49 | ) 50 | email_message.attach_alternative(html_email, 'text/html') 51 | email_message.send() 52 | return True 53 | -------------------------------------------------------------------------------- /web/main/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bandirom/django-template/fa36ea9234870b2c05e6c8da7a7c7144a5889622/web/main/tests/__init__.py -------------------------------------------------------------------------------- /web/main/tests/test_managements.py: -------------------------------------------------------------------------------- 1 | from io import StringIO 2 | 3 | from django.core.management import call_command 4 | from django.test import TestCase 5 | 6 | 7 | class ManagementTest(TestCase): 8 | def test_wait_for_db(self): 9 | out = StringIO() 10 | call_command('wait_for_db', stdout=out) 11 | self.assertIn('Database available!', out.getvalue()) 12 | -------------------------------------------------------------------------------- /web/main/tests/test_managers.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from django.test import TestCase 3 | 4 | User = get_user_model() 5 | 6 | 7 | class UserManagerTest(TestCase): 8 | def test_create_user(self): 9 | user = User.objects.create_user( 10 | email='tom.cruize@test.com', password='password1234', first_name='Tom', last_name='Cruise' 11 | ) 12 | self.assertTrue(user.is_active) 13 | self.assertEqual(str(user), 'tom.cruize@test.com') 14 | self.assertEqual(user.full_name, 'Tom Cruise') 15 | 16 | def test_create_super_user(self): 17 | user = User.objects.create_superuser(email='super_tester@test.com', password='password1234') 18 | self.assertTrue(user.is_active) 19 | self.assertTrue(user.is_staff) 20 | self.assertTrue(user.is_superuser) 21 | -------------------------------------------------------------------------------- /web/main/tests/test_middlewares.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from rest_framework.status import HTTP_200_OK 3 | from rest_framework.test import APITestCase 4 | 5 | 6 | class MiddlewareTest(APITestCase): 7 | def test_health_check_middleware(self): 8 | url = settings.HEALTH_CHECK_URL 9 | response = self.client.get(url) 10 | self.assertEqual(response.status_code, HTTP_200_OK) 11 | self.assertEqual(type(response.content), bytes) 12 | self.assertEqual(response.content.decode('utf-8'), 'pong') 13 | -------------------------------------------------------------------------------- /web/main/tests/test_tasks.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from django.core import mail 4 | from django.test import TestCase, override_settings 5 | 6 | from main import tasks 7 | 8 | locmem_email_backend = override_settings( 9 | EMAIL_BACKEND='django.core.mail.backends.locmem.EmailBackend', 10 | CELERY_TASK_ALWAYS_EAGER=True, 11 | ) 12 | 13 | 14 | class CeleryTaskTestCase(TestCase): 15 | @unittest.skip("Template does not exist") 16 | @locmem_email_backend 17 | def test_send_information_email(self): 18 | data = { 19 | 'subject': 'Test', 20 | 'context': { 21 | 'test123': '456', 22 | }, 23 | 'template_name': 'user_timezone.html', 24 | 'to_email': 'test@test.com', 25 | } 26 | tasks.send_information_email.delay(**data) 27 | self.assertEqual(len(mail.outbox), 1) 28 | -------------------------------------------------------------------------------- /web/main/tests/tests_decorators.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | 3 | from django.contrib.auth import get_user_model 4 | from django.test import TestCase, override_settings 5 | 6 | from main import decorators 7 | 8 | User = get_user_model() 9 | 10 | CACHES = {'default': {'BACKEND': 'django.core.cache.backends.locmem.LocMemCache'}} 11 | 12 | locmem_cache = override_settings(CACHES=CACHES) 13 | 14 | 15 | class DecoratorTest(TestCase): 16 | @classmethod 17 | def setUpTestData(cls): 18 | cls.user = User.objects.create_user(email='test111@test.com', password='test_test_test') 19 | 20 | @decorators.except_shell((User.DoesNotExist,)) 21 | def get_user(self, email): 22 | return User.objects.get(email=email) 23 | 24 | @decorators.execution_time(stdout='console') 25 | def time_measure_console(self): 26 | return 'Hello World' 27 | 28 | @decorators.execution_time(stdout='tuple') 29 | def time_measure_tuple(self): 30 | sleep(1) 31 | return 'Hello World' 32 | 33 | @decorators.execution_time(stdout='tuple') 34 | @decorators.cached_result(cache_key='cache_test', timeout=5) 35 | def cached_result_function(self, sleep_time: int = 2): 36 | sleep(sleep_time) 37 | return 'Result after hard func' 38 | 39 | def test_except_decorator(self): 40 | test_user = self.get_user(email='test111@test.com') 41 | self.assertEqual(test_user, self.user) 42 | self.assertEqual(test_user.email, self.user.email) 43 | non_exist = self.get_user(email='non_exist_user@test.com') 44 | self.assertEqual(non_exist, None) 45 | 46 | def test_execution_time(self): 47 | data = self.time_measure_console() 48 | self.assertEqual(data, 'Hello World') 49 | 50 | data, delta = self.time_measure_tuple() 51 | self.assertEqual(data, 'Hello World') 52 | self.assertGreater(delta, 1, f'Delta: {delta}') 53 | 54 | @locmem_cache 55 | def test_cached_function_result(self): 56 | sleep_time = 2 57 | data, delta = self.cached_result_function(sleep_time) 58 | self.assertEqual(data, 'Result after hard func') 59 | self.assertGreater(delta, sleep_time, f'Delta: {delta}') 60 | data, delta = self.cached_result_function(sleep_time) 61 | self.assertLess(delta, 0.1, f'Delta: {delta}') 62 | -------------------------------------------------------------------------------- /web/main/tests/tests_utils.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase, override_settings 2 | from django.test.client import RequestFactory 3 | 4 | from main import utils 5 | 6 | 7 | class UtilsTestCase(TestCase): 8 | def test_parse_str_with_space(self): 9 | str1 = 'We are the champions' 10 | self.assertEqual(utils.parse_str_with_space(str1), str1) 11 | str2 = ' You are looking great ' 12 | self.assertEqual(utils.parse_str_with_space(str2), 'You are looking great') 13 | str3 = ' This double life you lead is eating you up from within ' 14 | self.assertEqual(utils.parse_str_with_space(str3), 'This double life you lead is eating you up from within') 15 | 16 | def test_find_dict_in_list(self): 17 | list_1 = [ 18 | { 19 | 'key1': 'Test1', 20 | 'key2': 'test2', 21 | }, 22 | { 23 | 'key1': 'Value2', 24 | 'key2': 'Valey3', 25 | }, 26 | { 27 | 'key1': 1, 28 | 'key2': False, 29 | }, 30 | { 31 | 'key1': 100500, 32 | 'key2': ['test1'], 33 | }, 34 | ] 35 | result = utils.find_dict_in_list(target=list_1, dict_key='key1', lookup_value='Test1') 36 | self.assertEqual(result, list_1[0]) 37 | result = utils.find_dict_in_list(target=list_1, dict_key='key2', lookup_value='test2') 38 | self.assertEqual(result, list_1[0]) 39 | result = utils.find_dict_in_list(target=list_1, dict_key='key1', lookup_value=100500) 40 | self.assertEqual(result, list_1[3]) 41 | result = utils.find_dict_in_list(target=list_1, dict_key='key2', lookup_value=['test1']) 42 | self.assertEqual(result, list_1[3]) 43 | result = utils.find_dict_in_list(target=list_1, dict_key='key2', lookup_value=False) 44 | self.assertEqual(result, list_1[2]) 45 | 46 | @override_settings( 47 | LANGUAGES=( 48 | ('en', 'English'), 49 | ('fr', 'French'), 50 | ('uk', 'Ukrainian'), 51 | ) 52 | ) 53 | def test_supported_languages(self): 54 | factory = RequestFactory() 55 | request = factory.get('/') 56 | self.assertEqual(utils.get_supported_user_language(request), 'en') 57 | request.META['HTTP_ACCEPT_LANGUAGE'] = 'uk' 58 | self.assertEqual(utils.get_supported_user_language(request), 'uk') 59 | request.META['HTTP_ACCEPT_LANGUAGE'] = 'ru;q=0.9,en-US;q=0.8,en;q=0.7,ru-RU;q=0.6' 60 | self.assertEqual(utils.get_supported_user_language(request), 'en') 61 | -------------------------------------------------------------------------------- /web/main/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.decorators import login_required 2 | from django.urls import path 3 | from django.views.generic import RedirectView 4 | 5 | urlpatterns = [ 6 | path('', login_required(RedirectView.as_view(pattern_name='admin:index'))), 7 | ] 8 | -------------------------------------------------------------------------------- /web/main/utils.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from django.utils.translation import get_language_from_request 4 | 5 | 6 | def parse_str_with_space(var: str) -> str: 7 | """return string without multiply whitespaces 8 | Example: var = 'My name is John ' 9 | Return var = 'My name is John' 10 | """ 11 | str_list = list(filter(None, var.split(' '))) 12 | return ' '.join(x for x in str_list) 13 | 14 | 15 | def find_dict_in_list(target: list[dict], dict_key: str | int, lookup_value: Any) -> dict: 16 | """Find a dict in a list of dict by dict key""" 17 | return next(iter(x for x in target if x.get(dict_key) == lookup_value), {}) 18 | 19 | 20 | def get_supported_user_language(request) -> str: 21 | return get_language_from_request(request) 22 | -------------------------------------------------------------------------------- /web/main/views.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING 2 | 3 | from drf_spectacular.utils import extend_schema 4 | from rest_framework.permissions import AllowAny 5 | from rest_framework.renderers import JSONRenderer, TemplateHTMLRenderer 6 | from rest_framework.response import Response 7 | from rest_framework.views import APIView 8 | 9 | if TYPE_CHECKING: 10 | from rest_framework.request import Request 11 | 12 | 13 | class TemplateAPIView(APIView): 14 | """Help to build CMS System using DRF, JWT and Cookies 15 | path('some-path/', TemplateAPIView.as_view(template_name='template.html')) 16 | """ 17 | 18 | permission_classes = (AllowAny,) 19 | renderer_classes = (JSONRenderer, TemplateHTMLRenderer) 20 | template_name: str = '' 21 | 22 | @extend_schema(exclude=True) 23 | def get(self, request: 'Request', *args, **kwargs): 24 | return Response() 25 | -------------------------------------------------------------------------------- /web/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'src.settings') 9 | try: 10 | from django.core.management import execute_from_command_line 11 | except ImportError as exc: 12 | raise ImportError( 13 | "Couldn't import Django. Are you sure it's installed and " 14 | "available on your PYTHONPATH environment variable? Did you " 15 | "forget to activate a virtual environment?" 16 | ) from exc 17 | execute_from_command_line(sys.argv) 18 | 19 | 20 | if __name__ == '__main__': 21 | main() 22 | -------------------------------------------------------------------------------- /web/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "Django Template" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["Nazarii "] 6 | 7 | [tool.poetry.dependencies] 8 | python = "^3.12" 9 | 10 | 11 | [build-system] 12 | requires = ["setuptools>=64.0.0"] 13 | build-backend = "setuptools.build_meta" 14 | 15 | [tool.pytest.ini_options] 16 | norecursedirs = [".git ", "node_modules", "venv"] 17 | addopts = "--tb=short --nomigrations -s" 18 | python_files = "*test*.py" 19 | python_classes = "*Test*" 20 | DJANGO_SETTINGS_MODULE = "src.settings_prod" 21 | log_cli = "true" 22 | log_cli_level = "info" 23 | 24 | [tool.coverage.run] 25 | omit = [ 26 | "*/migrations/*", 27 | "src/settings*", 28 | "manage.py", 29 | "*/apps.py", 30 | ] 31 | 32 | [tool.coverage.report] 33 | exclude_lines = [ 34 | "^\\s*@(abc.)?abstractmethod", 35 | "^\\s*@(typing.)?overload", 36 | "^\\s*if (typing.)?TYPE_CHECKING:", 37 | "^\\s*if (settings.)?DEBUG:", 38 | "pragma: no ?cover", 39 | "def __repr__", 40 | "def __str__", 41 | "if self.debug:", 42 | "raise AssertionError", 43 | "raise NotImplementedError", 44 | "if __name__ == .__main__.:", 45 | ] 46 | 47 | [tool.coverage.xml] 48 | output = "coverage.xml" 49 | 50 | [tool.mypy] 51 | python_version = "3.10" 52 | cache_dir = ".cache/mypy" 53 | exclude = [ 54 | "tests" 55 | ] 56 | disallow_untyped_defs = true 57 | plugins = [ 58 | "mypy_django_plugin.main", 59 | "mypy_drf_plugin.main" 60 | ] 61 | 62 | [[tool.mypy.overrides]] 63 | module = ["*.migrations.*", "manage"] 64 | ignore_errors = true 65 | 66 | [[tool.mypy.overrides]] 67 | module = ['celery.*', 'django_filters', 'kombu.*', 'drf_yasg.*', 'factory.*', 'src.additional_settings.*'] 68 | ignore_missing_imports = true 69 | 70 | 71 | [tool.django-stubs] 72 | django_settings_module = "src.settings_prod" 73 | ignore_missing_model_attributes = true 74 | 75 | [tool.doc8] 76 | max_line_length = 120 77 | 78 | [tool.black] 79 | line-length = 120 80 | extend-exclude = "migrations" 81 | skip-string-normalization = true 82 | 83 | [tool.isort] 84 | multi_line_output = 3 85 | skip = ["migrations", "venv"] 86 | line_length = 120 87 | include_trailing_comma = true 88 | profile = "black" 89 | known_third_party = "celery" 90 | known_local_folder = ["src", "main"] 91 | -------------------------------------------------------------------------------- /web/setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = django-template 3 | description = Django template 4 | author = Nazarii Romanchenko 5 | url = https://github.com/bandirom/django-template 6 | classifiers = 7 | Environment :: Web Environment 8 | Framework :: Django 9 | Framework :: Django :: 4.2 10 | Intended Audience :: Developers 11 | License :: OSI Approved :: MIT License 12 | Operating System :: OS Independent 13 | Programming Language :: Python 14 | Programming Language :: Python :: 3 15 | Programming Language :: Python :: 3 :: Only 16 | Programming Language :: Python :: 3.10 17 | Programming Language :: Python :: 3.11 18 | Programming Language :: Python :: 3.12 19 | Programming Language :: Python :: 3.13 20 | Topic :: Internet :: WWW/HTTP 21 | Topic :: Internet :: WWW/HTTP :: Dynamic Content 22 | Topic :: Internet :: WWW/HTTP :: WSGI 23 | Topic :: Internet :: WWW/HTTP :: ASGI 24 | 25 | [options] 26 | packages = find: 27 | include_package_data = True 28 | python_requires = >=3.10 29 | zip_safe = False 30 | install_requires = file: src/requirements/base.txt 31 | 32 | [options.extras_require] 33 | local = file: src/requirements/local.txt 34 | production = file: src/requirements/production.txt 35 | 36 | [flake8] 37 | max-line-length = 120 38 | exclude = .ash_history, .cache, venv, media, db.sqlite3, .mypy_cache, .idea, */migrations/*, *src/settings*.py 39 | per-file-ignores = 40 | src/additional_settings/__init__.py: F401, F403 41 | -------------------------------------------------------------------------------- /web/src/__init__.py: -------------------------------------------------------------------------------- 1 | from .celery import app as celery_app 2 | 3 | __all__ = ('celery_app',) 4 | -------------------------------------------------------------------------------- /web/src/additional_settings/__init__.py: -------------------------------------------------------------------------------- 1 | from .celery_settings import * 2 | from .smtp_settings import * 3 | -------------------------------------------------------------------------------- /web/src/additional_settings/celery_settings.py: -------------------------------------------------------------------------------- 1 | from os import environ 2 | 3 | from kombu import Exchange, Queue 4 | 5 | CELERY_BROKER_URL = environ.get('CELERY_BROKER_URL') 6 | CELERY_RESULT_BACKEND = environ.get('CELERY_RESULT_BACKEND') 7 | 8 | CELERY_TIMEZONE = environ.get('TZ', 'UTC') 9 | 10 | CELERY_RESULT_PERSISTENT = True 11 | CELERY_TASK_TRACK_STARTED = True 12 | CELERY_TASK_TIME_LIMIT = 30 * 60 13 | CELERY_ACCEPT_CONTENT = ['json'] 14 | CELERY_TASK_SERIALIZER = 'json' 15 | 16 | CELERY_BROKER_HEARTBEAT_CHECKRATE = 10 17 | CELERY_EVENT_QUEUE_EXPIRES = 10 18 | CELERY_EVENT_QUEUE_TTL = 10 19 | CELERY_TASK_SOFT_TIME_LIMIT = 60 20 | 21 | CELERY_BROKER_TRANSPORT_OPTIONS = { 22 | 'max_retries': 4, 23 | 'interval_start': 0, 24 | 'interval_step': 0.5, 25 | 'interval_max': 3, 26 | } 27 | 28 | celery_exchange = Exchange('celery', type='direct') # topic, fanout 29 | 30 | CELERY_TASK_ROUTES = { 31 | '*': {'queue': 'celery'}, 32 | } 33 | 34 | CELERY_TASK_QUEUES = ( 35 | Queue( 36 | name='celery', 37 | exchange=celery_exchange, 38 | queue_arguments={'x-queue-mode': 'lazy'}, 39 | ), 40 | ) 41 | 42 | 43 | CELERY_BEAT_SCHEDULE: dict[str, dict] = {} 44 | -------------------------------------------------------------------------------- /web/src/additional_settings/smtp_settings.py: -------------------------------------------------------------------------------- 1 | from os import environ 2 | 3 | EMAIL_HOST = environ.get('EMAIL_HOST', 'localhost') 4 | EMAIL_PORT = int(environ.get('EMAIL_PORT', 1025)) 5 | EMAIL_HOST_USER = environ.get('EMAIL_HOST_USER') 6 | EMAIL_HOST_PASSWORD = environ.get('EMAIL_HOST_PASSWORD') 7 | DEFAULT_FROM_EMAIL = environ.get('DEFAULT_FROM_EMAIL') 8 | EMAIL_TIMEOUT = int(environ.get('EMAIL_TIMEOUT', 15)) 9 | EMAIL_USE_SSL = int(environ.get('EMAIL_USE_SSL', 0)) 10 | EMAIL_USE_TLS = int(environ.get('EMAIL_USE_TLS', 0)) 11 | 12 | # Available choice: console, smtp, locmem, etc.. 13 | EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' 14 | -------------------------------------------------------------------------------- /web/src/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for src project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'src.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /web/src/celery.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from celery import Celery 4 | 5 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'src.settings') 6 | 7 | app = Celery('src') 8 | app.config_from_object('django.conf:settings', namespace='CELERY') 9 | app.autodiscover_tasks() 10 | -------------------------------------------------------------------------------- /web/src/requirements/base.txt: -------------------------------------------------------------------------------- 1 | ### Required dependencies ### 2 | 3 | django~=4.2 4 | psycopg[binary]~=3.1 5 | redis~=5.0 6 | djangorestframework~=3.14 7 | drf-spectacular~=0.27 8 | celery~=5.3 9 | 10 | ### Optional dependencies ### 11 | 12 | # Filtering 13 | django-filter~=23.5 14 | 15 | # Cors headers 16 | django-cors-headers~=4.3 17 | 18 | # Ddos defender: required redis 19 | django-defender~=0.9 20 | 21 | # for auth system 22 | dj-rest-auth~=5.0 23 | djangorestframework-simplejwt~=5.3 24 | 25 | # ImageField 26 | pillow~=10.1 27 | 28 | # Translation 29 | django-rosetta~=0.9 30 | 31 | # Monitoring System 32 | sentry-sdk~=1.40 33 | -------------------------------------------------------------------------------- /web/src/requirements/local.txt: -------------------------------------------------------------------------------- 1 | # For SQL debug 2 | django-silk~=5.0 3 | 4 | # Test coverage 5 | coverage~=7.2 6 | 7 | # Freeze time in tests 8 | freezegun~=1.2 9 | 10 | # Data factory 11 | factory-boy~=3.3 12 | 13 | # Utility for displaying the installed python packages in a form of a dependency tree 14 | pipdeptree~=2.13 15 | 16 | # Debug Toolbar 17 | django-debug-toolbar~=4.2 18 | django-debug-toolbar-request-history~=0.1 19 | 20 | # Linters 21 | black~=24.2 22 | flake8~=7.0 23 | isort~=5.13 24 | 25 | pytest~=8.0 26 | pytest-django~=4.8 27 | pytest-cov~=4.1 28 | pytest-freezegun~=0.4 29 | -------------------------------------------------------------------------------- /web/src/requirements/production.txt: -------------------------------------------------------------------------------- 1 | gunicorn==21.2.0 2 | uvicorn[standard]~=0.27 3 | -------------------------------------------------------------------------------- /web/src/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | 4 | from .additional_settings import * 5 | 6 | BASE_DIR = Path(__file__).resolve().parent.parent 7 | 8 | SECRET_KEY = os.environ.get('SECRET_KEY', 'django-insecure-b2sh!qk&=%azim-=s&=d1(-1upbq7H&-^-=tmPeHPLKXD') 9 | 10 | DEBUG = int(os.environ.get('DEBUG', 0)) 11 | 12 | ALLOWED_HOSTS: list = os.environ.get('DJANGO_ALLOWED_HOSTS', '').split(',') 13 | 14 | if DEBUG: 15 | ALLOWED_HOSTS: list = ['*'] 16 | 17 | AUTH_USER_MODEL = 'main.User' 18 | 19 | PROJECT_TITLE = os.environ.get('PROJECT_TITLE', 'Template') 20 | 21 | REDIS_URL = os.environ.get('REDIS_URL', 'redis://redis:6379') 22 | 23 | USE_HTTPS = int(os.environ.get('USE_HTTPS', 0)) 24 | ENABLE_SENTRY = int(os.environ.get('ENABLE_SENTRY', 0)) 25 | ENABLE_SILK = int(os.environ.get('ENABLE_SILK', 0)) 26 | ENABLE_DEBUG_TOOLBAR = int(os.environ.get('ENABLE_DEBUG_TOOLBAR', 0)) 27 | 28 | INTERNAL_IPS: list[str] = [] 29 | 30 | ADMIN_URL = os.environ.get('ADMIN_URL', 'admin') 31 | 32 | SWAGGER_URL = os.environ.get('SWAGGER_URL') 33 | 34 | HEALTH_CHECK_URL = os.environ.get('HEALTH_CHECK_URL', '/application/health/') 35 | 36 | DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' 37 | 38 | INSTALLED_APPS = [ 39 | 'django.contrib.admin', 40 | 'django.contrib.auth', 41 | 'django.contrib.contenttypes', 42 | 'django.contrib.sessions', 43 | 'django.contrib.messages', 44 | 'django.contrib.staticfiles', 45 | ] 46 | 47 | THIRD_PARTY_APPS = [ 48 | 'defender', 49 | 'rest_framework', 50 | 'drf_spectacular', 51 | 'corsheaders', 52 | 'rosetta', 53 | ] 54 | 55 | LOCAL_APPS = [ 56 | 'main.apps.MainConfig', 57 | ] 58 | 59 | INSTALLED_APPS += THIRD_PARTY_APPS + LOCAL_APPS 60 | 61 | MIDDLEWARE = [ 62 | 'corsheaders.middleware.CorsMiddleware', 63 | 'main.middleware.HealthCheckMiddleware', 64 | 'django.middleware.security.SecurityMiddleware', 65 | 'django.contrib.sessions.middleware.SessionMiddleware', 66 | 'django.middleware.common.CommonMiddleware', 67 | 'django.middleware.csrf.CsrfViewMiddleware', 68 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 69 | 'django.contrib.messages.middleware.MessageMiddleware', 70 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 71 | 'defender.middleware.FailedLoginMiddleware', 72 | 'django.middleware.locale.LocaleMiddleware', 73 | ] 74 | 75 | REST_FRAMEWORK = { 76 | 'DEFAULT_PERMISSION_CLASSES': ('rest_framework.permissions.IsAuthenticated',), 77 | 'DEFAULT_AUTHENTICATION_CLASSES': ('rest_framework.authentication.SessionAuthentication',), 78 | 'DEFAULT_FILTER_BACKENDS': ('django_filters.rest_framework.DjangoFilterBackend',), 79 | 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', 80 | } 81 | 82 | 83 | ROOT_URLCONF = 'src.urls' 84 | 85 | LOGIN_URL = 'rest_framework:login' 86 | LOGOUT_URL = 'rest_framework:logout' 87 | 88 | TEMPLATES = [ 89 | { 90 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 91 | 'DIRS': [os.path.join(BASE_DIR, 'templates')], 92 | 'APP_DIRS': True, 93 | 'OPTIONS': { 94 | 'context_processors': [ 95 | 'django.template.context_processors.debug', 96 | 'django.template.context_processors.request', 97 | 'django.contrib.auth.context_processors.auth', 98 | 'django.contrib.messages.context_processors.messages', 99 | ], 100 | }, 101 | }, 102 | ] 103 | 104 | WSGI_APPLICATION = 'src.wsgi.application' 105 | ASGI_APPLICATION = 'src.asgi.application' 106 | 107 | DATABASES = { 108 | 'default': { 109 | 'ENGINE': os.environ.get('SQL_ENGINE', 'django.db.backends.sqlite3'), 110 | 'NAME': os.environ.get('POSTGRES_DB', BASE_DIR / 'db.sqlite3'), 111 | 'USER': os.environ.get('POSTGRES_USER'), 112 | 'PASSWORD': os.environ.get('POSTGRES_PASSWORD'), 113 | 'HOST': os.environ.get('POSTGRES_HOST'), 114 | 'PORT': os.environ.get('POSTGRES_PORT'), 115 | 'CONN_MAX_AGE': 0, 116 | }, 117 | } 118 | 119 | AUTH_PASSWORD_VALIDATORS = [ 120 | { 121 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 122 | }, 123 | { 124 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 125 | }, 126 | { 127 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 128 | }, 129 | { 130 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 131 | }, 132 | ] 133 | 134 | CACHES = { 135 | 'default': { 136 | 'BACKEND': 'django.core.cache.backends.redis.RedisCache', 137 | 'LOCATION': REDIS_URL, 138 | } 139 | } 140 | 141 | LANGUAGE_CODE = 'en-us' 142 | 143 | TIME_ZONE = os.environ.get('TZ', 'UTC') 144 | 145 | USE_I18N = True 146 | 147 | USE_L10N = True 148 | 149 | USE_TZ = True 150 | 151 | STATIC_URL = '/static/' 152 | STATIC_ROOT = os.path.join(BASE_DIR, 'static') 153 | 154 | MEDIA_URL = '/media/' 155 | MEDIA_ROOT = os.path.join(BASE_DIR, 'media') 156 | 157 | LOCALE_PATHS = (os.path.join(BASE_DIR, 'locale'),) 158 | 159 | LANGUAGES = (('en', 'English'),) 160 | 161 | SESSION_COOKIE_NAME = 'sessionid' 162 | CSRF_COOKIE_NAME = 'csrftoken' 163 | 164 | ROSETTA_SHOW_AT_ADMIN_PANEL = DEBUG 165 | 166 | DEFENDER_REDIS_URL = REDIS_URL + '/1' 167 | DEFENDER_USE_CELERY = False 168 | 169 | LOGGING = { 170 | 'version': 1, 171 | 'disable_existing_loggers': False, 172 | 'root': {'level': 'INFO', 'handlers': ['default']}, 173 | 'formatters': { 174 | 'simple': {'format': '%(levelname)s %(message)s'}, 175 | 'verbose': {'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s'}, 176 | 'django.server': { 177 | '()': 'django.utils.log.ServerFormatter', 178 | 'format': '[{server_time}] {message}', 179 | 'style': '{', 180 | }, 181 | }, 182 | 'filters': { 183 | 'require_debug_false': { 184 | '()': 'django.utils.log.RequireDebugFalse', 185 | }, 186 | 'require_debug_true': { 187 | '()': 'django.utils.log.RequireDebugTrue', 188 | }, 189 | }, 190 | 'handlers': { 191 | 'console': { 192 | 'class': 'logging.StreamHandler', 193 | }, 194 | 'null': { 195 | 'class': 'logging.NullHandler', 196 | }, 197 | 'default': { 198 | 'level': 'DEBUG', 199 | 'class': 'logging.StreamHandler', 200 | 'formatter': 'verbose', 201 | }, 202 | 'django.server': { 203 | 'level': 'INFO', 204 | 'class': 'logging.StreamHandler', 205 | 'formatter': 'django.server', 206 | }, 207 | }, 208 | 'loggers': { 209 | 'django': {'level': 'INFO', 'propagate': True}, 210 | 'django.request': { 211 | 'handlers': ['django.server'], 212 | 'level': 'ERROR', 213 | 'propagate': False, 214 | }, 215 | 'django.server': { 216 | 'handlers': ['django.server'], 217 | 'level': 'INFO', 218 | 'propagate': False, 219 | }, 220 | }, 221 | } 222 | 223 | SPECTACULAR_SETTINGS = { 224 | 'TITLE': PROJECT_TITLE, 225 | 'DESCRIPTION': 'API description', 226 | 'VERSION': '1.0.0', 227 | 'SCHEMA_PATH_PREFIX': '/api/v[0-9]', 228 | 'SERVE_PERMISSIONS': ['rest_framework.permissions.IsAdminUser'], 229 | 'SERVE_AUTHENTICATION': ['rest_framework.authentication.SessionAuthentication'], 230 | 'SWAGGER_UI_SETTINGS': { 231 | 'tryItOutEnabled': True, 232 | 'displayRequestDuration': True, 233 | "persistAuthorization": True, 234 | 'filter': True, 235 | }, 236 | 'APPEND_COMPONENTS': { 237 | 'securitySchemes': { 238 | 'Authorization': { 239 | 'type': 'apiKey', 240 | 'in': 'header', 241 | 'name': 'Authorization', 242 | 'description': 'Bearer jwt token', 243 | }, 244 | 'Language': { 245 | 'type': 'apiKey', 246 | 'in': 'header', 247 | 'name': 'Accept-Language', 248 | 'description': 'Authorization by Token', 249 | }, 250 | }, 251 | }, 252 | 'SECURITY': [ 253 | {'Authorization': [], 'Language': []}, 254 | ], 255 | } 256 | 257 | 258 | if (SENTRY_DSN := os.environ.get('SENTRY_DSN')) and ENABLE_SENTRY: 259 | # More information on site https://sentry.io/ 260 | from sentry_sdk import init 261 | from sentry_sdk.integrations.celery import CeleryIntegration 262 | from sentry_sdk.integrations.django import DjangoIntegration 263 | from sentry_sdk.integrations.redis import RedisIntegration 264 | 265 | init( 266 | dsn=SENTRY_DSN, 267 | integrations=[ 268 | DjangoIntegration(), 269 | RedisIntegration(), 270 | CeleryIntegration(), 271 | ], 272 | # Set traces_sample_rate to 1.0 to capture 100% 273 | # of transactions for performance monitoring. 274 | # We recommend adjusting this value in production. 275 | traces_sample_rate=float(os.environ.get('SENTRY_TRACES_SAMPLE_RATE', '1.0')), 276 | environment=os.environ.get('SENTRY_ENV', 'development'), 277 | sample_rate=float(os.environ.get('SENTRY_SAMPLE_RATE', '1.0')), 278 | # If you wish to associate users to errors (assuming you are using 279 | # django.contrib.auth) you may enable sending PII data. 280 | send_default_pii=True, 281 | ) 282 | -------------------------------------------------------------------------------- /web/src/settings_dev.py: -------------------------------------------------------------------------------- 1 | from .settings import * 2 | from .settings import ENABLE_SILK, INSTALLED_APPS, INTERNAL_IPS, MIDDLEWARE 3 | 4 | CORS_ORIGIN_ALLOW_ALL = True 5 | 6 | if ENABLE_SILK: 7 | INSTALLED_APPS += ['silk'] 8 | MIDDLEWARE += ['silk.middleware.SilkyMiddleware'] 9 | 10 | if ENABLE_DEBUG_TOOLBAR: 11 | from socket import gethostbyname_ex, gethostname 12 | 13 | INSTALLED_APPS += ['debug_toolbar'] 14 | MIDDLEWARE += ['debug_toolbar.middleware.DebugToolbarMiddleware'] 15 | hostname, d, ips = gethostbyname_ex(gethostname()) 16 | INTERNAL_IPS += [ip[:-1] + '1' for ip in ips] 17 | # More info: https://django-debug-toolbar.readthedocs.io/en/latest/configuration.html#debug-toolbar-panels 18 | DEBUG_TOOLBAR_PANELS = [ 19 | 'ddt_request_history.panels.request_history.RequestHistoryPanel', 20 | 'debug_toolbar.panels.timer.TimerPanel', 21 | 'debug_toolbar.panels.settings.SettingsPanel', 22 | 'debug_toolbar.panels.headers.HeadersPanel', 23 | 'debug_toolbar.panels.request.RequestPanel', 24 | 'debug_toolbar.panels.sql.SQLPanel', 25 | 'debug_toolbar.panels.profiling.ProfilingPanel', 26 | ] 27 | DEBUG_TOOLBAR_CONFIG = {'RESULTS_CACHE_SIZE': 100} 28 | -------------------------------------------------------------------------------- /web/src/settings_prod.py: -------------------------------------------------------------------------------- 1 | from .settings import * 2 | from .settings import USE_HTTPS 3 | 4 | CORS_ALLOW_CREDENTIALS = True 5 | 6 | CORS_ORIGIN_ALLOW_ALL = True # change this for production 7 | 8 | X_FRAME_OPTIONS = 'DENY' 9 | 10 | # Only via HTTPS 11 | if USE_HTTPS: 12 | CSRF_COOKIE_SECURE = True 13 | SESSION_COOKIE_SECURE = True 14 | SECURE_HSTS_SECONDS = 31536000 15 | SECURE_HSTS_INCLUDE_SUBDOMAINS = True 16 | SECURE_HSTS_PRELOAD = True 17 | SECURE_REFERRER_POLICY = 'strict-origin' 18 | SECURE_BROWSER_XSS_FILTER = True 19 | SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') 20 | USE_X_FORWARDED_HOST = True 21 | -------------------------------------------------------------------------------- /web/src/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.conf.urls.static import static 3 | from django.contrib import admin 4 | from django.urls import include, path 5 | from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView 6 | 7 | admin_url = settings.ADMIN_URL 8 | 9 | urlpatterns = [ 10 | path('', include('main.urls')), 11 | path('api/', include('api.urls')), 12 | path(f'{admin_url}/defender/', include('defender.urls')), 13 | path(f'{admin_url}/', admin.site.urls), 14 | path('api/', include('rest_framework.urls')), 15 | path('rosetta/', include('rosetta.urls')), 16 | path('api/schema/', SpectacularAPIView.as_view(), name='schema'), 17 | path('api/schema/swagger-ui/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'), 18 | ] 19 | 20 | 21 | if settings.DEBUG: 22 | urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 23 | urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) 24 | 25 | if settings.ENABLE_SILK: 26 | urlpatterns.append(path('silk/', include('silk.urls', namespace='silk'))) 27 | if settings.ENABLE_DEBUG_TOOLBAR: 28 | urlpatterns.append(path('__debug__/', include('debug_toolbar.urls'))) 29 | -------------------------------------------------------------------------------- /web/src/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for src project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'src.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /web/templates/403.html: -------------------------------------------------------------------------------- 1 | { 2 | detail: "Forbidden" 3 | } -------------------------------------------------------------------------------- /web/templates/404.html: -------------------------------------------------------------------------------- 1 | { 2 | detail: "Not Found" 3 | } -------------------------------------------------------------------------------- /web/templates/500.html: -------------------------------------------------------------------------------- 1 | { 2 | detail: "Something went wrong" 3 | } -------------------------------------------------------------------------------- /web/templates/admin/base_site.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base.html" %} 2 | {% load static i18n %} 3 | 4 | {% block title %}{{ title }} | {{ site_title }}{% endblock %} 5 | 6 | {% block branding %} 7 |

{{ site_header }}  8 | 9 |  Swagger 10 | 11 |

12 | {% endblock %} 13 | 14 | {% block extrahead %} 15 | 16 | {% endblock %} 17 | 18 | -------------------------------------------------------------------------------- /web/templates/rest_framework/login.html: -------------------------------------------------------------------------------- 1 | {% extends 'rest_framework/login_base.html' %} 2 | 3 | {% block branding %}

API

{% endblock %} 4 | --------------------------------------------------------------------------------