[1.6.x] Fixed #21428 -- editable GenericRelation regression
The GenericRelation refactoring removed GenericRelations from
model._meta.many_to_many. This had the side effect of disallowing
editable GenericRelations in ModelForms. Editable GenericRelations
aren't officially supported, but if we don't fix this we don't offer any
upgrade path for those who used the ability to set editable=True
in GenericRelation subclass.
Thanks to Trac alias joshcartme for the report and stephencmd and Loic
for working on this issue.
Backpatch of 0e079e4331
from master.
This commit is contained in:
parent
e8dea1f35c
commit
1fd762c106
|
@ -39,6 +39,7 @@ class GenericForeignKey(six.with_metaclass(RenameGenericForeignKeyMethods)):
|
||||||
self.ct_field = ct_field
|
self.ct_field = ct_field
|
||||||
self.fk_field = fk_field
|
self.fk_field = fk_field
|
||||||
self.for_concrete_model = for_concrete_model
|
self.for_concrete_model = for_concrete_model
|
||||||
|
self.editable = False
|
||||||
|
|
||||||
def contribute_to_class(self, cls, name):
|
def contribute_to_class(self, cls, name):
|
||||||
self.name = name
|
self.name = name
|
||||||
|
|
|
@ -82,7 +82,12 @@ def save_instance(form, instance, fields=None, fail_message='saved',
|
||||||
# Wrap up the saving of m2m data as a function.
|
# Wrap up the saving of m2m data as a function.
|
||||||
def save_m2m():
|
def save_m2m():
|
||||||
cleaned_data = form.cleaned_data
|
cleaned_data = form.cleaned_data
|
||||||
for f in opts.many_to_many:
|
# Note that for historical reasons we want to include also
|
||||||
|
# virtual_fields here. (GenericRelation was previously a fake
|
||||||
|
# m2m field).
|
||||||
|
for f in opts.many_to_many + opts.virtual_fields:
|
||||||
|
if not hasattr(f, 'save_form_data'):
|
||||||
|
continue
|
||||||
if fields and f.name not in fields:
|
if fields and f.name not in fields:
|
||||||
continue
|
continue
|
||||||
if exclude and f.name in exclude:
|
if exclude and f.name in exclude:
|
||||||
|
@ -118,8 +123,8 @@ def model_to_dict(instance, fields=None, exclude=None):
|
||||||
from django.db.models.fields.related import ManyToManyField
|
from django.db.models.fields.related import ManyToManyField
|
||||||
opts = instance._meta
|
opts = instance._meta
|
||||||
data = {}
|
data = {}
|
||||||
for f in opts.concrete_fields + opts.many_to_many:
|
for f in opts.concrete_fields + opts.virtual_fields + opts.many_to_many:
|
||||||
if not f.editable:
|
if not getattr(f, 'editable', False):
|
||||||
continue
|
continue
|
||||||
if fields and not f.name in fields:
|
if fields and not f.name in fields:
|
||||||
continue
|
continue
|
||||||
|
@ -168,8 +173,8 @@ def fields_for_model(model, fields=None, exclude=None, widgets=None,
|
||||||
field_list = []
|
field_list = []
|
||||||
ignored = []
|
ignored = []
|
||||||
opts = model._meta
|
opts = model._meta
|
||||||
for f in sorted(opts.concrete_fields + opts.many_to_many):
|
for f in sorted(opts.concrete_fields + opts.virtual_fields + opts.many_to_many):
|
||||||
if not f.editable:
|
if not getattr(f, 'editable', False):
|
||||||
continue
|
continue
|
||||||
if fields is not None and not f.name in fields:
|
if fields is not None and not f.name in fields:
|
||||||
continue
|
continue
|
||||||
|
|
|
@ -20,3 +20,5 @@ Bug fixes
|
||||||
* Fixed :class:`~django.contrib.auth.backends.ModelBackend` raising
|
* Fixed :class:`~django.contrib.auth.backends.ModelBackend` raising
|
||||||
``UnboundLocalError`` if :func:`~django.contrib.auth.get_user_model`
|
``UnboundLocalError`` if :func:`~django.contrib.auth.get_user_model`
|
||||||
raised an error (#21439).
|
raised an error (#21439).
|
||||||
|
* Fixed a regression that prevented editable ``GenericRelation`` subclasses
|
||||||
|
from working in ``ModelForms``.
|
||||||
|
|
|
@ -123,8 +123,17 @@ class Tag(models.Model):
|
||||||
class Board(models.Model):
|
class Board(models.Model):
|
||||||
name = models.CharField(primary_key=True, max_length=15)
|
name = models.CharField(primary_key=True, max_length=15)
|
||||||
|
|
||||||
|
class SpecialGenericRelation(generic.GenericRelation):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(SpecialGenericRelation, self).__init__(*args, **kwargs)
|
||||||
|
self.editable = True
|
||||||
|
self.save_form_data_calls = 0
|
||||||
|
|
||||||
|
def save_form_data(self, *args, **kwargs):
|
||||||
|
self.save_form_data_calls += 1
|
||||||
|
|
||||||
class HasLinks(models.Model):
|
class HasLinks(models.Model):
|
||||||
links = generic.GenericRelation(Link)
|
links = SpecialGenericRelation(Link)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.db.utils import IntegrityError
|
from django.db.utils import IntegrityError
|
||||||
from django.test import TestCase, skipIfDBFeature
|
from django.test import TestCase, skipIfDBFeature
|
||||||
|
from django.forms.models import modelform_factory
|
||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
Address, Place, Restaurant, Link, CharLink, TextLink,
|
Address, Place, Restaurant, Link, CharLink, TextLink,
|
||||||
|
@ -212,3 +213,13 @@ class GenericRelationTests(TestCase):
|
||||||
# B would then fail).
|
# B would then fail).
|
||||||
self.assertNotIn(" join ", str(B.objects.exclude(a__flag=True).query).lower())
|
self.assertNotIn(" join ", str(B.objects.exclude(a__flag=True).query).lower())
|
||||||
self.assertIn("content_type_id", str(B.objects.exclude(a__flag=True).query).lower())
|
self.assertIn("content_type_id", str(B.objects.exclude(a__flag=True).query).lower())
|
||||||
|
|
||||||
|
def test_editable_generic_rel(self):
|
||||||
|
GenericRelationForm = modelform_factory(HasLinkThing, fields='__all__')
|
||||||
|
form = GenericRelationForm()
|
||||||
|
self.assertIn('links', form.fields)
|
||||||
|
form = GenericRelationForm({'links': None})
|
||||||
|
self.assertTrue(form.is_valid())
|
||||||
|
form.save()
|
||||||
|
links = HasLinkThing._meta.get_field_by_name('links')[0].field
|
||||||
|
self.assertEqual(links.save_form_data_calls, 1)
|
||||||
|
|
Loading…
Reference in New Issue