diff --git a/docker-compose.yml b/docker-compose.yml index 7a39a3f..e168162 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,52 +1,56 @@ version: '3' +volumes: + postgres-data: + static-files: + node-modules: services: db: - # See https://hub.docker.com/_/postgres image: postgres - container_name: postgres environment: - - POSTGRES_DB=$POSTGRES_NAME - - POSTGRES_USER=$POSTGRES_USER - - POSTGRES_PASSWORD=$POSTGRES_PASSWORD + POSTGRES_DB: "newsreader" + POSTGRES_USER: "newsreader" + POSTGRES_PASSWORD: "newsreader" + volumes: + - postgres-data:/var/lib/postgresql/data rabbitmq: image: rabbitmq:3.7 - container_name: rabbitmq - celery: - build: . - container_name: celery - command: poetry run celery -A newsreader worker -l INFO --beat --scheduler django --workdir=/app/src/ - environment: - - POSTGRES_HOST=$POSTGRES_HOST - - POSTGRES_NAME=$POSTGRES_NAME - - POSTGRES_USER=$POSTGRES_USER - - POSTGRES_PASSWORD=$POSTGRES_PASSWORD - - DJANGO_SETTINGS_MODULE=newsreader.conf.docker - volumes: - - .:/app - depends_on: - - rabbitmq memcached: image: memcached:1.5.22 - container_name: memcached ports: - "11211:11211" entrypoint: - memcached - -m 64 - web: - build: . - container_name: web + celery: + build: + context: . + dockerfile: ./docker/django + command: celery worker --app newsreader --loglevel INFO --beat --scheduler django --workdir /app/src/ + environment: + - DJANGO_SETTINGS_MODULE=newsreader.conf.docker + depends_on: + - rabbitmq + django: + build: + context: . + dockerfile: ./docker/django command: src/entrypoint.sh environment: - - POSTGRES_HOST=$POSTGRES_HOST - - POSTGRES_NAME=$POSTGRES_NAME - - POSTGRES_USER=$POSTGRES_USER - - POSTGRES_PASSWORD=$POSTGRES_PASSWORD - DJANGO_SETTINGS_MODULE=newsreader.conf.docker - volumes: - - .:/app ports: - '8000:8000' depends_on: - db + volumes: + - .:/app + - static-files:/app/src/newsreader/static + webpack: + build: + context: . + dockerfile: ./docker/webpack + command: npm run build:watch + volumes: + - .:/app + - static-files:/app/src/newsreader/static + - node-modules:/app/node_modules diff --git a/Dockerfile b/docker/django similarity index 59% rename from Dockerfile rename to docker/django index 61ef10b..871828a 100644 --- a/Dockerfile +++ b/docker/django @@ -5,7 +5,6 @@ RUN pip install poetry WORKDIR /app COPY poetry.lock pyproject.toml /app/ -RUN poetry config virtualenvs.create false -RUN poetry install --no-interaction +RUN poetry config virtualenvs.create false && poetry install --no-interaction COPY . /app/ diff --git a/docker/webpack b/docker/webpack new file mode 100644 index 0000000..6909ee9 --- /dev/null +++ b/docker/webpack @@ -0,0 +1,9 @@ +FROM node:12 + +WORKDIR /app + +COPY package.json package-lock.json /app/ + +RUN npm install + +COPY . /app/ diff --git a/src/entrypoint.sh b/src/entrypoint.sh index 451b7d3..3fbf941 100755 --- a/src/entrypoint.sh +++ b/src/entrypoint.sh @@ -1,5 +1,5 @@ #!/bin/bash # This file should only be used in conjuction with docker-compose -poetry run /app/src/manage.py migrate -poetry run /app/src/manage.py runserver 0.0.0.0:8000 +python /app/src/manage.py migrate +python /app/src/manage.py runserver 0.0.0.0:8000 diff --git a/src/newsreader/accounts/admin.py b/src/newsreader/accounts/admin.py index 846f6b4..c223687 100644 --- a/src/newsreader/accounts/admin.py +++ b/src/newsreader/accounts/admin.py @@ -1 +1,30 @@ -# Register your models here. +from django.contrib import admin +from django.utils.translation import ugettext as _ + +from newsreader.accounts.models import User + + +class UserAdmin(admin.ModelAdmin): + list_display = ("email", "last_name", "date_joined", "is_active") + list_filter = ("is_active", "is_staff", "is_superuser") + ordering = ("email",) + + search_fields = ["email", "last_name", "first_name"] + readonly_fields = ("last_login", "date_joined") + fieldsets = ( + ( + _("User settings"), + {"fields": ("email", "first_name", "last_name", "is_active")}, + ), + ( + _("Permission settings"), + { + "classes": ("collapse",), + "fields": ("is_staff", "is_superuser", "groups", "user_permissions"), + }, + ), + (_("Misc settings"), {"fields": ("date_joined", "last_login")}), + ) + + +admin.site.register(User, UserAdmin) diff --git a/src/newsreader/accounts/forms.py b/src/newsreader/accounts/forms.py new file mode 100644 index 0000000..7a29f99 --- /dev/null +++ b/src/newsreader/accounts/forms.py @@ -0,0 +1,9 @@ +from django import forms + +from newsreader.accounts.models import User + + +class UserSettingsForm(forms.ModelForm): + class Meta: + model = User + fields = ("first_name", "last_name") diff --git a/src/newsreader/accounts/templates/accounts/components/login-form.html b/src/newsreader/accounts/templates/accounts/components/login-form.html new file mode 100644 index 0000000..87dceb9 --- /dev/null +++ b/src/newsreader/accounts/templates/accounts/components/login-form.html @@ -0,0 +1,17 @@ +{% extends "components/form/form.html" %} +{% load i18n %} + +{% block actions %} +
+
+ {% include "components/form/cancel-button.html" %} + {% include "components/form/confirm-button.html" %} +
+ +
+ + {% trans "I forgot my password" %} + +
+
+{% endblock actions %} diff --git a/src/newsreader/accounts/templates/accounts/components/settings-form.html b/src/newsreader/accounts/templates/accounts/components/settings-form.html new file mode 100644 index 0000000..ff06cb7 --- /dev/null +++ b/src/newsreader/accounts/templates/accounts/components/settings-form.html @@ -0,0 +1,18 @@ +{% extends "components/form/form.html" %} +{% load i18n %} + +{% block actions %} +
+
+ {% include "components/form/cancel-button.html" %} +
+ +
+ + {% trans "Change password" %} + + + {% include "components/form/confirm-button.html" %} +
+
+{% endblock actions %} diff --git a/src/newsreader/accounts/templates/accounts/login.html b/src/newsreader/accounts/templates/accounts/login.html deleted file mode 100644 index ab308b2..0000000 --- a/src/newsreader/accounts/templates/accounts/login.html +++ /dev/null @@ -1,24 +0,0 @@ -{% extends "base.html" %} - -{% load static %} - -{% block content %} -
-
- {% csrf_token %} -
-

Login

