diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 000000000..93abdc7b5 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,20 @@ +[run] +branch = True +source = . +data_file=/tmp/coverage +omit = + mitxpro/wsgi.py + manage.py + ./.tox/* + */migrations/* + *_test.py + *_tests.py + +[report] +exclude_lines = + pragma: no cover +show_missing = True +precision = 2 + +[html] +directory = ${COVERAGE_DIR} diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..6517bdeb0 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +.tox +.cache +htmlcov +.coverage +node_modules +staticfiles/ diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..c6d9feab6 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +root = true + +[*.py] +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.js] +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.env.example b/.env.example new file mode 100644 index 000000000..6fcaf32ee --- /dev/null +++ b/.env.example @@ -0,0 +1,11 @@ +CELERY_TASK_ALWAYS_EAGER=True +DJANGO_LOG_LEVEL=INFO +LOG_LEVEL=INFO +SENTRY_LOG_LEVEL=ERROR +MAILGUN_KEY= +MAILGUN_URL= +MAILGUN_RECIPIENT_OVERRIDE= +SECRET_KEY= +STATUS_TOKEN= +UWSGI_THREAD_COUNT=5 +SENTRY_DSN= diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 000000000..99cf75fce --- /dev/null +++ b/.eslintrc @@ -0,0 +1,3 @@ +{ + "extends": "eslint-config-mitodl" +} diff --git a/.flowconfig b/.flowconfig new file mode 100644 index 000000000..dd97d56be --- /dev/null +++ b/.flowconfig @@ -0,0 +1,18 @@ +[ignore] +.*/node_modules/fbjs/.* +.*/node_modules/draft-js/.* +.*/node_modules/draftjs-utils/.* +.*/node_modules/react-event-listener/src/index.js +.*.git/.* + +[include] +./static/js + +[libs] +./static/js/flow/declarations.js +./flow-typed/npm/ + +[options] +esproposal.class_static_fields=enable +esproposal.class_instance_fields=enable +suppress_comment= \\(.\\|\n\\)*\\$FlowFixMe diff --git a/.github/ISSUE_TEMPLATE/bug_template.md b/.github/ISSUE_TEMPLATE/bug_template.md new file mode 100644 index 000000000..e6f01cedd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_template.md @@ -0,0 +1,28 @@ +--- +labels: bug +name: "Bug Report" +about: "Something isn't quite right" +--- +### Steps to Reproduce + +(List or Description) + +### Expected Behavior + +(Optional) + +### Actual Behavior + +(Optional) + +### Stacktrace + +(Optional) + +### Related Issues + +(Optional) + +### Screenshot or Screencast + +(Optional) diff --git a/.github/ISSUE_TEMPLATE/feature_template.md b/.github/ISSUE_TEMPLATE/feature_template.md new file mode 100644 index 000000000..e406ed64e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_template.md @@ -0,0 +1,17 @@ +--- +name: New Feature +about: "New feature to implement" +--- +As a X, I'd like to Y + +#### Designs and Mockups + + + +#### Acceptance Criteria: + +- [ ] Requirement + +#### Out of Scope + +- Not going to do diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md new file mode 100644 index 000000000..bca79c706 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md @@ -0,0 +1,41 @@ +--- +labels: "Work in Progress" +name: "Pull Request" +about: "Regular Pull Request" +--- + +#### Pre-Flight checklist + +- [ ] Screenshots and design review for any changes that affect layout or styling + - [ ] Desktop screenshots + - [ ] Mobile width screenshots + - [ ] Tag @ferdi or @pdpinch for review +- [ ] Migrations + - [ ] Migration is backwards-compatible with current production code +- [ ] Testing + - [ ] Code is tested + - [ ] Changes have been manually tested +- [ ] Settings + - [ ] New settings are documented and present in `app.json` + - [ ] New settings have reasonable development defaults, if applicable + +#### What are the relevant tickets? +(Required) + +#### What's this PR do? +(Required) + +#### How should this be manually tested? +(Required) + +#### Where should the reviewer start? +(Optional) + +#### Any background context you want to provide? +(Optional) + +#### Screenshots (if appropriate) +(Optional) + +#### What GIF best describes this PR or how it makes you feel? +(Optional) diff --git a/.github/PULL_REQUEST_TEMPLATE/rfc_template.md b/.github/PULL_REQUEST_TEMPLATE/rfc_template.md new file mode 100644 index 000000000..db9af2fd6 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/rfc_template.md @@ -0,0 +1,13 @@ +--- +title: 'RFC: ' +labels: RFC +name: "Request For Comment" +about: "Put up an RFC" +--- + + +[Rendered](https://github.com/mitodl/open-discussions/blob/COMMIT_BLOB/docs/rfcs/XXXX-feature-name.md) + +#### Relevant Issues + +#### Summary diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..6f2d4d337 --- /dev/null +++ b/.gitignore @@ -0,0 +1,103 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +./lib/ +./lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ +coverage/ +.nyc_output/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +celerybeat-schedule.db + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +#Ipython/Jupyter Notebook +.ipynb_checkpoints +*.ipynb + +# Heroku/Foreman +.env + +# webpack +static/bundles +node_modules + +# JS directories +!static/js/lib + +# Editor stuff +*.swp +*.orig + +/nbproject +.idea/ +.redcar/ +codekit-config.json +.pycharm_helpers/ + +.project +.pydevproject + +*.DS_Store + +# Django static +staticfiles/ + +.venv +release-notes-checklist + +# Webpack +webpack-stats.json + +# Celery Beat +celerybeat-schedule diff --git a/.istanbul.yml b/.istanbul.yml new file mode 100644 index 000000000..6c8f22862 --- /dev/null +++ b/.istanbul.yml @@ -0,0 +1,2 @@ +instrumentation: + excludes: ['**/*_test.js'] diff --git a/.rsync-ignore b/.rsync-ignore new file mode 100644 index 000000000..57080acef --- /dev/null +++ b/.rsync-ignore @@ -0,0 +1,8 @@ +node_modules +.git +.tox +.rsync-ignore +Makefile +README.rst +.DS_Store +.idea/ diff --git a/.sass-lint.yml b/.sass-lint.yml new file mode 100644 index 000000000..55a751a30 --- /dev/null +++ b/.sass-lint.yml @@ -0,0 +1,39 @@ +options: + merge-default-rules: false +files: + include: 'static/scss/**/*.scss' +rules: + extends-before-mixins: 2 + extends-before-declarations: 2 + placeholder-in-extend: 2 + mixins-before-declarations: 2 + no-warn: 1 + no-duplicate-properties: 2 + no-debug: 1 + no-important: 0 + no-empty-rulesets: 2 + no-misspelled-properties: 2 + no-mergeable-selectors: 2 + no-trailing-whitespace: 2 + trailing-semicolon: 2 + declarations-before-nesting: 2 + space-between-parens: 2 + no-vendor-prefixes: 2 + one-declaration-per-line: 2 + space-before-colon: 2 + space-after-colon: 2 + space-before-bang: 2 + hex-notation: + - 2 + - + style: lowercase + indentation: + - 2 + - + size: 2 + brace-style: + - 2 + - + allow-single-line: false + empty-line-between-blocks: 2 + space-before-brace: 2 diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..b422deb08 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,26 @@ +sudo: false +# Set Ruby as the language so it doesn't download the pip things. Instead, let docker do that. +language: ruby + +matrix: + include: + - install: + - env | grep TRAVIS > .env + - env | grep CI >> .env + - docker-compose -f docker-compose.yml -f docker-compose.travis.yml run celery echo + script: (docker-compose -f docker-compose.yml -f docker-compose.travis.yml up celery &); sleep 5; docker-compose -f docker-compose.yml -f docker-compose.travis.yml run web tox + services: + - docker + env: + name: Python + - install: + - env | grep TRAVIS > .env + - env | grep CI >> .env + # Uncomment after we upload docker images + # - docker build -t travis-watch -f ./travis/Dockerfile-travis-watch . + - docker build -t travis-watch -f ./Dockerfile-node . + script: bash ./travis/js_tests.sh + services: + - docker + env: + name: JavaScript diff --git a/Aptfile b/Aptfile new file mode 100644 index 000000000..42c88c553 --- /dev/null +++ b/Aptfile @@ -0,0 +1 @@ +libxmlsec1-dev diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..ffefcdac9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,43 @@ +FROM python:3.6.4 +LABEL maintainer "ODL DevOps " + +# Add package files, install updated node and pip +WORKDIR /tmp + +# Install packages and add repo needed for postgres 9.6 +COPY apt.txt /tmp/apt.txt +RUN echo deb http://apt.postgresql.org/pub/repos/apt/ jessie-pgdg main > /etc/apt/sources.list.d/pgdg.list +RUN curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - +RUN apt-get update +RUN apt-get install -y $(grep -vE "^\s*#" apt.txt | tr "\n" " ") + +# Add repo needed for postgres 9.6 and install it +RUN apt-get update && apt-get install libpq-dev postgresql-client-9.6 -y + +# pip +RUN curl --silent --location https://bootstrap.pypa.io/get-pip.py | python3 - + +# Add, and run as, non-root user. +RUN mkdir /src +RUN adduser --disabled-password --gecos "" mitodl +RUN mkdir /var/media && chown -R mitodl:mitodl /var/media + +# Install project packages +COPY requirements.txt /tmp/requirements.txt +COPY test_requirements.txt /tmp/test_requirements.txt +RUN pip install -r requirements.txt -r test_requirements.txt + +# Add project +COPY . /src +WORKDIR /src +RUN chown -R mitodl:mitodl /src + +RUN apt-get clean && apt-get purge +USER mitodl + +# Set pip cache folder, as it is breaking pip when it is on a shared volume +ENV XDG_CACHE_HOME /tmp/.cache + +EXPOSE 8053 +ENV PORT 8053 +CMD uwsgi uwsgi.ini diff --git a/Dockerfile-node b/Dockerfile-node new file mode 100644 index 000000000..ef402e6d5 --- /dev/null +++ b/Dockerfile-node @@ -0,0 +1,16 @@ +FROM node:9.3 +LABEL maintainer "ODL DevOps " + +RUN apt-get update && apt-get install libelf1 + +COPY package.json /src/ + +COPY scripts /src/scripts + +RUN node /src/scripts/install_yarn.js + +RUN mkdir -p /home/node/.cache/yarn + +RUN chown node:node /home/node/.cache/yarn + +USER node diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..7ab0bf72d --- /dev/null +++ b/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2017, MIT Office of Digital Learning +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Procfile b/Procfile new file mode 100644 index 000000000..bb99c2c57 --- /dev/null +++ b/Procfile @@ -0,0 +1,3 @@ +web: bin/start-nginx bin/start-pgbouncer-stunnel newrelic-admin run-program uwsgi uwsgi.ini +worker: celery -A mitxpro.celery:app worker -B -l $MITXPRO_LOG_LEVEL +extra_worker: celery -A mitxpro.celery:app worker -l $MITXPRO_LOG_LEVEL diff --git a/README.md b/README.md new file mode 100644 index 000000000..c43479c5f --- /dev/null +++ b/README.md @@ -0,0 +1,124 @@ +# mitxpro + + +## Major Dependencies +- Docker + - OSX recommended install method: [Download from Docker website](https://docs.docker.com/mac/) +- docker-compose + - Recommended install: pip (`pip install docker-compose`) +- Virtualbox (https://www.virtualbox.org/wiki/Downloads) +- _(OSX only)_ Node/NPM, and Yarn + - OSX recommended install method: [Installer on Node website](https://nodejs.org/en/download/) + - No specific version has been chosen yet. + +## (OSX only) Getting your machine Docker-ready + +#### Create your docker container: + +The following commands create a Docker machine named ``mitxpro``, start the +container, and configure environment variables to facilitate communication +with the edX instance. + + docker-machine create --driver virtualbox mitxpro + docker-machine start mitxpro + # 'docker-machine env (machine)' prints export commands for important environment variables + eval "$(docker-machine env mitxpro)" + + +## Docker Container Configuration and Start-up + +#### 1) Create your ``.env`` file + +This file should be copied from the example in the codebase: + + cp .env.example .env + +#### 2) _(OSX only)_ Set up and run the webpack dev server on your host machine + +In the development environment, our static assets are served from the webpack +dev server. When this environment variable is set, the script sources will +look for the webpack server at that host instead of the host where Docker is running. + +You'll need to install the [yarn](https://yarnpkg.com/en/docs/cli/) +package manager. You can do: + + sudo node ./scripts/install_yarn.js + +To install it. Nice! You can check which version is installed in +`package.json` to be make you're getting the version we are +standardized on. + +Now, in a separate terminal tab, use the webpack helper script to install npm modules and run the dev server: + + ./webpack_dev_server.sh --install + +The ``--install`` flag is only needed if you're starting up for the first time, or if updates have been made +to the packages in ``./package.json``. If you've installed those packages and there are no ``./package.json`` +updates, you can run this without the ``--install`` flag: ``./webpack_dev_server.sh`` + +**DEBUGGING NOTE:** If you see an error related to node-sass when you run this script, try running +``yarn install`` again. + +#### 3) Build the containers +Run this command: + + docker-compose build + +You will also need to run this command whenever ``requirements.txt`` or ``test_requirements.txt`` change. + +#### 4) Run migrations +Create the database tables from the Django models: + + docker-compose run web ./manage.py migrate + +#### 5) Run the container + +Start Django, PostgreSQL, and other related services: + + docker-compose up + +In another terminal tab, navigate to the mitxpro directory +and add a superuser in the now-running Docker container: + + docker-compose run web ./manage.py createsuperuser + +You should now be able to do the following: + +1. Visit mitxpro in your browser on port `8053`. _(OSX Only)_ Docker auto-assigns + the container IP. Run ``docker-machine ip`` to see it. Your mitxpro URL will + be something like this: ``192.168.99.100:8053``. + +## Running Commands and Testing + +As shown above, manage commands can be executed on the Docker-contained +mitxpro app. For example, you can run a Python shell with the following command: + + docker-compose run web ./manage.py shell + +Tests should be run in the Docker container, not the host machine. They can be run with the following commands: + + # Run the full suite + ./test_suite.sh + # Run Python tests only + docker-compose run web tox + # Single file test + docker-compose run web tox /path/to/test.py + # Run the JS tests with coverage report + docker-compose run watch npm run-script coverage + # run the JS tests without coverage report + docker-compose run watch npm test + # run a single JS test file + docker-compose run watch npm run-script singleTest /path/to/test.js + # Run the JS linter + docker-compose run watch npm run-script lint + # Run JS type-checking + docker-compose run watch npm run-script flow + # Run SCSS linter + docker-compose run watch npm run scss_lint + +Note that running [`flow`](https://flowtype.org) may not work properly if your +host machine isn't running Linux. If you are using a Mac, you'll need to run +`flow` on your host machine, like this: + + yarn install --frozen-lockfile + npm run-script flow diff --git a/app.json b/app.json new file mode 100644 index 000000000..a4d09d9cb --- /dev/null +++ b/app.json @@ -0,0 +1,145 @@ +{ + "addons": [ + "heroku-postgresql:hobby-dev", + "newrelic:wayne", + "rediscloud:30" + ], + "buildpacks": [ + { + "url": "https://github.com/heroku/heroku-buildpack-nodejs" + }, + { + "url": "https://github.com/heroku/heroku-buildpack-python" + }, + { + "url": "https://github.com/heroku/heroku-buildpack-pgbouncer" + }, + { + "url": "https://github.com/heroku/heroku-buildpack-nginx" + }, + { + "url": "https://github.com/heroku/heroku-buildpack-apt" + } + ], + "description": " ", + "env": { + "AWS_ACCESS_KEY_ID": { + "description": "AWS Access Key for S3 storage." + }, + "AWS_SECRET_ACCESS_KEY": { + "description": "AWS Secret Key for S3 storage." + }, + "AWS_STORAGE_BUCKET_NAME": { + "description": "S3 Bucket name." + }, + "GA_TRACKING_ID": { + "description": "Google analytics tracking ID", + "required": false + }, + "MAILGUN_URL": { + "description": "The URL for communicating with Mailgun" + }, + "MAILGUN_KEY": { + "description": "The token for authenticating against the Mailgun API" + }, + "MAILGUN_BATCH_CHUNK_SIZE": { + "description": "Maximum number of emails to send in a batch", + "required": false + }, + "MAILGUN_FROM_EMAIL": { + "description": "Email which mail comes from" + }, + "MAILGUN_BCC_TO_EMAIL": { + "description": "Email address used with bcc email" + }, + "MITXPRO_ADMIN_EMAIL": { + "description": "E-mail to send 500 reports to." + }, + "MITXPRO_DB_CONN_MAX_AGE": { + "value": "0" + }, + "MITXPRO_DB_DISABLE_SSL": { + "value": "true" + }, + "MITXPRO_EMAIL_HOST": { + "description": "Outgoing e-mail settings" + }, + "MITXPRO_EMAIL_PASSWORD": { + "description": "Outgoing e-mail settings" + }, + "MITXPRO_EMAIL_PORT": { + "description": "Outgoing e-mail settings", + "value": "587" + }, + "MITXPRO_EMAIL_TLS": { + "description": "Outgoing e-mail settings", + "value": "True" + }, + "MITXPRO_EMAIL_USER": { + "description": "Outgoing e-mail settings" + }, + "MITXPRO_ENVIRONMENT": { + "description": "The execution environment that the app is in (e.g. dev, staging, prod)" + }, + "MITXPRO_FROM_EMAIL": { + "description": "E-mail to use for the from field" + }, + "MITXPRO_LOG_LEVEL": { + "description": "The logging level for the application", + "required": true, + "value": "INFO" + }, + "MITXPRO_SECURE_SSL_REDIRECT": { + "description": "Application-level SSL redirect setting.", + "required": false, + "value": "True" + }, + "MITXPRO_SUPPORT_EMAIL": { + "description": "Email address listed for customer support" + }, + "MITXPRO_USE_S3": { + "description": "Use S3 for storage backend (required on Heroku)", + "value": "True" + }, + "NEW_RELIC_APP_NAME": { + "description": "Application identifier in New Relic." + }, + "NODE_MODULES_CACHE": { + "description": "If false, disables the node_modules cache to fix yarn install", + "value": "false" + }, + "PGBOUNCER_DEFAULT_POOL_SIZE": { + "value": "50" + }, + "PGBOUNCER_MIN_POOL_SIZE": { + "value": "5" + }, + "SECRET_KEY": { + "description": "Django secret key.", + "generator": "secret" + }, + "SENTRY_DSN": { + "description": "The connection settings for Sentry" + }, + "SENTRY_LOG_LEVEL": { + "description": "The log level for Sentry", + "required": false + }, + "STATUS_TOKEN": { + "description": "Token to access the status API." + } + }, + "keywords": [ + "Django", + "Python", + "MIT", + "Office of Digital Learning" + ], + "name": "mitxpro", + "repository": "https://github.com/mitodl/mitxpro", + "scripts": { + "postdeploy": "./manage.py migrate --noinput" + }, + "success_url": "/", + "website": "https://github.com/mitodl/mitxpro" +} diff --git a/apt.txt b/apt.txt new file mode 100644 index 000000000..8cf1677d4 --- /dev/null +++ b/apt.txt @@ -0,0 +1,6 @@ +# Core requirements +git +curl +libjpeg-dev +zlib1g-dev +net-tools diff --git a/bin/build_heroku_pipeline.sh b/bin/build_heroku_pipeline.sh new file mode 100755 index 000000000..afce37f4e --- /dev/null +++ b/bin/build_heroku_pipeline.sh @@ -0,0 +1,46 @@ +#!/bin/bash + +APP_NAME="$1" + +if [ -z $APP_NAME ] +then + echo "Usage: bin/build_heroku_pipeline.sh some-app-name" + exit 1 +fi + +if [ -z `which heroku` ] +then + echo "You need to install the Heroku CLI for this script to work. https://devcenter.heroku.com/articles/heroku-cli" + exit 1 +fi + +if [ -z `which jq` ] +then + echo "You need to install the 'jq' command line tool for this script to work. https://stedolan.github.io/jq/" + exit 1 +fi + +if [ ! -f 'app.json' ] +then + echo "Could not find the 'app.json' file. Either change to the directory where it exists or create one. https://devcenter.heroku.com/articles/app-json-schema " + exit 1 +fi + +for suffix in '-ci' '-rc' '' +do + heroku apps:create $APP_NAME$suffix + + for addon_ in `jq '.addons | .[]' app.json` + do + heroku addons:add ${addon_//\"} -a $APP_NAME$suffix + done + + for buildpack_ in `jq ".buildpacks | .[] | .url" app.json` + do + heroku buildpacks:add ${buildpack_//\"} -a $APP_NAME$suffix + done +done + +heroku pipelines:create $APP_NAME -a $APP_NAME-ci -s development +heroku pipelines:add $APP_NAME -a $APP_NAME-rc -s staging +heroku pipelines:add $APP_NAME -a $APP_NAME -s production diff --git a/bin/post_compile b/bin/post_compile new file mode 100755 index 000000000..240e80000 --- /dev/null +++ b/bin/post_compile @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +set -eo pipefail +indent() { + RE="s/^/ /" + [ $(uname) == "Darwin" ] && sed -l "$RE" || sed -u "$RE" +} + +MANAGE_FILE=$(find . -maxdepth 3 -type f -name 'manage.py' | head -1) +MANAGE_FILE=${MANAGE_FILE:2} + +# Run migrations + +echo "-----> Running django migrations" +python $MANAGE_FILE showmigrations --list 2>&1 | indent +python $MANAGE_FILE migrate --noinput 2>&1 | indent + +echo diff --git a/bin/pre_compile b/bin/pre_compile new file mode 100755 index 000000000..fcf240562 --- /dev/null +++ b/bin/pre_compile @@ -0,0 +1,6 @@ +#!/bin/sh + +# Syntax bin/compile + +echo "-----> Writing out SOURCE_VERSION to static/hash ($SOURCE_VERSION)" +echo $SOURCE_VERSION > $BUILD_DIR/static/hash.txt diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 000000000..fe88d8e91 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,11 @@ +coverage: + status: + project: + default: + enabled: no + patch: + default: + enabled: no + changes: + default: + enabled: no diff --git a/config/nginx.conf b/config/nginx.conf new file mode 100644 index 000000000..42b988a8d --- /dev/null +++ b/config/nginx.conf @@ -0,0 +1,25 @@ +# This is the version used in development environments +server { + listen 8053 default_server; + root /src; + + location = /robots.txt { + alias /src/static/robots.txt; + } + + location = /.well-known/dnt-policy.txt { + return 204; + } + + location = /favicon.ico { + try_files /static/images/favicon.ico /favicon.ico; + } + + location / { + include uwsgi_params; + uwsgi_pass web:8051; + uwsgi_pass_request_headers on; + uwsgi_pass_request_body on; + client_max_body_size 25M; + } +} diff --git a/config/nginx.conf.erb b/config/nginx.conf.erb new file mode 100644 index 000000000..76a3b7c5a --- /dev/null +++ b/config/nginx.conf.erb @@ -0,0 +1,84 @@ +# This file is for configuring Nginx on Heroku using the nginx-buildpack + +daemon off; +#Heroku dynos have at least 4 cores. +worker_processes <%= ENV['NGINX_WORKERS'] || 4 %>; + +events { + use epoll; + accept_mutex on; + worker_connections 1024; +} + +http { + gzip on; + gzip_comp_level 2; + gzip_min_length 512; + + server_tokens off; + + log_format l2met 'measure#nginx.service=$request_time request_id=$http_x_request_id'; + log_format apm '"$time_local" client=$remote_addr ' + 'method=$request_method request="$request" ' + 'request_length=$request_length ' + 'status=$status bytes_sent=$bytes_sent ' + 'body_bytes_sent=$body_bytes_sent ' + 'referer=$http_referer ' + 'user_agent="$http_user_agent" ' + 'upstream_addr=$upstream_addr ' + 'upstream_status=$upstream_status ' + 'request_time=$request_time ' + 'upstream_response_time=$upstream_response_time ' + 'upstream_connect_time=$upstream_connect_time ' + 'upstream_header_time=$upstream_header_time'; + access_log logs/nginx/access.log apm; + error_log logs/nginx/error.log; + + include mime.types; + default_type application/octet-stream; + sendfile on; + + server { + listen <%= ENV["PORT"] %> default_server; + server_name _; + root /app; + + location = /favicon.ico { + try_files /static/images/favicon.ico /favicon.ico; + } + + location ~* /static/(.*$) { + expires max; + add_header Access-Control-Allow-Origin *; + try_files $uri $uri/ /staticfiles/$1 /staticfiles/$1/ =404; + } + + location = /robots.txt { + alias /app/static/robots.txt; + } + + location = /.well-known/dnt-policy.txt { + return 204; + } + + location / { + uwsgi_param QUERY_STRING $query_string; + uwsgi_param REQUEST_METHOD $request_method; + uwsgi_param CONTENT_TYPE $content_type; + uwsgi_param CONTENT_LENGTH $content_length; + uwsgi_param REQUEST_URI $request_uri; + uwsgi_param PATH_INFO $document_uri; + uwsgi_param DOCUMENT_ROOT $document_root; + uwsgi_param SERVER_PROTOCOL $server_protocol; + uwsgi_param REMOTE_ADDR $remote_addr; + uwsgi_param REMOTE_PORT $remote_port; + uwsgi_param SERVER_ADDR $server_addr; + uwsgi_param SERVER_PORT $server_port; + uwsgi_param SERVER_NAME $server_name; + uwsgi_pass unix:/tmp/nginx.socket; + uwsgi_pass_request_headers on; + uwsgi_pass_request_body on; + client_max_body_size 25M; + } + } +} diff --git a/docker-compose.override.yml b/docker-compose.override.yml new file mode 100644 index 000000000..0acb22d84 --- /dev/null +++ b/docker-compose.override.yml @@ -0,0 +1,44 @@ +version: '2.1' +services: + python: + volumes: + - .:/src + - django_media:/var/media + environment: + DEBUG: 'True' + NODE_ENV: 'development' + MITXPRO_USE_WEBPACK_DEV_SERVER: 'True' + + web: + volumes: + - .:/src + - django_media:/var/media + environment: + DEBUG: 'True' + NODE_ENV: 'development' + MITXPRO_USE_WEBPACK_DEV_SERVER: 'True' + + celery: + volumes: + - .:/src + - django_media:/var/media + environment: + DEBUG: 'True' + NODE_ENV: 'development' + MITXPRO_USE_WEBPACK_DEV_SERVER: 'True' + + nginx: + volumes: + - ./config/nginx.conf:/etc/nginx/conf.d/web.conf + - ./:/src + + watch: + volumes: + - .:/src + - yarn-cache:/home/mitodl/.cache/yarn + environment: + NODE_ENV: 'development' + +volumes: + django_media: {} + yarn-cache: {} diff --git a/docker-compose.travis.yml b/docker-compose.travis.yml new file mode 100644 index 000000000..8e379216e --- /dev/null +++ b/docker-compose.travis.yml @@ -0,0 +1,26 @@ +version: '2.1' +services: + python: + build: + context: . + # TODO: upload dockerfile first then uncomment this line + # dockerfile: ./travis/Dockerfile-travis-web + dockerfile: ./Dockerfile + + web: + build: + context: . + # TODO: upload dockerfile first then uncomment this line + # dockerfile: ./travis/Dockerfile-travis-web + dockerfile: ./Dockerfile + + celery: + build: + context: . + # TODO: upload dockerfile first then uncomment this line + # dockerfile: ./travis/Dockerfile-travis-web + dockerfile: ./Dockerfile + environment: + # for celery, to avoid ImproperlyConfigured + MAILGUN_URL: 'http://fake.example.com' + MAILGUN_KEY: 'fake' diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..4a867edf0 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,83 @@ +version: '2.1' +services: + db: + image: postgres + ports: + - "5432" + + redis: + image: redis + ports: + - "6379" + + nginx: + image: nginx + ports: + - "8053:8053" + links: + - web + + python: + build: + context: . + dockerfile: Dockerfile + command: /bin/true + environment: + DEBUG: 'False' + NODE_ENV: 'production' + DEV_ENV: 'True' # necessary to have nginx connect to web container + PORT: 8051 + COVERAGE_DIR: htmlcov + DATABASE_URL: postgres://postgres@db:5432/postgres + MITXPRO_SECURE_SSL_REDIRECT: 'False' + MITXPRO_DB_DISABLE_SSL: 'True' + ELASTICSEARCH_URL: elastic:9200 + CELERY_TASK_ALWAYS_EAGER: 'False' + CELERY_BROKER_URL: redis://redis:6379/4 + CELERY_RESULT_BACKEND: redis://redis:6379/4 + DOCKER_HOST: ${DOCKER_HOST:-missing} + WEBPACK_DEV_SERVER_HOST: ${WEBPACK_DEV_SERVER_HOST:-localhost} + env_file: .env + + web: + image: mitxpro_python + extends: + service: python + command: > + /bin/bash -c ' + sleep 3 && + python3 manage.py collectstatic --noinput && + python3 manage.py migrate --noinput && + uwsgi uwsgi.ini' + ports: + - "8051:8051" + links: + - db + - redis + + watch: + build: + context: . + dockerfile: Dockerfile-node + working_dir: /src + command: > + /bin/bash -c './webpack_dev_server.sh --install' + ports: + - "8052:8052" + environment: + NODE_ENV: 'production' + DOCKER_HOST: ${DOCKER_HOST:-missing} + CONTAINER_NAME: 'watch' + env_file: .env + + celery: + image: mitxpro_python + extends: + service: python + command: > + /bin/bash -c ' + sleep 3; + celery -A mitxpro.celery:app worker -B -l ${MITXPRO_LOG_LEVEL:-INFO}' + links: + - db + - redis diff --git a/docs/rfcs/0000-template.md b/docs/rfcs/0000-template.md new file mode 100644 index 000000000..00bd5d204 --- /dev/null +++ b/docs/rfcs/0000-template.md @@ -0,0 +1,12 @@ +## Title for RFC + +#### Abstract + + +#### Architecture Changes + + +#### Security Considerations + + +#### Testing & Rollout diff --git a/flow-typed/npm/lodash_v4.x.x.js b/flow-typed/npm/lodash_v4.x.x.js new file mode 100644 index 000000000..7a4af6a7c --- /dev/null +++ b/flow-typed/npm/lodash_v4.x.x.js @@ -0,0 +1,4357 @@ +// flow-typed signature: cd54ebd9cef0bce96a268ad51fc9df0b +// flow-typed version: da30fe6876/lodash_v4.x.x/flow_>=v0.47.x <=v0.54.x + +declare module "lodash" { + declare type __CurriedFunction1 = (...r: [AA]) => R; + declare type CurriedFunction1 = __CurriedFunction1; + + declare type __CurriedFunction2 = (( + ...r: [AA] + ) => CurriedFunction1) & + ((...r: [AA, BB]) => R); + declare type CurriedFunction2 = __CurriedFunction2; + + declare type __CurriedFunction3 = (( + ...r: [AA] + ) => CurriedFunction2) & + ((...r: [AA, BB]) => CurriedFunction1) & + ((...r: [AA, BB, CC]) => R); + declare type CurriedFunction3 = __CurriedFunction3< + A, + B, + C, + R, + *, + *, + * + >; + + declare type __CurriedFunction4< + A, + B, + C, + D, + R, + AA: A, + BB: B, + CC: C, + DD: D + > = ((...r: [AA]) => CurriedFunction3) & + ((...r: [AA, BB]) => CurriedFunction2) & + ((...r: [AA, BB, CC]) => CurriedFunction1) & + ((...r: [AA, BB, CC, DD]) => R); + declare type CurriedFunction4 = __CurriedFunction4< + A, + B, + C, + D, + R, + *, + *, + *, + * + >; + + declare type __CurriedFunction5< + A, + B, + C, + D, + E, + R, + AA: A, + BB: B, + CC: C, + DD: D, + EE: E + > = ((...r: [AA]) => CurriedFunction4) & + ((...r: [AA, BB]) => CurriedFunction3) & + ((...r: [AA, BB, CC]) => CurriedFunction2) & + ((...r: [AA, BB, CC, DD]) => CurriedFunction1) & + ((...r: [AA, BB, CC, DD, EE]) => R); + declare type CurriedFunction5 = __CurriedFunction5< + A, + B, + C, + D, + E, + R, + *, + *, + *, + *, + * + >; + + declare type __CurriedFunction6< + A, + B, + C, + D, + E, + F, + R, + AA: A, + BB: B, + CC: C, + DD: D, + EE: E, + FF: F + > = ((...r: [AA]) => CurriedFunction5) & + ((...r: [AA, BB]) => CurriedFunction4) & + ((...r: [AA, BB, CC]) => CurriedFunction3) & + ((...r: [AA, BB, CC, DD]) => CurriedFunction2) & + ((...r: [AA, BB, CC, DD, EE]) => CurriedFunction1) & + ((...r: [AA, BB, CC, DD, EE, FF]) => R); + declare type CurriedFunction6 = __CurriedFunction6< + A, + B, + C, + D, + E, + F, + R, + *, + *, + *, + *, + *, + * + >; + + declare type Curry = (((...r: [A]) => R) => CurriedFunction1) & + (((...r: [A, B]) => R) => CurriedFunction2) & + (((...r: [A, B, C]) => R) => CurriedFunction3) & + (( + (...r: [A, B, C, D]) => R + ) => CurriedFunction4) & + (( + (...r: [A, B, C, D, E]) => R + ) => CurriedFunction5) & + (( + (...r: [A, B, C, D, E, F]) => R + ) => CurriedFunction6); + + declare type UnaryFn = (a: A) => R; + + declare type Flow = (( + ab: UnaryFn, + bc: UnaryFn, + cd: UnaryFn, + de: UnaryFn, + ef: UnaryFn, + fg: UnaryFn, + ...rest: Array + ) => UnaryFn) & + (( + ab: UnaryFn, + bc: UnaryFn, + cd: UnaryFn, + de: UnaryFn, + ef: UnaryFn, + ...rest: Array + ) => UnaryFn) & + (( + ab: UnaryFn, + bc: UnaryFn, + cd: UnaryFn, + de: UnaryFn, + ...rest: Array + ) => UnaryFn) & + (( + ab: UnaryFn, + bc: UnaryFn, + cd: UnaryFn, + ...rest: Array + ) => UnaryFn) & + (( + ab: UnaryFn, + bc: UnaryFn, + ...rest: Array + ) => UnaryFn) & + ((ab: UnaryFn, ...rest: Array) => UnaryFn); + + declare type FlowRight = (( + fg: UnaryFn, + ef: UnaryFn, + de: UnaryFn, + cd: UnaryFn, + bc: UnaryFn, + ab: UnaryFn, + ...rest: Array + ) => UnaryFn) & + (( + ef: UnaryFn, + de: UnaryFn, + cd: UnaryFn, + bc: UnaryFn, + ab: UnaryFn, + ...rest: Array + ) => UnaryFn) & + (( + de: UnaryFn, + cd: UnaryFn, + bc: UnaryFn, + ab: UnaryFn, + ...rest: Array + ) => UnaryFn) & + (( + cd: UnaryFn, + bc: UnaryFn, + ab: UnaryFn, + ...rest: Array + ) => UnaryFn) & + (( + bc: UnaryFn, + ab: UnaryFn, + ...rest: Array + ) => UnaryFn) & + ((ab: UnaryFn, ...rest: Array) => UnaryFn); + + declare type TemplateSettings = { + escape?: RegExp, + evaluate?: RegExp, + imports?: Object, + interpolate?: RegExp, + variable?: string + }; + + declare type TruncateOptions = { + length?: number, + omission?: string, + separator?: RegExp | string + }; + + declare type DebounceOptions = { + leading?: boolean, + maxWait?: number, + trailing?: boolean + }; + + declare type ThrottleOptions = { + leading?: boolean, + trailing?: boolean + }; + + declare type NestedArray = Array>; + + declare type matchesIterateeShorthand = Object; + declare type matchesPropertyIterateeShorthand = [string, any]; + declare type propertyIterateeShorthand = string; + + declare type OPredicate = + | ((value: A, key: string, object: O) => any) + | matchesIterateeShorthand + | matchesPropertyIterateeShorthand + | propertyIterateeShorthand; + + declare type OIterateeWithResult = + | Object + | string + | ((value: V, key: string, object: O) => R); + declare type OIteratee = OIterateeWithResult; + declare type OFlatMapIteratee = OIterateeWithResult>; + + declare type Predicate = + | ((value: T, index: number, array: Array) => any) + | matchesIterateeShorthand + | matchesPropertyIterateeShorthand + | propertyIterateeShorthand; + + declare type _ValueOnlyIteratee = (value: T) => mixed; + declare type ValueOnlyIteratee = _ValueOnlyIteratee | string; + declare type _Iteratee = ( + item: T, + index: number, + array: ?Array + ) => mixed; + declare type Iteratee = _Iteratee | Object | string; + declare type FlatMapIteratee = + | ((item: T, index: number, array: ?Array) => Array) + | Object + | string; + declare type Comparator = (item: T, item2: T) => boolean; + + declare type MapIterator = + | ((item: T, index: number, array: Array) => U) + | propertyIterateeShorthand; + + declare type OMapIterator = + | ((item: T, key: string, object: O) => U) + | propertyIterateeShorthand; + + declare class Lodash { + // Array + chunk(array: ?Array, size?: number): Array>; + compact(array: Array): Array; + concat(base: Array, ...elements: Array): Array; + difference(array: ?Array, values?: Array): Array; + differenceBy( + array: ?Array, + values: Array, + iteratee: ValueOnlyIteratee + ): T[]; + differenceWith(array: T[], values: T[], comparator?: Comparator): T[]; + drop(array: ?Array, n?: number): Array; + dropRight(array: ?Array, n?: number): Array; + dropRightWhile(array: ?Array, predicate?: Predicate): Array; + dropWhile(array: ?Array, predicate?: Predicate): Array; + fill( + array: ?Array, + value: U, + start?: number, + end?: number + ): Array; + findIndex( + array: ?Array, + predicate?: Predicate, + fromIndex?: number + ): number; + findLastIndex( + array: ?Array, + predicate?: Predicate, + fromIndex?: number + ): number; + // alias of _.head + first(array: ?Array): T; + flatten(array: Array | X>): Array; + flattenDeep(array: any[]): Array; + flattenDepth(array: any[], depth?: number): any[]; + fromPairs(pairs: Array<[A, B]>): { [key: A]: B }; + head(array: ?Array): T; + indexOf(array: ?Array, value: T, fromIndex?: number): number; + initial(array: ?Array): Array; + intersection(...arrays: Array>): Array; + //Workaround until (...parameter: T, parameter2: U) works + intersectionBy(a1: Array, iteratee?: ValueOnlyIteratee): Array; + intersectionBy( + a1: Array, + a2: Array, + iteratee?: ValueOnlyIteratee + ): Array; + intersectionBy( + a1: Array, + a2: Array, + a3: Array, + iteratee?: ValueOnlyIteratee + ): Array; + intersectionBy( + a1: Array, + a2: Array, + a3: Array, + a4: Array, + iteratee?: ValueOnlyIteratee + ): Array; + //Workaround until (...parameter: T, parameter2: U) works + intersectionWith(a1: Array, comparator: Comparator): Array; + intersectionWith( + a1: Array, + a2: Array, + comparator: Comparator + ): Array; + intersectionWith( + a1: Array, + a2: Array, + a3: Array, + comparator: Comparator + ): Array; + intersectionWith( + a1: Array, + a2: Array, + a3: Array, + a4: Array, + comparator: Comparator + ): Array; + join(array: ?Array, separator?: string): string; + last(array: ?Array): T; + lastIndexOf(array: ?Array, value: T, fromIndex?: number): number; + nth(array: T[], n?: number): T; + pull(array: ?Array, ...values?: Array): Array; + pullAll(array: ?Array, values: Array): Array; + pullAllBy( + array: ?Array, + values: Array, + iteratee?: ValueOnlyIteratee + ): Array; + pullAllWith(array?: T[], values: T[], comparator?: Function): T[]; + pullAt(array: ?Array, ...indexed?: Array): Array; + pullAt(array: ?Array, indexed?: Array): Array; + remove(array: ?Array, predicate?: Predicate): Array; + reverse(array: ?Array): Array; + slice(array: ?Array, start?: number, end?: number): Array; + sortedIndex(array: ?Array, value: T): number; + sortedIndexBy( + array: ?Array, + value: T, + iteratee?: ValueOnlyIteratee + ): number; + sortedIndexOf(array: ?Array, value: T): number; + sortedLastIndex(array: ?Array, value: T): number; + sortedLastIndexBy( + array: ?Array, + value: T, + iteratee?: ValueOnlyIteratee + ): number; + sortedLastIndexOf(array: ?Array, value: T): number; + sortedUniq(array: ?Array): Array; + sortedUniqBy(array: ?Array, iteratee?: (value: T) => mixed): Array; + tail(array: ?Array): Array; + take(array: ?Array, n?: number): Array; + takeRight(array: ?Array, n?: number): Array; + takeRightWhile(array: ?Array, predicate?: Predicate): Array; + takeWhile(array: ?Array, predicate?: Predicate): Array; + union(...arrays?: Array>): Array; + //Workaround until (...parameter: T, parameter2: U) works + unionBy(a1: Array, iteratee?: ValueOnlyIteratee): Array; + unionBy( + a1: Array, + a2: Array, + iteratee?: ValueOnlyIteratee + ): Array; + unionBy( + a1: Array, + a2: Array, + a3: Array, + iteratee?: ValueOnlyIteratee + ): Array; + unionBy( + a1: Array, + a2: Array, + a3: Array, + a4: Array, + iteratee?: ValueOnlyIteratee + ): Array; + //Workaround until (...parameter: T, parameter2: U) works + unionWith(a1: Array, comparator?: Comparator): Array; + unionWith( + a1: Array, + a2: Array, + comparator?: Comparator + ): Array; + unionWith( + a1: Array, + a2: Array, + a3: Array, + comparator?: Comparator + ): Array; + unionWith( + a1: Array, + a2: Array, + a3: Array, + a4: Array, + comparator?: Comparator + ): Array; + uniq(array: ?Array): Array; + uniqBy(array: ?Array, iteratee?: ValueOnlyIteratee): Array; + uniqWith(array: ?Array, comparator?: Comparator): Array; + unzip(array: ?Array): Array; + unzipWith(array: ?Array, iteratee?: Iteratee): Array; + without(array: ?Array, ...values?: Array): Array; + xor(...array: Array>): Array; + //Workaround until (...parameter: T, parameter2: U) works + xorBy(a1: Array, iteratee?: ValueOnlyIteratee): Array; + xorBy( + a1: Array, + a2: Array, + iteratee?: ValueOnlyIteratee + ): Array; + xorBy( + a1: Array, + a2: Array, + a3: Array, + iteratee?: ValueOnlyIteratee + ): Array; + xorBy( + a1: Array, + a2: Array, + a3: Array, + a4: Array, + iteratee?: ValueOnlyIteratee + ): Array; + //Workaround until (...parameter: T, parameter2: U) works + xorWith(a1: Array, comparator?: Comparator): Array; + xorWith( + a1: Array, + a2: Array, + comparator?: Comparator + ): Array; + xorWith( + a1: Array, + a2: Array, + a3: Array, + comparator?: Comparator + ): Array; + xorWith( + a1: Array, + a2: Array, + a3: Array, + a4: Array, + comparator?: Comparator + ): Array; + zip(a1: A[], a2: B[]): Array<[A, B]>; + zip(a1: A[], a2: B[], a3: C[]): Array<[A, B, C]>; + zip(a1: A[], a2: B[], a3: C[], a4: D[]): Array<[A, B, C, D]>; + zip( + a1: A[], + a2: B[], + a3: C[], + a4: D[], + a5: E[] + ): Array<[A, B, C, D, E]>; + + zipObject(props?: Array, values?: Array): { [key: K]: V }; + zipObjectDeep(props?: any[], values?: any): Object; + //Workaround until (...parameter: T, parameter2: U) works + zipWith(a1: NestedArray, iteratee?: Iteratee): Array; + zipWith( + a1: NestedArray, + a2: NestedArray, + iteratee?: Iteratee + ): Array; + zipWith( + a1: NestedArray, + a2: NestedArray, + a3: NestedArray, + iteratee?: Iteratee + ): Array; + zipWith( + a1: NestedArray, + a2: NestedArray, + a3: NestedArray, + a4: NestedArray, + iteratee?: Iteratee + ): Array; + + // Collection + countBy(array: ?Array, iteratee?: ValueOnlyIteratee): Object; + countBy(object: T, iteratee?: ValueOnlyIteratee): Object; + // alias of _.forEach + each(array: ?Array, iteratee?: Iteratee): Array; + each(object: T, iteratee?: OIteratee): T; + // alias of _.forEachRight + eachRight(array: ?Array, iteratee?: Iteratee): Array; + eachRight(object: T, iteratee?: OIteratee): T; + every(array: ?Array, iteratee?: Iteratee): boolean; + every(object: T, iteratee?: OIteratee): boolean; + filter(array: ?Array, predicate?: Predicate): Array; + filter( + object: T, + predicate?: OPredicate + ): Array; + find( + array: ?Array, + predicate?: Predicate, + fromIndex?: number + ): T | void; + find( + object: T, + predicate?: OPredicate, + fromIndex?: number + ): V; + findLast( + array: ?Array, + predicate?: Predicate, + fromIndex?: number + ): T | void; + findLast( + object: T, + predicate?: OPredicate + ): V; + flatMap(array: ?Array, iteratee?: FlatMapIteratee): Array; + flatMap( + object: T, + iteratee?: OFlatMapIteratee + ): Array; + flatMapDeep( + array: ?Array, + iteratee?: FlatMapIteratee + ): Array; + flatMapDeep( + object: T, + iteratee?: OFlatMapIteratee + ): Array; + flatMapDepth( + array: ?Array, + iteratee?: FlatMapIteratee, + depth?: number + ): Array; + flatMapDepth( + object: T, + iteratee?: OFlatMapIteratee, + depth?: number + ): Array; + forEach(array: ?Array, iteratee?: Iteratee): Array; + forEach(object: T, iteratee?: OIteratee): T; + forEachRight(array: ?Array, iteratee?: Iteratee): Array; + forEachRight(object: T, iteratee?: OIteratee): T; + groupBy( + array: ?Array, + iteratee?: ValueOnlyIteratee + ): { [key: V]: Array }; + groupBy( + object: T, + iteratee?: ValueOnlyIteratee + ): { [key: V]: Array }; + includes(array: ?Array, value: T, fromIndex?: number): boolean; + includes(object: T, value: any, fromIndex?: number): boolean; + includes(str: string, value: string, fromIndex?: number): boolean; + invokeMap( + array: ?Array, + path: ((value: T) => Array | string) | Array | string, + ...args?: Array + ): Array; + invokeMap( + object: T, + path: ((value: any) => Array | string) | Array | string, + ...args?: Array + ): Array; + keyBy( + array: ?Array, + iteratee?: ValueOnlyIteratee + ): { [key: V]: ?T }; + keyBy( + object: T, + iteratee?: ValueOnlyIteratee + ): { [key: V]: ?A }; + map(array: ?Array, iteratee?: MapIterator): Array; + map( + object: ?T, + iteratee?: OMapIterator + ): Array; + map( + str: ?string, + iteratee?: (char: string, index: number, str: string) => any + ): string; + orderBy( + array: ?Array, + iteratees?: Array> | string, + orders?: Array<"asc" | "desc"> | string + ): Array; + orderBy( + object: T, + iteratees?: Array> | string, + orders?: Array<"asc" | "desc"> | string + ): Array; + partition( + array: ?Array, + predicate?: Predicate + ): [Array, Array]; + partition( + object: T, + predicate?: OPredicate + ): [Array, Array]; + reduce( + array: ?Array, + iteratee?: ( + accumulator: U, + value: T, + index: number, + array: ?Array + ) => U, + accumulator?: U + ): U; + reduce( + object: T, + iteratee?: (accumulator: U, value: any, key: string, object: T) => U, + accumulator?: U + ): U; + reduceRight( + array: ?Array, + iteratee?: ( + accumulator: U, + value: T, + index: number, + array: ?Array + ) => U, + accumulator?: U + ): U; + reduceRight( + object: T, + iteratee?: (accumulator: U, value: any, key: string, object: T) => U, + accumulator?: U + ): U; + reject(array: ?Array, predicate?: Predicate): Array; + reject( + object: T, + predicate?: OPredicate + ): Array; + sample(array: ?Array): T; + sample(object: T): V; + sampleSize(array: ?Array, n?: number): Array; + sampleSize(object: T, n?: number): Array; + shuffle(array: ?Array): Array; + shuffle(object: T): Array; + size(collection: Array | Object): number; + some(array: ?Array, predicate?: Predicate): boolean; + some( + object?: ?T, + predicate?: OPredicate + ): boolean; + sortBy(array: ?Array, ...iteratees?: Array>): Array; + sortBy(array: ?Array, iteratees?: Array>): Array; + sortBy( + object: T, + ...iteratees?: Array> + ): Array; + sortBy(object: T, iteratees?: Array>): Array; + + // Date + now(): number; + + // Function + after(n: number, fn: Function): Function; + ary(func: Function, n?: number): Function; + before(n: number, fn: Function): Function; + bind(func: Function, thisArg: any, ...partials: Array): Function; + bindKey(obj: Object, key: string, ...partials: Array): Function; + curry: Curry; + curry(func: Function, arity?: number): Function; + curryRight(func: Function, arity?: number): Function; + debounce( + func: Function, + wait?: number, + options?: DebounceOptions + ): Function; + defer(func: Function, ...args?: Array): number; + delay(func: Function, wait: number, ...args?: Array): number; + flip(func: Function): Function; + memoize(func: Function, resolver?: Function): Function; + negate(predicate: Function): Function; + once(func: Function): Function; + overArgs(func: Function, ...transforms: Array): Function; + overArgs(func: Function, transforms: Array): Function; + partial(func: Function, ...partials: any[]): Function; + partialRight(func: Function, ...partials: Array): Function; + partialRight(func: Function, partials: Array): Function; + rearg(func: Function, ...indexes: Array): Function; + rearg(func: Function, indexes: Array): Function; + rest(func: Function, start?: number): Function; + spread(func: Function): Function; + throttle( + func: Function, + wait?: number, + options?: ThrottleOptions + ): Function; + unary(func: Function): Function; + wrap(value: any, wrapper: Function): Function; + + // Lang + castArray(value: *): any[]; + clone(value: T): T; + cloneDeep(value: T): T; + cloneDeepWith( + value: T, + customizer?: ?(value: T, key: number | string, object: T, stack: any) => U + ): U; + cloneWith( + value: T, + customizer?: ?(value: T, key: number | string, object: T, stack: any) => U + ): U; + conformsTo( + source: T, + predicates: T & { [key: string]: (x: any) => boolean } + ): boolean; + eq(value: any, other: any): boolean; + gt(value: any, other: any): boolean; + gte(value: any, other: any): boolean; + isArguments(value: any): boolean; + isArray(value: any): boolean; + isArrayBuffer(value: any): boolean; + isArrayLike(value: any): boolean; + isArrayLikeObject(value: any): boolean; + isBoolean(value: any): boolean; + isBuffer(value: any): boolean; + isDate(value: any): boolean; + isElement(value: any): boolean; + isEmpty(value: any): boolean; + isEqual(value: any, other: any): boolean; + isEqualWith( + value: T, + other: U, + customizer?: ( + objValue: any, + otherValue: any, + key: number | string, + object: T, + other: U, + stack: any + ) => boolean | void + ): boolean; + isError(value: any): boolean; + isFinite(value: any): boolean; + isFunction(value: Function): true; + isFunction(value: number | string | void | null | Object): false; + isInteger(value: any): boolean; + isLength(value: any): boolean; + isMap(value: any): boolean; + isMatch(object?: ?Object, source: Object): boolean; + isMatchWith( + object: T, + source: U, + customizer?: ( + objValue: any, + srcValue: any, + key: number | string, + object: T, + source: U + ) => boolean | void + ): boolean; + isNaN(value: any): boolean; + isNative(value: any): boolean; + isNil(value: any): boolean; + isNull(value: any): boolean; + isNumber(value: any): boolean; + isObject(value: any): boolean; + isObjectLike(value: any): boolean; + isPlainObject(value: any): boolean; + isRegExp(value: any): boolean; + isSafeInteger(value: any): boolean; + isSet(value: any): boolean; + isString(value: string): true; + isString( + value: number | boolean | Function | void | null | Object | Array + ): false; + isSymbol(value: any): boolean; + isTypedArray(value: any): boolean; + isUndefined(value: any): boolean; + isWeakMap(value: any): boolean; + isWeakSet(value: any): boolean; + lt(value: any, other: any): boolean; + lte(value: any, other: any): boolean; + toArray(value: any): Array; + toFinite(value: any): number; + toInteger(value: any): number; + toLength(value: any): number; + toNumber(value: any): number; + toPlainObject(value: any): Object; + toSafeInteger(value: any): number; + toString(value: any): string; + + // Math + add(augend: number, addend: number): number; + ceil(number: number, precision?: number): number; + divide(dividend: number, divisor: number): number; + floor(number: number, precision?: number): number; + max(array: ?Array): T; + maxBy(array: ?Array, iteratee?: Iteratee): T; + mean(array: Array<*>): number; + meanBy(array: Array, iteratee?: Iteratee): number; + min(array: ?Array): T; + minBy(array: ?Array, iteratee?: Iteratee): T; + multiply(multiplier: number, multiplicand: number): number; + round(number: number, precision?: number): number; + subtract(minuend: number, subtrahend: number): number; + sum(array: Array<*>): number; + sumBy(array: Array, iteratee?: Iteratee): number; + + // number + clamp(number: number, lower?: number, upper: number): number; + inRange(number: number, start?: number, end: number): boolean; + random(lower?: number, upper?: number, floating?: boolean): number; + + // Object + assign(object?: ?Object, ...sources?: Array): Object; + assignIn(a: A, b: B): A & B; + assignIn(a: A, b: B, c: C): A & B & C; + assignIn(a: A, b: B, c: C, d: D): A & B & C & D; + assignIn(a: A, b: B, c: C, d: D, e: E): A & B & C & D & E; + assignInWith( + object: T, + s1: A, + customizer?: ( + objValue: any, + srcValue: any, + key: string, + object: T, + source: A + ) => any | void + ): Object; + assignInWith( + object: T, + s1: A, + s2: B, + customizer?: ( + objValue: any, + srcValue: any, + key: string, + object: T, + source: A | B + ) => any | void + ): Object; + assignInWith( + object: T, + s1: A, + s2: B, + s3: C, + customizer?: ( + objValue: any, + srcValue: any, + key: string, + object: T, + source: A | B | C + ) => any | void + ): Object; + assignInWith( + object: T, + s1: A, + s2: B, + s3: C, + s4: D, + customizer?: ( + objValue: any, + srcValue: any, + key: string, + object: T, + source: A | B | C | D + ) => any | void + ): Object; + assignWith( + object: T, + s1: A, + customizer?: ( + objValue: any, + srcValue: any, + key: string, + object: T, + source: A + ) => any | void + ): Object; + assignWith( + object: T, + s1: A, + s2: B, + customizer?: ( + objValue: any, + srcValue: any, + key: string, + object: T, + source: A | B + ) => any | void + ): Object; + assignWith( + object: T, + s1: A, + s2: B, + s3: C, + customizer?: ( + objValue: any, + srcValue: any, + key: string, + object: T, + source: A | B | C + ) => any | void + ): Object; + assignWith( + object: T, + s1: A, + s2: B, + s3: C, + s4: D, + customizer?: ( + objValue: any, + srcValue: any, + key: string, + object: T, + source: A | B | C | D + ) => any | void + ): Object; + at(object?: ?Object, ...paths: Array): Array; + at(object?: ?Object, paths: Array): Array; + create(prototype: T, properties?: Object): $Supertype; + defaults(object?: ?Object, ...sources?: Array): Object; + defaultsDeep(object?: ?Object, ...sources?: Array): Object; + // alias for _.toPairs + entries(object?: ?Object): Array<[string, any]>; + // alias for _.toPairsIn + entriesIn(object?: ?Object): Array<[string, any]>; + // alias for _.assignIn + extend(a: A, b: B): A & B; + extend(a: A, b: B, c: C): A & B & C; + extend(a: A, b: B, c: C, d: D): A & B & C & D; + extend(a: A, b: B, c: C, d: D, e: E): A & B & C & D & E; + // alias for _.assignInWith + extendWith( + object: T, + s1: A, + customizer?: ( + objValue: any, + srcValue: any, + key: string, + object: T, + source: A + ) => any | void + ): Object; + extendWith( + object: T, + s1: A, + s2: B, + customizer?: ( + objValue: any, + srcValue: any, + key: string, + object: T, + source: A | B + ) => any | void + ): Object; + extendWith( + object: T, + s1: A, + s2: B, + s3: C, + customizer?: ( + objValue: any, + srcValue: any, + key: string, + object: T, + source: A | B | C + ) => any | void + ): Object; + extendWith( + object: T, + s1: A, + s2: B, + s3: C, + s4: D, + customizer?: ( + objValue: any, + srcValue: any, + key: string, + object: T, + source: A | B | C | D + ) => any | void + ): Object; + findKey( + object?: ?T, + predicate?: OPredicate + ): string | void; + findLastKey( + object?: ?T, + predicate?: OPredicate + ): string | void; + forIn(object?: ?Object, iteratee?: OIteratee<*>): Object; + forInRight(object?: ?Object, iteratee?: OIteratee<*>): Object; + forOwn(object?: ?Object, iteratee?: OIteratee<*>): Object; + forOwnRight(object?: ?Object, iteratee?: OIteratee<*>): Object; + functions(object?: ?Object): Array; + functionsIn(object?: ?Object): Array; + get( + object?: ?Object | ?Array, + path?: ?Array | string, + defaultValue?: any + ): any; + has(object?: ?Object, path?: ?Array | string): boolean; + hasIn(object?: ?Object, path?: ?Array | string): boolean; + invert(object?: ?Object, multiVal?: boolean): Object; + invertBy(object: ?Object, iteratee?: Function): Object; + invoke( + object?: ?Object, + path?: ?Array | string, + ...args?: Array + ): any; + keys(object?: ?{ [key: K]: any }): Array; + keys(object?: ?Object): Array; + keysIn(object?: ?Object): Array; + mapKeys(object?: ?Object, iteratee?: OIteratee<*>): Object; + mapValues(object?: ?Object, iteratee?: OIteratee<*>): Object; + merge(object?: ?Object, ...sources?: Array): Object; + mergeWith( + object: T, + customizer?: ( + objValue: any, + srcValue: any, + key: string, + object: T, + source: A + ) => any | void + ): Object; + mergeWith( + object: T, + s1: A, + s2: B, + customizer?: ( + objValue: any, + srcValue: any, + key: string, + object: T, + source: A | B + ) => any | void + ): Object; + mergeWith( + object: T, + s1: A, + s2: B, + s3: C, + customizer?: ( + objValue: any, + srcValue: any, + key: string, + object: T, + source: A | B | C + ) => any | void + ): Object; + mergeWith( + object: T, + s1: A, + s2: B, + s3: C, + s4: D, + customizer?: ( + objValue: any, + srcValue: any, + key: string, + object: T, + source: A | B | C | D + ) => any | void + ): Object; + omit(object?: ?Object, ...props: Array): Object; + omit(object?: ?Object, props: Array): Object; + omitBy( + object?: ?T, + predicate?: OPredicate + ): Object; + pick(object?: ?Object, ...props: Array): Object; + pick(object?: ?Object, props: Array): Object; + pickBy( + object?: ?T, + predicate?: OPredicate + ): Object; + result( + object?: ?Object, + path?: ?Array | string, + defaultValue?: any + ): any; + set(object?: ?Object, path?: ?Array | string, value: any): Object; + setWith( + object: T, + path?: ?Array | string, + value: any, + customizer?: (nsValue: any, key: string, nsObject: T) => any + ): Object; + toPairs(object?: ?Object | Array<*>): Array<[string, any]>; + toPairsIn(object?: ?Object): Array<[string, any]>; + transform( + collection: Object | Array, + iteratee?: OIteratee<*>, + accumulator?: any + ): any; + unset(object?: ?Object, path?: ?Array | string): boolean; + update(object: Object, path: string[] | string, updater: Function): Object; + updateWith( + object: Object, + path: string[] | string, + updater: Function, + customizer?: Function + ): Object; + values(object?: ?Object): Array; + valuesIn(object?: ?Object): Array; + + // Seq + // harder to read, but this is _() + (value: any): any; + chain(value: T): any; + tap(value: T, interceptor: (value: T) => any): T; + thru(value: T1, interceptor: (value: T1) => T2): T2; + // TODO: _.prototype.* + + // String + camelCase(string?: ?string): string; + capitalize(string?: string): string; + deburr(string?: string): string; + endsWith(string?: string, target?: string, position?: number): boolean; + escape(string?: string): string; + escapeRegExp(string?: string): string; + kebabCase(string?: string): string; + lowerCase(string?: string): string; + lowerFirst(string?: string): string; + pad(string?: string, length?: number, chars?: string): string; + padEnd(string?: string, length?: number, chars?: string): string; + padStart(string?: string, length?: number, chars?: string): string; + parseInt(string: string, radix?: number): number; + repeat(string?: string, n?: number): string; + replace( + string?: string, + pattern: RegExp | string, + replacement: ((string: string) => string) | string + ): string; + snakeCase(string?: string): string; + split( + string?: string, + separator: RegExp | string, + limit?: number + ): Array; + startCase(string?: string): string; + startsWith(string?: string, target?: string, position?: number): boolean; + template(string?: string, options?: TemplateSettings): Function; + toLower(string?: string): string; + toUpper(string?: string): string; + trim(string?: string, chars?: string): string; + trimEnd(string?: string, chars?: string): string; + trimStart(string?: string, chars?: string): string; + truncate(string?: string, options?: TruncateOptions): string; + unescape(string?: string): string; + upperCase(string?: string): string; + upperFirst(string?: string): string; + words(string?: string, pattern?: RegExp | string): Array; + + // Util + attempt(func: Function, ...args: Array): any; + bindAll(object?: ?Object, methodNames: Array): Object; + bindAll(object?: ?Object, ...methodNames: Array): Object; + cond(pairs: NestedArray): Function; + conforms(source: Object): Function; + constant(value: T): () => T; + defaultTo( + value: T1, + defaultValue: T2 + ): T1; + // NaN is a number instead of its own type, otherwise it would behave like null/void + defaultTo(value: T1, defaultValue: T2): T1 | T2; + defaultTo(value: T1, defaultValue: T2): T2; + flow: Flow; + flow(funcs?: Array): Function; + flowRight: FlowRight; + flowRight(funcs?: Array): Function; + identity(value: T): T; + iteratee(func?: any): Function; + matches(source: Object): Function; + matchesProperty(path?: ?Array | string, srcValue: any): Function; + method(path?: ?Array | string, ...args?: Array): Function; + methodOf(object?: ?Object, ...args?: Array): Function; + mixin( + object?: T, + source: Object, + options?: { chain: boolean } + ): T; + noConflict(): Lodash; + noop(...args: Array): void; + nthArg(n?: number): Function; + over(...iteratees: Array): Function; + over(iteratees: Array): Function; + overEvery(...predicates: Array): Function; + overEvery(predicates: Array): Function; + overSome(...predicates: Array): Function; + overSome(predicates: Array): Function; + property(path?: ?Array | string): Function; + propertyOf(object?: ?Object): Function; + range(start: number, end: number, step?: number): Array; + range(end: number, step?: number): Array; + rangeRight(start: number, end: number, step?: number): Array; + rangeRight(end: number, step?: number): Array; + runInContext(context?: Object): Function; + + stubArray(): Array<*>; + stubFalse(): false; + stubObject(): {}; + stubString(): ""; + stubTrue(): true; + times(n: number, ...rest: Array): Array; + times(n: number, iteratee: (i: number) => T): Array; + toPath(value: any): Array; + uniqueId(prefix?: string): string; + + // Properties + VERSION: string; + templateSettings: TemplateSettings; + } + + declare module.exports: Lodash; +} + +declare module "lodash/fp" { + declare type __CurriedFunction1 = (...r: [AA]) => R; + declare type CurriedFunction1 = __CurriedFunction1; + + declare type __CurriedFunction2 = (( + ...r: [AA] + ) => CurriedFunction1) & + ((...r: [AA, BB]) => R); + declare type CurriedFunction2 = __CurriedFunction2; + + declare type __CurriedFunction3 = (( + ...r: [AA] + ) => CurriedFunction2) & + ((...r: [AA, BB]) => CurriedFunction1) & + ((...r: [AA, BB, CC]) => R); + declare type CurriedFunction3 = __CurriedFunction3< + A, + B, + C, + R, + *, + *, + * + >; + + declare type __CurriedFunction4< + A, + B, + C, + D, + R, + AA: A, + BB: B, + CC: C, + DD: D + > = ((...r: [AA]) => CurriedFunction3) & + ((...r: [AA, BB]) => CurriedFunction2) & + ((...r: [AA, BB, CC]) => CurriedFunction1) & + ((...r: [AA, BB, CC, DD]) => R); + declare type CurriedFunction4 = __CurriedFunction4< + A, + B, + C, + D, + R, + *, + *, + *, + * + >; + + declare type __CurriedFunction5< + A, + B, + C, + D, + E, + R, + AA: A, + BB: B, + CC: C, + DD: D, + EE: E + > = ((...r: [AA]) => CurriedFunction4) & + ((...r: [AA, BB]) => CurriedFunction3) & + ((...r: [AA, BB, CC]) => CurriedFunction2) & + ((...r: [AA, BB, CC, DD]) => CurriedFunction1) & + ((...r: [AA, BB, CC, DD, EE]) => R); + declare type CurriedFunction5 = __CurriedFunction5< + A, + B, + C, + D, + E, + R, + *, + *, + *, + *, + * + >; + + declare type __CurriedFunction6< + A, + B, + C, + D, + E, + F, + R, + AA: A, + BB: B, + CC: C, + DD: D, + EE: E, + FF: F + > = ((...r: [AA]) => CurriedFunction5) & + ((...r: [AA, BB]) => CurriedFunction4) & + ((...r: [AA, BB, CC]) => CurriedFunction3) & + ((...r: [AA, BB, CC, DD]) => CurriedFunction2) & + ((...r: [AA, BB, CC, DD, EE]) => CurriedFunction1) & + ((...r: [AA, BB, CC, DD, EE, FF]) => R); + declare type CurriedFunction6 = __CurriedFunction6< + A, + B, + C, + D, + E, + F, + R, + *, + *, + *, + *, + *, + * + >; + + declare type Curry = (((...r: [A]) => R) => CurriedFunction1) & + (((...r: [A, B]) => R) => CurriedFunction2) & + (((...r: [A, B, C]) => R) => CurriedFunction3) & + (( + (...r: [A, B, C, D]) => R + ) => CurriedFunction4) & + (( + (...r: [A, B, C, D, E]) => R + ) => CurriedFunction5) & + (( + (...r: [A, B, C, D, E, F]) => R + ) => CurriedFunction6); + + declare type UnaryFn = (a: A) => R; + + declare type Flow = (( + ab: UnaryFn, + bc: UnaryFn, + cd: UnaryFn, + de: UnaryFn, + ef: UnaryFn, + fg: UnaryFn, + ...rest: Array + ) => UnaryFn) & + (( + ab: UnaryFn, + bc: UnaryFn, + cd: UnaryFn, + de: UnaryFn, + ef: UnaryFn, + ...rest: Array + ) => UnaryFn) & + (( + ab: UnaryFn, + bc: UnaryFn, + cd: UnaryFn, + de: UnaryFn, + ...rest: Array + ) => UnaryFn) & + (( + ab: UnaryFn, + bc: UnaryFn, + cd: UnaryFn, + ...rest: Array + ) => UnaryFn) & + (( + ab: UnaryFn, + bc: UnaryFn, + ...rest: Array + ) => UnaryFn) & + ((ab: UnaryFn, ...rest: Array) => UnaryFn); + + declare type FlowRight = (( + fg: UnaryFn, + ef: UnaryFn, + de: UnaryFn, + cd: UnaryFn, + bc: UnaryFn, + ab: UnaryFn, + ...rest: Array + ) => UnaryFn) & + (( + ef: UnaryFn, + de: UnaryFn, + cd: UnaryFn, + bc: UnaryFn, + ab: UnaryFn, + ...rest: Array + ) => UnaryFn) & + (( + de: UnaryFn, + cd: UnaryFn, + bc: UnaryFn, + ab: UnaryFn, + ...rest: Array + ) => UnaryFn) & + (( + cd: UnaryFn, + bc: UnaryFn, + ab: UnaryFn, + ...rest: Array + ) => UnaryFn) & + (( + bc: UnaryFn, + ab: UnaryFn, + ...rest: Array + ) => UnaryFn) & + ((ab: UnaryFn, ...rest: Array) => UnaryFn); + + declare type TemplateSettings = { + escape?: RegExp, + evaluate?: RegExp, + imports?: Object, + interpolate?: RegExp, + variable?: string + }; + + declare type TruncateOptions = { + length?: number, + omission?: string, + separator?: RegExp | string + }; + + declare type DebounceOptions = { + leading?: boolean, + maxWait?: number, + trailing?: boolean + }; + + declare type ThrottleOptions = { + leading?: boolean, + trailing?: boolean + }; + + declare type NestedArray = Array>; + + declare type matchesIterateeShorthand = Object; + declare type matchesPropertyIterateeShorthand = [string, any]; + declare type propertyIterateeShorthand = string; + + declare type OPredicate = + | ((value: A) => any) + | matchesIterateeShorthand + | matchesPropertyIterateeShorthand + | propertyIterateeShorthand; + + declare type OIterateeWithResult = Object | string | ((value: V) => R); + declare type OIteratee = OIterateeWithResult; + declare type OFlatMapIteratee = OIterateeWithResult>; + + declare type Predicate = + | ((value: T) => any) + | matchesIterateeShorthand + | matchesPropertyIterateeShorthand + | propertyIterateeShorthand; + + declare type _ValueOnlyIteratee = (value: T) => mixed; + declare type ValueOnlyIteratee = _ValueOnlyIteratee | string; + declare type _Iteratee = (item: T) => mixed; + declare type Iteratee = _Iteratee | Object | string; + declare type FlatMapIteratee = + | ((item: T) => Array) + | Object + | string; + declare type Comparator = (item: T, item2: T) => boolean; + + declare type MapIterator = ((item: T) => U) | propertyIterateeShorthand; + + declare type OMapIterator = + | ((item: T) => U) + | propertyIterateeShorthand; + + declare class Lodash { + // Array + chunk(size: number): (array: Array) => Array>; + chunk(size: number, array: Array): Array>; + compact(array: Array): Array; + concat | T, B: Array | U>( + base: A + ): (elements: B) => Array; + concat | T, B: Array | U>( + base: A, + elements: B + ): Array; + difference(values: Array): (array: Array) => Array; + difference(values: Array, array: Array): Array; + differenceBy( + iteratee: ValueOnlyIteratee + ): ((values: Array) => (array: Array) => T[]) & + ((values: Array, array: Array) => T[]); + differenceBy( + iteratee: ValueOnlyIteratee, + values: Array + ): (array: Array) => T[]; + differenceBy( + iteratee: ValueOnlyIteratee, + values: Array, + array: Array + ): T[]; + differenceWith( + values: T[] + ): ((comparator: Comparator) => (array: T[]) => T[]) & + ((comparator: Comparator, array: T[]) => T[]); + differenceWith( + values: T[], + comparator: Comparator + ): (array: T[]) => T[]; + differenceWith(values: T[], comparator: Comparator, array: T[]): T[]; + drop(n: number): (array: Array) => Array; + drop(n: number, array: Array): Array; + dropLast(n: number): (array: Array) => Array; + dropLast(n: number, array: Array): Array; + dropRight(n: number): (array: Array) => Array; + dropRight(n: number, array: Array): Array; + dropRightWhile(predicate: Predicate): (array: Array) => Array; + dropRightWhile(predicate: Predicate, array: Array): Array; + dropWhile(predicate: Predicate): (array: Array) => Array; + dropWhile(predicate: Predicate, array: Array): Array; + dropLastWhile(predicate: Predicate): (array: Array) => Array; + dropLastWhile(predicate: Predicate, array: Array): Array; + fill( + start: number + ): (( + end: number + ) => ((value: U) => (array: Array) => Array) & + ((value: U, array: Array) => Array)) & + ((end: number, value: U) => (array: Array) => Array) & + ((end: number, value: U, array: Array) => Array); + fill( + start: number, + end: number + ): ((value: U) => (array: Array) => Array) & + ((value: U, array: Array) => Array); + fill( + start: number, + end: number, + value: U + ): (array: Array) => Array; + fill( + start: number, + end: number, + value: U, + array: Array + ): Array; + findIndex(predicate: Predicate): (array: Array) => number; + findIndex(predicate: Predicate, array: Array): number; + findIndexFrom( + predicate: Predicate + ): ((fromIndex: number) => (array: Array) => number) & + ((fromIndex: number, array: Array) => number); + findIndexFrom( + predicate: Predicate, + fromIndex: number + ): (array: Array) => number; + findIndexFrom( + predicate: Predicate, + fromIndex: number, + array: Array + ): number; + findLastIndex(predicate: Predicate): (array: Array) => number; + findLastIndex(predicate: Predicate, array: Array): number; + findLastIndexFrom( + predicate: Predicate + ): ((fromIndex: number) => (array: Array) => number) & + ((fromIndex: number, array: Array) => number); + findLastIndexFrom( + predicate: Predicate, + fromIndex: number + ): (array: Array) => number; + findLastIndexFrom( + predicate: Predicate, + fromIndex: number, + array: Array + ): number; + // alias of _.head + first(array: Array): T; + flatten(array: Array | X>): Array; + unnest(array: Array | X>): Array; + flattenDeep(array: any[]): Array; + flattenDepth(depth: number): (array: any[]) => any[]; + flattenDepth(depth: number, array: any[]): any[]; + fromPairs(pairs: Array<[A, B]>): { [key: A]: B }; + head(array: Array): T; + indexOf(value: T): (array: Array) => number; + indexOf(value: T, array: Array): number; + indexOfFrom( + value: T + ): ((fromIndex: number) => (array: Array) => number) & + ((fromIndex: number, array: Array) => number); + indexOfFrom(value: T, fromIndex: number): (array: Array) => number; + indexOfFrom(value: T, fromIndex: number, array: Array): number; + initial(array: Array): Array; + init(array: Array): Array; + intersection(a1: Array): (a2: Array) => Array; + intersection(a1: Array, a2: Array): Array; + intersectionBy( + iteratee: ValueOnlyIteratee + ): ((a1: Array) => (a2: Array) => Array) & + ((a1: Array, a2: Array) => Array); + intersectionBy( + iteratee: ValueOnlyIteratee, + a1: Array + ): (a2: Array) => Array; + intersectionBy( + iteratee: ValueOnlyIteratee, + a1: Array, + a2: Array + ): Array; + intersectionWith( + comparator: Comparator + ): ((a1: Array) => (a2: Array) => Array) & + ((a1: Array, a2: Array) => Array); + intersectionWith( + comparator: Comparator, + a1: Array + ): (a2: Array) => Array; + intersectionWith( + comparator: Comparator, + a1: Array, + a2: Array + ): Array; + join(separator: string): (array: Array) => string; + join(separator: string, array: Array): string; + last(array: Array): T; + lastIndexOf(value: T): (array: Array) => number; + lastIndexOf(value: T, array: Array): number; + lastIndexOfFrom( + value: T + ): ((fromIndex: number) => (array: Array) => number) & + ((fromIndex: number, array: Array) => number); + lastIndexOfFrom( + value: T, + fromIndex: number + ): (array: Array) => number; + lastIndexOfFrom(value: T, fromIndex: number, array: Array): number; + nth(n: number): (array: T[]) => T; + nth(n: number, array: T[]): T; + pull(value: T): (array: Array) => Array; + pull(value: T, array: Array): Array; + pullAll(values: Array): (array: Array) => Array; + pullAll(values: Array, array: Array): Array; + pullAllBy( + iteratee: ValueOnlyIteratee + ): ((values: Array) => (array: Array) => Array) & + ((values: Array, array: Array) => Array); + pullAllBy( + iteratee: ValueOnlyIteratee, + values: Array + ): (array: Array) => Array; + pullAllBy( + iteratee: ValueOnlyIteratee, + values: Array, + array: Array + ): Array; + pullAllWith( + comparator: Function + ): ((values: T[]) => (array: T[]) => T[]) & + ((values: T[], array: T[]) => T[]); + pullAllWith(comparator: Function, values: T[]): (array: T[]) => T[]; + pullAllWith(comparator: Function, values: T[], array: T[]): T[]; + pullAt(indexed: Array): (array: Array) => Array; + pullAt(indexed: Array, array: Array): Array; + remove(predicate: Predicate): (array: Array) => Array; + remove(predicate: Predicate, array: Array): Array; + reverse(array: Array): Array; + slice( + start: number + ): ((end: number) => (array: Array) => Array) & + ((end: number, array: Array) => Array); + slice(start: number, end: number): (array: Array) => Array; + slice(start: number, end: number, array: Array): Array; + sortedIndex(value: T): (array: Array) => number; + sortedIndex(value: T, array: Array): number; + sortedIndexBy( + iteratee: ValueOnlyIteratee + ): ((value: T) => (array: Array) => number) & + ((value: T, array: Array) => number); + sortedIndexBy( + iteratee: ValueOnlyIteratee, + value: T + ): (array: Array) => number; + sortedIndexBy( + iteratee: ValueOnlyIteratee, + value: T, + array: Array + ): number; + sortedIndexOf(value: T): (array: Array) => number; + sortedIndexOf(value: T, array: Array): number; + sortedLastIndex(value: T): (array: Array) => number; + sortedLastIndex(value: T, array: Array): number; + sortedLastIndexBy( + iteratee: ValueOnlyIteratee + ): ((value: T) => (array: Array) => number) & + ((value: T, array: Array) => number); + sortedLastIndexBy( + iteratee: ValueOnlyIteratee, + value: T + ): (array: Array) => number; + sortedLastIndexBy( + iteratee: ValueOnlyIteratee, + value: T, + array: Array + ): number; + sortedLastIndexOf(value: T): (array: Array) => number; + sortedLastIndexOf(value: T, array: Array): number; + sortedUniq(array: Array): Array; + sortedUniqBy( + iteratee: (value: T) => mixed + ): (array: Array) => Array; + sortedUniqBy(iteratee: (value: T) => mixed, array: Array): Array; + tail(array: Array): Array; + take(n: number): (array: Array) => Array; + take(n: number, array: Array): Array; + takeRight(n: number): (array: Array) => Array; + takeRight(n: number, array: Array): Array; + takeLast(n: number): (array: Array) => Array; + takeLast(n: number, array: Array): Array; + takeRightWhile(predicate: Predicate): (array: Array) => Array; + takeRightWhile(predicate: Predicate, array: Array): Array; + takeLastWhile(predicate: Predicate): (array: Array) => Array; + takeLastWhile(predicate: Predicate, array: Array): Array; + takeWhile(predicate: Predicate): (array: Array) => Array; + takeWhile(predicate: Predicate, array: Array): Array; + union(a1: Array): (a2: Array) => Array; + union(a1: Array, a2: Array): Array; + unionBy( + iteratee: ValueOnlyIteratee + ): ((a1: Array) => (a2: Array) => Array) & + ((a1: Array, a2: Array) => Array); + unionBy( + iteratee: ValueOnlyIteratee, + a1: Array + ): (a2: Array) => Array; + unionBy( + iteratee: ValueOnlyIteratee, + a1: Array, + a2: Array + ): Array; + unionWith( + comparator: Comparator + ): ((a1: Array) => (a2: Array) => Array) & + ((a1: Array, a2: Array) => Array); + unionWith( + comparator: Comparator, + a1: Array + ): (a2: Array) => Array; + unionWith( + comparator: Comparator, + a1: Array, + a2: Array + ): Array; + uniq(array: Array): Array; + uniqBy(iteratee: ValueOnlyIteratee): (array: Array) => Array; + uniqBy(iteratee: ValueOnlyIteratee, array: Array): Array; + uniqWith(comparator: Comparator): (array: Array) => Array; + uniqWith(comparator: Comparator, array: Array): Array; + unzip(array: Array): Array; + unzipWith(iteratee: Iteratee): (array: Array) => Array; + unzipWith(iteratee: Iteratee, array: Array): Array; + without(values: Array): (array: Array) => Array; + without(values: Array, array: Array): Array; + xor(a1: Array): (a2: Array) => Array; + xor(a1: Array, a2: Array): Array; + symmetricDifference(a1: Array): (a2: Array) => Array; + symmetricDifference(a1: Array, a2: Array): Array; + xorBy( + iteratee: ValueOnlyIteratee + ): ((a1: Array) => (a2: Array) => Array) & + ((a1: Array, a2: Array) => Array); + xorBy( + iteratee: ValueOnlyIteratee, + a1: Array + ): (a2: Array) => Array; + xorBy( + iteratee: ValueOnlyIteratee, + a1: Array, + a2: Array + ): Array; + symmetricDifferenceBy( + iteratee: ValueOnlyIteratee + ): ((a1: Array) => (a2: Array) => Array) & + ((a1: Array, a2: Array) => Array); + symmetricDifferenceBy( + iteratee: ValueOnlyIteratee, + a1: Array + ): (a2: Array) => Array; + symmetricDifferenceBy( + iteratee: ValueOnlyIteratee, + a1: Array, + a2: Array + ): Array; + xorWith( + comparator: Comparator + ): ((a1: Array) => (a2: Array) => Array) & + ((a1: Array, a2: Array) => Array); + xorWith( + comparator: Comparator, + a1: Array + ): (a2: Array) => Array; + xorWith(comparator: Comparator, a1: Array, a2: Array): Array; + symmetricDifferenceWith( + comparator: Comparator + ): ((a1: Array) => (a2: Array) => Array) & + ((a1: Array, a2: Array) => Array); + symmetricDifferenceWith( + comparator: Comparator, + a1: Array + ): (a2: Array) => Array; + symmetricDifferenceWith( + comparator: Comparator, + a1: Array, + a2: Array + ): Array; + zip(a1: A[]): (a2: B[]) => Array<[A, B]>; + zip(a1: A[], a2: B[]): Array<[A, B]>; + zipAll(arrays: Array>): Array; + zipObject(props?: Array): (values?: Array) => { [key: K]: V }; + zipObject(props?: Array, values?: Array): { [key: K]: V }; + zipObj(props: Array): (values: Array) => Object; + zipObj(props: Array, values: Array): Object; + zipObjectDeep(props: any[]): (values: any) => Object; + zipObjectDeep(props: any[], values: any): Object; + zipWith( + iteratee: Iteratee + ): ((a1: NestedArray) => (a2: NestedArray) => Array) & + ((a1: NestedArray, a2: NestedArray) => Array); + zipWith( + iteratee: Iteratee, + a1: NestedArray + ): (a2: NestedArray) => Array; + zipWith( + iteratee: Iteratee, + a1: NestedArray, + a2: NestedArray + ): Array; + // Collection + countBy( + iteratee: ValueOnlyIteratee + ): (collection: Array | { [id: any]: T }) => { [string]: number }; + countBy( + iteratee: ValueOnlyIteratee, + collection: Array | { [id: any]: T } + ): { [string]: number }; + // alias of _.forEach + each( + iteratee: Iteratee | OIteratee + ): (collection: Array | { [id: any]: T }) => Array; + each( + iteratee: Iteratee | OIteratee, + collection: Array | { [id: any]: T } + ): Array; + // alias of _.forEachRight + eachRight( + iteratee: Iteratee | OIteratee + ): (collection: Array | { [id: any]: T }) => Array; + eachRight( + iteratee: Iteratee | OIteratee, + collection: Array | { [id: any]: T } + ): Array; + every( + iteratee: Iteratee | OIteratee + ): (collection: Array | { [id: any]: T }) => boolean; + every( + iteratee: Iteratee | OIteratee, + collection: Array | { [id: any]: T } + ): boolean; + all( + iteratee: Iteratee | OIteratee + ): (collection: Array | { [id: any]: T }) => boolean; + all( + iteratee: Iteratee | OIteratee, + collection: Array | { [id: any]: T } + ): boolean; + filter( + predicate: Predicate | OPredicate + ): (collection: Array | { [id: any]: T }) => Array; + filter( + predicate: Predicate | OPredicate, + collection: Array | { [id: any]: T } + ): Array; + find( + predicate: Predicate | OPredicate + ): (collection: Array | { [id: any]: T }) => T | void; + find( + predicate: Predicate | OPredicate, + collection: Array | { [id: any]: T } + ): T | void; + findFrom( + predicate: Predicate | OPredicate + ): (( + fromIndex: number + ) => (collection: Array | { [id: any]: T }) => T | void) & + (( + fromIndex: number, + collection: Array | { [id: any]: T } + ) => T | void); + findFrom( + predicate: Predicate | OPredicate, + fromIndex: number + ): (collection: Array | { [id: any]: T }) => T | void; + findFrom( + predicate: Predicate | OPredicate, + fromIndex: number, + collection: Array | { [id: any]: T } + ): T | void; + findLast( + predicate: Predicate | OPredicate + ): (collection: Array | { [id: any]: T }) => T | void; + findLast( + predicate: Predicate | OPredicate, + collection: Array | { [id: any]: T } + ): T | void; + findLastFrom( + predicate: Predicate | OPredicate + ): (( + fromIndex: number + ) => (collection: Array | { [id: any]: T }) => T | void) & + (( + fromIndex: number, + collection: Array | { [id: any]: T } + ) => T | void); + findLastFrom( + predicate: Predicate | OPredicate, + fromIndex: number + ): (collection: Array | { [id: any]: T }) => T | void; + findLastFrom( + predicate: Predicate | OPredicate, + fromIndex: number, + collection: Array | { [id: any]: T } + ): T | void; + flatMap( + iteratee: FlatMapIteratee | OFlatMapIteratee + ): (collection: Array | { [id: any]: T }) => Array; + flatMap( + iteratee: FlatMapIteratee | OFlatMapIteratee, + collection: Array | { [id: any]: T } + ): Array; + flatMapDeep( + iteratee: FlatMapIteratee | OFlatMapIteratee + ): (collection: Array | { [id: any]: T }) => Array; + flatMapDeep( + iteratee: FlatMapIteratee | OFlatMapIteratee, + collection: Array | { [id: any]: T } + ): Array; + flatMapDepth( + iteratee: FlatMapIteratee | OFlatMapIteratee + ): (( + depth: number + ) => (collection: Array | { [id: any]: T }) => Array) & + ((depth: number, collection: Array | { [id: any]: T }) => Array); + flatMapDepth( + iteratee: FlatMapIteratee | OFlatMapIteratee, + depth: number + ): (collection: Array | { [id: any]: T }) => Array; + flatMapDepth( + iteratee: FlatMapIteratee | OFlatMapIteratee, + depth: number, + collection: Array | { [id: any]: T } + ): Array; + forEach( + iteratee: Iteratee | OIteratee + ): (collection: Array | { [id: any]: T }) => Array; + forEach( + iteratee: Iteratee | OIteratee, + collection: Array | { [id: any]: T } + ): Array; + forEachRight( + iteratee: Iteratee | OIteratee + ): (collection: Array | { [id: any]: T }) => Array; + forEachRight( + iteratee: Iteratee | OIteratee, + collection: Array | { [id: any]: T } + ): Array; + groupBy( + iteratee: ValueOnlyIteratee + ): (collection: Array | { [id: any]: T }) => { [key: V]: Array }; + groupBy( + iteratee: ValueOnlyIteratee, + collection: Array | { [id: any]: T } + ): { [key: V]: Array }; + includes(value: string): (str: string) => boolean; + includes(value: string, str: string): boolean; + includes(value: T): (collection: Array | { [id: any]: T }) => boolean; + includes(value: T, collection: Array | { [id: any]: T }): boolean; + contains(value: string): (str: string) => boolean; + contains(value: string, str: string): boolean; + contains(value: T): (collection: Array | { [id: any]: T }) => boolean; + contains(value: T, collection: Array | { [id: any]: T }): boolean; + includesFrom( + value: string + ): ((fromIndex: number) => (str: string) => boolean) & + ((fromIndex: number, str: string) => boolean); + includesFrom(value: string, fromIndex: number): (str: string) => boolean; + includesFrom(value: string, fromIndex: number, str: string): boolean; + includesFrom( + value: T + ): ((fromIndex: number) => (collection: Array) => boolean) & + ((fromIndex: number, collection: Array) => boolean); + includesFrom( + value: T, + fromIndex: number + ): (collection: Array) => boolean; + includesFrom(value: T, fromIndex: number, collection: Array): boolean; + invokeMap( + path: ((value: T) => Array | string) | Array | string + ): (collection: Array | { [id: any]: T }) => Array; + invokeMap( + path: ((value: T) => Array | string) | Array | string, + collection: Array | { [id: any]: T } + ): Array; + invokeArgsMap( + path: ((value: T) => Array | string) | Array | string + ): (( + collection: Array | { [id: any]: T } + ) => (args: Array) => Array) & + (( + collection: Array | { [id: any]: T }, + args: Array + ) => Array); + invokeArgsMap( + path: ((value: T) => Array | string) | Array | string, + collection: Array | { [id: any]: T } + ): (args: Array) => Array; + invokeArgsMap( + path: ((value: T) => Array | string) | Array | string, + collection: Array | { [id: any]: T }, + args: Array + ): Array; + keyBy( + iteratee: ValueOnlyIteratee + ): (collection: Array | { [id: any]: T }) => { [key: V]: T }; + keyBy( + iteratee: ValueOnlyIteratee, + collection: Array | { [id: any]: T } + ): { [key: V]: T }; + indexBy( + iteratee: ValueOnlyIteratee + ): (collection: Array | { [id: any]: T }) => { [key: V]: T }; + indexBy( + iteratee: ValueOnlyIteratee, + collection: Array | { [id: any]: T } + ): { [key: V]: T }; + map( + iteratee: MapIterator | OMapIterator + ): (collection: Array | { [id: any]: T }) => Array; + map( + iteratee: MapIterator | OMapIterator, + collection: Array | { [id: any]: T } + ): Array; + map(iteratee: (char: string) => any): (str: string) => string; + map(iteratee: (char: string) => any, str: string): string; + pluck( + iteratee: MapIterator | OMapIterator + ): (collection: Array | { [id: any]: T }) => Array; + pluck( + iteratee: MapIterator | OMapIterator, + collection: Array | { [id: any]: T } + ): Array; + pluck(iteratee: (char: string) => any): (str: string) => string; + pluck(iteratee: (char: string) => any, str: string): string; + orderBy( + iteratees: Array | OIteratee<*>> | string + ): (( + orders: Array<"asc" | "desc"> | string + ) => (collection: Array | { [id: any]: T }) => Array) & + (( + orders: Array<"asc" | "desc"> | string, + collection: Array | { [id: any]: T } + ) => Array); + orderBy( + iteratees: Array | OIteratee<*>> | string, + orders: Array<"asc" | "desc"> | string + ): (collection: Array | { [id: any]: T }) => Array; + orderBy( + iteratees: Array | OIteratee<*>> | string, + orders: Array<"asc" | "desc"> | string, + collection: Array | { [id: any]: T } + ): Array; + partition( + predicate: Predicate | OPredicate + ): (collection: Array | { [id: any]: T }) => [Array, Array]; + partition( + predicate: Predicate | OPredicate, + collection: Array | { [id: any]: T } + ): [Array, Array]; + reduce( + iteratee: (accumulator: U, value: T) => U + ): ((accumulator: U) => (collection: Array | { [id: any]: T }) => U) & + ((accumulator: U, collection: Array | { [id: any]: T }) => U); + reduce( + iteratee: (accumulator: U, value: T) => U, + accumulator: U + ): (collection: Array | { [id: any]: T }) => U; + reduce( + iteratee: (accumulator: U, value: T) => U, + accumulator: U, + collection: Array | { [id: any]: T } + ): U; + reduceRight( + iteratee: (value: T, accumulator: U) => U + ): ((accumulator: U) => (collection: Array | { [id: any]: T }) => U) & + ((accumulator: U, collection: Array | { [id: any]: T }) => U); + reduceRight( + iteratee: (value: T, accumulator: U) => U, + accumulator: U + ): (collection: Array | { [id: any]: T }) => U; + reduceRight( + iteratee: (value: T, accumulator: U) => U, + accumulator: U, + collection: Array | { [id: any]: T } + ): U; + reject( + predicate: Predicate | OPredicate + ): (collection: Array | { [id: any]: T }) => Array; + reject( + predicate: Predicate | OPredicate, + collection: Array | { [id: any]: T } + ): Array; + sample(collection: Array | { [id: any]: T }): T; + sampleSize( + n: number + ): (collection: Array | { [id: any]: T }) => Array; + sampleSize(n: number, collection: Array | { [id: any]: T }): Array; + shuffle(collection: Array | { [id: any]: T }): Array; + size(collection: Array | Object): number; + some( + predicate: Predicate | OPredicate + ): (collection: Array | { [id: any]: T }) => boolean; + some( + predicate: Predicate | OPredicate, + collection: Array | { [id: any]: T } + ): boolean; + any( + predicate: Predicate | OPredicate + ): (collection: Array | { [id: any]: T }) => boolean; + any( + predicate: Predicate | OPredicate, + collection: Array | { [id: any]: T } + ): boolean; + sortBy( + iteratees: Array | OIteratee> | Iteratee | OIteratee + ): (collection: Array | { [id: any]: T }) => Array; + sortBy( + iteratees: Array | OIteratee> | Iteratee | OIteratee, + collection: Array | { [id: any]: T } + ): Array; + + // Date + now(): number; + + // Function + after(fn: Function): (n: number) => Function; + after(fn: Function, n: number): Function; + ary(func: Function): Function; + nAry(n: number): (func: Function) => Function; + nAry(n: number, func: Function): Function; + before(fn: Function): (n: number) => Function; + before(fn: Function, n: number): Function; + bind(func: Function): (thisArg: any) => Function; + bind(func: Function, thisArg: any): Function; + bindKey(obj: Object): (key: string) => Function; + bindKey(obj: Object, key: string): Function; + curry: Curry; + curryN(arity: number): (func: Function) => Function; + curryN(arity: number, func: Function): Function; + curryRight(func: Function): Function; + curryRightN(arity: number): (func: Function) => Function; + curryRightN(arity: number, func: Function): Function; + debounce(wait: number): (func: Function) => Function; + debounce(wait: number, func: Function): Function; + defer(func: Function): number; + delay(wait: number): (func: Function) => number; + delay(wait: number, func: Function): number; + flip(func: Function): Function; + memoize(func: Function): Function; + negate(predicate: Function): Function; + complement(predicate: Function): Function; + once(func: Function): Function; + overArgs(func: Function): (transforms: Array) => Function; + overArgs(func: Function, transforms: Array): Function; + useWith(func: Function): (transforms: Array) => Function; + useWith(func: Function, transforms: Array): Function; + partial(func: Function): (partials: any[]) => Function; + partial(func: Function, partials: any[]): Function; + partialRight(func: Function): (partials: Array) => Function; + partialRight(func: Function, partials: Array): Function; + rearg(indexes: Array): (func: Function) => Function; + rearg(indexes: Array, func: Function): Function; + rest(func: Function): Function; + unapply(func: Function): Function; + restFrom(start: number): (func: Function) => Function; + restFrom(start: number, func: Function): Function; + spread(func: Function): Function; + apply(func: Function): Function; + spreadFrom(start: number): (func: Function) => Function; + spreadFrom(start: number, func: Function): Function; + throttle(wait: number): (func: Function) => Function; + throttle(wait: number, func: Function): Function; + unary(func: Function): Function; + wrap(wrapper: Function): (value: any) => Function; + wrap(wrapper: Function, value: any): Function; + + // Lang + castArray(value: *): any[]; + clone(value: T): T; + cloneDeep(value: T): T; + cloneDeepWith( + customizer: (value: T, key: number | string, object: T, stack: any) => U + ): (value: T) => U; + cloneDeepWith( + customizer: (value: T, key: number | string, object: T, stack: any) => U, + value: T + ): U; + cloneWith( + customizer: (value: T, key: number | string, object: T, stack: any) => U + ): (value: T) => U; + cloneWith( + customizer: (value: T, key: number | string, object: T, stack: any) => U, + value: T + ): U; + conformsTo( + predicates: T & { [key: string]: (x: any) => boolean } + ): (source: T) => boolean; + conformsTo( + predicates: T & { [key: string]: (x: any) => boolean }, + source: T + ): boolean; + where( + predicates: T & { [key: string]: (x: any) => boolean } + ): (source: T) => boolean; + where( + predicates: T & { [key: string]: (x: any) => boolean }, + source: T + ): boolean; + conforms( + predicates: T & { [key: string]: (x: any) => boolean } + ): (source: T) => boolean; + conforms( + predicates: T & { [key: string]: (x: any) => boolean }, + source: T + ): boolean; + eq(value: any): (other: any) => boolean; + eq(value: any, other: any): boolean; + identical(value: any): (other: any) => boolean; + identical(value: any, other: any): boolean; + gt(value: any): (other: any) => boolean; + gt(value: any, other: any): boolean; + gte(value: any): (other: any) => boolean; + gte(value: any, other: any): boolean; + isArguments(value: any): boolean; + isArray(value: any): boolean; + isArrayBuffer(value: any): boolean; + isArrayLike(value: any): boolean; + isArrayLikeObject(value: any): boolean; + isBoolean(value: any): boolean; + isBuffer(value: any): boolean; + isDate(value: any): boolean; + isElement(value: any): boolean; + isEmpty(value: any): boolean; + isEqual(value: any): (other: any) => boolean; + isEqual(value: any, other: any): boolean; + equals(value: any): (other: any) => boolean; + equals(value: any, other: any): boolean; + isEqualWith( + customizer: ( + objValue: any, + otherValue: any, + key: number | string, + object: T, + other: U, + stack: any + ) => boolean | void + ): ((value: T) => (other: U) => boolean) & + ((value: T, other: U) => boolean); + isEqualWith( + customizer: ( + objValue: any, + otherValue: any, + key: number | string, + object: T, + other: U, + stack: any + ) => boolean | void, + value: T + ): (other: U) => boolean; + isEqualWith( + customizer: ( + objValue: any, + otherValue: any, + key: number | string, + object: T, + other: U, + stack: any + ) => boolean | void, + value: T, + other: U + ): boolean; + isError(value: any): boolean; + isFinite(value: any): boolean; + isFunction(value: Function): true; + isFunction(value: number | string | void | null | Object): false; + isInteger(value: any): boolean; + isLength(value: any): boolean; + isMap(value: any): boolean; + isMatch(source: Object): (object: Object) => boolean; + isMatch(source: Object, object: Object): boolean; + whereEq(source: Object): (object: Object) => boolean; + whereEq(source: Object, object: Object): boolean; + isMatchWith( + customizer: ( + objValue: any, + srcValue: any, + key: number | string, + object: T, + source: U + ) => boolean | void + ): ((source: U) => (object: T) => boolean) & + ((source: U, object: T) => boolean); + isMatchWith( + customizer: ( + objValue: any, + srcValue: any, + key: number | string, + object: T, + source: U + ) => boolean | void, + source: U + ): (object: T) => boolean; + isMatchWith( + customizer: ( + objValue: any, + srcValue: any, + key: number | string, + object: T, + source: U + ) => boolean | void, + source: U, + object: T + ): boolean; + isNaN(value: any): boolean; + isNative(value: any): boolean; + isNil(value: any): boolean; + isNull(value: any): boolean; + isNumber(value: any): boolean; + isObject(value: any): boolean; + isObjectLike(value: any): boolean; + isPlainObject(value: any): boolean; + isRegExp(value: any): boolean; + isSafeInteger(value: any): boolean; + isSet(value: any): boolean; + isString(value: string): true; + isString( + value: number | boolean | Function | void | null | Object | Array + ): false; + isSymbol(value: any): boolean; + isTypedArray(value: any): boolean; + isUndefined(value: any): boolean; + isWeakMap(value: any): boolean; + isWeakSet(value: any): boolean; + lt(value: any): (other: any) => boolean; + lt(value: any, other: any): boolean; + lte(value: any): (other: any) => boolean; + lte(value: any, other: any): boolean; + toArray(value: any): Array; + toFinite(value: any): number; + toInteger(value: any): number; + toLength(value: any): number; + toNumber(value: any): number; + toPlainObject(value: any): Object; + toSafeInteger(value: any): number; + toString(value: any): string; + + // Math + add(augend: number): (addend: number) => number; + add(augend: number, addend: number): number; + ceil(number: number): number; + divide(dividend: number): (divisor: number) => number; + divide(dividend: number, divisor: number): number; + floor(number: number): number; + max(array: Array): T; + maxBy(iteratee: Iteratee): (array: Array) => T; + maxBy(iteratee: Iteratee, array: Array): T; + mean(array: Array<*>): number; + meanBy(iteratee: Iteratee): (array: Array) => number; + meanBy(iteratee: Iteratee, array: Array): number; + min(array: Array): T; + minBy(iteratee: Iteratee): (array: Array) => T; + minBy(iteratee: Iteratee, array: Array): T; + multiply(multiplier: number): (multiplicand: number) => number; + multiply(multiplier: number, multiplicand: number): number; + round(number: number): number; + subtract(minuend: number): (subtrahend: number) => number; + subtract(minuend: number, subtrahend: number): number; + sum(array: Array<*>): number; + sumBy(iteratee: Iteratee): (array: Array) => number; + sumBy(iteratee: Iteratee, array: Array): number; + + // number + clamp( + lower: number + ): ((upper: number) => (number: number) => number) & + ((upper: number, number: number) => number); + clamp(lower: number, upper: number): (number: number) => number; + clamp(lower: number, upper: number, number: number): number; + inRange( + start: number + ): ((end: number) => (number: number) => boolean) & + ((end: number, number: number) => boolean); + inRange(start: number, end: number): (number: number) => boolean; + inRange(start: number, end: number, number: number): boolean; + random(lower: number): (upper: number) => number; + random(lower: number, upper: number): number; + + // Object + assign(object: Object): (source: Object) => Object; + assign(object: Object, source: Object): Object; + assignAll(objects: Array): Object; + assignInAll(objects: Array): Object; + extendAll(objects: Array): Object; + assignIn(a: A): (b: B) => A & B; + assignIn(a: A, b: B): A & B; + assignInWith( + customizer: ( + objValue: any, + srcValue: any, + key: string, + object: T, + source: A + ) => any | void + ): ((object: T) => (s1: A) => Object) & ((object: T, s1: A) => Object); + assignInWith( + customizer: ( + objValue: any, + srcValue: any, + key: string, + object: T, + source: A + ) => any | void, + object: T + ): (s1: A) => Object; + assignInWith( + customizer: ( + objValue: any, + srcValue: any, + key: string, + object: T, + source: A + ) => any | void, + object: T, + s1: A + ): Object; + assignWith( + customizer: ( + objValue: any, + srcValue: any, + key: string, + object: T, + source: A + ) => any | void + ): ((object: T) => (s1: A) => Object) & ((object: T, s1: A) => Object); + assignWith( + customizer: ( + objValue: any, + srcValue: any, + key: string, + object: T, + source: A + ) => any | void, + object: T + ): (s1: A) => Object; + assignWith( + customizer: ( + objValue: any, + srcValue: any, + key: string, + object: T, + source: A + ) => any | void, + object: T, + s1: A + ): Object; + assignInAllWith( + customizer: ( + objValue: any, + srcValue: any, + key: string, + object: Object, + source: Object + ) => any | void + ): (objects: Array) => Object; + assignInAllWith( + customizer: ( + objValue: any, + srcValue: any, + key: string, + object: Object, + source: Object + ) => any | void, + objects: Array + ): Object; + extendAllWith( + customizer: ( + objValue: any, + srcValue: any, + key: string, + object: Object, + source: Object + ) => any | void + ): (objects: Array) => Object; + extendAllWith( + customizer: ( + objValue: any, + srcValue: any, + key: string, + object: Object, + source: Object + ) => any | void, + objects: Array + ): Object; + assignAllWith( + customizer: ( + objValue: any, + srcValue: any, + key: string, + object: Object, + source: Object + ) => any | void + ): (objects: Array) => Object; + assignAllWith( + customizer: ( + objValue: any, + srcValue: any, + key: string, + object: Object, + source: Object + ) => any | void, + objects: Array + ): Object; + at(paths: Array): (object: Object) => Array; + at(paths: Array, object: Object): Array; + props(paths: Array): (object: Object) => Array; + props(paths: Array, object: Object): Array; + paths(paths: Array): (object: Object) => Array; + paths(paths: Array, object: Object): Array; + create(prototype: T): $Supertype; + defaults(source: Object): (object: Object) => Object; + defaults(source: Object, object: Object): Object; + defaultsAll(objects: Array): Object; + defaultsDeep(source: Object): (object: Object) => Object; + defaultsDeep(source: Object, object: Object): Object; + defaultsDeepAll(objects: Array): Object; + // alias for _.toPairs + entries(object: Object): Array<[string, any]>; + // alias for _.toPairsIn + entriesIn(object: Object): Array<[string, any]>; + // alias for _.assignIn + extend(a: A): (b: B) => A & B; + extend(a: A, b: B): A & B; + // alias for _.assignInWith + extendWith( + customizer: ( + objValue: any, + srcValue: any, + key: string, + object: T, + source: A + ) => any | void + ): ((object: T) => (s1: A) => Object) & ((object: T, s1: A) => Object); + extendWith( + customizer: ( + objValue: any, + srcValue: any, + key: string, + object: T, + source: A + ) => any | void, + object: T + ): (s1: A) => Object; + extendWith( + customizer: ( + objValue: any, + srcValue: any, + key: string, + object: T, + source: A + ) => any | void, + object: T, + s1: A + ): Object; + findKey( + predicate: OPredicate + ): (object: T) => string | void; + findKey( + predicate: OPredicate, + object: T + ): string | void; + findLastKey( + predicate: OPredicate + ): (object: T) => string | void; + findLastKey( + predicate: OPredicate, + object: T + ): string | void; + forIn(iteratee: OIteratee<*>): (object: Object) => Object; + forIn(iteratee: OIteratee<*>, object: Object): Object; + forInRight(iteratee: OIteratee<*>): (object: Object) => Object; + forInRight(iteratee: OIteratee<*>, object: Object): Object; + forOwn(iteratee: OIteratee<*>): (object: Object) => Object; + forOwn(iteratee: OIteratee<*>, object: Object): Object; + forOwnRight(iteratee: OIteratee<*>): (object: Object) => Object; + forOwnRight(iteratee: OIteratee<*>, object: Object): Object; + functions(object: Object): Array; + functionsIn(object: Object): Array; + get(path: Array | string): (object: Object | Array) => any; + get(path: Array | string, object: Object | Array): any; + prop(path: Array | string): (object: Object | Array) => any; + prop(path: Array | string, object: Object | Array): any; + path(path: Array | string): (object: Object | Array) => any; + path(path: Array | string, object: Object | Array): any; + getOr( + defaultValue: any + ): (( + path: Array | string + ) => (object: Object | Array) => any) & + ((path: Array | string, object: Object | Array) => any); + getOr( + defaultValue: any, + path: Array | string + ): (object: Object | Array) => any; + getOr( + defaultValue: any, + path: Array | string, + object: Object | Array + ): any; + propOr( + defaultValue: any + ): (( + path: Array | string + ) => (object: Object | Array) => any) & + ((path: Array | string, object: Object | Array) => any); + propOr( + defaultValue: any, + path: Array | string + ): (object: Object | Array) => any; + propOr( + defaultValue: any, + path: Array | string, + object: Object | Array + ): any; + pathOr( + defaultValue: any + ): (( + path: Array | string + ) => (object: Object | Array) => any) & + ((path: Array | string, object: Object | Array) => any); + pathOr( + defaultValue: any, + path: Array | string + ): (object: Object | Array) => any; + pathOr( + defaultValue: any, + path: Array | string, + object: Object | Array + ): any; + has(path: Array | string): (object: Object) => boolean; + has(path: Array | string, object: Object): boolean; + hasIn(path: Array | string): (object: Object) => boolean; + hasIn(path: Array | string, object: Object): boolean; + invert(object: Object): Object; + invertObj(object: Object): Object; + invertBy(iteratee: Function): (object: Object) => Object; + invertBy(iteratee: Function, object: Object): Object; + invoke(path: Array | string): (object: Object) => any; + invoke(path: Array | string, object: Object): any; + invokeArgs( + path: Array | string + ): ((object: Object) => (args: Array) => any) & + ((object: Object, args: Array) => any); + invokeArgs( + path: Array | string, + object: Object + ): (args: Array) => any; + invokeArgs( + path: Array | string, + object: Object, + args: Array + ): any; + keys(object: { [key: K]: any }): Array; + keys(object: Object): Array; + keysIn(object: Object): Array; + mapKeys(iteratee: OIteratee<*>): (object: Object) => Object; + mapKeys(iteratee: OIteratee<*>, object: Object): Object; + mapValues(iteratee: OIteratee<*>): (object: Object) => Object; + mapValues(iteratee: OIteratee<*>, object: Object): Object; + merge(object: Object): (source: Object) => Object; + merge(object: Object, source: Object): Object; + mergeAll(objects: Array): Object; + mergeWith( + customizer: ( + objValue: any, + srcValue: any, + key: string, + object: T, + source: A | B + ) => any | void + ): ((object: T) => (s1: A) => Object) & ((object: T, s1: A) => Object); + mergeWith( + customizer: ( + objValue: any, + srcValue: any, + key: string, + object: T, + source: A | B + ) => any | void, + object: T + ): (s1: A) => Object; + mergeWith( + customizer: ( + objValue: any, + srcValue: any, + key: string, + object: T, + source: A | B + ) => any | void, + object: T, + s1: A + ): Object; + mergeAllWith( + customizer: ( + objValue: any, + srcValue: any, + key: string, + object: Object, + source: Object + ) => any | void + ): (objects: Array) => Object; + mergeAllWith( + customizer: ( + objValue: any, + srcValue: any, + key: string, + object: Object, + source: Object + ) => any | void, + objects: Array + ): Object; + omit(props: Array): (object: Object) => Object; + omit(props: Array, object: Object): Object; + omitAll(props: Array): (object: Object) => Object; + omitAll(props: Array, object: Object): Object; + omitBy( + predicate: OPredicate + ): (object: T) => Object; + omitBy(predicate: OPredicate, object: T): Object; + pick(props: Array): (object: Object) => Object; + pick(props: Array, object: Object): Object; + pickAll(props: Array): (object: Object) => Object; + pickAll(props: Array, object: Object): Object; + pickBy( + predicate: OPredicate + ): (object: T) => Object; + pickBy(predicate: OPredicate, object: T): Object; + result(path: Array | string): (object: Object) => any; + result(path: Array | string, object: Object): any; + set( + path: Array | string + ): ((value: any) => (object: Object) => Object) & + ((value: any, object: Object) => Object); + set(path: Array | string, value: any): (object: Object) => Object; + set(path: Array | string, value: any, object: Object): Object; + assoc( + path: Array | string + ): ((value: any) => (object: Object) => Object) & + ((value: any, object: Object) => Object); + assoc(path: Array | string, value: any): (object: Object) => Object; + assoc(path: Array | string, value: any, object: Object): Object; + assocPath( + path: Array | string + ): ((value: any) => (object: Object) => Object) & + ((value: any, object: Object) => Object); + assocPath( + path: Array | string, + value: any + ): (object: Object) => Object; + assocPath(path: Array | string, value: any, object: Object): Object; + setWith( + customizer: (nsValue: any, key: string, nsObject: T) => any + ): (( + path: Array | string + ) => ((value: any) => (object: T) => Object) & + ((value: any, object: T) => Object)) & + ((path: Array | string, value: any) => (object: T) => Object) & + ((path: Array | string, value: any, object: T) => Object); + setWith( + customizer: (nsValue: any, key: string, nsObject: T) => any, + path: Array | string + ): ((value: any) => (object: T) => Object) & + ((value: any, object: T) => Object); + setWith( + customizer: (nsValue: any, key: string, nsObject: T) => any, + path: Array | string, + value: any + ): (object: T) => Object; + setWith( + customizer: (nsValue: any, key: string, nsObject: T) => any, + path: Array | string, + value: any, + object: T + ): Object; + toPairs(object: Object | Array<*>): Array<[string, any]>; + toPairsIn(object: Object): Array<[string, any]>; + transform( + iteratee: OIteratee<*> + ): ((accumulator: any) => (collection: Object | Array) => any) & + ((accumulator: any, collection: Object | Array) => any); + transform( + iteratee: OIteratee<*>, + accumulator: any + ): (collection: Object | Array) => any; + transform( + iteratee: OIteratee<*>, + accumulator: any, + collection: Object | Array + ): any; + unset(path: Array | string): (object: Object) => boolean; + unset(path: Array | string, object: Object): boolean; + dissoc(path: Array | string): (object: Object) => boolean; + dissoc(path: Array | string, object: Object): boolean; + dissocPath(path: Array | string): (object: Object) => boolean; + dissocPath(path: Array | string, object: Object): boolean; + update( + path: string[] | string + ): ((updater: Function) => (object: Object) => Object) & + ((updater: Function, object: Object) => Object); + update( + path: string[] | string, + updater: Function + ): (object: Object) => Object; + update(path: string[] | string, updater: Function, object: Object): Object; + updateWith( + customizer: Function + ): (( + path: string[] | string + ) => ((updater: Function) => (object: Object) => Object) & + ((updater: Function, object: Object) => Object)) & + (( + path: string[] | string, + updater: Function + ) => (object: Object) => Object) & + ((path: string[] | string, updater: Function, object: Object) => Object); + updateWith( + customizer: Function, + path: string[] | string + ): ((updater: Function) => (object: Object) => Object) & + ((updater: Function, object: Object) => Object); + updateWith( + customizer: Function, + path: string[] | string, + updater: Function + ): (object: Object) => Object; + updateWith( + customizer: Function, + path: string[] | string, + updater: Function, + object: Object + ): Object; + values(object: Object): Array; + valuesIn(object: Object): Array; + + tap(interceptor: (value: T) => any): (value: T) => T; + tap(interceptor: (value: T) => any, value: T): T; + thru(interceptor: (value: T1) => T2): (value: T1) => T2; + thru(interceptor: (value: T1) => T2, value: T1): T2; + + // String + camelCase(string: string): string; + capitalize(string: string): string; + deburr(string: string): string; + endsWith(target: string): (string: string) => boolean; + endsWith(target: string, string: string): boolean; + escape(string: string): string; + escapeRegExp(string: string): string; + kebabCase(string: string): string; + lowerCase(string: string): string; + lowerFirst(string: string): string; + pad(length: number): (string: string) => string; + pad(length: number, string: string): string; + padChars( + chars: string + ): ((length: number) => (string: string) => string) & + ((length: number, string: string) => string); + padChars(chars: string, length: number): (string: string) => string; + padChars(chars: string, length: number, string: string): string; + padEnd(length: number): (string: string) => string; + padEnd(length: number, string: string): string; + padCharsEnd( + chars: string + ): ((length: number) => (string: string) => string) & + ((length: number, string: string) => string); + padCharsEnd(chars: string, length: number): (string: string) => string; + padCharsEnd(chars: string, length: number, string: string): string; + padStart(length: number): (string: string) => string; + padStart(length: number, string: string): string; + padCharsStart( + chars: string + ): ((length: number) => (string: string) => string) & + ((length: number, string: string) => string); + padCharsStart(chars: string, length: number): (string: string) => string; + padCharsStart(chars: string, length: number, string: string): string; + parseInt(radix: number): (string: string) => number; + parseInt(radix: number, string: string): number; + repeat(n: number): (string: string) => string; + repeat(n: number, string: string): string; + replace( + pattern: RegExp | string + ): (( + replacement: ((string: string) => string) | string + ) => (string: string) => string) & + (( + replacement: ((string: string) => string) | string, + string: string + ) => string); + replace( + pattern: RegExp | string, + replacement: ((string: string) => string) | string + ): (string: string) => string; + replace( + pattern: RegExp | string, + replacement: ((string: string) => string) | string, + string: string + ): string; + snakeCase(string: string): string; + split(separator: RegExp | string): (string: string) => Array; + split(separator: RegExp | string, string: string): Array; + startCase(string: string): string; + startsWith(target: string): (string: string) => boolean; + startsWith(target: string, string: string): boolean; + template(string: string): Function; + toLower(string: string): string; + toUpper(string: string): string; + trim(string: string): string; + trimChars(chars: string): (string: string) => string; + trimChars(chars: string, string: string): string; + trimEnd(string: string): string; + trimCharsEnd(chars: string): (string: string) => string; + trimCharsEnd(chars: string, string: string): string; + trimStart(string: string): string; + trimCharsStart(chars: string): (string: string) => string; + trimCharsStart(chars: string, string: string): string; + truncate(options: TruncateOptions): (string: string) => string; + truncate(options: TruncateOptions, string: string): string; + unescape(string: string): string; + upperCase(string: string): string; + upperFirst(string: string): string; + words(string: string): Array; + + // Util + attempt(func: Function): any; + bindAll(methodNames: Array): (object: Object) => Object; + bindAll(methodNames: Array, object: Object): Object; + cond(pairs: NestedArray): Function; + constant(value: T): () => T; + always(value: T): () => T; + defaultTo( + defaultValue: T2 + ): (value: T1) => T1; + defaultTo( + defaultValue: T2, + value: T1 + ): T1; + // NaN is a number instead of its own type, otherwise it would behave like null/void + defaultTo(defaultValue: T2): (value: T1) => T1 | T2; + defaultTo(defaultValue: T2, value: T1): T1 | T2; + defaultTo(defaultValue: T2): (value: T1) => T2; + defaultTo(defaultValue: T2, value: T1): T2; + flow: Flow; + flow(funcs: Array): Function; + pipe: Flow; + pipe(funcs: Array): Function; + flowRight: FlowRight; + flowRight(funcs: Array): Function; + compose: FlowRight; + compose(funcs: Array): Function; + identity(value: T): T; + iteratee(func: any): Function; + matches(source: Object): (object: Object) => boolean; + matches(source: Object, object: Object): boolean; + matchesProperty(path: Array | string): (srcValue: any) => Function; + matchesProperty(path: Array | string, srcValue: any): Function; + propEq(path: Array | string): (srcValue: any) => Function; + propEq(path: Array | string, srcValue: any): Function; + pathEq(path: Array | string): (srcValue: any) => Function; + pathEq(path: Array | string, srcValue: any): Function; + method(path: Array | string): Function; + methodOf(object: Object): Function; + mixin( + object: T + ): ((source: Object) => (options: { chain: boolean }) => T) & + ((source: Object, options: { chain: boolean }) => T); + mixin( + object: T, + source: Object + ): (options: { chain: boolean }) => T; + mixin( + object: T, + source: Object, + options: { chain: boolean } + ): T; + noConflict(): Lodash; + noop(...args: Array): void; + nthArg(n: number): Function; + over(iteratees: Array): Function; + juxt(iteratees: Array): Function; + overEvery(predicates: Array): Function; + allPass(predicates: Array): Function; + overSome(predicates: Array): Function; + anyPass(predicates: Array): Function; + property( + path: Array | string + ): (object: Object | Array) => any; + property(path: Array | string, object: Object | Array): any; + propertyOf(object: Object): (path: Array | string) => Function; + propertyOf(object: Object, path: Array | string): Function; + range(start: number): (end: number) => Array; + range(start: number, end: number): Array; + rangeStep( + step: number + ): ((start: number) => (end: number) => Array) & + ((start: number, end: number) => Array); + rangeStep(step: number, start: number): (end: number) => Array; + rangeStep(step: number, start: number, end: number): Array; + rangeRight(start: number): (end: number) => Array; + rangeRight(start: number, end: number): Array; + rangeStepRight( + step: number + ): ((start: number) => (end: number) => Array) & + ((start: number, end: number) => Array); + rangeStepRight(step: number, start: number): (end: number) => Array; + rangeStepRight(step: number, start: number, end: number): Array; + runInContext(context: Object): Function; + + stubArray(): Array<*>; + stubFalse(): false; + F(): false; + stubObject(): {}; + stubString(): ""; + stubTrue(): true; + T(): true; + times(iteratee: (i: number) => T): (n: number) => Array; + times(iteratee: (i: number) => T, n: number): Array; + toPath(value: any): Array; + uniqueId(prefix: string): string; + + __: any; + placeholder: any; + + convert(options: { + cap?: boolean, + curry?: boolean, + fixed?: boolean, + immutable?: boolean, + rearg?: boolean + }): void; + + // Properties + VERSION: string; + templateSettings: TemplateSettings; + } + + declare module.exports: Lodash; +} + +declare module "lodash/chunk" { + declare module.exports: $PropertyType<$Exports<"lodash">, "chunk">; +} + +declare module "lodash/compact" { + declare module.exports: $PropertyType<$Exports<"lodash">, "compact">; +} + +declare module "lodash/concat" { + declare module.exports: $PropertyType<$Exports<"lodash">, "concat">; +} + +declare module "lodash/difference" { + declare module.exports: $PropertyType<$Exports<"lodash">, "difference">; +} + +declare module "lodash/differenceBy" { + declare module.exports: $PropertyType<$Exports<"lodash">, "differenceBy">; +} + +declare module "lodash/differenceWith" { + declare module.exports: $PropertyType<$Exports<"lodash">, "differenceWith">; +} + +declare module "lodash/drop" { + declare module.exports: $PropertyType<$Exports<"lodash">, "drop">; +} + +declare module "lodash/dropRight" { + declare module.exports: $PropertyType<$Exports<"lodash">, "dropRight">; +} + +declare module "lodash/dropRightWhile" { + declare module.exports: $PropertyType<$Exports<"lodash">, "dropRightWhile">; +} + +declare module "lodash/dropWhile" { + declare module.exports: $PropertyType<$Exports<"lodash">, "dropWhile">; +} + +declare module "lodash/fill" { + declare module.exports: $PropertyType<$Exports<"lodash">, "fill">; +} + +declare module "lodash/findIndex" { + declare module.exports: $PropertyType<$Exports<"lodash">, "findIndex">; +} + +declare module "lodash/findLastIndex" { + declare module.exports: $PropertyType<$Exports<"lodash">, "findLastIndex">; +} + +declare module "lodash/first" { + declare module.exports: $PropertyType<$Exports<"lodash">, "first">; +} + +declare module "lodash/flatten" { + declare module.exports: $PropertyType<$Exports<"lodash">, "flatten">; +} + +declare module "lodash/flattenDeep" { + declare module.exports: $PropertyType<$Exports<"lodash">, "flattenDeep">; +} + +declare module "lodash/flattenDepth" { + declare module.exports: $PropertyType<$Exports<"lodash">, "flattenDepth">; +} + +declare module "lodash/fromPairs" { + declare module.exports: $PropertyType<$Exports<"lodash">, "fromPairs">; +} + +declare module "lodash/head" { + declare module.exports: $PropertyType<$Exports<"lodash">, "head">; +} + +declare module "lodash/indexOf" { + declare module.exports: $PropertyType<$Exports<"lodash">, "indexOf">; +} + +declare module "lodash/initial" { + declare module.exports: $PropertyType<$Exports<"lodash">, "initial">; +} + +declare module "lodash/intersection" { + declare module.exports: $PropertyType<$Exports<"lodash">, "intersection">; +} + +declare module "lodash/intersectionBy" { + declare module.exports: $PropertyType<$Exports<"lodash">, "intersectionBy">; +} + +declare module "lodash/intersectionWith" { + declare module.exports: $PropertyType<$Exports<"lodash">, "intersectionWith">; +} + +declare module "lodash/join" { + declare module.exports: $PropertyType<$Exports<"lodash">, "join">; +} + +declare module "lodash/last" { + declare module.exports: $PropertyType<$Exports<"lodash">, "last">; +} + +declare module "lodash/lastIndexOf" { + declare module.exports: $PropertyType<$Exports<"lodash">, "lastIndexOf">; +} + +declare module "lodash/nth" { + declare module.exports: $PropertyType<$Exports<"lodash">, "nth">; +} + +declare module "lodash/pull" { + declare module.exports: $PropertyType<$Exports<"lodash">, "pull">; +} + +declare module "lodash/pullAll" { + declare module.exports: $PropertyType<$Exports<"lodash">, "pullAll">; +} + +declare module "lodash/pullAllBy" { + declare module.exports: $PropertyType<$Exports<"lodash">, "pullAllBy">; +} + +declare module "lodash/pullAllWith" { + declare module.exports: $PropertyType<$Exports<"lodash">, "pullAllWith">; +} + +declare module "lodash/pullAt" { + declare module.exports: $PropertyType<$Exports<"lodash">, "pullAt">; +} + +declare module "lodash/remove" { + declare module.exports: $PropertyType<$Exports<"lodash">, "remove">; +} + +declare module "lodash/reverse" { + declare module.exports: $PropertyType<$Exports<"lodash">, "reverse">; +} + +declare module "lodash/slice" { + declare module.exports: $PropertyType<$Exports<"lodash">, "slice">; +} + +declare module "lodash/sortedIndex" { + declare module.exports: $PropertyType<$Exports<"lodash">, "sortedIndex">; +} + +declare module "lodash/sortedIndexBy" { + declare module.exports: $PropertyType<$Exports<"lodash">, "sortedIndexBy">; +} + +declare module "lodash/sortedIndexOf" { + declare module.exports: $PropertyType<$Exports<"lodash">, "sortedIndexOf">; +} + +declare module "lodash/sortedLastIndex" { + declare module.exports: $PropertyType<$Exports<"lodash">, "sortedLastIndex">; +} + +declare module "lodash/sortedLastIndexBy" { + declare module.exports: $PropertyType< + $Exports<"lodash">, + "sortedLastIndexBy" + >; +} + +declare module "lodash/sortedLastIndexOf" { + declare module.exports: $PropertyType< + $Exports<"lodash">, + "sortedLastIndexOf" + >; +} + +declare module "lodash/sortedUniq" { + declare module.exports: $PropertyType<$Exports<"lodash">, "sortedUniq">; +} + +declare module "lodash/sortedUniqBy" { + declare module.exports: $PropertyType<$Exports<"lodash">, "sortedUniqBy">; +} + +declare module "lodash/tail" { + declare module.exports: $PropertyType<$Exports<"lodash">, "tail">; +} + +declare module "lodash/take" { + declare module.exports: $PropertyType<$Exports<"lodash">, "take">; +} + +declare module "lodash/takeRight" { + declare module.exports: $PropertyType<$Exports<"lodash">, "takeRight">; +} + +declare module "lodash/takeRightWhile" { + declare module.exports: $PropertyType<$Exports<"lodash">, "takeRightWhile">; +} + +declare module "lodash/takeWhile" { + declare module.exports: $PropertyType<$Exports<"lodash">, "takeWhile">; +} + +declare module "lodash/union" { + declare module.exports: $PropertyType<$Exports<"lodash">, "union">; +} + +declare module "lodash/unionBy" { + declare module.exports: $PropertyType<$Exports<"lodash">, "unionBy">; +} + +declare module "lodash/unionWith" { + declare module.exports: $PropertyType<$Exports<"lodash">, "unionWith">; +} + +declare module "lodash/uniq" { + declare module.exports: $PropertyType<$Exports<"lodash">, "uniq">; +} + +declare module "lodash/uniqBy" { + declare module.exports: $PropertyType<$Exports<"lodash">, "uniqBy">; +} + +declare module "lodash/uniqWith" { + declare module.exports: $PropertyType<$Exports<"lodash">, "uniqWith">; +} + +declare module "lodash/unzip" { + declare module.exports: $PropertyType<$Exports<"lodash">, "unzip">; +} + +declare module "lodash/unzipWith" { + declare module.exports: $PropertyType<$Exports<"lodash">, "unzipWith">; +} + +declare module "lodash/without" { + declare module.exports: $PropertyType<$Exports<"lodash">, "without">; +} + +declare module "lodash/xor" { + declare module.exports: $PropertyType<$Exports<"lodash">, "xor">; +} + +declare module "lodash/xorBy" { + declare module.exports: $PropertyType<$Exports<"lodash">, "xorBy">; +} + +declare module "lodash/xorWith" { + declare module.exports: $PropertyType<$Exports<"lodash">, "xorWith">; +} + +declare module "lodash/zip" { + declare module.exports: $PropertyType<$Exports<"lodash">, "zip">; +} + +declare module "lodash/zipObject" { + declare module.exports: $PropertyType<$Exports<"lodash">, "zipObject">; +} + +declare module "lodash/zipObjectDeep" { + declare module.exports: $PropertyType<$Exports<"lodash">, "zipObjectDeep">; +} + +declare module "lodash/zipWith" { + declare module.exports: $PropertyType<$Exports<"lodash">, "zipWith">; +} + +declare module "lodash/countBy" { + declare module.exports: $PropertyType<$Exports<"lodash">, "countBy">; +} + +declare module "lodash/each" { + declare module.exports: $PropertyType<$Exports<"lodash">, "each">; +} + +declare module "lodash/eachRight" { + declare module.exports: $PropertyType<$Exports<"lodash">, "eachRight">; +} + +declare module "lodash/every" { + declare module.exports: $PropertyType<$Exports<"lodash">, "every">; +} + +declare module "lodash/filter" { + declare module.exports: $PropertyType<$Exports<"lodash">, "filter">; +} + +declare module "lodash/find" { + declare module.exports: $PropertyType<$Exports<"lodash">, "find">; +} + +declare module "lodash/findLast" { + declare module.exports: $PropertyType<$Exports<"lodash">, "findLast">; +} + +declare module "lodash/flatMap" { + declare module.exports: $PropertyType<$Exports<"lodash">, "flatMap">; +} + +declare module "lodash/flatMapDeep" { + declare module.exports: $PropertyType<$Exports<"lodash">, "flatMapDeep">; +} + +declare module "lodash/flatMapDepth" { + declare module.exports: $PropertyType<$Exports<"lodash">, "flatMapDepth">; +} + +declare module "lodash/forEach" { + declare module.exports: $PropertyType<$Exports<"lodash">, "forEach">; +} + +declare module "lodash/forEachRight" { + declare module.exports: $PropertyType<$Exports<"lodash">, "forEachRight">; +} + +declare module "lodash/groupBy" { + declare module.exports: $PropertyType<$Exports<"lodash">, "groupBy">; +} + +declare module "lodash/includes" { + declare module.exports: $PropertyType<$Exports<"lodash">, "includes">; +} + +declare module "lodash/invokeMap" { + declare module.exports: $PropertyType<$Exports<"lodash">, "invokeMap">; +} + +declare module "lodash/keyBy" { + declare module.exports: $PropertyType<$Exports<"lodash">, "keyBy">; +} + +declare module "lodash/map" { + declare module.exports: $PropertyType<$Exports<"lodash">, "map">; +} + +declare module "lodash/orderBy" { + declare module.exports: $PropertyType<$Exports<"lodash">, "orderBy">; +} + +declare module "lodash/partition" { + declare module.exports: $PropertyType<$Exports<"lodash">, "partition">; +} + +declare module "lodash/reduce" { + declare module.exports: $PropertyType<$Exports<"lodash">, "reduce">; +} + +declare module "lodash/reduceRight" { + declare module.exports: $PropertyType<$Exports<"lodash">, "reduceRight">; +} + +declare module "lodash/reject" { + declare module.exports: $PropertyType<$Exports<"lodash">, "reject">; +} + +declare module "lodash/sample" { + declare module.exports: $PropertyType<$Exports<"lodash">, "sample">; +} + +declare module "lodash/sampleSize" { + declare module.exports: $PropertyType<$Exports<"lodash">, "sampleSize">; +} + +declare module "lodash/shuffle" { + declare module.exports: $PropertyType<$Exports<"lodash">, "shuffle">; +} + +declare module "lodash/size" { + declare module.exports: $PropertyType<$Exports<"lodash">, "size">; +} + +declare module "lodash/some" { + declare module.exports: $PropertyType<$Exports<"lodash">, "some">; +} + +declare module "lodash/sortBy" { + declare module.exports: $PropertyType<$Exports<"lodash">, "sortBy">; +} + +declare module "lodash/now" { + declare module.exports: $PropertyType<$Exports<"lodash">, "now">; +} + +declare module "lodash/after" { + declare module.exports: $PropertyType<$Exports<"lodash">, "after">; +} + +declare module "lodash/ary" { + declare module.exports: $PropertyType<$Exports<"lodash">, "ary">; +} + +declare module "lodash/before" { + declare module.exports: $PropertyType<$Exports<"lodash">, "before">; +} + +declare module "lodash/bind" { + declare module.exports: $PropertyType<$Exports<"lodash">, "bind">; +} + +declare module "lodash/bindKey" { + declare module.exports: $PropertyType<$Exports<"lodash">, "bindKey">; +} + +declare module "lodash/curry" { + declare module.exports: $PropertyType<$Exports<"lodash">, "curry">; +} + +declare module "lodash/curryRight" { + declare module.exports: $PropertyType<$Exports<"lodash">, "curryRight">; +} + +declare module "lodash/debounce" { + declare module.exports: $PropertyType<$Exports<"lodash">, "debounce">; +} + +declare module "lodash/defer" { + declare module.exports: $PropertyType<$Exports<"lodash">, "defer">; +} + +declare module "lodash/delay" { + declare module.exports: $PropertyType<$Exports<"lodash">, "delay">; +} + +declare module "lodash/flip" { + declare module.exports: $PropertyType<$Exports<"lodash">, "flip">; +} + +declare module "lodash/memoize" { + declare module.exports: $PropertyType<$Exports<"lodash">, "memoize">; +} + +declare module "lodash/negate" { + declare module.exports: $PropertyType<$Exports<"lodash">, "negate">; +} + +declare module "lodash/once" { + declare module.exports: $PropertyType<$Exports<"lodash">, "once">; +} + +declare module "lodash/overArgs" { + declare module.exports: $PropertyType<$Exports<"lodash">, "overArgs">; +} + +declare module "lodash/partial" { + declare module.exports: $PropertyType<$Exports<"lodash">, "partial">; +} + +declare module "lodash/partialRight" { + declare module.exports: $PropertyType<$Exports<"lodash">, "partialRight">; +} + +declare module "lodash/rearg" { + declare module.exports: $PropertyType<$Exports<"lodash">, "rearg">; +} + +declare module "lodash/rest" { + declare module.exports: $PropertyType<$Exports<"lodash">, "rest">; +} + +declare module "lodash/spread" { + declare module.exports: $PropertyType<$Exports<"lodash">, "spread">; +} + +declare module "lodash/throttle" { + declare module.exports: $PropertyType<$Exports<"lodash">, "throttle">; +} + +declare module "lodash/unary" { + declare module.exports: $PropertyType<$Exports<"lodash">, "unary">; +} + +declare module "lodash/wrap" { + declare module.exports: $PropertyType<$Exports<"lodash">, "wrap">; +} + +declare module "lodash/castArray" { + declare module.exports: $PropertyType<$Exports<"lodash">, "castArray">; +} + +declare module "lodash/clone" { + declare module.exports: $PropertyType<$Exports<"lodash">, "clone">; +} + +declare module "lodash/cloneDeep" { + declare module.exports: $PropertyType<$Exports<"lodash">, "cloneDeep">; +} + +declare module "lodash/cloneDeepWith" { + declare module.exports: $PropertyType<$Exports<"lodash">, "cloneDeepWith">; +} + +declare module "lodash/cloneWith" { + declare module.exports: $PropertyType<$Exports<"lodash">, "cloneWith">; +} + +declare module "lodash/conformsTo" { + declare module.exports: $PropertyType<$Exports<"lodash">, "conformsTo">; +} + +declare module "lodash/eq" { + declare module.exports: $PropertyType<$Exports<"lodash">, "eq">; +} + +declare module "lodash/gt" { + declare module.exports: $PropertyType<$Exports<"lodash">, "gt">; +} + +declare module "lodash/gte" { + declare module.exports: $PropertyType<$Exports<"lodash">, "gte">; +} + +declare module "lodash/isArguments" { + declare module.exports: $PropertyType<$Exports<"lodash">, "isArguments">; +} + +declare module "lodash/isArray" { + declare module.exports: $PropertyType<$Exports<"lodash">, "isArray">; +} + +declare module "lodash/isArrayBuffer" { + declare module.exports: $PropertyType<$Exports<"lodash">, "isArrayBuffer">; +} + +declare module "lodash/isArrayLike" { + declare module.exports: $PropertyType<$Exports<"lodash">, "isArrayLike">; +} + +declare module "lodash/isArrayLikeObject" { + declare module.exports: $PropertyType< + $Exports<"lodash">, + "isArrayLikeObject" + >; +} + +declare module "lodash/isBoolean" { + declare module.exports: $PropertyType<$Exports<"lodash">, "isBoolean">; +} + +declare module "lodash/isBuffer" { + declare module.exports: $PropertyType<$Exports<"lodash">, "isBuffer">; +} + +declare module "lodash/isDate" { + declare module.exports: $PropertyType<$Exports<"lodash">, "isDate">; +} + +declare module "lodash/isElement" { + declare module.exports: $PropertyType<$Exports<"lodash">, "isElement">; +} + +declare module "lodash/isEmpty" { + declare module.exports: $PropertyType<$Exports<"lodash">, "isEmpty">; +} + +declare module "lodash/isEqual" { + declare module.exports: $PropertyType<$Exports<"lodash">, "isEqual">; +} + +declare module "lodash/isEqualWith" { + declare module.exports: $PropertyType<$Exports<"lodash">, "isEqualWith">; +} + +declare module "lodash/isError" { + declare module.exports: $PropertyType<$Exports<"lodash">, "isError">; +} + +declare module "lodash/isFinite" { + declare module.exports: $PropertyType<$Exports<"lodash">, "isFinite">; +} + +declare module "lodash/isFunction" { + declare module.exports: $PropertyType<$Exports<"lodash">, "isFunction">; +} + +declare module "lodash/isInteger" { + declare module.exports: $PropertyType<$Exports<"lodash">, "isInteger">; +} + +declare module "lodash/isLength" { + declare module.exports: $PropertyType<$Exports<"lodash">, "isLength">; +} + +declare module "lodash/isMap" { + declare module.exports: $PropertyType<$Exports<"lodash">, "isMap">; +} + +declare module "lodash/isMatch" { + declare module.exports: $PropertyType<$Exports<"lodash">, "isMatch">; +} + +declare module "lodash/isMatchWith" { + declare module.exports: $PropertyType<$Exports<"lodash">, "isMatchWith">; +} + +declare module "lodash/isNaN" { + declare module.exports: $PropertyType<$Exports<"lodash">, "isNaN">; +} + +declare module "lodash/isNative" { + declare module.exports: $PropertyType<$Exports<"lodash">, "isNative">; +} + +declare module "lodash/isNil" { + declare module.exports: $PropertyType<$Exports<"lodash">, "isNil">; +} + +declare module "lodash/isNull" { + declare module.exports: $PropertyType<$Exports<"lodash">, "isNull">; +} + +declare module "lodash/isNumber" { + declare module.exports: $PropertyType<$Exports<"lodash">, "isNumber">; +} + +declare module "lodash/isObject" { + declare module.exports: $PropertyType<$Exports<"lodash">, "isObject">; +} + +declare module "lodash/isObjectLike" { + declare module.exports: $PropertyType<$Exports<"lodash">, "isObjectLike">; +} + +declare module "lodash/isPlainObject" { + declare module.exports: $PropertyType<$Exports<"lodash">, "isPlainObject">; +} + +declare module "lodash/isRegExp" { + declare module.exports: $PropertyType<$Exports<"lodash">, "isRegExp">; +} + +declare module "lodash/isSafeInteger" { + declare module.exports: $PropertyType<$Exports<"lodash">, "isSafeInteger">; +} + +declare module "lodash/isSet" { + declare module.exports: $PropertyType<$Exports<"lodash">, "isSet">; +} + +declare module "lodash/isString" { + declare module.exports: $PropertyType<$Exports<"lodash">, "isString">; +} + +declare module "lodash/isSymbol" { + declare module.exports: $PropertyType<$Exports<"lodash">, "isSymbol">; +} + +declare module "lodash/isTypedArray" { + declare module.exports: $PropertyType<$Exports<"lodash">, "isTypedArray">; +} + +declare module "lodash/isUndefined" { + declare module.exports: $PropertyType<$Exports<"lodash">, "isUndefined">; +} + +declare module "lodash/isWeakMap" { + declare module.exports: $PropertyType<$Exports<"lodash">, "isWeakMap">; +} + +declare module "lodash/isWeakSet" { + declare module.exports: $PropertyType<$Exports<"lodash">, "isWeakSet">; +} + +declare module "lodash/lt" { + declare module.exports: $PropertyType<$Exports<"lodash">, "lt">; +} + +declare module "lodash/lte" { + declare module.exports: $PropertyType<$Exports<"lodash">, "lte">; +} + +declare module "lodash/toArray" { + declare module.exports: $PropertyType<$Exports<"lodash">, "toArray">; +} + +declare module "lodash/toFinite" { + declare module.exports: $PropertyType<$Exports<"lodash">, "toFinite">; +} + +declare module "lodash/toInteger" { + declare module.exports: $PropertyType<$Exports<"lodash">, "toInteger">; +} + +declare module "lodash/toLength" { + declare module.exports: $PropertyType<$Exports<"lodash">, "toLength">; +} + +declare module "lodash/toNumber" { + declare module.exports: $PropertyType<$Exports<"lodash">, "toNumber">; +} + +declare module "lodash/toPlainObject" { + declare module.exports: $PropertyType<$Exports<"lodash">, "toPlainObject">; +} + +declare module "lodash/toSafeInteger" { + declare module.exports: $PropertyType<$Exports<"lodash">, "toSafeInteger">; +} + +declare module "lodash/toString" { + declare module.exports: $PropertyType<$Exports<"lodash">, "toString">; +} + +declare module "lodash/add" { + declare module.exports: $PropertyType<$Exports<"lodash">, "add">; +} + +declare module "lodash/ceil" { + declare module.exports: $PropertyType<$Exports<"lodash">, "ceil">; +} + +declare module "lodash/divide" { + declare module.exports: $PropertyType<$Exports<"lodash">, "divide">; +} + +declare module "lodash/floor" { + declare module.exports: $PropertyType<$Exports<"lodash">, "floor">; +} + +declare module "lodash/max" { + declare module.exports: $PropertyType<$Exports<"lodash">, "max">; +} + +declare module "lodash/maxBy" { + declare module.exports: $PropertyType<$Exports<"lodash">, "maxBy">; +} + +declare module "lodash/mean" { + declare module.exports: $PropertyType<$Exports<"lodash">, "mean">; +} + +declare module "lodash/meanBy" { + declare module.exports: $PropertyType<$Exports<"lodash">, "meanBy">; +} + +declare module "lodash/min" { + declare module.exports: $PropertyType<$Exports<"lodash">, "min">; +} + +declare module "lodash/minBy" { + declare module.exports: $PropertyType<$Exports<"lodash">, "minBy">; +} + +declare module "lodash/multiply" { + declare module.exports: $PropertyType<$Exports<"lodash">, "multiply">; +} + +declare module "lodash/round" { + declare module.exports: $PropertyType<$Exports<"lodash">, "round">; +} + +declare module "lodash/subtract" { + declare module.exports: $PropertyType<$Exports<"lodash">, "subtract">; +} + +declare module "lodash/sum" { + declare module.exports: $PropertyType<$Exports<"lodash">, "sum">; +} + +declare module "lodash/sumBy" { + declare module.exports: $PropertyType<$Exports<"lodash">, "sumBy">; +} + +declare module "lodash/clamp" { + declare module.exports: $PropertyType<$Exports<"lodash">, "clamp">; +} + +declare module "lodash/inRange" { + declare module.exports: $PropertyType<$Exports<"lodash">, "inRange">; +} + +declare module "lodash/random" { + declare module.exports: $PropertyType<$Exports<"lodash">, "random">; +} + +declare module "lodash/assign" { + declare module.exports: $PropertyType<$Exports<"lodash">, "assign">; +} + +declare module "lodash/assignIn" { + declare module.exports: $PropertyType<$Exports<"lodash">, "assignIn">; +} + +declare module "lodash/assignInWith" { + declare module.exports: $PropertyType<$Exports<"lodash">, "assignInWith">; +} + +declare module "lodash/assignWith" { + declare module.exports: $PropertyType<$Exports<"lodash">, "assignWith">; +} + +declare module "lodash/at" { + declare module.exports: $PropertyType<$Exports<"lodash">, "at">; +} + +declare module "lodash/create" { + declare module.exports: $PropertyType<$Exports<"lodash">, "create">; +} + +declare module "lodash/defaults" { + declare module.exports: $PropertyType<$Exports<"lodash">, "defaults">; +} + +declare module "lodash/defaultsDeep" { + declare module.exports: $PropertyType<$Exports<"lodash">, "defaultsDeep">; +} + +declare module "lodash/entries" { + declare module.exports: $PropertyType<$Exports<"lodash">, "entries">; +} + +declare module "lodash/entriesIn" { + declare module.exports: $PropertyType<$Exports<"lodash">, "entriesIn">; +} + +declare module "lodash/extend" { + declare module.exports: $PropertyType<$Exports<"lodash">, "extend">; +} + +declare module "lodash/extendWith" { + declare module.exports: $PropertyType<$Exports<"lodash">, "extendWith">; +} + +declare module "lodash/findKey" { + declare module.exports: $PropertyType<$Exports<"lodash">, "findKey">; +} + +declare module "lodash/findLastKey" { + declare module.exports: $PropertyType<$Exports<"lodash">, "findLastKey">; +} + +declare module "lodash/forIn" { + declare module.exports: $PropertyType<$Exports<"lodash">, "forIn">; +} + +declare module "lodash/forInRight" { + declare module.exports: $PropertyType<$Exports<"lodash">, "forInRight">; +} + +declare module "lodash/forOwn" { + declare module.exports: $PropertyType<$Exports<"lodash">, "forOwn">; +} + +declare module "lodash/forOwnRight" { + declare module.exports: $PropertyType<$Exports<"lodash">, "forOwnRight">; +} + +declare module "lodash/functions" { + declare module.exports: $PropertyType<$Exports<"lodash">, "functions">; +} + +declare module "lodash/functionsIn" { + declare module.exports: $PropertyType<$Exports<"lodash">, "functionsIn">; +} + +declare module "lodash/get" { + declare module.exports: $PropertyType<$Exports<"lodash">, "get">; +} + +declare module "lodash/has" { + declare module.exports: $PropertyType<$Exports<"lodash">, "has">; +} + +declare module "lodash/hasIn" { + declare module.exports: $PropertyType<$Exports<"lodash">, "hasIn">; +} + +declare module "lodash/invert" { + declare module.exports: $PropertyType<$Exports<"lodash">, "invert">; +} + +declare module "lodash/invertBy" { + declare module.exports: $PropertyType<$Exports<"lodash">, "invertBy">; +} + +declare module "lodash/invoke" { + declare module.exports: $PropertyType<$Exports<"lodash">, "invoke">; +} + +declare module "lodash/keys" { + declare module.exports: $PropertyType<$Exports<"lodash">, "keys">; +} + +declare module "lodash/keysIn" { + declare module.exports: $PropertyType<$Exports<"lodash">, "keysIn">; +} + +declare module "lodash/mapKeys" { + declare module.exports: $PropertyType<$Exports<"lodash">, "mapKeys">; +} + +declare module "lodash/mapValues" { + declare module.exports: $PropertyType<$Exports<"lodash">, "mapValues">; +} + +declare module "lodash/merge" { + declare module.exports: $PropertyType<$Exports<"lodash">, "merge">; +} + +declare module "lodash/mergeWith" { + declare module.exports: $PropertyType<$Exports<"lodash">, "mergeWith">; +} + +declare module "lodash/omit" { + declare module.exports: $PropertyType<$Exports<"lodash">, "omit">; +} + +declare module "lodash/omitBy" { + declare module.exports: $PropertyType<$Exports<"lodash">, "omitBy">; +} + +declare module "lodash/pick" { + declare module.exports: $PropertyType<$Exports<"lodash">, "pick">; +} + +declare module "lodash/pickBy" { + declare module.exports: $PropertyType<$Exports<"lodash">, "pickBy">; +} + +declare module "lodash/result" { + declare module.exports: $PropertyType<$Exports<"lodash">, "result">; +} + +declare module "lodash/set" { + declare module.exports: $PropertyType<$Exports<"lodash">, "set">; +} + +declare module "lodash/setWith" { + declare module.exports: $PropertyType<$Exports<"lodash">, "setWith">; +} + +declare module "lodash/toPairs" { + declare module.exports: $PropertyType<$Exports<"lodash">, "toPairs">; +} + +declare module "lodash/toPairsIn" { + declare module.exports: $PropertyType<$Exports<"lodash">, "toPairsIn">; +} + +declare module "lodash/transform" { + declare module.exports: $PropertyType<$Exports<"lodash">, "transform">; +} + +declare module "lodash/unset" { + declare module.exports: $PropertyType<$Exports<"lodash">, "unset">; +} + +declare module "lodash/update" { + declare module.exports: $PropertyType<$Exports<"lodash">, "update">; +} + +declare module "lodash/updateWith" { + declare module.exports: $PropertyType<$Exports<"lodash">, "updateWith">; +} + +declare module "lodash/values" { + declare module.exports: $PropertyType<$Exports<"lodash">, "values">; +} + +declare module "lodash/valuesIn" { + declare module.exports: $PropertyType<$Exports<"lodash">, "valuesIn">; +} + +declare module "lodash/chain" { + declare module.exports: $PropertyType<$Exports<"lodash">, "chain">; +} + +declare module "lodash/tap" { + declare module.exports: $PropertyType<$Exports<"lodash">, "tap">; +} + +declare module "lodash/thru" { + declare module.exports: $PropertyType<$Exports<"lodash">, "thru">; +} + +declare module "lodash/camelCase" { + declare module.exports: $PropertyType<$Exports<"lodash">, "camelCase">; +} + +declare module "lodash/capitalize" { + declare module.exports: $PropertyType<$Exports<"lodash">, "capitalize">; +} + +declare module "lodash/deburr" { + declare module.exports: $PropertyType<$Exports<"lodash">, "deburr">; +} + +declare module "lodash/endsWith" { + declare module.exports: $PropertyType<$Exports<"lodash">, "endsWith">; +} + +declare module "lodash/escape" { + declare module.exports: $PropertyType<$Exports<"lodash">, "escape">; +} + +declare module "lodash/escapeRegExp" { + declare module.exports: $PropertyType<$Exports<"lodash">, "escapeRegExp">; +} + +declare module "lodash/kebabCase" { + declare module.exports: $PropertyType<$Exports<"lodash">, "kebabCase">; +} + +declare module "lodash/lowerCase" { + declare module.exports: $PropertyType<$Exports<"lodash">, "lowerCase">; +} + +declare module "lodash/lowerFirst" { + declare module.exports: $PropertyType<$Exports<"lodash">, "lowerFirst">; +} + +declare module "lodash/pad" { + declare module.exports: $PropertyType<$Exports<"lodash">, "pad">; +} + +declare module "lodash/padEnd" { + declare module.exports: $PropertyType<$Exports<"lodash">, "padEnd">; +} + +declare module "lodash/padStart" { + declare module.exports: $PropertyType<$Exports<"lodash">, "padStart">; +} + +declare module "lodash/parseInt" { + declare module.exports: $PropertyType<$Exports<"lodash">, "parseInt">; +} + +declare module "lodash/repeat" { + declare module.exports: $PropertyType<$Exports<"lodash">, "repeat">; +} + +declare module "lodash/replace" { + declare module.exports: $PropertyType<$Exports<"lodash">, "replace">; +} + +declare module "lodash/snakeCase" { + declare module.exports: $PropertyType<$Exports<"lodash">, "snakeCase">; +} + +declare module "lodash/split" { + declare module.exports: $PropertyType<$Exports<"lodash">, "split">; +} + +declare module "lodash/startCase" { + declare module.exports: $PropertyType<$Exports<"lodash">, "startCase">; +} + +declare module "lodash/startsWith" { + declare module.exports: $PropertyType<$Exports<"lodash">, "startsWith">; +} + +declare module "lodash/template" { + declare module.exports: $PropertyType<$Exports<"lodash">, "template">; +} + +declare module "lodash/toLower" { + declare module.exports: $PropertyType<$Exports<"lodash">, "toLower">; +} + +declare module "lodash/toUpper" { + declare module.exports: $PropertyType<$Exports<"lodash">, "toUpper">; +} + +declare module "lodash/trim" { + declare module.exports: $PropertyType<$Exports<"lodash">, "trim">; +} + +declare module "lodash/trimEnd" { + declare module.exports: $PropertyType<$Exports<"lodash">, "trimEnd">; +} + +declare module "lodash/trimStart" { + declare module.exports: $PropertyType<$Exports<"lodash">, "trimStart">; +} + +declare module "lodash/truncate" { + declare module.exports: $PropertyType<$Exports<"lodash">, "truncate">; +} + +declare module "lodash/unescape" { + declare module.exports: $PropertyType<$Exports<"lodash">, "unescape">; +} + +declare module "lodash/upperCase" { + declare module.exports: $PropertyType<$Exports<"lodash">, "upperCase">; +} + +declare module "lodash/upperFirst" { + declare module.exports: $PropertyType<$Exports<"lodash">, "upperFirst">; +} + +declare module "lodash/words" { + declare module.exports: $PropertyType<$Exports<"lodash">, "words">; +} + +declare module "lodash/attempt" { + declare module.exports: $PropertyType<$Exports<"lodash">, "attempt">; +} + +declare module "lodash/bindAll" { + declare module.exports: $PropertyType<$Exports<"lodash">, "bindAll">; +} + +declare module "lodash/cond" { + declare module.exports: $PropertyType<$Exports<"lodash">, "cond">; +} + +declare module "lodash/conforms" { + declare module.exports: $PropertyType<$Exports<"lodash">, "conforms">; +} + +declare module "lodash/constant" { + declare module.exports: $PropertyType<$Exports<"lodash">, "constant">; +} + +declare module "lodash/defaultTo" { + declare module.exports: $PropertyType<$Exports<"lodash">, "defaultTo">; +} + +declare module "lodash/flow" { + declare module.exports: $PropertyType<$Exports<"lodash">, "flow">; +} + +declare module "lodash/flowRight" { + declare module.exports: $PropertyType<$Exports<"lodash">, "flowRight">; +} + +declare module "lodash/identity" { + declare module.exports: $PropertyType<$Exports<"lodash">, "identity">; +} + +declare module "lodash/iteratee" { + declare module.exports: $PropertyType<$Exports<"lodash">, "iteratee">; +} + +declare module "lodash/matches" { + declare module.exports: $PropertyType<$Exports<"lodash">, "matches">; +} + +declare module "lodash/matchesProperty" { + declare module.exports: $PropertyType<$Exports<"lodash">, "matchesProperty">; +} + +declare module "lodash/method" { + declare module.exports: $PropertyType<$Exports<"lodash">, "method">; +} + +declare module "lodash/methodOf" { + declare module.exports: $PropertyType<$Exports<"lodash">, "methodOf">; +} + +declare module "lodash/mixin" { + declare module.exports: $PropertyType<$Exports<"lodash">, "mixin">; +} + +declare module "lodash/noConflict" { + declare module.exports: $PropertyType<$Exports<"lodash">, "noConflict">; +} + +declare module "lodash/noop" { + declare module.exports: $PropertyType<$Exports<"lodash">, "noop">; +} + +declare module "lodash/nthArg" { + declare module.exports: $PropertyType<$Exports<"lodash">, "nthArg">; +} + +declare module "lodash/over" { + declare module.exports: $PropertyType<$Exports<"lodash">, "over">; +} + +declare module "lodash/overEvery" { + declare module.exports: $PropertyType<$Exports<"lodash">, "overEvery">; +} + +declare module "lodash/overSome" { + declare module.exports: $PropertyType<$Exports<"lodash">, "overSome">; +} + +declare module "lodash/property" { + declare module.exports: $PropertyType<$Exports<"lodash">, "property">; +} + +declare module "lodash/propertyOf" { + declare module.exports: $PropertyType<$Exports<"lodash">, "propertyOf">; +} + +declare module "lodash/range" { + declare module.exports: $PropertyType<$Exports<"lodash">, "range">; +} + +declare module "lodash/rangeRight" { + declare module.exports: $PropertyType<$Exports<"lodash">, "rangeRight">; +} + +declare module "lodash/runInContext" { + declare module.exports: $PropertyType<$Exports<"lodash">, "runInContext">; +} + +declare module "lodash/stubArray" { + declare module.exports: $PropertyType<$Exports<"lodash">, "stubArray">; +} + +declare module "lodash/stubFalse" { + declare module.exports: $PropertyType<$Exports<"lodash">, "stubFalse">; +} + +declare module "lodash/stubObject" { + declare module.exports: $PropertyType<$Exports<"lodash">, "stubObject">; +} + +declare module "lodash/stubString" { + declare module.exports: $PropertyType<$Exports<"lodash">, "stubString">; +} + +declare module "lodash/stubTrue" { + declare module.exports: $PropertyType<$Exports<"lodash">, "stubTrue">; +} + +declare module "lodash/times" { + declare module.exports: $PropertyType<$Exports<"lodash">, "times">; +} + +declare module "lodash/toPath" { + declare module.exports: $PropertyType<$Exports<"lodash">, "toPath">; +} + +declare module "lodash/uniqueId" { + declare module.exports: $PropertyType<$Exports<"lodash">, "uniqueId">; +} diff --git a/flow-typed/npm/moment_v2.x.x.js b/flow-typed/npm/moment_v2.x.x.js new file mode 100644 index 000000000..39c8febd9 --- /dev/null +++ b/flow-typed/npm/moment_v2.x.x.js @@ -0,0 +1,232 @@ +// flow-typed signature: 92a33b062085f6db1b97258fd12b6beb +// flow-typed version: 5a82c7cd27/moment_v2.x.x/flow_>=v0.28.x + +type moment$MomentOptions = { + y?: number|string, + year?: number|string, + years?: number|string, + M?: number|string, + month?: number|string, + months?: number|string, + d?: number|string, + day?: number|string, + days?: number|string, + date?: number|string, + h?: number|string, + hour?: number|string, + hours?: number|string, + m?: number|string, + minute?: number|string, + minutes?: number|string, + s?: number|string, + second?: number|string, + seconds?: number|string, + ms?: number|string, + millisecond?: number|string, + milliseconds?: number|string, +}; + +type moment$MomentObject = { + years: number, + months: number, + date: number, + hours: number, + minutes: number, + seconds: number, + milliseconds: number, +}; + +type moment$MomentCreationData = { + input: string, + format: string, + locale: Object, + isUTC: bool, + strict: bool, +}; + +type moment$CalendarFormats = { + sameDay?: string, + nextDay?: string, + nextWeek?: string, + lastDay?: string, + lastWeek?: string, + sameElse?: string, +}; + +declare class moment$LocaleData { + months(moment: moment$Moment): string; + monthsShort(moment: moment$Moment): string; + monthsParse(month: string): number; + weekdays(moment: moment$Moment): string; + weekdaysShort(moment: moment$Moment): string; + weekdaysMin(moment: moment$Moment): string; + weekdaysParse(weekDay: string): number; + longDateFormat(dateFormat: string): string; + isPM(date: string): bool; + meridiem(hours: number, minutes: number, isLower: bool): string; + calendar(key: 'sameDay'|'nextDay'|'lastDay'|'nextWeek'|'prevWeek'|'sameElse', moment: moment$Moment): string; + relativeTime(number: number, withoutSuffix: bool, key: 's'|'m'|'mm'|'h'|'hh'|'d'|'dd'|'M'|'MM'|'y'|'yy', isFuture: bool): string; + pastFuture(diff: any, relTime: string): string; + ordinal(number: number): string; + preparse(str: string): any; + postformat(str: string): any; + week(moment: moment$Moment): string; + invalidDate(): string; + firstDayOfWeek(): number; + firstDayOfYear(): number; +} +declare class moment$MomentDuration { + humanize(suffix?: bool): string; + milliseconds(): number; + asMilliseconds(): number; + seconds(): number; + asSeconds(): number; + minutes(): number; + asMinutes(): number; + hours(): number; + asHours(): number; + days(): number; + asDays(): number; + months(): number; + asMonths(): number; + years(): number; + asYears(): number; + add(value: number|moment$MomentDuration|Object, unit?: string): this; + subtract(value: number|moment$MomentDuration|Object, unit?: string): this; + as(unit: string): number; + get(unit: string): number; + toJSON(): string; +} +declare class moment$Moment { + static ISO_8601: string; + static (string?: string, format?: string|Array, locale?: string, strict?: bool): moment$Moment; + static (initDate: ?Object|number|Date|Array|moment$Moment|string): moment$Moment; + static unix(seconds: number): moment$Moment; + static utc(): moment$Moment; + static utc(number: number|Array): moment$Moment; + static utc(str: string, str2?: string|Array, str3?: string): moment$Moment; + static utc(moment: moment$Moment): moment$Moment; + static utc(date: Date): moment$Moment; + static parseZone(rawDate: string): moment$Moment; + isValid(): bool; + invalidAt(): 0|1|2|3|4|5|6; + creationData(): moment$MomentCreationData; + millisecond(number: number): this; + milliseconds(number: number): this; + millisecond(): number; + milliseconds(): number; + second(number: number): this; + seconds(number: number): this; + second(): number; + seconds(): number; + minute(number: number): this; + minutes(number: number): this; + minute(): number; + minutes(): number; + hour(number: number): this; + hours(number: number): this; + hour(): number; + hours(): number; + date(number: number): this; + dates(number: number): this; + date(): number; + dates(): number; + day(day: number|string): this; + days(day: number|string): this; + day(): number; + days(): number; + weekday(number: number): this; + weekday(): number; + isoWeekday(number: number): this; + isoWeekday(): number; + dayOfYear(number: number): this; + dayOfYear(): number; + week(number: number): this; + weeks(number: number): this; + week(): number; + weeks(): number; + isoWeek(number: number): this; + isoWeeks(number: number): this; + isoWeek(): number; + isoWeeks(): number; + month(number: number): this; + months(number: number): this; + month(): number; + months(): number; + quarter(number: number): this; + quarter(): number; + year(number: number): this; + years(number: number): this; + year(): number; + years(): number; + weekYear(number: number): this; + weekYear(): number; + isoWeekYear(number: number): this; + isoWeekYear(): number; + weeksInYear(): number; + isoWeeksInYear(): number; + get(string: string): number; + set(unit: string, value: number): this; + set(options: { unit: string, value: number }): this; + static max(...dates: Array): moment$Moment; + static max(dates: Array): moment$Moment; + static min(...dates: Array): moment$Moment; + static min(dates: Array): moment$Moment; + add(value: number|moment$MomentDuration|moment$Moment|Object, unit?: string): this; + subtract(value: number|moment$MomentDuration|moment$Moment|string, unit?: string): this; + startOf(unit: string): this; + endOf(unit: string): this; + local(): void; + utc(): void; + utcOffset(offset?: number|string): void; + format(format?: string): string; + fromNow(removeSuffix?: bool): string; + from(value: moment$Moment|string|number|Date|Array, removePrefix?: bool): string; + toNow(removePrefix?: bool): string; + to(value: moment$Moment|string|number|Date|Array, removePrefix?: bool): string; + calendar(refTime?: any, formats?: moment$CalendarFormats): string; + diff(date: moment$Moment|string|number|Date|Array, format?: string, floating?: bool): number; + valueOf(): number; + unix(): number; + daysInMonth(): number; + toDate(): Date; + toArray(): Array; + toJSON(): string; + toISOString(): string; + toObject(): moment$MomentObject; + isBefore(date: moment$Moment|string|number|Date|Array): bool; + isSame(date: moment$Moment|string|number|Date|Array): bool; + isAfter(date: moment$Moment|string|number|Date|Array): bool; + isSameOrBefore(date: moment$Moment|string|number|Date|Array): bool; + isSameOrAfter(date: moment$Moment|string|number|Date|Array): bool; + isBetween(date: moment$Moment|string|number|Date|Array): bool; + isDST(): bool; + isDSTShifted(): bool; + isLeapYear(): bool; + clone(): moment$Moment; + static isMoment(obj: any): bool; + static isDatE(obj: any): bool; + static locale(locale: string, localeData?: Object): void; + static locale(locales: Array): void; + locale(locale: string, customization?: Object|null): void; + locale(): string; + static months(): Array; + static monthsShort(): Array; + static weekdays(): Array; + static weekdaysShort(): Array; + static weekdaysMin(): Array; + static months(): string; + static monthsShort(): string; + static weekdays(): string; + static weekdaysShort(): string; + static weekdaysMin(): string; + static localeData(key?: string): moment$LocaleData; + static duration(value: number|Object|string, unit?: string): moment$MomentDuration; + static isDuration(obj: any): bool; + static normalizeUnits(unit: string): string; + static invalid(object: any): moment$Moment; +} + +declare module 'moment' { + declare module.exports: Class; +} diff --git a/flow-typed/npm/react-router_v4.x.x.js b/flow-typed/npm/react-router_v4.x.x.js new file mode 100644 index 000000000..68505200d --- /dev/null +++ b/flow-typed/npm/react-router_v4.x.x.js @@ -0,0 +1,125 @@ +// flow-typed signature: 1e6728f0a649edac3689d6e2db7487a7 +// flow-typed version: 01716df816/react-router_v4.x.x/flow_>=v0.53.x + +declare module "react-router" { + // NOTE: many of these are re-exported by react-router-dom and + // react-router-native, so when making changes, please be sure to update those + // as well. + declare export type Location = { + pathname: string, + search: string, + hash: string, + state?: any, + key?: string + }; + + declare export type LocationShape = { + pathname?: string, + search?: string, + hash?: string, + state?: any + }; + + declare export type HistoryAction = "PUSH" | "REPLACE" | "POP"; + + declare export type RouterHistory = { + length: number, + location: Location, + action: HistoryAction, + listen( + callback: (location: Location, action: HistoryAction) => void + ): () => void, + push(path: string | LocationShape, state?: any): void, + replace(path: string | LocationShape, state?: any): void, + go(n: number): void, + goBack(): void, + goForward(): void, + canGo?: (n: number) => boolean, + block( + callback: (location: Location, action: HistoryAction) => boolean + ): void, + // createMemoryHistory + index?: number, + entries?: Array + }; + + declare export type Match = { + params: { [key: string]: ?string }, + isExact: boolean, + path: string, + url: string + }; + + declare export type ContextRouter = {| + history: RouterHistory, + location: Location, + match: Match + |}; + + declare export type GetUserConfirmation = ( + message: string, + callback: (confirmed: boolean) => void + ) => void; + + declare type StaticRouterContext = { + url?: string + }; + + declare export class StaticRouter extends React$Component<{ + basename?: string, + location?: string | Location, + context: StaticRouterContext, + children?: React$Node + }> {} + + declare export class MemoryRouter extends React$Component<{ + initialEntries?: Array, + initialIndex?: number, + getUserConfirmation?: GetUserConfirmation, + keyLength?: number, + children?: React$Node + }> {} + + declare export class Router extends React$Component<{ + history: RouterHistory, + children?: React$Node + }> {} + + declare export class Prompt extends React$Component<{ + message: string | ((location: Location) => string | true), + when?: boolean + }> {} + + declare export class Redirect extends React$Component<{ + to: string | LocationShape, + push?: boolean + }> {} + + declare export class Route extends React$Component<{ + component?: React$ComponentType<*>, + render?: (router: ContextRouter) => React$Node, + children?: React$ComponentType | React$Node, + path?: string, + exact?: boolean, + strict?: boolean + }> {} + + declare export class Switch extends React$Component<{ + children?: React$Node + }> {} + + declare export function withRouter

