diff --git a/django/contrib/admin/utils.py b/django/contrib/admin/utils.py index a6e61122bd..029fe3857a 100644 --- a/django/contrib/admin/utils.py +++ b/django/contrib/admin/utils.py @@ -383,7 +383,7 @@ def help_text_for_field(name, model): def display_for_field(value, field, empty_value_display): from django.contrib.admin.templatetags.admin_list import _boolean_icon - if field.flatchoices: + if getattr(field, 'flatchoices', None): return dict(field.flatchoices).get(value, empty_value_display) # NullBooleanField needs special-case null-handling, so it comes # before the general null test. diff --git a/docs/releases/1.8.9.txt b/docs/releases/1.8.9.txt index 8c5445b549..86bb03d916 100644 --- a/docs/releases/1.8.9.txt +++ b/docs/releases/1.8.9.txt @@ -26,3 +26,6 @@ Bugfixes * Fixed a crash when using an ``__in`` lookup inside a ``Case`` expression (:ticket:`26071`). + +* Fixed a crash when using a reverse ``OneToOneField`` in + ``ModelAdmin.readonly_fields`` (:ticket:`26060`). diff --git a/docs/releases/1.9.2.txt b/docs/releases/1.9.2.txt index 0da9cf65cd..179cfafbb3 100644 --- a/docs/releases/1.9.2.txt +++ b/docs/releases/1.9.2.txt @@ -49,3 +49,6 @@ Bugfixes SQLite with more than 2000 parameters when :setting:`DEBUG` is ``True`` on distributions that increase the ``SQLITE_MAX_VARIABLE_NUMBER`` compile-time limit to over 2000, such as Debian (:ticket:`26063`). + +* Fixed a crash when using a reverse ``OneToOneField`` in + ``ModelAdmin.readonly_fields`` (:ticket:`26060`). diff --git a/tests/admin_views/admin.py b/tests/admin_views/admin.py index dbc77e2f7c..932a29f3ac 100644 --- a/tests/admin_views/admin.py +++ b/tests/admin_views/admin.py @@ -35,15 +35,16 @@ from .models import ( InlineReference, InlineReferer, Inquisition, Language, Link, MainPrepopulated, ModelWithStringPrimaryKey, NotReferenced, OldSubscriber, OtherStory, Paper, Parent, ParentWithDependentChildren, ParentWithUUIDPK, - Person, Persona, Picture, Pizza, Plot, PlotDetails, 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, + 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, ) @@ -866,6 +867,10 @@ class InlineRefererAdmin(admin.ModelAdmin): inlines = [InlineReferenceInline] +class PlotReadonlyAdmin(admin.ModelAdmin): + readonly_fields = ('plotdetails',) + + class GetFormsetsArgumentCheckingAdmin(admin.ModelAdmin): fields = ['name'] @@ -920,6 +925,7 @@ site.register(Villain) site.register(SuperVillain) site.register(Plot) site.register(PlotDetails) +site.register(PlotProxy, PlotReadonlyAdmin) site.register(Bookmark) site.register(CyclicOne) site.register(CyclicTwo) diff --git a/tests/admin_views/models.py b/tests/admin_views/models.py index 2c7a217c42..7cd5bdd8b9 100644 --- a/tests/admin_views/models.py +++ b/tests/admin_views/models.py @@ -538,12 +538,17 @@ class Plot(models.Model): @python_2_unicode_compatible class PlotDetails(models.Model): details = models.CharField(max_length=100) - plot = models.OneToOneField(Plot, models.CASCADE) + plot = models.OneToOneField(Plot, models.CASCADE, null=True, blank=True) def __str__(self): return self.details +class PlotProxy(Plot): + class Meta: + proxy = True + + @python_2_unicode_compatible class SecretHideout(models.Model): """ Secret! Not registered with the admin! """ diff --git a/tests/admin_views/tests.py b/tests/admin_views/tests.py index cb72c20975..ae4c68cbb0 100644 --- a/tests/admin_views/tests.py +++ b/tests/admin_views/tests.py @@ -67,6 +67,43 @@ ERROR_MESSAGE = "Please enter the correct username and password \ for a staff account. Note that both fields may be case-sensitive." +class AdminFieldExtractionMixin(object): + """ + Helper methods for extracting data from AdminForm. + """ + def get_admin_form_fields(self, response): + """ + Return a list of AdminFields for the AdminForm in the response. + """ + admin_form = response.context['adminform'] + fieldsets = list(admin_form) + + field_lines = [] + for fieldset in fieldsets: + field_lines += list(fieldset) + + fields = [] + for field_line in field_lines: + fields += list(field_line) + + return fields + + def get_admin_readonly_fields(self, response): + """ + Return the readonly fields for the response's AdminForm. + """ + return [f for f in self.get_admin_form_fields(response) if f.is_readonly] + + def get_admin_readonly_field(self, response, field_name): + """ + Return the readonly field for the given field_name. + """ + admin_readonly_fields = self.get_admin_readonly_fields(response) + for field in admin_readonly_fields: + if field.field['name'] == field_name: + return field + + @override_settings(PASSWORD_HASHERS=['django.contrib.auth.hashers.SHA1PasswordHasher'], ROOT_URLCONF="admin_views.urls", USE_I18N=True, USE_L10N=False, LANGUAGE_CODE='en') @@ -4556,7 +4593,7 @@ class SeleniumAdminViewsIETests(SeleniumAdminViewsFirefoxTests): @override_settings(PASSWORD_HASHERS=['django.contrib.auth.hashers.SHA1PasswordHasher'], ROOT_URLCONF="admin_views.urls") -class ReadonlyTest(TestCase): +class ReadonlyTest(AdminFieldExtractionMixin, TestCase): @classmethod def setUpTestData(cls): @@ -4677,7 +4714,7 @@ class ReadonlyTest(TestCase): self.assertContains(response, '
No opinion
', html=True) self.assertNotContains(response, '(None)
') - def test_readonly_backwards_ref(self): + def test_readonly_manytomany_backwards_ref(self): """ Regression test for #16433 - backwards references for related objects broke if the related field is read-only due to the help_text attribute @@ -4688,6 +4725,26 @@ class ReadonlyTest(TestCase): response = self.client.get(reverse('admin:admin_views_topping_add')) self.assertEqual(response.status_code, 200) + def test_readonly_onetoone_backwards_ref(self): + """ + Can reference a reverse OneToOneField in ModelAdmin.readonly_fields. + """ + v1 = Villain.objects.create(name='Adam') + pl = Plot.objects.create(name='Test Plot', team_leader=v1, contact=v1) + pd = PlotDetails.objects.create(details='Brand New Plot', plot=pl) + + response = self.client.get(reverse('admin:admin_views_plotproxy_change', args=(pl.pk,))) + field = self.get_admin_readonly_field(response, 'plotdetails') + self.assertEqual(field.contents(), 'Brand New Plot') + + # The reverse relation also works if the OneToOneField is null. + pd.plot = None + pd.save() + + response = self.client.get(reverse('admin:admin_views_plotproxy_change', args=(pl.pk,))) + field = self.get_admin_readonly_field(response, 'plotdetails') + self.assertEqual(field.contents(), '-') # default empty value + @ignore_warnings(category=RemovedInDjango20Warning) # for allow_tags deprecation def test_readonly_field_overrides(self): """