- Add user runnable favicon task
- Update messages styling
This commit is contained in:
Sonny Bakker 2020-10-06 22:51:23 +02:00
parent ca5c2f6b55
commit b6921a20e7
19 changed files with 247 additions and 117 deletions

View file

@ -4,14 +4,25 @@
{% block actions %}
<section class="section form__section--last">
<fieldset class="fieldset form__fieldset">
{% include "components/form/confirm-button.html" %}
<a class="link button button--primary" href="{% url 'accounts:password-change' %}">
{% trans "Change password" %}
</a>
<a class="link button button--primary" href="{% url 'accounts:integrations' %}">
{% if favicon_task_allowed %}
<a class="link button button--primary" href="{% url 'accounts:settings:favicon' %}">
{% trans "Fetch favicons" %}
</a>
{% else %}
<button class="button button--primary button--disabled" disabled>
{% trans "Fetch favicons" %}
</button>
{% endif %}
<a class="link button button--primary" href="{% url 'accounts:settings:integrations' %}">
{% trans "Third party integrations" %}
</a>
{% include "components/form/confirm-button.html" %}
</fieldset>
</section>
{% endblock actions %}

View file

@ -2,7 +2,7 @@
{% block content %}
<main id="password-change--page" class="main">
{% url 'accounts:settings' as cancel_url %}
{% url 'accounts:settings:home' 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

@ -13,7 +13,7 @@
{% endif %}
<p>
<a class="link" href="{% url 'accounts:integrations' %}">{% trans "Return to integrations page" %}</a>
<a class="link" href="{% url 'accounts:settings:integrations' %}">{% trans "Return to integrations page" %}</a>
</p>
</section>
</main>

View file

@ -13,7 +13,7 @@
{% endif %}
<p>
<a class="link" href="{% url 'accounts:integrations' %}">{% trans "Return to integrations page" %}</a>
<a class="link" href="{% url 'accounts:settings:integrations' %}">{% trans "Return to integrations page" %}</a>
</p>
</section>
</main>

View file

@ -0,0 +1,37 @@
from unittest.mock import patch
from django.core.cache import cache
from django.test import TestCase
from django.urls import reverse
from newsreader.accounts.tests.factories import UserFactory
class FaviconRedirectViewTestCase(TestCase):
def setUp(self):
self.user = UserFactory(email="test@test.nl", password="test")
self.client.force_login(self.user)
self.patch = patch("newsreader.accounts.views.favicon.FaviconTask")
self.mocked_task = self.patch.start()
def tearDown(self):
cache.clear()
def test_simple(self):
response = self.client.get(reverse("accounts:settings:favicon"))
self.assertRedirects(response, reverse("accounts:settings:home"))
self.mocked_task.delay.assert_called_once_with(self.user.pk)
self.assertEqual(1, cache.get(f"{self.user.email}-favicon-task"))
def test_not_active(self):
cache.set(f"{self.user.email}-favicon-task", 1)
response = self.client.get(reverse("accounts:settings:favicon"))
self.assertRedirects(response, reverse("accounts:settings:home"))
self.mocked_task.delay.assert_not_called()

View file

@ -22,7 +22,7 @@ class IntegrationsViewTestCase(TestCase):
self.user = UserFactory(email="test@test.nl", password="test")
self.client.force_login(self.user)
self.url = reverse("accounts:integrations")
self.url = reverse("accounts:settings:integrations")
class RedditIntegrationsTestCase(IntegrationsViewTestCase):
@ -69,7 +69,7 @@ class RedditTemplateViewTestCase(TestCase):
self.user = UserFactory(email="test@test.nl", password="test")
self.client.force_login(self.user)
self.base_url = reverse("accounts:reddit-template")
self.base_url = reverse("accounts:settings:reddit-template")
self.state = str(uuid4())
self.patch = patch("newsreader.news.collection.reddit.post")
@ -190,9 +190,9 @@ class RedditTokenRedirectViewTestCase(TestCase):
cache.clear()
def test_simple(self):
response = self.client.get(reverse("accounts:reddit-refresh"))
response = self.client.get(reverse("accounts:settings:reddit-refresh"))
self.assertRedirects(response, reverse("accounts:integrations"))
self.assertRedirects(response, reverse("accounts:settings:integrations"))
self.mocked_task.delay.assert_called_once_with(self.user.pk)
@ -201,9 +201,9 @@ class RedditTokenRedirectViewTestCase(TestCase):
def test_not_active(self):
cache.set(f"{self.user.email}-reddit-refresh", 1)
response = self.client.get(reverse("accounts:reddit-refresh"))
response = self.client.get(reverse("accounts:settings:reddit-refresh"))
self.assertRedirects(response, reverse("accounts:integrations"))
self.assertRedirects(response, reverse("accounts:settings:integrations"))
self.mocked_task.delay.assert_not_called()
@ -223,9 +223,9 @@ class RedditRevokeRedirectViewTestCase(TestCase):
self.mocked_revoke.return_value = True
response = self.client.get(reverse("accounts:reddit-revoke"))
response = self.client.get(reverse("accounts:settings:reddit-revoke"))
self.assertRedirects(response, reverse("accounts:integrations"))
self.assertRedirects(response, reverse("accounts:settings:integrations"))
self.mocked_revoke.assert_called_once_with(self.user)
@ -238,9 +238,9 @@ class RedditRevokeRedirectViewTestCase(TestCase):
self.user.reddit_refresh_token = None
self.user.save()
response = self.client.get(reverse("accounts:reddit-revoke"))
response = self.client.get(reverse("accounts:settings:reddit-revoke"))
self.assertRedirects(response, reverse("accounts:integrations"))
self.assertRedirects(response, reverse("accounts:settings:integrations"))
self.mocked_revoke.assert_not_called()
@ -251,9 +251,9 @@ class RedditRevokeRedirectViewTestCase(TestCase):
self.mocked_revoke.return_value = False
response = self.client.get(reverse("accounts:reddit-revoke"))
response = self.client.get(reverse("accounts:settings:reddit-revoke"))
self.assertRedirects(response, reverse("accounts:integrations"))
self.assertRedirects(response, reverse("accounts:settings:integrations"))
self.user.refresh_from_db()
@ -267,9 +267,9 @@ class RedditRevokeRedirectViewTestCase(TestCase):
self.mocked_revoke.side_effect = StreamException
response = self.client.get(reverse("accounts:reddit-revoke"))
response = self.client.get(reverse("accounts:settings:reddit-revoke"))
self.assertRedirects(response, reverse("accounts:integrations"))
self.assertRedirects(response, reverse("accounts:settings:integrations"))
self.user.refresh_from_db()
@ -293,9 +293,9 @@ class TwitterRevokeRedirectView(TestCase):
self.user.twitter_oauth_token_secret = "jadajadajada"
self.user.save()
response = self.client.get(reverse("accounts:twitter-revoke"))
response = self.client.get(reverse("accounts:settings:twitter-revoke"))
self.assertRedirects(response, reverse("accounts:integrations"))
self.assertRedirects(response, reverse("accounts:settings:integrations"))
self.user.refresh_from_db()
@ -307,9 +307,9 @@ class TwitterRevokeRedirectView(TestCase):
self.user.twitter_oauth_token_secret = None
self.user.save()
response = self.client.get(reverse("accounts:twitter-revoke"))
response = self.client.get(reverse("accounts:settings:twitter-revoke"))
self.assertRedirects(response, reverse("accounts:integrations"))
self.assertRedirects(response, reverse("accounts:settings:integrations"))
self.mocked_post.assert_not_called()
@ -320,9 +320,9 @@ class TwitterRevokeRedirectView(TestCase):
self.mocked_post.side_effect = StreamException
response = self.client.get(reverse("accounts:twitter-revoke"))
response = self.client.get(reverse("accounts:settings:twitter-revoke"))
self.assertRedirects(response, reverse("accounts:integrations"))
self.assertRedirects(response, reverse("accounts:settings:integrations"))
self.user.refresh_from_db()
@ -346,7 +346,7 @@ class TwitterAuthRedirectViewTestCase(TestCase):
text="oauth_token=foo&oauth_token_secret=bar"
)
response = self.client.get(reverse("accounts:twitter-auth"))
response = self.client.get(reverse("accounts:settings:twitter-auth"))
self.assertRedirects(
response,
@ -363,9 +363,9 @@ class TwitterAuthRedirectViewTestCase(TestCase):
def test_stream_exception(self):
self.mocked_post.side_effect = StreamException
response = self.client.get(reverse("accounts:twitter-auth"))
response = self.client.get(reverse("accounts:settings:twitter-auth"))
self.assertRedirects(response, reverse("accounts:integrations"))
self.assertRedirects(response, reverse("accounts:settings:integrations"))
cached_token = cache.get(f"twitter-{self.user.email}-token")
cached_secret = cache.get(f"twitter-{self.user.email}-secret")
@ -376,9 +376,9 @@ class TwitterAuthRedirectViewTestCase(TestCase):
def test_unexpected_contents(self):
self.mocked_post.return_value = Mock(text="foo=bar&oauth_token_secret=bar")
response = self.client.get(reverse("accounts:twitter-auth"))
response = self.client.get(reverse("accounts:settings:twitter-auth"))
self.assertRedirects(response, reverse("accounts:integrations"))
self.assertRedirects(response, reverse("accounts:settings:integrations"))
cached_token = cache.get(f"twitter-{self.user.email}-token")
cached_secret = cache.get(f"twitter-{self.user.email}-secret")
@ -413,7 +413,7 @@ class TwitterTemplateViewTestCase(TestCase):
)
response = self.client.get(
f"{reverse('accounts:twitter-template')}?{urlencode(params)}"
f"{reverse('accounts:settings:twitter-template')}?{urlencode(params)}"
)
self.assertContains(response, _("Twitter account is linked"))
@ -430,7 +430,7 @@ class TwitterTemplateViewTestCase(TestCase):
params = {"denied": "true", "oauth_token": "foo", "oauth_verifier": "barfoo"}
response = self.client.get(
f"{reverse('accounts:twitter-template')}?{urlencode(params)}"
f"{reverse('accounts:settings:twitter-template')}?{urlencode(params)}"
)
self.assertContains(response, _("Twitter authorization failed"))
@ -453,7 +453,7 @@ class TwitterTemplateViewTestCase(TestCase):
params = {"denied": "", "oauth_token": "boo", "oauth_verifier": "barfoo"}
response = self.client.get(
f"{reverse('accounts:twitter-template')}?{urlencode(params)}"
f"{reverse('accounts:settings:twitter-template')}?{urlencode(params)}"
)
self.assertContains(response, _("OAuth tokens failed to match"))
@ -471,7 +471,7 @@ class TwitterTemplateViewTestCase(TestCase):
params = {"denied": "", "oauth_token": "foo", "oauth_verifier": "barfoo"}
response = self.client.get(
f"{reverse('accounts:twitter-template')}?{urlencode(params)}"
f"{reverse('accounts:settings:twitter-template')}?{urlencode(params)}"
)
self.assertContains(response, _("No matching tokens found for this user"))
@ -495,7 +495,7 @@ class TwitterTemplateViewTestCase(TestCase):
self.mocked_post.side_effect = StreamException
response = self.client.get(
f"{reverse('accounts:twitter-template')}?{urlencode(params)}"
f"{reverse('accounts:settings:twitter-template')}?{urlencode(params)}"
)
self.assertContains(response, _("Failed requesting access token"))
@ -523,7 +523,7 @@ class TwitterTemplateViewTestCase(TestCase):
)
response = self.client.get(
f"{reverse('accounts:twitter-template')}?{urlencode(params)}"
f"{reverse('accounts:settings:twitter-template')}?{urlencode(params)}"
)
self.assertContains(response, _("No credentials found in Twitter response"))

