├── .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 | [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.0-4baaaa.svg)](code_of_conduct.md) 4 | [![License: MIT](https://img.shields.io/github/license/vintasoftware/django-react-boilerplate.svg)](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 [![main](https://github.com/vintasoftware/django-react-boilerplate/actions/workflows/main.yml/badge.svg)](https://github.com/vintasoftware/django-react-boilerplate/actions/workflows/main.yml) [![Known Vulnerabilities](https://snyk.io/test/github/vintasoftware/django-react-boilerplate/badge.svg)](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 | [![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](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 | [![alt text](https://avatars2.githubusercontent.com/u/5529080?s=80&v=4 "Vinta Logo")](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 |

11 |
12 |
13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /backend/templates/defender/lockout.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block body %} 4 |
5 |

Temporary User Lockout

6 |

7 | You have been locked out due to too many failed attempts. You have reached the failure limit of {{ failure_limit }} attempts. 8 |

9 |

10 | The lockout will last for {{ cooloff_time_seconds }} seconds (approximately {{ cooloff_time_minutes }} minutes). 11 |

12 |

Please try again after the cool off period.

13 |
14 | {% endblock %} -------------------------------------------------------------------------------- /backend/templates/includes/sentry_init.html: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /backend/users/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dragonsea0927/django-react-boilerplate/d76b185f3f12cf3bfbda67db72693e45ab5ec2d7/backend/users/__init__.py -------------------------------------------------------------------------------- /backend/users/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.auth.admin import UserAdmin 3 | from django.utils.translation import gettext_lazy as _ 4 | 5 | from .models import User 6 | 7 | 8 | @admin.register(User) 9 | class CustomUserAdmin(UserAdmin): 10 | list_display = ("id", "email", "created", "modified") 11 | list_filter = ("is_active", "is_staff", "groups") 12 | search_fields = ("email",) 13 | ordering = ("email",) 14 | filter_horizontal = ( 15 | "groups", 16 | "user_permissions", 17 | ) 18 | 19 | fieldsets = ( 20 | (None, {"fields": ("email", "password")}), 21 | ( 22 | _("Permissions"), 23 | {"fields": ("is_active", "is_staff", "is_superuser", "groups", "user_permissions")}, 24 | ), 25 | ) 26 | add_fieldsets = ((None, {"classes": ("wide",), "fields": ("email", "password1", "password2")}),) 27 | -------------------------------------------------------------------------------- /backend/users/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class UsersConfig(AppConfig): 5 | name = "users" 6 | -------------------------------------------------------------------------------- /backend/users/managers.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import BaseUserManager 2 | 3 | 4 | class UserManager(BaseUserManager): 5 | def create_user(self, email, password=None, **kwargs): 6 | email = self.normalize_email(email) 7 | user = self.model(email=email, **kwargs) 8 | user.set_password(password) 9 | user.save(using=self._db) 10 | return user 11 | 12 | def create_superuser(self, **kwargs): 13 | user = self.create_user(**kwargs) 14 | user.is_superuser = True 15 | user.is_staff = True 16 | user.save(using=self._db) 17 | return user 18 | -------------------------------------------------------------------------------- /backend/users/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dragonsea0927/django-react-boilerplate/d76b185f3f12cf3bfbda67db72693e45ab5ec2d7/backend/users/migrations/__init__.py -------------------------------------------------------------------------------- /backend/users/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin 2 | from django.db import models 3 | from django.utils.translation import gettext_lazy as _ 4 | 5 | from common.models import IndexedTimeStampedModel 6 | 7 | from .managers import UserManager 8 | 9 | 10 | class User(AbstractBaseUser, PermissionsMixin, IndexedTimeStampedModel): 11 | email = models.EmailField(max_length=255, unique=True) 12 | is_staff = models.BooleanField( 13 | default=False, help_text=_("Designates whether the user can log into this admin site.") 14 | ) 15 | is_active = models.BooleanField( 16 | default=True, 17 | help_text=_( 18 | "Designates whether this user should be treated as " 19 | "active. Unselect this instead of deleting accounts." 20 | ), 21 | ) 22 | 23 | objects = UserManager() 24 | 25 | USERNAME_FIELD = "email" 26 | 27 | def get_full_name(self): 28 | return self.email 29 | 30 | def get_short_name(self): 31 | return self.email 32 | 33 | def __str__(self): 34 | return self.email 35 | -------------------------------------------------------------------------------- /backend/users/routes.py: -------------------------------------------------------------------------------- 1 | from .views import UserViewSet 2 | 3 | 4 | routes = [ 5 | {"regex": r"users", "viewset": UserViewSet, "basename": "user"}, 6 | ] 7 | -------------------------------------------------------------------------------- /backend/users/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from .models import User 4 | 5 | 6 | class UserSerializer(serializers.ModelSerializer): 7 | class Meta: 8 | model = User 9 | fields = [ # noqa: RUF012 10 | "id", 11 | "email", 12 | "is_active", 13 | "is_staff", 14 | "is_superuser", 15 | "created", 16 | "modified", 17 | "last_login", 18 | ] 19 | -------------------------------------------------------------------------------- /backend/users/tasks.py: -------------------------------------------------------------------------------- 1 | from django.core import management 2 | 3 | from {{project_name}} import celery_app 4 | 5 | 6 | @celery_app.task 7 | def clearsessions(): 8 | management.call_command("clearsessions") 9 | -------------------------------------------------------------------------------- /backend/users/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dragonsea0927/django-react-boilerplate/d76b185f3f12cf3bfbda67db72693e45ab5ec2d7/backend/users/tests/__init__.py -------------------------------------------------------------------------------- /backend/users/tests/test_views.py: -------------------------------------------------------------------------------- 1 | from django.urls import reverse 2 | 3 | from common.utils.tests import TestCaseUtils 4 | from model_bakery import baker 5 | from rest_framework.test import APITestCase 6 | 7 | from ..models import User 8 | 9 | 10 | class UserViewSetTest(TestCaseUtils, APITestCase): 11 | def test_list_users(self): 12 | baker.make(User, _fill_optional=True, _quantity=5) 13 | 14 | response = self.auth_client.get(reverse("user-list")) 15 | 16 | self.assertResponse200(response) 17 | # Note: One user is already created in the setUp method of TestCaseUtils 18 | self.assertEqual(response.data.get("count"), 6) 19 | self.assertEqual(len(response.data.get("results")), 6) 20 | 21 | def test_create_user(self): 22 | data = { 23 | "email": "testuser@test.com", 24 | "password": "12345678", 25 | } 26 | 27 | response = self.auth_client.post(reverse("user-list"), data=data) 28 | 29 | self.assertResponse201(response) 30 | user = User.objects.get(id=response.data["id"]) 31 | self.assertEqual(user.email, data["email"]) 32 | 33 | def test_retrieve_user(self): 34 | user = baker.make(User, _fill_optional=True) 35 | 36 | response = self.auth_client.get(reverse("user-detail", args=[user.id])) 37 | 38 | self.assertResponse200(response) 39 | self.assertEqual(response.data["id"], user.id) 40 | self.assertEqual(response.data["email"], user.email) 41 | 42 | def test_put_update_user(self): 43 | user = baker.make(User, email="testuser@test.com", _fill_optional=True) 44 | data = { 45 | "email": "user@test.com", 46 | "password": "87654321", 47 | } 48 | 49 | response = self.auth_client.put( 50 | reverse("user-detail", args=[user.id]), data=data 51 | ) 52 | 53 | self.assertResponse200(response) 54 | user.refresh_from_db() 55 | self.assertEqual(user.email, data["email"]) 56 | 57 | def test_patch_update_user(self): 58 | user = baker.make(User, email="testuser@test.com", _fill_optional=True) 59 | data = { 60 | "email": "user@test.com", 61 | } 62 | 63 | response = self.auth_client.patch( 64 | reverse("user-detail", args=[user.id]), data=data 65 | ) 66 | 67 | self.assertResponse200(response) 68 | user.refresh_from_db() 69 | self.assertEqual(user.email, data["email"]) 70 | 71 | def test_delete_user(self): 72 | user = baker.make(User, _fill_optional=True) 73 | 74 | response = self.auth_client.delete(reverse("user-detail", args=[user.id])) 75 | 76 | self.assertResponse204(response) 77 | self.assertFalse(User.objects.filter(id=user.id).exists()) 78 | -------------------------------------------------------------------------------- /backend/users/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework import viewsets 2 | 3 | from .models import User 4 | from .serializers import UserSerializer 5 | 6 | 7 | class UserViewSet(viewsets.ModelViewSet): 8 | queryset = User.objects.all() 9 | serializer_class = UserSerializer 10 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | 3 | services: 4 | db: 5 | image: "postgres:alpine" 6 | environment: 7 | - POSTGRES_USER={{project_name}} 8 | - POSTGRES_PASSWORD=password 9 | - POSTGRES_DB={{project_name}} 10 | ports: 11 | - "5432" 12 | volumes: 13 | - dbdata:/var/lib/postgresql/data:delegated 14 | 15 | broker: 16 | image: "rabbitmq:alpine" 17 | 18 | result: 19 | image: "redis:alpine" 20 | ports: 21 | - "6379:6379" 22 | 23 | frontend: 24 | build: 25 | dockerfile: frontend/Dockerfile 26 | context: . 27 | volumes: 28 | - .:/app/ 29 | - /app/node_modules 30 | ports: 31 | - "3000:3000" 32 | 33 | backend: 34 | build: 35 | dockerfile: backend/Dockerfile 36 | context: . 37 | ports: 38 | - "8000:8000" 39 | volumes: 40 | - ./:/home/user/app/ 41 | env_file: backend/.env 42 | depends_on: 43 | - db 44 | - broker 45 | - result 46 | - frontend 47 | 48 | celery: 49 | build: 50 | dockerfile: backend/Dockerfile 51 | context: . 52 | command: celery --app={{project_name}} worker --loglevel=info 53 | volumes: 54 | - ./:/home/user/app/ 55 | env_file: backend/.env 56 | depends_on: 57 | - db 58 | - broker 59 | - result 60 | 61 | mailhog: # service for faking a SMTP server 62 | image: mailhog/mailhog 63 | ports: 64 | - '1025:1025' # smtp server 65 | - '8025:8025' # web ui 66 | 67 | volumes: 68 | dbdata: 69 | name: {{project_name}}_dbdata 70 | external: true 71 | -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-alpine 2 | 3 | WORKDIR /app/ 4 | ADD package.json /app/package.json 5 | RUN npm install 6 | ADD . /app/ 7 | 8 | CMD ["npm", "run", "dev"] 9 | -------------------------------------------------------------------------------- /frontend/assets/images/django-logo-negative.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dragonsea0927/django-react-boilerplate/d76b185f3f12cf3bfbda67db72693e45ab5ec2d7/frontend/assets/images/django-logo-negative.png -------------------------------------------------------------------------------- /frontend/assets/images/django-logo-positive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dragonsea0927/django-react-boilerplate/d76b185f3f12cf3bfbda67db72693e45ab5ec2d7/frontend/assets/images/django-logo-positive.png -------------------------------------------------------------------------------- /frontend/assets/images/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.png" { 2 | const value: string; 3 | export = value; 4 | } 5 | 6 | declare module "*.jpg" { 7 | const value: string; 8 | export = value; 9 | } 10 | 11 | declare module "*.jpeg" { 12 | const value: string; 13 | export = value; 14 | } 15 | 16 | declare module "*.gif" { 17 | const value: string; 18 | export = value; 19 | } 20 | 21 | declare module "*.svg" { 22 | const value: string; 23 | export = value; 24 | } 25 | -------------------------------------------------------------------------------- /frontend/js/App.tsx: -------------------------------------------------------------------------------- 1 | import * as Sentry from "@sentry/react"; 2 | import cookie from "cookie"; 3 | 4 | import { OpenAPI } from "./api"; 5 | import Home from "./pages/Home"; 6 | 7 | OpenAPI.interceptors.request.use((request) => { 8 | const { csrftoken } = cookie.parse(document.cookie); 9 | if (request.headers && csrftoken) { 10 | request.headers["X-CSRFTOKEN"] = csrftoken; 11 | } 12 | return request; 13 | }); 14 | 15 | const App = () => ( 16 | An error has occurred

}> 17 | 18 |
19 | ); 20 | 21 | export default App; 22 | -------------------------------------------------------------------------------- /frontend/js/api/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dragonsea0927/django-react-boilerplate/d76b185f3f12cf3bfbda67db72693e45ab5ec2d7/frontend/js/api/index.ts -------------------------------------------------------------------------------- /frontend/js/constants/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dragonsea0927/django-react-boilerplate/d76b185f3f12cf3bfbda67db72693e45ab5ec2d7/frontend/js/constants/index.ts -------------------------------------------------------------------------------- /frontend/js/index.tsx: -------------------------------------------------------------------------------- 1 | // import pages 2 | import * as Sentry from "@sentry/browser"; 3 | import { createRoot } from "react-dom/client"; 4 | 5 | import App from "./App"; 6 | 7 | import "../sass/style.scss"; 8 | 9 | Sentry.init({ 10 | dsn: window.SENTRY_DSN, 11 | release: window.COMMIT_SHA, 12 | }); 13 | 14 | const root = createRoot(document.getElementById("react-app") as HTMLElement); 15 | root.render(); 16 | -------------------------------------------------------------------------------- /frontend/js/pages/Home.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import Button from "react-bootstrap/Button"; 3 | 4 | import DjangoImgSrc from "../../assets/images/django-logo-negative.png"; 5 | import { RestService } from "../api"; 6 | 7 | const Home = () => { 8 | const [showBugComponent, setShowBugComponent] = useState(false); 9 | const [restCheck, setRestCheck] = 10 | useState>>(); 11 | 12 | useEffect(() => { 13 | async function onFetchRestCheck() { 14 | setRestCheck(await RestService.restRestCheckRetrieve()); 15 | } 16 | onFetchRestCheck(); 17 | }, []); 18 | 19 | return ( 20 | <> 21 |

