From 7ab2d803533dd3ffe310055414ad584cfbbf0bea Mon Sep 17 00:00:00 2001 From: anurag629 Date: Tue, 17 Mar 2026 22:17:31 +0530 Subject: [PATCH 1/2] Fixed #17631 -- Handled fields and files with the same name in encode_multipart(). When data is a MultiValueDict, encode_multipart() now uses data.lists() instead of data.items() to preserve all values per key. Previously, only the last value for each key was encoded, silently dropping form field values when a file upload shared the same name. --- django/test/client.py | 5 ++++- tests/test_client/tests.py | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/django/test/client.py b/django/test/client.py index d4990453635c..d13bc28fc6ab 100644 --- a/django/test/client.py +++ b/django/test/client.py @@ -295,7 +295,10 @@ def is_file(thing): # Each bit of the multipart form data could be either a form value or a # file, or a *list* of form values and/or files. Remember that HTTP field # names can be duplicated! - for key, value in data.items(): + # Use lists() if data is a MultiValueDict, so that all values for a key + # are preserved (e.g. a form field and a file sharing the same name). + raw_data = getattr(data, "lists", data.items)() + for key, value in raw_data: if value is None: raise TypeError( "Cannot encode None for key '%s' as POST data. Did you mean " diff --git a/tests/test_client/tests.py b/tests/test_client/tests.py index 2ba9455d0c16..98f01827d0a7 100644 --- a/tests/test_client/tests.py +++ b/tests/test_client/tests.py @@ -27,6 +27,7 @@ from django.contrib.auth.models import Permission, User from django.core import mail +from django.core.files.uploadedfile import SimpleUploadedFile from django.http import HttpResponse, HttpResponseNotAllowed from django.test import ( AsyncRequestFactory, @@ -38,6 +39,7 @@ override_settings, ) from django.urls import reverse_lazy +from django.utils.datastructures import MultiValueDict from django.utils.decorators import async_only_middleware from django.views.generic import RedirectView @@ -1045,6 +1047,23 @@ def test_uploading_named_temp_file(self): ) self.assertEqual(response.content, b"named_temp_file") + def test_uploading_file_and_field_with_same_name(self): + """ + A form field and file uploads sharing the same name in a + MultiValueDict are both preserved when posted by the test client. + """ + file = SimpleUploadedFile("file.txt", b"content") + response = self.client.post( + "/upload_view/", + data=MultiValueDict({"shared": ["field_value", file]}), + ) + request = response.wsgi_request + self.assertEqual(request.POST.getlist("shared"), ["field_value"]) + self.assertEqual( + [f.name for f in request.FILES.getlist("shared")], + ["file.txt"], + ) + def test_query_params(self): tests = ( "get", From e28c2a9a011a8260e58fe4cf2b4b948116d4223f Mon Sep 17 00:00:00 2001 From: antoliny0919 Date: Fri, 10 Oct 2025 20:57:34 +0900 Subject: [PATCH 2/2] Fixed #36621 -- Fixed arrow buttons position in admin filter_vertical widget. --- .../admin/static/admin/css/responsive.css | 2 ++ .../contrib/admin/static/admin/css/widgets.css | 3 +-- tests/admin_widgets/tests.py | 17 +++++++++++++++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/django/contrib/admin/static/admin/css/responsive.css b/django/contrib/admin/static/admin/css/responsive.css index 509b4fb9ff8e..862fdd445828 100644 --- a/django/contrib/admin/static/admin/css/responsive.css +++ b/django/contrib/admin/static/admin/css/responsive.css @@ -301,6 +301,7 @@ button { .stacked ul.selector-chooser { padding: 0 2px; transform: none; + margin: 10px auto; } .stacked .selector-chooser li { @@ -571,6 +572,7 @@ button { height: 30px; padding: 0 2px; transform: none; + margin: auto; } .selector ul.selector-chooser li { diff --git a/django/contrib/admin/static/admin/css/widgets.css b/django/contrib/admin/static/admin/css/widgets.css index 55d91035a3b3..128b2498dd72 100644 --- a/django/contrib/admin/static/admin/css/widgets.css +++ b/django/contrib/admin/static/admin/css/widgets.css @@ -241,7 +241,7 @@ display: flex; height: 30px; width: 64px; - margin: 0 0 10px 40%; + margin: 0 auto 10px auto; background-color: #eee; border-radius: 10px; transform: none; @@ -628,7 +628,6 @@ ul.timelist, .related-widget-wrapper { display: flex; gap: 0 10px; - flex-grow: 1; flex-wrap: wrap; margin-bottom: 5px; } diff --git a/tests/admin_widgets/tests.py b/tests/admin_widgets/tests.py index a607f1fedcd1..0e4749563aad 100644 --- a/tests/admin_widgets/tests.py +++ b/tests/admin_widgets/tests.py @@ -1861,6 +1861,23 @@ def test_form_submission_via_enter_key_with_filter_horizontal(self): school.alumni.all().order_by("name"), [self.lisa, self.peter] ) + @screenshot_cases(["desktop_size", "mobile_size", "rtl", "dark", "high_contrast"]) + def test_vertical_arrow_buttons_layout(self): + from selenium.webdriver.common.by import By + + self.admin_login(username="super", password="secret", login_url="/") + self.selenium.get( + self.live_server_url + + reverse("admin:admin_widgets_school_change", args=(self.school.id,)) + ) + students = self.selenium.find_element(By.CSS_SELECTOR, ".field-students") + self.selenium.execute_script("window.scrollTo(0, %s);" % students.location["y"]) + buttons = self.selenium.find_element( + By.CSS_SELECTOR, "div.selector.stacked ul.selector-chooser" + ) + self.assertTrue(buttons.is_displayed()) + self.take_screenshot("arrow_buttons") + class AdminRawIdWidgetSeleniumTests(AdminWidgetSeleniumTestCase): def setUp(self):