View file

@ -10,7 +10,7 @@ class SettingsViewTestCase(TestCase):
self.user = UserFactory(email="test@test.nl", password="test")
self.client.force_login(self.user)
self.url = reverse("accounts:settings")
self.url = reverse("accounts:settings:home")
def test_simple(self):
response = self.client.get(self.url)
@ -19,13 +19,13 @@ class SettingsViewTestCase(TestCase):
def test_user_credential_change(self):
response = self.client.post(
reverse("accounts:settings"),
reverse("accounts:settings:home"),
{"first_name": "First name", "last_name": "Last name"},
)
user = User.objects.get()
self.assertRedirects(response, reverse("accounts:settings"))
self.assertRedirects(response, reverse("accounts:settings:home"))
self.assertEquals(user.first_name, "First name")
self.assertEquals(user.last_name, "Last name")

View file

@ -1,10 +1,11 @@
from django.contrib.auth.decorators import login_required
from django.urls import path
from django.urls import include, path
from newsreader.accounts.views import (
ActivationCompleteView,
ActivationResendView,
ActivationView,
FaviconRedirectView,
IntegrationsView,
LoginView,
LogoutView,
@ -26,6 +27,46 @@ from newsreader.accounts.views import (
)
settings_patterns = [
# Integrations
path(
"integrations/reddit/callback/",
login_required(RedditTemplateView.as_view()),
name="reddit-template",
),
path(
"integrations/reddit/refresh/",
login_required(RedditTokenRedirectView.as_view()),
name="reddit-refresh",
),
path(
"integrations/reddit/revoke/",
login_required(RedditRevokeRedirectView.as_view()),
name="reddit-revoke",
),
path(
"integrations/twitter/auth/",
login_required(TwitterAuthRedirectView.as_view()),
name="twitter-auth",
),
path(
"integrations/twitter/callback/",
login_required(TwitterTemplateView.as_view()),
name="twitter-template",
),
path(
"integrations/twitter/revoke/",
login_required(TwitterRevokeRedirectView.as_view()),
name="twitter-revoke",
),
path(
"integrations/", login_required(IntegrationsView.as_view()), name="integrations"
),
# Misc
path("favicon/", login_required(FaviconRedirectView.as_view()), name="favicon"),
path("", login_required(SettingsView.as_view()), name="home"),
]
urlpatterns = [
# Auth
path("login/", LoginView.as_view(), name="login"),
@ -70,42 +111,6 @@ urlpatterns = [
login_required(PasswordChangeView.as_view()),
name="password-change",
),
# Integrations
path(
"settings/integrations/reddit/callback/",
login_required(RedditTemplateView.as_view()),
name="reddit-template",
),
path(
"settings/integrations/reddit/refresh/",
login_required(RedditTokenRedirectView.as_view()),
name="reddit-refresh",
),
path(
"settings/integrations/reddit/revoke/",
login_required(RedditRevokeRedirectView.as_view()),
name="reddit-revoke",
),
path(
"settings/integrations/twitter/auth/",
login_required(TwitterAuthRedirectView.as_view()),
name="twitter-auth",
),
path(
"settings/integrations/twitter/callback/",
login_required(TwitterTemplateView.as_view()),
name="twitter-template",
),
path(
"settings/integrations/twitter/revoke/",
login_required(TwitterRevokeRedirectView.as_view()),
name="twitter-revoke",
),
path(
"settings/integrations",
login_required(IntegrationsView.as_view()),
name="integrations",
),
# Settings
path("settings/", login_required(SettingsView.as_view()), name="settings"),
path("settings/", include((settings_patterns, "settings"))),
]

View file

@ -1,4 +1,5 @@
from newsreader.accounts.views.auth import LoginView, LogoutView
from newsreader.accounts.views.favicon import FaviconRedirectView
from newsreader.accounts.views.integrations import (
IntegrationsView,
RedditRevokeRedirectView,

View file

@ -0,0 +1,26 @@
from django.contrib import messages
from django.core.cache import cache
from django.urls import reverse_lazy
from django.utils.translation import gettext as _
from django.views.generic import RedirectView
from newsreader.news.collection.tasks import FaviconTask
class FaviconRedirectView(RedirectView):
url = reverse_lazy("accounts:settings:home")
def get(self, request, *args, **kwargs):
response = super().get(request, *args, **kwargs)
user = request.user
task_active = cache.get(f"{user.email}-favicon-task")
if not task_active:
FaviconTask.delay(user.pk)
messages.success(request, _("Favicons are being fetched"))
cache.set(f"{user.email}-favicon-task", 1, 18000) # 5 hours
return response
messages.error(request, _("Limit reached, try again later"))
return response

View file

@ -53,7 +53,7 @@ class IntegrationsView(TemplateView):
and not user.reddit_access_token
and not reddit_task_active
):
reddit_refresh_url = reverse_lazy("accounts:reddit-refresh")
reddit_refresh_url = reverse_lazy("accounts:settings:reddit-refresh")
if not user.reddit_refresh_token:
reddit_authorization_url = get_reddit_authorization_url(user)
@ -62,7 +62,7 @@ class IntegrationsView(TemplateView):
"reddit_authorization_url": reddit_authorization_url,
"reddit_refresh_url": reddit_refresh_url,
"reddit_revoke_url": (
reverse_lazy("accounts:reddit-revoke")
reverse_lazy("accounts:settings:reddit-revoke")
if not reddit_authorization_url
else None
),
@ -72,10 +72,10 @@ class IntegrationsView(TemplateView):
twitter_revoke_url = None
if self.request.user.has_twitter_auth:
twitter_revoke_url = reverse_lazy("accounts:twitter-revoke")
twitter_revoke_url = reverse_lazy("accounts:settings:twitter-revoke")
return {
"twitter_auth_url": reverse_lazy("accounts:twitter-auth"),
"twitter_auth_url": reverse_lazy("accounts:settings:twitter-auth"),
"twitter_revoke_url": twitter_revoke_url,
}
@ -130,7 +130,7 @@ class RedditTemplateView(TemplateView):
class RedditTokenRedirectView(RedirectView):
url = reverse_lazy("accounts:integrations")
url = reverse_lazy("accounts:settings:integrations")
def get(self, request, *args, **kwargs):
response = super().get(request, *args, **kwargs)
@ -149,7 +149,7 @@ class RedditTokenRedirectView(RedirectView):
class RedditRevokeRedirectView(RedirectView):
url = reverse_lazy("accounts:integrations")
url = reverse_lazy("accounts:settings:integrations")
def get(self, request, *args, **kwargs):
response = super().get(request, *args, **kwargs)
@ -181,7 +181,7 @@ class RedditRevokeRedirectView(RedirectView):
class TwitterRevokeRedirectView(RedirectView):
url = reverse_lazy("accounts:integrations")
url = reverse_lazy("accounts:settings:integrations")
def get(self, request, *args, **kwargs):
if not request.user.has_twitter_auth:
@ -212,7 +212,7 @@ class TwitterRevokeRedirectView(RedirectView):
class TwitterAuthRedirectView(RedirectView):
url = reverse_lazy("accounts:integrations")
url = reverse_lazy("accounts:settings:integrations")
def get(self, request, *args, **kwargs):
oauth = OAuth(

View file

@ -1,3 +1,4 @@
from django.core.cache import cache
from django.urls import reverse_lazy
from django.views.generic.edit import FormView, ModelFormMixin
@ -11,7 +12,7 @@ from newsreader.news.collection.reddit import (
class SettingsView(ModelFormMixin, FormView):
template_name = "accounts/views/settings.html"
success_url = reverse_lazy("accounts:settings")
success_url = reverse_lazy("accounts:settings:home")
form_class = UserSettingsForm
model = User
@ -19,6 +20,14 @@ class SettingsView(ModelFormMixin, FormView):
self.object = self.get_object()
return super().get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
return {
**context,
"favicon_task_allowed": not cache.get(f"{self.request.user}-favicon-task"),
}
def get_object(self, **kwargs):
return self.request.user

View file

@ -22,7 +22,7 @@ class Messages extends React.Component {
);
});
return <ul className="list messages">{messages}</ul>;
return <ul className="list messages messages--fixed">{messages}</ul>;
}
}

View file

@ -126,7 +126,7 @@ class FaviconCollector(Collector):
feed_client, favicon_client = (FeedClient, FaviconClient)
url_builder, favicon_builder = (WebsiteURLBuilder, FaviconBuilder)
def collect(self, rules=None):
def collect(self, rules=[]):
streams = []
with self.feed_client(rules=rules) as client:

View file

@ -1,11 +0,0 @@
from django.core.management.base import BaseCommand
from newsreader.news.collection.feed import FeedCollector
class Command(BaseCommand):
help = "Collects Atom/RSS feeds"
def handle(self, *args, **options):
collector = FeedCollector()
collector.collect()

View file

@ -1,11 +0,0 @@
from django.core.management.base import BaseCommand
from newsreader.news.collection.favicon import FaviconCollector
class Command(BaseCommand):
help = "Fetch favicons for collection rules"
def handle(self, *args, **options):
collector = FaviconCollector()
collector.collect()

View file

@ -147,7 +147,49 @@ class TwitterTimelineTask(app.Task):
raise Reject(reason="Task already running", requeue=False)
class FaviconTask(app.Task):
name = "FaviconTask"
ignore_result = True
def run(self, user_pk):
from newsreader.news.collection.favicon import FaviconCollector
try:
user = User.objects.get(pk=user_pk)
except ObjectDoesNotExist:
message = f"User {user_pk} does not exist"
logger.exception(message)
raise Reject(reason=message, requeue=False)
with MemCacheLock("f{user.email}-favicon-task", self.app.oid) as acquired:
if acquired:
logger.info(f"Running favicon task for user {user_pk}")
rules = user.rules.enabled().filter(type=RuleTypeChoices.feed)
collector = FaviconCollector()
collector.collect(rules=rules)
third_party_rules = user.rules.enabled().exclude(
type=RuleTypeChoices.feed
)
for rule in third_party_rules:
if rule.type == RuleTypeChoices.subreddit:
rule.favicon = "https://www.reddit.com/favicon.ico"
rule.save()
elif rule.type == RuleTypeChoices.twitter_timeline:
rule.favicon = "https://abs.twimg.com/favicons/favicon.ico"
rule.save()
else:
logger.warning(f"Cancelling task due to existing lock")
raise Reject(reason="Task already running", requeue=False)
FeedTask = app.register_task(FeedTask())
FaviconTask = app.register_task(FaviconTask())
RedditTask = app.register_task(RedditTask())
RedditTokenTask = app.register_task(RedditTokenTask())
TwitterTimelineTask = app.register_task(TwitterTimelineTask())