-
- - - -
-
-{% endblock %} diff --git a/src/newsreader/accounts/templates/accounts/views/login.html b/src/newsreader/accounts/templates/accounts/views/login.html new file mode 100644 index 0000000..b4c391d --- /dev/null +++ b/src/newsreader/accounts/templates/accounts/views/login.html @@ -0,0 +1,7 @@ +{% extends "base.html" %} + +{% block content %} +
+ {% include "accounts/components/login-form.html" with form=form title="Login" confirm_text="Login" %} +
+{% endblock %} diff --git a/src/newsreader/accounts/templates/accounts/views/password-change.html b/src/newsreader/accounts/templates/accounts/views/password-change.html new file mode 100644 index 0000000..fb8a98b --- /dev/null +++ b/src/newsreader/accounts/templates/accounts/views/password-change.html @@ -0,0 +1,8 @@ +{% extends "base.html" %} + +{% block content %} +
+ {% url 'accounts:settings' as cancel_url %} + {% include "components/form/form.html" with form=form title="Change password" confirm_text="Change password" cancel_url=cancel_url %} +
+{% endblock %} diff --git a/src/newsreader/accounts/templates/accounts/views/settings.html b/src/newsreader/accounts/templates/accounts/views/settings.html new file mode 100644 index 0000000..bf01f8e --- /dev/null +++ b/src/newsreader/accounts/templates/accounts/views/settings.html @@ -0,0 +1,7 @@ +{% extends "base.html" %} + +{% block content %} +
+ {% include "accounts/components/settings-form.html" with form=form title="User profile" confirm_text="Save" %} +
+{% endblock %} diff --git a/src/newsreader/accounts/tests/test_views.py b/src/newsreader/accounts/tests/test_views.py new file mode 100644 index 0000000..d3ac77c --- /dev/null +++ b/src/newsreader/accounts/tests/test_views.py @@ -0,0 +1,29 @@ +from django.test import TestCase +from django.urls import reverse + +from newsreader.accounts.models import User +from newsreader.accounts.tests.factories import UserFactory + + +class UserSettingsViewTestCase(TestCase): + def setUp(self): + self.user = UserFactory(password="test") + self.client.force_login(self.user) + + def test_simple(self): + response = self.client.get(reverse("accounts:settings")) + + self.assertEquals(response.status_code, 200) + + def test_user_credential_change(self): + response = self.client.post( + reverse("accounts:settings"), + {"first_name": "First name", "last_name": "Last name"}, + ) + + user = User.objects.get() + + self.assertRedirects(response, reverse("accounts:settings")) + + self.assertEquals(user.first_name, "First name") + self.assertEquals(user.last_name, "Last name") diff --git a/src/newsreader/accounts/urls.py b/src/newsreader/accounts/urls.py index 8605233..d42ae13 100644 --- a/src/newsreader/accounts/urls.py +++ b/src/newsreader/accounts/urls.py @@ -1,3 +1,4 @@ +from django.contrib.auth.decorators import login_required from django.urls import path from newsreader.accounts.views import ( @@ -6,6 +7,7 @@ from newsreader.accounts.views import ( ActivationView, LoginView, LogoutView, + PasswordChangeView, PasswordResetCompleteView, PasswordResetConfirmView, PasswordResetDoneView, @@ -13,6 +15,7 @@ from newsreader.accounts.views import ( RegistrationClosedView, RegistrationCompleteView, RegistrationView, + SettingsView, ) @@ -52,5 +55,10 @@ urlpatterns = [ PasswordResetCompleteView.as_view(), name="password-reset-complete", ), - # TODO: create password change views + path( + "password-change/", + login_required(PasswordChangeView.as_view()), + name="password-change", + ), + path("settings/", login_required(SettingsView.as_view()), name="settings"), ] diff --git a/src/newsreader/accounts/views.py b/src/newsreader/accounts/views.py index 28ae92d..fed60eb 100644 --- a/src/newsreader/accounts/views.py +++ b/src/newsreader/accounts/views.py @@ -2,15 +2,17 @@ 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 django.views.generic.edit import FormView, ModelFormMixin from registration.backends.default import views as registration_views +from newsreader.accounts.forms import UserSettingsForm +from newsreader.accounts.models import User + class LoginView(django_views.LoginView): - template_name = "accounts/login.html" - - def get_success_url(self): - return reverse_lazy("index") + template_name = "accounts/views/login.html" + success_url = reverse_lazy("index") class LogoutView(django_views.LogoutView): @@ -72,20 +74,42 @@ class ActivationResendView(registration_views.ResendActivationView): # 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" + template_name = "password-reset/password-reset.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" + template_name = "password-reset/password-reset-done.html" class PasswordResetConfirmView(django_views.PasswordResetConfirmView): - template_name = "password-reset/password_reset_confirm.html" + 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" + template_name = "password-reset/password-reset-complete.html" + + +class PasswordChangeView(django_views.PasswordChangeView): + template_name = "accounts/views/password-change.html" + success_url = reverse_lazy("accounts:settings") + + +class SettingsView(ModelFormMixin, FormView): + template_name = "accounts/views/settings.html" + success_url = reverse_lazy("accounts:settings") + form_class = UserSettingsForm + model = User + + def get(self, request, *args, **kwargs): + self.object = self.get_object() + return super().get(request, *args, **kwargs) + + def get_object(self, **kwargs): + return self.request.user + + def get_form_kwargs(self): + return {**super().get_form_kwargs(), "instance": self.request.user} diff --git a/src/newsreader/conf/base.py b/src/newsreader/conf/base.py index 3692deb..ee5a296 100644 --- a/src/newsreader/conf/base.py +++ b/src/newsreader/conf/base.py @@ -31,6 +31,7 @@ INSTALLED_APPS = [ "axes", # app modules "newsreader.accounts", + "newsreader.news", "newsreader.news.core", "newsreader.news.collection", ] @@ -171,6 +172,8 @@ AUTH_PASSWORD_VALIDATORS = [ # Authentication user model AUTH_USER_MODEL = "accounts.User" +LOGIN_REDIRECT_URL = "/" + # Internationalization # https://docs.djangoproject.com/en/2.2/topics/i18n/ LANGUAGE_CODE = "en-us" diff --git a/src/newsreader/conf/docker.py b/src/newsreader/conf/docker.py index 3584b30..dd2471f 100644 --- a/src/newsreader/conf/docker.py +++ b/src/newsreader/conf/docker.py @@ -3,9 +3,15 @@ from .dev import * # isort:skip SECRET_KEY = "=q(ztyo)b6noom#a164g&s9vcj1aawa^g#ing_ir99=_zl4g&$" -# Celery -# https://docs.celeryproject.org/en/latest/userguide/configuration.html -BROKER_URL = "amqp://guest:guest@rabbitmq:5672//" +DATABASES = { + "default": { + "ENGINE": "django.db.backends.postgresql", + "NAME": "newsreader", + "USER": "newsreader", + "PASSWORD": "newsreader", + "HOST": "db", + } +} CACHES = { "default": { @@ -17,3 +23,7 @@ CACHES = { "LOCATION": "memcached:11211", }, } + +# Celery +# https://docs.celeryproject.org/en/latest/userguide/configuration.html +CELERY_BROKER_URL = "amqp://guest:guest@rabbitmq:5672//" diff --git a/src/newsreader/fixtures/default-fixture.json b/src/newsreader/fixtures/default-fixture.json index 7b7ecdf..a6a9162 100644 --- a/src/newsreader/fixtures/default-fixture.json +++ b/src/newsreader/fixtures/default-fixture.json @@ -1,68 +1,143 @@ [ { - "model": "django_celery_beat.periodictask", - "pk": 10, + "model": "contenttypes.contenttype", "fields": { - "name": "sonny@bakker.nl-collection-task", - "task": "newsreader.news.collection.tasks.FeedTask", - "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": "" + "app_label": "admin", + "model": "logentry" } }, { - "model": "django_celery_beat.periodictask", - "pk": 26, + "model": "contenttypes.contenttype", "fields": { - "name": "sonnyba871@gmail.com-collection-task", - "task": "newsreader.news.collection.tasks.FeedTask", - "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": "" + "app_label": "auth", + "model": "permission" } }, { - "model": "django_celery_beat.crontabschedule", - "pk": 1, + "model": "contenttypes.contenttype", "fields": { - "minute": "0", - "hour": "4", - "day_of_week": "*", - "day_of_month": "*", - "month_of_year": "*", - "timezone": "UTC" + "app_label": "auth", + "model": "group" + } +}, +{ + "model": "contenttypes.contenttype", + "fields": { + "app_label": "contenttypes", + "model": "contenttype" + } +}, +{ + "model": "contenttypes.contenttype", + "fields": { + "app_label": "sessions", + "model": "session" + } +}, +{ + "model": "contenttypes.contenttype", + "fields": { + "app_label": "django_celery_beat", + "model": "crontabschedule" + } +}, +{ + "model": "contenttypes.contenttype", + "fields": { + "app_label": "django_celery_beat", + "model": "intervalschedule" + } +}, +{ + "model": "contenttypes.contenttype", + "fields": { + "app_label": "django_celery_beat", + "model": "periodictask" + } +}, +{ + "model": "contenttypes.contenttype", + "fields": { + "app_label": "django_celery_beat", + "model": "periodictasks" + } +}, +{ + "model": "contenttypes.contenttype", + "fields": { + "app_label": "django_celery_beat", + "model": "solarschedule" + } +}, +{ + "model": "contenttypes.contenttype", + "fields": { + "app_label": "django_celery_beat", + "model": "clockedschedule" + } +}, +{ + "model": "contenttypes.contenttype", + "fields": { + "app_label": "registration", + "model": "registrationprofile" + } +}, +{ + "model": "contenttypes.contenttype", + "fields": { + "app_label": "registration", + "model": "supervisedregistrationprofile" + } +}, +{ + "model": "contenttypes.contenttype", + "fields": { + "app_label": "axes", + "model": "accessattempt" + } +}, +{ + "model": "contenttypes.contenttype", + "fields": { + "app_label": "axes", + "model": "accesslog" + } +}, +{ + "model": "contenttypes.contenttype", + "fields": { + "app_label": "accounts", + "model": "user" + } +}, +{ + "model": "contenttypes.contenttype", + "fields": { + "app_label": "core", + "model": "post" + } +}, +{ + "model": "contenttypes.contenttype", + "fields": { + "app_label": "core", + "model": "category" + } +}, +{ + "model": "contenttypes.contenttype", + "fields": { + "app_label": "collection", + "model": "collectionrule" + } +}, +{ + "model": "sessions.session", + "pk": "3sumq22krk8tsvexcs4b8czu82yhvuer", + "fields": { + "session_data": "OWZkZTQyZDQ2NzNkYzdkOTBhM2ZlOWU3MDhhNDkyMWQ0MDdmZTc5ODp7Il9hdXRoX3VzZXJfaWQiOiIxIiwiX2F1dGhfdXNlcl9iYWNrZW5kIjoiZGphbmdvLmNvbnRyaWIuYXV0aC5iYWNrZW5kcy5Nb2RlbEJhY2tlbmQiLCJfYXV0aF91c2VyX2hhc2giOiJhZTMwMWFlMzI5OGFlOThkNjY1MTY1NDIxM2EyMmM0NDA0Y2FkZTc3In0=", + "expire_date": "2020-05-16T18:29:04.049Z" } }, { @@ -97,11 +172,950 @@ "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": "2020-05-02T20:40:29.029Z" + } +}, +{ + "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, + "expire_seconds": 43200, + "one_off": false, + "start_time": null, + "enabled": true, + "last_run_at": null, + "total_run_count": 0, + "date_changed": "2020-05-02T20:06:23.985Z", + "description": "" + } +}, +{ + "model": "django_celery_beat.periodictask", + "pk": 10, + "fields": { + "name": "sonny@bakker.nl-collection-task", + "task": "newsreader.news.collection.tasks.FeedTask", + "interval": 4, + "crontab": null, + "solar": null, + "clocked": null, + "args": "[2]", + "kwargs": "{}", + "queue": null, + "exchange": null, + "routing_key": null, + "headers": "{}", + "priority": null, + "expires": null, + "expire_seconds": null, + "one_off": false, + "start_time": null, + "enabled": true, + "last_run_at": "2020-05-02T20:06:24.012Z", + "total_run_count": 292, + "date_changed": "2020-05-02T20:06:24.027Z", + "description": "" + } +}, +{ + "model": "django_celery_beat.periodictask", + "pk": 26, + "fields": { + "name": "sonnyba871@gmail.com-collection-task", + "task": "newsreader.news.collection.tasks.FeedTask", + "interval": 4, + "crontab": null, + "solar": null, + "clocked": null, + "args": "[18]", + "kwargs": "{}", + "queue": null, + "exchange": null, + "routing_key": null, + "headers": "{}", + "priority": null, + "expires": null, + "expire_seconds": null, + "one_off": false, + "start_time": null, + "enabled": true, + "last_run_at": "2020-05-02T20:06:24.045Z", + "total_run_count": 105, + "date_changed": "2020-05-02T20:09:24.331Z", + "description": "" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can add log entry", + "content_type": [ + "admin", + "logentry" + ], + "codename": "add_logentry" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can change log entry", + "content_type": [ + "admin", + "logentry" + ], + "codename": "change_logentry" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can delete log entry", + "content_type": [ + "admin", + "logentry" + ], + "codename": "delete_logentry" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can view log entry", + "content_type": [ + "admin", + "logentry" + ], + "codename": "view_logentry" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can add permission", + "content_type": [ + "auth", + "permission" + ], + "codename": "add_permission" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can change permission", + "content_type": [ + "auth", + "permission" + ], + "codename": "change_permission" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can delete permission", + "content_type": [ + "auth", + "permission" + ], + "codename": "delete_permission" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can view permission", + "content_type": [ + "auth", + "permission" + ], + "codename": "view_permission" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can add group", + "content_type": [ + "auth", + "group" + ], + "codename": "add_group" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can change group", + "content_type": [ + "auth", + "group" + ], + "codename": "change_group" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can delete group", + "content_type": [ + "auth", + "group" + ], + "codename": "delete_group" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can view group", + "content_type": [ + "auth", + "group" + ], + "codename": "view_group" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can add content type", + "content_type": [ + "contenttypes", + "contenttype" + ], + "codename": "add_contenttype" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can change content type", + "content_type": [ + "contenttypes", + "contenttype" + ], + "codename": "change_contenttype" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can delete content type", + "content_type": [ + "contenttypes", + "contenttype" + ], + "codename": "delete_contenttype" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can view content type", + "content_type": [ + "contenttypes", + "contenttype" + ], + "codename": "view_contenttype" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can add session", + "content_type": [ + "sessions", + "session" + ], + "codename": "add_session" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can change session", + "content_type": [ + "sessions", + "session" + ], + "codename": "change_session" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can delete session", + "content_type": [ + "sessions", + "session" + ], + "codename": "delete_session" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can view session", + "content_type": [ + "sessions", + "session" + ], + "codename": "view_session" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can add crontab", + "content_type": [ + "django_celery_beat", + "crontabschedule" + ], + "codename": "add_crontabschedule" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can change crontab", + "content_type": [ + "django_celery_beat", + "crontabschedule" + ], + "codename": "change_crontabschedule" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can delete crontab", + "content_type": [ + "django_celery_beat", + "crontabschedule" + ], + "codename": "delete_crontabschedule" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can view crontab", + "content_type": [ + "django_celery_beat", + "crontabschedule" + ], + "codename": "view_crontabschedule" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can add interval", + "content_type": [ + "django_celery_beat", + "intervalschedule" + ], + "codename": "add_intervalschedule" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can change interval", + "content_type": [ + "django_celery_beat", + "intervalschedule" + ], + "codename": "change_intervalschedule" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can delete interval", + "content_type": [ + "django_celery_beat", + "intervalschedule" + ], + "codename": "delete_intervalschedule" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can view interval", + "content_type": [ + "django_celery_beat", + "intervalschedule" + ], + "codename": "view_intervalschedule" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can add periodic task", + "content_type": [ + "django_celery_beat", + "periodictask" + ], + "codename": "add_periodictask" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can change periodic task", + "content_type": [ + "django_celery_beat", + "periodictask" + ], + "codename": "change_periodictask" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can delete periodic task", + "content_type": [ + "django_celery_beat", + "periodictask" + ], + "codename": "delete_periodictask" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can view periodic task", + "content_type": [ + "django_celery_beat", + "periodictask" + ], + "codename": "view_periodictask" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can add periodic tasks", + "content_type": [ + "django_celery_beat", + "periodictasks" + ], + "codename": "add_periodictasks" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can change periodic tasks", + "content_type": [ + "django_celery_beat", + "periodictasks" + ], + "codename": "change_periodictasks" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can delete periodic tasks", + "content_type": [ + "django_celery_beat", + "periodictasks" + ], + "codename": "delete_periodictasks" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can view periodic tasks", + "content_type": [ + "django_celery_beat", + "periodictasks" + ], + "codename": "view_periodictasks" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can add solar event", + "content_type": [ + "django_celery_beat", + "solarschedule" + ], + "codename": "add_solarschedule" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can change solar event", + "content_type": [ + "django_celery_beat", + "solarschedule" + ], + "codename": "change_solarschedule" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can delete solar event", + "content_type": [ + "django_celery_beat", + "solarschedule" + ], + "codename": "delete_solarschedule" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can view solar event", + "content_type": [ + "django_celery_beat", + "solarschedule" + ], + "codename": "view_solarschedule" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can add clocked", + "content_type": [ + "django_celery_beat", + "clockedschedule" + ], + "codename": "add_clockedschedule" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can change clocked", + "content_type": [ + "django_celery_beat", + "clockedschedule" + ], + "codename": "change_clockedschedule" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can delete clocked", + "content_type": [ + "django_celery_beat", + "clockedschedule" + ], + "codename": "delete_clockedschedule" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can view clocked", + "content_type": [ + "django_celery_beat", + "clockedschedule" + ], + "codename": "view_clockedschedule" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can add registration profile", + "content_type": [ + "registration", + "registrationprofile" + ], + "codename": "add_registrationprofile" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can change registration profile", + "content_type": [ + "registration", + "registrationprofile" + ], + "codename": "change_registrationprofile" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can delete registration profile", + "content_type": [ + "registration", + "registrationprofile" + ], + "codename": "delete_registrationprofile" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can view registration profile", + "content_type": [ + "registration", + "registrationprofile" + ], + "codename": "view_registrationprofile" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can add supervised registration profile", + "content_type": [ + "registration", + "supervisedregistrationprofile" + ], + "codename": "add_supervisedregistrationprofile" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can change supervised registration profile", + "content_type": [ + "registration", + "supervisedregistrationprofile" + ], + "codename": "change_supervisedregistrationprofile" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can delete supervised registration profile", + "content_type": [ + "registration", + "supervisedregistrationprofile" + ], + "codename": "delete_supervisedregistrationprofile" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can view supervised registration profile", + "content_type": [ + "registration", + "supervisedregistrationprofile" + ], + "codename": "view_supervisedregistrationprofile" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can add access attempt", + "content_type": [ + "axes", + "accessattempt" + ], + "codename": "add_accessattempt" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can change access attempt", + "content_type": [ + "axes", + "accessattempt" + ], + "codename": "change_accessattempt" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can delete access attempt", + "content_type": [ + "axes", + "accessattempt" + ], + "codename": "delete_accessattempt" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can view access attempt", + "content_type": [ + "axes", + "accessattempt" + ], + "codename": "view_accessattempt" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can add access log", + "content_type": [ + "axes", + "accesslog" + ], + "codename": "add_accesslog" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can change access log", + "content_type": [ + "axes", + "accesslog" + ], + "codename": "change_accesslog" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can delete access log", + "content_type": [ + "axes", + "accesslog" + ], + "codename": "delete_accesslog" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can view access log", + "content_type": [ + "axes", + "accesslog" + ], + "codename": "view_accesslog" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can add user", + "content_type": [ + "accounts", + "user" + ], + "codename": "add_user" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can change user", + "content_type": [ + "accounts", + "user" + ], + "codename": "change_user" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can delete user", + "content_type": [ + "accounts", + "user" + ], + "codename": "delete_user" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can view user", + "content_type": [ + "accounts", + "user" + ], + "codename": "view_user" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can add post", + "content_type": [ + "core", + "post" + ], + "codename": "add_post" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can change post", + "content_type": [ + "core", + "post" + ], + "codename": "change_post" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can delete post", + "content_type": [ + "core", + "post" + ], + "codename": "delete_post" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can view post", + "content_type": [ + "core", + "post" + ], + "codename": "view_post" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can add Category", + "content_type": [ + "core", + "category" + ], + "codename": "add_category" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can change Category", + "content_type": [ + "core", + "category" + ], + "codename": "change_category" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can delete Category", + "content_type": [ + "core", + "category" + ], + "codename": "delete_category" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can view Category", + "content_type": [ + "core", + "category" + ], + "codename": "view_category" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can add collection rule", + "content_type": [ + "collection", + "collectionrule" + ], + "codename": "add_collectionrule" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can change collection rule", + "content_type": [ + "collection", + "collectionrule" + ], + "codename": "change_collectionrule" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can delete collection rule", + "content_type": [ + "collection", + "collectionrule" + ], + "codename": "delete_collectionrule" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can view collection rule", + "content_type": [ + "collection", + "collectionrule" + ], + "codename": "view_collectionrule" + } +}, { "model": "accounts.user", "fields": { - "password": "pbkdf2_sha256$150000$5lBD7JemxYfE$B+lM5wWUW2n/ZulPFaWHtzWjyQ/QZ6iwjAC2I0R/VzU=", - "last_login": "2019-11-27T18:57:36.686Z", + "password": "pbkdf2_sha256$180000$KGKGsPnSwyiN$RqQAD46r4Kzqndqp5dmpj+H/drDrPRI0r6j4gLtYBjE=", + "last_login": "2020-05-02T18:29:04.047Z", "is_superuser": true, "first_name": "", "last_name": "", @@ -114,23 +1128,6 @@ "user_permissions": [] } }, -{ - "model": "accounts.user", - "fields": { - "password": "pbkdf2_sha256$150000$vUwxT8T25R8C$S+Eq2tMRbSDE31/X5KGJ/M+Nblh7kKfzuM/z7HraR/Q=", - "last_login": null, - "is_superuser": false, - "first_name": "", - "last_name": "", - "is_staff": false, - "is_active": false, - "date_joined": "2019-11-25T15:35:14.051Z", - "email": "sonnyba871@gmail.com", - "task": 26, - "groups": [], - "user_permissions": [] - } -}, { "model": "core.category", "pk": 8, @@ -160,14 +1157,14 @@ "pk": 3, "fields": { "created": "2019-07-14T13:08:10.374Z", - "modified": "2019-11-29T22:35:20.346Z", + "modified": "2020-05-02T20:06:25.841Z", "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", + "last_suceeded": "2020-05-02T20:06:25.793Z", "succeeded": true, "error": null, "user": [ @@ -180,14 +1177,14 @@ "pk": 4, "fields": { "created": "2019-07-20T11:24:32.745Z", - "modified": "2019-11-29T22:35:19.525Z", + "modified": "2020-05-02T20:06:24.719Z", "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", + "last_suceeded": "2020-05-02T20:06:24.128Z", "succeeded": true, "error": null, "user": [ @@ -200,14 +1197,14 @@ "pk": 5, "fields": { "created": "2019-07-20T11:24:50.411Z", - "modified": "2019-11-29T22:35:20.010Z", + "modified": "2020-05-02T20:06:25.548Z", "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", + "last_suceeded": "2020-05-02T20:06:25.364Z", "succeeded": true, "error": null, "user": [ @@ -220,14 +1217,14 @@ "pk": 6, "fields": { "created": "2019-07-20T11:25:02.089Z", - "modified": "2019-11-29T22:35:20.233Z", + "modified": "2020-05-02T20:06:25.741Z", "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", + "last_suceeded": "2020-05-02T20:06:25.620Z", "succeeded": true, "error": null, "user": [ @@ -240,14 +1237,14 @@ "pk": 7, "fields": { "created": "2019-07-20T11:25:30.121Z", - "modified": "2019-11-29T22:35:19.695Z", + "modified": "2020-05-02T20:06:25.352Z", "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", + "last_suceeded": "2020-05-02T20:06:24.730Z", "succeeded": true, "error": null, "user": [ @@ -260,14 +1257,14 @@ "pk": 8, "fields": { "created": "2019-07-20T11:25:46.256Z", - "modified": "2019-11-29T22:35:20.074Z", + "modified": "2020-05-02T20:06:25.792Z", "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", + "last_suceeded": "2020-05-02T20:06:25.742Z", "succeeded": true, "error": null, "user": [ @@ -280,19 +1277,1419 @@ "pk": 9, "fields": { "created": "2019-11-24T15:28:41.399Z", - "modified": "2019-11-29T22:35:19.807Z", + "modified": "2020-05-02T20:06:25.619Z", "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", + "last_suceeded": "2020-05-02T20:06:25.549Z", "succeeded": true, "error": null, "user": [ "sonny@bakker.nl" ] } +}, +{ + "model": "collection.collectionrule", + "pk": 10, + "fields": { + "created": "2020-05-02T20:32:34.107Z", + "modified": "2020-05-02T20:32:34.107Z", + "name": "CollectionRule-0", + "url": "http://rasmussen-guerra.com/", + "website_url": "https://ritter.com/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 11, + "fields": { + "created": "2020-05-02T20:32:34.164Z", + "modified": "2020-05-02T20:32:34.164Z", + "name": "CollectionRule-1", + "url": "https://www.evans.com/", + "website_url": "https://taylor.com/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 12, + "fields": { + "created": "2020-05-02T20:32:34.220Z", + "modified": "2020-05-02T20:32:34.220Z", + "name": "CollectionRule-2", + "url": "http://weaver-quinn.net/", + "website_url": "https://www.mcintyre.com/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 13, + "fields": { + "created": "2020-05-02T20:32:34.277Z", + "modified": "2020-05-02T20:32:34.277Z", + "name": "CollectionRule-3", + "url": "http://www.palmer.com/", + "website_url": "http://www.riggs.org/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 14, + "fields": { + "created": "2020-05-02T20:32:34.333Z", + "modified": "2020-05-02T20:32:34.333Z", + "name": "CollectionRule-4", + "url": "http://moody-stein.net/", + "website_url": "https://www.lewis.com/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 15, + "fields": { + "created": "2020-05-02T20:32:34.390Z", + "modified": "2020-05-02T20:32:34.391Z", + "name": "CollectionRule-5", + "url": "http://www.ochoa.com/", + "website_url": "https://brown.com/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 16, + "fields": { + "created": "2020-05-02T20:32:34.448Z", + "modified": "2020-05-02T20:32:34.448Z", + "name": "CollectionRule-6", + "url": "https://www.pearson.biz/", + "website_url": "http://acosta-johnson.com/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 17, + "fields": { + "created": "2020-05-02T20:32:34.506Z", + "modified": "2020-05-02T20:32:34.506Z", + "name": "CollectionRule-7", + "url": "https://jones.com/", + "website_url": "https://www.thornton.com/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 18, + "fields": { + "created": "2020-05-02T20:32:34.562Z", + "modified": "2020-05-02T20:32:34.562Z", + "name": "CollectionRule-8", + "url": "http://www.matthews-graves.com/", + "website_url": "http://stewart.com/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 19, + "fields": { + "created": "2020-05-02T20:32:34.618Z", + "modified": "2020-05-02T20:32:34.618Z", + "name": "CollectionRule-9", + "url": "http://www.kelly-martinez.com/", + "website_url": "https://www.freeman.com/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 20, + "fields": { + "created": "2020-05-02T20:32:34.674Z", + "modified": "2020-05-02T20:32:34.674Z", + "name": "CollectionRule-10", + "url": "https://www.roberts.biz/", + "website_url": "http://www.lopez.info/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 21, + "fields": { + "created": "2020-05-02T20:32:34.730Z", + "modified": "2020-05-02T20:32:34.730Z", + "name": "CollectionRule-11", + "url": "https://www.holmes-cross.com/", + "website_url": "https://www.ramirez.net/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 22, + "fields": { + "created": "2020-05-02T20:32:34.786Z", + "modified": "2020-05-02T20:32:34.786Z", + "name": "CollectionRule-12", + "url": "https://www.jenkins.com/", + "website_url": "https://www.faulkner.com/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 23, + "fields": { + "created": "2020-05-02T20:32:34.841Z", + "modified": "2020-05-02T20:32:34.842Z", + "name": "CollectionRule-13", + "url": "https://www.adkins.com/", + "website_url": "https://www.munoz-brown.info/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 24, + "fields": { + "created": "2020-05-02T20:32:34.897Z", + "modified": "2020-05-02T20:32:34.898Z", + "name": "CollectionRule-14", + "url": "https://www.rodriguez-ortega.biz/", + "website_url": "http://www.santos.info/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 25, + "fields": { + "created": "2020-05-02T20:32:34.953Z", + "modified": "2020-05-02T20:32:34.954Z", + "name": "CollectionRule-15", + "url": "https://www.hawkins-stewart.com/", + "website_url": "http://www.jones.com/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 26, + "fields": { + "created": "2020-05-02T20:32:35.010Z", + "modified": "2020-05-02T20:32:35.010Z", + "name": "CollectionRule-16", + "url": "http://mullins.net/", + "website_url": "https://www.curtis.org/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 27, + "fields": { + "created": "2020-05-02T20:32:35.067Z", + "modified": "2020-05-02T20:32:35.067Z", + "name": "CollectionRule-17", + "url": "http://frederick.com/", + "website_url": "https://www.fowler.info/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 28, + "fields": { + "created": "2020-05-02T20:32:35.124Z", + "modified": "2020-05-02T20:32:35.124Z", + "name": "CollectionRule-18", + "url": "http://schmidt.com/", + "website_url": "http://bryant-hoffman.com/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 29, + "fields": { + "created": "2020-05-02T20:32:35.180Z", + "modified": "2020-05-02T20:32:35.180Z", + "name": "CollectionRule-19", + "url": "https://www.jones.net/", + "website_url": "http://benjamin.com/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 30, + "fields": { + "created": "2020-05-02T20:32:35.237Z", + "modified": "2020-05-02T20:32:35.237Z", + "name": "CollectionRule-20", + "url": "https://www.parker-lewis.com/", + "website_url": "http://www.anderson.com/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 31, + "fields": { + "created": "2020-05-02T20:32:35.294Z", + "modified": "2020-05-02T20:32:35.294Z", + "name": "CollectionRule-21", + "url": "http://martinez.com/", + "website_url": "http://burton-scott.biz/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 32, + "fields": { + "created": "2020-05-02T20:32:35.350Z", + "modified": "2020-05-02T20:32:35.350Z", + "name": "CollectionRule-22", + "url": "https://gibbs.com/", + "website_url": "https://www.robertson.com/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 33, + "fields": { + "created": "2020-05-02T20:32:35.407Z", + "modified": "2020-05-02T20:32:35.407Z", + "name": "CollectionRule-23", + "url": "http://www.fisher.com/", + "website_url": "https://mcclure-miller.com/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 34, + "fields": { + "created": "2020-05-02T20:32:35.463Z", + "modified": "2020-05-02T20:32:35.463Z", + "name": "CollectionRule-24", + "url": "https://schneider-lopez.org/", + "website_url": "https://andrews-williams.biz/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 35, + "fields": { + "created": "2020-05-02T20:32:35.522Z", + "modified": "2020-05-02T20:32:35.522Z", + "name": "CollectionRule-25", + "url": "http://www.rogers.info/", + "website_url": "https://www.petersen-stewart.biz/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 36, + "fields": { + "created": "2020-05-02T20:32:35.581Z", + "modified": "2020-05-02T20:32:35.581Z", + "name": "CollectionRule-26", + "url": "http://torres.com/", + "website_url": "https://hart-tapia.org/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 37, + "fields": { + "created": "2020-05-02T20:32:35.637Z", + "modified": "2020-05-02T20:32:35.638Z", + "name": "CollectionRule-27", + "url": "http://www.pham-scott.com/", + "website_url": "http://smith-diaz.com/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 38, + "fields": { + "created": "2020-05-02T20:32:35.699Z", + "modified": "2020-05-02T20:32:35.699Z", + "name": "CollectionRule-28", + "url": "http://www.gonzalez-castillo.com/", + "website_url": "http://www.conley.biz/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 39, + "fields": { + "created": "2020-05-02T20:32:35.758Z", + "modified": "2020-05-02T20:32:35.758Z", + "name": "CollectionRule-29", + "url": "https://rogers-smith.net/", + "website_url": "http://www.sharp.com/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 40, + "fields": { + "created": "2020-05-02T20:32:35.814Z", + "modified": "2020-05-02T20:32:35.814Z", + "name": "CollectionRule-30", + "url": "https://neal-salinas.com/", + "website_url": "https://www.baird-warner.net/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 41, + "fields": { + "created": "2020-05-02T20:32:35.873Z", + "modified": "2020-05-02T20:32:35.874Z", + "name": "CollectionRule-31", + "url": "http://www.williams.com/", + "website_url": "http://www.wood.com/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 42, + "fields": { + "created": "2020-05-02T20:32:35.930Z", + "modified": "2020-05-02T20:32:35.930Z", + "name": "CollectionRule-32", + "url": "https://www.mueller.com/", + "website_url": "http://www.miller-ramirez.org/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 43, + "fields": { + "created": "2020-05-02T20:32:35.988Z", + "modified": "2020-05-02T20:32:35.989Z", + "name": "CollectionRule-33", + "url": "http://lee.com/", + "website_url": "http://www.moody.org/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 44, + "fields": { + "created": "2020-05-02T20:32:36.044Z", + "modified": "2020-05-02T20:32:36.045Z", + "name": "CollectionRule-34", + "url": "http://estrada.com/", + "website_url": "http://www.hicks.com/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 45, + "fields": { + "created": "2020-05-02T20:32:36.102Z", + "modified": "2020-05-02T20:32:36.102Z", + "name": "CollectionRule-35", + "url": "https://griffin-brewer.org/", + "website_url": "http://jones.info/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 46, + "fields": { + "created": "2020-05-02T20:32:36.161Z", + "modified": "2020-05-02T20:32:36.161Z", + "name": "CollectionRule-36", + "url": "http://www.dixon-johnson.com/", + "website_url": "https://mason.com/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 47, + "fields": { + "created": "2020-05-02T20:32:36.217Z", + "modified": "2020-05-02T20:32:36.217Z", + "name": "CollectionRule-37", + "url": "https://perez.com/", + "website_url": "http://www.miller.com/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 48, + "fields": { + "created": "2020-05-02T20:32:36.278Z", + "modified": "2020-05-02T20:32:36.279Z", + "name": "CollectionRule-38", + "url": "https://www.grant.net/", + "website_url": "https://www.clayton.com/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 49, + "fields": { + "created": "2020-05-02T20:32:36.336Z", + "modified": "2020-05-02T20:32:36.336Z", + "name": "CollectionRule-39", + "url": "http://www.lewis.org/", + "website_url": "http://cook.org/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 50, + "fields": { + "created": "2020-05-02T20:32:36.395Z", + "modified": "2020-05-02T20:32:36.395Z", + "name": "CollectionRule-40", + "url": "https://galloway-allen.net/", + "website_url": "http://www.rodriguez-callahan.info/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 51, + "fields": { + "created": "2020-05-02T20:32:36.453Z", + "modified": "2020-05-02T20:32:36.453Z", + "name": "CollectionRule-41", + "url": "https://www.macias.com/", + "website_url": "https://jarvis-green.com/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 52, + "fields": { + "created": "2020-05-02T20:32:36.510Z", + "modified": "2020-05-02T20:32:36.510Z", + "name": "CollectionRule-42", + "url": "http://mccullough-grant.com/", + "website_url": "https://shannon.com/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 53, + "fields": { + "created": "2020-05-02T20:32:36.566Z", + "modified": "2020-05-02T20:32:36.566Z", + "name": "CollectionRule-43", + "url": "http://www.foster-oneal.org/", + "website_url": "http://johns.org/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 54, + "fields": { + "created": "2020-05-02T20:32:36.623Z", + "modified": "2020-05-02T20:32:36.623Z", + "name": "CollectionRule-44", + "url": "http://www.wright.net/", + "website_url": "http://www.ali.biz/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 55, + "fields": { + "created": "2020-05-02T20:32:36.682Z", + "modified": "2020-05-02T20:32:36.682Z", + "name": "CollectionRule-45", + "url": "http://www.payne-gibbs.info/", + "website_url": "http://knight.com/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 56, + "fields": { + "created": "2020-05-02T20:32:36.740Z", + "modified": "2020-05-02T20:32:36.740Z", + "name": "CollectionRule-46", + "url": "http://hammond.biz/", + "website_url": "http://www.nelson.net/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 57, + "fields": { + "created": "2020-05-02T20:32:36.797Z", + "modified": "2020-05-02T20:32:36.797Z", + "name": "CollectionRule-47", + "url": "http://gilmore.com/", + "website_url": "http://coleman.com/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 58, + "fields": { + "created": "2020-05-02T20:32:36.855Z", + "modified": "2020-05-02T20:32:36.855Z", + "name": "CollectionRule-48", + "url": "https://www.hernandez.com/", + "website_url": "https://www.phillips.com/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 59, + "fields": { + "created": "2020-05-02T20:32:36.912Z", + "modified": "2020-05-02T20:32:36.912Z", + "name": "CollectionRule-49", + "url": "https://www.nguyen.com/", + "website_url": "http://www.floyd.com/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 60, + "fields": { + "created": "2020-05-02T20:32:36.969Z", + "modified": "2020-05-02T20:32:36.969Z", + "name": "CollectionRule-50", + "url": "https://meyer-brown.net/", + "website_url": "https://www.blankenship.biz/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 61, + "fields": { + "created": "2020-05-02T20:32:37.026Z", + "modified": "2020-05-02T20:32:37.027Z", + "name": "CollectionRule-51", + "url": "https://marks.net/", + "website_url": "http://gregory.net/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 62, + "fields": { + "created": "2020-05-02T20:32:37.087Z", + "modified": "2020-05-02T20:32:37.087Z", + "name": "CollectionRule-52", + "url": "http://www.baxter.com/", + "website_url": "http://barrera.com/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 63, + "fields": { + "created": "2020-05-02T20:32:37.143Z", + "modified": "2020-05-02T20:32:37.143Z", + "name": "CollectionRule-53", + "url": "http://johnson.com/", + "website_url": "https://abbott.com/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 64, + "fields": { + "created": "2020-05-02T20:32:37.202Z", + "modified": "2020-05-02T20:32:37.202Z", + "name": "CollectionRule-54", + "url": "https://hebert-marshall.biz/", + "website_url": "https://www.ashley-walsh.org/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 65, + "fields": { + "created": "2020-05-02T20:32:37.261Z", + "modified": "2020-05-02T20:32:37.261Z", + "name": "CollectionRule-55", + "url": "https://miller.com/", + "website_url": "https://www.hoffman.com/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 66, + "fields": { + "created": "2020-05-02T20:32:37.320Z", + "modified": "2020-05-02T20:32:37.320Z", + "name": "CollectionRule-56", + "url": "http://frey.com/", + "website_url": "https://long.com/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 67, + "fields": { + "created": "2020-05-02T20:32:37.379Z", + "modified": "2020-05-02T20:32:37.379Z", + "name": "CollectionRule-57", + "url": "https://edwards.com/", + "website_url": "http://www.nixon-doyle.com/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 68, + "fields": { + "created": "2020-05-02T20:32:37.435Z", + "modified": "2020-05-02T20:32:37.435Z", + "name": "CollectionRule-58", + "url": "https://www.bennett.com/", + "website_url": "http://sullivan.com/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 69, + "fields": { + "created": "2020-05-02T20:32:37.493Z", + "modified": "2020-05-02T20:32:37.493Z", + "name": "CollectionRule-59", + "url": "http://stokes-thomas.com/", + "website_url": "http://morgan.net/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 70, + "fields": { + "created": "2020-05-02T20:32:37.550Z", + "modified": "2020-05-02T20:32:37.550Z", + "name": "CollectionRule-60", + "url": "https://moore.net/", + "website_url": "http://www.hubbard.biz/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 71, + "fields": { + "created": "2020-05-02T20:32:37.609Z", + "modified": "2020-05-02T20:32:37.609Z", + "name": "CollectionRule-61", + "url": "https://baker-edwards.com/", + "website_url": "https://www.anderson.com/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 72, + "fields": { + "created": "2020-05-02T20:32:37.666Z", + "modified": "2020-05-02T20:32:37.666Z", + "name": "CollectionRule-62", + "url": "https://www.jackson.com/", + "website_url": "https://www.edwards.com/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 73, + "fields": { + "created": "2020-05-02T20:32:37.724Z", + "modified": "2020-05-02T20:32:37.724Z", + "name": "CollectionRule-63", + "url": "https://kemp-pollard.biz/", + "website_url": "http://www.fuentes.com/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 74, + "fields": { + "created": "2020-05-02T20:32:37.782Z", + "modified": "2020-05-02T20:32:37.782Z", + "name": "CollectionRule-64", + "url": "https://hanna-cook.com/", + "website_url": "http://www.bowen.com/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 75, + "fields": { + "created": "2020-05-02T20:32:37.839Z", + "modified": "2020-05-02T20:32:37.839Z", + "name": "CollectionRule-65", + "url": "http://www.williams.net/", + "website_url": "http://www.chandler.org/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 76, + "fields": { + "created": "2020-05-02T20:32:37.896Z", + "modified": "2020-05-02T20:32:37.896Z", + "name": "CollectionRule-66", + "url": "https://www.alexander.com/", + "website_url": "https://johnson-ellis.com/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 77, + "fields": { + "created": "2020-05-02T20:32:37.951Z", + "modified": "2020-05-02T20:32:37.951Z", + "name": "CollectionRule-67", + "url": "https://www.cisneros.com/", + "website_url": "http://fox.com/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 78, + "fields": { + "created": "2020-05-02T20:32:38.008Z", + "modified": "2020-05-02T20:32:38.008Z", + "name": "CollectionRule-68", + "url": "http://www.foster-burton.com/", + "website_url": "https://grant.com/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } +}, +{ + "model": "collection.collectionrule", + "pk": 79, + "fields": { + "created": "2020-05-02T20:32:38.066Z", + "modified": "2020-05-02T20:32:38.066Z", + "name": "CollectionRule-69", + "url": "https://www.hayes.net/", + "website_url": "http://morgan.com/", + "favicon": null, + "timezone": "UTC", + "category": null, + "last_suceeded": null, + "succeeded": false, + "error": null, + "user": [ + "sonny@bakker.nl" + ] + } } ] diff --git a/src/newsreader/js/components/Selector.js b/src/newsreader/js/components/Selector.js new file mode 100644 index 0000000..8b701f5 --- /dev/null +++ b/src/newsreader/js/components/Selector.js @@ -0,0 +1,23 @@ +class Selector { + onClick = ::this.onClick; + + inputs = []; + + constructor() { + const selectAllInput = document.querySelector('#select-all'); + + this.inputs = document.querySelectorAll(`[name=${selectAllInput.dataset.input}`); + + selectAllInput.onchange = this.onClick; + } + + onClick(e) { + const targetValue = e.target.checked; + + this.inputs.forEach(input => { + input.checked = targetValue; + }); + } +} + +export default Selector; diff --git a/src/newsreader/js/index.js b/src/newsreader/js/index.js index 48db0b2..1ed14ed 100644 --- a/src/newsreader/js/index.js +++ b/src/newsreader/js/index.js @@ -1,3 +1,3 @@ import './pages/homepage/index.js'; -import './pages/rules/index.js'; import './pages/categories/index.js'; +import './pages/rules/index.js'; diff --git a/src/newsreader/js/pages/categories/App.js b/src/newsreader/js/pages/categories/App.js index 95ab396..691aaed 100644 --- a/src/newsreader/js/pages/categories/App.js +++ b/src/newsreader/js/pages/categories/App.js @@ -80,7 +80,7 @@ class App extends React.Component { const pageHeader = ( <>

Categories

- + Create category diff --git a/src/newsreader/js/pages/categories/components/CategoryCard.js b/src/newsreader/js/pages/categories/components/CategoryCard.js index a3a242d..94bd6f4 100644 --- a/src/newsreader/js/pages/categories/components/CategoryCard.js +++ b/src/newsreader/js/pages/categories/components/CategoryCard.js @@ -31,7 +31,10 @@ const CategoryCard = props => { const cardContent = <>{category.rules && }; 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 deleted file mode 100644 index d174cc3..0000000 --- a/src/newsreader/js/pages/rules/components/RuleModal.js +++ /dev/null @@ -1,35 +0,0 @@ -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 index d0b46e9..b888121 100644 --- a/src/newsreader/js/pages/rules/index.js +++ b/src/newsreader/js/pages/rules/index.js @@ -1,13 +1,7 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; - -import App from './App.js'; +import Selector from '../../components/Selector.js'; const page = document.getElementById('rules--page'); if (page) { - const dataScript = document.getElementById('rules-data'); - const rules = JSON.parse(dataScript.textContent); - - ReactDOM.render(, page); + new Selector(); } diff --git a/src/newsreader/news/apps.py b/src/newsreader/news/apps.py new file mode 100644 index 0000000..42c63ba --- /dev/null +++ b/src/newsreader/news/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class NewsConfig(AppConfig): + name = "news" diff --git a/src/newsreader/news/collection/base.py b/src/newsreader/news/collection/base.py index 519f4f8..7fcefff 100644 --- a/src/newsreader/news/collection/base.py +++ b/src/newsreader/news/collection/base.py @@ -23,7 +23,7 @@ class Client: stream = Stream def __init__(self, rules=None): - self.rules = rules if rules else CollectionRule.objects.all() + self.rules = rules if rules else CollectionRule.objects.filter(enabled=True) def __enter__(self): for rule in self.rules: diff --git a/src/newsreader/news/collection/feed.py b/src/newsreader/news/collection/feed.py index 46a7a3b..b14f375 100644 --- a/src/newsreader/news/collection/feed.py +++ b/src/newsreader/news/collection/feed.py @@ -1,6 +1,7 @@ import logging from concurrent.futures import ThreadPoolExecutor, as_completed +from datetime import timedelta from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist from django.db.models.fields import CharField, TextField @@ -184,6 +185,9 @@ class FeedCollector(Collector): class FeedDuplicateHandler: + duplicate_fields = ("url", "title", "body", "rule") + time_slot_minutes = 10 + def __init__(self, rule): self.queryset = rule.posts.all() @@ -199,39 +203,44 @@ class FeedDuplicateHandler: def check(self, instances): for instance in instances: if instance.remote_identifier in self.existing_identifiers: - existing_post = self.handle_duplicate(instance) + existing_post = self.handle_duplicate_identifier(instance) yield existing_post continue - elif not instance.remote_identifier and self.in_database(instance): - continue + elif self.in_database(instance): + existing_post = self.get_duplicate_in_database(instance) + + if self.in_time_slot(instance, existing_post): + yield self.update_existing_post(instance, existing_post) + continue yield instance def in_database(self, post): - values = { - "url": post.url, - "title": post.title, - "body": post.body, - "publication_date": post.publication_date, - } + values = {field: getattr(post, field, None) for field in self.duplicate_fields} - for existing_post in self.queryset.order_by("-publication_date")[:500]: + for existing_post in self.queryset.filter(**values): if self.is_duplicate(existing_post, values): return True + def in_time_slot(self, instance, existing_post): + time_delta_slot = timedelta(minutes=self.time_slot_minutes) + + time_difference = instance.publication_date - existing_post.publication_date + + if time_difference <= time_delta_slot: + return True + def is_duplicate(self, existing_post, values): - for key, value in values.items(): - existing_value = getattr(existing_post, key, None) - if existing_value != value: - return False + return all( + getattr(existing_post, field, None) == value + for field, value in values.items() + ) - return True - - def handle_duplicate(self, instance): + def handle_duplicate_identifier(self, instance): try: - existing_instance = self.queryset.get( + existing_post = self.queryset.get( remote_identifier=instance.remote_identifier ) except ObjectDoesNotExist: @@ -240,17 +249,43 @@ class FeedDuplicateHandler: ) return instance except MultipleObjectsReturned: - existing_instances = self.queryset.filter( + existing_posts = self.queryset.filter( remote_identifier=instance.remote_identifier ).order_by("-publication_date") - existing_instance = existing_instances.last() - existing_instances.exclude(pk=existing_instance.pk).delete() + existing_post = existing_posts.last() + existing_posts.exclude(pk=existing_post.pk).delete() + updated_post = self.update_existing_post(instance, existing_post) + + return updated_post + + def get_duplicate_in_database(self, instance): + query_values = { + field: getattr(instance, field, None) for field in self.duplicate_fields + } + + try: + existing_post = self.queryset.get(**query_values) + except ObjectDoesNotExist: + logger.error( + f"Duplicate handler tried retrieving post {instance.remote_identifier} but failed doing so." + ) + return instance + except MultipleObjectsReturned: + existing_posts = self.queryset.filter(**query_values).order_by( + "-publication_date" + ) + existing_post = existing_posts.last() + existing_posts.exclude(pk=existing_post.pk).delete() + + return existing_post + + def update_existing_post(self, instance, existing_post): for field in instance._meta.get_fields(): - getattr(existing_instance, field.name, object()) + getattr(existing_post, field.name, object()) new_value = getattr(instance, field.name, object()) if new_value and field.name != "id": - setattr(existing_instance, field.name, new_value) + setattr(existing_post, field.name, new_value) - return existing_instance + return existing_post diff --git a/src/newsreader/news/collection/forms.py b/src/newsreader/news/collection/forms.py index d0b02be..7e5fc97 100644 --- a/src/newsreader/news/collection/forms.py +++ b/src/newsreader/news/collection/forms.py @@ -1,4 +1,5 @@ from django import forms +from django.utils.translation import gettext_lazy as _ import pytz @@ -11,6 +12,7 @@ class CollectionRuleForm(forms.ModelForm): timezone = forms.ChoiceField( widget=forms.Select(attrs={"size": len(pytz.all_timezones)}), choices=((timezone, timezone) for timezone in pytz.all_timezones), + help_text=_("The timezone which the feed uses"), ) def __init__(self, *args, **kwargs): @@ -36,6 +38,17 @@ class CollectionRuleForm(forms.ModelForm): fields = ("name", "url", "timezone", "favicon", "category") +class CollectionRuleBulkForm(forms.Form): + rules = forms.ModelMultipleChoiceField(queryset=CollectionRule.objects.none()) + + def __init__(self, user, *args, **kwargs): + self.user = user + + super().__init__(*args, **kwargs) + + self.fields["rules"].queryset = CollectionRule.objects.filter(user=user) + + class OPMLImportForm(forms.Form): file = forms.FileField(allow_empty_file=False) skip_existing = forms.BooleanField(initial=False, required=False) diff --git a/src/newsreader/news/collection/migrations/0007_collectionrule_enabled.py b/src/newsreader/news/collection/migrations/0007_collectionrule_enabled.py new file mode 100644 index 0000000..fe6b0eb --- /dev/null +++ b/src/newsreader/news/collection/migrations/0007_collectionrule_enabled.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.5 on 2020-05-10 13:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [("collection", "0006_auto_20200412_1955")] + + operations = [ + migrations.AddField( + model_name="collectionrule", + name="enabled", + field=models.BooleanField( + default=True, help_text="Wether or not to collect items from this feed" + ), + ) + ] diff --git a/src/newsreader/news/collection/models.py b/src/newsreader/news/collection/models.py index d1d62ce..3ab9c0d 100644 --- a/src/newsreader/news/collection/models.py +++ b/src/newsreader/news/collection/models.py @@ -34,6 +34,9 @@ class CollectionRule(TimeStampedModel): last_suceeded = models.DateTimeField(blank=True, null=True) succeeded = models.BooleanField(default=False) error = models.CharField(max_length=1024, blank=True, null=True) + enabled = models.BooleanField( + default=True, help_text=_("Wether or not to collect items from this feed") + ) user = models.ForeignKey( "accounts.User", diff --git a/src/newsreader/news/collection/templates/collection/import.html b/src/newsreader/news/collection/templates/collection/import.html deleted file mode 100644 index ac8317d..0000000 --- a/src/newsreader/news/collection/templates/collection/import.html +++ /dev/null @@ -1,37 +0,0 @@ -{% extends "base.html" %} - -{% load static i18n %} - -{% block content %} -
-
- {% csrf_token %} - {{ form.non_field_errors }} - -
-

{% trans "Import an OPML file" %}

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

Create a rule

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

Update rule

-{% endblock %} - -{% block confirm-button %} - -{% endblock %} diff --git a/src/newsreader/news/collection/templates/collection/rule.html b/src/newsreader/news/collection/templates/collection/rule.html deleted file mode 100644 index 32aa370..0000000 --- a/src/newsreader/news/collection/templates/collection/rule.html +++ /dev/null @@ -1,55 +0,0 @@ -{% extends "base.html" %} - -{% load static %} - -{% block content %} -
-
- {% csrf_token %} - {{ form.non_field_errors }} - -
- {% block form-header %}{% endblock %} -
-
-
- - {{ form.name.errors }} - {{ form.name }} -
- -
- - {{ form.category.errors }} - {{ form.category }} -
- -
- - {{ form.url.errors }} - {{ form.url }} -
- -
- - {{ form.favicon.errors }} - {{ form.favicon }} -
- -
- - The timezone which the feed uses - {{ form.timezone.errors }} - {{ form.timezone }} -
-
- -
-
- Cancel - {% block confirm-button %}{% endblock %} -
-
-
-
-{% endblock %} diff --git a/src/newsreader/news/collection/templates/collection/rules.html b/src/newsreader/news/collection/templates/collection/rules.html deleted file mode 100644 index 508916a..0000000 --- a/src/newsreader/news/collection/templates/collection/rules.html +++ /dev/null @@ -1,30 +0,0 @@ -{% extends "base.html" %} - -{% load static %} - -{% block content %} -
-{% endblock %} - -{% block scripts %} - - - {{ block.super }} -{% endblock %} diff --git a/src/newsreader/news/collection/templates/news/collection/views/import.html b/src/newsreader/news/collection/templates/news/collection/views/import.html new file mode 100644 index 0000000..df19887 --- /dev/null +++ b/src/newsreader/news/collection/templates/news/collection/views/import.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} +{% load static %} + +{% block content %} +
+ {% url "news:collection:rules" as cancel_url %} + {% include "components/form/form.html" with form=form title="Import an OPML file" cancel_url=cancel_url confirm_text="Import rules" %} +
+{% endblock %} diff --git a/src/newsreader/news/collection/templates/news/collection/views/rule-create.html b/src/newsreader/news/collection/templates/news/collection/views/rule-create.html new file mode 100644 index 0000000..82ed6c5 --- /dev/null +++ b/src/newsreader/news/collection/templates/news/collection/views/rule-create.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} +{% load static %} + +{% block content %} +
+ {% url "news:collection:rules" as cancel_url %} + {% include "components/form/form.html" with form=form title="Create rule" cancel_url=cancel_url confirm_text="Create rule" %} +
+{% endblock %} diff --git a/src/newsreader/news/collection/templates/news/collection/views/rule-update.html b/src/newsreader/news/collection/templates/news/collection/views/rule-update.html new file mode 100644 index 0000000..3f0a8fe --- /dev/null +++ b/src/newsreader/news/collection/templates/news/collection/views/rule-update.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} +{% load static %} + +{% block content %} +
+ {% url "news:collection:rules" as cancel_url %} + {% include "components/form/form.html" with form=form title="Update rule" cancel_url=cancel_url confirm_text="Save rule" %} +
+{% endblock %} diff --git a/src/newsreader/news/collection/templates/news/collection/views/rules.html b/src/newsreader/news/collection/templates/news/collection/views/rules.html new file mode 100644 index 0000000..a17b818 --- /dev/null +++ b/src/newsreader/news/collection/templates/news/collection/views/rules.html @@ -0,0 +1,79 @@ +{% extends "base.html" %} +{% load i18n static %} + +{% block content %} +
+
+ {% csrf_token %} + +
+
+ + + +
+ + +
+ +
+ + + + + + + + + + + + + + {% for rule in rules %} + + + + + + + + + + {% endfor %} + +
+ + {% trans "Name" %}{% trans "Category" %}{% trans "URL" %}{% trans "Successfuly ran" %}{% trans "Enabled" %}
{{ rule.name }}{{ rule.category.name }}{{ rule.url }}{{ rule.succeeded }}{{ rule.enabled }} + +
+
+
+ + +
+{% endblock %} diff --git a/src/newsreader/news/collection/tests/endpoints/rule/detail/tests.py b/src/newsreader/news/collection/tests/endpoints/rule/detail/tests.py index 1c281d9..02f7334 100644 --- a/src/newsreader/news/collection/tests/endpoints/rule/detail/tests.py +++ b/src/newsreader/news/collection/tests/endpoints/rule/detail/tests.py @@ -17,7 +17,9 @@ class CollectionRuleDetailViewTestCase(TestCase): def test_simple(self): rule = CollectionRuleFactory(user=self.user) - response = self.client.get(reverse("api:rules-detail", args=[rule.pk])) + response = self.client.get( + reverse("api:news:collection:rules-detail", args=[rule.pk]) + ) data = response.json() self.assertEquals(response.status_code, 200) @@ -29,7 +31,9 @@ class CollectionRuleDetailViewTestCase(TestCase): self.assertTrue("category" in data) def test_not_known(self): - response = self.client.get(reverse("api:rules-detail", args=[100])) + response = self.client.get( + reverse("api:news:collection:rules-detail", args=[100]) + ) data = response.json() self.assertEquals(response.status_code, 404) @@ -38,7 +42,9 @@ class CollectionRuleDetailViewTestCase(TestCase): def test_post(self): rule = CollectionRuleFactory(user=self.user) - response = self.client.post(reverse("api:rules-detail", args=[rule.pk])) + response = self.client.post( + reverse("api:news:collection:rules-detail", args=[rule.pk]) + ) data = response.json() self.assertEquals(response.status_code, 405) @@ -48,7 +54,7 @@ class CollectionRuleDetailViewTestCase(TestCase): rule = CollectionRuleFactory(name="BBC", user=self.user) response = self.client.patch( - reverse("api:rules-detail", args=[rule.pk]), + reverse("api:news:collection:rules-detail", args=[rule.pk]), data=json.dumps({"name": "The guardian"}), content_type="application/json", ) @@ -64,7 +70,7 @@ class CollectionRuleDetailViewTestCase(TestCase): rule = CollectionRuleFactory(name="BBC", category=old_category, user=self.user) response = self.client.patch( - reverse("api:rules-detail", args=[rule.pk]), + reverse("api:news:collection:rules-detail", args=[rule.pk]), data=json.dumps({"category": absolute_url}), content_type="application/json", ) @@ -77,7 +83,7 @@ class CollectionRuleDetailViewTestCase(TestCase): rule = CollectionRuleFactory(user=self.user) response = self.client.patch( - reverse("api:rules-detail", args=[rule.pk]), + reverse("api:news:collection:rules-detail", args=[rule.pk]), data=json.dumps({"id": 44}), content_type="application/json", ) @@ -91,7 +97,7 @@ class CollectionRuleDetailViewTestCase(TestCase): category = CategoryFactory(user=self.user) response = self.client.patch( - reverse("api:rules-detail", args=[rule.pk]), + reverse("api:news:collection:rules-detail", args=[rule.pk]), data=json.dumps({"category": category.pk}), content_type="application/json", ) @@ -105,7 +111,7 @@ class CollectionRuleDetailViewTestCase(TestCase): rule = CollectionRuleFactory(name="BBC", user=self.user) response = self.client.put( - reverse("api:rules-detail", args=[rule.pk]), + reverse("api:news:collection:rules-detail", args=[rule.pk]), data=json.dumps({"name": "BBC", "url": "https://www.bbc.co.uk"}), content_type="application/json", ) @@ -117,7 +123,9 @@ class CollectionRuleDetailViewTestCase(TestCase): def test_delete(self): rule = CollectionRuleFactory(user=self.user) - response = self.client.delete(reverse("api:rules-detail", args=[rule.pk])) + response = self.client.delete( + reverse("api:news:collection:rules-detail", args=[rule.pk]) + ) self.assertEquals(response.status_code, 204) @@ -127,7 +135,7 @@ class CollectionRuleDetailViewTestCase(TestCase): rule = CollectionRuleFactory(name="BBC", user=self.user) response = self.client.patch( - reverse("api:rules-detail", args=[rule.pk]), + reverse("api:news:collection:rules-detail", args=[rule.pk]), data=json.dumps({"name": "The guardian"}), content_type="application/json", ) @@ -139,7 +147,7 @@ class CollectionRuleDetailViewTestCase(TestCase): rule = CollectionRuleFactory(name="BBC", user=other_user) response = self.client.patch( - reverse("api:rules-detail", args=[rule.pk]), + reverse("api:news:collection:rules-detail", args=[rule.pk]), data=json.dumps({"name": "The guardian"}), content_type="application/json", ) @@ -152,7 +160,9 @@ class CollectionRuleDetailViewTestCase(TestCase): 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])) + response = self.client.get( + reverse("api:news:collection:rules-detail", args=[rule.pk]) + ) data = response.json() self.assertEquals(response.status_code, 200) @@ -169,14 +179,18 @@ class CollectionRuleReadTestCase(TestCase): PostFactory.create_batch(size=20, read=False, rule=rule) - response = self.client.post(reverse("api:rules-read", args=[rule.pk])) + response = self.client.post( + reverse("api:news:collection: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])) + response = self.client.post( + reverse("api:news:collection:rules-read", args=[101]) + ) self.assertEquals(response.status_code, 404) @@ -187,7 +201,9 @@ class CollectionRuleReadTestCase(TestCase): PostFactory.create_batch(size=20, read=False, rule=rule) - response = self.client.post(reverse("api:rules-read", args=[rule.pk])) + response = self.client.post( + reverse("api:news:collection:rules-read", args=[rule.pk]) + ) self.assertEquals(response.status_code, 403) @@ -197,7 +213,9 @@ class CollectionRuleReadTestCase(TestCase): PostFactory.create_batch(size=20, read=False, rule=rule) - response = self.client.post(reverse("api:rules-read", args=[rule.pk])) + response = self.client.post( + reverse("api:news:collection:rules-read", args=[rule.pk]) + ) self.assertEquals(response.status_code, 403) self.assertEquals(Post.objects.filter(read=False).count(), 20) @@ -205,7 +223,9 @@ class CollectionRuleReadTestCase(TestCase): def test_get(self): rule = CollectionRuleFactory(user=self.user) - response = self.client.get(reverse("api:rules-read", args=[rule.pk])) + response = self.client.get( + reverse("api:news:collection:rules-read", args=[rule.pk]) + ) self.assertEquals(response.status_code, 405) @@ -213,7 +233,7 @@ class CollectionRuleReadTestCase(TestCase): rule = CollectionRuleFactory(name="BBC", user=self.user) response = self.client.patch( - reverse("api:rules-read", args=[rule.pk]), + reverse("api:news:collection:rules-read", args=[rule.pk]), data=json.dumps({"name": "Not possible"}), content_type="application/json", ) @@ -224,7 +244,7 @@ class CollectionRuleReadTestCase(TestCase): rule = CollectionRuleFactory(name="BBC", user=self.user) response = self.client.put( - reverse("api:rules-read", args=[rule.pk]), + reverse("api:news:collection:rules-read", args=[rule.pk]), data=json.dumps({"name": "Not possible"}), content_type="application/json", ) @@ -234,6 +254,8 @@ class CollectionRuleReadTestCase(TestCase): def test_delete(self): rule = CollectionRuleFactory(user=self.user) - response = self.client.delete(reverse("api:rules-read", args=[rule.pk])) + response = self.client.delete( + reverse("api:news:collection:rules-read", args=[rule.pk]) + ) self.assertEquals(response.status_code, 405) diff --git a/src/newsreader/news/collection/tests/endpoints/rule/list/tests.py b/src/newsreader/news/collection/tests/endpoints/rule/list/tests.py index 0e2a269..19d2029 100644 --- a/src/newsreader/news/collection/tests/endpoints/rule/list/tests.py +++ b/src/newsreader/news/collection/tests/endpoints/rule/list/tests.py @@ -20,7 +20,7 @@ class RuleListViewTestCase(TestCase): def test_simple(self): CollectionRuleFactory.create_batch(size=3, user=self.user) - response = self.client.get(reverse("api:rules-list")) + response = self.client.get(reverse("api:news:collection:rules-list")) data = response.json() self.assertEquals(response.status_code, 200) @@ -50,7 +50,7 @@ class RuleListViewTestCase(TestCase): ), ] - response = self.client.get(reverse("api:rules-list")) + response = self.client.get(reverse("api:news:collection:rules-list")) data = response.json() self.assertEquals(response.status_code, 200) @@ -65,7 +65,9 @@ class RuleListViewTestCase(TestCase): def test_pagination_count(self): CollectionRuleFactory.create_batch(size=80, user=self.user) - response = self.client.get(reverse("api:rules-list"), {"count": 30}) + response = self.client.get( + reverse("api:news:collection:rules-list"), {"count": 30} + ) data = response.json() self.assertEquals(response.status_code, 200) @@ -73,7 +75,7 @@ class RuleListViewTestCase(TestCase): self.assertEquals(len(data["results"]), 30) def test_empty(self): - response = self.client.get(reverse("api:rules-list")) + response = self.client.get(reverse("api:news:collection:rules-list")) data = response.json() self.assertEquals(response.status_code, 200) @@ -89,7 +91,7 @@ class RuleListViewTestCase(TestCase): data = {"name": "BBC", "url": "https://www.bbc.co.uk", "category": category.pk} response = self.client.post( - reverse("api:rules-list"), + reverse("api:news:collection:rules-list"), data=json.dumps(data), content_type="application/json", ) @@ -99,21 +101,21 @@ class RuleListViewTestCase(TestCase): self.assertEquals(data["detail"], 'Method "POST" not allowed.') def test_patch(self): - response = self.client.patch(reverse("api:rules-list")) + response = self.client.patch(reverse("api:news:collection: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")) + response = self.client.put(reverse("api:news:collection: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")) + response = self.client.delete(reverse("api:news:collection:rules-list")) data = response.json() self.assertEquals(response.status_code, 405) @@ -124,7 +126,7 @@ class RuleListViewTestCase(TestCase): CollectionRuleFactory.create_batch(size=3, user=self.user) - response = self.client.get(reverse("api:rules-list")) + response = self.client.get(reverse("api:news:collection:rules-list")) self.assertEquals(response.status_code, 403) @@ -132,7 +134,7 @@ class RuleListViewTestCase(TestCase): other_user = UserFactory() CollectionRuleFactory.create_batch(size=3, user=other_user) - response = self.client.get(reverse("api:rules-list")) + response = self.client.get(reverse("api:news:collection:rules-list")) data = response.json() self.assertEquals(response.status_code, 200) @@ -151,7 +153,7 @@ class NestedRuleListViewTestCase(TestCase): PostFactory.create_batch(size=5, rule=rule) response = self.client.get( - reverse("api:rules-nested-posts", kwargs={"pk": rule.pk}) + reverse("api:news:collection:rules-nested-posts", kwargs={"pk": rule.pk}) ) data = response.json() @@ -166,7 +168,8 @@ class NestedRuleListViewTestCase(TestCase): PostFactory.create_batch(size=80, rule=rule) response = self.client.get( - reverse("api:rules-nested-posts", kwargs={"pk": rule.pk}), {"count": 30} + reverse("api:news:collection:rules-nested-posts", kwargs={"pk": rule.pk}), + {"count": 30}, ) data = response.json() @@ -178,7 +181,7 @@ class NestedRuleListViewTestCase(TestCase): rule = CollectionRuleFactory.create(user=self.user) response = self.client.get( - reverse("api:rules-nested-posts", kwargs={"pk": rule.pk}) + reverse("api:news:collection:rules-nested-posts", kwargs={"pk": rule.pk}) ) data = response.json() @@ -187,7 +190,9 @@ class NestedRuleListViewTestCase(TestCase): self.assertEquals(len(data["results"]), 0) def test_not_known(self): - response = self.client.get(reverse("api:rules-nested-posts", kwargs={"pk": 0})) + response = self.client.get( + reverse("api:news:collection:rules-nested-posts", kwargs={"pk": 0}) + ) self.assertEquals(response.status_code, 404) @@ -195,7 +200,7 @@ class NestedRuleListViewTestCase(TestCase): rule = CollectionRuleFactory.create(user=self.user) response = self.client.post( - reverse("api:rules-nested-posts", kwargs={"pk": rule.pk}), + reverse("api:news:collection:rules-nested-posts", kwargs={"pk": rule.pk}), data=json.dumps({}), content_type="application/json", ) @@ -208,7 +213,7 @@ class NestedRuleListViewTestCase(TestCase): rule = CollectionRuleFactory.create(user=self.user) response = self.client.patch( - reverse("api:rules-nested-posts", kwargs={"pk": rule.pk}), + reverse("api:news:collection:rules-nested-posts", kwargs={"pk": rule.pk}), data=json.dumps({}), content_type="application/json", ) @@ -221,7 +226,7 @@ class NestedRuleListViewTestCase(TestCase): rule = CollectionRuleFactory.create(user=self.user) response = self.client.put( - reverse("api:rules-nested-posts", kwargs={"pk": rule.pk}), + reverse("api:news:collection:rules-nested-posts", kwargs={"pk": rule.pk}), data=json.dumps({}), content_type="application/json", ) @@ -234,7 +239,7 @@ class NestedRuleListViewTestCase(TestCase): rule = CollectionRuleFactory.create(user=self.user) response = self.client.delete( - reverse("api:rules-nested-posts", kwargs={"pk": rule.pk}), + reverse("api:news:collection:rules-nested-posts", kwargs={"pk": rule.pk}), data=json.dumps({}), content_type="application/json", ) @@ -249,7 +254,7 @@ class NestedRuleListViewTestCase(TestCase): rule = CollectionRuleFactory(user=self.user) response = self.client.get( - reverse("api:rules-nested-posts", kwargs={"pk": rule.pk}) + reverse("api:news:collection:rules-nested-posts", kwargs={"pk": rule.pk}) ) self.assertEquals(response.status_code, 403) @@ -259,7 +264,7 @@ class NestedRuleListViewTestCase(TestCase): rule = CollectionRuleFactory(user=other_user) response = self.client.get( - reverse("api:rules-nested-posts", kwargs={"pk": rule.pk}) + reverse("api:news:collection:rules-nested-posts", kwargs={"pk": rule.pk}) ) self.assertEquals(response.status_code, 403) @@ -294,7 +299,7 @@ class NestedRuleListViewTestCase(TestCase): ] response = self.client.get( - reverse("api:rules-nested-posts", kwargs={"pk": rule.pk}) + reverse("api:news:collection:rules-nested-posts", kwargs={"pk": rule.pk}) ) data = response.json() @@ -315,7 +320,7 @@ class NestedRuleListViewTestCase(TestCase): PostFactory.create_batch(size=5, rule=other_rule) response = self.client.get( - reverse("api:rules-nested-posts", kwargs={"pk": rule.pk}) + reverse("api:news:collection:rules-nested-posts", kwargs={"pk": rule.pk}) ) data = response.json() @@ -335,7 +340,8 @@ class NestedRuleListViewTestCase(TestCase): PostFactory.create_batch(size=10, rule=rule, read=True) response = self.client.get( - reverse("api:rules-nested-posts", kwargs={"pk": rule.pk}), {"read": "false"} + reverse("api:news:collection:rules-nested-posts", kwargs={"pk": rule.pk}), + {"read": "false"}, ) data = response.json() @@ -354,7 +360,8 @@ class NestedRuleListViewTestCase(TestCase): PostFactory.create_batch(size=10, rule=rule, read=True) response = self.client.get( - reverse("api:rules-nested-posts", kwargs={"pk": rule.pk}), {"read": "true"} + reverse("api:news:collection:rules-nested-posts", kwargs={"pk": rule.pk}), + {"read": "true"}, ) data = response.json() diff --git a/src/newsreader/news/collection/tests/feed/collector/tests.py b/src/newsreader/news/collection/tests/feed/collector/tests.py index e9ae98a..88f2875 100644 --- a/src/newsreader/news/collection/tests/feed/collector/tests.py +++ b/src/newsreader/news/collection/tests/feed/collector/tests.py @@ -245,3 +245,26 @@ class FeedCollectorTestCase(TestCase): self.assertEquals( third_post.title, "Birmingham head teacher threatened over LGBT lessons" ) + + @freeze_time("2019-02-22 12:30:00") + def test_disabled_rules(self): + rules = ( + CollectionRuleFactory(enabled=False), + CollectionRuleFactory(enabled=True), + ) + + self.mocked_parse.return_value = multiple_mock + + collector = FeedCollector() + collector.collect() + + for rule in rules: + rule.refresh_from_db() + + self.assertEquals(Post.objects.count(), 3) + self.assertEquals(rules[1].succeeded, True) + self.assertEquals(rules[1].last_suceeded, timezone.now()) + self.assertEquals(rules[1].error, None) + + self.assertEquals(rules[0].last_suceeded, None) + self.assertEquals(rules[0].succeeded, False) 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 b794f3e..005771a 100644 --- a/src/newsreader/news/collection/tests/feed/duplicate_handler/tests.py +++ b/src/newsreader/news/collection/tests/feed/duplicate_handler/tests.py @@ -1,8 +1,13 @@ +from datetime import timedelta + from django.test import TestCase from django.utils import timezone +from freezegun import freeze_time + from newsreader.news.collection.feed import FeedDuplicateHandler from newsreader.news.collection.tests.factories import CollectionRuleFactory +from newsreader.news.core.models import Post from newsreader.news.core.tests.factories import PostFactory @@ -25,16 +30,57 @@ class FeedDuplicateHandlerTestCase(TestCase): posts_gen = duplicate_handler.check([new_post]) posts = list(posts_gen) + self.assertEquals(len(posts), 1) + post = posts[0] + existing_post.refresh_from_db() + + self.assertEquals(existing_post.pk, post.pk) + self.assertEquals(post.publication_date, new_post.publication_date) + self.assertEquals(post.title, new_post.title) + self.assertEquals(post.body, new_post.body) + self.assertEquals(post.rule, new_post.rule) + self.assertEquals(post.read, False) + + @freeze_time("2019-10-30 12:30:00") + def test_duplicate_entries_with_different_remote_identifiers(self): + rule = CollectionRuleFactory() + publication_date = timezone.now() + + existing_post = PostFactory.create( + remote_identifier="28f79ae4-8f9a-11e9-b143-00163ef6bee7", + url="https://bbc.com", + title="New post", + body="Body", + publication_date=publication_date, + rule=rule, + ) + new_post = PostFactory.build( + remote_identifier="28f79ae4-8f9a-11e9-b143-00163ef6bee7Q", + url="https://bbc.com", + title="New post", + body="Body", + publication_date=publication_date, + rule=rule, + ) + + with FeedDuplicateHandler(rule) as duplicate_handler: + posts_gen = duplicate_handler.check([new_post]) + posts = list(posts_gen) self.assertEquals(len(posts), 1) + + existing_post.refresh_from_db() + post = posts[0] + + self.assertEquals(existing_post.pk, post.pk) + self.assertEquals(post.title, new_post.title) + self.assertEquals(post.body, new_post.body) + self.assertEquals(post.rule, new_post.rule) 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) + self.assertEquals(post.read, False) def test_duplicate_entries_in_recent_database(self): - PostFactory.create_batch(size=10) - publication_date = timezone.now() rule = CollectionRuleFactory() @@ -43,7 +89,7 @@ class FeedDuplicateHandlerTestCase(TestCase): 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, + remote_identifier="jabbadabadoe", rule=rule, ) new_post = PostFactory.build( @@ -59,10 +105,19 @@ class FeedDuplicateHandlerTestCase(TestCase): posts_gen = duplicate_handler.check([new_post]) posts = list(posts_gen) - self.assertEquals(len(posts), 0) + self.assertEquals(len(posts), 1) + + existing_post.refresh_from_db() + post = posts[0] + + self.assertEquals(existing_post.pk, post.pk) + self.assertEquals(post.title, new_post.title) + self.assertEquals(post.body, new_post.body) + self.assertEquals(post.rule, new_post.rule) + self.assertEquals(post.publication_date, new_post.publication_date) + self.assertEquals(post.read, False) def test_multiple_existing_entries_with_identifier(self): - timezone.now() rule = CollectionRuleFactory() PostFactory.create_batch( @@ -80,4 +135,56 @@ class FeedDuplicateHandlerTestCase(TestCase): posts = list(posts_gen) self.assertEquals(len(posts), 1) - self.assertEquals(posts[0].title, new_post.title) + + self.assertEquals( + Post.objects.filter( + remote_identifier="28f79ae4-8f9a-11e9-b143-00163ef6bee7" + ).count(), + 1, + ) + + post = posts[0] + + self.assertEquals(post.title, new_post.title) + self.assertEquals(post.body, new_post.body) + self.assertEquals(post.publication_date, new_post.publication_date) + self.assertEquals(post.rule, new_post.rule) + self.assertEquals(post.read, False) + + @freeze_time("2019-10-30 12:30:00") + def test_duplicate_entries_outside_time_slot(self): + 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="jabbadabadoe", + 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 + timedelta(minutes=12), + 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), 1) + + existing_post.refresh_from_db() + post = posts[0] + + self.assertEquals(post.pk, None) + self.assertEquals(post.title, new_post.title) + self.assertEquals(post.body, new_post.body) + self.assertEquals(post.rule, new_post.rule) + self.assertEquals(post.publication_date, new_post.publication_date) + self.assertEquals(post.read, False) diff --git a/src/newsreader/news/collection/tests/views/test_bulk_views.py b/src/newsreader/news/collection/tests/views/test_bulk_views.py new file mode 100644 index 0000000..39817c2 --- /dev/null +++ b/src/newsreader/news/collection/tests/views/test_bulk_views.py @@ -0,0 +1,242 @@ +from django.test import TestCase +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ + +from newsreader.accounts.tests.factories import UserFactory +from newsreader.news.collection.models import CollectionRule +from newsreader.news.collection.tests.factories import CollectionRuleFactory + + +class CollectionRuleBulkViewTestCase: + def setUp(self): + self.redirect_url = reverse("news:collection:rules") + + self.user = UserFactory() + self.client.force_login(self.user) + + +class CollectionRuleBulkEnableViewTestCase(CollectionRuleBulkViewTestCase, TestCase): + def setUp(self): + super().setUp() + + self.url = reverse("news:collection:rules-enable") + + self.rules = CollectionRuleFactory.create_batch( + size=5, user=self.user, enabled=False + ) + + def test_simple(self): + response = self.client.post( + self.url, {"rules": [rule.pk for rule in self.rules]}, follow=True + ) + + self.assertRedirects(response, self.redirect_url) + + rules = CollectionRule.objects.filter(user=self.user) + + for rule in rules: + with self.subTest(rule=rule): + self.assertEqual(rule.enabled, True) + + self.assertNotContains(response, _("The form contains errors, try again later")) + + def test_empty_rules(self): + response = self.client.post(self.url, {"rules": []}, follow=True) + + self.assertRedirects(response, self.redirect_url) + + rules = CollectionRule.objects.filter(user=self.user) + + for rule in rules: + with self.subTest(rule=rule): + self.assertEqual(rule.enabled, False) + + self.assertContains(response, _("The form contains errors, try again later")) + + def test_rule_from_other_user(self): + other_user = UserFactory() + other_rules = CollectionRuleFactory.create_batch( + size=5, user=other_user, enabled=False + ) + + response = self.client.post( + self.url, + {"rules": [other_rule.pk for other_rule in other_rules]}, + follow=True, + ) + + self.assertRedirects(response, self.redirect_url) + + rules = CollectionRule.objects.filter(user=other_user) + + for rule in rules: + with self.subTest(rule=rule): + self.assertEqual(rule.enabled, False) + + self.assertContains(response, _("The form contains errors, try again later")) + + def test_unauthenticated(self): + self.client.logout() + + response = self.client.post( + self.url, {"rules": [rule.pk for rule in self.rules]}, follow=True + ) + + self.assertRedirects( + response, + f"{reverse('accounts:login')}?next={reverse('news:collection:rules-enable')}", + ) + + rules = CollectionRule.objects.filter(user=self.user) + + for rule in rules: + with self.subTest(rule=rule): + self.assertEqual(rule.enabled, False) + + +class CollectionRuleBulkDisableViewTestCase(CollectionRuleBulkViewTestCase, TestCase): + def setUp(self): + super().setUp() + + self.url = reverse("news:collection:rules-disable") + + self.rules = CollectionRuleFactory.create_batch( + size=5, user=self.user, enabled=True + ) + + def test_simple(self): + response = self.client.post( + self.url, {"rules": [rule.pk for rule in self.rules]}, follow=True + ) + + self.assertRedirects(response, self.redirect_url) + + rules = CollectionRule.objects.filter(user=self.user) + + for rule in rules: + with self.subTest(rule=rule): + self.assertEqual(rule.enabled, False) + + self.assertNotContains(response, _("The form contains errors, try again later")) + + def test_empty_rules(self): + response = self.client.post(self.url, {"rules": []}, follow=True) + + self.assertRedirects(response, self.redirect_url) + + rules = CollectionRule.objects.filter(user=self.user) + + for rule in rules: + with self.subTest(rule=rule): + self.assertEqual(rule.enabled, True) + + self.assertContains(response, _("The form contains errors, try again later")) + + def test_rule_from_other_user(self): + other_user = UserFactory() + other_rules = CollectionRuleFactory.create_batch( + size=5, user=other_user, enabled=True + ) + + response = self.client.post( + self.url, + {"rules": [other_rule.pk for other_rule in other_rules]}, + follow=True, + ) + + self.assertRedirects(response, self.redirect_url) + + rules = CollectionRule.objects.filter(user=other_user) + + for rule in rules: + with self.subTest(rule=rule): + self.assertEqual(rule.enabled, True) + + self.assertContains(response, _("The form contains errors, try again later")) + + def test_unauthenticated(self): + self.client.logout() + + response = self.client.post( + self.url, {"rules": [rule.pk for rule in self.rules]}, follow=True + ) + + self.assertRedirects( + response, + f"{reverse('accounts:login')}?next={reverse('news:collection:rules-disable')}", + ) + + rules = CollectionRule.objects.filter(user=self.user) + + for rule in rules: + with self.subTest(rule=rule): + self.assertEqual(rule.enabled, True) + + +class CollectionRuleBulkDeleteViewTestCase(CollectionRuleBulkViewTestCase, TestCase): + def setUp(self): + super().setUp() + + self.url = reverse("news:collection:rules-delete") + + self.rules = CollectionRuleFactory.create_batch(size=5, user=self.user) + + def test_simple(self): + response = self.client.post( + self.url, {"rules": [rule.pk for rule in self.rules]}, follow=True + ) + + self.assertRedirects(response, self.redirect_url) + + rules = CollectionRule.objects.filter(user=self.user) + + self.assertCountEqual(rules, []) + + self.assertNotContains(response, _("The form contains errors, try again later")) + + def test_empty_rules(self): + response = self.client.post(self.url, {"rules": []}, follow=True) + + self.assertRedirects(response, self.redirect_url) + + rules = CollectionRule.objects.filter(user=self.user) + + self.assertCountEqual(rules, self.rules) + + self.assertContains(response, _("The form contains errors, try again later")) + + def test_rule_from_other_user(self): + other_user = UserFactory() + other_rules = CollectionRuleFactory.create_batch( + size=5, user=other_user, enabled=True + ) + + response = self.client.post( + self.url, + {"rules": [other_rule.pk for other_rule in other_rules]}, + follow=True, + ) + + self.assertRedirects(response, self.redirect_url) + + rules = CollectionRule.objects.filter(user=other_user) + + self.assertCountEqual(rules, other_rules) + + self.assertContains(response, _("The form contains errors, try again later")) + + def test_unauthenticated(self): + self.client.logout() + + response = self.client.post( + self.url, {"rules": [rule.pk for rule in self.rules]}, follow=True + ) + + self.assertRedirects( + response, + f"{reverse('accounts:login')}?next={reverse('news:collection:rules-delete')}", + ) + + rules = CollectionRule.objects.filter(user=self.user) + + self.assertCountEqual(rules, self.rules) diff --git a/src/newsreader/news/collection/tests/test_views.py b/src/newsreader/news/collection/tests/views/test_crud.py similarity index 51% rename from src/newsreader/news/collection/tests/test_views.py rename to src/newsreader/news/collection/tests/views/test_crud.py index 0acd3ed..a581f0c 100644 --- a/src/newsreader/news/collection/tests/test_views.py +++ b/src/newsreader/news/collection/tests/views/test_crud.py @@ -1,9 +1,5 @@ -import os - -from django.conf import settings from django.test import TestCase from django.urls import reverse -from django.utils.translation import gettext_lazy as _ import pytz @@ -150,135 +146,3 @@ class CollectionRuleUpdateViewTestCase(CollectionRuleViewTestCase, TestCase): self.rule.refresh_from_db() self.assertEquals(self.rule.category, None) - - -class OPMLImportTestCase(TestCase): - def setUp(self): - self.user = UserFactory(password="test") - self.client.force_login(self.user) - - self.form_data = {"file": "", "skip_existing": False} - self.url = reverse("import") - - def _get_file_path(self, name): - file_dir = os.path.join(settings.DJANGO_PROJECT_DIR, "utils", "tests", "files") - return os.path.join(file_dir, name) - - def test_simple(self): - file_path = self._get_file_path("feeds.opml") - - with open(file_path) as file: - self.form_data.update(file=file) - - response = self.client.post(self.url, self.form_data) - - self.assertRedirects(response, reverse("rules")) - - rules = CollectionRule.objects.all() - self.assertEquals(len(rules), 4) - - def test_existing_rules(self): - CollectionRuleFactory( - url="http://www.engadget.com/rss-full.xml", user=self.user - ) - CollectionRuleFactory(url="https://news.ycombinator.com/rss", user=self.user) - CollectionRuleFactory( - url="http://feeds.feedburner.com/Techcrunch", user=self.user - ) - CollectionRuleFactory( - url="http://feeds.feedburner.com/tweakers/nieuws", user=self.user - ) - - file_path = self._get_file_path("feeds.opml") - - with open(file_path) as file: - self.form_data.update(file=file) - - response = self.client.post(self.url, self.form_data) - - self.assertRedirects(response, reverse("rules")) - - rules = CollectionRule.objects.all() - self.assertEquals(len(rules), 8) - - def test_skip_existing_rules(self): - CollectionRuleFactory( - url="http://www.engadget.com/rss-full.xml", user=self.user - ) - CollectionRuleFactory(url="https://news.ycombinator.com/rss", user=self.user) - CollectionRuleFactory( - url="http://feeds.feedburner.com/Techcrunch", user=self.user - ) - CollectionRuleFactory( - url="http://feeds.feedburner.com/tweakers/nieuws", user=self.user - ) - - file_path = self._get_file_path("feeds.opml") - - with open(file_path) as file: - self.form_data.update(file=file, skip_existing=True) - - response = self.client.post(self.url, self.form_data) - - self.assertEquals(response.status_code, 200) - - rules = CollectionRule.objects.all() - self.assertEquals(len(rules), 4) - - def test_empty_feed_file(self): - file_path = self._get_file_path("empty-feeds.opml") - - with open(file_path) as file: - self.form_data.update(file=file) - - response = self.client.post(self.url, self.form_data) - - self.assertEquals(response.status_code, 200) - - rules = CollectionRule.objects.all() - self.assertEquals(len(rules), 0) - - self.assertFormError(response, "form", "file", _("No (new) rules found")) - - def test_invalid_feeds(self): - file_path = self._get_file_path("invalid-url-feeds.opml") - - with open(file_path) as file: - self.form_data.update(file=file) - - response = self.client.post(self.url, self.form_data) - - self.assertEquals(response.status_code, 200) - - rules = CollectionRule.objects.all() - - self.assertEquals(len(rules), 0) - self.assertFormError(response, "form", "file", _("No (new) rules found")) - - def test_invalid_file(self): - file_path = self._get_file_path("test.png") - - with open(file_path, "rb") as file: - self.form_data.update(file=file) - - response = self.client.post(self.url, self.form_data) - - self.assertEquals(response.status_code, 200) - - rules = CollectionRule.objects.all() - self.assertEquals(len(rules), 0) - - self.assertFormError(response, "form", "file", _("Invalid OPML file")) - - def test_feeds_with_missing_attr(self): - file_path = self._get_file_path("missing-feeds.opml") - - with open(file_path) as file: - self.form_data.update(file=file) - - response = self.client.post(self.url, self.form_data) - - self.assertRedirects(response, reverse("rules")) - - rules = CollectionRule.objects.all() - self.assertEquals(len(rules), 2) diff --git a/src/newsreader/news/collection/tests/views/test_import_view.py b/src/newsreader/news/collection/tests/views/test_import_view.py new file mode 100644 index 0000000..776e4c6 --- /dev/null +++ b/src/newsreader/news/collection/tests/views/test_import_view.py @@ -0,0 +1,142 @@ +import os + +from django.conf import settings +from django.test import TestCase +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ + +from newsreader.accounts.tests.factories import UserFactory +from newsreader.news.collection.models import CollectionRule +from newsreader.news.collection.tests.factories import CollectionRuleFactory + + +class OPMLImportTestCase(TestCase): + def setUp(self): + self.user = UserFactory(password="test") + self.client.force_login(self.user) + + self.form_data = {"file": "", "skip_existing": False} + self.url = reverse("import") + + def _get_file_path(self, name): + file_dir = os.path.join(settings.DJANGO_PROJECT_DIR, "utils", "tests", "files") + return os.path.join(file_dir, name) + + def test_simple(self): + file_path = self._get_file_path("feeds.opml") + + with open(file_path) as file: + self.form_data.update(file=file) + + response = self.client.post(self.url, self.form_data) + + self.assertRedirects(response, reverse("rules")) + + rules = CollectionRule.objects.all() + self.assertEquals(len(rules), 4) + + def test_existing_rules(self): + CollectionRuleFactory( + url="http://www.engadget.com/rss-full.xml", user=self.user + ) + CollectionRuleFactory(url="https://news.ycombinator.com/rss", user=self.user) + CollectionRuleFactory( + url="http://feeds.feedburner.com/Techcrunch", user=self.user + ) + CollectionRuleFactory( + url="http://feeds.feedburner.com/tweakers/nieuws", user=self.user + ) + + file_path = self._get_file_path("feeds.opml") + + with open(file_path) as file: + self.form_data.update(file=file) + + response = self.client.post(self.url, self.form_data) + + self.assertRedirects(response, reverse("rules")) + + rules = CollectionRule.objects.all() + self.assertEquals(len(rules), 8) + + def test_skip_existing_rules(self): + CollectionRuleFactory( + url="http://www.engadget.com/rss-full.xml", user=self.user + ) + CollectionRuleFactory(url="https://news.ycombinator.com/rss", user=self.user) + CollectionRuleFactory( + url="http://feeds.feedburner.com/Techcrunch", user=self.user + ) + CollectionRuleFactory( + url="http://feeds.feedburner.com/tweakers/nieuws", user=self.user + ) + + file_path = self._get_file_path("feeds.opml") + + with open(file_path) as file: + self.form_data.update(file=file, skip_existing=True) + + response = self.client.post(self.url, self.form_data) + + self.assertEquals(response.status_code, 200) + + rules = CollectionRule.objects.all() + self.assertEquals(len(rules), 4) + + def test_empty_feed_file(self): + file_path = self._get_file_path("empty-feeds.opml") + + with open(file_path) as file: + self.form_data.update(file=file) + + response = self.client.post(self.url, self.form_data) + + self.assertEquals(response.status_code, 200) + + rules = CollectionRule.objects.all() + self.assertEquals(len(rules), 0) + + self.assertFormError(response, "form", "file", _("No (new) rules found")) + + def test_invalid_feeds(self): + file_path = self._get_file_path("invalid-url-feeds.opml") + + with open(file_path) as file: + self.form_data.update(file=file) + + response = self.client.post(self.url, self.form_data) + + self.assertEquals(response.status_code, 200) + + rules = CollectionRule.objects.all() + + self.assertEquals(len(rules), 0) + self.assertFormError(response, "form", "file", _("No (new) rules found")) + + def test_invalid_file(self): + file_path = self._get_file_path("test.png") + + with open(file_path, "rb") as file: + self.form_data.update(file=file) + + response = self.client.post(self.url, self.form_data) + + self.assertEquals(response.status_code, 200) + + rules = CollectionRule.objects.all() + self.assertEquals(len(rules), 0) + + self.assertFormError(response, "form", "file", _("Invalid OPML file")) + + def test_feeds_with_missing_attr(self): + file_path = self._get_file_path("missing-feeds.opml") + + with open(file_path) as file: + self.form_data.update(file=file) + + response = self.client.post(self.url, self.form_data) + + self.assertRedirects(response, reverse("rules")) + + rules = CollectionRule.objects.all() + self.assertEquals(len(rules), 2) diff --git a/src/newsreader/news/collection/urls.py b/src/newsreader/news/collection/urls.py index 28b6f38..1ea17d6 100644 --- a/src/newsreader/news/collection/urls.py +++ b/src/newsreader/news/collection/urls.py @@ -8,6 +8,9 @@ from newsreader.news.collection.endpoints import ( RuleReadView, ) from newsreader.news.collection.views import ( + CollectionRuleBulkDeleteView, + CollectionRuleBulkDisableView, + CollectionRuleBulkEnableView, CollectionRuleCreateView, CollectionRuleListView, CollectionRuleUpdateView, @@ -34,5 +37,20 @@ urlpatterns = [ login_required(CollectionRuleCreateView.as_view()), name="rule-create", ), + path( + "rules/delete/", + login_required(CollectionRuleBulkDeleteView.as_view()), + name="rules-delete", + ), + path( + "rules/enable/", + login_required(CollectionRuleBulkEnableView.as_view()), + name="rules-enable", + ), + path( + "rules/disable/", + login_required(CollectionRuleBulkDisableView.as_view()), + name="rules-disable", + ), path("rules/import/", login_required(OPMLImportView.as_view()), name="import"), ] diff --git a/src/newsreader/news/collection/utils.py b/src/newsreader/news/collection/utils.py index 0aa096f..fd6ab0a 100644 --- a/src/newsreader/news/collection/utils.py +++ b/src/newsreader/news/collection/utils.py @@ -2,6 +2,7 @@ from datetime import datetime from django.utils import timezone +import pytz import requests from requests.exceptions import RequestException @@ -15,7 +16,8 @@ def build_publication_date(dt, tz): published_parsed = timezone.make_aware(naive_datetime, timezone=tz) except (TypeError, ValueError): return None, False - return published_parsed, True + + return published_parsed.astimezone(pytz.utc), True def fetch(url): diff --git a/src/newsreader/news/collection/views.py b/src/newsreader/news/collection/views.py index 8d254e2..6fb88df 100644 --- a/src/newsreader/news/collection/views.py +++ b/src/newsreader/news/collection/views.py @@ -1,12 +1,17 @@ from django.contrib import messages -from django.urls import reverse_lazy +from django.shortcuts import redirect +from django.urls import reverse, reverse_lazy from django.utils.translation import gettext_lazy as _ from django.views.generic.edit import CreateView, FormView, UpdateView from django.views.generic.list import ListView import pytz -from newsreader.news.collection.forms import CollectionRuleForm, OPMLImportForm +from newsreader.news.collection.forms import ( + CollectionRuleBulkForm, + CollectionRuleForm, + OPMLImportForm, +) from newsreader.news.collection.models import CollectionRule from newsreader.news.core.models import Category from newsreader.utils.opml import parse_opml @@ -17,11 +22,11 @@ class CollectionRuleViewMixin: def get_queryset(self): user = self.request.user - return self.queryset.filter(user=user) + return self.queryset.filter(user=user).order_by("name") class CollectionRuleDetailMixin: - success_url = reverse_lazy("rules") + success_url = reverse_lazy("news:collection:rules") form_class = CollectionRuleForm def get_context_data(self, **kwargs): @@ -42,27 +47,82 @@ class CollectionRuleDetailMixin: class CollectionRuleListView(CollectionRuleViewMixin, ListView): - template_name = "collection/rules.html" + paginate_by = 50 + template_name = "news/collection/views/rules.html" context_object_name = "rules" class CollectionRuleUpdateView( CollectionRuleViewMixin, CollectionRuleDetailMixin, UpdateView ): - template_name = "collection/rule-update.html" + template_name = "news/collection/views/rule-update.html" context_object_name = "rule" class CollectionRuleCreateView( CollectionRuleViewMixin, CollectionRuleDetailMixin, CreateView ): - template_name = "collection/rule-create.html" + template_name = "news/collection/views/rule-create.html" + + +class CollectionRuleBulkView(FormView): + form_class = CollectionRuleBulkForm + + def get_redirect_url(self): + return reverse("news:collection:rules") + + def get_success_url(self): + return self.get_redirect_url() + + def get_form(self, form_class=None): + if form_class is None: + form_class = self.get_form_class() + return form_class(self.request.user, **self.get_form_kwargs()) + + def form_invalid(self, form): + url = self.get_redirect_url() + + messages.error(self.request, _("The form contains errors, try again later")) + + return redirect(url) + + +class CollectionRuleBulkEnableView(CollectionRuleBulkView): + def form_valid(self, form): + response = super().form_valid(form) + + for rule in form.cleaned_data["rules"]: + rule.enabled = True + rule.save() + + return response + + +class CollectionRuleBulkDisableView(CollectionRuleBulkView): + def form_valid(self, form): + response = super().form_valid(form) + + for rule in form.cleaned_data["rules"]: + rule.enabled = False + rule.save() + + return response + + +class CollectionRuleBulkDeleteView(CollectionRuleBulkView): + def form_valid(self, form): + response = super().form_valid(form) + + for rule in form.cleaned_data["rules"]: + rule.delete() + + return response class OPMLImportView(FormView): form_class = OPMLImportForm - success_url = reverse_lazy("rules") - template_name = "collection/import.html" + success_url = reverse_lazy("news:collection:rules") + template_name = "news/collection/views/import.html" def form_valid(self, form): user = self.request.user diff --git a/src/newsreader/news/core/forms.py b/src/newsreader/news/core/forms.py index a86e2b2..a08022a 100644 --- a/src/newsreader/news/core/forms.py +++ b/src/newsreader/news/core/forms.py @@ -1,15 +1,27 @@ from django import forms +from django.forms.widgets import CheckboxSelectMultiple from newsreader.accounts.models import User from newsreader.news.collection.models import CollectionRule from newsreader.news.core.models import Category +class RulesWidget(CheckboxSelectMultiple): + template_name = "news/core/widgets/rules.html" + option_template_name = "news/core/widgets/rule.html" + + def create_option(self, *args, **kwargs): + option = super().create_option(*args, **kwargs) + instance = self.choices.queryset.get(pk=option["value"]) + + if self.category and instance.category: + option["selected"] = self.category.pk == instance.category.pk + return {**option, "instance": instance} + + class CategoryForm(forms.ModelForm): rules = forms.ModelMultipleChoiceField( - required=False, - queryset=CollectionRule.objects.all(), - widget=forms.widgets.CheckboxSelectMultiple, + required=False, queryset=CollectionRule.objects.none(), widget=RulesWidget ) user = forms.ModelChoiceField( @@ -23,6 +35,8 @@ class CategoryForm(forms.ModelForm): super().__init__(*args, **kwargs) self.fields["rules"].queryset = CollectionRule.objects.filter(user=self.user) + self.fields["rules"].widget.category = self.instance + self.fields["user"].queryset = User.objects.filter(pk=self.user.pk) self.initial["user"] = self.user diff --git a/src/newsreader/news/core/templates/core/category-create.html b/src/newsreader/news/core/templates/core/category-create.html deleted file mode 100644 index 73d05b5..0000000 --- a/src/newsreader/news/core/templates/core/category-create.html +++ /dev/null @@ -1,9 +0,0 @@ -{% extends "core/category.html" %} - -{% block form-header %} -

Create a category

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

Update category

-{% endblock %} - -{% block confirm-button %} - -{% endblock %} diff --git a/src/newsreader/news/core/templates/core/category.html b/src/newsreader/news/core/templates/core/category.html deleted file mode 100644 index 0771345..0000000 --- a/src/newsreader/news/core/templates/core/category.html +++ /dev/null @@ -1,62 +0,0 @@ -{% extends "base.html" %} - -{% load static %} - -{% block content %} -
-
- {% csrf_token %} - -
- {% block form-header %}{% endblock %} -
- - {{ form.non_field_errors }} - {{ form.user.errors }} - {{ form.user }} - -
-
- - {{ form.name.errors }} - {{ form.name }} -
-
- -
-
- - - Note that existing assigned rules will be reassigned to this category - - {{ form.rules.errors }} - -
    - {% for rule in rules %} -
  • - - - {% if rule.favicon %} - - {% else %} - - {% endif %} - - {{ rule.name }} -
  • - {% endfor %} -
-
-
- -
-
- Cancel - {% block confirm-button %}{% endblock %} -
-
-
-
-{% endblock %} diff --git a/src/newsreader/news/core/templates/core/categories.html b/src/newsreader/news/core/templates/news/core/views/categories.html similarity index 99% rename from src/newsreader/news/core/templates/core/categories.html rename to src/newsreader/news/core/templates/news/core/views/categories.html index be4a449..35fc741 100644 --- a/src/newsreader/news/core/templates/core/categories.html +++ b/src/newsreader/news/core/templates/news/core/views/categories.html @@ -1,5 +1,4 @@ {% extends "base.html" %} - {% load static %} {% block content %} diff --git a/src/newsreader/news/core/templates/news/core/views/category-create.html b/src/newsreader/news/core/templates/news/core/views/category-create.html new file mode 100644 index 0000000..6da166f --- /dev/null +++ b/src/newsreader/news/core/templates/news/core/views/category-create.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} +{% load static %} + +{% block content %} +
+ {% url "news:core:categories" as cancel_url %} + {% include "components/form/form.html" with form=form title="Create category" cancel_url=cancel_url confirm_text="Create category" %} +
+{% endblock %} diff --git a/src/newsreader/news/core/templates/news/core/views/category-update.html b/src/newsreader/news/core/templates/news/core/views/category-update.html new file mode 100644 index 0000000..1ec1487 --- /dev/null +++ b/src/newsreader/news/core/templates/news/core/views/category-update.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} +{% load static %} + +{% block content %} +
+ {% url "news:core:categories" as cancel_url %} + {% include "components/form/form.html" with form=form title="Update category" cancel_url=cancel_url confirm_text="Save category" %} +
+{% endblock %} diff --git a/src/newsreader/news/core/templates/core/homepage.html b/src/newsreader/news/core/templates/news/core/views/homepage.html similarity index 99% rename from src/newsreader/news/core/templates/core/homepage.html rename to src/newsreader/news/core/templates/news/core/views/homepage.html index 8904517..79e1ccc 100644 --- a/src/newsreader/news/core/templates/core/homepage.html +++ b/src/newsreader/news/core/templates/news/core/views/homepage.html @@ -1,5 +1,4 @@ {% extends "base.html" %} - {% load static %} {% block content %} diff --git a/src/newsreader/news/core/templates/news/core/widgets/rule.html b/src/newsreader/news/core/templates/news/core/widgets/rule.html new file mode 100644 index 0000000..b3c7b68 --- /dev/null +++ b/src/newsreader/news/core/templates/news/core/widgets/rule.html @@ -0,0 +1,10 @@ + + +{% if option.instance.favicon %} + +{% else %} + +{% endif %} + +{{ option.label }} diff --git a/src/newsreader/news/core/templates/news/core/widgets/rules.html b/src/newsreader/news/core/templates/news/core/widgets/rules.html new file mode 100644 index 0000000..bbdd43a --- /dev/null +++ b/src/newsreader/news/core/templates/news/core/widgets/rules.html @@ -0,0 +1,9 @@ +
    + {% for group, options, index in widget.optgroups %} + {% for option in options %} +
  • + {% include "news/core/widgets/rule.html" with option=option only %} +
  • + {% endfor %} + {% endfor %} +
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 2bd6bcb..864a144 100644 --- a/src/newsreader/news/core/tests/endpoints/category/detail/tests.py +++ b/src/newsreader/news/core/tests/endpoints/category/detail/tests.py @@ -16,7 +16,9 @@ class CategoryDetailViewTestCase(TestCase): def test_simple(self): category = CategoryFactory(user=self.user) - response = self.client.get(reverse("api:categories-detail", args=[category.pk])) + response = self.client.get( + reverse("api:news:core:categories-detail", args=[category.pk]) + ) data = response.json() self.assertEquals(response.status_code, 200) @@ -24,7 +26,9 @@ class CategoryDetailViewTestCase(TestCase): self.assertTrue("name" in data) def test_not_known(self): - response = self.client.get(reverse("api:categories-detail", args=[100])) + response = self.client.get( + reverse("api:news:core:categories-detail", args=[100]) + ) data = response.json() self.assertEquals(response.status_code, 404) @@ -34,7 +38,7 @@ class CategoryDetailViewTestCase(TestCase): category = CategoryFactory(user=self.user) response = self.client.post( - reverse("api:categories-detail", args=[category.pk]) + reverse("api:news:core:categories-detail", args=[category.pk]) ) data = response.json() @@ -45,7 +49,7 @@ class CategoryDetailViewTestCase(TestCase): category = CategoryFactory(name="Clickbait", user=self.user) response = self.client.patch( - reverse("api:categories-detail", args=[category.pk]), + reverse("api:news:core:categories-detail", args=[category.pk]), data=json.dumps({"name": "Interesting posts"}), content_type="application/json", ) @@ -58,7 +62,7 @@ class CategoryDetailViewTestCase(TestCase): category = CategoryFactory(user=self.user) response = self.client.patch( - reverse("api:categories-detail", args=[category.pk]), + reverse("api:news:core:categories-detail", args=[category.pk]), data=json.dumps({"id": 44}), content_type="application/json", ) @@ -71,7 +75,7 @@ class CategoryDetailViewTestCase(TestCase): category = CategoryFactory(name="Clickbait", user=self.user) response = self.client.put( - reverse("api:categories-detail", args=[category.pk]), + reverse("api:news:core:categories-detail", args=[category.pk]), data=json.dumps({"name": "Interesting posts"}), content_type="application/json", ) @@ -84,7 +88,7 @@ class CategoryDetailViewTestCase(TestCase): category = CategoryFactory(user=self.user) response = self.client.delete( - reverse("api:categories-detail", args=[category.pk]) + reverse("api:news:core:categories-detail", args=[category.pk]) ) self.assertEquals(response.status_code, 204) @@ -94,7 +98,9 @@ class CategoryDetailViewTestCase(TestCase): category = CategoryFactory(user=self.user) - response = self.client.get(reverse("api:categories-detail", args=[category.pk])) + response = self.client.get( + reverse("api:news:core:categories-detail", args=[category.pk]) + ) self.assertEquals(response.status_code, 403) @@ -102,7 +108,9 @@ class CategoryDetailViewTestCase(TestCase): other_user = UserFactory() category = CategoryFactory(user=other_user) - response = self.client.get(reverse("api:categories-detail", args=[category.pk])) + response = self.client.get( + reverse("api:news:core:categories-detail", args=[category.pk]) + ) self.assertEquals(response.status_code, 403) @@ -114,7 +122,9 @@ class CategoryDetailViewTestCase(TestCase): 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])) + response = self.client.get( + reverse("api:news:core:categories-detail", args=[category.pk]) + ) data = response.json() self.assertEquals(response.status_code, 200) @@ -133,7 +143,9 @@ class CategoryReadTestCase(TestCase): for rule in CollectionRuleFactory.create_batch(size=5, category=category) ] - response = self.client.post(reverse("api:categories-read", args=[category.pk])) + response = self.client.post( + reverse("api:news:core:categories-read", args=[category.pk]) + ) data = response.json() @@ -142,7 +154,9 @@ class CategoryReadTestCase(TestCase): self.assertEquals(data["id"], category.pk) def test_category_unknown(self): - response = self.client.post(reverse("api:categories-read", args=[101])) + response = self.client.post( + reverse("api:news:core:categories-read", args=[101]) + ) self.assertEquals(response.status_code, 404) @@ -157,7 +171,9 @@ class CategoryReadTestCase(TestCase): ) ] - response = self.client.post(reverse("api:categories-read", args=[category.pk])) + response = self.client.post( + reverse("api:news:core:categories-read", args=[category.pk]) + ) self.assertEquals(response.status_code, 403) @@ -172,14 +188,18 @@ class CategoryReadTestCase(TestCase): ) ] - response = self.client.post(reverse("api:categories-read", args=[category.pk])) + response = self.client.post( + reverse("api:news:core: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])) + response = self.client.get( + reverse("api:news:core:categories-read", args=[category.pk]) + ) self.assertEquals(response.status_code, 405) @@ -187,7 +207,7 @@ class CategoryReadTestCase(TestCase): category = CategoryFactory(name="Clickbait", user=self.user) response = self.client.patch( - reverse("api:categories-read", args=[category.pk]), + reverse("api:news:core:categories-read", args=[category.pk]), data=json.dumps({"name": "Not possible"}), content_type="application/json", ) @@ -198,7 +218,7 @@ class CategoryReadTestCase(TestCase): category = CategoryFactory(name="Clickbait", user=self.user) response = self.client.put( - reverse("api:categories-read", args=[category.pk]), + reverse("api:news:core:categories-read", args=[category.pk]), data=json.dumps({"name": "Not possible"}), content_type="application/json", ) @@ -209,7 +229,7 @@ class CategoryReadTestCase(TestCase): category = CategoryFactory(name="Clickbait", user=self.user) response = self.client.delete( - reverse("api:categories-read", args=[category.pk]) + reverse("api:news:core: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 d44f204..aedd5e1 100644 --- a/src/newsreader/news/core/tests/endpoints/category/list/tests.py +++ b/src/newsreader/news/core/tests/endpoints/category/list/tests.py @@ -20,7 +20,7 @@ class CategoryListViewTestCase(TestCase): def test_simple(self): CategoryFactory.create_batch(size=3, user=self.user) - response = self.client.get(reverse("api:categories-list")) + response = self.client.get(reverse("api:news:core:categories-list")) data = response.json() self.assertEquals(response.status_code, 200) @@ -48,7 +48,7 @@ class CategoryListViewTestCase(TestCase): ), ] - response = self.client.get(reverse("api:categories-list")) + response = self.client.get(reverse("api:news:core:categories-list")) data = response.json() self.assertEquals(response.status_code, 200) @@ -58,7 +58,7 @@ class CategoryListViewTestCase(TestCase): self.assertEquals(data[2]["id"], categories[0].pk) def test_empty(self): - response = self.client.get(reverse("api:categories-list")) + response = self.client.get(reverse("api:news:core:categories-list")) data = response.json() self.assertEquals(response.status_code, 200) @@ -68,7 +68,7 @@ class CategoryListViewTestCase(TestCase): data = {"name": "Tech"} response = self.client.post( - reverse("api:categories-list"), + reverse("api:news:core:categories-list"), data=json.dumps(data), content_type="application/json", ) @@ -78,21 +78,21 @@ class CategoryListViewTestCase(TestCase): self.assertEquals(response_data["detail"], 'Method "POST" not allowed.') def test_patch(self): - response = self.client.patch(reverse("api:categories-list")) + response = self.client.patch(reverse("api:news:core:categories-list")) data = response.json() self.assertEquals(response.status_code, 405) self.assertEquals(data["detail"], 'Method "PATCH" not allowed.') def test_put(self): - response = self.client.put(reverse("api:categories-list")) + response = self.client.put(reverse("api:news:core:categories-list")) data = response.json() self.assertEquals(response.status_code, 405) self.assertEquals(data["detail"], 'Method "PUT" not allowed.') def test_delete(self): - response = self.client.delete(reverse("api:categories-list")) + response = self.client.delete(reverse("api:news:core:categories-list")) data = response.json() self.assertEquals(response.status_code, 405) @@ -103,7 +103,7 @@ class CategoryListViewTestCase(TestCase): CategoryFactory.create_batch(size=3, user=self.user) - response = self.client.get(reverse("api:categories-list")) + response = self.client.get(reverse("api:news:core:categories-list")) self.assertEquals(response.status_code, 403) @@ -111,7 +111,7 @@ class CategoryListViewTestCase(TestCase): other_user = UserFactory() CategoryFactory.create_batch(size=3, user=other_user) - response = self.client.get(reverse("api:categories-list")) + response = self.client.get(reverse("api:news:core:categories-list")) data = response.json() self.assertEquals(response.status_code, 200) @@ -128,7 +128,7 @@ class NestedCategoryListViewTestCase(TestCase): rules = CollectionRuleFactory.create_batch(size=5, category=category) response = self.client.get( - reverse("api:categories-nested-rules", kwargs={"pk": category.pk}) + reverse("api:news:core:categories-nested-rules", kwargs={"pk": category.pk}) ) data = response.json() @@ -145,7 +145,7 @@ class NestedCategoryListViewTestCase(TestCase): category = CategoryFactory.create(user=self.user) response = self.client.get( - reverse("api:categories-nested-rules", kwargs={"pk": category.pk}) + reverse("api:news:core:categories-nested-rules", kwargs={"pk": category.pk}) ) data = response.json() @@ -155,14 +155,14 @@ class NestedCategoryListViewTestCase(TestCase): def test_not_known(self): response = self.client.get( - reverse("api:categories-nested-rules", kwargs={"pk": 100}) + reverse("api:news:core: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}), + reverse("api:news:core:categories-nested-rules", kwargs={"pk": 100}), data=json.dumps({}), content_type="application/json", ) @@ -175,7 +175,9 @@ class NestedCategoryListViewTestCase(TestCase): category = CategoryFactory.create(user=self.user) response = self.client.patch( - reverse("api:categories-nested-rules", kwargs={"pk": category.pk}), + reverse( + "api:news:core:categories-nested-rules", kwargs={"pk": category.pk} + ), data=json.dumps({"name": "test"}), content_type="application/json", ) @@ -188,7 +190,9 @@ class NestedCategoryListViewTestCase(TestCase): category = CategoryFactory.create(user=self.user) response = self.client.put( - reverse("api:categories-nested-rules", kwargs={"pk": category.pk}), + reverse( + "api:news:core:categories-nested-rules", kwargs={"pk": category.pk} + ), data=json.dumps({"name": "test"}), content_type="application/json", ) @@ -201,7 +205,9 @@ class NestedCategoryListViewTestCase(TestCase): category = CategoryFactory.create(user=self.user) response = self.client.delete( - reverse("api:categories-nested-rules", kwargs={"pk": category.pk}), + reverse( + "api:news:core:categories-nested-rules", kwargs={"pk": category.pk} + ), content_type="application/json", ) data = response.json() @@ -216,7 +222,7 @@ class NestedCategoryListViewTestCase(TestCase): rules = CollectionRuleFactory.create_batch(size=5, category=category) response = self.client.get( - reverse("api:categories-nested-rules", kwargs={"pk": category.pk}) + reverse("api:news:core:categories-nested-rules", kwargs={"pk": category.pk}) ) self.assertEquals(response.status_code, 403) @@ -228,7 +234,7 @@ class NestedCategoryListViewTestCase(TestCase): rules = CollectionRuleFactory.create_batch(size=5, category=category) response = self.client.get( - reverse("api:categories-nested-rules", kwargs={"pk": category.pk}) + reverse("api:news:core:categories-nested-rules", kwargs={"pk": category.pk}) ) self.assertEquals(response.status_code, 403) @@ -242,7 +248,7 @@ class NestedCategoryListViewTestCase(TestCase): ] response = self.client.get( - reverse("api:categories-nested-rules", kwargs={"pk": category.pk}) + reverse("api:news:core:categories-nested-rules", kwargs={"pk": category.pk}) ) data = response.json() @@ -265,7 +271,7 @@ class NestedCategoryListViewTestCase(TestCase): ] response = self.client.get( - reverse("api:categories-nested-rules", kwargs={"pk": category.pk}) + reverse("api:news:core:categories-nested-rules", kwargs={"pk": category.pk}) ) data = response.json() @@ -292,7 +298,7 @@ class NestedCategoryPostView(TestCase): } response = self.client.get( - reverse("api:categories-nested-posts", kwargs={"pk": category.pk}) + reverse("api:news:core:categories-nested-posts", kwargs={"pk": category.pk}) ) data = response.json() posts = data["results"] @@ -310,7 +316,7 @@ class NestedCategoryPostView(TestCase): category = CategoryFactory.create(user=self.user) response = self.client.get( - reverse("api:categories-nested-posts", kwargs={"pk": category.pk}) + reverse("api:news:core:categories-nested-posts", kwargs={"pk": category.pk}) ) data = response.json() posts = data["results"] @@ -326,7 +332,7 @@ class NestedCategoryPostView(TestCase): ) response = self.client.get( - reverse("api:categories-nested-posts", kwargs={"pk": category.pk}) + reverse("api:news:core:categories-nested-posts", kwargs={"pk": category.pk}) ) data = response.json() posts = data["results"] @@ -337,14 +343,14 @@ class NestedCategoryPostView(TestCase): def test_not_known(self): response = self.client.get( - reverse("api:categories-nested-posts", kwargs={"pk": 100}) + reverse("api:news:core: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}), + reverse("api:news:core:categories-nested-posts", kwargs={"pk": 100}), data=json.dumps({}), content_type="application/json", ) @@ -357,7 +363,9 @@ class NestedCategoryPostView(TestCase): category = CategoryFactory.create(user=self.user) response = self.client.patch( - reverse("api:categories-nested-posts", kwargs={"pk": category.pk}), + reverse( + "api:news:core:categories-nested-posts", kwargs={"pk": category.pk} + ), data=json.dumps({}), content_type="application/json", ) @@ -370,7 +378,9 @@ class NestedCategoryPostView(TestCase): category = CategoryFactory.create(user=self.user) response = self.client.put( - reverse("api:categories-nested-posts", kwargs={"pk": category.pk}), + reverse( + "api:news:core:categories-nested-posts", kwargs={"pk": category.pk} + ), data=json.dumps({}), content_type="application/json", ) @@ -383,7 +393,9 @@ class NestedCategoryPostView(TestCase): category = CategoryFactory.create(user=self.user) response = self.client.delete( - reverse("api:categories-nested-posts", kwargs={"pk": category.pk}), + reverse( + "api:news:core:categories-nested-posts", kwargs={"pk": category.pk} + ), content_type="application/json", ) data = response.json() @@ -397,7 +409,7 @@ class NestedCategoryPostView(TestCase): category = CategoryFactory.create(user=self.user) response = self.client.get( - reverse("api:categories-nested-posts", kwargs={"pk": category.pk}) + reverse("api:news:core:categories-nested-posts", kwargs={"pk": category.pk}) ) self.assertEquals(response.status_code, 403) @@ -407,7 +419,7 @@ class NestedCategoryPostView(TestCase): category = CategoryFactory.create(user=other_user) response = self.client.get( - reverse("api:categories-nested-posts", kwargs={"pk": category.pk}) + reverse("api:news:core:categories-nested-posts", kwargs={"pk": category.pk}) ) self.assertEquals(response.status_code, 403) @@ -477,7 +489,7 @@ class NestedCategoryPostView(TestCase): ] response = self.client.get( - reverse("api:categories-nested-posts", kwargs={"pk": category.pk}) + reverse("api:news:core:categories-nested-posts", kwargs={"pk": category.pk}) ) data = response.json() posts = data["results"] @@ -514,7 +526,7 @@ class NestedCategoryPostView(TestCase): ] response = self.client.get( - reverse("api:categories-nested-posts", kwargs={"pk": category.pk}) + reverse("api:news:core:categories-nested-posts", kwargs={"pk": category.pk}) ) data = response.json() posts = data["results"] @@ -533,7 +545,9 @@ class NestedCategoryPostView(TestCase): PostFactory.create_batch(size=10, rule=rule, read=True) response = self.client.get( - reverse("api:categories-nested-posts", kwargs={"pk": category.pk}), + reverse( + "api:news:core:categories-nested-posts", kwargs={"pk": category.pk} + ), {"read": "false"}, ) @@ -554,7 +568,9 @@ class NestedCategoryPostView(TestCase): PostFactory.create_batch(size=10, rule=rule, read=True) response = self.client.get( - reverse("api:categories-nested-posts", kwargs={"pk": category.pk}), + reverse( + "api:news:core:categories-nested-posts", kwargs={"pk": category.pk} + ), {"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 7c8c31e..c804ff5 100644 --- a/src/newsreader/news/core/tests/endpoints/post/detail/tests.py +++ b/src/newsreader/news/core/tests/endpoints/post/detail/tests.py @@ -19,7 +19,9 @@ class PostDetailViewTestCase(TestCase): ) post = PostFactory(rule=rule) - response = self.client.get(reverse("api:posts-detail", args=[post.pk])) + response = self.client.get( + reverse("api:news:core:posts-detail", args=[post.pk]) + ) data = response.json() self.assertEquals(response.status_code, 200) @@ -34,7 +36,7 @@ class PostDetailViewTestCase(TestCase): self.assertTrue("remoteIdentifier" in data) def test_not_known(self): - response = self.client.get(reverse("api:posts-detail", args=[100])) + response = self.client.get(reverse("api:news:core:posts-detail", args=[100])) data = response.json() self.assertEquals(response.status_code, 404) @@ -46,7 +48,9 @@ class PostDetailViewTestCase(TestCase): ) post = PostFactory(rule=rule) - response = self.client.post(reverse("api:posts-detail", args=[post.pk])) + response = self.client.post( + reverse("api:news:core:posts-detail", args=[post.pk]) + ) data = response.json() self.assertEquals(response.status_code, 405) @@ -59,7 +63,7 @@ class PostDetailViewTestCase(TestCase): post = PostFactory(title="This is clickbait for sure", rule=rule) response = self.client.patch( - reverse("api:posts-detail", args=[post.pk]), + reverse("api:news:core:posts-detail", args=[post.pk]), data=json.dumps({"title": "This title is very accurate"}), content_type="application/json", ) @@ -75,7 +79,7 @@ class PostDetailViewTestCase(TestCase): post = PostFactory(title="This is clickbait for sure", rule=rule) response = self.client.patch( - reverse("api:posts-detail", args=[post.pk]), + reverse("api:news:core:posts-detail", args=[post.pk]), data=json.dumps({"id": 44}), content_type="application/json", ) @@ -94,8 +98,14 @@ class PostDetailViewTestCase(TestCase): post = PostFactory(title="This is clickbait for sure", rule=rule) response = self.client.patch( - reverse("api:posts-detail", args=[post.pk]), - data=json.dumps({"rule": reverse("api:rules-detail", args=[new_rule.pk])}), + reverse("api:news:core:posts-detail", args=[post.pk]), + data=json.dumps( + { + "rule": reverse( + "api:news:collection:rules-detail", args=[new_rule.pk] + ) + } + ), content_type="application/json", ) data = response.json() @@ -111,7 +121,7 @@ class PostDetailViewTestCase(TestCase): post = PostFactory(title="This is clickbait for sure", rule=rule) response = self.client.put( - reverse("api:posts-detail", args=[post.pk]), + reverse("api:news:core:posts-detail", args=[post.pk]), data=json.dumps({"title": "This title is very accurate"}), content_type="application/json", ) @@ -126,7 +136,9 @@ class PostDetailViewTestCase(TestCase): ) post = PostFactory(rule=rule) - response = self.client.delete(reverse("api:posts-detail", args=[post.pk])) + response = self.client.delete( + reverse("api:news:core:posts-detail", args=[post.pk]) + ) data = response.json() self.assertEquals(response.status_code, 405) @@ -138,7 +150,9 @@ class PostDetailViewTestCase(TestCase): rule = CollectionRuleFactory(user=self.user, category=None) post = PostFactory(rule=rule) - response = self.client.get(reverse("api:posts-detail", args=[post.pk])) + response = self.client.get( + reverse("api:news:core:posts-detail", args=[post.pk]) + ) self.assertEquals(response.status_code, 403) @@ -150,7 +164,9 @@ class PostDetailViewTestCase(TestCase): ) post = PostFactory(rule=rule) - response = self.client.get(reverse("api:posts-detail", args=[post.pk])) + response = self.client.get( + reverse("api:news:core:posts-detail", args=[post.pk]) + ) self.assertEquals(response.status_code, 403) @@ -159,7 +175,9 @@ class PostDetailViewTestCase(TestCase): rule = CollectionRuleFactory(user=other_user, category=None) post = PostFactory(rule=rule) - response = self.client.get(reverse("api:posts-detail", args=[post.pk])) + response = self.client.get( + reverse("api:news:core:posts-detail", args=[post.pk]) + ) self.assertEquals(response.status_code, 403) @@ -170,7 +188,9 @@ class PostDetailViewTestCase(TestCase): ) post = PostFactory(rule=rule) - response = self.client.get(reverse("api:posts-detail", args=[post.pk])) + response = self.client.get( + reverse("api:news:core:posts-detail", args=[post.pk]) + ) self.assertEquals(response.status_code, 403) @@ -181,7 +201,9 @@ class PostDetailViewTestCase(TestCase): ) post = PostFactory(rule=rule) - response = self.client.get(reverse("api:posts-detail", args=[post.pk])) + response = self.client.get( + reverse("api:news:core:posts-detail", args=[post.pk]) + ) self.assertEquals(response.status_code, 403) @@ -192,7 +214,7 @@ class PostDetailViewTestCase(TestCase): post = PostFactory(rule=rule, read=False) response = self.client.patch( - reverse("api:posts-detail", args=[post.pk]), + reverse("api:news:core:posts-detail", args=[post.pk]), data=json.dumps({"read": True}), content_type="application/json", ) @@ -208,7 +230,7 @@ class PostDetailViewTestCase(TestCase): post = PostFactory(rule=rule, read=True) response = self.client.patch( - reverse("api:posts-detail", args=[post.pk]), + reverse("api:news:core:posts-detail", args=[post.pk]), data=json.dumps({"read": False}), content_type="application/json", ) 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 f3639bf..3800b64 100644 --- a/src/newsreader/news/core/tests/endpoints/post/list/tests.py +++ b/src/newsreader/news/core/tests/endpoints/post/list/tests.py @@ -21,7 +21,7 @@ class PostListViewTestCase(TestCase): ) PostFactory.create_batch(size=3, rule=rule) - response = self.client.get(reverse("api:posts-list")) + response = self.client.get(reverse("api:news:core:posts-list")) data = response.json() self.assertEquals(response.status_code, 200) @@ -58,7 +58,7 @@ class PostListViewTestCase(TestCase): ), ] - response = self.client.get(reverse("api:posts-list")) + response = self.client.get(reverse("api:news:core:posts-list")) data = response.json() self.assertEquals(response.status_code, 200) @@ -77,7 +77,7 @@ class PostListViewTestCase(TestCase): PostFactory.create_batch(size=80, rule=rule) page_size = 50 - response = self.client.get(reverse("api:posts-list"), {"count": 50}) + response = self.client.get(reverse("api:news:core:posts-list"), {"count": 50}) data = response.json() self.assertEquals(response.status_code, 200) @@ -85,7 +85,7 @@ class PostListViewTestCase(TestCase): self.assertEquals(len(data["results"]), page_size) def test_empty(self): - response = self.client.get(reverse("api:posts-list")) + response = self.client.get(reverse("api:news:core:posts-list")) data = response.json() self.assertEquals(response.status_code, 200) @@ -96,28 +96,28 @@ class PostListViewTestCase(TestCase): self.assertEquals(len(data["results"]), 0) def test_post(self): - response = self.client.post(reverse("api:posts-list")) + response = self.client.post(reverse("api:news:core:posts-list")) data = response.json() self.assertEquals(response.status_code, 405) self.assertEquals(data["detail"], 'Method "POST" not allowed.') def test_patch(self): - response = self.client.patch(reverse("api:posts-list")) + response = self.client.patch(reverse("api:news:core:posts-list")) data = response.json() self.assertEquals(response.status_code, 405) self.assertEquals(data["detail"], 'Method "PATCH" not allowed.') def test_put(self): - response = self.client.put(reverse("api:posts-list")) + response = self.client.put(reverse("api:news:core:posts-list")) data = response.json() self.assertEquals(response.status_code, 405) self.assertEquals(data["detail"], 'Method "PUT" not allowed.') def test_delete(self): - response = self.client.delete(reverse("api:posts-list")) + response = self.client.delete(reverse("api:news:core:posts-list")) data = response.json() self.assertEquals(response.status_code, 405) @@ -128,7 +128,7 @@ class PostListViewTestCase(TestCase): PostFactory.create_batch(size=3, rule=CollectionRuleFactory(user=self.user)) - response = self.client.get(reverse("api:posts-list")) + response = self.client.get(reverse("api:news:core:posts-list")) self.assertEquals(response.status_code, 403) @@ -141,7 +141,7 @@ class PostListViewTestCase(TestCase): size=3, rule=CollectionRuleFactory(user=self.user, category=category) ) - response = self.client.get(reverse("api:posts-list")) + response = self.client.get(reverse("api:news:core:posts-list")) self.assertEquals(response.status_code, 403) @@ -151,7 +151,7 @@ class PostListViewTestCase(TestCase): rule = CollectionRuleFactory(user=other_user, category=None) PostFactory.create_batch(size=3, rule=rule) - response = self.client.get(reverse("api:posts-list")) + response = self.client.get(reverse("api:news:core:posts-list")) data = response.json() self.assertEquals(response.status_code, 200) @@ -166,7 +166,7 @@ class PostListViewTestCase(TestCase): size=3, rule=CollectionRuleFactory(user=other_user, category=category) ) - response = self.client.get(reverse("api:posts-list")) + response = self.client.get(reverse("api:news:core:posts-list")) data = response.json() self.assertEquals(response.status_code, 200) @@ -183,7 +183,7 @@ class PostListViewTestCase(TestCase): ) PostFactory.create_batch(size=3, rule=rule) - response = self.client.get(reverse("api:posts-list")) + response = self.client.get(reverse("api:news:core:posts-list")) data = response.json() self.assertEquals(response.status_code, 200) @@ -195,7 +195,7 @@ class PostListViewTestCase(TestCase): rule = CollectionRuleFactory(user=self.user, category=None) PostFactory.create_batch(size=3, rule=rule) - response = self.client.get(reverse("api:posts-list")) + response = self.client.get(reverse("api:news:core:posts-list")) data = response.json() self.assertEquals(response.status_code, 200) @@ -211,7 +211,9 @@ class PostListViewTestCase(TestCase): 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"}) + response = self.client.get( + reverse("api:news:core:posts-list"), {"read": "false"} + ) data = response.json() posts = data["results"] @@ -230,7 +232,9 @@ class PostListViewTestCase(TestCase): 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"}) + response = self.client.get( + reverse("api:news:core:posts-list"), {"read": "true"} + ) data = response.json() posts = data["results"] diff --git a/src/newsreader/news/core/tests/test_views.py b/src/newsreader/news/core/tests/test_views.py index e4bf458..2601b4a 100644 --- a/src/newsreader/news/core/tests/test_views.py +++ b/src/newsreader/news/core/tests/test_views.py @@ -22,7 +22,7 @@ class CategoryCreateViewTestCase(CategoryViewTestCase, TestCase): def setUp(self): super().setUp() - self.url = reverse("category-create") + self.url = reverse("news:core:category-create") def test_creation(self): rules = CollectionRuleFactory.create_batch(size=4, user=self.user) @@ -88,7 +88,7 @@ class CategoryUpdateViewTestCase(CategoryViewTestCase, TestCase): super().setUp() self.category = CategoryFactory(name="category", user=self.user) - self.url = reverse("category-update", args=[self.category.pk]) + self.url = reverse("news:core:category-update", args=[self.category.pk]) def test_name_change(self): data = {"name": "durp", "user": self.user.pk} @@ -172,7 +172,7 @@ class CategoryUpdateViewTestCase(CategoryViewTestCase, TestCase): other_category.rules.set([*other_rules]) data = {"name": "durp", "user": other_user.pk} - other_url = reverse("category-update", args=[other_category.pk]) + other_url = reverse("news:core:category-update", args=[other_category.pk]) response = self.client.post(other_url, data) self.assertEquals(response.status_code, 404) @@ -218,7 +218,7 @@ class CategoryUpdateViewTestCase(CategoryViewTestCase, TestCase): def test_unique_together(self): other_category = CategoryFactory(name="other category", user=self.user) - url = reverse("category-update", args=[other_category.pk]) + url = reverse("news:core:category-update", args=[other_category.pk]) data = {"name": "category", "user": self.user.pk, "rules": []} response = self.client.post(url, data) diff --git a/src/newsreader/news/core/urls.py b/src/newsreader/news/core/urls.py index 4b92428..8096cf8 100644 --- a/src/newsreader/news/core/urls.py +++ b/src/newsreader/news/core/urls.py @@ -19,7 +19,6 @@ from newsreader.news.core.views import ( urlpatterns = [ - path("", login_required(NewsView.as_view()), name="index"), path("categories/", login_required(CategoryListView.as_view()), name="categories"), path( "categories//", diff --git a/src/newsreader/news/core/views.py b/src/newsreader/news/core/views.py index fde3974..9ef81eb 100644 --- a/src/newsreader/news/core/views.py +++ b/src/newsreader/news/core/views.py @@ -9,7 +9,7 @@ from newsreader.news.core.models import Category class NewsView(TemplateView): - template_name = "core/homepage.html" + template_name = "news/core/views/homepage.html" # TODO serialize objects to show filled main page def get_context_data(self, **kwargs): @@ -39,7 +39,7 @@ class CategoryViewMixin: class CategoryDetailMixin: - success_url = reverse_lazy("categories") + success_url = reverse_lazy("news:core:categories") form_class = CategoryForm def get_context_data(self, **kwargs): @@ -55,14 +55,14 @@ class CategoryDetailMixin: class CategoryListView(CategoryViewMixin, ListView): - template_name = "core/categories.html" + template_name = "news/core/views/categories.html" context_object_name = "categories" class CategoryUpdateView(CategoryViewMixin, CategoryDetailMixin, UpdateView): - template_name = "core/category-update.html" + template_name = "news/core/views/category-update.html" context_object_name = "category" class CategoryCreateView(CategoryViewMixin, CategoryDetailMixin, CreateView): - template_name = "core/category-create.html" + template_name = "news/core/views/category-create.html" diff --git a/src/newsreader/news/urls.py b/src/newsreader/news/urls.py new file mode 100644 index 0000000..3e16f7e --- /dev/null +++ b/src/newsreader/news/urls.py @@ -0,0 +1,19 @@ +from django.urls import include, path + +from newsreader.news.collection.urls import endpoints as collection_endpoints +from newsreader.news.collection.urls import urlpatterns as collection_urls +from newsreader.news.core.urls import endpoints as core_endpoints +from newsreader.news.core.urls import urlpatterns as core_urls + + +app_name = "news" + +urlpatterns = [ + path("core/", include((core_urls, "core"))), + path("collection/", include((collection_urls, "collection"))), +] + +endpoints = [ + path("", include((core_endpoints, "core"))), + path("", include((collection_endpoints, "collection"))), +] diff --git a/src/newsreader/scss/components/form/_activation-form.scss b/src/newsreader/scss/components/form/_activation-form.scss deleted file mode 100644 index 39ecc27..0000000 --- a/src/newsreader/scss/components/form/_activation-form.scss +++ /dev/null @@ -1,11 +0,0 @@ -.activation-form { - margin: 10px 0; - & h4 { - padding: 20px 24px 5px 24px; - } - - &__fieldset:last-child { - flex-direction: row; - justify-content: space-between; - } -} diff --git a/src/newsreader/scss/components/form/_category-form.scss b/src/newsreader/scss/components/form/_category-form.scss deleted file mode 100644 index 8132ed2..0000000 --- a/src/newsreader/scss/components/form/_category-form.scss +++ /dev/null @@ -1,13 +0,0 @@ -.category-form { - @extend .form; - - margin: 20px 0; - - &__section:last-child { - & .category-form__fieldset { - display: flex; - flex-direction: row; - justify-content: space-between; - } - } -} diff --git a/src/newsreader/scss/components/form/_form.scss b/src/newsreader/scss/components/form/_form.scss index 931fba9..19e9d4b 100644 --- a/src/newsreader/scss/components/form/_form.scss +++ b/src/newsreader/scss/components/form/_form.scss @@ -1,3 +1,5 @@ +@import "mixin.scss"; + .form { display: flex; flex-direction: column; @@ -8,6 +10,29 @@ font-family: $form-font; background-color: $white; + &__section { + &--last { + & .form__fieldset { + display: flex; + flex-direction: row; + justify-content: space-between; + } + } + + &--actions { + display: flex; + flex-direction: row !important; + + & .form__fieldset { + flex-direction: row; + + & > * { + margin: 0 0 0 5px; + } + } + } + } + &__fieldset { @extend .fieldset; } @@ -16,13 +41,24 @@ display: flex; flex-direction: row; - padding: 15px; + @include form-padding; + } + + &__actions { + display: flex; + flex-direction: row; + + @include form-padding; } &__title { font-size: 18px; } + &__intro { + @include form-padding; + } + & .favicon { height: 20px; } diff --git a/src/newsreader/scss/components/form/_import-form.scss b/src/newsreader/scss/components/form/_import-form.scss deleted file mode 100644 index 19acc5c..0000000 --- a/src/newsreader/scss/components/form/_import-form.scss +++ /dev/null @@ -1,17 +0,0 @@ -.import-form { - margin: 20px 0; - - &__fieldset:last-child { - display: flex; - flex-direction: row; - justify-content: space-between; - } - - & input[type=file] { - width: 50%; - } - - & input[type=checkbox] { - margin: 0 auto 0 10px; - } -} diff --git a/src/newsreader/scss/components/form/_login-form.scss b/src/newsreader/scss/components/form/_login-form.scss deleted file mode 100644 index 10e81a0..0000000 --- a/src/newsreader/scss/components/form/_login-form.scss +++ /dev/null @@ -1,33 +0,0 @@ -.login-form { - @extend .form; - - width: 100%; - - h4 { - margin: 0; - padding: 20px 24px 5px 24px; - } - - &__fieldset { - @extend .form__fieldset; - - & label { - @extend .label; - } - - & input { - @extend .input; - } - } - - &__fieldset:last-child { - flex-direction: row-reverse; - justify-content: space-between; - } - - &__fieldset:last-child { - .button { - padding: 10px 50px; - } - } -} diff --git a/src/newsreader/scss/components/form/_mixin.scss b/src/newsreader/scss/components/form/_mixin.scss new file mode 100644 index 0000000..4f55a9e --- /dev/null +++ b/src/newsreader/scss/components/form/_mixin.scss @@ -0,0 +1,3 @@ +@mixin form-padding { + padding: 15px; +} diff --git a/src/newsreader/scss/components/form/_password-reset-confirm-form.scss b/src/newsreader/scss/components/form/_password-reset-confirm-form.scss deleted file mode 100644 index d570c38..0000000 --- a/src/newsreader/scss/components/form/_password-reset-confirm-form.scss +++ /dev/null @@ -1,3 +0,0 @@ -.password-reset-confirm-form { - margin: 20px 0; -} diff --git a/src/newsreader/scss/components/form/_password-reset-form.scss b/src/newsreader/scss/components/form/_password-reset-form.scss deleted file mode 100644 index be92ff4..0000000 --- a/src/newsreader/scss/components/form/_password-reset-form.scss +++ /dev/null @@ -1,18 +0,0 @@ -.password-reset-form { - margin: 20px 0; - - &__fieldset:last-child { - display: flex; - flex-direction: row; - justify-content: space-between; - } - - & .form__header { - display: flex; - flex-direction: column; - } - - & .form__title { - margin: 0 0 5px 0; - } -} diff --git a/src/newsreader/scss/components/form/_register-form.scss b/src/newsreader/scss/components/form/_register-form.scss deleted file mode 100644 index e406ae7..0000000 --- a/src/newsreader/scss/components/form/_register-form.scss +++ /dev/null @@ -1,11 +0,0 @@ -.register-form { - margin: 10px 0; - & h4 { - padding: 20px 24px 5px 24px; - } - - &__fieldset:last-child { - flex-direction: row; - justify-content: space-between; - } -} diff --git a/src/newsreader/scss/components/form/_rule-form.scss b/src/newsreader/scss/components/form/_rule-form.scss deleted file mode 100644 index 82651aa..0000000 --- a/src/newsreader/scss/components/form/_rule-form.scss +++ /dev/null @@ -1,25 +0,0 @@ -.rule-form { - margin: 20px 0; - - &__section:last-child { - & .rule-form__fieldset { - display: flex; - flex-direction: row; - justify-content: space-between; - } - } - - #id_category { - width: 50%; - - padding: 0 10px; - } - - #id_timezone { - max-height: 200px; - width: 50%; - - margin: 0 15px; - padding: 0 10px; - } -} diff --git a/src/newsreader/scss/components/form/_rules-form.scss b/src/newsreader/scss/components/form/_rules-form.scss new file mode 100644 index 0000000..44d4765 --- /dev/null +++ b/src/newsreader/scss/components/form/_rules-form.scss @@ -0,0 +1,5 @@ +.rules-form { + @extend .form; + + width: 90%; +} diff --git a/src/newsreader/scss/components/form/index.scss b/src/newsreader/scss/components/form/index.scss index 2c70cdd..8069223 100644 --- a/src/newsreader/scss/components/form/index.scss +++ b/src/newsreader/scss/components/form/index.scss @@ -1,12 +1,3 @@ @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"; +@import "rules-form"; diff --git a/src/newsreader/scss/components/index.scss b/src/newsreader/scss/components/index.scss index 4bddb31..53e0f71 100644 --- a/src/newsreader/scss/components/index.scss +++ b/src/newsreader/scss/components/index.scss @@ -12,7 +12,9 @@ @import "section/index"; @import "errorlist/index"; @import "fieldset/index"; +@import "pagination/index"; @import "sidebar/index"; +@import "table/index"; @import "rules/index"; @import "category/index"; diff --git a/src/newsreader/scss/components/pagination/_pagination.scss b/src/newsreader/scss/components/pagination/_pagination.scss new file mode 100644 index 0000000..d4ba4a9 --- /dev/null +++ b/src/newsreader/scss/components/pagination/_pagination.scss @@ -0,0 +1,18 @@ +@import "../../elements/button/mixins"; + +.pagination { + display: flex; + justify-content: space-evenly; + + &__previous, &__current, &__next { + display: flex; + justify-content: space-evenly; + + width: 33%; + text-align: center; + } + + &__current { + @include button-padding; + } +} diff --git a/src/newsreader/scss/components/pagination/index.scss b/src/newsreader/scss/components/pagination/index.scss new file mode 100644 index 0000000..d92e61f --- /dev/null +++ b/src/newsreader/scss/components/pagination/index.scss @@ -0,0 +1 @@ +@import "pagination"; diff --git a/src/newsreader/scss/components/section/_text-section.scss b/src/newsreader/scss/components/section/_text-section.scss new file mode 100644 index 0000000..88e3e72 --- /dev/null +++ b/src/newsreader/scss/components/section/_text-section.scss @@ -0,0 +1,11 @@ +.text-section { + @extend .section; + + width: 70%; + border-radius: 5px; + + padding: 10px; + + background-color: $white; +} + diff --git a/src/newsreader/scss/components/section/index.scss b/src/newsreader/scss/components/section/index.scss index 4fb6763..0e02686 100644 --- a/src/newsreader/scss/components/section/index.scss +++ b/src/newsreader/scss/components/section/index.scss @@ -1 +1,2 @@ @import "section"; +@import "text-section"; diff --git a/src/newsreader/scss/components/table/_rules-table.scss b/src/newsreader/scss/components/table/_rules-table.scss new file mode 100644 index 0000000..3eaf3b3 --- /dev/null +++ b/src/newsreader/scss/components/table/_rules-table.scss @@ -0,0 +1,38 @@ +.rules-table { + &__heading { + &--select { + width: 5%; + } + + &--name { + width: 20%; + } + + &--category { + width: 15%; + } + + &--url { + width: 40%; + } + + &--succeeded { + width: 15%; + } + + &--enabled { + width: 10%; + } + + &--link { + width: 5%; + } + } + + & .link { + display: flex; + justify-content: center; + + padding: 10px; + } +} diff --git a/src/newsreader/scss/components/table/_table.scss b/src/newsreader/scss/components/table/_table.scss new file mode 100644 index 0000000..60ab7e8 --- /dev/null +++ b/src/newsreader/scss/components/table/_table.scss @@ -0,0 +1,32 @@ +@import "../../lib/mixins"; + +.table { + @include rounded; + + table-layout: fixed; + background-color: $white; + width: 90%; + padding: 20px; + + text-align: left; + white-space: nowrap; + + &__heading { + @extend .h1; + } + + &__item { + padding: 10px 0; + + border-bottom: 1px solid $border-gray; + + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + &__footer { + width: 80%; + padding: 10px 0; + } +} diff --git a/src/newsreader/scss/components/table/index.scss b/src/newsreader/scss/components/table/index.scss new file mode 100644 index 0000000..d175a21 --- /dev/null +++ b/src/newsreader/scss/components/table/index.scss @@ -0,0 +1,2 @@ +@import "table"; +@import "rules-table"; diff --git a/src/newsreader/scss/elements/button/_button.scss b/src/newsreader/scss/elements/button/_button.scss index a6bec19..3a06cd3 100644 --- a/src/newsreader/scss/elements/button/_button.scss +++ b/src/newsreader/scss/elements/button/_button.scss @@ -1,10 +1,12 @@ +@import "mixins"; + .button { display: flex; align-items: center; justify-content: center; - padding: 10px 50px; + @include button-padding; border: none; border-radius: 2px; diff --git a/src/newsreader/scss/elements/button/_mixins.scss b/src/newsreader/scss/elements/button/_mixins.scss new file mode 100644 index 0000000..75b70e3 --- /dev/null +++ b/src/newsreader/scss/elements/button/_mixins.scss @@ -0,0 +1,3 @@ +@mixin button-padding { + padding: 7px 40px; +} diff --git a/src/newsreader/scss/elements/index.scss b/src/newsreader/scss/elements/index.scss index f0d7be3..3e2a01c 100644 --- a/src/newsreader/scss/elements/index.scss +++ b/src/newsreader/scss/elements/index.scss @@ -1,10 +1,11 @@ +@import "badge/index"; @import "button/index"; +@import "help-text/index"; +@import "input/index"; +@import "label/index"; @import "link/index"; @import "h1/index"; @import "h2/index"; @import "h3/index"; @import "small/index"; -@import "input/index"; -@import "label/index"; -@import "help-text/index"; -@import "badge/index"; +@import "select/index"; diff --git a/src/newsreader/scss/elements/input/_input.scss b/src/newsreader/scss/elements/input/_input.scss index 1cfb4bb..897fbf9 100644 --- a/src/newsreader/scss/elements/input/_input.scss +++ b/src/newsreader/scss/elements/input/_input.scss @@ -8,6 +8,15 @@ &:focus { border: 1px $focus-blue solid; } + + &[type="file"] { + width: 40%; + } + + &[type="checkbox"] { + align-self: flex-start; + margin: 0 0 0 10px; + } } input { diff --git a/src/newsreader/scss/elements/link/_link.scss b/src/newsreader/scss/elements/link/_link.scss index 1843c0b..b485cb3 100644 --- a/src/newsreader/scss/elements/link/_link.scss +++ b/src/newsreader/scss/elements/link/_link.scss @@ -10,7 +10,3 @@ a { @extend .link; } - -.gg-link { - color: initial; -} diff --git a/src/newsreader/scss/elements/select/_select.scss b/src/newsreader/scss/elements/select/_select.scss new file mode 100644 index 0000000..d8737b4 --- /dev/null +++ b/src/newsreader/scss/elements/select/_select.scss @@ -0,0 +1,13 @@ +.select { + max-height: 200px; + + &:not([size]){ + width: 40%; + } + + padding: 0 15px; +} + +select { + @extend .select; +} diff --git a/src/newsreader/scss/elements/select/index.scss b/src/newsreader/scss/elements/select/index.scss new file mode 100644 index 0000000..8320088 --- /dev/null +++ b/src/newsreader/scss/elements/select/index.scss @@ -0,0 +1 @@ +@import "select"; diff --git a/src/newsreader/scss/lib/_css.gg.scss b/src/newsreader/scss/lib/_css.gg.scss index 389e533..e7096d5 100644 --- a/src/newsreader/scss/lib/_css.gg.scss +++ b/src/newsreader/scss/lib/_css.gg.scss @@ -1 +1,9 @@ @import "~css.gg/icons-scss/icons"; + +.gg-link { + color: initial; +} + +.gg-pen { + color: initial; +} diff --git a/src/newsreader/scss/lib/_mixins.scss b/src/newsreader/scss/lib/_mixins.scss new file mode 100644 index 0000000..e2d28aa --- /dev/null +++ b/src/newsreader/scss/lib/_mixins.scss @@ -0,0 +1,3 @@ +@mixin rounded { + border-radius: 5px; +} diff --git a/src/newsreader/scss/pages/index.scss b/src/newsreader/scss/pages/index.scss index 872ac89..ddfaf85 100644 --- a/src/newsreader/scss/pages/index.scss +++ b/src/newsreader/scss/pages/index.scss @@ -8,5 +8,7 @@ @import "password-reset/index"; @import "register/index"; -@import "rules/index"; @import "rule/index"; +@import "rules/index"; + +@import "settings/index"; diff --git a/src/newsreader/scss/pages/login/index.scss b/src/newsreader/scss/pages/login/index.scss index 69b946e..82b9457 100644 --- a/src/newsreader/scss/pages/login/index.scss +++ b/src/newsreader/scss/pages/login/index.scss @@ -3,4 +3,29 @@ width: 50%; border-radius: 4px; + + & .form { + @extend .form; + + width: 100%; + + h4 { + margin: 0; + padding: 20px 24px 5px 24px; + } + + &__section { + &--last { + flex-direction: row-reverse; + justify-content: space-between; + } + } + + &__fieldset { + @extend .form__fieldset; + + &--last { + } + } + } } diff --git a/src/newsreader/scss/pages/rules/index.scss b/src/newsreader/scss/pages/rules/index.scss index 68b92cb..64f46b4 100644 --- a/src/newsreader/scss/pages/rules/index.scss +++ b/src/newsreader/scss/pages/rules/index.scss @@ -1,7 +1,5 @@ #rules--page { - .list__item { - & .link { - margin: 0; - } + & .table { + width: 100%; } } diff --git a/src/newsreader/scss/pages/settings/index.scss b/src/newsreader/scss/pages/settings/index.scss new file mode 100644 index 0000000..28837cd --- /dev/null +++ b/src/newsreader/scss/pages/settings/index.scss @@ -0,0 +1,12 @@ +#settings--page { + .settings-form__fieldset:last-child { + & span { + display: flex; + flex-direction: row; + + & >:first-child { + margin: 0 5px; + } + } + } +} diff --git a/src/newsreader/templates/base.html b/src/newsreader/templates/base.html index 42d438b..3f677c0 100644 --- a/src/newsreader/templates/base.html +++ b/src/newsreader/templates/base.html @@ -15,9 +15,12 @@
    {% if request.user.is_authenticated %} - - - + + + + {% if request.user.is_superuser %} + + {% endif %} {% else %} diff --git a/src/newsreader/templates/components/card/card.html b/src/newsreader/templates/components/card/card.html new file mode 100644 index 0000000..750bd06 --- /dev/null +++ b/src/newsreader/templates/components/card/card.html @@ -0,0 +1,13 @@ +
    + {% if header_text %} + {% include "components/card/header.html" %} + {% endif %} + + {% if content %} + {% include "components/card/content.html" %} + {% endif %} + + {% if footer_text %} + {% include "components/card/footer.html" %} + {% endif %} +
    diff --git a/src/newsreader/templates/components/card/content.html b/src/newsreader/templates/components/card/content.html new file mode 100644 index 0000000..8ae0141 --- /dev/null +++ b/src/newsreader/templates/components/card/content.html @@ -0,0 +1,3 @@ +
    +

    {{ content }}

    +
    diff --git a/src/newsreader/templates/components/card/footer.html b/src/newsreader/templates/components/card/footer.html new file mode 100644 index 0000000..1acffe0 --- /dev/null +++ b/src/newsreader/templates/components/card/footer.html @@ -0,0 +1,3 @@ + diff --git a/src/newsreader/templates/components/card/header.html b/src/newsreader/templates/components/card/header.html new file mode 100644 index 0000000..567756a --- /dev/null +++ b/src/newsreader/templates/components/card/header.html @@ -0,0 +1,3 @@ +
    +

    {{ header_text }}

    +
    diff --git a/src/newsreader/templates/components/form/cancel-button.html b/src/newsreader/templates/components/form/cancel-button.html new file mode 100644 index 0000000..0891136 --- /dev/null +++ b/src/newsreader/templates/components/form/cancel-button.html @@ -0,0 +1,5 @@ +{% load i18n %} + +{% if cancel_url %} + {% trans "Cancel" %} +{% endif %} diff --git a/src/newsreader/templates/components/form/confirm-button.html b/src/newsreader/templates/components/form/confirm-button.html new file mode 100644 index 0000000..e18b560 --- /dev/null +++ b/src/newsreader/templates/components/form/confirm-button.html @@ -0,0 +1,9 @@ +{% load i18n %} + + diff --git a/src/newsreader/templates/components/form/errors.html b/src/newsreader/templates/components/form/errors.html new file mode 100644 index 0000000..eed67f5 --- /dev/null +++ b/src/newsreader/templates/components/form/errors.html @@ -0,0 +1,3 @@ +
    + {{ errors }} +
    diff --git a/src/newsreader/templates/components/form/form.html b/src/newsreader/templates/components/form/form.html new file mode 100644 index 0000000..d854eb1 --- /dev/null +++ b/src/newsreader/templates/components/form/form.html @@ -0,0 +1,43 @@ +{% load i18n %} + +
    + {% csrf_token %} + + {% if title %} + {% include "components/form/title.html" with title=title only %} + {% endif %} + + {% block intro %} + {% endblock intro %} + + {% if form.non_field_errors %} + {% include "components/form/errors.html" with errors=form.non_field_errors only %} + {% endif %} + + {% block fields %} +
    + {% for field in form.hidden_fields %} + {{ field }} + {% endfor %} + + {% for field in form.visible_fields %} +
    + {% include "components/form/label.html" %} + + {{ field.errors }} + {{ field }} + {% include "components/form/help-text.html" %} +
    + {% endfor %} +
    + {% endblock fields %} + + {% block actions %} +
    +
    + {% include "components/form/cancel-button.html" %} + {% include "components/form/confirm-button.html" %} +
    +
    + {% endblock actions %} +
    diff --git a/src/newsreader/templates/components/form/help-text.html b/src/newsreader/templates/components/form/help-text.html new file mode 100644 index 0000000..eb02d82 --- /dev/null +++ b/src/newsreader/templates/components/form/help-text.html @@ -0,0 +1 @@ +{{ field.help_text }} diff --git a/src/newsreader/templates/components/form/label.html b/src/newsreader/templates/components/form/label.html new file mode 100644 index 0000000..4058b29 --- /dev/null +++ b/src/newsreader/templates/components/form/label.html @@ -0,0 +1,3 @@ + diff --git a/src/newsreader/templates/components/form/title.html b/src/newsreader/templates/components/form/title.html new file mode 100644 index 0000000..3adcb75 --- /dev/null +++ b/src/newsreader/templates/components/form/title.html @@ -0,0 +1,3 @@ +
    +

    {{ title }}

    +
    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..0b7796f --- /dev/null +++ b/src/newsreader/templates/password-reset/password-reset-complete.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block content %} +
    + {% trans "Password reset complete" as header_text %} + {% blocktrans asvar content %} + You may now log in + {% endblocktrans %} + + {% include "components/card/card.html" with header_text=header_text content=content %} +
    +{% 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..d0d5037 --- /dev/null +++ b/src/newsreader/templates/password-reset/password-reset-confirm.html @@ -0,0 +1,30 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block meta %} + + +{% endblock %} + +{% block content %} +
    + {% if validlink %} + {% url 'accounts:login' as cancel_url %} + {% trans "Enter your new password below to reset your password:" as title %} + {% trans "Change password" as confirm_text %} + {% include "components/form/form.html" with form=form title=title confirm_text=confirm_text cancel_url=cancel_url %} + {% else %} + {% trans "Password reset unsuccessful" as header_text %} + {% url 'accounts:password-reset' as reset_url %} + {% blocktrans asvar content %} + Password reset unsuccessful. Please + try again. + {% endblocktrans %} + + {% include "components/card/card.html" with header_text=header_text content=content %} + {% 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..7012439 --- /dev/null +++ b/src/newsreader/templates/password-reset/password-reset-done.html @@ -0,0 +1,16 @@ +{% extends "base.html" %} +{% load static i18n %} + +{% block title %}{% trans "Password reset" %}{% endblock %} + +{% block content %} +
    + {% trans "Password reset" as header_text %} + {% blocktrans asvar content %} + We have sent you an email with a link to reset your password. Please check + your email and click the link to continue. + {% endblocktrans %} + + {% include "components/card/card.html" with header_text=header_text content=content %} +
    +{% endblock %} diff --git a/src/newsreader/templates/password-reset/password_reset_email.html b/src/newsreader/templates/password-reset/password-reset-email.html similarity index 100% rename from src/newsreader/templates/password-reset/password_reset_email.html rename to src/newsreader/templates/password-reset/password-reset-email.html 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 100644 index 0000000..e423560 --- /dev/null +++ b/src/newsreader/templates/password-reset/password-reset-form.html @@ -0,0 +1,11 @@ +{% extends "components/form/form.html" %} +{% load i18n %} + +{% block intro %} +

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

    +{% endblock intro %} diff --git a/src/newsreader/templates/password-reset/password_reset_subject.txt b/src/newsreader/templates/password-reset/password-reset-subject.txt similarity index 100% rename from src/newsreader/templates/password-reset/password_reset_subject.txt rename to src/newsreader/templates/password-reset/password-reset-subject.txt diff --git a/src/newsreader/templates/password-reset/password-reset.html b/src/newsreader/templates/password-reset/password-reset.html new file mode 100644 index 0000000..97e5678 --- /dev/null +++ b/src/newsreader/templates/password-reset/password-reset.html @@ -0,0 +1,7 @@ +{% extends "base.html" %} + +{% block content %} +
    + {% include "password-reset/password-reset-form.html" with form=form title="Reset password" confirm_text="Reset password" %} +
    +{% endblock %} diff --git a/src/newsreader/templates/password-reset/password_reset_complete.html b/src/newsreader/templates/password-reset/password_reset_complete.html deleted file mode 100755 index 8a47f55..0000000 --- a/src/newsreader/templates/password-reset/password_reset_complete.html +++ /dev/null @@ -1,23 +0,0 @@ -{% extends "base.html" %} -{% load static i18n %} - -{% block title %}{% trans "Password reset complete" %}{% endblock %} - -{% block content %} -
    -
    -
    -

    {% trans "Password reset complete" %}

    -
    -
    -

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

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

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

    -
    - -
    - {{ form }} -
    -
    - Cancel - -
    -
    - - {% else %} -
    -
    -

    {% trans "Password reset unsuccessful" %}

    -
    -
    -

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

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

    {% trans "Password reset" %}

    -
    -
    -

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

    -
    - -
    -{% endblock %} diff --git a/src/newsreader/templates/password-reset/password_reset_form.html b/src/newsreader/templates/password-reset/password_reset_form.html deleted file mode 100755 index cd5fc3e..0000000 --- a/src/newsreader/templates/password-reset/password_reset_form.html +++ /dev/null @@ -1,30 +0,0 @@ -{% extends "base.html" %} -{% load static i18n %} - -{% block title %}{% trans "Reset password" %}{% endblock %} - -{% block content %} -
    -
    - {% csrf_token %} -
    -

    {% trans "Reset password" %}

    - -

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

    -
    - -
    - {{ form }} -
    -
    - Cancel - -
    -
    -
    -{% endblock %} diff --git a/src/newsreader/templates/registration/activation_complete.html b/src/newsreader/templates/registration/activation_complete.html index 61ea493..f8dd91b 100755 --- a/src/newsreader/templates/registration/activation_complete.html +++ b/src/newsreader/templates/registration/activation_complete.html @@ -1,7 +1,5 @@ {% extends "base.html" %} -{% load static i18n %} - -{% block title %}{% trans "Account Activated" %}{% endblock %} +{% load i18n %} {% comment %} **registration/activation_complete.html** @@ -13,19 +11,14 @@ account is now active. {% block content %}
    -
    -
    -

    {% trans "Account activated" %}

    -
    -
    -

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

    -
    - + {% trans "Account activated" as header_text %} + + {% if user.is_authenticated %} + {% trans "Your account is activated. You can now log in." as content %} + {% else %} + {% trans "Your account is activated." as content %} + {% endif %} + + {% include "components/card/card.html" with header_text=header_text content=content %}
    {% endblock %} diff --git a/src/newsreader/templates/registration/activation_email.html b/src/newsreader/templates/registration/activation_email.html index 8be4421..8773b29 100644 --- a/src/newsreader/templates/registration/activation_email.html +++ b/src/newsreader/templates/registration/activation_email.html @@ -68,5 +68,5 @@ following context: ``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 %} + {{ request.scheme }}://{{ request.get_host }}{% url 'accounts:activate' activation_key %} {% endcomment %} diff --git a/src/newsreader/templates/registration/activation_email.txt b/src/newsreader/templates/registration/activation_email.txt index 7f52a60..d07e785 100644 --- a/src/newsreader/templates/registration/activation_email.txt +++ b/src/newsreader/templates/registration/activation_email.txt @@ -48,5 +48,5 @@ following context: ``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 %} + {{ request.scheme }}://{{ request.get_host }}{% url 'accounts:activate' activation_key %} {% endcomment %} diff --git a/src/newsreader/templates/registration/activation_failure.html b/src/newsreader/templates/registration/activation_failure.html index 5cf0f67..c99cc34 100644 --- a/src/newsreader/templates/registration/activation_failure.html +++ b/src/newsreader/templates/registration/activation_failure.html @@ -1,7 +1,5 @@ {% extends "base.html" %} -{% load static i18n %} - -{% block title %}{% trans "Activation Failure" %}{% endblock %} +{% load i18n %} {% comment %} **registration/activate.html** @@ -14,14 +12,8 @@ Used if account activation fails. With the default setup, has the following cont {% block content %}
    -
    -
    -

    {% trans "Activation Failure" %}

    -
    -
    -

    {% trans "Account activation failed." %}

    -
    - + {% trans "Activation Failure" as header_text %} + {% trans "Account activation failed." as content %} + {% include "components/card/card.html" with header_text=header_text content=content %}
    {% endblock %} diff --git a/src/newsreader/templates/registration/activation_resend_complete.html b/src/newsreader/templates/registration/activation_resend_complete.html index dcf1e79..6d01fee 100644 --- a/src/newsreader/templates/registration/activation_resend_complete.html +++ b/src/newsreader/templates/registration/activation_resend_complete.html @@ -14,18 +14,10 @@ the following context: {% block content %}
    -
    -
    -

    {% trans "Account activation resent" %}

    -
    -
    -

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

    -
    - + {% trans "Account activation resent" as header_text %} + {% blocktrans asvar content %} + We have sent an email to {{ email }} with further instructions. + {% endblocktrans %} + {% include "components/card/card.html" with header_text=header_text content=content %}
    {% endblock %} diff --git a/src/newsreader/templates/registration/activation_resend_form.html b/src/newsreader/templates/registration/activation_resend_form.html index f721242..5f0dd82 100644 --- a/src/newsreader/templates/registration/activation_resend_form.html +++ b/src/newsreader/templates/registration/activation_resend_form.html @@ -1,35 +1,9 @@ {% extends "base.html" %} -{% load static i18n %} - -{% block title %}{% trans "Resend Activation Email" %}{% endblock %} - -{% comment %} -**registration/resend_activation_form.html** -Used to show the form users will fill out to resend the activation email. By -default, has the following context: - -``form`` - The registration form. This will be an instance of some subclass - of ``django.forms.Form``; consult `Django's forms documentation - `_ for - information on how to display this in a template. -{% endcomment %} +{% load static %} {% block content %}
    -
    - {% csrf_token %} -
    -

    Resend activation code

    -
    - -
    - {{ form }} -
    -
    - Cancel - -
    -
    + {% url "accounts:login" as cancel_url %} + {% include "components/form/form.html" with form=form title="Resend activation code" cancel_url=cancel_url confirm_text="Resend code" %}
    {% endblock %} diff --git a/src/newsreader/templates/registration/registration_closed.html b/src/newsreader/templates/registration/registration_closed.html index 6169ebe..c7cfd9a 100755 --- a/src/newsreader/templates/registration/registration_closed.html +++ b/src/newsreader/templates/registration/registration_closed.html @@ -1,20 +1,10 @@ {% extends "base.html" %} {% load static i18n %} -{% block title %}{% trans "Registration is closed" %}{% endblock %} - {% block content %}
    -
    -
    -

    {% trans "Registration is closed" %}

    -
    -
    -

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

    -
    - + {% trans "Registration is closed" as header_text %} + {% trans "Sorry, but registration is closed at this moment. Come back later." as content %} + {% include "components/card/card.html" with header_text=header_text content=content %}
    {% endblock %} diff --git a/src/newsreader/templates/registration/registration_complete.html b/src/newsreader/templates/registration/registration_complete.html index cc5f868..ccf70b2 100755 --- a/src/newsreader/templates/registration/registration_complete.html +++ b/src/newsreader/templates/registration/registration_complete.html @@ -1,7 +1,5 @@ {% extends "base.html" %} -{% load static i18n %} - -{% block title %}{% trans "Activation email sent" %}{% endblock %} +{% load i18n %} {% comment %} **registration/registration_complete.html** @@ -14,16 +12,8 @@ been sent. {% block content %}
    -
    -
    -

    {% trans "Activation email sent" %}

    -
    -
    -

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

    -
    - + {% trans "Activation email sent" as header_text %} + {% trans "Please check your email to complete the registration process." as content %} + {% include "components/card/card.html" with header_text=header_text content=content %}
    {% endblock %} diff --git a/src/newsreader/templates/registration/registration_form.html b/src/newsreader/templates/registration/registration_form.html index 9b8619c..ccc07c9 100644 --- a/src/newsreader/templates/registration/registration_form.html +++ b/src/newsreader/templates/registration/registration_form.html @@ -1,22 +1,9 @@ {% extends "base.html" %} - {% load static %} {% block content %}
    -
    - {% csrf_token %} -
    -

    Register

    -
    - -
    - {{ form }} -
    -
    - Cancel - -
    -
    + {% url "accounts:login" as cancel_url %} + {% include "components/form/form.html" with form=form title="Register" cancel_url=cancel_url confirm_text="Register" %}
    {% endblock %} diff --git a/src/newsreader/urls.py b/src/newsreader/urls.py index c609d91..0779b29 100644 --- a/src/newsreader/urls.py +++ b/src/newsreader/urls.py @@ -1,29 +1,26 @@ from django.conf import settings from django.contrib import admin +from django.contrib.auth.decorators import login_required from django.urls import include, path from drf_yasg import openapi from drf_yasg.views import get_schema_view from newsreader.accounts.urls import urlpatterns as login_urls -from newsreader.news.collection.urls import endpoints as collection_endpoints -from newsreader.news.collection.urls import urlpatterns as collection_patterns -from newsreader.news.core.urls import endpoints as core_endpoints -from newsreader.news.core.urls import urlpatterns as core_patterns +from newsreader.news.core.views import NewsView +from newsreader.news.urls import endpoints as news_endpoints +from newsreader.news.urls import urlpatterns as news_patterns -apipatterns = [ - path("api/", include(core_endpoints)), - path("api/", include(collection_endpoints)), -] +api_patterns = [path("api/", include((news_endpoints, "news")))] schema_info = openapi.Info(title="Newsreader API", default_version="v1") -schema_view = get_schema_view(schema_info, patterns=apipatterns) +schema_view = get_schema_view(schema_info, patterns=api_patterns) urlpatterns = [ - path("", include(core_patterns)), - path("", include(collection_patterns)), - path("", include((apipatterns, "api")), name="api"), + path("", login_required(NewsView.as_view()), name="index"), + path("", include((news_patterns, "news"))), + path("", include((api_patterns, "api"))), path("accounts/", include((login_urls, "accounts")), name="accounts"), path("admin/", admin.site.urls, name="admin"), path("api/", schema_view.with_ui("swagger"), name="api"),