From 18479a3f568bc75b7be68c88a00bafc7a5cadb61 Mon Sep 17 00:00:00 2001 From: Sonny Date: Wed, 15 Apr 2020 22:07:12 +0200 Subject: [PATCH] 0.2 release --- .babelrc | 11 + .coveragerc | 16 + .gitignore | 207 + .gitlab-ci.yml | 28 + .isort.cfg | 12 + .prettierrc.json | 10 + Dockerfile | 11 + docker-compose.yml | 52 + gitlab-ci/build.yml | 7 + gitlab-ci/deploy.yml | 16 + gitlab-ci/lint.yml | 22 + gitlab-ci/test.yml | 23 + jest.config.js | 188 + package-lock.json | 9443 +++++++++++++++++ package.json | 61 + poetry.lock | 1159 ++ pyproject.toml | 40 + src/entrypoint.sh | 5 + src/manage.py | 21 + src/newsreader/__init__.py | 4 + src/newsreader/accounts/__init__.py | 0 src/newsreader/accounts/admin.py | 1 + src/newsreader/accounts/apps.py | 5 + .../accounts/migrations/0001_initial.py | 151 + .../migrations/0002_remove_user_username.py | 10 + .../migrations/0003_auto_20190714_1417.py | 17 + .../migrations/0004_auto_20190714_1501.py | 22 + .../0005_remove_user_task_interval.py | 10 + .../migrations/0006_auto_20191116_1253.py | 24 + .../migrations/0007_auto_20191116_1255.py | 25 + .../accounts/migrations/__init__.py | 0 src/newsreader/accounts/models.py | 80 + src/newsreader/accounts/permissions.py | 23 + .../accounts/templates/accounts/login.html | 24 + src/newsreader/accounts/tests/__init__.py | 0 src/newsreader/accounts/tests/factories.py | 39 + .../accounts/tests/test_activation.py | 99 + .../accounts/tests/test_password_reset.py | 160 + .../accounts/tests/test_registration.py | 110 + .../accounts/tests/test_resend_activation.py | 77 + src/newsreader/accounts/tests/tests.py | 22 + src/newsreader/accounts/urls.py | 56 + src/newsreader/accounts/views.py | 91 + src/newsreader/celery.py | 14 + src/newsreader/conf/__init__.py | 0 src/newsreader/conf/base.py | 160 + src/newsreader/conf/dev.py | 35 + src/newsreader/conf/docker.py | 19 + src/newsreader/conf/gitlab.py | 19 + src/newsreader/conf/production.py | 45 + src/newsreader/core/__init__.py | 0 src/newsreader/core/admin.py | 1 + src/newsreader/core/apps.py | 5 + src/newsreader/core/migrations/__init__.py | 0 src/newsreader/core/models.py | 15 + src/newsreader/core/pagination.py | 12 + src/newsreader/core/permissions.py | 6 + src/newsreader/core/tests.py | 1 + src/newsreader/core/views.py | 1 + src/newsreader/fixtures/default-fixture.json | 298 + src/newsreader/fixtures/local/fixture.json | 168 + src/newsreader/js/components/Card.js | 13 + .../js/components/LoadingIndicator.js | 13 + src/newsreader/js/components/Messages.js | 29 + src/newsreader/js/components/Modal.js | 11 + src/newsreader/js/index.js | 3 + src/newsreader/js/pages/categories/App.js | 106 + .../categories/components/CategoryCard.js | 51 + .../categories/components/CategoryModal.js | 37 + src/newsreader/js/pages/categories/index.js | 13 + src/newsreader/js/pages/homepage/App.js | 64 + .../js/pages/homepage/actions/categories.js | 87 + .../js/pages/homepage/actions/error.js | 6 + .../js/pages/homepage/actions/posts.js | 89 + .../js/pages/homepage/actions/rules.js | 75 + .../js/pages/homepage/actions/selected.js | 89 + .../js/pages/homepage/components/PostModal.js | 91 + .../homepage/components/feedlist/FeedList.js | 86 + .../homepage/components/feedlist/PostItem.js | 49 + .../homepage/components/feedlist/RuleItem.js | 24 + .../homepage/components/feedlist/filters.js | 46 + .../components/sidebar/CategoryItem.js | 62 + .../homepage/components/sidebar/ReadButton.js | 33 + .../homepage/components/sidebar/RuleItem.js | 50 + .../homepage/components/sidebar/Sidebar.js | 49 + .../homepage/components/sidebar/filters.js | 7 + .../homepage/components/sidebar/functions.js | 7 + .../js/pages/homepage/configureStore.js | 18 + src/newsreader/js/pages/homepage/constants.js | 2 + src/newsreader/js/pages/homepage/index.js | 20 + .../js/pages/homepage/reducers/categories.js | 93 + .../js/pages/homepage/reducers/error.js | 12 + .../js/pages/homepage/reducers/index.js | 11 + .../js/pages/homepage/reducers/posts.js | 68 + .../js/pages/homepage/reducers/rules.js | 82 + .../js/pages/homepage/reducers/selected.js | 86 + src/newsreader/js/pages/rules/App.js | 106 + .../js/pages/rules/components/RuleCard.js | 65 + .../js/pages/rules/components/RuleModal.js | 35 + src/newsreader/js/pages/rules/index.js | 13 + .../tests/homepage/actions/category.test.js | 316 + .../js/tests/homepage/actions/post.test.js | 408 + .../js/tests/homepage/actions/rule.test.js | 341 + .../tests/homepage/actions/selected.test.js | 237 + .../tests/homepage/reducers/category.test.js | 212 + .../js/tests/homepage/reducers/post.test.js | 307 + .../js/tests/homepage/reducers/rule.test.js | 184 + .../tests/homepage/reducers/selected.test.js | 425 + src/newsreader/js/utils.js | 24 + src/newsreader/news/__init__.py | 0 src/newsreader/news/collection/__init__.py | 0 src/newsreader/news/collection/admin.py | 18 + src/newsreader/news/collection/apps.py | 5 + src/newsreader/news/collection/base.py | 115 + src/newsreader/news/collection/constants.py | 28 + src/newsreader/news/collection/endpoints.py | 65 + src/newsreader/news/collection/exceptions.py | 32 + src/newsreader/news/collection/favicon.py | 120 + src/newsreader/news/collection/feed.py | 256 + src/newsreader/news/collection/forms.py | 41 + .../collection/management/commands/collect.py | 11 + .../management/commands/fetch_favicons.py | 11 + .../collection/migrations/0001_initial.py | 687 ++ .../migrations/0002_auto_20190714_1036.py | 39 + .../migrations/0003_auto_20190714_1417.py | 21 + .../migrations/0004_auto_20190714_1422.py | 26 + .../migrations/0005_auto_20200303_1932.py | 16 + .../migrations/0006_auto_20200412_1955.py | 27 + .../news/collection/migrations/__init__.py | 0 src/newsreader/news/collection/models.py | 46 + .../news/collection/response_handler.py | 42 + src/newsreader/news/collection/serializers.py | 15 + src/newsreader/news/collection/tasks.py | 21 + .../templates/collection/import.html | 37 + .../templates/collection/rule-create.html | 9 + .../templates/collection/rule-update.html | 9 + .../collection/templates/collection/rule.html | 55 + .../templates/collection/rules.html | 30 + .../news/collection/tests/__init__.py | 0 .../collection/tests/endpoints/__init__.py | 0 .../tests/endpoints/rule/__init__.py | 0 .../tests/endpoints/rule/detail/__init__.py | 0 .../tests/endpoints/rule/detail/tests.py | 239 + .../tests/endpoints/rule/list/__init__.py | 0 .../tests/endpoints/rule/list/tests.py | 367 + .../news/collection/tests/factories.py | 19 + .../news/collection/tests/favicon/__init__.py | 0 .../tests/favicon/builder/__init__.py | 0 .../collection/tests/favicon/builder/mocks.py | 89 + .../collection/tests/favicon/builder/tests.py | 62 + .../tests/favicon/client/__init__.py | 0 .../collection/tests/favicon/client/mocks.py | 13 + .../collection/tests/favicon/client/tests.py | 90 + .../tests/favicon/collector/__init__.py | 0 .../tests/favicon/collector/mocks.py | 166 + .../tests/favicon/collector/tests.py | 148 + .../news/collection/tests/feed/__init__.py | 0 .../collection/tests/feed/builder/__init__.py | 0 .../tests/feed/builder/mock_html.py | 14 + .../collection/tests/feed/builder/mocks.py | 343 + .../collection/tests/feed/builder/tests.py | 373 + .../collection/tests/feed/client/__init__.py | 0 .../collection/tests/feed/client/mocks.py | 69 + .../collection/tests/feed/client/tests.py | 130 + .../tests/feed/collector/__init__.py | 0 .../collection/tests/feed/collector/mocks.py | 466 + .../collection/tests/feed/collector/tests.py | 247 + .../tests/feed/duplicate_handler/__init__.py | 0 .../tests/feed/duplicate_handler/tests.py | 83 + .../collection/tests/feed/stream/__init__.py | 0 .../collection/tests/feed/stream/mocks.py | 69 + .../collection/tests/feed/stream/tests.py | 105 + src/newsreader/news/collection/tests/mocks.py | 53 + .../news/collection/tests/test_views.py | 284 + src/newsreader/news/collection/tests/tests.py | 136 + .../news/collection/tests/utils/__init__.py | 0 .../news/collection/tests/utils/tests.py | 102 + src/newsreader/news/collection/urls.py | 38 + src/newsreader/news/collection/utils.py | 29 + src/newsreader/news/collection/views.py | 87 + src/newsreader/news/core/__init__.py | 0 src/newsreader/news/core/admin.py | 26 + src/newsreader/news/core/apps.py | 5 + src/newsreader/news/core/endpoints.py | 116 + src/newsreader/news/core/filters.py | 32 + src/newsreader/news/core/forms.py | 44 + .../news/core/migrations/0001_initial.py | 80 + .../migrations/0002_auto_20190714_1425.py | 33 + .../news/core/migrations/0003_post_read.py | 14 + .../migrations/0004_auto_20191116_1315.py | 21 + .../migrations/0005_auto_20200412_1955.py | 27 + .../news/core/migrations/__init__.py | 0 src/newsreader/news/core/models.py | 47 + src/newsreader/news/core/serializers.py | 38 + .../news/core/templates/core/categories.html | 35 + .../core/templates/core/category-create.html | 9 + .../core/templates/core/category-update.html | 9 + .../news/core/templates/core/category.html | 62 + .../news/core/templates/core/homepage.html | 7 + src/newsreader/news/core/tests/__init__.py | 0 .../news/core/tests/endpoints/__init__.py | 0 .../core/tests/endpoints/category/__init__.py | 0 .../endpoints/category/detail/__init__.py | 0 .../tests/endpoints/category/detail/tests.py | 215 + .../tests/endpoints/category/list/__init__.py | 0 .../tests/endpoints/category/list/tests.py | 568 + .../core/tests/endpoints/post/__init__.py | 0 .../tests/endpoints/post/detail/__init__.py | 0 .../core/tests/endpoints/post/detail/tests.py | 218 + .../tests/endpoints/post/list/__init__.py | 0 .../core/tests/endpoints/post/list/tests.py | 242 + src/newsreader/news/core/tests/factories.py | 31 + src/newsreader/news/core/tests/test_views.py | 229 + src/newsreader/news/core/urls.py | 56 + src/newsreader/news/core/views.py | 68 + .../scss/components/body/_body.scss | 19 + .../scss/components/body/index.scss | 1 + .../scss/components/card/_card.scss | 36 + .../scss/components/card/_rule-card.scss | 26 + .../scss/components/card/index.scss | 2 + .../scss/components/category/_category.scss | 45 + .../scss/components/category/index.scss | 1 + .../scss/components/errorlist/_errorlist.scss | 23 + .../scss/components/errorlist/index.scss | 1 + .../scss/components/fieldset/_fieldset.scss | 11 + .../scss/components/fieldset/index.scss | 1 + .../components/form/_activation-form.scss | 11 + .../scss/components/form/_category-form.scss | 13 + .../scss/components/form/_form.scss | 33 + .../scss/components/form/_import-form.scss | 17 + .../scss/components/form/_login-form.scss | 33 + .../form/_password-reset-confirm-form.scss | 3 + .../components/form/_password-reset-form.scss | 18 + .../scss/components/form/_register-form.scss | 11 + .../scss/components/form/_rule-form.scss | 25 + .../scss/components/form/index.scss | 12 + src/newsreader/scss/components/index.scss | 26 + .../scss/components/list/_list.scss | 20 + .../scss/components/list/index.scss | 1 + .../loading-indicator/_loading-indicator.scss | 41 + .../components/loading-indicator/index.scss | 1 + .../scss/components/main/_main.scss | 7 + .../scss/components/main/index.scss | 1 + .../scss/components/messages/_messages.scss | 43 + .../scss/components/messages/index.scss | 1 + .../scss/components/modal/_modal.scss | 42 + .../scss/components/modal/_post-modal.scss | 9 + .../scss/components/modal/index.scss | 3 + .../scss/components/navbar/_navbar.scss | 45 + .../scss/components/navbar/index.scss | 1 + .../components/post-block/_post-block.scss | 12 + .../scss/components/post-block/index.scss | 1 + .../post-message/_post-message.scss | 25 + .../scss/components/post-message/index.scss | 1 + .../scss/components/post/_post.scss | 132 + .../scss/components/post/index.scss | 1 + .../posts-header/_posts-header.scss | 15 + .../scss/components/posts-header/index.scss | 1 + .../components/posts-info/_posts-info.scss | 15 + .../scss/components/posts-info/index.scss | 1 + .../posts-section/_post-section.scss | 20 + .../scss/components/posts-section/index.scss | 1 + .../scss/components/posts/_posts.scss | 34 + .../scss/components/posts/index.scss | 1 + .../scss/components/rules/_rules.scss | 52 + .../scss/components/rules/index.scss | 1 + .../scss/components/section/_section.scss | 6 + .../scss/components/section/index.scss | 1 + .../scss/components/sidebar/_sidebar.scss | 26 + .../scss/components/sidebar/index.scss | 1 + .../scss/elements/badge/_badge.scss | 14 + src/newsreader/scss/elements/badge/index.scss | 1 + .../scss/elements/button/_button.scss | 46 + .../scss/elements/button/_read-button.scss | 12 + .../scss/elements/button/index.scss | 2 + src/newsreader/scss/elements/h1/_h1.scss | 3 + src/newsreader/scss/elements/h1/index.scss | 1 + src/newsreader/scss/elements/h2/_h2.scss | 3 + src/newsreader/scss/elements/h2/index.scss | 1 + src/newsreader/scss/elements/h3/_h3.scss | 3 + src/newsreader/scss/elements/h3/index.scss | 1 + .../scss/elements/help-text/_help-text.scss | 9 + .../scss/elements/help-text/index.scss | 1 + src/newsreader/scss/elements/index.scss | 10 + .../scss/elements/input/_input.scss | 15 + src/newsreader/scss/elements/input/index.scss | 1 + .../scss/elements/label/_label.scss | 7 + src/newsreader/scss/elements/label/index.scss | 1 + src/newsreader/scss/elements/link/_link.scss | 16 + src/newsreader/scss/elements/link/index.scss | 1 + .../scss/elements/small/_small.scss | 8 + src/newsreader/scss/elements/small/index.scss | 1 + src/newsreader/scss/index.scss | 6 + src/newsreader/scss/lib/_css.gg.scss | 1 + src/newsreader/scss/lib/index.scss | 1 + .../scss/pages/categories/index.scss | 7 + src/newsreader/scss/pages/category/index.scss | 2 + src/newsreader/scss/pages/homepage/index.scss | 9 + src/newsreader/scss/pages/import/index.scss | 1 + src/newsreader/scss/pages/index.scss | 12 + src/newsreader/scss/pages/login/index.scss | 6 + .../scss/pages/password-reset/index.scss | 1 + src/newsreader/scss/pages/register/index.scss | 1 + src/newsreader/scss/pages/rule/index.scss | 1 + src/newsreader/scss/pages/rules/index.scss | 7 + src/newsreader/scss/partials/_colors.scss | 38 + src/newsreader/scss/partials/_fonts.scss | 17 + src/newsreader/scss/partials/index.scss | 2 + src/newsreader/static/favicon.png | Bin 0 -> 1913 bytes src/newsreader/templates/base.html | 45 + .../password_reset_complete.html | 23 + .../password_reset_confirm.html | 55 + .../password-reset/password_reset_done.html | 23 + .../password-reset/password_reset_email.html | 30 + .../password-reset/password_reset_form.html | 30 + .../password-reset/password_reset_subject.txt | 6 + .../registration/activation_complete.html | 31 + .../registration/activation_email.html | 72 + .../registration/activation_email.txt | 52 + .../registration/activation_email_subject.txt | 28 + .../registration/activation_failure.html | 27 + .../activation_resend_complete.html | 31 + .../registration/activation_resend_form.html | 35 + .../registration/registration_closed.html | 20 + .../registration/registration_complete.html | 29 + .../registration/registration_form.html | 22 + src/newsreader/urls.py | 37 + src/newsreader/utils/celery.py | 22 + src/newsreader/utils/opml.py | 41 + src/newsreader/utils/tests/__init__.py | 0 .../utils/tests/files/empty-feeds.opml | 9 + src/newsreader/utils/tests/files/feeds.opml | 15 + .../utils/tests/files/invalid-url-feeds.opml | 16 + .../utils/tests/files/missing-feeds.opml | 17 + src/newsreader/utils/tests/files/test.png | Bin 0 -> 81814 bytes src/newsreader/utils/tests/test_opml.py | 46 + src/newsreader/wsgi.py | 17 + webpack.common.babel.js | 35 + webpack.dev.babel.js | 7 + webpack.prod.babel.js | 4 + 340 files changed, 27295 insertions(+) create mode 100644 .babelrc create mode 100644 .coveragerc create mode 100644 .gitignore create mode 100644 .gitlab-ci.yml create mode 100644 .isort.cfg create mode 100644 .prettierrc.json create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100644 gitlab-ci/build.yml create mode 100644 gitlab-ci/deploy.yml create mode 100644 gitlab-ci/lint.yml create mode 100644 gitlab-ci/test.yml create mode 100644 jest.config.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 poetry.lock create mode 100644 pyproject.toml create mode 100755 src/entrypoint.sh create mode 100755 src/manage.py create mode 100644 src/newsreader/__init__.py create mode 100644 src/newsreader/accounts/__init__.py create mode 100644 src/newsreader/accounts/admin.py create mode 100644 src/newsreader/accounts/apps.py create mode 100644 src/newsreader/accounts/migrations/0001_initial.py create mode 100644 src/newsreader/accounts/migrations/0002_remove_user_username.py create mode 100644 src/newsreader/accounts/migrations/0003_auto_20190714_1417.py create mode 100644 src/newsreader/accounts/migrations/0004_auto_20190714_1501.py create mode 100644 src/newsreader/accounts/migrations/0005_remove_user_task_interval.py create mode 100644 src/newsreader/accounts/migrations/0006_auto_20191116_1253.py create mode 100644 src/newsreader/accounts/migrations/0007_auto_20191116_1255.py create mode 100644 src/newsreader/accounts/migrations/__init__.py create mode 100644 src/newsreader/accounts/models.py create mode 100644 src/newsreader/accounts/permissions.py create mode 100644 src/newsreader/accounts/templates/accounts/login.html create mode 100644 src/newsreader/accounts/tests/__init__.py create mode 100644 src/newsreader/accounts/tests/factories.py create mode 100644 src/newsreader/accounts/tests/test_activation.py create mode 100644 src/newsreader/accounts/tests/test_password_reset.py create mode 100644 src/newsreader/accounts/tests/test_registration.py create mode 100644 src/newsreader/accounts/tests/test_resend_activation.py create mode 100644 src/newsreader/accounts/tests/tests.py create mode 100644 src/newsreader/accounts/urls.py create mode 100644 src/newsreader/accounts/views.py create mode 100644 src/newsreader/celery.py create mode 100644 src/newsreader/conf/__init__.py create mode 100644 src/newsreader/conf/base.py create mode 100644 src/newsreader/conf/dev.py create mode 100644 src/newsreader/conf/docker.py create mode 100644 src/newsreader/conf/gitlab.py create mode 100644 src/newsreader/conf/production.py create mode 100644 src/newsreader/core/__init__.py create mode 100644 src/newsreader/core/admin.py create mode 100644 src/newsreader/core/apps.py create mode 100644 src/newsreader/core/migrations/__init__.py create mode 100644 src/newsreader/core/models.py create mode 100644 src/newsreader/core/pagination.py create mode 100644 src/newsreader/core/permissions.py create mode 100644 src/newsreader/core/tests.py create mode 100644 src/newsreader/core/views.py create mode 100644 src/newsreader/fixtures/default-fixture.json create mode 100644 src/newsreader/fixtures/local/fixture.json create mode 100644 src/newsreader/js/components/Card.js create mode 100644 src/newsreader/js/components/LoadingIndicator.js create mode 100644 src/newsreader/js/components/Messages.js create mode 100644 src/newsreader/js/components/Modal.js create mode 100644 src/newsreader/js/index.js create mode 100644 src/newsreader/js/pages/categories/App.js create mode 100644 src/newsreader/js/pages/categories/components/CategoryCard.js create mode 100644 src/newsreader/js/pages/categories/components/CategoryModal.js create mode 100644 src/newsreader/js/pages/categories/index.js create mode 100644 src/newsreader/js/pages/homepage/App.js create mode 100644 src/newsreader/js/pages/homepage/actions/categories.js create mode 100644 src/newsreader/js/pages/homepage/actions/error.js create mode 100644 src/newsreader/js/pages/homepage/actions/posts.js create mode 100644 src/newsreader/js/pages/homepage/actions/rules.js create mode 100644 src/newsreader/js/pages/homepage/actions/selected.js create mode 100644 src/newsreader/js/pages/homepage/components/PostModal.js create mode 100644 src/newsreader/js/pages/homepage/components/feedlist/FeedList.js create mode 100644 src/newsreader/js/pages/homepage/components/feedlist/PostItem.js create mode 100644 src/newsreader/js/pages/homepage/components/feedlist/RuleItem.js create mode 100644 src/newsreader/js/pages/homepage/components/feedlist/filters.js create mode 100644 src/newsreader/js/pages/homepage/components/sidebar/CategoryItem.js create mode 100644 src/newsreader/js/pages/homepage/components/sidebar/ReadButton.js create mode 100644 src/newsreader/js/pages/homepage/components/sidebar/RuleItem.js create mode 100644 src/newsreader/js/pages/homepage/components/sidebar/Sidebar.js create mode 100644 src/newsreader/js/pages/homepage/components/sidebar/filters.js create mode 100644 src/newsreader/js/pages/homepage/components/sidebar/functions.js create mode 100644 src/newsreader/js/pages/homepage/configureStore.js create mode 100644 src/newsreader/js/pages/homepage/constants.js create mode 100644 src/newsreader/js/pages/homepage/index.js create mode 100644 src/newsreader/js/pages/homepage/reducers/categories.js create mode 100644 src/newsreader/js/pages/homepage/reducers/error.js create mode 100644 src/newsreader/js/pages/homepage/reducers/index.js create mode 100644 src/newsreader/js/pages/homepage/reducers/posts.js create mode 100644 src/newsreader/js/pages/homepage/reducers/rules.js create mode 100644 src/newsreader/js/pages/homepage/reducers/selected.js create mode 100644 src/newsreader/js/pages/rules/App.js create mode 100644 src/newsreader/js/pages/rules/components/RuleCard.js create mode 100644 src/newsreader/js/pages/rules/components/RuleModal.js create mode 100644 src/newsreader/js/pages/rules/index.js create mode 100644 src/newsreader/js/tests/homepage/actions/category.test.js create mode 100644 src/newsreader/js/tests/homepage/actions/post.test.js create mode 100644 src/newsreader/js/tests/homepage/actions/rule.test.js create mode 100644 src/newsreader/js/tests/homepage/actions/selected.test.js create mode 100644 src/newsreader/js/tests/homepage/reducers/category.test.js create mode 100644 src/newsreader/js/tests/homepage/reducers/post.test.js create mode 100644 src/newsreader/js/tests/homepage/reducers/rule.test.js create mode 100644 src/newsreader/js/tests/homepage/reducers/selected.test.js create mode 100644 src/newsreader/js/utils.js create mode 100644 src/newsreader/news/__init__.py create mode 100644 src/newsreader/news/collection/__init__.py create mode 100644 src/newsreader/news/collection/admin.py create mode 100644 src/newsreader/news/collection/apps.py create mode 100644 src/newsreader/news/collection/base.py create mode 100644 src/newsreader/news/collection/constants.py create mode 100644 src/newsreader/news/collection/endpoints.py create mode 100644 src/newsreader/news/collection/exceptions.py create mode 100644 src/newsreader/news/collection/favicon.py create mode 100644 src/newsreader/news/collection/feed.py create mode 100644 src/newsreader/news/collection/forms.py create mode 100644 src/newsreader/news/collection/management/commands/collect.py create mode 100644 src/newsreader/news/collection/management/commands/fetch_favicons.py create mode 100644 src/newsreader/news/collection/migrations/0001_initial.py create mode 100644 src/newsreader/news/collection/migrations/0002_auto_20190714_1036.py create mode 100644 src/newsreader/news/collection/migrations/0003_auto_20190714_1417.py create mode 100644 src/newsreader/news/collection/migrations/0004_auto_20190714_1422.py create mode 100644 src/newsreader/news/collection/migrations/0005_auto_20200303_1932.py create mode 100644 src/newsreader/news/collection/migrations/0006_auto_20200412_1955.py create mode 100644 src/newsreader/news/collection/migrations/__init__.py create mode 100644 src/newsreader/news/collection/models.py create mode 100644 src/newsreader/news/collection/response_handler.py create mode 100644 src/newsreader/news/collection/serializers.py create mode 100644 src/newsreader/news/collection/tasks.py create mode 100644 src/newsreader/news/collection/templates/collection/import.html create mode 100644 src/newsreader/news/collection/templates/collection/rule-create.html create mode 100644 src/newsreader/news/collection/templates/collection/rule-update.html create mode 100644 src/newsreader/news/collection/templates/collection/rule.html create mode 100644 src/newsreader/news/collection/templates/collection/rules.html create mode 100644 src/newsreader/news/collection/tests/__init__.py create mode 100644 src/newsreader/news/collection/tests/endpoints/__init__.py create mode 100644 src/newsreader/news/collection/tests/endpoints/rule/__init__.py create mode 100644 src/newsreader/news/collection/tests/endpoints/rule/detail/__init__.py create mode 100644 src/newsreader/news/collection/tests/endpoints/rule/detail/tests.py create mode 100644 src/newsreader/news/collection/tests/endpoints/rule/list/__init__.py create mode 100644 src/newsreader/news/collection/tests/endpoints/rule/list/tests.py create mode 100644 src/newsreader/news/collection/tests/factories.py create mode 100644 src/newsreader/news/collection/tests/favicon/__init__.py create mode 100644 src/newsreader/news/collection/tests/favicon/builder/__init__.py create mode 100644 src/newsreader/news/collection/tests/favicon/builder/mocks.py create mode 100644 src/newsreader/news/collection/tests/favicon/builder/tests.py create mode 100644 src/newsreader/news/collection/tests/favicon/client/__init__.py create mode 100644 src/newsreader/news/collection/tests/favicon/client/mocks.py create mode 100644 src/newsreader/news/collection/tests/favicon/client/tests.py create mode 100644 src/newsreader/news/collection/tests/favicon/collector/__init__.py create mode 100644 src/newsreader/news/collection/tests/favicon/collector/mocks.py create mode 100644 src/newsreader/news/collection/tests/favicon/collector/tests.py create mode 100644 src/newsreader/news/collection/tests/feed/__init__.py create mode 100644 src/newsreader/news/collection/tests/feed/builder/__init__.py create mode 100644 src/newsreader/news/collection/tests/feed/builder/mock_html.py create mode 100644 src/newsreader/news/collection/tests/feed/builder/mocks.py create mode 100644 src/newsreader/news/collection/tests/feed/builder/tests.py create mode 100644 src/newsreader/news/collection/tests/feed/client/__init__.py create mode 100644 src/newsreader/news/collection/tests/feed/client/mocks.py create mode 100644 src/newsreader/news/collection/tests/feed/client/tests.py create mode 100644 src/newsreader/news/collection/tests/feed/collector/__init__.py create mode 100644 src/newsreader/news/collection/tests/feed/collector/mocks.py create mode 100644 src/newsreader/news/collection/tests/feed/collector/tests.py create mode 100644 src/newsreader/news/collection/tests/feed/duplicate_handler/__init__.py create mode 100644 src/newsreader/news/collection/tests/feed/duplicate_handler/tests.py create mode 100644 src/newsreader/news/collection/tests/feed/stream/__init__.py create mode 100644 src/newsreader/news/collection/tests/feed/stream/mocks.py create mode 100644 src/newsreader/news/collection/tests/feed/stream/tests.py create mode 100644 src/newsreader/news/collection/tests/mocks.py create mode 100644 src/newsreader/news/collection/tests/test_views.py create mode 100644 src/newsreader/news/collection/tests/tests.py create mode 100644 src/newsreader/news/collection/tests/utils/__init__.py create mode 100644 src/newsreader/news/collection/tests/utils/tests.py create mode 100644 src/newsreader/news/collection/urls.py create mode 100644 src/newsreader/news/collection/utils.py create mode 100644 src/newsreader/news/collection/views.py create mode 100644 src/newsreader/news/core/__init__.py create mode 100644 src/newsreader/news/core/admin.py create mode 100644 src/newsreader/news/core/apps.py create mode 100644 src/newsreader/news/core/endpoints.py create mode 100644 src/newsreader/news/core/filters.py create mode 100644 src/newsreader/news/core/forms.py create mode 100644 src/newsreader/news/core/migrations/0001_initial.py create mode 100644 src/newsreader/news/core/migrations/0002_auto_20190714_1425.py create mode 100644 src/newsreader/news/core/migrations/0003_post_read.py create mode 100644 src/newsreader/news/core/migrations/0004_auto_20191116_1315.py create mode 100644 src/newsreader/news/core/migrations/0005_auto_20200412_1955.py create mode 100644 src/newsreader/news/core/migrations/__init__.py create mode 100644 src/newsreader/news/core/models.py create mode 100644 src/newsreader/news/core/serializers.py create mode 100644 src/newsreader/news/core/templates/core/categories.html create mode 100644 src/newsreader/news/core/templates/core/category-create.html create mode 100644 src/newsreader/news/core/templates/core/category-update.html create mode 100644 src/newsreader/news/core/templates/core/category.html create mode 100644 src/newsreader/news/core/templates/core/homepage.html create mode 100644 src/newsreader/news/core/tests/__init__.py create mode 100644 src/newsreader/news/core/tests/endpoints/__init__.py create mode 100644 src/newsreader/news/core/tests/endpoints/category/__init__.py create mode 100644 src/newsreader/news/core/tests/endpoints/category/detail/__init__.py create mode 100644 src/newsreader/news/core/tests/endpoints/category/detail/tests.py create mode 100644 src/newsreader/news/core/tests/endpoints/category/list/__init__.py create mode 100644 src/newsreader/news/core/tests/endpoints/category/list/tests.py create mode 100644 src/newsreader/news/core/tests/endpoints/post/__init__.py create mode 100644 src/newsreader/news/core/tests/endpoints/post/detail/__init__.py create mode 100644 src/newsreader/news/core/tests/endpoints/post/detail/tests.py create mode 100644 src/newsreader/news/core/tests/endpoints/post/list/__init__.py create mode 100644 src/newsreader/news/core/tests/endpoints/post/list/tests.py create mode 100644 src/newsreader/news/core/tests/factories.py create mode 100644 src/newsreader/news/core/tests/test_views.py create mode 100644 src/newsreader/news/core/urls.py create mode 100644 src/newsreader/news/core/views.py create mode 100644 src/newsreader/scss/components/body/_body.scss create mode 100644 src/newsreader/scss/components/body/index.scss create mode 100644 src/newsreader/scss/components/card/_card.scss create mode 100644 src/newsreader/scss/components/card/_rule-card.scss create mode 100644 src/newsreader/scss/components/card/index.scss create mode 100644 src/newsreader/scss/components/category/_category.scss create mode 100644 src/newsreader/scss/components/category/index.scss create mode 100644 src/newsreader/scss/components/errorlist/_errorlist.scss create mode 100644 src/newsreader/scss/components/errorlist/index.scss create mode 100644 src/newsreader/scss/components/fieldset/_fieldset.scss create mode 100644 src/newsreader/scss/components/fieldset/index.scss create mode 100644 src/newsreader/scss/components/form/_activation-form.scss create mode 100644 src/newsreader/scss/components/form/_category-form.scss create mode 100644 src/newsreader/scss/components/form/_form.scss create mode 100644 src/newsreader/scss/components/form/_import-form.scss create mode 100644 src/newsreader/scss/components/form/_login-form.scss create mode 100644 src/newsreader/scss/components/form/_password-reset-confirm-form.scss create mode 100644 src/newsreader/scss/components/form/_password-reset-form.scss create mode 100644 src/newsreader/scss/components/form/_register-form.scss create mode 100644 src/newsreader/scss/components/form/_rule-form.scss create mode 100644 src/newsreader/scss/components/form/index.scss create mode 100644 src/newsreader/scss/components/index.scss create mode 100644 src/newsreader/scss/components/list/_list.scss create mode 100644 src/newsreader/scss/components/list/index.scss create mode 100644 src/newsreader/scss/components/loading-indicator/_loading-indicator.scss create mode 100644 src/newsreader/scss/components/loading-indicator/index.scss create mode 100644 src/newsreader/scss/components/main/_main.scss create mode 100644 src/newsreader/scss/components/main/index.scss create mode 100644 src/newsreader/scss/components/messages/_messages.scss create mode 100644 src/newsreader/scss/components/messages/index.scss create mode 100644 src/newsreader/scss/components/modal/_modal.scss create mode 100644 src/newsreader/scss/components/modal/_post-modal.scss create mode 100644 src/newsreader/scss/components/modal/index.scss create mode 100644 src/newsreader/scss/components/navbar/_navbar.scss create mode 100644 src/newsreader/scss/components/navbar/index.scss create mode 100644 src/newsreader/scss/components/post-block/_post-block.scss create mode 100644 src/newsreader/scss/components/post-block/index.scss create mode 100644 src/newsreader/scss/components/post-message/_post-message.scss create mode 100644 src/newsreader/scss/components/post-message/index.scss create mode 100644 src/newsreader/scss/components/post/_post.scss create mode 100644 src/newsreader/scss/components/post/index.scss create mode 100644 src/newsreader/scss/components/posts-header/_posts-header.scss create mode 100644 src/newsreader/scss/components/posts-header/index.scss create mode 100644 src/newsreader/scss/components/posts-info/_posts-info.scss create mode 100644 src/newsreader/scss/components/posts-info/index.scss create mode 100644 src/newsreader/scss/components/posts-section/_post-section.scss create mode 100644 src/newsreader/scss/components/posts-section/index.scss create mode 100644 src/newsreader/scss/components/posts/_posts.scss create mode 100644 src/newsreader/scss/components/posts/index.scss create mode 100644 src/newsreader/scss/components/rules/_rules.scss create mode 100644 src/newsreader/scss/components/rules/index.scss create mode 100644 src/newsreader/scss/components/section/_section.scss create mode 100644 src/newsreader/scss/components/section/index.scss create mode 100644 src/newsreader/scss/components/sidebar/_sidebar.scss create mode 100644 src/newsreader/scss/components/sidebar/index.scss create mode 100644 src/newsreader/scss/elements/badge/_badge.scss create mode 100644 src/newsreader/scss/elements/badge/index.scss create mode 100644 src/newsreader/scss/elements/button/_button.scss create mode 100644 src/newsreader/scss/elements/button/_read-button.scss create mode 100644 src/newsreader/scss/elements/button/index.scss create mode 100644 src/newsreader/scss/elements/h1/_h1.scss create mode 100644 src/newsreader/scss/elements/h1/index.scss create mode 100644 src/newsreader/scss/elements/h2/_h2.scss create mode 100644 src/newsreader/scss/elements/h2/index.scss create mode 100644 src/newsreader/scss/elements/h3/_h3.scss create mode 100644 src/newsreader/scss/elements/h3/index.scss create mode 100644 src/newsreader/scss/elements/help-text/_help-text.scss create mode 100644 src/newsreader/scss/elements/help-text/index.scss create mode 100644 src/newsreader/scss/elements/index.scss create mode 100644 src/newsreader/scss/elements/input/_input.scss create mode 100644 src/newsreader/scss/elements/input/index.scss create mode 100644 src/newsreader/scss/elements/label/_label.scss create mode 100644 src/newsreader/scss/elements/label/index.scss create mode 100644 src/newsreader/scss/elements/link/_link.scss create mode 100644 src/newsreader/scss/elements/link/index.scss create mode 100644 src/newsreader/scss/elements/small/_small.scss create mode 100644 src/newsreader/scss/elements/small/index.scss create mode 100644 src/newsreader/scss/index.scss create mode 100644 src/newsreader/scss/lib/_css.gg.scss create mode 100644 src/newsreader/scss/lib/index.scss create mode 100644 src/newsreader/scss/pages/categories/index.scss create mode 100644 src/newsreader/scss/pages/category/index.scss create mode 100644 src/newsreader/scss/pages/homepage/index.scss create mode 100644 src/newsreader/scss/pages/import/index.scss create mode 100644 src/newsreader/scss/pages/index.scss create mode 100644 src/newsreader/scss/pages/login/index.scss create mode 100644 src/newsreader/scss/pages/password-reset/index.scss create mode 100644 src/newsreader/scss/pages/register/index.scss create mode 100644 src/newsreader/scss/pages/rule/index.scss create mode 100644 src/newsreader/scss/pages/rules/index.scss create mode 100644 src/newsreader/scss/partials/_colors.scss create mode 100644 src/newsreader/scss/partials/_fonts.scss create mode 100644 src/newsreader/scss/partials/index.scss create mode 100644 src/newsreader/static/favicon.png create mode 100644 src/newsreader/templates/base.html create mode 100755 src/newsreader/templates/password-reset/password_reset_complete.html create mode 100755 src/newsreader/templates/password-reset/password_reset_confirm.html create mode 100755 src/newsreader/templates/password-reset/password_reset_done.html create mode 100755 src/newsreader/templates/password-reset/password_reset_email.html create mode 100755 src/newsreader/templates/password-reset/password_reset_form.html create mode 100644 src/newsreader/templates/password-reset/password_reset_subject.txt create mode 100755 src/newsreader/templates/registration/activation_complete.html create mode 100644 src/newsreader/templates/registration/activation_email.html create mode 100644 src/newsreader/templates/registration/activation_email.txt create mode 100644 src/newsreader/templates/registration/activation_email_subject.txt create mode 100644 src/newsreader/templates/registration/activation_failure.html create mode 100644 src/newsreader/templates/registration/activation_resend_complete.html create mode 100644 src/newsreader/templates/registration/activation_resend_form.html create mode 100755 src/newsreader/templates/registration/registration_closed.html create mode 100755 src/newsreader/templates/registration/registration_complete.html create mode 100644 src/newsreader/templates/registration/registration_form.html create mode 100644 src/newsreader/urls.py create mode 100644 src/newsreader/utils/celery.py create mode 100644 src/newsreader/utils/opml.py create mode 100644 src/newsreader/utils/tests/__init__.py create mode 100644 src/newsreader/utils/tests/files/empty-feeds.opml create mode 100644 src/newsreader/utils/tests/files/feeds.opml create mode 100644 src/newsreader/utils/tests/files/invalid-url-feeds.opml create mode 100644 src/newsreader/utils/tests/files/missing-feeds.opml create mode 100644 src/newsreader/utils/tests/files/test.png create mode 100644 src/newsreader/utils/tests/test_opml.py create mode 100644 src/newsreader/wsgi.py create mode 100644 webpack.common.babel.js create mode 100644 webpack.dev.babel.js create mode 100644 webpack.prod.babel.js diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..610dee0 --- /dev/null +++ b/.babelrc @@ -0,0 +1,11 @@ +{ + "presets": ["@babel/preset-env"], + "plugins": [ + "@babel/plugin-transform-runtime", + "@babel/plugin-syntax-dynamic-import", + "@babel/plugin-transform-react-jsx", + "@babel/plugin-syntax-function-bind", + "@babel/plugin-proposal-function-bind", + ["@babel/plugin-proposal-class-properties", {loose: true}], + ] +} diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..d1a0d79 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,16 @@ +[run] +source = ./src/newsreader/ +omit = + **/tests/** + **/migrations/** + **/conf/** + **/apps.py + **/admin.py + **/tests.py + **/urls.py + **/wsgi.py + **/celery.py + **/__init__.py + +[html] +directory = coverage diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..754490c --- /dev/null +++ b/.gitignore @@ -0,0 +1,207 @@ + +# Created by https://www.gitignore.io/api/django,python +# Edit at https://www.gitignore.io/?templates=django,python + +### Django ### +*.log +*.pot +*.pyc +__pycache__/ +local_settings.py +local.py +db.sqlite3 +media + +# If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/ +# in your Git repository. Update and uncomment the following line accordingly. +# /staticfiles/ + +### Django.Python Stack ### +# Byte-compiled / optimized / DLL files +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ + +lib/ +!src/newsreader/scss/lib + +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +coverage/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo + +# Django stuff: + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don’t work, or not +# install all needed dependencies. +#Pipfile.lock + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +### Python ### +# Byte-compiled / optimized / DLL files + +# C extensions + +# Distribution / packaging + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. + +# Installer logs + +# Unit test / coverage reports + +# Translations + +# Django stuff: +src/newsreader/fixtures/local + +# Flask stuff: + +# Scrapy stuff: + +# Sphinx documentation + +# PyBuilder + +# Jupyter Notebook + +# IPython + +# pyenv + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don’t work, or not +# install all needed dependencies. + +# celery beat schedule file + +# SageMath parsed files + +# Environments + +# Spyder project settings + +# Rope project settings + +# mkdocs documentation + +# mypy + +# Pyre type checker + +# End of https://www.gitignore.io/api/django,python + +# Javascript +node_modules/ + +static/ + +# Css +*.css diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..fd895d6 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,28 @@ +stages: + - build + - test + - lint + - deploy + +variables: + PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" + DJANGO_SETTINGS_MODULE: "newsreader.conf.gitlab" + POSTGRES_HOST: "$POSTGRES_HOST" + POSTGRES_DB: "$POSTGRES_NAME" + POSTGRES_NAME: "$POSTGRES_NAME" + POSTGRES_USER: "$POSTGRES_USER" + POSTGRES_PASSWORD: "$POSTGRES_PASSWORD" + +cache: + key: "$CI_COMMIT_REF_SLUG" + paths: + - .venv/ + - .cache/pip + - .cache/poetry + - node_modules/ + +include: + - local: '/gitlab-ci/build.yml' + - local: '/gitlab-ci/test.yml' + - local: '/gitlab-ci/lint.yml' + - local: '/gitlab-ci/deploy.yml' diff --git a/.isort.cfg b/.isort.cfg new file mode 100644 index 0000000..0c8e37f --- /dev/null +++ b/.isort.cfg @@ -0,0 +1,12 @@ +[settings] +include_trailing_comma = true +line_length = 88 +multi_line_output = 3 +skip = env/, venv/ +default_section = THIRDPARTY +known_first_party = newsreader +known_django = django +sections = FUTURE,STDLIB,DJANGO,THIRDPARTY,FIRSTPARTY,LOCALFOLDER +lines_between_types=1 +lines_after_imports=2 +lines_between_types=1 diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..146a217 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,10 @@ +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": true, + "printWidth": 90, + "tabWidth": 2, + "useTabs": false, + "bracketSpacing": true, + "arrowParens": "avoid" +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..61ef10b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM python:3.7-buster + +RUN pip install poetry + +WORKDIR /app +COPY poetry.lock pyproject.toml /app/ + +RUN poetry config virtualenvs.create false +RUN poetry install --no-interaction + +COPY . /app/ diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..7a39a3f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,52 @@ +version: '3' + +services: + db: + # See https://hub.docker.com/_/postgres + image: postgres + container_name: postgres + environment: + - POSTGRES_DB=$POSTGRES_NAME + - POSTGRES_USER=$POSTGRES_USER + - POSTGRES_PASSWORD=$POSTGRES_PASSWORD + rabbitmq: + image: rabbitmq:3.7 + container_name: rabbitmq + celery: + build: . + container_name: celery + command: poetry run celery -A newsreader worker -l INFO --beat --scheduler django --workdir=/app/src/ + environment: + - POSTGRES_HOST=$POSTGRES_HOST + - POSTGRES_NAME=$POSTGRES_NAME + - POSTGRES_USER=$POSTGRES_USER + - POSTGRES_PASSWORD=$POSTGRES_PASSWORD + - DJANGO_SETTINGS_MODULE=newsreader.conf.docker + volumes: + - .:/app + depends_on: + - rabbitmq + memcached: + image: memcached:1.5.22 + container_name: memcached + ports: + - "11211:11211" + entrypoint: + - memcached + - -m 64 + web: + build: . + container_name: web + command: src/entrypoint.sh + environment: + - POSTGRES_HOST=$POSTGRES_HOST + - POSTGRES_NAME=$POSTGRES_NAME + - POSTGRES_USER=$POSTGRES_USER + - POSTGRES_PASSWORD=$POSTGRES_PASSWORD + - DJANGO_SETTINGS_MODULE=newsreader.conf.docker + volumes: + - .:/app + ports: + - '8000:8000' + depends_on: + - db diff --git a/gitlab-ci/build.yml b/gitlab-ci/build.yml new file mode 100644 index 0000000..c8df615 --- /dev/null +++ b/gitlab-ci/build.yml @@ -0,0 +1,7 @@ +static: + stage: build + image: node:12 + before_script: + - npm install + script: + - npm run build diff --git a/gitlab-ci/deploy.yml b/gitlab-ci/deploy.yml new file mode 100644 index 0000000..fedc5eb --- /dev/null +++ b/gitlab-ci/deploy.yml @@ -0,0 +1,16 @@ +deploy: + stage: deploy + image: debian:buster + environment: + name: production + url: rss.fudiggity.nl + before_script: + - apt-get update && apt-get install -y ansible git + - git clone https://gitlab-ci-token:${CI_JOB_TOKEN}@git.fudiggity.nl/sonny/ansible-playbooks.git deployment + - mkdir /root/.ssh + - echo "192.168.178.63 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILbtcdgJBhVCKsO88cV19EYefDTopdYejEQCp1pYr1Ga" > /root/.ssh/known_hosts + - echo "$DEPLOY_KEY" > deployment/deploy_key && chmod 0600 deployment/deploy_key + script: + - ansible-playbook deployment/playbook.yml --inventory deployment/apps.yml --limit newsreader --user ansible --private-key deployment/deploy_key + only: + - master diff --git a/gitlab-ci/lint.yml b/gitlab-ci/lint.yml new file mode 100644 index 0000000..3f1e259 --- /dev/null +++ b/gitlab-ci/lint.yml @@ -0,0 +1,22 @@ +python-linting: + stage: lint + allow_failure: true + image: python:3.7.4-slim-stretch + before_script: + - pip install poetry + - poetry config cache-dir ~/.cache/poetry + - poetry config virtualenvs.in-project true + - poetry install --no-interaction + script: + - poetry run isort src/ --check-only --recursive + - poetry run black src/ --line-length 88 --check + - poetry run autoflake src/ --check --recursive --remove-all-unused-imports --ignore-init-module-imports + +javascript-linting: + stage: lint + allow_failure: true + image: node:12 + before_script: + - npm install + script: + - npm run lint diff --git a/gitlab-ci/test.yml b/gitlab-ci/test.yml new file mode 100644 index 0000000..3e8eccb --- /dev/null +++ b/gitlab-ci/test.yml @@ -0,0 +1,23 @@ +python-tests: + stage: test + coverage: '/TOTAL\s+\d+\s+\d+\s+(\d+%)/' + services: + - postgres:11 + - memcached:1.5.22 + image: python:3.7.4-slim-stretch + before_script: + - pip install poetry + - poetry config cache-dir .cache/poetry + - poetry config virtualenvs.in-project true + - poetry install --no-interaction + script: + - poetry run coverage run src/manage.py test newsreader + - poetry run coverage report + +javascript-tests: + stage: test + image: node:12 + before_script: + - npm install + script: + - npm test diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..01afeb8 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,188 @@ +// For a detailed explanation regarding each configuration property, visit: +// https://jestjs.io/docs/en/configuration.html + +module.exports = { + // All imported modules in your tests should be mocked automatically + // automock: false, + + // Stop running tests after `n` failures + // bail: 0, + + // Respect "browser" field in package.json when resolving modules + // browser: false, + + // The directory where Jest should store its cached dependency information + // cacheDirectory: "/tmp/jest_rs", + + // Automatically clear mock calls and instances between every test + clearMocks: true, + + // Indicates whether the coverage information should be collected while executing the test + // collectCoverage: false, + + // An array of glob patterns indicating a set of files for which coverage information should be collected + // collectCoverageFrom: null, + + // The directory where Jest should output its coverage files + coverageDirectory: 'coverage', + + // An array of regexp pattern strings used to skip coverage collection + // coveragePathIgnorePatterns: [ + // "/node_modules/" + // ], + + // A list of reporter names that Jest uses when writing coverage reports + // coverageReporters: [ + // "json", + // "text", + // "lcov", + // "clover" + // ], + + // An object that configures minimum threshold enforcement for coverage results + // coverageThreshold: null, + + // A path to a custom dependency extractor + // dependencyExtractor: null, + + // Make calling deprecated APIs throw helpful error messages + // errorOnDeprecated: false, + + // Force coverage collection from ignored files using an array of glob patterns + // forceCoverageMatch: [], + + // A path to a module which exports an async function that is triggered once before all test suites + // globalSetup: null, + + // A path to a module which exports an async function that is triggered once after all test suites + // globalTeardown: null, + + // A set of global variables that need to be available in all test environments + // globals: {}, + + // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. + // maxWorkers: "50%", + + // An array of directory names to be searched recursively up from the requiring module's location + // moduleDirectories: [ + // "node_modules" + // ], + + // An array of file extensions your modules use + // moduleFileExtensions: [ + // "js", + // "json", + // "jsx", + // "ts", + // "tsx", + // "node" + // ], + + // A map from regular expressions to module names that allow to stub out resources with a single module + // moduleNameMapper: {}, + + // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader + // modulePathIgnorePatterns: [], + + // Activates notifications for test results + // notify: false, + + // An enum that specifies notification mode. Requires { notify: true } + // notifyMode: "failure-change", + + // A preset that is used as a base for Jest's configuration + // preset: null, + + // Run tests from one or more projects + // projects: null, + + // Use this configuration option to add custom reporters to Jest + // reporters: undefined, + + // Automatically reset mock state between every test + // resetMocks: false, + + // Reset the module registry before running each individual test + // resetModules: false, + + // A path to a custom resolver + // resolver: null, + + // Automatically restore mock state between every test + // restoreMocks: false, + + // The root directory that Jest should scan for tests and modules within + rootDir: 'src/newsreader/js/tests/', + + // A list of paths to directories that Jest should use to search for files in + // roots: [ + // "" + // ], + + // Allows you to use a custom runner instead of Jest's default test runner + // runner: "jest-runner", + + // The paths to modules that run some code to configure or set up the testing environment before each test + // setupFiles: [], + + // A list of paths to modules that run some code to configure or set up the testing framework before each test + // setupFilesAfterEnv: [], + + // A list of paths to snapshot serializer modules Jest should use for snapshot testing + // snapshotSerializers: [], + + // The test environment that will be used for testing + testEnvironment: 'node', + + // Options that will be passed to the testEnvironment + // testEnvironmentOptions: {}, + + // Adds a location field to test results + // testLocationInResults: false, + + // The glob patterns Jest uses to detect test files + // testMatch: [ + // "**/__tests__/**/*.[jt]s?(x)", + // "**/?(*.)+(spec|test).[tj]s?(x)" + // ], + + // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped + // testPathIgnorePatterns: [ + // "/node_modules/" + // ], + + // The regexp pattern or array of patterns that Jest uses to detect test files + // testRegex: [], + + // This option allows the use of a custom results processor + // testResultsProcessor: null, + + // This option allows use of a custom test runner + // testRunner: "jasmine2", + + // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href + // testURL: "http://localhost", + + // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" + // timers: "real", + + // A map from regular expressions to paths to transformers + // transform: null, + + // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation + // transformIgnorePatterns: [ + // "/node_modules/" + // ], + + // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them + // unmockedModulePathPatterns: undefined, + + // Indicates whether each individual test should be reported during the run + // verbose: null, + + // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode + // watchPathIgnorePatterns: [], + + // Whether to use watchman for file crawling + // watchman: true, +}; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..50f72a4 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,9443 @@ +{ + "name": "newsreader", + "version": "0.1.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@babel/code-frame": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz", + "integrity": "sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==", + "dev": true, + "requires": { + "@babel/highlight": "^7.0.0" + } + }, + "@babel/core": { + "version": "7.7.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.7.7.tgz", + "integrity": "sha512-jlSjuj/7z138NLZALxVgrx13AOtqip42ATZP7+kYl53GvDV6+4dCek1mVUo8z8c8Xnw/mx2q3d9HWh3griuesQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.5.5", + "@babel/generator": "^7.7.7", + "@babel/helpers": "^7.7.4", + "@babel/parser": "^7.7.7", + "@babel/template": "^7.7.4", + "@babel/traverse": "^7.7.4", + "@babel/types": "^7.7.4", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "json5": "^2.1.0", + "lodash": "^4.17.13", + "resolve": "^1.3.2", + "semver": "^5.4.1", + "source-map": "^0.5.0" + }, + "dependencies": { + "convert-source-map": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", + "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + } + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@babel/generator": { + "version": "7.7.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.7.7.tgz", + "integrity": "sha512-/AOIBpHh/JU1l0ZFS4kiRCBnLi6OTHzh0RPk3h9isBxkkqELtQNFi1Vr/tiG9p1yfoUdKVwISuXWQR+hwwM4VQ==", + "dev": true, + "requires": { + "@babel/types": "^7.7.4", + "jsesc": "^2.5.1", + "lodash": "^4.17.13", + "source-map": "^0.5.0" + } + }, + "@babel/helper-annotate-as-pure": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.7.4.tgz", + "integrity": "sha512-2BQmQgECKzYKFPpiycoF9tlb5HA4lrVyAmLLVK177EcQAqjVLciUb2/R+n1boQ9y5ENV3uz2ZqiNw7QMBBw1Og==", + "dev": true, + "requires": { + "@babel/types": "^7.7.4" + } + }, + "@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.7.4.tgz", + "integrity": "sha512-Biq/d/WtvfftWZ9Uf39hbPBYDUo986m5Bb4zhkeYDGUllF43D+nUe5M6Vuo6/8JDK/0YX/uBdeoQpyaNhNugZQ==", + "dev": true, + "requires": { + "@babel/helper-explode-assignable-expression": "^7.7.4", + "@babel/types": "^7.7.4" + } + }, + "@babel/helper-builder-react-jsx": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.7.4.tgz", + "integrity": "sha512-kvbfHJNN9dg4rkEM4xn1s8d1/h6TYNvajy9L1wx4qLn9HFg0IkTsQi4rfBe92nxrPUFcMsHoMV+8rU7MJb3fCA==", + "dev": true, + "requires": { + "@babel/types": "^7.7.4", + "esutils": "^2.0.0" + } + }, + "@babel/helper-call-delegate": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helper-call-delegate/-/helper-call-delegate-7.7.4.tgz", + "integrity": "sha512-8JH9/B7J7tCYJ2PpWVpw9JhPuEVHztagNVuQAFBVFYluRMlpG7F1CgKEgGeL6KFqcsIa92ZYVj6DSc0XwmN1ZA==", + "dev": true, + "requires": { + "@babel/helper-hoist-variables": "^7.7.4", + "@babel/traverse": "^7.7.4", + "@babel/types": "^7.7.4" + } + }, + "@babel/helper-create-class-features-plugin": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.7.4.tgz", + "integrity": "sha512-l+OnKACG4uiDHQ/aJT8dwpR+LhCJALxL0mJ6nzjB25e5IPwqV1VOsY7ah6UB1DG+VOXAIMtuC54rFJGiHkxjgA==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.7.4", + "@babel/helper-member-expression-to-functions": "^7.7.4", + "@babel/helper-optimise-call-expression": "^7.7.4", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-replace-supers": "^7.7.4", + "@babel/helper-split-export-declaration": "^7.7.4" + } + }, + "@babel/helper-create-regexp-features-plugin": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.7.4.tgz", + "integrity": "sha512-Mt+jBKaxL0zfOIWrfQpnfYCN7/rS6GKx6CCCfuoqVVd+17R8zNDlzVYmIi9qyb2wOk002NsmSTDymkIygDUH7A==", + "dev": true, + "requires": { + "@babel/helper-regex": "^7.4.4", + "regexpu-core": "^4.6.0" + } + }, + "@babel/helper-define-map": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.7.4.tgz", + "integrity": "sha512-v5LorqOa0nVQUvAUTUF3KPastvUt/HzByXNamKQ6RdJRTV7j8rLL+WB5C/MzzWAwOomxDhYFb1wLLxHqox86lg==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.7.4", + "@babel/types": "^7.7.4", + "lodash": "^4.17.13" + } + }, + "@babel/helper-explode-assignable-expression": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.7.4.tgz", + "integrity": "sha512-2/SicuFrNSXsZNBxe5UGdLr+HZg+raWBLE9vC98bdYOKX/U6PY0mdGlYUJdtTDPSU0Lw0PNbKKDpwYHJLn2jLg==", + "dev": true, + "requires": { + "@babel/traverse": "^7.7.4", + "@babel/types": "^7.7.4" + } + }, + "@babel/helper-function-name": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.7.4.tgz", + "integrity": "sha512-AnkGIdiBhEuiwdoMnKm7jfPfqItZhgRaZfMg1XX3bS25INOnLPjPG1Ppnajh8eqgt5kPJnfqrRHqFqmjKDZLzQ==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.7.4", + "@babel/template": "^7.7.4", + "@babel/types": "^7.7.4" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.7.4.tgz", + "integrity": "sha512-QTGKEdCkjgzgfJ3bAyRwF4yyT3pg+vDgan8DSivq1eS0gwi+KGKE5x8kRcbeFTb/673mkO5SN1IZfmCfA5o+EA==", + "dev": true, + "requires": { + "@babel/types": "^7.7.4" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.7.4.tgz", + "integrity": "sha512-wQC4xyvc1Jo/FnLirL6CEgPgPCa8M74tOdjWpRhQYapz5JC7u3NYU1zCVoVAGCE3EaIP9T1A3iW0WLJ+reZlpQ==", + "dev": true, + "requires": { + "@babel/types": "^7.7.4" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.7.4.tgz", + "integrity": "sha512-9KcA1X2E3OjXl/ykfMMInBK+uVdfIVakVe7W7Lg3wfXUNyS3Q1HWLFRwZIjhqiCGbslummPDnmb7vIekS0C1vw==", + "dev": true, + "requires": { + "@babel/types": "^7.7.4" + } + }, + "@babel/helper-module-imports": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.7.4.tgz", + "integrity": "sha512-dGcrX6K9l8258WFjyDLJwuVKxR4XZfU0/vTUgOQYWEnRD8mgr+p4d6fCUMq/ys0h4CCt/S5JhbvtyErjWouAUQ==", + "dev": true, + "requires": { + "@babel/types": "^7.7.4" + } + }, + "@babel/helper-module-transforms": { + "version": "7.7.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.7.5.tgz", + "integrity": "sha512-A7pSxyJf1gN5qXVcidwLWydjftUN878VkalhXX5iQDuGyiGK3sOrrKKHF4/A4fwHtnsotv/NipwAeLzY4KQPvw==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.7.4", + "@babel/helper-simple-access": "^7.7.4", + "@babel/helper-split-export-declaration": "^7.7.4", + "@babel/template": "^7.7.4", + "@babel/types": "^7.7.4", + "lodash": "^4.17.13" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.7.4.tgz", + "integrity": "sha512-VB7gWZ2fDkSuqW6b1AKXkJWO5NyNI3bFL/kK79/30moK57blr6NbH8xcl2XcKCwOmJosftWunZqfO84IGq3ZZg==", + "dev": true, + "requires": { + "@babel/types": "^7.7.4" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0.tgz", + "integrity": "sha512-CYAOUCARwExnEixLdB6sDm2dIJ/YgEAKDM1MOeMeZu9Ld/bDgVo8aiWrXwcY7OBh+1Ea2uUcVRcxKk0GJvW7QA==", + "dev": true + }, + "@babel/helper-regex": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/helper-regex/-/helper-regex-7.5.5.tgz", + "integrity": "sha512-CkCYQLkfkiugbRDO8eZn6lRuR8kzZoGXCg3149iTk5se7g6qykSpy3+hELSwquhu+TgHn8nkLiBwHvNX8Hofcw==", + "dev": true, + "requires": { + "lodash": "^4.17.13" + } + }, + "@babel/helper-remap-async-to-generator": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.7.4.tgz", + "integrity": "sha512-Sk4xmtVdM9sA/jCI80f+KS+Md+ZHIpjuqmYPk1M7F/upHou5e4ReYmExAiu6PVe65BhJPZA2CY9x9k4BqE5klw==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.7.4", + "@babel/helper-wrap-function": "^7.7.4", + "@babel/template": "^7.7.4", + "@babel/traverse": "^7.7.4", + "@babel/types": "^7.7.4" + } + }, + "@babel/helper-replace-supers": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.7.4.tgz", + "integrity": "sha512-pP0tfgg9hsZWo5ZboYGuBn/bbYT/hdLPVSS4NMmiRJdwWhP0IznPwN9AE1JwyGsjSPLC364I0Qh5p+EPkGPNpg==", + "dev": true, + "requires": { + "@babel/helper-member-expression-to-functions": "^7.7.4", + "@babel/helper-optimise-call-expression": "^7.7.4", + "@babel/traverse": "^7.7.4", + "@babel/types": "^7.7.4" + } + }, + "@babel/helper-simple-access": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.7.4.tgz", + "integrity": "sha512-zK7THeEXfan7UlWsG2A6CI/L9jVnI5+xxKZOdej39Y0YtDYKx9raHk5F2EtK9K8DHRTihYwg20ADt9S36GR78A==", + "dev": true, + "requires": { + "@babel/template": "^7.7.4", + "@babel/types": "^7.7.4" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.7.4.tgz", + "integrity": "sha512-guAg1SXFcVr04Guk9eq0S4/rWS++sbmyqosJzVs8+1fH5NI+ZcmkaSkc7dmtAFbHFva6yRJnjW3yAcGxjueDug==", + "dev": true, + "requires": { + "@babel/types": "^7.7.4" + } + }, + "@babel/helper-wrap-function": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.7.4.tgz", + "integrity": "sha512-VsfzZt6wmsocOaVU0OokwrIytHND55yvyT4BPB9AIIgwr8+x7617hetdJTsuGwygN5RC6mxA9EJztTjuwm2ofg==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.7.4", + "@babel/template": "^7.7.4", + "@babel/traverse": "^7.7.4", + "@babel/types": "^7.7.4" + } + }, + "@babel/helpers": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.7.4.tgz", + "integrity": "sha512-ak5NGZGJ6LV85Q1Zc9gn2n+ayXOizryhjSUBTdu5ih1tlVCJeuQENzc4ItyCVhINVXvIT/ZQ4mheGIsfBkpskg==", + "dev": true, + "requires": { + "@babel/template": "^7.7.4", + "@babel/traverse": "^7.7.4", + "@babel/types": "^7.7.4" + } + }, + "@babel/highlight": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.5.0.tgz", + "integrity": "sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.7.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.7.7.tgz", + "integrity": "sha512-WtTZMZAZLbeymhkd/sEaPD8IQyGAhmuTuvTzLiCFM7iXiVdY0gc0IaI+cW0fh1BnSMbJSzXX6/fHllgHKwHhXw==", + "dev": true + }, + "@babel/plugin-proposal-async-generator-functions": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.7.4.tgz", + "integrity": "sha512-1ypyZvGRXriY/QP668+s8sFr2mqinhkRDMPSQLNghCQE+GAkFtp+wkHVvg2+Hdki8gwP+NFzJBJ/N1BfzCCDEw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-remap-async-to-generator": "^7.7.4", + "@babel/plugin-syntax-async-generators": "^7.7.4" + } + }, + "@babel/plugin-proposal-class-properties": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.7.4.tgz", + "integrity": "sha512-EcuXeV4Hv1X3+Q1TsuOmyyxeTRiSqurGJ26+I/FW1WbymmRRapVORm6x1Zl3iDIHyRxEs+VXWp6qnlcfcJSbbw==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.7.4", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-proposal-dynamic-import": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.7.4.tgz", + "integrity": "sha512-StH+nGAdO6qDB1l8sZ5UBV8AC3F2VW2I8Vfld73TMKyptMU9DY5YsJAS8U81+vEtxcH3Y/La0wG0btDrhpnhjQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-dynamic-import": "^7.7.4" + } + }, + "@babel/plugin-proposal-function-bind": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-function-bind/-/plugin-proposal-function-bind-7.7.4.tgz", + "integrity": "sha512-0qJlxfYKHs/JUg+JFISl29YObUCKAOQ0ENHMYoxErBFp58XTXwQEsrVPhs2TGL3cxI21XPs2fpommO6zmCd3/A==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-function-bind": "^7.7.4" + } + }, + "@babel/plugin-proposal-json-strings": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.7.4.tgz", + "integrity": "sha512-wQvt3akcBTfLU/wYoqm/ws7YOAQKu8EVJEvHip/mzkNtjaclQoCCIqKXFP5/eyfnfbQCDV3OLRIK3mIVyXuZlw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-json-strings": "^7.7.4" + } + }, + "@babel/plugin-proposal-object-rest-spread": { + "version": "7.7.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.7.7.tgz", + "integrity": "sha512-3qp9I8lelgzNedI3hrhkvhaEYree6+WHnyA/q4Dza9z7iEIs1eyhWyJnetk3jJ69RT0AT4G0UhEGwyGFJ7GUuQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-object-rest-spread": "^7.7.4" + } + }, + "@babel/plugin-proposal-optional-catch-binding": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.7.4.tgz", + "integrity": "sha512-DyM7U2bnsQerCQ+sejcTNZh8KQEUuC3ufzdnVnSiUv/qoGJp2Z3hanKL18KDhsBT5Wj6a7CMT5mdyCNJsEaA9w==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-optional-catch-binding": "^7.7.4" + } + }, + "@babel/plugin-proposal-unicode-property-regex": { + "version": "7.7.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.7.7.tgz", + "integrity": "sha512-80PbkKyORBUVm1fbTLrHpYdJxMThzM1UqFGh0ALEhO9TYbG86Ah9zQYAB/84axz2vcxefDLdZwWwZNlYARlu9w==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.7.4", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-async-generators": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.7.4.tgz", + "integrity": "sha512-Li4+EjSpBgxcsmeEF8IFcfV/+yJGxHXDirDkEoyFjumuwbmfCVHUt0HuowD/iGM7OhIRyXJH9YXxqiH6N815+g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-dynamic-import": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.7.4.tgz", + "integrity": "sha512-jHQW0vbRGvwQNgyVxwDh4yuXu4bH1f5/EICJLAhl1SblLs2CDhrsmCk+v5XLdE9wxtAFRyxx+P//Iw+a5L/tTg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-function-bind": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-function-bind/-/plugin-syntax-function-bind-7.7.4.tgz", + "integrity": "sha512-dF3QkkaFA3Z7eiD2Cv7Y5x4w2sAKQVHUV2hLqi9iPKexw+/oqpL4crnnalg/Lq31XN33cH3G41kONSCqu06i/Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.7.4.tgz", + "integrity": "sha512-QpGupahTQW1mHRXddMG5srgpHWqRLwJnJZKXTigB9RPFCCGbDGCgBeM/iC82ICXp414WeYx/tD54w7M2qRqTMg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-jsx": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.7.4.tgz", + "integrity": "sha512-wuy6fiMe9y7HeZBWXYCGt2RGxZOj0BImZ9EyXJVnVGBKO/Br592rbR3rtIQn0eQhAk9vqaKP5n8tVqEFBQMfLg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.7.4.tgz", + "integrity": "sha512-mObR+r+KZq0XhRVS2BrBKBpr5jqrqzlPvS9C9vuOf5ilSwzloAl7RPWLrgKdWS6IreaVrjHxTjtyqFiOisaCwg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.7.4.tgz", + "integrity": "sha512-4ZSuzWgFxqHRE31Glu+fEr/MirNZOMYmD/0BhBWyLyOOQz/gTAl7QmWm2hX1QxEIXsr2vkdlwxIzTyiYRC4xcQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-top-level-await": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.7.4.tgz", + "integrity": "sha512-wdsOw0MvkL1UIgiQ/IFr3ETcfv1xb8RMM0H9wbiDyLaJFyiDg5oZvDLCXosIXmFeIlweML5iOBXAkqddkYNizg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-arrow-functions": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.7.4.tgz", + "integrity": "sha512-zUXy3e8jBNPiffmqkHRNDdZM2r8DWhCB7HhcoyZjiK1TxYEluLHAvQuYnTT+ARqRpabWqy/NHkO6e3MsYB5YfA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-async-to-generator": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.7.4.tgz", + "integrity": "sha512-zpUTZphp5nHokuy8yLlyafxCJ0rSlFoSHypTUWgpdwoDXWQcseaect7cJ8Ppk6nunOM6+5rPMkod4OYKPR5MUg==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.7.4", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-remap-async-to-generator": "^7.7.4" + } + }, + "@babel/plugin-transform-block-scoped-functions": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.7.4.tgz", + "integrity": "sha512-kqtQzwtKcpPclHYjLK//3lH8OFsCDuDJBaFhVwf8kqdnF6MN4l618UDlcA7TfRs3FayrHj+svYnSX8MC9zmUyQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-block-scoping": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.7.4.tgz", + "integrity": "sha512-2VBe9u0G+fDt9B5OV5DQH4KBf5DoiNkwFKOz0TCvBWvdAN2rOykCTkrL+jTLxfCAm76l9Qo5OqL7HBOx2dWggg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "lodash": "^4.17.13" + } + }, + "@babel/plugin-transform-classes": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.7.4.tgz", + "integrity": "sha512-sK1mjWat7K+buWRuImEzjNf68qrKcrddtpQo3swi9j7dUcG6y6R6+Di039QN2bD1dykeswlagupEmpOatFHHUg==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.7.4", + "@babel/helper-define-map": "^7.7.4", + "@babel/helper-function-name": "^7.7.4", + "@babel/helper-optimise-call-expression": "^7.7.4", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-replace-supers": "^7.7.4", + "@babel/helper-split-export-declaration": "^7.7.4", + "globals": "^11.1.0" + } + }, + "@babel/plugin-transform-computed-properties": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.7.4.tgz", + "integrity": "sha512-bSNsOsZnlpLLyQew35rl4Fma3yKWqK3ImWMSC/Nc+6nGjC9s5NFWAer1YQ899/6s9HxO2zQC1WoFNfkOqRkqRQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-destructuring": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.7.4.tgz", + "integrity": "sha512-4jFMXI1Cu2aXbcXXl8Lr6YubCn6Oc7k9lLsu8v61TZh+1jny2BWmdtvY9zSUlLdGUvcy9DMAWyZEOqjsbeg/wA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-dotall-regex": { + "version": "7.7.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.7.7.tgz", + "integrity": "sha512-b4in+YlTeE/QmTgrllnb3bHA0HntYvjz8O3Mcbx75UBPJA2xhb5A8nle498VhxSXJHQefjtQxpnLPehDJ4TRlg==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.7.4", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-duplicate-keys": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.7.4.tgz", + "integrity": "sha512-g1y4/G6xGWMD85Tlft5XedGaZBCIVN+/P0bs6eabmcPP9egFleMAo65OOjlhcz1njpwagyY3t0nsQC9oTFegJA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-exponentiation-operator": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.7.4.tgz", + "integrity": "sha512-MCqiLfCKm6KEA1dglf6Uqq1ElDIZwFuzz1WH5mTf8k2uQSxEJMbOIEh7IZv7uichr7PMfi5YVSrr1vz+ipp7AQ==", + "dev": true, + "requires": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.7.4", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-for-of": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.7.4.tgz", + "integrity": "sha512-zZ1fD1B8keYtEcKF+M1TROfeHTKnijcVQm0yO/Yu1f7qoDoxEIc/+GX6Go430Bg84eM/xwPFp0+h4EbZg7epAA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-function-name": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.7.4.tgz", + "integrity": "sha512-E/x09TvjHNhsULs2IusN+aJNRV5zKwxu1cpirZyRPw+FyyIKEHPXTsadj48bVpc1R5Qq1B5ZkzumuFLytnbT6g==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.7.4", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-literals": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.7.4.tgz", + "integrity": "sha512-X2MSV7LfJFm4aZfxd0yLVFrEXAgPqYoDG53Br/tCKiKYfX0MjVjQeWPIhPHHsCqzwQANq+FLN786fF5rgLS+gw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-member-expression-literals": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.7.4.tgz", + "integrity": "sha512-9VMwMO7i69LHTesL0RdGy93JU6a+qOPuvB4F4d0kR0zyVjJRVJRaoaGjhtki6SzQUu8yen/vxPKN6CWnCUw6bA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-modules-amd": { + "version": "7.7.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.7.5.tgz", + "integrity": "sha512-CT57FG4A2ZUNU1v+HdvDSDrjNWBrtCmSH6YbbgN3Lrf0Di/q/lWRxZrE72p3+HCCz9UjfZOEBdphgC0nzOS6DQ==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.7.5", + "@babel/helper-plugin-utils": "^7.0.0", + "babel-plugin-dynamic-import-node": "^2.3.0" + } + }, + "@babel/plugin-transform-modules-commonjs": { + "version": "7.7.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.7.5.tgz", + "integrity": "sha512-9Cq4zTFExwFhQI6MT1aFxgqhIsMWQWDVwOgLzl7PTWJHsNaqFvklAU+Oz6AQLAS0dJKTwZSOCo20INwktxpi3Q==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.7.5", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-simple-access": "^7.7.4", + "babel-plugin-dynamic-import-node": "^2.3.0" + } + }, + "@babel/plugin-transform-modules-systemjs": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.7.4.tgz", + "integrity": "sha512-y2c96hmcsUi6LrMqvmNDPBBiGCiQu0aYqpHatVVu6kD4mFEXKjyNxd/drc18XXAf9dv7UXjrZwBVmTTGaGP8iw==", + "dev": true, + "requires": { + "@babel/helper-hoist-variables": "^7.7.4", + "@babel/helper-plugin-utils": "^7.0.0", + "babel-plugin-dynamic-import-node": "^2.3.0" + } + }, + "@babel/plugin-transform-modules-umd": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.7.4.tgz", + "integrity": "sha512-u2B8TIi0qZI4j8q4C51ktfO7E3cQ0qnaXFI1/OXITordD40tt17g/sXqgNNCcMTcBFKrUPcGDx+TBJuZxLx7tw==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.7.4", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.7.4.tgz", + "integrity": "sha512-jBUkiqLKvUWpv9GLSuHUFYdmHg0ujC1JEYoZUfeOOfNydZXp1sXObgyPatpcwjWgsdBGsagWW0cdJpX/DO2jMw==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.7.4" + } + }, + "@babel/plugin-transform-new-target": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.7.4.tgz", + "integrity": "sha512-CnPRiNtOG1vRodnsyGX37bHQleHE14B9dnnlgSeEs3ek3fHN1A1SScglTCg1sfbe7sRQ2BUcpgpTpWSfMKz3gg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-object-super": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.7.4.tgz", + "integrity": "sha512-ho+dAEhC2aRnff2JCA0SAK7V2R62zJd/7dmtoe7MHcso4C2mS+vZjn1Pb1pCVZvJs1mgsvv5+7sT+m3Bysb6eg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-replace-supers": "^7.7.4" + } + }, + "@babel/plugin-transform-parameters": { + "version": "7.7.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.7.7.tgz", + "integrity": "sha512-OhGSrf9ZBrr1fw84oFXj5hgi8Nmg+E2w5L7NhnG0lPvpDtqd7dbyilM2/vR8CKbJ907RyxPh2kj6sBCSSfI9Ew==", + "dev": true, + "requires": { + "@babel/helper-call-delegate": "^7.7.4", + "@babel/helper-get-function-arity": "^7.7.4", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-property-literals": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.7.4.tgz", + "integrity": "sha512-MatJhlC4iHsIskWYyawl53KuHrt+kALSADLQQ/HkhTjX954fkxIEh4q5slL4oRAnsm/eDoZ4q0CIZpcqBuxhJQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-react-jsx": { + "version": "7.7.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.7.7.tgz", + "integrity": "sha512-SlPjWPbva2+7/ZJbGcoqjl4LsQaLpKEzxW9hcxU7675s24JmdotJOSJ4cgAbV82W3FcZpHIGmRZIlUL8ayMvjw==", + "dev": true, + "requires": { + "@babel/helper-builder-react-jsx": "^7.7.4", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-jsx": "^7.7.4" + } + }, + "@babel/plugin-transform-regenerator": { + "version": "7.7.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.7.5.tgz", + "integrity": "sha512-/8I8tPvX2FkuEyWbjRCt4qTAgZK0DVy8QRguhA524UH48RfGJy94On2ri+dCuwOpcerPRl9O4ebQkRcVzIaGBw==", + "dev": true, + "requires": { + "regenerator-transform": "^0.14.0" + } + }, + "@babel/plugin-transform-reserved-words": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.7.4.tgz", + "integrity": "sha512-OrPiUB5s5XvkCO1lS7D8ZtHcswIC57j62acAnJZKqGGnHP+TIc/ljQSrgdX/QyOTdEK5COAhuc820Hi1q2UgLQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-runtime": { + "version": "7.7.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.7.6.tgz", + "integrity": "sha512-tajQY+YmXR7JjTwRvwL4HePqoL3DYxpYXIHKVvrOIvJmeHe2y1w4tz5qz9ObUDC9m76rCzIMPyn4eERuwA4a4A==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.7.4", + "@babel/helper-plugin-utils": "^7.0.0", + "resolve": "^1.8.1", + "semver": "^5.5.1" + } + }, + "@babel/plugin-transform-shorthand-properties": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.7.4.tgz", + "integrity": "sha512-q+suddWRfIcnyG5YiDP58sT65AJDZSUhXQDZE3r04AuqD6d/XLaQPPXSBzP2zGerkgBivqtQm9XKGLuHqBID6Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-spread": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.7.4.tgz", + "integrity": "sha512-8OSs0FLe5/80cndziPlg4R0K6HcWSM0zyNhHhLsmw/Nc5MaA49cAsnoJ/t/YZf8qkG7fD+UjTRaApVDB526d7Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-sticky-regex": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.7.4.tgz", + "integrity": "sha512-Ls2NASyL6qtVe1H1hXts9yuEeONV2TJZmplLONkMPUG158CtmnrzW5Q5teibM5UVOFjG0D3IC5mzXR6pPpUY7A==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-regex": "^7.0.0" + } + }, + "@babel/plugin-transform-template-literals": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.7.4.tgz", + "integrity": "sha512-sA+KxLwF3QwGj5abMHkHgshp9+rRz+oY9uoRil4CyLtgEuE/88dpkeWgNk5qKVsJE9iSfly3nvHapdRiIS2wnQ==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.7.4", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-typeof-symbol": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.7.4.tgz", + "integrity": "sha512-KQPUQ/7mqe2m0B8VecdyaW5XcQYaePyl9R7IsKd+irzj6jvbhoGnRE+M0aNkyAzI07VfUQ9266L5xMARitV3wg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-unicode-regex": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.7.4.tgz", + "integrity": "sha512-N77UUIV+WCvE+5yHw+oks3m18/umd7y392Zv7mYTpFqHtkpcc+QUz+gLJNTWVlWROIWeLqY0f3OjZxV5TcXnRw==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.7.4", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/preset-env": { + "version": "7.7.7", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.7.7.tgz", + "integrity": "sha512-pCu0hrSSDVI7kCVUOdcMNQEbOPJ52E+LrQ14sN8uL2ALfSqePZQlKrOy+tM4uhEdYlCHi4imr8Zz2cZe9oSdIg==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.7.4", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-proposal-async-generator-functions": "^7.7.4", + "@babel/plugin-proposal-dynamic-import": "^7.7.4", + "@babel/plugin-proposal-json-strings": "^7.7.4", + "@babel/plugin-proposal-object-rest-spread": "^7.7.7", + "@babel/plugin-proposal-optional-catch-binding": "^7.7.4", + "@babel/plugin-proposal-unicode-property-regex": "^7.7.7", + "@babel/plugin-syntax-async-generators": "^7.7.4", + "@babel/plugin-syntax-dynamic-import": "^7.7.4", + "@babel/plugin-syntax-json-strings": "^7.7.4", + "@babel/plugin-syntax-object-rest-spread": "^7.7.4", + "@babel/plugin-syntax-optional-catch-binding": "^7.7.4", + "@babel/plugin-syntax-top-level-await": "^7.7.4", + "@babel/plugin-transform-arrow-functions": "^7.7.4", + "@babel/plugin-transform-async-to-generator": "^7.7.4", + "@babel/plugin-transform-block-scoped-functions": "^7.7.4", + "@babel/plugin-transform-block-scoping": "^7.7.4", + "@babel/plugin-transform-classes": "^7.7.4", + "@babel/plugin-transform-computed-properties": "^7.7.4", + "@babel/plugin-transform-destructuring": "^7.7.4", + "@babel/plugin-transform-dotall-regex": "^7.7.7", + "@babel/plugin-transform-duplicate-keys": "^7.7.4", + "@babel/plugin-transform-exponentiation-operator": "^7.7.4", + "@babel/plugin-transform-for-of": "^7.7.4", + "@babel/plugin-transform-function-name": "^7.7.4", + "@babel/plugin-transform-literals": "^7.7.4", + "@babel/plugin-transform-member-expression-literals": "^7.7.4", + "@babel/plugin-transform-modules-amd": "^7.7.5", + "@babel/plugin-transform-modules-commonjs": "^7.7.5", + "@babel/plugin-transform-modules-systemjs": "^7.7.4", + "@babel/plugin-transform-modules-umd": "^7.7.4", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.7.4", + "@babel/plugin-transform-new-target": "^7.7.4", + "@babel/plugin-transform-object-super": "^7.7.4", + "@babel/plugin-transform-parameters": "^7.7.7", + "@babel/plugin-transform-property-literals": "^7.7.4", + "@babel/plugin-transform-regenerator": "^7.7.5", + "@babel/plugin-transform-reserved-words": "^7.7.4", + "@babel/plugin-transform-shorthand-properties": "^7.7.4", + "@babel/plugin-transform-spread": "^7.7.4", + "@babel/plugin-transform-sticky-regex": "^7.7.4", + "@babel/plugin-transform-template-literals": "^7.7.4", + "@babel/plugin-transform-typeof-symbol": "^7.7.4", + "@babel/plugin-transform-unicode-regex": "^7.7.4", + "@babel/types": "^7.7.4", + "browserslist": "^4.6.0", + "core-js-compat": "^3.6.0", + "invariant": "^2.2.2", + "js-levenshtein": "^1.1.3", + "semver": "^5.5.0" + } + }, + "@babel/register": { + "version": "7.7.7", + "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.7.7.tgz", + "integrity": "sha512-S2mv9a5dc2pcpg/ConlKZx/6wXaEwHeqfo7x/QbXsdCAZm+WJC1ekVvL1TVxNsedTs5y/gG63MhJTEsmwmjtiA==", + "dev": true, + "requires": { + "find-cache-dir": "^2.0.0", + "lodash": "^4.17.13", + "make-dir": "^2.1.0", + "pirates": "^4.0.0", + "source-map-support": "^0.5.16" + } + }, + "@babel/runtime": { + "version": "7.7.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.7.7.tgz", + "integrity": "sha512-uCnC2JEVAu8AKB5do1WRIsvrdJ0flYx/A/9f/6chdacnEZ7LmavjdsDXr5ksYBegxtuTPR5Va9/+13QF/kFkCA==", + "requires": { + "regenerator-runtime": "^0.13.2" + } + }, + "@babel/template": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.7.4.tgz", + "integrity": "sha512-qUzihgVPguAzXCK7WXw8pqs6cEwi54s3E+HrejlkuWO6ivMKx9hZl3Y2fSXp9i5HgyWmj7RKP+ulaYnKM4yYxw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/parser": "^7.7.4", + "@babel/types": "^7.7.4" + } + }, + "@babel/traverse": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.7.4.tgz", + "integrity": "sha512-P1L58hQyupn8+ezVA2z5KBm4/Zr4lCC8dwKCMYzsa5jFMDMQAzaBNy9W5VjB+KAmBjb40U7a/H6ao+Xo+9saIw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.5.5", + "@babel/generator": "^7.7.4", + "@babel/helper-function-name": "^7.7.4", + "@babel/helper-split-export-declaration": "^7.7.4", + "@babel/parser": "^7.7.4", + "@babel/types": "^7.7.4", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.13" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@babel/types": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.7.4.tgz", + "integrity": "sha512-cz5Ji23KCi4T+YIE/BolWosrJuSmoZeN1EFnRtBwF+KKLi8GG/Z2c2hOJJeCXPk4mwk4QFvTmwIodJowXgttRA==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + }, + "@cnakazawa/watch": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@cnakazawa/watch/-/watch-1.0.3.tgz", + "integrity": "sha512-r5160ogAvGyHsal38Kux7YYtodEKOj89RGb28ht1jh3SJb08VwRwAKKJL0bGb04Zd/3r9FL3BFIc3bBidYffCA==", + "dev": true, + "requires": { + "exec-sh": "^0.3.2", + "minimist": "^1.2.0" + } + }, + "@jest/console": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-24.9.0.tgz", + "integrity": "sha512-Zuj6b8TnKXi3q4ymac8EQfc3ea/uhLeCGThFqXeC8H9/raaH8ARPUTdId+XyGd03Z4In0/VjD2OYFcBF09fNLQ==", + "dev": true, + "requires": { + "@jest/source-map": "^24.9.0", + "chalk": "^2.0.1", + "slash": "^2.0.0" + }, + "dependencies": { + "slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true + } + } + }, + "@jest/core": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-24.9.0.tgz", + "integrity": "sha512-Fogg3s4wlAr1VX7q+rhV9RVnUv5tD7VuWfYy1+whMiWUrvl7U3QJSJyWcDio9Lq2prqYsZaeTv2Rz24pWGkJ2A==", + "dev": true, + "requires": { + "@jest/console": "^24.7.1", + "@jest/reporters": "^24.9.0", + "@jest/test-result": "^24.9.0", + "@jest/transform": "^24.9.0", + "@jest/types": "^24.9.0", + "ansi-escapes": "^3.0.0", + "chalk": "^2.0.1", + "exit": "^0.1.2", + "graceful-fs": "^4.1.15", + "jest-changed-files": "^24.9.0", + "jest-config": "^24.9.0", + "jest-haste-map": "^24.9.0", + "jest-message-util": "^24.9.0", + "jest-regex-util": "^24.3.0", + "jest-resolve": "^24.9.0", + "jest-resolve-dependencies": "^24.9.0", + "jest-runner": "^24.9.0", + "jest-runtime": "^24.9.0", + "jest-snapshot": "^24.9.0", + "jest-util": "^24.9.0", + "jest-validate": "^24.9.0", + "jest-watcher": "^24.9.0", + "micromatch": "^3.1.10", + "p-each-series": "^1.0.0", + "realpath-native": "^1.1.0", + "rimraf": "^2.5.4", + "slash": "^2.0.0", + "strip-ansi": "^5.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "@jest/environment": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-24.9.0.tgz", + "integrity": "sha512-5A1QluTPhvdIPFYnO3sZC3smkNeXPVELz7ikPbhUj0bQjB07EoE9qtLrem14ZUYWdVayYbsjVwIiL4WBIMV4aQ==", + "dev": true, + "requires": { + "@jest/fake-timers": "^24.9.0", + "@jest/transform": "^24.9.0", + "@jest/types": "^24.9.0", + "jest-mock": "^24.9.0" + } + }, + "@jest/fake-timers": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-24.9.0.tgz", + "integrity": "sha512-eWQcNa2YSwzXWIMC5KufBh3oWRIijrQFROsIqt6v/NS9Io/gknw1jsAC9c+ih/RQX4A3O7SeWAhQeN0goKhT9A==", + "dev": true, + "requires": { + "@jest/types": "^24.9.0", + "jest-message-util": "^24.9.0", + "jest-mock": "^24.9.0" + } + }, + "@jest/reporters": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-24.9.0.tgz", + "integrity": "sha512-mu4X0yjaHrffOsWmVLzitKmmmWSQ3GGuefgNscUSWNiUNcEOSEQk9k3pERKEQVBb0Cnn88+UESIsZEMH3o88Gw==", + "dev": true, + "requires": { + "@jest/environment": "^24.9.0", + "@jest/test-result": "^24.9.0", + "@jest/transform": "^24.9.0", + "@jest/types": "^24.9.0", + "chalk": "^2.0.1", + "exit": "^0.1.2", + "glob": "^7.1.2", + "istanbul-lib-coverage": "^2.0.2", + "istanbul-lib-instrument": "^3.0.1", + "istanbul-lib-report": "^2.0.4", + "istanbul-lib-source-maps": "^3.0.1", + "istanbul-reports": "^2.2.6", + "jest-haste-map": "^24.9.0", + "jest-resolve": "^24.9.0", + "jest-runtime": "^24.9.0", + "jest-util": "^24.9.0", + "jest-worker": "^24.6.0", + "node-notifier": "^5.4.2", + "slash": "^2.0.0", + "source-map": "^0.6.0", + "string-length": "^2.0.0" + }, + "dependencies": { + "slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "@jest/source-map": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-24.9.0.tgz", + "integrity": "sha512-/Xw7xGlsZb4MJzNDgB7PW5crou5JqWiBQaz6xyPd3ArOg2nfn/PunV8+olXbbEZzNl591o5rWKE9BRDaFAuIBg==", + "dev": true, + "requires": { + "callsites": "^3.0.0", + "graceful-fs": "^4.1.15", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "@jest/test-result": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-24.9.0.tgz", + "integrity": "sha512-XEFrHbBonBJ8dGp2JmF8kP/nQI/ImPpygKHwQ/SY+es59Z3L5PI4Qb9TQQMAEeYsThG1xF0k6tmG0tIKATNiiA==", + "dev": true, + "requires": { + "@jest/console": "^24.9.0", + "@jest/types": "^24.9.0", + "@types/istanbul-lib-coverage": "^2.0.0" + } + }, + "@jest/test-sequencer": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-24.9.0.tgz", + "integrity": "sha512-6qqsU4o0kW1dvA95qfNog8v8gkRN9ph6Lz7r96IvZpHdNipP2cBcb07J1Z45mz/VIS01OHJ3pY8T5fUY38tg4A==", + "dev": true, + "requires": { + "@jest/test-result": "^24.9.0", + "jest-haste-map": "^24.9.0", + "jest-runner": "^24.9.0", + "jest-runtime": "^24.9.0" + } + }, + "@jest/transform": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-24.9.0.tgz", + "integrity": "sha512-TcQUmyNRxV94S0QpMOnZl0++6RMiqpbH/ZMccFB/amku6Uwvyb1cjYX7xkp5nGNkbX4QPH/FcB6q1HBTHynLmQ==", + "dev": true, + "requires": { + "@babel/core": "^7.1.0", + "@jest/types": "^24.9.0", + "babel-plugin-istanbul": "^5.1.0", + "chalk": "^2.0.1", + "convert-source-map": "^1.4.0", + "fast-json-stable-stringify": "^2.0.0", + "graceful-fs": "^4.1.15", + "jest-haste-map": "^24.9.0", + "jest-regex-util": "^24.9.0", + "jest-util": "^24.9.0", + "micromatch": "^3.1.10", + "pirates": "^4.0.1", + "realpath-native": "^1.1.0", + "slash": "^2.0.0", + "source-map": "^0.6.1", + "write-file-atomic": "2.4.1" + }, + "dependencies": { + "slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "@jest/types": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-24.9.0.tgz", + "integrity": "sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^13.0.0" + } + }, + "@types/anymatch": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@types/anymatch/-/anymatch-1.3.1.tgz", + "integrity": "sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA==", + "dev": true + }, + "@types/babel__core": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.3.tgz", + "integrity": "sha512-8fBo0UR2CcwWxeX7WIIgJ7lXjasFxoYgRnFHUj+hRvKkpiBJbxhdAPTCY6/ZKM0uxANFVzt4yObSLuTiTnazDA==", + "dev": true, + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "@types/babel__generator": { + "version": "7.6.1", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.1.tgz", + "integrity": "sha512-bBKm+2VPJcMRVwNhxKu8W+5/zT7pwNEqeokFOmbvVSqGzFneNxYcEBro9Ac7/N9tlsaPYnZLK8J1LWKkMsLAew==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@types/babel__template": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.0.2.tgz", + "integrity": "sha512-/K6zCpeW7Imzgab2bLkLEbz0+1JlFSrUMdw7KoIIu+IUdu51GWaBZpd3y1VXGVXzynvGa4DaIaxNZHiON3GXUg==", + "dev": true, + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@types/babel__traverse": { + "version": "7.0.8", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.0.8.tgz", + "integrity": "sha512-yGeB2dHEdvxjP0y4UbRtQaSkXJ9649fYCmIdRoul5kfAoGCwxuCbMhag0k3RPfnuh9kPGm8x89btcfDEXdVWGw==", + "dev": true, + "requires": { + "@babel/types": "^7.3.0" + } + }, + "@types/events": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", + "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==", + "dev": true + }, + "@types/glob": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz", + "integrity": "sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==", + "dev": true, + "requires": { + "@types/events": "*", + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "@types/istanbul-lib-coverage": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz", + "integrity": "sha512-hRJD2ahnnpLgsj6KWMYSrmXkM3rm2Dl1qkx6IOFD5FnuNPXJIG5L0dhgKXCYTRMGzU4n0wImQ/xfmRc4POUFlg==", + "dev": true + }, + "@types/istanbul-lib-report": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-1.1.1.tgz", + "integrity": "sha512-3BUTyMzbZa2DtDI2BkERNC6jJw2Mr2Y0oGI7mRxYNBPxppbtEK1F66u3bKwU2g+wxwWI7PAoRpJnOY1grJqzHg==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*" + } + }, + "@types/istanbul-reports": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.1.tgz", + "integrity": "sha512-UpYjBi8xefVChsCoBpKShdxTllC9pwISirfoZsUa2AAdQg/Jd2KQGtSbw+ya7GPo7x/wAPlH6JBhKhAsXUEZNA==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "@types/minimatch": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", + "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", + "dev": true + }, + "@types/node": { + "version": "13.7.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-13.7.0.tgz", + "integrity": "sha512-GnZbirvmqZUzMgkFn70c74OQpTTUcCzlhQliTzYjQMqg+hVKcDnxdL19Ne3UdYzdMA/+W3eb646FWn/ZaT1NfQ==", + "dev": true + }, + "@types/source-list-map": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz", + "integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==", + "dev": true + }, + "@types/stack-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz", + "integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==", + "dev": true + }, + "@types/tapable": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.5.tgz", + "integrity": "sha512-/gG2M/Imw7cQFp8PGvz/SwocNrmKFjFsm5Pb8HdbHkZ1K8pmuPzOX4VeVoiEecFCVf4CsN1r3/BRvx+6sNqwtQ==", + "dev": true + }, + "@types/uglify-js": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.0.4.tgz", + "integrity": "sha512-SudIN9TRJ+v8g5pTG8RRCqfqTMNqgWCKKd3vtynhGzkIIjxaicNAMuY5TRadJ6tzDu3Dotf3ngaMILtmOdmWEQ==", + "dev": true, + "requires": { + "source-map": "^0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "@types/webpack": { + "version": "4.41.8", + "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.8.tgz", + "integrity": "sha512-mh4litLHTlDG84TGCFv1pZldndI34vkrW9Mks++Zx4KET7DRMoCXUvLbTISiuF4++fMgNnhV9cc1nCXJQyBYbQ==", + "dev": true, + "requires": { + "@types/anymatch": "*", + "@types/node": "*", + "@types/tapable": "*", + "@types/uglify-js": "*", + "@types/webpack-sources": "*", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "@types/webpack-sources": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-0.1.7.tgz", + "integrity": "sha512-XyaHrJILjK1VHVC4aVlKsdNN5KBTwufMb43cQs+flGxtPAf/1Qwl8+Q0tp5BwEGaI8D6XT1L+9bSWXckgkjTLw==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/source-list-map": "*", + "source-map": "^0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "@types/yargs": { + "version": "13.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.4.tgz", + "integrity": "sha512-Ke1WmBbIkVM8bpvsNEcGgQM70XcEh/nbpxQhW7FhrsbCsXSY9BmLB1+LHtD7r9zrsOcFlLiF+a/UeJsdfw3C5A==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "@types/yargs-parser": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-13.1.0.tgz", + "integrity": "sha512-gCubfBUZ6KxzoibJ+SCUc/57Ms1jz5NjHe4+dI2krNmU5zCPAphyLJYyTOg06ueIyfj+SaCUqmzun7ImlxDcKg==", + "dev": true + }, + "@webassemblyjs/ast": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", + "integrity": "sha512-C6wW5L+b7ogSDVqymbkkvuW9kruN//YisMED04xzeBBqjHa2FYnmvOlS6Xj68xWQRgWvI9cIglsjFowH/RJyEA==", + "dev": true, + "requires": { + "@webassemblyjs/helper-module-context": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/wast-parser": "1.9.0" + } + }, + "@webassemblyjs/floating-point-hex-parser": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.9.0.tgz", + "integrity": "sha512-TG5qcFsS8QB4g4MhrxK5TqfdNe7Ey/7YL/xN+36rRjl/BlGE/NcBvJcqsRgCP6Z92mRE+7N50pRIi8SmKUbcQA==", + "dev": true + }, + "@webassemblyjs/helper-api-error": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.9.0.tgz", + "integrity": "sha512-NcMLjoFMXpsASZFxJ5h2HZRcEhDkvnNFOAKneP5RbKRzaWJN36NC4jqQHKwStIhGXu5mUWlUUk7ygdtrO8lbmw==", + "dev": true + }, + "@webassemblyjs/helper-buffer": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.9.0.tgz", + "integrity": "sha512-qZol43oqhq6yBPx7YM3m9Bv7WMV9Eevj6kMi6InKOuZxhw+q9hOkvq5e/PpKSiLfyetpaBnogSbNCfBwyB00CA==", + "dev": true + }, + "@webassemblyjs/helper-code-frame": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.9.0.tgz", + "integrity": "sha512-ERCYdJBkD9Vu4vtjUYe8LZruWuNIToYq/ME22igL+2vj2dQ2OOujIZr3MEFvfEaqKoVqpsFKAGsRdBSBjrIvZA==", + "dev": true, + "requires": { + "@webassemblyjs/wast-printer": "1.9.0" + } + }, + "@webassemblyjs/helper-fsm": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.9.0.tgz", + "integrity": "sha512-OPRowhGbshCb5PxJ8LocpdX9Kl0uB4XsAjl6jH/dWKlk/mzsANvhwbiULsaiqT5GZGT9qinTICdj6PLuM5gslw==", + "dev": true + }, + "@webassemblyjs/helper-module-context": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.9.0.tgz", + "integrity": "sha512-MJCW8iGC08tMk2enck1aPW+BE5Cw8/7ph/VGZxwyvGbJwjktKkDK7vy7gAmMDx88D7mhDTCNKAW5tED+gZ0W8g==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0" + } + }, + "@webassemblyjs/helper-wasm-bytecode": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.9.0.tgz", + "integrity": "sha512-R7FStIzyNcd7xKxCZH5lE0Bqy+hGTwS3LJjuv1ZVxd9O7eHCedSdrId/hMOd20I+v8wDXEn+bjfKDLzTepoaUw==", + "dev": true + }, + "@webassemblyjs/helper-wasm-section": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.9.0.tgz", + "integrity": "sha512-XnMB8l3ek4tvrKUUku+IVaXNHz2YsJyOOmz+MMkZvh8h1uSJpSen6vYnw3IoQ7WwEuAhL8Efjms1ZWjqh2agvw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-buffer": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/wasm-gen": "1.9.0" + } + }, + "@webassemblyjs/ieee754": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.9.0.tgz", + "integrity": "sha512-dcX8JuYU/gvymzIHc9DgxTzUUTLexWwt8uCTWP3otys596io0L5aW02Gb1RjYpx2+0Jus1h4ZFqjla7umFniTg==", + "dev": true, + "requires": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "@webassemblyjs/leb128": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.9.0.tgz", + "integrity": "sha512-ENVzM5VwV1ojs9jam6vPys97B/S65YQtv/aanqnU7D8aSoHFX8GyhGg0CMfyKNIHBuAVjy3tlzd5QMMINa7wpw==", + "dev": true, + "requires": { + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/utf8": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.9.0.tgz", + "integrity": "sha512-GZbQlWtopBTP0u7cHrEx+73yZKrQoBMpwkGEIqlacljhXCkVM1kMQge/Mf+csMJAjEdSwhOyLAS0AoR3AG5P8w==", + "dev": true + }, + "@webassemblyjs/wasm-edit": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.9.0.tgz", + "integrity": "sha512-FgHzBm80uwz5M8WKnMTn6j/sVbqilPdQXTWraSjBwFXSYGirpkSWE2R9Qvz9tNiTKQvoKILpCuTjBKzOIm0nxw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-buffer": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/helper-wasm-section": "1.9.0", + "@webassemblyjs/wasm-gen": "1.9.0", + "@webassemblyjs/wasm-opt": "1.9.0", + "@webassemblyjs/wasm-parser": "1.9.0", + "@webassemblyjs/wast-printer": "1.9.0" + } + }, + "@webassemblyjs/wasm-gen": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.9.0.tgz", + "integrity": "sha512-cPE3o44YzOOHvlsb4+E9qSqjc9Qf9Na1OO/BHFy4OI91XDE14MjFN4lTMezzaIWdPqHnsTodGGNP+iRSYfGkjA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/ieee754": "1.9.0", + "@webassemblyjs/leb128": "1.9.0", + "@webassemblyjs/utf8": "1.9.0" + } + }, + "@webassemblyjs/wasm-opt": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.9.0.tgz", + "integrity": "sha512-Qkjgm6Anhm+OMbIL0iokO7meajkzQD71ioelnfPEj6r4eOFuqm4YC3VBPqXjFyyNwowzbMD+hizmprP/Fwkl2A==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-buffer": "1.9.0", + "@webassemblyjs/wasm-gen": "1.9.0", + "@webassemblyjs/wasm-parser": "1.9.0" + } + }, + "@webassemblyjs/wasm-parser": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.9.0.tgz", + "integrity": "sha512-9+wkMowR2AmdSWQzsPEjFU7njh8HTO5MqO8vjwEHuM+AMHioNqSBONRdr0NQQ3dVQrzp0s8lTcYqzUdb7YgELA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-api-error": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/ieee754": "1.9.0", + "@webassemblyjs/leb128": "1.9.0", + "@webassemblyjs/utf8": "1.9.0" + } + }, + "@webassemblyjs/wast-parser": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.9.0.tgz", + "integrity": "sha512-qsqSAP3QQ3LyZjNC/0jBJ/ToSxfYJ8kYyuiGvtn/8MK89VrNEfwj7BPQzJVHi0jGTRK2dGdJ5PRqhtjzoww+bw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/floating-point-hex-parser": "1.9.0", + "@webassemblyjs/helper-api-error": "1.9.0", + "@webassemblyjs/helper-code-frame": "1.9.0", + "@webassemblyjs/helper-fsm": "1.9.0", + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/wast-printer": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.9.0.tgz", + "integrity": "sha512-2J0nE95rHXHyQ24cWjMKJ1tqB/ds8z/cyeOZxJhcb+rW+SQASVjuznUSmdz5GpVJTzU8JkhYut0D3siFDD6wsA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/wast-parser": "1.9.0", + "@xtuc/long": "4.2.2" + } + }, + "@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "abab": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.3.tgz", + "integrity": "sha512-tsFzPpcttalNjFBCFMqsKYQcWxxen1pgJR56by//QwvJc4/OUS3kPOOttx2tSIfjsylB0pYu7f5D3K1RCxUnUg==", + "dev": true + }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "acorn": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", + "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==", + "dev": true + }, + "acorn-globals": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.4.tgz", + "integrity": "sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A==", + "dev": true, + "requires": { + "acorn": "^6.0.1", + "acorn-walk": "^6.0.1" + }, + "dependencies": { + "acorn-walk": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz", + "integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==", + "dev": true + } + } + }, + "ajv": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", + "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", + "dev": true, + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-errors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", + "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==", + "dev": true + }, + "ajv-keywords": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.1.tgz", + "integrity": "sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==", + "dev": true + }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", + "dev": true + }, + "ansi-escapes": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", + "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", + "dev": true + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + } + }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "dev": true + }, + "are-we-there-yet": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", + "dev": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", + "dev": true + }, + "array-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", + "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=", + "dev": true + }, + "array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", + "dev": true + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "dev": true, + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "asn1.js": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", + "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", + "dev": true, + "requires": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "assert": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz", + "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==", + "dev": true, + "requires": { + "object-assign": "^4.1.1", + "util": "0.10.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", + "dev": true + }, + "util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "dev": true, + "requires": { + "inherits": "2.0.1" + } + } + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", + "dev": true + }, + "astral-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "dev": true + }, + "async-each": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", + "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", + "dev": true + }, + "async-foreach": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz", + "integrity": "sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=", + "dev": true + }, + "async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", + "dev": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "dev": true + }, + "aws4": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.0.tgz", + "integrity": "sha512-Uvq6hVe90D0B2WEnUqtdgY1bATGz3mw33nH9Y+dmA+w5DHvUmBgkr5rM/KCHpCsiFNRUfokW/szpPPgMK2hm4A==", + "dev": true + }, + "babel-jest": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-24.9.0.tgz", + "integrity": "sha512-ntuddfyiN+EhMw58PTNL1ph4C9rECiQXjI4nMMBKBaNjXvqLdkXpPRcMSr4iyBrJg/+wz9brFUD6RhOAT6r4Iw==", + "dev": true, + "requires": { + "@jest/transform": "^24.9.0", + "@jest/types": "^24.9.0", + "@types/babel__core": "^7.1.0", + "babel-plugin-istanbul": "^5.1.0", + "babel-preset-jest": "^24.9.0", + "chalk": "^2.4.2", + "slash": "^2.0.0" + }, + "dependencies": { + "slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true + } + } + }, + "babel-loader": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.1.0.tgz", + "integrity": "sha512-7q7nC1tYOrqvUrN3LQK4GwSk/TQorZSOlO9C+RZDZpODgyN4ZlCqE5q9cDsyWOliN+aU9B4JX01xK9eJXowJLw==", + "dev": true, + "requires": { + "find-cache-dir": "^2.1.0", + "loader-utils": "^1.4.0", + "mkdirp": "^0.5.3", + "pify": "^4.0.1", + "schema-utils": "^2.6.5" + } + }, + "babel-plugin-dynamic-import-node": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.0.tgz", + "integrity": "sha512-o6qFkpeQEBxcqt0XYlWzAVxNCSCZdUgcR8IRlhD/8DylxjjO4foPcvTW0GGKa/cVt3rvxZ7o5ippJ+/0nvLhlQ==", + "dev": true, + "requires": { + "object.assign": "^4.1.0" + } + }, + "babel-plugin-istanbul": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-5.2.0.tgz", + "integrity": "sha512-5LphC0USA8t4i1zCtjbbNb6jJj/9+X6P37Qfirc/70EQ34xKlMW+a1RHGwxGI+SwWpNwZ27HqvzAobeqaXwiZw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "find-up": "^3.0.0", + "istanbul-lib-instrument": "^3.3.0", + "test-exclude": "^5.2.3" + } + }, + "babel-plugin-jest-hoist": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-24.9.0.tgz", + "integrity": "sha512-2EMA2P8Vp7lG0RAzr4HXqtYwacfMErOuv1U3wrvxHX6rD1sV6xS3WXG3r8TRQ2r6w8OhvSdWt+z41hQNwNm3Xw==", + "dev": true, + "requires": { + "@types/babel__traverse": "^7.0.6" + } + }, + "babel-preset-jest": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-24.9.0.tgz", + "integrity": "sha512-izTUuhE4TMfTRPF92fFwD2QfdXaZW08qvWTFCI51V8rW5x00UuPgc3ajRoWofXOuxjfcOM5zzSYsQS3H8KGCAg==", + "dev": true, + "requires": { + "@babel/plugin-syntax-object-rest-spread": "^7.0.0", + "babel-plugin-jest-hoist": "^24.9.0" + } + }, + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "dev": true, + "requires": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + }, + "dependencies": { + "core-js": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz", + "integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==", + "dev": true + }, + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", + "dev": true + } + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, + "requires": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "base64-js": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", + "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==", + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "dev": true, + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true + }, + "binary-extensions": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", + "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", + "dev": true + }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "optional": true, + "requires": { + "file-uri-to-path": "1.0.0" + } + }, + "block-stream": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", + "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", + "dev": true, + "requires": { + "inherits": "~2.0.0" + } + }, + "bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true + }, + "bn.js": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", + "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", + "dev": true + }, + "browser-process-hrtime": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz", + "integrity": "sha512-bRFnI4NnjO6cnyLmOV/7PVoDEMJChlcfN0z4s1YMBY989/SvlfMI1lgCnkFUs53e9gQF+w7qu7XdllSTiSl8Aw==", + "dev": true + }, + "browser-resolve": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.3.tgz", + "integrity": "sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ==", + "dev": true, + "requires": { + "resolve": "1.1.7" + }, + "dependencies": { + "resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", + "dev": true + } + } + }, + "browserify-aes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "dev": true, + "requires": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "browserify-cipher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", + "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", + "dev": true, + "requires": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" + } + }, + "browserify-des": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", + "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", + "dev": true, + "requires": { + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "browserify-rsa": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", + "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "randombytes": "^2.0.1" + } + }, + "browserify-sign": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", + "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", + "dev": true, + "requires": { + "bn.js": "^4.1.1", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.2", + "elliptic": "^6.0.0", + "inherits": "^2.0.1", + "parse-asn1": "^5.0.0" + } + }, + "browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "dev": true, + "requires": { + "pako": "~1.0.5" + } + }, + "browserslist": { + "version": "4.8.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.8.2.tgz", + "integrity": "sha512-+M4oeaTplPm/f1pXDw84YohEv7B1i/2Aisei8s4s6k3QsoSHa7i5sz8u/cGQkkatCPxMASKxPualR4wwYgVboA==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001015", + "electron-to-chromium": "^1.3.322", + "node-releases": "^1.1.42" + } + }, + "bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "requires": { + "node-int64": "^0.4.0" + } + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, + "buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", + "dev": true + }, + "builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", + "dev": true + }, + "cacache": { + "version": "12.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.4.tgz", + "integrity": "sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ==", + "dev": true, + "requires": { + "bluebird": "^3.5.5", + "chownr": "^1.1.1", + "figgy-pudding": "^3.5.1", + "glob": "^7.1.4", + "graceful-fs": "^4.1.15", + "infer-owner": "^1.0.3", + "lru-cache": "^5.1.1", + "mississippi": "^3.0.0", + "mkdirp": "^0.5.1", + "move-concurrently": "^1.0.1", + "promise-inflight": "^1.0.1", + "rimraf": "^2.6.3", + "ssri": "^6.0.1", + "unique-filename": "^1.1.1", + "y18n": "^4.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + } + } + }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, + "requires": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", + "dev": true + }, + "camelcase-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", + "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", + "dev": true, + "requires": { + "camelcase": "^2.0.0", + "map-obj": "^1.0.0" + }, + "dependencies": { + "camelcase": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", + "dev": true + } + } + }, + "caniuse-lite": { + "version": "1.0.30001017", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001017.tgz", + "integrity": "sha512-EDnZyOJ6eYh6lHmCvCdHAFbfV4KJ9lSdfv4h/ppEhrU/Yudkl7jujwMZ1we6RX7DXqBfT04pVMQ4J+1wcTlsKA==", + "dev": true + }, + "capture-exit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz", + "integrity": "sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g==", + "dev": true, + "requires": { + "rsvp": "^4.8.4" + } + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "dev": true + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "chokidar": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", + "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", + "dev": true, + "requires": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "fsevents": "^1.2.7", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + }, + "dependencies": { + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + } + } + }, + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true + }, + "chrome-trace-event": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz", + "integrity": "sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "dev": true + }, + "cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "clean-webpack-plugin": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/clean-webpack-plugin/-/clean-webpack-plugin-3.0.0.tgz", + "integrity": "sha512-MciirUH5r+cYLGCOL5JX/ZLzOZbVr1ot3Fw+KcvbhUb6PM+yycqd9ZhIlcigQ5gl+XhppNmw3bEFuaaMNyLj3A==", + "dev": true, + "requires": { + "@types/webpack": "^4.4.31", + "del": "^4.1.1" + }, + "dependencies": { + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "dev": true, + "requires": { + "array-uniq": "^1.0.1" + } + }, + "del": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/del/-/del-4.1.1.tgz", + "integrity": "sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ==", + "dev": true, + "requires": { + "@types/glob": "^7.1.1", + "globby": "^6.1.0", + "is-path-cwd": "^2.0.0", + "is-path-in-cwd": "^2.0.0", + "p-map": "^2.0.0", + "pify": "^4.0.1", + "rimraf": "^2.6.3" + } + }, + "globby": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", + "dev": true, + "requires": { + "array-union": "^1.0.1", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + }, + "p-map": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", + "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", + "dev": true + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "dev": true, + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wrap-ansi": "^2.0.0" + } + }, + "clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "dev": true + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "dev": true, + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "console-browserify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", + "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==", + "dev": true + }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "dev": true + }, + "constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=", + "dev": true + }, + "convert-source-map": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", + "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + } + }, + "copy-concurrently": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", + "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", + "dev": true, + "requires": { + "aproba": "^1.1.1", + "fs-write-stream-atomic": "^1.0.8", + "iferr": "^0.1.5", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.0" + }, + "dependencies": { + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", + "dev": true + }, + "core-js": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.1.tgz", + "integrity": "sha512-186WjSik2iTGfDjfdCZAxv2ormxtKgemjC3SI6PL31qOA0j5LhTDVjHChccoc7brwLvpvLPiMyRlcO88C4l1QQ==", + "dev": true + }, + "core-js-compat": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.6.1.tgz", + "integrity": "sha512-2Tl1EuxZo94QS2VeH28Ebf5g3xbPZG/hj/N5HDDy4XMP/ImR0JIer/nggQRiMN91Q54JVkGbytf42wO29oXVHg==", + "dev": true, + "requires": { + "browserslist": "^4.8.2", + "semver": "7.0.0" + }, + "dependencies": { + "semver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", + "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", + "dev": true + } + } + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "create-ecdh": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", + "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "elliptic": "^6.0.0" + } + }, + "create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "dev": true, + "requires": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "dev": true, + "requires": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "cross-spawn": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz", + "integrity": "sha1-ElYDfsufDF9549bvE14wdwGEuYI=", + "dev": true, + "requires": { + "lru-cache": "^4.0.1", + "which": "^1.2.9" + } + }, + "crypto-browserify": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", + "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", + "dev": true, + "requires": { + "browserify-cipher": "^1.0.0", + "browserify-sign": "^4.0.0", + "create-ecdh": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.0", + "diffie-hellman": "^5.0.0", + "inherits": "^2.0.1", + "pbkdf2": "^3.0.3", + "public-encrypt": "^4.0.0", + "randombytes": "^2.0.0", + "randomfill": "^1.0.3" + } + }, + "css-loader": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-3.4.2.tgz", + "integrity": "sha512-jYq4zdZT0oS0Iykt+fqnzVLRIeiPWhka+7BqPn+oSIpWJAHak5tmB/WZrJ2a21JhCeFyNnnlroSl8c+MtVndzA==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "cssesc": "^3.0.0", + "icss-utils": "^4.1.1", + "loader-utils": "^1.2.3", + "normalize-path": "^3.0.0", + "postcss": "^7.0.23", + "postcss-modules-extract-imports": "^2.0.0", + "postcss-modules-local-by-default": "^3.0.2", + "postcss-modules-scope": "^2.1.1", + "postcss-modules-values": "^3.0.0", + "postcss-value-parser": "^4.0.2", + "schema-utils": "^2.6.0" + }, + "dependencies": { + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + } + } + }, + "css.gg": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/css.gg/-/css.gg-1.0.6.tgz", + "integrity": "sha512-Bv8GTVkeuSqqkgdCJ+tJopRxf/mp/wP6hkL13BdCSs3FadD0GWyU3gKdjuaaFkfxkgYK+GhjSX3EA+cXLHBFpA==" + }, + "cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true + }, + "cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true + }, + "cssstyle": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-1.4.0.tgz", + "integrity": "sha512-GBrLZYZ4X4x6/QEoBnIrqb8B/f5l4+8me2dkom/j1Gtbxy0kBv6OGzKuAsGM75bkGwGAFkt56Iwg28S3XTZgSA==", + "dev": true, + "requires": { + "cssom": "0.3.x" + } + }, + "currently-unhandled": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", + "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", + "dev": true, + "requires": { + "array-find-index": "^1.0.1" + } + }, + "cyclist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", + "integrity": "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=", + "dev": true + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "data-urls": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.1.0.tgz", + "integrity": "sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==", + "dev": true, + "requires": { + "abab": "^2.0.0", + "whatwg-mimetype": "^2.2.0", + "whatwg-url": "^7.0.0" + }, + "dependencies": { + "whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "dev": true, + "requires": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + } + } + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "dev": true + }, + "deep-diff": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/deep-diff/-/deep-diff-0.3.8.tgz", + "integrity": "sha1-wB3mPvsO7JeYgB1Ax+Da4ltYLIQ=" + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "requires": { + "object-keys": "^1.0.12" + } + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "dev": true + }, + "des.js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", + "integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "detect-file": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", + "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=", + "dev": true + }, + "detect-newline": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz", + "integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=", + "dev": true + }, + "diff-sequences": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-24.9.0.tgz", + "integrity": "sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew==", + "dev": true + }, + "diffie-hellman": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" + } + }, + "domain-browser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", + "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", + "dev": true + }, + "domexception": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", + "integrity": "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==", + "dev": true, + "requires": { + "webidl-conversions": "^4.0.2" + } + }, + "duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "dev": true, + "requires": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "dev": true, + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "electron-to-chromium": { + "version": "1.3.322", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.322.tgz", + "integrity": "sha512-Tc8JQEfGQ1MzfSzI/bTlSr7btJv/FFO7Yh6tanqVmIWOuNCu6/D1MilIEgLtmWqIrsv+o4IjpLAhgMBr/ncNAA==", + "dev": true + }, + "elliptic": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.2.tgz", + "integrity": "sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw==", + "dev": true, + "requires": { + "bn.js": "^4.4.0", + "brorand": "^1.0.1", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.0" + } + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true + }, + "end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, + "enhanced-resolve": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.1.tgz", + "integrity": "sha512-98p2zE+rL7/g/DzMHMTF4zZlCgeVdJ7yr6xzEpJRYwFYrGi9ANdn5DnJURg6RpBkyk60XYDnWIv51VfIhfNGuA==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "memory-fs": "^0.5.0", + "tapable": "^1.0.0" + }, + "dependencies": { + "memory-fs": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", + "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", + "dev": true, + "requires": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + } + } + } + }, + "errno": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", + "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", + "dev": true, + "requires": { + "prr": "~1.0.1" + } + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es-abstract": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.0.tgz", + "integrity": "sha512-yYkE07YF+6SIBmg1MsJ9dlub5L48Ek7X0qz+c/CPCHS9EBXfESorzng4cJQjJW5/pB6vDF41u7F8vUhLVDqIug==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.1.5", + "is-regex": "^1.0.5", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimleft": "^2.1.1", + "string.prototype.trimright": "^2.1.1" + }, + "dependencies": { + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true + } + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "escodegen": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.12.0.tgz", + "integrity": "sha512-TuA+EhsanGcme5T3R0L80u4t8CpbXQjegRmf7+FPTJrtCTErXFeelblRgHQa1FofEzqYYJmJ/OqjTwREp9qgmg==", + "dev": true, + "requires": { + "esprima": "^3.1.3", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true + } + } + }, + "eslint-scope": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", + "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "esprima": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", + "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=", + "dev": true + }, + "esrecurse": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", + "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "dev": true, + "requires": { + "estraverse": "^4.1.0" + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "dev": true, + "requires": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, + "exec-sh": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.4.tgz", + "integrity": "sha512-sEFIkc61v75sWeOe72qyrqg2Qg0OuLESziUDk/O/z2qgS15y2gWVFrI6f2Qn/qw/0/NCfCEsmNA4zOjkwEZT1A==", + "dev": true + }, + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + } + } + }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", + "dev": true + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=", + "dev": true, + "requires": { + "homedir-polyfill": "^1.0.1" + } + }, + "expect": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-24.9.0.tgz", + "integrity": "sha512-wvVAx8XIol3Z5m9zvZXiyZOQ+sRJqNTIm6sGjdWlaZIeupQGO3WbYI+15D/AmEwZywL6wtJkbAbJtzkOfBuR0Q==", + "dev": true, + "requires": { + "@jest/types": "^24.9.0", + "ansi-styles": "^3.2.0", + "jest-get-type": "^24.9.0", + "jest-matcher-utils": "^24.9.0", + "jest-message-util": "^24.9.0", + "jest-regex-util": "^24.9.0" + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "dev": true + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "fb-watchman": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.1.tgz", + "integrity": "sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg==", + "dev": true, + "requires": { + "bser": "2.1.1" + } + }, + "fetch-mock": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/fetch-mock/-/fetch-mock-8.3.1.tgz", + "integrity": "sha512-7IEIUvkHO6zOHbDSzkMAvkb2mx3N5xy9BS4RjFnIe8kCUDOomoNKBDKGwhTj5E0uuieo8rg55c6cUKorJuk4rg==", + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "core-js": "^3.0.0", + "glob-to-regexp": "^0.4.0", + "lodash.isequal": "^4.5.0", + "path-to-regexp": "^2.2.1", + "querystring": "^0.2.0", + "whatwg-url": "^6.5.0" + } + }, + "figgy-pudding": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz", + "integrity": "sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==", + "dev": true + }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "optional": true + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "find-cache-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" + } + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "findup-sync": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz", + "integrity": "sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==", + "dev": true, + "requires": { + "detect-file": "^1.0.0", + "is-glob": "^4.0.0", + "micromatch": "^3.0.4", + "resolve-dir": "^1.0.1" + } + }, + "flush-write-stream": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", + "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "readable-stream": "^2.3.6" + } + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "dev": true, + "requires": { + "map-cache": "^0.2.2" + } + }, + "from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + } + }, + "fs-write-stream-atomic": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", + "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "iferr": "^0.1.5", + "imurmurhash": "^0.1.4", + "readable-stream": "1 || 2" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "fsevents": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.12.tgz", + "integrity": "sha512-Ggd/Ktt7E7I8pxZRbGIs7vwqAPscSESMrCSkx2FtWeqmheJgCo2R74fTsZFCifr0VTPwqRpPv17+6b8Zp7th0Q==", + "dev": true, + "optional": true, + "requires": { + "bindings": "^1.5.0", + "nan": "^2.12.1", + "node-pre-gyp": "*" + }, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "aproba": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "chownr": { + "version": "1.1.4", + "bundled": true, + "dev": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "optional": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "optional": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "debug": { + "version": "3.2.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ms": "^2.1.1" + } + }, + "deep-extend": { + "version": "0.6.0", + "bundled": true, + "dev": true, + "optional": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "detect-libc": { + "version": "1.0.3", + "bundled": true, + "dev": true, + "optional": true + }, + "fs-minipass": { + "version": "1.2.7", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.6.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "glob": { + "version": "7.1.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "iconv-lite": { + "version": "0.4.24", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore-walk": { + "version": "3.0.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minimatch": "^3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "bundled": true, + "dev": true, + "optional": true + }, + "ini": { + "version": "1.3.5", + "bundled": true, + "dev": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "bundled": true, + "dev": true, + "optional": true + }, + "minipass": { + "version": "2.9.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.3.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.9.0" + } + }, + "mkdirp": { + "version": "0.5.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "ms": { + "version": "2.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "needle": { + "version": "2.3.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "debug": "^3.2.6", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.14.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.1", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4.4.2" + } + }, + "nopt": { + "version": "4.0.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "npm-bundled": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "npm-normalize-package-bin": "^1.0.1" + } + }, + "npm-normalize-package-bin": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "npm-packlist": { + "version": "1.4.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1", + "npm-normalize-package-bin": "^1.0.1" + } + }, + "npmlog": { + "version": "4.1.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "wrappy": "1" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "osenv": { + "version": "0.1.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "process-nextick-args": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "rc": { + "version": "1.2.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + } + }, + "readable-stream": { + "version": "2.3.7", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "rimraf": { + "version": "2.7.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "glob": "^7.1.3" + } + }, + "safe-buffer": { + "version": "5.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "safer-buffer": { + "version": "2.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "sax": { + "version": "1.2.4", + "bundled": true, + "dev": true, + "optional": true + }, + "semver": { + "version": "5.7.1", + "bundled": true, + "dev": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "tar": { + "version": "4.4.13", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.8.6", + "minizlib": "^1.2.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.3" + } + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "wide-align": { + "version": "1.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "yallist": { + "version": "3.1.1", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "dependencies": { + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "dev": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "gaze": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", + "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", + "dev": true, + "requires": { + "globule": "^1.0.0" + } + }, + "get-caller-file": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", + "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", + "dev": true + }, + "get-stdin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", + "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", + "dev": true + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + }, + "dependencies": { + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } + } + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", + "dev": true + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, + "global-modules": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", + "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "dev": true, + "requires": { + "global-prefix": "^1.0.1", + "is-windows": "^1.0.1", + "resolve-dir": "^1.0.0" + } + }, + "global-prefix": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", + "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=", + "dev": true, + "requires": { + "expand-tilde": "^2.0.2", + "homedir-polyfill": "^1.0.1", + "ini": "^1.3.4", + "is-windows": "^1.0.1", + "which": "^1.2.14" + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "globule": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/globule/-/globule-1.3.1.tgz", + "integrity": "sha512-OVyWOHgw29yosRHCHo7NncwR1hW5ew0W/UrvtwvjefVJeQ26q4/8r8FmPsSF1hJ93IgWkyv16pCTz6WblMzm/g==", + "dev": true, + "requires": { + "glob": "~7.1.1", + "lodash": "~4.17.12", + "minimatch": "~3.0.2" + } + }, + "graceful-fs": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.0.tgz", + "integrity": "sha512-jpSvDPV4Cq/bgtpndIWbI5hmYxhQGHPC4d4cqBPb4DLniCfhJokdXhwhaDuLBGLQdvvRum/UiX6ECVIPvDXqdg==", + "dev": true + }, + "growly": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", + "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", + "dev": true + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "dev": true + }, + "har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "dev": true, + "requires": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "has-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", + "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", + "dev": true + }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "dev": true + }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "dev": true, + "requires": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "hash-base": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", + "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "dev": true, + "requires": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "hoist-non-react-statics": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", + "integrity": "sha512-wbg3bpgA/ZqWrZuMOeJi8+SKMhr7X9TesL/rXMjTzh0p0JUBo3II8DHboYbuIXWRlttrUFxwcu/5kygrCw8fJw==", + "requires": { + "react-is": "^16.7.0" + } + }, + "homedir-polyfill": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", + "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", + "dev": true, + "requires": { + "parse-passwd": "^1.0.0" + } + }, + "hosted-git-info": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", + "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==", + "dev": true + }, + "html-encoding-sniffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz", + "integrity": "sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==", + "dev": true, + "requires": { + "whatwg-encoding": "^1.0.1" + } + }, + "html-escaper": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.1.tgz", + "integrity": "sha512-hNX23TjWwD3q56HpWjUHOKj1+4KKlnjv9PcmBUYKVpga+2cnb9nDx/B1o0yO4n+RZXZdiNxzx6B24C9aNMTkkQ==", + "dev": true + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "https-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", + "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", + "dev": true + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "icss-utils": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-4.1.1.tgz", + "integrity": "sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA==", + "dev": true, + "requires": { + "postcss": "^7.0.14" + } + }, + "ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", + "dev": true + }, + "iferr": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", + "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=", + "dev": true + }, + "import-local": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", + "integrity": "sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==", + "dev": true, + "requires": { + "pkg-dir": "^3.0.0", + "resolve-cwd": "^2.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "in-publish": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.1.tgz", + "integrity": "sha512-oDM0kUSNFC31ShNxHKUyfZKy8ZeXZBWMjMdZHKLOk13uvT27VTL/QzRGfRUcevJhpkZAvlhPYuXkF7eNWrtyxQ==", + "dev": true + }, + "indexes-of": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", + "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=", + "dev": true + }, + "infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "dev": true + }, + "interpret": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.2.0.tgz", + "integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==", + "dev": true + }, + "invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "requires": { + "loose-envify": "^1.0.0" + } + }, + "invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", + "dev": true + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "dev": true, + "requires": { + "binary-extensions": "^1.0.0" + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-callable": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", + "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", + "dev": true + }, + "is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "dev": true, + "requires": { + "ci-info": "^2.0.0" + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-date-object": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", + "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", + "dev": true + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-finite": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz", + "integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-path-cwd": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", + "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", + "dev": true + }, + "is-path-in-cwd": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz", + "integrity": "sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==", + "dev": true, + "requires": { + "is-path-inside": "^2.1.0" + }, + "dependencies": { + "is-path-inside": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz", + "integrity": "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==", + "dev": true, + "requires": { + "path-is-inside": "^1.0.2" + } + } + } + }, + "is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", + "dev": true + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "is-regex": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", + "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + }, + "is-symbol": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", + "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + }, + "dependencies": { + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true + } + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, + "is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", + "dev": true + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true + }, + "is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "dev": true + }, + "istanbul-lib-coverage": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", + "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==", + "dev": true + }, + "istanbul-lib-instrument": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz", + "integrity": "sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA==", + "dev": true, + "requires": { + "@babel/generator": "^7.4.0", + "@babel/parser": "^7.4.3", + "@babel/template": "^7.4.0", + "@babel/traverse": "^7.4.3", + "@babel/types": "^7.4.0", + "istanbul-lib-coverage": "^2.0.5", + "semver": "^6.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "istanbul-lib-report": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-2.0.8.tgz", + "integrity": "sha512-fHBeG573EIihhAblwgxrSenp0Dby6tJMFR/HvlerBsrCTD5bkUuoNtn3gVh29ZCS824cGGBPn7Sg7cNk+2xUsQ==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^2.0.5", + "make-dir": "^2.1.0", + "supports-color": "^6.1.0" + }, + "dependencies": { + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz", + "integrity": "sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^2.0.5", + "make-dir": "^2.1.0", + "rimraf": "^2.6.3", + "source-map": "^0.6.1" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "istanbul-reports": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-2.2.7.tgz", + "integrity": "sha512-uu1F/L1o5Y6LzPVSVZXNOoD/KXpJue9aeLRd0sM9uMXfZvzomB0WxVamWb5ue8kA2vVWEmW7EG+A5n3f1kqHKg==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0" + } + }, + "jest": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-24.9.0.tgz", + "integrity": "sha512-YvkBL1Zm7d2B1+h5fHEOdyjCG+sGMz4f8D86/0HiqJ6MB4MnDc8FgP5vdWsGnemOQro7lnYo8UakZ3+5A0jxGw==", + "dev": true, + "requires": { + "import-local": "^2.0.0", + "jest-cli": "^24.9.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "jest-cli": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-24.9.0.tgz", + "integrity": "sha512-+VLRKyitT3BWoMeSUIHRxV/2g8y9gw91Jh5z2UmXZzkZKpbC08CSehVxgHUwTpy+HwGcns/tqafQDJW7imYvGg==", + "dev": true, + "requires": { + "@jest/core": "^24.9.0", + "@jest/test-result": "^24.9.0", + "@jest/types": "^24.9.0", + "chalk": "^2.0.1", + "exit": "^0.1.2", + "import-local": "^2.0.0", + "is-ci": "^2.0.0", + "jest-config": "^24.9.0", + "jest-util": "^24.9.0", + "jest-validate": "^24.9.0", + "prompts": "^2.0.1", + "realpath-native": "^1.1.0", + "yargs": "^13.3.0" + } + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + } + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + }, + "yargs": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.0.tgz", + "integrity": "sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA==", + "dev": true, + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.1" + } + }, + "yargs-parser": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz", + "integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "jest-changed-files": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-24.9.0.tgz", + "integrity": "sha512-6aTWpe2mHF0DhL28WjdkO8LyGjs3zItPET4bMSeXU6T3ub4FPMw+mcOcbdGXQOAfmLcxofD23/5Bl9Z4AkFwqg==", + "dev": true, + "requires": { + "@jest/types": "^24.9.0", + "execa": "^1.0.0", + "throat": "^4.0.0" + } + }, + "jest-config": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-24.9.0.tgz", + "integrity": "sha512-RATtQJtVYQrp7fvWg6f5y3pEFj9I+H8sWw4aKxnDZ96mob5i5SD6ZEGWgMLXQ4LE8UurrjbdlLWdUeo+28QpfQ==", + "dev": true, + "requires": { + "@babel/core": "^7.1.0", + "@jest/test-sequencer": "^24.9.0", + "@jest/types": "^24.9.0", + "babel-jest": "^24.9.0", + "chalk": "^2.0.1", + "glob": "^7.1.1", + "jest-environment-jsdom": "^24.9.0", + "jest-environment-node": "^24.9.0", + "jest-get-type": "^24.9.0", + "jest-jasmine2": "^24.9.0", + "jest-regex-util": "^24.3.0", + "jest-resolve": "^24.9.0", + "jest-util": "^24.9.0", + "jest-validate": "^24.9.0", + "micromatch": "^3.1.10", + "pretty-format": "^24.9.0", + "realpath-native": "^1.1.0" + } + }, + "jest-diff": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-24.9.0.tgz", + "integrity": "sha512-qMfrTs8AdJE2iqrTp0hzh7kTd2PQWrsFyj9tORoKmu32xjPjeE4NyjVRDz8ybYwqS2ik8N4hsIpiVTyFeo2lBQ==", + "dev": true, + "requires": { + "chalk": "^2.0.1", + "diff-sequences": "^24.9.0", + "jest-get-type": "^24.9.0", + "pretty-format": "^24.9.0" + } + }, + "jest-docblock": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-24.9.0.tgz", + "integrity": "sha512-F1DjdpDMJMA1cN6He0FNYNZlo3yYmOtRUnktrT9Q37njYzC5WEaDdmbynIgy0L/IvXvvgsG8OsqhLPXTpfmZAA==", + "dev": true, + "requires": { + "detect-newline": "^2.1.0" + } + }, + "jest-each": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-24.9.0.tgz", + "integrity": "sha512-ONi0R4BvW45cw8s2Lrx8YgbeXL1oCQ/wIDwmsM3CqM/nlblNCPmnC3IPQlMbRFZu3wKdQ2U8BqM6lh3LJ5Bsog==", + "dev": true, + "requires": { + "@jest/types": "^24.9.0", + "chalk": "^2.0.1", + "jest-get-type": "^24.9.0", + "jest-util": "^24.9.0", + "pretty-format": "^24.9.0" + } + }, + "jest-environment-jsdom": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-24.9.0.tgz", + "integrity": "sha512-Zv9FV9NBRzLuALXjvRijO2351DRQeLYXtpD4xNvfoVFw21IOKNhZAEUKcbiEtjTkm2GsJ3boMVgkaR7rN8qetA==", + "dev": true, + "requires": { + "@jest/environment": "^24.9.0", + "@jest/fake-timers": "^24.9.0", + "@jest/types": "^24.9.0", + "jest-mock": "^24.9.0", + "jest-util": "^24.9.0", + "jsdom": "^11.5.1" + } + }, + "jest-environment-node": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-24.9.0.tgz", + "integrity": "sha512-6d4V2f4nxzIzwendo27Tr0aFm+IXWa0XEUnaH6nU0FMaozxovt+sfRvh4J47wL1OvF83I3SSTu0XK+i4Bqe7uA==", + "dev": true, + "requires": { + "@jest/environment": "^24.9.0", + "@jest/fake-timers": "^24.9.0", + "@jest/types": "^24.9.0", + "jest-mock": "^24.9.0", + "jest-util": "^24.9.0" + } + }, + "jest-get-type": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-24.9.0.tgz", + "integrity": "sha512-lUseMzAley4LhIcpSP9Jf+fTrQ4a1yHQwLNeeVa2cEmbCGeoZAtYPOIv8JaxLD/sUpKxetKGP+gsHl8f8TSj8Q==", + "dev": true + }, + "jest-haste-map": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-24.9.0.tgz", + "integrity": "sha512-kfVFmsuWui2Sj1Rp1AJ4D9HqJwE4uwTlS/vO+eRUaMmd54BFpli2XhMQnPC2k4cHFVbB2Q2C+jtI1AGLgEnCjQ==", + "dev": true, + "requires": { + "@jest/types": "^24.9.0", + "anymatch": "^2.0.0", + "fb-watchman": "^2.0.0", + "fsevents": "^1.2.7", + "graceful-fs": "^4.1.15", + "invariant": "^2.2.4", + "jest-serializer": "^24.9.0", + "jest-util": "^24.9.0", + "jest-worker": "^24.9.0", + "micromatch": "^3.1.10", + "sane": "^4.0.3", + "walker": "^1.0.7" + } + }, + "jest-jasmine2": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-24.9.0.tgz", + "integrity": "sha512-Cq7vkAgaYKp+PsX+2/JbTarrk0DmNhsEtqBXNwUHkdlbrTBLtMJINADf2mf5FkowNsq8evbPc07/qFO0AdKTzw==", + "dev": true, + "requires": { + "@babel/traverse": "^7.1.0", + "@jest/environment": "^24.9.0", + "@jest/test-result": "^24.9.0", + "@jest/types": "^24.9.0", + "chalk": "^2.0.1", + "co": "^4.6.0", + "expect": "^24.9.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^24.9.0", + "jest-matcher-utils": "^24.9.0", + "jest-message-util": "^24.9.0", + "jest-runtime": "^24.9.0", + "jest-snapshot": "^24.9.0", + "jest-util": "^24.9.0", + "pretty-format": "^24.9.0", + "throat": "^4.0.0" + } + }, + "jest-leak-detector": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-24.9.0.tgz", + "integrity": "sha512-tYkFIDsiKTGwb2FG1w8hX9V0aUb2ot8zY/2nFg087dUageonw1zrLMP4W6zsRO59dPkTSKie+D4rhMuP9nRmrA==", + "dev": true, + "requires": { + "jest-get-type": "^24.9.0", + "pretty-format": "^24.9.0" + } + }, + "jest-matcher-utils": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-24.9.0.tgz", + "integrity": "sha512-OZz2IXsu6eaiMAwe67c1T+5tUAtQyQx27/EMEkbFAGiw52tB9em+uGbzpcgYVpA8wl0hlxKPZxrly4CXU/GjHA==", + "dev": true, + "requires": { + "chalk": "^2.0.1", + "jest-diff": "^24.9.0", + "jest-get-type": "^24.9.0", + "pretty-format": "^24.9.0" + } + }, + "jest-message-util": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-24.9.0.tgz", + "integrity": "sha512-oCj8FiZ3U0hTP4aSui87P4L4jC37BtQwUMqk+zk/b11FR19BJDeZsZAvIHutWnmtw7r85UmR3CEWZ0HWU2mAlw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@jest/test-result": "^24.9.0", + "@jest/types": "^24.9.0", + "@types/stack-utils": "^1.0.1", + "chalk": "^2.0.1", + "micromatch": "^3.1.10", + "slash": "^2.0.0", + "stack-utils": "^1.0.1" + }, + "dependencies": { + "slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true + } + } + }, + "jest-mock": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-24.9.0.tgz", + "integrity": "sha512-3BEYN5WbSq9wd+SyLDES7AHnjH9A/ROBwmz7l2y+ol+NtSFO8DYiEBzoO1CeFc9a8DYy10EO4dDFVv/wN3zl1w==", + "dev": true, + "requires": { + "@jest/types": "^24.9.0" + } + }, + "jest-pnp-resolver": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.1.tgz", + "integrity": "sha512-pgFw2tm54fzgYvc/OHrnysABEObZCUNFnhjoRjaVOCN8NYc032/gVjPaHD4Aq6ApkSieWtfKAFQtmDKAmhupnQ==", + "dev": true + }, + "jest-regex-util": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-24.9.0.tgz", + "integrity": "sha512-05Cmb6CuxaA+Ys6fjr3PhvV3bGQmO+2p2La4hFbU+W5uOc479f7FdLXUWXw4pYMAhhSZIuKHwSXSu6CsSBAXQA==", + "dev": true + }, + "jest-resolve": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-24.9.0.tgz", + "integrity": "sha512-TaLeLVL1l08YFZAt3zaPtjiVvyy4oSA6CRe+0AFPPVX3Q/VI0giIWWoAvoS5L96vj9Dqxj4fB5p2qrHCmTU/MQ==", + "dev": true, + "requires": { + "@jest/types": "^24.9.0", + "browser-resolve": "^1.11.3", + "chalk": "^2.0.1", + "jest-pnp-resolver": "^1.2.1", + "realpath-native": "^1.1.0" + } + }, + "jest-resolve-dependencies": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-24.9.0.tgz", + "integrity": "sha512-Fm7b6AlWnYhT0BXy4hXpactHIqER7erNgIsIozDXWl5dVm+k8XdGVe1oTg1JyaFnOxarMEbax3wyRJqGP2Pq+g==", + "dev": true, + "requires": { + "@jest/types": "^24.9.0", + "jest-regex-util": "^24.3.0", + "jest-snapshot": "^24.9.0" + } + }, + "jest-runner": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-24.9.0.tgz", + "integrity": "sha512-KksJQyI3/0mhcfspnxxEOBueGrd5E4vV7ADQLT9ESaCzz02WnbdbKWIf5Mkaucoaj7obQckYPVX6JJhgUcoWWg==", + "dev": true, + "requires": { + "@jest/console": "^24.7.1", + "@jest/environment": "^24.9.0", + "@jest/test-result": "^24.9.0", + "@jest/types": "^24.9.0", + "chalk": "^2.4.2", + "exit": "^0.1.2", + "graceful-fs": "^4.1.15", + "jest-config": "^24.9.0", + "jest-docblock": "^24.3.0", + "jest-haste-map": "^24.9.0", + "jest-jasmine2": "^24.9.0", + "jest-leak-detector": "^24.9.0", + "jest-message-util": "^24.9.0", + "jest-resolve": "^24.9.0", + "jest-runtime": "^24.9.0", + "jest-util": "^24.9.0", + "jest-worker": "^24.6.0", + "source-map-support": "^0.5.6", + "throat": "^4.0.0" + } + }, + "jest-runtime": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-24.9.0.tgz", + "integrity": "sha512-8oNqgnmF3v2J6PVRM2Jfuj8oX3syKmaynlDMMKQ4iyzbQzIG6th5ub/lM2bCMTmoTKM3ykcUYI2Pw9xwNtjMnw==", + "dev": true, + "requires": { + "@jest/console": "^24.7.1", + "@jest/environment": "^24.9.0", + "@jest/source-map": "^24.3.0", + "@jest/transform": "^24.9.0", + "@jest/types": "^24.9.0", + "@types/yargs": "^13.0.0", + "chalk": "^2.0.1", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.1.15", + "jest-config": "^24.9.0", + "jest-haste-map": "^24.9.0", + "jest-message-util": "^24.9.0", + "jest-mock": "^24.9.0", + "jest-regex-util": "^24.3.0", + "jest-resolve": "^24.9.0", + "jest-snapshot": "^24.9.0", + "jest-util": "^24.9.0", + "jest-validate": "^24.9.0", + "realpath-native": "^1.1.0", + "slash": "^2.0.0", + "strip-bom": "^3.0.0", + "yargs": "^13.3.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + } + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + }, + "yargs": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.0.tgz", + "integrity": "sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA==", + "dev": true, + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.1" + } + }, + "yargs-parser": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz", + "integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "jest-serializer": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-24.9.0.tgz", + "integrity": "sha512-DxYipDr8OvfrKH3Kel6NdED3OXxjvxXZ1uIY2I9OFbGg+vUkkg7AGvi65qbhbWNPvDckXmzMPbK3u3HaDO49bQ==", + "dev": true + }, + "jest-snapshot": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-24.9.0.tgz", + "integrity": "sha512-uI/rszGSs73xCM0l+up7O7a40o90cnrk429LOiK3aeTvfC0HHmldbd81/B7Ix81KSFe1lwkbl7GnBGG4UfuDew==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0", + "@jest/types": "^24.9.0", + "chalk": "^2.0.1", + "expect": "^24.9.0", + "jest-diff": "^24.9.0", + "jest-get-type": "^24.9.0", + "jest-matcher-utils": "^24.9.0", + "jest-message-util": "^24.9.0", + "jest-resolve": "^24.9.0", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "pretty-format": "^24.9.0", + "semver": "^6.2.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "jest-util": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-24.9.0.tgz", + "integrity": "sha512-x+cZU8VRmOJxbA1K5oDBdxQmdq0OIdADarLxk0Mq+3XS4jgvhG/oKGWcIDCtPG0HgjxOYvF+ilPJQsAyXfbNOg==", + "dev": true, + "requires": { + "@jest/console": "^24.9.0", + "@jest/fake-timers": "^24.9.0", + "@jest/source-map": "^24.9.0", + "@jest/test-result": "^24.9.0", + "@jest/types": "^24.9.0", + "callsites": "^3.0.0", + "chalk": "^2.0.1", + "graceful-fs": "^4.1.15", + "is-ci": "^2.0.0", + "mkdirp": "^0.5.1", + "slash": "^2.0.0", + "source-map": "^0.6.0" + }, + "dependencies": { + "slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "jest-validate": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-24.9.0.tgz", + "integrity": "sha512-HPIt6C5ACwiqSiwi+OfSSHbK8sG7akG8eATl+IPKaeIjtPOeBUd/g3J7DghugzxrGjI93qS/+RPKe1H6PqvhRQ==", + "dev": true, + "requires": { + "@jest/types": "^24.9.0", + "camelcase": "^5.3.1", + "chalk": "^2.0.1", + "jest-get-type": "^24.9.0", + "leven": "^3.1.0", + "pretty-format": "^24.9.0" + }, + "dependencies": { + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + } + } + }, + "jest-watcher": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-24.9.0.tgz", + "integrity": "sha512-+/fLOfKPXXYJDYlks62/4R4GoT+GU1tYZed99JSCOsmzkkF7727RqKrjNAxtfO4YpGv11wybgRvCjR73lK2GZw==", + "dev": true, + "requires": { + "@jest/test-result": "^24.9.0", + "@jest/types": "^24.9.0", + "@types/yargs": "^13.0.0", + "ansi-escapes": "^3.0.0", + "chalk": "^2.0.1", + "jest-util": "^24.9.0", + "string-length": "^2.0.0" + } + }, + "jest-worker": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-24.9.0.tgz", + "integrity": "sha512-51PE4haMSXcHohnSMdM42anbvZANYTqMrr52tVKPqqsPJMzoP6FYYDVqahX/HrAoKEKz3uUPzSvKs9A3qR4iVw==", + "dev": true, + "requires": { + "merge-stream": "^2.0.0", + "supports-color": "^6.1.0" + }, + "dependencies": { + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "js-base64": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.2.tgz", + "integrity": "sha512-Vg8czh0Q7sFBSUMWWArX/miJeBWYBPpdU/3M/DKSaekLMqrqVPaedp+5mZhie/r0lgrcaYBfwXatEew6gwgiQQ==", + "dev": true + }, + "js-cookie": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz", + "integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==" + }, + "js-levenshtein": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", + "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==", + "dev": true + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "dev": true + }, + "jsdom": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-11.12.0.tgz", + "integrity": "sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw==", + "dev": true, + "requires": { + "abab": "^2.0.0", + "acorn": "^5.5.3", + "acorn-globals": "^4.1.0", + "array-equal": "^1.0.0", + "cssom": ">= 0.3.2 < 0.4.0", + "cssstyle": "^1.0.0", + "data-urls": "^1.0.0", + "domexception": "^1.0.1", + "escodegen": "^1.9.1", + "html-encoding-sniffer": "^1.0.2", + "left-pad": "^1.3.0", + "nwsapi": "^2.0.7", + "parse5": "4.0.0", + "pn": "^1.1.0", + "request": "^2.87.0", + "request-promise-native": "^1.0.5", + "sax": "^1.2.4", + "symbol-tree": "^3.2.2", + "tough-cookie": "^2.3.4", + "w3c-hr-time": "^1.0.1", + "webidl-conversions": "^4.0.2", + "whatwg-encoding": "^1.0.3", + "whatwg-mimetype": "^2.1.0", + "whatwg-url": "^6.4.1", + "ws": "^5.2.0", + "xml-name-validator": "^3.0.0" + }, + "dependencies": { + "acorn": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", + "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==", + "dev": true + } + } + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, + "json5": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.1.tgz", + "integrity": "sha512-l+3HXD0GEI3huGq1njuqtzYK8OYJyXMkOLtQ53pjWh89tvWS2h6l+1zMkYWqlb57+SiQodKZyvMEFb2X+KrFhQ==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + }, + "kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true + }, + "lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "dev": true, + "requires": { + "invert-kv": "^1.0.0" + } + }, + "left-pad": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz", + "integrity": "sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA==", + "dev": true + }, + "leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + }, + "loader-runner": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz", + "integrity": "sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==", + "dev": true + }, + "loader-utils": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", + "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + }, + "dependencies": { + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + } + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + }, + "lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=", + "dev": true + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=", + "dev": true + }, + "lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", + "dev": true + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "loud-rejection": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", + "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", + "dev": true, + "requires": { + "currently-unhandled": "^0.4.1", + "signal-exit": "^3.0.0" + } + }, + "lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "makeerror": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz", + "integrity": "sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw=", + "dev": true, + "requires": { + "tmpl": "1.0.x" + } + }, + "map-age-cleaner": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", + "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", + "dev": true, + "requires": { + "p-defer": "^1.0.0" + } + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", + "dev": true + }, + "map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", + "dev": true + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "dev": true, + "requires": { + "object-visit": "^1.0.0" + } + }, + "md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "dev": true, + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "mem": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", + "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", + "dev": true, + "requires": { + "map-age-cleaner": "^0.1.1", + "mimic-fn": "^2.0.0", + "p-is-promise": "^2.0.0" + } + }, + "memory-fs": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", + "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", + "dev": true, + "requires": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + } + }, + "meow": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", + "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", + "dev": true, + "requires": { + "camelcase-keys": "^2.0.0", + "decamelize": "^1.1.2", + "loud-rejection": "^1.0.0", + "map-obj": "^1.0.1", + "minimist": "^1.1.3", + "normalize-package-data": "^2.3.4", + "object-assign": "^4.0.1", + "read-pkg-up": "^1.0.1", + "redent": "^1.0.0", + "trim-newlines": "^1.0.0" + } + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "dev": true, + "requires": { + "bn.js": "^4.0.0", + "brorand": "^1.0.1" + } + }, + "mime-db": { + "version": "1.42.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.42.0.tgz", + "integrity": "sha512-UbfJCR4UAVRNgMpfImz05smAXK7+c+ZntjaA26ANtkXLlOe947Aag5zdIcKQULAiF9Cq4WxBi9jUs5zkA84bYQ==", + "dev": true + }, + "mime-types": { + "version": "2.1.25", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.25.tgz", + "integrity": "sha512-5KhStqB5xpTAeGqKBAMgwaYMnQik7teQN4IAzC7npDv6kzeU6prfkR67bc87J1kWMPGkoaZSq1npmexMgkmEVg==", + "dev": true, + "requires": { + "mime-db": "1.42.0" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "mini-css-extract-plugin": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.9.0.tgz", + "integrity": "sha512-lp3GeY7ygcgAmVIcRPBVhIkf8Us7FZjA+ILpal44qLdSu11wmjKQ3d9k15lfD7pO4esu9eUIAW7qiYIBppv40A==", + "dev": true, + "requires": { + "loader-utils": "^1.1.0", + "normalize-url": "1.9.1", + "schema-utils": "^1.0.0", + "webpack-sources": "^1.1.0" + }, + "dependencies": { + "schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + } + } + } + }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, + "mississippi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", + "integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==", + "dev": true, + "requires": { + "concat-stream": "^1.5.0", + "duplexify": "^3.4.2", + "end-of-stream": "^1.1.0", + "flush-write-stream": "^1.0.0", + "from2": "^2.1.0", + "parallel-transform": "^1.1.0", + "pump": "^3.0.0", + "pumpify": "^1.3.3", + "stream-each": "^1.1.0", + "through2": "^2.0.0" + }, + "dependencies": { + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } + } + }, + "mixin-deep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "dev": true, + "requires": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "mkdirp": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.3.tgz", + "integrity": "sha512-P+2gwrFqx8lhew375MQHHeTlY8AuOJSrGf0R5ddkEndUkmwpgUob/vQuBD1V22/Cw1/lJr4x+EjllSezBThzBg==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "move-concurrently": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", + "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=", + "dev": true, + "requires": { + "aproba": "^1.1.1", + "copy-concurrently": "^1.0.0", + "fs-write-stream-atomic": "^1.0.8", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.3" + }, + "dependencies": { + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "nan": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", + "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", + "dev": true + }, + "nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + } + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "neo-async": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", + "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", + "dev": true + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "node-fetch": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", + "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==", + "dev": true + }, + "node-gyp": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz", + "integrity": "sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==", + "dev": true, + "requires": { + "fstream": "^1.0.0", + "glob": "^7.0.3", + "graceful-fs": "^4.1.2", + "mkdirp": "^0.5.0", + "nopt": "2 || 3", + "npmlog": "0 || 1 || 2 || 3 || 4", + "osenv": "0", + "request": "^2.87.0", + "rimraf": "2", + "semver": "~5.3.0", + "tar": "^2.0.0", + "which": "1" + }, + "dependencies": { + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "semver": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", + "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", + "dev": true + } + } + }, + "node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=", + "dev": true + }, + "node-libs-browser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz", + "integrity": "sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==", + "dev": true, + "requires": { + "assert": "^1.1.1", + "browserify-zlib": "^0.2.0", + "buffer": "^4.3.0", + "console-browserify": "^1.1.0", + "constants-browserify": "^1.0.0", + "crypto-browserify": "^3.11.0", + "domain-browser": "^1.1.1", + "events": "^3.0.0", + "https-browserify": "^1.0.0", + "os-browserify": "^0.3.0", + "path-browserify": "0.0.1", + "process": "^0.11.10", + "punycode": "^1.2.4", + "querystring-es3": "^0.2.0", + "readable-stream": "^2.3.3", + "stream-browserify": "^2.0.1", + "stream-http": "^2.7.2", + "string_decoder": "^1.0.0", + "timers-browserify": "^2.0.4", + "tty-browserify": "0.0.0", + "url": "^0.11.0", + "util": "^0.11.0", + "vm-browserify": "^1.0.1" + }, + "dependencies": { + "buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "dev": true, + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "events": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.1.0.tgz", + "integrity": "sha512-Rv+u8MLHNOdMjTAFeT3nCjHn2aGlx435FP/sDHNaRhDEMwyI/aB22Kj2qIN8R0cw3z28psEQLYwxVKLsKrMgWg==", + "dev": true + }, + "stream-http": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz", + "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==", + "dev": true, + "requires": { + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.3.6", + "to-arraybuffer": "^1.0.0", + "xtend": "^4.0.0" + } + }, + "timers-browserify": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.11.tgz", + "integrity": "sha512-60aV6sgJ5YEbzUdn9c8kYGIqOubPoUdqQCul3SBAsRCZ40s6Y5cMcrW4dt3/k/EsbLVJNl9n6Vz3fTc+k2GeKQ==", + "dev": true, + "requires": { + "setimmediate": "^1.0.4" + } + }, + "tty-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", + "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=", + "dev": true + }, + "util": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", + "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", + "dev": true, + "requires": { + "inherits": "2.0.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + } + } + } + } + }, + "node-modules-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz", + "integrity": "sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=", + "dev": true + }, + "node-notifier": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-5.4.3.tgz", + "integrity": "sha512-M4UBGcs4jeOK9CjTsYwkvH6/MzuUmGCyTW+kCY7uO+1ZVr0+FHGdPdIf5CCLqAaxnRrWidyoQlNkMIIVwbKB8Q==", + "dev": true, + "requires": { + "growly": "^1.3.0", + "is-wsl": "^1.1.0", + "semver": "^5.5.0", + "shellwords": "^0.1.1", + "which": "^1.3.0" + } + }, + "node-releases": { + "version": "1.1.44", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.44.tgz", + "integrity": "sha512-NwbdvJyR7nrcGrXvKAvzc5raj/NkoJudkarh2yIpJ4t0NH4aqjUDz/486P+ynIW5eokKOfzGNRdYoLfBlomruw==", + "dev": true, + "requires": { + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "node-sass": { + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.13.1.tgz", + "integrity": "sha512-TTWFx+ZhyDx1Biiez2nB0L3YrCZ/8oHagaDalbuBSlqXgUPsdkUSzJsVxeDO9LtPB49+Fh3WQl3slABo6AotNw==", + "dev": true, + "requires": { + "async-foreach": "^0.1.3", + "chalk": "^1.1.1", + "cross-spawn": "^3.0.0", + "gaze": "^1.0.0", + "get-stdin": "^4.0.1", + "glob": "^7.0.3", + "in-publish": "^2.0.0", + "lodash": "^4.17.15", + "meow": "^3.7.0", + "mkdirp": "^0.5.1", + "nan": "^2.13.2", + "node-gyp": "^3.8.0", + "npmlog": "^4.0.0", + "request": "^2.88.0", + "sass-graph": "^2.2.4", + "stdout-stream": "^1.4.0", + "true-case-path": "^1.0.2" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, + "nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "dev": true, + "requires": { + "abbrev": "1" + } + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + }, + "normalize-url": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-1.9.1.tgz", + "integrity": "sha1-LMDWazHqIwNkWENuNiDYWVTGbDw=", + "dev": true, + "requires": { + "object-assign": "^4.0.1", + "prepend-http": "^1.0.0", + "query-string": "^4.1.0", + "sort-keys": "^1.0.0" + } + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "dev": true, + "requires": { + "path-key": "^2.0.0" + } + }, + "npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "dev": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "nwsapi": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", + "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==", + "dev": true + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "dev": true, + "requires": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "object-inspect": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", + "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==", + "dev": true + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "dev": true, + "requires": { + "isobject": "^3.0.0" + } + }, + "object.assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", + "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.0", + "object-keys": "^1.0.11" + } + }, + "object.getownpropertydescriptors": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz", + "integrity": "sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1" + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "os-browserify": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", + "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", + "dev": true + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true + }, + "os-locale": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", + "dev": true, + "requires": { + "lcid": "^1.0.0" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, + "osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "dev": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "p-defer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", + "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", + "dev": true + }, + "p-each-series": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-1.0.0.tgz", + "integrity": "sha1-kw89Et0fUOdDRFeiLNbwSsatf3E=", + "dev": true, + "requires": { + "p-reduce": "^1.0.0" + } + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "dev": true + }, + "p-is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", + "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", + "dev": true + }, + "p-limit": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.1.tgz", + "integrity": "sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-reduce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-reduce/-/p-reduce-1.0.0.tgz", + "integrity": "sha1-GMKw3ZNqRpClKfgjH1ig/bakffo=", + "dev": true + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "pako": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.10.tgz", + "integrity": "sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw==", + "dev": true + }, + "parallel-transform": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.2.0.tgz", + "integrity": "sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==", + "dev": true, + "requires": { + "cyclist": "^1.0.1", + "inherits": "^2.0.3", + "readable-stream": "^2.1.5" + } + }, + "parse-asn1": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.5.tgz", + "integrity": "sha512-jkMYn1dcJqF6d5CpU689bq7w/b5ALS9ROVSpQDPrZsqqesUJii9qutvoT5ltGedNXMO2e16YUWIghG9KxaViTQ==", + "dev": true, + "requires": { + "asn1.js": "^4.0.0", + "browserify-aes": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.0", + "pbkdf2": "^3.0.3", + "safe-buffer": "^5.1.1" + } + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true, + "requires": { + "error-ex": "^1.2.0" + } + }, + "parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=", + "dev": true + }, + "parse5": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz", + "integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==", + "dev": true + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", + "dev": true + }, + "path-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", + "integrity": "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==", + "dev": true + }, + "path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", + "dev": true + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true + }, + "path-to-regexp": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-2.4.0.tgz", + "integrity": "sha512-G6zHoVqC6GGTQkZwF4lkuEyMbVOjoBKAEybQUypI1WTkqinCOrq2x6U2+phkJ1XsEMTy4LjtwPI7HW+NVrRR2w==", + "dev": true + }, + "path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + }, + "pbkdf2": { + "version": "3.0.17", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz", + "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==", + "dev": true, + "requires": { + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4", + "ripemd160": "^2.0.1", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "dev": true + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "requires": { + "pinkie": "^2.0.0" + } + }, + "pirates": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.1.tgz", + "integrity": "sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA==", + "dev": true, + "requires": { + "node-modules-regexp": "^1.0.0" + } + }, + "pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "dev": true, + "requires": { + "find-up": "^3.0.0" + } + }, + "pn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", + "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==", + "dev": true + }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", + "dev": true + }, + "postcss": { + "version": "7.0.27", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.27.tgz", + "integrity": "sha512-WuQETPMcW9Uf1/22HWUWP9lgsIC+KEHg2kozMflKjbeUtw9ujvFX6QmIfozaErDkmLWS9WEnEdEe6Uo9/BNTdQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-modules-extract-imports": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz", + "integrity": "sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ==", + "dev": true, + "requires": { + "postcss": "^7.0.5" + } + }, + "postcss-modules-local-by-default": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.2.tgz", + "integrity": "sha512-jM/V8eqM4oJ/22j0gx4jrp63GSvDH6v86OqyTHHUvk4/k1vceipZsaymiZ5PvocqZOl5SFHiFJqjs3la0wnfIQ==", + "dev": true, + "requires": { + "icss-utils": "^4.1.1", + "postcss": "^7.0.16", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.0.0" + } + }, + "postcss-modules-scope": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-2.2.0.tgz", + "integrity": "sha512-YyEgsTMRpNd+HmyC7H/mh3y+MeFWevy7V1evVhJWewmMbjDHIbZbOXICC2y+m1xI1UVfIT1HMW/O04Hxyu9oXQ==", + "dev": true, + "requires": { + "postcss": "^7.0.6", + "postcss-selector-parser": "^6.0.0" + } + }, + "postcss-modules-values": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz", + "integrity": "sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg==", + "dev": true, + "requires": { + "icss-utils": "^4.0.0", + "postcss": "^7.0.6" + } + }, + "postcss-selector-parser": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz", + "integrity": "sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg==", + "dev": true, + "requires": { + "cssesc": "^3.0.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + }, + "postcss-value-parser": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.0.3.tgz", + "integrity": "sha512-N7h4pG+Nnu5BEIzyeaaIYWs0LI5XC40OrRh5L60z0QjFsqGWcHcbkBvpe1WYpcIS9yQ8sOi/vIPt1ejQCrMVrg==", + "dev": true + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "prepend-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", + "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", + "dev": true + }, + "prettier": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz", + "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==", + "dev": true + }, + "pretty-format": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-24.9.0.tgz", + "integrity": "sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA==", + "dev": true, + "requires": { + "@jest/types": "^24.9.0", + "ansi-regex": "^4.0.0", + "ansi-styles": "^3.2.0", + "react-is": "^16.8.4" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + } + } + }, + "private": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", + "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==", + "dev": true + }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", + "dev": true + }, + "prompts": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.3.0.tgz", + "integrity": "sha512-NfbbPPg/74fT7wk2XYQ7hAIp9zJyZp5Fu19iRbORqqy1BhtrkZ0fPafBU+7bmn8ie69DpT0R6QpJIN2oisYjJg==", + "dev": true, + "requires": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.3" + } + }, + "prop-types": { + "version": "15.7.2", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", + "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.8.1" + } + }, + "prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", + "dev": true + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", + "dev": true + }, + "psl": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.7.0.tgz", + "integrity": "sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ==", + "dev": true + }, + "public-encrypt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", + "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "parse-asn1": "^5.0.0", + "randombytes": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "pumpify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "dev": true, + "requires": { + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" + } + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "dev": true + }, + "query-string": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz", + "integrity": "sha1-u7aTucqRXCMlFbIosaArYJBD2+s=", + "dev": true, + "requires": { + "object-assign": "^4.1.0", + "strict-uri-encode": "^1.0.0" + } + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "dev": true + }, + "querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", + "dev": true + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "randomfill": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", + "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "dev": true, + "requires": { + "randombytes": "^2.0.5", + "safe-buffer": "^5.1.0" + } + }, + "react": { + "version": "16.12.0", + "resolved": "https://registry.npmjs.org/react/-/react-16.12.0.tgz", + "integrity": "sha512-fglqy3k5E+81pA8s+7K0/T3DBCF0ZDOher1elBFzF7O6arXJgzyu/FW+COxFvAWXJoJN9KIZbT2LXlukwphYTA==", + "dev": true, + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2" + } + }, + "react-dom": { + "version": "16.12.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.12.0.tgz", + "integrity": "sha512-LMxFfAGrcS3kETtQaCkTKjMiifahaMySFDn71fZUNpPHZQEzmk/GiAeIT8JSOrHB23fnuCOMruL2a8NYlw+8Gw==", + "dev": true, + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "scheduler": "^0.18.0" + } + }, + "react-is": { + "version": "16.12.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.12.0.tgz", + "integrity": "sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q==" + }, + "react-redux": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.1.3.tgz", + "integrity": "sha512-uI1wca+ECG9RoVkWQFF4jDMqmaw0/qnvaSvOoL/GA4dNxf6LoV8sUAcNDvE5NWKs4hFpn0t6wswNQnY3f7HT3w==", + "requires": { + "@babel/runtime": "^7.5.5", + "hoist-non-react-statics": "^3.3.0", + "invariant": "^2.2.4", + "loose-envify": "^1.4.0", + "prop-types": "^15.7.2", + "react-is": "^16.9.0" + } + }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "dev": true, + "requires": { + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "dev": true, + "requires": { + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" + }, + "dependencies": { + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "dev": true, + "requires": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "dev": true, + "requires": { + "pinkie-promise": "^2.0.0" + } + } + } + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + } + }, + "realpath-native": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/realpath-native/-/realpath-native-1.1.0.tgz", + "integrity": "sha512-wlgPA6cCIIg9gKz0fgAPjnzh4yR/LnXovwuo9hvyGvx3h8nX4+/iLZplfUWasXpqD8BdnGnP5njOFjkUwPzvjA==", + "dev": true, + "requires": { + "util.promisify": "^1.0.0" + } + }, + "redent": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", + "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", + "dev": true, + "requires": { + "indent-string": "^2.1.0", + "strip-indent": "^1.0.1" + }, + "dependencies": { + "indent-string": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", + "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", + "dev": true, + "requires": { + "repeating": "^2.0.0" + } + } + } + }, + "redux": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.0.5.tgz", + "integrity": "sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==", + "requires": { + "loose-envify": "^1.4.0", + "symbol-observable": "^1.2.0" + } + }, + "redux-logger": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/redux-logger/-/redux-logger-3.0.6.tgz", + "integrity": "sha1-91VZZvMJjzyIYExEnPC69XeCdL8=", + "requires": { + "deep-diff": "^0.3.5" + } + }, + "redux-mock-store": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/redux-mock-store/-/redux-mock-store-1.5.4.tgz", + "integrity": "sha512-xmcA0O/tjCLXhh9Fuiq6pMrJCwFRaouA8436zcikdIpYWWCjU76CRk+i2bHx8EeiSiMGnB85/lZdU3wIJVXHTA==", + "dev": true, + "requires": { + "lodash.isplainobject": "^4.0.6" + } + }, + "redux-thunk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz", + "integrity": "sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw==" + }, + "regenerate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", + "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==", + "dev": true + }, + "regenerate-unicode-properties": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-8.1.0.tgz", + "integrity": "sha512-LGZzkgtLY79GeXLm8Dp0BVLdQlWICzBnJz/ipWUgo59qBaZ+BHtq51P2q1uVZlppMuUAT37SDk39qUbjTWB7bA==", + "dev": true, + "requires": { + "regenerate": "^1.4.0" + } + }, + "regenerator-runtime": { + "version": "0.13.3", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz", + "integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw==" + }, + "regenerator-transform": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.1.tgz", + "integrity": "sha512-flVuee02C3FKRISbxhXl9mGzdbWUVHubl1SMaknjxkFB1/iqpJhArQUvRxOOPEc/9tAiX0BaQ28FJH10E4isSQ==", + "dev": true, + "requires": { + "private": "^0.1.6" + } + }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + } + }, + "regexpu-core": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.6.0.tgz", + "integrity": "sha512-YlVaefl8P5BnFYOITTNzDvan1ulLOiXJzCNZxduTIosN17b87h3bvG9yHMoHaRuo88H4mQ06Aodj5VtYGGGiTg==", + "dev": true, + "requires": { + "regenerate": "^1.4.0", + "regenerate-unicode-properties": "^8.1.0", + "regjsgen": "^0.5.0", + "regjsparser": "^0.6.0", + "unicode-match-property-ecmascript": "^1.0.4", + "unicode-match-property-value-ecmascript": "^1.1.0" + } + }, + "regjsgen": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.1.tgz", + "integrity": "sha512-5qxzGZjDs9w4tzT3TPhCJqWdCc3RLYwy9J2NB0nm5Lz+S273lvWcpjaTGHsT1dc6Hhfq41uSEOw8wBmxrKOuyg==", + "dev": true + }, + "regjsparser": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.6.2.tgz", + "integrity": "sha512-E9ghzUtoLwDekPT0DYCp+c4h+bvuUpe6rRHCTYn6eGoqj1LgKXxT6I0Il4WbjhQkOghzi/V+y03bPKvbllL93Q==", + "dev": true, + "requires": { + "jsesc": "~0.5.0" + }, + "dependencies": { + "jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", + "dev": true + } + } + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", + "dev": true + }, + "repeat-element": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", + "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true + }, + "repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "dev": true, + "requires": { + "is-finite": "^1.0.0" + } + }, + "request": { + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "dev": true, + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + } + }, + "request-promise-core": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.3.tgz", + "integrity": "sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ==", + "dev": true, + "requires": { + "lodash": "^4.17.15" + } + }, + "request-promise-native": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.8.tgz", + "integrity": "sha512-dapwLGqkHtwL5AEbfenuzjTYg35Jd6KPytsC2/TLkVMz8rm+tNt72MGUWT1RP/aYawMpN6HqbNGBQaRcBtjQMQ==", + "dev": true, + "requires": { + "request-promise-core": "1.1.3", + "stealthy-require": "^1.1.1", + "tough-cookie": "^2.3.3" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true + }, + "require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", + "dev": true + }, + "resolve": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.11.1.tgz", + "integrity": "sha512-vIpgF6wfuJOZI7KKKSP+HmiKggadPQAdsp5HiC1mvqnfp0gF1vdwgBWZIdrVft9pgqoMFQN+R7BSWZiBxx+BBw==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + }, + "resolve-cwd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", + "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", + "dev": true, + "requires": { + "resolve-from": "^3.0.0" + } + }, + "resolve-dir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", + "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=", + "dev": true, + "requires": { + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" + } + }, + "resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", + "dev": true + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", + "dev": true + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true + }, + "ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "dev": true, + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "rsvp": { + "version": "4.8.5", + "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", + "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==", + "dev": true + }, + "run-queue": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", + "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=", + "dev": true, + "requires": { + "aproba": "^1.1.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "dev": true, + "requires": { + "ret": "~0.1.10" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "sane": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/sane/-/sane-4.1.0.tgz", + "integrity": "sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA==", + "dev": true, + "requires": { + "@cnakazawa/watch": "^1.0.3", + "anymatch": "^2.0.0", + "capture-exit": "^2.0.0", + "exec-sh": "^0.3.2", + "execa": "^1.0.0", + "fb-watchman": "^2.0.0", + "micromatch": "^3.1.4", + "minimist": "^1.1.1", + "walker": "~1.0.5" + } + }, + "sass-graph": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.4.tgz", + "integrity": "sha1-E/vWPNHK8JCLn9k0dq1DpR0eC0k=", + "dev": true, + "requires": { + "glob": "^7.0.0", + "lodash": "^4.0.0", + "scss-tokenizer": "^0.2.3", + "yargs": "^7.0.0" + } + }, + "sass-loader": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-8.0.2.tgz", + "integrity": "sha512-7o4dbSK8/Ol2KflEmSco4jTjQoV988bM82P9CZdmo9hR3RLnvNc0ufMNdMrB0caq38JQ/FgF4/7RcbcfKzxoFQ==", + "dev": true, + "requires": { + "clone-deep": "^4.0.1", + "loader-utils": "^1.2.3", + "neo-async": "^2.6.1", + "schema-utils": "^2.6.1", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "dev": true + }, + "scheduler": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.18.0.tgz", + "integrity": "sha512-agTSHR1Nbfi6ulI0kYNK0203joW2Y5W4po4l+v03tOoiJKpTBbxpNhWDvqc/4IcOw+KLmSiQLTasZ4cab2/UWQ==", + "dev": true, + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, + "schema-utils": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.6.5.tgz", + "integrity": "sha512-5KXuwKziQrTVHh8j/Uxz+QUbxkaLW9X/86NBlx/gnKgtsZA2GIVMUn17qWhRFwF8jdYb3Dig5hRO/W5mZqy6SQ==", + "dev": true, + "requires": { + "ajv": "^6.12.0", + "ajv-keywords": "^3.4.1" + }, + "dependencies": { + "ajv": { + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz", + "integrity": "sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "fast-deep-equal": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", + "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", + "dev": true + } + } + }, + "scss-tokenizer": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz", + "integrity": "sha1-jrBtualyMzOCTT9VMGQRSYR85dE=", + "dev": true, + "requires": { + "js-base64": "^2.1.8", + "source-map": "^0.4.2" + }, + "dependencies": { + "source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "dev": true, + "requires": { + "amdefine": ">=0.0.4" + } + } + } + }, + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "dev": true + }, + "serialize-javascript": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.2.tgz", + "integrity": "sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ==", + "dev": true + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, + "set-value": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", + "dev": true + }, + "sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "requires": { + "kind-of": "^6.0.2" + } + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "shellwords": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", + "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", + "dev": true + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true + }, + "sisteransi": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.4.tgz", + "integrity": "sha512-/ekMoM4NJ59ivGSfKapeG+FWtrmWvA1p6FBZwXrqojw90vJu8lBmrTxCMuBCydKtkaUe2zt4PlxeTKpjwMbyig==", + "dev": true + }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "dev": true, + "requires": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, + "requires": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, + "requires": { + "kind-of": "^3.2.0" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "sort-keys": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", + "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=", + "dev": true, + "requires": { + "is-plain-obj": "^1.0.0" + } + }, + "source-list-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", + "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + }, + "source-map-resolve": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", + "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", + "dev": true, + "requires": { + "atob": "^2.1.1", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "source-map-support": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.16.tgz", + "integrity": "sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", + "dev": true + }, + "spdx-correct": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", + "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", + "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", + "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", + "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", + "dev": true + }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.0" + } + }, + "sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "dev": true, + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "ssri": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", + "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", + "dev": true, + "requires": { + "figgy-pudding": "^3.5.1" + } + }, + "stack-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.2.tgz", + "integrity": "sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA==", + "dev": true + }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "dev": true, + "requires": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "stdout-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.1.tgz", + "integrity": "sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA==", + "dev": true, + "requires": { + "readable-stream": "^2.0.1" + } + }, + "stealthy-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", + "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=", + "dev": true + }, + "stream-browserify": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", + "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==", + "dev": true, + "requires": { + "inherits": "~2.0.1", + "readable-stream": "^2.0.2" + } + }, + "stream-each": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz", + "integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "stream-shift": "^1.0.0" + } + }, + "stream-shift": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", + "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=", + "dev": true + }, + "strict-uri-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", + "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=", + "dev": true + }, + "string-length": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-2.0.0.tgz", + "integrity": "sha1-1A27aGo6zpYMHP/KVivyxF+DY+0=", + "dev": true, + "requires": { + "astral-regex": "^1.0.0", + "strip-ansi": "^4.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + }, + "dependencies": { + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + } + } + }, + "string.prototype.trimleft": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz", + "integrity": "sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "function-bind": "^1.1.1" + } + }, + "string.prototype.trimright": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz", + "integrity": "sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "function-bind": "^1.1.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, + "requires": { + "is-utf8": "^0.2.0" + } + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "dev": true + }, + "strip-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", + "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", + "dev": true, + "requires": { + "get-stdin": "^4.0.1" + } + }, + "style-loader": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-1.1.3.tgz", + "integrity": "sha512-rlkH7X/22yuwFYK357fMN/BxYOorfnfq0eD7+vqlemSK4wEcejFF1dg4zxP0euBW8NrYx2WZzZ8PPFevr7D+Kw==", + "dev": true, + "requires": { + "loader-utils": "^1.2.3", + "schema-utils": "^2.6.4" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" + }, + "symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true + }, + "tapable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", + "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", + "dev": true + }, + "tar": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz", + "integrity": "sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==", + "dev": true, + "requires": { + "block-stream": "*", + "fstream": "^1.0.12", + "inherits": "2" + } + }, + "terser": { + "version": "4.6.7", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.6.7.tgz", + "integrity": "sha512-fmr7M1f7DBly5cX2+rFDvmGBAaaZyPrHYK4mMdHEDAdNTqXSZgSOfqsfGq2HqPGT/1V0foZZuCZFx8CHKgAk3g==", + "dev": true, + "requires": { + "commander": "^2.20.0", + "source-map": "~0.6.1", + "source-map-support": "~0.5.12" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "terser-webpack-plugin": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.3.tgz", + "integrity": "sha512-QMxecFz/gHQwteWwSo5nTc6UaICqN1bMedC5sMtUc7y3Ha3Q8y6ZO0iCR8pq4RJC8Hjf0FEPEHZqcMB/+DFCrA==", + "dev": true, + "requires": { + "cacache": "^12.0.2", + "find-cache-dir": "^2.1.0", + "is-wsl": "^1.1.0", + "schema-utils": "^1.0.0", + "serialize-javascript": "^2.1.2", + "source-map": "^0.6.1", + "terser": "^4.1.2", + "webpack-sources": "^1.4.0", + "worker-farm": "^1.7.0" + }, + "dependencies": { + "schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "test-exclude": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-5.2.3.tgz", + "integrity": "sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g==", + "dev": true, + "requires": { + "glob": "^7.1.3", + "minimatch": "^3.0.4", + "read-pkg-up": "^4.0.0", + "require-main-filename": "^2.0.0" + }, + "dependencies": { + "load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + } + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "dev": true, + "requires": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + } + }, + "read-pkg-up": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-4.0.0.tgz", + "integrity": "sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA==", + "dev": true, + "requires": { + "find-up": "^3.0.0", + "read-pkg": "^3.0.0" + } + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + } + } + }, + "throat": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/throat/-/throat-4.1.0.tgz", + "integrity": "sha1-iQN8vJLFarGJJua6TLsgDhVnKmo=", + "dev": true + }, + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "tmpl": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", + "integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=", + "dev": true + }, + "to-arraybuffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", + "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=", + "dev": true + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true + }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "dev": true, + "requires": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + }, + "tough-cookie": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "dev": true, + "requires": { + "psl": "^1.1.24", + "punycode": "^1.4.1" + } + }, + "tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", + "dev": true, + "requires": { + "punycode": "^2.1.0" + }, + "dependencies": { + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + } + } + }, + "trim-newlines": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", + "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", + "dev": true + }, + "true-case-path": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.3.tgz", + "integrity": "sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew==", + "dev": true, + "requires": { + "glob": "^7.1.2" + } + }, + "tslib": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz", + "integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==", + "dev": true + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dev": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "dev": true + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true + }, + "unicode-canonical-property-names-ecmascript": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", + "integrity": "sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==", + "dev": true + }, + "unicode-match-property-ecmascript": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz", + "integrity": "sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==", + "dev": true, + "requires": { + "unicode-canonical-property-names-ecmascript": "^1.0.4", + "unicode-property-aliases-ecmascript": "^1.0.4" + } + }, + "unicode-match-property-value-ecmascript": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.1.0.tgz", + "integrity": "sha512-hDTHvaBk3RmFzvSl0UVrUmC3PuW9wKVnpoUDYH0JDkSIovzw+J5viQmeYHxVSBptubnr7PbH2e0fnpDRQnQl5g==", + "dev": true + }, + "unicode-property-aliases-ecmascript": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.5.tgz", + "integrity": "sha512-L5RAqCfXqAwR3RriF8pM0lU0w4Ryf/GgzONwi6KnL1taJQa7x1TCxdJnILX59WIGOwR57IVxn7Nej0fz1Ny6fw==", + "dev": true + }, + "union-value": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^2.0.1" + } + }, + "uniq": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", + "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=", + "dev": true + }, + "unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "dev": true, + "requires": { + "unique-slug": "^2.0.0" + } + }, + "unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4" + } + }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "dev": true, + "requires": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "dev": true, + "requires": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", + "dev": true + } + } + }, + "upath": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.2.tgz", + "integrity": "sha512-kXpym8nmDmlCBr7nKdIx8P2jNBa+pBpIUFRnKJ4dr8htyYGJFokkr2ZvERRtUN+9SY+JqXouNgUPtv6JQva/2Q==", + "dev": true + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + }, + "dependencies": { + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + } + } + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", + "dev": true + }, + "url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "dev": true, + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", + "dev": true + } + } + }, + "use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "dev": true + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "util.promisify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz", + "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "object.getownpropertydescriptors": "^2.0.3" + } + }, + "uuid": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz", + "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==", + "dev": true + }, + "v8-compile-cache": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz", + "integrity": "sha512-CNmdbwQMBjwr9Gsmohvm0pbL954tJrNzf6gWL3K+QMQf00PF7ERGrEiLgjuU3mKreLC2MeGhUsNV9ybTbLgd3w==", + "dev": true + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "vm-browserify": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", + "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", + "dev": true + }, + "w3c-hr-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz", + "integrity": "sha1-gqwr/2PZUOqeMYmlimViX+3xkEU=", + "dev": true, + "requires": { + "browser-process-hrtime": "^0.1.2" + } + }, + "walker": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz", + "integrity": "sha1-L3+bj9ENZ3JisYqITijRlhjgKPs=", + "dev": true, + "requires": { + "makeerror": "1.0.x" + } + }, + "watchpack": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.0.tgz", + "integrity": "sha512-i6dHe3EyLjMmDlU1/bGQpEw25XSjkJULPuAVKCbNRefQVq48yXKUpwg538F7AZTf9kyr57zj++pQFltUa5H7yA==", + "dev": true, + "requires": { + "chokidar": "^2.0.2", + "graceful-fs": "^4.1.2", + "neo-async": "^2.5.0" + } + }, + "webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true + }, + "webpack": { + "version": "4.42.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.42.1.tgz", + "integrity": "sha512-SGfYMigqEfdGchGhFFJ9KyRpQKnipvEvjc1TwrXEPCM6H5Wywu10ka8o3KGrMzSMxMQKt8aCHUFh5DaQ9UmyRg==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-module-context": "1.9.0", + "@webassemblyjs/wasm-edit": "1.9.0", + "@webassemblyjs/wasm-parser": "1.9.0", + "acorn": "^6.2.1", + "ajv": "^6.10.2", + "ajv-keywords": "^3.4.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^4.1.0", + "eslint-scope": "^4.0.3", + "json-parse-better-errors": "^1.0.2", + "loader-runner": "^2.4.0", + "loader-utils": "^1.2.3", + "memory-fs": "^0.4.1", + "micromatch": "^3.1.10", + "mkdirp": "^0.5.3", + "neo-async": "^2.6.1", + "node-libs-browser": "^2.2.1", + "schema-utils": "^1.0.0", + "tapable": "^1.1.3", + "terser-webpack-plugin": "^1.4.3", + "watchpack": "^1.6.0", + "webpack-sources": "^1.4.1" + }, + "dependencies": { + "acorn": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", + "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==", + "dev": true + }, + "schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + } + } + } + }, + "webpack-cli": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-3.3.11.tgz", + "integrity": "sha512-dXlfuml7xvAFwYUPsrtQAA9e4DOe58gnzSxhgrO/ZM/gyXTBowrsYeubyN4mqGhYdpXMFNyQ6emjJS9M7OBd4g==", + "dev": true, + "requires": { + "chalk": "2.4.2", + "cross-spawn": "6.0.5", + "enhanced-resolve": "4.1.0", + "findup-sync": "3.0.0", + "global-modules": "2.0.0", + "import-local": "2.0.0", + "interpret": "1.2.0", + "loader-utils": "1.2.3", + "supports-color": "6.1.0", + "v8-compile-cache": "2.0.3", + "yargs": "13.2.4" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "emojis-list": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", + "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=", + "dev": true + }, + "enhanced-resolve": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz", + "integrity": "sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "memory-fs": "^0.4.0", + "tapable": "^1.0.0" + } + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "global-modules": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", + "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", + "dev": true, + "requires": { + "global-prefix": "^3.0.0" + } + }, + "global-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", + "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", + "dev": true, + "requires": { + "ini": "^1.3.5", + "kind-of": "^6.0.2", + "which": "^1.3.1" + } + }, + "invert-kv": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", + "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", + "dev": true + }, + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "lcid": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", + "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", + "dev": true, + "requires": { + "invert-kv": "^2.0.0" + } + }, + "loader-utils": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", + "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^2.0.0", + "json5": "^1.0.1" + } + }, + "os-locale": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", + "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", + "dev": true, + "requires": { + "execa": "^1.0.0", + "lcid": "^2.0.0", + "mem": "^4.0.0" + } + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + } + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + }, + "yargs": { + "version": "13.2.4", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.4.tgz", + "integrity": "sha512-HG/DWAJa1PAnHT9JAhNa8AbAv3FPaiLzioSjCcmuXXhP8MlpHO5vwls4g4j6n30Z74GVQj8Xa62dWVx1QCGklg==", + "dev": true, + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "os-locale": "^3.1.0", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.0" + } + }, + "yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "webpack-merge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-4.2.2.tgz", + "integrity": "sha512-TUE1UGoTX2Cd42j3krGYqObZbOD+xF7u28WB7tfUordytSjbWTIjK/8V0amkBfTYN4/pB/GIDlJZZ657BGG19g==", + "dev": true, + "requires": { + "lodash": "^4.17.15" + } + }, + "webpack-sources": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", + "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "dev": true, + "requires": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "whatwg-encoding": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", + "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", + "dev": true, + "requires": { + "iconv-lite": "0.4.24" + } + }, + "whatwg-mimetype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", + "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", + "dev": true + }, + "whatwg-url": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz", + "integrity": "sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==", + "dev": true, + "requires": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", + "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=", + "dev": true + }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "dev": true, + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, + "worker-farm": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.7.0.tgz", + "integrity": "sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==", + "dev": true, + "requires": { + "errno": "~0.1.7" + } + }, + "wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "dev": true, + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "write-file-atomic": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.1.tgz", + "integrity": "sha512-TGHFeZEZMnv+gBFRfjAcxL5bPHrsGKtnb4qsFAws7/vlh+QfwAaySIw4AXP9ZskTTh5GWu3FLuJhsWVdiJPGvg==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.2" + } + }, + "ws": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz", + "integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==", + "dev": true, + "requires": { + "async-limiter": "~1.0.0" + } + }, + "xml-name-validator": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", + "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", + "dev": true + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true + }, + "y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=", + "dev": true + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "dev": true + }, + "yargs": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.0.tgz", + "integrity": "sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg=", + "dev": true, + "requires": { + "camelcase": "^3.0.0", + "cliui": "^3.2.0", + "decamelize": "^1.1.1", + "get-caller-file": "^1.0.1", + "os-locale": "^1.4.0", + "read-pkg-up": "^1.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^1.0.2", + "which-module": "^1.0.0", + "y18n": "^3.2.1", + "yargs-parser": "^5.0.0" + } + }, + "yargs-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0.tgz", + "integrity": "sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo=", + "dev": true, + "requires": { + "camelcase": "^3.0.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..e2c4667 --- /dev/null +++ b/package.json @@ -0,0 +1,61 @@ +{ + "name": "newsreader", + "version": "0.1.0", + "description": "Application for viewing RSS feeds", + "main": "index.js", + "scripts": { + "lint": "npx prettier \"src/newsreader/js/**/*.js\" --check", + "format": "npx prettier \"src/newsreader/js/**/*.js\" --write", + "build": "npx webpack --config webpack.dev.babel.js", + "build:watch": "npx webpack --config webpack.dev.babel.js --watch", + "build:prod": "npx webpack --config webpack.prod.babel.js", + "test": "npx jest", + "test:watch": "npm test -- --watch" + }, + "repository": { + "type": "git", + "url": "[git@git.fudiggity.nl:5000]:sonny/newsreader.git" + }, + "author": "Sonny", + "license": "GPL-3.0-or-later", + "dependencies": { + "css.gg": "^1.0.6", + "js-cookie": "^2.2.1", + "lodash": "^4.17.15", + "object-assign": "^4.1.1", + "react-redux": "^7.1.3", + "redux": "^4.0.5", + "redux-logger": "^3.0.6", + "redux-thunk": "^2.3.0" + }, + "devDependencies": { + "@babel/core": "^7.7.7", + "@babel/plugin-proposal-class-properties": "^7.7.4", + "@babel/plugin-proposal-function-bind": "^7.7.4", + "@babel/plugin-syntax-dynamic-import": "^7.7.4", + "@babel/plugin-syntax-function-bind": "^7.7.4", + "@babel/plugin-transform-react-jsx": "^7.7.7", + "@babel/plugin-transform-runtime": "^7.7.6", + "@babel/preset-env": "^7.7.7", + "@babel/register": "^7.7.7", + "@babel/runtime": "^7.7.7", + "babel-jest": "^24.9.0", + "babel-loader": "^8.1.0", + "clean-webpack-plugin": "^3.0.0", + "css-loader": "^3.4.2", + "fetch-mock": "^8.3.1", + "jest": "^24.9.0", + "mini-css-extract-plugin": "^0.9.0", + "node-fetch": "^2.6.0", + "node-sass": "^4.13.1", + "prettier": "^1.19.1", + "react": "^16.12.0", + "react-dom": "^16.12.0", + "redux-mock-store": "^1.5.4", + "sass-loader": "^8.0.2", + "style-loader": "^1.1.3", + "webpack": "^4.42.1", + "webpack-cli": "^3.3.11", + "webpack-merge": "^4.2.2" + } +} diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..0681268 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1159 @@ +[[package]] +category = "main" +description = "Low-level AMQP client for Python (fork of amqplib)." +name = "amqp" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "2.5.2" + +[package.dependencies] +vine = ">=1.1.3,<5.0.0a1" + +[[package]] +category = "dev" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +name = "appdirs" +optional = false +python-versions = "*" +version = "1.4.3" + +[[package]] +category = "main" +description = "ASGI specs, helper code, and adapters" +name = "asgiref" +optional = false +python-versions = ">=3.5" +version = "3.2.7" + +[package.extras] +tests = ["pytest (>=4.3.0,<4.4.0)", "pytest-asyncio (>=0.10.0,<0.11.0)"] + +[[package]] +category = "dev" +description = "Classes Without Boilerplate" +name = "attrs" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "19.3.0" + +[package.extras] +azure-pipelines = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "pytest-azurepipelines"] +dev = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "pre-commit"] +docs = ["sphinx", "zope.interface"] +tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] + +[[package]] +category = "dev" +description = "Removes unused imports and unused variables" +name = "autoflake" +optional = false +python-versions = "*" +version = "1.3.1" + +[package.dependencies] +pyflakes = ">=1.1.0" + +[[package]] +category = "main" +description = "Screen-scraping library" +name = "beautifulsoup4" +optional = false +python-versions = "*" +version = "4.9.0" + +[package.dependencies] +soupsieve = [">1.2", "<2.0"] + +[package.extras] +html5lib = ["html5lib"] +lxml = ["lxml"] + +[[package]] +category = "main" +description = "Python multiprocessing fork with improvements and bugfixes" +name = "billiard" +optional = false +python-versions = "*" +version = "3.6.3.0" + +[[package]] +category = "dev" +description = "The uncompromising code formatter." +name = "black" +optional = false +python-versions = ">=3.6" +version = "19.3b0" + +[package.dependencies] +appdirs = "*" +attrs = ">=18.1.0" +click = ">=6.5" +toml = ">=0.9.4" + +[package.extras] +d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] + +[[package]] +category = "main" +description = "An easy safelist-based HTML-sanitizing tool." +name = "bleach" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "3.1.4" + +[package.dependencies] +six = ">=1.9.0" +webencodings = "*" + +[[package]] +category = "main" +description = "Distributed Task Queue." +name = "celery" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*," +version = "4.4.2" + +[package.dependencies] +billiard = ">=3.6.3.0,<4.0" +kombu = ">=4.6.8,<4.7" +pytz = ">0.0-dev" +vine = "1.3.0" + +[package.extras] +arangodb = ["pyArango (>=1.3.2)"] +auth = ["cryptography"] +azureblockblob = ["azure-storage (0.36.0)", "azure-common (1.1.5)", "azure-storage-common (1.1.0)"] +brotli = ["brotli (>=1.0.0)", "brotlipy (>=0.7.0)"] +cassandra = ["cassandra-driver"] +consul = ["python-consul"] +cosmosdbsql = ["pydocumentdb (2.3.2)"] +couchbase = ["couchbase", "couchbase-cffi"] +couchdb = ["pycouchdb"] +django = ["Django (>=1.11)"] +dynamodb = ["boto3 (>=1.9.178)"] +elasticsearch = ["elasticsearch"] +eventlet = ["eventlet (>=0.24.1)"] +gevent = ["gevent"] +librabbitmq = ["librabbitmq (>=1.5.0)"] +lzma = ["backports.lzma"] +memcache = ["pylibmc"] +mongodb = ["pymongo (>=3.3.0)"] +msgpack = ["msgpack"] +pymemcache = ["python-memcached"] +pyro = ["pyro4"] +redis = ["redis (>=3.2.0)"] +riak = ["riak (>=2.0)"] +s3 = ["boto3 (>=1.9.125)"] +slmq = ["softlayer-messaging (>=1.0.3)"] +solar = ["ephem"] +sqlalchemy = ["sqlalchemy"] +sqs = ["boto3 (>=1.9.125)", "pycurl (7.43.0.2)"] +tblib = ["tblib (>=1.3.0)", "tblib (>=1.5.0)"] +yaml = ["PyYAML (>=3.10)"] +zookeeper = ["kazoo (>=1.3.1)"] +zstd = ["zstandard"] + +[[package]] +category = "main" +description = "Python package for providing Mozilla's CA Bundle." +name = "certifi" +optional = false +python-versions = "*" +version = "2020.4.5.1" + +[[package]] +category = "main" +description = "Universal encoding detector for Python 2 and 3" +name = "chardet" +optional = false +python-versions = "*" +version = "3.0.4" + +[[package]] +category = "dev" +description = "Composable command line interface toolkit" +name = "click" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "7.1.1" + +[[package]] +category = "main" +description = "Python client library for Core API." +name = "coreapi" +optional = false +python-versions = "*" +version = "2.3.3" + +[package.dependencies] +coreschema = "*" +itypes = "*" +requests = "*" +uritemplate = "*" + +[[package]] +category = "main" +description = "Core Schema." +name = "coreschema" +optional = false +python-versions = "*" +version = "0.0.4" + +[package.dependencies] +jinja2 = "*" + +[[package]] +category = "dev" +description = "Code coverage measurement for Python" +name = "coverage" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" +version = "5.1" + +[package.extras] +toml = ["toml"] + +[[package]] +category = "main" +description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." +name = "django" +optional = false +python-versions = ">=3.6" +version = "3.0.5" + +[package.dependencies] +asgiref = ">=3.2,<4.0" +pytz = "*" +sqlparse = ">=0.2.2" + +[package.extras] +argon2 = ["argon2-cffi (>=16.1.0)"] +bcrypt = ["bcrypt"] + +[[package]] +category = "main" +description = "A helper class for handling configuration defaults of packaged apps gracefully." +name = "django-appconf" +optional = false +python-versions = "*" +version = "1.0.4" + +[package.dependencies] +django = "*" + +[[package]] +category = "main" +description = "Keep track of failed login attempts in Django-powered sites." +name = "django-axes" +optional = false +python-versions = "~=3.6" +version = "5.3.1" + +[package.dependencies] +django = ">=1.11" +django-appconf = ">=1.0.3" +django-ipware = ">=2.0.2" + +[[package]] +category = "main" +description = "Database-backed Periodic Tasks." +name = "django-celery-beat" +optional = false +python-versions = "*" +version = "2.0.0" + +[package.dependencies] +Django = ">=1.11.17" +celery = "*" +django-timezone-field = ">=4.0,<5.0" +python-crontab = ">=2.3.4" + +[[package]] +category = "dev" +description = "A configurable set of panels that display various debug information about the current request/response." +name = "django-debug-toolbar" +optional = false +python-versions = ">=3.5" +version = "2.2" + +[package.dependencies] +Django = ">=1.11" +sqlparse = ">=0.2.0" + +[[package]] +category = "dev" +description = "Extensions for Django" +name = "django-extensions" +optional = false +python-versions = "*" +version = "2.2.9" + +[package.dependencies] +six = ">=1.2" + +[[package]] +category = "main" +description = "A Django utility application that returns client's real IP address" +name = "django-ipware" +optional = false +python-versions = "*" +version = "2.1.0" + +[[package]] +category = "main" +description = "An extensible user-registration application for Django" +name = "django-registration-redux" +optional = false +python-versions = "*" +version = "2.7" + +[[package]] +category = "main" +description = "A Django app providing database and form fields for pytz timezone objects." +name = "django-timezone-field" +optional = false +python-versions = ">=3.5" +version = "4.0" + +[package.dependencies] +django = ">=2.2" +pytz = "*" + +[[package]] +category = "main" +description = "Web APIs for Django, made easy." +name = "djangorestframework" +optional = false +python-versions = ">=3.5" +version = "3.11.0" + +[package.dependencies] +django = ">=1.11" + +[[package]] +category = "main" +description = "Automated generation of real Swagger/OpenAPI 2.0 schemas from Django Rest Framework code." +name = "drf-yasg" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "1.17.1" + +[package.dependencies] +Django = ">=1.11.7" +coreapi = ">=2.3.3" +coreschema = ">=0.0.4" +djangorestframework = ">=3.8" +inflection = ">=0.3.1" +packaging = "*" +"ruamel.yaml" = ">=0.15.34" +six = ">=1.10.0" +uritemplate = ">=3.0.0" + +[package.extras] +validation = ["swagger-spec-validator (>=2.1.0)"] + +[[package]] +category = "dev" +description = "A versatile test fixtures replacement based on thoughtbot's factory_bot for Ruby." +name = "factory-boy" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.12.0" + +[package.dependencies] +Faker = ">=0.7.0" + +[[package]] +category = "dev" +description = "Faker is a Python package that generates fake data for you." +name = "faker" +optional = false +python-versions = ">=3.4" +version = "4.0.2" + +[package.dependencies] +python-dateutil = ">=2.4" +text-unidecode = "1.3" + +[[package]] +category = "main" +description = "Universal feed parser, handles RSS 0.9x, RSS 1.0, RSS 2.0, CDF, Atom 0.3, and Atom 1.0 feeds" +name = "feedparser" +optional = false +python-versions = "*" +version = "5.2.1" + +[[package]] +category = "dev" +description = "Let your Python tests travel through time" +name = "freezegun" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "0.3.15" + +[package.dependencies] +python-dateutil = ">=1.0,<2.0 || >2.0" +six = "*" + +[[package]] +category = "main" +description = "WSGI HTTP Server for UNIX" +name = "gunicorn" +optional = false +python-versions = ">=3.4" +version = "20.0.4" + +[package.dependencies] +setuptools = ">=3.0" + +[package.extras] +eventlet = ["eventlet (>=0.9.7)"] +gevent = ["gevent (>=0.13)"] +setproctitle = ["setproctitle"] +tornado = ["tornado (>=0.2)"] + +[[package]] +category = "main" +description = "Internationalized Domain Names in Applications (IDNA)" +name = "idna" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.9" + +[[package]] +category = "main" +description = "Read metadata from Python packages" +marker = "python_version < \"3.8\"" +name = "importlib-metadata" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +version = "1.6.0" + +[package.dependencies] +zipp = ">=0.5" + +[package.extras] +docs = ["sphinx", "rst.linker"] +testing = ["packaging", "importlib-resources"] + +[[package]] +category = "main" +description = "A port of Ruby on Rails inflector to Python" +name = "inflection" +optional = false +python-versions = ">=3.5" +version = "0.4.0" + +[[package]] +category = "dev" +description = "A Python utility / library to sort Python imports." +name = "isort" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "4.3.21" + +[package.extras] +pipfile = ["pipreqs", "requirementslib"] +pyproject = ["toml"] +requirements = ["pipreqs", "pip-api"] +xdg_home = ["appdirs (>=1.4.0)"] + +[[package]] +category = "main" +description = "Simple immutable types for python." +name = "itypes" +optional = false +python-versions = "*" +version = "1.1.0" + +[[package]] +category = "main" +description = "A very fast and expressive template engine." +name = "jinja2" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "2.11.1" + +[package.dependencies] +MarkupSafe = ">=0.23" + +[package.extras] +i18n = ["Babel (>=0.8)"] + +[[package]] +category = "main" +description = "Messaging library for Python." +name = "kombu" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "4.6.8" + +[package.dependencies] +amqp = ">=2.5.2,<2.6" + +[package.dependencies.importlib-metadata] +python = "<3.8" +version = ">=0.18" + +[package.extras] +azureservicebus = ["azure-servicebus (>=0.21.1)"] +azurestoragequeues = ["azure-storage-queue"] +consul = ["python-consul (>=0.6.0)"] +librabbitmq = ["librabbitmq (>=1.5.2)"] +mongodb = ["pymongo (>=3.3.0)"] +msgpack = ["msgpack"] +pyro = ["pyro4"] +qpid = ["qpid-python (>=0.26)", "qpid-tools (>=0.26)"] +redis = ["redis (>=3.3.11)"] +slmq = ["softlayer-messaging (>=1.0.3)"] +sqlalchemy = ["sqlalchemy"] +sqs = ["boto3 (>=1.4.4)", "pycurl (7.43.0.2)"] +yaml = ["PyYAML (>=3.10)"] +zookeeper = ["kazoo (>=1.3.1)"] + +[[package]] +category = "main" +description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." +name = "lxml" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" +version = "4.5.0" + +[package.extras] +cssselect = ["cssselect (>=0.7)"] +html5 = ["html5lib"] +htmlsoup = ["beautifulsoup4"] +source = ["Cython (>=0.29.7)"] + +[[package]] +category = "main" +description = "Safely add untrusted strings to HTML/XML markup." +name = "markupsafe" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" +version = "1.1.1" + +[[package]] +category = "main" +description = "Core utilities for Python packages" +name = "packaging" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "20.3" + +[package.dependencies] +pyparsing = ">=2.0.2" +six = "*" + +[[package]] +category = "main" +description = "psycopg2 - Python-PostgreSQL Database Adapter" +name = "psycopg2-binary" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" +version = "2.8.5" + +[[package]] +category = "dev" +description = "passive checker of Python programs" +name = "pyflakes" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.2.0" + +[[package]] +category = "main" +description = "Python parsing module" +name = "pyparsing" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +version = "2.4.7" + +[[package]] +category = "main" +description = "Python Crontab API" +name = "python-crontab" +optional = false +python-versions = "*" +version = "2.4.1" + +[package.dependencies] +python-dateutil = "*" + +[package.extras] +cron-description = ["cron-descriptor"] +cron-schedule = ["croniter"] + +[[package]] +category = "main" +description = "Extensions to the standard Python datetime module" +name = "python-dateutil" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +version = "2.8.1" + +[package.dependencies] +six = ">=1.5" + +[[package]] +category = "main" +description = "Add .env support to your django/flask apps in development and deployments" +name = "python-dotenv" +optional = false +python-versions = "*" +version = "0.12.0" + +[package.extras] +cli = ["click (>=5.0)"] + +[[package]] +category = "main" +description = "Pure python memcached client" +name = "python-memcached" +optional = false +python-versions = "*" +version = "1.59" + +[package.dependencies] +six = ">=1.4.0" + +[[package]] +category = "main" +description = "World timezone definitions, modern and historical" +name = "pytz" +optional = false +python-versions = "*" +version = "2019.3" + +[[package]] +category = "main" +description = "Python HTTP for Humans." +name = "requests" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "2.23.0" + +[package.dependencies] +certifi = ">=2017.4.17" +chardet = ">=3.0.2,<4" +idna = ">=2.5,<3" +urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" + +[package.extras] +security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] +socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"] + +[[package]] +category = "main" +description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" +name = "ruamel.yaml" +optional = false +python-versions = "*" +version = "0.16.10" + +[package.dependencies] +[package.dependencies."ruamel.yaml.clib"] +python = "<3.9" +version = ">=0.1.2" + +[package.extras] +docs = ["ryd"] +jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"] + +[[package]] +category = "main" +description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" +marker = "platform_python_implementation == \"CPython\" and python_version < \"3.9\"" +name = "ruamel.yaml.clib" +optional = false +python-versions = "*" +version = "0.2.0" + +[[package]] +category = "main" +description = "Python 2 and 3 compatibility utilities" +name = "six" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +version = "1.14.0" + +[[package]] +category = "main" +description = "A modern CSS selector implementation for Beautiful Soup." +name = "soupsieve" +optional = false +python-versions = "*" +version = "1.9.5" + +[[package]] +category = "main" +description = "Non-validating SQL parser" +name = "sqlparse" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "0.3.1" + +[[package]] +category = "dev" +description = "Traceback serialization library." +name = "tblib" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "1.6.0" + +[[package]] +category = "dev" +description = "The most basic Text::Unidecode port" +name = "text-unidecode" +optional = false +python-versions = "*" +version = "1.3" + +[[package]] +category = "dev" +description = "Python Library for Tom's Obvious, Minimal Language" +name = "toml" +optional = false +python-versions = "*" +version = "0.10.0" + +[[package]] +category = "main" +description = "URI templates" +name = "uritemplate" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "3.0.1" + +[[package]] +category = "main" +description = "HTTP library with thread-safe connection pooling, file post, and more." +name = "urllib3" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" +version = "1.25.8" + +[package.extras] +brotli = ["brotlipy (>=0.6.0)"] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"] + +[[package]] +category = "main" +description = "Promises, promises, promises." +name = "vine" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.3.0" + +[[package]] +category = "main" +description = "Character encoding aliases for legacy web content" +name = "webencodings" +optional = false +python-versions = "*" +version = "0.5.1" + +[[package]] +category = "main" +description = "Backport of pathlib-compatible object wrapper for zip files" +marker = "python_version < \"3.8\"" +name = "zipp" +optional = false +python-versions = ">=3.6" +version = "3.1.0" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] +testing = ["jaraco.itertools", "func-timeout"] + +[metadata] +content-hash = "38cc29547dab994d438a7a4082fca9f557acfff59626df37ec9ee9f15ff094a0" +python-versions = "^3.7" + +[metadata.files] +amqp = [ + {file = "amqp-2.5.2-py2.py3-none-any.whl", hash = "sha256:6e649ca13a7df3faacdc8bbb280aa9a6602d22fd9d545336077e573a1f4ff3b8"}, + {file = "amqp-2.5.2.tar.gz", hash = "sha256:77f1aef9410698d20eaeac5b73a87817365f457a507d82edf292e12cbb83b08d"}, +] +appdirs = [ + {file = "appdirs-1.4.3-py2.py3-none-any.whl", hash = "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"}, + {file = "appdirs-1.4.3.tar.gz", hash = "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92"}, +] +asgiref = [ + {file = "asgiref-3.2.7-py2.py3-none-any.whl", hash = "sha256:9ca8b952a0a9afa61d30aa6d3d9b570bb3fd6bafcf7ec9e6bed43b936133db1c"}, + {file = "asgiref-3.2.7.tar.gz", hash = "sha256:8036f90603c54e93521e5777b2b9a39ba1bad05773fcf2d208f0299d1df58ce5"}, +] +attrs = [ + {file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"}, + {file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"}, +] +autoflake = [ + {file = "autoflake-1.3.1.tar.gz", hash = "sha256:680cb9dade101ed647488238ccb8b8bfb4369b53d58ba2c8cdf7d5d54e01f95b"}, +] +beautifulsoup4 = [ + {file = "beautifulsoup4-4.9.0-py2-none-any.whl", hash = "sha256:a4bbe77fd30670455c5296242967a123ec28c37e9702a8a81bd2f20a4baf0368"}, + {file = "beautifulsoup4-4.9.0-py3-none-any.whl", hash = "sha256:d4e96ac9b0c3a6d3f0caae2e4124e6055c5dcafde8e2f831ff194c104f0775a0"}, + {file = "beautifulsoup4-4.9.0.tar.gz", hash = "sha256:594ca51a10d2b3443cbac41214e12dbb2a1cd57e1a7344659849e2e20ba6a8d8"}, +] +billiard = [ + {file = "billiard-3.6.3.0-py3-none-any.whl", hash = "sha256:bff575450859a6e0fbc2f9877d9b715b0bbc07c3565bb7ed2280526a0cdf5ede"}, + {file = "billiard-3.6.3.0.tar.gz", hash = "sha256:d91725ce6425f33a97dfa72fb6bfef0e47d4652acd98a032bd1a7fbf06d5fa6a"}, +] +black = [ + {file = "black-19.3b0-py36-none-any.whl", hash = "sha256:09a9dcb7c46ed496a9850b76e4e825d6049ecd38b611f1224857a79bd985a8cf"}, + {file = "black-19.3b0.tar.gz", hash = "sha256:68950ffd4d9169716bcb8719a56c07a2f4485354fec061cdd5910aa07369731c"}, +] +bleach = [ + {file = "bleach-3.1.4-py2.py3-none-any.whl", hash = "sha256:cc8da25076a1fe56c3ac63671e2194458e0c4d9c7becfd52ca251650d517903c"}, + {file = "bleach-3.1.4.tar.gz", hash = "sha256:e78e426105ac07026ba098f04de8abe9b6e3e98b5befbf89b51a5ef0a4292b03"}, +] +celery = [ + {file = "celery-4.4.2-py2.py3-none-any.whl", hash = "sha256:5b4b37e276033fe47575107a2775469f0b721646a08c96ec2c61531e4fe45f2a"}, + {file = "celery-4.4.2.tar.gz", hash = "sha256:108a0bf9018a871620936c33a3ee9f6336a89f8ef0a0f567a9001f4aa361415f"}, +] +certifi = [ + {file = "certifi-2020.4.5.1-py2.py3-none-any.whl", hash = "sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304"}, + {file = "certifi-2020.4.5.1.tar.gz", hash = "sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519"}, +] +chardet = [ + {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, + {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, +] +click = [ + {file = "click-7.1.1-py2.py3-none-any.whl", hash = "sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a"}, + {file = "click-7.1.1.tar.gz", hash = "sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc"}, +] +coreapi = [ + {file = "coreapi-2.3.3-py2.py3-none-any.whl", hash = "sha256:bf39d118d6d3e171f10df9ede5666f63ad80bba9a29a8ec17726a66cf52ee6f3"}, + {file = "coreapi-2.3.3.tar.gz", hash = "sha256:46145fcc1f7017c076a2ef684969b641d18a2991051fddec9458ad3f78ffc1cb"}, +] +coreschema = [ + {file = "coreschema-0.0.4-py2-none-any.whl", hash = "sha256:5e6ef7bf38c1525d5e55a895934ab4273548629f16aed5c0a6caa74ebf45551f"}, + {file = "coreschema-0.0.4.tar.gz", hash = "sha256:9503506007d482ab0867ba14724b93c18a33b22b6d19fb419ef2d239dd4a1607"}, +] +coverage = [ + {file = "coverage-5.1-cp27-cp27m-macosx_10_12_x86_64.whl", hash = "sha256:0cb4be7e784dcdc050fc58ef05b71aa8e89b7e6636b99967fadbdba694cf2b65"}, + {file = "coverage-5.1-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:c317eaf5ff46a34305b202e73404f55f7389ef834b8dbf4da09b9b9b37f76dd2"}, + {file = "coverage-5.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:b83835506dfc185a319031cf853fa4bb1b3974b1f913f5bb1a0f3d98bdcded04"}, + {file = "coverage-5.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5f2294dbf7875b991c381e3d5af2bcc3494d836affa52b809c91697449d0eda6"}, + {file = "coverage-5.1-cp27-cp27m-win32.whl", hash = "sha256:de807ae933cfb7f0c7d9d981a053772452217df2bf38e7e6267c9cbf9545a796"}, + {file = "coverage-5.1-cp27-cp27m-win_amd64.whl", hash = "sha256:bf9cb9a9fd8891e7efd2d44deb24b86d647394b9705b744ff6f8261e6f29a730"}, + {file = "coverage-5.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:acf3763ed01af8410fc36afea23707d4ea58ba7e86a8ee915dfb9ceff9ef69d0"}, + {file = "coverage-5.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:dec5202bfe6f672d4511086e125db035a52b00f1648d6407cc8e526912c0353a"}, + {file = "coverage-5.1-cp35-cp35m-macosx_10_12_x86_64.whl", hash = "sha256:7a5bdad4edec57b5fb8dae7d3ee58622d626fd3a0be0dfceda162a7035885ecf"}, + {file = "coverage-5.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:1601e480b9b99697a570cea7ef749e88123c04b92d84cedaa01e117436b4a0a9"}, + {file = "coverage-5.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:dbe8c6ae7534b5b024296464f387d57c13caa942f6d8e6e0346f27e509f0f768"}, + {file = "coverage-5.1-cp35-cp35m-win32.whl", hash = "sha256:a027ef0492ede1e03a8054e3c37b8def89a1e3c471482e9f046906ba4f2aafd2"}, + {file = "coverage-5.1-cp35-cp35m-win_amd64.whl", hash = "sha256:0e61d9803d5851849c24f78227939c701ced6704f337cad0a91e0972c51c1ee7"}, + {file = "coverage-5.1-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:2d27a3f742c98e5c6b461ee6ef7287400a1956c11421eb574d843d9ec1f772f0"}, + {file = "coverage-5.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:66460ab1599d3cf894bb6baee8c684788819b71a5dc1e8fa2ecc152e5d752019"}, + {file = "coverage-5.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:5c542d1e62eece33c306d66fe0a5c4f7f7b3c08fecc46ead86d7916684b36d6c"}, + {file = "coverage-5.1-cp36-cp36m-win32.whl", hash = "sha256:2742c7515b9eb368718cd091bad1a1b44135cc72468c731302b3d641895b83d1"}, + {file = "coverage-5.1-cp36-cp36m-win_amd64.whl", hash = "sha256:dead2ddede4c7ba6cb3a721870f5141c97dc7d85a079edb4bd8d88c3ad5b20c7"}, + {file = "coverage-5.1-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:01333e1bd22c59713ba8a79f088b3955946e293114479bbfc2e37d522be03355"}, + {file = "coverage-5.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:e1ea316102ea1e1770724db01998d1603ed921c54a86a2efcb03428d5417e489"}, + {file = "coverage-5.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:adeb4c5b608574a3d647011af36f7586811a2c1197c861aedb548dd2453b41cd"}, + {file = "coverage-5.1-cp37-cp37m-win32.whl", hash = "sha256:782caea581a6e9ff75eccda79287daefd1d2631cc09d642b6ee2d6da21fc0a4e"}, + {file = "coverage-5.1-cp37-cp37m-win_amd64.whl", hash = "sha256:00f1d23f4336efc3b311ed0d807feb45098fc86dee1ca13b3d6768cdab187c8a"}, + {file = "coverage-5.1-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:402e1744733df483b93abbf209283898e9f0d67470707e3c7516d84f48524f55"}, + {file = "coverage-5.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:a3f3654d5734a3ece152636aad89f58afc9213c6520062db3978239db122f03c"}, + {file = "coverage-5.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:6402bd2fdedabbdb63a316308142597534ea8e1895f4e7d8bf7476c5e8751fef"}, + {file = "coverage-5.1-cp38-cp38-win32.whl", hash = "sha256:8fa0cbc7ecad630e5b0f4f35b0f6ad419246b02bc750de7ac66db92667996d24"}, + {file = "coverage-5.1-cp38-cp38-win_amd64.whl", hash = "sha256:79a3cfd6346ce6c13145731d39db47b7a7b859c0272f02cdb89a3bdcbae233a0"}, + {file = "coverage-5.1-cp39-cp39-win32.whl", hash = "sha256:a82b92b04a23d3c8a581fc049228bafde988abacba397d57ce95fe95e0338ab4"}, + {file = "coverage-5.1-cp39-cp39-win_amd64.whl", hash = "sha256:bb28a7245de68bf29f6fb199545d072d1036a1917dca17a1e75bbb919e14ee8e"}, + {file = "coverage-5.1.tar.gz", hash = "sha256:f90bfc4ad18450c80b024036eaf91e4a246ae287701aaa88eaebebf150868052"}, +] +django = [ + {file = "Django-3.0.5-py3-none-any.whl", hash = "sha256:642d8eceab321ca743ae71e0f985ff8fdca59f07aab3a9fb362c617d23e33a76"}, + {file = "Django-3.0.5.tar.gz", hash = "sha256:d4666c2edefa38c5ede0ec1655424c56dc47ceb04b6d8d62a7eac09db89545c1"}, +] +django-appconf = [ + {file = "django-appconf-1.0.4.tar.gz", hash = "sha256:be58deb54a43d77d2e1621fe59f787681376d3cd0b8bd8e4758ef6c3a6453380"}, + {file = "django_appconf-1.0.4-py2.py3-none-any.whl", hash = "sha256:1b1d0e1069c843ebe8ae5aa48ec52403b1440402b320c3e3a206a0907e97bb06"}, +] +django-axes = [ + {file = "django-axes-5.3.1.tar.gz", hash = "sha256:23eee8297dfcb5aa780e4925f58d723387afe8ecc8fd6a7e9522d26c95c7b880"}, + {file = "django_axes-5.3.1-py3-none-any.whl", hash = "sha256:49fa9736cbbf7d83a61ed57f7b2ebd65f8d3064bb0c45b945bfa7421288031a1"}, +] +django-celery-beat = [ + {file = "django-celery-beat-2.0.0.tar.gz", hash = "sha256:fdf1255eecfbeb770c6521fe3e69989dfc6373cd5a7f0fe62038d37f80f47e48"}, + {file = "django_celery_beat-2.0.0-py2.py3-none-any.whl", hash = "sha256:fe0b2a1b31d4a6234fea4b31986ddfd4644a48fab216ce1843f3ed0ddd2e9097"}, +] +django-debug-toolbar = [ + {file = "django-debug-toolbar-2.2.tar.gz", hash = "sha256:eabbefe89881bbe4ca7c980ff102e3c35c8e8ad6eb725041f538988f2f39a943"}, + {file = "django_debug_toolbar-2.2-py3-none-any.whl", hash = "sha256:ff94725e7aae74b133d0599b9bf89bd4eb8f5d2c964106e61d11750228c8774c"}, +] +django-extensions = [ + {file = "django-extensions-2.2.9.tar.gz", hash = "sha256:2f81b618ba4d1b0e58603e25012e5c74f88a4b706e0022a3b21f24f0322a6ce6"}, + {file = "django_extensions-2.2.9-py2.py3-none-any.whl", hash = "sha256:b19182d101a441fe001c5753553a901e2ef3ff60e8fbbe38881eb4a61fdd17c4"}, +] +django-ipware = [ + {file = "django-ipware-2.1.0.tar.gz", hash = "sha256:a7c7a8fd019dbdc9c357e6e582f65034e897572fc79a7e467674efa8aef9d00b"}, +] +django-registration-redux = [ + {file = "django-registration-redux-2.7.tar.gz", hash = "sha256:1aaf08c9c16b7f185ffa36e7251c7a6149fe953f5af21c4f1e01cbe03902520b"}, + {file = "django_registration_redux-2.7-py2.py3-none-any.whl", hash = "sha256:5998a8dbee2a84d66cd56a61c4fb6b1a5be801b083fb1ef53ba04939d8a44606"}, +] +django-timezone-field = [ + {file = "django-timezone-field-4.0.tar.gz", hash = "sha256:7e3620fe2211c2d372fad54db8f86ff884098d018d56fda4dca5e64929e05ffc"}, + {file = "django_timezone_field-4.0-py3-none-any.whl", hash = "sha256:758b7d41084e9ea2e89e59eb616e9b6326e6fbbf9d14b6ef062d624fe8cc6246"}, +] +djangorestframework = [ + {file = "djangorestframework-3.11.0-py3-none-any.whl", hash = "sha256:05809fc66e1c997fd9a32ea5730d9f4ba28b109b9da71fccfa5ff241201fd0a4"}, + {file = "djangorestframework-3.11.0.tar.gz", hash = "sha256:e782087823c47a26826ee5b6fa0c542968219263fb3976ec3c31edab23a4001f"}, +] +drf-yasg = [ + {file = "drf-yasg-1.17.1.tar.gz", hash = "sha256:5572e9d5baab9f6b49318169df9789f7399d0e3c7bdac8fdb8dfccf1d5d2b1ca"}, + {file = "drf_yasg-1.17.1-py2.py3-none-any.whl", hash = "sha256:7d7af27ad16e18507e9392b2afd6b218fbffc432ec8dbea053099a2241e184ff"}, +] +factory-boy = [ + {file = "factory_boy-2.12.0-py2.py3-none-any.whl", hash = "sha256:728df59b372c9588b83153facf26d3d28947fc750e8e3c95cefa9bed0e6394ee"}, + {file = "factory_boy-2.12.0.tar.gz", hash = "sha256:faf48d608a1735f0d0a3c9cbf536d64f9132b547dae7ba452c4d99a79e84a370"}, +] +faker = [ + {file = "Faker-4.0.2-py3-none-any.whl", hash = "sha256:b89aa33837498498e15c709eb40c31386408a901a53c7a5e12a425737a767976"}, + {file = "Faker-4.0.2.tar.gz", hash = "sha256:2d3f866ef25e1a5af80e7b0ceeacc3c92dec5d0fdbad3e2cb6adf6e60b22188f"}, +] +feedparser = [ + {file = "feedparser-5.2.1.tar.bz2", hash = "sha256:ce875495c90ebd74b179855449040003a1beb40cd13d5f037a0654251e260b02"}, + {file = "feedparser-5.2.1.tar.gz", hash = "sha256:bd030652c2d08532c034c27fcd7c85868e7fa3cb2b17f230a44a6bbc92519bf9"}, + {file = "feedparser-5.2.1.zip", hash = "sha256:cd2485472e41471632ed3029d44033ee420ad0b57111db95c240c9160a85831c"}, +] +freezegun = [ + {file = "freezegun-0.3.15-py2.py3-none-any.whl", hash = "sha256:82c757a05b7c7ca3e176bfebd7d6779fd9139c7cb4ef969c38a28d74deef89b2"}, + {file = "freezegun-0.3.15.tar.gz", hash = "sha256:e2062f2c7f95cc276a834c22f1a17179467176b624cc6f936e8bc3be5535ad1b"}, +] +gunicorn = [ + {file = "gunicorn-20.0.4-py2.py3-none-any.whl", hash = "sha256:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c"}, + {file = "gunicorn-20.0.4.tar.gz", hash = "sha256:1904bb2b8a43658807108d59c3f3d56c2b6121a701161de0ddf9ad140073c626"}, +] +idna = [ + {file = "idna-2.9-py2.py3-none-any.whl", hash = "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"}, + {file = "idna-2.9.tar.gz", hash = "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb"}, +] +importlib-metadata = [ + {file = "importlib_metadata-1.6.0-py2.py3-none-any.whl", hash = "sha256:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f"}, + {file = "importlib_metadata-1.6.0.tar.gz", hash = "sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e"}, +] +inflection = [ + {file = "inflection-0.4.0-py2.py3-none-any.whl", hash = "sha256:9a15d3598f01220e93f2207c432cfede50daff53137ce660fb8be838ef1ca6cc"}, + {file = "inflection-0.4.0.tar.gz", hash = "sha256:32a5c3341d9583ec319548b9015b7fbdf8c429cbcb575d326c33ae3a0e90d52c"}, +] +isort = [ + {file = "isort-4.3.21-py2.py3-none-any.whl", hash = "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"}, + {file = "isort-4.3.21.tar.gz", hash = "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1"}, +] +itypes = [ + {file = "itypes-1.1.0.tar.gz", hash = "sha256:c6e77bb9fd68a4bfeb9d958fea421802282451a25bac4913ec94db82a899c073"}, +] +jinja2 = [ + {file = "Jinja2-2.11.1-py2.py3-none-any.whl", hash = "sha256:b0eaf100007721b5c16c1fc1eecb87409464edc10469ddc9a22a27a99123be49"}, + {file = "Jinja2-2.11.1.tar.gz", hash = "sha256:93187ffbc7808079673ef52771baa950426fd664d3aad1d0fa3e95644360e250"}, +] +kombu = [ + {file = "kombu-4.6.8-py2.py3-none-any.whl", hash = "sha256:598e7e749d6ab54f646b74b2d2df67755dee13894f73ab02a2a9feb8870c7cb2"}, + {file = "kombu-4.6.8.tar.gz", hash = "sha256:2d1cda774126a044d91a7ff5fa6d09edf99f46924ab332a810760fe6740e9b76"}, +] +lxml = [ + {file = "lxml-4.5.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:0701f7965903a1c3f6f09328c1278ac0eee8f56f244e66af79cb224b7ef3801c"}, + {file = "lxml-4.5.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:06d4e0bbb1d62e38ae6118406d7cdb4693a3fa34ee3762238bcb96c9e36a93cd"}, + {file = "lxml-4.5.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5828c7f3e615f3975d48f40d4fe66e8a7b25f16b5e5705ffe1d22e43fb1f6261"}, + {file = "lxml-4.5.0-cp27-cp27m-win32.whl", hash = "sha256:afdb34b715daf814d1abea0317b6d672476b498472f1e5aacbadc34ebbc26e89"}, + {file = "lxml-4.5.0-cp27-cp27m-win_amd64.whl", hash = "sha256:585c0869f75577ac7a8ff38d08f7aac9033da2c41c11352ebf86a04652758b7a"}, + {file = "lxml-4.5.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:8a0ebda56ebca1a83eb2d1ac266649b80af8dd4b4a3502b2c1e09ac2f88fe128"}, + {file = "lxml-4.5.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:fe976a0f1ef09b3638778024ab9fb8cde3118f203364212c198f71341c0715ca"}, + {file = "lxml-4.5.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:7bc1b221e7867f2e7ff1933165c0cec7153dce93d0cdba6554b42a8beb687bdb"}, + {file = "lxml-4.5.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:d068f55bda3c2c3fcaec24bd083d9e2eede32c583faf084d6e4b9daaea77dde8"}, + {file = "lxml-4.5.0-cp35-cp35m-win32.whl", hash = "sha256:e4aa948eb15018a657702fee0b9db47e908491c64d36b4a90f59a64741516e77"}, + {file = "lxml-4.5.0-cp35-cp35m-win_amd64.whl", hash = "sha256:1f2c4ec372bf1c4a2c7e4bb20845e8bcf8050365189d86806bad1e3ae473d081"}, + {file = "lxml-4.5.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:5d467ce9c5d35b3bcc7172c06320dddb275fea6ac2037f72f0a4d7472035cea9"}, + {file = "lxml-4.5.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:95e67224815ef86924fbc2b71a9dbd1f7262384bca4bc4793645794ac4200717"}, + {file = "lxml-4.5.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ebec08091a22c2be870890913bdadd86fcd8e9f0f22bcb398abd3af914690c15"}, + {file = "lxml-4.5.0-cp36-cp36m-win32.whl", hash = "sha256:deadf4df349d1dcd7b2853a2c8796593cc346600726eff680ed8ed11812382a7"}, + {file = "lxml-4.5.0-cp36-cp36m-win_amd64.whl", hash = "sha256:f2b74784ed7e0bc2d02bd53e48ad6ba523c9b36c194260b7a5045071abbb1012"}, + {file = "lxml-4.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fa071559f14bd1e92077b1b5f6c22cf09756c6de7139370249eb372854ce51e6"}, + {file = "lxml-4.5.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:edc15fcfd77395e24543be48871c251f38132bb834d9fdfdad756adb6ea37679"}, + {file = "lxml-4.5.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:fd52e796fee7171c4361d441796b64df1acfceb51f29e545e812f16d023c4bbc"}, + {file = "lxml-4.5.0-cp37-cp37m-win32.whl", hash = "sha256:90ed0e36455a81b25b7034038e40880189169c308a3df360861ad74da7b68c1a"}, + {file = "lxml-4.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:df533af6f88080419c5a604d0d63b2c33b1c0c4409aba7d0cb6de305147ea8c8"}, + {file = "lxml-4.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b4b2c63cc7963aedd08a5f5a454c9f67251b1ac9e22fd9d72836206c42dc2a72"}, + {file = "lxml-4.5.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e5d842c73e4ef6ed8c1bd77806bf84a7cb535f9c0cf9b2c74d02ebda310070e1"}, + {file = "lxml-4.5.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:63dbc21efd7e822c11d5ddbedbbb08cd11a41e0032e382a0fd59b0b08e405a3a"}, + {file = "lxml-4.5.0-cp38-cp38-win32.whl", hash = "sha256:4235bc124fdcf611d02047d7034164897ade13046bda967768836629bc62784f"}, + {file = "lxml-4.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:d5b3c4b7edd2e770375a01139be11307f04341ec709cf724e0f26ebb1eef12c3"}, + {file = "lxml-4.5.0.tar.gz", hash = "sha256:8620ce80f50d023d414183bf90cc2576c2837b88e00bea3f33ad2630133bbb60"}, +] +markupsafe = [ + {file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-win32.whl", hash = "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e"}, + {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f"}, + {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-win32.whl", hash = "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-win_amd64.whl", hash = "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"}, + {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, +] +packaging = [ + {file = "packaging-20.3-py2.py3-none-any.whl", hash = "sha256:82f77b9bee21c1bafbf35a84905d604d5d1223801d639cf3ed140bd651c08752"}, + {file = "packaging-20.3.tar.gz", hash = "sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3"}, +] +psycopg2-binary = [ + {file = "psycopg2-binary-2.8.5.tar.gz", hash = "sha256:ccdc6a87f32b491129ada4b87a43b1895cf2c20fdb7f98ad979647506ffc41b6"}, + {file = "psycopg2_binary-2.8.5-cp27-cp27m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:96d3038f5bd061401996614f65d27a4ecb62d843eb4f48e212e6d129171a721f"}, + {file = "psycopg2_binary-2.8.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:08507efbe532029adee21b8d4c999170a83760d38249936038bd0602327029b5"}, + {file = "psycopg2_binary-2.8.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:b9a8b391c2b0321e0cd7ec6b4cfcc3dd6349347bd1207d48bcb752aa6c553a66"}, + {file = "psycopg2_binary-2.8.5-cp27-cp27m-win32.whl", hash = "sha256:3286541b9d85a340ee4ed42732d15fc1bb441dc500c97243a768154ab8505bb5"}, + {file = "psycopg2_binary-2.8.5-cp27-cp27m-win_amd64.whl", hash = "sha256:008da3ab51adc70a5f1cfbbe5db3a22607ab030eb44bcecf517ad11a0c2b3cac"}, + {file = "psycopg2_binary-2.8.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:ba13346ff6d3eb2dca0b6fa0d8a9d999eff3dcd9b55f3a890f12b0b6362b2b38"}, + {file = "psycopg2_binary-2.8.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:c8830b7d5f16fd79d39b21e3d94f247219036b29b30c8270314c46bf8b732389"}, + {file = "psycopg2_binary-2.8.5-cp34-cp34m-win32.whl", hash = "sha256:51f7823f1b087d2020d8e8c9e6687473d3d239ba9afc162d9b2ab6e80b53f9f9"}, + {file = "psycopg2_binary-2.8.5-cp34-cp34m-win_amd64.whl", hash = "sha256:107d9be3b614e52a192719c6bf32e8813030020ea1d1215daa86ded9a24d8b04"}, + {file = "psycopg2_binary-2.8.5-cp35-cp35m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:930315ac53dc65cbf52ab6b6d27422611f5fb461d763c531db229c7e1af6c0b3"}, + {file = "psycopg2_binary-2.8.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:6bb2dd006a46a4a4ce95201f836194eb6a1e863f69ee5bab506673e0ca767057"}, + {file = "psycopg2_binary-2.8.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:3939cf75fc89c5e9ed836e228c4a63604dff95ad19aed2bbf71d5d04c15ed5ce"}, + {file = "psycopg2_binary-2.8.5-cp35-cp35m-win32.whl", hash = "sha256:a20299ee0ea2f9cca494396ac472d6e636745652a64a418b39522c120fd0a0a4"}, + {file = "psycopg2_binary-2.8.5-cp35-cp35m-win_amd64.whl", hash = "sha256:cc30cb900f42c8a246e2cb76539d9726f407330bc244ca7729c41a44e8d807fb"}, + {file = "psycopg2_binary-2.8.5-cp36-cp36m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:40abc319f7f26c042a11658bf3dd3b0b3bceccf883ec1c565d5c909a90204434"}, + {file = "psycopg2_binary-2.8.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:702f09d8f77dc4794651f650828791af82f7c2efd8c91ae79e3d9fe4bb7d4c98"}, + {file = "psycopg2_binary-2.8.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d1a8b01f6a964fec702d6b6dac1f91f2b9f9fe41b310cbb16c7ef1fac82df06d"}, + {file = "psycopg2_binary-2.8.5-cp36-cp36m-win32.whl", hash = "sha256:17a0ea0b0eabf07035e5e0d520dabc7950aeb15a17c6d36128ba99b2721b25b1"}, + {file = "psycopg2_binary-2.8.5-cp36-cp36m-win_amd64.whl", hash = "sha256:e004db88e5a75e5fdab1620fb9f90c9598c2a195a594225ac4ed2a6f1c23e162"}, + {file = "psycopg2_binary-2.8.5-cp37-cp37m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:a34826d6465c2e2bbe9d0605f944f19d2480589f89863ed5f091943be27c9de4"}, + {file = "psycopg2_binary-2.8.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:cac918cd7c4c498a60f5d2a61d4f0a6091c2c9490d81bc805c963444032d0dab"}, + {file = "psycopg2_binary-2.8.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:7b832d76cc65c092abd9505cc670c4e3421fd136fb6ea5b94efbe4c146572505"}, + {file = "psycopg2_binary-2.8.5-cp37-cp37m-win32.whl", hash = "sha256:bb0608694a91db1e230b4a314e8ed00ad07ed0c518f9a69b83af2717e31291a3"}, + {file = "psycopg2_binary-2.8.5-cp37-cp37m-win_amd64.whl", hash = "sha256:eb2f43ae3037f1ef5e19339c41cf56947021ac892f668765cd65f8ab9814192e"}, + {file = "psycopg2_binary-2.8.5-cp38-cp38-macosx_10_9_x86_64.macosx_10_9_intel.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:07cf82c870ec2d2ce94d18e70c13323c89f2f2a2628cbf1feee700630be2519a"}, + {file = "psycopg2_binary-2.8.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:a69970ee896e21db4c57e398646af9edc71c003bc52a3cc77fb150240fefd266"}, + {file = "psycopg2_binary-2.8.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7036ccf715925251fac969f4da9ad37e4b7e211b1e920860148a10c0de963522"}, + {file = "psycopg2_binary-2.8.5-cp38-cp38-win32.whl", hash = "sha256:8f74e631b67482d504d7e9cf364071fc5d54c28e79a093ff402d5f8f81e23bfa"}, + {file = "psycopg2_binary-2.8.5-cp38-cp38-win_amd64.whl", hash = "sha256:fa466306fcf6b39b8a61d003123d442b23707d635a5cb05ac4e1b62cc79105cd"}, +] +pyflakes = [ + {file = "pyflakes-2.2.0-py2.py3-none-any.whl", hash = "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92"}, + {file = "pyflakes-2.2.0.tar.gz", hash = "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"}, +] +pyparsing = [ + {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, + {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, +] +python-crontab = [ + {file = "python-crontab-2.4.1.tar.gz", hash = "sha256:2366c7aa373118315de7c082401907bacd28e8b1e4e0a6d702334d17b89e71aa"}, +] +python-dateutil = [ + {file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"}, + {file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"}, +] +python-dotenv = [ + {file = "python-dotenv-0.12.0.tar.gz", hash = "sha256:92b3123fb2d58a284f76cc92bfe4ee6c502c32ded73e8b051c4f6afc8b6751ed"}, + {file = "python_dotenv-0.12.0-py2.py3-none-any.whl", hash = "sha256:81822227f771e0cab235a2939f0f265954ac4763cafd806d845801c863bf372f"}, +] +python-memcached = [ + {file = "python-memcached-1.59.tar.gz", hash = "sha256:a2e28637be13ee0bf1a8b6843e7490f9456fd3f2a4cb60471733c7b5d5557e4f"}, + {file = "python_memcached-1.59-py2.py3-none-any.whl", hash = "sha256:4dac64916871bd3550263323fc2ce18e1e439080a2d5670c594cf3118d99b594"}, +] +pytz = [ + {file = "pytz-2019.3-py2.py3-none-any.whl", hash = "sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d"}, + {file = "pytz-2019.3.tar.gz", hash = "sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"}, +] +requests = [ + {file = "requests-2.23.0-py2.py3-none-any.whl", hash = "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee"}, + {file = "requests-2.23.0.tar.gz", hash = "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6"}, +] +"ruamel.yaml" = [ + {file = "ruamel.yaml-0.16.10-py2.py3-none-any.whl", hash = "sha256:0962fd7999e064c4865f96fb1e23079075f4a2a14849bcdc5cdba53a24f9759b"}, + {file = "ruamel.yaml-0.16.10.tar.gz", hash = "sha256:099c644a778bf72ffa00524f78dd0b6476bca94a1da344130f4bf3381ce5b954"}, +] +"ruamel.yaml.clib" = [ + {file = "ruamel.yaml.clib-0.2.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:9c6d040d0396c28d3eaaa6cb20152cb3b2f15adf35a0304f4f40a3cf9f1d2448"}, + {file = "ruamel.yaml.clib-0.2.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:4d55386129291b96483edcb93b381470f7cd69f97585829b048a3d758d31210a"}, + {file = "ruamel.yaml.clib-0.2.0-cp27-cp27m-win32.whl", hash = "sha256:8073c8b92b06b572e4057b583c3d01674ceaf32167801fe545a087d7a1e8bf52"}, + {file = "ruamel.yaml.clib-0.2.0-cp27-cp27m-win_amd64.whl", hash = "sha256:615b0396a7fad02d1f9a0dcf9f01202bf9caefee6265198f252c865f4227fcc6"}, + {file = "ruamel.yaml.clib-0.2.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:a0ff786d2a7dbe55f9544b3f6ebbcc495d7e730df92a08434604f6f470b899c5"}, + {file = "ruamel.yaml.clib-0.2.0-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:ea4362548ee0cbc266949d8a441238d9ad3600ca9910c3fe4e82ee3a50706973"}, + {file = "ruamel.yaml.clib-0.2.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:77556a7aa190be9a2bd83b7ee075d3df5f3c5016d395613671487e79b082d784"}, + {file = "ruamel.yaml.clib-0.2.0-cp35-cp35m-win32.whl", hash = "sha256:392b7c371312abf27fb549ec2d5e0092f7ef6e6c9f767bfb13e83cb903aca0fd"}, + {file = "ruamel.yaml.clib-0.2.0-cp35-cp35m-win_amd64.whl", hash = "sha256:ed5b3698a2bb241b7f5cbbe277eaa7fe48b07a58784fba4f75224fd066d253ad"}, + {file = "ruamel.yaml.clib-0.2.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:7aee724e1ff424757b5bd8f6c5bbdb033a570b2b4683b17ace4dbe61a99a657b"}, + {file = "ruamel.yaml.clib-0.2.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d0d3ac228c9bbab08134b4004d748cf9f8743504875b3603b3afbb97e3472947"}, + {file = "ruamel.yaml.clib-0.2.0-cp36-cp36m-win32.whl", hash = "sha256:f9dcc1ae73f36e8059589b601e8e4776b9976effd76c21ad6a855a74318efd6e"}, + {file = "ruamel.yaml.clib-0.2.0-cp36-cp36m-win_amd64.whl", hash = "sha256:1e77424825caba5553bbade750cec2277ef130647d685c2b38f68bc03453bac6"}, + {file = "ruamel.yaml.clib-0.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d10e9dd744cf85c219bf747c75194b624cc7a94f0c80ead624b06bfa9f61d3bc"}, + {file = "ruamel.yaml.clib-0.2.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:550168c02d8de52ee58c3d8a8193d5a8a9491a5e7b2462d27ac5bf63717574c9"}, + {file = "ruamel.yaml.clib-0.2.0-cp37-cp37m-win32.whl", hash = "sha256:57933a6986a3036257ad7bf283529e7c19c2810ff24c86f4a0cfeb49d2099919"}, + {file = "ruamel.yaml.clib-0.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:b1b7fcee6aedcdc7e62c3a73f238b3d080c7ba6650cd808bce8d7761ec484070"}, + {file = "ruamel.yaml.clib-0.2.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:be018933c2f4ee7de55e7bd7d0d801b3dfb09d21dad0cce8a97995fd3e44be30"}, + {file = "ruamel.yaml.clib-0.2.0.tar.gz", hash = "sha256:b66832ea8077d9b3f6e311c4a53d06273db5dc2db6e8a908550f3c14d67e718c"}, +] +six = [ + {file = "six-1.14.0-py2.py3-none-any.whl", hash = "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"}, + {file = "six-1.14.0.tar.gz", hash = "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a"}, +] +soupsieve = [ + {file = "soupsieve-1.9.5-py2.py3-none-any.whl", hash = "sha256:bdb0d917b03a1369ce964056fc195cfdff8819c40de04695a80bc813c3cfa1f5"}, + {file = "soupsieve-1.9.5.tar.gz", hash = "sha256:e2c1c5dee4a1c36bcb790e0fabd5492d874b8ebd4617622c4f6a731701060dda"}, +] +sqlparse = [ + {file = "sqlparse-0.3.1-py2.py3-none-any.whl", hash = "sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e"}, + {file = "sqlparse-0.3.1.tar.gz", hash = "sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548"}, +] +tblib = [ + {file = "tblib-1.6.0-py2.py3-none-any.whl", hash = "sha256:e222f44485d45ed13fada73b57775e2ff9bd8af62160120bbb6679f5ad80315b"}, + {file = "tblib-1.6.0.tar.gz", hash = "sha256:229bee3754cb5d98b4837dd5c4405e80cfab57cb9f93220410ad367f8b352344"}, +] +text-unidecode = [ + {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"}, + {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, +] +toml = [ + {file = "toml-0.10.0-py2.7.egg", hash = "sha256:f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"}, + {file = "toml-0.10.0-py2.py3-none-any.whl", hash = "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"}, + {file = "toml-0.10.0.tar.gz", hash = "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c"}, +] +uritemplate = [ + {file = "uritemplate-3.0.1-py2.py3-none-any.whl", hash = "sha256:07620c3f3f8eed1f12600845892b0e036a2420acf513c53f7de0abd911a5894f"}, + {file = "uritemplate-3.0.1.tar.gz", hash = "sha256:5af8ad10cec94f215e3f48112de2022e1d5a37ed427fbd88652fa908f2ab7cae"}, +] +urllib3 = [ + {file = "urllib3-1.25.8-py2.py3-none-any.whl", hash = "sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc"}, + {file = "urllib3-1.25.8.tar.gz", hash = "sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc"}, +] +vine = [ + {file = "vine-1.3.0-py2.py3-none-any.whl", hash = "sha256:ea4947cc56d1fd6f2095c8d543ee25dad966f78692528e68b4fada11ba3f98af"}, + {file = "vine-1.3.0.tar.gz", hash = "sha256:133ee6d7a9016f177ddeaf191c1f58421a1dcc6ee9a42c58b34bed40e1d2cd87"}, +] +webencodings = [ + {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, + {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, +] +zipp = [ + {file = "zipp-3.1.0-py3-none-any.whl", hash = "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b"}, + {file = "zipp-3.1.0.tar.gz", hash = "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96"}, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..047c544 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,40 @@ +[tool.poetry] +name = "newsreader" +version = "0.2" +description = "Webapplication for reading RSS feeds" +authors = ["Sonny "] +license = "GPL-3.0" + +[tool.poetry.dependencies] +python = "^3.7" +bleach = "^3.1.4" +Django = "^3.0.5" +celery = "^4.4.2" +beautifulsoup4 = "^4.9.0" +django-axes = "^5.3.1" +django-celery-beat = "^2.0.0" +djangorestframework = "^3.11.0" +drf-yasg = "^1.17.1" +django-registration-redux = "^2.7" +lxml = "^4.5.0" +feedparser = "^5.2.1" +python-memcached = "^1.59" +requests = "^2.23.0" +psycopg2-binary = "^2.8.5" +gunicorn = "^20.0.4" +python-dotenv = "^0.12.0" + +[tool.poetry.dev-dependencies] +factory-boy = "^2.12.0" +freezegun = "^0.3.15" +django-debug-toolbar = "^2.2" +django-extensions = "^2.2.9" +black = "19.3b0" +isort = "4.3.21" +autoflake = "1.3.1" +tblib = "1.6.0" +coverage = "^5.1" + +[build-system] +requires = ["poetry>=0.12"] +build-backend = "poetry.masonry.api" diff --git a/src/entrypoint.sh b/src/entrypoint.sh new file mode 100755 index 0000000..451b7d3 --- /dev/null +++ b/src/entrypoint.sh @@ -0,0 +1,5 @@ +#!/bin/bash +# This file should only be used in conjuction with docker-compose + +poetry run /app/src/manage.py migrate +poetry run /app/src/manage.py runserver 0.0.0.0:8000 diff --git a/src/manage.py b/src/manage.py new file mode 100755 index 0000000..45fc02f --- /dev/null +++ b/src/manage.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "newsreader.conf.dev") + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == "__main__": + main() diff --git a/src/newsreader/__init__.py b/src/newsreader/__init__.py new file mode 100644 index 0000000..c08c1d6 --- /dev/null +++ b/src/newsreader/__init__.py @@ -0,0 +1,4 @@ +from .celery import app as celery_app + + +__all__ = ["celery_app"] diff --git a/src/newsreader/accounts/__init__.py b/src/newsreader/accounts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/newsreader/accounts/admin.py b/src/newsreader/accounts/admin.py new file mode 100644 index 0000000..846f6b4 --- /dev/null +++ b/src/newsreader/accounts/admin.py @@ -0,0 +1 @@ +# Register your models here. diff --git a/src/newsreader/accounts/apps.py b/src/newsreader/accounts/apps.py new file mode 100644 index 0000000..fb0257e --- /dev/null +++ b/src/newsreader/accounts/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class AccountsConfig(AppConfig): + name = "accounts" diff --git a/src/newsreader/accounts/migrations/0001_initial.py b/src/newsreader/accounts/migrations/0001_initial.py new file mode 100644 index 0000000..17b5729 --- /dev/null +++ b/src/newsreader/accounts/migrations/0001_initial.py @@ -0,0 +1,151 @@ +# Generated by Django 2.2 on 2019-07-14 10:36 + +import django.contrib.auth.models +import django.contrib.auth.validators +import django.utils.timezone + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("auth", "0011_update_proxy_permissions"), + ("django_celery_beat", "0011_auto_20190508_0153"), + ] + + operations = [ + migrations.CreateModel( + name="User", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("password", models.CharField(max_length=128, verbose_name="password")), + ( + "last_login", + models.DateTimeField( + blank=True, null=True, verbose_name="last login" + ), + ), + ( + "is_superuser", + models.BooleanField( + default=False, + help_text="Designates that this user has all permissions without explicitly assigning them.", + verbose_name="superuser status", + ), + ), + ( + "username", + models.CharField( + error_messages={ + "unique": "A user with that username already exists." + }, + help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", + max_length=150, + unique=True, + validators=[ + django.contrib.auth.validators.UnicodeUsernameValidator() + ], + verbose_name="username", + ), + ), + ( + "first_name", + models.CharField( + blank=True, max_length=30, verbose_name="first name" + ), + ), + ( + "last_name", + models.CharField( + blank=True, max_length=150, verbose_name="last name" + ), + ), + ( + "is_staff", + models.BooleanField( + default=False, + help_text="Designates whether the user can log into this admin site.", + verbose_name="staff status", + ), + ), + ( + "is_active", + models.BooleanField( + default=True, + help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", + verbose_name="active", + ), + ), + ( + "date_joined", + models.DateTimeField( + default=django.utils.timezone.now, verbose_name="date joined" + ), + ), + ( + "email", + models.EmailField( + max_length=254, unique=True, verbose_name="email address" + ), + ), + ( + "groups", + models.ManyToManyField( + blank=True, + help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", + related_name="user_set", + related_query_name="user", + to="auth.Group", + verbose_name="groups", + ), + ), + ( + "task", + models.ForeignKey( + blank=True, + editable=False, + null=True, + on_delete=models.SET_NULL, + to="django_celery_beat.PeriodicTask", + ), + ), + ( + "task_interval", + models.ForeignKey( + blank=True, + null=True, + on_delete=models.SET_NULL, + to="django_celery_beat.IntervalSchedule", + ), + ), + ( + "user_permissions", + models.ManyToManyField( + blank=True, + help_text="Specific permissions for this user.", + related_name="user_set", + related_query_name="user", + to="auth.Permission", + verbose_name="user permissions", + ), + ), + ], + options={ + "verbose_name": "user", + "verbose_name_plural": "users", + "abstract": False, + }, + managers=[("objects", django.contrib.auth.models.UserManager())], + ) + ] diff --git a/src/newsreader/accounts/migrations/0002_remove_user_username.py b/src/newsreader/accounts/migrations/0002_remove_user_username.py new file mode 100644 index 0000000..b6848a3 --- /dev/null +++ b/src/newsreader/accounts/migrations/0002_remove_user_username.py @@ -0,0 +1,10 @@ +# Generated by Django 2.2 on 2019-07-14 10:37 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [("accounts", "0001_initial")] + + operations = [migrations.RemoveField(model_name="user", name="username")] diff --git a/src/newsreader/accounts/migrations/0003_auto_20190714_1417.py b/src/newsreader/accounts/migrations/0003_auto_20190714_1417.py new file mode 100644 index 0000000..3d55f65 --- /dev/null +++ b/src/newsreader/accounts/migrations/0003_auto_20190714_1417.py @@ -0,0 +1,17 @@ +# Generated by Django 2.2 on 2019-07-14 14:17 + +from django.db import migrations + +import newsreader.accounts.models + + +class Migration(migrations.Migration): + + dependencies = [("accounts", "0002_remove_user_username")] + + operations = [ + migrations.AlterModelManagers( + name="user", + managers=[("objects", newsreader.accounts.models.UserManager())], + ) + ] diff --git a/src/newsreader/accounts/migrations/0004_auto_20190714_1501.py b/src/newsreader/accounts/migrations/0004_auto_20190714_1501.py new file mode 100644 index 0000000..69a78e3 --- /dev/null +++ b/src/newsreader/accounts/migrations/0004_auto_20190714_1501.py @@ -0,0 +1,22 @@ +# Generated by Django 2.2 on 2019-07-14 15:01 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [("accounts", "0003_auto_20190714_1417")] + + operations = [ + migrations.AlterField( + model_name="user", + name="task", + field=models.OneToOneField( + blank=True, + editable=False, + null=True, + on_delete=models.SET_NULL, + to="django_celery_beat.PeriodicTask", + ), + ) + ] diff --git a/src/newsreader/accounts/migrations/0005_remove_user_task_interval.py b/src/newsreader/accounts/migrations/0005_remove_user_task_interval.py new file mode 100644 index 0000000..262ed44 --- /dev/null +++ b/src/newsreader/accounts/migrations/0005_remove_user_task_interval.py @@ -0,0 +1,10 @@ +# Generated by Django 2.2.6 on 2019-11-16 11:28 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [("accounts", "0004_auto_20190714_1501")] + + operations = [migrations.RemoveField(model_name="user", name="task_interval")] diff --git a/src/newsreader/accounts/migrations/0006_auto_20191116_1253.py b/src/newsreader/accounts/migrations/0006_auto_20191116_1253.py new file mode 100644 index 0000000..2afd7c4 --- /dev/null +++ b/src/newsreader/accounts/migrations/0006_auto_20191116_1253.py @@ -0,0 +1,24 @@ +# Generated by Django 2.2.6 on 2019-11-16 11:53 + +import django.db.models.deletion + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [("accounts", "0005_remove_user_task_interval")] + + operations = [ + migrations.AlterField( + model_name="user", + name="task", + field=models.OneToOneField( + blank=True, + editable=False, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="django_celery_beat.PeriodicTask", + ), + ) + ] diff --git a/src/newsreader/accounts/migrations/0007_auto_20191116_1255.py b/src/newsreader/accounts/migrations/0007_auto_20191116_1255.py new file mode 100644 index 0000000..eb1204a --- /dev/null +++ b/src/newsreader/accounts/migrations/0007_auto_20191116_1255.py @@ -0,0 +1,25 @@ +# Generated by Django 2.2.6 on 2019-11-16 11:55 + +import django.db.models.deletion + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [("accounts", "0006_auto_20191116_1253")] + + operations = [ + migrations.AlterField( + model_name="user", + name="task", + field=models.OneToOneField( + blank=True, + editable=False, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="django_celery_beat.PeriodicTask", + verbose_name="collection task", + ), + ) + ] diff --git a/src/newsreader/accounts/migrations/__init__.py b/src/newsreader/accounts/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/newsreader/accounts/models.py b/src/newsreader/accounts/models.py new file mode 100644 index 0000000..423b97b --- /dev/null +++ b/src/newsreader/accounts/models.py @@ -0,0 +1,80 @@ +import json + +from django.contrib.auth.models import AbstractUser +from django.contrib.auth.models import UserManager as DjangoUserManager +from django.db import models +from django.utils.translation import gettext as _ + +from django_celery_beat.models import IntervalSchedule, PeriodicTask + + +class UserManager(DjangoUserManager): + def _create_user(self, email, password, **extra_fields): + """ + Create and save a user with the given username, email, and password. + """ + if not email: + raise ValueError("The given email must be set") + email = self.normalize_email(email) + user = self.model(email=email, **extra_fields) + user.set_password(password) + user.save(using=self._db) + return user + + def create_user(self, email, password=None, **extra_fields): + extra_fields.setdefault("is_staff", False) + extra_fields.setdefault("is_superuser", False) + return self._create_user(email, password, **extra_fields) + + def create_superuser(self, email, password, **extra_fields): + extra_fields.setdefault("is_staff", True) + extra_fields.setdefault("is_superuser", True) + + if extra_fields.get("is_staff") is not True: + raise ValueError("Superuser must have is_staff=True.") + if extra_fields.get("is_superuser") is not True: + raise ValueError("Superuser must have is_superuser=True.") + + return self._create_user(email, password, **extra_fields) + + +class User(AbstractUser): + email = models.EmailField(_("email address"), unique=True) + + task = models.OneToOneField( + PeriodicTask, + on_delete=models.SET_NULL, + null=True, + blank=True, + editable=False, + verbose_name="collection task", + ) + + username = None + + objects = UserManager() + + USERNAME_FIELD = "email" + REQUIRED_FIELDS = [] + + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + + if not self.task: + task_interval, _ = IntervalSchedule.objects.get_or_create( + every=1, period=IntervalSchedule.HOURS + ) + + self.task, _ = PeriodicTask.objects.get_or_create( + enabled=True, + interval=task_interval, + name=f"{self.email}-collection-task", + task="newsreader.news.collection.tasks.collect", + args=json.dumps([self.pk]), + ) + + self.save() + + def delete(self, *args, **kwargs): + self.task.delete() + return super().delete(*args, **kwargs) diff --git a/src/newsreader/accounts/permissions.py b/src/newsreader/accounts/permissions.py new file mode 100644 index 0000000..2c6cf25 --- /dev/null +++ b/src/newsreader/accounts/permissions.py @@ -0,0 +1,23 @@ +from rest_framework.permissions import BasePermission + + +class IsOwner(BasePermission): + def has_object_permission(self, request, view, obj): + if hasattr(obj, "user"): + return obj.user == request.user + + +class IsPostOwner(BasePermission): + def has_object_permission(self, request, view, obj): + is_category_user = False + is_rule_user = False + rule = obj.rule + + if rule and rule.user: + is_rule_user = bool(rule.user == request.user) + + if rule.category and rule.category.user: + is_category_user = bool(rule.category.user == request.user) + return bool(is_category_user and is_rule_user) + + return is_rule_user diff --git a/src/newsreader/accounts/templates/accounts/login.html b/src/newsreader/accounts/templates/accounts/login.html new file mode 100644 index 0000000..ab308b2 --- /dev/null +++ b/src/newsreader/accounts/templates/accounts/login.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} + +{% load static %} + +{% block content %} +
+ +
+{% endblock %} diff --git a/src/newsreader/accounts/tests/__init__.py b/src/newsreader/accounts/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/newsreader/accounts/tests/factories.py b/src/newsreader/accounts/tests/factories.py new file mode 100644 index 0000000..fc13d74 --- /dev/null +++ b/src/newsreader/accounts/tests/factories.py @@ -0,0 +1,39 @@ +import hashlib +import string + +from django.utils.crypto import get_random_string + +import factory + +from registration.models import RegistrationProfile + +from newsreader.accounts.models import User + + +def get_activation_key(): + random_string = get_random_string(length=32, allowed_chars=string.printable) + return hashlib.sha1(random_string.encode("utf-8")).hexdigest() + + +class UserFactory(factory.django.DjangoModelFactory): + email = factory.Faker("email") + password = factory.Faker("password") + + is_staff = False + is_active = True + + @classmethod + def _create(cls, model_class, *args, **kwargs): + manager = cls._get_manager(model_class) + return manager.create_user(*args, **kwargs) + + class Meta: + model = User + + +class RegistrationProfileFactory(factory.django.DjangoModelFactory): + user = factory.SubFactory(UserFactory) + activation_key = factory.LazyFunction(get_activation_key) + + class Meta: + model = RegistrationProfile diff --git a/src/newsreader/accounts/tests/test_activation.py b/src/newsreader/accounts/tests/test_activation.py new file mode 100644 index 0000000..45d0909 --- /dev/null +++ b/src/newsreader/accounts/tests/test_activation.py @@ -0,0 +1,99 @@ +import datetime + +from django.conf import settings +from django.test import TestCase +from django.urls import reverse +from django.utils.translation import gettext as _ + +from registration.models import RegistrationProfile + +from newsreader.accounts.models import User + + +class ActivationTestCase(TestCase): + def setUp(self): + self.register_url = reverse("accounts:register") + self.register_success_url = reverse("accounts:register-complete") + self.success_url = reverse("accounts:activate-complete") + + def test_activation(self): + data = { + "email": "test@test.com", + "password1": "test12456", + "password2": "test12456", + } + + response = self.client.post(self.register_url, data) + self.assertRedirects(response, self.register_success_url) + + register_profile = RegistrationProfile.objects.get() + + kwargs = {"activation_key": register_profile.activation_key} + response = self.client.get(reverse("accounts:activate", kwargs=kwargs)) + + self.assertRedirects(response, self.success_url) + + def test_expired_key(self): + data = { + "email": "test@test.com", + "password1": "test12456", + "password2": "test12456", + } + + response = self.client.post(self.register_url, data) + + register_profile = RegistrationProfile.objects.get() + user = register_profile.user + + user.date_joined -= datetime.timedelta(days=settings.ACCOUNT_ACTIVATION_DAYS) + user.save() + + kwargs = {"activation_key": register_profile.activation_key} + response = self.client.get(reverse("accounts:activate", kwargs=kwargs)) + + self.assertEqual(200, response.status_code) + self.assertContains(response, _("Account activation failed")) + + user.refresh_from_db() + self.assertFalse(user.is_active) + + def test_invalid_key(self): + data = { + "email": "test@test.com", + "password1": "test12456", + "password2": "test12456", + } + + response = self.client.post(self.register_url, data) + self.assertRedirects(response, self.register_success_url) + + kwargs = {"activation_key": "not-a-valid-key"} + response = self.client.get(reverse("accounts:activate", kwargs=kwargs)) + + self.assertContains(response, _("Account activation failed")) + + user = User.objects.get() + + self.assertEquals(user.is_active, False) + + def test_activated_key(self): + data = { + "email": "test@test.com", + "password1": "test12456", + "password2": "test12456", + } + + response = self.client.post(self.register_url, data) + self.assertRedirects(response, self.register_success_url) + + register_profile = RegistrationProfile.objects.get() + + kwargs = {"activation_key": register_profile.activation_key} + response = self.client.get(reverse("accounts:activate", kwargs=kwargs)) + + self.assertRedirects(response, self.success_url) + + # try this a second time + response = self.client.get(reverse("accounts:activate", kwargs=kwargs)) + + self.assertRedirects(response, self.success_url) diff --git a/src/newsreader/accounts/tests/test_password_reset.py b/src/newsreader/accounts/tests/test_password_reset.py new file mode 100644 index 0000000..c7871d5 --- /dev/null +++ b/src/newsreader/accounts/tests/test_password_reset.py @@ -0,0 +1,160 @@ +from typing import Dict + +from django.core import mail +from django.test import TestCase +from django.urls import reverse +from django.utils.translation import gettext as _ + +from newsreader.accounts.tests.factories import UserFactory + + +class PasswordResetTestCase(TestCase): + def setUp(self): + self.url = reverse("accounts:password-reset") + self.success_url = reverse("accounts:password-reset-done") + self.user = UserFactory(email="test@test.com") + + def test_simple(self): + response = self.client.get(self.url) + + self.assertEquals(response.status_code, 200) + + def test_password_change(self): + data = {"email": "test@test.com"} + + response = self.client.post(self.url, data) + self.assertRedirects(response, self.success_url) + + self.assertEquals(len(mail.outbox), 1) + + def test_unkown_email(self): + data = {"email": "unknown@test.com"} + + response = self.client.post(self.url, data) + self.assertRedirects(response, self.success_url) + + self.assertEquals(len(mail.outbox), 0) + + def test_repeatedly(self): + data = {"email": "test@test.com"} + + response = self.client.post(self.url, data) + self.assertRedirects(response, self.success_url) + + response = self.client.post(self.url, data) + self.assertRedirects(response, self.success_url) + + self.assertEquals(len(mail.outbox), 2) + + +class PasswordResetConfirmTestCase(TestCase): + def setUp(self): + self.success_url = reverse("accounts:password-reset-complete") + self.user = UserFactory(email="test@test.com") + + def _get_reset_credentials(self) -> Dict: + data = {"email": self.user.email} + + response = self.client.post(reverse("accounts:password-reset"), data) + + return { + "uidb64": response.context[0]["uid"], + "token": response.context[0]["token"], + } + + def test_simple(self): + kwargs = self._get_reset_credentials() + + response = self.client.get( + reverse("accounts:password-reset-confirm", kwargs=kwargs) + ) + + self.assertRedirects( + response, f"/accounts/password-reset/{kwargs['uidb64']}/set-password/" + ) + + def test_confirm_password(self): + kwargs = self._get_reset_credentials() + + response = self.client.get( + reverse("accounts:password-reset-confirm", kwargs=kwargs) + ) + + data = {"new_password1": "jabbadabadoe", "new_password2": "jabbadabadoe"} + + response = self.client.post(response.url, data) + + self.assertRedirects(response, self.success_url) + + self.user.refresh_from_db() + + self.assertTrue(self.user.check_password("jabbadabadoe")) + + def test_wrong_uuid(self): + correct_kwargs = self._get_reset_credentials() + wrong_kwargs = {"uidb64": "burp", "token": correct_kwargs["token"]} + + response = self.client.get( + reverse("accounts:password-reset-confirm", kwargs=wrong_kwargs) + ) + + self.assertContains(response, _("Password reset unsuccessful")) + + def test_wrong_token(self): + correct_kwargs = self._get_reset_credentials() + wrong_kwargs = {"uidb64": correct_kwargs["uidb64"], "token": "token"} + + response = self.client.get( + reverse("accounts:password-reset-confirm", kwargs=wrong_kwargs) + ) + + self.assertContains(response, _("Password reset unsuccessful")) + + def test_wrong_url_args(self): + kwargs = {"uidb64": "burp", "token": "token"} + + response = self.client.get( + reverse("accounts:password-reset-confirm", kwargs=kwargs) + ) + + self.assertContains(response, _("Password reset unsuccessful")) + + def test_token_repeatedly(self): + kwargs = self._get_reset_credentials() + + response = self.client.get( + reverse("accounts:password-reset-confirm", kwargs=kwargs) + ) + + data = {"new_password1": "jabbadabadoe", "new_password2": "jabbadabadoe"} + + self.client.post(response.url, data) + + response = self.client.get( + reverse("accounts:password-reset-confirm", kwargs=kwargs) + ) + + self.assertContains(response, _("Password reset unsuccessful")) + + def test_change_form_repeatedly(self): + kwargs = self._get_reset_credentials() + + response = self.client.get( + reverse("accounts:password-reset-confirm", kwargs=kwargs) + ) + + data = {"new_password1": "new-password", "new_password2": "new-password"} + + self.client.post(response.url, data) + + data = {"new_password1": "jabbadabadoe", "new_password2": "jabbadabadoe"} + + response = self.client.post( + reverse("accounts:password-reset-confirm", kwargs=kwargs) + ) + + self.assertContains(response, _("Password reset unsuccessful")) + + self.user.refresh_from_db() + + self.assertTrue(self.user.check_password("new-password")) diff --git a/src/newsreader/accounts/tests/test_registration.py b/src/newsreader/accounts/tests/test_registration.py new file mode 100644 index 0000000..27c87bf --- /dev/null +++ b/src/newsreader/accounts/tests/test_registration.py @@ -0,0 +1,110 @@ +from django.core import mail +from django.test import TransactionTestCase as TestCase +from django.test.utils import override_settings +from django.urls import reverse +from django.utils.translation import gettext as _ + +from registration.models import RegistrationProfile + +from newsreader.accounts.models import User +from newsreader.accounts.tests.factories import UserFactory + + +class RegistrationTestCase(TestCase): + def setUp(self): + self.url = reverse("accounts:register") + self.success_url = reverse("accounts:register-complete") + self.disallowed_url = reverse("accounts:register-closed") + + def test_simple(self): + response = self.client.get(self.url) + + self.assertEquals(response.status_code, 200) + + def test_registration(self): + data = { + "email": "test@test.com", + "password1": "test12456", + "password2": "test12456", + } + + response = self.client.post(self.url, data) + self.assertRedirects(response, self.success_url) + + self.assertEquals(User.objects.count(), 1) + self.assertEquals(RegistrationProfile.objects.count(), 1) + + user = User.objects.get() + + self.assertEquals(user.is_active, False) + self.assertEquals(len(mail.outbox), 1) + + def test_existing_email(self): + UserFactory(email="test@test.com") + + data = { + "email": "test@test.com", + "password1": "test12456", + "password2": "test12456", + } + + response = self.client.post(self.url, data) + self.assertEquals(response.status_code, 200) + + self.assertEquals(User.objects.count(), 1) + self.assertContains(response, _("User with this Email address already exists")) + + def test_pending_registration(self): + data = { + "email": "test@test.com", + "password1": "test12456", + "password2": "test12456", + } + + response = self.client.post(self.url, data) + self.assertRedirects(response, self.success_url) + + self.assertEquals(User.objects.count(), 1) + self.assertEquals(RegistrationProfile.objects.count(), 1) + + user = User.objects.get() + + self.assertEquals(user.is_active, False) + self.assertEquals(len(mail.outbox), 1) + + response = self.client.post(self.url, data) + self.assertEquals(response.status_code, 200) + self.assertContains(response, _("User with this Email address already exists")) + + def test_disabled_account(self): + UserFactory(email="test@test.com", is_active=False) + + data = { + "email": "test@test.com", + "password1": "test12456", + "password2": "test12456", + } + + response = self.client.post(self.url, data) + self.assertEquals(response.status_code, 200) + + self.assertEquals(User.objects.count(), 1) + self.assertContains(response, _("User with this Email address already exists")) + + @override_settings(REGISTRATION_OPEN=False) + def test_registration_closed(self): + response = self.client.get(self.url) + + self.assertRedirects(response, self.disallowed_url) + + data = { + "email": "test@test.com", + "password1": "test12456", + "password2": "test12456", + } + + response = self.client.post(self.url, data) + self.assertRedirects(response, self.disallowed_url) + + self.assertEquals(User.objects.count(), 0) + self.assertEquals(RegistrationProfile.objects.count(), 0) diff --git a/src/newsreader/accounts/tests/test_resend_activation.py b/src/newsreader/accounts/tests/test_resend_activation.py new file mode 100644 index 0000000..0209f94 --- /dev/null +++ b/src/newsreader/accounts/tests/test_resend_activation.py @@ -0,0 +1,77 @@ +from django.core import mail +from django.test import TransactionTestCase as TestCase +from django.urls import reverse +from django.utils.translation import gettext as _ + +from registration.models import RegistrationProfile + +from newsreader.accounts.tests.factories import RegistrationProfileFactory, UserFactory + + +class ResendActivationTestCase(TestCase): + def setUp(self): + self.url = reverse("accounts:activate-resend") + self.success_url = reverse("accounts:activate-complete") + self.register_url = reverse("accounts:register") + + def test_simple(self): + response = self.client.get(self.url) + + self.assertEquals(response.status_code, 200) + + def test_resent_form(self): + data = { + "email": "test@test.com", + "password1": "test12456", + "password2": "test12456", + } + + response = self.client.post(self.register_url, data) + + register_profile = RegistrationProfile.objects.get() + original_kwargs = {"activation_key": register_profile.activation_key} + + response = self.client.post(self.url, {"email": "test@test.com"}) + + self.assertContains(response, _("We have sent an email to")) + + self.assertEquals(len(mail.outbox), 2) + + register_profile.refresh_from_db() + + kwargs = {"activation_key": register_profile.activation_key} + response = self.client.get(reverse("accounts:activate", kwargs=kwargs)) + + self.assertRedirects(response, self.success_url) + + register_profile.refresh_from_db() + user = register_profile.user + + self.assertEquals(user.is_active, True) + + # test the old activation code + response = self.client.get(reverse("accounts:activate", kwargs=original_kwargs)) + + self.assertEquals(response.status_code, 200) + self.assertContains(response, _("Account activation failed")) + + def test_existing_account(self): + user = UserFactory(is_active=True) + profile = RegistrationProfileFactory(user=user, activated=True) + + response = self.client.post(self.url, {"email": user.email}) + self.assertEquals(response.status_code, 200) + + # default behaviour is to show success page but not send an email + self.assertContains(response, _("We have sent an email to")) + + self.assertEquals(len(mail.outbox), 0) + + def test_no_account(self): + response = self.client.post(self.url, {"email": "fake@mail.com"}) + self.assertEquals(response.status_code, 200) + + # default behaviour is to show success page but not send an email + self.assertContains(response, _("We have sent an email to")) + + self.assertEquals(len(mail.outbox), 0) diff --git a/src/newsreader/accounts/tests/tests.py b/src/newsreader/accounts/tests/tests.py new file mode 100644 index 0000000..e28dbd3 --- /dev/null +++ b/src/newsreader/accounts/tests/tests.py @@ -0,0 +1,22 @@ +from django.test import TestCase + +from django_celery_beat.models import PeriodicTask + +from newsreader.accounts.models import User + + +class UserTestCase(TestCase): + def test_task_is_created(self): + user = User.objects.create(email="durp@burp.nl", task=None) + task = PeriodicTask.objects.get(name=f"{user.email}-collection-task") + + user.refresh_from_db() + + self.assertEquals(task, user.task) + self.assertEquals(PeriodicTask.objects.count(), 1) + + def test_task_is_deleted(self): + user = User.objects.create(email="durp@burp.nl", task=None) + user.delete() + + self.assertEquals(PeriodicTask.objects.count(), 0) diff --git a/src/newsreader/accounts/urls.py b/src/newsreader/accounts/urls.py new file mode 100644 index 0000000..8605233 --- /dev/null +++ b/src/newsreader/accounts/urls.py @@ -0,0 +1,56 @@ +from django.urls import path + +from newsreader.accounts.views import ( + ActivationCompleteView, + ActivationResendView, + ActivationView, + LoginView, + LogoutView, + PasswordResetCompleteView, + PasswordResetConfirmView, + PasswordResetDoneView, + PasswordResetView, + RegistrationClosedView, + RegistrationCompleteView, + RegistrationView, +) + + +urlpatterns = [ + path("login/", LoginView.as_view(), name="login"), + path("logout/", LogoutView.as_view(), name="logout"), + path("register/", RegistrationView.as_view(), name="register"), + path( + "register/complete/", + RegistrationCompleteView.as_view(), + name="register-complete", + ), + path("register/closed/", RegistrationClosedView.as_view(), name="register-closed"), + path( + "activate/complete/", ActivationCompleteView.as_view(), name="activate-complete" + ), + path("activate/resend/", ActivationResendView.as_view(), name="activate-resend"), + path( + # This URL should be placed after all activate/ url's (see arg) + "activate//", + ActivationView.as_view(), + name="activate", + ), + path("password-reset/", PasswordResetView.as_view(), name="password-reset"), + path( + "password-reset/done/", + PasswordResetDoneView.as_view(), + name="password-reset-done", + ), + path( + "password-reset///", + PasswordResetConfirmView.as_view(), + name="password-reset-confirm", + ), + path( + "password-reset/done/", + PasswordResetCompleteView.as_view(), + name="password-reset-complete", + ), + # TODO: create password change views +] diff --git a/src/newsreader/accounts/views.py b/src/newsreader/accounts/views.py new file mode 100644 index 0000000..28ae92d --- /dev/null +++ b/src/newsreader/accounts/views.py @@ -0,0 +1,91 @@ +from django.contrib.auth import views as django_views +from django.shortcuts import render +from django.urls import reverse_lazy +from django.views.generic import TemplateView + +from registration.backends.default import views as registration_views + + +class LoginView(django_views.LoginView): + template_name = "accounts/login.html" + + def get_success_url(self): + return reverse_lazy("index") + + +class LogoutView(django_views.LogoutView): + next_page = reverse_lazy("accounts:login") + + +# RegistrationView shows a registration form and sends the email +# RegistrationCompleteView shows after filling in the registration form +# ActivationView is send within the activation email and activates the account +# ActivationCompleteView shows the success screen when activation was succesful +# ActivationResendView can be used when activation links are expired +# RegistrationClosedView shows when registration is disabled +class RegistrationView(registration_views.RegistrationView): + disallowed_url = reverse_lazy("accounts:register-closed") + template_name = "registration/registration_form.html" + success_url = reverse_lazy("accounts:register-complete") + + +class RegistrationCompleteView(TemplateView): + template_name = "registration/registration_complete.html" + + +class RegistrationClosedView(TemplateView): + template_name = "registration/registration_closed.html" + + +# Redirects or renders failed activation template +class ActivationView(registration_views.ActivationView): + template_name = "registration/activation_failure.html" + + def get_success_url(self, user): + return ("accounts:activate-complete", (), {}) + + +class ActivationCompleteView(TemplateView): + template_name = "registration/activation_complete.html" + + +# Renders activation form resend or resend_activation_complete +class ActivationResendView(registration_views.ResendActivationView): + template_name = "registration/activation_resend_form.html" + + def render_form_submitted_template(self, form): + """ + Renders resend activation complete template with the submitted email. + + """ + email = form.cleaned_data["email"] + context = {"email": email} + + return render( + self.request, "registration/activation_resend_complete.html", context + ) + + +# PasswordResetView sends the mail +# PasswordResetDoneView shows a success message for the above +# PasswordResetConfirmView checks the link the user clicked and +# prompts for a new password +# PasswordResetCompleteView shows a success message for the above +class PasswordResetView(django_views.PasswordResetView): + template_name = "password-reset/password_reset_form.html" + subject_template_name = "password-reset/password_reset_subject.txt" + email_template_name = "password-reset/password_reset_email.html" + success_url = reverse_lazy("accounts:password-reset-done") + + +class PasswordResetDoneView(django_views.PasswordResetDoneView): + template_name = "password-reset/password_reset_done.html" + + +class PasswordResetConfirmView(django_views.PasswordResetConfirmView): + template_name = "password-reset/password_reset_confirm.html" + success_url = reverse_lazy("accounts:password-reset-complete") + + +class PasswordResetCompleteView(django_views.PasswordResetCompleteView): + template_name = "password-reset/password_reset_complete.html" diff --git a/src/newsreader/celery.py b/src/newsreader/celery.py new file mode 100644 index 0000000..aa15a08 --- /dev/null +++ b/src/newsreader/celery.py @@ -0,0 +1,14 @@ +import os + +from celery import Celery + + +# note: this should be consistent with the setting from manage.py +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "newsreader.conf.dev") + +# note: use the --workdir flag when running from different directories +app = Celery("newsreader") + +app.config_from_object("django.conf:settings") + +app.autodiscover_tasks() diff --git a/src/newsreader/conf/__init__.py b/src/newsreader/conf/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/newsreader/conf/base.py b/src/newsreader/conf/base.py new file mode 100644 index 0000000..743973d --- /dev/null +++ b/src/newsreader/conf/base.py @@ -0,0 +1,160 @@ +import os + +from pathlib import Path + + +BASE_DIR = Path(__file__).resolve().parent.parent.parent.parent +DJANGO_PROJECT_DIR = os.path.join(BASE_DIR, "src", "newsreader") + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/ +# SECURITY WARNING: don"t run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = ["127.0.0.1"] +INTERNAL_IPS = ["127.0.0.1"] + +# Application definition +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + # third party apps + "rest_framework", + "drf_yasg", + "celery", + "django_celery_beat", + "registration", + "axes", + # app modules + "newsreader.accounts", + "newsreader.news.core", + "newsreader.news.collection", +] + +AUTHENTICATION_BACKENDS = [ + "axes.backends.AxesBackend", + "django.contrib.auth.backends.ModelBackend", +] + +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", + "axes.middleware.AxesMiddleware", +] + +ROOT_URLCONF = "newsreader.urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [os.path.join(DJANGO_PROJECT_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", + ] + }, + } +] + +WSGI_APPLICATION = "newsreader.wsgi.application" + +# Database +# https://docs.djangoproject.com/en/2.2/ref/settings/#databases +DATABASES = { + "default": { + "ENGINE": "django.db.backends.postgresql", + "HOST": os.environ.get("POSTGRES_HOST", ""), + "NAME": os.environ.get("POSTGRES_NAME", "newsreader"), + "USER": os.environ.get("POSTGRES_USER"), + "PASSWORD": os.environ.get("POSTGRES_PASSWORD"), + } +} + +CACHES = { + "default": { + "BACKEND": "django.core.cache.backends.memcached.MemcachedCache", + "LOCATION": "localhost:11211", + }, + "axes": { + "BACKEND": "django.core.cache.backends.memcached.MemcachedCache", + "LOCATION": "localhost:11211", + }, +} + +# Password validation +# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators +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"}, +] + +# Authentication user model +AUTH_USER_MODEL = "accounts.User" + +# Internationalization +# https://docs.djangoproject.com/en/2.2/topics/i18n/ +LANGUAGE_CODE = "en-us" + +TIME_ZONE = "Europe/Amsterdam" +USE_I18N = True +USE_L10N = True +USE_TZ = True + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/2.2/howto/static-files/ +STATIC_URL = "/static/" +STATIC_ROOT = os.path.join(BASE_DIR, "static") +STATICFILES_DIRS = [os.path.join(DJANGO_PROJECT_DIR, "static")] + +# https://docs.djangoproject.com/en/2.2/ref/settings/#std:setting-STATICFILES_FINDERS +STATICFILES_FINDERS = [ + "django.contrib.staticfiles.finders.FileSystemFinder", + "django.contrib.staticfiles.finders.AppDirectoriesFinder", +] + +DEFAULT_FROM_EMAIL = "newsreader@rss.fudiggity.nl" + +# Third party settings +AXES_HANDLER = "axes.handlers.cache.AxesCacheHandler" +AXES_CACHE = "axes" +AXES_FAILURE_LIMIT = 5 +AXES_COOLOFF_TIME = 3 # in hours +AXES_RESET_ON_SUCCESS = True + +REST_FRAMEWORK = { + "DEFAULT_AUTHENTICATION_CLASSES": ( + "rest_framework.authentication.SessionAuthentication", + ), + "DEFAULT_PERMISSION_CLASSES": ( + "rest_framework.permissions.IsAuthenticated", + "newsreader.accounts.permissions.IsOwner", + ), + "DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",), +} + +SWAGGER_SETTINGS = { + "LOGIN_URL": "rest_framework:login", + "LOGOUT_URL": "rest_framework:logout", + "DOC_EXPANSION": "list", +} + +REGISTRATION_OPEN = True +REGISTRATION_AUTO_LOGIN = True +ACCOUNT_ACTIVATION_DAYS = 7 diff --git a/src/newsreader/conf/dev.py b/src/newsreader/conf/dev.py new file mode 100644 index 0000000..b81a9fa --- /dev/null +++ b/src/newsreader/conf/dev.py @@ -0,0 +1,35 @@ +from .base import * # isort:skip + + +SECRET_KEY = "mv4&5#+)-=abz3^&1r^nk_ca6y54--p(4n4cg%z*g&rb64j%wl" + +MIDDLEWARE += ["debug_toolbar.middleware.DebugToolbarMiddleware"] + +EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" + +INSTALLED_APPS += ["debug_toolbar", "django_extensions"] + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [os.path.join(DJANGO_PROJECT_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", + ] + }, + } +] + +# Third party settings +AXES_FAILURE_LIMIT = 50 +AXES_COOLOFF_TIME = None + +try: + from .local import * # noqa +except ImportError: + pass diff --git a/src/newsreader/conf/docker.py b/src/newsreader/conf/docker.py new file mode 100644 index 0000000..3584b30 --- /dev/null +++ b/src/newsreader/conf/docker.py @@ -0,0 +1,19 @@ +from .dev import * # isort:skip + + +SECRET_KEY = "=q(ztyo)b6noom#a164g&s9vcj1aawa^g#ing_ir99=_zl4g&$" + +# Celery +# https://docs.celeryproject.org/en/latest/userguide/configuration.html +BROKER_URL = "amqp://guest:guest@rabbitmq:5672//" + +CACHES = { + "default": { + "BACKEND": "django.core.cache.backends.memcached.MemcachedCache", + "LOCATION": "memcached:11211", + }, + "axes": { + "BACKEND": "django.core.cache.backends.memcached.MemcachedCache", + "LOCATION": "memcached:11211", + }, +} diff --git a/src/newsreader/conf/gitlab.py b/src/newsreader/conf/gitlab.py new file mode 100644 index 0000000..67a20dd --- /dev/null +++ b/src/newsreader/conf/gitlab.py @@ -0,0 +1,19 @@ +from .base import * # isort:skip + + +SECRET_KEY = "29%lkw+&n%^w4k#@_db2mo%*tc&xzb)x7xuq*(0$eucii%4r0c" + +EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" + +AXES_ENABLED = False + +CACHES = { + "default": { + "BACKEND": "django.core.cache.backends.memcached.MemcachedCache", + "LOCATION": "memcached:11211", + }, + "axes": { + "BACKEND": "django.core.cache.backends.memcached.MemcachedCache", + "LOCATION": "memcached:11211", + }, +} diff --git a/src/newsreader/conf/production.py b/src/newsreader/conf/production.py new file mode 100644 index 0000000..f287498 --- /dev/null +++ b/src/newsreader/conf/production.py @@ -0,0 +1,45 @@ +import os + +from dotenv import load_dotenv + + +from .base import * # isort:skip + + +load_dotenv() + +DEBUG = False +ALLOWED_HOSTS = ["rss.fudiggity.nl"] + +SECRET_KEY = os.environ["DJANGO_SECRET_KEY"] + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.postgresql", + "HOST": os.environ["POSTGRES_HOST"], + "PORT": os.environ["POSTGRES_PORT"], + "NAME": os.environ["POSTGRES_NAME"], + "USER": os.environ["POSTGRES_USER"], + "PASSWORD": os.environ["POSTGRES_PASSWORD"], + } +} + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [os.path.join(DJANGO_PROJECT_DIR, "templates")], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ] + }, + } +] + +# Third party settings +AXES_HANDLER = "axes.handlers.database.DatabaseHandler" + +REGISTRATION_OPEN = False diff --git a/src/newsreader/core/__init__.py b/src/newsreader/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/newsreader/core/admin.py b/src/newsreader/core/admin.py new file mode 100644 index 0000000..846f6b4 --- /dev/null +++ b/src/newsreader/core/admin.py @@ -0,0 +1 @@ +# Register your models here. diff --git a/src/newsreader/core/apps.py b/src/newsreader/core/apps.py new file mode 100644 index 0000000..5ef1d60 --- /dev/null +++ b/src/newsreader/core/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class CoreConfig(AppConfig): + name = "core" diff --git a/src/newsreader/core/migrations/__init__.py b/src/newsreader/core/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/newsreader/core/models.py b/src/newsreader/core/models.py new file mode 100644 index 0000000..f8bd80f --- /dev/null +++ b/src/newsreader/core/models.py @@ -0,0 +1,15 @@ +from django.db import models +from django.utils import timezone + + +class TimeStampedModel(models.Model): + """ + An abstract base class model that provides self- + updating ``created`` and ``modified`` fields. + """ + + created = models.DateTimeField(default=timezone.now) + modified = models.DateTimeField(auto_now=True) + + class Meta: + abstract = True diff --git a/src/newsreader/core/pagination.py b/src/newsreader/core/pagination.py new file mode 100644 index 0000000..5e19771 --- /dev/null +++ b/src/newsreader/core/pagination.py @@ -0,0 +1,12 @@ +from rest_framework.pagination import PageNumberPagination + + +class ResultSetPagination(PageNumberPagination): + page_size_query_param = "count" + max_page_size = 50 + page_size = 30 + + +class LargeResultSetPagination(ResultSetPagination): + max_page_size = 100 + page_size = 50 diff --git a/src/newsreader/core/permissions.py b/src/newsreader/core/permissions.py new file mode 100644 index 0000000..b584b63 --- /dev/null +++ b/src/newsreader/core/permissions.py @@ -0,0 +1,6 @@ +from rest_framework.permissions import BasePermission + + +class IsOwner(BasePermission): + def has_object_permission(self, request, view, obj): + return obj.user == request.user diff --git a/src/newsreader/core/tests.py b/src/newsreader/core/tests.py new file mode 100644 index 0000000..a39b155 --- /dev/null +++ b/src/newsreader/core/tests.py @@ -0,0 +1 @@ +# Create your tests here. diff --git a/src/newsreader/core/views.py b/src/newsreader/core/views.py new file mode 100644 index 0000000..60f00ef --- /dev/null +++ b/src/newsreader/core/views.py @@ -0,0 +1 @@ +# Create your views here. diff --git a/src/newsreader/fixtures/default-fixture.json b/src/newsreader/fixtures/default-fixture.json new file mode 100644 index 0000000..e0de28f --- /dev/null +++ b/src/newsreader/fixtures/default-fixture.json @@ -0,0 +1,298 @@ +[ +{ + "model": "django_celery_beat.periodictask", + "pk": 10, + "fields": { + "name": "sonny@bakker.nl-collection-task", + "task": "newsreader.news.collection.tasks.collect", + "interval": 4, + "crontab": null, + "solar": null, + "clocked": null, + "args": "[2]", + "kwargs": "{}", + "queue": null, + "exchange": null, + "routing_key": null, + "headers": "{}", + "priority": null, + "expires": null, + "one_off": false, + "start_time": null, + "enabled": true, + "last_run_at": "2019-11-29T22:29:08.345Z", + "total_run_count": 290, + "date_changed": "2019-11-29T22:29:18.378Z", + "description": "" + } +}, +{ + "model": "django_celery_beat.periodictask", + "pk": 26, + "fields": { + "name": "sonnyba871@gmail.com-collection-task", + "task": "newsreader.news.collection.tasks.collect", + "interval": 4, + "crontab": null, + "solar": null, + "clocked": null, + "args": "[18]", + "kwargs": "{}", + "queue": null, + "exchange": null, + "routing_key": null, + "headers": "{}", + "priority": null, + "expires": null, + "one_off": false, + "start_time": null, + "enabled": true, + "last_run_at": "2019-11-29T22:35:19.134Z", + "total_run_count": 103, + "date_changed": "2019-11-29T22:38:19.464Z", + "description": "" + } +}, +{ + "model": "django_celery_beat.crontabschedule", + "pk": 1, + "fields": { + "minute": "0", + "hour": "4", + "day_of_week": "*", + "day_of_month": "*", + "month_of_year": "*", + "timezone": "UTC" + } +}, +{ + "model": "django_celery_beat.intervalschedule", + "pk": 1, + "fields": { + "every": 5, + "period": "minutes" + } +}, +{ + "model": "django_celery_beat.intervalschedule", + "pk": 2, + "fields": { + "every": 15, + "period": "minutes" + } +}, +{ + "model": "django_celery_beat.intervalschedule", + "pk": 3, + "fields": { + "every": 30, + "period": "minutes" + } +}, +{ + "model": "django_celery_beat.intervalschedule", + "pk": 4, + "fields": { + "every": 1, + "period": "hours" + } +}, +{ + "model": "accounts.user", + "fields": { + "password": "pbkdf2_sha256$150000$5lBD7JemxYfE$B+lM5wWUW2n/ZulPFaWHtzWjyQ/QZ6iwjAC2I0R/VzU=", + "last_login": "2019-11-27T18:57:36.686Z", + "is_superuser": true, + "first_name": "", + "last_name": "", + "is_staff": true, + "is_active": true, + "date_joined": "2019-07-18T18:52:36.080Z", + "email": "sonny@bakker.nl", + "task": 10, + "groups": [], + "user_permissions": [] + } +}, +{ + "model": "accounts.user", + "fields": { + "password": "pbkdf2_sha256$150000$vUwxT8T25R8C$S+Eq2tMRbSDE31/X5KGJ/M+Nblh7kKfzuM/z7HraR/Q=", + "last_login": null, + "is_superuser": false, + "first_name": "", + "last_name": "", + "is_staff": false, + "is_active": false, + "date_joined": "2019-11-25T15:35:14.051Z", + "email": "sonnyba871@gmail.com", + "task": 26, + "groups": [], + "user_permissions": [] + } +}, +{ + "model": "core.category", + "pk": 8, + "fields": { + "created": "2019-11-17T19:37:24.671Z", + "modified": "2019-11-18T19:59:55.010Z", + "name": "World news", + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "core.category", + "pk": 9, + "fields": { + "created": "2019-11-17T19:37:26.161Z", + "modified": "2019-11-18T19:59:45.010Z", + "name": "Tech", + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 3, + "fields": { + "created": "2019-07-14T13:08:10.374Z", + "modified": "2019-11-29T22:35:20.346Z", + "name": "Hackers News", + "url": "https://news.ycombinator.com/rss", + "website_url": "https://news.ycombinator.com/", + "favicon": "https://news.ycombinator.com/favicon.ico", + "timezone": "UTC", + "category": 9, + "last_suceeded": "2019-11-29T22:35:20.235Z", + "succeeded": true, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 4, + "fields": { + "created": "2019-07-20T11:24:32.745Z", + "modified": "2019-11-29T22:35:19.525Z", + "name": "BBC", + "url": "http://feeds.bbci.co.uk/news/world/rss.xml", + "website_url": "https://www.bbc.co.uk/news/", + "favicon": "https://m.files.bbci.co.uk/modules/bbc-morph-news-waf-page-meta/2.5.2/apple-touch-icon-57x57-precomposed.png", + "timezone": "UTC", + "category": 8, + "last_suceeded": "2019-11-29T22:35:19.241Z", + "succeeded": true, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 5, + "fields": { + "created": "2019-07-20T11:24:50.411Z", + "modified": "2019-11-29T22:35:20.010Z", + "name": "Ars Technica", + "url": "http://feeds.arstechnica.com/arstechnica/index?fmt=xml", + "website_url": "https://arstechnica.com", + "favicon": "https://cdn.arstechnica.net/favicon.ico", + "timezone": "UTC", + "category": 9, + "last_suceeded": "2019-11-29T22:35:19.808Z", + "succeeded": true, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 6, + "fields": { + "created": "2019-07-20T11:25:02.089Z", + "modified": "2019-11-29T22:35:20.233Z", + "name": "The Guardian", + "url": "https://www.theguardian.com/world/rss", + "website_url": "https://www.theguardian.com/world", + "favicon": "https://assets.guim.co.uk/images/favicons/873381bf11d58e20f551905d51575117/72x72.png", + "timezone": "UTC", + "category": 8, + "last_suceeded": "2019-11-29T22:35:20.076Z", + "succeeded": true, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 7, + "fields": { + "created": "2019-07-20T11:25:30.121Z", + "modified": "2019-11-29T22:35:19.695Z", + "name": "Tweakers", + "url": "http://feeds.feedburner.com/tweakers/mixed?fmt=xml", + "website_url": "https://tweakers.net/", + "favicon": null, + "timezone": "UTC", + "category": 9, + "last_suceeded": "2019-11-29T22:35:19.528Z", + "succeeded": true, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 8, + "fields": { + "created": "2019-07-20T11:25:46.256Z", + "modified": "2019-11-29T22:35:20.074Z", + "name": "The Verge", + "url": "https://www.theverge.com/rss/index.xml", + "website_url": "https://www.theverge.com/", + "favicon": "https://cdn.vox-cdn.com/uploads/chorus_asset/file/7395367/favicon-16x16.0.png", + "timezone": "UTC", + "category": 9, + "last_suceeded": "2019-11-29T22:35:20.012Z", + "succeeded": true, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 9, + "fields": { + "created": "2019-11-24T15:28:41.399Z", + "modified": "2019-11-29T22:35:19.807Z", + "name": "NOS", + "url": "http://feeds.nos.nl/nosnieuwsalgemeen", + "website_url": null, + "favicon": null, + "timezone": "Europe/Amsterdam", + "category": 8, + "last_suceeded": "2019-11-29T22:35:19.697Z", + "succeeded": true, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +} +] diff --git a/src/newsreader/fixtures/local/fixture.json b/src/newsreader/fixtures/local/fixture.json new file mode 100644 index 0000000..ffcc4fd --- /dev/null +++ b/src/newsreader/fixtures/local/fixture.json @@ -0,0 +1,168 @@ +[ + { + "fields" : { + "is_active" : true, + "is_superuser" : true, + "task_interval" : null, + "user_permissions" : [], + "is_staff" : true, + "last_name" : "", + "first_name" : "", + "groups" : [], + "date_joined" : "2019-07-14T10:44:35.228Z", + "password" : "pbkdf2_sha256$150000$vAOYP6XgN40C$bvW265Is2toKzEnbMmLVufd+DA6z1kIhUv/bhtUiDcA=", + "task" : null, + "last_login" : "2019-07-14T12:28:05.473Z", + "email" : "sonnyba871@gmail.com" + }, + "pk" : 1, + "model" : "accounts.user" + }, + { + "model" : "accounts.user", + "fields" : { + "task" : null, + "email" : "sonny@bakker.nl", + "last_login" : "2019-07-20T07:52:59.491Z", + "first_name" : "", + "groups" : [], + "last_name" : "", + "password" : "pbkdf2_sha256$150000$SMI9E7GFkJQk$usX0YN3q0ArqAd6bUQ9sUm6Ugms3XRxaiizHGIa3Pk4=", + "date_joined" : "2019-07-18T18:52:36.080Z", + "is_staff" : true, + "task_interval" : null, + "user_permissions" : [], + "is_active" : true, + "is_superuser" : true + }, + "pk" : 2 + }, + { + "pk" : 3, + "fields" : { + "favicon" : null, + "category" : null, + "url" : "https://news.ycombinator.com/rss", + "error" : null, + "user" : 2, + "succeeded" : true, + "modified" : "2019-07-20T11:28:16.473Z", + "last_suceeded" : "2019-07-20T11:28:16.316Z", + "name" : "Hackers News", + "website_url" : null, + "created" : "2019-07-14T13:08:10.374Z", + "timezone" : "UTC" + }, + "model" : "collection.collectionrule" + }, + { + "model" : "collection.collectionrule", + "pk" : 4, + "fields" : { + "favicon" : null, + "category" : 2, + "url" : "http://feeds.bbci.co.uk/news/world/rss.xml", + "error" : null, + "user" : 2, + "succeeded" : true, + "last_suceeded" : "2019-07-20T11:28:15.691Z", + "name" : "BBC", + "modified" : "2019-07-20T12:07:49.164Z", + "timezone" : "UTC", + "website_url" : null, + "created" : "2019-07-20T11:24:32.745Z" + } + }, + { + "pk" : 5, + "fields" : { + "error" : null, + "category" : null, + "url" : "http://feeds.arstechnica.com/arstechnica/index?fmt=xml", + "favicon" : null, + "timezone" : "UTC", + "created" : "2019-07-20T11:24:50.411Z", + "website_url" : null, + "name" : "Ars Technica", + "succeeded" : true, + "last_suceeded" : "2019-07-20T11:28:15.986Z", + "modified" : "2019-07-20T11:28:16.033Z", + "user" : 2 + }, + "model" : "collection.collectionrule" + }, + { + "model" : "collection.collectionrule", + "pk" : 6, + "fields" : { + "favicon" : null, + "category" : 2, + "url" : "https://www.theguardian.com/world/rss", + "error" : null, + "user" : 2, + "name" : "The Guardian", + "succeeded" : true, + "last_suceeded" : "2019-07-20T11:28:16.078Z", + "modified" : "2019-07-20T12:07:44.292Z", + "created" : "2019-07-20T11:25:02.089Z", + "website_url" : null, + "timezone" : "UTC" + } + }, + { + "fields" : { + "url" : "http://feeds.feedburner.com/tweakers/mixed?fmt=xml", + "category" : 1, + "error" : null, + "favicon" : null, + "timezone" : "UTC", + "website_url" : null, + "created" : "2019-07-20T11:25:30.121Z", + "user" : 2, + "last_suceeded" : "2019-07-20T11:28:15.860Z", + "succeeded" : true, + "modified" : "2019-07-20T12:07:28.473Z", + "name" : "Tweakers" + }, + "pk" : 7, + "model" : "collection.collectionrule" + }, + { + "model" : "collection.collectionrule", + "pk" : 8, + "fields" : { + "category" : 1, + "url" : "https://www.theverge.com/rss/index.xml", + "error" : null, + "favicon" : null, + "created" : "2019-07-20T11:25:46.256Z", + "website_url" : null, + "timezone" : "UTC", + "user" : 2, + "last_suceeded" : "2019-07-20T11:28:16.034Z", + "succeeded" : true, + "modified" : "2019-07-20T12:07:21.704Z", + "name" : "The Verge" + } + }, + { + "pk" : 1, + "fields" : { + "user" : 2, + "name" : "Tech", + "modified" : "2019-07-20T12:07:17.396Z", + "created" : "2019-07-20T12:07:10Z" + }, + "model" : "core.category" + }, + { + "model" : "core.category", + "pk" : 2, + "fields" : { + "user" : 2, + "modified" : "2019-07-20T12:07:42.329Z", + "name" : "World News", + "created" : "2019-07-20T12:07:34Z" + } + } +] diff --git a/src/newsreader/js/components/Card.js b/src/newsreader/js/components/Card.js new file mode 100644 index 0000000..d1580a4 --- /dev/null +++ b/src/newsreader/js/components/Card.js @@ -0,0 +1,13 @@ +import React from 'react'; + +const Card = props => { + return ( +
+
{props.header}
+
{props.content}
+
{props.footer}
+
+ ); +}; + +export default Card; diff --git a/src/newsreader/js/components/LoadingIndicator.js b/src/newsreader/js/components/LoadingIndicator.js new file mode 100644 index 0000000..b3a3cb6 --- /dev/null +++ b/src/newsreader/js/components/LoadingIndicator.js @@ -0,0 +1,13 @@ +import React from 'react'; + +const LoadingIndicator = props => { + return ( +
+
+
+
+
+ ); +}; + +export default LoadingIndicator; diff --git a/src/newsreader/js/components/Messages.js b/src/newsreader/js/components/Messages.js new file mode 100644 index 0000000..843677c --- /dev/null +++ b/src/newsreader/js/components/Messages.js @@ -0,0 +1,29 @@ +import React from 'react'; + +class Messages extends React.Component { + state = { messages: this.props.messages }; + + close = ::this.close; + + close(index) { + const newMessages = this.state.messages.filter((message, currentIndex) => { + return currentIndex != index; + }); + + this.setState({ messages: newMessages }); + } + + render() { + const messages = this.state.messages.map((message, index) => { + return ( +
  • + {message.text} this.close(index)} /> +
  • + ); + }); + + return
      {messages}
    ; + } +} + +export default Messages; diff --git a/src/newsreader/js/components/Modal.js b/src/newsreader/js/components/Modal.js new file mode 100644 index 0000000..1b77b84 --- /dev/null +++ b/src/newsreader/js/components/Modal.js @@ -0,0 +1,11 @@ +import React from 'react'; + +const Modal = props => { + return ( +
    +
    {props.content}
    +
    + ); +}; + +export default Modal; diff --git a/src/newsreader/js/index.js b/src/newsreader/js/index.js new file mode 100644 index 0000000..48db0b2 --- /dev/null +++ b/src/newsreader/js/index.js @@ -0,0 +1,3 @@ +import './pages/homepage/index.js'; +import './pages/rules/index.js'; +import './pages/categories/index.js'; diff --git a/src/newsreader/js/pages/categories/App.js b/src/newsreader/js/pages/categories/App.js new file mode 100644 index 0000000..95ab396 --- /dev/null +++ b/src/newsreader/js/pages/categories/App.js @@ -0,0 +1,106 @@ +import React from 'react'; + +import Cookies from 'js-cookie'; + +import Card from '../../components/Card.js'; +import CategoryCard from './components/CategoryCard.js'; +import CategoryModal from './components/CategoryModal.js'; +import Messages from '../../components/Messages.js'; + +class App extends React.Component { + selectCategory = ::this.selectCategory; + deselectCategory = ::this.deselectCategory; + deleteCategory = ::this.deleteCategory; + + constructor(props) { + super(props); + + this.token = Cookies.get('csrftoken'); + this.state = { + categories: props.categories, + selectedCategoryId: null, + message: null, + }; + } + + selectCategory(categoryId) { + this.setState({ selectedCategoryId: categoryId }); + } + + deselectCategory() { + this.setState({ selectedCategoryId: null }); + } + + deleteCategory(categoryId) { + const url = `/api/categories/${categoryId}/`; + const options = { + method: 'DELETE', + headers: { + 'X-CSRFToken': this.token, + }, + }; + + fetch(url, options).then(response => { + if (response.ok) { + const categories = this.state.categories.filter(category => { + return category.pk != categoryId; + }); + + return this.setState({ + categories: categories, + selectedCategoryId: null, + message: null, + }); + } + }); + + const message = { + type: 'error', + text: 'Unable to remove category, try again later', + }; + return this.setState({ selectedCategoryId: null, message: message }); + } + + render() { + const { categories } = this.state; + const cards = categories.map(category => { + return ( + + ); + }); + + const selectedCategory = categories.find(category => { + return category.pk === this.state.selectedCategoryId; + }); + + const pageHeader = ( + <> +

    Categories

    + + Create category + + + ); + + return ( + <> + {this.state.message && } + + {cards} + {selectedCategory && ( + + )} + + ); + } +} + +export default App; diff --git a/src/newsreader/js/pages/categories/components/CategoryCard.js b/src/newsreader/js/pages/categories/components/CategoryCard.js new file mode 100644 index 0000000..a3a242d --- /dev/null +++ b/src/newsreader/js/pages/categories/components/CategoryCard.js @@ -0,0 +1,51 @@ +import React from 'react'; + +import Card from '../../../components/Card.js'; + +const CategoryCard = props => { + const { category } = props; + + const categoryRules = category.rules.map(rule => { + let favicon = null; + + if (rule.favicon) { + favicon = ; + } else { + favicon = ; + } + + return ( +
  • + {favicon} + {rule.name} +
  • + ); + }); + + const cardHeader = ( + <> +

    {category.name}

    + {category.created} + + ); + const cardContent = <>{category.rules &&
      {categoryRules}
    }; + const cardFooter = ( + <> + + Edit + + + + ); + + return ; +}; + +export default CategoryCard; diff --git a/src/newsreader/js/pages/categories/components/CategoryModal.js b/src/newsreader/js/pages/categories/components/CategoryModal.js new file mode 100644 index 0000000..565e12c --- /dev/null +++ b/src/newsreader/js/pages/categories/components/CategoryModal.js @@ -0,0 +1,37 @@ +import React from 'react'; + +import Modal from '../../../components/Modal.js'; + +const CategoryModal = props => { + const content = ( + <> +
    +

    Delete category

    +
    + +
    +

    Are you sure you want to delete {props.category.name}?

    + + Collection rules coupled to this category will not be deleted but will have no + category + +
    + +
    + + +
    + + ); + + return ; +}; + +export default CategoryModal; diff --git a/src/newsreader/js/pages/categories/index.js b/src/newsreader/js/pages/categories/index.js new file mode 100644 index 0000000..9d75bb9 --- /dev/null +++ b/src/newsreader/js/pages/categories/index.js @@ -0,0 +1,13 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; + +import App from './App.js'; + +const page = document.getElementById('categories--page'); + +if (page) { + const dataScript = document.getElementById('categories-data'); + const categories = JSON.parse(dataScript.textContent); + + ReactDOM.render(, page); +} diff --git a/src/newsreader/js/pages/homepage/App.js b/src/newsreader/js/pages/homepage/App.js new file mode 100644 index 0000000..bdf0149 --- /dev/null +++ b/src/newsreader/js/pages/homepage/App.js @@ -0,0 +1,64 @@ +import React from 'react'; + +import { connect } from 'react-redux'; +import { isEqual } from 'lodash'; + +import { fetchCategories } from './actions/categories'; + +import Sidebar from './components/sidebar/Sidebar.js'; +import FeedList from './components/feedlist/FeedList.js'; +import PostModal from './components/PostModal.js'; +import Messages from '../../components/Messages.js'; + +class App extends React.Component { + componentDidMount() { + this.props.fetchCategories(); + } + + render() { + return ( + <> + + + + {this.props.error && ( + + )} + + {!isEqual(this.props.post, {}) && ( + + )} + + ); + } +} + +const mapStateToProps = state => { + const { error } = state.error; + + if (!isEqual(state.selected.post, {})) { + const ruleId = state.selected.post.rule; + + const rule = state.rules.items[ruleId]; + const category = state.categories.items[rule.category]; + + return { + category, + error, + rule, + post: state.selected.post, + }; + } + + return { error, post: state.selected.post }; +}; + +const mapDispatchToProps = dispatch => ({ + fetchCategories: () => dispatch(fetchCategories()), +}); + +export default connect(mapStateToProps, mapDispatchToProps)(App); diff --git a/src/newsreader/js/pages/homepage/actions/categories.js b/src/newsreader/js/pages/homepage/actions/categories.js new file mode 100644 index 0000000..d569f53 --- /dev/null +++ b/src/newsreader/js/pages/homepage/actions/categories.js @@ -0,0 +1,87 @@ +import { requestRules, receiveRules, fetchRulesByCategory } from './rules.js'; +import { handleAPIError } from './error.js'; + +import { CATEGORY_TYPE } from '../constants.js'; + +export const SELECT_CATEGORY = 'SELECT_CATEGORY'; + +export const RECEIVE_CATEGORY = 'RECEIVE_CATEGORY'; +export const RECEIVE_CATEGORIES = 'RECEIVE_CATEGORIES'; + +export const REQUEST_CATEGORY = 'REQUEST_CATEGORY'; +export const REQUEST_CATEGORIES = 'REQUEST_CATEGORIES'; + +export const selectCategory = category => ({ + type: SELECT_CATEGORY, + section: { ...category, type: CATEGORY_TYPE }, +}); + +export const receiveCategory = category => ({ + type: RECEIVE_CATEGORY, + category, +}); + +export const receiveCategories = categories => ({ + type: RECEIVE_CATEGORIES, + categories, +}); + +export const requestCategory = () => ({ type: REQUEST_CATEGORY }); +export const requestCategories = () => ({ type: REQUEST_CATEGORIES }); + +export const fetchCategory = category => { + return (dispatch, getState) => { + const { selected } = getState(); + const selectedSection = { ...selected.item }; + + if (selectedSection.type === CATEGORY_TYPE && selectedSection.clicks <= 1) { + return; + } + + dispatch(requestCategory()); + + return fetch(`/api/categories/${category.id}`) + .then(response => response.json()) + .then(json => { + dispatch(receiveCategory({ ...json })); + + if (category.unread === 0) { + return dispatch(fetchRulesByCategory(category)); + } + }) + .catch(error => { + dispatch(receiveCategory({})); + dispatch(handleAPIError(error)); + }); + }; +}; + +export const fetchCategories = () => { + return dispatch => { + dispatch(requestCategories()); + + return fetch('/api/categories/') + .then(response => response.json()) + .then(categories => { + dispatch(receiveCategories(categories)); + + return categories; + }) + .then(categories => { + dispatch(requestRules()); + + const promises = categories.map(category => { + return fetch(`/api/categories/${category.id}/rules/`); + }); + + return Promise.all(promises); + }) + .then(responses => Promise.all(responses.map(response => response.json()))) + .then(nestedRules => dispatch(receiveRules(nestedRules.flat()))) + .catch(error => { + dispatch(receiveCategories([])); + dispatch(receiveRules([])); + dispatch(handleAPIError(error)); + }); + }; +}; diff --git a/src/newsreader/js/pages/homepage/actions/error.js b/src/newsreader/js/pages/homepage/actions/error.js new file mode 100644 index 0000000..6449f84 --- /dev/null +++ b/src/newsreader/js/pages/homepage/actions/error.js @@ -0,0 +1,6 @@ +export const RECEIVE_API_ERROR = 'RECEIVE_API_ERROR'; + +export const handleAPIError = error => ({ + type: RECEIVE_API_ERROR, + error, +}); diff --git a/src/newsreader/js/pages/homepage/actions/posts.js b/src/newsreader/js/pages/homepage/actions/posts.js new file mode 100644 index 0000000..b7ad5cb --- /dev/null +++ b/src/newsreader/js/pages/homepage/actions/posts.js @@ -0,0 +1,89 @@ +import { handleAPIError } from './error.js'; +import { RULE_TYPE, CATEGORY_TYPE } from '../constants.js'; + +export const SELECT_POST = 'SELECT_POST'; +export const UNSELECT_POST = 'UNSELECT_POST'; + +export const RECEIVE_POSTS = 'RECEIVE_POSTS'; +export const RECEIVE_POST = 'RECEIVE_POST'; +export const REQUEST_POSTS = 'REQUEST_POSTS'; + +export const MARK_POST_READ = 'MARK_POST_READ'; + +export const requestPosts = () => ({ type: REQUEST_POSTS }); + +export const receivePosts = (posts, next) => ({ + type: RECEIVE_POSTS, + posts, + next, +}); + +export const receivePost = post => ({ type: RECEIVE_POST, post }); + +export const selectPost = post => ({ type: SELECT_POST, post }); + +export const unSelectPost = () => ({ type: UNSELECT_POST }); + +export const postRead = (post, section) => ({ + type: MARK_POST_READ, + post, + section, +}); + +export const markPostRead = (post, token) => { + return (dispatch, getState) => { + const { selected } = getState(); + + const url = `/api/posts/${post.id}/`; + const options = { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': token, + }, + body: JSON.stringify({ read: true }), + }; + + const section = { ...selected.item }; + + return fetch(url, options) + .then(response => response.json()) + .then(updatedPost => { + dispatch(receivePost({ ...updatedPost })); + dispatch(postRead({ ...updatedPost }, section)); + }) + .catch(error => { + dispatch(receivePost({})); + dispatch(handleAPIError(error)); + }); + }; +}; + +export const fetchPostsBySection = (section, page = false) => { + return dispatch => { + if (section.unread === 0) { + return; + } + + dispatch(requestPosts()); + + let url = null; + + switch (section.type) { + case RULE_TYPE: + url = page ? page : `/api/rules/${section.id}/posts/?read=false`; + break; + case CATEGORY_TYPE: + url = page ? page : `/api/categories/${section.id}/posts/?read=false`; + break; + } + + return fetch(url) + .then(response => response.json()) + .then(posts => dispatch(receivePosts(posts.results, posts.next))) + .catch(error => { + dispatch(receivePosts([])); + dispatch(handleAPIError(error)); + }); + }; +}; diff --git a/src/newsreader/js/pages/homepage/actions/rules.js b/src/newsreader/js/pages/homepage/actions/rules.js new file mode 100644 index 0000000..98b494e --- /dev/null +++ b/src/newsreader/js/pages/homepage/actions/rules.js @@ -0,0 +1,75 @@ +import { fetchCategory } from './categories.js'; +import { RULE_TYPE } from '../constants.js'; +import { handleAPIError } from './error.js'; + +export const SELECT_RULE = 'SELECT_RULE'; +export const SELECT_RULES = 'SELECT_RULES'; + +export const RECEIVE_RULE = 'RECEIVE_RULE'; +export const RECEIVE_RULES = 'RECEIVE_RULES'; + +export const REQUEST_RULE = 'REQUEST_RULE'; +export const REQUEST_RULES = 'REQUEST_RULES'; + +export const selectRule = rule => ({ + type: SELECT_RULE, + section: { ...rule, type: RULE_TYPE }, +}); + +export const requestRule = () => ({ type: REQUEST_RULE }); +export const requestRules = () => ({ type: REQUEST_RULES }); + +export const receiveRule = rule => ({ + type: RECEIVE_RULE, + rule, +}); + +export const receiveRules = rules => ({ + type: RECEIVE_RULES, + rules, +}); + +export const fetchRule = rule => { + return (dispatch, getState) => { + const { selected } = getState(); + const selectedSection = { ...selected.item }; + + if (selectedSection.type === RULE_TYPE && selectedSection.clicks <= 1) { + return; + } + + dispatch(requestRule()); + + const { categories } = getState(); + const category = categories['items'][rule.category]; + + return fetch(`/api/rules/${rule.id}`) + .then(response => response.json()) + .then(receivedRule => { + dispatch(receiveRule({ ...receivedRule })); + + // fetch & update category info when the rule is read + if (rule.unread === 0) { + return dispatch(fetchCategory({ ...category })); + } + }) + .catch(error => { + dispatch(receiveRule({})); + dispatch(handleAPIError(error)); + }); + }; +}; + +export const fetchRulesByCategory = category => { + return dispatch => { + dispatch(requestRules()); + + return fetch(`/api/categories/${category.id}/rules/`) + .then(response => response.json()) + .then(rules => dispatch(receiveRules(rules))) + .catch(error => { + dispatch(receiveRules([])); + dispatch(handleAPIError(error)); + }); + }; +}; diff --git a/src/newsreader/js/pages/homepage/actions/selected.js b/src/newsreader/js/pages/homepage/actions/selected.js new file mode 100644 index 0000000..189cad6 --- /dev/null +++ b/src/newsreader/js/pages/homepage/actions/selected.js @@ -0,0 +1,89 @@ +import { handleAPIError } from './error.js'; +import { receiveCategory, requestCategory } from './categories.js'; +import { receiveRule, requestRule } from './rules.js'; +import { CATEGORY_TYPE, RULE_TYPE } from '../constants.js'; + +export const MARK_SECTION_READ = 'MARK_SECTION_READ'; + +export const markSectionRead = section => ({ + type: MARK_SECTION_READ, + section, +}); + +const markCategoryRead = (category, token) => { + return (dispatch, getState) => { + dispatch(requestCategory(category)); + + const { rules } = getState(); + const categoryRules = Object.values({ ...rules.items }).filter(rule => { + return rule.category === category.id; + }); + const ruleMapping = {}; + + categoryRules.forEach(rule => { + ruleMapping[rule.id] = { ...rule }; + }); + + const url = `/api/categories/${category.id}/read/`; + const options = { + method: 'POST', + headers: { + 'X-CSRFToken': token, + }, + }; + + return fetch(url, options) + .then(response => response.json()) + .then(updatedCategory => { + dispatch(receiveCategory({ ...updatedCategory })); + return dispatch( + markSectionRead({ + ...category, + ...updatedCategory, + rules: ruleMapping, + type: CATEGORY_TYPE, + }) + ); + }) + .catch(error => { + dispatch(receiveCategory({})); + dispatch(handleAPIError(error)); + }); + }; +}; + +const markRuleRead = (rule, token) => { + return (dispatch, getState) => { + dispatch(requestRule(rule)); + + const url = `/api/rules/${rule.id}/read/`; + const options = { + method: 'POST', + headers: { + 'X-CSRFToken': token, + }, + }; + + return fetch(url, options) + .then(response => response.json()) + .then(updatedRule => { + dispatch(receiveRule({ ...updatedRule })); + + // Use the old rule to decrement category with old unread count + dispatch(markSectionRead({ ...rule, type: RULE_TYPE })); + }) + .catch(error => { + dispatch(receiveRule({})); + dispatch(handleAPIError(error)); + }); + }; +}; + +export const markRead = (section, token) => { + switch (section.type) { + case RULE_TYPE: + return markRuleRead(section, token); + case CATEGORY_TYPE: + return markCategoryRead(section, token); + } +}; diff --git a/src/newsreader/js/pages/homepage/components/PostModal.js b/src/newsreader/js/pages/homepage/components/PostModal.js new file mode 100644 index 0000000..4abf710 --- /dev/null +++ b/src/newsreader/js/pages/homepage/components/PostModal.js @@ -0,0 +1,91 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import Cookies from 'js-cookie'; + +import { unSelectPost, markPostRead } from '../actions/posts.js'; +import { formatDatetime } from '../../../utils.js'; + +class PostModal extends React.Component { + modalListener = ::this.modalListener; + readTimer = null; + + componentDidMount() { + const post = { ...this.props.post }; + const markPostRead = this.props.markPostRead; + const token = Cookies.get('csrftoken'); + + if (!post.read) { + this.readTimer = setTimeout(markPostRead, 3000, post, token); + } + + window.addEventListener('click', this.modalListener); + } + + componentWillUnmount() { + if (this.readTimer) { + clearTimeout(this.readTimer); + } + + this.readTimer = null; + + window.removeEventListener('click', this.modalListener); + } + + modalListener(e) { + const targetClassName = e.target.className; + + if (this.props.post && targetClassName == 'modal post-modal') { + this.props.unSelectPost(); + } + } + + render() { + const post = this.props.post; + const publicationDate = formatDatetime(post.publicationDate); + const titleClassName = post.read ? 'post__title post__title--read' : 'post__title'; + + return ( +
    +
    + +
    +

    {`${post.title} `}

    +
    + {publicationDate} + + + +
    +
    + + + {/* HTML is sanitized by the collectors */} +
    +
    +
    + ); + } +} + +const mapDispatchToProps = dispatch => ({ + unSelectPost: () => dispatch(unSelectPost()), + markPostRead: (post, token) => dispatch(markPostRead(post, token)), +}); + +export default connect(null, mapDispatchToProps)(PostModal); diff --git a/src/newsreader/js/pages/homepage/components/feedlist/FeedList.js b/src/newsreader/js/pages/homepage/components/feedlist/FeedList.js new file mode 100644 index 0000000..e873965 --- /dev/null +++ b/src/newsreader/js/pages/homepage/components/feedlist/FeedList.js @@ -0,0 +1,86 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { isEqual } from 'lodash'; + +import { fetchPostsBySection } from '../../actions/posts.js'; +import { filterPosts } from './filters.js'; + +import LoadingIndicator from '../../../../components/LoadingIndicator.js'; +import RuleItem from './RuleItem.js'; + +class FeedList extends React.Component { + checkScrollHeight = ::this.checkScrollHeight; + + componentDidMount() { + window.addEventListener('scroll', this.checkScrollHeight); + } + + componentWillUnmount() { + window.removeEventListener('scroll', this.checkScrollHeight); + } + + checkScrollHeight(e) { + const currentHeight = window.scrollY + window.innerHeight; + const totalHeight = document.body.offsetHeight; + + const currentPercentage = (currentHeight / totalHeight) * 100; + + if (this.props.next && !this.props.lastReached) { + if (currentPercentage > 60 && !this.props.isFetching) { + this.paginate(); + } + } + } + + paginate() { + this.props.fetchPostsBySection(this.props.selected, this.props.next); + } + + render() { + const ruleItems = this.props.posts.map((item, index) => { + return ; + }); + + if (isEqual(this.props.selected, {})) { + return ( +
    +
    + +

    Select an item to show its unread posts

    +
    +
    + ); + } else if (ruleItems.length === 0 && !this.props.isFetching) { + return ( +
    +
    +

    + No unread posts from the selected section at this moment, try again later +

    +
    +
    + ); + } else { + return ( +
    + {ruleItems} + {this.props.isFetching && } +
    + ); + } + } +} + +const mapStateToProps = state => ({ + isFetching: state.posts.isFetching, + posts: filterPosts(state), + next: state.selected.next, + lastReached: state.selected.lastReached, + selected: state.selected.item, +}); + +const mapDispatchToProps = dispatch => ({ + fetchPostsBySection: (rule, page = false) => dispatch(fetchPostsBySection(rule, page)), +}); + +export default connect(mapStateToProps, mapDispatchToProps)(FeedList); diff --git a/src/newsreader/js/pages/homepage/components/feedlist/PostItem.js b/src/newsreader/js/pages/homepage/components/feedlist/PostItem.js new file mode 100644 index 0000000..a24b9c0 --- /dev/null +++ b/src/newsreader/js/pages/homepage/components/feedlist/PostItem.js @@ -0,0 +1,49 @@ +import React from 'react'; +import { connect } from 'react-redux'; + +import { selectPost } from '../../actions/posts.js'; + +import { formatDatetime } from '../../../../utils.js'; + +class PostItem extends React.Component { + render() { + const post = this.props.post; + const publicationDate = formatDatetime(post.publicationDate); + const titleClassName = post.read + ? 'posts-header__title posts-header__title--read' + : 'posts-header__title'; + + return ( +
  • { + this.props.selectPost(post); + }} + > +
    + {post.title} +
    + +
    + + {publicationDate} + + + + +
    +
  • + ); + } +} + +const mapDispatchToProps = dispatch => ({ + selectPost: post => dispatch(selectPost(post)), +}); + +export default connect(null, mapDispatchToProps)(PostItem); diff --git a/src/newsreader/js/pages/homepage/components/feedlist/RuleItem.js b/src/newsreader/js/pages/homepage/components/feedlist/RuleItem.js new file mode 100644 index 0000000..608e8a1 --- /dev/null +++ b/src/newsreader/js/pages/homepage/components/feedlist/RuleItem.js @@ -0,0 +1,24 @@ +import React from 'react'; + +import PostItem from './PostItem.js'; + +class RuleItem extends React.Component { + render() { + const posts = Object.values(this.props.posts).sort((firstEl, secondEl) => { + return new Date(secondEl.publicationDate) - new Date(firstEl.publicationDate); + }); + + const postItems = posts.map(post => { + return ; + }); + + return ( +
    +

    {this.props.rule.name}

    +
      {postItems}
    +
    + ); + } +} + +export default RuleItem; diff --git a/src/newsreader/js/pages/homepage/components/feedlist/filters.js b/src/newsreader/js/pages/homepage/components/feedlist/filters.js new file mode 100644 index 0000000..a3ee886 --- /dev/null +++ b/src/newsreader/js/pages/homepage/components/feedlist/filters.js @@ -0,0 +1,46 @@ +import { CATEGORY_TYPE, RULE_TYPE } from '../../constants.js'; + +const isEmpty = (object = {}) => { + return Object.keys(object).length === 0; +}; + +export const filterPostsByRule = (rule = {}, posts = []) => { + const filteredPosts = posts.filter(post => { + return post.rule === rule.id; + }); + + return filteredPosts.length > 0 ? [{ rule, posts: filteredPosts }] : []; +}; + +export const filterPostsByCategory = (category = {}, rules = [], posts = []) => { + const filteredRules = rules.filter(rule => { + return rule.category === category.id; + }); + + const filteredData = filteredRules.map(rule => { + const filteredPosts = posts.filter(post => { + return post.rule === rule.id; + }); + + return { + rule: { ...rule }, + posts: filteredPosts, + }; + }); + + return filteredData.filter(rule => rule.posts.length > 0); +}; + +export const filterPosts = state => { + const posts = Object.values({ ...state.posts.items }); + + switch (state.selected.item.type) { + case CATEGORY_TYPE: + const rules = Object.values({ ...state.rules.items }); + return filterPostsByCategory({ ...state.selected.item }, rules, posts); + case RULE_TYPE: + return filterPostsByRule({ ...state.selected.item }, posts); + } + + return []; +}; diff --git a/src/newsreader/js/pages/homepage/components/sidebar/CategoryItem.js b/src/newsreader/js/pages/homepage/components/sidebar/CategoryItem.js new file mode 100644 index 0000000..d1a0c94 --- /dev/null +++ b/src/newsreader/js/pages/homepage/components/sidebar/CategoryItem.js @@ -0,0 +1,62 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { isEqual } from 'lodash'; + +import { CATEGORY_TYPE } from '../../constants.js'; +import { selectCategory, fetchCategory } from '../../actions/categories.js'; +import { fetchPostsBySection } from '../../actions/posts.js'; +import { isSelected } from './functions.js'; +import RuleItem from './RuleItem.js'; + +class CategoryItem extends React.Component { + state = { open: false }; + + toggleRules() { + this.setState({ open: !this.state.open }); + } + + handleSelect() { + const category = this.props.category; + + this.props.selectCategory(category); + this.props.fetchPostsBySection({ ...category, type: CATEGORY_TYPE }); + this.props.fetchCategory(category); + } + + render() { + const chevronClass = this.state.open ? 'gg-chevron-down' : 'gg-chevron-right'; + const selected = isSelected(this.props.category, this.props.selected, CATEGORY_TYPE); + const className = selected ? 'category category--selected' : 'category'; + + const ruleItems = this.props.rules.map(rule => { + return ; + }); + + return ( +
  • +
    +
    this.toggleRules()}> + +
    + +
    this.handleSelect()}> +

    {this.props.category.name}

    + {this.props.category.unread} +
    +
    + + {ruleItems.length > 0 && this.state.open && ( +
      {ruleItems}
    + )} +
  • + ); + } +} + +const mapDispatchToProps = dispatch => ({ + selectCategory: category => dispatch(selectCategory(category)), + fetchPostsBySection: section => dispatch(fetchPostsBySection(section)), + fetchCategory: category => dispatch(fetchCategory(category)), +}); + +export default connect(null, mapDispatchToProps)(CategoryItem); diff --git a/src/newsreader/js/pages/homepage/components/sidebar/ReadButton.js b/src/newsreader/js/pages/homepage/components/sidebar/ReadButton.js new file mode 100644 index 0000000..3d33fc0 --- /dev/null +++ b/src/newsreader/js/pages/homepage/components/sidebar/ReadButton.js @@ -0,0 +1,33 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import Cookies from 'js-cookie'; + +import { markRead } from '../../actions/selected.js'; + +class ReadButton extends React.Component { + markSelectedRead = ::this.markSelectedRead; + + markSelectedRead() { + const token = Cookies.get('csrftoken'); + + if (this.props.selected.unread > 0) { + this.props.markRead({ ...this.props.selected }, token); + } + } + + render() { + return ( + + ); + } +} + +const mapDispatchToProps = dispatch => ({ + markRead: (selected, token) => dispatch(markRead(selected, token)), +}); + +const mapStateToProps = state => ({ selected: state.selected.item }); + +export default connect(mapStateToProps, mapDispatchToProps)(ReadButton); diff --git a/src/newsreader/js/pages/homepage/components/sidebar/RuleItem.js b/src/newsreader/js/pages/homepage/components/sidebar/RuleItem.js new file mode 100644 index 0000000..879745f --- /dev/null +++ b/src/newsreader/js/pages/homepage/components/sidebar/RuleItem.js @@ -0,0 +1,50 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { isEqual } from 'lodash'; + +import { RULE_TYPE } from '../../constants.js'; +import { selectRule, fetchRule } from '../../actions/rules.js'; +import { fetchPostsBySection } from '../../actions/posts.js'; +import { isSelected } from './functions.js'; + +class RuleItem extends React.Component { + handleSelect() { + const rule = { ...this.props.rule }; + + this.props.selectRule(rule); + this.props.fetchPostsBySection({ ...rule, type: RULE_TYPE }); + this.props.fetchRule(rule); + } + + render() { + const selected = isSelected(this.props.rule, this.props.selected, RULE_TYPE); + const className = `rules__item ${selected ? 'rules__item--selected' : ''}`; + let favicon = null; + + if (this.props.rule.favicon) { + favicon = ; + } else { + favicon = ; + } + + return ( +
  • this.handleSelect()}> +
    + {favicon} +
    + {this.props.rule.name} +
    +
    + {this.props.rule.unread} +
  • + ); + } +} + +const mapDispatchToProps = dispatch => ({ + selectRule: rule => dispatch(selectRule(rule)), + fetchPostsBySection: section => dispatch(fetchPostsBySection(section)), + fetchRule: rule => dispatch(fetchRule(rule)), +}); + +export default connect(null, mapDispatchToProps)(RuleItem); diff --git a/src/newsreader/js/pages/homepage/components/sidebar/Sidebar.js b/src/newsreader/js/pages/homepage/components/sidebar/Sidebar.js new file mode 100644 index 0000000..3780afb --- /dev/null +++ b/src/newsreader/js/pages/homepage/components/sidebar/Sidebar.js @@ -0,0 +1,49 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { isEqual } from 'lodash'; + +import { filterCategories, filterRules } from './filters.js'; + +import LoadingIndicator from '../../../../components/LoadingIndicator.js'; +import CategoryItem from './CategoryItem.js'; +import ReadButton from './ReadButton.js'; + +// TODO: show empty category message +class Sidebar extends React.Component { + render() { + const items = this.props.categories.items.map(category => { + const rules = this.props.rules.items.filter(rule => { + return rule.category === category.id; + }); + + return ( + + ); + }); + + return ( +
    + {(this.props.categories.isFetching || this.props.rules.isFetching) && ( + + )} + +
      {items}
    + + {!isEqual(this.props.selected.item, {}) && } +
    + ); + } +} + +const mapStateToProps = state => ({ + categories: { ...state.categories, items: filterCategories(state.categories.items) }, + rules: { ...state.rules, items: filterRules(state.rules.items) }, + selected: state.selected, +}); + +export default connect(mapStateToProps)(Sidebar); diff --git a/src/newsreader/js/pages/homepage/components/sidebar/filters.js b/src/newsreader/js/pages/homepage/components/sidebar/filters.js new file mode 100644 index 0000000..5e51d6c --- /dev/null +++ b/src/newsreader/js/pages/homepage/components/sidebar/filters.js @@ -0,0 +1,7 @@ +export const filterCategories = (categories = {}) => { + return Object.values({ ...categories }); +}; + +export const filterRules = (rules = {}) => { + return Object.values({ ...rules }); +}; diff --git a/src/newsreader/js/pages/homepage/components/sidebar/functions.js b/src/newsreader/js/pages/homepage/components/sidebar/functions.js new file mode 100644 index 0000000..06ac716 --- /dev/null +++ b/src/newsreader/js/pages/homepage/components/sidebar/functions.js @@ -0,0 +1,7 @@ +export const isSelected = (section, selected, type) => { + if (!selected || selected.type != type) { + return false; + } + + return section.id === selected.id; +}; diff --git a/src/newsreader/js/pages/homepage/configureStore.js b/src/newsreader/js/pages/homepage/configureStore.js new file mode 100644 index 0000000..e00952f --- /dev/null +++ b/src/newsreader/js/pages/homepage/configureStore.js @@ -0,0 +1,18 @@ +import { createStore, applyMiddleware } from 'redux'; +import thunkMiddleware from 'redux-thunk'; + +import { createLogger } from 'redux-logger'; + +import rootReducer from './reducers/index.js'; + +const loggerMiddleware = createLogger(); + +const configureStore = preloadedState => { + return createStore( + rootReducer, + preloadedState, + applyMiddleware(thunkMiddleware, loggerMiddleware) + ); +}; + +export default configureStore; diff --git a/src/newsreader/js/pages/homepage/constants.js b/src/newsreader/js/pages/homepage/constants.js new file mode 100644 index 0000000..0e3f3d3 --- /dev/null +++ b/src/newsreader/js/pages/homepage/constants.js @@ -0,0 +1,2 @@ +export const RULE_TYPE = 'RULE'; +export const CATEGORY_TYPE = 'CATEGORY'; diff --git a/src/newsreader/js/pages/homepage/index.js b/src/newsreader/js/pages/homepage/index.js new file mode 100644 index 0000000..c16ed39 --- /dev/null +++ b/src/newsreader/js/pages/homepage/index.js @@ -0,0 +1,20 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; + +import { Provider } from 'react-redux'; +import configureStore from './configureStore.js'; + +import App from './App.js'; + +const page = document.getElementById('homepage--page'); + +if (page) { + const store = configureStore(); + + ReactDOM.render( + + + , + page + ); +} diff --git a/src/newsreader/js/pages/homepage/reducers/categories.js b/src/newsreader/js/pages/homepage/reducers/categories.js new file mode 100644 index 0000000..612b98f --- /dev/null +++ b/src/newsreader/js/pages/homepage/reducers/categories.js @@ -0,0 +1,93 @@ +import { isEqual } from 'lodash'; + +import { CATEGORY_TYPE, RULE_TYPE } from '../constants.js'; + +import { objectsFromArray } from '../../../utils.js'; + +import { + RECEIVE_CATEGORY, + RECEIVE_CATEGORIES, + REQUEST_CATEGORY, + REQUEST_CATEGORIES, +} from '../actions/categories.js'; + +import { MARK_POST_READ } from '../actions/posts.js'; +import { MARK_SECTION_READ } from '../actions/selected.js'; + +const defaultState = { items: {}, isFetching: false }; + +export const categories = (state = { ...defaultState }, action) => { + switch (action.type) { + case RECEIVE_CATEGORY: + return { + ...state, + items: { + ...state.items, + [action.category.id]: { ...action.category }, + }, + isFetching: false, + }; + case RECEIVE_CATEGORIES: + const receivedCategories = objectsFromArray(action.categories, 'id'); + + return { + ...state, + items: { ...state.items, ...receivedCategories }, + isFetching: false, + }; + case REQUEST_CATEGORIES: + case REQUEST_CATEGORY: + return { ...state, isFetching: true }; + case MARK_POST_READ: + let category = {}; + + switch (action.section.type) { + case CATEGORY_TYPE: + category = { ...state.items[action.section.id] }; + break; + case RULE_TYPE: + category = { ...state.items[action.section.category] }; + break; + } + + return { + ...state, + items: { + ...state.items, + [category.id]: { ...category, unread: category.unread - 1 }, + }, + }; + case MARK_SECTION_READ: + category = {}; + + switch (action.section.type) { + case CATEGORY_TYPE: + category = { ...state.items[action.section.id] }; + + return { + ...state, + items: { + ...state.items, + [category.id]: { ...category, unread: 0 }, + }, + }; + case RULE_TYPE: + category = { ...state.items[action.section.category] }; + + return { + ...state, + items: { + ...state.items, + [category.id]: { + ...category, + unread: category.unread - action.section.unread, + }, + }, + }; + } + + return state; + default: + return state; + } +}; diff --git a/src/newsreader/js/pages/homepage/reducers/error.js b/src/newsreader/js/pages/homepage/reducers/error.js new file mode 100644 index 0000000..3d20b14 --- /dev/null +++ b/src/newsreader/js/pages/homepage/reducers/error.js @@ -0,0 +1,12 @@ +import { RECEIVE_API_ERROR } from '../actions/error.js'; + +const defaultState = {}; + +export const error = (state = { ...defaultState }, action) => { + switch (action.type) { + case RECEIVE_API_ERROR: + return { ...state, error: action.error }; + default: + return {}; + } +}; diff --git a/src/newsreader/js/pages/homepage/reducers/index.js b/src/newsreader/js/pages/homepage/reducers/index.js new file mode 100644 index 0000000..20dea78 --- /dev/null +++ b/src/newsreader/js/pages/homepage/reducers/index.js @@ -0,0 +1,11 @@ +import { combineReducers } from 'redux'; + +import { categories } from './categories.js'; +import { error } from './error.js'; +import { rules } from './rules.js'; +import { posts } from './posts.js'; +import { selected } from './selected.js'; + +const rootReducer = combineReducers({ categories, error, rules, posts, selected }); + +export default rootReducer; diff --git a/src/newsreader/js/pages/homepage/reducers/posts.js b/src/newsreader/js/pages/homepage/reducers/posts.js new file mode 100644 index 0000000..220c59b --- /dev/null +++ b/src/newsreader/js/pages/homepage/reducers/posts.js @@ -0,0 +1,68 @@ +import { isEqual } from 'lodash'; + +import { objectsFromArray } from '../../../utils.js'; +import { CATEGORY_TYPE, RULE_TYPE } from '../constants.js'; + +import { + SELECT_POST, + RECEIVE_POST, + RECEIVE_POSTS, + REQUEST_POSTS, +} from '../actions/posts.js'; +import { SELECT_CATEGORY } from '../actions/categories.js'; +import { SELECT_RULE } from '../actions/rules.js'; +import { MARK_SECTION_READ } from '../actions/selected.js'; + +const defaultState = { items: {}, isFetching: false }; + +export const posts = (state = { ...defaultState }, action) => { + switch (action.type) { + case REQUEST_POSTS: + return { ...state, isFetching: true }; + case RECEIVE_POST: + return { + ...state, + items: { ...state.items, [action.post.id]: { ...action.post } }, + }; + case RECEIVE_POSTS: + const receivedItems = objectsFromArray(action.posts, 'id'); + + return { + ...state, + isFetching: false, + items: { ...state.items, ...receivedItems }, + }; + case MARK_SECTION_READ: + const updatedPosts = {}; + let relatedPosts = []; + + switch (action.section.type) { + case CATEGORY_TYPE: + relatedPosts = Object.values({ ...state.items }).filter(post => { + return post.rule in { ...action.section.rules }; + }); + + break; + case RULE_TYPE: + relatedPosts = Object.values({ ...state.items }).filter(post => { + return post.rule === action.section.id; + }); + + break; + } + + relatedPosts.forEach(post => { + updatedPosts[post.id] = { ...post, read: true }; + }); + + return { + ...state, + items: { + ...state.items, + ...updatedPosts, + }, + }; + default: + return state; + } +}; diff --git a/src/newsreader/js/pages/homepage/reducers/rules.js b/src/newsreader/js/pages/homepage/reducers/rules.js new file mode 100644 index 0000000..ea3480c --- /dev/null +++ b/src/newsreader/js/pages/homepage/reducers/rules.js @@ -0,0 +1,82 @@ +import { isEqual } from 'lodash'; + +import { objectsFromArray } from '../../../utils.js'; + +import { CATEGORY_TYPE, RULE_TYPE } from '../constants.js'; + +import { + REQUEST_RULES, + REQUEST_RULE, + RECEIVE_RULES, + RECEIVE_RULE, +} from '../actions/rules.js'; +import { MARK_POST_READ } from '../actions/posts.js'; +import { MARK_SECTION_READ } from '../actions/selected.js'; + +const defaultState = { items: {}, isFetching: false }; + +export const rules = (state = { ...defaultState }, action) => { + switch (action.type) { + case REQUEST_RULE: + case REQUEST_RULES: + return { ...state, isFetching: true }; + case RECEIVE_RULES: + const receivedItems = objectsFromArray(action.rules, 'id'); + + return { + ...state, + items: { ...state.items, ...receivedItems }, + isFetching: false, + }; + case RECEIVE_RULE: + return { + ...state, + items: { ...state.items, [action.rule.id]: { ...action.rule } }, + isFetching: false, + }; + case MARK_POST_READ: + const rule = { ...state.items[action.post.rule] }; + + return { + ...state, + items: { + ...state.items, + [rule.id]: { ...rule, unread: rule.unread - 1 }, + }, + }; + case MARK_SECTION_READ: + switch (action.section.type) { + case RULE_TYPE: + const rule = { ...state.items[action.section.id] }; + + return { + ...state, + items: { + ...state.items, + [rule.id]: { ...rule, unread: 0 }, + }, + }; + case CATEGORY_TYPE: + const updatedRules = {}; + const categoryRules = Object.values({ ...state.items }).filter(rule => { + return rule.category === action.section.id; + }); + + categoryRules.forEach(rule => { + updatedRules[rule.id] = { ...rule, unread: 0 }; + }); + + return { + ...state, + items: { + ...state.items, + ...updatedRules, + }, + }; + } + + return state; + default: + return state; + } +}; diff --git a/src/newsreader/js/pages/homepage/reducers/selected.js b/src/newsreader/js/pages/homepage/reducers/selected.js new file mode 100644 index 0000000..babcb82 --- /dev/null +++ b/src/newsreader/js/pages/homepage/reducers/selected.js @@ -0,0 +1,86 @@ +import { isEqual } from 'lodash'; + +import { SELECT_CATEGORY } from '../actions/categories.js'; +import { SELECT_RULE } from '../actions/rules.js'; +import { + RECEIVE_POST, + RECEIVE_POSTS, + SELECT_POST, + UNSELECT_POST, +} from '../actions/posts.js'; + +import { MARK_SECTION_READ } from '../actions/selected.js'; +import { MARK_POST_READ } from '../actions/posts.js'; + +const defaultState = { item: {}, next: false, lastReached: false, post: {} }; + +export const selected = (state = { ...defaultState }, action) => { + switch (action.type) { + case SELECT_CATEGORY: + case SELECT_RULE: + if (state.item) { + if ( + state.item.id === action.section.id && + state.item.type === action.section.type + ) { + if (state.item.clicks >= 2) { + return { + ...state, + item: { ...action.section, clicks: 1 }, + next: false, + lastReached: false, + }; + } + + return { + ...state, + item: { ...action.section, clicks: state.item.clicks + 1 }, + next: false, + lastReached: false, + }; + } + } + + return { + ...state, + item: { ...action.section, clicks: 1 }, + next: false, + lastReached: false, + }; + case RECEIVE_POSTS: + return { + ...state, + next: action.next, + lastReached: !action.next, + }; + case RECEIVE_POST: + const isCurrentPost = !isEqual(state.post, {}) && state.post.id === action.post.id; + + if (isCurrentPost) { + return { + ...state, + post: { ...action.post }, + }; + } + + return { + ...state, + }; + case SELECT_POST: + return { ...state, post: action.post }; + case UNSELECT_POST: + return { ...state, post: {} }; + case MARK_POST_READ: + return { + ...state, + item: { ...action.section, unread: action.section.unread - 1 }, + }; + case MARK_SECTION_READ: + return { + ...state, + item: { ...action.section, clicks: 0, unread: 0 }, + }; + default: + return state; + } +}; diff --git a/src/newsreader/js/pages/rules/App.js b/src/newsreader/js/pages/rules/App.js new file mode 100644 index 0000000..7ceae4a --- /dev/null +++ b/src/newsreader/js/pages/rules/App.js @@ -0,0 +1,106 @@ +import React from 'react'; + +import Cookies from 'js-cookie'; + +import Card from '../../components/Card.js'; +import RuleCard from './components/RuleCard.js'; +import RuleModal from './components/RuleModal.js'; +import Messages from '../../components/Messages.js'; + +class App extends React.Component { + selectRule = ::this.selectRule; + deselectRule = ::this.deselectRule; + deleteRule = ::this.deleteRule; + + constructor(props) { + super(props); + + this.token = Cookies.get('csrftoken'); + this.state = { + rules: props.rules, + selectedRuleId: null, + message: null, + }; + } + + selectRule(ruleId) { + this.setState({ selectedRuleId: ruleId }); + } + + deselectRule() { + this.setState({ selectedRuleId: null }); + } + + deleteRule(ruleId) { + const url = `/api/rules/${ruleId}/`; + const options = { + method: 'DELETE', + headers: { + 'X-CSRFToken': this.token, + }, + }; + + fetch(url, options).then(response => { + if (response.ok) { + const rules = this.state.rules.filter(rule => { + return rule.pk != ruleId; + }); + + return this.setState({ + rules: rules, + selectedRuleId: null, + message: null, + }); + } + }); + + const message = { + type: 'error', + text: 'Unable to remove rule, try again later', + }; + return this.setState({ selectedRuleId: null, message: message }); + } + + render() { + const { rules } = this.state; + const cards = rules.map(rule => { + return ; + }); + + const selectedRule = rules.find(rule => { + return rule.pk === this.state.selectedRuleId; + }); + + const pageHeader = ( + <> +

    Rules

    + + + + ); + + return ( + <> + {this.state.message && } + + {cards} + {selectedRule && ( + + )} + + ); + } +} + +export default App; diff --git a/src/newsreader/js/pages/rules/components/RuleCard.js b/src/newsreader/js/pages/rules/components/RuleCard.js new file mode 100644 index 0000000..d74b8d1 --- /dev/null +++ b/src/newsreader/js/pages/rules/components/RuleCard.js @@ -0,0 +1,65 @@ +import React from 'react'; + +import Card from '../../../components/Card.js'; + +const RuleCard = props => { + const { rule } = props; + let favicon = null; + + if (rule.favicon) { + favicon = ; + } else { + favicon = ; + } + + const stateIcon = !rule.error ? 'gg-check' : 'gg-danger'; + + const cardHeader = ( + <> + +

    {rule.name}

    + {favicon} + + ); + + const cardContent = ( + <> +
      + {rule.error && ( +
        +
      • {rule.error}
      • +
      + )} + + {rule.category &&
    • {rule.category}
    • } +
    • + + {rule.url} + +
    • +
    • {rule.created}
    • +
    • {rule.timezone}
    • +
    + + ); + + const cardFooter = ( + <> + + Edit + + + + ); + + return ; +}; + +export default RuleCard; diff --git a/src/newsreader/js/pages/rules/components/RuleModal.js b/src/newsreader/js/pages/rules/components/RuleModal.js new file mode 100644 index 0000000..d174cc3 --- /dev/null +++ b/src/newsreader/js/pages/rules/components/RuleModal.js @@ -0,0 +1,35 @@ +import React from 'react'; + +import Modal from '../../../components/Modal.js'; + +const RuleModal = props => { + const content = ( + <> +
    +
    +

    Delete rule

    +
    + +
    +

    Are you sure you want to delete {props.rule.name}?

    +
    + +
    + + +
    +
    + + ); + + return ; +}; + +export default RuleModal; diff --git a/src/newsreader/js/pages/rules/index.js b/src/newsreader/js/pages/rules/index.js new file mode 100644 index 0000000..d0b46e9 --- /dev/null +++ b/src/newsreader/js/pages/rules/index.js @@ -0,0 +1,13 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; + +import App from './App.js'; + +const page = document.getElementById('rules--page'); + +if (page) { + const dataScript = document.getElementById('rules-data'); + const rules = JSON.parse(dataScript.textContent); + + ReactDOM.render(, page); +} diff --git a/src/newsreader/js/tests/homepage/actions/category.test.js b/src/newsreader/js/tests/homepage/actions/category.test.js new file mode 100644 index 0000000..8c0266c --- /dev/null +++ b/src/newsreader/js/tests/homepage/actions/category.test.js @@ -0,0 +1,316 @@ +import configureMockStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; +import fetchMock from 'fetch-mock'; + +import * as actions from '../../../pages/homepage/actions/categories.js'; +import * as constants from '../../../pages/homepage/constants.js'; +import * as ruleActions from '../../../pages/homepage/actions/rules.js'; +import * as errorActions from '../../../pages/homepage/actions/error.js'; + +const middlewares = [thunk]; +const mockStore = configureMockStore(middlewares); + +describe('category actions', () => { + afterEach(() => { + fetchMock.restore(); + }); + + it('should create an action to select a category', () => { + const category = { id: 1, name: 'Test category', unread: 100 }; + + const expectedAction = { + section: { ...category, type: constants.CATEGORY_TYPE }, + type: actions.SELECT_CATEGORY, + }; + + expect(actions.selectCategory(category)).toEqual(expectedAction); + }); + + it('should create an action to receive a category', () => { + const category = { id: 1, name: 'Test category', unread: 100 }; + + const expectedAction = { + type: actions.RECEIVE_CATEGORY, + category, + }; + + expect(actions.receiveCategory(category)).toEqual(expectedAction); + }); + + it('should create an action to receive multiple categories', () => { + const categories = [ + { id: 1, name: 'Test category 1', unread: 200 }, + { id: 2, name: 'Test category 2', unread: 500 }, + { id: 3, name: 'Test category 3', unread: 600 }, + ]; + + const expectedAction = { + type: actions.RECEIVE_CATEGORIES, + categories, + }; + + expect(actions.receiveCategories(categories)).toEqual(expectedAction); + }); + + it('should create an action to request a category', () => { + const expectedAction = { type: actions.REQUEST_CATEGORY }; + + expect(actions.requestCategory()).toEqual(expectedAction); + }); + + it('should create an action to request multiple categories', () => { + const expectedAction = { type: actions.REQUEST_CATEGORIES }; + + expect(actions.requestCategories()).toEqual(expectedAction); + }); + + it('should create multiple actions when fetching a category', () => { + const category = { + id: 1, + name: 'Tech', + unread: 1138, + }; + + fetchMock.getOnce('/api/categories/1', { + body: { ...category, unread: 500 }, + headers: { 'content-type': 'application/json' }, + }); + + const expectedActions = [ + { type: actions.REQUEST_CATEGORY }, + { + type: actions.RECEIVE_CATEGORY, + category: { ...category, unread: 500 }, + }, + ]; + + const store = mockStore({ + categories: { items: {}, isFetching: false }, + rules: { items: {}, isFetching: false }, + posts: { items: {}, isFetching: false }, + selected: { item: {}, next: false, lastReached: false, post: {} }, + }); + + return store.dispatch(actions.fetchCategory(category)).then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + }); + + it('should create multiple actions when fetching categories', () => { + const categories = [ + { id: 1, name: 'Tech', unread: 29 }, + { id: 2, name: 'World news', unread: 956 }, + ]; + + const rules = [ + { + id: 5, + name: 'Ars Technica', + url: 'http://feeds.arstechnica.com/arstechnica/index?fmt=xml', + favicon: 'https://cdn.arstechnica.net/favicon.ico', + category: 1, + unread: 7, + }, + { + id: 6, + name: 'BBC', + url: 'http://feeds.bbci.co.uk/news/world/rss.xml', + favicon: + 'https://m.files.bbci.co.uk/modules/bbc-morph-news-waf-page-meta/2.5.2/apple-touch-icon-57x57-precomposed.png', + category: 2, + unread: 345, + }, + ]; + + fetchMock + .get('/api/categories/', { + body: categories, + headers: { 'content-type': 'application/json' }, + }) + .get('/api/categories/1/rules/', { + body: [{ ...rules[0] }], + headers: { 'content-type': 'application/json' }, + }) + .get('/api/categories/2/rules/', { + body: [{ ...rules[1] }], + headers: { 'content-type': 'application/json' }, + }); + + const expectedActions = [ + { type: actions.REQUEST_CATEGORIES }, + { type: actions.RECEIVE_CATEGORIES, categories }, + { type: ruleActions.REQUEST_RULES }, + { type: ruleActions.RECEIVE_RULES, rules }, + ]; + + const store = mockStore({ + categories: { items: {}, isFetching: false }, + rules: { items: {}, isFetching: false }, + posts: { items: {}, isFetching: false }, + selected: { item: {}, next: false, lastReached: false, post: {} }, + }); + + return store.dispatch(actions.fetchCategories()).then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + }); + + it('should create multiple actions when fetching a category which is read', () => { + const category = { + id: 1, + name: 'Tech', + unread: 0, + }; + + const rules = [ + { + id: 1, + name: 'Ars Technica', + url: 'http://feeds.arstechnica.com/arstechnica/index?fmt=xml', + favicon: 'https://cdn.arstechnica.net/favicon.ico', + category: 1, + unread: 200, + }, + { + id: 2, + name: 'Hacker News', + url: 'https://news.ycombinator.com/rss', + favicon: 'https://news.ycombinator.com/favicon.ico', + category: 1, + unread: 350, + }, + ]; + + fetchMock + .get('/api/categories/1', { + body: { ...category, unread: 500 }, + headers: { 'content-type': 'application/json' }, + }) + .get('/api/categories/1/rules/', { + body: rules, + headers: { 'content-type': 'application/json' }, + }); + + const expectedActions = [ + { type: actions.REQUEST_CATEGORY }, + { + type: actions.RECEIVE_CATEGORY, + category: { ...category, unread: 500 }, + }, + { type: ruleActions.REQUEST_RULES }, + { type: ruleActions.RECEIVE_RULES, rules }, + ]; + + const store = mockStore({ + categories: { items: {}, isFetching: false }, + rules: { items: {}, isFetching: false }, + posts: { items: {}, isFetching: false }, + selected: { + item: { ...category, type: constants.CATEGORY_TYPE, clicks: 2 }, + next: false, + lastReached: false, + post: {}, + }, + }); + + return store.dispatch(actions.fetchCategory(category)).then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + }); + + it('should create no actions for a category which is selected less than x', () => { + const category = { + id: 1, + name: 'Tech', + unread: 200, + }; + + fetchMock.getOnce('/api/categories/1', { + body: { ...category, unread: 100 }, + headers: { 'content-type': 'application/json' }, + }); + + const store = mockStore({ + categories: { items: {}, isFetching: false }, + rules: { items: {}, isFetching: false }, + posts: { items: {}, isFetching: false }, + selected: { + item: { ...category, type: constants.CATEGORY_TYPE, clicks: 1 }, + next: false, + lastReached: false, + post: {}, + }, + }); + + const expectedActions = []; + + store.dispatch(actions.fetchCategory(category)); + + expect(store.getActions()).toEqual(expectedActions); + }); + + it('should handle an unexpected response when fetching a category', () => { + const category = { + id: 1, + name: 'Tech', + unread: 1138, + }; + + const errorMessage = 'Key id not found'; + + fetchMock.getOnce('/api/categories/1', () => { + throw new TypeError(errorMessage); + }); + + const expectedActions = [ + { type: actions.REQUEST_CATEGORY }, + { type: actions.RECEIVE_CATEGORY, category: {} }, + { type: errorActions.RECEIVE_API_ERROR, error: TypeError(errorMessage) }, + ]; + + const store = mockStore({ + categories: { items: {}, isFetching: false }, + rules: { items: {}, isFetching: false }, + posts: { items: {}, isFetching: false }, + selected: { item: {}, next: false, lastReached: false, post: {} }, + error: { error: {} }, + }); + + return store.dispatch(actions.fetchCategory(category)).then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + }); + + it('should handle an unexpected response when multiple categories', () => { + const category = { + id: 1, + name: 'Tech', + unread: 1138, + }; + + const errorMessage = 'URL not found'; + + fetchMock.getOnce('/api/categories/', () => { + throw new Error(errorMessage); + }); + + const expectedActions = [ + { type: actions.REQUEST_CATEGORIES }, + { type: actions.RECEIVE_CATEGORIES, categories: [] }, + { type: ruleActions.RECEIVE_RULES, rules: [] }, + { type: errorActions.RECEIVE_API_ERROR, error: Error(errorMessage) }, + ]; + + const store = mockStore({ + categories: { items: {}, isFetching: false }, + rules: { items: {}, isFetching: false }, + posts: { items: {}, isFetching: false }, + selected: { item: {}, next: false, lastReached: false, post: {} }, + error: { error: {} }, + }); + + return store.dispatch(actions.fetchCategories()).then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + }); +}); diff --git a/src/newsreader/js/tests/homepage/actions/post.test.js b/src/newsreader/js/tests/homepage/actions/post.test.js new file mode 100644 index 0000000..e8f84de --- /dev/null +++ b/src/newsreader/js/tests/homepage/actions/post.test.js @@ -0,0 +1,408 @@ +import configureMockStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; +import fetchMock from 'fetch-mock'; + +import * as actions from '../../../pages/homepage/actions/posts.js'; +import * as errorActions from '../../../pages/homepage/actions/error.js'; +import * as constants from '../../../pages/homepage/constants.js'; + +const middlewares = [thunk]; +const mockStore = configureMockStore(middlewares); + +describe('post actions', () => { + afterEach(() => { + fetchMock.restore(); + }); + + it('should create an action request posts', () => { + const expectedAction = { type: actions.REQUEST_POSTS }; + + expect(actions.requestPosts()).toEqual(expectedAction); + }); + + it('should create an action receive a post', () => { + const post = { + id: 2067, + remoteIdentifier: 'https://arstechnica.com/?p=1648607', + title: + 'This amazing glitch puts Star Fox 64 ships in an unmodified Zelda cartridge', + body: + '"Stale-reference manipulation," 300-character file names, and a clash between worlds.', + author: 'Kyle Orland', + publicationDate: '2020-01-24T19:50:12Z', + url: 'https://arstechnica.com/?p=1648607', + rule: 5, + read: false, + }; + + const expectedAction = { + type: actions.RECEIVE_POST, + post, + }; + + expect(actions.receivePost(post)).toEqual(expectedAction); + }); + + it('should create an action to select a post', () => { + const post = { + id: 2067, + remoteIdentifier: 'https://arstechnica.com/?p=1648607', + title: + 'This amazing glitch puts Star Fox 64 ships in an unmodified Zelda cartridge', + body: + '"Stale-reference manipulation," 300-character file names, and a clash between worlds.', + author: 'Kyle Orland', + publicationDate: '2020-01-24T19:50:12Z', + url: 'https://arstechnica.com/?p=1648607', + rule: 5, + read: false, + }; + + const expectedAction = { + type: actions.SELECT_POST, + post, + }; + + expect(actions.selectPost(post)).toEqual(expectedAction); + }); + + it('should create an action to unselect a post', () => { + const expectedAction = { type: actions.UNSELECT_POST }; + + expect(actions.unSelectPost()).toEqual(expectedAction); + }); + + it('should create an action mark a post read', () => { + const post = { + id: 2067, + remoteIdentifier: 'https://arstechnica.com/?p=1648607', + title: + 'This amazing glitch puts Star Fox 64 ships in an unmodified Zelda cartridge', + body: + '"Stale-reference manipulation," 300-character file names, and a clash between worlds.', + author: 'Kyle Orland', + publicationDate: '2020-01-24T19:50:12Z', + url: 'https://arstechnica.com/?p=1648607', + rule: 5, + read: false, + }; + + const rule = { + id: 4, + name: 'Ars Technica', + unread: 100, + category: 1, + url: 'http://feeds.arstechnica.com/arstechnica/index?fmt=xml', + favicon: 'https://cdn.arstechnica.net/favicon.ico', + }; + + const expectedAction = { + type: actions.MARK_POST_READ, + section: rule, + post, + }; + + expect(actions.postRead(post, rule)).toEqual(expectedAction); + }); + + it('should create multiple actions to mark post read', () => { + const post = { + id: 2067, + remoteIdentifier: 'https://arstechnica.com/?p=1648607', + title: + 'This amazing glitch puts Star Fox 64 ships in an unmodified Zelda cartridge', + body: + '"Stale-reference manipulation," 300-character file names, and a clash between worlds.', + author: 'Kyle Orland', + publicationDate: '2020-01-24T19:50:12Z', + url: 'https://arstechnica.com/?p=1648607', + rule: 5, + read: false, + }; + + const rule = { + id: 1, + name: 'Test rule', + unread: 100, + category: 1, + url: 'http://feeds.arstechnica.com/arstechnica/index?fmt=xml', + favicon: 'https://cdn.arstechnica.net/favicon.ico', + }; + + fetchMock.patchOnce('/api/posts/2067/', { + body: { ...post, read: true }, + headers: { 'content-type': 'application/json' }, + }); + + const store = mockStore({ + categories: { items: {}, isFetching: false }, + rules: { items: {}, isFetching: false }, + posts: { items: {}, isFetching: false }, + selected: { + item: rule, + next: false, + lastReached: false, + post: {}, + }, + }); + + const expectedActions = [ + { + type: actions.RECEIVE_POST, + post: { ...post, read: true }, + }, + { + type: actions.MARK_POST_READ, + post: { ...post, read: true }, + section: rule, + }, + ]; + + return store.dispatch(actions.markPostRead(post, 'TOKEN')).then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + }); + + it('should create multiple actions to fetch posts by rule', () => { + const posts = [ + { + id: 2067, + remoteIdentifier: 'https://arstechnica.com/?p=1648607', + title: + 'This amazing glitch puts Star Fox 64 ships in an unmodified Zelda cartridge', + body: + '"Stale-reference manipulation," 300-character file names, and a clash between worlds.', + author: 'Kyle Orland', + publicationDate: '2020-01-24T19:50:12Z', + url: 'https://arstechnica.com/?p=1648607', + rule: 4, + read: false, + }, + { + id: 2141, + remoteIdentifier: 'https://arstechnica.com/?p=1648757', + title: 'The most complete brain map ever is here: A fly’s “connectome”', + body: + 'It took 12 years and at least $40 million to chart a region about 250µm across.', + author: 'WIRED', + publicationDate: '2020-01-25T11:06:46Z', + url: 'https://arstechnica.com/?p=1648757', + rule: 4, + read: false, + }, + ]; + + const rule = { + id: 4, + name: 'Ars Technica', + unread: 100, + category: 1, + url: 'http://feeds.arstechnica.com/arstechnica/index?fmt=xml', + favicon: 'https://cdn.arstechnica.net/favicon.ico', + type: constants.RULE_TYPE, + }; + + fetchMock.getOnce('/api/rules/4/posts/?read=false', { + body: { + count: 2, + next: 'https://durp.com/api/rules/4/posts/?page=2&read=false', + previous: null, + results: posts, + }, + headers: { 'content-type': 'application/json' }, + }); + + const store = mockStore({ + categories: { items: {}, isFetching: false }, + rules: { items: {}, isFetching: false }, + posts: { items: {}, isFetching: false }, + selected: { item: {}, next: false, lastReached: false, post: {} }, + }); + + const expectedActions = [ + { type: actions.REQUEST_POSTS }, + { + type: actions.RECEIVE_POSTS, + next: 'https://durp.com/api/rules/4/posts/?page=2&read=false', + posts, + }, + ]; + + return store.dispatch(actions.fetchPostsBySection(rule)).then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + }); + + it('should create multiple actions to fetch posts by category', () => { + const posts = [ + { + id: 2067, + remoteIdentifier: 'https://arstechnica.com/?p=1648607', + title: + 'This amazing glitch puts Star Fox 64 ships in an unmodified Zelda cartridge', + body: + '"Stale-reference manipulation," 300-character file names, and a clash between worlds.', + author: 'Kyle Orland', + publicationDate: '2020-01-24T19:50:12Z', + url: 'https://arstechnica.com/?p=1648607', + rule: 4, + read: false, + }, + { + id: 2141, + remoteIdentifier: 'https://arstechnica.com/?p=1648757', + title: 'The most complete brain map ever is here: A fly’s “connectome”', + body: + 'It took 12 years and at least $40 million to chart a region about 250µm across.', + author: 'WIRED', + publicationDate: '2020-01-25T11:06:46Z', + url: 'https://arstechnica.com/?p=1648757', + rule: 4, + read: false, + }, + ]; + + const category = { + id: 1, + name: 'Tech', + unread: 2, + type: constants.CATEGORY_TYPE, + }; + + fetchMock.getOnce('/api/categories/1/posts/?read=false', { + body: { + count: 2, + next: 'https://durp.com/api/categories/4/posts/?page=2&read=false', + previous: null, + results: posts, + }, + headers: { 'content-type': 'application/json' }, + }); + + const store = mockStore({ + categories: { items: {}, isFetching: false }, + rules: { items: {}, isFetching: false }, + posts: { items: {}, isFetching: false }, + selected: { item: {}, next: false, lastReached: false, post: {} }, + }); + + const expectedActions = [ + { type: actions.REQUEST_POSTS }, + { + type: actions.RECEIVE_POSTS, + next: 'https://durp.com/api/categories/4/posts/?page=2&read=false', + posts, + }, + ]; + + return store.dispatch(actions.fetchPostsBySection(category)).then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + }); + + it('should create no actions when fetching posts and section is read', () => { + const rule = { + id: 4, + name: 'Ars Technica', + unread: 0, + category: 1, + url: 'http://feeds.arstechnica.com/arstechnica/index?fmt=xml', + favicon: 'https://cdn.arstechnica.net/favicon.ico', + }; + + const store = mockStore({ + categories: { items: {}, isFetching: false }, + rules: { items: {}, isFetching: false }, + posts: { items: {}, isFetching: false }, + selected: { item: {}, next: false, lastReached: false, post: {} }, + }); + + const expectedActions = []; + + store.dispatch(actions.fetchPostsBySection(rule)); + + expect(store.getActions()).toEqual(expectedActions); + }); + + it('should handle exceptions when marking a post read', () => { + const post = { + id: 2067, + remoteIdentifier: 'https://arstechnica.com/?p=1648607', + title: + 'This amazing glitch puts Star Fox 64 ships in an unmodified Zelda cartridge', + body: + '"Stale-reference manipulation," 300-character file names, and a clash between worlds.', + author: 'Kyle Orland', + publicationDate: '2020-01-24T19:50:12Z', + url: 'https://arstechnica.com/?p=1648607', + rule: 5, + read: false, + }; + + const rule = { + id: 4, + name: 'Ars Technica', + unread: 100, + category: 1, + url: 'http://feeds.arstechnica.com/arstechnica/index?fmt=xml', + favicon: 'https://cdn.arstechnica.net/favicon.ico', + }; + + const errorMessage = 'Permission denied'; + + fetchMock.patch(`/api/posts/${post.id}/`, () => { + throw new Error(errorMessage); + }); + + const store = mockStore({ + categories: { items: {}, isFetching: false }, + rules: { items: {}, isFetching: false }, + posts: { items: {}, isFetching: false }, + selected: { item: { ...rule }, next: false, lastReached: false, post: {} }, + }); + + const expectedActions = [ + { type: actions.RECEIVE_POST, post: {} }, + { type: errorActions.RECEIVE_API_ERROR, error: TypeError(errorMessage) }, + ]; + + return store.dispatch(actions.markPostRead(post, 'FAKE_TOKEN')).then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + }); + + it('should handle exceptions when fetching posts by section', () => { + const rule = { + id: 4, + name: 'Ars Technica', + unread: 100, + category: 1, + url: 'http://feeds.arstechnica.com/arstechnica/index?fmt=xml', + favicon: 'https://cdn.arstechnica.net/favicon.ico', + type: constants.RULE_TYPE, + }; + + const errorMessage = 'Page not found'; + + fetchMock.getOnce(`/api/rules/${rule.id}/posts/?read=false`, () => { + throw new Error(errorMessage); + }); + + const store = mockStore({ + categories: { items: {}, isFetching: false }, + rules: { items: {}, isFetching: false }, + posts: { items: {}, isFetching: false }, + selected: { item: { ...rule }, next: false, lastReached: false, post: {} }, + }); + + const expectedActions = [ + { type: actions.REQUEST_POSTS }, + { type: actions.RECEIVE_POSTS, posts: [] }, + { type: errorActions.RECEIVE_API_ERROR, error: Error(errorMessage) }, + ]; + + return store.dispatch(actions.fetchPostsBySection(rule)).then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + }); +}); diff --git a/src/newsreader/js/tests/homepage/actions/rule.test.js b/src/newsreader/js/tests/homepage/actions/rule.test.js new file mode 100644 index 0000000..7e167e7 --- /dev/null +++ b/src/newsreader/js/tests/homepage/actions/rule.test.js @@ -0,0 +1,341 @@ +import configureMockStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; +import fetchMock from 'fetch-mock'; + +import { objectsFromArray } from '../../../utils.js'; + +import * as actions from '../../../pages/homepage/actions/rules.js'; +import * as constants from '../../../pages/homepage/constants.js'; +import * as categoryActions from '../../../pages/homepage/actions/categories.js'; +import * as errorActions from '../../../pages/homepage/actions/error.js'; + +const middlewares = [thunk]; +const mockStore = configureMockStore(middlewares); + +describe('rule actions', () => { + afterEach(() => { + fetchMock.restore(); + }); + + it('should create an action to select a rule', () => { + const rule = { + id: 1, + name: 'Test rule', + unread: 100, + category: 1, + url: 'http://feeds.arstechnica.com/arstechnica/index?fmt=xml', + favicon: 'https://cdn.arstechnica.net/favicon.ico', + }; + + const expectedAction = { + section: { ...rule, type: constants.RULE_TYPE }, + type: actions.SELECT_RULE, + }; + + expect(actions.selectRule(rule)).toEqual(expectedAction); + }); + + it('should create an action to request a rule', () => { + const expectedAction = { type: actions.REQUEST_RULE }; + + expect(actions.requestRule()).toEqual(expectedAction); + }); + + it('should create an action to request multiple rules', () => { + const expectedAction = { type: actions.REQUEST_RULES }; + + expect(actions.requestRules()).toEqual(expectedAction); + }); + + it('should create an action to receive a rule', () => { + const rule = { + id: 1, + name: 'Test rule', + unread: 100, + category: 1, + url: 'http://feeds.arstechnica.com/arstechnica/index?fmt=xml', + favicon: 'https://cdn.arstechnica.net/favicon.ico', + }; + + const expectedAction = { + type: actions.RECEIVE_RULE, + rule, + }; + + expect(actions.receiveRule(rule)).toEqual(expectedAction); + }); + + it('should create an action to receive multiple rules', () => { + const rules = [ + { + id: 1, + name: 'Test rule', + unread: 100, + category: 1, + url: 'http://feeds.arstechnica.com/arstechnica/index?fmt=xml', + favicon: 'https://cdn.arstechnica.net/favicon.ico', + }, + { + id: 2, + name: 'Test rule 2', + unread: 50, + category: 1, + url: 'https://xkcd.com/atom.xml', + favicon: null, + }, + ]; + + const expectedAction = { + type: actions.RECEIVE_RULES, + rules, + }; + + expect(actions.receiveRules(rules)).toEqual(expectedAction); + }); + + it('should create multiple actions to fetch a rule', () => { + const rule = { + id: 1, + name: 'Test rule', + unread: 100, + category: 1, + url: 'http://feeds.arstechnica.com/arstechnica/index?fmt=xml', + favicon: 'https://cdn.arstechnica.net/favicon.ico', + }; + + fetchMock.getOnce('/api/rules/1', { + body: { ...rule, unread: 500 }, + headers: { 'content-type': 'application/json' }, + }); + + const store = mockStore({ + categories: { items: {}, isFetching: false }, + rules: { items: {}, isFetching: false }, + posts: { items: {}, isFetching: false }, + selected: { item: {}, next: false, lastReached: false, post: {} }, + }); + + const expectedActions = [ + { type: actions.REQUEST_RULE }, + { type: actions.RECEIVE_RULE, rule: { ...rule, unread: 500 } }, + ]; + + return store.dispatch(actions.fetchRule(rule)).then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + }); + + it('should not create not create actions when rule is clicked less then twice', () => { + const rule = { + id: 1, + name: 'Test rule', + unread: 100, + category: 1, + url: 'http://feeds.arstechnica.com/arstechnica/index?fmt=xml', + favicon: 'https://cdn.arstechnica.net/favicon.ico', + }; + + fetchMock.getOnce('/api/rules/1', { + body: { ...rule, unread: 500 }, + headers: { 'content-type': 'application/json' }, + }); + + const store = mockStore({ + categories: { items: {}, isFetching: false }, + rules: { items: {}, isFetching: false }, + posts: { items: {}, isFetching: false }, + selected: { + item: { ...rule, type: constants.RULE_TYPE, clicks: 1 }, + next: false, + lastReached: false, + post: {}, + }, + }); + + const expectedActions = []; + + store.dispatch(actions.fetchRule(rule)); + + expect(store.getActions()).toEqual(expectedActions); + }); + + it('should create multiple actions to fetch a rule wich is read', () => { + const rule = { + id: 1, + name: 'Test rule', + unread: 0, + category: 1, + url: 'http://feeds.arstechnica.com/arstechnica/index?fmt=xml', + favicon: 'https://cdn.arstechnica.net/favicon.ico', + }; + + const category = { + id: 1, + name: 'Test category', + unread: 500, + }; + + fetchMock + .get('/api/rules/1', { + body: { ...rule, unread: 500 }, + headers: { 'content-type': 'application/json' }, + }) + .get('/api/categories/1', { + body: { ...category, unread: 2000 }, + headers: { 'content-type': 'application/json' }, + }); + + const store = mockStore({ + categories: { items: { 1: { ...category } }, isFetching: false }, + rules: { items: {}, isFetching: false }, + posts: { items: {}, isFetching: false }, + selected: { item: {}, next: false, lastReached: false, post: {} }, + }); + + const expectedActions = [ + { type: actions.REQUEST_RULE }, + { type: actions.RECEIVE_RULE, rule: { ...rule, unread: 500 } }, + { type: categoryActions.REQUEST_CATEGORY }, + { + type: categoryActions.RECEIVE_CATEGORY, + category: { ...category, unread: 2000 }, + }, + ]; + + return store.dispatch(actions.fetchRule(rule)).then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + }); + + it('should create multiple actions when fetching rules by category', () => { + const category = { + id: 1, + name: 'Tech', + unread: 0, + }; + + const rules = [ + { + id: 1, + name: 'Ars Technica', + url: 'http://feeds.arstechnica.com/arstechnica/index?fmt=xml', + favicon: 'https://cdn.arstechnica.net/favicon.ico', + category: 1, + unread: 200, + }, + { + id: 2, + name: 'Hacker News', + url: 'https://news.ycombinator.com/rss', + favicon: 'https://news.ycombinator.com/favicon.ico', + category: 1, + unread: 350, + }, + ]; + + fetchMock.getOnce('/api/categories/1/rules/', { + body: rules, + headers: { 'content-type': 'application/json' }, + }); + + const expectedActions = [ + { type: actions.REQUEST_RULES }, + { type: actions.RECEIVE_RULES, rules }, + ]; + + const store = mockStore({ + categories: { items: {}, isFetching: false }, + rules: { items: {}, isFetching: false }, + posts: { items: {}, isFetching: false }, + selected: {}, + }); + + return store.dispatch(actions.fetchRulesByCategory(category)).then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + }); + + it('should handle an unexpected response when fetching a rule', () => { + const rule = { + id: 1, + name: 'Test rule', + unread: 100, + category: 1, + url: 'http://feeds.arstechnica.com/arstechnica/index?fmt=xml', + favicon: 'https://cdn.arstechnica.net/favicon.ico', + }; + + const errorMessage = 'Too many requests'; + + fetchMock.getOnce('/api/rules/1', () => { + throw new TypeError(errorMessage); + }); + + const store = mockStore({ + categories: { items: {}, isFetching: false }, + rules: { items: {}, isFetching: false }, + posts: { items: {}, isFetching: false }, + selected: { item: {}, next: false, lastReached: false, post: {} }, + }); + + const expectedActions = [ + { type: actions.REQUEST_RULE }, + { type: actions.RECEIVE_RULE, rule: {} }, + { type: errorActions.RECEIVE_API_ERROR, error: TypeError(errorMessage) }, + ]; + + return store.dispatch(actions.fetchRule(rule)).then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + }); + + it('should handle an unexpected response when fetching rules by category', () => { + const category = { + id: 1, + name: 'Tech', + unread: 0, + }; + + const rules = [ + { + id: 1, + name: 'Ars Technica', + url: 'http://feeds.arstechnica.com/arstechnica/index?fmt=xml', + favicon: 'https://cdn.arstechnica.net/favicon.ico', + category: 1, + unread: 200, + }, + { + id: 2, + name: 'Hacker News', + url: 'https://news.ycombinator.com/rss', + favicon: 'https://news.ycombinator.com/favicon.ico', + category: 1, + unread: 350, + }, + ]; + + const errorMessage = 'Too many request'; + + fetchMock.getOnce('/api/categories/1/rules/', () => { + throw new Error(errorMessage); + }); + + const expectedActions = [ + { type: actions.REQUEST_RULES }, + { type: actions.RECEIVE_RULES, rules: [] }, + { type: errorActions.RECEIVE_API_ERROR, error: Error(errorMessage) }, + ]; + + const store = mockStore({ + categories: { items: {}, isFetching: false }, + rules: { items: {}, isFetching: false }, + posts: { items: {}, isFetching: false }, + selected: {}, + }); + + return store.dispatch(actions.fetchRulesByCategory(category)).then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + }); +}); diff --git a/src/newsreader/js/tests/homepage/actions/selected.test.js b/src/newsreader/js/tests/homepage/actions/selected.test.js new file mode 100644 index 0000000..b0f163c --- /dev/null +++ b/src/newsreader/js/tests/homepage/actions/selected.test.js @@ -0,0 +1,237 @@ +import configureMockStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; +import fetchMock from 'fetch-mock'; + +import * as actions from '../../../pages/homepage/actions/selected.js'; +import * as categoryActions from '../../../pages/homepage/actions/categories.js'; +import * as errorActions from '../../../pages/homepage/actions/error.js'; +import * as ruleActions from '../../../pages/homepage/actions/rules.js'; +import * as constants from '../../../pages/homepage/constants.js'; + +const middlewares = [thunk]; +const mockStore = configureMockStore(middlewares); + +describe('selected actions', () => { + afterEach(() => { + fetchMock.restore(); + }); + + it('should create an action to mark a section read', () => { + const category = { + id: 1, + name: 'Test category', + unread: 100, + type: constants.CATEGORY_TYPE, + }; + + const expectedAction = { + section: { ...category, type: constants.CATEGORY_TYPE }, + type: actions.MARK_SECTION_READ, + }; + + expect(actions.markSectionRead(category)).toEqual(expectedAction); + }); + + it('should mark a category as read', () => { + const category = { id: 1, name: 'Test category', unread: 100 }; + const rules = { + 1: { + id: 1, + name: 'Ars Technica', + url: 'http://feeds.arstechnica.com/arstechnica/index?fmt=xml', + favicon: 'https://cdn.arstechnica.net/favicon.ico', + category: 1, + unread: 200, + }, + 2: { + id: 2, + name: 'Hacker News', + url: 'https://news.ycombinator.com/rss', + favicon: 'https://news.ycombinator.com/favicon.ico', + category: 1, + unread: 350, + }, + }; + + fetchMock.postOnce('/api/categories/1/read/', { + body: { ...category, unread: 0 }, + headers: { 'content-type': 'application/json' }, + }); + + const expectedActions = [ + { type: categoryActions.REQUEST_CATEGORY }, + { + type: categoryActions.RECEIVE_CATEGORY, + category: { ...category, unread: 0 }, + }, + { + type: actions.MARK_SECTION_READ, + section: { + ...category, + unread: 0, + rules: rules, + type: constants.CATEGORY_TYPE, + }, + }, + ]; + + const store = mockStore({ + categories: { items: {}, isFetching: false }, + rules: { items: { ...rules }, isFetching: false }, + posts: { items: {}, isFetching: false }, + selected: { + item: { ...category, type: actions.CATEGORY_TYPE }, + next: false, + lastReached: false, + post: {}, + }, + }); + + return store + .dispatch(actions.markRead({ ...category, type: constants.CATEGORY_TYPE }, 'TOKEN')) + .then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + }); + + it('should mark a rule as read', () => { + const rule = { + id: 1, + name: 'Ars Technica', + url: 'http://feeds.arstechnica.com/arstechnica/index?fmt=xml', + favicon: 'https://cdn.arstechnica.net/favicon.ico', + category: 1, + unread: 200, + }; + + fetchMock.postOnce('/api/rules/1/read/', { + body: { ...rule, unread: 0 }, + headers: { 'content-type': 'application/json' }, + }); + + const expectedActions = [ + { type: ruleActions.REQUEST_RULE }, + { + type: ruleActions.RECEIVE_RULE, + rule: { ...rule, unread: 0 }, + }, + { + type: actions.MARK_SECTION_READ, + section: { ...rule, type: constants.RULE_TYPE }, + }, + ]; + + const store = mockStore({ + categories: { items: {}, isFetching: false }, + rules: { items: { [rule.id]: { ...rule } }, isFetching: false }, + posts: { items: {}, isFetching: false }, + selected: { + item: { ...rule, type: constants.RULE_TYPE }, + next: false, + lastReached: false, + post: {}, + }, + }); + + return store + .dispatch(actions.markRead({ ...rule, type: constants.RULE_TYPE }, 'TOKEN')) + .then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + }); + + it('should handle exceptions when marking a category as read', () => { + const category = { id: 1, name: 'Test category', unread: 100 }; + const rules = { + 1: { + id: 1, + name: 'Ars Technica', + url: 'http://feeds.arstechnica.com/arstechnica/index?fmt=xml', + favicon: 'https://cdn.arstechnica.net/favicon.ico', + category: 1, + unread: 200, + }, + 2: { + id: 2, + name: 'Hacker News', + url: 'https://news.ycombinator.com/rss', + favicon: 'https://news.ycombinator.com/favicon.ico', + category: 1, + unread: 350, + }, + }; + + const errorMessage = 'Page not found'; + + fetchMock.postOnce('/api/categories/1/read/', () => { + throw new Error(errorMessage); + }); + + const expectedActions = [ + { type: categoryActions.REQUEST_CATEGORY }, + { type: categoryActions.RECEIVE_CATEGORY, category: {} }, + { type: errorActions.RECEIVE_API_ERROR, error: Error(errorMessage) }, + ]; + + const store = mockStore({ + categories: { items: {}, isFetching: false }, + rules: { items: { ...rules }, isFetching: false }, + posts: { items: {}, isFetching: false }, + selected: { + item: { ...category, type: actions.CATEGORY_TYPE }, + next: false, + lastReached: false, + post: {}, + }, + error: {}, + }); + + return store + .dispatch(actions.markRead({ ...category, type: constants.CATEGORY_TYPE }, 'TOKEN')) + .then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + }); + + it('should handle exceptions when marking a rule as read', () => { + const rule = { + id: 1, + name: 'Ars Technica', + url: 'http://feeds.arstechnica.com/arstechnica/index?fmt=xml', + favicon: 'https://cdn.arstechnica.net/favicon.ico', + category: 1, + unread: 200, + }; + + const errorMessage = 'Page not found'; + + fetchMock.postOnce('/api/rules/1/read/', () => { + throw new Error(errorMessage); + }); + + const expectedActions = [ + { type: ruleActions.REQUEST_RULE }, + { type: ruleActions.RECEIVE_RULE, rule: {} }, + { type: errorActions.RECEIVE_API_ERROR, error: Error(errorMessage) }, + ]; + + const store = mockStore({ + categories: { items: {}, isFetching: false }, + rules: { items: { [rule.id]: { ...rule } }, isFetching: false }, + posts: { items: {}, isFetching: false }, + selected: { + item: { ...rule, type: constants.RULE_TYPE }, + next: false, + lastReached: false, + post: {}, + }, + error: {}, + }); + + return store + .dispatch(actions.markRead({ ...rule, type: constants.RULE_TYPE }, 'TOKEN')) + .then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + }); +}); diff --git a/src/newsreader/js/tests/homepage/reducers/category.test.js b/src/newsreader/js/tests/homepage/reducers/category.test.js new file mode 100644 index 0000000..f5c27ae --- /dev/null +++ b/src/newsreader/js/tests/homepage/reducers/category.test.js @@ -0,0 +1,212 @@ +import { categories as reducer } from '../../../pages/homepage/reducers/categories.js'; + +import { objectsFromArray } from '../../../utils.js'; + +import * as actions from '../../../pages/homepage/actions/categories.js'; +import * as postActions from '../../../pages/homepage/actions/posts.js'; +import * as selectedActions from '../../../pages/homepage/actions/selected.js'; +import * as constants from '../../../pages/homepage/constants.js'; + +const defaultState = { items: {}, isFetching: false }; + +describe('category reducer', () => { + it('should return default state', () => { + expect(reducer(undefined, {})).toEqual(defaultState); + }); + + it('should return state after receiving category', () => { + const receivedCategory = { id: 9, name: 'Tech', unread: 291 }; + const action = { type: actions.RECEIVE_CATEGORY, category: receivedCategory }; + + const expectedState = { + ...defaultState, + items: { [receivedCategory.id]: receivedCategory }, + }; + + expect(reducer(undefined, action)).toEqual(expectedState); + }); + + it('should return state after receiving multiple categories', () => { + const receivedCategories = [ + { id: 9, name: 'Tech', unread: 291 }, + { id: 2, name: 'World news', unread: 444 }, + ]; + + const action = { type: actions.RECEIVE_CATEGORIES, categories: receivedCategories }; + + const expectedCategories = objectsFromArray(receivedCategories, 'id'); + const expectedState = { ...defaultState, items: expectedCategories }; + + expect(reducer(undefined, action)).toEqual(expectedState); + }); + + it('should return state after requesting a category', () => { + const action = { type: actions.REQUEST_CATEGORY }; + + const expectedState = { ...defaultState, isFetching: true }; + + expect(reducer(undefined, action)).toEqual(expectedState); + }); + + it('should return state after requesting multiple categories', () => { + const action = { type: actions.REQUEST_CATEGORIES }; + + const expectedState = { ...defaultState, isFetching: true }; + + expect(reducer(undefined, action)).toEqual(expectedState); + }); + + it('should return state after marking a post read with a category selected', () => { + const category = { + id: 9, + name: 'Tech', + unread: 291, + }; + + const post = { + id: 2091, + remoteIdentifier: 'https://www.bbc.co.uk/news/world-asia-china-51249208', + title: 'China coronavirus spread is accelerating, Xi Jinping warns', + body: + 'China\'s president tells a high-level meeting that the country faces a "grave situation".', + author: null, + publicationDate: '2020-01-26T05:54:14Z', + url: 'https://www.bbc.co.uk/news/world-asia-china-51249208', + rule: 4, + read: false, + }; + + const action = { + type: postActions.MARK_POST_READ, + section: { ...category, type: constants.CATEGORY_TYPE }, + post, + }; + + const state = { + ...defaultState, + items: { [category.id]: { ...category } }, + }; + + const expectedState = { + ...defaultState, + items: { + [category.id]: { ...category, unread: 290 }, + }, + }; + + expect(reducer(state, action)).toEqual(expectedState); + }); + + it('should return state after marking a post read with a rule selected', () => { + const category = { + id: 9, + name: 'Tech', + unread: 433, + }; + + const rule = { + id: 1, + name: 'Test rule', + unread: 100, + category: 9, + url: 'http://feeds.arstechnica.com/arstechnica/index?fmt=xml', + favicon: 'https://cdn.arstechnica.net/favicon.ico', + }; + + const post = { + id: 2182, + remoteIdentifier: 'https://arstechnica.com/?p=1648871', + title: 'Tesla needs to fix Autopilot safety flaws, demands Senator Markey', + body: + 'It should be renamed and fitted with a real driver-monitoring system, he says.', + author: 'Jonathan M. Gitlin', + publicationDate: '2020-01-25T18:34:20Z', + url: 'https://arstechnica.com/?p=1648871', + rule: 1, + read: false, + }; + + const action = { + type: postActions.MARK_POST_READ, + section: { ...rule, type: constants.RULE_TYPE }, + post, + }; + + const state = { + ...defaultState, + items: { [category.id]: { ...category } }, + }; + + const expectedState = { + ...defaultState, + items: { + [category.id]: { ...category, unread: 432 }, + }, + }; + + expect(reducer(state, action)).toEqual(expectedState); + }); + + it('should return state after marking a section read with a category', () => { + const category = { + id: 9, + name: 'Tech', + unread: 433, + }; + + const action = { + type: selectedActions.MARK_SECTION_READ, + section: { ...category, type: constants.CATEGORY_TYPE }, + }; + + const state = { + ...defaultState, + items: { [category.id]: { ...category } }, + }; + + const expectedState = { + ...defaultState, + items: { + [category.id]: { ...category, unread: 0 }, + }, + }; + + expect(reducer(state, action)).toEqual(expectedState); + }); + + it('should return state after marking a section read with a rule', () => { + const category = { + id: 9, + name: 'Tech', + unread: 433, + }; + + const rule = { + id: 1, + name: 'Test rule', + unread: 211, + category: 9, + url: 'http://feeds.arstechnica.com/arstechnica/index?fmt=xml', + favicon: 'https://cdn.arstechnica.net/favicon.ico', + }; + + const action = { + type: selectedActions.MARK_SECTION_READ, + section: { ...rule, type: constants.RULE_TYPE }, + }; + + const state = { + ...defaultState, + items: { [category.id]: { ...category } }, + }; + + const expectedState = { + ...defaultState, + items: { + [category.id]: { ...category, unread: 222 }, + }, + }; + + expect(reducer(state, action)).toEqual(expectedState); + }); +}); diff --git a/src/newsreader/js/tests/homepage/reducers/post.test.js b/src/newsreader/js/tests/homepage/reducers/post.test.js new file mode 100644 index 0000000..ef4234a --- /dev/null +++ b/src/newsreader/js/tests/homepage/reducers/post.test.js @@ -0,0 +1,307 @@ +import { posts as reducer } from '../../../pages/homepage/reducers/posts.js'; + +import { objectsFromArray } from '../../../utils.js'; + +import * as actions from '../../../pages/homepage/actions/posts.js'; +import * as selectedActions from '../../../pages/homepage/actions/selected.js'; +import * as constants from '../../../pages/homepage/constants.js'; + +const defaultState = { items: {}, isFetching: false }; + +describe('post actions', () => { + it('should return state after requesting posts', () => { + const action = { type: actions.REQUEST_POSTS }; + + const expectedState = { ...defaultState, isFetching: true }; + + expect(reducer(undefined, action)).toEqual(expectedState); + }); + + it('should return state after receiving a post', () => { + const post = { + id: 2067, + remoteIdentifier: 'https://arstechnica.com/?p=1648607', + title: + 'This amazing glitch puts Star Fox 64 ships in an unmodified Zelda cartridge', + body: + '"Stale-reference manipulation," 300-character file names, and a clash between worlds.', + author: 'Kyle Orland', + publicationDate: '2020-01-24T19:50:12Z', + url: 'https://arstechnica.com/?p=1648607', + rule: 4, + read: false, + }; + + const action = { + type: actions.RECEIVE_POST, + post, + }; + + const expectedState = { + ...defaultState, + isFetching: false, + items: { [post.id]: post }, + }; + + expect(reducer(undefined, action)).toEqual(expectedState); + }); + + it('should return state after receiving posts', () => { + const posts = [ + { + id: 2067, + remoteIdentifier: 'https://arstechnica.com/?p=1648607', + title: + 'This amazing glitch puts Star Fox 64 ships in an unmodified Zelda cartridge', + body: + '"Stale-reference manipulation," 300-character file names, and a clash between worlds.', + author: 'Kyle Orland', + publicationDate: '2020-01-24T19:50:12Z', + url: 'https://arstechnica.com/?p=1648607', + rule: 4, + read: false, + }, + { + id: 2141, + remoteIdentifier: 'https://arstechnica.com/?p=1648757', + title: 'The most complete brain map ever is here: A fly’s “connectome”', + body: + 'It took 12 years and at least $40 million to chart a region about 250µm across.', + author: 'WIRED', + publicationDate: '2020-01-25T11:06:46Z', + url: 'https://arstechnica.com/?p=1648757', + rule: 4, + read: false, + }, + ]; + + const action = { + type: actions.RECEIVE_POSTS, + next: 'https://durp.com/api/rules/4/posts/?page=2&read=false', + posts, + }; + + const expectedPosts = objectsFromArray(posts, 'id'); + const expectedState = { + ...defaultState, + isFetching: false, + items: expectedPosts, + }; + + expect(reducer(undefined, action)).toEqual(expectedState); + }); + + it('should return state after marking a rule read', () => { + const posts = { + 2067: { + id: 2067, + remoteIdentifier: 'https://arstechnica.com/?p=1648607', + title: + 'This amazing glitch puts Star Fox 64 ships in an unmodified Zelda cartridge', + body: + '"Stale-reference manipulation," 300-character file names, and a clash between worlds.', + author: 'Kyle Orland', + publicationDate: '2020-01-24T19:50:12Z', + url: 'https://arstechnica.com/?p=1648607', + rule: 5, + read: false, + }, + 2141: { + id: 2141, + remoteIdentifier: 'https://arstechnica.com/?p=1648757', + title: 'The most complete brain map ever is here: A fly’s “connectome”', + body: + 'It took 12 years and at least $40 million to chart a region about 250µm across.', + author: 'WIRED', + publicationDate: '2020-01-25T11:06:46Z', + url: 'https://arstechnica.com/?p=1648757', + rule: 5, + read: false, + }, + 4637: { + id: 4637, + remoteIdentifier: 'https://www.bbc.co.uk/news/world-asia-china-51299195', + title: "Coronavirus: Whole world 'must take action', warns WHO", + body: + 'The World Health Organization will hold a further emergency meeting on the coronavirus on Thursday.', + author: null, + publicationDate: '2020-01-29T19:08:25Z', + url: 'https://www.bbc.co.uk/news/world-asia-china-51299195', + rule: 4, + read: false, + }, + 4638: { + id: 4638, + remoteIdentifier: 'https://www.bbc.co.uk/news/world-europe-51294305', + title: "Coronavirus: French Asians hit back at racism with 'I'm not a virus'", + body: + 'The coronavirus outbreak in Wuhan prompts French Asians to complain of a backlash against them.', + author: null, + publicationDate: '2020-01-29T18:27:56Z', + url: 'https://www.bbc.co.uk/news/world-europe-51294305', + rule: 4, + read: false, + }, + }; + + const rule = { + id: 5, + name: 'Ars Technica', + url: 'http://feeds.arstechnica.com/arstechnica/index?fmt=xml', + favicon: 'https://cdn.arstechnica.net/favicon.ico', + category: 9, + unread: 544, + }; + + const action = { + type: selectedActions.MARK_SECTION_READ, + section: { ...rule, type: constants.RULE_TYPE }, + }; + + const state = { ...defaultState, items: { ...posts } }; + + const expectedState = { + ...defaultState, + isFetching: false, + items: { + ...posts, + 2067: { ...posts[2067], read: true }, + 2141: { ...posts[2141], read: true }, + }, + }; + + expect(reducer(state, action)).toEqual(expectedState); + }); + + it('should return state after marking a category read', () => { + const posts = { + 2067: { + id: 2067, + remoteIdentifier: 'https://arstechnica.com/?p=1648607', + title: + 'This amazing glitch puts Star Fox 64 ships in an unmodified Zelda cartridge', + body: + '"Stale-reference manipulation," 300-character file names, and a clash between worlds.', + author: 'Kyle Orland', + publicationDate: '2020-01-24T19:50:12Z', + url: 'https://arstechnica.com/?p=1648607', + rule: 5, + read: false, + }, + 2141: { + id: 2141, + remoteIdentifier: 'https://arstechnica.com/?p=1648757', + title: 'The most complete brain map ever is here: A fly’s “connectome”', + body: + 'It took 12 years and at least $40 million to chart a region about 250µm across.', + author: 'WIRED', + publicationDate: '2020-01-25T11:06:46Z', + url: 'https://arstechnica.com/?p=1648757', + rule: 5, + read: false, + }, + 4637: { + id: 4637, + remoteIdentifier: 'https://www.bbc.co.uk/news/world-asia-china-51299195', + title: "Coronavirus: Whole world 'must take action', warns WHO", + body: + 'The World Health Organization will hold a further emergency meeting on the coronavirus on Thursday.', + author: null, + publicationDate: '2020-01-29T19:08:25Z', + url: 'https://www.bbc.co.uk/news/world-asia-china-51299195', + rule: 4, + read: false, + }, + 4638: { + id: 4638, + remoteIdentifier: 'https://www.bbc.co.uk/news/world-europe-51294305', + title: "Coronavirus: French Asians hit back at racism with 'I'm not a virus'", + body: + 'The coronavirus outbreak in Wuhan prompts French Asians to complain of a backlash against them.', + author: null, + publicationDate: '2020-01-29T18:27:56Z', + url: 'https://www.bbc.co.uk/news/world-europe-51294305', + rule: 4, + read: false, + }, + 4589: { + id: 4589, + remoteIdentifier: 'https://tweakers.net/nieuws/162878', + title: 'Analyse: Nintendo verdiende miljard dollar aan mobiele games', + body: + 'Nintendo heeft tot nu toe een miljard dollar verdiend aan mobiele games, zo heeft SensorTower becijferd. Daarbij gaat het om inkomsten uit de App Store van Apple en Play Store van Google. De game die het meeste opbracht is Fire Emblem Heroes.', + author: 'Arnoud Wokke', + publicationDate: '2020-01-29T19:03:01Z', + url: + 'https://tweakers.net/nieuws/162878/analyse-nintendo-verdiende-miljard-dollar-aan-mobiele-games.html', + rule: 7, + read: false, + }, + 4594: { + id: 4594, + remoteIdentifier: 'https://tweakers.net/nieuws/162870', + title: 'Samsung kondigt eerste tablet met 5g aan', + body: + 'Samsung heef zijn eerste tablet met 5g aangekondigd. Het gaat om een variant op de al bestaande Galaxy Tab S6, maar dan voorzien van Qualcomm X50-modem. Er gingen al maanden geruchten over de release van de tablet.', + author: 'Arnoud Wokke', + publicationDate: '2020-01-29T16:29:40Z', + url: + 'https://tweakers.net/nieuws/162870/samsung-kondigt-eerste-tablet-met-5g-aan.html', + rule: 7, + read: false, + }, + }; + + const rules = { + 4: { + id: 4, + name: 'BBC', + url: 'http://feeds.bbci.co.uk/news/world/rss.xml', + favicon: + 'https://m.files.bbci.co.uk/modules/bbc-morph-news-waf-page-meta/2.5.2/apple-touch-icon-57x57-precomposed.png', + category: 8, + unread: 321, + }, + 5: { + id: 4, + name: 'BBC', + url: 'http://feeds.bbci.co.uk/news/world/rss.xml', + favicon: + 'https://m.files.bbci.co.uk/modules/bbc-morph-news-waf-page-meta/2.5.2/apple-touch-icon-57x57-precomposed.png', + category: 8, + unread: 632, + }, + }; + + const category = { + id: 8, + name: 'News', + unread: 953, + }; + + const action = { + type: selectedActions.MARK_SECTION_READ, + section: { + ...category, + type: constants.CATEGORY_TYPE, + rules, + }, + }; + + const state = { ...defaultState, items: { ...posts } }; + + const expectedState = { + ...defaultState, + isFetching: false, + items: { + ...posts, + 2067: { ...posts[2067], read: true }, + 2141: { ...posts[2141], read: true }, + 4637: { ...posts[4637], read: true }, + 4638: { ...posts[4638], read: true }, + }, + }; + + expect(reducer(state, action)).toEqual(expectedState); + }); +}); diff --git a/src/newsreader/js/tests/homepage/reducers/rule.test.js b/src/newsreader/js/tests/homepage/reducers/rule.test.js new file mode 100644 index 0000000..171c301 --- /dev/null +++ b/src/newsreader/js/tests/homepage/reducers/rule.test.js @@ -0,0 +1,184 @@ +import { rules as reducer } from '../../../pages/homepage/reducers/rules.js'; + +import { objectsFromArray } from '../../../utils.js'; + +import * as actions from '../../../pages/homepage/actions/rules.js'; +import * as postActions from '../../../pages/homepage/actions/posts.js'; +import * as selectedActions from '../../../pages/homepage/actions/selected.js'; +import * as constants from '../../../pages/homepage/constants.js'; + +const defaultState = { items: {}, isFetching: false }; + +describe('category reducer', () => { + it('should return default state', () => { + expect(reducer(undefined, {})).toEqual(defaultState); + }); + + it('should return after requesting a rule', () => { + const action = { type: actions.REQUEST_RULE }; + const expectedState = { ...defaultState, isFetching: true }; + + expect(reducer(undefined, action)).toEqual(expectedState); + }); + + it('should return after requesting multiple rules', () => { + const action = { type: actions.REQUEST_RULES }; + const expectedState = { ...defaultState, isFetching: true }; + + expect(reducer(undefined, action)).toEqual(expectedState); + }); + + it('should return state after receiving a rule', () => { + const rule = { + id: 1, + name: 'Test rule', + unread: 100, + category: 1, + url: 'http://feeds.arstechnica.com/arstechnica/index?fmt=xml', + favicon: 'https://cdn.arstechnica.net/favicon.ico', + }; + + const action = { type: actions.RECEIVE_RULE, rule }; + + const expectedState = { + ...defaultState, + items: { + [rule.id]: rule, + }, + }; + + expect(reducer(undefined, action)).toEqual(expectedState); + }); + + it('should return state after receiving multiple rules', () => { + const rules = [ + { + id: 1, + name: 'Test rule', + unread: 100, + category: 1, + url: 'http://feeds.arstechnica.com/arstechnica/index?fmt=xml', + favicon: 'https://cdn.arstechnica.net/favicon.ico', + }, + { + id: 2, + name: 'Another Test rule', + unread: 444, + category: 1, + url: 'http://feeds.arstechnica.com/arstechnica/index?fmt=xml', + favicon: 'https://cdn.arstechnica.net/favicon.ico', + }, + ]; + + const action = { type: actions.RECEIVE_RULES, rules }; + + const mappedRules = objectsFromArray(rules, 'id'); + const expectedState = { ...defaultState, items: { ...mappedRules } }; + + expect(reducer(undefined, action)).toEqual(expectedState); + }); + + it('should return state after marking a post read', () => { + const rule = { + id: 1, + name: 'Test rule', + unread: 100, + category: 9, + url: 'http://feeds.arstechnica.com/arstechnica/index?fmt=xml', + favicon: 'https://cdn.arstechnica.net/favicon.ico', + }; + + const post = { + id: 2182, + remoteIdentifier: 'https://arstechnica.com/?p=1648871', + title: 'Tesla needs to fix Autopilot safety flaws, demands Senator Markey', + body: + 'It should be renamed and fitted with a real driver-monitoring system, he says.', + author: 'Jonathan M. Gitlin', + publicationDate: '2020-01-25T18:34:20Z', + url: 'https://arstechnica.com/?p=1648871', + rule: 1, + read: false, + }; + + const action = { + type: postActions.MARK_POST_READ, + section: { ...rule, type: constants.RULE_TYPE }, + post, + }; + + const state = { + ...defaultState, + items: { [rule.id]: rule }, + }; + + const expectedState = { + ...defaultState, + items: { [rule.id]: { ...rule, unread: 99 } }, + }; + + expect(reducer(state, action)).toEqual(expectedState); + }); + + it('should return state after marking a category read', () => { + const category = { + id: 9, + name: 'Tech', + unread: 433, + }; + + const rule = { + id: 1, + name: 'Test rule', + unread: 100, + category: 9, + url: 'http://feeds.arstechnica.com/arstechnica/index?fmt=xml', + favicon: 'https://cdn.arstechnica.net/favicon.ico', + }; + + const action = { + type: selectedActions.MARK_SECTION_READ, + section: { ...category, type: constants.CATEGORY_TYPE }, + }; + + const state = { + ...defaultState, + items: { [rule.id]: rule }, + }; + + const expectedState = { + ...defaultState, + items: { [rule.id]: { ...rule, unread: 0 } }, + }; + + expect(reducer(state, action)).toEqual(expectedState); + }); + + it('should return state after marking a rule read', () => { + const rule = { + id: 1, + name: 'Test rule', + unread: 100, + category: 9, + url: 'http://feeds.arstechnica.com/arstechnica/index?fmt=xml', + favicon: 'https://cdn.arstechnica.net/favicon.ico', + }; + + const action = { + type: selectedActions.MARK_SECTION_READ, + section: { ...rule, type: constants.RULE_TYPE }, + }; + + const state = { + ...defaultState, + items: { [rule.id]: rule }, + }; + + const expectedState = { + ...defaultState, + items: { [rule.id]: { ...rule, unread: 0 } }, + }; + + expect(reducer(state, action)).toEqual(expectedState); + }); +}); diff --git a/src/newsreader/js/tests/homepage/reducers/selected.test.js b/src/newsreader/js/tests/homepage/reducers/selected.test.js new file mode 100644 index 0000000..215c6e1 --- /dev/null +++ b/src/newsreader/js/tests/homepage/reducers/selected.test.js @@ -0,0 +1,425 @@ +import { selected as reducer } from '../../../pages/homepage/reducers/selected.js'; + +import * as actions from '../../../pages/homepage/actions/selected.js'; +import * as categoryActions from '../../../pages/homepage/actions/categories.js'; +import * as postActions from '../../../pages/homepage/actions/posts.js'; +import * as ruleActions from '../../../pages/homepage/actions/rules.js'; +import * as constants from '../../../pages/homepage/constants.js'; + +const defaultState = { item: {}, next: false, lastReached: false, post: {} }; + +describe('selected reducer', () => { + it('should return state', () => { + expect(reducer(undefined, {})).toEqual(defaultState); + }); + + it('should return state after selecting a category', () => { + const category = { id: 9, name: 'Tech', unread: 291 }; + + const action = { + type: categoryActions.SELECT_CATEGORY, + section: category, + }; + + const expectedState = { + ...defaultState, + item: { ...category, clicks: 1 }, + }; + + expect(reducer(undefined, action)).toEqual(expectedState); + }); + + it('should return state after selecting a rule', () => { + const rule = { + id: 1, + name: 'Test rule', + unread: 100, + category: 9, + url: 'http://feeds.arstechnica.com/arstechnica/index?fmt=xml', + favicon: 'https://cdn.arstechnica.net/favicon.ico', + }; + + const action = { + type: ruleActions.SELECT_RULE, + section: rule, + }; + + const expectedState = { + ...defaultState, + item: { ...rule, clicks: 1 }, + }; + + expect(reducer(undefined, action)).toEqual(expectedState); + }); + + it('should return state after selecting a category twice', () => { + const category = { id: 9, name: 'Tech', unread: 291 }; + + const action = { + type: categoryActions.SELECT_CATEGORY, + section: category, + }; + + const state = { + ...defaultState, + item: { ...category, clicks: 1 }, + }; + + const expectedState = { + ...defaultState, + item: { ...category, clicks: 2 }, + }; + + expect(reducer(state, action)).toEqual(expectedState); + }); + + it('should return state after selecting a rule twice', () => { + const rule = { + id: 1, + name: 'Test rule', + unread: 100, + category: 9, + url: 'http://feeds.arstechnica.com/arstechnica/index?fmt=xml', + favicon: 'https://cdn.arstechnica.net/favicon.ico', + }; + + const action = { + type: ruleActions.SELECT_RULE, + section: rule, + }; + + const state = { + ...defaultState, + item: { ...rule, clicks: 1 }, + }; + + const expectedState = { + ...defaultState, + item: { ...rule, clicks: 2 }, + }; + + expect(reducer(state, action)).toEqual(expectedState); + }); + + it('should return state after selecting a category the third time', () => { + const category = { id: 9, name: 'Tech', unread: 291 }; + + const action = { + type: categoryActions.SELECT_CATEGORY, + section: category, + }; + + const state = { + ...defaultState, + item: { ...category, clicks: 2 }, + }; + + const expectedState = { + ...defaultState, + item: { ...category, clicks: 1 }, + }; + + expect(reducer(state, action)).toEqual(expectedState); + }); + + it('should return state after selecting a rule the third time', () => { + const rule = { + id: 1, + name: 'Test rule', + unread: 100, + category: 9, + url: 'http://feeds.arstechnica.com/arstechnica/index?fmt=xml', + favicon: 'https://cdn.arstechnica.net/favicon.ico', + }; + + const action = { + type: ruleActions.SELECT_RULE, + section: rule, + }; + + const state = { + ...defaultState, + item: { ...rule, clicks: 2 }, + }; + + const expectedState = { + ...defaultState, + item: { ...rule, clicks: 1 }, + }; + + expect(reducer(state, action)).toEqual(expectedState); + }); + + it('should return state after selecting different (rule) section type', () => { + const category = { id: 9, name: 'Tech', unread: 291 }; + + const rule = { + id: 1, + name: 'Test rule', + unread: 100, + category: 9, + url: 'http://feeds.arstechnica.com/arstechnica/index?fmt=xml', + favicon: 'https://cdn.arstechnica.net/favicon.ico', + }; + + const action = { + type: ruleActions.SELECT_RULE, + section: rule, + }; + + const state = { + ...defaultState, + item: { ...category, clicks: 1 }, + }; + + const expectedState = { + ...defaultState, + item: { ...rule, clicks: 1 }, + }; + + expect(reducer(state, action)).toEqual(expectedState); + }); + + it('should return state after selecting different (category) section type', () => { + const category = { id: 9, name: 'Tech', unread: 291 }; + + const rule = { + id: 1, + name: 'Test rule', + unread: 100, + category: 9, + url: 'http://feeds.arstechnica.com/arstechnica/index?fmt=xml', + favicon: 'https://cdn.arstechnica.net/favicon.ico', + }; + + const action = { + type: categoryActions.SELECT_CATEGORY, + section: category, + }; + + const state = { + ...defaultState, + item: { ...rule, clicks: 1 }, + }; + + const expectedState = { + ...defaultState, + item: { ...category, clicks: 1 }, + }; + + expect(reducer(state, action)).toEqual(expectedState); + }); + + it('should return state after receiving posts', () => { + const posts = [ + { + id: 2067, + remoteIdentifier: 'https://arstechnica.com/?p=1648607', + title: + 'This amazing glitch puts Star Fox 64 ships in an unmodified Zelda cartridge', + body: + '"Stale-reference manipulation," 300-character file names, and a clash between worlds.', + author: 'Kyle Orland', + publicationDate: '2020-01-24T19:50:12Z', + url: 'https://arstechnica.com/?p=1648607', + rule: 4, + read: false, + }, + { + id: 2141, + remoteIdentifier: 'https://arstechnica.com/?p=1648757', + title: 'The most complete brain map ever is here: A fly’s “connectome”', + body: + 'It took 12 years and at least $40 million to chart a region about 250µm across.', + author: 'WIRED', + publicationDate: '2020-01-25T11:06:46Z', + url: 'https://arstechnica.com/?p=1648757', + rule: 4, + read: false, + }, + ]; + + const action = { + type: postActions.RECEIVE_POSTS, + next: 'https://durp.com/api/rules/4/posts/?page=2&read=false', + posts, + }; + + const expectedState = { + ...defaultState, + next: 'https://durp.com/api/rules/4/posts/?page=2&read=false', + lastReached: false, + }; + + expect(reducer(undefined, action)).toEqual(expectedState); + }); + + it('should return state after receiving a post which is selected', () => { + const post = { + id: 2067, + remoteIdentifier: 'https://arstechnica.com/?p=1648607', + title: + 'This amazing glitch puts Star Fox 64 ships in an unmodified Zelda cartridge', + body: + '"Stale-reference manipulation," 300-character file names, and a clash between worlds.', + author: 'Kyle Orland', + publicationDate: '2020-01-24T19:50:12Z', + url: 'https://arstechnica.com/?p=1648607', + rule: 4, + read: false, + }; + + const action = { + type: postActions.RECEIVE_POST, + post: { ...post, rule: 6 }, + }; + + const state = { ...defaultState, post }; + const expectedState = { ...defaultState, post: { ...post, rule: 6 } }; + + expect(reducer(state, action)).toEqual(expectedState); + }); + + it('should return state after receiving a post with none selected', () => { + const post = { + id: 2067, + remoteIdentifier: 'https://arstechnica.com/?p=1648607', + title: + 'This amazing glitch puts Star Fox 64 ships in an unmodified Zelda cartridge', + body: + '"Stale-reference manipulation," 300-character file names, and a clash between worlds.', + author: 'Kyle Orland', + publicationDate: '2020-01-24T19:50:12Z', + url: 'https://arstechnica.com/?p=1648607', + rule: 4, + read: false, + }; + + const action = { + type: postActions.RECEIVE_POST, + post: { ...post, rule: 6 }, + }; + + const state = { ...defaultState, post: {} }; + const expectedState = { ...defaultState, post: {} }; + + expect(reducer(state, action)).toEqual(expectedState); + }); + + it('should return state after selecting a post', () => { + const post = { + id: 2067, + remoteIdentifier: 'https://arstechnica.com/?p=1648607', + title: + 'This amazing glitch puts Star Fox 64 ships in an unmodified Zelda cartridge', + body: + '"Stale-reference manipulation," 300-character file names, and a clash between worlds.', + author: 'Kyle Orland', + publicationDate: '2020-01-24T19:50:12Z', + url: 'https://arstechnica.com/?p=1648607', + rule: 4, + read: false, + }; + + const action = { + type: postActions.SELECT_POST, + post, + }; + + const expectedState = { ...defaultState, post }; + + expect(reducer(undefined, action)).toEqual(expectedState); + }); + + it('should return state after unselecting a post', () => { + const post = { + id: 2067, + remoteIdentifier: 'https://arstechnica.com/?p=1648607', + title: + 'This amazing glitch puts Star Fox 64 ships in an unmodified Zelda cartridge', + body: + '"Stale-reference manipulation," 300-character file names, and a clash between worlds.', + author: 'Kyle Orland', + publicationDate: '2020-01-24T19:50:12Z', + url: 'https://arstechnica.com/?p=1648607', + rule: 4, + read: false, + }; + + const action = { + type: postActions.UNSELECT_POST, + post, + }; + + const state = { ...defaultState, post }; + const expectedState = { ...defaultState, post: {} }; + + expect(reducer(state, action)).toEqual(expectedState); + }); + + it('should return state after marking a post read', () => { + const post = { + id: 2067, + remoteIdentifier: 'https://arstechnica.com/?p=1648607', + title: + 'This amazing glitch puts Star Fox 64 ships in an unmodified Zelda cartridge', + body: + '"Stale-reference manipulation," 300-character file names, and a clash between worlds.', + author: 'Kyle Orland', + publicationDate: '2020-01-24T19:50:12Z', + url: 'https://arstechnica.com/?p=1648607', + rule: 4, + read: false, + }; + + const rule = { + id: 4, + name: 'Ars Technica', + unread: 100, + category: 1, + url: 'http://feeds.arstechnica.com/arstechnica/index?fmt=xml', + favicon: 'https://cdn.arstechnica.net/favicon.ico', + }; + + const action = { + type: postActions.MARK_POST_READ, + section: rule, + post, + }; + + const state = { + ...defaultState, + item: rule, + }; + const expectedState = { + ...defaultState, + item: { ...rule, unread: 99 }, + }; + + expect(reducer(state, action)).toEqual(expectedState); + }); + + it('should return state after marking a section read', () => { + const rule = { + id: 4, + name: 'Ars Technica', + unread: 100, + category: 1, + url: 'http://feeds.arstechnica.com/arstechnica/index?fmt=xml', + favicon: 'https://cdn.arstechnica.net/favicon.ico', + }; + + const action = { + section: { ...rule }, + type: actions.MARK_SECTION_READ, + }; + + const state = { ...defaultState, item: { ...rule, clicks: 2 } }; + const expectedState = { + ...defaultState, + item: { ...rule, unread: 0, clicks: 0 }, + }; + + expect(reducer(state, action)).toEqual(expectedState); + }); +}); diff --git a/src/newsreader/js/utils.js b/src/newsreader/js/utils.js new file mode 100644 index 0000000..bba4717 --- /dev/null +++ b/src/newsreader/js/utils.js @@ -0,0 +1,24 @@ +export const formatDatetime = dateString => { + const locale = navigator.language ? navigator.language : 'en-US'; + const dateOptions = { + year: 'numeric', + month: 'numeric', + day: 'numeric', + minute: 'numeric', + hour: 'numeric', + }; + + const date = new Date(dateString); + + return date.toLocaleDateString(locale, dateOptions); +}; + +export const objectsFromArray = (array, key) => { + const arrayEntries = array + .filter(object => key in object) + .map(object => { + return [object[key], { ...object }]; + }); + + return Object.fromEntries(arrayEntries); +}; diff --git a/src/newsreader/news/__init__.py b/src/newsreader/news/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/newsreader/news/collection/__init__.py b/src/newsreader/news/collection/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/newsreader/news/collection/admin.py b/src/newsreader/news/collection/admin.py new file mode 100644 index 0000000..e82dea5 --- /dev/null +++ b/src/newsreader/news/collection/admin.py @@ -0,0 +1,18 @@ +from django.contrib import admin + +from newsreader.news.collection.models import CollectionRule + + +class CollectionRuleAdmin(admin.ModelAdmin): + fields = ("url", "name", "timezone", "category", "favicon", "user") + + list_display = ("name", "category", "url", "last_suceeded", "succeeded") + list_filter = ("user",) + + def save_model(self, request, obj, form, change): + if not change: + obj.user = request.user + obj.save() + + +admin.site.register(CollectionRule, CollectionRuleAdmin) diff --git a/src/newsreader/news/collection/apps.py b/src/newsreader/news/collection/apps.py new file mode 100644 index 0000000..1f4c1c0 --- /dev/null +++ b/src/newsreader/news/collection/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class CollectionConfig(AppConfig): + name = "collection" diff --git a/src/newsreader/news/collection/base.py b/src/newsreader/news/collection/base.py new file mode 100644 index 0000000..519f4f8 --- /dev/null +++ b/src/newsreader/news/collection/base.py @@ -0,0 +1,115 @@ +from bs4 import BeautifulSoup + +from newsreader.news.collection.exceptions import StreamParseException +from newsreader.news.collection.models import CollectionRule +from newsreader.news.collection.utils import fetch + + +class Stream: + def __init__(self, rule): + self.rule = rule + + def read(self): + raise NotImplementedError + + def parse(self, payload): + raise NotImplementedError + + class Meta: + abstract = True + + +class Client: + stream = Stream + + def __init__(self, rules=None): + self.rules = rules if rules else CollectionRule.objects.all() + + def __enter__(self): + for rule in self.rules: + stream = self.stream(rule) + + yield stream.read() + + def __exit__(self, *args, **kwargs): + pass + + class Meta: + abstract = True + + +class Builder: + instances = [] + + def __init__(self, stream): + self.stream = stream + + def __enter__(self): + self.create_posts(self.stream) + return self + + def __exit__(self, *args, **kwargs): + pass + + def create_posts(self, stream): + pass + + def save(self): + pass + + class Meta: + abstract = True + + +class Collector: + client = None + builder = None + + def __init__(self, client=None, builder=None): + self.client = client if client else self.client + self.builder = builder if builder else self.builder + + def collect(self, rules=None): + with self.client(rules=rules) as client: + for data, stream in client: + with self.builder((data, stream)) as builder: + builder.save() + + class Meta: + abstract = True + + +class WebsiteStream(Stream): + def __init__(self, url): + self.url = url + + def read(self): + response = fetch(self.url) + + return (self.parse(response.content), self) + + def parse(self, payload): + try: + return BeautifulSoup(payload, "lxml") + except TypeError: + raise StreamParseException("Could not parse given HTML") + + +class URLBuilder(Builder): + def __enter__(self): + return self + + def build(self): + data, stream = self.stream + rule = stream.rule + + try: + url = data["feed"]["link"] + except (KeyError, TypeError): + url = None + + if url: + rule.website_url = url + rule.save() + + return rule, url diff --git a/src/newsreader/news/collection/constants.py b/src/newsreader/news/collection/constants.py new file mode 100644 index 0000000..eade898 --- /dev/null +++ b/src/newsreader/news/collection/constants.py @@ -0,0 +1,28 @@ +from bleach.sanitizer import ALLOWED_ATTRIBUTES as BLEACH_ATTRIBUTES +from bleach.sanitizer import ALLOWED_TAGS as BLEACH_TAGS + + +WHITELISTED_TAGS = ( + *BLEACH_TAGS, + "h1", + "h2", + "h3", + "article", + "p", + "img", + "figure", + "small", + "picture", + "b", + "video", + "source", + "div", + "body", +) + +WHITELISTED_ATTRIBUTES = { + **BLEACH_ATTRIBUTES, + "a": ["href", "rel"], + "img": ["alt", "src"], + "source": ["srcset", "media", "src", "type"], +} diff --git a/src/newsreader/news/collection/endpoints.py b/src/newsreader/news/collection/endpoints.py new file mode 100644 index 0000000..7f2ede0 --- /dev/null +++ b/src/newsreader/news/collection/endpoints.py @@ -0,0 +1,65 @@ +from rest_framework import status +from rest_framework.generics import ( + GenericAPIView, + ListAPIView, + RetrieveUpdateDestroyAPIView, + get_object_or_404, +) +from rest_framework.response import Response + +from newsreader.core.pagination import LargeResultSetPagination, ResultSetPagination +from newsreader.news.collection.models import CollectionRule +from newsreader.news.collection.serializers import RuleSerializer +from newsreader.news.core.filters import ReadFilter +from newsreader.news.core.models import Post +from newsreader.news.core.serializers import PostSerializer + + +class ListRuleView(ListAPIView): + queryset = CollectionRule.objects.all() + serializer_class = RuleSerializer + pagination_class = ResultSetPagination + + def get_queryset(self): + user = self.request.user + return self.queryset.filter(user=user).order_by("-created") + + +class DetailRuleView(RetrieveUpdateDestroyAPIView): + queryset = CollectionRule.objects.all() + serializer_class = RuleSerializer + pagination_class = ResultSetPagination + + +class NestedRuleView(ListAPIView): + queryset = CollectionRule.objects.prefetch_related("posts").all() + serializer_class = PostSerializer + pagination_class = LargeResultSetPagination + filter_backends = [ReadFilter] + + def get_queryset(self): + lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field + + # Default permission is IsOwner, therefore there shouldn't have to be + # filtered on the user. + filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]} + + rule = get_object_or_404(self.queryset, **filter_kwargs) + self.check_object_permissions(self.request, rule) + + return rule.posts.order_by("-publication_date") + + +class RuleReadView(GenericAPIView): + queryset = CollectionRule.objects.all() + serializer_class = RuleSerializer + + def post(self, request, *args, **kwargs): + rule = self.get_object() + + Post.objects.filter(rule=rule).update(read=True) + + rule.refresh_from_db() + serializer_class = self.get_serializer_class() + serializer = serializer_class(rule) + return Response(serializer.data, status=status.HTTP_201_CREATED) diff --git a/src/newsreader/news/collection/exceptions.py b/src/newsreader/news/collection/exceptions.py new file mode 100644 index 0000000..e636638 --- /dev/null +++ b/src/newsreader/news/collection/exceptions.py @@ -0,0 +1,32 @@ +class StreamException(Exception): + message = "Stream exception" + + def __init__(self, message=None): + self.message = message if message else self.message + + def __str__(self): + return self.message + + +class StreamNotFoundException(StreamException): + message = "Stream not found" + + +class StreamDeniedException(StreamException): + message = "Stream does not have sufficient permissions" + + +class StreamTimeOutException(StreamException): + message = "Stream timed out" + + +class StreamForbiddenException(StreamException): + message = "Stream forbidden" + + +class StreamParseException(StreamException): + message = "Stream could not be parsed" + + +class StreamConnectionError(StreamException): + message = "A connection to the stream could not be made" diff --git a/src/newsreader/news/collection/favicon.py b/src/newsreader/news/collection/favicon.py new file mode 100644 index 0000000..44b96bf --- /dev/null +++ b/src/newsreader/news/collection/favicon.py @@ -0,0 +1,120 @@ +from concurrent.futures import ThreadPoolExecutor, as_completed +from urllib.parse import urljoin, urlparse + +from newsreader.news.collection.base import ( + Builder, + Client, + Collector, + Stream, + URLBuilder, + WebsiteStream, +) +from newsreader.news.collection.exceptions import StreamException +from newsreader.news.collection.feed import FeedClient + + +LINK_RELS = [ + "icon", + "shortcut icon", + "apple-touch-icon", + "apple-touch-icon-precomposed", +] + + +class FaviconBuilder(Builder): + def build(self): + rule, soup = self.stream + + url = self.parse(soup, rule.website_url) + + if url: + rule.favicon = url + rule.save() + + def parse(self, soup, website_url): + if not soup.head: + return + + links = soup.head.find_all("link") + url = self.parse_links(links) + + if not url: + return + + parsed_url = urlparse(url) + + if not parsed_url.scheme and not parsed_url.netloc: + if not website_url: + return + return urljoin(website_url, url) + elif not parsed_url.scheme: + return urljoin(f"https://{parsed_url.netloc}", parsed_url.path) + + return url + + def parse_links(self, links): + favicons = set() + icons = set() + + for link in links: + if not "href" in link.attrs: + continue + + if "favicon" in link["href"]: + favicons.add(link["href"].lower()) + + if "rel" in link.attrs: + for rel in link["rel"]: + if rel in LINK_RELS: + icons.add(link["href"].lower()) + + if favicons: + return favicons.pop() + elif icons: + return icons.pop() + + +class FaviconClient(Client): + stream = WebsiteStream + + def __init__(self, streams): + self.streams = streams + + def __enter__(self): + with ThreadPoolExecutor(max_workers=10) as executor: + futures = { + executor.submit(stream.read): rule for rule, stream in self.streams + } + + for future in as_completed(futures): + rule = futures[future] + + try: + response_data, stream = future.result() + except StreamException: + continue + + yield (rule, response_data) + + +class FaviconCollector(Collector): + feed_client, favicon_client = (FeedClient, FaviconClient) + url_builder, favicon_builder = (URLBuilder, FaviconBuilder) + + def collect(self, rules=None): + streams = [] + + with self.feed_client(rules=rules) as client: + for data, stream in client: + with self.url_builder((data, stream)) as builder: + rule, url = builder.build() + + if not url: + continue + + streams.append((rule, WebsiteStream(url))) + + with self.favicon_client(streams) as client: + for rule, data in client: + with self.favicon_builder((rule, data)) as builder: + builder.build() diff --git a/src/newsreader/news/collection/feed.py b/src/newsreader/news/collection/feed.py new file mode 100644 index 0000000..46a7a3b --- /dev/null +++ b/src/newsreader/news/collection/feed.py @@ -0,0 +1,256 @@ +import logging + +from concurrent.futures import ThreadPoolExecutor, as_completed + +from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist +from django.db.models.fields import CharField, TextField +from django.template.defaultfilters import truncatechars +from django.utils import timezone + +import bleach +import pytz + +from feedparser import parse + +from newsreader.news.collection.base import Builder, Client, Collector, Stream +from newsreader.news.collection.constants import ( + WHITELISTED_ATTRIBUTES, + WHITELISTED_TAGS, +) +from newsreader.news.collection.exceptions import ( + StreamDeniedException, + StreamException, + StreamNotFoundException, + StreamParseException, + StreamTimeOutException, +) +from newsreader.news.collection.utils import build_publication_date, fetch +from newsreader.news.core.models import Post + + +logger = logging.getLogger(__name__) + + +class FeedBuilder(Builder): + instances = [] + + def __enter__(self): + _, stream = self.stream + self.instances = [] + self.existing_posts = { + post.remote_identifier: post + for post in Post.objects.filter(rule=stream.rule) + } + + return super().__enter__() + + def create_posts(self, stream): + data, stream = stream + entries = [] + + with FeedDuplicateHandler(stream.rule) as duplicate_handler: + try: + entries = data["entries"] + except KeyError: + pass + + instances = self.build(entries, stream.rule) + posts = duplicate_handler.check(instances) + + self.instances = [post for post in posts] + + def build(self, entries, rule): + field_mapping = { + "id": "remote_identifier", + "title": "title", + "summary": "body", + "link": "url", + "published_parsed": "publication_date", + "author": "author", + } + + tz = pytz.timezone(rule.timezone) + + for entry in entries: + data = {"rule_id": rule.pk} + + for field, model_field in field_mapping.items(): + if not field in entry: + continue + + value = self.truncate_text(model_field, entry[field]) + + if field == "published_parsed": + aware_datetime, created = build_publication_date(value, tz) + data[model_field] = aware_datetime if created else None + elif field == "summary": + summary = self.sanitize_fragment(value) + data[model_field] = summary + else: + data[model_field] = value + + if "content" in entry: + content = self.get_content(entry["content"]) + body = data.get("body", "") + + if not body or len(body) < len(content): + data["body"] = content + + yield Post(**data) + + def sanitize_fragment(self, fragment): + if not fragment: + return "" + + return bleach.clean( + fragment, + tags=WHITELISTED_TAGS, + attributes=WHITELISTED_ATTRIBUTES, + strip=True, + strip_comments=True, + ) + + def truncate_text(self, field_name, value): + field = Post._meta.get_field(field_name) + max_length = field.max_length + cls = type(field) + + if not value or not max_length: + return value + elif not bool(issubclass(cls, CharField) or issubclass(cls, TextField)): + return value + + if len(value) > max_length: + return truncatechars(value, max_length) + + return value + + def get_content(self, items): + content = "\n ".join([item.get("value") for item in items]) + return self.sanitize_fragment(content) + + def save(self): + for post in self.instances: + post.save() + + +class FeedStream(Stream): + def read(self): + url = self.rule.url + response = fetch(url) + + return (self.parse(response.content), self) + + def parse(self, payload): + try: + return parse(payload) + except TypeError as e: + raise StreamParseException("Could not parse feed") from e + + +class FeedClient(Client): + stream = FeedStream + + def __enter__(self): + streams = [self.stream(rule) for rule in self.rules] + + with ThreadPoolExecutor(max_workers=10) as executor: + futures = {executor.submit(stream.read): stream for stream in streams} + + for future in as_completed(futures): + stream = futures[future] + + try: + response_data = future.result() + + stream.rule.error = None + stream.rule.succeeded = True + stream.rule.last_suceeded = timezone.now() + + yield response_data + except StreamException as e: + length = stream.rule._meta.get_field("error").max_length + stream.rule.error = e.message[-length:] + stream.rule.succeeded = False + + yield ({"entries": []}, stream) + finally: + stream.rule.save() + + +class FeedCollector(Collector): + builder = FeedBuilder + client = FeedClient + + +class FeedDuplicateHandler: + def __init__(self, rule): + self.queryset = rule.posts.all() + + def __enter__(self): + self.existing_identifiers = self.queryset.filter( + remote_identifier__isnull=False + ).values_list("remote_identifier", flat=True) + return self + + def __exit__(self, *args, **kwargs): + pass + + def check(self, instances): + for instance in instances: + if instance.remote_identifier in self.existing_identifiers: + existing_post = self.handle_duplicate(instance) + + yield existing_post + + continue + elif not instance.remote_identifier and self.in_database(instance): + continue + + yield instance + + def in_database(self, post): + values = { + "url": post.url, + "title": post.title, + "body": post.body, + "publication_date": post.publication_date, + } + + for existing_post in self.queryset.order_by("-publication_date")[:500]: + if self.is_duplicate(existing_post, values): + return True + + def is_duplicate(self, existing_post, values): + for key, value in values.items(): + existing_value = getattr(existing_post, key, None) + if existing_value != value: + return False + + return True + + def handle_duplicate(self, instance): + try: + existing_instance = self.queryset.get( + remote_identifier=instance.remote_identifier + ) + except ObjectDoesNotExist: + logger.error( + f"Duplicate handler tried retrieving post {instance.remote_identifier} but failed doing so." + ) + return instance + except MultipleObjectsReturned: + existing_instances = self.queryset.filter( + remote_identifier=instance.remote_identifier + ).order_by("-publication_date") + existing_instance = existing_instances.last() + existing_instances.exclude(pk=existing_instance.pk).delete() + + for field in instance._meta.get_fields(): + getattr(existing_instance, field.name, object()) + new_value = getattr(instance, field.name, object()) + + if new_value and field.name != "id": + setattr(existing_instance, field.name, new_value) + + return existing_instance diff --git a/src/newsreader/news/collection/forms.py b/src/newsreader/news/collection/forms.py new file mode 100644 index 0000000..d0b02be --- /dev/null +++ b/src/newsreader/news/collection/forms.py @@ -0,0 +1,41 @@ +from django import forms + +import pytz + +from newsreader.news.collection.models import CollectionRule +from newsreader.news.core.models import Category + + +class CollectionRuleForm(forms.ModelForm): + category = forms.ModelChoiceField(required=False, queryset=Category.objects.all()) + timezone = forms.ChoiceField( + widget=forms.Select(attrs={"size": len(pytz.all_timezones)}), + choices=((timezone, timezone) for timezone in pytz.all_timezones), + ) + + def __init__(self, *args, **kwargs): + self.user = kwargs.pop("user") + + super().__init__(*args, **kwargs) + + if self.user: + self.fields["category"].queryset = Category.objects.filter(user=self.user) + + def save(self, commit=True): + instance = super().save(commit=False) + instance.user = self.user + + if commit: + instance.save() + self.save_m2m() + + return instance + + class Meta: + model = CollectionRule + fields = ("name", "url", "timezone", "favicon", "category") + + +class OPMLImportForm(forms.Form): + file = forms.FileField(allow_empty_file=False) + skip_existing = forms.BooleanField(initial=False, required=False) diff --git a/src/newsreader/news/collection/management/commands/collect.py b/src/newsreader/news/collection/management/commands/collect.py new file mode 100644 index 0000000..7d928f0 --- /dev/null +++ b/src/newsreader/news/collection/management/commands/collect.py @@ -0,0 +1,11 @@ +from django.core.management.base import BaseCommand + +from newsreader.news.collection.feed import FeedCollector + + +class Command(BaseCommand): + help = "Collects Atom/RSS feeds" + + def handle(self, *args, **options): + collector = FeedCollector() + collector.collect() diff --git a/src/newsreader/news/collection/management/commands/fetch_favicons.py b/src/newsreader/news/collection/management/commands/fetch_favicons.py new file mode 100644 index 0000000..1ee96cf --- /dev/null +++ b/src/newsreader/news/collection/management/commands/fetch_favicons.py @@ -0,0 +1,11 @@ +from django.core.management.base import BaseCommand + +from newsreader.news.collection.favicon import FaviconCollector + + +class Command(BaseCommand): + help = "Fetch favicons for collection rules" + + def handle(self, *args, **options): + collector = FaviconCollector() + collector.collect() diff --git a/src/newsreader/news/collection/migrations/0001_initial.py b/src/newsreader/news/collection/migrations/0001_initial.py new file mode 100644 index 0000000..59910e5 --- /dev/null +++ b/src/newsreader/news/collection/migrations/0001_initial.py @@ -0,0 +1,687 @@ +# Generated by Django 2.2 on 2019-07-14 10:36 + +import django.utils.timezone + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="CollectionRule", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created", models.DateTimeField(default=django.utils.timezone.now)), + ("modified", models.DateTimeField(auto_now=True)), + ("name", models.CharField(max_length=100)), + ("url", models.URLField(max_length=1024)), + ( + "website_url", + models.URLField( + blank=True, editable=False, max_length=1024, null=True + ), + ), + ("favicon", models.URLField(blank=True, null=True)), + ( + "timezone", + models.CharField( + choices=[ + ("Africa/Abidjan", "Africa/Abidjan"), + ("Africa/Accra", "Africa/Accra"), + ("Africa/Addis_Ababa", "Africa/Addis_Ababa"), + ("Africa/Algiers", "Africa/Algiers"), + ("Africa/Asmara", "Africa/Asmara"), + ("Africa/Asmera", "Africa/Asmera"), + ("Africa/Bamako", "Africa/Bamako"), + ("Africa/Bangui", "Africa/Bangui"), + ("Africa/Banjul", "Africa/Banjul"), + ("Africa/Bissau", "Africa/Bissau"), + ("Africa/Blantyre", "Africa/Blantyre"), + ("Africa/Brazzaville", "Africa/Brazzaville"), + ("Africa/Bujumbura", "Africa/Bujumbura"), + ("Africa/Cairo", "Africa/Cairo"), + ("Africa/Casablanca", "Africa/Casablanca"), + ("Africa/Ceuta", "Africa/Ceuta"), + ("Africa/Conakry", "Africa/Conakry"), + ("Africa/Dakar", "Africa/Dakar"), + ("Africa/Dar_es_Salaam", "Africa/Dar_es_Salaam"), + ("Africa/Djibouti", "Africa/Djibouti"), + ("Africa/Douala", "Africa/Douala"), + ("Africa/El_Aaiun", "Africa/El_Aaiun"), + ("Africa/Freetown", "Africa/Freetown"), + ("Africa/Gaborone", "Africa/Gaborone"), + ("Africa/Harare", "Africa/Harare"), + ("Africa/Johannesburg", "Africa/Johannesburg"), + ("Africa/Juba", "Africa/Juba"), + ("Africa/Kampala", "Africa/Kampala"), + ("Africa/Khartoum", "Africa/Khartoum"), + ("Africa/Kigali", "Africa/Kigali"), + ("Africa/Kinshasa", "Africa/Kinshasa"), + ("Africa/Lagos", "Africa/Lagos"), + ("Africa/Libreville", "Africa/Libreville"), + ("Africa/Lome", "Africa/Lome"), + ("Africa/Luanda", "Africa/Luanda"), + ("Africa/Lubumbashi", "Africa/Lubumbashi"), + ("Africa/Lusaka", "Africa/Lusaka"), + ("Africa/Malabo", "Africa/Malabo"), + ("Africa/Maputo", "Africa/Maputo"), + ("Africa/Maseru", "Africa/Maseru"), + ("Africa/Mbabane", "Africa/Mbabane"), + ("Africa/Mogadishu", "Africa/Mogadishu"), + ("Africa/Monrovia", "Africa/Monrovia"), + ("Africa/Nairobi", "Africa/Nairobi"), + ("Africa/Ndjamena", "Africa/Ndjamena"), + ("Africa/Niamey", "Africa/Niamey"), + ("Africa/Nouakchott", "Africa/Nouakchott"), + ("Africa/Ouagadougou", "Africa/Ouagadougou"), + ("Africa/Porto-Novo", "Africa/Porto-Novo"), + ("Africa/Sao_Tome", "Africa/Sao_Tome"), + ("Africa/Timbuktu", "Africa/Timbuktu"), + ("Africa/Tripoli", "Africa/Tripoli"), + ("Africa/Tunis", "Africa/Tunis"), + ("Africa/Windhoek", "Africa/Windhoek"), + ("America/Adak", "America/Adak"), + ("America/Anchorage", "America/Anchorage"), + ("America/Anguilla", "America/Anguilla"), + ("America/Antigua", "America/Antigua"), + ("America/Araguaina", "America/Araguaina"), + ( + "America/Argentina/Buenos_Aires", + "America/Argentina/Buenos_Aires", + ), + ( + "America/Argentina/Catamarca", + "America/Argentina/Catamarca", + ), + ( + "America/Argentina/ComodRivadavia", + "America/Argentina/ComodRivadavia", + ), + ("America/Argentina/Cordoba", "America/Argentina/Cordoba"), + ("America/Argentina/Jujuy", "America/Argentina/Jujuy"), + ( + "America/Argentina/La_Rioja", + "America/Argentina/La_Rioja", + ), + ("America/Argentina/Mendoza", "America/Argentina/Mendoza"), + ( + "America/Argentina/Rio_Gallegos", + "America/Argentina/Rio_Gallegos", + ), + ("America/Argentina/Salta", "America/Argentina/Salta"), + ( + "America/Argentina/San_Juan", + "America/Argentina/San_Juan", + ), + ( + "America/Argentina/San_Luis", + "America/Argentina/San_Luis", + ), + ("America/Argentina/Tucuman", "America/Argentina/Tucuman"), + ("America/Argentina/Ushuaia", "America/Argentina/Ushuaia"), + ("America/Aruba", "America/Aruba"), + ("America/Asuncion", "America/Asuncion"), + ("America/Atikokan", "America/Atikokan"), + ("America/Atka", "America/Atka"), + ("America/Bahia", "America/Bahia"), + ("America/Bahia_Banderas", "America/Bahia_Banderas"), + ("America/Barbados", "America/Barbados"), + ("America/Belem", "America/Belem"), + ("America/Belize", "America/Belize"), + ("America/Blanc-Sablon", "America/Blanc-Sablon"), + ("America/Boa_Vista", "America/Boa_Vista"), + ("America/Bogota", "America/Bogota"), + ("America/Boise", "America/Boise"), + ("America/Buenos_Aires", "America/Buenos_Aires"), + ("America/Cambridge_Bay", "America/Cambridge_Bay"), + ("America/Campo_Grande", "America/Campo_Grande"), + ("America/Cancun", "America/Cancun"), + ("America/Caracas", "America/Caracas"), + ("America/Catamarca", "America/Catamarca"), + ("America/Cayenne", "America/Cayenne"), + ("America/Cayman", "America/Cayman"), + ("America/Chicago", "America/Chicago"), + ("America/Chihuahua", "America/Chihuahua"), + ("America/Coral_Harbour", "America/Coral_Harbour"), + ("America/Cordoba", "America/Cordoba"), + ("America/Costa_Rica", "America/Costa_Rica"), + ("America/Creston", "America/Creston"), + ("America/Cuiaba", "America/Cuiaba"), + ("America/Curacao", "America/Curacao"), + ("America/Danmarkshavn", "America/Danmarkshavn"), + ("America/Dawson", "America/Dawson"), + ("America/Dawson_Creek", "America/Dawson_Creek"), + ("America/Denver", "America/Denver"), + ("America/Detroit", "America/Detroit"), + ("America/Dominica", "America/Dominica"), + ("America/Edmonton", "America/Edmonton"), + ("America/Eirunepe", "America/Eirunepe"), + ("America/El_Salvador", "America/El_Salvador"), + ("America/Ensenada", "America/Ensenada"), + ("America/Fort_Nelson", "America/Fort_Nelson"), + ("America/Fort_Wayne", "America/Fort_Wayne"), + ("America/Fortaleza", "America/Fortaleza"), + ("America/Glace_Bay", "America/Glace_Bay"), + ("America/Godthab", "America/Godthab"), + ("America/Goose_Bay", "America/Goose_Bay"), + ("America/Grand_Turk", "America/Grand_Turk"), + ("America/Grenada", "America/Grenada"), + ("America/Guadeloupe", "America/Guadeloupe"), + ("America/Guatemala", "America/Guatemala"), + ("America/Guayaquil", "America/Guayaquil"), + ("America/Guyana", "America/Guyana"), + ("America/Halifax", "America/Halifax"), + ("America/Havana", "America/Havana"), + ("America/Hermosillo", "America/Hermosillo"), + ( + "America/Indiana/Indianapolis", + "America/Indiana/Indianapolis", + ), + ("America/Indiana/Knox", "America/Indiana/Knox"), + ("America/Indiana/Marengo", "America/Indiana/Marengo"), + ( + "America/Indiana/Petersburg", + "America/Indiana/Petersburg", + ), + ("America/Indiana/Tell_City", "America/Indiana/Tell_City"), + ("America/Indiana/Vevay", "America/Indiana/Vevay"), + ("America/Indiana/Vincennes", "America/Indiana/Vincennes"), + ("America/Indiana/Winamac", "America/Indiana/Winamac"), + ("America/Indianapolis", "America/Indianapolis"), + ("America/Inuvik", "America/Inuvik"), + ("America/Iqaluit", "America/Iqaluit"), + ("America/Jamaica", "America/Jamaica"), + ("America/Jujuy", "America/Jujuy"), + ("America/Juneau", "America/Juneau"), + ( + "America/Kentucky/Louisville", + "America/Kentucky/Louisville", + ), + ( + "America/Kentucky/Monticello", + "America/Kentucky/Monticello", + ), + ("America/Knox_IN", "America/Knox_IN"), + ("America/Kralendijk", "America/Kralendijk"), + ("America/La_Paz", "America/La_Paz"), + ("America/Lima", "America/Lima"), + ("America/Los_Angeles", "America/Los_Angeles"), + ("America/Louisville", "America/Louisville"), + ("America/Lower_Princes", "America/Lower_Princes"), + ("America/Maceio", "America/Maceio"), + ("America/Managua", "America/Managua"), + ("America/Manaus", "America/Manaus"), + ("America/Marigot", "America/Marigot"), + ("America/Martinique", "America/Martinique"), + ("America/Matamoros", "America/Matamoros"), + ("America/Mazatlan", "America/Mazatlan"), + ("America/Mendoza", "America/Mendoza"), + ("America/Menominee", "America/Menominee"), + ("America/Merida", "America/Merida"), + ("America/Metlakatla", "America/Metlakatla"), + ("America/Mexico_City", "America/Mexico_City"), + ("America/Miquelon", "America/Miquelon"), + ("America/Moncton", "America/Moncton"), + ("America/Monterrey", "America/Monterrey"), + ("America/Montevideo", "America/Montevideo"), + ("America/Montreal", "America/Montreal"), + ("America/Montserrat", "America/Montserrat"), + ("America/Nassau", "America/Nassau"), + ("America/New_York", "America/New_York"), + ("America/Nipigon", "America/Nipigon"), + ("America/Nome", "America/Nome"), + ("America/Noronha", "America/Noronha"), + ( + "America/North_Dakota/Beulah", + "America/North_Dakota/Beulah", + ), + ( + "America/North_Dakota/Center", + "America/North_Dakota/Center", + ), + ( + "America/North_Dakota/New_Salem", + "America/North_Dakota/New_Salem", + ), + ("America/Ojinaga", "America/Ojinaga"), + ("America/Panama", "America/Panama"), + ("America/Pangnirtung", "America/Pangnirtung"), + ("America/Paramaribo", "America/Paramaribo"), + ("America/Phoenix", "America/Phoenix"), + ("America/Port-au-Prince", "America/Port-au-Prince"), + ("America/Port_of_Spain", "America/Port_of_Spain"), + ("America/Porto_Acre", "America/Porto_Acre"), + ("America/Porto_Velho", "America/Porto_Velho"), + ("America/Puerto_Rico", "America/Puerto_Rico"), + ("America/Punta_Arenas", "America/Punta_Arenas"), + ("America/Rainy_River", "America/Rainy_River"), + ("America/Rankin_Inlet", "America/Rankin_Inlet"), + ("America/Recife", "America/Recife"), + ("America/Regina", "America/Regina"), + ("America/Resolute", "America/Resolute"), + ("America/Rio_Branco", "America/Rio_Branco"), + ("America/Rosario", "America/Rosario"), + ("America/Santa_Isabel", "America/Santa_Isabel"), + ("America/Santarem", "America/Santarem"), + ("America/Santiago", "America/Santiago"), + ("America/Santo_Domingo", "America/Santo_Domingo"), + ("America/Sao_Paulo", "America/Sao_Paulo"), + ("America/Scoresbysund", "America/Scoresbysund"), + ("America/Shiprock", "America/Shiprock"), + ("America/Sitka", "America/Sitka"), + ("America/St_Barthelemy", "America/St_Barthelemy"), + ("America/St_Johns", "America/St_Johns"), + ("America/St_Kitts", "America/St_Kitts"), + ("America/St_Lucia", "America/St_Lucia"), + ("America/St_Thomas", "America/St_Thomas"), + ("America/St_Vincent", "America/St_Vincent"), + ("America/Swift_Current", "America/Swift_Current"), + ("America/Tegucigalpa", "America/Tegucigalpa"), + ("America/Thule", "America/Thule"), + ("America/Thunder_Bay", "America/Thunder_Bay"), + ("America/Tijuana", "America/Tijuana"), + ("America/Toronto", "America/Toronto"), + ("America/Tortola", "America/Tortola"), + ("America/Vancouver", "America/Vancouver"), + ("America/Virgin", "America/Virgin"), + ("America/Whitehorse", "America/Whitehorse"), + ("America/Winnipeg", "America/Winnipeg"), + ("America/Yakutat", "America/Yakutat"), + ("America/Yellowknife", "America/Yellowknife"), + ("Antarctica/Casey", "Antarctica/Casey"), + ("Antarctica/Davis", "Antarctica/Davis"), + ("Antarctica/DumontDUrville", "Antarctica/DumontDUrville"), + ("Antarctica/Macquarie", "Antarctica/Macquarie"), + ("Antarctica/Mawson", "Antarctica/Mawson"), + ("Antarctica/McMurdo", "Antarctica/McMurdo"), + ("Antarctica/Palmer", "Antarctica/Palmer"), + ("Antarctica/Rothera", "Antarctica/Rothera"), + ("Antarctica/South_Pole", "Antarctica/South_Pole"), + ("Antarctica/Syowa", "Antarctica/Syowa"), + ("Antarctica/Troll", "Antarctica/Troll"), + ("Antarctica/Vostok", "Antarctica/Vostok"), + ("Arctic/Longyearbyen", "Arctic/Longyearbyen"), + ("Asia/Aden", "Asia/Aden"), + ("Asia/Almaty", "Asia/Almaty"), + ("Asia/Amman", "Asia/Amman"), + ("Asia/Anadyr", "Asia/Anadyr"), + ("Asia/Aqtau", "Asia/Aqtau"), + ("Asia/Aqtobe", "Asia/Aqtobe"), + ("Asia/Ashgabat", "Asia/Ashgabat"), + ("Asia/Ashkhabad", "Asia/Ashkhabad"), + ("Asia/Atyrau", "Asia/Atyrau"), + ("Asia/Baghdad", "Asia/Baghdad"), + ("Asia/Bahrain", "Asia/Bahrain"), + ("Asia/Baku", "Asia/Baku"), + ("Asia/Bangkok", "Asia/Bangkok"), + ("Asia/Barnaul", "Asia/Barnaul"), + ("Asia/Beirut", "Asia/Beirut"), + ("Asia/Bishkek", "Asia/Bishkek"), + ("Asia/Brunei", "Asia/Brunei"), + ("Asia/Calcutta", "Asia/Calcutta"), + ("Asia/Chita", "Asia/Chita"), + ("Asia/Choibalsan", "Asia/Choibalsan"), + ("Asia/Chongqing", "Asia/Chongqing"), + ("Asia/Chungking", "Asia/Chungking"), + ("Asia/Colombo", "Asia/Colombo"), + ("Asia/Dacca", "Asia/Dacca"), + ("Asia/Damascus", "Asia/Damascus"), + ("Asia/Dhaka", "Asia/Dhaka"), + ("Asia/Dili", "Asia/Dili"), + ("Asia/Dubai", "Asia/Dubai"), + ("Asia/Dushanbe", "Asia/Dushanbe"), + ("Asia/Famagusta", "Asia/Famagusta"), + ("Asia/Gaza", "Asia/Gaza"), + ("Asia/Harbin", "Asia/Harbin"), + ("Asia/Hebron", "Asia/Hebron"), + ("Asia/Ho_Chi_Minh", "Asia/Ho_Chi_Minh"), + ("Asia/Hong_Kong", "Asia/Hong_Kong"), + ("Asia/Hovd", "Asia/Hovd"), + ("Asia/Irkutsk", "Asia/Irkutsk"), + ("Asia/Istanbul", "Asia/Istanbul"), + ("Asia/Jakarta", "Asia/Jakarta"), + ("Asia/Jayapura", "Asia/Jayapura"), + ("Asia/Jerusalem", "Asia/Jerusalem"), + ("Asia/Kabul", "Asia/Kabul"), + ("Asia/Kamchatka", "Asia/Kamchatka"), + ("Asia/Karachi", "Asia/Karachi"), + ("Asia/Kashgar", "Asia/Kashgar"), + ("Asia/Kathmandu", "Asia/Kathmandu"), + ("Asia/Katmandu", "Asia/Katmandu"), + ("Asia/Khandyga", "Asia/Khandyga"), + ("Asia/Kolkata", "Asia/Kolkata"), + ("Asia/Krasnoyarsk", "Asia/Krasnoyarsk"), + ("Asia/Kuala_Lumpur", "Asia/Kuala_Lumpur"), + ("Asia/Kuching", "Asia/Kuching"), + ("Asia/Kuwait", "Asia/Kuwait"), + ("Asia/Macao", "Asia/Macao"), + ("Asia/Macau", "Asia/Macau"), + ("Asia/Magadan", "Asia/Magadan"), + ("Asia/Makassar", "Asia/Makassar"), + ("Asia/Manila", "Asia/Manila"), + ("Asia/Muscat", "Asia/Muscat"), + ("Asia/Nicosia", "Asia/Nicosia"), + ("Asia/Novokuznetsk", "Asia/Novokuznetsk"), + ("Asia/Novosibirsk", "Asia/Novosibirsk"), + ("Asia/Omsk", "Asia/Omsk"), + ("Asia/Oral", "Asia/Oral"), + ("Asia/Phnom_Penh", "Asia/Phnom_Penh"), + ("Asia/Pontianak", "Asia/Pontianak"), + ("Asia/Pyongyang", "Asia/Pyongyang"), + ("Asia/Qatar", "Asia/Qatar"), + ("Asia/Qostanay", "Asia/Qostanay"), + ("Asia/Qyzylorda", "Asia/Qyzylorda"), + ("Asia/Rangoon", "Asia/Rangoon"), + ("Asia/Riyadh", "Asia/Riyadh"), + ("Asia/Saigon", "Asia/Saigon"), + ("Asia/Sakhalin", "Asia/Sakhalin"), + ("Asia/Samarkand", "Asia/Samarkand"), + ("Asia/Seoul", "Asia/Seoul"), + ("Asia/Shanghai", "Asia/Shanghai"), + ("Asia/Singapore", "Asia/Singapore"), + ("Asia/Srednekolymsk", "Asia/Srednekolymsk"), + ("Asia/Taipei", "Asia/Taipei"), + ("Asia/Tashkent", "Asia/Tashkent"), + ("Asia/Tbilisi", "Asia/Tbilisi"), + ("Asia/Tehran", "Asia/Tehran"), + ("Asia/Tel_Aviv", "Asia/Tel_Aviv"), + ("Asia/Thimbu", "Asia/Thimbu"), + ("Asia/Thimphu", "Asia/Thimphu"), + ("Asia/Tokyo", "Asia/Tokyo"), + ("Asia/Tomsk", "Asia/Tomsk"), + ("Asia/Ujung_Pandang", "Asia/Ujung_Pandang"), + ("Asia/Ulaanbaatar", "Asia/Ulaanbaatar"), + ("Asia/Ulan_Bator", "Asia/Ulan_Bator"), + ("Asia/Urumqi", "Asia/Urumqi"), + ("Asia/Ust-Nera", "Asia/Ust-Nera"), + ("Asia/Vientiane", "Asia/Vientiane"), + ("Asia/Vladivostok", "Asia/Vladivostok"), + ("Asia/Yakutsk", "Asia/Yakutsk"), + ("Asia/Yangon", "Asia/Yangon"), + ("Asia/Yekaterinburg", "Asia/Yekaterinburg"), + ("Asia/Yerevan", "Asia/Yerevan"), + ("Atlantic/Azores", "Atlantic/Azores"), + ("Atlantic/Bermuda", "Atlantic/Bermuda"), + ("Atlantic/Canary", "Atlantic/Canary"), + ("Atlantic/Cape_Verde", "Atlantic/Cape_Verde"), + ("Atlantic/Faeroe", "Atlantic/Faeroe"), + ("Atlantic/Faroe", "Atlantic/Faroe"), + ("Atlantic/Jan_Mayen", "Atlantic/Jan_Mayen"), + ("Atlantic/Madeira", "Atlantic/Madeira"), + ("Atlantic/Reykjavik", "Atlantic/Reykjavik"), + ("Atlantic/South_Georgia", "Atlantic/South_Georgia"), + ("Atlantic/St_Helena", "Atlantic/St_Helena"), + ("Atlantic/Stanley", "Atlantic/Stanley"), + ("Australia/ACT", "Australia/ACT"), + ("Australia/Adelaide", "Australia/Adelaide"), + ("Australia/Brisbane", "Australia/Brisbane"), + ("Australia/Broken_Hill", "Australia/Broken_Hill"), + ("Australia/Canberra", "Australia/Canberra"), + ("Australia/Currie", "Australia/Currie"), + ("Australia/Darwin", "Australia/Darwin"), + ("Australia/Eucla", "Australia/Eucla"), + ("Australia/Hobart", "Australia/Hobart"), + ("Australia/LHI", "Australia/LHI"), + ("Australia/Lindeman", "Australia/Lindeman"), + ("Australia/Lord_Howe", "Australia/Lord_Howe"), + ("Australia/Melbourne", "Australia/Melbourne"), + ("Australia/NSW", "Australia/NSW"), + ("Australia/North", "Australia/North"), + ("Australia/Perth", "Australia/Perth"), + ("Australia/Queensland", "Australia/Queensland"), + ("Australia/South", "Australia/South"), + ("Australia/Sydney", "Australia/Sydney"), + ("Australia/Tasmania", "Australia/Tasmania"), + ("Australia/Victoria", "Australia/Victoria"), + ("Australia/West", "Australia/West"), + ("Australia/Yancowinna", "Australia/Yancowinna"), + ("Brazil/Acre", "Brazil/Acre"), + ("Brazil/DeNoronha", "Brazil/DeNoronha"), + ("Brazil/East", "Brazil/East"), + ("Brazil/West", "Brazil/West"), + ("CET", "CET"), + ("CST6CDT", "CST6CDT"), + ("Canada/Atlantic", "Canada/Atlantic"), + ("Canada/Central", "Canada/Central"), + ("Canada/Eastern", "Canada/Eastern"), + ("Canada/Mountain", "Canada/Mountain"), + ("Canada/Newfoundland", "Canada/Newfoundland"), + ("Canada/Pacific", "Canada/Pacific"), + ("Canada/Saskatchewan", "Canada/Saskatchewan"), + ("Canada/Yukon", "Canada/Yukon"), + ("Chile/Continental", "Chile/Continental"), + ("Chile/EasterIsland", "Chile/EasterIsland"), + ("Cuba", "Cuba"), + ("EET", "EET"), + ("EST", "EST"), + ("EST5EDT", "EST5EDT"), + ("Egypt", "Egypt"), + ("Eire", "Eire"), + ("Etc/GMT", "Etc/GMT"), + ("Etc/GMT+0", "Etc/GMT+0"), + ("Etc/GMT+1", "Etc/GMT+1"), + ("Etc/GMT+10", "Etc/GMT+10"), + ("Etc/GMT+11", "Etc/GMT+11"), + ("Etc/GMT+12", "Etc/GMT+12"), + ("Etc/GMT+2", "Etc/GMT+2"), + ("Etc/GMT+3", "Etc/GMT+3"), + ("Etc/GMT+4", "Etc/GMT+4"), + ("Etc/GMT+5", "Etc/GMT+5"), + ("Etc/GMT+6", "Etc/GMT+6"), + ("Etc/GMT+7", "Etc/GMT+7"), + ("Etc/GMT+8", "Etc/GMT+8"), + ("Etc/GMT+9", "Etc/GMT+9"), + ("Etc/GMT-0", "Etc/GMT-0"), + ("Etc/GMT-1", "Etc/GMT-1"), + ("Etc/GMT-10", "Etc/GMT-10"), + ("Etc/GMT-11", "Etc/GMT-11"), + ("Etc/GMT-12", "Etc/GMT-12"), + ("Etc/GMT-13", "Etc/GMT-13"), + ("Etc/GMT-14", "Etc/GMT-14"), + ("Etc/GMT-2", "Etc/GMT-2"), + ("Etc/GMT-3", "Etc/GMT-3"), + ("Etc/GMT-4", "Etc/GMT-4"), + ("Etc/GMT-5", "Etc/GMT-5"), + ("Etc/GMT-6", "Etc/GMT-6"), + ("Etc/GMT-7", "Etc/GMT-7"), + ("Etc/GMT-8", "Etc/GMT-8"), + ("Etc/GMT-9", "Etc/GMT-9"), + ("Etc/GMT0", "Etc/GMT0"), + ("Etc/Greenwich", "Etc/Greenwich"), + ("Etc/UCT", "Etc/UCT"), + ("Etc/UTC", "Etc/UTC"), + ("Etc/Universal", "Etc/Universal"), + ("Etc/Zulu", "Etc/Zulu"), + ("Europe/Amsterdam", "Europe/Amsterdam"), + ("Europe/Andorra", "Europe/Andorra"), + ("Europe/Astrakhan", "Europe/Astrakhan"), + ("Europe/Athens", "Europe/Athens"), + ("Europe/Belfast", "Europe/Belfast"), + ("Europe/Belgrade", "Europe/Belgrade"), + ("Europe/Berlin", "Europe/Berlin"), + ("Europe/Bratislava", "Europe/Bratislava"), + ("Europe/Brussels", "Europe/Brussels"), + ("Europe/Bucharest", "Europe/Bucharest"), + ("Europe/Budapest", "Europe/Budapest"), + ("Europe/Busingen", "Europe/Busingen"), + ("Europe/Chisinau", "Europe/Chisinau"), + ("Europe/Copenhagen", "Europe/Copenhagen"), + ("Europe/Dublin", "Europe/Dublin"), + ("Europe/Gibraltar", "Europe/Gibraltar"), + ("Europe/Guernsey", "Europe/Guernsey"), + ("Europe/Helsinki", "Europe/Helsinki"), + ("Europe/Isle_of_Man", "Europe/Isle_of_Man"), + ("Europe/Istanbul", "Europe/Istanbul"), + ("Europe/Jersey", "Europe/Jersey"), + ("Europe/Kaliningrad", "Europe/Kaliningrad"), + ("Europe/Kiev", "Europe/Kiev"), + ("Europe/Kirov", "Europe/Kirov"), + ("Europe/Lisbon", "Europe/Lisbon"), + ("Europe/Ljubljana", "Europe/Ljubljana"), + ("Europe/London", "Europe/London"), + ("Europe/Luxembourg", "Europe/Luxembourg"), + ("Europe/Madrid", "Europe/Madrid"), + ("Europe/Malta", "Europe/Malta"), + ("Europe/Mariehamn", "Europe/Mariehamn"), + ("Europe/Minsk", "Europe/Minsk"), + ("Europe/Monaco", "Europe/Monaco"), + ("Europe/Moscow", "Europe/Moscow"), + ("Europe/Nicosia", "Europe/Nicosia"), + ("Europe/Oslo", "Europe/Oslo"), + ("Europe/Paris", "Europe/Paris"), + ("Europe/Podgorica", "Europe/Podgorica"), + ("Europe/Prague", "Europe/Prague"), + ("Europe/Riga", "Europe/Riga"), + ("Europe/Rome", "Europe/Rome"), + ("Europe/Samara", "Europe/Samara"), + ("Europe/San_Marino", "Europe/San_Marino"), + ("Europe/Sarajevo", "Europe/Sarajevo"), + ("Europe/Saratov", "Europe/Saratov"), + ("Europe/Simferopol", "Europe/Simferopol"), + ("Europe/Skopje", "Europe/Skopje"), + ("Europe/Sofia", "Europe/Sofia"), + ("Europe/Stockholm", "Europe/Stockholm"), + ("Europe/Tallinn", "Europe/Tallinn"), + ("Europe/Tirane", "Europe/Tirane"), + ("Europe/Tiraspol", "Europe/Tiraspol"), + ("Europe/Ulyanovsk", "Europe/Ulyanovsk"), + ("Europe/Uzhgorod", "Europe/Uzhgorod"), + ("Europe/Vaduz", "Europe/Vaduz"), + ("Europe/Vatican", "Europe/Vatican"), + ("Europe/Vienna", "Europe/Vienna"), + ("Europe/Vilnius", "Europe/Vilnius"), + ("Europe/Volgograd", "Europe/Volgograd"), + ("Europe/Warsaw", "Europe/Warsaw"), + ("Europe/Zagreb", "Europe/Zagreb"), + ("Europe/Zaporozhye", "Europe/Zaporozhye"), + ("Europe/Zurich", "Europe/Zurich"), + ("GB", "GB"), + ("GB-Eire", "GB-Eire"), + ("GMT", "GMT"), + ("GMT+0", "GMT+0"), + ("GMT-0", "GMT-0"), + ("GMT0", "GMT0"), + ("Greenwich", "Greenwich"), + ("HST", "HST"), + ("Hongkong", "Hongkong"), + ("Iceland", "Iceland"), + ("Indian/Antananarivo", "Indian/Antananarivo"), + ("Indian/Chagos", "Indian/Chagos"), + ("Indian/Christmas", "Indian/Christmas"), + ("Indian/Cocos", "Indian/Cocos"), + ("Indian/Comoro", "Indian/Comoro"), + ("Indian/Kerguelen", "Indian/Kerguelen"), + ("Indian/Mahe", "Indian/Mahe"), + ("Indian/Maldives", "Indian/Maldives"), + ("Indian/Mauritius", "Indian/Mauritius"), + ("Indian/Mayotte", "Indian/Mayotte"), + ("Indian/Reunion", "Indian/Reunion"), + ("Iran", "Iran"), + ("Israel", "Israel"), + ("Jamaica", "Jamaica"), + ("Japan", "Japan"), + ("Kwajalein", "Kwajalein"), + ("Libya", "Libya"), + ("MET", "MET"), + ("MST", "MST"), + ("MST7MDT", "MST7MDT"), + ("Mexico/BajaNorte", "Mexico/BajaNorte"), + ("Mexico/BajaSur", "Mexico/BajaSur"), + ("Mexico/General", "Mexico/General"), + ("NZ", "NZ"), + ("NZ-CHAT", "NZ-CHAT"), + ("Navajo", "Navajo"), + ("PRC", "PRC"), + ("PST8PDT", "PST8PDT"), + ("Pacific/Apia", "Pacific/Apia"), + ("Pacific/Auckland", "Pacific/Auckland"), + ("Pacific/Bougainville", "Pacific/Bougainville"), + ("Pacific/Chatham", "Pacific/Chatham"), + ("Pacific/Chuuk", "Pacific/Chuuk"), + ("Pacific/Easter", "Pacific/Easter"), + ("Pacific/Efate", "Pacific/Efate"), + ("Pacific/Enderbury", "Pacific/Enderbury"), + ("Pacific/Fakaofo", "Pacific/Fakaofo"), + ("Pacific/Fiji", "Pacific/Fiji"), + ("Pacific/Funafuti", "Pacific/Funafuti"), + ("Pacific/Galapagos", "Pacific/Galapagos"), + ("Pacific/Gambier", "Pacific/Gambier"), + ("Pacific/Guadalcanal", "Pacific/Guadalcanal"), + ("Pacific/Guam", "Pacific/Guam"), + ("Pacific/Honolulu", "Pacific/Honolulu"), + ("Pacific/Johnston", "Pacific/Johnston"), + ("Pacific/Kiritimati", "Pacific/Kiritimati"), + ("Pacific/Kosrae", "Pacific/Kosrae"), + ("Pacific/Kwajalein", "Pacific/Kwajalein"), + ("Pacific/Majuro", "Pacific/Majuro"), + ("Pacific/Marquesas", "Pacific/Marquesas"), + ("Pacific/Midway", "Pacific/Midway"), + ("Pacific/Nauru", "Pacific/Nauru"), + ("Pacific/Niue", "Pacific/Niue"), + ("Pacific/Norfolk", "Pacific/Norfolk"), + ("Pacific/Noumea", "Pacific/Noumea"), + ("Pacific/Pago_Pago", "Pacific/Pago_Pago"), + ("Pacific/Palau", "Pacific/Palau"), + ("Pacific/Pitcairn", "Pacific/Pitcairn"), + ("Pacific/Pohnpei", "Pacific/Pohnpei"), + ("Pacific/Ponape", "Pacific/Ponape"), + ("Pacific/Port_Moresby", "Pacific/Port_Moresby"), + ("Pacific/Rarotonga", "Pacific/Rarotonga"), + ("Pacific/Saipan", "Pacific/Saipan"), + ("Pacific/Samoa", "Pacific/Samoa"), + ("Pacific/Tahiti", "Pacific/Tahiti"), + ("Pacific/Tarawa", "Pacific/Tarawa"), + ("Pacific/Tongatapu", "Pacific/Tongatapu"), + ("Pacific/Truk", "Pacific/Truk"), + ("Pacific/Wake", "Pacific/Wake"), + ("Pacific/Wallis", "Pacific/Wallis"), + ("Pacific/Yap", "Pacific/Yap"), + ("Poland", "Poland"), + ("Portugal", "Portugal"), + ("ROC", "ROC"), + ("ROK", "ROK"), + ("Singapore", "Singapore"), + ("Turkey", "Turkey"), + ("UCT", "UCT"), + ("US/Alaska", "US/Alaska"), + ("US/Aleutian", "US/Aleutian"), + ("US/Arizona", "US/Arizona"), + ("US/Central", "US/Central"), + ("US/East-Indiana", "US/East-Indiana"), + ("US/Eastern", "US/Eastern"), + ("US/Hawaii", "US/Hawaii"), + ("US/Indiana-Starke", "US/Indiana-Starke"), + ("US/Michigan", "US/Michigan"), + ("US/Mountain", "US/Mountain"), + ("US/Pacific", "US/Pacific"), + ("US/Samoa", "US/Samoa"), + ("UTC", "UTC"), + ("Universal", "Universal"), + ("W-SU", "W-SU"), + ("WET", "WET"), + ("Zulu", "Zulu"), + ], + default="UTC", + max_length=100, + ), + ), + ("last_suceeded", models.DateTimeField(blank=True, null=True)), + ("succeeded", models.BooleanField(default=False)), + ("error", models.CharField(blank=True, max_length=255, null=True)), + ], + options={"abstract": False}, + ) + ] diff --git a/src/newsreader/news/collection/migrations/0002_auto_20190714_1036.py b/src/newsreader/news/collection/migrations/0002_auto_20190714_1036.py new file mode 100644 index 0000000..6854c0b --- /dev/null +++ b/src/newsreader/news/collection/migrations/0002_auto_20190714_1036.py @@ -0,0 +1,39 @@ +# Generated by Django 2.2 on 2019-07-14 10:36 + +import django.db.models.deletion + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("collection", "0001_initial"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("core", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="collectionrule", + name="category", + field=models.ForeignKey( + blank=True, + help_text="Posts from this rule will be tagged with this category", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="core.Category", + verbose_name="Category", + ), + ), + migrations.AddField( + model_name="collectionrule", + name="user", + field=models.ForeignKey( + on_delete=models.CASCADE, to=settings.AUTH_USER_MODEL + ), + ), + ] diff --git a/src/newsreader/news/collection/migrations/0003_auto_20190714_1417.py b/src/newsreader/news/collection/migrations/0003_auto_20190714_1417.py new file mode 100644 index 0000000..99f1018 --- /dev/null +++ b/src/newsreader/news/collection/migrations/0003_auto_20190714_1417.py @@ -0,0 +1,21 @@ +# Generated by Django 2.2 on 2019-07-14 14:17 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [("collection", "0002_auto_20190714_1036")] + + operations = [ + migrations.AlterField( + model_name="collectionrule", + name="user", + field=models.ForeignKey( + on_delete=models.CASCADE, + related_name="rules", + to=settings.AUTH_USER_MODEL, + ), + ) + ] diff --git a/src/newsreader/news/collection/migrations/0004_auto_20190714_1422.py b/src/newsreader/news/collection/migrations/0004_auto_20190714_1422.py new file mode 100644 index 0000000..4e9efb2 --- /dev/null +++ b/src/newsreader/news/collection/migrations/0004_auto_20190714_1422.py @@ -0,0 +1,26 @@ +# Generated by Django 2.2 on 2019-07-14 14:22 + +import django.db.models.deletion + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [("collection", "0003_auto_20190714_1417")] + + operations = [ + migrations.AlterField( + model_name="collectionrule", + name="category", + field=models.ForeignKey( + blank=True, + help_text="Posts from this rule will be tagged with this category", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="rules", + to="core.Category", + verbose_name="Category", + ), + ) + ] diff --git a/src/newsreader/news/collection/migrations/0005_auto_20200303_1932.py b/src/newsreader/news/collection/migrations/0005_auto_20200303_1932.py new file mode 100644 index 0000000..cdd3e32 --- /dev/null +++ b/src/newsreader/news/collection/migrations/0005_auto_20200303_1932.py @@ -0,0 +1,16 @@ +# Generated by Django 2.2 on 2020-03-03 19:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [("collection", "0004_auto_20190714_1422")] + + operations = [ + migrations.AlterField( + model_name="collectionrule", + name="error", + field=models.CharField(blank=True, max_length=1024, null=True), + ) + ] diff --git a/src/newsreader/news/collection/migrations/0006_auto_20200412_1955.py b/src/newsreader/news/collection/migrations/0006_auto_20200412_1955.py new file mode 100644 index 0000000..441d7f1 --- /dev/null +++ b/src/newsreader/news/collection/migrations/0006_auto_20200412_1955.py @@ -0,0 +1,27 @@ +# Generated by Django 3.0.5 on 2020-04-12 19:55 + +import django.db.models.deletion + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("collection", "0005_auto_20200303_1932"), + ] + + operations = [ + migrations.AlterField( + model_name="collectionrule", + name="user", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="rules", + to=settings.AUTH_USER_MODEL, + verbose_name="Owner", + ), + ) + ] diff --git a/src/newsreader/news/collection/migrations/__init__.py b/src/newsreader/news/collection/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/newsreader/news/collection/models.py b/src/newsreader/news/collection/models.py new file mode 100644 index 0000000..d1d62ce --- /dev/null +++ b/src/newsreader/news/collection/models.py @@ -0,0 +1,46 @@ +from django.db import models +from django.utils.translation import gettext as _ + +import pytz + +from newsreader.core.models import TimeStampedModel + + +class CollectionRule(TimeStampedModel): + name = models.CharField(max_length=100) + + url = models.URLField(max_length=1024) + website_url = models.URLField( + max_length=1024, editable=False, blank=True, null=True + ) + favicon = models.URLField(blank=True, null=True) + + timezone = models.CharField( + choices=((timezone, timezone) for timezone in pytz.all_timezones), + max_length=100, + default="UTC", + ) + + category = models.ForeignKey( + "core.Category", + blank=True, + null=True, + verbose_name=_("Category"), + related_name="rules", + help_text=_("Posts from this rule will be tagged with this category"), + on_delete=models.SET_NULL, + ) + + last_suceeded = models.DateTimeField(blank=True, null=True) + succeeded = models.BooleanField(default=False) + error = models.CharField(max_length=1024, blank=True, null=True) + + user = models.ForeignKey( + "accounts.User", + verbose_name=_("Owner"), + related_name="rules", + on_delete=models.CASCADE, + ) + + def __str__(self): + return self.name diff --git a/src/newsreader/news/collection/response_handler.py b/src/newsreader/news/collection/response_handler.py new file mode 100644 index 0000000..3a16376 --- /dev/null +++ b/src/newsreader/news/collection/response_handler.py @@ -0,0 +1,42 @@ +from requests.exceptions import ConnectionError as RequestConnectionError + +from newsreader.news.collection.exceptions import ( + StreamConnectionError, + StreamDeniedException, + StreamException, + StreamForbiddenException, + StreamNotFoundException, + StreamTimeOutException, +) + + +class ResponseHandler: + status_code_mapping = { + 404: StreamNotFoundException, + 401: StreamDeniedException, + 403: StreamForbiddenException, + 408: StreamTimeOutException, + } + + exception_mapping = {RequestConnectionError: StreamConnectionError} + + def __enter__(self): + return self + + def handle_response(self, response): + status_code = response.status_code + + if status_code in self.status_code_mapping: + raise self.status_code_mapping[status_code] + + def handle_exception(self, exception): + try: + stream_exception = self.exception_mapping[type(exception)] + except KeyError: + stream_exception = StreamException + + message = getattr(exception, "message", str(exception)) + raise stream_exception(message=message) from exception + + def __exit__(self, *args, **kwargs): + pass diff --git a/src/newsreader/news/collection/serializers.py b/src/newsreader/news/collection/serializers.py new file mode 100644 index 0000000..640d16e --- /dev/null +++ b/src/newsreader/news/collection/serializers.py @@ -0,0 +1,15 @@ +from rest_framework import serializers + +from newsreader.news.collection.models import CollectionRule + + +class RuleSerializer(serializers.ModelSerializer): + user = serializers.HiddenField(default=serializers.CurrentUserDefault()) + unread = serializers.SerializerMethodField() + + def get_unread(self, rule): + return rule.posts.filter(read=False).count() + + class Meta: + model = CollectionRule + fields = ("id", "name", "url", "favicon", "category", "user", "unread") diff --git a/src/newsreader/news/collection/tasks.py b/src/newsreader/news/collection/tasks.py new file mode 100644 index 0000000..b2dbf58 --- /dev/null +++ b/src/newsreader/news/collection/tasks.py @@ -0,0 +1,21 @@ +from django.core.exceptions import ObjectDoesNotExist + +from newsreader.accounts.models import User +from newsreader.celery import app +from newsreader.news.collection.feed import FeedCollector +from newsreader.utils.celery import MemCacheLock + + +@app.task(bind=True) +def collect(self, user_pk): + try: + user = User.objects.get(pk=user_pk) + except ObjectDoesNotExist: + return + + with MemCacheLock(f"{user.email}-task", self.app.oid) as acquired: + if acquired: + rules = user.rules.all() + + collector = FeedCollector() + collector.collect(rules=rules) diff --git a/src/newsreader/news/collection/templates/collection/import.html b/src/newsreader/news/collection/templates/collection/import.html new file mode 100644 index 0000000..ac8317d --- /dev/null +++ b/src/newsreader/news/collection/templates/collection/import.html @@ -0,0 +1,37 @@ +{% extends "base.html" %} + +{% load static i18n %} + +{% block content %} +
    +
    + {% csrf_token %} + {{ form.non_field_errors }} + +
    +

    {% trans "Import an OPML file" %}

    +
    +
    +
    + + {{ form.file.errors }} + {{ form.file }} +
    + +
    + + {{ form.skip_existing }} +
    + +
    + Cancel + +
    +
    +
    +
    +{% endblock %} diff --git a/src/newsreader/news/collection/templates/collection/rule-create.html b/src/newsreader/news/collection/templates/collection/rule-create.html new file mode 100644 index 0000000..b8db042 --- /dev/null +++ b/src/newsreader/news/collection/templates/collection/rule-create.html @@ -0,0 +1,9 @@ +{% extends "collection/rule.html" %} + +{% block form-header %} +

    Create a rule

    +{% endblock %} + +{% block confirm-button %} + +{% endblock %} diff --git a/src/newsreader/news/collection/templates/collection/rule-update.html b/src/newsreader/news/collection/templates/collection/rule-update.html new file mode 100644 index 0000000..403f86e --- /dev/null +++ b/src/newsreader/news/collection/templates/collection/rule-update.html @@ -0,0 +1,9 @@ +{% extends "collection/rule.html" %} + +{% block form-header %} +

    Update rule

    +{% endblock %} + +{% block confirm-button %} + +{% endblock %} diff --git a/src/newsreader/news/collection/templates/collection/rule.html b/src/newsreader/news/collection/templates/collection/rule.html new file mode 100644 index 0000000..32aa370 --- /dev/null +++ b/src/newsreader/news/collection/templates/collection/rule.html @@ -0,0 +1,55 @@ +{% extends "base.html" %} + +{% load static %} + +{% block content %} +
    +
    + {% csrf_token %} + {{ form.non_field_errors }} + +
    + {% block form-header %}{% endblock %} +
    +
    +
    + + {{ form.name.errors }} + {{ form.name }} +
    + +
    + + {{ form.category.errors }} + {{ form.category }} +
    + +
    + + {{ form.url.errors }} + {{ form.url }} +
    + +
    + + {{ form.favicon.errors }} + {{ form.favicon }} +
    + +
    + + The timezone which the feed uses + {{ form.timezone.errors }} + {{ form.timezone }} +
    +
    + +
    +
    + Cancel + {% block confirm-button %}{% endblock %} +
    +
    +
    +
    +{% endblock %} diff --git a/src/newsreader/news/collection/templates/collection/rules.html b/src/newsreader/news/collection/templates/collection/rules.html new file mode 100644 index 0000000..508916a --- /dev/null +++ b/src/newsreader/news/collection/templates/collection/rules.html @@ -0,0 +1,30 @@ +{% extends "base.html" %} + +{% load static %} + +{% block content %} +
    +{% endblock %} + +{% block scripts %} + + + {{ block.super }} +{% endblock %} diff --git a/src/newsreader/news/collection/tests/__init__.py b/src/newsreader/news/collection/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/newsreader/news/collection/tests/endpoints/__init__.py b/src/newsreader/news/collection/tests/endpoints/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/newsreader/news/collection/tests/endpoints/rule/__init__.py b/src/newsreader/news/collection/tests/endpoints/rule/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/newsreader/news/collection/tests/endpoints/rule/detail/__init__.py b/src/newsreader/news/collection/tests/endpoints/rule/detail/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/newsreader/news/collection/tests/endpoints/rule/detail/tests.py b/src/newsreader/news/collection/tests/endpoints/rule/detail/tests.py new file mode 100644 index 0000000..1c281d9 --- /dev/null +++ b/src/newsreader/news/collection/tests/endpoints/rule/detail/tests.py @@ -0,0 +1,239 @@ +import json + +from django.test import TestCase +from django.urls import reverse + +from newsreader.accounts.tests.factories import UserFactory +from newsreader.news.collection.tests.factories import CollectionRuleFactory +from newsreader.news.core.models import Post +from newsreader.news.core.tests.factories import CategoryFactory, PostFactory + + +class CollectionRuleDetailViewTestCase(TestCase): + def setUp(self): + self.user = UserFactory(password="test") + self.client.force_login(self.user) + + def test_simple(self): + rule = CollectionRuleFactory(user=self.user) + + response = self.client.get(reverse("api:rules-detail", args=[rule.pk])) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertEquals(data["id"], rule.pk) + + self.assertTrue("name" in data) + self.assertTrue("url" in data) + self.assertTrue("favicon" in data) + self.assertTrue("category" in data) + + def test_not_known(self): + response = self.client.get(reverse("api:rules-detail", args=[100])) + data = response.json() + + self.assertEquals(response.status_code, 404) + self.assertEquals(data["detail"], "Not found.") + + def test_post(self): + rule = CollectionRuleFactory(user=self.user) + + response = self.client.post(reverse("api:rules-detail", args=[rule.pk])) + data = response.json() + + self.assertEquals(response.status_code, 405) + self.assertEquals(data["detail"], 'Method "POST" not allowed.') + + def test_patch(self): + rule = CollectionRuleFactory(name="BBC", user=self.user) + + response = self.client.patch( + reverse("api:rules-detail", args=[rule.pk]), + data=json.dumps({"name": "The guardian"}), + content_type="application/json", + ) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertEquals(data["name"], "The guardian") + + def test_category_change(self): + old_category = CategoryFactory(user=self.user) + new_category = CategoryFactory(user=self.user) + + rule = CollectionRuleFactory(name="BBC", category=old_category, user=self.user) + + response = self.client.patch( + reverse("api:rules-detail", args=[rule.pk]), + data=json.dumps({"category": absolute_url}), + content_type="application/json", + ) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertEquals(data["category"], new_category.pk) + + def test_identifier_cannot_be_changed(self): + rule = CollectionRuleFactory(user=self.user) + + response = self.client.patch( + reverse("api:rules-detail", args=[rule.pk]), + data=json.dumps({"id": 44}), + content_type="application/json", + ) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertEquals(data["id"], rule.pk) + + def test_category_change(self): + rule = CollectionRuleFactory(user=self.user) + category = CategoryFactory(user=self.user) + + response = self.client.patch( + reverse("api:rules-detail", args=[rule.pk]), + data=json.dumps({"category": category.pk}), + content_type="application/json", + ) + data = response.json() + data["category"] + + self.assertEquals(response.status_code, 200) + self.assertEquals(data["category"], category.pk) + + def test_put(self): + rule = CollectionRuleFactory(name="BBC", user=self.user) + + response = self.client.put( + reverse("api:rules-detail", args=[rule.pk]), + data=json.dumps({"name": "BBC", "url": "https://www.bbc.co.uk"}), + content_type="application/json", + ) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertEquals(data["name"], "BBC") + + def test_delete(self): + rule = CollectionRuleFactory(user=self.user) + + response = self.client.delete(reverse("api:rules-detail", args=[rule.pk])) + + self.assertEquals(response.status_code, 204) + + def test_rule_with_unauthenticated_user(self): + self.client.logout() + + rule = CollectionRuleFactory(name="BBC", user=self.user) + + response = self.client.patch( + reverse("api:rules-detail", args=[rule.pk]), + data=json.dumps({"name": "The guardian"}), + content_type="application/json", + ) + + self.assertEquals(response.status_code, 403) + + def test_rule_with_unauthorized_user(self): + other_user = UserFactory() + rule = CollectionRuleFactory(name="BBC", user=other_user) + + response = self.client.patch( + reverse("api:rules-detail", args=[rule.pk]), + data=json.dumps({"name": "The guardian"}), + content_type="application/json", + ) + + self.assertEquals(response.status_code, 403) + + def test_read_count(self): + rule = CollectionRuleFactory(user=self.user) + + PostFactory.create_batch(size=20, read=False, rule=rule) + PostFactory.create_batch(size=20, read=True, rule=rule) + + response = self.client.get(reverse("api:rules-detail", args=[rule.pk])) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertEquals(data["unread"], 20) + + +class CollectionRuleReadTestCase(TestCase): + def setUp(self): + self.user = UserFactory(password="test") + self.client.force_login(self.user) + + def test_rule_read(self): + rule = CollectionRuleFactory(user=self.user) + + PostFactory.create_batch(size=20, read=False, rule=rule) + + response = self.client.post(reverse("api:rules-read", args=[rule.pk])) + data = response.json() + + self.assertEquals(response.status_code, 201) + self.assertEquals(data["unread"], 0) + + def test_rule_unknown(self): + response = self.client.post(reverse("api:rules-read", args=[101])) + + self.assertEquals(response.status_code, 404) + + def test_unauthenticated_user(self): + self.client.logout() + + rule = CollectionRuleFactory(user=self.user) + + PostFactory.create_batch(size=20, read=False, rule=rule) + + response = self.client.post(reverse("api:rules-read", args=[rule.pk])) + + self.assertEquals(response.status_code, 403) + + def test_unauthorized_user(self): + other_user = UserFactory() + rule = CollectionRuleFactory(user=other_user) + + PostFactory.create_batch(size=20, read=False, rule=rule) + + response = self.client.post(reverse("api:rules-read", args=[rule.pk])) + + self.assertEquals(response.status_code, 403) + self.assertEquals(Post.objects.filter(read=False).count(), 20) + + def test_get(self): + rule = CollectionRuleFactory(user=self.user) + + response = self.client.get(reverse("api:rules-read", args=[rule.pk])) + + self.assertEquals(response.status_code, 405) + + def test_patch(self): + rule = CollectionRuleFactory(name="BBC", user=self.user) + + response = self.client.patch( + reverse("api:rules-read", args=[rule.pk]), + data=json.dumps({"name": "Not possible"}), + content_type="application/json", + ) + + self.assertEquals(response.status_code, 405) + + def test_put(self): + rule = CollectionRuleFactory(name="BBC", user=self.user) + + response = self.client.put( + reverse("api:rules-read", args=[rule.pk]), + data=json.dumps({"name": "Not possible"}), + content_type="application/json", + ) + + self.assertEquals(response.status_code, 405) + + def test_delete(self): + rule = CollectionRuleFactory(user=self.user) + + response = self.client.delete(reverse("api:rules-read", args=[rule.pk])) + + self.assertEquals(response.status_code, 405) diff --git a/src/newsreader/news/collection/tests/endpoints/rule/list/__init__.py b/src/newsreader/news/collection/tests/endpoints/rule/list/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/newsreader/news/collection/tests/endpoints/rule/list/tests.py b/src/newsreader/news/collection/tests/endpoints/rule/list/tests.py new file mode 100644 index 0000000..0e2a269 --- /dev/null +++ b/src/newsreader/news/collection/tests/endpoints/rule/list/tests.py @@ -0,0 +1,367 @@ +import json + +from datetime import date, datetime, time + +from django.test import TestCase +from django.urls import reverse + +import pytz + +from newsreader.accounts.tests.factories import UserFactory +from newsreader.news.collection.tests.factories import CollectionRuleFactory +from newsreader.news.core.tests.factories import CategoryFactory, PostFactory + + +class RuleListViewTestCase(TestCase): + def setUp(self): + self.user = UserFactory(password="test") + self.client.force_login(self.user) + + def test_simple(self): + CollectionRuleFactory.create_batch(size=3, user=self.user) + + response = self.client.get(reverse("api:rules-list")) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertTrue("results" in data) + self.assertTrue("count" in data) + self.assertEquals(data["count"], 3) + + def test_ordering(self): + rules = [ + CollectionRuleFactory( + created=datetime.combine( + date(2019, 5, 20), time(hour=16, minute=7, second=37), pytz.utc + ), + user=self.user, + ), + CollectionRuleFactory( + created=datetime.combine( + date(2019, 7, 20), time(hour=18, minute=7, second=37), pytz.utc + ), + user=self.user, + ), + CollectionRuleFactory( + created=datetime.combine( + date(2019, 7, 20), time(hour=16, minute=7, second=37), pytz.utc + ), + user=self.user, + ), + ] + + response = self.client.get(reverse("api:rules-list")) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertTrue("results" in data) + self.assertTrue("count" in data) + self.assertEquals(data["count"], 3) + + self.assertEquals(data["results"][0]["id"], rules[1].pk) + self.assertEquals(data["results"][1]["id"], rules[2].pk) + self.assertEquals(data["results"][2]["id"], rules[0].pk) + + def test_pagination_count(self): + CollectionRuleFactory.create_batch(size=80, user=self.user) + + response = self.client.get(reverse("api:rules-list"), {"count": 30}) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertEquals(data["count"], 80) + self.assertEquals(len(data["results"]), 30) + + def test_empty(self): + response = self.client.get(reverse("api:rules-list")) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertTrue("results" in data) + self.assertTrue("count" in data) + + self.assertEquals(data["count"], 0) + self.assertEquals(len(data["results"]), 0) + + def test_post(self): + category = CategoryFactory(user=self.user) + + data = {"name": "BBC", "url": "https://www.bbc.co.uk", "category": category.pk} + + response = self.client.post( + reverse("api:rules-list"), + data=json.dumps(data), + content_type="application/json", + ) + data = response.json() + + self.assertEquals(response.status_code, 405) + self.assertEquals(data["detail"], 'Method "POST" not allowed.') + + def test_patch(self): + response = self.client.patch(reverse("api:rules-list")) + data = response.json() + + self.assertEquals(response.status_code, 405) + self.assertEquals(data["detail"], 'Method "PATCH" not allowed.') + + def test_put(self): + response = self.client.put(reverse("api:rules-list")) + data = response.json() + + self.assertEquals(response.status_code, 405) + self.assertEquals(data["detail"], 'Method "PUT" not allowed.') + + def test_delete(self): + response = self.client.delete(reverse("api:rules-list")) + data = response.json() + + self.assertEquals(response.status_code, 405) + self.assertEquals(data["detail"], 'Method "DELETE" not allowed.') + + def test_rules_with_unauthenticated_user(self): + self.client.logout() + + CollectionRuleFactory.create_batch(size=3, user=self.user) + + response = self.client.get(reverse("api:rules-list")) + + self.assertEquals(response.status_code, 403) + + def test_rules_with_unauthorized_user(self): + other_user = UserFactory() + CollectionRuleFactory.create_batch(size=3, user=other_user) + + response = self.client.get(reverse("api:rules-list")) + data = response.json() + + self.assertEquals(response.status_code, 200) + + self.assertEquals(data["count"], 0) + self.assertEquals(len(data["results"]), 0) + + +class NestedRuleListViewTestCase(TestCase): + def setUp(self): + self.user = UserFactory(password="test") + self.client.force_login(self.user) + + def test_simple(self): + rule = CollectionRuleFactory.create(user=self.user) + PostFactory.create_batch(size=5, rule=rule) + + response = self.client.get( + reverse("api:rules-nested-posts", kwargs={"pk": rule.pk}) + ) + data = response.json() + + self.assertEquals(response.status_code, 200) + + self.assertTrue("results" in data) + self.assertTrue("count" in data) + self.assertEquals(data["count"], 5) + + def test_pagination(self): + rule = CollectionRuleFactory.create(user=self.user) + PostFactory.create_batch(size=80, rule=rule) + + response = self.client.get( + reverse("api:rules-nested-posts", kwargs={"pk": rule.pk}), {"count": 30} + ) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertEquals(data["count"], 80) + self.assertEquals(len(data["results"]), 30) + + def test_empty(self): + rule = CollectionRuleFactory.create(user=self.user) + + response = self.client.get( + reverse("api:rules-nested-posts", kwargs={"pk": rule.pk}) + ) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertEquals(data["count"], 0) + self.assertEquals(len(data["results"]), 0) + + def test_not_known(self): + response = self.client.get(reverse("api:rules-nested-posts", kwargs={"pk": 0})) + + self.assertEquals(response.status_code, 404) + + def test_post(self): + rule = CollectionRuleFactory.create(user=self.user) + + response = self.client.post( + reverse("api:rules-nested-posts", kwargs={"pk": rule.pk}), + data=json.dumps({}), + content_type="application/json", + ) + data = response.json() + + self.assertEquals(response.status_code, 405) + self.assertEquals(data["detail"], 'Method "POST" not allowed.') + + def test_patch(self): + rule = CollectionRuleFactory.create(user=self.user) + + response = self.client.patch( + reverse("api:rules-nested-posts", kwargs={"pk": rule.pk}), + data=json.dumps({}), + content_type="application/json", + ) + data = response.json() + + self.assertEquals(response.status_code, 405) + self.assertEquals(data["detail"], 'Method "PATCH" not allowed.') + + def test_put(self): + rule = CollectionRuleFactory.create(user=self.user) + + response = self.client.put( + reverse("api:rules-nested-posts", kwargs={"pk": rule.pk}), + data=json.dumps({}), + content_type="application/json", + ) + data = response.json() + + self.assertEquals(response.status_code, 405) + self.assertEquals(data["detail"], 'Method "PUT" not allowed.') + + def test_delete(self): + rule = CollectionRuleFactory.create(user=self.user) + + response = self.client.delete( + reverse("api:rules-nested-posts", kwargs={"pk": rule.pk}), + data=json.dumps({}), + content_type="application/json", + ) + data = response.json() + + self.assertEquals(response.status_code, 405) + self.assertEquals(data["detail"], 'Method "DELETE" not allowed.') + + def test_rule_with_unauthenticated_user(self): + self.client.logout() + + rule = CollectionRuleFactory(user=self.user) + + response = self.client.get( + reverse("api:rules-nested-posts", kwargs={"pk": rule.pk}) + ) + + self.assertEquals(response.status_code, 403) + + def test_rule_with_unauthorized_user(self): + other_user = UserFactory() + rule = CollectionRuleFactory(user=other_user) + + response = self.client.get( + reverse("api:rules-nested-posts", kwargs={"pk": rule.pk}) + ) + + self.assertEquals(response.status_code, 403) + + def test_posts_ordering(self): + rule = CollectionRuleFactory( + user=self.user, category=CategoryFactory(user=self.user) + ) + + posts = [ + PostFactory( + title="I'm the first post", + rule=rule, + publication_date=datetime.combine( + date(2019, 5, 20), time(hour=16, minute=7, second=37), pytz.utc + ), + ), + PostFactory( + title="I'm the second post", + rule=rule, + publication_date=datetime.combine( + date(2019, 7, 20), time(hour=18, minute=7, second=37), pytz.utc + ), + ), + PostFactory( + title="I'm the third post", + rule=rule, + publication_date=datetime.combine( + date(2019, 7, 20), time(hour=16, minute=7, second=37), pytz.utc + ), + ), + ] + + response = self.client.get( + reverse("api:rules-nested-posts", kwargs={"pk": rule.pk}) + ) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertTrue("results" in data) + self.assertTrue("count" in data) + self.assertEquals(data["count"], 3) + + self.assertEquals(data["results"][0]["id"], posts[1].pk) + self.assertEquals(data["results"][1]["id"], posts[2].pk) + self.assertEquals(data["results"][2]["id"], posts[0].pk) + + def test_only_posts_from_rule_are_returned(self): + rule = CollectionRuleFactory.create(user=self.user) + other_rule = CollectionRuleFactory.create(user=self.user) + + PostFactory.create_batch(size=5, rule=rule) + PostFactory.create_batch(size=5, rule=other_rule) + + response = self.client.get( + reverse("api:rules-nested-posts", kwargs={"pk": rule.pk}) + ) + data = response.json() + + self.assertEquals(response.status_code, 200) + + self.assertTrue("results" in data) + self.assertTrue("count" in data) + self.assertEquals(data["count"], 5) + + for post in data["results"]: + self.assertEquals(post["rule"], rule.pk) + + def test_unread_posts(self): + rule = CollectionRuleFactory.create(user=self.user) + + PostFactory.create_batch(size=10, rule=rule, read=False) + PostFactory.create_batch(size=10, rule=rule, read=True) + + response = self.client.get( + reverse("api:rules-nested-posts", kwargs={"pk": rule.pk}), {"read": "false"} + ) + + data = response.json() + posts = data["results"] + + self.assertEquals(response.status_code, 200) + self.assertEquals(data["count"], 10) + + for post in posts: + self.assertEquals(post["read"], False) + + def test_read_posts(self): + rule = CollectionRuleFactory.create(user=self.user) + + PostFactory.create_batch(size=20, rule=rule, read=False) + PostFactory.create_batch(size=10, rule=rule, read=True) + + response = self.client.get( + reverse("api:rules-nested-posts", kwargs={"pk": rule.pk}), {"read": "true"} + ) + + data = response.json() + posts = data["results"] + + self.assertEquals(response.status_code, 200) + self.assertEquals(data["count"], 10) + + for post in posts: + self.assertEquals(post["read"], True) diff --git a/src/newsreader/news/collection/tests/factories.py b/src/newsreader/news/collection/tests/factories.py new file mode 100644 index 0000000..678e0f4 --- /dev/null +++ b/src/newsreader/news/collection/tests/factories.py @@ -0,0 +1,19 @@ +import factory + +from newsreader.accounts.tests.factories import UserFactory +from newsreader.news.collection.models import CollectionRule + + +class CollectionRuleFactory(factory.django.DjangoModelFactory): + name = factory.Sequence(lambda n: "CollectionRule-{}".format(n)) + url = factory.Faker("url") + website_url = factory.Faker("url") + + category = factory.SubFactory( + "newsreader.news.core.tests.factories.CategoryFactory" + ) + + user = factory.SubFactory(UserFactory) + + class Meta: + model = CollectionRule diff --git a/src/newsreader/news/collection/tests/favicon/__init__.py b/src/newsreader/news/collection/tests/favicon/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/newsreader/news/collection/tests/favicon/builder/__init__.py b/src/newsreader/news/collection/tests/favicon/builder/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/newsreader/news/collection/tests/favicon/builder/mocks.py b/src/newsreader/news/collection/tests/favicon/builder/mocks.py new file mode 100644 index 0000000..8011472 --- /dev/null +++ b/src/newsreader/news/collection/tests/favicon/builder/mocks.py @@ -0,0 +1,89 @@ +from bs4 import BeautifulSoup + + +simple_mock = BeautifulSoup( + """ + + + + + +
    + + + """, + "lxml", +) + +mock_without_url = BeautifulSoup( + """ + + + + + +
    + + + """, + "lxml", +) + +mock_without_header = BeautifulSoup( + """ + + +
    + + + """, + "lxml", +) + +mock_with_weird_path = BeautifulSoup( + """ + + + + + +
    + + + """, + "lxml", +) + +mock_with_other_url = BeautifulSoup( + """ + + + + + + +
    + + + """, + "lxml", +) + +mock_with_multiple_icons = BeautifulSoup( + """ + + + + + + + + + + +
    + + + """, + "lxml", +) diff --git a/src/newsreader/news/collection/tests/favicon/builder/tests.py b/src/newsreader/news/collection/tests/favicon/builder/tests.py new file mode 100644 index 0000000..e8a1a34 --- /dev/null +++ b/src/newsreader/news/collection/tests/favicon/builder/tests.py @@ -0,0 +1,62 @@ +from django.test import TestCase + +from newsreader.news.collection.favicon import FaviconBuilder +from newsreader.news.collection.tests.factories import CollectionRuleFactory +from newsreader.news.collection.tests.favicon.builder.mocks import * + + +class FaviconBuilderTestCase(TestCase): + def setUp(self): + self.maxDiff = None + + def test_simple(self): + rule = CollectionRuleFactory(favicon=None) + + with FaviconBuilder((rule, simple_mock)) as builder: + builder.build() + + self.assertEquals(rule.favicon, "https://www.bbc.com/favicon.ico") + + def test_without_url(self): + rule = CollectionRuleFactory( + website_url="https://www.theguardian.com/", favicon=None + ) + + with FaviconBuilder((rule, mock_without_url)) as builder: + builder.build() + + self.assertEquals(rule.favicon, "https://www.theguardian.com/favicon.ico") + + def test_without_header(self): + rule = CollectionRuleFactory(favicon=None) + + with FaviconBuilder((rule, mock_without_header)) as builder: + builder.build() + + self.assertEquals(rule.favicon, None) + + def test_weird_path(self): + rule = CollectionRuleFactory(favicon=None) + + with FaviconBuilder((rule, mock_with_weird_path)) as builder: + builder.build() + + self.assertEquals( + rule.favicon, "https://www.theguardian.com/jabadaba/doe/favicon.ico" + ) + + def test_other_url(self): + rule = CollectionRuleFactory(favicon=None) + + with FaviconBuilder((rule, mock_with_other_url)) as builder: + builder.build() + + self.assertEquals(rule.favicon, "https://www.theguardian.com/icon.png") + + def test_url_with_favicon_takes_precedence(self): + rule = CollectionRuleFactory(favicon=None) + + with FaviconBuilder((rule, mock_with_multiple_icons)) as builder: + builder.build() + + self.assertEquals(rule.favicon, "https://www.bbc.com/favicon.ico") diff --git a/src/newsreader/news/collection/tests/favicon/client/__init__.py b/src/newsreader/news/collection/tests/favicon/client/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/newsreader/news/collection/tests/favicon/client/mocks.py b/src/newsreader/news/collection/tests/favicon/client/mocks.py new file mode 100644 index 0000000..a4c5ee1 --- /dev/null +++ b/src/newsreader/news/collection/tests/favicon/client/mocks.py @@ -0,0 +1,13 @@ +from bs4 import BeautifulSoup + + +simple_mock = BeautifulSoup( + """ + + +
    + + + """, + "lxml", +) diff --git a/src/newsreader/news/collection/tests/favicon/client/tests.py b/src/newsreader/news/collection/tests/favicon/client/tests.py new file mode 100644 index 0000000..717ee0c --- /dev/null +++ b/src/newsreader/news/collection/tests/favicon/client/tests.py @@ -0,0 +1,90 @@ +from unittest.mock import MagicMock + +from django.test import TestCase + +from newsreader.news.collection.exceptions import ( + StreamDeniedException, + StreamException, + StreamNotFoundException, + StreamTimeOutException, +) +from newsreader.news.collection.favicon import FaviconClient +from newsreader.news.collection.tests.factories import CollectionRuleFactory +from newsreader.news.collection.tests.favicon.client.mocks import simple_mock + + +class FaviconClientTestCase(TestCase): + def setUp(self): + self.maxDiff = None + + def test_simple(self): + rule = CollectionRuleFactory() + stream = MagicMock(url="https://www.bbc.com") + stream.read.return_value = (simple_mock, stream) + + with FaviconClient([(rule, stream)]) as client: + for rule, data in client: + self.assertEquals(rule.pk, rule.pk) + self.assertEquals(data, simple_mock) + + stream.read.assert_called_once_with() + + def test_client_catches_stream_exception(self): + rule = CollectionRuleFactory(error=None, succeeded=True) + stream = MagicMock(url="https://www.bbc.com") + stream.read.side_effect = StreamException + + with FaviconClient([(rule, stream)]) as client: + for rule, data in client: + pass + + stream.read.assert_called_once_with() + + # The favicon client does not set CollectionRule errors + self.assertEquals(rule.succeeded, True) + self.assertEquals(rule.error, None) + + def test_client_catches_stream_not_found_exception(self): + rule = CollectionRuleFactory(error=None, succeeded=True) + stream = MagicMock(url="https://www.bbc.com") + stream.read.side_effect = StreamNotFoundException + + with FaviconClient([(rule, stream)]) as client: + for rule, data in client: + pass + + stream.read.assert_called_once_with() + + # The favicon client does not set CollectionRule errors + self.assertEquals(rule.succeeded, True) + self.assertEquals(rule.error, None) + + def test_client_catches_stream_denied_exception(self): + rule = CollectionRuleFactory(error=None, succeeded=True) + stream = MagicMock(url="https://www.bbc.com") + stream.read.side_effect = StreamDeniedException + + with FaviconClient([(rule, stream)]) as client: + for rule, data in client: + pass + + stream.read.assert_called_once_with() + + # The favicon client does not set CollectionRule errors + self.assertEquals(rule.succeeded, True) + self.assertEquals(rule.error, None) + + def test_client_catches_stream_timed_out(self): + rule = CollectionRuleFactory(error=None, succeeded=True) + stream = MagicMock(url="https://www.bbc.com") + stream.read.side_effect = StreamTimeOutException + + with FaviconClient([(rule, stream)]) as client: + for rule, data in client: + pass + + stream.read.assert_called_once_with() + + # The favicon client does not set CollectionRule errors + self.assertEquals(rule.succeeded, True) + self.assertEquals(rule.error, None) diff --git a/src/newsreader/news/collection/tests/favicon/collector/__init__.py b/src/newsreader/news/collection/tests/favicon/collector/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/newsreader/news/collection/tests/favicon/collector/mocks.py b/src/newsreader/news/collection/tests/favicon/collector/mocks.py new file mode 100644 index 0000000..3318ffd --- /dev/null +++ b/src/newsreader/news/collection/tests/favicon/collector/mocks.py @@ -0,0 +1,166 @@ +from time import struct_time + +from bs4 import BeautifulSoup + + +feed_mock = { + "bozo": 0, + "encoding": "utf-8", + "entries": [ + { + "guidislink": False, + "href": "", + "id": "https://www.bbc.co.uk/news/world-us-canada-48338168", + "link": "https://www.bbc.co.uk/news/world-us-canada-48338168", + "links": [ + { + "href": "https://www.bbc.co.uk/news/world-us-canada-48338168", + "rel": "alternate", + "type": "text/html", + } + ], + "media_thumbnail": [ + { + "height": "1152", + "url": "http://c.files.bbci.co.uk/7605/production/_107031203_mediaitem107031202.jpg", + "width": "2048", + } + ], + "published": "Mon, 20 May 2019 16:07:37 GMT", + "published_parsed": struct_time((2019, 5, 20, 16, 7, 37, 0, 140, 0)), + "summary": "Foreign Minister Mohammad Javad Zarif says the US " + "president should try showing Iranians some respect.", + "summary_detail": { + "base": "http://feeds.bbci.co.uk/news/rss.xml", + "language": None, + "type": "text/html", + "value": "Foreign Minister Mohammad Javad " + "Zarif says the US president should " + "try showing Iranians some " + "respect.", + }, + "title": "Trump's genocidal taunts will not end Iran - Zarif", + "title_detail": { + "base": "http://feeds.bbci.co.uk/news/rss.xml", + "language": None, + "type": "text/plain", + "value": "Trump's genocidal taunts will not " "end Iran - Zarif", + }, + }, + { + "guidislink": False, + "href": "", + "id": "https://www.bbc.co.uk/news/technology-48334739", + "link": "https://www.bbc.co.uk/news/technology-48334739", + "links": [ + { + "href": "https://www.bbc.co.uk/news/technology-48334739", + "rel": "alternate", + "type": "text/html", + } + ], + "media_thumbnail": [ + { + "height": "432", + "url": "http://c.files.bbci.co.uk/4789/production/_107031381_mediaitem107028670.jpg", + "width": "768", + } + ], + "published": "Mon, 20 May 2019 12:19:19 GMT", + "published_parsed": struct_time((2019, 5, 20, 12, 19, 19, 0, 140, 0)), + "summary": "Google's move to end business ties with Huawei will " + "affect current devices and future purchases.", + "summary_detail": { + "base": "http://feeds.bbci.co.uk/news/rss.xml", + "language": None, + "type": "text/html", + "value": "Google's move to end business ties " + "with Huawei will affect current " + "devices and future purchases.", + }, + "title": "Huawei's Android loss: How it affects you", + "title_detail": { + "base": "http://feeds.bbci.co.uk/news/rss.xml", + "language": None, + "type": "text/plain", + "value": "Huawei's Android loss: How it " "affects you", + }, + }, + { + "guidislink": False, + "href": "", + "id": "https://www.bbc.co.uk/news/uk-england-birmingham-48339080", + "link": "https://www.bbc.co.uk/news/uk-england-birmingham-48339080", + "links": [ + { + "href": "https://www.bbc.co.uk/news/uk-england-birmingham-48339080", + "rel": "alternate", + "type": "text/html", + } + ], + "media_thumbnail": [ + { + "height": "549", + "url": "http://c.files.bbci.co.uk/11D67/production/_107036037_lgbtheadjpg.jpg", + "width": "976", + } + ], + "published": "Mon, 20 May 2019 16:32:38 GMT", + "published_parsed": struct_time((2019, 5, 20, 16, 32, 38, 0, 140, 0)), + "summary": "Police are investigating the messages while an MP " + "calls for a protest exclusion zone to protect " + "children.", + "summary_detail": { + "base": "http://feeds.bbci.co.uk/news/rss.xml", + "language": None, + "type": "text/html", + "value": "Police are investigating the " + "messages while an MP calls for a " + "protest exclusion zone to protect " + "children.", + }, + "title": "Birmingham head teacher threatened over LGBT lessons", + "title_detail": { + "base": "http://feeds.bbci.co.uk/news/rss.xml", + "language": None, + "type": "text/plain", + "value": "Birmingham head teacher threatened " "over LGBT lessons", + }, + }, + ], + "feed": { + "image": { + "href": "https://news.bbcimg.co.uk/nol/shared/img/bbc_news_120x60.gif", + "link": "https://www.bbc.co.uk/news/", + "title": "BBC News - Home", + "language": "en-gb", + "link": "https://www.bbc.co.uk/news/", + }, + "link": "https://www.bbc.co.uk/news/", + "links": [ + { + "href": "https://www.bbc.co.uk/news/", + "rel": "alternate", + "type": "text/html", + } + ], + "title": "BBC News - Home", + }, + "href": "http://feeds.bbci.co.uk/news/rss.xml", + "status": 200, + "version": "rss20", +} + +website_mock = BeautifulSoup( + """ + + + + + +
    + + + """, + "lxml", +) diff --git a/src/newsreader/news/collection/tests/favicon/collector/tests.py b/src/newsreader/news/collection/tests/favicon/collector/tests.py new file mode 100644 index 0000000..44254a5 --- /dev/null +++ b/src/newsreader/news/collection/tests/favicon/collector/tests.py @@ -0,0 +1,148 @@ +from unittest.mock import MagicMock, patch + +from django.test import TestCase + +from bs4 import BeautifulSoup + +from newsreader.news.collection.exceptions import ( + StreamDeniedException, + StreamException, + StreamForbiddenException, + StreamNotFoundException, + StreamParseException, + StreamTimeOutException, +) +from newsreader.news.collection.favicon import FaviconCollector +from newsreader.news.collection.tests.factories import CollectionRuleFactory + +from .mocks import feed_mock, website_mock + + +class FaviconCollectorTestCase(TestCase): + def setUp(self): + self.maxDiff = None + + self.patched_feed_client = patch( + "newsreader.news.collection.favicon.FeedClient.__enter__" + ) + self.mocked_feed_client = self.patched_feed_client.start() + + self.patched_website_read = patch( + "newsreader.news.collection.favicon.WebsiteStream.read" + ) + self.mocked_website_read = self.patched_website_read.start() + + def tearDown(self): + patch.stopall() + + def test_simple(self): + rule = CollectionRuleFactory(succeeded=True, error=None) + + self.mocked_feed_client.return_value = [(feed_mock, MagicMock(rule=rule))] + self.mocked_website_read.return_value = (website_mock, MagicMock()) + + collector = FaviconCollector() + collector.collect() + + rule.refresh_from_db() + + self.assertEquals(rule.succeeded, True) + self.assertEquals(rule.error, None) + self.assertEquals(rule.website_url, "https://www.bbc.co.uk/news/") + self.assertEquals(rule.favicon, "https://www.bbc.co.uk/news/favicon.ico") + + def test_empty_stream(self): + rule = CollectionRuleFactory(succeeded=True, error=None) + + self.mocked_feed_client.return_value = [(feed_mock, MagicMock(rule=rule))] + self.mocked_website_read.return_value = (BeautifulSoup("", "lxml"), MagicMock()) + + collector = FaviconCollector() + collector.collect() + + rule.refresh_from_db() + + self.assertEquals(rule.succeeded, True) + self.assertEquals(rule.error, None) + self.assertEquals(rule.website_url, "https://www.bbc.co.uk/news/") + self.assertEquals(rule.favicon, None) + + def test_not_found(self): + rule = CollectionRuleFactory(succeeded=True, error=None) + + self.mocked_feed_client.return_value = [(feed_mock, MagicMock(rule=rule))] + self.mocked_website_read.side_effect = StreamNotFoundException + + collector = FaviconCollector() + collector.collect() + + rule.refresh_from_db() + + self.assertEquals(rule.succeeded, True) + self.assertEquals(rule.error, None) + self.assertEquals(rule.website_url, "https://www.bbc.co.uk/news/") + self.assertEquals(rule.favicon, None) + + def test_denied(self): + rule = CollectionRuleFactory(succeeded=True, error=None) + + self.mocked_feed_client.return_value = [(feed_mock, MagicMock(rule=rule))] + self.mocked_website_read.side_effect = StreamDeniedException + + collector = FaviconCollector() + collector.collect() + + rule.refresh_from_db() + + self.assertEquals(rule.succeeded, True) + self.assertEquals(rule.error, None) + self.assertEquals(rule.website_url, "https://www.bbc.co.uk/news/") + self.assertEquals(rule.favicon, None) + + def test_forbidden(self): + rule = CollectionRuleFactory(succeeded=True, error=None) + + self.mocked_feed_client.return_value = [(feed_mock, MagicMock(rule=rule))] + self.mocked_website_read.side_effect = StreamForbiddenException + + collector = FaviconCollector() + collector.collect() + + rule.refresh_from_db() + + self.assertEquals(rule.succeeded, True) + self.assertEquals(rule.error, None) + self.assertEquals(rule.website_url, "https://www.bbc.co.uk/news/") + self.assertEquals(rule.favicon, None) + + def test_timed_out(self): + rule = CollectionRuleFactory(succeeded=True, error=None) + + self.mocked_feed_client.return_value = [(feed_mock, MagicMock(rule=rule))] + self.mocked_website_read.side_effect = StreamTimeOutException + + collector = FaviconCollector() + collector.collect() + + rule.refresh_from_db() + + self.assertEquals(rule.succeeded, True) + self.assertEquals(rule.error, None) + self.assertEquals(rule.website_url, "https://www.bbc.co.uk/news/") + self.assertEquals(rule.favicon, None) + + def test_wrong_stream_content_type(self): + rule = CollectionRuleFactory(succeeded=True, error=None) + + self.mocked_feed_client.return_value = [(feed_mock, MagicMock(rule=rule))] + self.mocked_website_read.side_effect = StreamParseException + + collector = FaviconCollector() + collector.collect() + + rule.refresh_from_db() + + self.assertEquals(rule.succeeded, True) + self.assertEquals(rule.error, None) + self.assertEquals(rule.website_url, "https://www.bbc.co.uk/news/") + self.assertEquals(rule.favicon, None) diff --git a/src/newsreader/news/collection/tests/feed/__init__.py b/src/newsreader/news/collection/tests/feed/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/newsreader/news/collection/tests/feed/builder/__init__.py b/src/newsreader/news/collection/tests/feed/builder/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/newsreader/news/collection/tests/feed/builder/mock_html.py b/src/newsreader/news/collection/tests/feed/builder/mock_html.py new file mode 100644 index 0000000..0b814a4 --- /dev/null +++ b/src/newsreader/news/collection/tests/feed/builder/mock_html.py @@ -0,0 +1,14 @@ +html_summary = """ + + +
    +

    This is clickbait

    +

    This is clickbait

    + +
    + + + + + +""" diff --git a/src/newsreader/news/collection/tests/feed/builder/mocks.py b/src/newsreader/news/collection/tests/feed/builder/mocks.py new file mode 100644 index 0000000..83f7d0b --- /dev/null +++ b/src/newsreader/news/collection/tests/feed/builder/mocks.py @@ -0,0 +1,343 @@ +from time import struct_time + +from .mock_html import html_summary + + +simple_mock = { + "entries": [ + { + "author": "A. Author", + "id": "https://www.bbc.co.uk/news/world-us-canada-48338168", + "link": "https://www.bbc.co.uk/news/world-us-canada-48338168", + "published": "Mon, 20 May 2019 16:07:37 GMT", + "published_parsed": struct_time((2019, 5, 20, 16, 7, 37, 0, 140, 0)), + "summary": "Foreign Minister Mohammad Javad Zarif says the US " + "president should try showing Iranians some respect.", + "title": "Trump's 'genocidal taunts' will not end Iran - Zarif", + } + ] +} + +multiple_mock = { + "entries": [ + { + "author": "A. Author", + "id": "https://www.bbc.co.uk/news/world-us-canada-48338168", + "link": "https://www.bbc.co.uk/news/world-us-canada-48338168", + "published": "Mon, 20 May 2019 16:07:37 GMT", + "published_parsed": struct_time((2019, 5, 20, 16, 7, 37, 0, 140, 0)), + "summary": "Foreign Minister Mohammad Javad Zarif says the US " + "president should try showing Iranians some respect.", + "title": "Trump's 'genocidal taunts' will not end Iran - Zarif", + }, + { + "author": "A. Author", + "id": "https://www.bbc.co.uk/news/technology-48334739", + "link": "https://www.bbc.co.uk/news/technology-48334739", + "published": "Mon, 20 May 2019 12:19:19 GMT", + "published_parsed": struct_time((2019, 5, 20, 12, 19, 19, 0, 140, 0)), + "summary": "Google's move to end business ties with Huawei will " + "affect current devices and future purchases.", + "title": "Huawei's Android loss: How it affects you", + }, + { + "author": "A. Author", + "id": "https://www.bbc.co.uk/news/uk-england-birmingham-48339080", + "link": "https://www.bbc.co.uk/news/uk-england-birmingham-48339080", + "published": "Mon, 20 May 2019 16:32:38 GMT", + "published_parsed": struct_time((2019, 5, 20, 16, 32, 38, 0, 140, 0)), + "summary": "Police are investigating the messages while an MP " + 'calls for a protest exclusion zone "to protect ' + 'children".', + "title": "Birmingham head teacher threatened over LGBT lessons", + }, + ] +} + +mock_without_identifier = { + "entries": [ + { + "author": "A. Author", + "link": "https://www.bbc.co.uk/news/world-us-canada-48338168", + "published": "Mon, 20 May 2019 16:07:37 GMT", + "published_parsed": struct_time((2019, 5, 20, 16, 7, 37, 0, 140, 0)), + "summary": "Foreign Minister Mohammad Javad Zarif says the US " + "president should try showing Iranians some respect.", + "title": "Trump's 'genocidal taunts' will not end Iran - Zarif", + }, + { + "author": "A. Author", + "id": None, + "link": "https://www.bbc.co.uk/news/technology-48334739", + "published": "Mon, 20 May 2019 12:19:19 GMT", + "published_parsed": struct_time((2019, 5, 20, 12, 19, 19, 0, 140, 0)), + "summary": "Google's move to end business ties with Huawei will " + "affect current devices and future purchases.", + "title": "Huawei's Android loss: How it affects you", + }, + ] +} + +mock_without_publish_date = { + "entries": [ + { + "author": "A. Author", + "id": "https://www.bbc.co.uk/news/world-us-canada-48338168", + "link": "https://www.bbc.co.uk/news/world-us-canada-48338168", + "published": None, + "published_parsed": None, + "summary": "Foreign Minister Mohammad Javad Zarif says the US " + "president should try showing Iranians some respect.", + "title": "Trump's 'genocidal taunts' will not end Iran - Zarif", + }, + { + "author": "A. Author", + "id": "https://www.bbc.co.uk/news/technology-48334739", + "link": "https://www.bbc.co.uk/news/technology-48334739", + "summary": "Google's move to end business ties with Huawei will " + "affect current devices and future purchases.", + "title": "Huawei's Android loss: How it affects you", + }, + ] +} + +mock_without_url = { + "entries": [ + { + "author": "A. Author", + "id": "https://www.bbc.co.uk/news/world-us-canada-48338168", + "published": "Mon, 20 May 2019 16:07:37 GMT", + "published_parsed": struct_time((2019, 5, 20, 16, 7, 37, 0, 140, 0)), + "published": None, + "published_parsed": None, + "summary": "Foreign Minister Mohammad Javad Zarif says the US " + "president should try showing Iranians some respect.", + "title": "Trump's 'genocidal taunts' will not end Iran - Zarif", + }, + { + "author": "A. Author", + "id": "https://www.bbc.co.uk/news/technology-48334739", + "link": None, + "published": "Mon, 20 May 2019 16:07:37 GMT", + "published_parsed": struct_time((2019, 5, 20, 16, 7, 37, 0, 140, 0)), + "summary": "Google's move to end business ties with Huawei will " + "affect current devices and future purchases.", + "title": "Huawei's Android loss: How it affects you", + }, + ] +} + +mock_without_body = { + "entries": [ + { + "author": "A. Author", + "id": "https://www.bbc.co.uk/news/world-us-canada-48338168", + "link": "https://www.bbc.co.uk/news/world-us-canada-48338168", + "published": "Mon, 20 May 2019 16:07:37 GMT", + "published_parsed": struct_time((2019, 5, 20, 16, 7, 37, 0, 140, 0)), + "title": "Trump's 'genocidal taunts' will not end Iran - Zarif", + }, + { + "author": "A. Author", + "id": "https://www.bbc.co.uk/news/uk-england-birmingham-48339080", + "link": "https://www.bbc.co.uk/news/uk-england-birmingham-48339080", + "published": "Mon, 20 May 2019 16:32:38 GMT", + "published_parsed": struct_time((2019, 5, 20, 16, 32, 38, 0, 140, 0)), + "summary": None, + "title": "Birmingham head teacher threatened over LGBT lessons", + }, + ] +} + +mock_without_author = { + "entries": [ + { + "id": "https://www.bbc.co.uk/news/world-us-canada-48338168", + "link": "https://www.bbc.co.uk/news/world-us-canada-48338168", + "published": "Mon, 20 May 2019 16:07:37 GMT", + "published_parsed": struct_time((2019, 5, 20, 16, 7, 37, 0, 140, 0)), + "summary": "Foreign Minister Mohammad Javad Zarif says the US " + "president should try showing Iranians some respect.", + "title": "Trump's 'genocidal taunts' will not end Iran - Zarif", + }, + { + "author": None, + "id": "https://www.bbc.co.uk/news/technology-48334739", + "link": "https://www.bbc.co.uk/news/technology-48334739", + "published": "Mon, 20 May 2019 12:19:19 GMT", + "published_parsed": struct_time((2019, 5, 20, 12, 19, 19, 0, 140, 0)), + "summary": "Google's move to end business ties with Huawei will " + "affect current devices and future purchases.", + "title": "Huawei's Android loss: How it affects you", + }, + ] +} + +mock_without_entries = {"entries": []} + +mock_with_update_entries = { + "entries": [ + { + "author": "A. Author", + "id": "28f79ae4-8f9a-11e9-b143-00163ef6bee7", + "link": "https://www.bbc.co.uk/news/world-us-canada-48338168", + "published": "Mon, 20 May 2019 16:07:37 GMT", + "published_parsed": struct_time((2019, 5, 20, 16, 7, 37, 0, 140, 0)), + "summary": "Foreign Minister Mohammad Javad Zarif says the US " + "president should try showing Iranians some respect.", + "title": "Trump's 'genocidal taunts' will not end Iran - Zarif", + }, + { + "author": "A. Author", + "id": "a5479c66-8fae-11e9-8422-00163ef6bee7", + "link": "https://www.bbc.co.uk/news/technology-48334739", + "published": "Mon, 20 May 2019 12:19:19 GMT", + "published_parsed": struct_time((2019, 5, 20, 12, 19, 19, 0, 140, 0)), + "summary": "Google's move to end business ties with Huawei will " + "affect current devices and future purchases.", + "title": "Huawei's Android loss: How it affects you", + }, + { + "author": "A. Author", + "id": "https://www.bbc.co.uk/news/uk-england-birmingham-48339080", + "link": "https://www.bbc.co.uk/news/uk-england-birmingham-48339080", + "published": "Mon, 20 May 2019 16:32:38 GMT", + "published_parsed": struct_time((2019, 5, 20, 16, 32, 38, 0, 140, 0)), + "summary": "Police are investigating the messages while an MP " + 'calls for a protest exclusion zone "to protect ' + 'children".', + "title": "Birmingham head teacher threatened over LGBT lessons", + }, + ] +} + +mock_with_html = { + "entries": [ + { + "author": "A. Author", + "id": "https://www.bbc.co.uk/news/world-us-canada-48338168", + "link": "https://www.bbc.co.uk/news/world-us-canada-48338168", + "published": "Mon, 20 May 2019 16:07:37 GMT", + "published_parsed": struct_time((2019, 5, 20, 16, 7, 37, 0, 140, 0)), + "summary": html_summary, + "title": "Trump's 'genocidal taunts' will not end Iran - Zarif", + } + ] +} + +mock_with_long_author = { + "entries": [ + { + "author": "A. Author but this author name is way to long for an actual surname.", + "id": "https://www.bbc.co.uk/news/world-us-canada-48338168", + "link": "https://www.bbc.co.uk/news/world-us-canada-48338168", + "published": "Mon, 20 May 2019 16:07:37 GMT", + "published_parsed": struct_time((2019, 5, 20, 16, 7, 37, 0, 140, 0)), + "summary": "Foreign Minister Mohammad Javad Zarif says the US " + "president should try showing Iranians some respect.", + "title": "Trump's 'genocidal taunts' will not end Iran - Zarif", + } + ] +} + +mock_with_long_title = { + "entries": [ + { + "author": "A. Author", + "id": "https://www.bbc.co.uk/news/world-us-canada-48338168", + "link": "https://www.bbc.co.uk/news/world-us-canada-48338168", + "published": "Mon, 20 May 2019 16:07:37 GMT", + "published_parsed": struct_time((2019, 5, 20, 16, 7, 37, 0, 140, 0)), + "summary": "Foreign Minister Mohammad Javad Zarif says the US " + "president should try showing Iranians some respect.", + "title": "Trump's 'genocidal taunts' will not end Iran - Zarif" + "Trump's 'genocidal taunts' will not end Iran - Zarif" + "Trump's 'genocidal taunts' will not end Iran - Zarif" + "Trump's 'genocidal taunts' will not end Iran - Zarif" + "Trump's 'genocidal taunts' will not end Iran - Zarif" + "Trump's 'genocidal taunts' will not end Iran - Zarif", + } + ] +} + +mock_with_longer_content_detail = { + "entries": [ + { + "author": "A. Author", + "id": "https://www.bbc.co.uk/news/world-us-canada-48338168", + "link": "https://www.bbc.co.uk/news/world-us-canada-48338168", + "published": "Mon, 20 May 2019 16:07:37 GMT", + "published_parsed": struct_time((2019, 5, 20, 16, 7, 37, 0, 140, 0)), + "summary": "Foreign Minister Mohammad Javad Zarif says the US " + "president should try showing Iranians some respect.", + "title": "Trump's 'genocidal taunts' will not end Iran - Zarif", + "content": [ + { + "base": "", + "language": None, + "type": "text/html", + "value": '
    \n' + '

    Enlarge / Ajit Pai, chairman ' + "of the Federal Communications Commission, " + "during an interview in New York, on " + "Tuesday, Nov. 5, 2019. (credit: Getty ' + "Images | Bloomberg)

    " + "
    ", + } + ], + } + ] +} + +mock_with_shorter_content_detail = { + "entries": [ + { + "author": "A. Author", + "id": "https://www.bbc.co.uk/news/world-us-canada-48338168", + "link": "https://www.bbc.co.uk/news/world-us-canada-48338168", + "published": "Mon, 20 May 2019 16:07:37 GMT", + "published_parsed": struct_time((2019, 5, 20, 16, 7, 37, 0, 140, 0)), + "summary": "Foreign Minister Mohammad Javad Zarif says the US " + "president should try showing Iranians some respect.", + "title": "Trump's 'genocidal taunts' will not end Iran - Zarif", + "content": [ + { + "base": "", + "language": None, + "type": "text/html", + "value": '
    ', + } + ], + } + ] +} + +mock_with_multiple_content_detail = { + "entries": [ + { + "author": "A. Author", + "id": "https://www.bbc.co.uk/news/world-us-canada-48338168", + "link": "https://www.bbc.co.uk/news/world-us-canada-48338168", + "published": "Mon, 20 May 2019 16:07:37 GMT", + "published_parsed": struct_time((2019, 5, 20, 16, 7, 37, 0, 140, 0)), + "summary": "Foreign Min", + "title": "Trump's 'genocidal taunts' will not end Iran - Zarif", + "content": [ + {"base": "", "language": None, "type": "text/html", "value": "Yippie"}, + {"base": "", "language": None, "type": "text/html", "value": "Ya"}, + {"base": "", "language": None, "type": "text/html", "value": "Yee"}, + ], + } + ] +} diff --git a/src/newsreader/news/collection/tests/feed/builder/tests.py b/src/newsreader/news/collection/tests/feed/builder/tests.py new file mode 100644 index 0000000..be13908 --- /dev/null +++ b/src/newsreader/news/collection/tests/feed/builder/tests.py @@ -0,0 +1,373 @@ +from datetime import date, datetime, time +from unittest.mock import MagicMock + +from django.test import TestCase +from django.utils import timezone + +import pytz + +from freezegun import freeze_time + +from newsreader.news.collection.feed import FeedBuilder +from newsreader.news.collection.tests.factories import CollectionRuleFactory +from newsreader.news.core.models import Post +from newsreader.news.core.tests.factories import PostFactory + +from .mocks import * + + +class FeedBuilderTestCase(TestCase): + def setUp(self): + self.maxDiff = None + + def test_basic_entry(self): + builder = FeedBuilder + rule = CollectionRuleFactory() + mock_stream = MagicMock(rule=rule) + + with builder((simple_mock, mock_stream)) as builder: + builder.save() + + post = Post.objects.get() + + d = datetime.combine(date(2019, 5, 20), time(hour=16, minute=7, second=37)) + aware_date = pytz.utc.localize(d) + + self.assertEquals(post.publication_date, aware_date) + self.assertEquals(Post.objects.count(), 1) + + self.assertEquals( + post.remote_identifier, + "https://www.bbc.co.uk/news/world-us-canada-48338168", + ) + + self.assertEquals( + post.url, "https://www.bbc.co.uk/news/world-us-canada-48338168" + ) + + self.assertEquals( + post.title, "Trump's 'genocidal taunts' will not end Iran - Zarif" + ) + + def test_multiple_entries(self): + builder = FeedBuilder + rule = CollectionRuleFactory() + mock_stream = MagicMock(rule=rule) + + with builder((multiple_mock, mock_stream)) as builder: + builder.save() + + posts = Post.objects.order_by("id") + self.assertEquals(Post.objects.count(), 3) + + first_post = posts[0] + second_post = posts[1] + + d = datetime.combine(date(2019, 5, 20), time(hour=16, minute=7, second=37)) + aware_date = pytz.utc.localize(d) + + self.assertEquals(first_post.publication_date, aware_date) + + self.assertEquals( + first_post.remote_identifier, + "https://www.bbc.co.uk/news/world-us-canada-48338168", + ) + + self.assertEquals( + first_post.url, "https://www.bbc.co.uk/news/world-us-canada-48338168" + ) + + self.assertEquals( + first_post.title, "Trump's 'genocidal taunts' will not end Iran - Zarif" + ) + + d = datetime.combine(date(2019, 5, 20), time(hour=12, minute=19, second=19)) + aware_date = pytz.utc.localize(d) + + self.assertEquals(second_post.publication_date, aware_date) + + self.assertEquals( + second_post.remote_identifier, + "https://www.bbc.co.uk/news/technology-48334739", + ) + + self.assertEquals( + second_post.url, "https://www.bbc.co.uk/news/technology-48334739" + ) + + self.assertEquals( + second_post.title, "Huawei's Android loss: How it affects you" + ) + + def test_entry_without_remote_identifier(self): + builder = FeedBuilder + rule = CollectionRuleFactory() + mock_stream = MagicMock(rule=rule) + + with builder((mock_without_identifier, mock_stream)) as builder: + builder.save() + + posts = Post.objects.order_by("id") + self.assertEquals(Post.objects.count(), 2) + + first_post = posts[0] + + d = datetime.combine(date(2019, 5, 20), time(hour=16, minute=7, second=37)) + aware_date = pytz.utc.localize(d) + + self.assertEquals(first_post.publication_date, aware_date) + + self.assertEquals(first_post.remote_identifier, None) + + self.assertEquals( + first_post.url, "https://www.bbc.co.uk/news/world-us-canada-48338168" + ) + + self.assertEquals( + first_post.title, "Trump's 'genocidal taunts' will not end Iran - Zarif" + ) + + @freeze_time("2019-10-30 12:30:00") + def test_entry_without_publication_date(self): + builder = FeedBuilder + rule = CollectionRuleFactory() + mock_stream = MagicMock(rule=rule) + + with builder((mock_without_publish_date, mock_stream)) as builder: + builder.save() + + posts = Post.objects.order_by("id") + self.assertEquals(Post.objects.count(), 2) + + first_post = posts[0] + second_post = posts[1] + + self.assertEquals(first_post.created, timezone.now()) + self.assertEquals( + first_post.remote_identifier, + "https://www.bbc.co.uk/news/world-us-canada-48338168", + ) + + self.assertEquals(second_post.created, timezone.now()) + self.assertEquals( + second_post.remote_identifier, + "https://www.bbc.co.uk/news/technology-48334739", + ) + + @freeze_time("2019-10-30 12:30:00") + def test_entry_without_url(self): + builder = FeedBuilder + rule = CollectionRuleFactory() + mock_stream = MagicMock(rule=rule) + + with builder((mock_without_url, mock_stream)) as builder: + builder.save() + + posts = Post.objects.order_by("id") + self.assertEquals(Post.objects.count(), 2) + + first_post = posts[0] + second_post = posts[1] + + self.assertEquals(first_post.created, timezone.now()) + self.assertEquals( + first_post.remote_identifier, + "https://www.bbc.co.uk/news/world-us-canada-48338168", + ) + + self.assertEquals(second_post.created, timezone.now()) + self.assertEquals( + second_post.remote_identifier, + "https://www.bbc.co.uk/news/technology-48334739", + ) + + @freeze_time("2019-10-30 12:30:00") + def test_entry_without_body(self): + builder = FeedBuilder + rule = CollectionRuleFactory() + mock_stream = MagicMock(rule=rule) + + with builder((mock_without_body, mock_stream)) as builder: + builder.save() + + posts = Post.objects.order_by("id") + self.assertEquals(Post.objects.count(), 2) + + first_post = posts[0] + second_post = posts[1] + + self.assertEquals(first_post.created, timezone.now()) + self.assertEquals( + first_post.remote_identifier, + "https://www.bbc.co.uk/news/world-us-canada-48338168", + ) + + self.assertEquals(second_post.created, timezone.now()) + self.assertEquals( + second_post.remote_identifier, + "https://www.bbc.co.uk/news/uk-england-birmingham-48339080", + ) + + @freeze_time("2019-10-30 12:30:00") + def test_entry_without_author(self): + builder = FeedBuilder + rule = CollectionRuleFactory() + mock_stream = MagicMock(rule=rule) + + with builder((mock_without_author, mock_stream)) as builder: + builder.save() + + posts = Post.objects.order_by("id") + self.assertEquals(Post.objects.count(), 2) + + first_post = posts[0] + second_post = posts[1] + + self.assertEquals(first_post.created, timezone.now()) + self.assertEquals( + first_post.remote_identifier, + "https://www.bbc.co.uk/news/world-us-canada-48338168", + ) + + self.assertEquals(second_post.created, timezone.now()) + self.assertEquals( + second_post.remote_identifier, + "https://www.bbc.co.uk/news/technology-48334739", + ) + + def test_empty_entries(self): + builder = FeedBuilder + rule = CollectionRuleFactory() + mock_stream = MagicMock(rule=rule) + + with builder((mock_without_entries, mock_stream)) as builder: + builder.save() + + self.assertEquals(Post.objects.count(), 0) + + def test_update_entries(self): + builder = FeedBuilder + rule = CollectionRuleFactory() + mock_stream = MagicMock(rule=rule) + + existing_first_post = PostFactory.create( + remote_identifier="28f79ae4-8f9a-11e9-b143-00163ef6bee7", rule=rule + ) + + existing_second_post = PostFactory.create( + remote_identifier="a5479c66-8fae-11e9-8422-00163ef6bee7", rule=rule + ) + + with builder((mock_with_update_entries, mock_stream)) as builder: + builder.save() + + self.assertEquals(Post.objects.count(), 3) + + existing_first_post.refresh_from_db() + existing_second_post.refresh_from_db() + + self.assertEquals( + existing_first_post.title, + "Trump's 'genocidal taunts' will not end Iran - Zarif", + ) + + self.assertEquals( + existing_second_post.title, "Huawei's Android loss: How it affects you" + ) + + def test_html_sanitizing(self): + builder = FeedBuilder + rule = CollectionRuleFactory() + mock_stream = MagicMock(rule=rule) + + with builder((mock_with_html, mock_stream)) as builder: + builder.save() + + post = Post.objects.get() + + self.assertEquals(Post.objects.count(), 1) + + self.assertTrue("
    " in post.body) + self.assertTrue("

    " in post.body) + self.assertTrue("" in post.body) + self.assertTrue('' in post.body) + self.assertTrue("

    " in post.body) + + self.assertTrue("" not in post.body) + self.assertTrue(" + + {{ block.super }} +{% endblock %} diff --git a/src/newsreader/news/core/templates/core/category-create.html b/src/newsreader/news/core/templates/core/category-create.html new file mode 100644 index 0000000..73d05b5 --- /dev/null +++ b/src/newsreader/news/core/templates/core/category-create.html @@ -0,0 +1,9 @@ +{% extends "core/category.html" %} + +{% block form-header %} +

    Create a category

    +{% endblock %} + +{% block confirm-button %} + +{% endblock %} diff --git a/src/newsreader/news/core/templates/core/category-update.html b/src/newsreader/news/core/templates/core/category-update.html new file mode 100644 index 0000000..3e50df9 --- /dev/null +++ b/src/newsreader/news/core/templates/core/category-update.html @@ -0,0 +1,9 @@ +{% extends "core/category.html" %} + +{% block form-header %} +

    Update category

    +{% endblock %} + +{% block confirm-button %} + +{% endblock %} diff --git a/src/newsreader/news/core/templates/core/category.html b/src/newsreader/news/core/templates/core/category.html new file mode 100644 index 0000000..0771345 --- /dev/null +++ b/src/newsreader/news/core/templates/core/category.html @@ -0,0 +1,62 @@ +{% extends "base.html" %} + +{% load static %} + +{% block content %} +
    +
    + {% csrf_token %} + +
    + {% block form-header %}{% endblock %} +
    + + {{ form.non_field_errors }} + {{ form.user.errors }} + {{ form.user }} + +
    +
    + + {{ form.name.errors }} + {{ form.name }} +
    +
    + +
    +
    + + + Note that existing assigned rules will be reassigned to this category + + {{ form.rules.errors }} + +
      + {% for rule in rules %} +
    • + + + {% if rule.favicon %} + + {% else %} + + {% endif %} + + {{ rule.name }} +
    • + {% endfor %} +
    +
    +
    + +
    +
    + Cancel + {% block confirm-button %}{% endblock %} +
    +
    +
    +
    +{% endblock %} diff --git a/src/newsreader/news/core/templates/core/homepage.html b/src/newsreader/news/core/templates/core/homepage.html new file mode 100644 index 0000000..8904517 --- /dev/null +++ b/src/newsreader/news/core/templates/core/homepage.html @@ -0,0 +1,7 @@ +{% extends "base.html" %} + +{% load static %} + +{% block content %} +
    +{% endblock %} diff --git a/src/newsreader/news/core/tests/__init__.py b/src/newsreader/news/core/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/newsreader/news/core/tests/endpoints/__init__.py b/src/newsreader/news/core/tests/endpoints/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/newsreader/news/core/tests/endpoints/category/__init__.py b/src/newsreader/news/core/tests/endpoints/category/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/newsreader/news/core/tests/endpoints/category/detail/__init__.py b/src/newsreader/news/core/tests/endpoints/category/detail/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/newsreader/news/core/tests/endpoints/category/detail/tests.py b/src/newsreader/news/core/tests/endpoints/category/detail/tests.py new file mode 100644 index 0000000..2bd6bcb --- /dev/null +++ b/src/newsreader/news/core/tests/endpoints/category/detail/tests.py @@ -0,0 +1,215 @@ +import json + +from django.test import TestCase +from django.urls import reverse + +from newsreader.accounts.tests.factories import UserFactory +from newsreader.news.collection.tests.factories import CollectionRuleFactory +from newsreader.news.core.tests.factories import CategoryFactory, PostFactory + + +class CategoryDetailViewTestCase(TestCase): + def setUp(self): + self.user = UserFactory(password="test") + self.client.force_login(self.user) + + def test_simple(self): + category = CategoryFactory(user=self.user) + + response = self.client.get(reverse("api:categories-detail", args=[category.pk])) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertTrue("id" in data) + self.assertTrue("name" in data) + + def test_not_known(self): + response = self.client.get(reverse("api:categories-detail", args=[100])) + data = response.json() + + self.assertEquals(response.status_code, 404) + self.assertEquals(data["detail"], "Not found.") + + def test_post(self): + category = CategoryFactory(user=self.user) + + response = self.client.post( + reverse("api:categories-detail", args=[category.pk]) + ) + data = response.json() + + self.assertEquals(response.status_code, 405) + self.assertEquals(data["detail"], 'Method "POST" not allowed.') + + def test_patch(self): + category = CategoryFactory(name="Clickbait", user=self.user) + + response = self.client.patch( + reverse("api:categories-detail", args=[category.pk]), + data=json.dumps({"name": "Interesting posts"}), + content_type="application/json", + ) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertEquals(data["name"], "Interesting posts") + + def test_identifier_cannot_be_changed(self): + category = CategoryFactory(user=self.user) + + response = self.client.patch( + reverse("api:categories-detail", args=[category.pk]), + data=json.dumps({"id": 44}), + content_type="application/json", + ) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertEquals(data["id"], category.pk) + + def test_put(self): + category = CategoryFactory(name="Clickbait", user=self.user) + + response = self.client.put( + reverse("api:categories-detail", args=[category.pk]), + data=json.dumps({"name": "Interesting posts"}), + content_type="application/json", + ) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertEquals(data["name"], "Interesting posts") + + def test_delete(self): + category = CategoryFactory(user=self.user) + + response = self.client.delete( + reverse("api:categories-detail", args=[category.pk]) + ) + + self.assertEquals(response.status_code, 204) + + def test_category_with_unauthenticated_user(self): + self.client.logout() + + category = CategoryFactory(user=self.user) + + response = self.client.get(reverse("api:categories-detail", args=[category.pk])) + + self.assertEquals(response.status_code, 403) + + def test_category_with_unauthorized_user(self): + other_user = UserFactory() + category = CategoryFactory(user=other_user) + + response = self.client.get(reverse("api:categories-detail", args=[category.pk])) + + self.assertEquals(response.status_code, 403) + + def test_read_count(self): + category = CategoryFactory(user=self.user) + unread_rule = CollectionRuleFactory(category=category) + read_rule = CollectionRuleFactory(category=category) + + PostFactory.create_batch(size=20, read=False, rule=unread_rule) + PostFactory.create_batch(size=20, read=True, rule=read_rule) + + response = self.client.get(reverse("api:categories-detail", args=[category.pk])) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertEquals(data["unread"], 20) + + +class CategoryReadTestCase(TestCase): + def setUp(self): + self.user = UserFactory(password="test") + self.client.force_login(self.user) + + def test_category_read(self): + category = CategoryFactory(user=self.user) + rules = [ + PostFactory.create_batch(size=5, read=False, rule=rule) + for rule in CollectionRuleFactory.create_batch(size=5, category=category) + ] + + response = self.client.post(reverse("api:categories-read", args=[category.pk])) + + data = response.json() + + self.assertEquals(response.status_code, 201) + self.assertEquals(data["unread"], 0) + self.assertEquals(data["id"], category.pk) + + def test_category_unknown(self): + response = self.client.post(reverse("api:categories-read", args=[101])) + + self.assertEquals(response.status_code, 404) + + def test_unauthenticated_user(self): + self.client.logout() + + category = CategoryFactory(user=self.user) + rules = [ + PostFactory.create_batch(size=5, read=False, rule=rule) + for rule in CollectionRuleFactory.create_batch( + size=5, category=category, user=self.user + ) + ] + + response = self.client.post(reverse("api:categories-read", args=[category.pk])) + + self.assertEquals(response.status_code, 403) + + def test_unauthorized_user(self): + other_user = UserFactory() + category = CategoryFactory(user=other_user) + + rules = [ + PostFactory.create_batch(size=5, read=False, rule=rule) + for rule in CollectionRuleFactory.create_batch( + size=5, category=category, user=other_user + ) + ] + + response = self.client.post(reverse("api:categories-read", args=[category.pk])) + + self.assertEquals(response.status_code, 403) + + def test_get(self): + category = CategoryFactory(name="Clickbait", user=self.user) + + response = self.client.get(reverse("api:categories-read", args=[category.pk])) + + self.assertEquals(response.status_code, 405) + + def test_patch(self): + category = CategoryFactory(name="Clickbait", user=self.user) + + response = self.client.patch( + reverse("api:categories-read", args=[category.pk]), + data=json.dumps({"name": "Not possible"}), + content_type="application/json", + ) + + self.assertEquals(response.status_code, 405) + + def test_put(self): + category = CategoryFactory(name="Clickbait", user=self.user) + + response = self.client.put( + reverse("api:categories-read", args=[category.pk]), + data=json.dumps({"name": "Not possible"}), + content_type="application/json", + ) + + self.assertEquals(response.status_code, 405) + + def test_delete(self): + category = CategoryFactory(name="Clickbait", user=self.user) + + response = self.client.delete( + reverse("api:categories-read", args=[category.pk]) + ) + + self.assertEquals(response.status_code, 405) diff --git a/src/newsreader/news/core/tests/endpoints/category/list/__init__.py b/src/newsreader/news/core/tests/endpoints/category/list/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/newsreader/news/core/tests/endpoints/category/list/tests.py b/src/newsreader/news/core/tests/endpoints/category/list/tests.py new file mode 100644 index 0000000..d44f204 --- /dev/null +++ b/src/newsreader/news/core/tests/endpoints/category/list/tests.py @@ -0,0 +1,568 @@ +import json + +from datetime import date, datetime, time + +from django.test import TestCase +from django.urls import reverse + +import pytz + +from newsreader.accounts.tests.factories import UserFactory +from newsreader.news.collection.tests.factories import CollectionRuleFactory +from newsreader.news.core.tests.factories import CategoryFactory, PostFactory + + +class CategoryListViewTestCase(TestCase): + def setUp(self): + self.user = UserFactory(password="test") + self.client.force_login(self.user) + + def test_simple(self): + CategoryFactory.create_batch(size=3, user=self.user) + + response = self.client.get(reverse("api:categories-list")) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertEquals(len(data), 3) + + def test_ordering(self): + categories = [ + CategoryFactory( + created=datetime.combine( + date(2019, 5, 20), time(hour=16, minute=7, second=37), pytz.utc + ), + user=self.user, + ), + CategoryFactory( + created=datetime.combine( + date(2019, 7, 20), time(hour=18, minute=7, second=37), pytz.utc + ), + user=self.user, + ), + CategoryFactory( + created=datetime.combine( + date(2019, 7, 20), time(hour=16, minute=7, second=37), pytz.utc + ), + user=self.user, + ), + ] + + response = self.client.get(reverse("api:categories-list")) + data = response.json() + + self.assertEquals(response.status_code, 200) + + self.assertEquals(data[0]["id"], categories[1].pk) + self.assertEquals(data[1]["id"], categories[2].pk) + self.assertEquals(data[2]["id"], categories[0].pk) + + def test_empty(self): + response = self.client.get(reverse("api:categories-list")) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertEquals(len(data), 0) + + def test_post(self): + data = {"name": "Tech"} + + response = self.client.post( + reverse("api:categories-list"), + data=json.dumps(data), + content_type="application/json", + ) + response_data = response.json() + + self.assertEquals(response.status_code, 405) + self.assertEquals(response_data["detail"], 'Method "POST" not allowed.') + + def test_patch(self): + response = self.client.patch(reverse("api:categories-list")) + data = response.json() + + self.assertEquals(response.status_code, 405) + self.assertEquals(data["detail"], 'Method "PATCH" not allowed.') + + def test_put(self): + response = self.client.put(reverse("api:categories-list")) + data = response.json() + + self.assertEquals(response.status_code, 405) + self.assertEquals(data["detail"], 'Method "PUT" not allowed.') + + def test_delete(self): + response = self.client.delete(reverse("api:categories-list")) + data = response.json() + + self.assertEquals(response.status_code, 405) + self.assertEquals(data["detail"], 'Method "DELETE" not allowed.') + + def test_categories_with_unauthenticated_user(self): + self.client.logout() + + CategoryFactory.create_batch(size=3, user=self.user) + + response = self.client.get(reverse("api:categories-list")) + + self.assertEquals(response.status_code, 403) + + def test_categories_with_unauthorized_user(self): + other_user = UserFactory() + CategoryFactory.create_batch(size=3, user=other_user) + + response = self.client.get(reverse("api:categories-list")) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertEquals(len(data), 0) + + +class NestedCategoryListViewTestCase(TestCase): + def setUp(self): + self.user = UserFactory(password="test") + self.client.force_login(self.user) + + def test_simple(self): + category = CategoryFactory.create(user=self.user) + rules = CollectionRuleFactory.create_batch(size=5, category=category) + + response = self.client.get( + reverse("api:categories-nested-rules", kwargs={"pk": category.pk}) + ) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertEquals(len(data), 5) + + self.assertTrue("id" in data[0]) + self.assertTrue("name" in data[0]) + self.assertTrue("category" in data[0]) + self.assertTrue("url" in data[0]) + self.assertTrue("favicon" in data[0]) + + def test_empty(self): + category = CategoryFactory.create(user=self.user) + + response = self.client.get( + reverse("api:categories-nested-rules", kwargs={"pk": category.pk}) + ) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertEquals(len(data), 0) + self.assertEquals(data, []) + + def test_not_known(self): + response = self.client.get( + reverse("api:categories-nested-rules", kwargs={"pk": 100}) + ) + + self.assertEquals(response.status_code, 404) + + def test_post(self): + response = self.client.post( + reverse("api:categories-nested-rules", kwargs={"pk": 100}), + data=json.dumps({}), + content_type="application/json", + ) + data = response.json() + + self.assertEquals(response.status_code, 405) + self.assertEquals(data["detail"], 'Method "POST" not allowed.') + + def test_patch(self): + category = CategoryFactory.create(user=self.user) + + response = self.client.patch( + reverse("api:categories-nested-rules", kwargs={"pk": category.pk}), + data=json.dumps({"name": "test"}), + content_type="application/json", + ) + data = response.json() + + self.assertEquals(response.status_code, 405) + self.assertEquals(data["detail"], 'Method "PATCH" not allowed.') + + def test_put(self): + category = CategoryFactory.create(user=self.user) + + response = self.client.put( + reverse("api:categories-nested-rules", kwargs={"pk": category.pk}), + data=json.dumps({"name": "test"}), + content_type="application/json", + ) + data = response.json() + + self.assertEquals(response.status_code, 405) + self.assertEquals(data["detail"], 'Method "PUT" not allowed.') + + def test_delete(self): + category = CategoryFactory.create(user=self.user) + + response = self.client.delete( + reverse("api:categories-nested-rules", kwargs={"pk": category.pk}), + content_type="application/json", + ) + data = response.json() + + self.assertEquals(response.status_code, 405) + self.assertEquals(data["detail"], 'Method "DELETE" not allowed.') + + def test_with_unauthenticated_user(self): + self.client.logout() + + category = CategoryFactory.create(user=self.user) + rules = CollectionRuleFactory.create_batch(size=5, category=category) + + response = self.client.get( + reverse("api:categories-nested-rules", kwargs={"pk": category.pk}) + ) + + self.assertEquals(response.status_code, 403) + + def test_with_unauthorized_user(self): + other_user = UserFactory.create() + + category = CategoryFactory.create(user=other_user) + rules = CollectionRuleFactory.create_batch(size=5, category=category) + + response = self.client.get( + reverse("api:categories-nested-rules", kwargs={"pk": category.pk}) + ) + + self.assertEquals(response.status_code, 403) + + def test_ordering(self): + category = CategoryFactory.create(user=self.user) + rules = [ + CollectionRuleFactory.create(category=category, name="Durp"), + CollectionRuleFactory.create(category=category, name="Slurp"), + CollectionRuleFactory.create(category=category, name="Burp"), + ] + + response = self.client.get( + reverse("api:categories-nested-rules", kwargs={"pk": category.pk}) + ) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertEquals(len(data), 3) + + self.assertEquals(data[0]["id"], rules[2].pk) + self.assertEquals(data[1]["id"], rules[0].pk) + self.assertEquals(data[2]["id"], rules[1].pk) + + def test_only_rules_from_category_are_returned(self): + other_category = CategoryFactory(user=self.user) + CollectionRuleFactory.create_batch(size=5, category=other_category) + + category = CategoryFactory.create(user=self.user) + rules = [ + CollectionRuleFactory.create(category=category, name="Durp"), + CollectionRuleFactory.create(category=category, name="Slurp"), + CollectionRuleFactory.create(category=category, name="Burp"), + ] + + response = self.client.get( + reverse("api:categories-nested-rules", kwargs={"pk": category.pk}) + ) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertEquals(len(data), 3) + + self.assertEquals(data[0]["id"], rules[2].pk) + self.assertEquals(data[1]["id"], rules[0].pk) + self.assertEquals(data[2]["id"], rules[1].pk) + + +class NestedCategoryPostView(TestCase): + def setUp(self): + self.user = UserFactory(password="test") + self.client.force_login(self.user) + + def test_simple(self): + category = CategoryFactory.create(user=self.user) + rules = { + rule.pk: PostFactory.create_batch(size=5, rule=rule) + for rule in CollectionRuleFactory.create_batch( + size=5, category=category, user=self.user + ) + } + + response = self.client.get( + reverse("api:categories-nested-posts", kwargs={"pk": category.pk}) + ) + data = response.json() + posts = data["results"] + + self.assertEquals(response.status_code, 200) + self.assertEquals(data["count"], 25) + + self.assertTrue("id" in posts[0]) + self.assertTrue("title" in posts[0]) + self.assertTrue("body" in posts[0]) + self.assertTrue("rule" in posts[0]) + self.assertTrue("url" in posts[0]) + + def test_no_rules(self): + category = CategoryFactory.create(user=self.user) + + response = self.client.get( + reverse("api:categories-nested-posts", kwargs={"pk": category.pk}) + ) + data = response.json() + posts = data["results"] + + self.assertEquals(response.status_code, 200) + self.assertEquals(data["count"], 0) + self.assertEquals(posts, []) + + def test_no_posts(self): + category = CategoryFactory.create(user=self.user) + rules = CollectionRuleFactory.create_batch( + size=5, user=self.user, category=category + ) + + response = self.client.get( + reverse("api:categories-nested-posts", kwargs={"pk": category.pk}) + ) + data = response.json() + posts = data["results"] + + self.assertEquals(response.status_code, 200) + self.assertEquals(data["count"], 0) + self.assertEquals(posts, []) + + def test_not_known(self): + response = self.client.get( + reverse("api:categories-nested-posts", kwargs={"pk": 100}) + ) + + self.assertEquals(response.status_code, 404) + + def test_post(self): + response = self.client.post( + reverse("api:categories-nested-posts", kwargs={"pk": 100}), + data=json.dumps({}), + content_type="application/json", + ) + data = response.json() + + self.assertEquals(response.status_code, 405) + self.assertEquals(data["detail"], 'Method "POST" not allowed.') + + def test_patch(self): + category = CategoryFactory.create(user=self.user) + + response = self.client.patch( + reverse("api:categories-nested-posts", kwargs={"pk": category.pk}), + data=json.dumps({}), + content_type="application/json", + ) + data = response.json() + + self.assertEquals(response.status_code, 405) + self.assertEquals(data["detail"], 'Method "PATCH" not allowed.') + + def test_put(self): + category = CategoryFactory.create(user=self.user) + + response = self.client.put( + reverse("api:categories-nested-posts", kwargs={"pk": category.pk}), + data=json.dumps({}), + content_type="application/json", + ) + data = response.json() + + self.assertEquals(response.status_code, 405) + self.assertEquals(data["detail"], 'Method "PUT" not allowed.') + + def test_delete(self): + category = CategoryFactory.create(user=self.user) + + response = self.client.delete( + reverse("api:categories-nested-posts", kwargs={"pk": category.pk}), + content_type="application/json", + ) + data = response.json() + + self.assertEquals(response.status_code, 405) + self.assertEquals(data["detail"], 'Method "DELETE" not allowed.') + + def test_with_unauthenticated_user(self): + self.client.logout() + + category = CategoryFactory.create(user=self.user) + + response = self.client.get( + reverse("api:categories-nested-posts", kwargs={"pk": category.pk}) + ) + + self.assertEquals(response.status_code, 403) + + def test_with_unauthorized_user(self): + other_user = UserFactory.create() + category = CategoryFactory.create(user=other_user) + + response = self.client.get( + reverse("api:categories-nested-posts", kwargs={"pk": category.pk}) + ) + + self.assertEquals(response.status_code, 403) + + def test_ordering(self): + category = CategoryFactory.create(user=self.user) + + bbc_rule = CollectionRuleFactory.create( + name="BBC", category=category, user=self.user + ) + guardian_rule = CollectionRuleFactory.create( + name="The Guardian", category=category, user=self.user + ) + reuters_rule = CollectionRuleFactory.create( + name="Reuters", category=category, user=self.user + ) + + reuters_rule = [ + PostFactory.create( + title="Second Reuters post", + rule=reuters_rule, + publication_date=datetime.combine( + date(2019, 5, 21), time(hour=16, minute=7, second=37), pytz.utc + ), + ), + PostFactory.create( + title="First Reuters post", + rule=reuters_rule, + publication_date=datetime.combine( + date(2019, 5, 20), time(hour=16, minute=7, second=37), pytz.utc + ), + ), + ] + + guardian_posts = [ + PostFactory.create( + title="Second Guardian post", + rule=guardian_rule, + publication_date=datetime.combine( + date(2019, 5, 21), time(hour=16, minute=7, second=37), pytz.utc + ), + ), + PostFactory.create( + title="First Guardian post", + rule=guardian_rule, + publication_date=datetime.combine( + date(2019, 5, 20), time(hour=16, minute=7, second=37), pytz.utc + ), + ), + ] + + bbc_posts = [ + PostFactory.create( + title="Second BBC post", + rule=bbc_rule, + publication_date=datetime.combine( + date(2019, 5, 21), time(hour=16, minute=7, second=37), pytz.utc + ), + ), + PostFactory.create( + title="First BBC post", + rule=bbc_rule, + publication_date=datetime.combine( + date(2019, 5, 20), time(hour=16, minute=7, second=37), pytz.utc + ), + ), + ] + + response = self.client.get( + reverse("api:categories-nested-posts", kwargs={"pk": category.pk}) + ) + data = response.json() + posts = data["results"] + + self.assertEquals(response.status_code, 200) + self.assertEquals(data["count"], 6) + + self.assertEquals(posts[0]["title"], "Second BBC post") + self.assertEquals(posts[1]["title"], "First BBC post") + + self.assertEquals(posts[2]["title"], "Second Guardian post") + self.assertEquals(posts[3]["title"], "First Guardian post") + + self.assertEquals(posts[4]["title"], "Second Reuters post") + self.assertEquals(posts[5]["title"], "First Reuters post") + + def test_only_posts_from_category_are_returned(self): + category = CategoryFactory.create(user=self.user) + other_category = CategoryFactory.create(user=self.user) + + guardian_rule = CollectionRuleFactory.create( + name="BBC", category=category, user=self.user + ) + other_rule = CollectionRuleFactory.create(name="The Guardian", user=self.user) + + guardian_posts = [ + PostFactory.create(rule=guardian_rule), + PostFactory.create(rule=guardian_rule), + ] + + other_posts = [ + PostFactory.create(rule=other_rule), + PostFactory.create(rule=other_rule), + ] + + response = self.client.get( + reverse("api:categories-nested-posts", kwargs={"pk": category.pk}) + ) + data = response.json() + posts = data["results"] + + self.assertEquals(response.status_code, 200) + self.assertEquals(data["count"], 2) + + self.assertEquals(posts[0]["rule"], guardian_rule.pk) + self.assertEquals(posts[1]["rule"], guardian_rule.pk) + + def test_unread_posts(self): + category = CategoryFactory.create(user=self.user) + rule = CollectionRuleFactory(category=category) + + PostFactory.create_batch(size=10, rule=rule, read=False) + PostFactory.create_batch(size=10, rule=rule, read=True) + + response = self.client.get( + reverse("api:categories-nested-posts", kwargs={"pk": category.pk}), + {"read": "false"}, + ) + + data = response.json() + posts = data["results"] + + self.assertEquals(response.status_code, 200) + self.assertEquals(data["count"], 10) + + for post in posts: + self.assertEquals(post["read"], False) + + def test_read_posts(self): + category = CategoryFactory.create(user=self.user) + rule = CollectionRuleFactory(category=category) + + PostFactory.create_batch(size=20, rule=rule, read=False) + PostFactory.create_batch(size=10, rule=rule, read=True) + + response = self.client.get( + reverse("api:categories-nested-posts", kwargs={"pk": category.pk}), + {"read": "true"}, + ) + + data = response.json() + posts = data["results"] + + self.assertEquals(response.status_code, 200) + self.assertEquals(data["count"], 10) + + for post in posts: + self.assertEquals(post["read"], True) diff --git a/src/newsreader/news/core/tests/endpoints/post/__init__.py b/src/newsreader/news/core/tests/endpoints/post/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/newsreader/news/core/tests/endpoints/post/detail/__init__.py b/src/newsreader/news/core/tests/endpoints/post/detail/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/newsreader/news/core/tests/endpoints/post/detail/tests.py b/src/newsreader/news/core/tests/endpoints/post/detail/tests.py new file mode 100644 index 0000000..7c8c31e --- /dev/null +++ b/src/newsreader/news/core/tests/endpoints/post/detail/tests.py @@ -0,0 +1,218 @@ +import json + +from django.test import TestCase +from django.urls import reverse + +from newsreader.accounts.tests.factories import UserFactory +from newsreader.news.collection.tests.factories import CollectionRuleFactory +from newsreader.news.core.tests.factories import CategoryFactory, PostFactory + + +class PostDetailViewTestCase(TestCase): + def setUp(self): + self.user = UserFactory(password="test") + self.client.force_login(self.user) + + def test_simple(self): + rule = CollectionRuleFactory( + user=self.user, category=CategoryFactory(user=self.user) + ) + post = PostFactory(rule=rule) + + response = self.client.get(reverse("api:posts-detail", args=[post.pk])) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertEquals(data["id"], post.pk) + + self.assertTrue("title" in data) + self.assertTrue("body" in data) + self.assertTrue("author" in data) + self.assertTrue("publicationDate" in data) + self.assertTrue("url" in data) + self.assertTrue("rule" in data) + self.assertTrue("remoteIdentifier" in data) + + def test_not_known(self): + response = self.client.get(reverse("api:posts-detail", args=[100])) + data = response.json() + + self.assertEquals(response.status_code, 404) + self.assertEquals(data["detail"], "Not found.") + + def test_post(self): + rule = CollectionRuleFactory( + user=self.user, category=CategoryFactory(user=self.user) + ) + post = PostFactory(rule=rule) + + response = self.client.post(reverse("api:posts-detail", args=[post.pk])) + data = response.json() + + self.assertEquals(response.status_code, 405) + self.assertEquals(data["detail"], 'Method "POST" not allowed.') + + def test_patch(self): + rule = CollectionRuleFactory( + user=self.user, category=CategoryFactory(user=self.user) + ) + post = PostFactory(title="This is clickbait for sure", rule=rule) + + response = self.client.patch( + reverse("api:posts-detail", args=[post.pk]), + data=json.dumps({"title": "This title is very accurate"}), + content_type="application/json", + ) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertEquals(data["title"], "This title is very accurate") + + def test_identifier_cannot_be_changed(self): + rule = CollectionRuleFactory( + user=self.user, category=CategoryFactory(user=self.user) + ) + post = PostFactory(title="This is clickbait for sure", rule=rule) + + response = self.client.patch( + reverse("api:posts-detail", args=[post.pk]), + data=json.dumps({"id": 44}), + content_type="application/json", + ) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertEquals(data["id"], post.pk) + + def test_rule_cannot_be_changed(self): + rule = CollectionRuleFactory( + user=self.user, category=CategoryFactory(user=self.user) + ) + new_rule = CollectionRuleFactory( + user=self.user, category=CategoryFactory(user=self.user) + ) + post = PostFactory(title="This is clickbait for sure", rule=rule) + + response = self.client.patch( + reverse("api:posts-detail", args=[post.pk]), + data=json.dumps({"rule": reverse("api:rules-detail", args=[new_rule.pk])}), + content_type="application/json", + ) + data = response.json() + + self.assertEquals(response.status_code, 200) + + self.assertTrue(data["rule"], rule.pk) + + def test_put(self): + rule = CollectionRuleFactory( + user=self.user, category=CategoryFactory(user=self.user) + ) + post = PostFactory(title="This is clickbait for sure", rule=rule) + + response = self.client.put( + reverse("api:posts-detail", args=[post.pk]), + data=json.dumps({"title": "This title is very accurate"}), + content_type="application/json", + ) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertEquals(data["title"], "This title is very accurate") + + def test_delete(self): + rule = CollectionRuleFactory( + user=self.user, category=CategoryFactory(user=self.user) + ) + post = PostFactory(rule=rule) + + response = self.client.delete(reverse("api:posts-detail", args=[post.pk])) + data = response.json() + + self.assertEquals(response.status_code, 405) + self.assertEquals(data["detail"], 'Method "DELETE" not allowed.') + + def test_post_with_unauthenticated_user_without_category(self): + self.client.logout() + + rule = CollectionRuleFactory(user=self.user, category=None) + post = PostFactory(rule=rule) + + response = self.client.get(reverse("api:posts-detail", args=[post.pk])) + + self.assertEquals(response.status_code, 403) + + def test_post_with_unauthenticated_user_with_category(self): + self.client.logout() + + rule = CollectionRuleFactory( + user=self.user, category=CategoryFactory(user=self.user) + ) + post = PostFactory(rule=rule) + + response = self.client.get(reverse("api:posts-detail", args=[post.pk])) + + self.assertEquals(response.status_code, 403) + + def test_post_with_unauthorized_user_without_category(self): + other_user = UserFactory() + rule = CollectionRuleFactory(user=other_user, category=None) + post = PostFactory(rule=rule) + + response = self.client.get(reverse("api:posts-detail", args=[post.pk])) + + self.assertEquals(response.status_code, 403) + + def test_post_with_unauthorized_user_with_category(self): + other_user = UserFactory() + rule = CollectionRuleFactory( + user=other_user, category=CategoryFactory(user=other_user) + ) + post = PostFactory(rule=rule) + + response = self.client.get(reverse("api:posts-detail", args=[post.pk])) + + self.assertEquals(response.status_code, 403) + + def test_post_with_different_user_for_category_and_rule(self): + other_user = UserFactory() + rule = CollectionRuleFactory( + user=self.user, category=CategoryFactory(user=other_user) + ) + post = PostFactory(rule=rule) + + response = self.client.get(reverse("api:posts-detail", args=[post.pk])) + + self.assertEquals(response.status_code, 403) + + def test_mark_read(self): + rule = CollectionRuleFactory( + user=self.user, category=CategoryFactory(user=self.user) + ) + post = PostFactory(rule=rule, read=False) + + response = self.client.patch( + reverse("api:posts-detail", args=[post.pk]), + data=json.dumps({"read": True}), + content_type="application/json", + ) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertEquals(data["read"], True) + + def test_mark_unread(self): + rule = CollectionRuleFactory( + user=self.user, category=CategoryFactory(user=self.user) + ) + post = PostFactory(rule=rule, read=True) + + response = self.client.patch( + reverse("api:posts-detail", args=[post.pk]), + data=json.dumps({"read": False}), + content_type="application/json", + ) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertEquals(data["read"], False) diff --git a/src/newsreader/news/core/tests/endpoints/post/list/__init__.py b/src/newsreader/news/core/tests/endpoints/post/list/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/newsreader/news/core/tests/endpoints/post/list/tests.py b/src/newsreader/news/core/tests/endpoints/post/list/tests.py new file mode 100644 index 0000000..f3639bf --- /dev/null +++ b/src/newsreader/news/core/tests/endpoints/post/list/tests.py @@ -0,0 +1,242 @@ +from datetime import date, datetime, time + +from django.test import TestCase +from django.urls import reverse + +import pytz + +from newsreader.accounts.tests.factories import UserFactory +from newsreader.news.collection.tests.factories import CollectionRuleFactory +from newsreader.news.core.tests.factories import CategoryFactory, PostFactory + + +class PostListViewTestCase(TestCase): + def setUp(self): + self.user = UserFactory(is_staff=True, password="test") + self.client.force_login(self.user) + + def test_simple(self): + rule = CollectionRuleFactory( + user=self.user, category=CategoryFactory(user=self.user) + ) + PostFactory.create_batch(size=3, rule=rule) + + response = self.client.get(reverse("api:posts-list")) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertTrue("results" in data) + self.assertTrue("count" in data) + self.assertEquals(data["count"], 3) + + def test_ordering(self): + rule = CollectionRuleFactory( + user=self.user, category=CategoryFactory(user=self.user) + ) + + posts = [ + PostFactory( + title="I'm the first post", + rule=rule, + publication_date=datetime.combine( + date(2019, 5, 20), time(hour=16, minute=7, second=37), pytz.utc + ), + ), + PostFactory( + title="I'm the second post", + rule=rule, + publication_date=datetime.combine( + date(2019, 7, 20), time(hour=18, minute=7, second=37), pytz.utc + ), + ), + PostFactory( + title="I'm the third post", + rule=rule, + publication_date=datetime.combine( + date(2019, 7, 20), time(hour=16, minute=7, second=37), pytz.utc + ), + ), + ] + + response = self.client.get(reverse("api:posts-list")) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertTrue("results" in data) + self.assertTrue("count" in data) + self.assertEquals(data["count"], 3) + + self.assertEquals(data["results"][0]["id"], posts[1].pk) + self.assertEquals(data["results"][1]["id"], posts[2].pk) + self.assertEquals(data["results"][2]["id"], posts[0].pk) + + def test_pagination_count(self): + rule = CollectionRuleFactory( + user=self.user, category=CategoryFactory(user=self.user) + ) + PostFactory.create_batch(size=80, rule=rule) + page_size = 50 + + response = self.client.get(reverse("api:posts-list"), {"count": 50}) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertEquals(data["count"], 80) + self.assertEquals(len(data["results"]), page_size) + + def test_empty(self): + response = self.client.get(reverse("api:posts-list")) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertTrue("results" in data) + self.assertTrue("count" in data) + + self.assertEquals(data["count"], 0) + self.assertEquals(len(data["results"]), 0) + + def test_post(self): + response = self.client.post(reverse("api:posts-list")) + data = response.json() + + self.assertEquals(response.status_code, 405) + self.assertEquals(data["detail"], 'Method "POST" not allowed.') + + def test_patch(self): + response = self.client.patch(reverse("api:posts-list")) + data = response.json() + + self.assertEquals(response.status_code, 405) + self.assertEquals(data["detail"], 'Method "PATCH" not allowed.') + + def test_put(self): + response = self.client.put(reverse("api:posts-list")) + data = response.json() + + self.assertEquals(response.status_code, 405) + self.assertEquals(data["detail"], 'Method "PUT" not allowed.') + + def test_delete(self): + response = self.client.delete(reverse("api:posts-list")) + data = response.json() + + self.assertEquals(response.status_code, 405) + self.assertEquals(data["detail"], 'Method "DELETE" not allowed.') + + def test_posts_with_unauthenticated_user_without_category(self): + self.client.logout() + + PostFactory.create_batch(size=3, rule=CollectionRuleFactory(user=self.user)) + + response = self.client.get(reverse("api:posts-list")) + + self.assertEquals(response.status_code, 403) + + def test_posts_with_unauthenticated_user_with_category(self): + self.client.logout() + + category = CategoryFactory(user=self.user) + + PostFactory.create_batch( + size=3, rule=CollectionRuleFactory(user=self.user, category=category) + ) + + response = self.client.get(reverse("api:posts-list")) + + self.assertEquals(response.status_code, 403) + + def test_posts_with_unauthorized_user_without_category(self): + other_user = UserFactory() + + rule = CollectionRuleFactory(user=other_user, category=None) + PostFactory.create_batch(size=3, rule=rule) + + response = self.client.get(reverse("api:posts-list")) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertEquals(len(data["results"]), 0) + self.assertEquals(data["count"], 0) + + def test_posts_with_unauthorized_user_with_category(self): + other_user = UserFactory() + category = CategoryFactory(user=other_user) + + PostFactory.create_batch( + size=3, rule=CollectionRuleFactory(user=other_user, category=category) + ) + + response = self.client.get(reverse("api:posts-list")) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertEquals(len(data["results"]), 0) + self.assertEquals(data["count"], 0) + + # Note that this situation should not be possible, due to the user not being able + # to specify the user when creating categories/rules + def test_posts_with_authorized_rule_unauthorized_category(self): + other_user = UserFactory() + + rule = CollectionRuleFactory( + user=self.user, category=CategoryFactory(user=other_user) + ) + PostFactory.create_batch(size=3, rule=rule) + + response = self.client.get(reverse("api:posts-list")) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertTrue("results" in data) + self.assertTrue("count" in data) + self.assertEquals(data["count"], 0) + + def test_posts_with_authorized_user_without_category(self): + rule = CollectionRuleFactory(user=self.user, category=None) + PostFactory.create_batch(size=3, rule=rule) + + response = self.client.get(reverse("api:posts-list")) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertTrue("results" in data) + self.assertTrue("count" in data) + self.assertEquals(data["count"], 3) + + def test_unread_posts(self): + rule = CollectionRuleFactory( + user=self.user, category=CategoryFactory(user=self.user) + ) + + PostFactory.create_batch(size=10, rule=rule, read=False) + PostFactory.create_batch(size=10, rule=rule, read=True) + + response = self.client.get(reverse("api:posts-list"), {"read": "false"}) + + data = response.json() + posts = data["results"] + + self.assertEquals(response.status_code, 200) + self.assertEquals(data["count"], 10) + + for post in posts: + self.assertEquals(post["read"], False) + + def test_read_posts(self): + rule = CollectionRuleFactory( + user=self.user, category=CategoryFactory(user=self.user) + ) + + PostFactory.create_batch(size=20, rule=rule, read=False) + PostFactory.create_batch(size=10, rule=rule, read=True) + + response = self.client.get(reverse("api:posts-list"), {"read": "true"}) + + data = response.json() + posts = data["results"] + + self.assertEquals(response.status_code, 200) + self.assertEquals(data["count"], 10) + + for post in posts: + self.assertEquals(post["read"], True) diff --git a/src/newsreader/news/core/tests/factories.py b/src/newsreader/news/core/tests/factories.py new file mode 100644 index 0000000..46eeeae --- /dev/null +++ b/src/newsreader/news/core/tests/factories.py @@ -0,0 +1,31 @@ +import factory +import pytz + +from newsreader.accounts.tests.factories import UserFactory +from newsreader.news.core.models import Category, Post + + +class CategoryFactory(factory.django.DjangoModelFactory): + name = factory.Sequence(lambda n: "Category-{}".format(n)) + user = factory.SubFactory(UserFactory) + + class Meta: + model = Category + + +class PostFactory(factory.django.DjangoModelFactory): + title = factory.Faker("sentence") + body = factory.Faker("paragraph") + author = factory.Faker("name") + publication_date = factory.Faker("date_time_this_year", tzinfo=pytz.utc) + url = factory.Faker("url") + remote_identifier = factory.Faker("url") + + rule = factory.SubFactory( + "newsreader.news.collection.tests.factories.CollectionRuleFactory" + ) + + read = False + + class Meta: + model = Post diff --git a/src/newsreader/news/core/tests/test_views.py b/src/newsreader/news/core/tests/test_views.py new file mode 100644 index 0000000..e4bf458 --- /dev/null +++ b/src/newsreader/news/core/tests/test_views.py @@ -0,0 +1,229 @@ +from django.test import TestCase +from django.urls import reverse + +from newsreader.accounts.tests.factories import UserFactory +from newsreader.news.collection.tests.factories import CollectionRuleFactory +from newsreader.news.core.models import Category +from newsreader.news.core.tests.factories import CategoryFactory + + +class CategoryViewTestCase: + def setUp(self): + self.user = UserFactory(password="test") + self.client.force_login(self.user) + + def test_simple(self): + response = self.client.get(self.url) + + self.assertEquals(response.status_code, 200) + + +class CategoryCreateViewTestCase(CategoryViewTestCase, TestCase): + def setUp(self): + super().setUp() + + self.url = reverse("category-create") + + def test_creation(self): + rules = CollectionRuleFactory.create_batch(size=4, user=self.user) + + data = { + "name": "new-category", + "rules": [rule.pk for rule in rules], + "user": self.user.pk, + } + response = self.client.post(self.url, data) + + self.assertEquals(response.status_code, 302) + + category = Category.objects.get(name="new-category") + + self.assertCountEqual(category.rule_ids, [rule.pk for rule in rules]) + + def test_collection_rules_only_from_user(self): + other_user = UserFactory() + other_rules = CollectionRuleFactory.create_batch(size=4, user=other_user) + + response = self.client.get(self.url) + + for rule in other_rules: + self.assertNotContains(response, rule.name) + + def test_creation_with_other_user_rules(self): + other_user = UserFactory() + other_rules = CollectionRuleFactory.create_batch( + size=4, user=other_user, category=None + ) + + user_rules = CollectionRuleFactory.create_batch( + size=3, user=self.user, category=None + ) + + data = { + "name": "new-category", + "rules": [rule.pk for rule in other_rules], + "user": self.user.pk, + } + + response = self.client.post(self.url, data) + + self.assertContains(response, "not one of the available choices") + self.assertEquals(Category.objects.count(), 0) + + def test_unique_together(self): + category = CategoryFactory(name="category", user=self.user) + + data = {"name": "category", "user": self.user.pk, "rules": []} + response = self.client.post(self.url, data) + + categories = Category.objects.all() + + self.assertContains(response, "already exists") + + self.assertCountEqual(categories, [category]) + + +class CategoryUpdateViewTestCase(CategoryViewTestCase, TestCase): + def setUp(self): + super().setUp() + + self.category = CategoryFactory(name="category", user=self.user) + self.url = reverse("category-update", args=[self.category.pk]) + + def test_name_change(self): + data = {"name": "durp", "user": self.user.pk} + self.client.post(self.url, data) + + self.category.refresh_from_db() + + self.assertEquals(self.category.name, "durp") + + def test_add_collection_rules(self): + rules = CollectionRuleFactory.create_batch(size=4, user=self.user) + data = { + "name": self.category.name, + "rules": [rule.pk for rule in rules], + "user": self.user.pk, + } + + self.client.post(self.url, data) + + self.category.refresh_from_db() + + # this actually checks for sequence contents too + # see https://docs.python.org/3/library/unittest.html#unittest.TestCase.assertCountEqual + self.assertCountEqual(self.category.rule_ids, [rule.pk for rule in rules]) + + def test_collection_rule_change(self): + other_rules = CollectionRuleFactory.create_batch(size=4, user=self.user) + other_category = CategoryFactory(user=self.user) + other_category.rules.set([*other_rules]) + + current_rules = CollectionRuleFactory.create_batch(size=3, user=self.user) + self.category.rules.set([*current_rules]) + + self.assertCountEqual( + self.category.rule_ids, [rule.pk for rule in current_rules] + ) + + data = { + "name": self.category.name, + "rules": [rule.pk for rule in other_rules], + "user": self.user.pk, + } + + self.client.post(self.url, data) + + self.category.refresh_from_db() + self.assertCountEqual(self.category.rule_ids, [rule.pk for rule in other_rules]) + + other_category.refresh_from_db() + self.assertCountEqual(other_category.rule_ids, []) + + def test_collection_rule_removal(self): + current_rules = CollectionRuleFactory.create_batch(size=3, user=self.user) + self.category.rules.set([*current_rules]) + + self.assertCountEqual( + self.category.rule_ids, [rule.pk for rule in current_rules] + ) + + data = {"name": "durp", "user": self.user.pk} + self.client.post(self.url, data) + + self.category.refresh_from_db() + + self.assertEquals(self.category.rules.count(), 0) + + def test_collection_rules_only_from_user(self): + other_user = UserFactory() + other_rules = CollectionRuleFactory.create_batch(size=4, user=other_user) + + response = self.client.get(self.url) + + for rule in other_rules: + self.assertNotContains(response, rule.name) + + def test_update_category_of_other_user(self): + other_user = UserFactory() + other_rules = CollectionRuleFactory.create_batch(size=4, user=other_user) + + other_category = CategoryFactory(name="other category", user=other_user) + other_category.rules.set([*other_rules]) + + data = {"name": "durp", "user": other_user.pk} + other_url = reverse("category-update", args=[other_category.pk]) + response = self.client.post(other_url, data) + + self.assertEquals(response.status_code, 404) + + other_category.refresh_from_db() + + self.assertEquals(other_category.name, "other category") + self.assertEquals(other_category.rules.count(), 4) + + def test_category_update_with_other_user_rules(self): + other_user = UserFactory() + other_rules = CollectionRuleFactory.create_batch(size=4, user=other_user) + other_category = CategoryFactory(user=other_user) + other_category.rules.set([*other_rules]) + + current_rules = CollectionRuleFactory.create_batch(size=3, user=self.user) + self.category.rules.set([*current_rules]) + + self.assertCountEqual( + self.category.rule_ids, [rule.pk for rule in current_rules] + ) + + data = { + "name": self.category.name, + "rules": [rule.pk for rule in other_rules], + "user": self.user.pk, + } + + response = self.client.post(self.url, data) + + self.assertContains(response, "not one of the available choices") + + self.category.refresh_from_db() + self.assertCountEqual( + self.category.rule_ids, [rule.pk for rule in current_rules] + ) + + other_category.refresh_from_db() + self.assertCountEqual( + other_category.rule_ids, [rule.pk for rule in other_rules] + ) + + def test_unique_together(self): + other_category = CategoryFactory(name="other category", user=self.user) + + url = reverse("category-update", args=[other_category.pk]) + data = {"name": "category", "user": self.user.pk, "rules": []} + response = self.client.post(url, data) + + categories = Category.objects.all() + + self.assertContains(response, "already exists") + + self.assertCountEqual(categories, [self.category, other_category]) diff --git a/src/newsreader/news/core/urls.py b/src/newsreader/news/core/urls.py new file mode 100644 index 0000000..4b92428 --- /dev/null +++ b/src/newsreader/news/core/urls.py @@ -0,0 +1,56 @@ +from django.contrib.auth.decorators import login_required +from django.urls import path + +from newsreader.news.core.endpoints import ( + CategoryReadView, + DetailCategoryView, + DetailPostView, + ListCategoryView, + ListPostView, + NestedPostCategoryView, + NestedRuleCategoryView, +) +from newsreader.news.core.views import ( + CategoryCreateView, + CategoryListView, + CategoryUpdateView, + NewsView, +) + + +urlpatterns = [ + path("", login_required(NewsView.as_view()), name="index"), + path("categories/", login_required(CategoryListView.as_view()), name="categories"), + path( + "categories//", + login_required(CategoryUpdateView.as_view()), + name="category-update", + ), + path( + "categories/create/", + login_required(CategoryCreateView.as_view()), + name="category-create", + ), +] + +endpoints = [ + path("posts/", ListPostView.as_view(), name="posts-list"), + path("posts//", DetailPostView.as_view(), name="posts-detail"), + path("categories/", ListCategoryView.as_view(), name="categories-list"), + path( + "categories//", DetailCategoryView.as_view(), name="categories-detail" + ), + path( + "categories//read/", CategoryReadView.as_view(), name="categories-read" + ), + path( + "categories//rules/", + NestedRuleCategoryView.as_view(), + name="categories-nested-rules", + ), + path( + "categories//posts/", + NestedPostCategoryView.as_view(), + name="categories-nested-posts", + ), +] diff --git a/src/newsreader/news/core/views.py b/src/newsreader/news/core/views.py new file mode 100644 index 0000000..fde3974 --- /dev/null +++ b/src/newsreader/news/core/views.py @@ -0,0 +1,68 @@ +from django.urls import reverse_lazy +from django.views.generic.base import TemplateView +from django.views.generic.edit import CreateView, UpdateView +from django.views.generic.list import ListView + +from newsreader.news.collection.models import CollectionRule +from newsreader.news.core.forms import CategoryForm +from newsreader.news.core.models import Category + + +class NewsView(TemplateView): + template_name = "core/homepage.html" + + # TODO serialize objects to show filled main page + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + user = self.request.user + + categories = { + category: category.rules.order_by("-created") + for category in user.categories.order_by("name") + } + + rules = { + rule: rule.posts.order_by("-publication_date")[:30] + for rule in user.rules.order_by("-created") + } + + context.update(categories=categories, rules=rules) + return context + + +class CategoryViewMixin: + queryset = Category.objects.prefetch_related("rules").order_by("name") + + def get_queryset(self): + user = self.request.user + return self.queryset.filter(user=user) + + +class CategoryDetailMixin: + success_url = reverse_lazy("categories") + form_class = CategoryForm + + def get_context_data(self, **kwargs): + context_data = super().get_context_data(**kwargs) + + rules = CollectionRule.objects.filter(user=self.request.user).order_by("name") + context_data["rules"] = rules + + return context_data + + def get_form_kwargs(self): + return {**super().get_form_kwargs(), "user": self.request.user} + + +class CategoryListView(CategoryViewMixin, ListView): + template_name = "core/categories.html" + context_object_name = "categories" + + +class CategoryUpdateView(CategoryViewMixin, CategoryDetailMixin, UpdateView): + template_name = "core/category-update.html" + context_object_name = "category" + + +class CategoryCreateView(CategoryViewMixin, CategoryDetailMixin, CreateView): + template_name = "core/category-create.html" diff --git a/src/newsreader/scss/components/body/_body.scss b/src/newsreader/scss/components/body/_body.scss new file mode 100644 index 0000000..306ad7c --- /dev/null +++ b/src/newsreader/scss/components/body/_body.scss @@ -0,0 +1,19 @@ +.body { + margin: 0; + padding: 0; + background-color: $gainsboro; + + font-family: $default-font; + color: $default-font-color; +} + +body { + @extend .body; + + & * { + margin: 0; + padding: 0; + box-sizing: border-box; + } + +} diff --git a/src/newsreader/scss/components/body/index.scss b/src/newsreader/scss/components/body/index.scss new file mode 100644 index 0000000..533e39e --- /dev/null +++ b/src/newsreader/scss/components/body/index.scss @@ -0,0 +1 @@ +@import "body"; diff --git a/src/newsreader/scss/components/card/_card.scss b/src/newsreader/scss/components/card/_card.scss new file mode 100644 index 0000000..a9f957e --- /dev/null +++ b/src/newsreader/scss/components/card/_card.scss @@ -0,0 +1,36 @@ +.card { + display: flex; + flex-direction: column; + + margin: 20px 0; + padding: 15px; + + width: 50%; + border-radius: 5px; + + background-color: $white; + + &__header { + display: flex; + justify-content: space-between; + align-items: center; + + padding: 15px 0; + + border-bottom: 2px $border-gray solid; + } + + &__content { + display: flex; + padding: 10px; + } + + &__footer { + display: flex; + padding: 10px; + } + + & .favicon { + height: 20px; + } +} diff --git a/src/newsreader/scss/components/card/_rule-card.scss b/src/newsreader/scss/components/card/_rule-card.scss new file mode 100644 index 0000000..5edb25b --- /dev/null +++ b/src/newsreader/scss/components/card/_rule-card.scss @@ -0,0 +1,26 @@ +.card { + @extend .card; + + &__header { + & div { + display: flex; + flex-direction: row; + + & img { + padding: 0 10px; + } + } + + &--action > .button { + margin: 0 10px; + } + } + + &__content { + flex-direction: column; + } + + &__footer > *:last-child { + margin: 0 0 0 10px; + } +} diff --git a/src/newsreader/scss/components/card/index.scss b/src/newsreader/scss/components/card/index.scss new file mode 100644 index 0000000..149efa0 --- /dev/null +++ b/src/newsreader/scss/components/card/index.scss @@ -0,0 +1,2 @@ +@import "card"; +@import "rule-card"; diff --git a/src/newsreader/scss/components/category/_category.scss b/src/newsreader/scss/components/category/_category.scss new file mode 100644 index 0000000..9d8451f --- /dev/null +++ b/src/newsreader/scss/components/category/_category.scss @@ -0,0 +1,45 @@ +.category { + display: flex; + align-items: center; + + padding: 5px; + + border-radius: 5px; + + &__info { + display: flex; + justify-content: space-between; + + width: 100%; + padding: 0 0 0 20px; + + overflow: hidden; + white-space: nowrap; + + & h4 { + overflow: hidden; + text-overflow: ellipsis; + } + + &:hover { + cursor: pointer; + } + } + + &__menu { + display: flex; + align-items: center; + + &:hover { + cursor: pointer; + } + } + + &:hover { + background-color: darken($azureish-white, +10%); + } + + &--selected { + background-color: darken($azureish-white, +10%); + } +} diff --git a/src/newsreader/scss/components/category/index.scss b/src/newsreader/scss/components/category/index.scss new file mode 100644 index 0000000..702bb58 --- /dev/null +++ b/src/newsreader/scss/components/category/index.scss @@ -0,0 +1 @@ +@import "category"; diff --git a/src/newsreader/scss/components/errorlist/_errorlist.scss b/src/newsreader/scss/components/errorlist/_errorlist.scss new file mode 100644 index 0000000..006dafb --- /dev/null +++ b/src/newsreader/scss/components/errorlist/_errorlist.scss @@ -0,0 +1,23 @@ +.errorlist { + @extend .list; + + margin: 5px 0; + padding: 0; + + color: $white; + + list-style: disc; + list-style-position: inside; + + &__item { + margin: 10px 0; + padding: 10px; + + background-color: $error-red; + border-radius: 5px; + } + + & li { + @extend .errorlist__item; + } +} diff --git a/src/newsreader/scss/components/errorlist/index.scss b/src/newsreader/scss/components/errorlist/index.scss new file mode 100644 index 0000000..bf0c9c9 --- /dev/null +++ b/src/newsreader/scss/components/errorlist/index.scss @@ -0,0 +1 @@ +@import "errorlist"; diff --git a/src/newsreader/scss/components/fieldset/_fieldset.scss b/src/newsreader/scss/components/fieldset/_fieldset.scss new file mode 100644 index 0000000..c2588b5 --- /dev/null +++ b/src/newsreader/scss/components/fieldset/_fieldset.scss @@ -0,0 +1,11 @@ +.fieldset { + display: flex; + flex-direction: column; + + padding: 15px; + border: none; +} + +fieldset { + @extend .fieldset; +} diff --git a/src/newsreader/scss/components/fieldset/index.scss b/src/newsreader/scss/components/fieldset/index.scss new file mode 100644 index 0000000..be990a8 --- /dev/null +++ b/src/newsreader/scss/components/fieldset/index.scss @@ -0,0 +1 @@ +@import "fieldset"; diff --git a/src/newsreader/scss/components/form/_activation-form.scss b/src/newsreader/scss/components/form/_activation-form.scss new file mode 100644 index 0000000..39ecc27 --- /dev/null +++ b/src/newsreader/scss/components/form/_activation-form.scss @@ -0,0 +1,11 @@ +.activation-form { + margin: 10px 0; + & h4 { + padding: 20px 24px 5px 24px; + } + + &__fieldset:last-child { + flex-direction: row; + justify-content: space-between; + } +} diff --git a/src/newsreader/scss/components/form/_category-form.scss b/src/newsreader/scss/components/form/_category-form.scss new file mode 100644 index 0000000..8132ed2 --- /dev/null +++ b/src/newsreader/scss/components/form/_category-form.scss @@ -0,0 +1,13 @@ +.category-form { + @extend .form; + + margin: 20px 0; + + &__section:last-child { + & .category-form__fieldset { + display: flex; + flex-direction: row; + justify-content: space-between; + } + } +} diff --git a/src/newsreader/scss/components/form/_form.scss b/src/newsreader/scss/components/form/_form.scss new file mode 100644 index 0000000..931fba9 --- /dev/null +++ b/src/newsreader/scss/components/form/_form.scss @@ -0,0 +1,33 @@ +.form { + display: flex; + flex-direction: column; + + width: 70%; + border-radius: 5px; + + font-family: $form-font; + background-color: $white; + + &__fieldset { + @extend .fieldset; + } + + &__header { + display: flex; + flex-direction: row; + + padding: 15px; + } + + &__title { + font-size: 18px; + } + + & .favicon { + height: 20px; + } +} + +form { + @extend .form; +} diff --git a/src/newsreader/scss/components/form/_import-form.scss b/src/newsreader/scss/components/form/_import-form.scss new file mode 100644 index 0000000..19acc5c --- /dev/null +++ b/src/newsreader/scss/components/form/_import-form.scss @@ -0,0 +1,17 @@ +.import-form { + margin: 20px 0; + + &__fieldset:last-child { + display: flex; + flex-direction: row; + justify-content: space-between; + } + + & input[type=file] { + width: 50%; + } + + & input[type=checkbox] { + margin: 0 auto 0 10px; + } +} diff --git a/src/newsreader/scss/components/form/_login-form.scss b/src/newsreader/scss/components/form/_login-form.scss new file mode 100644 index 0000000..10e81a0 --- /dev/null +++ b/src/newsreader/scss/components/form/_login-form.scss @@ -0,0 +1,33 @@ +.login-form { + @extend .form; + + width: 100%; + + h4 { + margin: 0; + padding: 20px 24px 5px 24px; + } + + &__fieldset { + @extend .form__fieldset; + + & label { + @extend .label; + } + + & input { + @extend .input; + } + } + + &__fieldset:last-child { + flex-direction: row-reverse; + justify-content: space-between; + } + + &__fieldset:last-child { + .button { + padding: 10px 50px; + } + } +} diff --git a/src/newsreader/scss/components/form/_password-reset-confirm-form.scss b/src/newsreader/scss/components/form/_password-reset-confirm-form.scss new file mode 100644 index 0000000..d570c38 --- /dev/null +++ b/src/newsreader/scss/components/form/_password-reset-confirm-form.scss @@ -0,0 +1,3 @@ +.password-reset-confirm-form { + margin: 20px 0; +} diff --git a/src/newsreader/scss/components/form/_password-reset-form.scss b/src/newsreader/scss/components/form/_password-reset-form.scss new file mode 100644 index 0000000..be92ff4 --- /dev/null +++ b/src/newsreader/scss/components/form/_password-reset-form.scss @@ -0,0 +1,18 @@ +.password-reset-form { + margin: 20px 0; + + &__fieldset:last-child { + display: flex; + flex-direction: row; + justify-content: space-between; + } + + & .form__header { + display: flex; + flex-direction: column; + } + + & .form__title { + margin: 0 0 5px 0; + } +} diff --git a/src/newsreader/scss/components/form/_register-form.scss b/src/newsreader/scss/components/form/_register-form.scss new file mode 100644 index 0000000..e406ae7 --- /dev/null +++ b/src/newsreader/scss/components/form/_register-form.scss @@ -0,0 +1,11 @@ +.register-form { + margin: 10px 0; + & h4 { + padding: 20px 24px 5px 24px; + } + + &__fieldset:last-child { + flex-direction: row; + justify-content: space-between; + } +} diff --git a/src/newsreader/scss/components/form/_rule-form.scss b/src/newsreader/scss/components/form/_rule-form.scss new file mode 100644 index 0000000..82651aa --- /dev/null +++ b/src/newsreader/scss/components/form/_rule-form.scss @@ -0,0 +1,25 @@ +.rule-form { + margin: 20px 0; + + &__section:last-child { + & .rule-form__fieldset { + display: flex; + flex-direction: row; + justify-content: space-between; + } + } + + #id_category { + width: 50%; + + padding: 0 10px; + } + + #id_timezone { + max-height: 200px; + width: 50%; + + margin: 0 15px; + padding: 0 10px; + } +} diff --git a/src/newsreader/scss/components/form/index.scss b/src/newsreader/scss/components/form/index.scss new file mode 100644 index 0000000..2c70cdd --- /dev/null +++ b/src/newsreader/scss/components/form/index.scss @@ -0,0 +1,12 @@ +@import "form"; + +@import "category-form"; +@import "rule-form"; +@import "import-form"; + +@import "login-form"; +@import "activation-form"; +@import "register-form"; + +@import "password-reset-form"; +@import "password-reset-confirm-form"; diff --git a/src/newsreader/scss/components/index.scss b/src/newsreader/scss/components/index.scss new file mode 100644 index 0000000..4bddb31 --- /dev/null +++ b/src/newsreader/scss/components/index.scss @@ -0,0 +1,26 @@ +@import "body/index"; +@import "form/index"; +@import "main/index"; +@import "navbar/index"; +@import "loading-indicator/index"; + +@import "modal/index"; + +@import "card/index"; +@import "list/index"; +@import "messages/index"; +@import "section/index"; +@import "errorlist/index"; +@import "fieldset/index"; +@import "sidebar/index"; + +@import "rules/index"; +@import "category/index"; + +@import "post/index"; +@import "post-block/index"; +@import "post-message/index"; +@import "posts/index"; +@import "posts-header/index"; +@import "posts-info/index"; +@import "posts-section/index"; diff --git a/src/newsreader/scss/components/list/_list.scss b/src/newsreader/scss/components/list/_list.scss new file mode 100644 index 0000000..75e5e94 --- /dev/null +++ b/src/newsreader/scss/components/list/_list.scss @@ -0,0 +1,20 @@ +.list { + padding: 0 10px; + margin: 0; + + list-style: none; + + &__item { + display: flex; + align-items: center; + padding: 10px 0; + + & > * { + margin: 0 15px; + } + } +} + +ul { + @extend .list; +} diff --git a/src/newsreader/scss/components/list/index.scss b/src/newsreader/scss/components/list/index.scss new file mode 100644 index 0000000..0a92e49 --- /dev/null +++ b/src/newsreader/scss/components/list/index.scss @@ -0,0 +1 @@ +@import "list"; diff --git a/src/newsreader/scss/components/loading-indicator/_loading-indicator.scss b/src/newsreader/scss/components/loading-indicator/_loading-indicator.scss new file mode 100644 index 0000000..0651d1d --- /dev/null +++ b/src/newsreader/scss/components/loading-indicator/_loading-indicator.scss @@ -0,0 +1,41 @@ +.loading-indicator { + display: inline-block; + position: relative; + width: 64px; + height: 64px; + + & div { + display: inline-block; + position: absolute; + left: 6px; + width: 13px; + background-color: $lavendal-pink; + animation: loading-indicator 1.2s cubic-bezier(0, 0.5, 0.5, 1) infinite; + + &:nth-child(1){ + left: 6px; + animation-delay: -0.24s; + } + + &:nth-child(2){ + left: 26px; + animation-delay: -0.12s; + } + + &:nth-child(3){ + left: 45px; + animation-delay: 0; + } + } +} + +@keyframes loading-indicator { + 0% { + top: 6px; + height: 51px; + } + 50%, 100% { + top: 19px; + height: 26px; + } +} diff --git a/src/newsreader/scss/components/loading-indicator/index.scss b/src/newsreader/scss/components/loading-indicator/index.scss new file mode 100644 index 0000000..c3a3bc3 --- /dev/null +++ b/src/newsreader/scss/components/loading-indicator/index.scss @@ -0,0 +1 @@ +@import "loading-indicator"; diff --git a/src/newsreader/scss/components/main/_main.scss b/src/newsreader/scss/components/main/_main.scss new file mode 100644 index 0000000..5d0143f --- /dev/null +++ b/src/newsreader/scss/components/main/_main.scss @@ -0,0 +1,7 @@ +.main { + display: flex; + flex-direction: column; + align-items: center; + + margin: 20px 0; +} diff --git a/src/newsreader/scss/components/main/index.scss b/src/newsreader/scss/components/main/index.scss new file mode 100644 index 0000000..bdb4ce0 --- /dev/null +++ b/src/newsreader/scss/components/main/index.scss @@ -0,0 +1 @@ +@import "main"; diff --git a/src/newsreader/scss/components/messages/_messages.scss b/src/newsreader/scss/components/messages/_messages.scss new file mode 100644 index 0000000..5779820 --- /dev/null +++ b/src/newsreader/scss/components/messages/_messages.scss @@ -0,0 +1,43 @@ +.messages { + display: flex; + flex-direction: column; + align-items: center; + + position: fixed; + top: 0; + width: 100%; + margin: 5px 0 20px 0; + + color: $white; + + &__item { + width: 80%; + + position: relative; + padding: 20px 15px; + margin: 5px 0; + + border-radius: 5px; + + background-color: $focus-blue; + + &--error { + background-color: $error-red; + } + + &--warning { + background-color: $light-orange; + } + + &--success { + background-color: $success-green; + } + + & .gg-close { + position: absolute; + top: 15px; + right: 15px; + --ggs: 2; + } + } +} diff --git a/src/newsreader/scss/components/messages/index.scss b/src/newsreader/scss/components/messages/index.scss new file mode 100644 index 0000000..1e28703 --- /dev/null +++ b/src/newsreader/scss/components/messages/index.scss @@ -0,0 +1 @@ +@import "messages"; diff --git a/src/newsreader/scss/components/modal/_modal.scss b/src/newsreader/scss/components/modal/_modal.scss new file mode 100644 index 0000000..93fe54f --- /dev/null +++ b/src/newsreader/scss/components/modal/_modal.scss @@ -0,0 +1,42 @@ +.modal { + display: flex; + flex-direction: column; + align-items: center; + + position: fixed; + width: 100%; + height: 100%; + top: 0; + + background-color: $dark; + + &__item { + display: flex; + flex-direction: column; + align-self: center; + + margin: 20px 0; + padding: 20px; + + width: 60%; + + border-radius: 5px; + background-color: $white; + } + + &__header { + padding: 5px 20px; + } + + &__content { + padding: 10px 30px; + } + + &__footer { + display: flex; + flex-direction: row; + justify-content: space-between; + + padding: 10px; + } +} diff --git a/src/newsreader/scss/components/modal/_post-modal.scss b/src/newsreader/scss/components/modal/_post-modal.scss new file mode 100644 index 0000000..f357d77 --- /dev/null +++ b/src/newsreader/scss/components/modal/_post-modal.scss @@ -0,0 +1,9 @@ +.post-modal { + @extend .modal; + + margin: 0; + padding: 0; + + border-radius: 0; + cursor: pointer; +} diff --git a/src/newsreader/scss/components/modal/index.scss b/src/newsreader/scss/components/modal/index.scss new file mode 100644 index 0000000..d84836a --- /dev/null +++ b/src/newsreader/scss/components/modal/index.scss @@ -0,0 +1,3 @@ +@import "modal"; + +@import "post-modal"; diff --git a/src/newsreader/scss/components/navbar/_navbar.scss b/src/newsreader/scss/components/navbar/_navbar.scss new file mode 100644 index 0000000..d5699ed --- /dev/null +++ b/src/newsreader/scss/components/navbar/_navbar.scss @@ -0,0 +1,45 @@ +.nav { + display: flex; + justify-content: center; + + margin: 0 0 5px 0; + padding: 15px 0; + width: 100%; + + background-color: $white; + box-shadow: 0px 5px darken($azureish-white, +10%); + + ol { + display: flex; + justify-content: flex-start; + + width: 80%; + list-style-type: none; + } + + a { + color: $nickel; + text-decoration: none; + } + + &__item { + margin: 0px 10px; + + border: none; + border-radius: 2px; + + background-color: $azureish-white; + + &:hover{ + background-color: lighten($azureish-white, +5%); + } + + & a { + @extend .button; + } + } + + &__item:last-child { + margin: 0 10px 0 auto; + } +} diff --git a/src/newsreader/scss/components/navbar/index.scss b/src/newsreader/scss/components/navbar/index.scss new file mode 100644 index 0000000..b45a5a0 --- /dev/null +++ b/src/newsreader/scss/components/navbar/index.scss @@ -0,0 +1 @@ +@import "navbar"; diff --git a/src/newsreader/scss/components/post-block/_post-block.scss b/src/newsreader/scss/components/post-block/_post-block.scss new file mode 100644 index 0000000..c65352b --- /dev/null +++ b/src/newsreader/scss/components/post-block/_post-block.scss @@ -0,0 +1,12 @@ +.post-block { + display: flex; + flex-direction: column; + align-items: center; + + width: 70%; + margin: 0 0 2% 0; + + font-family: $article-font; + + border-radius: 2px; +} diff --git a/src/newsreader/scss/components/post-block/index.scss b/src/newsreader/scss/components/post-block/index.scss new file mode 100644 index 0000000..e17b7a9 --- /dev/null +++ b/src/newsreader/scss/components/post-block/index.scss @@ -0,0 +1 @@ +@import "post-block"; diff --git a/src/newsreader/scss/components/post-message/_post-message.scss b/src/newsreader/scss/components/post-message/_post-message.scss new file mode 100644 index 0000000..03a1dc2 --- /dev/null +++ b/src/newsreader/scss/components/post-message/_post-message.scss @@ -0,0 +1,25 @@ +.post-message { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + + width: 60%; + height: 80vh; + border-radius: 2px; + + font-family: $article-font; + background-color: $white; + + &__message { + font-size: 16px; + } + + &__block { + display: flex; + flex-direction: row; + align-items: center; + + margin: 5px; + } +} diff --git a/src/newsreader/scss/components/post-message/index.scss b/src/newsreader/scss/components/post-message/index.scss new file mode 100644 index 0000000..03cf130 --- /dev/null +++ b/src/newsreader/scss/components/post-message/index.scss @@ -0,0 +1 @@ +@import "post-message"; diff --git a/src/newsreader/scss/components/post/_post.scss b/src/newsreader/scss/components/post/_post.scss new file mode 100644 index 0000000..a7b5ed5 --- /dev/null +++ b/src/newsreader/scss/components/post/_post.scss @@ -0,0 +1,132 @@ +.post { + display: flex; + flex-direction: column; + align-items: center; + position: relative; + + width: 80%; + height: 90%; + + margin: 2% auto 5% auto; + + border-radius: 5px; + + overflow-y: auto; + + background-color: $white; + + cursor: initial; + + &__header { + display: flex; + flex-direction: column; + padding: 20px 0 10px 0; + width: 75%; + + font-family: $article-header-font; + } + + &__title { + line-height: 1; + + &--read { + color: $gainsboro; + } + } + + &__link { + display: inline-flex; + padding: 0 15px; + + & img { + width: 30px; + } + } + + &__date { + align-self: center; + font-size: small; + } + + &__body { + display:flex; + flex-direction: column; + + padding: 10px 0 30px 0; + width: 75%; + + line-height: 1.5; + font-family: $article-font; + font-size: 18px; + + & p { + padding: 10px 0; + } + + & h1, h2, h3 { + margin: 20px 0 5px 0; + } + + & img { + padding: 10px 10px 30px 10px; + + max-width: 70%; + width: inherit; + height: 100%; + + align-self: center; + } + } + + &__close-button { + position: relative; + margin: 1% 2% 0 0; + align-self: flex-end; + + &:hover { + background-color: lighten($gainsboro, +1%); + } + } + + &__meta-info { + display: flex; + flex-direction: row; + align-items: center; + + margin: 15px 0; + } + + &__section-info { + display: flex; + flex-direction: column; + + align-self: flex-end; + position: absolute; + top: 25%; + width: 10%; + + font-family: $button-font; + color: lighten($default-font-color, +10%); + + & h5 { + margin: 10px 0 0 0; + padding: 5px 1px 5px 5px; + + border-radius: 5px 0 0 5px; + + background-color: aquamarine; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + text-align: center; + } + + & h5:first-child { + background-color: $light-orange; + } + + & h5:last-child { + background-color: $beige; + } + } +} diff --git a/src/newsreader/scss/components/post/index.scss b/src/newsreader/scss/components/post/index.scss new file mode 100644 index 0000000..b31e7bb --- /dev/null +++ b/src/newsreader/scss/components/post/index.scss @@ -0,0 +1 @@ +@import "post"; diff --git a/src/newsreader/scss/components/posts-header/_posts-header.scss b/src/newsreader/scss/components/posts-header/_posts-header.scss new file mode 100644 index 0000000..be0dac8 --- /dev/null +++ b/src/newsreader/scss/components/posts-header/_posts-header.scss @@ -0,0 +1,15 @@ +.posts-header { + + &__title { + width: 80%; + + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 16px; + + &--read { + color: darken($gainsboro, +10%); + } + } +} diff --git a/src/newsreader/scss/components/posts-header/index.scss b/src/newsreader/scss/components/posts-header/index.scss new file mode 100644 index 0000000..451a453 --- /dev/null +++ b/src/newsreader/scss/components/posts-header/index.scss @@ -0,0 +1 @@ +@import "posts-header"; diff --git a/src/newsreader/scss/components/posts-info/_posts-info.scss b/src/newsreader/scss/components/posts-info/_posts-info.scss new file mode 100644 index 0000000..9f9bc6c --- /dev/null +++ b/src/newsreader/scss/components/posts-info/_posts-info.scss @@ -0,0 +1,15 @@ +.posts-info { + display: flex; + justify-content: space-around; + align-items: center; + + width: 20%; + + &__date { + align-self: center; + } + + &__link { + display: inline-flex; + } +} diff --git a/src/newsreader/scss/components/posts-info/index.scss b/src/newsreader/scss/components/posts-info/index.scss new file mode 100644 index 0000000..2a7a495 --- /dev/null +++ b/src/newsreader/scss/components/posts-info/index.scss @@ -0,0 +1 @@ +@import "posts-info"; diff --git a/src/newsreader/scss/components/posts-section/_post-section.scss b/src/newsreader/scss/components/posts-section/_post-section.scss new file mode 100644 index 0000000..1c40bec --- /dev/null +++ b/src/newsreader/scss/components/posts-section/_post-section.scss @@ -0,0 +1,20 @@ +.posts-section { + display: flex; + flex-direction: column; + width: 95%; + + margin: 20px; + padding: 10px; + border-radius: 5px; + + background-color: $white; + + &:first-child { + padding: 0 10px 10px 10px; + margin: 0 20px 20px 20px; + } + + &__name { + padding: 20px 0 10px 0; + } +} diff --git a/src/newsreader/scss/components/posts-section/index.scss b/src/newsreader/scss/components/posts-section/index.scss new file mode 100644 index 0000000..945ed28 --- /dev/null +++ b/src/newsreader/scss/components/posts-section/index.scss @@ -0,0 +1 @@ +@import "post-section"; diff --git a/src/newsreader/scss/components/posts/_posts.scss b/src/newsreader/scss/components/posts/_posts.scss new file mode 100644 index 0000000..9a3525b --- /dev/null +++ b/src/newsreader/scss/components/posts/_posts.scss @@ -0,0 +1,34 @@ +.posts { + display: flex; + flex-direction: column; + + list-style: none; + + padding: 0; + + &__item { + display: flex; + align-items: center; + + padding: 10px 0 10px 0; + + border-radius: 2px; + border-bottom: 2px solid $azureish-white; + + &:hover { + cursor: pointer; + background-color: $gainsboro; + } + + & span { + font-size: small; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + + &:last-child { + border-bottom: 0; + } + } +} diff --git a/src/newsreader/scss/components/posts/index.scss b/src/newsreader/scss/components/posts/index.scss new file mode 100644 index 0000000..66f1811 --- /dev/null +++ b/src/newsreader/scss/components/posts/index.scss @@ -0,0 +1 @@ +@import "posts"; diff --git a/src/newsreader/scss/components/rules/_rules.scss b/src/newsreader/scss/components/rules/_rules.scss new file mode 100644 index 0000000..029a070 --- /dev/null +++ b/src/newsreader/scss/components/rules/_rules.scss @@ -0,0 +1,52 @@ +.rules { + padding: 0; + + &__item { + display: flex; + justify-content: space-between; + align-items: center; + + padding: 5px 5px 5px 20px; + + border-radius: 5px; + + & * { + padding: 0 2px 0 2px; + } + + &:hover { + cursor: pointer; + background-color: darken($azureish-white, +10%); + } + + &--selected { + background-color: darken($azureish-white, +10%); + } + } + + &__info { + display: flex; + align-items: center; + width: 80%; + + & .gg-image { + --ggs: 80%; + margin: 0 5px 0 0; + min-width: 20px; + } + + & .favicon { + margin: 0 5px 0 0; + } + } + + &__title { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + & .badge { + display: flex; + } +} diff --git a/src/newsreader/scss/components/rules/index.scss b/src/newsreader/scss/components/rules/index.scss new file mode 100644 index 0000000..e6a0ebf --- /dev/null +++ b/src/newsreader/scss/components/rules/index.scss @@ -0,0 +1 @@ +@import "rules"; diff --git a/src/newsreader/scss/components/section/_section.scss b/src/newsreader/scss/components/section/_section.scss new file mode 100644 index 0000000..399e935 --- /dev/null +++ b/src/newsreader/scss/components/section/_section.scss @@ -0,0 +1,6 @@ +.section { + display: flex; + flex-direction: column; + + border: none; +} diff --git a/src/newsreader/scss/components/section/index.scss b/src/newsreader/scss/components/section/index.scss new file mode 100644 index 0000000..4fb6763 --- /dev/null +++ b/src/newsreader/scss/components/section/index.scss @@ -0,0 +1 @@ +@import "section"; diff --git a/src/newsreader/scss/components/sidebar/_sidebar.scss b/src/newsreader/scss/components/sidebar/_sidebar.scss new file mode 100644 index 0000000..feac44d --- /dev/null +++ b/src/newsreader/scss/components/sidebar/_sidebar.scss @@ -0,0 +1,26 @@ +.sidebar { + display: flex; + flex-direction: column; + align-items: center; + align-self: start; + + position: sticky; + top: 5%; + + width: 20%; + + &__nav { + width: 100%; + max-height: 80vh; + overflow: auto; + + list-style: none; + border-radius: 5px; + + font-family: $sidebar-font; + + &__item { + padding: 2px 10px 5px 10px; + } + } +} diff --git a/src/newsreader/scss/components/sidebar/index.scss b/src/newsreader/scss/components/sidebar/index.scss new file mode 100644 index 0000000..0abffa8 --- /dev/null +++ b/src/newsreader/scss/components/sidebar/index.scss @@ -0,0 +1 @@ +@import "sidebar"; diff --git a/src/newsreader/scss/elements/badge/_badge.scss b/src/newsreader/scss/elements/badge/_badge.scss new file mode 100644 index 0000000..1e2db24 --- /dev/null +++ b/src/newsreader/scss/elements/badge/_badge.scss @@ -0,0 +1,14 @@ +.badge { + display: inline-block; + + padding-left: 8px; + padding-right: 8px; + border-radius: 2px; + + text-align: center; + + background-color: lighten($pewter-blue, +20%); + + font-family: $button-font; + font-size: small; +} diff --git a/src/newsreader/scss/elements/badge/index.scss b/src/newsreader/scss/elements/badge/index.scss new file mode 100644 index 0000000..87110f0 --- /dev/null +++ b/src/newsreader/scss/elements/badge/index.scss @@ -0,0 +1 @@ +@import "badge"; diff --git a/src/newsreader/scss/elements/button/_button.scss b/src/newsreader/scss/elements/button/_button.scss new file mode 100644 index 0000000..a6bec19 --- /dev/null +++ b/src/newsreader/scss/elements/button/_button.scss @@ -0,0 +1,46 @@ +.button { + display: flex; + + align-items: center; + justify-content: center; + + padding: 10px 50px; + + border: none; + border-radius: 2px; + + font-family: $button-font; + font-size: 16px; + + &:hover { + cursor: pointer; + } + + &--success, &--confirm { + color: $white !important; + background-color: $confirm-green; + + &:hover { + background-color: lighten($confirm-green, +5%); + } + } + + &--error, &--cancel { + color: $white !important; + background-color: $error-red; + + &:hover { + background-color: lighten($error-red, +5%); + } + + } + + &--primary { + color: $white !important; + background-color: darken($azureish-white, +20%); + + &:hover { + background-color: $azureish-white; + } + } +} diff --git a/src/newsreader/scss/elements/button/_read-button.scss b/src/newsreader/scss/elements/button/_read-button.scss new file mode 100644 index 0000000..940d895 --- /dev/null +++ b/src/newsreader/scss/elements/button/_read-button.scss @@ -0,0 +1,12 @@ +.read-button { + @extend .button; + + margin: 20px 0 0 0; + + color: $white; + background-color: $confirm-green; + + &:hover { + background-color: darken($confirm-green, 10%); + } +} diff --git a/src/newsreader/scss/elements/button/index.scss b/src/newsreader/scss/elements/button/index.scss new file mode 100644 index 0000000..a9b2ec7 --- /dev/null +++ b/src/newsreader/scss/elements/button/index.scss @@ -0,0 +1,2 @@ +@import "button"; +@import "_read-button"; diff --git a/src/newsreader/scss/elements/h1/_h1.scss b/src/newsreader/scss/elements/h1/_h1.scss new file mode 100644 index 0000000..d82b6eb --- /dev/null +++ b/src/newsreader/scss/elements/h1/_h1.scss @@ -0,0 +1,3 @@ +.h1 { + font-family: $header-font; +} diff --git a/src/newsreader/scss/elements/h1/index.scss b/src/newsreader/scss/elements/h1/index.scss new file mode 100644 index 0000000..0fe45be --- /dev/null +++ b/src/newsreader/scss/elements/h1/index.scss @@ -0,0 +1 @@ +@import "h1"; diff --git a/src/newsreader/scss/elements/h2/_h2.scss b/src/newsreader/scss/elements/h2/_h2.scss new file mode 100644 index 0000000..18abeb4 --- /dev/null +++ b/src/newsreader/scss/elements/h2/_h2.scss @@ -0,0 +1,3 @@ +.h2 { + font-family: $header-font; +} diff --git a/src/newsreader/scss/elements/h2/index.scss b/src/newsreader/scss/elements/h2/index.scss new file mode 100644 index 0000000..6ca9eab --- /dev/null +++ b/src/newsreader/scss/elements/h2/index.scss @@ -0,0 +1 @@ +@import "h2"; diff --git a/src/newsreader/scss/elements/h3/_h3.scss b/src/newsreader/scss/elements/h3/_h3.scss new file mode 100644 index 0000000..955967b --- /dev/null +++ b/src/newsreader/scss/elements/h3/_h3.scss @@ -0,0 +1,3 @@ +.h3 { + font-family: $header-font; +} diff --git a/src/newsreader/scss/elements/h3/index.scss b/src/newsreader/scss/elements/h3/index.scss new file mode 100644 index 0000000..feb5951 --- /dev/null +++ b/src/newsreader/scss/elements/h3/index.scss @@ -0,0 +1 @@ +@import "h3"; diff --git a/src/newsreader/scss/elements/help-text/_help-text.scss b/src/newsreader/scss/elements/help-text/_help-text.scss new file mode 100644 index 0000000..a90552d --- /dev/null +++ b/src/newsreader/scss/elements/help-text/_help-text.scss @@ -0,0 +1,9 @@ +.help-text { + @extend .small; + + padding: 5px 15px; +} + +.helptext { + @extend .help-text; +} diff --git a/src/newsreader/scss/elements/help-text/index.scss b/src/newsreader/scss/elements/help-text/index.scss new file mode 100644 index 0000000..f5f595f --- /dev/null +++ b/src/newsreader/scss/elements/help-text/index.scss @@ -0,0 +1 @@ +@import "help-text"; diff --git a/src/newsreader/scss/elements/index.scss b/src/newsreader/scss/elements/index.scss new file mode 100644 index 0000000..f0d7be3 --- /dev/null +++ b/src/newsreader/scss/elements/index.scss @@ -0,0 +1,10 @@ +@import "button/index"; +@import "link/index"; +@import "h1/index"; +@import "h2/index"; +@import "h3/index"; +@import "small/index"; +@import "input/index"; +@import "label/index"; +@import "help-text/index"; +@import "badge/index"; diff --git a/src/newsreader/scss/elements/input/_input.scss b/src/newsreader/scss/elements/input/_input.scss new file mode 100644 index 0000000..1cfb4bb --- /dev/null +++ b/src/newsreader/scss/elements/input/_input.scss @@ -0,0 +1,15 @@ +.input { + padding: 10px; + + background-color: lighten($gainsboro, +4%); + border: 1px $border-gray solid; + border-radius: 2px; + + &:focus { + border: 1px $focus-blue solid; + } +} + +input { + @extend .input; +} diff --git a/src/newsreader/scss/elements/input/index.scss b/src/newsreader/scss/elements/input/index.scss new file mode 100644 index 0000000..84e5ed8 --- /dev/null +++ b/src/newsreader/scss/elements/input/index.scss @@ -0,0 +1 @@ +@import "input"; diff --git a/src/newsreader/scss/elements/label/_label.scss b/src/newsreader/scss/elements/label/_label.scss new file mode 100644 index 0000000..5030a4c --- /dev/null +++ b/src/newsreader/scss/elements/label/_label.scss @@ -0,0 +1,7 @@ +.label { + padding: 10px; +} + +label { + @extend .label; +} diff --git a/src/newsreader/scss/elements/label/index.scss b/src/newsreader/scss/elements/label/index.scss new file mode 100644 index 0000000..12e5523 --- /dev/null +++ b/src/newsreader/scss/elements/label/index.scss @@ -0,0 +1 @@ +@import "label"; diff --git a/src/newsreader/scss/elements/link/_link.scss b/src/newsreader/scss/elements/link/_link.scss new file mode 100644 index 0000000..1843c0b --- /dev/null +++ b/src/newsreader/scss/elements/link/_link.scss @@ -0,0 +1,16 @@ +.link { + color: darken($azureish-white, 30%); + text-decoration: none; + + &:hover { + cursor: pointer; + } +} + +a { + @extend .link; +} + +.gg-link { + color: initial; +} diff --git a/src/newsreader/scss/elements/link/index.scss b/src/newsreader/scss/elements/link/index.scss new file mode 100644 index 0000000..bab69d1 --- /dev/null +++ b/src/newsreader/scss/elements/link/index.scss @@ -0,0 +1 @@ +@import "link"; diff --git a/src/newsreader/scss/elements/small/_small.scss b/src/newsreader/scss/elements/small/_small.scss new file mode 100644 index 0000000..c95bfab --- /dev/null +++ b/src/newsreader/scss/elements/small/_small.scss @@ -0,0 +1,8 @@ +.small { + color: $nickel; + font-size: small; +} + +small { + @extend .small; +} diff --git a/src/newsreader/scss/elements/small/index.scss b/src/newsreader/scss/elements/small/index.scss new file mode 100644 index 0000000..ea3d25f --- /dev/null +++ b/src/newsreader/scss/elements/small/index.scss @@ -0,0 +1 @@ +@import "small"; diff --git a/src/newsreader/scss/index.scss b/src/newsreader/scss/index.scss new file mode 100644 index 0000000..2dbf46a --- /dev/null +++ b/src/newsreader/scss/index.scss @@ -0,0 +1,6 @@ +@import "lib/index"; +@import "partials/index"; +@import "components/index"; +@import "elements/index"; + +@import "pages/index"; diff --git a/src/newsreader/scss/lib/_css.gg.scss b/src/newsreader/scss/lib/_css.gg.scss new file mode 100644 index 0000000..389e533 --- /dev/null +++ b/src/newsreader/scss/lib/_css.gg.scss @@ -0,0 +1 @@ +@import "~css.gg/icons-scss/icons"; diff --git a/src/newsreader/scss/lib/index.scss b/src/newsreader/scss/lib/index.scss new file mode 100644 index 0000000..2aca0df --- /dev/null +++ b/src/newsreader/scss/lib/index.scss @@ -0,0 +1 @@ +@import "css.gg"; diff --git a/src/newsreader/scss/pages/categories/index.scss b/src/newsreader/scss/pages/categories/index.scss new file mode 100644 index 0000000..b683e0e --- /dev/null +++ b/src/newsreader/scss/pages/categories/index.scss @@ -0,0 +1,7 @@ +#categories--page { + & .card { + &__footer > *:last-child { + margin: 0 0 0 10px; + } + } +} diff --git a/src/newsreader/scss/pages/category/index.scss b/src/newsreader/scss/pages/category/index.scss new file mode 100644 index 0000000..ae62be1 --- /dev/null +++ b/src/newsreader/scss/pages/category/index.scss @@ -0,0 +1,2 @@ +#category--page { +} diff --git a/src/newsreader/scss/pages/homepage/index.scss b/src/newsreader/scss/pages/homepage/index.scss new file mode 100644 index 0000000..30f5a50 --- /dev/null +++ b/src/newsreader/scss/pages/homepage/index.scss @@ -0,0 +1,9 @@ +#homepage--page { + display: flex; + flex-direction: row; + align-items: initial; + width: 100%; + + margin: 20px 0 0 0; + background-color: initial; +} diff --git a/src/newsreader/scss/pages/import/index.scss b/src/newsreader/scss/pages/import/index.scss new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/newsreader/scss/pages/import/index.scss @@ -0,0 +1 @@ + diff --git a/src/newsreader/scss/pages/index.scss b/src/newsreader/scss/pages/index.scss new file mode 100644 index 0000000..872ac89 --- /dev/null +++ b/src/newsreader/scss/pages/index.scss @@ -0,0 +1,12 @@ +@import "categories/index"; +@import "category/index"; + +@import "import/index"; +@import "homepage/index"; + +@import "login/index"; +@import "password-reset/index"; +@import "register/index"; + +@import "rules/index"; +@import "rule/index"; diff --git a/src/newsreader/scss/pages/login/index.scss b/src/newsreader/scss/pages/login/index.scss new file mode 100644 index 0000000..69b946e --- /dev/null +++ b/src/newsreader/scss/pages/login/index.scss @@ -0,0 +1,6 @@ +#login--page { + margin: 5% auto; + width: 50%; + + border-radius: 4px; +} diff --git a/src/newsreader/scss/pages/password-reset/index.scss b/src/newsreader/scss/pages/password-reset/index.scss new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/newsreader/scss/pages/password-reset/index.scss @@ -0,0 +1 @@ + diff --git a/src/newsreader/scss/pages/register/index.scss b/src/newsreader/scss/pages/register/index.scss new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/newsreader/scss/pages/register/index.scss @@ -0,0 +1 @@ + diff --git a/src/newsreader/scss/pages/rule/index.scss b/src/newsreader/scss/pages/rule/index.scss new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/newsreader/scss/pages/rule/index.scss @@ -0,0 +1 @@ + diff --git a/src/newsreader/scss/pages/rules/index.scss b/src/newsreader/scss/pages/rules/index.scss new file mode 100644 index 0000000..68b92cb --- /dev/null +++ b/src/newsreader/scss/pages/rules/index.scss @@ -0,0 +1,7 @@ +#rules--page { + .list__item { + & .link { + margin: 0; + } + } +} diff --git a/src/newsreader/scss/partials/_colors.scss b/src/newsreader/scss/partials/_colors.scss new file mode 100644 index 0000000..664ddf7 --- /dev/null +++ b/src/newsreader/scss/partials/_colors.scss @@ -0,0 +1,38 @@ +$white: rgba(255, 255, 255, 1); +$black: rgba(0, 0, 0, 1); + +$dark: rgba(0, 0, 0, 0.4); + +// light blue +$azureish-white: rgba(205, 230, 245, 1); + +// dark blue +$pewter-blue: rgba(141, 167, 190, 1); + +// light gray +$gainsboro: rgba(238, 238, 238, 1); + +// medium gray +$roman-silver: rgba(135, 145, 158, 1); + +//dark gray +$nickel: rgba(112, 112, 120, 1); + +$pink: rgba(235, 229, 229, 1); +$lavendal-pink: rgba(162, 155, 254, 1); + +$beige: rgba(245, 245, 220, 1); + +$light-green: rgba(230, 247, 185, 1); +$light-orange: rgba(237, 212, 178, 1); +$light-red: rgba(255, 118, 117, 1); + +$success-green: rgba(46,204,113, 1); +$error-red: lighten(rgba(231, 76, 60, 1), 10%); + +$confirm-green: $success-green; +$cancel-red: $error-red; + +$border-gray: rgba(227, 227, 227, 1); +$focus-blue: darken($azureish-white, +50%); +$default-font-color: rgba(48, 51, 53, 1); diff --git a/src/newsreader/scss/partials/_fonts.scss b/src/newsreader/scss/partials/_fonts.scss new file mode 100644 index 0000000..31c5d56 --- /dev/null +++ b/src/newsreader/scss/partials/_fonts.scss @@ -0,0 +1,17 @@ +@import url("https://fonts.googleapis.com/css?family=Barlow&display=swap"); +@import url("https://fonts.googleapis.com/css?family=Oswald&display=swap"); +@import url("https://fonts.googleapis.com/css?family=Nunito&display=swap"); +@import url("https://fonts.googleapis.com/css?family=Noto+Sans&display=swap"); +@import url("https://fonts.googleapis.com/css?family=Noto+Serif&display=swap"); +@import url("https://fonts.googleapis.com/css?family=IBM+Plex+Sans:600&display=swap"); + +$default-font: "Noto Serif", serif; + +$button-font: "IBM Plex Sans", sans-serif; +$form-font: "Barlow", sans-serif; + +$article-font: "Noto Serif", serif; +$article-header-font: "Oswald", sans-serif; + +$header-font: "Noto Sans", sans-serif; +$sidebar-font: "Nunito", sans-serif; diff --git a/src/newsreader/scss/partials/index.scss b/src/newsreader/scss/partials/index.scss new file mode 100644 index 0000000..24bbbd0 --- /dev/null +++ b/src/newsreader/scss/partials/index.scss @@ -0,0 +1,2 @@ +@import "fonts"; +@import "colors"; diff --git a/src/newsreader/static/favicon.png b/src/newsreader/static/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..ea8855cb06e91f3e451fc597f2211c4ba1917e84 GIT binary patch literal 1913 zcmV-<2Zs2GP) zaB^>EX>4U6ba`-PAZ2)IW&i+q+QnB}lIti8{nsjH2}mFW%fURV?ha=8bFfJ|=}vc? z&Qx7fJS>DH&Jo0+{_meb|KJhMj)Gc>IYx&kA^Ybt&$l ztQy;{c{MWM=~fU7C^dy6FVa1Gx3}qtz#p}sdbHwo>S4b4;1i)^f7vSlx!c|qKz9q! z9mtmveMbK}`UJj|CHJ4Oj0+{pQ_Y{^?f_rPqTk)*4^kBj>JD?l28-Jc6Pxl!Zuy;O4?RVH zoC+pu%K#vPZ$&Wd!GMcMc#iB$#F&9&0zn0lix0Aa0alfUASHMog-nu6-&kY3>xxR0 zDaPFdBGgB3;3oS5vO-YgkA;LBD(W?;YE;uinzck9L(~|f#d*mU^A=1knpv`JCHWMR zrkE_HlvBx)J?=owFXj1ooFh`*7D{l{2Yx8%m?u;F$aAN8_P9JoNBtmr#&Aa_uhJ3C2pp4om zj8dL1Xucl{nqa5;_aizJ;optuQH1mi#Fyy54e_@T(ph?6!swa7pCYGs6Y(vKK0ENW z$zrYIu;bG4yXJ;l;^myA6F8nClAgeE)}W`8cs?~qFXLL{GT7ZH_*~oH5>rwRIzO~O z_1yEA1GS!m)$*O*r6=y{*8&aJ@D0-D3@-KXZ0(&dg0tKsF9Ci?@a6xTAn6B>9|lf1 zq8~iI6e&Fjm4CCEXhcV7|rZ4Fm6i{Vhh%PTPt8cZTq{t7Y7Z{{bmR3m#FmhEo6l0flKpLr_UWLm+T+ zZ)Rz1WdHzpoPCi!NW(xJ#a~-Rk&1&ER2%|UCkvt?f{Rw62o*xD(5i#UrC-pbAxUv@ z6kH1qek@iUT%2`va1{i>4-h9uCq)-2@qbC7MT`f>{djlparX`g)=EsX+QtA)x6Ncc zCZ;p1V#g~&=!bwI%*f0#<|HWr-|=;i0AKH-Jj?&wpQBsNS_}w?#IwvWZQ>2$sZHD9 zyiY8!f~*ps6OWp7LE=ZQD;~db&bu7snSz;0&Jzp7V!nly7G?!gBc3FVsG3gs!L-LJ z=Pk}!slpof#ebm(+40$T_tXSgF|2> zPuc4}@9u2v?cXzv{(b<=xpK>D9qEn$000JJOGiWi{{a60|De66lK=n!32;bRa{vGq zApigtApy3pqv8Mn00(qQO+^Rf0~r)B5rt`tTmS$8n@L1LR5;6}(?N(&Q547V&-*th zO;=ec@kWxcAvTO+VPldF-KMN8l(EtzJ9|n=QEEmkY*4R;#Wb=}NR*TkQmAl z{@L(s?w{A+-=yYj?(Lj&zxSNqy(g>d+G4B^sv^IRt@w!NmsJ7%a)zMvlJVuiu_?d6`39;YUlrnX8?TdZ z*cAS3!U4N4yMTN1a00000NkvXXu0mjfIjfc+ literal 0 HcmV?d00001 diff --git a/src/newsreader/templates/base.html b/src/newsreader/templates/base.html new file mode 100644 index 0000000..42d438b --- /dev/null +++ b/src/newsreader/templates/base.html @@ -0,0 +1,45 @@ +{% load static %} + + + + + Newreader + + {% block head %} + + {% endblock %} + + + + + + {% if messages %} +
      + {% for message in messages %} +
    • + {{ message }} +
    • + {% endfor %} +
    + {% endif %} + + {% block content %}{% endblock content %} + + + {% block scripts %} + + {% endblock %} + diff --git a/src/newsreader/templates/password-reset/password_reset_complete.html b/src/newsreader/templates/password-reset/password_reset_complete.html new file mode 100755 index 0000000..8a47f55 --- /dev/null +++ b/src/newsreader/templates/password-reset/password_reset_complete.html @@ -0,0 +1,23 @@ +{% extends "base.html" %} +{% load static i18n %} + +{% block title %}{% trans "Password reset complete" %}{% endblock %} + +{% block content %} +
    +
    +
    +

    {% trans "Password reset complete" %}

    +
    +
    +

    + {% trans "Your password has been reset!" %} + {% blocktrans %} + You may now log in + {% endblocktrans %}. +

    +
    + +
    +{% endblock %} diff --git a/src/newsreader/templates/password-reset/password_reset_confirm.html b/src/newsreader/templates/password-reset/password_reset_confirm.html new file mode 100755 index 0000000..c438971 --- /dev/null +++ b/src/newsreader/templates/password-reset/password_reset_confirm.html @@ -0,0 +1,55 @@ +{% extends "base.html" %} +{% load static i18n %} + +{% block meta %} + + +{% endblock %} + +{% block title %}{% trans "Confirm password reset" %}{% endblock %} + +{% block content %} +
    + + {% if validlink %} +
    + {% csrf_token %} +
    +

    + {% trans "Enter your new password below to reset your password:" %} +

    +
    + +
    + {{ form }} +
    +
    + Cancel + +
    +
    + + {% else %} +
    +
    +

    {% trans "Password reset unsuccessful" %}

    +
    +
    +

    + {% url 'accounts:password-reset' as reset_url %} + {% blocktrans %} + Password reset unsuccessful. Please + try again. + {% endblocktrans %} +

    +
    + + {% endif %} + +
    +{% endblock %} + + +{# This is used by django.contrib.auth #} diff --git a/src/newsreader/templates/password-reset/password_reset_done.html b/src/newsreader/templates/password-reset/password_reset_done.html new file mode 100755 index 0000000..dfa141c --- /dev/null +++ b/src/newsreader/templates/password-reset/password_reset_done.html @@ -0,0 +1,23 @@ +{% extends "base.html" %} +{% load static i18n %} + +{% block title %}{% trans "Password reset" %}{% endblock %} + +{% block content %} +
    +
    +
    +

    {% trans "Password reset" %}

    +
    +
    +

    + {% blocktrans %} + We have sent you an email with a link to reset your password. Please check + your email and click the link to continue. + {% endblocktrans %} +

    +
    + +
    +{% endblock %} diff --git a/src/newsreader/templates/password-reset/password_reset_email.html b/src/newsreader/templates/password-reset/password_reset_email.html new file mode 100755 index 0000000..69844e8 --- /dev/null +++ b/src/newsreader/templates/password-reset/password_reset_email.html @@ -0,0 +1,30 @@ +{% load i18n %} + +{% blocktrans %}Greetings{% endblocktrans %} {% if user.get_full_name %}{{ user.get_full_name }}{% else %}{{ user }}{% endif %}, + +{% blocktrans %} +You are receiving this email because you (or someone pretending to be you) +requested that your password be reset on the {{ domain }} site. If you do not +wish to reset your password, please ignore this message. +{% endblocktrans %} + +{% blocktrans %} +To reset your password, please click the following link, or copy and paste it +into your web browser: +{% endblocktrans %} + + + {{ protocol }}://{{ domain }}{% url 'accounts:password-reset-confirm' uid token %} + + +{% blocktrans %} + Your username, in case you've forgotten: +{% endblocktrans %} {{ user.get_username }} + +{% blocktrans %} + Best regards +{% endblocktrans %}, +{{ site_name }} +{% blocktrans %} + Management +{% endblocktrans %} diff --git a/src/newsreader/templates/password-reset/password_reset_form.html b/src/newsreader/templates/password-reset/password_reset_form.html new file mode 100755 index 0000000..cd5fc3e --- /dev/null +++ b/src/newsreader/templates/password-reset/password_reset_form.html @@ -0,0 +1,30 @@ +{% extends "base.html" %} +{% load static i18n %} + +{% block title %}{% trans "Reset password" %}{% endblock %} + +{% block content %} +
    +
    + {% csrf_token %} +
    +

    {% trans "Reset password" %}

    + +

    + {% blocktrans %} + Forgot your password? Enter your email in the form below and we'll send you + instructions for creating a new one. + {% endblocktrans %} +

    +
    + +
    + {{ form }} +
    +
    + Cancel + +
    +
    +
    +{% endblock %} diff --git a/src/newsreader/templates/password-reset/password_reset_subject.txt b/src/newsreader/templates/password-reset/password_reset_subject.txt new file mode 100644 index 0000000..bbf2b7e --- /dev/null +++ b/src/newsreader/templates/password-reset/password_reset_subject.txt @@ -0,0 +1,6 @@ +{% load i18n %}{% trans "Password reset on" %} {{ site_name }} + +{% comment %} +See the save method in the PasswordChangeForm in django/contrib/auth/forms.py +for the available context data +{% endcomment %} diff --git a/src/newsreader/templates/registration/activation_complete.html b/src/newsreader/templates/registration/activation_complete.html new file mode 100755 index 0000000..61ea493 --- /dev/null +++ b/src/newsreader/templates/registration/activation_complete.html @@ -0,0 +1,31 @@ +{% extends "base.html" %} +{% load static i18n %} + +{% block title %}{% trans "Account Activated" %}{% endblock %} + +{% comment %} +**registration/activation_complete.html** + +Used after successful account activation. This template has no context +variables of its own, and should simply inform the user that their +account is now active. +{% endcomment %} + +{% block content %} +
    +
    +
    +

    {% trans "Account activated" %}

    +
    +
    +

    + {% trans "Your account is now activated." %} + {% if not user.is_authenticated %} + {% trans "You can log in." %} + {% endif %} +

    +
    + +
    +{% endblock %} diff --git a/src/newsreader/templates/registration/activation_email.html b/src/newsreader/templates/registration/activation_email.html new file mode 100644 index 0000000..8be4421 --- /dev/null +++ b/src/newsreader/templates/registration/activation_email.html @@ -0,0 +1,72 @@ +{% load i18n %} + + + + + {{ site.name }} {% trans "registration" %} + + + +

    + {% blocktrans with site_name=site.name %} + You (or someone pretending to be you) have asked to register an account at + {{ site_name }}. If this wasn't you, please ignore this email + and your address will be removed from our records. + {% endblocktrans %} +

    +

    + {% blocktrans %} + To activate this account, please click the following link within the next + {{ expiration_days }} days: + {% endblocktrans %} +

    + +

    + + {{site.domain}}{% url 'accounts:activate' activation_key %} + +

    +

    + {% blocktrans with site_name=site.name %} + Sincerely, + {{ site_name }} Management + {% endblocktrans %} +

    + + + + + +{% comment %} +**registration/activation_email.html** + +Used to generate the html body of the activation email. Should display a +link the user can click to activate the account. This template has the +following context: + +``activation_key`` + The activation key for the new account. + +``expiration_days`` + The number of days remaining during which the account may be + activated. + +``site`` + An object representing the site on which the user registered; + depending on whether ``django.contrib.sites`` is installed, this + may be an instance of either ``django.contrib.sites.models.Site`` + (if the sites application is installed) or + ``django.contrib.sites.requests.RequestSite`` (if not). Consult `the + documentation for the Django sites framework + `_ for + details regarding these objects' interfaces. + +``user`` + The new user account + +``request`` + ``HttpRequest`` instance for better flexibility. + For example it can be used to compute absolute register URL: + + {{ request.scheme }}://{{ request.get_host }}{% url 'registration_activate' activation_key %} +{% endcomment %} diff --git a/src/newsreader/templates/registration/activation_email.txt b/src/newsreader/templates/registration/activation_email.txt new file mode 100644 index 0000000..7f52a60 --- /dev/null +++ b/src/newsreader/templates/registration/activation_email.txt @@ -0,0 +1,52 @@ +{% load i18n %} +{% blocktrans with site_name=site.name %} +You (or someone pretending to be you) have asked to register an account at +{{ site_name }}. If this wasn't you, please ignore this email +and your address will be removed from our records. +{% endblocktrans %} +{% blocktrans %} +To activate this account, please click the following link within the next +{{ expiration_days }} days: +{% endblocktrans %} + +http://{{site.domain}}{% url 'accounts:activate' activation_key %} + +{% blocktrans with site_name=site.name %} +Sincerely, +{{ site_name }} Management +{% endblocktrans %} + + +{% comment %} +**registration/activation_email.txt** + +Used to generate the text body of the activation email. Should display a +link the user can click to activate the account. This template has the +following context: + +``activation_key`` + The activation key for the new account. + +``expiration_days`` + The number of days remaining during which the account may be + activated. + +``site`` + An object representing the site on which the user registered; + depending on whether ``django.contrib.sites`` is installed, this + may be an instance of either ``django.contrib.sites.models.Site`` + (if the sites application is installed) or + ``django.contrib.sites.requests.RequestSite`` (if not). Consult `the + documentation for the Django sites framework + `_ for + details regarding these objects' interfaces. + +``user`` + The new user account + +``request`` + ``HttpRequest`` instance for better flexibility. + For example it can be used to compute absolute register URL: + + {{ request.scheme }}://{{ request.get_host }}{% url 'registration_activate' activation_key %} +{% endcomment %} diff --git a/src/newsreader/templates/registration/activation_email_subject.txt b/src/newsreader/templates/registration/activation_email_subject.txt new file mode 100644 index 0000000..da0ddeb --- /dev/null +++ b/src/newsreader/templates/registration/activation_email_subject.txt @@ -0,0 +1,28 @@ +{% load i18n %}{% trans "Account activation on" %} {{ site.name }} + + +{% comment %} +**registration/activation_email_subject.txt** + +Used to generate the subject line of the activation email. Because the +subject line of an email must be a single line of text, any output +from this template will be forcibly condensed to a single line before +being used. This template has the following context: + +``activation_key`` + The activation key for the new account. + +``expiration_days`` + The number of days remaining during which the account may be + activated. + +``site`` + An object representing the site on which the user registered; + depending on whether ``django.contrib.sites`` is installed, this + may be an instance of either ``django.contrib.sites.models.Site`` + (if the sites application is installed) or + ``django.contrib.sites.requests.RequestSite`` (if not). Consult `the + documentation for the Django sites framework + `_ for + details regarding these objects' interfaces. +{% endcomment %} diff --git a/src/newsreader/templates/registration/activation_failure.html b/src/newsreader/templates/registration/activation_failure.html new file mode 100644 index 0000000..5cf0f67 --- /dev/null +++ b/src/newsreader/templates/registration/activation_failure.html @@ -0,0 +1,27 @@ +{% extends "base.html" %} +{% load static i18n %} + +{% block title %}{% trans "Activation Failure" %}{% endblock %} + +{% comment %} +**registration/activate.html** + +Used if account activation fails. With the default setup, has the following context: + +``activation_key`` + The activation key used during the activation attempt. +{% endcomment %} + +{% block content %} +
    +
    +
    +

    {% trans "Activation Failure" %}

    +
    +
    +

    {% trans "Account activation failed." %}

    +
    + +
    +{% endblock %} diff --git a/src/newsreader/templates/registration/activation_resend_complete.html b/src/newsreader/templates/registration/activation_resend_complete.html new file mode 100644 index 0000000..dcf1e79 --- /dev/null +++ b/src/newsreader/templates/registration/activation_resend_complete.html @@ -0,0 +1,31 @@ +{% extends "base.html" %} +{% load static i18n %} + +{% block title %}{% trans "Account Activation Resent" %}{% endblock %} + +{% comment %} +**registration/resend_activation_complete.html** +Used after form for resending account activation is submitted. By default has +the following context: + +``email`` + The email address submitted in the resend activation form. +{% endcomment %} + +{% block content %} +
    +
    +
    +

    {% trans "Account activation resent" %}

    +
    +
    +

    + {% blocktrans %} + We have sent an email to {{ email }} with further instructions. + {% endblocktrans %} +

    +
    + +
    +{% endblock %} diff --git a/src/newsreader/templates/registration/activation_resend_form.html b/src/newsreader/templates/registration/activation_resend_form.html new file mode 100644 index 0000000..f721242 --- /dev/null +++ b/src/newsreader/templates/registration/activation_resend_form.html @@ -0,0 +1,35 @@ +{% extends "base.html" %} +{% load static i18n %} + +{% block title %}{% trans "Resend Activation Email" %}{% endblock %} + +{% comment %} +**registration/resend_activation_form.html** +Used to show the form users will fill out to resend the activation email. By +default, has the following context: + +``form`` + The registration form. This will be an instance of some subclass + of ``django.forms.Form``; consult `Django's forms documentation + `_ for + information on how to display this in a template. +{% endcomment %} + +{% block content %} +
    +
    + {% csrf_token %} +
    +

    Resend activation code

    +
    + +
    + {{ form }} +
    +
    + Cancel + +
    +
    +
    +{% endblock %} diff --git a/src/newsreader/templates/registration/registration_closed.html b/src/newsreader/templates/registration/registration_closed.html new file mode 100755 index 0000000..6169ebe --- /dev/null +++ b/src/newsreader/templates/registration/registration_closed.html @@ -0,0 +1,20 @@ +{% extends "base.html" %} +{% load static i18n %} + +{% block title %}{% trans "Registration is closed" %}{% endblock %} + +{% block content %} +
    +
    +
    +

    {% trans "Registration is closed" %}

    +
    +
    +

    + {% trans "Sorry, but registration is closed at this moment. Come back later." %} +

    +
    + +
    +{% endblock %} diff --git a/src/newsreader/templates/registration/registration_complete.html b/src/newsreader/templates/registration/registration_complete.html new file mode 100755 index 0000000..cc5f868 --- /dev/null +++ b/src/newsreader/templates/registration/registration_complete.html @@ -0,0 +1,29 @@ +{% extends "base.html" %} +{% load static i18n %} + +{% block title %}{% trans "Activation email sent" %}{% endblock %} + +{% comment %} +**registration/registration_complete.html** + +Used after successful completion of the registration form. This +template has no context variables of its own, and should simply inform +the user that an email containing account-activation information has +been sent. +{% endcomment %} + +{% block content %} +
    +
    +
    +

    {% trans "Activation email sent" %}

    +
    +
    +

    + {% trans "Please check your email to complete the registration process." %} +

    +
    + +
    +{% endblock %} diff --git a/src/newsreader/templates/registration/registration_form.html b/src/newsreader/templates/registration/registration_form.html new file mode 100644 index 0000000..9b8619c --- /dev/null +++ b/src/newsreader/templates/registration/registration_form.html @@ -0,0 +1,22 @@ +{% extends "base.html" %} + +{% load static %} + +{% block content %} +
    +
    + {% csrf_token %} +
    +

    Register

    +
    + +
    + {{ form }} +
    +
    + Cancel + +
    +
    +
    +{% endblock %} diff --git a/src/newsreader/urls.py b/src/newsreader/urls.py new file mode 100644 index 0000000..3b01563 --- /dev/null +++ b/src/newsreader/urls.py @@ -0,0 +1,37 @@ +from django.conf import settings +from django.contrib import admin +from django.urls import include, path + +from drf_yasg import openapi +from drf_yasg.views import get_schema_view + +from newsreader.accounts.urls import urlpatterns as login_urls +from newsreader.news.collection.urls import endpoints as collection_endpoints +from newsreader.news.collection.urls import urlpatterns as collection_patterns +from newsreader.news.core.urls import endpoints as core_endpoints +from newsreader.news.core.urls import urlpatterns as core_patterns + + +apipatterns = [ + path("api/", include(core_endpoints)), + path("api/", include(collection_endpoints)), +] + +schema_info = openapi.Info(title="Newsreader API", default_version="v1") +schema_view = get_schema_view(schema_info, patterns=apipatterns) + +urlpatterns = [ + path("", include(core_patterns)), + path("", include(collection_patterns)), + path("", include((apipatterns, "api")), name="api"), + path("accounts/", include((login_urls, "accounts")), name="accounts"), + path("admin/", admin.site.urls, name="admin"), + path("api/", schema_view.with_ui("swagger"), name="api"), + path("api/auth/", include("rest_framework.urls"), name="rest_framework"), +] + + +if settings.DEBUG: + import debug_toolbar + + urlpatterns = [path("debug/", include(debug_toolbar.urls))] + urlpatterns diff --git a/src/newsreader/utils/celery.py b/src/newsreader/utils/celery.py new file mode 100644 index 0000000..84572c6 --- /dev/null +++ b/src/newsreader/utils/celery.py @@ -0,0 +1,22 @@ +from django.core.cache import cache + +from celery.five import monotonic + + +LOCK_EXPIRE = 60 * 10 # 10 minutes + + +class MemCacheLock: + def __init__(self, lock_id, oid): + self.lock_id = lock_id + self.oid = oid + + self.timeout_at = monotonic() + LOCK_EXPIRE - 3 + + def __enter__(self): + self.status = cache.add(self.lock_id, self.oid, LOCK_EXPIRE) + return self.status + + def __exit__(self, *args, **kwargs): + if monotonic() < self.timeout_at and self.status: + cache.delete(self.lock_id) diff --git a/src/newsreader/utils/opml.py b/src/newsreader/utils/opml.py new file mode 100644 index 0000000..55a9387 --- /dev/null +++ b/src/newsreader/utils/opml.py @@ -0,0 +1,41 @@ +import logging + +from django.core.exceptions import ValidationError +from django.core.validators import URLValidator + +from lxml.etree import XMLSyntaxError, parse + +from newsreader.news.collection.models import CollectionRule + + +def parse_opml(file, user, skip_existing=False): + known_urls = CollectionRule.objects.filter(user=user).values_list("url", flat=True) + + try: + tree = parse(file) + except XMLSyntaxError as e: + raise IOError("Invalid file") from e + + root = tree.getroot() + + validate = URLValidator(schemes=["http", "https"]) + + for element in root.iter(tag="outline"): + if not "xmlUrl" in element.keys(): + continue + + feed_url = element.get("xmlUrl") + name = element.get("text") + + if not all([feed_url, name]): + continue + elif skip_existing and feed_url in known_urls: + continue + + try: + validate(feed_url) + except ValidationError as e: + logging.info(f"Skipped due to invalid URL: {e}") + continue + + yield CollectionRule(url=feed_url, name=name, user=user) diff --git a/src/newsreader/utils/tests/__init__.py b/src/newsreader/utils/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/newsreader/utils/tests/files/empty-feeds.opml b/src/newsreader/utils/tests/files/empty-feeds.opml new file mode 100644 index 0000000..34ad8f2 --- /dev/null +++ b/src/newsreader/utils/tests/files/empty-feeds.opml @@ -0,0 +1,9 @@ + + + + Sat, 18 Jan 2020 17:32:03 +0000 + Tiny Tiny RSS Feed Export + + + + diff --git a/src/newsreader/utils/tests/files/feeds.opml b/src/newsreader/utils/tests/files/feeds.opml new file mode 100644 index 0000000..2cc14cf --- /dev/null +++ b/src/newsreader/utils/tests/files/feeds.opml @@ -0,0 +1,15 @@ + + + + Sat, 18 Jan 2020 17:32:03 +0000 + Tiny Tiny RSS Feed Export + + + + + + + + + + diff --git a/src/newsreader/utils/tests/files/invalid-url-feeds.opml b/src/newsreader/utils/tests/files/invalid-url-feeds.opml new file mode 100644 index 0000000..aad9a7f --- /dev/null +++ b/src/newsreader/utils/tests/files/invalid-url-feeds.opml @@ -0,0 +1,16 @@ + + + + Sat, 18 Jan 2020 17:32:03 +0000 + Tiny Tiny RSS Feed Export + + + + + + + + + + + diff --git a/src/newsreader/utils/tests/files/missing-feeds.opml b/src/newsreader/utils/tests/files/missing-feeds.opml new file mode 100644 index 0000000..22ac9a3 --- /dev/null +++ b/src/newsreader/utils/tests/files/missing-feeds.opml @@ -0,0 +1,17 @@ + + + + Sat, 18 Jan 2020 17:32:03 +0000 + Tiny Tiny RSS Feed Export + + + + + + + + + + + + diff --git a/src/newsreader/utils/tests/files/test.png b/src/newsreader/utils/tests/files/test.png new file mode 100644 index 0000000000000000000000000000000000000000..848a7ef196174c8d710428447b6825f0ef03ad5e GIT binary patch literal 81814 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 Ue#kfMMn8)`t9B;il=1EV17%&%cK`qY literal 0 HcmV?d00001 diff --git a/src/newsreader/utils/tests/test_opml.py b/src/newsreader/utils/tests/test_opml.py new file mode 100644 index 0000000..d2baf8f --- /dev/null +++ b/src/newsreader/utils/tests/test_opml.py @@ -0,0 +1,46 @@ +import os + +from pathlib import Path + +from django.test import TestCase + +from newsreader.accounts.tests.factories import UserFactory +from newsreader.utils.opml import parse_opml + + +class OPMImportTestCase(TestCase): + def setUp(self): + self.directory = Path(__file__).parent.absolute() + self.user = UserFactory() + + def test_simple(self): + path = os.path.join(self.directory, "files", "feeds.opml") + + with open(path, "r") as file: + rules = list(parse_opml(file, self.user)) + + self.assertEquals(len(rules), 4) + + def test_file_without_feeds(self): + path = os.path.join(self.directory, "files", "empty-feeds.opml") + + with open(path, "r") as file: + rules = list(parse_opml(file, self.user)) + + self.assertEquals(len(rules), 0) + + def test_file_with_missing_rule_properties(self): + path = os.path.join(self.directory, "files", "missing-feeds.opml") + + with open(path, "r") as file: + rules = list(parse_opml(file, self.user)) + + self.assertEquals(len(rules), 2) + + def test_url_validation(self): + path = os.path.join(self.directory, "files", "invalid-url-feeds.opml") + + with open(path, "r") as file: + rules = list(parse_opml(file, self.user)) + + self.assertEquals(len(rules), 0) diff --git a/src/newsreader/wsgi.py b/src/newsreader/wsgi.py new file mode 100644 index 0000000..5872a04 --- /dev/null +++ b/src/newsreader/wsgi.py @@ -0,0 +1,17 @@ +""" +WSGI config for newsreader project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "newsreader.conf.production") + +application = get_wsgi_application() diff --git a/webpack.common.babel.js b/webpack.common.babel.js new file mode 100644 index 0000000..2c8471c --- /dev/null +++ b/webpack.common.babel.js @@ -0,0 +1,35 @@ +import { resolve } from 'path'; +import { CleanWebpackPlugin } from 'clean-webpack-plugin'; +import MiniCssExtractPlugin from 'mini-css-extract-plugin'; + +export default { + entry: { + main: ['./src/newsreader/js/index.js', './src/newsreader/scss/index.scss'], + }, + output: { + path: resolve(__dirname, 'src', 'newsreader', 'static'), + filename: 'js/[name].bundle.js', + }, + module: { + rules: [ + { + test: /\.(js|jsx)$/, + exclude: /node_modules/, + use: { loader: 'babel-loader' }, + }, + { + test: /\.(sass|scss)$/, + use: [{ loader: MiniCssExtractPlugin.loader }, 'css-loader', 'sass-loader'], + }, + ], + }, + plugins: [ + new MiniCssExtractPlugin({ + filename: 'css/main.css', + allChunks: true, + }), + new CleanWebpackPlugin({ + cleanOnceBeforeBuildPatterns: ['**/*', '!favicon.png'], + }), + ], +}; diff --git a/webpack.dev.babel.js b/webpack.dev.babel.js new file mode 100644 index 0000000..32c7fd0 --- /dev/null +++ b/webpack.dev.babel.js @@ -0,0 +1,7 @@ +import merge from 'webpack-merge'; +import common from './webpack.common.babel.js'; + +export default merge(common, { + mode: 'development', + devtool: 'inline-source-map', +}); diff --git a/webpack.prod.babel.js b/webpack.prod.babel.js new file mode 100644 index 0000000..6f1c942 --- /dev/null +++ b/webpack.prod.babel.js @@ -0,0 +1,4 @@ +import merge from 'webpack-merge'; +import common from './webpack.common.babel.js'; + +export default merge(common, { mode: 'production' });