diff --git a/ChangeLog.rst b/ChangeLog.rst
index e084f3ab8e..36d2e8826d 100644
--- a/ChangeLog.rst
+++ b/ChangeLog.rst
@@ -8,6 +8,10 @@ Note worthy changes
- Added a new provider, Atlassian
+- Next URL handling been streamlined to be consistently applied. Previously, the
+ password reset, change and email confirmation views only supported the
+ ``success_url`` class-level property.
+
Backwards incompatible changes
------------------------------
diff --git a/allauth/account/mixins.py b/allauth/account/mixins.py
new file mode 100644
index 0000000000..34c17a8924
--- /dev/null
+++ b/allauth/account/mixins.py
@@ -0,0 +1,178 @@
+from django.contrib.auth import REDIRECT_FIELD_NAME
+from django.core.exceptions import ImproperlyConfigured
+from django.http import HttpResponsePermanentRedirect, HttpResponseRedirect
+from django.utils.html import format_html
+
+from allauth.account import app_settings
+from allauth.account.adapter import get_adapter
+from allauth.account.internal import flows
+from allauth.account.utils import (
+ get_login_redirect_url,
+ get_next_redirect_url,
+ passthrough_next_redirect_url,
+)
+from allauth.core.exceptions import ImmediateHttpResponse
+from allauth.utils import get_request_param
+
+
+def _ajax_response(request, response, form=None, data=None):
+ adapter = get_adapter()
+ if adapter.is_ajax(request):
+ if isinstance(response, HttpResponseRedirect) or isinstance(
+ response, HttpResponsePermanentRedirect
+ ):
+ redirect_to = response["Location"]
+ else:
+ redirect_to = None
+ response = adapter.ajax_response(
+ request, response, form=form, data=data, redirect_to=redirect_to
+ )
+ return response
+
+
+class RedirectAuthenticatedUserMixin:
+ def dispatch(self, request, *args, **kwargs):
+ if request.user.is_authenticated and app_settings.AUTHENTICATED_LOGIN_REDIRECTS:
+ redirect_to = self.get_authenticated_redirect_url()
+ response = HttpResponseRedirect(redirect_to)
+ return _ajax_response(request, response)
+ else:
+ response = super().dispatch(request, *args, **kwargs)
+ return response
+
+ def get_authenticated_redirect_url(self):
+ redirect_field_name = self.redirect_field_name
+ return get_login_redirect_url(
+ self.request,
+ url=self.get_success_url(),
+ redirect_field_name=redirect_field_name,
+ )
+
+
+class LogoutFunctionalityMixin:
+ def logout(self):
+ flows.logout.logout(self.request)
+
+
+class AjaxCapableProcessFormViewMixin:
+ def get(self, request, *args, **kwargs):
+ response = super().get(request, *args, **kwargs)
+ form = self.get_form()
+ return _ajax_response(
+ self.request, response, form=form, data=self._get_ajax_data_if()
+ )
+
+ def post(self, request, *args, **kwargs):
+ form_class = self.get_form_class()
+ form = self.get_form(form_class)
+ if form.is_valid():
+ response = self.form_valid(form)
+ else:
+ response = self.form_invalid(form)
+ return _ajax_response(
+ self.request, response, form=form, data=self._get_ajax_data_if()
+ )
+
+ def get_form(self, form_class=None):
+ form = getattr(self, "_cached_form", None)
+ if form is None:
+ form = super().get_form(form_class)
+ self._cached_form = form
+ return form
+
+ def _get_ajax_data_if(self):
+ return (
+ self.get_ajax_data()
+ if get_adapter(self.request).is_ajax(self.request)
+ else None
+ )
+
+ def get_ajax_data(self):
+ return None
+
+
+class CloseableSignupMixin:
+ template_name_signup_closed = (
+ "account/signup_closed." + app_settings.TEMPLATE_EXTENSION
+ )
+
+ def dispatch(self, request, *args, **kwargs):
+ try:
+ if not self.is_open():
+ return self.closed()
+ except ImmediateHttpResponse as e:
+ return e.response
+ return super().dispatch(request, *args, **kwargs)
+
+ def is_open(self):
+ return get_adapter(self.request).is_open_for_signup(self.request)
+
+ def closed(self):
+ response_kwargs = {
+ "request": self.request,
+ "template": self.template_name_signup_closed,
+ }
+ return self.response_class(**response_kwargs)
+
+
+class NextRedirectMixin:
+ redirect_field_name = REDIRECT_FIELD_NAME
+
+ def get_context_data(self, **kwargs):
+ ret = super().get_context_data(**kwargs)
+ redirect_field_value = get_request_param(self.request, self.redirect_field_name)
+ ret.update(
+ {
+ "redirect_field_name": self.redirect_field_name,
+ "redirect_field_value": redirect_field_value,
+ "redirect_field": format_html(
+ '',
+ self.redirect_field_name,
+ redirect_field_value,
+ )
+ if redirect_field_value
+ else "",
+ }
+ )
+ return ret
+
+ def get_success_url(self):
+ """
+ We're in a mixin, so we cannot rely on the fact that our super() has a get_success_url.
+ Also, we want to check for -- in this order:
+ 1) The `?next=/foo`
+ 2) The `get_succes_url()` if available.
+ 3) The `.success_url` if available.
+ 4) A fallback default success URL: `get_default_success_url()`.
+ """
+ url = self.get_next_url()
+ if url:
+ return url
+
+ if not url:
+ if hasattr(super(), "get_success_url"):
+ try:
+ url = super().get_success_url()
+ except ImproperlyConfigured:
+ # Django's default get_success_url() checks self.succes_url,
+ # and throws this if that is not set. Yet, in our case, we
+ # want to fallback to the default.
+ pass
+ elif hasattr(self, "success_url"):
+ url = self.success_url
+ if url:
+ url = str(url) # reverse_lazy
+ if not url:
+ url = self.get_default_success_url()
+ return url
+
+ def get_default_success_url(self):
+ return None
+
+ def get_next_url(self):
+ return get_next_redirect_url(self.request, self.redirect_field_name)
+
+ def passthrough_next_url(self, url):
+ return passthrough_next_redirect_url(
+ self.request, url, self.redirect_field_name
+ )
diff --git a/allauth/account/tests/test_change_password.py b/allauth/account/tests/test_change_password.py
index acc95f5325..81bbc75ca7 100644
--- a/allauth/account/tests/test_change_password.py
+++ b/allauth/account/tests/test_change_password.py
@@ -19,16 +19,13 @@ def test_set_usable_password_redirects_to_change(auth_client, user):
@pytest.mark.parametrize(
- "logout,redirect_chain",
+ "logout,next_url,redirect_chain",
[
- (
- False,
- [
- (reverse("account_change_password"), 302),
- ],
- ),
+ (False, "", [(reverse("account_change_password"), 302)]),
+ (False, "/foo", [("/foo", 302)]),
(
True,
+ "",
[
(reverse("account_change_password"), 302),
(
@@ -37,33 +34,36 @@ def test_set_usable_password_redirects_to_change(auth_client, user):
),
],
),
+ (True, "/foo", [("/foo", 302)]),
],
)
-def test_set_password(client, user, password_factory, logout, settings, redirect_chain):
+def test_set_password(
+ client, user, next_url, password_factory, logout, settings, redirect_chain
+):
settings.ACCOUNT_LOGOUT_ON_PASSWORD_CHANGE = logout
user.set_unusable_password()
user.save()
client.force_login(user)
password = password_factory()
+ data = {"password1": password, "password2": password}
+ if next_url:
+ data["next"] = next_url
resp = client.post(
reverse("account_set_password"),
- {"password1": password, "password2": password},
+ data,
follow=True,
)
assert resp.redirect_chain == redirect_chain
@pytest.mark.parametrize(
- "logout,redirect_chain",
+ "logout,next_url,redirect_chain",
[
- (
- False,
- [
- (reverse("account_change_password"), 302),
- ],
- ),
+ (False, "", [(reverse("account_change_password"), 302)]),
+ (False, "/foo", [("/foo", 302)]),
(
True,
+ "",
[
(reverse("account_change_password"), 302),
(
@@ -72,12 +72,14 @@ def test_set_password(client, user, password_factory, logout, settings, redirect
),
],
),
+ (True, "/foo", [("/foo", 302)]),
],
)
def test_change_password(
auth_client,
user,
user_password,
+ next_url,
password_factory,
logout,
settings,
@@ -87,9 +89,12 @@ def test_change_password(
settings.ACCOUNT_LOGOUT_ON_PASSWORD_CHANGE = logout
settings.ACCOUNT_EMAIL_NOTIFICATIONS = True
password = password_factory()
+ data = {"oldpassword": user_password, "password1": password, "password2": password}
+ if next_url:
+ data["next"] = next_url
resp = auth_client.post(
reverse("account_change_password"),
- {"oldpassword": user_password, "password1": password, "password2": password},
+ data,
follow=True,
)
assert resp.redirect_chain == redirect_chain
diff --git a/allauth/account/tests/test_confirm_email.py b/allauth/account/tests/test_confirm_email.py
index 5661254128..25fb1c3dd4 100644
--- a/allauth/account/tests/test_confirm_email.py
+++ b/allauth/account/tests/test_confirm_email.py
@@ -8,6 +8,7 @@
from django.urls import reverse
from django.utils.timezone import now
+import pytest
from pytest_django.asserts import (
assertRedirects,
assertTemplateNotUsed,
@@ -26,7 +27,14 @@
from .test_models import UUIDUser
-def test_login_on_confirm(user_factory, client):
+@pytest.mark.parametrize(
+ "query,expected_location",
+ [
+ ("", settings.LOGIN_REDIRECT_URL),
+ ("?next=/foo", "/foo"),
+ ],
+)
+def test_login_on_confirm(user_factory, client, query, expected_location):
settings.ACCOUNT_EMAIL_CONFIRMATION_HMAC = True
settings.ACCOUNT_LOGIN_ON_EMAIL_CONFIRMATION = True
user = user_factory(email_verified=False)
@@ -41,7 +49,8 @@ def test_login_on_confirm(user_factory, client):
session["account_user"] = user_pk_to_url_str(user)
session.save()
- resp = client.post(reverse("account_confirm_email", args=[key]))
+ resp = client.post(reverse("account_confirm_email", args=[key]) + query)
+ assert resp["location"] == expected_location
email = EmailAddress.objects.get(pk=email.pk)
assert email.verified
diff --git a/allauth/account/tests/test_reset_password.py b/allauth/account/tests/test_reset_password.py
index e9d627245e..e94f7faae6 100644
--- a/allauth/account/tests/test_reset_password.py
+++ b/allauth/account/tests/test_reset_password.py
@@ -5,15 +5,30 @@
from django.core import mail
from django.test.utils import override_settings
from django.urls import reverse
+from django.utils.http import urlencode
import pytest
+from pytest_django.asserts import assertRedirects, assertTemplateUsed
from allauth.account import app_settings
-from allauth.account.forms import ResetPasswordForm
+from allauth.account.forms import ResetPasswordForm, default_token_generator
from allauth.account.models import EmailAddress
+from allauth.account.utils import user_pk_to_url_str
from allauth.tests import TestCase
+@pytest.fixture
+def password_reset_url():
+ def f(user):
+ temp_key = default_token_generator.make_token(user)
+ uid = user_pk_to_url_str(user)
+ return reverse(
+ "account_reset_password_from_key", kwargs={"uidb36": uid, "key": temp_key}
+ )
+
+ return f
+
+
@pytest.mark.django_db
def test_reset_password_unknown_account(client, settings):
settings.ACCOUNT_PREVENT_ENUMERATION = True
@@ -36,6 +51,18 @@ def test_reset_password_unknown_account_disabled(client, settings):
assert len(mail.outbox) == 0
+@pytest.mark.parametrize(
+ "query,expected_location",
+ [("", reverse("account_reset_password_done")), ("?next=/foo", "/foo")],
+)
+def test_reset_password_next_url(client, user, query, expected_location):
+ resp = client.post(
+ reverse("account_reset_password") + query,
+ data={"email": user.email},
+ )
+ assert resp["location"] == expected_location
+
+
@override_settings(
ACCOUNT_PREVENT_ENUMERATION=False,
ACCOUNT_DEFAULT_HTTP_PROTOCOL="https",
@@ -133,72 +160,6 @@ def test_password_reset_flow_with_empty_session(self):
self.assertTrue(resp.context_data["token_fail"])
- def test_password_reset_flow(self):
- """
- Tests the password reset flow: requesting a new password,
- receiving the reset link via email and finally resetting the
- password to a new value.
- """
- # Request new password
- user = self._request_new_password()
- body = mail.outbox[0].body
- self.assertGreater(body.find("https://"), 0)
-
- # Extract URL for `password_reset_from_key` view and access it
- url = body[body.find("/password/reset/") :].split()[0]
- resp = self.client.get(url)
- # Follow the redirect the actual password reset page with the key
- # hidden.
- url = resp.url
- resp = self.client.get(url)
- self.assertTemplateUsed(
- resp,
- "account/password_reset_from_key.%s" % app_settings.TEMPLATE_EXTENSION,
- )
- self.assertFalse("token_fail" in resp.context_data)
-
- # Reset the password
- resp = self.client.post(
- url, {"password1": "newpass123", "password2": "newpass123"}
- )
- self.assertRedirects(resp, reverse("account_reset_password_from_key_done"))
- assert "Your password has been reset" in mail.outbox[-1].body
-
- # Check the new password is in effect
- user = get_user_model().objects.get(pk=user.pk)
- self.assertTrue(user.check_password("newpass123"))
-
- # Trying to reset the password against the same URL (or any other
- # invalid/obsolete URL) returns a bad token response
- resp = self.client.post(
- url, {"password1": "newpass123", "password2": "newpass123"}
- )
- self.assertTemplateUsed(
- resp,
- "account/password_reset_from_key.%s" % app_settings.TEMPLATE_EXTENSION,
- )
- self.assertTrue(resp.context_data["token_fail"])
-
- # Same should happen when accessing the page directly
- response = self.client.get(url)
- self.assertTemplateUsed(
- response,
- "account/password_reset_from_key.%s" % app_settings.TEMPLATE_EXTENSION,
- )
- self.assertTrue(response.context_data["token_fail"])
-
- # When in XHR views, it should respond with a 400 bad request
- # code, and the response body should contain the JSON-encoded
- # error from the adapter
- response = self.client.post(
- url,
- {"password1": "newpass123", "password2": "newpass123"},
- HTTP_X_REQUESTED_WITH="XMLHttpRequest",
- )
- self.assertEqual(response.status_code, 400)
- data = json.loads(response.content.decode("utf8"))
- assert "invalid" in data["form"]["errors"][0]
-
@override_settings(
ACCOUNT_AUTHENTICATION_METHOD=app_settings.AuthenticationMethod.EMAIL
)
@@ -296,3 +257,103 @@ def _create_user_and_login(self, usable_password=True):
user = self._create_user(password=password)
self.client.force_login(user)
return user
+
+
+def test_password_reset_flow(client, user, mailoutbox, settings):
+ """
+ Tests the password reset flow: requesting a new password,
+ receiving the reset link via email and finally resetting the
+ password to a new value.
+ """
+ settings.ACCOUNT_EMAIL_NOTIFICATIONS = True
+
+ # Request new password
+ client.post(
+ reverse("account_reset_password"),
+ data={"email": user.email},
+ )
+ assert len(mail.outbox) == 1
+ assert mailoutbox[0].to == [user.email]
+ body = mailoutbox[0].body
+ assert body.find("http://") > 0
+
+ # Extract URL for `password_reset_from_key` view and access it
+ url = body[body.find("/password/reset/") :].split()[0]
+ resp = client.get(url)
+ # Follow the redirect the actual password reset page with the key
+ # hidden.
+ url = resp.url
+ resp = client.get(url)
+ assertTemplateUsed(
+ resp,
+ "account/password_reset_from_key.%s" % app_settings.TEMPLATE_EXTENSION,
+ )
+ assert "token_fail" not in resp.context_data
+
+ # Reset the password
+ resp = client.post(url, {"password1": "newpass123", "password2": "newpass123"})
+ assertRedirects(resp, reverse("account_reset_password_from_key_done"))
+ assert "Your password has been reset" in mailoutbox[-1].body
+
+ # Check the new password is in effect
+ user = get_user_model().objects.get(pk=user.pk)
+ assert user.check_password("newpass123")
+
+ # Trying to reset the password against the same URL (or any other
+ # invalid/obsolete URL) returns a bad token response
+ resp = client.post(url, {"password1": "newpass123", "password2": "newpass123"})
+ assertTemplateUsed(
+ resp,
+ "account/password_reset_from_key.%s" % app_settings.TEMPLATE_EXTENSION,
+ )
+ assert resp.context_data["token_fail"]
+
+ # Same should happen when accessing the page directly
+ response = client.get(url)
+ assertTemplateUsed(
+ response,
+ "account/password_reset_from_key.%s" % app_settings.TEMPLATE_EXTENSION,
+ )
+ assert response.context_data["token_fail"]
+
+ # When in XHR views, it should respond with a 400 bad request
+ # code, and the response body should contain the JSON-encoded
+ # error from the adapter
+ response = client.post(
+ url,
+ {"password1": "newpass123", "password2": "newpass123"},
+ HTTP_X_REQUESTED_WITH="XMLHttpRequest",
+ )
+ assert response.status_code == 400
+ data = json.loads(response.content.decode("utf8"))
+ assert "invalid" in data["form"]["errors"][0]
+
+
+@pytest.mark.parametrize(
+ "next_url,expected_location",
+ [(None, reverse("account_reset_password_from_key_done")), ("/foo", "/foo")],
+)
+def test_reset_password_from_key_next_url(
+ user, client, password_factory, next_url, expected_location, password_reset_url
+):
+ url = password_reset_url(user)
+ query = ""
+ if next_url:
+ query = "?" + urlencode({"next": next_url})
+ resp = client.get(url + query)
+ assert resp.status_code == 302
+ assert (
+ resp["location"]
+ == reverse(
+ "account_reset_password_from_key",
+ kwargs={"uidb36": user_pk_to_url_str(user), "key": "set-password"},
+ )
+ + query
+ )
+ password = password_factory()
+ data = {"password1": password, "password2": password}
+ if next_url:
+ data["next"] = next_url
+ resp = client.post(resp["location"], data)
+ assert resp.status_code == 302
+ assert resp["location"] == expected_location
diff --git a/allauth/account/utils.py b/allauth/account/utils.py
index b9c179114f..4c29381fc3 100644
--- a/allauth/account/utils.py
+++ b/allauth/account/utils.py
@@ -4,7 +4,7 @@
from django.conf import settings
from django.contrib import messages
-from django.contrib.auth import get_user_model
+from django.contrib.auth import REDIRECT_FIELD_NAME, get_user_model
from django.core.exceptions import FieldDoesNotExist
from django.db import models
from django.db.models import Q
@@ -34,7 +34,7 @@ def _unicode_ci_compare(s1, s2):
return norm_s1 == norm_s2
-def get_next_redirect_url(request, redirect_field_name="next"):
+def get_next_redirect_url(request, redirect_field_name=REDIRECT_FIELD_NAME):
"""
Returns the next URL to redirect to, if it was explicitly passed
via the request.
@@ -45,7 +45,9 @@ def get_next_redirect_url(request, redirect_field_name="next"):
return redirect_to
-def get_login_redirect_url(request, url=None, redirect_field_name="next", signup=False):
+def get_login_redirect_url(
+ request, url=None, redirect_field_name=REDIRECT_FIELD_NAME, signup=False
+):
ret = url
if url and callable(url):
# In order to be able to pass url getters around that depend
diff --git a/allauth/account/views.py b/allauth/account/views.py
index 80225b41de..c882ec2c6d 100644
--- a/allauth/account/views.py
+++ b/allauth/account/views.py
@@ -1,21 +1,15 @@
from django.contrib import messages
-from django.contrib.auth import REDIRECT_FIELD_NAME
from django.contrib.auth.decorators import login_required
from django.contrib.sites.shortcuts import get_current_site
from django.core.exceptions import PermissionDenied
from django.core.validators import validate_email
from django.forms import ValidationError
-from django.http import (
- Http404,
- HttpResponse,
- HttpResponsePermanentRedirect,
- HttpResponseRedirect,
-)
+from django.http import Http404, HttpResponse, HttpResponseRedirect
from django.urls import reverse, reverse_lazy
from django.utils.decorators import method_decorator
from django.views.decorators.cache import never_cache
from django.views.decorators.debug import sensitive_post_parameters
-from django.views.generic.base import TemplateResponseMixin, TemplateView, View
+from django.views.generic.base import TemplateView
from django.views.generic.edit import FormView
from allauth import app_settings as allauth_app_settings
@@ -33,6 +27,14 @@
UserTokenForm,
)
from allauth.account.internal import flows
+from allauth.account.mixins import (
+ AjaxCapableProcessFormViewMixin,
+ CloseableSignupMixin,
+ LogoutFunctionalityMixin,
+ NextRedirectMixin,
+ RedirectAuthenticatedUserMixin,
+ _ajax_response,
+)
from allauth.account.models import (
EmailAddress,
EmailConfirmation,
@@ -41,9 +43,6 @@
from allauth.account.reauthentication import resume_request
from allauth.account.utils import (
complete_signup,
- get_login_redirect_url,
- get_next_redirect_url,
- passthrough_next_redirect_url,
perform_login,
send_email_confirmation,
sync_user_email_addresses,
@@ -54,7 +53,7 @@
from allauth.core.exceptions import ImmediateHttpResponse
from allauth.core.internal.httpkit import redirect
from allauth.decorators import rate_limit
-from allauth.utils import get_form_class, get_request_param
+from allauth.utils import get_form_class
INTERNAL_RESET_SESSION_KEY = "_password_reset_key"
@@ -65,101 +64,23 @@
)
-def _ajax_response(request, response, form=None, data=None):
- adapter = get_adapter()
- if adapter.is_ajax(request):
- if isinstance(response, HttpResponseRedirect) or isinstance(
- response, HttpResponsePermanentRedirect
- ):
- redirect_to = response["Location"]
- else:
- redirect_to = None
- response = adapter.ajax_response(
- request, response, form=form, data=data, redirect_to=redirect_to
- )
- return response
-
-
-class RedirectAuthenticatedUserMixin(object):
- def dispatch(self, request, *args, **kwargs):
- if request.user.is_authenticated and app_settings.AUTHENTICATED_LOGIN_REDIRECTS:
- redirect_to = self.get_authenticated_redirect_url()
- response = HttpResponseRedirect(redirect_to)
- return _ajax_response(request, response)
- else:
- response = super(RedirectAuthenticatedUserMixin, self).dispatch(
- request, *args, **kwargs
- )
- return response
-
- def get_authenticated_redirect_url(self):
- redirect_field_name = self.redirect_field_name
- return get_login_redirect_url(
- self.request,
- url=self.get_success_url(),
- redirect_field_name=redirect_field_name,
- )
-
-
-class AjaxCapableProcessFormViewMixin(object):
- def get(self, request, *args, **kwargs):
- response = super(AjaxCapableProcessFormViewMixin, self).get(
- request, *args, **kwargs
- )
- form = self.get_form()
- return _ajax_response(
- self.request, response, form=form, data=self._get_ajax_data_if()
- )
-
- def post(self, request, *args, **kwargs):
- form_class = self.get_form_class()
- form = self.get_form(form_class)
- if form.is_valid():
- response = self.form_valid(form)
- else:
- response = self.form_invalid(form)
- return _ajax_response(
- self.request, response, form=form, data=self._get_ajax_data_if()
- )
-
- def get_form(self, form_class=None):
- form = getattr(self, "_cached_form", None)
- if form is None:
- form = super(AjaxCapableProcessFormViewMixin, self).get_form(form_class)
- self._cached_form = form
- return form
-
- def _get_ajax_data_if(self):
- return (
- self.get_ajax_data()
- if get_adapter(self.request).is_ajax(self.request)
- else None
- )
-
- def get_ajax_data(self):
- return None
-
-
-class LogoutFunctionalityMixin(object):
- def logout(self):
- flows.logout.logout(self.request)
-
-
class LoginView(
- RedirectAuthenticatedUserMixin, AjaxCapableProcessFormViewMixin, FormView
+ NextRedirectMixin,
+ RedirectAuthenticatedUserMixin,
+ AjaxCapableProcessFormViewMixin,
+ FormView,
):
form_class = LoginForm
template_name = "account/login." + app_settings.TEMPLATE_EXTENSION
success_url = None
- redirect_field_name = REDIRECT_FIELD_NAME
@sensitive_post_parameters_m
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
- return super(LoginView, self).dispatch(request, *args, **kwargs)
+ return super().dispatch(request, *args, **kwargs)
def get_form_kwargs(self):
- kwargs = super(LoginView, self).get_form_kwargs()
+ kwargs = super().get_form_kwargs()
kwargs["request"] = self.request
return kwargs
@@ -167,34 +88,21 @@ def get_form_class(self):
return get_form_class(app_settings.FORMS, "login", self.form_class)
def form_valid(self, form):
- success_url = self.get_success_url()
+ redirect_url = self.get_success_url()
try:
- return form.login(self.request, redirect_url=success_url)
+ return form.login(self.request, redirect_url=redirect_url)
except ImmediateHttpResponse as e:
return e.response
- def get_success_url(self):
- # Explicitly passed ?next= URL takes precedence
- ret = (
- get_next_redirect_url(self.request, self.redirect_field_name)
- or self.success_url
- )
- return ret
-
def get_context_data(self, **kwargs):
ret = super(LoginView, self).get_context_data(**kwargs)
- signup_url = passthrough_next_redirect_url(
- self.request, reverse("account_signup"), self.redirect_field_name
- )
- redirect_field_value = get_request_param(self.request, self.redirect_field_name)
+ signup_url = self.passthrough_next_url(reverse("account_signup"))
site = get_current_site(self.request)
ret.update(
{
"signup_url": signup_url,
"site": site,
- "redirect_field_name": self.redirect_field_name,
- "redirect_field_value": redirect_field_value,
"SOCIALACCOUNT_ENABLED": allauth_app_settings.SOCIALACCOUNT_ENABLED,
}
)
@@ -204,74 +112,42 @@ def get_context_data(self, **kwargs):
login = LoginView.as_view()
-class CloseableSignupMixin(object):
- template_name_signup_closed = (
- "account/signup_closed." + app_settings.TEMPLATE_EXTENSION
- )
-
- def dispatch(self, request, *args, **kwargs):
- try:
- if not self.is_open():
- return self.closed()
- except ImmediateHttpResponse as e:
- return e.response
- return super(CloseableSignupMixin, self).dispatch(request, *args, **kwargs)
-
- def is_open(self):
- return get_adapter(self.request).is_open_for_signup(self.request)
-
- def closed(self):
- response_kwargs = {
- "request": self.request,
- "template": self.template_name_signup_closed,
- }
- return self.response_class(**response_kwargs)
-
-
@method_decorator(rate_limit(action="signup"), name="dispatch")
class SignupView(
RedirectAuthenticatedUserMixin,
CloseableSignupMixin,
+ NextRedirectMixin,
AjaxCapableProcessFormViewMixin,
FormView,
):
template_name = "account/signup." + app_settings.TEMPLATE_EXTENSION
form_class = SignupForm
- redirect_field_name = REDIRECT_FIELD_NAME
- success_url = None
@sensitive_post_parameters_m
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
- return super(SignupView, self).dispatch(request, *args, **kwargs)
+ return super().dispatch(request, *args, **kwargs)
def get_form_class(self):
return get_form_class(app_settings.FORMS, "signup", self.form_class)
- def get_success_url(self):
- # Explicitly passed ?next= URL takes precedence
- ret = (
- get_next_redirect_url(self.request, self.redirect_field_name)
- or self.success_url
- )
- return ret
-
def form_valid(self, form):
self.user, resp = form.try_save(self.request)
if resp:
return resp
try:
+ redirect_url = self.get_success_url()
return complete_signup(
self.request,
self.user,
email_verification=None,
- success_url=self.get_success_url(),
+ success_url=redirect_url,
)
except ImmediateHttpResponse as e:
return e.response
def get_context_data(self, **kwargs):
- ret = super(SignupView, self).get_context_data(**kwargs)
+ ret = super().get_context_data(**kwargs)
form = ret["form"]
email = self.request.session.get("account_verified_email")
if email:
@@ -280,17 +156,11 @@ def get_context_data(self, **kwargs):
email_keys.append("email2")
for email_key in email_keys:
form.fields[email_key].initial = email
- login_url = passthrough_next_redirect_url(
- self.request, reverse("account_login"), self.redirect_field_name
- )
- redirect_field_name = self.redirect_field_name
+ login_url = self.passthrough_next_url(reverse("account_login"))
site = get_current_site(self.request)
- redirect_field_value = get_request_param(self.request, redirect_field_name)
ret.update(
{
"login_url": login_url,
- "redirect_field_name": redirect_field_name,
- "redirect_field_value": redirect_field_value,
"site": site,
"SOCIALACCOUNT_ENABLED": allauth_app_settings.SOCIALACCOUNT_ENABLED,
}
@@ -314,7 +184,7 @@ def get_initial(self):
signup = SignupView.as_view()
-class ConfirmEmailView(TemplateResponseMixin, LogoutFunctionalityMixin, View):
+class ConfirmEmailView(NextRedirectMixin, LogoutFunctionalityMixin, TemplateView):
template_name = "account/email_confirm." + app_settings.TEMPLATE_EXTENSION
def get(self, *args, **kwargs):
@@ -447,7 +317,7 @@ def get_ajax_data(self):
return ret
def get_context_data(self, **kwargs):
- ctx = kwargs
+ ctx = super().get_context_data(**kwargs)
site = get_current_site(self.request)
ctx.update(
{
@@ -462,9 +332,12 @@ def get_context_data(self, **kwargs):
return ctx
def get_redirect_url(self):
- return get_adapter(self.request).get_email_confirmation_redirect_url(
- self.request
- )
+ url = self.get_next_url()
+ if not url:
+ url = get_adapter(self.request).get_email_confirmation_redirect_url(
+ self.request
+ )
+ return url
confirm_email = ConfirmEmailView.as_view()
@@ -596,7 +469,7 @@ def get_ajax_data(self):
@method_decorator(login_required, name="dispatch")
@method_decorator(rate_limit(action="change_password"), name="dispatch")
-class PasswordChangeView(AjaxCapableProcessFormViewMixin, FormView):
+class PasswordChangeView(AjaxCapableProcessFormViewMixin, NextRedirectMixin, FormView):
template_name = "account/password_change." + app_settings.TEMPLATE_EXTENSION
form_class = ChangePasswordForm
@@ -614,9 +487,7 @@ def get_form_kwargs(self):
kwargs["user"] = self.request.user
return kwargs
- def get_success_url(self):
- if self.success_url:
- return self.success_url
+ def get_default_success_url(self):
return get_adapter().get_password_change_redirect_url(self.request)
def form_valid(self, form):
@@ -642,7 +513,7 @@ def get_context_data(self, **kwargs):
rate_limit(action="change_password"),
name="dispatch",
)
-class PasswordSetView(AjaxCapableProcessFormViewMixin, FormView):
+class PasswordSetView(AjaxCapableProcessFormViewMixin, NextRedirectMixin, FormView):
template_name = "account/password_set." + app_settings.TEMPLATE_EXTENSION
form_class = SetPasswordForm
@@ -660,9 +531,7 @@ def get_form_kwargs(self):
kwargs["user"] = self.request.user
return kwargs
- def get_success_url(self):
- if self.success_url:
- return self.success_url
+ def get_default_success_url(self):
return get_adapter().get_password_change_redirect_url(self.request)
def form_valid(self, form):
@@ -681,11 +550,10 @@ def get_context_data(self, **kwargs):
password_set = PasswordSetView.as_view()
-class PasswordResetView(AjaxCapableProcessFormViewMixin, FormView):
+class PasswordResetView(NextRedirectMixin, AjaxCapableProcessFormViewMixin, FormView):
template_name = "account/password_reset." + app_settings.TEMPLATE_EXTENSION
form_class = ResetPasswordForm
success_url = reverse_lazy("account_reset_password_done")
- redirect_field_name = REDIRECT_FIELD_NAME
def get_form_class(self):
return get_form_class(app_settings.FORMS, "reset_password", self.form_class)
@@ -699,13 +567,11 @@ def form_valid(self, form):
if r429:
return r429
form.save(self.request)
- return super(PasswordResetView, self).form_valid(form)
+ return super().form_valid(form)
def get_context_data(self, **kwargs):
- ret = super(PasswordResetView, self).get_context_data(**kwargs)
- login_url = passthrough_next_redirect_url(
- self.request, reverse("account_login"), self.redirect_field_name
- )
+ ret = super().get_context_data(**kwargs)
+ login_url = self.passthrough_next_url(reverse("account_login"))
# NOTE: For backwards compatibility
ret["password_reset_form"] = ret.get("form")
# (end NOTE)
@@ -725,7 +591,10 @@ class PasswordResetDoneView(TemplateView):
@method_decorator(rate_limit(action="reset_password_from_key"), name="dispatch")
class PasswordResetFromKeyView(
- AjaxCapableProcessFormViewMixin, LogoutFunctionalityMixin, FormView
+ AjaxCapableProcessFormViewMixin,
+ NextRedirectMixin,
+ LogoutFunctionalityMixin,
+ FormView,
):
template_name = "account/password_reset_from_key." + app_settings.TEMPLATE_EXTENSION
form_class = ResetPasswordKeyForm
@@ -763,9 +632,7 @@ def dispatch(self, request, uidb36, key, **kwargs):
self.logout()
self.request.session[INTERNAL_RESET_SESSION_KEY] = self.key
- return super(PasswordResetFromKeyView, self).dispatch(
- request, uidb36, self.key, **kwargs
- )
+ return super().dispatch(request, uidb36, self.key, **kwargs)
else:
token_form = user_token_form_class(data={"uidb36": uidb36, "key": self.key})
if token_form.is_valid():
@@ -774,7 +641,9 @@ def dispatch(self, request, uidb36, key, **kwargs):
# avoids the possibility of leaking the key in the
# HTTP Referer header.
self.request.session[INTERNAL_RESET_SESSION_KEY] = self.key
- redirect_url = self.request.path.replace(self.key, self.reset_url_key)
+ redirect_url = self.passthrough_next_url(
+ self.request.path.replace(self.key, self.reset_url_key)
+ )
return redirect(redirect_url)
self.reset_user = None
@@ -821,9 +690,8 @@ class PasswordResetFromKeyDoneView(TemplateView):
password_reset_from_key_done = PasswordResetFromKeyDoneView.as_view()
-class LogoutView(TemplateResponseMixin, LogoutFunctionalityMixin, View):
+class LogoutView(NextRedirectMixin, LogoutFunctionalityMixin, TemplateView):
template_name = "account/logout." + app_settings.TEMPLATE_EXTENSION
- redirect_field_name = REDIRECT_FIELD_NAME
def get(self, *args, **kwargs):
if app_settings.LOGOUT_ON_GET:
@@ -842,21 +710,10 @@ def post(self, *args, **kwargs):
response = redirect(url)
return _ajax_response(self.request, response)
- def get_context_data(self, **kwargs):
- ctx = kwargs
- redirect_field_value = get_request_param(self.request, self.redirect_field_name)
- ctx.update(
- {
- "redirect_field_name": self.redirect_field_name,
- "redirect_field_value": redirect_field_value,
- }
- )
- return ctx
-
def get_redirect_url(self):
- return get_next_redirect_url(
- self.request, self.redirect_field_name
- ) or get_adapter(self.request).get_logout_redirect_url(self.request)
+ return self.get_next_url() or get_adapter(self.request).get_logout_redirect_url(
+ self.request
+ )
logout = LogoutView.as_view()
@@ -876,9 +733,7 @@ class EmailVerificationSentView(TemplateView):
email_verification_sent = EmailVerificationSentView.as_view()
-class BaseReauthenticateView(FormView):
- redirect_field_name = REDIRECT_FIELD_NAME
-
+class BaseReauthenticateView(NextRedirectMixin, FormView):
def dispatch(self, request, *args, **kwargs):
resp = self._check_reauthentication_method_available(request)
if resp:
@@ -903,15 +758,11 @@ def _check_reauthentication_method_available(self, request):
if not methods:
# Reauthentication not available
raise PermissionDenied("Reauthentication not available")
- url = passthrough_next_redirect_url(
- request, methods[0]["url"], self.redirect_field_name
- )
+ url = self.passthrough_next_url(methods[0]["url"])
return HttpResponseRedirect(url)
- def get_success_url(self):
- url = get_next_redirect_url(self.request, self.redirect_field_name)
- if not url:
- url = get_adapter(self.request).get_login_redirect_url(self.request)
+ def get_default_success_url(self):
+ url = get_adapter(self.request).get_login_redirect_url(self.request)
return url
def form_valid(self, form):
@@ -922,11 +773,8 @@ def form_valid(self, form):
def get_context_data(self, **kwargs):
ret = super().get_context_data(**kwargs)
- redirect_field_value = get_request_param(self.request, self.redirect_field_name)
ret.update(
{
- "redirect_field_name": self.redirect_field_name,
- "redirect_field_value": redirect_field_value,
"reauthentication_alternatives": self.get_reauthentication_alternatives(),
}
)
@@ -939,9 +787,7 @@ def get_reauthentication_alternatives(self):
alt = dict(method)
if self.request.path == alt["url"]:
continue
- alt["url"] = passthrough_next_redirect_url(
- self.request, alt["url"], self.redirect_field_name
- )
+ alt["url"] = self.passthrough_next_url(alt["url"])
alts.append(alt)
alts = sorted(alts, key=lambda alt: alt["description"])
return alts
diff --git a/allauth/socialaccount/providers/facebook/provider.py b/allauth/socialaccount/providers/facebook/provider.py
index 8f0bb0f507..0a4417edb6 100644
--- a/allauth/socialaccount/providers/facebook/provider.py
+++ b/allauth/socialaccount/providers/facebook/provider.py
@@ -3,6 +3,7 @@
import string
from urllib.parse import quote
+from django.contrib.auth import REDIRECT_FIELD_NAME
from django.middleware.csrf import get_token
from django.template.loader import render_to_string
from django.urls import reverse
@@ -70,7 +71,7 @@ def get_method(self):
def get_login_url(self, request, **kwargs):
method = kwargs.pop("method", self.get_method())
if method == "js_sdk":
- next = "'%s'" % escapejs(kwargs.get("next") or "")
+ next = "'%s'" % escapejs(kwargs.get(REDIRECT_FIELD_NAME) or "")
process = "'%s'" % escapejs(kwargs.get("process") or AuthProcess.LOGIN)
action = "'%s'" % escapejs(kwargs.get("action") or AuthAction.AUTHENTICATE)
scope = "'%s'" % escapejs(kwargs.get("scope", ""))
diff --git a/allauth/socialaccount/providers/openid/views.py b/allauth/socialaccount/providers/openid/views.py
index f9db6f1a17..a1de59e510 100644
--- a/allauth/socialaccount/providers/openid/views.py
+++ b/allauth/socialaccount/providers/openid/views.py
@@ -1,3 +1,4 @@
+from django.contrib.auth import REDIRECT_FIELD_NAME
from django.http import HttpResponseRedirect
from django.shortcuts import render
from django.urls import reverse
@@ -64,7 +65,7 @@ def get_form(self):
if self.request.method == "GET" and "openid" not in self.request.GET:
return self.form_class(
initial={
- "next": self.request.GET.get("next"),
+ "next": self.request.GET.get(REDIRECT_FIELD_NAME),
"process": self.request.GET.get("process"),
}
)
diff --git a/allauth/socialaccount/templatetags/socialaccount.py b/allauth/socialaccount/templatetags/socialaccount.py
index 928544b606..74acc4b76c 100644
--- a/allauth/socialaccount/templatetags/socialaccount.py
+++ b/allauth/socialaccount/templatetags/socialaccount.py
@@ -1,4 +1,5 @@
from django import template
+from django.contrib.auth import REDIRECT_FIELD_NAME
from django.utils.safestring import mark_safe
from allauth.socialaccount.adapter import get_adapter
@@ -26,15 +27,15 @@ def provider_login_url(context, provider, **params):
del query["scope"]
if auth_params == "":
del query["auth_params"]
- if "next" not in query:
- next = get_request_param(request, "next")
+ if REDIRECT_FIELD_NAME not in query:
+ next = get_request_param(request, REDIRECT_FIELD_NAME)
if next:
- query["next"] = next
+ query[REDIRECT_FIELD_NAME] = next
elif process == "redirect":
- query["next"] = request.get_full_path()
+ query[REDIRECT_FIELD_NAME] = request.get_full_path()
else:
- if not query["next"]:
- del query["next"]
+ if not query[REDIRECT_FIELD_NAME]:
+ del query[REDIRECT_FIELD_NAME]
# get the login url and append query as url parameters
return provider.get_login_url(request, **query)
diff --git a/allauth/templates/account/email_confirm.html b/allauth/templates/account/email_confirm.html
index b34157429b..2020656a00 100644
--- a/allauth/templates/account/email_confirm.html
+++ b/allauth/templates/account/email_confirm.html
@@ -19,6 +19,7 @@
{% element form method="post" action=action_url %}
{% slot actions %}
{% csrf_token %}
+ {{ redirect_field }}
{% element button type="submit" %}
{% trans 'Confirm' %}
{% endelement %}
diff --git a/allauth/templates/account/login.html b/allauth/templates/account/login.html
index 765c2fdd5e..d3519f8b4e 100644
--- a/allauth/templates/account/login.html
+++ b/allauth/templates/account/login.html
@@ -18,11 +18,7 @@
{% csrf_token %}
{% element fields form=form unlabeled=True %}
{% endelement %}
- {% if redirect_field_value %}
-
- {% endif %}
+ {{ redirect_field }}
{% endslot %}
{% slot actions %}
{% element button type="submit" tags="prominent,login" %}
diff --git a/allauth/templates/account/logout.html b/allauth/templates/account/logout.html
index 7c4adfa210..84530a923b 100644
--- a/allauth/templates/account/logout.html
+++ b/allauth/templates/account/logout.html
@@ -12,11 +12,7 @@
{% element form method="post" action=action_url no_visible_fields=True %}
{% slot body %}
{% csrf_token %}
- {% if redirect_field_value %}
-
- {% endif %}
+ {{ redirect_field }}
{% endslot %}
{% slot actions %}
{% element button type="submit" %}
diff --git a/allauth/templates/account/password_change.html b/allauth/templates/account/password_change.html
index 9df9e262b5..297dba6f13 100644
--- a/allauth/templates/account/password_change.html
+++ b/allauth/templates/account/password_change.html
@@ -11,6 +11,7 @@
{% element form form=form method="post" action=action_url %}
{% slot body %}
{% csrf_token %}
+ {{ redirect_field }}
{% element fields form=form %}
{% endelement %}
{% endslot %}
diff --git a/allauth/templates/account/password_reset_from_key.html b/allauth/templates/account/password_reset_from_key.html
index f8f0d3f9bf..6c0ef3a5da 100644
--- a/allauth/templates/account/password_reset_from_key.html
+++ b/allauth/templates/account/password_reset_from_key.html
@@ -21,6 +21,7 @@
{% element form method="post" action=action_url %}
{% slot body %}
{% csrf_token %}
+ {{ redirect_field }}
{% element fields form=form %}
{% endelement %}
{% endslot %}
diff --git a/allauth/templates/account/password_set.html b/allauth/templates/account/password_set.html
index 21e2fc822c..2cb77a4222 100644
--- a/allauth/templates/account/password_set.html
+++ b/allauth/templates/account/password_set.html
@@ -12,6 +12,7 @@
{% element form method="post" action=action_url %}
{% slot body %}
{% csrf_token %}
+ {{ redirect_field }}
{% element fields form=form %}
{% endelement %}
{% endslot %}
diff --git a/allauth/templates/account/reauthenticate.html b/allauth/templates/account/reauthenticate.html
index 38c22def50..6d7fff5d99 100644
--- a/allauth/templates/account/reauthenticate.html
+++ b/allauth/templates/account/reauthenticate.html
@@ -9,11 +9,7 @@
{% csrf_token %}
{% element fields form=form unlabeled=True %}
{% endelement %}
- {% if redirect_field_value %}
-
- {% endif %}
+ {{ redirect_field }}
{% endslot %}
{% slot actions %}
{% element button type="submit" %}
diff --git a/allauth/templates/account/signup.html b/allauth/templates/account/signup.html
index d55fb146aa..d59c2553b3 100644
--- a/allauth/templates/account/signup.html
+++ b/allauth/templates/account/signup.html
@@ -16,11 +16,7 @@
{% csrf_token %}
{% element fields form=form unlabeled=True %}
{% endelement %}
- {% if redirect_field_value %}
-
- {% endif %}
+ {{ redirect_field }}
{% endslot %}
{% slot actions %}
{% element button tags="prominent,signup" type="submit" %}
diff --git a/allauth/templates/mfa/reauthenticate.html b/allauth/templates/mfa/reauthenticate.html
index 749af87af1..eca1dfe388 100644
--- a/allauth/templates/mfa/reauthenticate.html
+++ b/allauth/templates/mfa/reauthenticate.html
@@ -9,11 +9,7 @@
{% csrf_token %}
{% element fields form=form unlabeled=True %}
{% endelement %}
- {% if redirect_field_value %}
-
- {% endif %}
+ {{ redirect_field }}
{% endslot %}
{% slot actions %}
{% element button type="submit" tags="primary,mfa,login" %}
diff --git a/allauth/templates/socialaccount/signup.html b/allauth/templates/socialaccount/signup.html
index a361ec37df..cfcdfa88c0 100644
--- a/allauth/templates/socialaccount/signup.html
+++ b/allauth/templates/socialaccount/signup.html
@@ -18,11 +18,7 @@
{% csrf_token %}
{% element fields form=form unlabeled=True %}
{% endelement %}
- {% if redirect_field_value %}
-
- {% endif %}
+ {{ redirect_field }}
{% endslot %}
{% slot actions %}
{% element button type="submit" %}