0.2.3 #99

Merged
sonny merged 112 commits from development into master 2020-05-23 16:58:42 +02:00
32 changed files with 3199 additions and 472 deletions
Showing only changes of commit bec3488e63 - Show all commits

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

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

@ -36,6 +36,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

@ -10,9 +10,7 @@ class CollectionRule(TimeStampedModel):
name = models.CharField(max_length=100) name = models.CharField(max_length=100)
url = models.URLField(max_length=1024) url = models.URLField(max_length=1024)
website_url = models.URLField( website_url = models.URLField(max_length=1024, editable=False, blank=True, null=True)
max_length=1024, editable=False, blank=True, null=True
)
favicon = models.URLField(blank=True, null=True) favicon = models.URLField(blank=True, null=True)
timezone = models.CharField( timezone = models.CharField(
@ -34,6 +32,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,30 +1,71 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load i18n %}
{% load static %} {% load static %}
{% block content %} {% block content %}
<main id="rules--page" class="main"></main> <main id="rules--page" class="main">
{% endblock %} <form class="form rules-form">
{% csrf_token %}
{% block scripts %} <div class="form__actions">
<script id="rules-data"> <input type="submit" class="button button--primary" formaction="{% url "rules-enable" %}" formmethod="post" value="{% trans "Enable" %}" />
[ <input type="submit" class="button button--primary" formaction="{% url "rules-disable" %}" formmethod="post" value="{% trans "Disable" %}" />
{% for rule in rules %} <input type="submit" class="button button--error" formaction="{% url "rules-delete" %}" formmethod="post" value="{% trans "Delete" %}"/>
{% if not forloop.first %}, {% endif %} </div>
{ <table class="table rules-table">
"pk": {{ rule.pk }}, <thead class="table__header rules-table__header">
"name": "{{ rule.name }}", <tr class="table__row rules-table__row">
"url": "{{ rule.url }}", <th class="table__heading rules-table__heading--select">
"favicon": "{{ rule.favicon|default:'' }}", <input type="checkbox" id="select-all" data-input="rules" />
"category": "{{ rule.category|default:'' }}", </th>
"timezone": "{{ rule.timezone }}", <th class="table__heading rules-table__heading--name">{% trans "Name" %}</th>
"created": "{{ rule.created }}", <th class="table__heading rules-table__heading--category">{% trans "Category" %}</th>
"succeeded": {% if rule.succeeded %}true{% else %}false{% endif %}, <th class="table__heading rules-table__heading--url">{% trans "URL" %}</th>
"error": "{{ rule.error|default:'' }}" <th class="table__heading rules-table__heading--succeeded">{% trans "Successfuly ran" %}</th>
} <th class="table__heading rules-table__heading--enabled">{% trans "Enabled" %}</th>
{% endfor %} <th class="table__heading rules-table__heading--link"></th>
] </tr>
</script> </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 "rule-update" rule.pk %}"><i class="gg-pen"></i></a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</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>
{{ block.super }} <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 %} {% endblock %}

View file

@ -0,0 +1,244 @@
from django.conf import settings
from django.test import TestCase
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
import pytz
from newsreader.accounts.tests.factories import UserFactory
from newsreader.news.collection.models import CollectionRule
from newsreader.news.collection.tests.factories import CollectionRuleFactory
from newsreader.news.core.tests.factories import CategoryFactory
class CollectionRuleBulkViewTestCase:
def setUp(self):
self.redirect_url = reverse("rules")
self.user = UserFactory()
self.client.force_login(self.user)
class CollectionRuleBulkEnableViewTestCase(CollectionRuleBulkViewTestCase, TestCase):
def setUp(self):
super().setUp()
self.url = reverse("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('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("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('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("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('rules-delete')}"
)
rules = CollectionRule.objects.filter(user=self.user)
self.assertCountEqual(rules, self.rules)

View file

@ -150,135 +150,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,141 @@
import os
from django.conf import settings
from django.test import TestCase
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
import pytz
from newsreader.accounts.tests.factories import UserFactory
from newsreader.news.collection.models import CollectionRule
from newsreader.news.collection.tests.factories import CollectionRuleFactory
from newsreader.news.core.tests.factories import CategoryFactory
class 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

@ -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,7 +22,7 @@ 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:
@ -42,6 +47,7 @@ class CollectionRuleDetailMixin:
class CollectionRuleListView(CollectionRuleViewMixin, ListView): class CollectionRuleListView(CollectionRuleViewMixin, ListView):
paginate_by = 50
template_name = "collection/rules.html" template_name = "collection/rules.html"
context_object_name = "rules" context_object_name = "rules"
@ -59,6 +65,60 @@ class CollectionRuleCreateView(
template_name = "collection/rule-create.html" template_name = "collection/rule-create.html"
class CollectionRuleBulkView(FormView):
form_class = CollectionRuleBulkForm
def get_redirect_url(self):
return reverse("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("rules")

View file

@ -19,6 +19,14 @@
padding: 15px; padding: 15px;
} }
&__actions {
display: flex;
justify-content: space-between;
width: 50%;
padding: 15px;
}
&__title { &__title {
font-size: 18px; font-size: 18px;
} }

View file

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

View file

@ -2,6 +2,7 @@
@import "category-form"; @import "category-form";
@import "rule-form"; @import "rule-form";
@import "rules-form";
@import "import-form"; @import "import-form";
@import "login-form"; @import "login-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,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: 10px 50px;
}

View file

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

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

View file

@ -1,7 +1,5 @@
#rules--page { #rules--page {
.list__item { & .table {
& .link { width: 100%;
margin: 0;
}
} }
} }