0.3.2
- Add user runnable favicon task - Update messages styling
This commit is contained in:
parent
ca5c2f6b55
commit
b6921a20e7
19 changed files with 247 additions and 117 deletions
|
|
@ -4,14 +4,25 @@
|
||||||
{% block actions %}
|
{% block actions %}
|
||||||
<section class="section form__section--last">
|
<section class="section form__section--last">
|
||||||
<fieldset class="fieldset form__fieldset">
|
<fieldset class="fieldset form__fieldset">
|
||||||
|
{% include "components/form/confirm-button.html" %}
|
||||||
|
|
||||||
<a class="link button button--primary" href="{% url 'accounts:password-change' %}">
|
<a class="link button button--primary" href="{% url 'accounts:password-change' %}">
|
||||||
{% trans "Change password" %}
|
{% trans "Change password" %}
|
||||||
</a>
|
</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" %}
|
{% trans "Third party integrations" %}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
{% include "components/form/confirm-button.html" %}
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</section>
|
</section>
|
||||||
{% endblock actions %}
|
{% endblock actions %}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<main id="password-change--page" class="main">
|
<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 %}
|
{% include "components/form/form.html" with form=form title="Change password" confirm_text="Change password" cancel_url=cancel_url %}
|
||||||
</main>
|
</main>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<p>
|
<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>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<p>
|
<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>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
37
src/newsreader/accounts/tests/test_favicon.py
Normal file
37
src/newsreader/accounts/tests/test_favicon.py
Normal 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()
|
||||||
|
|
@ -22,7 +22,7 @@ class IntegrationsViewTestCase(TestCase):
|
||||||
self.user = UserFactory(email="test@test.nl", password="test")
|
self.user = UserFactory(email="test@test.nl", password="test")
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
self.url = reverse("accounts:integrations")
|
self.url = reverse("accounts:settings:integrations")
|
||||||
|
|
||||||
|
|
||||||
class RedditIntegrationsTestCase(IntegrationsViewTestCase):
|
class RedditIntegrationsTestCase(IntegrationsViewTestCase):
|
||||||
|
|
@ -69,7 +69,7 @@ class RedditTemplateViewTestCase(TestCase):
|
||||||
self.user = UserFactory(email="test@test.nl", password="test")
|
self.user = UserFactory(email="test@test.nl", password="test")
|
||||||
self.client.force_login(self.user)
|
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.state = str(uuid4())
|
||||||
|
|
||||||
self.patch = patch("newsreader.news.collection.reddit.post")
|
self.patch = patch("newsreader.news.collection.reddit.post")
|
||||||
|
|
@ -190,9 +190,9 @@ class RedditTokenRedirectViewTestCase(TestCase):
|
||||||
cache.clear()
|
cache.clear()
|
||||||
|
|
||||||
def test_simple(self):
|
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)
|
self.mocked_task.delay.assert_called_once_with(self.user.pk)
|
||||||
|
|
||||||
|
|
@ -201,9 +201,9 @@ class RedditTokenRedirectViewTestCase(TestCase):
|
||||||
def test_not_active(self):
|
def test_not_active(self):
|
||||||
cache.set(f"{self.user.email}-reddit-refresh", 1)
|
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()
|
self.mocked_task.delay.assert_not_called()
|
||||||
|
|
||||||
|
|
@ -223,9 +223,9 @@ class RedditRevokeRedirectViewTestCase(TestCase):
|
||||||
|
|
||||||
self.mocked_revoke.return_value = True
|
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)
|
self.mocked_revoke.assert_called_once_with(self.user)
|
||||||
|
|
||||||
|
|
@ -238,9 +238,9 @@ class RedditRevokeRedirectViewTestCase(TestCase):
|
||||||
self.user.reddit_refresh_token = None
|
self.user.reddit_refresh_token = None
|
||||||
self.user.save()
|
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()
|
self.mocked_revoke.assert_not_called()
|
||||||
|
|
||||||
|
|
@ -251,9 +251,9 @@ class RedditRevokeRedirectViewTestCase(TestCase):
|
||||||
|
|
||||||
self.mocked_revoke.return_value = False
|
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()
|
self.user.refresh_from_db()
|
||||||
|
|
||||||
|
|
@ -267,9 +267,9 @@ class RedditRevokeRedirectViewTestCase(TestCase):
|
||||||
|
|
||||||
self.mocked_revoke.side_effect = StreamException
|
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()
|
self.user.refresh_from_db()
|
||||||
|
|
||||||
|
|
@ -293,9 +293,9 @@ class TwitterRevokeRedirectView(TestCase):
|
||||||
self.user.twitter_oauth_token_secret = "jadajadajada"
|
self.user.twitter_oauth_token_secret = "jadajadajada"
|
||||||
self.user.save()
|
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()
|
self.user.refresh_from_db()
|
||||||
|
|
||||||
|
|
@ -307,9 +307,9 @@ class TwitterRevokeRedirectView(TestCase):
|
||||||
self.user.twitter_oauth_token_secret = None
|
self.user.twitter_oauth_token_secret = None
|
||||||
self.user.save()
|
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()
|
self.mocked_post.assert_not_called()
|
||||||
|
|
||||||
|
|
@ -320,9 +320,9 @@ class TwitterRevokeRedirectView(TestCase):
|
||||||
|
|
||||||
self.mocked_post.side_effect = StreamException
|
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()
|
self.user.refresh_from_db()
|
||||||
|
|
||||||
|
|
@ -346,7 +346,7 @@ class TwitterAuthRedirectViewTestCase(TestCase):
|
||||||
text="oauth_token=foo&oauth_token_secret=bar"
|
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(
|
self.assertRedirects(
|
||||||
response,
|
response,
|
||||||
|
|
@ -363,9 +363,9 @@ class TwitterAuthRedirectViewTestCase(TestCase):
|
||||||
def test_stream_exception(self):
|
def test_stream_exception(self):
|
||||||
self.mocked_post.side_effect = StreamException
|
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_token = cache.get(f"twitter-{self.user.email}-token")
|
||||||
cached_secret = cache.get(f"twitter-{self.user.email}-secret")
|
cached_secret = cache.get(f"twitter-{self.user.email}-secret")
|
||||||
|
|
@ -376,9 +376,9 @@ class TwitterAuthRedirectViewTestCase(TestCase):
|
||||||
def test_unexpected_contents(self):
|
def test_unexpected_contents(self):
|
||||||
self.mocked_post.return_value = Mock(text="foo=bar&oauth_token_secret=bar")
|
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_token = cache.get(f"twitter-{self.user.email}-token")
|
||||||
cached_secret = cache.get(f"twitter-{self.user.email}-secret")
|
cached_secret = cache.get(f"twitter-{self.user.email}-secret")
|
||||||
|
|
@ -413,7 +413,7 @@ class TwitterTemplateViewTestCase(TestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
response = self.client.get(
|
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"))
|
self.assertContains(response, _("Twitter account is linked"))
|
||||||
|
|
@ -430,7 +430,7 @@ class TwitterTemplateViewTestCase(TestCase):
|
||||||
params = {"denied": "true", "oauth_token": "foo", "oauth_verifier": "barfoo"}
|
params = {"denied": "true", "oauth_token": "foo", "oauth_verifier": "barfoo"}
|
||||||
|
|
||||||
response = self.client.get(
|
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"))
|
self.assertContains(response, _("Twitter authorization failed"))
|
||||||
|
|
@ -453,7 +453,7 @@ class TwitterTemplateViewTestCase(TestCase):
|
||||||
params = {"denied": "", "oauth_token": "boo", "oauth_verifier": "barfoo"}
|
params = {"denied": "", "oauth_token": "boo", "oauth_verifier": "barfoo"}
|
||||||
|
|
||||||
response = self.client.get(
|
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"))
|
self.assertContains(response, _("OAuth tokens failed to match"))
|
||||||
|
|
@ -471,7 +471,7 @@ class TwitterTemplateViewTestCase(TestCase):
|
||||||
params = {"denied": "", "oauth_token": "foo", "oauth_verifier": "barfoo"}
|
params = {"denied": "", "oauth_token": "foo", "oauth_verifier": "barfoo"}
|
||||||
|
|
||||||
response = self.client.get(
|
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"))
|
self.assertContains(response, _("No matching tokens found for this user"))
|
||||||
|
|
@ -495,7 +495,7 @@ class TwitterTemplateViewTestCase(TestCase):
|
||||||
self.mocked_post.side_effect = StreamException
|
self.mocked_post.side_effect = StreamException
|
||||||
|
|
||||||
response = self.client.get(
|
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"))
|
self.assertContains(response, _("Failed requesting access token"))
|
||||||
|
|
@ -523,7 +523,7 @@ class TwitterTemplateViewTestCase(TestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
response = self.client.get(
|
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"))
|
self.assertContains(response, _("No credentials found in Twitter response"))
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ class SettingsViewTestCase(TestCase):
|
||||||
self.user = UserFactory(email="test@test.nl", password="test")
|
self.user = UserFactory(email="test@test.nl", password="test")
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
self.url = reverse("accounts:settings")
|
self.url = reverse("accounts:settings:home")
|
||||||
|
|
||||||
def test_simple(self):
|
def test_simple(self):
|
||||||
response = self.client.get(self.url)
|
response = self.client.get(self.url)
|
||||||
|
|
@ -19,13 +19,13 @@ class SettingsViewTestCase(TestCase):
|
||||||
|
|
||||||
def test_user_credential_change(self):
|
def test_user_credential_change(self):
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("accounts:settings"),
|
reverse("accounts:settings:home"),
|
||||||
{"first_name": "First name", "last_name": "Last name"},
|
{"first_name": "First name", "last_name": "Last name"},
|
||||||
)
|
)
|
||||||
|
|
||||||
user = User.objects.get()
|
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.first_name, "First name")
|
||||||
self.assertEquals(user.last_name, "Last name")
|
self.assertEquals(user.last_name, "Last name")
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.urls import path
|
from django.urls import include, path
|
||||||
|
|
||||||
from newsreader.accounts.views import (
|
from newsreader.accounts.views import (
|
||||||
ActivationCompleteView,
|
ActivationCompleteView,
|
||||||
ActivationResendView,
|
ActivationResendView,
|
||||||
ActivationView,
|
ActivationView,
|
||||||
|
FaviconRedirectView,
|
||||||
IntegrationsView,
|
IntegrationsView,
|
||||||
LoginView,
|
LoginView,
|
||||||
LogoutView,
|
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 = [
|
urlpatterns = [
|
||||||
# Auth
|
# Auth
|
||||||
path("login/", LoginView.as_view(), name="login"),
|
path("login/", LoginView.as_view(), name="login"),
|
||||||
|
|
@ -70,42 +111,6 @@ urlpatterns = [
|
||||||
login_required(PasswordChangeView.as_view()),
|
login_required(PasswordChangeView.as_view()),
|
||||||
name="password-change",
|
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
|
# Settings
|
||||||
path("settings/", login_required(SettingsView.as_view()), name="settings"),
|
path("settings/", include((settings_patterns, "settings"))),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
from newsreader.accounts.views.auth import LoginView, LogoutView
|
from newsreader.accounts.views.auth import LoginView, LogoutView
|
||||||
|
from newsreader.accounts.views.favicon import FaviconRedirectView
|
||||||
from newsreader.accounts.views.integrations import (
|
from newsreader.accounts.views.integrations import (
|
||||||
IntegrationsView,
|
IntegrationsView,
|
||||||
RedditRevokeRedirectView,
|
RedditRevokeRedirectView,
|
||||||
|
|
|
||||||
26
src/newsreader/accounts/views/favicon.py
Normal file
26
src/newsreader/accounts/views/favicon.py
Normal 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
|
||||||
|
|
@ -53,7 +53,7 @@ class IntegrationsView(TemplateView):
|
||||||
and not user.reddit_access_token
|
and not user.reddit_access_token
|
||||||
and not reddit_task_active
|
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:
|
if not user.reddit_refresh_token:
|
||||||
reddit_authorization_url = get_reddit_authorization_url(user)
|
reddit_authorization_url = get_reddit_authorization_url(user)
|
||||||
|
|
@ -62,7 +62,7 @@ class IntegrationsView(TemplateView):
|
||||||
"reddit_authorization_url": reddit_authorization_url,
|
"reddit_authorization_url": reddit_authorization_url,
|
||||||
"reddit_refresh_url": reddit_refresh_url,
|
"reddit_refresh_url": reddit_refresh_url,
|
||||||
"reddit_revoke_url": (
|
"reddit_revoke_url": (
|
||||||
reverse_lazy("accounts:reddit-revoke")
|
reverse_lazy("accounts:settings:reddit-revoke")
|
||||||
if not reddit_authorization_url
|
if not reddit_authorization_url
|
||||||
else None
|
else None
|
||||||
),
|
),
|
||||||
|
|
@ -72,10 +72,10 @@ class IntegrationsView(TemplateView):
|
||||||
twitter_revoke_url = None
|
twitter_revoke_url = None
|
||||||
|
|
||||||
if self.request.user.has_twitter_auth:
|
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 {
|
return {
|
||||||
"twitter_auth_url": reverse_lazy("accounts:twitter-auth"),
|
"twitter_auth_url": reverse_lazy("accounts:settings:twitter-auth"),
|
||||||
"twitter_revoke_url": twitter_revoke_url,
|
"twitter_revoke_url": twitter_revoke_url,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -130,7 +130,7 @@ class RedditTemplateView(TemplateView):
|
||||||
|
|
||||||
|
|
||||||
class RedditTokenRedirectView(RedirectView):
|
class RedditTokenRedirectView(RedirectView):
|
||||||
url = reverse_lazy("accounts:integrations")
|
url = reverse_lazy("accounts:settings:integrations")
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
response = super().get(request, *args, **kwargs)
|
response = super().get(request, *args, **kwargs)
|
||||||
|
|
@ -149,7 +149,7 @@ class RedditTokenRedirectView(RedirectView):
|
||||||
|
|
||||||
|
|
||||||
class RedditRevokeRedirectView(RedirectView):
|
class RedditRevokeRedirectView(RedirectView):
|
||||||
url = reverse_lazy("accounts:integrations")
|
url = reverse_lazy("accounts:settings:integrations")
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
response = super().get(request, *args, **kwargs)
|
response = super().get(request, *args, **kwargs)
|
||||||
|
|
@ -181,7 +181,7 @@ class RedditRevokeRedirectView(RedirectView):
|
||||||
|
|
||||||
|
|
||||||
class TwitterRevokeRedirectView(RedirectView):
|
class TwitterRevokeRedirectView(RedirectView):
|
||||||
url = reverse_lazy("accounts:integrations")
|
url = reverse_lazy("accounts:settings:integrations")
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
if not request.user.has_twitter_auth:
|
if not request.user.has_twitter_auth:
|
||||||
|
|
@ -212,7 +212,7 @@ class TwitterRevokeRedirectView(RedirectView):
|
||||||
|
|
||||||
|
|
||||||
class TwitterAuthRedirectView(RedirectView):
|
class TwitterAuthRedirectView(RedirectView):
|
||||||
url = reverse_lazy("accounts:integrations")
|
url = reverse_lazy("accounts:settings:integrations")
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
oauth = OAuth(
|
oauth = OAuth(
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
from django.core.cache import cache
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.views.generic.edit import FormView, ModelFormMixin
|
from django.views.generic.edit import FormView, ModelFormMixin
|
||||||
|
|
||||||
|
|
@ -11,7 +12,7 @@ from newsreader.news.collection.reddit import (
|
||||||
|
|
||||||
class SettingsView(ModelFormMixin, FormView):
|
class SettingsView(ModelFormMixin, FormView):
|
||||||
template_name = "accounts/views/settings.html"
|
template_name = "accounts/views/settings.html"
|
||||||
success_url = reverse_lazy("accounts:settings")
|
success_url = reverse_lazy("accounts:settings:home")
|
||||||
form_class = UserSettingsForm
|
form_class = UserSettingsForm
|
||||||
model = User
|
model = User
|
||||||
|
|
||||||
|
|
@ -19,6 +20,14 @@ class SettingsView(ModelFormMixin, FormView):
|
||||||
self.object = self.get_object()
|
self.object = self.get_object()
|
||||||
return super().get(request, *args, **kwargs)
|
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):
|
def get_object(self, **kwargs):
|
||||||
return self.request.user
|
return self.request.user
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -126,7 +126,7 @@ class FaviconCollector(Collector):
|
||||||
feed_client, favicon_client = (FeedClient, FaviconClient)
|
feed_client, favicon_client = (FeedClient, FaviconClient)
|
||||||
url_builder, favicon_builder = (WebsiteURLBuilder, FaviconBuilder)
|
url_builder, favicon_builder = (WebsiteURLBuilder, FaviconBuilder)
|
||||||
|
|
||||||
def collect(self, rules=None):
|
def collect(self, rules=[]):
|
||||||
streams = []
|
streams = []
|
||||||
|
|
||||||
with self.feed_client(rules=rules) as client:
|
with self.feed_client(rules=rules) as client:
|
||||||
|
|
|
||||||
|
|
@ -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()
|
|
||||||
|
|
@ -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()
|
|
||||||
|
|
@ -147,7 +147,49 @@ class TwitterTimelineTask(app.Task):
|
||||||
raise Reject(reason="Task already running", requeue=False)
|
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())
|
FeedTask = app.register_task(FeedTask())
|
||||||
|
FaviconTask = app.register_task(FaviconTask())
|
||||||
RedditTask = app.register_task(RedditTask())
|
RedditTask = app.register_task(RedditTask())
|
||||||
RedditTokenTask = app.register_task(RedditTokenTask())
|
RedditTokenTask = app.register_task(RedditTokenTask())
|
||||||
TwitterTimelineTask = app.register_task(TwitterTimelineTask())
|
TwitterTimelineTask = app.register_task(TwitterTimelineTask())
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,10 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 5px 0 20px 0;
|
margin: 5px 0 20px 0;
|
||||||
|
|
||||||
color: $white;
|
color: $font-color;
|
||||||
|
|
||||||
&__item {
|
&__item {
|
||||||
width: 80%;
|
width: 80%;
|
||||||
|
|
@ -17,7 +15,7 @@
|
||||||
padding: 20px 15px;
|
padding: 20px 15px;
|
||||||
margin: 5px 0;
|
margin: 5px 0;
|
||||||
|
|
||||||
background-color: $blue;
|
background-color: $transparant-blue;
|
||||||
|
|
||||||
&--error {
|
&--error {
|
||||||
background-color: $transparant-red;
|
background-color: $transparant-red;
|
||||||
|
|
@ -27,7 +25,6 @@
|
||||||
background-color: $transparant-orange;
|
background-color: $transparant-orange;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO check this color
|
|
||||||
&--success {
|
&--success {
|
||||||
background-color: $transparant-green;
|
background-color: $transparant-green;
|
||||||
}
|
}
|
||||||
|
|
@ -39,4 +36,28 @@
|
||||||
--ggs: 2;
|
--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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
<li class="nav__item"><a href="{% url 'index' %}">Home</a></li>
|
<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: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 '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 %}
|
{% if request.user.is_superuser %}
|
||||||
<li class="nav__item"><a href="{% url 'admin:index' %}">Admin</a></li>
|
<li class="nav__item"><a href="{% url 'admin:index' %}">Admin</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue