diff options
Diffstat (limited to 'pydis_site/apps/home')
-rw-r--r-- | pydis_site/apps/home/forms/__init__.py | 0 | ||||
-rw-r--r-- | pydis_site/apps/home/forms/account_deletion.py | 10 | ||||
-rw-r--r-- | pydis_site/apps/home/signals.py | 88 | ||||
-rw-r--r-- | pydis_site/apps/home/tests/mock_github_api_response.json | 2 | ||||
-rw-r--r-- | pydis_site/apps/home/tests/test_repodata_helpers.py | 2 | ||||
-rw-r--r-- | pydis_site/apps/home/tests/test_signal_listener.py | 83 | ||||
-rw-r--r-- | pydis_site/apps/home/tests/test_views.py | 197 | ||||
-rw-r--r-- | pydis_site/apps/home/urls.py | 11 | ||||
-rw-r--r-- | pydis_site/apps/home/views/__init__.py | 3 | ||||
-rw-r--r-- | pydis_site/apps/home/views/account/__init__.py | 4 | ||||
-rw-r--r-- | pydis_site/apps/home/views/account/delete.py | 37 | ||||
-rw-r--r-- | pydis_site/apps/home/views/account/settings.py | 59 | ||||
-rw-r--r-- | pydis_site/apps/home/views/home.py | 4 |
13 files changed, 461 insertions, 39 deletions
diff --git a/pydis_site/apps/home/forms/__init__.py b/pydis_site/apps/home/forms/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/pydis_site/apps/home/forms/__init__.py diff --git a/pydis_site/apps/home/forms/account_deletion.py b/pydis_site/apps/home/forms/account_deletion.py new file mode 100644 index 00000000..eec70bea --- /dev/null +++ b/pydis_site/apps/home/forms/account_deletion.py @@ -0,0 +1,10 @@ +from django.forms import CharField, Form + + +class AccountDeletionForm(Form): + """Account deletion form, to collect username for confirmation of removal.""" + + username = CharField( + label="Username", + required=True + ) diff --git a/pydis_site/apps/home/signals.py b/pydis_site/apps/home/signals.py index 9f286882..8af48c15 100644 --- a/pydis_site/apps/home/signals.py +++ b/pydis_site/apps/home/signals.py @@ -1,3 +1,4 @@ +from contextlib import suppress from typing import List, Optional, Type from allauth.account.signals import user_logged_in @@ -8,7 +9,7 @@ from allauth.socialaccount.signals import ( pre_social_login, social_account_added, social_account_removed, social_account_updated) from django.contrib.auth.models import Group, User as DjangoUser -from django.db.models.signals import post_save, pre_delete, pre_save +from django.db.models.signals import post_delete, post_save, pre_save from pydis_site.apps.api.models import User as DiscordUser from pydis_site.apps.staff.models import RoleMapping @@ -37,7 +38,7 @@ class AllauthSignalListener: def __init__(self): post_save.connect(self.user_model_updated, sender=DiscordUser) - pre_delete.connect(self.mapping_model_deleted, sender=RoleMapping) + post_delete.connect(self.mapping_model_deleted, sender=RoleMapping) pre_save.connect(self.mapping_model_updated, sender=RoleMapping) pre_social_login.connect(self.social_account_updated) @@ -133,13 +134,29 @@ class AllauthSignalListener: Processes deletion signals from the RoleMapping model, removing perms from users. We need to do this to ensure that users aren't left with permissions groups that - they shouldn't have assigned to them when a RoleMapping is deleted from the database. + they shouldn't have assigned to them when a RoleMapping is deleted from the database, + and to remove their staff status if they should no longer have it. """ instance: RoleMapping = kwargs["instance"] for user in instance.group.user_set.all(): + # Firstly, remove their related user group user.groups.remove(instance.group) + with suppress(SocialAccount.DoesNotExist, DiscordUser.DoesNotExist): + # If we get either exception, then the user could not have been assigned staff + # with our system in the first place. + + social_account = SocialAccount.objects.get(user=user, provider=DiscordProvider.id) + discord_user = DiscordUser.objects.get(id=int(social_account.uid)) + + mappings = RoleMapping.objects.filter(role__id__in=discord_user.roles).all() + is_staff = any(m.is_staff for m in mappings) + + if user.is_staff != is_staff: + user.is_staff = is_staff + user.save(update_fields=("is_staff", )) + def mapping_model_updated(self, sender: Type[RoleMapping], **kwargs) -> None: """ Processes update signals from the RoleMapping model. @@ -168,12 +185,27 @@ class AllauthSignalListener: self.mapping_model_deleted(RoleMapping, instance=old_instance) accounts = SocialAccount.objects.filter( - uid__in=(u.id for u in instance.role.user_set.all()) + uid__in=(u.id for u in DiscordUser.objects.filter(roles__contains=[instance.role.id])) ) for account in accounts: account.user.groups.add(instance.group) + if instance.is_staff and not account.user.is_staff: + account.user.is_staff = instance.is_staff + account.user.save(update_fields=("is_staff", )) + else: + discord_user = DiscordUser.objects.get(id=int(account.uid)) + + mappings = RoleMapping.objects.filter( + role__id__in=discord_user.roles + ).exclude(id=instance.id).all() + is_staff = any(m.is_staff for m in mappings) + + if account.user.is_staff != is_staff: + account.user.is_staff = is_staff + account.user.save(update_fields=("is_staff",)) + def user_model_updated(self, sender: Type[DiscordUser], **kwargs) -> None: """ Processes update signals from the Discord User model, assigning perms as required. @@ -230,31 +262,53 @@ class AllauthSignalListener: except SocialAccount.user.RelatedObjectDoesNotExist: return # There's no user account yet, this will be handled by another receiver + # Ensure that the username on this account is correct + new_username = f"{user.name}#{user.discriminator}" + + if account.user.username != new_username: + account.user.username = new_username + account.user.first_name = new_username + if not user.in_guild: deletion = True if deletion: # They've unlinked Discord or left the server, so we have to remove their groups + # and their staff status - if not current_groups: - return # They have no groups anyway, no point in processing + if current_groups: + # They do have groups, so let's remove them + account.user.groups.remove( + *(mapping.group for mapping in mappings) + ) - account.user.groups.remove( - *(mapping.group for mapping in mappings) - ) + if account.user.is_staff: + # They're marked as a staff user and they shouldn't be, so let's fix that + account.user.is_staff = False else: new_groups = [] + is_staff = False - for role in user.roles.all(): + for role in user.roles: try: - new_groups.append(mappings.get(role=role).group) + mapping = mappings.get(role__id=role) except RoleMapping.DoesNotExist: continue # No mapping exists - account.user.groups.add( - *[group for group in new_groups if group not in current_groups] - ) + new_groups.append(mapping.group) - account.user.groups.remove( - *[mapping.group for mapping in mappings if mapping.group not in new_groups] - ) + if mapping.is_staff: + is_staff = True + + account.user.groups.add( + *[group for group in new_groups if group not in current_groups] + ) + + account.user.groups.remove( + *[mapping.group for mapping in mappings if mapping.group not in new_groups] + ) + + if account.user.is_staff != is_staff: + account.user.is_staff = is_staff + + account.user.save() diff --git a/pydis_site/apps/home/tests/mock_github_api_response.json b/pydis_site/apps/home/tests/mock_github_api_response.json index 37dc672e..10be4f99 100644 --- a/pydis_site/apps/home/tests/mock_github_api_response.json +++ b/pydis_site/apps/home/tests/mock_github_api_response.json @@ -28,7 +28,7 @@ "forks_count": 31 }, { - "full_name": "python-discord/django-crispy-bulma", + "full_name": "python-discord/metricity", "description": "test", "stargazers_count": 97, "language": "Python", diff --git a/pydis_site/apps/home/tests/test_repodata_helpers.py b/pydis_site/apps/home/tests/test_repodata_helpers.py index 71bd4f2d..77b1a68d 100644 --- a/pydis_site/apps/home/tests/test_repodata_helpers.py +++ b/pydis_site/apps/home/tests/test_repodata_helpers.py @@ -10,7 +10,7 @@ from pydis_site.apps.home.models import RepositoryMetadata from pydis_site.apps.home.views import HomeView -def mocked_requests_get(*args, **kwargs) -> "MockResponse": # noqa +def mocked_requests_get(*args, **kwargs) -> "MockResponse": # noqa: F821 """A mock version of requests.get, so we don't need to call the API every time we run a test.""" class MockResponse: def __init__(self, json_data, status_code): diff --git a/pydis_site/apps/home/tests/test_signal_listener.py b/pydis_site/apps/home/tests/test_signal_listener.py index 27fc7710..d99d81a5 100644 --- a/pydis_site/apps/home/tests/test_signal_listener.py +++ b/pydis_site/apps/home/tests/test_signal_listener.py @@ -67,36 +67,35 @@ class SignalListenerTests(TestCase): cls.admin_mapping = RoleMapping.objects.create( role=cls.admin_role, - group=cls.admin_group + group=cls.admin_group, + is_staff=True ) cls.moderator_mapping = RoleMapping.objects.create( role=cls.moderator_role, - group=cls.moderator_group + group=cls.moderator_group, + is_staff=False ) cls.discord_user = DiscordUser.objects.create( id=0, name="user", discriminator=0, - avatar_hash=None ) cls.discord_unmapped = DiscordUser.objects.create( id=2, name="unmapped", discriminator=0, - avatar_hash=None ) - cls.discord_unmapped.roles.add(cls.unmapped_role) + cls.discord_unmapped.roles.append(cls.unmapped_role.id) cls.discord_unmapped.save() cls.discord_not_in_guild = DiscordUser.objects.create( id=3, name="not-in-guild", discriminator=0, - avatar_hash=None, in_guild=False ) @@ -104,20 +103,18 @@ class SignalListenerTests(TestCase): id=1, name="admin", discriminator=0, - avatar_hash=None ) - cls.discord_admin.roles.set([cls.admin_role]) + cls.discord_admin.roles = [cls.admin_role.id] cls.discord_admin.save() cls.discord_moderator = DiscordUser.objects.create( id=4, name="admin", discriminator=0, - avatar_hash=None ) - cls.discord_moderator.roles.set([cls.moderator_role]) + cls.discord_moderator.roles = [cls.moderator_role.id] cls.discord_moderator.save() cls.django_user_discordless = DjangoUser.objects.create(username="no-discord") @@ -166,7 +163,7 @@ class SignalListenerTests(TestCase): cls.django_moderator = DjangoUser.objects.create( username="moderator", - is_staff=True, + is_staff=False, is_superuser=False ) @@ -336,9 +333,36 @@ class SignalListenerTests(TestCase): handler._apply_groups(self.discord_admin, self.social_admin) self.assertEqual(self.django_user_discordless.groups.all().count(), 0) - self.discord_admin.roles.add(self.admin_role) + self.discord_admin.roles.append(self.admin_role.id) self.discord_admin.save() + def test_apply_groups_moderator(self): + """Test application of groups by role, relating to a non-`is_staff` moderator user.""" + handler = AllauthSignalListener() + + self.assertEqual(self.django_user_discordless.groups.all().count(), 0) + + # Apply groups based on moderator role being present on Discord + handler._apply_groups(self.discord_moderator, self.social_moderator) + self.assertTrue(self.moderator_group in self.django_moderator.groups.all()) + + # Remove groups based on the user apparently leaving the server + handler._apply_groups(self.discord_moderator, self.social_moderator, True) + self.assertEqual(self.django_user_discordless.groups.all().count(), 0) + + # Apply the moderator role again + handler._apply_groups(self.discord_moderator, self.social_moderator) + + # Remove all of the roles from the user + self.discord_moderator.roles.clear() + + # Remove groups based on the user no longer having the moderator role on Discord + handler._apply_groups(self.discord_moderator, self.social_moderator) + self.assertEqual(self.django_user_discordless.groups.all().count(), 0) + + self.discord_moderator.roles.append(self.moderator_role.id) + self.discord_moderator.save() + def test_apply_groups_other(self): """Test application of groups by role, relating to non-standard cases.""" handler = AllauthSignalListener() @@ -373,10 +397,25 @@ class SignalListenerTests(TestCase): self.assertEqual(self.django_moderator.groups.all().count(), 1) self.assertEqual(self.django_admin.groups.all().count(), 1) + # Test is_staff changes + self.admin_mapping.is_staff = False + self.admin_mapping.save() + + self.assertFalse(self.django_moderator.is_staff) + self.assertFalse(self.django_admin.is_staff) + + self.admin_mapping.is_staff = True + self.admin_mapping.save() + + self.django_admin.refresh_from_db(fields=("is_staff", )) + self.assertTrue(self.django_admin.is_staff) + # Test mapping deletion self.admin_mapping.delete() + self.django_admin.refresh_from_db(fields=("is_staff",)) self.assertEqual(self.django_admin.groups.all().count(), 0) + self.assertFalse(self.django_admin.is_staff) # Test mapping update self.moderator_mapping.group = self.admin_group @@ -388,12 +427,30 @@ class SignalListenerTests(TestCase): # Test mapping creation new_mapping = RoleMapping.objects.create( role=self.admin_role, - group=self.moderator_group + group=self.moderator_group, + is_staff=True + ) + + self.assertEqual(self.django_admin.groups.all().count(), 1) + self.assertTrue(self.moderator_group in self.django_admin.groups.all()) + + self.django_admin.refresh_from_db(fields=("is_staff",)) + self.assertTrue(self.django_admin.is_staff) + + new_mapping.delete() + + # Test mapping creation (without is_staff) + new_mapping = RoleMapping.objects.create( + role=self.admin_role, + group=self.moderator_group, ) self.assertEqual(self.django_admin.groups.all().count(), 1) self.assertTrue(self.moderator_group in self.django_admin.groups.all()) + self.django_admin.refresh_from_db(fields=("is_staff",)) + self.assertFalse(self.django_admin.is_staff) + # Test that nothing happens when fixtures are loaded pre_save.send(RoleMapping, instance=new_mapping, raw=True) diff --git a/pydis_site/apps/home/tests/test_views.py b/pydis_site/apps/home/tests/test_views.py index 7aeaddd2..572317a7 100644 --- a/pydis_site/apps/home/tests/test_views.py +++ b/pydis_site/apps/home/tests/test_views.py @@ -1,5 +1,198 @@ +from allauth.socialaccount.models import SocialAccount +from django.contrib.auth.models import User +from django.http import HttpResponseRedirect from django.test import TestCase -from django_hosts.resolvers import reverse +from django_hosts.resolvers import get_host, reverse, reverse_host + + +def check_redirect_url( + response: HttpResponseRedirect, reversed_url: str, strip_params=True +) -> bool: + """ + Check whether a given redirect response matches a specific reversed URL. + + Arguments: + * `response`: The HttpResponseRedirect returned by the test client + * `reversed_url`: The URL returned by `reverse()` + * `strip_params`: Whether to strip URL parameters (following a "?") from the URL given in the + `response` object + """ + host = get_host(None) + hostname = reverse_host(host) + + redirect_url = response.url + + if strip_params and "?" in redirect_url: + redirect_url = redirect_url.split("?", 1)[0] + + result = reversed_url == f"//{hostname}{redirect_url}" + return result + + +class TestAccountDeleteView(TestCase): + def setUp(self) -> None: + """Create an authorized Django user for testing purposes.""" + self.user = User.objects.create( + username="user#0000" + ) + + def test_redirect_when_logged_out(self): + """Test that the user is redirected to the homepage when not logged in.""" + url = reverse("account_delete") + resp = self.client.get(url) + self.assertEqual(resp.status_code, 302) + self.assertTrue(check_redirect_url(resp, reverse("home"))) + + def test_get_when_logged_in(self): + """Test that the view returns a HTTP 200 when the user is logged in.""" + url = reverse("account_delete") + + self.client.force_login(self.user) + resp = self.client.get(url) + self.client.logout() + + self.assertEqual(resp.status_code, 200) + + def test_post_invalid(self): + """Test that the user is redirected when the form is filled out incorrectly.""" + url = reverse("account_delete") + + self.client.force_login(self.user) + + resp = self.client.post(url, {}) + self.assertEqual(resp.status_code, 302) + self.assertTrue(check_redirect_url(resp, url)) + + resp = self.client.post(url, {"username": "user"}) + self.assertEqual(resp.status_code, 302) + self.assertTrue(check_redirect_url(resp, url)) + + self.client.logout() + + def test_post_valid(self): + """Test that the account is deleted when the form is filled out correctly..""" + url = reverse("account_delete") + + self.client.force_login(self.user) + + resp = self.client.post(url, {"username": "user#0000"}) + self.assertEqual(resp.status_code, 302) + self.assertTrue(check_redirect_url(resp, reverse("home"))) + + with self.assertRaises(User.DoesNotExist): + User.objects.get(username=self.user.username) + + self.client.logout() + + +class TestAccountSettingsView(TestCase): + def setUp(self) -> None: + """Create an authorized Django user for testing purposes.""" + self.user = User.objects.create( + username="user#0000" + ) + + self.user_unlinked = User.objects.create( + username="user#9999" + ) + + self.user_unlinked_discord = User.objects.create( + username="user#1234" + ) + + self.user_unlinked_github = User.objects.create( + username="user#1111" + ) + + self.github_account = SocialAccount.objects.create( + user=self.user, + provider="github", + uid="0" + ) + + self.discord_account = SocialAccount.objects.create( + user=self.user, + provider="discord", + uid="0000" + ) + + self.github_account_secondary = SocialAccount.objects.create( + user=self.user_unlinked_discord, + provider="github", + uid="1" + ) + + self.discord_account_secondary = SocialAccount.objects.create( + user=self.user_unlinked_github, + provider="discord", + uid="1111" + ) + + def test_redirect_when_logged_out(self): + """Check that the user is redirected to the homepage when not logged in.""" + url = reverse("account_settings") + resp = self.client.get(url) + self.assertEqual(resp.status_code, 302) + self.assertTrue(check_redirect_url(resp, reverse("home"))) + + def test_get_when_logged_in(self): + """Test that the view returns a HTTP 200 when the user is logged in.""" + url = reverse("account_settings") + + self.client.force_login(self.user) + resp = self.client.get(url) + self.client.logout() + + self.assertEqual(resp.status_code, 200) + + self.client.force_login(self.user_unlinked) + resp = self.client.get(url) + self.client.logout() + + self.assertEqual(resp.status_code, 200) + + self.client.force_login(self.user_unlinked_discord) + resp = self.client.get(url) + self.client.logout() + + self.assertEqual(resp.status_code, 200) + + self.client.force_login(self.user_unlinked_github) + resp = self.client.get(url) + self.client.logout() + + self.assertEqual(resp.status_code, 200) + + def test_post_invalid(self): + """Test the behaviour of invalid POST submissions.""" + url = reverse("account_settings") + + self.client.force_login(self.user_unlinked) + + resp = self.client.post(url, {"provider": "discord"}) + self.assertEqual(resp.status_code, 302) + self.assertTrue(check_redirect_url(resp, reverse("home"))) + + resp = self.client.post(url, {"provider": "github"}) + self.assertEqual(resp.status_code, 302) + self.assertTrue(check_redirect_url(resp, reverse("home"))) + + self.client.logout() + + def test_post_valid(self): + """Ensure that GitHub is unlinked with a valid POST submission.""" + url = reverse("account_settings") + + self.client.force_login(self.user) + + resp = self.client.post(url, {"provider": "github"}) + self.assertEqual(resp.status_code, 302) + self.assertTrue(check_redirect_url(resp, reverse("home"))) + + with self.assertRaises(SocialAccount.DoesNotExist): + SocialAccount.objects.get(user=self.user, provider="github") + + self.client.logout() class TestIndexReturns200(TestCase): @@ -16,6 +209,7 @@ class TestLoginCancelledReturns302(TestCase): url = reverse('socialaccount_login_cancelled') resp = self.client.get(url) self.assertEqual(resp.status_code, 302) + self.assertTrue(check_redirect_url(resp, reverse("home"))) class TestLoginErrorReturns302(TestCase): @@ -24,3 +218,4 @@ class TestLoginErrorReturns302(TestCase): url = reverse('socialaccount_login_error') resp = self.client.get(url) self.assertEqual(resp.status_code, 302) + self.assertTrue(check_redirect_url(resp, reverse("home"))) diff --git a/pydis_site/apps/home/urls.py b/pydis_site/apps/home/urls.py index 211a7ad1..61e87a39 100644 --- a/pydis_site/apps/home/urls.py +++ b/pydis_site/apps/home/urls.py @@ -1,5 +1,4 @@ from allauth.account.views import LogoutView -from allauth.socialaccount.views import ConnectionsView from django.conf import settings from django.conf.urls.static import static from django.contrib import admin @@ -7,14 +6,18 @@ from django.contrib.messages import ERROR from django.urls import include, path from pydis_site.utils.views import MessageRedirectView -from .views import HomeView +from .views import AccountDeleteView, AccountSettingsView, HomeView app_name = 'home' urlpatterns = [ + # We do this twice because Allauth expects specific view names to exist path('', HomeView.as_view(), name='home'), + path('', HomeView.as_view(), name='socialaccount_connections'), + path('pages/', include('wiki.urls')), path('accounts/', include('allauth.socialaccount.providers.discord.urls')), + path('accounts/', include('allauth.socialaccount.providers.github.urls')), path( 'accounts/login/cancelled', MessageRedirectView.as_view( @@ -28,7 +31,9 @@ urlpatterns = [ ), name='socialaccount_login_error' ), - path('connections', ConnectionsView.as_view()), + path('accounts/settings', AccountSettingsView.as_view(), name="account_settings"), + path('accounts/delete', AccountDeleteView.as_view(), name="account_delete"), + path('logout', LogoutView.as_view(), name="logout"), path('admin/', admin.site.urls), diff --git a/pydis_site/apps/home/views/__init__.py b/pydis_site/apps/home/views/__init__.py index 971d73a3..801fd398 100644 --- a/pydis_site/apps/home/views/__init__.py +++ b/pydis_site/apps/home/views/__init__.py @@ -1,3 +1,4 @@ +from .account import DeleteView as AccountDeleteView, SettingsView as AccountSettingsView from .home import HomeView -__all__ = ["HomeView"] +__all__ = ["AccountDeleteView", "AccountSettingsView", "HomeView"] diff --git a/pydis_site/apps/home/views/account/__init__.py b/pydis_site/apps/home/views/account/__init__.py new file mode 100644 index 00000000..3b3250ea --- /dev/null +++ b/pydis_site/apps/home/views/account/__init__.py @@ -0,0 +1,4 @@ +from .delete import DeleteView +from .settings import SettingsView + +__all__ = ["DeleteView", "SettingsView"] diff --git a/pydis_site/apps/home/views/account/delete.py b/pydis_site/apps/home/views/account/delete.py new file mode 100644 index 00000000..798b8a33 --- /dev/null +++ b/pydis_site/apps/home/views/account/delete.py @@ -0,0 +1,37 @@ +from django.contrib.auth.mixins import LoginRequiredMixin +from django.contrib.messages import ERROR, INFO, add_message +from django.http import HttpRequest, HttpResponse +from django.shortcuts import redirect, render +from django.urls import reverse +from django.views import View + +from pydis_site.apps.home.forms.account_deletion import AccountDeletionForm + + +class DeleteView(LoginRequiredMixin, View): + """Account deletion view, for removing linked user accounts from the DB.""" + + def __init__(self, *args, **kwargs): + self.login_url = reverse("home") + super().__init__(*args, **kwargs) + + def get(self, request: HttpRequest) -> HttpResponse: + """HTTP GET: Return the view template.""" + return render( + request, "home/account/delete.html", + context={"form": AccountDeletionForm()} + ) + + def post(self, request: HttpRequest) -> HttpResponse: + """HTTP POST: Process the deletion, as requested by the user.""" + form = AccountDeletionForm(request.POST) + + if not form.is_valid() or request.user.username != form.cleaned_data["username"]: + add_message(request, ERROR, "Please enter your username exactly as shown.") + + return redirect(reverse("account_delete")) + + request.user.delete() + add_message(request, INFO, "Your account has been deleted.") + + return redirect(reverse("home")) diff --git a/pydis_site/apps/home/views/account/settings.py b/pydis_site/apps/home/views/account/settings.py new file mode 100644 index 00000000..3a817dbc --- /dev/null +++ b/pydis_site/apps/home/views/account/settings.py @@ -0,0 +1,59 @@ +from allauth.socialaccount.models import SocialAccount +from allauth.socialaccount.providers import registry +from django.contrib.auth.mixins import LoginRequiredMixin +from django.contrib.messages import ERROR, INFO, add_message +from django.http import HttpRequest, HttpResponse +from django.shortcuts import redirect, render +from django.urls import reverse +from django.views import View + + +class SettingsView(LoginRequiredMixin, View): + """ + Account settings view, for managing and deleting user accounts and connections. + + This view actually renders a template with a bare modal, and is intended to be + inserted into another template using JavaScript. + """ + + def __init__(self, *args, **kwargs): + self.login_url = reverse("home") + super().__init__(*args, **kwargs) + + def get(self, request: HttpRequest) -> HttpResponse: + """HTTP GET: Return the view template.""" + context = { + "groups": request.user.groups.all(), + + "discord": None, + "github": None, + + "discord_provider": registry.provider_map.get("discord"), + "github_provider": registry.provider_map.get("github"), + } + + for account in SocialAccount.objects.filter(user=request.user).all(): + if account.provider == "discord": + context["discord"] = account + + if account.provider == "github": + context["github"] = account + + return render(request, "home/account/settings.html", context=context) + + def post(self, request: HttpRequest) -> HttpResponse: + """HTTP POST: Process account disconnections.""" + provider = request.POST["provider"] + + if provider == "github": + try: + account = SocialAccount.objects.get(user=request.user, provider=provider) + except SocialAccount.DoesNotExist: + add_message(request, ERROR, "You do not have a GitHub account linked.") + else: + account.delete() + add_message(request, INFO, "The social account has been disconnected.") + else: + add_message(request, ERROR, f"Unknown provider: {provider}") + + return redirect(reverse("home")) diff --git a/pydis_site/apps/home/views/home.py b/pydis_site/apps/home/views/home.py index 4cf22594..3b5cd5ac 100644 --- a/pydis_site/apps/home/views/home.py +++ b/pydis_site/apps/home/views/home.py @@ -23,8 +23,8 @@ class HomeView(View): "python-discord/bot", "python-discord/snekbox", "python-discord/seasonalbot", + "python-discord/metricity", "python-discord/django-simple-bulma", - "python-discord/django-crispy-bulma", ] def _get_api_data(self) -> Dict[str, Dict[str, str]]: @@ -61,7 +61,7 @@ class HomeView(View): # Try to get new data from the API. If it fails, return the cached data. try: api_repositories = self._get_api_data() - except TypeError: + except (TypeError, ConnectionError): return RepositoryMetadata.objects.all() database_repositories = [] |