[1.8.x] Fixed #26060 -- Fixed crash with reverse OneToOneField in ModelAdmin.readonly_fields.
Backport of 9a33d3d764
from master
This commit is contained in:
parent
5b3c66d8b6
commit
8502e9f049
|
@ -203,7 +203,7 @@ class AdminReadonlyField(object):
|
|||
else:
|
||||
result_repr = linebreaksbr(result_repr)
|
||||
else:
|
||||
if isinstance(f.rel, ManyToManyRel) and value is not None:
|
||||
if isinstance(getattr(f, 'rel', None), ManyToManyRel) and value is not None:
|
||||
result_repr = ", ".join(map(six.text_type, value.all()))
|
||||
else:
|
||||
result_repr = display_for_field(value, f)
|
||||
|
|
|
@ -376,7 +376,7 @@ def display_for_field(value, field):
|
|||
from django.contrib.admin.templatetags.admin_list import _boolean_icon
|
||||
from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
|
||||
|
||||
if field.flatchoices:
|
||||
if getattr(field, 'flatchoices', None):
|
||||
return dict(field.flatchoices).get(value, EMPTY_CHANGELIST_VALUE)
|
||||
# NullBooleanField needs special-case null-handling, so it comes
|
||||
# before the general null test.
|
||||
|
|
|
@ -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`).
|
||||
|
|
|
@ -34,15 +34,15 @@ from .models import (
|
|||
InlineReference, InlineReferer, Inquisition, Language, Link,
|
||||
MainPrepopulated, ModelWithStringPrimaryKey, NotReferenced, OldSubscriber,
|
||||
OtherStory, Paper, Parent, ParentWithDependentChildren, Person, Persona,
|
||||
Picture, Pizza, Plot, PlotDetails, PluggableSearchPerson, Podcast, Post,
|
||||
PrePopulatedPost, PrePopulatedPostLargeSlug, PrePopulatedSubPost, Promo,
|
||||
Question, Recipe, Recommendation, Recommender, ReferencedByGenRel,
|
||||
ReferencedByInline, ReferencedByParent, RelatedPrepopulated, 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,
|
||||
Picture, Pizza, Plot, PlotDetails, PlotProxy, PluggableSearchPerson,
|
||||
Podcast, Post, PrePopulatedPost, PrePopulatedPostLargeSlug,
|
||||
PrePopulatedSubPost, Promo, Question, Recipe, Recommendation, Recommender,
|
||||
ReferencedByGenRel, ReferencedByInline, ReferencedByParent,
|
||||
RelatedPrepopulated, 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,
|
||||
)
|
||||
|
||||
|
||||
|
@ -853,6 +853,10 @@ class InlineRefererAdmin(admin.ModelAdmin):
|
|||
inlines = [InlineReferenceInline]
|
||||
|
||||
|
||||
class PlotReadonlyAdmin(admin.ModelAdmin):
|
||||
readonly_fields = ('plotdetails',)
|
||||
|
||||
|
||||
class GetFormsetsArgumentCheckingAdmin(admin.ModelAdmin):
|
||||
fields = ['name']
|
||||
|
||||
|
@ -907,6 +911,7 @@ site.register(Villain)
|
|||
site.register(SuperVillain)
|
||||
site.register(Plot)
|
||||
site.register(PlotDetails)
|
||||
site.register(PlotProxy, PlotReadonlyAdmin)
|
||||
site.register(CyclicOne)
|
||||
site.register(CyclicTwo)
|
||||
site.register(WorkHour, WorkHourAdmin)
|
||||
|
|
|
@ -501,12 +501,17 @@ class Plot(models.Model):
|
|||
@python_2_unicode_compatible
|
||||
class PlotDetails(models.Model):
|
||||
details = models.CharField(max_length=100)
|
||||
plot = models.OneToOneField(Plot)
|
||||
plot = models.OneToOneField(Plot, 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! """
|
||||
|
|
|
@ -52,11 +52,11 @@ from .models import (
|
|||
FoodDelivery, FunkyTag, Gallery, Grommet, Inquisition, Language, Link,
|
||||
MainPrepopulated, ModelWithStringPrimaryKey, OtherStory, Paper, Parent,
|
||||
ParentWithDependentChildren, Person, Persona, Picture, Pizza, Plot,
|
||||
PluggableSearchPerson, Podcast, Post, Promo, Question, RelatedPrepopulated,
|
||||
Report, Restaurant, RowLevelChangePermissionModel, Section, ShortMessage,
|
||||
Simple, Story, Subscriber, Telegram, Topping, UnchangeableObject,
|
||||
UndeletableObject, UnorderedObject, Vodcast, Whatsit, Widget, Worker,
|
||||
WorkHour,
|
||||
PlotDetails, PluggableSearchPerson, Podcast, Post, Promo, Question,
|
||||
RelatedPrepopulated, Report, Restaurant, RowLevelChangePermissionModel,
|
||||
Section, ShortMessage, Simple, Story, Subscriber, Telegram, Topping,
|
||||
UnchangeableObject, UndeletableObject, UnorderedObject, Villain, Vodcast,
|
||||
Whatsit, Widget, Worker, WorkHour,
|
||||
)
|
||||
|
||||
|
||||
|
@ -64,6 +64,44 @@ 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_fields(self, response):
|
||||
"""
|
||||
Return the fields for the response's AdminForm.
|
||||
"""
|
||||
return [f for f in self.get_admin_form_fields(response)]
|
||||
|
||||
def get_admin_field(self, response, field_name):
|
||||
"""
|
||||
Return the field for the given field_name.
|
||||
"""
|
||||
fields = self.get_admin_fields(response)
|
||||
for field in fields:
|
||||
name = field.field['name'] if isinstance(field.field, dict) else field.field.name
|
||||
if 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')
|
||||
|
@ -4029,7 +4067,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):
|
||||
fixtures = ['admin-views-users.xml']
|
||||
|
||||
def setUp(self):
|
||||
|
@ -4126,7 +4164,7 @@ class ReadonlyTest(TestCase):
|
|||
self.assertContains(response, '<p>No opinion</p>', html=True)
|
||||
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
|
||||
broke if the related field is read-only due to the help_text attribute
|
||||
|
@ -4137,6 +4175,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_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_field(response, 'plotdetails')
|
||||
self.assertEqual(force_text(field.contents()), '(None)')
|
||||
|
||||
def test_readonly_field_overrides(self):
|
||||
"""
|
||||
Regression test for #22087 - ModelForm Meta overrides are ignored by
|
||||
|
|
Loading…
Reference in New Issue