0.2.3 #99
32 changed files with 3199 additions and 472 deletions
File diff suppressed because it is too large
Load diff
23
src/newsreader/js/components/Selector.js
Normal file
23
src/newsreader/js/components/Selector.js
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
class Selector {
|
||||||
|
onClick = ::this.onClick;
|
||||||
|
|
||||||
|
inputs = [];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
const selectAllInput = document.querySelector('#select-all');
|
||||||
|
|
||||||
|
this.inputs = document.querySelectorAll(`[name=${selectAllInput.dataset.input}`);
|
||||||
|
|
||||||
|
selectAllInput.onchange = this.onClick;
|
||||||
|
}
|
||||||
|
|
||||||
|
onClick(e) {
|
||||||
|
const targetValue = e.target.checked;
|
||||||
|
|
||||||
|
this.inputs.forEach(input => {
|
||||||
|
input.checked = targetValue;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Selector;
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
import './pages/homepage/index.js';
|
import './pages/homepage/index.js';
|
||||||
import './pages/rules/index.js';
|
|
||||||
import './pages/categories/index.js';
|
import './pages/categories/index.js';
|
||||||
|
import './pages/rules/index.js';
|
||||||
|
|
|
||||||
|
|
@ -1,106 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import Cookies from 'js-cookie';
|
|
||||||
|
|
||||||
import Card from '../../components/Card.js';
|
|
||||||
import RuleCard from './components/RuleCard.js';
|
|
||||||
import RuleModal from './components/RuleModal.js';
|
|
||||||
import Messages from '../../components/Messages.js';
|
|
||||||
|
|
||||||
class App extends React.Component {
|
|
||||||
selectRule = ::this.selectRule;
|
|
||||||
deselectRule = ::this.deselectRule;
|
|
||||||
deleteRule = ::this.deleteRule;
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.token = Cookies.get('csrftoken');
|
|
||||||
this.state = {
|
|
||||||
rules: props.rules,
|
|
||||||
selectedRuleId: null,
|
|
||||||
message: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
selectRule(ruleId) {
|
|
||||||
this.setState({ selectedRuleId: ruleId });
|
|
||||||
}
|
|
||||||
|
|
||||||
deselectRule() {
|
|
||||||
this.setState({ selectedRuleId: null });
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteRule(ruleId) {
|
|
||||||
const url = `/api/rules/${ruleId}/`;
|
|
||||||
const options = {
|
|
||||||
method: 'DELETE',
|
|
||||||
headers: {
|
|
||||||
'X-CSRFToken': this.token,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
fetch(url, options).then(response => {
|
|
||||||
if (response.ok) {
|
|
||||||
const rules = this.state.rules.filter(rule => {
|
|
||||||
return rule.pk != ruleId;
|
|
||||||
});
|
|
||||||
|
|
||||||
return this.setState({
|
|
||||||
rules: rules,
|
|
||||||
selectedRuleId: null,
|
|
||||||
message: null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const message = {
|
|
||||||
type: 'error',
|
|
||||||
text: 'Unable to remove rule, try again later',
|
|
||||||
};
|
|
||||||
return this.setState({ selectedRuleId: null, message: message });
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { rules } = this.state;
|
|
||||||
const cards = rules.map(rule => {
|
|
||||||
return <RuleCard key={rule.pk} rule={rule} showDialog={this.selectRule} />;
|
|
||||||
});
|
|
||||||
|
|
||||||
const selectedRule = rules.find(rule => {
|
|
||||||
return rule.pk === this.state.selectedRuleId;
|
|
||||||
});
|
|
||||||
|
|
||||||
const pageHeader = (
|
|
||||||
<>
|
|
||||||
<h1 className="h1">Rules</h1>
|
|
||||||
|
|
||||||
<div className="card__header--action">
|
|
||||||
<a className="link button button--primary" href="/rules/import/">
|
|
||||||
Import rules
|
|
||||||
</a>
|
|
||||||
<a className="link button button--confirm" href="/rules/create/">
|
|
||||||
Create rule
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{this.state.message && <Messages messages={[this.state.message]} />}
|
|
||||||
<Card header={pageHeader} />
|
|
||||||
{cards}
|
|
||||||
{selectedRule && (
|
|
||||||
<RuleModal
|
|
||||||
rule={selectedRule}
|
|
||||||
handleCancel={this.deselectRule}
|
|
||||||
handleDelete={this.deleteRule}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App;
|
|
||||||
|
|
@ -1,65 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import Card from '../../../components/Card.js';
|
|
||||||
|
|
||||||
const RuleCard = props => {
|
|
||||||
const { rule } = props;
|
|
||||||
let favicon = null;
|
|
||||||
|
|
||||||
if (rule.favicon) {
|
|
||||||
favicon = <img className="favicon" src={rule.favicon} />;
|
|
||||||
} else {
|
|
||||||
favicon = <i className="gg-image" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const stateIcon = !rule.error ? 'gg-check' : 'gg-danger';
|
|
||||||
|
|
||||||
const cardHeader = (
|
|
||||||
<>
|
|
||||||
<i className={stateIcon} />
|
|
||||||
<h2 className="h2">{rule.name}</h2>
|
|
||||||
{favicon}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
const cardContent = (
|
|
||||||
<>
|
|
||||||
<ul className="list rules">
|
|
||||||
{rule.error && (
|
|
||||||
<ul className="list errorlist">
|
|
||||||
<li className="list__item errorlist__item">{rule.error}</li>
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{rule.category && <li className="list__item">{rule.category}</li>}
|
|
||||||
<li className="list__item">
|
|
||||||
<a className="link" target="_blank" rel="noopener noreferrer" href={rule.url}>
|
|
||||||
{rule.url}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li className="list__item">{rule.created}</li>
|
|
||||||
<li className="list__item">{rule.timezone}</li>
|
|
||||||
</ul>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
const cardFooter = (
|
|
||||||
<>
|
|
||||||
<a className="link button button--primary" href={`/rules/${rule.pk}/`}>
|
|
||||||
Edit
|
|
||||||
</a>
|
|
||||||
<button
|
|
||||||
id="rule-delete"
|
|
||||||
className="button button--error"
|
|
||||||
onClick={() => props.showDialog(rule.pk)}
|
|
||||||
data-id={`${rule.pk}`}
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
return <Card header={cardHeader} content={cardContent} footer={cardFooter} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RuleCard;
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import Modal from '../../../components/Modal.js';
|
|
||||||
|
|
||||||
const RuleModal = props => {
|
|
||||||
const content = (
|
|
||||||
<>
|
|
||||||
<div>
|
|
||||||
<div className="modal__header">
|
|
||||||
<h1 className="h1 modal__title">Delete rule</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="modal__content">
|
|
||||||
<p className="p">Are you sure you want to delete {props.rule.name}?</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="modal__footer">
|
|
||||||
<button className="button button--confirm" onClick={props.handleCancel}>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="button button--error"
|
|
||||||
onClick={() => props.handleDelete(props.rule.pk)}
|
|
||||||
>
|
|
||||||
Delete rule
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
return <Modal content={content} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RuleModal;
|
|
||||||
|
|
@ -1,13 +1,7 @@
|
||||||
import React from 'react';
|
import Selector from '../../components/Selector.js';
|
||||||
import ReactDOM from 'react-dom';
|
|
||||||
|
|
||||||
import App from './App.js';
|
|
||||||
|
|
||||||
const page = document.getElementById('rules--page');
|
const page = document.getElementById('rules--page');
|
||||||
|
|
||||||
if (page) {
|
if (page) {
|
||||||
const dataScript = document.getElementById('rules-data');
|
new Selector();
|
||||||
const rules = JSON.parse(dataScript.textContent);
|
|
||||||
|
|
||||||
ReactDOM.render(<App rules={rules} />, page);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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 %}
|
||||||
|
|
|
||||||
244
src/newsreader/news/collection/tests/views/test_bulk_views.py
Normal file
244
src/newsreader/news/collection/tests/views/test_bulk_views.py
Normal 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)
|
||||||
|
|
@ -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)
|
|
||||||
141
src/newsreader/news/collection/tests/views/test_import_view.py
Normal file
141
src/newsreader/news/collection/tests/views/test_import_view.py
Normal 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)
|
||||||
|
|
@ -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"),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
5
src/newsreader/scss/components/form/_rules-form.scss
Normal file
5
src/newsreader/scss/components/form/_rules-form.scss
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
.rules-form {
|
||||||
|
@extend .form;
|
||||||
|
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
|
@ -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";
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,9 @@
|
||||||
@import "section/index";
|
@import "section/index";
|
||||||
@import "errorlist/index";
|
@import "errorlist/index";
|
||||||
@import "fieldset/index";
|
@import "fieldset/index";
|
||||||
|
@import "pagination/index";
|
||||||
@import "sidebar/index";
|
@import "sidebar/index";
|
||||||
|
@import "table/index";
|
||||||
|
|
||||||
@import "rules/index";
|
@import "rules/index";
|
||||||
@import "category/index";
|
@import "category/index";
|
||||||
|
|
|
||||||
18
src/newsreader/scss/components/pagination/_pagination.scss
Normal file
18
src/newsreader/scss/components/pagination/_pagination.scss
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
@import "../../elements/button/mixins";
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
|
||||||
|
&__previous, &__current, &__next {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
|
||||||
|
width: 33%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__current {
|
||||||
|
@include button-padding;
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src/newsreader/scss/components/pagination/index.scss
Normal file
1
src/newsreader/scss/components/pagination/index.scss
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
@import "pagination";
|
||||||
38
src/newsreader/scss/components/table/_rules-table.scss
Normal file
38
src/newsreader/scss/components/table/_rules-table.scss
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
.rules-table {
|
||||||
|
&__heading {
|
||||||
|
&--select {
|
||||||
|
width: 5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--name {
|
||||||
|
width: 20%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--category {
|
||||||
|
width: 15%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--url {
|
||||||
|
width: 40%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--succeeded {
|
||||||
|
width: 15%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--enabled {
|
||||||
|
width: 10%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--link {
|
||||||
|
width: 5%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& .link {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src/newsreader/scss/components/table/_table.scss
Normal file
32
src/newsreader/scss/components/table/_table.scss
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
@import "../../lib/mixins";
|
||||||
|
|
||||||
|
.table {
|
||||||
|
@include rounded;
|
||||||
|
|
||||||
|
table-layout: fixed;
|
||||||
|
background-color: $white;
|
||||||
|
width: 90%;
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
text-align: left;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&__heading {
|
||||||
|
@extend .h1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__item {
|
||||||
|
padding: 10px 0;
|
||||||
|
|
||||||
|
border-bottom: 1px solid $border-gray;
|
||||||
|
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__footer {
|
||||||
|
width: 80%;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
2
src/newsreader/scss/components/table/index.scss
Normal file
2
src/newsreader/scss/components/table/index.scss
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
@import "table";
|
||||||
|
@import "rules-table";
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
|
@import "mixins";
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
padding: 10px 50px;
|
@include button-padding;
|
||||||
|
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
|
|
|
||||||
3
src/newsreader/scss/elements/button/_mixins.scss
Normal file
3
src/newsreader/scss/elements/button/_mixins.scss
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
@mixin button-padding {
|
||||||
|
padding: 10px 50px;
|
||||||
|
}
|
||||||
|
|
@ -10,7 +10,3 @@
|
||||||
a {
|
a {
|
||||||
@extend .link;
|
@extend .link;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gg-link {
|
|
||||||
color: initial;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1 +1,9 @@
|
||||||
@import "~css.gg/icons-scss/icons";
|
@import "~css.gg/icons-scss/icons";
|
||||||
|
|
||||||
|
.gg-link {
|
||||||
|
color: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gg-pen {
|
||||||
|
color: initial;
|
||||||
|
}
|
||||||
|
|
|
||||||
3
src/newsreader/scss/lib/_mixins.scss
Normal file
3
src/newsreader/scss/lib/_mixins.scss
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
@mixin rounded {
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
@ -8,5 +8,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";
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
#rules--page {
|
#rules--page {
|
||||||
.list__item {
|
& .table {
|
||||||
& .link {
|
width: 100%;
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue