Kaydet (Commit) b18650a2 authored tarafından Adam Donaghy's avatar Adam Donaghy Kaydeden (comit) Tim Graham

Fixed #28462 -- Decreased memory usage with ModelAdmin.list_editable.

Regression in 917cc288.
üst e1ebd225
...@@ -11,6 +11,7 @@ answer newbie questions, and generally made Django that much better: ...@@ -11,6 +11,7 @@ answer newbie questions, and generally made Django that much better:
Abeer Upadhyay <ab.esquarer@gmail.com> Abeer Upadhyay <ab.esquarer@gmail.com>
Abhishek Gautam <abhishekg1128@yahoo.com> Abhishek Gautam <abhishekg1128@yahoo.com>
Adam Bogdał <adam@bogdal.pl> Adam Bogdał <adam@bogdal.pl>
Adam Donaghy
Adam Johnson <https://github.com/adamchainz> Adam Johnson <https://github.com/adamchainz>
Adam Malinowski <http://adammalinowski.co.uk> Adam Malinowski <http://adammalinowski.co.uk>
Adam Vandenberg Adam Vandenberg
......
import copy import copy
import json import json
import operator import operator
import re
from collections import OrderedDict from collections import OrderedDict
from functools import partial, reduce, update_wrapper from functools import partial, reduce, update_wrapper
from urllib.parse import quote as urlquote from urllib.parse import quote as urlquote
...@@ -1633,6 +1634,27 @@ class ModelAdmin(BaseModelAdmin): ...@@ -1633,6 +1634,27 @@ class ModelAdmin(BaseModelAdmin):
def change_view(self, request, object_id, 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) 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 @csrf_protect_m
def changelist_view(self, request, extra_context=None): def changelist_view(self, request, extra_context=None):
""" """
...@@ -1713,7 +1735,8 @@ class ModelAdmin(BaseModelAdmin): ...@@ -1713,7 +1735,8 @@ class ModelAdmin(BaseModelAdmin):
if not self.has_change_permission(request): if not self.has_change_permission(request):
raise PermissionDenied raise PermissionDenied
FormSet = self.get_changelist_formset(request) 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(): if formset.is_valid():
changecount = 0 changecount = 0
for form in formset.forms: for form in formset.forms:
......
...@@ -11,3 +11,6 @@ Bugfixes ...@@ -11,3 +11,6 @@ Bugfixes
* Fixed ``WKBWriter.write()`` and ``write_hex()`` for empty polygons on * Fixed ``WKBWriter.write()`` and ``write_hex()`` for empty polygons on
GEOS 3.6.1+ (:ticket:`29460`). 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`).
...@@ -20,3 +20,6 @@ Bugfixes ...@@ -20,3 +20,6 @@ Bugfixes
* Fixed ``WKBWriter.write()`` and ``write_hex()`` for empty polygons on * Fixed ``WKBWriter.write()`` and ``write_hex()`` for empty polygons on
GEOS 3.6.1+ (:ticket:`29460`). 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`).
import uuid
from django.db import models from django.db import models
...@@ -73,6 +75,7 @@ class Invitation(models.Model): ...@@ -73,6 +75,7 @@ class Invitation(models.Model):
class Swallow(models.Model): class Swallow(models.Model):
uuid = models.UUIDField(primary_key=True, default=uuid.uuid4)
origin = models.CharField(max_length=255) origin = models.CharField(max_length=255)
load = models.FloatField() load = models.FloatField()
speed = models.FloatField() speed = models.FloatField()
......
...@@ -8,6 +8,7 @@ from django.contrib.admin.tests import AdminSeleniumTestCase ...@@ -8,6 +8,7 @@ from django.contrib.admin.tests import AdminSeleniumTestCase
from django.contrib.admin.views.main import ALL_VAR, SEARCH_VAR from django.contrib.admin.views.main import ALL_VAR, SEARCH_VAR
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db import connection
from django.db.models import F from django.db.models import F
from django.db.models.fields import Field, IntegerField from django.db.models.fields import Field, IntegerField
from django.db.models.functions import Upper from django.db.models.functions import Upper
...@@ -15,6 +16,7 @@ from django.db.models.lookups import Contains, Exact ...@@ -15,6 +16,7 @@ from django.db.models.lookups import Contains, Exact
from django.template import Context, Template, TemplateSyntaxError from django.template import Context, Template, TemplateSyntaxError
from django.test import TestCase, override_settings from django.test import TestCase, override_settings
from django.test.client import RequestFactory from django.test.client import RequestFactory
from django.test.utils import CaptureQueriesContext
from django.urls import reverse from django.urls import reverse
from django.utils import formats from django.utils import formats
...@@ -732,9 +734,9 @@ class ChangeListTests(TestCase): ...@@ -732,9 +734,9 @@ class ChangeListTests(TestCase):
'form-INITIAL_FORMS': '3', 'form-INITIAL_FORMS': '3',
'form-MIN_NUM_FORMS': '0', 'form-MIN_NUM_FORMS': '0',
'form-MAX_NUM_FORMS': '1000', 'form-MAX_NUM_FORMS': '1000',
'form-0-id': str(d.pk), 'form-0-uuid': str(d.pk),
'form-1-id': str(c.pk), 'form-1-uuid': str(c.pk),
'form-2-id': str(a.pk), 'form-2-uuid': str(a.pk),
'form-0-load': '9.0', 'form-0-load': '9.0',
'form-0-speed': '9.0', 'form-0-speed': '9.0',
'form-1-load': '5.0', 'form-1-load': '5.0',
...@@ -764,6 +766,83 @@ class ChangeListTests(TestCase): ...@@ -764,6 +766,83 @@ class ChangeListTests(TestCase):
# No new swallows were created. # No new swallows were created.
self.assertEqual(len(Swallow.objects.all()), 4) 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_changelist_view_list_editable_changed_objects_uses_filter(self):
"""list_editable edits use a filtered queryset to limit memory usage."""
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')
with CaptureQueriesContext(connection) as context:
response = self.client.post(changelist_url, data=data)
self.assertEqual(response.status_code, 200)
self.assertIn('WHERE', context.captured_queries[4]['sql'])
self.assertIn('IN', context.captured_queries[4]['sql'])
# Check only the first few characters since the UUID may have dashes.
self.assertIn(str(a.pk)[:8], context.captured_queries[4]['sql'])
def test_deterministic_order_for_unordered_model(self): def test_deterministic_order_for_unordered_model(self):
""" """
The primary key is used in the ordering of the changelist's results to The primary key is used in the ordering of the changelist's results to
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment