From 16ee54585b58fd99defbd85e0fceef77b969392f Mon Sep 17 00:00:00 2001 From: Raymond Penners Date: Fri, 17 Dec 2021 10:48:05 +0100 Subject: [PATCH] feat: `sites` is no longer required --- ChangeLog.rst | 7 ++++++ allauth/account/adapter.py | 2 +- allauth/account/tests.py | 25 +++++++++++++------ allauth/app_settings.py | 1 + allauth/socialaccount/admin.py | 3 ++- .../socialaccount/migrations/0001_initial.py | 25 ++++++++++++++----- allauth/socialaccount/models.py | 19 ++++++++------ .../socialaccount/providers/draugiem/tests.py | 7 ++++-- allauth/socialaccount/tests.py | 15 ++++++++--- allauth/tests.py | 12 ++++++--- allauth/utils.py | 9 ++++++- test_settings.py | 1 + 12 files changed, 92 insertions(+), 34 deletions(-) diff --git a/ChangeLog.rst b/ChangeLog.rst index a92ce49316..43e4d81d08 100644 --- a/ChangeLog.rst +++ b/ChangeLog.rst @@ -21,6 +21,13 @@ Note worthy changes ``allauth.account.views.PasswordResetFromKeyView`` which allows specifying a token parameter displayed as a component of password reset URLs. +- It is now possible to use allauth without having `sites` installed. Whether or + not sites is used affects the data models. For example, the social app model + uses a many-to-many pointing to the sites model if the `sites` app is + installed. Therefore, enabling or disabling `sites` is not something you can + do on the fly. + + Backwards incompatible changes ------------------------------ diff --git a/allauth/account/adapter.py b/allauth/account/adapter.py index 6d9ac52b54..6c7609574c 100644 --- a/allauth/account/adapter.py +++ b/allauth/account/adapter.py @@ -493,7 +493,7 @@ def respond_email_verification_sent(self, request, user): def _get_login_attempts_cache_key(self, request, **credentials): site = get_current_site(request) login = credentials.get("email", credentials.get("username", "")).lower() - return "{site_id}:{login}".format(site_id=site.pk, login=login) + return "{site}:{login}".format(site=site.domain, login=login) def _delete_login_attempts_cached_email(self, request, **credentials): if app_settings.LOGIN_ATTEMPTS_LIMIT: diff --git a/allauth/account/tests.py b/allauth/account/tests.py index 0542c081a2..c8137a9f6c 100644 --- a/allauth/account/tests.py +++ b/allauth/account/tests.py @@ -11,7 +11,6 @@ from django.contrib.messages.api import get_messages from django.contrib.messages.middleware import MessageMiddleware from django.contrib.sessions.middleware import SessionMiddleware -from django.contrib.sites.models import Site from django.core import mail, validators from django.core.exceptions import ValidationError from django.db import models @@ -22,6 +21,7 @@ from django.urls import reverse from django.utils.timezone import now +import allauth.app_settings from allauth.account.forms import BaseSignupForm, ResetPasswordForm, SignupForm from allauth.account.models import ( EmailAddress, @@ -66,7 +66,10 @@ def setUp(self): from ..socialaccount.models import SocialApp sa = SocialApp.objects.create(name="testfb", provider="facebook") - sa.sites.add(Site.objects.get_current()) + if allauth.app_settings.SITES_ENABLED: + from django.contrib.sites.models import Site + + sa.sites.add(Site.objects.get_current()) @override_settings( ACCOUNT_AUTHENTICATION_METHOD=app_settings.AuthenticationMethod.USERNAME_EMAIL @@ -544,13 +547,17 @@ def test_email_verification_mandatory(self): ) def test_email_escaping(self): - site = Site.objects.get_current() - site.name = '' - site.save() + site_name = "testserver" + if allauth.app_settings.SITES_ENABLED: + from django.contrib.sites.models import Site + + site = Site.objects.get_current() + site.name = site_name = '' + site.save() u = get_user_model().objects.create(username="test", email="user@example.com") request = RequestFactory().get("/") EmailAddress.objects.add_email(request, u, u.email, confirm=True) - self.assertTrue(mail.outbox[0].subject[1:].startswith(site.name)) + self.assertTrue(mail.outbox[0].subject[1:].startswith(site_name)) @override_settings( ACCOUNT_EMAIL_VERIFICATION=app_settings.EmailVerificationMethod.OPTIONAL @@ -925,7 +932,8 @@ def test_email_confirmation_hmac(self): user=user, email="a@b.com", verified=False, primary=True ) confirmation = EmailConfirmationHMAC(email) - confirmation.send() + request = RequestFactory().get("/") + confirmation.send(request=request) self.assertEqual(len(mail.outbox), 1) self.client.post(reverse("account_confirm_email", args=[confirmation.key])) email = EmailAddress.objects.get(pk=email.pk) @@ -941,7 +949,8 @@ def test_email_confirmation_hmac_timeout(self): user=user, email="a@b.com", verified=False, primary=True ) confirmation = EmailConfirmationHMAC(email) - confirmation.send() + request = RequestFactory().get("/") + confirmation.send(request=request) self.assertEqual(len(mail.outbox), 1) self.client.post(reverse("account_confirm_email", args=[confirmation.key])) email = EmailAddress.objects.get(pk=email.pk) diff --git a/allauth/app_settings.py b/allauth/app_settings.py index c25941c627..1a86718045 100644 --- a/allauth/app_settings.py +++ b/allauth/app_settings.py @@ -1,6 +1,7 @@ from django.conf import settings +SITES_ENABLED = "django.contrib.sites" in settings.INSTALLED_APPS SOCIALACCOUNT_ENABLED = "allauth.socialaccount" in settings.INSTALLED_APPS LOGIN_REDIRECT_URL = getattr(settings, "LOGIN_REDIRECT_URL", "/") diff --git a/allauth/socialaccount/admin.py b/allauth/socialaccount/admin.py index f8f40e5d96..30fd56e3ee 100644 --- a/allauth/socialaccount/admin.py +++ b/allauth/socialaccount/admin.py @@ -1,6 +1,7 @@ from django import forms from django.contrib import admin +from allauth import app_settings from allauth.account.adapter import get_adapter from .models import SocialAccount, SocialApp, SocialToken @@ -23,7 +24,7 @@ class SocialAppAdmin(admin.ModelAdmin): "name", "provider", ) - filter_horizontal = ("sites",) + filter_horizontal = ("sites",) if app_settings.SITES_ENABLED else () class SocialAccountAdmin(admin.ModelAdmin): diff --git a/allauth/socialaccount/migrations/0001_initial.py b/allauth/socialaccount/migrations/0001_initial.py index 133acfde73..43c652c730 100644 --- a/allauth/socialaccount/migrations/0001_initial.py +++ b/allauth/socialaccount/migrations/0001_initial.py @@ -5,15 +5,22 @@ from django.db import migrations, models import allauth.socialaccount.fields +from allauth import app_settings from allauth.socialaccount.providers import registry class Migration(migrations.Migration): - dependencies = [ - ("sites", "0001_initial"), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] + dependencies = ( + [ + ("sites", "0001_initial"), + ] + if app_settings.SITES_ENABLED + else [] + + [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + ) operations = [ migrations.CreateModel( @@ -118,8 +125,14 @@ class Migration(migrations.Migration): blank=True, ), ), - ("sites", models.ManyToManyField(to="sites.Site", blank=True)), - ], + ] + + ( + [ + ("sites", models.ManyToManyField(to="sites.Site", blank=True)), + ] + if app_settings.SITES_ENABLED + else [] + ), options={ "verbose_name": "social application", "verbose_name_plural": "social applications", diff --git a/allauth/socialaccount/models.py b/allauth/socialaccount/models.py index f2c7da114a..05e2475061 100644 --- a/allauth/socialaccount/models.py +++ b/allauth/socialaccount/models.py @@ -1,7 +1,6 @@ from __future__ import absolute_import from django.contrib.auth import authenticate -from django.contrib.sites.models import Site from django.contrib.sites.shortcuts import get_current_site from django.core.exceptions import PermissionDenied from django.db import models @@ -28,8 +27,11 @@ def get_current(self, provider, request=None): request._socialapp_cache = cache app = cache.get(provider) if not app: - site = get_current_site(request) - app = self.get(sites__id=site.id, provider=provider) + if allauth.app_settings.SITES_ENABLED: + site = get_current_site(request) + app = self.get(sites__id=site.id, provider=provider) + else: + app = self.get(provider=provider) cache[provider] = app return app @@ -57,11 +59,12 @@ class SocialApp(models.Model): key = models.CharField( verbose_name=_("key"), max_length=191, blank=True, help_text=_("Key") ) - # Most apps can be used across multiple domains, therefore we use - # a ManyToManyField. Note that Facebook requires an app per domain - # (unless the domains share a common base name). - # blank=True allows for disabling apps without removing them - sites = models.ManyToManyField(Site, blank=True) + if allauth.app_settings.SITES_ENABLED: + # Most apps can be used across multiple domains, therefore we use + # a ManyToManyField. Note that Facebook requires an app per domain + # (unless the domains share a common base name). + # blank=True allows for disabling apps without removing them + sites = models.ManyToManyField("sites.Site", blank=True) # We want to move away from storing secrets in the database. So, we're # putting a halt towards adding more fields for additional secrets, such as diff --git a/allauth/socialaccount/providers/draugiem/tests.py b/allauth/socialaccount/providers/draugiem/tests.py index c5a63df104..c84ecb0b6b 100644 --- a/allauth/socialaccount/providers/draugiem/tests.py +++ b/allauth/socialaccount/providers/draugiem/tests.py @@ -1,10 +1,10 @@ from hashlib import md5 from django.contrib.auth.models import User -from django.contrib.sites.models import Site from django.urls import reverse from django.utils.http import urlencode +from allauth import app_settings from allauth.socialaccount import providers from allauth.socialaccount.models import SocialApp, SocialToken from allauth.tests import Mock, TestCase, patch @@ -30,7 +30,10 @@ def setUp(self): key=self.provider.id, secret="dummy", ) - app.sites.add(Site.objects.get_current()) + if app_settings.SITES_ENABLED: + from django.contrib.sites.models import Site + + app.sites.add(Site.objects.get_current()) self.app = app def get_draugiem_login_response(self): diff --git a/allauth/socialaccount/tests.py b/allauth/socialaccount/tests.py index 3d06b18889..2ca37f446d 100644 --- a/allauth/socialaccount/tests.py +++ b/allauth/socialaccount/tests.py @@ -7,12 +7,13 @@ from django.contrib.auth.models import AnonymousUser from django.contrib.messages.middleware import MessageMiddleware from django.contrib.sessions.middleware import SessionMiddleware -from django.contrib.sites.models import Site from django.test.client import RequestFactory from django.test.utils import override_settings from django.urls import reverse from django.utils.http import urlencode +import allauth.app_settings + from ..account import app_settings as account_settings from ..account.models import EmailAddress from ..account.utils import user_email, user_username @@ -34,7 +35,10 @@ def setup_app(provider): key=provider.id, secret="dummy", ) - app.sites.add(Site.objects.get_current()) + if allauth.app_settings.SITES_ENABLED: + from django.contrib.sites.models import Site + + app.sites.add(Site.objects.get_current()) return app @@ -269,7 +273,6 @@ class Class(OAuth2TestsMixin, TestCase): class SocialAccountTests(TestCase): def setUp(self): super(SocialAccountTests, self).setUp() - site = Site.objects.get_current() for provider in providers.registry.get_list(): app = SocialApp.objects.create( provider=provider.id, @@ -278,7 +281,11 @@ def setUp(self): key="123", secret="dummy", ) - app.sites.add(site) + if allauth.app_settings.SITES_ENABLED: + from django.contrib.sites.models import Site + + site = Site.objects.get_current() + app.sites.add(site) @override_settings( SOCIALACCOUNT_AUTO_SIGNUP=True, diff --git a/allauth/tests.py b/allauth/tests.py index 39388697c8..24daffb04a 100644 --- a/allauth/tests.py +++ b/allauth/tests.py @@ -12,6 +12,8 @@ from django.utils.http import base36_to_int, int_to_base36 from django.views import csrf +from allauth import app_settings + from . import utils @@ -170,15 +172,19 @@ class SomeBinaryModel(models.Model): self.assertEqual(deserialized.bb_empty, b"") def test_build_absolute_uri(self): + request = None + if not app_settings.SITES_ENABLED: + request = self.factory.get("/") + request.META["SERVER_NAME"] = "example.com" self.assertEqual( - utils.build_absolute_uri(None, "/foo"), "http://example.com/foo" + utils.build_absolute_uri(request, "/foo"), "http://example.com/foo" ) self.assertEqual( - utils.build_absolute_uri(None, "/foo", protocol="ftp"), + utils.build_absolute_uri(request, "/foo", protocol="ftp"), "ftp://example.com/foo", ) self.assertEqual( - utils.build_absolute_uri(None, "http://foo.com/bar"), + utils.build_absolute_uri(request, "http://foo.com/bar"), "http://foo.com/bar", ) diff --git a/allauth/utils.py b/allauth/utils.py index 4cf43de0a0..5f947eb336 100644 --- a/allauth/utils.py +++ b/allauth/utils.py @@ -10,7 +10,6 @@ import django from django.contrib.auth import get_user_model -from django.contrib.sites.models import Site from django.core.exceptions import FieldDoesNotExist, ImproperlyConfigured from django.core.serializers.json import DjangoJSONEncoder from django.core.validators import ValidationError, validate_email @@ -25,6 +24,8 @@ from django.utils import dateparse from django.utils.encoding import force_bytes, force_str +from allauth import app_settings + # Magic number 7: if you run into collisions with this number, then you are # of big enough scale to start investing in a decent user model... @@ -271,6 +272,12 @@ def build_absolute_uri(request, location, protocol=None): from .account import app_settings as account_settings if request is None: + if not app_settings.SITES_ENABLED: + raise ImproperlyConfigured( + "Passing `request=None` requires `sites` to be enabled." + ) + from django.contrib.sites.models import Site + site = Site.objects.get_current() bits = urlsplit(location) if not (bits.scheme and bits.netloc): diff --git a/test_settings.py b/test_settings.py index bee337ad8a..8f3bd69aca 100644 --- a/test_settings.py +++ b/test_settings.py @@ -1,5 +1,6 @@ SECRET_KEY = "psst" SITE_ID = 1 +ALLOWED_HOSTS = ("*",) DATABASES = { "default": {