├── .babelrc ├── .coveragerc ├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── .nvmrc ├── Makefile ├── PULL_REQUEST_TEMPLATE.md ├── Procfile ├── README.md ├── core ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ └── __init__.py ├── models.py ├── templatetags │ ├── __init__.py │ └── render_vite_bundle.py ├── tests.py └── views.py ├── create_project.sh ├── cypress.json ├── cypress ├── fixtures │ └── WelcomePage │ │ └── sample-api-view.json ├── integration │ ├── SampleNestedPage │ │ └── index.spec.js │ └── WelcomePage │ │ └── index.spec.js ├── plugins │ └── index.js └── support │ ├── commands.js │ └── index.js ├── env.example ├── hooks └── pre-push.sh ├── manage.py ├── package.json ├── poetry.lock ├── postcss.config.js ├── project ├── __init__.py ├── settings.py ├── urls.py └── wsgi.py ├── pyproject.toml ├── pytest.ini ├── react-app ├── index.html ├── js │ ├── Header.jsx │ ├── NoMatch404.jsx │ ├── Routes.jsx │ ├── SampleNestedPage │ │ └── index.jsx │ ├── WelcomePage │ │ ├── __snapshots__ │ │ │ └── index.test.js.snap │ │ ├── actions.js │ │ ├── index.jsx │ │ ├── index.test.js │ │ └── reducers.js │ ├── main.jsx │ └── rootReducer.js └── scss │ ├── _base.scss │ ├── _overrides.scss │ ├── _variables.scss │ ├── components │ └── _welcome.scss │ └── main.scss ├── static └── images │ ├── my_app_icon.png │ └── splash-640x1136.png ├── templates └── frontend │ └── index.html ├── tests ├── __init__.py └── test_sample.py └── vite.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/env", 5 | { 6 | "targets": { 7 | "browsers": "last 3 versions" 8 | } 9 | } 10 | ], 11 | "@babel/react" 12 | ], 13 | "plugins": [ 14 | "@babel/plugin-transform-runtime", 15 | [ 16 | "@babel/plugin-proposal-class-properties", 17 | { 18 | "loose": true 19 | } 20 | ], 21 | [ 22 | "@babel/plugin-proposal-private-methods", 23 | { 24 | "loose": true 25 | } 26 | ], 27 | [ 28 | "@babel/plugin-proposal-private-property-in-object", 29 | { 30 | "loose": true 31 | } 32 | ] 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | show_missing = True 3 | omit = 4 | manage.py 5 | venv/* 6 | *wsgi.py -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 2 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | end_of_line = lf 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | 16 | [Makefile] 17 | indent_style = tab 18 | 19 | [*.py] 20 | indent_size = 4 21 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: "@babel/eslint-parser", 3 | extends: [ 4 | "airbnb", 5 | "prettier", 6 | "plugin:react/recommended", 7 | "plugin:cypress/recommended", 8 | ], 9 | plugins: ["prettier"], 10 | rules: { 11 | "arrow-body-style": ["error", "as-needed"], 12 | "class-methods-use-this": [1, { exceptMethods: ["render"] }], 13 | "jsx-a11y/label-has-associated-control": [ 14 | "error", 15 | { 16 | labelComponents: [], 17 | labelAttributes: [], 18 | controlComponents: [], 19 | assert: "both", 20 | depth: 25, 21 | }, 22 | ], 23 | "import/no-named-as-default": "off", 24 | "import/prefer-default-export": "off", 25 | 26 | "no-case-declarations": "off", 27 | "no-console": ["error", { allow: ["error"] }], 28 | "no-param-reassign": "off", 29 | "no-shadow": "off", 30 | "no-underscore-dangle": "off", 31 | "no-unneeded-ternary": "off", 32 | 33 | "prettier/prettier": "error", 34 | 35 | "react/forbid-prop-types": "off", 36 | "react/jsx-filename-extension": "off", 37 | "react/jsx-one-expression-per-line": "off", 38 | "react/prefer-stateless-function": "off", 39 | "react/state-in-constructor": "off", 40 | "react/static-property-placement": "off", 41 | }, 42 | env: { 43 | es6: true, 44 | jest: true, 45 | browser: true, 46 | }, 47 | parserOptions: { 48 | sourceType: "module", 49 | allowImportExportEverywhere: true, 50 | }, 51 | }; 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | pyvenv.cfg 27 | *.egg 28 | bin/ 29 | include/ 30 | lib/ 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | .hypothesis/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # pyenv 78 | .python-version 79 | 80 | # celery beat schedule file 81 | celerybeat-schedule 82 | 83 | # SageMath parsed files 84 | *.sage.py 85 | 86 | # dotenv 87 | .env 88 | 89 | # virtualenv 90 | .venv 91 | venv/ 92 | ENV/ 93 | 94 | # Spyder project settings 95 | .spyderproject 96 | .spyproject 97 | 98 | # Rope project settings 99 | .ropeproject 100 | 101 | # mkdocs documentation 102 | /site 103 | 104 | # mypy 105 | .mypy_cache/ 106 | 107 | # SQLite 108 | db.sqlite3 109 | 110 | # Django collected static files 111 | staticfiles/ 112 | 113 | # Node 114 | node_modules 115 | package-lock.json 116 | yarn.lock 117 | pnpm-lock.yaml 118 | 119 | # VSCode 120 | .vscode 121 | 122 | # idea 123 | .idea/ 124 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/* 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := help 2 | 3 | help: ## show help message 4 | @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[$$()% a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) 5 | 6 | 7 | .PHONY: build_frontend run_frontend run_django run install_dependencies setup_node_environment 8 | 9 | build_frontend: ## build frontend application 10 | npm run build 11 | 12 | run_frontend: ## run frontend application 13 | npm run dev 14 | 15 | run_django: ## run backend application 16 | python manage.py runserver 17 | 18 | run_cypress: 19 | ./node_modules/.bin/cypress open 20 | 21 | run: ## run both backend and frontend applications 22 | make -j2 run_django run_frontend 23 | 24 | run_e2e: 25 | make -j3 run_django run_frontend run_cypress 26 | 27 | install_dependencies: ## install both backend and frontend dependencies 28 | make setup_node_environment 29 | poetry install 30 | npm i 31 | 32 | setup_node_environment: ## setup node environment 33 | curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash 34 | export NVM_DIR=$$HOME/.nvm; . ~/.nvm/nvm.sh ; nvm install 35 | 36 | test: ## run the tests 37 | pytest 38 | 39 | cov: ## run the tests, and open coverage report in your favorite browser 40 | pytest && python -m webbrowser -t htmlcov/index.html 41 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Before submitting this pull request, make sure these boxes are checked. Thanks! This list for internal reference. You can delete it for your PR :relieved: . 2 | 3 | - [ ] Review your code and QA it as the user. Double check if it does everything that was asked on the task. 4 | - [ ] Merge the target branch into your branch and fix conflicts. 5 | - [ ] Look for forgotten comments, prints, console.log and typos. 6 | - [ ] DRY (Don’t repeat yourself). 7 | - [ ] KISS (Keep it simple). 8 | - [ ] YAGNI (You ain’t gonna need it) - Don’t do things that you don’t need yet. 9 | - [ ] Performance (check the queries you generated on backend). 10 | 11 | Also provide the following information: 12 | 13 | **Link to task:** 14 | 15 | **How to test:** 16 | 17 | **Description of your solution:** 18 | 19 | **Screenshots:** 20 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn project.wsgi 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # django-react-boilerplate 2 | 3 | This is Labcodes' boilerplate for django apps with frontend SPAs. We're currently using React and Vite to manage the frontend. 4 | 5 | The boilerplate is PWA, HMR and code splitting ready (and a couple of other acronyms as well). 6 | 7 | It's still on beta, so expect little bugs to happen. They shouldn't, but if they pop up, add an issue, and we'll get right to it! 8 | 9 | ## Setup 10 | 11 | First, to install our dependencies, we require: 12 | - wget 13 | - make 14 | - python>=3.6 15 | - [poetry](https://python-poetry.org) 16 | - nodejs>=8.10 17 | 18 | 19 | On Ubuntu>=18.04, you also need: 20 | - libpq-dev 21 | - python3-dev 22 | 23 | The easiest way to get started with this boilerplate is to run: 24 | 25 | ```sh 26 | wget https://raw.githubusercontent.com/labcodes/django-react-boilerplate/master/create_project.sh --no-cache -P /tmp/ && bash /tmp/create_project.sh 27 | ``` 28 | 29 | It downloads the current code, unpacks it, then sets up nvm and dependencies. You may check the code and run it manually as well, it's inside `create_project.sh` :] 30 | 31 | If everything went right, you should have a server running at `localhost:8000`. Visit that on your browser to make sure everything is working <3 32 | 33 | ### Installing it manually 34 | 35 | If the script isn't working or you want to install everything manually, first [download the zip with the boilerplate from github](https://github.com/labcodes/django-react-boilerplate/archive/feature/update.zip), run `poetry shell` to activate the virtualenv, then run `make install_dependencies` to install all dependencies. 36 | 37 | With that done, the app is almost ready to be run! You just need to copy the `env.example` file to `.env`, so that the django app has access to evironment variables via python-decouple, then run `python manage.py migrate` to create a dev database, and finally `make run`, so that both vite and django run in parallel. 38 | 39 | ## Running it 40 | 41 | After installing, when you want to run the project locally, be sure to have your virtualenv active (running `poetry shell` from the project folder) and nvm set to the correct node version (by running `nvm use` on the project folder). If nvm is not found, try restarting your terminal or running `make setup_node_environment` manually. 42 | 43 | Then, to run django and vite in parallel for development, just run: 44 | 45 | ```sh 46 | make run 47 | ``` 48 | 49 | Since nvm can be a little fiddly, if you have any issues with it, [try reading their documentation](https://github.com/nvm-sh/nvm#installing-and-updating) on how to use it. 50 | 51 | ## Testing it 52 | 53 | Run the command `pytest` in the main directory, or run the script `make test`. 54 | 55 | For coverage report, run the script `make cov`. Your default browser will open with the report for the tests, if all tests pass. 56 | 57 | ## Adding new dependencies 58 | 59 | If you wish to add new dependencies, just note that: 60 | 61 | - for python dependencies, use `poetry add name_of_the_dependency` to install and add it to the requirements (`pyproject.toml`); 62 | - to update python dependencies, run `poetry update`; 63 | - for js dependencies, we're using nvm and npm, so be sure to run `nvm use` before running `npm install name_of_the_dependency` or, if it's a development-only dependency, `npm install name_of_the_dependency -D`. 64 | 65 | ## Postinstall 66 | 67 | There are a couple of things we suggest you do after installing the boilerplate. 68 | 69 | You may want to: 70 | 71 | - rename or move the project folder to your preferred folder (though you [may need to remake your virtualenv to do so](https://stackoverflow.com/questions/32407365/can-i-move-a-virtualenv#answer-58772116)); 72 | - start a new git repository; 73 | - add `nvm use` to the end of your `bin/activate` script, so that you're always using nvm whenever the virtualenv is active; 74 | - deactivate the pre-push scripts, if you want ESLint or Black to **not** be run before pushing (just remove the "husky" key inside package.json); 75 | - check `project/settings.py` for variables that need to be customized (PWA related, for example). 76 | -------------------------------------------------------------------------------- /core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/labcodes/django-react-boilerplate/f24d6d13660420521c7f3ed14c2afbee5bff8f69/core/__init__.py -------------------------------------------------------------------------------- /core/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /core/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CoreConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "core" 7 | -------------------------------------------------------------------------------- /core/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/labcodes/django-react-boilerplate/f24d6d13660420521c7f3ed14c2afbee5bff8f69/core/migrations/__init__.py -------------------------------------------------------------------------------- /core/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /core/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/labcodes/django-react-boilerplate/f24d6d13660420521c7f3ed14c2afbee5bff8f69/core/templatetags/__init__.py -------------------------------------------------------------------------------- /core/templatetags/render_vite_bundle.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | 4 | from django import template 5 | from django.conf import settings 6 | from django.utils.safestring import mark_safe 7 | 8 | register = template.Library() 9 | 10 | 11 | def load_json_from_dist(json_filename="manifest.json"): 12 | manifest_file_path = Path(str(settings.VITE_APP_DIR), "dist", json_filename) 13 | if not manifest_file_path.exists(): 14 | raise Exception( 15 | f"Vite manifest file not found on path: {str(manifest_file_path)}" 16 | ) 17 | else: 18 | with open(manifest_file_path, "r") as manifest_file: 19 | try: 20 | manifest = json.load(manifest_file) 21 | except Exception: 22 | raise Exception( 23 | f"Vite manifest file invalid. Maybe your {str(manifest_file_path)} file is empty?" 24 | ) 25 | else: 26 | return manifest 27 | 28 | 29 | @register.simple_tag 30 | def render_vite_bundle(): 31 | """ 32 | Template tag to render a vite bundle. 33 | Supposed to only be used in production. 34 | For development, see other files. 35 | """ 36 | 37 | manifest = load_json_from_dist() 38 | 39 | imports_files = "".join( 40 | [ 41 | f'' 42 | for file in manifest["index.html"]["imports"] 43 | ] 44 | ) 45 | 46 | return mark_safe( 47 | f""" 48 | 49 | {imports_files}""" 50 | ) 51 | -------------------------------------------------------------------------------- /core/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /core/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /create_project.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo "" 4 | echo -e "--------------> \033[1mThank you for choosing our boilerplate! We'll begin installation right now :]\033[0m" 5 | read -p "--------------> Project folder name: " mainmenuinput 6 | echo "" 7 | echo "--------------> Downloading, unpacking and cleaning base code..." 8 | echo "" 9 | wget https://github.com/labcodes/django-react-boilerplate/archive/master.zip -P /tmp/ 10 | unzip -qq /tmp/master.zip 11 | rm /tmp/master.zip 12 | mv django-react-boilerplate-master "$mainmenuinput" 13 | cd "$mainmenuinput" 14 | 15 | echo "" 16 | echo "--------------> Installing python and js dependencies..." 17 | echo "" 18 | make install_dependencies 19 | 20 | echo "" 21 | echo "--------------> Activating the poetry python virtualenv..." 22 | echo "" 23 | sleep "0.1" 24 | source $(poetry env info --path)/bin/activate 25 | 26 | echo "" 27 | echo "--------------> Setting up basic configuration for the django server..." 28 | echo "" 29 | cp env.example .env 30 | python manage.py migrate 31 | 32 | echo "" 33 | echo -e "--------------> \033[1mDone! Now we'll run the app to check everything is running smoothly :]\033[0m" 34 | sleep 2 35 | echo "--------------> Running the app for the first time! Hit Ctrl+C or Command+C to interrupt." 36 | echo "" 37 | make run 38 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrl": "http://localhost:8000", 3 | "video": false 4 | } 5 | -------------------------------------------------------------------------------- /cypress/fixtures/WelcomePage/sample-api-view.json: -------------------------------------------------------------------------------- 1 | { 2 | "message": "This is a stubbed message!" 3 | } 4 | -------------------------------------------------------------------------------- /cypress/integration/SampleNestedPage/index.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context("Sample Nested Page", () => { 4 | beforeEach(() => { 5 | cy.visit("/sample-nested-page/"); 6 | }); 7 | 8 | it("renders page correctly", () => { 9 | cy.contains('a[href="/"]', "Back to home."); 10 | }); 11 | 12 | it("navigates to home", () => { 13 | cy.get('a[href="/"]').click(); 14 | cy.url().should("equal", `${Cypress.config("baseUrl")}/`); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /cypress/integration/WelcomePage/index.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context("Welcome Page", () => { 4 | beforeEach(() => { 5 | cy.intercept("GET", "/api/sample-api-view/", { 6 | fixture: "WelcomePage/sample-api-view.json", 7 | delay: 100 8 | }).as("getMessage"); 9 | 10 | cy.visit("/"); 11 | }); 12 | 13 | it("renders page correctly", () => { 14 | cy.contains( 15 | ".welcome__message", 16 | "Welcome Message is being fetched from the API..." 17 | ); 18 | cy.wait("@getMessage"); 19 | cy.contains(".welcome__message", "This is a stubbed message!"); 20 | 21 | cy.contains('a[href="/sample-nested-page/"]', " sample nested page"); 22 | }); 23 | 24 | it("navigates to sample nested page", () => { 25 | cy.get('a[href="/sample-nested-page/"]').click(); 26 | cy.url().should("include", "/sample-nested-page/"); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************************** 3 | // This example plugins/main.jsx can be used to load plugins 4 | // 5 | // You can change the location of this file or turn off loading 6 | // the plugins file with the 'pluginsFile' configuration option. 7 | // 8 | // You can read more here: 9 | // https://on.cypress.io/plugins-guide 10 | // *********************************************************** 11 | 12 | // This function is called when a project is opened or re-opened (e.g. due to 13 | // the project's config changing) 14 | 15 | /** 16 | * @type {Cypress.PluginConfig} 17 | */ 18 | // eslint-disable-next-line no-unused-vars 19 | module.exports = (on, config) => { 20 | // `on` is used to hook into various events Cypress emits 21 | // `config` is the resolved Cypress config 22 | }; 23 | -------------------------------------------------------------------------------- /cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add('login', (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This will overwrite an existing command -- 25 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) 26 | -------------------------------------------------------------------------------- /cypress/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/main.jsx is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import "./commands"; 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /env.example: -------------------------------------------------------------------------------- 1 | DEBUG=True 2 | SECRET_KEY="CHANGE ME" 3 | -------------------------------------------------------------------------------- /hooks/pre-push.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | run_eslint () { 4 | echo "--------------> Running ESLint" 5 | if npm run lint 6 | then 7 | return 8 | else 9 | echo "" 10 | echo "--------------> ESLint detected some errors. Please fix them then try again." 11 | exit 1 12 | fi 13 | } 14 | 15 | run_black () { 16 | echo "--------------> Running Black" 17 | echo "" 18 | 19 | if git diff --name-status | grep ".py" 20 | then 21 | echo "" 22 | read -p "We detected these unstaged python files. Continue with them unstaged? (y/N) " git_diff_confirm < /dev/tty 23 | case $git_diff_confirm in 24 | [Yy]* ) ;; 25 | [Nn]* ) 26 | echo "" 27 | echo "Please, commit your changes manually then try again." 28 | exit 1 29 | ;; 30 | * ) 31 | echo "" 32 | echo "Please, commit your changes manually then try again." 33 | exit 1 34 | ;; 35 | esac 36 | fi 37 | 38 | if black --check . -q 39 | then 40 | if git diff --name-status | grep ".py" > /dev/null 41 | then 42 | echo "" 43 | create_commit 44 | fi 45 | return 46 | else 47 | 48 | read -p "Black has detected files to be formatted. Format them? (Y/n) " black_confirm < /dev/tty 49 | case $black_confirm in 50 | [Yy]* ) black . -q;; 51 | [Nn]* ) echo "Exiting..."; exit 1;; 52 | * ) black . -q;; 53 | esac 54 | 55 | create_commit 56 | 57 | return 58 | fi 59 | } 60 | 61 | create_commit () { 62 | read -p "Create commit with all changes? ANY UNSTAGED FILES WILL ALSO BE COMMITED (y/N) " commit_confirm < /dev/tty 63 | case $commit_confirm in 64 | [Yy]* ) 65 | git add . 66 | git commit -m 'Black' 67 | git push --no-verify 68 | echo "" 69 | echo "Because of implementation details on git hooks, it will seem like the push failed. It didn't. Ignore any messages related to it and check manually, if it's the case." 70 | echo "" 71 | exit 1;; 72 | [Nn]* ) 73 | echo "" 74 | echo "Please, commit your changes manually then try again." 75 | exit 1 76 | ;; 77 | * ) 78 | echo "" 79 | echo "Please, commit your changes manually then try again." 80 | exit 1 81 | ;; 82 | esac 83 | } 84 | 85 | if run_eslint && run_black 86 | then 87 | echo "--------------> Checks done, continuing..." 88 | else 89 | exit 1 90 | fi 91 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError as exc: 10 | raise ImportError( 11 | "Couldn't import Django. Are you sure it's installed and " 12 | "available on your PYTHONPATH environment variable? Did you " 13 | "forget to activate a virtual environment?" 14 | ) from exc 15 | execute_from_command_line(sys.argv) 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "django-react-boilerplate", 3 | "version": "1.0.0", 4 | "description": "Boilerplate for a Django + React project", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest", 8 | "build": "vite build", 9 | "lint": "eslint ./react-app/js/ --ext .js,.jsx", 10 | "dev": "vite" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/labcodes/django-react-boilerplate.git" 15 | }, 16 | "keywords": [ 17 | "django", 18 | "react", 19 | "vite" 20 | ], 21 | "husky": { 22 | "hooks": { 23 | "pre-push": "./hooks/pre-push.sh" 24 | } 25 | }, 26 | "jest": { 27 | "testEnvironment": "jsdom", 28 | "setupFiles": [ 29 | "jest-prop-type-error" 30 | ], 31 | "testPathIgnorePatterns": [ 32 | "./cypress" 33 | ] 34 | }, 35 | "author": "", 36 | "license": "ISC", 37 | "bugs": { 38 | "url": "https://github.com/labcodes/django-react-boilerplate/issues" 39 | }, 40 | "homepage": "https://github.com/labcodes/django-react-boilerplate#readme", 41 | "devDependencies": { 42 | "@babel/core": "^7.8.7", 43 | "@babel/eslint-parser": "^7.15.4", 44 | "@babel/plugin-proposal-class-properties": "^7.8.3", 45 | "@babel/plugin-transform-react-constant-elements": "^7.8.3", 46 | "@babel/plugin-transform-react-inline-elements": "^7.8.3", 47 | "@babel/plugin-transform-runtime": "^7.8.3", 48 | "@babel/preset-env": "^7.8.7", 49 | "@babel/preset-react": "^7.8.3", 50 | "@testing-library/react": "^12.1.0", 51 | "@types/jest": "^25.1.4", 52 | "@vitejs/plugin-react-refresh": "^1.3.6", 53 | "autoprefixer": "^9.7.4", 54 | "babel-jest": "^27.3.1", 55 | "cypress": "^8.0.0", 56 | "eslint": "^7.32.0", 57 | "eslint-config-airbnb": "^18.2.1", 58 | "eslint-config-prettier": "^8.3.0", 59 | "eslint-plugin-cypress": "^2.12.1", 60 | "eslint-plugin-import": "^2.25.2", 61 | "eslint-plugin-jsx-a11y": "^6.4.1", 62 | "eslint-plugin-prettier": "^4.0.0", 63 | "eslint-plugin-react": "^7.26.1", 64 | "husky": "^4.2.3", 65 | "jest": "^27.3.1", 66 | "jest-prop-type-error": "^1.1.0", 67 | "prettier": "^2.4.1", 68 | "sass": "^1.39.2", 69 | "vite": "^2.5.6", 70 | "vite-plugin-compression": "^0.3.5", 71 | "vite-plugin-pwa": "^0.11.2" 72 | }, 73 | "dependencies": { 74 | "lodash": "^4.17.15", 75 | "prop-types": "^15.7.2", 76 | "react": "^16.13.0", 77 | "react-dom": "^16.13.0", 78 | "react-helmet": "^6.1.0", 79 | "react-redux": "^7.2.0", 80 | "react-redux-api-tools": "^2.1.1", 81 | "react-router-dom": "^5.1.2", 82 | "redux": "^4.0.5", 83 | "redux-thunk": "^2.3.0", 84 | "whatwg-fetch": "^3.0.0" 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "asgiref" 3 | version = "3.4.1" 4 | description = "ASGI specs, helper code, and adapters" 5 | category = "main" 6 | optional = false 7 | python-versions = ">=3.6" 8 | 9 | [package.dependencies] 10 | typing-extensions = {version = "*", markers = "python_version < \"3.8\""} 11 | 12 | [package.extras] 13 | tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"] 14 | 15 | [[package]] 16 | name = "atomicwrites" 17 | version = "1.4.0" 18 | description = "Atomic file writes." 19 | category = "main" 20 | optional = false 21 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 22 | 23 | [[package]] 24 | name = "attrs" 25 | version = "21.2.0" 26 | description = "Classes Without Boilerplate" 27 | category = "main" 28 | optional = false 29 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 30 | 31 | [package.extras] 32 | dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"] 33 | docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] 34 | tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] 35 | tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] 36 | 37 | [[package]] 38 | name = "black" 39 | version = "21.9b0" 40 | description = "The uncompromising code formatter." 41 | category = "main" 42 | optional = false 43 | python-versions = ">=3.6.2" 44 | 45 | [package.dependencies] 46 | click = ">=7.1.2" 47 | dataclasses = {version = ">=0.6", markers = "python_version < \"3.7\""} 48 | mypy-extensions = ">=0.4.3" 49 | pathspec = ">=0.9.0,<1" 50 | platformdirs = ">=2" 51 | regex = ">=2020.1.8" 52 | tomli = ">=0.2.6,<2.0.0" 53 | typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\""} 54 | typing-extensions = [ 55 | {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}, 56 | {version = "!=3.10.0.1", markers = "python_version >= \"3.10\""}, 57 | ] 58 | 59 | [package.extras] 60 | colorama = ["colorama (>=0.4.3)"] 61 | d = ["aiohttp (>=3.6.0)", "aiohttp-cors (>=0.4.0)"] 62 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 63 | python2 = ["typed-ast (>=1.4.2)"] 64 | uvloop = ["uvloop (>=0.15.2)"] 65 | 66 | [[package]] 67 | name = "click" 68 | version = "8.0.1" 69 | description = "Composable command line interface toolkit" 70 | category = "main" 71 | optional = false 72 | python-versions = ">=3.6" 73 | 74 | [package.dependencies] 75 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 76 | importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} 77 | 78 | [[package]] 79 | name = "colorama" 80 | version = "0.4.4" 81 | description = "Cross-platform colored terminal text." 82 | category = "main" 83 | optional = false 84 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 85 | 86 | [[package]] 87 | name = "coverage" 88 | version = "5.5" 89 | description = "Code coverage measurement for Python" 90 | category = "main" 91 | optional = false 92 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 93 | 94 | [package.extras] 95 | toml = ["toml"] 96 | 97 | [[package]] 98 | name = "dataclasses" 99 | version = "0.8" 100 | description = "A backport of the dataclasses module for Python 3.6" 101 | category = "main" 102 | optional = false 103 | python-versions = ">=3.6, <3.7" 104 | 105 | [[package]] 106 | name = "dj-database-url" 107 | version = "0.5.0" 108 | description = "Use Database URLs in your Django Application." 109 | category = "main" 110 | optional = false 111 | python-versions = "*" 112 | 113 | [[package]] 114 | name = "django" 115 | version = "3.2.7" 116 | description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." 117 | category = "main" 118 | optional = false 119 | python-versions = ">=3.6" 120 | 121 | [package.dependencies] 122 | asgiref = ">=3.3.2,<4" 123 | pytz = "*" 124 | sqlparse = ">=0.2.2" 125 | 126 | [package.extras] 127 | argon2 = ["argon2-cffi (>=19.1.0)"] 128 | bcrypt = ["bcrypt"] 129 | 130 | [[package]] 131 | name = "django-heroku" 132 | version = "0.3.1" 133 | description = "This is a Django library for Heroku apps." 134 | category = "main" 135 | optional = false 136 | python-versions = "*" 137 | 138 | [package.dependencies] 139 | dj-database-url = ">=0.5.0" 140 | django = "*" 141 | psycopg2 = "*" 142 | whitenoise = "*" 143 | 144 | [[package]] 145 | name = "django-pwa" 146 | version = "1.0.10" 147 | description = "A Django app to include a manifest.json and Service Worker instance to enable progressive web app behavior" 148 | category = "main" 149 | optional = false 150 | python-versions = "*" 151 | 152 | [package.dependencies] 153 | django = ">=1.8" 154 | 155 | [[package]] 156 | name = "djangorestframework" 157 | version = "3.12.4" 158 | description = "Web APIs for Django, made easy." 159 | category = "main" 160 | optional = false 161 | python-versions = ">=3.5" 162 | 163 | [package.dependencies] 164 | django = ">=2.2" 165 | 166 | [[package]] 167 | name = "importlib-metadata" 168 | version = "4.8.1" 169 | description = "Read metadata from Python packages" 170 | category = "main" 171 | optional = false 172 | python-versions = ">=3.6" 173 | 174 | [package.dependencies] 175 | typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} 176 | zipp = ">=0.5" 177 | 178 | [package.extras] 179 | docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] 180 | perf = ["ipython"] 181 | testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] 182 | 183 | [[package]] 184 | name = "iniconfig" 185 | version = "1.1.1" 186 | description = "iniconfig: brain-dead simple config-ini parsing" 187 | category = "main" 188 | optional = false 189 | python-versions = "*" 190 | 191 | [[package]] 192 | name = "mypy-extensions" 193 | version = "0.4.3" 194 | description = "Experimental type system extensions for programs checked with the mypy typechecker." 195 | category = "main" 196 | optional = false 197 | python-versions = "*" 198 | 199 | [[package]] 200 | name = "packaging" 201 | version = "21.0" 202 | description = "Core utilities for Python packages" 203 | category = "main" 204 | optional = false 205 | python-versions = ">=3.6" 206 | 207 | [package.dependencies] 208 | pyparsing = ">=2.0.2" 209 | 210 | [[package]] 211 | name = "pathspec" 212 | version = "0.9.0" 213 | description = "Utility library for gitignore style pattern matching of file paths." 214 | category = "main" 215 | optional = false 216 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 217 | 218 | [[package]] 219 | name = "platformdirs" 220 | version = "2.3.0" 221 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 222 | category = "main" 223 | optional = false 224 | python-versions = ">=3.6" 225 | 226 | [package.extras] 227 | docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] 228 | test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] 229 | 230 | [[package]] 231 | name = "pluggy" 232 | version = "1.0.0" 233 | description = "plugin and hook calling mechanisms for python" 234 | category = "main" 235 | optional = false 236 | python-versions = ">=3.6" 237 | 238 | [package.dependencies] 239 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 240 | 241 | [package.extras] 242 | dev = ["pre-commit", "tox"] 243 | testing = ["pytest", "pytest-benchmark"] 244 | 245 | [[package]] 246 | name = "psycopg2" 247 | version = "2.9.1" 248 | description = "psycopg2 - Python-PostgreSQL Database Adapter" 249 | category = "main" 250 | optional = false 251 | python-versions = ">=3.6" 252 | 253 | [[package]] 254 | name = "py" 255 | version = "1.10.0" 256 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 257 | category = "main" 258 | optional = false 259 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 260 | 261 | [[package]] 262 | name = "pyparsing" 263 | version = "2.4.7" 264 | description = "Python parsing module" 265 | category = "main" 266 | optional = false 267 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 268 | 269 | [[package]] 270 | name = "pytest" 271 | version = "6.2.5" 272 | description = "pytest: simple powerful testing with Python" 273 | category = "main" 274 | optional = false 275 | python-versions = ">=3.6" 276 | 277 | [package.dependencies] 278 | atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} 279 | attrs = ">=19.2.0" 280 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 281 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 282 | iniconfig = "*" 283 | packaging = "*" 284 | pluggy = ">=0.12,<2.0" 285 | py = ">=1.8.2" 286 | toml = "*" 287 | 288 | [package.extras] 289 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] 290 | 291 | [[package]] 292 | name = "pytest-cov" 293 | version = "2.12.1" 294 | description = "Pytest plugin for measuring coverage." 295 | category = "main" 296 | optional = false 297 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 298 | 299 | [package.dependencies] 300 | coverage = ">=5.2.1" 301 | pytest = ">=4.6" 302 | toml = "*" 303 | 304 | [package.extras] 305 | testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] 306 | 307 | [[package]] 308 | name = "python-decouple" 309 | version = "3.4" 310 | description = "Strict separation of settings from code." 311 | category = "main" 312 | optional = false 313 | python-versions = "*" 314 | 315 | [[package]] 316 | name = "pytz" 317 | version = "2021.1" 318 | description = "World timezone definitions, modern and historical" 319 | category = "main" 320 | optional = false 321 | python-versions = "*" 322 | 323 | [[package]] 324 | name = "regex" 325 | version = "2021.8.28" 326 | description = "Alternative regular expression module, to replace re." 327 | category = "main" 328 | optional = false 329 | python-versions = "*" 330 | 331 | [[package]] 332 | name = "sqlparse" 333 | version = "0.4.2" 334 | description = "A non-validating SQL parser." 335 | category = "main" 336 | optional = false 337 | python-versions = ">=3.5" 338 | 339 | [[package]] 340 | name = "toml" 341 | version = "0.10.2" 342 | description = "Python Library for Tom's Obvious, Minimal Language" 343 | category = "main" 344 | optional = false 345 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 346 | 347 | [[package]] 348 | name = "tomli" 349 | version = "1.2.1" 350 | description = "A lil' TOML parser" 351 | category = "main" 352 | optional = false 353 | python-versions = ">=3.6" 354 | 355 | [[package]] 356 | name = "typed-ast" 357 | version = "1.4.3" 358 | description = "a fork of Python 2 and 3 ast modules with type comment support" 359 | category = "main" 360 | optional = false 361 | python-versions = "*" 362 | 363 | [[package]] 364 | name = "typing-extensions" 365 | version = "3.10.0.2" 366 | description = "Backported and Experimental Type Hints for Python 3.5+" 367 | category = "main" 368 | optional = false 369 | python-versions = "*" 370 | 371 | [[package]] 372 | name = "whitenoise" 373 | version = "5.3.0" 374 | description = "Radically simplified static file serving for WSGI applications" 375 | category = "main" 376 | optional = false 377 | python-versions = ">=3.5, <4" 378 | 379 | [package.extras] 380 | brotli = ["brotli"] 381 | 382 | [[package]] 383 | name = "zipp" 384 | version = "3.5.0" 385 | description = "Backport of pathlib-compatible object wrapper for zip files" 386 | category = "main" 387 | optional = false 388 | python-versions = ">=3.6" 389 | 390 | [package.extras] 391 | docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] 392 | testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] 393 | 394 | [metadata] 395 | lock-version = "1.1" 396 | python-versions = "^3.6.14" 397 | content-hash = "21aaa5480933ede250df09051dd99b44fa00e6ac4459e76e8e4709d2a7cf4e87" 398 | 399 | [metadata.files] 400 | asgiref = [ 401 | {file = "asgiref-3.4.1-py3-none-any.whl", hash = "sha256:ffc141aa908e6f175673e7b1b3b7af4fdb0ecb738fc5c8b88f69f055c2415214"}, 402 | {file = "asgiref-3.4.1.tar.gz", hash = "sha256:4ef1ab46b484e3c706329cedeff284a5d40824200638503f5768edb6de7d58e9"}, 403 | ] 404 | atomicwrites = [ 405 | {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, 406 | {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, 407 | ] 408 | attrs = [ 409 | {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, 410 | {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, 411 | ] 412 | black = [ 413 | {file = "black-21.9b0-py3-none-any.whl", hash = "sha256:380f1b5da05e5a1429225676655dddb96f5ae8c75bdf91e53d798871b902a115"}, 414 | {file = "black-21.9b0.tar.gz", hash = "sha256:7de4cfc7eb6b710de325712d40125689101d21d25283eed7e9998722cf10eb91"}, 415 | ] 416 | click = [ 417 | {file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"}, 418 | {file = "click-8.0.1.tar.gz", hash = "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a"}, 419 | ] 420 | colorama = [ 421 | {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, 422 | {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, 423 | ] 424 | coverage = [ 425 | {file = "coverage-5.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf"}, 426 | {file = "coverage-5.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b"}, 427 | {file = "coverage-5.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669"}, 428 | {file = "coverage-5.5-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90"}, 429 | {file = "coverage-5.5-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c"}, 430 | {file = "coverage-5.5-cp27-cp27m-win32.whl", hash = "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a"}, 431 | {file = "coverage-5.5-cp27-cp27m-win_amd64.whl", hash = "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82"}, 432 | {file = "coverage-5.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905"}, 433 | {file = "coverage-5.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083"}, 434 | {file = "coverage-5.5-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5"}, 435 | {file = "coverage-5.5-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81"}, 436 | {file = "coverage-5.5-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6"}, 437 | {file = "coverage-5.5-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0"}, 438 | {file = "coverage-5.5-cp310-cp310-win_amd64.whl", hash = "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae"}, 439 | {file = "coverage-5.5-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb"}, 440 | {file = "coverage-5.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160"}, 441 | {file = "coverage-5.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6"}, 442 | {file = "coverage-5.5-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701"}, 443 | {file = "coverage-5.5-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793"}, 444 | {file = "coverage-5.5-cp35-cp35m-win32.whl", hash = "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e"}, 445 | {file = "coverage-5.5-cp35-cp35m-win_amd64.whl", hash = "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3"}, 446 | {file = "coverage-5.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066"}, 447 | {file = "coverage-5.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a"}, 448 | {file = "coverage-5.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465"}, 449 | {file = "coverage-5.5-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb"}, 450 | {file = "coverage-5.5-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821"}, 451 | {file = "coverage-5.5-cp36-cp36m-win32.whl", hash = "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45"}, 452 | {file = "coverage-5.5-cp36-cp36m-win_amd64.whl", hash = "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184"}, 453 | {file = "coverage-5.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a"}, 454 | {file = "coverage-5.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53"}, 455 | {file = "coverage-5.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d"}, 456 | {file = "coverage-5.5-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638"}, 457 | {file = "coverage-5.5-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3"}, 458 | {file = "coverage-5.5-cp37-cp37m-win32.whl", hash = "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a"}, 459 | {file = "coverage-5.5-cp37-cp37m-win_amd64.whl", hash = "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a"}, 460 | {file = "coverage-5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6"}, 461 | {file = "coverage-5.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2"}, 462 | {file = "coverage-5.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759"}, 463 | {file = "coverage-5.5-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873"}, 464 | {file = "coverage-5.5-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a"}, 465 | {file = "coverage-5.5-cp38-cp38-win32.whl", hash = "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6"}, 466 | {file = "coverage-5.5-cp38-cp38-win_amd64.whl", hash = "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502"}, 467 | {file = "coverage-5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b"}, 468 | {file = "coverage-5.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529"}, 469 | {file = "coverage-5.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b"}, 470 | {file = "coverage-5.5-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff"}, 471 | {file = "coverage-5.5-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b"}, 472 | {file = "coverage-5.5-cp39-cp39-win32.whl", hash = "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6"}, 473 | {file = "coverage-5.5-cp39-cp39-win_amd64.whl", hash = "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03"}, 474 | {file = "coverage-5.5-pp36-none-any.whl", hash = "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079"}, 475 | {file = "coverage-5.5-pp37-none-any.whl", hash = "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4"}, 476 | {file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"}, 477 | ] 478 | dataclasses = [ 479 | {file = "dataclasses-0.8-py3-none-any.whl", hash = "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf"}, 480 | {file = "dataclasses-0.8.tar.gz", hash = "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97"}, 481 | ] 482 | dj-database-url = [ 483 | {file = "dj-database-url-0.5.0.tar.gz", hash = "sha256:4aeaeb1f573c74835b0686a2b46b85990571159ffc21aa57ecd4d1e1cb334163"}, 484 | {file = "dj_database_url-0.5.0-py2.py3-none-any.whl", hash = "sha256:851785365761ebe4994a921b433062309eb882fedd318e1b0fcecc607ed02da9"}, 485 | ] 486 | django = [ 487 | {file = "Django-3.2.7-py3-none-any.whl", hash = "sha256:e93c93565005b37ddebf2396b4dc4b6913c1838baa82efdfb79acedd5816c240"}, 488 | {file = "Django-3.2.7.tar.gz", hash = "sha256:95b318319d6997bac3595517101ad9cc83fe5672ac498ba48d1a410f47afecd2"}, 489 | ] 490 | django-heroku = [ 491 | {file = "django-heroku-0.3.1.tar.gz", hash = "sha256:6af4bc3ae4a9b55eaad6dbe5164918982d2762661aebc9f83d9fa49f6009514e"}, 492 | {file = "django_heroku-0.3.1-py2.py3-none-any.whl", hash = "sha256:2bc690aab89eedbe01311752320a9a12e7548e3b0ed102681acc5736a41a4762"}, 493 | ] 494 | django-pwa = [ 495 | {file = "django-pwa-1.0.10.tar.gz", hash = "sha256:07ed9dd57108838e3fe44b551a82032ca4ed76e31cb3c3e8d51604e0fe7e81e9"}, 496 | {file = "django_pwa-1.0.10-py3-none-any.whl", hash = "sha256:b1a2057b1e72c40c3a14beb90b958482da185f1d40a141fcae3d76580984b930"}, 497 | ] 498 | djangorestframework = [ 499 | {file = "djangorestframework-3.12.4-py3-none-any.whl", hash = "sha256:6d1d59f623a5ad0509fe0d6bfe93cbdfe17b8116ebc8eda86d45f6e16e819aaf"}, 500 | {file = "djangorestframework-3.12.4.tar.gz", hash = "sha256:f747949a8ddac876e879190df194b925c177cdeb725a099db1460872f7c0a7f2"}, 501 | ] 502 | importlib-metadata = [ 503 | {file = "importlib_metadata-4.8.1-py3-none-any.whl", hash = "sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15"}, 504 | {file = "importlib_metadata-4.8.1.tar.gz", hash = "sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1"}, 505 | ] 506 | iniconfig = [ 507 | {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, 508 | {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, 509 | ] 510 | mypy-extensions = [ 511 | {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, 512 | {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, 513 | ] 514 | packaging = [ 515 | {file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"}, 516 | {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"}, 517 | ] 518 | pathspec = [ 519 | {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, 520 | {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, 521 | ] 522 | platformdirs = [ 523 | {file = "platformdirs-2.3.0-py3-none-any.whl", hash = "sha256:8003ac87717ae2c7ee1ea5a84a1a61e87f3fbd16eb5aadba194ea30a9019f648"}, 524 | {file = "platformdirs-2.3.0.tar.gz", hash = "sha256:15b056538719b1c94bdaccb29e5f81879c7f7f0f4a153f46086d155dffcd4f0f"}, 525 | ] 526 | pluggy = [ 527 | {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, 528 | {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, 529 | ] 530 | psycopg2 = [ 531 | {file = "psycopg2-2.9.1-cp36-cp36m-win32.whl", hash = "sha256:7f91312f065df517187134cce8e395ab37f5b601a42446bdc0f0d51773621854"}, 532 | {file = "psycopg2-2.9.1-cp36-cp36m-win_amd64.whl", hash = "sha256:830c8e8dddab6b6716a4bf73a09910c7954a92f40cf1d1e702fb93c8a919cc56"}, 533 | {file = "psycopg2-2.9.1-cp37-cp37m-win32.whl", hash = "sha256:89409d369f4882c47f7ea20c42c5046879ce22c1e4ea20ef3b00a4dfc0a7f188"}, 534 | {file = "psycopg2-2.9.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7640e1e4d72444ef012e275e7b53204d7fab341fb22bc76057ede22fe6860b25"}, 535 | {file = "psycopg2-2.9.1-cp38-cp38-win32.whl", hash = "sha256:079d97fc22de90da1d370c90583659a9f9a6ee4007355f5825e5f1c70dffc1fa"}, 536 | {file = "psycopg2-2.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:2c992196719fadda59f72d44603ee1a2fdcc67de097eea38d41c7ad9ad246e62"}, 537 | {file = "psycopg2-2.9.1-cp39-cp39-win32.whl", hash = "sha256:2087013c159a73e09713294a44d0c8008204d06326006b7f652bef5ace66eebb"}, 538 | {file = "psycopg2-2.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:bf35a25f1aaa8a3781195595577fcbb59934856ee46b4f252f56ad12b8043bcf"}, 539 | {file = "psycopg2-2.9.1.tar.gz", hash = "sha256:de5303a6f1d0a7a34b9d40e4d3bef684ccc44a49bbe3eb85e3c0bffb4a131b7c"}, 540 | ] 541 | py = [ 542 | {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, 543 | {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, 544 | ] 545 | pyparsing = [ 546 | {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, 547 | {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, 548 | ] 549 | pytest = [ 550 | {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, 551 | {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, 552 | ] 553 | pytest-cov = [ 554 | {file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"}, 555 | {file = "pytest_cov-2.12.1-py2.py3-none-any.whl", hash = "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a"}, 556 | ] 557 | python-decouple = [ 558 | {file = "python-decouple-3.4.tar.gz", hash = "sha256:2e5adb0263a4f963b58d7407c4760a2465d464ee212d733e2a2c179e54c08d8f"}, 559 | {file = "python_decouple-3.4-py3-none-any.whl", hash = "sha256:a8268466e6389a639a20deab9d880faee186eb1eb6a05e54375bdf158d691981"}, 560 | ] 561 | pytz = [ 562 | {file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"}, 563 | {file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"}, 564 | ] 565 | regex = [ 566 | {file = "regex-2021.8.28-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9d05ad5367c90814099000442b2125535e9d77581855b9bee8780f1b41f2b1a2"}, 567 | {file = "regex-2021.8.28-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3bf1bc02bc421047bfec3343729c4bbbea42605bcfd6d6bfe2c07ade8b12d2a"}, 568 | {file = "regex-2021.8.28-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f6a808044faae658f546dd5f525e921de9fa409de7a5570865467f03a626fc0"}, 569 | {file = "regex-2021.8.28-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a617593aeacc7a691cc4af4a4410031654f2909053bd8c8e7db837f179a630eb"}, 570 | {file = "regex-2021.8.28-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:79aef6b5cd41feff359acaf98e040844613ff5298d0d19c455b3d9ae0bc8c35a"}, 571 | {file = "regex-2021.8.28-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0fc1f8f06977c2d4f5e3d3f0d4a08089be783973fc6b6e278bde01f0544ff308"}, 572 | {file = "regex-2021.8.28-cp310-cp310-win32.whl", hash = "sha256:6eebf512aa90751d5ef6a7c2ac9d60113f32e86e5687326a50d7686e309f66ed"}, 573 | {file = "regex-2021.8.28-cp310-cp310-win_amd64.whl", hash = "sha256:ac88856a8cbccfc14f1b2d0b829af354cc1743cb375e7f04251ae73b2af6adf8"}, 574 | {file = "regex-2021.8.28-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c206587c83e795d417ed3adc8453a791f6d36b67c81416676cad053b4104152c"}, 575 | {file = "regex-2021.8.28-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8690ed94481f219a7a967c118abaf71ccc440f69acd583cab721b90eeedb77c"}, 576 | {file = "regex-2021.8.28-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:328a1fad67445550b982caa2a2a850da5989fd6595e858f02d04636e7f8b0b13"}, 577 | {file = "regex-2021.8.28-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c7cb4c512d2d3b0870e00fbbac2f291d4b4bf2634d59a31176a87afe2777c6f0"}, 578 | {file = "regex-2021.8.28-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66256b6391c057305e5ae9209941ef63c33a476b73772ca967d4a2df70520ec1"}, 579 | {file = "regex-2021.8.28-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8e44769068d33e0ea6ccdf4b84d80c5afffe5207aa4d1881a629cf0ef3ec398f"}, 580 | {file = "regex-2021.8.28-cp36-cp36m-win32.whl", hash = "sha256:08d74bfaa4c7731b8dac0a992c63673a2782758f7cfad34cf9c1b9184f911354"}, 581 | {file = "regex-2021.8.28-cp36-cp36m-win_amd64.whl", hash = "sha256:abb48494d88e8a82601af905143e0de838c776c1241d92021e9256d5515b3645"}, 582 | {file = "regex-2021.8.28-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b4c220a1fe0d2c622493b0a1fd48f8f991998fb447d3cd368033a4b86cf1127a"}, 583 | {file = "regex-2021.8.28-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4a332404baa6665b54e5d283b4262f41f2103c255897084ec8f5487ce7b9e8e"}, 584 | {file = "regex-2021.8.28-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c61dcc1cf9fd165127a2853e2c31eb4fb961a4f26b394ac9fe5669c7a6592892"}, 585 | {file = "regex-2021.8.28-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ee329d0387b5b41a5dddbb6243a21cb7896587a651bebb957e2d2bb8b63c0791"}, 586 | {file = "regex-2021.8.28-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f60667673ff9c249709160529ab39667d1ae9fd38634e006bec95611f632e759"}, 587 | {file = "regex-2021.8.28-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b844fb09bd9936ed158ff9df0ab601e2045b316b17aa8b931857365ea8586906"}, 588 | {file = "regex-2021.8.28-cp37-cp37m-win32.whl", hash = "sha256:4cde065ab33bcaab774d84096fae266d9301d1a2f5519d7bd58fc55274afbf7a"}, 589 | {file = "regex-2021.8.28-cp37-cp37m-win_amd64.whl", hash = "sha256:1413b5022ed6ac0d504ba425ef02549a57d0f4276de58e3ab7e82437892704fc"}, 590 | {file = "regex-2021.8.28-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ed4b50355b066796dacdd1cf538f2ce57275d001838f9b132fab80b75e8c84dd"}, 591 | {file = "regex-2021.8.28-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28fc475f560d8f67cc8767b94db4c9440210f6958495aeae70fac8faec631797"}, 592 | {file = "regex-2021.8.28-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdc178caebd0f338d57ae445ef8e9b737ddf8fbc3ea187603f65aec5b041248f"}, 593 | {file = "regex-2021.8.28-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:999ad08220467b6ad4bd3dd34e65329dd5d0df9b31e47106105e407954965256"}, 594 | {file = "regex-2021.8.28-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:808ee5834e06f57978da3e003ad9d6292de69d2bf6263662a1a8ae30788e080b"}, 595 | {file = "regex-2021.8.28-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d5111d4c843d80202e62b4fdbb4920db1dcee4f9366d6b03294f45ed7b18b42e"}, 596 | {file = "regex-2021.8.28-cp38-cp38-win32.whl", hash = "sha256:473858730ef6d6ff7f7d5f19452184cd0caa062a20047f6d6f3e135a4648865d"}, 597 | {file = "regex-2021.8.28-cp38-cp38-win_amd64.whl", hash = "sha256:31a99a4796bf5aefc8351e98507b09e1b09115574f7c9dbb9cf2111f7220d2e2"}, 598 | {file = "regex-2021.8.28-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:04f6b9749e335bb0d2f68c707f23bb1773c3fb6ecd10edf0f04df12a8920d468"}, 599 | {file = "regex-2021.8.28-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b006628fe43aa69259ec04ca258d88ed19b64791693df59c422b607b6ece8bb"}, 600 | {file = "regex-2021.8.28-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:121f4b3185feaade3f85f70294aef3f777199e9b5c0c0245c774ae884b110a2d"}, 601 | {file = "regex-2021.8.28-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a577a21de2ef8059b58f79ff76a4da81c45a75fe0bfb09bc8b7bb4293fa18983"}, 602 | {file = "regex-2021.8.28-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1743345e30917e8c574f273f51679c294effba6ad372db1967852f12c76759d8"}, 603 | {file = "regex-2021.8.28-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e1e8406b895aba6caa63d9fd1b6b1700d7e4825f78ccb1e5260551d168db38ed"}, 604 | {file = "regex-2021.8.28-cp39-cp39-win32.whl", hash = "sha256:ed283ab3a01d8b53de3a05bfdf4473ae24e43caee7dcb5584e86f3f3e5ab4374"}, 605 | {file = "regex-2021.8.28-cp39-cp39-win_amd64.whl", hash = "sha256:610b690b406653c84b7cb6091facb3033500ee81089867ee7d59e675f9ca2b73"}, 606 | {file = "regex-2021.8.28.tar.gz", hash = "sha256:f585cbbeecb35f35609edccb95efd95a3e35824cd7752b586503f7e6087303f1"}, 607 | ] 608 | sqlparse = [ 609 | {file = "sqlparse-0.4.2-py3-none-any.whl", hash = "sha256:48719e356bb8b42991bdbb1e8b83223757b93789c00910a616a071910ca4a64d"}, 610 | {file = "sqlparse-0.4.2.tar.gz", hash = "sha256:0c00730c74263a94e5a9919ade150dfc3b19c574389985446148402998287dae"}, 611 | ] 612 | toml = [ 613 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, 614 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, 615 | ] 616 | tomli = [ 617 | {file = "tomli-1.2.1-py3-none-any.whl", hash = "sha256:8dd0e9524d6f386271a36b41dbf6c57d8e32fd96fd22b6584679dc569d20899f"}, 618 | {file = "tomli-1.2.1.tar.gz", hash = "sha256:a5b75cb6f3968abb47af1b40c1819dc519ea82bcc065776a866e8d74c5ca9442"}, 619 | ] 620 | typed-ast = [ 621 | {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6"}, 622 | {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075"}, 623 | {file = "typed_ast-1.4.3-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528"}, 624 | {file = "typed_ast-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428"}, 625 | {file = "typed_ast-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3"}, 626 | {file = "typed_ast-1.4.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f"}, 627 | {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341"}, 628 | {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace"}, 629 | {file = "typed_ast-1.4.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f"}, 630 | {file = "typed_ast-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363"}, 631 | {file = "typed_ast-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7"}, 632 | {file = "typed_ast-1.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266"}, 633 | {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e"}, 634 | {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04"}, 635 | {file = "typed_ast-1.4.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899"}, 636 | {file = "typed_ast-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c"}, 637 | {file = "typed_ast-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805"}, 638 | {file = "typed_ast-1.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a"}, 639 | {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff"}, 640 | {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41"}, 641 | {file = "typed_ast-1.4.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39"}, 642 | {file = "typed_ast-1.4.3-cp38-cp38-win32.whl", hash = "sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927"}, 643 | {file = "typed_ast-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40"}, 644 | {file = "typed_ast-1.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3"}, 645 | {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4"}, 646 | {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0"}, 647 | {file = "typed_ast-1.4.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3"}, 648 | {file = "typed_ast-1.4.3-cp39-cp39-win32.whl", hash = "sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808"}, 649 | {file = "typed_ast-1.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c"}, 650 | {file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"}, 651 | ] 652 | typing-extensions = [ 653 | {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"}, 654 | {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"}, 655 | {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"}, 656 | ] 657 | whitenoise = [ 658 | {file = "whitenoise-5.3.0-py2.py3-none-any.whl", hash = "sha256:d963ef25639d1417e8a247be36e6aedd8c7c6f0a08adcb5a89146980a96b577c"}, 659 | {file = "whitenoise-5.3.0.tar.gz", hash = "sha256:d234b871b52271ae7ed6d9da47ffe857c76568f11dd30e28e18c5869dbd11e12"}, 660 | ] 661 | zipp = [ 662 | {file = "zipp-3.5.0-py3-none-any.whl", hash = "sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3"}, 663 | {file = "zipp-3.5.0.tar.gz", hash = "sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4"}, 664 | ] 665 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | module.exports = { 3 | plugins: [ 4 | require('autoprefixer') 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /project/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/labcodes/django-react-boilerplate/f24d6d13660420521c7f3ed14c2afbee5bff8f69/project/__init__.py -------------------------------------------------------------------------------- /project/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for project project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.0.1. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.0/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # If you're not using this project on heroku, comment the following lines 16 | import django_heroku 17 | from decouple import config 18 | 19 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 20 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 21 | 22 | # Quick-start development settings - unsuitable for production 23 | # See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/ 24 | 25 | # SECURITY WARNING: keep the secret key used in production secret! 26 | SECRET_KEY = config("SECRET_KEY") 27 | 28 | # SECURITY WARNING: don't run with debug turned on in production! 29 | DEBUG = config("DEBUG", False) 30 | 31 | # Needed for production. Avoid using '*'. 32 | ALLOWED_HOSTS = config("ALLOWED_HOSTS", ["localhost"]) 33 | 34 | # Application definition 35 | 36 | INSTALLED_APPS = [ 37 | "django.contrib.admin", 38 | "django.contrib.auth", 39 | "django.contrib.contenttypes", 40 | "django.contrib.sessions", 41 | "django.contrib.messages", 42 | "django.contrib.staticfiles", 43 | "rest_framework", 44 | "pwa", 45 | "core", 46 | ] 47 | 48 | MIDDLEWARE = [ 49 | "django.middleware.security.SecurityMiddleware", 50 | "django.contrib.sessions.middleware.SessionMiddleware", 51 | "django.middleware.common.CommonMiddleware", 52 | "django.middleware.csrf.CsrfViewMiddleware", 53 | "django.contrib.auth.middleware.AuthenticationMiddleware", 54 | "django.contrib.messages.middleware.MessageMiddleware", 55 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 56 | ] 57 | 58 | ROOT_URLCONF = "project.urls" 59 | 60 | TEMPLATES = [ 61 | { 62 | "BACKEND": "django.template.backends.django.DjangoTemplates", 63 | "DIRS": [os.path.join(BASE_DIR, "templates")], 64 | "APP_DIRS": True, 65 | "OPTIONS": { 66 | "context_processors": [ 67 | "django.template.context_processors.debug", 68 | "django.template.context_processors.request", 69 | "django.contrib.auth.context_processors.auth", 70 | "django.contrib.messages.context_processors.messages", 71 | ], 72 | }, 73 | }, 74 | ] 75 | 76 | WSGI_APPLICATION = "project.wsgi.application" 77 | 78 | # Database 79 | # https://docs.djangoproject.com/en/2.0/ref/settings/#databases 80 | 81 | DATABASES = { 82 | "default": { 83 | "ENGINE": "django.db.backends.sqlite3", 84 | "NAME": os.path.join(BASE_DIR, "db.sqlite3"), 85 | } 86 | } 87 | 88 | # Password validation 89 | # https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators 90 | 91 | AUTH_PASSWORD_VALIDATORS = [ 92 | { 93 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", 94 | }, 95 | { 96 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", 97 | }, 98 | { 99 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", 100 | }, 101 | { 102 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", 103 | }, 104 | ] 105 | 106 | # Internationalization 107 | # https://docs.djangoproject.com/en/2.0/topics/i18n/ 108 | 109 | LANGUAGE_CODE = "en-us" 110 | 111 | TIME_ZONE = "UTC" 112 | 113 | USE_I18N = True 114 | 115 | USE_L10N = True 116 | 117 | USE_TZ = True 118 | 119 | # Needed for 'debug' to be available inside templates. 120 | # https://docs.djangoproject.com/en/3.2/ref/templates/api/#django-template-context-processors-debug 121 | INTERNAL_IPS = ["127.0.0.1"] 122 | 123 | # Vite App Dir: point it to the folder your vite app is in. 124 | VITE_APP_DIR = os.path.join(BASE_DIR, "react-app") 125 | 126 | # Static files (CSS, JavaScript, Images) 127 | # https://docs.djangoproject.com/en/3.1/howto/static-files/ 128 | 129 | # You may change these, but it's important that the dist folder is includedself. 130 | # If it's not, collectstatic won't copy your bundle to production. 131 | # Static files (CSS, JavaScript, Images) 132 | # https://docs.djangoproject.com/en/2.0/howto/static-files/ 133 | 134 | STATIC_URL = "/static/" 135 | STATICFILES_DIRS = [ 136 | os.path.join(BASE_DIR, "static/"), 137 | os.path.join(VITE_APP_DIR, "dist"), 138 | ] 139 | STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles") 140 | 141 | # PWA configuration 142 | 143 | PWA_SERVICE_WORKER_PATH = os.path.join(BASE_DIR, "react-app/dist/sw.js") 144 | PWA_APP_NAME = "DRW - Change me!" 145 | PWA_APP_DESCRIPTION = "DRW - Change me!" 146 | PWA_APP_THEME_COLOR = "#0A0302" 147 | PWA_APP_BACKGROUND_COLOR = "#ffffff" 148 | PWA_APP_DISPLAY = "standalone" 149 | PWA_APP_SCOPE = "/" 150 | PWA_APP_ORIENTATION = "any" 151 | PWA_APP_START_URL = "/" 152 | PWA_APP_ICONS = [{"src": "/static/images/my_app_icon.png", "sizes": "192x192"}] 153 | PWA_APP_ICONS_APPLE = [{"src": "/static/images/my_app_icon.png", "sizes": "192x192"}] 154 | PWA_APP_SPLASH_SCREEN = [ 155 | { 156 | "src": "/static/images/splash-640x1136.png", 157 | "media": "(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2)", 158 | } 159 | ] 160 | PWA_APP_DIR = "ltr" 161 | PWA_APP_LANG = "en-US" 162 | 163 | # If you're not using this project on heroku, comment the following lines 164 | django_heroku.settings(locals()) 165 | -------------------------------------------------------------------------------- /project/urls.py: -------------------------------------------------------------------------------- 1 | """project URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/2.0/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.conf import settings 17 | from django.contrib import admin 18 | from django.views.generic import TemplateView 19 | from django.urls import path, re_path, include 20 | from time import sleep 21 | 22 | 23 | def sample_api_view(request): 24 | from django.http import JsonResponse 25 | 26 | sleep(2) 27 | return JsonResponse( 28 | { 29 | "message": """This message is coming from the backend. 30 | The django view is inside `project/urls.py` and the redux code is in `react-app/src/js/welcome/(actions|reducers).js`. 31 | Please remove them when starting your project :]""" 32 | } 33 | ) 34 | 35 | 36 | frontend_urls = [ 37 | re_path(r"^.*$", TemplateView.as_view(template_name="frontend/index.html")), 38 | ] 39 | 40 | 41 | if not settings.DEBUG: 42 | frontend_urls.insert(0, path("", include("pwa.urls"))) 43 | 44 | # if you wish to test the PWA on dev, uncomment the following lines, 45 | # so that django serves static files. 46 | # remember to built the frontend manually and run collectstatic as well. 47 | # from django.views.static import serve 48 | # frontend_urls += [ 49 | # re_path(r'^static/(?P.*)$', serve, {'document_root': settings.STATIC_ROOT}) 50 | # ] 51 | 52 | urlpatterns = [ 53 | path("admin/", admin.site.urls), 54 | path("api/sample-api-view/", sample_api_view), 55 | ] + frontend_urls 56 | -------------------------------------------------------------------------------- /project/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for project project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "django-react-boilerplate" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["Labcodes "] 6 | 7 | [tool.poetry.dependencies] 8 | python = "^3.6.14" 9 | Django = "^3.2.5" 10 | django-heroku = "^0.3.1" 11 | django-pwa = "^1.0.10" 12 | djangorestframework = "^3.12.4" 13 | dj-database-url = "^0.5.0" 14 | python-decouple = "^3.4" 15 | black = "^21.7b0" 16 | pytest = "^6.2.4" 17 | pytest-cov = "^2.12.1" 18 | 19 | [tool.poetry.dev-dependencies] 20 | 21 | [build-system] 22 | requires = ["poetry-core>=1.0.0"] 23 | build-backend = "poetry.core.masonry.api" 24 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | DJANGO_SETTINGS_MODULE = project.settings 3 | 4 | python_files = test.py test_*.py *_tests.py 5 | 6 | addopts = --cov-report html --cov . -------------------------------------------------------------------------------- /react-app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /react-app/js/Header.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function Header() { 4 | return ( 5 |
6 |

