Refactor endpoint tests

Replace force_login calls with login call from client class in setUp
This commit is contained in:
sonny 2020-05-23 16:58:42 +02:00
parent 428cd39d13
commit 6a4f33c182
135 changed files with 4315 additions and 1336 deletions

View file

@ -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

View file

@ -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
View file

@ -0,0 +1,9 @@
FROM node:12
WORKDIR /app
COPY package.json package-lock.json /app/
RUN npm install
COPY . /app/

View file

@ -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

View file

@ -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)

View 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")

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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 %}

View 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")

View file

@ -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"),
] ]

View file

@ -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}

View file

@ -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"

View file

@ -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

View 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;

View file

@ -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';

View file

@ -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>
</> </>

View file

@ -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

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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);
} }

View file

@ -0,0 +1,5 @@
from django.apps import AppConfig
class NewsConfig(AppConfig):
name = "news"

View file

@ -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:

View file

@ -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):
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 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 is_duplicate(self, existing_post, values): def in_time_slot(self, instance, existing_post):
for key, value in values.items(): time_delta_slot = timedelta(minutes=self.time_slot_minutes)
existing_value = getattr(existing_post, key, None)
if existing_value != value:
return False
time_difference = instance.publication_date - existing_post.publication_date
if time_difference <= time_delta_slot:
return True return True
def handle_duplicate(self, instance): def is_duplicate(self, existing_post, values):
return all(
getattr(existing_post, field, None) == value
for field, value in values.items()
)
def handle_duplicate_identifier(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

View file

@ -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)

View file

@ -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"
),
)
]

View file

@ -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",

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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)

View file

@ -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()

View file

@ -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)

View file

@ -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)

View 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)

View file

@ -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)

View 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)

View file

@ -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"),
] ]

View file

@ -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):

View file

@ -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

View file

@ -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

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -1,5 +1,4 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load static %} {% load static %}
{% block content %} {% block content %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -1,5 +1,4 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load static %} {% load static %}
{% block content %} {% block content %}

View file

@ -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>

View file

@ -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>

View file

@ -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)

View file

@ -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"},
) )

View file

@ -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",
) )

View file

@ -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"]

View file

@ -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)

View file

@ -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>/",

View file

@ -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"

View 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"))),
]

View file

@ -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;
}
}

View file

@ -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;
}
}
}

View file

@ -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;
} }

View file

@ -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;
}
}

View file

@ -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;
}
}
}

View file

@ -0,0 +1,3 @@
@mixin form-padding {
padding: 15px;
}

View file

@ -1,3 +0,0 @@
.password-reset-confirm-form {
margin: 20px 0;
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -0,0 +1,5 @@
.rules-form {
@extend .form;
width: 90%;
}

View file

@ -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";

View file

@ -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";

View 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;
}
}

View file

@ -0,0 +1 @@
@import "pagination";

View file

@ -0,0 +1,11 @@
.text-section {
@extend .section;
width: 70%;
border-radius: 5px;
padding: 10px;
background-color: $white;
}

View file

@ -1 +1,2 @@
@import "section"; @import "section";
@import "text-section";

View 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;
}
}

View 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;
}
}

View file

@ -0,0 +1,2 @@
@import "table";
@import "rules-table";

View file

@ -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;

View file

@ -0,0 +1,3 @@
@mixin button-padding {
padding: 7px 40px;
}

View file

@ -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";

View file

@ -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 {

View file

@ -10,7 +10,3 @@
a { a {
@extend .link; @extend .link;
} }
.gg-link {
color: initial;
}

View file

@ -0,0 +1,13 @@
.select {
max-height: 200px;
&:not([size]){
width: 40%;
}
padding: 0 15px;
}
select {
@extend .select;
}

View file

@ -0,0 +1 @@
@import "select";

View file

@ -1 +1,9 @@
@import "~css.gg/icons-scss/icons"; @import "~css.gg/icons-scss/icons";
.gg-link {
color: initial;
}
.gg-pen {
color: initial;
}

View file

@ -0,0 +1,3 @@
@mixin rounded {
border-radius: 5px;
}

View file

@ -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";

View file

@ -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