├── .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 | 
2 | 
3 | [](https://djangotemplatewithdocker.readthedocs.io/en/latest/?badge=latest)
4 | [](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 |
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 |
--------------------------------------------------------------------------------