( + Component: React$ComponentType<{| ...ContextRouter, ...P |}> + ): React$ComponentType

; + + declare type MatchPathOptions = { + path?: string, + exact?: boolean, + strict?: boolean, + sensitive?: boolean + }; + declare export function matchPath( + pathname: string, + options?: MatchPathOptions | string + ): null | Match; +} diff --git a/flow-typed/npm/redux-asserts_v0.x.x.js b/flow-typed/npm/redux-asserts_v0.x.x.js new file mode 100644 index 000000000..0c2ec4daa --- /dev/null +++ b/flow-typed/npm/redux-asserts_v0.x.x.js @@ -0,0 +1,19 @@ +// Not flow-typed, this was created manually +import type { Action, Dispatch, Reducer } from 'redux'; + +declare module 'redux-asserts' { + declare type State = any; + declare type StateFunc = ((state: State) => State); + + declare type TestStore = { + dispatch: Dispatch<*>, + getState: () => State, + subscribe: (listener: () => void) => () => void, + replaceReducer: (reducer: Reducer) => void, + createListenForActions: (stateFunc?: StateFunc) => ((actions: Array, () => void) => Promise), + createDispatchThen: (stateFunc?: StateFunc) => ( + (action: Action, expectedActions: Array) => Promise + ) + } + declare export default function configureTestStore(reducerFunc?: (state: State) => State): TestStore; +} diff --git a/flow-typed/npm/redux_v3.x.x.js b/flow-typed/npm/redux_v3.x.x.js new file mode 100644 index 000000000..08c122efb --- /dev/null +++ b/flow-typed/npm/redux_v3.x.x.js @@ -0,0 +1,109 @@ +// flow-typed signature: 33b83b6284653250e74578cf4dbe6124 +// flow-typed version: e282e4128f/redux_v3.x.x/flow_>=v0.33.x + +declare module 'redux' { + + /* + + S = State + A = Action + D = Dispatch + + */ + + declare export type DispatchAPI = (action: A) => A; + declare export type Dispatch }> = DispatchAPI; + + declare export type MiddlewareAPI> = { + dispatch: D; + getState(): S; + }; + + declare export type Store> = { + // rewrite MiddlewareAPI members in order to get nicer error messages (intersections produce long messages) + dispatch: D; + getState(): S; + subscribe(listener: () => void): () => void; + replaceReducer(nextReducer: Reducer): void + }; + + declare export type Reducer = (state: S, action: A) => S; + + declare export type CombinedReducer = (state: $Shape & {} | void, action: A) => S; + + declare export type Middleware> = + (api: MiddlewareAPI) => + (next: D) => D; + + declare export type StoreCreator> = { + (reducer: Reducer, enhancer?: StoreEnhancer): Store; + (reducer: Reducer, preloadedState: S, enhancer?: StoreEnhancer): Store; + }; + + declare export type StoreEnhancer> = (next: StoreCreator) => StoreCreator; + + declare export function createStore(reducer: Reducer, enhancer?: StoreEnhancer): Store; + declare export function createStore(reducer: Reducer, preloadedState: S, enhancer?: StoreEnhancer): Store; + + declare export function applyMiddleware(...middlewares: Array>): StoreEnhancer; + + declare export type ActionCreator = (...args: Array) => A; + declare export type ActionCreators = { [key: K]: ActionCreator }; + + declare export function bindActionCreators, D: DispatchAPI>(actionCreator: C, dispatch: D): C; + declare export function bindActionCreators, D: DispatchAPI>(actionCreators: C, dispatch: D): C; + + declare export function combineReducers(reducers: O): CombinedReducer<$ObjMap(r: Reducer) => S>, A>; + + declare export function compose(ab: (a: A) => B): (a: A) => B + declare export function compose( + bc: (b: B) => C, + ab: (a: A) => B + ): (a: A) => C + declare export function compose( + cd: (c: C) => D, + bc: (b: B) => C, + ab: (a: A) => B + ): (a: A) => D + declare export function compose( + de: (d: D) => E, + cd: (c: C) => D, + bc: (b: B) => C, + ab: (a: A) => B + ): (a: A) => E + declare export function compose( + ef: (e: E) => F, + de: (d: D) => E, + cd: (c: C) => D, + bc: (b: B) => C, + ab: (a: A) => B + ): (a: A) => F + declare export function compose( + fg: (f: F) => G, + ef: (e: E) => F, + de: (d: D) => E, + cd: (c: C) => D, + bc: (b: B) => C, + ab: (a: A) => B + ): (a: A) => G + declare export function compose( + gh: (g: G) => H, + fg: (f: F) => G, + ef: (e: E) => F, + de: (d: D) => E, + cd: (c: C) => D, + bc: (b: B) => C, + ab: (a: A) => B + ): (a: A) => H + declare export function compose( + hi: (h: H) => I, + gh: (g: G) => H, + fg: (f: F) => G, + ef: (e: E) => F, + de: (d: D) => E, + cd: (c: C) => D, + bc: (b: B) => C, + ab: (a: A) => B + ): (a: A) => I + +} diff --git a/hot-reload-dev-server.js b/hot-reload-dev-server.js new file mode 100644 index 000000000..09ffc0aab --- /dev/null +++ b/hot-reload-dev-server.js @@ -0,0 +1,35 @@ +var path = require('path'); +var webpack = require('webpack'); +var express = require('express'); +var devMiddleware = require('webpack-dev-middleware'); +var hotMiddleware = require('webpack-hot-middleware'); +var minimist = require('minimist'); + +var makeDevConfig = require('./webpack.config.dev'); + +const { host, port } = minimist(process.argv.slice(2)); + +const config = makeDevConfig(host, port); + +const app = express(); + +const compiler = webpack(config); + +app.use(function(req, res, next) { + res.header('Access-Control-Allow-Origin', '*'); + next(); +}); + +app.use(devMiddleware(compiler, { + publicPath: "/" +})); + +app.use(hotMiddleware(compiler)); + +app.listen(8052, (err) => { + if (err) { + return console.error(err) + } + console.log(`listening at http://${host}:${port}`); + console.log('building...'); +}); diff --git a/manage.py b/manage.py new file mode 100755 index 000000000..1966442f3 --- /dev/null +++ b/manage.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python +""" +manage.py +""" + +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mitxpro.settings") + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) diff --git a/mitxpro/__init__.py b/mitxpro/__init__.py new file mode 100644 index 000000000..df2d3ff1a --- /dev/null +++ b/mitxpro/__init__.py @@ -0,0 +1,3 @@ +"""Set the default AppConfig so we can validate settings""" + +default_app_config = "mitxpro.apps.RootConfig" diff --git a/mitxpro/apps.py b/mitxpro/apps.py new file mode 100644 index 000000000..e93613588 --- /dev/null +++ b/mitxpro/apps.py @@ -0,0 +1,26 @@ +""" +Django app +""" +from django.apps import AppConfig +from django.conf import settings +from django.core.exceptions import ImproperlyConfigured + + +class RootConfig(AppConfig): + """AppConfig for this project""" + + name = "mitxpro" + + def ready(self): + missing_settings = [ + setting_name + for setting_name in settings.MANDATORY_SETTINGS + if getattr(settings, setting_name, None) in (None, "") + ] + + if missing_settings: + raise ImproperlyConfigured( + "The following settings are missing: {}".format( + ", ".join(missing_settings) + ) + ) diff --git a/mitxpro/celery.py b/mitxpro/celery.py new file mode 100644 index 000000000..156c22b54 --- /dev/null +++ b/mitxpro/celery.py @@ -0,0 +1,34 @@ +""" +As described in +http://celery.readthedocs.org/en/latest/django/first-steps-with-django.html +""" + +import logging +import os + +from celery import Celery +from raven import Client +from raven.contrib.celery import register_logger_signal, register_signal + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mitxpro.settings") + +from django.conf import settings # noqa pylint: disable=wrong-import-position + +log = logging.getLogger(__name__) + + +client = Client(**settings.RAVEN_CONFIG) + +register_logger_signal(client, loglevel=settings.SENTRY_LOG_LEVEL) + +# The register_signal function can also take an optional argument +# `ignore_expected` which causes exception classes specified in Task.throws +# to be ignored +register_signal(client, ignore_expected=True) + +app = Celery("mitxpro") + +# Using a string here means the worker will not have to +# pickle the object when using Windows. +app.config_from_object("django.conf:settings", namespace="CELERY") +app.autodiscover_tasks() diff --git a/mitxpro/envs.py b/mitxpro/envs.py new file mode 100644 index 000000000..fccf328cc --- /dev/null +++ b/mitxpro/envs.py @@ -0,0 +1,101 @@ +"""Functions reading and parsing environment variables""" +import os + +from django.core.exceptions import ImproperlyConfigured + + +class EnvironmentVariableParseException(ImproperlyConfigured): + """Environment variable was not parsed correctly""" + + +def get_string(name, default): + """ + Get an environment variable as a string. + + Args: + name (str): An environment variable name + default (str): The default value to use if the environment variable doesn't exist. + + Returns: + str: + The environment variable value, or the default + """ + return os.environ.get(name, default) + + +def get_bool(name, default): + """ + Get an environment variable as a boolean. + + Args: + name (str): An environment variable name + default (bool): The default value to use if the environment variable doesn't exist. + + Returns: + bool: + The environment variable value parsed as a bool + """ + value = os.environ.get(name) + if value is None: + return default + + parsed_value = value.lower() + if parsed_value == "true": + return True + elif parsed_value == "false": + return False + + raise EnvironmentVariableParseException( + "Expected value in {name}={value} to be a boolean".format( + name=name, value=value + ) + ) + + +def get_int(name, default): + """ + Get an environment variable as an int. + + Args: + name (str): An environment variable name + default (int): The default value to use if the environment variable doesn't exist. + + Returns: + int: + The environment variable value parsed as an int + """ + value = os.environ.get(name) + if value is None: + return default + + try: + parsed_value = int(value) + except ValueError as ex: + raise EnvironmentVariableParseException( + "Expected value in {name}={value} to be an int".format( + name=name, value=value + ) + ) from ex + + return parsed_value + + +def get_any(name, default): + """ + Get an environment variable as a bool, int, or a string. + + Args: + name (str): An environment variable name + default (any): The default value to use if the environment variable doesn't exist. + + Returns: + any: + The environment variable value parsed as a bool, int, or a string + """ + try: + return get_bool(name, default) + except EnvironmentVariableParseException: + try: + return get_int(name, default) + except EnvironmentVariableParseException: + return get_string(name, default) diff --git a/mitxpro/envs_test.py b/mitxpro/envs_test.py new file mode 100644 index 000000000..99841dc29 --- /dev/null +++ b/mitxpro/envs_test.py @@ -0,0 +1,104 @@ +"""Tests for environment variable parsing functions""" +from unittest.mock import patch + +import pytest + +from mitxpro.envs import ( + EnvironmentVariableParseException, + get_any, + get_bool, + get_int, + get_string, +) + + +FAKE_ENVIRONS = { + "true": "True", + "false": "False", + "positive": "123", + "negative": "-456", + "zero": "0", + "float": "1.1", + "expression": "123-456", + "none": "None", + "string": "a b c d e f g", + "list_of_int": "[3,4,5]", + "list_of_str": '["x", "y", \'z\']', +} + + +def test_get_any(): + """ + get_any should parse an environment variable into a bool, int, or a string + """ + expected = { + "true": True, + "false": False, + "positive": 123, + "negative": -456, + "zero": 0, + "float": "1.1", + "expression": "123-456", + "none": "None", + "string": "a b c d e f g", + "list_of_int": "[3,4,5]", + "list_of_str": '["x", "y", \'z\']', + } + with patch("mitxpro.envs.os", environ=FAKE_ENVIRONS): + for key, value in expected.items(): + assert get_any(key, "default") == value + assert get_any("missing", "default") == "default" + + +def test_get_string(): + """ + get_string should get the string from the environment variable + """ + with patch("mitxpro.envs.os", environ=FAKE_ENVIRONS): + for key, value in FAKE_ENVIRONS.items(): + assert get_string(key, "default") == value + assert get_string("missing", "default") == "default" + assert get_string("missing", "default") == "default" + + +def test_get_int(): + """ + get_int should get the int from the environment variable, or raise an exception if it's not parseable as an int + """ + with patch("mitxpro.envs.os", environ=FAKE_ENVIRONS): + assert get_int("positive", 1234) == 123 + assert get_int("negative", 1234) == -456 + assert get_int("zero", 1234) == 0 + + for key, value in FAKE_ENVIRONS.items(): + if key not in ("positive", "negative", "zero"): + with pytest.raises(EnvironmentVariableParseException) as ex: + get_int(key, 1234) + assert ex.value.args[ + 0 + ] == "Expected value in {key}={value} to be an int".format( + key=key, value=value + ) + + assert get_int("missing", "default") == "default" + + +def test_get_bool(): + """ + get_bool should get the bool from the environment variable, or raise an exception if it's not parseable as a bool + """ + with patch("mitxpro.envs.os", environ=FAKE_ENVIRONS): + assert get_bool("true", 1234) is True + assert get_bool("false", 1234) is False + + for key, value in FAKE_ENVIRONS.items(): + if key not in ("true", "false"): + with pytest.raises(EnvironmentVariableParseException) as ex: + get_bool(key, 1234) + assert ex.value.args[ + 0 + ] == "Expected value in {key}={value} to be a boolean".format( + key=key, value=value + ) + + assert get_int("missing", "default") == "default" diff --git a/mitxpro/settings.py b/mitxpro/settings.py new file mode 100644 index 000000000..f0d9fc7ef --- /dev/null +++ b/mitxpro/settings.py @@ -0,0 +1,342 @@ +""" +Django settings for mitxpro. +""" +import logging +import os +import platform +from urllib.parse import urljoin + +import dj_database_url +from django.core.exceptions import ImproperlyConfigured + +from mitxpro.envs import get_any, get_bool, get_int, get_string + +VERSION = "0.0.0" + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = get_string("SECRET_KEY", None) + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = get_bool("DEBUG", False) + +ALLOWED_HOSTS = ["*"] + +SECURE_SSL_REDIRECT = get_bool("MITXPRO_SECURE_SSL_REDIRECT", True) + + +WEBPACK_LOADER = { + "DEFAULT": { + "CACHE": not DEBUG, + "BUNDLE_DIR_NAME": "bundles/", + "STATS_FILE": os.path.join(BASE_DIR, "webpack-stats.json"), + "POLL_INTERVAL": 0.1, + "TIMEOUT": None, + "IGNORE": [r".+\.hot-update\.+", r".+\.js\.map"], + } +} + + +# Application definition + +INSTALLED_APPS = ( + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "server_status", + "raven.contrib.django.raven_compat", + # Put our apps after this point + "mitxpro", +) + +DISABLE_WEBPACK_LOADER_STATS = get_bool("DISABLE_WEBPACK_LOADER_STATS", False) +if not DISABLE_WEBPACK_LOADER_STATS: + INSTALLED_APPS += ("webpack_loader",) + +MIDDLEWARE = ( + "django.middleware.security.SecurityMiddleware", + "raven.contrib.django.raven_compat.middleware.SentryResponseErrorIdMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", +) + +# enable the nplusone profiler only in debug mode +if DEBUG: + INSTALLED_APPS += ("nplusone.ext.django",) + MIDDLEWARE += ("nplusone.ext.django.NPlusOneMiddleware",) + +SESSION_ENGINE = "django.contrib.sessions.backends.signed_cookies" + +LOGIN_REDIRECT_URL = "/" +LOGIN_URL = "/" +LOGIN_ERROR_URL = "/" + +ROOT_URLCONF = "mitxpro.urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ] + }, + } +] + +WSGI_APPLICATION = "mitxpro.wsgi.application" + + +# Database +# https://docs.djangoproject.com/en/2.0/ref/settings/#databases +DEFAULT_DATABASE_CONFIG = dj_database_url.parse( + get_string( + "DATABASE_URL", "sqlite:///{0}".format(os.path.join(BASE_DIR, "db.sqlite3")) + ) +) +DEFAULT_DATABASE_CONFIG["CONN_MAX_AGE"] = get_int("MITXPRO_DB_CONN_MAX_AGE", 0) + +if get_bool("MITXPRO_DB_DISABLE_SSL", False): + DEFAULT_DATABASE_CONFIG["OPTIONS"] = {} +else: + DEFAULT_DATABASE_CONFIG["OPTIONS"] = {"sslmode": "require"} + +DATABASES = {"default": DEFAULT_DATABASE_CONFIG} + +# Internationalization +# https://docs.djangoproject.com/en/2.0/topics/i18n/ + +LANGUAGE_CODE = "en-us" + +TIME_ZONE = "UTC" + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/1.8/howto/static-files/ + +# Serve static files with dj-static +STATIC_URL = "/static/" +CLOUDFRONT_DIST = get_string("CLOUDFRONT_DIST", None) +if CLOUDFRONT_DIST: + STATIC_URL = urljoin( + "https://{dist}.cloudfront.net".format(dist=CLOUDFRONT_DIST), STATIC_URL + ) + +STATIC_ROOT = "staticfiles" +STATICFILES_DIRS = (os.path.join(BASE_DIR, "static"),) + +# Request files from the webpack dev server +USE_WEBPACK_DEV_SERVER = get_bool("MITXPRO_USE_WEBPACK_DEV_SERVER", False) +WEBPACK_DEV_SERVER_HOST = get_string("WEBPACK_DEV_SERVER_HOST", "") +WEBPACK_DEV_SERVER_PORT = get_int("WEBPACK_DEV_SERVER_PORT", 8052) + +# Important to define this so DEBUG works properly +INTERNAL_IPS = (get_string("HOST_IP", "127.0.0.1"),) + +# Configure e-mail settings +EMAIL_BACKEND = get_string( + "MITXPRO_EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend" +) +EMAIL_HOST = get_string("MITXPRO_EMAIL_HOST", "localhost") +EMAIL_PORT = get_int("MITXPRO_EMAIL_PORT", 25) +EMAIL_HOST_USER = get_string("MITXPRO_EMAIL_USER", "") +EMAIL_HOST_PASSWORD = get_string("MITXPRO_EMAIL_PASSWORD", "") +EMAIL_USE_TLS = get_bool("MITXPRO_EMAIL_TLS", False) +EMAIL_SUPPORT = get_string("MITXPRO_SUPPORT_EMAIL", "support@example.com") +DEFAULT_FROM_EMAIL = get_string("MITXPRO_FROM_EMAIL", "webmaster@localhost") + +MAILGUN_SENDER_DOMAIN = get_string("MAILGUN_SENDER_DOMAIN", None) +MAILGUN_KEY = get_string("MAILGUN_KEY", None) +MAILGUN_BATCH_CHUNK_SIZE = get_int("MAILGUN_BATCH_CHUNK_SIZE", 1000) +MAILGUN_RECIPIENT_OVERRIDE = get_string("MAILGUN_RECIPIENT_OVERRIDE", None) +MAILGUN_FROM_EMAIL = get_string("MAILGUN_FROM_EMAIL", "no-reply@example.com") +MAILGUN_BCC_TO_EMAIL = get_string("MAILGUN_BCC_TO_EMAIL", "no-reply@example.com") + + +# e-mail configurable admins +ADMIN_EMAIL = get_string("MITXPRO_ADMIN_EMAIL", "") +if ADMIN_EMAIL != "": + ADMINS = (("Admins", ADMIN_EMAIL),) +else: + ADMINS = () + +# Logging configuration +LOG_LEVEL = get_string("MITXPRO_LOG_LEVEL", "INFO") +DJANGO_LOG_LEVEL = get_string("DJANGO_LOG_LEVEL", "INFO") +SENTRY_LOG_LEVEL = get_string("SENTRY_LOG_LEVEL", "ERROR") + +# For logging to a remote syslog host +LOG_HOST = get_string("MITXPRO_LOG_HOST", "localhost") +LOG_HOST_PORT = get_int("MITXPRO_LOG_HOST_PORT", 514) + +HOSTNAME = platform.node().split(".")[0] + +# nplusone profiler logger configuration +NPLUSONE_LOGGER = logging.getLogger("nplusone") +NPLUSONE_LOG_LEVEL = logging.ERROR + +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "filters": {"require_debug_false": {"()": "django.utils.log.RequireDebugFalse"}}, + "formatters": { + "verbose": { + "format": ( + "[%(asctime)s] %(levelname)s %(process)d [%(name)s] " + "%(filename)s:%(lineno)d - " + "[{hostname}] - %(message)s" + ).format(hostname=HOSTNAME), + "datefmt": "%Y-%m-%d %H:%M:%S", + } + }, + "handlers": { + "console": { + "level": "DEBUG", + "class": "logging.StreamHandler", + "formatter": "verbose", + }, + "syslog": { + "level": LOG_LEVEL, + "class": "logging.handlers.SysLogHandler", + "facility": "local7", + "formatter": "verbose", + "address": (LOG_HOST, LOG_HOST_PORT), + }, + "mail_admins": { + "level": "ERROR", + "filters": ["require_debug_false"], + "class": "django.utils.log.AdminEmailHandler", + }, + "sentry": { + "level": SENTRY_LOG_LEVEL, + "class": "raven.contrib.django.raven_compat.handlers.SentryHandler", + "formatter": "verbose", + }, + }, + "loggers": { + "django": { + "propagate": True, + "level": DJANGO_LOG_LEVEL, + "handlers": ["console", "syslog", "sentry"], + }, + "django.request": { + "handlers": ["mail_admins"], + "level": DJANGO_LOG_LEVEL, + "propagate": True, + }, + "raven": {"level": SENTRY_LOG_LEVEL, "handlers": []}, + "nplusone": {"handlers": ["console"], "level": "ERROR"}, + }, + "root": {"handlers": ["console", "syslog", "sentry"], "level": LOG_LEVEL}, +} + +# Sentry +ENVIRONMENT = get_string("MITXPRO_ENVIRONMENT", "dev") +SENTRY_CLIENT = "raven.contrib.django.raven_compat.DjangoClient" +RAVEN_CONFIG = { + "dsn": get_string("SENTRY_DSN", ""), + "environment": ENVIRONMENT, + "release": VERSION, +} + +# server-status +STATUS_TOKEN = get_string("STATUS_TOKEN", "") +HEALTH_CHECK = ["CELERY", "REDIS", "POSTGRES"] + +GA_TRACKING_ID = get_string("GA_TRACKING_ID", "") +REACT_GA_DEBUG = get_bool("REACT_GA_DEBUG", False) + +MEDIA_ROOT = get_string("MEDIA_ROOT", "/var/media/") +MEDIA_URL = "/media/" +MITXPRO_USE_S3 = get_bool("MITXPRO_USE_S3", False) +AWS_ACCESS_KEY_ID = get_string("AWS_ACCESS_KEY_ID", False) +AWS_SECRET_ACCESS_KEY = get_string("AWS_SECRET_ACCESS_KEY", False) +AWS_STORAGE_BUCKET_NAME = get_string("AWS_STORAGE_BUCKET_NAME", False) +AWS_QUERYSTRING_AUTH = get_string("AWS_QUERYSTRING_AUTH", False) +# Provide nice validation of the configuration +if MITXPRO_USE_S3 and ( + not AWS_ACCESS_KEY_ID or not AWS_SECRET_ACCESS_KEY or not AWS_STORAGE_BUCKET_NAME +): + raise ImproperlyConfigured( + "You have enabled S3 support, but are missing one of " + "AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, or " + "AWS_STORAGE_BUCKET_NAME" + ) +if MITXPRO_USE_S3: + if CLOUDFRONT_DIST: + AWS_S3_CUSTOM_DOMAIN = "{dist}.cloudfront.net".format(dist=CLOUDFRONT_DIST) + DEFAULT_FILE_STORAGE = "storages.backends.s3boto.S3BotoStorage" +else: + # by default use django.core.files.storage.FileSystemStorage with + # overwrite feature + DEFAULT_FILE_STORAGE = "storages.backends.overwrite.OverwriteStorage" + +# Celery +USE_CELERY = True +CELERY_BROKER_URL = get_string("CELERY_BROKER_URL", get_string("REDISCLOUD_URL", None)) +CELERY_RESULT_BACKEND = get_string( + "CELERY_RESULT_BACKEND", get_string("REDISCLOUD_URL", None) +) +CELERY_TASK_ALWAYS_EAGER = get_bool("CELERY_TASK_ALWAYS_EAGER", False) +CELERY_TASK_EAGER_PROPAGATES = get_bool("CELERY_TASK_EAGER_PROPAGATES", True) + +CELERY_TASK_SERIALIZER = "json" +CELERY_RESULT_SERIALIZER = "json" +CELERY_ACCEPT_CONTENT = ["json"] +CELERY_TIMEZONE = "UTC" + + +# django cache back-ends +CACHES = { + "default": { + "BACKEND": "django.core.cache.backends.locmem.LocMemCache", + "LOCATION": "local-in-memory-cache", + }, + "redis": { + "BACKEND": "django_redis.cache.RedisCache", + "LOCATION": CELERY_BROKER_URL, + "OPTIONS": {"CLIENT_CLASS": "django_redis.client.DefaultClient"}, + }, +} + + +# features flags +def get_all_config_keys(): + """Returns all the configuration keys from both environment and configuration files""" + return list(os.environ.keys()) + + +MITXPRO_FEATURES_PREFIX = get_string("MITXPRO_FEATURES_PREFIX", "FEATURE_") +FEATURES = { + key[len(MITXPRO_FEATURES_PREFIX) :]: get_any(key, None) + for key in get_all_config_keys() + if key.startswith(MITXPRO_FEATURES_PREFIX) +} + +# django debug toolbar only in debug mode +if DEBUG: + INSTALLED_APPS += ("debug_toolbar",) + # it needs to be enabled before other middlewares + MIDDLEWARE = ("debug_toolbar.middleware.DebugToolbarMiddleware",) + MIDDLEWARE + +MANDATORY_SETTINGS = ["MAILGUN_SENDER_DOMAIN", "MAILGUN_KEY"] diff --git a/mitxpro/settings_test.py b/mitxpro/settings_test.py new file mode 100644 index 000000000..89d056bfe --- /dev/null +++ b/mitxpro/settings_test.py @@ -0,0 +1,129 @@ +""" +Validate that our settings functions work +""" + +import importlib +import sys +from unittest import mock + +from django.conf import settings +from django.core import mail +from django.core.exceptions import ImproperlyConfigured +from django.test import TestCase +import semantic_version + + +REQUIRED_SETTINGS = { + "MAILGUN_SENDER_DOMAIN": "mailgun.fake.domain", + "MAILGUN_KEY": "fake_mailgun_key", +} + + +class TestSettings(TestCase): + """Validate that settings work as expected.""" + + def reload_settings(self): + """ + Reload settings module with cleanup to restore it. + + Returns: + dict: dictionary of the newly reloaded settings ``vars`` + """ + importlib.reload(sys.modules["mitxpro.settings"]) + # Restore settings to original settings after test + self.addCleanup(importlib.reload, sys.modules["mitxpro.settings"]) + return vars(sys.modules["mitxpro.settings"]) + + def test_s3_settings(self): + """Verify that we enable and configure S3 with a variable""" + # Unset, we don't do S3 + with mock.patch.dict( + "os.environ", {**REQUIRED_SETTINGS, "MITXPRO_USE_S3": "False"}, clear=True + ): + settings_vars = self.reload_settings() + self.assertNotEqual( + settings_vars.get("DEFAULT_FILE_STORAGE"), + "storages.backends.s3boto.S3BotoStorage", + ) + + with self.assertRaises(ImproperlyConfigured): + with mock.patch.dict("os.environ", {"MITXPRO_USE_S3": "True"}, clear=True): + self.reload_settings() + + # Verify it all works with it enabled and configured 'properly' + with mock.patch.dict( + "os.environ", + { + **REQUIRED_SETTINGS, + "MITXPRO_USE_S3": "True", + "AWS_ACCESS_KEY_ID": "1", + "AWS_SECRET_ACCESS_KEY": "2", + "AWS_STORAGE_BUCKET_NAME": "3", + }, + clear=True, + ): + settings_vars = self.reload_settings() + self.assertEqual( + settings_vars.get("DEFAULT_FILE_STORAGE"), + "storages.backends.s3boto.S3BotoStorage", + ) + + def test_admin_settings(self): + """Verify that we configure email with environment variable""" + + with mock.patch.dict( + "os.environ", {**REQUIRED_SETTINGS, "MITXPRO_ADMIN_EMAIL": ""}, clear=True + ): + settings_vars = self.reload_settings() + self.assertFalse(settings_vars.get("ADMINS", False)) + + test_admin_email = "cuddle_bunnies@example.com" + with mock.patch.dict( + "os.environ", + {**REQUIRED_SETTINGS, "MITXPRO_ADMIN_EMAIL": test_admin_email}, + clear=True, + ): + settings_vars = self.reload_settings() + self.assertEqual((("Admins", test_admin_email),), settings_vars["ADMINS"]) + # Manually set ADMIN to our test setting and verify e-mail + # goes where we expect + settings.ADMINS = (("Admins", test_admin_email),) + mail.mail_admins("Test", "message") + self.assertIn(test_admin_email, mail.outbox[0].to) + + def test_db_ssl_enable(self): + """Verify that we can enable/disable database SSL with a var""" + + # Check default state is SSL on + with mock.patch.dict("os.environ", REQUIRED_SETTINGS, clear=True): + settings_vars = self.reload_settings() + self.assertEqual( + settings_vars["DATABASES"]["default"]["OPTIONS"], {"sslmode": "require"} + ) + + # Check enabling the setting explicitly + with mock.patch.dict( + "os.environ", + {**REQUIRED_SETTINGS, "MITXPRO_DB_DISABLE_SSL": "True"}, + clear=True, + ): + settings_vars = self.reload_settings() + self.assertEqual(settings_vars["DATABASES"]["default"]["OPTIONS"], {}) + + # Disable it + with mock.patch.dict( + "os.environ", + {**REQUIRED_SETTINGS, "MITXPRO_DB_DISABLE_SSL": "False"}, + clear=True, + ): + settings_vars = self.reload_settings() + self.assertEqual( + settings_vars["DATABASES"]["default"]["OPTIONS"], {"sslmode": "require"} + ) + + @staticmethod + def test_semantic_version(): + """ + Verify that we have a semantic compatible version. + """ + semantic_version.Version(settings.VERSION) diff --git a/mitxpro/templates/base.html b/mitxpro/templates/base.html new file mode 100644 index 000000000..788d754da --- /dev/null +++ b/mitxpro/templates/base.html @@ -0,0 +1,28 @@ + + + + {% spaceless %} + {% load static %} + + + {% load raven %} + + {% load raven %} + + {% load render_bundle %} + {% render_bundle 'common' %} + {% render_bundle 'style' %} + {% block title %}{% endblock %} + + + {% block extrahead %} + {% endblock %} + {% endspaceless %} + + + {% block content %} + {% endblock %} + + diff --git a/mitxpro/templates/index.html b/mitxpro/templates/index.html new file mode 100644 index 000000000..a573a33b6 --- /dev/null +++ b/mitxpro/templates/index.html @@ -0,0 +1,11 @@ +{% extends "base.html" %} +{% load i18n static %} + +{% block title %}{% trans "mitxpro" %}{% endblock %} + +{% block content %} +