Static assets

22 |
23 | If you are seeing the green Django logo on a white background and this 24 | text color is #092e20, frontend static files serving is working: 25 |
26 |
27 |
28 | Below this text, you should see an img tag with the white Django logo 29 | on a green background: 30 |
31 | Django Negative Logo 32 |
33 |

Rest API

34 |

{restCheck?.message}

35 | 39 | {/* NOTE: The next line intentionally contains an error for testing frontend errors in Sentry. */} 40 | {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */} 41 | {showBugComponent && (showBugComponent as any).field.notexist} 42 | 43 | ); 44 | }; 45 | 46 | export default Home; 47 | -------------------------------------------------------------------------------- /frontend/js/pages/__tests__/Home.spec.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen, waitFor } from "@testing-library/react"; 2 | 3 | import { RestService } from "../../api"; 4 | import Home from "../Home"; 5 | 6 | jest.mock("../../api", () => ({ 7 | RestService: { 8 | restRestCheckRetrieve: jest.fn(), 9 | }, 10 | })); 11 | 12 | describe("Home", () => { 13 | beforeEach(() => { 14 | (RestService.restRestCheckRetrieve as jest.Mock).mockResolvedValue({ 15 | message: "Test Result", 16 | }); 17 | }); 18 | 19 | afterEach(() => { 20 | jest.clearAllMocks(); 21 | }); 22 | 23 | test("renders static assets and rest API data", async () => { 24 | render(); 25 | 26 | expect(screen.getByText("Static assets")).toBeInTheDocument(); 27 | expect(screen.getByText("Rest API")).toBeInTheDocument(); 28 | expect(await screen.findByText("Test Result")).toBeInTheDocument(); 29 | }); 30 | 31 | test("calls restRestCheckRetrieve on mount", async () => { 32 | render(); 33 | 34 | await waitFor(() => { 35 | expect(RestService.restRestCheckRetrieve).toHaveBeenCalledWith(); 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /frontend/js/routes/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dragonsea0927/django-react-boilerplate/d76b185f3f12cf3bfbda67db72693e45ab5ec2d7/frontend/js/routes/index.ts -------------------------------------------------------------------------------- /frontend/js/types/index.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | 3 | declare global { 4 | interface Window { 5 | SENTRY_DSN: string; 6 | COMMIT_SHA: string; 7 | 8 | Urls: unknown; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /frontend/js/utils/index.ts: -------------------------------------------------------------------------------- 1 | import Urls from "./urls"; 2 | 3 | export { Urls }; 4 | -------------------------------------------------------------------------------- /frontend/js/utils/urls.ts: -------------------------------------------------------------------------------- 1 | const { Urls } = window; 2 | 3 | export default Urls; 4 | -------------------------------------------------------------------------------- /frontend/sass/_global.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dragonsea0927/django-react-boilerplate/d76b185f3f12cf3bfbda67db72693e45ab5ec2d7/frontend/sass/_global.scss -------------------------------------------------------------------------------- /frontend/sass/_variables.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dragonsea0927/django-react-boilerplate/d76b185f3f12cf3bfbda67db72693e45ab5ec2d7/frontend/sass/_variables.scss -------------------------------------------------------------------------------- /frontend/sass/components/_all.scss: -------------------------------------------------------------------------------- 1 | // Components 2 | 3 | // @import ""; -------------------------------------------------------------------------------- /frontend/sass/helpers/_all.scss: -------------------------------------------------------------------------------- 1 | // Helpers 2 | 3 | @import "typography"; 4 | @import "functions"; 5 | @import "placeholders"; 6 | @import "helpers"; 7 | @import "mixins"; -------------------------------------------------------------------------------- /frontend/sass/helpers/_functions.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dragonsea0927/django-react-boilerplate/d76b185f3f12cf3bfbda67db72693e45ab5ec2d7/frontend/sass/helpers/_functions.scss -------------------------------------------------------------------------------- /frontend/sass/helpers/_helpers.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dragonsea0927/django-react-boilerplate/d76b185f3f12cf3bfbda67db72693e45ab5ec2d7/frontend/sass/helpers/_helpers.scss -------------------------------------------------------------------------------- /frontend/sass/helpers/_mixins.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dragonsea0927/django-react-boilerplate/d76b185f3f12cf3bfbda67db72693e45ab5ec2d7/frontend/sass/helpers/_mixins.scss -------------------------------------------------------------------------------- /frontend/sass/helpers/_placeholders.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dragonsea0927/django-react-boilerplate/d76b185f3f12cf3bfbda67db72693e45ab5ec2d7/frontend/sass/helpers/_placeholders.scss -------------------------------------------------------------------------------- /frontend/sass/helpers/_typography.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dragonsea0927/django-react-boilerplate/d76b185f3f12cf3bfbda67db72693e45ab5ec2d7/frontend/sass/helpers/_typography.scss -------------------------------------------------------------------------------- /frontend/sass/pages/_all.scss: -------------------------------------------------------------------------------- 1 | // Pages 2 | 3 | // @import ""; -------------------------------------------------------------------------------- /frontend/sass/style.scss: -------------------------------------------------------------------------------- 1 | // Main Style 2 | 3 | // General 4 | @import "variables"; 5 | @import "vendor/bootstrap-includes"; 6 | @import "helpers/all"; 7 | @import "global"; 8 | @import "components/all"; 9 | @import "pages/all"; 10 | 11 | #django-background { 12 | color: #092e20; 13 | font-size: 11pt; 14 | background-repeat: no-repeat; 15 | background-size: auto 200px; 16 | background-position: center; 17 | height: 300px; 18 | background-image: url('../assets/images/django-logo-positive.png'); 19 | } 20 | 21 | #django-logo-wrapper { 22 | color: #092e20; 23 | & > img { 24 | width: 100px; 25 | } 26 | margin-bottom: 1em; 27 | } 28 | -------------------------------------------------------------------------------- /frontend/sass/vendor/_bootstrap-includes.scss: -------------------------------------------------------------------------------- 1 | // Mixins 2 | @import "~bootstrap/scss/functions"; 3 | @import "~bootstrap/scss/variables"; 4 | @import "~bootstrap/scss/mixins"; 5 | 6 | /* Customizations */ 7 | @import "custom-bootstrap"; 8 | 9 | // Configuration 10 | @import "~bootstrap/scss/functions"; 11 | @import "~bootstrap/scss/variables"; 12 | @import "~bootstrap/scss/variables-dark"; 13 | @import "~bootstrap/scss/maps"; 14 | @import "~bootstrap/scss/mixins"; 15 | @import "~bootstrap/scss/utilities"; 16 | 17 | // Layout 18 | @import "~bootstrap/scss/root"; 19 | @import "~bootstrap/scss/reboot"; 20 | @import "~bootstrap/scss/type"; 21 | @import "~bootstrap/scss/images"; 22 | @import "~bootstrap/scss/containers"; 23 | @import "~bootstrap/scss/grid"; 24 | 25 | // Components 26 | @import "~bootstrap/scss/tables"; 27 | @import "~bootstrap/scss/forms"; 28 | @import "~bootstrap/scss/buttons"; 29 | @import "~bootstrap/scss/transitions"; 30 | @import "~bootstrap/scss/button-group"; 31 | @import "~bootstrap/scss/card"; 32 | @import "~bootstrap/scss/nav"; 33 | @import "~bootstrap/scss/navbar"; 34 | // @import "~bootstrap/scss/breadcrumb"; 35 | // @import "~bootstrap/scss/pagination"; 36 | // @import "~bootstrap/scss/badge"; 37 | // @import "~bootstrap/scss/alert"; 38 | // @import "~bootstrap/scss/progress"; 39 | // @import "~bootstrap/scss/list-group"; 40 | // @import "~bootstrap/scss/close"; 41 | // @import "~bootstrap/scss/toasts"; 42 | // @import "~bootstrap/scss/spinners"; 43 | // @import "~bootstrap/scss/offcanvas"; 44 | // @import "~bootstrap/scss/placeholders"; 45 | 46 | /* Components with JS */ 47 | // @import "~bootstrap/scss/accordion"; 48 | // @import "~bootstrap/scss/carousel"; 49 | // @import "~bootstrap/scss/dropdown"; 50 | // @import "~bootstrap/scss/modal"; 51 | // @import "~bootstrap/scss/popover"; 52 | // @import "~bootstrap/scss/tooltip"; 53 | 54 | 55 | // Helpers 56 | @import "~bootstrap/scss/helpers"; 57 | 58 | // Utilities 59 | @import "~bootstrap/scss/utilities/api"; 60 | // scss-docs-end import-stack -------------------------------------------------------------------------------- /frontend/sass/vendor/custom-bootstrap.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dragonsea0927/django-react-boilerplate/d76b185f3f12cf3bfbda67db72693e45ab5ec2d7/frontend/sass/vendor/custom-bootstrap.scss -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | moduleNameMapper: { 5 | "^.+\\.(css|scss|png|svg|jpg|jpeg|gif|webp)$": "jest-transform-stub", 6 | }, 7 | transformIgnorePatterns: ["node_modules/*"], 8 | modulePaths: ["frontend", "frontend/js", "frontend/js/app"], 9 | setupFilesAfterEnv: ["./jest.setup.js"], 10 | testEnvironment: "jsdom", 11 | collectCoverageFrom: ["frontend/js/**/*.{js,jsx,ts,tsx}"], 12 | coveragePathIgnorePatterns: [ 13 | "frontend/js/store.js", 14 | "frontend/js/index.js", 15 | "frontend/js/constants/*", 16 | "frontend/js/pages/*", 17 | "frontend/js/tests/*", 18 | ], 19 | coverageThreshold: { 20 | global: { 21 | statements: 10, 22 | }, 23 | }, 24 | transform: { 25 | "^.+\\.(t|j)sx?$": "@swc/jest", 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | import "@testing-library/jest-dom"; 2 | -------------------------------------------------------------------------------- /openapi-ts.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "@hey-api/openapi-ts"; 2 | 3 | export default defineConfig({ 4 | input: "backend/schema.yml", 5 | output: { 6 | path: "frontend/js/api", 7 | format: "prettier", 8 | }, 9 | client: "axios", 10 | useOptions: true, 11 | }); 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "{{project_name}}-frontend", 3 | "version": "0.0.1", 4 | "private": true, 5 | "description": "{{project_name}} frontend.", 6 | "engines": { 7 | "node": ">=20 <21" 8 | }, 9 | "browserslist": "> 0.25%, not dead", 10 | "main": "frontend/js/index.tsx", 11 | "scripts": { 12 | "test": "jest", 13 | "test:watch": "npm test -- --watch", 14 | "test:update": "npm test -- --u", 15 | "dev": "webpack serve --mode=development --hot", 16 | "build": "NODE_ENV=production tsc && webpack --progress --bail --mode=production", 17 | "lint": "eslint frontend --fix", 18 | "tsc": "tsc -p ./tsconfig.json --noEmit", 19 | "coverage": "jest --coverage", 20 | "openapi-ts": "openapi-ts" 21 | }, 22 | "dependencies": { 23 | "@hey-api/openapi-ts": "^0.45.0", 24 | "@sentry/browser": "~8.0.0", 25 | "@sentry/react": "~8.0.0", 26 | "axios": "~1.6.8", 27 | "bootstrap": "~5.3.3", 28 | "classnames": "~2.5.1", 29 | "cookie": "~0.6.0", 30 | "lodash": "~4.17.21", 31 | "marked": "~12.0.2", 32 | "react": "~18.3.1", 33 | "react-bootstrap": "~2.10.2", 34 | "react-dom": "~18.3.1", 35 | "react-router": "~6.23.1" 36 | }, 37 | "devDependencies": { 38 | "@pmmmwh/react-refresh-webpack-plugin": "^0.5.13", 39 | "@swc/cli": "^0.3.12", 40 | "@swc/core": "^1.5.6", 41 | "@swc/jest": "^0.2.36", 42 | "@testing-library/jest-dom": "~6.4.5", 43 | "@testing-library/react": "~15.0.7", 44 | "@testing-library/user-event": "~14.5.2", 45 | "@types/cookie": "^0.6.0", 46 | "@types/jest": "^29.5.12", 47 | "@types/node": "^20.12.12", 48 | "@types/react": "^18.3.2", 49 | "@types/react-dom": "^18.3.0", 50 | "@typescript-eslint/eslint-plugin": "^6.21.0", 51 | "@typescript-eslint/parser": "^6.21.0", 52 | "ajv": "~8.13.0", 53 | "circular-dependency-plugin": "~5.2.2", 54 | "css-loader": "~7.1.1", 55 | "eslint": "~8.57.0", 56 | "eslint-config-vinta": "github:vintasoftware/eslint-config-vinta#9dd2803f4864a2e9942c636f78b367b4d5a56c67", 57 | "eslint-import-resolver-typescript": "^3.6.1", 58 | "eslint-import-resolver-webpack": "~0.13.8", 59 | "eslint-plugin-import": "~2.29.1", 60 | "eslint-plugin-jest": "~27.9.0", 61 | "eslint-plugin-jsx-a11y": "~6.8.0", 62 | "eslint-plugin-prettier": "~5.1.3", 63 | "eslint-plugin-promise": "~6.1.1", 64 | "eslint-plugin-react": "~7.34.1", 65 | "eslint-plugin-react-hooks": "~4.6.2", 66 | "eslint-plugin-sonarjs": "~0.23.0", 67 | "eslint-plugin-unicorn": "~49.0.0", 68 | "identity-obj-proxy": "~3.0.0", 69 | "jest": "~29.7.0", 70 | "jest-environment-jsdom": "~29.7.0", 71 | "jest-transform-stub": "^2.0.0", 72 | "mini-css-extract-plugin": "~2.9.0", 73 | "postcss": "~8.4.38", 74 | "postcss-loader": "~8.1.1", 75 | "prettier": "~3.2.5", 76 | "react-refresh": "^0.14.2", 77 | "sass": "~1.77.1", 78 | "sass-loader": "~14.2.1", 79 | "style-loader": "~4.0.0", 80 | "swc-loader": "^0.2.6", 81 | "typescript": "^5.4.5", 82 | "webpack": "~5.91.0", 83 | "webpack-bundle-tracker": "~3.1.0", 84 | "webpack-cli": "~5.1.4", 85 | "webpack-dev-server": "~5.0.4", 86 | "whatwg-fetch": "~3.6.20" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /proj_main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | build: 6 | name: Build {{project_name}} 7 | strategy: 8 | matrix: 9 | python: [3.12] 10 | node: [20.13] 11 | env: 12 | DATABASE_URL: "sqlite:///" 13 | REDIS_URL: "redis://" 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v4 18 | - name: Store branch and latest SHA 19 | run: | 20 | echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})" 21 | echo "::set-output name=sha_short::$(git rev-parse --short HEAD)" 22 | id: git 23 | - name: Setup Python ${% templatetag openvariable %} matrix.python {% templatetag closevariable %} 24 | uses: actions/setup-python@v5 25 | with: 26 | python-version: ${% templatetag openvariable %} matrix.python {% templatetag closevariable %} 27 | - name: Setup Node ${% templatetag openvariable %} matrix.node {% templatetag closevariable %} 28 | uses: actions/setup-node@v4 29 | with: 30 | node-version: ${% templatetag openvariable %} matrix.node {% templatetag closevariable %} 31 | - name: Cache node modules 32 | uses: actions/cache@v4 33 | env: 34 | cache_name: node-modules-cache 35 | with: 36 | path: ~/.npm 37 | key: build-${% templatetag openvariable%} env.cache_name {% templatetag closevariable %}-${% templatetag openvariable%} steps.git.outputs.branch {% templatetag closevariable %}-${% templatetag openvariable%} steps.git.outputs.sha_short {% templatetag closevariable %} 38 | restore-keys: | 39 | build-${% templatetag openvariable%} env.cache_name {% templatetag closevariable %}-${% templatetag openvariable%} steps.git.outputs.branch {% templatetag closevariable %}-${% templatetag openvariable%} steps.git.outputs.sha_short {% templatetag closevariable %} 40 | build-${% templatetag openvariable%} env.cache_name {% templatetag closevariable %}-${% templatetag openvariable%} steps.git.outputs.branch {% templatetag closevariable %} 41 | build-${% templatetag openvariable%} env.cache_name {% templatetag closevariable %} 42 | - name: Cache pip 43 | uses: actions/cache@v4 44 | env: 45 | cache_name: pip-cache 46 | with: 47 | path: ~/.cache/pip 48 | key: build-${% templatetag openvariable%} env.cache_name {% templatetag closevariable %}-${% templatetag openvariable%} steps.git.outputs.branch {% templatetag closevariable %}-${% templatetag openvariable%} steps.git.outputs.sha_short {% templatetag closevariable %} 49 | restore-keys: | 50 | build-${% templatetag openvariable%} env.cache_name {% templatetag closevariable %}-${% templatetag openvariable%} steps.git.outputs.branch {% templatetag closevariable %}-${% templatetag openvariable%} steps.git.outputs.sha_short {% templatetag closevariable %} 51 | build-${% templatetag openvariable%} env.cache_name {% templatetag closevariable %}-${% templatetag openvariable%} steps.git.outputs.branch {% templatetag closevariable %} 52 | build-${% templatetag openvariable%} env.cache_name {% templatetag closevariable %} 53 | - run: python -m pip install --upgrade pip 54 | - run: python -m pip install poetry==1.7.1 55 | - run: curl -s https://packagecloud.io/install/repositories/github/git-lfs/script.deb.sh | sudo bash 56 | - run: sudo apt-get install git-lfs --upgrade 57 | - run: poetry install --with dev --no-root --no-interaction --no-ansi 58 | - run: npm install 59 | - run: npm run build 60 | - run: npm run lint 61 | - name: Generate secret key 62 | run: echo '::set-output name=SECRET_KEY::`python -c "import uuid; print(uuid.uuid4().hex + uuid.uuid4().hex)"`' 63 | id: secret_id_generator 64 | - name: Linting 65 | run: poetry run ruff check ./backend/ 66 | env: 67 | DJANGO_SETTINGS_MODULE: "{{project_name}}.settings.local_base" 68 | SECRET_KEY: ${% templatetag openvariable %} steps.secret_id_generator.outputs.SECRET_KEY {% templatetag closevariable %} 69 | DATABASE_URL: "sqlite:///" 70 | ALLOWED_HOSTS: ".example.org" 71 | SENDGRID_USERNAME: "test" 72 | SENDGRID_PASSWORD: "test" 73 | REDIS_URL: "redis://" 74 | - run: poetry run pre-commit run --all-files 75 | env: 76 | SKIP: ruff,eslint,missing-migrations,backend-schema 77 | - run: poetry run python manage.py makemigrations --check --dry-run 78 | env: 79 | DJANGO_SETTINGS_MODULE: "{{project_name}}.settings.production" 80 | SECRET_KEY: ${% templatetag openvariable %} steps.secret_id_generator.outputs.SECRET_KEY {% templatetag closevariable %} 81 | DATABASE_URL: "sqlite:///" 82 | ALLOWED_HOSTS: ".example.org" 83 | SENDGRID_USERNAME: "test" 84 | SENDGRID_PASSWORD: "test" 85 | REDIS_URL: "redis://" 86 | working-directory: backend 87 | - run: poetry run python manage.py check --deploy 88 | env: 89 | DJANGO_SETTINGS_MODULE: "{{project_name}}.settings.production" 90 | SECRET_KEY: ${% templatetag openvariable %} steps.secret_id_generator.outputs.SECRET_KEY {% templatetag closevariable %} 91 | DATABASE_URL: "sqlite:///" 92 | ALLOWED_HOSTS: ".example.org" 93 | SENDGRID_USERNAME: "test" 94 | SENDGRID_PASSWORD: "test" 95 | REDIS_URL: "redis://" 96 | working-directory: backend 97 | - run: | 98 | poetry run coverage run manage.py test 99 | mkdir -p junit 100 | poetry run coverage xml -o junit/test-results.xml 101 | working-directory: backend 102 | - run: npm run test 103 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "{{project_name}}" 3 | version = "0.1.0" 4 | description = "Django 5, React, Bootstrap 5 with Python 3 and webpack project boilerplate" 5 | authors = ["Vinta Software "] 6 | license = "MIT" 7 | readme = "README.md" 8 | 9 | [tool.poetry.dependencies] 10 | python = "^3.12" 11 | Django = "^5.0" 12 | celery = {extras = ["redis"], version = "^5.3.6"} 13 | celery-redbeat = "^2.1.1" 14 | django-model-utils = "^4.3.1" 15 | django-webpack-loader = "^3.1.0" 16 | django-js-reverse = "^0.10.2" 17 | django-import-export = "^3.3.5" 18 | djangorestframework = "^3.14.0" 19 | python-decouple = "^3.8" 20 | psycopg = "^3.1.19" 21 | brotlipy = "^0.7.0" 22 | django-log-request-id = "^2.1.0" 23 | dj-database-url = "^2.1.0" 24 | gunicorn = "^21.2.0" 25 | whitenoise = "^6.6.0" 26 | ipython = "^8.18.1" 27 | sentry-sdk = "^1.39.1" 28 | setuptools = "^69.0.2" 29 | django-permissions-policy = "^4.18.0" 30 | django-csp = "^3.7" 31 | django-defender = "^0.9.7" 32 | django-guid = "^3.4.0" 33 | drf-spectacular = "^0.27.2" 34 | 35 | [tool.poetry.group.dev.dependencies] 36 | coverage = "^7.2.7" 37 | model-bakery = "^1.12.0" 38 | pre-commit = "^3.3.3" 39 | ruff = "^0.1.8" 40 | 41 | [build-system] 42 | requires = ["poetry-core>=1.2.0"] 43 | build-backend = "poetry.core.masonry.api" 44 | 45 | [tool.ruff] 46 | select = [ 47 | # pycodestyle 48 | "E", 49 | # Pyflakes 50 | "F", 51 | # pep8-naming 52 | "N", 53 | # pyupgrade 54 | "UP", 55 | # flake8-bugbear 56 | "B", 57 | # flake8-bandit 58 | "S", 59 | # flake8-blind-except 60 | "BLE", 61 | # flake8-builtins 62 | "A", 63 | # flake8-django 64 | "DJ", 65 | # isort 66 | "I", 67 | # flake8-logging-format 68 | "G", 69 | # flake8-no-pep420 70 | "INP", 71 | # Ruff-specific rules 72 | "RUF" 73 | ] 74 | exclude = [ 75 | ".bzr", 76 | ".direnv", 77 | ".eggs", 78 | ".git", 79 | ".git-rewrite", 80 | ".hg", 81 | ".mypy_cache", 82 | ".nox", 83 | ".pants.d", 84 | ".pytype", 85 | ".ruff_cache", 86 | ".svn", 87 | ".tox", 88 | ".venv", 89 | "__pypackages__", 90 | "_build", 91 | "buck-out", 92 | "build", 93 | "dist", 94 | "node_modules", 95 | "venv", 96 | "virtualenvs", 97 | ] 98 | ignore = [ 99 | # Disable eradicate (commented code removal) 100 | "ERA001", 101 | # Disable Conflicting lint rules, 102 | # see https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules 103 | "W191", 104 | "E501", 105 | "E111", 106 | "E117", 107 | "D206", 108 | "D300", 109 | "Q000", 110 | "Q001", 111 | "Q002", 112 | "Q003", 113 | "COM812", 114 | "COM819", 115 | "ISC001", 116 | "ISC002", 117 | # Disable unused `noqa` directive 118 | "RUF100", 119 | ] 120 | line-length = 100 121 | indent-width = 4 122 | target-version = "py312" 123 | # Allow unused variables when underscore-prefixed: 124 | dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" 125 | 126 | [tool.ruff.pycodestyle] 127 | ignore-overlong-task-comments = true 128 | 129 | [tool.ruff.lint.isort] 130 | section-order = ["future", "standard-library", "django", "third-party", "first-party", "local-folder"] 131 | lines-after-imports = 2 132 | 133 | [tool.ruff.lint.isort.sections] 134 | # Group all Django imports into a separate section. 135 | "django" = ["django"] 136 | 137 | [tool.ruff.per-file-ignores] 138 | # Ignore "E402", "F403", "F405" (import violations) in __init__.py files. 139 | # Ignore "S" (flake8-bandit) and "N802" (function name should be lowercase) in tests and docs. 140 | # Ignore "RUF" (Ruff-specific rules) and "I" (isort) in migrations. 141 | "__init__.py" = ["E402", "F403", "F405"] 142 | "**/{tests,docs}/*" = ["E402", "F403", "F405", "S", "N802"] 143 | "**/*test*.py" = ["E402", "F403", "F405", "S", "N802"] 144 | "**/{settings}/*" = ["E402", "F403", "F405"] 145 | "**/migrations/*" = ["RUF", "I"] 146 | 147 | [tool.coverage.run] 148 | branch = true 149 | source = ["backend"] 150 | omit = ["**/venv/*", "**/env/*", "**/virtualenvs/*", "**/node_modules/*", "**/migrations/*", "**/settings/*", "**/tests/*"] 151 | -------------------------------------------------------------------------------- /render.yaml: -------------------------------------------------------------------------------- 1 | databases: 2 | - name: {{project_name}}-postgres 3 | databaseName: {{project_name}} 4 | plan: free 5 | user: {{project_name}} 6 | 7 | services: 8 | - type: redis 9 | name: {{project_name}}-redis 10 | plan: free 11 | ipAllowList: [] # only allow internal connections 12 | 13 | - type: web 14 | name: {{project_name}} 15 | plan: free 16 | runtime: python 17 | buildCommand: "./render_build.sh" 18 | startCommand: "poetry run gunicorn {{project_name}}.wsgi:application --chdir backend --limit-request-line 8188 --log-file -" 19 | envVars: 20 | - key: ENABLE_DJANGO_COLLECTSTATIC 21 | value: 1 22 | - key: AUTO_MIGRATE 23 | value: 1 24 | - key: DATABASE_URL 25 | fromDatabase: 26 | name: {{project_name}}-postgres 27 | property: connectionString 28 | - key: REDIS_URL 29 | fromService: 30 | type: redis 31 | name: {{project_name}}-redis 32 | property: connectionString 33 | - fromGroup: python-services 34 | - fromGroup: integrations-credentials 35 | 36 | # As there aren't free plans for Workers in Render, the configuration for 37 | # celery workers/beat will be commented by default 38 | # 39 | # - type: worker 40 | # name: worker-default 41 | # runtime: python 42 | # env: python 43 | # buildCommand: poetry install 44 | # startCommand: "poetry run celery --workdir backend --app={{project_name}} worker --loglevel=info --max-memory-per-child=$WORKER_MAX_MEMORY --concurrency=$WORKER_CONCURRENCY" 45 | # envVars: 46 | # - key: REMAP_SIGTERM 47 | # value: SIGQUIT 48 | # - key: CELERY_BROKER_POOL_LIMIT 49 | # value: 1 50 | # - key: CELERY_BROKER_CONNECTION_TIMEOUT 51 | # value: 30.0 52 | # - key: CELERY_REDIS_MAX_CONNECTIONS 53 | # value: null 54 | # - key: CELERY_TASK_ACKS_ON_FAILURE_OR_TIMEOUT 55 | # value: true 56 | # - key: CELERY_TASK_REJECT_ON_WORKER_LOST 57 | # value: false 58 | # - key: CELERY_WORKER_PREFETCH_MULTIPLIER 59 | # value: 1 60 | # - key: CELERY_WORKER_CONCURRENCY 61 | # value: null 62 | # - key: CELERY_WORKER_MAX_TASKS_PER_CHILD 63 | # value: 1000 64 | # - key: CELERY_WORKER_SEND_TASK_EVENTS 65 | # value: true 66 | # - key: CELERY_EVENT_QUEUE_EXPIRES 67 | # value: 60.0 68 | # - key: CELERY_EVENT_QUEUE_TTL 69 | # value: 5.0 70 | # - key: WORKER_MAX_MEMORY 71 | # sync: false 72 | # - key: WORKER_CONCURRENCY 73 | # sync: false 74 | # - key: DATABASE_URL 75 | # fromDatabase: 76 | # name: {{project_name}}-postgres 77 | # property: connectionString 78 | # - key: REDIS_URL 79 | # fromService: 80 | # type: redis 81 | # name: {{project_name}}-redis 82 | # property: connectionString 83 | # - fromGroup: python-services 84 | # - fromGroup: integrations-credentials 85 | # - type: worker 86 | # name: beat 87 | # runtime: python 88 | # env: python 89 | # buildCommand: poetry install 90 | # startCommand: "poetry run celery --workdir backend --app={{project_name}} beat --loglevel=info" 91 | # envVars: 92 | # - key: REMAP_SIGTERM 93 | # value: SIGQUIT 94 | # - key: DATABASE_URL 95 | # fromDatabase: 96 | # name: {{project_name}}-postgres 97 | # property: connectionString 98 | # - key: REDIS_URL 99 | # fromService: 100 | # type: redis 101 | # name: {{project_name}}-redis 102 | # property: connectionString 103 | # - fromGroup: python-services 104 | # - fromGroup: integrations-credentials 105 | 106 | envVarGroups: 107 | - name: python-services 108 | envVars: 109 | - key: PYTHON_VERSION 110 | value: 3.12.0 111 | - key: POETRY_VERSION 112 | value: 1.7.1 113 | - key: SECRET_KEY 114 | generateValue: true 115 | - key: DJANGO_SETTINGS_MODULE 116 | value: {{project_name}}.settings.production 117 | - key: ALLOWED_HOSTS 118 | value: '*' 119 | - name: integrations-credentials 120 | envVars: 121 | - key: SENDGRID_USERNAME 122 | value: apikey 123 | - key: SENDGRID_PASSWORD 124 | value: placeholder-value 125 | -------------------------------------------------------------------------------- /render_build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euxo pipefail 3 | 4 | echo "-----> Build hook" 5 | 6 | echo "-----> Build frontend" 7 | npm install 8 | npm run build 9 | echo "-----> Build frontend done" 10 | 11 | echo "-----> Poetry install" 12 | poetry install --without dev --no-root --no-interaction 13 | echo "-----> Poetry done" 14 | 15 | echo "-----> Running manage.py check --deploy --fail-level WARNING" 16 | poetry run backend/manage.py check --deploy --fail-level WARNING 17 | 18 | if [ -n "$ENABLE_DJANGO_COLLECTSTATIC" ] && [ "$ENABLE_DJANGO_COLLECTSTATIC" == 1 ]; then 19 | echo "-----> Running collectstatic" 20 | 21 | echo "-----> Collecting static files" 22 | poetry run backend/manage.py collectstatic --noinput 2>&1 | sed '/^Copying/d;/^$/d;/^ /d' 23 | 24 | echo 25 | fi 26 | 27 | if [ -n "$AUTO_MIGRATE" ] && [ "$AUTO_MIGRATE" == 1 ]; then 28 | echo "-----> Running manage.py migrate" 29 | poetry run backend/manage.py migrate --noinput 30 | fi 31 | 32 | echo "-----> Pushing source maps to Sentry" 33 | if [ -n "$SENTRY_API_KEY" ] && [ -n "$SENTRY_ORG" ] && [ -n "$SENTRY_PROJECT_NAME" ] && [ -n "$RENDER_GIT_COMMIT" ]; then 34 | npx @sentry/cli --auth-token=$SENTRY_API_KEY releases --org=$SENTRY_ORG --project=$SENTRY_PROJECT_NAME files $RENDER_GIT_COMMIT upload-sourcemaps ./frontend/webpack_bundles/ --url-prefix "~/static/webpack_bundles/" --rewrite 35 | rm ./frontend/webpack_bundles/*.js.map 36 | fi 37 | 38 | echo "-----> Post-compile done" 39 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "build", 4 | "module": "esnext", 5 | "target": "ES2021", 6 | 7 | "allowJs": true, 8 | "baseUrl": "./", 9 | "esModuleInterop": true, 10 | "jsx": "react-jsx", 11 | "moduleResolution": "node", 12 | "noEmit": true, 13 | "paths": { "*": ["./frontend/*", "./frontend/js/*"] }, 14 | "skipLibCheck": true, 15 | "strict": true, 16 | "types": ["node", "jest", "@testing-library/jest-dom"] 17 | }, 18 | "include": ["./frontend/**/*.ts", "./frontend/**/*.tsx"], 19 | "exclude": ["node_modules", "build", "frontend/**/*.js", "frontend/**/*.jsx"] 20 | } 21 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | const ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin"); 4 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 5 | const BundleTracker = require("webpack-bundle-tracker"); 6 | 7 | module.exports = (env, argv) => { 8 | const isDev = argv.mode === "development"; 9 | const nodeModulesDir = path.resolve(__dirname, "node_modules"); 10 | const localhostOutput = { 11 | path: path.resolve("./frontend/webpack_bundles/"), 12 | publicPath: "http://localhost:3000/frontend/webpack_bundles/", 13 | filename: "[name].js", 14 | }; 15 | const productionOutput = { 16 | path: path.resolve("./frontend/webpack_bundles/"), 17 | publicPath: "auto", 18 | filename: "[name]-[chunkhash].js", 19 | }; 20 | 21 | return { 22 | mode: isDev ? "development" : "production", 23 | devtool: "source-map", 24 | devServer: { 25 | hot: true, 26 | historyApiFallback: true, 27 | host: "0.0.0.0", 28 | port: 3000, 29 | // Allow CORS requests from the Django dev server domain: 30 | headers: { "Access-Control-Allow-Origin": "*" }, 31 | }, 32 | context: __dirname, 33 | entry: ["./frontend/js/index.tsx"], 34 | output: isDev ? localhostOutput : productionOutput, 35 | module: { 36 | rules: [ 37 | { 38 | test: /\.(js|mjs|jsx|ts|tsx)$/, 39 | use: { 40 | loader: "swc-loader", 41 | }, 42 | }, 43 | { 44 | test: /\.css$/, 45 | use: [ 46 | isDev && "style-loader", 47 | !isDev && MiniCssExtractPlugin.loader, 48 | "css-loader", 49 | { 50 | loader: "postcss-loader", 51 | options: { 52 | postcssOptions: { 53 | plugins: [["postcss-preset-env"]], 54 | }, 55 | }, 56 | }, 57 | ].filter(Boolean), 58 | }, 59 | { 60 | test: /\.s[ac]ss$/i, 61 | use: [ 62 | // Creates `style` nodes from JS strings 63 | isDev && "style-loader", 64 | // Optimizes CSS in chunks 65 | !isDev && MiniCssExtractPlugin.loader, 66 | // Translates CSS into CommonJS 67 | "css-loader", 68 | // Compiles Sass to CSS 69 | "sass-loader", 70 | ].filter(Boolean), 71 | }, 72 | { 73 | test: /\.(svg)(\?v=\d+\.\d+\.\d+)?$/, 74 | type: "asset", 75 | }, 76 | { 77 | test: /\.(woff(2)?|eot|ttf|otf)(\?v=\d+\.\d+\.\d+)?$/, 78 | type: "asset", 79 | }, 80 | { 81 | test: /\.(png|jpg|jpeg|gif|webp)?$/, 82 | type: "asset", 83 | }, 84 | ], 85 | }, 86 | plugins: [ 87 | !isDev && 88 | new MiniCssExtractPlugin({ filename: "[name]-[chunkhash].css" }), 89 | isDev && new ReactRefreshWebpackPlugin(), 90 | new BundleTracker({ 91 | path: __dirname, 92 | filename: "webpack-stats.json", 93 | }), 94 | ].filter(Boolean), 95 | resolve: { 96 | modules: [nodeModulesDir, path.resolve(__dirname, "frontend/js/")], 97 | extensions: [".js", ".jsx", ".ts", ".tsx"], 98 | }, 99 | optimization: { 100 | minimize: !isDev, 101 | splitChunks: { 102 | // include all types of chunks 103 | chunks: "all", 104 | }, 105 | }, 106 | }; 107 | }; 108 | --------------------------------------------------------------------------------