Refactor endpoint tests
Replace force_login calls with login call from client class in setUp
This commit is contained in:
parent
428cd39d13
commit
6a4f33c182
135 changed files with 4315 additions and 1336 deletions
|
|
@ -1,52 +1,56 @@
|
||||||
version: '3'
|
version: '3'
|
||||||
|
volumes:
|
||||||
|
postgres-data:
|
||||||
|
static-files:
|
||||||
|
node-modules:
|
||||||
|
|
||||||
services:
|
services:
|
||||||
db:
|
db:
|
||||||
# See https://hub.docker.com/_/postgres
|
|
||||||
image: postgres
|
image: postgres
|
||||||
container_name: postgres
|
|
||||||
environment:
|
environment:
|
||||||
- POSTGRES_DB=$POSTGRES_NAME
|
POSTGRES_DB: "newsreader"
|
||||||
- POSTGRES_USER=$POSTGRES_USER
|
POSTGRES_USER: "newsreader"
|
||||||
- POSTGRES_PASSWORD=$POSTGRES_PASSWORD
|
POSTGRES_PASSWORD: "newsreader"
|
||||||
|
volumes:
|
||||||
|
- postgres-data:/var/lib/postgresql/data
|
||||||
rabbitmq:
|
rabbitmq:
|
||||||
image: rabbitmq:3.7
|
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:
|
memcached:
|
||||||
image: memcached:1.5.22
|
image: memcached:1.5.22
|
||||||
container_name: memcached
|
|
||||||
ports:
|
ports:
|
||||||
- "11211:11211"
|
- "11211:11211"
|
||||||
entrypoint:
|
entrypoint:
|
||||||
- memcached
|
- memcached
|
||||||
- -m 64
|
- -m 64
|
||||||
web:
|
celery:
|
||||||
build: .
|
build:
|
||||||
container_name: web
|
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
|
command: src/entrypoint.sh
|
||||||
environment:
|
environment:
|
||||||
- POSTGRES_HOST=$POSTGRES_HOST
|
|
||||||
- POSTGRES_NAME=$POSTGRES_NAME
|
|
||||||
- POSTGRES_USER=$POSTGRES_USER
|
|
||||||
- POSTGRES_PASSWORD=$POSTGRES_PASSWORD
|
|
||||||
- DJANGO_SETTINGS_MODULE=newsreader.conf.docker
|
- DJANGO_SETTINGS_MODULE=newsreader.conf.docker
|
||||||
volumes:
|
|
||||||
- .:/app
|
|
||||||
ports:
|
ports:
|
||||||
- '8000:8000'
|
- '8000:8000'
|
||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- 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
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ RUN pip install poetry
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY poetry.lock pyproject.toml /app/
|
COPY poetry.lock pyproject.toml /app/
|
||||||
|
|
||||||
RUN poetry config virtualenvs.create false
|
RUN poetry config virtualenvs.create false && poetry install --no-interaction
|
||||||
RUN poetry install --no-interaction
|
|
||||||
|
|
||||||
COPY . /app/
|
COPY . /app/
|
||||||
9
docker/webpack
Normal file
9
docker/webpack
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
FROM node:12
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json package-lock.json /app/
|
||||||
|
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
COPY . /app/
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# This file should only be used in conjuction with docker-compose
|
# This file should only be used in conjuction with docker-compose
|
||||||
|
|
||||||
poetry run /app/src/manage.py migrate
|
python /app/src/manage.py migrate
|
||||||
poetry run /app/src/manage.py runserver 0.0.0.0:8000
|
python /app/src/manage.py runserver 0.0.0.0:8000
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
9
src/newsreader/accounts/forms.py
Normal file
9
src/newsreader/accounts/forms.py
Normal file
|
|
@ -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")
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
{% extends "components/form/form.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block actions %}
|
||||||
|
<section class="section form__section--last">
|
||||||
|
<fieldset class="fieldset form__fieldset">
|
||||||
|
{% include "components/form/cancel-button.html" %}
|
||||||
|
{% include "components/form/confirm-button.html" %}
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset class="fieldset form__fieldset">
|
||||||
|
<a class="link" href="{% url 'accounts:password-reset' %}">
|
||||||
|
<small class="small">{% trans "I forgot my password" %}</small>
|
||||||
|
</a>
|
||||||
|
</fieldset>
|
||||||
|
</section>
|
||||||
|
{% endblock actions %}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
{% extends "components/form/form.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block actions %}
|
||||||
|
<section class="section form__section--last">
|
||||||
|
<fieldset class="fieldset form__fieldset">
|
||||||
|
{% include "components/form/cancel-button.html" %}
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset class="fieldset form__fieldset">
|
||||||
|
<a class="link button button--primary" href="{% url 'accounts:password-change' %}">
|
||||||
|
{% trans "Change password" %}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{% include "components/form/confirm-button.html" %}
|
||||||
|
</fieldset>
|
||||||
|
</section>
|
||||||
|
{% endblock actions %}
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% load static %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<main id="login--page" class="main">
|
|
||||||
<form class="form login-form" method="POST" action="{% url 'accounts:login' %}">
|
|
||||||
{% csrf_token %}
|
|
||||||
<div class="form__header">
|
|
||||||
<h1 class="form__title">Login</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<fieldset class="login-form__fieldset">
|
|
||||||
{{ form }}
|
|
||||||
</fieldset>
|
|
||||||
<fieldset class="login-form__fieldset">
|
|
||||||
<button class="button button--confirm" type="submit">Login</button>
|
|
||||||
<a class="link" href="{% url 'accounts:password-reset' %}">
|
|
||||||
<small class="small">I forgot my password</small>
|
|
||||||
</a>
|
|
||||||
</fieldset>
|
|
||||||
</form>
|
|
||||||
</main>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<main id="login--page" class="main">
|
||||||
|
{% include "accounts/components/login-form.html" with form=form title="Login" confirm_text="Login" %}
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<main id="password-change--page" class="main">
|
||||||
|
{% 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 %}
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<main id="settings--page" class="main">
|
||||||
|
{% include "accounts/components/settings-form.html" with form=form title="User profile" confirm_text="Save" %}
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
||||||
29
src/newsreader/accounts/tests/test_views.py
Normal file
29
src/newsreader/accounts/tests/test_views.py
Normal file
|
|
@ -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")
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from newsreader.accounts.views import (
|
from newsreader.accounts.views import (
|
||||||
|
|
@ -6,6 +7,7 @@ from newsreader.accounts.views import (
|
||||||
ActivationView,
|
ActivationView,
|
||||||
LoginView,
|
LoginView,
|
||||||
LogoutView,
|
LogoutView,
|
||||||
|
PasswordChangeView,
|
||||||
PasswordResetCompleteView,
|
PasswordResetCompleteView,
|
||||||
PasswordResetConfirmView,
|
PasswordResetConfirmView,
|
||||||
PasswordResetDoneView,
|
PasswordResetDoneView,
|
||||||
|
|
@ -13,6 +15,7 @@ from newsreader.accounts.views import (
|
||||||
RegistrationClosedView,
|
RegistrationClosedView,
|
||||||
RegistrationCompleteView,
|
RegistrationCompleteView,
|
||||||
RegistrationView,
|
RegistrationView,
|
||||||
|
SettingsView,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -52,5 +55,10 @@ urlpatterns = [
|
||||||
PasswordResetCompleteView.as_view(),
|
PasswordResetCompleteView.as_view(),
|
||||||
name="password-reset-complete",
|
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"),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,17 @@ from django.contrib.auth import views as django_views
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.views.generic import TemplateView
|
from django.views.generic import TemplateView
|
||||||
|
from django.views.generic.edit import FormView, ModelFormMixin
|
||||||
|
|
||||||
from registration.backends.default import views as registration_views
|
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):
|
class LoginView(django_views.LoginView):
|
||||||
template_name = "accounts/login.html"
|
template_name = "accounts/views/login.html"
|
||||||
|
success_url = reverse_lazy("index")
|
||||||
def get_success_url(self):
|
|
||||||
return reverse_lazy("index")
|
|
||||||
|
|
||||||
|
|
||||||
class LogoutView(django_views.LogoutView):
|
class LogoutView(django_views.LogoutView):
|
||||||
|
|
@ -72,20 +74,42 @@ class ActivationResendView(registration_views.ResendActivationView):
|
||||||
# prompts for a new password
|
# prompts for a new password
|
||||||
# PasswordResetCompleteView shows a success message for the above
|
# PasswordResetCompleteView shows a success message for the above
|
||||||
class PasswordResetView(django_views.PasswordResetView):
|
class PasswordResetView(django_views.PasswordResetView):
|
||||||
template_name = "password-reset/password_reset_form.html"
|
template_name = "password-reset/password-reset.html"
|
||||||
subject_template_name = "password-reset/password_reset_subject.txt"
|
subject_template_name = "password-reset/password-reset-subject.txt"
|
||||||
email_template_name = "password-reset/password_reset_email.html"
|
email_template_name = "password-reset/password-reset-email.html"
|
||||||
success_url = reverse_lazy("accounts:password-reset-done")
|
success_url = reverse_lazy("accounts:password-reset-done")
|
||||||
|
|
||||||
|
|
||||||
class PasswordResetDoneView(django_views.PasswordResetDoneView):
|
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):
|
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")
|
success_url = reverse_lazy("accounts:password-reset-complete")
|
||||||
|
|
||||||
|
|
||||||
class PasswordResetCompleteView(django_views.PasswordResetCompleteView):
|
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}
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ INSTALLED_APPS = [
|
||||||
"axes",
|
"axes",
|
||||||
# app modules
|
# app modules
|
||||||
"newsreader.accounts",
|
"newsreader.accounts",
|
||||||
|
"newsreader.news",
|
||||||
"newsreader.news.core",
|
"newsreader.news.core",
|
||||||
"newsreader.news.collection",
|
"newsreader.news.collection",
|
||||||
]
|
]
|
||||||
|
|
@ -171,6 +172,8 @@ AUTH_PASSWORD_VALIDATORS = [
|
||||||
# Authentication user model
|
# Authentication user model
|
||||||
AUTH_USER_MODEL = "accounts.User"
|
AUTH_USER_MODEL = "accounts.User"
|
||||||
|
|
||||||
|
LOGIN_REDIRECT_URL = "/"
|
||||||
|
|
||||||
# Internationalization
|
# Internationalization
|
||||||
# https://docs.djangoproject.com/en/2.2/topics/i18n/
|
# https://docs.djangoproject.com/en/2.2/topics/i18n/
|
||||||
LANGUAGE_CODE = "en-us"
|
LANGUAGE_CODE = "en-us"
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,15 @@ from .dev import * # isort:skip
|
||||||
|
|
||||||
SECRET_KEY = "=q(ztyo)b6noom#a164g&s9vcj1aawa^g#ing_ir99=_zl4g&$"
|
SECRET_KEY = "=q(ztyo)b6noom#a164g&s9vcj1aawa^g#ing_ir99=_zl4g&$"
|
||||||
|
|
||||||
# Celery
|
DATABASES = {
|
||||||
# https://docs.celeryproject.org/en/latest/userguide/configuration.html
|
"default": {
|
||||||
BROKER_URL = "amqp://guest:guest@rabbitmq:5672//"
|
"ENGINE": "django.db.backends.postgresql",
|
||||||
|
"NAME": "newsreader",
|
||||||
|
"USER": "newsreader",
|
||||||
|
"PASSWORD": "newsreader",
|
||||||
|
"HOST": "db",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
CACHES = {
|
CACHES = {
|
||||||
"default": {
|
"default": {
|
||||||
|
|
@ -17,3 +23,7 @@ CACHES = {
|
||||||
"LOCATION": "memcached:11211",
|
"LOCATION": "memcached:11211",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Celery
|
||||||
|
# https://docs.celeryproject.org/en/latest/userguide/configuration.html
|
||||||
|
CELERY_BROKER_URL = "amqp://guest:guest@rabbitmq:5672//"
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
23
src/newsreader/js/components/Selector.js
Normal file
23
src/newsreader/js/components/Selector.js
Normal file
|
|
@ -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;
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
import './pages/homepage/index.js';
|
import './pages/homepage/index.js';
|
||||||
import './pages/rules/index.js';
|
|
||||||
import './pages/categories/index.js';
|
import './pages/categories/index.js';
|
||||||
|
import './pages/rules/index.js';
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ class App extends React.Component {
|
||||||
const pageHeader = (
|
const pageHeader = (
|
||||||
<>
|
<>
|
||||||
<h1 className="h1">Categories</h1>
|
<h1 className="h1">Categories</h1>
|
||||||
<a className="link button button--confirm" href="/categories/create/">
|
<a className="link button button--confirm" href="/core/categories/create/">
|
||||||
Create category
|
Create category
|
||||||
</a>
|
</a>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,10 @@ const CategoryCard = props => {
|
||||||
const cardContent = <>{category.rules && <ul className="list">{categoryRules}</ul>}</>;
|
const cardContent = <>{category.rules && <ul className="list">{categoryRules}</ul>}</>;
|
||||||
const cardFooter = (
|
const cardFooter = (
|
||||||
<>
|
<>
|
||||||
<a className="link button button--primary" href={`/categories/${category.pk}/`}>
|
<a
|
||||||
|
className="link button button--primary"
|
||||||
|
href={`/core/categories/${category.pk}/`}
|
||||||
|
>
|
||||||
Edit
|
Edit
|
||||||
</a>
|
</a>
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
|
|
@ -1,106 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import Cookies from 'js-cookie';
|
|
||||||
|
|
||||||
import Card from '../../components/Card.js';
|
|
||||||
import RuleCard from './components/RuleCard.js';
|
|
||||||
import RuleModal from './components/RuleModal.js';
|
|
||||||
import Messages from '../../components/Messages.js';
|
|
||||||
|
|
||||||
class App extends React.Component {
|
|
||||||
selectRule = ::this.selectRule;
|
|
||||||
deselectRule = ::this.deselectRule;
|
|
||||||
deleteRule = ::this.deleteRule;
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.token = Cookies.get('csrftoken');
|
|
||||||
this.state = {
|
|
||||||
rules: props.rules,
|
|
||||||
selectedRuleId: null,
|
|
||||||
message: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
selectRule(ruleId) {
|
|
||||||
this.setState({ selectedRuleId: ruleId });
|
|
||||||
}
|
|
||||||
|
|
||||||
deselectRule() {
|
|
||||||
this.setState({ selectedRuleId: null });
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteRule(ruleId) {
|
|
||||||
const url = `/api/rules/${ruleId}/`;
|
|
||||||
const options = {
|
|
||||||
method: 'DELETE',
|
|
||||||
headers: {
|
|
||||||
'X-CSRFToken': this.token,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
fetch(url, options).then(response => {
|
|
||||||
if (response.ok) {
|
|
||||||
const rules = this.state.rules.filter(rule => {
|
|
||||||
return rule.pk != ruleId;
|
|
||||||
});
|
|
||||||
|
|
||||||
return this.setState({
|
|
||||||
rules: rules,
|
|
||||||
selectedRuleId: null,
|
|
||||||
message: null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const message = {
|
|
||||||
type: 'error',
|
|
||||||
text: 'Unable to remove rule, try again later',
|
|
||||||
};
|
|
||||||
return this.setState({ selectedRuleId: null, message: message });
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { rules } = this.state;
|
|
||||||
const cards = rules.map(rule => {
|
|
||||||
return <RuleCard key={rule.pk} rule={rule} showDialog={this.selectRule} />;
|
|
||||||
});
|
|
||||||
|
|
||||||
const selectedRule = rules.find(rule => {
|
|
||||||
return rule.pk === this.state.selectedRuleId;
|
|
||||||
});
|
|
||||||
|
|
||||||
const pageHeader = (
|
|
||||||
<>
|
|
||||||
<h1 className="h1">Rules</h1>
|
|
||||||
|
|
||||||
<div className="card__header--action">
|
|
||||||
<a className="link button button--primary" href="/rules/import/">
|
|
||||||
Import rules
|
|
||||||
</a>
|
|
||||||
<a className="link button button--confirm" href="/rules/create/">
|
|
||||||
Create rule
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{this.state.message && <Messages messages={[this.state.message]} />}
|
|
||||||
<Card header={pageHeader} />
|
|
||||||
{cards}
|
|
||||||
{selectedRule && (
|
|
||||||
<RuleModal
|
|
||||||
rule={selectedRule}
|
|
||||||
handleCancel={this.deselectRule}
|
|
||||||
handleDelete={this.deleteRule}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App;
|
|
||||||
|
|
@ -1,65 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import Card from '../../../components/Card.js';
|
|
||||||
|
|
||||||
const RuleCard = props => {
|
|
||||||
const { rule } = props;
|
|
||||||
let favicon = null;
|
|
||||||
|
|
||||||
if (rule.favicon) {
|
|
||||||
favicon = <img className="favicon" src={rule.favicon} />;
|
|
||||||
} else {
|
|
||||||
favicon = <i className="gg-image" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const stateIcon = !rule.error ? 'gg-check' : 'gg-danger';
|
|
||||||
|
|
||||||
const cardHeader = (
|
|
||||||
<>
|
|
||||||
<i className={stateIcon} />
|
|
||||||
<h2 className="h2">{rule.name}</h2>
|
|
||||||
{favicon}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
const cardContent = (
|
|
||||||
<>
|
|
||||||
<ul className="list rules">
|
|
||||||
{rule.error && (
|
|
||||||
<ul className="list errorlist">
|
|
||||||
<li className="list__item errorlist__item">{rule.error}</li>
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{rule.category && <li className="list__item">{rule.category}</li>}
|
|
||||||
<li className="list__item">
|
|
||||||
<a className="link" target="_blank" rel="noopener noreferrer" href={rule.url}>
|
|
||||||
{rule.url}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li className="list__item">{rule.created}</li>
|
|
||||||
<li className="list__item">{rule.timezone}</li>
|
|
||||||
</ul>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
const cardFooter = (
|
|
||||||
<>
|
|
||||||
<a className="link button button--primary" href={`/rules/${rule.pk}/`}>
|
|
||||||
Edit
|
|
||||||
</a>
|
|
||||||
<button
|
|
||||||
id="rule-delete"
|
|
||||||
className="button button--error"
|
|
||||||
onClick={() => props.showDialog(rule.pk)}
|
|
||||||
data-id={`${rule.pk}`}
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
return <Card header={cardHeader} content={cardContent} footer={cardFooter} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RuleCard;
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import Modal from '../../../components/Modal.js';
|
|
||||||
|
|
||||||
const RuleModal = props => {
|
|
||||||
const content = (
|
|
||||||
<>
|
|
||||||
<div>
|
|
||||||
<div className="modal__header">
|
|
||||||
<h1 className="h1 modal__title">Delete rule</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="modal__content">
|
|
||||||
<p className="p">Are you sure you want to delete {props.rule.name}?</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="modal__footer">
|
|
||||||
<button className="button button--confirm" onClick={props.handleCancel}>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="button button--error"
|
|
||||||
onClick={() => props.handleDelete(props.rule.pk)}
|
|
||||||
>
|
|
||||||
Delete rule
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
return <Modal content={content} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RuleModal;
|
|
||||||
|
|
@ -1,13 +1,7 @@
|
||||||
import React from 'react';
|
import Selector from '../../components/Selector.js';
|
||||||
import ReactDOM from 'react-dom';
|
|
||||||
|
|
||||||
import App from './App.js';
|
|
||||||
|
|
||||||
const page = document.getElementById('rules--page');
|
const page = document.getElementById('rules--page');
|
||||||
|
|
||||||
if (page) {
|
if (page) {
|
||||||
const dataScript = document.getElementById('rules-data');
|
new Selector();
|
||||||
const rules = JSON.parse(dataScript.textContent);
|
|
||||||
|
|
||||||
ReactDOM.render(<App rules={rules} />, page);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
5
src/newsreader/news/apps.py
Normal file
5
src/newsreader/news/apps.py
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class NewsConfig(AppConfig):
|
||||||
|
name = "news"
|
||||||
|
|
@ -23,7 +23,7 @@ class Client:
|
||||||
stream = Stream
|
stream = Stream
|
||||||
|
|
||||||
def __init__(self, rules=None):
|
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):
|
def __enter__(self):
|
||||||
for rule in self.rules:
|
for rule in self.rules:
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
|
from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
|
||||||
from django.db.models.fields import CharField, TextField
|
from django.db.models.fields import CharField, TextField
|
||||||
|
|
@ -184,6 +185,9 @@ class FeedCollector(Collector):
|
||||||
|
|
||||||
|
|
||||||
class FeedDuplicateHandler:
|
class FeedDuplicateHandler:
|
||||||
|
duplicate_fields = ("url", "title", "body", "rule")
|
||||||
|
time_slot_minutes = 10
|
||||||
|
|
||||||
def __init__(self, rule):
|
def __init__(self, rule):
|
||||||
self.queryset = rule.posts.all()
|
self.queryset = rule.posts.all()
|
||||||
|
|
||||||
|
|
@ -199,39 +203,44 @@ class FeedDuplicateHandler:
|
||||||
def check(self, instances):
|
def check(self, instances):
|
||||||
for instance in instances:
|
for instance in instances:
|
||||||
if instance.remote_identifier in self.existing_identifiers:
|
if instance.remote_identifier in self.existing_identifiers:
|
||||||
existing_post = self.handle_duplicate(instance)
|
existing_post = self.handle_duplicate_identifier(instance)
|
||||||
|
|
||||||
yield existing_post
|
yield existing_post
|
||||||
|
|
||||||
continue
|
continue
|
||||||
elif not instance.remote_identifier and self.in_database(instance):
|
elif self.in_database(instance):
|
||||||
continue
|
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
|
yield instance
|
||||||
|
|
||||||
def in_database(self, post):
|
def in_database(self, post):
|
||||||
values = {
|
values = {field: getattr(post, field, None) for field in self.duplicate_fields}
|
||||||
"url": post.url,
|
|
||||||
"title": post.title,
|
|
||||||
"body": post.body,
|
|
||||||
"publication_date": post.publication_date,
|
|
||||||
}
|
|
||||||
|
|
||||||
for existing_post in self.queryset.order_by("-publication_date")[:500]:
|
for existing_post in self.queryset.filter(**values):
|
||||||
if self.is_duplicate(existing_post, values):
|
if self.is_duplicate(existing_post, values):
|
||||||
return True
|
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):
|
def is_duplicate(self, existing_post, values):
|
||||||
for key, value in values.items():
|
return all(
|
||||||
existing_value = getattr(existing_post, key, None)
|
getattr(existing_post, field, None) == value
|
||||||
if existing_value != value:
|
for field, value in values.items()
|
||||||
return False
|
)
|
||||||
|
|
||||||
return True
|
def handle_duplicate_identifier(self, instance):
|
||||||
|
|
||||||
def handle_duplicate(self, instance):
|
|
||||||
try:
|
try:
|
||||||
existing_instance = self.queryset.get(
|
existing_post = self.queryset.get(
|
||||||
remote_identifier=instance.remote_identifier
|
remote_identifier=instance.remote_identifier
|
||||||
)
|
)
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
|
|
@ -240,17 +249,43 @@ class FeedDuplicateHandler:
|
||||||
)
|
)
|
||||||
return instance
|
return instance
|
||||||
except MultipleObjectsReturned:
|
except MultipleObjectsReturned:
|
||||||
existing_instances = self.queryset.filter(
|
existing_posts = self.queryset.filter(
|
||||||
remote_identifier=instance.remote_identifier
|
remote_identifier=instance.remote_identifier
|
||||||
).order_by("-publication_date")
|
).order_by("-publication_date")
|
||||||
existing_instance = existing_instances.last()
|
existing_post = existing_posts.last()
|
||||||
existing_instances.exclude(pk=existing_instance.pk).delete()
|
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():
|
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())
|
new_value = getattr(instance, field.name, object())
|
||||||
|
|
||||||
if new_value and field.name != "id":
|
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
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
from django import forms
|
from django import forms
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
|
|
||||||
|
|
@ -11,6 +12,7 @@ class CollectionRuleForm(forms.ModelForm):
|
||||||
timezone = forms.ChoiceField(
|
timezone = forms.ChoiceField(
|
||||||
widget=forms.Select(attrs={"size": len(pytz.all_timezones)}),
|
widget=forms.Select(attrs={"size": len(pytz.all_timezones)}),
|
||||||
choices=((timezone, timezone) for timezone in 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):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|
@ -36,6 +38,17 @@ class CollectionRuleForm(forms.ModelForm):
|
||||||
fields = ("name", "url", "timezone", "favicon", "category")
|
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):
|
class OPMLImportForm(forms.Form):
|
||||||
file = forms.FileField(allow_empty_file=False)
|
file = forms.FileField(allow_empty_file=False)
|
||||||
skip_existing = forms.BooleanField(initial=False, required=False)
|
skip_existing = forms.BooleanField(initial=False, required=False)
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
@ -34,6 +34,9 @@ class CollectionRule(TimeStampedModel):
|
||||||
last_suceeded = models.DateTimeField(blank=True, null=True)
|
last_suceeded = models.DateTimeField(blank=True, null=True)
|
||||||
succeeded = models.BooleanField(default=False)
|
succeeded = models.BooleanField(default=False)
|
||||||
error = models.CharField(max_length=1024, blank=True, null=True)
|
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(
|
user = models.ForeignKey(
|
||||||
"accounts.User",
|
"accounts.User",
|
||||||
|
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% load static i18n %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<main id="import--page" class="main">
|
|
||||||
<form class="form import-form" method="post" enctype="multipart/form-data">
|
|
||||||
{% csrf_token %}
|
|
||||||
{{ form.non_field_errors }}
|
|
||||||
|
|
||||||
<div class="form__header">
|
|
||||||
<h1 class="h1 form__title">{% trans "Import an OPML file" %}</h1>
|
|
||||||
</div>
|
|
||||||
<section class="section form__section import-form__section">
|
|
||||||
<fieldset class="form__fieldset import-form__fieldset">
|
|
||||||
<label class="label import-form__label" for="name">
|
|
||||||
{% trans "File" %}
|
|
||||||
</label>
|
|
||||||
{{ form.file.errors }}
|
|
||||||
{{ form.file }}
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<fieldset class="form__fieldset import-form__fieldset">
|
|
||||||
<label class="label import-form__label" for="name">
|
|
||||||
{% trans "Skip existing" %}
|
|
||||||
</label>
|
|
||||||
{{ form.skip_existing }}
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<fieldset class="form__fieldset import-form__fieldset">
|
|
||||||
<a class="link button button--cancel" href="{% url 'rules' %}">Cancel</a>
|
|
||||||
<button class="button button--confirm">Import</button>
|
|
||||||
</fieldset>
|
|
||||||
</section>
|
|
||||||
</form>
|
|
||||||
</main>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
{% extends "collection/rule.html" %}
|
|
||||||
|
|
||||||
{% block form-header %}
|
|
||||||
<h1 class="h1 form__title">Create a rule</h1>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block confirm-button %}
|
|
||||||
<button class="button button--confirm">Create rule</button>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
{% extends "collection/rule.html" %}
|
|
||||||
|
|
||||||
{% block form-header %}
|
|
||||||
<h1 class="h1 form__title">Update rule</h1>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block confirm-button %}
|
|
||||||
<button class="button button--confirm">Save rule</button>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% load static %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<main id="rule--page" class="main">
|
|
||||||
<form class="form rule-form" method="post">
|
|
||||||
{% csrf_token %}
|
|
||||||
{{ form.non_field_errors }}
|
|
||||||
|
|
||||||
<div class="form__header">
|
|
||||||
{% block form-header %}{% endblock %}
|
|
||||||
</div>
|
|
||||||
<section class="section form__section rule-form__section">
|
|
||||||
<fieldset class="form__fieldset rule-form__fieldset">
|
|
||||||
<label class="label" for="name">Name</label>
|
|
||||||
{{ form.name.errors }}
|
|
||||||
{{ form.name }}
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<fieldset class="form__fieldset rule-form__fieldset">
|
|
||||||
<label class="label" for="name">Category</label>
|
|
||||||
{{ form.category.errors }}
|
|
||||||
{{ form.category }}
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<fieldset class="form__fieldset rule-form__fieldset">
|
|
||||||
<label class="label" for="name">Feed url</label>
|
|
||||||
{{ form.url.errors }}
|
|
||||||
{{ form.url }}
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<fieldset class="form__fieldset rule-form__fieldset">
|
|
||||||
<label class="label" for="name">Favicon url</label>
|
|
||||||
{{ form.favicon.errors }}
|
|
||||||
{{ form.favicon }}
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<fieldset class="form__fieldset rule-form__fieldset">
|
|
||||||
<label class="label" for="name">Timezone</label>
|
|
||||||
<small class="small helptext">The timezone which the feed uses</small>
|
|
||||||
{{ form.timezone.errors }}
|
|
||||||
{{ form.timezone }}
|
|
||||||
</fieldset>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="section form__section rule-form__section">
|
|
||||||
<fieldset class="form__fieldset rule-form__fieldset">
|
|
||||||
<a class="link button button--cancel" href="{% url 'rules' %}">Cancel</a>
|
|
||||||
{% block confirm-button %}{% endblock %}
|
|
||||||
</fieldset>
|
|
||||||
</section>
|
|
||||||
</form>
|
|
||||||
</main>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% load static %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<main id="rules--page" class="main"></main>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block scripts %}
|
|
||||||
<script id="rules-data">
|
|
||||||
[
|
|
||||||
{% for rule in rules %}
|
|
||||||
{% if not forloop.first %}, {% endif %}
|
|
||||||
{
|
|
||||||
"pk": {{ rule.pk }},
|
|
||||||
"name": "{{ rule.name }}",
|
|
||||||
"url": "{{ rule.url }}",
|
|
||||||
"favicon": "{{ rule.favicon|default:'' }}",
|
|
||||||
"category": "{{ rule.category|default:'' }}",
|
|
||||||
"timezone": "{{ rule.timezone }}",
|
|
||||||
"created": "{{ rule.created }}",
|
|
||||||
"succeeded": {% if rule.succeeded %}true{% else %}false{% endif %},
|
|
||||||
"error": "{{ rule.error|default:'' }}"
|
|
||||||
}
|
|
||||||
{% endfor %}
|
|
||||||
]
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{{ block.super }}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<main id="import--page" class="main">
|
||||||
|
{% 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" %}
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<main id="rule--page" class="main">
|
||||||
|
{% 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" %}
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<main id="rule--page" class="main">
|
||||||
|
{% 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" %}
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load i18n static %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<main id="rules--page" class="main">
|
||||||
|
<form class="form rules-form">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<section class="section form__section form__section--actions">
|
||||||
|
<fieldset class="fieldset form__fieldset">
|
||||||
|
<input type="submit" class="button button--primary" formaction="{% url "news:collection:rules-enable" %}" formmethod="post" value="{% trans "Enable" %}" />
|
||||||
|
<input type="submit" class="button button--primary" formaction="{% url "news:collection:rules-disable" %}" formmethod="post" value="{% trans "Disable" %}" />
|
||||||
|
<input type="submit" class="button button--error" formaction="{% url "news:collection:rules-delete" %}" formmethod="post" value="{% trans "Delete" %}"/>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<div class="form__actions">
|
||||||
|
<a class="link button button--confirm" href="{% url "news:collection:rule-create" %}">{% trans "Add a rule" %}</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section form__section">
|
||||||
|
<table class="table rules-table">
|
||||||
|
<thead class="table__header rules-table__header">
|
||||||
|
<tr class="table__row rules-table__row">
|
||||||
|
<th class="table__heading rules-table__heading--select">
|
||||||
|
<input type="checkbox" id="select-all" data-input="rules" />
|
||||||
|
</th>
|
||||||
|
<th class="table__heading rules-table__heading--name">{% trans "Name" %}</th>
|
||||||
|
<th class="table__heading rules-table__heading--category">{% trans "Category" %}</th>
|
||||||
|
<th class="table__heading rules-table__heading--url">{% trans "URL" %}</th>
|
||||||
|
<th class="table__heading rules-table__heading--succeeded">{% trans "Successfuly ran" %}</th>
|
||||||
|
<th class="table__heading rules-table__heading--enabled">{% trans "Enabled" %}</th>
|
||||||
|
<th class="table__heading rules-table__heading--link"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="table__body">
|
||||||
|
{% for rule in rules %}
|
||||||
|
<tr class="table__row rules-table__row">
|
||||||
|
<td class="table__item rules-table__item"><input name="rules" type="checkbox" value="{{ rule.pk }}" /></td>
|
||||||
|
<td class="table__item rules-table__item" title="{{ rule.name }}">{{ rule.name }}</td>
|
||||||
|
<td class="table__item rules-table__item" title="{{ rule.category.name }}">{{ rule.category.name }}</td>
|
||||||
|
<td class="table__item rules-table__item" title="{{ rule.url }}">{{ rule.url }}</td>
|
||||||
|
<td class="table__item rules-table__item" title="{{ rule.succeeded }}">{{ rule.succeeded }}</td>
|
||||||
|
<td class="table__item rules-table__item" title="{{ rule.enabled }}">{{ rule.enabled }}</td>
|
||||||
|
<td class="table__item rules-table__item">
|
||||||
|
<a class="link" href="{% url "news:collection:rule-update" rule.pk %}"><i class="gg-pen"></i></a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="table__footer">
|
||||||
|
<div class="pagination">
|
||||||
|
<span class="pagination__previous">
|
||||||
|
{% if page_obj.has_previous %}
|
||||||
|
<a class="link button" href="?page=1">{% trans "first" %}</a>
|
||||||
|
<a class="link button" href="?page={{ page_obj.previous_page_number }}">{% trans "previous" %}</a>
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="pagination__current">
|
||||||
|
{% blocktrans with current_number=page_obj.number total_count=page_obj.paginator.num_pages %}
|
||||||
|
Page {{ current_number }} of {{ total_count }}
|
||||||
|
{% endblocktrans %}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="pagination__next">
|
||||||
|
{% if page_obj.has_next %}
|
||||||
|
<a class="link button" href="?page={{ page_obj.next_page_number }}">{% trans "next" %}</a>
|
||||||
|
<a class="link button" href="?page={{ page_obj.paginator.num_pages }}">{% trans "last" %}</a>
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -17,7 +17,9 @@ class CollectionRuleDetailViewTestCase(TestCase):
|
||||||
def test_simple(self):
|
def test_simple(self):
|
||||||
rule = CollectionRuleFactory(user=self.user)
|
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()
|
data = response.json()
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 200)
|
self.assertEquals(response.status_code, 200)
|
||||||
|
|
@ -29,7 +31,9 @@ class CollectionRuleDetailViewTestCase(TestCase):
|
||||||
self.assertTrue("category" in data)
|
self.assertTrue("category" in data)
|
||||||
|
|
||||||
def test_not_known(self):
|
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()
|
data = response.json()
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 404)
|
self.assertEquals(response.status_code, 404)
|
||||||
|
|
@ -38,7 +42,9 @@ class CollectionRuleDetailViewTestCase(TestCase):
|
||||||
def test_post(self):
|
def test_post(self):
|
||||||
rule = CollectionRuleFactory(user=self.user)
|
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()
|
data = response.json()
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 405)
|
self.assertEquals(response.status_code, 405)
|
||||||
|
|
@ -48,7 +54,7 @@ class CollectionRuleDetailViewTestCase(TestCase):
|
||||||
rule = CollectionRuleFactory(name="BBC", user=self.user)
|
rule = CollectionRuleFactory(name="BBC", user=self.user)
|
||||||
|
|
||||||
response = self.client.patch(
|
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"}),
|
data=json.dumps({"name": "The guardian"}),
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
)
|
)
|
||||||
|
|
@ -64,7 +70,7 @@ class CollectionRuleDetailViewTestCase(TestCase):
|
||||||
rule = CollectionRuleFactory(name="BBC", category=old_category, user=self.user)
|
rule = CollectionRuleFactory(name="BBC", category=old_category, user=self.user)
|
||||||
|
|
||||||
response = self.client.patch(
|
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}),
|
data=json.dumps({"category": absolute_url}),
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
)
|
)
|
||||||
|
|
@ -77,7 +83,7 @@ class CollectionRuleDetailViewTestCase(TestCase):
|
||||||
rule = CollectionRuleFactory(user=self.user)
|
rule = CollectionRuleFactory(user=self.user)
|
||||||
|
|
||||||
response = self.client.patch(
|
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}),
|
data=json.dumps({"id": 44}),
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
)
|
)
|
||||||
|
|
@ -91,7 +97,7 @@ class CollectionRuleDetailViewTestCase(TestCase):
|
||||||
category = CategoryFactory(user=self.user)
|
category = CategoryFactory(user=self.user)
|
||||||
|
|
||||||
response = self.client.patch(
|
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}),
|
data=json.dumps({"category": category.pk}),
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
)
|
)
|
||||||
|
|
@ -105,7 +111,7 @@ class CollectionRuleDetailViewTestCase(TestCase):
|
||||||
rule = CollectionRuleFactory(name="BBC", user=self.user)
|
rule = CollectionRuleFactory(name="BBC", user=self.user)
|
||||||
|
|
||||||
response = self.client.put(
|
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"}),
|
data=json.dumps({"name": "BBC", "url": "https://www.bbc.co.uk"}),
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
)
|
)
|
||||||
|
|
@ -117,7 +123,9 @@ class CollectionRuleDetailViewTestCase(TestCase):
|
||||||
def test_delete(self):
|
def test_delete(self):
|
||||||
rule = CollectionRuleFactory(user=self.user)
|
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)
|
self.assertEquals(response.status_code, 204)
|
||||||
|
|
||||||
|
|
@ -127,7 +135,7 @@ class CollectionRuleDetailViewTestCase(TestCase):
|
||||||
rule = CollectionRuleFactory(name="BBC", user=self.user)
|
rule = CollectionRuleFactory(name="BBC", user=self.user)
|
||||||
|
|
||||||
response = self.client.patch(
|
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"}),
|
data=json.dumps({"name": "The guardian"}),
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
)
|
)
|
||||||
|
|
@ -139,7 +147,7 @@ class CollectionRuleDetailViewTestCase(TestCase):
|
||||||
rule = CollectionRuleFactory(name="BBC", user=other_user)
|
rule = CollectionRuleFactory(name="BBC", user=other_user)
|
||||||
|
|
||||||
response = self.client.patch(
|
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"}),
|
data=json.dumps({"name": "The guardian"}),
|
||||||
content_type="application/json",
|
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=False, rule=rule)
|
||||||
PostFactory.create_batch(size=20, read=True, 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()
|
data = response.json()
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 200)
|
self.assertEquals(response.status_code, 200)
|
||||||
|
|
@ -169,14 +179,18 @@ class CollectionRuleReadTestCase(TestCase):
|
||||||
|
|
||||||
PostFactory.create_batch(size=20, read=False, rule=rule)
|
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()
|
data = response.json()
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 201)
|
self.assertEquals(response.status_code, 201)
|
||||||
self.assertEquals(data["unread"], 0)
|
self.assertEquals(data["unread"], 0)
|
||||||
|
|
||||||
def test_rule_unknown(self):
|
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)
|
self.assertEquals(response.status_code, 404)
|
||||||
|
|
||||||
|
|
@ -187,7 +201,9 @@ class CollectionRuleReadTestCase(TestCase):
|
||||||
|
|
||||||
PostFactory.create_batch(size=20, read=False, rule=rule)
|
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(response.status_code, 403)
|
||||||
|
|
||||||
|
|
@ -197,7 +213,9 @@ class CollectionRuleReadTestCase(TestCase):
|
||||||
|
|
||||||
PostFactory.create_batch(size=20, read=False, rule=rule)
|
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(response.status_code, 403)
|
||||||
self.assertEquals(Post.objects.filter(read=False).count(), 20)
|
self.assertEquals(Post.objects.filter(read=False).count(), 20)
|
||||||
|
|
@ -205,7 +223,9 @@ class CollectionRuleReadTestCase(TestCase):
|
||||||
def test_get(self):
|
def test_get(self):
|
||||||
rule = CollectionRuleFactory(user=self.user)
|
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)
|
self.assertEquals(response.status_code, 405)
|
||||||
|
|
||||||
|
|
@ -213,7 +233,7 @@ class CollectionRuleReadTestCase(TestCase):
|
||||||
rule = CollectionRuleFactory(name="BBC", user=self.user)
|
rule = CollectionRuleFactory(name="BBC", user=self.user)
|
||||||
|
|
||||||
response = self.client.patch(
|
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"}),
|
data=json.dumps({"name": "Not possible"}),
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
)
|
)
|
||||||
|
|
@ -224,7 +244,7 @@ class CollectionRuleReadTestCase(TestCase):
|
||||||
rule = CollectionRuleFactory(name="BBC", user=self.user)
|
rule = CollectionRuleFactory(name="BBC", user=self.user)
|
||||||
|
|
||||||
response = self.client.put(
|
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"}),
|
data=json.dumps({"name": "Not possible"}),
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
)
|
)
|
||||||
|
|
@ -234,6 +254,8 @@ class CollectionRuleReadTestCase(TestCase):
|
||||||
def test_delete(self):
|
def test_delete(self):
|
||||||
rule = CollectionRuleFactory(user=self.user)
|
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)
|
self.assertEquals(response.status_code, 405)
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ class RuleListViewTestCase(TestCase):
|
||||||
def test_simple(self):
|
def test_simple(self):
|
||||||
CollectionRuleFactory.create_batch(size=3, user=self.user)
|
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()
|
data = response.json()
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 200)
|
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()
|
data = response.json()
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 200)
|
self.assertEquals(response.status_code, 200)
|
||||||
|
|
@ -65,7 +65,9 @@ class RuleListViewTestCase(TestCase):
|
||||||
def test_pagination_count(self):
|
def test_pagination_count(self):
|
||||||
CollectionRuleFactory.create_batch(size=80, user=self.user)
|
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()
|
data = response.json()
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 200)
|
self.assertEquals(response.status_code, 200)
|
||||||
|
|
@ -73,7 +75,7 @@ class RuleListViewTestCase(TestCase):
|
||||||
self.assertEquals(len(data["results"]), 30)
|
self.assertEquals(len(data["results"]), 30)
|
||||||
|
|
||||||
def test_empty(self):
|
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()
|
data = response.json()
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 200)
|
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}
|
data = {"name": "BBC", "url": "https://www.bbc.co.uk", "category": category.pk}
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("api:rules-list"),
|
reverse("api:news:collection:rules-list"),
|
||||||
data=json.dumps(data),
|
data=json.dumps(data),
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
)
|
)
|
||||||
|
|
@ -99,21 +101,21 @@ class RuleListViewTestCase(TestCase):
|
||||||
self.assertEquals(data["detail"], 'Method "POST" not allowed.')
|
self.assertEquals(data["detail"], 'Method "POST" not allowed.')
|
||||||
|
|
||||||
def test_patch(self):
|
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()
|
data = response.json()
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 405)
|
self.assertEquals(response.status_code, 405)
|
||||||
self.assertEquals(data["detail"], 'Method "PATCH" not allowed.')
|
self.assertEquals(data["detail"], 'Method "PATCH" not allowed.')
|
||||||
|
|
||||||
def test_put(self):
|
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()
|
data = response.json()
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 405)
|
self.assertEquals(response.status_code, 405)
|
||||||
self.assertEquals(data["detail"], 'Method "PUT" not allowed.')
|
self.assertEquals(data["detail"], 'Method "PUT" not allowed.')
|
||||||
|
|
||||||
def test_delete(self):
|
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()
|
data = response.json()
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 405)
|
self.assertEquals(response.status_code, 405)
|
||||||
|
|
@ -124,7 +126,7 @@ class RuleListViewTestCase(TestCase):
|
||||||
|
|
||||||
CollectionRuleFactory.create_batch(size=3, user=self.user)
|
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)
|
self.assertEquals(response.status_code, 403)
|
||||||
|
|
||||||
|
|
@ -132,7 +134,7 @@ class RuleListViewTestCase(TestCase):
|
||||||
other_user = UserFactory()
|
other_user = UserFactory()
|
||||||
CollectionRuleFactory.create_batch(size=3, user=other_user)
|
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()
|
data = response.json()
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 200)
|
self.assertEquals(response.status_code, 200)
|
||||||
|
|
@ -151,7 +153,7 @@ class NestedRuleListViewTestCase(TestCase):
|
||||||
PostFactory.create_batch(size=5, rule=rule)
|
PostFactory.create_batch(size=5, rule=rule)
|
||||||
|
|
||||||
response = self.client.get(
|
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()
|
data = response.json()
|
||||||
|
|
||||||
|
|
@ -166,7 +168,8 @@ class NestedRuleListViewTestCase(TestCase):
|
||||||
PostFactory.create_batch(size=80, rule=rule)
|
PostFactory.create_batch(size=80, rule=rule)
|
||||||
|
|
||||||
response = self.client.get(
|
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()
|
data = response.json()
|
||||||
|
|
||||||
|
|
@ -178,7 +181,7 @@ class NestedRuleListViewTestCase(TestCase):
|
||||||
rule = CollectionRuleFactory.create(user=self.user)
|
rule = CollectionRuleFactory.create(user=self.user)
|
||||||
|
|
||||||
response = self.client.get(
|
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()
|
data = response.json()
|
||||||
|
|
||||||
|
|
@ -187,7 +190,9 @@ class NestedRuleListViewTestCase(TestCase):
|
||||||
self.assertEquals(len(data["results"]), 0)
|
self.assertEquals(len(data["results"]), 0)
|
||||||
|
|
||||||
def test_not_known(self):
|
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)
|
self.assertEquals(response.status_code, 404)
|
||||||
|
|
||||||
|
|
@ -195,7 +200,7 @@ class NestedRuleListViewTestCase(TestCase):
|
||||||
rule = CollectionRuleFactory.create(user=self.user)
|
rule = CollectionRuleFactory.create(user=self.user)
|
||||||
|
|
||||||
response = self.client.post(
|
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({}),
|
data=json.dumps({}),
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
)
|
)
|
||||||
|
|
@ -208,7 +213,7 @@ class NestedRuleListViewTestCase(TestCase):
|
||||||
rule = CollectionRuleFactory.create(user=self.user)
|
rule = CollectionRuleFactory.create(user=self.user)
|
||||||
|
|
||||||
response = self.client.patch(
|
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({}),
|
data=json.dumps({}),
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
)
|
)
|
||||||
|
|
@ -221,7 +226,7 @@ class NestedRuleListViewTestCase(TestCase):
|
||||||
rule = CollectionRuleFactory.create(user=self.user)
|
rule = CollectionRuleFactory.create(user=self.user)
|
||||||
|
|
||||||
response = self.client.put(
|
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({}),
|
data=json.dumps({}),
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
)
|
)
|
||||||
|
|
@ -234,7 +239,7 @@ class NestedRuleListViewTestCase(TestCase):
|
||||||
rule = CollectionRuleFactory.create(user=self.user)
|
rule = CollectionRuleFactory.create(user=self.user)
|
||||||
|
|
||||||
response = self.client.delete(
|
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({}),
|
data=json.dumps({}),
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
)
|
)
|
||||||
|
|
@ -249,7 +254,7 @@ class NestedRuleListViewTestCase(TestCase):
|
||||||
rule = CollectionRuleFactory(user=self.user)
|
rule = CollectionRuleFactory(user=self.user)
|
||||||
|
|
||||||
response = self.client.get(
|
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)
|
self.assertEquals(response.status_code, 403)
|
||||||
|
|
@ -259,7 +264,7 @@ class NestedRuleListViewTestCase(TestCase):
|
||||||
rule = CollectionRuleFactory(user=other_user)
|
rule = CollectionRuleFactory(user=other_user)
|
||||||
|
|
||||||
response = self.client.get(
|
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)
|
self.assertEquals(response.status_code, 403)
|
||||||
|
|
@ -294,7 +299,7 @@ class NestedRuleListViewTestCase(TestCase):
|
||||||
]
|
]
|
||||||
|
|
||||||
response = self.client.get(
|
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()
|
data = response.json()
|
||||||
|
|
||||||
|
|
@ -315,7 +320,7 @@ class NestedRuleListViewTestCase(TestCase):
|
||||||
PostFactory.create_batch(size=5, rule=other_rule)
|
PostFactory.create_batch(size=5, rule=other_rule)
|
||||||
|
|
||||||
response = self.client.get(
|
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()
|
data = response.json()
|
||||||
|
|
||||||
|
|
@ -335,7 +340,8 @@ class NestedRuleListViewTestCase(TestCase):
|
||||||
PostFactory.create_batch(size=10, rule=rule, read=True)
|
PostFactory.create_batch(size=10, rule=rule, read=True)
|
||||||
|
|
||||||
response = self.client.get(
|
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()
|
data = response.json()
|
||||||
|
|
@ -354,7 +360,8 @@ class NestedRuleListViewTestCase(TestCase):
|
||||||
PostFactory.create_batch(size=10, rule=rule, read=True)
|
PostFactory.create_batch(size=10, rule=rule, read=True)
|
||||||
|
|
||||||
response = self.client.get(
|
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()
|
data = response.json()
|
||||||
|
|
|
||||||
|
|
@ -245,3 +245,26 @@ class FeedCollectorTestCase(TestCase):
|
||||||
self.assertEquals(
|
self.assertEquals(
|
||||||
third_post.title, "Birmingham head teacher threatened over LGBT lessons"
|
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)
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,13 @@
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from freezegun import freeze_time
|
||||||
|
|
||||||
from newsreader.news.collection.feed import FeedDuplicateHandler
|
from newsreader.news.collection.feed import FeedDuplicateHandler
|
||||||
from newsreader.news.collection.tests.factories import CollectionRuleFactory
|
from newsreader.news.collection.tests.factories import CollectionRuleFactory
|
||||||
|
from newsreader.news.core.models import Post
|
||||||
from newsreader.news.core.tests.factories import PostFactory
|
from newsreader.news.core.tests.factories import PostFactory
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -25,16 +30,57 @@ class FeedDuplicateHandlerTestCase(TestCase):
|
||||||
posts_gen = duplicate_handler.check([new_post])
|
posts_gen = duplicate_handler.check([new_post])
|
||||||
posts = list(posts_gen)
|
posts = list(posts_gen)
|
||||||
|
|
||||||
|
self.assertEquals(len(posts), 1)
|
||||||
|
|
||||||
post = posts[0]
|
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)
|
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.publication_date, new_post.publication_date)
|
||||||
self.assertTrue(post.publication_date != existing_post.publication_date)
|
self.assertEquals(post.read, False)
|
||||||
self.assertTrue(post.title != existing_post.title)
|
|
||||||
|
|
||||||
def test_duplicate_entries_in_recent_database(self):
|
def test_duplicate_entries_in_recent_database(self):
|
||||||
PostFactory.create_batch(size=10)
|
|
||||||
|
|
||||||
publication_date = timezone.now()
|
publication_date = timezone.now()
|
||||||
|
|
||||||
rule = CollectionRuleFactory()
|
rule = CollectionRuleFactory()
|
||||||
|
|
@ -43,7 +89,7 @@ class FeedDuplicateHandlerTestCase(TestCase):
|
||||||
title="Birmingham head teacher threatened over LGBT lessons",
|
title="Birmingham head teacher threatened over LGBT lessons",
|
||||||
body="Google's move to end business ties with Huawei will affect current devices",
|
body="Google's move to end business ties with Huawei will affect current devices",
|
||||||
publication_date=publication_date,
|
publication_date=publication_date,
|
||||||
remote_identifier=None,
|
remote_identifier="jabbadabadoe",
|
||||||
rule=rule,
|
rule=rule,
|
||||||
)
|
)
|
||||||
new_post = PostFactory.build(
|
new_post = PostFactory.build(
|
||||||
|
|
@ -59,10 +105,19 @@ class FeedDuplicateHandlerTestCase(TestCase):
|
||||||
posts_gen = duplicate_handler.check([new_post])
|
posts_gen = duplicate_handler.check([new_post])
|
||||||
posts = list(posts_gen)
|
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):
|
def test_multiple_existing_entries_with_identifier(self):
|
||||||
timezone.now()
|
|
||||||
rule = CollectionRuleFactory()
|
rule = CollectionRuleFactory()
|
||||||
|
|
||||||
PostFactory.create_batch(
|
PostFactory.create_batch(
|
||||||
|
|
@ -80,4 +135,56 @@ class FeedDuplicateHandlerTestCase(TestCase):
|
||||||
posts = list(posts_gen)
|
posts = list(posts_gen)
|
||||||
|
|
||||||
self.assertEquals(len(posts), 1)
|
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)
|
||||||
|
|
|
||||||
242
src/newsreader/news/collection/tests/views/test_bulk_views.py
Normal file
242
src/newsreader/news/collection/tests/views/test_bulk_views.py
Normal file
|
|
@ -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)
|
||||||
|
|
@ -1,9 +1,5 @@
|
||||||
import os
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
|
|
||||||
|
|
@ -150,135 +146,3 @@ class CollectionRuleUpdateViewTestCase(CollectionRuleViewTestCase, TestCase):
|
||||||
self.rule.refresh_from_db()
|
self.rule.refresh_from_db()
|
||||||
|
|
||||||
self.assertEquals(self.rule.category, None)
|
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)
|
|
||||||
142
src/newsreader/news/collection/tests/views/test_import_view.py
Normal file
142
src/newsreader/news/collection/tests/views/test_import_view.py
Normal file
|
|
@ -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)
|
||||||
|
|
@ -8,6 +8,9 @@ from newsreader.news.collection.endpoints import (
|
||||||
RuleReadView,
|
RuleReadView,
|
||||||
)
|
)
|
||||||
from newsreader.news.collection.views import (
|
from newsreader.news.collection.views import (
|
||||||
|
CollectionRuleBulkDeleteView,
|
||||||
|
CollectionRuleBulkDisableView,
|
||||||
|
CollectionRuleBulkEnableView,
|
||||||
CollectionRuleCreateView,
|
CollectionRuleCreateView,
|
||||||
CollectionRuleListView,
|
CollectionRuleListView,
|
||||||
CollectionRuleUpdateView,
|
CollectionRuleUpdateView,
|
||||||
|
|
@ -34,5 +37,20 @@ urlpatterns = [
|
||||||
login_required(CollectionRuleCreateView.as_view()),
|
login_required(CollectionRuleCreateView.as_view()),
|
||||||
name="rule-create",
|
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"),
|
path("rules/import/", login_required(OPMLImportView.as_view()), name="import"),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ from datetime import datetime
|
||||||
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
|
import pytz
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from requests.exceptions import RequestException
|
from requests.exceptions import RequestException
|
||||||
|
|
@ -15,7 +16,8 @@ def build_publication_date(dt, tz):
|
||||||
published_parsed = timezone.make_aware(naive_datetime, timezone=tz)
|
published_parsed = timezone.make_aware(naive_datetime, timezone=tz)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
return None, False
|
return None, False
|
||||||
return published_parsed, True
|
|
||||||
|
return published_parsed.astimezone(pytz.utc), True
|
||||||
|
|
||||||
|
|
||||||
def fetch(url):
|
def fetch(url):
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,17 @@
|
||||||
from django.contrib import messages
|
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.utils.translation import gettext_lazy as _
|
||||||
from django.views.generic.edit import CreateView, FormView, UpdateView
|
from django.views.generic.edit import CreateView, FormView, UpdateView
|
||||||
from django.views.generic.list import ListView
|
from django.views.generic.list import ListView
|
||||||
|
|
||||||
import pytz
|
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.collection.models import CollectionRule
|
||||||
from newsreader.news.core.models import Category
|
from newsreader.news.core.models import Category
|
||||||
from newsreader.utils.opml import parse_opml
|
from newsreader.utils.opml import parse_opml
|
||||||
|
|
@ -17,11 +22,11 @@ class CollectionRuleViewMixin:
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
user = self.request.user
|
user = self.request.user
|
||||||
return self.queryset.filter(user=user)
|
return self.queryset.filter(user=user).order_by("name")
|
||||||
|
|
||||||
|
|
||||||
class CollectionRuleDetailMixin:
|
class CollectionRuleDetailMixin:
|
||||||
success_url = reverse_lazy("rules")
|
success_url = reverse_lazy("news:collection:rules")
|
||||||
form_class = CollectionRuleForm
|
form_class = CollectionRuleForm
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
|
|
@ -42,27 +47,82 @@ class CollectionRuleDetailMixin:
|
||||||
|
|
||||||
|
|
||||||
class CollectionRuleListView(CollectionRuleViewMixin, ListView):
|
class CollectionRuleListView(CollectionRuleViewMixin, ListView):
|
||||||
template_name = "collection/rules.html"
|
paginate_by = 50
|
||||||
|
template_name = "news/collection/views/rules.html"
|
||||||
context_object_name = "rules"
|
context_object_name = "rules"
|
||||||
|
|
||||||
|
|
||||||
class CollectionRuleUpdateView(
|
class CollectionRuleUpdateView(
|
||||||
CollectionRuleViewMixin, CollectionRuleDetailMixin, UpdateView
|
CollectionRuleViewMixin, CollectionRuleDetailMixin, UpdateView
|
||||||
):
|
):
|
||||||
template_name = "collection/rule-update.html"
|
template_name = "news/collection/views/rule-update.html"
|
||||||
context_object_name = "rule"
|
context_object_name = "rule"
|
||||||
|
|
||||||
|
|
||||||
class CollectionRuleCreateView(
|
class CollectionRuleCreateView(
|
||||||
CollectionRuleViewMixin, CollectionRuleDetailMixin, CreateView
|
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):
|
class OPMLImportView(FormView):
|
||||||
form_class = OPMLImportForm
|
form_class = OPMLImportForm
|
||||||
success_url = reverse_lazy("rules")
|
success_url = reverse_lazy("news:collection:rules")
|
||||||
template_name = "collection/import.html"
|
template_name = "news/collection/views/import.html"
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
user = self.request.user
|
user = self.request.user
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,27 @@
|
||||||
from django import forms
|
from django import forms
|
||||||
|
from django.forms.widgets import CheckboxSelectMultiple
|
||||||
|
|
||||||
from newsreader.accounts.models import User
|
from newsreader.accounts.models import User
|
||||||
from newsreader.news.collection.models import CollectionRule
|
from newsreader.news.collection.models import CollectionRule
|
||||||
from newsreader.news.core.models import Category
|
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):
|
class CategoryForm(forms.ModelForm):
|
||||||
rules = forms.ModelMultipleChoiceField(
|
rules = forms.ModelMultipleChoiceField(
|
||||||
required=False,
|
required=False, queryset=CollectionRule.objects.none(), widget=RulesWidget
|
||||||
queryset=CollectionRule.objects.all(),
|
|
||||||
widget=forms.widgets.CheckboxSelectMultiple,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
user = forms.ModelChoiceField(
|
user = forms.ModelChoiceField(
|
||||||
|
|
@ -23,6 +35,8 @@ class CategoryForm(forms.ModelForm):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.fields["rules"].queryset = CollectionRule.objects.filter(user=self.user)
|
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.fields["user"].queryset = User.objects.filter(pk=self.user.pk)
|
||||||
|
|
||||||
self.initial["user"] = self.user
|
self.initial["user"] = self.user
|
||||||
|
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
{% extends "core/category.html" %}
|
|
||||||
|
|
||||||
{% block form-header %}
|
|
||||||
<h1 class="h1 form__title">Create a category</h1>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block confirm-button %}
|
|
||||||
<button class="button button--confirm">Create category</button>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
{% extends "core/category.html" %}
|
|
||||||
|
|
||||||
{% block form-header %}
|
|
||||||
<h1 class="h1 form__title">Update category</h1>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block confirm-button %}
|
|
||||||
<button class="button button--confirm">Save category</button>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% load static %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<main id="category--page" class="main">
|
|
||||||
<form class="form category-form" method="post">
|
|
||||||
{% csrf_token %}
|
|
||||||
|
|
||||||
<div class="form__header">
|
|
||||||
{% block form-header %}{% endblock %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{ form.non_field_errors }}
|
|
||||||
{{ form.user.errors }}
|
|
||||||
{{ form.user }}
|
|
||||||
|
|
||||||
<section class="section form__section category-form__section">
|
|
||||||
<fieldset class="form__fieldset category-form__fieldset">
|
|
||||||
<label class="label category-form__label" for="name">Name</label>
|
|
||||||
{{ form.name.errors }}
|
|
||||||
{{ form.name }}
|
|
||||||
</fieldset>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="section form__section category-form__section">
|
|
||||||
<fieldset class="form__fieldset category-form__fieldset">
|
|
||||||
<label class="label category-form__label" for="rules">Collection rules</label>
|
|
||||||
<small class="small help-text">
|
|
||||||
Note that existing assigned rules will be reassigned to this category
|
|
||||||
</small>
|
|
||||||
{{ form.rules.errors }}
|
|
||||||
|
|
||||||
<ul class="list checkbox-list">
|
|
||||||
{% for rule in rules %}
|
|
||||||
<li class="list__item checkbox-list__item">
|
|
||||||
<input class="input category-form__input" type="checkbox" name="rules"
|
|
||||||
{% if category and rule.pk in category.rule_ids %}checked{% endif %}
|
|
||||||
value="{{ rule.pk }}" />
|
|
||||||
|
|
||||||
{% if rule.favicon %}
|
|
||||||
<img class="favicon" src="{{ rule.favicon }}" />
|
|
||||||
{% else %}
|
|
||||||
<i class="gg-image"></i>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<span>{{ rule.name }}</span>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</fieldset>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="section form__section category-form__section">
|
|
||||||
<fieldset class="form__fieldset category-form__fieldset">
|
|
||||||
<a class="link button button--cancel" href="{% url 'categories' %}">Cancel</a>
|
|
||||||
{% block confirm-button %}{% endblock %}
|
|
||||||
</fieldset>
|
|
||||||
</section>
|
|
||||||
</form>
|
|
||||||
</main>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<main id="category--page" class="main">
|
||||||
|
{% 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" %}
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<main id="category--page" class="main">
|
||||||
|
{% 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" %}
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
<input class="input category-form__input" type="{{ option.type }}"
|
||||||
|
name="{{ option.name }}" value="{{ option.value|stringformat:'s' }}"{% if option.selected %} checked{% endif %} />
|
||||||
|
|
||||||
|
{% if option.instance.favicon %}
|
||||||
|
<img class="favicon" src="{{ option.instance.favicon }}" />
|
||||||
|
{% else %}
|
||||||
|
<i class="gg-image"></i>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<span>{{ option.label }}</span>
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<ul class="list checkbox-list">
|
||||||
|
{% for group, options, index in widget.optgroups %}
|
||||||
|
{% for option in options %}
|
||||||
|
<li class="list__item checkbox-list__item">
|
||||||
|
{% include "news/core/widgets/rule.html" with option=option only %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
|
@ -16,7 +16,9 @@ class CategoryDetailViewTestCase(TestCase):
|
||||||
def test_simple(self):
|
def test_simple(self):
|
||||||
category = CategoryFactory(user=self.user)
|
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()
|
data = response.json()
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 200)
|
self.assertEquals(response.status_code, 200)
|
||||||
|
|
@ -24,7 +26,9 @@ class CategoryDetailViewTestCase(TestCase):
|
||||||
self.assertTrue("name" in data)
|
self.assertTrue("name" in data)
|
||||||
|
|
||||||
def test_not_known(self):
|
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()
|
data = response.json()
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 404)
|
self.assertEquals(response.status_code, 404)
|
||||||
|
|
@ -34,7 +38,7 @@ class CategoryDetailViewTestCase(TestCase):
|
||||||
category = CategoryFactory(user=self.user)
|
category = CategoryFactory(user=self.user)
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("api:categories-detail", args=[category.pk])
|
reverse("api:news:core:categories-detail", args=[category.pk])
|
||||||
)
|
)
|
||||||
data = response.json()
|
data = response.json()
|
||||||
|
|
||||||
|
|
@ -45,7 +49,7 @@ class CategoryDetailViewTestCase(TestCase):
|
||||||
category = CategoryFactory(name="Clickbait", user=self.user)
|
category = CategoryFactory(name="Clickbait", user=self.user)
|
||||||
|
|
||||||
response = self.client.patch(
|
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"}),
|
data=json.dumps({"name": "Interesting posts"}),
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
)
|
)
|
||||||
|
|
@ -58,7 +62,7 @@ class CategoryDetailViewTestCase(TestCase):
|
||||||
category = CategoryFactory(user=self.user)
|
category = CategoryFactory(user=self.user)
|
||||||
|
|
||||||
response = self.client.patch(
|
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}),
|
data=json.dumps({"id": 44}),
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
)
|
)
|
||||||
|
|
@ -71,7 +75,7 @@ class CategoryDetailViewTestCase(TestCase):
|
||||||
category = CategoryFactory(name="Clickbait", user=self.user)
|
category = CategoryFactory(name="Clickbait", user=self.user)
|
||||||
|
|
||||||
response = self.client.put(
|
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"}),
|
data=json.dumps({"name": "Interesting posts"}),
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
)
|
)
|
||||||
|
|
@ -84,7 +88,7 @@ class CategoryDetailViewTestCase(TestCase):
|
||||||
category = CategoryFactory(user=self.user)
|
category = CategoryFactory(user=self.user)
|
||||||
|
|
||||||
response = self.client.delete(
|
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)
|
self.assertEquals(response.status_code, 204)
|
||||||
|
|
@ -94,7 +98,9 @@ class CategoryDetailViewTestCase(TestCase):
|
||||||
|
|
||||||
category = CategoryFactory(user=self.user)
|
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)
|
self.assertEquals(response.status_code, 403)
|
||||||
|
|
||||||
|
|
@ -102,7 +108,9 @@ class CategoryDetailViewTestCase(TestCase):
|
||||||
other_user = UserFactory()
|
other_user = UserFactory()
|
||||||
category = CategoryFactory(user=other_user)
|
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)
|
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=False, rule=unread_rule)
|
||||||
PostFactory.create_batch(size=20, read=True, rule=read_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()
|
data = response.json()
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 200)
|
self.assertEquals(response.status_code, 200)
|
||||||
|
|
@ -133,7 +143,9 @@ class CategoryReadTestCase(TestCase):
|
||||||
for rule in CollectionRuleFactory.create_batch(size=5, category=category)
|
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()
|
data = response.json()
|
||||||
|
|
||||||
|
|
@ -142,7 +154,9 @@ class CategoryReadTestCase(TestCase):
|
||||||
self.assertEquals(data["id"], category.pk)
|
self.assertEquals(data["id"], category.pk)
|
||||||
|
|
||||||
def test_category_unknown(self):
|
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)
|
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)
|
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)
|
self.assertEquals(response.status_code, 403)
|
||||||
|
|
||||||
def test_get(self):
|
def test_get(self):
|
||||||
category = CategoryFactory(name="Clickbait", user=self.user)
|
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)
|
self.assertEquals(response.status_code, 405)
|
||||||
|
|
||||||
|
|
@ -187,7 +207,7 @@ class CategoryReadTestCase(TestCase):
|
||||||
category = CategoryFactory(name="Clickbait", user=self.user)
|
category = CategoryFactory(name="Clickbait", user=self.user)
|
||||||
|
|
||||||
response = self.client.patch(
|
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"}),
|
data=json.dumps({"name": "Not possible"}),
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
)
|
)
|
||||||
|
|
@ -198,7 +218,7 @@ class CategoryReadTestCase(TestCase):
|
||||||
category = CategoryFactory(name="Clickbait", user=self.user)
|
category = CategoryFactory(name="Clickbait", user=self.user)
|
||||||
|
|
||||||
response = self.client.put(
|
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"}),
|
data=json.dumps({"name": "Not possible"}),
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
)
|
)
|
||||||
|
|
@ -209,7 +229,7 @@ class CategoryReadTestCase(TestCase):
|
||||||
category = CategoryFactory(name="Clickbait", user=self.user)
|
category = CategoryFactory(name="Clickbait", user=self.user)
|
||||||
|
|
||||||
response = self.client.delete(
|
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)
|
self.assertEquals(response.status_code, 405)
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ class CategoryListViewTestCase(TestCase):
|
||||||
def test_simple(self):
|
def test_simple(self):
|
||||||
CategoryFactory.create_batch(size=3, user=self.user)
|
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()
|
data = response.json()
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 200)
|
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()
|
data = response.json()
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 200)
|
self.assertEquals(response.status_code, 200)
|
||||||
|
|
@ -58,7 +58,7 @@ class CategoryListViewTestCase(TestCase):
|
||||||
self.assertEquals(data[2]["id"], categories[0].pk)
|
self.assertEquals(data[2]["id"], categories[0].pk)
|
||||||
|
|
||||||
def test_empty(self):
|
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()
|
data = response.json()
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 200)
|
self.assertEquals(response.status_code, 200)
|
||||||
|
|
@ -68,7 +68,7 @@ class CategoryListViewTestCase(TestCase):
|
||||||
data = {"name": "Tech"}
|
data = {"name": "Tech"}
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("api:categories-list"),
|
reverse("api:news:core:categories-list"),
|
||||||
data=json.dumps(data),
|
data=json.dumps(data),
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
)
|
)
|
||||||
|
|
@ -78,21 +78,21 @@ class CategoryListViewTestCase(TestCase):
|
||||||
self.assertEquals(response_data["detail"], 'Method "POST" not allowed.')
|
self.assertEquals(response_data["detail"], 'Method "POST" not allowed.')
|
||||||
|
|
||||||
def test_patch(self):
|
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()
|
data = response.json()
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 405)
|
self.assertEquals(response.status_code, 405)
|
||||||
self.assertEquals(data["detail"], 'Method "PATCH" not allowed.')
|
self.assertEquals(data["detail"], 'Method "PATCH" not allowed.')
|
||||||
|
|
||||||
def test_put(self):
|
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()
|
data = response.json()
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 405)
|
self.assertEquals(response.status_code, 405)
|
||||||
self.assertEquals(data["detail"], 'Method "PUT" not allowed.')
|
self.assertEquals(data["detail"], 'Method "PUT" not allowed.')
|
||||||
|
|
||||||
def test_delete(self):
|
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()
|
data = response.json()
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 405)
|
self.assertEquals(response.status_code, 405)
|
||||||
|
|
@ -103,7 +103,7 @@ class CategoryListViewTestCase(TestCase):
|
||||||
|
|
||||||
CategoryFactory.create_batch(size=3, user=self.user)
|
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)
|
self.assertEquals(response.status_code, 403)
|
||||||
|
|
||||||
|
|
@ -111,7 +111,7 @@ class CategoryListViewTestCase(TestCase):
|
||||||
other_user = UserFactory()
|
other_user = UserFactory()
|
||||||
CategoryFactory.create_batch(size=3, user=other_user)
|
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()
|
data = response.json()
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 200)
|
self.assertEquals(response.status_code, 200)
|
||||||
|
|
@ -128,7 +128,7 @@ class NestedCategoryListViewTestCase(TestCase):
|
||||||
rules = CollectionRuleFactory.create_batch(size=5, category=category)
|
rules = CollectionRuleFactory.create_batch(size=5, category=category)
|
||||||
|
|
||||||
response = self.client.get(
|
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()
|
data = response.json()
|
||||||
|
|
||||||
|
|
@ -145,7 +145,7 @@ class NestedCategoryListViewTestCase(TestCase):
|
||||||
category = CategoryFactory.create(user=self.user)
|
category = CategoryFactory.create(user=self.user)
|
||||||
|
|
||||||
response = self.client.get(
|
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()
|
data = response.json()
|
||||||
|
|
||||||
|
|
@ -155,14 +155,14 @@ class NestedCategoryListViewTestCase(TestCase):
|
||||||
|
|
||||||
def test_not_known(self):
|
def test_not_known(self):
|
||||||
response = self.client.get(
|
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)
|
self.assertEquals(response.status_code, 404)
|
||||||
|
|
||||||
def test_post(self):
|
def test_post(self):
|
||||||
response = self.client.post(
|
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({}),
|
data=json.dumps({}),
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
)
|
)
|
||||||
|
|
@ -175,7 +175,9 @@ class NestedCategoryListViewTestCase(TestCase):
|
||||||
category = CategoryFactory.create(user=self.user)
|
category = CategoryFactory.create(user=self.user)
|
||||||
|
|
||||||
response = self.client.patch(
|
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"}),
|
data=json.dumps({"name": "test"}),
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
)
|
)
|
||||||
|
|
@ -188,7 +190,9 @@ class NestedCategoryListViewTestCase(TestCase):
|
||||||
category = CategoryFactory.create(user=self.user)
|
category = CategoryFactory.create(user=self.user)
|
||||||
|
|
||||||
response = self.client.put(
|
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"}),
|
data=json.dumps({"name": "test"}),
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
)
|
)
|
||||||
|
|
@ -201,7 +205,9 @@ class NestedCategoryListViewTestCase(TestCase):
|
||||||
category = CategoryFactory.create(user=self.user)
|
category = CategoryFactory.create(user=self.user)
|
||||||
|
|
||||||
response = self.client.delete(
|
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",
|
content_type="application/json",
|
||||||
)
|
)
|
||||||
data = response.json()
|
data = response.json()
|
||||||
|
|
@ -216,7 +222,7 @@ class NestedCategoryListViewTestCase(TestCase):
|
||||||
rules = CollectionRuleFactory.create_batch(size=5, category=category)
|
rules = CollectionRuleFactory.create_batch(size=5, category=category)
|
||||||
|
|
||||||
response = self.client.get(
|
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)
|
self.assertEquals(response.status_code, 403)
|
||||||
|
|
@ -228,7 +234,7 @@ class NestedCategoryListViewTestCase(TestCase):
|
||||||
rules = CollectionRuleFactory.create_batch(size=5, category=category)
|
rules = CollectionRuleFactory.create_batch(size=5, category=category)
|
||||||
|
|
||||||
response = self.client.get(
|
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)
|
self.assertEquals(response.status_code, 403)
|
||||||
|
|
@ -242,7 +248,7 @@ class NestedCategoryListViewTestCase(TestCase):
|
||||||
]
|
]
|
||||||
|
|
||||||
response = self.client.get(
|
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()
|
data = response.json()
|
||||||
|
|
||||||
|
|
@ -265,7 +271,7 @@ class NestedCategoryListViewTestCase(TestCase):
|
||||||
]
|
]
|
||||||
|
|
||||||
response = self.client.get(
|
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()
|
data = response.json()
|
||||||
|
|
||||||
|
|
@ -292,7 +298,7 @@ class NestedCategoryPostView(TestCase):
|
||||||
}
|
}
|
||||||
|
|
||||||
response = self.client.get(
|
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()
|
data = response.json()
|
||||||
posts = data["results"]
|
posts = data["results"]
|
||||||
|
|
@ -310,7 +316,7 @@ class NestedCategoryPostView(TestCase):
|
||||||
category = CategoryFactory.create(user=self.user)
|
category = CategoryFactory.create(user=self.user)
|
||||||
|
|
||||||
response = self.client.get(
|
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()
|
data = response.json()
|
||||||
posts = data["results"]
|
posts = data["results"]
|
||||||
|
|
@ -326,7 +332,7 @@ class NestedCategoryPostView(TestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
response = self.client.get(
|
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()
|
data = response.json()
|
||||||
posts = data["results"]
|
posts = data["results"]
|
||||||
|
|
@ -337,14 +343,14 @@ class NestedCategoryPostView(TestCase):
|
||||||
|
|
||||||
def test_not_known(self):
|
def test_not_known(self):
|
||||||
response = self.client.get(
|
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)
|
self.assertEquals(response.status_code, 404)
|
||||||
|
|
||||||
def test_post(self):
|
def test_post(self):
|
||||||
response = self.client.post(
|
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({}),
|
data=json.dumps({}),
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
)
|
)
|
||||||
|
|
@ -357,7 +363,9 @@ class NestedCategoryPostView(TestCase):
|
||||||
category = CategoryFactory.create(user=self.user)
|
category = CategoryFactory.create(user=self.user)
|
||||||
|
|
||||||
response = self.client.patch(
|
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({}),
|
data=json.dumps({}),
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
)
|
)
|
||||||
|
|
@ -370,7 +378,9 @@ class NestedCategoryPostView(TestCase):
|
||||||
category = CategoryFactory.create(user=self.user)
|
category = CategoryFactory.create(user=self.user)
|
||||||
|
|
||||||
response = self.client.put(
|
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({}),
|
data=json.dumps({}),
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
)
|
)
|
||||||
|
|
@ -383,7 +393,9 @@ class NestedCategoryPostView(TestCase):
|
||||||
category = CategoryFactory.create(user=self.user)
|
category = CategoryFactory.create(user=self.user)
|
||||||
|
|
||||||
response = self.client.delete(
|
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",
|
content_type="application/json",
|
||||||
)
|
)
|
||||||
data = response.json()
|
data = response.json()
|
||||||
|
|
@ -397,7 +409,7 @@ class NestedCategoryPostView(TestCase):
|
||||||
category = CategoryFactory.create(user=self.user)
|
category = CategoryFactory.create(user=self.user)
|
||||||
|
|
||||||
response = self.client.get(
|
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)
|
self.assertEquals(response.status_code, 403)
|
||||||
|
|
@ -407,7 +419,7 @@ class NestedCategoryPostView(TestCase):
|
||||||
category = CategoryFactory.create(user=other_user)
|
category = CategoryFactory.create(user=other_user)
|
||||||
|
|
||||||
response = self.client.get(
|
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)
|
self.assertEquals(response.status_code, 403)
|
||||||
|
|
@ -477,7 +489,7 @@ class NestedCategoryPostView(TestCase):
|
||||||
]
|
]
|
||||||
|
|
||||||
response = self.client.get(
|
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()
|
data = response.json()
|
||||||
posts = data["results"]
|
posts = data["results"]
|
||||||
|
|
@ -514,7 +526,7 @@ class NestedCategoryPostView(TestCase):
|
||||||
]
|
]
|
||||||
|
|
||||||
response = self.client.get(
|
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()
|
data = response.json()
|
||||||
posts = data["results"]
|
posts = data["results"]
|
||||||
|
|
@ -533,7 +545,9 @@ class NestedCategoryPostView(TestCase):
|
||||||
PostFactory.create_batch(size=10, rule=rule, read=True)
|
PostFactory.create_batch(size=10, rule=rule, read=True)
|
||||||
|
|
||||||
response = self.client.get(
|
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"},
|
{"read": "false"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -554,7 +568,9 @@ class NestedCategoryPostView(TestCase):
|
||||||
PostFactory.create_batch(size=10, rule=rule, read=True)
|
PostFactory.create_batch(size=10, rule=rule, read=True)
|
||||||
|
|
||||||
response = self.client.get(
|
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"},
|
{"read": "true"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,9 @@ class PostDetailViewTestCase(TestCase):
|
||||||
)
|
)
|
||||||
post = PostFactory(rule=rule)
|
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()
|
data = response.json()
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 200)
|
self.assertEquals(response.status_code, 200)
|
||||||
|
|
@ -34,7 +36,7 @@ class PostDetailViewTestCase(TestCase):
|
||||||
self.assertTrue("remoteIdentifier" in data)
|
self.assertTrue("remoteIdentifier" in data)
|
||||||
|
|
||||||
def test_not_known(self):
|
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()
|
data = response.json()
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 404)
|
self.assertEquals(response.status_code, 404)
|
||||||
|
|
@ -46,7 +48,9 @@ class PostDetailViewTestCase(TestCase):
|
||||||
)
|
)
|
||||||
post = PostFactory(rule=rule)
|
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()
|
data = response.json()
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 405)
|
self.assertEquals(response.status_code, 405)
|
||||||
|
|
@ -59,7 +63,7 @@ class PostDetailViewTestCase(TestCase):
|
||||||
post = PostFactory(title="This is clickbait for sure", rule=rule)
|
post = PostFactory(title="This is clickbait for sure", rule=rule)
|
||||||
|
|
||||||
response = self.client.patch(
|
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"}),
|
data=json.dumps({"title": "This title is very accurate"}),
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
)
|
)
|
||||||
|
|
@ -75,7 +79,7 @@ class PostDetailViewTestCase(TestCase):
|
||||||
post = PostFactory(title="This is clickbait for sure", rule=rule)
|
post = PostFactory(title="This is clickbait for sure", rule=rule)
|
||||||
|
|
||||||
response = self.client.patch(
|
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}),
|
data=json.dumps({"id": 44}),
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
)
|
)
|
||||||
|
|
@ -94,8 +98,14 @@ class PostDetailViewTestCase(TestCase):
|
||||||
post = PostFactory(title="This is clickbait for sure", rule=rule)
|
post = PostFactory(title="This is clickbait for sure", rule=rule)
|
||||||
|
|
||||||
response = self.client.patch(
|
response = self.client.patch(
|
||||||
reverse("api:posts-detail", args=[post.pk]),
|
reverse("api:news:core:posts-detail", args=[post.pk]),
|
||||||
data=json.dumps({"rule": reverse("api:rules-detail", args=[new_rule.pk])}),
|
data=json.dumps(
|
||||||
|
{
|
||||||
|
"rule": reverse(
|
||||||
|
"api:news:collection:rules-detail", args=[new_rule.pk]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
),
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
)
|
)
|
||||||
data = response.json()
|
data = response.json()
|
||||||
|
|
@ -111,7 +121,7 @@ class PostDetailViewTestCase(TestCase):
|
||||||
post = PostFactory(title="This is clickbait for sure", rule=rule)
|
post = PostFactory(title="This is clickbait for sure", rule=rule)
|
||||||
|
|
||||||
response = self.client.put(
|
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"}),
|
data=json.dumps({"title": "This title is very accurate"}),
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
)
|
)
|
||||||
|
|
@ -126,7 +136,9 @@ class PostDetailViewTestCase(TestCase):
|
||||||
)
|
)
|
||||||
post = PostFactory(rule=rule)
|
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()
|
data = response.json()
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 405)
|
self.assertEquals(response.status_code, 405)
|
||||||
|
|
@ -138,7 +150,9 @@ class PostDetailViewTestCase(TestCase):
|
||||||
rule = CollectionRuleFactory(user=self.user, category=None)
|
rule = CollectionRuleFactory(user=self.user, category=None)
|
||||||
post = PostFactory(rule=rule)
|
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)
|
self.assertEquals(response.status_code, 403)
|
||||||
|
|
||||||
|
|
@ -150,7 +164,9 @@ class PostDetailViewTestCase(TestCase):
|
||||||
)
|
)
|
||||||
post = PostFactory(rule=rule)
|
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)
|
self.assertEquals(response.status_code, 403)
|
||||||
|
|
||||||
|
|
@ -159,7 +175,9 @@ class PostDetailViewTestCase(TestCase):
|
||||||
rule = CollectionRuleFactory(user=other_user, category=None)
|
rule = CollectionRuleFactory(user=other_user, category=None)
|
||||||
post = PostFactory(rule=rule)
|
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)
|
self.assertEquals(response.status_code, 403)
|
||||||
|
|
||||||
|
|
@ -170,7 +188,9 @@ class PostDetailViewTestCase(TestCase):
|
||||||
)
|
)
|
||||||
post = PostFactory(rule=rule)
|
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)
|
self.assertEquals(response.status_code, 403)
|
||||||
|
|
||||||
|
|
@ -181,7 +201,9 @@ class PostDetailViewTestCase(TestCase):
|
||||||
)
|
)
|
||||||
post = PostFactory(rule=rule)
|
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)
|
self.assertEquals(response.status_code, 403)
|
||||||
|
|
||||||
|
|
@ -192,7 +214,7 @@ class PostDetailViewTestCase(TestCase):
|
||||||
post = PostFactory(rule=rule, read=False)
|
post = PostFactory(rule=rule, read=False)
|
||||||
|
|
||||||
response = self.client.patch(
|
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}),
|
data=json.dumps({"read": True}),
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
)
|
)
|
||||||
|
|
@ -208,7 +230,7 @@ class PostDetailViewTestCase(TestCase):
|
||||||
post = PostFactory(rule=rule, read=True)
|
post = PostFactory(rule=rule, read=True)
|
||||||
|
|
||||||
response = self.client.patch(
|
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}),
|
data=json.dumps({"read": False}),
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ class PostListViewTestCase(TestCase):
|
||||||
)
|
)
|
||||||
PostFactory.create_batch(size=3, rule=rule)
|
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()
|
data = response.json()
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 200)
|
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()
|
data = response.json()
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 200)
|
self.assertEquals(response.status_code, 200)
|
||||||
|
|
@ -77,7 +77,7 @@ class PostListViewTestCase(TestCase):
|
||||||
PostFactory.create_batch(size=80, rule=rule)
|
PostFactory.create_batch(size=80, rule=rule)
|
||||||
page_size = 50
|
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()
|
data = response.json()
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 200)
|
self.assertEquals(response.status_code, 200)
|
||||||
|
|
@ -85,7 +85,7 @@ class PostListViewTestCase(TestCase):
|
||||||
self.assertEquals(len(data["results"]), page_size)
|
self.assertEquals(len(data["results"]), page_size)
|
||||||
|
|
||||||
def test_empty(self):
|
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()
|
data = response.json()
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 200)
|
self.assertEquals(response.status_code, 200)
|
||||||
|
|
@ -96,28 +96,28 @@ class PostListViewTestCase(TestCase):
|
||||||
self.assertEquals(len(data["results"]), 0)
|
self.assertEquals(len(data["results"]), 0)
|
||||||
|
|
||||||
def test_post(self):
|
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()
|
data = response.json()
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 405)
|
self.assertEquals(response.status_code, 405)
|
||||||
self.assertEquals(data["detail"], 'Method "POST" not allowed.')
|
self.assertEquals(data["detail"], 'Method "POST" not allowed.')
|
||||||
|
|
||||||
def test_patch(self):
|
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()
|
data = response.json()
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 405)
|
self.assertEquals(response.status_code, 405)
|
||||||
self.assertEquals(data["detail"], 'Method "PATCH" not allowed.')
|
self.assertEquals(data["detail"], 'Method "PATCH" not allowed.')
|
||||||
|
|
||||||
def test_put(self):
|
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()
|
data = response.json()
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 405)
|
self.assertEquals(response.status_code, 405)
|
||||||
self.assertEquals(data["detail"], 'Method "PUT" not allowed.')
|
self.assertEquals(data["detail"], 'Method "PUT" not allowed.')
|
||||||
|
|
||||||
def test_delete(self):
|
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()
|
data = response.json()
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 405)
|
self.assertEquals(response.status_code, 405)
|
||||||
|
|
@ -128,7 +128,7 @@ class PostListViewTestCase(TestCase):
|
||||||
|
|
||||||
PostFactory.create_batch(size=3, rule=CollectionRuleFactory(user=self.user))
|
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)
|
self.assertEquals(response.status_code, 403)
|
||||||
|
|
||||||
|
|
@ -141,7 +141,7 @@ class PostListViewTestCase(TestCase):
|
||||||
size=3, rule=CollectionRuleFactory(user=self.user, category=category)
|
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)
|
self.assertEquals(response.status_code, 403)
|
||||||
|
|
||||||
|
|
@ -151,7 +151,7 @@ class PostListViewTestCase(TestCase):
|
||||||
rule = CollectionRuleFactory(user=other_user, category=None)
|
rule = CollectionRuleFactory(user=other_user, category=None)
|
||||||
PostFactory.create_batch(size=3, rule=rule)
|
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()
|
data = response.json()
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 200)
|
self.assertEquals(response.status_code, 200)
|
||||||
|
|
@ -166,7 +166,7 @@ class PostListViewTestCase(TestCase):
|
||||||
size=3, rule=CollectionRuleFactory(user=other_user, category=category)
|
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()
|
data = response.json()
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 200)
|
self.assertEquals(response.status_code, 200)
|
||||||
|
|
@ -183,7 +183,7 @@ class PostListViewTestCase(TestCase):
|
||||||
)
|
)
|
||||||
PostFactory.create_batch(size=3, rule=rule)
|
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()
|
data = response.json()
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 200)
|
self.assertEquals(response.status_code, 200)
|
||||||
|
|
@ -195,7 +195,7 @@ class PostListViewTestCase(TestCase):
|
||||||
rule = CollectionRuleFactory(user=self.user, category=None)
|
rule = CollectionRuleFactory(user=self.user, category=None)
|
||||||
PostFactory.create_batch(size=3, rule=rule)
|
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()
|
data = response.json()
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 200)
|
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=False)
|
||||||
PostFactory.create_batch(size=10, rule=rule, read=True)
|
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()
|
data = response.json()
|
||||||
posts = data["results"]
|
posts = data["results"]
|
||||||
|
|
@ -230,7 +232,9 @@ class PostListViewTestCase(TestCase):
|
||||||
PostFactory.create_batch(size=20, rule=rule, read=False)
|
PostFactory.create_batch(size=20, rule=rule, read=False)
|
||||||
PostFactory.create_batch(size=10, rule=rule, read=True)
|
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()
|
data = response.json()
|
||||||
posts = data["results"]
|
posts = data["results"]
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ class CategoryCreateViewTestCase(CategoryViewTestCase, TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
self.url = reverse("category-create")
|
self.url = reverse("news:core:category-create")
|
||||||
|
|
||||||
def test_creation(self):
|
def test_creation(self):
|
||||||
rules = CollectionRuleFactory.create_batch(size=4, user=self.user)
|
rules = CollectionRuleFactory.create_batch(size=4, user=self.user)
|
||||||
|
|
@ -88,7 +88,7 @@ class CategoryUpdateViewTestCase(CategoryViewTestCase, TestCase):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
self.category = CategoryFactory(name="category", user=self.user)
|
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):
|
def test_name_change(self):
|
||||||
data = {"name": "durp", "user": self.user.pk}
|
data = {"name": "durp", "user": self.user.pk}
|
||||||
|
|
@ -172,7 +172,7 @@ class CategoryUpdateViewTestCase(CategoryViewTestCase, TestCase):
|
||||||
other_category.rules.set([*other_rules])
|
other_category.rules.set([*other_rules])
|
||||||
|
|
||||||
data = {"name": "durp", "user": other_user.pk}
|
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)
|
response = self.client.post(other_url, data)
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 404)
|
self.assertEquals(response.status_code, 404)
|
||||||
|
|
@ -218,7 +218,7 @@ class CategoryUpdateViewTestCase(CategoryViewTestCase, TestCase):
|
||||||
def test_unique_together(self):
|
def test_unique_together(self):
|
||||||
other_category = CategoryFactory(name="other category", user=self.user)
|
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": []}
|
data = {"name": "category", "user": self.user.pk, "rules": []}
|
||||||
response = self.client.post(url, data)
|
response = self.client.post(url, data)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ from newsreader.news.core.views import (
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", login_required(NewsView.as_view()), name="index"),
|
|
||||||
path("categories/", login_required(CategoryListView.as_view()), name="categories"),
|
path("categories/", login_required(CategoryListView.as_view()), name="categories"),
|
||||||
path(
|
path(
|
||||||
"categories/<int:pk>/",
|
"categories/<int:pk>/",
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ from newsreader.news.core.models import Category
|
||||||
|
|
||||||
|
|
||||||
class NewsView(TemplateView):
|
class NewsView(TemplateView):
|
||||||
template_name = "core/homepage.html"
|
template_name = "news/core/views/homepage.html"
|
||||||
|
|
||||||
# TODO serialize objects to show filled main page
|
# TODO serialize objects to show filled main page
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
|
|
@ -39,7 +39,7 @@ class CategoryViewMixin:
|
||||||
|
|
||||||
|
|
||||||
class CategoryDetailMixin:
|
class CategoryDetailMixin:
|
||||||
success_url = reverse_lazy("categories")
|
success_url = reverse_lazy("news:core:categories")
|
||||||
form_class = CategoryForm
|
form_class = CategoryForm
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
|
|
@ -55,14 +55,14 @@ class CategoryDetailMixin:
|
||||||
|
|
||||||
|
|
||||||
class CategoryListView(CategoryViewMixin, ListView):
|
class CategoryListView(CategoryViewMixin, ListView):
|
||||||
template_name = "core/categories.html"
|
template_name = "news/core/views/categories.html"
|
||||||
context_object_name = "categories"
|
context_object_name = "categories"
|
||||||
|
|
||||||
|
|
||||||
class CategoryUpdateView(CategoryViewMixin, CategoryDetailMixin, UpdateView):
|
class CategoryUpdateView(CategoryViewMixin, CategoryDetailMixin, UpdateView):
|
||||||
template_name = "core/category-update.html"
|
template_name = "news/core/views/category-update.html"
|
||||||
context_object_name = "category"
|
context_object_name = "category"
|
||||||
|
|
||||||
|
|
||||||
class CategoryCreateView(CategoryViewMixin, CategoryDetailMixin, CreateView):
|
class CategoryCreateView(CategoryViewMixin, CategoryDetailMixin, CreateView):
|
||||||
template_name = "core/category-create.html"
|
template_name = "news/core/views/category-create.html"
|
||||||
|
|
|
||||||
19
src/newsreader/news/urls.py
Normal file
19
src/newsreader/news/urls.py
Normal file
|
|
@ -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"))),
|
||||||
|
]
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
@import "mixin.scss";
|
||||||
|
|
||||||
.form {
|
.form {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
@ -8,6 +10,29 @@
|
||||||
font-family: $form-font;
|
font-family: $form-font;
|
||||||
background-color: $white;
|
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 {
|
&__fieldset {
|
||||||
@extend .fieldset;
|
@extend .fieldset;
|
||||||
}
|
}
|
||||||
|
|
@ -16,13 +41,24 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
||||||
padding: 15px;
|
@include form-padding;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
@include form-padding;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__title {
|
&__title {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__intro {
|
||||||
|
@include form-padding;
|
||||||
|
}
|
||||||
|
|
||||||
& .favicon {
|
& .favicon {
|
||||||
height: 20px;
|
height: 20px;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
3
src/newsreader/scss/components/form/_mixin.scss
Normal file
3
src/newsreader/scss/components/form/_mixin.scss
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
@mixin form-padding {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
.password-reset-confirm-form {
|
|
||||||
margin: 20px 0;
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
5
src/newsreader/scss/components/form/_rules-form.scss
Normal file
5
src/newsreader/scss/components/form/_rules-form.scss
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
.rules-form {
|
||||||
|
@extend .form;
|
||||||
|
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
|
@ -1,12 +1,3 @@
|
||||||
@import "form";
|
@import "form";
|
||||||
|
|
||||||
@import "category-form";
|
@import "rules-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";
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,9 @@
|
||||||
@import "section/index";
|
@import "section/index";
|
||||||
@import "errorlist/index";
|
@import "errorlist/index";
|
||||||
@import "fieldset/index";
|
@import "fieldset/index";
|
||||||
|
@import "pagination/index";
|
||||||
@import "sidebar/index";
|
@import "sidebar/index";
|
||||||
|
@import "table/index";
|
||||||
|
|
||||||
@import "rules/index";
|
@import "rules/index";
|
||||||
@import "category/index";
|
@import "category/index";
|
||||||
|
|
|
||||||
18
src/newsreader/scss/components/pagination/_pagination.scss
Normal file
18
src/newsreader/scss/components/pagination/_pagination.scss
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src/newsreader/scss/components/pagination/index.scss
Normal file
1
src/newsreader/scss/components/pagination/index.scss
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
@import "pagination";
|
||||||
11
src/newsreader/scss/components/section/_text-section.scss
Normal file
11
src/newsreader/scss/components/section/_text-section.scss
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
.text-section {
|
||||||
|
@extend .section;
|
||||||
|
|
||||||
|
width: 70%;
|
||||||
|
border-radius: 5px;
|
||||||
|
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
background-color: $white;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -1 +1,2 @@
|
||||||
@import "section";
|
@import "section";
|
||||||
|
@import "text-section";
|
||||||
|
|
|
||||||
38
src/newsreader/scss/components/table/_rules-table.scss
Normal file
38
src/newsreader/scss/components/table/_rules-table.scss
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src/newsreader/scss/components/table/_table.scss
Normal file
32
src/newsreader/scss/components/table/_table.scss
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
2
src/newsreader/scss/components/table/index.scss
Normal file
2
src/newsreader/scss/components/table/index.scss
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
@import "table";
|
||||||
|
@import "rules-table";
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
|
@import "mixins";
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
padding: 10px 50px;
|
@include button-padding;
|
||||||
|
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
|
|
|
||||||
3
src/newsreader/scss/elements/button/_mixins.scss
Normal file
3
src/newsreader/scss/elements/button/_mixins.scss
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
@mixin button-padding {
|
||||||
|
padding: 7px 40px;
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
|
@import "badge/index";
|
||||||
@import "button/index";
|
@import "button/index";
|
||||||
|
@import "help-text/index";
|
||||||
|
@import "input/index";
|
||||||
|
@import "label/index";
|
||||||
@import "link/index";
|
@import "link/index";
|
||||||
@import "h1/index";
|
@import "h1/index";
|
||||||
@import "h2/index";
|
@import "h2/index";
|
||||||
@import "h3/index";
|
@import "h3/index";
|
||||||
@import "small/index";
|
@import "small/index";
|
||||||
@import "input/index";
|
@import "select/index";
|
||||||
@import "label/index";
|
|
||||||
@import "help-text/index";
|
|
||||||
@import "badge/index";
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,15 @@
|
||||||
&:focus {
|
&:focus {
|
||||||
border: 1px $focus-blue solid;
|
border: 1px $focus-blue solid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&[type="file"] {
|
||||||
|
width: 40%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[type="checkbox"] {
|
||||||
|
align-self: flex-start;
|
||||||
|
margin: 0 0 0 10px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input {
|
input {
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,3 @@
|
||||||
a {
|
a {
|
||||||
@extend .link;
|
@extend .link;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gg-link {
|
|
||||||
color: initial;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
13
src/newsreader/scss/elements/select/_select.scss
Normal file
13
src/newsreader/scss/elements/select/_select.scss
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
.select {
|
||||||
|
max-height: 200px;
|
||||||
|
|
||||||
|
&:not([size]){
|
||||||
|
width: 40%;
|
||||||
|
}
|
||||||
|
|
||||||
|
padding: 0 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
@extend .select;
|
||||||
|
}
|
||||||
1
src/newsreader/scss/elements/select/index.scss
Normal file
1
src/newsreader/scss/elements/select/index.scss
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
@import "select";
|
||||||
|
|
@ -1 +1,9 @@
|
||||||
@import "~css.gg/icons-scss/icons";
|
@import "~css.gg/icons-scss/icons";
|
||||||
|
|
||||||
|
.gg-link {
|
||||||
|
color: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gg-pen {
|
||||||
|
color: initial;
|
||||||
|
}
|
||||||
|
|
|
||||||
3
src/newsreader/scss/lib/_mixins.scss
Normal file
3
src/newsreader/scss/lib/_mixins.scss
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
@mixin rounded {
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
@ -8,5 +8,7 @@
|
||||||
@import "password-reset/index";
|
@import "password-reset/index";
|
||||||
@import "register/index";
|
@import "register/index";
|
||||||
|
|
||||||
@import "rules/index";
|
|
||||||
@import "rule/index";
|
@import "rule/index";
|
||||||
|
@import "rules/index";
|
||||||
|
|
||||||
|
@import "settings/index";
|
||||||
|
|
|
||||||
|
|
@ -3,4 +3,29 @@
|
||||||
width: 50%;
|
width: 50%;
|
||||||
|
|
||||||
border-radius: 4px;
|
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 {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue