├── backend ├── common │ ├── __init__.py │ ├── utils │ │ ├── __init__.py │ │ └── tests.py │ ├── management │ │ ├── __init__.py │ │ └── commands │ │ │ ├── __init__.py │ │ │ └── celery.py │ ├── routes.py │ ├── urls.py │ ├── context_processors.py │ ├── models.py │ └── views.py ├── users │ ├── __init__.py │ ├── tests │ │ └── __init__.py │ ├── migrations │ │ └── __init__.py │ ├── views.py │ ├── apps.py │ ├── tasks.py │ ├── managers.py │ ├── admin.py │ └── models.py ├── project_name │ ├── settings │ │ ├── __init__.py │ │ ├── local.py.example │ │ ├── test.py │ │ ├── local_base.py │ │ ├── production.py │ │ └── base.py │ ├── __init__.py │ ├── celerybeat_schedule.py │ ├── wsgi.py │ ├── urls.py │ └── celery.py ├── templates │ ├── includes │ │ └── sentry_init.html │ ├── common │ │ └── index.html │ └── base.html ├── .env.example ├── Dockerfile └── manage.py ├── frontend ├── js │ ├── routes │ │ └── index.js │ ├── constants │ │ └── index.js │ ├── utils │ │ ├── urls.js │ │ ├── index.js │ │ └── SentryBoundary.js │ ├── app │ │ └── example-app │ │ │ ├── index.js │ │ │ └── components │ │ │ ├── index.js │ │ │ ├── ColorChanger │ │ │ ├── index.js │ │ │ ├── style.scss │ │ │ ├── __tests__ │ │ │ │ ├── ColorChanger.snaps.spec.js │ │ │ │ └── __snapshots__ │ │ │ │ │ └── ColorChanger.snaps.spec.js.snap │ │ │ └── ColorChanger.js │ │ │ └── ColorDisplay │ │ │ ├── index.js │ │ │ ├── ColorDisplay.js │ │ │ ├── style.scss │ │ │ └── __tests__ │ │ │ ├── __snapshots__ │ │ │ └── ColorDisplay.snaps.spec.js.snap │ │ │ └── ColorDisplay.snaps.spec.js │ ├── store │ │ ├── reducers.js │ │ ├── api.js │ │ ├── index.js │ │ └── rest_check.js │ ├── index.js │ ├── App.js │ └── pages │ │ └── Home.js ├── sass │ ├── _global.scss │ ├── _variables.scss │ ├── helpers │ │ ├── _functions.scss │ │ ├── _helpers.scss │ │ ├── _mixins.scss │ │ ├── _typography.scss │ │ ├── _placeholders.scss │ │ └── _all.scss │ ├── vendor │ │ ├── custom-bootstrap.scss │ │ └── _bootstrap-includes.scss │ ├── pages │ │ └── _all.scss │ ├── components │ │ └── _all.scss │ └── style.scss ├── assets │ └── images │ │ ├── django-logo-negative.png │ │ └── django-logo-positive.png └── Dockerfile ├── runtime.txt ├── .github ├── workflows │ ├── nightly.yml │ ├── main.yml │ ├── deploy.yml │ └── shared-build │ │ └── action.yml ├── ISSUE_TEMPLATE │ ├── Feature_request.md │ └── Bug_report.md └── PULL_REQUEST_TEMPLATE.md ├── .dockerignore ├── .eslintignore ├── postcss.config.js ├── .coveragerc ├── .bandit ├── babel.config.js ├── dev-requirements.in ├── jest-setup.js ├── bin ├── run_webpack ├── run_collectstatic └── post_compile ├── pyproject.toml ├── Procfile ├── .isort.cfg ├── requirements.in ├── .editorconfig ├── .eslintrc.js ├── server.js ├── .gitignore ├── .prospector.yaml ├── jest.config.js ├── LICENSE.txt ├── webpack.base.config.js ├── docker-compose.yml ├── CONTRIBUTING.md ├── .pre-commit-config.yaml ├── app.json ├── webpack.prod.config.js ├── Makefile ├── webpack.local.config.js ├── .bootstraprc ├── package.json ├── proj_main.yml ├── CODE_OF_CONDUCT.md └── README.md /backend/common/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/users/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/js/routes/index.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/sass/_global.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.9.0 2 | -------------------------------------------------------------------------------- /backend/common/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/users/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/js/constants/index.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/sass/_variables.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/workflows/nightly.yml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /backend/common/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/users/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/sass/helpers/_functions.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/sass/helpers/_helpers.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/sass/helpers/_mixins.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/sass/helpers/_typography.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/project_name/settings/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/sass/helpers/_placeholders.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/sass/vendor/custom-bootstrap.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/common/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .db 2 | node_modules 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /frontend/sass/pages/_all.scss: -------------------------------------------------------------------------------- 1 | // Pages 2 | 3 | // @import ""; -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | frontend/bundles/ 2 | frontend/webpack_bundles/ 3 | -------------------------------------------------------------------------------- /frontend/sass/components/_all.scss: -------------------------------------------------------------------------------- 1 | // Components 2 | 3 | // @import ""; -------------------------------------------------------------------------------- /backend/project_name/__init__.py: -------------------------------------------------------------------------------- 1 | from .celery import app as celery_app # noqa 2 | -------------------------------------------------------------------------------- /backend/project_name/settings/local.py.example: -------------------------------------------------------------------------------- 1 | from .local_base import * # noqa 2 | -------------------------------------------------------------------------------- /frontend/js/utils/urls.js: -------------------------------------------------------------------------------- 1 | const { Urls } = window; 2 | 3 | export default Urls; 4 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('autoprefixer') 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /backend/users/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render # noqa 2 | 3 | 4 | # Create your views here. 5 | -------------------------------------------------------------------------------- /frontend/js/app/example-app/index.js: -------------------------------------------------------------------------------- 1 | import ColorChanger from './components'; 2 | 3 | export default ColorChanger; 4 | -------------------------------------------------------------------------------- /backend/users/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class UsersConfig(AppConfig): 5 | name = "users" 6 | -------------------------------------------------------------------------------- /frontend/js/app/example-app/components/index.js: -------------------------------------------------------------------------------- 1 | import ColorChanger from './ColorChanger'; 2 | 3 | export default ColorChanger; 4 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | omit = */virtualenvs/* 4 | */migrations/* 5 | */settings/* 6 | */tests/* 7 | -------------------------------------------------------------------------------- /frontend/js/utils/index.js: -------------------------------------------------------------------------------- 1 | import Urls from './urls'; 2 | 3 | export { Urls }; // eslint-disable-line import/prefer-default-export 4 | -------------------------------------------------------------------------------- /frontend/js/app/example-app/components/ColorChanger/index.js: -------------------------------------------------------------------------------- 1 | import ColorChanger from './ColorChanger'; 2 | 3 | export default ColorChanger; 4 | -------------------------------------------------------------------------------- /frontend/js/app/example-app/components/ColorDisplay/index.js: -------------------------------------------------------------------------------- 1 | import ColorDisplay from './ColorDisplay'; 2 | 3 | export default ColorDisplay; 4 | -------------------------------------------------------------------------------- /backend/common/routes.py: -------------------------------------------------------------------------------- 1 | from .views import RestViewSet 2 | 3 | routes = [ 4 | {'regex': r'rest', 'viewset': RestViewSet, 'basename': 'Rest'}, 5 | ] 6 | -------------------------------------------------------------------------------- /.bandit: -------------------------------------------------------------------------------- 1 | [bandit] 2 | exclude: test_*.py,./venv/,./env/,./node_modules/,./cacheback/,./.env,./.venv 3 | skips: B101,B311 4 | # B101: assert 5 | # B311: random 6 | -------------------------------------------------------------------------------- /backend/templates/includes/sentry_init.html: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /frontend/assets/images/django-logo-negative.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stack-guru/django-react-boilerplate/HEAD/frontend/assets/images/django-logo-negative.png -------------------------------------------------------------------------------- /frontend/assets/images/django-logo-positive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stack-guru/django-react-boilerplate/HEAD/frontend/assets/images/django-logo-positive.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | presets: ['@babel/preset-env', '@babel/preset-react'], 5 | plugins: ['react-hot-loader/babel'], 6 | }; 7 | -------------------------------------------------------------------------------- /frontend/sass/helpers/_all.scss: -------------------------------------------------------------------------------- 1 | // Helpers 2 | 3 | @import "typography"; 4 | @import "functions"; 5 | @import "placeholders"; 6 | @import "helpers"; 7 | @import "mixins"; -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14.5.0-alpine 2 | 3 | WORKDIR /app/ 4 | ADD package.json /app/package.json 5 | RUN npm install 6 | ADD . /app/ 7 | 8 | CMD ["npm", "run", "start"] 9 | -------------------------------------------------------------------------------- /backend/common/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | app_name = 'common' 6 | urlpatterns = [ 7 | path('', views.IndexView.as_view(), name='index'), 8 | ] -------------------------------------------------------------------------------- /dev-requirements.in: -------------------------------------------------------------------------------- 1 | bandit 2 | black 3 | coverage 4 | django>=3,<4 5 | flake8==3.8.4 6 | isort 7 | model-bakery 8 | pre-commit 9 | pylint-django 10 | prospector[with_vulture] 11 | safety 12 | -------------------------------------------------------------------------------- /backend/users/tasks.py: -------------------------------------------------------------------------------- 1 | from django.core import management 2 | 3 | from {{project_name}} import celery_app 4 | 5 | 6 | @celery_app.task 7 | def clearsessions(): 8 | management.call_command('clearsessions') 9 | -------------------------------------------------------------------------------- /jest-setup.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | 3 | import enzyme from 'enzyme'; 4 | import Adapter from 'enzyme-adapter-react-16'; 5 | 6 | enzyme.configure({ adapter: new Adapter() }); 7 | -------------------------------------------------------------------------------- /bin/run_webpack: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eo pipefail 3 | 4 | if [ ! -f webpack-stats.json ]; then 5 | touch webpack-stats.json 6 | chmod 777 webpack-stats.json 7 | echo "webpack-stats.json created" 8 | fi 9 | npm run build 10 | -------------------------------------------------------------------------------- /backend/common/context_processors.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | 4 | def sentry_dsn(request): 5 | return {"SENTRY_DSN": settings.SENTRY_DSN} 6 | 7 | 8 | def commit_sha(request): 9 | return {"COMMIT_SHA": settings.COMMIT_SHA} 10 | -------------------------------------------------------------------------------- /backend/.env.example: -------------------------------------------------------------------------------- 1 | DJANGO_SETTINGS_MODULE={{project_name}}.settings.local 2 | CELERY_BROKER_URL=amqp://broker:5672// 3 | # Please choose postgres or sqlite as your DB: 4 | # DATABASE_URL=postgres://{{project_name}}:password@db:5432/{{project_name}} 5 | # DATABASE_URL=sqlite:///backend/db.sqlite3 6 | -------------------------------------------------------------------------------- /backend/project_name/celerybeat_schedule.py: -------------------------------------------------------------------------------- 1 | from celery.schedules import crontab # pylint:disable=import-error,no-name-in-module 2 | 3 | 4 | CELERYBEAT_SCHEDULE = { 5 | # Internal tasks 6 | "clearsessions": {"schedule": crontab(hour=3, minute=0), "task": "users.tasks.clearsessions"}, 7 | } 8 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 100 3 | target-version = ['py36'] 4 | quiet = true 5 | exclude = ''' 6 | /( 7 | \.eggs 8 | | \.git 9 | | \.hg 10 | | \.mypy_cache 11 | | \.venv 12 | | _build 13 | | build 14 | | dist 15 | | [a-z_]+/migrations 16 | )/ 17 | ''' 18 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn {{project_name}}.wsgi --chdir backend --limit-request-line 8188 --log-file - 2 | worker: REMAP_SIGTERM=SIGQUIT celery --workdir backend --app={{project_name}} worker --loglevel=info 3 | beat: REMAP_SIGTERM=SIGQUIT celery --workdir backend --app={{project_name}} beat -S redbeat.RedBeatScheduler --loglevel=info 4 | -------------------------------------------------------------------------------- /.isort.cfg: -------------------------------------------------------------------------------- 1 | [settings] 2 | line_length=100 3 | multi_line_output=3 4 | include_trailing_comma=True 5 | force_grid_wrap=0 6 | use_parentheses=True 7 | known_django=django 8 | sections=FUTURE,STDLIB,DJANGO,THIRDPARTY,FIRSTPARTY,LOCALFOLDER 9 | lines_after_imports=2 10 | atomic=True 11 | combine_star=True 12 | skip=venv,env,node_modules,migrations,.env,.venv 13 | -------------------------------------------------------------------------------- /frontend/js/store/reducers.js: -------------------------------------------------------------------------------- 1 | import { connectRouter } from 'connected-react-router'; 2 | import { combineReducers } from 'redux'; 3 | 4 | import { restCheckReducer as restCheck } from './rest_check'; 5 | 6 | export const createRootReducer = (history) => { 7 | return combineReducers({ 8 | router: connectRouter(history), 9 | restCheck, 10 | }); 11 | }; 12 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | on: 3 | push: 4 | branches-ignore: 5 | - boilerplate-release 6 | pull_request: 7 | 8 | jobs: 9 | build: 10 | name: Build boilerplate code 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v2 15 | - uses: ./.github/workflows/shared-build 16 | -------------------------------------------------------------------------------- /frontend/js/store/api.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import cookie from 'cookie'; 3 | 4 | const api = axios.create(); 5 | api.interceptors.request.use((config) => { 6 | const { csrftoken } = cookie.parse(document.cookie); 7 | if (csrftoken) { 8 | config.headers['X-CSRFTOKEN'] = csrftoken; 9 | } 10 | return config; 11 | }); 12 | 13 | export default api; 14 | -------------------------------------------------------------------------------- /requirements.in: -------------------------------------------------------------------------------- 1 | django>=3,<4 2 | celery[redis] 3 | celery-redbeat 4 | django-model-utils 5 | django-webpack-loader>=1.0.0 6 | django-js-reverse 7 | django-import-export 8 | djangorestframework 9 | django-debreach 10 | python-decouple 11 | psycopg2 12 | brotlipy 13 | django-log-request-id 14 | dj-database-url 15 | gunicorn 16 | whitenoise 17 | psutil 18 | ipython 19 | sentry-sdk 20 | -------------------------------------------------------------------------------- /frontend/js/app/example-app/components/ColorChanger/style.scss: -------------------------------------------------------------------------------- 1 | .main-container { 2 | border: 1px dashed grey; 3 | margin: 5px; 4 | padding: 5px; 5 | 6 | .app-name { 7 | color: red; 8 | padding-top: 20px; 9 | } 10 | 11 | .inner-container { 12 | margin: 0; 13 | padding: 5px; 14 | 15 | .color-picker { 16 | margin: 0px 5px 0px 5px; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /frontend/js/index.js: -------------------------------------------------------------------------------- 1 | // import pages 2 | import * as Sentry from '@sentry/browser'; 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | 6 | import '../sass/style.scss'; 7 | 8 | import App from './App'; 9 | 10 | Sentry.init({ 11 | dsn: window.SENTRY_DSN, 12 | release: window.COMMIT_SHA, 13 | }); 14 | 15 | ReactDOM.render(, document.getElementById('react-app')); 16 | -------------------------------------------------------------------------------- /backend/templates/common/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | 4 | {% block body %} 5 |

Django React Boilerplate Example App

6 | 7 |

The setup of the Django React Boilerplate Example App was successful.

8 | 9 |

React Example App

10 |

Here is an example of a React app included in the Django framework:

11 | 12 |
13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /backend/common/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.utils.translation import ugettext_lazy as _ 3 | 4 | from model_utils.fields import AutoCreatedField, AutoLastModifiedField 5 | 6 | 7 | class IndexedTimeStampedModel(models.Model): 8 | created = AutoCreatedField(_("created"), db_index=True) 9 | modified = AutoLastModifiedField(_("modified"), db_index=True) 10 | 11 | class Meta: 12 | abstract = True 13 | -------------------------------------------------------------------------------- /bin/run_collectstatic: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eo pipefail 3 | 4 | indent() { 5 | RE="s/^/ /" 6 | [ $(uname) == "Darwin" ] && sed -l "$RE" || sed -u "$RE" 7 | } 8 | 9 | MANAGE_FILE=$(find . -maxdepth 3 -type f -name 'manage.py' | head -1) 10 | MANAGE_FILE=${MANAGE_FILE:2} 11 | 12 | echo "-----> Collecting static files" 13 | python $MANAGE_FILE collectstatic --noinput 2>&1 | sed '/^Copying/d;/^$/d;/^ /d' | indent 14 | 15 | echo 16 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | end_of_line = lf 12 | 13 | [*.js] 14 | indent_size = 2 15 | 16 | [*.html] 17 | indent_size = 2 18 | 19 | [*.scss] 20 | indent_size = 2 21 | 22 | [*.md] 23 | trim_trailing_whitespace = false 24 | 25 | [Makefile] 26 | indent_style = tab 27 | 28 | [*.yml] 29 | indent_size = 2 30 | -------------------------------------------------------------------------------- /frontend/js/app/example-app/components/ColorDisplay/ColorDisplay.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | 4 | import './style.scss'; 5 | 6 | const ColorDisplay = (props) => { 7 | const { color } = props; 8 | 9 | return {color}; 10 | }; 11 | 12 | ColorDisplay.defaultProps = { 13 | color: 'black', 14 | }; 15 | 16 | ColorDisplay.propTypes = { 17 | color: PropTypes.string, 18 | }; 19 | 20 | export default ColorDisplay; 21 | -------------------------------------------------------------------------------- /frontend/js/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { hot } from 'react-hot-loader/root'; 3 | import { Provider } from 'react-redux'; 4 | 5 | import Home from './pages/Home'; 6 | import configureStore from './store'; 7 | import SentryBoundary from './utils/SentryBoundary'; 8 | 9 | const store = configureStore({}); 10 | const App = () => ( 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | 18 | export default hot(App); 19 | -------------------------------------------------------------------------------- /backend/project_name/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for {{project_name}} 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/1.10/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | 15 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "{{project_name}}.settings.production") 16 | 17 | application = get_wsgi_application() 18 | -------------------------------------------------------------------------------- /frontend/js/app/example-app/components/ColorDisplay/style.scss: -------------------------------------------------------------------------------- 1 | %text-color { 2 | font-weight: bold; 3 | margin: 0px 5px; 4 | }; 5 | 6 | .color-black { 7 | @extend %text-color; 8 | color: black; 9 | } 10 | 11 | .color-green { 12 | @extend %text-color; 13 | color: green; 14 | } 15 | 16 | .color-red { 17 | @extend %text-color; 18 | color: red; 19 | } 20 | 21 | .color-blue { 22 | @extend %text-color; 23 | color: blue; 24 | } 25 | 26 | .color-purple { 27 | @extend %text-color; 28 | color: purple; 29 | } 30 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | root: true, 5 | extends: ['vinta/recommended'], 6 | rules: { 7 | "default-param-last": "off", 8 | }, 9 | env: { 10 | es6: true, 11 | browser: true, 12 | jest: true 13 | }, 14 | settings: { 15 | 'import/resolver': { 16 | webpack: { 17 | config: path.join(__dirname, '/webpack.local.config.js'), 18 | 'config-index': 1 19 | } 20 | }, 21 | react: { 22 | "version": "detect" 23 | }, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | // Webpack dev server 2 | import webpack from 'webpack'; 3 | import WebpackDevServer from 'webpack-dev-server'; 4 | 5 | import config from './webpack.local.config'; 6 | 7 | new WebpackDevServer(webpack(config), { 8 | publicPath: config.output.publicPath, 9 | port: 3000, 10 | hot: true, 11 | inline: true, 12 | historyApiFallback: true, 13 | headers: { 'Access-Control-Allow-Origin': '*' }, 14 | }).listen(3000, '0.0.0.0', (err) => { 15 | if (err) { 16 | console.log(err); 17 | } 18 | 19 | console.log('Listening at 0.0.0.0:3000'); 20 | }); 21 | -------------------------------------------------------------------------------- /frontend/js/app/example-app/components/ColorDisplay/__tests__/__snapshots__/ColorDisplay.snaps.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ColorDisplay no color (should default to black) 1`] = ` 4 | 7 | black 8 | 9 | `; 10 | 11 | exports[`ColorDisplay purple 1`] = ` 12 | 15 | purple 16 | 17 | `; 18 | 19 | exports[`ColorDisplay unknown color 1`] = ` 20 | 23 | caterpillar 24 | 25 | `; 26 | -------------------------------------------------------------------------------- /backend/users/managers.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import BaseUserManager 2 | 3 | 4 | class UserManager(BaseUserManager): 5 | def create_user(self, email, password=None, **kwargs): 6 | email = self.normalize_email(email) 7 | user = self.model(email=email, **kwargs) 8 | user.set_password(password) 9 | user.save(using=self._db) 10 | return user 11 | 12 | def create_superuser(self, **kwargs): 13 | user = self.create_user(**kwargs) 14 | user.is_superuser = True 15 | user.is_staff = True 16 | user.save(using=self._db) 17 | return user 18 | -------------------------------------------------------------------------------- /backend/project_name/settings/test.py: -------------------------------------------------------------------------------- 1 | from .base import * # noqa 2 | 3 | 4 | SECRET_KEY = "test" 5 | 6 | STATIC_ROOT = base_dir_join('staticfiles') 7 | STATIC_URL = '/static/' 8 | 9 | MEDIA_ROOT = base_dir_join('mediafiles') 10 | MEDIA_URL = '/media/' 11 | 12 | DEFAULT_FILE_STORAGE = "django.core.files.storage.FileSystemStorage" 13 | STATICFILES_STORAGE = "django.contrib.staticfiles.storage.StaticFilesStorage" 14 | 15 | # Speed up password hashing 16 | PASSWORD_HASHERS = [ 17 | "django.contrib.auth.hashers.MD5PasswordHasher", 18 | ] 19 | 20 | # Celery 21 | CELERY_TASK_ALWAYS_EAGER = True 22 | CELERY_TASK_EAGER_PROPAGATES = True 23 | -------------------------------------------------------------------------------- /frontend/js/app/example-app/components/ColorChanger/__tests__/ColorChanger.snaps.spec.js: -------------------------------------------------------------------------------- 1 | import { shallow } from 'enzyme'; 2 | import React from 'react'; 3 | 4 | import ColorChanger from '../ColorChanger'; 5 | 6 | jest.mock('../../ColorDisplay/ColorDisplay'); 7 | 8 | describe('ColorChanger', () => { 9 | test('some title', () => { 10 | const wrapper = shallow(); 11 | 12 | expect(wrapper).toMatchSnapshot(); 13 | }); 14 | 15 | test('no title (should use default)', () => { 16 | const wrapper = shallow(); 17 | 18 | expect(wrapper).toMatchSnapshot(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /frontend/sass/style.scss: -------------------------------------------------------------------------------- 1 | // Main Style 2 | 3 | // General 4 | @import "variables"; 5 | @import "vendor/bootstrap-includes"; 6 | @import "helpers/all"; 7 | @import "global"; 8 | @import "components/all"; 9 | @import "pages/all"; 10 | 11 | #django-background { 12 | color: #092e20; 13 | font-size: 11pt; 14 | background-repeat: no-repeat; 15 | background-size: auto 200px; 16 | background-position: center; 17 | height: 300px; 18 | background-image: url('../assets/images/django-logo-positive.png'); 19 | } 20 | 21 | #django-logo-wrapper { 22 | color: #092e20; 23 | & > img { 24 | width: 100px; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /backend/project_name/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import include 2 | from django.urls import path 3 | from django.contrib import admin 4 | 5 | import django_js_reverse.views 6 | from rest_framework.routers import DefaultRouter 7 | 8 | from common.routes import routes as common_routes 9 | 10 | router = DefaultRouter() 11 | 12 | routes = common_routes 13 | for route in routes: 14 | router.register(route['regex'], route['viewset'], basename=route['basename']) 15 | 16 | urlpatterns = [ 17 | path("", include("common.urls"), name="common"), 18 | path("admin/", admin.site.urls, name="admin"), 19 | path("jsreverse/", django_js_reverse.views.urls_js, name="js_reverse"), 20 | 21 | path("api/", include(router.urls), name="api"), 22 | ] 23 | -------------------------------------------------------------------------------- /frontend/js/app/example-app/components/ColorDisplay/__tests__/ColorDisplay.snaps.spec.js: -------------------------------------------------------------------------------- 1 | import { shallow } from 'enzyme'; 2 | import React from 'react'; 3 | 4 | import ColorDisplay from '../ColorDisplay'; 5 | 6 | describe('ColorDisplay', () => { 7 | test('purple', () => { 8 | const wrapper = shallow(); 9 | 10 | expect(wrapper).toMatchSnapshot(); 11 | }); 12 | 13 | test('no color (should default to black)', () => { 14 | const wrapper = shallow(); 15 | 16 | expect(wrapper).toMatchSnapshot(); 17 | }); 18 | 19 | test('unknown color', () => { 20 | const wrapper = shallow(); 21 | 22 | expect(wrapper).toMatchSnapshot(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /frontend/js/store/index.js: -------------------------------------------------------------------------------- 1 | import { routerMiddleware } from 'connected-react-router'; 2 | import { createBrowserHistory } from 'history'; 3 | import { createStore, applyMiddleware } from 'redux'; 4 | import { composeWithDevTools } from 'redux-devtools-extension'; 5 | import thunk from 'redux-thunk'; 6 | 7 | import { createRootReducer } from './reducers'; 8 | 9 | export const history = createBrowserHistory(); 10 | 11 | const rootReducer = createRootReducer(history); 12 | 13 | const enhancer = composeWithDevTools(applyMiddleware(thunk, routerMiddleware(history))); 14 | 15 | const configureStore = (preloadedState) => { 16 | const store = createStore(rootReducer, preloadedState, enhancer); 17 | return store; 18 | }; 19 | 20 | export default configureStore; 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | /backend/db.sqlite3 3 | .DS_Store 4 | .env 5 | */settings/local.py 6 | /staticfiles/* 7 | /mediafiles/* 8 | __pycache__/ 9 | /.vscode/ 10 | 11 | # coverage result 12 | .coverage 13 | /coverage/ 14 | 15 | # pycharm 16 | .idea/ 17 | 18 | # data 19 | *.dump 20 | 21 | # npm 22 | node_modules/ 23 | npm-debug.log 24 | 25 | # Webpack 26 | /frontend/bundles/* 27 | /frontend/webpack_bundles/* 28 | /webpack-stats.json 29 | 30 | # Sass 31 | .sass-cache 32 | *.map 33 | 34 | # General 35 | /{{project_name}}-venv/ 36 | /venv/ 37 | /env/ 38 | /output/ 39 | /cache/ 40 | boilerplate.zip 41 | 42 | # Spritesmith 43 | spritesmith-generated/ 44 | spritesmith.scss 45 | 46 | # templated email 47 | tmp_email/ 48 | 49 | .direnv 50 | .envrc 51 | .tool-versions 52 | -------------------------------------------------------------------------------- /backend/common/views.py: -------------------------------------------------------------------------------- 1 | from django.views import generic 2 | 3 | from rest_framework import viewsets, status 4 | from rest_framework.decorators import action 5 | from rest_framework.response import Response 6 | from rest_framework.permissions import AllowAny 7 | 8 | 9 | class IndexView(generic.TemplateView): 10 | template_name = 'common/index.html' 11 | 12 | 13 | class RestViewSet(viewsets.ViewSet): 14 | @action( 15 | detail=False, 16 | methods=['get'], 17 | permission_classes=[AllowAny], 18 | url_path='rest-check', 19 | ) 20 | def rest_check(self, request): 21 | return Response( 22 | {"result": "If you're seeing this, the REST API is working!"}, 23 | status=status.HTTP_200_OK, 24 | ) -------------------------------------------------------------------------------- /backend/project_name/celery.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | from django.apps import apps 5 | 6 | from celery import Celery 7 | from decouple import config 8 | 9 | from .celerybeat_schedule import CELERYBEAT_SCHEDULE 10 | 11 | settings_module = config("DJANGO_SETTINGS_MODULE", default=None) 12 | if settings_module is None: 13 | print( 14 | "Error: no DJANGO_SETTINGS_MODULE found. Will NOT start devserver. " 15 | "Remember to create .env file at project root. " 16 | "Check README for more info." 17 | ) 18 | sys.exit(1) 19 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", settings_module) 20 | 21 | app = Celery("{{project_name}}_tasks") 22 | app.config_from_object("django.conf:settings", namespace="CELERY") 23 | app.autodiscover_tasks(lambda: [n.name for n in apps.get_app_configs()]) 24 | app.conf.update(CELERYBEAT_SCHEDULE=CELERYBEAT_SCHEDULE) 25 | -------------------------------------------------------------------------------- /frontend/js/store/rest_check.js: -------------------------------------------------------------------------------- 1 | import api from './api'; 2 | 3 | // Action types 4 | const types = { 5 | FETCH_REQUESTED: 'rest_check/FETCH_REQUESTED', 6 | FETCH_SUCCESS: 'rest_check/FETCH_SUCCESS', 7 | FETCH_ERROR: 'rest_check/FETCH_ERROR', 8 | }; 9 | 10 | // Action creators 11 | export const creators = { 12 | fetchRestCheck: () => { 13 | return async (dispatch) => { 14 | dispatch({ type: types.FETCH_REQUESTED }); 15 | try { 16 | const res = await api.get('/api/rest/rest-check/'); 17 | dispatch({ type: types.FETCH_SUCCESS, data: res.data }); 18 | } catch (error) { 19 | dispatch({ type: types.FETCH_ERROR, error }); 20 | } 21 | }; 22 | }, 23 | }; 24 | 25 | // Reducer 26 | export const restCheckReducer = (state = {}, action) => { 27 | if (action.type === types.FETCH_SUCCESS) return action.data; 28 | return state; 29 | }; 30 | -------------------------------------------------------------------------------- /.prospector.yaml: -------------------------------------------------------------------------------- 1 | output-format: text 2 | 3 | strictness: veryhigh 4 | test-warnings: true 5 | doc-warnings: false 6 | member-warnings: true 7 | 8 | uses: 9 | - django 10 | - celery 11 | 12 | pep8: 13 | full: true 14 | disable: 15 | - D100 16 | - D101 17 | - D102 18 | - D103 19 | - D105 20 | - D205 21 | - D400 22 | - N802 # function name should be lowercase, breaks on tests 23 | options: 24 | max-line-length: 100 25 | 26 | pyflakes: 27 | disable: 28 | - F999 29 | 30 | pylint: 31 | disable: 32 | - too-few-public-methods 33 | - invalid-name 34 | - no-self-use 35 | - no-member 36 | options: 37 | max-line-length: 100 38 | 39 | dodgy: 40 | run: true 41 | 42 | ignore-paths: 43 | - node_modules 44 | - venv 45 | - env 46 | - .env 47 | - .venv 48 | 49 | ignore-patterns: 50 | - .+/migrations/.+ 51 | - .+/settings/.+ 52 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | transform: { 5 | '^.+\\.jsx$': 'babel-jest', 6 | '^.+\\.js$': 'babel-jest', 7 | }, 8 | moduleNameMapper: { 9 | '^.+\\.(css|scss)$': 'identity-obj-proxy', 10 | }, 11 | transformIgnorePatterns: [ 12 | 'node_modules/*', 13 | ], 14 | modulePaths: [ 15 | 'frontend', 16 | 'frontend/js', 17 | 'frontend/js/app', 18 | ], 19 | snapshotSerializers: [ 20 | 'enzyme-to-json/serializer', 21 | ], 22 | setupFiles: [ 23 | './jest-setup.js', 24 | ], 25 | collectCoverageFrom: [ 26 | 'frontend/js/**/*.{js,jsx}', 27 | ], 28 | coveragePathIgnorePatterns: [ 29 | 'frontend/js/store.js', 30 | 'frontend/js/index.js', 31 | 'frontend/js/constants/*', 32 | 'frontend/js/pages/*', 33 | 'frontend/js/tests/*', 34 | ], 35 | coverageThreshold: { 36 | global: { 37 | statements: 10, 38 | }, 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /backend/users/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.auth.admin import UserAdmin 3 | from django.utils.translation import ugettext_lazy as _ 4 | 5 | from .models import User 6 | 7 | 8 | class CustomUserAdmin(UserAdmin): 9 | list_display = ("id", "email", "created", "modified") 10 | list_filter = ("is_active", "is_staff", "groups") 11 | search_fields = ("email",) 12 | ordering = ("email",) 13 | filter_horizontal = ( 14 | "groups", 15 | "user_permissions", 16 | ) 17 | 18 | fieldsets = ( 19 | (None, {"fields": ("email", "password")}), 20 | ( 21 | _("Permissions"), 22 | {"fields": ("is_active", "is_staff", "is_superuser", "groups", "user_permissions")}, 23 | ), 24 | ) 25 | add_fieldsets = ((None, {"classes": ("wide",), "fields": ("email", "password1", "password2")}),) 26 | 27 | 28 | admin.site.register(User, CustomUserAdmin) 29 | -------------------------------------------------------------------------------- /backend/templates/base.html: -------------------------------------------------------------------------------- 1 | {% load render_bundle from webpack_loader %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | {% block title %}{% endblock %} 9 | 10 | 11 | 12 | 13 | 14 | 15 | {% render_bundle 'main' 'css' %} 16 | 17 | 18 | 19 | {% include 'includes/sentry_init.html' %} 20 | 21 | 22 | {% block body %}{% endblock %} 23 | 24 | 25 | 26 | 27 | 28 | {% render_bundle 'main' 'js' 'DEFAULT' %} 29 | {% block scripts %}{% endblock %} 30 | 31 | 32 | -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8-slim 2 | 3 | ENV PYTHONUNBUFFERED 1 4 | 5 | RUN groupadd user && useradd --create-home --home-dir /home/user -g user user 6 | WORKDIR /home/user/app/backend 7 | 8 | # Install system dependencies 9 | RUN apt-get update && apt-get install gcc build-essential libpq-dev -y && \ 10 | python3 -m pip install --no-cache-dir pip-tools 11 | 12 | # install python dependencies 13 | ADD *requirements.in /home/user/app/backend/ 14 | RUN pip-compile requirements.in > requirements.txt && \ 15 | pip-compile dev-requirements.in > dev-requirements.txt 16 | 17 | RUN pip install -r requirements.txt && \ 18 | pip install -r dev-requirements.txt && \ 19 | pip install psycopg2-binary 20 | 21 | # Clean the house 22 | RUN apt-get purge libpq-dev -y && apt-get autoremove -y && \ 23 | rm /var/lib/apt/lists/* rm -rf /var/cache/apt/* 24 | 25 | ADD backend/ /home/user/app/backend 26 | 27 | USER user 28 | CMD gunicorn {{project_name}}.wsgi --log-file - -b 0.0.0.0:8000 --reload 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Desktop (please complete the following information):** 24 | - OS: [e.g. iOS] 25 | - Browser [e.g. chrome, safari] 26 | - Version [e.g. 22] 27 | 28 | **Smartphone (please complete the following information):** 29 | - Device: [e.g. iPhone6] 30 | - OS: [e.g. iOS8.1] 31 | - Browser [e.g. stock browser, safari] 32 | - Version [e.g. 22] 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: deploy 2 | on: 3 | workflow_run: 4 | workflows: ["main"] 5 | branches: [master] 6 | types: 7 | - completed 8 | 9 | jobs: 10 | deploy: 11 | name: Generate stable boilerplate build 12 | strategy: 13 | matrix: 14 | python-version: [3.8] 15 | node-version: [14.5] 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout code 19 | uses: actions/checkout@v2 20 | with: 21 | ref: master 22 | - run: mkdir -p github/workflows 23 | - run: mv proj_main.yml github/workflows/main.yml 24 | - run: git checkout -b boilerplate-release 25 | - run: git add github/workflows/main.yml 26 | - run: git rm proj_main.yml 27 | - run: git commit -m "Replacing project actions" --author "Vinta Software " 28 | env: 29 | GIT_COMMITTER_NAME: "Vinta Software" 30 | GIT_COMMITTER_EMAIL: "contact@vinta.com.br" 31 | - run: git push origin boilerplate-release --force 32 | -------------------------------------------------------------------------------- /backend/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import sys 5 | 6 | from decouple import config 7 | 8 | 9 | if __name__ == "__main__": 10 | settings_module = config("DJANGO_SETTINGS_MODULE", default=None) 11 | 12 | if sys.argv[1] == "test": 13 | if settings_module: 14 | print( 15 | "Ignoring config('DJANGO_SETTINGS_MODULE') because it's test. " 16 | "Using '{{project_name}}.settings.test'" 17 | ) 18 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "{{project_name}}.settings.test") 19 | else: 20 | if settings_module is None: 21 | print( 22 | "Error: no DJANGO_SETTINGS_MODULE found. Will NOT start devserver. " 23 | "Remember to create .env file at project root. " 24 | "Check README for more info." 25 | ) 26 | sys.exit(1) 27 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", settings_module) 28 | 29 | from django.core.management import execute_from_command_line 30 | 31 | execute_from_command_line(sys.argv) 32 | -------------------------------------------------------------------------------- /backend/users/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin 2 | from django.db import models 3 | from django.utils.translation import ugettext_lazy as _ 4 | 5 | from common.models import IndexedTimeStampedModel 6 | 7 | from .managers import UserManager 8 | 9 | 10 | class User(AbstractBaseUser, PermissionsMixin, IndexedTimeStampedModel): 11 | email = models.EmailField(max_length=255, unique=True) 12 | is_staff = models.BooleanField( 13 | default=False, help_text=_("Designates whether the user can log into this admin " "site.") 14 | ) 15 | is_active = models.BooleanField( 16 | default=True, 17 | help_text=_( 18 | "Designates whether this user should be treated as " 19 | "active. Unselect this instead of deleting accounts." 20 | ), 21 | ) 22 | 23 | objects = UserManager() 24 | 25 | USERNAME_FIELD = "email" 26 | 27 | def get_full_name(self): 28 | return self.email 29 | 30 | def get_short_name(self): 31 | return self.email 32 | 33 | def __str__(self): 34 | return self.email 35 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Vinta Serviços e Soluções Tecnológicas Ltda 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /backend/common/management/commands/celery.py: -------------------------------------------------------------------------------- 1 | # pylint: skip-file 2 | import getpass 3 | import shlex 4 | from subprocess import PIPE # nosec 5 | 6 | from django.core.management.base import BaseCommand 7 | from django.utils.autoreload import run_with_reloader 8 | 9 | import psutil 10 | 11 | 12 | def restart_celery(): 13 | for proc in psutil.process_iter(): 14 | if proc.username() != getpass.getuser(): # skip processes not owned by user 15 | continue 16 | if proc.name() != "celery": 17 | continue 18 | # SIGTERM should only be sent to parent process, never to children processes 19 | # see: https://github.com/celery/celery/issues/2700#issuecomment-259716123 20 | if not proc.children(): 21 | continue 22 | celery_proc = proc # found parent celery process 23 | celery_proc.terminate() 24 | break 25 | cmd = "celery worker -A {{project_name}} -l INFO" 26 | psutil.Popen(shlex.split(cmd), stdout=PIPE) 27 | 28 | 29 | class Command(BaseCommand): 30 | def handle(self, *args, **kwargs): 31 | print("Starting celery worker with autoreload") 32 | run_with_reloader(restart_celery) 33 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Description 4 | 5 | 6 | ## Motivation and Context 7 | 8 | 9 | ## Screenshots (if appropriate): 10 | 11 | ## Steps to reproduce (if appropriate): 12 | 13 | ## Types of changes 14 | 15 | - [ ] Bug fix (non-breaking change which fixes an issue) 16 | - [ ] New feature (non-breaking change which adds functionality) 17 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 18 | 19 | ## Checklist: 20 | 21 | 22 | - [ ] My code follows the code style of this project. 23 | - [ ] My change requires documentation updates. 24 | - [ ] I have updated the documentation accordingly. 25 | - [ ] My change requires dependencies updates. 26 | - [ ] I have updated the dependencies accordingly. 27 | -------------------------------------------------------------------------------- /bin/post_compile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eo pipefail 3 | 4 | echo "-----> Post-compile hook" 5 | 6 | if [ -f bin/run_collectstatic ] && [ -n "$ENABLE_DJANGO_COLLECTSTATIC" ] && [ "$ENABLE_DJANGO_COLLECTSTATIC" == 1 ]; then 7 | echo "-----> Running run_collectstatic" 8 | chmod +x bin/run_collectstatic 9 | bin/run_collectstatic 10 | fi 11 | 12 | MANAGE_FILE=$(find . -maxdepth 3 -type f -name 'manage.py' | head -1) 13 | MANAGE_FILE=${MANAGE_FILE:2} 14 | 15 | echo "-----> Running manage.py check --deploy --fail-level WARNING" 16 | python $MANAGE_FILE check --deploy --fail-level WARNING 17 | 18 | if [ -n "$AUTO_MIGRATE" ] && [ "$AUTO_MIGRATE" == 1 ]; then 19 | echo "-----> Running manage.py migrate" 20 | python $MANAGE_FILE migrate --noinput 21 | fi 22 | 23 | echo "-----> Pushing source maps to Sentry" 24 | if [ -n "$SENTRY_API_KEY" ] && [ -n "$SENTRY_ORG" ] && [ -n "$SENTRY_PROJECT_NAME" ] && [ -n "$SOURCE_VERSION" ]; then 25 | npx @sentry/cli --auth-token=$SENTRY_API_KEY releases --org=$SENTRY_ORG --project=$SENTRY_PROJECT_NAME files $SOURCE_VERSION upload-sourcemaps ./frontend/webpack_bundles/ --url-prefix "~/static/webpack_bundles/" --rewrite 26 | rm ./frontend/webpack_bundles/*.js.map 27 | fi 28 | 29 | echo "-----> Post-compile done" 30 | -------------------------------------------------------------------------------- /webpack.base.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | context: __dirname, 5 | entry: [ 6 | // defined in local or prod 7 | ], 8 | output: { 9 | // defined in local or prod 10 | }, 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.css$/, 15 | use: [{ loader: 'style-loader' }, { loader: 'css-loader' }, { loader: 'postcss-loader' }], 16 | }, 17 | { 18 | test: /\.scss$/, 19 | use: [ 20 | { loader: 'style-loader' }, 21 | { loader: 'css-loader' }, 22 | { 23 | loader: 'postcss-loader', 24 | options: { 25 | sourceMap: true, 26 | }, 27 | }, 28 | { loader: 'sass-loader' }, 29 | ], 30 | }, 31 | { 32 | test: /\.(svg)(\?v=\d+\.\d+\.\d+)?$/, 33 | loader: 'url-loader?limit=100000', 34 | }, 35 | { 36 | test: /\.(jpg|png)?$/, 37 | loader: ['file-loader?name=i-[hash].[ext]'], 38 | }, 39 | ], 40 | }, 41 | plugins: [ 42 | // defined in local or prod 43 | ], 44 | resolve: { 45 | modules: ['node_modules', 'bower_components', path.resolve(__dirname, 'frontend/js/')], 46 | extensions: ['.js', '.jsx'], 47 | }, 48 | }; 49 | -------------------------------------------------------------------------------- /backend/project_name/settings/local_base.py: -------------------------------------------------------------------------------- 1 | from .base import * # noqa 2 | 3 | 4 | DEBUG = True 5 | 6 | HOST = "http://localhost:8000" 7 | 8 | SECRET_KEY = "secret" 9 | 10 | STATIC_ROOT = base_dir_join("staticfiles") 11 | STATIC_URL = "/static/" 12 | 13 | MEDIA_ROOT = base_dir_join("mediafiles") 14 | MEDIA_URL = "/media/" 15 | 16 | DEFAULT_FILE_STORAGE = "django.core.files.storage.FileSystemStorage" 17 | STATICFILES_STORAGE = "django.contrib.staticfiles.storage.StaticFilesStorage" 18 | 19 | AUTH_PASSWORD_VALIDATORS = [] # allow easy passwords only on local 20 | 21 | # Celery 22 | CELERY_TASK_ALWAYS_EAGER = True 23 | CELERY_TASK_EAGER_PROPAGATES = True 24 | 25 | # Email settings for mailhog 26 | EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" 27 | EMAIL_HOST = 'mailhog' 28 | EMAIL_PORT = 1025 29 | 30 | # Logging 31 | LOGGING = { 32 | "version": 1, 33 | "disable_existing_loggers": False, 34 | "formatters": {"standard": {"format": "%(levelname)-8s [%(asctime)s] %(name)s: %(message)s"},}, 35 | "handlers": { 36 | "console": {"level": "DEBUG", "class": "logging.StreamHandler", "formatter": "standard",}, 37 | }, 38 | "loggers": { 39 | "": {"handlers": ["console"], "level": "INFO"}, 40 | "celery": {"handlers": ["console"], "level": "INFO"}, 41 | }, 42 | } 43 | 44 | JS_REVERSE_JS_MINIFY = False 45 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.1' 2 | 3 | services: 4 | db: 5 | image: "postgres:alpine" 6 | environment: 7 | - POSTGRES_USER={{project_name}} 8 | - POSTGRES_PASSWORD=password 9 | - POSTGRES_DB={{project_name}} 10 | ports: 11 | - "5432" 12 | volumes: 13 | - dbdata:/var/lib/postgresql/data:delegated 14 | 15 | broker: 16 | image: "rabbitmq:alpine" 17 | 18 | result: 19 | image: "redis:alpine" 20 | 21 | frontend: 22 | build: 23 | dockerfile: frontend/Dockerfile 24 | context: . 25 | volumes: 26 | - .:/app/ 27 | - /app/node_modules 28 | ports: 29 | - "3000:3000" 30 | 31 | backend: 32 | build: 33 | dockerfile: backend/Dockerfile 34 | context: . 35 | ports: 36 | - "8000:8000" 37 | volumes: 38 | - ./:/home/user/app/ 39 | env_file: backend/.env 40 | depends_on: 41 | - db 42 | - broker 43 | - result 44 | - frontend 45 | 46 | celery: 47 | build: 48 | dockerfile: backend/Dockerfile 49 | context: . 50 | command: python manage.py celery 51 | volumes: 52 | - ./backend/:/home/user/app/ 53 | env_file: backend/.env 54 | depends_on: 55 | - db 56 | - broker 57 | - result 58 | 59 | mailhog: # service for faking a SMTP server 60 | image: mailhog/mailhog 61 | ports: 62 | - '1025:1025' # smtp server 63 | - '8025:8025' # web ui 64 | 65 | volumes: 66 | dbdata: 67 | external: 68 | name: {{project_name}}_dbdata -------------------------------------------------------------------------------- /frontend/js/pages/Home.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import Button from 'react-bootstrap/Button'; 3 | import { useDispatch, useSelector } from 'react-redux'; 4 | 5 | import DjangoImgSrc from '../../assets/images/django-logo-negative.png'; 6 | import { creators } from '../store/rest_check'; 7 | 8 | const Home = () => { 9 | const dispatch = useDispatch(); 10 | const restCheck = useSelector((state) => state.restCheck); 11 | useEffect(() => { 12 | const action = creators.fetchRestCheck(); 13 | dispatch(action); 14 | }, [dispatch]); 15 | 16 | const [showBugComponent, setShowBugComponent] = useState(false); 17 | 18 | return ( 19 | <> 20 |
21 | If you are seeing the green Django logo on a white background and this text color is 22 | #092e20, frontend static files serving is working 23 |
24 |
25 |
26 | Below this text, you should see an img tag with the white Django logo on a green 27 | background 28 |
29 | Django Negative Logo 30 |
31 |
{restCheck.result}
32 | 35 | {showBugComponent && showBugComponent.field.notexist} 36 | 37 | ); 38 | }; 39 | 40 | export default Home; 41 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Pull requests 4 | 5 | Read [this checklist](http://pullrequests.devchecklists.com) for a more detailed guide on best practices for opening pull requests. 6 | 7 | ## Testing your changes 8 | 9 | ### Testing `django-admin startproject` 10 | 11 | If you made changes to this boilerplate and want to test them, do as follows: 12 | 13 | - [Make sure you have pre-commit installed](https://github.com/vintasoftware/django-react-boilerplate#pre-commit-hooks) 14 | - Commit your changes 15 | - Run `git archive -o boilerplate.zip HEAD` to create the template zip file 16 | - Run the following: 17 | ```bash 18 | cd .. && django-admin startproject theprojectname --extension py,yml,json --name Procfile,README.md,.env.example,Dockerfile --template=django-react-boilerplate/boilerplate.zip 19 | ``` 20 | - A new folder called `theprojectname` will be created and now you can test your changes 21 | - Make sure that the project is still running fine with and without docker 22 | 23 | ### Testing Heroku deployment 24 | 25 | Push your changes to a branch and visit the link below 26 | 27 | https://dashboard.heroku.com/new?template=https://github.com/fill-org-or-user/fill-project-repo-name/tree/fill-branch 28 | 29 | > Make sure to replace all `fill-*` 30 | 31 | ## How to add a "Deploy to Heroku" button 32 | 33 | Read [this](https://devcenter.heroku.com/articles/heroku-button#adding-the-heroku-button). 34 | 35 | P.S. if you want to deploy in a different way, please check the `app.json` file for what needs to be configured. 36 | 37 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | fail_fast: true 2 | repos: 3 | - repo: git://github.com/pre-commit/pre-commit-hooks 4 | rev: v0.9.2 5 | hooks: 6 | - id: check-added-large-files 7 | args: ['--maxkb=500'] 8 | - id: check-byte-order-marker 9 | - id: check-case-conflict 10 | - id: check-merge-conflict 11 | - id: check-symlinks 12 | - id: debug-statements 13 | - id: detect-private-key 14 | 15 | - repo: local 16 | hooks: 17 | - id: isort 18 | name: isort-local 19 | entry : isort 20 | language: python 21 | types: [python] 22 | exclude: .+/(settings|migrations)/.+ 23 | pass_filenames: true 24 | - id: black 25 | name: black-local 26 | entry: black 27 | language: python 28 | types: [python] 29 | exclude: .+/(settings|migrations)/.+ 30 | pass_filenames: true 31 | - id: eslint 32 | name: eslint-local 33 | entry: npm run lint 34 | language: system 35 | types: [javascript] 36 | exclude: > 37 | (?x)^( 38 | .+\.config\.js| 39 | server\.js| 40 | \.eslintrc\.js 41 | )$ 42 | pass_filenames: true 43 | - id: missing-migrations 44 | name: missing-migrations-local 45 | entry: python manage.py makemigrations --check --dry-run 46 | language: system 47 | always_run: true 48 | pass_filenames: false 49 | - id: prospector 50 | name: prospector-local 51 | entry: prospector --messages-only 52 | language: python 53 | types: [python] 54 | exclude: .+/(settings|migrations)/.+ 55 | pass_filenames: true 56 | -------------------------------------------------------------------------------- /frontend/js/utils/SentryBoundary.js: -------------------------------------------------------------------------------- 1 | import * as Sentry from '@sentry/browser'; 2 | import PropTypes from 'prop-types'; 3 | import React, { Component } from 'react'; 4 | 5 | const FallbackUI = ({ eventId }) => ( 6 | <> 7 |

Check if there is an error on your Sentry app

8 | 11 | 12 | ); 13 | 14 | FallbackUI.propTypes = { 15 | eventId: PropTypes.string, 16 | }; 17 | 18 | FallbackUI.defaultProps = { 19 | eventId: '', 20 | }; 21 | 22 | class ExampleBoundary extends Component { 23 | static getDerivedStateFromError() { 24 | return { hasError: true }; 25 | } 26 | 27 | constructor(props) { 28 | super(props); 29 | this.state = { eventId: null }; 30 | } 31 | 32 | componentDidCatch(error, errorInfo) { 33 | Sentry.withScope((scope) => { 34 | scope.setExtras(errorInfo); 35 | const eventId = Sentry.captureException(error); 36 | this.setState({ eventId }); 37 | }); 38 | } 39 | 40 | render() { 41 | const { eventId, hasError } = this.state; 42 | const { children } = this.props; 43 | 44 | // render fallback UI 45 | if (hasError) { 46 | return ; 47 | } 48 | 49 | // when there's not an error, render children untouched 50 | return children; 51 | } 52 | } 53 | 54 | ExampleBoundary.propTypes = { 55 | children: PropTypes.node, 56 | }; 57 | 58 | ExampleBoundary.defaultProps = { 59 | children: null, 60 | }; 61 | 62 | export default ExampleBoundary; 63 | -------------------------------------------------------------------------------- /frontend/sass/vendor/_bootstrap-includes.scss: -------------------------------------------------------------------------------- 1 | // Mixins 2 | @import "~bootstrap/scss/functions"; 3 | @import "~bootstrap/scss/variables"; 4 | @import "~bootstrap/scss/mixins"; 5 | 6 | /* Customizations */ 7 | @import "custom-bootstrap"; 8 | 9 | /* Reset and dependencies */ 10 | @import "~bootstrap/scss/print"; 11 | 12 | /* Core CSS */ 13 | @import "~bootstrap/scss/buttons"; 14 | @import "~bootstrap/scss/code"; 15 | @import "~bootstrap/scss/forms"; 16 | @import "~bootstrap/scss/grid"; 17 | @import "~bootstrap/scss/images"; 18 | @import "~bootstrap/scss/reboot"; 19 | @import "~bootstrap/scss/tables"; 20 | @import "~bootstrap/scss/type"; 21 | 22 | /* Components */ 23 | // @import "~bootstrap/scss/alert"; 24 | // @import "~bootstrap/scss/badge"; 25 | // @import "~bootstrap/scss/breadcrumb"; 26 | // @import "~bootstrap/scss/button-group"; 27 | @import "~bootstrap/scss/card"; 28 | // @import "~bootstrap/scss/close"; 29 | // @import "~bootstrap/scss/custom-forms"; 30 | // @import "~bootstrap/scss/dropdown"; 31 | // @import "~bootstrap/scss/input-group"; 32 | @import "~bootstrap/scss/jumbotron"; 33 | // @import "~bootstrap/scss/list-group"; 34 | // @import "~bootstrap/scss/media"; 35 | @import "~bootstrap/scss/nav"; 36 | @import "~bootstrap/scss/navbar"; 37 | // @import "~bootstrap/scss/pagination"; 38 | // @import "~bootstrap/scss/progress"; 39 | @import "~bootstrap/scss/transitions"; 40 | 41 | /* Components with JS */ 42 | // @import "~bootstrap/scss/carousel"; 43 | // @import "~bootstrap/scss/modal"; 44 | // @import "~bootstrap/scss/popover"; 45 | // @import "~bootstrap/scss/tooltip"; 46 | // @import "~bootstrap/scss/utilities"; 47 | -------------------------------------------------------------------------------- /frontend/js/app/example-app/components/ColorChanger/ColorChanger.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | 4 | import ColorDisplay from '../ColorDisplay'; 5 | 6 | import './style.scss'; 7 | 8 | class ColorChanger extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | 12 | this.state = { 13 | color: 'black', 14 | }; 15 | 16 | this.handleChangeColor = this.handleChangeColor.bind(this); 17 | } 18 | 19 | handleChangeColor(e) { 20 | const color = e.target.value; 21 | this.setState({ color }); 22 | } 23 | 24 | render() { 25 | const { color } = this.state; 26 | const { title } = this.props; 27 | 28 | return ( 29 |
30 |

{title}

31 |

Color Changer App

32 |

Check this example app: change the color to see it reflected in the text next to it.

33 | 34 |
35 | 42 | 43 | 44 |
45 |
46 | ); 47 | } 48 | } 49 | 50 | ColorChanger.defaultProps = { 51 | title: 'React App Loaded!', 52 | }; 53 | 54 | ColorChanger.propTypes = { 55 | title: PropTypes.string, 56 | }; 57 | 58 | export default ColorChanger; 59 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "{{project_name}}", 3 | "description": "{{project_name|title}} Heroku app.", 4 | "env": { 5 | "ALLOWED_HOSTS": { 6 | "description": "Django ALLOWED_HOSTS setting, e.g.: .appname.herokuapp.com" 7 | }, 8 | "DISABLE_COLLECTSTATIC": { 9 | "description": "Disables Heroku collectstatic", 10 | "value": "1" 11 | }, 12 | "ENABLE_DJANGO_COLLECTSTATIC": { 13 | "description": "Enables post-compile collectstatic (it is run by bin/post_compile)", 14 | "value": "1" 15 | }, 16 | "AUTO_MIGRATE": { 17 | "description": "Heroku setting to run Django migrate automatically (it is run by bin/post_compile)", 18 | "value": "1" 19 | }, 20 | "DJANGO_SETTINGS_MODULE": { 21 | "description": "Django settings Python import path", 22 | "value": "{{project_name}}.settings.production" 23 | }, 24 | "SECRET_KEY": { 25 | "description": "Django SECRET_KEY setting", 26 | "generator": "secret" 27 | } 28 | }, 29 | "formation": { 30 | "web": { 31 | "quantity": 1, 32 | "size": "free" 33 | }, 34 | "worker": { 35 | "quantity": 1, 36 | "size": "free" 37 | } 38 | }, 39 | "addons": [ 40 | { 41 | "plan": "heroku-postgresql:hobby-dev", 42 | "options": { 43 | "version": "13" 44 | }, 45 | "as": "DATABASE" 46 | }, 47 | { 48 | "plan": "heroku-redis:hobby-dev", 49 | "options": { 50 | "version": "6" 51 | }, 52 | "as": "REDIS" 53 | }, 54 | { 55 | "plan": "sendgrid:starter" 56 | }, 57 | { 58 | "plan": "papertrail:choklad" 59 | } 60 | ], 61 | "buildpacks": [ 62 | { 63 | "url": "heroku/nodejs" 64 | }, 65 | { 66 | "url": "heroku/python" 67 | } 68 | ] 69 | } 70 | -------------------------------------------------------------------------------- /webpack.prod.config.js: -------------------------------------------------------------------------------- 1 | const autoprefixer = require('autoprefixer'); 2 | const webpack = require('webpack'); 3 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 4 | const BundleTracker = require('webpack-bundle-tracker'); 5 | const path = require('path'); 6 | 7 | const baseConfig = require('./webpack.base.config'); 8 | 9 | const nodeModulesDir = path.resolve(__dirname, 'node_modules'); 10 | 11 | baseConfig.mode = 'production'; 12 | baseConfig.devtool = 'source-map'; 13 | 14 | baseConfig.entry = ['whatwg-fetch', '@babel/polyfill', './frontend/js/index.js']; 15 | 16 | baseConfig.output = { 17 | path: path.resolve('./frontend/webpack_bundles/'), 18 | publicPath: '/static/webpack_bundles/', 19 | filename: '[name]-[hash].js', 20 | }; 21 | 22 | baseConfig.module.rules.push( 23 | { 24 | test: /\.jsx?$/, 25 | exclude: [nodeModulesDir], 26 | use: { 27 | loader: 'babel-loader', 28 | options: { 29 | presets: ['@babel/preset-env', '@babel/preset-react'], 30 | }, 31 | }, 32 | }, 33 | { 34 | test: /\.(woff(2)?|eot|ttf)(\?v=\d+\.\d+\.\d+)?$/, 35 | loader: 'file-loader?name=fonts/[name].[ext]', 36 | } 37 | ); 38 | 39 | baseConfig.optimization = { 40 | minimize: true, 41 | splitChunks: { 42 | chunks: 'all', 43 | }, 44 | }; 45 | 46 | baseConfig.plugins = [ 47 | new webpack.DefinePlugin({ 48 | // removes React warnings 49 | 'process.env': { 50 | NODE_ENV: JSON.stringify('production'), 51 | }, 52 | }), 53 | new MiniCssExtractPlugin({ filename: '[name]-[hash].css', disable: false, allChunks: true }), 54 | new BundleTracker({ 55 | filename: './webpack-stats.json', 56 | }), 57 | new webpack.LoaderOptionsPlugin({ 58 | options: { 59 | context: __dirname, 60 | postcss: [autoprefixer], 61 | }, 62 | }), 63 | ]; 64 | 65 | module.exports = baseConfig; 66 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash # Use bash syntax 2 | ARG := $(word 2, $(MAKECMDGOALS) ) 3 | 4 | clean: 5 | @find . -name "*.pyc" -exec rm -rf {} \; 6 | @find . -name "__pycache__" -delete 7 | 8 | test: 9 | python backend/manage.py test backend/ $(ARG) --parallel --keepdb 10 | 11 | test_reset: 12 | python backend/manage.py test backend/ $(ARG) --parallel 13 | 14 | backend_format: 15 | black backend 16 | 17 | upgrade: ## update the *requirements.txt files with the latest packages satisfying *requirements.in 18 | pip install -U -q pip-tools 19 | pip-compile --upgrade -o dev-requirements.txt dev-requirements.in 20 | pip-compile --upgrade -o requirements.txt requirements.in 21 | # Make everything =>, not == 22 | sed 's/==/>=/g' requirements.txt > requirements.tmp 23 | mv requirements.tmp requirements.txt 24 | 25 | compile_install_requirements: 26 | @echo 'Installing pip-tools...' 27 | export PIP_REQUIRE_VIRTUALENV=true; \ 28 | pip install pip-tools 29 | @echo 'Compiling requirements...' 30 | pip-compile requirements.in > requirements.txt 31 | pip-compile dev-requirements.in > dev-requirements.txt 32 | @echo 'Installing requirements...' 33 | pip install -r requirements.txt && pip install -r dev-requirements.txt 34 | 35 | # Commands for Docker version 36 | docker_setup: 37 | docker volume create {{project_name}}_dbdata 38 | docker-compose build --no-cache backend 39 | docker-compose run frontend npm install 40 | 41 | docker_test: 42 | docker-compose run backend python manage.py test $(ARG) --parallel --keepdb 43 | 44 | docker_test_reset: 45 | docker-compose run backend python manage.py test $(ARG) --parallel 46 | 47 | docker_up: 48 | docker-compose up -d 49 | 50 | docker_update_dependencies: 51 | docker-compose down 52 | docker-compose up -d --build 53 | 54 | docker_down: 55 | docker-compose down 56 | 57 | docker_logs: 58 | docker-compose logs -f $(ARG) 59 | 60 | docker_makemigrations: 61 | docker-compose run --rm backend python manage.py makemigrations 62 | 63 | docker_migrate: 64 | docker-compose run --rm backend python manage.py migrate 65 | -------------------------------------------------------------------------------- /webpack.local.config.js: -------------------------------------------------------------------------------- 1 | const autoprefixer = require('autoprefixer'); 2 | const webpack = require('webpack'); 3 | const BundleTracker = require('webpack-bundle-tracker'); 4 | const CircularDependencyPlugin = require('circular-dependency-plugin'); 5 | const path = require('path'); 6 | 7 | const baseConfig = require('./webpack.base.config'); 8 | 9 | const nodeModulesDir = path.resolve(__dirname, 'node_modules'); 10 | 11 | baseConfig.mode = 'development'; 12 | 13 | baseConfig.entry = [ 14 | 'react-hot-loader/patch', 15 | 'whatwg-fetch', 16 | '@babel/polyfill', 17 | './frontend/js/index.js', 18 | ]; 19 | 20 | baseConfig.optimization = { 21 | splitChunks: { 22 | chunks: 'all', 23 | }, 24 | }; 25 | 26 | baseConfig.output = { 27 | path: path.resolve('./frontend/bundles/'), 28 | publicPath: 'http://localhost:3000/frontend/bundles/', 29 | filename: '[name].js', 30 | }; 31 | 32 | baseConfig.module.rules.push( 33 | { 34 | test: /\.jsx?$/, 35 | exclude: [nodeModulesDir], 36 | loader: require.resolve('babel-loader'), 37 | }, 38 | { 39 | test: /\.(woff(2)?|eot|ttf)(\?v=\d+\.\d+\.\d+)?$/, 40 | loader: 'url-loader?limit=100000', 41 | } 42 | ); 43 | 44 | baseConfig.plugins = [ 45 | new webpack.EvalSourceMapDevToolPlugin({ 46 | exclude: /node_modules/ 47 | }), 48 | new webpack.NamedModulesPlugin(), 49 | new webpack.NoEmitOnErrorsPlugin(), // don't reload if there is an error 50 | new BundleTracker({ 51 | filename: './webpack-stats.json', 52 | }), 53 | new webpack.LoaderOptionsPlugin({ 54 | options: { 55 | context: __dirname, 56 | postcss: [autoprefixer], 57 | }, 58 | }), 59 | new CircularDependencyPlugin({ 60 | // exclude detection of files based on a RegExp 61 | exclude: /a\.js|node_modules/, 62 | // add errors to webpack instead of warnings 63 | failOnError: true, 64 | // set the current working directory for displaying module paths 65 | cwd: process.cwd(), 66 | }), 67 | ]; 68 | 69 | baseConfig.resolve.alias = { 70 | 'react-dom': '@hot-loader/react-dom', 71 | }; 72 | 73 | module.exports = baseConfig; 74 | -------------------------------------------------------------------------------- /frontend/js/app/example-app/components/ColorChanger/__tests__/__snapshots__/ColorChanger.snaps.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ColorChanger no title (should use default) 1`] = ` 4 |
7 |

8 | React App Loaded! 9 |

10 |

13 | Color Changer App 14 |

15 |

16 | Check this example app: change the color to see it reflected in the text next to it. 17 |

18 |
21 | 51 | 54 |
55 |
56 | `; 57 | 58 | exports[`ColorChanger some title 1`] = ` 59 |
62 |

63 | This is a test title 64 |

65 |

68 | Color Changer App 69 |

70 |

71 | Check this example app: change the color to see it reflected in the text next to it. 72 |

73 |
76 | 106 | 109 |
110 |
111 | `; 112 | -------------------------------------------------------------------------------- /backend/common/utils/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import Client, TestCase 2 | from django.urls import reverse 3 | 4 | from model_bakery import baker 5 | 6 | 7 | class TestCaseUtils(TestCase): 8 | def setUp(self): 9 | self._user_password = "123456" 10 | self.user = baker.prepare("users.User", email="user@email.com") 11 | self.user.set_password(self._user_password) 12 | self.user.save() 13 | 14 | self.auth_client = Client() 15 | self.auth_client.login(email=self.user.email, password=self._user_password) 16 | 17 | def reverse(self, name, *args, **kwargs): 18 | """ Reverse a url, convenience to avoid having to import reverse in tests """ 19 | return reverse(name, args=args, kwargs=kwargs) 20 | 21 | def assertResponse200(self, response): 22 | """ Given response has status_code 200 OK""" 23 | self.assertEqual(response.status_code, 200) 24 | 25 | def assertResponse201(self, response): 26 | """ Given response has status_code 201 CREATED""" 27 | self.assertEqual(response.status_code, 201) 28 | 29 | def assertResponse301(self, response): 30 | """ Given response has status_code 301 MOVED PERMANENTLY""" 31 | self.assertEqual(response.status_code, 301) 32 | 33 | def assertResponse302(self, response): 34 | """ Given response has status_code 302 FOUND""" 35 | self.assertEqual(response.status_code, 302) 36 | 37 | def assertResponse400(self, response): 38 | """ Given response has status_code 400 BAD REQUEST""" 39 | self.assertEqual(response.status_code, 400) 40 | 41 | def assertResponse401(self, response): 42 | """ Given response has status_code 401 UNAUTHORIZED""" 43 | self.assertEqual(response.status_code, 401) 44 | 45 | def assertResponse403(self, response): 46 | """ Given response has status_code 403 FORBIDDEN""" 47 | self.assertEqual(response.status_code, 403) 48 | 49 | def assertResponse404(self, response): 50 | """ Given response has status_code 404 NOT FOUND""" 51 | self.assertEqual(response.status_code, 404) 52 | 53 | 54 | class TestGetRequiresAuthenticatedUser: 55 | def test_get_requires_authenticated_user(self): 56 | response = self.client.get(self.view_url) 57 | self.assertResponse403(response) 58 | 59 | 60 | class TestAuthGetRequestSuccess: 61 | def test_auth_get_success(self): 62 | response = self.auth_client.get(self.view_url) 63 | self.assertResponse200(response) 64 | -------------------------------------------------------------------------------- /.bootstraprc: -------------------------------------------------------------------------------- 1 | --- 2 | # Output debugging info 3 | # loglevel: debug 4 | 5 | # Major version of Bootstrap: 3 or 4 6 | bootstrapVersion: 4 7 | 8 | # Webpack loaders, order matters 9 | styleLoaders: 10 | - style-loader 11 | - css-loader 12 | - sass-loader 13 | - postcss-loader 14 | 15 | # Extract styles to stand-alone css file 16 | # Different settings for different environments can be used, 17 | # It depends on value of NODE_ENV environment variable 18 | # This param can also be set in webpack config: 19 | # entry: 'bootstrap-loader/extractStyles' 20 | # env: 21 | # development: 22 | # extractStyles: false 23 | # production: 24 | # extractStyles: true 25 | 26 | 27 | # Customize Bootstrap variables that get imported before the original Bootstrap variables. 28 | # Thus, derived Bootstrap variables can depend on values from here. 29 | # See the Bootstrap _variables.scss file for examples of derived Bootstrap variables. 30 | # 31 | # preBootstrapCustomizations: ./path/to/bootstrap/pre-customizations.scss 32 | 33 | 34 | # This gets loaded after bootstrap/variables is loaded 35 | # Thus, you may customize Bootstrap variables 36 | # based on the values established in the Bootstrap _variables.scss file 37 | # 38 | bootstrapCustomizations: ./frontend/sass/vendor/custom-bootstrap.scss 39 | 40 | 41 | # Import your custom styles here 42 | # Usually this endpoint-file contains list of @imports of your application styles 43 | # 44 | # appStyles: ./path/to/your/app/styles/endpoint.scss 45 | appStyles: ./frontend/sass/style.scss 46 | 47 | 48 | ### Bootstrap styles 49 | styles: 50 | 51 | # Mixins 52 | mixins: true 53 | 54 | # Reset and dependencies 55 | print: true 56 | 57 | # Core CSS 58 | buttons: true 59 | code: true 60 | forms: true 61 | grid: true 62 | images: true 63 | reboot: true 64 | tables: true 65 | type: true 66 | 67 | # Components 68 | alert: false 69 | badge: false 70 | breadcrumb: false 71 | button-group: false 72 | card: true 73 | close: false 74 | custom-forms: false 75 | dropdown: false 76 | input-group: false 77 | jumbotron: true 78 | list-group: false 79 | media: false 80 | nav: true 81 | navbar: true 82 | pagination: false 83 | progress: false 84 | transitions: true 85 | 86 | # Components w/ JavaScript 87 | carousel: false 88 | modal: false 89 | popover: false 90 | tooltip: false 91 | 92 | # Utility classes 93 | utilities: true 94 | 95 | ### Bootstrap scripts 96 | scripts: 97 | alert: false 98 | button: true 99 | carousel: false 100 | collapse: true 101 | dropdown: false 102 | modal: false 103 | popover: false 104 | scrollspy: false 105 | tab: false 106 | tooltip: false 107 | util: true 108 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "{{project_name}}-frontend", 3 | "version": "0.0.1", 4 | "private": true, 5 | "description": "{{project_name}} frontend.", 6 | "main": "index.js", 7 | "prettier": "eslint-config-vinta/prettier", 8 | "scripts": { 9 | "test": "jest", 10 | "test:watch": "npm test -- --watch", 11 | "test:update": "npm test -- --u", 12 | "start": "babel-node server.js", 13 | "build": "NODE_ENV=production webpack -p --progress --colors --config webpack.prod.config.js --bail", 14 | "lint": "eslint frontend", 15 | "coverage": "jest --coverage" 16 | }, 17 | "dependencies": { 18 | "@babel/cli": "~7.13.10", 19 | "@babel/polyfill": "~7.8.3", 20 | "@babel/preset-env": "~7.13.10", 21 | "@babel/preset-react": "~7.12.13", 22 | "@sentry/browser": "~6.2.2", 23 | "@types/react": "~16.14.5", 24 | "autoprefixer": "~9.8.6", 25 | "axios": "~0.21.1", 26 | "babel-loader": "~8.2.2", 27 | "bootstrap": "~4.6.0", 28 | "classnames": "~2.2.6", 29 | "connected-react-router": "~6.9.1", 30 | "cookie": "~0.4.0", 31 | "css-loader": "~5.1.3", 32 | "expose-loader": "~0.7.5", 33 | "file-loader": "~6.2.0", 34 | "history": "~4.10.1", 35 | "imports-loader": "~0.8.0", 36 | "lodash": "~4.17.21", 37 | "marked": "~2.0.1", 38 | "node-sass": "~4.14.1", 39 | "postcss": "~8.2.8", 40 | "postcss-loader": "~2.1.6", 41 | "prop-types": "~15.7.2", 42 | "react": "~16.14.0", 43 | "react-bootstrap": "~1.5.2", 44 | "react-dom": "~16.14.0", 45 | "react-hot-loader": "~4.13.0", 46 | "react-redux": "~7.2.3", 47 | "react-router": "~5.2.0", 48 | "redux": "~4.0.5", 49 | "redux-devtools-extension": "~2.13.9", 50 | "redux-thunk": "~2.3.0", 51 | "resolve-url-loader": "~3.1.2", 52 | "sass-loader": "~8.0.2", 53 | "script-loader": "~0.7.2", 54 | "style-loader": "~2.0.0", 55 | "url-loader": "~4.1.1", 56 | "webpack": "~4.46.0", 57 | "webpack-bundle-tracker": "~1.0.0", 58 | "webpack-dev-server": "~3.11.2", 59 | "whatwg-fetch": "~3.6.2" 60 | }, 61 | "devDependencies": { 62 | "@babel/core": "~7.13.10", 63 | "@babel/node": "~7.13.10", 64 | "@hot-loader/react-dom": "~16.14.0", 65 | "ajv": "~6.12.6", 66 | "babel-core": "~7.0.0-bridge.0", 67 | "babel-eslint": "~10.1.0", 68 | "babel-jest": "~26.6.3", 69 | "circular-dependency-plugin": "~5.2.2", 70 | "enzyme": "~3.11.0", 71 | "enzyme-adapter-react-16": "~1.15.6", 72 | "enzyme-to-json": "~3.6.1", 73 | "eslint": "~6.8.0", 74 | "eslint-config-prettier": "~6.15.0", 75 | "eslint-config-vinta": "~1.4.5", 76 | "eslint-import-resolver-webpack": "~0.13.0", 77 | "eslint-plugin-babel": "~5.3.1", 78 | "eslint-plugin-import": "~2.22.1", 79 | "eslint-plugin-jest": "~22.21.0", 80 | "eslint-plugin-jsx-a11y": "~6.4.1", 81 | "eslint-plugin-prettier": "~3.3.1", 82 | "eslint-plugin-promise": "~4.3.1", 83 | "eslint-plugin-react": "~7.22.0", 84 | "eslint-plugin-react-hooks": "~2.5.1", 85 | "eslint-plugin-sonarjs": "~0.5.0", 86 | "eslint-plugin-unicorn": "~15.0.1", 87 | "identity-obj-proxy": "~3.0.0", 88 | "jest": "~26.6.3", 89 | "jest-css-modules": "~2.1.0", 90 | "mini-css-extract-plugin": "~0.12.0", 91 | "prettier": "~1.19.1", 92 | "redux-mock-store": "~1.5.4", 93 | "webpack-cli": "~3.3.12" 94 | }, 95 | "resolutions": { 96 | "babel-core": "7.0.0-bridge.0" 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /backend/project_name/settings/production.py: -------------------------------------------------------------------------------- 1 | import sentry_sdk 2 | from decouple import Csv, config 3 | from sentry_sdk.integrations.django import DjangoIntegration 4 | 5 | from .base import * # noqa 6 | 7 | 8 | DEBUG = False 9 | 10 | SECRET_KEY = config("SECRET_KEY") 11 | 12 | DATABASES["default"]["ATOMIC_REQUESTS"] = True 13 | 14 | ALLOWED_HOSTS = config("ALLOWED_HOSTS", cast=Csv()) 15 | 16 | STATIC_ROOT = base_dir_join("staticfiles") 17 | STATIC_URL = "/static/" 18 | 19 | MEDIA_ROOT = base_dir_join("mediafiles") 20 | MEDIA_URL = "/media/" 21 | 22 | SERVER_EMAIL = "foo@example.com" 23 | 24 | EMAIL_HOST = "smtp.sendgrid.net" 25 | EMAIL_HOST_USER = config("SENDGRID_USERNAME") 26 | EMAIL_HOST_PASSWORD = config("SENDGRID_PASSWORD") 27 | EMAIL_PORT = 587 28 | EMAIL_USE_TLS = True 29 | 30 | # Security 31 | SECURE_HSTS_PRELOAD = True 32 | SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") 33 | SECURE_SSL_REDIRECT = True 34 | SESSION_COOKIE_SECURE = True 35 | CSRF_COOKIE_SECURE = True 36 | SECURE_HSTS_SECONDS = config("SECURE_HSTS_SECONDS", default=3600, cast=int) 37 | SECURE_HSTS_INCLUDE_SUBDOMAINS = True 38 | 39 | SECURE_CONTENT_TYPE_NOSNIFF = True 40 | SECURE_BROWSER_XSS_FILTER = True 41 | X_FRAME_OPTIONS = "DENY" 42 | 43 | # Webpack 44 | WEBPACK_LOADER["DEFAULT"]["CACHE"] = True 45 | 46 | # Celery 47 | CELERY_BROKER_URL = config("REDIS_URL") 48 | CELERY_RESULT_BACKEND = config("REDIS_URL") 49 | CELERY_SEND_TASK_ERROR_EMAILS = True 50 | 51 | # Redbeat https://redbeat.readthedocs.io/en/latest/config.html#redbeat-redis-url 52 | redbeat_redis_url = config("REDBEAT_REDIS_URL", default="") 53 | 54 | # Whitenoise 55 | STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage" 56 | 57 | # django-log-request-id 58 | MIDDLEWARE.insert( # insert RequestIDMiddleware on the top 59 | 0, "log_request_id.middleware.RequestIDMiddleware" 60 | ) 61 | 62 | LOG_REQUEST_ID_HEADER = "HTTP_X_REQUEST_ID" 63 | LOG_REQUESTS = True 64 | 65 | LOGGING = { 66 | "version": 1, 67 | "disable_existing_loggers": False, 68 | "filters": { 69 | "require_debug_false": {"()": "django.utils.log.RequireDebugFalse"}, 70 | "request_id": {"()": "log_request_id.filters.RequestIDFilter"}, 71 | }, 72 | "formatters": { 73 | "standard": { 74 | "format": "%(levelname)-8s [%(asctime)s] [%(request_id)s] %(name)s: %(message)s" 75 | }, 76 | }, 77 | "handlers": { 78 | "null": {"class": "logging.NullHandler",}, 79 | "mail_admins": { 80 | "level": "ERROR", 81 | "class": "django.utils.log.AdminEmailHandler", 82 | "filters": ["require_debug_false"], 83 | }, 84 | "console": { 85 | "level": "DEBUG", 86 | "class": "logging.StreamHandler", 87 | "filters": ["request_id"], 88 | "formatter": "standard", 89 | }, 90 | }, 91 | "loggers": { 92 | "": {"handlers": ["console"], "level": "INFO"}, 93 | "django.security.DisallowedHost": {"handlers": ["null"], "propagate": False,}, 94 | "django.request": {"handlers": ["mail_admins"], "level": "ERROR", "propagate": True,}, 95 | "log_request_id.middleware": { 96 | "handlers": ["console"], 97 | "level": "DEBUG", 98 | "propagate": False, 99 | }, 100 | }, 101 | } 102 | 103 | JS_REVERSE_EXCLUDE_NAMESPACES = ["admin"] 104 | 105 | # Sentry 106 | sentry_sdk.init(dsn=SENTRY_DSN, integrations=[DjangoIntegration()], release=COMMIT_SHA) 107 | -------------------------------------------------------------------------------- /backend/project_name/settings/base.py: -------------------------------------------------------------------------------- 1 | # https://docs.djangoproject.com/en/1.10/ref/settings/ 2 | 3 | import os 4 | 5 | from decouple import config # noqa 6 | from dj_database_url import parse as db_url # noqa 7 | 8 | 9 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 10 | 11 | 12 | def base_dir_join(*args): 13 | return os.path.join(BASE_DIR, *args) 14 | 15 | 16 | SITE_ID = 1 17 | 18 | DEBUG = True 19 | 20 | ADMINS = (("Admin", "foo@example.com"),) 21 | 22 | AUTH_USER_MODEL = "users.User" 23 | 24 | ALLOWED_HOSTS = [] 25 | 26 | DATABASES = { 27 | "default": config("DATABASE_URL", cast=db_url), 28 | } 29 | 30 | INSTALLED_APPS = [ 31 | "django.contrib.admin", 32 | "django.contrib.auth", 33 | "django.contrib.contenttypes", 34 | "django.contrib.sessions", 35 | "django.contrib.messages", 36 | "django.contrib.staticfiles", 37 | "django_js_reverse", 38 | "webpack_loader", 39 | "import_export", 40 | "rest_framework", 41 | "common", 42 | "users", 43 | ] 44 | 45 | MIDDLEWARE = [ 46 | "debreach.middleware.RandomCommentMiddleware", 47 | "django.middleware.security.SecurityMiddleware", 48 | "whitenoise.middleware.WhiteNoiseMiddleware", 49 | "django.contrib.sessions.middleware.SessionMiddleware", 50 | "django.middleware.common.CommonMiddleware", 51 | "django.middleware.csrf.CsrfViewMiddleware", 52 | "django.contrib.auth.middleware.AuthenticationMiddleware", 53 | "django.contrib.messages.middleware.MessageMiddleware", 54 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 55 | ] 56 | 57 | ROOT_URLCONF = "{{project_name}}.urls" 58 | 59 | TEMPLATES = [ 60 | { 61 | "BACKEND": "django.template.backends.django.DjangoTemplates", 62 | "DIRS": [base_dir_join("templates")], 63 | "OPTIONS": { 64 | "context_processors": [ 65 | "django.template.context_processors.debug", 66 | "django.template.context_processors.request", 67 | "django.contrib.auth.context_processors.auth", 68 | "django.contrib.messages.context_processors.messages", 69 | "common.context_processors.sentry_dsn", 70 | "common.context_processors.commit_sha", 71 | ], 72 | "loaders": [ 73 | ( 74 | "django.template.loaders.cached.Loader", 75 | [ 76 | "django.template.loaders.filesystem.Loader", 77 | "django.template.loaders.app_directories.Loader", 78 | ], 79 | ), 80 | ], 81 | }, 82 | }, 83 | ] 84 | 85 | WSGI_APPLICATION = "{{project_name}}.wsgi.application" 86 | 87 | AUTH_PASSWORD_VALIDATORS = [ 88 | {"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",}, 89 | {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",}, 90 | {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",}, 91 | {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",}, 92 | ] 93 | 94 | REST_FRAMEWORK = { 95 | 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', 96 | 'PAGE_SIZE': 10, 97 | 'DEFAULT_AUTHENTICATION_CLASSES': [ 98 | 'rest_framework.authentication.SessionAuthentication', 99 | ], 100 | 'DEFAULT_PERMISSION_CLASSES': [ 101 | 'rest_framework.permissions.IsAuthenticated', 102 | ], 103 | } 104 | 105 | LANGUAGE_CODE = "en-us" 106 | 107 | TIME_ZONE = "UTC" 108 | 109 | USE_I18N = True 110 | 111 | USE_L10N = True 112 | 113 | USE_TZ = True 114 | 115 | STATICFILES_DIRS = (base_dir_join("../frontend"),) 116 | 117 | # Webpack 118 | WEBPACK_LOADER = { 119 | "DEFAULT": { 120 | "CACHE": False, # on DEBUG should be False 121 | "STATS_FILE": base_dir_join("../webpack-stats.json"), 122 | "POLL_INTERVAL": 0.1, 123 | "IGNORE": [".+\.hot-update.js", ".+\.map"], 124 | } 125 | } 126 | 127 | # Celery 128 | CELERY_ACCEPT_CONTENT = ["json"] 129 | CELERY_TASK_SERIALIZER = "json" 130 | CELERY_RESULT_SERIALIZER = "json" 131 | CELERY_ACKS_LATE = True 132 | CELERY_TIMEZONE = TIME_ZONE 133 | 134 | # Sentry 135 | SENTRY_DSN = config("SENTRY_DSN", default="") 136 | COMMIT_SHA = config("HEROKU_SLUG_COMMIT", default="") 137 | 138 | # Fix for Safari 12 compatibility issues, please check: 139 | # https://github.com/vintasoftware/safari-samesite-cookie-issue 140 | CSRF_COOKIE_SAMESITE = None 141 | SESSION_COOKIE_SAMESITE = None 142 | 143 | # Default primary key field type 144 | # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field 145 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" 146 | -------------------------------------------------------------------------------- /.github/workflows/shared-build/action.yml: -------------------------------------------------------------------------------- 1 | name: "Shared Build Steps" 2 | description: "Shared build steps for main and nightly" 3 | 4 | runs: 5 | using: "composite" 6 | steps: 7 | - name: Store branch and latest SHA 8 | id: vars 9 | shell: bash 10 | run: | 11 | echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})" 12 | echo "::set-output name=sha_short::$(git rev-parse --short HEAD)" 13 | - name: Setup Python 14 | uses: actions/setup-python@v2 15 | with: 16 | python-version: "3.8" 17 | - name: Setup Node 18 | uses: actions/setup-node@v2 19 | with: 20 | node-version: "14.5" 21 | - name: Cache node modules 22 | uses: actions/cache@v2 23 | env: 24 | cache-name: node-modules-cache 25 | with: 26 | path: ~/.npm 27 | key: build-${{ env.cache-name }}-${{ steps.vars.outputs.branch }}-${{ steps.vars.outputs.sha_short }} 28 | restore-keys: | 29 | build-${{ env.cache-name }}-${{ steps.vars.outputs.branch }}-${{ steps.vars.outputs.sha_short }} 30 | build-${{ env.cache-name }}-${{ steps.vars.outputs.branch }} 31 | build-${{ env.cache-name }} 32 | - name: Cache pip 33 | uses: actions/cache@v2 34 | env: 35 | cache-name: pip-cache 36 | with: 37 | path: ~/.cache/pip 38 | key: build-${{ env.cache-name }}-${{ steps.vars.outputs.branch }}-${{ steps.vars.outputs.sha_short }} 39 | restore-keys: | 40 | build-${{ env.cache-name }}-${{ steps.vars.outputs.branch }}-${{ steps.vars.outputs.sha_short }} 41 | build-${{ env.cache-name }}-${{ steps.vars.outputs.branch }} 42 | build-${{ env.cache-name }} 43 | - run: python -m pip install --upgrade pip 44 | shell: bash 45 | - name: Install Django 46 | run: pip install "django>=3,<4" 47 | shell: bash 48 | - name: Setup testproject 49 | run: django-admin startproject testproject --extension py,yml,json,example --name Procfile,README.md --template=. 50 | shell: bash 51 | - run: | 52 | npm update --save 53 | npm update --save-dev 54 | shell: bash 55 | working-directory: testproject 56 | - run: npm install --no-optional 57 | working-directory: testproject 58 | shell: bash 59 | - run: npm dedupe 60 | working-directory: testproject 61 | shell: bash 62 | - run: npm run test 63 | working-directory: testproject 64 | shell: bash 65 | - run: npm run lint 66 | working-directory: testproject 67 | shell: bash 68 | - run: npm run build 69 | working-directory: testproject 70 | shell: bash 71 | - run: pip install requests pip-tools --upgrade 72 | working-directory: testproject 73 | shell: bash 74 | - run: | 75 | pip-compile requirements.in > requirements.txt 76 | pip-compile dev-requirements.in > dev-requirements.txt 77 | working-directory: testproject 78 | shell: bash 79 | - run: | 80 | pip install --user -r requirements.txt 81 | pip install --user -r dev-requirements.txt 82 | working-directory: testproject 83 | shell: bash 84 | - run: cp testproject/settings/local.py.example testproject/settings/local.py 85 | working-directory: testproject/backend 86 | shell: bash 87 | - run: cp .env.example .env 88 | working-directory: testproject/backend 89 | shell: bash 90 | - run: python manage.py makemigrations 91 | working-directory: testproject/backend 92 | env: 93 | DATABASE_URL: 'sqlite:///' 94 | shell: bash 95 | - run: python manage.py migrate 96 | working-directory: testproject/backend 97 | env: 98 | DATABASE_URL: 'sqlite:///' 99 | shell: bash 100 | - run: python manage.py test 101 | working-directory: testproject/backend 102 | env: 103 | DATABASE_URL: 'sqlite:///' 104 | shell: bash 105 | - name: Generate secret key 106 | run: echo '::set-output name=SECRET_KEY::`python -c "import uuid; print(uuid.uuid4().hex + uuid.uuid4().hex)"`' 107 | id: secret-id-generator 108 | shell: bash 109 | - run: prospector -X 110 | working-directory: testproject/backend 111 | env: 112 | SECRET_KEY: ${{ steps.secret-id-generator.outputs.SECRET_KEY }} 113 | SENDGRID_USERNAME: foo 114 | SENDGRID_PASSWORD: password 115 | DJANGO_SETTINGS_MODULE: 'testproject.settings.local' 116 | ALLOWED_HOSTS: '.example.org' 117 | REDIS_URL: 'redis://' 118 | DATABASE_URL: 'sqlite:///' 119 | shell: bash 120 | - run: python manage.py makemigrations --check --dry-run 121 | working-directory: testproject/backend 122 | env: 123 | DATABASE_URL: 'sqlite:///' 124 | shell: bash 125 | - run: python manage.py check --deploy --fail-level WARNING 126 | working-directory: testproject/backend 127 | env: 128 | SECRET_KEY: ${{ steps.secret-id-generator.outputs.SECRET_KEY }} 129 | SENDGRID_USERNAME: foo 130 | SENDGRID_PASSWORD: password 131 | DJANGO_SETTINGS_MODULE: 'testproject.settings.production' 132 | ALLOWED_HOSTS: '.example.org' 133 | REDIS_URL: 'redis://' 134 | DATABASE_URL: 'sqlite:///' 135 | shell: bash 136 | -------------------------------------------------------------------------------- /proj_main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | build: 6 | name: Build {{project_name}} 7 | strategy: 8 | matrix: 9 | python: [3.8] 10 | node: [14.5] 11 | env: 12 | DATABASE_URL: 'sqlite:///' 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v2 17 | - name: Store branch and latest SHA 18 | run: | 19 | echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})" 20 | echo "::set-output name=sha_short::$(git rev-parse --short HEAD)" 21 | id: git 22 | - name: Setup Python ${% templatetag openvariable %} matrix.python {% templatetag closevariable %} 23 | uses: actions/setup-python@v2 24 | with: 25 | python-version: ${% templatetag openvariable %} matrix.python {% templatetag closevariable %} 26 | - name: Setup Node ${% templatetag openvariable %} matrix.node {% templatetag closevariable %} 27 | uses: actions/setup-node@v2 28 | with: 29 | node-version: ${% templatetag openvariable %} matrix.node {% templatetag closevariable %} 30 | - name: Cache node modules 31 | uses: actions/cache@v2 32 | env: 33 | cache_name: node-modules-cache 34 | with: 35 | path: ~/.npm 36 | key: build-${% templatetag openvariable%} env.cache_name {% templatetag closevariable %}-${% templatetag openvariable%} steps.git.outputs.branch {% templatetag closevariable %}-${% templatetag openvariable%} steps.git.outputs.sha_short {% templatetag closevariable %} 37 | restore-keys: | 38 | build-${% templatetag openvariable%} env.cache_name {% templatetag closevariable %}-${% templatetag openvariable%} steps.git.outputs.branch {% templatetag closevariable %}-${% templatetag openvariable%} steps.git.outputs.sha_short {% templatetag closevariable %} 39 | build-${% templatetag openvariable%} env.cache_name {% templatetag closevariable %}-${% templatetag openvariable%} steps.git.outputs.branch {% templatetag closevariable %} 40 | build-${% templatetag openvariable%} env.cache_name {% templatetag closevariable %} 41 | - name: Cache pip 42 | uses: actions/cache@v2 43 | env: 44 | cache_name: pip-cache 45 | with: 46 | path: ~/.cache/pip 47 | key: build-${% templatetag openvariable%} env.cache_name {% templatetag closevariable %}-${% templatetag openvariable%} steps.git.outputs.branch {% templatetag closevariable %}-${% templatetag openvariable%} steps.git.outputs.sha_short {% templatetag closevariable %} 48 | restore-keys: | 49 | build-${% templatetag openvariable%} env.cache_name {% templatetag closevariable %}-${% templatetag openvariable%} steps.git.outputs.branch {% templatetag closevariable %}-${% templatetag openvariable%} steps.git.outputs.sha_short {% templatetag closevariable %} 50 | build-${% templatetag openvariable%} env.cache_name {% templatetag closevariable %}-${% templatetag openvariable%} steps.git.outputs.branch {% templatetag closevariable %} 51 | build-${% templatetag openvariable%} env.cache_name {% templatetag closevariable %} 52 | - run: python -m pip install --upgrade pip 53 | - run: curl -s https://packagecloud.io/install/repositories/github/git-lfs/script.deb.sh | sudo bash 54 | - run: sudo apt-get install git-lfs --upgrade 55 | - run: pip install requests pip-tools --upgrade 56 | - run: | 57 | pip-compile requirements.in > requirements.txt 58 | pip-compile dev-requirements.in > dev-requirements.txt 59 | pip install --user -r requirements.txt 60 | pip install --user -r dev-requirements.txt 61 | - run: npm install 62 | - run: npm run build 63 | - run: npm run lint 64 | - name: Generate secret key 65 | run: echo '::set-output name=SECRET_KEY::`python -c "import uuid; print(uuid.uuid4().hex + uuid.uuid4().hex)"`' 66 | id: secret_id_generator 67 | - name: Style check 68 | run: prospector --messages-only 69 | env: 70 | DJANGO_SETTINGS_MODULE: '{{project_name}}.settings.local_base' 71 | SECRET_KEY: ${% templatetag openvariable %} steps.secret_id_generator.outputs.SECRET_KEY {% templatetag closevariable %} 72 | DATABASE_URL: 'sqlite:///' 73 | ALLOWED_HOSTS: '.example.org' 74 | SENDGRID_USERNAME: 'test' 75 | SENDGRID_PASSWORD: 'test' 76 | REDIS_URL: 'redis://' 77 | - name: Security checks 78 | run: bandit -r . 79 | - name: Imports check 80 | run: isort **/*.py --check-only 81 | - run: pre-commit run --all-files 82 | env: 83 | SKIP: prospector,isort,eslint,missing-migrations 84 | - run: python manage.py makemigrations --check --dry-run --ignore authtools 85 | working-directory: backend 86 | - run: python manage.py check --deploy 87 | env: 88 | DJANGO_SETTINGS_MODULE: '{{project_name}}.settings.production' 89 | SECRET_KEY: ${% templatetag openvariable %} steps.secret_id_generator.outputs.SECRET_KEY {% templatetag closevariable %} 90 | DATABASE_URL: 'sqlite:///' 91 | ALLOWED_HOSTS: '.example.org' 92 | SENDGRID_USERNAME: 'test' 93 | SENDGRID_PASSWORD: 'test' 94 | REDIS_URL: 'redis://' 95 | working-directory: backend 96 | - run: python manage.py test 97 | working-directory: backend 98 | - run: npm run test 99 | - run: | 100 | mkdir -p junit 101 | coverage xml -o junit/test-results.xml 102 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. 7 | 8 | We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. 9 | 10 | ## Our Standards 11 | 12 | Examples of behavior that contributes to a positive environment for our community include: 13 | 14 | * Demonstrating empathy and kindness toward other people 15 | * Being respectful of differing opinions, viewpoints, and experiences 16 | * Giving and gracefully accepting constructive feedback 17 | * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience 18 | * Focusing on what is best not just for us as individuals, but for the overall community 19 | 20 | Examples of unacceptable behavior include: 21 | 22 | * The use of sexualized language or imagery, and sexual attention or advances of any kind 23 | * Trolling, insulting or derogatory comments, and personal or political attacks 24 | * Public or private harassment 25 | * Publishing others' private information, such as a physical or email address, without their explicit permission 26 | * Other conduct which could reasonably be considered inappropriate in a professional setting 27 | 28 | ## Enforcement Responsibilities 29 | 30 | Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. 31 | 32 | Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. 33 | 34 | ## Scope 35 | 36 | This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. 37 | 38 | ## Enforcement 39 | 40 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [flavio@vinta.com.br](flavio@vinta.com.br). All complaints will be reviewed and investigated promptly and fairly. 41 | 42 | All community leaders are obligated to respect the privacy and security of the reporter of any incident. 43 | 44 | ## Enforcement Guidelines 45 | 46 | Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: 47 | 48 | ### 1. Correction 49 | 50 | **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. 51 | 52 | **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. 53 | 54 | ### 2. Warning 55 | 56 | **Community Impact**: A violation through a single incident or series of actions. 57 | 58 | **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. 59 | 60 | ### 3. Temporary Ban 61 | 62 | **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. 63 | 64 | **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. 65 | 66 | ### 4. Permanent Ban 67 | 68 | **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. 69 | 70 | **Consequence**: A permanent ban from any sort of public interaction within the community. 71 | 72 | ## Attribution 73 | 74 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. 75 | 76 | Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 77 | 78 | For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at [https://www.contributor-covenant.org/translations][translations]. 79 | 80 | [homepage]: https://www.contributor-covenant.org 81 | [v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html 82 | [Mozilla CoC]: https://github.com/mozilla/diversity 83 | [FAQ]: https://www.contributor-covenant.org/faq 84 | [translations]: https://www.contributor-covenant.org/translations 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django React Boilerplate 2 | 3 | [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.0-4baaaa.svg)](code_of_conduct.md) 4 | [![License: MIT](https://img.shields.io/github/license/vintasoftware/django-react-boilerplate.svg)](LICENSE.txt) 5 | 6 | ## About 7 | A [Django](https://www.djangoproject.com/) project boilerplate/template with lots of state of the art libraries and tools like: 8 | - [React](https://facebook.github.io/react/), for building interactive UIs 9 | - [django-js-reverse](https://github.com/ierror/django-js-reverse), for generating URLs on JS 10 | - [React Bootstrap](https://react-bootstrap.github.io/), for responsive styling 11 | - [Webpack](https://webpack.js.org/), for bundling static assets 12 | - [Celery](https://docs.celeryproject.org/), for background worker tasks 13 | - [WhiteNoise](http://whitenoise.evans.io/en/stable/) with [brotlipy](https://github.com/python-hyper/brotlipy), for efficient static files serving 14 | - [prospector](https://prospector.landscape.io/en/master/) and [ESLint](https://eslint.org/) with [pre-commit](https://pre-commit.com/) for automated quality assurance (does not replace proper testing!) 15 | 16 | For continuous integration, a [Github Action](https://github.com/features/actions) configuration `.github/workflows/main.yml` is included. 17 | 18 | Also, includes a Heroku `app.json` and a working Django `production.py` settings, enabling easy deployments with ['Deploy to Heroku' button](https://devcenter.heroku.com/articles/heroku-button). Those Heroku plugins are included in `app.json`: 19 | - PostgreSQL, for DB 20 | - Redis, for Celery 21 | - Sendgrid, for e-mail sending 22 | - Papertrail, for logs and platform errors alerts (must set them manually) 23 | 24 | This is a good starting point for modern Python/JavaScript web projects. 25 | 26 | ## Project bootstrap [![main](https://github.com/chocoelho/django-react-boilerplate/actions/workflows/main.yml/badge.svg)](https://github.com/chocoelho/django-react-boilerplate/actions/workflows/main.yml) [![Greenkeeper badge](https://badges.greenkeeper.io/vintasoftware/django-react-boilerplate.svg)](https://greenkeeper.io/) 27 | - [ ] Make sure you have Python 3.8 installed 28 | - [ ] Install Django with `pip install django`, to have the `django-admin` command available. 29 | - [ ] Open the command line and go to the directory you want to start your project in. 30 | - [ ] Start your project using: 31 | ``` 32 | django-admin startproject theprojectname --extension py,yml,json --name Procfile,Dockerfile,README.md,.env.example,.gitignore,Makefile --template=https://github.com/vintasoftware/django-react-boilerplate/archive/boilerplate-release.zip 33 | ``` 34 | Alternatively, you may start the project in the current directory by placing a `.` right after the project name, using the following command: 35 | ``` 36 | django-admin startproject theprojectname . --extension py,yml,json --name Procfile,Dockerfile,README.md,.env.example,.gitignore,Makefile --template=https://github.com/vintasoftware/django-react-boilerplate/archive/boilerplate-release.zip 37 | ``` 38 | In the next steps, always remember to replace theprojectname with your project's name 39 | - [ ] Above: don't forget the `--extension` and `--name` params! 40 | - [ ] Change the first line of README to the name of the project 41 | - [ ] Add an email address to the `ADMINS` settings variable in `{{project_name}}/backend/{{project_name}}/settings/base.py` 42 | - [ ] Change the `SERVER_EMAIL` to the email address used to send e-mails in `{{project_name}}/backend/{{project_name}}/settings/production.py` 43 | - [ ] Rename the folder `github` to `.github` with the command `mv github .github` 44 | 45 | After completing ALL of the above, remove this `Project bootstrap` section from the project README. Then follow `Running` below. 46 | 47 | ## Running 48 | ### Tools 49 | - Setup [editorconfig](http://editorconfig.org/), [prospector](https://prospector.landscape.io/en/master/) and [ESLint](http://eslint.org/) in the text editor you will use to develop. 50 | 51 | ### Setup 52 | - Inside the `backend` folder, do the following: 53 | - Create a copy of `{{project_name}}/settings/local.py.example`: 54 | `cp {{project_name}}/settings/local.py.example {{project_name}}/settings/local.py` 55 | - Create a copy of `.env.example`: 56 | `cp .env.example .env` 57 | 58 | ### If you are using Docker: 59 | - Open the `/backend/.env` file on a text editor and uncomment the line `DATABASE_URL=postgres://{{project_name}}:password@db:5432/{{project_name}}` 60 | - Open a new command line window and go to the project's directory 61 | - Run the initial setup: 62 | `make docker_setup` 63 | - Create the migrations for `users` app: 64 | `make docker_makemigrations` 65 | - Run the migrations: 66 | `make docker_migrate` 67 | - Run the project: 68 | `make docker_up` 69 | - Access `http://localhost:8000` on your browser and the project should be running there 70 | - When you run `make docker_up`, some containers are spinned up (frontend, backend, database, etc) and each one will be running on a different port 71 | - The container with the React app uses port 3000. However, if you try accessing it on your browser, the app won't appear there and you'll probably see a blank page with the "Cannot GET /" error 72 | - This happens because the container responsible for displaying the whole application is the Django app one (running on port 8000). The frontend container is responsible for providing a bundle with its assets for [django-webpack-loader](https://github.com/django-webpack/django-webpack-loader) to consume and render them on a Django template 73 | - To access the logs for each service, run: 74 | `make docker_logs ` (either `backend`, `frontend`, etc) 75 | - To stop the project, run: 76 | `make docker_down` 77 | 78 | #### Adding new dependencies 79 | - Open a new command line window and go to the project's directory 80 | - Update the dependencies management files by performing any number of the following steps: 81 | - To add a new **frontend** dependency, run `npm install --save` 82 | > The above command will update your `package.json`, but won't make the change effective inside the container yet 83 | - To add a new **backend** dependency, update `requirements.in` or `dev-requirements.in` with the newest requirements 84 | - After updating the desired file(s), run `make docker_update_dependencies` to update the containers with the new dependencies 85 | > The above command will stop and re-build the containers in order to make the new dependencies effective 86 | 87 | ### If you are not using Docker: 88 | #### Setup and run the frontend app 89 | - Open a new command line window and go to the project's directory 90 | - `npm install` 91 | - `npm run start` 92 | - This is used to serve the frontend assets to be consumed by [django-webpack-loader](https://github.com/django-webpack/django-webpack-loader) and not to run the React application as usual, so don't worry if you try to check what's running on port 3000 and see an error on your browser 93 | 94 | #### Setup the backend app 95 | - Open the `/backend/.env` file on a text editor and do one of the following: 96 | - If you wish to use SQLite locally, uncomment the line `DATABASE_URL=sqlite:///backend/db.sqlite3` 97 | - If you wish to use PostgreSQL locally, uncomment and edit the line `DATABASE_URL=postgres://{{project_name}}:password@db:5432/{{project_name}}` in order to make it correctly point to your database URL 98 | - The url format is the following: `postgres://USER:PASSWORD@HOST:PORT/NAME` 99 | - If you wish to use another database engine locally, add a new `DATABASE_URL` setting for the database you wish to use 100 | - Please refer to [dj-database-url](https://github.com/jacobian/dj-database-url#url-schema) on how to configure `DATABASE_URL` for commonly used engines 101 | - Open a new command line window and go to the project's directory 102 | - Create a new virtualenv with either [virtualenvwrapper](https://virtualenvwrapper.readthedocs.io/en/latest/) or only virtualenv: `mkvirtualenv {{project_name}}` or `python -m venv {{project_name}}-venv` 103 | > If you're using Python's virtualenv (the latter option), make sure to create the environment with the suggested name, otherwise it will be added to version control. 104 | - Make sure the virtualenv is activated `workon {{project_name}}` or `source {{project_name}}-venv/bin/activate` 105 | - Run `make compile_install_requirements` to install the requirements 106 | > Please make sure you have already setup PostgreSQL on your environment before installing the requirements 107 | 108 | > In case you wish to use a Conda virtual environment, please remove the line `export PIP_REQUIRE_VIRTUALENV=true; \` from `Makefile` 109 | 110 | #### Run the backend app 111 | - With the virtualenv enabled, go to the `backend` directory 112 | - Create the migrations for `users` app: 113 | `python manage.py makemigrations` 114 | - Run the migrations: 115 | `python manage.py migrate` 116 | - Run the project: 117 | `python manage.py runserver` 118 | - Open a browser and go to `http://localhost:8000` to see the project running 119 | 120 | #### Setup Celery 121 | - Open a command line window and go to the project's directory 122 | - `workon {{project_name}}` or `source {{project_name}}-venv/bin/activate` depending on if you are using virtualenvwrapper or just virtualenv. 123 | - `python manage.py celery` 124 | 125 | #### Mailhog 126 | - For development, we use Mailhog to test our e-mail workflows, since it allows us to inspect the messages to validate they're correctly built 127 | - Docker users already have it setup and running once they start the project 128 | - For non-Docker users, please have a look [here](https://github.com/mailhog/MailHog#installation) for instructions on how to setup Mailhog on specific environments 129 | > The project expects Mailhog SMTP server to be running on port 1025, you may alter that by changing `EMAIL_PORT` on settings 130 | 131 | 132 | ### Testing 133 | `make test` 134 | 135 | Will run django tests using `--keepdb` and `--parallel`. You may pass a path to the desired test module in the make command. E.g.: 136 | 137 | `make test someapp.tests.test_views` 138 | 139 | ### Adding new pypi libs 140 | Add the libname to either `requirements.in` or `dev-requirements.in`, then either upgrade the libs with `make upgrade` or manually compile it and then, install. 141 | `pip-compile requirements.in > requirements.txt` or `make upgrade` 142 | `pip install -r requirements.txt` 143 | 144 | ## Deployment 145 | ### Setup 146 | This project comes with an `app.json` file, which can be used to create an app on Heroku from a GitHub repository. 147 | 148 | Before deploying, please make sure you've generated an up-to-date `requirements.txt` file containing the Python dependencies. This is necessary even if you've used Docker for local runs. Do so by following [these instructions](#setup-the-backend-app). 149 | 150 | After setting up the project, you can init a repository and push it on GitHub. If your repository is public, you can use the following button: 151 | 152 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) 153 | 154 | If you are in a private repository, access the following link replacing `$YOUR_REPOSITORY_LINK$` with your repository link. 155 | 156 | - `https://heroku.com/deploy?template=$YOUR_REPOSITORY_LINK$` 157 | 158 | Remember to fill the `ALLOWED_HOSTS` with the URL of your app, the default on heroku is `appname.herokuapp.com`. Replace `appname` with your heroku app name. 159 | 160 | ### Sentry 161 | 162 | [Sentry](https://sentry.io) is already set up on the project. For production, add `SENTRY_DSN` environment variable on Heroku, with your Sentry DSN as the value. 163 | 164 | You can test your Sentry configuration by deploying the boilerplate with the sample page and clicking on the corresponding button. 165 | 166 | ### Sentry source maps for JS files 167 | 168 | The `bin/post_compile` script has a step to push Javascript source maps to Sentry, however some environment variables need to be set on Heroku. 169 | 170 | You need to enable Heroku dyno metadata on your Heroku App. Use the following command on Heroku CLI: 171 | 172 | - `heroku labs:enable runtime-dyno-metadata -a ` 173 | 174 | The environment variables that need to be set are: 175 | 176 | - `SENTRY_ORG` - Name of the Sentry Organization that owns your Sentry Project. 177 | - `SENTRY_PROJECT_NAME` - Name of the Sentry Project. 178 | - `SENTRY_API_KEY` - Sentry API key that needs to be generated on Sentry. [You can find or create authentication tokens within Sentry](https://sentry.io/api/). 179 | 180 | After enabling dyno metadata and setting the environment variables, your next Heroku Deploys will create a release on Sentry where the release name is the commit SHA, and it will push the source maps to it. 181 | 182 | ## Linting 183 | - Manually with `prospector` and `npm run lint` on project root. 184 | - During development with an editor compatible with prospector and ESLint. 185 | 186 | ## Pre-commit hooks 187 | - Run `pre-commit install` to enable the hook into your git repo. The hook will run automatically for each commit. 188 | - Run `git commit -m "Your message" -n` to skip the hook if you need. 189 | 190 | ## Opinionated Settings 191 | Some settings defaults were decided based on Vinta's experiences. Here's the rationale behind them: 192 | 193 | ### `CELERY_ACKS_LATE = True` 194 | We believe Celery tasks should be idempotent. So for us it's safe to set `CELERY_ACKS_LATE = True` to ensure tasks will be re-queued after a worker failure. Check Celery docs on ["Should I use retry or acks_late?"](https://docs.celeryproject.org/en/latest/faq.html#should-i-use-retry-or-acks-late) for more info. 195 | 196 | ## Features Catalogue 197 | 198 | ### Frontend 199 | - `react` for building interactive UIs 200 | - `react-dom` for rendering the UI 201 | - `react-router` for page navigation 202 | - `webpack` for bundling static assets 203 | - `webpack-bundle-tracker` for providing the bundled assets to Django 204 | - Styling 205 | - `bootstrap` for providing responsive stylesheets 206 | - `react-bootstrap` for providing components built on top of Bootstrap CSS without using plugins 207 | - `node-sass` for providing compatibility with SCSS files 208 | - State management and backend integration 209 | - `axios` for performing asynchronous calls 210 | - `cookie` for easy integration with Django using the `csrftoken` cookie 211 | - `redux` for easy state management across the application 212 | - `connected-react-router` for integrating Redux with React Router 213 | - `history` for providing browser history to Connected React Router 214 | - `react-redux` for integrating React with Redux 215 | - `redux-devtools-extension` for inspecting and debugging Redux via browser 216 | - `redux-thunk` for interacting with the Redux store through asynchronous logic 217 | - Utilities 218 | - `lodash` for general utility functions 219 | - `classnames` for easy working with complex CSS class names on components 220 | - `prop-types` for improving QoL while developing providing basic type-checking for React props 221 | - `react-hot-loader` for improving QoL while developing through automatic browser refreshing 222 | 223 | ### Backend 224 | - `django` for building backend logic using Python 225 | - `djangorestframework` for building a REST API on top of Django 226 | - `django-webpack-loader` for rendering the bundled frontend assets 227 | - `django-js-reverse` for easy handling of Django URLs on JS 228 | - `psycopg2` for using PostgreSQL database 229 | - `sentry-sdk` for error monitoring 230 | - `python-decouple` for reading environment variables on settings files 231 | - `celery` for background worker tasks 232 | - `django-debreach` for additional protection against BREACH attack 233 | - `whitenoise` and `brotlipy` for serving static assets 234 | 235 | ## Contributing 236 | 237 | If you wish to contribute to this project, please first discuss the change you wish to make via an [issue](https://github.com/vintasoftware/django-react-boilerplate/issues). 238 | 239 | Check our [contributing guide](https://github.com/vintasoftware/django-react-boilerplate/blob/master/CONTRIBUTING.md) to learn more about our development process and how you can test your changes to the boilerplate. 240 | 241 | ## Commercial Support 242 | [![alt text](https://avatars2.githubusercontent.com/u/5529080?s=80&v=4 "Vinta Logo")](https://www.vinta.com.br/) 243 | 244 | This project is maintained by [Vinta Software](https://www.vinta.com.br/) and is used in products of Vinta's clients. We are always looking for exciting work, so if you need any commercial support, feel free to get in touch: contact@vinta.com.br 245 | --------------------------------------------------------------------------------