summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--AUTHORS1
-rw-r--r--django/forms/models.py5
-rw-r--r--docs/releases/1.11.5.txt5
-rw-r--r--tests/admin_views/admin.py17
-rw-r--r--tests/admin_views/models.py7
-rw-r--r--tests/admin_views/tests.py24
-rw-r--r--tests/model_forms/tests.py15
7 files changed, 60 insertions, 14 deletions
diff --git a/AUTHORS b/AUTHORS
index cf4a559427..d1280eb6c9 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -459,6 +459,7 @@ answer newbie questions, and generally made Django that much better:
Lex Berezhny <lex@damoti.com>
Liang Feng <hutuworm@gmail.com>
limodou
+ Lincoln Smith <lincoln.smith@anu.edu.au>
Loek van Gent <loek@barakken.nl>
Loïc Bistuer <loic.bistuer@sixmedia.com>
Lowe Thiderman <lowe.thiderman@gmail.com>
diff --git a/django/forms/models.py b/django/forms/models.py
index db710a2ed2..0e80e19042 100644
--- a/django/forms/models.py
+++ b/django/forms/models.py
@@ -84,6 +84,7 @@ def model_to_dict(instance, fields=None, exclude=None):
fields will be excluded from the returned dict, even if they are listed in
the ``fields`` argument.
"""
+ from django.db import models
opts = instance._meta
data = {}
for f in chain(opts.concrete_fields, opts.private_fields, opts.many_to_many):
@@ -94,6 +95,10 @@ def model_to_dict(instance, fields=None, exclude=None):
if exclude and f.name in exclude:
continue
data[f.name] = f.value_from_object(instance)
+ # Evaluate ManyToManyField QuerySets to prevent subsequent model
+ # alteration of that field from being reflected in the data.
+ if isinstance(f, models.ManyToManyField):
+ data[f.name] = list(data[f.name])
return data
diff --git a/docs/releases/1.11.5.txt b/docs/releases/1.11.5.txt
index 91620eb740..cb9e466248 100644
--- a/docs/releases/1.11.5.txt
+++ b/docs/releases/1.11.5.txt
@@ -35,3 +35,8 @@ Bugfixes
* Fixed a regression in 1.11.4 where ``runserver`` crashed with non-Unicode
system encodings on Python 2 + Windows (:ticket:`28487`).
+
+* Fixed a regression in Django 1.10 where changes to a ``ManyToManyField``
+ weren't logged in the admin change history (:ticket:`27998`) and prevented
+ ``ManyToManyField`` initial data in model forms from being affected by
+ subsequent model changes (:ticket:`28543`).
diff --git a/tests/admin_views/admin.py b/tests/admin_views/admin.py
index b5e0ff4814..5d02f0f37d 100644
--- a/tests/admin_views/admin.py
+++ b/tests/admin_views/admin.py
@@ -38,14 +38,14 @@ from .models import (
OtherStory, Paper, Parent, ParentWithDependentChildren, ParentWithUUIDPK,
Person, Persona, Picture, Pizza, Plot, PlotDetails, PlotProxy,
PluggableSearchPerson, Podcast, Post, PrePopulatedPost,
- PrePopulatedPostLargeSlug, PrePopulatedSubPost, Promo, Question, Recipe,
- Recommendation, Recommender, ReferencedByGenRel, ReferencedByInline,
- ReferencedByParent, RelatedPrepopulated, RelatedWithUUIDPKModel, Report,
- Reservation, Restaurant, RowLevelChangePermissionModel, Section,
- ShortMessage, Simple, Sketch, State, Story, StumpJoke, Subscriber,
- SuperVillain, Telegram, Thing, Topping, UnchangeableObject,
- UndeletableObject, UnorderedObject, UserMessenger, Villain, Vodcast,
- Whatsit, Widget, Worker, WorkHour,
+ PrePopulatedPostLargeSlug, PrePopulatedSubPost, Promo, Question,
+ ReadablePizza, Recipe, Recommendation, Recommender, ReferencedByGenRel,
+ ReferencedByInline, ReferencedByParent, RelatedPrepopulated,
+ RelatedWithUUIDPKModel, Report, Reservation, Restaurant,
+ RowLevelChangePermissionModel, Section, ShortMessage, Simple, Sketch,
+ State, Story, StumpJoke, Subscriber, SuperVillain, Telegram, Thing,
+ Topping, UnchangeableObject, UndeletableObject, UnorderedObject,
+ UserMessenger, Villain, Vodcast, Whatsit, Widget, Worker, WorkHour,
)
@@ -988,6 +988,7 @@ site.register(Book, inlines=[ChapterInline])
site.register(Promo)
site.register(ChapterXtra1, ChapterXtra1Admin)
site.register(Pizza, PizzaAdmin)
+site.register(ReadablePizza)
site.register(Topping, ToppingAdmin)
site.register(Album, AlbumAdmin)
site.register(Question)
diff --git a/tests/admin_views/models.py b/tests/admin_views/models.py
index 9c7eee7547..29d96474c6 100644
--- a/tests/admin_views/models.py
+++ b/tests/admin_views/models.py
@@ -611,6 +611,13 @@ class Pizza(models.Model):
toppings = models.ManyToManyField('Topping', related_name='pizzas')
+# Pizza's ModelAdmin has readonly_fields = ['toppings'].
+# toppings is editable for this model's admin.
+class ReadablePizza(Pizza):
+ class Meta:
+ proxy = True
+
+
class Album(models.Model):
owner = models.ForeignKey(User, models.SET_NULL, null=True, blank=True)
title = models.CharField(max_length=30)
diff --git a/tests/admin_views/tests.py b/tests/admin_views/tests.py
index febb20914c..efddc49de7 100644
--- a/tests/admin_views/tests.py
+++ b/tests/admin_views/tests.py
@@ -57,12 +57,13 @@ from .models import (
MainPrepopulated, Media, ModelWithStringPrimaryKey, OtherStory, Paper,
Parent, ParentWithDependentChildren, ParentWithUUIDPK, Person, Persona,
Picture, Pizza, Plot, PlotDetails, PluggableSearchPerson, Podcast, Post,
- PrePopulatedPost, Promo, Question, Recommendation, Recommender,
- RelatedPrepopulated, RelatedWithUUIDPKModel, Report, Restaurant,
- RowLevelChangePermissionModel, SecretHideout, Section, ShortMessage,
- Simple, State, Story, Subscriber, SuperSecretHideout, SuperVillain,
- Telegram, TitleTranslation, Topping, UnchangeableObject, UndeletableObject,
- UnorderedObject, Villain, Vodcast, Whatsit, Widget, Worker, WorkHour,
+ PrePopulatedPost, Promo, Question, ReadablePizza, Recommendation,
+ Recommender, RelatedPrepopulated, RelatedWithUUIDPKModel, Report,
+ Restaurant, RowLevelChangePermissionModel, SecretHideout, Section,
+ ShortMessage, Simple, State, Story, Subscriber, SuperSecretHideout,
+ SuperVillain, Telegram, TitleTranslation, Topping, UnchangeableObject,
+ UndeletableObject, UnorderedObject, Villain, Vodcast, Whatsit, Widget,
+ Worker, WorkHour,
)
@@ -879,6 +880,17 @@ class AdminViewBasicTest(AdminViewBasicTestCase):
response = self.client.get(reverse('admin:admin_views_undeletableobject_change', args=(instance.pk,)))
self.assertNotContains(response, 'deletelink')
+ def test_change_view_logs_m2m_field_changes(self):
+ """Changes to ManyToManyFields are included in the object's history."""
+ pizza = ReadablePizza.objects.create(name='Cheese')
+ cheese = Topping.objects.create(name='cheese')
+ post_data = {'name': pizza.name, 'toppings': [cheese.pk]}
+ response = self.client.post(reverse('admin:admin_views_readablepizza_change', args=(pizza.pk,)), post_data)
+ self.assertRedirects(response, reverse('admin:admin_views_readablepizza_changelist'))
+ pizza_ctype = ContentType.objects.get_for_model(ReadablePizza, for_concrete_model=False)
+ log = LogEntry.objects.filter(content_type=pizza_ctype, object_id=pizza.pk).first()
+ self.assertEqual(log.get_change_message(), 'Changed toppings.')
+
def test_allows_attributeerror_to_bubble_up(self):
"""
AttributeErrors are allowed to bubble when raised inside a change list
diff --git a/tests/model_forms/tests.py b/tests/model_forms/tests.py
index f3317f59de..b14e0e2389 100644
--- a/tests/model_forms/tests.py
+++ b/tests/model_forms/tests.py
@@ -3142,3 +3142,18 @@ class StrictAssignmentTests(TestCase):
'__all__': ['Cannot set attribute'],
'title': ['This field cannot be blank.']
})
+
+
+class ModelToDictTests(TestCase):
+ def test_many_to_many(self):
+ """Data for a ManyToManyField is a list rather than a lazy QuerySet."""
+ blue = Colour.objects.create(name='blue')
+ red = Colour.objects.create(name='red')
+ item = ColourfulItem.objects.create()
+ item.colours.set([blue])
+ data = model_to_dict(item)['colours']
+ self.assertEqual(data, [blue])
+ item.colours.set([red])
+ # If data were a QuerySet, it would be reevaluated here and give "red"
+ # instead of the original value.
+ self.assertEqual(data, [blue])