diff --git a/django/contrib/admin/templates/admin/change_list_results.html b/django/contrib/admin/templates/admin/change_list_results.html index 3e38978f5d..29cf265804 100644 --- a/django/contrib/admin/templates/admin/change_list_results.html +++ b/django/contrib/admin/templates/admin/change_list_results.html @@ -16,7 +16,10 @@ {% for result in results %} -{% for item in result %}{{ item }}{% endfor %} +{% if result.form.non_field_errors %} + {{ result.form.non_field_errors }} +{% endif %} +{% for item in result.row %}{{ item }}{% endfor %} {% endfor %} diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py index 33f01753c4..199b1d97a3 100644 --- a/django/contrib/admin/templatetags/admin_list.py +++ b/django/contrib/admin/templatetags/admin_list.py @@ -198,10 +198,10 @@ def items_for_result(cl, result, form): def results(cl): if cl.formset: for res, form in zip(cl.result_list, cl.formset.forms): - yield list(items_for_result(cl, res, form)) + yield {'row': list(items_for_result(cl, res, form)), 'form': form} else: for res in cl.result_list: - yield list(items_for_result(cl, res, None)) + yield {'row': list(items_for_result(cl, res, None)), 'form': None} def result_hidden_fields(cl): if cl.formset: diff --git a/tests/regressiontests/admin_views/models.py b/tests/regressiontests/admin_views/models.py index 60319eaf7d..b601267e57 100644 --- a/tests/regressiontests/admin_views/models.py +++ b/tests/regressiontests/admin_views/models.py @@ -669,7 +669,32 @@ class Answer(models.Model): class Reservation(models.Model): start_date = models.DateTimeField() price = models.IntegerField() - + + +DRIVER_CHOICES = ( + (u'bill', 'Bill G'), + (u'steve', 'Steve J'), +) + +RESTAURANT_CHOICES = ( + (u'indian', u'A Taste of India'), + (u'thai', u'Thai Pography'), + (u'pizza', u'Pizza Mama'), +) + +class FoodDelivery(models.Model): + reference = models.CharField(max_length=100) + driver = models.CharField(max_length=100, choices=DRIVER_CHOICES, blank=True) + restaurant = models.CharField(max_length=100, choices=RESTAURANT_CHOICES, blank=True) + + class Meta: + unique_together = (("driver", "restaurant"),) + +class FoodDeliveryAdmin(admin.ModelAdmin): + list_display=('reference', 'driver', 'restaurant') + list_editable = ('driver', 'restaurant') + + admin.site.register(Article, ArticleAdmin) admin.site.register(CustomArticle, CustomArticleAdmin) admin.site.register(Section, save_as=True, inlines=[ArticleInline]) @@ -706,6 +731,7 @@ admin.site.register(CyclicOne) admin.site.register(CyclicTwo) admin.site.register(WorkHour, WorkHourAdmin) admin.site.register(Reservation) +admin.site.register(FoodDelivery, FoodDeliveryAdmin) # We intentionally register Promo and ChapterXtra1 but not Chapter nor ChapterXtra2. # That way we cover all four cases: diff --git a/tests/regressiontests/admin_views/tests.py b/tests/regressiontests/admin_views/tests.py index 222c1015f3..2685a05caa 100644 --- a/tests/regressiontests/admin_views/tests.py +++ b/tests/regressiontests/admin_views/tests.py @@ -35,7 +35,7 @@ from models import (Article, BarAccount, CustomArticle, EmptyModel, Person, Persona, Picture, Podcast, Section, Subscriber, Vodcast, Language, Collector, Widget, Grommet, DooHickey, FancyDoodad, Whatsit, Category, Post, Plot, FunkyTag, Chapter, Book, Promo, WorkHour, Employee, - Question, Answer, Inquisition, Actor) + Question, Answer, Inquisition, Actor, FoodDelivery) class AdminViewBasicTest(TestCase): @@ -1420,6 +1420,67 @@ class AdminViewListEditable(TestCase): self.assertEqual(Person.objects.get(name="John Mauchly").alive, False) + def test_non_field_errors(self): + ''' Ensure that non field errors are displayed for each of the + forms in the changelist's formset. Refs #13126. + ''' + FoodDelivery.objects.create(reference='123', driver='bill', restaurant='thai') + FoodDelivery.objects.create(reference='456', driver='bill', restaurant='india') + FoodDelivery.objects.create(reference='789', driver='bill', restaurant='pizza') + + data = { + "form-TOTAL_FORMS": "3", + "form-INITIAL_FORMS": "3", + "form-MAX_NUM_FORMS": "0", + + "form-0-id": "1", + "form-0-reference": "123", + "form-0-driver": "bill", + "form-0-restaurant": "thai", + + # Same data as above: Forbidden because of unique_together! + "form-1-id": "2", + "form-1-reference": "456", + "form-1-driver": "bill", + "form-1-restaurant": "thai", + + "form-2-id": "3", + "form-2-reference": "789", + "form-2-driver": "bill", + "form-2-restaurant": "pizza", + + "_save": "Save", + } + response = self.client.post('/test_admin/admin/admin_views/fooddelivery/', data) + self.assertContains(response, '', 1) + + data = { + "form-TOTAL_FORMS": "3", + "form-INITIAL_FORMS": "3", + "form-MAX_NUM_FORMS": "0", + + "form-0-id": "1", + "form-0-reference": "123", + "form-0-driver": "bill", + "form-0-restaurant": "thai", + + # Same data as above: Forbidden because of unique_together! + "form-1-id": "2", + "form-1-reference": "456", + "form-1-driver": "bill", + "form-1-restaurant": "thai", + + # Same data also. + "form-2-id": "3", + "form-2-reference": "789", + "form-2-driver": "bill", + "form-2-restaurant": "thai", + + "_save": "Save", + } + response = self.client.post('/test_admin/admin/admin_views/fooddelivery/', data) + self.assertContains(response, '', 2) + def test_non_form_errors(self): # test if non-form errors are handled; ticket #12716 data = {