diff --git a/docker-compose.yml b/docker-compose.yml index f29e719..02f1fab 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -29,11 +29,6 @@ x-django-env: &django-env EMAIL_USE_SSL: EMAIL_DEFAULT_FROM: - # Reddit - REDDIT_CLIENT_ID: - REDDIT_CLIENT_SECRET: - REDDIT_CALLBACK_URL: - # Sentry SENTRY_DSN: diff --git a/src/newsreader/accounts/admin.py b/src/newsreader/accounts/admin.py index 38bc2f3..8ae55cb 100644 --- a/src/newsreader/accounts/admin.py +++ b/src/newsreader/accounts/admin.py @@ -11,12 +11,6 @@ class UserAdminForm(UserChangeForm): class Meta: widgets = { "email": forms.EmailInput(attrs={"size": "50"}), - "reddit_access_token": forms.PasswordInput( - attrs={"size": "90"}, render_value=True - ), - "reddit_refresh_token": forms.PasswordInput( - attrs={"size": "90"}, render_value=True - ), } @@ -34,10 +28,6 @@ class UserAdmin(DjangoUserAdmin): _("User settings"), {"fields": ("email", "password", "first_name", "last_name", "is_active")}, ), - ( - _("Reddit settings"), - {"fields": ("reddit_access_token", "reddit_refresh_token")}, - ), ( _("Permission settings"), {"classes": ("collapse",), "fields": ("is_staff", "is_superuser")}, diff --git a/src/newsreader/accounts/migrations/0018_remove_user_reddit_access_token_and_more.py b/src/newsreader/accounts/migrations/0018_remove_user_reddit_access_token_and_more.py new file mode 100644 index 0000000..19bda0c --- /dev/null +++ b/src/newsreader/accounts/migrations/0018_remove_user_reddit_access_token_and_more.py @@ -0,0 +1,21 @@ +# Generated by Django 4.2.16 on 2025-03-26 08:46 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0017_auto_20240906_0914'), + ] + + operations = [ + migrations.RemoveField( + model_name='user', + name='reddit_access_token', + ), + migrations.RemoveField( + model_name='user', + name='reddit_refresh_token', + ), + ] diff --git a/src/newsreader/accounts/models.py b/src/newsreader/accounts/models.py index e78786f..15cc97e 100644 --- a/src/newsreader/accounts/models.py +++ b/src/newsreader/accounts/models.py @@ -39,10 +39,6 @@ class UserManager(DjangoUserManager): class User(AbstractUser): email = models.EmailField(_("email address"), unique=True) - # reddit settings - reddit_refresh_token = models.CharField(max_length=255, blank=True, null=True) - reddit_access_token = models.CharField(max_length=255, blank=True, null=True) - # settings auto_mark_read = models.BooleanField( _("Auto read marking"), diff --git a/src/newsreader/accounts/templates/accounts/components/settings-form.html b/src/newsreader/accounts/templates/accounts/components/settings-form.html index f5e7065..6e81e79 100644 --- a/src/newsreader/accounts/templates/accounts/components/settings-form.html +++ b/src/newsreader/accounts/templates/accounts/components/settings-form.html @@ -2,27 +2,23 @@ {% load i18n %} {% block actions %} -
-
- {% include "components/form/confirm-button.html" %} +
+
+ {% include "components/form/confirm-button.html" %} - - {% trans "Change password" %} - + + {% trans "Change password" %} + - {% if favicon_task_allowed %} - - {% trans "Fetch favicons" %} - - {% else %} - - {% endif %} - - - {% trans "Third party integrations" %} - -
-
+ {% if favicon_task_allowed %} + + {% trans "Fetch favicons" %} + + {% else %} + + {% endif %} +
+
{% endblock actions %} diff --git a/src/newsreader/accounts/templates/accounts/views/integrations.html b/src/newsreader/accounts/templates/accounts/views/integrations.html deleted file mode 100644 index 559d3d2..0000000 --- a/src/newsreader/accounts/templates/accounts/views/integrations.html +++ /dev/null @@ -1,47 +0,0 @@ -{% extends "sidebar.html" %} -{% load i18n %} - -{% block content %} -
-
-
- {% include "components/header/header.html" with title="Integrations" only %} - -
-

Reddit