View file

@ -3,12 +3,10 @@
flex-direction: column;
align-items: center;
position: fixed;
top: 0;
width: 100%;
margin: 5px 0 20px 0;
color: $white;
color: $font-color;
&__item {
width: 80%;
@ -17,7 +15,7 @@
padding: 20px 15px;
margin: 5px 0;
background-color: $blue;
background-color: $transparant-blue;
&--error {
background-color: $transparant-red;
@ -27,7 +25,6 @@
background-color: $transparant-orange;
}
// TODO check this color
&--success {
background-color: $transparant-green;
}
@ -39,4 +36,28 @@
--ggs: 2;
}
}
&--fixed {
position: fixed;
top: 0;
}
&--fixed &__item {
color: $white;
background-color: $blue;
}
&--fixed &__item--error {
color: $white;
background-color: $red;
}
&--fixed &__item--warning {
background-color: $orange;
}
&--fixed &__item--success {
color: $white;
background-color: $green;
}
}

View file

@ -17,7 +17,7 @@
<li class="nav__item"><a href="{% url 'index' %}">Home</a></li>
<li class="nav__item"><a href="{% url 'news:core:categories' %}">Categories</a></li>
<li class="nav__item"><a href="{% url 'news:collection:rules' %}">Feeds</a></li>
<li class="nav__item"><a href="{% url 'accounts:settings' %}">Settings</a></li>
<li class="nav__item"><a href="{% url 'accounts:settings:home' %}">Settings</a></li>
{% if request.user.is_superuser %}
<li class="nav__item"><a href="{% url 'admin:index' %}">Admin</a></li>
{% endif %}