Django + React Boilerplate

7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /react-app/js/NoMatch404.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function NoMatch404() { 4 | return

404 Not Found

; 5 | } 6 | -------------------------------------------------------------------------------- /react-app/js/Routes.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { BrowserRouter as Router, Route, Switch } from "react-router-dom"; 3 | import Header from "./Header"; 4 | import NoMatch404 from "./NoMatch404"; 5 | 6 | const WelcomePage = React.lazy(() => import("./WelcomePage")); 7 | const SampleNestedPage = React.lazy(() => import("./SampleNestedPage")); 8 | 9 | export default function Routes() { 10 | return ( 11 | 12 |
13 | Loading...}> 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /react-app/js/SampleNestedPage/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Helmet from "react-helmet"; 3 | import { Link } from "react-router-dom"; 4 | 5 | function SampleNestedPage() { 6 | return ( 7 |
8 | 9 | DRW - Sample Nested Page 10 | 11 | 12 |

Welcome to our sample nested page!

13 |

It exists so we can discuss one thing: routing.

14 |

15 | Since this is a SPA inside a django app, we end up having routing both 16 | in the backend (django) and the frontend (react-router); so we need to 17 | keep them both in sync. 18 |

19 |

20 | To do that, for every route we create on the frontend (this one, for 21 | example), we need to create a mirror on the backend. It points to the 22 | same html as the index one. Please, take a look at your `urls.py` to see 23 | how it's done. :D 24 |

25 |

26 | Back to home. 27 |

28 |
29 | ); 30 | } 31 | 32 | export default SampleNestedPage; 33 | -------------------------------------------------------------------------------- /react-app/js/WelcomePage/__snapshots__/index.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Welcome renders without props 1`] = ` 4 |
5 |
8 |

11 | Welcome to our app! 12 |

13 |

14 | Now, you may edit the routes on the \`routes.js\` file and/or edit this file to start developing your app :] 15 |

16 |

17 | To understand a bit better about routing, take a look at our 18 | 21 | sample nested page 22 | 23 | . 24 |

25 |
26 |
27 | `; 28 | -------------------------------------------------------------------------------- /react-app/js/WelcomePage/actions.js: -------------------------------------------------------------------------------- 1 | import { fetchFromApi } from "react-redux-api-tools"; 2 | 3 | export const FETCH_WELCOME_MESSAGE = "FETCH_WELCOME_MESSAGE"; 4 | export const FETCH_WELCOME_MESSAGE_SUCCESS = "FETCH_WELCOME_MESSAGE_SUCCESS"; 5 | export const FETCH_WELCOME_MESSAGE_FAILURE = "FETCH_WELCOME_MESSAGE_FAILURE"; 6 | 7 | // this way of making requests with actions is based on react-redux-api-tools 8 | // https://github.com/labcodes/react-redux-api-tools 9 | export function fetchWelcomeMessage() { 10 | return { 11 | types: { 12 | request: FETCH_WELCOME_MESSAGE, 13 | success: FETCH_WELCOME_MESSAGE_SUCCESS, 14 | failure: FETCH_WELCOME_MESSAGE_FAILURE, 15 | }, 16 | shouldDispatch: ({ welcome }) => !welcome.message, 17 | apiCallFunction: () => fetchFromApi("/api/sample-api-view/"), 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /react-app/js/WelcomePage/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import Helmet from "react-helmet"; 3 | import PropTypes from "prop-types"; 4 | import { connect } from "react-redux"; 5 | import { Link } from "react-router-dom"; 6 | 7 | import { fetchWelcomeMessage } from "./actions"; 8 | 9 | export function WelcomePage({ fetchWelcomeMessage, message }) { 10 | useEffect(() => { 11 | fetchWelcomeMessage(); 12 | }, []); 13 | 14 | return ( 15 |
16 | 17 | DRW - Welcome! 18 | 19 | 20 |

Welcome to our app!

21 |

22 | Now, you may edit the routes on the `routes.js` file and/or edit this 23 | file to start developing your app :] 24 |

