diff --git a/src/newsreader/accounts/templates/accounts/components/settings-form.html b/src/newsreader/accounts/templates/accounts/components/settings-form.html
index 51d4450..f5e7065 100644
--- a/src/newsreader/accounts/templates/accounts/components/settings-form.html
+++ b/src/newsreader/accounts/templates/accounts/components/settings-form.html
@@ -4,14 +4,25 @@
{% block actions %}
- {% trans "Return to integrations page" %} + {% trans "Return to integrations page" %}
diff --git a/src/newsreader/accounts/templates/accounts/views/twitter.html b/src/newsreader/accounts/templates/accounts/views/twitter.html index e2c51aa..6df1a97 100644 --- a/src/newsreader/accounts/templates/accounts/views/twitter.html +++ b/src/newsreader/accounts/templates/accounts/views/twitter.html @@ -13,7 +13,7 @@ {% endif %}- {% trans "Return to integrations page" %} + {% trans "Return to integrations page" %}
diff --git a/src/newsreader/accounts/tests/test_favicon.py b/src/newsreader/accounts/tests/test_favicon.py new file mode 100644 index 0000000..d3eb56b --- /dev/null +++ b/src/newsreader/accounts/tests/test_favicon.py @@ -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() diff --git a/src/newsreader/accounts/tests/test_integrations.py b/src/newsreader/accounts/tests/test_integrations.py index cdc9546..fbee223 100644 --- a/src/newsreader/accounts/tests/test_integrations.py +++ b/src/newsreader/accounts/tests/test_integrations.py @@ -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")) diff --git a/src/newsreader/accounts/tests/test_settings.py b/src/newsreader/accounts/tests/test_settings.py index 42db736..5a12637 100644 --- a/src/newsreader/accounts/tests/test_settings.py +++ b/src/newsreader/accounts/tests/test_settings.py @@ -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") diff --git a/src/newsreader/accounts/urls.py b/src/newsreader/accounts/urls.py index 3cdd1b1..0eaee5c 100644 --- a/src/newsreader/accounts/urls.py +++ b/src/newsreader/accounts/urls.py @@ -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"))), ] diff --git a/src/newsreader/accounts/views/__init__.py b/src/newsreader/accounts/views/__init__.py index 81dd1fc..3be2b81 100644 --- a/src/newsreader/accounts/views/__init__.py +++ b/src/newsreader/accounts/views/__init__.py @@ -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, diff --git a/src/newsreader/accounts/views/favicon.py b/src/newsreader/accounts/views/favicon.py new file mode 100644 index 0000000..1b85399 --- /dev/null +++ b/src/newsreader/accounts/views/favicon.py @@ -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 diff --git a/src/newsreader/accounts/views/integrations.py b/src/newsreader/accounts/views/integrations.py index 62d71fc..e6ed605 100644 --- a/src/newsreader/accounts/views/integrations.py +++ b/src/newsreader/accounts/views/integrations.py @@ -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( diff --git a/src/newsreader/accounts/views/settings.py b/src/newsreader/accounts/views/settings.py index 1603252..aac24fb 100644 --- a/src/newsreader/accounts/views/settings.py +++ b/src/newsreader/accounts/views/settings.py @@ -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 diff --git a/src/newsreader/js/components/Messages.js b/src/newsreader/js/components/Messages.js index 843677c..150b003 100644 --- a/src/newsreader/js/components/Messages.js +++ b/src/newsreader/js/components/Messages.js @@ -22,7 +22,7 @@ class Messages extends React.Component { ); }); - return