diff --git a/AUTHORS b/AUTHORS index 5acbe272337a..5e7bca67f587 100644 --- a/AUTHORS +++ b/AUTHORS @@ -159,6 +159,7 @@ answer newbie questions, and generally made Django that much better: Ben Slavin Ben Sturmfels Bendegúz Csirmaz + Benedict Etzel Berker Peksag Bernd Schlapsi Bernhard Essl diff --git a/django/contrib/admin/sites.py b/django/contrib/admin/sites.py index 17af19fd1bbb..410bf20da0ff 100644 --- a/django/contrib/admin/sites.py +++ b/django/contrib/admin/sites.py @@ -416,29 +416,27 @@ def login(self, request, extra_context=None): """ Display the login form for the given HttpRequest. """ - if request.method == "GET" and self.has_permission(request): - # Already logged-in, redirect to admin index - index_path = reverse("admin:index", current_app=self.name) - return HttpResponseRedirect(index_path) - # Since this module gets imported in the application's root package, # it cannot import models from other applications at the module level, # and django.contrib.admin.forms eventually imports User. from django.contrib.admin.forms import AdminAuthenticationForm from django.contrib.auth.views import LoginView + redirect_url = LoginView().get_redirect_url(request) or reverse( + "admin:index", current_app=self.name + ) + if request.method == "GET" and self.has_permission(request): + # Already logged-in, redirect accordingly. + return HttpResponseRedirect(redirect_url) + context = { **self.each_context(request), "title": _("Log in"), "subtitle": None, "app_path": request.get_full_path(), "username": request.user.get_username(), + REDIRECT_FIELD_NAME: redirect_url, } - if ( - REDIRECT_FIELD_NAME not in request.GET - and REDIRECT_FIELD_NAME not in request.POST - ): - context[REDIRECT_FIELD_NAME] = reverse("admin:index", current_app=self.name) context.update(extra_context or {}) defaults = { diff --git a/django/contrib/auth/views.py b/django/contrib/auth/views.py index 295f2219cf1f..dcffb4aca6e2 100644 --- a/django/contrib/auth/views.py +++ b/django/contrib/auth/views.py @@ -40,20 +40,28 @@ class RedirectURLMixin: def get_success_url(self): return self.get_redirect_url() or self.get_default_redirect_url() - def get_redirect_url(self): - """Return the user-originating redirect URL if it's safe.""" - redirect_to = self.request.POST.get( - self.redirect_field_name, self.request.GET.get(self.redirect_field_name) + def get_redirect_url(self, request=None): + """Return the user-originating redirect URL if it's safe. + + Optionally takes a request argument, allowing use outside class-based + views. + """ + if request is None: + request = self.request + redirect_to = request.POST.get( + self.redirect_field_name, request.GET.get(self.redirect_field_name) ) url_is_safe = url_has_allowed_host_and_scheme( url=redirect_to, - allowed_hosts=self.get_success_url_allowed_hosts(), - require_https=self.request.is_secure(), + allowed_hosts=self.get_success_url_allowed_hosts(request), + require_https=request.is_secure(), ) return redirect_to if url_is_safe else "" - def get_success_url_allowed_hosts(self): - return {self.request.get_host(), *self.success_url_allowed_hosts} + def get_success_url_allowed_hosts(self, request=None): + if request is None: + request = self.request + return {request.get_host(), *self.success_url_allowed_hosts} def get_default_redirect_url(self): """Return the default redirect URL.""" diff --git a/django/db/models/fields/json.py b/django/db/models/fields/json.py index 819c87119ab8..8ca9cc6811e7 100644 --- a/django/db/models/fields/json.py +++ b/django/db/models/fields/json.py @@ -12,6 +12,7 @@ PostgresOperatorLookup, Transform, ) +from django.utils.deconstruct import deconstructible from django.utils.deprecation import RemovedInDjango70Warning, django_file_prefixes from django.utils.translation import gettext_lazy as _ @@ -150,6 +151,7 @@ def formfield(self, **kwargs): ) +@deconstructible(path="django.db.models.JSONNull") class JSONNull(expressions.Value): """Represent JSON `null` primitive.""" diff --git a/docs/releases/6.1.txt b/docs/releases/6.1.txt index 036fee53cfc2..e0edf6876af0 100644 --- a/docs/releases/6.1.txt +++ b/docs/releases/6.1.txt @@ -92,7 +92,8 @@ Minor features :mod:`django.contrib.admin` ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -* ... +* The admin site login view now redirects authenticated users to the next URL, + if available, instead of always redirecting to the admin index page. :mod:`django.contrib.admindocs` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/admin_views/tests.py b/tests/admin_views/tests.py index 868b616d765a..f7eaad659e6c 100644 --- a/tests/admin_views/tests.py +++ b/tests/admin_views/tests.py @@ -2413,6 +2413,32 @@ def test_login_redirect_for_direct_get(self): self.assertEqual(response.status_code, 200) self.assertEqual(response.context[REDIRECT_FIELD_NAME], reverse("admin:index")) + def test_login_redirect_when_logged_in(self): + self.client.force_login(self.superuser) + response = self.client.get(reverse("admin:login")) + self.assertRedirects(response, reverse("admin:index")) + + def test_login_redirect_to_next_url_when_logged_in(self): + self.client.force_login(self.superuser) + next_url = reverse("admin:admin_views_article_add") + response = self.client.get( + reverse("admin:login"), + query_params={REDIRECT_FIELD_NAME: next_url}, + ) + self.assertRedirects(response, next_url) + + def test_login_redirect_unsafe_next_url_when_logged_in(self): + self.client.force_login(self.superuser) + response = self.client.get( + reverse("admin:login"), + query_params={ + REDIRECT_FIELD_NAME: "https://example.com/bad", + }, + ) + self.assertRedirects( + response, reverse("admin:index"), fetch_redirect_response=False + ) + def test_login_has_permission(self): # Regular User should not be able to login. response = self.client.get(reverse("has_permission_admin:index")) diff --git a/tests/model_fields/test_jsonfield.py b/tests/model_fields/test_jsonfield.py index 937b55779422..cf8e1888e51c 100644 --- a/tests/model_fields/test_jsonfield.py +++ b/tests/model_fields/test_jsonfield.py @@ -1265,6 +1265,13 @@ class JSONNullTests(TestCase): def test_repr(self): self.assertEqual(repr(JSONNull()), "JSONNull()") + def test_deconstruct(self): + jsonnull = JSONNull() + path, args, kwargs = jsonnull.deconstruct() + self.assertEqual(path, "django.db.models.JSONNull") + self.assertEqual(args, ()) + self.assertEqual(kwargs, {}) + def test_save_load(self): obj = JSONModel(value=JSONNull()) obj.save()