├── .dockerignore
├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .github
├── ISSUE_TEMPLATE
│ ├── Bug_report.md
│ └── Feature_request.md
├── PULL_REQUEST_TEMPLATE.md
└── workflows
│ ├── deploy.yml
│ ├── main.yml
│ ├── nightly.yml
│ └── shared-build
│ └── action.yml
├── .gitignore
├── .pre-commit-config.yaml
├── .swcrc
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE.txt
├── Makefile
├── README.md
├── backend
├── .env.example
├── Dockerfile
├── common
│ ├── __init__.py
│ ├── context_processors.py
│ ├── models.py
│ ├── routes.py
│ ├── serializers.py
│ ├── tests.py
│ ├── urls.py
│ ├── utils
│ │ ├── __init__.py
│ │ └── tests.py
│ └── views.py
├── manage.py
├── project_name
│ ├── __init__.py
│ ├── celery.py
│ ├── celerybeat_schedule.py
│ ├── settings
│ │ ├── __init__.py
│ │ ├── base.py
│ │ ├── local.py.example
│ │ ├── local_base.py
│ │ ├── production.py
│ │ └── test.py
│ ├── urls.py
│ └── wsgi.py
├── templates
│ ├── base.html
│ ├── common
│ │ └── index.html
│ ├── defender
│ │ └── lockout.html
│ └── includes
│ │ └── sentry_init.html
└── users
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── managers.py
│ ├── migrations
│ └── __init__.py
│ ├── models.py
│ ├── routes.py
│ ├── serializers.py
│ ├── tasks.py
│ ├── tests
│ ├── __init__.py
│ └── test_views.py
│ └── views.py
├── docker-compose.yml
├── frontend
├── Dockerfile
├── assets
│ └── images
│ │ ├── django-logo-negative.png
│ │ ├── django-logo-positive.png
│ │ └── index.d.ts
├── js
│ ├── App.tsx
│ ├── api
│ │ └── index.ts
│ ├── constants
│ │ └── index.ts
│ ├── index.tsx
│ ├── pages
│ │ ├── Home.tsx
│ │ └── __tests__
│ │ │ └── Home.spec.tsx
│ ├── routes
│ │ └── index.ts
│ ├── types
│ │ └── index.d.ts
│ └── utils
│ │ ├── index.ts
│ │ └── urls.ts
└── sass
│ ├── _global.scss
│ ├── _variables.scss
│ ├── components
│ └── _all.scss
│ ├── helpers
│ ├── _all.scss
│ ├── _functions.scss
│ ├── _helpers.scss
│ ├── _mixins.scss
│ ├── _placeholders.scss
│ └── _typography.scss
│ ├── pages
│ └── _all.scss
│ ├── style.scss
│ └── vendor
│ ├── _bootstrap-includes.scss
│ └── custom-bootstrap.scss
├── jest.config.js
├── jest.setup.js
├── openapi-ts.config.ts
├── package.json
├── proj_main.yml
├── pyproject.toml
├── render.yaml
├── render_build.sh
├── tsconfig.json
└── webpack.config.js
/.dockerignore:
--------------------------------------------------------------------------------
1 | .db
2 | node_modules
3 | npm-debug.log
4 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # editorconfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | indent_style = space
7 | indent_size = 4
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 | end_of_line = lf
12 |
13 | [*.{js,jsx,ts,tsx,json,html,css,scss,md,yml,yaml,swcrc}]
14 | indent_size = 2
15 |
16 | [*.md]
17 | trim_trailing_whitespace = false
18 |
19 | [Makefile]
20 | indent_style = tab
21 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | frontend/bundles/
2 | frontend/webpack_bundles/
3 | frontend/js/api/
4 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 |
3 | module.exports = {
4 | root: true,
5 | parser: "@typescript-eslint/parser",
6 | extends: ["vinta/recommended-typescript"],
7 | rules: {
8 | "import/extensions": [
9 | "error",
10 | "ignorePackages",
11 | {
12 | js: "never",
13 | jsx: "never",
14 | ts: "never",
15 | tsx: "never",
16 | },
17 | ],
18 | },
19 | env: {
20 | browser: true,
21 | es2021: true,
22 | jest: true,
23 | node: true,
24 | },
25 | settings: {
26 | "import/extensions": [".js", ".jsx", ".ts", ".tsx"],
27 | "import/parsers": {
28 | "@typescript-eslint/parser": [".ts", ".tsx"],
29 | },
30 | "import/resolver": {
31 | node: {
32 | paths: [path.resolve(__dirname, "node_modules")],
33 | extensions: [".js", ".jsx", ".ts", ".tsx"],
34 | },
35 | webpack: {
36 | config: path.join(__dirname, "/webpack.config.js"),
37 | "config-index": 1,
38 | },
39 | typescript: {
40 | alwaysTryTypes: true,
41 | project: "./tsconfig.json",
42 | },
43 | },
44 | react: {
45 | version: "detect",
46 | },
47 | },
48 | overrides: [
49 | {
50 | files: ["openapi-ts.config.ts"],
51 | rules: {
52 | "import/no-extraneous-dependencies": [
53 | "error",
54 | { devDependencies: true },
55 | ],
56 | },
57 | },
58 | ],
59 | };
60 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/Bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 |
5 | ---
6 |
7 | **Describe the bug**
8 | A clear and concise description of what the bug is.
9 |
10 | **To Reproduce**
11 | Steps to reproduce the behavior:
12 | 1. Go to '...'
13 | 2. Click on '....'
14 | 3. Scroll down to '....'
15 | 4. See error
16 |
17 | **Expected behavior**
18 | A clear and concise description of what you expected to happen.
19 |
20 | **Screenshots**
21 | If applicable, add screenshots to help explain your problem.
22 |
23 | **Desktop (please complete the following information):**
24 | - OS: [e.g. iOS]
25 | - Browser [e.g. chrome, safari]
26 | - Version [e.g. 22]
27 |
28 | **Smartphone (please complete the following information):**
29 | - Device: [e.g. iPhone6]
30 | - OS: [e.g. iOS8.1]
31 | - Browser [e.g. stock browser, safari]
32 | - Version [e.g. 22]
33 |
34 | **Additional context**
35 | Add any other context about the problem here.
36 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/Feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 |
5 | ---
6 |
7 | **Is your feature request related to a problem? Please describe.**
8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
9 |
10 | **Describe the solution you'd like**
11 | A clear and concise description of what you want to happen.
12 |
13 | **Describe alternatives you've considered**
14 | A clear and concise description of any alternative solutions or features you've considered.
15 |
16 | **Additional context**
17 | Add any other context or screenshots about the feature request here.
18 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ## Description
4 |
5 |
6 | ## Motivation and Context
7 |
8 |
9 | ## Screenshots (if appropriate):
10 |
11 | ## Steps to reproduce (if appropriate):
12 |
13 | ## Types of changes
14 |
15 | - [ ] Bug fix (non-breaking change which fixes an issue)
16 | - [ ] New feature (non-breaking change which adds functionality)
17 | - [ ] Breaking change (fix or feature that would cause existing functionality to change)
18 |
19 | ## Checklist:
20 |
21 |
22 | - [ ] My code follows the code style of this project.
23 | - [ ] My change requires documentation updates.
24 | - [ ] I have updated the documentation accordingly.
25 | - [ ] My change requires dependencies updates.
26 | - [ ] I have updated the dependencies accordingly.
27 |
--------------------------------------------------------------------------------
/.github/workflows/deploy.yml:
--------------------------------------------------------------------------------
1 | name: deploy
2 | on:
3 | workflow_run:
4 | workflows: ["main"]
5 | branches: [main]
6 | types:
7 | - completed
8 |
9 | jobs:
10 | deploy:
11 | name: Generate stable boilerplate build
12 | strategy:
13 | matrix:
14 | python-version: [3.12]
15 | node-version: [20.13]
16 | runs-on: ubuntu-latest
17 | steps:
18 | - name: Checkout code
19 | uses: actions/checkout@v4
20 | with:
21 | ref: main
22 | - run: mkdir -p github/workflows
23 | - run: mv proj_main.yml github/workflows/main.yml
24 | - run: git checkout -b boilerplate-release
25 | - run: git add github/workflows/main.yml
26 | - run: git rm proj_main.yml
27 | - run: git commit -m "Replacing project actions" --author "Vinta Software "
28 | env:
29 | GIT_COMMITTER_NAME: "Vinta Software"
30 | GIT_COMMITTER_EMAIL: "contact@vinta.com.br"
31 | - run: git push origin boilerplate-release --force
32 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: main
2 | on:
3 | push:
4 | branches-ignore:
5 | - boilerplate-release
6 | pull_request:
7 |
8 | jobs:
9 | build:
10 | name: Build boilerplate code
11 | runs-on: ubuntu-latest
12 | steps:
13 | - name: Checkout code
14 | uses: actions/checkout@v4
15 | - uses: ./.github/workflows/shared-build
16 |
--------------------------------------------------------------------------------
/.github/workflows/nightly.yml:
--------------------------------------------------------------------------------
1 | name: nightly
2 | on:
3 | schedule:
4 | - cron: "0 0 * * *"
5 |
6 | jobs:
7 | build:
8 | name: Build boilerplate code nightly
9 | runs-on: ubuntu-latest
10 | steps:
11 | - name: Checkout code
12 | uses: actions/checkout@v4
13 | with:
14 | ref: main
15 | - uses: ./.github/workflows/shared-build
16 |
--------------------------------------------------------------------------------
/.github/workflows/shared-build/action.yml:
--------------------------------------------------------------------------------
1 | name: "Shared Build Steps"
2 | description: "Shared build steps for main and nightly"
3 |
4 | runs:
5 | using: "composite"
6 | steps:
7 | - name: Store branch and latest SHA
8 | id: vars
9 | shell: bash
10 | run: |
11 | echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})"
12 | echo "::set-output name=sha_short::$(git rev-parse --short HEAD)"
13 | - name: Setup Python
14 | uses: actions/setup-python@v5
15 | with:
16 | python-version: "3.12"
17 | - name: Setup Node
18 | uses: actions/setup-node@v4
19 | with:
20 | node-version: "20.13"
21 | - name: Cache node modules
22 | uses: actions/cache@v4
23 | env:
24 | cache-name: node-modules-cache
25 | with:
26 | path: ~/.npm
27 | key: build-${{ env.cache-name }}-${{ steps.vars.outputs.branch }}-${{ steps.vars.outputs.sha_short }}
28 | restore-keys: |
29 | build-${{ env.cache-name }}-${{ steps.vars.outputs.branch }}-${{ steps.vars.outputs.sha_short }}
30 | build-${{ env.cache-name }}-${{ steps.vars.outputs.branch }}
31 | build-${{ env.cache-name }}
32 | - name: Cache pip
33 | uses: actions/cache@v4
34 | env:
35 | cache-name: pip-cache
36 | with:
37 | path: ~/.cache/pip
38 | key: build-${{ env.cache-name }}-${{ steps.vars.outputs.branch }}-${{ steps.vars.outputs.sha_short }}
39 | restore-keys: |
40 | build-${{ env.cache-name }}-${{ steps.vars.outputs.branch }}-${{ steps.vars.outputs.sha_short }}
41 | build-${{ env.cache-name }}-${{ steps.vars.outputs.branch }}
42 | build-${{ env.cache-name }}
43 | - run: python -m pip install --upgrade pip
44 | shell: bash
45 | - name: Install Django
46 | run: pip install "django>=4,<5"
47 | shell: bash
48 | - name: Setup testproject
49 | run: django-admin startproject testproject --extension py,json,yml,yaml,toml --name Dockerfile,README.md,.env.example,.gitignore,Makefile --template=.
50 | shell: bash
51 | - run: |
52 | npm update --save
53 | npm update --save-dev
54 | shell: bash
55 | working-directory: testproject
56 | - run: npm install --no-optional
57 | working-directory: testproject
58 | shell: bash
59 | - run: npm dedupe
60 | working-directory: testproject
61 | shell: bash
62 | - run: pip install poetry==1.7.1 --upgrade
63 | working-directory: testproject
64 | shell: bash
65 | - run: poetry install --with dev --no-root --no-interaction --no-ansi
66 | working-directory: testproject
67 | shell: bash
68 | - run: cp testproject/settings/local.py.example testproject/settings/local.py
69 | working-directory: testproject/backend
70 | shell: bash
71 | - run: cp .env.example .env
72 | working-directory: testproject/backend
73 | shell: bash
74 | - run: poetry run python manage.py makemigrations
75 | working-directory: testproject/backend
76 | env:
77 | DATABASE_URL: "sqlite:///"
78 | shell: bash
79 | - run: poetry run python manage.py migrate
80 | working-directory: testproject/backend
81 | env:
82 | DATABASE_URL: "sqlite:///"
83 | shell: bash
84 | - name: Generate backend schema
85 | run: poetry run python manage.py spectacular --color --file schema.yml
86 | working-directory: testproject/backend
87 | env:
88 | DATABASE_URL: "sqlite:///"
89 | shell: bash
90 | - name: Generate frontend API client
91 | run: npm run openapi-ts
92 | working-directory: testproject
93 | shell: bash
94 | - run: npm run lint
95 | working-directory: testproject
96 | shell: bash
97 | - run: npm run build
98 | working-directory: testproject
99 | shell: bash
100 | - run: npm run test
101 | working-directory: testproject
102 | shell: bash
103 | - run: poetry run python manage.py test
104 | working-directory: testproject/backend
105 | env:
106 | DATABASE_URL: "sqlite:///"
107 | shell: bash
108 | - name: Generate secret key
109 | run: echo '::set-output name=SECRET_KEY::`python -c "import uuid; print(uuid.uuid4().hex + uuid.uuid4().hex)"`'
110 | id: secret-id-generator
111 | shell: bash
112 | - run: rm .gitignore # prevents conflict with ruff
113 | shell: bash
114 | - run: poetry run ruff check .
115 | working-directory: testproject/backend
116 | shell: bash
117 | - run: poetry run python manage.py makemigrations --check --dry-run
118 | working-directory: testproject/backend
119 | env:
120 | DATABASE_URL: "sqlite:///"
121 | shell: bash
122 | - run: poetry run python manage.py check --deploy --fail-level WARNING
123 | working-directory: testproject/backend
124 | env:
125 | SECRET_KEY: ${{ steps.secret-id-generator.outputs.SECRET_KEY }}
126 | SENDGRID_USERNAME: foo
127 | SENDGRID_PASSWORD: password
128 | DJANGO_SETTINGS_MODULE: "testproject.settings.production"
129 | ALLOWED_HOSTS: ".example.org"
130 | REDIS_URL: "redis://"
131 | DATABASE_URL: "sqlite:///"
132 | shell: bash
133 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | db.sqlite3
3 | db.sqlite3-journal
4 | .DS_Store
5 | .env
6 | **/settings/local.py
7 | /staticfiles/*
8 | /mediafiles/*
9 | __pycache__/
10 | /.vscode/
11 |
12 | # coverage result
13 | .coverage
14 | /coverage/
15 |
16 | # pycharm
17 | .idea/
18 |
19 | # data
20 | *.dump
21 |
22 | # npm
23 | node_modules/
24 | npm-debug.log
25 |
26 | # Webpack
27 | /frontend/bundles/*
28 | /frontend/webpack_bundles/*
29 | /webpack-stats.json
30 |
31 | # Sass
32 | .sass-cache
33 | *.map
34 |
35 | # General
36 | /{{project_name}}-venv/
37 | /venv/
38 | /env/
39 | /output/
40 | /cache/
41 | boilerplate.zip
42 |
43 | # Spritesmith
44 | spritesmith-generated/
45 | spritesmith.scss
46 |
47 | # templated email
48 | tmp_email/
49 |
50 | .direnv
51 | .envrc
52 | .tool-versions
53 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | fail_fast: true
2 | repos:
3 | - repo: https://github.com/pre-commit/pre-commit-hooks
4 | rev: v4.5.0
5 | hooks:
6 | - id: check-added-large-files
7 | args: ["--maxkb=500"]
8 | exclude: >
9 | (?x)^(
10 | package-lock\.json
11 | )$
12 | - id: fix-byte-order-marker
13 | - id: check-case-conflict
14 | - id: check-merge-conflict
15 | - id: check-symlinks
16 | - id: debug-statements
17 | - id: detect-private-key
18 | - repo: https://github.com/adamchainz/django-upgrade
19 | rev: "1.15.0"
20 | hooks:
21 | - id: django-upgrade
22 | args: [--target-version, "5.0"]
23 | - repo: https://github.com/astral-sh/ruff-pre-commit
24 | # Ruff version.
25 | rev: v0.1.6
26 | hooks:
27 | # Run the linter.
28 | - id: ruff
29 | args: [--fix]
30 | # Run the formatter.
31 | - id: ruff-format
32 | - repo: local
33 | hooks:
34 | - id: eslint
35 | name: eslint-local
36 | entry: npm run lint
37 | language: system
38 | types: [file]
39 | files: \.(js|jsx|ts|tsx)$
40 | exclude: >
41 | (?x)^(
42 | .+\.config\.js|
43 | .+\.setup\.js|
44 | \.eslintrc\.js
45 | )$
46 | pass_filenames: true
47 | - id: tsc
48 | name: tsc-local
49 | entry: npm run tsc
50 | language: system
51 | types: [file]
52 | files: \.(ts|tsx)$
53 | pass_filenames: false
54 | - id: missing-migrations
55 | name: missing-migrations-local
56 | entry: poetry run python backend/manage.py makemigrations --check
57 | language: system
58 | # Only run missing migration check if migration-generating files have changed:
59 | files: (.*/?(settings|migrations|models)/.+|.+models\.py|.+constants\.py|.+choices\.py|.+pyproject\.toml)
60 | pass_filenames: false
61 | - id: backend-schema
62 | name: backend-schema-local
63 | entry: poetry run python backend/manage.py spectacular --color --file backend/schema.yml
64 | language: system
65 | files: ^backend/
66 | pass_filenames: false
67 | - id: frontend-api
68 | name: frontend-api-local
69 | entry: npm run openapi-ts
70 | language: system
71 | files: backend/schema\.yml$
72 | pass_filenames: false
73 |
--------------------------------------------------------------------------------
/.swcrc:
--------------------------------------------------------------------------------
1 | {
2 | "jsc": {
3 | "parser": {
4 | "jsx": true,
5 | "tsx": true,
6 | "syntax": "typescript",
7 | "decorators": true,
8 | "dynamicImport": true
9 | },
10 | "transform": {
11 | "react": {
12 | "runtime": "automatic"
13 | }
14 | }
15 | },
16 | // The 'env' field is used to specify settings for compiling JavaScript/TypeScript code to be
17 | // compatible with older environments.
18 | "env": {
19 | // 'entry' means SWC will include polyfills for all features used in the code that are not
20 | // available in the target environment.
21 | "mode": "entry",
22 | // 'corejs' specifies the version of core-js to use for polyfills.
23 | "corejs": 3
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 |
2 | # Contributor Covenant Code of Conduct
3 |
4 | ## Our Pledge
5 |
6 | We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation.
7 |
8 | We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
9 |
10 | ## Our Standards
11 |
12 | Examples of behavior that contributes to a positive environment for our community include:
13 |
14 | * Demonstrating empathy and kindness toward other people
15 | * Being respectful of differing opinions, viewpoints, and experiences
16 | * Giving and gracefully accepting constructive feedback
17 | * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
18 | * Focusing on what is best not just for us as individuals, but for the overall community
19 |
20 | Examples of unacceptable behavior include:
21 |
22 | * The use of sexualized language or imagery, and sexual attention or advances of any kind
23 | * Trolling, insulting or derogatory comments, and personal or political attacks
24 | * Public or private harassment
25 | * Publishing others' private information, such as a physical or email address, without their explicit permission
26 | * Other conduct which could reasonably be considered inappropriate in a professional setting
27 |
28 | ## Enforcement Responsibilities
29 |
30 | Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
31 |
32 | Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
33 |
34 | ## Scope
35 |
36 | This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
37 |
38 | ## Enforcement
39 |
40 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [flavio@vinta.com.br](flavio@vinta.com.br). All complaints will be reviewed and investigated promptly and fairly.
41 |
42 | All community leaders are obligated to respect the privacy and security of the reporter of any incident.
43 |
44 | ## Enforcement Guidelines
45 |
46 | Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
47 |
48 | ### 1. Correction
49 |
50 | **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
51 |
52 | **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
53 |
54 | ### 2. Warning
55 |
56 | **Community Impact**: A violation through a single incident or series of actions.
57 |
58 | **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
59 |
60 | ### 3. Temporary Ban
61 |
62 | **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
63 |
64 | **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
65 |
66 | ### 4. Permanent Ban
67 |
68 | **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
69 |
70 | **Consequence**: A permanent ban from any sort of public interaction within the community.
71 |
72 | ## Attribution
73 |
74 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0].
75 |
76 | Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
77 |
78 | For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at [https://www.contributor-covenant.org/translations][translations].
79 |
80 | [homepage]: https://www.contributor-covenant.org
81 | [v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html
82 | [Mozilla CoC]: https://github.com/mozilla/diversity
83 | [FAQ]: https://www.contributor-covenant.org/faq
84 | [translations]: https://www.contributor-covenant.org/translations
85 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | ## Pull requests
4 |
5 | Read [this checklist](https://devchecklists.com/en/checklist/pull-requests-checklist) for a more detailed guide on best practices for opening pull requests.
6 |
7 | ## Testing your changes
8 |
9 | ### Testing `django-admin startproject`
10 |
11 | If you made changes to this boilerplate and want to test them, do as follows:
12 |
13 | - [Make sure you have pre-commit installed](https://github.com/vintasoftware/django-react-boilerplate#pre-commit-hooks)
14 | - Commit your changes
15 | - Run `git archive -o boilerplate.zip HEAD` to create the template zip file
16 | - Run the following:
17 | ```bash
18 | cd .. && django-admin startproject theprojectname --extension py,json,yml,yaml,toml --name Dockerfile,README.md,.env.example,.gitignore,Makefile --template=django-react-boilerplate/boilerplate.zip
19 | ```
20 | - A new folder called `theprojectname` will be created and now you can test your changes
21 | - Make sure that the project is still running fine with and without docker
22 |
23 | ### Testing Render.com deployment
24 |
25 | Push your changes to a branch and visit the link below
26 |
27 | https://render.com/deploy?repo=https://github.com/fill-org-or-user/fill-project-repo-name/tree/fill-branch
28 |
29 | > Make sure to replace all `fill-*`
30 |
31 | ## How to add a "Deploy to Render.com" button
32 |
33 | Read [this](https://render.com/docs/deploy-to-render).
34 |
35 | P.S. if you want to deploy in a different way, please check the `render.yaml` file for what needs to be configured.
36 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Vinta Serviços e Soluções Tecnológicas Ltda
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 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | SHELL := /bin/bash # Use bash syntax
2 | ARG := $(word 2, $(MAKECMDGOALS) )
3 |
4 | clean:
5 | @find . -name "*.pyc" -exec rm -rf {} \;
6 | @find . -name "__pycache__" -delete
7 |
8 | test:
9 | poetry run backend/manage.py test backend/ $(ARG) --parallel --keepdb
10 |
11 | test_reset:
12 | poetry run backend/manage.py test backend/ $(ARG) --parallel
13 |
14 | backend_format:
15 | black backend
16 |
17 | # Commands for Docker version
18 | docker_setup:
19 | docker volume create {{project_name}}_dbdata
20 | docker compose build --no-cache backend
21 | docker compose run --rm backend python manage.py spectacular --color --file schema.yml
22 | docker compose run frontend npm install
23 | docker compose run --rm frontend npm run openapi-ts
24 |
25 | docker_test:
26 | docker compose run backend python manage.py test $(ARG) --parallel --keepdb
27 |
28 | docker_test_reset:
29 | docker compose run backend python manage.py test $(ARG) --parallel
30 |
31 | docker_up:
32 | docker compose up -d
33 |
34 | docker_update_dependencies:
35 | docker compose down
36 | docker compose up -d --build
37 |
38 | docker_down:
39 | docker compose down
40 |
41 | docker_logs:
42 | docker compose logs -f $(ARG)
43 |
44 | docker_makemigrations:
45 | docker compose run --rm backend python manage.py makemigrations
46 |
47 | docker_migrate:
48 | docker compose run --rm backend python manage.py migrate
49 |
50 | docker_backend_shell:
51 | docker compose run --rm backend bash
52 |
53 | docker_backend_update_schema:
54 | docker compose run --rm backend python manage.py spectacular --color --file schema.yml
55 |
56 | docker_frontend_update_api:
57 | docker compose run --rm frontend npm run openapi-ts
58 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Django React Boilerplate
2 |
3 | [](code_of_conduct.md)
4 | [](LICENSE.txt)
5 |
6 | ## About
7 |
8 | A [Django](https://www.djangoproject.com/) project boilerplate/template with a multitude of state-of-the-art libraries and tools. If pairing Django with React is a possibility for your project or spinoff, this is the best solution available. Save time with tools like:
9 |
10 | - [React](https://react.dev/), for building interactive UIs
11 | - [TypeScript](https://www.typescriptlang.org/), for static type checking
12 | - [Poetry](https://python-poetry.org/), for managing the environment and its dependencies
13 | - [django-js-reverse](https://github.com/vintasoftware/django-js-reverse), for generating URLs on JS
14 | - [React Bootstrap](https://react-bootstrap.github.io/), for responsive styling
15 | - [Webpack](https://webpack.js.org/), for bundling static assets
16 | - [Celery](https://docs.celeryq.dev/en/stable/), for background worker tasks
17 | - [WhiteNoise](https://whitenoise.readthedocs.io/en/stable/) with [brotlipy](https://github.com/python-hyper/brotlicffi), for efficient static files serving
18 | - [ruff](https://github.com/astral-sh/ruff) and [ESLint](https://eslint.org/) with [pre-commit](https://pre-commit.com/) for automated quality assurance (does not replace proper testing!)
19 |
20 | For continuous integration, a [Github Action](https://github.com/features/actions) configuration `.github/workflows/main.yml` is included.
21 |
22 | Also, includes a Render.com `render.yaml` and a working Django `production.py` settings, enabling easy deployments with ['Deploy to Render' button](https://render.com/docs/deploy-to-render). The `render.yaml` includes the following:
23 |
24 | - PostgreSQL, for DB
25 | - Redis, for Celery
26 |
27 | ## Features Catalogue
28 |
29 | ### Frontend
30 |
31 | - `react` for building interactive UIs
32 | - `react-dom` for rendering the UI
33 | - `react-router` for page navigation
34 | - `webpack` for bundling static assets
35 | - `webpack-bundle-tracker` for providing the bundled assets to Django
36 | - Styling
37 | - `bootstrap` for providing responsive stylesheets
38 | - `react-bootstrap` for providing components built on top of Bootstrap CSS without using plugins
39 | - `sass` for providing compatibility with SCSS files
40 | - State management and backend integration
41 | - `axios` for performing asynchronous calls
42 | - `cookie` for easy integration with Django using the `csrftoken` cookie
43 | - `openapi-ts` for generating TypeScript client API code from the backend OpenAPI schema
44 | - `history` for providing browser history to Connected React Router
45 | - Utilities
46 | - `lodash` for general utility functions
47 | - `classnames` for easy working with complex CSS class names on components
48 | - `react-refresh` for improving QoL while developing through automatic browser refreshing
49 |
50 | ### Backend
51 |
52 | - `django` for building backend logic using Python
53 | - `djangorestframework` for building a REST API on top of Django
54 | - `drf-spectacular` for generating an OpenAPI schema for the Django REST API
55 | - `django-webpack-loader` for rendering the bundled frontend assets
56 | - `django-js-reverse` for easy handling of Django URLs on JS
57 | - `django-upgrade` for automatically upgrading Django code to the target version on pre-commit
58 | - `django-guid` for adding a unique correlation ID to log messages from Django requests
59 | - `psycopg` for using PostgreSQL database
60 | - `sentry-sdk` for error monitoring
61 | - `python-decouple` for reading environment variables on settings files
62 | - `celery` for background worker tasks
63 | - `django-csp` for setting the draft security HTTP header Content-Security-Policy
64 | - `django-permissions-policy` for setting the draft security HTTP header Permissions-Policy
65 | - `django-defender` for blocking brute force attacks against login
66 | - `whitenoise` and `brotlipy` for serving static assets
67 |
68 | ## Share your project!
69 |
70 | Several people have leveraged our boilerplate to start spinoffs or to boost their efforts in the challenging pursuit of securing funding. Starting with a solid foundation allows you to create more resilient products and focus on what really matters: discovering and delivering value to your customers. If you are one of those people, we're eager to help you even more! We can spread the word about your project across our social media platforms, giving you access to a broader audience.
71 |
72 | Send us an email at contact@vintasoftware.com telling us a bit more about how our boilerplate helped you boost your project.
73 |
74 | ## Project bootstrap [](https://github.com/vintasoftware/django-react-boilerplate/actions/workflows/main.yml) [](https://snyk.io/test/github/vintasoftware/django-react-boilerplate)
75 |
76 | - [ ] Make sure you have Python 3.12 installed
77 | - [ ] Install Django with `pip install django`, to have the `django-admin` command available
78 | - [ ] Open the command line and go to the directory you want to start your project in
79 | - [ ] Start your project using (replace `project_name` with your project name and remove the curly braces):
80 | ```
81 | django-admin startproject {{project_name}} --extension py,json,yml,yaml,toml --name Dockerfile,README.md,.env.example,.gitignore,Makefile --template=https://github.com/vintasoftware/django-react-boilerplate/archive/refs/heads/main.zip
82 | ```
83 | Alternatively, you may start the project in the current directory by placing a `.` right after the project name, using the following command:
84 | ```
85 | django-admin startproject {{project_name}} . --extension py,json,yml,yaml,toml --name Dockerfile,README.md,.env.example,.gitignore,Makefile --template=https://github.com/vintasoftware/django-react-boilerplate/archive/refs/heads/main.zip
86 | ```
87 | In the next steps, always remember to replace {{project_name}} with your project's name (in case it isn't yet):
88 | - [ ] Above: don't forget the `--extension` and `--name` params!
89 | - [ ] Go into project's root directory: `cd {{project_name}}`
90 | - [ ] Change the first line of README to the name of the project
91 | - [ ] Add an email address to the `ADMINS` settings variable in `{{project_name}}/backend/{{project_name}}/settings/base.py`
92 | - [ ] Change the `SERVER_EMAIL` to the email address used to send e-mails in `{{project_name}}/backend/{{project_name}}/settings/production.py`
93 |
94 | After completing ALL of the above, remove this `Project bootstrap` section from the project README. Then follow `Running` below.
95 |
96 | ## Running
97 |
98 | ### Tools
99 |
100 | - Setup [editorconfig](http://editorconfig.org/), [ruff](https://github.com/astral-sh/ruff) and [ESLint](http://eslint.org/) in the text editor you will use to develop.
101 |
102 | ### Setup
103 |
104 | - Do the following:
105 | - Create a git-untracked `local.py` settings file:
106 | `cp backend/{{project_name}}/settings/local.py.example backend/{{project_name}}/settings/local.py`
107 | - Create a git-untracked `.env.example` file:
108 | `cp backend/.env.example backend/.env`
109 |
110 | ### If you are using Docker:
111 |
112 | - Open the `backend/.env` file on a text editor and uncomment the line `DATABASE_URL=postgres://{{project_name}}:password@db:5432/{{project_name}}`
113 | - Open a new command line window and go to the project's directory
114 | - Run the initial setup:
115 | `make docker_setup`
116 | - Create the migrations for `users` app:
117 | `make docker_makemigrations`
118 | - Run the migrations:
119 | `make docker_migrate`
120 | - Run the project:
121 | `make docker_up`
122 | - Access `http://localhost:8000` on your browser and the project should be running there
123 | - When you run `make docker_up`, some containers are spinned up (frontend, backend, database, etc) and each one will be running on a different port
124 | - The container with the React app uses port 3000. However, if you try accessing it on your browser, the app won't appear there and you'll probably see a blank page with the "Cannot GET /" error
125 | - This happens because the container responsible for displaying the whole application is the Django app one (running on port 8000). The frontend container is responsible for providing a bundle with its assets for [django-webpack-loader](https://github.com/django-webpack/django-webpack-loader) to consume and render them on a Django template
126 | - To access the logs for each service, run:
127 | `make docker_logs ` (either `backend`, `frontend`, etc)
128 | - To stop the project, run:
129 | `make docker_down`
130 |
131 | #### Adding new dependencies
132 |
133 | - Open a new command line window and go to the project's directory
134 | - Update the dependencies management files by performing any number of the following steps:
135 | - To add a new **frontend** dependency, run `npm install --save`
136 | > The above command will update your `package.json`, but won't make the change effective inside the container yet
137 | - To add a new **backend** dependency, run `docker compose run --rm backend bash` to open an interactive shell and then run `poetry add {dependency}` to add the dependency. If the dependency should be only available for development user append `-G dev` to the command.
138 | - After updating the desired file(s), run `make docker_update_dependencies` to update the containers with the new dependencies
139 | > The above command will stop and re-build the containers in order to make the new dependencies effective
140 |
141 | ### If you are not using Docker:
142 |
143 | #### Setup the backend app
144 |
145 | - Open the `backend/.env` file on a text editor and do one of the following:
146 | - If you wish to use SQLite locally, uncomment the line `DATABASE_URL=sqlite:///db.sqlite3`
147 | - If you wish to use PostgreSQL locally, uncomment and edit the line `DATABASE_URL=postgres://{{project_name}}:password@db:5432/{{project_name}}` in order to make it correctly point to your database URL
148 | - The url format is the following: `postgres://USER:PASSWORD@HOST:PORT/NAME`
149 | - If you wish to use another database engine locally, add a new `DATABASE_URL` setting for the database you wish to use
150 | - Please refer to [dj-database-url](https://github.com/jazzband/dj-database-url#url-schema) on how to configure `DATABASE_URL` for commonly used engines
151 | - Open a new command line window and go to the project's directory
152 | - Run `poetry install`
153 |
154 | #### Run the backend app
155 |
156 | - Go to the `backend` directory
157 | - Create the migrations for `users` app:
158 | `poetry run python manage.py makemigrations`
159 | - Run the migrations:
160 | `poetry run python manage.py migrate`
161 | - Generate the OpenAPI schema:
162 | `poetry run python manage.py spectacular --color --file schema.yml`
163 | - Run the project:
164 | `poetry run python manage.py runserver`
165 |
166 | #### Setup and run the frontend app
167 |
168 | - Open a new command line window and go to the project's directory
169 | - `npm install`
170 | - `npm run openapi-ts`
171 | - This is used to generate the TypeScript client API code from the backend OpenAPI schema
172 | - `npm run dev`
173 | - This is used to serve the frontend assets to be consumed by [django-webpack-loader](https://github.com/django-webpack/django-webpack-loader) and not to run the React application as usual, so don't worry if you try to check what's running on port 3000 and see an error on your browser
174 | - Open a browser and go to `http://localhost:8000` to see the project running
175 |
176 | #### Setup Celery
177 |
178 | - `poetry run celery --app={{project_name}} worker --loglevel=info`
179 |
180 | #### Setup Redis
181 |
182 | - Ensure that Redis is already installed on your system. Once confirmed, run `redis-server --port 6379` to start the Redis server.
183 | - If you wish to use Redis for Celery, you need to set the `CELERY_BROKER_URL` environment variable in the `backend/.env` file to `redis://localhost:6379/0`.
184 | - The `/0` at the end of the URL specifies the database number on the Redis server. Redis uses a zero-based numbering system for databases, so `0` is the first database. If you don't specify a database number, Redis will use the first database by default.
185 | - Note: Prefer RabbitMQ over Redis for Broker, mainly because RabbitMQ doesn't need visibility timeout. See [Recommended Celery Django settings for reliability](https://gist.github.com/fjsj/da41321ac96cf28a96235cb20e7236f6).
186 |
187 | #### Mailhog
188 |
189 | - For development, we use Mailhog to test our e-mail workflows, since it allows us to inspect the messages to validate they're correctly built
190 | - Docker users already have it setup and running once they start the project
191 | - For non-Docker users, please have a look [here](https://github.com/mailhog/MailHog#installation) for instructions on how to setup Mailhog on specific environments
192 | > The project expects Mailhog SMTP server to be running on port 1025, you may alter that by changing `EMAIL_PORT` on settings
193 |
194 | ### Testing
195 |
196 | `make test`
197 |
198 | Will run django tests using `--keepdb` and `--parallel`. You may pass a path to the desired test module in the make command. E.g.:
199 |
200 | `make test someapp.tests.test_views`
201 |
202 | ### Adding new pypi libs
203 |
204 | To add a new **backend** dependency, run `poetry add {dependency}`. If the dependency should be only available for development user append `-G dev` to the command.
205 |
206 | ### API Schema and Client generation
207 |
208 | We use the [`DRF-Spectacular`](https://drf-spectacular.readthedocs.io/en/latest/readme.html) tool to generate an OpenAPI schema from our Django Rest Framework API. The OpenAPI schema serves as the backbone for generating client code, creating comprehensive API documentation, and more.
209 |
210 | The API documentation pages are accessible at `http://localhost:8000/api/schema/swagger-ui/` or `http://localhost:8000/api/schema/redoc/`.
211 |
212 | > [!IMPORTANT]
213 | > Anytime a view is created, updated, or removed, the schema must be updated to reflect the changes. Failing to do so can lead to outdated client code or documentation.
214 | >
215 | > To update the schema, run:
216 | > - If you are using Docker: `make docker_backend_update_schema`
217 | > - If you are not using Docker: `poetry run python manage.py spectacular --color --file schema.yml`
218 |
219 | We use the [`openapi-ts`](https://heyapi.vercel.app/openapi-ts/get-started.html) tool to generate TypeScript client code from the OpenAPI schema. The generated client code is used to interact with the API in a type-safe manner.
220 |
221 | > [!IMPORTANT]
222 | > Anytime the API schema is updated, the client code must be regenerated to reflect the changes. Failing to do so can lead to type errors in the client code.
223 | >
224 | > To update the client code, run:
225 | > - If you are using Docker: `make docker_frontend_update_api`
226 | > - If you are not using Docker: `npm run openapi-ts`
227 |
228 | > [!NOTE]
229 | > If `pre-commit` is properly enabled, it will automatically update both schema and client before each commit whenever necessary.
230 |
231 | ## Github Actions
232 |
233 | To enable Continuous Integration through Github Actions, we provide a `proj_main.yml` file. To connect it to Github you need to rename it to `main.yml` and move it to the `.github/workflows/` directory.
234 |
235 | You can do it with the following commands:
236 |
237 | ```bash
238 | mkdir -p .github/workflows
239 | mv proj_main.yml .github/workflows/main.yml
240 | ```
241 |
242 | ## Production Deployment
243 |
244 | ### Setup
245 |
246 | This project comes with an `render.yaml` file, which can be used to create an app on Render.com from a GitHub repository.
247 |
248 | Before deploying, please make sure you've generated an up-to-date `poetry.lock` file containing the Python dependencies. This is necessary even if you've used Docker for local runs. Do so by following [these instructions](#setup-the-backend-app).
249 |
250 | After setting up the project, you can init a repository and push it on GitHub. If your repository is public, you can use the following button:
251 |
252 | [](https://render.com/deploy)
253 |
254 | If you are in a private repository, access the following link replacing `$YOUR_REPOSITORY_URL$` with your repository link.
255 |
256 | - `https://render.com/deploy?repo=$YOUR_REPOSITORY_URL$`
257 |
258 | Keep reading to learn how to configure the prompted environment variables.
259 |
260 | #### `ALLOWED_HOSTS`
261 |
262 | Chances are your project name isn't unique in Render, and you'll get a randomized suffix as your full app URL like: `https://{{project_name}}-a1b2.onrender.com`.
263 |
264 | But this will only happen after the first deploy, so you are not able to properly fill `ALLOWED_HOSTS` yet. Simply set it to `*` then fix it later to something like `{{project_name}}-a1b2.onrender.com` and your domain name like `example.org`.
265 |
266 | #### `ENABLE_DJANGO_COLLECTSTATIC`
267 |
268 | Default is 1, meaning the build script will run collectstatic during deploys.
269 |
270 | #### `AUTO_MIGRATE`
271 |
272 | Default is 1, meaning the build script will run collectstatic during deploys.
273 |
274 | ### Build script
275 |
276 | By default, the project will always run the `render_build.sh` script during deployments. This script does the following:
277 |
278 | 1. Build the frontend
279 | 2. Build the backend
280 | 3. Run Django checks
281 | 4. Run `collectstatic`
282 | 5. Run Django migrations
283 | 6. Push frontend source maps to Sentry
284 |
285 | ### Celery
286 |
287 | As there aren't free plans for Workers in Render.com, the configuration for Celery workers/beat will be commented by default in the `render.yaml`. This means celery won't be available by default.
288 |
289 | Uncommenting the worker configuration lines on `render.yaml` will imply in costs.
290 |
291 | ### SendGrid
292 |
293 | To enable sending emails from your application you'll need to have a valid SendGrid account and also a valid verified sender identity. After finishing the validation process you'll be able to generate the API credentials and define the `SENDGRID_USERNAME` and `SENDGRID_PASSWORD` environment variables on Render.com.
294 |
295 | These variables are required for your application to work on Render.com since it's pre-configured to automatically email admins when the application is unable to handle errors gracefully.
296 |
297 | ### Media storage
298 |
299 | Media files integration with S3 or similar is not supported yet. Please feel free to contribute!
300 |
301 | ### Sentry
302 |
303 | [Sentry](https://sentry.io) is already set up on the project. For production, add `SENTRY_DSN` environment variable on Render.com, with your Sentry DSN as the value.
304 |
305 | You can test your Sentry configuration by deploying the boilerplate with the sample page and clicking on the corresponding button.
306 |
307 | ### Sentry source maps for JS files
308 |
309 | The `render_build.sh` script has a step to push Javascript source maps to Sentry, however some environment variables need to be set on Render.com.
310 |
311 | The environment variables that need to be set are:
312 |
313 | - `SENTRY_ORG` - Name of the Sentry Organization that owns your Sentry Project.
314 | - `SENTRY_PROJECT_NAME` - Name of the Sentry Project.
315 | - `SENTRY_API_KEY` - Sentry API key that needs to be generated on Sentry. [You can find or create authentication tokens within Sentry](https://sentry.io/api/).
316 |
317 | After enabling dyno metadata and setting the environment variables, your next Render.com Deploys will create a release on Sentry where the release name is the commit SHA, and it will push the source maps to it.
318 |
319 | ## Linting
320 |
321 | - At pre-commit time (see below)
322 | - Manually with `poetry run ruff` and `npm run lint` on project root.
323 | - During development with an editor compatible with ruff and ESLint.
324 |
325 | ## Pre-commit hooks
326 |
327 | ### If you are using Docker:
328 |
329 | - Not supported yet. Please feel free to contribute!
330 |
331 | ### If you are not using Docker:
332 |
333 | - On project root, run `poetry run pre-commit install` to enable the hook into your git repo. The hook will run automatically for each commit.
334 |
335 | ## Opinionated Settings
336 |
337 | Some settings defaults were decided based on Vinta's experiences. Here's the rationale behind them:
338 |
339 | ### `DATABASES["default"]["ATOMIC_REQUESTS"] = True`
340 |
341 | - Using atomic requests in production prevents several database consistency issues. Check [Django docs for more details](https://docs.djangoproject.com/en/5.0/topics/db/transactions/#tying-transactions-to-http-requests).
342 |
343 | - **Important:** When you are queueing a new Celery task directly from a Django view, particularly with little or no delay/ETA, it is essential to use `transaction.on_commit(lambda: my_task.delay())`. This ensures that the task is only queued after the associated database transaction has been successfully committed.
344 | - If `transaction.on_commit` is not utilized, or if a significant delay is not set, you risk encountering race conditions. In such scenarios, the Celery task might execute before the completion of the request's transaction. This can lead to inconsistencies and unexpected behavior, as the task might operate on a database state that does not yet reflect the changes made in the transaction. Read more about this problem on [this article](https://www.vinta.com.br/blog/database-concurrency-in-django-the-right-way).
345 |
346 | ### `CELERY_TASK_ACKS_LATE = True`
347 |
348 | - We believe Celery tasks should be idempotent. So for us it's safe to set `CELERY_TASK_ACKS_LATE = True` to ensure tasks will be re-queued after a worker failure. Check Celery docs on ["Should I use retry or acks_late?"](https://docs.celeryq.dev/en/stable/faq.html#faq-acks-late-vs-retry) for more info.
349 |
350 | ### Django-CSP
351 |
352 | Django-CSP helps implementing Content Security Policy (CSP) in Django projects to mitigate cross-site scripting (XSS) attacks by declaring which dynamic resources are allowed to load.
353 |
354 | In this project, we have defined several CSP settings that define the sources from which different types of resources can be loaded. If you need to load external images, fonts, or other resources, you will need to add the sources to the corresponding CSP settings. For example:
355 | - To load scripts from an external source, such as https://browser.sentry-cdn.com, you would add this source to `CSP_SCRIPT_SRC`.
356 | - To load images from an external source, such as https://example.com, you would add this source to `CSP_IMG_SRC`.
357 |
358 | Please note that you should only add trusted sources to these settings to maintain the security of your site. For more details, please refer to the [Django-CSP documentation](https://django-csp.readthedocs.io/en/latest/).
359 |
360 | ## Contributing
361 |
362 | If you wish to contribute to this project, please first discuss the change you wish to make via an [issue](https://github.com/vintasoftware/django-react-boilerplate/issues).
363 |
364 | Check our [contributing guide](https://github.com/vintasoftware/django-react-boilerplate/blob/main/CONTRIBUTING.md) to learn more about our development process and how you can test your changes to the boilerplate.
365 |
366 | ## Commercial Support
367 |
368 | [](https://www.vinta.com.br/)
369 |
370 | This project is maintained by [Vinta Software](https://www.vinta.com.br/) and is used in products of Vinta's clients. We are always looking for exciting work! If you need any commercial support, feel free to get in touch: contact@vinta.com.br
371 |
--------------------------------------------------------------------------------
/backend/.env.example:
--------------------------------------------------------------------------------
1 | DJANGO_SETTINGS_MODULE={{project_name}}.settings.local
2 | CELERY_BROKER_URL=amqp://broker:5672//
3 | REDIS_URL=redis://result:6379
4 | # Please choose postgres or sqlite as your DB:
5 | # DATABASE_URL=postgres://{{project_name}}:password@db:5432/{{project_name}}
6 | # DATABASE_URL=sqlite:///db.sqlite3
7 |
--------------------------------------------------------------------------------
/backend/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.12-slim
2 |
3 | ENV PYTHONFAULTHANDLER=1 \
4 | PYTHONUNBUFFERED=1 \
5 | PYTHONHASHSEED=random \
6 | PIP_NO_CACHE_DIR=off \
7 | PIP_DISABLE_PIP_VERSION_CHECK=on \
8 | PIP_DEFAULT_TIMEOUT=100 \
9 | POETRY_VERSION=1.7.1
10 |
11 | RUN groupadd user && useradd --create-home --home-dir /home/user -g user user
12 |
13 | # Install system dependencies
14 | RUN apt-get update && apt-get install python3-dev gcc build-essential libpq-dev -y
15 |
16 | # install python dependencies
17 | RUN pip install "poetry==$POETRY_VERSION"
18 | COPY pyproject.toml /home/user/app/
19 | COPY *poetry.lock /home/user/app/
20 |
21 | WORKDIR /home/user/app/
22 |
23 | RUN poetry config virtualenvs.create false
24 | RUN poetry install --with dev --no-root --no-interaction --no-ansi
25 |
26 | WORKDIR /home/user/app/backend
27 | COPY backend/ /home/user/app/backend
28 |
29 | USER user
30 | CMD gunicorn {{project_name}}.wsgi --log-file - -b 0.0.0.0:8000 --reload
31 |
--------------------------------------------------------------------------------
/backend/common/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dragonsea0927/django-react-boilerplate/d76b185f3f12cf3bfbda67db72693e45ab5ec2d7/backend/common/__init__.py
--------------------------------------------------------------------------------
/backend/common/context_processors.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 |
3 |
4 | def sentry_dsn(request):
5 | return {"SENTRY_DSN": settings.SENTRY_DSN}
6 |
7 |
8 | def commit_sha(request):
9 | return {"COMMIT_SHA": settings.COMMIT_SHA}
10 |
--------------------------------------------------------------------------------
/backend/common/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | from django.utils.translation import gettext_lazy as _
3 |
4 | from model_utils.fields import AutoCreatedField, AutoLastModifiedField
5 |
6 |
7 | class IndexedTimeStampedModel(models.Model):
8 | created = AutoCreatedField(_("created"), db_index=True)
9 | modified = AutoLastModifiedField(_("modified"), db_index=True)
10 |
11 | class Meta:
12 | abstract = True
13 |
--------------------------------------------------------------------------------
/backend/common/routes.py:
--------------------------------------------------------------------------------
1 | from .views import RestViewSet
2 |
3 |
4 | routes = [
5 | {"regex": r"rest", "viewset": RestViewSet, "basename": "Rest"},
6 | ]
7 |
--------------------------------------------------------------------------------
/backend/common/serializers.py:
--------------------------------------------------------------------------------
1 | from rest_framework import serializers
2 |
3 |
4 | class MessageSerializer(serializers.Serializer):
5 | message = serializers.CharField()
6 |
--------------------------------------------------------------------------------
/backend/common/tests.py:
--------------------------------------------------------------------------------
1 | from common.utils.tests import TestCaseUtils
2 |
3 |
4 | class TestIndexView(TestCaseUtils):
5 | view_name = "common:index"
6 |
7 | def test_returns_status_200(self):
8 | response = self.auth_client.get(self.reverse(self.view_name))
9 | self.assertResponse200(response)
10 |
--------------------------------------------------------------------------------
/backend/common/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 |
3 | from . import views
4 |
5 |
6 | app_name = "common"
7 | urlpatterns = [
8 | path("", views.IndexView.as_view(), name="index"),
9 | ]
10 |
--------------------------------------------------------------------------------
/backend/common/utils/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dragonsea0927/django-react-boilerplate/d76b185f3f12cf3bfbda67db72693e45ab5ec2d7/backend/common/utils/__init__.py
--------------------------------------------------------------------------------
/backend/common/utils/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 | from django.urls import reverse
3 |
4 | from model_bakery import baker
5 | from rest_framework.test import APIClient
6 |
7 |
8 | class TestCaseUtils(TestCase):
9 | def setUp(self):
10 | self._user_password = "123456"
11 | self.user = baker.prepare("users.User", email="user@email.com")
12 | self.user.set_password(self._user_password)
13 | self.user.save()
14 |
15 | self.auth_client = APIClient()
16 | self.auth_client.login(email=self.user.email, password=self._user_password)
17 |
18 | def reverse(self, name, *args, **kwargs):
19 | """Reverse a url, convenience to avoid having to import reverse in tests"""
20 | return reverse(name, args=args, kwargs=kwargs)
21 |
22 | def assertResponse200(self, response):
23 | """Given response has status_code 200 OK"""
24 | self.assertEqual(response.status_code, 200)
25 |
26 | def assertResponse201(self, response):
27 | """Given response has status_code 201 CREATED"""
28 | self.assertEqual(response.status_code, 201)
29 |
30 | def assertResponse204(self, response):
31 | """Given response has status_code 204 NO CONTENT"""
32 | self.assertEqual(response.status_code, 204)
33 |
34 | def assertResponse301(self, response):
35 | """Given response has status_code 301 MOVED PERMANENTLY"""
36 | self.assertEqual(response.status_code, 301)
37 |
38 | def assertResponse302(self, response):
39 | """Given response has status_code 302 FOUND"""
40 | self.assertEqual(response.status_code, 302)
41 |
42 | def assertResponse400(self, response):
43 | """Given response has status_code 400 BAD REQUEST"""
44 | self.assertEqual(response.status_code, 400)
45 |
46 | def assertResponse401(self, response):
47 | """Given response has status_code 401 UNAUTHORIZED"""
48 | self.assertEqual(response.status_code, 401)
49 |
50 | def assertResponse403(self, response):
51 | """Given response has status_code 403 FORBIDDEN"""
52 | self.assertEqual(response.status_code, 403)
53 |
54 | def assertResponse404(self, response):
55 | """Given response has status_code 404 NOT FOUND"""
56 | self.assertEqual(response.status_code, 404)
57 |
58 |
59 | class TestGetRequiresAuthenticatedUser:
60 | def test_get_requires_authenticated_user(self):
61 | response = self.client.get(self.view_url)
62 | self.assertResponse403(response)
63 |
64 |
65 | class TestAuthGetRequestSuccess:
66 | def test_auth_get_success(self):
67 | response = self.auth_client.get(self.view_url)
68 | self.assertResponse200(response)
69 |
--------------------------------------------------------------------------------
/backend/common/views.py:
--------------------------------------------------------------------------------
1 | from django.views import generic
2 |
3 | from drf_spectacular.utils import OpenApiExample, extend_schema
4 | from rest_framework import status, viewsets
5 | from rest_framework.decorators import action
6 | from rest_framework.permissions import AllowAny
7 | from rest_framework.response import Response
8 |
9 | from .serializers import MessageSerializer
10 |
11 |
12 | class IndexView(generic.TemplateView):
13 | template_name = "common/index.html"
14 |
15 |
16 | class RestViewSet(viewsets.ViewSet):
17 | serializer_class = MessageSerializer
18 |
19 | @extend_schema(
20 | summary="Check REST API",
21 | description="This endpoint checks if the REST API is working.",
22 | examples=[
23 | OpenApiExample(
24 | "Successful Response",
25 | value={
26 | "message": "This message comes from the backend. "
27 | "If you're seeing this, the REST API is working!"
28 | },
29 | response_only=True,
30 | )
31 | ],
32 | methods=["GET"],
33 | )
34 | @action(
35 | detail=False,
36 | methods=["get"],
37 | permission_classes=[AllowAny],
38 | url_path="rest-check",
39 | )
40 | def rest_check(self, request):
41 | serializer = self.serializer_class(
42 | data={
43 | "message": "This message comes from the backend. "
44 | "If you're seeing this, the REST API is working!"
45 | }
46 | )
47 | serializer.is_valid(raise_exception=True)
48 | return Response(serializer.data, status=status.HTTP_200_OK)
49 |
--------------------------------------------------------------------------------
/backend/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import os
4 | import sys
5 |
6 | from decouple import config
7 |
8 |
9 | if __name__ == "__main__":
10 | settings_module = config("DJANGO_SETTINGS_MODULE", default=None)
11 |
12 | if sys.argv[1] == "test":
13 | if settings_module:
14 | print(
15 | "Ignoring config('DJANGO_SETTINGS_MODULE') because it's test. "
16 | "Using '{{project_name}}.settings.test'"
17 | )
18 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "{{project_name}}.settings.test")
19 | else:
20 | if settings_module is None:
21 | print(
22 | "Error: no DJANGO_SETTINGS_MODULE found. Will NOT start devserver. "
23 | "Remember to create .env file at project root. "
24 | "Check README for more info."
25 | )
26 | sys.exit(1)
27 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", settings_module)
28 |
29 | from django.core.management import execute_from_command_line
30 |
31 | execute_from_command_line(sys.argv)
32 |
--------------------------------------------------------------------------------
/backend/project_name/__init__.py:
--------------------------------------------------------------------------------
1 | from .celery import app as celery_app # noqa
2 |
--------------------------------------------------------------------------------
/backend/project_name/celery.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 |
4 | from django.apps import apps
5 |
6 | from celery import Celery
7 | from decouple import config
8 |
9 | from .celerybeat_schedule import CELERYBEAT_SCHEDULE
10 |
11 |
12 | settings_module = config("DJANGO_SETTINGS_MODULE", default=None)
13 | if settings_module is None:
14 | print(
15 | "Error: no DJANGO_SETTINGS_MODULE found. Will NOT start devserver. "
16 | "Remember to create .env file at project root. "
17 | "Check README for more info."
18 | )
19 | sys.exit(1)
20 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", settings_module)
21 |
22 | app = Celery("{{project_name}}_tasks")
23 | app.config_from_object("django.conf:settings", namespace="CELERY")
24 | app.autodiscover_tasks(lambda: [n.name for n in apps.get_app_configs()])
25 | app.conf.update(CELERYBEAT_SCHEDULE=CELERYBEAT_SCHEDULE)
26 |
--------------------------------------------------------------------------------
/backend/project_name/celerybeat_schedule.py:
--------------------------------------------------------------------------------
1 | from celery.schedules import crontab
2 |
3 |
4 | CELERYBEAT_SCHEDULE = {
5 | # Internal tasks
6 | "clearsessions": {
7 | "schedule": crontab(hour=3, minute=0),
8 | "task": "users.tasks.clearsessions",
9 | },
10 | }
11 |
--------------------------------------------------------------------------------
/backend/project_name/settings/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dragonsea0927/django-react-boilerplate/d76b185f3f12cf3bfbda67db72693e45ab5ec2d7/backend/project_name/settings/__init__.py
--------------------------------------------------------------------------------
/backend/project_name/settings/base.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from decouple import config
4 | from dj_database_url import parse as db_url
5 |
6 |
7 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
8 |
9 |
10 | def base_dir_join(*args):
11 | return os.path.join(BASE_DIR, *args)
12 |
13 |
14 | SITE_ID = 1
15 |
16 | DEBUG = True
17 |
18 | ADMINS = (("Admin", "foo@example.com"),)
19 |
20 | AUTH_USER_MODEL = "users.User"
21 |
22 | ALLOWED_HOSTS = []
23 |
24 | DATABASES = {
25 | "default": config("DATABASE_URL", cast=db_url),
26 | }
27 |
28 | INSTALLED_APPS = [
29 | "django.contrib.admin",
30 | "django.contrib.auth",
31 | "django.contrib.contenttypes",
32 | "django.contrib.sessions",
33 | "django.contrib.messages",
34 | "django.contrib.staticfiles",
35 | "django_js_reverse",
36 | "webpack_loader",
37 | "import_export",
38 | "rest_framework",
39 | "drf_spectacular",
40 | "defender",
41 | "django_guid",
42 | "common",
43 | "users",
44 | ]
45 |
46 | MIDDLEWARE = [
47 | "django.middleware.gzip.GZipMiddleware",
48 | "django.middleware.security.SecurityMiddleware",
49 | "django_permissions_policy.PermissionsPolicyMiddleware",
50 | "whitenoise.middleware.WhiteNoiseMiddleware",
51 | "django.contrib.sessions.middleware.SessionMiddleware",
52 | "django.middleware.common.CommonMiddleware",
53 | "django.middleware.csrf.CsrfViewMiddleware",
54 | "django.contrib.auth.middleware.AuthenticationMiddleware",
55 | "django.contrib.messages.middleware.MessageMiddleware",
56 | "django.middleware.clickjacking.XFrameOptionsMiddleware",
57 | "csp.middleware.CSPMiddleware",
58 | "defender.middleware.FailedLoginMiddleware",
59 | "django_guid.middleware.guid_middleware",
60 | ]
61 |
62 | ROOT_URLCONF = "{{project_name}}.urls"
63 |
64 | TEMPLATES = [
65 | {
66 | "BACKEND": "django.template.backends.django.DjangoTemplates",
67 | "DIRS": [base_dir_join("templates")],
68 | "OPTIONS": {
69 | "context_processors": [
70 | "django.template.context_processors.debug",
71 | "django.template.context_processors.request",
72 | "django.contrib.auth.context_processors.auth",
73 | "django.contrib.messages.context_processors.messages",
74 | "common.context_processors.sentry_dsn",
75 | "common.context_processors.commit_sha",
76 | ],
77 | "loaders": [
78 | (
79 | "django.template.loaders.cached.Loader",
80 | [
81 | "django.template.loaders.filesystem.Loader",
82 | "django.template.loaders.app_directories.Loader",
83 | ],
84 | ),
85 | ],
86 | },
87 | },
88 | ]
89 |
90 | WSGI_APPLICATION = "{{project_name}}.wsgi.application"
91 |
92 | AUTH_PASSWORD_VALIDATORS = [
93 | {
94 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
95 | },
96 | {
97 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
98 | },
99 | {
100 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
101 | },
102 | {
103 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
104 | },
105 | ]
106 |
107 | REST_FRAMEWORK = {
108 | "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination",
109 | "PAGE_SIZE": 10,
110 | "DEFAULT_AUTHENTICATION_CLASSES": [
111 | "rest_framework.authentication.SessionAuthentication",
112 | ],
113 | "DEFAULT_PERMISSION_CLASSES": [
114 | "rest_framework.permissions.IsAuthenticated",
115 | ],
116 | "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
117 | }
118 |
119 | # drf-spectacular
120 | SPECTACULAR_SETTINGS = {
121 | "TITLE": "Vinta Boilerplate API",
122 | "DESCRIPTION": "A Django project boilerplate with Vinta's best practices",
123 | "VERSION": "0.1.0",
124 | "SERVE_INCLUDE_SCHEMA": False,
125 | }
126 |
127 | LANGUAGE_CODE = "en-us"
128 |
129 | TIME_ZONE = "UTC"
130 |
131 | USE_I18N = True
132 |
133 |
134 | USE_TZ = True
135 |
136 | STATICFILES_DIRS = (base_dir_join("../frontend"),)
137 |
138 | # Webpack
139 | WEBPACK_LOADER = {
140 | "DEFAULT": {
141 | "CACHE": False, # on DEBUG should be False
142 | "STATS_FILE": base_dir_join("../webpack-stats.json"),
143 | "POLL_INTERVAL": 0.1,
144 | "IGNORE": [r".+\.hot-update.js", r".+\.map"],
145 | }
146 | }
147 |
148 | # Celery
149 | # Recommended settings for reliability: https://gist.github.com/fjsj/da41321ac96cf28a96235cb20e7236f6
150 | CELERY_ACCEPT_CONTENT = ["json"]
151 | CELERY_TASK_SERIALIZER = "json"
152 | CELERY_RESULT_SERIALIZER = "json"
153 | CELERY_TASK_ACKS_LATE = True
154 | CELERY_TIMEZONE = TIME_ZONE
155 | CELERY_BROKER_TRANSPORT_OPTIONS = {"confirm_publish": True, "confirm_timeout": 5.0}
156 | CELERY_BROKER_POOL_LIMIT = config("CELERY_BROKER_POOL_LIMIT", cast=int, default=1)
157 | CELERY_BROKER_CONNECTION_TIMEOUT = config(
158 | "CELERY_BROKER_CONNECTION_TIMEOUT", cast=float, default=30.0
159 | )
160 | CELERY_REDIS_MAX_CONNECTIONS = config(
161 | "CELERY_REDIS_MAX_CONNECTIONS", cast=lambda v: int(v) if v else None, default=None
162 | )
163 | CELERY_TASK_ACKS_ON_FAILURE_OR_TIMEOUT = config(
164 | "CELERY_TASK_ACKS_ON_FAILURE_OR_TIMEOUT", cast=bool, default=True
165 | )
166 | CELERY_TASK_REJECT_ON_WORKER_LOST = config(
167 | "CELERY_TASK_REJECT_ON_WORKER_LOST", cast=bool, default=False
168 | )
169 | CELERY_WORKER_PREFETCH_MULTIPLIER = config("CELERY_WORKER_PREFETCH_MULTIPLIER", cast=int, default=1)
170 | CELERY_WORKER_CONCURRENCY = config(
171 | "CELERY_WORKER_CONCURRENCY", cast=lambda v: int(v) if v else None, default=None
172 | )
173 | CELERY_WORKER_MAX_TASKS_PER_CHILD = config(
174 | "CELERY_WORKER_MAX_TASKS_PER_CHILD", cast=int, default=1000
175 | )
176 | CELERY_WORKER_SEND_TASK_EVENTS = config("CELERY_WORKER_SEND_TASK_EVENTS", cast=bool, default=True)
177 | CELERY_EVENT_QUEUE_EXPIRES = config("CELERY_EVENT_QUEUE_EXPIRES", cast=float, default=60.0)
178 | CELERY_EVENT_QUEUE_TTL = config("CELERY_EVENT_QUEUE_TTL", cast=float, default=5.0)
179 |
180 | # Sentry
181 | SENTRY_DSN = config("SENTRY_DSN", default="")
182 | COMMIT_SHA = config("RENDER_GIT_COMMIT", default="")
183 |
184 | # Fix for Safari 12 compatibility issues, please check:
185 | # https://github.com/vintasoftware/safari-samesite-cookie-issue
186 | CSRF_COOKIE_SAMESITE = None
187 | SESSION_COOKIE_SAMESITE = None
188 |
189 | # Default primary key field type
190 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
191 |
192 | # All available policies are listed at:
193 | # https://github.com/w3c/webappsec-permissions-policy/blob/main/features.md
194 | # Empty list means the policy is disabled
195 | PERMISSIONS_POLICY = {
196 | "accelerometer": [],
197 | "camera": [],
198 | "display-capture": [],
199 | "encrypted-media": [],
200 | "geolocation": [],
201 | "gyroscope": [],
202 | "magnetometer": [],
203 | "microphone": [],
204 | "midi": [],
205 | "payment": [],
206 | "usb": [],
207 | "xr-spatial-tracking": [],
208 | }
209 |
210 | # Django-CSP
211 | CSP_INCLUDE_NONCE_IN = ["script-src", "style-src", "font-src"]
212 | CSP_SCRIPT_SRC = [
213 | "'self'",
214 | "'unsafe-inline'",
215 | "'unsafe-eval'",
216 | "https://browser.sentry-cdn.com",
217 | # drf-spectacular UI (Swagger and ReDoc)
218 | "https://cdn.jsdelivr.net/npm/swagger-ui-dist@latest/",
219 | "https://cdn.jsdelivr.net/npm/redoc@latest/",
220 | "blob:",
221 | ] + [f"*{host}" if host.startswith(".") else host for host in ALLOWED_HOSTS]
222 | CSP_CONNECT_SRC = [
223 | "'self'",
224 | "*.sentry.io",
225 | ] + [f"*{host}" if host.startswith(".") else host for host in ALLOWED_HOSTS]
226 | CSP_STYLE_SRC = [
227 | "'self'",
228 | "'unsafe-inline'",
229 | # drf-spectacular UI (Swagger and ReDoc)
230 | "https://cdn.jsdelivr.net/npm/swagger-ui-dist@latest/",
231 | "https://cdn.jsdelivr.net/npm/redoc@latest/",
232 | "https://fonts.googleapis.com",
233 | ]
234 | CSP_FONT_SRC = [
235 | "'self'",
236 | "'unsafe-inline'",
237 | # drf-spectacular UI (Swagger and ReDoc)
238 | "https://fonts.gstatic.com",
239 | ] + [f"*{host}" if host.startswith(".") else host for host in ALLOWED_HOSTS]
240 | CSP_IMG_SRC = [
241 | "'self'",
242 | # drf-spectacular UI (Swagger and ReDoc)
243 | "data:",
244 | "https://cdn.jsdelivr.net/npm/swagger-ui-dist@latest/",
245 | "https://cdn.redoc.ly/redoc/",
246 | ]
247 |
248 | # Django-defender
249 | DEFENDER_LOGIN_FAILURE_LIMIT = 3
250 | DEFENDER_COOLOFF_TIME = 300 # 5 minutes
251 | DEFENDER_LOCKOUT_TEMPLATE = "defender/lockout.html"
252 | DEFENDER_REDIS_URL = config("REDIS_URL")
253 |
--------------------------------------------------------------------------------
/backend/project_name/settings/local.py.example:
--------------------------------------------------------------------------------
1 | from .local_base import * # noqa
2 |
--------------------------------------------------------------------------------
/backend/project_name/settings/local_base.py:
--------------------------------------------------------------------------------
1 | from .base import *
2 |
3 |
4 | DEBUG = True
5 |
6 | HOST = "http://localhost:8000"
7 |
8 | SECRET_KEY = "secret" # noqa: S105
9 |
10 | STATIC_ROOT = base_dir_join("staticfiles")
11 | STATIC_URL = "/static/"
12 |
13 | MEDIA_ROOT = base_dir_join("mediafiles")
14 | MEDIA_URL = "/media/"
15 |
16 | STORAGES = {
17 | "default": {
18 | "BACKEND": "django.core.files.storage.FileSystemStorage",
19 | },
20 | "staticfiles": {
21 | "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
22 | },
23 | }
24 |
25 | AUTH_PASSWORD_VALIDATORS = [] # allow easy passwords only on local
26 |
27 | # Celery
28 | CELERY_BROKER_URL = config("CELERY_BROKER_URL", default="")
29 | CELERY_TASK_ALWAYS_EAGER = True
30 | CELERY_TASK_EAGER_PROPAGATES = True
31 |
32 | # Email settings for mailhog
33 | EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
34 | EMAIL_HOST = "mailhog"
35 | EMAIL_PORT = 1025
36 |
37 | # Logging
38 | LOGGING = {
39 | "version": 1,
40 | "disable_existing_loggers": False,
41 | "filters": {
42 | "correlation_id": {"()": "django_guid.log_filters.CorrelationId"},
43 | },
44 | "formatters": {
45 | "standard": {
46 | "format": "%(levelname)-8s [%(asctime)s] [%(correlation_id)s] %(name)s: %(message)s"
47 | },
48 | },
49 | "handlers": {
50 | "console": {
51 | "level": "DEBUG",
52 | "class": "logging.StreamHandler",
53 | "formatter": "standard",
54 | "filters": ["correlation_id"],
55 | },
56 | },
57 | "loggers": {
58 | "": {"handlers": ["console"], "level": "INFO"},
59 | "celery": {"handlers": ["console"], "level": "INFO"},
60 | "django_guid": {
61 | "handlers": ["console"],
62 | "level": "WARNING",
63 | "propagate": False,
64 | },
65 | },
66 | }
67 |
68 | JS_REVERSE_JS_MINIFY = False
69 |
70 | # Django-CSP
71 | LOCAL_HOST_URL = "http://localhost:3000"
72 | LOCAL_HOST_WS_URL = "ws://localhost:3000/ws"
73 | CSP_SCRIPT_SRC += [LOCAL_HOST_URL, LOCAL_HOST_WS_URL]
74 | CSP_CONNECT_SRC += [LOCAL_HOST_URL, LOCAL_HOST_WS_URL]
75 | CSP_FONT_SRC += [LOCAL_HOST_URL]
76 | CSP_IMG_SRC += [LOCAL_HOST_URL]
77 |
--------------------------------------------------------------------------------
/backend/project_name/settings/production.py:
--------------------------------------------------------------------------------
1 | import sentry_sdk
2 | from decouple import Csv, config
3 | from django_guid.integrations import SentryIntegration as DjangoGUIDSentryIntegration
4 | from sentry_sdk.integrations.django import DjangoIntegration
5 |
6 | from .base import *
7 |
8 |
9 | DEBUG = False
10 |
11 | SECRET_KEY = config("SECRET_KEY")
12 |
13 | DATABASES["default"]["ATOMIC_REQUESTS"] = True
14 |
15 | ALLOWED_HOSTS = config("ALLOWED_HOSTS", cast=Csv())
16 |
17 | STATIC_ROOT = base_dir_join("staticfiles")
18 | STATIC_URL = "/static/"
19 |
20 | MEDIA_ROOT = base_dir_join("mediafiles")
21 | MEDIA_URL = "/media/"
22 |
23 | SERVER_EMAIL = "foo@example.com"
24 |
25 | EMAIL_HOST = "smtp.sendgrid.net"
26 | EMAIL_HOST_USER = config("SENDGRID_USERNAME")
27 | EMAIL_HOST_PASSWORD = config("SENDGRID_PASSWORD")
28 | EMAIL_PORT = 587
29 | EMAIL_USE_TLS = True
30 |
31 | # Security
32 | SECURE_HSTS_PRELOAD = True
33 | SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
34 | SECURE_SSL_REDIRECT = True
35 | SESSION_COOKIE_SECURE = True
36 | CSRF_COOKIE_SECURE = True
37 | SECURE_HSTS_SECONDS = config("SECURE_HSTS_SECONDS", default=3600, cast=int)
38 | SECURE_HSTS_INCLUDE_SUBDOMAINS = True
39 |
40 | SECURE_CONTENT_TYPE_NOSNIFF = True
41 | SECURE_BROWSER_XSS_FILTER = True
42 | X_FRAME_OPTIONS = "DENY"
43 |
44 | # Webpack
45 | WEBPACK_LOADER["DEFAULT"]["CACHE"] = True
46 |
47 | # Celery
48 | # Recommended settings for reliability: https://gist.github.com/fjsj/da41321ac96cf28a96235cb20e7236f6
49 | CELERY_BROKER_URL = config("RABBITMQ_URL", default="") or config("REDIS_URL")
50 | CELERY_RESULT_BACKEND = config("REDIS_URL")
51 | CELERY_SEND_TASK_ERROR_EMAILS = True
52 |
53 | # Redbeat https://redbeat.readthedocs.io/en/latest/config.html#redbeat-redis-url
54 | redbeat_redis_url = config("REDBEAT_REDIS_URL", default="")
55 |
56 | # Whitenoise
57 | STORAGES = {
58 | "default": {
59 | "BACKEND": "django.core.files.storage.FileSystemStorage",
60 | },
61 | "staticfiles": {
62 | "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
63 | },
64 | }
65 |
66 | # Django GUID
67 | DJANGO_GUID = {
68 | "INTEGRATIONS": [
69 | DjangoGUIDSentryIntegration(),
70 | ],
71 | }
72 |
73 | # django-log-request-id
74 | MIDDLEWARE.insert( # insert RequestIDMiddleware on the top
75 | 0, "log_request_id.middleware.RequestIDMiddleware"
76 | )
77 |
78 | LOG_REQUEST_ID_HEADER = "HTTP_X_REQUEST_ID"
79 | LOG_REQUESTS = True
80 |
81 | LOGGING = {
82 | "version": 1,
83 | "disable_existing_loggers": False,
84 | "filters": {
85 | "require_debug_false": {"()": "django.utils.log.RequireDebugFalse"},
86 | "request_id": {"()": "log_request_id.filters.RequestIDFilter"},
87 | "correlation_id": {"()": "django_guid.log_filters.CorrelationId"},
88 | },
89 | "formatters": {
90 | "standard": {
91 | "format": "%(levelname)-8s [%(asctime)s] [%(request_id)s] [%(correlation_id)s] %(name)s: %(message)s"
92 | },
93 | },
94 | "handlers": {
95 | "null": {
96 | "class": "logging.NullHandler",
97 | },
98 | "mail_admins": {
99 | "level": "ERROR",
100 | "class": "django.utils.log.AdminEmailHandler",
101 | "filters": ["require_debug_false"],
102 | },
103 | "console": {
104 | "level": "DEBUG",
105 | "class": "logging.StreamHandler",
106 | "filters": ["request_id", "correlation_id"],
107 | "formatter": "standard",
108 | },
109 | },
110 | "loggers": {
111 | "": {"handlers": ["console"], "level": "INFO"},
112 | "django.security.DisallowedHost": {
113 | "handlers": ["null"],
114 | "propagate": False,
115 | },
116 | "django.request": {
117 | "handlers": ["mail_admins"],
118 | "level": "ERROR",
119 | "propagate": True,
120 | },
121 | "log_request_id.middleware": {
122 | "handlers": ["console"],
123 | "level": "DEBUG",
124 | "propagate": False,
125 | },
126 | "django_guid": {
127 | "handlers": ["console"],
128 | "level": "WARNING",
129 | "propagate": False,
130 | },
131 | },
132 | }
133 |
134 | JS_REVERSE_EXCLUDE_NAMESPACES = ["admin"]
135 |
136 | # Sentry
137 | sentry_sdk.init(dsn=SENTRY_DSN, integrations=[DjangoIntegration()], release=COMMIT_SHA)
138 |
--------------------------------------------------------------------------------
/backend/project_name/settings/test.py:
--------------------------------------------------------------------------------
1 | from .base import *
2 |
3 |
4 | SECRET_KEY = "test" # nosec
5 |
6 | STATIC_ROOT = base_dir_join("staticfiles")
7 | STATIC_URL = "/static/"
8 |
9 | MEDIA_ROOT = base_dir_join("mediafiles")
10 | MEDIA_URL = "/media/"
11 |
12 | STORAGES = {
13 | "default": {
14 | "BACKEND": "django.core.files.storage.FileSystemStorage",
15 | },
16 | "staticfiles": {
17 | "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
18 | },
19 | }
20 |
21 | # Speed up password hashing
22 | PASSWORD_HASHERS = [
23 | "django.contrib.auth.hashers.MD5PasswordHasher",
24 | ]
25 |
26 | # Celery
27 | CELERY_TASK_ALWAYS_EAGER = True
28 | CELERY_TASK_EAGER_PROPAGATES = True
29 |
--------------------------------------------------------------------------------
/backend/project_name/urls.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from django.urls import include, path
3 |
4 | import django_js_reverse.views
5 | from common.routes import routes as common_routes
6 | from drf_spectacular.views import (
7 | SpectacularAPIView,
8 | SpectacularRedocView,
9 | SpectacularSwaggerView,
10 | )
11 | from rest_framework.routers import DefaultRouter
12 | from users.routes import routes as users_routes
13 |
14 |
15 | router = DefaultRouter()
16 |
17 | routes = common_routes + users_routes
18 | for route in routes:
19 | router.register(route["regex"], route["viewset"], basename=route["basename"])
20 |
21 | urlpatterns = [
22 | path("", include("common.urls"), name="common"),
23 | path("admin/", admin.site.urls, name="admin"),
24 | path("admin/defender/", include("defender.urls")),
25 | path("jsreverse/", django_js_reverse.views.urls_js, name="js_reverse"),
26 | path("api/", include(router.urls), name="api"),
27 | # drf-spectacular
28 | path("api/schema/", SpectacularAPIView.as_view(), name="schema"),
29 | path(
30 | "api/schema/swagger-ui/",
31 | SpectacularSwaggerView.as_view(url_name="schema"),
32 | name="swagger-ui",
33 | ),
34 | path(
35 | "api/schema/redoc/",
36 | SpectacularRedocView.as_view(url_name="schema"),
37 | name="redoc",
38 | ),
39 | ]
40 |
--------------------------------------------------------------------------------
/backend/project_name/wsgi.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from django.core.wsgi import get_wsgi_application
4 |
5 |
6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "{{project_name}}.settings.production")
7 |
8 | application = get_wsgi_application()
9 |
--------------------------------------------------------------------------------
/backend/templates/base.html:
--------------------------------------------------------------------------------
1 | {% load render_bundle from webpack_loader %}
2 |
3 |
4 |
5 |
6 |
7 |
8 | {% block title %}{% endblock %}
9 |
10 |
11 |
12 |
13 |
14 |
15 | {% render_bundle 'main' 'css' %}
16 |
17 |
18 |
19 | {% include 'includes/sentry_init.html' %}
20 |
21 |
22 | {% block body %}{% endblock %}
23 |
24 |
25 |
26 |
27 |
28 | {% render_bundle 'main' 'js' 'DEFAULT' %}
29 | {% block scripts %}{% endblock %}
30 |
31 |
32 |
--------------------------------------------------------------------------------
/backend/templates/common/index.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 |
3 |
4 | {% block body %}
5 |
6 |
Django React Boilerplate Example App
7 |
8 | Below is an example of a React app included in the Django template.
9 | Check backend/templates/common/index.html and frontend/js/index.js to understand how they're linked:
10 |