-
- {% if reddit_authorization_url %} - - {% trans "Authorize account" %} - - {% else %} - - {% endif %} - - {% if reddit_refresh_url %} - - {% trans "Refresh token" %} - - {% else %} - - {% endif %} - - {% if reddit_revoke_url %} - - {% trans "Deauthorize account" %} - - {% else %} - - {% endif %} -
-
-
-
-
-{% endblock %} diff --git a/src/newsreader/accounts/templates/accounts/views/reddit.html b/src/newsreader/accounts/templates/accounts/views/reddit.html deleted file mode 100644 index 9fa8378..0000000 --- a/src/newsreader/accounts/templates/accounts/views/reddit.html +++ /dev/null @@ -1,22 +0,0 @@ -{% extends "sidebar.html" %} -{% load i18n %} - -{% block content %} -
-
-
- {% if error %} -

{% trans "Reddit authorization failed" %}

-

{{ error }}

- {% elif access_token and refresh_token %} -

{% trans "Reddit account is linked" %}

-

{% trans "Your reddit account was successfully linked." %}

- {% endif %} - -

- {% trans "Return to integrations page" %} -

-
-
-
-{% endblock %} diff --git a/src/newsreader/accounts/tests/test_integrations.py b/src/newsreader/accounts/tests/test_integrations.py deleted file mode 100644 index de0b142..0000000 --- a/src/newsreader/accounts/tests/test_integrations.py +++ /dev/null @@ -1,275 +0,0 @@ -from unittest.mock import patch -from urllib.parse import urlencode -from uuid import uuid4 - -from django.core.cache import cache -from django.test import TestCase -from django.urls import reverse - -from bs4 import BeautifulSoup - -from newsreader.accounts.tests.factories import UserFactory -from newsreader.news.collection.exceptions import ( - StreamException, - StreamTooManyException, -) - - -class IntegrationsViewTestCase(TestCase): - def setUp(self): - self.user = UserFactory(email="test@test.nl", password="test") - self.client.force_login(self.user) - - self.url = reverse("accounts:settings:integrations") - - -class RedditIntegrationsTestCase(IntegrationsViewTestCase): - def test_reddit_authorization(self): - self.user.reddit_refresh_token = None - self.user.save() - - response = self.client.get(self.url) - - soup = BeautifulSoup(response.content, features="lxml") - button = soup.find("a", class_="link button button--reddit") - - self.assertEquals(button.text.strip(), "Authorize account") - - def test_reddit_refresh_token(self): - self.user.reddit_refresh_token = "jadajadajada" - self.user.reddit_access_token = None - self.user.save() - - response = self.client.get(self.url) - - soup = BeautifulSoup(response.content, features="lxml") - button = soup.find("a", class_="link button button--reddit") - - self.assertEquals(button.text.strip(), "Refresh token") - - def test_reddit_revoke(self): - self.user.reddit_refresh_token = "jadajadajada" - self.user.reddit_access_token = None - self.user.save() - - response = self.client.get(self.url) - - soup = BeautifulSoup(response.content, features="lxml") - buttons = soup.find_all("a", class_="link button button--reddit") - - self.assertIn( - "Deauthorize account", [button.text.strip() for button in buttons] - ) - - -class RedditTemplateViewTestCase(TestCase): - def setUp(self): - self.user = UserFactory(email="test@test.nl", password="test") - self.client.force_login(self.user) - - self.base_url = reverse("accounts:settings:reddit-template") - self.state = str(uuid4()) - - self.patch = patch("newsreader.news.collection.reddit.post") - self.mocked_post = self.patch.start() - - def tearDown(self): - patch.stopall() - - def test_simple(self): - response = self.client.get(self.base_url) - - self.assertEquals(response.status_code, 200) - self.assertContains(response, "Return to integrations page") - - def test_successful_authorization(self): - self.mocked_post.return_value.json.return_value = { - "access_token": "1001010412", - "refresh_token": "134510143", - } - - cache.set(f"{self.user.email}-reddit-auth", self.state) - - params = {"state": self.state, "code": "Valid code"} - url = f"{self.base_url}?{urlencode(params)}" - - response = self.client.get(url) - - self.mocked_post.assert_called_once() - - self.assertEquals(response.status_code, 200) - self.assertContains(response, "Your reddit account was successfully linked.") - - self.user.refresh_from_db() - - self.assertEquals(self.user.reddit_access_token, "1001010412") - self.assertEquals(self.user.reddit_refresh_token, "134510143") - - self.assertEquals(cache.get(f"{self.user.email}-reddit-auth"), None) - - def test_error(self): - params = {"error": "Denied authorization"} - - url = f"{self.base_url}?{urlencode(params)}" - - response = self.client.get(url) - - self.assertEquals(response.status_code, 200) - self.assertContains(response, "Denied authorization") - - def test_invalid_state(self): - cache.set(f"{self.user.email}-reddit-auth", str(uuid4())) - - params = {"code": "Valid code", "state": "Invalid state"} - - url = f"{self.base_url}?{urlencode(params)}" - - response = self.client.get(url) - - self.assertEquals(response.status_code, 200) - self.assertContains( - response, "The saved state for Reddit authorization did not match" - ) - - def test_stream_error(self): - self.mocked_post.side_effect = StreamTooManyException - - cache.set(f"{self.user.email}-reddit-auth", self.state) - - params = {"state": self.state, "code": "Valid code"} - url = f"{self.base_url}?{urlencode(params)}" - - response = self.client.get(url) - - self.mocked_post.assert_called_once() - - self.assertEquals(response.status_code, 200) - self.assertContains(response, "Too many requests") - - self.user.refresh_from_db() - - self.assertEquals(self.user.reddit_access_token, None) - self.assertEquals(self.user.reddit_refresh_token, None) - - self.assertEquals(cache.get(f"{self.user.email}-reddit-auth"), self.state) - - def test_unexpected_json(self): - self.mocked_post.return_value.json.return_value = {"message": "Happy eastern"} - - cache.set(f"{self.user.email}-reddit-auth", self.state) - - params = {"state": self.state, "code": "Valid code"} - url = f"{self.base_url}?{urlencode(params)}" - - response = self.client.get(url) - - self.mocked_post.assert_called_once() - - self.assertEquals(response.status_code, 200) - self.assertContains(response, "Access and refresh token not found in response") - - self.user.refresh_from_db() - - self.assertEquals(self.user.reddit_access_token, None) - self.assertEquals(self.user.reddit_refresh_token, None) - - self.assertEquals(cache.get(f"{self.user.email}-reddit-auth"), self.state) - - -class RedditTokenRedirectViewTestCase(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.integrations.RedditTokenTask") - self.mocked_task = self.patch.start() - - def tearDown(self): - cache.clear() - - def test_simple(self): - response = self.client.get(reverse("accounts:settings:reddit-refresh")) - - self.assertRedirects(response, reverse("accounts:settings:integrations")) - - self.mocked_task.delay.assert_called_once_with(self.user.pk) - - self.assertEquals(1, cache.get(f"{self.user.email}-reddit-refresh")) - - def test_not_active(self): - cache.set(f"{self.user.email}-reddit-refresh", 1) - - response = self.client.get(reverse("accounts:settings:reddit-refresh")) - - self.assertRedirects(response, reverse("accounts:settings:integrations")) - - self.mocked_task.delay.assert_not_called() - - -class RedditRevokeRedirectViewTestCase(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.integrations.revoke_reddit_token") - self.mocked_revoke = self.patch.start() - - def test_simple(self): - self.user.reddit_access_token = "jadajadajada" - self.user.reddit_refresh_token = "jadajadajada" - self.user.save() - - self.mocked_revoke.return_value = True - - response = self.client.get(reverse("accounts:settings:reddit-revoke")) - - self.assertRedirects(response, reverse("accounts:settings:integrations")) - - self.mocked_revoke.assert_called_once_with(self.user) - - self.user.refresh_from_db() - - self.assertEquals(self.user.reddit_access_token, None) - self.assertEquals(self.user.reddit_refresh_token, None) - - def test_no_refresh_token(self): - self.user.reddit_refresh_token = None - self.user.save() - - response = self.client.get(reverse("accounts:settings:reddit-revoke")) - - self.assertRedirects(response, reverse("accounts:settings:integrations")) - - self.mocked_revoke.assert_not_called() - - def test_unsuccessful_response(self): - self.user.reddit_access_token = "jadajadajada" - self.user.reddit_refresh_token = "jadajadajada" - self.user.save() - - self.mocked_revoke.return_value = False - - response = self.client.get(reverse("accounts:settings:reddit-revoke")) - - self.assertRedirects(response, reverse("accounts:settings:integrations")) - - self.user.refresh_from_db() - - self.assertEquals(self.user.reddit_access_token, "jadajadajada") - self.assertEquals(self.user.reddit_refresh_token, "jadajadajada") - - def test_stream_exception(self): - self.user.reddit_access_token = "jadajadajada" - self.user.reddit_refresh_token = "jadajadajada" - self.user.save() - - self.mocked_revoke.side_effect = StreamException - - response = self.client.get(reverse("accounts:settings:reddit-revoke")) - - self.assertRedirects(response, reverse("accounts:settings:integrations")) - - self.user.refresh_from_db() - - self.assertEquals(self.user.reddit_access_token, "jadajadajada") - self.assertEquals(self.user.reddit_refresh_token, "jadajadajada") diff --git a/src/newsreader/accounts/urls.py b/src/newsreader/accounts/urls.py index b363f82..18a9b21 100644 --- a/src/newsreader/accounts/urls.py +++ b/src/newsreader/accounts/urls.py @@ -3,7 +3,6 @@ from django.urls import include, path from newsreader.accounts.views import ( FaviconRedirectView, - IntegrationsView, LoginView, LogoutView, PasswordChangeView, @@ -11,33 +10,11 @@ from newsreader.accounts.views import ( PasswordResetConfirmView, PasswordResetDoneView, PasswordResetView, - RedditRevokeRedirectView, - RedditTemplateView, - RedditTokenRedirectView, SettingsView, ) 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/", 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"), diff --git a/src/newsreader/accounts/views/__init__.py b/src/newsreader/accounts/views/__init__.py index e62755c..d20e6bb 100644 --- a/src/newsreader/accounts/views/__init__.py +++ b/src/newsreader/accounts/views/__init__.py @@ -1,11 +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, - RedditTemplateView, - RedditTokenRedirectView, -) from newsreader.accounts.views.password import ( PasswordChangeView, PasswordResetCompleteView, @@ -20,10 +14,6 @@ __all__ = [ "LoginView", "LogoutView", "FaviconRedirectView", - "IntegrationsView", - "RedditRevokeRedirectView", - "RedditTemplateView", - "RedditTokenRedirectView", "PasswordChangeView", "PasswordResetCompleteView", "PasswordResetConfirmView", diff --git a/src/newsreader/accounts/views/integrations.py b/src/newsreader/accounts/views/integrations.py deleted file mode 100644 index 1235195..0000000 --- a/src/newsreader/accounts/views/integrations.py +++ /dev/null @@ -1,156 +0,0 @@ -import logging - -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, TemplateView - -from newsreader.news.collection.exceptions import StreamException -from newsreader.news.collection.reddit import ( - get_reddit_access_token, - get_reddit_authorization_url, - revoke_reddit_token, -) -from newsreader.news.collection.tasks import RedditTokenTask -from newsreader.utils.views import NavListMixin - - -logger = logging.getLogger(__name__) - - -class IntegrationsView(NavListMixin, TemplateView): - template_name = "accounts/views/integrations.html" - - def get_context_data(self, **kwargs): - return { - **super().get_context_data(**kwargs), - **self.get_reddit_context(**kwargs), - } - - def get_reddit_context(self, **kwargs): - user = self.request.user - reddit_authorization_url = None - reddit_refresh_url = None - - reddit_task_active = cache.get(f"{user.email}-reddit-refresh") - - if ( - user.reddit_refresh_token - and not user.reddit_access_token - and not reddit_task_active - ): - reddit_refresh_url = reverse_lazy("accounts:settings:reddit-refresh") - - if not user.reddit_refresh_token: - reddit_authorization_url = get_reddit_authorization_url(user) - - return { - "reddit_authorization_url": reddit_authorization_url, - "reddit_refresh_url": reddit_refresh_url, - "reddit_revoke_url": ( - reverse_lazy("accounts:settings:reddit-revoke") - if not reddit_authorization_url - else None - ), - } - - -class RedditTemplateView(NavListMixin, TemplateView): - template_name = "accounts/views/reddit.html" - - def get(self, request, *args, **kwargs): - context = self.get_context_data(**kwargs) - - error = request.GET.get("error", None) - state = request.GET.get("state", None) - code = request.GET.get("code", None) - - if error: - return self.render_to_response({**context, "error": error}) - - if not code or not state: - return self.render_to_response(context) - - cached_state = cache.get(f"{request.user.email}-reddit-auth") - - if state != cached_state: - return self.render_to_response( - { - **context, - "error": _( - "The saved state for Reddit authorization did not match" - ), - } - ) - - try: - access_token, refresh_token = get_reddit_access_token(code, request.user) - - return self.render_to_response( - { - **context, - "access_token": access_token, - "refresh_token": refresh_token, - } - ) - except StreamException as e: - return self.render_to_response({**context, "error": str(e)}) - except KeyError: - return self.render_to_response( - { - **context, - "error": _("Access and refresh token not found in response"), - } - ) - - -class RedditTokenRedirectView(RedirectView): - url = reverse_lazy("accounts:settings:integrations") - - def get(self, request, *args, **kwargs): - response = super().get(request, *args, **kwargs) - - user = request.user - task_active = cache.get(f"{user.email}-reddit-refresh") - - if not task_active: - RedditTokenTask.delay(user.pk) - messages.success(request, _("Access token is being retrieved")) - cache.set(f"{user.email}-reddit-refresh", 1, 300) - return response - - messages.error(request, _("Unable to retrieve token")) - return response - - -class RedditRevokeRedirectView(RedirectView): - url = reverse_lazy("accounts:settings:integrations") - - def get(self, request, *args, **kwargs): - response = super().get(request, *args, **kwargs) - - user = request.user - - if not user.reddit_refresh_token: - messages.error(request, _("No reddit account is linked to this account")) - return response - - try: - is_revoked = revoke_reddit_token(user) - except StreamException: - logger.exception(f"Unable to revoke reddit token for {user.pk}") - - messages.error(request, _("Unable to revoke reddit token")) - return response - - if not is_revoked: - messages.error(request, _("Unable to revoke reddit token")) - return response - - user.reddit_access_token = None - user.reddit_refresh_token = None - user.save() - - messages.success(request, _("Reddit account deathorized")) - return response diff --git a/src/newsreader/conf/base.py b/src/newsreader/conf/base.py index d17234a..5bee027 100644 --- a/src/newsreader/conf/base.py +++ b/src/newsreader/conf/base.py @@ -209,16 +209,6 @@ STATICFILES_FINDERS = [ # Email EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" -# Reddit integration -REDDIT_CLIENT_ID = "CLIENT_ID" -REDDIT_CLIENT_SECRET = "CLIENT_SECRET" -REDDIT_REDIRECT_URL = ( - "http://127.0.0.1:8000/accounts/settings/integrations/reddit/callback/" -) - -# Twitter integration -TWITTER_URL = "https://twitter.com" - # Third party settings AXES_HANDLER = "axes.handlers.cache.AxesCacheHandler" AXES_CACHE = "axes" diff --git a/src/newsreader/conf/production.py b/src/newsreader/conf/production.py index 8615aa2..ea22f30 100644 --- a/src/newsreader/conf/production.py +++ b/src/newsreader/conf/production.py @@ -48,11 +48,6 @@ EMAIL_USE_SSL = bool(os.environ.get("EMAIL_USE_SSL")) VERSION = get_current_version(debug=False) ENVIRONMENT = "production" -# Reddit integration -REDDIT_CLIENT_ID = os.environ.get("REDDIT_CLIENT_ID", "") -REDDIT_CLIENT_SECRET = os.environ.get("REDDIT_CLIENT_SECRET", "") -REDDIT_REDIRECT_URL = os.environ.get("REDDIT_CALLBACK_URL", "") - # Third party settings AXES_HANDLER = "axes.handlers.database.AxesDatabaseHandler" diff --git a/src/newsreader/js/components/Messages.js b/src/newsreader/js/components/Messages.js index 3fed5de..e3d776e 100644 --- a/src/newsreader/js/components/Messages.js +++ b/src/newsreader/js/components/Messages.js @@ -3,9 +3,7 @@ import React from 'react'; class Messages extends React.Component { state = { messages: this.props.messages }; - close = ::this.close; - - close(index) { + close = (index) => { const newMessages = this.state.messages.filter((message, currentIndex) => { return currentIndex != index; }); diff --git a/src/newsreader/js/pages/homepage/App.js b/src/newsreader/js/pages/homepage/App.js index 08c0330..e840407 100644 --- a/src/newsreader/js/pages/homepage/App.js +++ b/src/newsreader/js/pages/homepage/App.js @@ -33,7 +33,6 @@ class App extends React.Component { { const targetClassName = e.target.className; if (this.props.post && targetClassName == 'modal post-modal') { @@ -54,9 +53,6 @@ class PostModal extends React.Component { let ruleUrl = ''; switch (this.props.rule.type) { - case SUBREDDIT: - ruleUrl = `${this.props.subredditUrl}/${this.props.rule.id}/`; - break; default: ruleUrl = `${this.props.feedUrl}/${this.props.rule.id}/`; break; @@ -70,7 +66,7 @@ class PostModal extends React.Component {