├── .babelrc ├── .bootstraprc ├── .env ├── .gitignore ├── .gitmodules ├── .travis.yml ├── AUTHORS.md ├── LICENSE ├── README.md ├── docker-common.yml ├── docker-compose.yml ├── docker ├── django │ ├── Dockerfile │ └── django-entrypoint.sh ├── nginx │ └── default.conf ├── postgres │ ├── Dockerfile │ ├── data │ │ └── .gitkeep │ └── init-user-db.sh └── web │ ├── Dockerfile │ └── web-entrypoint.sh ├── package.json ├── py-requirements ├── base.txt ├── dev.txt └── prod.txt ├── pytest.ini ├── pytest_ci.ini ├── screenshots ├── screenshot_01.png └── screenshot_02.png ├── src ├── accounts │ ├── __init__.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_clean_user_model.py │ │ └── __init__.py │ ├── models.py │ ├── serializers.py │ ├── urls.py │ └── views.py ├── base │ ├── __init__.py │ ├── urls.py │ └── views.py ├── djangoreactredux │ ├── __init__.py │ ├── settings │ │ ├── __init__.py │ │ ├── base.py │ │ ├── ci.py │ │ ├── dev.py │ │ ├── dev_docker.py │ │ ├── prod.py │ │ └── staging.py │ ├── urls.py │ └── wsgi.py ├── fixtures.json ├── lib │ ├── __init__.py │ ├── testutils.py │ └── utils.py ├── manage.py └── static │ ├── actions │ ├── auth.js │ └── data.js │ ├── app.js │ ├── components │ └── .gitkeep │ ├── constants │ └── index.js │ ├── containers │ ├── Home │ │ ├── images │ │ │ ├── react-logo.png │ │ │ └── redux-logo.png │ │ ├── index.js │ │ └── style.scss │ ├── Login │ │ └── index.js │ ├── NotFound │ │ └── index.js │ ├── Protected │ │ └── index.js │ ├── Root │ │ ├── DevTools.js │ │ ├── Root.dev.js │ │ ├── Root.js │ │ └── Root.prod.js │ └── index.js │ ├── fonts │ └── .keep │ ├── images │ └── .keep │ ├── index.html │ ├── index.js │ ├── postcss.config.js │ ├── reducers │ ├── auth.js │ ├── data.js │ └── index.js │ ├── routes.js │ ├── store │ ├── configureStore.dev.js │ ├── configureStore.js │ └── configureStore.prod.js │ ├── styles │ ├── components │ │ └── .gitkeep │ ├── config │ │ ├── _colors.scss │ │ ├── _fonts.scss │ │ ├── _reset.scss │ │ ├── _typography.scss │ │ └── _variables.scss │ ├── font-awesome-helper.js │ ├── font-awesome.config.js │ ├── font-awesome.config.less │ ├── font-awesome.config.prod.js │ ├── main.scss │ ├── theme │ │ ├── _base.scss │ │ ├── _footer.scss │ │ ├── _login.scss │ │ └── _navbar.scss │ └── utils │ │ └── _margins.scss │ └── utils │ ├── config.js │ ├── index.js │ └── requireAuthentication.js ├── tests ├── __init__.py ├── js │ ├── actions │ │ ├── auth.specs.js │ │ └── data.specs.js │ ├── components │ │ └── AuthenticatedComponent.spec.js │ ├── containers │ │ ├── App.spec.js │ │ ├── Home.spec.js │ │ ├── Login.spec.js │ │ ├── NotFound.spec.js │ │ └── Protected.spec.js │ └── reducers │ │ ├── auth.spec.js │ │ ├── data.spec.js │ │ └── general.spec.js ├── python │ ├── __init__.py │ ├── accounts │ │ ├── __init__.py │ │ ├── test_models.py │ │ ├── test_serializers.py │ │ └── test_views.py │ ├── base │ │ ├── __init__.py │ │ └── test_views.py │ └── utils │ │ ├── __init__.py │ │ └── test_utils.py └── require.js └── webpack ├── common.config.js ├── dev.config.js └── prod.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | // need loose mode for IE9 https://phabricator.babeljs.io/T3041 3 | "presets": [ 4 | [ 5 | "es2015", 6 | { 7 | "loose": true 8 | } 9 | ], 10 | "stage-1", 11 | "react" 12 | ], 13 | "plugins": [ 14 | "transform-decorators-legacy" 15 | ] 16 | } -------------------------------------------------------------------------------- /.bootstraprc: -------------------------------------------------------------------------------- 1 | --- 2 | # Output debugging info 3 | # loglevel: debug 4 | 5 | # Major version of Bootstrap: 3 or 4 6 | bootstrapVersion: 3 7 | 8 | # If Bootstrap version 3 is used - turn on/off custom icon font path 9 | useCustomIconFontPath: false 10 | 11 | # Webpack loaders, order matters 12 | styleLoaders: 13 | - style-loader 14 | - css-loader 15 | - sass-loader 16 | 17 | # Extract styles to stand-alone css file 18 | # Different settings for different environments can be used, 19 | # It depends on value of NODE_ENV environment variable 20 | # This param can also be set in webpack config: 21 | # entry: 'bootstrap-loader/extractStyles' 22 | # extractStyles: false 23 | extractStyles: true 24 | # env: 25 | # development: 26 | # extractStyles: false 27 | # production: 28 | # extractStyles: true 29 | 30 | 31 | # Customize Bootstrap variables that get imported before the original Bootstrap variables. 32 | # Thus, derived Bootstrap variables can depend on values from here. 33 | # See the Bootstrap _variables.scss file for examples of derived Bootstrap variables. 34 | # 35 | # preBootstrapCustomizations: ./path/to/bootstrap/pre-customizations.scss 36 | 37 | 38 | # This gets loaded after bootstrap/variables is loaded 39 | # Thus, you may customize Bootstrap variables 40 | # based on the values established in the Bootstrap _variables.scss file 41 | # 42 | # bootstrapCustomizations: ./path/to/bootstrap/customizations.scss 43 | 44 | 45 | # Import your custom styles here 46 | # Usually this endpoint-file contains list of @imports of your application styles 47 | # 48 | # appStyles: ./path/to/your/app/styles/endpoint.scss 49 | 50 | 51 | ### Bootstrap styles 52 | styles: 53 | 54 | # Mixins 55 | mixins: true 56 | 57 | # Reset and dependencies 58 | normalize: true 59 | print: true 60 | glyphicons: true 61 | 62 | # Core CSS 63 | scaffolding: true 64 | type: true 65 | code: true 66 | grid: true 67 | tables: true 68 | forms: true 69 | buttons: true 70 | 71 | # Components 72 | component-animations: true 73 | dropdowns: true 74 | button-groups: true 75 | input-groups: true 76 | navs: true 77 | navbar: true 78 | breadcrumbs: true 79 | pagination: true 80 | pager: true 81 | labels: true 82 | badges: true 83 | jumbotron: true 84 | thumbnails: true 85 | alerts: true 86 | progress-bars: true 87 | media: true 88 | list-group: true 89 | panels: true 90 | wells: true 91 | responsive-embed: true 92 | close: true 93 | 94 | # Components w/ JavaScript 95 | modals: true 96 | tooltip: true 97 | popovers: true 98 | carousel: true 99 | 100 | # Utility classes 101 | utilities: true 102 | responsive-utilities: true 103 | 104 | ### Bootstrap scripts 105 | scripts: 106 | transition: true 107 | alert: true 108 | button: true 109 | carousel: true 110 | collapse: true 111 | dropdown: true 112 | modal: true 113 | tooltip: true 114 | popover: true 115 | scrollspy: true 116 | tab: true 117 | affix: true -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seedstars/django-react-redux-base/a8f9872f20c0fc77b0dcb0386ca7b1643f8f72e1/.env -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDEs 2 | .project 3 | .pydevproject 4 | .idea 5 | .vscode 6 | .settings 7 | 8 | # Random files 9 | *~ 10 | \#* 11 | 12 | virtualenv/ 13 | htmlcov/ 14 | src/static_dist/ 15 | src/static_root/ 16 | 17 | # Python compiled byte code 18 | *.pyc 19 | 20 | # SQLite3 database 21 | *.sqlite 22 | 23 | # Node modules 24 | node_modules/ 25 | yarn.lock 26 | npm-debug.log 27 | 28 | # Coverage 29 | coverage/ 30 | .coverage 31 | .cache 32 | 33 | # static validation files 34 | .eslintrc 35 | .sass-lint.yml 36 | .prospector.yml 37 | .coveragerc 38 | 39 | # in case we run shippable tests don't include it 40 | shippable/ 41 | 42 | # not include scripts because they come from external module 43 | scripts/ 44 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "scripts"] 2 | path = scripts 3 | url = https://github.com/Seedstars/culture-scripts 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | branches: 3 | only: 4 | - master 5 | python: 6 | - "3.4" 7 | services: 8 | - postgresql 9 | before_script: 10 | - psql -c 'create database travis_ci_test;' -U postgres 11 | - git submodule init 12 | - git submodule update 13 | - ./scripts/get_static_validation.sh 14 | # command to install dependencies 15 | install: 16 | - pip install -r py-requirements/dev.txt 17 | - nvm install 6 18 | - npm install 19 | - npm run prod 20 | # command to run tests 21 | script: 22 | - py.test -c pytest_ci.ini tests/ 23 | - prospector --profile-path=. --profile=.prospector.yml --path=src --ignore-patterns=static 24 | - bandit -r src 25 | - npm run coverage 26 | - npm run lintjs 27 | - npm run lintscss 28 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | This code was created at Seedstars Labs (www.seedstarslabs.com) for internal use. 2 | 3 | The JS part was inspired from the excellent [React-redux-jwt-auth-example](https://github.com/joshgeller/react-redux-jwt-auth-example). 4 | 5 | - Daniel Silva 6 | - Fernando Ferreira 7 | - Filipe Garcia 8 | - Luis Rodrigues 9 | - Pedro Gomes 10 | - Ruben Almeida 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Seedstars 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/Seedstars/django-react-redux-base.svg?branch=master)](https://travis-ci.org/Seedstars/django-react-redux-base) 2 | 3 | # Django React/Redux Base Project 4 | 5 | This repository includes a boilerplate project used for all Seedstars Labs applications. It uses Django as backend and React as frontend. 6 | 7 | We build on the shoulders of giants with the following technologies: 8 | 9 | **Frontend** 10 | 11 | * [React](https://github.com/facebook/react) 12 | * [React Router](https://github.com/ReactTraining/react-router) Declarative routing for React 13 | * [Babel](http://babeljs.io) for ES6 and ES7 magic 14 | * [Webpack](http://webpack.github.io) for bundling 15 | * [Webpack Dev Middleware](http://webpack.github.io/docs/webpack-dev-middleware.html) 16 | * [Clean Webpack Plugin](https://github.com/johnagan/clean-webpack-plugin) 17 | * [Redux](https://github.com/reactjs/redux) Predictable state container for JavaScript apps 18 | * [Redux Dev Tools](https://github.com/gaearon/redux-devtools) DevTools for Redux with hot reloading, action replay, and customizable UI. Watch [Dan Abramov's talk](https://www.youtube.com/watch?v=xsSnOQynTHs) 19 | * [Redux Thunk](https://github.com/gaearon/redux-thunk) Thunk middleware for Redux - used in async actions 20 | * [React Router Redux](https://github.com/reactjs/react-router-redux) Ruthlessly simple bindings to keep react-router and redux in sync 21 | * [fetch](https://github.com/github/fetch) A window.fetch JavaScript polyfill 22 | * [tcomb form](https://github.com/gcanti/tcomb-form) Forms library for react 23 | * [style-loader](https://github.com/webpack/style-loader), [sass-loader](https://github.com/jtangelder/sass-loader) and [less-loader](https://github.com/webpack/less-loader) to allow import of stylesheets in plain css, sass and less, 24 | * [font-awesome-webpack](https://github.com/gowravshekar/font-awesome-webpack) to customize FontAwesome 25 | * [bootstrap-loader](https://github.com/shakacode/bootstrap-loader) to customize Bootstrap 26 | * [ESLint](http://eslint.org), [Airbnb Javascript/React Styleguide](https://github.com/airbnb/javascript), [Airbnb CSS / Sass Styleguide](https://github.com/airbnb/css) to maintain a consistent code style and [eslint-plugin-import](https://github.com/benmosher/eslint-plugin-import) to make sure all imports are correct 27 | * [mocha](https://mochajs.org/) to allow writing unit tests for the project 28 | * [Enzyme](http://airbnb.io/enzyme/) JavaScript Testing utilities for React 29 | * [redux-mock-store](https://github.com/arnaudbenard/redux-mock-store) a mock store for your testing your redux async action creators and middleware 30 | * [expect](https://github.com/mjackson/expect) Write better assertions 31 | * [Nock](https://github.com/pgte/nock) HTTP mocking and expectations library 32 | * [istanbul](https://github.com/gotwarlost/istanbul) to generate coverage when running mocha 33 | 34 | **Backend** 35 | 36 | * [Django](https://www.djangoproject.com/) 37 | * [Django REST framework](http://www.django-rest-framework.org/) Django REST framework is a powerful and flexible toolkit for building Web APIs 38 | * [Django REST Knox](https://github.com/James1345/django-rest-knox) Token based authentication for API endpoints 39 | * [WhiteNoise](http://whitenoise.evans.io/en/latest/django.html) to serve files efficiently from Django 40 | * [Prospector](http://prospector.landscape.io/en/master/) a complete Python static analysis tool 41 | * [Bandit](https://github.com/openstack/bandit) a security linter from OpenStack Security 42 | * [pytest](http://pytest.org/latest/) a mature full-featured Python testing tool 43 | * [Mock](http://www.voidspace.org.uk/python/mock/) mocking and testing Library 44 | * [Responses](https://github.com/getsentry/responses) a utility for mocking out the Python Requests library 45 | 46 | 47 | ## Readme Notes 48 | 49 | * If the command line starts with $, the command should run with user privileges 50 | * If the command line starts with #, the command should run with root privileges 51 | 52 | 53 | ## Retrieve code 54 | 55 | * `$ git clone https://github.com/seedstars/django-react-redux-base.git` 56 | * `$ cd django-react-redux-base` 57 | * `$ git submodule init` 58 | * `$ git submodule update` 59 | * `$ ./scripts/get_static_validation.sh` 60 | 61 | 62 | Remember that when you copy this repository for a new project you need to add the scripts external module using: 63 | 64 | * `$ git submodule add https://github.com/Seedstars/culture-scripts scripts` 65 | 66 | NOTE: This is only needed in case you copy this code to a new project. If you only clone or fork the repository, the submodule is already configured 67 | 68 | 69 | ## Installation 70 | 71 | You have two ways of running this project: Using the Dockers scripts or running directly in the console. 72 | 73 | ### Running NO DOCKER 74 | 75 | **NodeJS tooling** 76 | 77 | * `$ wget -qO- https://deb.nodesource.com/setup_6.x | sudo bash -` 78 | * `$ apt-get install --yes nodejs` 79 | * `$ curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -` 80 | * `$ echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list` 81 | * `$ sudo apt-get update && sudo apt-get install yarn` 82 | 83 | **Compile and run project** 84 | 85 | There are commands you need to compile javascript and run project. Ideally `yarn run dev` should be run in another console because it blocks it. 86 | 87 | * `$ yarn ` 88 | * `$ yarn run dev` # will run webpack with watch and compile code as it changes 89 | 90 | * `$ virtualenv -p /usr/bin/python3 virtualenv` 91 | * `$ source virtualenv/bin/activate` 92 | * `$ pip install -r py-requirements/dev.txt` 93 | 94 | * `$ cd src` 95 | * `$ python manage.py migrate` 96 | * `$ python manage.py loaddata fixtures.json` 97 | * `$ python manage.py runserver` 98 | 99 | Then open your browser the page: http://localhost:8000/ If all goes ok you should see a React single page app. 100 | 101 | 102 | ### Running DOCKER 103 | 104 | We use Docker as a development environment. For production, we leave you to set it up the way you feel better, 105 | although it is trivial to extrapolate a production environment from the current docker-compose.yml. 106 | 107 | * Install [Docker](https://www.docker.com/products/overview) and [Docker Compose](https://docs.docker.com/compose/install/). 108 | * `$ docker-compose build` 109 | * `$ docker-compose up` 110 | 111 | To stop the development server: 112 | 113 | * `$ docker-compose stop` 114 | 115 | Stop Docker development server and remove containers, networks, volumes, and images created by up. 116 | 117 | * `$ docker-compose down` 118 | 119 | You can access shell in a container 120 | 121 | * `$ docker ps # get the name from the list of running containers` 122 | * `$ docker exec -i -t djangoreactreduxbase_frontend_1 /bin/bash` 123 | 124 | The database can be accessed @localhost:5433 125 | 126 | * `$ psql -h localhost -p 5433 -U djangoreactredux djangoreactredux_dev` 127 | 128 | 129 | ## Accessing Website 130 | 131 | The project has CORS enabled and the URL is hard-coded in javascript to http://localhost:8000 132 | For login to work you will to use this URL in your browser. 133 | 134 | 135 | ## Testing 136 | 137 | To make sure the code respects all coding guidelines you should run the statics analysis and test scripts before pushing any code. 138 | 139 | Frontend (javascript tests) 140 | 141 | * `$ ./scripts/test_local_frontend.sh` 142 | 143 | Backend (django/python tests) 144 | 145 | * `$ ./scripts/test_local_backend.sh` 146 | 147 | Please take into account that test_local_backend.sh runs py.test with `--nomigrations --reuse-db` flags to allow it be performant. Any time you add a migration please remove those flags next time you run the script. 148 | 149 | ### Static analysis 150 | 151 | 152 | Frontend (javascript static analysis) 153 | 154 | * `$ ./scripts/static_validate_frontend.sh` 155 | 156 | Backend (django/python static analysis) 157 | 158 | * `$ ./scripts/static_validate_backend.sh` 159 | 160 | ## Deployment in Production 161 | 162 | We deploy all our production code using Kubernetes. Explaining how to do deployments is beyond the scope of this boilerplate. 163 | 164 | Here's a great article from digital ocean on how to deploy django project in a VM: https://www.digitalocean.com/community/tutorials/how-to-set-up-django-with-postgres-nginx-and-gunicorn-on-ubuntu-16-04 165 | 166 | 167 | 168 | ## Screenshots 169 | 170 | Here are some screenshots of the boilerplate project. 171 | 172 | ![Screenshot01][1] 173 | 174 | [1]: ./screenshots/screenshot_01.png 175 | 176 | ![Screenshot02][2] 177 | 178 | [2]: ./screenshots/screenshot_02.png 179 | 180 | 181 | ## Gotchas in Docker 182 | 183 | * This project uses NodeJS v6.x (stable) and yarn 184 | * The development server takes longer than the django server to start, as it has to install the javascript dependencies (if not already installed) and fire webpack. This means that after the django server starts, you should wait that webpack finishes compiling the .js files. 185 | * If your IDE has builtin language support for python with auto-imports (e.g. PyCharm), you can create a virtualenv and install the py-requirements. 186 | * If you are annoyed by docker creating files belonging to root (which is Docker's intended behaviour), you can run `# chown -hR $(whoami) .` before firing up the server. 187 | 188 | 189 | ## Contributing 190 | 191 | We welcome contributions from the community, given that they respect these basic guidelines: 192 | 193 | * All Tests & Static Analysis passing; 194 | * 100% code coverage; 195 | 196 | Prior to any pull-request, we advise to [open an issue](https://github.com/Seedstars/django-react-redux-base/issues). This is because, although we are happy to merge your code, we must make sure the changes don't impact our way of doing things, thus resulting on a declined PR, and your time wasted. 197 | 198 | If you want to tackle any open issue, well..... Just go for it! :) 199 | -------------------------------------------------------------------------------- /docker-common.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | nginx: 5 | restart: always 6 | image: nginx:1.11.6-alpine 7 | postgres: 8 | restart: always 9 | build: 10 | context: . 11 | dockerfile: ./docker/postgres/Dockerfile 12 | expose: 13 | - 5432 14 | volumes: 15 | - ./docker/postgres/data:/var/lib/postgresql 16 | django: 17 | restart: always 18 | build: 19 | context: . 20 | dockerfile: ./docker/django/Dockerfile 21 | volumes: 22 | - .:/django 23 | web: 24 | restart: always 25 | build: 26 | context: . 27 | dockerfile: ./docker/web/Dockerfile 28 | volumes: 29 | - .:/django 30 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | nginx: 5 | extends: 6 | file: docker-common.yml 7 | service: nginx 8 | ports: 9 | - 8000:8000 10 | volumes: 11 | - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf 12 | volumes_from: 13 | - backend 14 | postgres: 15 | extends: 16 | file: docker-common.yml 17 | service: postgres 18 | ports: 19 | - 5433:5432 20 | volumes: 21 | - ./docker/postgres/init-user-db.sh:/docker-entrypoint-initdb.d/init-user-db.sh 22 | backend: 23 | extends: 24 | file: docker-common.yml 25 | service: django 26 | links: 27 | - postgres 28 | entrypoint: 29 | - /django-entrypoint.sh 30 | expose: 31 | - 8000 32 | frontend: 33 | extends: 34 | file: docker-common.yml 35 | service: web 36 | links: 37 | - backend 38 | entrypoint: 39 | - /web-entrypoint.sh 40 | -------------------------------------------------------------------------------- /docker/django/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.4.3 2 | MAINTAINER Filipe Garcia 3 | 4 | ENV PYTHONUNBUFFERED 1 5 | 6 | COPY ./docker/django/django-entrypoint.sh / 7 | COPY ./py-requirements /django/py-requirements 8 | 9 | WORKDIR /django 10 | 11 | RUN pip install pip==9.0.1 12 | RUN pip install -r py-requirements/dev.txt 13 | -------------------------------------------------------------------------------- /docker/django/django-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | until cd src 4 | do 5 | echo "Waiting for django volume..." 6 | sleep 2 7 | done 8 | 9 | until python manage.py migrate --settings=djangoreactredux.settings.dev_docker 10 | do 11 | echo "Waiting for postgres ready..." 12 | sleep 2 13 | done 14 | 15 | python manage.py loaddata fixtures.json --settings=djangoreactredux.settings.dev_docker 16 | python manage.py runserver 0.0.0.0:8000 --settings=djangoreactredux.settings.dev_docker 17 | -------------------------------------------------------------------------------- /docker/nginx/default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | # Set the port to listen on and the server name 3 | listen 8000 default_server; 4 | 5 | client_max_body_size 20M; 6 | 7 | location / { 8 | proxy_set_header Host $http_host; # django uses this by default 9 | proxy_set_header X-Forwarded-Host $server_name; # also in django settings (could disable) 10 | proxy_set_header X-Real-IP $remote_addr; 11 | proxy_pass http://backend:8000; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /docker/postgres/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM postgres:9.5.4 2 | ENV POSTGRES_USER postgres 3 | 4 | 5 | WORKDIR / 6 | 7 | -------------------------------------------------------------------------------- /docker/postgres/data/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seedstars/django-react-redux-base/a8f9872f20c0fc77b0dcb0386ca7b1643f8f72e1/docker/postgres/data/.gitkeep -------------------------------------------------------------------------------- /docker/postgres/init-user-db.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-EOSQL 6 | CREATE USER djangoreactredux WITH PASSWORD 'password' CREATEDB; 7 | CREATE DATABASE djangoreactredux_dev; 8 | GRANT ALL PRIVILEGES ON DATABASE djangoreactredux_dev TO djangoreactredux; 9 | EOSQL 10 | -------------------------------------------------------------------------------- /docker/web/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:14.04 2 | MAINTAINER Filipe Garcia 3 | 4 | COPY ./docker/web/web-entrypoint.sh / 5 | 6 | WORKDIR /django 7 | 8 | RUN apt-get update && apt-get install -y curl 9 | RUN curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash - 10 | RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - 11 | RUN echo "deb http://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list 12 | 13 | RUN apt-get update && apt-get install -y nodejs yarn 14 | 15 | COPY ./package.json /django/package.json 16 | -------------------------------------------------------------------------------- /docker/web/web-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | yarn 4 | npm run dev 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "django-react-redux-base", 3 | "version": "1.0.0", 4 | "description": "Django, React and Redux!", 5 | "scripts": { 6 | "dev": "webpack --progress --display-error-details --config webpack/common.config.js --watch", 7 | "prod": "webpack --progress -p --config webpack/common.config.js", 8 | "mocha": "mocha --require tests/require --recursive --compilers js:babel-core/register tests/js", 9 | "mocha:watch": "npm run mocha -- --watch tests/js", 10 | "coverage": "istanbul cover node_modules/mocha/bin/_mocha -- --require tests/require --recursive --compilers js:babel-core/register --colors --reporter dot tests/js/", 11 | "lintjs": "eslint -c .eslintrc src/static tests/js || true", 12 | "lintscss": "sass-lint --verbose --no-exit || true" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/Seedstars/django-react-redux-base.git" 17 | }, 18 | "author": "Luis Rodrigues ", 19 | "license": "MIT", 20 | "dependencies": { 21 | "autoprefixer": "7.1.2", 22 | "babel-core": "6.26.0", 23 | "babel-loader": "7.1.2", 24 | "babel-plugin-react-display-name": "2.0.0", 25 | "babel-plugin-react-transform": "2.0.2", 26 | "babel-plugin-transform-decorators-legacy": "1.3.4", 27 | "babel-plugin-undeclared-variables-check": "6.22.0", 28 | "babel-polyfill": "6.26.0", 29 | "babel-preset-es2015": "6.24.1", 30 | "babel-preset-es2015-loose": "8.0.0", 31 | "babel-preset-react": "6.24.1", 32 | "babel-preset-stage-1": "6.24.1", 33 | "babel-runtime": "6.26.0", 34 | "body-parser": "1.17.2", 35 | "bootstrap-loader": "2.2.0", 36 | "bootstrap-sass": "3.3.7", 37 | "classnames": "2.2.5", 38 | "clean-webpack-plugin": "0.1.16", 39 | "css-loader": "0.28.5", 40 | "errorhandler": "1.5.0", 41 | "es6-promise": "4.1.1", 42 | "exports-loader": "0.6.4", 43 | "extract-text-webpack-plugin": "3.0.0", 44 | "file-loader": "0.11.2", 45 | "font-awesome": "4.7.0", 46 | "font-awesome-webpack": "0.0.5-beta.2", 47 | "history": "4.7.2", 48 | "html-webpack-plugin": "2.30.1", 49 | "imports-loader": "0.7.1", 50 | "isomorphic-fetch": "2.2.1", 51 | "jquery": "3.2.1", 52 | "less": "2.7.2", 53 | "less-loader": "4.0.5", 54 | "node-sass": "4.5.3", 55 | "postcss-import": "10.0.0", 56 | "postcss-loader": "2.0.6", 57 | "prop-types": "15.5.10", 58 | "react": "15.6.1", 59 | "react-dom": "15.6.1", 60 | "react-mixin": "3.1.0", 61 | "react-redux": "5.0.6", 62 | "react-router": "4.2.0", 63 | "react-router-dom": "4.2.2", 64 | "react-router-redux": "5.0.0-alpha.6", 65 | "redux": "3.7.2", 66 | "redux-thunk": "2.2.0", 67 | "resolve-url-loader": "2.1.0", 68 | "sass-loader": "6.0.6", 69 | "style-loader": "0.18.2", 70 | "tcomb-form": "0.9.17", 71 | "url-loader": "0.5.9", 72 | "webpack": "3.5.5", 73 | "webpack-merge": "4.1.0", 74 | "whatwg-fetch": "2.0.3", 75 | "yargs": "8.0.2" 76 | }, 77 | "devDependencies": { 78 | "babel-eslint": "7.2.3", 79 | "chai": "4.1.1", 80 | "chai-as-promised": "7.1.1", 81 | "enzyme": "2.9.1", 82 | "eslint": "4.5.0", 83 | "eslint-config-airbnb": "15.1.0", 84 | "eslint-plugin-import": "2.7.0", 85 | "eslint-plugin-jsx-a11y": "5.1.1", 86 | "eslint-plugin-react": "7.3.0", 87 | "expect": "1.20.2", 88 | "ignore-styles": "5.0.01", 89 | "istanbul": "1.0.0-alpha.2", 90 | "jsdom": "11.2.0", 91 | "mocha": "3.5.0", 92 | "mocha-junit-reporter": "1.13.0", 93 | "nock": "9.0.14", 94 | "react-test-renderer": "15.6.1", 95 | "react-transform-catch-errors": "1.0.2", 96 | "react-transform-hmr": "1.0.4", 97 | "redbox-react": "1.5.0", 98 | "redux-devtools": "3.4.0", 99 | "redux-devtools-dock-monitor": "1.1.2", 100 | "redux-devtools-log-monitor": "1.3.0", 101 | "redux-logger": "3.0.6", 102 | "redux-mock-store": "1.2.3", 103 | "sass-lint": "1.10.2", 104 | "sinon": "3.2.1", 105 | "webpack-dev-middleware": "1.12.0", 106 | "webpack-hot-middleware": "2.18.2" 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /py-requirements/base.txt: -------------------------------------------------------------------------------- 1 | Django==1.11.4 2 | django-extensions==1.8.1 3 | 4 | django-disposable-email-checker==1.2.1 5 | 6 | # PostgresSQL 7 | psycopg2==2.7.3 8 | 9 | # serve files 10 | whitenoise==3.3.0 11 | 12 | # Rest Framework 13 | djangorestframework==3.6.4 14 | 15 | # Sentry 16 | raven==6.1.0 17 | 18 | # for date processing 19 | python-dateutil==2.6.1 20 | 21 | # Logging 22 | git+https://github.com/Seedstars/django-rest-logger.git 23 | 24 | django-rest-knox==3.0.3 25 | -------------------------------------------------------------------------------- /py-requirements/dev.txt: -------------------------------------------------------------------------------- 1 | -r base.txt 2 | 3 | mock==2.0.0 4 | factory-boy==2.9.2 5 | 6 | prospector==0.12.7 7 | bandit==1.4.0 8 | 9 | pytest==3.2.1 10 | pytest-cov==2.5.1 11 | pytest-django==3.1.2 12 | pytest-pythonpath==0.7.1 13 | pytest-xdist==1.20.0 14 | 15 | ptpython==0.41 16 | -------------------------------------------------------------------------------- /py-requirements/prod.txt: -------------------------------------------------------------------------------- 1 | -r base.txt 2 | 3 | gunicorn==19.7.1 4 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | norecursedirs = scripts node_modules py-requirements webpack .* {args} 3 | DJANGO_SETTINGS_MODULE=djangoreactredux.settings.dev 4 | python_paths = src tests/python 5 | 6 | -------------------------------------------------------------------------------- /pytest_ci.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | norecursedirs = scripts node_modules py-requirements webpack .* {args} 3 | DJANGO_SETTINGS_MODULE=djangoreactredux.settings.ci 4 | python_paths = src tests/python 5 | -------------------------------------------------------------------------------- /screenshots/screenshot_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seedstars/django-react-redux-base/a8f9872f20c0fc77b0dcb0386ca7b1643f8f72e1/screenshots/screenshot_01.png -------------------------------------------------------------------------------- /screenshots/screenshot_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seedstars/django-react-redux-base/a8f9872f20c0fc77b0dcb0386ca7b1643f8f72e1/screenshots/screenshot_02.png -------------------------------------------------------------------------------- /src/accounts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seedstars/django-react-redux-base/a8f9872f20c0fc77b0dcb0386ca7b1643f8f72e1/src/accounts/__init__.py -------------------------------------------------------------------------------- /src/accounts/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations, models 5 | import django.utils.timezone 6 | import django.core.validators 7 | import uuid 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | dependencies = [ 13 | ('auth', '0006_require_contenttypes_0002'), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='User', 19 | fields=[ 20 | ('password', models.CharField(max_length=128, verbose_name='password')), 21 | ('last_login', models.DateTimeField(blank=True, verbose_name='last login', null=True)), 22 | ('is_superuser', models.BooleanField(help_text='Designates that this user has all permissions without explicitly assigning them.', default=False, verbose_name='superuser status')), 23 | ('id', models.UUIDField(serialize=False, editable=False, primary_key=True, default=uuid.uuid4)), 24 | ('username', models.CharField(max_length=30, error_messages={'unique': 'A user with that username already exists.'}, unique=True, help_text='Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.', verbose_name='username', validators=[django.core.validators.RegexValidator('^[\\w.@+-]+$', 'Enter a valid username. This value may contain only letters, numbers and @/./+/-/_ characters.', 'invalid')])), 25 | ('first_name', models.CharField(max_length=50, verbose_name='First Name')), 26 | ('last_name', models.CharField(max_length=50, verbose_name='Last Name')), 27 | ('email', models.EmailField(max_length=254, unique=True, verbose_name='Email address')), 28 | ('gender', models.CharField(default='M', max_length=1, choices=[('M', 'Male'), ('F', 'Female')])), 29 | ('confirmed_email', models.BooleanField(default=False)), 30 | ('is_staff', models.BooleanField(help_text='Designates whether the user can log into this admin site.', default=False, verbose_name='staff status')), 31 | ('is_active', models.BooleanField(help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', default=True, verbose_name='active')), 32 | ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), 33 | ('activation_key', models.UUIDField(unique=True, default=uuid.uuid4)), 34 | ('groups', models.ManyToManyField(related_query_name='user', related_name='user_set', blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', to='auth.Group', verbose_name='groups')), 35 | ('user_permissions', models.ManyToManyField(related_query_name='user', related_name='user_set', blank=True, help_text='Specific permissions for this user.', to='auth.Permission', verbose_name='user permissions')), 36 | ], 37 | options={ 38 | 'abstract': False, 39 | }, 40 | ), 41 | ] 42 | -------------------------------------------------------------------------------- /src/accounts/migrations/0002_clean_user_model.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations, models 5 | from django.utils.timezone import utc 6 | import datetime 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('accounts', '0001_initial'), 13 | ] 14 | 15 | operations = [ 16 | migrations.RemoveField( 17 | model_name='user', 18 | name='groups', 19 | ), 20 | migrations.RemoveField( 21 | model_name='user', 22 | name='user_permissions', 23 | ), 24 | migrations.RemoveField( 25 | model_name='user', 26 | name='username', 27 | ), 28 | migrations.AddField( 29 | model_name='user', 30 | name='date_updated', 31 | field=models.DateTimeField(default=datetime.datetime(2017, 3, 17, 14, 31, 13, 924508, tzinfo=utc), auto_now=True, verbose_name='date updated'), 32 | preserve_default=False, 33 | ), 34 | migrations.AlterField( 35 | model_name='user', 36 | name='date_joined', 37 | field=models.DateTimeField(auto_now_add=True, verbose_name='date joined'), 38 | ), 39 | migrations.AlterField( 40 | model_name='user', 41 | name='is_active', 42 | field=models.BooleanField(default=True, verbose_name='active'), 43 | ), 44 | migrations.AlterField( 45 | model_name='user', 46 | name='is_staff', 47 | field=models.BooleanField(default=False, verbose_name='staff status'), 48 | ), 49 | migrations.AlterField( 50 | model_name='user', 51 | name='is_superuser', 52 | field=models.BooleanField(default=False, verbose_name='superuser status'), 53 | ), 54 | ] 55 | -------------------------------------------------------------------------------- /src/accounts/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seedstars/django-react-redux-base/a8f9872f20c0fc77b0dcb0386ca7b1643f8f72e1/src/accounts/migrations/__init__.py -------------------------------------------------------------------------------- /src/accounts/models.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | from datetime import timedelta 3 | 4 | from django.conf import settings 5 | from django.contrib.auth.models import AbstractBaseUser, BaseUserManager 6 | from django.db import models 7 | from django.utils import timezone 8 | from django.utils.translation import ugettext_lazy as _ 9 | 10 | 11 | class MyUserManager(BaseUserManager): 12 | def _create_user(self, email, password, first_name, last_name, is_staff, is_superuser, **extra_fields): 13 | """ 14 | Create and save an User with the given email, password, name and phone number. 15 | 16 | :param email: string 17 | :param password: string 18 | :param first_name: string 19 | :param last_name: string 20 | :param is_staff: boolean 21 | :param is_superuser: boolean 22 | :param extra_fields: 23 | :return: User 24 | """ 25 | now = timezone.now() 26 | email = self.normalize_email(email) 27 | user = self.model(email=email, 28 | first_name=first_name, 29 | last_name=last_name, 30 | is_staff=is_staff, 31 | is_active=True, 32 | is_superuser=is_superuser, 33 | last_login=now, 34 | date_joined=now, **extra_fields) 35 | user.set_password(password) 36 | user.save(using=self._db) 37 | 38 | return user 39 | 40 | def create_user(self, email, first_name, last_name, password, **extra_fields): 41 | """ 42 | Create and save an User with the given email, password and name. 43 | 44 | :param email: string 45 | :param first_name: string 46 | :param last_name: string 47 | :param password: string 48 | :param extra_fields: 49 | :return: User 50 | """ 51 | 52 | return self._create_user(email, password, first_name, last_name, is_staff=False, is_superuser=False, 53 | **extra_fields) 54 | 55 | def create_superuser(self, email, first_name='', last_name='', password=None, **extra_fields): 56 | """ 57 | Create a super user. 58 | 59 | :param email: string 60 | :param first_name: string 61 | :param last_name: string 62 | :param password: string 63 | :param extra_fields: 64 | :return: User 65 | """ 66 | return self._create_user(email, password, first_name, last_name, is_staff=True, is_superuser=True, 67 | **extra_fields) 68 | 69 | 70 | class User(AbstractBaseUser): 71 | """ 72 | Model that represents an user. 73 | 74 | To be active, the user must register and confirm his email. 75 | """ 76 | 77 | GENDER_MALE = 'M' 78 | GENDER_FEMALE = 'F' 79 | GENDER_CHOICES = ( 80 | (GENDER_MALE, 'Male'), 81 | (GENDER_FEMALE, 'Female') 82 | ) 83 | 84 | # we want primary key to be called id so need to ignore pytlint 85 | id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) # pylint: disable=invalid-name 86 | 87 | first_name = models.CharField(_('First Name'), max_length=50) 88 | last_name = models.CharField(_('Last Name'), max_length=50) 89 | email = models.EmailField(_('Email address'), unique=True) 90 | 91 | gender = models.CharField(max_length=1, choices=GENDER_CHOICES, default=GENDER_MALE) 92 | 93 | confirmed_email = models.BooleanField(default=False) 94 | 95 | is_staff = models.BooleanField(_('staff status'), default=False) 96 | is_superuser = models.BooleanField(_('superuser status'), default=False) 97 | is_active = models.BooleanField(_('active'), default=True) 98 | 99 | date_joined = models.DateTimeField(_('date joined'), auto_now_add=True) 100 | date_updated = models.DateTimeField(_('date updated'), auto_now=True) 101 | 102 | activation_key = models.UUIDField(unique=True, default=uuid.uuid4) # email 103 | 104 | USERNAME_FIELD = 'email' 105 | 106 | objects = MyUserManager() 107 | 108 | def __str__(self): 109 | """ 110 | Unicode representation for an user model. 111 | 112 | :return: string 113 | """ 114 | return self.email 115 | 116 | def get_full_name(self): 117 | """ 118 | Return the first_name plus the last_name, with a space in between. 119 | 120 | :return: string 121 | """ 122 | return "{0} {1}".format(self.first_name, self.last_name) 123 | 124 | def get_short_name(self): 125 | """ 126 | Return the first_name. 127 | 128 | :return: string 129 | """ 130 | return self.first_name 131 | 132 | def activation_expired(self): 133 | """ 134 | Check if user's activation has expired. 135 | 136 | :return: boolean 137 | """ 138 | return self.date_joined + timedelta(days=settings.ACCOUNT_ACTIVATION_DAYS) < timezone.now() 139 | 140 | def confirm_email(self): 141 | """ 142 | Confirm email. 143 | 144 | :return: boolean 145 | """ 146 | if not self.activation_expired() and not self.confirmed_email: 147 | self.confirmed_email = True 148 | self.save() 149 | return True 150 | return False 151 | -------------------------------------------------------------------------------- /src/accounts/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from accounts.models import User 4 | from lib.utils import validate_email as email_is_valid 5 | 6 | 7 | class UserSerializer(serializers.ModelSerializer): 8 | class Meta: 9 | model = User 10 | fields = ('email', 'first_name', 'last_name',) 11 | 12 | 13 | class UserRegistrationSerializer(serializers.ModelSerializer): 14 | email = serializers.EmailField() 15 | 16 | class Meta: 17 | model = User 18 | fields = ('id', 'email', 'first_name', 'last_name', 'password') 19 | 20 | def create(self, validated_data): 21 | """ 22 | Create the object. 23 | 24 | :param validated_data: string 25 | """ 26 | user = User.objects.create(**validated_data) 27 | user.set_password(validated_data['password']) 28 | user.save() 29 | return user 30 | 31 | def validate_email(self, value): 32 | """ 33 | Validate if email is valid or there is an user using the email. 34 | 35 | :param value: string 36 | :return: string 37 | """ 38 | 39 | if not email_is_valid(value): 40 | raise serializers.ValidationError('Please use a different email address provider.') 41 | 42 | if User.objects.filter(email=value).exists(): 43 | raise serializers.ValidationError('Email already in use, please use a different email address.') 44 | 45 | return value 46 | -------------------------------------------------------------------------------- /src/accounts/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | from django.utils.translation import ugettext_lazy as _ 3 | 4 | import accounts.views 5 | 6 | urlpatterns = [ 7 | url(_(r'^register/$'), 8 | accounts.views.UserRegisterView.as_view(), 9 | name='register'), 10 | url(_(r'^login/$'), 11 | accounts.views.UserLoginView.as_view(), 12 | name='login'), 13 | url(_(r'^confirm/email/(?P.*)/$'), 14 | accounts.views.UserConfirmEmailView.as_view(), 15 | name='confirm_email'), 16 | url(_(r'^status/email/$'), 17 | accounts.views.UserEmailConfirmationStatusView.as_view(), 18 | name='status'), 19 | 20 | ] 21 | -------------------------------------------------------------------------------- /src/accounts/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import get_object_or_404 2 | from django_rest_logger import log 3 | from knox.auth import TokenAuthentication 4 | from knox.models import AuthToken 5 | from rest_framework import status 6 | from rest_framework.authentication import BasicAuthentication 7 | from rest_framework.generics import GenericAPIView 8 | from rest_framework.mixins import CreateModelMixin 9 | from rest_framework.permissions import IsAuthenticated 10 | from rest_framework.response import Response 11 | 12 | from accounts.models import User 13 | from accounts.serializers import UserRegistrationSerializer, UserSerializer 14 | from lib.utils import AtomicMixin 15 | 16 | 17 | class UserRegisterView(AtomicMixin, CreateModelMixin, GenericAPIView): 18 | serializer_class = UserRegistrationSerializer 19 | authentication_classes = () 20 | 21 | def post(self, request): 22 | """User registration view.""" 23 | return self.create(request) 24 | 25 | 26 | class UserLoginView(GenericAPIView): 27 | serializer_class = UserSerializer 28 | authentication_classes = (BasicAuthentication,) 29 | permission_classes = (IsAuthenticated,) 30 | 31 | def post(self, request): 32 | """User login with username and password.""" 33 | token = AuthToken.objects.create(request.user) 34 | return Response({ 35 | 'user': self.get_serializer(request.user).data, 36 | 'token': token 37 | }) 38 | 39 | 40 | class UserConfirmEmailView(AtomicMixin, GenericAPIView): 41 | serializer_class = None 42 | authentication_classes = () 43 | 44 | def get(self, request, activation_key): 45 | """ 46 | View for confirm email. 47 | 48 | Receive an activation key as parameter and confirm email. 49 | """ 50 | user = get_object_or_404(User, activation_key=str(activation_key)) 51 | if user.confirm_email(): 52 | return Response(status=status.HTTP_200_OK) 53 | 54 | log.warning(message='Email confirmation key not found.', 55 | details={'http_status_code': status.HTTP_404_NOT_FOUND}) 56 | return Response(status=status.HTTP_404_NOT_FOUND) 57 | 58 | 59 | class UserEmailConfirmationStatusView(GenericAPIView): 60 | serializer_class = None 61 | authentication_classes = (TokenAuthentication,) 62 | permission_classes = (IsAuthenticated,) 63 | 64 | def get(self, request): 65 | """Retrieve user current confirmed_email status.""" 66 | user = self.request.user 67 | return Response({'status': user.confirmed_email}, status=status.HTTP_200_OK) 68 | -------------------------------------------------------------------------------- /src/base/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seedstars/django-react-redux-base/a8f9872f20c0fc77b0dcb0386ca7b1643f8f72e1/src/base/__init__.py -------------------------------------------------------------------------------- /src/base/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | 3 | from base import views as base_views 4 | 5 | urlpatterns = [ 6 | url(r'', 7 | base_views.ProtectedDataView.as_view(), 8 | name='protected_data'), 9 | ] 10 | -------------------------------------------------------------------------------- /src/base/views.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.conf import settings 4 | from django.http import HttpResponse 5 | from django.views.generic import View 6 | from knox.auth import TokenAuthentication 7 | from rest_framework import status 8 | from rest_framework.generics import GenericAPIView 9 | from rest_framework.permissions import IsAuthenticated 10 | from rest_framework.response import Response 11 | 12 | 13 | class IndexView(View): 14 | """Render main page.""" 15 | 16 | def get(self, request): 17 | """Return html for main application page.""" 18 | 19 | abspath = open(os.path.join(settings.BASE_DIR, 'static_dist/index.html'), 'r') 20 | return HttpResponse(content=abspath.read()) 21 | 22 | 23 | class ProtectedDataView(GenericAPIView): 24 | """Return protected data main page.""" 25 | 26 | authentication_classes = (TokenAuthentication,) 27 | permission_classes = (IsAuthenticated,) 28 | 29 | def get(self, request): 30 | """Process GET request and return protected data.""" 31 | 32 | data = { 33 | 'data': 'THIS IS THE PROTECTED STRING FROM SERVER', 34 | } 35 | 36 | return Response(data, status=status.HTTP_200_OK) 37 | -------------------------------------------------------------------------------- /src/djangoreactredux/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seedstars/django-react-redux-base/a8f9872f20c0fc77b0dcb0386ca7b1643f8f72e1/src/djangoreactredux/__init__.py -------------------------------------------------------------------------------- /src/djangoreactredux/settings/__init__.py: -------------------------------------------------------------------------------- 1 | from .dev import * 2 | -------------------------------------------------------------------------------- /src/djangoreactredux/settings/base.py: -------------------------------------------------------------------------------- 1 | """Django settings for djangoreactredux project.""" 2 | 3 | import os 4 | 5 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) # remove /sswmain/settings to get base folder 6 | 7 | # SECURITY WARNING: keep the secret key used in production secret! 8 | SECRET_KEY = 'ajsdgas7&*kosdsa21[]jaksdhlka-;kmcv8l$#diepsm8&ah^' 9 | 10 | DEBUG = True 11 | 12 | ALLOWED_HOSTS = ['localhost'] 13 | 14 | # Application definition 15 | 16 | INSTALLED_APPS = [ 17 | 'django.contrib.auth', 18 | 'django.contrib.contenttypes', 19 | 'django.contrib.staticfiles', 20 | 'django.contrib.messages', 21 | 'django.contrib.sessions', 22 | 'django.contrib.admin', 23 | 24 | 'rest_framework', 25 | 'knox', 26 | 'django_extensions', 27 | 28 | 'accounts', 29 | 'base' 30 | ] 31 | 32 | MIDDLEWARE_CLASSES = [ 33 | 'django.middleware.security.SecurityMiddleware', 34 | 'whitenoise.middleware.WhiteNoiseMiddleware', 35 | 'django.middleware.common.CommonMiddleware', 36 | 'django.middleware.csrf.CsrfViewMiddleware', 37 | 'django.contrib.sessions.middleware.SessionMiddleware', 38 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 39 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 40 | 'django.contrib.messages.middleware.MessageMiddleware', 41 | 'django.middleware.common.CommonMiddleware' 42 | ] 43 | 44 | TEMPLATES = [ 45 | { 46 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 47 | 'DIRS': [], 48 | 'APP_DIRS': True, 49 | 'OPTIONS': { 50 | 'context_processors': [ 51 | 'django.template.context_processors.debug', 52 | 'django.template.context_processors.request', 53 | 'django.contrib.auth.context_processors.auth', 54 | 'django.contrib.messages.context_processors.messages', 55 | ], 56 | }, 57 | }, 58 | ] 59 | 60 | ROOT_URLCONF = 'djangoreactredux.urls' 61 | 62 | WSGI_APPLICATION = 'djangoreactredux.wsgi.application' 63 | 64 | LANGUAGE_CODE = 'en-us' 65 | 66 | TIME_ZONE = 'UTC' 67 | 68 | USE_I18N = True 69 | 70 | USE_L10N = True 71 | 72 | USE_TZ = True 73 | 74 | AUTH_USER_MODEL = 'accounts.User' 75 | 76 | ACCOUNT_ACTIVATION_DAYS = 7 # days 77 | 78 | STATIC_URL = '/static/' 79 | STATIC_ROOT = os.path.join(BASE_DIR, 'static_root') 80 | STATICFILES_DIRS = [ 81 | os.path.join(BASE_DIR, 'static_dist'), 82 | ] 83 | 84 | # store static files locally and serve with whitenoise 85 | STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' 86 | 87 | # ############# REST FRAMEWORK ################### 88 | 89 | REST_FRAMEWORK = { 90 | 'DEFAULT_PERMISSION_CLASSES': [], 91 | 'DEFAULT_AUTHENTICATION_CLASSES': [ 92 | 'rest_framework.authentication.SessionAuthentication', 93 | 'rest_framework.authentication.BasicAuthentication', 94 | ], 95 | 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 96 | 'PAGE_SIZE': 20, 97 | 'DEFAULT_PARSER_CLASSES': [ 98 | 'rest_framework.parsers.JSONParser', 99 | 'rest_framework.parsers.FormParser', 100 | 'rest_framework.parsers.MultiPartParser', 101 | ], 102 | } 103 | 104 | # ############ REST KNOX ######################## 105 | REST_KNOX = { 106 | 'SECURE_HASH_ALGORITHM': 'cryptography.hazmat.primitives.hashes.SHA512', 107 | 'AUTH_TOKEN_CHARACTER_LENGTH': 64, 108 | 'USER_SERIALIZER': 'knox.serializers.UserSerializer' 109 | } 110 | -------------------------------------------------------------------------------- /src/djangoreactredux/settings/ci.py: -------------------------------------------------------------------------------- 1 | from djangoreactredux.settings.staging import * # NOQA (ignore all errors on this line) 2 | 3 | DATABASES = { 4 | 'default': { 5 | 'ENGINE': 'django.db.backends.postgresql_psycopg2', 6 | 'NAME': 'travis_ci_test', 7 | 'USER': 'postgres', 8 | 'PASSWORD': '', 9 | 'HOST': 'localhost', 10 | 'PORT': '', 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/djangoreactredux/settings/dev.py: -------------------------------------------------------------------------------- 1 | from djangoreactredux.settings.base import * # NOQA (ignore all errors on this line) 2 | 3 | 4 | DEBUG = True 5 | 6 | PAGE_CACHE_SECONDS = 1 7 | 8 | DATABASES = { 9 | 'default': { 10 | 'ENGINE': 'django.db.backends.sqlite3', 11 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite'), # NOQA (ignore all errors on this line) 12 | } 13 | } 14 | 15 | REST_FRAMEWORK['EXCEPTION_HANDLER'] = 'django_rest_logger.handlers.rest_exception_handler' # NOQA (ignore all errors on this line) 16 | 17 | LOGGING = { 18 | 'version': 1, 19 | 'disable_existing_loggers': True, 20 | 'root': { 21 | 'level': 'DEBUG', 22 | 'handlers': ['django_rest_logger_handler'], 23 | }, 24 | 'formatters': { 25 | 'verbose': { 26 | 'format': '%(levelname)s %(asctime)s %(module)s ' 27 | '%(process)d %(thread)d %(message)s' 28 | }, 29 | }, 30 | 'handlers': { 31 | 'django_rest_logger_handler': { 32 | 'level': 'DEBUG', 33 | 'class': 'logging.StreamHandler', 34 | 'formatter': 'verbose' 35 | } 36 | }, 37 | 'loggers': { 38 | 'django.db.backends': { 39 | 'level': 'ERROR', 40 | 'handlers': ['django_rest_logger_handler'], 41 | 'propagate': False, 42 | }, 43 | 'django_rest_logger': { 44 | 'level': 'DEBUG', 45 | 'handlers': ['django_rest_logger_handler'], 46 | 'propagate': False, 47 | }, 48 | }, 49 | } 50 | 51 | DEFAULT_LOGGER = 'django_rest_logger' 52 | 53 | LOGGER_EXCEPTION = DEFAULT_LOGGER 54 | LOGGER_ERROR = DEFAULT_LOGGER 55 | LOGGER_WARNING = DEFAULT_LOGGER 56 | -------------------------------------------------------------------------------- /src/djangoreactredux/settings/dev_docker.py: -------------------------------------------------------------------------------- 1 | from djangoreactredux.settings.dev import * # NOQA (ignore all errors on this line) 2 | 3 | DATABASES = { 4 | 'default': { 5 | 'ENGINE': 'django.db.backends.postgresql_psycopg2', 6 | 'NAME': 'djangoreactredux_dev', 7 | 'USER': 'djangoreactredux', 8 | 'PASSWORD': 'password', 9 | 'HOST': 'postgres', 10 | 'PORT': 5432, 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/djangoreactredux/settings/prod.py: -------------------------------------------------------------------------------- 1 | from djangoreactredux.settings.base import * # NOQA (ignore all errors on this line) 2 | 3 | 4 | DEBUG = False 5 | TEMPLATE_DEBUG = DEBUG 6 | 7 | PAGE_CACHE_SECONDS = 60 8 | 9 | # TODO: n a real production server this should have a proper url 10 | ALLOWED_HOSTS = ['*'] 11 | 12 | DATABASES = { 13 | 'default': { 14 | 'ENGINE': 'django.db.backends.postgresql_psycopg2', 15 | 'NAME': 'djangoreactredux_prod', 16 | 'USER': 'djangoreactredux', 17 | 'PASSWORD': 'password', 18 | 'HOST': 'postgres', 19 | 'PORT': 5432, 20 | } 21 | } 22 | 23 | REST_FRAMEWORK['EXCEPTION_HANDLER'] = 'django_rest_logger.handlers.rest_exception_handler' # NOQA (ignore all errors on this line) 24 | 25 | # ########### Sentry configuration 26 | 27 | # Change this to proper sentry url. 28 | RAVEN_CONFIG = { 29 | 'dsn': '', 30 | } 31 | 32 | INSTALLED_APPS = INSTALLED_APPS + ( # NOQA (ignore all errors on this line) 33 | 'raven.contrib.django.raven_compat', 34 | ) 35 | 36 | # ####### Logging 37 | 38 | LOGGING = { 39 | 'version': 1, 40 | 'disable_existing_loggers': True, 41 | 'root': { 42 | 'level': 'WARNING', 43 | 'handlers': ['sentry'], 44 | }, 45 | 'formatters': { 46 | 'verbose': { 47 | 'format': '%(levelname)s %(asctime)s %(module)s ' 48 | '%(process)d %(thread)d %(message)s' 49 | }, 50 | }, 51 | 'handlers': { 52 | 'sentry': { 53 | 'level': 'ERROR', 54 | 'class': 'raven.contrib.django.raven_compat.handlers.SentryHandler', 55 | }, 56 | 'console': { 57 | 'level': 'DEBUG', 58 | 'class': 'logging.StreamHandler', 59 | 'formatter': 'verbose' 60 | } 61 | }, 62 | 'loggers': { 63 | 'django.db.backends': { 64 | 'level': 'ERROR', 65 | 'handlers': ['console'], 66 | 'propagate': False, 67 | }, 68 | 'raven': { 69 | 'level': 'DEBUG', 70 | 'handlers': ['sentry'], 71 | 'propagate': False, 72 | }, 73 | 'sentry.errors': { 74 | 'level': 'DEBUG', 75 | 'handlers': ['sentry'], 76 | 'propagate': False, 77 | }, 78 | }, 79 | } 80 | 81 | DEFAULT_LOGGER = 'raven' 82 | 83 | LOGGER_EXCEPTION = DEFAULT_LOGGER 84 | LOGGER_ERROR = DEFAULT_LOGGER 85 | LOGGER_WARNING = DEFAULT_LOGGER 86 | -------------------------------------------------------------------------------- /src/djangoreactredux/settings/staging.py: -------------------------------------------------------------------------------- 1 | from djangoreactredux.settings.prod import * # NOQA (ignore all errors on this line) 2 | -------------------------------------------------------------------------------- /src/djangoreactredux/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.conf.urls import include, url 3 | from django.views.decorators.cache import cache_page 4 | 5 | from base import views as base_views 6 | 7 | urlpatterns = [ 8 | url(r'^api/v1/accounts/', include('accounts.urls', namespace='accounts')), 9 | url(r'^api/v1/getdata/', include('base.urls', namespace='base')), 10 | 11 | # catch all others because of how history is handled by react router - cache this page because it will never change 12 | url(r'', cache_page(settings.PAGE_CACHE_SECONDS)(base_views.IndexView.as_view()), name='index'), 13 | ] 14 | -------------------------------------------------------------------------------- /src/djangoreactredux/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for django-react-redux-base project. 3 | 4 | """ 5 | import os 6 | 7 | from django.core.wsgi import get_wsgi_application 8 | 9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djangoreactredux.settings") 10 | 11 | application = get_wsgi_application() 12 | -------------------------------------------------------------------------------- /src/fixtures.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "model": "accounts.user", 4 | "fields": { 5 | "last_name": "LastName", 6 | "last_login": "2015-12-29T00:57:23.201Z", 7 | "date_joined": "2015-12-29T00:57:23.201Z", 8 | "date_updated": "2015-12-29T00:57:23.201Z", 9 | "confirmed_email": false, 10 | "first_name": "FirstName", 11 | "email": "a@a.com", 12 | "gender": "M", 13 | "is_active": true, 14 | "password": "pbkdf2_sha256$20000$fSnaTbagnHiD$lZ5k1ZHDNM4oZir2IrWIrcRh0bEh4wYu+uuhh4ZZ9ZQ=", 15 | "is_staff": true, 16 | "is_superuser": true, 17 | "activation_key": "64f5b041-81ed-4251-bf62-54b9c0a14848" 18 | }, 19 | "pk": "7c32d60d-6312-42f6-a73a-22da56b07374" 20 | } 21 | ] 22 | -------------------------------------------------------------------------------- /src/lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seedstars/django-react-redux-base/a8f9872f20c0fc77b0dcb0386ca7b1643f8f72e1/src/lib/__init__.py -------------------------------------------------------------------------------- /src/lib/testutils.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | 4 | class CustomTestCase(TestCase): 5 | def assert_fields_required(self, required, form, list_fields): 6 | """ 7 | Check if a list of fields and required or not required in a form. 8 | 9 | :param required: True|False 10 | :param form: form to be checked 11 | :param list_fields: list of fields to check 12 | """ 13 | for field in list_fields: 14 | if required: 15 | self.assertTrue(form.fields[field].required, 'FIELD:%s' % field) 16 | else: 17 | self.assertFalse(form.fields[field].required, 'FIELD:%s' % field) 18 | 19 | def assert_invalid_data(self, form, invalid_data_dicts): 20 | """ 21 | Check invalid data errors in a form. 22 | 23 | :param form: 24 | :param invalid_data_dicts: 25 | :return: 26 | """ 27 | for invalid_dict in invalid_data_dicts: 28 | form_data = form(data=invalid_dict['data']) 29 | self.assertFalse(form_data.is_valid()) 30 | self.assertEqual(form_data.errors[invalid_dict['error'][0]], invalid_dict['error'][1], 31 | msg=invalid_dict['label']) 32 | 33 | def assert_valid_data(self, form, valid_data_dicts): 34 | """ 35 | Check valid data in a form. 36 | 37 | :param form: 38 | :param valid_data_dicts: 39 | :return: 40 | """ 41 | for valid_data in valid_data_dicts: 42 | form_data = form(data=valid_data) 43 | self.assertTrue(form_data.is_valid()) 44 | 45 | def assert_invalid_data_response(self, url, invalid_data_dicts): 46 | """ 47 | Check invalid data response status. 48 | 49 | :param url: 50 | :param invalid_data_dicts: 51 | :return: 52 | """ 53 | for invalid_dict in invalid_data_dicts: 54 | if invalid_dict['method'] == 'POST': 55 | response = self.client.post(url, data=invalid_dict['data'], format='json') 56 | elif invalid_dict['method'] == 'GET': 57 | response = self.client.get(url, data=invalid_dict['data'], format='json') 58 | else: 59 | print('Implement other methods.') # pragma: no cover 60 | error_msg = '{}-{}-{}'.format(invalid_dict['label'], response.status_code, response.content) 61 | self.assertEqual(response.status_code, invalid_dict['status'], msg=error_msg) 62 | -------------------------------------------------------------------------------- /src/lib/utils.py: -------------------------------------------------------------------------------- 1 | from disposable_email_checker.validators import validate_disposable_email 2 | from django.core.exceptions import ValidationError 3 | from django.core.validators import validate_email as django_validate_email 4 | from django.db import transaction 5 | 6 | 7 | def validate_email(value): 8 | """Validate a single email.""" 9 | if not value: 10 | return False 11 | # Check the regex, using the validate_email from django. 12 | try: 13 | django_validate_email(value) 14 | except ValidationError: 15 | return False 16 | else: 17 | # Check with the disposable list. 18 | try: 19 | validate_disposable_email(value) 20 | except ValidationError: 21 | return False 22 | else: 23 | return True 24 | 25 | 26 | class AtomicMixin(object): 27 | """ 28 | Ensure we rollback db transactions on exceptions. 29 | 30 | From https://gist.github.com/adamJLev/7e9499ba7e436535fd94 31 | """ 32 | 33 | @transaction.atomic() 34 | def dispatch(self, *args, **kwargs): 35 | """Atomic transaction.""" 36 | return super(AtomicMixin, self).dispatch(*args, **kwargs) 37 | 38 | def handle_exception(self, *args, **kwargs): 39 | """Handle exception with transaction rollback.""" 40 | response = super(AtomicMixin, self).handle_exception(*args, **kwargs) 41 | 42 | if getattr(response, 'exception'): 43 | # We've suppressed the exception but still need to rollback any transaction. 44 | transaction.set_rollback(True) 45 | 46 | return response 47 | -------------------------------------------------------------------------------- /src/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djangoreactredux.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /src/static/actions/auth.js: -------------------------------------------------------------------------------- 1 | import fetch from 'isomorphic-fetch'; 2 | import { push } from 'react-router-redux'; 3 | import { SERVER_URL } from '../utils/config'; 4 | import { checkHttpStatus, parseJSON } from '../utils'; 5 | import { 6 | AUTH_LOGIN_USER_REQUEST, 7 | AUTH_LOGIN_USER_FAILURE, 8 | AUTH_LOGIN_USER_SUCCESS, 9 | AUTH_LOGOUT_USER 10 | } from '../constants'; 11 | 12 | 13 | export function authLoginUserSuccess(token, user) { 14 | sessionStorage.setItem('token', token); 15 | sessionStorage.setItem('user', JSON.stringify(user)); 16 | return { 17 | type: AUTH_LOGIN_USER_SUCCESS, 18 | payload: { 19 | token, 20 | user 21 | } 22 | }; 23 | } 24 | 25 | export function authLoginUserFailure(error, message) { 26 | sessionStorage.removeItem('token'); 27 | return { 28 | type: AUTH_LOGIN_USER_FAILURE, 29 | payload: { 30 | status: error, 31 | statusText: message 32 | } 33 | }; 34 | } 35 | 36 | export function authLoginUserRequest() { 37 | return { 38 | type: AUTH_LOGIN_USER_REQUEST 39 | }; 40 | } 41 | 42 | export function authLogout() { 43 | sessionStorage.removeItem('token'); 44 | sessionStorage.removeItem('user'); 45 | return { 46 | type: AUTH_LOGOUT_USER 47 | }; 48 | } 49 | 50 | export function authLogoutAndRedirect() { 51 | return (dispatch, state) => { 52 | dispatch(authLogout()); 53 | dispatch(push('/login')); 54 | return Promise.resolve(); // TODO: we need a promise here because of the tests, find a better way 55 | }; 56 | } 57 | 58 | export function authLoginUser(email, password, redirect = '/') { 59 | return (dispatch) => { 60 | dispatch(authLoginUserRequest()); 61 | const auth = btoa(`${email}:${password}`); 62 | return fetch(`${SERVER_URL}/api/v1/accounts/login/`, { 63 | method: 'post', 64 | headers: { 65 | 'Accept': 'application/json', 66 | 'Content-Type': 'application/json', 67 | 'Authorization': `Basic ${auth}` 68 | } 69 | }) 70 | .then(checkHttpStatus) 71 | .then(parseJSON) 72 | .then((response) => { 73 | dispatch(authLoginUserSuccess(response.token, response.user)); 74 | dispatch(push(redirect)); 75 | }) 76 | .catch((error) => { 77 | if (error && typeof error.response !== 'undefined' && error.response.status === 401) { 78 | // Invalid authentication credentials 79 | return error.response.json().then((data) => { 80 | dispatch(authLoginUserFailure(401, data.non_field_errors[0])); 81 | }); 82 | } else if (error && typeof error.response !== 'undefined' && error.response.status >= 500) { 83 | // Server side error 84 | dispatch(authLoginUserFailure(500, 'A server error occurred while sending your data!')); 85 | } else { 86 | // Most likely connection issues 87 | dispatch(authLoginUserFailure('Connection Error', 'An error occurred while sending your data!')); 88 | } 89 | 90 | return Promise.resolve(); // TODO: we need a promise here because of the tests, find a better way 91 | }); 92 | }; 93 | } 94 | -------------------------------------------------------------------------------- /src/static/actions/data.js: -------------------------------------------------------------------------------- 1 | import fetch from 'isomorphic-fetch'; 2 | import { push } from 'react-router-redux'; 3 | 4 | import { SERVER_URL } from '../utils/config'; 5 | import { checkHttpStatus, parseJSON } from '../utils'; 6 | import { DATA_FETCH_PROTECTED_DATA_REQUEST, DATA_RECEIVE_PROTECTED_DATA } from '../constants'; 7 | import { authLoginUserFailure } from './auth'; 8 | 9 | 10 | export function dataReceiveProtectedData(data) { 11 | return { 12 | type: DATA_RECEIVE_PROTECTED_DATA, 13 | payload: { 14 | data 15 | } 16 | }; 17 | } 18 | 19 | export function dataFetchProtectedDataRequest() { 20 | return { 21 | type: DATA_FETCH_PROTECTED_DATA_REQUEST 22 | }; 23 | } 24 | 25 | export function dataFetchProtectedData(token) { 26 | return (dispatch, state) => { 27 | dispatch(dataFetchProtectedDataRequest()); 28 | return fetch(`${SERVER_URL}/api/v1/getdata/`, { 29 | credentials: 'include', 30 | headers: { 31 | Accept: 'application/json', 32 | Authorization: `Token ${token}` 33 | } 34 | }) 35 | .then(checkHttpStatus) 36 | .then(parseJSON) 37 | .then((response) => { 38 | dispatch(dataReceiveProtectedData(response.data)); 39 | }) 40 | .catch((error) => { 41 | if (error && typeof error.response !== 'undefined' && error.response.status === 401) { 42 | // Invalid authentication credentials 43 | return error.response.json().then((data) => { 44 | dispatch(authLoginUserFailure(401, data.non_field_errors[0])); 45 | dispatch(push('/login')); 46 | }); 47 | } else if (error && typeof error.response !== 'undefined' && error.response.status >= 500) { 48 | // Server side error 49 | dispatch(authLoginUserFailure(500, 'A server error occurred while sending your data!')); 50 | } else { 51 | // Most likely connection issues 52 | dispatch(authLoginUserFailure('Connection Error', 'An error occurred while sending your data!')); 53 | } 54 | 55 | dispatch(push('/login')); 56 | return Promise.resolve(); // TODO: we need a promise here because of the tests, find a better way 57 | }); 58 | }; 59 | } 60 | -------------------------------------------------------------------------------- /src/static/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { push } from 'react-router-redux'; 4 | import classNames from 'classnames'; 5 | import PropTypes from 'prop-types'; 6 | 7 | import { authLogoutAndRedirect } from './actions/auth'; 8 | import './styles/main.scss'; 9 | 10 | class App extends React.Component { 11 | static propTypes = { 12 | isAuthenticated: PropTypes.bool.isRequired, 13 | children: PropTypes.shape().isRequired, 14 | dispatch: PropTypes.func.isRequired, 15 | location: PropTypes.shape({ 16 | pathname: PropTypes.string 17 | }) 18 | }; 19 | 20 | static defaultProps = { 21 | location: undefined 22 | }; 23 | 24 | logout = () => { 25 | this.props.dispatch(authLogoutAndRedirect()); 26 | }; 27 | 28 | goToIndex = () => { 29 | this.props.dispatch(push('/')); 30 | }; 31 | 32 | goToLogin = () => { 33 | this.props.dispatch(push('/login')); 34 | }; 35 | 36 | goToProtected = () => { 37 | this.props.dispatch(push('/protected')); 38 | }; 39 | 40 | render() { 41 | const homeClass = classNames({ 42 | active: this.props.location && this.props.location.pathname === '/' 43 | }); 44 | const protectedClass = classNames({ 45 | active: this.props.location && this.props.location.pathname === '/protected' 46 | }); 47 | const loginClass = classNames({ 48 | active: this.props.location && this.props.location.pathname === '/login' 49 | }); 50 | 51 | return ( 52 |
53 | 107 | 108 |
109 | {this.props.children} 110 |
111 |
112 | ); 113 | } 114 | } 115 | 116 | const mapStateToProps = (state, ownProps) => { 117 | return { 118 | isAuthenticated: state.auth.isAuthenticated, 119 | location: state.routing.location 120 | }; 121 | }; 122 | 123 | export default connect(mapStateToProps)(App); 124 | export { App as AppNotConnected }; 125 | -------------------------------------------------------------------------------- /src/static/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seedstars/django-react-redux-base/a8f9872f20c0fc77b0dcb0386ca7b1643f8f72e1/src/static/components/.gitkeep -------------------------------------------------------------------------------- /src/static/constants/index.js: -------------------------------------------------------------------------------- 1 | export const AUTH_LOGIN_USER_REQUEST = 'AUTH_LOGIN_USER_REQUEST'; 2 | export const AUTH_LOGIN_USER_FAILURE = 'AUTH_LOGIN_USER_FAILURE'; 3 | export const AUTH_LOGIN_USER_SUCCESS = 'AUTH_LOGIN_USER_SUCCESS'; 4 | export const AUTH_LOGOUT_USER = 'AUTH_LOGOUT_USER'; 5 | 6 | export const DATA_FETCH_PROTECTED_DATA_REQUEST = 'DATA_FETCH_PROTECTED_DATA_REQUEST'; 7 | export const DATA_RECEIVE_PROTECTED_DATA = 'DATA_RECEIVE_PROTECTED_DATA'; 8 | -------------------------------------------------------------------------------- /src/static/containers/Home/images/react-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seedstars/django-react-redux-base/a8f9872f20c0fc77b0dcb0386ca7b1643f8f72e1/src/static/containers/Home/images/react-logo.png -------------------------------------------------------------------------------- /src/static/containers/Home/images/redux-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seedstars/django-react-redux-base/a8f9872f20c0fc77b0dcb0386ca7b1643f8f72e1/src/static/containers/Home/images/redux-logo.png -------------------------------------------------------------------------------- /src/static/containers/Home/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { push } from 'react-router-redux'; 3 | import { connect } from 'react-redux'; 4 | import PropTypes from 'prop-types'; 5 | 6 | import './style.scss'; 7 | import reactLogo from './images/react-logo.png'; 8 | import reduxLogo from './images/redux-logo.png'; 9 | 10 | class HomeView extends React.Component { 11 | static propTypes = { 12 | statusText: PropTypes.string, 13 | userName: PropTypes.string, 14 | dispatch: PropTypes.func.isRequired 15 | }; 16 | 17 | static defaultProps = { 18 | statusText: '', 19 | userName: '' 20 | }; 21 | 22 | goToProtected = () => { 23 | this.props.dispatch(push('/protected')); 24 | }; 25 | 26 | render() { 27 | return ( 28 |
29 |
30 | ReactJs 34 | Redux 38 |
39 |
40 |

Django React Redux Demo

41 |

Hello, {this.props.userName || 'guest'}.

42 |
43 |
44 |

Attempt to access some protected content.

45 |
46 |
47 | {this.props.statusText ? 48 |
49 | {this.props.statusText} 50 |
51 | : 52 | null 53 | } 54 |
55 |
56 | ); 57 | } 58 | } 59 | 60 | const mapStateToProps = (state) => { 61 | return { 62 | userName: state.auth.userName, 63 | statusText: state.auth.statusText 64 | }; 65 | }; 66 | 67 | export default connect(mapStateToProps)(HomeView); 68 | export { HomeView as HomeViewNotConnected }; 69 | -------------------------------------------------------------------------------- /src/static/containers/Home/style.scss: -------------------------------------------------------------------------------- 1 | .page-logo { 2 | max-width: 200px; 3 | } -------------------------------------------------------------------------------- /src/static/containers/Login/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { bindActionCreators } from 'redux'; 3 | import { connect } from 'react-redux'; 4 | import classNames from 'classnames'; 5 | import { push } from 'react-router-redux'; 6 | import t from 'tcomb-form'; 7 | import PropTypes from 'prop-types'; 8 | 9 | import * as actionCreators from '../../actions/auth'; 10 | 11 | const Form = t.form.Form; 12 | 13 | const Login = t.struct({ 14 | email: t.String, 15 | password: t.String 16 | }); 17 | 18 | const LoginFormOptions = { 19 | auto: 'placeholders', 20 | help: Hint: a@a.com / qw, 21 | fields: { 22 | password: { 23 | type: 'password' 24 | } 25 | } 26 | }; 27 | 28 | class LoginView extends React.Component { 29 | static propTypes = { 30 | dispatch: PropTypes.func.isRequired, 31 | isAuthenticated: PropTypes.bool.isRequired, 32 | isAuthenticating: PropTypes.bool.isRequired, 33 | statusText: PropTypes.string, 34 | actions: PropTypes.shape({ 35 | authLoginUser: PropTypes.func.isRequired 36 | }).isRequired, 37 | location: PropTypes.shape({ 38 | search: PropTypes.string.isRequired 39 | }) 40 | }; 41 | 42 | static defaultProps = { 43 | statusText: '', 44 | location: null 45 | }; 46 | 47 | constructor(props) { 48 | super(props); 49 | 50 | const redirectRoute = this.props.location ? this.extractRedirect(this.props.location.search) || '/' : '/'; 51 | this.state = { 52 | formValues: { 53 | email: '', 54 | password: '' 55 | }, 56 | redirectTo: redirectRoute 57 | }; 58 | } 59 | 60 | componentWillMount() { 61 | if (this.props.isAuthenticated) { 62 | this.props.dispatch(push('/')); 63 | } 64 | } 65 | 66 | onFormChange = (value) => { 67 | this.setState({ formValues: value }); 68 | }; 69 | 70 | extractRedirect = (string) => { 71 | const match = string.match(/next=(.*)/); 72 | return match ? match[1] : '/'; 73 | }; 74 | 75 | login = (e) => { 76 | e.preventDefault(); 77 | const value = this.loginForm.getValue(); 78 | if (value) { 79 | this.props.actions.authLoginUser(value.email, value.password, this.state.redirectTo); 80 | } 81 | }; 82 | 83 | render() { 84 | let statusText = null; 85 | if (this.props.statusText) { 86 | const statusTextClassNames = classNames({ 87 | 'alert': true, 88 | 'alert-danger': this.props.statusText.indexOf('Authentication Error') === 0, 89 | 'alert-success': this.props.statusText.indexOf('Authentication Error') !== 0 90 | }); 91 | 92 | statusText = ( 93 |
94 |
95 |
96 | {this.props.statusText} 97 |
98 |
99 |
100 | ); 101 | } 102 | 103 | return ( 104 |
105 |

Login

106 |
107 | {statusText} 108 |
109 | { this.loginForm = ref; }} 110 | type={Login} 111 | options={LoginFormOptions} 112 | value={this.state.formValues} 113 | onChange={this.onFormChange} 114 | /> 115 | 121 |
122 |
123 |
124 | ); 125 | } 126 | } 127 | 128 | const mapStateToProps = (state) => { 129 | return { 130 | isAuthenticated: state.auth.isAuthenticated, 131 | isAuthenticating: state.auth.isAuthenticating, 132 | statusText: state.auth.statusText 133 | }; 134 | }; 135 | 136 | const mapDispatchToProps = (dispatch) => { 137 | return { 138 | dispatch, 139 | actions: bindActionCreators(actionCreators, dispatch) 140 | }; 141 | }; 142 | 143 | export default connect(mapStateToProps, mapDispatchToProps)(LoginView); 144 | export { LoginView as LoginViewNotConnected }; 145 | -------------------------------------------------------------------------------- /src/static/containers/NotFound/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class NotFoundView extends React.Component { 4 | render() { 5 | return ( 6 |
7 |

NOT FOUND

8 |
9 | ); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/static/containers/Protected/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { bindActionCreators } from 'redux'; 4 | import PropTypes from 'prop-types'; 5 | 6 | import * as actionCreators from '../../actions/data'; 7 | 8 | class ProtectedView extends React.Component { 9 | static propTypes = { 10 | isFetching: PropTypes.bool.isRequired, 11 | data: PropTypes.string, 12 | token: PropTypes.string.isRequired, 13 | actions: PropTypes.shape({ 14 | dataFetchProtectedData: PropTypes.func.isRequired 15 | }).isRequired 16 | }; 17 | 18 | static defaultProps = { 19 | data: '' 20 | }; 21 | 22 | // Note: have to use componentWillMount, if I add this in constructor will get error: 23 | // Warning: setState(...): Cannot update during an existing state transition (such as within `render`). 24 | // Render methods should be a pure function of props and state. 25 | componentWillMount() { 26 | const token = this.props.token; 27 | this.props.actions.dataFetchProtectedData(token); 28 | } 29 | 30 | render() { 31 | return ( 32 |
33 |
34 |

Protected

35 | {this.props.isFetching === true ? 36 |

Loading data...

37 | : 38 |
39 |

Data received from the server:

40 |
41 |
42 | {this.props.data} 43 |
44 |
45 |
46 |
How does this work?
47 |

48 | On the componentWillMount method of the 49 |  ProtectedView component, the action 50 |  dataFetchProtectedData is called. This action will first 51 | dispatch a DATA_FETCH_PROTECTED_DATA_REQUEST action to the Redux 52 | store. When an action is dispatched to the store, an appropriate reducer for 53 | that specific action will change the state of the store. After that it will then 54 | make an asynchronous request to the server using 55 | the isomorphic-fetch library. On its 56 | response, it will dispatch the DATA_RECEIVE_PROTECTED_DATA action 57 | to the Redux store. In case of wrong credentials in the request, the  58 | AUTH_LOGIN_USER_FAILURE action will be dispatched. 59 |

60 |

61 | Because the ProtectedView is connected to the Redux store, when the 62 | value of a property connected to the view is changed, the view is re-rendered 63 | with the new data. 64 |

65 |
66 |
67 | } 68 |
69 |
70 | ); 71 | } 72 | } 73 | 74 | const mapStateToProps = (state) => { 75 | return { 76 | data: state.data.data, 77 | isFetching: state.data.isFetching 78 | }; 79 | }; 80 | 81 | const mapDispatchToProps = (dispatch) => { 82 | return { 83 | actions: bindActionCreators(actionCreators, dispatch) 84 | }; 85 | }; 86 | 87 | export default connect(mapStateToProps, mapDispatchToProps)(ProtectedView); 88 | export { ProtectedView as ProtectedViewNotConnected }; 89 | -------------------------------------------------------------------------------- /src/static/containers/Root/DevTools.js: -------------------------------------------------------------------------------- 1 | /* eslint import/no-extraneous-dependencies: ["error", {"devDependencies": true}] */ 2 | 3 | import React from 'react'; 4 | import { createDevTools } from 'redux-devtools'; 5 | 6 | // Monitors are separate packages, and you can make a custom one 7 | import LogMonitor from 'redux-devtools-log-monitor'; 8 | import DockMonitor from 'redux-devtools-dock-monitor'; 9 | 10 | // createDevTools takes a monitor and produces a DevTools component 11 | const DevTools = createDevTools( 12 | // Monitors are individually adjustable with props. 13 | // Consult their repositories to learn about those props. 14 | // Here, we put LogMonitor inside a DockMonitor. 15 | 19 | 20 | 21 | ); 22 | 23 | export default DevTools; 24 | -------------------------------------------------------------------------------- /src/static/containers/Root/Root.dev.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import { ConnectedRouter } from 'react-router-redux'; 4 | import PropTypes from 'prop-types'; 5 | 6 | import routes from '../../routes'; 7 | import DevTools from './DevTools'; 8 | import App from '../../app'; 9 | 10 | 11 | export default class Root extends React.Component { 12 | static propTypes = { 13 | store: PropTypes.shape().isRequired, 14 | history: PropTypes.shape().isRequired 15 | }; 16 | 17 | render() { 18 | return ( 19 |
20 | 21 |
22 | 23 | 24 | {routes} 25 | 26 | 27 | 28 |
29 |
30 |
31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/static/containers/Root/Root.js: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV === 'production') { 2 | module.exports = require('./Root.prod'); // eslint-disable-line global-require 3 | } else { 4 | module.exports = require('./Root.dev'); // eslint-disable-line global-require 5 | } 6 | -------------------------------------------------------------------------------- /src/static/containers/Root/Root.prod.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import { ConnectedRouter } from 'react-router-redux'; 4 | import PropTypes from 'prop-types'; 5 | 6 | import routes from '../../routes'; 7 | import App from '../../app'; 8 | 9 | export default class Root extends React.Component { 10 | static propTypes = { 11 | store: PropTypes.shape().isRequired, 12 | history: PropTypes.shape().isRequired 13 | }; 14 | 15 | render() { 16 | return ( 17 |
18 | 19 | 20 | 21 | {routes} 22 | 23 | 24 | 25 |
26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/static/containers/index.js: -------------------------------------------------------------------------------- 1 | export HomeView from './Home/index'; 2 | export LoginView from './Login/index'; 3 | export ProtectedView from './Protected/index'; 4 | export NotFoundView from './NotFound/index'; 5 | -------------------------------------------------------------------------------- /src/static/fonts/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seedstars/django-react-redux-base/a8f9872f20c0fc77b0dcb0386ca7b1643f8f72e1/src/static/fonts/.keep -------------------------------------------------------------------------------- /src/static/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seedstars/django-react-redux-base/a8f9872f20c0fc77b0dcb0386ca7b1643f8f72e1/src/static/images/.keep -------------------------------------------------------------------------------- /src/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Django React Redux Demo 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /src/static/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import createHistory from 'history/createBrowserHistory'; 4 | 5 | import { authLoginUserSuccess } from './actions/auth'; 6 | import Root from './containers/Root/Root'; 7 | import configureStore from './store/configureStore'; 8 | 9 | 10 | const initialState = {}; 11 | const target = document.getElementById('root'); 12 | 13 | const history = createHistory(); 14 | const store = configureStore(initialState, history); 15 | 16 | const node = ( 17 | 18 | ); 19 | 20 | const token = sessionStorage.getItem('token'); 21 | let user = {}; 22 | try { 23 | user = JSON.parse(sessionStorage.getItem('user')); 24 | } catch (e) { 25 | // Failed to parse 26 | } 27 | 28 | if (token !== null) { 29 | store.dispatch(authLoginUserSuccess(token, user)); 30 | } 31 | 32 | ReactDOM.render(node, target); 33 | -------------------------------------------------------------------------------- /src/static/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | 'postcss-import': {}, 4 | 'autoprefixer': { browsers: ['last 2 versions'] } 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /src/static/reducers/auth.js: -------------------------------------------------------------------------------- 1 | import { 2 | AUTH_LOGIN_USER_REQUEST, 3 | AUTH_LOGIN_USER_SUCCESS, 4 | AUTH_LOGIN_USER_FAILURE, 5 | AUTH_LOGOUT_USER 6 | } from '../constants'; 7 | 8 | 9 | const initialState = { 10 | token: null, 11 | userName: null, 12 | isAuthenticated: false, 13 | isAuthenticating: false, 14 | statusText: null 15 | }; 16 | 17 | export default function authReducer(state = initialState, action) { 18 | switch (action.type) { 19 | case AUTH_LOGIN_USER_REQUEST: 20 | return Object.assign({}, state, { 21 | isAuthenticating: true, 22 | statusText: null 23 | }); 24 | 25 | case AUTH_LOGIN_USER_SUCCESS: 26 | return Object.assign({}, state, { 27 | isAuthenticating: false, 28 | isAuthenticated: true, 29 | token: action.payload.token, 30 | userName: action.payload.user.email, 31 | statusText: 'You have been successfully logged in.' 32 | }); 33 | 34 | case AUTH_LOGIN_USER_FAILURE: 35 | return Object.assign({}, state, { 36 | isAuthenticating: false, 37 | isAuthenticated: false, 38 | token: null, 39 | userName: null, 40 | statusText: `Authentication Error: ${action.payload.status} - ${action.payload.statusText}` 41 | }); 42 | 43 | case AUTH_LOGOUT_USER: 44 | return Object.assign({}, state, { 45 | isAuthenticated: false, 46 | token: null, 47 | userName: null, 48 | statusText: 'You have been successfully logged out.' 49 | }); 50 | 51 | default: 52 | return state; 53 | } 54 | } 55 | 56 | -------------------------------------------------------------------------------- /src/static/reducers/data.js: -------------------------------------------------------------------------------- 1 | import { 2 | DATA_RECEIVE_PROTECTED_DATA, 3 | DATA_FETCH_PROTECTED_DATA_REQUEST 4 | } from '../constants'; 5 | 6 | const initialState = { 7 | data: null, 8 | isFetching: false 9 | }; 10 | 11 | export default function dataReducer(state = initialState, action) { 12 | switch (action.type) { 13 | case DATA_RECEIVE_PROTECTED_DATA: 14 | return Object.assign({}, state, { 15 | data: action.payload.data, 16 | isFetching: false 17 | }); 18 | 19 | case DATA_FETCH_PROTECTED_DATA_REQUEST: 20 | return Object.assign({}, state, { 21 | isFetching: true 22 | }); 23 | default: 24 | return state; 25 | } 26 | } 27 | 28 | -------------------------------------------------------------------------------- /src/static/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { routerReducer } from 'react-router-redux'; 3 | import authReducer from './auth'; 4 | import dataReducer from './data'; 5 | 6 | export default combineReducers({ 7 | auth: authReducer, 8 | data: dataReducer, 9 | routing: routerReducer 10 | }); 11 | -------------------------------------------------------------------------------- /src/static/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Switch } from 'react-router'; 3 | import { HomeView, LoginView, ProtectedView, NotFoundView } from './containers'; 4 | import requireAuthentication from './utils/requireAuthentication'; 5 | 6 | export default( 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | ); 15 | -------------------------------------------------------------------------------- /src/static/store/configureStore.dev.js: -------------------------------------------------------------------------------- 1 | /* eslint import/no-extraneous-dependencies: ["error", {"devDependencies": true}] */ 2 | 3 | import thunk from 'redux-thunk'; 4 | import { createLogger } from 'redux-logger'; 5 | 6 | import { createStore, applyMiddleware, compose } from 'redux'; 7 | import { routerMiddleware } from 'react-router-redux'; 8 | 9 | import rootReducer from '../reducers'; 10 | import DevTools from '../containers/Root/DevTools'; 11 | 12 | export default function configureStore(initialState, history) { 13 | const logger = createLogger(); 14 | 15 | // Build the middleware for intercepting and dispatching navigation actions 16 | const reduxRouterMiddleware = routerMiddleware(history); 17 | 18 | const middleware = applyMiddleware(thunk, logger, reduxRouterMiddleware); 19 | 20 | const middlewareWithDevTools = compose( 21 | middleware, 22 | DevTools.instrument() 23 | ); 24 | 25 | // Add the reducer to your store on the `router` key 26 | // Also apply our middleware for navigating 27 | const store = createStore(rootReducer, initialState, middlewareWithDevTools); 28 | 29 | if (module.hot) { 30 | module.hot 31 | .accept('../reducers', () => { 32 | const nextRootReducer = require('../reducers/index'); // eslint-disable-line global-require 33 | 34 | store.replaceReducer(nextRootReducer); 35 | }); 36 | } 37 | 38 | return store; 39 | } 40 | -------------------------------------------------------------------------------- /src/static/store/configureStore.js: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV === 'production') { 2 | module.exports = require('./configureStore.prod'); // eslint-disable-line global-require 3 | } else { 4 | module.exports = require('./configureStore.dev'); // eslint-disable-line global-require 5 | } 6 | -------------------------------------------------------------------------------- /src/static/store/configureStore.prod.js: -------------------------------------------------------------------------------- 1 | import thunk from 'redux-thunk'; 2 | import { applyMiddleware, createStore } from 'redux'; 3 | import { routerMiddleware } from 'react-router-redux'; 4 | 5 | import rootReducer from '../reducers'; 6 | 7 | export default function configureStore(initialState, history) { 8 | // Add so dispatched route actions to the history 9 | const reduxRouterMiddleware = routerMiddleware(history); 10 | 11 | const middleware = applyMiddleware(thunk, reduxRouterMiddleware); 12 | 13 | return createStore(rootReducer, initialState, middleware); 14 | } 15 | -------------------------------------------------------------------------------- /src/static/styles/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seedstars/django-react-redux-base/a8f9872f20c0fc77b0dcb0386ca7b1643f8f72e1/src/static/styles/components/.gitkeep -------------------------------------------------------------------------------- /src/static/styles/config/_colors.scss: -------------------------------------------------------------------------------- 1 | $color-white: #ffffff; 2 | 3 | $color-silver-dark: #828282; 4 | $color-silver: #a5a5a5; 5 | $color-silver-light: #f8f8f8; 6 | 7 | $color-red-dark: #d45161; 8 | $color-red: #f27d72; 9 | $color-red-light: #ffafaf; 10 | $color-red-light-2: #ffc5c5; 11 | 12 | $color-green-default: #16967a; 13 | $color-green: #89d78f; 14 | $color-green-light: #b3ffd8; 15 | 16 | $color-blue: #35A1FB; -------------------------------------------------------------------------------- /src/static/styles/config/_fonts.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seedstars/django-react-redux-base/a8f9872f20c0fc77b0dcb0386ca7b1643f8f72e1/src/static/styles/config/_fonts.scss -------------------------------------------------------------------------------- /src/static/styles/config/_reset.scss: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, 7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 8 | a, abbr, acronym, address, big, cite, code, 9 | del, dfn, em, img, ins, kbd, q, s, samp, 10 | small, strike, strong, sub, sup, tt, var, 11 | b, u, i, center, 12 | dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, 14 | table, caption, tbody, tfoot, thead, tr, th, td, 15 | article, aside, canvas, details, embed, 16 | figure, figcaption, footer, header, hgroup, 17 | menu, nav, output, ruby, section, summary, 18 | time, mark, audio, video { 19 | margin: 0; 20 | padding: 0; 21 | border: none; 22 | font-size: 100%; 23 | font: inherit; 24 | vertical-align: baseline; 25 | } 26 | /* HTML5 display-role reset for older browsers */ 27 | article, aside, details, figcaption, figure, 28 | footer, header, hgroup, menu, nav, section { 29 | display: block; 30 | } 31 | body { 32 | line-height: 1; 33 | } 34 | ol, ul { 35 | list-style: none; 36 | } 37 | blockquote, q { 38 | quotes: none; 39 | } 40 | blockquote:before, blockquote:after, 41 | q:before, q:after { 42 | content: ""; 43 | content: none; 44 | } 45 | table { 46 | border-collapse: collapse; 47 | border-spacing: 0; 48 | } 49 | a{ 50 | text-decoration: none; 51 | } 52 | -------------------------------------------------------------------------------- /src/static/styles/config/_typography.scss: -------------------------------------------------------------------------------- 1 | h1 { 2 | font-family: $font-family-regular; 3 | font-size: 3rem; 4 | color: $color-silver-dark; 5 | padding-bottom: .6rem; 6 | } 7 | 8 | h2 { 9 | font-family: $font-family-regular; 10 | font-size: 2.5rem; 11 | color: $color-silver-dark; 12 | } 13 | 14 | h3 { 15 | font-family: $font-family-regular; 16 | font-size: 2rem; 17 | color: $color-silver-dark; 18 | } 19 | 20 | h4 { 21 | font-family: $font-family-regular; 22 | font-size: 1.5rem; 23 | color: $color-silver-dark; 24 | } 25 | 26 | h5 { 27 | font-family: $font-family-regular; 28 | font-size: 1rem; 29 | color: $color-silver-dark; 30 | } 31 | 32 | p { 33 | font-family: $font-family-regular; 34 | font-size: 1rem; 35 | line-height: 2rem; 36 | color: $color-silver-dark; 37 | } 38 | 39 | a { 40 | font-family: $font-family-regular; 41 | font-size: 1rem; 42 | text-decoration: underline; 43 | 44 | &:hover { 45 | color: $color-silver-dark; 46 | cursor: pointer; 47 | } 48 | } 49 | 50 | b, strong { 51 | font-weight: bold; 52 | } 53 | 54 | code { 55 | padding: 2px 4px; 56 | } 57 | 58 | label { 59 | font-family: $font-family-regular; 60 | color: $color-silver-dark; 61 | } -------------------------------------------------------------------------------- /src/static/styles/config/_variables.scss: -------------------------------------------------------------------------------- 1 | @import "colors"; 2 | 3 | $font-family-regular: Arial; 4 | -------------------------------------------------------------------------------- /src/static/styles/font-awesome-helper.js: -------------------------------------------------------------------------------- 1 | // Help functions from: https://github.com/gowravshekar/font-awesome-webpack/issues/33 2 | 3 | function encodeLoader(loader) { 4 | if (typeof loader === 'string') { 5 | return loader; 6 | } 7 | 8 | if (typeof loader.options !== 'undefined') { 9 | const query = Object 10 | .keys(loader.options) 11 | .map((param) => { 12 | return `${encodeURIComponent(param)}=${encodeURIComponent(loader.options[param])}`; 13 | }) 14 | .join('&'); 15 | return `${loader.loader}?${query}`; 16 | } 17 | return loader.loader; 18 | } 19 | 20 | module.exports = function buildExtractStylesLoader(loaders) { 21 | const extractTextLoader = encodeLoader(loaders[0]); 22 | const fallbackLoader = encodeLoader(loaders[1]); 23 | 24 | const restLoaders = loaders 25 | .slice(2) 26 | .map( 27 | (loader) => { 28 | if (typeof loader === 'string') { 29 | return loader; 30 | } 31 | return encodeLoader(loader); 32 | } 33 | ); 34 | 35 | return [ 36 | extractTextLoader, 37 | fallbackLoader, 38 | ...restLoaders, 39 | ].join('!'); 40 | }; 41 | -------------------------------------------------------------------------------- /src/static/styles/font-awesome.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Configuration file for font-awesome-webpack 3 | * 4 | * In order to keep the bundle size low in production, 5 | * disable components you don't use. 6 | * 7 | */ 8 | 9 | module.exports = { 10 | styles: { 11 | mixins: true, 12 | core: true, 13 | icons: true, 14 | larger: true, 15 | path: true, 16 | animated: true 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /src/static/styles/font-awesome.config.less: -------------------------------------------------------------------------------- 1 | /** 2 | * Configuration file for font-awesome-webpack 3 | * 4 | */ 5 | 6 | // Example: 7 | // @fa-border-color: #ddd; 8 | -------------------------------------------------------------------------------- /src/static/styles/font-awesome.config.prod.js: -------------------------------------------------------------------------------- 1 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 2 | const fontAwesomeConfig = require('./font-awesome.config'); 3 | const buildExtractStylesLoader = require('./font-awesome-helper'); 4 | 5 | fontAwesomeConfig.styleLoader = buildExtractStylesLoader(ExtractTextPlugin.extract(['css-loader', 'less-loader'])); 6 | 7 | module.exports = fontAwesomeConfig; 8 | -------------------------------------------------------------------------------- /src/static/styles/main.scss: -------------------------------------------------------------------------------- 1 | @import "config/reset"; 2 | @import "config/fonts"; 3 | @import "config/typography"; 4 | 5 | @import "theme/base"; 6 | @import "theme/login"; 7 | @import "theme/navbar"; 8 | @import "theme/footer"; 9 | 10 | @import "utils/margins"; 11 | -------------------------------------------------------------------------------- /src/static/styles/theme/_base.scss: -------------------------------------------------------------------------------- 1 | html { 2 | width: 100%; 3 | height: 100%; 4 | font-size: 100%; 5 | font-family: $font-family-regular; 6 | } -------------------------------------------------------------------------------- /src/static/styles/theme/_footer.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seedstars/django-react-redux-base/a8f9872f20c0fc77b0dcb0386ca7b1643f8f72e1/src/static/styles/theme/_footer.scss -------------------------------------------------------------------------------- /src/static/styles/theme/_login.scss: -------------------------------------------------------------------------------- 1 | .login { 2 | .login-container { 3 | max-width: 600px; 4 | margin: 0 auto; 5 | padding: 20px; 6 | } 7 | } -------------------------------------------------------------------------------- /src/static/styles/theme/_navbar.scss: -------------------------------------------------------------------------------- 1 | .navbar { 2 | border-radius: 0px; 3 | 4 | a { 5 | text-decoration: none; 6 | } 7 | } -------------------------------------------------------------------------------- /src/static/styles/utils/_margins.scss: -------------------------------------------------------------------------------- 1 | .margin-none { 2 | margin: 0; 3 | } 4 | 5 | // Margins top 6 | .margin-top-none { 7 | margin-top: 0; 8 | } 9 | 10 | .margin-top-small { 11 | margin-top: .6rem; 12 | } 13 | 14 | .margin-top-medium { 15 | margin-top: 1.875rem; 16 | } 17 | 18 | .margin-top-large { 19 | margin-top: 3.7rem; 20 | } 21 | 22 | // Margins bottom 23 | .margin-bottom-none { 24 | margin-bottom: 0; 25 | } 26 | 27 | .margin-bottom-small { 28 | margin-bottom: .6rem; 29 | } 30 | 31 | .margin-bottom-medium { 32 | margin-bottom: 1.875rem; 33 | } 34 | 35 | .margin-bottom-large { 36 | margin-bottom: 3.7rem; 37 | } 38 | -------------------------------------------------------------------------------- /src/static/utils/config.js: -------------------------------------------------------------------------------- 1 | export const SERVER_URL = 'http://localhost:8000'; 2 | 3 | // config should use named export as there can be different exports, 4 | // just need to export default also because of eslint rules 5 | export { SERVER_URL as default }; 6 | -------------------------------------------------------------------------------- /src/static/utils/index.js: -------------------------------------------------------------------------------- 1 | export function checkHttpStatus(response) { 2 | if (response.status >= 200 && response.status < 300) { 3 | return response; 4 | } 5 | 6 | const error = new Error(response.statusText); 7 | error.response = response; 8 | throw error; 9 | } 10 | 11 | export function parseJSON(response) { 12 | return response.json(); 13 | } 14 | -------------------------------------------------------------------------------- /src/static/utils/requireAuthentication.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { push } from 'react-router-redux'; 4 | import PropTypes from 'prop-types'; 5 | 6 | export default function requireAuthentication(Component) { 7 | class AuthenticatedComponent extends React.Component { 8 | static propTypes = { 9 | isAuthenticated: PropTypes.bool.isRequired, 10 | location: PropTypes.shape({ 11 | pathname: PropTypes.string.isRequired 12 | }).isRequired, 13 | dispatch: PropTypes.func.isRequired 14 | }; 15 | 16 | componentWillMount() { 17 | this.checkAuth(); 18 | } 19 | 20 | componentWillReceiveProps(nextProps) { 21 | this.checkAuth(); 22 | } 23 | 24 | checkAuth() { 25 | if (!this.props.isAuthenticated) { 26 | const redirectAfterLogin = this.props.location.pathname; 27 | this.props.dispatch(push(`/login?next=${redirectAfterLogin}`)); 28 | } 29 | } 30 | 31 | render() { 32 | return ( 33 |
34 | {this.props.isAuthenticated === true 35 | ? 36 | : null 37 | } 38 |
39 | ); 40 | } 41 | } 42 | 43 | const mapStateToProps = (state) => { 44 | return { 45 | isAuthenticated: state.auth.isAuthenticated, 46 | token: state.auth.token 47 | }; 48 | }; 49 | 50 | return connect(mapStateToProps)(AuthenticatedComponent); 51 | } 52 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seedstars/django-react-redux-base/a8f9872f20c0fc77b0dcb0386ca7b1643f8f72e1/tests/__init__.py -------------------------------------------------------------------------------- /tests/js/actions/auth.specs.js: -------------------------------------------------------------------------------- 1 | /* eslint import/no-extraneous-dependencies: ["error", {"devDependencies": true}] */ 2 | 3 | import { expect } from 'chai'; 4 | import nock from 'nock'; 5 | import configureStore from 'redux-mock-store'; 6 | import thunk from 'redux-thunk'; 7 | 8 | import * as TYPES from '../../../src/static/constants'; 9 | import * as ACTIONS_AUTH from '../../../src/static/actions/auth'; 10 | 11 | import { SERVER_URL } from '../../../src/static/utils/config'; 12 | 13 | 14 | describe('Auth Actions:', () => { 15 | afterEach(() => { 16 | nock.cleanAll(); 17 | }); 18 | 19 | beforeEach(() => { 20 | localStorage.removeItem('token'); 21 | }); 22 | 23 | it('authLoginUserSuccess should create LOGIN_USER_SUCCESS action', () => { 24 | expect(ACTIONS_AUTH.authLoginUserSuccess('token', {})).to.eql({ 25 | type: TYPES.AUTH_LOGIN_USER_SUCCESS, 26 | payload: { 27 | token: 'token', 28 | user: {} 29 | } 30 | }); 31 | }); 32 | 33 | it('authLoginUserFailure should create LOGIN_USER_FAILURE action', () => { 34 | expect(ACTIONS_AUTH.authLoginUserFailure(404, 'Not found')).to.eql({ 35 | type: TYPES.AUTH_LOGIN_USER_FAILURE, 36 | payload: { 37 | status: 404, 38 | statusText: 'Not found' 39 | } 40 | }); 41 | }); 42 | 43 | it('authLoginUserRequest should create LOGIN_USER_REQUEST action', () => { 44 | expect(ACTIONS_AUTH.authLoginUserRequest()).to.eql({ 45 | type: TYPES.AUTH_LOGIN_USER_REQUEST 46 | }); 47 | }); 48 | 49 | it('authLogout should create LOGOUT_USER action', () => { 50 | expect(ACTIONS_AUTH.authLogout()).to.eql({ 51 | type: TYPES.AUTH_LOGOUT_USER 52 | }); 53 | }); 54 | 55 | it('authLogoutAndRedirect should create authLogout and pushState actions', (done) => { 56 | const expectedActions = [ 57 | { 58 | type: TYPES.AUTH_LOGOUT_USER 59 | }, { 60 | type: '@@router/CALL_HISTORY_METHOD', 61 | payload: { 62 | method: 'push', 63 | args: [ 64 | '/login' 65 | ] 66 | } 67 | } 68 | ]; 69 | 70 | const middlewares = [thunk]; 71 | const mockStore = configureStore(middlewares); 72 | const store = mockStore({}); 73 | 74 | store.dispatch(ACTIONS_AUTH.authLogoutAndRedirect()) 75 | .then(() => { 76 | expect(store.getActions()).to.deep.equal(expectedActions); 77 | }).then(done).catch(done); 78 | }); 79 | 80 | it('authLoginUser should create LOGIN_USER_REQUEST, LOGIN_USER_SUCCESS, ' + 81 | 'and PUSH_STATE actions when API returns 200', (done) => { 82 | const expectedActions = [ 83 | { 84 | type: TYPES.AUTH_LOGIN_USER_REQUEST 85 | }, { 86 | type: TYPES.AUTH_LOGIN_USER_SUCCESS, 87 | payload: { 88 | token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyTmFtZSI6IlRlc3QgVXNlciJ9.J6n4-v0I85' + 89 | 'zk9MkxBHroZ9ZPZEES-IKeul9ozxYnoZ8', 90 | user: {} 91 | } 92 | }, { 93 | type: '@@router/CALL_HISTORY_METHOD', 94 | payload: { 95 | method: 'push', 96 | args: [ 97 | '/' 98 | ] 99 | } 100 | } 101 | ]; 102 | 103 | nock(SERVER_URL) 104 | .post('/api/v1/accounts/login/') 105 | .reply(200, { 106 | token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyTmFtZSI6IlRlc3QgVXNlciJ9.J6n4-v0I85zk9Mk' + 107 | 'xBHroZ9ZPZEES-IKeul9ozxYnoZ8', 108 | user: {} 109 | }); 110 | 111 | const middlewares = [thunk]; 112 | const mockStore = configureStore(middlewares); 113 | const store = mockStore({}); 114 | 115 | store.dispatch(ACTIONS_AUTH.authLoginUser()) 116 | .then(() => { 117 | expect(store.getActions()).to.deep.equal(expectedActions); 118 | }).then(done).catch(done); 119 | }); 120 | 121 | it('authLoginUser should create LOGIN_USER_REQUEST and LOGIN_USER_FAILURE actions when API returns 401', (done) => { 122 | const expectedActions = [ 123 | { 124 | type: TYPES.AUTH_LOGIN_USER_REQUEST 125 | }, { 126 | type: TYPES.AUTH_LOGIN_USER_FAILURE, 127 | payload: { 128 | status: 401, 129 | statusText: 'Unauthorized' 130 | } 131 | } 132 | ]; 133 | 134 | nock(SERVER_URL) 135 | .post('/api/v1/accounts/login/') 136 | .reply(401, { non_field_errors: ['Unauthorized'] }); 137 | 138 | const middlewares = [thunk]; 139 | const mockStore = configureStore(middlewares); 140 | const store = mockStore({}); 141 | 142 | store.dispatch(ACTIONS_AUTH.authLoginUser()) 143 | .then(() => { 144 | expect(store.getActions()).to.deep.equal(expectedActions); 145 | }).then(done).catch(done); 146 | }); 147 | 148 | it('authLoginUser should create LOGIN_USER_REQUEST and LOGIN_USER_FAILURE actions when API returns 500', (done) => { 149 | const expectedActions = [ 150 | { 151 | type: TYPES.AUTH_LOGIN_USER_REQUEST 152 | }, { 153 | type: TYPES.AUTH_LOGIN_USER_FAILURE, 154 | payload: { 155 | status: 500, 156 | statusText: 'A server error occurred while sending your data!' 157 | } 158 | } 159 | ]; 160 | 161 | nock(SERVER_URL) 162 | .post('/api/v1/accounts/login/') 163 | .reply(500); 164 | 165 | const middlewares = [thunk]; 166 | const mockStore = configureStore(middlewares); 167 | const store = mockStore({}); 168 | 169 | store.dispatch(ACTIONS_AUTH.authLoginUser()) 170 | .then(() => { 171 | expect(store.getActions()).to.deep.equal(expectedActions); 172 | }).then(done).catch(done); 173 | }); 174 | 175 | it('authLoginUser should create LOGIN_USER_REQUEST and LOGIN_USER_FAILURE actions when API ' + 176 | 'has no response', (done) => { 177 | const expectedActions = [ 178 | { 179 | type: TYPES.AUTH_LOGIN_USER_REQUEST 180 | }, { 181 | type: TYPES.AUTH_LOGIN_USER_FAILURE, 182 | payload: { 183 | status: 'Connection Error', 184 | statusText: 'An error occurred while sending your data!' 185 | } 186 | } 187 | ]; 188 | 189 | nock(SERVER_URL) 190 | .post('/api/v1/accounts/login/') 191 | .reply(); 192 | 193 | const middlewares = [thunk]; 194 | const mockStore = configureStore(middlewares); 195 | const store = mockStore({}); 196 | 197 | store.dispatch(ACTIONS_AUTH.authLoginUser()) 198 | .then(() => { 199 | expect(store.getActions()).to.deep.equal(expectedActions); 200 | }).then(done).catch(done); 201 | }); 202 | }); 203 | -------------------------------------------------------------------------------- /tests/js/actions/data.specs.js: -------------------------------------------------------------------------------- 1 | /* eslint import/no-extraneous-dependencies: ["error", {"devDependencies": true}] */ 2 | 3 | import { expect } from 'chai'; 4 | import nock from 'nock'; 5 | import configureStore from 'redux-mock-store'; 6 | import thunk from 'redux-thunk'; 7 | 8 | import * as TYPES from '../../../src/static/constants'; 9 | import * as ACTIONS_DATA from '../../../src/static/actions/data'; 10 | import { SERVER_URL } from '../../../src/static/utils/config'; 11 | 12 | 13 | describe('Data Actions:', () => { 14 | afterEach(() => { 15 | nock.cleanAll(); 16 | }); 17 | 18 | beforeEach(() => { 19 | localStorage.removeItem('token'); 20 | }); 21 | 22 | it('dataReceiveProtectedData should create DATA_RECEIVE_PROTECTED_DATA action', () => { 23 | expect(ACTIONS_DATA.dataReceiveProtectedData('data')).to.eql({ 24 | type: TYPES.DATA_RECEIVE_PROTECTED_DATA, 25 | payload: { 26 | data: 'data' 27 | } 28 | }); 29 | }); 30 | 31 | it('dataFetchProtectedDataRequest should create FETCH_PROTECTED_DATA_REQUEST action', () => { 32 | expect(ACTIONS_DATA.dataFetchProtectedDataRequest()).to.eql({ 33 | type: TYPES.DATA_FETCH_PROTECTED_DATA_REQUEST 34 | }); 35 | }); 36 | 37 | it('dataFetchProtectedDataRequest should create DATA_RECEIVE_PROTECTED_DATA actions ' + 38 | 'when API returns 200', (done) => { 39 | const expectedActions = [ 40 | { 41 | type: TYPES.DATA_FETCH_PROTECTED_DATA_REQUEST 42 | }, { 43 | type: TYPES.DATA_RECEIVE_PROTECTED_DATA, 44 | payload: { 45 | data: 'data' 46 | } 47 | } 48 | ]; 49 | 50 | nock(SERVER_URL) 51 | .get('/api/v1/getdata/') 52 | .reply(200, { 53 | data: 'data' 54 | }); 55 | 56 | const middlewares = [thunk]; 57 | const mockStore = configureStore(middlewares); 58 | const store = mockStore({}); 59 | 60 | store.dispatch(ACTIONS_DATA.dataFetchProtectedData('token')) 61 | .then(() => { 62 | expect(store.getActions()).to.deep.equal(expectedActions); 63 | }).then(done).catch(done); 64 | }); 65 | 66 | it('dataFetchProtectedDataRequest should create authLogout and pushState actions when API returns 401', (done) => { 67 | const expectedActions = [ 68 | { 69 | type: TYPES.DATA_FETCH_PROTECTED_DATA_REQUEST 70 | }, { 71 | type: TYPES.AUTH_LOGIN_USER_FAILURE, 72 | payload: { 73 | status: 401, 74 | statusText: 'Unauthorized' 75 | } 76 | }, { 77 | type: '@@router/CALL_HISTORY_METHOD', 78 | payload: { 79 | method: 'push', 80 | args: [ 81 | '/login' 82 | ] 83 | } 84 | } 85 | ]; 86 | 87 | nock(SERVER_URL) 88 | .get('/api/v1/getdata/') 89 | .reply(401, { non_field_errors: ['Unauthorized'] }); 90 | 91 | const middlewares = [thunk]; 92 | const mockStore = configureStore(middlewares); 93 | const store = mockStore({}); 94 | 95 | store.dispatch(ACTIONS_DATA.dataFetchProtectedData('token')) 96 | .then(() => { 97 | expect(store.getActions()).to.deep.equal(expectedActions); 98 | }).then(done).catch(done); 99 | }); 100 | 101 | it('dataFetchProtectedDataRequest should create authLogout and pushState actions when API returns 500', (done) => { 102 | const expectedActions = [ 103 | { 104 | type: TYPES.DATA_FETCH_PROTECTED_DATA_REQUEST 105 | }, { 106 | type: TYPES.AUTH_LOGIN_USER_FAILURE, 107 | payload: { 108 | status: 500, 109 | statusText: 'A server error occurred while sending your data!' 110 | } 111 | }, { 112 | type: '@@router/CALL_HISTORY_METHOD', 113 | payload: { 114 | method: 'push', 115 | args: [ 116 | '/login' 117 | ] 118 | } 119 | } 120 | ]; 121 | 122 | nock(SERVER_URL) 123 | .get('/api/v1/getdata/') 124 | .reply(500); 125 | 126 | const middlewares = [thunk]; 127 | const mockStore = configureStore(middlewares); 128 | const store = mockStore({}); 129 | 130 | store.dispatch(ACTIONS_DATA.dataFetchProtectedData('token')) 131 | .then(() => { 132 | expect(store.getActions()).to.deep.equal(expectedActions); 133 | }).then(done).catch(done); 134 | }); 135 | 136 | it('dataFetchProtectedDataRequest should create authLogout and pushState actions when API ' + 137 | 'has no response', (done) => { 138 | const expectedActions = [ 139 | { 140 | type: TYPES.DATA_FETCH_PROTECTED_DATA_REQUEST 141 | }, { 142 | type: TYPES.AUTH_LOGIN_USER_FAILURE, 143 | payload: { 144 | status: 'Connection Error', 145 | statusText: 'An error occurred while sending your data!' 146 | } 147 | }, { 148 | type: '@@router/CALL_HISTORY_METHOD', 149 | payload: { 150 | method: 'push', 151 | args: [ 152 | '/login' 153 | ] 154 | } 155 | } 156 | ]; 157 | 158 | nock(SERVER_URL) 159 | .get('/api/v1/getdata/') 160 | .reply(); 161 | 162 | const middlewares = [thunk]; 163 | const mockStore = configureStore(middlewares); 164 | const store = mockStore({}); 165 | 166 | store.dispatch(ACTIONS_DATA.dataFetchProtectedData('token')) 167 | .then(() => { 168 | expect(store.getActions()).to.deep.equal(expectedActions); 169 | }).then(done).catch(done); 170 | }); 171 | }); 172 | -------------------------------------------------------------------------------- /tests/js/components/AuthenticatedComponent.spec.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seedstars/django-react-redux-base/a8f9872f20c0fc77b0dcb0386ca7b1643f8f72e1/tests/js/components/AuthenticatedComponent.spec.js -------------------------------------------------------------------------------- /tests/js/containers/App.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint import/no-extraneous-dependencies: ["error", {"devDependencies": true}] */ 2 | /* eslint import/no-named-default: 0 */ 3 | 4 | import React from 'react'; 5 | import sinon from 'sinon'; 6 | import { expect } from 'chai'; 7 | import { mount, shallow } from 'enzyme'; 8 | 9 | import configureStore from 'redux-mock-store'; 10 | import thunk from 'redux-thunk'; 11 | 12 | import { default as AppConnected, AppNotConnected } from '../../../src/static/app'; 13 | 14 | 15 | describe(' App View Tests (Container):', () => { 16 | describe('Implementation:', () => { 17 | context('Empty state:', () => { 18 | let wrapper; 19 | const spies = { 20 | redirect: sinon.spy() 21 | }; 22 | const props = { 23 | isAuthenticated: false, 24 | children:
, 25 | dispatch: spies.redirect, 26 | location: { pathname: '/' } 27 | }; 28 | 29 | beforeEach(() => { 30 | wrapper = shallow(); 31 | }); 32 | 33 | it('should render correctly', () => { 34 | return expect(wrapper).to.be.ok; 35 | }); 36 | 37 | it('should have only one menu', () => { 38 | expect(wrapper.find('nav')).to.have.length(1); 39 | }); 40 | 41 | it('should not have side-menu div', () => { 42 | expect(wrapper.find('side-menu')).to.have.length(0); 43 | }); 44 | 45 | it('should have the children elements', () => { 46 | expect(wrapper.find('.test-test')).to.have.length(1); 47 | }); 48 | 49 | it('should have one login button', () => { 50 | expect(wrapper.find('.js-login-button')).to.have.length(1); 51 | }); 52 | 53 | it('should have one navbar brand and call a dispatch onClick event', () => { 54 | const link = wrapper.find('.navbar-brand'); 55 | expect(link).to.have.length(1); 56 | 57 | link.simulate('click'); 58 | expect(spies.redirect.calledOnce).to.equal(true); 59 | }); 60 | 61 | it('should have one goToIndex link and call a dispatch onClick event', () => { 62 | const link = wrapper.find('.js-go-to-index-button'); 63 | expect(link).to.have.length(1); 64 | 65 | link.simulate('click'); 66 | expect(spies.redirect.calledTwice).to.equal(true); 67 | }); 68 | }); 69 | context('State with authentication:', () => { 70 | let wrapper; 71 | const spies = { 72 | redirect: sinon.spy(), 73 | willUnmount: sinon.spy() 74 | }; 75 | const props = { 76 | isAuthenticated: true, 77 | children:
, 78 | dispatch: spies.redirect, 79 | pathName: '/' 80 | }; 81 | 82 | beforeEach(() => { 83 | wrapper = mount(); 84 | }); 85 | 86 | it('should render correctly', () => { 87 | return expect(wrapper).to.be.ok; 88 | }); 89 | 90 | it('should have only one menu', () => { 91 | expect(wrapper.find('nav')).to.have.length(1); 92 | }); 93 | 94 | it('should not have side-menu div', () => { 95 | expect(wrapper.find('side-menu')).to.have.length(0); 96 | }); 97 | 98 | it('should have the children elements', () => { 99 | expect(wrapper.find('.test-test')).to.have.length(1); 100 | }); 101 | 102 | it('should have one authLogout button and call a dispatch onClick event', () => { 103 | const button = wrapper.find('.js-logout-button'); 104 | expect(button).to.have.length(1); 105 | 106 | button.simulate('click'); 107 | expect(spies.redirect.calledOnce).to.equal(true); 108 | }); 109 | 110 | it('should have one goToIndex link and call a dispatch onClick event', () => { 111 | const link = wrapper.find('.js-go-to-index-button'); 112 | expect(link).to.have.length(1); 113 | 114 | link.simulate('click'); 115 | expect(spies.redirect.calledTwice).to.equal(true); 116 | }); 117 | 118 | it('should have one goToProtected link and call a dispatch onClick event', () => { 119 | const link = wrapper.find('.js-go-to-protected-button'); 120 | expect(link).to.have.length(1); 121 | 122 | link.simulate('click'); 123 | expect(spies.redirect.calledThrice).to.equal(true); 124 | }); 125 | 126 | it('should unmount and remove resize event listener', () => { 127 | expect(spies.willUnmount.callCount).to.equal(0); 128 | wrapper.unmount(); 129 | }); 130 | }); 131 | }); 132 | 133 | describe('Store Integration:', () => { 134 | context('State map:', (done) => { 135 | const state = { 136 | auth: { 137 | token: 'token', 138 | userName: 'a@a.com', 139 | isAuthenticated: true, 140 | isAuthenticating: false, 141 | statusText: 'You have been successfully logged in.' 142 | }, 143 | routing: { 144 | location: { 145 | pathname: '/' 146 | } 147 | } 148 | }; 149 | const expectedActions = []; 150 | 151 | const middlewares = [thunk]; 152 | const mockStore = configureStore(middlewares); 153 | const store = mockStore(state, expectedActions, done); 154 | 155 | const wrapper = mount( 156 | 159 |
for testing only
160 |
); 161 | 162 | it('props', () => { 163 | expect(wrapper.node.selector.props.isAuthenticated).to.equal(state.auth.isAuthenticated); 164 | expect(wrapper.node.selector.props.location.pathname).to.equal('/'); 165 | }); 166 | }); 167 | }); 168 | }); 169 | -------------------------------------------------------------------------------- /tests/js/containers/Home.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint import/no-extraneous-dependencies: ["error", {"devDependencies": true}] */ 2 | /* eslint import/no-named-default: 0 */ 3 | 4 | import React from 'react'; 5 | import { expect } from 'chai'; 6 | import { mount, shallow } from 'enzyme'; 7 | 8 | import configureStore from 'redux-mock-store'; 9 | import thunk from 'redux-thunk'; 10 | 11 | import { default as HomeViewConnected, HomeViewNotConnected } from '../../../src/static/containers/Home'; 12 | 13 | 14 | describe('Home View Tests (Container):', () => { 15 | describe('Implementation:', () => { 16 | context('Empty state:', () => { 17 | let wrapper; 18 | const props = { 19 | statusText: null 20 | }; 21 | 22 | beforeEach(() => { 23 | wrapper = shallow(); 24 | }); 25 | 26 | it('should render correctly', () => { 27 | return expect(wrapper).to.be.ok; 28 | }); 29 | 30 | it('should have two img, one h1 and one p ', () => { 31 | expect(wrapper.find('img')).to.have.length(2); 32 | expect(wrapper.find('h1')).to.have.length(1); 33 | expect(wrapper.find('p')).to.have.length(1); 34 | }); 35 | }); 36 | 37 | context('State with statusText:', () => { 38 | let wrapper; 39 | const props = { 40 | statusText: 'Some status text' 41 | }; 42 | 43 | beforeEach(() => { 44 | wrapper = shallow(); 45 | }); 46 | 47 | it('should render correctly', () => { 48 | return expect(wrapper).to.be.ok; 49 | }); 50 | 51 | it('should have two img, one h1, one p and one alert alert-info div ', () => { 52 | expect(wrapper.find('img')).to.have.length(2); 53 | expect(wrapper.find('h1')).to.have.length(1); 54 | expect(wrapper.find('p')).to.have.length(1); 55 | 56 | const div = wrapper.find('div.alert.alert-info'); 57 | expect(div).to.have.length(1); 58 | expect(div.text()).to.equal(props.statusText); 59 | }); 60 | }); 61 | }); 62 | 63 | describe('Store Integration:', () => { 64 | context('State map:', (done) => { 65 | const state = { 66 | auth: { 67 | token: 'token', 68 | userName: 'a@a.com', 69 | isAuthenticated: true, 70 | isAuthenticating: false, 71 | statusText: 'You have been successfully logged in.' 72 | } 73 | }; 74 | const expectedActions = []; 75 | const middlewares = [thunk]; 76 | const mockStore = configureStore(middlewares); 77 | const store = mockStore(state, expectedActions, done); 78 | 79 | const wrapper = mount(); 80 | 81 | it('props', () => { 82 | expect(wrapper.node.selector.props.statusText).to.equal(state.auth.statusText); 83 | }); 84 | }); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /tests/js/containers/Login.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint import/no-extraneous-dependencies: ["error", {"devDependencies": true}] */ 2 | /* eslint import/no-named-default: 0 */ 3 | 4 | import React from 'react'; 5 | import sinon from 'sinon'; 6 | import nock from 'nock'; 7 | import { expect } from 'chai'; 8 | import { mount } from 'enzyme'; 9 | 10 | import configureStore from 'redux-mock-store'; 11 | import thunk from 'redux-thunk'; 12 | 13 | import { default as LoginViewConnected, LoginViewNotConnected } from '../../../src/static/containers/Login'; 14 | 15 | import * as TYPES from '../../../src/static/constants'; 16 | import { SERVER_URL } from '../../../src/static/utils/config'; 17 | 18 | describe('Login View Tests (Container):', () => { 19 | describe('Implementation:', () => { 20 | context('Empty State:', () => { 21 | let wrapper; 22 | const spies = { 23 | authLoginUser: sinon.spy() 24 | }; 25 | const props = { 26 | actions: { 27 | authLoginUser: spies.authLoginUser 28 | }, 29 | isAuthenticating: false, 30 | isAuthenticated: false, 31 | statusText: null, 32 | dispatch: () => {} 33 | }; 34 | 35 | beforeEach(() => { 36 | wrapper = mount(); 37 | }); 38 | 39 | it('should render correctly', () => { 40 | return expect(wrapper).to.be.ok; 41 | }); 42 | 43 | it('should have two inputs ([text,password]) and one button ', () => { 44 | const input = wrapper.find('input'); 45 | 46 | expect(input).to.have.length(2); 47 | expect(input.at(0).prop('type')).to.equal('text'); 48 | expect(input.at(1).prop('type')).to.equal('password'); 49 | }); 50 | 51 | it('should call prop action with 3 arguments on button click', () => { 52 | wrapper.setState({ 53 | formValues: { 54 | email: 'a@a.com', 55 | password: '123' 56 | }, 57 | redirectTo: '/' 58 | }); 59 | 60 | wrapper.find('form').simulate('submit'); 61 | expect(spies.authLoginUser.calledWith('a@a.com', '123', '/')).to.equal(true); 62 | }); 63 | }); 64 | context('Wrong login state:', () => { 65 | let wrapper; 66 | const spies = { 67 | authLoginUser: sinon.spy() 68 | }; 69 | const props = { 70 | actions: { 71 | authLoginUser: spies.authLoginUser 72 | }, 73 | isAuthenticating: false, 74 | isAuthenticated: false, 75 | dispatch: () => {}, 76 | statusText: 'Authentication Error: 401 UNAUTHORIZED' 77 | }; 78 | 79 | beforeEach(() => { 80 | wrapper = mount(); 81 | }); 82 | 83 | it('should render correctly', () => { 84 | return expect(wrapper).to.be.ok; 85 | }); 86 | 87 | it('should have one div with class alert alert-danger', () => { 88 | const div = wrapper.find('div.alert-danger'); 89 | 90 | expect(div).to.have.length(1); 91 | expect(div.text()).to.equal(props.statusText); 92 | }); 93 | 94 | it('should have two inputs ([text,password]) and one button', () => { 95 | const input = wrapper.find('input'); 96 | expect(input).to.have.length(2); 97 | expect(input.at(0).prop('type')).to.equal('text'); 98 | expect(input.at(1).prop('type')).to.equal('password'); 99 | 100 | expect(wrapper.find('button')).to.have.length(1); 101 | }); 102 | 103 | it('should call prop action with 3 arguments on button click', () => { 104 | wrapper.setState({ 105 | formValues: { 106 | email: 'a@a.com', 107 | password: '123' 108 | }, 109 | redirectTo: '/' 110 | }); 111 | 112 | wrapper.find('form').simulate('submit'); 113 | expect(spies.authLoginUser.calledWith('a@a.com', '123', '/')).to.equal(true); 114 | }); 115 | 116 | it('should update state on form change', () => { 117 | wrapper.setState({ 118 | formValues: { 119 | email: 'a@a.com', 120 | password: '123' 121 | }, 122 | redirectTo: '/' 123 | }); 124 | 125 | const inputEmail = wrapper.find('input').at(0); 126 | inputEmail.simulate('change', { target: { value: 'b@b.com' } }); 127 | 128 | expect(wrapper.state()).to.eql({ 129 | formValues: { 130 | email: 'b@b.com', 131 | password: '123' 132 | }, 133 | redirectTo: '/' 134 | }); 135 | }); 136 | }); 137 | }); 138 | 139 | describe('Store Integration:', () => { 140 | context('State map:', (done) => { 141 | nock(SERVER_URL) 142 | .post('/api/v1/accounts/login/') 143 | .reply(200, { 144 | statusText: 'some text' 145 | }); 146 | 147 | const state = { 148 | auth: { 149 | token: 'token', 150 | userName: 'a@a.com', 151 | isAuthenticated: true, 152 | isAuthenticating: false, 153 | statusText: 'You have been successfully logged in.' 154 | } 155 | }; 156 | const expectedActions = [ 157 | { 158 | type: TYPES.AUTH_LOGIN_USER_SUCCESS, 159 | payload: { 160 | auth: {} 161 | } 162 | }, { 163 | type: TYPES.AUTH_LOGIN_USER_FAILURE, 164 | payload: { 165 | status: null, 166 | statusText: null 167 | } 168 | }, 169 | { 170 | type: TYPES.AUTH_LOGIN_USER_REQUEST 171 | }, 172 | { 173 | type: TYPES.AUTH_LOGOUT_USER 174 | } 175 | ]; 176 | 177 | const middlewares = [thunk]; 178 | const mockStore = configureStore(middlewares); 179 | const store = mockStore(state, expectedActions, done); 180 | 181 | const wrapper = mount(); 182 | 183 | it('should have one div with class alert alert-success', () => { 184 | const div = wrapper.find('div.alert-success'); 185 | 186 | expect(div).to.have.length(1); 187 | expect(div.text()).to.equal(wrapper.node.selector.props.statusText); 188 | }); 189 | 190 | it('props', () => { 191 | expect(wrapper.node.selector.props.isAuthenticating).to.equal(state.auth.isAuthenticating); 192 | expect(wrapper.node.selector.props.statusText).to.equal(state.auth.statusText); 193 | }); 194 | 195 | it('actions', () => { 196 | expect(wrapper.node.selector.props.actions.authLoginUser).to.not.equal(undefined); 197 | }); 198 | 199 | nock.cleanAll(); 200 | }); 201 | }); 202 | }); 203 | -------------------------------------------------------------------------------- /tests/js/containers/NotFound.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint import/no-extraneous-dependencies: ["error", {"devDependencies": true}] */ 2 | 3 | import React from 'react'; 4 | import { expect } from 'chai'; 5 | import { shallow } from 'enzyme'; 6 | 7 | import NotFoundView from '../../../src/static/containers/NotFound'; 8 | 9 | 10 | describe('NotFound View Tests (Container):', () => { 11 | describe('Implementation:', () => { 12 | context('Empty state:', () => { 13 | let wrapper; 14 | const props = {}; 15 | 16 | beforeEach(() => { 17 | wrapper = shallow(); 18 | }); 19 | 20 | it('should render correctly', () => { 21 | return expect(wrapper).to.be.ok; 22 | }); 23 | 24 | it('should have one h1', () => { 25 | expect(wrapper.find('h1')).to.have.length(1); 26 | }); 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /tests/js/containers/Protected.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint import/no-extraneous-dependencies: ["error", {"devDependencies": true}] */ 2 | /* eslint import/no-named-default: 0 */ 3 | 4 | import React from 'react'; 5 | import sinon from 'sinon'; 6 | import nock from 'nock'; 7 | import { expect } from 'chai'; 8 | import { mount, shallow } from 'enzyme'; 9 | 10 | import configureStore from 'redux-mock-store'; 11 | import thunk from 'redux-thunk'; 12 | 13 | import { 14 | default as ProtectedViewConnected, 15 | ProtectedViewNotConnected 16 | } from '../../../src/static/containers/Protected'; 17 | 18 | import * as TYPES from '../../../src/static/constants'; 19 | import { SERVER_URL } from '../../../src/static/utils/config'; 20 | 21 | 22 | describe(' Protected View Tests (Container):', () => { 23 | describe('Implementation:', () => { 24 | context('Empty state:', () => { 25 | let wrapper; 26 | 27 | const spies = { 28 | dataFetchProtectedData: sinon.spy() 29 | }; 30 | const props = { 31 | isFetching: true, 32 | data: 'some random secret data that has to be available', 33 | token: 'token', 34 | actions: { 35 | dataFetchProtectedData: spies.dataFetchProtectedData 36 | } 37 | }; 38 | 39 | beforeEach(() => { 40 | wrapper = shallow(); 41 | }); 42 | 43 | it('should render correctly', () => { 44 | return expect(wrapper).to.be.ok; 45 | }); 46 | it('should have one h1', () => { 47 | expect(wrapper.find('h1')).to.have.length(1); 48 | }); 49 | it('should call actions.fetchProtectedData', () => { 50 | expect(spies.dataFetchProtectedData.calledWith('token')).to.equal(true); 51 | }); 52 | }); 53 | 54 | context('State with data:', () => { 55 | let wrapper; 56 | const props = { 57 | isFetching: false, 58 | data: 'some data', 59 | token: 'token', 60 | actions: { 61 | dataFetchProtectedData: () => { 62 | return null; 63 | } 64 | } 65 | }; 66 | 67 | beforeEach(() => { 68 | wrapper = shallow(); 69 | }); 70 | 71 | it('should render correctly', () => { 72 | return expect(wrapper).to.be.ok; 73 | }); 74 | 75 | it('should have one h1 with "Protected"', () => { 76 | const h1 = wrapper.find('h1'); 77 | 78 | expect(h1).to.have.length(1); 79 | expect(h1.text()).to.equal('Protected'); 80 | }); 81 | 82 | it('should have one div with class .alert .alert-info with "Data from server: {props.data}"', 83 | () => { 84 | const div = wrapper.find('.alert.alert-info'); 85 | 86 | expect(div).to.have.length(1); 87 | expect(div.text()).to.equal('some data'); 88 | }); 89 | }); 90 | }); 91 | 92 | describe('Store Integration:', () => { 93 | context('State map:', (done) => { 94 | nock(SERVER_URL) 95 | .get('/api/v1/getdata/') 96 | .reply(200, { 97 | data: 'data' 98 | }); 99 | 100 | const state = { 101 | auth: { 102 | token: 'token', 103 | userName: 'a@a.com', 104 | isAuthenticated: true, 105 | isAuthenticating: false, 106 | statusText: 'You have been successfully logged in.' 107 | }, 108 | data: { 109 | data: 'some random secret data that has to be available', 110 | isFetching: true 111 | } 112 | 113 | }; 114 | const expectedActions = [ 115 | { 116 | type: TYPES.DATA_FETCH_PROTECTED_DATA_REQUEST 117 | }, 118 | { 119 | type: TYPES.DATA_RECEIVE_PROTECTED_DATA, 120 | payload: { 121 | data: 'data' 122 | } 123 | } 124 | ]; 125 | 126 | const middlewares = [thunk]; 127 | const mockStore = configureStore(middlewares); 128 | const store = mockStore(state, expectedActions, done); 129 | 130 | // Had to pass token as a prop because it normally is passed down from the AuthenticaedComponent.js 131 | const wrapper = mount(); 132 | 133 | it('props', () => { 134 | expect(wrapper.node.selector.props.isFetching).to.equal(state.data.isFetching); 135 | expect(wrapper.node.selector.props.data).to.equal(state.data.data); 136 | }); 137 | 138 | it('actions', () => { 139 | expect(wrapper.node.selector.props.actions.dataFetchProtectedData).to.not.equal(undefined); 140 | }); 141 | 142 | nock.cleanAll(); 143 | }); 144 | }); 145 | }); 146 | -------------------------------------------------------------------------------- /tests/js/reducers/auth.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint import/no-extraneous-dependencies: ["error", {"devDependencies": true}] */ 2 | 3 | import { expect } from 'chai'; 4 | import authReducer from '../../../src/static/reducers/auth'; 5 | import * as TYPES from '../../../src/static/constants'; 6 | 7 | describe('Auth Reducers Tests', () => { 8 | it('should handle LOGIN_USER_REQUEST', () => { 9 | const reducerResponse = authReducer([], { 10 | type: TYPES.AUTH_LOGIN_USER_REQUEST 11 | }); 12 | expect(reducerResponse).to.eql({ 13 | isAuthenticating: true, 14 | statusText: null 15 | }); 16 | }); 17 | 18 | it('should handle AUTH_LOGIN_USER_SUCCESS', () => { 19 | const reducerResponse = authReducer([], { 20 | type: TYPES.AUTH_LOGIN_USER_SUCCESS, 21 | payload: { 22 | token: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJ1c2VybmFtZSI6ImFAYS5jb20iLCJleHAiOjE0NTI5NjA3N' + 23 | 'TUsInVzZXJfaWQiOjEsImVtYWlsIjoiYUBhLmNvbSJ9.RrJJ63OyWaZIPSmgS8h_vZyrPo0TV9SXvT_5HJhNKpMunJoY' + 24 | '76GKQ9xyjI27vlir0pA61j0X-j-Wk2phDDk39w', 25 | user: { 26 | email: 'a@a.com' 27 | } 28 | } 29 | }); 30 | expect(reducerResponse).to.eql({ 31 | token: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJ1c2VybmFtZSI6ImFAYS5jb20iLCJleHAiOjE0NTI5NjA3NTUs' + 32 | 'InVzZXJfaWQiOjEsImVtYWlsIjoiYUBhLmNvbSJ9.RrJJ63OyWaZIPSmgS8h_vZyrPo0TV9SXvT_5HJhNKpMunJoY76GKQ9x' + 33 | 'yjI27vlir0pA61j0X-j-Wk2phDDk39w', 34 | userName: 'a@a.com', 35 | isAuthenticating: false, 36 | isAuthenticated: true, 37 | statusText: 'You have been successfully logged in.' 38 | }); 39 | }); 40 | 41 | it('should handle AUTH_LOGIN_USER_FAILURE', () => { 42 | const reducerResponse = authReducer([], { 43 | type: TYPES.AUTH_LOGIN_USER_FAILURE, 44 | payload: { 45 | status: '401', 46 | statusText: 'Something Happened' 47 | } 48 | }); 49 | expect(reducerResponse).to.eql({ 50 | token: null, 51 | userName: null, 52 | isAuthenticating: false, 53 | isAuthenticated: false, 54 | statusText: 'Authentication Error: 401 - Something Happened' 55 | }); 56 | }); 57 | 58 | it('should handle AUTH_LOGOUT_USER', () => { 59 | const reducerResponse = authReducer([ 60 | { 61 | token: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJ1c2VybmFtZSI6ImFAYS5jb20iLCJleHAiOjE0NTI5NjA3NTUsIn' + 62 | 'VzZXJfaWQiOjEsImVtYWlsIjoiYUBhLmNvbSJ9.RrJJ63OyWaZIPSmgS8h_vZyrPo0TV9SXvT_5HJhNKpMunJoY76GKQ9xyjI2' + 63 | '7vlir0pA61j0X-j-Wk2phDDk39w', 64 | userName: 'a@a.com', 65 | isAuthenticating: false, 66 | isAuthenticated: true, 67 | statusText: 'You have been successfully logged in.' 68 | } 69 | 70 | ], { 71 | type: TYPES.AUTH_LOGOUT_USER 72 | }); 73 | 74 | expect(reducerResponse).to.eql({ 75 | 0: { 76 | token: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJ1c2VybmFtZSI6ImFAYS5jb20iLCJleHAiOjE0NTI5NjA3NTUsI' + 77 | 'nVzZXJfaWQiOjEsImVtYWlsIjoiYUBhLmNvbSJ9.RrJJ63OyWaZIPSmgS8h_vZyrPo0TV9SXvT_5HJhNKpMunJoY76GKQ9xyj' + 78 | 'I27vlir0pA61j0X-j-Wk2phDDk39w', 79 | userName: 'a@a.com', 80 | isAuthenticating: false, 81 | isAuthenticated: true, 82 | statusText: 'You have been successfully logged in.' 83 | }, 84 | isAuthenticated: false, 85 | token: null, 86 | userName: null, 87 | statusText: 'You have been successfully logged out.' 88 | }); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /tests/js/reducers/data.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint import/no-extraneous-dependencies: ["error", {"devDependencies": true}] */ 2 | 3 | import { expect } from 'chai'; 4 | import dataReducer from '../../../src/static/reducers/data'; 5 | import * as TYPES from '../../../src/static/constants'; 6 | 7 | describe('Data Reducers Tests', () => { 8 | it('should handle RECEIVE_PROTECTED_DATA', () => { 9 | const reducerResponse = dataReducer([], { 10 | type: TYPES.DATA_RECEIVE_PROTECTED_DATA, 11 | payload: { 12 | data: 'this is some data' 13 | } 14 | }); 15 | expect(reducerResponse).to.eql({ 16 | data: 'this is some data', 17 | isFetching: false 18 | }); 19 | }); 20 | 21 | it('should handle FETCH_PROTECTED_DATA_REQUEST', () => { 22 | const reducerResponse = dataReducer([], { 23 | type: TYPES.DATA_FETCH_PROTECTED_DATA_REQUEST 24 | }); 25 | expect(reducerResponse).to.eql({ 26 | isFetching: true 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /tests/js/reducers/general.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint import/no-extraneous-dependencies: ["error", {"devDependencies": true}] */ 2 | 3 | import { expect } from 'chai'; 4 | import reducer from '../../../src/static/reducers/data'; 5 | 6 | describe('General Reducers Tests', () => { 7 | it('the state should be the same when a actions doesnt exist and output a console warning', () => { 8 | const reducerResponse = reducer( 9 | { 10 | data: 'some data', 11 | isFetching: false 12 | }, 13 | { 14 | type: 'nonexistent action' 15 | } 16 | ); 17 | 18 | expect(reducerResponse).to.eql({ 19 | data: 'some data', 20 | isFetching: false 21 | }); 22 | }); 23 | 24 | it('the state should be the initial state when no state are present', () => { 25 | const initialState = { 26 | data: null, 27 | isFetching: false 28 | }; 29 | const reducerResponse = reducer(undefined, { type: 'nonexistent action' }); 30 | 31 | expect(reducerResponse).to.eql(initialState); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /tests/python/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seedstars/django-react-redux-base/a8f9872f20c0fc77b0dcb0386ca7b1643f8f72e1/tests/python/__init__.py -------------------------------------------------------------------------------- /tests/python/accounts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seedstars/django-react-redux-base/a8f9872f20c0fc77b0dcb0386ca7b1643f8f72e1/tests/python/accounts/__init__.py -------------------------------------------------------------------------------- /tests/python/accounts/test_models.py: -------------------------------------------------------------------------------- 1 | import factory 2 | from django.test import TestCase 3 | 4 | from accounts.models import User 5 | 6 | 7 | class UserFactory(factory.DjangoModelFactory): 8 | first_name = 'John' 9 | last_name = 'Doe' 10 | is_active = True 11 | 12 | class Meta: 13 | model = User 14 | django_get_or_create = ('email',) 15 | 16 | 17 | class AccountsModelsTests(TestCase): 18 | def setUp(self): 19 | self.user = UserFactory.create(email='test@test.com') 20 | 21 | def test_unicode(self): 22 | self.assertEqual(str(self.user), 'test@test.com') 23 | 24 | def test_super_user(self): 25 | super_user = User.objects.create_superuser(email='email@test.com') 26 | self.assertEqual(super_user.is_superuser, True) 27 | 28 | def test_user(self): 29 | user = User.objects.create_user(email='email@test.com', 30 | first_name='user', 31 | last_name='test', 32 | password='test') 33 | self.assertEqual(user.is_superuser, False) 34 | 35 | def test_get_full_name(self): 36 | self.assertEqual(self.user.get_full_name(), 'John Doe') 37 | 38 | def test_get_short_name(self): 39 | self.assertEqual(self.user.get_short_name(), 'John') 40 | -------------------------------------------------------------------------------- /tests/python/accounts/test_serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import status 2 | from rest_framework.test import APITestCase 3 | 4 | from accounts.serializers import UserRegistrationSerializer, UserSerializer 5 | from lib.testutils import CustomTestCase 6 | from tests.python.accounts.test_models import UserFactory 7 | 8 | 9 | class UserRegistrationSerializerTest(CustomTestCase, APITestCase): 10 | INVALID_DATA_DICT = [ 11 | {'data': {'email': 'test1@mailinator.com', 12 | 'first_name': 'test', 13 | 'last_name': 'user', 14 | 'password': 'test'}, 15 | 'error': ('email', ['Please use a different email address provider.']), 16 | 'label': 'Invalid email.', 17 | 'method': 'POST', 18 | 'status': status.HTTP_400_BAD_REQUEST 19 | }, 20 | 21 | {'data': {'email': 'test1@gmail', 22 | 'first_name': 'test', 23 | 'last_name': 'user', 24 | 'password': 'test'}, 25 | 'error': ('email', ['Enter a valid email address.']), 26 | 'label': 'Bad email format.', 27 | 'method': 'POST', 28 | 'status': status.HTTP_400_BAD_REQUEST 29 | }, 30 | ] 31 | VALID_DATA_DICT = [ 32 | {'email': 'emailsuccess@gmail.com', 33 | 'first_name': 'test', 34 | 'last_name': 'user', 35 | 'password': 'test'}, 36 | ] 37 | 38 | def setUp(self): 39 | self.required_fields = ['email', 'first_name', 'last_name', 'password'] 40 | self.not_required_fields = ['id'] 41 | self.user = UserFactory.create(email='emailwilllogininserializer@mydomain.com') 42 | 43 | def test_fields(self): 44 | serializer = UserRegistrationSerializer() 45 | self.assert_fields_required(True, serializer, self.required_fields) 46 | self.assert_fields_required(False, serializer, self.not_required_fields) 47 | self.assertEqual(len(serializer.fields), len(self.required_fields) + len(self.not_required_fields)) 48 | 49 | def test_invalid_data(self): 50 | serializer = UserRegistrationSerializer 51 | self.assert_invalid_data(serializer, self.INVALID_DATA_DICT) 52 | 53 | def test_validate_success(self): 54 | serializer = UserRegistrationSerializer 55 | self.assert_valid_data(serializer, self.VALID_DATA_DICT) 56 | 57 | 58 | class UserSerializerTest(CustomTestCase, APITestCase): 59 | INVALID_DATA_DICT = [ 60 | {'data': {'email': 'emailwilllogin@mydomain.com', 61 | 'first_name': 'test', 'last_name': ''}, 62 | 'error': ('last_name', ['This field may not be blank.']), 63 | 'label': 'Last name is required', 64 | 'method': 'POST', 65 | 'status': status.HTTP_400_BAD_REQUEST 66 | }, 67 | ] 68 | VALID_DATA_DICT = [ 69 | {'email': 'emailwilllogin@mydomain.com', 'first_name': 'test', 'last_name': 'test'}, 70 | ] 71 | 72 | def setUp(self): 73 | self.required_fields = ['email', 'first_name', 'last_name'] 74 | self.not_required_fields = [] 75 | 76 | def test_fields(self): 77 | serializer = UserSerializer() 78 | self.assert_fields_required(True, serializer, self.required_fields) 79 | self.assert_fields_required(False, serializer, self.not_required_fields) 80 | self.assertEqual(len(serializer.fields), len(self.required_fields) + len(self.not_required_fields)) 81 | 82 | def test_invalid_data(self): 83 | serializer = UserSerializer 84 | self.assert_invalid_data(serializer, self.INVALID_DATA_DICT) 85 | 86 | def test_validate_success(self): 87 | serializer = UserSerializer 88 | self.assert_valid_data(serializer, self.VALID_DATA_DICT) 89 | -------------------------------------------------------------------------------- /tests/python/accounts/test_views.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | import base64 4 | from django.core.urlresolvers import reverse 5 | from rest_framework import status 6 | from rest_framework.test import APITestCase 7 | 8 | from lib.testutils import CustomTestCase 9 | from tests.python.accounts.test_serializers import UserRegistrationSerializerTest, UserSerializerTest 10 | from .test_models import UserFactory 11 | 12 | 13 | def get_basic_auth_header(username, password): 14 | return 'Basic %s' % base64.b64encode(('%s:%s' % (username, password)).encode('ascii')).decode() 15 | 16 | 17 | class AccountTests(CustomTestCase, APITestCase): 18 | INVALID_DATA_DICT = [ 19 | {'data': {'email': 'emailwilllogin@mydomain.com', 20 | 'password': 'teste'}, 21 | 'error': ('non_field_errors', ['Unable to login with provided credentials.']), 22 | 'label': 'Invalid login credentials.', 23 | 'method': 'POST', 24 | 'status': status.HTTP_401_UNAUTHORIZED 25 | }, 26 | ] 27 | VALID_DATA_DICT = [ 28 | {'email': 'emailwilllogin@mydomain.com', 'password': 'test'}, 29 | ] 30 | 31 | def setUp(self): 32 | self.user = UserFactory.create(email='emailwilllogin@mydomain.com', 33 | first_name='Test', 34 | last_name='User') 35 | self.user.set_password('test') 36 | self.user.save() 37 | self.user_2 = UserFactory.create(email='emailwilllogininserializer@mydomain.com') 38 | 39 | def test_account_register_unsuccessful(self): 40 | self.assert_invalid_data_response(invalid_data_dicts=UserRegistrationSerializerTest.INVALID_DATA_DICT, 41 | url=reverse('accounts:register')) 42 | 43 | def test_account_login_unsuccessful(self): 44 | self.client.credentials(HTTP_AUTHORIZATION=get_basic_auth_header('emailwilllogin@mydomain.com', 'wrong')) 45 | response = self.client.post(reverse('accounts:login')) 46 | self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) 47 | 48 | def test_account_register_successful(self): 49 | url = reverse('accounts:register') 50 | data = { 51 | 'email': 'emailsuccess@mydomain.com', 52 | 'first_name': 'test', 53 | 'last_name': 'user', 54 | 'password': 'test' 55 | } 56 | response = self.client.post(url, data, format='json') 57 | self.assertEqual(response.status_code, status.HTTP_201_CREATED) 58 | 59 | # Confirm user can login after register 60 | url_login = reverse('accounts:login') 61 | self.client.credentials(HTTP_AUTHORIZATION=get_basic_auth_header('emailwilllogin@mydomain.com', 'test')) 62 | response_login = self.client.post(url_login, format='json') 63 | self.assertTrue('token' in response_login.data) 64 | self.assertEqual(response_login.status_code, status.HTTP_200_OK) 65 | self.client.credentials(HTTP_AUTHORIZATION='Token {}'.format(response_login.data['token'])) 66 | 67 | def test_account_register_email_already_exists(self): 68 | url = reverse('accounts:register') 69 | data = { 70 | 'email': 'emailsuccess@mydomain.com', 71 | 'first_name': 'test', 72 | 'last_name': 'user', 73 | 'password': 'test' 74 | } 75 | response = self.client.post(url, data, format='json') 76 | self.assertEqual(response.status_code, status.HTTP_201_CREATED) 77 | 78 | # Confirm user can login after register 79 | url_login = reverse('accounts:login') 80 | self.client.credentials(HTTP_AUTHORIZATION=get_basic_auth_header('emailwilllogin@mydomain.com', 'test')) 81 | response_login = self.client.post(url_login, format='json') 82 | self.assertTrue('token' in response_login.data) 83 | self.assertEqual(response_login.status_code, status.HTTP_200_OK) 84 | 85 | url = reverse('accounts:register') 86 | data = { 87 | 'email': 'emailsuccess@mydomain.com', 88 | 'first_name': 'test', 89 | 'last_name': 'user', 90 | 'password': 'test' 91 | } 92 | response = self.client.post(url, data, format='json') 93 | self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) 94 | self.assertEqual(response.data['email'], ['Email already in use, please use a different email address.']) 95 | 96 | def test_account_login_successful_and_perform_actions(self): 97 | # Ensure we can login with given credentials. 98 | url = reverse('accounts:login') 99 | self.client.credentials(HTTP_AUTHORIZATION=get_basic_auth_header('emailwilllogin@mydomain.com', 'test')) 100 | response = self.client.post(url, format='json') 101 | self.assertTrue('token' in response.data) 102 | self.assertEqual(response.status_code, status.HTTP_200_OK) 103 | self.client.credentials(HTTP_AUTHORIZATION='Token {}'.format(response.data['token'])) 104 | 105 | # user confirmed account unsuccessfully 106 | url = reverse('accounts:status') 107 | response = self.client.get(url) 108 | self.assertEqual(response.data['status'], False) 109 | 110 | def test_account_confirm_email_unsuccessful(self): 111 | # wrong activation key 112 | invalid_data = {'status': status.HTTP_404_NOT_FOUND, 'method': 'GET', 'data': {}, 'label': 'Not found'} 113 | self.assert_invalid_data_response(url=reverse('accounts:confirm_email', args=[str(uuid.uuid4())]), 114 | invalid_data_dicts=[invalid_data]) 115 | 116 | def test_account_confirm_email_successful(self): 117 | user = UserFactory.create(email='emailtoconfirm@mydomain.com', 118 | first_name='Test', 119 | last_name='User', 120 | confirmed_email=False) 121 | 122 | # confirm email successful 123 | url = reverse('accounts:confirm_email', args=[str(user.activation_key)]) 124 | response = self.client.get(url) 125 | self.assertEqual(response.status_code, status.HTTP_200_OK) 126 | 127 | # email already confirmed 128 | url = reverse('accounts:confirm_email', args=[str(user.activation_key)]) 129 | response = self.client.get(url) 130 | self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) 131 | -------------------------------------------------------------------------------- /tests/python/base/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seedstars/django-react-redux-base/a8f9872f20c0fc77b0dcb0386ca7b1643f8f72e1/tests/python/base/__init__.py -------------------------------------------------------------------------------- /tests/python/base/test_views.py: -------------------------------------------------------------------------------- 1 | from django.core.urlresolvers import reverse 2 | from django.test import override_settings 3 | from rest_framework import status 4 | from rest_framework.test import APITestCase 5 | 6 | from tests.python.accounts.test_models import UserFactory 7 | from tests.python.accounts.test_views import get_basic_auth_header 8 | 9 | 10 | @override_settings(CELERY_EAGER_PROPAGATES_EXCEPTIONS=True, CELERY_ALWAYS_EAGER=True, BROKER_BACKEND='memory') 11 | class BaseTests(APITestCase): 12 | def setUp(self): 13 | self.user = UserFactory.create(email='emailwilllogin@mydomain.com', 14 | first_name='Test', 15 | last_name='User') 16 | self.user.set_password('test') 17 | self.user.save() 18 | 19 | def test_get_protected_page(self): 20 | # Ensure we can login with given credentials. 21 | url = reverse('accounts:login') 22 | self.client.credentials(HTTP_AUTHORIZATION=get_basic_auth_header('emailwilllogin@mydomain.com', 'test')) 23 | response = self.client.post(url, format='json') 24 | self.assertTrue('token' in response.data) 25 | self.assertEqual(response.status_code, status.HTTP_200_OK) 26 | self.client.credentials(HTTP_AUTHORIZATION='Token {}'.format(response.data['token'])) 27 | 28 | # user confirmed account unsuccessfully 29 | url = reverse('base:protected_data') 30 | response = self.client.get(url) 31 | self.assertEqual(response.data['data'], 'THIS IS THE PROTECTED STRING FROM SERVER') 32 | 33 | def test_get_main_page(self): 34 | 35 | response = self.client.get(reverse('index')) 36 | 37 | self.assertEqual(response.status_code, status.HTTP_200_OK) 38 | -------------------------------------------------------------------------------- /tests/python/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seedstars/django-react-redux-base/a8f9872f20c0fc77b0dcb0386ca7b1643f8f72e1/tests/python/utils/__init__.py -------------------------------------------------------------------------------- /tests/python/utils/test_utils.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from lib.utils import validate_email 4 | 5 | 6 | class BaseTests(TestCase): 7 | def test_validate_email(self): 8 | self.assertEqual(validate_email('fail'), False) 9 | self.assertEqual(validate_email(''), False) 10 | self.assertEqual(validate_email('test@mailinator.com'), False) 11 | self.assertEqual(validate_email('good.email@example.com'), False) 12 | -------------------------------------------------------------------------------- /tests/require.js: -------------------------------------------------------------------------------- 1 | /* eslint import/no-extraneous-dependencies: ["error", {"devDependencies": true}] */ 2 | const { JSDOM } = require('jsdom'); 3 | const register = require('ignore-styles').default; 4 | 5 | register(['.scss', '.sass', '.css', '.png', '.jpeg']); 6 | 7 | // jsdom config from: https://github.com/airbnb/enzyme/blob/master/docs/guides/jsdom.md 8 | const jsdom = new JSDOM(''); 9 | const { window } = jsdom; 10 | 11 | function copyProps(src, target) { 12 | const props = Object.getOwnPropertyNames(src) 13 | .filter((prop) => { 14 | return typeof target[prop] === 'undefined'; 15 | }) 16 | .map((prop) => { 17 | return Object.getOwnPropertyDescriptor(src, prop); 18 | }); 19 | Object.defineProperties(target, props); 20 | } 21 | 22 | global.window = window; 23 | global.document = window.document; 24 | global.navigator = { 25 | userAgent: 'node.js', 26 | }; 27 | copyProps(window, global); 28 | 29 | function mockStorage() { 30 | const storage = {}; 31 | return { 32 | setItem(key, value) { 33 | storage[key] = value || ''; 34 | }, 35 | getItem(key) { 36 | return storage[key]; 37 | }, 38 | removeItem(key) { 39 | delete storage[key]; 40 | }, 41 | get length() { 42 | return Object.keys(storage).length; 43 | }, 44 | key(i) { 45 | const keys = Object.keys(storage); 46 | return keys[i] || null; 47 | } 48 | }; 49 | } 50 | 51 | global.localStorage = mockStorage(); 52 | global.sessionStorage = mockStorage(); 53 | 54 | // define btoa which is not part of jsdom 55 | // from: https://stackoverflow.com/questions/23097928/node-js-btoa-is-not-defined-error 56 | 57 | if (typeof btoa === 'undefined') { 58 | global.btoa = (str) => { 59 | return new Buffer(str).toString('base64'); 60 | }; 61 | } 62 | -------------------------------------------------------------------------------- /webpack/common.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const autoprefixer = require('autoprefixer'); 3 | const merge = require('webpack-merge'); 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | const webpack = require('webpack'); 6 | const CleanWebpackPlugin = require('clean-webpack-plugin'); 7 | 8 | const TARGET = process.env.npm_lifecycle_event; 9 | 10 | const PATHS = { 11 | app: path.join(__dirname, '../src/static'), 12 | build: path.join(__dirname, '../src/static_dist') 13 | }; 14 | 15 | const VENDOR = [ 16 | 'babel-polyfill', 17 | 'history', 18 | 'react', 19 | 'react-dom', 20 | 'react-redux', 21 | 'react-router', 22 | 'react-mixin', 23 | 'classnames', 24 | 'redux', 25 | 'react-router-redux', 26 | 'jquery', 27 | 'bootstrap-loader', 28 | 'font-awesome-webpack!./styles/font-awesome.config.prod.js' 29 | ]; 30 | 31 | const basePath = path.resolve(__dirname, '../src/static/'); 32 | 33 | const common = { 34 | context: basePath, 35 | entry: { 36 | vendor: VENDOR, 37 | app: PATHS.app 38 | }, 39 | output: { 40 | filename: '[name].[hash].js', 41 | path: PATHS.build, 42 | publicPath: '/static' 43 | }, 44 | plugins: [ 45 | // extract all common modules to vendor so we can load multiple apps in one page 46 | // new webpack.optimize.CommonsChunkPlugin({ 47 | // name: 'vendor', 48 | // filename: 'vendor.[hash].js' 49 | // }), 50 | new webpack.optimize.CommonsChunkPlugin({ 51 | children: true, 52 | async: true, 53 | minChunks: 2 54 | }), 55 | new HtmlWebpackPlugin({ 56 | template: path.join(__dirname, '../src/static/index.html'), 57 | hash: true, 58 | chunks: ['vendor', 'app'], 59 | chunksSortMode: 'manual', 60 | filename: 'index.html', 61 | inject: 'body' 62 | }), 63 | new webpack.DefinePlugin({ 64 | 'process.env': { NODE_ENV: TARGET === 'dev' ? '"development"' : '"production"' }, 65 | '__DEVELOPMENT__': TARGET === 'dev' 66 | }), 67 | new webpack.ProvidePlugin({ 68 | '$': 'jquery', 69 | 'jQuery': 'jquery', 70 | 'window.jQuery': 'jquery' 71 | }), 72 | new CleanWebpackPlugin([PATHS.build], { 73 | root: process.cwd() 74 | }) 75 | ], 76 | resolve: { 77 | extensions: ['.jsx', '.js', '.json', '.scss', '.css'], 78 | modules: ['node_modules'] 79 | }, 80 | module: { 81 | rules: [ 82 | { 83 | test: /\.js$/, 84 | use: { 85 | loader: 'babel-loader' 86 | }, 87 | exclude: /node_modules/ 88 | }, 89 | { 90 | test: /\.jpe?g$|\.gif$|\.png$/, 91 | loader: 'file-loader?name=/images/[name].[ext]?[hash]' 92 | }, 93 | { 94 | test: /\.woff(\?.*)?$/, 95 | loader: 'url-loader?name=/fonts/[name].[ext]&limit=10000&mimetype=application/font-woff' 96 | }, 97 | { 98 | test: /\.woff2(\?.*)?$/, 99 | loader: 'url-loader?name=/fonts/[name].[ext]&limit=10000&mimetype=application/font-woff2' 100 | }, 101 | { 102 | test: /\.ttf(\?.*)?$/, 103 | loader: 'url-loader?name=/fonts/[name].[ext]&limit=10000&mimetype=application/octet-stream' 104 | }, 105 | { 106 | test: /\.eot(\?.*)?$/, 107 | loader: 'file-loader?name=/fonts/[name].[ext]' 108 | }, 109 | { 110 | test: /\.otf(\?.*)?$/, 111 | loader: 'file-loader?name=/fonts/[name].[ext]&mimetype=application/font-otf' 112 | }, 113 | { 114 | test: /\.svg(\?.*)?$/, 115 | loader: 'url-loader?name=/fonts/[name].[ext]&limit=10000&mimetype=image/svg+xml' 116 | }, 117 | { 118 | test: /\.json(\?.*)?$/, 119 | loader: 'file-loader?name=/files/[name].[ext]' 120 | } 121 | ] 122 | }, 123 | }; 124 | 125 | switch (TARGET) { 126 | case 'dev': 127 | module.exports = merge(require('./dev.config'), common); 128 | break; 129 | case 'prod': 130 | module.exports = merge(require('./prod.config'), common); 131 | break; 132 | default: 133 | console.log('Target configuration not found. Valid targets: "dev" or "prod".'); 134 | } 135 | -------------------------------------------------------------------------------- /webpack/dev.config.js: -------------------------------------------------------------------------------- 1 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 2 | 3 | // importLoader:1 from https://blog.madewithenvy.com/webpack-2-postcss-cssnext-fdcd2fd7d0bd 4 | 5 | module.exports = { 6 | devtool: 'source-map', // 'cheap-module-eval-source-map' 7 | module: { 8 | rules: [{ 9 | test: /\.css$/, 10 | use: ExtractTextPlugin.extract([ 11 | { 12 | loader: 'css-loader', 13 | options: { importLoaders: 1 }, 14 | }, 15 | 'postcss-loader'] 16 | ) 17 | }, { 18 | test: /\.scss$/, 19 | use: ExtractTextPlugin.extract([ 20 | { 21 | loader: 'css-loader', 22 | options: { importLoaders: 1 }, 23 | }, 24 | 'postcss-loader', 25 | { 26 | loader: 'sass-loader', 27 | options: { 28 | data: `@import "${__dirname}/../src/static/styles/config/_variables.scss";` 29 | } 30 | }] 31 | ) 32 | }], 33 | }, 34 | plugins: [ 35 | new ExtractTextPlugin('styles/[name].css') 36 | ] 37 | }; 38 | -------------------------------------------------------------------------------- /webpack/prod.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 3 | 4 | // importLoader:1 from https://blog.madewithenvy.com/webpack-2-postcss-cssnext-fdcd2fd7d0bd 5 | 6 | module.exports = { 7 | // devtool: 'source-map', // No need for dev tool in production 8 | 9 | module: { 10 | rules: [{ 11 | test: /\.css$/, 12 | use: ExtractTextPlugin.extract([ 13 | { 14 | loader: 'css-loader', 15 | options: { importLoaders: 1 }, 16 | }, 17 | 'postcss-loader'] 18 | ) 19 | }, { 20 | test: /\.scss$/, 21 | use: ExtractTextPlugin.extract([ 22 | { 23 | loader: 'css-loader', 24 | options: { importLoaders: 1 }, 25 | }, 26 | 'postcss-loader', 27 | { 28 | loader: 'sass-loader', 29 | options: { 30 | data: `@import "${__dirname}/../src/static/styles/config/_variables.scss";` 31 | } 32 | }] 33 | ) 34 | }], 35 | }, 36 | 37 | plugins: [ 38 | new ExtractTextPlugin('styles/[name].css'), 39 | new webpack.optimize.OccurrenceOrderPlugin(), 40 | new webpack.optimize.UglifyJsPlugin({ 41 | compress: { 42 | warnings: false 43 | } 44 | }) 45 | ] 46 | }; 47 | --------------------------------------------------------------------------------