aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--pydis_site/__init__.py5
-rw-r--r--pydis_site/apps/home/signals.py84
-rw-r--r--pydis_site/apps/home/tests/test_signal_listener.py70
-rw-r--r--pydis_site/apps/home/urls.py8
-rw-r--r--pydis_site/apps/home/views/__init__.py3
-rw-r--r--pydis_site/apps/home/views/account/__init__.py4
-rw-r--r--pydis_site/apps/home/views/account/delete.py20
-rw-r--r--pydis_site/apps/home/views/account/settings.py20
-rw-r--r--pydis_site/apps/staff/models/role_mapping.py5
-rw-r--r--pydis_site/settings.py9
-rw-r--r--pydis_site/static/css/base/base.css10
-rw-r--r--pydis_site/templates/base/navbar.html10
-rw-r--r--pydis_site/templates/home/account/delete.html12
-rw-r--r--pydis_site/templates/home/account/settings.html12
-rw-r--r--pydis_site/tests/test_utils_account.py50
-rw-r--r--pydis_site/utils/account.py64
16 files changed, 361 insertions, 25 deletions
diff --git a/pydis_site/__init__.py b/pydis_site/__init__.py
index c6146450..df67cf71 100644
--- a/pydis_site/__init__.py
+++ b/pydis_site/__init__.py
@@ -2,3 +2,8 @@ from wiki.plugins.macros.mdx import toc
# Remove the toc header prefix. There's no option for this, so we gotta monkey patch it.
toc.HEADER_ID_PREFIX = ''
+
+# Empty list of validators for Allauth to ponder over. This is referred to in settings.py
+# by a string because Allauth won't let us just give it a list _there_, we have to point
+# at a list _somewhere else_ instead.
+VALIDATORS = []
diff --git a/pydis_site/apps/home/signals.py b/pydis_site/apps/home/signals.py
index 9f286882..4cb4564b 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__in=discord_user.roles.all()).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.
@@ -174,6 +191,21 @@ class AllauthSignalListener:
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__in=discord_user.roles.all()
+ ).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():
try:
- new_groups.append(mappings.get(role=role).group)
+ mapping = mappings.get(role=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/test_signal_listener.py b/pydis_site/apps/home/tests/test_signal_listener.py
index 27fc7710..66a67252 100644
--- a/pydis_site/apps/home/tests/test_signal_listener.py
+++ b/pydis_site/apps/home/tests/test_signal_listener.py
@@ -67,12 +67,14 @@ 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(
@@ -166,7 +168,7 @@ class SignalListenerTests(TestCase):
cls.django_moderator = DjangoUser.objects.create(
username="moderator",
- is_staff=True,
+ is_staff=False,
is_superuser=False
)
@@ -339,6 +341,33 @@ class SignalListenerTests(TestCase):
self.discord_admin.roles.add(self.admin_role)
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.add(self.moderator_role)
+ 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 +402,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 +432,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/urls.py b/pydis_site/apps/home/urls.py
index 211a7ad1..70a41177 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,7 +6,7 @@ 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 = [
@@ -15,6 +14,7 @@ urlpatterns = [
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 +28,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..f80089d5
--- /dev/null
+++ b/pydis_site/apps/home/views/account/delete.py
@@ -0,0 +1,20 @@
+from django.contrib.auth.mixins import LoginRequiredMixin
+from django.http import HttpRequest, HttpResponse
+from django.shortcuts import render
+from django.urls import reverse
+from django.views import View
+
+
+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")
+
+ def post(self, request: HttpRequest) -> HttpResponse:
+ """HTTP POST: Process the deletion, as requested by the user."""
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..aa272552
--- /dev/null
+++ b/pydis_site/apps/home/views/account/settings.py
@@ -0,0 +1,20 @@
+from django.contrib.auth.mixins import LoginRequiredMixin
+from django.http import HttpRequest, HttpResponse
+from django.shortcuts import 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."""
+
+ 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/settings.html")
+
+ def post(self, request: HttpRequest) -> HttpResponse:
+ """HTTP POST: Process account changes, as requested by the user."""
diff --git a/pydis_site/apps/staff/models/role_mapping.py b/pydis_site/apps/staff/models/role_mapping.py
index 10c09cf1..dff8081a 100644
--- a/pydis_site/apps/staff/models/role_mapping.py
+++ b/pydis_site/apps/staff/models/role_mapping.py
@@ -21,6 +21,11 @@ class RoleMapping(models.Model):
unique=True, # Unique in order to simplify group assignment logic
)
+ is_staff = models.BooleanField(
+ help_text="Whether this role mapping related to a Django staff group",
+ default=False
+ )
+
def __str__(self):
"""Returns the mapping, for display purposes."""
return f"@{self.role.name} -> {self.group.name}"
diff --git a/pydis_site/settings.py b/pydis_site/settings.py
index 56ac0a1d..0d893b2c 100644
--- a/pydis_site/settings.py
+++ b/pydis_site/settings.py
@@ -92,6 +92,7 @@ INSTALLED_APPS = [
'allauth.socialaccount',
'allauth.socialaccount.providers.discord',
+ 'allauth.socialaccount.providers.github',
'crispy_forms',
'django_crispy_bulma',
@@ -407,5 +408,13 @@ AUTHENTICATION_BACKENDS = (
'allauth.account.auth_backends.AuthenticationBackend',
)
+ACCOUNT_ADAPTER = "pydis_site.utils.account.AccountAdapter"
+ACCOUNT_EMAIL_REQUIRED = False # Undocumented allauth setting; don't require emails
ACCOUNT_EMAIL_VERIFICATION = "none" # No verification required; we don't use emails for anything
+
+# We use this validator because Allauth won't let us actually supply a list with no validators
+# in it, and we can't just give it a lambda - that'd be too easy, I suppose.
+ACCOUNT_USERNAME_VALIDATORS = "pydis_site.VALIDATORS"
+
LOGIN_REDIRECT_URL = "home"
+SOCIALACCOUNT_ADAPTER = "pydis_site.utils.account.SocialAccountAdapter"
diff --git a/pydis_site/static/css/base/base.css b/pydis_site/static/css/base/base.css
index 3ca6b2a7..7db9499d 100644
--- a/pydis_site/static/css/base/base.css
+++ b/pydis_site/static/css/base/base.css
@@ -84,7 +84,15 @@ div.card.has-equal-height {
/* Fix for logout form submit button in navbar */
-button.is-size-navbar-menu {
+button.is-size-navbar-menu, a.is-size-navbar-menu {
font-size: 14px;
+ padding-left: 1.5rem;
+ padding-right: 1.5rem;
}
+@media screen and (min-width: 1088px) {
+ button.is-size-navbar-menu, a.is-size-navbar-menu {
+ padding-left: 1rem;
+ padding-right: 1rem;
+ }
+}
diff --git a/pydis_site/templates/base/navbar.html b/pydis_site/templates/base/navbar.html
index 8cdac0de..bd0bab40 100644
--- a/pydis_site/templates/base/navbar.html
+++ b/pydis_site/templates/base/navbar.html
@@ -102,7 +102,15 @@
{% else %}
<form method="post" action="{% url 'logout' %}">
{% csrf_token %}
- <button type="submit" class="navbar-item button is-white is-inline is-fullwidth has-text-left is-size-navbar-menu has-text-grey-dark">Logout</button>
+
+ <div class="field navbar-item is-paddingless is-fullwidth is-grouped">
+ <button type="submit" class="button is-white is-inline is-fullwidth has-text-left is-size-navbar-menu has-text-grey-dark">Logout</button>
+ <a title="Settings" class="button is-white is-inline has-text-right is-size-navbar-menu has-text-grey-dark" href="{% url "account_settings" %}">
+ <span class="is-icon">
+ <i class="fas fa-cog"></i>
+ </span>
+ </a>
+ </div>
</form>
{% endif %}
diff --git a/pydis_site/templates/home/account/delete.html b/pydis_site/templates/home/account/delete.html
new file mode 100644
index 00000000..8d68a0e3
--- /dev/null
+++ b/pydis_site/templates/home/account/delete.html
@@ -0,0 +1,12 @@
+{% extends 'base/base.html' %}
+{% load static %}
+
+{% block title %}Delete Account{% endblock %}
+
+{% block content %}
+ {% include "base/navbar.html" %}
+
+ <section class="section">
+
+ </section>
+{% endblock %}
diff --git a/pydis_site/templates/home/account/settings.html b/pydis_site/templates/home/account/settings.html
new file mode 100644
index 00000000..ba1d38a2
--- /dev/null
+++ b/pydis_site/templates/home/account/settings.html
@@ -0,0 +1,12 @@
+{% extends 'base/base.html' %}
+{% load static %}
+
+{% block title %}My Account{% endblock %}
+
+{% block content %}
+ {% include "base/navbar.html" %}
+
+ <section class="section">
+
+ </section>
+{% endblock %}
diff --git a/pydis_site/tests/test_utils_account.py b/pydis_site/tests/test_utils_account.py
new file mode 100644
index 00000000..91c2808c
--- /dev/null
+++ b/pydis_site/tests/test_utils_account.py
@@ -0,0 +1,50 @@
+from unittest.mock import patch
+
+from allauth.exceptions import ImmediateHttpResponse
+from allauth.socialaccount.models import SocialAccount, SocialLogin
+from django.contrib.auth.models import User
+from django.contrib.messages.storage.base import BaseStorage
+from django.http import HttpRequest
+from django.test import TestCase
+
+from pydis_site.utils.account import AccountAdapter, SocialAccountAdapter
+
+
+class AccountUtilsTests(TestCase):
+ def setUp(self):
+ self.django_user = User.objects.create(username="user")
+
+ self.discord_account = SocialAccount.objects.create(
+ user=self.django_user, provider="discord", uid=0
+ )
+
+ self.github_account = SocialAccount.objects.create(
+ user=self.django_user, provider="github", uid=0
+ )
+
+ def test_account_adapter(self):
+ """Test that our Allauth account adapter functions correctly."""
+ adapter = AccountAdapter()
+
+ self.assertFalse(adapter.is_open_for_signup(HttpRequest()))
+
+ def test_social_account_adapter(self):
+ """Test that our Allauth social account adapter functions correctly."""
+ adapter = SocialAccountAdapter()
+
+ discord_login = SocialLogin(account=self.discord_account)
+ github_login = SocialLogin(account=self.github_account)
+
+ messages_request = HttpRequest()
+ messages_request._messages = BaseStorage(messages_request)
+
+ with patch("pydis_site.utils.account.reverse") as mock_reverse:
+ with patch("pydis_site.utils.account.redirect") as mock_redirect:
+ with self.assertRaises(ImmediateHttpResponse):
+ adapter.is_open_for_signup(messages_request, github_login)
+
+ self.assertEqual(len(messages_request._messages._queued_messages), 1)
+ self.assertEqual(mock_redirect.call_count, 1)
+ self.assertEqual(mock_reverse.call_count, 1)
+
+ self.assertTrue(adapter.is_open_for_signup(HttpRequest(), discord_login))
diff --git a/pydis_site/utils/account.py b/pydis_site/utils/account.py
new file mode 100644
index 00000000..9faad986
--- /dev/null
+++ b/pydis_site/utils/account.py
@@ -0,0 +1,64 @@
+from typing import Any, Dict
+
+from allauth.account.adapter import DefaultAccountAdapter
+from allauth.exceptions import ImmediateHttpResponse
+from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
+from allauth.socialaccount.models import SocialLogin
+from django.contrib.auth.models import User
+from django.contrib.messages import ERROR, add_message
+from django.http import HttpRequest
+from django.shortcuts import redirect
+from django.urls import reverse
+
+
+class AccountAdapter(DefaultAccountAdapter):
+ """An Allauth account adapter that prevents signups via form submission."""
+
+ def is_open_for_signup(self, request: HttpRequest) -> bool:
+ """
+ Checks whether or not the site is open for signups.
+
+ We override this to always return False so that users may never sign up using
+ Allauth's signup form endpoints, to be on the safe side - since we only want users
+ to sign up using their Discord account.
+ """
+ return False
+
+
+class SocialAccountAdapter(DefaultSocialAccountAdapter):
+ """An Allauth SocialAccount adapter that prevents signups via non-Discord connections."""
+
+ def is_open_for_signup(self, request: HttpRequest, social_login: SocialLogin) -> bool:
+ """
+ Checks whether or not the site is open for signups.
+
+ We override this method in order to prevent users from creating a new account using
+ a non-Discord connection, as we require this connection for our users.
+ """
+ if social_login.account.provider != "discord":
+ add_message(
+ request, ERROR,
+ "You must login with Discord before connecting another account. Your account "
+ "details have not been saved."
+ )
+
+ raise ImmediateHttpResponse(redirect(reverse("home")))
+
+ return True
+
+ def populate_user(self, request: HttpRequest,
+ social_login: SocialLogin,
+ data: Dict[str, Any]) -> User:
+ """
+ Method used to populate a Django User with data.
+
+ We override this so that the Django user is created with the username#discriminator,
+ instead of just the username, as Django users must have unique usernames. For display
+ purposes, we also set the `name` key, which is used for `first_name` in the database.
+ """
+ if social_login.account.provider == "discord":
+ discriminator = social_login.account.extra_data["discriminator"]
+ data["username"] = f"{data['username']}#{discriminator}"
+ data["name"] = data["username"]
+
+ return super().populate_user(request, social_login, data)