diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..08b249c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +* +!accounts +!assets +!dancingtogether +!frontend +!main +!radio +!static +!templates +!manage.py +!Pipfile +!Pipfile.lock diff --git a/.gitignore b/.gitignore index 9544b04..6874f58 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Python *pyc .mypy_cache +.python-version ## Django staticfiles diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..b622a9d --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,16 @@ +exclude: ^(accounts/migrations/|radio/migrations) +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.3.0 + hooks: + - id: check-toml + - id: check-yaml + - id: debug-statements + - id: end-of-file-fixer + - id: trailing-whitespace + - repo: https://github.com/psf/black + rev: stable + hooks: + - id: black + language_version: python3.8 + exclude: migrations diff --git a/Dockerfile b/Dockerfile index 667730d..ace5a0d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,16 +1,23 @@ -FROM python:3.7 +FROM python:3.8.1 ENV PYTHONUNBUFFERED 1 -RUN curl -sL https://deb.nodesource.com/setup_10.x | bash - -RUN apt-get install -y nodejs -RUN npm install -g webpack webpack-bundle-tracker typescript ts-loader +RUN ["/bin/bash", "-c", "set -o pipefail && curl -sL https://deb.nodesource.com/setup_10.x | bash -"] +# Update and install dependencies, and remove the package manager cache. Do +# this in a single step for better caching. +RUN apt-get --yes update && apt-get install --yes --no-install-recommends \ + libpq-dev \ + nodejs \ + && rm -rf /var/lib/apt/lists/* +RUN pip install pipenv +RUN npm install --global \ + ts-loader \ + typescript \ + webpack \ + webpack-bundle-tracker -RUN set -ex && mkdir /app +RUN mkdir /app +COPY Pipfile Pipfile.lock /app/ WORKDIR /app - -ADD Pipfile /app -ADD Pipfile.lock /app -RUN set -ex && pip install pipenv --upgrade RUN set -ex && pipenv install --dev --system --ignore-pipfile --deploy -ADD . /app +COPY . /app diff --git a/Makefile b/Makefile index bdc2229..aadf57b 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ build: .PHONY: check check: cd frontend && npx tslint --project . - pipenv run yapf --recursive . --exclude '*/migrations/*' --in-place --parallel + pipenv run black --check . bash -c "pipenv run mypy --ignore-missing-imports \$$(find . -name \*.py ! -path '*/migrations/*')" find . -name '*.py' ! -path '*/migrations/*' -print0 | xargs pipenv run pylint --load-plugins pylint_django @@ -64,7 +64,7 @@ test-client-watch: .PHONY: test-server test-server: - DJANGO_SETTINGS_MODULE=dancingtogether.settings.test pipenv run python3 manage.py test + tools/scripts/test_backend .PHONY: deploy deploy: diff --git a/Pipfile b/Pipfile index c9d9d18..2125fde 100644 --- a/Pipfile +++ b/Pipfile @@ -14,17 +14,21 @@ django-webpack-loader = "*" djangorestframework = "*" drf-nested-routers = "*" raven = "*" -"psycopg2-binary" = "*" +psycopg2 = "*" [requires] -python_version = "3.7" +python_version = "3.8" [dev-packages] pycodestyle = "*" mypy = "*" -yapf = "*" pylint = "*" pytest-django = "*" pytest-asyncio = "*" python-dateutil = "*" pylint-django = "*" +pre-commit = "*" +black = "*" + +[pipenv] +allow_prereleases = true diff --git a/Pipfile.lock b/Pipfile.lock index 8c58089..17d3e43 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,123 +1,131 @@ { "_meta": { "hash": { - "sha256": "eede57258fbdfc03624e61b173178b76b3373e2c2e53adb96ad88e2bcf01b28a" + "sha256": "227c5a98604bb5fdf6ba774541cdc79eaef93c115150f4afcecbab6fa2d080e3" }, "pipfile-spec": 6, "requires": { - "python_version": "3.7" - }, - "sources": [{ - "name": "pypi", - "url": "https://pypi.python.org/simple", - "verify_ssl": true - }] + "python_version": "3.8" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] }, "default": { "aioredis": { "hashes": [ - "sha256:84d62be729beb87118cf126c20b0e3f52d7a42bb7373dc5bcdd874f26f1f251a", - "sha256:aee16aa5cb3f636cf8fa0e2b62d2f6abc90366e19b5c30e94a5471d834a55975" + "sha256:15f8af30b044c771aee6787e5ec24694c048184c7b9e54c3b60c750a4b93273a", + "sha256:b61808d7e97b7cd5a92ed574937a079c9387fdadd22bfbfa7ad2fd319ecc26e3" ], - "version": "==1.2.0" + "version": "==1.3.1" }, "asgiref": { "hashes": [ - "sha256:865b7ccce5a6e815607b08d9059fe9c058cd75c77f896f5e0b74ff6c1ba81818", - "sha256:b718a9d35e204a96e2456c2271b0ef12e36124c363b3a8fd1d626744f23192aa" + "sha256:7e06d934a7718bf3975acbf87780ba678957b87c7adc056f13b6215d610695a0", + "sha256:ea448f92fc35a0ef4b1508f53a04c4670255a3f33d22a81c8fc9c872036adbe5" ], - "version": "==3.1.4" - }, - "asn1crypto": { - "hashes": [ - "sha256:2f1adbb7546ed199e3c90ef23ec95c5cf3585bac7d11fb7eb562a3fe89c64e87", - "sha256:9d5c20441baf0cb60a4ac34cc447c6c189024b6b4c6cd7877034f4965c464e49" - ], - "version": "==0.24.0" + "version": "==3.2.3" }, "async-timeout": { "hashes": [ - "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", - "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" + "sha256:0b0e3079aaca061347d171961953782c97f7b9c5eb29310eb8e5150f7931dcba", + "sha256:9eefb0f98094f3e59e1d55fd782e048494ea27e046ff0ddca63a3b5f08f17db3" ], - "version": "==3.0.1" + "version": "==4.0.0a2" }, "attrs": { "hashes": [ - "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", - "sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399" + "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", + "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" ], - "version": "==19.1.0" + "version": "==19.3.0" }, "autobahn": { "hashes": [ - "sha256:70f0cfb8005b5429df5709acf5d66a8eba00669765547029371648dffd4a0470", - "sha256:89f94a1535673b1655df28ef91e96b7f34faea76da04a5e56441c9ac779a2f9f" + "sha256:64fa063b3a1ab16588037d4713f13f66167f7ad2a2e95fd675decbc3bc85c089", + "sha256:986e284d32cefc7174849bf2ae3fe8999ce9903b5c455bce4623f119eb9bea15" ], - "version": "==19.7.1" + "version": "==19.11.2" }, "automat": { "hashes": [ - "sha256:cbd78b83fa2d81fe2a4d23d258e1661dd7493c9a50ee2f1a5b2cac61c1793b0e", - "sha256:fdccab66b68498af9ecfa1fa43693abe546014dd25cf28543cbe9d1334916a58" + "sha256:269a09dfb063a3b078983f4976d83f0a0d3e6e7aaf8e27d8df1095e09dc4a484", + "sha256:81c93c55d2742c55e74e6497a48e048a859fa01d7aa0b91a032be432229837e2" ], - "version": "==0.7.0" + "version": "==0.8.0" + }, + "beautifulsoup4": { + "hashes": [ + "sha256:05fd825eb01c290877657a56df4c6e4c311b3965bda790c613a3d6fb01a5462a", + "sha256:9fbb4d6e48ecd30bcacc5b63b94088192dcda178513b2ae3c394229f8911b887", + "sha256:e1505eeed31b0f4ce2dbb3bc8eb256c04cc2b3b72af7d551a4ab6efd5cbe5dae" + ], + "version": "==4.8.2" }, "certifi": { "hashes": [ - "sha256:046832c04d4e752f37383b628bc601a7ea7211496b4638f6514d0e5b9acc4939", - "sha256:945e3ba63a0b9f577b1395204e13c3a231f9bc0223888be653286534e5873695" + "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3", + "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f" ], - "version": "==2019.6.16" + "version": "==2019.11.28" }, "cffi": { "hashes": [ - "sha256:041c81822e9f84b1d9c401182e174996f0bae9991f33725d059b771744290774", - "sha256:046ef9a22f5d3eed06334d01b1e836977eeef500d9b78e9ef693f9380ad0b83d", - "sha256:066bc4c7895c91812eff46f4b1c285220947d4aa46fa0a2651ff85f2afae9c90", - "sha256:066c7ff148ae33040c01058662d6752fd73fbc8e64787229ea8498c7d7f4041b", - "sha256:2444d0c61f03dcd26dbf7600cf64354376ee579acad77aef459e34efcb438c63", - "sha256:300832850b8f7967e278870c5d51e3819b9aad8f0a2c8dbe39ab11f119237f45", - "sha256:34c77afe85b6b9e967bd8154e3855e847b70ca42043db6ad17f26899a3df1b25", - "sha256:46de5fa00f7ac09f020729148ff632819649b3e05a007d286242c4882f7b1dc3", - "sha256:4aa8ee7ba27c472d429b980c51e714a24f47ca296d53f4d7868075b175866f4b", - "sha256:4d0004eb4351e35ed950c14c11e734182591465a33e960a4ab5e8d4f04d72647", - "sha256:4e3d3f31a1e202b0f5a35ba3bc4eb41e2fc2b11c1eff38b362de710bcffb5016", - "sha256:50bec6d35e6b1aaeb17f7c4e2b9374ebf95a8975d57863546fa83e8d31bdb8c4", - "sha256:55cad9a6df1e2a1d62063f79d0881a414a906a6962bc160ac968cc03ed3efcfb", - "sha256:5662ad4e4e84f1eaa8efce5da695c5d2e229c563f9d5ce5b0113f71321bcf753", - "sha256:59b4dc008f98fc6ee2bb4fd7fc786a8d70000d058c2bbe2698275bc53a8d3fa7", - "sha256:73e1ffefe05e4ccd7bcea61af76f36077b914f92b76f95ccf00b0c1b9186f3f9", - "sha256:a1f0fd46eba2d71ce1589f7e50a9e2ffaeb739fb2c11e8192aa2b45d5f6cc41f", - "sha256:a2e85dc204556657661051ff4bab75a84e968669765c8a2cd425918699c3d0e8", - "sha256:a5457d47dfff24882a21492e5815f891c0ca35fefae8aa742c6c263dac16ef1f", - "sha256:a8dccd61d52a8dae4a825cdbb7735da530179fea472903eb871a5513b5abbfdc", - "sha256:ae61af521ed676cf16ae94f30fe202781a38d7178b6b4ab622e4eec8cefaff42", - "sha256:b012a5edb48288f77a63dba0840c92d0504aa215612da4541b7b42d849bc83a3", - "sha256:d2c5cfa536227f57f97c92ac30c8109688ace8fa4ac086d19d0af47d134e2909", - "sha256:d42b5796e20aacc9d15e66befb7a345454eef794fdb0737d1af593447c6c8f45", - "sha256:dee54f5d30d775f525894d67b1495625dd9322945e7fee00731952e0368ff42d", - "sha256:e070535507bd6aa07124258171be2ee8dfc19119c28ca94c9dfb7efd23564512", - "sha256:e1ff2748c84d97b065cc95429814cdba39bcbd77c9c85c89344b317dc0d9cbff", - "sha256:ed851c75d1e0e043cbf5ca9a8e1b13c4c90f3fbd863dacb01c0808e2b5204201" - ], - "version": "==1.12.3" + "sha256:0b49274afc941c626b605fb59b59c3485c17dc776dc3cc7cc14aca74cc19cc42", + "sha256:0e3ea92942cb1168e38c05c1d56b0527ce31f1a370f6117f1d490b8dcd6b3a04", + "sha256:135f69aecbf4517d5b3d6429207b2dff49c876be724ac0c8bf8e1ea99df3d7e5", + "sha256:19db0cdd6e516f13329cba4903368bff9bb5a9331d3410b1b448daaadc495e54", + "sha256:2781e9ad0e9d47173c0093321bb5435a9dfae0ed6a762aabafa13108f5f7b2ba", + "sha256:291f7c42e21d72144bb1c1b2e825ec60f46d0a7468f5346841860454c7aa8f57", + "sha256:2c5e309ec482556397cb21ede0350c5e82f0eb2621de04b2633588d118da4396", + "sha256:2e9c80a8c3344a92cb04661115898a9129c074f7ab82011ef4b612f645939f12", + "sha256:32a262e2b90ffcfdd97c7a5e24a6012a43c61f1f5a57789ad80af1d26c6acd97", + "sha256:3c9fff570f13480b201e9ab69453108f6d98244a7f495e91b6c654a47486ba43", + "sha256:415bdc7ca8c1c634a6d7163d43fb0ea885a07e9618a64bda407e04b04333b7db", + "sha256:42194f54c11abc8583417a7cf4eaff544ce0de8187abaf5d29029c91b1725ad3", + "sha256:4424e42199e86b21fc4db83bd76909a6fc2a2aefb352cb5414833c030f6ed71b", + "sha256:4a43c91840bda5f55249413037b7a9b79c90b1184ed504883b72c4df70778579", + "sha256:599a1e8ff057ac530c9ad1778293c665cb81a791421f46922d80a86473c13346", + "sha256:5c4fae4e9cdd18c82ba3a134be256e98dc0596af1e7285a3d2602c97dcfa5159", + "sha256:5ecfa867dea6fabe2a58f03ac9186ea64da1386af2159196da51c4904e11d652", + "sha256:62f2578358d3a92e4ab2d830cd1c2049c9c0d0e6d3c58322993cc341bdeac22e", + "sha256:6471a82d5abea994e38d2c2abc77164b4f7fbaaf80261cb98394d5793f11b12a", + "sha256:6d4f18483d040e18546108eb13b1dfa1000a089bcf8529e30346116ea6240506", + "sha256:71a608532ab3bd26223c8d841dde43f3516aa5d2bf37b50ac410bb5e99053e8f", + "sha256:74a1d8c85fb6ff0b30fbfa8ad0ac23cd601a138f7509dc617ebc65ef305bb98d", + "sha256:7b93a885bb13073afb0aa73ad82059a4c41f4b7d8eb8368980448b52d4c7dc2c", + "sha256:7d4751da932caaec419d514eaa4215eaf14b612cff66398dd51129ac22680b20", + "sha256:7f627141a26b551bdebbc4855c1157feeef18241b4b8366ed22a5c7d672ef858", + "sha256:8169cf44dd8f9071b2b9248c35fc35e8677451c52f795daa2bb4643f32a540bc", + "sha256:aa00d66c0fab27373ae44ae26a66a9e43ff2a678bf63a9c7c1a9a4d61172827a", + "sha256:ccb032fda0873254380aa2bfad2582aedc2959186cce61e3a17abc1a55ff89c3", + "sha256:d754f39e0d1603b5b24a7f8484b22d2904fa551fe865fd0d4c3332f078d20d4e", + "sha256:d75c461e20e29afc0aee7172a0950157c704ff0dd51613506bd7d82b718e7410", + "sha256:dcd65317dd15bc0451f3e01c80da2216a31916bdcffd6221ca1202d96584aa25", + "sha256:e570d3ab32e2c2861c4ebe6ffcad6a8abf9347432a37608fe1fbd157b3f0036b", + "sha256:fd43a88e045cf992ed09fa724b5315b790525f2676883a6ea64e3263bae6549d" + ], + "version": "==1.13.2" }, "channels": { "hashes": [ - "sha256:9191a85800673b790d1d74666fb7676f430600b71b662581e97dd69c9aedd29a", - "sha256:af7cdba9efb3f55b939917d1b15defb5d40259936013e60660e5e9aff98db4c5" + "sha256:08e756406d7165cb32f6fc3090c0643f41ca9f7e0f7fada0b31194662f20f414", + "sha256:80a5ad1962ae039a3dcc0a5cb5212413e66e2f11ad9e9db8004834436daf3400" ], "index": "pypi", - "version": "==2.2.0" + "version": "==2.4.0" }, "channels-redis": { "hashes": [ - "sha256:0e25f9a7851690a2433948ec24c899628c1f989d9602bbbcf48640412138b9c9", - "sha256:b7ccbcb2fd4e568c08c147891b26db59aa25d88b65af826ce7f70c815cfb91bc" + "sha256:ddfa0c067085fdce24fb80d9c0b848638cbdbf0e1167f14eb2e99d635ad216e6", + "sha256:de3ecced30b43df61bc944b84d45ebbaf008fdf1a4154fc8ee7c9c6e78f00a4c" ], "index": "pypi", - "version": "==2.4.0" + "version": "==2.4.1" }, "chardet": { "hashes": [ @@ -135,31 +143,36 @@ }, "cryptography": { "hashes": [ - "sha256:24b61e5fcb506424d3ec4e18bca995833839bf13c59fc43e530e488f28d46b8c", - "sha256:25dd1581a183e9e7a806fe0543f485103232f940fcfc301db65e630512cce643", - "sha256:3452bba7c21c69f2df772762be0066c7ed5dc65df494a1d53a58b683a83e1216", - "sha256:41a0be220dd1ed9e998f5891948306eb8c812b512dc398e5a01846d855050799", - "sha256:5751d8a11b956fbfa314f6553d186b94aa70fdb03d8a4d4f1c82dcacf0cbe28a", - "sha256:5f61c7d749048fa6e3322258b4263463bfccefecb0dd731b6561cb617a1d9bb9", - "sha256:72e24c521fa2106f19623a3851e9f89ddfdeb9ac63871c7643790f872a305dfc", - "sha256:7b97ae6ef5cba2e3bb14256625423413d5ce8d1abb91d4f29b6d1a081da765f8", - "sha256:961e886d8a3590fd2c723cf07be14e2a91cf53c25f02435c04d39e90780e3b53", - "sha256:96d8473848e984184b6728e2c9d391482008646276c3ff084a1bd89e15ff53a1", - "sha256:ae536da50c7ad1e002c3eee101871d93abdc90d9c5f651818450a0d3af718609", - "sha256:b0db0cecf396033abb4a93c95d1602f268b3a68bb0a9cc06a7cff587bb9a7292", - "sha256:cfee9164954c186b191b91d4193989ca994703b2fff406f71cf454a2d3c7327e", - "sha256:e6347742ac8f35ded4a46ff835c60e68c22a536a8ae5c4422966d06946b6d4c6", - "sha256:f27d93f0139a3c056172ebb5d4f9056e770fdf0206c2f422ff2ebbad142e09ed", - "sha256:f57b76e46a58b63d1c6375017f4564a28f19a5ca912691fd2e4261b3414b618d" - ], - "version": "==2.7" + "sha256:02079a6addc7b5140ba0825f542c0869ff4df9a69c360e339ecead5baefa843c", + "sha256:1df22371fbf2004c6f64e927668734070a8953362cd8370ddd336774d6743595", + "sha256:369d2346db5934345787451504853ad9d342d7f721ae82d098083e1f49a582ad", + "sha256:3cda1f0ed8747339bbdf71b9f38ca74c7b592f24f65cdb3ab3765e4b02871651", + "sha256:44ff04138935882fef7c686878e1c8fd80a723161ad6a98da31e14b7553170c2", + "sha256:4b1030728872c59687badcca1e225a9103440e467c17d6d1730ab3d2d64bfeff", + "sha256:58363dbd966afb4f89b3b11dfb8ff200058fbc3b947507675c19ceb46104b48d", + "sha256:6ec280fb24d27e3d97aa731e16207d58bd8ae94ef6eab97249a2afe4ba643d42", + "sha256:7270a6c29199adc1297776937a05b59720e8a782531f1f122f2eb8467f9aab4d", + "sha256:73fd30c57fa2d0a1d7a49c561c40c2f79c7d6c374cc7750e9ac7c99176f6428e", + "sha256:7f09806ed4fbea8f51585231ba742b58cbcfbfe823ea197d8c89a5e433c7e912", + "sha256:90df0cc93e1f8d2fba8365fb59a858f51a11a394d64dbf3ef844f783844cc793", + "sha256:971221ed40f058f5662a604bd1ae6e4521d84e6cad0b7b170564cc34169c8f13", + "sha256:a518c153a2b5ed6b8cc03f7ae79d5ffad7315ad4569b2d5333a13c38d64bd8d7", + "sha256:b0de590a8b0979649ebeef8bb9f54394d3a41f66c5584fff4220901739b6b2f0", + "sha256:b43f53f29816ba1db8525f006fa6f49292e9b029554b3eb56a189a70f2a40879", + "sha256:d31402aad60ed889c7e57934a03477b572a03af7794fa8fb1780f21ea8f6551f", + "sha256:de96157ec73458a7f14e3d26f17f8128c959084931e8997b9e655a39c8fde9f9", + "sha256:df6b4dca2e11865e6cfbfb708e800efb18370f5a46fd601d3755bc7f85b3a8a2", + "sha256:ecadccc7ba52193963c0475ac9f6fa28ac01e01349a2ca48509667ef41ffd2cf", + "sha256:fb81c17e0ebe3358486cd8cc3ad78adbae58af12fc2bf2bc0bb84e8090fa5ce8" + ], + "version": "==2.8" }, "daphne": { "hashes": [ - "sha256:2329b7a74b5559f7ea012879c10ba945c3a53df7d8d2b5932a904e3b4c9abcc2", - "sha256:3cae286a995ae5b127d7de84916f0480cb5be19f81125b6a150b8326250dadd5" + "sha256:1e41d02d78da373c1f8504835cdca6d447b5d64c2941661d4bd968d412b6c383", + "sha256:47d792aea2c6c957d352ad5ace3a1c4232e223ef65202b27aceb0743346d6b68" ], - "version": "==2.3.0" + "version": "==2.4.1" }, "dj-database-url": { "hashes": [ @@ -170,18 +183,19 @@ }, "django": { "hashes": [ - "sha256:4d23f61b26892bac785f07401bc38cbf8fa4cec993f400e9cd9ddf28fd51c0ea", - "sha256:6e974d4b57e3b29e4882b244d40171d6a75202ab8d2402b8e8adbd182e25cf0c" + "sha256:4f2c913303be4f874015993420bf0bd8fd2097a9c88e6b49c6a92f9bdd3fb13a", + "sha256:8c3575f81e11390893860d97e1e0154c47512f180ea55bd84ce8fa69ba8051ca" ], "index": "pypi", - "version": "==2.2.3" + "version": "==3.0.2" }, "django-bootstrap4": { "hashes": [ - "sha256:9f115534ae8d052d397201f3d716c10d7c9832b422e44dd7382418c6f274df18" + "sha256:0fcd84f8414a58b43df0b331c00c8b2f1786ae28f75f419b4d33b06fca43e0d1", + "sha256:39f97cbce85eb66f6d76be2029bae171bd3863d0c6932b1c2dae7f299c569b90" ], "index": "pypi", - "version": "==0.0.8" + "version": "==1.1.1" }, "django-heroku": { "hashes": [ @@ -201,11 +215,11 @@ }, "djangorestframework": { "hashes": [ - "sha256:1ca4a5599a5ec31f3d6238a687fcc1dd4c41b1d90edab9ad398fcbf87872b7ba", - "sha256:c3c5edfdbc5dd33f9121bb84305bfd603d2c791f20cff9782772f44a7684a4e4" + "sha256:05809fc66e1c997fd9a32ea5730d9f4ba28b109b9da71fccfa5ff241201fd0a4", + "sha256:e782087823c47a26826ee5b6fa0c542968219263fb3976ec3c31edab23a4001f" ], "index": "pypi", - "version": "==3.10.1" + "version": "==3.11.0" }, "drf-nested-routers": { "hashes": [ @@ -217,36 +231,48 @@ }, "hiredis": { "hashes": [ - "sha256:0124911115f2cb7deb4f8e221e109a53d3d718174b238a2c5e2162175a3929a5", - "sha256:0656658d0448c2c82c4890ae933c2c2e51196101d3d06fc19cc92e062410c2fd", - "sha256:09d284619f7142ddd7a4ffa94c12a0445e834737f4ce8739a737f2b1ca0f6142", - "sha256:12299b7026e5dc22ed0ff603375c1bf583cf59adbb0e4d062df434e9140d72dd", - "sha256:12fc6210f8dc3e9c8ce4b95e8f5db404b838dbdeb25bca41e33497de6d89334f", - "sha256:197febe5e63c77f4ad19b36e15ed33152064dc606c8b7413c7a0ca3fd04672cc", - "sha256:20e48289fbffb59a5ac7cc677fc02c2726c1da22488e5f7636b9feb9afde199f", - "sha256:26bed296b92b88db02afe214aa1fefad7f9e8ba88a5a7c0e355b55c4b168d212", - "sha256:321b19d2a21fd576111032fe7694d317de2c11b265ef775f2e3f22734a6b94c8", - "sha256:32d5f2c461250f5fc7ccef647682651b1d9f69443f16c213d7fa5e183222b233", - "sha256:36bfcc86715d109a5ef6edefd52b893de97d555cb5cb0e9cab83eb9665942ccc", - "sha256:438ddfd1484e98110959dc4648c0ba22c3307c9c0ae7e2a856755067f9ce9cef", - "sha256:66f17c1633b2fb967bf4165f7b3d369a1bdfe3537d3646cf9a7c208506c96c49", - "sha256:94ab0fa3ac93ab36a5400c474439881d182b43fd38a2766d984470c57931ae88", - "sha256:955f12da861f2608c181049f623bbb52851769e10639c4919cc586395b89813f", - "sha256:b1fd831f96ce0f715e9356574f5184b840b59eb8901fc5f9124fedbe84ad2a59", - "sha256:b3813c641494fca2eda66c32a2117816472a5a39b12f59f7887c6d17bdb8c77e", - "sha256:bbc3ee8663024c82a1226a0d56ad882f42a2fd8c2999bf52d27bdd25f1320f4b", - "sha256:bd12c2774b574f5b209196e25b03b5d62c7919bf69046bc7b955ebe84e0ec1fe", - "sha256:c54d2b3d7a2206df35f3c1140ac20ca6faf7819ff92ea5be8bf4d1cbdb433216", - "sha256:c7b0bcaf2353a2ad387dd8b5e1b5f55991adc3a7713ac3345a4ef0de58276690", - "sha256:c9319a1503efb3b5a4ec13b2f8fae2c23610a645e999cb8954d330f0610b0f6d", - "sha256:cbe5c0273224babe2ec77058643312d07aa5e8fed08901b3f7bccaa744c5728e", - "sha256:cc884ea50185009d794b31314a144110efc76b71beb0a5827a8bff970ae6d248", - "sha256:d1e2e751327781ad81df5a5a29d7c7b19ee0ebfbeddf037fd8df19ec1c06e18b", - "sha256:d2ef58cece6cae4b354411df498350d836f10b814c8a890df0d8079aff30c518", - "sha256:e97c953f08729900a5e740f1760305434d62db9f281ac351108d6c4b5bf51795", - "sha256:fcdf2e10f56113e1cb4326dbca7bf7edbfdbd246cd6d7ec088688e5439129e2c" - ], - "version": "==1.0.0" + "sha256:01b577f84c20ecc9c07fc4c184231b08e3c3942de096fa99978e053de231c423", + "sha256:01ff0900134166961c9e339df77c33b72f7edc5cb41739f0babcd9faa345926e", + "sha256:03ed34a13316d0c34213c4fd46e0fa3a5299073f4d4f08e93fed8c2108b399b3", + "sha256:040436e91df5143aff9e0debb49530d0b17a6bd52200ce568621c31ef581b10d", + "sha256:091eb38fbf968d1c5b703e412bbbd25f43a7967d8400842cee33a5a07b33c27b", + "sha256:102f9b9dc6ed57feb3a7c9bdf7e71cb7c278fe8df1edfcfe896bc3e0c2be9447", + "sha256:2b4b392c7e3082860c8371fab3ae762139090f9115819e12d9f56060f9ede05d", + "sha256:2c9cc0b986397b833073f466e6b9e9c70d1d4dc2c2c1b3e9cae3a23102ff296c", + "sha256:2fa65a9df683bca72073cd77709ddeb289ea2b114d3775d225fbbcc5faf808c5", + "sha256:38437a681f17c975fd22349e72c29bc643f8e7eb2d6dc5df419eac59afa4d7ce", + "sha256:3b3428fa3cf1ee178807b52c9bee8950ab94cd4eaa9bfae8c1bbae3c49501d34", + "sha256:3dd8c2fae7f5494978facb0e93297dd627b1a3f536f3b070cf0a7d9157a07dcb", + "sha256:4414a96c212e732723b5c3d7c04d386ebbb2ec359e1de646322cbc3f875cbd0d", + "sha256:48c627581ad4ef60adbac980981407939acf13a0e18f093502c7b542223c4f19", + "sha256:4a60e71625a2d78d8ab84dfb2fa2cfd9458c964b6e6c04fea76d9ade153fb371", + "sha256:585ace09f434e43d8a8dbeb366865b1a044d7c06319b3c7372a0a00e63b860f4", + "sha256:74b364b3f06c9cf0a53f7df611045bc9437ed972a283fa1f0b12537236d23ddc", + "sha256:75c65c3850e89e9daa68d1b9bedd5806f177d60aa5a7b0953b4829481cfc1f72", + "sha256:7f052de8bf744730a9120dbdc67bfeb7605a01f69fb8e7ba5c475af33c24e145", + "sha256:8113a7d5e87ecf57cd4ae263cc9e429adb9a3e59f5a7768da5d3312a8d0a051a", + "sha256:84857ce239eb8ed191ac78e77ff65d52902f00f30f4ee83bf80eb71da73b70e6", + "sha256:8644a48ddc4a40b3e3a6b9443f396c2ee353afb2d45656c4fc68d04a82e8e3f7", + "sha256:936aa565e673536e8a211e43ec43197406f24cd1f290138bd143765079c8ba00", + "sha256:9afeb88c67bbc663b9f27385c496da056d06ad87f55df6e393e1516cfecb0461", + "sha256:9d62cc7880110e4f83b0a51d218f465d3095e2751fbddd34e553dbd106a929ff", + "sha256:a1fadd062fc8d647ff39220c57ea2b48c99bb73f18223828ec97f88fc27e7898", + "sha256:a7754a783b1e5d6f627c19d099b178059c62f782ab62b4d8ba165b9fbc2ee34c", + "sha256:aa59dd63bb3f736de4fc2d080114429d5d369dfb3265f771778e8349d67a97a4", + "sha256:ae2ee0992f8de249715435942137843a93db204dd7db1e7cc9bdc5a8436443e8", + "sha256:b36842d7cf32929d568f37ec5b3173b72b2ec6572dec4d6be6ce774762215aee", + "sha256:bcbf9379c553b5facc6c04c1e5569b44b38ff16bcbf354676287698d61ee0c92", + "sha256:cbccbda6f1c62ab460449d9c85fdf24d0d32a6bf45176581151e53cc26a5d910", + "sha256:d0caf98dfb8af395d6732bd16561c0a2458851bea522e39f12f04802dbf6f502", + "sha256:d6456afeddba036def1a36d8a2758eca53202308d83db20ab5d0b66590919627", + "sha256:dbaef9a21a4f10bc281684ee4124f169e62bb533c2a92b55f8c06f64f9af7b8f", + "sha256:dce84916c09aaece006272b37234ae84a8ed13abb3a4d341a23933b8701abfb5", + "sha256:eb8c9c8b9869539d58d60ff4a28373a22514d40495911451343971cb4835b7a9", + "sha256:efc98b14ee3a8595e40b1425e8d42f5fd26f11a7b215a81ef9259068931754f4", + "sha256:fa2dc05b87d97acc1c6ae63f3e0f39eae5246565232484b08db6bf2dc1580678", + "sha256:fe7d6ce9f6a5fbe24f09d95ea93e9c7271abc4e1565da511e1449b107b4d7848" + ], + "version": "==1.0.1" }, "hyperlink": { "hashes": [ @@ -271,59 +297,62 @@ }, "msgpack": { "hashes": [ - "sha256:26cb40116111c232bc235ce131cc3b4e76549088cb154e66a2eb8ff6fcc907ec", - "sha256:300fd3f2c664a3bf473d6a952f843b4a71454f4c592ed7e74a36b205c1782d28", - "sha256:3129c355342853007de4a2a86e75eab966119733eb15748819b6554363d4e85c", - "sha256:31f6d645ee5a97d59d3263fab9e6be76f69fa131cddc0d94091a3c8aca30d67a", - "sha256:3ce7ef7ee2546c3903ca8c934d09250531b80c6127e6478781ae31ed835aac4c", - "sha256:4008c72f5ef2b7936447dcb83db41d97e9791c83221be13d5e19db0796df1972", - "sha256:62bd8e43d204580308d477a157b78d3fee2fb4c15d32578108dc5d89866036c8", - "sha256:70cebfe08fb32f83051971264466eadf183101e335d8107b80002e632f425511", - "sha256:72cb7cf85e9df5251abd7b61a1af1fb77add15f40fa7328e924a9c0b6bc7a533", - "sha256:7c55649965c35eb32c499d17dadfb8f53358b961582846e1bc06f66b9bccc556", - "sha256:86b963a5de11336ec26bc4f839327673c9796b398b9f1fe6bb6150c2a5d00f0f", - "sha256:8c73c9bcdfb526247c5e4f4f6cf581b9bb86b388df82cfcaffde0a6e7bf3b43a", - "sha256:8e68c76c6aff4849089962d25346d6784d38e02baa23ffa513cf46be72e3a540", - "sha256:97ac6b867a8f63debc64f44efdc695109d541ecc361ee2dce2c8884ab37360a1", - "sha256:9d4f546af72aa001241d74a79caec278bcc007b4bcde4099994732e98012c858", - "sha256:a28e69fe5468c9f5251c7e4e7232286d71b7dfadc74f312006ebe984433e9746", - "sha256:fd509d4aa95404ce8d86b4e32ce66d5d706fd6646c205e1c2a715d87078683a2" + "sha256:0cc7ca04e575ba34fea7cfcd76039f55def570e6950e4155a4174368142c8e1b", + "sha256:187794cd1eb73acccd528247e3565f6760bd842d7dc299241f830024a7dd5610", + "sha256:1904b7cb65342d0998b75908304a03cb004c63ef31e16c8c43fee6b989d7f0d7", + "sha256:229a0ccdc39e9b6c6d1033cd8aecd9c296823b6c87f0de3943c59b8bc7c64bee", + "sha256:24149a75643aeaa81ece4259084d11b792308a6cf74e796cbb35def94c89a25a", + "sha256:30b88c47e0cdb6062daed88ca283b0d84fa0d2ad6c273aa0788152a1c643e408", + "sha256:32fea0ea3cd1ef820286863a6202dcfd62a539b8ec3edcbdff76068a8c2cc6ce", + "sha256:355f7fd0f90134229eaeefaee3cf42e0afc8518e8f3cd4b25f541a7104dcb8f9", + "sha256:4abdb88a9b67e64810fb54b0c24a1fd76b12297b4f7a1467d85a14dd8367191a", + "sha256:757bd71a9b89e4f1db0622af4436d403e742506dbea978eba566815dc65ec895", + "sha256:76df51492bc6fa6cc8b65d09efdb67cbba3cbfe55004c3afc81352af92b4a43c", + "sha256:774f5edc3475917cd95fe593e625d23d8580f9b48b570d8853d06cac171cd170", + "sha256:8a3ada8401736df2bf497f65589293a86c56e197a80ae7634ec2c3150a2f5082", + "sha256:a06efd0482a1942aad209a6c18321b5e22d64eb531ea20af138b28172d8f35ba", + "sha256:b24afc52e18dccc8c175de07c1d680bdf315844566f4952b5bedb908894bec79", + "sha256:b8b4bd3dafc7b92608ae5462add1c8cc881851c2d4f5d8977fdea5b081d17f21", + "sha256:c6e5024fc0cdf7f83b6624850309ddd7e06c48a75fa0d1c5173de4d93300eb19", + "sha256:db7ff14abc73577b0bcbcf73ecff97d3580ecaa0fc8724babce21fdf3fe08ef6", + "sha256:dedf54d72d9e7b6d043c244c8213fe2b8bbfe66874b9a65b39c4cc892dd99dd4", + "sha256:ea3c2f859346fcd55fc46e96885301d9c2f7a36d453f5d8f2967840efa1e1830", + "sha256:f0f47bafe9c9b8ed03e19a100a743662dd8c6d0135e684feea720a0d0046d116" + ], + "version": "==0.6.2" + }, + "psycopg2": { + "hashes": [ + "sha256:4212ca404c4445dc5746c0d68db27d2cbfb87b523fe233dc84ecd24062e35677", + "sha256:47fc642bf6f427805daf52d6e52619fe0637648fe27017062d898f3bf891419d", + "sha256:72772181d9bad1fa349792a1e7384dde56742c14af2b9986013eb94a240f005b", + "sha256:8396be6e5ff844282d4d49b81631772f80dabae5658d432202faf101f5283b7c", + "sha256:893c11064b347b24ecdd277a094413e1954f8a4e8cdaf7ffbe7ca3db87c103f0", + "sha256:92a07dfd4d7c325dd177548c4134052d4842222833576c8391aab6f74038fc3f", + "sha256:965c4c93e33e6984d8031f74e51227bd755376a9df6993774fd5b6fb3288b1f4", + "sha256:9ab75e0b2820880ae24b7136c4d230383e07db014456a476d096591172569c38", + "sha256:b0845e3bdd4aa18dc2f9b6fb78fbd3d9d371ad167fd6d1b7ad01c0a6cdad4fc6", + "sha256:dca2d7203f0dfce8ea4b3efd668f8ea65cd2b35112638e488a4c12594015f67b", + "sha256:ed686e5926929887e2c7ae0a700e32c6129abb798b4ad2b846e933de21508151", + "sha256:ef6df7e14698e79c59c7ee7cf94cd62e5b869db369ed4b1b8f7b729ea825712a", + "sha256:f898e5cc0a662a9e12bde6f931263a1bbd350cfb18e1d5336a12927851825bb6" ], - "version": "==0.6.1" + "index": "pypi", + "version": "==2.8.4" }, - "psycopg2-binary": { - "hashes": [ - "sha256:080c72714784989474f97be9ab0ddf7b2ad2984527e77f2909fcd04d4df53809", - "sha256:110457be80b63ff4915febb06faa7be002b93a76e5ba19bf3f27636a2ef58598", - "sha256:171352a03b22fc099f15103959b52ee77d9a27e028895d7e5fde127aa8e3bac5", - "sha256:19d013e7b0817087517a4b3cab39c084d78898369e5c46258aab7be4f233d6a1", - "sha256:249b6b21ae4eb0f7b8423b330aa80fab5f821b9ffc3f7561a5e2fd6bb142cf5d", - "sha256:2ac0731d2d84b05c7bb39e85b7e123c3a0acd4cda631d8d542802c88deb9e87e", - "sha256:2b6d561193f0dc3f50acfb22dd52ea8c8dfbc64bcafe3938b5f209cc17cb6f00", - "sha256:2bd23e242e954214944481124755cbefe7c2cf563b1a54cd8d196d502f2578bf", - "sha256:3e1239242ca60b3725e65ab2f13765fc199b03af9eaf1b5572f0e97bdcee5b43", - "sha256:3eb70bb697abbe86b1d2b1316370c02ba320bfd1e9e35cf3b9566a855ea8e4e5", - "sha256:51a2fc7e94b98bd1bb5d4570936f24fc2b0541b63eccadf8fdea266db8ad2f70", - "sha256:52f1bdafdc764b7447e393ed39bb263eccb12bfda25a4ac06d82e3a9056251f6", - "sha256:5b3581319a3951f1e866f4f6c5e42023db0fae0284273b82e97dfd32c51985cd", - "sha256:63c1b66e3b2a3a336288e4bcec499e0dc310cd1dceaed1c46fa7419764c68877", - "sha256:8123a99f24ecee469e5c1339427bcdb2a33920a18bb5c0d58b7c13f3b0298ba3", - "sha256:85e699fcabe7f817c0f0a412d4e7c6627e00c412b418da7666ff353f38e30f67", - "sha256:8dbff4557bbef963697583366400822387cccf794ccb001f1f2307ed21854c68", - "sha256:908d21d08d6b81f1b7e056bbf40b2f77f8c499ab29e64ec5113052819ef1c89b", - "sha256:af39d0237b17d0a5a5f638e9dffb34013ce2b1d41441fd30283e42b22d16858a", - "sha256:af51bb9f055a3f4af0187149a8f60c9d516cf7d5565b3dac53358796a8fb2a5b", - "sha256:b2ecac57eb49e461e86c092761e6b8e1fd9654dbaaddf71a076dcc869f7014e2", - "sha256:cd37cc170678a4609becb26b53a2bc1edea65177be70c48dd7b39a1149cabd6e", - "sha256:d17e3054b17e1a6cb8c1140f76310f6ede811e75b7a9d461922d2c72973f583e", - "sha256:d305313c5a9695f40c46294d4315ed3a07c7d2b55e48a9010dad7db7a66c8b7f", - "sha256:dd0ef0eb1f7dd18a3f4187226e226a7284bda6af5671937a221766e6ef1ee88f", - "sha256:e1adff53b56db9905db48a972fb89370ad5736e0450b96f91bcf99cadd96cfd7", - "sha256:f0d43828003c82dbc9269de87aa449e9896077a71954fbbb10a614c017e65737", - "sha256:f78e8b487de4d92640105c1389e5b90be3496b1d75c90a666edd8737cc2dbab7" + "pyasn1": { + "hashes": [ + "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d", + "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba" ], - "index": "pypi", - "version": "==2.8.3" + "version": "==0.4.8" + }, + "pyasn1-modules": { + "hashes": [ + "sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e", + "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74" + ], + "version": "==0.2.8" }, "pycparser": { "hashes": [ @@ -338,12 +367,19 @@ ], "version": "==1.9.0" }, + "pyopenssl": { + "hashes": [ + "sha256:621880965a720b8ece2f1b2f54ea2071966ab00e2970ad2ce11d596102063504", + "sha256:9a24494b2602aaf402be5c9e30a0b82d4a5c67528fe8fb475e3f3bc00dd69507" + ], + "version": "==19.1.0" + }, "pytz": { "hashes": [ - "sha256:303879e36b721603cc54604edcac9d20401bdbe31e1e4fdee5b9f98d5d31dfda", - "sha256:d747dd3d23d77ef44c6a3526e274af6efeb0a6f1afd5a69ba4d5be4098c8e141" + "sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d", + "sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be" ], - "version": "==2019.1" + "version": "==2019.3" }, "raven": { "hashes": [ @@ -361,12 +397,26 @@ "index": "pypi", "version": "==2.22.0" }, + "service-identity": { + "hashes": [ + "sha256:001c0707759cb3de7e49c078a7c0c9cd12594161d3bf06b9c254fdcb1a60dc36", + "sha256:0858a54aabc5b459d1aafa8a518ed2081a285087f349fe3e55197989232e2e2d" + ], + "version": "==18.1.0" + }, "six": { "hashes": [ - "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", - "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd", + "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66" + ], + "version": "==1.13.0" + }, + "soupsieve": { + "hashes": [ + "sha256:bdb0d917b03a1369ce964056fc195cfdff8819c40de04695a80bc813c3cfa1f5", + "sha256:e2c1c5dee4a1c36bcb790e0fabd5492d874b8ebd4617622c4f6a731701060dda" ], - "version": "==1.12.0" + "version": "==1.9.5" }, "sqlparse": { "hashes": [ @@ -376,10 +426,31 @@ "version": "==0.3.0" }, "twisted": { - "hashes": [ - "sha256:fa2c04c2d68a9be7fc3975ba4947f653a57a656776f24be58ff0fe4b9aaf3e52" - ], - "version": "==19.2.1" + "extras": [ + "tls" + ], + "hashes": [ + "sha256:0f39698c2aac318032ed4fe95e28ee2bd7d72327c2f6927139811ad403770885", + "sha256:1f0919a0363b4fbed5def5315383db36fd581464bca80290764f8c4465e91c04", + "sha256:257dbc78e72bc69c2970035fc74df54b04573d5ddd380251a8a23f74d619db03", + "sha256:3f651c52ad78cc5a643f61e3b786a6b5c9b4ee68eced975c04fdf6b02026f470", + "sha256:58b581ae4eee5a831aac9d03edc331d662fa028f601015bb3df47f8704bfe876", + "sha256:611ef7696d406605962d9a7b040d357f3e91df20cf75c0b06e350947f541538b", + "sha256:6338e5b987e95c94360acb14e78b41097be9b45d44d15a68060db9c3bf89e102", + "sha256:7394ba7f272ae722a74f3d969dcf599bc4ef093bc392038748a490f1724a515d", + "sha256:776c65270b57ac074d5b7a471142f434b0ac5a8b39d9c974769c855c32abfd91", + "sha256:8b2f7f4dded5ad02931bed38042e55329d1e4919b63078f5a29f05502a163f1d", + "sha256:97f8a76632bf051a27179337fe937df315920a08e9bd146126e0126629db3721", + "sha256:a1de7598ce977943b3edbcc0a7d2112f134cc1b98b2fd7e348ee9e0bef092e50", + "sha256:d145c418a46f8a7021030a3246b9e5111f64531278e5252f2073f23c1661c8be", + "sha256:d53e1f883bc429b14fd2999d355352974f9d7b4ae7554154bbfe3f90aecede5f", + "sha256:d9037ff5e07909b1d31f81db71a3bbc8227ba1ed20c87332bdb2eb48e55f326e", + "sha256:ef1396d1680d6a1ae6dff293d778755c8e10d471f286aff678877b2cee235d42", + "sha256:f1fe9139fdcf7721d308e36c51cf975474678a8241a9799af02a7bb1c531b722", + "sha256:f28355e61ce0b5c1ce47784522022322cc5059c8ed80638e0caae8c7301e1705", + "sha256:f7cc56a45c983e4e48601fbeec879b62c740cb848c75164f69a48ac0c6e8a21c" + ], + "version": "==19.10.0" }, "txaio": { "hashes": [ @@ -388,83 +459,130 @@ ], "version": "==18.8.1" }, + "typing-extensions": { + "hashes": [ + "sha256:091ecc894d5e908ac75209f10d5b4f118fbdb2eb1ede6a63544054bb1edb41f2", + "sha256:910f4656f54de5993ad9304959ce9bb903f90aadc7c67a0bef07e678014e892d", + "sha256:cf8b63fedea4d89bab840ecbb93e75578af28f76f66c35889bd7065f5af88575" + ], + "version": "==3.7.4.1" + }, "urllib3": { "hashes": [ - "sha256:b246607a25ac80bedac05c6f282e3cdaf3afb65420fd024ac94435cabe6e18d1", - "sha256:dbe59173209418ae49d485b87d1681aefa36252ee85884c31346debd19463232" + "sha256:a8a318824cc77d1fd4b2bec2ded92646630d7fe8619497b142c84a9e6f5a7293", + "sha256:f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745" ], - "version": "==1.25.3" + "version": "==1.25.7" }, "whitenoise": { "hashes": [ - "sha256:59d880d25d0e90bcc6554fe0504a11195bd2e59b3d690b6fb42a8040d4e67ef5", - "sha256:c9b7c47fdc1dba4d37bf2787a01a844dc7a521e174fcd22a2d429e0be65e1782" + "sha256:0f9137f74bd95fa54329ace88d8dc695fbe895369a632e35f7a136e003e41d73", + "sha256:62556265ec1011bd87113fb81b7516f52688887b7a010ee899ff1fd18fd22700" ], - "version": "==4.1.3" + "version": "==5.0.1" }, "zope.interface": { "hashes": [ - "sha256:086707e0f413ff8800d9c4bc26e174f7ee4c9c8b0302fbad68d083071822316c", - "sha256:1157b1ec2a1f5bf45668421e3955c60c610e31913cc695b407a574efdbae1f7b", - "sha256:11ebddf765bff3bbe8dbce10c86884d87f90ed66ee410a7e6c392086e2c63d02", - "sha256:14b242d53f6f35c2d07aa2c0e13ccb710392bcd203e1b82a1828d216f6f6b11f", - "sha256:1b3d0dcabc7c90b470e59e38a9acaa361be43b3a6ea644c0063951964717f0e5", - "sha256:20a12ab46a7e72b89ce0671e7d7a6c3c1ca2c2766ac98112f78c5bddaa6e4375", - "sha256:298f82c0ab1b182bd1f34f347ea97dde0fffb9ecf850ecf7f8904b8442a07487", - "sha256:2f6175722da6f23dbfc76c26c241b67b020e1e83ec7fe93c9e5d3dd18667ada2", - "sha256:3b877de633a0f6d81b600624ff9137312d8b1d0f517064dfc39999352ab659f0", - "sha256:4265681e77f5ac5bac0905812b828c9fe1ce80c6f3e3f8574acfb5643aeabc5b", - "sha256:550695c4e7313555549aa1cdb978dc9413d61307531f123558e438871a883d63", - "sha256:5f4d42baed3a14c290a078e2696c5f565501abde1b2f3f1a1c0a94fbf6fbcc39", - "sha256:62dd71dbed8cc6a18379700701d959307823b3b2451bdc018594c48956ace745", - "sha256:7040547e5b882349c0a2cc9b50674b1745db551f330746af434aad4f09fba2cc", - "sha256:7e099fde2cce8b29434684f82977db4e24f0efa8b0508179fce1602d103296a2", - "sha256:7e5c9a5012b2b33e87980cee7d1c82412b2ebabcb5862d53413ba1a2cfde23aa", - "sha256:81295629128f929e73be4ccfdd943a0906e5fe3cdb0d43ff1e5144d16fbb52b1", - "sha256:95cc574b0b83b85be9917d37cd2fad0ce5a0d21b024e1a5804d044aabea636fc", - "sha256:968d5c5702da15c5bf8e4a6e4b67a4d92164e334e9c0b6acf080106678230b98", - "sha256:9e998ba87df77a85c7bed53240a7257afe51a07ee6bc3445a0bf841886da0b97", - "sha256:a0c39e2535a7e9c195af956610dba5a1073071d2d85e9d2e5d789463f63e52ab", - "sha256:a15e75d284178afe529a536b0e8b28b7e107ef39626a7809b4ee64ff3abc9127", - "sha256:a6a6ff82f5f9b9702478035d8f6fb6903885653bff7ec3a1e011edc9b1a7168d", - "sha256:b639f72b95389620c1f881d94739c614d385406ab1d6926a9ffe1c8abbea23fe", - "sha256:bad44274b151d46619a7567010f7cde23a908c6faa84b97598fd2f474a0c6891", - "sha256:bbcef00d09a30948756c5968863316c949d9cedbc7aabac5e8f0ffbdb632e5f1", - "sha256:d788a3999014ddf416f2dc454efa4a5dbeda657c6aba031cf363741273804c6b", - "sha256:eed88ae03e1ef3a75a0e96a55a99d7937ed03e53d0cffc2451c208db445a2966", - "sha256:f99451f3a579e73b5dd58b1b08d1179791d49084371d9a47baad3b22417f0317" - ], - "version": "==4.6.0" + "sha256:048b16ac882a05bc7ef534e8b9f15c9d7a6c190e24e8938a19b7617af4ed854a", + "sha256:05816cf8e7407cf62f2ec95c0a5d69ec4fa5741d9ccd10db9f21691916a9a098", + "sha256:065d6a1ac89d35445168813bed45048ed4e67a4cdfc5a68fdb626a770378869f", + "sha256:14157421f4121a57625002cc4f48ac7521ea238d697c4a4459a884b62132b977", + "sha256:18dc895945694f397a0be86be760ff664b790f95d8e7752d5bab80284ff9105d", + "sha256:1962c9f838bd6ae4075d0014f72697510daefc7e1c7e48b2607df0b6e157989c", + "sha256:1a67408cacd198c7e6274a19920bb4568d56459e659e23c4915528686ac1763a", + "sha256:21bf781076dd616bd07cf0223f79d61ab4f45176076f90bc2890e18c48195da4", + "sha256:21c0a5d98650aebb84efa16ce2c8df1a46bdc4fe8a9e33237d0ca0b23f416ead", + "sha256:23cfeea25d1e42ff3bf4f9a0c31e9d5950aa9e7c4b12f0c4bd086f378f7b7a71", + "sha256:24b6fce1fb71abf9f4093e3259084efcc0ef479f89356757780685bd2b06ef37", + "sha256:24f84ce24eb6b5fcdcb38ad9761524f1ae96f7126abb5e597f8a3973d9921409", + "sha256:25e0ef4a824017809d6d8b0ce4ab3288594ba283e4d4f94d8cfb81d73ed65114", + "sha256:2e8fdd625e9aba31228e7ddbc36bad5c38dc3ee99a86aa420f89a290bd987ce9", + "sha256:2f3bc2f49b67b1bea82b942d25bc958d4f4ea6709b411cb2b6b9718adf7914ce", + "sha256:35d24be9d04d50da3a6f4d61de028c1dd087045385a0ff374d93ef85af61b584", + "sha256:35dbe4e8c73003dff40dfaeb15902910a4360699375e7b47d3c909a83ff27cd0", + "sha256:3dfce831b824ab5cf446ed0c350b793ac6fa5fe33b984305cb4c966a86a8fb79", + "sha256:3f7866365df5a36a7b8de8056cd1c605648f56f9a226d918ed84c85d25e8d55f", + "sha256:455cc8c01de3bac6f9c223967cea41f4449f58b4c2e724ec8177382ddd183ab4", + "sha256:4bb937e998be9d5e345f486693e477ba79e4344674484001a0b646be1d530487", + "sha256:52303a20902ca0888dfb83230ca3ee6fbe63c0ad1dd60aa0bba7958ccff454d8", + "sha256:6e0a897d4e09859cc80c6a16a29697406ead752292ace17f1805126a4f63c838", + "sha256:6e1816e7c10966330d77af45f77501f9a68818c065dec0ad11d22b50a0e212e7", + "sha256:73b5921c5c6ce3358c836461b5470bf675601c96d5e5d8f2a446951470614f67", + "sha256:8093cd45cdb5f6c8591cfd1af03d32b32965b0f79b94684cd0c9afdf841982bb", + "sha256:864b4a94b60db301899cf373579fd9ef92edddbf0fb2cd5ae99f53ef423ccc56", + "sha256:8a27b4d3ea9c6d086ce8e7cdb3e8d319b6752e2a03238a388ccc83ccbe165f50", + "sha256:91b847969d4784abd855165a2d163f72ac1e58e6dce09a5e46c20e58f19cc96d", + "sha256:b47b1028be4758c3167e474884ccc079b94835f058984b15c145966c4df64d27", + "sha256:b68814a322835d8ad671b7acc23a3b2acecba527bb14f4b53fc925f8a27e44d8", + "sha256:bcb50a032c3b6ec7fb281b3a83d2b31ab5246c5b119588725b1350d3a1d9f6a3", + "sha256:c56db7d10b25ce8918b6aec6b08ac401842b47e6c136773bfb3b590753f7fb67", + "sha256:c94b77a13d4f47883e4f97f9fa00f5feadd38af3e6b3c7be45cfdb0a14c7149b", + "sha256:db381f6fdaef483ad435f778086ccc4890120aff8df2ba5cfeeac24d280b3145", + "sha256:e6487d01c8b7ed86af30ea141fcc4f93f8a7dde26f94177c1ad637c353bd5c07", + "sha256:e86923fa728dfba39c5bb6046a450bd4eec8ad949ac404eca728cfce320d1732", + "sha256:f6ca36dc1e9eeb46d779869c60001b3065fb670b5775c51421c099ea2a77c3c9", + "sha256:fb62f2cbe790a50d95593fb40e8cca261c31a2f5637455ea39440d6457c2ba25" + ], + "version": "==4.7.1" } }, "develop": { - "astroid": { + "appdirs": { "hashes": [ - "sha256:6560e1e1749f68c64a4b5dee4e091fce798d2f0d84ebe638cf0e0585a343acf4", - "sha256:b65db1bbaac9f9f4d190199bb8680af6f6f84fd3769a5ea883df8a91fe68b4c4" + "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", + "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e" ], - "version": "==2.2.5" + "version": "==1.4.3" }, - "atomicwrites": { + "aspy.yaml": { "hashes": [ - "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4", - "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6" + "sha256:463372c043f70160a9ec950c3f1e4c3a82db5fca01d334b6bc89c7164d744bdc", + "sha256:e7c742382eff2caed61f87a39d13f99109088e5e93f04d76eb8d4b28aa143f45" ], "version": "==1.3.0" }, + "astroid": { + "hashes": [ + "sha256:71ea07f44df9568a75d0f354c49143a4575d90645e9fead6dfb52c26a85ed13a", + "sha256:840947ebfa8b58f318d42301cf8c0a20fd794a33b61cc4638e28e9e61ba32f42" + ], + "version": "==2.3.3" + }, "attrs": { "hashes": [ - "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", - "sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399" + "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", + "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" ], - "version": "==19.1.0" + "version": "==19.3.0" + }, + "black": { + "hashes": [ + "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b", + "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539" + ], + "index": "pypi", + "version": "==19.10b0" + }, + "cfgv": { + "hashes": [ + "sha256:edb387943b665bf9c434f717bf630fa78aecd53d5900d2e05da6ad6048553144", + "sha256:fbd93c9ab0a523bf7daec408f3be2ed99a980e20b2d19b50fc184ca6b820d289" + ], + "version": "==2.0.1" }, - "importlib-metadata": { + "click": { "hashes": [ - "sha256:6dfd58dfe281e8d240937776065dd3624ad5469c835248219bd16cf2e12dbeb7", - "sha256:cb6ee23b46173539939964df59d3d72c3e0c1b5d54b84f1d8a7e912fe43612db" + "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", + "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" ], - "version": "==0.18" + "version": "==7.0" + }, + "identify": { + "hashes": [ + "sha256:6f44e637caa40d1b4cb37f6ed3b262ede74901d28b1cc5b1fc07360871edd65d", + "sha256:72e9c4ed3bc713c7045b762b0d2e2115c572b85abfc1f4604f5a4fd4c6642b71" + ], + "version": "==1.4.9" }, "isort": { "hashes": [ @@ -475,26 +593,29 @@ }, "lazy-object-proxy": { "hashes": [ - "sha256:159a745e61422217881c4de71f9eafd9d703b93af95618635849fe469a283661", - "sha256:23f63c0821cc96a23332e45dfaa83266feff8adc72b9bcaef86c202af765244f", - "sha256:3b11be575475db2e8a6e11215f5aa95b9ec14de658628776e10d96fa0b4dac13", - "sha256:3f447aff8bc61ca8b42b73304f6a44fa0d915487de144652816f950a3f1ab821", - "sha256:4ba73f6089cd9b9478bc0a4fa807b47dbdb8fad1d8f31a0f0a5dbf26a4527a71", - "sha256:4f53eadd9932055eac465bd3ca1bd610e4d7141e1278012bd1f28646aebc1d0e", - "sha256:64483bd7154580158ea90de5b8e5e6fc29a16a9b4db24f10193f0c1ae3f9d1ea", - "sha256:6f72d42b0d04bfee2397aa1862262654b56922c20a9bb66bb76b6f0e5e4f9229", - "sha256:7c7f1ec07b227bdc561299fa2328e85000f90179a2f44ea30579d38e037cb3d4", - "sha256:7c8b1ba1e15c10b13cad4171cfa77f5bb5ec2580abc5a353907780805ebe158e", - "sha256:8559b94b823f85342e10d3d9ca4ba5478168e1ac5658a8a2f18c991ba9c52c20", - "sha256:a262c7dfb046f00e12a2bdd1bafaed2408114a89ac414b0af8755c696eb3fc16", - "sha256:acce4e3267610c4fdb6632b3886fe3f2f7dd641158a843cf6b6a68e4ce81477b", - "sha256:be089bb6b83fac7f29d357b2dc4cf2b8eb8d98fe9d9ff89f9ea6012970a853c7", - "sha256:bfab710d859c779f273cc48fb86af38d6e9210f38287df0069a63e40b45a2f5c", - "sha256:c10d29019927301d524a22ced72706380de7cfc50f767217485a912b4c8bd82a", - "sha256:dd6e2b598849b3d7aee2295ac765a578879830fb8966f70be8cd472e6069932e", - "sha256:e408f1eacc0a68fed0c08da45f31d0ebb38079f043328dce69ff133b95c29dc1" - ], - "version": "==1.4.1" + "sha256:0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d", + "sha256:194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449", + "sha256:1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08", + "sha256:4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a", + "sha256:48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50", + "sha256:5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd", + "sha256:59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239", + "sha256:8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb", + "sha256:9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea", + "sha256:9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e", + "sha256:97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156", + "sha256:9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142", + "sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442", + "sha256:a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62", + "sha256:ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db", + "sha256:cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531", + "sha256:d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383", + "sha256:d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a", + "sha256:eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357", + "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4", + "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0" + ], + "version": "==1.4.3" }, "mccabe": { "hashes": [ @@ -505,55 +626,79 @@ }, "more-itertools": { "hashes": [ - "sha256:3ad685ff8512bf6dc5a8b82ebf73543999b657eded8c11803d9ba6b648986f4d", - "sha256:8bb43d1f51ecef60d81854af61a3a880555a14643691cc4b64a6ee269c78f09a" + "sha256:b84b238cce0d9adad5ed87e745778d20a3f8487d0f0cb8b8a586816c7496458d", + "sha256:c833ef592a0324bcc6a60e48440da07645063c453880c9477ceb22490aec1564" ], - "version": "==7.1.0" + "version": "==8.0.2" }, "mypy": { "hashes": [ - "sha256:0107bff4f46a289f0e4081d59b77cef1c48ea43da5a0dbf0005d54748b26df2a", - "sha256:07957f5471b3bb768c61f08690c96d8a09be0912185a27a68700f3ede99184e4", - "sha256:10af62f87b6921eac50271e667cc234162a194e742d8e02fc4ddc121e129a5b0", - "sha256:11fd60d2f69f0cefbe53ce551acf5b1cec1a89e7ce2d47b4e95a84eefb2899ae", - "sha256:15e43d3b1546813669bd1a6ec7e6a11d2888db938e0607f7b5eef6b976671339", - "sha256:352c24ba054a89bb9a35dd064ee95ab9b12903b56c72a8d3863d882e2632dc76", - "sha256:437020a39417e85e22ea8edcb709612903a9924209e10b3ec6d8c9f05b79f498", - "sha256:49925f9da7cee47eebf3420d7c0e00ec662ec6abb2780eb0a16260a7ba25f9c4", - "sha256:6724fcd5777aa6cebfa7e644c526888c9d639bd22edd26b2a8038c674a7c34bd", - "sha256:7a17613f7ea374ab64f39f03257f22b5755335b73251d0d253687a69029701ba", - "sha256:cdc1151ced496ca1496272da7fc356580e95f2682be1d32377c22ddebdf73c91" + "sha256:0a9a45157e532da06fe56adcfef8a74629566b607fa2c1ac0122d1ff995c748a", + "sha256:2c35cae79ceb20d47facfad51f952df16c2ae9f45db6cb38405a3da1cf8fc0a7", + "sha256:4b9365ade157794cef9685791032521233729cb00ce76b0ddc78749abea463d2", + "sha256:53ea810ae3f83f9c9b452582261ea859828a9ed666f2e1ca840300b69322c474", + "sha256:634aef60b4ff0f650d3e59d4374626ca6153fcaff96ec075b215b568e6ee3cb0", + "sha256:7e396ce53cacd5596ff6d191b47ab0ea18f8e0ec04e15d69728d530e86d4c217", + "sha256:7eadc91af8270455e0d73565b8964da1642fe226665dd5c9560067cd64d56749", + "sha256:7f672d02fffcbace4db2b05369142e0506cdcde20cea0e07c7c2171c4fd11dd6", + "sha256:85baab8d74ec601e86134afe2bcccd87820f79d2f8d5798c889507d1088287bf", + "sha256:87c556fb85d709dacd4b4cb6167eecc5bbb4f0a9864b69136a0d4640fdc76a36", + "sha256:a6bd44efee4dc8c3324c13785a9dc3519b3ee3a92cada42d2b57762b7053b49b", + "sha256:c6d27bd20c3ba60d5b02f20bd28e20091d6286a699174dfad515636cb09b5a72", + "sha256:e2bb577d10d09a2d8822a042a23b8d62bc3b269667c9eb8e60a6edfa000211b1", + "sha256:f97a605d7c8bc2c6d1172c2f0d5a65b24142e11a58de689046e62c2d632ca8c1" ], "index": "pypi", - "version": "==0.720" + "version": "==0.761" }, "mypy-extensions": { "hashes": [ - "sha256:37e0e956f41369209a3d5f34580150bcacfabaa57b33a15c0b25f4b5725e0812", - "sha256:b16cabe759f55e3409a7d231ebd2841378fb0c27a5d1994719e340e4f429ac3e" + "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", + "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" + ], + "version": "==0.4.3" + }, + "nodeenv": { + "hashes": [ + "sha256:561057acd4ae3809e665a9aaaf214afff110bbb6a6d5c8a96121aea6878408b3" ], - "version": "==0.4.1" + "version": "==1.3.4" }, "packaging": { "hashes": [ - "sha256:0c98a5d0be38ed775798ece1b9727178c4469d9c3b4ada66e8e6b7849f8732af", - "sha256:9e1cbf8c12b1f1ce0bb5344b8d7ecf66a6f8a6e91bcb0c84593ed6d3ab5c4ab3" + "sha256:aec3fdbb8bc9e4bb65f0634b9f551ced63983a529d6a8931817d52fdd0816ddb", + "sha256:fe1d8331dfa7cc0a883b49d75fc76380b2ab2734b220fbb87d774e4fd4b851f8" ], - "version": "==19.0" + "version": "==20.0" + }, + "pathspec": { + "hashes": [ + "sha256:163b0632d4e31cef212976cf57b43d9fd6b0bac6e67c26015d611a647d5e7424", + "sha256:562aa70af2e0d434367d9790ad37aed893de47f1693e4201fd1d3dca15d19b96" + ], + "version": "==0.7.0" }, "pluggy": { "hashes": [ - "sha256:0825a152ac059776623854c1543d65a4ad408eb3d33ee114dff91e57ec6ae6fc", - "sha256:b9817417e95936bf75d85d3f8767f7df6cdde751fc40aed3bb3074cbcb77757c" + "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", + "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" ], - "version": "==0.12.0" + "version": "==0.13.1" + }, + "pre-commit": { + "hashes": [ + "sha256:8f48d8637bdae6fa70cc97db9c1dd5aa7c5c8bf71968932a380628c25978b850", + "sha256:f92a359477f3252452ae2e8d3029de77aec59415c16ae4189bcfba40b757e029" + ], + "index": "pypi", + "version": "==1.21.0" }, "py": { "hashes": [ - "sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa", - "sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53" + "sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa", + "sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0" ], - "version": "==1.8.0" + "version": "==1.8.1" }, "pycodestyle": { "hashes": [ @@ -565,39 +710,40 @@ }, "pylint": { "hashes": [ - "sha256:5d77031694a5fb97ea95e828c8d10fc770a1df6eb3906067aaed42201a8a6a09", - "sha256:723e3db49555abaf9bf79dc474c6b9e2935ad82230b10c1138a71ea41ac0fff1" + "sha256:3db5468ad013380e987410a8d6956226963aed94ecb5f9d3a28acca6d9ac36cd", + "sha256:886e6afc935ea2590b462664b161ca9a5e40168ea99e5300935f6591ad467df4" ], "index": "pypi", - "version": "==2.3.1" + "version": "==2.4.4" }, "pylint-django": { "hashes": [ - "sha256:6740b60dd94b6896cffa28d7fe1bb5dd167cbdcb3d9fb3db01b30390ea18b987", - "sha256:e1273decd5ee60eaf9a742f62dd67bacb7ccf2cb1e7641dba0361108d0dff455" + "sha256:440beb814464928aedd2e21196bb6e47a83b63e2cbe886a701ba0f4a64206bbb", + "sha256:d5d113605a64cf0e638b707d4cb42106e626f8851bc30a44d5b22bd698ad8483" ], "index": "pypi", - "version": "==2.0.11" + "version": "==2.0.13" }, "pylint-plugin-utils": { "hashes": [ - "sha256:8d9e31d5ea8b7b0003e1f0f136b44a5235896a32e47c5bc2ef1143e9f6ba0b74" + "sha256:2f30510e1c46edf268d3a195b2849bd98a1b9433229bb2ba63b8d776e1fc4d0a", + "sha256:57625dcca20140f43731311cd8fd879318bf45a8b0fd17020717a8781714a25a" ], - "version": "==0.5" + "version": "==0.6" }, "pyparsing": { "hashes": [ - "sha256:1873c03321fc118f4e9746baf201ff990ceb915f433f23b395f5580d1840cb2a", - "sha256:9b6323ef4ab914af344ba97510e966d64ba91055d6b9afa6b30799340e89cc03" + "sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f", + "sha256:c342dccb5250c08d45fd6f8b4a559613ca603b57498511740e65cd11a2e7dcec" ], - "version": "==2.4.0" + "version": "==2.4.6" }, "pytest": { "hashes": [ - "sha256:6ef6d06de77ce2961156013e9dff62f1b2688aa04d0dc244299fe7d67e09370d", - "sha256:a736fed91c12681a7b34617c8fcefe39ea04599ca72c608751c31d89579a3f77" + "sha256:6b571215b5a790f9b41f19f3531c53a45cf6bb8ef2988bc1ff9afb38270b25fa", + "sha256:e41d489ff43948babd0fad7ad5e49b8735d5d55e26628a58673c39ff61d95de4" ], - "version": "==5.0.1" + "version": "==5.3.2" }, "pytest-asyncio": { "hashes": [ @@ -609,83 +755,128 @@ }, "pytest-django": { "hashes": [ - "sha256:264fb4c506db5d48a6364c311a0b00b7b48a52715bad8839b2d8bee9b99ed6bb", - "sha256:4adfe5fb3ed47f0ba55506dd3daf688b1f74d5e69148c10ad2dd2f79f40c0d62" + "sha256:17592f06d51c2ef4b7a0fb24aa32c8b6998506a03c8439606cb96db160106659", + "sha256:ef3d15b35ed7e46293475e6f282e71a53bcd8c6f87bdc5d5e7ad0f4d49352127" ], "index": "pypi", - "version": "==3.5.1" + "version": "==3.7.0" }, "python-dateutil": { "hashes": [ - "sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb", - "sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e" + "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", + "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" ], "index": "pypi", - "version": "==2.8.0" + "version": "==2.8.1" + }, + "pyyaml": { + "hashes": [ + "sha256:059b2ee3194d718896c0ad077dd8c043e5e909d9180f387ce42012662a4946d6", + "sha256:1cf708e2ac57f3aabc87405f04b86354f66799c8e62c28c5fc5f88b5521b2dbf", + "sha256:24521fa2890642614558b492b473bee0ac1f8057a7263156b02e8b14c88ce6f5", + "sha256:4fee71aa5bc6ed9d5f116327c04273e25ae31a3020386916905767ec4fc5317e", + "sha256:70024e02197337533eef7b85b068212420f950319cc8c580261963aefc75f811", + "sha256:74782fbd4d4f87ff04159e986886931456a1894c61229be9eaf4de6f6e44b99e", + "sha256:940532b111b1952befd7db542c370887a8611660d2b9becff75d39355303d82d", + "sha256:cb1f2f5e426dc9f07a7681419fe39cee823bb74f723f36f70399123f439e9b20", + "sha256:dbbb2379c19ed6042e8f11f2a2c66d39cceb8aeace421bfc29d085d93eda3689", + "sha256:e3a057b7a64f1222b56e47bcff5e4b94c4f61faac04c7c4ecb1985e18caa3994", + "sha256:e9f45bd5b92c7974e59bcd2dcc8631a6b6cc380a904725fce7bc08872e691615" + ], + "version": "==5.3" + }, + "regex": { + "hashes": [ + "sha256:07b39bf943d3d2fe63d46281d8504f8df0ff3fe4c57e13d1656737950e53e525", + "sha256:0932941cdfb3afcbc26cc3bcf7c3f3d73d5a9b9c56955d432dbf8bbc147d4c5b", + "sha256:0e182d2f097ea8549a249040922fa2b92ae28be4be4895933e369a525ba36576", + "sha256:10671601ee06cf4dc1bc0b4805309040bb34c9af423c12c379c83d7895622bb5", + "sha256:23e2c2c0ff50f44877f64780b815b8fd2e003cda9ce817a7fd00dea5600c84a0", + "sha256:26ff99c980f53b3191d8931b199b29d6787c059f2e029b2b0c694343b1708c35", + "sha256:27429b8d74ba683484a06b260b7bb00f312e7c757792628ea251afdbf1434003", + "sha256:3e77409b678b21a056415da3a56abfd7c3ad03da71f3051bbcdb68cf44d3c34d", + "sha256:4e8f02d3d72ca94efc8396f8036c0d3bcc812aefc28ec70f35bb888c74a25161", + "sha256:4eae742636aec40cf7ab98171ab9400393360b97e8f9da67b1867a9ee0889b26", + "sha256:6a6ae17bf8f2d82d1e8858a47757ce389b880083c4ff2498dba17c56e6c103b9", + "sha256:6a6ba91b94427cd49cd27764679024b14a96874e0dc638ae6bdd4b1a3ce97be1", + "sha256:7bcd322935377abcc79bfe5b63c44abd0b29387f267791d566bbb566edfdd146", + "sha256:98b8ed7bb2155e2cbb8b76f627b2fd12cf4b22ab6e14873e8641f266e0fb6d8f", + "sha256:bd25bb7980917e4e70ccccd7e3b5740614f1c408a642c245019cff9d7d1b6149", + "sha256:d0f424328f9822b0323b3b6f2e4b9c90960b24743d220763c7f07071e0778351", + "sha256:d58e4606da2a41659c84baeb3cfa2e4c87a74cec89a1e7c56bee4b956f9d7461", + "sha256:e3cd21cc2840ca67de0bbe4071f79f031c81418deb544ceda93ad75ca1ee9f7b", + "sha256:e6c02171d62ed6972ca8631f6f34fa3281d51db8b326ee397b9c83093a6b7242", + "sha256:e7c7661f7276507bce416eaae22040fd91ca471b5b33c13f8ff21137ed6f248c", + "sha256:ecc6de77df3ef68fee966bb8cb4e067e84d4d1f397d0ef6fce46913663540d77" + ], + "version": "==2020.1.8" }, "six": { "hashes": [ - "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", - "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd", + "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66" ], - "version": "==1.12.0" + "version": "==1.13.0" + }, + "toml": { + "hashes": [ + "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", + "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" + ], + "version": "==0.10.0" }, "typed-ast": { "hashes": [ + "sha256:1170afa46a3799e18b4c977777ce137bb53c7485379d9706af8a59f2ea1aa161", "sha256:18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e", "sha256:262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e", "sha256:2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0", "sha256:354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c", + "sha256:48e5b1e71f25cfdef98b013263a88d7145879fbb2d5185f2a0c79fa7ebbeae47", "sha256:4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631", "sha256:630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4", "sha256:66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34", "sha256:71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b", + "sha256:7954560051331d003b4e2b3eb822d9dd2e376fa4f6d98fee32f452f52dd6ebb2", + "sha256:838997f4310012cf2e1ad3803bce2f3402e9ffb71ded61b5ee22617b3a7f6b6e", "sha256:95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a", "sha256:bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233", "sha256:cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1", "sha256:d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36", "sha256:d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d", "sha256:d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a", + "sha256:fdc1c9bbf79510b76408840e009ed65958feba92a88833cdceecff93ae8fff66", "sha256:ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12" ], - "markers": "implementation_name == 'cpython'", "version": "==1.4.0" }, "typing-extensions": { "hashes": [ - "sha256:2ed632b30bb54fc3941c382decfd0ee4148f5c591651c9272473fea2c6397d95", - "sha256:b1edbbf0652660e32ae780ac9433f4231e7339c7f9a8057d0f042fcbcea49b87", - "sha256:d8179012ec2c620d3791ca6fe2bf7979d979acdbef1fca0bc56b37411db682ed" + "sha256:091ecc894d5e908ac75209f10d5b4f118fbdb2eb1ede6a63544054bb1edb41f2", + "sha256:910f4656f54de5993ad9304959ce9bb903f90aadc7c67a0bef07e678014e892d", + "sha256:cf8b63fedea4d89bab840ecbb93e75578af28f76f66c35889bd7065f5af88575" ], - "version": "==3.7.4" + "version": "==3.7.4.1" + }, + "virtualenv": { + "hashes": [ + "sha256:0d62c70883c0342d59c11d0ddac0d954d0431321a41ab20851facf2b222598f3", + "sha256:55059a7a676e4e19498f1aad09b8313a38fcc0cdbe4fdddc0e9b06946d21b4bb" + ], + "version": "==16.7.9" }, "wcwidth": { "hashes": [ - "sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e", - "sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c" + "sha256:8fd29383f539be45b20bd4df0dc29c20ba48654a41e661925e612311e9f3c603", + "sha256:f28b3e8a6483e5d49e7f8949ac1a78314e740333ae305b4ba5defd3e74fb37a8" ], - "version": "==0.1.7" + "version": "==0.1.8" }, "wrapt": { "hashes": [ "sha256:565a021fd19419476b9362b05eeaa094178de64f8361e44468f9e9d7843901e1" ], "version": "==1.11.2" - }, - "yapf": { - "hashes": [ - "sha256:02ace10a00fa2e36c7ebd1df2ead91dbfbd7989686dc4ccbdc549e95d19f5780", - "sha256:6f94b6a176a7c114cfa6bad86d40f259bbe0f10cf2fa7f2f4b3596fc5802a41b" - ], - "index": "pypi", - "version": "==0.28.0" - }, - "zipp": { - "hashes": [ - "sha256:4970c3758f4e89a7857a973b1e2a5d75bcdc47794442f2e2dd4fe8e0466e809a", - "sha256:8a5712cfd3bb4248015eb3b0b3c54a5f6ee3f2425963ef2a0125b8bc40aafaec" - ], - "version": "==0.5.2" } } } diff --git a/README.md b/README.md index 037947b..a483746 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,8 @@ hit play. Each person listens to music using their own Spotify account and Dancing Together keeps the jammin' in sync. Pause it, skip it, rewind it, enjoy it! +It has a Python 3.8 Django backend with a JavaScript React frontend. + ## Features - Create your own radio station and invite friends via email diff --git a/accounts/apps.py b/accounts/apps.py index 9b3fc5a..fb0257e 100644 --- a/accounts/apps.py +++ b/accounts/apps.py @@ -2,4 +2,4 @@ class AccountsConfig(AppConfig): - name = 'accounts' + name = "accounts" diff --git a/accounts/models.py b/accounts/models.py index e453e30..469da25 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -3,4 +3,4 @@ class User(AbstractUser): def __str__(self): - return f'user-{self.id}' + return f"user-{self.id}" diff --git a/accounts/tests/test_views.py b/accounts/tests/test_views.py index eab2b2e..b581f8d 100644 --- a/accounts/tests/test_views.py +++ b/accounts/tests/test_views.py @@ -5,10 +5,10 @@ from django.test import TestCase import pytest -MOCK_USERNAME = 'MockUsername' -MOCK_USERNAME2 = 'MockUsername2' -MOCK_PASSWORD = 'MockPassword' -MOCK_PASSWORD2 = 'MockPassword2' +MOCK_USERNAME = "MockUsername" +MOCK_USERNAME2 = "MockUsername2" +MOCK_PASSWORD = "MockPassword" +MOCK_PASSWORD2 = "MockPassword2" class AccountsViewsTests(TestCase): @@ -18,12 +18,14 @@ def tearDown(self): def test_user_sign_up(self): response = self.client.post( - '/join/', { - 'username': MOCK_USERNAME, - 'password1': MOCK_PASSWORD, - 'password2': MOCK_PASSWORD, - }) - self.assertRedirects(response, '/stations/') + "/join/", + { + "username": MOCK_USERNAME, + "password1": MOCK_PASSWORD, + "password2": MOCK_PASSWORD, + }, + ) + self.assertRedirects(response, "/stations/") user = auth.get_user(self.client) assert user.is_authenticated @@ -32,24 +34,25 @@ def test_user_sign_up_already_logged_in(self): user = create_user() self.client.force_login(user) - response = self.client.get('/join/') - self.assertRedirects(response, '/') + response = self.client.get("/join/") + self.assertRedirects(response, "/") response = self.client.post( - '/join/', { - 'username': MOCK_USERNAME, - 'password1': MOCK_PASSWORD, - 'password2': MOCK_PASSWORD - }) - self.assertRedirects(response, '/') + "/join/", + { + "username": MOCK_USERNAME, + "password1": MOCK_PASSWORD, + "password2": MOCK_PASSWORD, + }, + ) + self.assertRedirects(response, "/") def test_user_login(self): user = create_user() - response = self.client.post('/login/', { - 'username': MOCK_USERNAME, - 'password': MOCK_PASSWORD, - }) - self.assertRedirects(response, '/stations/') + response = self.client.post( + "/login/", {"username": MOCK_USERNAME, "password": MOCK_PASSWORD,} + ) + self.assertRedirects(response, "/stations/") assert user.is_authenticated @@ -57,76 +60,80 @@ def test_user_login_already_logged_in(self): user = create_user() self.client.force_login(user) - response = self.client.get('/login/') - self.assertRedirects(response, '/') + response = self.client.get("/login/") + self.assertRedirects(response, "/") - response = self.client.post('/login/', { - 'username': MOCK_USERNAME, - 'password': MOCK_PASSWORD, - }) - self.assertRedirects(response, '/') + response = self.client.post( + "/login/", {"username": MOCK_USERNAME, "password": MOCK_PASSWORD,} + ) + self.assertRedirects(response, "/") def test_user_can_see_profile(self): user = create_user() self.client.force_login(user) - response = self.client.get(f'/accounts/{user.id}/') + response = self.client.get(f"/accounts/{user.id}/") assert response.status_code == HTTPStatus.OK.value def test_user_can_only_see_their_profile(self): user = create_user() self.client.force_login(user) - response = self.client.get('/accounts/2/') - self.assertRedirects(response, '/') + response = self.client.get("/accounts/2/") + self.assertRedirects(response, "/") def test_user_needs_to_be_logged_in_to_see_profile(self): - response = self.client.get('/accounts/1/') - self.assertRedirects(response, '/accounts/login/?next=/accounts/1/') + response = self.client.get("/accounts/1/") + self.assertRedirects(response, "/accounts/login/?next=/accounts/1/") def test_user_can_change_password(self): user = create_user() self.client.force_login(user) response = self.client.post( - f'/accounts/{user.id}/', { - 'old_password': MOCK_PASSWORD, - 'new_password1': MOCK_PASSWORD2, - 'new_password2': MOCK_PASSWORD2, - }) - self.assertRedirects(response, f'/accounts/{user.id}/') + f"/accounts/{user.id}/", + { + "old_password": MOCK_PASSWORD, + "new_password1": MOCK_PASSWORD2, + "new_password2": MOCK_PASSWORD2, + }, + ) + self.assertRedirects(response, f"/accounts/{user.id}/") self.client.logout() - assert self.client.login(username=MOCK_USERNAME, - password=MOCK_PASSWORD2) + assert self.client.login(username=MOCK_USERNAME, password=MOCK_PASSWORD2) def test_user_can_only_change_their_password(self): user = create_user() self.client.force_login(user) response = self.client.post( - f'/accounts/2/', { - 'old_password': MOCK_PASSWORD, - 'new_password1': MOCK_PASSWORD2, - 'new_password2': MOCK_PASSWORD2, - }) - self.assertRedirects(response, '/') + f"/accounts/2/", + { + "old_password": MOCK_PASSWORD, + "new_password1": MOCK_PASSWORD2, + "new_password2": MOCK_PASSWORD2, + }, + ) + self.assertRedirects(response, "/") def test_user_needs_to_be_logged_in_to_change_password(self): response = self.client.post( - f'/accounts/1/', { - 'old_password': MOCK_PASSWORD, - 'new_password1': MOCK_PASSWORD2, - 'new_password2': MOCK_PASSWORD2, - }) - self.assertRedirects(response, '/accounts/login/?next=/accounts/1/') + f"/accounts/1/", + { + "old_password": MOCK_PASSWORD, + "new_password1": MOCK_PASSWORD2, + "new_password2": MOCK_PASSWORD2, + }, + ) + self.assertRedirects(response, "/accounts/login/?next=/accounts/1/") def test_user_can_delete_account(self): user = create_user() self.client.force_login(user) - response = self.client.post(f'/accounts/{user.id}/delete/') - self.assertRedirects(response, '/') + response = self.client.post(f"/accounts/{user.id}/delete/") + self.assertRedirects(response, "/") with pytest.raises(auth.get_user_model().DoesNotExist): auth.get_user_model().objects.get(id=user.id) @@ -134,16 +141,18 @@ def test_user_can_delete_only_their_account(self): user = create_user() self.client.force_login(user) - response = self.client.post(f'/accounts/2/delete/') - self.assertRedirects(response, '/') + response = self.client.post(f"/accounts/2/delete/") + self.assertRedirects(response, "/") def test_user_needs_to_be_logged_in_to_delete_account(self): user = create_user() - response = self.client.post(f'/accounts/{user.id}/delete/') + response = self.client.post(f"/accounts/{user.id}/delete/") self.assertRedirects( - response, f'/accounts/login/?next=/accounts/{user.id}/delete/') + response, f"/accounts/login/?next=/accounts/{user.id}/delete/" + ) def create_user(username=MOCK_USERNAME) -> User: - return auth.get_user_model().objects.create_user(username=MOCK_USERNAME, - password=MOCK_PASSWORD) + return auth.get_user_model().objects.create_user( + username=MOCK_USERNAME, password=MOCK_PASSWORD + ) diff --git a/accounts/urls.py b/accounts/urls.py index e1af6f1..f550937 100644 --- a/accounts/urls.py +++ b/accounts/urls.py @@ -3,10 +3,8 @@ from . import views urlpatterns = [ - path('join/', views.JoinView.as_view(), name='join'), - path('/', views.UserDetailView.as_view(), name='account-detail'), - path('/delete/', - views.UserDeleteView.as_view(), - name='account-delete'), - path('', include('django.contrib.auth.urls')), + path("join/", views.JoinView.as_view(), name="join"), + path("/", views.UserDetailView.as_view(), name="account-detail"), + path("/delete/", views.UserDeleteView.as_view(), name="account-delete"), + path("", include("django.contrib.auth.urls")), ] diff --git a/accounts/views.py b/accounts/views.py index d88227e..cb2abf0 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -1,7 +1,11 @@ from django.contrib import auth from django.contrib.auth.forms import PasswordChangeForm from django.contrib.auth.mixins import LoginRequiredMixin -from django.contrib.auth.views import LoginView as DefaultLoginView, LogoutView, PasswordChangeView +from django.contrib.auth.views import ( + LoginView as DefaultLoginView, + LogoutView, + PasswordChangeView, +) from django.shortcuts import redirect from django.urls import reverse, reverse_lazy from django.views import View @@ -15,18 +19,18 @@ class JoinView(CreateView): model = User form_class = CustomUserCreationForm - template_name = 'registration/join.html' - success_url = reverse_lazy('radio:index') + template_name = "registration/join.html" + success_url = reverse_lazy("radio:index") def get(self, request, *args, **kwargs): if request.user.is_authenticated: - return redirect('homepage') + return redirect("homepage") else: return super().get(request, *args, **kwargs) def post(self, request, *args, **kwargs): if request.user.is_authenticated: - return redirect('homepage') + return redirect("homepage") else: return super().post(request, *args, **kwargs) @@ -39,13 +43,13 @@ def form_valid(self, form): class LoginView(DefaultLoginView): def get(self, request, *args, **kwargs): if request.user.is_authenticated: - return redirect('homepage') + return redirect("homepage") else: return super().get(request, *args, **kwargs) def post(self, request, *args, **kwargs): if request.user.is_authenticated: - return redirect('homepage') + return redirect("homepage") else: return super().post(request, *args, **kwargs) @@ -62,44 +66,44 @@ def post(self, request, *args, **kwargs): class ShowUserDetailView(DetailView): model = User - template_name = 'accounts/detail.html' + template_name = "accounts/detail.html" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['password_change_form'] = PasswordChangeForm(self.request.user) + context["password_change_form"] = PasswordChangeForm(self.request.user) return context def get(self, request, *args, **kwargs): - if request.user.id == kwargs['pk']: + if request.user.id == kwargs["pk"]: return super().get(request, *args, **kwargs) else: - return redirect('/') + return redirect("/") class UserPasswordChangeDoneView(PasswordChangeView): - template_name = 'accounts/detail.html' + template_name = "accounts/detail.html" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['password_change_form'] = PasswordChangeForm(self.request.user) + context["password_change_form"] = PasswordChangeForm(self.request.user) return context def get_success_url(self): - return reverse('account-detail', kwargs={'pk': self.request.user.id}) + return reverse("account-detail", kwargs={"pk": self.request.user.id}) def post(self, request, *args, **kwargs): - if request.user.id == kwargs['pk']: + if request.user.id == kwargs["pk"]: return super().post(request, *args, **kwargs) else: - return redirect('homepage') + return redirect("homepage") class UserDeleteView(LoginRequiredMixin, DeleteView): model = User - success_url = reverse_lazy('homepage') + success_url = reverse_lazy("homepage") def post(self, request, *args, **kwargs): - if request.user.id == kwargs['pk']: + if request.user.id == kwargs["pk"]: return super().post(request, *args, **kwargs) else: - return redirect('homepage') + return redirect("homepage") diff --git a/ci/azure-pipelines.yml b/ci/azure-pipelines.yml index b7cd691..8086693 100644 --- a/ci/azure-pipelines.yml +++ b/ci/azure-pipelines.yml @@ -4,12 +4,12 @@ # https://docs.microsoft.com/vsts/pipelines/languages/python pool: - vmImage: 'Ubuntu 16.04' + vmImage: 'Ubuntu 18.04' steps: - task: UsePythonVersion@0 inputs: - versionSpec: '3.7' + versionSpec: '3.8' architecture: 'x64' - task: PythonScript@0 @@ -34,7 +34,7 @@ steps: - script: ci/run_lint_tests displayName: 'Run lint tests' -- script: ci/run_tests +- script: tools/scripts/test_all displayName: 'Run tests' - task: PublishTestResults@2 diff --git a/ci/install_prerequisites b/ci/install_prerequisites index 6cca640..84798bd 100755 --- a/ci/install_prerequisites +++ b/ci/install_prerequisites @@ -8,5 +8,7 @@ REPO_ROOT="$(git rev-parse --show-toplevel)" # install backend dependencies -python -m pip install --upgrade pip pipenv +sudo apt-get install libpq-dev python-dev + +python3 -m pip install --upgrade pip pipenv pipenv install --dev --system --ignore-pipfile --deploy diff --git a/ci/run_lint_tests b/ci/run_lint_tests index 79eb4cd..a3cf9b7 100755 --- a/ci/run_lint_tests +++ b/ci/run_lint_tests @@ -10,6 +10,6 @@ mkdir -p junit # run backend lint tests -yapf --recursive . --exclude '*/migrations/*' --parallel --diff -mypy --ignore-missing-imports --junit-xml junit/test-backend-lint.xml $(find . -name \*.py ! -path '*/migrations/*') +python3 -m black --check . +python3 -m mypy --ignore-missing-imports --junit-xml junit/test-backend-lint.xml $(find . -name \*.py ! -path '*/migrations/*') find . -name '*.py' ! -path '*/migrations/*' -print0 | xargs pylint --load-plugins pylint_django diff --git a/ci/run_tests b/ci/run_tests deleted file mode 100755 index 01a9acd..0000000 --- a/ci/run_tests +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash - -REPO_ROOT="$(git rev-parse --show-toplevel)" - -# run frontend tests - -pushd "$REPO_ROOT/frontend" || exit 1 -npm run build -CI=1 JEST_JUNIT_OUTPUT=junit/test-frontend.xml npm test -- --ci --reporters=default --reporters=jest-junit -popd || exit 1 - -# run backend tests - -DJANGO_SETTINGS_MODULE=dancingtogether.settings.test python manage.py collectstatic --noinput -DJANGO_SETTINGS_MODULE=dancingtogether.settings.test python manage.py test --junit-xml junit/test-backend.xml --no-input diff --git a/dancingtogether/asgi.py b/dancingtogether/asgi.py index 1288168..85ef436 100644 --- a/dancingtogether/asgi.py +++ b/dancingtogether/asgi.py @@ -7,7 +7,6 @@ import django from channels.routing import get_default_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', - 'dancingtogether.settings.production') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dancingtogether.settings.production") django.setup() application = get_default_application() diff --git a/dancingtogether/middleware.py b/dancingtogether/middleware.py index d896e47..27a134d 100644 --- a/dancingtogether/middleware.py +++ b/dancingtogether/middleware.py @@ -4,7 +4,7 @@ def __init__(self, get_response): def __call__(self, request): response = self.get_response(request) - response['X-Content-Type-Options'] = 'nosniff' + response["X-Content-Type-Options"] = "nosniff" return response @@ -14,5 +14,5 @@ def __init__(self, get_response): def __call__(self, request): response = self.get_response(request) - response['X-Xss-Protection'] = '1; mode=block' + response["X-Xss-Protection"] = "1; mode=block" return response diff --git a/dancingtogether/routing.py b/dancingtogether/routing.py index 61881a9..75f73fe 100644 --- a/dancingtogether/routing.py +++ b/dancingtogether/routing.py @@ -8,13 +8,19 @@ # selecting on either the connection type (ProtocolTypeRouter) or properties # of the connection's scope (like URLRouter, which looks at scope["path"]) # For more, see http://channels.readthedocs.io/en/latest/topics/routing.html -application = ProtocolTypeRouter({ - # Channels will do this for you automatically. It's included here as an example. - # "http": AsgiHandler, - 'websocket': - AuthMiddlewareStack( - URLRouter([ - path('api/stations//stream/', - radio.consumers.StationConsumer), - ])), -}) +application = ProtocolTypeRouter( + { + # Channels will do this for you automatically. It's included here as an example. + # "http": AsgiHandler, + "websocket": AuthMiddlewareStack( + URLRouter( + [ + path( + "api/stations//stream/", + radio.consumers.StationConsumer, + ), + ] + ) + ), + } +) diff --git a/dancingtogether/runner.py b/dancingtogether/runner.py index ada80dd..b3312d9 100644 --- a/dancingtogether/runner.py +++ b/dancingtogether/runner.py @@ -4,12 +4,9 @@ class PytestTestRunner(DiscoverRunner): """Runs pytest to discover and run tests.""" - def __init__(self, - verbosity=1, - failfast=False, - keepdb=False, - junit_xml=None, - **kwargs): + def __init__( + self, verbosity=1, failfast=False, keepdb=False, junit_xml=None, **kwargs + ): self.verbosity = verbosity self.failfast = failfast self.keepdb = keepdb @@ -18,8 +15,8 @@ def __init__(self, @classmethod def add_arguments(cls, parser): parser.add_argument( - '--junit-xml', - help='Create junit-xml style report file at given path') + "--junit-xml", help="Create junit-xml style report file at given path" + ) def run_tests(self, test_labels): """Run pytest and return the exitcode. @@ -30,17 +27,17 @@ def run_tests(self, test_labels): argv = [] if self.verbosity == 0: - argv.append('--quiet') + argv.append("--quiet") elif self.verbosity == 2: - argv.append('--verbose') + argv.append("--verbose") elif self.verbosity == 3: - argv.append('-vv') + argv.append("-vv") if self.failfast: - argv.append('--exitfirst') + argv.append("--exitfirst") if self.keepdb: - argv.append('--reuse-db') + argv.append("--reuse-db") if self.junit_xml: - argv.append(f'--junit-xml={self.junit_xml}') + argv.append(f"--junit-xml={self.junit_xml}") argv.extend(test_labels) return pytest.main(argv) diff --git a/dancingtogether/settings/base.py b/dancingtogether/settings/base.py index df36e4f..bb6db9b 100644 --- a/dancingtogether/settings/base.py +++ b/dancingtogether/settings/base.py @@ -29,71 +29,72 @@ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = os.environ.get( - 'DT_SECRET_KEY', '%)jx84v!m(5$52w6yf6@*ib*likhgl2!o#k(171%4$c3_ykwb!') + "DT_SECRET_KEY", "%)jx84v!m(5$52w6yf6@*ib*likhgl2!o#k(171%4$c3_ykwb!" +) # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = bool(os.environ.get('DT_DEBUG', False)) +DEBUG = bool(os.environ.get("DT_DEBUG", False)) ALLOWED_HOSTS: typing.List[str] = [] # Application definition INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'bootstrap4', - 'channels', - 'raven.contrib.django.raven_compat', - 'rest_framework', - 'webpack_loader', + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "bootstrap4", + "channels", + "raven.contrib.django.raven_compat", + "rest_framework", + "webpack_loader", # Project Apps - 'main', - 'radio', - 'accounts', + "main", + "radio", + "accounts", ] MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - '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', - 'dancingtogether.middleware.XContentTypeOptionsMiddleware', - 'dancingtogether.middleware.XXssProtectionMiddleware', + "django.middleware.security.SecurityMiddleware", + "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", + "dancingtogether.middleware.XContentTypeOptionsMiddleware", + "dancingtogether.middleware.XXssProtectionMiddleware", ] -ROOT_URLCONF = 'dancingtogether.urls' +ROOT_URLCONF = "dancingtogether.urls" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [os.path.join(BASE_DIR, 'templates')], - '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', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [os.path.join(BASE_DIR, "templates")], + "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", ], }, }, ] -ASGI_APPLICATION = 'dancingtogether.routing.application' +ASGI_APPLICATION = "dancingtogether.routing.application" # SSL/HTTPS -SECURE_SSL_REDIRECT = bool(os.environ.get('DT_USE_HTTPS', True)) -SESSION_COOKIE_SECURE = bool(os.environ.get('DT_USE_HTTPS', True)) +SECURE_SSL_REDIRECT = bool(os.environ.get("DT_USE_HTTPS", True)) +SESSION_COOKIE_SECURE = bool(os.environ.get("DT_USE_HTTPS", True)) if SECURE_SSL_REDIRECT: - SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') + SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") # Database # https://docs.djangoproject.com/en/2.0/ref/settings/#databases @@ -105,31 +106,21 @@ AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': - 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', - }, - { - 'NAME': - 'django.contrib.auth.password_validation.MinimumLengthValidator', - }, - { - 'NAME': - 'django.contrib.auth.password_validation.CommonPasswordValidator', - }, - { - 'NAME': - 'django.contrib.auth.password_validation.NumericPasswordValidator', + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", }, + {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",}, + {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",}, + {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",}, ] -AUTH_USER_MODEL = 'accounts.User' +AUTH_USER_MODEL = "accounts.User" # Internationalization # https://docs.djangoproject.com/en/2.0/topics/i18n/ -LANGUAGE_CODE = 'en-us' +LANGUAGE_CODE = "en-us" -TIME_ZONE = 'UTC' +TIME_ZONE = "UTC" USE_I18N = True @@ -140,68 +131,47 @@ # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/2.0/howto/static-files/ -STATIC_URL = '/static/' +STATIC_URL = "/static/" # Logging LOGGING = { - 'version': 1, - 'disable_existing_loggers': True, - 'root': { - 'level': 'WARNING', - 'handlers': ['sentry'], - }, - 'formatters': { - 'verbose': { - 'format': '%(levelname)s %(asctime)s %(module)s: %(message)s', - }, + "version": 1, + "disable_existing_loggers": True, + "root": {"level": "WARNING", "handlers": ["sentry"],}, + "formatters": { + "verbose": {"format": "%(levelname)s %(asctime)s %(module)s: %(message)s",}, }, - 'handlers': { - 'sentry': { - 'level': 'ERROR', - 'class': - 'raven.contrib.django.raven_compat.handlers.SentryHandler', - 'tags': { - 'custom-tag': 'x' - }, + "handlers": { + "sentry": { + "level": "ERROR", + "class": "raven.contrib.django.raven_compat.handlers.SentryHandler", + "tags": {"custom-tag": "x"}, }, - 'console': { - 'level': 'DEBUG', - 'class': 'logging.StreamHandler', - 'formatter': 'verbose', + "console": { + "level": "DEBUG", + "class": "logging.StreamHandler", + "formatter": "verbose", }, }, - 'loggers': { - '': { - 'handlers': ['console'], - 'level': 'INFO', - }, - 'dancingtogether': { - 'handlers': ['console'], - 'level': 'DEBUG', - }, - 'django': { - 'handlers': ['console'], - 'level': os.getenv('DJANGO_LOG_LEVEL', 'INFO'), - }, - 'django.db.backends': { - 'level': 'ERROR', - 'handlers': ['console'], - 'propagate': False, + "loggers": { + "": {"handlers": ["console"], "level": "INFO",}, + "dancingtogether": {"handlers": ["console"], "level": "DEBUG",}, + "django": { + "handlers": ["console"], + "level": os.getenv("DJANGO_LOG_LEVEL", "INFO"), }, - 'raven': { - 'level': 'DEBUG', - 'handlers': ['console'], - 'propagate': False, + "django.db.backends": { + "level": "ERROR", + "handlers": ["console"], + "propagate": False, }, - 'radio': { - 'handlers': ['console'], - 'level': 'DEBUG', - }, - 'sentry.errors': { - 'level': 'DEBUG', - 'handlers': ['console'], - 'propagate': False, + "raven": {"level": "DEBUG", "handlers": ["console"], "propagate": False,}, + "radio": {"handlers": ["console"], "level": "DEBUG",}, + "sentry.errors": { + "level": "DEBUG", + "handlers": ["console"], + "propagate": False, }, }, } @@ -210,41 +180,37 @@ # http://channels.readthedocs.io/en/latest/topics/channel_layers.html CHANNEL_LAYERS = { - 'default': { + "default": { # This example app uses the Redis channel layer implementation channels_redis - 'BACKEND': 'channels_redis.core.RedisChannelLayer', - 'CONFIG': { - 'hosts': [os.environ.get('REDIS_URL')], - }, + "BACKEND": "channels_redis.core.RedisChannelLayer", + "CONFIG": {"hosts": [os.environ.get("REDIS_URL")],}, }, } # Django Rest Framework REST_FRAMEWORK = { - 'DEFAULT_AUTHENTICATION_CLASSES': [ - 'rest_framework.authentication.BasicAuthentication', - 'rest_framework.authentication.SessionAuthentication', - ], - 'DEFAULT_PERMISSION_CLASSES': [ - 'rest_framework.permissions.IsAuthenticated', + "DEFAULT_AUTHENTICATION_CLASSES": [ + "rest_framework.authentication.BasicAuthentication", + "rest_framework.authentication.SessionAuthentication", ], + "DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticated",], } # View settings -LOGIN_REDIRECT_URL = '/stations/' -LOGOUT_REDIRECT_URL = '/' +LOGIN_REDIRECT_URL = "/stations/" +LOGOUT_REDIRECT_URL = "/" django_heroku.settings(locals(), logging=False) -if 'DATABASE_URL' in os.environ: - locals()['DATABASES']['default'] = dj_database_url.config( - conn_max_age=django_heroku.MAX_CONN_AGE, ssl_require=False) - -SITE_URL = os.environ.get('DT_SITE_URL') -SPOTIFY_CLIENT_ID = os.environ.get('DT_SPOTIFY_CLIENT_ID') -SPOTIFY_CLIENT_SECRET = os.environ.get('DT_SPOTIFY_CLIENT_SECRET') -SPOTIFY_PLAYER_NAME = os.environ.get('DT_SPOTIFY_PLAYER_NAME', - 'Dancing Together') - -SPOTIFY_TOKEN_API_URL = 'https://accounts.spotify.com/api/token' -SPOTIFY_PLAYER_PLAY_API_URL = 'https://api.spotify.com/v1/me/player/play' +if "DATABASE_URL" in os.environ: + locals()["DATABASES"]["default"] = dj_database_url.config( + conn_max_age=django_heroku.MAX_CONN_AGE, ssl_require=False + ) + +SITE_URL = os.environ.get("DT_SITE_URL") +SPOTIFY_CLIENT_ID = os.environ.get("DT_SPOTIFY_CLIENT_ID") +SPOTIFY_CLIENT_SECRET = os.environ.get("DT_SPOTIFY_CLIENT_SECRET") +SPOTIFY_PLAYER_NAME = os.environ.get("DT_SPOTIFY_PLAYER_NAME", "Dancing Together") + +SPOTIFY_TOKEN_API_URL = "https://accounts.spotify.com/api/token" +SPOTIFY_PLAYER_PLAY_API_URL = "https://api.spotify.com/v1/me/player/play" diff --git a/dancingtogether/settings/development.py b/dancingtogether/settings/development.py index dd46355..96db260 100644 --- a/dancingtogether/settings/development.py +++ b/dancingtogether/settings/development.py @@ -1,14 +1,14 @@ from .base import * STATICFILES_DIRS = [ - os.path.join(BASE_DIR, 'static'), + os.path.join(BASE_DIR, "static"), ] # Webpack WEBPACK_LOADER = { - 'DEFAULT': { - 'BUNDLE_DIR_NAME': 'bundles/', - 'STATS_FILE': os.path.join(BASE_DIR, 'webpack-stats.dev.json'), + "DEFAULT": { + "BUNDLE_DIR_NAME": "bundles/", + "STATS_FILE": os.path.join(BASE_DIR, "webpack-stats.dev.json"), }, } diff --git a/dancingtogether/settings/production.py b/dancingtogether/settings/production.py index 7739e84..4e24cc3 100644 --- a/dancingtogether/settings/production.py +++ b/dancingtogether/settings/production.py @@ -4,25 +4,25 @@ # https://docs.djangoproject.com/en/2.1/ref/middleware/#http-strict-transport-security -SECURE_HSTS_SECONDS = os.environ['DT_SECURE_HSTS_SECONDS'] +SECURE_HSTS_SECONDS = os.environ["DT_SECURE_HSTS_SECONDS"] SECURE_HSTS_INCLUDE_SUBDOMAINS = True # Sentry Error Reporting RAVEN_CONFIG = { - 'dsn': os.environ['DT_RAVEN_CONFIG_DSN'], + "dsn": os.environ["DT_RAVEN_CONFIG_DSN"], } # Webpack STATICFILES_DIRS = [ - os.path.join(BASE_DIR, 'static'), - os.path.join(BASE_DIR, 'assets'), + os.path.join(BASE_DIR, "static"), + os.path.join(BASE_DIR, "assets"), ] WEBPACK_LOADER = { - 'DEFAULT': { - 'BUNDLE_DIR_NAME': 'bundles/', - 'STATS_FILE': os.path.join(BASE_DIR, 'webpack-stats.prod.json'), + "DEFAULT": { + "BUNDLE_DIR_NAME": "bundles/", + "STATS_FILE": os.path.join(BASE_DIR, "webpack-stats.prod.json"), }, } diff --git a/dancingtogether/settings/test.py b/dancingtogether/settings/test.py index a9d5276..110c0b4 100644 --- a/dancingtogether/settings/test.py +++ b/dancingtogether/settings/test.py @@ -4,28 +4,24 @@ # https://docs.djangoproject.com/en/2.0/ref/settings/#databases DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - }, + "default": {"ENGINE": "django.db.backends.sqlite3",}, } STATICFILES_DIRS = [ - os.path.join(BASE_DIR, 'static'), + os.path.join(BASE_DIR, "static"), ] # Test # https://docs.djangoproject.com/en/2.0/topics/testing/ -TEST_RUNNER = 'dancingtogether.runner.PytestTestRunner' +TEST_RUNNER = "dancingtogether.runner.PytestTestRunner" # Channel layer definitions # http://channels.readthedocs.io/en/latest/topics/channel_layers.html CHANNEL_LAYERS = { - 'default': { - 'BACKEND': 'channels.layers.InMemoryChannelLayer', - 'TEST_CONFIG': { - 'expiry': 100500, - }, + "default": { + "BACKEND": "channels.layers.InMemoryChannelLayer", + "TEST_CONFIG": {"expiry": 100500,}, }, } diff --git a/dancingtogether/urls.py b/dancingtogether/urls.py index fbdd138..06cecba 100644 --- a/dancingtogether/urls.py +++ b/dancingtogether/urls.py @@ -20,14 +20,14 @@ import main.views urlpatterns = [ - path('', main.views.index, name='homepage'), - path('about/', main.views.about, name='about'), - path('contact/', main.views.contact, name='contact'), - path('accounts/', include('accounts.urls')), - path('admin/', admin.site.urls), - path('join/', accounts.views.JoinView.as_view(), name='join'), - path('login/', accounts.views.LoginView.as_view(), name='login'), - path('logout/', accounts.views.LogoutView.as_view(), name='logout'), - path('api/v1/', include('radio.api.urls')), - path('stations/', include('radio.urls', namespace='radio')), + path("", main.views.index, name="homepage"), + path("about/", main.views.about, name="about"), + path("contact/", main.views.contact, name="contact"), + path("accounts/", include("accounts.urls")), + path("admin/", admin.site.urls), + path("join/", accounts.views.JoinView.as_view(), name="join"), + path("login/", accounts.views.LoginView.as_view(), name="login"), + path("logout/", accounts.views.LogoutView.as_view(), name="logout"), + path("api/v1/", include("radio.api.urls")), + path("stations/", include("radio.urls", namespace="radio")), ] diff --git a/docker-compose.yml b/docker-compose.yml index 78ad9fb..e70664d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,4 @@ -version: '3' +version: "3" services: db: diff --git a/docs/design_thoughts.md b/docs/design_thoughts.md index 85ba801..c51359d 100644 --- a/docs/design_thoughts.md +++ b/docs/design_thoughts.md @@ -53,7 +53,7 @@ 3. Invite users via email to join room - make link expire after 24 hours - + 4. Add stop/stream controls to join/disconnect from station for non-djs 5. show warning if non-dj tries to change music diff --git a/frontend/README.md b/frontend/README.md index c773d24..9911a6f 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -299,7 +299,7 @@ In the WebStorm menu `Run` select `Edit Configurations...`. Then click `+` and s Start your app by running `npm start`, then press `^D` on macOS or `F9` on Windows and Linux or click the green debug icon to start debugging in WebStorm. -The same way you can debug your application in IntelliJ IDEA Ultimate, PhpStorm, PyCharm Pro, and RubyMine. +The same way you can debug your application in IntelliJ IDEA Ultimate, PhpStorm, PyCharm Pro, and RubyMine. ## Formatting Code Automatically @@ -1692,7 +1692,7 @@ Use the following [`launch.json`](https://code.visualstudio.com/docs/editor/debu "name": "Debug CRA Tests", "type": "node", "request": "launch", - "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/react-scripts", + "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/react-scripts", "args": [ "test", "--runInBand", @@ -2002,7 +2002,7 @@ If you’re using [Apache HTTP Server](https://httpd.apache.org/), you need to c RewriteRule ^ index.html [QSA,L] ``` -It will get copied to the `build` folder when you run `npm run build`. +It will get copied to the `build` folder when you run `npm run build`. If you’re using [Apache Tomcat](http://tomcat.apache.org/), you need to follow [this Stack Overflow answer](https://stackoverflow.com/a/41249464/4878474). diff --git a/frontend/tsconfig.prod.json b/frontend/tsconfig.prod.json index 4144216..fc8520e 100644 --- a/frontend/tsconfig.prod.json +++ b/frontend/tsconfig.prod.json @@ -1,3 +1,3 @@ { "extends": "./tsconfig.json" -} \ No newline at end of file +} diff --git a/frontend/tsconfig.test.json b/frontend/tsconfig.test.json index 65ffdd4..2c7b284 100644 --- a/frontend/tsconfig.test.json +++ b/frontend/tsconfig.test.json @@ -3,4 +3,4 @@ "compilerOptions": { "module": "commonjs" } -} \ No newline at end of file +} diff --git a/main/apps.py b/main/apps.py index 833bff6..de0edc1 100644 --- a/main/apps.py +++ b/main/apps.py @@ -2,4 +2,4 @@ class MainConfig(AppConfig): - name = 'main' + name = "main" diff --git a/main/views.py b/main/views.py index 204c7a2..70c14c9 100644 --- a/main/views.py +++ b/main/views.py @@ -2,12 +2,12 @@ def index(request): - return render(request, 'homepage.html') + return render(request, "homepage.html") def about(request): - return render(request, 'about.html') + return render(request, "about.html") def contact(request): - return render(request, 'contact.html') + return render(request, "contact.html") diff --git a/manage.py b/manage.py index 64ba819..67c6c7e 100755 --- a/manage.py +++ b/manage.py @@ -3,13 +3,15 @@ import sys if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", - "dancingtogether.settings.production") + os.environ.setdefault( + "DJANGO_SETTINGS_MODULE", "dancingtogether.settings.production" + ) 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 + "forget to activate a virtual environment?" + ) from exc execute_from_command_line(sys.argv) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..46f92b3 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,21 @@ +[tool.black] +line-length = 88 +target-version = ['py38'] +include = '\.pyi?$' +exclude = ''' +/( + \.eggs + | \.git + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | _build + | buck-out + | build + | dist + + # The following are specific to Black, you probably don't want those. + | migrations +)/ +''' diff --git a/pytest.ini b/pytest.ini index a70b4d2..2680537 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,4 +1,5 @@ [pytest] DJANGO_SETTINGS_MODULE = dancingtogether.settings.test +junit_family=xunit2 python_files = tests.py test_*.py *_tests.py testpaths = accounts dancingtogether main radio diff --git a/radio/api/serializers.py b/radio/api/serializers.py index 2516ad3..d2eff9f 100644 --- a/radio/api/serializers.py +++ b/radio/api/serializers.py @@ -11,12 +11,19 @@ class PlaybackStateSerializer(serializers.ModelSerializer): class Meta: model = PlaybackState - fields = ('station_id', 'context_uri', 'current_track_uri', 'paused', - 'raw_position_ms', 'sample_time', 'last_updated_time') + fields = ( + "station_id", + "context_uri", + "current_track_uri", + "paused", + "raw_position_ms", + "sample_time", + "last_updated_time", + ) def update(self, instance, validated_data): for field in PlaybackStateSerializer.Meta.fields: - if field != 'last_updated_time': + if field != "last_updated_time": new_value = validated_data.get(field, getattr(instance, field)) setattr(instance, field, new_value) @@ -29,17 +36,19 @@ class StationSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = Station - fields = ('title', 'playbackstate') + fields = ("title", "playbackstate") def update(self, instance, validated_data): - if 'playbackstate' in validated_data: + if "playbackstate" in validated_data: try: playback_state = PlaybackStateSerializer().update( - instance.playbackstate, validated_data['playbackstate']) + instance.playbackstate, validated_data["playbackstate"] + ) except PlaybackState.DoesNotExist: - validated_data['playbackstate']['station_id'] = instance.id + validated_data["playbackstate"]["station_id"] = instance.id playback_state = PlaybackStateSerializer().create( - validated_data['playbackstate']) + validated_data["playbackstate"] + ) instance.playbackstate = playback_state @@ -48,15 +57,13 @@ def update(self, instance, validated_data): class ListenerSerializer(serializers.ModelSerializer): user = serializers.SlugRelatedField( - queryset=auth.get_user_model().objects.all(), - slug_field='username', + queryset=auth.get_user_model().objects.all(), slug_field="username", ) - station = serializers.PrimaryKeyRelatedField( - queryset=Station.objects.all()) + station = serializers.PrimaryKeyRelatedField(queryset=Station.objects.all()) class Meta: model = Listener - fields = ('id', 'user', 'station', 'is_admin', 'is_dj') + fields = ("id", "user", "station", "is_admin", "is_dj") class AccessTokenSerializer(serializers.Serializer): diff --git a/radio/api/urls.py b/radio/api/urls.py index 8f3c099..8ca6efd 100644 --- a/radio/api/urls.py +++ b/radio/api/urls.py @@ -4,21 +4,19 @@ from . import views router = routers.SimpleRouter() -router.register(r'stations', views.StationViewSet) +router.register(r"stations", views.StationViewSet) -listeners_router = routers.NestedSimpleRouter(router, - r'stations', - lookup='station') -listeners_router.register(r'listeners', - views.ListenerViewSet, - basename='listeners') +listeners_router = routers.NestedSimpleRouter(router, r"stations", lookup="station") +listeners_router.register(r"listeners", views.ListenerViewSet, basename="listeners") # Wire up our API using automatic URL routing. # Additionally, we include login URLs for the browsable API. urlpatterns = [ - re_path(r'^', include(router.urls)), - re_path(r'^', include(listeners_router.urls)), - re_path(r'^users/(?P[^/.]+)/accesstoken/refresh/$', - views.RefreshAccessToken.as_view()), - re_path(r'^api-auth/', include('rest_framework.urls')), + re_path(r"^", include(router.urls)), + re_path(r"^", include(listeners_router.urls)), + re_path( + r"^users/(?P[^/.]+)/accesstoken/refresh/$", + views.RefreshAccessToken.as_view(), + ), + re_path(r"^api-auth/", include("rest_framework.urls")), ] diff --git a/radio/api/views.py b/radio/api/views.py index 3899a32..d5ee7fe 100644 --- a/radio/api/views.py +++ b/radio/api/views.py @@ -12,8 +12,7 @@ from .. import spotify from ..models import Listener, PlaybackState, SpotifyCredentials, Station from ..spotify import AccessToken -from .serializers import (AccessTokenSerializer, ListenerSerializer, - StationSerializer) +from .serializers import AccessTokenSerializer, ListenerSerializer, StationSerializer logger = logging.getLogger(__name__) @@ -22,11 +21,12 @@ class StationViewSet(viewsets.ModelViewSet): """ API endpoint that allows stations to be viewed or edited. """ + queryset = Station.objects.all() serializer_class = StationSerializer def get_object(self): - return get_object_or_404(self.get_queryset(), id=self.kwargs['pk']) + return get_object_or_404(self.get_queryset(), id=self.kwargs["pk"]) def get_queryset(self): return self.request.user.stations.all() @@ -36,14 +36,13 @@ class ListenerViewSet(viewsets.ModelViewSet): serializer_class = ListenerSerializer def get_object(self): - return get_object_or_404(self.get_queryset(), id=self.kwargs['pk']) + return get_object_or_404(self.get_queryset(), id=self.kwargs["pk"]) def get_queryset(self): - station_id = self.kwargs['station_pk'] + station_id = self.kwargs["station_pk"] get_object_or_404(self.request.user.stations.all(), id=station_id) - listener = Listener.objects.get(station=station_id, - user=self.request.user) + listener = Listener.objects.get(station=station_id, user=self.request.user) if not listener.is_admin: raise PermissionDenied() @@ -52,8 +51,7 @@ def get_queryset(self): def create(self, request: Request, station_pk=None): get_object_or_404(self.request.user.stations.all(), id=station_pk) - listener = Listener.objects.get(station=station_pk, - user=self.request.user) + listener = Listener.objects.get(station=station_pk, user=self.request.user) if not listener.is_admin: raise PermissionDenied() @@ -66,21 +64,17 @@ def has_object_permission(self, request: Request, view, obj): class RefreshAccessToken(APIView): - permission_classes = (BelongsToUser, ) + permission_classes = (BelongsToUser,) def get_object(self): - obj = get_object_or_404(self.get_queryset(), - user_id=self.kwargs['user_pk']) + obj = get_object_or_404(self.get_queryset(), user_id=self.kwargs["user_pk"]) self.check_object_permissions(self.request, obj) return AccessToken.from_db_model(obj) def get_queryset(self): return SpotifyCredentials.objects.all() - def post(self, - request: Request, - format=None, - user_pk: Optional[int] = None): + def post(self, request: Request, format=None, user_pk: Optional[int] = None): access_token = self.get_object() access_token.refresh() access_token.save() diff --git a/radio/apps.py b/radio/apps.py index bd336cf..4f0f1cf 100644 --- a/radio/apps.py +++ b/radio/apps.py @@ -2,4 +2,4 @@ class RadioConfig(AppConfig): - name = 'radio' + name = "radio" diff --git a/radio/consumers.py b/radio/consumers.py index 4829555..29c9292 100644 --- a/radio/consumers.py +++ b/radio/consumers.py @@ -27,8 +27,7 @@ class StationState(enum.Enum): def station_join_required(func): def wrap(self, *args, **kwargs): if self.state != StationState.Connected: - raise ClientError('bad_request', - 'user has not connected to station') + raise ClientError("bad_request", "user has not connected to station") else: return func(self, *args, **kwargs) @@ -39,8 +38,8 @@ def station_admin_required(func): def wrap(self, *args, **kwargs): if not self.is_admin: raise ClientError( - 'forbidden', - 'user does not have permission to request listeners') + "forbidden", "user does not have permission to request listeners" + ) else: return func(self, *args, **kwargs) @@ -52,14 +51,14 @@ class StationConsumer(AsyncJsonWebsocketConsumer): @property def station_id(self): - return self.scope['url_route']['kwargs']['station_id'] + return self.scope["url_route"]["kwargs"]["station_id"] async def connect(self): """Called during initial websocket handshaking.""" try: self.user = await channels.auth.get_user(self.scope) except: - self.user = self.scope['user'] + self.user = self.scope["user"] if self.user.is_anonymous: await self.close() @@ -74,13 +73,13 @@ async def connect(self): async def receive_json(self, content): """Called when we get a text frame.""" - command = content.get('command', None) + command = content.get("command", None) try: - if command == 'ping': - await self.send_pong(content['start_time']) + if command == "ping": + await self.send_pong(content["start_time"]) except ClientError as e: - await self.send_json({'error': e.code, 'message': e.message}) + await self.send_json({"error": e.code, "message": e.message}) async def disconnect(self, code): """Called when the WebSocket closes for any reason.""" @@ -88,7 +87,7 @@ async def disconnect(self, code): if self.state != StationState.NotConnected: await self.leave_station() except ClientError as e: - logger.error(f'Station client error: {e.code}: {e.message}') + logger.error(f"Station client error: {e.code}: {e.message}") # Command helper methods called by receive_json @@ -98,36 +97,38 @@ async def join_station(self): self.is_dj = listener.is_dj station = await get_station_or_error(self.station_id, self.user) - await self.channel_layer.group_add(station.group_name, - self.channel_name) + await self.channel_layer.group_add(station.group_name, self.channel_name) if self.is_admin: - await self.channel_layer.group_add(station.admin_group_name, - self.channel_name) + await self.channel_layer.group_add( + station.admin_group_name, self.channel_name + ) # Message admins that a user has joined the station - await self.admin_group_send_join(station.admin_group_name, - self.user.username, self.user.email) + await self.admin_group_send_join( + station.admin_group_name, self.user.username, self.user.email + ) # Subscribe for PlaybackState change notifications signals.post_save.connect(self.notify_playback_state_changed) # Reply to client to finish setting up station self.state = StationState.Connected - await self.send_json({'join': station.title}) + await self.send_json({"join": station.title}) @station_join_required async def leave_station(self): station = await get_station_or_error(self.station_id, self.user) - await self.admin_group_send_leave(station.admin_group_name, - self.user.username, self.user.email) + await self.admin_group_send_leave( + station.admin_group_name, self.user.username, self.user.email + ) - await self.channel_layer.group_discard(station.group_name, - self.channel_name) + await self.channel_layer.group_discard(station.group_name, self.channel_name) if self.is_admin: - await self.channel_layer.group_discard(station.admin_group_name, - self.channel_name) + await self.channel_layer.group_discard( + station.admin_group_name, self.channel_name + ) if self.is_dj: await ensure_station_playback_state_is_paused(station) @@ -137,14 +138,13 @@ async def leave_station(self): self.is_dj = None async def send_pong(self, start_time): - await self.send_json({ - 'type': - 'pong', - 'start_time': - start_time, - 'server_time': - datetime.now(timezone.utc).isoformat(), - }) + await self.send_json( + { + "type": "pong", + "start_time": start_time, + "server_time": datetime.now(timezone.utc).isoformat(), + } + ) # Playback State Change Notification Management @@ -152,7 +152,7 @@ def notify_playback_state_changed(self, sender, **kwargs): if sender is not models.PlaybackState: return - playback_state = kwargs['instance'] + playback_state = kwargs["instance"] if playback_state.station_id != self.station_id: # This notification is not for this station return @@ -167,60 +167,67 @@ def notify_playback_state_changed(self, sender, **kwargs): async def admin_group_send_join(self, group_name, username, email): await self.channel_layer.group_send( - group_name, { - 'type': 'station.join', - 'sender_user_id': self.user.id, - 'username': username, - 'email': email, - }) + group_name, + { + "type": "station.join", + "sender_user_id": self.user.id, + "username": username, + "email": email, + }, + ) async def admin_group_send_leave(self, group_name, username, email): await self.channel_layer.group_send( - group_name, { - 'type': 'station.leave', - 'sender_user_id': self.user.id, - 'username': username, - 'email': email, - }) + group_name, + { + "type": "station.leave", + "sender_user_id": self.user.id, + "username": username, + "email": email, + }, + ) # Handlers for messages sent over the channel layer async def station_join(self, event): """Called when someone has joined our station.""" - sender_user_id = event['sender_user_id'] + sender_user_id = event["sender_user_id"] if sender_user_id != self.user.id: - await self.send_json({ - 'type': 'listener_change', - 'listener_change_type': 'join', - 'listener': { - 'username': event['username'], - 'email': event['email'], + await self.send_json( + { + "type": "listener_change", + "listener_change_type": "join", + "listener": { + "username": event["username"], + "email": event["email"], + }, } - }) + ) async def station_leave(self, event): """Called when someone has left our station.""" - sender_user_id = event['sender_user_id'] + sender_user_id = event["sender_user_id"] if sender_user_id != self.user.id: - await self.send_json({ - 'type': 'listener_change', - 'listener_change_type': 'leave', - 'listener': { - 'username': event['username'], - 'email': event['email'], + await self.send_json( + { + "type": "listener_change", + "listener_change_type": "leave", + "listener": { + "username": event["username"], + "email": event["email"], + }, } - }) + ) async def send_playback_state_changed(self, playback_state): serializer = PlaybackStateSerializer(playback_state) - await self.send_json({ - 'type': 'playback_state_changed', - 'playbackstate': serializer.data, - }) + await self.send_json( + {"type": "playback_state_changed", "playbackstate": serializer.data,} + ) async def ensure_station_playback_state_is_paused(station): - playback_state = getattr(station, 'playbackstate', None) + playback_state = getattr(station, "playbackstate", None) if (playback_state is not None) and (not playback_state.paused): playback_state.paused = True await save_station_playback_state(playback_state) @@ -232,26 +239,26 @@ async def ensure_station_playback_state_is_paused(station): @database_sync_to_async def get_station_or_error(station_id, user): """Fetch station for user and check permissions.""" - assert not user.is_anonymous, 'Anonymous users cannot connect to station' + assert not user.is_anonymous, "Anonymous users cannot connect to station" assert station_id is not None try: station = Station.objects.get(pk=station_id) except Station.DoesNotExist: - raise ClientError('invalid_station', 'invalid station') + raise ClientError("invalid_station", "invalid station") return station @database_sync_to_async def get_listener_or_error(station_id, user): - assert not user.is_anonymous, 'Anonymous users cannot connect to station' + assert not user.is_anonymous, "Anonymous users cannot connect to station" assert station_id is not None try: listener = Listener.objects.get(user_id=user.id, station_id=station_id) except Listener.DoesNotExist: - raise ClientError('forbidden', 'This station is not available') + raise ClientError("forbidden", "This station is not available") return listener diff --git a/radio/exceptions.py b/radio/exceptions.py index 7577e83..5a12d21 100644 --- a/radio/exceptions.py +++ b/radio/exceptions.py @@ -6,5 +6,6 @@ class ClientError(Error): """Caught by WebSocket receive() handler and returned to client.""" def __init__(self, code, message): + super().__init__(self, code, message) self.code = code self.message = message diff --git a/radio/forms.py b/radio/forms.py index 2d6a280..5bd5bd8 100644 --- a/radio/forms.py +++ b/radio/forms.py @@ -6,4 +6,4 @@ class StationForm(ModelForm): class Meta: model = Station - fields = ['title'] + fields = ["title"] diff --git a/radio/models.py b/radio/models.py index bc897b2..40007ed 100644 --- a/radio/models.py +++ b/radio/models.py @@ -7,8 +7,7 @@ class SpotifyCredentials(models.Model): class Meta: verbose_name_plural = "spotify credentials" - user = models.OneToOneField(settings.AUTH_USER_MODEL, - on_delete=models.CASCADE) + user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) refresh_token = models.CharField(max_length=256) access_token = models.CharField(max_length=256) access_token_expiration_time = models.DateTimeField() @@ -17,35 +16,34 @@ class Meta: class Station(models.Model): title = models.CharField(max_length=256) - members = models.ManyToManyField(settings.AUTH_USER_MODEL, - related_name='stations', - through='Listener') + members = models.ManyToManyField( + settings.AUTH_USER_MODEL, related_name="stations", through="Listener" + ) def __str__(self): return self.title @property def group_name(self): - return f'station-{self.id}' + return f"station-{self.id}" @property def admin_group_name(self): - return f'station-admin-{self.id}' + return f"station-admin-{self.id}" def get_absolute_url(self): - return reverse('radio:detail', kwargs={'pk': self.pk}) + return reverse("radio:detail", kwargs={"pk": self.pk}) class Listener(models.Model): - user = models.ForeignKey(settings.AUTH_USER_MODEL, - on_delete=models.CASCADE) + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) station = models.ForeignKey(Station, on_delete=models.CASCADE) is_admin = models.BooleanField() is_dj = models.BooleanField() class Meta: - unique_together = (('user', 'station'), ) + unique_together = (("user", "station"),) class PlaybackState(models.Model): diff --git a/radio/spotify.py b/radio/spotify.py index 0337321..df53fb3 100644 --- a/radio/spotify.py +++ b/radio/spotify.py @@ -26,7 +26,7 @@ class AuthorizationRequiredMixin: """ def dispatch(self, request, *args, **kwargs): - if not hasattr(request.user, 'spotifycredentials'): + if not hasattr(request.user, "spotifycredentials"): return request_spotify_authorization(request) return super().dispatch(request, *args, **kwargs) @@ -43,7 +43,7 @@ def dispatch(self, request, *args, **kwargs): access_token.refresh() access_token.save() - request.session['access_token'] = access_token.token + request.session["access_token"] = access_token.token return super().dispatch(request, *args, **kwargs) @@ -51,11 +51,11 @@ def dispatch(self, request, *args, **kwargs): def get_oauth_redirect_uri(): - return f'{settings.SITE_URL}/stations/request-authorization-callback' + return f"{settings.SITE_URL}/stations/request-authorization-callback" def get_url_safe_oauth_request_state(request): - session_id = request.COOKIES['sessionid'] + session_id = request.COOKIES["sessionid"] m = hashlib.sha256() m.update(session_id.encode()) return m.hexdigest() @@ -70,26 +70,25 @@ def request_spotify_authorization(request): def build_request_authorization_url(request): - url = 'https://accounts.spotify.com/authorize' - scope = 'streaming user-modify-playback-state user-read-birthdate user-read-email user-read-private' + url = "https://accounts.spotify.com/authorize" + scope = "streaming user-modify-playback-state user-read-birthdate user-read-email user-read-private" query_params = { - 'client_id': settings.SPOTIFY_CLIENT_ID, - 'response_type': 'code', - 'redirect_uri': get_oauth_redirect_uri(), - 'state': get_url_safe_oauth_request_state(request), - 'scope': scope, + "client_id": settings.SPOTIFY_CLIENT_ID, + "response_type": "code", + "redirect_uri": get_oauth_redirect_uri(), + "state": get_url_safe_oauth_request_state(request), + "scope": scope, } query_params = urllib.parse.urlencode(query_params) - return f'{url}?{query_params}' + return f"{url}?{query_params}" # Spotify OAuth Step 2: Request access and refresh tokens class AccessToken: - def __init__(self, user, refresh_token, access_token, - access_token_expiration_time): + def __init__(self, user, refresh_token, access_token, access_token_expiration_time): self.user = user self.refresh_token = refresh_token self.token = access_token @@ -99,24 +98,23 @@ def __str__(self): return self.token def is_valid(self): - return ((self.token is not None) - and (self.token_expiration_time is not None)) + return (self.token is not None) and (self.token_expiration_time is not None) def has_expired(self): return timezone.now() > self.token_expiration_time def refresh(self): data = { - 'grant_type': 'refresh_token', - 'refresh_token': self.refresh_token, - 'client_id': settings.SPOTIFY_CLIENT_ID, - 'client_secret': settings.SPOTIFY_CLIENT_SECRET, + "grant_type": "refresh_token", + "refresh_token": self.refresh_token, + "client_id": settings.SPOTIFY_CLIENT_ID, + "client_secret": settings.SPOTIFY_CLIENT_SECRET, } response = requests.post(settings.SPOTIFY_TOKEN_API_URL, data=data) if response.status_code == HTTPStatus.OK.value: response_data = response.json() - self.token = response_data['access_token'] - expires_in = int(response_data['expires_in']) + self.token = response_data["access_token"] + expires_in = int(response_data["expires_in"]) expires_in = timedelta(seconds=expires_in) self.token_expiration_time = timezone.now() + expires_in else: @@ -134,8 +132,9 @@ def save(self): try: creds = SpotifyCredentials.objects.get(user_id=self.user.id) except SpotifyCredentials.DoesNotExist: - creds = SpotifyCredentials(user_id=self.user.id, - refresh_token=self.refresh_token) + creds = SpotifyCredentials( + user_id=self.user.id, refresh_token=self.refresh_token + ) creds.access_token = self.token creds.access_token_expiration_time = self.token_expiration_time @@ -143,26 +142,33 @@ def save(self): @classmethod def from_db_model(cls, creds: SpotifyCredentials): - return cls(creds.user, creds.refresh_token, creds.access_token, - creds.access_token_expiration_time) + return cls( + creds.user, + creds.refresh_token, + creds.access_token, + creds.access_token_expiration_time, + ) @staticmethod def request_refresh_and_access_token(code, user): data = { - 'grant_type': 'authorization_code', - 'code': code, - 'redirect_uri': get_oauth_redirect_uri(), - 'client_id': settings.SPOTIFY_CLIENT_ID, - 'client_secret': settings.SPOTIFY_CLIENT_SECRET, + "grant_type": "authorization_code", + "code": code, + "redirect_uri": get_oauth_redirect_uri(), + "client_id": settings.SPOTIFY_CLIENT_ID, + "client_secret": settings.SPOTIFY_CLIENT_SECRET, } r = requests.post(settings.SPOTIFY_TOKEN_API_URL, data) response_data = r.json() - expires_in = int(response_data['expires_in']) + expires_in = int(response_data["expires_in"]) expires_in = timedelta(seconds=expires_in) expiration_time = timezone.now() + expires_in - access_token = AccessToken(user, response_data['refresh_token'], - response_data['access_token'], - expiration_time) + access_token = AccessToken( + user, + response_data["refresh_token"], + response_data["access_token"], + expiration_time, + ) access_token.save() diff --git a/radio/tests/mocks.py b/radio/tests/mocks.py index 818c8c5..017a3ed 100644 --- a/radio/tests/mocks.py +++ b/radio/tests/mocks.py @@ -7,11 +7,11 @@ import socket from threading import Thread -TEST_ACCESS_TOKEN = 'test_access_token' +TEST_ACCESS_TOKEN = "test_access_token" class MockSpotifyRequestHandler(BaseHTTPRequestHandler): - TOKEN_PATTERN = re.compile(r'/api/token') + TOKEN_PATTERN = re.compile(r"/api/token") # BaseHTTPRequestHandler @@ -19,26 +19,26 @@ def do_POST(self): if re.search(self.TOKEN_PATTERN, self.path): self.send_response(HTTPStatus.OK.value) - self.send_header('Content-Type', 'application/json; charset=utf-8') + self.send_header("Content-Type", "application/json; charset=utf-8") self.end_headers() response_data = { - 'access_token': TEST_ACCESS_TOKEN, - 'expires_in': timedelta(hours=1).seconds, + "access_token": TEST_ACCESS_TOKEN, + "expires_in": timedelta(hours=1).seconds, } response_content = json.dumps(response_data) - self.wfile.write(response_content.encode('utf-8')) + self.wfile.write(response_content.encode("utf-8")) def get_free_port(): with closing(socket.socket(socket.AF_INET, type=socket.SOCK_STREAM)) as s: - s.bind(('localhost', 0)) + s.bind(("localhost", 0)) _, port = s.getsockname() return port def start_mock_spotify_server(port, handler=MockSpotifyRequestHandler): - mock_server = HTTPServer(('localhost', port), handler) + mock_server = HTTPServer(("localhost", port), handler) mock_server_thread = Thread(target=mock_server.serve_forever) mock_server_thread.setDaemon(True) mock_server_thread.start() diff --git a/radio/tests/test_api.py b/radio/tests/test_api.py index a7cb711..f9b2de3 100644 --- a/radio/tests/test_api.py +++ b/radio/tests/test_api.py @@ -12,29 +12,28 @@ from ..models import Listener, PlaybackState, SpotifyCredentials, Station from . import mocks -MOCK_USERNAME1 = 'MockUsername1' -MOCK_USERNAME2 = 'MockUsername2' +MOCK_USERNAME1 = "MockUsername1" +MOCK_USERNAME2 = "MockUsername2" class StationTests(APITestCase): def setUp(self): - password = 'testpassword' + password = "testpassword" self.user1 = create_user1(password) - assert self.client.login(username=self.user1.username, - password=password) + assert self.client.login(username=self.user1.username, password=password) create_spotify_credentials(self.user1) def tearDown(self): self.client.logout() def test_can_list_stations(self): - response = self.client.get('/api/v1/stations/') + response = self.client.get("/api/v1/stations/") assert response.status_code == HTTPStatus.OK assert not response.data station = create_station() create_listener(station, self.user1) - response = self.client.get('/api/v1/stations/') + response = self.client.get("/api/v1/stations/") assert response.status_code == HTTPStatus.OK assert len(response.data) == 1 assert response.data[0] == StationSerializer(station).data @@ -43,7 +42,7 @@ def test_can_only_list_authorized_stations(self): # Create a station that user1 is not a listener of create_station() - response = self.client.get('/api/v1/stations/') + response = self.client.get("/api/v1/stations/") assert response.status_code == HTTPStatus.OK assert not response.data @@ -52,42 +51,36 @@ def test_can_update_stations(self): create_listener(station, self.user1) playback_state = create_playback_state(station) - response = self.client.patch(f'/api/v1/stations/{station.id}/', - data={ - 'playbackstate': { - 'raw_position_ms': 1, - }, - }, - format='json') + response = self.client.patch( + f"/api/v1/stations/{station.id}/", + data={"playbackstate": {"raw_position_ms": 1,},}, + format="json", + ) assert response.status_code == HTTPStatus.OK - assert PlaybackState.objects.get( - station_id=station.id).raw_position_ms == 1 + assert PlaybackState.objects.get(station_id=station.id).raw_position_ms == 1 def test_user_can_only_update_authorized_stations(self): station = create_station() playback_state = create_playback_state(station) playback_state.raw_position_ms = 1 - response = self.client.patch(f'/api/v1/stations/{station.id}/', - data={ - 'playbackstate.raw_position_ms': 1, - }, - format='json') + response = self.client.patch( + f"/api/v1/stations/{station.id}/", + data={"playbackstate.raw_position_ms": 1,}, + format="json", + ) assert response.status_code == HTTPStatus.NOT_FOUND class ListenerTests(APITestCase): def setUp(self): - password = 'testpassword' + password = "testpassword" self.user1 = create_user1(password) - assert self.client.login(username=self.user1.username, - password=password) + assert self.client.login(username=self.user1.username, password=password) create_spotify_credentials(self.user1) self.station = create_station() - self.listener = create_listener(self.station, - self.user1, - is_admin=True) + self.listener = create_listener(self.station, self.user1, is_admin=True) def tearDown(self): self.client.logout() @@ -97,13 +90,14 @@ def test_can_create_listener(self): username2 = create_user2().username data = { - 'user': username2, - 'station': self.station.id, - 'is_admin': False, - 'is_dj': False, + "user": username2, + "station": self.station.id, + "is_admin": False, + "is_dj": False, } response = self.client.post( - f'/api/v1/stations/{self.station.id}/listeners/', data=data) + f"/api/v1/stations/{self.station.id}/listeners/", data=data + ) assert response.status_code == HTTPStatus.CREATED.value def test_can_only_create_listener_if_authorized(self): @@ -112,82 +106,76 @@ def test_can_only_create_listener_if_authorized(self): # user1 is not a listener of station2 data = { - 'user': username2, - 'station': self.station.id, - 'is_admin': False, - 'is_dj': False, + "user": username2, + "station": self.station.id, + "is_admin": False, + "is_dj": False, } response = self.client.post( - f'/api/v1/stations/{station2.id}/listeners/', data=data) + f"/api/v1/stations/{station2.id}/listeners/", data=data + ) assert response.status_code == HTTPStatus.NOT_FOUND.value # user1 is not an admin of station2 create_listener(station2, self.user1, is_admin=False) data = { - 'user': username2, - 'station': self.station.id, - 'is_admin': False, - 'is_dj': False, + "user": username2, + "station": self.station.id, + "is_admin": False, + "is_dj": False, } response = self.client.post( - f'/api/v1/stations/{station2.id}/listeners/', data=data) + f"/api/v1/stations/{station2.id}/listeners/", data=data + ) assert response.status_code == HTTPStatus.FORBIDDEN.value def test_cannot_create_listener_for_nonexistent_user(self): data = { - 'user': 'NonexistentUsername', - 'station': self.station.id, - 'is_admin': False, - 'is_dj': False, + "user": "NonexistentUsername", + "station": self.station.id, + "is_admin": False, + "is_dj": False, } response = self.client.post( - f'/api/v1/stations/{self.station.id}/listeners/', data=data) + f"/api/v1/stations/{self.station.id}/listeners/", data=data + ) assert response.status_code == HTTPStatus.BAD_REQUEST.value def test_can_get_listeners(self): user2 = create_user2() - listener2 = create_listener(self.station, - user2, - is_admin=False, - is_dj=True) + listener2 = create_listener(self.station, user2, is_admin=False, is_dj=True) - response = self.client.get( - f'/api/v1/stations/{self.station.id}/listeners/') + response = self.client.get(f"/api/v1/stations/{self.station.id}/listeners/") data = { self.listener.id: { - 'id': self.listener.id, - 'user': MOCK_USERNAME1, - 'station': self.station.id, - 'is_admin': True, - 'is_dj': True + "id": self.listener.id, + "user": MOCK_USERNAME1, + "station": self.station.id, + "is_admin": True, + "is_dj": True, }, listener2.id: { - 'id': listener2.id, - 'user': user2.username, - 'station': self.station.id, - 'is_admin': False, - 'is_dj': True - } - } - actual_data = { - listener['id']: dict(listener) - for listener in response.data + "id": listener2.id, + "user": user2.username, + "station": self.station.id, + "is_admin": False, + "is_dj": True, + }, } + actual_data = {listener["id"]: dict(listener) for listener in response.data} assert actual_data == data def test_can_only_get_listeners_if_authorized(self): station2 = create_station() # user1 is not a listener of station2 - response = self.client.get( - f'/api/v1/stations/{station2.id}/listeners/') + response = self.client.get(f"/api/v1/stations/{station2.id}/listeners/") assert response.status_code == HTTPStatus.NOT_FOUND.value # user1 is not an admin of station2 create_listener(station2, self.user1, is_admin=False) - response = self.client.get( - f'/api/v1/stations/{station2.id}/listeners/') + response = self.client.get(f"/api/v1/stations/{station2.id}/listeners/") assert response.status_code == HTTPStatus.FORBIDDEN.value def test_can_delete_listener(self): @@ -195,7 +183,8 @@ def test_can_delete_listener(self): listener_id = create_listener(self.station, user).id response = self.client.delete( - f'/api/v1/stations/{self.station.id}/listeners/{listener_id}/') + f"/api/v1/stations/{self.station.id}/listeners/{listener_id}/" + ) assert response.status_code == HTTPStatus.NO_CONTENT.value def test_can_only_delete_listener_if_authorized(self): @@ -205,22 +194,23 @@ def test_can_only_delete_listener_if_authorized(self): # user1 is not a listener of station2 response = self.client.delete( - f'/api/v1/stations/{station2.id}/listeners/{listener_id}/') + f"/api/v1/stations/{station2.id}/listeners/{listener_id}/" + ) assert response.status_code == HTTPStatus.NOT_FOUND.value # user1 is not an admin of station2 create_listener(station2, self.user1, is_admin=False) response = self.client.delete( - f'/api/v1/stations/{station2.id}/listeners/{listener_id}/') + f"/api/v1/stations/{station2.id}/listeners/{listener_id}/" + ) assert response.status_code == HTTPStatus.FORBIDDEN.value class AccessTokenTests(APITestCase): def setUp(self): - password = 'testpassword' + password = "testpassword" self.user1 = create_user1(password) - assert self.client.login(username=self.user1.username, - password=password) + assert self.client.login(username=self.user1.username, password=password) create_spotify_credentials(self.user1) def tearDown(self): @@ -231,53 +221,56 @@ def test_can_refresh_own_access_token(self): mocks.start_mock_spotify_server(port) with override_settings( - SPOTIFY_TOKEN_API_URL=f'http://localhost:{port}/api/token'): + SPOTIFY_TOKEN_API_URL=f"http://localhost:{port}/api/token" + ): response = self.client.post( - f'/api/v1/users/{self.user1.id}/accesstoken/refresh/') - assert response.data['token'] == mocks.TEST_ACCESS_TOKEN + f"/api/v1/users/{self.user1.id}/accesstoken/refresh/" + ) + assert response.data["token"] == mocks.TEST_ACCESS_TOKEN def test_cannot_refresh_someone_elses_access_token(self): user2 = create_user2() create_spotify_credentials(user2) # As user1, attempt to refresh user2's access token - response = self.client.post( - f'/api/v1/users/{user2.id}/accesstoken/refresh/') + response = self.client.post(f"/api/v1/users/{user2.id}/accesstoken/refresh/") assert response.status_code == HTTPStatus.FORBIDDEN.value def create_user1(password: str) -> User: return auth.get_user_model().objects.create_user( - username=MOCK_USERNAME1, - email='testuser1@example.com', - password=password) + username=MOCK_USERNAME1, email="testuser1@example.com", password=password + ) def create_user2() -> User: return auth.get_user_model().objects.create_user( - username=MOCK_USERNAME2, email='testuser2@example.com') + username=MOCK_USERNAME2, email="testuser2@example.com" + ) def create_station() -> Station: - return Station.objects.create(title='Station1') + return Station.objects.create(title="Station1") -def create_listener(station: Station, user: User, is_admin=False, - is_dj=True) -> Listener: - return Listener.objects.create(station=station, - user=user, - is_admin=is_admin, - is_dj=is_dj) +def create_listener( + station: Station, user: User, is_admin=False, is_dj=True +) -> Listener: + return Listener.objects.create( + station=station, user=user, is_admin=is_admin, is_dj=is_dj + ) def create_spotify_credentials(user: User) -> SpotifyCredentials: return SpotifyCredentials.objects.create( - user=user, access_token_expiration_time=timezone.now()) + user=user, access_token_expiration_time=timezone.now() + ) -def create_playback_state(station: Station, paused=True, - raw_position_ms=0) -> PlaybackState: +def create_playback_state( + station: Station, paused=True, raw_position_ms=0 +) -> PlaybackState: station_state = PlaybackState(station=station) station_state.paused = paused station_state.raw_position_ms = raw_position_ms diff --git a/radio/tests/test_consumer.py b/radio/tests/test_consumer.py index 1159697..9d5275a 100644 --- a/radio/tests/test_consumer.py +++ b/radio/tests/test_consumer.py @@ -15,13 +15,12 @@ from ..consumers import StationConsumer from ..models import Listener, PlaybackState, Station -MOCK_CONTEXT_URI1 = 'MockContextUri1' -MOCK_CONTEXT_URI2 = 'MockContextUri2' -NON_USER_EMAIL = 'nonuser@example.com' +MOCK_CONTEXT_URI1 = "MockContextUri1" +MOCK_CONTEXT_URI2 = "MockContextUri2" +NON_USER_EMAIL = "nonuser@example.com" -@pytest.mark.skip( - reason='Authentication does not work with StationCommunicator') +@pytest.mark.skip(reason="Authentication does not work with StationCommunicator") @pytest.mark.django_db(transaction=True) @pytest.mark.asyncio async def test_ping_pong(user1, station1): @@ -32,35 +31,33 @@ async def test_ping_pong(user1, station1): await communicator.ping(start_time) response = await communicator.receive_json_from() - assert response['type'] == 'pong' - assert response['start_time'] == start_time - assert dateutil.parser.isoparse(response['server_time']) + assert response["type"] == "pong" + assert response["start_time"] == start_time + assert dateutil.parser.isoparse(response["server_time"]) -@pytest.mark.skip(reason='Test bug: deserialization not working') +@pytest.mark.skip(reason="Test bug: deserialization not working") @pytest.mark.django_db(transaction=True) @pytest.mark.asyncio -async def test_playback_state_changed_notifications(user1: User, - station1: Station): +async def test_playback_state_changed_notifications(user1: User, station1: Station): await create_listener(user1, station1, is_dj=False) playback_state = await create_playback_state(station1) - async with disconnecting(StationCommunicator( - station1.id)) as listener_communicator: + async with disconnecting(StationCommunicator(station1.id)) as listener_communicator: # The DJ changes the playback state playback_state.context_uri = MOCK_CONTEXT_URI2 await consumers.save_station_playback_state(playback_state) response = await listener_communicator.receive_json_from() - assert response['type'] == 'playback_state_changed' + assert response["type"] == "playback_state_changed" response_playback_state = PlaybackStateSerializer( - data=response['playbackstate']) + data=response["playbackstate"] + ) assert response_playback_state.is_valid() -@pytest.mark.skip( - reason='Authentication does not work with StationCommunicator') +@pytest.mark.skip(reason="Authentication does not work with StationCommunicator") @pytest.mark.django_db(transaction=True) @pytest.mark.asyncio async def test_dj_leaves_station(user1: User, station1: Station): @@ -69,8 +66,7 @@ async def test_dj_leaves_station(user1: User, station1: Station): # precondition: station playback state exists and is playing await create_playback_state(station1, paused=False) - async with disconnecting(StationCommunicator( - station1.id)) as _communicator: + async with disconnecting(StationCommunicator(station1.id)) as _communicator: pass new_playback_state = await get_playback_state(station1) @@ -82,41 +78,40 @@ async def test_dj_leaves_station(user1: User, station1: Station): @pytest.fixture def user1() -> User: - return get_user_model().objects.create(username='testuser1', - email='testuser1@example.com') + return get_user_model().objects.create( + username="testuser1", email="testuser1@example.com" + ) @pytest.fixture def user2() -> User: - return get_user_model().objects.create(username='testuser2', - email='testuser2@example.com') + return get_user_model().objects.create( + username="testuser2", email="testuser2@example.com" + ) @pytest.fixture def station1() -> Station: - return Station.objects.create(title='TestStation1') + return Station.objects.create(title="TestStation1") # Utils @database_sync_to_async -def create_listener(user: User, - station: Station, - *, - is_admin=False, - is_dj=False) -> Listener: - return Listener.objects.create(user=user, - station=station, - is_admin=is_admin, - is_dj=is_dj) +def create_listener( + user: User, station: Station, *, is_admin=False, is_dj=False +) -> Listener: + return Listener.objects.create( + user=user, station=station, is_admin=is_admin, is_dj=is_dj + ) @database_sync_to_async def create_playback_state(station: Station, **kwargs): station_state = PlaybackState(station=station) - station_state.paused = kwargs.get('paused', True) - station_state.raw_position_ms = kwargs.get('raw_position_ms', 0) + station_state.paused = kwargs.get("paused", True) + station_state.raw_position_ms = kwargs.get("raw_position_ms", 0) station_state.sample_time = timezone.now() station_state.save() return station_state @@ -146,14 +141,13 @@ async def disconnecting(communicator: WebsocketCommunicator): class StationCommunicator(WebsocketCommunicator): def __init__(self, station_id: int): - application = URLRouter([ - path('api/stations//stream/', StationConsumer), - ]) - url = f'/api/stations/{station_id}/stream/' + application = URLRouter( + [path("api/stations//stream/", StationConsumer),] + ) + url = f"/api/stations/{station_id}/stream/" super().__init__(application, url) async def ping(self, start_time: str): - await self.send_json_to({ - 'command': 'ping', - 'start_time': start_time, - }) + await self.send_json_to( + {"command": "ping", "start_time": start_time,} + ) diff --git a/radio/tests/test_models.py b/radio/tests/test_models.py index 427da84..a2622dc 100644 --- a/radio/tests/test_models.py +++ b/radio/tests/test_models.py @@ -8,23 +8,20 @@ @pytest.mark.django_db(transaction=True) def test_listeners_uniqueness(user1: User, station1: Station): - Listener.objects.create(user=user1, - station=station1, - is_admin=False, - is_dj=False) + Listener.objects.create(user=user1, station=station1, is_admin=False, is_dj=False) with pytest.raises(IntegrityError): - Listener.objects.create(user=user1, - station=station1, - is_admin=False, - is_dj=False) + Listener.objects.create( + user=user1, station=station1, is_admin=False, is_dj=False + ) @pytest.fixture def user1() -> User: - return auth.get_user_model().objects.create(username='testuser1', - email='testuser1@example.com') + return auth.get_user_model().objects.create( + username="testuser1", email="testuser1@example.com" + ) @pytest.fixture def station1() -> Station: - return Station.objects.create(title='Station1') + return Station.objects.create(title="Station1") diff --git a/radio/tests/test_spotify.py b/radio/tests/test_spotify.py index df8b1f3..102a576 100644 --- a/radio/tests/test_spotify.py +++ b/radio/tests/test_spotify.py @@ -17,8 +17,7 @@ def test_refresh_access_token(user1: User): port = mocks.get_free_port() mocks.start_mock_spotify_server(port) - with override_settings( - SPOTIFY_TOKEN_API_URL=f'http://localhost:{port}/api/token'): + with override_settings(SPOTIFY_TOKEN_API_URL=f"http://localhost:{port}/api/token"): access_token = AccessToken.load(user1.id) access_token.refresh() @@ -27,10 +26,12 @@ def test_refresh_access_token(user1: User): @pytest.fixture def user1() -> User: - return auth.get_user_model().objects.create(username='testuser1', - email='testuser1@example.com') + return auth.get_user_model().objects.create( + username="testuser1", email="testuser1@example.com" + ) def create_spotify_credentials(user): return SpotifyCredentials.objects.create( - user=user, access_token_expiration_time=timezone.now()) + user=user, access_token_expiration_time=timezone.now() + ) diff --git a/radio/tests/test_views.py b/radio/tests/test_views.py index 933d841..59f334e 100644 --- a/radio/tests/test_views.py +++ b/radio/tests/test_views.py @@ -7,8 +7,8 @@ from ..models import Listener, SpotifyCredentials, Station -MOCK_USERNAME = 'MockUsername' -MOCK_PASSWORD = 'MockPassword' +MOCK_USERNAME = "MockUsername" +MOCK_PASSWORD = "MockPassword" class RadioViewsTests(TestCase): @@ -22,20 +22,20 @@ def test_user_can_only_see_stations_they_are_members_of_index(self): create_listener(station1, user) station2 = create_station() - response = self.client.get('/stations/') + response = self.client.get("/stations/") - assert station1 in response.context['stations'] - assert station2 not in response.context['stations'] + assert station1 in response.context["stations"] + assert station2 not in response.context["stations"] def test_user_needs_to_be_logged_in_to_access_station_index(self): - response = self.client.get('/stations/') - self.assertRedirects(response, '/accounts/login/?next=/stations/') + response = self.client.get("/stations/") + self.assertRedirects(response, "/accounts/login/?next=/stations/") def test_user_can_only_access_stations_they_are_members_of(self): user = create_user() self.client.force_login(user) - response = self.client.get('/stations/1/') + response = self.client.get("/stations/1/") assert response.status_code == HTTPStatus.NOT_FOUND.value @@ -45,16 +45,15 @@ def test_user_can_delete_station(self): station = create_station() create_listener(station, user, is_admin=True) - response = self.client.post(f'/stations/{station.id}/delete/') + response = self.client.post(f"/stations/{station.id}/delete/") - self.assertRedirects(response, '/stations/') + self.assertRedirects(response, "/stations/") with pytest.raises(Station.DoesNotExist): Station.objects.get(id=station.id) def test_user_needs_to_be_logged_in_to_delete_station(self): - response = self.client.post('/stations/1/delete/') - self.assertRedirects(response, - '/accounts/login/?next=/stations/1/delete/') + response = self.client.post("/stations/1/delete/") + self.assertRedirects(response, "/accounts/login/?next=/stations/1/delete/") def test_user_can_only_delete_stations_they_are_admins_of(self): user = create_user() @@ -62,23 +61,24 @@ def test_user_can_only_delete_stations_they_are_admins_of(self): station = create_station() create_listener(station, user) - response = self.client.post(f'/stations/{station.id}/delete/') + response = self.client.post(f"/stations/{station.id}/delete/") - self.assertRedirects(response, '/stations/') + self.assertRedirects(response, "/stations/") def create_user(username=MOCK_USERNAME) -> User: - return auth.get_user_model().objects.create_user(username=MOCK_USERNAME, - password=MOCK_PASSWORD) + return auth.get_user_model().objects.create_user( + username=MOCK_USERNAME, password=MOCK_PASSWORD + ) def create_station() -> Station: - return Station.objects.create(title='Station1') + return Station.objects.create(title="Station1") -def create_listener(station: Station, user: User, is_admin=False, - is_dj=False) -> Listener: - return Listener.objects.create(station=station, - user=user, - is_admin=is_admin, - is_dj=is_dj) +def create_listener( + station: Station, user: User, is_admin=False, is_dj=False +) -> Listener: + return Listener.objects.create( + station=station, user=user, is_admin=is_admin, is_dj=is_dj + ) diff --git a/radio/urls.py b/radio/urls.py index 874495b..b33b7d1 100644 --- a/radio/urls.py +++ b/radio/urls.py @@ -2,11 +2,11 @@ from . import views -app_name = 'radio' +app_name = "radio" urlpatterns = [ - path('', views.IndexView.as_view(), name='index'), - path('/', views.DetailStationView.as_view(), name='detail'), - path('/delete/', views.DeleteStationView.as_view(), name='delete'), - path('request-authorization-callback', views.oauth_callback) + path("", views.IndexView.as_view(), name="index"), + path("/", views.DetailStationView.as_view(), name="detail"), + path("/delete/", views.DeleteStationView.as_view(), name="delete"), + path("request-authorization-callback", views.oauth_callback), ] diff --git a/radio/views.py b/radio/views.py index 174f716..d68fb81 100644 --- a/radio/views.py +++ b/radio/views.py @@ -16,14 +16,15 @@ class ListenerRequiredMixin: - user_check_failure_path = '' + user_check_failure_path = "" def dispatch(self, request: HttpRequest, *args, **kwargs): # Django does not have a base class for mixins, so ignore some type errors get_object_or_404( Listener, user=self.request.user, # type: ignore - station_id=kwargs['pk']) + station_id=kwargs["pk"], + ) return super().dispatch(request, *args, **kwargs) # type: ignore @@ -39,74 +40,76 @@ def post(self, request: HttpRequest, *args, **kwargs): class ListStationsView(generic.ListView): model = Station - context_object_name = 'stations' - template_name = 'radio/index.html' + context_object_name = "stations" + template_name = "radio/index.html" def get_queryset(self): - station_ids = Listener.objects.filter( - user=self.request.user).values_list('station_id', flat=True) + station_ids = Listener.objects.filter(user=self.request.user).values_list( + "station_id", flat=True + ) return Station.objects.filter(id__in=station_ids) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['form'] = StationForm + context["form"] = StationForm return context class CreateStationView(CreateView): model = Station form_class = StationForm - template_name = 'radio/index.html' + template_name = "radio/index.html" def form_valid(self, form): response = super().form_valid(form) # Creator is automatically an admin and DJ of this station - Listener.objects.create(user=self.request.user, - station=self.object, - is_admin=True, - is_dj=True) + Listener.objects.create( + user=self.request.user, station=self.object, is_admin=True, is_dj=True + ) return response -class DetailStationView(LoginRequiredMixin, ListenerRequiredMixin, - spotify.AuthorizationRequiredMixin, - spotify.FreshAccessTokenRequiredMixin, - generic.DetailView): +class DetailStationView( + LoginRequiredMixin, + ListenerRequiredMixin, + spotify.AuthorizationRequiredMixin, + spotify.FreshAccessTokenRequiredMixin, + generic.DetailView, +): model = Station - template_name = 'radio/detail.html' + template_name = "radio/detail.html" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['user_id'] = self.request.user.id - context['debug'] = settings.DEBUG + context["user_id"] = self.request.user.id + context["debug"] = settings.DEBUG - listener = get_object_or_404(Listener, - user=self.request.user, - station=context['object']) - context['is_dj'] = listener.is_dj - context['is_admin'] = listener.is_admin + listener = get_object_or_404( + Listener, user=self.request.user, station=context["object"] + ) + context["is_dj"] = listener.is_dj + context["is_admin"] = listener.is_admin - context['player_name'] = settings.SPOTIFY_PLAYER_NAME - context['access_token'] = self.request.session['access_token'] + context["player_name"] = settings.SPOTIFY_PLAYER_NAME + context["access_token"] = self.request.session["access_token"] return context class DeleteStationView(LoginRequiredMixin, ListenerRequiredMixin, DeleteView): model = Station - success_url = reverse_lazy('radio:index') + success_url = reverse_lazy("radio:index") def post(self, request: HttpRequest, *args, **kwargs): # precondition: ListenerRequiredMixin - listener = Listener.objects.get(user=request.user, - station_id=kwargs['pk']) + listener = Listener.objects.get(user=request.user, station_id=kwargs["pk"]) if listener.is_admin: return super().post(request, *args, **kwargs) else: - return redirect('/stations/') + return redirect("/stations/") def oauth_callback(request: HttpRequest): @@ -114,17 +117,16 @@ def oauth_callback(request: HttpRequest): # Validate OAuth state parameter expected_state = spotify.get_url_safe_oauth_request_state(request) - if result['state'] != expected_state: - logger.warning('User received invalid oauth request state response') - return HttpResponse('Invalid OAuth state', status_code=400) - - if 'error' in result: - error = result['error'] - logger.warning(f'User rejected the oauth permissions: {error}') - return redirect('/') + if result["state"] != expected_state: + logger.warning("User received invalid oauth request state response") + return HttpResponse("Invalid OAuth state", status_code=400) + + if "error" in result: + error = result["error"] + logger.warning(f"User rejected the oauth permissions: {error}") + return redirect("/") else: - code = result['code'] - spotify.AccessToken.request_refresh_and_access_token( - code, request.user) + code = result["code"] + spotify.AccessToken.request_refresh_and_access_token(code, request.user) # TODO: redirect to original destination before oauth request - return redirect('/stations') + return redirect("/stations") diff --git a/runtime.txt b/runtime.txt new file mode 100644 index 0000000..c5d7ce3 --- /dev/null +++ b/runtime.txt @@ -0,0 +1 @@ +python-3.8.1 diff --git a/templates/base.html b/templates/base.html index 8edc7f5..604d613 100644 --- a/templates/base.html +++ b/templates/base.html @@ -75,4 +75,3 @@ {% block extra_script %}{% endblock %} {% endblock %} - diff --git a/tools/scripts/dev_setup.sh b/tools/scripts/dev_setup.sh old mode 100644 new mode 100755 index 508cbe8..5e445ef --- a/tools/scripts/dev_setup.sh +++ b/tools/scripts/dev_setup.sh @@ -2,7 +2,7 @@ set -euo pipefail -readonly repo_root="$(git rev-parse --show-toplevel)" +readonly python_version="3.8.1" # OS detection is_macos() { @@ -11,19 +11,16 @@ is_macos() { is_ubuntu() { [[ "$(cat /etc/issue 2> /dev/null)" =~ Ubuntu ]] || return 1 } -get_os() { - for os in macos ubuntu; do - "is_$os"; [[ $? == "${1:-0}" ]] && echo $os - done -} if is_macos; then - brew install heroku python3 getsentry/tools/sentry-cli || true + brew install heroku python3 pyenv getsentry/tools/sentry-cli || true + pyenv install "${python_version}" --skip-existing + + virtualenv_name="dancingtogether-${python_version}" + pyenv virtualenv "${python_version}" "${virtualenv_name}" || true + pyenv local "${virtualenv_name}" elif is_ubuntu; then - # Install Python3.6 - sudo add-apt-repository ppa:jonathonf/python-3.6 || true - sudo apt-get update - sudo apt-get install python3.6 || true + echo "You are on your own for installing python ${python_version}" 2>&1 # Install heroku sudo add-apt-repository "deb https://cli-assets.heroku.com/branches/stable/apt ./" @@ -33,7 +30,7 @@ elif is_ubuntu; then fi git remote add heroku https://git.heroku.com/dancingtogether.git || true -ln -s -f "$repo_root"/tools/scripts/git-hooks/pre-commit .git/hooks/pre-commit -pip3 install pipenv -pipenv install --dev +python3 -m pip install pipenv +python3 -m pipenv install --dev +python3 -m pre-commit install diff --git a/tools/scripts/git-hooks/pre-commit b/tools/scripts/git-hooks/pre-commit deleted file mode 100755 index a21bd6c..0000000 --- a/tools/scripts/git-hooks/pre-commit +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/sh - -readonly repo_root="$(git rev-parse --show-toplevel)" - -# warn and block commit for formatting changes -echo "Running pre-commit validation" -pipenv run yapf --diff --recursive --exclude '*migrations*' --parallel "$repo_root" >/dev/null -exit_status="$?" -if [ "$exit_status" -ne 0 ]; then - pipenv run yapf --in-place --recursive --exclude '*migrations*' "$repo_root" --parallel - >&2 echo "Review formatting changes" -fi - -exit "$exit_status" diff --git a/tools/scripts/test_all b/tools/scripts/test_all new file mode 100755 index 0000000..5c5457e --- /dev/null +++ b/tools/scripts/test_all @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# +# Runs all Dancing Together tests. + +set -euo pipefail + +repo_root="$(git rev-parse --show-toplevel)" +readonly repo_root + +set +e # allow failures +failed=0 +"${repo_root}/tools/scripts/test_frontend" || failed=1 +"${repo_root}/tools/scripts/test_backend" || failed=1 + +# Exit with error if any of the above failed +if [ "$failed" -ne 0 ] ; then + exit 1 +fi diff --git a/tools/scripts/test_backend b/tools/scripts/test_backend new file mode 100755 index 0000000..0a20677 --- /dev/null +++ b/tools/scripts/test_backend @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# +# Runs Dancing Together backend tests. + +set -euo pipefail + +DJANGO_SETTINGS_MODULE=dancingtogether.settings.test python manage.py collectstatic --noinput +DJANGO_SETTINGS_MODULE=dancingtogether.settings.test python manage.py test --junit-xml junit/test-backend.xml --no-input diff --git a/tools/scripts/test_frontend b/tools/scripts/test_frontend new file mode 100755 index 0000000..91e0728 --- /dev/null +++ b/tools/scripts/test_frontend @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +# +# Runs Dancing Together frontend tests. + +repo_root="$(git rev-parse --show-toplevel)" +readonly repo_root + +pushd "${repo_root}/frontend" || exit 1 +npm run build +CI=1 JEST_JUNIT_OUTPUT=junit/test-frontend.xml npm test -- --ci --reporters=default --reporters=jest-junit