Skip to content

Commit

Permalink
Fixed #28462 -- Decreased memory usage with ModelAdmin.list_editable.
Browse files Browse the repository at this point in the history
  • Loading branch information
AdamDonna authored and timgraham committed Jun 1, 2018
1 parent f185d92 commit c0e803e
Show file tree
Hide file tree
Showing 6 changed files with 90 additions and 4 deletions.
1 change: 1 addition & 0 deletions AUTHORS
Expand Up @@ -11,6 +11,7 @@ answer newbie questions, and generally made Django that much better:
Abeer Upadhyay <ab.esquarer@gmail.com>
Abhishek Gautam <abhishekg1128@yahoo.com>
Adam Bogdał <adam@bogdal.pl>
Adam Donaghy
Adam Johnson <https://github.com/adamchainz>
Adam Malinowski <http://adammalinowski.co.uk>
Adam Vandenberg
Expand Down
25 changes: 24 additions & 1 deletion django/contrib/admin/options.py
@@ -1,6 +1,7 @@
import copy
import json
import operator
import re
from collections import OrderedDict
from functools import partial, reduce, update_wrapper
from urllib.parse import quote as urlquote
Expand Down Expand Up @@ -1633,6 +1634,27 @@ def add_view(self, request, form_url='', extra_context=None):
def change_view(self, request, object_id, form_url='', extra_context=None):
return self.changeform_view(request, object_id, form_url, extra_context)

def _get_edited_object_pks(self, request, prefix):
"""Return POST data values of list_editable primary keys."""
pk_pattern = re.compile('{}-\d+-{}$'.format(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):
"""
Based on POST data, return a queryset of the objects that were edited
via list_editable.
"""
object_pks = self._get_edited_object_pks(request, prefix)
queryset = self.get_queryset(request)
validate = queryset.model._meta.pk.to_python
try:
for pk in object_pks:
validate(pk)
except ValidationError:
# Disable the optimization if the POST data was tampered with.
return queryset
return queryset.filter(pk__in=object_pks)

@csrf_protect_m
def changelist_view(self, request, extra_context=None):
"""
Expand Down Expand Up @@ -1713,7 +1735,8 @@ def changelist_view(self, request, extra_context=None):
if not self.has_change_permission(request):
raise PermissionDenied
FormSet = self.get_changelist_formset(request)
formset = cl.formset = FormSet(request.POST, request.FILES, queryset=self.get_queryset(request))
modified_objects = self._get_list_editable_queryset(request, FormSet.get_default_prefix())
formset = cl.formset = FormSet(request.POST, request.FILES, queryset=modified_objects)
if formset.is_valid():
changecount = 0
for form in formset.forms:
Expand Down
3 changes: 3 additions & 0 deletions docs/releases/1.11.14.txt
Expand Up @@ -11,3 +11,6 @@ Bugfixes

* Fixed ``WKBWriter.write()`` and ``write_hex()`` for empty polygons on
GEOS 3.6.1+ (:ticket:`29460`).

* Fixed a regression in Django 1.10 that could result in large memory usage
when making edits using ``ModelAdmin.list_editable`` (:ticket:`28462`).
3 changes: 3 additions & 0 deletions docs/releases/2.0.6.txt
Expand Up @@ -20,3 +20,6 @@ Bugfixes

* Fixed ``WKBWriter.write()`` and ``write_hex()`` for empty polygons on
GEOS 3.6.1+ (:ticket:`29460`).

* Fixed a regression in Django 1.10 that could result in large memory usage
when making edits using ``ModelAdmin.list_editable`` (:ticket:`28462`).
3 changes: 3 additions & 0 deletions tests/admin_changelist/models.py
@@ -1,3 +1,5 @@
import uuid

from django.db import models


Expand Down Expand Up @@ -73,6 +75,7 @@ class Invitation(models.Model):


class Swallow(models.Model):
uuid = models.UUIDField(primary_key=True, default=uuid.uuid4)
origin = models.CharField(max_length=255)
load = models.FloatField()
speed = models.FloatField()
Expand Down
59 changes: 56 additions & 3 deletions tests/admin_changelist/tests.py
Expand Up @@ -732,9 +732,9 @@ def test_multiuser_edit(self):
'form-INITIAL_FORMS': '3',
'form-MIN_NUM_FORMS': '0',
'form-MAX_NUM_FORMS': '1000',
'form-0-id': str(d.pk),
'form-1-id': str(c.pk),
'form-2-id': str(a.pk),
'form-0-uuid': str(d.pk),
'form-1-uuid': str(c.pk),
'form-2-uuid': str(a.pk),
'form-0-load': '9.0',
'form-0-speed': '9.0',
'form-1-load': '5.0',
Expand Down Expand Up @@ -764,6 +764,59 @@ def test_multiuser_edit(self):
# No new swallows were created.
self.assertEqual(len(Swallow.objects.all()), 4)

def test_get_edited_object_ids(self):
a = Swallow.objects.create(origin='Swallow A', load=4, speed=1)
b = Swallow.objects.create(origin='Swallow B', load=2, speed=2)
c = Swallow.objects.create(origin='Swallow C', load=5, speed=5)
superuser = self._create_superuser('superuser')
self.client.force_login(superuser)
changelist_url = reverse('admin:admin_changelist_swallow_changelist')
m = SwallowAdmin(Swallow, custom_site)
data = {
'form-TOTAL_FORMS': '3',
'form-INITIAL_FORMS': '3',
'form-MIN_NUM_FORMS': '0',
'form-MAX_NUM_FORMS': '1000',
'form-0-uuid': str(a.pk),
'form-1-uuid': str(b.pk),
'form-2-uuid': str(c.pk),
'form-0-load': '9.0',
'form-0-speed': '9.0',
'form-1-load': '5.0',
'form-1-speed': '5.0',
'form-2-load': '5.0',
'form-2-speed': '4.0',
'_save': 'Save',
}
request = self.factory.post(changelist_url, data=data)
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_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)
data = {
'form-TOTAL_FORMS': '2',
'form-INITIAL_FORMS': '2',
'form-MIN_NUM_FORMS': '0',
'form-MAX_NUM_FORMS': '1000',
'form-0-uuid': str(a.pk),
'form-0-load': 10,
'_save': 'Save',
}
superuser = self._create_superuser('superuser')
self.client.force_login(superuser)
changelist_url = reverse('admin:admin_changelist_swallow_changelist')
m = SwallowAdmin(Swallow, custom_site)
request = self.factory.post(changelist_url, data=data)
queryset = m._get_list_editable_queryset(request, prefix='form')
self.assertEqual(queryset.count(), 1)
data['form-0-uuid'] = 'INVALD_PRIMARY_KEY'
# The unfiltered queryset is returned if there's invalid data.
request = self.factory.post(changelist_url, data=data)
queryset = m._get_list_editable_queryset(request, prefix='form')
self.assertEqual(queryset.count(), 2)

def test_deterministic_order_for_unordered_model(self):
"""
The primary key is used in the ordering of the changelist's results to
Expand Down

0 comments on commit c0e803e

Please sign in to comment.