diff --git a/.eslintignore b/.eslintignore index ffeec6e..b562b34 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1,2 @@ assets/bundles/ +assets/webpack_bundles/ diff --git a/.eslintrc.js b/.eslintrc.js index e9b4934..4f148b6 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -8,7 +8,8 @@ module.exports = { }, "env": { "es6": true, - "browser": true + "browser": true, + "jest": true }, "settings": { "import/resolver": { diff --git a/.gitignore b/.gitignore index 1479510..16a4496 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ __pycache__/ # coverage result .coverage +/coverage/ # pycharm .idea diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8035904..fb27c0f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,9 +12,9 @@ - repo: local hooks: - - id: isort - name: isort-local - entry : isort + - id: prospector + name: prospector-local + entry: prospector language: python types: [python] exclude: .+/(settings|migrations)/.+ @@ -31,16 +31,16 @@ \.eslintrc\.js )$ pass_filenames: true + - id: isort + name: isort-local + entry : isort + language: python + types: [python] + exclude: .+/(settings|migrations)/.+ + pass_filenames: true - id: missing-migrations name: missing-migrations-local - entry: python manage.py has_missing_migrations + entry: pipenv run python manage.py has_missing_migrations language: system always_run: true pass_filenames: false - - id: prospector - name: prospector-local - entry: prospector --messages-only - language: python - types: [python] - exclude: .+/(settings|migrations)/.+ - pass_filenames: true diff --git a/Makefile b/Makefile index 46c77d6..28a84d9 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ clean: @find . -name "__pycache__" -delete test: - python manage.py test $(ARG) --parallel --keepdb + pipenv run python manage.py test $(ARG) --parallel --keepdb testreset: - python manage.py test $(ARG) --parallel + pipenv run python manage.py test $(ARG) --parallel diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..bf6209c --- /dev/null +++ b/Pipfile @@ -0,0 +1,46 @@ +[[source]] + +url = "https://pypi.python.org/simple" +verify_ssl = true +name = "pypi" + + +[dev-packages] +django-debug-toolbar = "*" +django-debug-toolbar-request-history = "*" +django-naomi = "*" +fixmydjango = "*" +model-mommy = "*" +pre_commit = "*" + + + +[packages] + +Django = ">=1,<2" +celery = {extras = ["redis"]} +django-model-utils = "*" +django-webpack-loader = "*" +django-js-reverse = "*" +django-import-export = "*" +python-decouple = "*" +"psycopg2" = "*" +brotlipy = "*" +django-log-request-id = "*" +dj-database-url = "*" +gunicorn = "*" +opbeat = "*" +whitenoise = "*" +bandit = "*" +coverage = "*" +astroid = "<1.6" +pylint = "<1.8" +prospector = {extras = ["with-vulture"]} +isort = "*" +safety = "*" +ipython = "*" + + +[requires] + +python_version = "3.6" diff --git a/README.md b/README.md index 72d3eca..ebb51df 100644 --- a/README.md +++ b/README.md @@ -29,8 +29,10 @@ This is a good starting point for modern Python/JavaScript web projects. django-admin startproject theprojectname --extension py,yml,json --name Procfile,README.md,.env.example --template=https://github.com/vintasoftware/django-react-boilerplate/archive/boilerplate-release.zip ``` - [ ] Above: don't forget the `--extension` and `--name` params! -- [ ] `pip install -r requirements-to-freeze.txt` -- [ ] `pip freeze > requirements.txt` +- [ ] Install pipenv if not installed yet: `pip install pipenv` (maybe you have to run this command as an OS superuser) +- [ ] Make sure you have Python 3.6 installed +- [ ] `pipenv install --dev` +- [ ] Activate the newly created virtualenv with `pipenv shell` - [ ] `npm update --save` - [ ] `npm update --save-dev` - [ ] Check for outdated npm dependencies with `npm outdated` and update them @@ -56,9 +58,10 @@ After completing ALL of the above, remove this `Project bootstrap` section from - Setup [editorconfig](http://editorconfig.org/), [prospector](https://prospector.landscape.io/en/master/) and [ESLint](http://eslint.org/) in the text editor you will use to develop. ### Running the project -- `pip install -r requirements.txt` +- `pipenv install --dev` - `npm install` - `npm run start` +- `pipenv shell` - `python manage.py runserver` ### Testing @@ -69,7 +72,7 @@ Will run django tests using `--keepdb` and `--parallel`. You may pass a path to `make test someapp.tests.test_views` ### Adding new pypi libs -Add high level dependecies to `requirements-to-freeze.txt` and `pip freeze > requirements.txt`. This is [A Better Pip Workflow](http://www.kennethreitz.org/essays/a-better-pip-workflow). +Just run `pipenv install LIB_NAME_ON_PYPI` and then `pipenv lock` to lock the version in Pipfile.lock file ## Linting - Manually with `prospector` and `npm run lint` on project root. diff --git a/assets/js/app/example-app/components/ColorChanger/ColorChanger.js b/assets/js/app/example-app/components/ColorChanger/ColorChanger.js new file mode 100644 index 0000000..5880877 --- /dev/null +++ b/assets/js/app/example-app/components/ColorChanger/ColorChanger.js @@ -0,0 +1,60 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import ColorDisplay from '../ColorDisplay'; + +import './style.scss'; + + +class ColorChanger extends React.Component { + constructor(props) { + super(props); + + this.state = { + color: 'black', + }; + + this.handleChangeColor = this.handleChangeColor.bind(this); + } + + handleChangeColor(e) { + const color = e.target.value; + this.setState({ color }); + } + + render() { + const { title } = this.props; + + return ( +
+

{title}

+

Color Changer App

+

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

+ +
+ + + +
+
+ ); + } +} + +ColorChanger.defaultProps = { + title: 'React App Loaded!', +}; + +ColorChanger.propTypes = { + title: PropTypes.string, +}; + +export default ColorChanger; diff --git a/assets/js/app/example-app/components/ColorChanger/__tests__/ColorChanger.snaps.spec.js b/assets/js/app/example-app/components/ColorChanger/__tests__/ColorChanger.snaps.spec.js new file mode 100644 index 0000000..761e338 --- /dev/null +++ b/assets/js/app/example-app/components/ColorChanger/__tests__/ColorChanger.snaps.spec.js @@ -0,0 +1,28 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; + +import ColorChanger from '../ColorChanger'; + +jest.mock('../../ColorDisplay/ColorDisplay'); + + +describe('ColorChanger', () => { + let Component; + let tree; + + test('Some title', () => { + Component = renderer.create( + ); + + tree = Component.toJSON(); + expect(tree).toMatchSnapshot(); + }); + + test('No title (should use default)', () => { + Component = renderer.create( + ); + + tree = Component.toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/assets/js/app/example-app/components/ColorChanger/__tests__/__snapshots__/ColorChanger.snaps.spec.js.snap b/assets/js/app/example-app/components/ColorChanger/__tests__/__snapshots__/ColorChanger.snaps.spec.js.snap new file mode 100644 index 0000000..6a30a3a --- /dev/null +++ b/assets/js/app/example-app/components/ColorChanger/__tests__/__snapshots__/ColorChanger.snaps.spec.js.snap @@ -0,0 +1,113 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ColorChanger No title (should use default) 1`] = ` +
+

+ React App Loaded! +

+

+ Color Changer App +

+

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

+
+ + + ColorDisplay + {"color":"black"} + +
+
+`; + +exports[`ColorChanger Some title 1`] = ` +
+

+ This is a test title +

+

+ Color Changer App +

+

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

+
+ + + ColorDisplay + {"color":"black"} + +
+
+`; diff --git a/assets/js/app/example-app/components/ColorChanger/index.js b/assets/js/app/example-app/components/ColorChanger/index.js new file mode 100644 index 0000000..9173f84 --- /dev/null +++ b/assets/js/app/example-app/components/ColorChanger/index.js @@ -0,0 +1,3 @@ +import ColorChanger from './ColorChanger'; + +export default ColorChanger; diff --git a/assets/js/app/example-app/components/ColorChanger/style.scss b/assets/js/app/example-app/components/ColorChanger/style.scss new file mode 100644 index 0000000..276697b --- /dev/null +++ b/assets/js/app/example-app/components/ColorChanger/style.scss @@ -0,0 +1,19 @@ +.main-container { + border: 1px dashed grey; + margin: 5px; + padding: 5px; + + .app-name { + color: red; + padding-top: 20px; + } + + .inner-container { + margin: 0; + padding: 5px; + + .color-picker { + margin: 0px 5px 0px 5px; + } + } +} diff --git a/assets/js/app/example-app/components/ColorDisplay/ColorDisplay.js b/assets/js/app/example-app/components/ColorDisplay/ColorDisplay.js new file mode 100644 index 0000000..fda4710 --- /dev/null +++ b/assets/js/app/example-app/components/ColorDisplay/ColorDisplay.js @@ -0,0 +1,25 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import './style.scss'; + + +const ColorDisplay = (props) => { + const { color } = props; + + return ( + + {color} + + ); +}; + +ColorDisplay.defaultProps = { + color: 'black', +}; + +ColorDisplay.propTypes = { + color: PropTypes.string, +}; + +export default ColorDisplay; diff --git a/assets/js/app/example-app/components/ColorDisplay/__mocks__/ColorDisplay.js b/assets/js/app/example-app/components/ColorDisplay/__mocks__/ColorDisplay.js new file mode 100644 index 0000000..654c744 --- /dev/null +++ b/assets/js/app/example-app/components/ColorDisplay/__mocks__/ColorDisplay.js @@ -0,0 +1,8 @@ +import React from 'react'; + + +const mock = props => ( + ColorDisplay {JSON.stringify(props)} +); + +export default mock; diff --git a/assets/js/app/example-app/components/ColorDisplay/__tests__/ColorDisplay.snaps.spec.js b/assets/js/app/example-app/components/ColorDisplay/__tests__/ColorDisplay.snaps.spec.js new file mode 100644 index 0000000..06be0d9 --- /dev/null +++ b/assets/js/app/example-app/components/ColorDisplay/__tests__/ColorDisplay.snaps.spec.js @@ -0,0 +1,34 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; + +import ColorDisplay from '../ColorDisplay'; + + +describe('ColorDisplay', () => { + let Component; + let tree; + + test('purple', () => { + Component = renderer.create( + ); + + tree = Component.toJSON(); + expect(tree).toMatchSnapshot(); + }); + + test('no color (should default to black)', () => { + Component = renderer.create( + ); + + tree = Component.toJSON(); + expect(tree).toMatchSnapshot(); + }); + + test('unknown color', () => { + Component = renderer.create( + ); + + tree = Component.toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/assets/js/app/example-app/components/ColorDisplay/__tests__/__snapshots__/ColorDisplay.snaps.spec.js.snap b/assets/js/app/example-app/components/ColorDisplay/__tests__/__snapshots__/ColorDisplay.snaps.spec.js.snap new file mode 100644 index 0000000..57517fa --- /dev/null +++ b/assets/js/app/example-app/components/ColorDisplay/__tests__/__snapshots__/ColorDisplay.snaps.spec.js.snap @@ -0,0 +1,25 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ColorDisplay no color (should default to black) 1`] = ` + + black + +`; + +exports[`ColorDisplay purple 1`] = ` + + purple + +`; + +exports[`ColorDisplay unknown color 1`] = ` + + caterpillar + +`; diff --git a/assets/js/app/example-app/components/ColorDisplay/index.js b/assets/js/app/example-app/components/ColorDisplay/index.js new file mode 100644 index 0000000..c09f2da --- /dev/null +++ b/assets/js/app/example-app/components/ColorDisplay/index.js @@ -0,0 +1,3 @@ +import ColorDisplay from './ColorDisplay'; + +export default ColorDisplay; diff --git a/assets/js/app/example-app/components/ColorDisplay/style.scss b/assets/js/app/example-app/components/ColorDisplay/style.scss new file mode 100644 index 0000000..096c67d --- /dev/null +++ b/assets/js/app/example-app/components/ColorDisplay/style.scss @@ -0,0 +1,29 @@ +%text-color { + font-weight: bold; + margin: 0px 5px; +}; + +.color-black { + @extend %text-color; + color: black; +} + +.color-green { + @extend %text-color; + color: green; +} + +.color-red { + @extend %text-color; + color: red; +} + +.color-blue { + @extend %text-color; + color: blue; +} + +.color-purple { + @extend %text-color; + color: purple; +} diff --git a/assets/js/app/example-app/components/index.js b/assets/js/app/example-app/components/index.js new file mode 100644 index 0000000..9173f84 --- /dev/null +++ b/assets/js/app/example-app/components/index.js @@ -0,0 +1,3 @@ +import ColorChanger from './ColorChanger'; + +export default ColorChanger; diff --git a/assets/js/app/example-app/index.js b/assets/js/app/example-app/index.js new file mode 100644 index 0000000..c1e0e57 --- /dev/null +++ b/assets/js/app/example-app/index.js @@ -0,0 +1,3 @@ +import ColorChanger from './components'; + +export default ColorChanger; diff --git a/assets/js/components/HomePageReactTitle.js b/assets/js/components/HomePageReactTitle.js deleted file mode 100644 index afe99a3..0000000 --- a/assets/js/components/HomePageReactTitle.js +++ /dev/null @@ -1,16 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import { Urls } from 'utils'; - -const HomePageReactTitle = ({ title }) => { - const homeURL = Urls.home(); - - return

{title} (this is page {homeURL})

; -}; - -HomePageReactTitle.propTypes = { - title: PropTypes.string.isRequired, -}; - -export default HomePageReactTitle; diff --git a/assets/js/containers/HomePageContainer.js b/assets/js/containers/HomePageContainer.js deleted file mode 100644 index 8df70c5..0000000 --- a/assets/js/containers/HomePageContainer.js +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; -import HomePageReactTitle from 'components/HomePageReactTitle'; - -/* You also get this warning in v1.x if you write your root component as - stateless plain function instead of using React.Component. This problem - is already solved completely in the upcoming v3.x. - https://github.com/gaearon/react-hot-loader/blob/4978bffbb82a2508cf5d4ef2eee8b9b9101284ad/docs/Troubleshooting.md */ -// eslint-disable-next-line react/prefer-stateless-function -export default class HomePageContainer extends React.Component { - render() { - const title = 'It really does work! (rendered by React, change this message to test hot reloading)'; - return ; - } -} diff --git a/assets/js/pages/homePage.js b/assets/js/pages/homePage.js index 240c4dd..7ab5e03 100644 --- a/assets/js/pages/homePage.js +++ b/assets/js/pages/homePage.js @@ -1,5 +1,9 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import HomePageContainer from 'containers/HomePageContainer'; +import ColorChanger from '../app/example-app'; -ReactDOM.render(, document.getElementById('react-app')); + +const title = 'It really does work! (this section is rendered by React, ' + + "change the app's name below to test hot reloading)"; + +ReactDOM.render(, document.getElementById('react-app')); diff --git a/bin/run_collectstatic b/bin/run_collectstatic index db42c1c..9a9c95d 100644 --- a/bin/run_collectstatic +++ b/bin/run_collectstatic @@ -12,4 +12,4 @@ MANAGE_FILE=${MANAGE_FILE:2} echo "-----> Collecting static files" python $MANAGE_FILE collectstatic --noinput 2>&1 | sed '/^Copying/d;/^$/d;/^ /d' | indent -echo \ No newline at end of file +echo diff --git a/circle.yml b/circle.yml index 39c7d25..7f5800d 100644 --- a/circle.yml +++ b/circle.yml @@ -7,17 +7,19 @@ machine: version: 3.6.0 node: version: 6.1.0 + environment: + PIPENV_IGNORE_VIRTUALENVS: True + PIPENV_DONT_LOAD_ENV: 0 dependencies: post: - - pip install django - - pip install virtualenv + - pip install "django<2" - django-admin startproject testproject --extension py,yml,json --name Procfile,README.md --template=. + - pip install requests pipenv --upgrade + test: override: - - virtualenv testproject/venv - - source testproject/venv/bin/activate - - pip install -r requirements-to-freeze.txt: + - pipenv install --dev: pwd: testproject - npm update --save: pwd: testproject @@ -33,17 +35,19 @@ test: pwd: testproject - cp testproject/testproject/settings/local.py.example testproject/testproject/settings/local.py - echo 'DJANGO_SETTINGS_MODULE="testproject.settings.local"' > testproject/.env - - python manage.py makemigrations: + - pipenv run python manage.py makemigrations: + pwd: testproject + - pipenv run python manage.py migrate: pwd: testproject - - python manage.py migrate: + - pipenv run python manage.py test: pwd: testproject - - python manage.py test: + - pipenv run prospector: pwd: testproject - - prospector: + - pipenv run python manage.py has_missing_migrations: pwd: testproject - - python manage.py has_missing_migrations: + - SECRET_KEY="$(python -c "import uuid; print(uuid.uuid4().hex + uuid.uuid4().hex)")" SENDGRID_USERNAME='foo' SENDGRID_PASSWORD='password' DJANGO_SETTINGS_MODULE='testproject.settings.production' DATABASE_URL='sqlite:///' ALLOWED_HOSTS='.example.org' REDIS_URL='redis://' pipenv run python manage.py check --deploy --fail-level WARNING: pwd: testproject - - SECRET_KEY="$(python -c "import uuid; print(uuid.uuid4().hex + uuid.uuid4().hex)")" SENDGRID_USERNAME='foo' SENDGRID_PASSWORD='password' DJANGO_SETTINGS_MODULE='testproject.settings.production' DATABASE_URL='sqlite:///' ALLOWED_HOSTS='.example.org' REDIS_URL='redis://' python manage.py check --deploy --fail-level WARNING: + - npm run test: pwd: testproject - npm run lint: pwd: testproject diff --git a/package.json b/package.json index 437b315..15900b8 100644 --- a/package.json +++ b/package.json @@ -5,10 +5,38 @@ "description": "{{project_name}} frontend assets.", "main": "index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", + "test": "jest", + "test:watch": "npm test -- --watch", "start": "babel-node server.js", "build": "NODE_ENV=production webpack -p --progress --colors --config webpack.prod.config.js --bail", - "lint": "eslint assets" + "lint": "eslint assets", + "coverage": "jest --coverage" + }, + "jest": { + "transform": { + ".*": "/node_modules/jest-css-modules" + }, + "collectCoverageFrom": [ + "assets/js/**/*.{js,jsx}" + ], + "coveragePathIgnorePatterns": [ + "assets/js/store.js", + "assets/js/index.js", + "assets/js/jquery-index.js", + "assets/js/constants/*", + "assets/js/pages/*", + "assets/js/tests/*" + ], + "coverageThreshold": { + "global": { + "statements": 10 + } + }, + "modulePaths": [ + "assets", + "assets/js", + "assets/js/app" + ] }, "dependencies": { "autoprefixer": "^7.1.1", @@ -55,6 +83,9 @@ "eslint-plugin-import": "^2.6.0", "eslint-plugin-jsx-a11y": "^5.1.0", "eslint-plugin-react": "^7.1.0", - "react-hot-loader": "^1.3.1" + "react-hot-loader": "^1.3.1", + "jest": "^21.2.1", + "jest-css-modules": "^1.1.0", + "react-test-renderer": "^15.4.1" } } diff --git a/proj_circle.yml b/proj_circle.yml index 5d62619..0537dca 100644 --- a/proj_circle.yml +++ b/proj_circle.yml @@ -3,35 +3,49 @@ machine: # remeber to update those! version: 3.6.0 node: version: 6.1.0 + environment: + # makes default virtualenv be ignored by pipenv avoiding dependencies conflict + PIPENV_IGNORE_VIRTUALENVS: True + +dependencies: + pre: + # this updates git-lfs to make pre-commit large files check hook work properly + # more details in https://github.com/pre-commit/pre-commit-hooks/issues/252 + - curl -s https://packagecloud.io/install/repositories/github/git-lfs/script.deb.sh | sudo bash + - sudo apt-get install git-lfs --upgrade + post: + - pip install requests pipenv --upgrade + - pipenv install --dev test: override: - - npm install - npm run build + - npm run lint # style check - - prospector + - pipenv run prospector # security checks - - bandit -r . - - cat requirements.txt | safety check --stdin + - pipenv run bandit -r . + - pipenv run pip freeze | safety check --stdin # imports check - - isort **/*.py --check-only + - pipenv run isort **/*.py --check-only # pre-commit additional checks - - SKIP=prospector,isort pre-commit run --all-files - - > - DJANGO_SETTINGS_MODULE={{project_name}}.settings.local - python manage.py has_missing_migrations --ignore authtools; - - > - SECRET_KEY=$(python -c 'import uuid; print(uuid.uuid4().hex + uuid.uuid4().hex)') - DATABASE_URL='sqlite:///' - ALLOWED_HOSTS='.example.org' - SENDGRID_USERNAME='test' - SENDGRID_PASSWORD='test' - REDIS_URL='redis://' - python manage.py check --deploy --settings={{project_name}}.settings.production - - coverage run manage.py test - - npm run lint + - SKIP=prospector,isort,eslint,missing-migrations pipenv run pre-commit run --all-files + - >- + DJANGO_SETTINGS_MODULE={{project_name}}.settings.local_base + pipenv run python manage.py has_missing_migrations --ignore authtools; + - >- + DJANGO_SETTINGS_MODULE={{project_name}}.settings.production + SECRET_KEY=$(python -c 'import uuid; print(uuid.uuid4().hex + uuid.uuid4().hex)') + DATABASE_URL='sqlite:///' + ALLOWED_HOSTS='.example.org' + SENDGRID_USERNAME='test' + SENDGRID_PASSWORD='test' + REDIS_URL='redis://' + pipenv run python manage.py check --deploy + - pipenv run coverage run manage.py test + - npm run test post: - - coverage html -d $CIRCLE_ARTIFACTS + - pipenv run coverage html -d $CIRCLE_ARTIFACTS # This is necessary for the boilerplate's CI. You can remove these lines general: diff --git a/project_name/urls.py b/project_name/urls.py index ac0d86f..de83aa6 100644 --- a/project_name/urls.py +++ b/project_name/urls.py @@ -2,6 +2,7 @@ from django.conf.urls import include, url # noqa from django.contrib import admin from django.views.generic import TemplateView + import django_js_reverse.views diff --git a/requirements-to-freeze.txt b/requirements-to-freeze.txt deleted file mode 100644 index 73cafa3..0000000 --- a/requirements-to-freeze.txt +++ /dev/null @@ -1,35 +0,0 @@ -# see http://www.kennethreitz.org/essays/a-better-pip-workflow - -# app -Django -celery[redis] -django-model-utils -django-webpack-loader -django-js-reverse -django-import-export -python-decouple<=4.0 -psycopg2 - -# heroku -brotlipy -django-log-request-id -dj-database-url -gunicorn -opbeat -whitenoise - -# test -bandit -coverage -prospector[with_vulture] -isort -safety - -# local -django-naomi -fixmydjango -ipython -model-mommy -pre-commit -django-debug-toolbar -django-debug-toolbar-request-history