25 | {message ?

{message}

: null} 26 |

27 | To understand a bit better about routing, take a look at our 28 | sample nested page. 29 |

30 |
31 | ); 32 | } 33 | 34 | WelcomePage.propTypes = { 35 | message: PropTypes.string, 36 | fetchWelcomeMessage: PropTypes.func, 37 | }; 38 | 39 | WelcomePage.defaultProps = { 40 | message: null, 41 | fetchWelcomeMessage: () => {}, 42 | }; 43 | 44 | const mapStateToProps = ({ welcome }) => ({ message: welcome.message }); 45 | 46 | const mapDispatchToProps = (dispatch) => ({ 47 | fetchWelcomeMessage: () => dispatch(fetchWelcomeMessage()), 48 | }); 49 | 50 | export default connect(mapStateToProps, mapDispatchToProps)(WelcomePage); 51 | -------------------------------------------------------------------------------- /react-app/js/WelcomePage/index.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { MemoryRouter } from "react-router-dom"; 3 | import { render, screen } from "@testing-library/react"; 4 | 5 | import { WelcomePage } from "./index"; 6 | 7 | describe("Welcome", () => { 8 | it("renders without props", async () => { 9 | const component = render( 10 | 11 | 12 | 13 | ); 14 | expect(component).toBeTruthy(); 15 | expect(component.container.querySelector(".welcome__message")).toBeFalsy(); 16 | expect(component.container).toMatchSnapshot(); 17 | }); 18 | 19 | it("calls fetchWelcomeMessage on mounting", async () => { 20 | const mockedFunc = jest.fn(); 21 | expect(mockedFunc).not.toBeCalled(); 22 | render( 23 | 24 | 25 | 26 | ); 27 | expect(mockedFunc).toBeCalled(); 28 | }); 29 | 30 | it("renders message if passed", async () => { 31 | const message = "Test message"; 32 | render( 33 | 34 | 35 | 36 | ); 37 | expect(screen.getByText(message)).toBeTruthy(); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /react-app/js/WelcomePage/reducers.js: -------------------------------------------------------------------------------- 1 | import { 2 | FETCH_WELCOME_MESSAGE, 3 | FETCH_WELCOME_MESSAGE_FAILURE, 4 | FETCH_WELCOME_MESSAGE_SUCCESS, 5 | } from "./actions"; 6 | 7 | const initialState = { message: null }; 8 | 9 | export default function welcomeReducers(state = initialState, action) { 10 | switch (action.type) { 11 | case FETCH_WELCOME_MESSAGE: 12 | return { 13 | ...state, 14 | message: "Welcome Message is being fetched from the API...", 15 | }; 16 | 17 | case FETCH_WELCOME_MESSAGE_FAILURE: 18 | return { 19 | ...state, 20 | message: 21 | "It seems we're having trouble accessing the API. Are you testing the PWA?", 22 | }; 23 | 24 | case FETCH_WELCOME_MESSAGE_SUCCESS: 25 | const { message } = action.response.data; 26 | return { 27 | ...state, 28 | message, 29 | }; 30 | 31 | default: 32 | return state; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /react-app/js/main.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import thunk from "redux-thunk"; 4 | import { Provider } from "react-redux"; 5 | import { applyMiddleware, compose, createStore } from "redux"; 6 | import { apiMiddleware } from "react-redux-api-tools"; 7 | 8 | import rootReducer from "./rootReducer"; 9 | 10 | import "whatwg-fetch"; 11 | import "../scss/main.scss"; 12 | import Routes from "./Routes"; 13 | 14 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; 15 | 16 | const store = createStore( 17 | rootReducer, 18 | window.initialState, 19 | composeEnhancers(applyMiddleware(thunk, apiMiddleware)) 20 | ); 21 | 22 | function App() { 23 | return ( 24 | 25 | 26 | 27 | ); 28 | } 29 | 30 | ReactDOM.render(, document.getElementById("react-app")); 31 | -------------------------------------------------------------------------------- /react-app/js/rootReducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | 3 | import welcomeReducers from "./WelcomePage/reducers"; 4 | 5 | export default combineReducers({ 6 | welcome: welcomeReducers, 7 | }); 8 | -------------------------------------------------------------------------------- /react-app/scss/_base.scss: -------------------------------------------------------------------------------- 1 | // _base.scss 2 | // This partial contains basic css rules for our app, like vendor scss files, styles for headers, inputs, etc. 3 | // Any custom css rules that use ids and classes should not be defined here 4 | -------------------------------------------------------------------------------- /react-app/scss/_overrides.scss: -------------------------------------------------------------------------------- 1 | // _overrides.scss 2 | // This file is for last resort css rules, mostly used to override vendor/plugins css rules 3 | // This is the only file in which using "!important" is not discouraged 4 | -------------------------------------------------------------------------------- /react-app/scss/_variables.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/labcodes/django-react-boilerplate/f24d6d13660420521c7f3ed14c2afbee5bff8f69/react-app/scss/_variables.scss -------------------------------------------------------------------------------- /react-app/scss/components/_welcome.scss: -------------------------------------------------------------------------------- 1 | .welcome { 2 | .welcome__title { color: #333; } 3 | } 4 | -------------------------------------------------------------------------------- /react-app/scss/main.scss: -------------------------------------------------------------------------------- 1 | // main.scss 2 | // Even though it is the root file for styles, this file will only have imports of other scss files 3 | 4 | // first, we import the required scss partials 5 | @import "variables"; 6 | @import "base"; 7 | 8 | // then, we import our custom css 9 | @import "components/welcome"; 10 | 11 | // finally, we import the overrides needed for customizing third-party css 12 | @import "overrides"; 13 | -------------------------------------------------------------------------------- /static/images/my_app_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/labcodes/django-react-boilerplate/f24d6d13660420521c7f3ed14c2afbee5bff8f69/static/images/my_app_icon.png -------------------------------------------------------------------------------- /static/images/splash-640x1136.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/labcodes/django-react-boilerplate/f24d6d13660420521c7f3ed14c2afbee5bff8f69/static/images/splash-640x1136.png -------------------------------------------------------------------------------- /templates/frontend/index.html: -------------------------------------------------------------------------------- 1 | {% load render_vite_bundle %} 2 | {% load pwa %} 3 | {% load i18n %} 4 | 5 | {% get_current_language as LANGUAGE_CODE %} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Django React Boilerplate 15 | 16 | {% if debug %} 17 | 18 | 19 | 20 | 31 | {% else %} 32 | 33 | {% progressive_web_app_meta %} 34 | {% endif %} 35 | 36 | 37 | 38 | 39 | 40 |
41 | 42 | {% if debug %} 43 | 44 | 45 | {% else %} 46 | {% render_vite_bundle %} 47 | {% endif %} 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/labcodes/django-react-boilerplate/f24d6d13660420521c7f3ed14c2afbee5bff8f69/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_sample.py: -------------------------------------------------------------------------------- 1 | def test_sample(): 2 | assert True 3 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import { VitePWA } from "vite-plugin-pwa"; 3 | import viteCompression from "vite-plugin-compression"; 4 | import reactRefresh from "@vitejs/plugin-react-refresh"; 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | build: { manifest: true }, 9 | base: process.env.mode === "production" ? "/static/" : "/", 10 | root: "./react-app", 11 | plugins: [ 12 | reactRefresh(), 13 | viteCompression(), 14 | VitePWA({ 15 | workbox: { 16 | inlineWorkboxRuntime: true, 17 | navigateFallbackDenylist: [/^\/admin/, /^\/api/], 18 | modifyURLPrefix: { 19 | assets: "static/assets" 20 | } 21 | } 22 | }) 23 | ] 24 | }); 25 | --------------------------------------------------------------------------------