From 7a833f2b2909c180109234324fc83144ea50fbc6 Mon Sep 17 00:00:00 2001 From: "utkarsh.arya@zomato.com" Date: Sat, 15 Nov 2025 22:58:06 +0000 Subject: [PATCH] Fix data loss in admin when formset prefix has regex chars Escape formset prefix in regex to prevent key matching errors when special regex characters are used in the prefix. Regression introduced in b18650a2634890aa758abae2f33875daa13a9ba3. --- django/contrib/admin/options.py | 2 +- tests/admin_changelist/tests.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index 137e6faa0f..a100f1c499 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -1631,7 +1631,7 @@ class ModelAdmin(BaseModelAdmin): def _get_edited_object_pks(self, request, prefix): """Return POST data values of list_editable primary keys.""" - pk_pattern = re.compile(r'{}-\d+-{}$'.format(prefix, self.model._meta.pk.name)) + pk_pattern = re.compile(r'{}-\d+-{}$'.format(re.escape(prefix), self.model._meta.pk.name)) return [value for key, value in request.POST.items() if pk_pattern.match(key)] def _get_list_editable_queryset(self, request, prefix): diff --git a/tests/admin_changelist/tests.py b/tests/admin_changelist/tests.py index 05490b061a..b04fd5f8f0 100644 --- a/tests/admin_changelist/tests.py +++ b/tests/admin_changelist/tests.py @@ -819,6 +819,38 @@ class ChangeListTests(TestCase): pks = m._get_edited_object_pks(request, prefix='form') self.assertEqual(sorted(pks), sorted([str(a.pk), str(b.pk), str(c.pk)])) + def test_get_edited_object_ids_with_regex_chars_in_prefix(self): + """ + Test that _get_edited_object_pks() correctly handles formset prefixes + that contain regex special characters. + """ + a = Swallow.objects.create(origin='Swallow A', load=4, speed=1) + b = Swallow.objects.create(origin='Swallow B', load=2, speed=2) + superuser = self._create_superuser('superuser') + self.client.force_login(superuser) + changelist_url = reverse('admin:admin_changelist_swallow_changelist') + m = SwallowAdmin(Swallow, custom_site) + + # Test with various regex special characters in prefix + for prefix in ['form.test', 'form[test]', 'form(test)', 'form+test', 'form*test', 'form?test', 'form$test', 'form^test']: + with self.subTest(prefix=prefix): + data = { + f'{prefix}-TOTAL_FORMS': '2', + f'{prefix}-INITIAL_FORMS': '2', + f'{prefix}-MIN_NUM_FORMS': '0', + f'{prefix}-MAX_NUM_FORMS': '1000', + f'{prefix}-0-uuid': str(a.pk), + f'{prefix}-1-uuid': str(b.pk), + f'{prefix}-0-load': '9.0', + f'{prefix}-0-speed': '9.0', + f'{prefix}-1-load': '5.0', + f'{prefix}-1-speed': '5.0', + '_save': 'Save', + } + request = self.factory.post(changelist_url, data=data) + pks = m._get_edited_object_pks(request, prefix=prefix) + self.assertEqual(sorted(pks), sorted([str(a.pk), str(b.pk)])) + def test_get_list_editable_queryset(self): a = Swallow.objects.create(origin='Swallow A', load=4, speed=1) Swallow.objects.create(origin='Swallow B', load=2, speed=2)