Fixed #26060 -- Fixed crash with reverse OneToOneField in ModelAdmin.readonly_fields.
This commit is contained in:
parent
fb4272f0e6
commit
9a33d3d764
|
@ -383,7 +383,7 @@ def help_text_for_field(name, model):
|
||||||
def display_for_field(value, field, empty_value_display):
|
def display_for_field(value, field, empty_value_display):
|
||||||
from django.contrib.admin.templatetags.admin_list import _boolean_icon
|
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)
|
return dict(field.flatchoices).get(value, empty_value_display)
|
||||||
# NullBooleanField needs special-case null-handling, so it comes
|
# NullBooleanField needs special-case null-handling, so it comes
|
||||||
# before the general null test.
|
# before the general null test.
|
||||||
|
|
|
@ -26,3 +26,6 @@ Bugfixes
|
||||||
|
|
||||||
* Fixed a crash when using an ``__in`` lookup inside a ``Case`` expression
|
* Fixed a crash when using an ``__in`` lookup inside a ``Case`` expression
|
||||||
(:ticket:`26071`).
|
(:ticket:`26071`).
|
||||||
|
|
||||||
|
* Fixed a crash when using a reverse ``OneToOneField`` in
|
||||||
|
``ModelAdmin.readonly_fields`` (:ticket:`26060`).
|
||||||
|
|
|
@ -49,3 +49,6 @@ Bugfixes
|
||||||
SQLite with more than 2000 parameters when :setting:`DEBUG` is ``True`` on
|
SQLite with more than 2000 parameters when :setting:`DEBUG` is ``True`` on
|
||||||
distributions that increase the ``SQLITE_MAX_VARIABLE_NUMBER`` compile-time
|
distributions that increase the ``SQLITE_MAX_VARIABLE_NUMBER`` compile-time
|
||||||
limit to over 2000, such as Debian (:ticket:`26063`).
|
limit to over 2000, such as Debian (:ticket:`26063`).
|
||||||
|
|
||||||
|
* Fixed a crash when using a reverse ``OneToOneField`` in
|
||||||
|
``ModelAdmin.readonly_fields`` (:ticket:`26060`).
|
||||||
|
|
|
@ -35,15 +35,16 @@ from .models import (
|
||||||
InlineReference, InlineReferer, Inquisition, Language, Link,
|
InlineReference, InlineReferer, Inquisition, Language, Link,
|
||||||
MainPrepopulated, ModelWithStringPrimaryKey, NotReferenced, OldSubscriber,
|
MainPrepopulated, ModelWithStringPrimaryKey, NotReferenced, OldSubscriber,
|
||||||
OtherStory, Paper, Parent, ParentWithDependentChildren, ParentWithUUIDPK,
|
OtherStory, Paper, Parent, ParentWithDependentChildren, ParentWithUUIDPK,
|
||||||
Person, Persona, Picture, Pizza, Plot, PlotDetails, PluggableSearchPerson,
|
Person, Persona, Picture, Pizza, Plot, PlotDetails, PlotProxy,
|
||||||
Podcast, Post, PrePopulatedPost, PrePopulatedPostLargeSlug,
|
PluggableSearchPerson, Podcast, Post, PrePopulatedPost,
|
||||||
PrePopulatedSubPost, Promo, Question, Recipe, Recommendation, Recommender,
|
PrePopulatedPostLargeSlug, PrePopulatedSubPost, Promo, Question, Recipe,
|
||||||
ReferencedByGenRel, ReferencedByInline, ReferencedByParent,
|
Recommendation, Recommender, ReferencedByGenRel, ReferencedByInline,
|
||||||
RelatedPrepopulated, RelatedWithUUIDPKModel, Report, Reservation,
|
ReferencedByParent, RelatedPrepopulated, RelatedWithUUIDPKModel, Report,
|
||||||
Restaurant, RowLevelChangePermissionModel, Section, ShortMessage, Simple,
|
Reservation, Restaurant, RowLevelChangePermissionModel, Section,
|
||||||
Sketch, State, Story, StumpJoke, Subscriber, SuperVillain, Telegram, Thing,
|
ShortMessage, Simple, Sketch, State, Story, StumpJoke, Subscriber,
|
||||||
Topping, UnchangeableObject, UndeletableObject, UnorderedObject,
|
SuperVillain, Telegram, Thing, Topping, UnchangeableObject,
|
||||||
UserMessenger, Villain, Vodcast, Whatsit, Widget, Worker, WorkHour,
|
UndeletableObject, UnorderedObject, UserMessenger, Villain, Vodcast,
|
||||||
|
Whatsit, Widget, Worker, WorkHour,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -866,6 +867,10 @@ class InlineRefererAdmin(admin.ModelAdmin):
|
||||||
inlines = [InlineReferenceInline]
|
inlines = [InlineReferenceInline]
|
||||||
|
|
||||||
|
|
||||||
|
class PlotReadonlyAdmin(admin.ModelAdmin):
|
||||||
|
readonly_fields = ('plotdetails',)
|
||||||
|
|
||||||
|
|
||||||
class GetFormsetsArgumentCheckingAdmin(admin.ModelAdmin):
|
class GetFormsetsArgumentCheckingAdmin(admin.ModelAdmin):
|
||||||
fields = ['name']
|
fields = ['name']
|
||||||
|
|
||||||
|
@ -920,6 +925,7 @@ site.register(Villain)
|
||||||
site.register(SuperVillain)
|
site.register(SuperVillain)
|
||||||
site.register(Plot)
|
site.register(Plot)
|
||||||
site.register(PlotDetails)
|
site.register(PlotDetails)
|
||||||
|
site.register(PlotProxy, PlotReadonlyAdmin)
|
||||||
site.register(Bookmark)
|
site.register(Bookmark)
|
||||||
site.register(CyclicOne)
|
site.register(CyclicOne)
|
||||||
site.register(CyclicTwo)
|
site.register(CyclicTwo)
|
||||||
|
|
|
@ -538,12 +538,17 @@ class Plot(models.Model):
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class PlotDetails(models.Model):
|
class PlotDetails(models.Model):
|
||||||
details = models.CharField(max_length=100)
|
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):
|
def __str__(self):
|
||||||
return self.details
|
return self.details
|
||||||
|
|
||||||
|
|
||||||
|
class PlotProxy(Plot):
|
||||||
|
class Meta:
|
||||||
|
proxy = True
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class SecretHideout(models.Model):
|
class SecretHideout(models.Model):
|
||||||
""" Secret! Not registered with the admin! """
|
""" Secret! Not registered with the admin! """
|
||||||
|
|
|
@ -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."
|
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'],
|
@override_settings(PASSWORD_HASHERS=['django.contrib.auth.hashers.SHA1PasswordHasher'],
|
||||||
ROOT_URLCONF="admin_views.urls",
|
ROOT_URLCONF="admin_views.urls",
|
||||||
USE_I18N=True, USE_L10N=False, LANGUAGE_CODE='en')
|
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'],
|
@override_settings(PASSWORD_HASHERS=['django.contrib.auth.hashers.SHA1PasswordHasher'],
|
||||||
ROOT_URLCONF="admin_views.urls")
|
ROOT_URLCONF="admin_views.urls")
|
||||||
class ReadonlyTest(TestCase):
|
class ReadonlyTest(AdminFieldExtractionMixin, TestCase):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
|
@ -4677,7 +4714,7 @@ class ReadonlyTest(TestCase):
|
||||||
self.assertContains(response, '<p>No opinion</p>', html=True)
|
self.assertContains(response, '<p>No opinion</p>', html=True)
|
||||||
self.assertNotContains(response, '<p>(None)</p>')
|
self.assertNotContains(response, '<p>(None)</p>')
|
||||||
|
|
||||||
def test_readonly_backwards_ref(self):
|
def test_readonly_manytomany_backwards_ref(self):
|
||||||
"""
|
"""
|
||||||
Regression test for #16433 - backwards references for related objects
|
Regression test for #16433 - backwards references for related objects
|
||||||
broke if the related field is read-only due to the help_text attribute
|
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'))
|
response = self.client.get(reverse('admin:admin_views_topping_add'))
|
||||||
self.assertEqual(response.status_code, 200)
|
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
|
@ignore_warnings(category=RemovedInDjango20Warning) # for allow_tags deprecation
|
||||||
def test_readonly_field_overrides(self):
|
def test_readonly_field_overrides(self):
|
||||||
"""
|
"""
|
||||||
|
|
Loading…
Reference in New Issue