{% trans "Hi, I'm mitxpro" %}

+
+{% load render_bundle %} +{% render_bundle 'root' %} +{% endblock %} diff --git a/mitxpro/templatetags/__init__.py b/mitxpro/templatetags/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/mitxpro/templatetags/render_bundle.py b/mitxpro/templatetags/render_bundle.py new file mode 100644 index 000000000..174bc6d8b --- /dev/null +++ b/mitxpro/templatetags/render_bundle.py @@ -0,0 +1,103 @@ +"""Templatetags for rendering script tags""" + +from django import template +from django.conf import settings +from django.templatetags.static import static +from django.utils.safestring import mark_safe + +from webpack_loader.utils import get_loader + +from mitxpro.utils import webpack_dev_server_url + + +register = template.Library() + + +def ensure_trailing_slash(url): + """ensure a url has a trailing slash""" + return url if url.endswith("/") else url + "/" + + +def public_path(request): + """ + Return the correct public_path for Webpack to use + """ + if settings.USE_WEBPACK_DEV_SERVER: + return ensure_trailing_slash(webpack_dev_server_url(request)) + else: + return ensure_trailing_slash(static("bundles/")) + + +def _get_bundle(request, bundle_name): + """ + Update bundle URLs to handle webpack hot reloading correctly if DEBUG=True + + Args: + request (django.http.request.HttpRequest): A request + bundle_name (str): The name of the webpack bundle + + Returns: + iterable of dict: + The chunks of the bundle. Usually there's only one but I suppose you could have + CSS and JS chunks for one bundle for example + """ + if not settings.DISABLE_WEBPACK_LOADER_STATS: + for chunk in get_loader("DEFAULT").get_bundle(bundle_name): + chunk_copy = dict(chunk) + chunk_copy["url"] = "{host_url}/{bundle}".format( + host_url=public_path(request).rstrip("/"), bundle=chunk["name"] + ) + yield chunk_copy + + +@register.simple_tag(takes_context=True) +def render_bundle(context, bundle_name): + """ + Render the script tags for a Webpack bundle + + We use this instead of webpack_loader.templatetags.webpack_loader.render_bundle because we want to substitute + a dynamic URL for webpack dev environments. Maybe in the future we should refactor to use publicPath + instead for this. + + Args: + context (dict): The context for rendering the template (includes request) + bundle_name (str): The name of the bundle to render + + Returns: + django.utils.safestring.SafeText: + """ + try: + bundle = _get_bundle(context["request"], bundle_name) + return _render_tags(bundle) + except OSError: + # webpack-stats.json doesn't exist + return mark_safe("") + + +def _render_tags(bundle): + """ + Outputs tags for template rendering. + Adapted from webpack_loader.utils.get_as_tags and webpack_loader.templatetags.webpack_loader. + + Args: + bundle (iterable of dict): The information about a webpack bundle + + Returns: + django.utils.safestring.SafeText: HTML for rendering bundles + """ + + tags = [] + for chunk in bundle: + if chunk["name"].endswith((".js", ".js.gz")): + tags.append( + ('').format( + chunk["url"] + ) + ) + elif chunk["name"].endswith((".css", ".css.gz")): + tags.append( + ('').format( + chunk["url"] + ) + ) + return mark_safe("\n".join(tags)) diff --git a/mitxpro/templatetags/render_bundle_test.py b/mitxpro/templatetags/render_bundle_test.py new file mode 100644 index 000000000..65654dcf0 --- /dev/null +++ b/mitxpro/templatetags/render_bundle_test.py @@ -0,0 +1,99 @@ +""" +Tests for render_bundle +""" +from unittest.mock import patch, Mock + +from django.test.client import RequestFactory +from django.test import override_settings, TestCase +from mitxpro.utils import webpack_dev_server_url +from mitxpro.templatetags.render_bundle import render_bundle, public_path + + +FAKE_COMMON_BUNDLE = [ + { + "name": "common-1f11431a92820b453513.js", + "path": "/project/static/bundles/common-1f11431a92820b453513.js", + } +] + + +@override_settings(DISABLE_WEBPACK_LOADER_STATS=False) +class TestRenderBundle(TestCase): + """ + Tests for render_bundle + """ + + @override_settings(USE_WEBPACK_DEV_SERVER=True) + def test_debug(self): + """ + If USE_WEBPACK_DEV_SERVER=True, return a hot reload URL + """ + request = RequestFactory().get("/") + context = {"request": request} + + # convert to generator + common_bundle = (chunk for chunk in FAKE_COMMON_BUNDLE) + get_bundle = Mock(return_value=common_bundle) + loader = Mock(get_bundle=get_bundle) + bundle_name = "bundle_name" + with patch( + "mitxpro.templatetags.render_bundle.get_loader", return_value=loader + ) as get_loader: + assert render_bundle(context, bundle_name) == ( + '".format( + base=webpack_dev_server_url(request), + filename=FAKE_COMMON_BUNDLE[0]["name"], + ) + ) + + assert public_path(request) == webpack_dev_server_url(request) + "/" + + get_bundle.assert_called_with(bundle_name) + get_loader.assert_called_with("DEFAULT") + + @override_settings(USE_WEBPACK_DEV_SERVER=False) + def test_production(self): + """ + If USE_WEBPACK_DEV_SERVER=False, return a static URL for production + """ + request = RequestFactory().get("/") + context = {"request": request} + + # convert to generator + common_bundle = (chunk for chunk in FAKE_COMMON_BUNDLE) + get_bundle = Mock(return_value=common_bundle) + loader = Mock(get_bundle=get_bundle) + bundle_name = "bundle_name" + with patch( + "mitxpro.templatetags.render_bundle.get_loader", return_value=loader + ) as get_loader: + assert render_bundle(context, bundle_name) == ( + '".format( + base="/static/bundles", filename=FAKE_COMMON_BUNDLE[0]["name"] + ) + ) + + assert public_path(request) == "/static/bundles/" + + get_bundle.assert_called_with(bundle_name) + get_loader.assert_called_with("DEFAULT") + + def test_missing_file(self): + """ + If webpack-stats.json is missing, return an empty string + """ + request = RequestFactory().get("/") + context = {"request": request} + + get_bundle = Mock(side_effect=OSError) + loader = Mock(get_bundle=get_bundle) + bundle_name = "bundle_name" + with patch( + "mitxpro.templatetags.render_bundle.get_loader", return_value=loader + ) as get_loader: + assert render_bundle(context, bundle_name) == "" + + get_bundle.assert_called_with(bundle_name) + get_loader.assert_called_with("DEFAULT") diff --git a/mitxpro/urls.py b/mitxpro/urls.py new file mode 100644 index 000000000..e90e9d55b --- /dev/null +++ b/mitxpro/urls.py @@ -0,0 +1,34 @@ +"""project URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/2.0/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.conf import settings +from django.conf.urls import include +from django.contrib import admin +from django.urls import path + +from mitxpro.views import index + + +urlpatterns = [ + path("admin/", admin.site.urls), + path("status/", include("server_status.urls")), + # Example view + path("", index, name="mitxpro-index"), +] + +if settings.DEBUG: + import debug_toolbar # pylint: disable=wrong-import-position, wrong-import-order + + urlpatterns += [path("__debug__/", include(debug_toolbar.urls))] diff --git a/mitxpro/urls_test.py b/mitxpro/urls_test.py new file mode 100644 index 000000000..9e336fd27 --- /dev/null +++ b/mitxpro/urls_test.py @@ -0,0 +1,12 @@ +"""Tests for URLs""" + +from unittest import TestCase +from django.urls import reverse + + +class URLTests(TestCase): + """URL tests""" + + def test_urls(self): + """Make sure URLs match with resolved names""" + assert reverse("mitxpro-index") == "/" diff --git a/mitxpro/utils.py b/mitxpro/utils.py new file mode 100644 index 000000000..deb36267f --- /dev/null +++ b/mitxpro/utils.py @@ -0,0 +1,35 @@ +"""mitxpro utilities""" +from enum import auto, Flag +import logging + +from django.conf import settings + + +log = logging.getLogger(__name__) + + +class FeatureFlag(Flag): + """ + FeatureFlag enum + + Members should have values of increasing powers of 2 (1, 2, 4, 8, ...) + + """ + + EXAMPLE_FEATURE = auto() + + +def webpack_dev_server_host(request): + """ + Get the correct webpack dev server host + """ + return settings.WEBPACK_DEV_SERVER_HOST or request.get_host().split(":")[0] + + +def webpack_dev_server_url(request): + """ + Get the full URL where the webpack dev server should be running + """ + return "http://{}:{}".format( + webpack_dev_server_host(request), settings.WEBPACK_DEV_SERVER_PORT + ) diff --git a/mitxpro/views.py b/mitxpro/views.py new file mode 100644 index 000000000..392fafa28 --- /dev/null +++ b/mitxpro/views.py @@ -0,0 +1,28 @@ +""" +mitxpro views +""" +import json + +from django.conf import settings +from django.shortcuts import render +from raven.contrib.django.raven_compat.models import client as sentry + +from mitxpro.templatetags.render_bundle import public_path + + +def index(request): + """ + The index view. Display available programs + """ + + js_settings = { + "gaTrackingID": settings.GA_TRACKING_ID, + "environment": settings.ENVIRONMENT, + "public_path": public_path(request), + "release_version": settings.VERSION, + "sentry_dsn": sentry.get_public_dsn(), + } + + return render( + request, "index.html", context={"js_settings_json": json.dumps(js_settings)} + ) diff --git a/mitxpro/views_test.py b/mitxpro/views_test.py new file mode 100644 index 000000000..9934f8ffd --- /dev/null +++ b/mitxpro/views_test.py @@ -0,0 +1,38 @@ +""" +Test end to end django views. +""" +import json + +from django.urls import reverse +import pytest + + +pytestmark = [pytest.mark.django_db] + + +def test_index_view(client): + """Verify the index view is as expected""" + response = client.get(reverse("mitxpro-index")) + assert response.status_code == 200 + assert b"Hi, I'm mitxpro" in response.content + + +def test_webpack_url(mocker, settings, client): + """Verify that webpack bundle src shows up in production""" + settings.GA_TRACKING_ID = "fake" + settings.ENVIRONMENT = "test" + settings.VERSION = "4.5.6" + get_bundle = mocker.patch("mitxpro.templatetags.render_bundle._get_bundle") + + response = client.get(reverse("mitxpro-index")) + + bundles = [bundle[0][1] for bundle in get_bundle.call_args_list] + assert set(bundles) == {"common", "root", "style"} + js_settings = json.loads(response.context["js_settings_json"]) + assert js_settings == { + "gaTrackingID": "fake", + "public_path": "/static/bundles/", + "environment": settings.ENVIRONMENT, + "sentry_dsn": None, + "release_version": settings.VERSION, + } diff --git a/mitxpro/wsgi.py b/mitxpro/wsgi.py new file mode 100644 index 000000000..1935cf2b9 --- /dev/null +++ b/mitxpro/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for ui app. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/1.8/howto/deployment/wsgi/ +""" +import os + +from raven.contrib.django.raven_compat.middleware.wsgi import Sentry +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mitxpro.settings") + +application = Sentry(get_wsgi_application()) # pylint: disable=invalid-name diff --git a/package.json b/package.json new file mode 100644 index 000000000..405cb95c1 --- /dev/null +++ b/package.json @@ -0,0 +1,100 @@ +{ + "name": "mitxpro", + "version": "0.0.0", + "license": "BSD-3-Clause", + "repository": { + "type": "git", + "url": "https://github.com/mitodl/mitxpro.git" + }, + "dependencies": { + "autoprefixer": "^7.1.2", + "babel-core": "^6.25.0", + "babel-eslint": "^8.1.2", + "babel-loader": "^7.1.2", + "babel-plugin-istanbul": "^4.1.5", + "babel-plugin-jsx": "^1.2.0", + "babel-plugin-syntax-dynamic-import": "^6.18.0", + "babel-plugin-transform-class-properties": "^6.24.1", + "babel-plugin-transform-flow-strip-types": "^6.22.0", + "babel-plugin-transform-object-rest-spread": "^6.23.0", + "babel-plugin-transform-react-constant-elements": "^6.23.0", + "babel-plugin-transform-react-inline-elements": "^6.22.0", + "babel-plugin-transform-react-jsx": "^6.24.1", + "babel-polyfill": "^6.23.0", + "babel-preset-env": "^1.6.1", + "babel-preset-react": "^6.24.1", + "babel-register": "^6.26.0", + "chai": "^4.1.2", + "chai-as-promised": "^7.1.1", + "codecov": "^2.2.0", + "css-loader": "^0.28.4", + "enzyme": "^2.9.1", + "eslint": "^4.2.0", + "eslint-config-google": "^0.9.1", + "eslint-plugin-babel": "^4.1.1", + "eslint-plugin-flow-vars": "^0.5.0", + "eslint-plugin-flowtype": "^2.35.0", + "eslint-plugin-mocha": "^4.11.0", + "eslint-config-mitodl": "^0.0.4", + "eslint-plugin-react": "^7.1.0", + "express": "^4.15.3", + "extract-text-webpack-plugin": "2", + "fetch-mock": "^5.12.1", + "flow-bin": "^0.65.0", + "history": "^4.6.3", + "isomorphic-fetch": "^2.2.1", + "jsdom": "^11.5.1", + "jsdom-global": "^3.0.2", + "lodash": "^4.17.4", + "mocha": "^4.0.1", + "node-sass": "^4.7.2", + "nyc": "^11.0.3", + "object.entries": "^1.0.4", + "postcss-loader": "^2.0.6", + "prettier-eslint-cli": "^4.3.2", + "prop-types": "^15.5.10", + "ramda": "^0.24.1", + "raven-js": "^3.26.4", + "react": "^15.6.1", + "react-addons-shallow-compare": "^15.6.0", + "react-dom": "^15.6.1", + "react-ga": "^2.2.0", + "react-hot-loader": "3.1.3", + "react-redux": "^5.0.5", + "react-router": "^4.1.1", + "react-router-dom": "^4.1.1", + "react-test-renderer": "^15.6.1", + "redux": "^3.7.2", + "redux-actions": "^2.2.1", + "redux-asserts": "^0.0.10", + "redux-logger": "^3.0.6", + "redux-thunk": "^2.2.0", + "sanctuary": "^0.13.2", + "sass-lint": "^1.10.2", + "sass-loader": "^6.0.6", + "sinon": "^4.1.3", + "style-loader": "^0.18.2", + "url-loader": "^0.5.9", + "webpack": "^3.2.0", + "webpack-bundle-tracker": "^0.2.0", + "webpack-dev-middleware": "^1.11.0", + "webpack-hot-middleware": "^2.18.2" + }, + "engines": { + "node": "9.3.0", + "yarn": "1.3.2" + }, + "scripts": { + "postinstall": "./webpack_if_prod.sh", + "lint": "node ./node_modules/eslint/bin/eslint.js ./static/js", + "scss_lint": "node ./node_modules/sass-lint/bin/sass-lint.js --verbose --no-exit", + "test": "./scripts/test/js_test.sh", + "coverage": "COVERAGE=1 ./scripts/test/js_test.sh", + "codecov": "CODECOV=1 ./scripts/test/js_test.sh", + "watch": "WATCH=1 ./scripts/test/js_test.sh", + "repl": "node --require ./scripts/repl.js", + "flow": "flow check", + "fmt": "LOG_LEVEL= prettier-eslint --write --no-semi --ignore 'static/js/flow/**/*.js' 'static/js/**/*.js'", + "fmt:check": "LOG_LEVEL= prettier-eslint --list-different --no-semi --ignore 'static/js/flow/**/*.js' 'static/js/**/*.js'" + } +} diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 000000000..e9135b45d --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,7 @@ +module.exports = { + plugins: [ + require('autoprefixer')({ + browsers: ['> 1%'] + }) + ] +} diff --git a/pylintrc b/pylintrc new file mode 100644 index 000000000..d498ff4e4 --- /dev/null +++ b/pylintrc @@ -0,0 +1,22 @@ +[MASTER] +ignore=migrations,.git +load-plugins = pylint_django + +[BASIC] +# Allow django's urlpatterns, and our log preference +const-rgx = (([A-Z_][A-Z0-9_]*)|(__.*__)|log|urlpatterns)$ +# Don't require docstrings for double-underscore methods, or for unittest support methods +no-docstring-rgx = __.*__$|setUp$|setUpClass$|tearDown$|tearDownClass$|Meta$|Params$ + +[TYPECHECK] +generated-members = + status_code +ignored-classes= + six, + six.moves, +ignored-modules= + six, + six.moves, + +[MESSAGES CONTROL] +disable = no-member, old-style-class, no-init, too-few-public-methods, abstract-method, invalid-name, too-many-ancestors, line-too-long, no-self-use, len-as-condition, no-else-return, cyclic-import, duplicate-code, inconsistent-return-statements, bad-continuation, diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..c56d9d84c --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,14 @@ +[tool.black] +py36 = true +include = '\.pyi?$' +exclude = ''' +/( + \.git + | \.mypy_cache + | \.tox + | \.venv + | build + | dist + | node_modules +)/ +''' diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 000000000..968d5be62 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,13 @@ +[pytest] +addopts = --cov . --pep8 --pylint --cov-report term --cov-report html --ds=mitxpro.settings --reuse-db +norecursedirs = node_modules .git .tox static templates .* CVS _darcs {arch} *.egg +pep8ignore = + */migrations/* ALL + W503 + E203 + E501 +pep8maxlinelength = 119 +filterwarnings = + error + ignore::DeprecationWarning + ignore:Failed to load HostKeys diff --git a/repl.py b/repl.py new file mode 100755 index 000000000..9bf95f3bc --- /dev/null +++ b/repl.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +"""Run Django shell with imported modules""" +if __name__ == "__main__": + import os + + if not os.environ.get("PYTHONSTARTUP"): + from subprocess import check_call + import sys + + base_dir = os.path.dirname(os.path.abspath(__file__)) + + sys.exit( + check_call( + [os.path.join(base_dir, "manage.py"), "shell", *sys.argv[1:]], + env={**os.environ, "PYTHONSTARTUP": os.path.join(base_dir, "repl.py")}, + ) + ) + + # put imports here used by PYTHONSTARTUP + from django.conf import settings + + for app in settings.INSTALLED_APPS: + try: + exec( # pylint: disable=exec-used + "from {app}.models import *".format(app=app) + ) + except ModuleNotFoundError: + pass diff --git a/requirements.in b/requirements.in new file mode 100644 index 000000000..01e47f8a0 --- /dev/null +++ b/requirements.in @@ -0,0 +1,13 @@ +django==2.1.7 +celery==4.2.0 +dj-database-url==0.4.2 +django-redis==4.7.0 +django-server-status==0.5.0 +django-webpack-loader==0.5.0 +ipython +newrelic +psycopg2==2.7.3.2 +raven +redis==2.10.6 +requests +uwsgi diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..6c8cae09a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,42 @@ +# +# This file is autogenerated by pip-compile +# To update, run: +# +# pip-compile --upgrade +# +amqp==2.4.1 # via kombu +backcall==0.1.0 # via ipython +billiard==3.5.0.5 # via celery +celery==4.2.0 +certifi==2018.11.29 # via requests +chardet==3.0.4 # via requests +decorator==4.3.2 # via ipython, traitlets +dj-database-url==0.4.2 +django-redis==4.7.0 +django-server-status==0.5.0 +django-webpack-loader==0.5.0 +django==2.1.7 +elasticsearch==6.3.1 # via django-server-status +idna==2.8 # via requests +ipython-genutils==0.2.0 # via traitlets +ipython==7.3.0 +jedi==0.13.2 # via ipython +kombu==4.3.0 # via celery, django-server-status +newrelic==4.14.0.115 +parso==0.3.4 # via jedi +pexpect==4.6.0 # via ipython +pickleshare==0.7.5 # via ipython +prompt-toolkit==2.0.9 # via ipython +psycopg2==2.7.3.2 +ptyprocess==0.6.0 # via pexpect +pygments==2.3.1 # via ipython +pytz==2018.9 # via celery, django +raven==6.10.0 +redis==2.10.6 +requests==2.21.0 +six==1.12.0 # via django-server-status, prompt-toolkit, traitlets +traitlets==4.3.2 # via ipython +urllib3==1.24.1 # via elasticsearch, requests +uwsgi==2.0.18 +vine==1.2.0 # via amqp +wcwidth==0.1.7 # via prompt-toolkit diff --git a/runtime.txt b/runtime.txt new file mode 100644 index 000000000..c91e43be5 --- /dev/null +++ b/runtime.txt @@ -0,0 +1 @@ +python-3.6.1 diff --git a/scripts/envs.sh b/scripts/envs.sh new file mode 100755 index 000000000..483c446ea --- /dev/null +++ b/scripts/envs.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +set -e -o pipefail + +# If we're running from inside a container /.dockerenv should exist (not a guarantee but the best we can do) +if [[ -e "/.dockerenv" ]] +then + INSIDE_CONTAINER="true" +else + INSIDE_CONTAINER="false" +fi + +# Set WEBPACK_DEV_SERVER_HOST to the IP or hostname which the browser will use to contact the webpack dev server +WEBPACK_DEV_SERVER_HOST="localhost" + +# Set WEBPACK_SELENIUM_DEV_SERVER_HOST to the IP address for the webpack dev server +# This is different from WEBPACK_DEV_SERVER_HOST because localhost won't suffice here since the request +# is coming from a docker container, not the browser. If we can't detect this the user must set it via a script. +if [[ "$INSIDE_CONTAINER" == "true" ]] +then + # Linux container + WEBPACK_SELENIUM_DEV_SERVER_HOST="$(ip route | grep default | awk '{ print $3 }')" +else + # Linux host + CONTAINER_NAME="$(docker-compose ps -q watch)" + if [[ -z "$CONTAINER_NAME" ]] + then + echo "Missing container watch" + exit 1 + fi + + + CONTAINER_STATUS="$(docker inspect "$CONTAINER_NAME" -f '{{.State.Status}}')" + + if [[ "$CONTAINER_STATUS" != "running" ]] + then + echo "watch container status for $CONTAINER_NAME was expected to be running but is $CONTAINER_STATUS" + exit 1 + fi + WEBPACK_SELENIUM_DEV_SERVER_HOST="$(docker exec "$CONTAINER_NAME" ip route | grep default | awk '{ print $3 }')" +fi + +export INSIDE_CONTAINER="$INSIDE_CONTAINER" +export WEBPACK_DEV_SERVER_HOST="$WEBPACK_DEV_SERVER_HOST" +export WEBPACK_SELENIUM_DEV_SERVER_HOST="$WEBPACK_SELENIUM_DEV_SERVER_HOST" + +echo "Vars set:" +echo INSIDE_CONTAINER="$INSIDE_CONTAINER" +echo WEBPACK_DEV_SERVER_HOST="$WEBPACK_DEV_SERVER_HOST" +echo WEBPACK_SELENIUM_DEV_SERVER_HOST="$WEBPACK_SELENIUM_DEV_SERVER_HOST" diff --git a/scripts/install_yarn.js b/scripts/install_yarn.js new file mode 100644 index 000000000..e5612cb9d --- /dev/null +++ b/scripts/install_yarn.js @@ -0,0 +1,25 @@ +// Install version of yarn specified in package.json + +const fs = require('fs'); +const { spawn } = require('child_process'); + +const { engines: { yarn: yarnVersion }} = JSON.parse(fs.readFileSync(__dirname + "/../package.json")); + +let install = spawn('npm', [ + 'install', + '-g', + `yarn@${yarnVersion}` +]); + +install.stdout.on('data', data => console.log(`${data}`)); + +install.stderr.on('data', err => console.log(`${err}`)); + +install.on('close', code => { + if ( code === 0 ) { + console.log('yarn installed successfully!') + } else { + console.error(`error: code ${code}`); + console.error('\ndid you run as root?') + } +}); diff --git a/scripts/repl.js b/scripts/repl.js new file mode 100644 index 000000000..1ad5eff68 --- /dev/null +++ b/scripts/repl.js @@ -0,0 +1,7 @@ +require("babel-register") + +R = require("ramda") +moment = require("moment") +S = require("sanctuary") +sinon = require("sinon") +assert = require("chai").assert diff --git a/scripts/test/check_pip.sh b/scripts/test/check_pip.sh new file mode 100755 index 000000000..ab4e50d98 --- /dev/null +++ b/scripts/test/check_pip.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -e -o pipefail + +PIP_OLD=$(mktemp) +PIP_NEW=$(mktemp) +VENV=$(mktemp -d) + + +pip freeze > $PIP_NEW +/usr/local/bin/pip freeze > $PIP_OLD +if ! cmp -s $PIP_OLD $PIP_NEW +then + echo "requirements files differ from docker image pip environment. Running diff image-pip tox-pip:" + diff $PIP_OLD $PIP_NEW + exit 1 +fi + +rm $PIP_OLD +rm $PIP_NEW +rm -r $VENV diff --git a/scripts/test/detect_missing_migrations.sh b/scripts/test/detect_missing_migrations.sh new file mode 100755 index 000000000..f565908ba --- /dev/null +++ b/scripts/test/detect_missing_migrations.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +TMPFILE=$(mktemp) +fail() { + echo "Error: one or more migrations are missing" + echo + cat "$TMPFILE" + rm "$TMPFILE" + exit 1 +} + +./manage.py makemigrations --no-input --dry-run >& "$TMPFILE" +if [[ $? -ne 0 ]] +then + # makemigrations has returned a non-zero for some reason, possibly + # because it needs input but --no-input is set + fail +elif [[ $(cat "$TMPFILE" | grep "No changes detected" | wc -l) -eq 0 ]] +then + fail +else + rm "$TMPFILE" +fi diff --git a/scripts/test/js_test.sh b/scripts/test/js_test.sh new file mode 100755 index 000000000..cea0f17b3 --- /dev/null +++ b/scripts/test/js_test.sh @@ -0,0 +1,67 @@ +#!/bin/bash +export TMP_FILE=$(mktemp) + +if [[ ! -z "$COVERAGE" ]] +then + export CMD="node ./node_modules/nyc/bin/nyc.js --reporter=html mocha" +elif [[ ! -z "$CODECOV" ]] +then + export CMD="node ./node_modules/nyc/bin/nyc.js --reporter=lcovonly -R spec mocha" +elif [[ ! -z "$WATCH" ]] +then + export CMD="node ./node_modules/mocha/bin/_mocha --watch" +else + export CMD="node ./node_modules/mocha/bin/_mocha" +fi + +export FILE_PATTERN=${1:-'"static/**/*/*_test.js"'} +CMD_ARGS="--require ./static/js/babelhook.js static/js/global_init.js $FILE_PATTERN" + +# Second argument (if specified) should be a string that will match specific test case descriptions +# +# EXAMPLE: +# (./static/js/SomeComponent_test.js) +# it('should test basic arithmetic') { +# assert.equal(1 + 1, 2); +# } +# +# (in command line...) +# > ./js_test.sh static/js/SomeComponent_test.js "should test basic arithmetic" +if [[ ! -z "$2" ]]; then + CMD_ARGS+=" -g \"$2\"" +fi + +eval "$CMD $CMD_ARGS" 2> >(tee "$TMP_FILE") + +export TEST_RESULT=$? +export TRAVIS_BUILD_DIR=$PWD +if [[ ! -z "$CODECOV" ]] +then + echo "Uploading coverage..." + node ./node_modules/codecov/bin/codecov +fi + +if [[ $TEST_RESULT -ne 0 ]] +then + echo "Tests failed, exiting with error $TEST_RESULT..." + rm -f "$TMP_FILE" + exit 1 +fi + +if [[ $( + cat "$TMP_FILE" | + grep -v 'ignored, nothing could be mapped' | + grep -v "This browser doesn't support the \`onScroll\` event" | + grep -v "process.on(SIGPROF) is reserved while debugging" | + wc -l | + awk '{print $1}' + ) -ne 0 ]] # is file empty? +then + echo "Error output found:" + cat "$TMP_FILE" + echo "End of output" + rm -f "$TMP_FILE" + exit 1 +fi + +rm -f "$TMP_FILE" diff --git a/scripts/test/no_auto_migrations.sh b/scripts/test/no_auto_migrations.sh new file mode 100755 index 000000000..8781cfba0 --- /dev/null +++ b/scripts/test/no_auto_migrations.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +TMPFILE=$(mktemp) +fail() { + echo "Error: migrations with generated names exist" + echo + cat "$TMPFILE" + rm "$TMPFILE" + exit 1 +} + +# search for auto migrations excluded the preexisting one +find */migrations/*_auto_*.py | grep -v "20170113_2133" > "$TMPFILE" + +if [[ $(cat "$TMPFILE" | wc -l) -ne 0 ]] +then + fail +else + rm "$TMPFILE" +fi diff --git a/scripts/test/test_suite.sh b/scripts/test/test_suite.sh new file mode 100755 index 000000000..9e2044ac0 --- /dev/null +++ b/scripts/test/test_suite.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -euf -o pipefail + +docker-compose run web tox +docker-compose run watch npm run-script lint +docker-compose run watch npm run-script flow +docker-compose run watch npm run-script coverage diff --git a/static/images/favicon.ico b/static/images/favicon.ico new file mode 100644 index 000000000..67203a5c9 Binary files /dev/null and b/static/images/favicon.ico differ diff --git a/static/js/Router.js b/static/js/Router.js new file mode 100644 index 000000000..0f754b75f --- /dev/null +++ b/static/js/Router.js @@ -0,0 +1,27 @@ +import React from "react" +import { Route, Router as ReactRouter } from "react-router" +import { Provider } from "react-redux" + +import App from "./containers/App" +import withTracker from "./util/withTracker" + +export default class Root extends React.Component { + props: { + history: Object, + store: Store + } + + render() { + const { children, history, store } = this.props + + return ( +
+ + {children} + +
+ ) + } +} + +export const routes = diff --git a/static/js/actions/index.js b/static/js/actions/index.js new file mode 100644 index 000000000..a4db9d6e8 --- /dev/null +++ b/static/js/actions/index.js @@ -0,0 +1,7 @@ +// @flow +export const UPDATE_CHECKBOX = "UPDATE_CHECKBOX" + +export const updateCheckbox = (checked: boolean) => ({ + type: UPDATE_CHECKBOX, + payload: { checked } +}) diff --git a/static/js/babelhook.js b/static/js/babelhook.js new file mode 100644 index 000000000..0f944b6b0 --- /dev/null +++ b/static/js/babelhook.js @@ -0,0 +1,23 @@ +const { babelSharedLoader } = require("../../webpack.config.shared") + +babelSharedLoader.query.presets = ["env", "react"] + +require("babel-polyfill") + +// window and global must be defined here before React is imported +require("jsdom-global")(undefined, { + url: "http://fake/" +}) + +// We need to explicitly change the URL when window.location is used +const changeURL = require("jsdom/lib/old-api").changeURL +Object.defineProperty(window, "location", { + set: value => { + if (!value.startsWith("http")) { + value = `http://fake${value}` + } + changeURL(window, value) + } +}) + +require("babel-register")(babelSharedLoader.query) diff --git a/static/js/constants.js b/static/js/constants.js new file mode 100644 index 000000000..8100720c9 --- /dev/null +++ b/static/js/constants.js @@ -0,0 +1,5 @@ +// Put constants here + +export const THING_RESPONSE = { + value: "thing" +} diff --git a/static/js/containers/App.js b/static/js/containers/App.js new file mode 100644 index 000000000..0df6585d3 --- /dev/null +++ b/static/js/containers/App.js @@ -0,0 +1,20 @@ +// @flow +import React from "react" +import { Route } from "react-router" +import type { Match } from "react-router" + +import CheckboxPage from "./CheckboxPage" + +export default class App extends React.Component<*, void> { + props: { + match: Match + } + + render() { + return ( +
+ +
+ ) + } +} diff --git a/static/js/containers/CheckboxPage.js b/static/js/containers/CheckboxPage.js new file mode 100644 index 000000000..016318d53 --- /dev/null +++ b/static/js/containers/CheckboxPage.js @@ -0,0 +1,46 @@ +// @flow +import React from "react" +import { connect } from "react-redux" +import type { Match } from "react-router" +import type { Dispatch } from "redux" + +import { updateCheckbox } from "../actions" + +class CheckboxPage extends React.Component<*, void> { + props: { + dispatch: Dispatch<*>, + checkbox: { + checked: boolean + }, + match: Match + } + + handleClick(e) { + const { dispatch } = this.props + dispatch(updateCheckbox(e.target.checked)) + } + + render() { + const { checked } = this.props.checkbox + return ( +
+
+ Click the checkbox: + +
+
+ ) + } +} + +const mapStateToProps = state => { + return { + checkbox: state.checkbox + } +} + +export default connect(mapStateToProps)(CheckboxPage) diff --git a/static/js/containers/CheckboxPage_test.js b/static/js/containers/CheckboxPage_test.js new file mode 100644 index 000000000..5c23c0263 --- /dev/null +++ b/static/js/containers/CheckboxPage_test.js @@ -0,0 +1,22 @@ +// @flow +import { assert } from "chai" +import IntegrationTestHelper from "../util/integration_test_helper" + +describe("CheckboxPage", () => { + let helper, renderComponent + + beforeEach(() => { + helper = new IntegrationTestHelper() + renderComponent = helper.renderComponent.bind(helper) + }) + + afterEach(() => { + helper.cleanup() + }) + + it("renders properly", () => { + return renderComponent("/").then(([wrapper]) => { + assert.include(wrapper.text(), "Click the checkbox:") + }) + }) +}) diff --git a/static/js/entry/root.js b/static/js/entry/root.js new file mode 100644 index 000000000..948488f5a --- /dev/null +++ b/static/js/entry/root.js @@ -0,0 +1,50 @@ +require("react-hot-loader/patch") +/* global SETTINGS:false */ +__webpack_public_path__ = SETTINGS.public_path // eslint-disable-line no-undef, camelcase +import React from "react" +import ReactDOM from "react-dom" +import { AppContainer } from "react-hot-loader" +import { createBrowserHistory } from "history" + +import configureStore from "../store/configureStore" +import Router, { routes } from "../Router" + +import Raven from "raven-js" + +Raven.config(SETTINGS.sentry_dsn, { + release: SETTINGS.release_version, + environment: SETTINGS.environment +}).install() + +window.Raven = Raven + +// Object.entries polyfill +import entries from "object.entries" +if (!Object.entries) { + entries.shim() +} + +const store = configureStore() + +const rootEl = document.getElementById("container") + +const renderApp = Component => { + const history = createBrowserHistory() + ReactDOM.render( + + + {routes} + + , + rootEl + ) +} + +renderApp(Router) + +if (module.hot) { + module.hot.accept("../Router", () => { + const RouterNext = require("../Router").default + renderApp(RouterNext) + }) +} diff --git a/static/js/entry/style.js b/static/js/entry/style.js new file mode 100644 index 000000000..ef7031117 --- /dev/null +++ b/static/js/entry/style.js @@ -0,0 +1,3 @@ +/* global SETTINGS:false */ +__webpack_public_path__ = SETTINGS.public_path // eslint-disable-line no-undef, camelcase +import "../../scss/layout.scss" diff --git a/static/js/flow/declarations.js b/static/js/flow/declarations.js new file mode 100644 index 000000000..e389e4bfb --- /dev/null +++ b/static/js/flow/declarations.js @@ -0,0 +1,25 @@ +// @flow +/* eslint-disable no-unused-vars */ +declare var SETTINGS: { + public_path: string, + FEATURES: { + [key: string]: boolean, + }, + reactGaDebug: string, + sentry_dsn: string, + release_version: string, + environment: string, +}; + +// mocha +declare var it: Function; +declare var beforeEach: Function; +declare var afterEach: Function; +declare var describe: Function; + +// webpack +declare var __webpack_public_path__: string; // eslint-disable-line camelcase + +declare var module: { + hot: any, +} diff --git a/static/js/flow/reduxTypes.js b/static/js/flow/reduxTypes.js new file mode 100644 index 000000000..b77997c41 --- /dev/null +++ b/static/js/flow/reduxTypes.js @@ -0,0 +1,8 @@ +// @flow +export type ActionType = string + +export type Action = { + type: ActionType, + payload: payload, + meta: meta, +} diff --git a/static/js/flow/sinonTypes.js b/static/js/flow/sinonTypes.js new file mode 100644 index 000000000..ed01208fe --- /dev/null +++ b/static/js/flow/sinonTypes.js @@ -0,0 +1,3 @@ +export type Sandbox = { + +}; diff --git a/static/js/global_init.js b/static/js/global_init.js new file mode 100644 index 000000000..877efa05b --- /dev/null +++ b/static/js/global_init.js @@ -0,0 +1,29 @@ +// Define globals we would usually get from Django +import ReactDOM from "react-dom" + +const _createSettings = () => ({}) + +global.SETTINGS = _createSettings() + +// polyfill for Object.entries +import entries from "object.entries" +if (!Object.entries) { + entries.shim() +} + +// cleanup after each test run +// eslint-disable-next-line mocha/no-top-level-hooks +afterEach(function() { + const node = document.querySelector("#integration_test_div") + if (node) { + ReactDOM.unmountComponentAtNode(node) + } + document.body.innerHTML = "" + global.SETTINGS = _createSettings() + window.location = "http://fake/" +}) + +// enable chai-as-promised +import chai from "chai" +import chaiAsPromised from "chai-as-promised" +chai.use(chaiAsPromised) diff --git a/static/js/lib/api.js b/static/js/lib/api.js new file mode 100644 index 000000000..906d13944 --- /dev/null +++ b/static/js/lib/api.js @@ -0,0 +1,147 @@ +// @flow +/* global SETTINGS:false, fetch: false */ +// For mocking purposes we need to use 'fetch' defined as a global instead of importing as a local. +import "isomorphic-fetch" +import R from "ramda" + +import { S, parseJSON, filterE } from "./sanctuary" + +export function getCookie(name: string): string | null { + let cookieValue = null + + if (document.cookie && document.cookie !== "") { + const cookies = document.cookie.split(";") + + for (let cookie of cookies) { + cookie = cookie.trim() + + // Does this cookie string begin with the name we want? + if (cookie.substring(0, name.length + 1) === `${name}=`) { + cookieValue = decodeURIComponent(cookie.substring(name.length + 1)) + break + } + } + } + return cookieValue +} + +export function csrfSafeMethod(method: string): boolean { + // these HTTP methods do not require CSRF protection + return /^(GET|HEAD|OPTIONS|TRACE)$/.test(method) +} + +const headers = R.merge({ headers: {} }) + +const method = R.merge({ method: "GET" }) + +const credentials = R.merge({ credentials: "same-origin" }) + +const setWith = R.curry((path, valFunc, obj) => R.set(path, valFunc(), obj)) + +const csrfToken = R.unless( + R.compose( + csrfSafeMethod, + R.prop("method") + ), + setWith(R.lensPath(["headers", "X-CSRFToken"]), () => getCookie("csrftoken")) +) + +const jsonHeaders = R.merge({ + headers: { + "Content-Type": "application/json", + Accept: "application/json" + } +}) + +const formatRequest = R.compose( + csrfToken, + credentials, + method, + headers +) + +const formatJSONRequest = R.compose( + formatRequest, + jsonHeaders +) + +const _fetchWithCSRF = async (path: string, init: Object = {}): Promise<*> => { + const response = await fetch(path, formatRequest(init)) + const text = await response.text() + + if (response.status < 200 || response.status >= 300) { + return Promise.reject([text, response.status]) + } + return text +} + +export { _fetchWithCSRF as fetchWithCSRF } + +// resolveEither :: Either -> Promise +// if the Either is a Left, returns Promise.reject(val) +// if the Either is a Right, returns Promise.resolve(val) +// where val is the unwrapped value in the Either +const resolveEither = S.either( + val => Promise.reject(val), + val => Promise.resolve(val) +) + +const handleEmptyJSON = json => (json.length === 0 ? JSON.stringify({}) : json) + +/** + * Calls to fetch but does a few other things: + * - turn cookies on for this domain + * - set headers to handle JSON properly + * - handle CSRF + * - non 2xx status codes will reject the promise returned + * - response JSON is returned in place of response + */ +const _fetchJSONWithCSRF = async ( + input: string, + init: Object = {}, + loginOnError: boolean = false +): Promise<*> => { + const response = await fetch(input, formatJSONRequest(init)) + // For 400 and 401 errors, force login + // the 400 error comes from edX in case there are problems with the refresh + // token because the data stored locally is wrong and the solution is only + // to force a new login + if ( + loginOnError === true && + (response.status === 400 || response.status === 401) + ) { + const relativePath = window.location.pathname + window.location.search + const loginRedirect = `/login/edxorg/?next=${encodeURIComponent( + relativePath + )}` + window.location = `/logout?next=${encodeURIComponent(loginRedirect)}` + } + + // we pull the text out of the response + const text = await response.text() + + // Here we use the `parseJSON` function, which returns an Either. + // Left records an error parsing the JSON, and Right success. `filterE` will turn a Right + // into a Left based on a boolean function (similar to filtering a Maybe), and we use `bimap` + // to merge an error code into a Left. The `resolveEither` function above will resolve a Right + // and reject a Left. + return R.compose( + resolveEither, + S.bimap(R.merge({ errorStatusCode: response.status }), R.identity), + filterE(() => response.ok), + parseJSON, + handleEmptyJSON + )(text) +} + +// allow mocking in tests +export { _fetchJSONWithCSRF as fetchJSONWithCSRF } +import { fetchJSONWithCSRF } from "./api" + +// import to allow mocking in tests +export function patchThing(username: string, newThing: Object) { + return fetchJSONWithCSRF(`/api/v0/thing/${username}/`, { + method: "PATCH", + body: JSON.stringify(newThing) + }) +} diff --git a/static/js/lib/api_test.js b/static/js/lib/api_test.js new file mode 100644 index 000000000..37aa1ab69 --- /dev/null +++ b/static/js/lib/api_test.js @@ -0,0 +1,269 @@ +/* global SETTINGS: false */ +import { assert } from "chai" +import fetchMock from "fetch-mock/src/server" +import sinon from "sinon" + +import { + getCookie, + fetchJSONWithCSRF, + fetchWithCSRF, + csrfSafeMethod, + patchThing +} from "./api" +import * as api from "./api" + +describe("api", function() { + this.timeout(5000) // eslint-disable-line no-invalid-this + + let sandbox + beforeEach(() => { + sandbox = sinon.createSandbox({}) + }) + afterEach(function() { + sandbox.restore() + + for (const cookie of document.cookie.split(";")) { + const key = cookie.split("=")[0].trim() + document.cookie = `${key}=` + } + }) + + describe("REST functions", () => { + const THING_RESPONSE = { + a: "thing" + } + + let fetchStub + beforeEach(() => { + fetchStub = sandbox.stub(api, "fetchJSONWithCSRF") + }) + + it("patches a thing", () => { + fetchStub.returns(Promise.resolve(THING_RESPONSE)) + + return patchThing("jane", THING_RESPONSE).then(thing => { + assert.ok( + fetchStub.calledWith("/api/v0/thing/jane/", { + method: "PATCH", + body: JSON.stringify(THING_RESPONSE) + }) + ) + assert.deepEqual(thing, THING_RESPONSE) + }) + }) + + it("fails to patch a thing", () => { + fetchStub.returns(Promise.reject()) + return patchThing("jane", THING_RESPONSE).catch(() => { + assert.ok( + fetchStub.calledWith("/api/v0/thing/jane/", { + method: "PATCH", + body: JSON.stringify(THING_RESPONSE) + }) + ) + }) + }) + }) + + describe("fetch functions", () => { + const CSRF_TOKEN = "asdf" + + afterEach(() => { + fetchMock.restore() + }) + + describe("fetchWithCSRF", () => { + beforeEach(() => { + document.cookie = `csrftoken=${CSRF_TOKEN}` + }) + + it("fetches and populates appropriate headers for GET", () => { + const body = "body" + + fetchMock.mock("/url", (url, opts) => { + assert.deepEqual(opts, { + credentials: "same-origin", + headers: {}, + body: body, + method: "GET" + }) + + return { + status: 200, + body: "Some text" + } + }) + + return fetchWithCSRF("/url", { + body: body + }).then(responseBody => { + assert.equal(responseBody, "Some text") + }) + }) + + for (const method of ["PATCH", "PUT", "POST"]) { + it(`fetches and populates appropriate headers for ${method}`, () => { + const body = "body" + + fetchMock.mock("/url", (url, opts) => { + assert.deepEqual(opts, { + credentials: "same-origin", + headers: { + "X-CSRFToken": CSRF_TOKEN + }, + body: body, + method: method + }) + + return { + status: 200, + body: "Some text" + } + }) + + return fetchWithCSRF("/url", { + body, + method + }).then(responseBody => { + assert.equal(responseBody, "Some text") + }) + }) + } + + for (const statusCode of [300, 400, 500]) { + it(`rejects the promise if the status code is ${statusCode}`, () => { + fetchMock.get("/url", statusCode) + return assert.isRejected(fetchWithCSRF("/url")) + }) + } + }) + + describe("fetchJSONWithCSRF", () => { + it("fetches and populates appropriate headers for JSON", () => { + document.cookie = `csrftoken=${CSRF_TOKEN}` + const expectedJSON = { data: true } + + fetchMock.mock("/url", (url, opts) => { + assert.deepEqual(opts, { + credentials: "same-origin", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + "X-CSRFToken": CSRF_TOKEN + }, + body: JSON.stringify(expectedJSON), + method: "PATCH" + }) + return { + status: 200, + body: '{"json": "here"}' + } + }) + + return fetchJSONWithCSRF("/url", { + method: "PATCH", + body: JSON.stringify(expectedJSON) + }).then(responseBody => { + assert.deepEqual(responseBody, { + json: "here" + }) + }) + }) + + it("handles responses with no data", () => { + document.cookie = `csrftoken=${CSRF_TOKEN}` + const expectedJSON = { data: true } + + fetchMock.mock("/url", (url, opts) => { + assert.deepEqual(opts, { + credentials: "same-origin", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + "X-CSRFToken": CSRF_TOKEN + }, + body: JSON.stringify(expectedJSON), + method: "PATCH" + }) + return { + status: 200 + } + }) + + return fetchJSONWithCSRF("/url", { + method: "PATCH", + body: JSON.stringify(expectedJSON) + }).then(responseBody => { + assert.deepEqual(responseBody, {}) + }) + }) + + for (const statusCode of [300, 400, 500]) { + it(`rejects the promise if the status code is ${statusCode}`, () => { + fetchMock.mock("/url", { + status: statusCode, + body: JSON.stringify({ + error: "an error" + }) + }) + + return assert + .isRejected(fetchJSONWithCSRF("/url")) + .then(responseBody => { + assert.deepEqual(responseBody, { + error: "an error", + errorStatusCode: statusCode + }) + }) + }) + } + + for (const statusCode of [400, 401]) { + it(`redirects to login if we set loginOnError and status = ${statusCode}`, () => { + fetchMock.mock("/url", () => { + return { status: statusCode } + }) + + return assert + .isRejected(fetchJSONWithCSRF("/url", {}, true)) + .then(() => { + const redirectUrl = `/logout?next=${encodeURIComponent( + "/login/edxorg/" + )}` + assert.include(window.location.toString(), redirectUrl) + }) + }) + } + }) + }) + + describe("getCookie", () => { + it("gets a cookie", () => { + document.cookie = "key=cookie" + assert.equal("cookie", getCookie("key")) + }) + + it("handles multiple cookies correctly", () => { + document.cookie = "key1=cookie1" + document.cookie = "key2=cookie2" + assert.equal("cookie1", getCookie("key1")) + assert.equal("cookie2", getCookie("key2")) + }) + it("returns null if cookie not found", () => { + assert.equal(null, getCookie("unknown")) + }) + }) + + describe("csrfSafeMethod", () => { + it("knows safe methods", () => { + for (const method of ["GET", "HEAD", "OPTIONS", "TRACE"]) { + assert.ok(csrfSafeMethod(method)) + } + }) + it("knows unsafe methods", () => { + for (const method of ["PATCH", "PUT", "DELETE", "POST"]) { + assert.ok(!csrfSafeMethod(method)) + } + }) + }) +}) diff --git a/static/js/lib/sanctuary.js b/static/js/lib/sanctuary.js new file mode 100644 index 000000000..cd4607985 --- /dev/null +++ b/static/js/lib/sanctuary.js @@ -0,0 +1,72 @@ +// @flow +import R from "ramda" +import { create, env } from "sanctuary" + +export const S = create({ checkTypes: false, env: env }) + +/* + * returns Just(items) if all items are Just, else Nothing + */ +export const allJust = R.curry((items: S.Maybe[]) => + R.all(S.isJust)(items) ? S.Just(items) : S.Nothing +) + +/* + * converts a Maybe to a string + */ +export const mstr = S.maybe("", String) + +/* + * returns Nothing if the input is undefined|null, + * else passes the input through a provided function + * (the third argument to R.ifElse) + */ +export const ifNil = R.ifElse(R.isNil, () => S.Nothing) + +/* + * wraps a function in a guard, which will return Nothing + * if any of the arguments are null || undefined, + * and otherwise will return Just(fn(...args)) + * + * Similar to S.toMaybe + * + * guard :: (a -> b) -> (a -> Maybe b) + */ +export const guard = (func: Function) => (...args: any) => { + if (R.any(R.isNil, args)) { + return S.Nothing + } else { + return S.Just(func(...args)) + } +} + +// getm :: String -> Object -> Maybe a +export const getm = R.curry((prop, obj) => S.toMaybe(R.prop(prop, obj))) + +// parseJSON :: String -> Either Object Object +// A Right value indicates the JSON parsed successfully, +// a Left value indicates the JSON was malformed (a Left contains +// an empty object) +export const parseJSON = S.encaseEither(() => ({}), JSON.parse) + +// filterE :: (Either -> Boolean) -> Either -> Either +// filterE takes a function f and an either E(v). +// if the Either is a Left, it returns it. +// if the f(v) === true, it returns, E. Else, +// if returns Left(v). +export const filterE = R.curry((predicate, either) => + S.either( + S.Left, + right => (predicate(right) ? S.Right(right) : S.Left(right)), + either + ) +) + +// reduceM :: forall a b. b -> (a -> b) -> Maybe a -> b +// this is how I think Sanctuary's `reduce` should handle a maybe +// pass a default value, a function, and a maybe +// if Nothing, return the function called with the default value +// if Just, return the function called with the value in the Just +export const reduceM = R.curry((def, fn, maybe) => + S.maybe_(() => fn(def), fn, maybe) +) diff --git a/static/js/lib/sanctuary_test.js b/static/js/lib/sanctuary_test.js new file mode 100644 index 000000000..4a13223f7 --- /dev/null +++ b/static/js/lib/sanctuary_test.js @@ -0,0 +1,171 @@ +// @flow +import { assert } from "chai" +import R from "ramda" + +import { + S, + allJust, + mstr, + ifNil, + guard, + getm, + parseJSON, + filterE, + reduceM +} from "./sanctuary" +const { Just, Nothing } = S +import { + assertMaybeEquality, + assertIsNothing, + assertIsJust +} from "./test_utils" + +const assertIsLeft = (e, val) => { + assert(e.isLeft, "should be left") + assert.deepEqual(e.value, val) +} + +const assertIsRight = (e, val) => { + assert(e.isRight, "should be right") + assert.deepEqual(e.value, val) +} + +describe("sanctuary util functions", () => { + describe("allJust", () => { + const maybes = [Just(2), Just("maybe?")] + + it("should return Just(values) if passed an array of Just values", () => { + const checked = allJust(maybes) + assert(S.isJust(checked)) + checked.value.forEach((m, i) => assertMaybeEquality(m, maybes[i])) + }) + + it("should return Nothing if passed an array with a Nothing in it", () => { + assertIsNothing(allJust(maybes.concat(Nothing))) + }) + }) + + describe("mstr", () => { + it("should print an empty string if called on Nothing", () => { + assert.equal("", mstr(Nothing)) + }) + + it("should print the value wrapped with Just", () => { + assert.equal("4", mstr(Just(4))) + assert.equal("some text", mstr(Just("some text"))) + }) + }) + + describe("ifNil", () => { + it("returns Nothing if the input is undefined", () => { + assertIsNothing(ifNil(x => x)(undefined)) + }) + + it("returns Nothing if the input is null", () => { + assertIsNothing(ifNil(x => x)(null)) + }) + + it("return func(input) if the input is not nil", () => { + const result = ifNil(x => x)("test input") + assert.equal("test input", result) + }) + }) + + describe("guard", () => { + const wrappedFunc = guard(x => x + 2) + const wrappedRestFunc = guard((x, y, z) => [x, y, z]) + + it("takes a function and returns a function", () => { + assert.isFunction(wrappedFunc) + assert.isFunction(wrappedRestFunc) + }) + + it("returns a function that returns Nothing if any arguments are nil", () => { + assertIsNothing(wrappedFunc(null)) + assertIsNothing(wrappedFunc(undefined)) + }) + + it("returns a function that returns the Just(fn(args)) if no args are undefined", () => { + assertIsJust(wrappedFunc(2), 4) + }) + + it("accepts rest parameters", () => { + assertIsJust(wrappedRestFunc(1, 2, 3), [1, 2, 3]) + }) + + it("returns Nothing if any rest parameter arg is undefined", () => { + const args = [1, 2, 3] + ;[null, undefined].forEach(nilVal => { + for (let i = 0; i < 3; i++) { + assertIsNothing(wrappedRestFunc(...R.update(i, nilVal, args))) + } + }) + }) + }) + + describe("getm", () => { + it("returns Nothing if a prop is not present", () => { + assertIsNothing(getm("prop", {})) + }) + ;[null, undefined].forEach(nil => { + // $FlowFixMe + it(`returns Nothing if a prop is ${nil}`, () => { + assertIsNothing(getm("prop", { prop: nil })) + }) + }) + + it("returns Just(val) if a prop is present", () => { + assertIsJust(getm("prop", { prop: "HI" }), "HI") + }) + }) + + describe("parseJSON", () => { + it("returns Left({}) if handed bad JSON", () => { + assertIsLeft(parseJSON(""), {}) + assertIsLeft(parseJSON("[[[["), {}) + assertIsLeft(parseJSON("@#R@#FASDF"), {}) + }) + + it("returns Right(Object) if handed good JSON", () => { + const testObj = { + foo: ["bar", "baz"] + } + assertIsRight(parseJSON(JSON.stringify(testObj)), testObj) + }) + }) + + describe("filterE", () => { + const left = S.Left(2) + const right = S.Right(4) + it("returns a Left if passed one, regardless of predicate", () => { + assertIsLeft(filterE(x => x === 2, left), 2) + assertIsLeft(filterE(x => x !== 2, left), 2) + }) + + it("returns a Left if predicate(right.value) === false", () => { + assertIsLeft(filterE(x => x === 2, right), 4) + assertIsLeft(filterE(R.isNil, right), 4) + }) + + it("returns a Right if predicate(right.value) === true", () => { + assertIsRight(filterE(x => x === 4, right), 4) + assertIsRight(filterE(x => x % 2 === 0, right), 4) + }) + }) + + describe("reduceM", () => { + it("returns fn(val) where maybe is Just(val)", () => { + assert.equal( + reduceM("default", str => `${str} value`, S.Just("maybe")), + "maybe value" + ) + }) + + it("returns fn(default) where maybe is Nothing", () => { + assert.equal( + reduceM("default", str => `${str} value`, S.Nothing), + "default value" + ) + }) + }) +}) diff --git a/static/js/lib/test_utils.js b/static/js/lib/test_utils.js new file mode 100644 index 000000000..c754a1c67 --- /dev/null +++ b/static/js/lib/test_utils.js @@ -0,0 +1,18 @@ +// @flow +import { assert } from "chai" + +import { S } from "./sanctuary" +const { Maybe } = S + +export const assertMaybeEquality = (m1: Maybe, m2: Maybe) => { + assert(S.equals(m1, m2), `expected ${m1.value} to equal ${m2.value}`) +} + +export const assertIsNothing = (m: Maybe) => { + assert(m.isNothing, `should be nothing, is ${m}`) +} + +export const assertIsJust = (m: Maybe, val: any) => { + assert(m.isJust, `should be Just(${val}), is ${m}`) + assert.deepEqual(m.value, val) +} diff --git a/static/js/reducers/index.js b/static/js/reducers/index.js new file mode 100644 index 000000000..85e40edca --- /dev/null +++ b/static/js/reducers/index.js @@ -0,0 +1,30 @@ +// @flow +import { combineReducers } from "redux" +import type { Action } from "../flow/reduxTypes" +import { UPDATE_CHECKBOX } from "../actions" + +export type CheckboxType = { + checked: boolean +} + +const INITIAL_CHECKBOX_STATE: CheckboxType = { + checked: false +} + +export const checkbox = ( + state: CheckboxType = INITIAL_CHECKBOX_STATE, + action: Action +): CheckboxType => { + switch (action.type) { + case UPDATE_CHECKBOX: + return Object.assign({}, state, { + checked: action.payload.checked + }) + default: + return state + } +} + +export default combineReducers({ + checkbox +}) diff --git a/static/js/store/configureStore.js b/static/js/store/configureStore.js new file mode 100644 index 000000000..188e76a7e --- /dev/null +++ b/static/js/store/configureStore.js @@ -0,0 +1,33 @@ +/* global require:false, module:false */ +import { compose, createStore, applyMiddleware } from "redux" +import thunkMiddleware from "redux-thunk" +import { createLogger } from "redux-logger" + +import rootReducer from "../reducers" + +let createStoreWithMiddleware +if (process.env.NODE_ENV !== "production") { + createStoreWithMiddleware = compose( + applyMiddleware(thunkMiddleware, createLogger()), + window.devToolsExtension ? window.devToolsExtension() : f => f + )(createStore) +} else { + createStoreWithMiddleware = compose(applyMiddleware(thunkMiddleware))( + createStore + ) +} + +export default function configureStore(initialState: Object) { + const store = createStoreWithMiddleware(rootReducer, initialState) + + if (module.hot) { + // Enable Webpack hot module replacement for reducers + module.hot.accept("../reducers", () => { + const nextRootReducer = require("../reducers") + + store.replaceReducer(nextRootReducer) + }) + } + + return store +} diff --git a/static/js/util/integration_test_helper.js b/static/js/util/integration_test_helper.js new file mode 100644 index 000000000..beeaf4e44 --- /dev/null +++ b/static/js/util/integration_test_helper.js @@ -0,0 +1,85 @@ +/* global SETTINGS: false */ +import React from "react" +import { mount } from "enzyme" +import sinon from "sinon" +import { createMemoryHistory } from "history" +import configureTestStore from "redux-asserts" + +import Router, { routes } from "../Router" +import rootReducer from "../reducers" +import type { Action } from "../flow/reduxTypes" +import type { TestStore } from "../flow/reduxTypes" +import type { Sandbox } from "../flow/sinonTypes" + +export default class IntegrationTestHelper { + listenForActions: (a: Array, f: Function) => Promise<*> + dispatchThen: (a: Action) => Promise<*> + sandbox: Sandbox + store: TestStore + browserHistory: History + + constructor() { + this.sandbox = sinon.createSandbox({}) + this.store = configureTestStore((...args) => { + // uncomment to listen on dispatched actions + // console.log(args); + return rootReducer(...args) + }) + + this.listenForActions = this.store.createListenForActions() + this.dispatchThen = this.store.createDispatchThen() + + this.scrollIntoViewStub = this.sandbox.stub() + window.HTMLDivElement.prototype.scrollIntoView = this.scrollIntoViewStub + window.HTMLFieldSetElement.prototype.scrollIntoView = this.scrollIntoViewStub + this.browserHistory = createMemoryHistory() + this.currentLocation = null + this.browserHistory.listen(url => { + this.currentLocation = url + }) + } + + cleanup() { + this.sandbox.restore() + } + + /** + * Renders the components using the given URL. + * @param url {String} The react-router URL + * @param typesToAssert {Array|null} A list of redux actions to listen for. + * If null, actions types for the success case is assumed. + * @returns {Promise<*>} A promise which provides [wrapper, div] on success + */ + renderComponent( + url: string = "/", + typesToAssert: Array | null = null + ): Promise<*> { + let expectedTypes = [] + if (typesToAssert === null) { + expectedTypes = [] + } else { + expectedTypes = typesToAssert + } + + let wrapper, div + + return this.listenForActions(expectedTypes, () => { + this.browserHistory.push(url) + div = document.createElement("div") + div.setAttribute("id", "integration_test_div") + document.body.appendChild(div) + wrapper = mount( +
+ + {routes} + +
, + { + attachTo: div + } + ) + }).then(() => { + return Promise.resolve([wrapper, div]) + }) + } +} diff --git a/static/js/util/withTracker.js b/static/js/util/withTracker.js new file mode 100644 index 000000000..8c396ad77 --- /dev/null +++ b/static/js/util/withTracker.js @@ -0,0 +1,26 @@ +// @flow +/* global SETTINGS: false */ + +// From https://github.com/ReactTraining/react-router/issues/4278#issuecomment-299692502 +import React from "react" +import ga from "react-ga" + +const withTracker = (WrappedComponent: Class>) => { + const debug = SETTINGS.reactGaDebug === "true" + + if (SETTINGS.gaTrackingID) { + ga.initialize(SETTINGS.gaTrackingID, { debug: debug }) + } + + const HOC = (props: Object) => { + const page = props.location.pathname + if (SETTINGS.gaTrackingID) { + ga.pageview(page) + } + return + } + + return HOC +} + +export default withTracker diff --git a/static/robots.txt b/static/robots.txt new file mode 100644 index 000000000..eb0536286 --- /dev/null +++ b/static/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/static/scss/layout.scss b/static/scss/layout.scss new file mode 100644 index 000000000..8ac7ad7ee --- /dev/null +++ b/static/scss/layout.scss @@ -0,0 +1,8 @@ +// Layout goes here + +// Fake style to confirm that webpack dev server works +h1 { + border-radius: 25px; + border: 2px solid blue; + padding: 2px; +} diff --git a/test_requirements.txt b/test_requirements.txt new file mode 100644 index 000000000..45e12e74e --- /dev/null +++ b/test_requirements.txt @@ -0,0 +1,21 @@ +astroid==2.0.4 +black==18.9b0 +bpython +codecov +django-debug-toolbar +ipdb +nplusone>=0.8.1 +pdbpp +pip-tools +pylint==2.1.1 +pylint-django==2.0.2 +pytest +pytest-cov>=2.6.1 +pytest-django +pytest-pep8 +pytest-mock +pytest-pylint==0.12.3 +pytest-lazy-fixture==0.4.2 +safety +semantic-version +tox diff --git a/tox.ini b/tox.ini new file mode 100644 index 000000000..e1302b234 --- /dev/null +++ b/tox.ini @@ -0,0 +1,35 @@ +[tox] +envlist = py36 +skipsdist = True + +[testenv] +sitepackages = True +deps = + -r{toxinidir}/requirements.txt + -r{toxinidir}/test_requirements.txt +commands = + {toxinidir}/scripts/test/check_pip.sh + black --check . + py.test {posargs} + {toxinidir}/travis/codecov_python.sh + {toxinidir}/scripts/test/detect_missing_migrations.sh + {toxinidir}/scripts/test/no_auto_migrations.sh + +passenv = + COVERAGE_DIR + DATABASE_URL + CELERY_BROKER_URL + CELERY_RESULT_BACKEND + PORT + TRAVIS* + CI +setenv = + DEBUG=False + CELERY_TASK_ALWAYS_EAGER=True + SENTRY_DSN= + DISABLE_WEBPACK_LOADER_STATS=True + MITXPRO_DB_DISABLE_SSL=True + MITXPRO_SECURE_SSL_REDIRECT=False + MAILGUN_SENDER_DOMAIN=other.fake.site + MAILGUN_KEY=fake_mailgun_key + SECRET_KEY=not_very_secret_key diff --git a/travis/Dockerfile-travis-watch b/travis/Dockerfile-travis-watch new file mode 100644 index 000000000..7338bad17 --- /dev/null +++ b/travis/Dockerfile-travis-watch @@ -0,0 +1,21 @@ +FROM mitodl/mitxpro_watch_travis + +WORKDIR /src + +COPY package.json /src + +COPY yarn.lock /src + +ADD ./webpack_if_prod.sh /src + +USER node + +RUN yarn install --frozen-lockfile + +COPY . /src + +USER root + +RUN chown -R node:node /src + +USER node diff --git a/travis/Dockerfile-travis-watch-build b/travis/Dockerfile-travis-watch-build new file mode 100644 index 000000000..2638337ca --- /dev/null +++ b/travis/Dockerfile-travis-watch-build @@ -0,0 +1,28 @@ +FROM node:9.3 +LABEL maintainer "ODL DevOps " + +# this dockerfile builds the hub image for the watch container +# we don't use this directly, instead we push to docker-hub, and +# then Dockerfile-travis-watch uses that pushed image to bootstrap +# itself + +RUN apt-get update && apt-get install libelf1 + +RUN mkdir /src + +WORKDIR /src + +COPY package.json yarn.lock ./webpack_if_prod.sh /src/ +COPY scripts /src/scripts + +RUN node /src/scripts/install_yarn.js + +RUN chown -R node:node /src + +USER node + +# this is just to get a warm cache, we delete node_modules afterwards to +# avoid issues with native extensions (mainly node-sass) +RUN yarn install --frozen-lockfile + +RUN rm -rf node_modules diff --git a/travis/Dockerfile-travis-web b/travis/Dockerfile-travis-web new file mode 100644 index 000000000..4f4a588c2 --- /dev/null +++ b/travis/Dockerfile-travis-web @@ -0,0 +1,21 @@ +FROM mitodl/mitxpro_web_travis_next + +WORKDIR /tmp + +USER root + +COPY requirements.txt /tmp/requirements.txt +COPY test_requirements.txt /tmp/test_requirements.txt +RUN pip install -r requirements.txt -r test_requirements.txt + +# mitxpro_web_travis comes with a copy of the source which may not match the current copy, so we need to copy it again +RUN rm -rf /src +RUN mkdir /src + +WORKDIR /src + +COPY . /src + +RUN chown -R mitodl:mitodl /src + +USER mitodl diff --git a/travis/codecov_python.sh b/travis/codecov_python.sh new file mode 100755 index 000000000..2595b7fba --- /dev/null +++ b/travis/codecov_python.sh @@ -0,0 +1,10 @@ +#!/bin/bash +set -e -o pipefail + +if [[ ! -z "$TRAVIS_COMMIT" ]] +then + echo "Uploading coverage..." + export TRAVIS_BUILD_DIR=$PWD + coverage xml + codecov +fi diff --git a/travis/js_tests.sh b/travis/js_tests.sh new file mode 100755 index 000000000..71f2d3f3e --- /dev/null +++ b/travis/js_tests.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +status=0 + +function run_test { + "$@" + local test_status=$? + if [ $test_status -ne 0 ]; then + status=$test_status + fi + return $status +} + +run_test docker run --env-file .env -t travis-watch npm run codecov +run_test docker run --env-file .env -t travis-watch npm run lint +run_test docker run --env-file .env -t travis-watch npm run fmt:check +run_test docker run --env-file .env -t travis-watch npm run scss_lint +run_test docker run --env-file .env -t travis-watch npm run flow +run_test docker run --env-file .env -e "NODE_ENV=production" -t travis-watch ./webpack_if_prod.sh + +exit $status diff --git a/travis/update-docker-hub.sh b/travis/update-docker-hub.sh new file mode 100755 index 000000000..b062dce77 --- /dev/null +++ b/travis/update-docker-hub.sh @@ -0,0 +1,18 @@ +#!/bin/bash +set -eo pipefail + +export NEXT=$(date | md5sum | cut -c -6) +export PROJECT_NAME="mitxpro" +echo "Next hash is $NEXT" + +export WEB_IMAGE=mitodl/${PROJECT_NAME}_web_travis_${NEXT} +export WATCH_IMAGE=mitodl/${PROJECT_NAME}_watch_travis_${NEXT} + +echo docker build -t $WEB_IMAGE -f Dockerfile . +echo docker build -t $WATCH_IMAGE -f travis/Dockerfile-travis-watch-build . + +echo docker push $WEB_IMAGE +echo docker push $WATCH_IMAGE + +sed -i "s/^FROM mitodl\/.+$/$WEB_IMAGE/" travis/Dockerfile-travis-web +sed -i "s/^FROM mitodl\/.+$/$WATCH_IMAGE/" travis/Dockerfile-travis-watch diff --git a/uwsgi.ini b/uwsgi.ini new file mode 100644 index 000000000..c521519d5 --- /dev/null +++ b/uwsgi.ini @@ -0,0 +1,41 @@ +[uwsgi] +if-env = DEV_ENV +socket = :$(PORT) +endif = +if-not-env = DEV_ENV +socket = /tmp/nginx.socket +endif = +hook-accepting1 = exec:touch /tmp/app-initialized +master = true +if-env = UWSGI_PROCESS_COUNT +processes = %(_) +endif = +if-not-env = UWSGI_PROCESS_COUNT +processes = 2 +endif = +if-env = UWSGI_THREAD_COUNT +threads = %(_) +endif = +if-not-env = UWSGI_THREAD_COUNT +threads = 100 +endif = +die-on-term = true +wsgi-file = mitxpro/wsgi.py +pidfile=/tmp/mitxpro-mast.pid +vacuum=True +enable-threads = true +single-interpreter = true +offload-threads = 2 +thunder-lock = +if-env = DEV_ENV +python-autoreload = 1 +endif = +if-not-env = DEV_ENV +memory-report = true +endif = +if-env = UWSGI_SOCKET_TIMEOUT +socket-timeout = %(_) +endif = +if-not-env = UWSGI_SOCKET_TIMEOUT +socket-timeout = 3 +endif = diff --git a/webpack.config.dev.js b/webpack.config.dev.js new file mode 100644 index 000000000..a0b6541d5 --- /dev/null +++ b/webpack.config.dev.js @@ -0,0 +1,59 @@ +var webpack = require('webpack'); +var path = require("path"); +var R = require('ramda'); +var BundleTracker = require('webpack-bundle-tracker'); +const { config, babelSharedLoader } = require(path.resolve("./webpack.config.shared.js")); + +const hotEntry = (host, port) => ( + `webpack-hot-middleware/client?path=http://${host}:${port}/__webpack_hmr&timeout=20000&reload=true` +); + +const insertHotReload = (host, port, entries) => ( + R.map(R.compose(R.flatten, v => [v].concat(hotEntry(host, port))), entries) +); + +const devConfig = Object.assign({}, config, { + context: __dirname, + output: { + path: path.resolve('./static/bundles/'), + filename: "[name].js" + }, + plugins: [ + new webpack.DefinePlugin({ + 'process.env': { + 'NODE_ENV': '"development"' + } + }), + new webpack.optimize.CommonsChunkPlugin({ + name: 'common', + minChunks: 2, + }), + new webpack.HotModuleReplacementPlugin(), + new webpack.NamedModulesPlugin(), + new webpack.NoEmitOnErrorsPlugin(), + new BundleTracker({filename: './webpack-stats.json'}) + ], + devtool: 'source-map' +}); + +devConfig.module.rules = [ + babelSharedLoader, + ...config.module.rules, + { + test: /\.css$|\.scss$/, + use: [ + { loader: 'style-loader' }, + { loader: 'css-loader' }, + { loader: 'postcss-loader' }, + { loader: 'sass-loader' }, + ] + }, +]; + +const makeDevConfig = (host, port) => ( + Object.assign({}, devConfig, { + entry: insertHotReload(host, port, devConfig.entry), + }) +); + +module.exports = makeDevConfig; diff --git a/webpack.config.prod.js b/webpack.config.prod.js new file mode 100644 index 000000000..ab6bd3d66 --- /dev/null +++ b/webpack.config.prod.js @@ -0,0 +1,66 @@ +var webpack = require('webpack'); +var path = require("path"); +var BundleTracker = require('webpack-bundle-tracker'); +const ExtractTextPlugin = require("extract-text-webpack-plugin"); +const { config, babelSharedLoader } = require(path.resolve("./webpack.config.shared.js")); + +const prodBabelConfig = Object.assign({}, babelSharedLoader); + +prodBabelConfig.query.plugins.push( + "transform-react-constant-elements", + "transform-react-inline-elements" +); + +const prodConfig = Object.assign({}, config); +prodConfig.module.rules = [ + prodBabelConfig, + ...config.module.rules, + { + test: /\.css$|\.scss$/, + use: ExtractTextPlugin.extract({ + fallback: 'style-loader', + use: ['css-loader', 'postcss-loader', 'sass-loader'], + }) + } +]; + +module.exports = Object.assign(prodConfig, { + context: __dirname, + output: { + path: path.resolve('./static/bundles/'), + filename: "[name]-[chunkhash].js", + chunkFilename: "[id]-[chunkhash].js", + crossOriginLoading: "anonymous", + }, + + plugins: [ + new webpack.DefinePlugin({ + 'process.env': { + 'NODE_ENV': '"production"' + } + }), + new webpack.optimize.UglifyJsPlugin({ + compress: { + warnings: false + }, + sourceMap: true, + }), + new webpack.optimize.CommonsChunkPlugin({ + name: 'common', + minChunks: 2, + }), + new BundleTracker({ + filename: './webpack-stats.json' + }), + new webpack.LoaderOptionsPlugin({ + minimize: true + }), + new webpack.optimize.AggressiveMergingPlugin(), + new ExtractTextPlugin({ + filename: "[name]-[contenthash].css", + allChunks: true, + ignoreOrder: false, + }) + ], + devtool: 'source-map' +}); diff --git a/webpack.config.shared.js b/webpack.config.shared.js new file mode 100644 index 000000000..49d65e49b --- /dev/null +++ b/webpack.config.shared.js @@ -0,0 +1,50 @@ +const path = require("path"); +const webpack = require("webpack"); + +module.exports = { + config: { + entry: { + 'root': ['babel-polyfill', './static/js/entry/root'], + 'style': './static/js/entry/style', + }, + module: { + rules: [ + { + test: /\.(svg|ttf|woff|woff2|eot|gif)$/, + use: 'url-loader' + }, + ] + }, + resolve: { + modules: [ + path.join(__dirname, "static/js"), + "node_modules" + ], + extensions: ['.js', '.jsx'], + }, + performance: { + hints: false + } + }, + babelSharedLoader: { + test: /\.jsx?$/, + exclude: /node_modules/, + loader: 'babel-loader', + query: { + "presets": [ + ["env", { "modules": false }], + "react", + ], + "ignore": [ + "node_modules/**" + ], + "plugins": [ + "transform-flow-strip-types", + "react-hot-loader/babel", + "transform-object-rest-spread", + "transform-class-properties", + "syntax-dynamic-import", + ] + } + }, +}; diff --git a/webpack_dev_server.sh b/webpack_dev_server.sh new file mode 100755 index 000000000..49a03bc33 --- /dev/null +++ b/webpack_dev_server.sh @@ -0,0 +1,11 @@ +#!/bin/bash +set -ef -o pipefail + +WEBPACK_HOST='0.0.0.0' +WEBPACK_PORT='8052' + +if [[ "$1" == "--install" ]] ; then +yarn install --frozen-lockfile && echo "Finished yarn install" +fi +# Start the webpack dev server on the appropriate host and port +node ./hot-reload-dev-server.js --host "$WEBPACK_HOST" --port "$WEBPACK_PORT" diff --git a/webpack_if_prod.sh b/webpack_if_prod.sh new file mode 100755 index 000000000..67340508c --- /dev/null +++ b/webpack_if_prod.sh @@ -0,0 +1,6 @@ +#!/bin/bash +set -ef -o pipefail +if [[ "$NODE_ENV" != "" && "$NODE_ENV" != "development" ]] +then + node node_modules/webpack/bin/webpack.js --config webpack.config.prod.js --bail +fi diff --git a/with_host.sh b/with_host.sh new file mode 100755 index 000000000..a85073506 --- /dev/null +++ b/with_host.sh @@ -0,0 +1,7 @@ +#!/bin/bash +set -e + +export HOST_IP=$(netstat -nr | grep ^0\.0\.0\.0 | awk "{print \$2}") + +# Execute passed in arguments +$@