diff --git a/django/views/generic/base.py b/django/views/generic/base.py index 8412288be1..485b74d377 100644 --- a/django/views/generic/base.py +++ b/django/views/generic/base.py @@ -1,4 +1,5 @@ import logging +from urllib.parse import urlparse from asgiref.sync import iscoroutinefunction, markcoroutinefunction @@ -252,7 +253,10 @@ class RedirectView(View): args = self.request.META.get("QUERY_STRING", "") if args and self.query_string: - url = "%s?%s" % (url, args) + if urlparse(url).query: + url = f"{url}&{args}" + else: + url = f"{url}?{args}" return url def get(self, request, *args, **kwargs): diff --git a/tests/generic_views/test_base.py b/tests/generic_views/test_base.py index cc5dcf4e39..9d6cef8a46 100644 --- a/tests/generic_views/test_base.py +++ b/tests/generic_views/test_base.py @@ -587,6 +587,31 @@ class RedirectViewTest(LoggingAssertionMixin, SimpleTestCase): handler, f"Gone: {escaped}", logging.WARNING, 410, request ) + def test_redirect_with_querry_string_in_destination(self): + response = RedirectView.as_view(url="/bar/?pork=spam", query_string=True)( + self.rf.get("/foo") + ) + self.assertEqual(response.status_code, 302) + self.assertEqual(response.headers["Location"], "/bar/?pork=spam") + + def test_redirect_with_query_string_in_destination_and_request(self): + response = RedirectView.as_view(url="/bar/?pork=spam", query_string=True)( + self.rf.get("/foo/?utm_source=social") + ) + self.assertEqual(response.status_code, 302) + self.assertEqual( + response.headers["Location"], "/bar/?pork=spam&utm_source=social" + ) + + def test_redirect_with_same_query_string_param_will_append_not_replace(self): + response = RedirectView.as_view(url="/bar/?pork=spam", query_string=True)( + self.rf.get("/foo/?utm_source=social&pork=ham") + ) + self.assertEqual(response.status_code, 302) + self.assertEqual( + response.headers["Location"], "/bar/?pork=spam&utm_source=social&pork=ham" + ) + class GetContextDataTest(SimpleTestCase): def test_get_context_data_super(self):