From abacb72b3018a1e0cc4635d6cf3eb1a02b8b83ba Mon Sep 17 00:00:00 2001 From: Sonny Date: Sun, 7 Apr 2019 15:34:33 +0000 Subject: [PATCH 001/364] Add gitignore --- .gitignore | 193 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8cab59f --- /dev/null +++ b/.gitignore @@ -0,0 +1,193 @@ + +# 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 +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/ +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/ +.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: + +# 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 From c508cca08016076a68f6f3f17b6773858d7fd296 Mon Sep 17 00:00:00 2001 From: Sonny Date: Sat, 22 Jun 2019 09:29:56 +0000 Subject: [PATCH 002/364] Merge first implementation --- .gitignore | 1 + .isort.cfg | 7 + requirements/base.txt | 12 + requirements/dev.txt | 4 + src/manage.py | 21 + src/newsreader/__init__.py | 0 src/newsreader/conf/__init__.py | 0 src/newsreader/conf/base.py | 111 +++ src/newsreader/conf/dev.py | 12 + src/newsreader/core/__init__.py | 0 src/newsreader/core/admin.py | 3 + src/newsreader/core/apps.py | 5 + src/newsreader/core/migrations/__init__.py | 0 src/newsreader/core/models.py | 13 + src/newsreader/core/tests.py | 3 + src/newsreader/core/views.py | 3 + src/newsreader/news/__init__.py | 0 src/newsreader/news/collection/__init__.py | 0 src/newsreader/news/collection/admin.py | 23 + src/newsreader/news/collection/apps.py | 5 + src/newsreader/news/collection/base.py | 81 ++ src/newsreader/news/collection/exceptions.py | 28 + src/newsreader/news/collection/feed.py | 211 +++++ .../collection/management/commands/collect.py | 14 + .../collection/migrations/0001_initial.py | 31 + .../migrations/0002_auto_20190410_2028.py | 18 + .../0003_collectionrule_category.py | 28 + .../0004_collectionrule_timezone.py | 517 +++++++++++ .../migrations/0005_auto_20190521_1941.py | 24 + .../migrations/0006_collectionrule_error.py | 18 + .../news/collection/migrations/__init__.py | 0 src/newsreader/news/collection/models.py | 35 + .../news/collection/response_handler.py | 30 + .../news/collection/tests/__init__.py | 1 + .../news/collection/tests/factories.py | 12 + .../news/collection/tests/feed/__init__.py | 5 + .../collection/tests/feed/builder/__init__.py | 1 + .../tests/feed/builder/mock_html.py | 10 + .../collection/tests/feed/builder/mocks.py | 878 ++++++++++++++++++ .../collection/tests/feed/builder/tests.py | 304 ++++++ .../collection/tests/feed/client/__init__.py | 1 + .../collection/tests/feed/client/mocks.py | 61 ++ .../collection/tests/feed/client/tests.py | 90 ++ .../tests/feed/collector/__init__.py | 1 + .../collection/tests/feed/collector/mocks.py | 430 +++++++++ .../collection/tests/feed/collector/tests.py | 251 +++++ .../tests/feed/duplicate_handler/__init__.py | 1 + .../tests/feed/duplicate_handler/tests.py | 63 ++ .../collection/tests/feed/stream/__init__.py | 1 + .../collection/tests/feed/stream/mocks.py | 61 ++ .../collection/tests/feed/stream/tests.py | 109 +++ src/newsreader/news/collection/utils.py | 13 + src/newsreader/news/collection/views.py | 3 + src/newsreader/news/posts/__init__.py | 0 src/newsreader/news/posts/admin.py | 39 + src/newsreader/news/posts/apps.py | 5 + .../news/posts/migrations/0001_initial.py | 78 ++ .../migrations/0002_auto_20190520_2206.py | 20 + .../migrations/0003_auto_20190520_2031.py | 18 + .../migrations/0004_auto_20190521_1941.py | 22 + .../migrations/0005_auto_20190608_1054.py | 23 + .../migrations/0006_auto_20190608_1520.py | 33 + .../news/posts/migrations/__init__.py | 0 src/newsreader/news/posts/models.py | 34 + src/newsreader/news/posts/tests/__init__.py | 0 src/newsreader/news/posts/tests/factories.py | 28 + src/newsreader/news/posts/views.py | 3 + src/newsreader/urls.py | 6 + src/newsreader/utils/formatter.sh | 10 + src/newsreader/utils/pre-commit | 13 + src/newsreader/wsgi.py | 16 + 71 files changed, 3902 insertions(+) create mode 100644 .isort.cfg create mode 100644 requirements/base.txt create mode 100644 requirements/dev.txt create mode 100755 src/manage.py create mode 100644 src/newsreader/__init__.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/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/tests.py create mode 100644 src/newsreader/core/views.py 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/exceptions.py create mode 100644 src/newsreader/news/collection/feed.py create mode 100644 src/newsreader/news/collection/management/commands/collect.py create mode 100644 src/newsreader/news/collection/migrations/0001_initial.py create mode 100644 src/newsreader/news/collection/migrations/0002_auto_20190410_2028.py create mode 100644 src/newsreader/news/collection/migrations/0003_collectionrule_category.py create mode 100644 src/newsreader/news/collection/migrations/0004_collectionrule_timezone.py create mode 100644 src/newsreader/news/collection/migrations/0005_auto_20190521_1941.py create mode 100644 src/newsreader/news/collection/migrations/0006_collectionrule_error.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/tests/__init__.py create mode 100644 src/newsreader/news/collection/tests/factories.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/utils.py create mode 100644 src/newsreader/news/collection/views.py create mode 100644 src/newsreader/news/posts/__init__.py create mode 100644 src/newsreader/news/posts/admin.py create mode 100644 src/newsreader/news/posts/apps.py create mode 100644 src/newsreader/news/posts/migrations/0001_initial.py create mode 100644 src/newsreader/news/posts/migrations/0002_auto_20190520_2206.py create mode 100644 src/newsreader/news/posts/migrations/0003_auto_20190520_2031.py create mode 100644 src/newsreader/news/posts/migrations/0004_auto_20190521_1941.py create mode 100644 src/newsreader/news/posts/migrations/0005_auto_20190608_1054.py create mode 100644 src/newsreader/news/posts/migrations/0006_auto_20190608_1520.py create mode 100644 src/newsreader/news/posts/migrations/__init__.py create mode 100644 src/newsreader/news/posts/models.py create mode 100644 src/newsreader/news/posts/tests/__init__.py create mode 100644 src/newsreader/news/posts/tests/factories.py create mode 100644 src/newsreader/news/posts/views.py create mode 100644 src/newsreader/urls.py create mode 100644 src/newsreader/utils/formatter.sh create mode 100644 src/newsreader/utils/pre-commit create mode 100644 src/newsreader/wsgi.py diff --git a/.gitignore b/.gitignore index 8cab59f..8d9e86a 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ *.pyc __pycache__/ local_settings.py +local.py db.sqlite3 media diff --git a/.isort.cfg b/.isort.cfg new file mode 100644 index 0000000..e453b8d --- /dev/null +++ b/.isort.cfg @@ -0,0 +1,7 @@ +[settings] +include_trailing_comma = true +line_length = 80 +multi_line_output = 3 +skip = env/ +forced_separate=django, newsreader +lines_between_types=1 diff --git a/requirements/base.txt b/requirements/base.txt new file mode 100644 index 0000000..0f82f61 --- /dev/null +++ b/requirements/base.txt @@ -0,0 +1,12 @@ +certifi==2019.3.9 +chardet==3.0.4 +Django==2.2 +feedparser==5.2.1 +idna==2.8 +pkg-resources==0.0.0 +pytz==2018.9 +requests==2.21.0 +sqlparse==0.3.0 +urllib3==1.24.1 +psycopg2-binary==2.8.1 +Pillow==6.0.0 diff --git a/requirements/dev.txt b/requirements/dev.txt new file mode 100644 index 0000000..0a685e3 --- /dev/null +++ b/requirements/dev.txt @@ -0,0 +1,4 @@ +-r base.txt + +factory-boy==2.12.0 +freezegun==0.3.12 diff --git a/src/manage.py b/src/manage.py new file mode 100755 index 0000000..a30fa10 --- /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.base') + 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..e69de29 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..4b9f9df --- /dev/null +++ b/src/newsreader/conf/base.py @@ -0,0 +1,111 @@ +""" +Django settings for newsreader project. + +Generated by "django-admin startproject" using Django 2.2. + +For more information on this file, see +https://docs.djangoproject.com/en/2.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/2.2/ref/settings/ +""" + +import os + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = "^!7a2jq5j!exc-55vf$anx9^6ff6=u_ub5=5p1(1x47fix)syh" + +# SECURITY WARNING: don"t run with debug turned on in production! +DEBUG = False + +ALLOWED_HOSTS = ["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", + # app modules + "newsreader.news.collection", + "newsreader.news.posts", +] + +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", +] + +ROOT_URLCONF = "newsreader.urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] + +WSGI_APPLICATION = "newsreader.wsgi.application" + +# Database +# https://docs.djangoproject.com/en/2.2/ref/settings/#databases +DATABASES = { + "default": { + "ENGINE": "django.db.backends.postgresql_psycopg2", + "NAME": "newsreader", + "USER": "newsreader", + } +} + +# 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", + }, +] + +# Internationalization +# https://docs.djangoproject.com/en/2.2/topics/i18n/ +LANGUAGE_CODE = "en-us" + +TIME_ZONE = "UTC" +USE_I18N = True +USE_L10N = True +USE_TZ = True + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/2.2/howto/static-files/ +STATIC_URL = "/static/" diff --git a/src/newsreader/conf/dev.py b/src/newsreader/conf/dev.py new file mode 100644 index 0000000..8b5de69 --- /dev/null +++ b/src/newsreader/conf/dev.py @@ -0,0 +1,12 @@ +from .base import * + +# Development settings + +DEBUG = True + +EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" + +try: + from .local import * +except ImportError: + pass 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..8c38f3f --- /dev/null +++ b/src/newsreader/core/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/src/newsreader/core/apps.py b/src/newsreader/core/apps.py new file mode 100644 index 0000000..26f78a8 --- /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..4bd2e28 --- /dev/null +++ b/src/newsreader/core/models.py @@ -0,0 +1,13 @@ +from django.db import models + + +class TimeStampedModel(models.Model): + """ + An abstract base class model that provides self- + updating ``created`` and ``modified`` fields. + """ + created = models.DateTimeField(auto_now_add=True) + modified = models.DateTimeField(auto_now=True) + + class Meta: + abstract = True diff --git a/src/newsreader/core/tests.py b/src/newsreader/core/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/src/newsreader/core/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/src/newsreader/core/views.py b/src/newsreader/core/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/src/newsreader/core/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. 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..972b020 --- /dev/null +++ b/src/newsreader/news/collection/admin.py @@ -0,0 +1,23 @@ +from django.contrib import admin + +from newsreader.news.collection.models import CollectionRule + + +class CollectionRuleAdmin(admin.ModelAdmin): + fields = ( + "url", + "name", + "timezone", + "category", + ) + + list_display = ( + "name", + "category", + "url", + "last_suceeded", + "succeeded", + ) + + +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..1454371 --- /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..58cd9c4 --- /dev/null +++ b/src/newsreader/news/collection/base.py @@ -0,0 +1,81 @@ +import requests + +from django.utils import timezone + +from newsreader.news.collection.models import CollectionRule + + +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 Stream: + def __init__(self, rule): + self.rule = rule + + def read(self): + url = self.rule.url + response = requests.get(url) + return (self.parse(response.content), self) + + 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 diff --git a/src/newsreader/news/collection/exceptions.py b/src/newsreader/news/collection/exceptions.py new file mode 100644 index 0000000..8e12da1 --- /dev/null +++ b/src/newsreader/news/collection/exceptions.py @@ -0,0 +1,28 @@ +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" diff --git a/src/newsreader/news/collection/feed.py b/src/newsreader/news/collection/feed.py new file mode 100644 index 0000000..fe4e2cf --- /dev/null +++ b/src/newsreader/news/collection/feed.py @@ -0,0 +1,211 @@ +from concurrent.futures import ThreadPoolExecutor, as_completed + +import bleach +import pytz +import requests + +from feedparser import parse + +from django.utils import timezone + +from newsreader.news.collection.base import Builder, Client, Collector, Stream +from newsreader.news.collection.exceptions import ( + StreamDeniedException, + StreamException, + StreamNotFoundException, + StreamParseException, + StreamTimeOutException, +) +from newsreader.news.collection.response_handler import ResponseHandler +from newsreader.news.collection.utils import build_publication_date +from newsreader.news.posts.models import Post + + +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, + "category": rule.category + } + + for field, value in field_mapping.items(): + if field in entry: + if field == "published_parsed": + created, aware_datetime = build_publication_date( + entry[field], tz + ) + data[value] = aware_datetime if created else None + elif field == "summary": + summary = self.sanitize_summary(entry[field]) + data[value] = summary + else: + data[value] = entry[field] + + yield Post(**data) + + def sanitize_summary(self, summary): + attrs = {"a": ["href", "rel"], "img": ["alt", "src"],} + tags = ["a", "img", "p"] + + return bleach.clean(summary, tags=tags, attributes=attrs) if summary else None + + def save(self): + for post in self.instances: + post.save() + + +class FeedStream(Stream): + def read(self): + url = self.rule.url + response = requests.get(url) + + with ResponseHandler(response) as response_handler: + response_handler.handle_response() + + 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: + stream.rule.error = e.message + 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.post_set.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) + + if existing_post: + yield existing_post + continue + elif not instance.remote_identifier and self.in_database(instance): + continue + + yield instance + + def in_database(self, entry): + values = { + "url": entry.url, + "title": entry.title, + "body": entry.body, + "publication_date": entry.publication_date + } + + for existing_entry in self.queryset.order_by("-publication_date")[:50]: + if self.is_duplicate(existing_entry, values): + return True + + def is_duplicate(self, existing_entry, values): + for key, value in values.items(): + existing_value = getattr(existing_entry, key, object()) + 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: + return + + 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/management/commands/collect.py b/src/newsreader/news/collection/management/commands/collect.py new file mode 100644 index 0000000..c72301f --- /dev/null +++ b/src/newsreader/news/collection/management/commands/collect.py @@ -0,0 +1,14 @@ +from django.core.management.base import BaseCommand, CommandError + +from newsreader.news.collection.feed import FeedCollector +from newsreader.news.collection.models import CollectionRule + + +class Command(BaseCommand): + help = 'Collects Atom/RSS feeds' + + def handle(self, *args, **options): + CollectionRule.objects.all() + + collector = FeedCollector() + 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..354a97f --- /dev/null +++ b/src/newsreader/news/collection/migrations/0001_initial.py @@ -0,0 +1,31 @@ +# Generated by Django 2.2 on 2019-04-10 20:10 + +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' + ) + ), + ('name', models.CharField(max_length=100)), + ('url', models.URLField()), + ('last_suceeded', models.DateTimeField()), + ('succeeded', models.BooleanField(default=False)), + ], + ), + ] diff --git a/src/newsreader/news/collection/migrations/0002_auto_20190410_2028.py b/src/newsreader/news/collection/migrations/0002_auto_20190410_2028.py new file mode 100644 index 0000000..9c0807e --- /dev/null +++ b/src/newsreader/news/collection/migrations/0002_auto_20190410_2028.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2 on 2019-04-10 20:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('collection', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='collectionrule', + name='last_suceeded', + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/src/newsreader/news/collection/migrations/0003_collectionrule_category.py b/src/newsreader/news/collection/migrations/0003_collectionrule_category.py new file mode 100644 index 0000000..4c3f267 --- /dev/null +++ b/src/newsreader/news/collection/migrations/0003_collectionrule_category.py @@ -0,0 +1,28 @@ +# Generated by Django 2.2 on 2019-05-20 20:06 + +import django.db.models.deletion + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('posts', '0002_auto_20190520_2206'), + ('collection', '0002_auto_20190410_2028'), + ] + + 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='posts.Category', + verbose_name='Category' + ), + ), + ] diff --git a/src/newsreader/news/collection/migrations/0004_collectionrule_timezone.py b/src/newsreader/news/collection/migrations/0004_collectionrule_timezone.py new file mode 100644 index 0000000..e5943dc --- /dev/null +++ b/src/newsreader/news/collection/migrations/0004_collectionrule_timezone.py @@ -0,0 +1,517 @@ +# Generated by Django 2.2 on 2019-05-20 20:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('collection', '0003_collectionrule_category'), + ] + + operations = [ + migrations.AddField( + model_name='collectionrule', + name='timezone', + field=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 + ), + ), + ] diff --git a/src/newsreader/news/collection/migrations/0005_auto_20190521_1941.py b/src/newsreader/news/collection/migrations/0005_auto_20190521_1941.py new file mode 100644 index 0000000..e9ab3d4 --- /dev/null +++ b/src/newsreader/news/collection/migrations/0005_auto_20190521_1941.py @@ -0,0 +1,24 @@ +# Generated by Django 2.2 on 2019-05-21 19:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('collection', '0004_collectionrule_timezone'), + ] + + operations = [ + migrations.AddField( + model_name='collectionrule', + name='favicon', + field=models.ImageField(blank=True, null=True, upload_to=''), + ), + migrations.AddField( + model_name='collectionrule', + name='source', + field=models.CharField(default='source', max_length=100), + preserve_default=False, + ), + ] diff --git a/src/newsreader/news/collection/migrations/0006_collectionrule_error.py b/src/newsreader/news/collection/migrations/0006_collectionrule_error.py new file mode 100644 index 0000000..78843b1 --- /dev/null +++ b/src/newsreader/news/collection/migrations/0006_collectionrule_error.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2 on 2019-06-08 14:13 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('collection', '0005_auto_20190521_1941'), + ] + + operations = [ + migrations.AddField( + model_name='collectionrule', + name='error', + field=models.CharField(blank=True, max_length=255, null=True), + ), + ] 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..ffb4131 --- /dev/null +++ b/src/newsreader/news/collection/models.py @@ -0,0 +1,35 @@ +import pytz + +from django.db import models +from django.utils.translation import gettext as _ + + +class CollectionRule(models.Model): + name = models.CharField(max_length=100) + source = models.CharField(max_length=100) + + url = models.URLField() + favicon = models.ImageField(blank=True, null=True) + + timezone = models.CharField( + choices=((timezone, timezone) for timezone in pytz.all_timezones), + max_length=100, + default="UTC", + ) + + category = models.ForeignKey( + "posts.Category", + blank=True, + null=True, + verbose_name=_("Category"), + 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=255, blank=True, null=True) + + 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..dc33190 --- /dev/null +++ b/src/newsreader/news/collection/response_handler.py @@ -0,0 +1,30 @@ +from newsreader.news.collection.exceptions import ( + StreamDeniedException, + StreamForbiddenException, + StreamNotFoundException, + StreamTimeOutException, +) + + +class ResponseHandler: + message_mapping = { + 404: StreamNotFoundException, + 401: StreamDeniedException, + 403: StreamForbiddenException, + 408: StreamTimeOutException, + } + + def __init__(self, response): + self.response = response + + def __enter__(self): + return self + + def handle_response(self): + status_code = self.response.status_code + + if status_code in self.message_mapping: + raise self.message_mapping[status_code] + + def __exit__(self, *args, **kwargs): + self.response = None diff --git a/src/newsreader/news/collection/tests/__init__.py b/src/newsreader/news/collection/tests/__init__.py new file mode 100644 index 0000000..fb6723f --- /dev/null +++ b/src/newsreader/news/collection/tests/__init__.py @@ -0,0 +1 @@ +from .feed import * diff --git a/src/newsreader/news/collection/tests/factories.py b/src/newsreader/news/collection/tests/factories.py new file mode 100644 index 0000000..6b42292 --- /dev/null +++ b/src/newsreader/news/collection/tests/factories.py @@ -0,0 +1,12 @@ +import factory + +from newsreader.news.collection.models import CollectionRule + + +class CollectionRuleFactory(factory.django.DjangoModelFactory): + class Meta: + model = CollectionRule + + name = factory.Sequence(lambda n: "CollectionRule-{}".format(n)) + source = factory.Faker("name") + url = factory.Faker("url") 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..50cea54 --- /dev/null +++ b/src/newsreader/news/collection/tests/feed/__init__.py @@ -0,0 +1,5 @@ +from .builder import * +from .client import * +from .collector import * +from .duplicate_handler import * +from .stream import * 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..8baa6e5 --- /dev/null +++ b/src/newsreader/news/collection/tests/feed/builder/__init__.py @@ -0,0 +1 @@ +from .tests import * 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..788495f --- /dev/null +++ b/src/newsreader/news/collection/tests/feed/builder/mock_html.py @@ -0,0 +1,10 @@ +html_summary = ''' + + + + + +''' 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..631f582 --- /dev/null +++ b/src/newsreader/news/collection/tests/feed/builder/mocks.py @@ -0,0 +1,878 @@ +from time import struct_time + +from .mock_html import html_summary + +simple_mock = { + 'bozo': 0, + 'encoding': 'utf-8', + 'entries': [{ + 'author': 'A. Author', + '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' + } + }], + '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/' + }, + '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' +} + +multiple_mock = { + 'bozo': 0, + 'encoding': 'utf-8', + 'entries': [ + { + 'author': 'A. Author', + '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' + } + }, + { + 'author': 'A. Author', + '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' + } + }, + { + 'author': 'A. Author', + '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/' + }, + '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' +} + +mock_without_identifier = { + 'bozo': 0, + 'encoding': 'utf-8', + 'entries': [ + { + 'author': 'A. Author', + 'guidislink': False, + 'href': '', + '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' + } + }, + { + 'author': 'A. Author', + 'guidislink': False, + 'href': '', + 'id': None, + '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' + } + }, + ], + '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/' + }, + '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' +} + +mock_without_publish_date = { + 'bozo': 0, + 'encoding': 'utf-8', + 'entries': [ + { + 'author': 'A. Author', + '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': None, + 'published_parsed': None, + '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' + } + }, + { + 'author': 'A. Author', + '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' + }], + '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' + } + }, + ], + '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/' + }, + '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' +} + +mock_without_url = { + 'bozo': 0, + 'encoding': 'utf-8', + 'entries': [ + { + 'author': 'A. Author', + 'guidislink': False, + 'href': '', + 'id': 'https://www.bbc.co.uk/news/world-us-canada-48338168', + '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)), + 'published': None, + 'published_parsed': None, + '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' + } + }, + { + 'author': 'A. Author', + 'guidislink': False, + 'href': '', + 'id': 'https://www.bbc.co.uk/news/technology-48334739', + 'link': None, + 'links': [], + 'media_thumbnail': [{ + 'height': '432', + 'url': 'http://c.files.bbci.co.uk/4789/production/_107031381_mediaitem107028670.jpg', + 'width': '768' + }], + '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.', + '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' + } + }, + ], + '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/' + }, + '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' +} + +mock_without_body = { + 'bozo': 0, + 'encoding': 'utf-8', + 'entries': [ + { + 'author': 'A. Author', + '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)), + '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' + } + }, + { + 'author': 'A. Author', + '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': None, + 'summary_detail': {}, + '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/' + }, + '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' +} + +mock_without_author = { + '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' + } + }, + { + 'author': None, + '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' + } + }, + ], + '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/' + }, + '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' +} + +mock_without_entries = { + 'entries': [], +} + +mock_with_update_entries = { + 'bozo': 0, + 'encoding': 'utf-8', + 'entries': [ + { + 'author': 'A. Author', + 'guidislink': False, + 'href': '', + 'id': '28f79ae4-8f9a-11e9-b143-00163ef6bee7', + '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' + } + }, + { + 'author': 'A. Author', + 'guidislink': False, + 'href': '', + 'id': 'a5479c66-8fae-11e9-8422-00163ef6bee7', + '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' + } + }, + { + 'author': 'A. Author', + '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/' + }, + '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' +} + +mock_with_html = { + 'bozo': 0, + 'encoding': 'utf-8', + 'entries': [ + { + 'author': 'A. Author', + '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': html_summary, + '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' + } + }, + ], + '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/' + }, + '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' +} 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..6e4e44d --- /dev/null +++ b/src/newsreader/news/collection/tests/feed/builder/tests.py @@ -0,0 +1,304 @@ +from datetime import date, datetime, time +from unittest.mock import MagicMock + +import pytz + +from freezegun import freeze_time + +from django.test import TestCase +from django.utils import timezone + +from newsreader.news.collection.feed import FeedBuilder +from newsreader.news.collection.tests.factories import CollectionRuleFactory +from newsreader.news.collection.tests.feed.builder.mocks import * +from newsreader.news.posts.models import Post +from newsreader.news.posts.tests.factories import PostFactory + + +class FeedBuilderTestCase(TestCase): + def setUp(self): + pass + + 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("" not in post.body) + self.assertTrue("" not in post.body) + self.assertTrue("
" not in post.body) + self.assertTrue("

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

" in post.body) diff --git a/src/newsreader/news/collection/tests/feed/client/__init__.py b/src/newsreader/news/collection/tests/feed/client/__init__.py new file mode 100644 index 0000000..8baa6e5 --- /dev/null +++ b/src/newsreader/news/collection/tests/feed/client/__init__.py @@ -0,0 +1 @@ +from .tests import * diff --git a/src/newsreader/news/collection/tests/feed/client/mocks.py b/src/newsreader/news/collection/tests/feed/client/mocks.py new file mode 100644 index 0000000..5853eb7 --- /dev/null +++ b/src/newsreader/news/collection/tests/feed/client/mocks.py @@ -0,0 +1,61 @@ +from time import struct_time + +simple_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' + } + }], + '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/' + }, + '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' +} diff --git a/src/newsreader/news/collection/tests/feed/client/tests.py b/src/newsreader/news/collection/tests/feed/client/tests.py new file mode 100644 index 0000000..0bb4cdd --- /dev/null +++ b/src/newsreader/news/collection/tests/feed/client/tests.py @@ -0,0 +1,90 @@ +from unittest.mock import MagicMock, patch + +from django.test import TestCase +from django.utils import timezone + +from newsreader.news.collection.exceptions import ( + StreamDeniedException, + StreamException, + StreamFieldException, + StreamNotFoundException, + StreamTimeOutException, +) +from newsreader.news.collection.feed import FeedClient +from newsreader.news.collection.tests.factories import CollectionRuleFactory +from newsreader.news.collection.tests.feed.client.mocks import simple_mock + + +class FeedClientTestCase(TestCase): + def setUp(self): + self.patched_read = patch( + 'newsreader.news.collection.feed.FeedStream.read' + ) + self.mocked_read = self.patched_read.start() + + def tearDown(self): + patch.stopall() + + def test_client_retrieves_single_rules(self): + rule = CollectionRuleFactory.create() + mock_stream = MagicMock(rule=rule) + self.mocked_read.return_value = (simple_mock, mock_stream) + + with FeedClient([rule]) as client: + for data, stream in client: + self.assertEquals(data, simple_mock) + self.assertEquals(stream, mock_stream) + + self.mocked_read.assert_called_once_with() + + def test_client_catches_stream_exception(self): + rule = CollectionRuleFactory.create() + mock_stream = MagicMock(rule=rule) + self.mocked_read.side_effect = StreamException("Stream exception") + + with FeedClient([rule]) as client: + for data, stream in client: + self.assertEquals(data, {"entries": []}) + self.assertEquals(stream.rule.error, "Stream exception") + self.assertEquals(stream.rule.succeeded, False) + + self.mocked_read.assert_called_once_with() + + def test_client_catches_stream_not_found_exception(self): + rule = CollectionRuleFactory.create() + mock_stream = MagicMock(rule=rule) + self.mocked_read.side_effect = StreamNotFoundException("Stream not found") + + with FeedClient([rule]) as client: + for data, stream in client: + self.assertEquals(data, {"entries": []}) + self.assertEquals(stream.rule.error, "Stream not found") + self.assertEquals(stream.rule.succeeded, False) + + self.mocked_read.assert_called_once_with() + + def test_client_catches_stream_denied_exception(self): + rule = CollectionRuleFactory.create() + mock_stream = MagicMock(rule=rule) + self.mocked_read.side_effect = StreamDeniedException("Stream denied") + + with FeedClient([rule]) as client: + for data, stream in client: + self.assertEquals(data, {"entries": []}) + self.assertEquals(stream.rule.error, "Stream denied") + self.assertEquals(stream.rule.succeeded, False) + + self.mocked_read.assert_called_once_with() + + def test_client_catches_stream_timed_out(self): + rule = CollectionRuleFactory.create() + mock_stream = MagicMock(rule=rule) + self.mocked_read.side_effect = StreamTimeOutException("Stream timed out") + + with FeedClient([rule]) as client: + for data, stream in client: + self.assertEquals(data, {"entries": []}) + self.assertEquals(stream.rule.error, "Stream timed out") + self.assertEquals(stream.rule.succeeded, False) + + self.mocked_read.assert_called_once_with() diff --git a/src/newsreader/news/collection/tests/feed/collector/__init__.py b/src/newsreader/news/collection/tests/feed/collector/__init__.py new file mode 100644 index 0000000..8baa6e5 --- /dev/null +++ b/src/newsreader/news/collection/tests/feed/collector/__init__.py @@ -0,0 +1 @@ +from .tests import * diff --git a/src/newsreader/news/collection/tests/feed/collector/mocks.py b/src/newsreader/news/collection/tests/feed/collector/mocks.py new file mode 100644 index 0000000..930e977 --- /dev/null +++ b/src/newsreader/news/collection/tests/feed/collector/mocks.py @@ -0,0 +1,430 @@ +from time import struct_time + +multiple_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/' + }, + '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' +} + +empty_mock = { + 'bozo': 0, + 'encoding': 'utf-8', + 'entries': [], + '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/' + }, + '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' +} + +duplicate_mock = { + 'bozo': 0, + 'encoding': 'utf-8', + 'entries': [ + { + 'guidislink': False, + 'href': '', + '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': '', + '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': '', + '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/' + }, + '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' +} + +multiple_update_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/' + }, + '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' +} diff --git a/src/newsreader/news/collection/tests/feed/collector/tests.py b/src/newsreader/news/collection/tests/feed/collector/tests.py new file mode 100644 index 0000000..db95ccb --- /dev/null +++ b/src/newsreader/news/collection/tests/feed/collector/tests.py @@ -0,0 +1,251 @@ +from datetime import date, datetime, time +from time import struct_time +from unittest.mock import MagicMock, patch + +import pytz + +from freezegun import freeze_time + +from django.test import TestCase +from django.utils import timezone + +from newsreader.news.collection.feed import FeedCollector +from newsreader.news.collection.tests.factories import CollectionRuleFactory +from newsreader.news.collection.tests.feed.collector.mocks import ( + duplicate_mock, + empty_mock, + multiple_mock, + multiple_update_mock, +) +from newsreader.news.collection.utils import build_publication_date +from newsreader.news.posts.models import Post +from newsreader.news.posts.tests.factories import PostFactory + + +class FeedCollectorTestCase(TestCase): + def setUp(self): + self.patched_get = patch( + 'newsreader.news.collection.feed.requests.get' + ) + self.mocked_get = self.patched_get.start() + + self.patched_parse = patch( + 'newsreader.news.collection.feed.FeedStream.parse' + ) + self.mocked_parse = self.patched_parse.start() + + def tearDown(self): + patch.stopall() + + @freeze_time("2019-10-30 12:30:00") + def test_simple_batch(self): + self.mocked_parse.return_value = multiple_mock + rule = CollectionRuleFactory() + + collector = FeedCollector() + collector.collect() + + rule.refresh_from_db() + + self.assertEquals(Post.objects.count(), 3) + self.assertEquals(rule.succeeded, True) + self.assertEquals(rule.last_suceeded, timezone.now()) + self.assertEquals(rule.error, None) + + @freeze_time("2019-10-30 12:30:00") + def test_emtpy_batch(self): + self.mocked_get.return_value = MagicMock(status_code=200) + self.mocked_parse.return_value = empty_mock + rule = CollectionRuleFactory() + + collector = FeedCollector() + collector.collect() + + rule.refresh_from_db() + + self.assertEquals(Post.objects.count(), 0) + self.assertEquals(rule.succeeded, True) + self.assertEquals(rule.error, None) + self.assertEquals(rule.last_suceeded, timezone.now()) + + def test_not_found(self): + self.mocked_get.return_value = MagicMock(status_code=404) + rule = CollectionRuleFactory() + + collector = FeedCollector() + collector.collect() + + rule.refresh_from_db() + + self.assertEquals(Post.objects.count(), 0) + self.assertEquals(rule.succeeded, False) + self.assertEquals(rule.error, "Stream not found") + + def test_denied(self): + self.mocked_get.return_value = MagicMock(status_code=404) + last_suceeded = timezone.make_aware( + datetime.combine(date=date(2019, 10, 30), time=time(12, 30)) + ) + rule = CollectionRuleFactory(last_suceeded=last_suceeded) + + collector = FeedCollector() + collector.collect() + + rule.refresh_from_db() + + self.assertEquals(Post.objects.count(), 0) + self.assertEquals(rule.succeeded, False) + self.assertEquals(rule.error, "Stream not found") + self.assertEquals(rule.last_suceeded, last_suceeded) + + def test_forbidden(self): + self.mocked_get.return_value = MagicMock(status_code=403) + last_suceeded = timezone.make_aware( + datetime.combine(date=date(2019, 10, 30), time=time(12, 30)) + ) + rule = CollectionRuleFactory(last_suceeded=last_suceeded) + + collector = FeedCollector() + collector.collect() + + rule.refresh_from_db() + + self.assertEquals(Post.objects.count(), 0) + self.assertEquals(rule.succeeded, False) + self.assertEquals(rule.error, "Stream forbidden") + self.assertEquals(rule.last_suceeded, last_suceeded) + + def test_timed_out(self): + self.mocked_get.return_value = MagicMock(status_code=408) + last_suceeded = timezone.make_aware( + datetime.combine(date=date(2019, 10, 30), time=time(12, 30)) + ) + rule = CollectionRuleFactory(last_suceeded=last_suceeded) + + collector = FeedCollector() + collector.collect() + + rule.refresh_from_db() + + self.assertEquals(Post.objects.count(), 0) + self.assertEquals(rule.succeeded, False) + self.assertEquals(rule.error, "Stream timed out") + self.assertEquals(rule.last_suceeded, last_suceeded) + + @freeze_time("2019-10-30 12:30:00") + def test_duplicates(self): + self.mocked_parse.return_value = duplicate_mock + rule = CollectionRuleFactory() + + _, aware_datetime = build_publication_date( + struct_time((2019, 5, 20, 16, 7, 37, 0, 140, 0)), + pytz.utc + ) + + first_post = PostFactory( + url="https://www.bbc.co.uk/news/world-us-canada-48338168", + title="Trump's 'genocidal taunts' will not end Iran - Zarif", + body="Foreign Minister Mohammad Javad Zarif says the US " + "president should try showing Iranians some respect.", + publication_date=aware_datetime, + rule=rule + ) + + _, aware_datetime = build_publication_date( + struct_time((2019, 5, 20, 12, 19, 19, 0, 140, 0,)), + pytz.utc + ) + + second_post = PostFactory( + url="https://www.bbc.co.uk/news/technology-48334739", + title="Huawei's Android loss: How it affects you", + body="Google's move to end business ties with Huawei will " + "affect current devices and future purchases.", + publication_date=aware_datetime, + rule=rule + ) + + _, aware_datetime = build_publication_date( + struct_time((2019, 5, 20, 16, 32, 38, 0, 140, 0)), + pytz.utc + ) + + third_post = PostFactory( + url="https://www.bbc.co.uk/news/uk-england-birmingham-48339080", + title="Birmingham head teacher threatened over LGBT lessons", + body="Police are investigating the messages while an MP " + "calls for a protest exclusion zone \"to protect " + "children\".", + publication_date=aware_datetime, + rule=rule + ) + + collector = FeedCollector() + collector.collect(rules=[rule]) + + rule.refresh_from_db() + + self.assertEquals(Post.objects.count(), 3) + self.assertEquals(rule.succeeded, True) + self.assertEquals(rule.last_suceeded, timezone.now()) + self.assertEquals(rule.error, None) + + @freeze_time("2019-02-22 12:30:00") + def test_items_with_identifiers_get_updated(self): + self.mocked_parse.return_value = multiple_update_mock + rule = CollectionRuleFactory() + + first_post = PostFactory( + remote_identifier="https://www.bbc.co.uk/news/world-us-canada-48338168", + url="https://www.bbc.co.uk/", + title="Trump", + body="Foreign Minister Mohammad Javad Zarif", + publication_date=timezone.now(), + rule=rule + ) + + second_post = PostFactory( + remote_identifier="https://www.bbc.co.uk/news/technology-48334739", + url="https://www.bbc.co.uk/", + title="Huawei's Android loss: How it affects you", + body="Google's move to end business ties with Huawei will", + publication_date=timezone.now(), + rule=rule + ) + + third_post = PostFactory( + remote_identifier="https://www.bbc.co.uk/news/uk-england-birmingham-48339080", + url="https://www.bbc.co.uk/news/uk-england-birmingham-48339080", + title="Birmingham head teacher threatened over LGBT lessons", + body="Police are investigating the messages while an MP", + publication_date=timezone.now(), + rule=rule + ) + + collector = FeedCollector() + collector.collect(rules=[rule]) + + rule.refresh_from_db() + first_post.refresh_from_db() + second_post.refresh_from_db() + third_post.refresh_from_db() + + self.assertEquals(Post.objects.count(), 3) + self.assertEquals(rule.succeeded, True) + self.assertEquals(rule.last_suceeded, timezone.now()) + self.assertEquals(rule.error, None) + + self.assertEquals( + first_post.title, + "Trump's 'genocidal taunts' will not end Iran - Zarif" + ) + + self.assertEquals( + second_post.title, + "Huawei's Android loss: How it affects you" + ) + + self.assertEquals( + third_post.title, + 'Birmingham head teacher threatened over LGBT lessons' + ) diff --git a/src/newsreader/news/collection/tests/feed/duplicate_handler/__init__.py b/src/newsreader/news/collection/tests/feed/duplicate_handler/__init__.py new file mode 100644 index 0000000..8baa6e5 --- /dev/null +++ b/src/newsreader/news/collection/tests/feed/duplicate_handler/__init__.py @@ -0,0 +1 @@ +from .tests import * diff --git a/src/newsreader/news/collection/tests/feed/duplicate_handler/tests.py b/src/newsreader/news/collection/tests/feed/duplicate_handler/tests.py new file mode 100644 index 0000000..a8600af --- /dev/null +++ b/src/newsreader/news/collection/tests/feed/duplicate_handler/tests.py @@ -0,0 +1,63 @@ +from django.test import TestCase +from django.utils import timezone + +from newsreader.news.collection.feed import FeedDuplicateHandler +from newsreader.news.collection.tests.factories import CollectionRuleFactory +from newsreader.news.posts.models import Post +from newsreader.news.posts.tests.factories import PostFactory + + +class FeedDuplicateHandlerTestCase(TestCase): + def setUp(self): + pass + + def test_duplicate_entries_with_remote_identifiers(self): + rule = CollectionRuleFactory() + existing_post = PostFactory.create( + remote_identifier="28f79ae4-8f9a-11e9-b143-00163ef6bee7", rule=rule + ) + new_post = PostFactory.build( + remote_identifier="28f79ae4-8f9a-11e9-b143-00163ef6bee7", + title="title got updated", + rule=rule + ) + + with FeedDuplicateHandler(rule) as duplicate_handler: + posts_gen = duplicate_handler.check([new_post]) + posts = list(posts_gen) + + post = posts[0] + + self.assertEquals(len(posts), 1) + self.assertEquals(post.publication_date, new_post.publication_date) + self.assertTrue(post.publication_date != existing_post.publication_date) + self.assertTrue(post.title != existing_post.title) + + def test_duplicate_entries_in_recent_database(self): + PostFactory.create_batch(size=20) + + publication_date = timezone.now() + + rule = CollectionRuleFactory() + existing_post = PostFactory.create( + url="https://www.bbc.co.uk/news/uk-england-birmingham-48339080", + title="Birmingham head teacher threatened over LGBT lessons", + body="Google's move to end business ties with Huawei will affect current devices", + publication_date=publication_date, + remote_identifier=None, + rule=rule + ) + new_post = PostFactory.build( + url="https://www.bbc.co.uk/news/uk-england-birmingham-48339080", + title="Birmingham head teacher threatened over LGBT lessons", + body="Google's move to end business ties with Huawei will affect current devices", + publication_date=publication_date, + remote_identifier=None, + rule=rule + ) + + with FeedDuplicateHandler(rule) as duplicate_handler: + posts_gen = duplicate_handler.check([new_post]) + posts = list(posts_gen) + + self.assertEquals(len(posts), 0) diff --git a/src/newsreader/news/collection/tests/feed/stream/__init__.py b/src/newsreader/news/collection/tests/feed/stream/__init__.py new file mode 100644 index 0000000..8baa6e5 --- /dev/null +++ b/src/newsreader/news/collection/tests/feed/stream/__init__.py @@ -0,0 +1 @@ +from .tests import * diff --git a/src/newsreader/news/collection/tests/feed/stream/mocks.py b/src/newsreader/news/collection/tests/feed/stream/mocks.py new file mode 100644 index 0000000..5853eb7 --- /dev/null +++ b/src/newsreader/news/collection/tests/feed/stream/mocks.py @@ -0,0 +1,61 @@ +from time import struct_time + +simple_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' + } + }], + '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/' + }, + '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' +} diff --git a/src/newsreader/news/collection/tests/feed/stream/tests.py b/src/newsreader/news/collection/tests/feed/stream/tests.py new file mode 100644 index 0000000..6e15194 --- /dev/null +++ b/src/newsreader/news/collection/tests/feed/stream/tests.py @@ -0,0 +1,109 @@ +from unittest.mock import MagicMock, patch + +from django.test import TestCase +from django.utils import timezone + +from newsreader.news.collection.exceptions import ( + StreamDeniedException, + StreamException, + StreamForbiddenException, + StreamNotFoundException, + StreamParseException, + StreamTimeOutException, +) +from newsreader.news.collection.feed import FeedStream +from newsreader.news.collection.tests.factories import CollectionRuleFactory +from newsreader.news.collection.tests.feed.stream.mocks import simple_mock + + +class FeedStreamTestCase(TestCase): + def setUp(self): + self.patched_get = patch( + 'newsreader.news.collection.feed.requests.get' + ) + self.mocked_get = self.patched_get.start() + + self.patched_parse = patch( + 'newsreader.news.collection.feed.FeedStream.parse' + ) + self.mocked_parse = self.patched_parse.start() + + def tearDown(self): + patch.stopall() + + def test_simple_stream(self): + self.mocked_parse.return_value = simple_mock + + rule = CollectionRuleFactory() + stream = FeedStream(rule) + return_value = stream.read() + + self.mocked_get.assert_called_once_with(rule.url) + self.assertEquals(return_value, (simple_mock, stream)) + + def test_stream_raises_exception(self): + self.mocked_parse.side_effect = StreamException + + rule = CollectionRuleFactory() + stream = FeedStream(rule) + + with self.assertRaises(StreamException): + stream.read() + + self.mocked_get.assert_called_once_with(rule.url) + + def test_stream_raises_denied_exception(self): + self.mocked_get.return_value = MagicMock(status_code=401) + + rule = CollectionRuleFactory() + stream = FeedStream(rule) + + with self.assertRaises(StreamDeniedException): + stream.read() + + self.mocked_get.assert_called_once_with(rule.url) + + def test_stream_raises_not_found_exception(self): + self.mocked_get.return_value = MagicMock(status_code=404) + + rule = CollectionRuleFactory() + stream = FeedStream(rule) + + with self.assertRaises(StreamNotFoundException): + stream.read() + + self.mocked_get.assert_called_once_with(rule.url) + + def test_stream_raises_time_out_exception(self): + self.mocked_get.return_value = MagicMock(status_code=408) + + rule = CollectionRuleFactory() + stream = FeedStream(rule) + + with self.assertRaises(StreamTimeOutException): + stream.read() + + self.mocked_get.assert_called_once_with(rule.url) + + def test_stream_raises_forbidden_exception(self): + self.mocked_get.return_value = MagicMock(status_code=403) + + rule = CollectionRuleFactory() + stream = FeedStream(rule) + + with self.assertRaises(StreamForbiddenException): + stream.read() + + self.mocked_get.assert_called_once_with(rule.url) + + @patch("newsreader.news.collection.feed.parse") + def test_stream_raises_parse_exception(self, mocked_parse): + self.mocked_get.return_value = MagicMock(status_code=200) + mocked_parse.side_effect = TypeError + self.patched_parse.stop() + + rule = CollectionRuleFactory() + stream = FeedStream(rule) + + with self.assertRaises(StreamParseException): + stream.read() diff --git a/src/newsreader/news/collection/utils.py b/src/newsreader/news/collection/utils.py new file mode 100644 index 0000000..6a80ed7 --- /dev/null +++ b/src/newsreader/news/collection/utils.py @@ -0,0 +1,13 @@ +from datetime import datetime +from time import mktime + +from django.utils import timezone + + +def build_publication_date(dt, tz): + try: + naive_datetime = datetime.fromtimestamp(mktime(dt)) + published_parsed = timezone.make_aware(naive_datetime, timezone=tz) + except TypeError: + return False, None + return True, published_parsed diff --git a/src/newsreader/news/collection/views.py b/src/newsreader/news/collection/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/src/newsreader/news/collection/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/src/newsreader/news/posts/__init__.py b/src/newsreader/news/posts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/newsreader/news/posts/admin.py b/src/newsreader/news/posts/admin.py new file mode 100644 index 0000000..2ba7c81 --- /dev/null +++ b/src/newsreader/news/posts/admin.py @@ -0,0 +1,39 @@ +from django.contrib import admin + +from newsreader.news.posts.models import Category, Post + + +class PostAdmin(admin.ModelAdmin): + list_display = ( + "publication_date", + "author", + "rule", + "title", + ) + list_display_links = ("title", ) + list_filter = ("rule", ) + + ordering = ("-publication_date", "title") + + fields = ( + "title", + "body", + "author", + "publication_date", + "url", + "remote_identifier", + "category", + ) + + search_fields = ["title"] + + def rule(self, obj): + return obj.rule + + +class CategoryAdmin(admin.ModelAdmin): + pass + + +admin.site.register(Post, PostAdmin) +admin.site.register(Category, CategoryAdmin) diff --git a/src/newsreader/news/posts/apps.py b/src/newsreader/news/posts/apps.py new file mode 100644 index 0000000..2c2b982 --- /dev/null +++ b/src/newsreader/news/posts/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class PostsConfig(AppConfig): + name = 'posts' diff --git a/src/newsreader/news/posts/migrations/0001_initial.py b/src/newsreader/news/posts/migrations/0001_initial.py new file mode 100644 index 0000000..36666b3 --- /dev/null +++ b/src/newsreader/news/posts/migrations/0001_initial.py @@ -0,0 +1,78 @@ +# Generated by Django 2.2 on 2019-04-10 20:10 + +import django.db.models.deletion + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('collection', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Category', + fields=[ + ( + 'id', + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name='ID' + ) + ), + ('created', models.DateTimeField(auto_now_add=True)), + ('modified', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=50)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Post', + fields=[ + ( + 'id', + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name='ID' + ) + ), + ('created', models.DateTimeField(auto_now_add=True)), + ('modified', models.DateTimeField(auto_now=True)), + ('title', models.CharField(max_length=200)), + ('body', models.TextField()), + ('source', models.CharField(max_length=200)), + ('publication_date', models.DateTimeField()), + ('url', models.URLField()), + ('remote_identifier', models.CharField(max_length=500)), + ( + 'category', + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + to='posts.Category' + ) + ), + ( + 'rule', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to='collection.CollectionRule' + ) + ), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/src/newsreader/news/posts/migrations/0002_auto_20190520_2206.py b/src/newsreader/news/posts/migrations/0002_auto_20190520_2206.py new file mode 100644 index 0000000..cb86d51 --- /dev/null +++ b/src/newsreader/news/posts/migrations/0002_auto_20190520_2206.py @@ -0,0 +1,20 @@ +# Generated by Django 2.2 on 2019-05-20 20:06 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('posts', '0001_initial'), + ] + + operations = [ + migrations.AlterModelOptions( + name='category', + options={ + 'verbose_name': 'Category', + 'verbose_name_plural': 'Categories' + }, + ), + ] diff --git a/src/newsreader/news/posts/migrations/0003_auto_20190520_2031.py b/src/newsreader/news/posts/migrations/0003_auto_20190520_2031.py new file mode 100644 index 0000000..a790477 --- /dev/null +++ b/src/newsreader/news/posts/migrations/0003_auto_20190520_2031.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2 on 2019-05-20 20:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('posts', '0002_auto_20190520_2206'), + ] + + operations = [ + migrations.AlterField( + model_name='category', + name='name', + field=models.CharField(max_length=50, unique=True), + ), + ] diff --git a/src/newsreader/news/posts/migrations/0004_auto_20190521_1941.py b/src/newsreader/news/posts/migrations/0004_auto_20190521_1941.py new file mode 100644 index 0000000..a14c636 --- /dev/null +++ b/src/newsreader/news/posts/migrations/0004_auto_20190521_1941.py @@ -0,0 +1,22 @@ +# Generated by Django 2.2 on 2019-05-21 19:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('posts', '0003_auto_20190520_2031'), + ] + + operations = [ + migrations.RemoveField( + model_name='post', + name='source', + ), + migrations.AddField( + model_name='post', + name='author', + field=models.CharField(blank=True, max_length=100, null=True), + ), + ] diff --git a/src/newsreader/news/posts/migrations/0005_auto_20190608_1054.py b/src/newsreader/news/posts/migrations/0005_auto_20190608_1054.py new file mode 100644 index 0000000..96c9d8c --- /dev/null +++ b/src/newsreader/news/posts/migrations/0005_auto_20190608_1054.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2 on 2019-06-08 10:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('posts', '0004_auto_20190521_1941'), + ] + + operations = [ + migrations.AlterField( + model_name='post', + name='body', + field=models.TextField(blank=True), + ), + migrations.AlterField( + model_name='post', + name='remote_identifier', + field=models.CharField(blank=True, max_length=500, null=True), + ), + ] diff --git a/src/newsreader/news/posts/migrations/0006_auto_20190608_1520.py b/src/newsreader/news/posts/migrations/0006_auto_20190608_1520.py new file mode 100644 index 0000000..8215ea9 --- /dev/null +++ b/src/newsreader/news/posts/migrations/0006_auto_20190608_1520.py @@ -0,0 +1,33 @@ +# Generated by Django 2.2 on 2019-06-08 15:20 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('posts', '0005_auto_20190608_1054'), + ] + + operations = [ + migrations.AlterField( + model_name='post', + name='body', + field=models.TextField(blank=True, null=True), + ), + migrations.AlterField( + model_name='post', + name='publication_date', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AlterField( + model_name='post', + name='title', + field=models.CharField(blank=True, max_length=200, null=True), + ), + migrations.AlterField( + model_name='post', + name='url', + field=models.URLField(blank=True, null=True), + ), + ] diff --git a/src/newsreader/news/posts/migrations/__init__.py b/src/newsreader/news/posts/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/newsreader/news/posts/models.py b/src/newsreader/news/posts/models.py new file mode 100644 index 0000000..0528187 --- /dev/null +++ b/src/newsreader/news/posts/models.py @@ -0,0 +1,34 @@ +from django.db import models +from django.utils.translation import gettext as _ + +from newsreader.core.models import TimeStampedModel +from newsreader.news.collection.models import CollectionRule + + +class Post(TimeStampedModel): + title = models.CharField(max_length=200, blank=True, null=True) + body = models.TextField(blank=True, null=True) + author = models.CharField(max_length=100, blank=True, null=True) + publication_date = models.DateTimeField(blank=True, null=True) + url = models.URLField(blank=True, null=True) + + rule = models.ForeignKey(CollectionRule, on_delete=models.CASCADE) + remote_identifier = models.CharField(max_length=500, blank=True, null=True) + + category = models.ForeignKey( + 'Category', blank=True, null=True, on_delete=models.PROTECT + ) + + def __str__(self): + return "Post-{}".format(self.pk) + + +class Category(TimeStampedModel): + name = models.CharField(max_length=50, unique=True) + + class Meta: + verbose_name = _("Category") + verbose_name_plural = _("Categories") + + def __str__(self): + return self.name diff --git a/src/newsreader/news/posts/tests/__init__.py b/src/newsreader/news/posts/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/newsreader/news/posts/tests/factories.py b/src/newsreader/news/posts/tests/factories.py new file mode 100644 index 0000000..d059335 --- /dev/null +++ b/src/newsreader/news/posts/tests/factories.py @@ -0,0 +1,28 @@ +import factory +import pytz + +from newsreader.news.collection.tests.factories import CollectionRuleFactory +from newsreader.news.posts.models import Category, Post + + +class CategoryFactory(factory.django.DjangoModelFactory): + class Meta: + model = Category + + name = factory.Sequence(lambda n: "Category-{}".format(n)) + + +class PostFactory(factory.django.DjangoModelFactory): + class Meta: + model = Post + + 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(CollectionRuleFactory) + + category = factory.SubFactory(CategoryFactory) diff --git a/src/newsreader/news/posts/views.py b/src/newsreader/news/posts/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/src/newsreader/news/posts/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/src/newsreader/urls.py b/src/newsreader/urls.py new file mode 100644 index 0000000..fdd5749 --- /dev/null +++ b/src/newsreader/urls.py @@ -0,0 +1,6 @@ +from django.contrib import admin +from django.urls import include, path + +urlpatterns = [ + path("admin/", admin.site.urls), +] diff --git a/src/newsreader/utils/formatter.sh b/src/newsreader/utils/formatter.sh new file mode 100644 index 0000000..de70b4a --- /dev/null +++ b/src/newsreader/utils/formatter.sh @@ -0,0 +1,10 @@ +#!/bin/bash +FILES=$(git diff --cached --name-only --diff-filter=ACM "*.py" | sed 's| |\\ |g') + +if [ ! -z "$FILES" ]; then + # Format all selected files + echo "$FILES" | xargs ./env/bin/isort + + # Add back the modified/prettified files to staging + echo "$FILES" | xargs git add +fi diff --git a/src/newsreader/utils/pre-commit b/src/newsreader/utils/pre-commit new file mode 100644 index 0000000..d1e29b9 --- /dev/null +++ b/src/newsreader/utils/pre-commit @@ -0,0 +1,13 @@ +#!/bin/bash + +# Check if the directory is the root directory +if [ ! -d ".git/" ]; then + echo "Please commit from within the root directory" + exit 1 +fi + +# Run every file inside the pre-commit.d directory +for file in .git/hooks/pre-commit.d/* +do + . $file +done diff --git a/src/newsreader/wsgi.py b/src/newsreader/wsgi.py new file mode 100644 index 0000000..4c5ea26 --- /dev/null +++ b/src/newsreader/wsgi.py @@ -0,0 +1,16 @@ +""" +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.settings') + +application = get_wsgi_application() From 69e4e7b26941ac6746e2f3bb16608e0eb13c393d Mon Sep 17 00:00:00 2001 From: Sonny Date: Sat, 22 Jun 2019 21:55:47 +0200 Subject: [PATCH 003/364] Update project requirements --- requirements/base.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/base.txt b/requirements/base.txt index 0f82f61..68e2d7d 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,9 +1,9 @@ +bleach==3.1.0 certifi==2019.3.9 chardet==3.0.4 Django==2.2 feedparser==5.2.1 idna==2.8 -pkg-resources==0.0.0 pytz==2018.9 requests==2.21.0 sqlparse==0.3.0 From c75de1c469f2a473dd8f558266dd65db38fe5d3a Mon Sep 17 00:00:00 2001 From: Sonny Date: Sun, 23 Jun 2019 12:52:49 +0200 Subject: [PATCH 004/364] Add type hinting --- src/newsreader/news/collection/base.py | 86 ++++++++++--------- src/newsreader/news/collection/feed.py | 79 ++++++++--------- .../news/collection/response_handler.py | 12 ++- .../collection/tests/feed/client/tests.py | 5 +- src/newsreader/news/collection/utils.py | 7 +- 5 files changed, 92 insertions(+), 97 deletions(-) diff --git a/src/newsreader/news/collection/base.py b/src/newsreader/news/collection/base.py index 58cd9c4..0ab25d3 100644 --- a/src/newsreader/news/collection/base.py +++ b/src/newsreader/news/collection/base.py @@ -1,3 +1,5 @@ +from typing import ContextManager, Dict, List, Optional, Tuple + import requests from django.utils import timezone @@ -5,23 +7,58 @@ from django.utils import timezone from newsreader.news.collection.models import CollectionRule +class Stream: + def __init__(self, rule: CollectionRule) -> None: + self.rule = rule + + def read(self) -> Tuple: + url = self.rule.url + response = requests.get(url) + return (self.parse(response.content), self) + + def parse(self, payload: bytes) -> Dict: + raise NotImplementedError + + class Meta: + abstract = True + + +class Client: + stream = Stream + + def __init__(self, rules: Optional[CollectionRule] = None) -> None: + self.rules = rules if rules else CollectionRule.objects.all() + + def __enter__(self) -> ContextManager: + for rule in self.rules: + stream = self.stream(rule) + + yield stream.read() + + def __exit__(self, *args, **kwargs) -> None: + pass + + class Meta: + abstract = True + + class Builder: instances = [] - def __init__(self, stream): + def __init__(self, stream: Stream) -> None: self.stream = stream - def __enter__(self): + def __enter__(self) -> ContextManager: self.create_posts(self.stream) return self - def __exit__(self, *args, **kwargs): + def __exit__(self, *args, **kwargs) -> None: pass - def create_posts(self, stream): + def create_posts(self, stream: Tuple) -> None: pass - def save(self): + def save(self) -> None: pass class Meta: @@ -32,11 +69,11 @@ class Collector: client = None builder = None - def __init__(self, client=None, builder=None): + def __init__(self, client: Optional[Client] = None, builder: Optional[Builder] = None) -> None: self.client = client if client else self.client self.builder = builder if builder else self.builder - def collect(self, rules=None): + def collect(self, rules: Optional[List] = None) -> None: with self.client(rules=rules) as client: for data, stream in client: with self.builder((data, stream)) as builder: @@ -44,38 +81,3 @@ class Collector: class Meta: abstract = True - - -class Stream: - def __init__(self, rule): - self.rule = rule - - def read(self): - url = self.rule.url - response = requests.get(url) - return (self.parse(response.content), self) - - 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 diff --git a/src/newsreader/news/collection/feed.py b/src/newsreader/news/collection/feed.py index fe4e2cf..e2c098f 100644 --- a/src/newsreader/news/collection/feed.py +++ b/src/newsreader/news/collection/feed.py @@ -1,4 +1,5 @@ from concurrent.futures import ThreadPoolExecutor, as_completed +from typing import ContextManager, Dict, Generator, List, Optional, Tuple import bleach import pytz @@ -16,6 +17,7 @@ from newsreader.news.collection.exceptions import ( StreamParseException, StreamTimeOutException, ) +from newsreader.news.collection.models import CollectionRule from newsreader.news.collection.response_handler import ResponseHandler from newsreader.news.collection.utils import build_publication_date from newsreader.news.posts.models import Post @@ -24,17 +26,16 @@ from newsreader.news.posts.models import Post class FeedBuilder(Builder): instances = [] - def __enter__(self): + def __enter__(self) -> ContextManager: _, stream = self.stream self.instances = [] self.existing_posts = { - post.remote_identifier: post - for post in Post.objects.filter(rule=stream.rule) + post.remote_identifier: post for post in Post.objects.filter(rule=stream.rule) } return super().__enter__() - def create_posts(self, stream): + def create_posts(self, stream: Tuple) -> None: data, stream = stream entries = [] @@ -49,30 +50,25 @@ class FeedBuilder(Builder): self.instances = [post for post in posts] - def build(self, entries, rule): + def build(self, entries: List, rule: CollectionRule) -> Generator[Post, None, None]: field_mapping = { "id": "remote_identifier", "title": "title", "summary": "body", "link": "url", "published_parsed": "publication_date", - "author": "author" + "author": "author", } tz = pytz.timezone(rule.timezone) for entry in entries: - data = { - "rule_id": rule.pk, - "category": rule.category - } + data = {"rule_id": rule.pk, "category": rule.category} for field, value in field_mapping.items(): if field in entry: if field == "published_parsed": - created, aware_datetime = build_publication_date( - entry[field], tz - ) + created, aware_datetime = build_publication_date(entry[field], tz) data[value] = aware_datetime if created else None elif field == "summary": summary = self.sanitize_summary(entry[field]) @@ -82,19 +78,19 @@ class FeedBuilder(Builder): yield Post(**data) - def sanitize_summary(self, summary): - attrs = {"a": ["href", "rel"], "img": ["alt", "src"],} + def sanitize_summary(self, summary: str) -> Optional[str]: + attrs = {"a": ["href", "rel"], "img": ["alt", "src"]} tags = ["a", "img", "p"] return bleach.clean(summary, tags=tags, attributes=attrs) if summary else None - def save(self): + def save(self) -> None: for post in self.instances: post.save() class FeedStream(Stream): - def read(self): + def read(self) -> Tuple: url = self.rule.url response = requests.get(url) @@ -103,7 +99,7 @@ class FeedStream(Stream): return (self.parse(response.content), self) - def parse(self, payload): + def parse(self, payload: bytes) -> Dict: try: return parse(payload) except TypeError as e: @@ -113,14 +109,11 @@ class FeedStream(Stream): class FeedClient(Client): stream = FeedStream - def __enter__(self): + def __enter__(self) -> ContextManager: 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 - } + futures = {executor.submit(stream.read): stream for stream in streams} for future in as_completed(futures): stream = futures[future] @@ -148,20 +141,20 @@ class FeedCollector(Collector): class FeedDuplicateHandler: - def __init__(self, rule): + def __init__(self, rule: CollectionRule) -> None: self.queryset = rule.post_set.all() - def __enter__(self): - self.existing_identifiers = self.queryset.filter(remote_identifier__isnull=False).values_list( - "remote_identifier", flat=True - ) + def __enter__(self) -> ContextManager: + self.existing_identifiers = self.queryset.filter( + remote_identifier__isnull=False + ).values_list("remote_identifier", flat=True) return self - def __exit__(self, *args, **kwargs): + def __exit__(self, *args, **kwargs) -> None: pass - def check(self, instances): - for instance in instances: + def check(self, instances: List) -> Generator[Post, None, None]: + for instance in instances: if instance.remote_identifier in self.existing_identifiers: existing_post = self.handle_duplicate(instance) @@ -173,31 +166,29 @@ class FeedDuplicateHandler: yield instance - def in_database(self, entry): + def in_database(self, post: Post) -> Optional[bool]: values = { - "url": entry.url, - "title": entry.title, - "body": entry.body, - "publication_date": entry.publication_date + "url": post.url, + "title": post.title, + "body": post.body, + "publication_date": post.publication_date, } - for existing_entry in self.queryset.order_by("-publication_date")[:50]: - if self.is_duplicate(existing_entry, values): + for existing_post in self.queryset.order_by("-publication_date")[:50]: + if self.is_duplicate(existing_post, values): return True - def is_duplicate(self, existing_entry, values): + def is_duplicate(self, existing_post: Post, values: Dict) -> bool: for key, value in values.items(): - existing_value = getattr(existing_entry, key, object()) + existing_value = getattr(existing_post, key, object()) if existing_value != value: return False return True - def handle_duplicate(self, instance): + def handle_duplicate(self, instance: Post) -> Optional[Post]: try: - existing_instance = self.queryset.get( - remote_identifier=instance.remote_identifier, - ) + existing_instance = self.queryset.get(remote_identifier=instance.remote_identifier) except ObjectDoesNotExist: return diff --git a/src/newsreader/news/collection/response_handler.py b/src/newsreader/news/collection/response_handler.py index dc33190..ff46171 100644 --- a/src/newsreader/news/collection/response_handler.py +++ b/src/newsreader/news/collection/response_handler.py @@ -1,3 +1,7 @@ +from typing import ContextManager + +from requests import Response + from newsreader.news.collection.exceptions import ( StreamDeniedException, StreamForbiddenException, @@ -14,17 +18,17 @@ class ResponseHandler: 408: StreamTimeOutException, } - def __init__(self, response): + def __init__(self, response: Response) -> None: self.response = response - def __enter__(self): + def __enter__(self) -> ContextManager: return self - def handle_response(self): + def handle_response(self) -> None: status_code = self.response.status_code if status_code in self.message_mapping: raise self.message_mapping[status_code] - def __exit__(self, *args, **kwargs): + def __exit__(self, *args, **kwargs) -> None: self.response = None diff --git a/src/newsreader/news/collection/tests/feed/client/tests.py b/src/newsreader/news/collection/tests/feed/client/tests.py index 0bb4cdd..e236666 100644 --- a/src/newsreader/news/collection/tests/feed/client/tests.py +++ b/src/newsreader/news/collection/tests/feed/client/tests.py @@ -6,7 +6,6 @@ from django.utils import timezone from newsreader.news.collection.exceptions import ( StreamDeniedException, StreamException, - StreamFieldException, StreamNotFoundException, StreamTimeOutException, ) @@ -17,9 +16,7 @@ from newsreader.news.collection.tests.feed.client.mocks import simple_mock class FeedClientTestCase(TestCase): def setUp(self): - self.patched_read = patch( - 'newsreader.news.collection.feed.FeedStream.read' - ) + self.patched_read = patch("newsreader.news.collection.feed.FeedStream.read") self.mocked_read = self.patched_read.start() def tearDown(self): diff --git a/src/newsreader/news/collection/utils.py b/src/newsreader/news/collection/utils.py index 6a80ed7..172fb54 100644 --- a/src/newsreader/news/collection/utils.py +++ b/src/newsreader/news/collection/utils.py @@ -1,10 +1,11 @@ -from datetime import datetime -from time import mktime +from datetime import datetime, tzinfo +from time import mktime, struct_time +from typing import Tuple from django.utils import timezone -def build_publication_date(dt, tz): +def build_publication_date(dt: struct_time, tz: tzinfo) -> Tuple: try: naive_datetime = datetime.fromtimestamp(mktime(dt)) published_parsed = timezone.make_aware(naive_datetime, timezone=tz) From 48a9b25545770db48e318e3250919b134deef53d Mon Sep 17 00:00:00 2001 From: Sonny Date: Mon, 1 Jul 2019 09:36:01 +0200 Subject: [PATCH 005/364] Favicon fetcher --- src/newsreader/news/collection/admin.py | 15 +- src/newsreader/news/collection/base.py | 46 ++++- src/newsreader/news/collection/favicon.py | 113 +++++++++++++ src/newsreader/news/collection/feed.py | 8 +- .../management/commands/fetch_favicons.py | 11 ++ .../migrations/0007_auto_20190623_1837.py | 16 ++ .../migrations/0008_auto_20190623_1847.py | 16 ++ .../0009_collectionrule_website_url.py | 16 ++ .../migrations/0010_auto_20190628_2142.py | 19 +++ src/newsreader/news/collection/models.py | 8 +- .../news/collection/tests/__init__.py | 3 + .../news/collection/tests/factories.py | 1 + .../news/collection/tests/favicon/__init__.py | 3 + .../tests/favicon/builder/__init__.py | 1 + .../collection/tests/favicon/builder/mocks.py | 88 ++++++++++ .../collection/tests/favicon/builder/tests.py | 60 +++++++ .../tests/favicon/client/__init__.py | 1 + .../collection/tests/favicon/client/mocks.py | 12 ++ .../collection/tests/favicon/client/tests.py | 91 ++++++++++ .../tests/favicon/collector/__init__.py | 1 + .../tests/favicon/collector/mocks.py | 159 ++++++++++++++++++ .../tests/favicon/collector/tests.py | 147 ++++++++++++++++ .../collection/tests/feed/builder/tests.py | 105 ++++-------- .../collection/tests/feed/client/tests.py | 19 ++- .../collection/tests/feed/collector/tests.py | 85 +++++----- .../tests/feed/duplicate_handler/tests.py | 8 +- .../collection/tests/feed/stream/mocks.py | 111 ++++++------ .../collection/tests/feed/stream/tests.py | 47 +++--- src/newsreader/news/collection/tests/mocks.py | 47 ++++++ src/newsreader/news/collection/tests/tests.py | 134 +++++++++++++++ .../news/collection/tests/utils/__init__.py | 1 + .../news/collection/tests/utils/tests.py | 57 +++++++ src/newsreader/news/collection/utils.py | 17 +- 33 files changed, 1238 insertions(+), 228 deletions(-) create mode 100644 src/newsreader/news/collection/favicon.py create mode 100644 src/newsreader/news/collection/management/commands/fetch_favicons.py create mode 100644 src/newsreader/news/collection/migrations/0007_auto_20190623_1837.py create mode 100644 src/newsreader/news/collection/migrations/0008_auto_20190623_1847.py create mode 100644 src/newsreader/news/collection/migrations/0009_collectionrule_website_url.py create mode 100644 src/newsreader/news/collection/migrations/0010_auto_20190628_2142.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/mocks.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 diff --git a/src/newsreader/news/collection/admin.py b/src/newsreader/news/collection/admin.py index 972b020..77ae900 100644 --- a/src/newsreader/news/collection/admin.py +++ b/src/newsreader/news/collection/admin.py @@ -4,20 +4,9 @@ from newsreader.news.collection.models import CollectionRule class CollectionRuleAdmin(admin.ModelAdmin): - fields = ( - "url", - "name", - "timezone", - "category", - ) + fields = ("url", "name", "timezone", "category", "favicon") - list_display = ( - "name", - "category", - "url", - "last_suceeded", - "succeeded", - ) + list_display = ("name", "category", "url", "last_suceeded", "succeeded") admin.site.register(CollectionRule, CollectionRuleAdmin) diff --git a/src/newsreader/news/collection/base.py b/src/newsreader/news/collection/base.py index 0ab25d3..2f392b7 100644 --- a/src/newsreader/news/collection/base.py +++ b/src/newsreader/news/collection/base.py @@ -2,9 +2,13 @@ from typing import ContextManager, Dict, List, Optional, Tuple import requests +from bs4 import BeautifulSoup + from django.utils import timezone +from newsreader.news.collection.exceptions import StreamParseException from newsreader.news.collection.models import CollectionRule +from newsreader.news.collection.utils import fetch class Stream: @@ -12,9 +16,7 @@ class Stream: self.rule = rule def read(self) -> Tuple: - url = self.rule.url - response = requests.get(url) - return (self.parse(response.content), self) + raise NotImplementedError def parse(self, payload: bytes) -> Dict: raise NotImplementedError @@ -45,7 +47,7 @@ class Client: class Builder: instances = [] - def __init__(self, stream: Stream) -> None: + def __init__(self, stream: Tuple) -> None: self.stream = stream def __enter__(self) -> ContextManager: @@ -81,3 +83,39 @@ class Collector: class Meta: abstract = True + + +class WebsiteStream(Stream): + def __init__(self, url: str) -> None: + self.url = url + + def read(self) -> Tuple: + response = fetch(self.url) + + return (self.parse(response.content), self) + + def parse(self, payload: bytes) -> BeautifulSoup: + try: + return BeautifulSoup(payload, "lxml") + except TypeError: + raise StreamParseException("Could not parse given HTML") + + +class URLBuilder(Builder): + def __enter__(self) -> ContextManager: + return self + + def build(self) -> Tuple: + 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/favicon.py b/src/newsreader/news/collection/favicon.py new file mode 100644 index 0000000..8270692 --- /dev/null +++ b/src/newsreader/news/collection/favicon.py @@ -0,0 +1,113 @@ +from concurrent.futures import ThreadPoolExecutor, as_completed +from typing import ContextManager, List, Optional +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) -> None: + rule, soup = self.stream + + url = self.parse(soup, rule.website_url) + + if url: + rule.favicon = url + rule.save() + + def parse(self, soup, website_url) -> Optional[str]: + 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: List) -> Optional[str]: + 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: List) -> None: + self.streams = streams + + def __enter__(self) -> ContextManager: + 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: Optional[List] = None) -> 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 index e2c098f..ef66b69 100644 --- a/src/newsreader/news/collection/feed.py +++ b/src/newsreader/news/collection/feed.py @@ -3,7 +3,6 @@ from typing import ContextManager, Dict, Generator, List, Optional, Tuple import bleach import pytz -import requests from feedparser import parse @@ -19,7 +18,7 @@ from newsreader.news.collection.exceptions import ( ) from newsreader.news.collection.models import CollectionRule from newsreader.news.collection.response_handler import ResponseHandler -from newsreader.news.collection.utils import build_publication_date +from newsreader.news.collection.utils import build_publication_date, fetch from newsreader.news.posts.models import Post @@ -92,10 +91,7 @@ class FeedBuilder(Builder): class FeedStream(Stream): def read(self) -> Tuple: url = self.rule.url - response = requests.get(url) - - with ResponseHandler(response) as response_handler: - response_handler.handle_response() + response = fetch(url) return (self.parse(response.content), self) 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/0007_auto_20190623_1837.py b/src/newsreader/news/collection/migrations/0007_auto_20190623_1837.py new file mode 100644 index 0000000..cc27d6e --- /dev/null +++ b/src/newsreader/news/collection/migrations/0007_auto_20190623_1837.py @@ -0,0 +1,16 @@ +# Generated by Django 2.2 on 2019-06-23 18:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [("collection", "0006_collectionrule_error")] + + operations = [ + migrations.AlterField( + model_name="collectionrule", + name="favicon", + field=models.ImageField(default="favicons/default-favicon.ico", upload_to="favicons/"), + ) + ] diff --git a/src/newsreader/news/collection/migrations/0008_auto_20190623_1847.py b/src/newsreader/news/collection/migrations/0008_auto_20190623_1847.py new file mode 100644 index 0000000..3c8ae66 --- /dev/null +++ b/src/newsreader/news/collection/migrations/0008_auto_20190623_1847.py @@ -0,0 +1,16 @@ +# Generated by Django 2.2 on 2019-06-23 18:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [("collection", "0007_auto_20190623_1837")] + + operations = [ + migrations.AlterField( + model_name="collectionrule", + name="favicon", + field=models.URLField(blank=True, null=True), + ) + ] diff --git a/src/newsreader/news/collection/migrations/0009_collectionrule_website_url.py b/src/newsreader/news/collection/migrations/0009_collectionrule_website_url.py new file mode 100644 index 0000000..e5273b3 --- /dev/null +++ b/src/newsreader/news/collection/migrations/0009_collectionrule_website_url.py @@ -0,0 +1,16 @@ +# Generated by Django 2.2 on 2019-06-27 21:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [("collection", "0008_auto_20190623_1847")] + + operations = [ + migrations.AddField( + model_name="collectionrule", + name="website_url", + field=models.URLField(blank=True, editable=False, null=True), + ) + ] diff --git a/src/newsreader/news/collection/migrations/0010_auto_20190628_2142.py b/src/newsreader/news/collection/migrations/0010_auto_20190628_2142.py new file mode 100644 index 0000000..3726158 --- /dev/null +++ b/src/newsreader/news/collection/migrations/0010_auto_20190628_2142.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2 on 2019-06-28 21:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [("collection", "0009_collectionrule_website_url")] + + operations = [ + migrations.AlterField( + model_name="collectionrule", name="url", field=models.URLField(max_length=1024) + ), + migrations.AlterField( + model_name="collectionrule", + name="website_url", + field=models.URLField(blank=True, editable=False, max_length=1024, null=True), + ), + ] diff --git a/src/newsreader/news/collection/models.py b/src/newsreader/news/collection/models.py index ffb4131..1de9849 100644 --- a/src/newsreader/news/collection/models.py +++ b/src/newsreader/news/collection/models.py @@ -1,5 +1,6 @@ import pytz +from django.conf import settings from django.db import models from django.utils.translation import gettext as _ @@ -8,8 +9,9 @@ class CollectionRule(models.Model): name = models.CharField(max_length=100) source = models.CharField(max_length=100) - url = models.URLField() - favicon = models.ImageField(blank=True, null=True) + 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), @@ -23,7 +25,7 @@ class CollectionRule(models.Model): null=True, verbose_name=_("Category"), help_text=_("Posts from this rule will be tagged with this category"), - on_delete=models.SET_NULL + on_delete=models.SET_NULL, ) last_suceeded = models.DateTimeField(blank=True, null=True) diff --git a/src/newsreader/news/collection/tests/__init__.py b/src/newsreader/news/collection/tests/__init__.py index fb6723f..ea6a7c0 100644 --- a/src/newsreader/news/collection/tests/__init__.py +++ b/src/newsreader/news/collection/tests/__init__.py @@ -1 +1,4 @@ +from .favicon import * from .feed import * +from .tests import * +from .utils import * diff --git a/src/newsreader/news/collection/tests/factories.py b/src/newsreader/news/collection/tests/factories.py index 6b42292..be54806 100644 --- a/src/newsreader/news/collection/tests/factories.py +++ b/src/newsreader/news/collection/tests/factories.py @@ -10,3 +10,4 @@ class CollectionRuleFactory(factory.django.DjangoModelFactory): name = factory.Sequence(lambda n: "CollectionRule-{}".format(n)) source = factory.Faker("name") url = factory.Faker("url") + website_url = factory.Faker("url") 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..5fb0299 --- /dev/null +++ b/src/newsreader/news/collection/tests/favicon/__init__.py @@ -0,0 +1,3 @@ +from .builder import * +from .client import * +from .collector import * 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..8baa6e5 --- /dev/null +++ b/src/newsreader/news/collection/tests/favicon/builder/__init__.py @@ -0,0 +1 @@ +from .tests import * 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..ce02475 --- /dev/null +++ b/src/newsreader/news/collection/tests/favicon/builder/mocks.py @@ -0,0 +1,88 @@ +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..d08fce7 --- /dev/null +++ b/src/newsreader/news/collection/tests/favicon/builder/tests.py @@ -0,0 +1,60 @@ +from freezegun import freeze_time + +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..8baa6e5 --- /dev/null +++ b/src/newsreader/news/collection/tests/favicon/client/__init__.py @@ -0,0 +1 @@ +from .tests import * 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..ba79b27 --- /dev/null +++ b/src/newsreader/news/collection/tests/favicon/client/mocks.py @@ -0,0 +1,12 @@ +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..4ac2a40 --- /dev/null +++ b/src/newsreader/news/collection/tests/favicon/client/tests.py @@ -0,0 +1,91 @@ +from unittest.mock import MagicMock + +from django.test import TestCase + +from newsreader.news.collection.base import WebsiteStream +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..8baa6e5 --- /dev/null +++ b/src/newsreader/news/collection/tests/favicon/collector/__init__.py @@ -0,0 +1 @@ +from .tests import * 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..8e58167 --- /dev/null +++ b/src/newsreader/news/collection/tests/favicon/collector/mocks.py @@ -0,0 +1,159 @@ +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..6554de4 --- /dev/null +++ b/src/newsreader/news/collection/tests/favicon/collector/tests.py @@ -0,0 +1,147 @@ +from unittest.mock import MagicMock, patch + +import pytz + +from bs4 import BeautifulSoup + +from .mocks import feed_mock, website_mock + +from django.test import TestCase +from django.utils import timezone + +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 + + +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/builder/tests.py b/src/newsreader/news/collection/tests/feed/builder/tests.py index 6e4e44d..49a130a 100644 --- a/src/newsreader/news/collection/tests/feed/builder/tests.py +++ b/src/newsreader/news/collection/tests/feed/builder/tests.py @@ -5,26 +5,27 @@ import pytz from freezegun import freeze_time +from .mocks import * + from django.test import TestCase from django.utils import timezone from newsreader.news.collection.feed import FeedBuilder from newsreader.news.collection.tests.factories import CollectionRuleFactory -from newsreader.news.collection.tests.feed.builder.mocks import * from newsreader.news.posts.models import Post from newsreader.news.posts.tests.factories import PostFactory class FeedBuilderTestCase(TestCase): def setUp(self): - pass + 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: + with builder((simple_mock, mock_stream)) as builder: builder.save() post = Post.objects.get() @@ -36,26 +37,19 @@ class FeedBuilderTestCase(TestCase): self.assertEquals(Post.objects.count(), 1) self.assertEquals( - post.remote_identifier, - "https://www.bbc.co.uk/news/world-us-canada-48338168" + 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.url, "https://www.bbc.co.uk/news/world-us-canada-48338168") - self.assertEquals( - post.title, - "Trump's 'genocidal taunts' will not end Iran - Zarif" - ) + 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: + with builder((multiple_mock, mock_stream)) as builder: builder.save() posts = Post.objects.order_by("id") @@ -70,19 +64,12 @@ class FeedBuilderTestCase(TestCase): self.assertEquals(first_post.publication_date, aware_date) self.assertEquals( - first_post.remote_identifier, - "https://www.bbc.co.uk/news/world-us-canada-48338168" + 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.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" - ) + 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) @@ -90,26 +77,19 @@ class FeedBuilderTestCase(TestCase): self.assertEquals(second_post.publication_date, aware_date) self.assertEquals( - second_post.remote_identifier, - "https://www.bbc.co.uk/news/technology-48334739" + 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.url, "https://www.bbc.co.uk/news/technology-48334739") - self.assertEquals( - second_post.title, - "Huawei's Android loss: How it affects you" - ) + 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: + with builder((mock_without_identifier, mock_stream)) as builder: builder.save() posts = Post.objects.order_by("id") @@ -124,15 +104,9 @@ class FeedBuilderTestCase(TestCase): 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.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" - ) + 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): @@ -140,7 +114,7 @@ class FeedBuilderTestCase(TestCase): rule = CollectionRuleFactory() mock_stream = MagicMock(rule=rule) - with builder((mock_without_publish_date, mock_stream,)) as builder: + with builder((mock_without_publish_date, mock_stream)) as builder: builder.save() posts = Post.objects.order_by("id") @@ -151,14 +125,12 @@ class FeedBuilderTestCase(TestCase): self.assertEquals(first_post.created, timezone.now()) self.assertEquals( - first_post.remote_identifier, - 'https://www.bbc.co.uk/news/world-us-canada-48338168' + 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' + second_post.remote_identifier, "https://www.bbc.co.uk/news/technology-48334739" ) @freeze_time("2019-10-30 12:30:00") @@ -167,7 +139,7 @@ class FeedBuilderTestCase(TestCase): rule = CollectionRuleFactory() mock_stream = MagicMock(rule=rule) - with builder((mock_without_url, mock_stream,)) as builder: + with builder((mock_without_url, mock_stream)) as builder: builder.save() posts = Post.objects.order_by("id") @@ -178,14 +150,12 @@ class FeedBuilderTestCase(TestCase): self.assertEquals(first_post.created, timezone.now()) self.assertEquals( - first_post.remote_identifier, - 'https://www.bbc.co.uk/news/world-us-canada-48338168' + 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' + second_post.remote_identifier, "https://www.bbc.co.uk/news/technology-48334739" ) @freeze_time("2019-10-30 12:30:00") @@ -194,7 +164,7 @@ class FeedBuilderTestCase(TestCase): rule = CollectionRuleFactory() mock_stream = MagicMock(rule=rule) - with builder((mock_without_body, mock_stream,)) as builder: + with builder((mock_without_body, mock_stream)) as builder: builder.save() posts = Post.objects.order_by("id") @@ -205,14 +175,13 @@ class FeedBuilderTestCase(TestCase): self.assertEquals(first_post.created, timezone.now()) self.assertEquals( - first_post.remote_identifier, - 'https://www.bbc.co.uk/news/world-us-canada-48338168' + 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' + "https://www.bbc.co.uk/news/uk-england-birmingham-48339080", ) @freeze_time("2019-10-30 12:30:00") @@ -221,7 +190,7 @@ class FeedBuilderTestCase(TestCase): rule = CollectionRuleFactory() mock_stream = MagicMock(rule=rule) - with builder((mock_without_author, mock_stream,)) as builder: + with builder((mock_without_author, mock_stream)) as builder: builder.save() posts = Post.objects.order_by("id") @@ -232,14 +201,12 @@ class FeedBuilderTestCase(TestCase): self.assertEquals(first_post.created, timezone.now()) self.assertEquals( - first_post.remote_identifier, - 'https://www.bbc.co.uk/news/world-us-canada-48338168' + 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' + second_post.remote_identifier, "https://www.bbc.co.uk/news/technology-48334739" ) def test_empty_entries(self): @@ -247,7 +214,7 @@ class FeedBuilderTestCase(TestCase): rule = CollectionRuleFactory() mock_stream = MagicMock(rule=rule) - with builder((mock_without_entries, mock_stream,)) as builder: + with builder((mock_without_entries, mock_stream)) as builder: builder.save() self.assertEquals(Post.objects.count(), 0) @@ -265,7 +232,7 @@ class FeedBuilderTestCase(TestCase): remote_identifier="a5479c66-8fae-11e9-8422-00163ef6bee7", rule=rule ) - with builder((mock_with_update_entries, mock_stream,)) as builder: + with builder((mock_with_update_entries, mock_stream)) as builder: builder.save() self.assertEquals(Post.objects.count(), 3) @@ -274,21 +241,17 @@ class FeedBuilderTestCase(TestCase): existing_second_post.refresh_from_db() self.assertEquals( - existing_first_post.title, - "Trump's 'genocidal taunts' will not end Iran - Zarif" + 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" - ) + 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: + with builder((mock_with_html, mock_stream)) as builder: builder.save() post = Post.objects.get() diff --git a/src/newsreader/news/collection/tests/feed/client/tests.py b/src/newsreader/news/collection/tests/feed/client/tests.py index e236666..e49762d 100644 --- a/src/newsreader/news/collection/tests/feed/client/tests.py +++ b/src/newsreader/news/collection/tests/feed/client/tests.py @@ -1,5 +1,7 @@ from unittest.mock import MagicMock, patch +from .mocks import simple_mock + from django.test import TestCase from django.utils import timezone @@ -7,15 +9,17 @@ from newsreader.news.collection.exceptions import ( StreamDeniedException, StreamException, StreamNotFoundException, + StreamParseException, StreamTimeOutException, ) from newsreader.news.collection.feed import FeedClient from newsreader.news.collection.tests.factories import CollectionRuleFactory -from newsreader.news.collection.tests.feed.client.mocks import simple_mock class FeedClientTestCase(TestCase): def setUp(self): + self.maxDiff = None + self.patched_read = patch("newsreader.news.collection.feed.FeedStream.read") self.mocked_read = self.patched_read.start() @@ -85,3 +89,16 @@ class FeedClientTestCase(TestCase): self.assertEquals(stream.rule.succeeded, False) self.mocked_read.assert_called_once_with() + + def test_client_catches_stream_parse_exception(self): + rule = CollectionRuleFactory.create() + mock_stream = MagicMock(rule=rule) + self.mocked_read.side_effect = StreamParseException("Stream has wrong contents") + + with FeedClient([rule]) as client: + for data, stream in client: + self.assertEquals(data, {"entries": []}) + self.assertEquals(stream.rule.error, "Stream has wrong contents") + self.assertEquals(stream.rule.succeeded, False) + + self.mocked_read.assert_called_once_with() diff --git a/src/newsreader/news/collection/tests/feed/collector/tests.py b/src/newsreader/news/collection/tests/feed/collector/tests.py index db95ccb..0d14730 100644 --- a/src/newsreader/news/collection/tests/feed/collector/tests.py +++ b/src/newsreader/news/collection/tests/feed/collector/tests.py @@ -6,17 +6,26 @@ import pytz from freezegun import freeze_time -from django.test import TestCase -from django.utils import timezone - -from newsreader.news.collection.feed import FeedCollector -from newsreader.news.collection.tests.factories import CollectionRuleFactory -from newsreader.news.collection.tests.feed.collector.mocks import ( +from .mocks import ( duplicate_mock, empty_mock, multiple_mock, multiple_update_mock, ) + +from django.test import TestCase +from django.utils import timezone + +from newsreader.news.collection.exceptions import ( + StreamDeniedException, + StreamException, + StreamForbiddenException, + StreamNotFoundException, + StreamParseException, + StreamTimeOutException, +) +from newsreader.news.collection.feed import FeedCollector +from newsreader.news.collection.tests.factories import CollectionRuleFactory from newsreader.news.collection.utils import build_publication_date from newsreader.news.posts.models import Post from newsreader.news.posts.tests.factories import PostFactory @@ -24,14 +33,12 @@ from newsreader.news.posts.tests.factories import PostFactory class FeedCollectorTestCase(TestCase): def setUp(self): - self.patched_get = patch( - 'newsreader.news.collection.feed.requests.get' - ) - self.mocked_get = self.patched_get.start() + self.maxDiff = None - self.patched_parse = patch( - 'newsreader.news.collection.feed.FeedStream.parse' - ) + self.patched_get = patch("newsreader.news.collection.feed.fetch") + self.mocked_fetch = self.patched_get.start() + + self.patched_parse = patch("newsreader.news.collection.feed.FeedStream.parse") self.mocked_parse = self.patched_parse.start() def tearDown(self): @@ -54,7 +61,7 @@ class FeedCollectorTestCase(TestCase): @freeze_time("2019-10-30 12:30:00") def test_emtpy_batch(self): - self.mocked_get.return_value = MagicMock(status_code=200) + self.mocked_fetch.return_value = MagicMock() self.mocked_parse.return_value = empty_mock rule = CollectionRuleFactory() @@ -69,7 +76,7 @@ class FeedCollectorTestCase(TestCase): self.assertEquals(rule.last_suceeded, timezone.now()) def test_not_found(self): - self.mocked_get.return_value = MagicMock(status_code=404) + self.mocked_fetch.side_effect = StreamNotFoundException rule = CollectionRuleFactory() collector = FeedCollector() @@ -82,7 +89,7 @@ class FeedCollectorTestCase(TestCase): self.assertEquals(rule.error, "Stream not found") def test_denied(self): - self.mocked_get.return_value = MagicMock(status_code=404) + self.mocked_fetch.side_effect = StreamDeniedException last_suceeded = timezone.make_aware( datetime.combine(date=date(2019, 10, 30), time=time(12, 30)) ) @@ -95,11 +102,11 @@ class FeedCollectorTestCase(TestCase): self.assertEquals(Post.objects.count(), 0) self.assertEquals(rule.succeeded, False) - self.assertEquals(rule.error, "Stream not found") + self.assertEquals(rule.error, "Stream does not have sufficient permissions") self.assertEquals(rule.last_suceeded, last_suceeded) def test_forbidden(self): - self.mocked_get.return_value = MagicMock(status_code=403) + self.mocked_fetch.side_effect = StreamForbiddenException last_suceeded = timezone.make_aware( datetime.combine(date=date(2019, 10, 30), time=time(12, 30)) ) @@ -116,7 +123,7 @@ class FeedCollectorTestCase(TestCase): self.assertEquals(rule.last_suceeded, last_suceeded) def test_timed_out(self): - self.mocked_get.return_value = MagicMock(status_code=408) + self.mocked_fetch.side_effect = StreamTimeOutException last_suceeded = timezone.make_aware( datetime.combine(date=date(2019, 10, 30), time=time(12, 30)) ) @@ -138,8 +145,7 @@ class FeedCollectorTestCase(TestCase): rule = CollectionRuleFactory() _, aware_datetime = build_publication_date( - struct_time((2019, 5, 20, 16, 7, 37, 0, 140, 0)), - pytz.utc + struct_time((2019, 5, 20, 16, 7, 37, 0, 140, 0)), pytz.utc ) first_post = PostFactory( @@ -148,12 +154,11 @@ class FeedCollectorTestCase(TestCase): body="Foreign Minister Mohammad Javad Zarif says the US " "president should try showing Iranians some respect.", publication_date=aware_datetime, - rule=rule + rule=rule, ) _, aware_datetime = build_publication_date( - struct_time((2019, 5, 20, 12, 19, 19, 0, 140, 0,)), - pytz.utc + struct_time((2019, 5, 20, 12, 19, 19, 0, 140, 0)), pytz.utc ) second_post = PostFactory( @@ -162,22 +167,21 @@ class FeedCollectorTestCase(TestCase): body="Google's move to end business ties with Huawei will " "affect current devices and future purchases.", publication_date=aware_datetime, - rule=rule + rule=rule, ) _, aware_datetime = build_publication_date( - struct_time((2019, 5, 20, 16, 32, 38, 0, 140, 0)), - pytz.utc + struct_time((2019, 5, 20, 16, 32, 38, 0, 140, 0)), pytz.utc ) third_post = PostFactory( url="https://www.bbc.co.uk/news/uk-england-birmingham-48339080", title="Birmingham head teacher threatened over LGBT lessons", body="Police are investigating the messages while an MP " - "calls for a protest exclusion zone \"to protect " - "children\".", + 'calls for a protest exclusion zone "to protect ' + 'children".', publication_date=aware_datetime, - rule=rule + rule=rule, ) collector = FeedCollector() @@ -201,7 +205,7 @@ class FeedCollectorTestCase(TestCase): title="Trump", body="Foreign Minister Mohammad Javad Zarif", publication_date=timezone.now(), - rule=rule + rule=rule, ) second_post = PostFactory( @@ -210,7 +214,7 @@ class FeedCollectorTestCase(TestCase): title="Huawei's Android loss: How it affects you", body="Google's move to end business ties with Huawei will", publication_date=timezone.now(), - rule=rule + rule=rule, ) third_post = PostFactory( @@ -219,7 +223,7 @@ class FeedCollectorTestCase(TestCase): title="Birmingham head teacher threatened over LGBT lessons", body="Police are investigating the messages while an MP", publication_date=timezone.now(), - rule=rule + rule=rule, ) collector = FeedCollector() @@ -235,17 +239,8 @@ class FeedCollectorTestCase(TestCase): self.assertEquals(rule.last_suceeded, timezone.now()) self.assertEquals(rule.error, None) - self.assertEquals( - first_post.title, - "Trump's 'genocidal taunts' will not end Iran - Zarif" - ) + self.assertEquals(first_post.title, "Trump's 'genocidal taunts' will not end Iran - Zarif") - self.assertEquals( - second_post.title, - "Huawei's Android loss: How it affects you" - ) + self.assertEquals(second_post.title, "Huawei's Android loss: How it affects you") - self.assertEquals( - third_post.title, - 'Birmingham head teacher threatened over LGBT lessons' - ) + self.assertEquals(third_post.title, "Birmingham head teacher threatened over LGBT lessons") diff --git a/src/newsreader/news/collection/tests/feed/duplicate_handler/tests.py b/src/newsreader/news/collection/tests/feed/duplicate_handler/tests.py index a8600af..eff63cc 100644 --- a/src/newsreader/news/collection/tests/feed/duplicate_handler/tests.py +++ b/src/newsreader/news/collection/tests/feed/duplicate_handler/tests.py @@ -9,7 +9,7 @@ from newsreader.news.posts.tests.factories import PostFactory class FeedDuplicateHandlerTestCase(TestCase): def setUp(self): - pass + self.maxDiff = None def test_duplicate_entries_with_remote_identifiers(self): rule = CollectionRuleFactory() @@ -19,7 +19,7 @@ class FeedDuplicateHandlerTestCase(TestCase): new_post = PostFactory.build( remote_identifier="28f79ae4-8f9a-11e9-b143-00163ef6bee7", title="title got updated", - rule=rule + rule=rule, ) with FeedDuplicateHandler(rule) as duplicate_handler: @@ -45,7 +45,7 @@ class FeedDuplicateHandlerTestCase(TestCase): body="Google's move to end business ties with Huawei will affect current devices", publication_date=publication_date, remote_identifier=None, - rule=rule + rule=rule, ) new_post = PostFactory.build( url="https://www.bbc.co.uk/news/uk-england-birmingham-48339080", @@ -53,7 +53,7 @@ class FeedDuplicateHandlerTestCase(TestCase): body="Google's move to end business ties with Huawei will affect current devices", publication_date=publication_date, remote_identifier=None, - rule=rule + rule=rule, ) with FeedDuplicateHandler(rule) as duplicate_handler: diff --git a/src/newsreader/news/collection/tests/feed/stream/mocks.py b/src/newsreader/news/collection/tests/feed/stream/mocks.py index 5853eb7..a098383 100644 --- a/src/newsreader/news/collection/tests/feed/stream/mocks.py +++ b/src/newsreader/news/collection/tests/feed/stream/mocks.py @@ -1,61 +1,62 @@ from time import struct_time simple_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' - } - }], - '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/' + "bozo": 1, + "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.", }, - 'links': [{ - 'href': 'https://www.bbc.co.uk/news/', - 'rel': 'alternate', - 'type': 'text/html' - }], - 'title': 'BBC News - Home', + "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", + }, + } + ], + "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/", + }, + "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' + "href": "http://feeds.bbci.co.uk/news/rss.xml", + "status": 200, + "version": "rss20", } diff --git a/src/newsreader/news/collection/tests/feed/stream/tests.py b/src/newsreader/news/collection/tests/feed/stream/tests.py index 6e15194..3a7811b 100644 --- a/src/newsreader/news/collection/tests/feed/stream/tests.py +++ b/src/newsreader/news/collection/tests/feed/stream/tests.py @@ -1,5 +1,7 @@ from unittest.mock import MagicMock, patch +from .mocks import simple_mock + from django.test import TestCase from django.utils import timezone @@ -13,36 +15,32 @@ from newsreader.news.collection.exceptions import ( ) from newsreader.news.collection.feed import FeedStream from newsreader.news.collection.tests.factories import CollectionRuleFactory -from newsreader.news.collection.tests.feed.stream.mocks import simple_mock class FeedStreamTestCase(TestCase): def setUp(self): - self.patched_get = patch( - 'newsreader.news.collection.feed.requests.get' - ) - self.mocked_get = self.patched_get.start() + self.maxDiff = None - self.patched_parse = patch( - 'newsreader.news.collection.feed.FeedStream.parse' - ) - self.mocked_parse = self.patched_parse.start() + self.patched_fetch = patch("newsreader.news.collection.feed.fetch") + self.mocked_fetch = self.patched_fetch.start() def tearDown(self): patch.stopall() def test_simple_stream(self): - self.mocked_parse.return_value = simple_mock + self.mocked_fetch.return_value = MagicMock(content=simple_mock) rule = CollectionRuleFactory() stream = FeedStream(rule) - return_value = stream.read() - self.mocked_get.assert_called_once_with(rule.url) - self.assertEquals(return_value, (simple_mock, stream)) + data, stream = stream.read() + + self.mocked_fetch.assert_called_once_with(rule.url) + self.assertEquals(data["entries"], data["entries"]) + self.assertEquals(stream, stream) def test_stream_raises_exception(self): - self.mocked_parse.side_effect = StreamException + self.mocked_fetch.side_effect = StreamException rule = CollectionRuleFactory() stream = FeedStream(rule) @@ -50,10 +48,10 @@ class FeedStreamTestCase(TestCase): with self.assertRaises(StreamException): stream.read() - self.mocked_get.assert_called_once_with(rule.url) + self.mocked_fetch.assert_called_once_with(rule.url) def test_stream_raises_denied_exception(self): - self.mocked_get.return_value = MagicMock(status_code=401) + self.mocked_fetch.side_effect = StreamDeniedException rule = CollectionRuleFactory() stream = FeedStream(rule) @@ -61,10 +59,10 @@ class FeedStreamTestCase(TestCase): with self.assertRaises(StreamDeniedException): stream.read() - self.mocked_get.assert_called_once_with(rule.url) + self.mocked_fetch.assert_called_once_with(rule.url) def test_stream_raises_not_found_exception(self): - self.mocked_get.return_value = MagicMock(status_code=404) + self.mocked_fetch.side_effect = StreamNotFoundException rule = CollectionRuleFactory() stream = FeedStream(rule) @@ -72,10 +70,10 @@ class FeedStreamTestCase(TestCase): with self.assertRaises(StreamNotFoundException): stream.read() - self.mocked_get.assert_called_once_with(rule.url) + self.mocked_fetch.assert_called_once_with(rule.url) def test_stream_raises_time_out_exception(self): - self.mocked_get.return_value = MagicMock(status_code=408) + self.mocked_fetch.side_effect = StreamTimeOutException rule = CollectionRuleFactory() stream = FeedStream(rule) @@ -83,10 +81,10 @@ class FeedStreamTestCase(TestCase): with self.assertRaises(StreamTimeOutException): stream.read() - self.mocked_get.assert_called_once_with(rule.url) + self.mocked_fetch.assert_called_once_with(rule.url) def test_stream_raises_forbidden_exception(self): - self.mocked_get.return_value = MagicMock(status_code=403) + self.mocked_fetch.side_effect = StreamForbiddenException rule = CollectionRuleFactory() stream = FeedStream(rule) @@ -94,13 +92,12 @@ class FeedStreamTestCase(TestCase): with self.assertRaises(StreamForbiddenException): stream.read() - self.mocked_get.assert_called_once_with(rule.url) + self.mocked_fetch.assert_called_once_with(rule.url) @patch("newsreader.news.collection.feed.parse") def test_stream_raises_parse_exception(self, mocked_parse): - self.mocked_get.return_value = MagicMock(status_code=200) + self.mocked_fetch.return_value = MagicMock() mocked_parse.side_effect = TypeError - self.patched_parse.stop() rule = CollectionRuleFactory() stream = FeedStream(rule) diff --git a/src/newsreader/news/collection/tests/mocks.py b/src/newsreader/news/collection/tests/mocks.py new file mode 100644 index 0000000..32ad699 --- /dev/null +++ b/src/newsreader/news/collection/tests/mocks.py @@ -0,0 +1,47 @@ +simple_mock = """ + + +
+

Clickbait

+
+ + +""" + +simple_feed_mock = { + "bozo": 0, + "encoding": "utf-8", + "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", +} + +feed_mock_without_link = { + "bozo": 0, + "encoding": "utf-8", + "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/", + }, + "title": "BBC News - Home", + }, + "href": "http://feeds.bbci.co.uk/news/rss.xml", + "status": 200, + "version": "rss20", +} diff --git a/src/newsreader/news/collection/tests/tests.py b/src/newsreader/news/collection/tests/tests.py new file mode 100644 index 0000000..08bc4e0 --- /dev/null +++ b/src/newsreader/news/collection/tests/tests.py @@ -0,0 +1,134 @@ +from unittest.mock import MagicMock, patch + +from bs4 import BeautifulSoup + +from .mocks import feed_mock_without_link, simple_feed_mock, simple_mock + +from django.test import TestCase + +from newsreader.news.collection.base import URLBuilder, WebsiteStream +from newsreader.news.collection.exceptions import ( + StreamDeniedException, + StreamException, + StreamForbiddenException, + StreamNotFoundException, + StreamParseException, + StreamTimeOutException, +) +from newsreader.news.collection.tests.factories import CollectionRuleFactory + + +class WebsiteStreamTestCase(TestCase): + def setUp(self): + self.patched_fetch = patch("newsreader.news.collection.base.fetch") + self.mocked_fetch = self.patched_fetch.start() + + def tearDown(self): + patch.stopall() + + def test_simple(self): + self.mocked_fetch.return_value = MagicMock(content=simple_mock) + + rule = CollectionRuleFactory() + stream = WebsiteStream(rule.url) + return_value = stream.read() + + self.mocked_fetch.assert_called_once_with(rule.url) + self.assertEquals(return_value, (BeautifulSoup(simple_mock, "lxml"), stream)) + + def test_raises_exception(self): + self.mocked_fetch.side_effect = StreamException + + rule = CollectionRuleFactory() + stream = WebsiteStream(rule.url) + + with self.assertRaises(StreamException): + stream.read() + + self.mocked_fetch.assert_called_once_with(rule.url) + + def test_raises_denied_exception(self): + self.mocked_fetch.side_effect = StreamDeniedException + + rule = CollectionRuleFactory() + stream = WebsiteStream(rule.url) + + with self.assertRaises(StreamDeniedException): + stream.read() + + self.mocked_fetch.assert_called_once_with(rule.url) + + def test_raises_stream_not_found_exception(self): + self.mocked_fetch.side_effect = StreamNotFoundException + + rule = CollectionRuleFactory() + stream = WebsiteStream(rule.url) + + with self.assertRaises(StreamNotFoundException): + stream.read() + + self.mocked_fetch.assert_called_once_with(rule.url) + + def test_stream_raises_time_out_exception(self): + self.mocked_fetch.side_effect = StreamTimeOutException + + rule = CollectionRuleFactory() + stream = WebsiteStream(rule.url) + + with self.assertRaises(StreamTimeOutException): + stream.read() + + self.mocked_fetch.assert_called_once_with(rule.url) + + def test_stream_raises_forbidden_exception(self): + self.mocked_fetch.side_effect = StreamForbiddenException + + rule = CollectionRuleFactory() + stream = WebsiteStream(rule.url) + + with self.assertRaises(StreamForbiddenException): + stream.read() + + self.mocked_fetch.assert_called_once_with(rule.url) + + @patch("newsreader.news.collection.base.WebsiteStream.parse") + def test_stream_raises_parse_exception(self, mocked_parse): + self.mocked_fetch.return_value = MagicMock() + mocked_parse.side_effect = StreamParseException + + rule = CollectionRuleFactory() + stream = WebsiteStream(rule.url) + + with self.assertRaises(StreamParseException): + stream.read() + + self.mocked_fetch.assert_called_once_with(rule.url) + + +class URLBuilderTestCase(TestCase): + def test_simple(self): + initial_rule = CollectionRuleFactory() + + with URLBuilder((simple_feed_mock, MagicMock(rule=initial_rule))) as builder: + rule, url = builder.build() + + self.assertEquals(rule.pk, initial_rule.pk) + self.assertEquals(url, "https://www.bbc.co.uk/news/") + + def test_no_link(self): + initial_rule = CollectionRuleFactory() + + with URLBuilder((feed_mock_without_link, MagicMock(rule=initial_rule))) as builder: + rule, url = builder.build() + + self.assertEquals(rule.pk, initial_rule.pk) + self.assertEquals(url, None) + + def test_no_data(self): + initial_rule = CollectionRuleFactory() + + with URLBuilder((None, MagicMock(rule=initial_rule))) as builder: + rule, url = builder.build() + + self.assertEquals(rule.pk, initial_rule.pk) + self.assertEquals(url, None) diff --git a/src/newsreader/news/collection/tests/utils/__init__.py b/src/newsreader/news/collection/tests/utils/__init__.py new file mode 100644 index 0000000..8baa6e5 --- /dev/null +++ b/src/newsreader/news/collection/tests/utils/__init__.py @@ -0,0 +1 @@ +from .tests import * diff --git a/src/newsreader/news/collection/tests/utils/tests.py b/src/newsreader/news/collection/tests/utils/tests.py new file mode 100644 index 0000000..3c95df0 --- /dev/null +++ b/src/newsreader/news/collection/tests/utils/tests.py @@ -0,0 +1,57 @@ +from unittest.mock import MagicMock, patch + +from django.test import TestCase + +from newsreader.news.collection.exceptions import ( + StreamDeniedException, + StreamForbiddenException, + StreamNotFoundException, + StreamTimeOutException, +) +from newsreader.news.collection.utils import fetch + + +class FetchTestCase(TestCase): + def setUp(self): + self.patched_get = patch("newsreader.news.collection.utils.requests.get") + self.mocked_get = self.patched_get.start() + + def test_simple(self): + self.mocked_get.return_value = MagicMock(status_code=200, content="content") + + url = "https://www.bbc.co.uk/news" + response = fetch(url) + + self.assertEquals(response.content, "content") + + def test_raises_not_found(self): + self.mocked_get.return_value = MagicMock(status_code=404) + + url = "https://www.bbc.co.uk/news" + + with self.assertRaises(StreamNotFoundException): + fetch(url) + + def test_raises_denied(self): + self.mocked_get.return_value = MagicMock(status_code=401) + + url = "https://www.bbc.co.uk/news" + + with self.assertRaises(StreamDeniedException): + fetch(url) + + def test_raises_forbidden(self): + self.mocked_get.return_value = MagicMock(status_code=403) + + url = "https://www.bbc.co.uk/news" + + with self.assertRaises(StreamForbiddenException): + fetch(url) + + def test_raises_timed_out(self): + self.mocked_get.return_value = MagicMock(status_code=408) + + url = "https://www.bbc.co.uk/news" + + with self.assertRaises(StreamTimeOutException): + fetch(url) diff --git a/src/newsreader/news/collection/utils.py b/src/newsreader/news/collection/utils.py index 172fb54..17f379a 100644 --- a/src/newsreader/news/collection/utils.py +++ b/src/newsreader/news/collection/utils.py @@ -1,9 +1,15 @@ from datetime import datetime, tzinfo from time import mktime, struct_time -from typing import Tuple +from typing import Optional, Tuple + +import requests + +from requests.models import Response from django.utils import timezone +from newsreader.news.collection.response_handler import ResponseHandler + def build_publication_date(dt: struct_time, tz: tzinfo) -> Tuple: try: @@ -12,3 +18,12 @@ def build_publication_date(dt: struct_time, tz: tzinfo) -> Tuple: except TypeError: return False, None return True, published_parsed + + +def fetch(url: str) -> Optional[Response]: + response = requests.get(url) + + with ResponseHandler(response) as response_handler: + response_handler.handle_response() + + return response From cfd064ec85977dbd15c2026310cf7ba6fdd6de3f Mon Sep 17 00:00:00 2001 From: Sonny Date: Mon, 1 Jul 2019 09:39:48 +0200 Subject: [PATCH 006/364] Add missing requirements --- requirements/base.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/requirements/base.txt b/requirements/base.txt index 68e2d7d..00667bb 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,7 +1,9 @@ bleach==3.1.0 +beautifulsoup4==4.7.1 certifi==2019.3.9 chardet==3.0.4 Django==2.2 +lxml==4.3.4 feedparser==5.2.1 idna==2.8 pytz==2018.9 From ed658c4dfdebf8a1ea18bcfc1c3844923cbd5519 Mon Sep 17 00:00:00 2001 From: Sonny Date: Mon, 1 Jul 2019 11:38:23 +0200 Subject: [PATCH 007/364] Handle request exceptions --- src/newsreader/news/collection/exceptions.py | 4 ++ .../news/collection/response_handler.py | 33 +++++++++--- .../news/collection/tests/utils/tests.py | 50 +++++++++++++++++++ src/newsreader/news/collection/utils.py | 15 +++--- 4 files changed, 88 insertions(+), 14 deletions(-) diff --git a/src/newsreader/news/collection/exceptions.py b/src/newsreader/news/collection/exceptions.py index 8e12da1..e636638 100644 --- a/src/newsreader/news/collection/exceptions.py +++ b/src/newsreader/news/collection/exceptions.py @@ -26,3 +26,7 @@ class StreamForbiddenException(StreamException): 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/response_handler.py b/src/newsreader/news/collection/response_handler.py index ff46171..e412475 100644 --- a/src/newsreader/news/collection/response_handler.py +++ b/src/newsreader/news/collection/response_handler.py @@ -1,9 +1,18 @@ from typing import ContextManager from requests import Response +from requests.exceptions import ConnectionError as RequestConnectionError +from requests.exceptions import ( + HTTPError, + RequestException, + SSLError, + TooManyRedirects, +) from newsreader.news.collection.exceptions import ( + StreamConnectionError, StreamDeniedException, + StreamException, StreamForbiddenException, StreamNotFoundException, StreamTimeOutException, @@ -11,24 +20,32 @@ from newsreader.news.collection.exceptions import ( class ResponseHandler: - message_mapping = { + status_code_mapping = { 404: StreamNotFoundException, 401: StreamDeniedException, 403: StreamForbiddenException, 408: StreamTimeOutException, } - def __init__(self, response: Response) -> None: - self.response = response + exception_mapping = {RequestConnectionError: StreamConnectionError} def __enter__(self) -> ContextManager: return self - def handle_response(self) -> None: - status_code = self.response.status_code + def handle_response(self, response) -> None: + status_code = response.status_code - if status_code in self.message_mapping: - raise self.message_mapping[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) -> None: - self.response = None + pass diff --git a/src/newsreader/news/collection/tests/utils/tests.py b/src/newsreader/news/collection/tests/utils/tests.py index 3c95df0..e2f0fcf 100644 --- a/src/newsreader/news/collection/tests/utils/tests.py +++ b/src/newsreader/news/collection/tests/utils/tests.py @@ -1,9 +1,19 @@ from unittest.mock import MagicMock, patch +from requests.exceptions import ConnectionError as RequestConnectionError +from requests.exceptions import ( + HTTPError, + RequestException, + SSLError, + TooManyRedirects, +) + from django.test import TestCase from newsreader.news.collection.exceptions import ( + StreamConnectionError, StreamDeniedException, + StreamException, StreamForbiddenException, StreamNotFoundException, StreamTimeOutException, @@ -55,3 +65,43 @@ class FetchTestCase(TestCase): with self.assertRaises(StreamTimeOutException): fetch(url) + + def test_raises_stream_error_on_ssl_error(self): + self.mocked_get.side_effect = SSLError + + url = "https://www.bbc.co.uk/news" + + with self.assertRaises(StreamException): + fetch(url) + + def test_raises_stream_error_on_connection_error(self): + self.mocked_get.side_effect = RequestConnectionError + + url = "https://www.bbc.co.uk/news" + + with self.assertRaises(StreamConnectionError): + fetch(url) + + def test_raises_stream_error_on_http_error(self): + self.mocked_get.side_effect = HTTPError + + url = "https://www.bbc.co.uk/news" + + with self.assertRaises(StreamException): + fetch(url) + + def test_raises_stream_error_on_request_exception(self): + self.mocked_get.side_effect = RequestException + + url = "https://www.bbc.co.uk/news" + + with self.assertRaises(StreamException): + fetch(url) + + def test_raises_stream_error_on_too_many_redirects(self): + self.mocked_get.side_effect = TooManyRedirects + + url = "https://www.bbc.co.uk/news" + + with self.assertRaises(StreamException): + fetch(url) diff --git a/src/newsreader/news/collection/utils.py b/src/newsreader/news/collection/utils.py index 17f379a..527587d 100644 --- a/src/newsreader/news/collection/utils.py +++ b/src/newsreader/news/collection/utils.py @@ -1,9 +1,10 @@ from datetime import datetime, tzinfo from time import mktime, struct_time -from typing import Optional, Tuple +from typing import Tuple import requests +from requests.exceptions import RequestException from requests.models import Response from django.utils import timezone @@ -20,10 +21,12 @@ def build_publication_date(dt: struct_time, tz: tzinfo) -> Tuple: return True, published_parsed -def fetch(url: str) -> Optional[Response]: - response = requests.get(url) - - with ResponseHandler(response) as response_handler: - response_handler.handle_response() +def fetch(url: str) -> Response: + with ResponseHandler() as response_handler: + try: + response = requests.get(url) + response_handler.handle_response(response) + except RequestException as exception: + response_handler.handle_exception(exception) return response From 982c5bb132190eeb235ea0a73bfa03a6b254154e Mon Sep 17 00:00:00 2001 From: Sonny Date: Mon, 1 Jul 2019 12:11:44 +0200 Subject: [PATCH 008/364] Update isort & rerun formatting --- .isort.cfg | 9 +- src/newsreader/conf/base.py | 22 +- src/newsreader/conf/dev.py | 1 + src/newsreader/core/admin.py | 1 + src/newsreader/core/apps.py | 2 +- src/newsreader/core/models.py | 1 + src/newsreader/core/tests.py | 1 + src/newsreader/core/views.py | 1 + src/newsreader/news/collection/apps.py | 2 +- src/newsreader/news/collection/base.py | 4 +- src/newsreader/news/collection/favicon.py | 1 + src/newsreader/news/collection/feed.py | 4 +- .../collection/management/commands/collect.py | 2 +- .../collection/migrations/0001_initial.py | 21 +- .../migrations/0002_auto_20190410_2028.py | 10 +- .../0003_collectionrule_category.py | 17 +- .../0004_collectionrule_timezone.py | 1100 ++++++------ .../migrations/0005_auto_20190521_1941.py | 16 +- .../migrations/0006_collectionrule_error.py | 10 +- src/newsreader/news/collection/models.py | 4 +- .../collection/tests/favicon/builder/mocks.py | 1 + .../collection/tests/favicon/builder/tests.py | 4 +- .../collection/tests/favicon/client/mocks.py | 1 + .../tests/favicon/collector/mocks.py | 1 + .../tests/favicon/collector/tests.py | 10 +- .../tests/feed/builder/mock_html.py | 4 +- .../collection/tests/feed/builder/mocks.py | 1562 +++++++++-------- .../collection/tests/feed/builder/tests.py | 10 +- .../collection/tests/feed/client/mocks.py | 112 +- .../collection/tests/feed/client/tests.py | 4 +- .../collection/tests/feed/collector/mocks.py | 760 ++++---- .../collection/tests/feed/collector/tests.py | 20 +- .../collection/tests/feed/stream/mocks.py | 1 + .../collection/tests/feed/stream/tests.py | 4 +- src/newsreader/news/collection/tests/tests.py | 8 +- .../news/collection/tests/utils/tests.py | 4 +- src/newsreader/news/collection/utils.py | 4 +- src/newsreader/news/collection/views.py | 1 + src/newsreader/news/posts/admin.py | 21 +- src/newsreader/news/posts/apps.py | 2 +- .../news/posts/migrations/0001_initial.py | 69 +- .../migrations/0002_auto_20190520_2206.py | 13 +- .../migrations/0003_auto_20190520_2031.py | 10 +- .../migrations/0004_auto_20190521_1941.py | 13 +- .../migrations/0005_auto_20190608_1054.py | 14 +- .../migrations/0006_auto_20190608_1520.py | 20 +- src/newsreader/news/posts/models.py | 4 +- src/newsreader/news/posts/tests/factories.py | 4 +- src/newsreader/news/posts/views.py | 1 + src/newsreader/urls.py | 5 +- src/newsreader/wsgi.py | 3 +- 51 files changed, 1993 insertions(+), 1926 deletions(-) diff --git a/.isort.cfg b/.isort.cfg index e453b8d..2b81405 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -2,6 +2,11 @@ include_trailing_comma = true line_length = 80 multi_line_output = 3 -skip = env/ -forced_separate=django, newsreader +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/src/newsreader/conf/base.py b/src/newsreader/conf/base.py index 4b9f9df..16927e4 100644 --- a/src/newsreader/conf/base.py +++ b/src/newsreader/conf/base.py @@ -12,6 +12,7 @@ https://docs.djangoproject.com/en/2.2/ref/settings/ import os + # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -62,9 +63,9 @@ TEMPLATES = [ "django.template.context_processors.request", "django.contrib.auth.context_processors.auth", "django.contrib.messages.context_processors.messages", - ], + ] }, - }, + } ] WSGI_APPLICATION = "newsreader.wsgi.application" @@ -82,19 +83,10 @@ DATABASES = { # 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", - }, + {"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"}, ] # Internationalization diff --git a/src/newsreader/conf/dev.py b/src/newsreader/conf/dev.py index 8b5de69..245a78c 100644 --- a/src/newsreader/conf/dev.py +++ b/src/newsreader/conf/dev.py @@ -1,5 +1,6 @@ from .base import * + # Development settings DEBUG = True diff --git a/src/newsreader/core/admin.py b/src/newsreader/core/admin.py index 8c38f3f..a011d19 100644 --- a/src/newsreader/core/admin.py +++ b/src/newsreader/core/admin.py @@ -1,3 +1,4 @@ from django.contrib import admin + # Register your models here. diff --git a/src/newsreader/core/apps.py b/src/newsreader/core/apps.py index 26f78a8..5ef1d60 100644 --- a/src/newsreader/core/apps.py +++ b/src/newsreader/core/apps.py @@ -2,4 +2,4 @@ from django.apps import AppConfig class CoreConfig(AppConfig): - name = 'core' + name = "core" diff --git a/src/newsreader/core/models.py b/src/newsreader/core/models.py index 4bd2e28..2e696fb 100644 --- a/src/newsreader/core/models.py +++ b/src/newsreader/core/models.py @@ -6,6 +6,7 @@ class TimeStampedModel(models.Model): An abstract base class model that provides self- updating ``created`` and ``modified`` fields. """ + created = models.DateTimeField(auto_now_add=True) modified = models.DateTimeField(auto_now=True) diff --git a/src/newsreader/core/tests.py b/src/newsreader/core/tests.py index 7ce503c..7c72b39 100644 --- a/src/newsreader/core/tests.py +++ b/src/newsreader/core/tests.py @@ -1,3 +1,4 @@ from django.test import TestCase + # Create your tests here. diff --git a/src/newsreader/core/views.py b/src/newsreader/core/views.py index 91ea44a..dc1ba72 100644 --- a/src/newsreader/core/views.py +++ b/src/newsreader/core/views.py @@ -1,3 +1,4 @@ from django.shortcuts import render + # Create your views here. diff --git a/src/newsreader/news/collection/apps.py b/src/newsreader/news/collection/apps.py index 1454371..1f4c1c0 100644 --- a/src/newsreader/news/collection/apps.py +++ b/src/newsreader/news/collection/apps.py @@ -2,4 +2,4 @@ from django.apps import AppConfig class CollectionConfig(AppConfig): - name = 'collection' + name = "collection" diff --git a/src/newsreader/news/collection/base.py b/src/newsreader/news/collection/base.py index 2f392b7..c202df8 100644 --- a/src/newsreader/news/collection/base.py +++ b/src/newsreader/news/collection/base.py @@ -1,11 +1,11 @@ from typing import ContextManager, Dict, List, Optional, Tuple +from django.utils import timezone + import requests from bs4 import BeautifulSoup -from django.utils import timezone - from newsreader.news.collection.exceptions import StreamParseException from newsreader.news.collection.models import CollectionRule from newsreader.news.collection.utils import fetch diff --git a/src/newsreader/news/collection/favicon.py b/src/newsreader/news/collection/favicon.py index 8270692..05bd6c9 100644 --- a/src/newsreader/news/collection/favicon.py +++ b/src/newsreader/news/collection/favicon.py @@ -13,6 +13,7 @@ from newsreader.news.collection.base import ( 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"] diff --git a/src/newsreader/news/collection/feed.py b/src/newsreader/news/collection/feed.py index ef66b69..2cf248c 100644 --- a/src/newsreader/news/collection/feed.py +++ b/src/newsreader/news/collection/feed.py @@ -1,13 +1,13 @@ from concurrent.futures import ThreadPoolExecutor, as_completed from typing import ContextManager, Dict, Generator, List, Optional, Tuple +from django.utils import timezone + import bleach import pytz from feedparser import parse -from django.utils import timezone - from newsreader.news.collection.base import Builder, Client, Collector, Stream from newsreader.news.collection.exceptions import ( StreamDeniedException, diff --git a/src/newsreader/news/collection/management/commands/collect.py b/src/newsreader/news/collection/management/commands/collect.py index c72301f..855089f 100644 --- a/src/newsreader/news/collection/management/commands/collect.py +++ b/src/newsreader/news/collection/management/commands/collect.py @@ -5,7 +5,7 @@ from newsreader.news.collection.models import CollectionRule class Command(BaseCommand): - help = 'Collects Atom/RSS feeds' + help = "Collects Atom/RSS feeds" def handle(self, *args, **options): CollectionRule.objects.all() diff --git a/src/newsreader/news/collection/migrations/0001_initial.py b/src/newsreader/news/collection/migrations/0001_initial.py index 354a97f..1091b7a 100644 --- a/src/newsreader/news/collection/migrations/0001_initial.py +++ b/src/newsreader/news/collection/migrations/0001_initial.py @@ -11,21 +11,18 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='CollectionRule', + name="CollectionRule", fields=[ ( - 'id', + "id", models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name='ID' - ) + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), ), - ('name', models.CharField(max_length=100)), - ('url', models.URLField()), - ('last_suceeded', models.DateTimeField()), - ('succeeded', models.BooleanField(default=False)), + ("name", models.CharField(max_length=100)), + ("url", models.URLField()), + ("last_suceeded", models.DateTimeField()), + ("succeeded", models.BooleanField(default=False)), ], - ), + ) ] diff --git a/src/newsreader/news/collection/migrations/0002_auto_20190410_2028.py b/src/newsreader/news/collection/migrations/0002_auto_20190410_2028.py index 9c0807e..ce45e0e 100644 --- a/src/newsreader/news/collection/migrations/0002_auto_20190410_2028.py +++ b/src/newsreader/news/collection/migrations/0002_auto_20190410_2028.py @@ -5,14 +5,12 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('collection', '0001_initial'), - ] + dependencies = [("collection", "0001_initial")] operations = [ migrations.AlterField( - model_name='collectionrule', - name='last_suceeded', + model_name="collectionrule", + name="last_suceeded", field=models.DateTimeField(blank=True, null=True), - ), + ) ] diff --git a/src/newsreader/news/collection/migrations/0003_collectionrule_category.py b/src/newsreader/news/collection/migrations/0003_collectionrule_category.py index 4c3f267..b15f7f7 100644 --- a/src/newsreader/news/collection/migrations/0003_collectionrule_category.py +++ b/src/newsreader/news/collection/migrations/0003_collectionrule_category.py @@ -7,22 +7,19 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('posts', '0002_auto_20190520_2206'), - ('collection', '0002_auto_20190410_2028'), - ] + dependencies = [("posts", "0002_auto_20190520_2206"), ("collection", "0002_auto_20190410_2028")] operations = [ migrations.AddField( - model_name='collectionrule', - name='category', + model_name="collectionrule", + name="category", field=models.ForeignKey( blank=True, - help_text='Posts from this rule will be tagged with this category', + help_text="Posts from this rule will be tagged with this category", null=True, on_delete=django.db.models.deletion.SET_NULL, - to='posts.Category', - verbose_name='Category' + to="posts.Category", + verbose_name="Category", ), - ), + ) ] diff --git a/src/newsreader/news/collection/migrations/0004_collectionrule_timezone.py b/src/newsreader/news/collection/migrations/0004_collectionrule_timezone.py index e5943dc..837e625 100644 --- a/src/newsreader/news/collection/migrations/0004_collectionrule_timezone.py +++ b/src/newsreader/news/collection/migrations/0004_collectionrule_timezone.py @@ -5,513 +5,609 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('collection', '0003_collectionrule_category'), - ] + dependencies = [("collection", "0003_collectionrule_category")] operations = [ migrations.AddField( - model_name='collectionrule', - name='timezone', + model_name="collectionrule", + name="timezone", field=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') + ("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 + default="UTC", + max_length=100, ), - ), + ) ] diff --git a/src/newsreader/news/collection/migrations/0005_auto_20190521_1941.py b/src/newsreader/news/collection/migrations/0005_auto_20190521_1941.py index e9ab3d4..8e3a53f 100644 --- a/src/newsreader/news/collection/migrations/0005_auto_20190521_1941.py +++ b/src/newsreader/news/collection/migrations/0005_auto_20190521_1941.py @@ -5,20 +5,18 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('collection', '0004_collectionrule_timezone'), - ] + dependencies = [("collection", "0004_collectionrule_timezone")] operations = [ migrations.AddField( - model_name='collectionrule', - name='favicon', - field=models.ImageField(blank=True, null=True, upload_to=''), + model_name="collectionrule", + name="favicon", + field=models.ImageField(blank=True, null=True, upload_to=""), ), migrations.AddField( - model_name='collectionrule', - name='source', - field=models.CharField(default='source', max_length=100), + model_name="collectionrule", + name="source", + field=models.CharField(default="source", max_length=100), preserve_default=False, ), ] diff --git a/src/newsreader/news/collection/migrations/0006_collectionrule_error.py b/src/newsreader/news/collection/migrations/0006_collectionrule_error.py index 78843b1..b70b638 100644 --- a/src/newsreader/news/collection/migrations/0006_collectionrule_error.py +++ b/src/newsreader/news/collection/migrations/0006_collectionrule_error.py @@ -5,14 +5,12 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('collection', '0005_auto_20190521_1941'), - ] + dependencies = [("collection", "0005_auto_20190521_1941")] operations = [ migrations.AddField( - model_name='collectionrule', - name='error', + model_name="collectionrule", + name="error", field=models.CharField(blank=True, max_length=255, null=True), - ), + ) ] diff --git a/src/newsreader/news/collection/models.py b/src/newsreader/news/collection/models.py index 1de9849..03768ac 100644 --- a/src/newsreader/news/collection/models.py +++ b/src/newsreader/news/collection/models.py @@ -1,9 +1,9 @@ -import pytz - from django.conf import settings from django.db import models from django.utils.translation import gettext as _ +import pytz + class CollectionRule(models.Model): name = models.CharField(max_length=100) diff --git a/src/newsreader/news/collection/tests/favicon/builder/mocks.py b/src/newsreader/news/collection/tests/favicon/builder/mocks.py index ce02475..8011472 100644 --- a/src/newsreader/news/collection/tests/favicon/builder/mocks.py +++ b/src/newsreader/news/collection/tests/favicon/builder/mocks.py @@ -1,5 +1,6 @@ from bs4 import BeautifulSoup + simple_mock = BeautifulSoup( """ diff --git a/src/newsreader/news/collection/tests/favicon/builder/tests.py b/src/newsreader/news/collection/tests/favicon/builder/tests.py index d08fce7..c8bd14c 100644 --- a/src/newsreader/news/collection/tests/favicon/builder/tests.py +++ b/src/newsreader/news/collection/tests/favicon/builder/tests.py @@ -1,7 +1,7 @@ -from freezegun import freeze_time - from django.test import TestCase +from freezegun import freeze_time + from newsreader.news.collection.favicon import FaviconBuilder from newsreader.news.collection.tests.factories import CollectionRuleFactory from newsreader.news.collection.tests.favicon.builder.mocks import * diff --git a/src/newsreader/news/collection/tests/favicon/client/mocks.py b/src/newsreader/news/collection/tests/favicon/client/mocks.py index ba79b27..a4c5ee1 100644 --- a/src/newsreader/news/collection/tests/favicon/client/mocks.py +++ b/src/newsreader/news/collection/tests/favicon/client/mocks.py @@ -1,5 +1,6 @@ from bs4 import BeautifulSoup + simple_mock = BeautifulSoup( """ diff --git a/src/newsreader/news/collection/tests/favicon/collector/mocks.py b/src/newsreader/news/collection/tests/favicon/collector/mocks.py index 8e58167..097b1dd 100644 --- a/src/newsreader/news/collection/tests/favicon/collector/mocks.py +++ b/src/newsreader/news/collection/tests/favicon/collector/mocks.py @@ -2,6 +2,7 @@ from time import struct_time from bs4 import BeautifulSoup + feed_mock = { "bozo": 0, "encoding": "utf-8", diff --git a/src/newsreader/news/collection/tests/favicon/collector/tests.py b/src/newsreader/news/collection/tests/favicon/collector/tests.py index 6554de4..a292c16 100644 --- a/src/newsreader/news/collection/tests/favicon/collector/tests.py +++ b/src/newsreader/news/collection/tests/favicon/collector/tests.py @@ -1,14 +1,12 @@ from unittest.mock import MagicMock, patch +from django.test import TestCase +from django.utils import timezone + import pytz from bs4 import BeautifulSoup -from .mocks import feed_mock, website_mock - -from django.test import TestCase -from django.utils import timezone - from newsreader.news.collection.exceptions import ( StreamDeniedException, StreamException, @@ -20,6 +18,8 @@ from newsreader.news.collection.exceptions import ( 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): diff --git a/src/newsreader/news/collection/tests/feed/builder/mock_html.py b/src/newsreader/news/collection/tests/feed/builder/mock_html.py index 788495f..44d46f7 100644 --- a/src/newsreader/news/collection/tests/feed/builder/mock_html.py +++ b/src/newsreader/news/collection/tests/feed/builder/mock_html.py @@ -1,4 +1,4 @@ -html_summary = ''' +html_summary = """
@@ -7,4 +7,4 @@ html_summary = '''
-''' +""" diff --git a/src/newsreader/news/collection/tests/feed/builder/mocks.py b/src/newsreader/news/collection/tests/feed/builder/mocks.py index 631f582..a486626 100644 --- a/src/newsreader/news/collection/tests/feed/builder/mocks.py +++ b/src/newsreader/news/collection/tests/feed/builder/mocks.py @@ -2,877 +2,891 @@ from time import struct_time from .mock_html import html_summary + simple_mock = { - 'bozo': 0, - 'encoding': 'utf-8', - 'entries': [{ - 'author': 'A. Author', - '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' - } - }], - '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/' + "bozo": 0, + "encoding": "utf-8", + "entries": [ + { + "author": "A. Author", + "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.", }, - 'links': [{ - 'href': 'https://www.bbc.co.uk/news/', - 'rel': 'alternate', - 'type': 'text/html' - }], - 'title': 'BBC News - Home', + "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", + }, + } + ], + "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/", + }, + "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' + "href": "http://feeds.bbci.co.uk/news/rss.xml", + "status": 200, + "version": "rss20", } multiple_mock = { - 'bozo': 0, - 'encoding': 'utf-8', - 'entries': [ + "bozo": 0, + "encoding": "utf-8", + "entries": [ { - 'author': 'A. Author', - '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.' + "author": "A. Author", + "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", }, - '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' - } }, { - 'author': 'A. Author', - '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.' + "author": "A. Author", + "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", }, - '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' - } }, { - 'author': 'A. Author', - '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 ' + "author": "A. Author", + "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 ' + "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".' + '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", }, - '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/' - }, - 'links': [{ - 'href': 'https://www.bbc.co.uk/news/', - 'rel': 'alternate', - 'type': 'text/html' - }], - 'title': 'BBC News - Home', + "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/", + }, + "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' + "href": "http://feeds.bbci.co.uk/news/rss.xml", + "status": 200, + "version": "rss20", } mock_without_identifier = { - 'bozo': 0, - 'encoding': 'utf-8', - 'entries': [ + "bozo": 0, + "encoding": "utf-8", + "entries": [ { - 'author': 'A. Author', - 'guidislink': False, - 'href': '', - '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.' + "author": "A. Author", + "guidislink": False, + "href": "", + "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", }, - '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' - } }, { - 'author': 'A. Author', - 'guidislink': False, - 'href': '', - 'id': None, - '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.' + "author": "A. Author", + "guidislink": False, + "href": "", + "id": None, + "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", }, - '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' - } }, ], - '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/' - }, - 'links': [{ - 'href': 'https://www.bbc.co.uk/news/', - 'rel': 'alternate', - 'type': 'text/html' - }], - 'title': 'BBC News - Home', + "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/", + }, + "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' + "href": "http://feeds.bbci.co.uk/news/rss.xml", + "status": 200, + "version": "rss20", } mock_without_publish_date = { - 'bozo': 0, - 'encoding': 'utf-8', - 'entries': [ + "bozo": 0, + "encoding": "utf-8", + "entries": [ { - 'author': 'A. Author', - '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': None, - 'published_parsed': None, - '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.' + "author": "A. Author", + "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": None, + "published_parsed": None, + "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", }, - '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' - } }, { - 'author': 'A. Author', - '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' - }], - '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.' + "author": "A. Author", + "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", + } + ], + "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", }, - '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' - } }, ], - '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/' - }, - 'links': [{ - 'href': 'https://www.bbc.co.uk/news/', - 'rel': 'alternate', - 'type': 'text/html' - }], - 'title': 'BBC News - Home', + "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/", + }, + "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' + "href": "http://feeds.bbci.co.uk/news/rss.xml", + "status": 200, + "version": "rss20", } mock_without_url = { - 'bozo': 0, - 'encoding': 'utf-8', - 'entries': [ + "bozo": 0, + "encoding": "utf-8", + "entries": [ { - 'author': 'A. Author', - 'guidislink': False, - 'href': '', - 'id': 'https://www.bbc.co.uk/news/world-us-canada-48338168', - '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)), - 'published': None, - 'published_parsed': None, - '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.' + "author": "A. Author", + "guidislink": False, + "href": "", + "id": "https://www.bbc.co.uk/news/world-us-canada-48338168", + "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)), + "published": None, + "published_parsed": None, + "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", }, - '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' - } }, { - 'author': 'A. Author', - 'guidislink': False, - 'href': '', - 'id': 'https://www.bbc.co.uk/news/technology-48334739', - 'link': None, - 'links': [], - 'media_thumbnail': [{ - 'height': '432', - 'url': 'http://c.files.bbci.co.uk/4789/production/_107031381_mediaitem107028670.jpg', - 'width': '768' - }], - '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.', - '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.' + "author": "A. Author", + "guidislink": False, + "href": "", + "id": "https://www.bbc.co.uk/news/technology-48334739", + "link": None, + "links": [], + "media_thumbnail": [ + { + "height": "432", + "url": "http://c.files.bbci.co.uk/4789/production/_107031381_mediaitem107028670.jpg", + "width": "768", + } + ], + "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.", + "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", }, - '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' - } }, ], - '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/' - }, - 'links': [{ - 'href': 'https://www.bbc.co.uk/news/', - 'rel': 'alternate', - 'type': 'text/html' - }], - 'title': 'BBC News - Home', + "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/", + }, + "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' + "href": "http://feeds.bbci.co.uk/news/rss.xml", + "status": 200, + "version": "rss20", } mock_without_body = { - 'bozo': 0, - 'encoding': 'utf-8', - 'entries': [ + "bozo": 0, + "encoding": "utf-8", + "entries": [ { - 'author': 'A. Author', - '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)), - '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' - } + "author": "A. Author", + "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)), + "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", + }, }, { - 'author': 'A. Author', - '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': None, - 'summary_detail': {}, - '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' + "author": "A. Author", + "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": None, + "summary_detail": {}, + "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/' - }, - 'links': [{ - 'href': 'https://www.bbc.co.uk/news/', - 'rel': 'alternate', - 'type': 'text/html' - }], - 'title': 'BBC News - Home', + "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/", + }, + "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' + "href": "http://feeds.bbci.co.uk/news/rss.xml", + "status": 200, + "version": "rss20", } mock_without_author = { - 'bozo': 0, - 'encoding': 'utf-8', - 'entries': [ + "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.' + "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", }, - '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' - } }, { - 'author': None, - '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.' + "author": None, + "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", }, - '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' - } }, ], - '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/' - }, - 'links': [{ - 'href': 'https://www.bbc.co.uk/news/', - 'rel': 'alternate', - 'type': 'text/html' - }], - 'title': 'BBC News - Home', + "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/", + }, + "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' + "href": "http://feeds.bbci.co.uk/news/rss.xml", + "status": 200, + "version": "rss20", } -mock_without_entries = { - 'entries': [], -} +mock_without_entries = {"entries": []} mock_with_update_entries = { - 'bozo': 0, - 'encoding': 'utf-8', - 'entries': [ + "bozo": 0, + "encoding": "utf-8", + "entries": [ { - 'author': 'A. Author', - 'guidislink': False, - 'href': '', - 'id': '28f79ae4-8f9a-11e9-b143-00163ef6bee7', - '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.' + "author": "A. Author", + "guidislink": False, + "href": "", + "id": "28f79ae4-8f9a-11e9-b143-00163ef6bee7", + "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", }, - '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' - } }, { - 'author': 'A. Author', - 'guidislink': False, - 'href': '', - 'id': 'a5479c66-8fae-11e9-8422-00163ef6bee7', - '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.' + "author": "A. Author", + "guidislink": False, + "href": "", + "id": "a5479c66-8fae-11e9-8422-00163ef6bee7", + "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", }, - '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' - } }, { - 'author': 'A. Author', - '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 ' + "author": "A. Author", + "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 ' + "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".' + '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", }, - '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/' - }, - 'links': [{ - 'href': 'https://www.bbc.co.uk/news/', - 'rel': 'alternate', - 'type': 'text/html' - }], - 'title': 'BBC News - Home', + "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/", + }, + "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' + "href": "http://feeds.bbci.co.uk/news/rss.xml", + "status": 200, + "version": "rss20", } mock_with_html = { - 'bozo': 0, - 'encoding': 'utf-8', - 'entries': [ + "bozo": 0, + "encoding": "utf-8", + "entries": [ { - 'author': 'A. Author', - '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': html_summary, - '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.' + "author": "A. Author", + "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": html_summary, + "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' - } - }, + "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", + }, + } ], - '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/' - }, - 'links': [{ - 'href': 'https://www.bbc.co.uk/news/', - 'rel': 'alternate', - 'type': 'text/html' - }], - 'title': 'BBC News - Home', + "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/", + }, + "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' + "href": "http://feeds.bbci.co.uk/news/rss.xml", + "status": 200, + "version": "rss20", } diff --git a/src/newsreader/news/collection/tests/feed/builder/tests.py b/src/newsreader/news/collection/tests/feed/builder/tests.py index 49a130a..6efd432 100644 --- a/src/newsreader/news/collection/tests/feed/builder/tests.py +++ b/src/newsreader/news/collection/tests/feed/builder/tests.py @@ -1,20 +1,20 @@ 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 .mocks import * - -from django.test import TestCase -from django.utils import timezone - from newsreader.news.collection.feed import FeedBuilder from newsreader.news.collection.tests.factories import CollectionRuleFactory from newsreader.news.posts.models import Post from newsreader.news.posts.tests.factories import PostFactory +from .mocks import * + class FeedBuilderTestCase(TestCase): def setUp(self): diff --git a/src/newsreader/news/collection/tests/feed/client/mocks.py b/src/newsreader/news/collection/tests/feed/client/mocks.py index 5853eb7..e055e7b 100644 --- a/src/newsreader/news/collection/tests/feed/client/mocks.py +++ b/src/newsreader/news/collection/tests/feed/client/mocks.py @@ -1,61 +1,63 @@ from time import struct_time + simple_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' - } - }], - '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/' + "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.", }, - 'links': [{ - 'href': 'https://www.bbc.co.uk/news/', - 'rel': 'alternate', - 'type': 'text/html' - }], - 'title': 'BBC News - Home', + "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", + }, + } + ], + "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/", + }, + "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' + "href": "http://feeds.bbci.co.uk/news/rss.xml", + "status": 200, + "version": "rss20", } diff --git a/src/newsreader/news/collection/tests/feed/client/tests.py b/src/newsreader/news/collection/tests/feed/client/tests.py index e49762d..bd9e4eb 100644 --- a/src/newsreader/news/collection/tests/feed/client/tests.py +++ b/src/newsreader/news/collection/tests/feed/client/tests.py @@ -1,7 +1,5 @@ from unittest.mock import MagicMock, patch -from .mocks import simple_mock - from django.test import TestCase from django.utils import timezone @@ -15,6 +13,8 @@ from newsreader.news.collection.exceptions import ( from newsreader.news.collection.feed import FeedClient from newsreader.news.collection.tests.factories import CollectionRuleFactory +from .mocks import simple_mock + class FeedClientTestCase(TestCase): def setUp(self): diff --git a/src/newsreader/news/collection/tests/feed/collector/mocks.py b/src/newsreader/news/collection/tests/feed/collector/mocks.py index 930e977..211f4ef 100644 --- a/src/newsreader/news/collection/tests/feed/collector/mocks.py +++ b/src/newsreader/news/collection/tests/feed/collector/mocks.py @@ -1,430 +1,442 @@ from time import struct_time + multiple_mock = { - 'bozo': 0, - 'encoding': 'utf-8', - 'entries': [ + "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.' + "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", }, - '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.' + "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", }, - '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 ' + "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 ' + "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".' + '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", }, - '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/' - }, - 'links': [{ - 'href': 'https://www.bbc.co.uk/news/', - 'rel': 'alternate', - 'type': 'text/html' - }], - 'title': 'BBC News - Home', + "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/", + }, + "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' + "href": "http://feeds.bbci.co.uk/news/rss.xml", + "status": 200, + "version": "rss20", } empty_mock = { - 'bozo': 0, - 'encoding': 'utf-8', - 'entries': [], - '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/' - }, - 'links': [{ - 'href': 'https://www.bbc.co.uk/news/', - 'rel': 'alternate', - 'type': 'text/html' - }], - 'title': 'BBC News - Home', + "bozo": 0, + "encoding": "utf-8", + "entries": [], + "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/", + }, + "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' + "href": "http://feeds.bbci.co.uk/news/rss.xml", + "status": 200, + "version": "rss20", } duplicate_mock = { - 'bozo': 0, - 'encoding': 'utf-8', - 'entries': [ + "bozo": 0, + "encoding": "utf-8", + "entries": [ { - 'guidislink': False, - 'href': '', - '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.' + "guidislink": False, + "href": "", + "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", }, - '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': '', - '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.' + "guidislink": False, + "href": "", + "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", }, - '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': '', - '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 ' + "guidislink": False, + "href": "", + "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 ' + "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".' + '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", }, - '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/' - }, - 'links': [{ - 'href': 'https://www.bbc.co.uk/news/', - 'rel': 'alternate', - 'type': 'text/html' - }], - 'title': 'BBC News - Home', + "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/", + }, + "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' + "href": "http://feeds.bbci.co.uk/news/rss.xml", + "status": 200, + "version": "rss20", } multiple_update_mock = { - 'bozo': 0, - 'encoding': 'utf-8', - 'entries': [ + "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.' + "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", }, - '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.' + "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", }, - '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 ' + "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 ' + "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".' + '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", }, - '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/' - }, - 'links': [{ - 'href': 'https://www.bbc.co.uk/news/', - 'rel': 'alternate', - 'type': 'text/html' - }], - 'title': 'BBC News - Home', + "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/", + }, + "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' + "href": "http://feeds.bbci.co.uk/news/rss.xml", + "status": 200, + "version": "rss20", } diff --git a/src/newsreader/news/collection/tests/feed/collector/tests.py b/src/newsreader/news/collection/tests/feed/collector/tests.py index 0d14730..16008dd 100644 --- a/src/newsreader/news/collection/tests/feed/collector/tests.py +++ b/src/newsreader/news/collection/tests/feed/collector/tests.py @@ -2,20 +2,13 @@ from datetime import date, datetime, time from time import struct_time from unittest.mock import MagicMock, patch +from django.test import TestCase +from django.utils import timezone + import pytz from freezegun import freeze_time -from .mocks import ( - duplicate_mock, - empty_mock, - multiple_mock, - multiple_update_mock, -) - -from django.test import TestCase -from django.utils import timezone - from newsreader.news.collection.exceptions import ( StreamDeniedException, StreamException, @@ -30,6 +23,13 @@ from newsreader.news.collection.utils import build_publication_date from newsreader.news.posts.models import Post from newsreader.news.posts.tests.factories import PostFactory +from .mocks import ( + duplicate_mock, + empty_mock, + multiple_mock, + multiple_update_mock, +) + class FeedCollectorTestCase(TestCase): def setUp(self): diff --git a/src/newsreader/news/collection/tests/feed/stream/mocks.py b/src/newsreader/news/collection/tests/feed/stream/mocks.py index a098383..9e7d796 100644 --- a/src/newsreader/news/collection/tests/feed/stream/mocks.py +++ b/src/newsreader/news/collection/tests/feed/stream/mocks.py @@ -1,5 +1,6 @@ from time import struct_time + simple_mock = { "bozo": 1, "encoding": "utf-8", diff --git a/src/newsreader/news/collection/tests/feed/stream/tests.py b/src/newsreader/news/collection/tests/feed/stream/tests.py index 3a7811b..8509156 100644 --- a/src/newsreader/news/collection/tests/feed/stream/tests.py +++ b/src/newsreader/news/collection/tests/feed/stream/tests.py @@ -1,7 +1,5 @@ from unittest.mock import MagicMock, patch -from .mocks import simple_mock - from django.test import TestCase from django.utils import timezone @@ -16,6 +14,8 @@ from newsreader.news.collection.exceptions import ( from newsreader.news.collection.feed import FeedStream from newsreader.news.collection.tests.factories import CollectionRuleFactory +from .mocks import simple_mock + class FeedStreamTestCase(TestCase): def setUp(self): diff --git a/src/newsreader/news/collection/tests/tests.py b/src/newsreader/news/collection/tests/tests.py index 08bc4e0..c39abea 100644 --- a/src/newsreader/news/collection/tests/tests.py +++ b/src/newsreader/news/collection/tests/tests.py @@ -1,11 +1,9 @@ from unittest.mock import MagicMock, patch -from bs4 import BeautifulSoup - -from .mocks import feed_mock_without_link, simple_feed_mock, simple_mock - from django.test import TestCase +from bs4 import BeautifulSoup + from newsreader.news.collection.base import URLBuilder, WebsiteStream from newsreader.news.collection.exceptions import ( StreamDeniedException, @@ -17,6 +15,8 @@ from newsreader.news.collection.exceptions import ( ) from newsreader.news.collection.tests.factories import CollectionRuleFactory +from .mocks import feed_mock_without_link, simple_feed_mock, simple_mock + class WebsiteStreamTestCase(TestCase): def setUp(self): diff --git a/src/newsreader/news/collection/tests/utils/tests.py b/src/newsreader/news/collection/tests/utils/tests.py index e2f0fcf..0362c71 100644 --- a/src/newsreader/news/collection/tests/utils/tests.py +++ b/src/newsreader/news/collection/tests/utils/tests.py @@ -1,5 +1,7 @@ from unittest.mock import MagicMock, patch +from django.test import TestCase + from requests.exceptions import ConnectionError as RequestConnectionError from requests.exceptions import ( HTTPError, @@ -8,8 +10,6 @@ from requests.exceptions import ( TooManyRedirects, ) -from django.test import TestCase - from newsreader.news.collection.exceptions import ( StreamConnectionError, StreamDeniedException, diff --git a/src/newsreader/news/collection/utils.py b/src/newsreader/news/collection/utils.py index 527587d..0abc367 100644 --- a/src/newsreader/news/collection/utils.py +++ b/src/newsreader/news/collection/utils.py @@ -2,13 +2,13 @@ from datetime import datetime, tzinfo from time import mktime, struct_time from typing import Tuple +from django.utils import timezone + import requests from requests.exceptions import RequestException from requests.models import Response -from django.utils import timezone - from newsreader.news.collection.response_handler import ResponseHandler diff --git a/src/newsreader/news/collection/views.py b/src/newsreader/news/collection/views.py index 91ea44a..dc1ba72 100644 --- a/src/newsreader/news/collection/views.py +++ b/src/newsreader/news/collection/views.py @@ -1,3 +1,4 @@ from django.shortcuts import render + # Create your views here. diff --git a/src/newsreader/news/posts/admin.py b/src/newsreader/news/posts/admin.py index 2ba7c81..3e6940b 100644 --- a/src/newsreader/news/posts/admin.py +++ b/src/newsreader/news/posts/admin.py @@ -4,26 +4,13 @@ from newsreader.news.posts.models import Category, Post class PostAdmin(admin.ModelAdmin): - list_display = ( - "publication_date", - "author", - "rule", - "title", - ) - list_display_links = ("title", ) - list_filter = ("rule", ) + list_display = ("publication_date", "author", "rule", "title") + list_display_links = ("title",) + list_filter = ("rule",) ordering = ("-publication_date", "title") - fields = ( - "title", - "body", - "author", - "publication_date", - "url", - "remote_identifier", - "category", - ) + fields = ("title", "body", "author", "publication_date", "url", "remote_identifier", "category") search_fields = ["title"] diff --git a/src/newsreader/news/posts/apps.py b/src/newsreader/news/posts/apps.py index 2c2b982..d39b3e1 100644 --- a/src/newsreader/news/posts/apps.py +++ b/src/newsreader/news/posts/apps.py @@ -2,4 +2,4 @@ from django.apps import AppConfig class PostsConfig(AppConfig): - name = 'posts' + name = "posts" diff --git a/src/newsreader/news/posts/migrations/0001_initial.py b/src/newsreader/news/posts/migrations/0001_initial.py index 36666b3..c7e7604 100644 --- a/src/newsreader/news/posts/migrations/0001_initial.py +++ b/src/newsreader/news/posts/migrations/0001_initial.py @@ -9,70 +9,57 @@ class Migration(migrations.Migration): initial = True - dependencies = [ - ('collection', '0001_initial'), - ] + dependencies = [("collection", "0001_initial")] operations = [ migrations.CreateModel( - name='Category', + name="Category", fields=[ ( - 'id', + "id", models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name='ID' - ) + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), ), - ('created', models.DateTimeField(auto_now_add=True)), - ('modified', models.DateTimeField(auto_now=True)), - ('name', models.CharField(max_length=50)), + ("created", models.DateTimeField(auto_now_add=True)), + ("modified", models.DateTimeField(auto_now=True)), + ("name", models.CharField(max_length=50)), ], - options={ - 'abstract': False, - }, + options={"abstract": False}, ), migrations.CreateModel( - name='Post', + name="Post", fields=[ ( - 'id', + "id", models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name='ID' - ) + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), ), - ('created', models.DateTimeField(auto_now_add=True)), - ('modified', models.DateTimeField(auto_now=True)), - ('title', models.CharField(max_length=200)), - ('body', models.TextField()), - ('source', models.CharField(max_length=200)), - ('publication_date', models.DateTimeField()), - ('url', models.URLField()), - ('remote_identifier', models.CharField(max_length=500)), + ("created", models.DateTimeField(auto_now_add=True)), + ("modified", models.DateTimeField(auto_now=True)), + ("title", models.CharField(max_length=200)), + ("body", models.TextField()), + ("source", models.CharField(max_length=200)), + ("publication_date", models.DateTimeField()), + ("url", models.URLField()), + ("remote_identifier", models.CharField(max_length=500)), ( - 'category', + "category", models.ForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, - to='posts.Category' - ) + to="posts.Category", + ), ), ( - 'rule', + "rule", models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - to='collection.CollectionRule' - ) + on_delete=django.db.models.deletion.CASCADE, to="collection.CollectionRule" + ), ), ], - options={ - 'abstract': False, - }, + options={"abstract": False}, ), ] diff --git a/src/newsreader/news/posts/migrations/0002_auto_20190520_2206.py b/src/newsreader/news/posts/migrations/0002_auto_20190520_2206.py index cb86d51..4365972 100644 --- a/src/newsreader/news/posts/migrations/0002_auto_20190520_2206.py +++ b/src/newsreader/news/posts/migrations/0002_auto_20190520_2206.py @@ -5,16 +5,11 @@ from django.db import migrations class Migration(migrations.Migration): - dependencies = [ - ('posts', '0001_initial'), - ] + dependencies = [("posts", "0001_initial")] operations = [ migrations.AlterModelOptions( - name='category', - options={ - 'verbose_name': 'Category', - 'verbose_name_plural': 'Categories' - }, - ), + name="category", + options={"verbose_name": "Category", "verbose_name_plural": "Categories"}, + ) ] diff --git a/src/newsreader/news/posts/migrations/0003_auto_20190520_2031.py b/src/newsreader/news/posts/migrations/0003_auto_20190520_2031.py index a790477..0905440 100644 --- a/src/newsreader/news/posts/migrations/0003_auto_20190520_2031.py +++ b/src/newsreader/news/posts/migrations/0003_auto_20190520_2031.py @@ -5,14 +5,10 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('posts', '0002_auto_20190520_2206'), - ] + dependencies = [("posts", "0002_auto_20190520_2206")] operations = [ migrations.AlterField( - model_name='category', - name='name', - field=models.CharField(max_length=50, unique=True), - ), + model_name="category", name="name", field=models.CharField(max_length=50, unique=True) + ) ] diff --git a/src/newsreader/news/posts/migrations/0004_auto_20190521_1941.py b/src/newsreader/news/posts/migrations/0004_auto_20190521_1941.py index a14c636..4a0ceb2 100644 --- a/src/newsreader/news/posts/migrations/0004_auto_20190521_1941.py +++ b/src/newsreader/news/posts/migrations/0004_auto_20190521_1941.py @@ -5,18 +5,13 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('posts', '0003_auto_20190520_2031'), - ] + dependencies = [("posts", "0003_auto_20190520_2031")] operations = [ - migrations.RemoveField( - model_name='post', - name='source', - ), + migrations.RemoveField(model_name="post", name="source"), migrations.AddField( - model_name='post', - name='author', + model_name="post", + name="author", field=models.CharField(blank=True, max_length=100, null=True), ), ] diff --git a/src/newsreader/news/posts/migrations/0005_auto_20190608_1054.py b/src/newsreader/news/posts/migrations/0005_auto_20190608_1054.py index 96c9d8c..9a5e03a 100644 --- a/src/newsreader/news/posts/migrations/0005_auto_20190608_1054.py +++ b/src/newsreader/news/posts/migrations/0005_auto_20190608_1054.py @@ -5,19 +5,13 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('posts', '0004_auto_20190521_1941'), - ] + dependencies = [("posts", "0004_auto_20190521_1941")] operations = [ + migrations.AlterField(model_name="post", name="body", field=models.TextField(blank=True)), migrations.AlterField( - model_name='post', - name='body', - field=models.TextField(blank=True), - ), - migrations.AlterField( - model_name='post', - name='remote_identifier', + model_name="post", + name="remote_identifier", field=models.CharField(blank=True, max_length=500, null=True), ), ] diff --git a/src/newsreader/news/posts/migrations/0006_auto_20190608_1520.py b/src/newsreader/news/posts/migrations/0006_auto_20190608_1520.py index 8215ea9..7a5f7ec 100644 --- a/src/newsreader/news/posts/migrations/0006_auto_20190608_1520.py +++ b/src/newsreader/news/posts/migrations/0006_auto_20190608_1520.py @@ -5,29 +5,23 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('posts', '0005_auto_20190608_1054'), - ] + dependencies = [("posts", "0005_auto_20190608_1054")] operations = [ migrations.AlterField( - model_name='post', - name='body', - field=models.TextField(blank=True, null=True), + model_name="post", name="body", field=models.TextField(blank=True, null=True) ), migrations.AlterField( - model_name='post', - name='publication_date', + model_name="post", + name="publication_date", field=models.DateTimeField(blank=True, null=True), ), migrations.AlterField( - model_name='post', - name='title', + model_name="post", + name="title", field=models.CharField(blank=True, max_length=200, null=True), ), migrations.AlterField( - model_name='post', - name='url', - field=models.URLField(blank=True, null=True), + model_name="post", name="url", field=models.URLField(blank=True, null=True) ), ] diff --git a/src/newsreader/news/posts/models.py b/src/newsreader/news/posts/models.py index 0528187..fd2c6c1 100644 --- a/src/newsreader/news/posts/models.py +++ b/src/newsreader/news/posts/models.py @@ -15,9 +15,7 @@ class Post(TimeStampedModel): rule = models.ForeignKey(CollectionRule, on_delete=models.CASCADE) remote_identifier = models.CharField(max_length=500, blank=True, null=True) - category = models.ForeignKey( - 'Category', blank=True, null=True, on_delete=models.PROTECT - ) + category = models.ForeignKey("Category", blank=True, null=True, on_delete=models.PROTECT) def __str__(self): return "Post-{}".format(self.pk) diff --git a/src/newsreader/news/posts/tests/factories.py b/src/newsreader/news/posts/tests/factories.py index d059335..a961b4f 100644 --- a/src/newsreader/news/posts/tests/factories.py +++ b/src/newsreader/news/posts/tests/factories.py @@ -19,8 +19,8 @@ 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') + publication_date = factory.Faker("date_time_this_year", tzinfo=pytz.utc) + url = factory.Faker("url") remote_identifier = factory.Faker("url") rule = factory.SubFactory(CollectionRuleFactory) diff --git a/src/newsreader/news/posts/views.py b/src/newsreader/news/posts/views.py index 91ea44a..dc1ba72 100644 --- a/src/newsreader/news/posts/views.py +++ b/src/newsreader/news/posts/views.py @@ -1,3 +1,4 @@ from django.shortcuts import render + # Create your views here. diff --git a/src/newsreader/urls.py b/src/newsreader/urls.py index fdd5749..ed82d43 100644 --- a/src/newsreader/urls.py +++ b/src/newsreader/urls.py @@ -1,6 +1,5 @@ from django.contrib import admin from django.urls import include, path -urlpatterns = [ - path("admin/", admin.site.urls), -] + +urlpatterns = [path("admin/", admin.site.urls)] diff --git a/src/newsreader/wsgi.py b/src/newsreader/wsgi.py index 4c5ea26..ffe4b3e 100644 --- a/src/newsreader/wsgi.py +++ b/src/newsreader/wsgi.py @@ -11,6 +11,7 @@ import os from django.core.wsgi import get_wsgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'newsreader.settings') + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "newsreader.settings") application = get_wsgi_application() From 9c6be7357d0daa9df8f2e409ccd0944e5cd04b37 Mon Sep 17 00:00:00 2001 From: Sonny Date: Mon, 8 Jul 2019 22:54:24 +0200 Subject: [PATCH 009/364] Add post/category/rule endpoints --- requirements/base.txt | 1 + requirements/dev.txt | 2 + src/manage.py | 4 +- .../{news/posts => auth}/__init__.py | 0 src/newsreader/auth/admin.py | 1 + src/newsreader/auth/apps.py | 5 + src/newsreader/auth/backends.py | 15 + .../posts => auth}/migrations/__init__.py | 0 .../tests/__init__.py => auth/models.py} | 0 src/newsreader/auth/permissions.py | 23 + src/newsreader/auth/tests/__init__.py | 0 src/newsreader/auth/tests/factories.py | 20 + src/newsreader/auth/views.py | 1 + src/newsreader/conf/base.py | 17 +- src/newsreader/conf/dev.py | 8 +- src/newsreader/core/admin.py | 3 - src/newsreader/core/models.py | 3 +- src/newsreader/core/pagination.py | 12 + src/newsreader/core/permissions.py | 6 + src/newsreader/core/tests.py | 3 - src/newsreader/core/views.py | 3 - src/newsreader/news/collection/base.py | 4 - src/newsreader/news/collection/feed.py | 5 +- .../collection/management/commands/collect.py | 2 +- .../collection/migrations/0001_initial.py | 621 +++++++++++++++++- .../migrations/0002_auto_20190410_2028.py | 16 - ...ory.py => 0002_collectionrule_category.py} | 11 +- .../migrations/0003_collectionrule_user.py | 21 + .../0004_collectionrule_timezone.py | 613 ----------------- .../migrations/0005_auto_20190521_1941.py | 22 - .../migrations/0006_collectionrule_error.py | 16 - .../migrations/0007_auto_20190623_1837.py | 16 - .../migrations/0008_auto_20190623_1847.py | 16 - .../0009_collectionrule_website_url.py | 16 - .../migrations/0010_auto_20190628_2142.py | 19 - src/newsreader/news/collection/models.py | 11 +- .../news/collection/response_handler.py | 1 - src/newsreader/news/collection/serializers.py | 21 + .../news/collection/tests/__init__.py | 4 - .../collection/tests/endpoints/__init__.py | 0 .../tests/endpoints/rules/__init__.py | 0 .../tests/endpoints/rules/detail/__init__.py | 0 .../tests/endpoints/rules/detail/tests.py | 186 ++++++ .../tests/endpoints/rules/list/__init__.py | 0 .../tests/endpoints/rules/list/tests.py | 206 ++++++ .../news/collection/tests/factories.py | 12 +- .../news/collection/tests/favicon/__init__.py | 3 - .../tests/favicon/builder/__init__.py | 1 - .../collection/tests/favicon/builder/tests.py | 2 - .../tests/favicon/client/__init__.py | 1 - .../collection/tests/favicon/client/tests.py | 1 - .../tests/favicon/collector/__init__.py | 1 - .../tests/favicon/collector/tests.py | 3 - .../news/collection/tests/feed/__init__.py | 5 - .../collection/tests/feed/builder/__init__.py | 1 - .../collection/tests/feed/builder/tests.py | 4 +- .../collection/tests/feed/client/__init__.py | 1 - .../collection/tests/feed/client/tests.py | 1 - .../tests/feed/collector/__init__.py | 1 - .../collection/tests/feed/collector/tests.py | 4 +- .../tests/feed/duplicate_handler/__init__.py | 1 - .../tests/feed/duplicate_handler/tests.py | 3 +- .../collection/tests/feed/stream/__init__.py | 1 - .../collection/tests/feed/stream/tests.py | 1 - .../news/collection/tests/utils/__init__.py | 1 - src/newsreader/news/collection/urls.py | 12 + src/newsreader/news/collection/views.py | 24 +- src/newsreader/news/core/__init__.py | 0 src/newsreader/news/{posts => core}/admin.py | 4 +- src/newsreader/news/core/apps.py | 5 + .../migrations/0001_initial.py | 38 +- .../core/migrations/0002_category_user.py | 21 + .../news/core/migrations/__init__.py | 0 src/newsreader/news/{posts => core}/models.py | 11 +- src/newsreader/news/core/pagination.py | 31 + src/newsreader/news/core/serializers.py | 39 ++ 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 | 168 +++++ .../tests/endpoints/category/list/__init__.py | 0 .../tests/endpoints/category/list/tests.py | 186 ++++++ .../core/tests/endpoints/post/__init__.py | 0 .../tests/endpoints/post/detail/__init__.py | 0 .../core/tests/endpoints/post/detail/tests.py | 175 +++++ .../tests/endpoints/post/list/__init__.py | 0 .../core/tests/endpoints/post/list/tests.py | 208 ++++++ .../news/{posts => core}/tests/factories.py | 17 +- src/newsreader/news/core/urls.py | 16 + src/newsreader/news/core/views.py | 52 ++ src/newsreader/news/posts/apps.py | 5 - .../migrations/0002_auto_20190520_2206.py | 15 - .../migrations/0003_auto_20190520_2031.py | 14 - .../migrations/0004_auto_20190521_1941.py | 17 - .../migrations/0005_auto_20190608_1054.py | 17 - .../migrations/0006_auto_20190608_1520.py | 27 - src/newsreader/news/posts/views.py | 4 - src/newsreader/urls.py | 17 +- 99 files changed, 2174 insertions(+), 951 deletions(-) rename src/newsreader/{news/posts => auth}/__init__.py (100%) create mode 100644 src/newsreader/auth/admin.py create mode 100644 src/newsreader/auth/apps.py create mode 100644 src/newsreader/auth/backends.py rename src/newsreader/{news/posts => auth}/migrations/__init__.py (100%) rename src/newsreader/{news/posts/tests/__init__.py => auth/models.py} (100%) create mode 100644 src/newsreader/auth/permissions.py create mode 100644 src/newsreader/auth/tests/__init__.py create mode 100644 src/newsreader/auth/tests/factories.py create mode 100644 src/newsreader/auth/views.py create mode 100644 src/newsreader/core/pagination.py create mode 100644 src/newsreader/core/permissions.py delete mode 100644 src/newsreader/news/collection/migrations/0002_auto_20190410_2028.py rename src/newsreader/news/collection/migrations/{0003_collectionrule_category.py => 0002_collectionrule_category.py} (74%) create mode 100644 src/newsreader/news/collection/migrations/0003_collectionrule_user.py delete mode 100644 src/newsreader/news/collection/migrations/0004_collectionrule_timezone.py delete mode 100644 src/newsreader/news/collection/migrations/0005_auto_20190521_1941.py delete mode 100644 src/newsreader/news/collection/migrations/0006_collectionrule_error.py delete mode 100644 src/newsreader/news/collection/migrations/0007_auto_20190623_1837.py delete mode 100644 src/newsreader/news/collection/migrations/0008_auto_20190623_1847.py delete mode 100644 src/newsreader/news/collection/migrations/0009_collectionrule_website_url.py delete mode 100644 src/newsreader/news/collection/migrations/0010_auto_20190628_2142.py create mode 100644 src/newsreader/news/collection/serializers.py create mode 100644 src/newsreader/news/collection/tests/endpoints/__init__.py create mode 100644 src/newsreader/news/collection/tests/endpoints/rules/__init__.py create mode 100644 src/newsreader/news/collection/tests/endpoints/rules/detail/__init__.py create mode 100644 src/newsreader/news/collection/tests/endpoints/rules/detail/tests.py create mode 100644 src/newsreader/news/collection/tests/endpoints/rules/list/__init__.py create mode 100644 src/newsreader/news/collection/tests/endpoints/rules/list/tests.py create mode 100644 src/newsreader/news/collection/urls.py create mode 100644 src/newsreader/news/core/__init__.py rename src/newsreader/news/{posts => core}/admin.py (85%) create mode 100644 src/newsreader/news/core/apps.py rename src/newsreader/news/{posts => core}/migrations/0001_initial.py (54%) create mode 100644 src/newsreader/news/core/migrations/0002_category_user.py create mode 100644 src/newsreader/news/core/migrations/__init__.py rename src/newsreader/news/{posts => core}/models.py (78%) create mode 100644 src/newsreader/news/core/pagination.py create mode 100644 src/newsreader/news/core/serializers.py 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 rename src/newsreader/news/{posts => core}/tests/factories.py (68%) create mode 100644 src/newsreader/news/core/urls.py create mode 100644 src/newsreader/news/core/views.py delete mode 100644 src/newsreader/news/posts/apps.py delete mode 100644 src/newsreader/news/posts/migrations/0002_auto_20190520_2206.py delete mode 100644 src/newsreader/news/posts/migrations/0003_auto_20190520_2031.py delete mode 100644 src/newsreader/news/posts/migrations/0004_auto_20190521_1941.py delete mode 100644 src/newsreader/news/posts/migrations/0005_auto_20190608_1054.py delete mode 100644 src/newsreader/news/posts/migrations/0006_auto_20190608_1520.py delete mode 100644 src/newsreader/news/posts/views.py diff --git a/requirements/base.txt b/requirements/base.txt index 00667bb..05be61f 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -3,6 +3,7 @@ beautifulsoup4==4.7.1 certifi==2019.3.9 chardet==3.0.4 Django==2.2 +djangorestframework==3.9.4 lxml==4.3.4 feedparser==5.2.1 idna==2.8 diff --git a/requirements/dev.txt b/requirements/dev.txt index 0a685e3..dbf9e15 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -2,3 +2,5 @@ factory-boy==2.12.0 freezegun==0.3.12 +django-debug-toolbar==2.0 +django-extensions==2.1.9 diff --git a/src/manage.py b/src/manage.py index a30fa10..45fc02f 100755 --- a/src/manage.py +++ b/src/manage.py @@ -5,7 +5,7 @@ import sys def main(): - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'newsreader.conf.base') + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "newsreader.conf.dev") try: from django.core.management import execute_from_command_line except ImportError as exc: @@ -17,5 +17,5 @@ def main(): execute_from_command_line(sys.argv) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/src/newsreader/news/posts/__init__.py b/src/newsreader/auth/__init__.py similarity index 100% rename from src/newsreader/news/posts/__init__.py rename to src/newsreader/auth/__init__.py diff --git a/src/newsreader/auth/admin.py b/src/newsreader/auth/admin.py new file mode 100644 index 0000000..846f6b4 --- /dev/null +++ b/src/newsreader/auth/admin.py @@ -0,0 +1 @@ +# Register your models here. diff --git a/src/newsreader/auth/apps.py b/src/newsreader/auth/apps.py new file mode 100644 index 0000000..c467d4e --- /dev/null +++ b/src/newsreader/auth/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class AuthConfig(AppConfig): + name = "auth" diff --git a/src/newsreader/auth/backends.py b/src/newsreader/auth/backends.py new file mode 100644 index 0000000..30a78b9 --- /dev/null +++ b/src/newsreader/auth/backends.py @@ -0,0 +1,15 @@ +from django.contrib.auth import get_user_model +from django.contrib.auth.backends import ModelBackend + + +class EmailBackend(ModelBackend): + def authenticate(self, request, username=None, password=None, **kwargs): + user_model_class = get_user_model() + + try: + user = user_model_class.objects.get(email=username) + except user_model_class.DoesNotExist: + return + + if user.check_password(password) and self.user_can_authenticate(user): + return user diff --git a/src/newsreader/news/posts/migrations/__init__.py b/src/newsreader/auth/migrations/__init__.py similarity index 100% rename from src/newsreader/news/posts/migrations/__init__.py rename to src/newsreader/auth/migrations/__init__.py diff --git a/src/newsreader/news/posts/tests/__init__.py b/src/newsreader/auth/models.py similarity index 100% rename from src/newsreader/news/posts/tests/__init__.py rename to src/newsreader/auth/models.py diff --git a/src/newsreader/auth/permissions.py b/src/newsreader/auth/permissions.py new file mode 100644 index 0000000..2c6cf25 --- /dev/null +++ b/src/newsreader/auth/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/auth/tests/__init__.py b/src/newsreader/auth/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/newsreader/auth/tests/factories.py b/src/newsreader/auth/tests/factories.py new file mode 100644 index 0000000..3975f62 --- /dev/null +++ b/src/newsreader/auth/tests/factories.py @@ -0,0 +1,20 @@ +from django.contrib.auth.models import User + +import factory + + +class UserFactory(factory.django.DjangoModelFactory): + username = factory.Sequence(lambda n: f"user-{n}") + email = factory.LazyAttribute(lambda o: f"{o.username}@example.org") + 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 diff --git a/src/newsreader/auth/views.py b/src/newsreader/auth/views.py new file mode 100644 index 0000000..60f00ef --- /dev/null +++ b/src/newsreader/auth/views.py @@ -0,0 +1 @@ +# Create your views here. diff --git a/src/newsreader/conf/base.py b/src/newsreader/conf/base.py index 16927e4..593c42c 100644 --- a/src/newsreader/conf/base.py +++ b/src/newsreader/conf/base.py @@ -26,6 +26,7 @@ SECRET_KEY = "^!7a2jq5j!exc-55vf$anx9^6ff6=u_ub5=5p1(1x47fix)syh" DEBUG = False ALLOWED_HOSTS = ["127.0.0.1"] +INTERNAL_IPS = ["127.0.0.1"] # Application definition INSTALLED_APPS = [ @@ -35,9 +36,11 @@ INSTALLED_APPS = [ "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", + # third party apps + "rest_framework", # app modules + "newsreader.news.core", "newsreader.news.collection", - "newsreader.news.posts", ] MIDDLEWARE = [ @@ -89,6 +92,9 @@ AUTH_PASSWORD_VALIDATORS = [ {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, ] +# Authentication +AUTHENTICATION_BACKENDS = ["newsreader.auth.backends.EmailBackend"] + # Internationalization # https://docs.djangoproject.com/en/2.2/topics/i18n/ LANGUAGE_CODE = "en-us" @@ -101,3 +107,12 @@ USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/2.2/howto/static-files/ STATIC_URL = "/static/" + +# Third party settings +REST_FRAMEWORK = { + "DEFAULT_AUTHENTICATION_CLASSES": ("rest_framework.authentication.SessionAuthentication",), + "DEFAULT_PERMISSION_CLASSES": ( + "rest_framework.permissions.IsAuthenticated", + "newsreader.auth.permissions.IsOwner", + ), +} diff --git a/src/newsreader/conf/dev.py b/src/newsreader/conf/dev.py index 245a78c..7453d52 100644 --- a/src/newsreader/conf/dev.py +++ b/src/newsreader/conf/dev.py @@ -1,13 +1,15 @@ from .base import * -# Development settings - DEBUG = True +MIDDLEWARE += ["debug_toolbar.middleware.DebugToolbarMiddleware"] + EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" +INSTALLED_APPS += ["debug_toolbar", "django_extensions"] + try: - from .local import * + pass except ImportError: pass diff --git a/src/newsreader/core/admin.py b/src/newsreader/core/admin.py index a011d19..846f6b4 100644 --- a/src/newsreader/core/admin.py +++ b/src/newsreader/core/admin.py @@ -1,4 +1 @@ -from django.contrib import admin - - # Register your models here. diff --git a/src/newsreader/core/models.py b/src/newsreader/core/models.py index 2e696fb..f8bd80f 100644 --- a/src/newsreader/core/models.py +++ b/src/newsreader/core/models.py @@ -1,4 +1,5 @@ from django.db import models +from django.utils import timezone class TimeStampedModel(models.Model): @@ -7,7 +8,7 @@ class TimeStampedModel(models.Model): updating ``created`` and ``modified`` fields. """ - created = models.DateTimeField(auto_now_add=True) + created = models.DateTimeField(default=timezone.now) modified = models.DateTimeField(auto_now=True) class Meta: 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 index 7c72b39..a39b155 100644 --- a/src/newsreader/core/tests.py +++ b/src/newsreader/core/tests.py @@ -1,4 +1 @@ -from django.test import TestCase - - # Create your tests here. diff --git a/src/newsreader/core/views.py b/src/newsreader/core/views.py index dc1ba72..60f00ef 100644 --- a/src/newsreader/core/views.py +++ b/src/newsreader/core/views.py @@ -1,4 +1 @@ -from django.shortcuts import render - - # Create your views here. diff --git a/src/newsreader/news/collection/base.py b/src/newsreader/news/collection/base.py index c202df8..1b80256 100644 --- a/src/newsreader/news/collection/base.py +++ b/src/newsreader/news/collection/base.py @@ -1,9 +1,5 @@ from typing import ContextManager, Dict, List, Optional, Tuple -from django.utils import timezone - -import requests - from bs4 import BeautifulSoup from newsreader.news.collection.exceptions import StreamParseException diff --git a/src/newsreader/news/collection/feed.py b/src/newsreader/news/collection/feed.py index 2cf248c..a7ab2fc 100644 --- a/src/newsreader/news/collection/feed.py +++ b/src/newsreader/news/collection/feed.py @@ -17,9 +17,8 @@ from newsreader.news.collection.exceptions import ( StreamTimeOutException, ) from newsreader.news.collection.models import CollectionRule -from newsreader.news.collection.response_handler import ResponseHandler from newsreader.news.collection.utils import build_publication_date, fetch -from newsreader.news.posts.models import Post +from newsreader.news.core.models import Post class FeedBuilder(Builder): @@ -62,7 +61,7 @@ class FeedBuilder(Builder): tz = pytz.timezone(rule.timezone) for entry in entries: - data = {"rule_id": rule.pk, "category": rule.category} + data = {"rule_id": rule.pk} for field, value in field_mapping.items(): if field in entry: diff --git a/src/newsreader/news/collection/management/commands/collect.py b/src/newsreader/news/collection/management/commands/collect.py index 855089f..3da9905 100644 --- a/src/newsreader/news/collection/management/commands/collect.py +++ b/src/newsreader/news/collection/management/commands/collect.py @@ -1,4 +1,4 @@ -from django.core.management.base import BaseCommand, CommandError +from django.core.management.base import BaseCommand from newsreader.news.collection.feed import FeedCollector from newsreader.news.collection.models import CollectionRule diff --git a/src/newsreader/news/collection/migrations/0001_initial.py b/src/newsreader/news/collection/migrations/0001_initial.py index 1091b7a..00d3d31 100644 --- a/src/newsreader/news/collection/migrations/0001_initial.py +++ b/src/newsreader/news/collection/migrations/0001_initial.py @@ -1,4 +1,6 @@ -# Generated by Django 2.2 on 2019-04-10 20:10 +# Generated by Django 2.2 on 2019-07-05 20:59 + +import django.utils.timezone from django.db import migrations, models @@ -19,10 +21,623 @@ class Migration(migrations.Migration): 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()), - ("last_suceeded", models.DateTimeField()), + ("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_20190410_2028.py b/src/newsreader/news/collection/migrations/0002_auto_20190410_2028.py deleted file mode 100644 index ce45e0e..0000000 --- a/src/newsreader/news/collection/migrations/0002_auto_20190410_2028.py +++ /dev/null @@ -1,16 +0,0 @@ -# Generated by Django 2.2 on 2019-04-10 20:28 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [("collection", "0001_initial")] - - operations = [ - migrations.AlterField( - model_name="collectionrule", - name="last_suceeded", - field=models.DateTimeField(blank=True, null=True), - ) - ] diff --git a/src/newsreader/news/collection/migrations/0003_collectionrule_category.py b/src/newsreader/news/collection/migrations/0002_collectionrule_category.py similarity index 74% rename from src/newsreader/news/collection/migrations/0003_collectionrule_category.py rename to src/newsreader/news/collection/migrations/0002_collectionrule_category.py index b15f7f7..2cb1ee4 100644 --- a/src/newsreader/news/collection/migrations/0003_collectionrule_category.py +++ b/src/newsreader/news/collection/migrations/0002_collectionrule_category.py @@ -1,13 +1,14 @@ -# Generated by Django 2.2 on 2019-05-20 20:06 - -import django.db.models.deletion +# Generated by Django 2.2 on 2019-07-05 20:59 from django.db import migrations, models +import django.db.models.deletion class Migration(migrations.Migration): - dependencies = [("posts", "0002_auto_20190520_2206"), ("collection", "0002_auto_20190410_2028")] + initial = True + + dependencies = [("collection", "0001_initial"), ("core", "0001_initial")] operations = [ migrations.AddField( @@ -18,7 +19,7 @@ class Migration(migrations.Migration): help_text="Posts from this rule will be tagged with this category", null=True, on_delete=django.db.models.deletion.SET_NULL, - to="posts.Category", + to="core.Category", verbose_name="Category", ), ) diff --git a/src/newsreader/news/collection/migrations/0003_collectionrule_user.py b/src/newsreader/news/collection/migrations/0003_collectionrule_user.py new file mode 100644 index 0000000..a735ea3 --- /dev/null +++ b/src/newsreader/news/collection/migrations/0003_collectionrule_user.py @@ -0,0 +1,21 @@ +# Generated by Django 2.2 on 2019-07-07 17:08 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("collection", "0002_collectionrule_category"), + ] + + operations = [ + migrations.AddField( + model_name="collectionrule", + name="user", + field=models.ForeignKey(default=None, on_delete="Owner", to=settings.AUTH_USER_MODEL), + preserve_default=False, + ) + ] diff --git a/src/newsreader/news/collection/migrations/0004_collectionrule_timezone.py b/src/newsreader/news/collection/migrations/0004_collectionrule_timezone.py deleted file mode 100644 index 837e625..0000000 --- a/src/newsreader/news/collection/migrations/0004_collectionrule_timezone.py +++ /dev/null @@ -1,613 +0,0 @@ -# Generated by Django 2.2 on 2019-05-20 20:41 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [("collection", "0003_collectionrule_category")] - - operations = [ - migrations.AddField( - model_name="collectionrule", - name="timezone", - field=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, - ), - ) - ] diff --git a/src/newsreader/news/collection/migrations/0005_auto_20190521_1941.py b/src/newsreader/news/collection/migrations/0005_auto_20190521_1941.py deleted file mode 100644 index 8e3a53f..0000000 --- a/src/newsreader/news/collection/migrations/0005_auto_20190521_1941.py +++ /dev/null @@ -1,22 +0,0 @@ -# Generated by Django 2.2 on 2019-05-21 19:41 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [("collection", "0004_collectionrule_timezone")] - - operations = [ - migrations.AddField( - model_name="collectionrule", - name="favicon", - field=models.ImageField(blank=True, null=True, upload_to=""), - ), - migrations.AddField( - model_name="collectionrule", - name="source", - field=models.CharField(default="source", max_length=100), - preserve_default=False, - ), - ] diff --git a/src/newsreader/news/collection/migrations/0006_collectionrule_error.py b/src/newsreader/news/collection/migrations/0006_collectionrule_error.py deleted file mode 100644 index b70b638..0000000 --- a/src/newsreader/news/collection/migrations/0006_collectionrule_error.py +++ /dev/null @@ -1,16 +0,0 @@ -# Generated by Django 2.2 on 2019-06-08 14:13 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [("collection", "0005_auto_20190521_1941")] - - operations = [ - migrations.AddField( - model_name="collectionrule", - name="error", - field=models.CharField(blank=True, max_length=255, null=True), - ) - ] diff --git a/src/newsreader/news/collection/migrations/0007_auto_20190623_1837.py b/src/newsreader/news/collection/migrations/0007_auto_20190623_1837.py deleted file mode 100644 index cc27d6e..0000000 --- a/src/newsreader/news/collection/migrations/0007_auto_20190623_1837.py +++ /dev/null @@ -1,16 +0,0 @@ -# Generated by Django 2.2 on 2019-06-23 18:37 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [("collection", "0006_collectionrule_error")] - - operations = [ - migrations.AlterField( - model_name="collectionrule", - name="favicon", - field=models.ImageField(default="favicons/default-favicon.ico", upload_to="favicons/"), - ) - ] diff --git a/src/newsreader/news/collection/migrations/0008_auto_20190623_1847.py b/src/newsreader/news/collection/migrations/0008_auto_20190623_1847.py deleted file mode 100644 index 3c8ae66..0000000 --- a/src/newsreader/news/collection/migrations/0008_auto_20190623_1847.py +++ /dev/null @@ -1,16 +0,0 @@ -# Generated by Django 2.2 on 2019-06-23 18:47 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [("collection", "0007_auto_20190623_1837")] - - operations = [ - migrations.AlterField( - model_name="collectionrule", - name="favicon", - field=models.URLField(blank=True, null=True), - ) - ] diff --git a/src/newsreader/news/collection/migrations/0009_collectionrule_website_url.py b/src/newsreader/news/collection/migrations/0009_collectionrule_website_url.py deleted file mode 100644 index e5273b3..0000000 --- a/src/newsreader/news/collection/migrations/0009_collectionrule_website_url.py +++ /dev/null @@ -1,16 +0,0 @@ -# Generated by Django 2.2 on 2019-06-27 21:27 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [("collection", "0008_auto_20190623_1847")] - - operations = [ - migrations.AddField( - model_name="collectionrule", - name="website_url", - field=models.URLField(blank=True, editable=False, null=True), - ) - ] diff --git a/src/newsreader/news/collection/migrations/0010_auto_20190628_2142.py b/src/newsreader/news/collection/migrations/0010_auto_20190628_2142.py deleted file mode 100644 index 3726158..0000000 --- a/src/newsreader/news/collection/migrations/0010_auto_20190628_2142.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 2.2 on 2019-06-28 21:42 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [("collection", "0009_collectionrule_website_url")] - - operations = [ - migrations.AlterField( - model_name="collectionrule", name="url", field=models.URLField(max_length=1024) - ), - migrations.AlterField( - model_name="collectionrule", - name="website_url", - field=models.URLField(blank=True, editable=False, max_length=1024, null=True), - ), - ] diff --git a/src/newsreader/news/collection/models.py b/src/newsreader/news/collection/models.py index 03768ac..d65176a 100644 --- a/src/newsreader/news/collection/models.py +++ b/src/newsreader/news/collection/models.py @@ -1,13 +1,13 @@ -from django.conf import settings from django.db import models from django.utils.translation import gettext as _ import pytz +from newsreader.core.models import TimeStampedModel -class CollectionRule(models.Model): + +class CollectionRule(TimeStampedModel): name = models.CharField(max_length=100) - source = models.CharField(max_length=100) url = models.URLField(max_length=1024) website_url = models.URLField(max_length=1024, editable=False, blank=True, null=True) @@ -20,7 +20,7 @@ class CollectionRule(models.Model): ) category = models.ForeignKey( - "posts.Category", + "core.Category", blank=True, null=True, verbose_name=_("Category"), @@ -30,8 +30,9 @@ class CollectionRule(models.Model): last_suceeded = models.DateTimeField(blank=True, null=True) succeeded = models.BooleanField(default=False) - error = models.CharField(max_length=255, blank=True, null=True) + user = models.ForeignKey("auth.User", _("Owner")) + def __str__(self): return self.name diff --git a/src/newsreader/news/collection/response_handler.py b/src/newsreader/news/collection/response_handler.py index e412475..d9598c4 100644 --- a/src/newsreader/news/collection/response_handler.py +++ b/src/newsreader/news/collection/response_handler.py @@ -1,6 +1,5 @@ from typing import ContextManager -from requests import Response from requests.exceptions import ConnectionError as RequestConnectionError from requests.exceptions import ( HTTPError, diff --git a/src/newsreader/news/collection/serializers.py b/src/newsreader/news/collection/serializers.py new file mode 100644 index 0000000..cf6f9ea --- /dev/null +++ b/src/newsreader/news/collection/serializers.py @@ -0,0 +1,21 @@ +from rest_framework import serializers + +from newsreader.news import core +from newsreader.news.collection.models import CollectionRule + + +class CollectionRuleSerializer(serializers.HyperlinkedModelSerializer): + posts = serializers.SerializerMethodField() + user = serializers.HiddenField(default=serializers.CurrentUserDefault()) + + def get_posts(self, instance): + request = self.context.get("request") + posts = instance.post_set.order_by("-publication_date") + + serializer = core.serializers.PostSerializer(posts, context={"request": request}, many=True) + return serializer.data + + class Meta: + model = CollectionRule + fields = ("id", "name", "url", "favicon", "category", "posts", "user") + extra_kwargs = {"category": {"view_name": "api:categories-detail"}} diff --git a/src/newsreader/news/collection/tests/__init__.py b/src/newsreader/news/collection/tests/__init__.py index ea6a7c0..e69de29 100644 --- a/src/newsreader/news/collection/tests/__init__.py +++ b/src/newsreader/news/collection/tests/__init__.py @@ -1,4 +0,0 @@ -from .favicon import * -from .feed import * -from .tests import * -from .utils import * 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/rules/__init__.py b/src/newsreader/news/collection/tests/endpoints/rules/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/newsreader/news/collection/tests/endpoints/rules/detail/__init__.py b/src/newsreader/news/collection/tests/endpoints/rules/detail/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/newsreader/news/collection/tests/endpoints/rules/detail/tests.py b/src/newsreader/news/collection/tests/endpoints/rules/detail/tests.py new file mode 100644 index 0000000..8782533 --- /dev/null +++ b/src/newsreader/news/collection/tests/endpoints/rules/detail/tests.py @@ -0,0 +1,186 @@ +import json + +from urllib.parse import urljoin + +from django.test import Client, TestCase +from django.urls import reverse + +from newsreader.auth.tests.factories import UserFactory +from newsreader.news.collection.tests.factories import CollectionRuleFactory +from newsreader.news.core.tests.factories import CategoryFactory + + +class CollectionRuleDetailViewTestCase(TestCase): + def setUp(self): + self.maxDiff = None + + self.user = UserFactory(is_staff=True, password="test") + self.client = Client() + + def test_simple(self): + rule = CollectionRuleFactory(user=self.user) + + self.client.force_login(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) + self.assertTrue("posts" in data) + + def test_not_known(self): + self.client.force_login(self.user) + 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) + + self.client.force_login(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) + + self.client.force_login(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_with_absolute_url(self): + old_category = CategoryFactory(user=self.user) + new_category = CategoryFactory(user=self.user) + + base_url = "http://testserver" + relative_url = reverse("api:categories-detail", args=[new_category.pk]) + + absolute_url = urljoin(base_url, relative_url) + + rule = CollectionRuleFactory(name="BBC", category=old_category, user=self.user) + + self.client.force_login(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"], absolute_url) + + def test_category_change_with_relative_url(self): + old_category = CategoryFactory(user=self.user) + new_category = CategoryFactory(user=self.user) + + base_url = "http://testserver" + relative_url = reverse("api:categories-detail", args=[new_category.pk]) + + absolute_url = urljoin(base_url, relative_url) + + rule = CollectionRuleFactory(name="BBC", category=old_category, user=self.user) + + self.client.force_login(self.user) + response = self.client.patch( + reverse("api:rules-detail", args=[rule.pk]), + data=json.dumps({"category": relative_url}), + content_type="application/json", + ) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertEquals(data["category"], absolute_url) + + def test_identifier_cannot_be_changed(self): + rule = CollectionRuleFactory(user=self.user) + + self.client.force_login(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) + + self.client.force_login(self.user) + response = self.client.patch( + reverse("api:rules-detail", args=[rule.pk]), + data=json.dumps({"category": reverse("api:categories-detail", args=[category.pk])}), + content_type="application/json", + ) + data = response.json() + url = data["category"] + + self.assertEquals(response.status_code, 200) + self.assertTrue(url.endswith(reverse("api:categories-detail", args=[category.pk]))) + + def test_put(self): + rule = CollectionRuleFactory(name="BBC", user=self.user) + + self.client.force_login(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) + + self.client.force_login(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): + 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) + + self.client.force_login(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) diff --git a/src/newsreader/news/collection/tests/endpoints/rules/list/__init__.py b/src/newsreader/news/collection/tests/endpoints/rules/list/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/newsreader/news/collection/tests/endpoints/rules/list/tests.py b/src/newsreader/news/collection/tests/endpoints/rules/list/tests.py new file mode 100644 index 0000000..f5a0cea --- /dev/null +++ b/src/newsreader/news/collection/tests/endpoints/rules/list/tests.py @@ -0,0 +1,206 @@ +import json + +from datetime import date, datetime, time + +from django.test import Client, TestCase +from django.urls import reverse + +import pytz + +from newsreader.auth.tests.factories import UserFactory +from newsreader.news.collection.tests.factories import CollectionRuleFactory +from newsreader.news.core.tests.factories import CategoryFactory, PostFactory + + +class CollectionRuleListViewTestCase(TestCase): + def setUp(self): + self.maxDiff = None + + self.user = UserFactory(is_staff=True, password="test") + self.client = Client() + + def test_simple(self): + CollectionRuleFactory.create_batch(size=3, user=self.user) + + self.client.force_login(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, + ), + ] + + self.client.force_login(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) + + self.client.force_login(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): + self.client.force_login(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"], 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": reverse("api:categories-detail", args=[category.pk]), + } + + self.client.force_login(self.user) + response = self.client.post( + reverse("api:rules-list"), data=json.dumps(data), content_type="application/json" + ) + data = response.json() + category_url = data["category"] + + self.assertEquals(response.status_code, 201) + + self.assertEquals(data["name"], "BBC") + self.assertEquals(data["url"], "https://www.bbc.co.uk") + + self.assertTrue(category_url.endswith(reverse("api:categories-detail", args=[category.pk]))) + + def test_patch(self): + self.client.force_login(self.user) + 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): + self.client.force_login(self.user) + 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): + self.client.force_login(self.user) + 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_posts(self): + rules = { + rule: PostFactory.create_batch(size=5, rule=rule) + for rule in CollectionRuleFactory.create_batch(size=5, user=self.user) + } + + self.client.force_login(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"], 5) + + self.assertEquals(len(data["results"]), 5) + + self.assertEquals(len(data["results"][0]["posts"]), 5) + + def test_rules_with_posts_ordered(self): + rules = { + rule: PostFactory.create_batch(size=5, rule=rule) + for rule in CollectionRuleFactory.create_batch(size=2, user=self.user) + } + + self.client.force_login(self.user) + response = self.client.get(reverse("api:rules-list")) + data = response.json() + + first_post_set = data["results"][0]["posts"] + second_post_set = data["results"][1]["posts"] + + self.assertEquals(response.status_code, 200) + self.assertTrue("results" in data) + self.assertTrue("count" in data) + self.assertEquals(data["count"], 2) + + self.assertEquals(len(data["results"]), 2) + + for result_set in [first_post_set, second_post_set]: + for count, post in enumerate(result_set): + if count < 1: + continue + + self.assertTrue( + post["publication_date"] < result_set[count - 1]["publication_date"] + ) + + def test_rule_with_unauthenticated_user(self): + CollectionRuleFactory.create_batch(size=3, user=self.user) + + response = self.client.get(reverse("api:rules-list")) + response.json() + + self.assertEquals(response.status_code, 403) + + def test_rule_with_unauthorized_user(self): + other_user = UserFactory() + CollectionRuleFactory.create_batch(size=3, user=other_user) + + self.client.force_login(self.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) diff --git a/src/newsreader/news/collection/tests/factories.py b/src/newsreader/news/collection/tests/factories.py index be54806..9d0803f 100644 --- a/src/newsreader/news/collection/tests/factories.py +++ b/src/newsreader/news/collection/tests/factories.py @@ -1,13 +1,17 @@ import factory +from newsreader.auth.tests.factories import UserFactory from newsreader.news.collection.models import CollectionRule class CollectionRuleFactory(factory.django.DjangoModelFactory): - class Meta: - model = CollectionRule - name = factory.Sequence(lambda n: "CollectionRule-{}".format(n)) - source = factory.Faker("name") 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 index 5fb0299..e69de29 100644 --- a/src/newsreader/news/collection/tests/favicon/__init__.py +++ b/src/newsreader/news/collection/tests/favicon/__init__.py @@ -1,3 +0,0 @@ -from .builder import * -from .client import * -from .collector import * diff --git a/src/newsreader/news/collection/tests/favicon/builder/__init__.py b/src/newsreader/news/collection/tests/favicon/builder/__init__.py index 8baa6e5..e69de29 100644 --- a/src/newsreader/news/collection/tests/favicon/builder/__init__.py +++ b/src/newsreader/news/collection/tests/favicon/builder/__init__.py @@ -1 +0,0 @@ -from .tests import * diff --git a/src/newsreader/news/collection/tests/favicon/builder/tests.py b/src/newsreader/news/collection/tests/favicon/builder/tests.py index c8bd14c..2e7b57a 100644 --- a/src/newsreader/news/collection/tests/favicon/builder/tests.py +++ b/src/newsreader/news/collection/tests/favicon/builder/tests.py @@ -1,7 +1,5 @@ from django.test import TestCase -from freezegun import freeze_time - from newsreader.news.collection.favicon import FaviconBuilder from newsreader.news.collection.tests.factories import CollectionRuleFactory from newsreader.news.collection.tests.favicon.builder.mocks import * diff --git a/src/newsreader/news/collection/tests/favicon/client/__init__.py b/src/newsreader/news/collection/tests/favicon/client/__init__.py index 8baa6e5..e69de29 100644 --- a/src/newsreader/news/collection/tests/favicon/client/__init__.py +++ b/src/newsreader/news/collection/tests/favicon/client/__init__.py @@ -1 +0,0 @@ -from .tests import * diff --git a/src/newsreader/news/collection/tests/favicon/client/tests.py b/src/newsreader/news/collection/tests/favicon/client/tests.py index 4ac2a40..717ee0c 100644 --- a/src/newsreader/news/collection/tests/favicon/client/tests.py +++ b/src/newsreader/news/collection/tests/favicon/client/tests.py @@ -2,7 +2,6 @@ from unittest.mock import MagicMock from django.test import TestCase -from newsreader.news.collection.base import WebsiteStream from newsreader.news.collection.exceptions import ( StreamDeniedException, StreamException, diff --git a/src/newsreader/news/collection/tests/favicon/collector/__init__.py b/src/newsreader/news/collection/tests/favicon/collector/__init__.py index 8baa6e5..e69de29 100644 --- a/src/newsreader/news/collection/tests/favicon/collector/__init__.py +++ b/src/newsreader/news/collection/tests/favicon/collector/__init__.py @@ -1 +0,0 @@ -from .tests import * diff --git a/src/newsreader/news/collection/tests/favicon/collector/tests.py b/src/newsreader/news/collection/tests/favicon/collector/tests.py index a292c16..48c16e7 100644 --- a/src/newsreader/news/collection/tests/favicon/collector/tests.py +++ b/src/newsreader/news/collection/tests/favicon/collector/tests.py @@ -1,9 +1,6 @@ from unittest.mock import MagicMock, patch from django.test import TestCase -from django.utils import timezone - -import pytz from bs4 import BeautifulSoup diff --git a/src/newsreader/news/collection/tests/feed/__init__.py b/src/newsreader/news/collection/tests/feed/__init__.py index 50cea54..e69de29 100644 --- a/src/newsreader/news/collection/tests/feed/__init__.py +++ b/src/newsreader/news/collection/tests/feed/__init__.py @@ -1,5 +0,0 @@ -from .builder import * -from .client import * -from .collector import * -from .duplicate_handler import * -from .stream import * diff --git a/src/newsreader/news/collection/tests/feed/builder/__init__.py b/src/newsreader/news/collection/tests/feed/builder/__init__.py index 8baa6e5..e69de29 100644 --- a/src/newsreader/news/collection/tests/feed/builder/__init__.py +++ b/src/newsreader/news/collection/tests/feed/builder/__init__.py @@ -1 +0,0 @@ -from .tests import * diff --git a/src/newsreader/news/collection/tests/feed/builder/tests.py b/src/newsreader/news/collection/tests/feed/builder/tests.py index 6efd432..94e84ea 100644 --- a/src/newsreader/news/collection/tests/feed/builder/tests.py +++ b/src/newsreader/news/collection/tests/feed/builder/tests.py @@ -10,8 +10,8 @@ from freezegun import freeze_time from newsreader.news.collection.feed import FeedBuilder from newsreader.news.collection.tests.factories import CollectionRuleFactory -from newsreader.news.posts.models import Post -from newsreader.news.posts.tests.factories import PostFactory +from newsreader.news.core.models import Post +from newsreader.news.core.tests.factories import PostFactory from .mocks import * diff --git a/src/newsreader/news/collection/tests/feed/client/__init__.py b/src/newsreader/news/collection/tests/feed/client/__init__.py index 8baa6e5..e69de29 100644 --- a/src/newsreader/news/collection/tests/feed/client/__init__.py +++ b/src/newsreader/news/collection/tests/feed/client/__init__.py @@ -1 +0,0 @@ -from .tests import * diff --git a/src/newsreader/news/collection/tests/feed/client/tests.py b/src/newsreader/news/collection/tests/feed/client/tests.py index bd9e4eb..9c11cbd 100644 --- a/src/newsreader/news/collection/tests/feed/client/tests.py +++ b/src/newsreader/news/collection/tests/feed/client/tests.py @@ -1,7 +1,6 @@ from unittest.mock import MagicMock, patch from django.test import TestCase -from django.utils import timezone from newsreader.news.collection.exceptions import ( StreamDeniedException, diff --git a/src/newsreader/news/collection/tests/feed/collector/__init__.py b/src/newsreader/news/collection/tests/feed/collector/__init__.py index 8baa6e5..e69de29 100644 --- a/src/newsreader/news/collection/tests/feed/collector/__init__.py +++ b/src/newsreader/news/collection/tests/feed/collector/__init__.py @@ -1 +0,0 @@ -from .tests import * diff --git a/src/newsreader/news/collection/tests/feed/collector/tests.py b/src/newsreader/news/collection/tests/feed/collector/tests.py index 16008dd..6978ee9 100644 --- a/src/newsreader/news/collection/tests/feed/collector/tests.py +++ b/src/newsreader/news/collection/tests/feed/collector/tests.py @@ -20,8 +20,8 @@ from newsreader.news.collection.exceptions import ( from newsreader.news.collection.feed import FeedCollector from newsreader.news.collection.tests.factories import CollectionRuleFactory from newsreader.news.collection.utils import build_publication_date -from newsreader.news.posts.models import Post -from newsreader.news.posts.tests.factories import PostFactory +from newsreader.news.core.models import Post +from newsreader.news.core.tests.factories import PostFactory from .mocks import ( duplicate_mock, diff --git a/src/newsreader/news/collection/tests/feed/duplicate_handler/__init__.py b/src/newsreader/news/collection/tests/feed/duplicate_handler/__init__.py index 8baa6e5..e69de29 100644 --- a/src/newsreader/news/collection/tests/feed/duplicate_handler/__init__.py +++ b/src/newsreader/news/collection/tests/feed/duplicate_handler/__init__.py @@ -1 +0,0 @@ -from .tests import * diff --git a/src/newsreader/news/collection/tests/feed/duplicate_handler/tests.py b/src/newsreader/news/collection/tests/feed/duplicate_handler/tests.py index eff63cc..18b6a99 100644 --- a/src/newsreader/news/collection/tests/feed/duplicate_handler/tests.py +++ b/src/newsreader/news/collection/tests/feed/duplicate_handler/tests.py @@ -3,8 +3,7 @@ from django.utils import timezone from newsreader.news.collection.feed import FeedDuplicateHandler from newsreader.news.collection.tests.factories import CollectionRuleFactory -from newsreader.news.posts.models import Post -from newsreader.news.posts.tests.factories import PostFactory +from newsreader.news.core.tests.factories import PostFactory class FeedDuplicateHandlerTestCase(TestCase): diff --git a/src/newsreader/news/collection/tests/feed/stream/__init__.py b/src/newsreader/news/collection/tests/feed/stream/__init__.py index 8baa6e5..e69de29 100644 --- a/src/newsreader/news/collection/tests/feed/stream/__init__.py +++ b/src/newsreader/news/collection/tests/feed/stream/__init__.py @@ -1 +0,0 @@ -from .tests import * diff --git a/src/newsreader/news/collection/tests/feed/stream/tests.py b/src/newsreader/news/collection/tests/feed/stream/tests.py index 8509156..7c0f203 100644 --- a/src/newsreader/news/collection/tests/feed/stream/tests.py +++ b/src/newsreader/news/collection/tests/feed/stream/tests.py @@ -1,7 +1,6 @@ from unittest.mock import MagicMock, patch from django.test import TestCase -from django.utils import timezone from newsreader.news.collection.exceptions import ( StreamDeniedException, diff --git a/src/newsreader/news/collection/tests/utils/__init__.py b/src/newsreader/news/collection/tests/utils/__init__.py index 8baa6e5..e69de29 100644 --- a/src/newsreader/news/collection/tests/utils/__init__.py +++ b/src/newsreader/news/collection/tests/utils/__init__.py @@ -1 +0,0 @@ -from .tests import * diff --git a/src/newsreader/news/collection/urls.py b/src/newsreader/news/collection/urls.py new file mode 100644 index 0000000..4b59a09 --- /dev/null +++ b/src/newsreader/news/collection/urls.py @@ -0,0 +1,12 @@ +from django.urls import path + +from newsreader.news.collection.views import ( + CollectionRuleAPIListView, + CollectionRuleDetailView, +) + + +endpoints = [ + path("rules/", CollectionRuleDetailView.as_view(), name="rules-detail"), + path("rules/", CollectionRuleAPIListView.as_view(), name="rules-list"), +] diff --git a/src/newsreader/news/collection/views.py b/src/newsreader/news/collection/views.py index dc1ba72..3de472b 100644 --- a/src/newsreader/news/collection/views.py +++ b/src/newsreader/news/collection/views.py @@ -1,4 +1,24 @@ -from django.shortcuts import render +from rest_framework.generics import ( + ListCreateAPIView, + RetrieveUpdateDestroyAPIView, +) + +from newsreader.core.pagination import ResultSetPagination +from newsreader.news.collection.models import CollectionRule +from newsreader.news.collection.serializers import CollectionRuleSerializer -# Create your views here. +class CollectionRuleAPIListView(ListCreateAPIView): + queryset = CollectionRule.objects.all() + serializer_class = CollectionRuleSerializer + pagination_class = ResultSetPagination + + def get_queryset(self): + user = self.request.user + return self.queryset.filter(user=user).order_by("-created") + + +class CollectionRuleDetailView(RetrieveUpdateDestroyAPIView): + queryset = CollectionRule.objects.all() + serializer_class = CollectionRuleSerializer + pagination_class = ResultSetPagination diff --git a/src/newsreader/news/core/__init__.py b/src/newsreader/news/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/newsreader/news/posts/admin.py b/src/newsreader/news/core/admin.py similarity index 85% rename from src/newsreader/news/posts/admin.py rename to src/newsreader/news/core/admin.py index 3e6940b..3bcfc19 100644 --- a/src/newsreader/news/posts/admin.py +++ b/src/newsreader/news/core/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from newsreader.news.posts.models import Category, Post +from newsreader.news.core.models import Category, Post class PostAdmin(admin.ModelAdmin): @@ -10,7 +10,7 @@ class PostAdmin(admin.ModelAdmin): ordering = ("-publication_date", "title") - fields = ("title", "body", "author", "publication_date", "url", "remote_identifier", "category") + fields = ("title", "body", "author", "publication_date", "url") search_fields = ["title"] diff --git a/src/newsreader/news/core/apps.py b/src/newsreader/news/core/apps.py new file mode 100644 index 0000000..5ef1d60 --- /dev/null +++ b/src/newsreader/news/core/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class CoreConfig(AppConfig): + name = "core" diff --git a/src/newsreader/news/posts/migrations/0001_initial.py b/src/newsreader/news/core/migrations/0001_initial.py similarity index 54% rename from src/newsreader/news/posts/migrations/0001_initial.py rename to src/newsreader/news/core/migrations/0001_initial.py index c7e7604..5e0ffca 100644 --- a/src/newsreader/news/posts/migrations/0001_initial.py +++ b/src/newsreader/news/core/migrations/0001_initial.py @@ -1,8 +1,8 @@ -# Generated by Django 2.2 on 2019-04-10 20:10 - -import django.db.models.deletion +# Generated by Django 2.2 on 2019-07-05 20:59 from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone class Migration(migrations.Migration): @@ -21,11 +21,11 @@ class Migration(migrations.Migration): auto_created=True, primary_key=True, serialize=False, verbose_name="ID" ), ), - ("created", models.DateTimeField(auto_now_add=True)), + ("created", models.DateTimeField(default=django.utils.timezone.now)), ("modified", models.DateTimeField(auto_now=True)), - ("name", models.CharField(max_length=50)), + ("name", models.CharField(max_length=50, unique=True)), ], - options={"abstract": False}, + options={"verbose_name": "Category", "verbose_name_plural": "Categories"}, ), migrations.CreateModel( name="Post", @@ -36,27 +36,23 @@ class Migration(migrations.Migration): auto_created=True, primary_key=True, serialize=False, verbose_name="ID" ), ), - ("created", models.DateTimeField(auto_now_add=True)), + ("created", models.DateTimeField(default=django.utils.timezone.now)), ("modified", models.DateTimeField(auto_now=True)), - ("title", models.CharField(max_length=200)), - ("body", models.TextField()), - ("source", models.CharField(max_length=200)), - ("publication_date", models.DateTimeField()), - ("url", models.URLField()), - ("remote_identifier", models.CharField(max_length=500)), + ("title", models.CharField(blank=True, max_length=200, null=True)), + ("body", models.TextField(blank=True, null=True)), + ("author", models.CharField(blank=True, max_length=200, null=True)), + ("publication_date", models.DateTimeField(blank=True, null=True)), + ("url", models.URLField(blank=True, max_length=1024, null=True)), ( - "category", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.PROTECT, - to="posts.Category", - ), + "remote_identifier", + models.CharField(blank=True, editable=False, max_length=500, null=True), ), ( "rule", models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to="collection.CollectionRule" + editable=False, + on_delete=django.db.models.deletion.CASCADE, + to="collection.CollectionRule", ), ), ], diff --git a/src/newsreader/news/core/migrations/0002_category_user.py b/src/newsreader/news/core/migrations/0002_category_user.py new file mode 100644 index 0000000..d2fa17f --- /dev/null +++ b/src/newsreader/news/core/migrations/0002_category_user.py @@ -0,0 +1,21 @@ +# Generated by Django 2.2 on 2019-07-07 17:08 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("core", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="category", + name="user", + field=models.ForeignKey(default=None, on_delete="Owner", to=settings.AUTH_USER_MODEL), + preserve_default=False, + ) + ] diff --git a/src/newsreader/news/core/migrations/__init__.py b/src/newsreader/news/core/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/newsreader/news/posts/models.py b/src/newsreader/news/core/models.py similarity index 78% rename from src/newsreader/news/posts/models.py rename to src/newsreader/news/core/models.py index fd2c6c1..704e752 100644 --- a/src/newsreader/news/posts/models.py +++ b/src/newsreader/news/core/models.py @@ -8,14 +8,12 @@ from newsreader.news.collection.models import CollectionRule class Post(TimeStampedModel): title = models.CharField(max_length=200, blank=True, null=True) body = models.TextField(blank=True, null=True) - author = models.CharField(max_length=100, blank=True, null=True) + author = models.CharField(max_length=200, blank=True, null=True) publication_date = models.DateTimeField(blank=True, null=True) - url = models.URLField(blank=True, null=True) + url = models.URLField(max_length=1024, blank=True, null=True) - rule = models.ForeignKey(CollectionRule, on_delete=models.CASCADE) - remote_identifier = models.CharField(max_length=500, blank=True, null=True) - - category = models.ForeignKey("Category", blank=True, null=True, on_delete=models.PROTECT) + rule = models.ForeignKey(CollectionRule, on_delete=models.CASCADE, editable=False) + remote_identifier = models.CharField(max_length=500, blank=True, null=True, editable=False) def __str__(self): return "Post-{}".format(self.pk) @@ -23,6 +21,7 @@ class Post(TimeStampedModel): class Category(TimeStampedModel): name = models.CharField(max_length=50, unique=True) + user = models.ForeignKey("auth.User", _("Owner")) class Meta: verbose_name = _("Category") diff --git a/src/newsreader/news/core/pagination.py b/src/newsreader/news/core/pagination.py new file mode 100644 index 0000000..357ff71 --- /dev/null +++ b/src/newsreader/news/core/pagination.py @@ -0,0 +1,31 @@ +from rest_framework import serializers + +from newsreader.news.posts.models import Category, Post + + +class CategorySerializer(serializers.ModelSerializer): + rules = serializers.SerializerMethodField() + + def get_rules(self, instance): + rules = instance.collectionrule_set.order_by("-modified", "-created") + serializer = CollectionRuleSerializer(rules, many=True) + return serializer.data + + class Meta: + model = Category + fields = ("id", "name", "rules") + + +class PostSerializer(serializers.ModelSerializer): + class Meta: + model = Post + fields = ( + "id", + "title", + "body", + "author", + "publication_date", + "url", + "rule", + "remote_identifier", + ) diff --git a/src/newsreader/news/core/serializers.py b/src/newsreader/news/core/serializers.py new file mode 100644 index 0000000..ca3e93c --- /dev/null +++ b/src/newsreader/news/core/serializers.py @@ -0,0 +1,39 @@ +from rest_framework import serializers + +from newsreader.news import collection +from newsreader.news.core.models import Category, Post + + +class PostSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = Post + fields = ( + "id", + "remote_identifier", + "title", + "body", + "author", + "publication_date", + "url", + "rule", + ) + extra_kwargs = {"rule": {"view_name": "api:rules-detail"}} + + +class CategorySerializer(serializers.HyperlinkedModelSerializer): + rules = serializers.SerializerMethodField() + user = serializers.HiddenField(default=serializers.CurrentUserDefault()) + + def get_rules(self, instance): + request = self.context.get("request") + rules = instance.collectionrule_set.order_by("-modified", "-created") + + serializer = collection.serializers.CollectionRuleSerializer( + rules, context={"request": request}, many=True + ) + return serializer.data + + class Meta: + model = Category + fields = ("id", "name", "rules", "user") + extra_kwargs = {"rules": {"view_name": "api:rules-detail"}} 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..d65fcb7 --- /dev/null +++ b/src/newsreader/news/core/tests/endpoints/category/detail/tests.py @@ -0,0 +1,168 @@ +import json + +from django.test import Client, TestCase +from django.urls import reverse + +from newsreader.auth.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.maxDiff = None + + self.client = Client() + self.user = UserFactory(is_staff=True, password="test") + + def test_simple(self): + category = CategoryFactory(user=self.user) + + self.client.force_login(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) + self.assertTrue("rules" in data) + + def test_not_known(self): + self.client.force_login(self.user) + 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) + + self.client.force_login(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) + + self.client.force_login(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) + + self.client.force_login(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) + + self.client.force_login(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) + + self.client.force_login(self.user) + response = self.client.delete(reverse("api:categories-detail", args=[category.pk])) + + self.assertEquals(response.status_code, 204) + + def test_rules(self): + category = CategoryFactory(user=self.user) + rules = CollectionRuleFactory.create_batch(size=5, category=category, user=self.user) + + self.client.force_login(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["rules"][0]) + self.assertTrue("name" in data["rules"][0]) + self.assertTrue("url" in data["rules"][0]) + + def test_rules_with_posts(self): + category = CategoryFactory(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 + ) + } + + self.client.force_login(self.user) + response = self.client.get(reverse("api:categories-detail", args=[category.pk])) + data = response.json() + + self.assertEquals(response.status_code, 200) + + self.assertEquals(len(data["rules"][0]["posts"]), 5) + + def test_rules_with_posts_ordered(self): + category = CategoryFactory(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 + ) + } + + self.client.force_login(self.user) + response = self.client.get(reverse("api:categories-detail", args=[category.pk])) + data = response.json() + + self.assertEquals(response.status_code, 200) + + posts = data["rules"][0]["posts"] + + for count, post in enumerate(posts): + if count < 1: + continue + + self.assertTrue(post["publication_date"] < posts[count - 1]["publication_date"]) + + def test_category_with_unauthenticated_user(self): + 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) + + self.client.force_login(self.user) + response = self.client.get(reverse("api:categories-detail", args=[category.pk])) + + self.assertEquals(response.status_code, 403) 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..050a430 --- /dev/null +++ b/src/newsreader/news/core/tests/endpoints/category/list/tests.py @@ -0,0 +1,186 @@ +import json + +from datetime import date, datetime, time + +from django.test import Client, TestCase +from django.urls import reverse + +import pytz + +from newsreader.auth.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.maxDiff = None + + self.client = Client() + self.user = UserFactory(is_staff=True, password="test") + + def test_simple(self): + CategoryFactory.create_batch(size=3, user=self.user) + + self.client.force_login(self.user) + response = self.client.get(reverse("api:categories-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): + 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, + ), + ] + + self.client.force_login(self.user) + response = self.client.get(reverse("api:categories-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"], categories[1].pk) + self.assertEquals(data["results"][1]["id"], categories[2].pk) + self.assertEquals(data["results"][2]["id"], categories[0].pk) + + def test_empty(self): + self.client.force_login(self.user) + response = self.client.get(reverse("api:categories-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): + data = {"name": "Tech"} + + self.client.force_login(self.user) + 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, 201) + self.assertEquals(response_data["name"], "Tech") + + def test_patch(self): + self.client.force_login(self.user) + 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): + self.client.force_login(self.user) + 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): + self.client.force_login(self.user) + 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_rules(self): + categories = { + category.pk: CollectionRuleFactory.create_batch( + size=5, category=category, user=self.user + ) + for category in CategoryFactory.create_batch(size=5, user=self.user) + } + + self.client.force_login(self.user) + response = self.client.get(reverse("api:categories-list")) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertTrue("results" in data) + self.assertTrue("count" in data) + self.assertEquals(data["count"], 5) + + self.assertEquals(len(data["results"]), 5) + + self.assertEquals(len(data["results"][0]["rules"]), 5) + + self.assertTrue("id" in data["results"][0]["rules"][0]) + self.assertTrue("name" in data["results"][0]["rules"][0]) + self.assertTrue("url" in data["results"][0]["rules"][0]) + self.assertTrue("posts" in data["results"][0]["rules"][0]) + + def test_rules_with_posts(self): + categories = { + category.pk: CollectionRuleFactory.create_batch( + size=5, category=category, user=self.user + ) + for category in CategoryFactory.create_batch(size=5, user=self.user) + } + + for category in categories: + for rule in categories[category]: + PostFactory.create_batch(size=5, rule=rule) + + self.client.force_login(self.user) + response = self.client.get(reverse("api:categories-list")) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertTrue("results" in data) + self.assertTrue("count" in data) + self.assertEquals(data["count"], 5) + + self.assertEquals(len(data["results"]), 5) + + self.assertEquals(len(data["results"][0]["rules"]), 5) + self.assertEquals(len(data["results"][0]["rules"][0]["posts"]), 5) + + def test_categories_with_unauthenticated_user(self): + 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) + + self.client.force_login(self.user) + response = self.client.get(reverse("api:categories-list")) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertEquals(len(data["results"]), 0) + self.assertEquals(data["count"], 0) 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..e963035 --- /dev/null +++ b/src/newsreader/news/core/tests/endpoints/post/detail/tests.py @@ -0,0 +1,175 @@ +import json + +from django.test import Client, TestCase +from django.urls import reverse + +from newsreader.auth.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.maxDiff = None + self.client = Client() + + self.client = Client() + self.user = UserFactory(is_staff=True, password="test") + + def test_simple(self): + rule = CollectionRuleFactory(user=self.user, category=CategoryFactory(user=self.user)) + post = PostFactory(rule=rule) + + self.client.force_login(self.user) + 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("publication_date" in data) + self.assertTrue("url" in data) + self.assertTrue("rule" in data) + self.assertTrue("remote_identifier" in data) + + def test_not_known(self): + self.client.force_login(self.user) + 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) + + self.client.force_login(self.user) + 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) + + self.client.force_login(self.user) + 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) + + self.client.force_login(self.user) + 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) + + self.client.force_login(self.user) + 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() + rule_url = data["rule"] + + self.assertEquals(response.status_code, 200) + + self.assertTrue(rule_url.endswith(reverse("api:rules-detail", args=[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) + + self.client.force_login(self.user) + 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) + + self.client.force_login(self.user) + 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): + 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): + 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) + + self.client.force_login(self.user) + 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) + + self.client.force_login(self.user) + 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) + + self.client.force_login(self.user) + response = self.client.get(reverse("api:posts-detail", args=[post.pk])) + + self.assertEquals(response.status_code, 403) 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..ed1c29e --- /dev/null +++ b/src/newsreader/news/core/tests/endpoints/post/list/tests.py @@ -0,0 +1,208 @@ +from datetime import date, datetime, time + +from django.test import Client, TestCase +from django.urls import reverse + +import pytz + +from newsreader.auth.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.maxDiff = None + + self.client = Client() + self.user = UserFactory(is_staff=True, password="test") + + def test_simple(self): + rule = CollectionRuleFactory(user=self.user, category=CategoryFactory(user=self.user)) + PostFactory.create_batch(size=3, rule=rule) + + self.client.force_login(self.user) + 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 + ), + ), + ] + + self.client.force_login(self.user) + 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 + + self.client.force_login(self.user) + 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): + self.client.force_login(self.user) + 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): + self.client.force_login(self.user) + 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): + self.client.force_login(self.user) + 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): + self.client.force_login(self.user) + 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): + self.client.force_login(self.user) + 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): + 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): + 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) + + self.client.force_login(self.user) + 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) + ) + + self.client.force_login(self.user) + 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) + + self.client.force_login(self.user) + 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): + UserFactory() + + rule = CollectionRuleFactory(user=self.user, category=None) + PostFactory.create_batch(size=3, rule=rule) + + self.client.force_login(self.user) + 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) diff --git a/src/newsreader/news/posts/tests/factories.py b/src/newsreader/news/core/tests/factories.py similarity index 68% rename from src/newsreader/news/posts/tests/factories.py rename to src/newsreader/news/core/tests/factories.py index a961b4f..c4c6d4c 100644 --- a/src/newsreader/news/posts/tests/factories.py +++ b/src/newsreader/news/core/tests/factories.py @@ -1,21 +1,19 @@ import factory import pytz -from newsreader.news.collection.tests.factories import CollectionRuleFactory -from newsreader.news.posts.models import Category, Post +from newsreader.auth.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 - name = factory.Sequence(lambda n: "Category-{}".format(n)) - class PostFactory(factory.django.DjangoModelFactory): - class Meta: - model = Post - title = factory.Faker("sentence") body = factory.Faker("paragraph") author = factory.Faker("name") @@ -23,6 +21,7 @@ class PostFactory(factory.django.DjangoModelFactory): url = factory.Faker("url") remote_identifier = factory.Faker("url") - rule = factory.SubFactory(CollectionRuleFactory) + rule = factory.SubFactory("newsreader.news.collection.tests.factories.CollectionRuleFactory") - category = factory.SubFactory(CategoryFactory) + class Meta: + model = Post diff --git a/src/newsreader/news/core/urls.py b/src/newsreader/news/core/urls.py new file mode 100644 index 0000000..c207440 --- /dev/null +++ b/src/newsreader/news/core/urls.py @@ -0,0 +1,16 @@ +from django.urls import path + +from newsreader.news.core.views import ( + DetailCategoryAPIView, + DetailPostAPIView, + ListCategoryAPIView, + ListPostAPIView, +) + + +endpoints = [ + path("posts/", ListPostAPIView.as_view(), name="posts-list"), + path("posts//", DetailPostAPIView.as_view(), name="posts-detail"), + path("categories/", ListCategoryAPIView.as_view(), name="categories-list"), + path("categories//", DetailCategoryAPIView.as_view(), name="categories-detail"), +] diff --git a/src/newsreader/news/core/views.py b/src/newsreader/news/core/views.py new file mode 100644 index 0000000..5631796 --- /dev/null +++ b/src/newsreader/news/core/views.py @@ -0,0 +1,52 @@ +from django.db.models import Q + +from rest_framework.generics import ( + ListAPIView, + ListCreateAPIView, + RetrieveUpdateAPIView, + RetrieveUpdateDestroyAPIView, +) +from rest_framework.permissions import IsAuthenticated + +from newsreader.auth.permissions import IsPostOwner +from newsreader.core.pagination import ( + LargeResultSetPagination, + ResultSetPagination, +) +from newsreader.news.core.models import Category, Post +from newsreader.news.core.serializers import CategorySerializer, PostSerializer + + +class ListPostAPIView(ListAPIView): + queryset = Post.objects.all() + serializer_class = PostSerializer + pagination_class = LargeResultSetPagination + permission_classes = (IsAuthenticated, IsPostOwner) + + def get_queryset(self): + user = self.request.user + initial_queryset = self.queryset.filter(rule__user=user) + return initial_queryset.filter( + Q(rule__category=None) | Q(rule__category__user=user) + ).order_by("rule", "-publication_date", "-created") + + +class DetailPostAPIView(RetrieveUpdateAPIView): + queryset = Post.objects.all() + serializer_class = PostSerializer + permission_classes = (IsAuthenticated, IsPostOwner) + + +class ListCategoryAPIView(ListCreateAPIView): + queryset = Category.objects.all() + serializer_class = CategorySerializer + pagination_class = ResultSetPagination + + def get_queryset(self): + user = self.request.user + return self.queryset.filter(user=user).order_by("-created", "-modified") + + +class DetailCategoryAPIView(RetrieveUpdateDestroyAPIView): + queryset = Category.objects.all() + serializer_class = CategorySerializer diff --git a/src/newsreader/news/posts/apps.py b/src/newsreader/news/posts/apps.py deleted file mode 100644 index d39b3e1..0000000 --- a/src/newsreader/news/posts/apps.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.apps import AppConfig - - -class PostsConfig(AppConfig): - name = "posts" diff --git a/src/newsreader/news/posts/migrations/0002_auto_20190520_2206.py b/src/newsreader/news/posts/migrations/0002_auto_20190520_2206.py deleted file mode 100644 index 4365972..0000000 --- a/src/newsreader/news/posts/migrations/0002_auto_20190520_2206.py +++ /dev/null @@ -1,15 +0,0 @@ -# Generated by Django 2.2 on 2019-05-20 20:06 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [("posts", "0001_initial")] - - operations = [ - migrations.AlterModelOptions( - name="category", - options={"verbose_name": "Category", "verbose_name_plural": "Categories"}, - ) - ] diff --git a/src/newsreader/news/posts/migrations/0003_auto_20190520_2031.py b/src/newsreader/news/posts/migrations/0003_auto_20190520_2031.py deleted file mode 100644 index 0905440..0000000 --- a/src/newsreader/news/posts/migrations/0003_auto_20190520_2031.py +++ /dev/null @@ -1,14 +0,0 @@ -# Generated by Django 2.2 on 2019-05-20 20:31 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [("posts", "0002_auto_20190520_2206")] - - operations = [ - migrations.AlterField( - model_name="category", name="name", field=models.CharField(max_length=50, unique=True) - ) - ] diff --git a/src/newsreader/news/posts/migrations/0004_auto_20190521_1941.py b/src/newsreader/news/posts/migrations/0004_auto_20190521_1941.py deleted file mode 100644 index 4a0ceb2..0000000 --- a/src/newsreader/news/posts/migrations/0004_auto_20190521_1941.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 2.2 on 2019-05-21 19:41 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [("posts", "0003_auto_20190520_2031")] - - operations = [ - migrations.RemoveField(model_name="post", name="source"), - migrations.AddField( - model_name="post", - name="author", - field=models.CharField(blank=True, max_length=100, null=True), - ), - ] diff --git a/src/newsreader/news/posts/migrations/0005_auto_20190608_1054.py b/src/newsreader/news/posts/migrations/0005_auto_20190608_1054.py deleted file mode 100644 index 9a5e03a..0000000 --- a/src/newsreader/news/posts/migrations/0005_auto_20190608_1054.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 2.2 on 2019-06-08 10:54 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [("posts", "0004_auto_20190521_1941")] - - operations = [ - migrations.AlterField(model_name="post", name="body", field=models.TextField(blank=True)), - migrations.AlterField( - model_name="post", - name="remote_identifier", - field=models.CharField(blank=True, max_length=500, null=True), - ), - ] diff --git a/src/newsreader/news/posts/migrations/0006_auto_20190608_1520.py b/src/newsreader/news/posts/migrations/0006_auto_20190608_1520.py deleted file mode 100644 index 7a5f7ec..0000000 --- a/src/newsreader/news/posts/migrations/0006_auto_20190608_1520.py +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by Django 2.2 on 2019-06-08 15:20 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [("posts", "0005_auto_20190608_1054")] - - operations = [ - migrations.AlterField( - model_name="post", name="body", field=models.TextField(blank=True, null=True) - ), - migrations.AlterField( - model_name="post", - name="publication_date", - field=models.DateTimeField(blank=True, null=True), - ), - migrations.AlterField( - model_name="post", - name="title", - field=models.CharField(blank=True, max_length=200, null=True), - ), - migrations.AlterField( - model_name="post", name="url", field=models.URLField(blank=True, null=True) - ), - ] diff --git a/src/newsreader/news/posts/views.py b/src/newsreader/news/posts/views.py deleted file mode 100644 index dc1ba72..0000000 --- a/src/newsreader/news/posts/views.py +++ /dev/null @@ -1,4 +0,0 @@ -from django.shortcuts import render - - -# Create your views here. diff --git a/src/newsreader/urls.py b/src/newsreader/urls.py index ed82d43..e8a3b87 100644 --- a/src/newsreader/urls.py +++ b/src/newsreader/urls.py @@ -1,5 +1,20 @@ +from django.conf import settings from django.contrib import admin from django.urls import include, path +from newsreader.news.collection.urls import endpoints as collection_endpoints +from newsreader.news.core.urls import endpoints as core_endpoints -urlpatterns = [path("admin/", admin.site.urls)] + +endpoints = collection_endpoints + core_endpoints + +urlpatterns = [ + path("admin/", admin.site.urls, name="admin"), + path("api/", include((endpoints, "api")), name="api"), +] + + +if settings.DEBUG: + import debug_toolbar + + urlpatterns = [path("debug/", include(debug_toolbar.urls))] + urlpatterns From a798b9858c434261553846a286993eb3ed7ab879 Mon Sep 17 00:00:00 2001 From: Sonny Date: Tue, 9 Jul 2019 19:17:39 +0200 Subject: [PATCH 010/364] Set the default content type --- src/newsreader/conf/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/newsreader/conf/base.py b/src/newsreader/conf/base.py index 593c42c..bba7a37 100644 --- a/src/newsreader/conf/base.py +++ b/src/newsreader/conf/base.py @@ -115,4 +115,5 @@ REST_FRAMEWORK = { "rest_framework.permissions.IsAuthenticated", "newsreader.auth.permissions.IsOwner", ), + "DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",), } From 88cf5f9bd45c9c499a530cdc8f2152337f3801c3 Mon Sep 17 00:00:00 2001 From: Sonny Date: Tue, 9 Jul 2019 20:33:05 +0200 Subject: [PATCH 011/364] Add gitlab ci settings --- .gitlab-ci.yml | 25 +++++++++++++++++++ .isort.cfg | 2 +- requirements/gitlab.txt | 6 +++++ requirements/testing.txt | 4 +++ src/newsreader/conf/dev.py | 2 +- src/newsreader/conf/gitlab.py | 15 +++++++++++ .../0002_collectionrule_category.py | 3 ++- .../news/collection/response_handler.py | 6 ----- .../collection/tests/feed/collector/tests.py | 7 +----- .../news/collection/tests/utils/tests.py | 7 +----- src/newsreader/news/collection/urls.py | 5 +--- src/newsreader/news/collection/views.py | 5 +--- .../news/core/migrations/0001_initial.py | 3 ++- src/newsreader/news/core/views.py | 5 +--- 14 files changed, 61 insertions(+), 34 deletions(-) create mode 100644 .gitlab-ci.yml create mode 100644 requirements/gitlab.txt create mode 100644 requirements/testing.txt create mode 100644 src/newsreader/conf/gitlab.py diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..72b07ed --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,25 @@ +services: + - postgres:9.6 + +variables: + POSTGRES_DB: newsreader + POSTGRES_USER: newsreader + +python tests: + image: python:3.7.4-slim-stretch + stage: test + variables: + PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" + cache: + key: "$CI_JOB_NAME-$CI_COMMIT_REF_SLUG" + paths: + - .cache/pip + - env/ + before_script: + - python3 -m venv env + - source env/bin/activate + - pip install -r requirements/gitlab.txt + script: + - python src/manage.py test newsreader --settings=newsreader.conf.gitlab + - isort -rc src/ --check-only + - black -l 100 --check src/ diff --git a/.isort.cfg b/.isort.cfg index 2b81405..fa6fb9b 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -1,6 +1,6 @@ [settings] include_trailing_comma = true -line_length = 80 +line_length = 100 multi_line_output = 3 skip = env/, venv/ default_section = THIRDPARTY diff --git a/requirements/gitlab.txt b/requirements/gitlab.txt new file mode 100644 index 0000000..a0b3eca --- /dev/null +++ b/requirements/gitlab.txt @@ -0,0 +1,6 @@ +-r base.txt + +factory-boy==2.12.0 +freezegun==0.3.12 +black==19.3b0 +isort==4.3.20 diff --git a/requirements/testing.txt b/requirements/testing.txt new file mode 100644 index 0000000..0a685e3 --- /dev/null +++ b/requirements/testing.txt @@ -0,0 +1,4 @@ +-r base.txt + +factory-boy==2.12.0 +freezegun==0.3.12 diff --git a/src/newsreader/conf/dev.py b/src/newsreader/conf/dev.py index 7453d52..2b0e050 100644 --- a/src/newsreader/conf/dev.py +++ b/src/newsreader/conf/dev.py @@ -10,6 +10,6 @@ EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" INSTALLED_APPS += ["debug_toolbar", "django_extensions"] try: - pass + from .local import * # noqa except ImportError: pass diff --git a/src/newsreader/conf/gitlab.py b/src/newsreader/conf/gitlab.py new file mode 100644 index 0000000..a50781e --- /dev/null +++ b/src/newsreader/conf/gitlab.py @@ -0,0 +1,15 @@ +from .base import * # noqa + + +DEBUG = True + +EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.postgresql_psycopg2", + "NAME": "newsreader", + "USER": "newsreader", + "HOST": "postgres", + } +} diff --git a/src/newsreader/news/collection/migrations/0002_collectionrule_category.py b/src/newsreader/news/collection/migrations/0002_collectionrule_category.py index 2cb1ee4..b9449a7 100644 --- a/src/newsreader/news/collection/migrations/0002_collectionrule_category.py +++ b/src/newsreader/news/collection/migrations/0002_collectionrule_category.py @@ -1,8 +1,9 @@ # Generated by Django 2.2 on 2019-07-05 20:59 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models + class Migration(migrations.Migration): diff --git a/src/newsreader/news/collection/response_handler.py b/src/newsreader/news/collection/response_handler.py index d9598c4..275bc27 100644 --- a/src/newsreader/news/collection/response_handler.py +++ b/src/newsreader/news/collection/response_handler.py @@ -1,12 +1,6 @@ from typing import ContextManager from requests.exceptions import ConnectionError as RequestConnectionError -from requests.exceptions import ( - HTTPError, - RequestException, - SSLError, - TooManyRedirects, -) from newsreader.news.collection.exceptions import ( StreamConnectionError, diff --git a/src/newsreader/news/collection/tests/feed/collector/tests.py b/src/newsreader/news/collection/tests/feed/collector/tests.py index 6978ee9..cb61242 100644 --- a/src/newsreader/news/collection/tests/feed/collector/tests.py +++ b/src/newsreader/news/collection/tests/feed/collector/tests.py @@ -23,12 +23,7 @@ from newsreader.news.collection.utils import build_publication_date from newsreader.news.core.models import Post from newsreader.news.core.tests.factories import PostFactory -from .mocks import ( - duplicate_mock, - empty_mock, - multiple_mock, - multiple_update_mock, -) +from .mocks import duplicate_mock, empty_mock, multiple_mock, multiple_update_mock class FeedCollectorTestCase(TestCase): diff --git a/src/newsreader/news/collection/tests/utils/tests.py b/src/newsreader/news/collection/tests/utils/tests.py index 0362c71..95c5dd2 100644 --- a/src/newsreader/news/collection/tests/utils/tests.py +++ b/src/newsreader/news/collection/tests/utils/tests.py @@ -3,12 +3,7 @@ from unittest.mock import MagicMock, patch from django.test import TestCase from requests.exceptions import ConnectionError as RequestConnectionError -from requests.exceptions import ( - HTTPError, - RequestException, - SSLError, - TooManyRedirects, -) +from requests.exceptions import HTTPError, RequestException, SSLError, TooManyRedirects from newsreader.news.collection.exceptions import ( StreamConnectionError, diff --git a/src/newsreader/news/collection/urls.py b/src/newsreader/news/collection/urls.py index 4b59a09..de289d1 100644 --- a/src/newsreader/news/collection/urls.py +++ b/src/newsreader/news/collection/urls.py @@ -1,9 +1,6 @@ from django.urls import path -from newsreader.news.collection.views import ( - CollectionRuleAPIListView, - CollectionRuleDetailView, -) +from newsreader.news.collection.views import CollectionRuleAPIListView, CollectionRuleDetailView endpoints = [ diff --git a/src/newsreader/news/collection/views.py b/src/newsreader/news/collection/views.py index 3de472b..2c08185 100644 --- a/src/newsreader/news/collection/views.py +++ b/src/newsreader/news/collection/views.py @@ -1,7 +1,4 @@ -from rest_framework.generics import ( - ListCreateAPIView, - RetrieveUpdateDestroyAPIView, -) +from rest_framework.generics import ListCreateAPIView, RetrieveUpdateDestroyAPIView from newsreader.core.pagination import ResultSetPagination from newsreader.news.collection.models import CollectionRule diff --git a/src/newsreader/news/core/migrations/0001_initial.py b/src/newsreader/news/core/migrations/0001_initial.py index 5e0ffca..9d9ebc9 100644 --- a/src/newsreader/news/core/migrations/0001_initial.py +++ b/src/newsreader/news/core/migrations/0001_initial.py @@ -1,9 +1,10 @@ # Generated by Django 2.2 on 2019-07-05 20:59 -from django.db import migrations, models import django.db.models.deletion import django.utils.timezone +from django.db import migrations, models + class Migration(migrations.Migration): diff --git a/src/newsreader/news/core/views.py b/src/newsreader/news/core/views.py index 5631796..a0559be 100644 --- a/src/newsreader/news/core/views.py +++ b/src/newsreader/news/core/views.py @@ -9,10 +9,7 @@ from rest_framework.generics import ( from rest_framework.permissions import IsAuthenticated from newsreader.auth.permissions import IsPostOwner -from newsreader.core.pagination import ( - LargeResultSetPagination, - ResultSetPagination, -) +from newsreader.core.pagination import LargeResultSetPagination, ResultSetPagination from newsreader.news.core.models import Category, Post from newsreader.news.core.serializers import CategorySerializer, PostSerializer From a95db91726e79a96b3fca927a12479509ff9b532 Mon Sep 17 00:00:00 2001 From: Sonny Date: Wed, 10 Jul 2019 21:55:28 +0200 Subject: [PATCH 012/364] Add truncate tests for title and author fields --- src/newsreader/news/collection/feed.py | 32 ++++- .../collection/tests/feed/builder/mocks.py | 129 ++++++++++++++++++ .../collection/tests/feed/builder/tests.py | 28 ++++ .../migrations/0003_auto_20190710_2022.py | 16 +++ src/newsreader/news/core/models.py | 2 +- 5 files changed, 200 insertions(+), 7 deletions(-) create mode 100644 src/newsreader/news/core/migrations/0003_auto_20190710_2022.py diff --git a/src/newsreader/news/collection/feed.py b/src/newsreader/news/collection/feed.py index a7ab2fc..c7e9b6d 100644 --- a/src/newsreader/news/collection/feed.py +++ b/src/newsreader/news/collection/feed.py @@ -1,6 +1,8 @@ from concurrent.futures import ThreadPoolExecutor, as_completed from typing import ContextManager, Dict, Generator, List, Optional, Tuple +from django.db.models.fields import CharField, TextField +from django.template.defaultfilters import truncatechars from django.utils import timezone import bleach @@ -63,16 +65,18 @@ class FeedBuilder(Builder): for entry in entries: data = {"rule_id": rule.pk} - for field, value in field_mapping.items(): + for field, model_field in field_mapping.items(): if field in entry: + value = self.truncate_text(model_field, entry[field]) + if field == "published_parsed": - created, aware_datetime = build_publication_date(entry[field], tz) - data[value] = aware_datetime if created else None + created, aware_datetime = build_publication_date(value, tz) + data[model_field] = aware_datetime if created else None elif field == "summary": - summary = self.sanitize_summary(entry[field]) - data[value] = summary + summary = self.sanitize_summary(value) + data[model_field] = summary else: - data[value] = entry[field] + data[model_field] = value yield Post(**data) @@ -82,6 +86,22 @@ class FeedBuilder(Builder): return bleach.clean(summary, tags=tags, attributes=attrs) if summary else None + 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: + print(f"Truncated {field_name}") + return truncatechars(value, max_length) + + return value + def save(self) -> None: for post in self.instances: post.save() diff --git a/src/newsreader/news/collection/tests/feed/builder/mocks.py b/src/newsreader/news/collection/tests/feed/builder/mocks.py index a486626..9003f86 100644 --- a/src/newsreader/news/collection/tests/feed/builder/mocks.py +++ b/src/newsreader/news/collection/tests/feed/builder/mocks.py @@ -890,3 +890,132 @@ mock_with_html = { "status": 200, "version": "rss20", } + +mock_with_long_author = { + "bozo": 0, + "encoding": "utf-8", + "entries": [ + { + "author": "A. Author but this author name is way to long for an actual surname.", + "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", + }, + } + ], + "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/", + }, + "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", +} + +mock_with_long_title = { + "bozo": 0, + "encoding": "utf-8", + "entries": [ + { + "author": "A. Author", + "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" + "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", + "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", + }, + } + ], + "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/", + }, + "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", +} diff --git a/src/newsreader/news/collection/tests/feed/builder/tests.py b/src/newsreader/news/collection/tests/feed/builder/tests.py index 94e84ea..33aae7f 100644 --- a/src/newsreader/news/collection/tests/feed/builder/tests.py +++ b/src/newsreader/news/collection/tests/feed/builder/tests.py @@ -265,3 +265,31 @@ class FeedBuilderTestCase(TestCase): self.assertTrue("" not in post.body) self.assertTrue('' in post.body) self.assertTrue("

" in post.body) + + def test_long_author_text_is_truncated(self): + builder = FeedBuilder + rule = CollectionRuleFactory() + mock_stream = MagicMock(rule=rule) + + with builder((mock_with_long_author, mock_stream)) as builder: + builder.save() + + post = Post.objects.get() + + self.assertEquals(Post.objects.count(), 1) + + self.assertEquals(len(post.author), 40) + + def test_long_title_text_is_truncated(self): + builder = FeedBuilder + rule = CollectionRuleFactory() + mock_stream = MagicMock(rule=rule) + + with builder((mock_with_long_title, mock_stream)) as builder: + builder.save() + + post = Post.objects.get() + + self.assertEquals(Post.objects.count(), 1) + + self.assertEquals(len(post.title), 200) diff --git a/src/newsreader/news/core/migrations/0003_auto_20190710_2022.py b/src/newsreader/news/core/migrations/0003_auto_20190710_2022.py new file mode 100644 index 0000000..3c7fe84 --- /dev/null +++ b/src/newsreader/news/core/migrations/0003_auto_20190710_2022.py @@ -0,0 +1,16 @@ +# Generated by Django 2.2 on 2019-07-10 18:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [("core", "0002_category_user")] + + operations = [ + migrations.AlterField( + model_name="post", + name="author", + field=models.CharField(blank=True, max_length=40, null=True), + ) + ] diff --git a/src/newsreader/news/core/models.py b/src/newsreader/news/core/models.py index 704e752..c7d2638 100644 --- a/src/newsreader/news/core/models.py +++ b/src/newsreader/news/core/models.py @@ -8,7 +8,7 @@ from newsreader.news.collection.models import CollectionRule class Post(TimeStampedModel): title = models.CharField(max_length=200, blank=True, null=True) body = models.TextField(blank=True, null=True) - author = models.CharField(max_length=200, blank=True, null=True) + author = models.CharField(max_length=40, blank=True, null=True) publication_date = models.DateTimeField(blank=True, null=True) url = models.URLField(max_length=1024, blank=True, null=True) From 8f314e0ca6637a2a1cfdb722a58067430a1558d5 Mon Sep 17 00:00:00 2001 From: Sonny Date: Wed, 10 Jul 2019 22:01:15 +0200 Subject: [PATCH 013/364] Remove leftover print statement --- src/newsreader/news/collection/feed.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/newsreader/news/collection/feed.py b/src/newsreader/news/collection/feed.py index c7e9b6d..ef80365 100644 --- a/src/newsreader/news/collection/feed.py +++ b/src/newsreader/news/collection/feed.py @@ -97,7 +97,6 @@ class FeedBuilder(Builder): return value if len(value) > max_length: - print(f"Truncated {field_name}") return truncatechars(value, max_length) return value From 1b774a72088314f3aacc5f6c14f5b95a0fa6b8fe Mon Sep 17 00:00:00 2001 From: Sonny Date: Fri, 12 Jul 2019 20:20:58 +0200 Subject: [PATCH 014/364] Construct datetime without converting to localtime --- src/newsreader/news/collection/feed.py | 2 +- .../news/collection/tests/feed/collector/tests.py | 6 +++--- src/newsreader/news/collection/utils.py | 10 +++++----- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/newsreader/news/collection/feed.py b/src/newsreader/news/collection/feed.py index ef80365..7237073 100644 --- a/src/newsreader/news/collection/feed.py +++ b/src/newsreader/news/collection/feed.py @@ -70,7 +70,7 @@ class FeedBuilder(Builder): value = self.truncate_text(model_field, entry[field]) if field == "published_parsed": - created, aware_datetime = build_publication_date(value, tz) + aware_datetime, created = build_publication_date(value, tz) data[model_field] = aware_datetime if created else None elif field == "summary": summary = self.sanitize_summary(value) diff --git a/src/newsreader/news/collection/tests/feed/collector/tests.py b/src/newsreader/news/collection/tests/feed/collector/tests.py index cb61242..6834b9c 100644 --- a/src/newsreader/news/collection/tests/feed/collector/tests.py +++ b/src/newsreader/news/collection/tests/feed/collector/tests.py @@ -139,7 +139,7 @@ class FeedCollectorTestCase(TestCase): self.mocked_parse.return_value = duplicate_mock rule = CollectionRuleFactory() - _, aware_datetime = build_publication_date( + aware_datetime, _ = build_publication_date( struct_time((2019, 5, 20, 16, 7, 37, 0, 140, 0)), pytz.utc ) @@ -152,7 +152,7 @@ class FeedCollectorTestCase(TestCase): rule=rule, ) - _, aware_datetime = build_publication_date( + aware_datetime, _ = build_publication_date( struct_time((2019, 5, 20, 12, 19, 19, 0, 140, 0)), pytz.utc ) @@ -165,7 +165,7 @@ class FeedCollectorTestCase(TestCase): rule=rule, ) - _, aware_datetime = build_publication_date( + aware_datetime, _ = build_publication_date( struct_time((2019, 5, 20, 16, 32, 38, 0, 140, 0)), pytz.utc ) diff --git a/src/newsreader/news/collection/utils.py b/src/newsreader/news/collection/utils.py index 0abc367..83eb708 100644 --- a/src/newsreader/news/collection/utils.py +++ b/src/newsreader/news/collection/utils.py @@ -1,5 +1,5 @@ from datetime import datetime, tzinfo -from time import mktime, struct_time +from time import struct_time from typing import Tuple from django.utils import timezone @@ -14,11 +14,11 @@ from newsreader.news.collection.response_handler import ResponseHandler def build_publication_date(dt: struct_time, tz: tzinfo) -> Tuple: try: - naive_datetime = datetime.fromtimestamp(mktime(dt)) + naive_datetime = datetime(*dt[:6]) published_parsed = timezone.make_aware(naive_datetime, timezone=tz) - except TypeError: - return False, None - return True, published_parsed + except (TypeError, ValueError): + return None, False + return published_parsed, True def fetch(url: str) -> Response: From a74ffae9a798ab37996bb767b52c62bab2aab56b Mon Sep 17 00:00:00 2001 From: Sonny Date: Sun, 14 Jul 2019 18:44:15 +0200 Subject: [PATCH 015/364] Celery integration --- .gitlab-ci.yml | 5 +- .isort.cfg | 2 +- requirements/base.txt | 2 + src/newsreader/__init__.py | 4 + src/newsreader/{auth => accounts}/__init__.py | 0 src/newsreader/{auth => accounts}/admin.py | 0 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 | 16 ++ .../migrations/0004_auto_20190714_1501.py | 22 +++ .../{auth => accounts}/migrations/__init__.py | 0 src/newsreader/accounts/models.py | 84 ++++++++++ .../{auth => accounts}/permissions.py | 0 .../{auth => accounts}/tests/__init__.py | 0 .../{auth => accounts}/tests/factories.py | 7 +- src/newsreader/{auth => accounts}/views.py | 0 src/newsreader/auth/apps.py | 5 - src/newsreader/auth/backends.py | 15 -- src/newsreader/auth/models.py | 0 src/newsreader/celery.py | 12 ++ src/newsreader/conf/base.py | 13 +- src/newsreader/news/collection/admin.py | 5 + src/newsreader/news/collection/base.py | 10 +- src/newsreader/news/collection/favicon.py | 4 +- src/newsreader/news/collection/feed.py | 6 +- .../collection/management/commands/collect.py | 3 - .../collection/migrations/0001_initial.py | 56 +++++-- .../migrations/0002_auto_20190714_1036.py | 37 +++++ .../migrations/0003_auto_20190714_1417.py | 19 +++ .../migrations/0003_collectionrule_user.py | 21 --- ...category.py => 0004_auto_20190714_1422.py} | 9 +- src/newsreader/news/collection/models.py | 3 +- src/newsreader/news/collection/serializers.py | 6 +- src/newsreader/news/collection/tasks.py | 19 +++ .../tests/endpoints/rules/detail/tests.py | 10 +- .../tests/endpoints/rules/list/tests.py | 10 +- .../news/collection/tests/factories.py | 2 +- .../collection/tests/favicon/builder/tests.py | 8 +- .../tests/favicon/collector/mocks.py | 8 +- .../tests/favicon/collector/tests.py | 8 +- .../collection/tests/feed/builder/mocks.py | 88 ++++++++-- .../collection/tests/feed/builder/tests.py | 58 +++++-- .../collection/tests/feed/client/mocks.py | 8 +- .../collection/tests/feed/collector/mocks.py | 32 +++- .../collection/tests/feed/collector/tests.py | 8 +- .../collection/tests/feed/stream/mocks.py | 8 +- src/newsreader/news/collection/tests/mocks.py | 8 +- src/newsreader/news/collection/tests/tests.py | 4 +- src/newsreader/news/collection/urls.py | 5 +- .../news/core/migrations/0001_initial.py | 56 ++++--- .../migrations/0002_auto_20190714_1425.py | 31 ++++ .../core/migrations/0002_category_user.py | 21 --- .../migrations/0003_auto_20190710_2022.py | 16 -- src/newsreader/news/core/models.py | 10 +- src/newsreader/news/core/pagination.py | 2 +- src/newsreader/news/core/serializers.py | 2 +- .../tests/endpoints/category/detail/tests.py | 14 +- .../tests/endpoints/category/list/tests.py | 6 +- .../core/tests/endpoints/post/detail/tests.py | 46 ++++-- .../core/tests/endpoints/post/list/tests.py | 18 ++- src/newsreader/news/core/tests/factories.py | 6 +- src/newsreader/news/core/urls.py | 4 +- src/newsreader/news/core/views.py | 2 +- 64 files changed, 829 insertions(+), 221 deletions(-) rename src/newsreader/{auth => accounts}/__init__.py (100%) rename src/newsreader/{auth => accounts}/admin.py (100%) 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 rename src/newsreader/{auth => accounts}/migrations/__init__.py (100%) create mode 100644 src/newsreader/accounts/models.py rename src/newsreader/{auth => accounts}/permissions.py (100%) rename src/newsreader/{auth => accounts}/tests/__init__.py (100%) rename src/newsreader/{auth => accounts}/tests/factories.py (67%) rename src/newsreader/{auth => accounts}/views.py (100%) delete mode 100644 src/newsreader/auth/apps.py delete mode 100644 src/newsreader/auth/backends.py delete mode 100644 src/newsreader/auth/models.py create mode 100644 src/newsreader/celery.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 delete mode 100644 src/newsreader/news/collection/migrations/0003_collectionrule_user.py rename src/newsreader/news/collection/migrations/{0002_collectionrule_category.py => 0004_auto_20190714_1422.py} (75%) create mode 100644 src/newsreader/news/collection/tasks.py create mode 100644 src/newsreader/news/core/migrations/0002_auto_20190714_1425.py delete mode 100644 src/newsreader/news/core/migrations/0002_category_user.py delete mode 100644 src/newsreader/news/core/migrations/0003_auto_20190710_2022.py diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 72b07ed..11fc181 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -10,6 +10,7 @@ python tests: stage: test variables: PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" + DJANGO_SETTINGS_MODULE: "newsreader.conf.gitlab" cache: key: "$CI_JOB_NAME-$CI_COMMIT_REF_SLUG" paths: @@ -20,6 +21,6 @@ python tests: - source env/bin/activate - pip install -r requirements/gitlab.txt script: - - python src/manage.py test newsreader --settings=newsreader.conf.gitlab + - python src/manage.py test newsreader - isort -rc src/ --check-only - - black -l 100 --check src/ + - black -l 90 --check src/ diff --git a/.isort.cfg b/.isort.cfg index fa6fb9b..8d6ccf3 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -1,6 +1,6 @@ [settings] include_trailing_comma = true -line_length = 100 +line_length = 90 multi_line_output = 3 skip = env/, venv/ default_section = THIRDPARTY diff --git a/requirements/base.txt b/requirements/base.txt index 05be61f..c9686ee 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,8 +1,10 @@ bleach==3.1.0 beautifulsoup4==4.7.1 +celery==4.3.0 certifi==2019.3.9 chardet==3.0.4 Django==2.2 +django-celery-beat==1.5.0 djangorestframework==3.9.4 lxml==4.3.4 feedparser==5.2.1 diff --git a/src/newsreader/__init__.py b/src/newsreader/__init__.py index e69de29..c08c1d6 100644 --- a/src/newsreader/__init__.py +++ b/src/newsreader/__init__.py @@ -0,0 +1,4 @@ +from .celery import app as celery_app + + +__all__ = ["celery_app"] diff --git a/src/newsreader/auth/__init__.py b/src/newsreader/accounts/__init__.py similarity index 100% rename from src/newsreader/auth/__init__.py rename to src/newsreader/accounts/__init__.py diff --git a/src/newsreader/auth/admin.py b/src/newsreader/accounts/admin.py similarity index 100% rename from src/newsreader/auth/admin.py rename to src/newsreader/accounts/admin.py 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..1f58ca8 --- /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="collection task", + to="django_celery_beat.PeriodicTask", + ), + ), + ( + "task_interval", + models.ForeignKey( + blank=True, + null=True, + on_delete="collection schedule", + 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..2cbbb0d --- /dev/null +++ b/src/newsreader/accounts/migrations/0003_auto_20190714_1417.py @@ -0,0 +1,16 @@ +# 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..ba2fc84 --- /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="collection task", + to="django_celery_beat.PeriodicTask", + ), + ) + ] diff --git a/src/newsreader/auth/migrations/__init__.py b/src/newsreader/accounts/migrations/__init__.py similarity index 100% rename from src/newsreader/auth/migrations/__init__.py rename to src/newsreader/accounts/migrations/__init__.py diff --git a/src/newsreader/accounts/models.py b/src/newsreader/accounts/models.py new file mode 100644 index 0000000..0c29801 --- /dev/null +++ b/src/newsreader/accounts/models.py @@ -0,0 +1,84 @@ +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, _("collection task"), null=True, blank=True, editable=False + ) + task_interval = models.ForeignKey( + IntervalSchedule, _("collection schedule"), null=True, blank=True + ) + + username = None + + objects = UserManager() + + USERNAME_FIELD = "email" + REQUIRED_FIELDS = [] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._original_interval = self.task_interval + + def save(self, *args, **kwargs): + if self._original_interval != self.task_interval: + + if self.task_interval and self.task: + self.task.interval = self.task_interval + self.task.enabled = True + self.task.save() + + elif self.task_interval and not self.task: + self.task = PeriodicTask.objects.create( + enabled=True, + interval=self.task_interval, + name=f"{self.email}-collection-task", + task="newsreader.news.collection.tasks", + args=json.dumps([self.pk]), + kwargs=None, + ) + + elif not self.task_interval and self.task: + self.task.enabled = False + self.task.save() + + super().save(*args, **kwargs) diff --git a/src/newsreader/auth/permissions.py b/src/newsreader/accounts/permissions.py similarity index 100% rename from src/newsreader/auth/permissions.py rename to src/newsreader/accounts/permissions.py diff --git a/src/newsreader/auth/tests/__init__.py b/src/newsreader/accounts/tests/__init__.py similarity index 100% rename from src/newsreader/auth/tests/__init__.py rename to src/newsreader/accounts/tests/__init__.py diff --git a/src/newsreader/auth/tests/factories.py b/src/newsreader/accounts/tests/factories.py similarity index 67% rename from src/newsreader/auth/tests/factories.py rename to src/newsreader/accounts/tests/factories.py index 3975f62..d073c1c 100644 --- a/src/newsreader/auth/tests/factories.py +++ b/src/newsreader/accounts/tests/factories.py @@ -1,11 +1,10 @@ -from django.contrib.auth.models import User - import factory +from newsreader.accounts.models import User + class UserFactory(factory.django.DjangoModelFactory): - username = factory.Sequence(lambda n: f"user-{n}") - email = factory.LazyAttribute(lambda o: f"{o.username}@example.org") + email = factory.Faker("email") password = factory.Faker("password") is_staff = False diff --git a/src/newsreader/auth/views.py b/src/newsreader/accounts/views.py similarity index 100% rename from src/newsreader/auth/views.py rename to src/newsreader/accounts/views.py diff --git a/src/newsreader/auth/apps.py b/src/newsreader/auth/apps.py deleted file mode 100644 index c467d4e..0000000 --- a/src/newsreader/auth/apps.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.apps import AppConfig - - -class AuthConfig(AppConfig): - name = "auth" diff --git a/src/newsreader/auth/backends.py b/src/newsreader/auth/backends.py deleted file mode 100644 index 30a78b9..0000000 --- a/src/newsreader/auth/backends.py +++ /dev/null @@ -1,15 +0,0 @@ -from django.contrib.auth import get_user_model -from django.contrib.auth.backends import ModelBackend - - -class EmailBackend(ModelBackend): - def authenticate(self, request, username=None, password=None, **kwargs): - user_model_class = get_user_model() - - try: - user = user_model_class.objects.get(email=username) - except user_model_class.DoesNotExist: - return - - if user.check_password(password) and self.user_can_authenticate(user): - return user diff --git a/src/newsreader/auth/models.py b/src/newsreader/auth/models.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/newsreader/celery.py b/src/newsreader/celery.py new file mode 100644 index 0000000..4aeb7a1 --- /dev/null +++ b/src/newsreader/celery.py @@ -0,0 +1,12 @@ +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", broker="amqp://") + +app.autodiscover_tasks() diff --git a/src/newsreader/conf/base.py b/src/newsreader/conf/base.py index bba7a37..812707b 100644 --- a/src/newsreader/conf/base.py +++ b/src/newsreader/conf/base.py @@ -38,7 +38,10 @@ INSTALLED_APPS = [ "django.contrib.staticfiles", # third party apps "rest_framework", + "celery", + "django_celery_beat", # app modules + "newsreader.accounts", "newsreader.news.core", "newsreader.news.collection", ] @@ -92,8 +95,8 @@ AUTH_PASSWORD_VALIDATORS = [ {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, ] -# Authentication -AUTHENTICATION_BACKENDS = ["newsreader.auth.backends.EmailBackend"] +# Authentication user model +AUTH_USER_MODEL = "accounts.User" # Internationalization # https://docs.djangoproject.com/en/2.2/topics/i18n/ @@ -110,10 +113,12 @@ STATIC_URL = "/static/" # Third party settings REST_FRAMEWORK = { - "DEFAULT_AUTHENTICATION_CLASSES": ("rest_framework.authentication.SessionAuthentication",), + "DEFAULT_AUTHENTICATION_CLASSES": ( + "rest_framework.authentication.SessionAuthentication", + ), "DEFAULT_PERMISSION_CLASSES": ( "rest_framework.permissions.IsAuthenticated", - "newsreader.auth.permissions.IsOwner", + "newsreader.accounts.permissions.IsOwner", ), "DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",), } diff --git a/src/newsreader/news/collection/admin.py b/src/newsreader/news/collection/admin.py index 77ae900..9727b69 100644 --- a/src/newsreader/news/collection/admin.py +++ b/src/newsreader/news/collection/admin.py @@ -8,5 +8,10 @@ class CollectionRuleAdmin(admin.ModelAdmin): list_display = ("name", "category", "url", "last_suceeded", "succeeded") + 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/base.py b/src/newsreader/news/collection/base.py index 1b80256..0524585 100644 --- a/src/newsreader/news/collection/base.py +++ b/src/newsreader/news/collection/base.py @@ -1,4 +1,6 @@ -from typing import ContextManager, Dict, List, Optional, Tuple +from typing import ContextManager, Dict, Optional, Tuple + +from django.db.models.query import QuerySet from bs4 import BeautifulSoup @@ -67,11 +69,13 @@ class Collector: client = None builder = None - def __init__(self, client: Optional[Client] = None, builder: Optional[Builder] = None) -> None: + def __init__( + self, client: Optional[Client] = None, builder: Optional[Builder] = None + ) -> None: self.client = client if client else self.client self.builder = builder if builder else self.builder - def collect(self, rules: Optional[List] = None) -> None: + def collect(self, rules: Optional[QuerySet] = None) -> None: with self.client(rules=rules) as client: for data, stream in client: with self.builder((data, stream)) as builder: diff --git a/src/newsreader/news/collection/favicon.py b/src/newsreader/news/collection/favicon.py index 05bd6c9..f0a5a6b 100644 --- a/src/newsreader/news/collection/favicon.py +++ b/src/newsreader/news/collection/favicon.py @@ -78,7 +78,9 @@ class FaviconClient(Client): def __enter__(self) -> ContextManager: with ThreadPoolExecutor(max_workers=10) as executor: - futures = {executor.submit(stream.read): rule for rule, stream in self.streams} + futures = { + executor.submit(stream.read): rule for rule, stream in self.streams + } for future in as_completed(futures): rule = futures[future] diff --git a/src/newsreader/news/collection/feed.py b/src/newsreader/news/collection/feed.py index 7237073..2ac4854 100644 --- a/src/newsreader/news/collection/feed.py +++ b/src/newsreader/news/collection/feed.py @@ -156,7 +156,7 @@ class FeedCollector(Collector): class FeedDuplicateHandler: def __init__(self, rule: CollectionRule) -> None: - self.queryset = rule.post_set.all() + self.queryset = rule.posts.all() def __enter__(self) -> ContextManager: self.existing_identifiers = self.queryset.filter( @@ -202,7 +202,9 @@ class FeedDuplicateHandler: def handle_duplicate(self, instance: Post) -> Optional[Post]: try: - existing_instance = self.queryset.get(remote_identifier=instance.remote_identifier) + existing_instance = self.queryset.get( + remote_identifier=instance.remote_identifier + ) except ObjectDoesNotExist: return diff --git a/src/newsreader/news/collection/management/commands/collect.py b/src/newsreader/news/collection/management/commands/collect.py index 3da9905..7d928f0 100644 --- a/src/newsreader/news/collection/management/commands/collect.py +++ b/src/newsreader/news/collection/management/commands/collect.py @@ -1,14 +1,11 @@ from django.core.management.base import BaseCommand from newsreader.news.collection.feed import FeedCollector -from newsreader.news.collection.models import CollectionRule class Command(BaseCommand): help = "Collects Atom/RSS feeds" def handle(self, *args, **options): - CollectionRule.objects.all() - collector = FeedCollector() collector.collect() diff --git a/src/newsreader/news/collection/migrations/0001_initial.py b/src/newsreader/news/collection/migrations/0001_initial.py index 00d3d31..51b9396 100644 --- a/src/newsreader/news/collection/migrations/0001_initial.py +++ b/src/newsreader/news/collection/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 2.2 on 2019-07-05 20:59 +# Generated by Django 2.2 on 2019-07-14 10:36 import django.utils.timezone @@ -18,7 +18,10 @@ class Migration(migrations.Migration): ( "id", models.AutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", ), ), ("created", models.DateTimeField(default=django.utils.timezone.now)), @@ -27,7 +30,9 @@ class Migration(migrations.Migration): ("url", models.URLField(max_length=1024)), ( "website_url", - models.URLField(blank=True, editable=False, max_length=1024, null=True), + models.URLField( + blank=True, editable=False, max_length=1024, null=True + ), ), ("favicon", models.URLField(blank=True, null=True)), ( @@ -93,8 +98,14 @@ class Migration(migrations.Migration): ("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/Buenos_Aires", + "America/Argentina/Buenos_Aires", + ), + ( + "America/Argentina/Catamarca", + "America/Argentina/Catamarca", + ), ( "America/Argentina/ComodRivadavia", "America/Argentina/ComodRivadavia", @@ -103,7 +114,10 @@ class Migration(migrations.Migration): ("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/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"), @@ -163,7 +177,10 @@ class Migration(migrations.Migration): ("America/Halifax", "America/Halifax"), ("America/Havana", "America/Havana"), ("America/Hermosillo", "America/Hermosillo"), - ("America/Indiana/Indianapolis", "America/Indiana/Indianapolis"), + ( + "America/Indiana/Indianapolis", + "America/Indiana/Indianapolis", + ), ("America/Indiana/Knox", "America/Indiana/Knox"), ("America/Indiana/Marengo", "America/Indiana/Marengo"), ("America/Indiana/Petersburg", "America/Indiana/Petersburg"), @@ -177,8 +194,14 @@ class Migration(migrations.Migration): ("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/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"), @@ -209,9 +232,18 @@ class Migration(migrations.Migration): ("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/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"), 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..09c01cf --- /dev/null +++ b/src/newsreader/news/collection/migrations/0002_auto_20190714_1036.py @@ -0,0 +1,37 @@ +# 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="Owner", 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..9f86c32 --- /dev/null +++ b/src/newsreader/news/collection/migrations/0003_auto_20190714_1417.py @@ -0,0 +1,19 @@ +# 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="Owner", related_name="rules", to=settings.AUTH_USER_MODEL + ), + ) + ] diff --git a/src/newsreader/news/collection/migrations/0003_collectionrule_user.py b/src/newsreader/news/collection/migrations/0003_collectionrule_user.py deleted file mode 100644 index a735ea3..0000000 --- a/src/newsreader/news/collection/migrations/0003_collectionrule_user.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 2.2 on 2019-07-07 17:08 - -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("collection", "0002_collectionrule_category"), - ] - - operations = [ - migrations.AddField( - model_name="collectionrule", - name="user", - field=models.ForeignKey(default=None, on_delete="Owner", to=settings.AUTH_USER_MODEL), - preserve_default=False, - ) - ] diff --git a/src/newsreader/news/collection/migrations/0002_collectionrule_category.py b/src/newsreader/news/collection/migrations/0004_auto_20190714_1422.py similarity index 75% rename from src/newsreader/news/collection/migrations/0002_collectionrule_category.py rename to src/newsreader/news/collection/migrations/0004_auto_20190714_1422.py index b9449a7..4e9efb2 100644 --- a/src/newsreader/news/collection/migrations/0002_collectionrule_category.py +++ b/src/newsreader/news/collection/migrations/0004_auto_20190714_1422.py @@ -1,4 +1,4 @@ -# Generated by Django 2.2 on 2019-07-05 20:59 +# Generated by Django 2.2 on 2019-07-14 14:22 import django.db.models.deletion @@ -7,12 +7,10 @@ from django.db import migrations, models class Migration(migrations.Migration): - initial = True - - dependencies = [("collection", "0001_initial"), ("core", "0001_initial")] + dependencies = [("collection", "0003_auto_20190714_1417")] operations = [ - migrations.AddField( + migrations.AlterField( model_name="collectionrule", name="category", field=models.ForeignKey( @@ -20,6 +18,7 @@ class Migration(migrations.Migration): 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/models.py b/src/newsreader/news/collection/models.py index d65176a..4432f77 100644 --- a/src/newsreader/news/collection/models.py +++ b/src/newsreader/news/collection/models.py @@ -24,6 +24,7 @@ class CollectionRule(TimeStampedModel): 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, ) @@ -32,7 +33,7 @@ class CollectionRule(TimeStampedModel): succeeded = models.BooleanField(default=False) error = models.CharField(max_length=255, blank=True, null=True) - user = models.ForeignKey("auth.User", _("Owner")) + user = models.ForeignKey("accounts.User", _("Owner"), related_name="rules") def __str__(self): return self.name diff --git a/src/newsreader/news/collection/serializers.py b/src/newsreader/news/collection/serializers.py index cf6f9ea..4f7f3a5 100644 --- a/src/newsreader/news/collection/serializers.py +++ b/src/newsreader/news/collection/serializers.py @@ -10,9 +10,11 @@ class CollectionRuleSerializer(serializers.HyperlinkedModelSerializer): def get_posts(self, instance): request = self.context.get("request") - posts = instance.post_set.order_by("-publication_date") + posts = instance.posts.order_by("-publication_date") - serializer = core.serializers.PostSerializer(posts, context={"request": request}, many=True) + serializer = core.serializers.PostSerializer( + posts, context={"request": request}, many=True + ) return serializer.data class Meta: diff --git a/src/newsreader/news/collection/tasks.py b/src/newsreader/news/collection/tasks.py new file mode 100644 index 0000000..bdf8ffc --- /dev/null +++ b/src/newsreader/news/collection/tasks.py @@ -0,0 +1,19 @@ +from django.core.exceptions import ObjectDoesNotExist + +from newsreader.accounts.models import User +from newsreader.celery import app +from newsreader.news.collection.feed import FeedCollector + + +@app.task +def collect(user_pk): + try: + user = User.objects.get(pk=user_pk) + except ObjectDoesNotExist: + # TODO remove this task + return + + rules = user.rules.all() + + collector = FeedCollector() + collector.collect(rules=rules) diff --git a/src/newsreader/news/collection/tests/endpoints/rules/detail/tests.py b/src/newsreader/news/collection/tests/endpoints/rules/detail/tests.py index 8782533..6a85345 100644 --- a/src/newsreader/news/collection/tests/endpoints/rules/detail/tests.py +++ b/src/newsreader/news/collection/tests/endpoints/rules/detail/tests.py @@ -5,7 +5,7 @@ from urllib.parse import urljoin from django.test import Client, TestCase from django.urls import reverse -from newsreader.auth.tests.factories import UserFactory +from newsreader.accounts.tests.factories import UserFactory from newsreader.news.collection.tests.factories import CollectionRuleFactory from newsreader.news.core.tests.factories import CategoryFactory @@ -130,14 +130,18 @@ class CollectionRuleDetailViewTestCase(TestCase): self.client.force_login(self.user) response = self.client.patch( reverse("api:rules-detail", args=[rule.pk]), - data=json.dumps({"category": reverse("api:categories-detail", args=[category.pk])}), + data=json.dumps( + {"category": reverse("api:categories-detail", args=[category.pk])} + ), content_type="application/json", ) data = response.json() url = data["category"] self.assertEquals(response.status_code, 200) - self.assertTrue(url.endswith(reverse("api:categories-detail", args=[category.pk]))) + self.assertTrue( + url.endswith(reverse("api:categories-detail", args=[category.pk])) + ) def test_put(self): rule = CollectionRuleFactory(name="BBC", user=self.user) diff --git a/src/newsreader/news/collection/tests/endpoints/rules/list/tests.py b/src/newsreader/news/collection/tests/endpoints/rules/list/tests.py index f5a0cea..04c7b73 100644 --- a/src/newsreader/news/collection/tests/endpoints/rules/list/tests.py +++ b/src/newsreader/news/collection/tests/endpoints/rules/list/tests.py @@ -7,7 +7,7 @@ from django.urls import reverse import pytz -from newsreader.auth.tests.factories import UserFactory +from newsreader.accounts.tests.factories import UserFactory from newsreader.news.collection.tests.factories import CollectionRuleFactory from newsreader.news.core.tests.factories import CategoryFactory, PostFactory @@ -100,7 +100,9 @@ class CollectionRuleListViewTestCase(TestCase): self.client.force_login(self.user) response = self.client.post( - reverse("api:rules-list"), data=json.dumps(data), content_type="application/json" + reverse("api:rules-list"), + data=json.dumps(data), + content_type="application/json", ) data = response.json() category_url = data["category"] @@ -110,7 +112,9 @@ class CollectionRuleListViewTestCase(TestCase): self.assertEquals(data["name"], "BBC") self.assertEquals(data["url"], "https://www.bbc.co.uk") - self.assertTrue(category_url.endswith(reverse("api:categories-detail", args=[category.pk]))) + self.assertTrue( + category_url.endswith(reverse("api:categories-detail", args=[category.pk])) + ) def test_patch(self): self.client.force_login(self.user) diff --git a/src/newsreader/news/collection/tests/factories.py b/src/newsreader/news/collection/tests/factories.py index 9d0803f..bddcf1b 100644 --- a/src/newsreader/news/collection/tests/factories.py +++ b/src/newsreader/news/collection/tests/factories.py @@ -1,6 +1,6 @@ import factory -from newsreader.auth.tests.factories import UserFactory +from newsreader.accounts.tests.factories import UserFactory from newsreader.news.collection.models import CollectionRule diff --git a/src/newsreader/news/collection/tests/favicon/builder/tests.py b/src/newsreader/news/collection/tests/favicon/builder/tests.py index 2e7b57a..e8a1a34 100644 --- a/src/newsreader/news/collection/tests/favicon/builder/tests.py +++ b/src/newsreader/news/collection/tests/favicon/builder/tests.py @@ -18,7 +18,9 @@ class FaviconBuilderTestCase(TestCase): 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) + rule = CollectionRuleFactory( + website_url="https://www.theguardian.com/", favicon=None + ) with FaviconBuilder((rule, mock_without_url)) as builder: builder.build() @@ -39,7 +41,9 @@ class FaviconBuilderTestCase(TestCase): with FaviconBuilder((rule, mock_with_weird_path)) as builder: builder.build() - self.assertEquals(rule.favicon, "https://www.theguardian.com/jabadaba/doe/favicon.ico") + self.assertEquals( + rule.favicon, "https://www.theguardian.com/jabadaba/doe/favicon.ico" + ) def test_other_url(self): rule = CollectionRuleFactory(favicon=None) diff --git a/src/newsreader/news/collection/tests/favicon/collector/mocks.py b/src/newsreader/news/collection/tests/favicon/collector/mocks.py index 097b1dd..3318ffd 100644 --- a/src/newsreader/news/collection/tests/favicon/collector/mocks.py +++ b/src/newsreader/news/collection/tests/favicon/collector/mocks.py @@ -137,7 +137,13 @@ feed_mock = { "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"}], + "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", diff --git a/src/newsreader/news/collection/tests/favicon/collector/tests.py b/src/newsreader/news/collection/tests/favicon/collector/tests.py index 48c16e7..44254a5 100644 --- a/src/newsreader/news/collection/tests/favicon/collector/tests.py +++ b/src/newsreader/news/collection/tests/favicon/collector/tests.py @@ -22,10 +22,14 @@ class FaviconCollectorTestCase(TestCase): def setUp(self): self.maxDiff = None - self.patched_feed_client = patch("newsreader.news.collection.favicon.FeedClient.__enter__") + 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.patched_website_read = patch( + "newsreader.news.collection.favicon.WebsiteStream.read" + ) self.mocked_website_read = self.patched_website_read.start() def tearDown(self): diff --git a/src/newsreader/news/collection/tests/feed/builder/mocks.py b/src/newsreader/news/collection/tests/feed/builder/mocks.py index 9003f86..945347b 100644 --- a/src/newsreader/news/collection/tests/feed/builder/mocks.py +++ b/src/newsreader/news/collection/tests/feed/builder/mocks.py @@ -57,7 +57,13 @@ simple_mock = { "language": "en-gb", "link": "https://www.bbc.co.uk/news/", }, - "links": [{"href": "https://www.bbc.co.uk/news/", "rel": "alternate", "type": "text/html"}], + "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", @@ -201,7 +207,13 @@ multiple_mock = { "language": "en-gb", "link": "https://www.bbc.co.uk/news/", }, - "links": [{"href": "https://www.bbc.co.uk/news/", "rel": "alternate", "type": "text/html"}], + "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", @@ -302,7 +314,13 @@ mock_without_identifier = { "language": "en-gb", "link": "https://www.bbc.co.uk/news/", }, - "links": [{"href": "https://www.bbc.co.uk/news/", "rel": "alternate", "type": "text/html"}], + "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", @@ -402,7 +420,13 @@ mock_without_publish_date = { "language": "en-gb", "link": "https://www.bbc.co.uk/news/", }, - "links": [{"href": "https://www.bbc.co.uk/news/", "rel": "alternate", "type": "text/html"}], + "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", @@ -492,7 +516,13 @@ mock_without_url = { "language": "en-gb", "link": "https://www.bbc.co.uk/news/", }, - "links": [{"href": "https://www.bbc.co.uk/news/", "rel": "alternate", "type": "text/html"}], + "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", @@ -575,7 +605,13 @@ mock_without_body = { "language": "en-gb", "link": "https://www.bbc.co.uk/news/", }, - "links": [{"href": "https://www.bbc.co.uk/news/", "rel": "alternate", "type": "text/html"}], + "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", @@ -676,7 +712,13 @@ mock_without_author = { "language": "en-gb", "link": "https://www.bbc.co.uk/news/", }, - "links": [{"href": "https://www.bbc.co.uk/news/", "rel": "alternate", "type": "text/html"}], + "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", @@ -822,7 +864,13 @@ mock_with_update_entries = { "language": "en-gb", "link": "https://www.bbc.co.uk/news/", }, - "links": [{"href": "https://www.bbc.co.uk/news/", "rel": "alternate", "type": "text/html"}], + "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", @@ -883,7 +931,13 @@ mock_with_html = { "language": "en-gb", "link": "https://www.bbc.co.uk/news/", }, - "links": [{"href": "https://www.bbc.co.uk/news/", "rel": "alternate", "type": "text/html"}], + "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", @@ -945,7 +999,13 @@ mock_with_long_author = { "language": "en-gb", "link": "https://www.bbc.co.uk/news/", }, - "links": [{"href": "https://www.bbc.co.uk/news/", "rel": "alternate", "type": "text/html"}], + "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", @@ -1012,7 +1072,13 @@ mock_with_long_title = { "language": "en-gb", "link": "https://www.bbc.co.uk/news/", }, - "links": [{"href": "https://www.bbc.co.uk/news/", "rel": "alternate", "type": "text/html"}], + "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", diff --git a/src/newsreader/news/collection/tests/feed/builder/tests.py b/src/newsreader/news/collection/tests/feed/builder/tests.py index 33aae7f..2f09591 100644 --- a/src/newsreader/news/collection/tests/feed/builder/tests.py +++ b/src/newsreader/news/collection/tests/feed/builder/tests.py @@ -42,7 +42,9 @@ class FeedBuilderTestCase(TestCase): 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") + self.assertEquals( + post.title, "Trump's 'genocidal taunts' will not end Iran - Zarif" + ) def test_multiple_entries(self): builder = FeedBuilder @@ -64,12 +66,17 @@ class FeedBuilderTestCase(TestCase): self.assertEquals(first_post.publication_date, aware_date) self.assertEquals( - first_post.remote_identifier, "https://www.bbc.co.uk/news/world-us-canada-48338168" + 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.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") + 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) @@ -77,10 +84,13 @@ class FeedBuilderTestCase(TestCase): self.assertEquals(second_post.publication_date, aware_date) self.assertEquals( - second_post.remote_identifier, "https://www.bbc.co.uk/news/technology-48334739" + 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.url, "https://www.bbc.co.uk/news/technology-48334739" + ) self.assertEquals(second_post.title, "Huawei's Android loss: How it affects you") @@ -104,9 +114,13 @@ class FeedBuilderTestCase(TestCase): 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.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") + 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): @@ -125,12 +139,14 @@ class FeedBuilderTestCase(TestCase): self.assertEquals(first_post.created, timezone.now()) self.assertEquals( - first_post.remote_identifier, "https://www.bbc.co.uk/news/world-us-canada-48338168" + 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" + second_post.remote_identifier, + "https://www.bbc.co.uk/news/technology-48334739", ) @freeze_time("2019-10-30 12:30:00") @@ -150,12 +166,14 @@ class FeedBuilderTestCase(TestCase): self.assertEquals(first_post.created, timezone.now()) self.assertEquals( - first_post.remote_identifier, "https://www.bbc.co.uk/news/world-us-canada-48338168" + 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" + second_post.remote_identifier, + "https://www.bbc.co.uk/news/technology-48334739", ) @freeze_time("2019-10-30 12:30:00") @@ -175,7 +193,8 @@ class FeedBuilderTestCase(TestCase): self.assertEquals(first_post.created, timezone.now()) self.assertEquals( - first_post.remote_identifier, "https://www.bbc.co.uk/news/world-us-canada-48338168" + first_post.remote_identifier, + "https://www.bbc.co.uk/news/world-us-canada-48338168", ) self.assertEquals(second_post.created, timezone.now()) @@ -201,12 +220,14 @@ class FeedBuilderTestCase(TestCase): self.assertEquals(first_post.created, timezone.now()) self.assertEquals( - first_post.remote_identifier, "https://www.bbc.co.uk/news/world-us-canada-48338168" + 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" + second_post.remote_identifier, + "https://www.bbc.co.uk/news/technology-48334739", ) def test_empty_entries(self): @@ -241,10 +262,13 @@ class FeedBuilderTestCase(TestCase): existing_second_post.refresh_from_db() self.assertEquals( - existing_first_post.title, "Trump's 'genocidal taunts' will not end Iran - Zarif" + 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") + self.assertEquals( + existing_second_post.title, "Huawei's Android loss: How it affects you" + ) def test_html_sanitizing(self): builder = FeedBuilder diff --git a/src/newsreader/news/collection/tests/feed/client/mocks.py b/src/newsreader/news/collection/tests/feed/client/mocks.py index e055e7b..05283a4 100644 --- a/src/newsreader/news/collection/tests/feed/client/mocks.py +++ b/src/newsreader/news/collection/tests/feed/client/mocks.py @@ -54,7 +54,13 @@ simple_mock = { "language": "en-gb", "link": "https://www.bbc.co.uk/news/", }, - "links": [{"href": "https://www.bbc.co.uk/news/", "rel": "alternate", "type": "text/html"}], + "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", diff --git a/src/newsreader/news/collection/tests/feed/collector/mocks.py b/src/newsreader/news/collection/tests/feed/collector/mocks.py index 211f4ef..8ff19b9 100644 --- a/src/newsreader/news/collection/tests/feed/collector/mocks.py +++ b/src/newsreader/news/collection/tests/feed/collector/mocks.py @@ -134,7 +134,13 @@ multiple_mock = { "language": "en-gb", "link": "https://www.bbc.co.uk/news/", }, - "links": [{"href": "https://www.bbc.co.uk/news/", "rel": "alternate", "type": "text/html"}], + "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", @@ -154,7 +160,13 @@ empty_mock = { "language": "en-gb", "link": "https://www.bbc.co.uk/news/", }, - "links": [{"href": "https://www.bbc.co.uk/news/", "rel": "alternate", "type": "text/html"}], + "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", @@ -292,7 +304,13 @@ duplicate_mock = { "language": "en-gb", "link": "https://www.bbc.co.uk/news/", }, - "links": [{"href": "https://www.bbc.co.uk/news/", "rel": "alternate", "type": "text/html"}], + "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", @@ -433,7 +451,13 @@ multiple_update_mock = { "language": "en-gb", "link": "https://www.bbc.co.uk/news/", }, - "links": [{"href": "https://www.bbc.co.uk/news/", "rel": "alternate", "type": "text/html"}], + "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", diff --git a/src/newsreader/news/collection/tests/feed/collector/tests.py b/src/newsreader/news/collection/tests/feed/collector/tests.py index 6834b9c..35dd8d8 100644 --- a/src/newsreader/news/collection/tests/feed/collector/tests.py +++ b/src/newsreader/news/collection/tests/feed/collector/tests.py @@ -234,8 +234,12 @@ class FeedCollectorTestCase(TestCase): self.assertEquals(rule.last_suceeded, timezone.now()) self.assertEquals(rule.error, None) - self.assertEquals(first_post.title, "Trump's 'genocidal taunts' will not end Iran - Zarif") + self.assertEquals( + first_post.title, "Trump's 'genocidal taunts' will not end Iran - Zarif" + ) self.assertEquals(second_post.title, "Huawei's Android loss: How it affects you") - self.assertEquals(third_post.title, "Birmingham head teacher threatened over LGBT lessons") + self.assertEquals( + third_post.title, "Birmingham head teacher threatened over LGBT lessons" + ) diff --git a/src/newsreader/news/collection/tests/feed/stream/mocks.py b/src/newsreader/news/collection/tests/feed/stream/mocks.py index 9e7d796..7dfeba6 100644 --- a/src/newsreader/news/collection/tests/feed/stream/mocks.py +++ b/src/newsreader/news/collection/tests/feed/stream/mocks.py @@ -54,7 +54,13 @@ simple_mock = { "language": "en-gb", "link": "https://www.bbc.co.uk/news/", }, - "links": [{"href": "https://www.bbc.co.uk/news/", "rel": "alternate", "type": "text/html"}], + "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", diff --git a/src/newsreader/news/collection/tests/mocks.py b/src/newsreader/news/collection/tests/mocks.py index 32ad699..574d3a5 100644 --- a/src/newsreader/news/collection/tests/mocks.py +++ b/src/newsreader/news/collection/tests/mocks.py @@ -20,7 +20,13 @@ simple_feed_mock = { "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"}], + "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", diff --git a/src/newsreader/news/collection/tests/tests.py b/src/newsreader/news/collection/tests/tests.py index c39abea..363e0b5 100644 --- a/src/newsreader/news/collection/tests/tests.py +++ b/src/newsreader/news/collection/tests/tests.py @@ -118,7 +118,9 @@ class URLBuilderTestCase(TestCase): def test_no_link(self): initial_rule = CollectionRuleFactory() - with URLBuilder((feed_mock_without_link, MagicMock(rule=initial_rule))) as builder: + with URLBuilder( + (feed_mock_without_link, MagicMock(rule=initial_rule)) + ) as builder: rule, url = builder.build() self.assertEquals(rule.pk, initial_rule.pk) diff --git a/src/newsreader/news/collection/urls.py b/src/newsreader/news/collection/urls.py index de289d1..4b59a09 100644 --- a/src/newsreader/news/collection/urls.py +++ b/src/newsreader/news/collection/urls.py @@ -1,6 +1,9 @@ from django.urls import path -from newsreader.news.collection.views import CollectionRuleAPIListView, CollectionRuleDetailView +from newsreader.news.collection.views import ( + CollectionRuleAPIListView, + CollectionRuleDetailView, +) endpoints = [ diff --git a/src/newsreader/news/core/migrations/0001_initial.py b/src/newsreader/news/core/migrations/0001_initial.py index 9d9ebc9..be138d9 100644 --- a/src/newsreader/news/core/migrations/0001_initial.py +++ b/src/newsreader/news/core/migrations/0001_initial.py @@ -1,8 +1,9 @@ -# Generated by Django 2.2 on 2019-07-05 20:59 +# Generated by Django 2.2 on 2019-07-14 10:36 import django.db.models.deletion import django.utils.timezone +from django.conf import settings from django.db import migrations, models @@ -10,43 +11,36 @@ class Migration(migrations.Migration): initial = True - dependencies = [("collection", "0001_initial")] + dependencies = [ + ("collection", "0001_initial"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] operations = [ - migrations.CreateModel( - name="Category", - 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=50, unique=True)), - ], - options={"verbose_name": "Category", "verbose_name_plural": "Categories"}, - ), migrations.CreateModel( name="Post", fields=[ ( "id", models.AutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + 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)), ("title", models.CharField(blank=True, max_length=200, null=True)), ("body", models.TextField(blank=True, null=True)), - ("author", models.CharField(blank=True, max_length=200, null=True)), + ("author", models.CharField(blank=True, max_length=40, null=True)), ("publication_date", models.DateTimeField(blank=True, null=True)), ("url", models.URLField(blank=True, max_length=1024, null=True)), ( "remote_identifier", - models.CharField(blank=True, editable=False, max_length=500, null=True), + models.CharField( + blank=True, editable=False, max_length=500, null=True + ), ), ( "rule", @@ -59,4 +53,26 @@ class Migration(migrations.Migration): ], options={"abstract": False}, ), + migrations.CreateModel( + name="Category", + 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=50, unique=True)), + ( + "user", + models.ForeignKey(on_delete="Owner", to=settings.AUTH_USER_MODEL), + ), + ], + options={"verbose_name": "Category", "verbose_name_plural": "Categories"}, + ), ] diff --git a/src/newsreader/news/core/migrations/0002_auto_20190714_1425.py b/src/newsreader/news/core/migrations/0002_auto_20190714_1425.py new file mode 100644 index 0000000..acb2d9d --- /dev/null +++ b/src/newsreader/news/core/migrations/0002_auto_20190714_1425.py @@ -0,0 +1,31 @@ +# Generated by Django 2.2 on 2019-07-14 14:25 + +import django.db.models.deletion + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [("core", "0001_initial")] + + operations = [ + migrations.AlterField( + model_name="category", + name="user", + field=models.ForeignKey( + on_delete="Owner", related_name="categories", to=settings.AUTH_USER_MODEL + ), + ), + migrations.AlterField( + model_name="post", + name="rule", + field=models.ForeignKey( + editable=False, + on_delete=django.db.models.deletion.CASCADE, + related_name="posts", + to="collection.CollectionRule", + ), + ), + ] diff --git a/src/newsreader/news/core/migrations/0002_category_user.py b/src/newsreader/news/core/migrations/0002_category_user.py deleted file mode 100644 index d2fa17f..0000000 --- a/src/newsreader/news/core/migrations/0002_category_user.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 2.2 on 2019-07-07 17:08 - -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("core", "0001_initial"), - ] - - operations = [ - migrations.AddField( - model_name="category", - name="user", - field=models.ForeignKey(default=None, on_delete="Owner", to=settings.AUTH_USER_MODEL), - preserve_default=False, - ) - ] diff --git a/src/newsreader/news/core/migrations/0003_auto_20190710_2022.py b/src/newsreader/news/core/migrations/0003_auto_20190710_2022.py deleted file mode 100644 index 3c7fe84..0000000 --- a/src/newsreader/news/core/migrations/0003_auto_20190710_2022.py +++ /dev/null @@ -1,16 +0,0 @@ -# Generated by Django 2.2 on 2019-07-10 18:22 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [("core", "0002_category_user")] - - operations = [ - migrations.AlterField( - model_name="post", - name="author", - field=models.CharField(blank=True, max_length=40, null=True), - ) - ] diff --git a/src/newsreader/news/core/models.py b/src/newsreader/news/core/models.py index c7d2638..ce5fa16 100644 --- a/src/newsreader/news/core/models.py +++ b/src/newsreader/news/core/models.py @@ -12,8 +12,12 @@ class Post(TimeStampedModel): publication_date = models.DateTimeField(blank=True, null=True) url = models.URLField(max_length=1024, blank=True, null=True) - rule = models.ForeignKey(CollectionRule, on_delete=models.CASCADE, editable=False) - remote_identifier = models.CharField(max_length=500, blank=True, null=True, editable=False) + rule = models.ForeignKey( + CollectionRule, on_delete=models.CASCADE, editable=False, related_name="posts" + ) + remote_identifier = models.CharField( + max_length=500, blank=True, null=True, editable=False + ) def __str__(self): return "Post-{}".format(self.pk) @@ -21,7 +25,7 @@ class Post(TimeStampedModel): class Category(TimeStampedModel): name = models.CharField(max_length=50, unique=True) - user = models.ForeignKey("auth.User", _("Owner")) + user = models.ForeignKey("accounts.User", _("Owner"), related_name="categories") class Meta: verbose_name = _("Category") diff --git a/src/newsreader/news/core/pagination.py b/src/newsreader/news/core/pagination.py index 357ff71..8a09234 100644 --- a/src/newsreader/news/core/pagination.py +++ b/src/newsreader/news/core/pagination.py @@ -7,7 +7,7 @@ class CategorySerializer(serializers.ModelSerializer): rules = serializers.SerializerMethodField() def get_rules(self, instance): - rules = instance.collectionrule_set.order_by("-modified", "-created") + rules = instance.rules.order_by("-modified", "-created") serializer = CollectionRuleSerializer(rules, many=True) return serializer.data diff --git a/src/newsreader/news/core/serializers.py b/src/newsreader/news/core/serializers.py index ca3e93c..cb6eb12 100644 --- a/src/newsreader/news/core/serializers.py +++ b/src/newsreader/news/core/serializers.py @@ -26,7 +26,7 @@ class CategorySerializer(serializers.HyperlinkedModelSerializer): def get_rules(self, instance): request = self.context.get("request") - rules = instance.collectionrule_set.order_by("-modified", "-created") + rules = instance.rules.order_by("-modified", "-created") serializer = collection.serializers.CollectionRuleSerializer( rules, context={"request": request}, many=True diff --git a/src/newsreader/news/core/tests/endpoints/category/detail/tests.py b/src/newsreader/news/core/tests/endpoints/category/detail/tests.py index d65fcb7..251023d 100644 --- a/src/newsreader/news/core/tests/endpoints/category/detail/tests.py +++ b/src/newsreader/news/core/tests/endpoints/category/detail/tests.py @@ -3,7 +3,7 @@ import json from django.test import Client, TestCase from django.urls import reverse -from newsreader.auth.tests.factories import UserFactory +from newsreader.accounts.tests.factories import UserFactory from newsreader.news.collection.tests.factories import CollectionRuleFactory from newsreader.news.core.tests.factories import CategoryFactory, PostFactory @@ -91,13 +91,17 @@ class CategoryDetailViewTestCase(TestCase): category = CategoryFactory(user=self.user) self.client.force_login(self.user) - response = self.client.delete(reverse("api:categories-detail", args=[category.pk])) + response = self.client.delete( + reverse("api:categories-detail", args=[category.pk]) + ) self.assertEquals(response.status_code, 204) def test_rules(self): category = CategoryFactory(user=self.user) - rules = CollectionRuleFactory.create_batch(size=5, category=category, user=self.user) + rules = CollectionRuleFactory.create_batch( + size=5, category=category, user=self.user + ) self.client.force_login(self.user) response = self.client.get(reverse("api:categories-detail", args=[category.pk])) @@ -149,7 +153,9 @@ class CategoryDetailViewTestCase(TestCase): if count < 1: continue - self.assertTrue(post["publication_date"] < posts[count - 1]["publication_date"]) + self.assertTrue( + post["publication_date"] < posts[count - 1]["publication_date"] + ) def test_category_with_unauthenticated_user(self): category = CategoryFactory(user=self.user) diff --git a/src/newsreader/news/core/tests/endpoints/category/list/tests.py b/src/newsreader/news/core/tests/endpoints/category/list/tests.py index 050a430..974645d 100644 --- a/src/newsreader/news/core/tests/endpoints/category/list/tests.py +++ b/src/newsreader/news/core/tests/endpoints/category/list/tests.py @@ -7,7 +7,7 @@ from django.urls import reverse import pytz -from newsreader.auth.tests.factories import UserFactory +from newsreader.accounts.tests.factories import UserFactory from newsreader.news.collection.tests.factories import CollectionRuleFactory from newsreader.news.core.tests.factories import CategoryFactory, PostFactory @@ -83,7 +83,9 @@ class CategoryListViewTestCase(TestCase): self.client.force_login(self.user) response = self.client.post( - reverse("api:categories-list"), data=json.dumps(data), content_type="application/json" + reverse("api:categories-list"), + data=json.dumps(data), + content_type="application/json", ) response_data = response.json() diff --git a/src/newsreader/news/core/tests/endpoints/post/detail/tests.py b/src/newsreader/news/core/tests/endpoints/post/detail/tests.py index e963035..465e2f2 100644 --- a/src/newsreader/news/core/tests/endpoints/post/detail/tests.py +++ b/src/newsreader/news/core/tests/endpoints/post/detail/tests.py @@ -3,7 +3,7 @@ import json from django.test import Client, TestCase from django.urls import reverse -from newsreader.auth.tests.factories import UserFactory +from newsreader.accounts.tests.factories import UserFactory from newsreader.news.collection.tests.factories import CollectionRuleFactory from newsreader.news.core.tests.factories import CategoryFactory, PostFactory @@ -17,7 +17,9 @@ class PostDetailViewTestCase(TestCase): self.user = UserFactory(is_staff=True, password="test") def test_simple(self): - rule = CollectionRuleFactory(user=self.user, category=CategoryFactory(user=self.user)) + rule = CollectionRuleFactory( + user=self.user, category=CategoryFactory(user=self.user) + ) post = PostFactory(rule=rule) self.client.force_login(self.user) @@ -44,7 +46,9 @@ class PostDetailViewTestCase(TestCase): self.assertEquals(data["detail"], "Not found.") def test_post(self): - rule = CollectionRuleFactory(user=self.user, category=CategoryFactory(user=self.user)) + rule = CollectionRuleFactory( + user=self.user, category=CategoryFactory(user=self.user) + ) post = PostFactory(rule=rule) self.client.force_login(self.user) @@ -55,7 +59,9 @@ class PostDetailViewTestCase(TestCase): self.assertEquals(data["detail"], 'Method "POST" not allowed.') def test_patch(self): - rule = CollectionRuleFactory(user=self.user, category=CategoryFactory(user=self.user)) + rule = CollectionRuleFactory( + user=self.user, category=CategoryFactory(user=self.user) + ) post = PostFactory(title="This is clickbait for sure", rule=rule) self.client.force_login(self.user) @@ -70,7 +76,9 @@ class PostDetailViewTestCase(TestCase): 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)) + rule = CollectionRuleFactory( + user=self.user, category=CategoryFactory(user=self.user) + ) post = PostFactory(title="This is clickbait for sure", rule=rule) self.client.force_login(self.user) @@ -85,8 +93,12 @@ class PostDetailViewTestCase(TestCase): 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)) + 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) self.client.force_login(self.user) @@ -103,7 +115,9 @@ class PostDetailViewTestCase(TestCase): self.assertTrue(rule_url.endswith(reverse("api:rules-detail", args=[rule.pk]))) def test_put(self): - rule = CollectionRuleFactory(user=self.user, category=CategoryFactory(user=self.user)) + rule = CollectionRuleFactory( + user=self.user, category=CategoryFactory(user=self.user) + ) post = PostFactory(title="This is clickbait for sure", rule=rule) self.client.force_login(self.user) @@ -118,7 +132,9 @@ class PostDetailViewTestCase(TestCase): self.assertEquals(data["title"], "This title is very accurate") def test_delete(self): - rule = CollectionRuleFactory(user=self.user, category=CategoryFactory(user=self.user)) + rule = CollectionRuleFactory( + user=self.user, category=CategoryFactory(user=self.user) + ) post = PostFactory(rule=rule) self.client.force_login(self.user) @@ -137,7 +153,9 @@ class PostDetailViewTestCase(TestCase): self.assertEquals(response.status_code, 403) def test_post_with_unauthenticated_user_with_category(self): - rule = CollectionRuleFactory(user=self.user, category=CategoryFactory(user=self.user)) + 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])) @@ -156,7 +174,9 @@ class PostDetailViewTestCase(TestCase): def test_post_with_unauthorized_user_with_category(self): other_user = UserFactory() - rule = CollectionRuleFactory(user=other_user, category=CategoryFactory(user=other_user)) + rule = CollectionRuleFactory( + user=other_user, category=CategoryFactory(user=other_user) + ) post = PostFactory(rule=rule) self.client.force_login(self.user) @@ -166,7 +186,9 @@ class PostDetailViewTestCase(TestCase): def test_post_with_different_user_for_category_and_rule(self): other_user = UserFactory() - rule = CollectionRuleFactory(user=self.user, category=CategoryFactory(user=other_user)) + rule = CollectionRuleFactory( + user=self.user, category=CategoryFactory(user=other_user) + ) post = PostFactory(rule=rule) self.client.force_login(self.user) diff --git a/src/newsreader/news/core/tests/endpoints/post/list/tests.py b/src/newsreader/news/core/tests/endpoints/post/list/tests.py index ed1c29e..724d8b2 100644 --- a/src/newsreader/news/core/tests/endpoints/post/list/tests.py +++ b/src/newsreader/news/core/tests/endpoints/post/list/tests.py @@ -5,7 +5,7 @@ from django.urls import reverse import pytz -from newsreader.auth.tests.factories import UserFactory +from newsreader.accounts.tests.factories import UserFactory from newsreader.news.collection.tests.factories import CollectionRuleFactory from newsreader.news.core.tests.factories import CategoryFactory, PostFactory @@ -18,7 +18,9 @@ class PostListViewTestCase(TestCase): self.user = UserFactory(is_staff=True, password="test") def test_simple(self): - rule = CollectionRuleFactory(user=self.user, category=CategoryFactory(user=self.user)) + rule = CollectionRuleFactory( + user=self.user, category=CategoryFactory(user=self.user) + ) PostFactory.create_batch(size=3, rule=rule) self.client.force_login(self.user) @@ -31,7 +33,9 @@ class PostListViewTestCase(TestCase): self.assertEquals(data["count"], 3) def test_ordering(self): - rule = CollectionRuleFactory(user=self.user, category=CategoryFactory(user=self.user)) + rule = CollectionRuleFactory( + user=self.user, category=CategoryFactory(user=self.user) + ) posts = [ PostFactory( @@ -71,7 +75,9 @@ class PostListViewTestCase(TestCase): self.assertEquals(data["results"][2]["id"], posts[0].pk) def test_pagination_count(self): - rule = CollectionRuleFactory(user=self.user, category=CategoryFactory(user=self.user)) + rule = CollectionRuleFactory( + user=self.user, category=CategoryFactory(user=self.user) + ) PostFactory.create_batch(size=80, rule=rule) page_size = 50 @@ -180,7 +186,9 @@ class PostListViewTestCase(TestCase): def test_posts_with_authorized_rule_unauthorized_category(self): other_user = UserFactory() - rule = CollectionRuleFactory(user=self.user, category=CategoryFactory(user=other_user)) + rule = CollectionRuleFactory( + user=self.user, category=CategoryFactory(user=other_user) + ) PostFactory.create_batch(size=3, rule=rule) self.client.force_login(self.user) diff --git a/src/newsreader/news/core/tests/factories.py b/src/newsreader/news/core/tests/factories.py index c4c6d4c..3ccf52d 100644 --- a/src/newsreader/news/core/tests/factories.py +++ b/src/newsreader/news/core/tests/factories.py @@ -1,7 +1,7 @@ import factory import pytz -from newsreader.auth.tests.factories import UserFactory +from newsreader.accounts.tests.factories import UserFactory from newsreader.news.core.models import Category, Post @@ -21,7 +21,9 @@ class PostFactory(factory.django.DjangoModelFactory): url = factory.Faker("url") remote_identifier = factory.Faker("url") - rule = factory.SubFactory("newsreader.news.collection.tests.factories.CollectionRuleFactory") + rule = factory.SubFactory( + "newsreader.news.collection.tests.factories.CollectionRuleFactory" + ) class Meta: model = Post diff --git a/src/newsreader/news/core/urls.py b/src/newsreader/news/core/urls.py index c207440..c5ccaa9 100644 --- a/src/newsreader/news/core/urls.py +++ b/src/newsreader/news/core/urls.py @@ -12,5 +12,7 @@ endpoints = [ path("posts/", ListPostAPIView.as_view(), name="posts-list"), path("posts//", DetailPostAPIView.as_view(), name="posts-detail"), path("categories/", ListCategoryAPIView.as_view(), name="categories-list"), - path("categories//", DetailCategoryAPIView.as_view(), name="categories-detail"), + path( + "categories//", DetailCategoryAPIView.as_view(), name="categories-detail" + ), ] diff --git a/src/newsreader/news/core/views.py b/src/newsreader/news/core/views.py index a0559be..415816c 100644 --- a/src/newsreader/news/core/views.py +++ b/src/newsreader/news/core/views.py @@ -8,7 +8,7 @@ from rest_framework.generics import ( ) from rest_framework.permissions import IsAuthenticated -from newsreader.auth.permissions import IsPostOwner +from newsreader.accounts.permissions import IsPostOwner from newsreader.core.pagination import LargeResultSetPagination, ResultSetPagination from newsreader.news.core.models import Category, Post from newsreader.news.core.serializers import CategorySerializer, PostSerializer From b1c5be61f114fa1ade18794df227941c470b9ce2 Mon Sep 17 00:00:00 2001 From: Sonny Date: Mon, 15 Jul 2019 08:29:24 +0200 Subject: [PATCH 016/364] increase duplicate handler's history range --- src/newsreader/news/collection/feed.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/newsreader/news/collection/feed.py b/src/newsreader/news/collection/feed.py index 2ac4854..09acac0 100644 --- a/src/newsreader/news/collection/feed.py +++ b/src/newsreader/news/collection/feed.py @@ -188,13 +188,13 @@ class FeedDuplicateHandler: "publication_date": post.publication_date, } - for existing_post in self.queryset.order_by("-publication_date")[:50]: + 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: Post, values: Dict) -> bool: for key, value in values.items(): - existing_value = getattr(existing_post, key, object()) + existing_value = getattr(existing_post, key, None) if existing_value != value: return False From 679414a703c2b6de029ec20c701ad1a52bf8e7ce Mon Sep 17 00:00:00 2001 From: Sonny Date: Sat, 20 Jul 2019 09:57:10 +0200 Subject: [PATCH 017/364] Add Login page --- .babelrc | 3 + .gitignore | 6 + gulp/sass.js | 17 + gulpfile.babel.js | 22 + package-lock.json | 6189 +++++++++++++++++ package.json | 40 + .../accounts/templates/accounts/login.html | 24 + src/newsreader/accounts/urls.py | 9 + src/newsreader/accounts/views.py | 17 +- src/newsreader/conf/base.py | 2 +- .../scss/accounts/components/form/_form.scss | 27 + .../scss/accounts/components/form/index.scss | 1 + .../src/scss/accounts/components/index.scss | 2 + .../scss/accounts/components/main/_main.scss | 8 + .../scss/accounts/components/main/index.scss | 1 + .../static/src/scss/accounts/index.scss | 4 + .../src/scss/components/body/_body.scss | 5 + .../src/scss/components/body/index.scss | 1 + .../src/scss/components/button/_button.scss | 28 + .../src/scss/components/button/index.scss | 1 + .../src/scss/components/error/_error.scss | 5 + .../src/scss/components/error/_errorlist.scss | 3 + .../src/scss/components/error/index.scss | 2 + .../src/scss/components/form/_form.scss | 13 + .../src/scss/components/form/index.scss | 1 + .../static/src/scss/components/index.scss | 6 + .../src/scss/components/input/input.scss | 0 .../src/scss/components/main/_main.scss | 4 + .../src/scss/components/main/index.scss | 1 + .../src/scss/components/navbar/_navbar.scss | 43 + .../src/scss/components/navbar/index.scss | 1 + .../static/src/scss/partials/_variables.scss | 26 + src/newsreader/templates/base.html | 33 + src/newsreader/urls.py | 2 + 34 files changed, 6545 insertions(+), 2 deletions(-) create mode 100644 .babelrc create mode 100644 gulp/sass.js create mode 100644 gulpfile.babel.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/newsreader/accounts/templates/accounts/login.html create mode 100644 src/newsreader/accounts/urls.py create mode 100644 src/newsreader/static/src/scss/accounts/components/form/_form.scss create mode 100644 src/newsreader/static/src/scss/accounts/components/form/index.scss create mode 100644 src/newsreader/static/src/scss/accounts/components/index.scss create mode 100644 src/newsreader/static/src/scss/accounts/components/main/_main.scss create mode 100644 src/newsreader/static/src/scss/accounts/components/main/index.scss create mode 100644 src/newsreader/static/src/scss/accounts/index.scss create mode 100644 src/newsreader/static/src/scss/components/body/_body.scss create mode 100644 src/newsreader/static/src/scss/components/body/index.scss create mode 100644 src/newsreader/static/src/scss/components/button/_button.scss create mode 100644 src/newsreader/static/src/scss/components/button/index.scss create mode 100644 src/newsreader/static/src/scss/components/error/_error.scss create mode 100644 src/newsreader/static/src/scss/components/error/_errorlist.scss create mode 100644 src/newsreader/static/src/scss/components/error/index.scss create mode 100644 src/newsreader/static/src/scss/components/form/_form.scss create mode 100644 src/newsreader/static/src/scss/components/form/index.scss create mode 100644 src/newsreader/static/src/scss/components/index.scss create mode 100644 src/newsreader/static/src/scss/components/input/input.scss create mode 100644 src/newsreader/static/src/scss/components/main/_main.scss create mode 100644 src/newsreader/static/src/scss/components/main/index.scss create mode 100644 src/newsreader/static/src/scss/components/navbar/_navbar.scss create mode 100644 src/newsreader/static/src/scss/components/navbar/index.scss create mode 100644 src/newsreader/static/src/scss/partials/_variables.scss create mode 100644 src/newsreader/templates/base.html diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..1320b9a --- /dev/null +++ b/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["@babel/preset-env"] +} diff --git a/.gitignore b/.gitignore index 8d9e86a..0cc2b5c 100644 --- a/.gitignore +++ b/.gitignore @@ -192,3 +192,9 @@ dmypy.json # Pyre type checker # End of https://www.gitignore.io/api/django,python + +# Javascript +node_modules/ + +# Css +*.css diff --git a/gulp/sass.js b/gulp/sass.js new file mode 100644 index 0000000..637817f --- /dev/null +++ b/gulp/sass.js @@ -0,0 +1,17 @@ +import { src, dest } from "gulp"; + +import concat from "gulp-concat"; +import path from "path"; +import sass from "gulp-sass"; + +const PROJECT_DIR = path.join("src", "newsreader"); +const STATIC_DIR = path.join(PROJECT_DIR, "static"); + +export const ACCOUNTS_DIR = path.join(PROJECT_DIR, "accounts", "static"); + +export default function accountsTask(){ + return src(`${STATIC_DIR}/src/scss/accounts/index.scss`) + .pipe(sass().on("error", sass.logError)) + .pipe(concat("accounts.css")) + .pipe(dest(`${ACCOUNTS_DIR}/accounts/dist/css`)); +}; diff --git a/gulpfile.babel.js b/gulpfile.babel.js new file mode 100644 index 0000000..56a18ee --- /dev/null +++ b/gulpfile.babel.js @@ -0,0 +1,22 @@ +import { series, watch as _watch } from 'gulp'; + +import path from "path"; +import del from "del"; + +import buildSass, { ACCOUNTS_DIR } from "./gulp/sass"; + +const STATIC_DIR = path.join("src", "newsreader", "static"); + +function clean(){ + return del([ + `${ACCOUNTS_DIR}/accounts/dist/css/*`, + ]); +}; + +export function watch(){ + _watch(`${STATIC_DIR}/src/scss/**/*.scss`, (done) => { + series(clean, buildSass)(done); + }); +}; + +export default series(clean, buildSass); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..c412ebf --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6189 @@ +{ + "name": "newsreader", + "version": "0.1.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@babel/code-frame": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", + "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==", + "dev": true, + "requires": { + "@babel/highlight": "^7.0.0" + } + }, + "@babel/core": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.5.4.tgz", + "integrity": "sha512-+DaeBEpYq6b2+ZmHx3tHspC+ZRflrvLqwfv8E3hNr5LVQoyBnL8RPKSBCg+rK2W2My9PWlujBiqd0ZPsR9Q6zQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/generator": "^7.5.0", + "@babel/helpers": "^7.5.4", + "@babel/parser": "^7.5.0", + "@babel/template": "^7.4.4", + "@babel/traverse": "^7.5.0", + "@babel/types": "^7.5.0", + "convert-source-map": "^1.1.0", + "debug": "^4.1.0", + "json5": "^2.1.0", + "lodash": "^4.17.11", + "resolve": "^1.3.2", + "semver": "^5.4.1", + "source-map": "^0.5.0" + }, + "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/generator": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.5.0.tgz", + "integrity": "sha512-1TTVrt7J9rcG5PMjvO7VEG3FrEoEJNHxumRq66GemPmzboLWtIjjcJgk8rokuAS7IiRSpgVSu5Vb9lc99iJkOA==", + "dev": true, + "requires": { + "@babel/types": "^7.5.0", + "jsesc": "^2.5.1", + "lodash": "^4.17.11", + "source-map": "^0.5.0", + "trim-right": "^1.0.1" + } + }, + "@babel/helper-annotate-as-pure": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.0.0.tgz", + "integrity": "sha512-3UYcJUj9kvSLbLbUIfQTqzcy5VX7GRZ/CCDrnOaZorFFM01aXp1+GJwuFGV4NDDoAS+mOUyHcO6UD/RfqOks3Q==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.1.0.tgz", + "integrity": "sha512-qNSR4jrmJ8M1VMM9tibvyRAHXQs2PmaksQF7c1CGJNipfe3D8p+wgNwgso/P2A2r2mdgBWAXljNWR0QRZAMW8w==", + "dev": true, + "requires": { + "@babel/helper-explode-assignable-expression": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-call-delegate": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/helper-call-delegate/-/helper-call-delegate-7.4.4.tgz", + "integrity": "sha512-l79boDFJ8S1c5hvQvG+rc+wHw6IuH7YldmRKsYtpbawsxURu/paVy57FZMomGK22/JckepaikOkY0MoAmdyOlQ==", + "dev": true, + "requires": { + "@babel/helper-hoist-variables": "^7.4.4", + "@babel/traverse": "^7.4.4", + "@babel/types": "^7.4.4" + } + }, + "@babel/helper-define-map": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.4.4.tgz", + "integrity": "sha512-IX3Ln8gLhZpSuqHJSnTNBWGDE9kdkTEWl21A/K7PQ00tseBwbqCHTvNLHSBd9M0R5rER4h5Rsvj9vw0R5SieBg==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.1.0", + "@babel/types": "^7.4.4", + "lodash": "^4.17.11" + } + }, + "@babel/helper-explode-assignable-expression": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.1.0.tgz", + "integrity": "sha512-NRQpfHrJ1msCHtKjbzs9YcMmJZOg6mQMmGRB+hbamEdG5PNpaSm95275VD92DvJKuyl0s2sFiDmMZ+EnnvufqA==", + "dev": true, + "requires": { + "@babel/traverse": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-function-name": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz", + "integrity": "sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.0.0", + "@babel/template": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz", + "integrity": "sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.4.4.tgz", + "integrity": "sha512-VYk2/H/BnYbZDDg39hr3t2kKyifAm1W6zHRfhx8jGjIHpQEBv9dry7oQ2f3+J703TLu69nYdxsovl0XYfcnK4w==", + "dev": true, + "requires": { + "@babel/types": "^7.4.4" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.0.0.tgz", + "integrity": "sha512-avo+lm/QmZlv27Zsi0xEor2fKcqWG56D5ae9dzklpIaY7cQMK5N8VSpaNVPPagiqmy7LrEjK1IWdGMOqPu5csg==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-module-imports": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.0.0.tgz", + "integrity": "sha512-aP/hlLq01DWNEiDg4Jn23i+CXxW/owM4WpDLFUbpjxe4NS3BhLVZQ5i7E0ZrxuQ/vwekIeciyamgB1UIYxxM6A==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-module-transforms": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.4.4.tgz", + "integrity": "sha512-3Z1yp8TVQf+B4ynN7WoHPKS8EkdTbgAEy0nU0rs/1Kw4pDgmvYH3rz3aI11KgxKCba2cn7N+tqzV1mY2HMN96w==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.0.0", + "@babel/helper-simple-access": "^7.1.0", + "@babel/helper-split-export-declaration": "^7.4.4", + "@babel/template": "^7.4.4", + "@babel/types": "^7.4.4", + "lodash": "^4.17.11" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.0.0.tgz", + "integrity": "sha512-u8nd9NQePYNQV8iPWu/pLLYBqZBa4ZaY1YWRFMuxrid94wKI1QNt67NEZ7GAe5Kc/0LLScbim05xZFWkAdrj9g==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@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.4.4", + "resolved": "https://registry.npmjs.org/@babel/helper-regex/-/helper-regex-7.4.4.tgz", + "integrity": "sha512-Y5nuB/kESmR3tKjU8Nkn1wMGEx1tjJX076HBMeL3XLQCu6vA/YRzuTW0bbb+qRnXvQGn+d6Rx953yffl8vEy7Q==", + "dev": true, + "requires": { + "lodash": "^4.17.11" + } + }, + "@babel/helper-remap-async-to-generator": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.1.0.tgz", + "integrity": "sha512-3fOK0L+Fdlg8S5al8u/hWE6vhufGSn0bN09xm2LXMy//REAF8kDCrYoOBKYmA8m5Nom+sV9LyLCwrFynA8/slg==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.0.0", + "@babel/helper-wrap-function": "^7.1.0", + "@babel/template": "^7.1.0", + "@babel/traverse": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-replace-supers": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.4.4.tgz", + "integrity": "sha512-04xGEnd+s01nY1l15EuMS1rfKktNF+1CkKmHoErDppjAAZL+IUBZpzT748x262HF7fibaQPhbvWUl5HeSt1EXg==", + "dev": true, + "requires": { + "@babel/helper-member-expression-to-functions": "^7.0.0", + "@babel/helper-optimise-call-expression": "^7.0.0", + "@babel/traverse": "^7.4.4", + "@babel/types": "^7.4.4" + } + }, + "@babel/helper-simple-access": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.1.0.tgz", + "integrity": "sha512-Vk+78hNjRbsiu49zAPALxTb+JUQCz1aolpd8osOF16BGnLtseD21nbHgLPGUwrXEurZgiCOUmvs3ExTu4F5x6w==", + "dev": true, + "requires": { + "@babel/template": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz", + "integrity": "sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q==", + "dev": true, + "requires": { + "@babel/types": "^7.4.4" + } + }, + "@babel/helper-wrap-function": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.2.0.tgz", + "integrity": "sha512-o9fP1BZLLSrYlxYEYyl2aS+Flun5gtjTIG8iln+XuEzQTs0PLagAGSXUcqruJwD5fM48jzIEggCKpIfWTcR7pQ==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.1.0", + "@babel/template": "^7.1.0", + "@babel/traverse": "^7.1.0", + "@babel/types": "^7.2.0" + } + }, + "@babel/helpers": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.5.4.tgz", + "integrity": "sha512-6LJ6xwUEJP51w0sIgKyfvFMJvIb9mWAfohJp0+m6eHJigkFdcH8duZ1sfhn0ltJRzwUIT/yqqhdSfRpCpL7oow==", + "dev": true, + "requires": { + "@babel/template": "^7.4.4", + "@babel/traverse": "^7.5.0", + "@babel/types": "^7.5.0" + } + }, + "@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.5.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.5.0.tgz", + "integrity": "sha512-I5nW8AhGpOXGCCNYGc+p7ExQIBxRFnS2fd/d862bNOKvmoEPjYPcfIjsfdy0ujagYOIYPczKgD9l3FsgTkAzKA==", + "dev": true + }, + "@babel/plugin-proposal-async-generator-functions": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.2.0.tgz", + "integrity": "sha512-+Dfo/SCQqrwx48ptLVGLdE39YtWRuKc/Y9I5Fy0P1DDBB9lsAHpjcEJQt+4IifuSOSTLBKJObJqMvaO1pIE8LQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-remap-async-to-generator": "^7.1.0", + "@babel/plugin-syntax-async-generators": "^7.2.0" + } + }, + "@babel/plugin-proposal-dynamic-import": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.5.0.tgz", + "integrity": "sha512-x/iMjggsKTFHYC6g11PL7Qy58IK8H5zqfm9e6hu4z1iH2IRyAp9u9dL80zA6R76yFovETFLKz2VJIC2iIPBuFw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-dynamic-import": "^7.2.0" + } + }, + "@babel/plugin-proposal-json-strings": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.2.0.tgz", + "integrity": "sha512-MAFV1CA/YVmYwZG0fBQyXhmj0BHCB5egZHCKWIFVv/XCxAeVGIHfos3SwDck4LvCllENIAg7xMKOG5kH0dzyUg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-json-strings": "^7.2.0" + } + }, + "@babel/plugin-proposal-object-rest-spread": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.5.4.tgz", + "integrity": "sha512-KCx0z3y7y8ipZUMAEEJOyNi11lMb/FOPUjjB113tfowgw0c16EGYos7worCKBcUAh2oG+OBnoUhsnTSoLpV9uA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-object-rest-spread": "^7.2.0" + } + }, + "@babel/plugin-proposal-optional-catch-binding": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.2.0.tgz", + "integrity": "sha512-mgYj3jCcxug6KUcX4OBoOJz3CMrwRfQELPQ5560F70YQUBZB7uac9fqaWamKR1iWUzGiK2t0ygzjTScZnVz75g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-optional-catch-binding": "^7.2.0" + } + }, + "@babel/plugin-proposal-unicode-property-regex": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.4.4.tgz", + "integrity": "sha512-j1NwnOqMG9mFUOH58JTFsA/+ZYzQLUZ/drqWUqxCYLGeu2JFZL8YrNC9hBxKmWtAuOCHPcRpgv7fhap09Fb4kA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-regex": "^7.4.4", + "regexpu-core": "^4.5.4" + } + }, + "@babel/plugin-syntax-async-generators": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.2.0.tgz", + "integrity": "sha512-1ZrIRBv2t0GSlcwVoQ6VgSLpLgiN/FVQUzt9znxo7v2Ov4jJrs8RY8tv0wvDmFN3qIdMKWrmMMW6yZ0G19MfGg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-dynamic-import": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.2.0.tgz", + "integrity": "sha512-mVxuJ0YroI/h/tbFTPGZR8cv6ai+STMKNBq0f8hFxsxWjl94qqhsb+wXbpNMDPU3cfR1TIsVFzU3nXyZMqyK4w==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.2.0.tgz", + "integrity": "sha512-5UGYnMSLRE1dqqZwug+1LISpA403HzlSfsg6P9VXU6TBjcSHeNlw4DxDx7LgpF+iKZoOG/+uzqoRHTdcUpiZNg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.2.0.tgz", + "integrity": "sha512-t0JKGgqk2We+9may3t0xDdmneaXmyxq0xieYcKHxIsrJO64n1OiMWNUtc5gQK1PA0NpdCRrtZp4z+IUaKugrSA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.2.0.tgz", + "integrity": "sha512-bDe4xKNhb0LI7IvZHiA13kff0KEfaGX/Hv4lMA9+7TEc63hMNvfKo6ZFpXhKuEp+II/q35Gc4NoMeDZyaUbj9w==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-arrow-functions": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.2.0.tgz", + "integrity": "sha512-ER77Cax1+8/8jCB9fo4Ud161OZzWN5qawi4GusDuRLcDbDG+bIGYY20zb2dfAFdTRGzrfq2xZPvF0R64EHnimg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-async-to-generator": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.5.0.tgz", + "integrity": "sha512-mqvkzwIGkq0bEF1zLRRiTdjfomZJDV33AH3oQzHVGkI2VzEmXLpKKOBvEVaFZBJdN0XTyH38s9j/Kiqr68dggg==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.0.0", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-remap-async-to-generator": "^7.1.0" + } + }, + "@babel/plugin-transform-block-scoped-functions": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.2.0.tgz", + "integrity": "sha512-ntQPR6q1/NKuphly49+QiQiTN0O63uOwjdD6dhIjSWBI5xlrbUFh720TIpzBhpnrLfv2tNH/BXvLIab1+BAI0w==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-block-scoping": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.4.4.tgz", + "integrity": "sha512-jkTUyWZcTrwxu5DD4rWz6rDB5Cjdmgz6z7M7RLXOJyCUkFBawssDGcGh8M/0FTSB87avyJI1HsTwUXp9nKA1PA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "lodash": "^4.17.11" + } + }, + "@babel/plugin-transform-classes": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.4.4.tgz", + "integrity": "sha512-/e44eFLImEGIpL9qPxSRat13I5QNRgBLu2hOQJCF7VLy/otSM/sypV1+XaIw5+502RX/+6YaSAPmldk+nhHDPw==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.0.0", + "@babel/helper-define-map": "^7.4.4", + "@babel/helper-function-name": "^7.1.0", + "@babel/helper-optimise-call-expression": "^7.0.0", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-replace-supers": "^7.4.4", + "@babel/helper-split-export-declaration": "^7.4.4", + "globals": "^11.1.0" + } + }, + "@babel/plugin-transform-computed-properties": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.2.0.tgz", + "integrity": "sha512-kP/drqTxY6Xt3NNpKiMomfgkNn4o7+vKxK2DDKcBG9sHj51vHqMBGy8wbDS/J4lMxnqs153/T3+DmCEAkC5cpA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-destructuring": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.5.0.tgz", + "integrity": "sha512-YbYgbd3TryYYLGyC7ZR+Tq8H/+bCmwoaxHfJHupom5ECstzbRLTch6gOQbhEY9Z4hiCNHEURgq06ykFv9JZ/QQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-dotall-regex": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.4.4.tgz", + "integrity": "sha512-P05YEhRc2h53lZDjRPk/OektxCVevFzZs2Gfjd545Wde3k+yFDbXORgl2e0xpbq8mLcKJ7Idss4fAg0zORN/zg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-regex": "^7.4.4", + "regexpu-core": "^4.5.4" + } + }, + "@babel/plugin-transform-duplicate-keys": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.5.0.tgz", + "integrity": "sha512-igcziksHizyQPlX9gfSjHkE2wmoCH3evvD2qR5w29/Dk0SMKE/eOI7f1HhBdNhR/zxJDqrgpoDTq5YSLH/XMsQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-exponentiation-operator": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.2.0.tgz", + "integrity": "sha512-umh4hR6N7mu4Elq9GG8TOu9M0bakvlsREEC+ialrQN6ABS4oDQ69qJv1VtR3uxlKMCQMCvzk7vr17RHKcjx68A==", + "dev": true, + "requires": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.1.0", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-for-of": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.4.4.tgz", + "integrity": "sha512-9T/5Dlr14Z9TIEXLXkt8T1DU7F24cbhwhMNUziN3hB1AXoZcdzPcTiKGRn/6iOymDqtTKWnr/BtRKN9JwbKtdQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-function-name": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.4.4.tgz", + "integrity": "sha512-iU9pv7U+2jC9ANQkKeNF6DrPy4GBa4NWQtl6dHB4Pb3izX2JOEvDTFarlNsBj/63ZEzNNIAMs3Qw4fNCcSOXJA==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.1.0", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-literals": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.2.0.tgz", + "integrity": "sha512-2ThDhm4lI4oV7fVQ6pNNK+sx+c/GM5/SaML0w/r4ZB7sAneD/piDJtwdKlNckXeyGK7wlwg2E2w33C/Hh+VFCg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-member-expression-literals": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.2.0.tgz", + "integrity": "sha512-HiU3zKkSU6scTidmnFJ0bMX8hz5ixC93b4MHMiYebmk2lUVNGOboPsqQvx5LzooihijUoLR/v7Nc1rbBtnc7FA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-modules-amd": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.5.0.tgz", + "integrity": "sha512-n20UsQMKnWrltocZZm24cRURxQnWIvsABPJlw/fvoy9c6AgHZzoelAIzajDHAQrDpuKFFPPcFGd7ChsYuIUMpg==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.1.0", + "@babel/helper-plugin-utils": "^7.0.0", + "babel-plugin-dynamic-import-node": "^2.3.0" + } + }, + "@babel/plugin-transform-modules-commonjs": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.5.0.tgz", + "integrity": "sha512-xmHq0B+ytyrWJvQTc5OWAC4ii6Dhr0s22STOoydokG51JjWhyYo5mRPXoi+ZmtHQhZZwuXNN+GG5jy5UZZJxIQ==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.4.4", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-simple-access": "^7.1.0", + "babel-plugin-dynamic-import-node": "^2.3.0" + } + }, + "@babel/plugin-transform-modules-systemjs": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.5.0.tgz", + "integrity": "sha512-Q2m56tyoQWmuNGxEtUyeEkm6qJYFqs4c+XyXH5RAuYxObRNz9Zgj/1g2GMnjYp2EUyEy7YTrxliGCXzecl/vJg==", + "dev": true, + "requires": { + "@babel/helper-hoist-variables": "^7.4.4", + "@babel/helper-plugin-utils": "^7.0.0", + "babel-plugin-dynamic-import-node": "^2.3.0" + } + }, + "@babel/plugin-transform-modules-umd": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.2.0.tgz", + "integrity": "sha512-BV3bw6MyUH1iIsGhXlOK6sXhmSarZjtJ/vMiD9dNmpY8QXFFQTj+6v92pcfy1iqa8DeAfJFwoxcrS/TUZda6sw==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.1.0", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.4.5.tgz", + "integrity": "sha512-z7+2IsWafTBbjNsOxU/Iv5CvTJlr5w4+HGu1HovKYTtgJ362f7kBcQglkfmlspKKZ3bgrbSGvLfNx++ZJgCWsg==", + "dev": true, + "requires": { + "regexp-tree": "^0.1.6" + } + }, + "@babel/plugin-transform-new-target": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.4.4.tgz", + "integrity": "sha512-r1z3T2DNGQwwe2vPGZMBNjioT2scgWzK9BCnDEh+46z8EEwXBq24uRzd65I7pjtugzPSj921aM15RpESgzsSuA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-object-super": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.2.0.tgz", + "integrity": "sha512-VMyhPYZISFZAqAPVkiYb7dUe2AsVi2/wCT5+wZdsNO31FojQJa9ns40hzZ6U9f50Jlq4w6qwzdBB2uwqZ00ebg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-replace-supers": "^7.1.0" + } + }, + "@babel/plugin-transform-parameters": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.4.4.tgz", + "integrity": "sha512-oMh5DUO1V63nZcu/ZVLQFqiihBGo4OpxJxR1otF50GMeCLiRx5nUdtokd+u9SuVJrvvuIh9OosRFPP4pIPnwmw==", + "dev": true, + "requires": { + "@babel/helper-call-delegate": "^7.4.4", + "@babel/helper-get-function-arity": "^7.0.0", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-property-literals": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.2.0.tgz", + "integrity": "sha512-9q7Dbk4RhgcLp8ebduOpCbtjh7C0itoLYHXd9ueASKAG/is5PQtMR5VJGka9NKqGhYEGn5ITahd4h9QeBMylWQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-regenerator": { + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.4.5.tgz", + "integrity": "sha512-gBKRh5qAaCWntnd09S8QC7r3auLCqq5DI6O0DlfoyDjslSBVqBibrMdsqO+Uhmx3+BlOmE/Kw1HFxmGbv0N9dA==", + "dev": true, + "requires": { + "regenerator-transform": "^0.14.0" + } + }, + "@babel/plugin-transform-reserved-words": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.2.0.tgz", + "integrity": "sha512-fz43fqW8E1tAB3DKF19/vxbpib1fuyCwSPE418ge5ZxILnBhWyhtPgz8eh1RCGGJlwvksHkyxMxh0eenFi+kFw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-shorthand-properties": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.2.0.tgz", + "integrity": "sha512-QP4eUM83ha9zmYtpbnyjTLAGKQritA5XW/iG9cjtuOI8s1RuL/3V6a3DeSHfKutJQ+ayUfeZJPcnCYEQzaPQqg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-spread": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.2.2.tgz", + "integrity": "sha512-KWfky/58vubwtS0hLqEnrWJjsMGaOeSBn90Ezn5Jeg9Z8KKHmELbP1yGylMlm5N6TPKeY9A2+UaSYLdxahg01w==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-sticky-regex": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.2.0.tgz", + "integrity": "sha512-KKYCoGaRAf+ckH8gEL3JHUaFVyNHKe3ASNsZ+AlktgHevvxGigoIttrEJb8iKN03Q7Eazlv1s6cx2B2cQ3Jabw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-regex": "^7.0.0" + } + }, + "@babel/plugin-transform-template-literals": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.4.4.tgz", + "integrity": "sha512-mQrEC4TWkhLN0z8ygIvEL9ZEToPhG5K7KDW3pzGqOfIGZ28Jb0POUkeWcoz8HnHvhFy6dwAT1j8OzqN8s804+g==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.0.0", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-typeof-symbol": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.2.0.tgz", + "integrity": "sha512-2LNhETWYxiYysBtrBTqL8+La0jIoQQnIScUJc74OYvUGRmkskNY4EzLCnjHBzdmb38wqtTaixpo1NctEcvMDZw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-unicode-regex": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.4.4.tgz", + "integrity": "sha512-il+/XdNw01i93+M9J9u4T7/e/Ue/vWfNZE4IRUQjplu2Mqb/AFTDimkw2tdEdSH50wuQXZAbXSql0UphQke+vA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-regex": "^7.4.4", + "regexpu-core": "^4.5.4" + } + }, + "@babel/preset-env": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.5.4.tgz", + "integrity": "sha512-hFnFnouyRNiH1rL8YkX1ANCNAUVC8Djwdqfev8i1415tnAG+7hlA5zhZ0Q/3Q5gkop4HioIPbCEWAalqcbxRoQ==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.0.0", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-proposal-async-generator-functions": "^7.2.0", + "@babel/plugin-proposal-dynamic-import": "^7.5.0", + "@babel/plugin-proposal-json-strings": "^7.2.0", + "@babel/plugin-proposal-object-rest-spread": "^7.5.4", + "@babel/plugin-proposal-optional-catch-binding": "^7.2.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", + "@babel/plugin-syntax-async-generators": "^7.2.0", + "@babel/plugin-syntax-dynamic-import": "^7.2.0", + "@babel/plugin-syntax-json-strings": "^7.2.0", + "@babel/plugin-syntax-object-rest-spread": "^7.2.0", + "@babel/plugin-syntax-optional-catch-binding": "^7.2.0", + "@babel/plugin-transform-arrow-functions": "^7.2.0", + "@babel/plugin-transform-async-to-generator": "^7.5.0", + "@babel/plugin-transform-block-scoped-functions": "^7.2.0", + "@babel/plugin-transform-block-scoping": "^7.4.4", + "@babel/plugin-transform-classes": "^7.4.4", + "@babel/plugin-transform-computed-properties": "^7.2.0", + "@babel/plugin-transform-destructuring": "^7.5.0", + "@babel/plugin-transform-dotall-regex": "^7.4.4", + "@babel/plugin-transform-duplicate-keys": "^7.5.0", + "@babel/plugin-transform-exponentiation-operator": "^7.2.0", + "@babel/plugin-transform-for-of": "^7.4.4", + "@babel/plugin-transform-function-name": "^7.4.4", + "@babel/plugin-transform-literals": "^7.2.0", + "@babel/plugin-transform-member-expression-literals": "^7.2.0", + "@babel/plugin-transform-modules-amd": "^7.5.0", + "@babel/plugin-transform-modules-commonjs": "^7.5.0", + "@babel/plugin-transform-modules-systemjs": "^7.5.0", + "@babel/plugin-transform-modules-umd": "^7.2.0", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.4.5", + "@babel/plugin-transform-new-target": "^7.4.4", + "@babel/plugin-transform-object-super": "^7.2.0", + "@babel/plugin-transform-parameters": "^7.4.4", + "@babel/plugin-transform-property-literals": "^7.2.0", + "@babel/plugin-transform-regenerator": "^7.4.5", + "@babel/plugin-transform-reserved-words": "^7.2.0", + "@babel/plugin-transform-shorthand-properties": "^7.2.0", + "@babel/plugin-transform-spread": "^7.2.0", + "@babel/plugin-transform-sticky-regex": "^7.2.0", + "@babel/plugin-transform-template-literals": "^7.4.4", + "@babel/plugin-transform-typeof-symbol": "^7.2.0", + "@babel/plugin-transform-unicode-regex": "^7.4.4", + "@babel/types": "^7.5.0", + "browserslist": "^4.6.0", + "core-js-compat": "^3.1.1", + "invariant": "^2.2.2", + "js-levenshtein": "^1.1.3", + "semver": "^5.5.0" + } + }, + "@babel/register": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.4.4.tgz", + "integrity": "sha512-sn51H88GRa00+ZoMqCVgOphmswG4b7mhf9VOB0LUBAieykq2GnRFerlN+JQkO/ntT7wz4jaHNSRPg9IdMPEUkA==", + "dev": true, + "requires": { + "core-js": "^3.0.0", + "find-cache-dir": "^2.0.0", + "lodash": "^4.17.11", + "mkdirp": "^0.5.1", + "pirates": "^4.0.0", + "source-map-support": "^0.5.9" + } + }, + "@babel/template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.4.4.tgz", + "integrity": "sha512-CiGzLN9KgAvgZsnivND7rkA+AeJ9JB0ciPOD4U59GKbQP2iQl+olF1l76kJOupqidozfZ32ghwBEJDhnk9MEcw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/parser": "^7.4.4", + "@babel/types": "^7.4.4" + } + }, + "@babel/traverse": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.5.0.tgz", + "integrity": "sha512-SnA9aLbyOCcnnbQEGwdfBggnc142h/rbqqsXcaATj2hZcegCl903pUD/lfpsNBlBSuWow/YDfRyJuWi2EPR5cg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/generator": "^7.5.0", + "@babel/helper-function-name": "^7.1.0", + "@babel/helper-split-export-declaration": "^7.4.4", + "@babel/parser": "^7.5.0", + "@babel/types": "^7.5.0", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.11" + }, + "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.5.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.5.0.tgz", + "integrity": "sha512-UFpDVqRABKsW01bvw7/wSUe56uy6RXM5+VJibVVAybDGxEW25jdwiFJEf7ASvSaC7sN7rbE/l3cLp2izav+CtQ==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.11", + "to-fast-properties": "^2.0.0" + } + }, + "@nodelib/fs.scandir": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.1.tgz", + "integrity": "sha512-NT/skIZjgotDSiXs0WqYhgcuBKhUMgfekCmCGtkUAiLqZdOnrdjmZr9wRl3ll64J9NF79uZ4fk16Dx0yMc/Xbg==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.1", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.1.tgz", + "integrity": "sha512-+RqhBlLn6YRBGOIoVYthsG0J9dfpO79eJyN7BYBkZJtfqrBwf2KK+rD/M/yjZR6WBmIhAgOV7S60eCgaSWtbFw==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.2.tgz", + "integrity": "sha512-J/DR3+W12uCzAJkw7niXDcqcKBg6+5G5Q/ZpThpGNzAUz70eOR6RV4XnnSN01qHZiVl0eavoxJsBypQoKsV2QQ==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.1", + "fastq": "^1.6.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/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": "12.6.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.6.3.tgz", + "integrity": "sha512-7TEYTQT1/6PP53NftXXabIZDaZfaoBdeBm8Md/i7zsWRoBe0YwOXguyK8vhHs8ehgB/w9U4K/6EWuTyp0W6nIA==", + "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 + }, + "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" + } + }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", + "dev": true + }, + "ansi-colors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", + "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", + "dev": true, + "requires": { + "ansi-wrap": "^0.1.0" + } + }, + "ansi-gray": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-gray/-/ansi-gray-0.1.1.tgz", + "integrity": "sha1-KWLPVOyXksSFEKPetSRDaGHvclE=", + "dev": true, + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "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" + } + }, + "ansi-wrap": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz", + "integrity": "sha1-qCJQ3bABXponyoLoLqYDu/pF768=", + "dev": true + }, + "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" + } + }, + "append-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/append-buffer/-/append-buffer-1.0.2.tgz", + "integrity": "sha1-2CIM9GYIFSXv6lBhTz3mUU36WPE=", + "dev": true, + "requires": { + "buffer-equal": "^1.0.0" + } + }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "dev": true + }, + "archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", + "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-filter": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/arr-filter/-/arr-filter-1.1.2.tgz", + "integrity": "sha1-Q/3d0JHo7xGqTEXZzcGOLf8XEe4=", + "dev": true, + "requires": { + "make-iterator": "^1.0.0" + } + }, + "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-map": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/arr-map/-/arr-map-2.0.2.tgz", + "integrity": "sha1-Onc0X/wc814qkYJWAfnljy4kysQ=", + "dev": true, + "requires": { + "make-iterator": "^1.0.0" + } + }, + "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-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", + "integrity": "sha1-p5SvDAWrF1KEbudTofIRoFugxE8=", + "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-initial": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/array-initial/-/array-initial-1.1.0.tgz", + "integrity": "sha1-L6dLJnOTccOUe9enrcc74zSz15U=", + "dev": true, + "requires": { + "array-slice": "^1.0.0", + "is-number": "^4.0.0" + }, + "dependencies": { + "is-number": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", + "dev": true + } + } + }, + "array-last": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/array-last/-/array-last-1.3.0.tgz", + "integrity": "sha512-eOCut5rXlI6aCOS7Z7kCplKRKyiFQ6dHFBem4PwlwKeNFk2/XxTrhRh5T9PyaEWGy/NHTZWbY+nsZlNFJu9rYg==", + "dev": true, + "requires": { + "is-number": "^4.0.0" + }, + "dependencies": { + "is-number": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", + "dev": true + } + } + }, + "array-slice": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", + "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==", + "dev": true + }, + "array-sort": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-sort/-/array-sort-1.0.0.tgz", + "integrity": "sha512-ihLeJkonmdiAsD7vpgN3CRcx2J2S0TiYW+IS/5zHBI7mKUq3ySvBdzzBfD236ubDBQFiiyG3SWCPc+msQ9KoYg==", + "dev": true, + "requires": { + "default-compare": "^1.0.0", + "get-value": "^2.0.6", + "kind-of": "^5.0.2" + }, + "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 + } + } + }, + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "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" + } + }, + "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 + }, + "async-done": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/async-done/-/async-done-1.3.2.tgz", + "integrity": "sha512-uYkTP8dw2og1tu1nmza1n1CMW0qb8gWWlwqMmLb7MhBVs4BXrFziT6HXUd+/RlRA/i4H9AkofYloUbs1fwMqlw==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.2", + "process-nextick-args": "^2.0.0", + "stream-exhaust": "^1.0.1" + } + }, + "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-settle": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-settle/-/async-settle-1.0.0.tgz", + "integrity": "sha1-HQqRS7Aldb7IqPOnTlCA9yssDGs=", + "dev": true, + "requires": { + "async-done": "^1.2.2" + } + }, + "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.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", + "dev": true + }, + "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" + } + }, + "bach": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/bach/-/bach-1.2.0.tgz", + "integrity": "sha1-Szzpa/JxNPeaG0FKUcFONMO9mIA=", + "dev": true, + "requires": { + "arr-filter": "^1.1.1", + "arr-flatten": "^1.0.1", + "arr-map": "^2.0.0", + "array-each": "^1.0.0", + "array-initial": "^1.0.0", + "array-last": "^1.1.1", + "async-done": "^1.2.2", + "async-settle": "^1.0.0", + "now-and-later": "^2.0.0" + } + }, + "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" + } + } + } + }, + "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" + } + }, + "binary-extensions": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", + "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", + "dev": true + }, + "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" + } + }, + "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" + } + } + } + }, + "browserslist": { + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.6.6.tgz", + "integrity": "sha512-D2Nk3W9JL9Fp/gIcWei8LrERCS+eXu9AM5cfXA8WEZ84lFks+ARnZ0q/R69m2SV3Wjma83QDDPxsNKXUwdIsyA==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30000984", + "electron-to-chromium": "^1.3.191", + "node-releases": "^1.1.25" + } + }, + "buffer-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.0.tgz", + "integrity": "sha1-WWFrSYME1Var1GaWayLu2j7KX74=", + "dev": true + }, + "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 + }, + "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" + } + }, + "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.30000984", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000984.tgz", + "integrity": "sha512-n5tKOjMaZ1fksIpQbjERuqCyfgec/m9pferkFQbLmWtqLUdmt12hNhjSwsmPdqeiG2NkITOQhr1VYIwWSAceiA==", + "dev": true + }, + "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.6", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.6.tgz", + "integrity": "sha512-V2jUo67OKkc6ySiRpJrjlpJKl9kDuG+Xb8VgsGzb+aEouhgS1D0weyPU4lEzdAcsCAvrih2J2BqyXqHWvVLw5g==", + "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 + } + } + }, + "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" + } + } + } + }, + "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": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", + "dev": true + }, + "clone-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-buffer/-/clone-buffer-1.0.0.tgz", + "integrity": "sha1-4+JbIHrE5wGvch4staFnksrD3Fg=", + "dev": true + }, + "clone-stats": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", + "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=", + "dev": true + }, + "cloneable-readable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/cloneable-readable/-/cloneable-readable-1.1.3.tgz", + "integrity": "sha512-2EF8zTQOxYq70Y4XKtorQupqF0m49MBz2/yf5Bj+MHjvpG3Hy7sImifnqD6UA+TKYxeSV+u6qqQPawN5UvnpKQ==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "process-nextick-args": "^2.0.0", + "readable-stream": "^2.3.5" + } + }, + "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-map": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-map/-/collection-map-1.0.0.tgz", + "integrity": "sha1-rqDwb40mx4DCt1SUOFVEsiVa8Yw=", + "dev": true, + "requires": { + "arr-map": "^2.0.2", + "for-own": "^1.0.0", + "make-iterator": "^1.0.0" + } + }, + "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 + }, + "color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "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" + } + }, + "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" + } + }, + "concat-with-sourcemaps": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/concat-with-sourcemaps/-/concat-with-sourcemaps-1.1.0.tgz", + "integrity": "sha512-4gEjHJFT9e+2W/77h/DS5SGUgwDaOwprX8L/gl5+3ixnzkVJJsZWDSelmN3Oilw3LNDZjZV0yqH1hLG3k6nghg==", + "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 + } + } + }, + "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 + }, + "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-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", + "dev": true + }, + "copy-props": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/copy-props/-/copy-props-2.0.4.tgz", + "integrity": "sha512-7cjuUME+p+S3HZlbllgsn2CDwS+5eCCX16qBgNC4jgSTf49qR1VKy/Zhl400m0IQXl/bPGEVqncgUUMjrr4s8A==", + "dev": true, + "requires": { + "each-props": "^1.3.0", + "is-plain-object": "^2.0.1" + } + }, + "core-js": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.1.4.tgz", + "integrity": "sha512-YNZN8lt82XIMLnLirj9MhKDFZHalwzzrL9YLt6eb0T5D0EDl4IQ90IGkua8mHbnxNrkj1d8hbdizMc0Qmg1WnQ==", + "dev": true + }, + "core-js-compat": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.1.4.tgz", + "integrity": "sha512-Z5zbO9f1d0YrJdoaQhphVAnKPimX92D6z8lCGphH89MNRxlL1prI9ExJPqVwP0/kgkQCv8c4GJGT8X16yUncOg==", + "dev": true, + "requires": { + "browserslist": "^4.6.2", + "core-js-pure": "3.1.4", + "semver": "^6.1.1" + }, + "dependencies": { + "semver": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.2.0.tgz", + "integrity": "sha512-jdFC1VdUGT/2Scgbimf7FSx9iJLXoqfglSF+gJeuNWVpiE37OIbc1jywR/GJyFdz3mnkz2/id0L0J/cr0izR5A==", + "dev": true + } + } + }, + "core-js-pure": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.1.4.tgz", + "integrity": "sha512-uJ4Z7iPNwiu1foygbcZYJsJs1jiXrTTCvxfLDXNhI/I+NHbSIEyr548y4fcsCEyWY0XgfAG/qqaunJ1SThHenA==", + "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 + }, + "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" + } + }, + "d": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", + "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", + "dev": true, + "requires": { + "es5-ext": "^0.10.50", + "type": "^1.0.1" + } + }, + "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" + } + }, + "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 + }, + "default-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/default-compare/-/default-compare-1.0.0.tgz", + "integrity": "sha512-QWfXlM0EkAbqOCbD/6HjdwT19j7WCkMyiRhWilc4H9/5h/RzTF9gv5LYh1+CmDV5d1rki6KAWLtQale0xt20eQ==", + "dev": true, + "requires": { + "kind-of": "^5.0.2" + }, + "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 + } + } + }, + "default-resolution": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/default-resolution/-/default-resolution-2.0.0.tgz", + "integrity": "sha1-vLgrqnKtebQmp2cy8aga1t8m1oQ=", + "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" + } + } + } + }, + "del": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/del/-/del-5.0.0.tgz", + "integrity": "sha512-TfU3nUY0WDIhN18eq+pgpbLY9AfL5RfiE9czKaTSolc6aK7qASXfDErvYgjV1UqCR4sNXDoxO0/idPmhDUt2Sg==", + "dev": true, + "requires": { + "globby": "^10.0.0", + "is-path-cwd": "^2.0.0", + "is-path-in-cwd": "^2.0.0", + "p-map": "^2.0.0", + "rimraf": "^2.6.3" + } + }, + "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 + }, + "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 + }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.0.0" + }, + "dependencies": { + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + } + } + }, + "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" + } + }, + "each-props": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/each-props/-/each-props-1.3.2.tgz", + "integrity": "sha512-vV0Hem3zAGkJAyU7JSjixeU66rwdynTAa1vofCrSA5fEln+m67Az9CcnkVD776/fsN/UjIWmBDoNRS6t6G9RfA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.1", + "object.defaults": "^1.1.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.191", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.191.tgz", + "integrity": "sha512-jasjtY5RUy/TOyiUYM2fb4BDaPZfm6CXRFeJDMfFsXYADGxUN49RBqtgB7EL2RmJXeIRUk9lM1U6A5yk2YJMPQ==", + "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" + } + }, + "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" + } + }, + "es5-ext": { + "version": "0.10.50", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.50.tgz", + "integrity": "sha512-KMzZTPBkeQV/JcSQhI5/z6d9VWJ3EnQ194USTUwIYZ2ZbpN8+SGXQKt1h68EX44+qt+Fzr8DO17vnxrw7c3agw==", + "dev": true, + "requires": { + "es6-iterator": "~2.0.3", + "es6-symbol": "~3.1.1", + "next-tick": "^1.0.0" + } + }, + "es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", + "dev": true, + "requires": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "es6-symbol": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", + "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", + "dev": true, + "requires": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "es6-weak-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", + "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", + "dev": true, + "requires": { + "d": "1", + "es5-ext": "^0.10.46", + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.1" + } + }, + "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 + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "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" + } + }, + "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 + }, + "fancy-log": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.3.tgz", + "integrity": "sha512-k9oEhlyc0FrVh25qYuSELjr8oxsCoc4/LEZfg2iJJrfEk/tZL9bCoJE47gqAvI2m/AUjluCS4+3I0eTx8n3AEw==", + "dev": true, + "requires": { + "ansi-gray": "^0.1.1", + "color-support": "^1.1.3", + "parse-node-version": "^1.0.0", + "time-stamp": "^1.0.0" + } + }, + "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-glob": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.0.4.tgz", + "integrity": "sha512-wkIbV6qg37xTJwqSsdnIphL1e+LaGz4AIQqr00mIubMaEhv1/HEmJ0uuCGZRNRUkZZmOB5mJKO0ZUTVq+SxMQg==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.1", + "@nodelib/fs.walk": "^1.2.1", + "glob-parent": "^5.0.0", + "is-glob": "^4.0.1", + "merge2": "^1.2.3", + "micromatch": "^4.0.2" + }, + "dependencies": { + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "glob-parent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.0.0.tgz", + "integrity": "sha512-Z2RwiujPRGluePM6j699ktJYxmPpJKCfpGA13jz2hmFZC7gKetzrWvg5KN3+OsIFmydGyZ1AVwERCq1w/ZZwRg==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + } + } + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", + "dev": true + }, + "fastq": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.6.0.tgz", + "integrity": "sha512-jmxqQ3Z/nXoeyDmWAzF9kH1aGZSis6e/SbfPmJpUnyZ0ogr6iscHQaml4wsEepEWSdtmpy+eVXmCRIMpxaXqOA==", + "dev": true, + "requires": { + "reusify": "^1.0.0" + } + }, + "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" + } + }, + "fined": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz", + "integrity": "sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==", + "dev": true, + "requires": { + "expand-tilde": "^2.0.2", + "is-plain-object": "^2.0.3", + "object.defaults": "^1.1.0", + "object.pick": "^1.2.0", + "parse-filepath": "^1.0.1" + } + }, + "flagged-respawn": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz", + "integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==", + "dev": true + }, + "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 + }, + "for-own": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", + "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", + "dev": true, + "requires": { + "for-in": "^1.0.1" + } + }, + "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" + } + }, + "fs-mkdirp-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-1.0.0.tgz", + "integrity": "sha1-C3gV/DIBxqaeFNuYzgmMFpNSWes=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "through2": "^2.0.3" + } + }, + "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.9", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz", + "integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==", + "dev": true, + "optional": true, + "requires": { + "nan": "^2.12.1", + "node-pre-gyp": "^0.12.0" + }, + "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.1", + "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": "4.1.1", + "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.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "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.3", + "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.1", + "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.3", + "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": "0.0.8", + "bundled": true, + "dev": true, + "optional": true + }, + "minipass": { + "version": "2.3.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.2.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "needle": { + "version": "2.3.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "debug": "^4.1.0", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.12.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" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "npm-bundled": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "optional": true + }, + "npm-packlist": { + "version": "1.4.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^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.0", + "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" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.3.6", + "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.6.3", + "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.0", + "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.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.3.4", + "minizlib": "^1.1.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.2" + } + }, + "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.0.3", + "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" + } + }, + "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" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "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-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" + } + }, + "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" + } + } + } + }, + "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-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-stream": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-6.1.0.tgz", + "integrity": "sha1-cEXJlBOz65SIjYOrRtC0BMx73eQ=", + "dev": true, + "requires": { + "extend": "^3.0.0", + "glob": "^7.1.1", + "glob-parent": "^3.1.0", + "is-negated-glob": "^1.0.0", + "ordered-read-streams": "^1.0.0", + "pumpify": "^1.3.5", + "readable-stream": "^2.1.5", + "remove-trailing-separator": "^1.0.1", + "to-absolute-glob": "^2.0.0", + "unique-stream": "^2.0.2" + } + }, + "glob-watcher": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-5.0.3.tgz", + "integrity": "sha512-8tWsULNEPHKQ2MR4zXuzSmqbdyV5PtwwCaWSGQ1WwHsJ07ilNeN1JB8ntxhckbnpSHaf9dXFUHzIWvm1I13dsg==", + "dev": true, + "requires": { + "anymatch": "^2.0.0", + "async-done": "^1.2.0", + "chokidar": "^2.0.0", + "is-negated-glob": "^1.0.0", + "just-debounce": "^1.0.0", + "object.defaults": "^1.1.0" + } + }, + "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 + }, + "globby": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/globby/-/globby-10.0.1.tgz", + "integrity": "sha512-sSs4inE1FB2YQiymcmTv6NWENryABjUNPeWhOvmn4SjtKybglsyPZxFB3U1/+L1bYi0rNZDqCLlHyLYDl1Pq5A==", + "dev": true, + "requires": { + "@types/glob": "^7.1.1", + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.0.3", + "glob": "^7.1.3", + "ignore": "^5.1.1", + "merge2": "^1.2.3", + "slash": "^3.0.0" + } + }, + "globule": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/globule/-/globule-1.2.1.tgz", + "integrity": "sha512-g7QtgWF4uYSL5/dn71WxubOrS7JVGCnFPEnoeChJmBnyR9Mw8nGoEwOgJL/RC2Te0WhbsEUCejfH8SZNJ+adYQ==", + "dev": true, + "requires": { + "glob": "~7.1.1", + "lodash": "~4.17.10", + "minimatch": "~3.0.2" + } + }, + "glogg": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/glogg/-/glogg-1.0.2.tgz", + "integrity": "sha512-5mwUoSuBk44Y4EshyiqcH95ZntbDdTQqA3QYSrxmzj28Ai0vXBGMH1ApSANH14j2sIRtqCEyg6PfsuP7ElOEDA==", + "dev": true, + "requires": { + "sparkles": "^1.0.0" + } + }, + "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 + }, + "gulp": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/gulp/-/gulp-4.0.2.tgz", + "integrity": "sha512-dvEs27SCZt2ibF29xYgmnwwCYZxdxhQ/+LFWlbAW8y7jt68L/65402Lz3+CKy0Ov4rOs+NERmDq7YlZaDqUIfA==", + "dev": true, + "requires": { + "glob-watcher": "^5.0.3", + "gulp-cli": "^2.2.0", + "undertaker": "^1.2.1", + "vinyl-fs": "^3.0.0" + } + }, + "gulp-babel": { + "version": "8.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gulp-babel/-/gulp-babel-8.0.0-beta.2.tgz", + "integrity": "sha512-GTC2PxAXWkp6u1fP+C5+kn5biQ0dKGhkOSSXvKAf3ykF0+R3tevmLm/zSIkc1+S7U1JwH3XTvuMwRL6LD+sEiw==", + "dev": true, + "requires": { + "plugin-error": "^1.0.1", + "replace-ext": "^1.0.0", + "through2": "^2.0.0", + "vinyl-sourcemaps-apply": "^0.2.0" + } + }, + "gulp-cli": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-2.2.0.tgz", + "integrity": "sha512-rGs3bVYHdyJpLqR0TUBnlcZ1O5O++Zs4bA0ajm+zr3WFCfiSLjGwoCBqFs18wzN+ZxahT9DkOK5nDf26iDsWjA==", + "dev": true, + "requires": { + "ansi-colors": "^1.0.1", + "archy": "^1.0.0", + "array-sort": "^1.0.0", + "color-support": "^1.1.3", + "concat-stream": "^1.6.0", + "copy-props": "^2.0.1", + "fancy-log": "^1.3.2", + "gulplog": "^1.0.0", + "interpret": "^1.1.0", + "isobject": "^3.0.1", + "liftoff": "^3.1.0", + "matchdep": "^2.0.0", + "mute-stdout": "^1.0.0", + "pretty-hrtime": "^1.0.0", + "replace-homedir": "^1.0.0", + "semver-greatest-satisfied-range": "^1.1.0", + "v8flags": "^3.0.1", + "yargs": "^7.1.0" + } + }, + "gulp-concat": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/gulp-concat/-/gulp-concat-2.6.1.tgz", + "integrity": "sha1-Yz0WyV2IUEYorQJmVmPO5aR5M1M=", + "dev": true, + "requires": { + "concat-with-sourcemaps": "^1.0.0", + "through2": "^2.0.0", + "vinyl": "^2.0.0" + } + }, + "gulp-sass": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/gulp-sass/-/gulp-sass-4.0.2.tgz", + "integrity": "sha512-q8psj4+aDrblJMMtRxihNBdovfzGrXJp1l4JU0Sz4b/Mhsi2DPrKFYCGDwjIWRENs04ELVHxdOJQ7Vs98OFohg==", + "dev": true, + "requires": { + "chalk": "^2.3.0", + "lodash.clonedeep": "^4.3.2", + "node-sass": "^4.8.3", + "plugin-error": "^1.0.1", + "replace-ext": "^1.0.0", + "strip-ansi": "^4.0.0", + "through2": "^2.0.0", + "vinyl-sourcemaps-apply": "^0.2.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" + } + } + } + }, + "gulplog": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gulplog/-/gulplog-1.0.0.tgz", + "integrity": "sha1-4oxNRdBey77YGDY86PnFkmIp/+U=", + "dev": true, + "requires": { + "glogg": "^1.0.0" + } + }, + "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-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" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + } + } + }, + "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" + } + } + } + }, + "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 + }, + "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" + } + }, + "ignore": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.2.tgz", + "integrity": "sha512-vdqWBp7MyzdmHkkRWV5nY+PfGRbYbahfuvsBCh277tq+w9zyNi7h5CYJCK0kmzti9kU+O/cB7sE8HvKv6aXAKQ==", + "dev": true + }, + "in-publish": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.0.tgz", + "integrity": "sha1-4g/146KvwmkDILbcVSaCqcf631E=", + "dev": true + }, + "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" + } + }, + "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==", + "dev": true, + "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-absolute": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", + "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", + "dev": true, + "requires": { + "is-relative": "^1.0.0", + "is-windows": "^1.0.1" + } + }, + "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-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-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.0.2", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", + "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "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-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-negated-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", + "integrity": "sha1-aRC8pdqMleeEtXUbl2z1oQ/uNtI=", + "dev": true + }, + "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" + } + }, + "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-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-relative": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", + "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", + "dev": true, + "requires": { + "is-unc-path": "^1.0.0" + } + }, + "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-unc-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", + "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", + "dev": true, + "requires": { + "unc-path-regex": "^0.1.2" + } + }, + "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-valid-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-1.0.0.tgz", + "integrity": "sha1-Kb8+/3Ab4tTTFdusw5vDn+j2Aao=", + "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 + }, + "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 + }, + "js-base64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.1.tgz", + "integrity": "sha512-M7kLczedRMYX4L8Mdh4MzyAMM9O5osx+4FcOQuTvr3A9F2D9S5JXheN0ewNbrvK2UatkTRhL5ejGmGSjNMiZuw==", + "dev": true + }, + "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==", + "dev": true + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "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-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-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "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.0", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.0.tgz", + "integrity": "sha512-8Mh9h6xViijj36g7Dxi+Y4S6hNGV96vcJZr/SrlHh1LR/pEn/8j/+qIBbs44YKl69Lrfctp4QD+AdWLTMqEZAQ==", + "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" + } + }, + "just-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/just-debounce/-/just-debounce-1.0.0.tgz", + "integrity": "sha1-h/zPrv/AtozRnVX2cilD+SnqNeo=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + }, + "last-run": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/last-run/-/last-run-1.1.1.tgz", + "integrity": "sha1-RblpQsF7HHnHchmCWbqUO+v4yls=", + "dev": true, + "requires": { + "default-resolution": "^2.0.0", + "es6-weak-map": "^2.0.1" + } + }, + "lazystream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz", + "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=", + "dev": true, + "requires": { + "readable-stream": "^2.0.5" + } + }, + "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" + } + }, + "lead": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lead/-/lead-1.0.0.tgz", + "integrity": "sha1-bxT5mje+Op3XhPVJVpDlkDRm7kI=", + "dev": true, + "requires": { + "flush-write-stream": "^1.0.2" + } + }, + "liftoff": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-3.1.0.tgz", + "integrity": "sha512-DlIPlJUkCV0Ips2zf2pJP0unEoT1kwYhiiPUGF3s/jtxTCjziNLoiVVh+jqWOWeFi6mmwQ5fNxvAUyPad4Dfog==", + "dev": true, + "requires": { + "extend": "^3.0.0", + "findup-sync": "^3.0.0", + "fined": "^1.0.1", + "flagged-respawn": "^1.0.0", + "is-plain-object": "^2.0.4", + "object.map": "^1.0.0", + "rechoir": "^0.6.2", + "resolve": "^1.1.7" + } + }, + "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 + } + } + }, + "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.14", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.14.tgz", + "integrity": "sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw==", + "dev": true + }, + "lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", + "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==", + "dev": true, + "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" + } + }, + "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" + } + }, + "make-iterator": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz", + "integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==", + "dev": true, + "requires": { + "kind-of": "^6.0.2" + } + }, + "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" + } + }, + "matchdep": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/matchdep/-/matchdep-2.0.0.tgz", + "integrity": "sha1-xvNINKDY28OzfCfui7yyfHd1WC4=", + "dev": true, + "requires": { + "findup-sync": "^2.0.0", + "micromatch": "^3.0.4", + "resolve": "^1.4.0", + "stack-trace": "0.0.10" + }, + "dependencies": { + "findup-sync": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-2.0.0.tgz", + "integrity": "sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw=", + "dev": true, + "requires": { + "detect-file": "^1.0.0", + "is-glob": "^3.1.0", + "micromatch": "^3.0.4", + "resolve-dir": "^1.0.1" + } + }, + "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" + } + } + } + }, + "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" + } + }, + "merge2": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.2.3.tgz", + "integrity": "sha512-gdUU1Fwj5ep4kplwcmftruWofEFt6lfpkkr3h860CXbAB9c3hGb55EOL2ali0Td5oebvW0E1+3Sr+Ur7XfKpRA==", + "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" + } + }, + "mime-db": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", + "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==", + "dev": true + }, + "mime-types": { + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", + "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", + "dev": true, + "requires": { + "mime-db": "1.40.0" + } + }, + "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.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "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.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + } + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "mute-stdout": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mute-stdout/-/mute-stdout-1.0.1.tgz", + "integrity": "sha512-kDcwXR4PS7caBpuRYYBUz9iVixUk3anO3f5OYFiIPwK/20vCzKCHyKoulbiDY1S53zD2bxUpxN/IJ+TnXjfvxg==", + "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" + } + }, + "next-tick": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", + "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", + "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": { + "semver": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", + "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", + "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-releases": { + "version": "1.1.25", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.25.tgz", + "integrity": "sha512-fI5BXuk83lKEoZDdH3gRhtsNgh05/wZacuXkgbiYkceE7+QIMXOg98n9ZV7mz27B+kFHnqHcUpscZZlGRSmTpQ==", + "dev": true, + "requires": { + "semver": "^5.3.0" + } + }, + "node-sass": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.12.0.tgz", + "integrity": "sha512-A1Iv4oN+Iel6EPv77/HddXErL2a+gZ4uBeZUy+a8O35CFYTXhgA8MgLCWBtwpGZdCvTvQ9d+bQxX/QC36GDPpQ==", + "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.11", + "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-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": "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" + } + }, + "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" + } + }, + "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" + } + }, + "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" + } + }, + "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 + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "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" + } + }, + "now-and-later": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-2.0.1.tgz", + "integrity": "sha512-KGvQ0cB70AQfg107Xvs/Fbu+dGmZoTRJp2TaPwcwQm3/7PteUyN2BCgk8KBMPGBUXZdVwyWS8fDCGFygBm19UQ==", + "dev": true, + "requires": { + "once": "^1.3.2" + } + }, + "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 + }, + "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=", + "dev": true + }, + "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-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.defaults": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", + "integrity": "sha1-On+GgzS0B96gbaFtiNXNKeQ1/s8=", + "dev": true, + "requires": { + "array-each": "^1.0.1", + "array-slice": "^1.0.0", + "for-own": "^1.0.0", + "isobject": "^3.0.0" + } + }, + "object.map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz", + "integrity": "sha1-z4Plncj8wK1fQlDh94s7gb2AHTc=", + "dev": true, + "requires": { + "for-own": "^1.0.0", + "make-iterator": "^1.0.0" + } + }, + "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" + } + }, + "object.reduce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.reduce/-/object.reduce-1.0.1.tgz", + "integrity": "sha1-b+NI8qx/oPlcpiEiZZkJaCW7A60=", + "dev": true, + "requires": { + "for-own": "^1.0.0", + "make-iterator": "^1.0.0" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "ordered-read-streams": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz", + "integrity": "sha1-d8DLN8QVJdZBZtmQ/61+xqDhNj4=", + "dev": true, + "requires": { + "readable-stream": "^2.0.1" + } + }, + "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-limit": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", + "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", + "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-map": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", + "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", + "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 + }, + "parse-filepath": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", + "integrity": "sha1-pjISf1Oq89FYdvWHLz/6x2PWyJE=", + "dev": true, + "requires": { + "is-absolute": "^1.0.0", + "map-cache": "^0.2.0", + "path-root": "^0.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-node-version": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", + "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", + "dev": true + }, + "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 + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", + "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-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-root": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", + "integrity": "sha1-mkpoFMrBwM1zNgqV8yCDyOpHRbc=", + "dev": true, + "requires": { + "path-root-regex": "^0.1.0" + } + }, + "path-root-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", + "integrity": "sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0=", + "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 + } + } + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "dev": true + }, + "picomatch": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.0.7.tgz", + "integrity": "sha512-oLHIdio3tZ0qH76NybpeneBhYVj0QFTfXEFTc/B3zKQspYfYYkWYgFsmzo+4kvId/bQRcNkVeguI3y+CD22BtA==", + "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" + } + }, + "plugin-error": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz", + "integrity": "sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA==", + "dev": true, + "requires": { + "ansi-colors": "^1.0.1", + "arr-diff": "^4.0.0", + "arr-union": "^3.1.0", + "extend-shallow": "^3.0.2" + } + }, + "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 + }, + "prettier": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.18.2.tgz", + "integrity": "sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw==", + "dev": true + }, + "pretty-hrtime": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", + "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=", + "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-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 + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", + "dev": true + }, + "psl": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.2.0.tgz", + "integrity": "sha512-GEn74ZffufCmkDDLNcl3uuyF/aSD6exEyh1v/ZSdAomB82t6G9hzJVRx0jBmLDW+VfZqks3aScmMw9DszwUalA==", + "dev": true + }, + "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": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "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 + }, + "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" + } + }, + "rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "dev": true, + "requires": { + "resolve": "^1.1.6" + } + }, + "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" + } + }, + "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-transform": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.0.tgz", + "integrity": "sha512-rtOelq4Cawlbmq9xuMR5gdFmv7ku/sFoB7sRiywx7aq53bc52b4j6zvH7Te1Vt/X2YveDKnCGUbioieU7FEL3w==", + "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" + } + }, + "regexp-tree": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.11.tgz", + "integrity": "sha512-7/l/DgapVVDzZobwMCCgMlqiqyLFJ0cduo/j+3BcDJIB+yJdsYCfKuI3l/04NV+H/rfNRdPIDbXNZHM9XvQatg==", + "dev": true + }, + "regexpu-core": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.5.4.tgz", + "integrity": "sha512-BtizvGtFQKGPUcTy56o3nk1bGRp4SZOTYrDtGNlqCQufptV5IkkLN6Emw+yunAJjzf+C9FQFtvq7IoA3+oMYHQ==", + "dev": true, + "requires": { + "regenerate": "^1.4.0", + "regenerate-unicode-properties": "^8.0.2", + "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.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.0.tgz", + "integrity": "sha512-RnIrLhrXCX5ow/E5/Mh2O4e/oa1/jW0eaBKTSy3LaCj+M3Bqvm97GWDp2yUtzIs4LEn65zR2yiYGFqb2ApnzDA==", + "dev": true + }, + "regjsparser": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.6.0.tgz", + "integrity": "sha512-RQ7YyokLiQBomUJuUG8iGVvkgOLxwyZM8k6d3q5SAXpg4r5TZJZigKFvC6PpD+qQ98bCDC5YelPeA3EucDoNeQ==", + "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-bom-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/remove-bom-buffer/-/remove-bom-buffer-3.0.0.tgz", + "integrity": "sha512-8v2rWhaakv18qcvNeli2mZ/TMTL2nEyAKRvzo1WtnZBl15SHyEhrCu2/xKlJyUFKHiHgfXIyuY6g2dObJJycXQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5", + "is-utf8": "^0.2.1" + } + }, + "remove-bom-stream": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/remove-bom-stream/-/remove-bom-stream-1.2.0.tgz", + "integrity": "sha1-BfGlk/FuQuH7kOv1nejlaVJflSM=", + "dev": true, + "requires": { + "remove-bom-buffer": "^3.0.0", + "safe-buffer": "^5.1.0", + "through2": "^2.0.3" + } + }, + "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" + } + }, + "replace-ext": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", + "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=", + "dev": true + }, + "replace-homedir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/replace-homedir/-/replace-homedir-1.0.0.tgz", + "integrity": "sha1-6H9tUTuSjd6AgmDBK+f+xv9ueYw=", + "dev": true, + "requires": { + "homedir-polyfill": "^1.0.1", + "is-absolute": "^1.0.0", + "remove-trailing-separator": "^1.1.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" + } + }, + "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-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-options": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/resolve-options/-/resolve-options-1.1.0.tgz", + "integrity": "sha1-MrueOcBtZzONyTeMDW1gdFZq0TE=", + "dev": true, + "requires": { + "value-or-function": "^3.0.0" + } + }, + "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 + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "run-parallel": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.9.tgz", + "integrity": "sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q==", + "dev": true + }, + "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 + }, + "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" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "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 + }, + "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" + } + }, + "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 + }, + "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-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" + } + }, + "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" + } + }, + "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" + } + }, + "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 + }, + "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" + } + }, + "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" + } + }, + "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 + }, + "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" + } + }, + "y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=", + "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" + } + } + } + }, + "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 + }, + "semver-greatest-satisfied-range": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/semver-greatest-satisfied-range/-/semver-greatest-satisfied-range-1.1.0.tgz", + "integrity": "sha1-E+jCZYq5aRywzXEJMkAoDTb3els=", + "dev": true, + "requires": { + "sver-compat": "^1.5.0" + } + }, + "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" + } + } + } + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "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" + } + } + } + }, + "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.12", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.12.tgz", + "integrity": "sha512-4h2Pbvyy15EE02G+JOZpUCmqWJuqrs+sEkzewTm++BPi7Hvn/HwcqLAcNxYAyI0x13CpPPn+kMjl+hplXMHITQ==", + "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 + }, + "sparkles": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sparkles/-/sparkles-1.0.1.tgz", + "integrity": "sha512-dSO0DDYUahUt/0/pD/Is3VIm5TGJjludZ0HVymmhYF6eNA53PVLhnUk0znSYbH8IYBuJdCE+1luR22jNLMaQdw==", + "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" + } + }, + "stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=", + "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" + } + }, + "stream-exhaust": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stream-exhaust/-/stream-exhaust-1.0.2.tgz", + "integrity": "sha512-b/qaq/GlBK5xaq1yrK9/zFcyRSTNxmcZwFLGSTG0mXgZl/4Z6GgiyYOXOvY7N3eEvFRAG1bkDRz5EPGSvPYQlw==", + "dev": true + }, + "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 + }, + "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_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-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" + } + }, + "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" + } + }, + "sver-compat": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/sver-compat/-/sver-compat-1.5.0.tgz", + "integrity": "sha1-PPh9/rTQe0o/FIJ7wYaz/QxkXNg=", + "dev": true, + "requires": { + "es6-iterator": "^2.0.1", + "es6-symbol": "^3.1.1" + } + }, + "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" + } + }, + "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" + } + }, + "through2-filter": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-3.0.0.tgz", + "integrity": "sha512-jaRjI2WxN3W1V8/FMZ9HKIBXixtiqs3SQSX4/YGIiP3gL6djW48VoZq9tDqeCWs3MT8YY5wb/zli8VW8snY1CA==", + "dev": true, + "requires": { + "through2": "~2.0.0", + "xtend": "~4.0.0" + } + }, + "time-stamp": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/time-stamp/-/time-stamp-1.1.0.tgz", + "integrity": "sha1-dkpaEa9QVhkhsTPztE5hhofg9cM=", + "dev": true + }, + "to-absolute-glob": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz", + "integrity": "sha1-GGX0PZ50sIItufFFt4z/fQ98hJs=", + "dev": true, + "requires": { + "is-absolute": "^1.0.0", + "is-negated-glob": "^1.0.0" + } + }, + "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" + } + }, + "to-through": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-through/-/to-through-2.0.0.tgz", + "integrity": "sha1-/JKtq6ByZHvAtn1rA2ZKoZUJOvY=", + "dev": true, + "requires": { + "through2": "^2.0.3" + } + }, + "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" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "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 + }, + "trim-right": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", + "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", + "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" + } + }, + "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": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/type/-/type-1.0.1.tgz", + "integrity": "sha512-MAM5dBMJCJNKs9E7JXo4CXRAansRfG0nlJxW7Wf6GZzSOvH31zClSaHdIMWLehe/EGMBkqeC55rrkaOr5Oo7Nw==", + "dev": true + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true + }, + "unc-path-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", + "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=", + "dev": true + }, + "undertaker": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/undertaker/-/undertaker-1.2.1.tgz", + "integrity": "sha512-71WxIzDkgYk9ZS+spIB8iZXchFhAdEo2YU8xYqBYJ39DIUIqziK78ftm26eecoIY49X0J2MLhG4hr18Yp6/CMA==", + "dev": true, + "requires": { + "arr-flatten": "^1.0.1", + "arr-map": "^2.0.0", + "bach": "^1.0.0", + "collection-map": "^1.0.0", + "es6-weak-map": "^2.0.1", + "last-run": "^1.1.0", + "object.defaults": "^1.0.0", + "object.reduce": "^1.0.0", + "undertaker-registry": "^1.0.0" + } + }, + "undertaker-registry": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/undertaker-registry/-/undertaker-registry-1.0.1.tgz", + "integrity": "sha1-XkvaMI5KiirlhPm5pDWaSZglzFA=", + "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" + } + }, + "unique-stream": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.3.1.tgz", + "integrity": "sha512-2nY4TnBE70yoxHkDli7DMazpWiP7xMdCYqU2nBRO0UB+ZpEkGsSija7MvmvnZFUeC+mrgiUfcHSr3LmRFIg4+A==", + "dev": true, + "requires": { + "json-stable-stringify-without-jsonify": "^1.0.1", + "through2-filter": "^3.0.0" + } + }, + "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" + } + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", + "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 + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", + "dev": true + }, + "v8flags": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.1.3.tgz", + "integrity": "sha512-amh9CCg3ZxkzQ48Mhcb8iX7xpAfYJgePHxWMQCBWECpOSqJUXgY26ncA61UTV0BkPqfhcy6mzwCIoP4ygxpW8w==", + "dev": true, + "requires": { + "homedir-polyfill": "^1.0.1" + } + }, + "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" + } + }, + "value-or-function": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-3.0.0.tgz", + "integrity": "sha1-HCQ6ULWVwb5Up1S/7OhWO5/42BM=", + "dev": true + }, + "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" + } + }, + "vinyl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.0.tgz", + "integrity": "sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg==", + "dev": true, + "requires": { + "clone": "^2.1.1", + "clone-buffer": "^1.0.0", + "clone-stats": "^1.0.0", + "cloneable-readable": "^1.0.0", + "remove-trailing-separator": "^1.0.1", + "replace-ext": "^1.0.0" + } + }, + "vinyl-fs": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-3.0.3.tgz", + "integrity": "sha512-vIu34EkyNyJxmP0jscNzWBSygh7VWhqun6RmqVfXePrOwi9lhvRs//dOaGOTRUQr4tx7/zd26Tk5WeSVZitgng==", + "dev": true, + "requires": { + "fs-mkdirp-stream": "^1.0.0", + "glob-stream": "^6.1.0", + "graceful-fs": "^4.0.0", + "is-valid-glob": "^1.0.0", + "lazystream": "^1.0.0", + "lead": "^1.0.0", + "object.assign": "^4.0.4", + "pumpify": "^1.3.5", + "readable-stream": "^2.3.3", + "remove-bom-buffer": "^3.0.0", + "remove-bom-stream": "^1.2.0", + "resolve-options": "^1.1.0", + "through2": "^2.0.0", + "to-through": "^2.0.0", + "value-or-function": "^3.0.0", + "vinyl": "^2.0.0", + "vinyl-sourcemap": "^1.1.0" + } + }, + "vinyl-sourcemap": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/vinyl-sourcemap/-/vinyl-sourcemap-1.1.0.tgz", + "integrity": "sha1-kqgAWTo4cDqM2xHYswCtS+Y7PhY=", + "dev": true, + "requires": { + "append-buffer": "^1.0.2", + "convert-source-map": "^1.5.0", + "graceful-fs": "^4.1.6", + "normalize-path": "^2.1.1", + "now-and-later": "^2.0.0", + "remove-bom-buffer": "^3.0.0", + "vinyl": "^2.0.0" + } + }, + "vinyl-sourcemaps-apply": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/vinyl-sourcemaps-apply/-/vinyl-sourcemaps-apply-0.2.1.tgz", + "integrity": "sha1-q2VJ1h0XLCsbh75cUI0jnI74dwU=", + "dev": true, + "requires": { + "source-map": "^0.5.1" + } + }, + "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" + }, + "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 + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "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" + } + } + } + }, + "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 + }, + "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 + }, + "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..5e339a4 --- /dev/null +++ b/package.json @@ -0,0 +1,40 @@ +{ + "name": "newsreader", + "version": "0.1.0", + "description": "Application for viewing RSS feeds", + "main": "index.js", + "scripts": { + "lint": "prettier \"src/newsreader/**/*.js\" --check", + "format": "prettier \"src/newsreader/**/*.js\" --write" + }, + "repository": { + "type": "git", + "url": "[git@git.fudiggity.nl:5000]:sonny/newsreader.git" + }, + "author": "Sonny", + "license": "GPL-3.0-or-later", + "prettier": { + "semi": true, + "trailingComma": "es5", + "singleQuote": false, + "printWidth": 80, + "tabWidth": 2, + "useTabs": false, + "bracketSpacing": false, + "arrowParens": "always" + }, + "dependencies": {}, + "devDependencies": { + "@babel/core": "^7.5.4", + "@babel/preset-env": "^7.5.4", + "@babel/register": "^7.4.4", + "del": "^5.0.0", + "gulp": "^4.0.2", + "gulp-babel": "^8.0.0-beta.2", + "gulp-cli": "^2.2.0", + "gulp-concat": "^2.6.1", + "gulp-sass": "^4.0.2", + "node-sass": "^4.12.0", + "prettier": "^1.18.2" + } +} diff --git a/src/newsreader/accounts/templates/accounts/login.html b/src/newsreader/accounts/templates/accounts/login.html new file mode 100644 index 0000000..c9a9bca --- /dev/null +++ b/src/newsreader/accounts/templates/accounts/login.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} + +{% load static %} + +{% block head %} + +{% endblock %} + +{% block content %} +

+ +
+{% endblock %} diff --git a/src/newsreader/accounts/urls.py b/src/newsreader/accounts/urls.py new file mode 100644 index 0000000..61593ed --- /dev/null +++ b/src/newsreader/accounts/urls.py @@ -0,0 +1,9 @@ +from django.urls import include, path + +from newsreader.accounts.views import LoginView, LogoutView + + +urlpatterns = [ + path("login/", LoginView.as_view(), name="login"), + path("logout/", LogoutView.as_view(), name="logout"), +] diff --git a/src/newsreader/accounts/views.py b/src/newsreader/accounts/views.py index 60f00ef..ae33591 100644 --- a/src/newsreader/accounts/views.py +++ b/src/newsreader/accounts/views.py @@ -1 +1,16 @@ -# Create your views here. +from django.contrib.auth.views import LoginView as DjangoLoginView +from django.contrib.auth.views import LogoutView as DjangoLogoutView +from django.urls import reverse_lazy + + +# TODO redirect to homepage when logged in +class LoginView(DjangoLoginView): + template_name = "accounts/login.html" + + def get_success_url(self): + # TODO redirect to homepage + return reverse_lazy("admin:index") + + +class LogoutView(DjangoLogoutView): + next_page = reverse_lazy("accounts:login") diff --git a/src/newsreader/conf/base.py b/src/newsreader/conf/base.py index 812707b..cffe291 100644 --- a/src/newsreader/conf/base.py +++ b/src/newsreader/conf/base.py @@ -61,7 +61,7 @@ ROOT_URLCONF = "newsreader.urls" TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": [], + "DIRS": [os.path.join(BASE_DIR, "templates")], "APP_DIRS": True, "OPTIONS": { "context_processors": [ diff --git a/src/newsreader/static/src/scss/accounts/components/form/_form.scss b/src/newsreader/static/src/scss/accounts/components/form/_form.scss new file mode 100644 index 0000000..aa2fb6c --- /dev/null +++ b/src/newsreader/static/src/scss/accounts/components/form/_form.scss @@ -0,0 +1,27 @@ +.login-form { + @extend .form; + + h4 { + margin: 0; + padding: 20px 24px 5px 24px; + } + + &__fieldset { + @extend .form__fieldset; + } + + &__fieldset * { + padding: 10px; + } + + &__fieldset:last-child { + flex-direction: row-reverse; + justify-content: space-between; + } + + &__fieldset:last-child { + .button { + padding: 10px 50px; + } + } +} diff --git a/src/newsreader/static/src/scss/accounts/components/form/index.scss b/src/newsreader/static/src/scss/accounts/components/form/index.scss new file mode 100644 index 0000000..dc477a7 --- /dev/null +++ b/src/newsreader/static/src/scss/accounts/components/form/index.scss @@ -0,0 +1 @@ +@import "form"; diff --git a/src/newsreader/static/src/scss/accounts/components/index.scss b/src/newsreader/static/src/scss/accounts/components/index.scss new file mode 100644 index 0000000..246a1a1 --- /dev/null +++ b/src/newsreader/static/src/scss/accounts/components/index.scss @@ -0,0 +1,2 @@ +@import "./main/index"; +@import "./form/index"; diff --git a/src/newsreader/static/src/scss/accounts/components/main/_main.scss b/src/newsreader/static/src/scss/accounts/components/main/_main.scss new file mode 100644 index 0000000..1efb986 --- /dev/null +++ b/src/newsreader/static/src/scss/accounts/components/main/_main.scss @@ -0,0 +1,8 @@ +.main { + @extend .main; + + margin: 5% auto; + width: 50%; + + border-radius: 4px; +} diff --git a/src/newsreader/static/src/scss/accounts/components/main/index.scss b/src/newsreader/static/src/scss/accounts/components/main/index.scss new file mode 100644 index 0000000..bdb4ce0 --- /dev/null +++ b/src/newsreader/static/src/scss/accounts/components/main/index.scss @@ -0,0 +1 @@ +@import "main"; diff --git a/src/newsreader/static/src/scss/accounts/index.scss b/src/newsreader/static/src/scss/accounts/index.scss new file mode 100644 index 0000000..d0a748c --- /dev/null +++ b/src/newsreader/static/src/scss/accounts/index.scss @@ -0,0 +1,4 @@ +@import "../partials/variables"; +@import "../components/index"; + +@import "./components/index"; diff --git a/src/newsreader/static/src/scss/components/body/_body.scss b/src/newsreader/static/src/scss/components/body/_body.scss new file mode 100644 index 0000000..f0829bf --- /dev/null +++ b/src/newsreader/static/src/scss/components/body/_body.scss @@ -0,0 +1,5 @@ +.body { + margin: 0; + padding: 0; + background-color: $gainsboro; +} diff --git a/src/newsreader/static/src/scss/components/body/index.scss b/src/newsreader/static/src/scss/components/body/index.scss new file mode 100644 index 0000000..533e39e --- /dev/null +++ b/src/newsreader/static/src/scss/components/body/index.scss @@ -0,0 +1 @@ +@import "body"; diff --git a/src/newsreader/static/src/scss/components/button/_button.scss b/src/newsreader/static/src/scss/components/button/_button.scss new file mode 100644 index 0000000..4047b6c --- /dev/null +++ b/src/newsreader/static/src/scss/components/button/_button.scss @@ -0,0 +1,28 @@ +.button { + display: flex; + + align-items: center; + justify-content: center; + + padding: 10px 50px; + + width: 50px; + + border: none; + border-radius: 2px; + + font-family: $button-font; + + &:hover { + cursor: pointer; + } + + &--confirm { + color: $white; + background-color: $confirm-green; + + &:hover { + background-color: lighten($confirm-green, +5%); + } + } +} diff --git a/src/newsreader/static/src/scss/components/button/index.scss b/src/newsreader/static/src/scss/components/button/index.scss new file mode 100644 index 0000000..ac3b5de --- /dev/null +++ b/src/newsreader/static/src/scss/components/button/index.scss @@ -0,0 +1 @@ +@import "button"; diff --git a/src/newsreader/static/src/scss/components/error/_error.scss b/src/newsreader/static/src/scss/components/error/_error.scss new file mode 100644 index 0000000..bc726d2 --- /dev/null +++ b/src/newsreader/static/src/scss/components/error/_error.scss @@ -0,0 +1,5 @@ +%error { + background-color: $error-red; + color: $white; + list-style: none; +} diff --git a/src/newsreader/static/src/scss/components/error/_errorlist.scss b/src/newsreader/static/src/scss/components/error/_errorlist.scss new file mode 100644 index 0000000..6f13d9a --- /dev/null +++ b/src/newsreader/static/src/scss/components/error/_errorlist.scss @@ -0,0 +1,3 @@ +.errorlist { + @extend %error; +} diff --git a/src/newsreader/static/src/scss/components/error/index.scss b/src/newsreader/static/src/scss/components/error/index.scss new file mode 100644 index 0000000..baf8ad8 --- /dev/null +++ b/src/newsreader/static/src/scss/components/error/index.scss @@ -0,0 +1,2 @@ +@import "error"; +@import "errorlist"; diff --git a/src/newsreader/static/src/scss/components/form/_form.scss b/src/newsreader/static/src/scss/components/form/_form.scss new file mode 100644 index 0000000..8b0b3f6 --- /dev/null +++ b/src/newsreader/static/src/scss/components/form/_form.scss @@ -0,0 +1,13 @@ +.form { + display: flex; + flex-direction: column; + + font-family: $form-font; + + &__fieldset { + display: flex; + flex-direction: column; + + border: none; + } +} diff --git a/src/newsreader/static/src/scss/components/form/index.scss b/src/newsreader/static/src/scss/components/form/index.scss new file mode 100644 index 0000000..dc477a7 --- /dev/null +++ b/src/newsreader/static/src/scss/components/form/index.scss @@ -0,0 +1 @@ +@import "form"; diff --git a/src/newsreader/static/src/scss/components/index.scss b/src/newsreader/static/src/scss/components/index.scss new file mode 100644 index 0000000..14ddc8e --- /dev/null +++ b/src/newsreader/static/src/scss/components/index.scss @@ -0,0 +1,6 @@ +@import "./body/index"; +@import "./button/index"; +@import "./form/index"; +@import "./main/index"; +@import "./navbar/index"; +@import "./error/index"; diff --git a/src/newsreader/static/src/scss/components/input/input.scss b/src/newsreader/static/src/scss/components/input/input.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/newsreader/static/src/scss/components/main/_main.scss b/src/newsreader/static/src/scss/components/main/_main.scss new file mode 100644 index 0000000..1c4ed2e --- /dev/null +++ b/src/newsreader/static/src/scss/components/main/_main.scss @@ -0,0 +1,4 @@ +.main { + margin: 1% 10% 5% 10%; + background-color: $white; +} diff --git a/src/newsreader/static/src/scss/components/main/index.scss b/src/newsreader/static/src/scss/components/main/index.scss new file mode 100644 index 0000000..bdb4ce0 --- /dev/null +++ b/src/newsreader/static/src/scss/components/main/index.scss @@ -0,0 +1 @@ +@import "main"; diff --git a/src/newsreader/static/src/scss/components/navbar/_navbar.scss b/src/newsreader/static/src/scss/components/navbar/_navbar.scss new file mode 100644 index 0000000..d0ea3b9 --- /dev/null +++ b/src/newsreader/static/src/scss/components/navbar/_navbar.scss @@ -0,0 +1,43 @@ +.nav { + display: flex; + justify-content: center; + + 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/static/src/scss/components/navbar/index.scss b/src/newsreader/static/src/scss/components/navbar/index.scss new file mode 100644 index 0000000..b45a5a0 --- /dev/null +++ b/src/newsreader/static/src/scss/components/navbar/index.scss @@ -0,0 +1 @@ +@import "navbar"; diff --git a/src/newsreader/static/src/scss/partials/_variables.scss b/src/newsreader/static/src/scss/partials/_variables.scss new file mode 100644 index 0000000..d983d2a --- /dev/null +++ b/src/newsreader/static/src/scss/partials/_variables.scss @@ -0,0 +1,26 @@ +@import url("https://fonts.googleapis.com/css?family=IBM+Plex+Sans:600&display=swap"); +@import url('https://fonts.googleapis.com/css?family=Barlow&display=swap'); + +$button-font: "IBM Plex Sans", sans-serif; +$form-font: "Barlow", sans-serif; + +/* colors */ +$white: rgba(255, 255, 255, 1); + +$confirm-green: rgba(46,204,113, 1); +$error-red: rgba(231,76,60, 1); + +// light blue +$azureish-white: rgba(205, 230, 245, 1); + +// dark blue +$pewter-blue: rgba(141, 167, 190, 1); + +// light gray +$gainsboro: rgba(224, 221, 220, 1); + +// medium gray +$roman-silver: rgba(135, 145, 158, 1); + +//dark gray +$nickel: rgba(112, 112, 120, 1); diff --git a/src/newsreader/templates/base.html b/src/newsreader/templates/base.html new file mode 100644 index 0000000..460ab8f --- /dev/null +++ b/src/newsreader/templates/base.html @@ -0,0 +1,33 @@ + + + + Newreader + {% block head %}{% endblock %} + + + +
+ + {% if messages %} +
    + {% for message in messages %} + + {{ message }} + + {% endfor %} +
+ {% endif %} + + {% block content %}{% endblock content %} + + diff --git a/src/newsreader/urls.py b/src/newsreader/urls.py index e8a3b87..5b30662 100644 --- a/src/newsreader/urls.py +++ b/src/newsreader/urls.py @@ -2,6 +2,7 @@ from django.conf import settings from django.contrib import admin from django.urls import include, path +from newsreader.accounts.urls import urlpatterns as login_urls from newsreader.news.collection.urls import endpoints as collection_endpoints from newsreader.news.core.urls import endpoints as core_endpoints @@ -9,6 +10,7 @@ from newsreader.news.core.urls import endpoints as core_endpoints endpoints = collection_endpoints + core_endpoints urlpatterns = [ + path("accounts/", include((login_urls, "accounts")), name="accounts"), path("admin/", admin.site.urls, name="admin"), path("api/", include((endpoints, "api")), name="api"), ] From 0658d6404f67a8f0e746077c4412fef1118a230b Mon Sep 17 00:00:00 2001 From: Sonny Date: Sat, 10 Aug 2019 21:12:59 +0200 Subject: [PATCH 018/364] Add docker specific files --- Dockerfile | 14 ++++++++++++++ docker-compose.yml | 31 +++++++++++++++++++++++++++++++ src/entrypoint.sh | 5 +++++ src/newsreader/celery.py | 4 +++- src/newsreader/conf/docker.py | 17 +++++++++++++++++ 5 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100755 src/entrypoint.sh create mode 100644 src/newsreader/conf/docker.py diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..bbdfe24 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM python:3.7-buster + +RUN mkdir /app +WORKDIR /app + +# Use a seperate layer for the project requirements +COPY ./requirements /app/requirements +RUN pip install -r requirements/dev.txt + +COPY . /app/ + +# Set the default shell & add a home dir +RUN useradd -ms /bin/bash newsreader +USER newsreader diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..182821d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,31 @@ +version: '3' + +services: + db: + # See https://hub.docker.com/_/postgres + image: postgres + environment: + - POSTGRES_USER=newsreader + - POSTGRES_DB=newsreader + web: + build: . + command: src/entrypoint.sh + environment: + - DJANGO_SETTINGS_MODULE=newsreader.conf.docker + volumes: + - .:/app + ports: + - '8000:8000' + depends_on: + - db + rabbitmq: + image: rabbitmq:3.7 + celery: + build: . + command: celery -A newsreader worker --beat --scheduler django --loglevel=info --workdir=/app/src/ + environment: + - DJANGO_SETTINGS_MODULE=newsreader.conf.docker + volumes: + - .:/app + depends_on: + - rabbitmq diff --git a/src/entrypoint.sh b/src/entrypoint.sh new file mode 100755 index 0000000..3fbf941 --- /dev/null +++ b/src/entrypoint.sh @@ -0,0 +1,5 @@ +#!/bin/bash +# This file should only be used in conjuction with docker-compose + +python /app/src/manage.py migrate +python /app/src/manage.py runserver 0.0.0.0:8000 diff --git a/src/newsreader/celery.py b/src/newsreader/celery.py index 4aeb7a1..aa15a08 100644 --- a/src/newsreader/celery.py +++ b/src/newsreader/celery.py @@ -7,6 +7,8 @@ from celery import Celery os.environ.setdefault("DJANGO_SETTINGS_MODULE", "newsreader.conf.dev") # note: use the --workdir flag when running from different directories -app = Celery("newsreader", broker="amqp://") +app = Celery("newsreader") + +app.config_from_object("django.conf:settings") app.autodiscover_tasks() diff --git a/src/newsreader/conf/docker.py b/src/newsreader/conf/docker.py new file mode 100644 index 0000000..d8e3142 --- /dev/null +++ b/src/newsreader/conf/docker.py @@ -0,0 +1,17 @@ +from .dev import * + + +# Database +# https://docs.djangoproject.com/en/2.2/ref/settings/#databases +DATABASES = { + "default": { + "ENGINE": "django.db.backends.postgresql_psycopg2", + "NAME": "newsreader", + "USER": "newsreader", + "HOST": "db", + } +} + +# Celery +# https://docs.celeryproject.org/en/latest/userguide/configuration.html +BROKER_URL = "amqp://guest:guest@rabbitmq:5672//" From 8c3bc408b91c726d320f2bd1cb78e90627e71c8c Mon Sep 17 00:00:00 2001 From: Sonny Date: Sat, 10 Aug 2019 22:28:00 +0200 Subject: [PATCH 019/364] Add a default fixture --- src/newsreader/fixtures/default-fixture.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 src/newsreader/fixtures/default-fixture.json diff --git a/src/newsreader/fixtures/default-fixture.json b/src/newsreader/fixtures/default-fixture.json new file mode 100644 index 0000000..514930a --- /dev/null +++ b/src/newsreader/fixtures/default-fixture.json @@ -0,0 +1 @@ +[{"model": "accounts.user", "pk": 1, "fields": {"password": "pbkdf2_sha256$150000$OeNoz2LRSpI5$jkkUf/BjTuWZULyldvNTt9f45/ErxaCmCQHfZwtzji8=", "last_login": "2019-08-10T18:07:27.224Z", "is_superuser": true, "first_name": "", "last_name": "", "is_staff": true, "is_active": true, "date_joined": "2019-08-10T18:07:19.699Z", "email": "sonny@newsreader.nl", "task": null, "task_interval": null, "groups": [], "user_permissions": []}}, {"model": "django_celery_beat.intervalschedule", "pk": 1, "fields": {"every": 5, "period": "minutes"}}, {"model": "django_celery_beat.intervalschedule", "pk": 2, "fields": {"every": 30, "period": "minutes"}}, {"model": "django_celery_beat.intervalschedule", "pk": 3, "fields": {"every": 3, "period": "hours"}}, {"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.periodictasks", "pk": 1, "fields": {"last_update": "2019-08-10T20:01:21.152Z"}}, {"model": "django_celery_beat.periodictask", "pk": 1, "fields": {"name": "celery.backend_cleanup", "task": "celery.backend_cleanup", "interval": null, "crontab": 1, "solar": null, "clocked": null, "args": "[]", "kwargs": "{}", "queue": null, "exchange": null, "routing_key": null, "headers": "{}", "priority": null, "expires": null, "one_off": false, "start_time": null, "enabled": true, "last_run_at": null, "total_run_count": 0, "date_changed": "2019-08-10T19:56:33.160Z", "description": ""}}, {"model": "django_celery_beat.periodictask", "pk": 3, "fields": {"name": "Collection testing task", "task": "newsreader.news.collection.tasks.collect", "interval": 3, "crontab": null, "solar": null, "clocked": null, "args": "[1]", "kwargs": "{}", "queue": null, "exchange": null, "routing_key": null, "headers": "{}", "priority": null, "expires": null, "one_off": false, "start_time": "2019-08-10T20:00:52Z", "enabled": true, "last_run_at": null, "total_run_count": 0, "date_changed": "2019-08-10T20:01:21.153Z", "description": ""}}, {"model": "core.category", "pk": 1, "fields": {"created": "2019-08-10T19:57:53Z", "modified": "2019-08-10T19:58:02.048Z", "name": "Tech", "user": 1}}, {"model": "core.category", "pk": 2, "fields": {"created": "2019-08-10T19:58:13Z", "modified": "2019-08-10T19:58:20.951Z", "name": "News", "user": 1}}, {"model": "collection.collectionrule", "pk": 1, "fields": {"created": "2019-08-10T18:08:10.520Z", "modified": "2019-08-10T19:59:54.547Z", "name": "Tweakers", "url": "http://feeds.feedburner.com/tweakers/mixed", "website_url": null, "favicon": null, "timezone": "Europe/Amsterdam", "category": 1, "last_suceeded": "2019-08-10T18:25:32.325Z", "succeeded": true, "error": null, "user": 1}}, {"model": "collection.collectionrule", "pk": 2, "fields": {"created": "2019-08-10T19:58:05.615Z", "modified": "2019-08-10T19:58:05.691Z", "name": "Hackers News", "url": "https://news.ycombinator.com/rss", "website_url": null, "favicon": null, "timezone": "UTC", "category": 1, "last_suceeded": null, "succeeded": false, "error": null, "user": 1}}, {"model": "collection.collectionrule", "pk": 3, "fields": {"created": "2019-08-10T19:58:23.441Z", "modified": "2019-08-10T19:58:23.449Z", "name": "BBC", "url": "http://feeds.bbci.co.uk/news/world/rss.xml", "website_url": null, "favicon": null, "timezone": "UTC", "category": 2, "last_suceeded": null, "succeeded": false, "error": null, "user": 1}}, {"model": "collection.collectionrule", "pk": 4, "fields": {"created": "2019-08-10T19:58:31.867Z", "modified": "2019-08-10T19:58:31.873Z", "name": "Ars Technica", "url": "http://feeds.arstechnica.com/arstechnica/index?fmt=xml", "website_url": null, "favicon": null, "timezone": "UTC", "category": 1, "last_suceeded": null, "succeeded": false, "error": null, "user": 1}}, {"model": "collection.collectionrule", "pk": 5, "fields": {"created": "2019-08-10T19:58:48.529Z", "modified": "2019-08-10T19:58:48.535Z", "name": "The Guardian", "url": "https://www.theguardian.com/world/rss", "website_url": null, "favicon": null, "timezone": "UTC", "category": 2, "last_suceeded": null, "succeeded": false, "error": null, "user": 1}}, {"model": "collection.collectionrule", "pk": 6, "fields": {"created": "2019-08-10T19:58:58.641Z", "modified": "2019-08-10T19:58:58.647Z", "name": "Engadget", "url": "https://www.engadget.com/rss.xml", "website_url": null, "favicon": null, "timezone": "UTC", "category": 1, "last_suceeded": null, "succeeded": false, "error": null, "user": 1}}, {"model": "collection.collectionrule", "pk": 7, "fields": {"created": "2019-08-10T19:59:29.909Z", "modified": "2019-08-10T19:59:29.917Z", "name": "The Verge", "url": "https://www.theverge.com/rss/index.xml", "website_url": null, "favicon": null, "timezone": "UTC", "category": 1, "last_suceeded": null, "succeeded": false, "error": null, "user": 1}}, {"model": "collection.collectionrule", "pk": 8, "fields": {"created": "2019-08-10T19:59:44.833Z", "modified": "2019-08-10T19:59:44.838Z", "name": "News", "url": "http://feeds.boingboing.net/boingboing/iBag", "website_url": null, "favicon": null, "timezone": "UTC", "category": 2, "last_suceeded": null, "succeeded": false, "error": null, "user": 1}}] \ No newline at end of file From 752ba62aee7e181491e333611460fadf0858e3c0 Mon Sep 17 00:00:00 2001 From: Sonny Date: Sun, 18 Aug 2019 11:49:04 +0200 Subject: [PATCH 020/364] Add pre-commit config --- .pre-commit-config.yaml | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..e5b4349 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,39 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +default_language_version: + python: python3.7 + +repos: + - repo: local + hooks: + - id: autoflake + name: autoflake + entry: autoflake + language: system + types: [python] + args: ["--in-place", "--remove-unused-variables"] + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + - id: pretty-format-json + + - repo: https://github.com/psf/black + rev: 19.3b0 + hooks: + - id: black + args: [--line-length=90] + + - repo: https://github.com/timothycrosley/isort + rev: 4.3.21-2 + hooks: + - id: isort + + - repo: https://github.com/prettier/prettier + rev: 1.18.2 + hooks: + - id: prettier From 61702e720a846e35076153530ee603417a8a1fa4 Mon Sep 17 00:00:00 2001 From: Sonny Date: Sun, 25 Aug 2019 10:06:51 +0200 Subject: [PATCH 021/364] Add swagger integration --- requirements/base.txt | 1 + src/newsreader/conf/base.py | 1 + src/newsreader/fixtures/local/fixture.json | 168 +++++++++++++++++++++ src/newsreader/urls.py | 9 +- 4 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 src/newsreader/fixtures/local/fixture.json diff --git a/requirements/base.txt b/requirements/base.txt index c9686ee..25bf4ff 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -6,6 +6,7 @@ chardet==3.0.4 Django==2.2 django-celery-beat==1.5.0 djangorestframework==3.9.4 +django-rest-swagger-2.2.0 lxml==4.3.4 feedparser==5.2.1 idna==2.8 diff --git a/src/newsreader/conf/base.py b/src/newsreader/conf/base.py index cffe291..6d5e1b0 100644 --- a/src/newsreader/conf/base.py +++ b/src/newsreader/conf/base.py @@ -38,6 +38,7 @@ INSTALLED_APPS = [ "django.contrib.staticfiles", # third party apps "rest_framework", + "rest_framework_swagger", "celery", "django_celery_beat", # app modules 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/urls.py b/src/newsreader/urls.py index 5b30662..6271598 100644 --- a/src/newsreader/urls.py +++ b/src/newsreader/urls.py @@ -2,12 +2,19 @@ from django.conf import settings from django.contrib import admin from django.urls import include, path +from rest_framework_swagger.views import get_swagger_view + from newsreader.accounts.urls import urlpatterns as login_urls from newsreader.news.collection.urls import endpoints as collection_endpoints from newsreader.news.core.urls import endpoints as core_endpoints -endpoints = collection_endpoints + core_endpoints +schema_view = get_swagger_view(title="Newsreader API") +endpoints = [ + path("", schema_view, name="schema-view"), + *collection_endpoints, + *core_endpoints, +] urlpatterns = [ path("accounts/", include((login_urls, "accounts")), name="accounts"), From 858f84aaadf7854b4f075944d2f5d1fdb38ea267 Mon Sep 17 00:00:00 2001 From: sonny Date: Mon, 28 Oct 2019 21:35:19 +0100 Subject: [PATCH 022/364] Refactor endpoint tests Replace force_login calls with login call from client class in setUp --- .babelrc | 10 +- .gitignore | 1 + .gitlab-ci.yml | 56 +- .prettierrc.json | 10 + Dockerfile | 16 +- docker-compose.yml | 26 +- gulp/babel.js | 28 + gulp/sass.js | 30 +- gulpfile.babel.js | 25 +- package-lock.json | 1367 ++++++++++++++++- package.json | 36 +- requirements/base.txt | 2 +- requirements/gitlab.txt | 5 +- src/newsreader/accounts/views.py | 4 +- src/newsreader/conf/base.py | 8 + src/newsreader/fixtures/default-fixture.json | 283 +++- .../js/components/LoadingIndicator.js | 13 + src/newsreader/js/homepage/App.js | 63 + .../js/homepage/actions/categories.js | 86 ++ src/newsreader/js/homepage/actions/posts.js | 119 ++ src/newsreader/js/homepage/actions/rules.js | 68 + .../js/homepage/actions/selected.js | 65 + .../js/homepage/components/PostModal.js | 84 + .../homepage/components/feedlist/FeedList.js | 102 ++ .../homepage/components/feedlist/PostItem.js | 50 + .../homepage/components/feedlist/RuleItem.js | 25 + .../homepage/components/feedlist/filters.js | 44 + .../components/sidebar/CategoryItem.js | 68 + .../homepage/components/sidebar/ReadButton.js | 36 + .../homepage/components/sidebar/RuleItem.js | 56 + .../js/homepage/components/sidebar/Sidebar.js | 52 + .../js/homepage/components/sidebar/filters.js | 7 + src/newsreader/js/homepage/configureStore.js | 18 + src/newsreader/js/homepage/index.js | 16 + .../js/homepage/reducers/categories.js | 116 ++ src/newsreader/js/homepage/reducers/index.js | 10 + src/newsreader/js/homepage/reducers/posts.js | 67 + src/newsreader/js/homepage/reducers/rules.js | 65 + .../js/homepage/reducers/selected.js | 70 + src/newsreader/js/utils.js | 14 + src/newsreader/news/collection/admin.py | 3 +- src/newsreader/news/collection/endpoints.py | 69 + src/newsreader/news/collection/serializers.py | 18 +- .../endpoints/{rules => rule}/__init__.py | 0 .../{rules => rule}/detail/__init__.py | 0 .../endpoints/{rules => rule}/detail/tests.py | 157 +- .../{rules => rule}/list/__init__.py | 0 .../tests/endpoints/rule/list/tests.py | 371 +++++ .../tests/endpoints/rules/list/tests.py | 210 --- src/newsreader/news/collection/urls.py | 14 +- src/newsreader/news/collection/views.py | 21 - src/newsreader/news/core/endpoints.py | 118 ++ src/newsreader/news/core/filters.py | 32 + .../news/core/migrations/0003_post_read.py | 14 + src/newsreader/news/core/models.py | 4 +- src/newsreader/news/core/serializers.py | 23 +- .../news/core/templates/core/main.html | 15 + .../tests/endpoints/category/detail/tests.py | 183 ++- .../tests/endpoints/category/list/tests.py | 538 ++++++- .../core/tests/endpoints/post/detail/tests.py | 57 +- .../core/tests/endpoints/post/list/tests.py | 60 +- src/newsreader/news/core/tests/factories.py | 2 + src/newsreader/news/core/urls.py | 34 +- src/newsreader/news/core/views.py | 58 +- .../scss/accounts/components/form/_form.scss | 2 + .../scss/accounts/components/form/index.scss | 0 .../scss/accounts/components/index.scss | 0 .../scss/accounts/components/main/_main.scss | 0 .../scss/accounts/components/main/index.scss | 0 .../{static/src => }/scss/accounts/index.scss | 2 + .../src => }/scss/components/body/_body.scss | 5 + .../src => }/scss/components/body/index.scss | 0 .../scss/components/button/_button.scss | 2 - .../scss/components/button/index.scss | 0 .../scss/components/error/_error.scss | 0 .../scss/components/error/_errorlist.scss | 0 .../src => }/scss/components/error/index.scss | 0 .../src => }/scss/components/form/_form.scss | 0 .../src => }/scss/components/form/index.scss | 0 .../src => }/scss/components/index.scss | 2 + .../src => }/scss/components/input/input.scss | 0 .../loading-indicator/_loading-indicator.scss | 41 + .../components/loading-indicator/index.scss | 1 + .../src => }/scss/components/main/_main.scss | 0 .../src => }/scss/components/main/index.scss | 0 .../scss/components/modal/_modal.scss | 9 + .../scss/components/modal/index.scss | 1 + .../scss/components/navbar/_navbar.scss | 1 + .../scss/components/navbar/index.scss | 0 .../components/categories/_categories.scss | 24 + .../homepage/components/categories/index.scss | 1 + .../components/category/_category.scss | 46 + .../homepage/components/category/index.scss | 1 + .../homepage/components/content/_content.scss | 7 + .../homepage/components/content/index.scss | 1 + .../scss/homepage/components/index.scss | 17 + .../scss/homepage/components/main/_main.scss | 12 + .../scss/homepage/components/main/index.scss | 1 + .../components/post-block/_post-block.scss | 12 + .../homepage/components/post-block/index.scss | 1 + .../post-message/_post-message.scss | 25 + .../components/post-message/index.scss | 1 + .../scss/homepage/components/post/_post.scss | 125 ++ .../scss/homepage/components/post/index.scss | 1 + .../posts-header/_posts-header.scss | 21 + .../components/posts-header/index.scss | 1 + .../components/posts-section/index.scss | 12 + .../homepage/components/posts/_posts.scss | 34 + .../scss/homepage/components/posts/index.scss | 1 + .../components/read-button/_read-button.scss | 10 + .../components/read-button/index.scss | 1 + .../scss/homepage/components/rule/_rule.scss | 10 + .../scss/homepage/components/rule/index.scss | 1 + .../homepage/components/rules/_rules.scss | 29 + .../scss/homepage/components/rules/index.scss | 1 + .../homepage/components/sidebar/_sidebar.scss | 11 + .../homepage/components/sidebar/index.scss | 1 + .../scss/homepage/elements/badge/_badge.scss | 15 + .../scss/homepage/elements/badge/index.scss | 1 + .../scss/homepage/elements/index.scss | 1 + src/newsreader/scss/homepage/index.scss | 7 + src/newsreader/scss/partials/_variables.scss | 42 + src/newsreader/static/icons/angle-down.svg | 1 + src/newsreader/static/icons/angle-right.svg | 1 + src/newsreader/static/icons/arrow-left.svg | 6 + src/newsreader/static/icons/chevron-down.svg | 6 + src/newsreader/static/icons/chevron-right.svg | 6 + src/newsreader/static/icons/link.svg | 1 + src/newsreader/static/icons/times.svg | 1 + .../static/src/scss/partials/_variables.scss | 26 - src/newsreader/templates/base.html | 6 +- src/newsreader/urls.py | 3 + 132 files changed, 5158 insertions(+), 661 deletions(-) create mode 100644 .prettierrc.json create mode 100644 gulp/babel.js create mode 100644 src/newsreader/js/components/LoadingIndicator.js create mode 100644 src/newsreader/js/homepage/App.js create mode 100644 src/newsreader/js/homepage/actions/categories.js create mode 100644 src/newsreader/js/homepage/actions/posts.js create mode 100644 src/newsreader/js/homepage/actions/rules.js create mode 100644 src/newsreader/js/homepage/actions/selected.js create mode 100644 src/newsreader/js/homepage/components/PostModal.js create mode 100644 src/newsreader/js/homepage/components/feedlist/FeedList.js create mode 100644 src/newsreader/js/homepage/components/feedlist/PostItem.js create mode 100644 src/newsreader/js/homepage/components/feedlist/RuleItem.js create mode 100644 src/newsreader/js/homepage/components/feedlist/filters.js create mode 100644 src/newsreader/js/homepage/components/sidebar/CategoryItem.js create mode 100644 src/newsreader/js/homepage/components/sidebar/ReadButton.js create mode 100644 src/newsreader/js/homepage/components/sidebar/RuleItem.js create mode 100644 src/newsreader/js/homepage/components/sidebar/Sidebar.js create mode 100644 src/newsreader/js/homepage/components/sidebar/filters.js create mode 100644 src/newsreader/js/homepage/configureStore.js create mode 100644 src/newsreader/js/homepage/index.js create mode 100644 src/newsreader/js/homepage/reducers/categories.js create mode 100644 src/newsreader/js/homepage/reducers/index.js create mode 100644 src/newsreader/js/homepage/reducers/posts.js create mode 100644 src/newsreader/js/homepage/reducers/rules.js create mode 100644 src/newsreader/js/homepage/reducers/selected.js create mode 100644 src/newsreader/js/utils.js create mode 100644 src/newsreader/news/collection/endpoints.py rename src/newsreader/news/collection/tests/endpoints/{rules => rule}/__init__.py (100%) rename src/newsreader/news/collection/tests/endpoints/{rules => rule}/detail/__init__.py (100%) rename src/newsreader/news/collection/tests/endpoints/{rules => rule}/detail/tests.py (59%) rename src/newsreader/news/collection/tests/endpoints/{rules => rule}/list/__init__.py (100%) create mode 100644 src/newsreader/news/collection/tests/endpoints/rule/list/tests.py delete mode 100644 src/newsreader/news/collection/tests/endpoints/rules/list/tests.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/migrations/0003_post_read.py create mode 100644 src/newsreader/news/core/templates/core/main.html rename src/newsreader/{static/src => }/scss/accounts/components/form/_form.scss (94%) rename src/newsreader/{static/src => }/scss/accounts/components/form/index.scss (100%) rename src/newsreader/{static/src => }/scss/accounts/components/index.scss (100%) rename src/newsreader/{static/src => }/scss/accounts/components/main/_main.scss (100%) rename src/newsreader/{static/src => }/scss/accounts/components/main/index.scss (100%) rename src/newsreader/{static/src => }/scss/accounts/index.scss (72%) rename src/newsreader/{static/src => }/scss/components/body/_body.scss (61%) rename src/newsreader/{static/src => }/scss/components/body/index.scss (100%) rename src/newsreader/{static/src => }/scss/components/button/_button.scss (95%) rename src/newsreader/{static/src => }/scss/components/button/index.scss (100%) rename src/newsreader/{static/src => }/scss/components/error/_error.scss (100%) rename src/newsreader/{static/src => }/scss/components/error/_errorlist.scss (100%) rename src/newsreader/{static/src => }/scss/components/error/index.scss (100%) rename src/newsreader/{static/src => }/scss/components/form/_form.scss (100%) rename src/newsreader/{static/src => }/scss/components/form/index.scss (100%) rename src/newsreader/{static/src => }/scss/components/index.scss (70%) rename src/newsreader/{static/src => }/scss/components/input/input.scss (100%) create mode 100644 src/newsreader/scss/components/loading-indicator/_loading-indicator.scss create mode 100644 src/newsreader/scss/components/loading-indicator/index.scss rename src/newsreader/{static/src => }/scss/components/main/_main.scss (100%) rename src/newsreader/{static/src => }/scss/components/main/index.scss (100%) create mode 100644 src/newsreader/scss/components/modal/_modal.scss create mode 100644 src/newsreader/scss/components/modal/index.scss rename src/newsreader/{static/src => }/scss/components/navbar/_navbar.scss (96%) rename src/newsreader/{static/src => }/scss/components/navbar/index.scss (100%) create mode 100644 src/newsreader/scss/homepage/components/categories/_categories.scss create mode 100644 src/newsreader/scss/homepage/components/categories/index.scss create mode 100644 src/newsreader/scss/homepage/components/category/_category.scss create mode 100644 src/newsreader/scss/homepage/components/category/index.scss create mode 100644 src/newsreader/scss/homepage/components/content/_content.scss create mode 100644 src/newsreader/scss/homepage/components/content/index.scss create mode 100644 src/newsreader/scss/homepage/components/index.scss create mode 100644 src/newsreader/scss/homepage/components/main/_main.scss create mode 100644 src/newsreader/scss/homepage/components/main/index.scss create mode 100644 src/newsreader/scss/homepage/components/post-block/_post-block.scss create mode 100644 src/newsreader/scss/homepage/components/post-block/index.scss create mode 100644 src/newsreader/scss/homepage/components/post-message/_post-message.scss create mode 100644 src/newsreader/scss/homepage/components/post-message/index.scss create mode 100644 src/newsreader/scss/homepage/components/post/_post.scss create mode 100644 src/newsreader/scss/homepage/components/post/index.scss create mode 100644 src/newsreader/scss/homepage/components/posts-header/_posts-header.scss create mode 100644 src/newsreader/scss/homepage/components/posts-header/index.scss create mode 100644 src/newsreader/scss/homepage/components/posts-section/index.scss create mode 100644 src/newsreader/scss/homepage/components/posts/_posts.scss create mode 100644 src/newsreader/scss/homepage/components/posts/index.scss create mode 100644 src/newsreader/scss/homepage/components/read-button/_read-button.scss create mode 100644 src/newsreader/scss/homepage/components/read-button/index.scss create mode 100644 src/newsreader/scss/homepage/components/rule/_rule.scss create mode 100644 src/newsreader/scss/homepage/components/rule/index.scss create mode 100644 src/newsreader/scss/homepage/components/rules/_rules.scss create mode 100644 src/newsreader/scss/homepage/components/rules/index.scss create mode 100644 src/newsreader/scss/homepage/components/sidebar/_sidebar.scss create mode 100644 src/newsreader/scss/homepage/components/sidebar/index.scss create mode 100644 src/newsreader/scss/homepage/elements/badge/_badge.scss create mode 100644 src/newsreader/scss/homepage/elements/badge/index.scss create mode 100644 src/newsreader/scss/homepage/elements/index.scss create mode 100644 src/newsreader/scss/homepage/index.scss create mode 100644 src/newsreader/scss/partials/_variables.scss create mode 100644 src/newsreader/static/icons/angle-down.svg create mode 100644 src/newsreader/static/icons/angle-right.svg create mode 100644 src/newsreader/static/icons/arrow-left.svg create mode 100644 src/newsreader/static/icons/chevron-down.svg create mode 100644 src/newsreader/static/icons/chevron-right.svg create mode 100644 src/newsreader/static/icons/link.svg create mode 100644 src/newsreader/static/icons/times.svg delete mode 100644 src/newsreader/static/src/scss/partials/_variables.scss diff --git a/.babelrc b/.babelrc index 1320b9a..610dee0 100644 --- a/.babelrc +++ b/.babelrc @@ -1,3 +1,11 @@ { - "presets": ["@babel/preset-env"] + "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/.gitignore b/.gitignore index 0cc2b5c..a5aa72e 100644 --- a/.gitignore +++ b/.gitignore @@ -154,6 +154,7 @@ dmypy.json # Translations # Django stuff: +src/newsreader/fixtures/local # Flask stuff: diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 11fc181..c4ac909 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,13 +1,57 @@ -services: - - postgres:9.6 +stages: + - build + - test + - lint -variables: - POSTGRES_DB: newsreader - POSTGRES_USER: newsreader +javascript build: + image: node:12 + stage: build + cache: + key: "$CI_JOB_NAME-$CI_COMMIT_REF_SLUG" + paths: + - node_modules/ + before_script: + - npm install --dev + script: + - npx gulp python tests: + services: + - postgres:11 image: python:3.7.4-slim-stretch stage: test + variables: + PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" + DJANGO_SETTINGS_MODULE: "newsreader.conf.gitlab" + POSTGRES_DB: newsreader + POSTGRES_USER: newsreader + cache: + key: "$CI_JOB_NAME-$CI_COMMIT_REF_SLUG" + paths: + - .cache/pip + - env/ + before_script: + - python3 -m venv env + - source env/bin/activate + - pip install -r requirements/gitlab.txt + script: + - python src/manage.py test newsreader + +javascript linting: + image: node:12 + stage: lint + cache: + key: "$CI_JOB_NAME-$CI_COMMIT_REF_SLUG" + paths: + - node_modules/ + before_script: + - npm install --dev + script: + - npm run lint + +python linting: + image: python:3.7.4-slim-stretch + stage: lint variables: PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" DJANGO_SETTINGS_MODULE: "newsreader.conf.gitlab" @@ -21,6 +65,6 @@ python tests: - source env/bin/activate - pip install -r requirements/gitlab.txt script: - - python src/manage.py test newsreader - isort -rc src/ --check-only - black -l 90 --check src/ + - autoflake -rc src/ 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 index bbdfe24..ac7ac5b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,14 +1,18 @@ FROM python:3.7-buster +# Run project binaries from the user's local bin folder +ENV PATH=/home/newsreader/.local/bin:$PATH + +# Set the default shell +RUN useradd -ms /bin/bash newsreader + RUN mkdir /app WORKDIR /app +RUN chown newsreader:newsreader /app +USER newsreader # Use a seperate layer for the project requirements -COPY ./requirements /app/requirements -RUN pip install -r requirements/dev.txt +COPY requirements /app/requirements +RUN pip install --user -r requirements/dev.txt COPY . /app/ - -# Set the default shell & add a home dir -RUN useradd -ms /bin/bash newsreader -USER newsreader diff --git a/docker-compose.yml b/docker-compose.yml index 182821d..7987022 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,11 +4,26 @@ services: db: # See https://hub.docker.com/_/postgres image: postgres + container_name: postgres environment: - POSTGRES_USER=newsreader - POSTGRES_DB=newsreader + rabbitmq: + image: rabbitmq:3.7 + container_name: rabbitmq + celery: + build: . + container_name: celery + command: celery -A newsreader worker --beat --scheduler django --workdir=/app/src/ + environment: + - DJANGO_SETTINGS_MODULE=newsreader.conf.docker + volumes: + - .:/app + depends_on: + - rabbitmq web: build: . + container_name: web command: src/entrypoint.sh environment: - DJANGO_SETTINGS_MODULE=newsreader.conf.docker @@ -18,14 +33,3 @@ services: - '8000:8000' depends_on: - db - rabbitmq: - image: rabbitmq:3.7 - celery: - build: . - command: celery -A newsreader worker --beat --scheduler django --loglevel=info --workdir=/app/src/ - environment: - - DJANGO_SETTINGS_MODULE=newsreader.conf.docker - volumes: - - .:/app - depends_on: - - rabbitmq diff --git a/gulp/babel.js b/gulp/babel.js new file mode 100644 index 0000000..8bff9fc --- /dev/null +++ b/gulp/babel.js @@ -0,0 +1,28 @@ +import path from 'path'; + +import { dest } from 'gulp'; +import babelify from 'babelify'; +import browserify from 'browserify'; +import source from 'vinyl-source-stream'; +import buffer from 'vinyl-buffer'; +import concat from 'gulp-concat'; + +const PROJECT_DIR = path.join('src', 'newsreader'); +const STATIC_DIR = path.join(PROJECT_DIR, 'js'); +const CORE_DIR = path.join(PROJECT_DIR, 'news', 'core', 'static'); + +const babelTask = () => { + const config = browserify({ + entries: `${STATIC_DIR}/homepage/index.js`, + debug: true, + }).transform(babelify); + + return config + .bundle() + .pipe(source('index.js')) + .pipe(buffer()) + .pipe(concat('homepage.js')) + .pipe(dest(`${CORE_DIR}/core/dist/js/`)); +}; + +export default babelTask; diff --git a/gulp/sass.js b/gulp/sass.js index 637817f..58cf4b0 100644 --- a/gulp/sass.js +++ b/gulp/sass.js @@ -1,17 +1,25 @@ -import { src, dest } from "gulp"; +import { src, dest } from 'gulp'; -import concat from "gulp-concat"; -import path from "path"; -import sass from "gulp-sass"; +import concat from 'gulp-concat'; +import path from 'path'; +import sass from 'gulp-sass'; -const PROJECT_DIR = path.join("src", "newsreader"); -const STATIC_DIR = path.join(PROJECT_DIR, "static"); +const PROJECT_DIR = path.join('src', 'newsreader'); +const STATIC_DIR = path.join(PROJECT_DIR, 'scss'); -export const ACCOUNTS_DIR = path.join(PROJECT_DIR, "accounts", "static"); +export const ACCOUNTS_DIR = path.join(PROJECT_DIR, 'accounts', 'static'); +export const CORE_DIR = path.join(PROJECT_DIR, 'news', 'core', 'static'); -export default function accountsTask(){ - return src(`${STATIC_DIR}/src/scss/accounts/index.scss`) - .pipe(sass().on("error", sass.logError)) - .pipe(concat("accounts.css")) +export const accountsTask = () => { + return src(`${STATIC_DIR}/accounts/index.scss`) + .pipe(sass().on('error', sass.logError)) + .pipe(concat('accounts.css')) .pipe(dest(`${ACCOUNTS_DIR}/accounts/dist/css`)); }; + +export const coreTask = () => { + return src(`${STATIC_DIR}/homepage/index.scss`) + .pipe(sass().on('error', sass.logError)) + .pipe(concat('core.css')) + .pipe(dest(`${CORE_DIR}/core/dist/css`)); +}; diff --git a/gulpfile.babel.js b/gulpfile.babel.js index 56a18ee..bea1785 100644 --- a/gulpfile.babel.js +++ b/gulpfile.babel.js @@ -1,22 +1,27 @@ -import { series, watch as _watch } from 'gulp'; +import { parallel, series, watch as _watch } from 'gulp'; -import path from "path"; -import del from "del"; +import path from 'path'; +import del from 'del'; -import buildSass, { ACCOUNTS_DIR } from "./gulp/sass"; +import { ACCOUNTS_DIR, CORE_DIR, accountsTask, coreTask } from './gulp/sass'; +import babelTask from './gulp/babel'; -const STATIC_DIR = path.join("src", "newsreader", "static"); +const PROJECT_DIR = path.join('src', 'newsreader'); +const sassTasks = [accountsTask, coreTask]; -function clean(){ +const clean = () => { return del([ `${ACCOUNTS_DIR}/accounts/dist/css/*`, + + `${CORE_DIR}/core/dist/css/*`, + `${CORE_DIR}/core/dist/js/*`, ]); }; -export function watch(){ - _watch(`${STATIC_DIR}/src/scss/**/*.scss`, (done) => { - series(clean, buildSass)(done); +export const watch = () => { + return _watch([`${PROJECT_DIR}/scss/**/*.scss`, `${PROJECT_DIR}/js/**/*.js`], done => { + series(clean, ...sassTasks, babelTask)(done); }); }; -export default series(clean, buildSass); +export default series(clean, ...sassTasks, babelTask); diff --git a/package-lock.json b/package-lock.json index c412ebf..2b98192 100644 --- a/package-lock.json +++ b/package-lock.json @@ -84,6 +84,16 @@ "@babel/types": "^7.0.0" } }, + "@babel/helper-builder-react-jsx": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.3.0.tgz", + "integrity": "sha512-MjA9KgwCuPEkQd9ncSXvSyJ5y+j2sICHyrI0M3L+6fnS4wMSNDc1ARXsbTfbb2cXHn17VisSnU/sHFTCxVxSMw==", + "dev": true, + "requires": { + "@babel/types": "^7.3.0", + "esutils": "^2.0.0" + } + }, "@babel/helper-call-delegate": { "version": "7.4.4", "resolved": "https://registry.npmjs.org/@babel/helper-call-delegate/-/helper-call-delegate-7.4.4.tgz", @@ -95,6 +105,114 @@ "@babel/types": "^7.4.4" } }, + "@babel/helper-create-class-features-plugin": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.5.5.tgz", + "integrity": "sha512-ZsxkyYiRA7Bg+ZTRpPvB6AbOFKTFFK4LrvTet8lInm0V468MWCaSYJE+I7v2z2r8KNLtYiV+K5kTCnR7dvyZjg==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.1.0", + "@babel/helper-member-expression-to-functions": "^7.5.5", + "@babel/helper-optimise-call-expression": "^7.0.0", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-replace-supers": "^7.5.5", + "@babel/helper-split-export-declaration": "^7.4.4" + }, + "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/generator": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.5.5.tgz", + "integrity": "sha512-ETI/4vyTSxTzGnU2c49XHv2zhExkv9JHLTwDAFz85kmcwuShvYG2H08FwgIguQf4JC75CBnXAUM5PqeF4fj0nQ==", + "dev": true, + "requires": { + "@babel/types": "^7.5.5", + "jsesc": "^2.5.1", + "lodash": "^4.17.13", + "source-map": "^0.5.0", + "trim-right": "^1.0.1" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.5.5.tgz", + "integrity": "sha512-5qZ3D1uMclSNqYcXqiHoA0meVdv+xUEex9em2fqMnrk/scphGlGgg66zjMrPJESPwrFJ6sbfFQYUSa0Mz7FabA==", + "dev": true, + "requires": { + "@babel/types": "^7.5.5" + } + }, + "@babel/helper-replace-supers": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.5.5.tgz", + "integrity": "sha512-XvRFWrNnlsow2u7jXDuH4jDDctkxbS7gXssrP4q2nUD606ukXHRvydj346wmNg+zAgpFx4MWf4+usfC93bElJg==", + "dev": true, + "requires": { + "@babel/helper-member-expression-to-functions": "^7.5.5", + "@babel/helper-optimise-call-expression": "^7.0.0", + "@babel/traverse": "^7.5.5", + "@babel/types": "^7.5.5" + } + }, + "@babel/parser": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.5.5.tgz", + "integrity": "sha512-E5BN68cqR7dhKan1SfqgPGhQ178bkVKpXTPEXnFJBrEt8/DKRZlybmy+IgYLTeN7tp1R5Ccmbm2rBk17sHYU3g==", + "dev": true + }, + "@babel/traverse": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.5.5.tgz", + "integrity": "sha512-MqB0782whsfffYfSjH4TM+LMjrJnhCNEDMDIjeTpl+ASaUvxcjoiVCo/sM1GhS1pHOXYfWVCYneLjMckuUxDaQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.5.5", + "@babel/generator": "^7.5.5", + "@babel/helper-function-name": "^7.1.0", + "@babel/helper-split-export-declaration": "^7.4.4", + "@babel/parser": "^7.5.5", + "@babel/types": "^7.5.5", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.13" + } + }, + "@babel/types": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.5.5.tgz", + "integrity": "sha512-s63F9nJioLqOlW3UkyMd+BYhXt44YuaFm/VV0VwuteqjYwRrObkU7ra9pY4wAJR3oXi8hJrMcrcJdO/HH33vtw==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + }, + "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/helper-define-map": { "version": "7.4.4", "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.4.4.tgz", @@ -296,6 +414,16 @@ "@babel/plugin-syntax-async-generators": "^7.2.0" } }, + "@babel/plugin-proposal-class-properties": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.5.5.tgz", + "integrity": "sha512-AF79FsnWFxjlaosgdi421vmYG6/jg79bVD0dpD44QdgobzHKuLZ6S3vl8la9qIeSwGi8i1fS0O1mfuDAAdo1/A==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.5.5", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, "@babel/plugin-proposal-dynamic-import": { "version": "7.5.0", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.5.0.tgz", @@ -306,6 +434,16 @@ "@babel/plugin-syntax-dynamic-import": "^7.2.0" } }, + "@babel/plugin-proposal-function-bind": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-function-bind/-/plugin-proposal-function-bind-7.2.0.tgz", + "integrity": "sha512-qOFJ/eX1Is78sywwTxDcsntLOdb5ZlHVVqUz5xznq8ldAfOVIyZzp1JE2rzHnaksZIhrqMrwIpQL/qcEprnVbw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-function-bind": "^7.2.0" + } + }, "@babel/plugin-proposal-json-strings": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.2.0.tgz", @@ -365,6 +503,15 @@ "@babel/helper-plugin-utils": "^7.0.0" } }, + "@babel/plugin-syntax-function-bind": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-function-bind/-/plugin-syntax-function-bind-7.2.0.tgz", + "integrity": "sha512-/WzU1lLU2l0wDfB42Wkg6tahrmtBbiD8C4H6EGSX0M4GAjzN6JiOpq/Uh8G6GSoR6lPMvhjM0MNiV6znj6y/zg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, "@babel/plugin-syntax-json-strings": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.2.0.tgz", @@ -374,6 +521,15 @@ "@babel/helper-plugin-utils": "^7.0.0" } }, + "@babel/plugin-syntax-jsx": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.2.0.tgz", + "integrity": "sha512-VyN4QANJkRW6lDBmENzRszvZf3/4AXaj9YR7GwrWeeN9tEBPuXbmDYVU9bYBN0D70zCWVwUy0HWq2553VCb6Hw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, "@babel/plugin-syntax-object-rest-spread": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.2.0.tgz", @@ -624,6 +780,17 @@ "@babel/helper-plugin-utils": "^7.0.0" } }, + "@babel/plugin-transform-react-jsx": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.3.0.tgz", + "integrity": "sha512-a/+aRb7R06WcKvQLOu4/TpjKOdvVEKRLWFpKcNuHhiREPgGRB4TQJxq07+EZLS8LFVYpfq1a5lDUnuMdcCpBKg==", + "dev": true, + "requires": { + "@babel/helper-builder-react-jsx": "^7.3.0", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-jsx": "^7.2.0" + } + }, "@babel/plugin-transform-regenerator": { "version": "7.4.5", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.4.5.tgz", @@ -642,6 +809,18 @@ "@babel/helper-plugin-utils": "^7.0.0" } }, + "@babel/plugin-transform-runtime": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.5.5.tgz", + "integrity": "sha512-6Xmeidsun5rkwnGfMOp6/z9nSzWpHFNVr2Jx7kwoq4mVatQfQx5S56drBgEHF+XQbKOdIaOiMIINvp/kAwMN+w==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.0.0", + "@babel/helper-plugin-utils": "^7.0.0", + "resolve": "^1.8.1", + "semver": "^5.5.1" + } + }, "@babel/plugin-transform-shorthand-properties": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.2.0.tgz", @@ -772,6 +951,14 @@ "source-map-support": "^0.5.9" } }, + "@babel/runtime": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.5.5.tgz", + "integrity": "sha512-28QvEGyQyNkB0/m2B4FU7IEZGK2NUrcMtT6BZEFALTguLk+AUT6ofsHtPk5QyjAdUkpMJ+/Em+quwz4HOt30AQ==", + "requires": { + "regenerator-runtime": "^0.13.2" + } + }, "@babel/template": { "version": "7.4.4", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.4.4.tgz", @@ -883,12 +1070,52 @@ "integrity": "sha512-7TEYTQT1/6PP53NftXXabIZDaZfaoBdeBm8Md/i7zsWRoBe0YwOXguyK8vhHs8ehgB/w9U4K/6EWuTyp0W6nIA==", "dev": true }, + "JSONStream": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "dev": true, + "requires": { + "jsonparse": "^1.2.0", + "through": ">=2.2.7 <3" + } + }, "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.2.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.2.1.tgz", + "integrity": "sha512-JD0xT5FCRDNyjDda3Lrg/IxFscp9q4tiYtxE1/nOzlKCk7hIRuYjhq1kCNkbPjMRMZuFq20HNQn1I9k8Oj0E+Q==", + "dev": true + }, + "acorn-dynamic-import": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/acorn-dynamic-import/-/acorn-dynamic-import-4.0.0.tgz", + "integrity": "sha512-d3OEjQV4ROpoflsnUA8HozoIR504TFxNivYEUi6uwz0IYhBkTDXGuWlNdMtybRt3nqVx/L6XqMt0FxkXuWKZhw==", + "dev": true + }, + "acorn-node": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.7.0.tgz", + "integrity": "sha512-XhahLSsCB6X6CJbe+uNu3Mn9sJBNFxtBN9NLgAOQovfS6Kh0lDUtmlclhjn9CvEK7A7YyRU13PXlNcpSiLI9Yw==", + "dev": true, + "requires": { + "acorn": "^6.1.1", + "acorn-dynamic-import": "^4.0.0", + "acorn-walk": "^6.1.1", + "xtend": "^4.0.1" + } + }, + "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", @@ -1029,6 +1256,12 @@ "integrity": "sha1-p5SvDAWrF1KEbudTofIRoFugxE8=", "dev": true }, + "array-filter": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/array-filter/-/array-filter-0.0.1.tgz", + "integrity": "sha1-fajPLiZijtcygDWB/SH2fKzS7uw=", + "dev": true + }, "array-find-index": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", @@ -1070,6 +1303,18 @@ } } }, + "array-map": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/array-map/-/array-map-0.0.0.tgz", + "integrity": "sha1-iKK6tz0c97zVwbEYoAP2b2ZfpmI=", + "dev": true + }, + "array-reduce": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/array-reduce/-/array-reduce-0.0.0.tgz", + "integrity": "sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys=", + "dev": true + }, "array-slice": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", @@ -1116,6 +1361,44 @@ "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", @@ -1194,6 +1477,12 @@ "object.assign": "^4.1.0" } }, + "babelify": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/babelify/-/babelify-10.0.0.tgz", + "integrity": "sha512-X40FaxyH7t3X+JFAKvb1H9wooWKLRCi8pg3m8poqtdZaIng+bjzp9RvKQCvRjF9isHiPkXspbbXT/zwXLtwgwg==", + "dev": true + }, "bach": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/bach/-/bach-1.2.0.tgz", @@ -1272,6 +1561,12 @@ } } }, + "base64-js": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", + "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==", + "dev": true + }, "bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -1287,6 +1582,16 @@ "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", "dev": true }, + "bl": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", + "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", + "dev": true, + "requires": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + }, "block-stream": { "version": "0.0.9", "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", @@ -1296,6 +1601,12 @@ "inherits": "~2.0.0" } }, + "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", @@ -1335,6 +1646,178 @@ } } }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", + "dev": true + }, + "browser-pack": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/browser-pack/-/browser-pack-6.1.0.tgz", + "integrity": "sha512-erYug8XoqzU3IfcU8fUgyHqyOXqIE4tUTTQ+7mqUjQlvnXkOO6OlT9c/ZoJVHYoAaqGxr09CN53G7XIsO4KtWA==", + "dev": true, + "requires": { + "JSONStream": "^1.0.3", + "combine-source-map": "~0.8.0", + "defined": "^1.0.0", + "safe-buffer": "^5.1.1", + "through2": "^2.0.0", + "umd": "^3.0.0" + } + }, + "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": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/browserify/-/browserify-16.3.0.tgz", + "integrity": "sha512-BWaaD7alyGZVEBBwSTYx4iJF5DswIGzK17o8ai9w4iKRbYpk3EOiprRHMRRA8DCZFmFeOdx7A385w2XdFvxWmg==", + "dev": true, + "requires": { + "JSONStream": "^1.0.3", + "assert": "^1.4.0", + "browser-pack": "^6.0.1", + "browser-resolve": "^1.11.0", + "browserify-zlib": "~0.2.0", + "buffer": "^5.0.2", + "cached-path-relative": "^1.0.0", + "concat-stream": "^1.6.0", + "console-browserify": "^1.1.0", + "constants-browserify": "~1.0.0", + "crypto-browserify": "^3.0.0", + "defined": "^1.0.0", + "deps-sort": "^2.0.0", + "domain-browser": "^1.2.0", + "duplexer2": "~0.1.2", + "events": "^2.0.0", + "glob": "^7.1.0", + "has": "^1.0.0", + "htmlescape": "^1.1.0", + "https-browserify": "^1.0.0", + "inherits": "~2.0.1", + "insert-module-globals": "^7.0.0", + "labeled-stream-splicer": "^2.0.0", + "mkdirp": "^0.5.0", + "module-deps": "^6.0.0", + "os-browserify": "~0.3.0", + "parents": "^1.0.1", + "path-browserify": "~0.0.0", + "process": "~0.11.0", + "punycode": "^1.3.2", + "querystring-es3": "~0.2.0", + "read-only-stream": "^2.0.0", + "readable-stream": "^2.0.2", + "resolve": "^1.1.4", + "shasum": "^1.0.0", + "shell-quote": "^1.6.1", + "stream-browserify": "^2.0.0", + "stream-http": "^2.0.0", + "string_decoder": "^1.1.1", + "subarg": "^1.0.0", + "syntax-error": "^1.1.1", + "through2": "^2.0.0", + "timers-browserify": "^1.0.1", + "tty-browserify": "0.0.1", + "url": "~0.11.0", + "util": "~0.10.1", + "vm-browserify": "^1.0.0", + "xtend": "^4.0.0" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "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.6.6", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.6.6.tgz", @@ -1346,6 +1829,16 @@ "node-releases": "^1.1.25" } }, + "buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.2.1.tgz", + "integrity": "sha512-c+Ko0loDaFfuPWiL02ls9Xd3GO3cPVmUobQ6t3rXNUk304u6hGq+8N/kFi+QEIKhzK3uwolVhLzszmfLmMLnqg==", + "dev": true, + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4" + } + }, "buffer-equal": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.0.tgz", @@ -1358,6 +1851,18 @@ "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 + }, "cache-base": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", @@ -1375,6 +1880,12 @@ "unset-value": "^1.0.0" } }, + "cached-path-relative": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.0.2.tgz", + "integrity": "sha512-5r2GqsoEb4qMTTN9J+WzXfjov+hjxT+j3u5K+kIVNIwAd99DLCJE9pBIMP1qVeybV6JiijL385Oz0DcYxfbOIg==", + "dev": true + }, "camelcase": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", @@ -1450,6 +1961,16 @@ } } }, + "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", @@ -1561,6 +2082,26 @@ "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", "dev": true }, + "combine-source-map": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/combine-source-map/-/combine-source-map-0.8.0.tgz", + "integrity": "sha1-pY0N8ELBhvz4IqjoAV9UUNLXmos=", + "dev": true, + "requires": { + "convert-source-map": "~1.1.0", + "inline-source-map": "~0.6.0", + "lodash.memoize": "~3.0.3", + "source-map": "~0.5.3" + }, + "dependencies": { + "convert-source-map": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.1.3.tgz", + "integrity": "sha1-SCnId+n+SbMWHzvzZziI4gRpmGA=", + "dev": true + } + } + }, "combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -1617,12 +2158,27 @@ } } }, + "console-browserify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", + "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", + "dev": true, + "requires": { + "date-now": "^0.1.4" + } + }, "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", @@ -1685,6 +2241,62 @@ "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" + } + }, + "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" + } + }, "currently-unhandled": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", @@ -1704,6 +2316,12 @@ "type": "^1.0.1" } }, + "dash-ast": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dash-ast/-/dash-ast-1.0.0.tgz", + "integrity": "sha512-Vy4dx7gquTeMcQR/hDkYLGUnwVil6vk4FOOct+djUnHOUWt+zJPJAaRIXaAFkPXtJjvlY7o3rfRu0/3hpnwoUA==", + "dev": true + }, "dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", @@ -1713,6 +2331,12 @@ "assert-plus": "^1.0.0" } }, + "date-now": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", + "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=", + "dev": true + }, "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -1734,6 +2358,11 @@ "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=" + }, "default-compare": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/default-compare/-/default-compare-1.0.0.tgz", @@ -1807,6 +2436,12 @@ } } }, + "defined": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", + "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=", + "dev": true + }, "del": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/del/-/del-5.0.0.tgz", @@ -1832,12 +2467,56 @@ "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", "dev": true }, + "deps-sort": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/deps-sort/-/deps-sort-2.0.0.tgz", + "integrity": "sha1-CRckkC6EZYJg65EHSMzNGvbiH7U=", + "dev": true, + "requires": { + "JSONStream": "^1.0.3", + "shasum": "^1.0.0", + "subarg": "^1.0.0", + "through2": "^2.0.0" + } + }, + "des.js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", + "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=", + "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 }, + "detective": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.0.tgz", + "integrity": "sha512-6SsIx+nUUbuK0EthKjv0zrdnajCCXVYGmbYYiYjFVpzcjwEs/JMDZ8tPRG29J/HhN56t3GJp2cGSWDRjjot8Pg==", + "dev": true, + "requires": { + "acorn-node": "^1.6.1", + "defined": "^1.0.0", + "minimist": "^1.1.1" + } + }, + "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" + } + }, "dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -1855,6 +2534,21 @@ } } }, + "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 + }, + "duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", + "dev": true, + "requires": { + "readable-stream": "^2.0.2" + } + }, "duplexify": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", @@ -1893,6 +2587,21 @@ "integrity": "sha512-jasjtY5RUy/TOyiUYM2fb4BDaPZfm6CXRFeJDMfFsXYADGxUN49RBqtgB7EL2RmJXeIRUk9lM1U6A5yk2YJMPQ==", "dev": true }, + "elliptic": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.0.tgz", + "integrity": "sha512-eFOJTMyCYb7xtE/caJ6JJu+bhi67WCYNbkGSknu20pmM8Ke/bqOfdnZWxyoGN26JgfxTbXrsCkEw4KheCT/KGg==", + "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" + } + }, "end-of-stream": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", @@ -1967,6 +2676,22 @@ "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", "dev": true }, + "events": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/events/-/events-2.1.0.tgz", + "integrity": "sha512-3Zmiobend8P9DjmKAty0Era4jV8oJ0yGYe2nJJAxgymF9+N8F2m0hhZiMoWtcfepExzNKZumFU3ksdQbInGWCg==", + "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" + } + }, "expand-brackets": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", @@ -2979,6 +3704,12 @@ "globule": "^1.0.0" } }, + "get-assigned-identifiers": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-assigned-identifiers/-/get-assigned-identifiers-1.2.0.tgz", + "integrity": "sha512-mBBwmeGTrxEMO4pMaaf/uUEFHnYtwr8FTe8Y/mer4rcV/bye0qGm6pw1bGZFGStxC5O76c5ZAVBGnqHmOaJpdQ==", + "dev": true + }, "get-caller-file": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", @@ -3264,6 +3995,15 @@ "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", @@ -3331,6 +4071,45 @@ } } }, + "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.0", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.0.tgz", + "integrity": "sha512-0XsbTXxgiaCDYDIWFcwkmerZPSwywfUqYmwT4jzewKTQSWoE6FCMoUVOeBJWK3E/CrWbxRG3m5GzY4lnIwGRBA==", + "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", @@ -3346,6 +4125,12 @@ "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==", "dev": true }, + "htmlescape": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/htmlescape/-/htmlescape-1.1.1.tgz", + "integrity": "sha1-OgPtwiFLyjtmQko+eVk0lQnLA1E=", + "dev": true + }, "http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", @@ -3357,6 +4142,18 @@ "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 + }, + "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 + }, "ignore": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.2.tgz", @@ -3400,6 +4197,33 @@ "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", "dev": true }, + "inline-source-map": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/inline-source-map/-/inline-source-map-0.6.2.tgz", + "integrity": "sha1-+Tk0ccGKedFyT4Y/o4tYY3Ct4qU=", + "dev": true, + "requires": { + "source-map": "~0.5.3" + } + }, + "insert-module-globals": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/insert-module-globals/-/insert-module-globals-7.2.0.tgz", + "integrity": "sha512-VE6NlW+WGn2/AeOMd496AHFYmE7eLKkUY6Ty31k4og5vmA3Fjuwe9v6ifH6Xx/Hz27QvdoMoviw1/pqWRB09Sw==", + "dev": true, + "requires": { + "JSONStream": "^1.0.3", + "acorn-node": "^1.5.2", + "combine-source-map": "^0.8.0", + "concat-stream": "^1.6.1", + "is-buffer": "^1.1.0", + "path-is-absolute": "^1.0.1", + "process": "~0.11.0", + "through2": "^2.0.0", + "undeclared-identifiers": "^1.1.2", + "xtend": "^4.0.0" + } + }, "interpret": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.2.0.tgz", @@ -3410,7 +4234,6 @@ "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", - "dev": true, "requires": { "loose-envify": "^1.0.0" } @@ -3678,6 +4501,11 @@ "integrity": "sha512-M7kLczedRMYX4L8Mdh4MzyAMM9O5osx+4FcOQuTvr3A9F2D9S5JXheN0ewNbrvK2UatkTRhL5ejGmGSjNMiZuw==", "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", @@ -3687,8 +4515,7 @@ "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "jsbn": { "version": "0.1.1", @@ -3714,6 +4541,15 @@ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, + "json-stable-stringify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-0.0.1.tgz", + "integrity": "sha1-YRwj6BTbN1Un34URk9tZ3Sryf0U=", + "dev": true, + "requires": { + "jsonify": "~0.0.0" + } + }, "json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -3735,6 +4571,18 @@ "minimist": "^1.2.0" } }, + "jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", + "dev": true + }, + "jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", + "dev": true + }, "jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", @@ -3759,6 +4607,16 @@ "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", "dev": true }, + "labeled-stream-splicer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/labeled-stream-splicer/-/labeled-stream-splicer-2.0.2.tgz", + "integrity": "sha512-Ca4LSXFFZUjPScRaqOcFxneA0VpKZr4MMYCljyQr4LIewTLb3Y0IUTIsnBBsVubIeEfxeSZpSjSsRM8APEQaAw==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "stream-splicer": "^2.0.0" + } + }, "last-run": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/last-run/-/last-run-1.1.1.tgz", @@ -3844,10 +4702,9 @@ } }, "lodash": { - "version": "4.17.14", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.14.tgz", - "integrity": "sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw==", - "dev": true + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" }, "lodash.clonedeep": { "version": "4.5.0", @@ -3855,11 +4712,16 @@ "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", "dev": true }, + "lodash.memoize": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-3.0.4.tgz", + "integrity": "sha1-LcvSwofLwKVcxCMovQxzYVDVPj8=", + "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==", - "dev": true, "requires": { "js-tokens": "^3.0.0 || ^4.0.0" } @@ -3949,6 +4811,17 @@ } } }, + "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" + } + }, "meow": { "version": "3.7.0", "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", @@ -3994,6 +4867,16 @@ "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.40.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", @@ -4009,6 +4892,18 @@ "mime-db": "1.40.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", @@ -4062,6 +4957,29 @@ } } }, + "module-deps": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/module-deps/-/module-deps-6.2.1.tgz", + "integrity": "sha512-UnEn6Ah36Tu4jFiBbJVUtt0h+iXqxpLqDvPS8nllbw5RZFmNJ1+Mz5BjYnM9ieH80zyxHkARGLnMIHlPK5bu6A==", + "dev": true, + "requires": { + "JSONStream": "^1.0.3", + "browser-resolve": "^1.7.0", + "cached-path-relative": "^1.0.2", + "concat-stream": "~1.6.0", + "defined": "^1.0.0", + "detective": "^5.0.2", + "duplexer2": "^0.1.2", + "inherits": "^2.0.1", + "parents": "^1.0.0", + "readable-stream": "^2.0.2", + "resolve": "^1.4.0", + "stream-combiner2": "^1.1.1", + "subarg": "^1.0.0", + "through2": "^2.0.0", + "xtend": "^4.0.0" + } + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -4307,8 +5225,7 @@ "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, "object-copy": { "version": "0.1.0", @@ -4427,6 +5344,12 @@ "readable-stream": "^2.0.1" } }, + "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", @@ -4488,6 +5411,35 @@ "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 + }, + "parents": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parents/-/parents-1.0.1.tgz", + "integrity": "sha1-/t1NK/GTp3dF/nHjcdc8MwfZx1E=", + "dev": true, + "requires": { + "path-platform": "~0.11.15" + } + }, + "parse-asn1": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.4.tgz", + "integrity": "sha512-Qs5duJcuvNExRfFZ99HDD3z4mAi3r9Wl/FOjEOijlxwCZs7E7mW2vjTpgQ4J8LpTF8x5v+1Vn5UQFejmWT11aw==", + "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-filepath": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", @@ -4526,6 +5478,12 @@ "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", @@ -4556,6 +5514,12 @@ "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", "dev": true }, + "path-platform": { + "version": "0.11.15", + "resolved": "https://registry.npmjs.org/path-platform/-/path-platform-0.11.15.tgz", + "integrity": "sha1-6GQhf3TDaFDwhSt43Hv31KVyG/I=", + "dev": true + }, "path-root": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", @@ -4590,6 +5554,19 @@ } } }, + "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", @@ -4677,12 +5654,28 @@ "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 }, + "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" + } + }, "pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", @@ -4695,6 +5688,20 @@ "integrity": "sha512-GEn74ZffufCmkDDLNcl3uuyF/aSD6exEyh1v/ZSdAomB82t6G9hzJVRx0jBmLDW+VfZqks3aScmMw9DszwUalA==", "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", @@ -4728,6 +5735,88 @@ "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", "dev": true }, + "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.8.6", + "resolved": "https://registry.npmjs.org/react/-/react-16.8.6.tgz", + "integrity": "sha512-pC0uMkhLaHm11ZSJULfOBqV4tIZkx87ZLvbbQYunNixAAvjnC+snJCg0XQXn9VIsttVsbZP/H/ewzgsd5fxKXw==", + "dev": true, + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "scheduler": "^0.13.6" + } + }, + "react-dom": { + "version": "16.8.6", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.8.6.tgz", + "integrity": "sha512-1nL7PIq9LTL3fthPqwkvr2zY7phIPjYrT0jp4HjyEQrEROnw4dG41VVwi/wfoCneoleqrNX7iAD+pXebJZwrwA==", + "dev": true, + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "scheduler": "^0.13.6" + } + }, + "react-is": { + "version": "16.8.6", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz", + "integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==" + }, + "react-redux": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.1.0.tgz", + "integrity": "sha512-hyu/PoFK3vZgdLTg9ozbt7WF3GgX5+Yn3pZm5/96/o4UueXA+zj08aiSC9Mfj2WtD1bvpIb3C5yvskzZySzzaw==", + "requires": { + "@babel/runtime": "^7.4.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.8.6" + } + }, + "read-only-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-only-stream/-/read-only-stream-2.0.0.tgz", + "integrity": "sha1-JyT9aoET1zdkrCiNQ4YnDB2/F/A=", + "dev": true, + "requires": { + "readable-stream": "^2.0.2" + } + }, "read-pkg": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", @@ -4815,6 +5904,28 @@ "strip-indent": "^1.0.1" } }, + "redux": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.0.4.tgz", + "integrity": "sha512-vKv4WdiJxOWKxK0yRoaK3Y4pxxB0ilzVx6dszU2W8wLxlb2yikRph4iV/ymtdJ6ZxpBLFbyrxklnT5yBbQSl3Q==", + "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-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", @@ -4830,6 +5941,11 @@ "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.0", "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.0.tgz", @@ -5052,6 +6168,16 @@ "glob": "^7.1.3" } }, + "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" + } + }, "run-parallel": { "version": "1.1.9", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.9.tgz", @@ -5233,6 +6359,16 @@ } } }, + "scheduler": { + "version": "0.13.6", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.13.6.tgz", + "integrity": "sha512-IWnObHt413ucAYKsD9J1QShUKkbKLQQHdxRyw73sw4FN26iWr3DY/H34xGPe4nmL1DwXyWmSWmMrA9TfQbE/XQ==", + "dev": true, + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, "scss-tokenizer": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz", @@ -5298,12 +6434,50 @@ } } }, + "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" + } + }, + "shasum": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/shasum/-/shasum-1.0.2.tgz", + "integrity": "sha1-5wEjENj0F/TetXEhUOVni4euVl8=", + "dev": true, + "requires": { + "json-stable-stringify": "~0.0.0", + "sha.js": "~2.4.4" + } + }, + "shell-quote": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.6.1.tgz", + "integrity": "sha1-9HgZSczkAmlxJ0MOo7PFR29IF2c=", + "dev": true, + "requires": { + "array-filter": "~0.0.0", + "array-map": "~0.0.0", + "array-reduce": "~0.0.0", + "jsonify": "~0.0.0" + } + }, "signal-exit": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", "dev": true }, + "simple-concat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.0.tgz", + "integrity": "sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY=", + "dev": true + }, "slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -5560,18 +6734,61 @@ "readable-stream": "^2.0.1" } }, + "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-combiner2": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stream-combiner2/-/stream-combiner2-1.1.1.tgz", + "integrity": "sha1-+02KFCDqNidk4hrUeAOXvry0HL4=", + "dev": true, + "requires": { + "duplexer2": "~0.1.0", + "readable-stream": "^2.0.2" + } + }, "stream-exhaust": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/stream-exhaust/-/stream-exhaust-1.0.2.tgz", "integrity": "sha512-b/qaq/GlBK5xaq1yrK9/zFcyRSTNxmcZwFLGSTG0mXgZl/4Z6GgiyYOXOvY7N3eEvFRAG1bkDRz5EPGSvPYQlw==", "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" + } + }, "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 }, + "stream-splicer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/stream-splicer/-/stream-splicer-2.0.1.tgz", + "integrity": "sha512-Xizh4/NPuYSyAXyT7g8IvdJ9HJpxIGL9PjyhtywCZvvP0OPIdqyrr4dMikeuvY8xahpdKEBlBTySe583totajg==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.2" + } + }, "string-width": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", @@ -5630,6 +6847,15 @@ "get-stdin": "^4.0.1" } }, + "subarg": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/subarg/-/subarg-1.0.0.tgz", + "integrity": "sha1-9izxdYHplrSPyWVpn1TAauJouNI=", + "dev": true, + "requires": { + "minimist": "^1.1.0" + } + }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -5649,6 +6875,20 @@ "es6-symbol": "^3.1.1" } }, + "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==" + }, + "syntax-error": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/syntax-error/-/syntax-error-1.4.0.tgz", + "integrity": "sha512-YPPlu67mdnHGTup2A8ff7BC2Pjq0e0Yp/IyTFN03zWO0RcK07uLcbi7C2KpGR2FvWbaB0+bfE27a+sBKebSo7w==", + "dev": true, + "requires": { + "acorn-node": "^1.2.0" + } + }, "tar": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz", @@ -5660,6 +6900,12 @@ "inherits": "2" } }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, "through2": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", @@ -5686,6 +6932,15 @@ "integrity": "sha1-dkpaEa9QVhkhsTPztE5hhofg9cM=", "dev": true }, + "timers-browserify": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-1.4.2.tgz", + "integrity": "sha1-ycWLV1voQHN1y14kYtrO50NZ9B0=", + "dev": true, + "requires": { + "process": "~0.11.0" + } + }, "to-absolute-glob": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz", @@ -5696,6 +6951,12 @@ "is-negated-glob": "^1.0.0" } }, + "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", @@ -5792,6 +7053,12 @@ "glob": "^7.1.2" } }, + "tty-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz", + "integrity": "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==", + "dev": true + }, "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -5819,12 +7086,31 @@ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", "dev": true }, + "umd": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/umd/-/umd-3.0.3.tgz", + "integrity": "sha512-4IcGSufhFshvLNcMCV80UnQVlZ5pMOC8mvNPForqwA4+lzYQuetTESLDQkeLmihq8bRcnpbQa48Wb8Lh16/xow==", + "dev": true + }, "unc-path-regex": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=", "dev": true }, + "undeclared-identifiers": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/undeclared-identifiers/-/undeclared-identifiers-1.1.3.tgz", + "integrity": "sha512-pJOW4nxjlmfwKApE4zvxLScM/njmwj/DiUBv7EabwE4O8kRUy+HIwxQtZLBPll/jx1LJyBcqNfB3/cpv9EZwOw==", + "dev": true, + "requires": { + "acorn-node": "^1.3.0", + "dash-ast": "^1.0.0", + "get-assigned-identifiers": "^1.2.0", + "simple-concat": "^1.0.0", + "xtend": "^4.0.1" + } + }, "undertaker": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/undertaker/-/undertaker-1.2.1.tgz", @@ -5959,12 +7245,47 @@ "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": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", + "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", + "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 + } + } + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -6027,6 +7348,16 @@ "replace-ext": "^1.0.0" } }, + "vinyl-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/vinyl-buffer/-/vinyl-buffer-1.0.1.tgz", + "integrity": "sha1-lsGjR5uMU5JULGEgKQE7Wyf4i78=", + "dev": true, + "requires": { + "bl": "^1.2.1", + "through2": "^2.0.3" + } + }, "vinyl-fs": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-3.0.3.tgz", @@ -6052,6 +7383,16 @@ "vinyl-sourcemap": "^1.1.0" } }, + "vinyl-source-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vinyl-source-stream/-/vinyl-source-stream-2.0.0.tgz", + "integrity": "sha1-84pa+53R6Ttl1VBGmsYYKsT1S44=", + "dev": true, + "requires": { + "through2": "^2.0.3", + "vinyl": "^2.1.0" + } + }, "vinyl-sourcemap": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/vinyl-sourcemap/-/vinyl-sourcemap-1.1.0.tgz", @@ -6076,6 +7417,12 @@ "source-map": "^0.5.1" } }, + "vm-browserify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.0.tgz", + "integrity": "sha512-iq+S7vZJE60yejDYM0ek6zg308+UZsdtPExWP9VZoCFCz1zkJoXFnAX7aZfd/ZwrkidzdUZL0C/ryW+JwAiIGw==", + "dev": true + }, "which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", diff --git a/package.json b/package.json index 5e339a4..5eca7dd 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,8 @@ "description": "Application for viewing RSS feeds", "main": "index.js", "scripts": { - "lint": "prettier \"src/newsreader/**/*.js\" --check", - "format": "prettier \"src/newsreader/**/*.js\" --write" + "lint": "prettier \"src/newsreader/js/**/*.js\" --check", + "format": "prettier \"src/newsreader/js/**/*.js\" --write" }, "repository": { "type": "git", @@ -13,21 +13,27 @@ }, "author": "Sonny", "license": "GPL-3.0-or-later", - "prettier": { - "semi": true, - "trailingComma": "es5", - "singleQuote": false, - "printWidth": 80, - "tabWidth": 2, - "useTabs": false, - "bracketSpacing": false, - "arrowParens": "always" + "dependencies": { + "js-cookie": "^2.2.1", + "lodash": "^4.17.15", + "react-redux": "^7.1.0", + "redux": "^4.0.4", + "redux-logger": "^3.0.6", + "redux-thunk": "^2.3.0" }, - "dependencies": {}, "devDependencies": { "@babel/core": "^7.5.4", + "@babel/plugin-proposal-class-properties": "^7.5.5", + "@babel/plugin-proposal-function-bind": "^7.2.0", + "@babel/plugin-syntax-dynamic-import": "^7.2.0", + "@babel/plugin-syntax-function-bind": "^7.2.0", + "@babel/plugin-transform-react-jsx": "^7.3.0", + "@babel/plugin-transform-runtime": "^7.5.5", "@babel/preset-env": "^7.5.4", "@babel/register": "^7.4.4", + "@babel/runtime": "^7.5.5", + "babelify": "^10.0.0", + "browserify": "^16.3.0", "del": "^5.0.0", "gulp": "^4.0.2", "gulp-babel": "^8.0.0-beta.2", @@ -35,6 +41,10 @@ "gulp-concat": "^2.6.1", "gulp-sass": "^4.0.2", "node-sass": "^4.12.0", - "prettier": "^1.18.2" + "prettier": "^1.18.2", + "react": "^16.8.6", + "react-dom": "^16.8.6", + "vinyl-buffer": "^1.0.1", + "vinyl-source-stream": "^2.0.0" } } diff --git a/requirements/base.txt b/requirements/base.txt index 25bf4ff..b3bcaf7 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -6,7 +6,7 @@ chardet==3.0.4 Django==2.2 django-celery-beat==1.5.0 djangorestframework==3.9.4 -django-rest-swagger-2.2.0 +django-rest-swagger==2.2.0 lxml==4.3.4 feedparser==5.2.1 idna==2.8 diff --git a/requirements/gitlab.txt b/requirements/gitlab.txt index a0b3eca..5e3e231 100644 --- a/requirements/gitlab.txt +++ b/requirements/gitlab.txt @@ -1,6 +1,5 @@ --r base.txt +-r testing.txt -factory-boy==2.12.0 -freezegun==0.3.12 black==19.3b0 isort==4.3.20 +autoflake==1.3 diff --git a/src/newsreader/accounts/views.py b/src/newsreader/accounts/views.py index ae33591..4957350 100644 --- a/src/newsreader/accounts/views.py +++ b/src/newsreader/accounts/views.py @@ -3,13 +3,11 @@ from django.contrib.auth.views import LogoutView as DjangoLogoutView from django.urls import reverse_lazy -# TODO redirect to homepage when logged in class LoginView(DjangoLoginView): template_name = "accounts/login.html" def get_success_url(self): - # TODO redirect to homepage - return reverse_lazy("admin:index") + return reverse_lazy("index") class LogoutView(DjangoLogoutView): diff --git a/src/newsreader/conf/base.py b/src/newsreader/conf/base.py index 6d5e1b0..e8bcc68 100644 --- a/src/newsreader/conf/base.py +++ b/src/newsreader/conf/base.py @@ -112,6 +112,8 @@ USE_TZ = True # https://docs.djangoproject.com/en/2.2/howto/static-files/ STATIC_URL = "/static/" +STATICFILES_DIRS = ["src/newsreader/static/icons"] + # Third party settings REST_FRAMEWORK = { "DEFAULT_AUTHENTICATION_CLASSES": ( @@ -123,3 +125,9 @@ REST_FRAMEWORK = { ), "DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",), } + +SWAGGER_SETTINGS = { + "LOGIN_URL": "rest_framework:login", + "LOGOUT_URL": "rest_framework:logout", + "DOC_EXPANSION": "list", +} diff --git a/src/newsreader/fixtures/default-fixture.json b/src/newsreader/fixtures/default-fixture.json index 514930a..e0ed80f 100644 --- a/src/newsreader/fixtures/default-fixture.json +++ b/src/newsreader/fixtures/default-fixture.json @@ -1 +1,282 @@ -[{"model": "accounts.user", "pk": 1, "fields": {"password": "pbkdf2_sha256$150000$OeNoz2LRSpI5$jkkUf/BjTuWZULyldvNTt9f45/ErxaCmCQHfZwtzji8=", "last_login": "2019-08-10T18:07:27.224Z", "is_superuser": true, "first_name": "", "last_name": "", "is_staff": true, "is_active": true, "date_joined": "2019-08-10T18:07:19.699Z", "email": "sonny@newsreader.nl", "task": null, "task_interval": null, "groups": [], "user_permissions": []}}, {"model": "django_celery_beat.intervalschedule", "pk": 1, "fields": {"every": 5, "period": "minutes"}}, {"model": "django_celery_beat.intervalschedule", "pk": 2, "fields": {"every": 30, "period": "minutes"}}, {"model": "django_celery_beat.intervalschedule", "pk": 3, "fields": {"every": 3, "period": "hours"}}, {"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.periodictasks", "pk": 1, "fields": {"last_update": "2019-08-10T20:01:21.152Z"}}, {"model": "django_celery_beat.periodictask", "pk": 1, "fields": {"name": "celery.backend_cleanup", "task": "celery.backend_cleanup", "interval": null, "crontab": 1, "solar": null, "clocked": null, "args": "[]", "kwargs": "{}", "queue": null, "exchange": null, "routing_key": null, "headers": "{}", "priority": null, "expires": null, "one_off": false, "start_time": null, "enabled": true, "last_run_at": null, "total_run_count": 0, "date_changed": "2019-08-10T19:56:33.160Z", "description": ""}}, {"model": "django_celery_beat.periodictask", "pk": 3, "fields": {"name": "Collection testing task", "task": "newsreader.news.collection.tasks.collect", "interval": 3, "crontab": null, "solar": null, "clocked": null, "args": "[1]", "kwargs": "{}", "queue": null, "exchange": null, "routing_key": null, "headers": "{}", "priority": null, "expires": null, "one_off": false, "start_time": "2019-08-10T20:00:52Z", "enabled": true, "last_run_at": null, "total_run_count": 0, "date_changed": "2019-08-10T20:01:21.153Z", "description": ""}}, {"model": "core.category", "pk": 1, "fields": {"created": "2019-08-10T19:57:53Z", "modified": "2019-08-10T19:58:02.048Z", "name": "Tech", "user": 1}}, {"model": "core.category", "pk": 2, "fields": {"created": "2019-08-10T19:58:13Z", "modified": "2019-08-10T19:58:20.951Z", "name": "News", "user": 1}}, {"model": "collection.collectionrule", "pk": 1, "fields": {"created": "2019-08-10T18:08:10.520Z", "modified": "2019-08-10T19:59:54.547Z", "name": "Tweakers", "url": "http://feeds.feedburner.com/tweakers/mixed", "website_url": null, "favicon": null, "timezone": "Europe/Amsterdam", "category": 1, "last_suceeded": "2019-08-10T18:25:32.325Z", "succeeded": true, "error": null, "user": 1}}, {"model": "collection.collectionrule", "pk": 2, "fields": {"created": "2019-08-10T19:58:05.615Z", "modified": "2019-08-10T19:58:05.691Z", "name": "Hackers News", "url": "https://news.ycombinator.com/rss", "website_url": null, "favicon": null, "timezone": "UTC", "category": 1, "last_suceeded": null, "succeeded": false, "error": null, "user": 1}}, {"model": "collection.collectionrule", "pk": 3, "fields": {"created": "2019-08-10T19:58:23.441Z", "modified": "2019-08-10T19:58:23.449Z", "name": "BBC", "url": "http://feeds.bbci.co.uk/news/world/rss.xml", "website_url": null, "favicon": null, "timezone": "UTC", "category": 2, "last_suceeded": null, "succeeded": false, "error": null, "user": 1}}, {"model": "collection.collectionrule", "pk": 4, "fields": {"created": "2019-08-10T19:58:31.867Z", "modified": "2019-08-10T19:58:31.873Z", "name": "Ars Technica", "url": "http://feeds.arstechnica.com/arstechnica/index?fmt=xml", "website_url": null, "favicon": null, "timezone": "UTC", "category": 1, "last_suceeded": null, "succeeded": false, "error": null, "user": 1}}, {"model": "collection.collectionrule", "pk": 5, "fields": {"created": "2019-08-10T19:58:48.529Z", "modified": "2019-08-10T19:58:48.535Z", "name": "The Guardian", "url": "https://www.theguardian.com/world/rss", "website_url": null, "favicon": null, "timezone": "UTC", "category": 2, "last_suceeded": null, "succeeded": false, "error": null, "user": 1}}, {"model": "collection.collectionrule", "pk": 6, "fields": {"created": "2019-08-10T19:58:58.641Z", "modified": "2019-08-10T19:58:58.647Z", "name": "Engadget", "url": "https://www.engadget.com/rss.xml", "website_url": null, "favicon": null, "timezone": "UTC", "category": 1, "last_suceeded": null, "succeeded": false, "error": null, "user": 1}}, {"model": "collection.collectionrule", "pk": 7, "fields": {"created": "2019-08-10T19:59:29.909Z", "modified": "2019-08-10T19:59:29.917Z", "name": "The Verge", "url": "https://www.theverge.com/rss/index.xml", "website_url": null, "favicon": null, "timezone": "UTC", "category": 1, "last_suceeded": null, "succeeded": false, "error": null, "user": 1}}, {"model": "collection.collectionrule", "pk": 8, "fields": {"created": "2019-08-10T19:59:44.833Z", "modified": "2019-08-10T19:59:44.838Z", "name": "News", "url": "http://feeds.boingboing.net/boingboing/iBag", "website_url": null, "favicon": null, "timezone": "UTC", "category": 2, "last_suceeded": null, "succeeded": false, "error": null, "user": 1}}] \ No newline at end of file +[ + { + "fields" : { + "last_login" : "2019-08-10T18:07:27.224Z", + "last_name" : "", + "is_superuser" : true, + "is_staff" : true, + "task_interval" : null, + "email" : "sonny@newsreader.nl", + "user_permissions" : [], + "groups" : [], + "password" : "pbkdf2_sha256$150000$OeNoz2LRSpI5$jkkUf/BjTuWZULyldvNTt9f45/ErxaCmCQHfZwtzji8=", + "first_name" : "", + "task" : null, + "is_active" : true, + "date_joined" : "2019-08-10T18:07:19.699Z" + }, + "pk" : 1, + "model" : "accounts.user" + }, + { + "pk" : 1, + "model" : "django_celery_beat.intervalschedule", + "fields" : { + "period" : "minutes", + "every" : 5 + } + }, + { + "pk" : 2, + "model" : "django_celery_beat.intervalschedule", + "fields" : { + "every" : 30, + "period" : "minutes" + } + }, + { + "fields" : { + "every" : 3, + "period" : "hours" + }, + "model" : "django_celery_beat.intervalschedule", + "pk" : 3 + }, + { + "fields" : { + "minute" : "0", + "timezone" : "UTC", + "day_of_month" : "*", + "hour" : "4", + "month_of_year" : "*", + "day_of_week" : "*" + }, + "model" : "django_celery_beat.crontabschedule", + "pk" : 1 + }, + { + "fields" : { + "last_update" : "2019-08-10T20:01:21.152Z" + }, + "model" : "django_celery_beat.periodictasks", + "pk" : 1 + }, + { + "model" : "django_celery_beat.periodictask", + "pk" : 1, + "fields" : { + "description" : "", + "last_run_at" : null, + "routing_key" : null, + "kwargs" : "{}", + "queue" : null, + "name" : "celery.backend_cleanup", + "clocked" : null, + "solar" : null, + "task" : "celery.backend_cleanup", + "one_off" : false, + "expires" : null, + "total_run_count" : 0, + "date_changed" : "2019-08-10T19:56:33.160Z", + "args" : "[]", + "start_time" : null, + "priority" : null, + "crontab" : 1, + "interval" : null, + "enabled" : true, + "headers" : "{}", + "exchange" : null + } + }, + { + "pk" : 3, + "model" : "django_celery_beat.periodictask", + "fields" : { + "exchange" : null, + "headers" : "{}", + "enabled" : true, + "priority" : null, + "crontab" : null, + "interval" : 3, + "start_time" : "2019-08-10T20:00:52Z", + "args" : "[1]", + "date_changed" : "2019-08-10T20:01:21.153Z", + "total_run_count" : 0, + "expires" : null, + "task" : "newsreader.news.collection.tasks.collect", + "one_off" : false, + "clocked" : null, + "solar" : null, + "name" : "Collection testing task", + "queue" : null, + "kwargs" : "{}", + "routing_key" : null, + "last_run_at" : null, + "description" : "" + } + }, + { + "fields" : { + "user" : 1, + "created" : "2019-08-10T19:57:53Z", + "name" : "Tech", + "modified" : "2019-08-10T19:58:02.048Z" + }, + "pk" : 1, + "model" : "core.category" + }, + { + "fields" : { + "modified" : "2019-08-10T19:58:20.951Z", + "user" : 1, + "created" : "2019-08-10T19:58:13Z", + "name" : "News" + }, + "pk" : 2, + "model" : "core.category" + }, + { + "pk" : 1, + "model" : "collection.collectionrule", + "fields" : { + "timezone" : "Europe/Amsterdam", + "succeeded" : true, + "error" : null, + "website_url" : null, + "favicon" : null, + "category" : 1, + "created" : "2019-08-10T18:08:10.520Z", + "user" : 1, + "name" : "Tweakers", + "url" : "http://feeds.feedburner.com/tweakers/mixed", + "last_suceeded" : "2019-08-10T18:25:32.325Z", + "modified" : "2019-08-10T19:59:54.547Z" + } + }, + { + "fields" : { + "favicon" : null, + "category" : 1, + "timezone" : "UTC", + "error" : null, + "website_url" : null, + "succeeded" : false, + "modified" : "2019-08-10T19:58:05.691Z", + "last_suceeded" : null, + "name" : "Hackers News", + "created" : "2019-08-10T19:58:05.615Z", + "user" : 1, + "url" : "https://news.ycombinator.com/rss" + }, + "pk" : 2, + "model" : "collection.collectionrule" + }, + { + "fields" : { + "timezone" : "UTC", + "error" : null, + "website_url" : null, + "succeeded" : false, + "favicon" : null, + "category" : 2, + "name" : "BBC", + "created" : "2019-08-10T19:58:23.441Z", + "user" : 1, + "url" : "http://feeds.bbci.co.uk/news/world/rss.xml", + "modified" : "2019-08-10T19:58:23.449Z", + "last_suceeded" : null + }, + "pk" : 3, + "model" : "collection.collectionrule" + }, + { + "fields" : { + "favicon" : null, + "category" : 1, + "timezone" : "UTC", + "succeeded" : false, + "error" : null, + "website_url" : null, + "last_suceeded" : null, + "modified" : "2019-08-10T19:58:31.873Z", + "user" : 1, + "created" : "2019-08-10T19:58:31.867Z", + "name" : "Ars Technica", + "url" : "http://feeds.arstechnica.com/arstechnica/index?fmt=xml" + }, + "model" : "collection.collectionrule", + "pk" : 4 + }, + { + "pk" : 5, + "model" : "collection.collectionrule", + "fields" : { + "last_suceeded" : null, + "modified" : "2019-08-10T19:58:48.535Z", + "url" : "https://www.theguardian.com/world/rss", + "created" : "2019-08-10T19:58:48.529Z", + "user" : 1, + "name" : "The Guardian", + "category" : 2, + "favicon" : null, + "succeeded" : false, + "error" : null, + "website_url" : null, + "timezone" : "UTC" + } + }, + { + "model" : "collection.collectionrule", + "pk" : 6, + "fields" : { + "error" : null, + "website_url" : null, + "succeeded" : false, + "timezone" : "UTC", + "category" : 1, + "favicon" : null, + "url" : "https://www.engadget.com/rss.xml", + "name" : "Engadget", + "created" : "2019-08-10T19:58:58.641Z", + "user" : 1, + "modified" : "2019-08-10T19:58:58.647Z", + "last_suceeded" : null + } + }, + { + "fields" : { + "last_suceeded" : null, + "modified" : "2019-08-10T19:59:29.917Z", + "user" : 1, + "created" : "2019-08-10T19:59:29.909Z", + "name" : "The Verge", + "url" : "https://www.theverge.com/rss/index.xml", + "favicon" : null, + "category" : 1, + "timezone" : "UTC", + "succeeded" : false, + "error" : null, + "website_url" : null + }, + "model" : "collection.collectionrule", + "pk" : 7 + }, + { + "pk" : 8, + "model" : "collection.collectionrule", + "fields" : { + "modified" : "2019-08-10T19:59:44.838Z", + "last_suceeded" : null, + "name" : "News", + "user" : 1, + "created" : "2019-08-10T19:59:44.833Z", + "url" : "http://feeds.boingboing.net/boingboing/iBag", + "favicon" : null, + "category" : 2, + "timezone" : "UTC", + "website_url" : null, + "error" : null, + "succeeded" : false + } + } +] 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/homepage/App.js b/src/newsreader/js/homepage/App.js new file mode 100644 index 0000000..fa9bc2c --- /dev/null +++ b/src/newsreader/js/homepage/App.js @@ -0,0 +1,63 @@ +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'; + +class App extends React.Component { + componentDidMount() { + this.props.fetchCategories(); + } + + render() { + return ( + <> +
+ + + + {!isEqual(this.props.post, {}) && ( + + )} +
+ + ); + } +} + +const mapStateToProps = state => { + 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 { + post: state.selected.post, + rule, + category, + }; + } + + return { + post: state.selected.post, + }; +}; + +const mapDispatchToProps = dispatch => ({ + fetchCategories: () => dispatch(fetchCategories()), +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(App); diff --git a/src/newsreader/js/homepage/actions/categories.js b/src/newsreader/js/homepage/actions/categories.js new file mode 100644 index 0000000..07a1066 --- /dev/null +++ b/src/newsreader/js/homepage/actions/categories.js @@ -0,0 +1,86 @@ +import { requestRules, receiveRules, fetchRulesByCategory } from './rules.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, + item: category, +}); + +export const receiveCategory = category => ({ + type: RECEIVE_CATEGORY, + category, +}); + +export const receiveCategories = json => ({ + type: RECEIVE_CATEGORIES, + categories: json, +}); + +export const requestCategory = () => ({ type: REQUEST_CATEGORY }); +export const requestCategories = () => ({ type: REQUEST_CATEGORIES }); + +export const fetchCategories = () => { + return dispatch => { + dispatch(requestCategories()); + + return fetch('/api/categories/') + .then(response => response.json()) + .then(json => { + const categories = {}; + + json.forEach(category => { + categories[category.id] = { ...category }; + }); + + dispatch(receiveCategories(categories)); + return json; + }) + .then(json => { + const promises = json.map(category => { + return fetch(`/api/categories/${category.id}/rules/`); + }); + + dispatch(requestRules()); + return Promise.all(promises); + }) + .then(responses => { + return Promise.all(responses.map(response => response.json())); + }) + .then(responseData => { + let rules = {}; + + responseData.forEach(json => { + const data = Object.values(json); + + data.forEach(item => { + rules = { ...rules, [item.id]: item }; + }); + }); + + setTimeout(dispatch, 500, receiveRules(rules)); + }); + }; +}; + +export const fetchCategory = category => { + return dispatch => { + dispatch(requestCategory()); + + return fetch(`/api/categories/${category.id}`) + .then(response => response.json()) + .then(json => { + dispatch(receiveCategory({ ...json })); + + if (category.unread === 0) { + dispatch(fetchRulesByCategory(category)); + } + }); + }; +}; diff --git a/src/newsreader/js/homepage/actions/posts.js b/src/newsreader/js/homepage/actions/posts.js new file mode 100644 index 0000000..9951859 --- /dev/null +++ b/src/newsreader/js/homepage/actions/posts.js @@ -0,0 +1,119 @@ +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 selectPost = post => ({ + type: SELECT_POST, + post, +}); + +export const unSelectPost = () => ({ + type: UNSELECT_POST, +}); + +export const postRead = (post, rule, category) => ({ + type: MARK_POST_READ, + category: category, + post: post, + rule: rule, +}); + +export const markPostRead = (post, token) => { + return (dispatch, getState) => { + const { rules } = getState(); + const { categories } = 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 rule = rules.items[post.rule]; + const category = categories.items[rule.category]; + + return fetch(url, options) + .then(response => response.json()) + .then(updatedPost => { + const updatedRule = { ...rule, unread: rule.unread - 1 }; + const updatedCategory = { ...category, unread: category.unread - 1 }; + + dispatch(receivePost({ ...updatedPost })); + dispatch(postRead({ ...updatedPost }, updatedRule, updatedCategory)); + }); + }; +}; + +export const receivePosts = json => ({ + type: RECEIVE_POSTS, + posts: json.items, + next: json.next, +}); + +export const receivePost = post => ({ + type: RECEIVE_POST, + post, +}); + +export const requestPosts = () => ({ type: REQUEST_POSTS }); + +export const fetchPostsByCategory = (category, page = false) => { + return dispatch => { + dispatch(requestPosts()); + + const url = page ? page : `/api/categories/${category.id}/posts/?read=false`; + return fetch(url) + .then(response => response.json()) + .then(json => { + const posts = {}; + + json.results.forEach(post => { + posts[post.id] = post; + }); + + dispatch(receivePosts({ items: posts, next: json.next })); + }) + .catch(error => { + if (error instanceof TypeError) { + console.log(`Unable to parse posts from request: ${error}`); + } + + dispatch(receivePosts({ items: {}, next: null })); + }); + }; +}; + +export const fetchPostsByRule = (rule, page = false) => { + return dispatch => { + dispatch(requestPosts()); + + const url = page ? page : `/api/rules/${rule.id}/posts/?read=false`; + return fetch(url) + .then(response => response.json()) + .then(json => { + const posts = {}; + + json.results.forEach(post => { + posts[post.id] = post; + }); + + dispatch(receivePosts({ items: posts, next: json.next })); + }) + .catch(error => { + if (error instanceof TypeError) { + console.log(`Unable to parse posts from request: ${error}`); + } + + dispatch(receivePosts({ items: {}, next: null })); + }); + }; +}; diff --git a/src/newsreader/js/homepage/actions/rules.js b/src/newsreader/js/homepage/actions/rules.js new file mode 100644 index 0000000..0b843f6 --- /dev/null +++ b/src/newsreader/js/homepage/actions/rules.js @@ -0,0 +1,68 @@ +import { fetchCategory } from './categories.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, + item: rule, +}); + +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) => { + 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) { + dispatch(fetchCategory({ ...category })); + } + }); + }; +}; + +export const fetchRulesByCategory = category => { + return (dispatch, getState) => { + dispatch(requestRules()); + + return fetch(`/api/categories/${category.id}/rules/`) + .then(response => response.json()) + .then(responseData => { + dispatch(receiveRules()); + + const rules = {}; + + responseData.forEach(rule => { + rules[rule.id] = { ...rule }; + }); + + setTimeout(dispatch, 500, receiveRules(rules)); + }); + }; +}; diff --git a/src/newsreader/js/homepage/actions/selected.js b/src/newsreader/js/homepage/actions/selected.js new file mode 100644 index 0000000..95b8603 --- /dev/null +++ b/src/newsreader/js/homepage/actions/selected.js @@ -0,0 +1,65 @@ +import { receiveCategory, requestCategory } from './categories.js'; +import { receiveRule, requestRule } from './rules.js'; + +export const MARK_SECTION_READ = 'MARK_SECTION_READ'; + +export const markSectionRead = (category, rule = {}) => ({ + category: category, + rule: rule, + type: MARK_SECTION_READ, +}); + +const markCategoryRead = (category, token) => { + return dispatch => { + dispatch(requestCategory(category)); + + 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 })); + dispatch(markSectionRead({ ...category, ...updatedCategory })); + }); + }; +}; + +const markRuleRead = (rule, token) => { + return (dispatch, getState) => { + const { categories } = getState(); + const category = categories.items[rule.category]; + + 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({ ...category }, { ...rule })); + }); + }; +}; + +export const markRead = (selected, token) => { + if ('category' in selected) { + return markRuleRead(selected, token); + } else { + return markCategoryRead(selected, token); + } +}; diff --git a/src/newsreader/js/homepage/components/PostModal.js b/src/newsreader/js/homepage/components/PostModal.js new file mode 100644 index 0000000..4749ccc --- /dev/null +++ b/src/newsreader/js/homepage/components/PostModal.js @@ -0,0 +1,84 @@ +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 { + 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, 30000, post, token); + } + } + + componentWillUnmount() { + if (this.readTimer) { + clearTimeout(this.readTimer); + } + + this.readTimer = null; + } + + render() { + const post = this.props.post; + const publicationDate = formatDatetime(post.publication_date); + 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/homepage/components/feedlist/FeedList.js b/src/newsreader/js/homepage/components/feedlist/FeedList.js new file mode 100644 index 0000000..47fa5fb --- /dev/null +++ b/src/newsreader/js/homepage/components/feedlist/FeedList.js @@ -0,0 +1,102 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { isEqual } from 'lodash'; + +import { fetchPostsByRule, fetchPostsByCategory } 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() { + if ('category' in this.props.selected) { + return this.props.fetchPostsByRule(this.props.selected, this.props.next); + } else { + return this.props.fetchPostsByCategory(this.props.selected, this.props.next); + } + } + + render() { + const ruleItems = this.props.posts.map((item, index) => { + return ; + }); + + if (ruleItems.length > 0) { + return ( +
+ {ruleItems} + {this.props.isFetching && } +
+ ); + } else if (isEqual(this.props.selected, {})) { + return ( +
+
+ +

+ Select a category or rule to show its unread posts +

+
+
+ ); + } else if (ruleItems.length === 0) { + return ( +
+
+

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

+
+
+ ); + } else { + return ( +
{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 => ({ + fetchPostsByRule: (rule, page = false) => dispatch(fetchPostsByRule(rule, page)), + fetchPostsByCategory: (category, page = false) => { + dispatch(fetchPostsByCategory(category, page)); + }, +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(FeedList); diff --git a/src/newsreader/js/homepage/components/feedlist/PostItem.js b/src/newsreader/js/homepage/components/feedlist/PostItem.js new file mode 100644 index 0000000..6511830 --- /dev/null +++ b/src/newsreader/js/homepage/components/feedlist/PostItem.js @@ -0,0 +1,50 @@ +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.publication_date); + 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/homepage/components/feedlist/RuleItem.js b/src/newsreader/js/homepage/components/feedlist/RuleItem.js new file mode 100644 index 0000000..08cb3aa --- /dev/null +++ b/src/newsreader/js/homepage/components/feedlist/RuleItem.js @@ -0,0 +1,25 @@ +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.publication_date) - new Date(firstEl.publication_date); + }); + + const postItems = posts.map(post => { + return ; + }); + + return ( +
    +

    {this.props.rule.name}

    + {/* TODO: Add empty posts message */} +
      {postItems}
    +
    + ); + } +} + +export default RuleItem; diff --git a/src/newsreader/js/homepage/components/feedlist/filters.js b/src/newsreader/js/homepage/components/feedlist/filters.js new file mode 100644 index 0000000..c392775 --- /dev/null +++ b/src/newsreader/js/homepage/components/feedlist/filters.js @@ -0,0 +1,44 @@ +const isEmpty = (object = {}) => { + return Object.keys(object).length === 0; +}; + +export const filterPostsByRule = (rule = {}, posts = []) => { + const filteredPosts = posts.filter(post => { + return post.rule === rule.id && !post.read; + }); + + 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 && !post.read; + }); + + return { + rule: { ...rule }, + posts: filteredPosts, + }; + }); + + return filteredData.filter(rule => rule.posts.length > 0); +}; + +export const filterPosts = state => { + const posts = Object.values({ ...state.posts.items }); + + if (!isEmpty(state.selected.item) && !('category' in state.selected.item)) { + const rules = Object.values({ ...state.rules.items }); + + return filterPostsByCategory({ ...state.selected.item }, rules, posts); + } else if ('category' in state.selected.item) { + return filterPostsByRule({ ...state.selected.item }, posts); + } + + return []; +}; diff --git a/src/newsreader/js/homepage/components/sidebar/CategoryItem.js b/src/newsreader/js/homepage/components/sidebar/CategoryItem.js new file mode 100644 index 0000000..7644fd7 --- /dev/null +++ b/src/newsreader/js/homepage/components/sidebar/CategoryItem.js @@ -0,0 +1,68 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { isEqual } from 'lodash'; + +import { selectCategory, fetchCategory } from '../../actions/categories.js'; +import { fetchPostsByCategory } from '../../actions/posts.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.fetchPostsByCategory(category); + + if (category.unread === 0) { + this.props.fetchCategory(category); + } + } + + render() { + const imageSrc = this.state.open + ? '/static/chevron-down.svg' + : '/static/chevron-right.svg'; + const selected = isEqual(this.props.category, this.props.selected); + 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)), + fetchPostsByCategory: category => dispatch(fetchPostsByCategory(category)), + fetchCategory: category => dispatch(fetchCategory(category)), +}); + +export default connect( + null, + mapDispatchToProps +)(CategoryItem); diff --git a/src/newsreader/js/homepage/components/sidebar/ReadButton.js b/src/newsreader/js/homepage/components/sidebar/ReadButton.js new file mode 100644 index 0000000..45e3781 --- /dev/null +++ b/src/newsreader/js/homepage/components/sidebar/ReadButton.js @@ -0,0 +1,36 @@ +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/homepage/components/sidebar/RuleItem.js b/src/newsreader/js/homepage/components/sidebar/RuleItem.js new file mode 100644 index 0000000..0a55aab --- /dev/null +++ b/src/newsreader/js/homepage/components/sidebar/RuleItem.js @@ -0,0 +1,56 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { isEqual } from 'lodash'; + +import { selectRule, fetchRule } from '../../actions/rules.js'; +import { fetchPostsByRule } from '../../actions/posts.js'; + +class RuleItem extends React.Component { + handleSelect() { + const rule = { ...this.props.rule }; + + this.props.selectRule(rule); + this.props.fetchPostsByRule(rule); + + if (rule.unread === 0) { + this.props.fetchRule(rule); + } + } + + render() { + const selected = isEqual(this.props.selected, this.props.rule); + const className = `rules__item ${selected ? 'rules__item--selected' : ''}`; + + return ( +
  • this.handleSelect()}> +
    + {this.props.rule.favicon && ( + + + + )} +
    + {this.props.rule.name} +
    +
    + {this.props.rule.unread} +
  • + ); + } +} + +const mapDispatchToProps = dispatch => ({ + selectRule: rule => dispatch(selectRule(rule)), + fetchPostsByRule: rule => dispatch(fetchPostsByRule(rule)), + fetchRule: rule => dispatch(fetchRule(rule)), +}); + +export default connect( + null, + mapDispatchToProps +)(RuleItem); diff --git a/src/newsreader/js/homepage/components/sidebar/Sidebar.js b/src/newsreader/js/homepage/components/sidebar/Sidebar.js new file mode 100644 index 0000000..55595ea --- /dev/null +++ b/src/newsreader/js/homepage/components/sidebar/Sidebar.js @@ -0,0 +1,52 @@ +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 ( +
    + + + {!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/homepage/components/sidebar/filters.js b/src/newsreader/js/homepage/components/sidebar/filters.js new file mode 100644 index 0000000..5e51d6c --- /dev/null +++ b/src/newsreader/js/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/homepage/configureStore.js b/src/newsreader/js/homepage/configureStore.js new file mode 100644 index 0000000..e00952f --- /dev/null +++ b/src/newsreader/js/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/homepage/index.js b/src/newsreader/js/homepage/index.js new file mode 100644 index 0000000..c6ce2a2 --- /dev/null +++ b/src/newsreader/js/homepage/index.js @@ -0,0 +1,16 @@ +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 store = configureStore(); + +ReactDOM.render( + + + , + document.getElementsByClassName('content')[0] +); diff --git a/src/newsreader/js/homepage/reducers/categories.js b/src/newsreader/js/homepage/reducers/categories.js new file mode 100644 index 0000000..f34930e --- /dev/null +++ b/src/newsreader/js/homepage/reducers/categories.js @@ -0,0 +1,116 @@ +import { isEqual } from 'lodash'; + +import { + RECEIVE_CATEGORY, + RECEIVE_CATEGORIES, + REQUEST_CATEGORY, + REQUEST_CATEGORIES, +} from '../actions/categories.js'; + +import { RECEIVE_RULE, RECEIVE_RULES } 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 categories = (state = { ...defaultState }, action) => { + switch (action.type) { + case RECEIVE_CATEGORY: + return { + ...state, + items: { + ...state.items, + [action.category.id]: { ...action.category, rules: {} }, + }, + isFetching: false, + }; + case RECEIVE_CATEGORIES: + const receivedCategories = {}; + + Object.values({ ...action.categories }).forEach(category => { + receivedCategories[category.id] = { + ...category, + rules: {}, + }; + }); + + return { + ...state, + items: { ...state.items, ...receivedCategories }, + isFetching: false, + }; + case RECEIVE_RULE: + const category = { ...state.items[action.rule.category] }; + + category['rules'][action.rule.id] = { ...action.rule }; + + return { + ...state, + items: { + ...state.items, + [category.id]: { ...category }, + }, + }; + case RECEIVE_RULES: + const relevantCategories = {}; + + Object.values({ ...action.rules }).forEach(rule => { + if (!(rule.category in relevantCategories)) { + const category = { ...state.items[rule.category] }; + + relevantCategories[rule.category] = { + ...category, + rules: { + ...category.rules, + [rule.id]: { ...rule }, + }, + }; + } else { + relevantCategories[rule.category]['rules'][rule.id] = { ...rule }; + } + }); + + return { + ...state, + items: { + ...state.items, + ...relevantCategories, + }, + }; + case REQUEST_CATEGORIES: + case REQUEST_CATEGORY: + return { + ...state, + isFetching: true, + }; + case MARK_POST_READ: + return { + ...state, + items: { ...state.items, [action.category.id]: { ...action.category } }, + }; + case MARK_SECTION_READ: + if (!isEqual(action.rule, {})) { + return { + ...state, + items: { + ...state.items, + [action.category.id]: { + ...action.category, + unread: action.category.unread - action.rule.unread, + }, + }, + }; + } + + return { + ...state, + items: { + ...state.items, + [action.category.id]: { ...action.category, unread: 0 }, + }, + }; + default: + return state; + } +}; diff --git a/src/newsreader/js/homepage/reducers/index.js b/src/newsreader/js/homepage/reducers/index.js new file mode 100644 index 0000000..f70ca2a --- /dev/null +++ b/src/newsreader/js/homepage/reducers/index.js @@ -0,0 +1,10 @@ +import { combineReducers } from 'redux'; + +import { categories } from './categories.js'; +import { rules } from './rules.js'; +import { posts } from './posts.js'; +import { selected } from './selected.js'; + +const rootReducer = combineReducers({ categories, rules, posts, selected }); + +export default rootReducer; diff --git a/src/newsreader/js/homepage/reducers/posts.js b/src/newsreader/js/homepage/reducers/posts.js new file mode 100644 index 0000000..4f613ab --- /dev/null +++ b/src/newsreader/js/homepage/reducers/posts.js @@ -0,0 +1,67 @@ +import { isEqual } from 'lodash'; + +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 RECEIVE_POSTS: + return { + ...state, + type: RECEIVE_POSTS, + isFetching: false, + items: { ...state.items, ...action.posts }, + }; + case REQUEST_POSTS: + return { + ...state, + type: REQUEST_POSTS, + isFetching: true, + }; + case RECEIVE_POST: + const items = { ...state.items, [action.post.id]: { ...action.post } }; + + return { + ...state, + items: items, + type: RECEIVE_POST, + }; + case MARK_SECTION_READ: + const updatedPosts = {}; + let relatedPosts = []; + + if (!isEqual(action.rule, {})) { + relatedPosts = Object.values({ ...state.items }).filter(post => { + return post.rule === action.rule.id; + }); + } else { + relatedPosts = Object.values({ ...state.items }).filter(post => { + return post.rule in { ...action.category.rules }; + }); + } + + relatedPosts.forEach(post => { + updatedPosts[post.id] = { ...post, read: true }; + }); + + return { + ...state, + items: { + ...state.items, + ...updatedPosts, + }, + }; + + default: + return state; + } +}; diff --git a/src/newsreader/js/homepage/reducers/rules.js b/src/newsreader/js/homepage/reducers/rules.js new file mode 100644 index 0000000..f23c98f --- /dev/null +++ b/src/newsreader/js/homepage/reducers/rules.js @@ -0,0 +1,65 @@ +import { isEqual } from 'lodash'; + +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: + return { + ...state, + items: { ...state.items, ...action.rules }, + isFetching: false, + }; + case RECEIVE_RULE: + return { + ...state, + items: { ...state.items, [action.rule.id]: { ...action.rule } }, + isFetching: false, + }; + case MARK_POST_READ: + case MARK_SECTION_READ: + if (!isEqual(action.rule, {})) { + return { + ...state, + items: { + ...state.items, + [action.rule.id]: { + ...action.rule, + unread: 0, + }, + }, + }; + } + + const updatedRules = {}; + Object.values({ ...state.items }).forEach(rule => { + if (rule.category === action.category.id) { + updatedRules[rule.id] = { + ...rule, + unread: 0, + }; + } else { + updatedRules[rule.id] = { ...rule }; + } + }); + + return { ...state, items: { ...updatedRules } }; + default: + return state; + } +}; diff --git a/src/newsreader/js/homepage/reducers/selected.js b/src/newsreader/js/homepage/reducers/selected.js new file mode 100644 index 0000000..dea13db --- /dev/null +++ b/src/newsreader/js/homepage/reducers/selected.js @@ -0,0 +1,70 @@ +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'; + +const defaultState = { item: {}, next: false, lastReached: false, post: {} }; + +export const selected = (state = { ...defaultState }, action) => { + switch (action.type) { + case SELECT_CATEGORY: + case SELECT_RULE: + return { + ...state, + item: action.item, + 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_SECTION_READ: + if (!isEqual(action.rule, {})) { + return { + ...state, + item: { ...action.rule, unread: 0 }, + }; + } + + return { + ...state, + item: { ...action.category }, + }; + default: + return state; + } +}; diff --git a/src/newsreader/js/utils.js b/src/newsreader/js/utils.js new file mode 100644 index 0000000..0794a1a --- /dev/null +++ b/src/newsreader/js/utils.js @@ -0,0 +1,14 @@ +export const formatDatetime = dateString => { + const locale = navigator.language ? navigator.language : 'en-US'; + const dateOptions = { + hour: '2-digit', + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + }; + + const date = new Date(dateString); + + return date.toLocaleDateString(locale, dateOptions); +}; diff --git a/src/newsreader/news/collection/admin.py b/src/newsreader/news/collection/admin.py index 9727b69..e82dea5 100644 --- a/src/newsreader/news/collection/admin.py +++ b/src/newsreader/news/collection/admin.py @@ -4,9 +4,10 @@ from newsreader.news.collection.models import CollectionRule class CollectionRuleAdmin(admin.ModelAdmin): - fields = ("url", "name", "timezone", "category", "favicon") + 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: diff --git a/src/newsreader/news/collection/endpoints.py b/src/newsreader/news/collection/endpoints.py new file mode 100644 index 0000000..02ea917 --- /dev/null +++ b/src/newsreader/news/collection/endpoints.py @@ -0,0 +1,69 @@ +from django.db.models.query import QuerySet + +from rest_framework import status +from rest_framework.generics import ( + GenericAPIView, + ListAPIView, + ListCreateAPIView, + RetrieveUpdateDestroyAPIView, + get_object_or_404, +) +from rest_framework.response import Response +from rest_framework.serializers import Serializer + +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(ListCreateAPIView): + queryset = CollectionRule.objects.all() + serializer_class = RuleSerializer + pagination_class = ResultSetPagination + + def get_queryset(self) -> QuerySet: + 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) -> QuerySet: + 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/serializers.py b/src/newsreader/news/collection/serializers.py index 4f7f3a5..640d16e 100644 --- a/src/newsreader/news/collection/serializers.py +++ b/src/newsreader/news/collection/serializers.py @@ -1,23 +1,15 @@ from rest_framework import serializers -from newsreader.news import core from newsreader.news.collection.models import CollectionRule -class CollectionRuleSerializer(serializers.HyperlinkedModelSerializer): - posts = serializers.SerializerMethodField() +class RuleSerializer(serializers.ModelSerializer): user = serializers.HiddenField(default=serializers.CurrentUserDefault()) + unread = serializers.SerializerMethodField() - def get_posts(self, instance): - request = self.context.get("request") - posts = instance.posts.order_by("-publication_date") - - serializer = core.serializers.PostSerializer( - posts, context={"request": request}, many=True - ) - return serializer.data + def get_unread(self, rule): + return rule.posts.filter(read=False).count() class Meta: model = CollectionRule - fields = ("id", "name", "url", "favicon", "category", "posts", "user") - extra_kwargs = {"category": {"view_name": "api:categories-detail"}} + fields = ("id", "name", "url", "favicon", "category", "user", "unread") diff --git a/src/newsreader/news/collection/tests/endpoints/rules/__init__.py b/src/newsreader/news/collection/tests/endpoints/rule/__init__.py similarity index 100% rename from src/newsreader/news/collection/tests/endpoints/rules/__init__.py rename to src/newsreader/news/collection/tests/endpoints/rule/__init__.py diff --git a/src/newsreader/news/collection/tests/endpoints/rules/detail/__init__.py b/src/newsreader/news/collection/tests/endpoints/rule/detail/__init__.py similarity index 100% rename from src/newsreader/news/collection/tests/endpoints/rules/detail/__init__.py rename to src/newsreader/news/collection/tests/endpoints/rule/detail/__init__.py diff --git a/src/newsreader/news/collection/tests/endpoints/rules/detail/tests.py b/src/newsreader/news/collection/tests/endpoints/rule/detail/tests.py similarity index 59% rename from src/newsreader/news/collection/tests/endpoints/rules/detail/tests.py rename to src/newsreader/news/collection/tests/endpoints/rule/detail/tests.py index 6a85345..8dc75d0 100644 --- a/src/newsreader/news/collection/tests/endpoints/rules/detail/tests.py +++ b/src/newsreader/news/collection/tests/endpoints/rule/detail/tests.py @@ -1,26 +1,22 @@ import json -from urllib.parse import urljoin - from django.test import Client, 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 +from newsreader.news.core.models import Post +from newsreader.news.core.tests.factories import CategoryFactory, PostFactory class CollectionRuleDetailViewTestCase(TestCase): def setUp(self): - self.maxDiff = None - - self.user = UserFactory(is_staff=True, password="test") - self.client = Client() + self.user = UserFactory(password="test") + self.client.login(email=self.user.email, password="test") def test_simple(self): rule = CollectionRuleFactory(user=self.user) - self.client.force_login(self.user) response = self.client.get(reverse("api:rules-detail", args=[rule.pk])) data = response.json() @@ -31,10 +27,8 @@ class CollectionRuleDetailViewTestCase(TestCase): self.assertTrue("url" in data) self.assertTrue("favicon" in data) self.assertTrue("category" in data) - self.assertTrue("posts" in data) def test_not_known(self): - self.client.force_login(self.user) response = self.client.get(reverse("api:rules-detail", args=[100])) data = response.json() @@ -44,7 +38,6 @@ class CollectionRuleDetailViewTestCase(TestCase): def test_post(self): rule = CollectionRuleFactory(user=self.user) - self.client.force_login(self.user) response = self.client.post(reverse("api:rules-detail", args=[rule.pk])) data = response.json() @@ -54,7 +47,6 @@ class CollectionRuleDetailViewTestCase(TestCase): def test_patch(self): rule = CollectionRuleFactory(name="BBC", user=self.user) - self.client.force_login(self.user) response = self.client.patch( reverse("api:rules-detail", args=[rule.pk]), data=json.dumps({"name": "The guardian"}), @@ -65,18 +57,12 @@ class CollectionRuleDetailViewTestCase(TestCase): self.assertEquals(response.status_code, 200) self.assertEquals(data["name"], "The guardian") - def test_category_change_with_absolute_url(self): + def test_category_change(self): old_category = CategoryFactory(user=self.user) new_category = CategoryFactory(user=self.user) - base_url = "http://testserver" - relative_url = reverse("api:categories-detail", args=[new_category.pk]) - - absolute_url = urljoin(base_url, relative_url) - rule = CollectionRuleFactory(name="BBC", category=old_category, user=self.user) - self.client.force_login(self.user) response = self.client.patch( reverse("api:rules-detail", args=[rule.pk]), data=json.dumps({"category": absolute_url}), @@ -85,34 +71,11 @@ class CollectionRuleDetailViewTestCase(TestCase): data = response.json() self.assertEquals(response.status_code, 200) - self.assertEquals(data["category"], absolute_url) - - def test_category_change_with_relative_url(self): - old_category = CategoryFactory(user=self.user) - new_category = CategoryFactory(user=self.user) - - base_url = "http://testserver" - relative_url = reverse("api:categories-detail", args=[new_category.pk]) - - absolute_url = urljoin(base_url, relative_url) - - rule = CollectionRuleFactory(name="BBC", category=old_category, user=self.user) - - self.client.force_login(self.user) - response = self.client.patch( - reverse("api:rules-detail", args=[rule.pk]), - data=json.dumps({"category": relative_url}), - content_type="application/json", - ) - data = response.json() - - self.assertEquals(response.status_code, 200) - self.assertEquals(data["category"], absolute_url) + self.assertEquals(data["category"], new_category.pk) def test_identifier_cannot_be_changed(self): rule = CollectionRuleFactory(user=self.user) - self.client.force_login(self.user) response = self.client.patch( reverse("api:rules-detail", args=[rule.pk]), data=json.dumps({"id": 44}), @@ -127,26 +90,20 @@ class CollectionRuleDetailViewTestCase(TestCase): rule = CollectionRuleFactory(user=self.user) category = CategoryFactory(user=self.user) - self.client.force_login(self.user) response = self.client.patch( reverse("api:rules-detail", args=[rule.pk]), - data=json.dumps( - {"category": reverse("api:categories-detail", args=[category.pk])} - ), + data=json.dumps({"category": category.pk}), content_type="application/json", ) data = response.json() - url = data["category"] + data["category"] self.assertEquals(response.status_code, 200) - self.assertTrue( - url.endswith(reverse("api:categories-detail", args=[category.pk])) - ) + self.assertEquals(data["category"], category.pk) def test_put(self): rule = CollectionRuleFactory(name="BBC", user=self.user) - self.client.force_login(self.user) response = self.client.put( reverse("api:rules-detail", args=[rule.pk]), data=json.dumps({"name": "BBC", "url": "https://www.bbc.co.uk"}), @@ -160,12 +117,13 @@ class CollectionRuleDetailViewTestCase(TestCase): def test_delete(self): rule = CollectionRuleFactory(user=self.user) - self.client.force_login(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( @@ -180,7 +138,6 @@ class CollectionRuleDetailViewTestCase(TestCase): other_user = UserFactory() rule = CollectionRuleFactory(name="BBC", user=other_user) - self.client.force_login(self.user) response = self.client.patch( reverse("api:rules-detail", args=[rule.pk]), data=json.dumps({"name": "The guardian"}), @@ -188,3 +145,95 @@ class CollectionRuleDetailViewTestCase(TestCase): ) 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.login(email=self.user.email, password="test") + + 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/rules/list/__init__.py b/src/newsreader/news/collection/tests/endpoints/rule/list/__init__.py similarity index 100% rename from src/newsreader/news/collection/tests/endpoints/rules/list/__init__.py rename to src/newsreader/news/collection/tests/endpoints/rule/list/__init__.py 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..4526bdd --- /dev/null +++ b/src/newsreader/news/collection/tests/endpoints/rule/list/tests.py @@ -0,0 +1,371 @@ +import json + +from datetime import date, datetime, time + +from django.test import Client, 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.login(email=self.user.email, password="test") + + 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() + data["category"] + + self.assertEquals(response.status_code, 201) + + self.assertEquals(data["name"], "BBC") + self.assertEquals(data["url"], "https://www.bbc.co.uk") + self.assertEquals(data["category"], category.pk) + + 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.login(email=self.user.email, password="test") + + 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/endpoints/rules/list/tests.py b/src/newsreader/news/collection/tests/endpoints/rules/list/tests.py deleted file mode 100644 index 04c7b73..0000000 --- a/src/newsreader/news/collection/tests/endpoints/rules/list/tests.py +++ /dev/null @@ -1,210 +0,0 @@ -import json - -from datetime import date, datetime, time - -from django.test import Client, 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 CollectionRuleListViewTestCase(TestCase): - def setUp(self): - self.maxDiff = None - - self.user = UserFactory(is_staff=True, password="test") - self.client = Client() - - def test_simple(self): - CollectionRuleFactory.create_batch(size=3, user=self.user) - - self.client.force_login(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, - ), - ] - - self.client.force_login(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) - - self.client.force_login(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): - self.client.force_login(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"], 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": reverse("api:categories-detail", args=[category.pk]), - } - - self.client.force_login(self.user) - response = self.client.post( - reverse("api:rules-list"), - data=json.dumps(data), - content_type="application/json", - ) - data = response.json() - category_url = data["category"] - - self.assertEquals(response.status_code, 201) - - self.assertEquals(data["name"], "BBC") - self.assertEquals(data["url"], "https://www.bbc.co.uk") - - self.assertTrue( - category_url.endswith(reverse("api:categories-detail", args=[category.pk])) - ) - - def test_patch(self): - self.client.force_login(self.user) - 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): - self.client.force_login(self.user) - 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): - self.client.force_login(self.user) - 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_posts(self): - rules = { - rule: PostFactory.create_batch(size=5, rule=rule) - for rule in CollectionRuleFactory.create_batch(size=5, user=self.user) - } - - self.client.force_login(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"], 5) - - self.assertEquals(len(data["results"]), 5) - - self.assertEquals(len(data["results"][0]["posts"]), 5) - - def test_rules_with_posts_ordered(self): - rules = { - rule: PostFactory.create_batch(size=5, rule=rule) - for rule in CollectionRuleFactory.create_batch(size=2, user=self.user) - } - - self.client.force_login(self.user) - response = self.client.get(reverse("api:rules-list")) - data = response.json() - - first_post_set = data["results"][0]["posts"] - second_post_set = data["results"][1]["posts"] - - self.assertEquals(response.status_code, 200) - self.assertTrue("results" in data) - self.assertTrue("count" in data) - self.assertEquals(data["count"], 2) - - self.assertEquals(len(data["results"]), 2) - - for result_set in [first_post_set, second_post_set]: - for count, post in enumerate(result_set): - if count < 1: - continue - - self.assertTrue( - post["publication_date"] < result_set[count - 1]["publication_date"] - ) - - def test_rule_with_unauthenticated_user(self): - CollectionRuleFactory.create_batch(size=3, user=self.user) - - response = self.client.get(reverse("api:rules-list")) - response.json() - - self.assertEquals(response.status_code, 403) - - def test_rule_with_unauthorized_user(self): - other_user = UserFactory() - CollectionRuleFactory.create_batch(size=3, user=other_user) - - self.client.force_login(self.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) diff --git a/src/newsreader/news/collection/urls.py b/src/newsreader/news/collection/urls.py index 4b59a09..606ec3a 100644 --- a/src/newsreader/news/collection/urls.py +++ b/src/newsreader/news/collection/urls.py @@ -1,12 +1,16 @@ from django.urls import path -from newsreader.news.collection.views import ( - CollectionRuleAPIListView, - CollectionRuleDetailView, +from newsreader.news.collection.endpoints import ( + DetailRuleView, + ListRuleView, + NestedRuleView, + RuleReadView, ) endpoints = [ - path("rules/", CollectionRuleDetailView.as_view(), name="rules-detail"), - path("rules/", CollectionRuleAPIListView.as_view(), name="rules-list"), + path("rules/", DetailRuleView.as_view(), name="rules-detail"), + path("rules//posts/", NestedRuleView.as_view(), name="rules-nested-posts"), + path("rules//read/", RuleReadView.as_view(), name="rules-read"), + path("rules/", ListRuleView.as_view(), name="rules-list"), ] diff --git a/src/newsreader/news/collection/views.py b/src/newsreader/news/collection/views.py index 2c08185..e69de29 100644 --- a/src/newsreader/news/collection/views.py +++ b/src/newsreader/news/collection/views.py @@ -1,21 +0,0 @@ -from rest_framework.generics import ListCreateAPIView, RetrieveUpdateDestroyAPIView - -from newsreader.core.pagination import ResultSetPagination -from newsreader.news.collection.models import CollectionRule -from newsreader.news.collection.serializers import CollectionRuleSerializer - - -class CollectionRuleAPIListView(ListCreateAPIView): - queryset = CollectionRule.objects.all() - serializer_class = CollectionRuleSerializer - pagination_class = ResultSetPagination - - def get_queryset(self): - user = self.request.user - return self.queryset.filter(user=user).order_by("-created") - - -class CollectionRuleDetailView(RetrieveUpdateDestroyAPIView): - queryset = CollectionRule.objects.all() - serializer_class = CollectionRuleSerializer - pagination_class = ResultSetPagination diff --git a/src/newsreader/news/core/endpoints.py b/src/newsreader/news/core/endpoints.py new file mode 100644 index 0000000..3f0207e --- /dev/null +++ b/src/newsreader/news/core/endpoints.py @@ -0,0 +1,118 @@ +from django.db.models import Q +from django.db.models.query import QuerySet + +from rest_framework import status +from rest_framework.generics import ( + GenericAPIView, + ListAPIView, + ListCreateAPIView, + RetrieveUpdateAPIView, + RetrieveUpdateDestroyAPIView, + get_object_or_404, +) +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response + +from newsreader.accounts.permissions import IsPostOwner +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 Category, Post +from newsreader.news.core.serializers import CategorySerializer, PostSerializer + + +class ListPostView(ListAPIView): + queryset = Post.objects.all() + serializer_class = PostSerializer + pagination_class = LargeResultSetPagination + filter_backends = [ReadFilter] + + def get_queryset(self): + user = self.request.user + queryset = ( + self.queryset.filter(rule__user=user) + .filter(Q(rule__category=None) | Q(rule__category__user=user)) + .order_by("rule", "-publication_date", "-created") + ) + + return queryset + + +class DetailPostView(RetrieveUpdateAPIView): + queryset = Post.objects.all() + serializer_class = PostSerializer + permission_classes = (IsAuthenticated, IsPostOwner) + + +class ListCategoryView(ListCreateAPIView): + queryset = Category.objects.all() + serializer_class = CategorySerializer + + def get_queryset(self): + user = self.request.user + return self.queryset.filter(user=user).order_by("-created", "-modified") + + +class DetailCategoryView(RetrieveUpdateDestroyAPIView): + queryset = Category.objects.all() + serializer_class = CategorySerializer + + +class NestedRuleCategoryView(ListAPIView): + queryset = Category.objects.prefetch_related("rules").all() + serializer_class = RuleSerializer + + def get_queryset(self) -> QuerySet: + 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]} + + category = get_object_or_404(self.queryset, **filter_kwargs) + self.check_object_permissions(self.request, category) + + return category.rules.order_by("name") + + +class NestedPostCategoryView(ListAPIView): + queryset = Category.objects.prefetch_related("rules", "rules__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]} + + category = get_object_or_404(self.queryset, **filter_kwargs) + self.check_object_permissions(self.request, category) + + queryset = Post.objects.filter( + rule__in=category.rules.values_list("id", flat=True) + ).order_by("rule__name", "-publication_date") + + return queryset + + +class CategoryReadView(GenericAPIView): + queryset = Category.objects.all() + serializer_class = CategorySerializer + + def post(self, request, *args, **kwargs): + category = self.get_object() + + ( + Post.objects.filter( + rule__in=category.rules.values_list("pk", flat=True) + ).update(read=True) + ) + + category.refresh_from_db() + serializer_class = self.get_serializer_class() + serializer = serializer_class(category) + return Response(serializer.data, status=status.HTTP_201_CREATED) diff --git a/src/newsreader/news/core/filters.py b/src/newsreader/news/core/filters.py new file mode 100644 index 0000000..d322d83 --- /dev/null +++ b/src/newsreader/news/core/filters.py @@ -0,0 +1,32 @@ +from django.utils.encoding import force_text +from django.utils.translation import ugettext_lazy as _ + +from rest_framework import filters +from rest_framework.compat import coreapi, coreschema + + +class ReadFilter(filters.BaseFilterBackend): + query_param = "read" + + def filter_queryset(self, request, queryset, view): + key = request.query_params.get(self.query_param, None) + available_values = {"True": True, "true": True, "False": False, "false": False} + + if not key or key not in available_values.keys(): + return queryset + + value = available_values[key] + return queryset.filter(read=value) + + def get_schema_fields(self, view): + return [ + coreapi.Field( + name=self.query_param, + required=False, + location="query", + schema=coreschema.String( + title=force_text(self.query_param), + description=force_text(_("Wether posts should be read or not")), + ), + ) + ] diff --git a/src/newsreader/news/core/migrations/0003_post_read.py b/src/newsreader/news/core/migrations/0003_post_read.py new file mode 100644 index 0000000..8306051 --- /dev/null +++ b/src/newsreader/news/core/migrations/0003_post_read.py @@ -0,0 +1,14 @@ +# Generated by Django 2.2 on 2019-09-09 19:06 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [("core", "0002_auto_20190714_1425")] + + operations = [ + migrations.AddField( + model_name="post", name="read", field=models.BooleanField(default=False) + ) + ] diff --git a/src/newsreader/news/core/models.py b/src/newsreader/news/core/models.py index ce5fa16..498d9fd 100644 --- a/src/newsreader/news/core/models.py +++ b/src/newsreader/news/core/models.py @@ -12,6 +12,8 @@ class Post(TimeStampedModel): publication_date = models.DateTimeField(blank=True, null=True) url = models.URLField(max_length=1024, blank=True, null=True) + read = models.BooleanField(default=False) + rule = models.ForeignKey( CollectionRule, on_delete=models.CASCADE, editable=False, related_name="posts" ) @@ -24,7 +26,7 @@ class Post(TimeStampedModel): class Category(TimeStampedModel): - name = models.CharField(max_length=50, unique=True) + name = models.CharField(max_length=50, unique=True) # TODO remove unique value user = models.ForeignKey("accounts.User", _("Owner"), related_name="categories") class Meta: diff --git a/src/newsreader/news/core/serializers.py b/src/newsreader/news/core/serializers.py index cb6eb12..791d873 100644 --- a/src/newsreader/news/core/serializers.py +++ b/src/newsreader/news/core/serializers.py @@ -4,7 +4,7 @@ from newsreader.news import collection from newsreader.news.core.models import Category, Post -class PostSerializer(serializers.HyperlinkedModelSerializer): +class PostSerializer(serializers.ModelSerializer): class Meta: model = Post fields = ( @@ -16,24 +16,19 @@ class PostSerializer(serializers.HyperlinkedModelSerializer): "publication_date", "url", "rule", + "read", ) - extra_kwargs = {"rule": {"view_name": "api:rules-detail"}} -class CategorySerializer(serializers.HyperlinkedModelSerializer): - rules = serializers.SerializerMethodField() +class CategorySerializer(serializers.ModelSerializer): user = serializers.HiddenField(default=serializers.CurrentUserDefault()) + unread = serializers.SerializerMethodField() - def get_rules(self, instance): - request = self.context.get("request") - rules = instance.rules.order_by("-modified", "-created") - - serializer = collection.serializers.CollectionRuleSerializer( - rules, context={"request": request}, many=True - ) - return serializer.data + def get_unread(self, category): + return Post.objects.filter( + rule__in=category.rules.values_list("pk", flat=True), read=False + ).count() class Meta: model = Category - fields = ("id", "name", "rules", "user") - extra_kwargs = {"rules": {"view_name": "api:rules-detail"}} + fields = ("id", "name", "user", "unread") diff --git a/src/newsreader/news/core/templates/core/main.html b/src/newsreader/news/core/templates/core/main.html new file mode 100644 index 0000000..9de45f4 --- /dev/null +++ b/src/newsreader/news/core/templates/core/main.html @@ -0,0 +1,15 @@ +{% extends "base.html" %} + +{% load static %} + +{% block head %} + +{% endblock %} + +{% block content %} +
    +{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/src/newsreader/news/core/tests/endpoints/category/detail/tests.py b/src/newsreader/news/core/tests/endpoints/category/detail/tests.py index 251023d..787d8a9 100644 --- a/src/newsreader/news/core/tests/endpoints/category/detail/tests.py +++ b/src/newsreader/news/core/tests/endpoints/category/detail/tests.py @@ -10,25 +10,20 @@ from newsreader.news.core.tests.factories import CategoryFactory, PostFactory class CategoryDetailViewTestCase(TestCase): def setUp(self): - self.maxDiff = None - - self.client = Client() - self.user = UserFactory(is_staff=True, password="test") + self.user = UserFactory(password="test") + self.client.login(email=self.user.email, password="test") def test_simple(self): category = CategoryFactory(user=self.user) - self.client.force_login(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) - self.assertTrue("rules" in data) def test_not_known(self): - self.client.force_login(self.user) response = self.client.get(reverse("api:categories-detail", args=[100])) data = response.json() @@ -38,7 +33,6 @@ class CategoryDetailViewTestCase(TestCase): def test_post(self): category = CategoryFactory(user=self.user) - self.client.force_login(self.user) response = self.client.post(reverse("api:categories-detail", args=[category.pk])) data = response.json() @@ -48,7 +42,6 @@ class CategoryDetailViewTestCase(TestCase): def test_patch(self): category = CategoryFactory(name="Clickbait", user=self.user) - self.client.force_login(self.user) response = self.client.patch( reverse("api:categories-detail", args=[category.pk]), data=json.dumps({"name": "Interesting posts"}), @@ -62,7 +55,6 @@ class CategoryDetailViewTestCase(TestCase): def test_identifier_cannot_be_changed(self): category = CategoryFactory(user=self.user) - self.client.force_login(self.user) response = self.client.patch( reverse("api:categories-detail", args=[category.pk]), data=json.dumps({"id": 44}), @@ -76,7 +68,6 @@ class CategoryDetailViewTestCase(TestCase): def test_put(self): category = CategoryFactory(name="Clickbait", user=self.user) - self.client.force_login(self.user) response = self.client.put( reverse("api:categories-detail", args=[category.pk]), data=json.dumps({"name": "Interesting posts"}), @@ -90,74 +81,15 @@ class CategoryDetailViewTestCase(TestCase): def test_delete(self): category = CategoryFactory(user=self.user) - self.client.force_login(self.user) response = self.client.delete( reverse("api:categories-detail", args=[category.pk]) ) self.assertEquals(response.status_code, 204) - def test_rules(self): - category = CategoryFactory(user=self.user) - rules = CollectionRuleFactory.create_batch( - size=5, category=category, user=self.user - ) - - self.client.force_login(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["rules"][0]) - self.assertTrue("name" in data["rules"][0]) - self.assertTrue("url" in data["rules"][0]) - - def test_rules_with_posts(self): - category = CategoryFactory(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 - ) - } - - self.client.force_login(self.user) - response = self.client.get(reverse("api:categories-detail", args=[category.pk])) - data = response.json() - - self.assertEquals(response.status_code, 200) - - self.assertEquals(len(data["rules"][0]["posts"]), 5) - - def test_rules_with_posts_ordered(self): - category = CategoryFactory(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 - ) - } - - self.client.force_login(self.user) - response = self.client.get(reverse("api:categories-detail", args=[category.pk])) - data = response.json() - - self.assertEquals(response.status_code, 200) - - posts = data["rules"][0]["posts"] - - for count, post in enumerate(posts): - if count < 1: - continue - - self.assertTrue( - post["publication_date"] < posts[count - 1]["publication_date"] - ) - 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])) @@ -168,7 +100,112 @@ class CategoryDetailViewTestCase(TestCase): other_user = UserFactory() category = CategoryFactory(user=other_user) - self.client.force_login(self.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.login(email=self.user.email, password="test") + + 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/tests.py b/src/newsreader/news/core/tests/endpoints/category/list/tests.py index 974645d..f97884b 100644 --- a/src/newsreader/news/core/tests/endpoints/category/list/tests.py +++ b/src/newsreader/news/core/tests/endpoints/category/list/tests.py @@ -14,22 +14,17 @@ from newsreader.news.core.tests.factories import CategoryFactory, PostFactory class CategoryListViewTestCase(TestCase): def setUp(self): - self.maxDiff = None - - self.client = Client() - self.user = UserFactory(is_staff=True, password="test") + self.user = UserFactory(password="test") + self.client.login(email=self.user.email, password="test") def test_simple(self): CategoryFactory.create_batch(size=3, user=self.user) - self.client.force_login(self.user) response = self.client.get(reverse("api:categories-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(len(data), 3) def test_ordering(self): categories = [ @@ -53,35 +48,25 @@ class CategoryListViewTestCase(TestCase): ), ] - self.client.force_login(self.user) response = self.client.get(reverse("api:categories-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"], categories[1].pk) - self.assertEquals(data["results"][1]["id"], categories[2].pk) - self.assertEquals(data["results"][2]["id"], categories[0].pk) + 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): - self.client.force_login(self.user) response = self.client.get(reverse("api:categories-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) + self.assertEquals(len(data), 0) def test_post(self): data = {"name": "Tech"} - self.client.force_login(self.user) response = self.client.post( reverse("api:categories-list"), data=json.dumps(data), @@ -93,7 +78,6 @@ class CategoryListViewTestCase(TestCase): self.assertEquals(response_data["name"], "Tech") def test_patch(self): - self.client.force_login(self.user) response = self.client.patch(reverse("api:categories-list")) data = response.json() @@ -101,7 +85,6 @@ class CategoryListViewTestCase(TestCase): self.assertEquals(data["detail"], 'Method "PATCH" not allowed.') def test_put(self): - self.client.force_login(self.user) response = self.client.put(reverse("api:categories-list")) data = response.json() @@ -109,66 +92,15 @@ class CategoryListViewTestCase(TestCase): self.assertEquals(data["detail"], 'Method "PUT" not allowed.') def test_delete(self): - self.client.force_login(self.user) 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_rules(self): - categories = { - category.pk: CollectionRuleFactory.create_batch( - size=5, category=category, user=self.user - ) - for category in CategoryFactory.create_batch(size=5, user=self.user) - } - - self.client.force_login(self.user) - response = self.client.get(reverse("api:categories-list")) - data = response.json() - - self.assertEquals(response.status_code, 200) - self.assertTrue("results" in data) - self.assertTrue("count" in data) - self.assertEquals(data["count"], 5) - - self.assertEquals(len(data["results"]), 5) - - self.assertEquals(len(data["results"][0]["rules"]), 5) - - self.assertTrue("id" in data["results"][0]["rules"][0]) - self.assertTrue("name" in data["results"][0]["rules"][0]) - self.assertTrue("url" in data["results"][0]["rules"][0]) - self.assertTrue("posts" in data["results"][0]["rules"][0]) - - def test_rules_with_posts(self): - categories = { - category.pk: CollectionRuleFactory.create_batch( - size=5, category=category, user=self.user - ) - for category in CategoryFactory.create_batch(size=5, user=self.user) - } - - for category in categories: - for rule in categories[category]: - PostFactory.create_batch(size=5, rule=rule) - - self.client.force_login(self.user) - response = self.client.get(reverse("api:categories-list")) - data = response.json() - - self.assertEquals(response.status_code, 200) - self.assertTrue("results" in data) - self.assertTrue("count" in data) - self.assertEquals(data["count"], 5) - - self.assertEquals(len(data["results"]), 5) - - self.assertEquals(len(data["results"][0]["rules"]), 5) - self.assertEquals(len(data["results"][0]["rules"][0]["posts"]), 5) - 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")) @@ -179,10 +111,458 @@ class CategoryListViewTestCase(TestCase): other_user = UserFactory() CategoryFactory.create_batch(size=3, user=other_user) - self.client.force_login(self.user) response = self.client.get(reverse("api:categories-list")) data = response.json() self.assertEquals(response.status_code, 200) - self.assertEquals(len(data["results"]), 0) + self.assertEquals(len(data), 0) + + +class NestedCategoryListViewTestCase(TestCase): + def setUp(self): + self.user = UserFactory(password="test") + self.client.login(email=self.user.email, password="test") + + 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.login(email=self.user.email, password="test") + + 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 Reuters post") + self.assertEquals(posts[3]["title"], "First Reuters post") + + self.assertEquals(posts[4]["title"], "Second Guardian post") + self.assertEquals(posts[5]["title"], "First Guardian 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/detail/tests.py b/src/newsreader/news/core/tests/endpoints/post/detail/tests.py index 465e2f2..acc4bd1 100644 --- a/src/newsreader/news/core/tests/endpoints/post/detail/tests.py +++ b/src/newsreader/news/core/tests/endpoints/post/detail/tests.py @@ -10,11 +10,8 @@ from newsreader.news.core.tests.factories import CategoryFactory, PostFactory class PostDetailViewTestCase(TestCase): def setUp(self): - self.maxDiff = None - self.client = Client() - - self.client = Client() - self.user = UserFactory(is_staff=True, password="test") + self.user = UserFactory(password="test") + self.client.login(email=self.user.email, password="test") def test_simple(self): rule = CollectionRuleFactory( @@ -22,7 +19,6 @@ class PostDetailViewTestCase(TestCase): ) post = PostFactory(rule=rule) - self.client.force_login(self.user) response = self.client.get(reverse("api:posts-detail", args=[post.pk])) data = response.json() @@ -38,7 +34,6 @@ class PostDetailViewTestCase(TestCase): self.assertTrue("remote_identifier" in data) def test_not_known(self): - self.client.force_login(self.user) response = self.client.get(reverse("api:posts-detail", args=[100])) data = response.json() @@ -51,7 +46,6 @@ class PostDetailViewTestCase(TestCase): ) post = PostFactory(rule=rule) - self.client.force_login(self.user) response = self.client.post(reverse("api:posts-detail", args=[post.pk])) data = response.json() @@ -64,7 +58,6 @@ class PostDetailViewTestCase(TestCase): ) post = PostFactory(title="This is clickbait for sure", rule=rule) - self.client.force_login(self.user) response = self.client.patch( reverse("api:posts-detail", args=[post.pk]), data=json.dumps({"title": "This title is very accurate"}), @@ -81,7 +74,6 @@ class PostDetailViewTestCase(TestCase): ) post = PostFactory(title="This is clickbait for sure", rule=rule) - self.client.force_login(self.user) response = self.client.patch( reverse("api:posts-detail", args=[post.pk]), data=json.dumps({"id": 44}), @@ -101,18 +93,16 @@ class PostDetailViewTestCase(TestCase): ) post = PostFactory(title="This is clickbait for sure", rule=rule) - self.client.force_login(self.user) 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() - rule_url = data["rule"] self.assertEquals(response.status_code, 200) - self.assertTrue(rule_url.endswith(reverse("api:rules-detail", args=[rule.pk]))) + self.assertTrue(data["rule"], rule.pk) def test_put(self): rule = CollectionRuleFactory( @@ -120,7 +110,6 @@ class PostDetailViewTestCase(TestCase): ) post = PostFactory(title="This is clickbait for sure", rule=rule) - self.client.force_login(self.user) response = self.client.put( reverse("api:posts-detail", args=[post.pk]), data=json.dumps({"title": "This title is very accurate"}), @@ -137,7 +126,6 @@ class PostDetailViewTestCase(TestCase): ) post = PostFactory(rule=rule) - self.client.force_login(self.user) response = self.client.delete(reverse("api:posts-detail", args=[post.pk])) data = response.json() @@ -145,6 +133,8 @@ class PostDetailViewTestCase(TestCase): 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) @@ -153,6 +143,8 @@ class PostDetailViewTestCase(TestCase): 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) ) @@ -167,7 +159,6 @@ class PostDetailViewTestCase(TestCase): rule = CollectionRuleFactory(user=other_user, category=None) post = PostFactory(rule=rule) - self.client.force_login(self.user) response = self.client.get(reverse("api:posts-detail", args=[post.pk])) self.assertEquals(response.status_code, 403) @@ -179,7 +170,6 @@ class PostDetailViewTestCase(TestCase): ) post = PostFactory(rule=rule) - self.client.force_login(self.user) response = self.client.get(reverse("api:posts-detail", args=[post.pk])) self.assertEquals(response.status_code, 403) @@ -191,7 +181,38 @@ class PostDetailViewTestCase(TestCase): ) post = PostFactory(rule=rule) - self.client.force_login(self.user) 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/tests.py b/src/newsreader/news/core/tests/endpoints/post/list/tests.py index 724d8b2..013decd 100644 --- a/src/newsreader/news/core/tests/endpoints/post/list/tests.py +++ b/src/newsreader/news/core/tests/endpoints/post/list/tests.py @@ -12,10 +12,8 @@ from newsreader.news.core.tests.factories import CategoryFactory, PostFactory class PostListViewTestCase(TestCase): def setUp(self): - self.maxDiff = None - - self.client = Client() self.user = UserFactory(is_staff=True, password="test") + self.client.login(email=self.user.email, password="test") def test_simple(self): rule = CollectionRuleFactory( @@ -23,7 +21,6 @@ class PostListViewTestCase(TestCase): ) PostFactory.create_batch(size=3, rule=rule) - self.client.force_login(self.user) response = self.client.get(reverse("api:posts-list")) data = response.json() @@ -61,7 +58,6 @@ class PostListViewTestCase(TestCase): ), ] - self.client.force_login(self.user) response = self.client.get(reverse("api:posts-list")) data = response.json() @@ -81,7 +77,6 @@ class PostListViewTestCase(TestCase): PostFactory.create_batch(size=80, rule=rule) page_size = 50 - self.client.force_login(self.user) response = self.client.get(reverse("api:posts-list"), {"count": 50}) data = response.json() @@ -90,7 +85,6 @@ class PostListViewTestCase(TestCase): self.assertEquals(len(data["results"]), page_size) def test_empty(self): - self.client.force_login(self.user) response = self.client.get(reverse("api:posts-list")) data = response.json() @@ -102,7 +96,6 @@ class PostListViewTestCase(TestCase): self.assertEquals(len(data["results"]), 0) def test_post(self): - self.client.force_login(self.user) response = self.client.post(reverse("api:posts-list")) data = response.json() @@ -110,7 +103,6 @@ class PostListViewTestCase(TestCase): self.assertEquals(data["detail"], 'Method "POST" not allowed.') def test_patch(self): - self.client.force_login(self.user) response = self.client.patch(reverse("api:posts-list")) data = response.json() @@ -118,7 +110,6 @@ class PostListViewTestCase(TestCase): self.assertEquals(data["detail"], 'Method "PATCH" not allowed.') def test_put(self): - self.client.force_login(self.user) response = self.client.put(reverse("api:posts-list")) data = response.json() @@ -126,7 +117,6 @@ class PostListViewTestCase(TestCase): self.assertEquals(data["detail"], 'Method "PUT" not allowed.') def test_delete(self): - self.client.force_login(self.user) response = self.client.delete(reverse("api:posts-list")) data = response.json() @@ -134,6 +124,8 @@ class PostListViewTestCase(TestCase): 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")) @@ -141,6 +133,8 @@ class PostListViewTestCase(TestCase): 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( @@ -157,7 +151,6 @@ class PostListViewTestCase(TestCase): rule = CollectionRuleFactory(user=other_user, category=None) PostFactory.create_batch(size=3, rule=rule) - self.client.force_login(self.user) response = self.client.get(reverse("api:posts-list")) data = response.json() @@ -173,7 +166,6 @@ class PostListViewTestCase(TestCase): size=3, rule=CollectionRuleFactory(user=other_user, category=category) ) - self.client.force_login(self.user) response = self.client.get(reverse("api:posts-list")) data = response.json() @@ -191,7 +183,6 @@ class PostListViewTestCase(TestCase): ) PostFactory.create_batch(size=3, rule=rule) - self.client.force_login(self.user) response = self.client.get(reverse("api:posts-list")) data = response.json() @@ -201,12 +192,9 @@ class PostListViewTestCase(TestCase): self.assertEquals(data["count"], 0) def test_posts_with_authorized_user_without_category(self): - UserFactory() - rule = CollectionRuleFactory(user=self.user, category=None) PostFactory.create_batch(size=3, rule=rule) - self.client.force_login(self.user) response = self.client.get(reverse("api:posts-list")) data = response.json() @@ -214,3 +202,41 @@ class PostListViewTestCase(TestCase): 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 index 3ccf52d..46eeeae 100644 --- a/src/newsreader/news/core/tests/factories.py +++ b/src/newsreader/news/core/tests/factories.py @@ -25,5 +25,7 @@ class PostFactory(factory.django.DjangoModelFactory): "newsreader.news.collection.tests.factories.CollectionRuleFactory" ) + read = False + class Meta: model = Post diff --git a/src/newsreader/news/core/urls.py b/src/newsreader/news/core/urls.py index c5ccaa9..3255cee 100644 --- a/src/newsreader/news/core/urls.py +++ b/src/newsreader/news/core/urls.py @@ -1,18 +1,34 @@ +from django.contrib.auth.decorators import login_required from django.urls import path -from newsreader.news.core.views import ( - DetailCategoryAPIView, - DetailPostAPIView, - ListCategoryAPIView, - ListPostAPIView, +from newsreader.news.core.endpoints import ( + CategoryReadView, + DetailCategoryView, + DetailPostView, + ListCategoryView, + ListPostView, + NestedPostCategoryView, + NestedRuleCategoryView, ) +from newsreader.news.core.views import MainView +index_page = login_required(MainView.as_view()) + endpoints = [ - path("posts/", ListPostAPIView.as_view(), name="posts-list"), - path("posts//", DetailPostAPIView.as_view(), name="posts-detail"), - path("categories/", ListCategoryAPIView.as_view(), name="categories-list"), + 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//", DetailCategoryAPIView.as_view(), name="categories-detail" + "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 index 415816c..7832673 100644 --- a/src/newsreader/news/core/views.py +++ b/src/newsreader/news/core/views.py @@ -1,49 +1,25 @@ -from django.db.models import Q +from typing import Dict -from rest_framework.generics import ( - ListAPIView, - ListCreateAPIView, - RetrieveUpdateAPIView, - RetrieveUpdateDestroyAPIView, -) -from rest_framework.permissions import IsAuthenticated - -from newsreader.accounts.permissions import IsPostOwner -from newsreader.core.pagination import LargeResultSetPagination, ResultSetPagination -from newsreader.news.core.models import Category, Post -from newsreader.news.core.serializers import CategorySerializer, PostSerializer +from django.views.generic.base import TemplateView -class ListPostAPIView(ListAPIView): - queryset = Post.objects.all() - serializer_class = PostSerializer - pagination_class = LargeResultSetPagination - permission_classes = (IsAuthenticated, IsPostOwner) +class MainView(TemplateView): + template_name = "core/main.html" - def get_queryset(self): + # TODO serialize objects to show filled main page + def get_context_data(self, **kwargs) -> Dict: + context = super().get_context_data(**kwargs) user = self.request.user - initial_queryset = self.queryset.filter(rule__user=user) - return initial_queryset.filter( - Q(rule__category=None) | Q(rule__category__user=user) - ).order_by("rule", "-publication_date", "-created") + categories = { + category: category.rules.order_by("-created") + for category in user.categories.order_by("name") + } -class DetailPostAPIView(RetrieveUpdateAPIView): - queryset = Post.objects.all() - serializer_class = PostSerializer - permission_classes = (IsAuthenticated, IsPostOwner) + rules = { + rule: rule.posts.order_by("-publication_date")[:30] + for rule in user.rules.order_by("-created") + } - -class ListCategoryAPIView(ListCreateAPIView): - queryset = Category.objects.all() - serializer_class = CategorySerializer - pagination_class = ResultSetPagination - - def get_queryset(self): - user = self.request.user - return self.queryset.filter(user=user).order_by("-created", "-modified") - - -class DetailCategoryAPIView(RetrieveUpdateDestroyAPIView): - queryset = Category.objects.all() - serializer_class = CategorySerializer + context.update(categories=categories, rules=rules) + return context diff --git a/src/newsreader/static/src/scss/accounts/components/form/_form.scss b/src/newsreader/scss/accounts/components/form/_form.scss similarity index 94% rename from src/newsreader/static/src/scss/accounts/components/form/_form.scss rename to src/newsreader/scss/accounts/components/form/_form.scss index aa2fb6c..8e6cf7a 100644 --- a/src/newsreader/static/src/scss/accounts/components/form/_form.scss +++ b/src/newsreader/scss/accounts/components/form/_form.scss @@ -8,6 +8,8 @@ &__fieldset { @extend .form__fieldset; + + padding: 10px; } &__fieldset * { diff --git a/src/newsreader/static/src/scss/accounts/components/form/index.scss b/src/newsreader/scss/accounts/components/form/index.scss similarity index 100% rename from src/newsreader/static/src/scss/accounts/components/form/index.scss rename to src/newsreader/scss/accounts/components/form/index.scss diff --git a/src/newsreader/static/src/scss/accounts/components/index.scss b/src/newsreader/scss/accounts/components/index.scss similarity index 100% rename from src/newsreader/static/src/scss/accounts/components/index.scss rename to src/newsreader/scss/accounts/components/index.scss diff --git a/src/newsreader/static/src/scss/accounts/components/main/_main.scss b/src/newsreader/scss/accounts/components/main/_main.scss similarity index 100% rename from src/newsreader/static/src/scss/accounts/components/main/_main.scss rename to src/newsreader/scss/accounts/components/main/_main.scss diff --git a/src/newsreader/static/src/scss/accounts/components/main/index.scss b/src/newsreader/scss/accounts/components/main/index.scss similarity index 100% rename from src/newsreader/static/src/scss/accounts/components/main/index.scss rename to src/newsreader/scss/accounts/components/main/index.scss diff --git a/src/newsreader/static/src/scss/accounts/index.scss b/src/newsreader/scss/accounts/index.scss similarity index 72% rename from src/newsreader/static/src/scss/accounts/index.scss rename to src/newsreader/scss/accounts/index.scss index d0a748c..d155753 100644 --- a/src/newsreader/static/src/scss/accounts/index.scss +++ b/src/newsreader/scss/accounts/index.scss @@ -1,4 +1,6 @@ +// General imports @import "../partials/variables"; @import "../components/index"; +// Page specific @import "./components/index"; diff --git a/src/newsreader/static/src/scss/components/body/_body.scss b/src/newsreader/scss/components/body/_body.scss similarity index 61% rename from src/newsreader/static/src/scss/components/body/_body.scss rename to src/newsreader/scss/components/body/_body.scss index f0829bf..0e2dad3 100644 --- a/src/newsreader/static/src/scss/components/body/_body.scss +++ b/src/newsreader/scss/components/body/_body.scss @@ -2,4 +2,9 @@ margin: 0; padding: 0; background-color: $gainsboro; + + & * { + margin: 0; + padding: 0; + } } diff --git a/src/newsreader/static/src/scss/components/body/index.scss b/src/newsreader/scss/components/body/index.scss similarity index 100% rename from src/newsreader/static/src/scss/components/body/index.scss rename to src/newsreader/scss/components/body/index.scss diff --git a/src/newsreader/static/src/scss/components/button/_button.scss b/src/newsreader/scss/components/button/_button.scss similarity index 95% rename from src/newsreader/static/src/scss/components/button/_button.scss rename to src/newsreader/scss/components/button/_button.scss index 4047b6c..61ddc81 100644 --- a/src/newsreader/static/src/scss/components/button/_button.scss +++ b/src/newsreader/scss/components/button/_button.scss @@ -6,8 +6,6 @@ padding: 10px 50px; - width: 50px; - border: none; border-radius: 2px; diff --git a/src/newsreader/static/src/scss/components/button/index.scss b/src/newsreader/scss/components/button/index.scss similarity index 100% rename from src/newsreader/static/src/scss/components/button/index.scss rename to src/newsreader/scss/components/button/index.scss diff --git a/src/newsreader/static/src/scss/components/error/_error.scss b/src/newsreader/scss/components/error/_error.scss similarity index 100% rename from src/newsreader/static/src/scss/components/error/_error.scss rename to src/newsreader/scss/components/error/_error.scss diff --git a/src/newsreader/static/src/scss/components/error/_errorlist.scss b/src/newsreader/scss/components/error/_errorlist.scss similarity index 100% rename from src/newsreader/static/src/scss/components/error/_errorlist.scss rename to src/newsreader/scss/components/error/_errorlist.scss diff --git a/src/newsreader/static/src/scss/components/error/index.scss b/src/newsreader/scss/components/error/index.scss similarity index 100% rename from src/newsreader/static/src/scss/components/error/index.scss rename to src/newsreader/scss/components/error/index.scss diff --git a/src/newsreader/static/src/scss/components/form/_form.scss b/src/newsreader/scss/components/form/_form.scss similarity index 100% rename from src/newsreader/static/src/scss/components/form/_form.scss rename to src/newsreader/scss/components/form/_form.scss diff --git a/src/newsreader/static/src/scss/components/form/index.scss b/src/newsreader/scss/components/form/index.scss similarity index 100% rename from src/newsreader/static/src/scss/components/form/index.scss rename to src/newsreader/scss/components/form/index.scss diff --git a/src/newsreader/static/src/scss/components/index.scss b/src/newsreader/scss/components/index.scss similarity index 70% rename from src/newsreader/static/src/scss/components/index.scss rename to src/newsreader/scss/components/index.scss index 14ddc8e..a6c0511 100644 --- a/src/newsreader/static/src/scss/components/index.scss +++ b/src/newsreader/scss/components/index.scss @@ -4,3 +4,5 @@ @import "./main/index"; @import "./navbar/index"; @import "./error/index"; +@import "./loading-indicator/index"; +@import "./modal/index"; diff --git a/src/newsreader/static/src/scss/components/input/input.scss b/src/newsreader/scss/components/input/input.scss similarity index 100% rename from src/newsreader/static/src/scss/components/input/input.scss rename to src/newsreader/scss/components/input/input.scss 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/static/src/scss/components/main/_main.scss b/src/newsreader/scss/components/main/_main.scss similarity index 100% rename from src/newsreader/static/src/scss/components/main/_main.scss rename to src/newsreader/scss/components/main/_main.scss diff --git a/src/newsreader/static/src/scss/components/main/index.scss b/src/newsreader/scss/components/main/index.scss similarity index 100% rename from src/newsreader/static/src/scss/components/main/index.scss rename to src/newsreader/scss/components/main/index.scss diff --git a/src/newsreader/scss/components/modal/_modal.scss b/src/newsreader/scss/components/modal/_modal.scss new file mode 100644 index 0000000..c4c951f --- /dev/null +++ b/src/newsreader/scss/components/modal/_modal.scss @@ -0,0 +1,9 @@ +.modal { + position: fixed; + + width: 100%; + height: 100%; + top: 0; + + background-color: $dark; +} diff --git a/src/newsreader/scss/components/modal/index.scss b/src/newsreader/scss/components/modal/index.scss new file mode 100644 index 0000000..bcb7d8e --- /dev/null +++ b/src/newsreader/scss/components/modal/index.scss @@ -0,0 +1 @@ +@import "modal"; diff --git a/src/newsreader/static/src/scss/components/navbar/_navbar.scss b/src/newsreader/scss/components/navbar/_navbar.scss similarity index 96% rename from src/newsreader/static/src/scss/components/navbar/_navbar.scss rename to src/newsreader/scss/components/navbar/_navbar.scss index d0ea3b9..b387be0 100644 --- a/src/newsreader/static/src/scss/components/navbar/_navbar.scss +++ b/src/newsreader/scss/components/navbar/_navbar.scss @@ -2,6 +2,7 @@ display: flex; justify-content: center; + padding: 15px 0; width: 100%; background-color: $white; diff --git a/src/newsreader/static/src/scss/components/navbar/index.scss b/src/newsreader/scss/components/navbar/index.scss similarity index 100% rename from src/newsreader/static/src/scss/components/navbar/index.scss rename to src/newsreader/scss/components/navbar/index.scss diff --git a/src/newsreader/scss/homepage/components/categories/_categories.scss b/src/newsreader/scss/homepage/components/categories/_categories.scss new file mode 100644 index 0000000..002a66a --- /dev/null +++ b/src/newsreader/scss/homepage/components/categories/_categories.scss @@ -0,0 +1,24 @@ +.categories { + display: flex; + flex-direction: column; + align-items: center; + + width: 90%; + + font-family: $sidebar-font; + border-radius: 2px; + + & ul { + margin: 0; + padding: 0; + + width: 100%; + + list-style: none; + border-radius: 5px; + } + + &__item { + padding: 2px 10px 5px 10px; + } +} diff --git a/src/newsreader/scss/homepage/components/categories/index.scss b/src/newsreader/scss/homepage/components/categories/index.scss new file mode 100644 index 0000000..0eebf91 --- /dev/null +++ b/src/newsreader/scss/homepage/components/categories/index.scss @@ -0,0 +1 @@ +@import "categories"; diff --git a/src/newsreader/scss/homepage/components/category/_category.scss b/src/newsreader/scss/homepage/components/category/_category.scss new file mode 100644 index 0000000..b272bb1 --- /dev/null +++ b/src/newsreader/scss/homepage/components/category/_category.scss @@ -0,0 +1,46 @@ +.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($gainsboro, 10%); + } + + &--selected { + color: $white; + background-color: darken($gainsboro, 10%); + } +} diff --git a/src/newsreader/scss/homepage/components/category/index.scss b/src/newsreader/scss/homepage/components/category/index.scss new file mode 100644 index 0000000..702bb58 --- /dev/null +++ b/src/newsreader/scss/homepage/components/category/index.scss @@ -0,0 +1 @@ +@import "category"; diff --git a/src/newsreader/scss/homepage/components/content/_content.scss b/src/newsreader/scss/homepage/components/content/_content.scss new file mode 100644 index 0000000..9b9efb9 --- /dev/null +++ b/src/newsreader/scss/homepage/components/content/_content.scss @@ -0,0 +1,7 @@ +.content { + display: flex; + flex-direction: column; + align-items: center; + + margin: 2% 0 0 0; +} diff --git a/src/newsreader/scss/homepage/components/content/index.scss b/src/newsreader/scss/homepage/components/content/index.scss new file mode 100644 index 0000000..b424282 --- /dev/null +++ b/src/newsreader/scss/homepage/components/content/index.scss @@ -0,0 +1 @@ +@import "content"; diff --git a/src/newsreader/scss/homepage/components/index.scss b/src/newsreader/scss/homepage/components/index.scss new file mode 100644 index 0000000..b3891f8 --- /dev/null +++ b/src/newsreader/scss/homepage/components/index.scss @@ -0,0 +1,17 @@ +@import "content/index"; +@import "main/index"; + +@import "sidebar/index"; +@import "categories/index"; +@import "category/index"; + +@import "rules/index"; +@import "rule/index"; + +@import "post-block/index"; +@import "posts-section/index"; +@import "posts/index"; +@import "posts-header/index"; +@import "post/index"; +@import "post-message/index"; +@import "read-button/index"; diff --git a/src/newsreader/scss/homepage/components/main/_main.scss b/src/newsreader/scss/homepage/components/main/_main.scss new file mode 100644 index 0000000..42cb2d5 --- /dev/null +++ b/src/newsreader/scss/homepage/components/main/_main.scss @@ -0,0 +1,12 @@ +.main { + display: flex; + flex-direction: row; + width: 100%; + + margin: 0; + background-color: initial; + + &--centered { + justify-content: center; + } +} diff --git a/src/newsreader/scss/homepage/components/main/index.scss b/src/newsreader/scss/homepage/components/main/index.scss new file mode 100644 index 0000000..bdb4ce0 --- /dev/null +++ b/src/newsreader/scss/homepage/components/main/index.scss @@ -0,0 +1 @@ +@import "main"; diff --git a/src/newsreader/scss/homepage/components/post-block/_post-block.scss b/src/newsreader/scss/homepage/components/post-block/_post-block.scss new file mode 100644 index 0000000..e0d2dcd --- /dev/null +++ b/src/newsreader/scss/homepage/components/post-block/_post-block.scss @@ -0,0 +1,12 @@ +.post-block { + display: flex; + flex-direction: column; + + width: 60%; + margin: 0 0 2% 0; + + font-family: $article-font; + + border-radius: 2px; + background-color: $white; +} diff --git a/src/newsreader/scss/homepage/components/post-block/index.scss b/src/newsreader/scss/homepage/components/post-block/index.scss new file mode 100644 index 0000000..e17b7a9 --- /dev/null +++ b/src/newsreader/scss/homepage/components/post-block/index.scss @@ -0,0 +1 @@ +@import "post-block"; diff --git a/src/newsreader/scss/homepage/components/post-message/_post-message.scss b/src/newsreader/scss/homepage/components/post-message/_post-message.scss new file mode 100644 index 0000000..21c5603 --- /dev/null +++ b/src/newsreader/scss/homepage/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: max-content; + 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/homepage/components/post-message/index.scss b/src/newsreader/scss/homepage/components/post-message/index.scss new file mode 100644 index 0000000..03cf130 --- /dev/null +++ b/src/newsreader/scss/homepage/components/post-message/index.scss @@ -0,0 +1 @@ +@import "post-message"; diff --git a/src/newsreader/scss/homepage/components/post/_post.scss b/src/newsreader/scss/homepage/components/post/_post.scss new file mode 100644 index 0000000..2f8d581 --- /dev/null +++ b/src/newsreader/scss/homepage/components/post/_post.scss @@ -0,0 +1,125 @@ +.post { + display: flex; + flex-direction: column; + align-items: center; + position: relative; + + width: 80%; + max-height: 90%; + + margin: 2% auto 5% auto; + + border-radius: 2px; + + overflow-y: auto; + + background-color: $white; + + &__header { + display: flex; + flex-direction: column; + padding: 20px 0 20px 0; + width: 75%; + + font-family: $header-font; + } + + &__title { + line-height: 1; + + &--read { + color: $gainsboro; + } + } + + &__link { + height: 100%; + + & img { + height: 50%; + } + } + + &__date { + margin: 10px 0 0 0; + font-size: small; + } + + &__body { + display:flex; + flex-direction: column; + + padding: 10px 0 30px 0; + width: 75%; + + line-height: 1.5; + font-family: $article-font; + + & p { + padding: 20px 0 0 0; + } + + & img { + padding: 10px 10px 30px 10px; + + max-width: 70%; + width: inherit; + height: 100%; + + align-self: center; + } + } + + &__close-button { + margin: 1% 2% 0 0; + align-self: flex-end; + + & span { + display: inline-flex; + align-items: center; + margin: 0 0 0 5px; + + & img { + width: 10px; + } + } + + &:hover { + background-color: lighten($gainsboro, +1%); + } + } + + &__meta-info { + display: flex; + flex-direction: column; + + align-self: flex-end; + position: absolute; + top: 25%; + width: 10%; + + font-family: $button-font; + color: $nickel; + + & 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: $light-green; + } + } +} diff --git a/src/newsreader/scss/homepage/components/post/index.scss b/src/newsreader/scss/homepage/components/post/index.scss new file mode 100644 index 0000000..b31e7bb --- /dev/null +++ b/src/newsreader/scss/homepage/components/post/index.scss @@ -0,0 +1 @@ +@import "post"; diff --git a/src/newsreader/scss/homepage/components/posts-header/_posts-header.scss b/src/newsreader/scss/homepage/components/posts-header/_posts-header.scss new file mode 100644 index 0000000..068c2b3 --- /dev/null +++ b/src/newsreader/scss/homepage/components/posts-header/_posts-header.scss @@ -0,0 +1,21 @@ +.posts-header { + display: flex; + padding: 0 5px 0 0; + + width: 80%; + + &__link { + padding: 0 0 0 5px; + } + + &__title { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: small; + + &--read { + color: $gainsboro; + } + } +} diff --git a/src/newsreader/scss/homepage/components/posts-header/index.scss b/src/newsreader/scss/homepage/components/posts-header/index.scss new file mode 100644 index 0000000..451a453 --- /dev/null +++ b/src/newsreader/scss/homepage/components/posts-header/index.scss @@ -0,0 +1 @@ +@import "posts-header"; diff --git a/src/newsreader/scss/homepage/components/posts-section/index.scss b/src/newsreader/scss/homepage/components/posts-section/index.scss new file mode 100644 index 0000000..dc0e29b --- /dev/null +++ b/src/newsreader/scss/homepage/components/posts-section/index.scss @@ -0,0 +1,12 @@ +.posts-section { + display: flex; + flex-direction: column; + + padding: 10px; + + &__name { + padding: 20px 0 10px 0; + + border-top: 4px solid $azureish-white; + } +} diff --git a/src/newsreader/scss/homepage/components/posts/_posts.scss b/src/newsreader/scss/homepage/components/posts/_posts.scss new file mode 100644 index 0000000..0734e77 --- /dev/null +++ b/src/newsreader/scss/homepage/components/posts/_posts.scss @@ -0,0 +1,34 @@ +.posts { + display: flex; + flex-direction: column; + + list-style: none; + + &__item { + display: flex; + justify-content: space-between; + + padding: 10px 0 0px 0; + + border-radius: 2px; + border-bottom: 2px solid $azureish-white; + + &:hover { + cursor: pointer; + background-color: lighten($gainsboro, 10%); + } + + & span { + width: 20%; + + font-size: small; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + + &:last-child { + border-bottom: 0; + } + } +} diff --git a/src/newsreader/scss/homepage/components/posts/index.scss b/src/newsreader/scss/homepage/components/posts/index.scss new file mode 100644 index 0000000..66f1811 --- /dev/null +++ b/src/newsreader/scss/homepage/components/posts/index.scss @@ -0,0 +1 @@ +@import "posts"; diff --git a/src/newsreader/scss/homepage/components/read-button/_read-button.scss b/src/newsreader/scss/homepage/components/read-button/_read-button.scss new file mode 100644 index 0000000..a8eab4c --- /dev/null +++ b/src/newsreader/scss/homepage/components/read-button/_read-button.scss @@ -0,0 +1,10 @@ +.read-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/homepage/components/read-button/index.scss b/src/newsreader/scss/homepage/components/read-button/index.scss new file mode 100644 index 0000000..8e49454 --- /dev/null +++ b/src/newsreader/scss/homepage/components/read-button/index.scss @@ -0,0 +1 @@ +@import 'read-button'; diff --git a/src/newsreader/scss/homepage/components/rule/_rule.scss b/src/newsreader/scss/homepage/components/rule/_rule.scss new file mode 100644 index 0000000..dba7124 --- /dev/null +++ b/src/newsreader/scss/homepage/components/rule/_rule.scss @@ -0,0 +1,10 @@ +.rule { + display: flex; + width: 80%; + + &__title { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } +} diff --git a/src/newsreader/scss/homepage/components/rule/index.scss b/src/newsreader/scss/homepage/components/rule/index.scss new file mode 100644 index 0000000..7ec839a --- /dev/null +++ b/src/newsreader/scss/homepage/components/rule/index.scss @@ -0,0 +1 @@ +@import "rule"; diff --git a/src/newsreader/scss/homepage/components/rules/_rules.scss b/src/newsreader/scss/homepage/components/rules/_rules.scss new file mode 100644 index 0000000..c8b261e --- /dev/null +++ b/src/newsreader/scss/homepage/components/rules/_rules.scss @@ -0,0 +1,29 @@ +.rules { + &__item { + display: flex; + justify-content: space-between; + align-items: center; + + padding: 5px 5px 5px 20px; + + border-radius: 5px; + + & * { + padding: 0 2px 0 2px; + } + + & div { + padding: 0; + } + + &:hover { + cursor: pointer; + background-color: darken($gainsboro, 10%); + } + + &--selected { + color: $white; + background-color: darken($gainsboro, 10%); + } + } +} diff --git a/src/newsreader/scss/homepage/components/rules/index.scss b/src/newsreader/scss/homepage/components/rules/index.scss new file mode 100644 index 0000000..e6a0ebf --- /dev/null +++ b/src/newsreader/scss/homepage/components/rules/index.scss @@ -0,0 +1 @@ +@import "rules"; diff --git a/src/newsreader/scss/homepage/components/sidebar/_sidebar.scss b/src/newsreader/scss/homepage/components/sidebar/_sidebar.scss new file mode 100644 index 0000000..5c6575b --- /dev/null +++ b/src/newsreader/scss/homepage/components/sidebar/_sidebar.scss @@ -0,0 +1,11 @@ +.sidebar { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + align-self: flex-start; + + position: sticky; + top: 5%; + width: 20%; +} diff --git a/src/newsreader/scss/homepage/components/sidebar/index.scss b/src/newsreader/scss/homepage/components/sidebar/index.scss new file mode 100644 index 0000000..0abffa8 --- /dev/null +++ b/src/newsreader/scss/homepage/components/sidebar/index.scss @@ -0,0 +1 @@ +@import "sidebar"; diff --git a/src/newsreader/scss/homepage/elements/badge/_badge.scss b/src/newsreader/scss/homepage/elements/badge/_badge.scss new file mode 100644 index 0000000..6abab65 --- /dev/null +++ b/src/newsreader/scss/homepage/elements/badge/_badge.scss @@ -0,0 +1,15 @@ +.badge { + display: inline-block; + + padding-left: 8px; + padding-right: 8px; + border-radius: 20%; + + text-align: center; + + background-color: darken($pink, 10%); + color: $white; + + font-family: $button-font; + font-size: small; +} diff --git a/src/newsreader/scss/homepage/elements/badge/index.scss b/src/newsreader/scss/homepage/elements/badge/index.scss new file mode 100644 index 0000000..87110f0 --- /dev/null +++ b/src/newsreader/scss/homepage/elements/badge/index.scss @@ -0,0 +1 @@ +@import "badge"; diff --git a/src/newsreader/scss/homepage/elements/index.scss b/src/newsreader/scss/homepage/elements/index.scss new file mode 100644 index 0000000..66bce62 --- /dev/null +++ b/src/newsreader/scss/homepage/elements/index.scss @@ -0,0 +1 @@ +@import "badge/index"; diff --git a/src/newsreader/scss/homepage/index.scss b/src/newsreader/scss/homepage/index.scss new file mode 100644 index 0000000..aad2761 --- /dev/null +++ b/src/newsreader/scss/homepage/index.scss @@ -0,0 +1,7 @@ +// General imports +@import "../partials/variables"; +@import "../components/index"; + +// Page specific +@import "./components/index"; +@import "./elements/index"; diff --git a/src/newsreader/scss/partials/_variables.scss b/src/newsreader/scss/partials/_variables.scss new file mode 100644 index 0000000..f3a2c77 --- /dev/null +++ b/src/newsreader/scss/partials/_variables.scss @@ -0,0 +1,42 @@ +@import url("https://fonts.googleapis.com/css?family=IBM+Plex+Sans:600&display=swap"); +@import url("https://fonts.googleapis.com/css?family=Barlow&display=swap"); +@import url("https://fonts.googleapis.com/css?family=Open+Sans&display=swap"); +@import url("https://fonts.googleapis.com/css?family=Oswald&display=swap"); +@import url("https://fonts.googleapis.com/css?family=Nunito&display=swap"); + + +$button-font: "IBM Plex Sans", sans-serif; +$form-font: "Barlow", sans-serif; +$article-font: "Open Sans", sans-serif; +$header-font: "Oswald", sans-serif; +$sidebar-font: "Nunito", sans-serif; + +/* colors */ +$white: rgba(255, 255, 255, 1); + +$confirm-green: rgba(46,204,113, 1); +$error-red: rgba(231,76,60, 1); + +// light blue +$azureish-white: rgba(205, 230, 245, 1); + +// dark blue +$pewter-blue: rgba(141, 167, 190, 1); + +// light gray +$gainsboro: rgba(224, 221, 220, 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); + +$light-green: rgba(230, 247, 185, 1); +$light-orange: rgba(237, 212, 178, 1); +$light-red: rgba(255, 118, 117, 1); + +$dark: rgba(0, 0, 0, 0.4); diff --git a/src/newsreader/static/icons/angle-down.svg b/src/newsreader/static/icons/angle-down.svg new file mode 100644 index 0000000..1462342 --- /dev/null +++ b/src/newsreader/static/icons/angle-down.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/newsreader/static/icons/angle-right.svg b/src/newsreader/static/icons/angle-right.svg new file mode 100644 index 0000000..ec7fbe9 --- /dev/null +++ b/src/newsreader/static/icons/angle-right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/newsreader/static/icons/arrow-left.svg b/src/newsreader/static/icons/arrow-left.svg new file mode 100644 index 0000000..88526e8 --- /dev/null +++ b/src/newsreader/static/icons/arrow-left.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/newsreader/static/icons/chevron-down.svg b/src/newsreader/static/icons/chevron-down.svg new file mode 100644 index 0000000..211fd17 --- /dev/null +++ b/src/newsreader/static/icons/chevron-down.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/newsreader/static/icons/chevron-right.svg b/src/newsreader/static/icons/chevron-right.svg new file mode 100644 index 0000000..ea2b601 --- /dev/null +++ b/src/newsreader/static/icons/chevron-right.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/newsreader/static/icons/link.svg b/src/newsreader/static/icons/link.svg new file mode 100644 index 0000000..57caa9f --- /dev/null +++ b/src/newsreader/static/icons/link.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/newsreader/static/icons/times.svg b/src/newsreader/static/icons/times.svg new file mode 100644 index 0000000..571a32a --- /dev/null +++ b/src/newsreader/static/icons/times.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/newsreader/static/src/scss/partials/_variables.scss b/src/newsreader/static/src/scss/partials/_variables.scss deleted file mode 100644 index d983d2a..0000000 --- a/src/newsreader/static/src/scss/partials/_variables.scss +++ /dev/null @@ -1,26 +0,0 @@ -@import url("https://fonts.googleapis.com/css?family=IBM+Plex+Sans:600&display=swap"); -@import url('https://fonts.googleapis.com/css?family=Barlow&display=swap'); - -$button-font: "IBM Plex Sans", sans-serif; -$form-font: "Barlow", sans-serif; - -/* colors */ -$white: rgba(255, 255, 255, 1); - -$confirm-green: rgba(46,204,113, 1); -$error-red: rgba(231,76,60, 1); - -// light blue -$azureish-white: rgba(205, 230, 245, 1); - -// dark blue -$pewter-blue: rgba(141, 167, 190, 1); - -// light gray -$gainsboro: rgba(224, 221, 220, 1); - -// medium gray -$roman-silver: rgba(135, 145, 158, 1); - -//dark gray -$nickel: rgba(112, 112, 120, 1); diff --git a/src/newsreader/templates/base.html b/src/newsreader/templates/base.html index 460ab8f..8163fa6 100644 --- a/src/newsreader/templates/base.html +++ b/src/newsreader/templates/base.html @@ -9,7 +9,9 @@ {% if messages %} -
      +
        {% for message in messages %} - + {{ message }} {% endfor %} diff --git a/src/newsreader/urls.py b/src/newsreader/urls.py index 77568d3..be59ad1 100644 --- a/src/newsreader/urls.py +++ b/src/newsreader/urls.py @@ -7,7 +7,7 @@ from rest_framework_swagger.views import get_swagger_view from newsreader.accounts.urls import urlpatterns as login_urls from newsreader.news.collection.urls import endpoints as collection_endpoints from newsreader.news.core.urls import endpoints as core_endpoints -from newsreader.news.core.urls import index_page +from newsreader.news.core.urls import urlpatterns schema_view = get_swagger_view(title="Newsreader API") @@ -18,7 +18,7 @@ endpoints = [ ] urlpatterns = [ - path("", index_page, name="index"), + path("", include(urlpatterns)), path("accounts/", include((login_urls, "accounts")), name="accounts"), path("admin/", admin.site.urls, name="admin"), path("api/", include((endpoints, "api")), name="api"), From 94f4ed63278d9d42cb2089c75be241f4eeffcedf Mon Sep 17 00:00:00 2001 From: Sonny Date: Sun, 24 Nov 2019 16:41:46 +0100 Subject: [PATCH 026/364] Redux refactor --- .../categories/components/CategoryCard.js | 2 +- .../js/pages/homepage/actions/categories.js | 17 ++- .../js/pages/homepage/actions/posts.js | 64 +++++------ .../js/pages/homepage/actions/rules.js | 10 +- .../js/pages/homepage/actions/selected.js | 41 ++++--- .../js/pages/homepage/components/PostModal.js | 6 +- .../homepage/components/feedlist/FeedList.js | 13 +-- .../homepage/components/feedlist/PostItem.js | 2 +- .../homepage/components/feedlist/RuleItem.js | 1 - .../homepage/components/feedlist/filters.js | 18 ++-- .../components/sidebar/CategoryItem.js | 17 ++- .../homepage/components/sidebar/RuleItem.js | 31 +++--- .../homepage/components/sidebar/Sidebar.js | 1 - .../homepage/components/sidebar/functions.js | 7 ++ src/newsreader/js/pages/homepage/constants.js | 2 + .../js/pages/homepage/reducers/categories.js | 101 +++++++----------- .../js/pages/homepage/reducers/posts.js | 22 ++-- .../js/pages/homepage/reducers/rules.js | 62 +++++++---- .../js/pages/homepage/reducers/selected.js | 34 ++++-- .../news/core/templates/core/category.html | 2 +- .../scss/components/body/_body.scss | 2 + src/newsreader/scss/elements/a/_a.scss | 3 + src/newsreader/scss/elements/a/index.scss | 1 + .../scss/elements/button/_button.scss | 6 +- src/newsreader/scss/elements/index.scss | 1 + src/newsreader/scss/elements/link/_link.scss | 1 + .../components/category/_category.scss | 5 +- .../components/post-block/_post-block.scss | 2 +- .../pages/homepage/components/post/_post.scss | 7 +- .../posts-header/_posts-header.scss | 6 +- .../homepage/components/posts/_posts.scss | 7 +- .../pages/homepage/components/rule/_rule.scss | 5 + .../homepage/components/rules/_rules.scss | 5 +- .../pages/homepage/elements/badge/_badge.scss | 5 +- src/newsreader/scss/partials/_colors.scss | 3 +- src/newsreader/static/icons/alarm.svg | 6 ++ src/newsreader/static/icons/apartment.svg | 30 ++++++ .../static/icons/arrow-down-circle.svg | 7 ++ src/newsreader/static/icons/arrow-down.svg | 6 ++ .../static/icons/arrow-left-circle.svg | 7 ++ src/newsreader/static/icons/arrow-left.svg | 12 +-- .../static/icons/arrow-right-circle.svg | 7 ++ src/newsreader/static/icons/arrow-right.svg | 6 ++ .../static/icons/arrow-up-circle.svg | 7 ++ src/newsreader/static/icons/arrow-up.svg | 6 ++ src/newsreader/static/icons/bicycle.svg | 9 ++ src/newsreader/static/icons/bold.svg | 8 ++ src/newsreader/static/icons/book.svg | 7 ++ src/newsreader/static/icons/bookmark.svg | 6 ++ src/newsreader/static/icons/briefcase.svg | 6 ++ src/newsreader/static/icons/bubble.svg | 6 ++ src/newsreader/static/icons/bug.svg | 6 ++ src/newsreader/static/icons/bullhorn.svg | 6 ++ src/newsreader/static/icons/bus.svg | 10 ++ src/newsreader/static/icons/calendar-full.svg | 25 +++++ src/newsreader/static/icons/camera-video.svg | 7 ++ src/newsreader/static/icons/camera.svg | 7 ++ src/newsreader/static/icons/car.svg | 8 ++ src/newsreader/static/icons/cart.svg | 8 ++ src/newsreader/static/icons/chart-bars.svg | 9 ++ .../static/icons/checkmark-circle.svg | 7 ++ .../static/icons/chevron-down-circle.svg | 7 ++ src/newsreader/static/icons/chevron-down.svg | 12 +-- .../static/icons/chevron-left-circle.svg | 7 ++ src/newsreader/static/icons/chevron-left.svg | 6 ++ .../static/icons/chevron-right-circle.svg | 7 ++ src/newsreader/static/icons/chevron-right.svg | 12 +-- .../static/icons/chevron-up-circle.svg | 7 ++ src/newsreader/static/icons/chevron-up.svg | 6 ++ src/newsreader/static/icons/circle-minus.svg | 7 ++ src/newsreader/static/icons/clock.svg | 7 ++ src/newsreader/static/icons/cloud-check.svg | 7 ++ .../static/icons/cloud-download.svg | 7 ++ src/newsreader/static/icons/cloud-sync.svg | 8 ++ src/newsreader/static/icons/cloud-upload.svg | 7 ++ src/newsreader/static/icons/cloud.svg | 6 ++ src/newsreader/static/icons/code.svg | 8 ++ src/newsreader/static/icons/coffee-cup.svg | 7 ++ src/newsreader/static/icons/cog.svg | 7 ++ src/newsreader/static/icons/construction.svg | 6 ++ src/newsreader/static/icons/crop.svg | 9 ++ src/newsreader/static/icons/cross-circle.svg | 7 ++ src/newsreader/static/icons/cross.svg | 6 ++ src/newsreader/static/icons/database.svg | 6 ++ src/newsreader/static/icons/diamond.svg | 6 ++ src/newsreader/static/icons/dice.svg | 12 +++ src/newsreader/static/icons/dinner.svg | 7 ++ src/newsreader/static/icons/direction-ltr.svg | 7 ++ src/newsreader/static/icons/direction-rtl.svg | 7 ++ src/newsreader/static/icons/download.svg | 7 ++ src/newsreader/static/icons/drop.svg | 6 ++ src/newsreader/static/icons/earth.svg | 6 ++ src/newsreader/static/icons/enter-down.svg | 7 ++ src/newsreader/static/icons/enter.svg | 7 ++ src/newsreader/static/icons/envelope.svg | 6 ++ src/newsreader/static/icons/exit-up.svg | 7 ++ src/newsreader/static/icons/exit.svg | 7 ++ src/newsreader/static/icons/eye.svg | 6 ++ src/newsreader/static/icons/file-add.svg | 7 ++ src/newsreader/static/icons/file-empty.svg | 6 ++ src/newsreader/static/icons/film-play.svg | 7 ++ src/newsreader/static/icons/flag.svg | 7 ++ .../static/icons/frame-contract.svg | 9 ++ src/newsreader/static/icons/frame-expand.svg | 9 ++ src/newsreader/static/icons/funnel.svg | 6 ++ src/newsreader/static/icons/gift.svg | 6 ++ .../static/icons/graduation-hat.svg | 6 ++ src/newsreader/static/icons/hand.svg | 6 ++ src/newsreader/static/icons/heart-pulse.svg | 8 ++ src/newsreader/static/icons/heart.svg | 6 ++ src/newsreader/static/icons/highlight.svg | 6 ++ src/newsreader/static/icons/history.svg | 7 ++ src/newsreader/static/icons/home.svg | 6 ++ src/newsreader/static/icons/hourglass.svg | 7 ++ src/newsreader/static/icons/inbox.svg | 6 ++ .../static/icons/indent-decrease.svg | 11 ++ .../static/icons/indent-increase.svg | 11 ++ src/newsreader/static/icons/italic.svg | 6 ++ src/newsreader/static/icons/keyboard.svg | 27 +++++ src/newsreader/static/icons/laptop-phone.svg | 9 ++ src/newsreader/static/icons/laptop.svg | 7 ++ src/newsreader/static/icons/layers.svg | 8 ++ src/newsreader/static/icons/leaf.svg | 6 ++ src/newsreader/static/icons/license.svg | 12 +++ src/newsreader/static/icons/lighter.svg | 7 ++ src/newsreader/static/icons/line-spacing.svg | 10 ++ src/newsreader/static/icons/linearicons.svg | 7 ++ src/newsreader/static/icons/link.svg | 8 +- src/newsreader/static/icons/list.svg | 11 ++ src/newsreader/static/icons/location.svg | 6 ++ src/newsreader/static/icons/lock.svg | 6 ++ src/newsreader/static/icons/magic-wand.svg | 10 ++ src/newsreader/static/icons/magnifier.svg | 6 ++ src/newsreader/static/icons/map-marker.svg | 7 ++ src/newsreader/static/icons/map.svg | 6 ++ src/newsreader/static/icons/menu-circle.svg | 9 ++ src/newsreader/static/icons/menu.svg | 8 ++ src/newsreader/static/icons/mic.svg | 7 ++ src/newsreader/static/icons/moon.svg | 6 ++ src/newsreader/static/icons/move.svg | 6 ++ src/newsreader/static/icons/music-note.svg | 6 ++ src/newsreader/static/icons/mustache.svg | 9 ++ src/newsreader/static/icons/neutral.svg | 9 ++ src/newsreader/static/icons/page-break.svg | 14 +++ src/newsreader/static/icons/paperclip.svg | 6 ++ src/newsreader/static/icons/paw.svg | 10 ++ src/newsreader/static/icons/pencil.svg | 6 ++ src/newsreader/static/icons/phone-handset.svg | 6 ++ src/newsreader/static/icons/phone.svg | 8 ++ src/newsreader/static/icons/picture.svg | 8 ++ src/newsreader/static/icons/pie-chart.svg | 7 ++ src/newsreader/static/icons/pilcrow.svg | 6 ++ src/newsreader/static/icons/plus-circle.svg | 7 ++ src/newsreader/static/icons/pointer-down.svg | 6 ++ src/newsreader/static/icons/pointer-left.svg | 6 ++ src/newsreader/static/icons/pointer-right.svg | 6 ++ src/newsreader/static/icons/pointer-up.svg | 6 ++ src/newsreader/static/icons/poop.svg | 6 ++ src/newsreader/static/icons/power-switch.svg | 7 ++ src/newsreader/static/icons/printer.svg | 10 ++ src/newsreader/static/icons/pushpin.svg | 6 ++ .../static/icons/question-circle.svg | 8 ++ src/newsreader/static/icons/redo.svg | 6 ++ src/newsreader/static/icons/rocket.svg | 8 ++ src/newsreader/static/icons/sad.svg | 9 ++ src/newsreader/static/icons/screen.svg | 6 ++ src/newsreader/static/icons/select.svg | 7 ++ src/newsreader/static/icons/shirt.svg | 6 ++ src/newsreader/static/icons/smartphone.svg | 8 ++ src/newsreader/static/icons/smile.svg | 9 ++ .../static/icons/sort-alpha-asc.svg | 8 ++ .../static/icons/sort-amount-asc.svg | 10 ++ src/newsreader/static/icons/spell-check.svg | 9 ++ src/newsreader/static/icons/star-empty.svg | 15 +++ src/newsreader/static/icons/star-half.svg | 10 ++ src/newsreader/static/icons/star.svg | 6 ++ src/newsreader/static/icons/store.svg | 10 ++ src/newsreader/static/icons/strikethrough.svg | 8 ++ src/newsreader/static/icons/sun.svg | 14 +++ src/newsreader/static/icons/sync.svg | 7 ++ src/newsreader/static/icons/tablet.svg | 8 ++ src/newsreader/static/icons/tag.svg | 7 ++ .../static/icons/text-align-center.svg | 10 ++ .../static/icons/text-align-justify.svg | 10 ++ .../static/icons/text-align-left.svg | 10 ++ .../static/icons/text-align-right.svg | 10 ++ .../static/icons/text-format-remove.svg | 8 ++ src/newsreader/static/icons/text-format.svg | 7 ++ src/newsreader/static/icons/text-size.svg | 7 ++ src/newsreader/static/icons/thumbs-down.svg | 6 ++ src/newsreader/static/icons/thumbs-up.svg | 6 ++ src/newsreader/static/icons/train.svg | 11 ++ src/newsreader/static/icons/trash.svg | 9 ++ src/newsreader/static/icons/underline.svg | 7 ++ src/newsreader/static/icons/undo.svg | 6 ++ src/newsreader/static/icons/unlink.svg | 13 +++ src/newsreader/static/icons/upload.svg | 7 ++ src/newsreader/static/icons/user.svg | 7 ++ src/newsreader/static/icons/users.svg | 9 ++ src/newsreader/static/icons/volume-high.svg | 9 ++ src/newsreader/static/icons/volume-low.svg | 7 ++ src/newsreader/static/icons/volume-medium.svg | 8 ++ src/newsreader/static/icons/volume.svg | 6 ++ src/newsreader/static/icons/warning.svg | 8 ++ src/newsreader/static/icons/wheelchair.svg | 7 ++ 205 files changed, 1610 insertions(+), 252 deletions(-) create mode 100644 src/newsreader/js/pages/homepage/components/sidebar/functions.js create mode 100644 src/newsreader/js/pages/homepage/constants.js create mode 100644 src/newsreader/scss/elements/a/_a.scss create mode 100644 src/newsreader/scss/elements/a/index.scss create mode 100755 src/newsreader/static/icons/alarm.svg create mode 100755 src/newsreader/static/icons/apartment.svg create mode 100755 src/newsreader/static/icons/arrow-down-circle.svg create mode 100755 src/newsreader/static/icons/arrow-down.svg create mode 100755 src/newsreader/static/icons/arrow-left-circle.svg create mode 100755 src/newsreader/static/icons/arrow-right-circle.svg create mode 100755 src/newsreader/static/icons/arrow-right.svg create mode 100755 src/newsreader/static/icons/arrow-up-circle.svg create mode 100755 src/newsreader/static/icons/arrow-up.svg create mode 100755 src/newsreader/static/icons/bicycle.svg create mode 100755 src/newsreader/static/icons/bold.svg create mode 100755 src/newsreader/static/icons/book.svg create mode 100755 src/newsreader/static/icons/bookmark.svg create mode 100755 src/newsreader/static/icons/briefcase.svg create mode 100755 src/newsreader/static/icons/bubble.svg create mode 100755 src/newsreader/static/icons/bug.svg create mode 100755 src/newsreader/static/icons/bullhorn.svg create mode 100755 src/newsreader/static/icons/bus.svg create mode 100755 src/newsreader/static/icons/calendar-full.svg create mode 100755 src/newsreader/static/icons/camera-video.svg create mode 100755 src/newsreader/static/icons/camera.svg create mode 100755 src/newsreader/static/icons/car.svg create mode 100755 src/newsreader/static/icons/cart.svg create mode 100755 src/newsreader/static/icons/chart-bars.svg create mode 100755 src/newsreader/static/icons/checkmark-circle.svg create mode 100755 src/newsreader/static/icons/chevron-down-circle.svg create mode 100755 src/newsreader/static/icons/chevron-left-circle.svg create mode 100755 src/newsreader/static/icons/chevron-left.svg create mode 100755 src/newsreader/static/icons/chevron-right-circle.svg create mode 100755 src/newsreader/static/icons/chevron-up-circle.svg create mode 100755 src/newsreader/static/icons/chevron-up.svg create mode 100755 src/newsreader/static/icons/circle-minus.svg create mode 100755 src/newsreader/static/icons/clock.svg create mode 100755 src/newsreader/static/icons/cloud-check.svg create mode 100755 src/newsreader/static/icons/cloud-download.svg create mode 100755 src/newsreader/static/icons/cloud-sync.svg create mode 100755 src/newsreader/static/icons/cloud-upload.svg create mode 100755 src/newsreader/static/icons/cloud.svg create mode 100755 src/newsreader/static/icons/code.svg create mode 100755 src/newsreader/static/icons/coffee-cup.svg create mode 100755 src/newsreader/static/icons/cog.svg create mode 100755 src/newsreader/static/icons/construction.svg create mode 100755 src/newsreader/static/icons/crop.svg create mode 100755 src/newsreader/static/icons/cross-circle.svg create mode 100755 src/newsreader/static/icons/cross.svg create mode 100755 src/newsreader/static/icons/database.svg create mode 100755 src/newsreader/static/icons/diamond.svg create mode 100755 src/newsreader/static/icons/dice.svg create mode 100755 src/newsreader/static/icons/dinner.svg create mode 100755 src/newsreader/static/icons/direction-ltr.svg create mode 100755 src/newsreader/static/icons/direction-rtl.svg create mode 100755 src/newsreader/static/icons/download.svg create mode 100755 src/newsreader/static/icons/drop.svg create mode 100755 src/newsreader/static/icons/earth.svg create mode 100755 src/newsreader/static/icons/enter-down.svg create mode 100755 src/newsreader/static/icons/enter.svg create mode 100755 src/newsreader/static/icons/envelope.svg create mode 100755 src/newsreader/static/icons/exit-up.svg create mode 100755 src/newsreader/static/icons/exit.svg create mode 100755 src/newsreader/static/icons/eye.svg create mode 100755 src/newsreader/static/icons/file-add.svg create mode 100755 src/newsreader/static/icons/file-empty.svg create mode 100755 src/newsreader/static/icons/film-play.svg create mode 100755 src/newsreader/static/icons/flag.svg create mode 100755 src/newsreader/static/icons/frame-contract.svg create mode 100755 src/newsreader/static/icons/frame-expand.svg create mode 100755 src/newsreader/static/icons/funnel.svg create mode 100755 src/newsreader/static/icons/gift.svg create mode 100755 src/newsreader/static/icons/graduation-hat.svg create mode 100755 src/newsreader/static/icons/hand.svg create mode 100755 src/newsreader/static/icons/heart-pulse.svg create mode 100755 src/newsreader/static/icons/heart.svg create mode 100755 src/newsreader/static/icons/highlight.svg create mode 100755 src/newsreader/static/icons/history.svg create mode 100755 src/newsreader/static/icons/home.svg create mode 100755 src/newsreader/static/icons/hourglass.svg create mode 100755 src/newsreader/static/icons/inbox.svg create mode 100755 src/newsreader/static/icons/indent-decrease.svg create mode 100755 src/newsreader/static/icons/indent-increase.svg create mode 100755 src/newsreader/static/icons/italic.svg create mode 100755 src/newsreader/static/icons/keyboard.svg create mode 100755 src/newsreader/static/icons/laptop-phone.svg create mode 100755 src/newsreader/static/icons/laptop.svg create mode 100755 src/newsreader/static/icons/layers.svg create mode 100755 src/newsreader/static/icons/leaf.svg create mode 100755 src/newsreader/static/icons/license.svg create mode 100755 src/newsreader/static/icons/lighter.svg create mode 100755 src/newsreader/static/icons/line-spacing.svg create mode 100755 src/newsreader/static/icons/linearicons.svg create mode 100755 src/newsreader/static/icons/list.svg create mode 100755 src/newsreader/static/icons/location.svg create mode 100755 src/newsreader/static/icons/lock.svg create mode 100755 src/newsreader/static/icons/magic-wand.svg create mode 100755 src/newsreader/static/icons/magnifier.svg create mode 100755 src/newsreader/static/icons/map-marker.svg create mode 100755 src/newsreader/static/icons/map.svg create mode 100755 src/newsreader/static/icons/menu-circle.svg create mode 100755 src/newsreader/static/icons/menu.svg create mode 100755 src/newsreader/static/icons/mic.svg create mode 100755 src/newsreader/static/icons/moon.svg create mode 100755 src/newsreader/static/icons/move.svg create mode 100755 src/newsreader/static/icons/music-note.svg create mode 100755 src/newsreader/static/icons/mustache.svg create mode 100755 src/newsreader/static/icons/neutral.svg create mode 100755 src/newsreader/static/icons/page-break.svg create mode 100755 src/newsreader/static/icons/paperclip.svg create mode 100755 src/newsreader/static/icons/paw.svg create mode 100755 src/newsreader/static/icons/pencil.svg create mode 100755 src/newsreader/static/icons/phone-handset.svg create mode 100755 src/newsreader/static/icons/phone.svg create mode 100755 src/newsreader/static/icons/picture.svg create mode 100755 src/newsreader/static/icons/pie-chart.svg create mode 100755 src/newsreader/static/icons/pilcrow.svg create mode 100755 src/newsreader/static/icons/plus-circle.svg create mode 100755 src/newsreader/static/icons/pointer-down.svg create mode 100755 src/newsreader/static/icons/pointer-left.svg create mode 100755 src/newsreader/static/icons/pointer-right.svg create mode 100755 src/newsreader/static/icons/pointer-up.svg create mode 100755 src/newsreader/static/icons/poop.svg create mode 100755 src/newsreader/static/icons/power-switch.svg create mode 100755 src/newsreader/static/icons/printer.svg create mode 100755 src/newsreader/static/icons/pushpin.svg create mode 100755 src/newsreader/static/icons/question-circle.svg create mode 100755 src/newsreader/static/icons/redo.svg create mode 100755 src/newsreader/static/icons/rocket.svg create mode 100755 src/newsreader/static/icons/sad.svg create mode 100755 src/newsreader/static/icons/screen.svg create mode 100755 src/newsreader/static/icons/select.svg create mode 100755 src/newsreader/static/icons/shirt.svg create mode 100755 src/newsreader/static/icons/smartphone.svg create mode 100755 src/newsreader/static/icons/smile.svg create mode 100755 src/newsreader/static/icons/sort-alpha-asc.svg create mode 100755 src/newsreader/static/icons/sort-amount-asc.svg create mode 100755 src/newsreader/static/icons/spell-check.svg create mode 100755 src/newsreader/static/icons/star-empty.svg create mode 100755 src/newsreader/static/icons/star-half.svg create mode 100755 src/newsreader/static/icons/star.svg create mode 100755 src/newsreader/static/icons/store.svg create mode 100755 src/newsreader/static/icons/strikethrough.svg create mode 100755 src/newsreader/static/icons/sun.svg create mode 100755 src/newsreader/static/icons/sync.svg create mode 100755 src/newsreader/static/icons/tablet.svg create mode 100755 src/newsreader/static/icons/tag.svg create mode 100755 src/newsreader/static/icons/text-align-center.svg create mode 100755 src/newsreader/static/icons/text-align-justify.svg create mode 100755 src/newsreader/static/icons/text-align-left.svg create mode 100755 src/newsreader/static/icons/text-align-right.svg create mode 100755 src/newsreader/static/icons/text-format-remove.svg create mode 100755 src/newsreader/static/icons/text-format.svg create mode 100755 src/newsreader/static/icons/text-size.svg create mode 100755 src/newsreader/static/icons/thumbs-down.svg create mode 100755 src/newsreader/static/icons/thumbs-up.svg create mode 100755 src/newsreader/static/icons/train.svg create mode 100755 src/newsreader/static/icons/trash.svg create mode 100755 src/newsreader/static/icons/underline.svg create mode 100755 src/newsreader/static/icons/undo.svg create mode 100755 src/newsreader/static/icons/unlink.svg create mode 100755 src/newsreader/static/icons/upload.svg create mode 100755 src/newsreader/static/icons/user.svg create mode 100755 src/newsreader/static/icons/users.svg create mode 100755 src/newsreader/static/icons/volume-high.svg create mode 100755 src/newsreader/static/icons/volume-low.svg create mode 100755 src/newsreader/static/icons/volume-medium.svg create mode 100755 src/newsreader/static/icons/volume.svg create mode 100755 src/newsreader/static/icons/warning.svg create mode 100755 src/newsreader/static/icons/wheelchair.svg diff --git a/src/newsreader/js/pages/categories/components/CategoryCard.js b/src/newsreader/js/pages/categories/components/CategoryCard.js index df5df70..04fccef 100644 --- a/src/newsreader/js/pages/categories/components/CategoryCard.js +++ b/src/newsreader/js/pages/categories/components/CategoryCard.js @@ -6,7 +6,7 @@ const CategoryCard = props => { const { category } = props; const categoryRules = category.rules.map(rule => { - const faviconUrl = rule.favicon ? rule.favicon : '/static/favicon-placeholder.svg'; + const faviconUrl = rule.favicon ? rule.favicon : '/static/picture.svg'; return (
      • diff --git a/src/newsreader/js/pages/homepage/actions/categories.js b/src/newsreader/js/pages/homepage/actions/categories.js index 07a1066..85772a6 100644 --- a/src/newsreader/js/pages/homepage/actions/categories.js +++ b/src/newsreader/js/pages/homepage/actions/categories.js @@ -1,5 +1,7 @@ import { requestRules, receiveRules, fetchRulesByCategory } from './rules.js'; +import { CATEGORY_TYPE } from '../constants.js'; + export const SELECT_CATEGORY = 'SELECT_CATEGORY'; export const RECEIVE_CATEGORY = 'RECEIVE_CATEGORY'; @@ -10,7 +12,7 @@ export const REQUEST_CATEGORIES = 'REQUEST_CATEGORIES'; export const selectCategory = category => ({ type: SELECT_CATEGORY, - item: category, + section: { ...category, type: CATEGORY_TYPE }, }); export const receiveCategory = category => ({ @@ -18,9 +20,9 @@ export const receiveCategory = category => ({ category, }); -export const receiveCategories = json => ({ +export const receiveCategories = categories => ({ type: RECEIVE_CATEGORIES, - categories: json, + categories, }); export const requestCategory = () => ({ type: REQUEST_CATEGORY }); @@ -70,7 +72,14 @@ export const fetchCategories = () => { }; export const fetchCategory = category => { - return dispatch => { + 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}`) diff --git a/src/newsreader/js/pages/homepage/actions/posts.js b/src/newsreader/js/pages/homepage/actions/posts.js index 9951859..59229ab 100644 --- a/src/newsreader/js/pages/homepage/actions/posts.js +++ b/src/newsreader/js/pages/homepage/actions/posts.js @@ -1,3 +1,5 @@ +import { RULE_TYPE } from '../constants.js'; + export const SELECT_POST = 'SELECT_POST'; export const UNSELECT_POST = 'UNSELECT_POST'; @@ -16,17 +18,15 @@ export const unSelectPost = () => ({ type: UNSELECT_POST, }); -export const postRead = (post, rule, category) => ({ +export const postRead = (post, section) => ({ type: MARK_POST_READ, - category: category, - post: post, - rule: rule, + post, + section, }); export const markPostRead = (post, token) => { return (dispatch, getState) => { const { rules } = getState(); - const { categories } = getState(); const url = `/api/posts/${post.id}/`; const options = { @@ -38,17 +38,16 @@ export const markPostRead = (post, token) => { body: JSON.stringify({ read: true }), }; - const rule = rules.items[post.rule]; - const category = categories.items[rule.category]; + const section = { + ...rules.items[post.rule], + type: RULE_TYPE, + }; return fetch(url, options) .then(response => response.json()) .then(updatedPost => { - const updatedRule = { ...rule, unread: rule.unread - 1 }; - const updatedCategory = { ...category, unread: category.unread - 1 }; - dispatch(receivePost({ ...updatedPost })); - dispatch(postRead({ ...updatedPost }, updatedRule, updatedCategory)); + dispatch(postRead({ ...updatedPost }, section)); }); }; }; @@ -66,37 +65,24 @@ export const receivePost = post => ({ export const requestPosts = () => ({ type: REQUEST_POSTS }); -export const fetchPostsByCategory = (category, page = false) => { +export const fetchPostsBySection = (section, page = false) => { return dispatch => { + if (section.unread === 0) { + return; + } + dispatch(requestPosts()); - const url = page ? page : `/api/categories/${category.id}/posts/?read=false`; - return fetch(url) - .then(response => response.json()) - .then(json => { - const posts = {}; - - json.results.forEach(post => { - posts[post.id] = post; - }); - - dispatch(receivePosts({ items: posts, next: json.next })); - }) - .catch(error => { - if (error instanceof TypeError) { - console.log(`Unable to parse posts from request: ${error}`); - } - - dispatch(receivePosts({ items: {}, next: null })); - }); - }; -}; - -export const fetchPostsByRule = (rule, page = false) => { - return dispatch => { - dispatch(requestPosts()); - - const url = page ? page : `/api/rules/${rule.id}/posts/?read=false`; + let url = null; + + switch ('category' in section) { + case true: + url = page ? page : `/api/rules/${section.id}/posts/?read=false`; + break; + default: + url = page ? page : `/api/categories/${section.id}/posts/?read=false`; + } + return fetch(url) .then(response => response.json()) .then(json => { diff --git a/src/newsreader/js/pages/homepage/actions/rules.js b/src/newsreader/js/pages/homepage/actions/rules.js index 0b843f6..50c8f95 100644 --- a/src/newsreader/js/pages/homepage/actions/rules.js +++ b/src/newsreader/js/pages/homepage/actions/rules.js @@ -1,4 +1,5 @@ import { fetchCategory } from './categories.js'; +import { RULE_TYPE } from '../constants.js'; export const SELECT_RULE = 'SELECT_RULE'; export const SELECT_RULES = 'SELECT_RULES'; @@ -11,7 +12,7 @@ export const REQUEST_RULES = 'REQUEST_RULES'; export const selectRule = rule => ({ type: SELECT_RULE, - item: rule, + section: { ...rule, type: RULE_TYPE }, }); export const requestRule = () => ({ type: REQUEST_RULE }); @@ -29,6 +30,13 @@ export const receiveRules = 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(); diff --git a/src/newsreader/js/pages/homepage/actions/selected.js b/src/newsreader/js/pages/homepage/actions/selected.js index 95b8603..0a69266 100644 --- a/src/newsreader/js/pages/homepage/actions/selected.js +++ b/src/newsreader/js/pages/homepage/actions/selected.js @@ -1,18 +1,28 @@ 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 = (category, rule = {}) => ({ - category: category, - rule: rule, +export const markSectionRead = section => ({ type: MARK_SECTION_READ, + section, }); const markCategoryRead = (category, token) => { - return dispatch => { + 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', @@ -25,16 +35,20 @@ const markCategoryRead = (category, token) => { .then(response => response.json()) .then(updatedCategory => { dispatch(receiveCategory({ ...updatedCategory })); - dispatch(markSectionRead({ ...category, ...updatedCategory })); + dispatch( + markSectionRead({ + ...category, + ...updatedCategory, + rules: ruleMapping, + type: CATEGORY_TYPE, + }) + ); }); }; }; const markRuleRead = (rule, token) => { return (dispatch, getState) => { - const { categories } = getState(); - const category = categories.items[rule.category]; - dispatch(requestRule(rule)); const url = `/api/rules/${rule.id}/read/`; @@ -51,15 +65,16 @@ const markRuleRead = (rule, token) => { dispatch(receiveRule({ ...updatedRule })); // Use the old rule to decrement category with old unread count - dispatch(markSectionRead({ ...category }, { ...rule })); + dispatch(markSectionRead({ ...rule, type: RULE_TYPE })); }); }; }; export const markRead = (selected, token) => { - if ('category' in selected) { - return markRuleRead(selected, token); - } else { - return markCategoryRead(selected, token); + switch ('category' in selected) { + case true: + return markRuleRead(selected, token); + default: + return markCategoryRead(selected, token); } }; diff --git a/src/newsreader/js/pages/homepage/components/PostModal.js b/src/newsreader/js/pages/homepage/components/PostModal.js index 9e40a89..524a217 100644 --- a/src/newsreader/js/pages/homepage/components/PostModal.js +++ b/src/newsreader/js/pages/homepage/components/PostModal.js @@ -14,7 +14,7 @@ class PostModal extends React.Component { const token = Cookies.get('csrftoken'); if (!post.read) { - this.readTimer = setTimeout(markPostRead, 30000, post, token); + this.readTimer = setTimeout(markPostRead, 3000, post, token); } } @@ -40,7 +40,7 @@ class PostModal extends React.Component { > Close{' '} - +
        @@ -52,7 +52,7 @@ class PostModal extends React.Component { target="_blank" rel="noopener noreferrer" > - +

    {publicationDate} diff --git a/src/newsreader/js/pages/homepage/components/feedlist/FeedList.js b/src/newsreader/js/pages/homepage/components/feedlist/FeedList.js index d7bc94c..36966c8 100644 --- a/src/newsreader/js/pages/homepage/components/feedlist/FeedList.js +++ b/src/newsreader/js/pages/homepage/components/feedlist/FeedList.js @@ -2,7 +2,7 @@ import React from 'react'; import { connect } from 'react-redux'; import { isEqual } from 'lodash'; -import { fetchPostsByRule, fetchPostsByCategory } from '../../actions/posts.js'; +import { fetchPostsBySection } from '../../actions/posts.js'; import { filterPosts } from './filters.js'; import LoadingIndicator from '../../../../components/LoadingIndicator.js'; @@ -33,11 +33,7 @@ class FeedList extends React.Component { } paginate() { - if ('category' in this.props.selected) { - return this.props.fetchPostsByRule(this.props.selected, this.props.next); - } else { - return this.props.fetchPostsByCategory(this.props.selected, this.props.next); - } + this.props.fetchPostsBySection(this.props.selected, this.props.next); } render() { @@ -90,10 +86,7 @@ const mapStateToProps = state => ({ }); const mapDispatchToProps = dispatch => ({ - fetchPostsByRule: (rule, page = false) => dispatch(fetchPostsByRule(rule, page)), - fetchPostsByCategory: (category, page = false) => { - dispatch(fetchPostsByCategory(category, page)); - }, + fetchPostsBySection: (rule, page = false) => dispatch(fetchPostsBySection(rule, page)), }); export default connect( diff --git a/src/newsreader/js/pages/homepage/components/feedlist/PostItem.js b/src/newsreader/js/pages/homepage/components/feedlist/PostItem.js index 3e8a8cb..e226e9b 100644 --- a/src/newsreader/js/pages/homepage/components/feedlist/PostItem.js +++ b/src/newsreader/js/pages/homepage/components/feedlist/PostItem.js @@ -31,7 +31,7 @@ class PostItem extends React.Component { target="_blank" rel="noopener noreferrer" > - + {publicationDate} diff --git a/src/newsreader/js/pages/homepage/components/feedlist/RuleItem.js b/src/newsreader/js/pages/homepage/components/feedlist/RuleItem.js index 08cb3aa..f7a8a9b 100644 --- a/src/newsreader/js/pages/homepage/components/feedlist/RuleItem.js +++ b/src/newsreader/js/pages/homepage/components/feedlist/RuleItem.js @@ -15,7 +15,6 @@ class RuleItem extends React.Component { return (

    {this.props.rule.name}

    - {/* TODO: Add empty posts message */}
      {postItems}
    ); diff --git a/src/newsreader/js/pages/homepage/components/feedlist/filters.js b/src/newsreader/js/pages/homepage/components/feedlist/filters.js index c392775..a3ee886 100644 --- a/src/newsreader/js/pages/homepage/components/feedlist/filters.js +++ b/src/newsreader/js/pages/homepage/components/feedlist/filters.js @@ -1,10 +1,12 @@ +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 && !post.read; + return post.rule === rule.id; }); return filteredPosts.length > 0 ? [{ rule, posts: filteredPosts }] : []; @@ -17,7 +19,7 @@ export const filterPostsByCategory = (category = {}, rules = [], posts = []) => const filteredData = filteredRules.map(rule => { const filteredPosts = posts.filter(post => { - return post.rule === rule.id && !post.read; + return post.rule === rule.id; }); return { @@ -32,12 +34,12 @@ export const filterPostsByCategory = (category = {}, rules = [], posts = []) => export const filterPosts = state => { const posts = Object.values({ ...state.posts.items }); - if (!isEmpty(state.selected.item) && !('category' in state.selected.item)) { - const rules = Object.values({ ...state.rules.items }); - - return filterPostsByCategory({ ...state.selected.item }, rules, posts); - } else if ('category' in state.selected.item) { - return filterPostsByRule({ ...state.selected.item }, posts); + 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 index 7644fd7..36279ba 100644 --- a/src/newsreader/js/pages/homepage/components/sidebar/CategoryItem.js +++ b/src/newsreader/js/pages/homepage/components/sidebar/CategoryItem.js @@ -2,8 +2,10 @@ 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 { fetchPostsByCategory } from '../../actions/posts.js'; +import { fetchPostsBySection } from '../../actions/posts.js'; +import { isSelected } from './functions.js'; import RuleItem from './RuleItem.js'; class CategoryItem extends React.Component { @@ -17,22 +19,19 @@ class CategoryItem extends React.Component { const category = this.props.category; this.props.selectCategory(category); - this.props.fetchPostsByCategory(category); - - if (category.unread === 0) { - this.props.fetchCategory(category); - } + this.props.fetchPostsBySection(category); + this.props.fetchCategory(category); } render() { const imageSrc = this.state.open ? '/static/chevron-down.svg' : '/static/chevron-right.svg'; - const selected = isEqual(this.props.category, this.props.selected); + 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 ; }); return ( @@ -58,7 +57,7 @@ class CategoryItem extends React.Component { const mapDispatchToProps = dispatch => ({ selectCategory: category => dispatch(selectCategory(category)), - fetchPostsByCategory: category => dispatch(fetchPostsByCategory(category)), + fetchPostsBySection: section => dispatch(fetchPostsBySection(section)), fetchCategory: category => dispatch(fetchCategory(category)), }); diff --git a/src/newsreader/js/pages/homepage/components/sidebar/RuleItem.js b/src/newsreader/js/pages/homepage/components/sidebar/RuleItem.js index 0a55aab..d93eb26 100644 --- a/src/newsreader/js/pages/homepage/components/sidebar/RuleItem.js +++ b/src/newsreader/js/pages/homepage/components/sidebar/RuleItem.js @@ -2,38 +2,33 @@ 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 { fetchPostsByRule } from '../../actions/posts.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.fetchPostsByRule(rule); - - if (rule.unread === 0) { - this.props.fetchRule(rule); - } + this.props.fetchPostsBySection(rule); + this.props.fetchRule(rule); } render() { - const selected = isEqual(this.props.selected, this.props.rule); + const selected = isSelected(this.props.rule, this.props.selected, RULE_TYPE); const className = `rules__item ${selected ? 'rules__item--selected' : ''}`; + const favicon = this.props.rule.favicon + ? this.props.rule.favicon + : '/static/picture.svg'; return (
  • this.handleSelect()}>
    - {this.props.rule.favicon && ( - - - - )} + + +
    {this.props.rule.name}
    @@ -46,7 +41,7 @@ class RuleItem extends React.Component { const mapDispatchToProps = dispatch => ({ selectRule: rule => dispatch(selectRule(rule)), - fetchPostsByRule: rule => dispatch(fetchPostsByRule(rule)), + fetchPostsBySection: section => dispatch(fetchPostsBySection(section)), fetchRule: rule => dispatch(fetchRule(rule)), }); diff --git a/src/newsreader/js/pages/homepage/components/sidebar/Sidebar.js b/src/newsreader/js/pages/homepage/components/sidebar/Sidebar.js index cd73dbe..1721f03 100644 --- a/src/newsreader/js/pages/homepage/components/sidebar/Sidebar.js +++ b/src/newsreader/js/pages/homepage/components/sidebar/Sidebar.js @@ -22,7 +22,6 @@ class Sidebar extends React.Component { category={category} rules={rules} selected={this.props.selected.item} - selectedRule={this.props.selected.item} /> ); }); 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/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/reducers/categories.js b/src/newsreader/js/pages/homepage/reducers/categories.js index f34930e..4fd391f 100644 --- a/src/newsreader/js/pages/homepage/reducers/categories.js +++ b/src/newsreader/js/pages/homepage/reducers/categories.js @@ -1,5 +1,7 @@ import { isEqual } from 'lodash'; +import { CATEGORY_TYPE, RULE_TYPE } from '../constants.js'; + import { RECEIVE_CATEGORY, RECEIVE_CATEGORIES, @@ -7,11 +9,36 @@ import { REQUEST_CATEGORIES, } from '../actions/categories.js'; -import { RECEIVE_RULE, RECEIVE_RULES } from '../actions/rules.js'; - import { MARK_POST_READ } from '../actions/posts.js'; import { MARK_SECTION_READ } from '../actions/selected.js'; +const markCategoryRead = (action, state) => { + const category = { ...state.items[action.section.id] }; + + return { + ...state, + items: { + ...state.items, + [category.id]: { ...category, unread: 0 }, + }, + }; +}; + +const markCategoryByRule = (action, state) => { + const category = { ...state.items[action.section.category] }; + + return { + ...state, + items: { + ...state.items, + [category.id]: { + ...category, + unread: category.unread - action.section.unread, + }, + }, + }; +}; + const defaultState = { items: {}, isFetching: false }; export const categories = (state = { ...defaultState }, action) => { @@ -21,7 +48,7 @@ export const categories = (state = { ...defaultState }, action) => { ...state, items: { ...state.items, - [action.category.id]: { ...action.category, rules: {} }, + [action.category.id]: { ...action.category }, }, isFetching: false, }; @@ -31,7 +58,6 @@ export const categories = (state = { ...defaultState }, action) => { Object.values({ ...action.categories }).forEach(category => { receivedCategories[category.id] = { ...category, - rules: {}, }; }); @@ -40,44 +66,6 @@ export const categories = (state = { ...defaultState }, action) => { items: { ...state.items, ...receivedCategories }, isFetching: false, }; - case RECEIVE_RULE: - const category = { ...state.items[action.rule.category] }; - - category['rules'][action.rule.id] = { ...action.rule }; - - return { - ...state, - items: { - ...state.items, - [category.id]: { ...category }, - }, - }; - case RECEIVE_RULES: - const relevantCategories = {}; - - Object.values({ ...action.rules }).forEach(rule => { - if (!(rule.category in relevantCategories)) { - const category = { ...state.items[rule.category] }; - - relevantCategories[rule.category] = { - ...category, - rules: { - ...category.rules, - [rule.id]: { ...rule }, - }, - }; - } else { - relevantCategories[rule.category]['rules'][rule.id] = { ...rule }; - } - }); - - return { - ...state, - items: { - ...state.items, - ...relevantCategories, - }, - }; case REQUEST_CATEGORIES: case REQUEST_CATEGORY: return { @@ -85,31 +73,24 @@ export const categories = (state = { ...defaultState }, action) => { isFetching: true, }; case MARK_POST_READ: - return { - ...state, - items: { ...state.items, [action.category.id]: { ...action.category } }, - }; - case MARK_SECTION_READ: - if (!isEqual(action.rule, {})) { - return { - ...state, - items: { - ...state.items, - [action.category.id]: { - ...action.category, - unread: action.category.unread - action.rule.unread, - }, - }, - }; - } + let category = { ...state.items[action.section.category] }; return { ...state, items: { ...state.items, - [action.category.id]: { ...action.category, unread: 0 }, + [category.id]: { ...category, unread: category.unread - 1 }, }, }; + case MARK_SECTION_READ: + switch (action.section.type) { + case CATEGORY_TYPE: + return markCategoryRead(action, state); + case RULE_TYPE: + return markCategoryByRule(action, state); + } + + return state; default: return state; } diff --git a/src/newsreader/js/pages/homepage/reducers/posts.js b/src/newsreader/js/pages/homepage/reducers/posts.js index 4f613ab..358b05b 100644 --- a/src/newsreader/js/pages/homepage/reducers/posts.js +++ b/src/newsreader/js/pages/homepage/reducers/posts.js @@ -1,4 +1,5 @@ import { isEqual } from 'lodash'; +import { CATEGORY_TYPE, RULE_TYPE } from '../constants.js'; import { SELECT_POST, @@ -39,14 +40,19 @@ export const posts = (state = { ...defaultState }, action) => { const updatedPosts = {}; let relatedPosts = []; - if (!isEqual(action.rule, {})) { - relatedPosts = Object.values({ ...state.items }).filter(post => { - return post.rule === action.rule.id; - }); - } else { - relatedPosts = Object.values({ ...state.items }).filter(post => { - return post.rule in { ...action.category.rules }; - }); + 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 => { diff --git a/src/newsreader/js/pages/homepage/reducers/rules.js b/src/newsreader/js/pages/homepage/reducers/rules.js index f23c98f..69cd703 100644 --- a/src/newsreader/js/pages/homepage/reducers/rules.js +++ b/src/newsreader/js/pages/homepage/reducers/rules.js @@ -1,5 +1,7 @@ import { isEqual } from 'lodash'; +import { CATEGORY_TYPE, RULE_TYPE } from '../constants.js'; + import { REQUEST_RULES, REQUEST_RULE, @@ -32,33 +34,47 @@ export const rules = (state = { ...defaultState }, action) => { isFetching: false, }; case MARK_POST_READ: + const rule = { ...state.items[action.section.id] }; + + return { + ...state, + items: { + ...state.items, + [rule.id]: { ...rule, unread: rule.unread - 1 }, + }, + }; case MARK_SECTION_READ: - if (!isEqual(action.rule, {})) { - return { - ...state, - items: { - ...state.items, - [action.rule.id]: { - ...action.rule, - unread: 0, + 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, + }, + }; } - const updatedRules = {}; - Object.values({ ...state.items }).forEach(rule => { - if (rule.category === action.category.id) { - updatedRules[rule.id] = { - ...rule, - unread: 0, - }; - } else { - updatedRules[rule.id] = { ...rule }; - } - }); - - return { ...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 index dea13db..0bef90c 100644 --- a/src/newsreader/js/pages/homepage/reducers/selected.js +++ b/src/newsreader/js/pages/homepage/reducers/selected.js @@ -17,9 +17,32 @@ 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: 0 }, + next: false, + lastReached: false, + }; + } + + return { + ...state, + item: { ...action.section, clicks: state.item.clicks + 1 }, + next: false, + lastReached: false, + }; + } + } + return { ...state, - item: action.item, + item: { ...action.section, clicks: 0 }, next: false, lastReached: false, }; @@ -53,16 +76,9 @@ export const selected = (state = { ...defaultState }, action) => { post: {}, }; case MARK_SECTION_READ: - if (!isEqual(action.rule, {})) { - return { - ...state, - item: { ...action.rule, unread: 0 }, - }; - } - return { ...state, - item: { ...action.category }, + item: { ...action.section, clicks: 0, unread: 0 }, }; default: return state; diff --git a/src/newsreader/news/core/templates/core/category.html b/src/newsreader/news/core/templates/core/category.html index afc5dee..1b0203c 100644 --- a/src/newsreader/news/core/templates/core/category.html +++ b/src/newsreader/news/core/templates/core/category.html @@ -34,7 +34,7 @@
  • {% block rule-input %}{% endblock %} + src="{% if rule.favicon %}{{ rule.favicon }}{% else %}/static/picture.svg{% endif %}" /> {{ rule.name }}
  • {% endfor %} diff --git a/src/newsreader/scss/components/body/_body.scss b/src/newsreader/scss/components/body/_body.scss index d5ede84..d4316d3 100644 --- a/src/newsreader/scss/components/body/_body.scss +++ b/src/newsreader/scss/components/body/_body.scss @@ -4,9 +4,11 @@ background-color: $gainsboro; font-family: $default-font; + color: $default-font-color; & * { margin: 0; padding: 0; + box-sizing: border-box; } } diff --git a/src/newsreader/scss/elements/a/_a.scss b/src/newsreader/scss/elements/a/_a.scss new file mode 100644 index 0000000..69113c5 --- /dev/null +++ b/src/newsreader/scss/elements/a/_a.scss @@ -0,0 +1,3 @@ +a { + @extend .link; +} diff --git a/src/newsreader/scss/elements/a/index.scss b/src/newsreader/scss/elements/a/index.scss new file mode 100644 index 0000000..1bf17d2 --- /dev/null +++ b/src/newsreader/scss/elements/a/index.scss @@ -0,0 +1 @@ +@import "a"; diff --git a/src/newsreader/scss/elements/button/_button.scss b/src/newsreader/scss/elements/button/_button.scss index 215029f..a6bec19 100644 --- a/src/newsreader/scss/elements/button/_button.scss +++ b/src/newsreader/scss/elements/button/_button.scss @@ -17,7 +17,7 @@ } &--success, &--confirm { - color: $white; + color: $white !important; background-color: $confirm-green; &:hover { @@ -26,7 +26,7 @@ } &--error, &--cancel { - color: $white; + color: $white !important; background-color: $error-red; &:hover { @@ -36,7 +36,7 @@ } &--primary { - color: $white; + color: $white !important; background-color: darken($azureish-white, +20%); &:hover { diff --git a/src/newsreader/scss/elements/index.scss b/src/newsreader/scss/elements/index.scss index 8cf91e9..cc587d8 100644 --- a/src/newsreader/scss/elements/index.scss +++ b/src/newsreader/scss/elements/index.scss @@ -1,5 +1,6 @@ @import "./button/index"; @import "link/index"; +@import "a/index"; @import "h1/index"; @import "h2/index"; @import "h3/index"; diff --git a/src/newsreader/scss/elements/link/_link.scss b/src/newsreader/scss/elements/link/_link.scss index 56d39c2..96f737f 100644 --- a/src/newsreader/scss/elements/link/_link.scss +++ b/src/newsreader/scss/elements/link/_link.scss @@ -1,4 +1,5 @@ .link { + color: darken($azureish-white, 30%); text-decoration: none; &:hover { diff --git a/src/newsreader/scss/pages/homepage/components/category/_category.scss b/src/newsreader/scss/pages/homepage/components/category/_category.scss index b272bb1..9d8451f 100644 --- a/src/newsreader/scss/pages/homepage/components/category/_category.scss +++ b/src/newsreader/scss/pages/homepage/components/category/_category.scss @@ -36,11 +36,10 @@ } &:hover { - background-color: darken($gainsboro, 10%); + background-color: darken($azureish-white, +10%); } &--selected { - color: $white; - background-color: darken($gainsboro, 10%); + background-color: darken($azureish-white, +10%); } } diff --git a/src/newsreader/scss/pages/homepage/components/post-block/_post-block.scss b/src/newsreader/scss/pages/homepage/components/post-block/_post-block.scss index e0d2dcd..ee30eb4 100644 --- a/src/newsreader/scss/pages/homepage/components/post-block/_post-block.scss +++ b/src/newsreader/scss/pages/homepage/components/post-block/_post-block.scss @@ -2,7 +2,7 @@ display: flex; flex-direction: column; - width: 60%; + width: 70%; margin: 0 0 2% 0; font-family: $article-font; diff --git a/src/newsreader/scss/pages/homepage/components/post/_post.scss b/src/newsreader/scss/pages/homepage/components/post/_post.scss index a0ddae4..51ac564 100644 --- a/src/newsreader/scss/pages/homepage/components/post/_post.scss +++ b/src/newsreader/scss/pages/homepage/components/post/_post.scss @@ -54,6 +54,7 @@ line-height: 1.5; font-family: $article-font; + font-size: 18px; & p { padding: 20px 0 0 0; @@ -80,7 +81,7 @@ margin: 0 0 0 5px; & img { - width: 10px; + width: 20px; } } @@ -99,7 +100,7 @@ width: 10%; font-family: $button-font; - color: $nickel; + color: lighten($default-font-color, +10%); & h5 { margin: 10px 0 0 0; @@ -119,7 +120,7 @@ } & h5:last-child { - background-color: $light-green; + background-color: $beige; } } } diff --git a/src/newsreader/scss/pages/homepage/components/posts-header/_posts-header.scss b/src/newsreader/scss/pages/homepage/components/posts-header/_posts-header.scss index 068c2b3..c0ea036 100644 --- a/src/newsreader/scss/pages/homepage/components/posts-header/_posts-header.scss +++ b/src/newsreader/scss/pages/homepage/components/posts-header/_posts-header.scss @@ -1,10 +1,12 @@ .posts-header { display: flex; + align-items: center; padding: 0 5px 0 0; width: 80%; &__link { + display: flex; padding: 0 0 0 5px; } @@ -12,10 +14,10 @@ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - font-size: small; + font-size: 16px; &--read { - color: $gainsboro; + color: darken($gainsboro, +10%); } } } diff --git a/src/newsreader/scss/pages/homepage/components/posts/_posts.scss b/src/newsreader/scss/pages/homepage/components/posts/_posts.scss index 0734e77..9329a85 100644 --- a/src/newsreader/scss/pages/homepage/components/posts/_posts.scss +++ b/src/newsreader/scss/pages/homepage/components/posts/_posts.scss @@ -7,20 +7,19 @@ &__item { display: flex; justify-content: space-between; + align-items: center; - padding: 10px 0 0px 0; + padding: 10px 0 10px 0; border-radius: 2px; border-bottom: 2px solid $azureish-white; &:hover { cursor: pointer; - background-color: lighten($gainsboro, 10%); + background-color: $gainsboro; } & span { - width: 20%; - font-size: small; text-overflow: ellipsis; overflow: hidden; diff --git a/src/newsreader/scss/pages/homepage/components/rule/_rule.scss b/src/newsreader/scss/pages/homepage/components/rule/_rule.scss index dba7124..ba34bf6 100644 --- a/src/newsreader/scss/pages/homepage/components/rule/_rule.scss +++ b/src/newsreader/scss/pages/homepage/components/rule/_rule.scss @@ -1,5 +1,6 @@ .rule { display: flex; + align-items: center; width: 80%; &__title { @@ -7,4 +8,8 @@ text-overflow: ellipsis; white-space: nowrap; } + + & span { + display: flex; + } } diff --git a/src/newsreader/scss/pages/homepage/components/rules/_rules.scss b/src/newsreader/scss/pages/homepage/components/rules/_rules.scss index c8b261e..ca12c9c 100644 --- a/src/newsreader/scss/pages/homepage/components/rules/_rules.scss +++ b/src/newsreader/scss/pages/homepage/components/rules/_rules.scss @@ -18,12 +18,11 @@ &:hover { cursor: pointer; - background-color: darken($gainsboro, 10%); + background-color: darken($azureish-white, +10%); } &--selected { - color: $white; - background-color: darken($gainsboro, 10%); + background-color: darken($azureish-white, +10%); } } } diff --git a/src/newsreader/scss/pages/homepage/elements/badge/_badge.scss b/src/newsreader/scss/pages/homepage/elements/badge/_badge.scss index 6abab65..1e2db24 100644 --- a/src/newsreader/scss/pages/homepage/elements/badge/_badge.scss +++ b/src/newsreader/scss/pages/homepage/elements/badge/_badge.scss @@ -3,12 +3,11 @@ padding-left: 8px; padding-right: 8px; - border-radius: 20%; + border-radius: 2px; text-align: center; - background-color: darken($pink, 10%); - color: $white; + background-color: lighten($pewter-blue, +20%); font-family: $button-font; font-size: small; diff --git a/src/newsreader/scss/partials/_colors.scss b/src/newsreader/scss/partials/_colors.scss index 641afff..0259883 100644 --- a/src/newsreader/scss/partials/_colors.scss +++ b/src/newsreader/scss/partials/_colors.scss @@ -10,7 +10,7 @@ $azureish-white: rgba(205, 230, 245, 1); $pewter-blue: rgba(141, 167, 190, 1); // light gray -$gainsboro: rgba(224, 221, 220, 1); +$gainsboro: rgba(238, 238, 238, 1); // medium gray $roman-silver: rgba(135, 145, 158, 1); @@ -35,3 +35,4 @@ $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/static/icons/alarm.svg b/src/newsreader/static/icons/alarm.svg new file mode 100755 index 0000000..94c3105 --- /dev/null +++ b/src/newsreader/static/icons/alarm.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/newsreader/static/icons/apartment.svg b/src/newsreader/static/icons/apartment.svg new file mode 100755 index 0000000..ff14506 --- /dev/null +++ b/src/newsreader/static/icons/apartment.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/newsreader/static/icons/arrow-down-circle.svg b/src/newsreader/static/icons/arrow-down-circle.svg new file mode 100755 index 0000000..c09ee4e --- /dev/null +++ b/src/newsreader/static/icons/arrow-down-circle.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/newsreader/static/icons/arrow-down.svg b/src/newsreader/static/icons/arrow-down.svg new file mode 100755 index 0000000..d802122 --- /dev/null +++ b/src/newsreader/static/icons/arrow-down.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/newsreader/static/icons/arrow-left-circle.svg b/src/newsreader/static/icons/arrow-left-circle.svg new file mode 100755 index 0000000..a135160 --- /dev/null +++ b/src/newsreader/static/icons/arrow-left-circle.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/newsreader/static/icons/arrow-left.svg b/src/newsreader/static/icons/arrow-left.svg index 88526e8..c1168f6 100644 --- a/src/newsreader/static/icons/arrow-left.svg +++ b/src/newsreader/static/icons/arrow-left.svg @@ -1,6 +1,6 @@ - - - - - - \ No newline at end of file + + + + + + diff --git a/src/newsreader/static/icons/arrow-right-circle.svg b/src/newsreader/static/icons/arrow-right-circle.svg new file mode 100755 index 0000000..9b8d2f5 --- /dev/null +++ b/src/newsreader/static/icons/arrow-right-circle.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/newsreader/static/icons/arrow-right.svg b/src/newsreader/static/icons/arrow-right.svg new file mode 100755 index 0000000..21c0a0c --- /dev/null +++ b/src/newsreader/static/icons/arrow-right.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/newsreader/static/icons/arrow-up-circle.svg b/src/newsreader/static/icons/arrow-up-circle.svg new file mode 100755 index 0000000..044fc48 --- /dev/null +++ b/src/newsreader/static/icons/arrow-up-circle.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/newsreader/static/icons/arrow-up.svg b/src/newsreader/static/icons/arrow-up.svg new file mode 100755 index 0000000..fa2d12e --- /dev/null +++ b/src/newsreader/static/icons/arrow-up.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/newsreader/static/icons/bicycle.svg b/src/newsreader/static/icons/bicycle.svg new file mode 100755 index 0000000..2599f0d --- /dev/null +++ b/src/newsreader/static/icons/bicycle.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/newsreader/static/icons/bold.svg b/src/newsreader/static/icons/bold.svg new file mode 100755 index 0000000..86271f6 --- /dev/null +++ b/src/newsreader/static/icons/bold.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/newsreader/static/icons/book.svg b/src/newsreader/static/icons/book.svg new file mode 100755 index 0000000..cc4892e --- /dev/null +++ b/src/newsreader/static/icons/book.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/newsreader/static/icons/bookmark.svg b/src/newsreader/static/icons/bookmark.svg new file mode 100755 index 0000000..6057646 --- /dev/null +++ b/src/newsreader/static/icons/bookmark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/newsreader/static/icons/briefcase.svg b/src/newsreader/static/icons/briefcase.svg new file mode 100755 index 0000000..58d54b6 --- /dev/null +++ b/src/newsreader/static/icons/briefcase.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/newsreader/static/icons/bubble.svg b/src/newsreader/static/icons/bubble.svg new file mode 100755 index 0000000..87317cc --- /dev/null +++ b/src/newsreader/static/icons/bubble.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/newsreader/static/icons/bug.svg b/src/newsreader/static/icons/bug.svg new file mode 100755 index 0000000..7cedf5a --- /dev/null +++ b/src/newsreader/static/icons/bug.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/newsreader/static/icons/bullhorn.svg b/src/newsreader/static/icons/bullhorn.svg new file mode 100755 index 0000000..bc8ffcc --- /dev/null +++ b/src/newsreader/static/icons/bullhorn.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/newsreader/static/icons/bus.svg b/src/newsreader/static/icons/bus.svg new file mode 100755 index 0000000..1a3416f --- /dev/null +++ b/src/newsreader/static/icons/bus.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/newsreader/static/icons/calendar-full.svg b/src/newsreader/static/icons/calendar-full.svg new file mode 100755 index 0000000..c835eaa --- /dev/null +++ b/src/newsreader/static/icons/calendar-full.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/newsreader/static/icons/camera-video.svg b/src/newsreader/static/icons/camera-video.svg new file mode 100755 index 0000000..99e6ebe --- /dev/null +++ b/src/newsreader/static/icons/camera-video.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/newsreader/static/icons/camera.svg b/src/newsreader/static/icons/camera.svg new file mode 100755 index 0000000..b1e662a --- /dev/null +++ b/src/newsreader/static/icons/camera.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/newsreader/static/icons/car.svg b/src/newsreader/static/icons/car.svg new file mode 100755 index 0000000..dd9af01 --- /dev/null +++ b/src/newsreader/static/icons/car.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/newsreader/static/icons/cart.svg b/src/newsreader/static/icons/cart.svg new file mode 100755 index 0000000..cf0df35 --- /dev/null +++ b/src/newsreader/static/icons/cart.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/newsreader/static/icons/chart-bars.svg b/src/newsreader/static/icons/chart-bars.svg new file mode 100755 index 0000000..15b0f54 --- /dev/null +++ b/src/newsreader/static/icons/chart-bars.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/newsreader/static/icons/checkmark-circle.svg b/src/newsreader/static/icons/checkmark-circle.svg new file mode 100755 index 0000000..e46b93f --- /dev/null +++ b/src/newsreader/static/icons/checkmark-circle.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/newsreader/static/icons/chevron-down-circle.svg b/src/newsreader/static/icons/chevron-down-circle.svg new file mode 100755 index 0000000..e530413 --- /dev/null +++ b/src/newsreader/static/icons/chevron-down-circle.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/newsreader/static/icons/chevron-down.svg b/src/newsreader/static/icons/chevron-down.svg index 211fd17..5342155 100644 --- a/src/newsreader/static/icons/chevron-down.svg +++ b/src/newsreader/static/icons/chevron-down.svg @@ -1,6 +1,6 @@ - - - - - - \ No newline at end of file + + + + + + diff --git a/src/newsreader/static/icons/chevron-left-circle.svg b/src/newsreader/static/icons/chevron-left-circle.svg new file mode 100755 index 0000000..495f43b --- /dev/null +++ b/src/newsreader/static/icons/chevron-left-circle.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/newsreader/static/icons/chevron-left.svg b/src/newsreader/static/icons/chevron-left.svg new file mode 100755 index 0000000..4117261 --- /dev/null +++ b/src/newsreader/static/icons/chevron-left.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/newsreader/static/icons/chevron-right-circle.svg b/src/newsreader/static/icons/chevron-right-circle.svg new file mode 100755 index 0000000..b5bd4d4 --- /dev/null +++ b/src/newsreader/static/icons/chevron-right-circle.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/newsreader/static/icons/chevron-right.svg b/src/newsreader/static/icons/chevron-right.svg index ea2b601..14db8e8 100644 --- a/src/newsreader/static/icons/chevron-right.svg +++ b/src/newsreader/static/icons/chevron-right.svg @@ -1,6 +1,6 @@ - - - - - - \ No newline at end of file + + + + + + diff --git a/src/newsreader/static/icons/chevron-up-circle.svg b/src/newsreader/static/icons/chevron-up-circle.svg new file mode 100755 index 0000000..9e8acdd --- /dev/null +++ b/src/newsreader/static/icons/chevron-up-circle.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/newsreader/static/icons/chevron-up.svg b/src/newsreader/static/icons/chevron-up.svg new file mode 100755 index 0000000..778bbaa --- /dev/null +++ b/src/newsreader/static/icons/chevron-up.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/newsreader/static/icons/circle-minus.svg b/src/newsreader/static/icons/circle-minus.svg new file mode 100755 index 0000000..1e4a8c9 --- /dev/null +++ b/src/newsreader/static/icons/circle-minus.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/newsreader/static/icons/clock.svg b/src/newsreader/static/icons/clock.svg new file mode 100755 index 0000000..55169fe --- /dev/null +++ b/src/newsreader/static/icons/clock.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/newsreader/static/icons/cloud-check.svg b/src/newsreader/static/icons/cloud-check.svg new file mode 100755 index 0000000..253b386 --- /dev/null +++ b/src/newsreader/static/icons/cloud-check.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/newsreader/static/icons/cloud-download.svg b/src/newsreader/static/icons/cloud-download.svg new file mode 100755 index 0000000..1cbe14c --- /dev/null +++ b/src/newsreader/static/icons/cloud-download.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/newsreader/static/icons/cloud-sync.svg b/src/newsreader/static/icons/cloud-sync.svg new file mode 100755 index 0000000..c239e0d --- /dev/null +++ b/src/newsreader/static/icons/cloud-sync.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/newsreader/static/icons/cloud-upload.svg b/src/newsreader/static/icons/cloud-upload.svg new file mode 100755 index 0000000..9fcbabe --- /dev/null +++ b/src/newsreader/static/icons/cloud-upload.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/newsreader/static/icons/cloud.svg b/src/newsreader/static/icons/cloud.svg new file mode 100755 index 0000000..24918b1 --- /dev/null +++ b/src/newsreader/static/icons/cloud.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/newsreader/static/icons/code.svg b/src/newsreader/static/icons/code.svg new file mode 100755 index 0000000..fbdfd39 --- /dev/null +++ b/src/newsreader/static/icons/code.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/newsreader/static/icons/coffee-cup.svg b/src/newsreader/static/icons/coffee-cup.svg new file mode 100755 index 0000000..478ee27 --- /dev/null +++ b/src/newsreader/static/icons/coffee-cup.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/newsreader/static/icons/cog.svg b/src/newsreader/static/icons/cog.svg new file mode 100755 index 0000000..4b4948a --- /dev/null +++ b/src/newsreader/static/icons/cog.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/newsreader/static/icons/construction.svg b/src/newsreader/static/icons/construction.svg new file mode 100755 index 0000000..72726ff --- /dev/null +++ b/src/newsreader/static/icons/construction.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/newsreader/static/icons/crop.svg b/src/newsreader/static/icons/crop.svg new file mode 100755 index 0000000..1568611 --- /dev/null +++ b/src/newsreader/static/icons/crop.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/newsreader/static/icons/cross-circle.svg b/src/newsreader/static/icons/cross-circle.svg new file mode 100755 index 0000000..dbc08af --- /dev/null +++ b/src/newsreader/static/icons/cross-circle.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/newsreader/static/icons/cross.svg b/src/newsreader/static/icons/cross.svg new file mode 100755 index 0000000..2edad61 --- /dev/null +++ b/src/newsreader/static/icons/cross.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/newsreader/static/icons/database.svg b/src/newsreader/static/icons/database.svg new file mode 100755 index 0000000..64236ad --- /dev/null +++ b/src/newsreader/static/icons/database.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/newsreader/static/icons/diamond.svg b/src/newsreader/static/icons/diamond.svg new file mode 100755 index 0000000..679df4a --- /dev/null +++ b/src/newsreader/static/icons/diamond.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/newsreader/static/icons/dice.svg b/src/newsreader/static/icons/dice.svg new file mode 100755 index 0000000..6859d8b --- /dev/null +++ b/src/newsreader/static/icons/dice.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/newsreader/static/icons/dinner.svg b/src/newsreader/static/icons/dinner.svg new file mode 100755 index 0000000..0cf54d6 --- /dev/null +++ b/src/newsreader/static/icons/dinner.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/newsreader/static/icons/direction-ltr.svg b/src/newsreader/static/icons/direction-ltr.svg new file mode 100755 index 0000000..827ada0 --- /dev/null +++ b/src/newsreader/static/icons/direction-ltr.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/newsreader/static/icons/direction-rtl.svg b/src/newsreader/static/icons/direction-rtl.svg new file mode 100755 index 0000000..47ce7d4 --- /dev/null +++ b/src/newsreader/static/icons/direction-rtl.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/newsreader/static/icons/download.svg b/src/newsreader/static/icons/download.svg new file mode 100755 index 0000000..51b561f --- /dev/null +++ b/src/newsreader/static/icons/download.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/newsreader/static/icons/drop.svg b/src/newsreader/static/icons/drop.svg new file mode 100755 index 0000000..d726f67 --- /dev/null +++ b/src/newsreader/static/icons/drop.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/newsreader/static/icons/earth.svg b/src/newsreader/static/icons/earth.svg new file mode 100755 index 0000000..411b22c --- /dev/null +++ b/src/newsreader/static/icons/earth.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/newsreader/static/icons/enter-down.svg b/src/newsreader/static/icons/enter-down.svg new file mode 100755 index 0000000..85794a1 --- /dev/null +++ b/src/newsreader/static/icons/enter-down.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/newsreader/static/icons/enter.svg b/src/newsreader/static/icons/enter.svg new file mode 100755 index 0000000..1130e28 --- /dev/null +++ b/src/newsreader/static/icons/enter.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/newsreader/static/icons/envelope.svg b/src/newsreader/static/icons/envelope.svg new file mode 100755 index 0000000..f030873 --- /dev/null +++ b/src/newsreader/static/icons/envelope.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/newsreader/static/icons/exit-up.svg b/src/newsreader/static/icons/exit-up.svg new file mode 100755 index 0000000..c67b28c --- /dev/null +++ b/src/newsreader/static/icons/exit-up.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/newsreader/static/icons/exit.svg b/src/newsreader/static/icons/exit.svg new file mode 100755 index 0000000..b54f4b5 --- /dev/null +++ b/src/newsreader/static/icons/exit.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/newsreader/static/icons/eye.svg b/src/newsreader/static/icons/eye.svg new file mode 100755 index 0000000..f67edba --- /dev/null +++ b/src/newsreader/static/icons/eye.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/newsreader/static/icons/file-add.svg b/src/newsreader/static/icons/file-add.svg new file mode 100755 index 0000000..5f0b06d --- /dev/null +++ b/src/newsreader/static/icons/file-add.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/newsreader/static/icons/file-empty.svg b/src/newsreader/static/icons/file-empty.svg new file mode 100755 index 0000000..c203b20 --- /dev/null +++ b/src/newsreader/static/icons/file-empty.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/newsreader/static/icons/film-play.svg b/src/newsreader/static/icons/film-play.svg new file mode 100755 index 0000000..d2a9db0 --- /dev/null +++ b/src/newsreader/static/icons/film-play.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/newsreader/static/icons/flag.svg b/src/newsreader/static/icons/flag.svg new file mode 100755 index 0000000..2f91d68 --- /dev/null +++ b/src/newsreader/static/icons/flag.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/newsreader/static/icons/frame-contract.svg b/src/newsreader/static/icons/frame-contract.svg new file mode 100755 index 0000000..3a70458 --- /dev/null +++ b/src/newsreader/static/icons/frame-contract.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/newsreader/static/icons/frame-expand.svg b/src/newsreader/static/icons/frame-expand.svg new file mode 100755 index 0000000..40f6af0 --- /dev/null +++ b/src/newsreader/static/icons/frame-expand.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/newsreader/static/icons/funnel.svg b/src/newsreader/static/icons/funnel.svg new file mode 100755 index 0000000..d2688f7 --- /dev/null +++ b/src/newsreader/static/icons/funnel.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/newsreader/static/icons/gift.svg b/src/newsreader/static/icons/gift.svg new file mode 100755 index 0000000..72c0bdc --- /dev/null +++ b/src/newsreader/static/icons/gift.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/newsreader/static/icons/graduation-hat.svg b/src/newsreader/static/icons/graduation-hat.svg new file mode 100755 index 0000000..abb6099 --- /dev/null +++ b/src/newsreader/static/icons/graduation-hat.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/newsreader/static/icons/hand.svg b/src/newsreader/static/icons/hand.svg new file mode 100755 index 0000000..1eacb25 --- /dev/null +++ b/src/newsreader/static/icons/hand.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/newsreader/static/icons/heart-pulse.svg b/src/newsreader/static/icons/heart-pulse.svg new file mode 100755 index 0000000..62e0c08 --- /dev/null +++ b/src/newsreader/static/icons/heart-pulse.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/newsreader/static/icons/heart.svg b/src/newsreader/static/icons/heart.svg new file mode 100755 index 0000000..660600b --- /dev/null +++ b/src/newsreader/static/icons/heart.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/newsreader/static/icons/highlight.svg b/src/newsreader/static/icons/highlight.svg new file mode 100755 index 0000000..eb706fa --- /dev/null +++ b/src/newsreader/static/icons/highlight.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/newsreader/static/icons/history.svg b/src/newsreader/static/icons/history.svg new file mode 100755 index 0000000..4acfb22 --- /dev/null +++ b/src/newsreader/static/icons/history.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/newsreader/static/icons/home.svg b/src/newsreader/static/icons/home.svg new file mode 100755 index 0000000..c259dc3 --- /dev/null +++ b/src/newsreader/static/icons/home.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/newsreader/static/icons/hourglass.svg b/src/newsreader/static/icons/hourglass.svg new file mode 100755 index 0000000..0e72fba --- /dev/null +++ b/src/newsreader/static/icons/hourglass.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/newsreader/static/icons/inbox.svg b/src/newsreader/static/icons/inbox.svg new file mode 100755 index 0000000..2e0a9f8 --- /dev/null +++ b/src/newsreader/static/icons/inbox.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/newsreader/static/icons/indent-decrease.svg b/src/newsreader/static/icons/indent-decrease.svg new file mode 100755 index 0000000..9443dcf --- /dev/null +++ b/src/newsreader/static/icons/indent-decrease.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/newsreader/static/icons/indent-increase.svg b/src/newsreader/static/icons/indent-increase.svg new file mode 100755 index 0000000..25666f4 --- /dev/null +++ b/src/newsreader/static/icons/indent-increase.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/newsreader/static/icons/italic.svg b/src/newsreader/static/icons/italic.svg new file mode 100755 index 0000000..6ddde14 --- /dev/null +++ b/src/newsreader/static/icons/italic.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/newsreader/static/icons/keyboard.svg b/src/newsreader/static/icons/keyboard.svg new file mode 100755 index 0000000..ae51d9c --- /dev/null +++ b/src/newsreader/static/icons/keyboard.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/newsreader/static/icons/laptop-phone.svg b/src/newsreader/static/icons/laptop-phone.svg new file mode 100755 index 0000000..e67b5b3 --- /dev/null +++ b/src/newsreader/static/icons/laptop-phone.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/newsreader/static/icons/laptop.svg b/src/newsreader/static/icons/laptop.svg new file mode 100755 index 0000000..51fdb07 --- /dev/null +++ b/src/newsreader/static/icons/laptop.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/newsreader/static/icons/layers.svg b/src/newsreader/static/icons/layers.svg new file mode 100755 index 0000000..8cec392 --- /dev/null +++ b/src/newsreader/static/icons/layers.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/newsreader/static/icons/leaf.svg b/src/newsreader/static/icons/leaf.svg new file mode 100755 index 0000000..428a264 --- /dev/null +++ b/src/newsreader/static/icons/leaf.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/newsreader/static/icons/license.svg b/src/newsreader/static/icons/license.svg new file mode 100755 index 0000000..cf95671 --- /dev/null +++ b/src/newsreader/static/icons/license.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/newsreader/static/icons/lighter.svg b/src/newsreader/static/icons/lighter.svg new file mode 100755 index 0000000..09c5e0c --- /dev/null +++ b/src/newsreader/static/icons/lighter.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/newsreader/static/icons/line-spacing.svg b/src/newsreader/static/icons/line-spacing.svg new file mode 100755 index 0000000..8cf8f24 --- /dev/null +++ b/src/newsreader/static/icons/line-spacing.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/newsreader/static/icons/linearicons.svg b/src/newsreader/static/icons/linearicons.svg new file mode 100755 index 0000000..bbf2a26 --- /dev/null +++ b/src/newsreader/static/icons/linearicons.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/newsreader/static/icons/link.svg b/src/newsreader/static/icons/link.svg index 57caa9f..7bb4a0e 100644 --- a/src/newsreader/static/icons/link.svg +++ b/src/newsreader/static/icons/link.svg @@ -1 +1,7 @@ - \ No newline at end of file + + + + + + + diff --git a/src/newsreader/static/icons/list.svg b/src/newsreader/static/icons/list.svg new file mode 100755 index 0000000..6255ad9 --- /dev/null +++ b/src/newsreader/static/icons/list.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/newsreader/static/icons/location.svg b/src/newsreader/static/icons/location.svg new file mode 100755 index 0000000..272c1d9 --- /dev/null +++ b/src/newsreader/static/icons/location.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/newsreader/static/icons/lock.svg b/src/newsreader/static/icons/lock.svg new file mode 100755 index 0000000..76259cf --- /dev/null +++ b/src/newsreader/static/icons/lock.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/newsreader/static/icons/magic-wand.svg b/src/newsreader/static/icons/magic-wand.svg new file mode 100755 index 0000000..55753d8 --- /dev/null +++ b/src/newsreader/static/icons/magic-wand.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/newsreader/static/icons/magnifier.svg b/src/newsreader/static/icons/magnifier.svg new file mode 100755 index 0000000..9c26539 --- /dev/null +++ b/src/newsreader/static/icons/magnifier.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/newsreader/static/icons/map-marker.svg b/src/newsreader/static/icons/map-marker.svg new file mode 100755 index 0000000..3a20637 --- /dev/null +++ b/src/newsreader/static/icons/map-marker.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/newsreader/static/icons/map.svg b/src/newsreader/static/icons/map.svg new file mode 100755 index 0000000..319e300 --- /dev/null +++ b/src/newsreader/static/icons/map.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/newsreader/static/icons/menu-circle.svg b/src/newsreader/static/icons/menu-circle.svg new file mode 100755 index 0000000..f3544eb --- /dev/null +++ b/src/newsreader/static/icons/menu-circle.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/newsreader/static/icons/menu.svg b/src/newsreader/static/icons/menu.svg new file mode 100755 index 0000000..e0952e0 --- /dev/null +++ b/src/newsreader/static/icons/menu.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/newsreader/static/icons/mic.svg b/src/newsreader/static/icons/mic.svg new file mode 100755 index 0000000..9c08d2d --- /dev/null +++ b/src/newsreader/static/icons/mic.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/newsreader/static/icons/moon.svg b/src/newsreader/static/icons/moon.svg new file mode 100755 index 0000000..3f150c6 --- /dev/null +++ b/src/newsreader/static/icons/moon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/newsreader/static/icons/move.svg b/src/newsreader/static/icons/move.svg new file mode 100755 index 0000000..86c223d --- /dev/null +++ b/src/newsreader/static/icons/move.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/newsreader/static/icons/music-note.svg b/src/newsreader/static/icons/music-note.svg new file mode 100755 index 0000000..a4ab001 --- /dev/null +++ b/src/newsreader/static/icons/music-note.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/newsreader/static/icons/mustache.svg b/src/newsreader/static/icons/mustache.svg new file mode 100755 index 0000000..8c12f70 --- /dev/null +++ b/src/newsreader/static/icons/mustache.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/newsreader/static/icons/neutral.svg b/src/newsreader/static/icons/neutral.svg new file mode 100755 index 0000000..4f55a69 --- /dev/null +++ b/src/newsreader/static/icons/neutral.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/newsreader/static/icons/page-break.svg b/src/newsreader/static/icons/page-break.svg new file mode 100755 index 0000000..493c248 --- /dev/null +++ b/src/newsreader/static/icons/page-break.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/newsreader/static/icons/paperclip.svg b/src/newsreader/static/icons/paperclip.svg new file mode 100755 index 0000000..2afa342 --- /dev/null +++ b/src/newsreader/static/icons/paperclip.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/newsreader/static/icons/paw.svg b/src/newsreader/static/icons/paw.svg new file mode 100755 index 0000000..4170890 --- /dev/null +++ b/src/newsreader/static/icons/paw.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/newsreader/static/icons/pencil.svg b/src/newsreader/static/icons/pencil.svg new file mode 100755 index 0000000..fb618b1 --- /dev/null +++ b/src/newsreader/static/icons/pencil.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/newsreader/static/icons/phone-handset.svg b/src/newsreader/static/icons/phone-handset.svg new file mode 100755 index 0000000..cdadbe5 --- /dev/null +++ b/src/newsreader/static/icons/phone-handset.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/newsreader/static/icons/phone.svg b/src/newsreader/static/icons/phone.svg new file mode 100755 index 0000000..2883bc8 --- /dev/null +++ b/src/newsreader/static/icons/phone.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/newsreader/static/icons/picture.svg b/src/newsreader/static/icons/picture.svg new file mode 100755 index 0000000..c927a37 --- /dev/null +++ b/src/newsreader/static/icons/picture.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/newsreader/static/icons/pie-chart.svg b/src/newsreader/static/icons/pie-chart.svg new file mode 100755 index 0000000..a02c1a5 --- /dev/null +++ b/src/newsreader/static/icons/pie-chart.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/newsreader/static/icons/pilcrow.svg b/src/newsreader/static/icons/pilcrow.svg new file mode 100755 index 0000000..1a61c76 --- /dev/null +++ b/src/newsreader/static/icons/pilcrow.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/newsreader/static/icons/plus-circle.svg b/src/newsreader/static/icons/plus-circle.svg new file mode 100755 index 0000000..a0cf6fa --- /dev/null +++ b/src/newsreader/static/icons/plus-circle.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/newsreader/static/icons/pointer-down.svg b/src/newsreader/static/icons/pointer-down.svg new file mode 100755 index 0000000..e2321c6 --- /dev/null +++ b/src/newsreader/static/icons/pointer-down.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/newsreader/static/icons/pointer-left.svg b/src/newsreader/static/icons/pointer-left.svg new file mode 100755 index 0000000..e3aa356 --- /dev/null +++ b/src/newsreader/static/icons/pointer-left.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/newsreader/static/icons/pointer-right.svg b/src/newsreader/static/icons/pointer-right.svg new file mode 100755 index 0000000..011ffba --- /dev/null +++ b/src/newsreader/static/icons/pointer-right.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/newsreader/static/icons/pointer-up.svg b/src/newsreader/static/icons/pointer-up.svg new file mode 100755 index 0000000..08f82a1 --- /dev/null +++ b/src/newsreader/static/icons/pointer-up.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/newsreader/static/icons/poop.svg b/src/newsreader/static/icons/poop.svg new file mode 100755 index 0000000..570fade --- /dev/null +++ b/src/newsreader/static/icons/poop.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/newsreader/static/icons/power-switch.svg b/src/newsreader/static/icons/power-switch.svg new file mode 100755 index 0000000..9f47372 --- /dev/null +++ b/src/newsreader/static/icons/power-switch.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/newsreader/static/icons/printer.svg b/src/newsreader/static/icons/printer.svg new file mode 100755 index 0000000..d338626 --- /dev/null +++ b/src/newsreader/static/icons/printer.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/newsreader/static/icons/pushpin.svg b/src/newsreader/static/icons/pushpin.svg new file mode 100755 index 0000000..d88009d --- /dev/null +++ b/src/newsreader/static/icons/pushpin.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/newsreader/static/icons/question-circle.svg b/src/newsreader/static/icons/question-circle.svg new file mode 100755 index 0000000..45e5929 --- /dev/null +++ b/src/newsreader/static/icons/question-circle.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/newsreader/static/icons/redo.svg b/src/newsreader/static/icons/redo.svg new file mode 100755 index 0000000..ec68693 --- /dev/null +++ b/src/newsreader/static/icons/redo.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/newsreader/static/icons/rocket.svg b/src/newsreader/static/icons/rocket.svg new file mode 100755 index 0000000..552cbcc --- /dev/null +++ b/src/newsreader/static/icons/rocket.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/newsreader/static/icons/sad.svg b/src/newsreader/static/icons/sad.svg new file mode 100755 index 0000000..ed63b85 --- /dev/null +++ b/src/newsreader/static/icons/sad.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/newsreader/static/icons/screen.svg b/src/newsreader/static/icons/screen.svg new file mode 100755 index 0000000..057f0d9 --- /dev/null +++ b/src/newsreader/static/icons/screen.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/newsreader/static/icons/select.svg b/src/newsreader/static/icons/select.svg new file mode 100755 index 0000000..3e8cfee --- /dev/null +++ b/src/newsreader/static/icons/select.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/newsreader/static/icons/shirt.svg b/src/newsreader/static/icons/shirt.svg new file mode 100755 index 0000000..f6dd52d --- /dev/null +++ b/src/newsreader/static/icons/shirt.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/newsreader/static/icons/smartphone.svg b/src/newsreader/static/icons/smartphone.svg new file mode 100755 index 0000000..779f7d2 --- /dev/null +++ b/src/newsreader/static/icons/smartphone.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/newsreader/static/icons/smile.svg b/src/newsreader/static/icons/smile.svg new file mode 100755 index 0000000..a1a0a0a --- /dev/null +++ b/src/newsreader/static/icons/smile.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/newsreader/static/icons/sort-alpha-asc.svg b/src/newsreader/static/icons/sort-alpha-asc.svg new file mode 100755 index 0000000..56b8d3f --- /dev/null +++ b/src/newsreader/static/icons/sort-alpha-asc.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/newsreader/static/icons/sort-amount-asc.svg b/src/newsreader/static/icons/sort-amount-asc.svg new file mode 100755 index 0000000..d24c0e4 --- /dev/null +++ b/src/newsreader/static/icons/sort-amount-asc.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/newsreader/static/icons/spell-check.svg b/src/newsreader/static/icons/spell-check.svg new file mode 100755 index 0000000..1c4875c --- /dev/null +++ b/src/newsreader/static/icons/spell-check.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/newsreader/static/icons/star-empty.svg b/src/newsreader/static/icons/star-empty.svg new file mode 100755 index 0000000..fd5098c --- /dev/null +++ b/src/newsreader/static/icons/star-empty.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/newsreader/static/icons/star-half.svg b/src/newsreader/static/icons/star-half.svg new file mode 100755 index 0000000..c48aa79 --- /dev/null +++ b/src/newsreader/static/icons/star-half.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/newsreader/static/icons/star.svg b/src/newsreader/static/icons/star.svg new file mode 100755 index 0000000..3302123 --- /dev/null +++ b/src/newsreader/static/icons/star.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/newsreader/static/icons/store.svg b/src/newsreader/static/icons/store.svg new file mode 100755 index 0000000..9fce882 --- /dev/null +++ b/src/newsreader/static/icons/store.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/newsreader/static/icons/strikethrough.svg b/src/newsreader/static/icons/strikethrough.svg new file mode 100755 index 0000000..825d1d0 --- /dev/null +++ b/src/newsreader/static/icons/strikethrough.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/newsreader/static/icons/sun.svg b/src/newsreader/static/icons/sun.svg new file mode 100755 index 0000000..b9d9038 --- /dev/null +++ b/src/newsreader/static/icons/sun.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/newsreader/static/icons/sync.svg b/src/newsreader/static/icons/sync.svg new file mode 100755 index 0000000..982223f --- /dev/null +++ b/src/newsreader/static/icons/sync.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/newsreader/static/icons/tablet.svg b/src/newsreader/static/icons/tablet.svg new file mode 100755 index 0000000..8554d69 --- /dev/null +++ b/src/newsreader/static/icons/tablet.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/newsreader/static/icons/tag.svg b/src/newsreader/static/icons/tag.svg new file mode 100755 index 0000000..f2a207b --- /dev/null +++ b/src/newsreader/static/icons/tag.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/newsreader/static/icons/text-align-center.svg b/src/newsreader/static/icons/text-align-center.svg new file mode 100755 index 0000000..4ca60a9 --- /dev/null +++ b/src/newsreader/static/icons/text-align-center.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/newsreader/static/icons/text-align-justify.svg b/src/newsreader/static/icons/text-align-justify.svg new file mode 100755 index 0000000..814e51e --- /dev/null +++ b/src/newsreader/static/icons/text-align-justify.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/newsreader/static/icons/text-align-left.svg b/src/newsreader/static/icons/text-align-left.svg new file mode 100755 index 0000000..ee71585 --- /dev/null +++ b/src/newsreader/static/icons/text-align-left.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/newsreader/static/icons/text-align-right.svg b/src/newsreader/static/icons/text-align-right.svg new file mode 100755 index 0000000..4884054 --- /dev/null +++ b/src/newsreader/static/icons/text-align-right.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/newsreader/static/icons/text-format-remove.svg b/src/newsreader/static/icons/text-format-remove.svg new file mode 100755 index 0000000..f472c8c --- /dev/null +++ b/src/newsreader/static/icons/text-format-remove.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/newsreader/static/icons/text-format.svg b/src/newsreader/static/icons/text-format.svg new file mode 100755 index 0000000..5fe551c --- /dev/null +++ b/src/newsreader/static/icons/text-format.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/newsreader/static/icons/text-size.svg b/src/newsreader/static/icons/text-size.svg new file mode 100755 index 0000000..aef49f1 --- /dev/null +++ b/src/newsreader/static/icons/text-size.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/newsreader/static/icons/thumbs-down.svg b/src/newsreader/static/icons/thumbs-down.svg new file mode 100755 index 0000000..efe684e --- /dev/null +++ b/src/newsreader/static/icons/thumbs-down.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/newsreader/static/icons/thumbs-up.svg b/src/newsreader/static/icons/thumbs-up.svg new file mode 100755 index 0000000..bcfaec2 --- /dev/null +++ b/src/newsreader/static/icons/thumbs-up.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/newsreader/static/icons/train.svg b/src/newsreader/static/icons/train.svg new file mode 100755 index 0000000..efdeef9 --- /dev/null +++ b/src/newsreader/static/icons/train.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/newsreader/static/icons/trash.svg b/src/newsreader/static/icons/trash.svg new file mode 100755 index 0000000..4b35080 --- /dev/null +++ b/src/newsreader/static/icons/trash.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/newsreader/static/icons/underline.svg b/src/newsreader/static/icons/underline.svg new file mode 100755 index 0000000..0d440e5 --- /dev/null +++ b/src/newsreader/static/icons/underline.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/newsreader/static/icons/undo.svg b/src/newsreader/static/icons/undo.svg new file mode 100755 index 0000000..ba812e0 --- /dev/null +++ b/src/newsreader/static/icons/undo.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/newsreader/static/icons/unlink.svg b/src/newsreader/static/icons/unlink.svg new file mode 100755 index 0000000..2c176da --- /dev/null +++ b/src/newsreader/static/icons/unlink.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/newsreader/static/icons/upload.svg b/src/newsreader/static/icons/upload.svg new file mode 100755 index 0000000..12ac621 --- /dev/null +++ b/src/newsreader/static/icons/upload.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/newsreader/static/icons/user.svg b/src/newsreader/static/icons/user.svg new file mode 100755 index 0000000..5931e5f --- /dev/null +++ b/src/newsreader/static/icons/user.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/newsreader/static/icons/users.svg b/src/newsreader/static/icons/users.svg new file mode 100755 index 0000000..743ef13 --- /dev/null +++ b/src/newsreader/static/icons/users.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/newsreader/static/icons/volume-high.svg b/src/newsreader/static/icons/volume-high.svg new file mode 100755 index 0000000..7e891c2 --- /dev/null +++ b/src/newsreader/static/icons/volume-high.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/newsreader/static/icons/volume-low.svg b/src/newsreader/static/icons/volume-low.svg new file mode 100755 index 0000000..4872d97 --- /dev/null +++ b/src/newsreader/static/icons/volume-low.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/newsreader/static/icons/volume-medium.svg b/src/newsreader/static/icons/volume-medium.svg new file mode 100755 index 0000000..71b2fca --- /dev/null +++ b/src/newsreader/static/icons/volume-medium.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/newsreader/static/icons/volume.svg b/src/newsreader/static/icons/volume.svg new file mode 100755 index 0000000..edb1f6e --- /dev/null +++ b/src/newsreader/static/icons/volume.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/newsreader/static/icons/warning.svg b/src/newsreader/static/icons/warning.svg new file mode 100755 index 0000000..21f9e68 --- /dev/null +++ b/src/newsreader/static/icons/warning.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/newsreader/static/icons/wheelchair.svg b/src/newsreader/static/icons/wheelchair.svg new file mode 100755 index 0000000..25cdac0 --- /dev/null +++ b/src/newsreader/static/icons/wheelchair.svg @@ -0,0 +1,7 @@ + + + + + + + From b2829716b0d5add3112bf6ace2aa5a8f963de747 Mon Sep 17 00:00:00 2001 From: sonny Date: Wed, 27 Nov 2019 22:10:02 +0100 Subject: [PATCH 027/364] Account management --- gulp/sass.js | 3 + requirements/base.txt | 1 + .../accounts/templates/accounts/login.html | 8 +- src/newsreader/accounts/tests/factories.py | 20 +++ .../accounts/tests/test_activation.py | 102 +++++++++++ .../accounts/tests/test_password_reset.py | 164 ++++++++++++++++++ .../accounts/tests/test_registration.py | 110 ++++++++++++ .../accounts/tests/test_resend_activation.py | 80 +++++++++ src/newsreader/accounts/urls.py | 47 ++++- src/newsreader/accounts/views.py | 85 ++++++++- src/newsreader/conf/base.py | 5 + .../core/templates/core/category-create.html | 2 +- .../core/templates/core/category-update.html | 2 +- .../scss/components/body/_body.scss | 5 + .../scss/components/fieldset/_fieldset.scss | 11 ++ .../scss/components/fieldset/index.scss | 1 + .../scss/components/form/_form.scss | 14 +- src/newsreader/scss/components/index.scss | 1 + .../scss/components/list/_list.scss | 4 + src/newsreader/scss/elements/a/_a.scss | 3 - src/newsreader/scss/elements/a/index.scss | 1 - .../scss/elements/help-text/_help-text.scss | 6 +- src/newsreader/scss/elements/index.scss | 3 +- .../scss/elements/input/_input.scss | 5 + .../scss/elements/label/_label.scss | 4 + src/newsreader/scss/elements/link/_link.scss | 4 + .../scss/elements/small/_small.scss | 5 + .../scss/pages/activate/components/index.scss | 0 .../scss/pages/activate/elements/index.scss | 0 src/newsreader/scss/pages/activate/index.scss | 8 + .../password-reset/components/index.scss | 2 + .../_password-reset-confirm-form.scss | 3 + .../password-reset-confirm-form/index.scss | 1 + .../_password-reset-form.scss | 18 ++ .../components/password-reset-form/index.scss | 1 + .../pages/password-reset/elements/index.scss | 0 .../scss/pages/password-reset/index.scss | 8 + .../activation-form/_activation-form.scss | 11 ++ .../components/activation-form/index.scss | 1 + .../scss/pages/register/components/index.scss | 2 + .../register-form/_register-form.scss | 11 ++ .../components/register-form/index.scss | 1 + .../scss/pages/register/elements/index.scss | 0 src/newsreader/scss/pages/register/index.scss | 8 + src/newsreader/templates/base.html | 1 + .../password_reset_complete.html | 28 +++ .../password_reset_confirm.html | 59 +++++++ .../password-reset/password_reset_done.html | 27 +++ .../password-reset/password_reset_email.html | 30 ++++ .../password-reset/password_reset_form.html | 34 ++++ .../password-reset/password_reset_subject.txt | 6 + .../registration/activation_complete.html | 35 ++++ .../registration/activation_email.html | 72 ++++++++ .../registration/activation_email.txt | 52 ++++++ .../registration/activation_email_subject.txt | 28 +++ .../registration/activation_failure.html | 31 ++++ .../activation_resend_complete.html | 35 ++++ .../registration/activation_resend_form.html | 39 +++++ .../registration/registration_closed.html | 25 +++ .../registration/registration_complete.html | 33 ++++ .../registration/registration_form.html | 26 +++ 61 files changed, 1311 insertions(+), 21 deletions(-) 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/scss/components/fieldset/_fieldset.scss create mode 100644 src/newsreader/scss/components/fieldset/index.scss delete mode 100644 src/newsreader/scss/elements/a/_a.scss delete mode 100644 src/newsreader/scss/elements/a/index.scss create mode 100644 src/newsreader/scss/pages/activate/components/index.scss create mode 100644 src/newsreader/scss/pages/activate/elements/index.scss create mode 100644 src/newsreader/scss/pages/activate/index.scss create mode 100644 src/newsreader/scss/pages/password-reset/components/index.scss create mode 100644 src/newsreader/scss/pages/password-reset/components/password-reset-confirm-form/_password-reset-confirm-form.scss create mode 100644 src/newsreader/scss/pages/password-reset/components/password-reset-confirm-form/index.scss create mode 100644 src/newsreader/scss/pages/password-reset/components/password-reset-form/_password-reset-form.scss create mode 100644 src/newsreader/scss/pages/password-reset/components/password-reset-form/index.scss create mode 100644 src/newsreader/scss/pages/password-reset/elements/index.scss create mode 100644 src/newsreader/scss/pages/password-reset/index.scss create mode 100644 src/newsreader/scss/pages/register/components/activation-form/_activation-form.scss create mode 100644 src/newsreader/scss/pages/register/components/activation-form/index.scss create mode 100644 src/newsreader/scss/pages/register/components/index.scss create mode 100644 src/newsreader/scss/pages/register/components/register-form/_register-form.scss create mode 100644 src/newsreader/scss/pages/register/components/register-form/index.scss create mode 100644 src/newsreader/scss/pages/register/elements/index.scss create mode 100644 src/newsreader/scss/pages/register/index.scss 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 diff --git a/gulp/sass.js b/gulp/sass.js index abe9c53..2a71455 100644 --- a/gulp/sass.js +++ b/gulp/sass.js @@ -13,6 +13,9 @@ export const CORE_DIR = path.join(PROJECT_DIR, 'news', 'core', 'static', 'core') const taskMappings = [ { name: 'login', destDir: `${ACCOUNTS_DIR}/${STATIC_SUFFIX}` }, + { name: 'register', destDir: `${ACCOUNTS_DIR}/${STATIC_SUFFIX}` }, + { name: 'activate', destDir: `${ACCOUNTS_DIR}/${STATIC_SUFFIX}` }, + { name: 'password-reset', destDir: `${ACCOUNTS_DIR}/${STATIC_SUFFIX}` }, { name: 'homepage', destDir: `${CORE_DIR}/${STATIC_SUFFIX}` }, { name: 'categories', destDir: `${CORE_DIR}/${STATIC_SUFFIX}` }, { name: 'category', destDir: `${CORE_DIR}/${STATIC_SUFFIX}` }, diff --git a/requirements/base.txt b/requirements/base.txt index b3bcaf7..b5a6858 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -7,6 +7,7 @@ Django==2.2 django-celery-beat==1.5.0 djangorestframework==3.9.4 django-rest-swagger==2.2.0 +django-registration-redux==2.6 lxml==4.3.4 feedparser==5.2.1 idna==2.8 diff --git a/src/newsreader/accounts/templates/accounts/login.html b/src/newsreader/accounts/templates/accounts/login.html index f923643..f98a216 100644 --- a/src/newsreader/accounts/templates/accounts/login.html +++ b/src/newsreader/accounts/templates/accounts/login.html @@ -10,14 +10,18 @@
    diff --git a/src/newsreader/accounts/tests/factories.py b/src/newsreader/accounts/tests/factories.py index d073c1c..fc13d74 100644 --- a/src/newsreader/accounts/tests/factories.py +++ b/src/newsreader/accounts/tests/factories.py @@ -1,8 +1,20 @@ +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") @@ -17,3 +29,11 @@ class UserFactory(factory.django.DjangoModelFactory): 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..b5a9943 --- /dev/null +++ b/src/newsreader/accounts/tests/test_activation.py @@ -0,0 +1,102 @@ +import datetime + +from django.conf import settings +from django.core import mail +from django.test import 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 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..1f818c8 --- /dev/null +++ b/src/newsreader/accounts/tests/test_password_reset.py @@ -0,0 +1,164 @@ +from typing import Dict + +from django.contrib.auth.tokens import default_token_generator as token_generator +from django.core import mail +from django.test import TestCase +from django.urls import reverse +from django.utils.encoding import force_bytes +from django.utils.http import urlsafe_base64_encode +from django.utils.translation import gettext as _ + +from newsreader.accounts.models import User +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..a18df2a --- /dev/null +++ b/src/newsreader/accounts/tests/test_resend_activation.py @@ -0,0 +1,80 @@ +from django.conf import settings +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 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/urls.py b/src/newsreader/accounts/urls.py index 61593ed..ac8b2ab 100644 --- a/src/newsreader/accounts/urls.py +++ b/src/newsreader/accounts/urls.py @@ -1,9 +1,54 @@ from django.urls import include, path -from newsreader.accounts.views import LoginView, LogoutView +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 index 4957350..28ae92d 100644 --- a/src/newsreader/accounts/views.py +++ b/src/newsreader/accounts/views.py @@ -1,14 +1,91 @@ -from django.contrib.auth.views import LoginView as DjangoLoginView -from django.contrib.auth.views import LogoutView as DjangoLogoutView +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(DjangoLoginView): +class LoginView(django_views.LoginView): template_name = "accounts/login.html" def get_success_url(self): return reverse_lazy("index") -class LogoutView(DjangoLogoutView): +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/conf/base.py b/src/newsreader/conf/base.py index 46bc34a..6f19f91 100644 --- a/src/newsreader/conf/base.py +++ b/src/newsreader/conf/base.py @@ -41,6 +41,7 @@ INSTALLED_APPS = [ "rest_framework_swagger", "celery", "django_celery_beat", + "registration", # app modules "newsreader.accounts", "newsreader.news.core", @@ -137,3 +138,7 @@ SWAGGER_SETTINGS = { "LOGOUT_URL": "rest_framework:logout", "DOC_EXPANSION": "list", } + +REGISTRATION_OPEN = True +ACCOUNT_ACTIVATION_DAYS = 7 +REGISTRATION_AUTO_LOGIN = True diff --git a/src/newsreader/news/core/templates/core/category-create.html b/src/newsreader/news/core/templates/core/category-create.html index 49801bc..50c3a0a 100644 --- a/src/newsreader/news/core/templates/core/category-create.html +++ b/src/newsreader/news/core/templates/core/category-create.html @@ -1,7 +1,7 @@ {% extends "core/category.html" %} {% block form-header %} -

    Create a category

    +

    Create a category

    {% endblock %} {% block name-input %} diff --git a/src/newsreader/news/core/templates/core/category-update.html b/src/newsreader/news/core/templates/core/category-update.html index 524d81c..25c4d66 100644 --- a/src/newsreader/news/core/templates/core/category-update.html +++ b/src/newsreader/news/core/templates/core/category-update.html @@ -1,7 +1,7 @@ {% extends "core/category.html" %} {% block form-header %} -

    Update category

    +

    Update category

    {% endblock %} {% block name-input %} diff --git a/src/newsreader/scss/components/body/_body.scss b/src/newsreader/scss/components/body/_body.scss index d4316d3..306ad7c 100644 --- a/src/newsreader/scss/components/body/_body.scss +++ b/src/newsreader/scss/components/body/_body.scss @@ -5,10 +5,15 @@ 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/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/_form.scss b/src/newsreader/scss/components/form/_form.scss index d806a7f..8c51866 100644 --- a/src/newsreader/scss/components/form/_form.scss +++ b/src/newsreader/scss/components/form/_form.scss @@ -8,11 +8,7 @@ background-color: $white; &__fieldset { - display: flex; - flex-direction: column; - - padding: 15px; - border: none; + @extend .fieldset; } &__header { @@ -22,7 +18,15 @@ padding: 15px; } + &__title { + font-size: 18px; + } + & .favicon { height: 30px; } } + +form { + @extend .form; +} diff --git a/src/newsreader/scss/components/index.scss b/src/newsreader/scss/components/index.scss index f52df7a..6a18d4a 100644 --- a/src/newsreader/scss/components/index.scss +++ b/src/newsreader/scss/components/index.scss @@ -10,3 +10,4 @@ @import "./messages/index"; @import "./section/index"; @import "./errorlist/index"; +@import "./fieldset/index"; diff --git a/src/newsreader/scss/components/list/_list.scss b/src/newsreader/scss/components/list/_list.scss index 0e4666c..75e5e94 100644 --- a/src/newsreader/scss/components/list/_list.scss +++ b/src/newsreader/scss/components/list/_list.scss @@ -14,3 +14,7 @@ } } } + +ul { + @extend .list; +} diff --git a/src/newsreader/scss/elements/a/_a.scss b/src/newsreader/scss/elements/a/_a.scss deleted file mode 100644 index 69113c5..0000000 --- a/src/newsreader/scss/elements/a/_a.scss +++ /dev/null @@ -1,3 +0,0 @@ -a { - @extend .link; -} diff --git a/src/newsreader/scss/elements/a/index.scss b/src/newsreader/scss/elements/a/index.scss deleted file mode 100644 index 1bf17d2..0000000 --- a/src/newsreader/scss/elements/a/index.scss +++ /dev/null @@ -1 +0,0 @@ -@import "a"; diff --git a/src/newsreader/scss/elements/help-text/_help-text.scss b/src/newsreader/scss/elements/help-text/_help-text.scss index 6c5fc9b..a90552d 100644 --- a/src/newsreader/scss/elements/help-text/_help-text.scss +++ b/src/newsreader/scss/elements/help-text/_help-text.scss @@ -1,5 +1,9 @@ .help-text { - @extends .small; + @extend .small; padding: 5px 15px; } + +.helptext { + @extend .help-text; +} diff --git a/src/newsreader/scss/elements/index.scss b/src/newsreader/scss/elements/index.scss index cc587d8..46a8bbd 100644 --- a/src/newsreader/scss/elements/index.scss +++ b/src/newsreader/scss/elements/index.scss @@ -1,6 +1,5 @@ -@import "./button/index"; +@import "button/index"; @import "link/index"; -@import "a/index"; @import "h1/index"; @import "h2/index"; @import "h3/index"; diff --git a/src/newsreader/scss/elements/input/_input.scss b/src/newsreader/scss/elements/input/_input.scss index 8cf2340..1cfb4bb 100644 --- a/src/newsreader/scss/elements/input/_input.scss +++ b/src/newsreader/scss/elements/input/_input.scss @@ -1,6 +1,7 @@ .input { padding: 10px; + background-color: lighten($gainsboro, +4%); border: 1px $border-gray solid; border-radius: 2px; @@ -8,3 +9,7 @@ border: 1px $focus-blue solid; } } + +input { + @extend .input; +} diff --git a/src/newsreader/scss/elements/label/_label.scss b/src/newsreader/scss/elements/label/_label.scss index 8c8261c..5030a4c 100644 --- a/src/newsreader/scss/elements/label/_label.scss +++ b/src/newsreader/scss/elements/label/_label.scss @@ -1,3 +1,7 @@ .label { padding: 10px; } + +label { + @extend .label; +} diff --git a/src/newsreader/scss/elements/link/_link.scss b/src/newsreader/scss/elements/link/_link.scss index 96f737f..b485cb3 100644 --- a/src/newsreader/scss/elements/link/_link.scss +++ b/src/newsreader/scss/elements/link/_link.scss @@ -6,3 +6,7 @@ cursor: pointer; } } + +a { + @extend .link; +} diff --git a/src/newsreader/scss/elements/small/_small.scss b/src/newsreader/scss/elements/small/_small.scss index 59830e4..c95bfab 100644 --- a/src/newsreader/scss/elements/small/_small.scss +++ b/src/newsreader/scss/elements/small/_small.scss @@ -1,3 +1,8 @@ .small { color: $nickel; + font-size: small; +} + +small { + @extend .small; } diff --git a/src/newsreader/scss/pages/activate/components/index.scss b/src/newsreader/scss/pages/activate/components/index.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/newsreader/scss/pages/activate/elements/index.scss b/src/newsreader/scss/pages/activate/elements/index.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/newsreader/scss/pages/activate/index.scss b/src/newsreader/scss/pages/activate/index.scss new file mode 100644 index 0000000..16b6493 --- /dev/null +++ b/src/newsreader/scss/pages/activate/index.scss @@ -0,0 +1,8 @@ +// General imports +@import "../../partials/variables"; +@import "../../components/index"; +@import "../../elements/index"; + +// Page specific +@import "./components/index"; +@import "./elements/index"; diff --git a/src/newsreader/scss/pages/password-reset/components/index.scss b/src/newsreader/scss/pages/password-reset/components/index.scss new file mode 100644 index 0000000..4536bb6 --- /dev/null +++ b/src/newsreader/scss/pages/password-reset/components/index.scss @@ -0,0 +1,2 @@ +@import "password-reset-form/index"; +@import "password-reset-confirm-form/index"; diff --git a/src/newsreader/scss/pages/password-reset/components/password-reset-confirm-form/_password-reset-confirm-form.scss b/src/newsreader/scss/pages/password-reset/components/password-reset-confirm-form/_password-reset-confirm-form.scss new file mode 100644 index 0000000..d570c38 --- /dev/null +++ b/src/newsreader/scss/pages/password-reset/components/password-reset-confirm-form/_password-reset-confirm-form.scss @@ -0,0 +1,3 @@ +.password-reset-confirm-form { + margin: 20px 0; +} diff --git a/src/newsreader/scss/pages/password-reset/components/password-reset-confirm-form/index.scss b/src/newsreader/scss/pages/password-reset/components/password-reset-confirm-form/index.scss new file mode 100644 index 0000000..2448efe --- /dev/null +++ b/src/newsreader/scss/pages/password-reset/components/password-reset-confirm-form/index.scss @@ -0,0 +1 @@ +@import "password-reset-confirm-form"; diff --git a/src/newsreader/scss/pages/password-reset/components/password-reset-form/_password-reset-form.scss b/src/newsreader/scss/pages/password-reset/components/password-reset-form/_password-reset-form.scss new file mode 100644 index 0000000..be92ff4 --- /dev/null +++ b/src/newsreader/scss/pages/password-reset/components/password-reset-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/pages/password-reset/components/password-reset-form/index.scss b/src/newsreader/scss/pages/password-reset/components/password-reset-form/index.scss new file mode 100644 index 0000000..1d60faf --- /dev/null +++ b/src/newsreader/scss/pages/password-reset/components/password-reset-form/index.scss @@ -0,0 +1 @@ +@import "password-reset-form"; diff --git a/src/newsreader/scss/pages/password-reset/elements/index.scss b/src/newsreader/scss/pages/password-reset/elements/index.scss new file mode 100644 index 0000000..e69de29 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..16b6493 --- /dev/null +++ b/src/newsreader/scss/pages/password-reset/index.scss @@ -0,0 +1,8 @@ +// General imports +@import "../../partials/variables"; +@import "../../components/index"; +@import "../../elements/index"; + +// Page specific +@import "./components/index"; +@import "./elements/index"; diff --git a/src/newsreader/scss/pages/register/components/activation-form/_activation-form.scss b/src/newsreader/scss/pages/register/components/activation-form/_activation-form.scss new file mode 100644 index 0000000..39ecc27 --- /dev/null +++ b/src/newsreader/scss/pages/register/components/activation-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/pages/register/components/activation-form/index.scss b/src/newsreader/scss/pages/register/components/activation-form/index.scss new file mode 100644 index 0000000..748302f --- /dev/null +++ b/src/newsreader/scss/pages/register/components/activation-form/index.scss @@ -0,0 +1 @@ +@import "activation-form"; diff --git a/src/newsreader/scss/pages/register/components/index.scss b/src/newsreader/scss/pages/register/components/index.scss new file mode 100644 index 0000000..3377ff1 --- /dev/null +++ b/src/newsreader/scss/pages/register/components/index.scss @@ -0,0 +1,2 @@ +@import "register-form/index"; +@import "activation-form/index"; diff --git a/src/newsreader/scss/pages/register/components/register-form/_register-form.scss b/src/newsreader/scss/pages/register/components/register-form/_register-form.scss new file mode 100644 index 0000000..e406ae7 --- /dev/null +++ b/src/newsreader/scss/pages/register/components/register-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/pages/register/components/register-form/index.scss b/src/newsreader/scss/pages/register/components/register-form/index.scss new file mode 100644 index 0000000..f0d9b70 --- /dev/null +++ b/src/newsreader/scss/pages/register/components/register-form/index.scss @@ -0,0 +1 @@ +@import "register-form"; diff --git a/src/newsreader/scss/pages/register/elements/index.scss b/src/newsreader/scss/pages/register/elements/index.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/newsreader/scss/pages/register/index.scss b/src/newsreader/scss/pages/register/index.scss new file mode 100644 index 0000000..16b6493 --- /dev/null +++ b/src/newsreader/scss/pages/register/index.scss @@ -0,0 +1,8 @@ +// General imports +@import "../../partials/variables"; +@import "../../components/index"; +@import "../../elements/index"; + +// Page specific +@import "./components/index"; +@import "./elements/index"; diff --git a/src/newsreader/templates/base.html b/src/newsreader/templates/base.html index 7346a66..445d507 100644 --- a/src/newsreader/templates/base.html +++ b/src/newsreader/templates/base.html @@ -16,6 +16,7 @@ {% else %} + {% endif %} 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..80c2b69 --- /dev/null +++ b/src/newsreader/templates/password-reset/password_reset_complete.html @@ -0,0 +1,28 @@ +{% extends "base.html" %} +{% load static i18n %} + +{% block title %}{% trans "Password reset complete" %}{% endblock %} + +{% block head %} + +{% 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..ff6883f --- /dev/null +++ b/src/newsreader/templates/password-reset/password_reset_confirm.html @@ -0,0 +1,59 @@ +{% extends "base.html" %} +{% load static i18n %} + +{% block meta %} + + +{% endblock %} + +{% block title %}{% trans "Confirm password reset" %}{% endblock %} + +{% block head %} + +{% 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..11a59ff --- /dev/null +++ b/src/newsreader/templates/password-reset/password_reset_done.html @@ -0,0 +1,27 @@ +{% extends "base.html" %} +{% load static i18n %} + +{% block title %}{% trans "Password reset" %}{% endblock %} + +{% block head %} + +{% 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..2ceb647 --- /dev/null +++ b/src/newsreader/templates/password-reset/password_reset_form.html @@ -0,0 +1,34 @@ +{% extends "base.html" %} +{% load static i18n %} + +{% block title %}{% trans "Reset password" %}{% endblock %} + +{% block head %} + +{% 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..77561ef --- /dev/null +++ b/src/newsreader/templates/registration/activation_complete.html @@ -0,0 +1,35 @@ +{% extends "base.html" %} +{% load static i18n %} + +{% block title %}{% trans "Account Activated" %}{% endblock %} + +{% block head %} + +{% 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..88c9053 --- /dev/null +++ b/src/newsreader/templates/registration/activation_failure.html @@ -0,0 +1,31 @@ +{% extends "base.html" %} +{% load static i18n %} + +{% block title %}{% trans "Activation Failure" %}{% endblock %} + +{% block head %} + +{% 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..0b63c89 --- /dev/null +++ b/src/newsreader/templates/registration/activation_resend_complete.html @@ -0,0 +1,35 @@ +{% extends "base.html" %} +{% load static i18n %} + +{% block title %}{% trans "Account Activation Resent" %}{% endblock %} + +{% block head %} + +{% 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..e819a1f --- /dev/null +++ b/src/newsreader/templates/registration/activation_resend_form.html @@ -0,0 +1,39 @@ +{% extends "base.html" %} +{% load static i18n %} + +{% block title %}{% trans "Resend Activation Email" %}{% endblock %} + +{% block head %} + +{% 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..1ac2ad5 --- /dev/null +++ b/src/newsreader/templates/registration/registration_closed.html @@ -0,0 +1,25 @@ +{% extends "base.html" %} +{% load static i18n %} + +{% block title %}{% trans "Registration is closed" %}{% endblock %} + +{% block head %} + +{% 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..6e508a2 --- /dev/null +++ b/src/newsreader/templates/registration/registration_complete.html @@ -0,0 +1,33 @@ +{% extends "base.html" %} +{% load static i18n %} + +{% block title %}{% trans "Activation email sent" %}{% endblock %} + +{% block head %} + +{% 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..d5e8a13 --- /dev/null +++ b/src/newsreader/templates/registration/registration_form.html @@ -0,0 +1,26 @@ +{% extends "base.html" %} + +{% load static %} + +{% block head %} + +{% endblock %} + +{% block content %} +
    +
    + {% csrf_token %} +
    +

    Register

    +
    + +
    + {{ form }} +
    +
    + Cancel + +
    +
    +
    +{% endblock %} From a9edd520a71ec84681213b92442204f8b8a75d26 Mon Sep 17 00:00:00 2001 From: Sonny Date: Thu, 28 Nov 2019 21:18:45 +0100 Subject: [PATCH 028/364] Fix negative unread count --- .../js/pages/homepage/reducers/categories.js | 52 ++++++++----------- .../js/pages/homepage/reducers/selected.js | 6 +++ 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/newsreader/js/pages/homepage/reducers/categories.js b/src/newsreader/js/pages/homepage/reducers/categories.js index 4fd391f..90fe063 100644 --- a/src/newsreader/js/pages/homepage/reducers/categories.js +++ b/src/newsreader/js/pages/homepage/reducers/categories.js @@ -12,33 +12,6 @@ import { import { MARK_POST_READ } from '../actions/posts.js'; import { MARK_SECTION_READ } from '../actions/selected.js'; -const markCategoryRead = (action, state) => { - const category = { ...state.items[action.section.id] }; - - return { - ...state, - items: { - ...state.items, - [category.id]: { ...category, unread: 0 }, - }, - }; -}; - -const markCategoryByRule = (action, state) => { - const category = { ...state.items[action.section.category] }; - - return { - ...state, - items: { - ...state.items, - [category.id]: { - ...category, - unread: category.unread - action.section.unread, - }, - }, - }; -}; - const defaultState = { items: {}, isFetching: false }; export const categories = (state = { ...defaultState }, action) => { @@ -83,11 +56,32 @@ export const categories = (state = { ...defaultState }, action) => { }, }; case MARK_SECTION_READ: + category = {}; + switch (action.section.type) { case CATEGORY_TYPE: - return markCategoryRead(action, state); + category = { ...state.items[action.section.id] }; + + return { + ...state, + items: { + ...state.items, + [category.id]: { ...category, unread: 0 }, + }, + }; case RULE_TYPE: - return markCategoryByRule(action, state); + category = { ...state.items[action.section.category] }; + + return { + ...state, + items: { + ...state.items, + [category.id]: { + ...category, + unread: category.unread - action.section.unread, + }, + }, + }; } return state; diff --git a/src/newsreader/js/pages/homepage/reducers/selected.js b/src/newsreader/js/pages/homepage/reducers/selected.js index 0bef90c..8b1f7f8 100644 --- a/src/newsreader/js/pages/homepage/reducers/selected.js +++ b/src/newsreader/js/pages/homepage/reducers/selected.js @@ -10,6 +10,7 @@ import { } 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: {} }; @@ -75,6 +76,11 @@ export const selected = (state = { ...defaultState }, action) => { ...state, post: {}, }; + case MARK_POST_READ: + return { + ...state, + item: { ...action.section, unread: action.section.unread - 1 }, + }; case MARK_SECTION_READ: return { ...state, From 08e4d043b91eca006544c579262f72f02f8f471b Mon Sep 17 00:00:00 2001 From: Sonny Date: Fri, 29 Nov 2019 23:15:09 +0100 Subject: [PATCH 029/364] Update default fixture --- src/newsreader/fixtures/default-fixture.json | 555 +++++++++---------- 1 file changed, 275 insertions(+), 280 deletions(-) diff --git a/src/newsreader/fixtures/default-fixture.json b/src/newsreader/fixtures/default-fixture.json index e0ed80f..7639b81 100644 --- a/src/newsreader/fixtures/default-fixture.json +++ b/src/newsreader/fixtures/default-fixture.json @@ -1,282 +1,277 @@ [ - { - "fields" : { - "last_login" : "2019-08-10T18:07:27.224Z", - "last_name" : "", - "is_superuser" : true, - "is_staff" : true, - "task_interval" : null, - "email" : "sonny@newsreader.nl", - "user_permissions" : [], - "groups" : [], - "password" : "pbkdf2_sha256$150000$OeNoz2LRSpI5$jkkUf/BjTuWZULyldvNTt9f45/ErxaCmCQHfZwtzji8=", - "first_name" : "", - "task" : null, - "is_active" : true, - "date_joined" : "2019-08-10T18:07:19.699Z" - }, - "pk" : 1, - "model" : "accounts.user" - }, - { - "pk" : 1, - "model" : "django_celery_beat.intervalschedule", - "fields" : { - "period" : "minutes", - "every" : 5 - } - }, - { - "pk" : 2, - "model" : "django_celery_beat.intervalschedule", - "fields" : { - "every" : 30, - "period" : "minutes" - } - }, - { - "fields" : { - "every" : 3, - "period" : "hours" - }, - "model" : "django_celery_beat.intervalschedule", - "pk" : 3 - }, - { - "fields" : { - "minute" : "0", - "timezone" : "UTC", - "day_of_month" : "*", - "hour" : "4", - "month_of_year" : "*", - "day_of_week" : "*" - }, - "model" : "django_celery_beat.crontabschedule", - "pk" : 1 - }, - { - "fields" : { - "last_update" : "2019-08-10T20:01:21.152Z" - }, - "model" : "django_celery_beat.periodictasks", - "pk" : 1 - }, - { - "model" : "django_celery_beat.periodictask", - "pk" : 1, - "fields" : { - "description" : "", - "last_run_at" : null, - "routing_key" : null, - "kwargs" : "{}", - "queue" : null, - "name" : "celery.backend_cleanup", - "clocked" : null, - "solar" : null, - "task" : "celery.backend_cleanup", - "one_off" : false, - "expires" : null, - "total_run_count" : 0, - "date_changed" : "2019-08-10T19:56:33.160Z", - "args" : "[]", - "start_time" : null, - "priority" : null, - "crontab" : 1, - "interval" : null, - "enabled" : true, - "headers" : "{}", - "exchange" : null - } - }, - { - "pk" : 3, - "model" : "django_celery_beat.periodictask", - "fields" : { - "exchange" : null, - "headers" : "{}", - "enabled" : true, - "priority" : null, - "crontab" : null, - "interval" : 3, - "start_time" : "2019-08-10T20:00:52Z", - "args" : "[1]", - "date_changed" : "2019-08-10T20:01:21.153Z", - "total_run_count" : 0, - "expires" : null, - "task" : "newsreader.news.collection.tasks.collect", - "one_off" : false, - "clocked" : null, - "solar" : null, - "name" : "Collection testing task", - "queue" : null, - "kwargs" : "{}", - "routing_key" : null, - "last_run_at" : null, - "description" : "" - } - }, - { - "fields" : { - "user" : 1, - "created" : "2019-08-10T19:57:53Z", - "name" : "Tech", - "modified" : "2019-08-10T19:58:02.048Z" - }, - "pk" : 1, - "model" : "core.category" - }, - { - "fields" : { - "modified" : "2019-08-10T19:58:20.951Z", - "user" : 1, - "created" : "2019-08-10T19:58:13Z", - "name" : "News" - }, - "pk" : 2, - "model" : "core.category" - }, - { - "pk" : 1, - "model" : "collection.collectionrule", - "fields" : { - "timezone" : "Europe/Amsterdam", - "succeeded" : true, - "error" : null, - "website_url" : null, - "favicon" : null, - "category" : 1, - "created" : "2019-08-10T18:08:10.520Z", - "user" : 1, - "name" : "Tweakers", - "url" : "http://feeds.feedburner.com/tweakers/mixed", - "last_suceeded" : "2019-08-10T18:25:32.325Z", - "modified" : "2019-08-10T19:59:54.547Z" - } - }, - { - "fields" : { - "favicon" : null, - "category" : 1, - "timezone" : "UTC", - "error" : null, - "website_url" : null, - "succeeded" : false, - "modified" : "2019-08-10T19:58:05.691Z", - "last_suceeded" : null, - "name" : "Hackers News", - "created" : "2019-08-10T19:58:05.615Z", - "user" : 1, - "url" : "https://news.ycombinator.com/rss" - }, - "pk" : 2, - "model" : "collection.collectionrule" - }, - { - "fields" : { - "timezone" : "UTC", - "error" : null, - "website_url" : null, - "succeeded" : false, - "favicon" : null, - "category" : 2, - "name" : "BBC", - "created" : "2019-08-10T19:58:23.441Z", - "user" : 1, - "url" : "http://feeds.bbci.co.uk/news/world/rss.xml", - "modified" : "2019-08-10T19:58:23.449Z", - "last_suceeded" : null - }, - "pk" : 3, - "model" : "collection.collectionrule" - }, - { - "fields" : { - "favicon" : null, - "category" : 1, - "timezone" : "UTC", - "succeeded" : false, - "error" : null, - "website_url" : null, - "last_suceeded" : null, - "modified" : "2019-08-10T19:58:31.873Z", - "user" : 1, - "created" : "2019-08-10T19:58:31.867Z", - "name" : "Ars Technica", - "url" : "http://feeds.arstechnica.com/arstechnica/index?fmt=xml" - }, - "model" : "collection.collectionrule", - "pk" : 4 - }, - { - "pk" : 5, - "model" : "collection.collectionrule", - "fields" : { - "last_suceeded" : null, - "modified" : "2019-08-10T19:58:48.535Z", - "url" : "https://www.theguardian.com/world/rss", - "created" : "2019-08-10T19:58:48.529Z", - "user" : 1, - "name" : "The Guardian", - "category" : 2, - "favicon" : null, - "succeeded" : false, - "error" : null, - "website_url" : null, - "timezone" : "UTC" - } - }, - { - "model" : "collection.collectionrule", - "pk" : 6, - "fields" : { - "error" : null, - "website_url" : null, - "succeeded" : false, - "timezone" : "UTC", - "category" : 1, - "favicon" : null, - "url" : "https://www.engadget.com/rss.xml", - "name" : "Engadget", - "created" : "2019-08-10T19:58:58.641Z", - "user" : 1, - "modified" : "2019-08-10T19:58:58.647Z", - "last_suceeded" : null - } - }, - { - "fields" : { - "last_suceeded" : null, - "modified" : "2019-08-10T19:59:29.917Z", - "user" : 1, - "created" : "2019-08-10T19:59:29.909Z", - "name" : "The Verge", - "url" : "https://www.theverge.com/rss/index.xml", - "favicon" : null, - "category" : 1, - "timezone" : "UTC", - "succeeded" : false, - "error" : null, - "website_url" : null - }, - "model" : "collection.collectionrule", - "pk" : 7 - }, - { - "pk" : 8, - "model" : "collection.collectionrule", - "fields" : { - "modified" : "2019-08-10T19:59:44.838Z", - "last_suceeded" : null, - "name" : "News", - "user" : 1, - "created" : "2019-08-10T19:59:44.833Z", - "url" : "http://feeds.boingboing.net/boingboing/iBag", - "favicon" : null, - "category" : 2, - "timezone" : "UTC", - "website_url" : null, - "error" : null, - "succeeded" : false - } - } +{ + "model": "django_celery_beat.periodictask", + "pk": 2, + "fields": { + "name": "celery.backend_cleanup", + "task": "celery.backend_cleanup", + "interval": null, + "crontab": 1, + "solar": null, + "clocked": null, + "args": "[]", + "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-29T04:00:00.000Z", + "total_run_count": 17, + "date_changed": "2019-11-29T04:02:15.258Z", + "description": "" + } +}, +{ + "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": "accounts.user", + "pk": 2, + "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", + "pk": 18, + "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": 2 + } +}, +{ + "model": "core.category", + "pk": 9, + "fields": { + "created": "2019-11-17T19:37:26.161Z", + "modified": "2019-11-18T19:59:45.010Z", + "name": "Tech", + "user": 2 + } +}, +{ + "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": 2 + } +}, +{ + "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": 2 + } +}, +{ + "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": 2 + } +}, +{ + "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": 2 + } +}, +{ + "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": 2 + } +}, +{ + "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": 2 + } +}, +{ + "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": 2 + } +} ] From d345bc2595dba8d1e6596986c2d7e9dac807afb4 Mon Sep 17 00:00:00 2001 From: Sonny Date: Sat, 30 Nov 2019 16:27:50 +0100 Subject: [PATCH 030/364] Add missing model to fixture --- src/newsreader/fixtures/default-fixture.json | 70 +++++++++++++++++--- 1 file changed, 59 insertions(+), 11 deletions(-) diff --git a/src/newsreader/fixtures/default-fixture.json b/src/newsreader/fixtures/default-fixture.json index 7639b81..ed3c3be 100644 --- a/src/newsreader/fixtures/default-fixture.json +++ b/src/newsreader/fixtures/default-fixture.json @@ -93,8 +93,39 @@ } }, { - "model": "accounts.user", + "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", @@ -112,7 +143,6 @@ }, { "model": "accounts.user", - "pk": 18, "fields": { "password": "pbkdf2_sha256$150000$vUwxT8T25R8C$S+Eq2tMRbSDE31/X5KGJ/M+Nblh7kKfzuM/z7HraR/Q=", "last_login": null, @@ -135,7 +165,9 @@ "created": "2019-11-17T19:37:24.671Z", "modified": "2019-11-18T19:59:55.010Z", "name": "World news", - "user": 2 + "user": [ + "sonny@bakker.nl" + ] } }, { @@ -145,7 +177,9 @@ "created": "2019-11-17T19:37:26.161Z", "modified": "2019-11-18T19:59:45.010Z", "name": "Tech", - "user": 2 + "user": [ + "sonny@bakker.nl" + ] } }, { @@ -163,7 +197,9 @@ "last_suceeded": "2019-11-29T22:35:20.235Z", "succeeded": true, "error": null, - "user": 2 + "user": [ + "sonny@bakker.nl" + ] } }, { @@ -181,7 +217,9 @@ "last_suceeded": "2019-11-29T22:35:19.241Z", "succeeded": true, "error": null, - "user": 2 + "user": [ + "sonny@bakker.nl" + ] } }, { @@ -199,7 +237,9 @@ "last_suceeded": "2019-11-29T22:35:19.808Z", "succeeded": true, "error": null, - "user": 2 + "user": [ + "sonny@bakker.nl" + ] } }, { @@ -217,7 +257,9 @@ "last_suceeded": "2019-11-29T22:35:20.076Z", "succeeded": true, "error": null, - "user": 2 + "user": [ + "sonny@bakker.nl" + ] } }, { @@ -235,7 +277,9 @@ "last_suceeded": "2019-11-29T22:35:19.528Z", "succeeded": true, "error": null, - "user": 2 + "user": [ + "sonny@bakker.nl" + ] } }, { @@ -253,7 +297,9 @@ "last_suceeded": "2019-11-29T22:35:20.012Z", "succeeded": true, "error": null, - "user": 2 + "user": [ + "sonny@bakker.nl" + ] } }, { @@ -271,7 +317,9 @@ "last_suceeded": "2019-11-29T22:35:19.697Z", "succeeded": true, "error": null, - "user": 2 + "user": [ + "sonny@bakker.nl" + ] } } ] From 38d9d74db416cfe3fc8cf559b8f21ef666acd692 Mon Sep 17 00:00:00 2001 From: sonny Date: Tue, 31 Dec 2019 14:32:29 +0100 Subject: [PATCH 031/364] Collection rule pages --- gulp/babel.js | 8 + gulp/sass.js | 9 ++ src/newsreader/js/components/Messages.js | 6 +- .../categories/components/CategoryCard.js | 4 +- src/newsreader/js/pages/rules/App.js | 100 ++++++++++++ .../js/pages/rules/components/RuleCard.js | 62 ++++++++ .../js/pages/rules/components/RuleModal.js | 33 ++++ src/newsreader/js/pages/rules/index.js | 9 ++ src/newsreader/news/collection/forms.py | 30 ++++ .../templates/collection/rule-create.html | 31 ++++ .../templates/collection/rule-update.html | 34 ++++ .../collection/templates/collection/rule.html | 70 +++++++++ .../templates/collection/rules.html | 33 ++++ .../news/collection/tests/test_views.py | 148 ++++++++++++++++++ src/newsreader/news/collection/urls.py | 22 ++- src/newsreader/news/collection/views.py | 58 +++++++ src/newsreader/news/core/forms.py | 6 +- .../scss/components/card/_card.scss | 1 + src/newsreader/scss/components/errorlist/; | 3 - .../scss/pages/rule/components/index.scss | 1 + .../rule/components/rule-form/_rule-form.scss | 27 ++++ .../rule/components/rule-form/index.scss | 1 + .../scss/pages/rule/elements/index.scss | 0 src/newsreader/scss/pages/rule/index.scss | 8 + .../pages/rules/components/card/_card.scss | 20 +++ .../pages/rules/components/card/index.scss | 1 + .../scss/pages/rules/components/index.scss | 3 + .../components/rule-modal/_rule-modal.scss | 27 ++++ .../rules/components/rule-modal/index.scss | 1 + .../pages/rules/components/rules/_rules.scss | 7 + .../pages/rules/components/rules/index.scss | 1 + .../scss/pages/rules/elements/index.scss | 0 src/newsreader/scss/pages/rules/index.scss | 8 + src/newsreader/templates/base.html | 2 +- src/newsreader/urls.py | 6 +- 35 files changed, 766 insertions(+), 14 deletions(-) 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/news/collection/forms.py 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/test_views.py delete mode 100644 src/newsreader/scss/components/errorlist/; create mode 100644 src/newsreader/scss/pages/rule/components/index.scss create mode 100644 src/newsreader/scss/pages/rule/components/rule-form/_rule-form.scss create mode 100644 src/newsreader/scss/pages/rule/components/rule-form/index.scss create mode 100644 src/newsreader/scss/pages/rule/elements/index.scss create mode 100644 src/newsreader/scss/pages/rule/index.scss create mode 100644 src/newsreader/scss/pages/rules/components/card/_card.scss create mode 100644 src/newsreader/scss/pages/rules/components/card/index.scss create mode 100644 src/newsreader/scss/pages/rules/components/index.scss create mode 100644 src/newsreader/scss/pages/rules/components/rule-modal/_rule-modal.scss create mode 100644 src/newsreader/scss/pages/rules/components/rule-modal/index.scss create mode 100644 src/newsreader/scss/pages/rules/components/rules/_rules.scss create mode 100644 src/newsreader/scss/pages/rules/components/rules/index.scss create mode 100644 src/newsreader/scss/pages/rules/elements/index.scss create mode 100644 src/newsreader/scss/pages/rules/index.scss diff --git a/gulp/babel.js b/gulp/babel.js index 3c33996..3ef77d3 100644 --- a/gulp/babel.js +++ b/gulp/babel.js @@ -12,10 +12,18 @@ const SRC_DIR = path.join(PROJECT_DIR, 'js'); const STATIC_SUFFIX = 'dist/js/'; const CORE_DIR = path.join(PROJECT_DIR, 'news', 'core', 'static', 'core'); +const COLLECTION_DIR = path.join( + PROJECT_DIR, + 'news', + 'collection', + 'static', + 'collection' +); const taskMappings = [ { name: 'homepage', destDir: `${CORE_DIR}/${STATIC_SUFFIX}` }, { name: 'categories', destDir: `${CORE_DIR}/${STATIC_SUFFIX}` }, + { name: 'rules', destDir: `${COLLECTION_DIR}/${STATIC_SUFFIX}` }, ]; const babelTask = done => { diff --git a/gulp/sass.js b/gulp/sass.js index 2a71455..46cd291 100644 --- a/gulp/sass.js +++ b/gulp/sass.js @@ -10,6 +10,13 @@ const STATIC_SUFFIX = 'dist/css/'; export const ACCOUNTS_DIR = path.join(PROJECT_DIR, 'accounts', 'static', 'accounts'); export const CORE_DIR = path.join(PROJECT_DIR, 'news', 'core', 'static', 'core'); +export const COLLECTION_DIR = path.join( + PROJECT_DIR, + 'news', + 'collection', + 'static', + 'collection' +); const taskMappings = [ { name: 'login', destDir: `${ACCOUNTS_DIR}/${STATIC_SUFFIX}` }, @@ -19,6 +26,8 @@ const taskMappings = [ { name: 'homepage', destDir: `${CORE_DIR}/${STATIC_SUFFIX}` }, { name: 'categories', destDir: `${CORE_DIR}/${STATIC_SUFFIX}` }, { name: 'category', destDir: `${CORE_DIR}/${STATIC_SUFFIX}` }, + { name: 'rules', destDir: `${COLLECTION_DIR}/${STATIC_SUFFIX}` }, + { name: 'rule', destDir: `${COLLECTION_DIR}/${STATIC_SUFFIX}` }, ]; export const sassTask = done => { diff --git a/src/newsreader/js/components/Messages.js b/src/newsreader/js/components/Messages.js index fe8cb20..a985381 100644 --- a/src/newsreader/js/components/Messages.js +++ b/src/newsreader/js/components/Messages.js @@ -1,9 +1,11 @@ import React from 'react'; const Messages = props => { - const messages = props.messages.map(message => { + const messages = props.messages.map((index, message) => { return ( -
  • {message.text}
  • +
  • + {message.text} +
  • ); }); diff --git a/src/newsreader/js/pages/categories/components/CategoryCard.js b/src/newsreader/js/pages/categories/components/CategoryCard.js index 04fccef..5c58a51 100644 --- a/src/newsreader/js/pages/categories/components/CategoryCard.js +++ b/src/newsreader/js/pages/categories/components/CategoryCard.js @@ -19,9 +19,7 @@ const CategoryCard = props => { const cardHeader = ( <>

    {category.name}

    - - {category.created} - + {category.created} ); const cardContent = <>{category.rules &&
      {categoryRules}
    }; diff --git a/src/newsreader/js/pages/rules/App.js b/src/newsreader/js/pages/rules/App.js new file mode 100644 index 0000000..fd01229 --- /dev/null +++ b/src/newsreader/js/pages/rules/App.js @@ -0,0 +1,100 @@ +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

    + + Create rule + + + ); + + 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..c94144e --- /dev/null +++ b/src/newsreader/js/pages/rules/components/RuleCard.js @@ -0,0 +1,62 @@ +import React from 'react'; + +import Card from '../../../components/Card.js'; + +const RuleCard = props => { + const { rule } = props; + + const faviconUrl = rule.favicon ? rule.favicon : '/static/picture.svg'; + const stateIcon = rule.succeeded + ? '/static/checkmark-circle.svg' + : '/static/warning.svg'; + + const cardHeader = ( + <> +
    + +

    {rule.name}

    +
    + + + ); + + const cardContent = ( + <> + + {!rule.succeeded && ( +
      +
    • {rule.error}
    • +
    + )} + + ); + + 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..5d6f09b --- /dev/null +++ b/src/newsreader/js/pages/rules/components/RuleModal.js @@ -0,0 +1,33 @@ +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..7fc7a5c --- /dev/null +++ b/src/newsreader/js/pages/rules/index.js @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; + +import App from './App.js'; + +const dataScript = document.getElementById('rules-data'); +const rules = JSON.parse(dataScript.textContent); + +ReactDOM.render(, document.getElementsByClassName('content')[0]); diff --git a/src/newsreader/news/collection/forms.py b/src/newsreader/news/collection/forms.py new file mode 100644 index 0000000..89c72e9 --- /dev/null +++ b/src/newsreader/news/collection/forms.py @@ -0,0 +1,30 @@ +from django import forms + +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()) + + def __init__(self, *args, **kwargs) -> None: + 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) -> CollectionRule: + 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") 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..4bb242a --- /dev/null +++ b/src/newsreader/news/collection/templates/collection/rule-create.html @@ -0,0 +1,31 @@ +{% extends "collection/rule.html" %} + +{% block form-header %} +

    Create a rule

    +{% endblock %} + +{% block name-input %} + +{% endblock %} + +{% block category-input %} + +{% endblock %} + +{% block url-input %} + +{% endblock %} + +{% block favicon-input %} + +{% endblock %} + +{% block timezone-input %} + +{% 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..7c0a4ba --- /dev/null +++ b/src/newsreader/news/collection/templates/collection/rule-update.html @@ -0,0 +1,34 @@ +{% extends "collection/rule.html" %} + +{% block form-header %} +

    Update rule

    +{% endblock %} + +{% block name-input %} + +{% endblock %} + +{% block category-input %} + +{% endblock %} + +{% block url-input %} + +{% endblock %} + +{% block favicon-input %} + +{% endblock %} + +{% block timezone-input %} + +{% 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..0d67e08 --- /dev/null +++ b/src/newsreader/news/collection/templates/collection/rule.html @@ -0,0 +1,70 @@ +{% extends "base.html" %} + +{% load static %} + +{% block head %} + +{% endblock %} + +{% block content %} +
    +
    + {% csrf_token %} + {{ form.non_field_errors }} + +
    + {% block form-header %}{% endblock %} +
    +
    +
    + + {% block name-input %}{% endblock %} + {{ form.name.errors }} +
    + +
    + + + + + {{ form.category.errors }} +
    + +
    + + {% block url-input %}{% endblock %} + {{ form.url.errors }} +
    + +
    + + {% block favicon-input %}{% endblock %} + {{ form.favicon.errors }} +
    + +
    + + The timezone which the feed uses + + + {{ form.timezone.errors }} +
    +
    + +
    +
    + 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..844e27b --- /dev/null +++ b/src/newsreader/news/collection/templates/collection/rules.html @@ -0,0 +1,33 @@ +{% extends "base.html" %} + +{% load static %} + +{% block head %} + +{% endblock %} + +{% block content %} +
    +{% endblock %} + +{% block scripts %} + + +{% endblock %} diff --git a/src/newsreader/news/collection/tests/test_views.py b/src/newsreader/news/collection/tests/test_views.py new file mode 100644 index 0000000..d6a478e --- /dev/null +++ b/src/newsreader/news/collection/tests/test_views.py @@ -0,0 +1,148 @@ +from django.test import Client, TestCase +from django.urls import reverse + +import pytz + +from newsreader.accounts.tests.factories import UserFactory +from newsreader.news.collection.models import CollectionRule +from newsreader.news.collection.tests.factories import CollectionRuleFactory +from newsreader.news.core.tests.factories import CategoryFactory + + +class CollectionRuleViewTestCase: + def setUp(self): + self.user = UserFactory(password="test") + self.client.login(email=self.user.email, password="test") + + self.category = CategoryFactory(user=self.user) + self.form_data = {"name": "", "category": "", "url": "", "timezone": ""} + + def test_simple(self): + response = self.client.get(self.url) + + self.assertEquals(response.status_code, 200) + + def test_no_category(self): + self.form_data.update(category="") + + response = self.client.post(self.url, self.form_data) + self.assertEquals(response.status_code, 302) + + rule = CollectionRule.objects.get() + + self.assertEquals(rule.category, None) + + def test_categories_only_from_user(self): + other_user = UserFactory() + other_categories = CategoryFactory.create_batch(size=4, user=other_user) + + response = self.client.get(self.url) + + for category in other_categories: + self.assertNotContains(response, category.name) + + def test_category_of_other_user(self): + other_user = UserFactory() + other_rule = CollectionRuleFactory(name="other rule", user=other_user) + + self.form_data.update( + name="new name", + category=other_rule.category, + url=other_rule.url, + timezone=other_rule.timezone, + ) + + other_url = reverse("rule-update", args=[other_rule.pk]) + response = self.client.post(other_url, self.form_data) + + self.assertEquals(response.status_code, 404) + + other_rule.refresh_from_db() + + self.assertEquals(other_rule.name, "other rule") + + def test_with_other_user_rules(self): + other_user = UserFactory() + other_categories = CategoryFactory.create_batch(size=4, user=other_user) + + self.form_data.update(category=other_categories[2].pk) + + response = self.client.post(self.url, self.form_data) + + self.assertContains(response, "not one of the available choices") + + +class CollectionRuleCreateViewTestCase(CollectionRuleViewTestCase, TestCase): + def setUp(self): + super().setUp() + + self.url = reverse("rule-create") + + self.form_data.update( + name="new rule", + url="https://www.rss.com/rss", + timezone=pytz.utc, + category=str(self.category.pk), + ) + + def test_creation(self): + response = self.client.post(self.url, self.form_data) + + self.assertEquals(response.status_code, 302) + + rule = CollectionRule.objects.get(name="new rule") + + self.assertEquals(rule.url, "https://www.rss.com/rss") + self.assertEquals(rule.timezone, str(pytz.utc)) + self.assertEquals(rule.favicon, None) + self.assertEquals(rule.category.pk, self.category.pk) + self.assertEquals(rule.user.pk, self.user.pk) + + +class CollectionRuleUpdateViewTestCase(CollectionRuleViewTestCase, TestCase): + def setUp(self): + super().setUp() + + self.rule = CollectionRuleFactory( + name="collection rule", user=self.user, category=self.category + ) + self.url = reverse("rule-update", args=[self.rule.pk]) + + self.form_data.update( + name=self.rule.name, + category=self.rule.category.pk, + url=self.rule.url, + timezone=self.rule.timezone, + ) + + def test_name_change(self): + self.form_data.update(name="new name") + + response = self.client.post(self.url, self.form_data) + self.assertEquals(response.status_code, 302) + + self.rule.refresh_from_db() + + self.assertEquals(self.rule.name, "new name") + + def test_category_change(self): + new_category = CategoryFactory(user=self.user) + + self.form_data.update(category=new_category.pk) + + response = self.client.post(self.url, self.form_data) + self.assertEquals(response.status_code, 302) + + self.rule.refresh_from_db() + + self.assertEquals(self.rule.category.pk, new_category.pk) + + def test_category_removal(self): + self.form_data.update(category="") + + response = self.client.post(self.url, self.form_data) + self.assertEquals(response.status_code, 302) + + self.rule.refresh_from_db() + + self.assertEquals(self.rule.category, None) diff --git a/src/newsreader/news/collection/urls.py b/src/newsreader/news/collection/urls.py index 606ec3a..16c80b7 100644 --- a/src/newsreader/news/collection/urls.py +++ b/src/newsreader/news/collection/urls.py @@ -1,3 +1,4 @@ +from django.contrib.auth.decorators import login_required from django.urls import path from newsreader.news.collection.endpoints import ( @@ -6,11 +7,30 @@ from newsreader.news.collection.endpoints import ( NestedRuleView, RuleReadView, ) +from newsreader.news.collection.views import ( + CollectionRuleCreateView, + CollectionRuleListView, + CollectionRuleUpdateView, +) endpoints = [ - path("rules/", DetailRuleView.as_view(), name="rules-detail"), + path("rules//", DetailRuleView.as_view(), name="rules-detail"), path("rules//posts/", NestedRuleView.as_view(), name="rules-nested-posts"), path("rules//read/", RuleReadView.as_view(), name="rules-read"), path("rules/", ListRuleView.as_view(), name="rules-list"), ] + +urlpatterns = [ + path("rules/", login_required(CollectionRuleListView.as_view()), name="rules"), + path( + "rules//", + login_required(CollectionRuleUpdateView.as_view()), + name="rule-update", + ), + path( + "rules/create/", + login_required(CollectionRuleCreateView.as_view()), + name="rule-create", + ), +] diff --git a/src/newsreader/news/collection/views.py b/src/newsreader/news/collection/views.py index e69de29..8919ddf 100644 --- a/src/newsreader/news/collection/views.py +++ b/src/newsreader/news/collection/views.py @@ -0,0 +1,58 @@ +from typing import Dict, Iterable + +from django.urls import reverse_lazy +from django.views.generic.edit import CreateView, UpdateView +from django.views.generic.list import ListView + +import pytz + +from newsreader.news.collection.forms import CollectionRuleForm +from newsreader.news.collection.models import CollectionRule +from newsreader.news.core.models import Category + + +class CollectionRuleViewMixin: + queryset = CollectionRule.objects.order_by("name") + + def get_queryset(self) -> Iterable: + user = self.request.user + return self.queryset.filter(user=user) + + +class CollectionRuleDetailMixin: + success_url = reverse_lazy("rules") + form_class = CollectionRuleForm + + def get_context_data(self, **kwargs) -> Dict: + context_data = super().get_context_data(**kwargs) + + rules = Category.objects.filter(user=self.request.user).order_by("name") + timezones = [timezone for timezone in pytz.all_timezones] + + context_data["categories"] = rules + context_data["timezones"] = timezones + + return context_data + + def get_form_kwargs(self) -> Dict: + kwargs = super().get_form_kwargs() + kwargs["user"] = self.request.user + return kwargs + + +class CollectionRuleListView(CollectionRuleViewMixin, ListView): + template_name = "collection/rules.html" + context_object_name = "rules" + + +class CollectionRuleUpdateView( + CollectionRuleViewMixin, CollectionRuleDetailMixin, UpdateView +): + template_name = "collection/rule-update.html" + context_object_name = "rule" + + +class CollectionRuleCreateView( + CollectionRuleViewMixin, CollectionRuleDetailMixin, CreateView +): + template_name = "collection/rule-create.html" diff --git a/src/newsreader/news/core/forms.py b/src/newsreader/news/core/forms.py index 38e03f4..8fa2221 100644 --- a/src/newsreader/news/core/forms.py +++ b/src/newsreader/news/core/forms.py @@ -6,8 +6,8 @@ from newsreader.news.core.models import Category class CategoryForm(forms.ModelForm): rules = forms.ModelMultipleChoiceField( - queryset=CollectionRule.objects.all(), required=False, + queryset=CollectionRule.objects.all(), widget=forms.widgets.CheckboxSelectMultiple, ) @@ -17,7 +17,9 @@ class CategoryForm(forms.ModelForm): super().__init__(*args, **kwargs) if self.user: - self.fields["rules"].queryset = CollectionRule.objects.filter(user=self.user) + self.fields["rules"].queryset = CollectionRule.objects.filter( + user=self.user + ) def save(self, commit=True) -> Category: instance = super().save(commit=False) diff --git a/src/newsreader/scss/components/card/_card.scss b/src/newsreader/scss/components/card/_card.scss index d979eeb..23a8079 100644 --- a/src/newsreader/scss/components/card/_card.scss +++ b/src/newsreader/scss/components/card/_card.scss @@ -13,6 +13,7 @@ &__header { display: flex; justify-content: space-between; + align-items: center; padding: 15px 0; diff --git a/src/newsreader/scss/components/errorlist/; b/src/newsreader/scss/components/errorlist/; deleted file mode 100644 index efcee40..0000000 --- a/src/newsreader/scss/components/errorlist/; +++ /dev/null @@ -1,3 +0,0 @@ -.errorlist { - -} diff --git a/src/newsreader/scss/pages/rule/components/index.scss b/src/newsreader/scss/pages/rule/components/index.scss new file mode 100644 index 0000000..de2a031 --- /dev/null +++ b/src/newsreader/scss/pages/rule/components/index.scss @@ -0,0 +1 @@ +@import "rule-form/index"; diff --git a/src/newsreader/scss/pages/rule/components/rule-form/_rule-form.scss b/src/newsreader/scss/pages/rule/components/rule-form/_rule-form.scss new file mode 100644 index 0000000..95a2388 --- /dev/null +++ b/src/newsreader/scss/pages/rule/components/rule-form/_rule-form.scss @@ -0,0 +1,27 @@ +.rule-form { + margin: 20px 0; + + &__section:last-child { + + & .rule-form__fieldset { + display: flex; + flex-direction: row; + justify-content: space-between; + } + } + + &__select[name=category] { + width: 50%; + + padding: 0 10px; + } + + &__select[name=timezone] { + max-height: 200px; + width: 50%; + + margin: 0 15px; + padding: 0 10px; + + } +} diff --git a/src/newsreader/scss/pages/rule/components/rule-form/index.scss b/src/newsreader/scss/pages/rule/components/rule-form/index.scss new file mode 100644 index 0000000..4c7fbee --- /dev/null +++ b/src/newsreader/scss/pages/rule/components/rule-form/index.scss @@ -0,0 +1 @@ +@import "rule-form"; diff --git a/src/newsreader/scss/pages/rule/elements/index.scss b/src/newsreader/scss/pages/rule/elements/index.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/newsreader/scss/pages/rule/index.scss b/src/newsreader/scss/pages/rule/index.scss new file mode 100644 index 0000000..16b6493 --- /dev/null +++ b/src/newsreader/scss/pages/rule/index.scss @@ -0,0 +1,8 @@ +// General imports +@import "../../partials/variables"; +@import "../../components/index"; +@import "../../elements/index"; + +// Page specific +@import "./components/index"; +@import "./elements/index"; diff --git a/src/newsreader/scss/pages/rules/components/card/_card.scss b/src/newsreader/scss/pages/rules/components/card/_card.scss new file mode 100644 index 0000000..ec09189 --- /dev/null +++ b/src/newsreader/scss/pages/rules/components/card/_card.scss @@ -0,0 +1,20 @@ +.card { + &__header { + & div { + display: flex; + flex-direction: row; + + & img { + padding: 0 10px; + } + } + } + + &__content { + flex-direction: column; + } + + &__footer > *:last-child { + margin: 0 0 0 10px; + } +} diff --git a/src/newsreader/scss/pages/rules/components/card/index.scss b/src/newsreader/scss/pages/rules/components/card/index.scss new file mode 100644 index 0000000..484e154 --- /dev/null +++ b/src/newsreader/scss/pages/rules/components/card/index.scss @@ -0,0 +1 @@ +@import "card"; diff --git a/src/newsreader/scss/pages/rules/components/index.scss b/src/newsreader/scss/pages/rules/components/index.scss new file mode 100644 index 0000000..7e96fec --- /dev/null +++ b/src/newsreader/scss/pages/rules/components/index.scss @@ -0,0 +1,3 @@ +@import "card/index"; +@import "rules/index"; +@import "rule-modal/index"; diff --git a/src/newsreader/scss/pages/rules/components/rule-modal/_rule-modal.scss b/src/newsreader/scss/pages/rules/components/rule-modal/_rule-modal.scss new file mode 100644 index 0000000..0dc53bb --- /dev/null +++ b/src/newsreader/scss/pages/rules/components/rule-modal/_rule-modal.scss @@ -0,0 +1,27 @@ +.rule-modal { + display: flex; + flex-direction: column; + align-self: center; + + margin: 20px 0; + width: 50%; + + border-radius: 2px; + 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/pages/rules/components/rule-modal/index.scss b/src/newsreader/scss/pages/rules/components/rule-modal/index.scss new file mode 100644 index 0000000..c19060f --- /dev/null +++ b/src/newsreader/scss/pages/rules/components/rule-modal/index.scss @@ -0,0 +1 @@ +@import "rule-modal"; diff --git a/src/newsreader/scss/pages/rules/components/rules/_rules.scss b/src/newsreader/scss/pages/rules/components/rules/_rules.scss new file mode 100644 index 0000000..3fd4c22 --- /dev/null +++ b/src/newsreader/scss/pages/rules/components/rules/_rules.scss @@ -0,0 +1,7 @@ +.rules { + &__item { + & > * { + margin: 0; + } + } +} diff --git a/src/newsreader/scss/pages/rules/components/rules/index.scss b/src/newsreader/scss/pages/rules/components/rules/index.scss new file mode 100644 index 0000000..e6a0ebf --- /dev/null +++ b/src/newsreader/scss/pages/rules/components/rules/index.scss @@ -0,0 +1 @@ +@import "rules"; diff --git a/src/newsreader/scss/pages/rules/elements/index.scss b/src/newsreader/scss/pages/rules/elements/index.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/newsreader/scss/pages/rules/index.scss b/src/newsreader/scss/pages/rules/index.scss new file mode 100644 index 0000000..16b6493 --- /dev/null +++ b/src/newsreader/scss/pages/rules/index.scss @@ -0,0 +1,8 @@ +// General imports +@import "../../partials/variables"; +@import "../../components/index"; +@import "../../elements/index"; + +// Page specific +@import "./components/index"; +@import "./elements/index"; diff --git a/src/newsreader/templates/base.html b/src/newsreader/templates/base.html index 445d507..087aca7 100644 --- a/src/newsreader/templates/base.html +++ b/src/newsreader/templates/base.html @@ -11,7 +11,7 @@ {% if request.user.is_authenticated %} - + {% else %} diff --git a/src/newsreader/urls.py b/src/newsreader/urls.py index be59ad1..fb68235 100644 --- a/src/newsreader/urls.py +++ b/src/newsreader/urls.py @@ -6,8 +6,9 @@ from rest_framework_swagger.views import get_swagger_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 +from newsreader.news.core.urls import urlpatterns as core_patterns schema_view = get_swagger_view(title="Newsreader API") @@ -18,7 +19,8 @@ endpoints = [ ] urlpatterns = [ - path("", include(urlpatterns)), + path("", include(core_patterns)), + path("", include(collection_patterns)), path("accounts/", include((login_urls, "accounts")), name="accounts"), path("admin/", admin.site.urls, name="admin"), path("api/", include((endpoints, "api")), name="api"), From 8acf3bbc7f1078cf7b4e56967d29d757cc4d6822 Mon Sep 17 00:00:00 2001 From: sonny Date: Tue, 31 Dec 2019 14:46:42 +0100 Subject: [PATCH 032/364] Rerun black --- src/newsreader/news/core/forms.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/newsreader/news/core/forms.py b/src/newsreader/news/core/forms.py index 8fa2221..5c14fab 100644 --- a/src/newsreader/news/core/forms.py +++ b/src/newsreader/news/core/forms.py @@ -17,9 +17,7 @@ class CategoryForm(forms.ModelForm): super().__init__(*args, **kwargs) if self.user: - self.fields["rules"].queryset = CollectionRule.objects.filter( - user=self.user - ) + self.fields["rules"].queryset = CollectionRule.objects.filter(user=self.user) def save(self, commit=True) -> Category: instance = super().save(commit=False) From 5a630ea4b3d5197b9eb289aea0184cdffa95f7b8 Mon Sep 17 00:00:00 2001 From: Sonny Date: Wed, 1 Jan 2020 16:38:10 +0100 Subject: [PATCH 033/364] Various changes - Update npm dependencies - Update default fixture to exclude default celery task --- package-lock.json | 1857 ++++++++---------- package.json | 38 +- src/newsreader/fixtures/default-fixture.json | 27 - 3 files changed, 792 insertions(+), 1130 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2b98192..a7433b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,36 +5,45 @@ "requires": true, "dependencies": { "@babel/code-frame": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", - "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==", + "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.5.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.5.4.tgz", - "integrity": "sha512-+DaeBEpYq6b2+ZmHx3tHspC+ZRflrvLqwfv8E3hNr5LVQoyBnL8RPKSBCg+rK2W2My9PWlujBiqd0ZPsR9Q6zQ==", + "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.0.0", - "@babel/generator": "^7.5.0", - "@babel/helpers": "^7.5.4", - "@babel/parser": "^7.5.0", - "@babel/template": "^7.4.4", - "@babel/traverse": "^7.5.0", - "@babel/types": "^7.5.0", - "convert-source-map": "^1.1.0", + "@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.11", + "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", @@ -53,255 +62,170 @@ } }, "@babel/generator": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.5.0.tgz", - "integrity": "sha512-1TTVrt7J9rcG5PMjvO7VEG3FrEoEJNHxumRq66GemPmzboLWtIjjcJgk8rokuAS7IiRSpgVSu5Vb9lc99iJkOA==", + "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.5.0", + "@babel/types": "^7.7.4", "jsesc": "^2.5.1", - "lodash": "^4.17.11", - "source-map": "^0.5.0", - "trim-right": "^1.0.1" + "lodash": "^4.17.13", + "source-map": "^0.5.0" } }, "@babel/helper-annotate-as-pure": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.0.0.tgz", - "integrity": "sha512-3UYcJUj9kvSLbLbUIfQTqzcy5VX7GRZ/CCDrnOaZorFFM01aXp1+GJwuFGV4NDDoAS+mOUyHcO6UD/RfqOks3Q==", + "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.0.0" + "@babel/types": "^7.7.4" } }, "@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.1.0.tgz", - "integrity": "sha512-qNSR4jrmJ8M1VMM9tibvyRAHXQs2PmaksQF7c1CGJNipfe3D8p+wgNwgso/P2A2r2mdgBWAXljNWR0QRZAMW8w==", + "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.1.0", - "@babel/types": "^7.0.0" + "@babel/helper-explode-assignable-expression": "^7.7.4", + "@babel/types": "^7.7.4" } }, "@babel/helper-builder-react-jsx": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.3.0.tgz", - "integrity": "sha512-MjA9KgwCuPEkQd9ncSXvSyJ5y+j2sICHyrI0M3L+6fnS4wMSNDc1ARXsbTfbb2cXHn17VisSnU/sHFTCxVxSMw==", + "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.3.0", + "@babel/types": "^7.7.4", "esutils": "^2.0.0" } }, "@babel/helper-call-delegate": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/helper-call-delegate/-/helper-call-delegate-7.4.4.tgz", - "integrity": "sha512-l79boDFJ8S1c5hvQvG+rc+wHw6IuH7YldmRKsYtpbawsxURu/paVy57FZMomGK22/JckepaikOkY0MoAmdyOlQ==", + "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.4.4", - "@babel/traverse": "^7.4.4", - "@babel/types": "^7.4.4" + "@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.5.5", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.5.5.tgz", - "integrity": "sha512-ZsxkyYiRA7Bg+ZTRpPvB6AbOFKTFFK4LrvTet8lInm0V468MWCaSYJE+I7v2z2r8KNLtYiV+K5kTCnR7dvyZjg==", + "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.1.0", - "@babel/helper-member-expression-to-functions": "^7.5.5", - "@babel/helper-optimise-call-expression": "^7.0.0", + "@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.5.5", - "@babel/helper-split-export-declaration": "^7.4.4" - }, - "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/generator": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.5.5.tgz", - "integrity": "sha512-ETI/4vyTSxTzGnU2c49XHv2zhExkv9JHLTwDAFz85kmcwuShvYG2H08FwgIguQf4JC75CBnXAUM5PqeF4fj0nQ==", - "dev": true, - "requires": { - "@babel/types": "^7.5.5", - "jsesc": "^2.5.1", - "lodash": "^4.17.13", - "source-map": "^0.5.0", - "trim-right": "^1.0.1" - } - }, - "@babel/helper-member-expression-to-functions": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.5.5.tgz", - "integrity": "sha512-5qZ3D1uMclSNqYcXqiHoA0meVdv+xUEex9em2fqMnrk/scphGlGgg66zjMrPJESPwrFJ6sbfFQYUSa0Mz7FabA==", - "dev": true, - "requires": { - "@babel/types": "^7.5.5" - } - }, - "@babel/helper-replace-supers": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.5.5.tgz", - "integrity": "sha512-XvRFWrNnlsow2u7jXDuH4jDDctkxbS7gXssrP4q2nUD606ukXHRvydj346wmNg+zAgpFx4MWf4+usfC93bElJg==", - "dev": true, - "requires": { - "@babel/helper-member-expression-to-functions": "^7.5.5", - "@babel/helper-optimise-call-expression": "^7.0.0", - "@babel/traverse": "^7.5.5", - "@babel/types": "^7.5.5" - } - }, - "@babel/parser": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.5.5.tgz", - "integrity": "sha512-E5BN68cqR7dhKan1SfqgPGhQ178bkVKpXTPEXnFJBrEt8/DKRZlybmy+IgYLTeN7tp1R5Ccmbm2rBk17sHYU3g==", - "dev": true - }, - "@babel/traverse": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.5.5.tgz", - "integrity": "sha512-MqB0782whsfffYfSjH4TM+LMjrJnhCNEDMDIjeTpl+ASaUvxcjoiVCo/sM1GhS1pHOXYfWVCYneLjMckuUxDaQ==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.5.5", - "@babel/generator": "^7.5.5", - "@babel/helper-function-name": "^7.1.0", - "@babel/helper-split-export-declaration": "^7.4.4", - "@babel/parser": "^7.5.5", - "@babel/types": "^7.5.5", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.13" - } - }, - "@babel/types": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.5.5.tgz", - "integrity": "sha512-s63F9nJioLqOlW3UkyMd+BYhXt44YuaFm/VV0VwuteqjYwRrObkU7ra9pY4wAJR3oXi8hJrMcrcJdO/HH33vtw==", - "dev": true, - "requires": { - "esutils": "^2.0.2", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - } - }, - "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/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.4.4", - "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.4.4.tgz", - "integrity": "sha512-IX3Ln8gLhZpSuqHJSnTNBWGDE9kdkTEWl21A/K7PQ00tseBwbqCHTvNLHSBd9M0R5rER4h5Rsvj9vw0R5SieBg==", + "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.1.0", - "@babel/types": "^7.4.4", - "lodash": "^4.17.11" + "@babel/helper-function-name": "^7.7.4", + "@babel/types": "^7.7.4", + "lodash": "^4.17.13" } }, "@babel/helper-explode-assignable-expression": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.1.0.tgz", - "integrity": "sha512-NRQpfHrJ1msCHtKjbzs9YcMmJZOg6mQMmGRB+hbamEdG5PNpaSm95275VD92DvJKuyl0s2sFiDmMZ+EnnvufqA==", + "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.1.0", - "@babel/types": "^7.0.0" + "@babel/traverse": "^7.7.4", + "@babel/types": "^7.7.4" } }, "@babel/helper-function-name": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz", - "integrity": "sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw==", + "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.0.0", - "@babel/template": "^7.1.0", - "@babel/types": "^7.0.0" + "@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.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz", - "integrity": "sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ==", + "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.0.0" + "@babel/types": "^7.7.4" } }, "@babel/helper-hoist-variables": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.4.4.tgz", - "integrity": "sha512-VYk2/H/BnYbZDDg39hr3t2kKyifAm1W6zHRfhx8jGjIHpQEBv9dry7oQ2f3+J703TLu69nYdxsovl0XYfcnK4w==", + "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.4.4" + "@babel/types": "^7.7.4" } }, "@babel/helper-member-expression-to-functions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.0.0.tgz", - "integrity": "sha512-avo+lm/QmZlv27Zsi0xEor2fKcqWG56D5ae9dzklpIaY7cQMK5N8VSpaNVPPagiqmy7LrEjK1IWdGMOqPu5csg==", + "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.0.0" + "@babel/types": "^7.7.4" } }, "@babel/helper-module-imports": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.0.0.tgz", - "integrity": "sha512-aP/hlLq01DWNEiDg4Jn23i+CXxW/owM4WpDLFUbpjxe4NS3BhLVZQ5i7E0ZrxuQ/vwekIeciyamgB1UIYxxM6A==", + "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.0.0" + "@babel/types": "^7.7.4" } }, "@babel/helper-module-transforms": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.4.4.tgz", - "integrity": "sha512-3Z1yp8TVQf+B4ynN7WoHPKS8EkdTbgAEy0nU0rs/1Kw4pDgmvYH3rz3aI11KgxKCba2cn7N+tqzV1mY2HMN96w==", + "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.0.0", - "@babel/helper-simple-access": "^7.1.0", - "@babel/helper-split-export-declaration": "^7.4.4", - "@babel/template": "^7.4.4", - "@babel/types": "^7.4.4", - "lodash": "^4.17.11" + "@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.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.0.0.tgz", - "integrity": "sha512-u8nd9NQePYNQV8iPWu/pLLYBqZBa4ZaY1YWRFMuxrid94wKI1QNt67NEZ7GAe5Kc/0LLScbim05xZFWkAdrj9g==", + "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.0.0" + "@babel/types": "^7.7.4" } }, "@babel/helper-plugin-utils": { @@ -311,79 +235,79 @@ "dev": true }, "@babel/helper-regex": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/helper-regex/-/helper-regex-7.4.4.tgz", - "integrity": "sha512-Y5nuB/kESmR3tKjU8Nkn1wMGEx1tjJX076HBMeL3XLQCu6vA/YRzuTW0bbb+qRnXvQGn+d6Rx953yffl8vEy7Q==", + "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.11" + "lodash": "^4.17.13" } }, "@babel/helper-remap-async-to-generator": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.1.0.tgz", - "integrity": "sha512-3fOK0L+Fdlg8S5al8u/hWE6vhufGSn0bN09xm2LXMy//REAF8kDCrYoOBKYmA8m5Nom+sV9LyLCwrFynA8/slg==", + "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.0.0", - "@babel/helper-wrap-function": "^7.1.0", - "@babel/template": "^7.1.0", - "@babel/traverse": "^7.1.0", - "@babel/types": "^7.0.0" + "@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.4.4", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.4.4.tgz", - "integrity": "sha512-04xGEnd+s01nY1l15EuMS1rfKktNF+1CkKmHoErDppjAAZL+IUBZpzT748x262HF7fibaQPhbvWUl5HeSt1EXg==", + "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.0.0", - "@babel/helper-optimise-call-expression": "^7.0.0", - "@babel/traverse": "^7.4.4", - "@babel/types": "^7.4.4" + "@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.1.0", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.1.0.tgz", - "integrity": "sha512-Vk+78hNjRbsiu49zAPALxTb+JUQCz1aolpd8osOF16BGnLtseD21nbHgLPGUwrXEurZgiCOUmvs3ExTu4F5x6w==", + "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.1.0", - "@babel/types": "^7.0.0" + "@babel/template": "^7.7.4", + "@babel/types": "^7.7.4" } }, "@babel/helper-split-export-declaration": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz", - "integrity": "sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q==", + "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.4.4" + "@babel/types": "^7.7.4" } }, "@babel/helper-wrap-function": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.2.0.tgz", - "integrity": "sha512-o9fP1BZLLSrYlxYEYyl2aS+Flun5gtjTIG8iln+XuEzQTs0PLagAGSXUcqruJwD5fM48jzIEggCKpIfWTcR7pQ==", + "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.1.0", - "@babel/template": "^7.1.0", - "@babel/traverse": "^7.1.0", - "@babel/types": "^7.2.0" + "@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.5.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.5.4.tgz", - "integrity": "sha512-6LJ6xwUEJP51w0sIgKyfvFMJvIb9mWAfohJp0+m6eHJigkFdcH8duZ1sfhn0ltJRzwUIT/yqqhdSfRpCpL7oow==", + "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.4.4", - "@babel/traverse": "^7.5.0", - "@babel/types": "^7.5.0" + "@babel/template": "^7.7.4", + "@babel/traverse": "^7.7.4", + "@babel/types": "^7.7.4" } }, "@babel/highlight": { @@ -398,451 +322,458 @@ } }, "@babel/parser": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.5.0.tgz", - "integrity": "sha512-I5nW8AhGpOXGCCNYGc+p7ExQIBxRFnS2fd/d862bNOKvmoEPjYPcfIjsfdy0ujagYOIYPczKgD9l3FsgTkAzKA==", + "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.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.2.0.tgz", - "integrity": "sha512-+Dfo/SCQqrwx48ptLVGLdE39YtWRuKc/Y9I5Fy0P1DDBB9lsAHpjcEJQt+4IifuSOSTLBKJObJqMvaO1pIE8LQ==", + "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.1.0", - "@babel/plugin-syntax-async-generators": "^7.2.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.5.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.5.5.tgz", - "integrity": "sha512-AF79FsnWFxjlaosgdi421vmYG6/jg79bVD0dpD44QdgobzHKuLZ6S3vl8la9qIeSwGi8i1fS0O1mfuDAAdo1/A==", + "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.5.5", + "@babel/helper-create-class-features-plugin": "^7.7.4", "@babel/helper-plugin-utils": "^7.0.0" } }, "@babel/plugin-proposal-dynamic-import": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.5.0.tgz", - "integrity": "sha512-x/iMjggsKTFHYC6g11PL7Qy58IK8H5zqfm9e6hu4z1iH2IRyAp9u9dL80zA6R76yFovETFLKz2VJIC2iIPBuFw==", + "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.2.0" + "@babel/plugin-syntax-dynamic-import": "^7.7.4" } }, "@babel/plugin-proposal-function-bind": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-function-bind/-/plugin-proposal-function-bind-7.2.0.tgz", - "integrity": "sha512-qOFJ/eX1Is78sywwTxDcsntLOdb5ZlHVVqUz5xznq8ldAfOVIyZzp1JE2rzHnaksZIhrqMrwIpQL/qcEprnVbw==", + "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.2.0" + "@babel/plugin-syntax-function-bind": "^7.7.4" } }, "@babel/plugin-proposal-json-strings": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.2.0.tgz", - "integrity": "sha512-MAFV1CA/YVmYwZG0fBQyXhmj0BHCB5egZHCKWIFVv/XCxAeVGIHfos3SwDck4LvCllENIAg7xMKOG5kH0dzyUg==", + "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.2.0" + "@babel/plugin-syntax-json-strings": "^7.7.4" } }, "@babel/plugin-proposal-object-rest-spread": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.5.4.tgz", - "integrity": "sha512-KCx0z3y7y8ipZUMAEEJOyNi11lMb/FOPUjjB113tfowgw0c16EGYos7worCKBcUAh2oG+OBnoUhsnTSoLpV9uA==", + "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.2.0" + "@babel/plugin-syntax-object-rest-spread": "^7.7.4" } }, "@babel/plugin-proposal-optional-catch-binding": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.2.0.tgz", - "integrity": "sha512-mgYj3jCcxug6KUcX4OBoOJz3CMrwRfQELPQ5560F70YQUBZB7uac9fqaWamKR1iWUzGiK2t0ygzjTScZnVz75g==", + "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.2.0" + "@babel/plugin-syntax-optional-catch-binding": "^7.7.4" } }, "@babel/plugin-proposal-unicode-property-regex": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.4.4.tgz", - "integrity": "sha512-j1NwnOqMG9mFUOH58JTFsA/+ZYzQLUZ/drqWUqxCYLGeu2JFZL8YrNC9hBxKmWtAuOCHPcRpgv7fhap09Fb4kA==", + "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-plugin-utils": "^7.0.0", - "@babel/helper-regex": "^7.4.4", - "regexpu-core": "^4.5.4" + "@babel/helper-create-regexp-features-plugin": "^7.7.4", + "@babel/helper-plugin-utils": "^7.0.0" } }, "@babel/plugin-syntax-async-generators": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.2.0.tgz", - "integrity": "sha512-1ZrIRBv2t0GSlcwVoQ6VgSLpLgiN/FVQUzt9znxo7v2Ov4jJrs8RY8tv0wvDmFN3qIdMKWrmMMW6yZ0G19MfGg==", + "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.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.2.0.tgz", - "integrity": "sha512-mVxuJ0YroI/h/tbFTPGZR8cv6ai+STMKNBq0f8hFxsxWjl94qqhsb+wXbpNMDPU3cfR1TIsVFzU3nXyZMqyK4w==", + "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.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-function-bind/-/plugin-syntax-function-bind-7.2.0.tgz", - "integrity": "sha512-/WzU1lLU2l0wDfB42Wkg6tahrmtBbiD8C4H6EGSX0M4GAjzN6JiOpq/Uh8G6GSoR6lPMvhjM0MNiV6znj6y/zg==", + "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.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.2.0.tgz", - "integrity": "sha512-5UGYnMSLRE1dqqZwug+1LISpA403HzlSfsg6P9VXU6TBjcSHeNlw4DxDx7LgpF+iKZoOG/+uzqoRHTdcUpiZNg==", + "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.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.2.0.tgz", - "integrity": "sha512-VyN4QANJkRW6lDBmENzRszvZf3/4AXaj9YR7GwrWeeN9tEBPuXbmDYVU9bYBN0D70zCWVwUy0HWq2553VCb6Hw==", + "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.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.2.0.tgz", - "integrity": "sha512-t0JKGgqk2We+9may3t0xDdmneaXmyxq0xieYcKHxIsrJO64n1OiMWNUtc5gQK1PA0NpdCRrtZp4z+IUaKugrSA==", + "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.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.2.0.tgz", - "integrity": "sha512-bDe4xKNhb0LI7IvZHiA13kff0KEfaGX/Hv4lMA9+7TEc63hMNvfKo6ZFpXhKuEp+II/q35Gc4NoMeDZyaUbj9w==", + "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.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.2.0.tgz", - "integrity": "sha512-ER77Cax1+8/8jCB9fo4Ud161OZzWN5qawi4GusDuRLcDbDG+bIGYY20zb2dfAFdTRGzrfq2xZPvF0R64EHnimg==", + "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.5.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.5.0.tgz", - "integrity": "sha512-mqvkzwIGkq0bEF1zLRRiTdjfomZJDV33AH3oQzHVGkI2VzEmXLpKKOBvEVaFZBJdN0XTyH38s9j/Kiqr68dggg==", + "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.0.0", + "@babel/helper-module-imports": "^7.7.4", "@babel/helper-plugin-utils": "^7.0.0", - "@babel/helper-remap-async-to-generator": "^7.1.0" + "@babel/helper-remap-async-to-generator": "^7.7.4" } }, "@babel/plugin-transform-block-scoped-functions": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.2.0.tgz", - "integrity": "sha512-ntQPR6q1/NKuphly49+QiQiTN0O63uOwjdD6dhIjSWBI5xlrbUFh720TIpzBhpnrLfv2tNH/BXvLIab1+BAI0w==", + "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.4.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.4.4.tgz", - "integrity": "sha512-jkTUyWZcTrwxu5DD4rWz6rDB5Cjdmgz6z7M7RLXOJyCUkFBawssDGcGh8M/0FTSB87avyJI1HsTwUXp9nKA1PA==", + "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.11" + "lodash": "^4.17.13" } }, "@babel/plugin-transform-classes": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.4.4.tgz", - "integrity": "sha512-/e44eFLImEGIpL9qPxSRat13I5QNRgBLu2hOQJCF7VLy/otSM/sypV1+XaIw5+502RX/+6YaSAPmldk+nhHDPw==", + "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.0.0", - "@babel/helper-define-map": "^7.4.4", - "@babel/helper-function-name": "^7.1.0", - "@babel/helper-optimise-call-expression": "^7.0.0", + "@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.4.4", - "@babel/helper-split-export-declaration": "^7.4.4", + "@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.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.2.0.tgz", - "integrity": "sha512-kP/drqTxY6Xt3NNpKiMomfgkNn4o7+vKxK2DDKcBG9sHj51vHqMBGy8wbDS/J4lMxnqs153/T3+DmCEAkC5cpA==", + "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.5.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.5.0.tgz", - "integrity": "sha512-YbYgbd3TryYYLGyC7ZR+Tq8H/+bCmwoaxHfJHupom5ECstzbRLTch6gOQbhEY9Z4hiCNHEURgq06ykFv9JZ/QQ==", + "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.4.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.4.4.tgz", - "integrity": "sha512-P05YEhRc2h53lZDjRPk/OektxCVevFzZs2Gfjd545Wde3k+yFDbXORgl2e0xpbq8mLcKJ7Idss4fAg0zORN/zg==", + "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-plugin-utils": "^7.0.0", - "@babel/helper-regex": "^7.4.4", - "regexpu-core": "^4.5.4" + "@babel/helper-create-regexp-features-plugin": "^7.7.4", + "@babel/helper-plugin-utils": "^7.0.0" } }, "@babel/plugin-transform-duplicate-keys": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.5.0.tgz", - "integrity": "sha512-igcziksHizyQPlX9gfSjHkE2wmoCH3evvD2qR5w29/Dk0SMKE/eOI7f1HhBdNhR/zxJDqrgpoDTq5YSLH/XMsQ==", + "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.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.2.0.tgz", - "integrity": "sha512-umh4hR6N7mu4Elq9GG8TOu9M0bakvlsREEC+ialrQN6ABS4oDQ69qJv1VtR3uxlKMCQMCvzk7vr17RHKcjx68A==", + "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.1.0", + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.7.4", "@babel/helper-plugin-utils": "^7.0.0" } }, "@babel/plugin-transform-for-of": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.4.4.tgz", - "integrity": "sha512-9T/5Dlr14Z9TIEXLXkt8T1DU7F24cbhwhMNUziN3hB1AXoZcdzPcTiKGRn/6iOymDqtTKWnr/BtRKN9JwbKtdQ==", + "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.4.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.4.4.tgz", - "integrity": "sha512-iU9pv7U+2jC9ANQkKeNF6DrPy4GBa4NWQtl6dHB4Pb3izX2JOEvDTFarlNsBj/63ZEzNNIAMs3Qw4fNCcSOXJA==", + "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.1.0", + "@babel/helper-function-name": "^7.7.4", "@babel/helper-plugin-utils": "^7.0.0" } }, "@babel/plugin-transform-literals": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.2.0.tgz", - "integrity": "sha512-2ThDhm4lI4oV7fVQ6pNNK+sx+c/GM5/SaML0w/r4ZB7sAneD/piDJtwdKlNckXeyGK7wlwg2E2w33C/Hh+VFCg==", + "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.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.2.0.tgz", - "integrity": "sha512-HiU3zKkSU6scTidmnFJ0bMX8hz5ixC93b4MHMiYebmk2lUVNGOboPsqQvx5LzooihijUoLR/v7Nc1rbBtnc7FA==", + "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.5.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.5.0.tgz", - "integrity": "sha512-n20UsQMKnWrltocZZm24cRURxQnWIvsABPJlw/fvoy9c6AgHZzoelAIzajDHAQrDpuKFFPPcFGd7ChsYuIUMpg==", + "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.1.0", + "@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.5.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.5.0.tgz", - "integrity": "sha512-xmHq0B+ytyrWJvQTc5OWAC4ii6Dhr0s22STOoydokG51JjWhyYo5mRPXoi+ZmtHQhZZwuXNN+GG5jy5UZZJxIQ==", + "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.4.4", + "@babel/helper-module-transforms": "^7.7.5", "@babel/helper-plugin-utils": "^7.0.0", - "@babel/helper-simple-access": "^7.1.0", + "@babel/helper-simple-access": "^7.7.4", "babel-plugin-dynamic-import-node": "^2.3.0" } }, "@babel/plugin-transform-modules-systemjs": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.5.0.tgz", - "integrity": "sha512-Q2m56tyoQWmuNGxEtUyeEkm6qJYFqs4c+XyXH5RAuYxObRNz9Zgj/1g2GMnjYp2EUyEy7YTrxliGCXzecl/vJg==", + "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.4.4", + "@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.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.2.0.tgz", - "integrity": "sha512-BV3bw6MyUH1iIsGhXlOK6sXhmSarZjtJ/vMiD9dNmpY8QXFFQTj+6v92pcfy1iqa8DeAfJFwoxcrS/TUZda6sw==", + "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.1.0", + "@babel/helper-module-transforms": "^7.7.4", "@babel/helper-plugin-utils": "^7.0.0" } }, "@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.4.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.4.5.tgz", - "integrity": "sha512-z7+2IsWafTBbjNsOxU/Iv5CvTJlr5w4+HGu1HovKYTtgJ362f7kBcQglkfmlspKKZ3bgrbSGvLfNx++ZJgCWsg==", + "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": { - "regexp-tree": "^0.1.6" + "@babel/helper-create-regexp-features-plugin": "^7.7.4" } }, "@babel/plugin-transform-new-target": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.4.4.tgz", - "integrity": "sha512-r1z3T2DNGQwwe2vPGZMBNjioT2scgWzK9BCnDEh+46z8EEwXBq24uRzd65I7pjtugzPSj921aM15RpESgzsSuA==", + "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.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.2.0.tgz", - "integrity": "sha512-VMyhPYZISFZAqAPVkiYb7dUe2AsVi2/wCT5+wZdsNO31FojQJa9ns40hzZ6U9f50Jlq4w6qwzdBB2uwqZ00ebg==", + "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.1.0" + "@babel/helper-replace-supers": "^7.7.4" } }, "@babel/plugin-transform-parameters": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.4.4.tgz", - "integrity": "sha512-oMh5DUO1V63nZcu/ZVLQFqiihBGo4OpxJxR1otF50GMeCLiRx5nUdtokd+u9SuVJrvvuIh9OosRFPP4pIPnwmw==", + "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.4.4", - "@babel/helper-get-function-arity": "^7.0.0", + "@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.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.2.0.tgz", - "integrity": "sha512-9q7Dbk4RhgcLp8ebduOpCbtjh7C0itoLYHXd9ueASKAG/is5PQtMR5VJGka9NKqGhYEGn5ITahd4h9QeBMylWQ==", + "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.3.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.3.0.tgz", - "integrity": "sha512-a/+aRb7R06WcKvQLOu4/TpjKOdvVEKRLWFpKcNuHhiREPgGRB4TQJxq07+EZLS8LFVYpfq1a5lDUnuMdcCpBKg==", + "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.3.0", + "@babel/helper-builder-react-jsx": "^7.7.4", "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-syntax-jsx": "^7.2.0" + "@babel/plugin-syntax-jsx": "^7.7.4" } }, "@babel/plugin-transform-regenerator": { - "version": "7.4.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.4.5.tgz", - "integrity": "sha512-gBKRh5qAaCWntnd09S8QC7r3auLCqq5DI6O0DlfoyDjslSBVqBibrMdsqO+Uhmx3+BlOmE/Kw1HFxmGbv0N9dA==", + "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.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.2.0.tgz", - "integrity": "sha512-fz43fqW8E1tAB3DKF19/vxbpib1fuyCwSPE418ge5ZxILnBhWyhtPgz8eh1RCGGJlwvksHkyxMxh0eenFi+kFw==", + "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.5.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.5.5.tgz", - "integrity": "sha512-6Xmeidsun5rkwnGfMOp6/z9nSzWpHFNVr2Jx7kwoq4mVatQfQx5S56drBgEHF+XQbKOdIaOiMIINvp/kAwMN+w==", + "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.0.0", + "@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.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.2.0.tgz", - "integrity": "sha512-QP4eUM83ha9zmYtpbnyjTLAGKQritA5XW/iG9cjtuOI8s1RuL/3V6a3DeSHfKutJQ+ayUfeZJPcnCYEQzaPQqg==", + "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.2.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.2.2.tgz", - "integrity": "sha512-KWfky/58vubwtS0hLqEnrWJjsMGaOeSBn90Ezn5Jeg9Z8KKHmELbP1yGylMlm5N6TPKeY9A2+UaSYLdxahg01w==", + "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.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.2.0.tgz", - "integrity": "sha512-KKYCoGaRAf+ckH8gEL3JHUaFVyNHKe3ASNsZ+AlktgHevvxGigoIttrEJb8iKN03Q7Eazlv1s6cx2B2cQ3Jabw==", + "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", @@ -850,141 +781,140 @@ } }, "@babel/plugin-transform-template-literals": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.4.4.tgz", - "integrity": "sha512-mQrEC4TWkhLN0z8ygIvEL9ZEToPhG5K7KDW3pzGqOfIGZ28Jb0POUkeWcoz8HnHvhFy6dwAT1j8OzqN8s804+g==", + "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.0.0", + "@babel/helper-annotate-as-pure": "^7.7.4", "@babel/helper-plugin-utils": "^7.0.0" } }, "@babel/plugin-transform-typeof-symbol": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.2.0.tgz", - "integrity": "sha512-2LNhETWYxiYysBtrBTqL8+La0jIoQQnIScUJc74OYvUGRmkskNY4EzLCnjHBzdmb38wqtTaixpo1NctEcvMDZw==", + "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.4.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.4.4.tgz", - "integrity": "sha512-il+/XdNw01i93+M9J9u4T7/e/Ue/vWfNZE4IRUQjplu2Mqb/AFTDimkw2tdEdSH50wuQXZAbXSql0UphQke+vA==", + "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-plugin-utils": "^7.0.0", - "@babel/helper-regex": "^7.4.4", - "regexpu-core": "^4.5.4" + "@babel/helper-create-regexp-features-plugin": "^7.7.4", + "@babel/helper-plugin-utils": "^7.0.0" } }, "@babel/preset-env": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.5.4.tgz", - "integrity": "sha512-hFnFnouyRNiH1rL8YkX1ANCNAUVC8Djwdqfev8i1415tnAG+7hlA5zhZ0Q/3Q5gkop4HioIPbCEWAalqcbxRoQ==", + "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.0.0", + "@babel/helper-module-imports": "^7.7.4", "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-proposal-async-generator-functions": "^7.2.0", - "@babel/plugin-proposal-dynamic-import": "^7.5.0", - "@babel/plugin-proposal-json-strings": "^7.2.0", - "@babel/plugin-proposal-object-rest-spread": "^7.5.4", - "@babel/plugin-proposal-optional-catch-binding": "^7.2.0", - "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", - "@babel/plugin-syntax-async-generators": "^7.2.0", - "@babel/plugin-syntax-dynamic-import": "^7.2.0", - "@babel/plugin-syntax-json-strings": "^7.2.0", - "@babel/plugin-syntax-object-rest-spread": "^7.2.0", - "@babel/plugin-syntax-optional-catch-binding": "^7.2.0", - "@babel/plugin-transform-arrow-functions": "^7.2.0", - "@babel/plugin-transform-async-to-generator": "^7.5.0", - "@babel/plugin-transform-block-scoped-functions": "^7.2.0", - "@babel/plugin-transform-block-scoping": "^7.4.4", - "@babel/plugin-transform-classes": "^7.4.4", - "@babel/plugin-transform-computed-properties": "^7.2.0", - "@babel/plugin-transform-destructuring": "^7.5.0", - "@babel/plugin-transform-dotall-regex": "^7.4.4", - "@babel/plugin-transform-duplicate-keys": "^7.5.0", - "@babel/plugin-transform-exponentiation-operator": "^7.2.0", - "@babel/plugin-transform-for-of": "^7.4.4", - "@babel/plugin-transform-function-name": "^7.4.4", - "@babel/plugin-transform-literals": "^7.2.0", - "@babel/plugin-transform-member-expression-literals": "^7.2.0", - "@babel/plugin-transform-modules-amd": "^7.5.0", - "@babel/plugin-transform-modules-commonjs": "^7.5.0", - "@babel/plugin-transform-modules-systemjs": "^7.5.0", - "@babel/plugin-transform-modules-umd": "^7.2.0", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.4.5", - "@babel/plugin-transform-new-target": "^7.4.4", - "@babel/plugin-transform-object-super": "^7.2.0", - "@babel/plugin-transform-parameters": "^7.4.4", - "@babel/plugin-transform-property-literals": "^7.2.0", - "@babel/plugin-transform-regenerator": "^7.4.5", - "@babel/plugin-transform-reserved-words": "^7.2.0", - "@babel/plugin-transform-shorthand-properties": "^7.2.0", - "@babel/plugin-transform-spread": "^7.2.0", - "@babel/plugin-transform-sticky-regex": "^7.2.0", - "@babel/plugin-transform-template-literals": "^7.4.4", - "@babel/plugin-transform-typeof-symbol": "^7.2.0", - "@babel/plugin-transform-unicode-regex": "^7.4.4", - "@babel/types": "^7.5.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.1.1", + "core-js-compat": "^3.6.0", "invariant": "^2.2.2", "js-levenshtein": "^1.1.3", "semver": "^5.5.0" } }, "@babel/register": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.4.4.tgz", - "integrity": "sha512-sn51H88GRa00+ZoMqCVgOphmswG4b7mhf9VOB0LUBAieykq2GnRFerlN+JQkO/ntT7wz4jaHNSRPg9IdMPEUkA==", + "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": { - "core-js": "^3.0.0", "find-cache-dir": "^2.0.0", - "lodash": "^4.17.11", - "mkdirp": "^0.5.1", + "lodash": "^4.17.13", + "make-dir": "^2.1.0", "pirates": "^4.0.0", - "source-map-support": "^0.5.9" + "source-map-support": "^0.5.16" } }, "@babel/runtime": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.5.5.tgz", - "integrity": "sha512-28QvEGyQyNkB0/m2B4FU7IEZGK2NUrcMtT6BZEFALTguLk+AUT6ofsHtPk5QyjAdUkpMJ+/Em+quwz4HOt30AQ==", + "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.4.4", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.4.4.tgz", - "integrity": "sha512-CiGzLN9KgAvgZsnivND7rkA+AeJ9JB0ciPOD4U59GKbQP2iQl+olF1l76kJOupqidozfZ32ghwBEJDhnk9MEcw==", + "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.4.4", - "@babel/types": "^7.4.4" + "@babel/parser": "^7.7.4", + "@babel/types": "^7.7.4" } }, "@babel/traverse": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.5.0.tgz", - "integrity": "sha512-SnA9aLbyOCcnnbQEGwdfBggnc142h/rbqqsXcaATj2hZcegCl903pUD/lfpsNBlBSuWow/YDfRyJuWi2EPR5cg==", + "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.0.0", - "@babel/generator": "^7.5.0", - "@babel/helper-function-name": "^7.1.0", - "@babel/helper-split-export-declaration": "^7.4.4", - "@babel/parser": "^7.5.0", - "@babel/types": "^7.5.0", + "@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.11" + "lodash": "^4.17.13" }, "dependencies": { "debug": { @@ -1005,39 +935,39 @@ } }, "@babel/types": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.5.0.tgz", - "integrity": "sha512-UFpDVqRABKsW01bvw7/wSUe56uy6RXM5+VJibVVAybDGxEW25jdwiFJEf7ASvSaC7sN7rbE/l3cLp2izav+CtQ==", + "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.11", + "lodash": "^4.17.13", "to-fast-properties": "^2.0.0" } }, "@nodelib/fs.scandir": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.1.tgz", - "integrity": "sha512-NT/skIZjgotDSiXs0WqYhgcuBKhUMgfekCmCGtkUAiLqZdOnrdjmZr9wRl3ll64J9NF79uZ4fk16Dx0yMc/Xbg==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz", + "integrity": "sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw==", "dev": true, "requires": { - "@nodelib/fs.stat": "2.0.1", + "@nodelib/fs.stat": "2.0.3", "run-parallel": "^1.1.9" } }, "@nodelib/fs.stat": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.1.tgz", - "integrity": "sha512-+RqhBlLn6YRBGOIoVYthsG0J9dfpO79eJyN7BYBkZJtfqrBwf2KK+rD/M/yjZR6WBmIhAgOV7S60eCgaSWtbFw==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz", + "integrity": "sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA==", "dev": true }, "@nodelib/fs.walk": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.2.tgz", - "integrity": "sha512-J/DR3+W12uCzAJkw7niXDcqcKBg6+5G5Q/ZpThpGNzAUz70eOR6RV4XnnSN01qHZiVl0eavoxJsBypQoKsV2QQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz", + "integrity": "sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ==", "dev": true, "requires": { - "@nodelib/fs.scandir": "2.1.1", + "@nodelib/fs.scandir": "2.1.3", "fastq": "^1.6.0" } }, @@ -1065,9 +995,9 @@ "dev": true }, "@types/node": { - "version": "12.6.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.6.3.tgz", - "integrity": "sha512-7TEYTQT1/6PP53NftXXabIZDaZfaoBdeBm8Md/i7zsWRoBe0YwOXguyK8vhHs8ehgB/w9U4K/6EWuTyp0W6nIA==", + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-13.1.2.tgz", + "integrity": "sha512-B8emQA1qeKerqd1dmIsQYnXi+mmAzTB7flExjmy5X1aVAKFNNNDubkavwR13kR6JnpeLp3aLoJhwn9trWPAyFQ==", "dev": true }, "JSONStream": { @@ -1087,35 +1017,38 @@ "dev": true }, "acorn": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.2.1.tgz", - "integrity": "sha512-JD0xT5FCRDNyjDda3Lrg/IxFscp9q4tiYtxE1/nOzlKCk7hIRuYjhq1kCNkbPjMRMZuFq20HNQn1I9k8Oj0E+Q==", - "dev": true - }, - "acorn-dynamic-import": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/acorn-dynamic-import/-/acorn-dynamic-import-4.0.0.tgz", - "integrity": "sha512-d3OEjQV4ROpoflsnUA8HozoIR504TFxNivYEUi6uwz0IYhBkTDXGuWlNdMtybRt3nqVx/L6XqMt0FxkXuWKZhw==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.0.tgz", + "integrity": "sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ==", "dev": true }, "acorn-node": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.7.0.tgz", - "integrity": "sha512-XhahLSsCB6X6CJbe+uNu3Mn9sJBNFxtBN9NLgAOQovfS6Kh0lDUtmlclhjn9CvEK7A7YyRU13PXlNcpSiLI9Yw==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", + "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==", "dev": true, "requires": { - "acorn": "^6.1.1", - "acorn-dynamic-import": "^4.0.0", - "acorn-walk": "^6.1.1", - "xtend": "^4.0.1" + "acorn": "^7.0.0", + "acorn-walk": "^7.0.0", + "xtend": "^4.0.2" } }, "acorn-walk": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz", - "integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.0.0.tgz", + "integrity": "sha512-7Bv1We7ZGuU79zZbb6rRqcpxo3OY+zrdtloZWoyD8fmGX+FeXRjE+iuGkZjSXLVovLzrsvMGMy0EkwA0E0umxg==", "dev": true }, + "aggregate-error": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.0.1.tgz", + "integrity": "sha512-quoaXsZ9/BLNae5yiNoUz+Nhkwz83GhWwtYFglcjEQB2NDHCIpApbqXxIFnm4Pq/Nvhrsq5sYJFyohrrxnTGAA==", + "dev": true, + "requires": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + } + }, "ajv": { "version": "6.10.2", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", @@ -1256,12 +1189,6 @@ "integrity": "sha1-p5SvDAWrF1KEbudTofIRoFugxE8=", "dev": true }, - "array-filter": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/array-filter/-/array-filter-0.0.1.tgz", - "integrity": "sha1-fajPLiZijtcygDWB/SH2fKzS7uw=", - "dev": true - }, "array-find-index": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", @@ -1303,18 +1230,6 @@ } } }, - "array-map": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/array-map/-/array-map-0.0.0.tgz", - "integrity": "sha1-iKK6tz0c97zVwbEYoAP2b2ZfpmI=", - "dev": true - }, - "array-reduce": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/array-reduce/-/array-reduce-0.0.0.tgz", - "integrity": "sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys=", - "dev": true - }, "array-slice": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", @@ -1463,9 +1378,9 @@ "dev": true }, "aws4": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", + "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-plugin-dynamic-import-node": { @@ -1562,9 +1477,9 @@ } }, "base64-js": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", - "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==", + "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": { @@ -1684,9 +1599,9 @@ } }, "browserify": { - "version": "16.3.0", - "resolved": "https://registry.npmjs.org/browserify/-/browserify-16.3.0.tgz", - "integrity": "sha512-BWaaD7alyGZVEBBwSTYx4iJF5DswIGzK17o8ai9w4iKRbYpk3EOiprRHMRRA8DCZFmFeOdx7A385w2XdFvxWmg==", + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/browserify/-/browserify-16.5.0.tgz", + "integrity": "sha512-6bfI3cl76YLAnCZ75AGu/XPOsqUhRyc0F/olGIJeCxtfxF2HvPKEcmjU9M8oAPxl4uBY1U7Nry33Q6koV3f2iw==", "dev": true, "requires": { "JSONStream": "^1.0.3", @@ -1726,7 +1641,7 @@ "shasum": "^1.0.0", "shell-quote": "^1.6.1", "stream-browserify": "^2.0.0", - "stream-http": "^2.0.0", + "stream-http": "^3.0.0", "string_decoder": "^1.1.1", "subarg": "^1.0.0", "syntax-error": "^1.1.1", @@ -1737,14 +1652,6 @@ "util": "~0.10.1", "vm-browserify": "^1.0.0", "xtend": "^4.0.0" - }, - "dependencies": { - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", - "dev": true - } } }, "browserify-aes": { @@ -1819,20 +1726,20 @@ } }, "browserslist": { - "version": "4.6.6", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.6.6.tgz", - "integrity": "sha512-D2Nk3W9JL9Fp/gIcWei8LrERCS+eXu9AM5cfXA8WEZ84lFks+ARnZ0q/R69m2SV3Wjma83QDDPxsNKXUwdIsyA==", + "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.30000984", - "electron-to-chromium": "^1.3.191", - "node-releases": "^1.1.25" + "caniuse-lite": "^1.0.30001015", + "electron-to-chromium": "^1.3.322", + "node-releases": "^1.1.42" } }, "buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.2.1.tgz", - "integrity": "sha512-c+Ko0loDaFfuPWiL02ls9Xd3GO3cPVmUobQ6t3rXNUk304u6hGq+8N/kFi+QEIKhzK3uwolVhLzszmfLmMLnqg==", + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.4.3.tgz", + "integrity": "sha512-zvj65TkFeIt3i6aj5bIvJDzjjQQGs4o/sNoezg1F1kYap9Nu2jcUdpwzRSJTHMMzG0H7bZkn4rNQpImhuxWX2A==", "dev": true, "requires": { "base64-js": "^1.0.2", @@ -1911,9 +1818,9 @@ } }, "caniuse-lite": { - "version": "1.0.30000984", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000984.tgz", - "integrity": "sha512-n5tKOjMaZ1fksIpQbjERuqCyfgec/m9pferkFQbLmWtqLUdmt12hNhjSwsmPdqeiG2NkITOQhr1VYIwWSAceiA==", + "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 }, "caseless": { @@ -1994,6 +1901,12 @@ } } }, + "clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true + }, "cliui": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", @@ -2159,13 +2072,10 @@ } }, "console-browserify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", - "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", - "dev": true, - "requires": { - "date-now": "^0.1.4" - } + "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", @@ -2204,37 +2114,24 @@ "is-plain-object": "^2.0.1" } }, - "core-js": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.1.4.tgz", - "integrity": "sha512-YNZN8lt82XIMLnLirj9MhKDFZHalwzzrL9YLt6eb0T5D0EDl4IQ90IGkua8mHbnxNrkj1d8hbdizMc0Qmg1WnQ==", - "dev": true - }, "core-js-compat": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.1.4.tgz", - "integrity": "sha512-Z5zbO9f1d0YrJdoaQhphVAnKPimX92D6z8lCGphH89MNRxlL1prI9ExJPqVwP0/kgkQCv8c4GJGT8X16yUncOg==", + "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.6.2", - "core-js-pure": "3.1.4", - "semver": "^6.1.1" + "browserslist": "^4.8.2", + "semver": "7.0.0" }, "dependencies": { "semver": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.2.0.tgz", - "integrity": "sha512-jdFC1VdUGT/2Scgbimf7FSx9iJLXoqfglSF+gJeuNWVpiE37OIbc1jywR/GJyFdz3mnkz2/id0L0J/cr0izR5A==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", + "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", "dev": true } } }, - "core-js-pure": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.1.4.tgz", - "integrity": "sha512-uJ4Z7iPNwiu1foygbcZYJsJs1jiXrTTCvxfLDXNhI/I+NHbSIEyr548y4fcsCEyWY0XgfAG/qqaunJ1SThHenA==", - "dev": true - }, "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", @@ -2278,6 +2175,16 @@ "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", @@ -2331,12 +2238,6 @@ "assert-plus": "^1.0.0" } }, - "date-now": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", - "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=", - "dev": true - }, "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -2443,16 +2344,27 @@ "dev": true }, "del": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/del/-/del-5.0.0.tgz", - "integrity": "sha512-TfU3nUY0WDIhN18eq+pgpbLY9AfL5RfiE9czKaTSolc6aK7qASXfDErvYgjV1UqCR4sNXDoxO0/idPmhDUt2Sg==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/del/-/del-5.1.0.tgz", + "integrity": "sha512-wH9xOVHnczo9jN2IW68BabcecVPxacIA3g/7z6vhSU/4stOKQzeCRK0yD0A24WiAAUJmmVpWqrERcTxnLo3AnA==", "dev": true, "requires": { - "globby": "^10.0.0", - "is-path-cwd": "^2.0.0", - "is-path-in-cwd": "^2.0.0", - "p-map": "^2.0.0", - "rimraf": "^2.6.3" + "globby": "^10.0.1", + "graceful-fs": "^4.2.2", + "is-glob": "^4.0.1", + "is-path-cwd": "^2.2.0", + "is-path-inside": "^3.0.1", + "p-map": "^3.0.0", + "rimraf": "^3.0.0", + "slash": "^3.0.0" + }, + "dependencies": { + "graceful-fs": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", + "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==", + "dev": true + } } }, "delayed-stream": { @@ -2468,21 +2380,21 @@ "dev": true }, "deps-sort": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/deps-sort/-/deps-sort-2.0.0.tgz", - "integrity": "sha1-CRckkC6EZYJg65EHSMzNGvbiH7U=", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/deps-sort/-/deps-sort-2.0.1.tgz", + "integrity": "sha512-1orqXQr5po+3KI6kQb9A4jnXT1PBwggGl2d7Sq2xsnOeI9GPcE/tGcF9UiSZtZBM7MukY4cAh7MemS6tZYipfw==", "dev": true, "requires": { "JSONStream": "^1.0.3", - "shasum": "^1.0.0", + "shasum-object": "^1.0.0", "subarg": "^1.0.0", "through2": "^2.0.0" } }, "des.js": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", - "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=", + "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", @@ -2582,15 +2494,15 @@ } }, "electron-to-chromium": { - "version": "1.3.191", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.191.tgz", - "integrity": "sha512-jasjtY5RUy/TOyiUYM2fb4BDaPZfm6CXRFeJDMfFsXYADGxUN49RBqtgB7EL2RmJXeIRUk9lM1U6A5yk2YJMPQ==", + "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.0", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.0.tgz", - "integrity": "sha512-eFOJTMyCYb7xtE/caJ6JJu+bhi67WCYNbkGSknu20pmM8Ke/bqOfdnZWxyoGN26JgfxTbXrsCkEw4KheCT/KGg==", + "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", @@ -2671,9 +2583,9 @@ "dev": true }, "esutils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true }, "events": { @@ -2853,16 +2765,15 @@ "dev": true }, "fast-glob": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.0.4.tgz", - "integrity": "sha512-wkIbV6qg37xTJwqSsdnIphL1e+LaGz4AIQqr00mIubMaEhv1/HEmJ0uuCGZRNRUkZZmOB5mJKO0ZUTVq+SxMQg==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.1.1.tgz", + "integrity": "sha512-nTCREpBY8w8r+boyFYAx21iL6faSsQynliPHM4Uf56SbkyohCNxpVPEH9xrF5TXKy+IsjkPUHDKiUkzBVRXn9g==", "dev": true, "requires": { - "@nodelib/fs.stat": "^2.0.1", - "@nodelib/fs.walk": "^1.2.1", - "glob-parent": "^5.0.0", - "is-glob": "^4.0.1", - "merge2": "^1.2.3", + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.0", + "merge2": "^1.3.0", "micromatch": "^4.0.2" }, "dependencies": { @@ -2885,9 +2796,9 @@ } }, "glob-parent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.0.0.tgz", - "integrity": "sha512-Z2RwiujPRGluePM6j699ktJYxmPpJKCfpGA13jz2hmFZC7gKetzrWvg5KN3+OsIFmydGyZ1AVwERCq1w/ZZwRg==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.0.tgz", + "integrity": "sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw==", "dev": true, "requires": { "is-glob": "^4.0.1" @@ -2921,9 +2832,15 @@ } }, "fast-json-stable-stringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", + "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-safe-stringify": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz", + "integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==", "dev": true }, "fastq": { @@ -3634,6 +3551,17 @@ "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": { @@ -3656,43 +3584,6 @@ "string-width": "^1.0.1", "strip-ansi": "^3.0.1", "wide-align": "^1.1.0" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "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-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" - } - }, - "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" - } - } } }, "gaze": { @@ -3851,9 +3742,9 @@ } }, "globule": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/globule/-/globule-1.2.1.tgz", - "integrity": "sha512-g7QtgWF4uYSL5/dn71WxubOrS7JVGCnFPEnoeChJmBnyR9Mw8nGoEwOgJL/RC2Te0WhbsEUCejfH8SZNJ+adYQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/globule/-/globule-1.3.0.tgz", + "integrity": "sha512-YlD4kdMqRCQHrhVdonet4TdRtv1/sZKepvoxNT4Nrhrp5HI8XFfc8kFlGlBn2myBo80aGp8Eft259mbcUJhgSg==", "dev": true, "requires": { "glob": "~7.1.1", @@ -3889,9 +3780,9 @@ } }, "gulp-babel": { - "version": "8.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gulp-babel/-/gulp-babel-8.0.0-beta.2.tgz", - "integrity": "sha512-GTC2PxAXWkp6u1fP+C5+kn5biQ0dKGhkOSSXvKAf3ykF0+R3tevmLm/zSIkc1+S7U1JwH3XTvuMwRL6LD+sEiw==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/gulp-babel/-/gulp-babel-8.0.0.tgz", + "integrity": "sha512-oomaIqDXxFkg7lbpBou/gnUkX51/Y/M2ZfSjL2hdqXTAlSWZcgZtd2o0cOH0r/eE8LWD0+Q/PsLsr2DKOoqToQ==", "dev": true, "requires": { "plugin-error": "^1.0.1", @@ -4011,14 +3902,6 @@ "dev": true, "requires": { "ansi-regex": "^2.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - } } }, "has-flag": { @@ -4103,9 +3986,9 @@ } }, "hoist-non-react-statics": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.0.tgz", - "integrity": "sha512-0XsbTXxgiaCDYDIWFcwkmerZPSwywfUqYmwT4jzewKTQSWoE6FCMoUVOeBJWK3E/CrWbxRG3m5GzY4lnIwGRBA==", + "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" } @@ -4155,9 +4038,9 @@ "dev": true }, "ignore": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.2.tgz", - "integrity": "sha512-vdqWBp7MyzdmHkkRWV5nY+PfGRbYbahfuvsBCh277tq+w9zyNi7h5CYJCK0kmzti9kU+O/cB7sE8HvKv6aXAKQ==", + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.4.tgz", + "integrity": "sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A==", "dev": true }, "in-publish": { @@ -4167,13 +4050,10 @@ "dev": true }, "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" - } + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true }, "inflight": { "version": "1.0.6", @@ -4355,12 +4235,6 @@ "number-is-nan": "^1.0.0" } }, - "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-glob": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", @@ -4402,23 +4276,11 @@ "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" - } - }, "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" - } + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.2.tgz", + "integrity": "sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg==", + "dev": true }, "is-plain-object": { "version": "2.0.4", @@ -4563,9 +4425,9 @@ "dev": true }, "json5": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.0.tgz", - "integrity": "sha512-8Mh9h6xViijj36g7Dxi+Y4S6hNGV96vcJZr/SrlHh1LR/pEn/8j/+qIBbs44YKl69Lrfctp4QD+AdWLTMqEZAQ==", + "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" @@ -4736,6 +4598,16 @@ "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", @@ -4841,9 +4713,9 @@ } }, "merge2": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.2.3.tgz", - "integrity": "sha512-gdUU1Fwj5ep4kplwcmftruWofEFt6lfpkkr3h860CXbAB9c3hGb55EOL2ali0Td5oebvW0E1+3Sr+Ur7XfKpRA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.3.0.tgz", + "integrity": "sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw==", "dev": true }, "micromatch": { @@ -4878,18 +4750,18 @@ } }, "mime-db": { - "version": "1.40.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", - "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==", + "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.24", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", - "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", + "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.40.0" + "mime-db": "1.42.0" } }, "minimalistic-assert": { @@ -4958,9 +4830,9 @@ } }, "module-deps": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/module-deps/-/module-deps-6.2.1.tgz", - "integrity": "sha512-UnEn6Ah36Tu4jFiBbJVUtt0h+iXqxpLqDvPS8nllbw5RZFmNJ1+Mz5BjYnM9ieH80zyxHkARGLnMIHlPK5bu6A==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/module-deps/-/module-deps-6.2.2.tgz", + "integrity": "sha512-a9y6yDv5u5I4A+IPHTnqFxcaKr4p50/zxTjcQJaX2ws9tN/W6J6YXnEKhqRyPhl494dkcxx951onSKVezmI+3w==", "dev": true, "requires": { "JSONStream": "^1.0.3", @@ -4968,7 +4840,7 @@ "cached-path-relative": "^1.0.2", "concat-stream": "~1.6.0", "defined": "^1.0.0", - "detective": "^5.0.2", + "detective": "^5.2.0", "duplexer2": "^0.1.2", "inherits": "^2.0.1", "parents": "^1.0.0", @@ -5043,6 +4915,15 @@ "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", @@ -5058,18 +4939,26 @@ "dev": true }, "node-releases": { - "version": "1.1.25", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.25.tgz", - "integrity": "sha512-fI5BXuk83lKEoZDdH3gRhtsNgh05/wZacuXkgbiYkceE7+QIMXOg98n9ZV7mz27B+kFHnqHcUpscZZlGRSmTpQ==", + "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": "^5.3.0" + "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.12.0", - "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.12.0.tgz", - "integrity": "sha512-A1Iv4oN+Iel6EPv77/HddXErL2a+gZ4uBeZUy+a8O35CFYTXhgA8MgLCWBtwpGZdCvTvQ9d+bQxX/QC36GDPpQ==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.13.0.tgz", + "integrity": "sha512-W1XBrvoJ1dy7VsvTAS5q1V45lREbTlZQqFbiHb3R3OTTCma0XBtuG6xZ6Z4506nR4lmHPTqVRwxT6KgtWC97CA==", "dev": true, "requires": { "async-foreach": "^0.1.3", @@ -5079,7 +4968,7 @@ "get-stdin": "^4.0.1", "glob": "^7.0.3", "in-publish": "^2.0.0", - "lodash": "^4.17.11", + "lodash": "^4.17.15", "meow": "^3.7.0", "mkdirp": "^0.5.1", "nan": "^2.13.2", @@ -5091,12 +4980,6 @@ "true-case-path": "^1.0.2" }, "dependencies": { - "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": "2.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", @@ -5116,46 +4999,11 @@ "supports-color": "^2.0.0" } }, - "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" - } - }, - "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" - } - }, - "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" - } - }, "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 - }, - "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", - "dev": true } } }, @@ -5382,9 +5230,9 @@ } }, "p-limit": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", - "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", + "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" @@ -5400,10 +5248,13 @@ } }, "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 + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dev": true, + "requires": { + "aggregate-error": "^3.0.0" + } }, "p-try": { "version": "2.2.0", @@ -5427,9 +5278,9 @@ } }, "parse-asn1": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.4.tgz", - "integrity": "sha512-Qs5duJcuvNExRfFZ99HDD3z4mAi3r9Wl/FOjEOijlxwCZs7E7mW2vjTpgQ4J8LpTF8x5v+1Vn5UQFejmWT11aw==", + "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", @@ -5502,12 +5353,6 @@ "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-parse": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", @@ -5574,9 +5419,9 @@ "dev": true }, "picomatch": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.0.7.tgz", - "integrity": "sha512-oLHIdio3tZ0qH76NybpeneBhYVj0QFTfXEFTc/B3zKQspYfYYkWYgFsmzo+4kvId/bQRcNkVeguI3y+CD22BtA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.1.1.tgz", + "integrity": "sha512-OYMyqkKzK7blWO/+XZYP6w8hH0LDvkBvdvKukti+7kqYFCiEAk+gI3DWnryapc0Dau05ugGTy0foQ6mqn4AHYA==", "dev": true }, "pify": { @@ -5637,9 +5482,9 @@ "dev": true }, "prettier": { - "version": "1.18.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.18.2.tgz", - "integrity": "sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw==", + "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-hrtime": { @@ -5683,9 +5528,9 @@ "dev": true }, "psl": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.2.0.tgz", - "integrity": "sha512-GEn74ZffufCmkDDLNcl3uuyF/aSD6exEyh1v/ZSdAomB82t6G9hzJVRx0jBmLDW+VfZqks3aScmMw9DszwUalA==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.7.0.tgz", + "integrity": "sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ==", "dev": true }, "public-encrypt": { @@ -5724,9 +5569,9 @@ } }, "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", "dev": true }, "qs": { @@ -5767,45 +5612,44 @@ } }, "react": { - "version": "16.8.6", - "resolved": "https://registry.npmjs.org/react/-/react-16.8.6.tgz", - "integrity": "sha512-pC0uMkhLaHm11ZSJULfOBqV4tIZkx87ZLvbbQYunNixAAvjnC+snJCg0XQXn9VIsttVsbZP/H/ewzgsd5fxKXw==", + "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", - "scheduler": "^0.13.6" + "prop-types": "^15.6.2" } }, "react-dom": { - "version": "16.8.6", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.8.6.tgz", - "integrity": "sha512-1nL7PIq9LTL3fthPqwkvr2zY7phIPjYrT0jp4HjyEQrEROnw4dG41VVwi/wfoCneoleqrNX7iAD+pXebJZwrwA==", + "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.13.6" + "scheduler": "^0.18.0" } }, "react-is": { - "version": "16.8.6", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz", - "integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==" + "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.0", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.1.0.tgz", - "integrity": "sha512-hyu/PoFK3vZgdLTg9ozbt7WF3GgX5+Yn3pZm5/96/o4UueXA+zj08aiSC9Mfj2WtD1bvpIb3C5yvskzZySzzaw==", + "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.4.5", + "@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.8.6" + "react-is": "^16.9.0" } }, "read-only-stream": { @@ -5902,12 +5746,23 @@ "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.4", - "resolved": "https://registry.npmjs.org/redux/-/redux-4.0.4.tgz", - "integrity": "sha512-vKv4WdiJxOWKxK0yRoaK3Y4pxxB0ilzVx6dszU2W8wLxlb2yikRph4iV/ymtdJ6ZxpBLFbyrxklnT5yBbQSl3Q==", + "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" @@ -5947,9 +5802,9 @@ "integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw==" }, "regenerator-transform": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.0.tgz", - "integrity": "sha512-rtOelq4Cawlbmq9xuMR5gdFmv7ku/sFoB7sRiywx7aq53bc52b4j6zvH7Te1Vt/X2YveDKnCGUbioieU7FEL3w==", + "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" @@ -5965,20 +5820,14 @@ "safe-regex": "^1.1.0" } }, - "regexp-tree": { - "version": "0.1.11", - "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.11.tgz", - "integrity": "sha512-7/l/DgapVVDzZobwMCCgMlqiqyLFJ0cduo/j+3BcDJIB+yJdsYCfKuI3l/04NV+H/rfNRdPIDbXNZHM9XvQatg==", - "dev": true - }, "regexpu-core": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.5.4.tgz", - "integrity": "sha512-BtizvGtFQKGPUcTy56o3nk1bGRp4SZOTYrDtGNlqCQufptV5IkkLN6Emw+yunAJjzf+C9FQFtvq7IoA3+oMYHQ==", + "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.0.2", + "regenerate-unicode-properties": "^8.1.0", "regjsgen": "^0.5.0", "regjsparser": "^0.6.0", "unicode-match-property-ecmascript": "^1.0.4", @@ -5986,15 +5835,15 @@ } }, "regjsgen": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.0.tgz", - "integrity": "sha512-RnIrLhrXCX5ow/E5/Mh2O4e/oa1/jW0eaBKTSy3LaCj+M3Bqvm97GWDp2yUtzIs4LEn65zR2yiYGFqb2ApnzDA==", + "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.0", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.6.0.tgz", - "integrity": "sha512-RQ7YyokLiQBomUJuUG8iGVvkgOLxwyZM8k6d3q5SAXpg4r5TZJZigKFvC6PpD+qQ98bCDC5YelPeA3EucDoNeQ==", + "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" @@ -6160,9 +6009,9 @@ "dev": true }, "rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.0.tgz", + "integrity": "sha512-NDGVxTsjqfunkds7CqsOiEnxln4Bo7Nddl3XhS4pXg5OzwkLqJ971ZVAAnB+DDLnF76N+VnDEiBHaVV8I06SUg==", "dev": true, "requires": { "glob": "^7.1.3" @@ -6215,154 +6064,12 @@ "lodash": "^4.0.0", "scss-tokenizer": "^0.2.3", "yargs": "^7.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "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 - }, - "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" - } - }, - "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 - }, - "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-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" - } - }, - "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" - } - }, - "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" - } - }, - "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 - }, - "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" - } - }, - "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" - } - }, - "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 - }, - "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" - } - }, - "y18n": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", - "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=", - "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" - } - } } }, "scheduler": { - "version": "0.13.6", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.13.6.tgz", - "integrity": "sha512-IWnObHt413ucAYKsD9J1QShUKkbKLQQHdxRyw73sw4FN26iWr3DY/H34xGPe4nmL1DwXyWmSWmMrA9TfQbE/XQ==", + "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", @@ -6454,18 +6161,21 @@ "sha.js": "~2.4.4" } }, - "shell-quote": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.6.1.tgz", - "integrity": "sha1-9HgZSczkAmlxJ0MOo7PFR29IF2c=", + "shasum-object": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shasum-object/-/shasum-object-1.0.0.tgz", + "integrity": "sha512-Iqo5rp/3xVi6M4YheapzZhhGPVs0yZwHj7wvwQ1B9z8H6zk+FEnI7y3Teq7qwnekfEhu8WmG2z0z4iWZaxLWVg==", "dev": true, "requires": { - "array-filter": "~0.0.0", - "array-map": "~0.0.0", - "array-reduce": "~0.0.0", - "jsonify": "~0.0.0" + "fast-safe-stringify": "^2.0.7" } }, + "shell-quote": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz", + "integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==", + "dev": true + }, "signal-exit": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", @@ -6611,9 +6321,9 @@ } }, "source-map-support": { - "version": "0.5.12", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.12.tgz", - "integrity": "sha512-4h2Pbvyy15EE02G+JOZpUCmqWJuqrs+sEkzewTm++BPi7Hvn/HwcqLAcNxYAyI0x13CpPPn+kMjl+hplXMHITQ==", + "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", @@ -6761,16 +6471,28 @@ "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==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-3.1.0.tgz", + "integrity": "sha512-cuB6RgO7BqC4FBYzmnvhob5Do3wIdIsXAgGycHJnW+981gHqoYcYz9lqjJrk8WXRddbwPuqPYRl+bag6mYv4lw==", "dev": true, "requires": { "builtin-status-codes": "^3.0.0", "inherits": "^2.0.1", - "readable-stream": "^2.3.6", - "to-arraybuffer": "^1.0.0", + "readable-stream": "^3.0.6", "xtend": "^4.0.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz", + "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } } }, "stream-shift": { @@ -6951,12 +6673,6 @@ "is-negated-glob": "^1.0.0" } }, - "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", @@ -7022,14 +6738,6 @@ "requires": { "psl": "^1.1.24", "punycode": "^1.4.1" - }, - "dependencies": { - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", - "dev": true - } } }, "trim-newlines": { @@ -7038,12 +6746,6 @@ "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", "dev": true }, - "trim-right": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", - "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", - "dev": true - }, "true-case-path": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.3.tgz", @@ -7237,6 +6939,14 @@ "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": { @@ -7293,9 +7003,9 @@ "dev": true }, "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz", + "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==", "dev": true }, "v8flags": { @@ -7418,9 +7128,9 @@ } }, "vm-browserify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.0.tgz", - "integrity": "sha512-iq+S7vZJE60yejDYM0ek6zg308+UZsdtPExWP9VZoCFCz1zkJoXFnAX7aZfd/ZwrkidzdUZL0C/ryW+JwAiIGw==", + "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 }, "which": { @@ -7445,33 +7155,6 @@ "dev": true, "requires": { "string-width": "^1.0.2 || 2" - }, - "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 - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "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" - } - } } }, "wrap-ansi": { @@ -7502,6 +7185,12 @@ "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", diff --git a/package.json b/package.json index 5eca7dd..f19d3fb 100644 --- a/package.json +++ b/package.json @@ -16,34 +16,34 @@ "dependencies": { "js-cookie": "^2.2.1", "lodash": "^4.17.15", - "react-redux": "^7.1.0", - "redux": "^4.0.4", + "react-redux": "^7.1.3", + "redux": "^4.0.5", "redux-logger": "^3.0.6", "redux-thunk": "^2.3.0" }, "devDependencies": { - "@babel/core": "^7.5.4", - "@babel/plugin-proposal-class-properties": "^7.5.5", - "@babel/plugin-proposal-function-bind": "^7.2.0", - "@babel/plugin-syntax-dynamic-import": "^7.2.0", - "@babel/plugin-syntax-function-bind": "^7.2.0", - "@babel/plugin-transform-react-jsx": "^7.3.0", - "@babel/plugin-transform-runtime": "^7.5.5", - "@babel/preset-env": "^7.5.4", - "@babel/register": "^7.4.4", - "@babel/runtime": "^7.5.5", + "@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", "babelify": "^10.0.0", - "browserify": "^16.3.0", - "del": "^5.0.0", + "browserify": "^16.5.0", + "del": "^5.1.0", "gulp": "^4.0.2", - "gulp-babel": "^8.0.0-beta.2", + "gulp-babel": "^8.0.0", "gulp-cli": "^2.2.0", "gulp-concat": "^2.6.1", "gulp-sass": "^4.0.2", - "node-sass": "^4.12.0", - "prettier": "^1.18.2", - "react": "^16.8.6", - "react-dom": "^16.8.6", + "node-sass": "^4.13.0", + "prettier": "^1.19.1", + "react": "^16.12.0", + "react-dom": "^16.12.0", "vinyl-buffer": "^1.0.1", "vinyl-source-stream": "^2.0.0" } diff --git a/src/newsreader/fixtures/default-fixture.json b/src/newsreader/fixtures/default-fixture.json index ed3c3be..e0de28f 100644 --- a/src/newsreader/fixtures/default-fixture.json +++ b/src/newsreader/fixtures/default-fixture.json @@ -1,31 +1,4 @@ [ -{ - "model": "django_celery_beat.periodictask", - "pk": 2, - "fields": { - "name": "celery.backend_cleanup", - "task": "celery.backend_cleanup", - "interval": null, - "crontab": 1, - "solar": null, - "clocked": null, - "args": "[]", - "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-29T04:00:00.000Z", - "total_run_count": 17, - "date_changed": "2019-11-29T04:02:15.258Z", - "description": "" - } -}, { "model": "django_celery_beat.periodictask", "pk": 10, From 68534cd5413c6931dd217d3dd6bfa3da496238a3 Mon Sep 17 00:00:00 2001 From: Sonny Date: Wed, 1 Jan 2020 21:39:07 +0100 Subject: [PATCH 034/364] Remove deprecated npm options --- .gitlab-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c4ac909..cb9600d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -11,7 +11,7 @@ javascript build: paths: - node_modules/ before_script: - - npm install --dev + - npm install --only=dev script: - npx gulp @@ -45,7 +45,7 @@ javascript linting: paths: - node_modules/ before_script: - - npm install --dev + - npm install --only=dev script: - npm run lint From 28620eab2986ef8fcd3c18aae0d9b3092250a3e5 Mon Sep 17 00:00:00 2001 From: Sonny Date: Wed, 1 Jan 2020 21:46:35 +0100 Subject: [PATCH 035/364] Rerun prettier --- src/newsreader/js/pages/homepage/App.js | 5 +---- src/newsreader/js/pages/homepage/components/PostModal.js | 5 +---- .../js/pages/homepage/components/feedlist/FeedList.js | 5 +---- .../js/pages/homepage/components/feedlist/PostItem.js | 5 +---- .../js/pages/homepage/components/sidebar/CategoryItem.js | 5 +---- .../js/pages/homepage/components/sidebar/ReadButton.js | 5 +---- .../js/pages/homepage/components/sidebar/RuleItem.js | 5 +---- 7 files changed, 7 insertions(+), 28 deletions(-) diff --git a/src/newsreader/js/pages/homepage/App.js b/src/newsreader/js/pages/homepage/App.js index fa9bc2c..9f88e46 100644 --- a/src/newsreader/js/pages/homepage/App.js +++ b/src/newsreader/js/pages/homepage/App.js @@ -57,7 +57,4 @@ const mapDispatchToProps = dispatch => ({ fetchCategories: () => dispatch(fetchCategories()), }); -export default connect( - mapStateToProps, - mapDispatchToProps -)(App); +export default connect(mapStateToProps, mapDispatchToProps)(App); diff --git a/src/newsreader/js/pages/homepage/components/PostModal.js b/src/newsreader/js/pages/homepage/components/PostModal.js index 524a217..75650fb 100644 --- a/src/newsreader/js/pages/homepage/components/PostModal.js +++ b/src/newsreader/js/pages/homepage/components/PostModal.js @@ -78,7 +78,4 @@ const mapDispatchToProps = dispatch => ({ markPostRead: (post, token) => dispatch(markPostRead(post, token)), }); -export default connect( - null, - mapDispatchToProps -)(PostModal); +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 index 36966c8..418e028 100644 --- a/src/newsreader/js/pages/homepage/components/feedlist/FeedList.js +++ b/src/newsreader/js/pages/homepage/components/feedlist/FeedList.js @@ -89,7 +89,4 @@ const mapDispatchToProps = dispatch => ({ fetchPostsBySection: (rule, page = false) => dispatch(fetchPostsBySection(rule, page)), }); -export default connect( - mapStateToProps, - mapDispatchToProps -)(FeedList); +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 index e226e9b..865af41 100644 --- a/src/newsreader/js/pages/homepage/components/feedlist/PostItem.js +++ b/src/newsreader/js/pages/homepage/components/feedlist/PostItem.js @@ -44,7 +44,4 @@ const mapDispatchToProps = dispatch => ({ selectPost: post => dispatch(selectPost(post)), }); -export default connect( - null, - mapDispatchToProps -)(PostItem); +export default connect(null, mapDispatchToProps)(PostItem); diff --git a/src/newsreader/js/pages/homepage/components/sidebar/CategoryItem.js b/src/newsreader/js/pages/homepage/components/sidebar/CategoryItem.js index 36279ba..6413406 100644 --- a/src/newsreader/js/pages/homepage/components/sidebar/CategoryItem.js +++ b/src/newsreader/js/pages/homepage/components/sidebar/CategoryItem.js @@ -61,7 +61,4 @@ const mapDispatchToProps = dispatch => ({ fetchCategory: category => dispatch(fetchCategory(category)), }); -export default connect( - null, - mapDispatchToProps -)(CategoryItem); +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 index 45e3781..3d33fc0 100644 --- a/src/newsreader/js/pages/homepage/components/sidebar/ReadButton.js +++ b/src/newsreader/js/pages/homepage/components/sidebar/ReadButton.js @@ -30,7 +30,4 @@ const mapDispatchToProps = dispatch => ({ const mapStateToProps = state => ({ selected: state.selected.item }); -export default connect( - mapStateToProps, - mapDispatchToProps -)(ReadButton); +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 index d93eb26..5f70cfd 100644 --- a/src/newsreader/js/pages/homepage/components/sidebar/RuleItem.js +++ b/src/newsreader/js/pages/homepage/components/sidebar/RuleItem.js @@ -45,7 +45,4 @@ const mapDispatchToProps = dispatch => ({ fetchRule: rule => dispatch(fetchRule(rule)), }); -export default connect( - null, - mapDispatchToProps -)(RuleItem); +export default connect(null, mapDispatchToProps)(RuleItem); From 06ce82bf0c5d42ead5ab8d171da72e9c77aa1cde Mon Sep 17 00:00:00 2001 From: sonny Date: Thu, 2 Jan 2020 20:15:35 +0100 Subject: [PATCH 036/364] Merge category view tests into single file --- ...t_category_updateview.py => test_views.py} | 59 +++++++++++++++++-- .../news/core/tests/views/__init__.py | 0 .../core/tests/views/category/__init__.py | 0 .../category/test_category_createview.py | 58 ------------------ 4 files changed, 55 insertions(+), 62 deletions(-) rename src/newsreader/news/core/tests/{views/category/test_category_updateview.py => test_views.py} (73%) delete mode 100644 src/newsreader/news/core/tests/views/__init__.py delete mode 100644 src/newsreader/news/core/tests/views/category/__init__.py delete mode 100644 src/newsreader/news/core/tests/views/category/test_category_createview.py diff --git a/src/newsreader/news/core/tests/views/category/test_category_updateview.py b/src/newsreader/news/core/tests/test_views.py similarity index 73% rename from src/newsreader/news/core/tests/views/category/test_category_updateview.py rename to src/newsreader/news/core/tests/test_views.py index 23fa769..47381d2 100644 --- a/src/newsreader/news/core/tests/views/category/test_category_updateview.py +++ b/src/newsreader/news/core/tests/test_views.py @@ -3,22 +3,73 @@ 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 CategoryUpdateViewTestCase(TestCase): +class CategoryViewTestCase: def setUp(self): self.user = UserFactory(password="test") self.client.login(email=self.user.email, password="test") - self.category = CategoryFactory(name="category", user=self.user) - self.url = reverse("category-update", args=[self.category.pk]) - 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]} + 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]} + + response = self.client.post(self.url, data) + + self.assertContains(response, "not one of the available choices") + self.assertEquals(Category.objects.count(), 0) + + +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"} self.client.post(self.url, data) diff --git a/src/newsreader/news/core/tests/views/__init__.py b/src/newsreader/news/core/tests/views/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/newsreader/news/core/tests/views/category/__init__.py b/src/newsreader/news/core/tests/views/category/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/newsreader/news/core/tests/views/category/test_category_createview.py b/src/newsreader/news/core/tests/views/category/test_category_createview.py deleted file mode 100644 index ce25e20..0000000 --- a/src/newsreader/news/core/tests/views/category/test_category_createview.py +++ /dev/null @@ -1,58 +0,0 @@ -from django.test import Client, 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 CategoryCreateViewTestCase(TestCase): - def setUp(self): - self.user = UserFactory(password="test") - self.client.login(email=self.user.email, password="test") - - self.url = reverse("category-create") - - def test_simple(self): - response = self.client.get(self.url) - - self.assertEquals(response.status_code, 200) - - def test_creation(self): - rules = CollectionRuleFactory.create_batch(size=4, user=self.user) - - data = {"name": "new-category", "rules": [rule.pk for rule in rules]} - 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]} - - response = self.client.post(self.url, data) - - self.assertContains(response, "not one of the available choices") - self.assertEquals(Category.objects.count(), 0) From 12693dac106facb12ff9393c10b1321b43d1c259 Mon Sep 17 00:00:00 2001 From: sonny Date: Thu, 2 Jan 2020 20:50:09 +0100 Subject: [PATCH 037/364] Fix auto read marking with category selected --- src/newsreader/js/pages/homepage/actions/posts.js | 6 ++---- .../js/pages/homepage/reducers/categories.js | 11 ++++++++++- src/newsreader/js/pages/homepage/reducers/rules.js | 2 +- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/newsreader/js/pages/homepage/actions/posts.js b/src/newsreader/js/pages/homepage/actions/posts.js index 59229ab..9cfab5d 100644 --- a/src/newsreader/js/pages/homepage/actions/posts.js +++ b/src/newsreader/js/pages/homepage/actions/posts.js @@ -27,6 +27,7 @@ export const postRead = (post, section) => ({ export const markPostRead = (post, token) => { return (dispatch, getState) => { const { rules } = getState(); + const { selected } = getState(); const url = `/api/posts/${post.id}/`; const options = { @@ -38,10 +39,7 @@ export const markPostRead = (post, token) => { body: JSON.stringify({ read: true }), }; - const section = { - ...rules.items[post.rule], - type: RULE_TYPE, - }; + const section = { ...selected.item }; return fetch(url, options) .then(response => response.json()) diff --git a/src/newsreader/js/pages/homepage/reducers/categories.js b/src/newsreader/js/pages/homepage/reducers/categories.js index 90fe063..a1f8961 100644 --- a/src/newsreader/js/pages/homepage/reducers/categories.js +++ b/src/newsreader/js/pages/homepage/reducers/categories.js @@ -46,7 +46,16 @@ export const categories = (state = { ...defaultState }, action) => { isFetching: true, }; case MARK_POST_READ: - let category = { ...state.items[action.section.category] }; + 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, diff --git a/src/newsreader/js/pages/homepage/reducers/rules.js b/src/newsreader/js/pages/homepage/reducers/rules.js index 69cd703..0802576 100644 --- a/src/newsreader/js/pages/homepage/reducers/rules.js +++ b/src/newsreader/js/pages/homepage/reducers/rules.js @@ -34,7 +34,7 @@ export const rules = (state = { ...defaultState }, action) => { isFetching: false, }; case MARK_POST_READ: - const rule = { ...state.items[action.section.id] }; + const rule = { ...state.items[action.post.rule] }; return { ...state, From 62e763604ed493574a6570480f964c6c08ca8df4 Mon Sep 17 00:00:00 2001 From: sonny Date: Sat, 18 Jan 2020 19:44:11 +0100 Subject: [PATCH 038/364] Install all javascript dependencies by default --- .gitlab-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index cb9600d..81a727e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -11,7 +11,7 @@ javascript build: paths: - node_modules/ before_script: - - npm install --only=dev + - npm install script: - npx gulp @@ -45,7 +45,7 @@ javascript linting: paths: - node_modules/ before_script: - - npm install --only=dev + - npm install script: - npm run lint From c3b087e004b238f1fcffd16dd1e7f45f3cbe1472 Mon Sep 17 00:00:00 2001 From: Sonny Date: Thu, 30 Jan 2020 20:29:32 +0100 Subject: [PATCH 039/364] Squashed commit of the following: commit f6174179405cbf696415b17bbfcb157b6c3415cf Author: sonny Date: Thu Jan 2 23:26:49 2020 +0100 redux tests --- .gitlab-ci.yml | 12 + jest.config.js | 188 ++ package-lock.json | 2419 +++++++++++++++++ package.json | 11 +- .../js/pages/homepage/actions/categories.js | 4 +- .../js/pages/homepage/actions/posts.js | 44 +- .../js/pages/homepage/actions/rules.js | 8 +- .../js/pages/homepage/actions/selected.js | 14 +- .../components/sidebar/CategoryItem.js | 2 +- .../homepage/components/sidebar/RuleItem.js | 2 +- .../js/pages/homepage/reducers/posts.js | 13 +- .../js/pages/homepage/reducers/selected.js | 4 +- .../tests/homepage/actions/category.test.js | 250 ++ .../js/tests/homepage/actions/post.test.js | 325 +++ .../js/tests/homepage/actions/rule.test.js | 254 ++ .../tests/homepage/actions/selected.test.js | 141 + .../tests/homepage/reducers/category.test.js | 214 ++ .../js/tests/homepage/reducers/post.test.js | 304 +++ .../js/tests/homepage/reducers/rule.test.js | 181 ++ .../tests/homepage/reducers/selected.test.js | 399 +++ 20 files changed, 4733 insertions(+), 56 deletions(-) create mode 100644 jest.config.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 diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 81a727e..caf624e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -37,6 +37,18 @@ python tests: script: - python src/manage.py test newsreader +javascript tests: + image: node:12 + stage: test + cache: + key: "$CI_JOB_NAME-$CI_COMMIT_REF_SLUG" + paths: + - node_modules/ + before_script: + - npm install + script: + - npm test + javascript linting: image: node:12 stage: lint 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 index a7433b6..4ee3371 100644 --- a/package-lock.json +++ b/package-lock.json @@ -945,6 +945,260 @@ "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" + } + }, "@nodelib/fs.scandir": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz", @@ -971,6 +1225,47 @@ "fastq": "^1.6.0" } }, + "@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", @@ -988,6 +1283,31 @@ "@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", @@ -1000,6 +1320,27 @@ "integrity": "sha512-B8emQA1qeKerqd1dmIsQYnXi+mmAzTB7flExjmy5X1aVAKFNNNDubkavwR13kR6JnpeLp3aLoJhwn9trWPAyFQ==", "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/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 + }, "JSONStream": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", @@ -1010,6 +1351,12 @@ "through": ">=2.2.7 <3" } }, + "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", @@ -1022,6 +1369,30 @@ "integrity": "sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ==", "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": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.0.tgz", + "integrity": "sha512-gac8OEcQ2Li1dxIEWGZzsp2BitJxwkwcOm0zHAJLcPJaVvm58FRnk6RkuLRpU1EujipU2ZFODv2P9DLMfnV8mw==", + "dev": true + }, + "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 + } + } + }, "acorn-node": { "version": "1.8.2", "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", @@ -1076,6 +1447,12 @@ "ansi-wrap": "^0.1.0" } }, + "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-gray": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/ansi-gray/-/ansi-gray-0.1.1.tgz", @@ -1189,6 +1566,12 @@ "integrity": "sha1-p5SvDAWrF1KEbudTofIRoFugxE8=", "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", @@ -1326,6 +1709,12 @@ "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-done": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/async-done/-/async-done-1.3.2.tgz", @@ -1350,6 +1739,12 @@ "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 + }, "async-settle": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-settle/-/async-settle-1.0.0.tgz", @@ -1383,6 +1778,29 @@ "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-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", @@ -1392,6 +1810,61 @@ "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 + } + } + }, "babelify": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/babelify/-/babelify-10.0.0.tgz", @@ -1581,6 +2054,12 @@ "umd": "^3.0.0" } }, + "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", @@ -1736,6 +2215,15 @@ "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": { "version": "5.4.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.4.3.tgz", @@ -1793,6 +2281,12 @@ "integrity": "sha512-5r2GqsoEb4qMTTN9J+WzXfjov+hjxT+j3u5K+kIVNIwAd99DLCJE9pBIMP1qVeybV6JiijL385Oz0DcYxfbOIg==", "dev": true }, + "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", @@ -1823,6 +2317,15 @@ "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", @@ -1868,6 +2371,12 @@ } } }, + "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", @@ -1947,6 +2456,12 @@ "readable-stream": "^2.3.5" } }, + "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", @@ -2024,6 +2539,13 @@ "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, + "optional": true + }, "commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", @@ -2114,6 +2636,12 @@ "is-plain-object": "^2.0.1" } }, + "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", @@ -2204,6 +2732,21 @@ "randomfill": "^1.0.3" } }, + "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", @@ -2238,6 +2781,30 @@ "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", @@ -2264,6 +2831,12 @@ "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 + }, "default-compare": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/default-compare/-/default-compare-1.0.0.tgz", @@ -2407,6 +2980,12 @@ "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 + }, "detective": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.0.tgz", @@ -2418,6 +2997,12 @@ "minimist": "^1.1.1" } }, + "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", @@ -2452,6 +3037,15 @@ "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" + } + }, "duplexer2": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", @@ -2514,6 +3108,12 @@ "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 + }, "end-of-stream": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", @@ -2532,6 +3132,44 @@ "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" + } + }, "es5-ext": { "version": "0.10.50", "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.50.tgz", @@ -2582,6 +3220,40 @@ "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 + } + } + }, + "esprima": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", + "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=", + "dev": true + }, + "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", @@ -2604,6 +3276,48 @@ "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", @@ -2648,6 +3362,20 @@ "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", @@ -2837,6 +3565,12 @@ "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 + }, "fast-safe-stringify": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz", @@ -2852,6 +3586,30 @@ "reusify": "^1.0.0" } }, + "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" + } + }, "fill-range": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", @@ -3613,6 +4371,27 @@ "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", @@ -3681,6 +4460,12 @@ "unique-stream": "^2.0.2" } }, + "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 + }, "glob-watcher": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-5.0.3.tgz", @@ -3767,6 +4552,12 @@ "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 + }, "gulp": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/gulp/-/gulp-4.0.2.tgz", @@ -3870,6 +4661,26 @@ "glogg": "^1.0.0" } }, + "handlebars": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.3.tgz", + "integrity": "sha512-3yPecJoJHK/4c6aZhSvxOyG4vJKDshV36VHp0iVCDVh7o9w2vwi3NSnL2MMPj3YdduqaBcu7cGbggJQM0br9xA==", + "dev": true, + "requires": { + "neo-async": "^2.6.0", + "optimist": "^0.6.1", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4" + }, + "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 + } + } + }, "har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", @@ -4008,6 +4819,15 @@ "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" + } + }, "htmlescape": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/htmlescape/-/htmlescape-1.1.1.tgz", @@ -4031,6 +4851,15 @@ "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" + } + }, "ieee754": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", @@ -4043,6 +4872,22 @@ "integrity": "sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A==", "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.0", "resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.0.tgz", @@ -4175,6 +5020,21 @@ "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", @@ -4195,6 +5055,12 @@ } } }, + "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", @@ -4235,6 +5101,18 @@ "number-is-nan": "^1.0.0" } }, + "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", @@ -4291,6 +5169,15 @@ "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-relative": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", @@ -4300,6 +5187,29 @@ "is-unc-path": "^1.0.0" } }, + "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", @@ -4333,6 +5243,12 @@ "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", @@ -4357,6 +5273,793 @@ "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.6", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-2.2.6.tgz", + "integrity": "sha512-SKi4rnMyLBKe0Jy2uUdx28h8oG7ph2PPuQPvIAh31d+Ci+lSiEu4C+h3oBPuJ9+mPKhOyW0M8gY4U5NM1WLeXA==", + "dev": true, + "requires": { + "handlebars": "^4.1.2" + } + }, + "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.1", "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.1.tgz", @@ -4385,12 +6088,60 @@ "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", @@ -4469,6 +6220,12 @@ "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", "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 + }, "labeled-stream-splicer": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/labeled-stream-splicer/-/labeled-stream-splicer-2.0.2.tgz", @@ -4516,6 +6273,28 @@ "flush-write-stream": "^1.0.2" } }, + "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" + } + }, "liftoff": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-3.1.0.tgz", @@ -4574,12 +6353,30 @@ "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", "dev": true }, + "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.memoize": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-3.0.4.tgz", "integrity": "sha1-LcvSwofLwKVcxCMovQxzYVDVPj8=", "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", @@ -4627,6 +6424,15 @@ "kind-of": "^6.0.2" } }, + "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-cache": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", @@ -4712,6 +6518,12 @@ "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 + }, "merge2": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.3.0.tgz", @@ -4889,12 +6701,36 @@ "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 + }, "next-tick": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", "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", @@ -4932,12 +6768,31 @@ } } }, + "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-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", @@ -5046,6 +6901,15 @@ "once": "^1.3.2" } }, + "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", @@ -5064,6 +6928,12 @@ "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", @@ -5106,6 +6976,12 @@ } } }, + "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", @@ -5145,6 +7021,16 @@ "isobject": "^3.0.0" } }, + "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.map": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz", @@ -5183,6 +7069,38 @@ "wrappy": "1" } }, + "optimist": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", + "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "dev": true, + "requires": { + "minimist": "~0.0.1", + "wordwrap": "~0.0.2" + }, + "dependencies": { + "minimist": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", + "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", + "dev": true + } + } + }, + "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" + } + }, "ordered-read-streams": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz", @@ -5229,6 +7147,21 @@ "os-tmpdir": "^1.0.0" } }, + "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-limit": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.1.tgz", @@ -5256,6 +7189,12 @@ "aggregate-error": "^3.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", @@ -5323,6 +7262,12 @@ "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", @@ -5353,6 +7298,12 @@ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "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", @@ -5380,6 +7331,12 @@ "integrity": "sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0=", "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", @@ -5475,18 +7432,50 @@ "extend-shallow": "^3.0.2" } }, + "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 }, + "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 + }, "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 + } + } + }, "pretty-hrtime": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", @@ -5511,6 +7500,16 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "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", @@ -5729,6 +7728,15 @@ "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" + } + }, "rechoir": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", @@ -5776,6 +7784,15 @@ "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", @@ -5950,6 +7967,26 @@ "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", @@ -5971,6 +8008,15 @@ "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", @@ -5981,6 +8027,12 @@ "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-options": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/resolve-options/-/resolve-options-1.1.0.tgz", @@ -6027,6 +8079,12 @@ "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-parallel": { "version": "1.1.9", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.9.tgz", @@ -6054,6 +8112,23 @@ "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", @@ -6066,6 +8141,12 @@ "yargs": "^7.0.0" } }, + "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", @@ -6170,12 +8251,33 @@ "fast-safe-stringify": "^2.0.7" } }, + "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 + }, "shell-quote": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz", "integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==", "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", @@ -6188,6 +8290,12 @@ "integrity": "sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY=", "dev": true }, + "sisteransi": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.4.tgz", + "integrity": "sha512-/ekMoM4NJ59ivGSfKapeG+FWtrmWvA1p6FBZwXrqojw90vJu8lBmrTxCMuBCydKtkaUe2zt4PlxeTKpjwMbyig==", + "dev": true + }, "slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -6414,6 +8522,12 @@ "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=", "dev": true }, + "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", @@ -6444,6 +8558,12 @@ "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", @@ -6511,6 +8631,33 @@ "readable-stream": "^2.0.2" } }, + "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", @@ -6533,6 +8680,26 @@ } } }, + "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", @@ -6560,6 +8727,12 @@ "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", @@ -6602,6 +8775,12 @@ "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 + }, "syntax-error": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/syntax-error/-/syntax-error-1.4.0.tgz", @@ -6622,6 +8801,96 @@ "inherits": "2" } }, + "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 + }, "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -6663,6 +8932,12 @@ "process": "~0.11.0" } }, + "tmpl": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", + "integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=", + "dev": true + }, "to-absolute-glob": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz", @@ -6740,6 +9015,23 @@ "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", @@ -6782,12 +9074,41 @@ "integrity": "sha512-MAM5dBMJCJNKs9E7JXo4CXRAansRfG0nlJxW7Wf6GZzSOvH31zClSaHdIMWLehe/EGMBkqeC55rrkaOr5Oo7Nw==", "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 }, + "uglify-js": { + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.7.3.tgz", + "integrity": "sha512-7tINm46/3puUA4hCkKYo4Xdts+JDaVC9ZPRcG8Xw9R4nhO/gZgUM3TENq8IF4Vatk8qCig4MzP/c8G4u2BkVQg==", + "dev": true, + "optional": true, + "requires": { + "commander": "~2.20.3", + "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 + } + } + }, "umd": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/umd/-/umd-3.0.3.tgz", @@ -7002,6 +9323,16 @@ "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", @@ -7133,6 +9464,56 @@ "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" + } + }, + "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 + }, + "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", @@ -7157,6 +9538,18 @@ "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 + }, + "wordwrap": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", + "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", + "dev": true + }, "wrap-ansi": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", @@ -7173,6 +9566,32 @@ "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", diff --git a/package.json b/package.json index f19d3fb..da22e0d 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,10 @@ "main": "index.js", "scripts": { "lint": "prettier \"src/newsreader/js/**/*.js\" --check", - "format": "prettier \"src/newsreader/js/**/*.js\" --write" + "format": "prettier \"src/newsreader/js/**/*.js\" --write", + "watch": "npx gulp watch", + "test": "jest", + "test:watch": "npm test -- --watch" }, "repository": { "type": "git", @@ -16,6 +19,7 @@ "dependencies": { "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", @@ -32,18 +36,23 @@ "@babel/preset-env": "^7.7.7", "@babel/register": "^7.7.7", "@babel/runtime": "^7.7.7", + "babel-jest": "^24.9.0", "babelify": "^10.0.0", "browserify": "^16.5.0", "del": "^5.1.0", + "fetch-mock": "^8.3.1", "gulp": "^4.0.2", "gulp-babel": "^8.0.0", "gulp-cli": "^2.2.0", "gulp-concat": "^2.6.1", "gulp-sass": "^4.0.2", + "jest": "^24.9.0", + "node-fetch": "^2.6.0", "node-sass": "^4.13.0", "prettier": "^1.19.1", "react": "^16.12.0", "react-dom": "^16.12.0", + "redux-mock-store": "^1.5.4", "vinyl-buffer": "^1.0.1", "vinyl-source-stream": "^2.0.0" } diff --git a/src/newsreader/js/pages/homepage/actions/categories.js b/src/newsreader/js/pages/homepage/actions/categories.js index 85772a6..f30424f 100644 --- a/src/newsreader/js/pages/homepage/actions/categories.js +++ b/src/newsreader/js/pages/homepage/actions/categories.js @@ -66,7 +66,7 @@ export const fetchCategories = () => { }); }); - setTimeout(dispatch, 500, receiveRules(rules)); + dispatch(receiveRules(rules)); }); }; }; @@ -88,7 +88,7 @@ export const fetchCategory = category => { dispatch(receiveCategory({ ...json })); if (category.unread === 0) { - dispatch(fetchRulesByCategory(category)); + return dispatch(fetchRulesByCategory(category)); } }); }; diff --git a/src/newsreader/js/pages/homepage/actions/posts.js b/src/newsreader/js/pages/homepage/actions/posts.js index 9cfab5d..598dde1 100644 --- a/src/newsreader/js/pages/homepage/actions/posts.js +++ b/src/newsreader/js/pages/homepage/actions/posts.js @@ -1,4 +1,4 @@ -import { RULE_TYPE } from '../constants.js'; +import { RULE_TYPE, CATEGORY_TYPE } from '../constants.js'; export const SELECT_POST = 'SELECT_POST'; export const UNSELECT_POST = 'UNSELECT_POST'; @@ -9,14 +9,19 @@ export const REQUEST_POSTS = 'REQUEST_POSTS'; export const MARK_POST_READ = 'MARK_POST_READ'; -export const selectPost = post => ({ - type: SELECT_POST, - post, +export const requestPosts = () => ({ type: REQUEST_POSTS }); + +export const receivePosts = (posts, next) => ({ + type: RECEIVE_POSTS, + posts, + next, }); -export const unSelectPost = () => ({ - type: UNSELECT_POST, -}); +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, @@ -26,7 +31,6 @@ export const postRead = (post, section) => ({ export const markPostRead = (post, token) => { return (dispatch, getState) => { - const { rules } = getState(); const { selected } = getState(); const url = `/api/posts/${post.id}/`; @@ -50,19 +54,6 @@ export const markPostRead = (post, token) => { }; }; -export const receivePosts = json => ({ - type: RECEIVE_POSTS, - posts: json.items, - next: json.next, -}); - -export const receivePost = post => ({ - type: RECEIVE_POST, - post, -}); - -export const requestPosts = () => ({ type: REQUEST_POSTS }); - export const fetchPostsBySection = (section, page = false) => { return dispatch => { if (section.unread === 0) { @@ -73,12 +64,13 @@ export const fetchPostsBySection = (section, page = false) => { let url = null; - switch ('category' in section) { - case true: + switch (section.type) { + case RULE_TYPE: url = page ? page : `/api/rules/${section.id}/posts/?read=false`; break; - default: + case CATEGORY_TYPE: url = page ? page : `/api/categories/${section.id}/posts/?read=false`; + break; } return fetch(url) @@ -90,14 +82,14 @@ export const fetchPostsBySection = (section, page = false) => { posts[post.id] = post; }); - dispatch(receivePosts({ items: posts, next: json.next })); + dispatch(receivePosts(posts, json.next)); }) .catch(error => { if (error instanceof TypeError) { console.log(`Unable to parse posts from request: ${error}`); } - dispatch(receivePosts({ items: {}, next: null })); + dispatch(receivePosts({}, null)); }); }; }; diff --git a/src/newsreader/js/pages/homepage/actions/rules.js b/src/newsreader/js/pages/homepage/actions/rules.js index 50c8f95..41e2e06 100644 --- a/src/newsreader/js/pages/homepage/actions/rules.js +++ b/src/newsreader/js/pages/homepage/actions/rules.js @@ -49,28 +49,26 @@ export const fetchRule = rule => { // fetch & update category info when the rule is read if (rule.unread === 0) { - dispatch(fetchCategory({ ...category })); + return dispatch(fetchCategory({ ...category })); } }); }; }; export const fetchRulesByCategory = category => { - return (dispatch, getState) => { + return dispatch => { dispatch(requestRules()); return fetch(`/api/categories/${category.id}/rules/`) .then(response => response.json()) .then(responseData => { - dispatch(receiveRules()); - const rules = {}; responseData.forEach(rule => { rules[rule.id] = { ...rule }; }); - setTimeout(dispatch, 500, receiveRules(rules)); + dispatch(receiveRules(rules)); }); }; }; diff --git a/src/newsreader/js/pages/homepage/actions/selected.js b/src/newsreader/js/pages/homepage/actions/selected.js index 0a69266..f767d42 100644 --- a/src/newsreader/js/pages/homepage/actions/selected.js +++ b/src/newsreader/js/pages/homepage/actions/selected.js @@ -35,7 +35,7 @@ const markCategoryRead = (category, token) => { .then(response => response.json()) .then(updatedCategory => { dispatch(receiveCategory({ ...updatedCategory })); - dispatch( + return dispatch( markSectionRead({ ...category, ...updatedCategory, @@ -70,11 +70,11 @@ const markRuleRead = (rule, token) => { }; }; -export const markRead = (selected, token) => { - switch ('category' in selected) { - case true: - return markRuleRead(selected, token); - default: - return markCategoryRead(selected, token); +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/sidebar/CategoryItem.js b/src/newsreader/js/pages/homepage/components/sidebar/CategoryItem.js index 6413406..b03ec30 100644 --- a/src/newsreader/js/pages/homepage/components/sidebar/CategoryItem.js +++ b/src/newsreader/js/pages/homepage/components/sidebar/CategoryItem.js @@ -19,7 +19,7 @@ class CategoryItem extends React.Component { const category = this.props.category; this.props.selectCategory(category); - this.props.fetchPostsBySection(category); + this.props.fetchPostsBySection({ ...category, type: CATEGORY_TYPE }); this.props.fetchCategory(category); } diff --git a/src/newsreader/js/pages/homepage/components/sidebar/RuleItem.js b/src/newsreader/js/pages/homepage/components/sidebar/RuleItem.js index 5f70cfd..7b8278d 100644 --- a/src/newsreader/js/pages/homepage/components/sidebar/RuleItem.js +++ b/src/newsreader/js/pages/homepage/components/sidebar/RuleItem.js @@ -12,7 +12,7 @@ class RuleItem extends React.Component { const rule = { ...this.props.rule }; this.props.selectRule(rule); - this.props.fetchPostsBySection(rule); + this.props.fetchPostsBySection({ ...rule, type: RULE_TYPE }); this.props.fetchRule(rule); } diff --git a/src/newsreader/js/pages/homepage/reducers/posts.js b/src/newsreader/js/pages/homepage/reducers/posts.js index 358b05b..4e7cb8c 100644 --- a/src/newsreader/js/pages/homepage/reducers/posts.js +++ b/src/newsreader/js/pages/homepage/reducers/posts.js @@ -18,23 +18,15 @@ export const posts = (state = { ...defaultState }, action) => { case RECEIVE_POSTS: return { ...state, - type: RECEIVE_POSTS, isFetching: false, items: { ...state.items, ...action.posts }, }; case REQUEST_POSTS: - return { - ...state, - type: REQUEST_POSTS, - isFetching: true, - }; + return { ...state, isFetching: true }; case RECEIVE_POST: - const items = { ...state.items, [action.post.id]: { ...action.post } }; - return { ...state, - items: items, - type: RECEIVE_POST, + items: { ...state.items, [action.post.id]: { ...action.post } }, }; case MARK_SECTION_READ: const updatedPosts = {}; @@ -66,7 +58,6 @@ export const posts = (state = { ...defaultState }, action) => { ...updatedPosts, }, }; - default: return state; } diff --git a/src/newsreader/js/pages/homepage/reducers/selected.js b/src/newsreader/js/pages/homepage/reducers/selected.js index 8b1f7f8..632654d 100644 --- a/src/newsreader/js/pages/homepage/reducers/selected.js +++ b/src/newsreader/js/pages/homepage/reducers/selected.js @@ -26,7 +26,7 @@ export const selected = (state = { ...defaultState }, action) => { if (state.item.clicks >= 2) { return { ...state, - item: { ...action.section, clicks: 0 }, + item: { ...action.section, clicks: 1 }, next: false, lastReached: false, }; @@ -43,7 +43,7 @@ export const selected = (state = { ...defaultState }, action) => { return { ...state, - item: { ...action.section, clicks: 0 }, + item: { ...action.section, clicks: 1 }, next: false, lastReached: false, }; 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..235de76 --- /dev/null +++ b/src/newsreader/js/tests/homepage/actions/category.test.js @@ -0,0 +1,250 @@ +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'; + +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 = { + 1: { id: 1, name: 'Tech', unread: 29 }, + 2: { id: 2, name: 'World news', unread: 956 }, + }; + + 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: 2, + unread: 345, + }, + 5: { + 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, + }, + }; + + fetchMock + .get('/api/categories/', { + body: Object.values({ ...categories }), + headers: { 'content-type': 'application/json' }, + }) + .get('/api/categories/1/rules/', { + body: [{ ...rules[5] }], + headers: { 'content-type': 'application/json' }, + }) + .get('/api/categories/2/rules/', { + body: [{ ...rules[4] }], + 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 = { + 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 + .get('/api/categories/1', { + body: { ...category, unread: 500 }, + headers: { 'content-type': 'application/json' }, + }) + .get('/api/categories/1/rules/', { + body: Object.values({ ...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: { ...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); + }); +}); 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..b9bdddf --- /dev/null +++ b/src/newsreader/js/tests/homepage/actions/post.test.js @@ -0,0 +1,325 @@ +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 constants from '../../../pages/homepage/constants.js'; + +const middlewares = [thunk]; +const mockStore = configureMockStore(middlewares); + +describe('rule 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, + remote_identifier: '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', + publication_date: '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, + remote_identifier: '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', + publication_date: '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, + remote_identifier: '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', + publication_date: '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, + remote_identifier: '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', + publication_date: '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 = { + 2067: { + id: 2067, + remote_identifier: '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', + publication_date: '2020-01-24T19:50:12Z', + url: 'https://arstechnica.com/?p=1648607', + rule: 4, + read: false, + }, + 2141: { + id: 2141, + remote_identifier: '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', + publication_date: '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: Object.values({ ...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 = { + 2067: { + id: 2067, + remote_identifier: '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', + publication_date: '2020-01-24T19:50:12Z', + url: 'https://arstechnica.com/?p=1648607', + rule: 4, + read: false, + }, + 2141: { + id: 2141, + remote_identifier: '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', + publication_date: '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: Object.values({ ...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); + }); +}); 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..509938d --- /dev/null +++ b/src/newsreader/js/tests/homepage/actions/rule.test.js @@ -0,0 +1,254 @@ +import configureMockStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; +import fetchMock from 'fetch-mock'; + +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'; + +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 = { + 1: { + 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', + }, + 2: { + 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 = { + 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.getOnce('/api/categories/1/rules/', { + body: Object.values({ ...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); + }); + }); +}); 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..a55d232 --- /dev/null +++ b/src/newsreader/js/tests/homepage/actions/selected.test.js @@ -0,0 +1,141 @@ +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 ruleActions from '../../../pages/homepage/actions/rules.js'; +import * as constants from '../../../pages/homepage/constants.js'; + +const middlewares = [thunk]; +const mockStore = configureMockStore(middlewares); + +describe('category 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); + }); + }); +}); 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..4211ad2 --- /dev/null +++ b/src/newsreader/js/tests/homepage/reducers/category.test.js @@ -0,0 +1,214 @@ +import { categories as reducer } from '../../../pages/homepage/reducers/categories.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 = { + 0: { id: 9, name: 'Tech', unread: 291 }, + 1: { id: 2, name: 'World news', unread: 444 }, + }; + const action = { type: actions.RECEIVE_CATEGORIES, categories: receivedCategories }; + + const items = {}; + + Object.values({ ...receivedCategories }).forEach(category => { + items[category.id] = category; + }); + + const expectedState = { ...defaultState, items }; + + 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, + remote_identifier: '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, + publication_date: '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, + remote_identifier: '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', + publication_date: '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..fe72ce0 --- /dev/null +++ b/src/newsreader/js/tests/homepage/reducers/post.test.js @@ -0,0 +1,304 @@ +import { posts as reducer } from '../../../pages/homepage/reducers/posts.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, + remote_identifier: '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', + publication_date: '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 = { + 2067: { + id: 2067, + remote_identifier: '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', + publication_date: '2020-01-24T19:50:12Z', + url: 'https://arstechnica.com/?p=1648607', + rule: 4, + read: false, + }, + 2141: { + id: 2141, + remote_identifier: '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', + publication_date: '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 expectedState = { + ...defaultState, + isFetching: false, + items: posts, + }; + + expect(reducer(undefined, action)).toEqual(expectedState); + }); + + it('should return state after marking a rule read', () => { + const posts = { + 2067: { + id: 2067, + remote_identifier: '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', + publication_date: '2020-01-24T19:50:12Z', + url: 'https://arstechnica.com/?p=1648607', + rule: 5, + read: false, + }, + 2141: { + id: 2141, + remote_identifier: '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', + publication_date: '2020-01-25T11:06:46Z', + url: 'https://arstechnica.com/?p=1648757', + rule: 5, + read: false, + }, + 4637: { + id: 4637, + remote_identifier: '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, + publication_date: '2020-01-29T19:08:25Z', + url: 'https://www.bbc.co.uk/news/world-asia-china-51299195', + rule: 4, + read: false, + }, + 4638: { + id: 4638, + remote_identifier: '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, + publication_date: '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, + remote_identifier: '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', + publication_date: '2020-01-24T19:50:12Z', + url: 'https://arstechnica.com/?p=1648607', + rule: 5, + read: false, + }, + 2141: { + id: 2141, + remote_identifier: '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', + publication_date: '2020-01-25T11:06:46Z', + url: 'https://arstechnica.com/?p=1648757', + rule: 5, + read: false, + }, + 4637: { + id: 4637, + remote_identifier: '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, + publication_date: '2020-01-29T19:08:25Z', + url: 'https://www.bbc.co.uk/news/world-asia-china-51299195', + rule: 4, + read: false, + }, + 4638: { + id: 4638, + remote_identifier: '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, + publication_date: '2020-01-29T18:27:56Z', + url: 'https://www.bbc.co.uk/news/world-europe-51294305', + rule: 4, + read: false, + }, + 4589: { + id: 4589, + remote_identifier: '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', + publication_date: '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, + remote_identifier: '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', + publication_date: '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..0332a51 --- /dev/null +++ b/src/newsreader/js/tests/homepage/reducers/rule.test.js @@ -0,0 +1,181 @@ +import { rules as reducer } from '../../../pages/homepage/reducers/rules.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 = { + 1: { + 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', + }, + 2: { + 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 expectedState = { ...defaultState, items: { ...rules } }; + + 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, + remote_identifier: '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', + publication_date: '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..365143a --- /dev/null +++ b/src/newsreader/js/tests/homepage/reducers/selected.test.js @@ -0,0 +1,399 @@ +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 = { + 2067: { + id: 2067, + remote_identifier: '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', + publication_date: '2020-01-24T19:50:12Z', + url: 'https://arstechnica.com/?p=1648607', + rule: 4, + read: false, + }, + 2141: { + id: 2141, + remote_identifier: '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', + publication_date: '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', () => { + const post = { + id: 2067, + remote_identifier: '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', + publication_date: '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 selecting a post', () => { + const post = { + id: 2067, + remote_identifier: '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', + publication_date: '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, + remote_identifier: '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', + publication_date: '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, + remote_identifier: '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', + publication_date: '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); + }); +}); From e1e6571bb0c05973746ceeffd6fc718117483d8e Mon Sep 17 00:00:00 2001 From: Sonny Date: Fri, 31 Jan 2020 21:29:11 +0100 Subject: [PATCH 040/364] Update post serializer fields --- requirements/base.txt | 2 +- .../js/pages/homepage/components/PostModal.js | 2 +- .../homepage/components/feedlist/PostItem.js | 2 +- .../homepage/components/feedlist/RuleItem.js | 2 +- .../js/tests/homepage/actions/post.test.js | 32 ++++++------ .../tests/homepage/reducers/category.test.js | 8 +-- .../js/tests/homepage/reducers/post.test.js | 52 +++++++++---------- .../js/tests/homepage/reducers/rule.test.js | 4 +- .../tests/homepage/reducers/selected.test.js | 24 ++++----- src/newsreader/news/core/serializers.py | 7 ++- .../core/tests/endpoints/post/detail/tests.py | 4 +- 11 files changed, 71 insertions(+), 68 deletions(-) diff --git a/requirements/base.txt b/requirements/base.txt index b5a6858..033f18c 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -8,7 +8,7 @@ django-celery-beat==1.5.0 djangorestframework==3.9.4 django-rest-swagger==2.2.0 django-registration-redux==2.6 -lxml==4.3.4 +lxml==4.4.2 feedparser==5.2.1 idna==2.8 pytz==2018.9 diff --git a/src/newsreader/js/pages/homepage/components/PostModal.js b/src/newsreader/js/pages/homepage/components/PostModal.js index 75650fb..b077230 100644 --- a/src/newsreader/js/pages/homepage/components/PostModal.js +++ b/src/newsreader/js/pages/homepage/components/PostModal.js @@ -28,7 +28,7 @@ class PostModal extends React.Component { render() { const post = this.props.post; - const publicationDate = formatDatetime(post.publication_date); + const publicationDate = formatDatetime(post.publicationDate); const titleClassName = post.read ? 'post__title post__title--read' : 'post__title'; return ( diff --git a/src/newsreader/js/pages/homepage/components/feedlist/PostItem.js b/src/newsreader/js/pages/homepage/components/feedlist/PostItem.js index 865af41..47d4832 100644 --- a/src/newsreader/js/pages/homepage/components/feedlist/PostItem.js +++ b/src/newsreader/js/pages/homepage/components/feedlist/PostItem.js @@ -8,7 +8,7 @@ import { formatDatetime } from '../../../../utils.js'; class PostItem extends React.Component { render() { const post = this.props.post; - const publicationDate = formatDatetime(post.publication_date); + const publicationDate = formatDatetime(post.publicationDate); const titleClassName = post.read ? 'posts-header__title posts-header__title--read' : 'posts-header__title'; diff --git a/src/newsreader/js/pages/homepage/components/feedlist/RuleItem.js b/src/newsreader/js/pages/homepage/components/feedlist/RuleItem.js index f7a8a9b..608e8a1 100644 --- a/src/newsreader/js/pages/homepage/components/feedlist/RuleItem.js +++ b/src/newsreader/js/pages/homepage/components/feedlist/RuleItem.js @@ -5,7 +5,7 @@ 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.publication_date) - new Date(firstEl.publication_date); + return new Date(secondEl.publicationDate) - new Date(firstEl.publicationDate); }); const postItems = posts.map(post => { diff --git a/src/newsreader/js/tests/homepage/actions/post.test.js b/src/newsreader/js/tests/homepage/actions/post.test.js index b9bdddf..31bb666 100644 --- a/src/newsreader/js/tests/homepage/actions/post.test.js +++ b/src/newsreader/js/tests/homepage/actions/post.test.js @@ -22,13 +22,13 @@ describe('rule actions', () => { it('should create an action receive a post', () => { const post = { id: 2067, - remote_identifier: 'https://arstechnica.com/?p=1648607', + 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', - publication_date: '2020-01-24T19:50:12Z', + publicationDate: '2020-01-24T19:50:12Z', url: 'https://arstechnica.com/?p=1648607', rule: 5, read: false, @@ -45,13 +45,13 @@ describe('rule actions', () => { it('should create an action to select a post', () => { const post = { id: 2067, - remote_identifier: 'https://arstechnica.com/?p=1648607', + 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', - publication_date: '2020-01-24T19:50:12Z', + publicationDate: '2020-01-24T19:50:12Z', url: 'https://arstechnica.com/?p=1648607', rule: 5, read: false, @@ -74,13 +74,13 @@ describe('rule actions', () => { it('should create an action mark a post read', () => { const post = { id: 2067, - remote_identifier: 'https://arstechnica.com/?p=1648607', + 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', - publication_date: '2020-01-24T19:50:12Z', + publicationDate: '2020-01-24T19:50:12Z', url: 'https://arstechnica.com/?p=1648607', rule: 5, read: false, @@ -107,13 +107,13 @@ describe('rule actions', () => { it('should create multiple actions to mark post read', () => { const post = { id: 2067, - remote_identifier: 'https://arstechnica.com/?p=1648607', + 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', - publication_date: '2020-01-24T19:50:12Z', + publicationDate: '2020-01-24T19:50:12Z', url: 'https://arstechnica.com/?p=1648607', rule: 5, read: false, @@ -166,25 +166,25 @@ describe('rule actions', () => { const posts = { 2067: { id: 2067, - remote_identifier: 'https://arstechnica.com/?p=1648607', + 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', - publication_date: '2020-01-24T19:50:12Z', + publicationDate: '2020-01-24T19:50:12Z', url: 'https://arstechnica.com/?p=1648607', rule: 4, read: false, }, 2141: { id: 2141, - remote_identifier: 'https://arstechnica.com/?p=1648757', + 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', - publication_date: '2020-01-25T11:06:46Z', + publicationDate: '2020-01-25T11:06:46Z', url: 'https://arstechnica.com/?p=1648757', rule: 4, read: false, @@ -236,25 +236,25 @@ describe('rule actions', () => { const posts = { 2067: { id: 2067, - remote_identifier: 'https://arstechnica.com/?p=1648607', + 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', - publication_date: '2020-01-24T19:50:12Z', + publicationDate: '2020-01-24T19:50:12Z', url: 'https://arstechnica.com/?p=1648607', rule: 4, read: false, }, 2141: { id: 2141, - remote_identifier: 'https://arstechnica.com/?p=1648757', + 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', - publication_date: '2020-01-25T11:06:46Z', + publicationDate: '2020-01-25T11:06:46Z', url: 'https://arstechnica.com/?p=1648757', rule: 4, read: false, diff --git a/src/newsreader/js/tests/homepage/reducers/category.test.js b/src/newsreader/js/tests/homepage/reducers/category.test.js index 4211ad2..2eeed65 100644 --- a/src/newsreader/js/tests/homepage/reducers/category.test.js +++ b/src/newsreader/js/tests/homepage/reducers/category.test.js @@ -67,12 +67,12 @@ describe('category reducer', () => { const post = { id: 2091, - remote_identifier: 'https://www.bbc.co.uk/news/world-asia-china-51249208', + 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, - publication_date: '2020-01-26T05:54:14Z', + publicationDate: '2020-01-26T05:54:14Z', url: 'https://www.bbc.co.uk/news/world-asia-china-51249208', rule: 4, read: false, @@ -117,12 +117,12 @@ describe('category reducer', () => { const post = { id: 2182, - remote_identifier: 'https://arstechnica.com/?p=1648871', + 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', - publication_date: '2020-01-25T18:34:20Z', + publicationDate: '2020-01-25T18:34:20Z', url: 'https://arstechnica.com/?p=1648871', rule: 1, read: false, diff --git a/src/newsreader/js/tests/homepage/reducers/post.test.js b/src/newsreader/js/tests/homepage/reducers/post.test.js index fe72ce0..b54c3a1 100644 --- a/src/newsreader/js/tests/homepage/reducers/post.test.js +++ b/src/newsreader/js/tests/homepage/reducers/post.test.js @@ -18,13 +18,13 @@ describe('post actions', () => { it('should return state after receiving a post', () => { const post = { id: 2067, - remote_identifier: 'https://arstechnica.com/?p=1648607', + 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', - publication_date: '2020-01-24T19:50:12Z', + publicationDate: '2020-01-24T19:50:12Z', url: 'https://arstechnica.com/?p=1648607', rule: 4, read: false, @@ -48,25 +48,25 @@ describe('post actions', () => { const posts = { 2067: { id: 2067, - remote_identifier: 'https://arstechnica.com/?p=1648607', + 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', - publication_date: '2020-01-24T19:50:12Z', + publicationDate: '2020-01-24T19:50:12Z', url: 'https://arstechnica.com/?p=1648607', rule: 4, read: false, }, 2141: { id: 2141, - remote_identifier: 'https://arstechnica.com/?p=1648757', + 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', - publication_date: '2020-01-25T11:06:46Z', + publicationDate: '2020-01-25T11:06:46Z', url: 'https://arstechnica.com/?p=1648757', rule: 4, read: false, @@ -92,49 +92,49 @@ describe('post actions', () => { const posts = { 2067: { id: 2067, - remote_identifier: 'https://arstechnica.com/?p=1648607', + 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', - publication_date: '2020-01-24T19:50:12Z', + publicationDate: '2020-01-24T19:50:12Z', url: 'https://arstechnica.com/?p=1648607', rule: 5, read: false, }, 2141: { id: 2141, - remote_identifier: 'https://arstechnica.com/?p=1648757', + 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', - publication_date: '2020-01-25T11:06:46Z', + publicationDate: '2020-01-25T11:06:46Z', url: 'https://arstechnica.com/?p=1648757', rule: 5, read: false, }, 4637: { id: 4637, - remote_identifier: 'https://www.bbc.co.uk/news/world-asia-china-51299195', + 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, - publication_date: '2020-01-29T19:08:25Z', + publicationDate: '2020-01-29T19:08:25Z', url: 'https://www.bbc.co.uk/news/world-asia-china-51299195', rule: 4, read: false, }, 4638: { id: 4638, - remote_identifier: 'https://www.bbc.co.uk/news/world-europe-51294305', + 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, - publication_date: '2020-01-29T18:27:56Z', + publicationDate: '2020-01-29T18:27:56Z', url: 'https://www.bbc.co.uk/news/world-europe-51294305', rule: 4, read: false, @@ -174,61 +174,61 @@ describe('post actions', () => { const posts = { 2067: { id: 2067, - remote_identifier: 'https://arstechnica.com/?p=1648607', + 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', - publication_date: '2020-01-24T19:50:12Z', + publicationDate: '2020-01-24T19:50:12Z', url: 'https://arstechnica.com/?p=1648607', rule: 5, read: false, }, 2141: { id: 2141, - remote_identifier: 'https://arstechnica.com/?p=1648757', + 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', - publication_date: '2020-01-25T11:06:46Z', + publicationDate: '2020-01-25T11:06:46Z', url: 'https://arstechnica.com/?p=1648757', rule: 5, read: false, }, 4637: { id: 4637, - remote_identifier: 'https://www.bbc.co.uk/news/world-asia-china-51299195', + 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, - publication_date: '2020-01-29T19:08:25Z', + publicationDate: '2020-01-29T19:08:25Z', url: 'https://www.bbc.co.uk/news/world-asia-china-51299195', rule: 4, read: false, }, 4638: { id: 4638, - remote_identifier: 'https://www.bbc.co.uk/news/world-europe-51294305', + 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, - publication_date: '2020-01-29T18:27:56Z', + publicationDate: '2020-01-29T18:27:56Z', url: 'https://www.bbc.co.uk/news/world-europe-51294305', rule: 4, read: false, }, 4589: { id: 4589, - remote_identifier: 'https://tweakers.net/nieuws/162878', + 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', - publication_date: '2020-01-29T19:03:01Z', + publicationDate: '2020-01-29T19:03:01Z', url: 'https://tweakers.net/nieuws/162878/analyse-nintendo-verdiende-miljard-dollar-aan-mobiele-games.html', rule: 7, @@ -236,12 +236,12 @@ describe('post actions', () => { }, 4594: { id: 4594, - remote_identifier: 'https://tweakers.net/nieuws/162870', + 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', - publication_date: '2020-01-29T16:29:40Z', + publicationDate: '2020-01-29T16:29:40Z', url: 'https://tweakers.net/nieuws/162870/samsung-kondigt-eerste-tablet-met-5g-aan.html', rule: 7, diff --git a/src/newsreader/js/tests/homepage/reducers/rule.test.js b/src/newsreader/js/tests/homepage/reducers/rule.test.js index 0332a51..67e1f4c 100644 --- a/src/newsreader/js/tests/homepage/reducers/rule.test.js +++ b/src/newsreader/js/tests/homepage/reducers/rule.test.js @@ -87,12 +87,12 @@ describe('category reducer', () => { const post = { id: 2182, - remote_identifier: 'https://arstechnica.com/?p=1648871', + 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', - publication_date: '2020-01-25T18:34:20Z', + publicationDate: '2020-01-25T18:34:20Z', url: 'https://arstechnica.com/?p=1648871', rule: 1, read: false, diff --git a/src/newsreader/js/tests/homepage/reducers/selected.test.js b/src/newsreader/js/tests/homepage/reducers/selected.test.js index 365143a..22a5e7e 100644 --- a/src/newsreader/js/tests/homepage/reducers/selected.test.js +++ b/src/newsreader/js/tests/homepage/reducers/selected.test.js @@ -214,25 +214,25 @@ describe('selected reducer', () => { const posts = { 2067: { id: 2067, - remote_identifier: 'https://arstechnica.com/?p=1648607', + 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', - publication_date: '2020-01-24T19:50:12Z', + publicationDate: '2020-01-24T19:50:12Z', url: 'https://arstechnica.com/?p=1648607', rule: 4, read: false, }, 2141: { id: 2141, - remote_identifier: 'https://arstechnica.com/?p=1648757', + 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', - publication_date: '2020-01-25T11:06:46Z', + publicationDate: '2020-01-25T11:06:46Z', url: 'https://arstechnica.com/?p=1648757', rule: 4, read: false, @@ -257,13 +257,13 @@ describe('selected reducer', () => { it('should return state after receiving a post', () => { const post = { id: 2067, - remote_identifier: 'https://arstechnica.com/?p=1648607', + 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', - publication_date: '2020-01-24T19:50:12Z', + publicationDate: '2020-01-24T19:50:12Z', url: 'https://arstechnica.com/?p=1648607', rule: 4, read: false, @@ -283,13 +283,13 @@ describe('selected reducer', () => { it('should return state after selecting a post', () => { const post = { id: 2067, - remote_identifier: 'https://arstechnica.com/?p=1648607', + 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', - publication_date: '2020-01-24T19:50:12Z', + publicationDate: '2020-01-24T19:50:12Z', url: 'https://arstechnica.com/?p=1648607', rule: 4, read: false, @@ -308,13 +308,13 @@ describe('selected reducer', () => { it('should return state after unselecting a post', () => { const post = { id: 2067, - remote_identifier: 'https://arstechnica.com/?p=1648607', + 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', - publication_date: '2020-01-24T19:50:12Z', + publicationDate: '2020-01-24T19:50:12Z', url: 'https://arstechnica.com/?p=1648607', rule: 4, read: false, @@ -334,13 +334,13 @@ describe('selected reducer', () => { it('should return state after marking a post read', () => { const post = { id: 2067, - remote_identifier: 'https://arstechnica.com/?p=1648607', + 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', - publication_date: '2020-01-24T19:50:12Z', + publicationDate: '2020-01-24T19:50:12Z', url: 'https://arstechnica.com/?p=1648607', rule: 4, read: false, diff --git a/src/newsreader/news/core/serializers.py b/src/newsreader/news/core/serializers.py index 791d873..e18070f 100644 --- a/src/newsreader/news/core/serializers.py +++ b/src/newsreader/news/core/serializers.py @@ -5,18 +5,21 @@ from newsreader.news.core.models import Category, Post class PostSerializer(serializers.ModelSerializer): + publicationDate = serializers.DateTimeField(source="publication_date", required=False) + remoteIdentifier = serializers.CharField(source="remote_identifier", required=False) + class Meta: model = Post fields = ( "id", - "remote_identifier", "title", "body", "author", - "publication_date", "url", "rule", "read", + "publicationDate", + "remoteIdentifier", ) diff --git a/src/newsreader/news/core/tests/endpoints/post/detail/tests.py b/src/newsreader/news/core/tests/endpoints/post/detail/tests.py index acc4bd1..bc184a3 100644 --- a/src/newsreader/news/core/tests/endpoints/post/detail/tests.py +++ b/src/newsreader/news/core/tests/endpoints/post/detail/tests.py @@ -28,10 +28,10 @@ class PostDetailViewTestCase(TestCase): self.assertTrue("title" in data) self.assertTrue("body" in data) self.assertTrue("author" in data) - self.assertTrue("publication_date" in data) + self.assertTrue("publicationDate" in data) self.assertTrue("url" in data) self.assertTrue("rule" in data) - self.assertTrue("remote_identifier" in data) + self.assertTrue("remoteIdentifier" in data) def test_not_known(self): response = self.client.get(reverse("api:posts-detail", args=[100])) From f9bce3507cef3841b464438360fbe54d8c65e23d Mon Sep 17 00:00:00 2001 From: Sonny Date: Sat, 1 Feb 2020 21:42:29 +0100 Subject: [PATCH 041/364] Redux actions refactor --- .../js/pages/homepage/actions/categories.js | 68 +++++++------------ .../js/pages/homepage/actions/posts.js | 10 +-- .../js/pages/homepage/actions/rules.js | 10 +-- .../js/pages/homepage/reducers/categories.js | 15 ++-- .../js/pages/homepage/reducers/posts.js | 16 +++-- .../js/pages/homepage/reducers/rules.js | 11 +-- .../js/pages/homepage/reducers/selected.js | 10 +-- .../tests/homepage/actions/category.test.js | 50 +++++++------- .../js/tests/homepage/actions/post.test.js | 20 +++--- .../js/tests/homepage/actions/rule.test.js | 20 +++--- .../tests/homepage/reducers/category.test.js | 20 +++--- .../js/tests/homepage/reducers/post.test.js | 13 ++-- .../js/tests/homepage/reducers/rule.test.js | 13 ++-- .../tests/homepage/reducers/selected.test.js | 36 ++++++++-- src/newsreader/js/utils.js | 10 +++ 15 files changed, 161 insertions(+), 161 deletions(-) diff --git a/src/newsreader/js/pages/homepage/actions/categories.js b/src/newsreader/js/pages/homepage/actions/categories.js index f30424f..0fc63a6 100644 --- a/src/newsreader/js/pages/homepage/actions/categories.js +++ b/src/newsreader/js/pages/homepage/actions/categories.js @@ -28,49 +28,6 @@ export const receiveCategories = categories => ({ export const requestCategory = () => ({ type: REQUEST_CATEGORY }); export const requestCategories = () => ({ type: REQUEST_CATEGORIES }); -export const fetchCategories = () => { - return dispatch => { - dispatch(requestCategories()); - - return fetch('/api/categories/') - .then(response => response.json()) - .then(json => { - const categories = {}; - - json.forEach(category => { - categories[category.id] = { ...category }; - }); - - dispatch(receiveCategories(categories)); - return json; - }) - .then(json => { - const promises = json.map(category => { - return fetch(`/api/categories/${category.id}/rules/`); - }); - - dispatch(requestRules()); - return Promise.all(promises); - }) - .then(responses => { - return Promise.all(responses.map(response => response.json())); - }) - .then(responseData => { - let rules = {}; - - responseData.forEach(json => { - const data = Object.values(json); - - data.forEach(item => { - rules = { ...rules, [item.id]: item }; - }); - }); - - dispatch(receiveRules(rules)); - }); - }; -}; - export const fetchCategory = category => { return (dispatch, getState) => { const { selected } = getState(); @@ -93,3 +50,28 @@ export const fetchCategory = category => { }); }; }; + +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()))); + }; +}; diff --git a/src/newsreader/js/pages/homepage/actions/posts.js b/src/newsreader/js/pages/homepage/actions/posts.js index 598dde1..d1fc79b 100644 --- a/src/newsreader/js/pages/homepage/actions/posts.js +++ b/src/newsreader/js/pages/homepage/actions/posts.js @@ -75,15 +75,7 @@ export const fetchPostsBySection = (section, page = false) => { return fetch(url) .then(response => response.json()) - .then(json => { - const posts = {}; - - json.results.forEach(post => { - posts[post.id] = post; - }); - - dispatch(receivePosts(posts, json.next)); - }) + .then(posts => dispatch(receivePosts(posts.results, posts.next))) .catch(error => { if (error instanceof TypeError) { console.log(`Unable to parse posts from request: ${error}`); diff --git a/src/newsreader/js/pages/homepage/actions/rules.js b/src/newsreader/js/pages/homepage/actions/rules.js index 41e2e06..0f45f1d 100644 --- a/src/newsreader/js/pages/homepage/actions/rules.js +++ b/src/newsreader/js/pages/homepage/actions/rules.js @@ -61,14 +61,6 @@ export const fetchRulesByCategory = category => { return fetch(`/api/categories/${category.id}/rules/`) .then(response => response.json()) - .then(responseData => { - const rules = {}; - - responseData.forEach(rule => { - rules[rule.id] = { ...rule }; - }); - - dispatch(receiveRules(rules)); - }); + .then(rules => dispatch(receiveRules(rules))); }; }; diff --git a/src/newsreader/js/pages/homepage/reducers/categories.js b/src/newsreader/js/pages/homepage/reducers/categories.js index a1f8961..612b98f 100644 --- a/src/newsreader/js/pages/homepage/reducers/categories.js +++ b/src/newsreader/js/pages/homepage/reducers/categories.js @@ -2,6 +2,8 @@ import { isEqual } from 'lodash'; import { CATEGORY_TYPE, RULE_TYPE } from '../constants.js'; +import { objectsFromArray } from '../../../utils.js'; + import { RECEIVE_CATEGORY, RECEIVE_CATEGORIES, @@ -26,13 +28,7 @@ export const categories = (state = { ...defaultState }, action) => { isFetching: false, }; case RECEIVE_CATEGORIES: - const receivedCategories = {}; - - Object.values({ ...action.categories }).forEach(category => { - receivedCategories[category.id] = { - ...category, - }; - }); + const receivedCategories = objectsFromArray(action.categories, 'id'); return { ...state, @@ -41,10 +37,7 @@ export const categories = (state = { ...defaultState }, action) => { }; case REQUEST_CATEGORIES: case REQUEST_CATEGORY: - return { - ...state, - isFetching: true, - }; + return { ...state, isFetching: true }; case MARK_POST_READ: let category = {}; diff --git a/src/newsreader/js/pages/homepage/reducers/posts.js b/src/newsreader/js/pages/homepage/reducers/posts.js index 4e7cb8c..220c59b 100644 --- a/src/newsreader/js/pages/homepage/reducers/posts.js +++ b/src/newsreader/js/pages/homepage/reducers/posts.js @@ -1,4 +1,6 @@ import { isEqual } from 'lodash'; + +import { objectsFromArray } from '../../../utils.js'; import { CATEGORY_TYPE, RULE_TYPE } from '../constants.js'; import { @@ -15,12 +17,6 @@ const defaultState = { items: {}, isFetching: false }; export const posts = (state = { ...defaultState }, action) => { switch (action.type) { - case RECEIVE_POSTS: - return { - ...state, - isFetching: false, - items: { ...state.items, ...action.posts }, - }; case REQUEST_POSTS: return { ...state, isFetching: true }; case RECEIVE_POST: @@ -28,6 +24,14 @@ export const posts = (state = { ...defaultState }, action) => { ...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 = []; diff --git a/src/newsreader/js/pages/homepage/reducers/rules.js b/src/newsreader/js/pages/homepage/reducers/rules.js index 0802576..ea3480c 100644 --- a/src/newsreader/js/pages/homepage/reducers/rules.js +++ b/src/newsreader/js/pages/homepage/reducers/rules.js @@ -1,5 +1,7 @@ import { isEqual } from 'lodash'; +import { objectsFromArray } from '../../../utils.js'; + import { CATEGORY_TYPE, RULE_TYPE } from '../constants.js'; import { @@ -17,14 +19,13 @@ export const rules = (state = { ...defaultState }, action) => { switch (action.type) { case REQUEST_RULE: case REQUEST_RULES: - return { - ...state, - isFetching: true, - }; + return { ...state, isFetching: true }; case RECEIVE_RULES: + const receivedItems = objectsFromArray(action.rules, 'id'); + return { ...state, - items: { ...state.items, ...action.rules }, + items: { ...state.items, ...receivedItems }, isFetching: false, }; case RECEIVE_RULE: diff --git a/src/newsreader/js/pages/homepage/reducers/selected.js b/src/newsreader/js/pages/homepage/reducers/selected.js index 632654d..babcb82 100644 --- a/src/newsreader/js/pages/homepage/reducers/selected.js +++ b/src/newsreader/js/pages/homepage/reducers/selected.js @@ -67,15 +67,9 @@ export const selected = (state = { ...defaultState }, action) => { ...state, }; case SELECT_POST: - return { - ...state, - post: action.post, - }; + return { ...state, post: action.post }; case UNSELECT_POST: - return { - ...state, - post: {}, - }; + return { ...state, post: {} }; case MARK_POST_READ: return { ...state, diff --git a/src/newsreader/js/tests/homepage/actions/category.test.js b/src/newsreader/js/tests/homepage/actions/category.test.js index 235de76..a6be5ad 100644 --- a/src/newsreader/js/tests/homepage/actions/category.test.js +++ b/src/newsreader/js/tests/homepage/actions/category.test.js @@ -96,22 +96,13 @@ describe('category actions', () => { }); it('should create multiple actions when fetching categories', () => { - const categories = { - 1: { id: 1, name: 'Tech', unread: 29 }, - 2: { id: 2, name: 'World news', unread: 956 }, - }; + const categories = [ + { id: 1, name: 'Tech', unread: 29 }, + { id: 2, name: 'World news', unread: 956 }, + ]; - 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: 2, - unread: 345, - }, - 5: { + const rules = [ + { id: 5, name: 'Ars Technica', url: 'http://feeds.arstechnica.com/arstechnica/index?fmt=xml', @@ -119,19 +110,28 @@ describe('category actions', () => { 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: Object.values({ ...categories }), + body: categories, headers: { 'content-type': 'application/json' }, }) .get('/api/categories/1/rules/', { - body: [{ ...rules[5] }], + body: [{ ...rules[0] }], headers: { 'content-type': 'application/json' }, }) .get('/api/categories/2/rules/', { - body: [{ ...rules[4] }], + body: [{ ...rules[1] }], headers: { 'content-type': 'application/json' }, }); @@ -161,8 +161,8 @@ describe('category actions', () => { unread: 0, }; - const rules = { - 1: { + const rules = [ + { id: 1, name: 'Ars Technica', url: 'http://feeds.arstechnica.com/arstechnica/index?fmt=xml', @@ -170,7 +170,7 @@ describe('category actions', () => { category: 1, unread: 200, }, - 2: { + { id: 2, name: 'Hacker News', url: 'https://news.ycombinator.com/rss', @@ -178,7 +178,7 @@ describe('category actions', () => { category: 1, unread: 350, }, - }; + ]; fetchMock .get('/api/categories/1', { @@ -186,7 +186,7 @@ describe('category actions', () => { headers: { 'content-type': 'application/json' }, }) .get('/api/categories/1/rules/', { - body: Object.values({ ...rules }), + body: rules, headers: { 'content-type': 'application/json' }, }); @@ -197,7 +197,7 @@ describe('category actions', () => { category: { ...category, unread: 500 }, }, { type: ruleActions.REQUEST_RULES }, - { type: ruleActions.RECEIVE_RULES, rules: { ...rules } }, + { type: ruleActions.RECEIVE_RULES, rules }, ]; const store = mockStore({ diff --git a/src/newsreader/js/tests/homepage/actions/post.test.js b/src/newsreader/js/tests/homepage/actions/post.test.js index 31bb666..65967b4 100644 --- a/src/newsreader/js/tests/homepage/actions/post.test.js +++ b/src/newsreader/js/tests/homepage/actions/post.test.js @@ -163,8 +163,8 @@ describe('rule actions', () => { }); it('should create multiple actions to fetch posts by rule', () => { - const posts = { - 2067: { + const posts = [ + { id: 2067, remoteIdentifier: 'https://arstechnica.com/?p=1648607', title: @@ -177,7 +177,7 @@ describe('rule actions', () => { rule: 4, read: false, }, - 2141: { + { id: 2141, remoteIdentifier: 'https://arstechnica.com/?p=1648757', title: 'The most complete brain map ever is here: A fly’s “connectome”', @@ -189,7 +189,7 @@ describe('rule actions', () => { rule: 4, read: false, }, - }; + ]; const rule = { id: 4, @@ -206,7 +206,7 @@ describe('rule actions', () => { count: 2, next: 'https://durp.com/api/rules/4/posts/?page=2&read=false', previous: null, - results: Object.values({ ...posts }), + results: posts, }, headers: { 'content-type': 'application/json' }, }); @@ -233,8 +233,8 @@ describe('rule actions', () => { }); it('should create multiple actions to fetch posts by category', () => { - const posts = { - 2067: { + const posts = [ + { id: 2067, remoteIdentifier: 'https://arstechnica.com/?p=1648607', title: @@ -247,7 +247,7 @@ describe('rule actions', () => { rule: 4, read: false, }, - 2141: { + { id: 2141, remoteIdentifier: 'https://arstechnica.com/?p=1648757', title: 'The most complete brain map ever is here: A fly’s “connectome”', @@ -259,7 +259,7 @@ describe('rule actions', () => { rule: 4, read: false, }, - }; + ]; const category = { id: 1, @@ -273,7 +273,7 @@ describe('rule actions', () => { count: 2, next: 'https://durp.com/api/categories/4/posts/?page=2&read=false', previous: null, - results: Object.values({ ...posts }), + results: posts, }, headers: { 'content-type': 'application/json' }, }); diff --git a/src/newsreader/js/tests/homepage/actions/rule.test.js b/src/newsreader/js/tests/homepage/actions/rule.test.js index 509938d..70a3a89 100644 --- a/src/newsreader/js/tests/homepage/actions/rule.test.js +++ b/src/newsreader/js/tests/homepage/actions/rule.test.js @@ -2,6 +2,8 @@ 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'; @@ -63,8 +65,8 @@ describe('rule actions', () => { }); it('should create an action to receive multiple rules', () => { - const rules = { - 1: { + const rules = [ + { id: 1, name: 'Test rule', unread: 100, @@ -72,7 +74,7 @@ describe('rule actions', () => { url: 'http://feeds.arstechnica.com/arstechnica/index?fmt=xml', favicon: 'https://cdn.arstechnica.net/favicon.ico', }, - 2: { + { id: 2, name: 'Test rule 2', unread: 50, @@ -80,7 +82,7 @@ describe('rule actions', () => { url: 'https://xkcd.com/atom.xml', favicon: null, }, - }; + ]; const expectedAction = { type: actions.RECEIVE_RULES, @@ -211,8 +213,8 @@ describe('rule actions', () => { unread: 0, }; - const rules = { - 1: { + const rules = [ + { id: 1, name: 'Ars Technica', url: 'http://feeds.arstechnica.com/arstechnica/index?fmt=xml', @@ -220,7 +222,7 @@ describe('rule actions', () => { category: 1, unread: 200, }, - 2: { + { id: 2, name: 'Hacker News', url: 'https://news.ycombinator.com/rss', @@ -228,10 +230,10 @@ describe('rule actions', () => { category: 1, unread: 350, }, - }; + ]; fetchMock.getOnce('/api/categories/1/rules/', { - body: Object.values({ ...rules }), + body: rules, headers: { 'content-type': 'application/json' }, }); diff --git a/src/newsreader/js/tests/homepage/reducers/category.test.js b/src/newsreader/js/tests/homepage/reducers/category.test.js index 2eeed65..f5c27ae 100644 --- a/src/newsreader/js/tests/homepage/reducers/category.test.js +++ b/src/newsreader/js/tests/homepage/reducers/category.test.js @@ -1,5 +1,7 @@ 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'; @@ -25,19 +27,15 @@ describe('category reducer', () => { }); it('should return state after receiving multiple categories', () => { - const receivedCategories = { - 0: { id: 9, name: 'Tech', unread: 291 }, - 1: { id: 2, name: 'World news', unread: 444 }, - }; + const receivedCategories = [ + { id: 9, name: 'Tech', unread: 291 }, + { id: 2, name: 'World news', unread: 444 }, + ]; + const action = { type: actions.RECEIVE_CATEGORIES, categories: receivedCategories }; - const items = {}; - - Object.values({ ...receivedCategories }).forEach(category => { - items[category.id] = category; - }); - - const expectedState = { ...defaultState, items }; + const expectedCategories = objectsFromArray(receivedCategories, 'id'); + const expectedState = { ...defaultState, items: expectedCategories }; expect(reducer(undefined, 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 index b54c3a1..ef4234a 100644 --- a/src/newsreader/js/tests/homepage/reducers/post.test.js +++ b/src/newsreader/js/tests/homepage/reducers/post.test.js @@ -1,5 +1,7 @@ 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'; @@ -45,8 +47,8 @@ describe('post actions', () => { }); it('should return state after receiving posts', () => { - const posts = { - 2067: { + const posts = [ + { id: 2067, remoteIdentifier: 'https://arstechnica.com/?p=1648607', title: @@ -59,7 +61,7 @@ describe('post actions', () => { rule: 4, read: false, }, - 2141: { + { id: 2141, remoteIdentifier: 'https://arstechnica.com/?p=1648757', title: 'The most complete brain map ever is here: A fly’s “connectome”', @@ -71,7 +73,7 @@ describe('post actions', () => { rule: 4, read: false, }, - }; + ]; const action = { type: actions.RECEIVE_POSTS, @@ -79,10 +81,11 @@ describe('post actions', () => { posts, }; + const expectedPosts = objectsFromArray(posts, 'id'); const expectedState = { ...defaultState, isFetching: false, - items: posts, + items: expectedPosts, }; expect(reducer(undefined, 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 index 67e1f4c..171c301 100644 --- a/src/newsreader/js/tests/homepage/reducers/rule.test.js +++ b/src/newsreader/js/tests/homepage/reducers/rule.test.js @@ -1,5 +1,7 @@ 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'; @@ -49,8 +51,8 @@ describe('category reducer', () => { }); it('should return state after receiving multiple rules', () => { - const rules = { - 1: { + const rules = [ + { id: 1, name: 'Test rule', unread: 100, @@ -58,7 +60,7 @@ describe('category reducer', () => { url: 'http://feeds.arstechnica.com/arstechnica/index?fmt=xml', favicon: 'https://cdn.arstechnica.net/favicon.ico', }, - 2: { + { id: 2, name: 'Another Test rule', unread: 444, @@ -66,11 +68,12 @@ describe('category reducer', () => { url: 'http://feeds.arstechnica.com/arstechnica/index?fmt=xml', favicon: 'https://cdn.arstechnica.net/favicon.ico', }, - }; + ]; const action = { type: actions.RECEIVE_RULES, rules }; - const expectedState = { ...defaultState, items: { ...rules } }; + const mappedRules = objectsFromArray(rules, 'id'); + const expectedState = { ...defaultState, items: { ...mappedRules } }; expect(reducer(undefined, 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 index 22a5e7e..215c6e1 100644 --- a/src/newsreader/js/tests/homepage/reducers/selected.test.js +++ b/src/newsreader/js/tests/homepage/reducers/selected.test.js @@ -211,8 +211,8 @@ describe('selected reducer', () => { }); it('should return state after receiving posts', () => { - const posts = { - 2067: { + const posts = [ + { id: 2067, remoteIdentifier: 'https://arstechnica.com/?p=1648607', title: @@ -225,7 +225,7 @@ describe('selected reducer', () => { rule: 4, read: false, }, - 2141: { + { id: 2141, remoteIdentifier: 'https://arstechnica.com/?p=1648757', title: 'The most complete brain map ever is here: A fly’s “connectome”', @@ -237,7 +237,7 @@ describe('selected reducer', () => { rule: 4, read: false, }, - }; + ]; const action = { type: postActions.RECEIVE_POSTS, @@ -254,7 +254,7 @@ describe('selected reducer', () => { expect(reducer(undefined, action)).toEqual(expectedState); }); - it('should return state after receiving a post', () => { + it('should return state after receiving a post which is selected', () => { const post = { id: 2067, remoteIdentifier: 'https://arstechnica.com/?p=1648607', @@ -280,6 +280,32 @@ describe('selected reducer', () => { 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, diff --git a/src/newsreader/js/utils.js b/src/newsreader/js/utils.js index 0794a1a..9db723e 100644 --- a/src/newsreader/js/utils.js +++ b/src/newsreader/js/utils.js @@ -12,3 +12,13 @@ export const formatDatetime = 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); +}; From 36076dbc40094d287126db07956c7dd1a69b79c5 Mon Sep 17 00:00:00 2001 From: Sonny Date: Sun, 2 Feb 2020 15:43:37 +0100 Subject: [PATCH 042/364] Some style changes --- .../accounts/templates/accounts/login.html | 2 +- .../js/pages/homepage/components/PostModal.js | 12 +++++------ .../homepage/components/feedlist/PostItem.js | 16 ++++++++------- .../scss/components/card/_card.scss | 2 +- .../scss/components/form/_form.scss | 3 ++- .../scss/pages/homepage/components/index.scss | 1 + .../components/post-block/_post-block.scss | 1 - .../pages/homepage/components/post/_post.scss | 20 +++++++++++++------ .../posts-header/_posts-header.scss | 12 ++--------- .../components/posts-info/_posts-info.scss | 14 +++++++++++++ .../homepage/components/posts-info/index.scss | 1 + .../posts-section/_post-section.scss | 19 ++++++++++++++++++ .../components/posts-section/index.scss | 13 +----------- .../homepage/components/posts/_posts.scss | 3 ++- 14 files changed, 73 insertions(+), 46 deletions(-) create mode 100644 src/newsreader/scss/pages/homepage/components/posts-info/_posts-info.scss create mode 100644 src/newsreader/scss/pages/homepage/components/posts-info/index.scss create mode 100644 src/newsreader/scss/pages/homepage/components/posts-section/_post-section.scss diff --git a/src/newsreader/accounts/templates/accounts/login.html b/src/newsreader/accounts/templates/accounts/login.html index f98a216..00b248e 100644 --- a/src/newsreader/accounts/templates/accounts/login.html +++ b/src/newsreader/accounts/templates/accounts/login.html @@ -8,7 +8,7 @@ {% block content %}
    -
    {% endblock %} diff --git a/src/newsreader/news/core/templates/core/homepage.html b/src/newsreader/news/core/templates/core/homepage.html index b513d7f..bddb5a3 100644 --- a/src/newsreader/news/core/templates/core/homepage.html +++ b/src/newsreader/news/core/templates/core/homepage.html @@ -2,14 +2,10 @@ {% load static %} -{% block head %} - -{% endblock %} - {% block content %} -
    +
    {% endblock %} {% block scripts %} - + {% endblock %} diff --git a/src/newsreader/news/core/tests/endpoints/category/list/tests.py b/src/newsreader/news/core/tests/endpoints/category/list/tests.py index f97884b..f58832e 100644 --- a/src/newsreader/news/core/tests/endpoints/category/list/tests.py +++ b/src/newsreader/news/core/tests/endpoints/category/list/tests.py @@ -488,11 +488,11 @@ class NestedCategoryPostView(TestCase): self.assertEquals(posts[0]["title"], "Second BBC post") self.assertEquals(posts[1]["title"], "First BBC post") - self.assertEquals(posts[2]["title"], "Second Reuters post") - self.assertEquals(posts[3]["title"], "First Reuters post") + self.assertEquals(posts[2]["title"], "Second Guardian post") + self.assertEquals(posts[3]["title"], "First Guardian post") - self.assertEquals(posts[4]["title"], "Second Guardian post") - self.assertEquals(posts[5]["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) diff --git a/src/newsreader/news/core/tests/test_views.py b/src/newsreader/news/core/tests/test_views.py index 47381d2..ad1dc1d 100644 --- a/src/newsreader/news/core/tests/test_views.py +++ b/src/newsreader/news/core/tests/test_views.py @@ -27,7 +27,11 @@ class CategoryCreateViewTestCase(CategoryViewTestCase, TestCase): def test_creation(self): rules = CollectionRuleFactory.create_batch(size=4, user=self.user) - data = {"name": "new-category", "rules": [rule.pk for rule in rules]} + 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) @@ -55,13 +59,29 @@ class CategoryCreateViewTestCase(CategoryViewTestCase, TestCase): size=3, user=self.user, category=None ) - data = {"name": "new-category", "rules": [rule.pk for rule in other_rules]} + 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): @@ -71,7 +91,7 @@ class CategoryUpdateViewTestCase(CategoryViewTestCase, TestCase): self.url = reverse("category-update", args=[self.category.pk]) def test_name_change(self): - data = {"name": "durp"} + data = {"name": "durp", "user": self.user.pk} self.client.post(self.url, data) self.category.refresh_from_db() @@ -80,7 +100,11 @@ class CategoryUpdateViewTestCase(CategoryViewTestCase, TestCase): 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]} + data = { + "name": self.category.name, + "rules": [rule.pk for rule in rules], + "user": self.user.pk, + } self.client.post(self.url, data) @@ -100,7 +124,11 @@ class CategoryUpdateViewTestCase(CategoryViewTestCase, TestCase): 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]} + data = { + "name": self.category.name, + "rules": [rule.pk for rule in other_rules], + "user": self.user.pk, + } self.client.post(self.url, data) @@ -116,7 +144,7 @@ class CategoryUpdateViewTestCase(CategoryViewTestCase, TestCase): self.assertCountEqual(self.category.rule_ids, [rule.pk for rule in current_rules]) - data = {"name": "durp"} + data = {"name": "durp", "user": self.user.pk} self.client.post(self.url, data) self.category.refresh_from_db() @@ -139,7 +167,7 @@ class CategoryUpdateViewTestCase(CategoryViewTestCase, TestCase): other_category = CategoryFactory(name="other category", user=other_user) other_category.rules.set([*other_rules]) - data = {"name": "durp"} + data = {"name": "durp", "user": other_user.pk} other_url = reverse("category-update", args=[other_category.pk]) response = self.client.post(other_url, data) @@ -161,7 +189,11 @@ class CategoryUpdateViewTestCase(CategoryViewTestCase, TestCase): 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]} + data = { + "name": self.category.name, + "rules": [rule.pk for rule in other_rules], + "user": self.user.pk, + } response = self.client.post(self.url, data) @@ -172,3 +204,16 @@ class CategoryUpdateViewTestCase(CategoryViewTestCase, TestCase): 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/views.py b/src/newsreader/news/core/views.py index a0da62a..4832837 100644 --- a/src/newsreader/news/core/views.py +++ b/src/newsreader/news/core/views.py @@ -53,9 +53,7 @@ class CategoryDetailMixin: return context_data def get_form_kwargs(self): - kwargs = super().get_form_kwargs() - kwargs["user"] = self.request.user - return kwargs + return {**super().get_form_kwargs(), "user": self.request.user} class CategoryListView(CategoryViewMixin, ListView): diff --git a/src/newsreader/scss/pages/rules/components/card/_card.scss b/src/newsreader/scss/components/card/_rule-card.scss similarity index 94% rename from src/newsreader/scss/pages/rules/components/card/_card.scss rename to src/newsreader/scss/components/card/_rule-card.scss index efc2418..5edb25b 100644 --- a/src/newsreader/scss/pages/rules/components/card/_card.scss +++ b/src/newsreader/scss/components/card/_rule-card.scss @@ -1,4 +1,6 @@ .card { + @extend .card; + &__header { & div { display: flex; diff --git a/src/newsreader/scss/components/card/index.scss b/src/newsreader/scss/components/card/index.scss index 484e154..149efa0 100644 --- a/src/newsreader/scss/components/card/index.scss +++ b/src/newsreader/scss/components/card/index.scss @@ -1 +1,2 @@ @import "card"; +@import "rule-card"; diff --git a/src/newsreader/scss/pages/homepage/components/category/_category.scss b/src/newsreader/scss/components/category/_category.scss similarity index 100% rename from src/newsreader/scss/pages/homepage/components/category/_category.scss rename to src/newsreader/scss/components/category/_category.scss diff --git a/src/newsreader/scss/pages/homepage/components/category/index.scss b/src/newsreader/scss/components/category/index.scss similarity index 100% rename from src/newsreader/scss/pages/homepage/components/category/index.scss rename to src/newsreader/scss/components/category/index.scss diff --git a/src/newsreader/scss/components/content/_content.scss b/src/newsreader/scss/components/content/_content.scss deleted file mode 100644 index 1bb58ca..0000000 --- a/src/newsreader/scss/components/content/_content.scss +++ /dev/null @@ -1,5 +0,0 @@ -.content { - display: flex; - flex-direction: column; - align-items: center; -} diff --git a/src/newsreader/scss/components/content/index.scss b/src/newsreader/scss/components/content/index.scss deleted file mode 100644 index b424282..0000000 --- a/src/newsreader/scss/components/content/index.scss +++ /dev/null @@ -1 +0,0 @@ -@import "content"; diff --git a/src/newsreader/scss/components/errorlist/_errorlist.scss b/src/newsreader/scss/components/errorlist/_errorlist.scss index 393899f..006dafb 100644 --- a/src/newsreader/scss/components/errorlist/_errorlist.scss +++ b/src/newsreader/scss/components/errorlist/_errorlist.scss @@ -1,20 +1,23 @@ .errorlist { @extend .list; - padding: 10px; margin: 5px 0; + padding: 0; - background-color: $error-red; color: $white; list-style: disc; list-style-position: inside; - & li { - margin: 15px 0 0 0; + &__item { + margin: 10px 0; + padding: 10px; - &:first-child { - margin: 0; - } + background-color: $error-red; + border-radius: 5px; + } + + & li { + @extend .errorlist__item; } } diff --git a/src/newsreader/scss/pages/register/components/activation-form/_activation-form.scss b/src/newsreader/scss/components/form/_activation-form.scss similarity index 100% rename from src/newsreader/scss/pages/register/components/activation-form/_activation-form.scss rename to src/newsreader/scss/components/form/_activation-form.scss diff --git a/src/newsreader/scss/pages/category/components/category-form/_category-form.scss b/src/newsreader/scss/components/form/_category-form.scss similarity index 91% rename from src/newsreader/scss/pages/category/components/category-form/_category-form.scss rename to src/newsreader/scss/components/form/_category-form.scss index b6ef175..8132ed2 100644 --- a/src/newsreader/scss/pages/category/components/category-form/_category-form.scss +++ b/src/newsreader/scss/components/form/_category-form.scss @@ -1,8 +1,9 @@ .category-form { + @extend .form; + margin: 20px 0; &__section:last-child { - & .category-form__fieldset { display: flex; flex-direction: row; diff --git a/src/newsreader/scss/pages/import/components/import-form/_import-form.scss b/src/newsreader/scss/components/form/_import-form.scss similarity index 100% rename from src/newsreader/scss/pages/import/components/import-form/_import-form.scss rename to src/newsreader/scss/components/form/_import-form.scss diff --git a/src/newsreader/scss/pages/login/components/form/_form.scss b/src/newsreader/scss/components/form/_login-form.scss similarity index 100% rename from src/newsreader/scss/pages/login/components/form/_form.scss rename to src/newsreader/scss/components/form/_login-form.scss diff --git a/src/newsreader/scss/pages/password-reset/components/password-reset-confirm-form/_password-reset-confirm-form.scss b/src/newsreader/scss/components/form/_password-reset-confirm-form.scss similarity index 100% rename from src/newsreader/scss/pages/password-reset/components/password-reset-confirm-form/_password-reset-confirm-form.scss rename to src/newsreader/scss/components/form/_password-reset-confirm-form.scss diff --git a/src/newsreader/scss/pages/password-reset/components/password-reset-form/_password-reset-form.scss b/src/newsreader/scss/components/form/_password-reset-form.scss similarity index 100% rename from src/newsreader/scss/pages/password-reset/components/password-reset-form/_password-reset-form.scss rename to src/newsreader/scss/components/form/_password-reset-form.scss diff --git a/src/newsreader/scss/pages/register/components/register-form/_register-form.scss b/src/newsreader/scss/components/form/_register-form.scss similarity index 100% rename from src/newsreader/scss/pages/register/components/register-form/_register-form.scss rename to src/newsreader/scss/components/form/_register-form.scss diff --git a/src/newsreader/scss/pages/rule/components/rule-form/_rule-form.scss b/src/newsreader/scss/components/form/_rule-form.scss similarity index 83% rename from src/newsreader/scss/pages/rule/components/rule-form/_rule-form.scss rename to src/newsreader/scss/components/form/_rule-form.scss index 95a2388..82651aa 100644 --- a/src/newsreader/scss/pages/rule/components/rule-form/_rule-form.scss +++ b/src/newsreader/scss/components/form/_rule-form.scss @@ -2,7 +2,6 @@ margin: 20px 0; &__section:last-child { - & .rule-form__fieldset { display: flex; flex-direction: row; @@ -10,18 +9,17 @@ } } - &__select[name=category] { + #id_category { width: 50%; padding: 0 10px; } - &__select[name=timezone] { + #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 index dc477a7..2c70cdd 100644 --- a/src/newsreader/scss/components/form/index.scss +++ b/src/newsreader/scss/components/form/index.scss @@ -1 +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 index 6a18d4a..4bddb31 100644 --- a/src/newsreader/scss/components/index.scss +++ b/src/newsreader/scss/components/index.scss @@ -1,13 +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 "./content/index"; -@import "./messages/index"; -@import "./section/index"; -@import "./errorlist/index"; -@import "./fieldset/index"; +@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/main/_main.scss b/src/newsreader/scss/components/main/_main.scss index 1c4ed2e..5d0143f 100644 --- a/src/newsreader/scss/components/main/_main.scss +++ b/src/newsreader/scss/components/main/_main.scss @@ -1,4 +1,7 @@ .main { - margin: 1% 10% 5% 10%; - background-color: $white; + display: flex; + flex-direction: column; + align-items: center; + + margin: 20px 0; } diff --git a/src/newsreader/scss/components/messages/_messages.scss b/src/newsreader/scss/components/messages/_messages.scss index 11b53b8..56af888 100644 --- a/src/newsreader/scss/components/messages/_messages.scss +++ b/src/newsreader/scss/components/messages/_messages.scss @@ -1,16 +1,32 @@ .messages { display: flex; flex-direction: column; - - width: 100%; + align-items: center; margin: 5px 0 20px 0; - padding: 15px 0; color: $white; - background-color: $error-red; &__item { - padding: 0 30px; + width: 80%; + + 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; + } } } diff --git a/src/newsreader/scss/components/modal/_modal.scss b/src/newsreader/scss/components/modal/_modal.scss index 16e8d1d..93fe54f 100644 --- a/src/newsreader/scss/components/modal/_modal.scss +++ b/src/newsreader/scss/components/modal/_modal.scss @@ -1,12 +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 index bcb7d8e..d84836a 100644 --- a/src/newsreader/scss/components/modal/index.scss +++ b/src/newsreader/scss/components/modal/index.scss @@ -1 +1,3 @@ @import "modal"; + +@import "post-modal"; diff --git a/src/newsreader/scss/pages/homepage/components/post-block/_post-block.scss b/src/newsreader/scss/components/post-block/_post-block.scss similarity index 86% rename from src/newsreader/scss/pages/homepage/components/post-block/_post-block.scss rename to src/newsreader/scss/components/post-block/_post-block.scss index e694de0..c65352b 100644 --- a/src/newsreader/scss/pages/homepage/components/post-block/_post-block.scss +++ b/src/newsreader/scss/components/post-block/_post-block.scss @@ -1,6 +1,7 @@ .post-block { display: flex; flex-direction: column; + align-items: center; width: 70%; margin: 0 0 2% 0; diff --git a/src/newsreader/scss/pages/homepage/components/post-block/index.scss b/src/newsreader/scss/components/post-block/index.scss similarity index 100% rename from src/newsreader/scss/pages/homepage/components/post-block/index.scss rename to src/newsreader/scss/components/post-block/index.scss diff --git a/src/newsreader/scss/pages/homepage/components/post-message/_post-message.scss b/src/newsreader/scss/components/post-message/_post-message.scss similarity index 100% rename from src/newsreader/scss/pages/homepage/components/post-message/_post-message.scss rename to src/newsreader/scss/components/post-message/_post-message.scss diff --git a/src/newsreader/scss/pages/homepage/components/post-message/index.scss b/src/newsreader/scss/components/post-message/index.scss similarity index 100% rename from src/newsreader/scss/pages/homepage/components/post-message/index.scss rename to src/newsreader/scss/components/post-message/index.scss diff --git a/src/newsreader/scss/pages/homepage/components/post/_post.scss b/src/newsreader/scss/components/post/_post.scss similarity index 95% rename from src/newsreader/scss/pages/homepage/components/post/_post.scss rename to src/newsreader/scss/components/post/_post.scss index 35c557e..bcf92a6 100644 --- a/src/newsreader/scss/pages/homepage/components/post/_post.scss +++ b/src/newsreader/scss/components/post/_post.scss @@ -15,6 +15,8 @@ background-color: $white; + cursor: initial; + &__header { display: flex; flex-direction: column; @@ -58,7 +60,11 @@ font-size: 18px; & p { - padding: 20px 0 0 0; + padding: 10px 0; + } + + & h1, h2, h3 { + margin: 20px 0 5px 0; } & img { diff --git a/src/newsreader/scss/pages/homepage/components/post/index.scss b/src/newsreader/scss/components/post/index.scss similarity index 100% rename from src/newsreader/scss/pages/homepage/components/post/index.scss rename to src/newsreader/scss/components/post/index.scss diff --git a/src/newsreader/scss/pages/homepage/components/posts-header/_posts-header.scss b/src/newsreader/scss/components/posts-header/_posts-header.scss similarity index 100% rename from src/newsreader/scss/pages/homepage/components/posts-header/_posts-header.scss rename to src/newsreader/scss/components/posts-header/_posts-header.scss diff --git a/src/newsreader/scss/pages/homepage/components/posts-header/index.scss b/src/newsreader/scss/components/posts-header/index.scss similarity index 100% rename from src/newsreader/scss/pages/homepage/components/posts-header/index.scss rename to src/newsreader/scss/components/posts-header/index.scss diff --git a/src/newsreader/scss/pages/homepage/components/posts-info/_posts-info.scss b/src/newsreader/scss/components/posts-info/_posts-info.scss similarity index 100% rename from src/newsreader/scss/pages/homepage/components/posts-info/_posts-info.scss rename to src/newsreader/scss/components/posts-info/_posts-info.scss diff --git a/src/newsreader/scss/pages/homepage/components/posts-info/index.scss b/src/newsreader/scss/components/posts-info/index.scss similarity index 100% rename from src/newsreader/scss/pages/homepage/components/posts-info/index.scss rename to src/newsreader/scss/components/posts-info/index.scss diff --git a/src/newsreader/scss/pages/homepage/components/posts-section/_post-section.scss b/src/newsreader/scss/components/posts-section/_post-section.scss similarity index 95% rename from src/newsreader/scss/pages/homepage/components/posts-section/_post-section.scss rename to src/newsreader/scss/components/posts-section/_post-section.scss index 902e1a2..1c40bec 100644 --- a/src/newsreader/scss/pages/homepage/components/posts-section/_post-section.scss +++ b/src/newsreader/scss/components/posts-section/_post-section.scss @@ -1,6 +1,7 @@ .posts-section { display: flex; flex-direction: column; + width: 95%; margin: 20px; padding: 10px; diff --git a/src/newsreader/scss/pages/homepage/components/posts-section/index.scss b/src/newsreader/scss/components/posts-section/index.scss similarity index 100% rename from src/newsreader/scss/pages/homepage/components/posts-section/index.scss rename to src/newsreader/scss/components/posts-section/index.scss diff --git a/src/newsreader/scss/pages/homepage/components/posts/_posts.scss b/src/newsreader/scss/components/posts/_posts.scss similarity index 100% rename from src/newsreader/scss/pages/homepage/components/posts/_posts.scss rename to src/newsreader/scss/components/posts/_posts.scss diff --git a/src/newsreader/scss/pages/homepage/components/posts/index.scss b/src/newsreader/scss/components/posts/index.scss similarity index 100% rename from src/newsreader/scss/pages/homepage/components/posts/index.scss rename to src/newsreader/scss/components/posts/index.scss diff --git a/src/newsreader/scss/pages/homepage/components/rules/_rules.scss b/src/newsreader/scss/components/rules/_rules.scss similarity index 65% rename from src/newsreader/scss/pages/homepage/components/rules/_rules.scss rename to src/newsreader/scss/components/rules/_rules.scss index ca12c9c..a1e9f67 100644 --- a/src/newsreader/scss/pages/homepage/components/rules/_rules.scss +++ b/src/newsreader/scss/components/rules/_rules.scss @@ -1,4 +1,6 @@ .rules { + padding: 0; + &__item { display: flex; justify-content: space-between; @@ -25,4 +27,20 @@ background-color: darken($azureish-white, +10%); } } + + &__info { + display: flex; + align-items: center; + width: 80%; + } + + &__title { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + & .badge { + display: flex; + } } diff --git a/src/newsreader/scss/pages/homepage/components/rules/index.scss b/src/newsreader/scss/components/rules/index.scss similarity index 100% rename from src/newsreader/scss/pages/homepage/components/rules/index.scss rename to src/newsreader/scss/components/rules/index.scss 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/pages/homepage/components/sidebar/index.scss b/src/newsreader/scss/components/sidebar/index.scss similarity index 100% rename from src/newsreader/scss/pages/homepage/components/sidebar/index.scss rename to src/newsreader/scss/components/sidebar/index.scss diff --git a/src/newsreader/scss/pages/homepage/elements/badge/_badge.scss b/src/newsreader/scss/elements/badge/_badge.scss similarity index 100% rename from src/newsreader/scss/pages/homepage/elements/badge/_badge.scss rename to src/newsreader/scss/elements/badge/_badge.scss diff --git a/src/newsreader/scss/pages/homepage/elements/badge/index.scss b/src/newsreader/scss/elements/badge/index.scss similarity index 100% rename from src/newsreader/scss/pages/homepage/elements/badge/index.scss rename to src/newsreader/scss/elements/badge/index.scss diff --git a/src/newsreader/scss/pages/homepage/components/read-button/_read-button.scss b/src/newsreader/scss/elements/button/_read-button.scss similarity index 88% rename from src/newsreader/scss/pages/homepage/components/read-button/_read-button.scss rename to src/newsreader/scss/elements/button/_read-button.scss index a8eab4c..940d895 100644 --- a/src/newsreader/scss/pages/homepage/components/read-button/_read-button.scss +++ b/src/newsreader/scss/elements/button/_read-button.scss @@ -1,4 +1,6 @@ .read-button { + @extend .button; + margin: 20px 0 0 0; color: $white; diff --git a/src/newsreader/scss/elements/button/index.scss b/src/newsreader/scss/elements/button/index.scss index ac3b5de..a9b2ec7 100644 --- a/src/newsreader/scss/elements/button/index.scss +++ b/src/newsreader/scss/elements/button/index.scss @@ -1 +1,2 @@ @import "button"; +@import "_read-button"; diff --git a/src/newsreader/scss/elements/index.scss b/src/newsreader/scss/elements/index.scss index 46a8bbd..f0d7be3 100644 --- a/src/newsreader/scss/elements/index.scss +++ b/src/newsreader/scss/elements/index.scss @@ -7,3 +7,4 @@ @import "input/index"; @import "label/index"; @import "help-text/index"; +@import "badge/index"; diff --git a/src/newsreader/scss/index.scss b/src/newsreader/scss/index.scss new file mode 100644 index 0000000..1fa6f17 --- /dev/null +++ b/src/newsreader/scss/index.scss @@ -0,0 +1,5 @@ +@import "partials/index"; +@import "components/index"; +@import "elements/index"; + +@import "pages/index"; diff --git a/src/newsreader/scss/pages/activate/components/index.scss b/src/newsreader/scss/pages/activate/components/index.scss deleted file mode 100644 index e69de29..0000000 diff --git a/src/newsreader/scss/pages/activate/elements/index.scss b/src/newsreader/scss/pages/activate/elements/index.scss deleted file mode 100644 index e69de29..0000000 diff --git a/src/newsreader/scss/pages/activate/index.scss b/src/newsreader/scss/pages/activate/index.scss deleted file mode 100644 index 16b6493..0000000 --- a/src/newsreader/scss/pages/activate/index.scss +++ /dev/null @@ -1,8 +0,0 @@ -// General imports -@import "../../partials/variables"; -@import "../../components/index"; -@import "../../elements/index"; - -// Page specific -@import "./components/index"; -@import "./elements/index"; diff --git a/src/newsreader/scss/pages/categories/components/card/_card.scss b/src/newsreader/scss/pages/categories/components/card/_card.scss deleted file mode 100644 index 79d3c89..0000000 --- a/src/newsreader/scss/pages/categories/components/card/_card.scss +++ /dev/null @@ -1,5 +0,0 @@ -.card { - &__footer > *:last-child { - margin: 0 0 0 10px; - } -} diff --git a/src/newsreader/scss/pages/categories/components/card/index.scss b/src/newsreader/scss/pages/categories/components/card/index.scss deleted file mode 100644 index 484e154..0000000 --- a/src/newsreader/scss/pages/categories/components/card/index.scss +++ /dev/null @@ -1 +0,0 @@ -@import "card"; diff --git a/src/newsreader/scss/pages/categories/components/category-modal/_category-modal.scss b/src/newsreader/scss/pages/categories/components/category-modal/_category-modal.scss deleted file mode 100644 index 289c09f..0000000 --- a/src/newsreader/scss/pages/categories/components/category-modal/_category-modal.scss +++ /dev/null @@ -1,27 +0,0 @@ -.category-modal { - display: flex; - flex-direction: column; - align-self: center; - - margin: 20px 0; - width: 50%; - - border-radius: 2px; - 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/pages/categories/components/category-modal/index.scss b/src/newsreader/scss/pages/categories/components/category-modal/index.scss deleted file mode 100644 index 0126261..0000000 --- a/src/newsreader/scss/pages/categories/components/category-modal/index.scss +++ /dev/null @@ -1 +0,0 @@ -@import "category-modal"; diff --git a/src/newsreader/scss/pages/categories/components/index.scss b/src/newsreader/scss/pages/categories/components/index.scss deleted file mode 100644 index 2890f60..0000000 --- a/src/newsreader/scss/pages/categories/components/index.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import "card/index"; -@import "category-modal/index"; diff --git a/src/newsreader/scss/pages/categories/elements/index.scss b/src/newsreader/scss/pages/categories/elements/index.scss deleted file mode 100644 index e69de29..0000000 diff --git a/src/newsreader/scss/pages/categories/index.scss b/src/newsreader/scss/pages/categories/index.scss index 16b6493..b683e0e 100644 --- a/src/newsreader/scss/pages/categories/index.scss +++ b/src/newsreader/scss/pages/categories/index.scss @@ -1,8 +1,7 @@ -// General imports -@import "../../partials/variables"; -@import "../../components/index"; -@import "../../elements/index"; - -// Page specific -@import "./components/index"; -@import "./elements/index"; +#categories--page { + & .card { + &__footer > *:last-child { + margin: 0 0 0 10px; + } + } +} diff --git a/src/newsreader/scss/pages/category/components/category-form/index.scss b/src/newsreader/scss/pages/category/components/category-form/index.scss deleted file mode 100644 index 715c81e..0000000 --- a/src/newsreader/scss/pages/category/components/category-form/index.scss +++ /dev/null @@ -1 +0,0 @@ -@import "category-form"; diff --git a/src/newsreader/scss/pages/category/components/index.scss b/src/newsreader/scss/pages/category/components/index.scss deleted file mode 100644 index aa4af19..0000000 --- a/src/newsreader/scss/pages/category/components/index.scss +++ /dev/null @@ -1 +0,0 @@ -@import "category-form/index"; diff --git a/src/newsreader/scss/pages/category/elements/index.scss b/src/newsreader/scss/pages/category/elements/index.scss deleted file mode 100644 index e69de29..0000000 diff --git a/src/newsreader/scss/pages/category/index.scss b/src/newsreader/scss/pages/category/index.scss index 16b6493..ae62be1 100644 --- a/src/newsreader/scss/pages/category/index.scss +++ b/src/newsreader/scss/pages/category/index.scss @@ -1,8 +1,2 @@ -// General imports -@import "../../partials/variables"; -@import "../../components/index"; -@import "../../elements/index"; - -// Page specific -@import "./components/index"; -@import "./elements/index"; +#category--page { +} diff --git a/src/newsreader/scss/pages/homepage/components/categories/_categories.scss b/src/newsreader/scss/pages/homepage/components/categories/_categories.scss deleted file mode 100644 index 002a66a..0000000 --- a/src/newsreader/scss/pages/homepage/components/categories/_categories.scss +++ /dev/null @@ -1,24 +0,0 @@ -.categories { - display: flex; - flex-direction: column; - align-items: center; - - width: 90%; - - font-family: $sidebar-font; - border-radius: 2px; - - & ul { - margin: 0; - padding: 0; - - width: 100%; - - list-style: none; - border-radius: 5px; - } - - &__item { - padding: 2px 10px 5px 10px; - } -} diff --git a/src/newsreader/scss/pages/homepage/components/categories/index.scss b/src/newsreader/scss/pages/homepage/components/categories/index.scss deleted file mode 100644 index 0eebf91..0000000 --- a/src/newsreader/scss/pages/homepage/components/categories/index.scss +++ /dev/null @@ -1 +0,0 @@ -@import "categories"; diff --git a/src/newsreader/scss/pages/homepage/components/content/_content.scss b/src/newsreader/scss/pages/homepage/components/content/_content.scss deleted file mode 100644 index 9b9efb9..0000000 --- a/src/newsreader/scss/pages/homepage/components/content/_content.scss +++ /dev/null @@ -1,7 +0,0 @@ -.content { - display: flex; - flex-direction: column; - align-items: center; - - margin: 2% 0 0 0; -} diff --git a/src/newsreader/scss/pages/homepage/components/content/index.scss b/src/newsreader/scss/pages/homepage/components/content/index.scss deleted file mode 100644 index b424282..0000000 --- a/src/newsreader/scss/pages/homepage/components/content/index.scss +++ /dev/null @@ -1 +0,0 @@ -@import "content"; diff --git a/src/newsreader/scss/pages/homepage/components/index.scss b/src/newsreader/scss/pages/homepage/components/index.scss deleted file mode 100644 index 2a0a85e..0000000 --- a/src/newsreader/scss/pages/homepage/components/index.scss +++ /dev/null @@ -1,18 +0,0 @@ -@import "content/index"; -@import "main/index"; - -@import "sidebar/index"; -@import "categories/index"; -@import "category/index"; - -@import "rules/index"; -@import "rule/index"; - -@import "post-block/index"; -@import "posts-section/index"; -@import "posts/index"; -@import "posts-header/index"; -@import "posts-info/index"; -@import "post/index"; -@import "post-message/index"; -@import "read-button/index"; diff --git a/src/newsreader/scss/pages/homepage/components/main/_main.scss b/src/newsreader/scss/pages/homepage/components/main/_main.scss deleted file mode 100644 index 42cb2d5..0000000 --- a/src/newsreader/scss/pages/homepage/components/main/_main.scss +++ /dev/null @@ -1,12 +0,0 @@ -.main { - display: flex; - flex-direction: row; - width: 100%; - - margin: 0; - background-color: initial; - - &--centered { - justify-content: center; - } -} diff --git a/src/newsreader/scss/pages/homepage/components/main/index.scss b/src/newsreader/scss/pages/homepage/components/main/index.scss deleted file mode 100644 index bdb4ce0..0000000 --- a/src/newsreader/scss/pages/homepage/components/main/index.scss +++ /dev/null @@ -1 +0,0 @@ -@import "main"; diff --git a/src/newsreader/scss/pages/homepage/components/read-button/index.scss b/src/newsreader/scss/pages/homepage/components/read-button/index.scss deleted file mode 100644 index 8e49454..0000000 --- a/src/newsreader/scss/pages/homepage/components/read-button/index.scss +++ /dev/null @@ -1 +0,0 @@ -@import 'read-button'; diff --git a/src/newsreader/scss/pages/homepage/components/rule/_rule.scss b/src/newsreader/scss/pages/homepage/components/rule/_rule.scss deleted file mode 100644 index ba34bf6..0000000 --- a/src/newsreader/scss/pages/homepage/components/rule/_rule.scss +++ /dev/null @@ -1,15 +0,0 @@ -.rule { - display: flex; - align-items: center; - width: 80%; - - &__title { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - - & span { - display: flex; - } -} diff --git a/src/newsreader/scss/pages/homepage/components/rule/index.scss b/src/newsreader/scss/pages/homepage/components/rule/index.scss deleted file mode 100644 index 7ec839a..0000000 --- a/src/newsreader/scss/pages/homepage/components/rule/index.scss +++ /dev/null @@ -1 +0,0 @@ -@import "rule"; diff --git a/src/newsreader/scss/pages/homepage/components/sidebar/_sidebar.scss b/src/newsreader/scss/pages/homepage/components/sidebar/_sidebar.scss deleted file mode 100644 index 5c6575b..0000000 --- a/src/newsreader/scss/pages/homepage/components/sidebar/_sidebar.scss +++ /dev/null @@ -1,11 +0,0 @@ -.sidebar { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - align-self: flex-start; - - position: sticky; - top: 5%; - width: 20%; -} diff --git a/src/newsreader/scss/pages/homepage/elements/index.scss b/src/newsreader/scss/pages/homepage/elements/index.scss deleted file mode 100644 index 66bce62..0000000 --- a/src/newsreader/scss/pages/homepage/elements/index.scss +++ /dev/null @@ -1 +0,0 @@ -@import "badge/index"; diff --git a/src/newsreader/scss/pages/homepage/index.scss b/src/newsreader/scss/pages/homepage/index.scss index 16b6493..30f5a50 100644 --- a/src/newsreader/scss/pages/homepage/index.scss +++ b/src/newsreader/scss/pages/homepage/index.scss @@ -1,8 +1,9 @@ -// General imports -@import "../../partials/variables"; -@import "../../components/index"; -@import "../../elements/index"; +#homepage--page { + display: flex; + flex-direction: row; + align-items: initial; + width: 100%; -// Page specific -@import "./components/index"; -@import "./elements/index"; + margin: 20px 0 0 0; + background-color: initial; +} diff --git a/src/newsreader/scss/pages/import/components/import-form/index.scss b/src/newsreader/scss/pages/import/components/import-form/index.scss deleted file mode 100644 index b6407f2..0000000 --- a/src/newsreader/scss/pages/import/components/import-form/index.scss +++ /dev/null @@ -1 +0,0 @@ -@import "import-form"; diff --git a/src/newsreader/scss/pages/import/components/index.scss b/src/newsreader/scss/pages/import/components/index.scss deleted file mode 100644 index 0bccecf..0000000 --- a/src/newsreader/scss/pages/import/components/index.scss +++ /dev/null @@ -1 +0,0 @@ -@import "import-form/index"; diff --git a/src/newsreader/scss/pages/import/elements/index.scss b/src/newsreader/scss/pages/import/elements/index.scss deleted file mode 100644 index e69de29..0000000 diff --git a/src/newsreader/scss/pages/import/index.scss b/src/newsreader/scss/pages/import/index.scss index 16b6493..8b13789 100644 --- a/src/newsreader/scss/pages/import/index.scss +++ b/src/newsreader/scss/pages/import/index.scss @@ -1,8 +1 @@ -// General imports -@import "../../partials/variables"; -@import "../../components/index"; -@import "../../elements/index"; -// Page specific -@import "./components/index"; -@import "./elements/index"; 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/components/form/index.scss b/src/newsreader/scss/pages/login/components/form/index.scss deleted file mode 100644 index dc477a7..0000000 --- a/src/newsreader/scss/pages/login/components/form/index.scss +++ /dev/null @@ -1 +0,0 @@ -@import "form"; diff --git a/src/newsreader/scss/pages/login/components/index.scss b/src/newsreader/scss/pages/login/components/index.scss deleted file mode 100644 index 246a1a1..0000000 --- a/src/newsreader/scss/pages/login/components/index.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import "./main/index"; -@import "./form/index"; diff --git a/src/newsreader/scss/pages/login/components/main/_main.scss b/src/newsreader/scss/pages/login/components/main/_main.scss deleted file mode 100644 index 1efb986..0000000 --- a/src/newsreader/scss/pages/login/components/main/_main.scss +++ /dev/null @@ -1,8 +0,0 @@ -.main { - @extend .main; - - margin: 5% auto; - width: 50%; - - border-radius: 4px; -} diff --git a/src/newsreader/scss/pages/login/components/main/index.scss b/src/newsreader/scss/pages/login/components/main/index.scss deleted file mode 100644 index bdb4ce0..0000000 --- a/src/newsreader/scss/pages/login/components/main/index.scss +++ /dev/null @@ -1 +0,0 @@ -@import "main"; diff --git a/src/newsreader/scss/pages/login/index.scss b/src/newsreader/scss/pages/login/index.scss index 859e33f..69b946e 100644 --- a/src/newsreader/scss/pages/login/index.scss +++ b/src/newsreader/scss/pages/login/index.scss @@ -1,7 +1,6 @@ -// General imports -@import "../../partials/variables"; -@import "../../components/index"; -@import "../../elements/index"; +#login--page { + margin: 5% auto; + width: 50%; -// Page specific -@import "./components/index"; + border-radius: 4px; +} diff --git a/src/newsreader/scss/pages/password-reset/components/index.scss b/src/newsreader/scss/pages/password-reset/components/index.scss deleted file mode 100644 index 4536bb6..0000000 --- a/src/newsreader/scss/pages/password-reset/components/index.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import "password-reset-form/index"; -@import "password-reset-confirm-form/index"; diff --git a/src/newsreader/scss/pages/password-reset/components/password-reset-confirm-form/index.scss b/src/newsreader/scss/pages/password-reset/components/password-reset-confirm-form/index.scss deleted file mode 100644 index 2448efe..0000000 --- a/src/newsreader/scss/pages/password-reset/components/password-reset-confirm-form/index.scss +++ /dev/null @@ -1 +0,0 @@ -@import "password-reset-confirm-form"; diff --git a/src/newsreader/scss/pages/password-reset/components/password-reset-form/index.scss b/src/newsreader/scss/pages/password-reset/components/password-reset-form/index.scss deleted file mode 100644 index 1d60faf..0000000 --- a/src/newsreader/scss/pages/password-reset/components/password-reset-form/index.scss +++ /dev/null @@ -1 +0,0 @@ -@import "password-reset-form"; diff --git a/src/newsreader/scss/pages/password-reset/elements/index.scss b/src/newsreader/scss/pages/password-reset/elements/index.scss deleted file mode 100644 index e69de29..0000000 diff --git a/src/newsreader/scss/pages/password-reset/index.scss b/src/newsreader/scss/pages/password-reset/index.scss index 16b6493..8b13789 100644 --- a/src/newsreader/scss/pages/password-reset/index.scss +++ b/src/newsreader/scss/pages/password-reset/index.scss @@ -1,8 +1 @@ -// General imports -@import "../../partials/variables"; -@import "../../components/index"; -@import "../../elements/index"; -// Page specific -@import "./components/index"; -@import "./elements/index"; diff --git a/src/newsreader/scss/pages/register/components/activation-form/index.scss b/src/newsreader/scss/pages/register/components/activation-form/index.scss deleted file mode 100644 index 748302f..0000000 --- a/src/newsreader/scss/pages/register/components/activation-form/index.scss +++ /dev/null @@ -1 +0,0 @@ -@import "activation-form"; diff --git a/src/newsreader/scss/pages/register/components/index.scss b/src/newsreader/scss/pages/register/components/index.scss deleted file mode 100644 index 3377ff1..0000000 --- a/src/newsreader/scss/pages/register/components/index.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import "register-form/index"; -@import "activation-form/index"; diff --git a/src/newsreader/scss/pages/register/components/register-form/index.scss b/src/newsreader/scss/pages/register/components/register-form/index.scss deleted file mode 100644 index f0d9b70..0000000 --- a/src/newsreader/scss/pages/register/components/register-form/index.scss +++ /dev/null @@ -1 +0,0 @@ -@import "register-form"; diff --git a/src/newsreader/scss/pages/register/elements/index.scss b/src/newsreader/scss/pages/register/elements/index.scss deleted file mode 100644 index e69de29..0000000 diff --git a/src/newsreader/scss/pages/register/index.scss b/src/newsreader/scss/pages/register/index.scss index 16b6493..8b13789 100644 --- a/src/newsreader/scss/pages/register/index.scss +++ b/src/newsreader/scss/pages/register/index.scss @@ -1,8 +1 @@ -// General imports -@import "../../partials/variables"; -@import "../../components/index"; -@import "../../elements/index"; -// Page specific -@import "./components/index"; -@import "./elements/index"; diff --git a/src/newsreader/scss/pages/rule/components/index.scss b/src/newsreader/scss/pages/rule/components/index.scss deleted file mode 100644 index de2a031..0000000 --- a/src/newsreader/scss/pages/rule/components/index.scss +++ /dev/null @@ -1 +0,0 @@ -@import "rule-form/index"; diff --git a/src/newsreader/scss/pages/rule/components/rule-form/index.scss b/src/newsreader/scss/pages/rule/components/rule-form/index.scss deleted file mode 100644 index 4c7fbee..0000000 --- a/src/newsreader/scss/pages/rule/components/rule-form/index.scss +++ /dev/null @@ -1 +0,0 @@ -@import "rule-form"; diff --git a/src/newsreader/scss/pages/rule/elements/index.scss b/src/newsreader/scss/pages/rule/elements/index.scss deleted file mode 100644 index e69de29..0000000 diff --git a/src/newsreader/scss/pages/rule/index.scss b/src/newsreader/scss/pages/rule/index.scss index 16b6493..8b13789 100644 --- a/src/newsreader/scss/pages/rule/index.scss +++ b/src/newsreader/scss/pages/rule/index.scss @@ -1,8 +1 @@ -// General imports -@import "../../partials/variables"; -@import "../../components/index"; -@import "../../elements/index"; -// Page specific -@import "./components/index"; -@import "./elements/index"; diff --git a/src/newsreader/scss/pages/rules/components/card/index.scss b/src/newsreader/scss/pages/rules/components/card/index.scss deleted file mode 100644 index 484e154..0000000 --- a/src/newsreader/scss/pages/rules/components/card/index.scss +++ /dev/null @@ -1 +0,0 @@ -@import "card"; diff --git a/src/newsreader/scss/pages/rules/components/index.scss b/src/newsreader/scss/pages/rules/components/index.scss deleted file mode 100644 index 7e96fec..0000000 --- a/src/newsreader/scss/pages/rules/components/index.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import "card/index"; -@import "rules/index"; -@import "rule-modal/index"; diff --git a/src/newsreader/scss/pages/rules/components/rule-modal/_rule-modal.scss b/src/newsreader/scss/pages/rules/components/rule-modal/_rule-modal.scss deleted file mode 100644 index 0dc53bb..0000000 --- a/src/newsreader/scss/pages/rules/components/rule-modal/_rule-modal.scss +++ /dev/null @@ -1,27 +0,0 @@ -.rule-modal { - display: flex; - flex-direction: column; - align-self: center; - - margin: 20px 0; - width: 50%; - - border-radius: 2px; - 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/pages/rules/components/rule-modal/index.scss b/src/newsreader/scss/pages/rules/components/rule-modal/index.scss deleted file mode 100644 index c19060f..0000000 --- a/src/newsreader/scss/pages/rules/components/rule-modal/index.scss +++ /dev/null @@ -1 +0,0 @@ -@import "rule-modal"; diff --git a/src/newsreader/scss/pages/rules/components/rules/_rules.scss b/src/newsreader/scss/pages/rules/components/rules/_rules.scss deleted file mode 100644 index 3fd4c22..0000000 --- a/src/newsreader/scss/pages/rules/components/rules/_rules.scss +++ /dev/null @@ -1,7 +0,0 @@ -.rules { - &__item { - & > * { - margin: 0; - } - } -} diff --git a/src/newsreader/scss/pages/rules/components/rules/index.scss b/src/newsreader/scss/pages/rules/components/rules/index.scss deleted file mode 100644 index e6a0ebf..0000000 --- a/src/newsreader/scss/pages/rules/components/rules/index.scss +++ /dev/null @@ -1 +0,0 @@ -@import "rules"; diff --git a/src/newsreader/scss/pages/rules/elements/index.scss b/src/newsreader/scss/pages/rules/elements/index.scss deleted file mode 100644 index e69de29..0000000 diff --git a/src/newsreader/scss/pages/rules/index.scss b/src/newsreader/scss/pages/rules/index.scss index 16b6493..68b92cb 100644 --- a/src/newsreader/scss/pages/rules/index.scss +++ b/src/newsreader/scss/pages/rules/index.scss @@ -1,8 +1,7 @@ -// General imports -@import "../../partials/variables"; -@import "../../components/index"; -@import "../../elements/index"; - -// Page specific -@import "./components/index"; -@import "./elements/index"; +#rules--page { + .list__item { + & .link { + margin: 0; + } + } +} diff --git a/src/newsreader/scss/partials/_variables.scss b/src/newsreader/scss/partials/index.scss similarity index 100% rename from src/newsreader/scss/partials/_variables.scss rename to src/newsreader/scss/partials/index.scss diff --git a/src/newsreader/templates/base.html b/src/newsreader/templates/base.html index 087aca7..746ef54 100644 --- a/src/newsreader/templates/base.html +++ b/src/newsreader/templates/base.html @@ -1,8 +1,12 @@ +{% load static %} + Newreader - {% block head %}{% endblock %} + {% block head %} + + {% endblock %} @@ -24,7 +28,7 @@ {% if messages %}