mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +00:00 
			
		
		
		
	Fixed #27998 -- Made ManyToManyField changes logged in admin's object history.
This commit is contained in:
		
				
					committed by
					
						 Tim Graham
						Tim Graham
					
				
			
			
				
	
			
			
			
						parent
						
							451b585c2f
						
					
				
				
					commit
					15b465c584
				
			
							
								
								
									
										1
									
								
								AUTHORS
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								AUTHORS
									
									
									
									
									
								
							| @@ -468,6 +468,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> | ||||
|   | ||||
| @@ -1442,6 +1442,10 @@ class ModelAdmin(BaseModelAdmin): | ||||
|                 new_object = form.instance | ||||
|             formsets, inline_instances = self._create_formsets(request, new_object, change=not add) | ||||
|             if all_valid(formsets) and form_validated: | ||||
|                 if not add: | ||||
|                     # Evalute querysets in form.initial so that changes to | ||||
|                     # ManyToManyFields are reflected in this change's LogEntry. | ||||
|                     form.has_changed() | ||||
|                 self.save_model(request, new_object, form, not add) | ||||
|                 self.save_related(request, form, formsets, not add) | ||||
|                 change_message = self.construct_change_message(request, form, formsets, add) | ||||
|   | ||||
| @@ -35,14 +35,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, | ||||
| ) | ||||
|  | ||||
|  | ||||
| @@ -970,6 +970,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) | ||||
|   | ||||
| @@ -575,6 +575,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) | ||||
|   | ||||
| @@ -54,12 +54,13 @@ from .models import ( | ||||
|     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, | ||||
| ) | ||||
|  | ||||
|  | ||||
| @@ -876,6 +877,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 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user