Fixed #17648 -- Add `for_concrete_model` to `GenericForeignKey`.
Allows a `GenericForeignKey` to reference proxy models. The default for `for_concrete_model` is `True` to keep backwards compatibility. Also added the analog `for_concrete_model` kwarg to `generic_inlineformset_factory` to provide an API at the form level.
This commit is contained in:
parent
8dda8a5ecc
commit
48424adaba
1
AUTHORS
1
AUTHORS
|
@ -598,6 +598,7 @@ answer newbie questions, and generally made Django that much better:
|
||||||
Milton Waddams
|
Milton Waddams
|
||||||
Chris Wagner <cw264701@ohio.edu>
|
Chris Wagner <cw264701@ohio.edu>
|
||||||
Rick Wagner <rwagner@physics.ucsd.edu>
|
Rick Wagner <rwagner@physics.ucsd.edu>
|
||||||
|
Gavin Wahl <gavinwahl@gmail.com>
|
||||||
wam-djangobug@wamber.net
|
wam-djangobug@wamber.net
|
||||||
Wang Chun <wangchun@exoweb.net>
|
Wang Chun <wangchun@exoweb.net>
|
||||||
Filip Wasilewski <filip.wasilewski@gmail.com>
|
Filip Wasilewski <filip.wasilewski@gmail.com>
|
||||||
|
|
|
@ -35,9 +35,10 @@ class GenericForeignKey(six.with_metaclass(RenameGenericForeignKeyMethods)):
|
||||||
fields.
|
fields.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, ct_field="content_type", fk_field="object_id"):
|
def __init__(self, ct_field="content_type", fk_field="object_id", for_concrete_model=True):
|
||||||
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
|
||||||
|
|
||||||
def contribute_to_class(self, cls, name):
|
def contribute_to_class(self, cls, name):
|
||||||
self.name = name
|
self.name = name
|
||||||
|
@ -63,7 +64,8 @@ class GenericForeignKey(six.with_metaclass(RenameGenericForeignKeyMethods)):
|
||||||
|
|
||||||
def get_content_type(self, obj=None, id=None, using=None):
|
def get_content_type(self, obj=None, id=None, using=None):
|
||||||
if obj is not None:
|
if obj is not None:
|
||||||
return ContentType.objects.db_manager(obj._state.db).get_for_model(obj)
|
return ContentType.objects.db_manager(obj._state.db).get_for_model(
|
||||||
|
obj, for_concrete_model=self.for_concrete_model)
|
||||||
elif id:
|
elif id:
|
||||||
return ContentType.objects.db_manager(using).get_for_id(id)
|
return ContentType.objects.db_manager(using).get_for_id(id)
|
||||||
else:
|
else:
|
||||||
|
@ -160,6 +162,8 @@ class GenericRelation(ForeignObject):
|
||||||
self.object_id_field_name = kwargs.pop("object_id_field", "object_id")
|
self.object_id_field_name = kwargs.pop("object_id_field", "object_id")
|
||||||
self.content_type_field_name = kwargs.pop("content_type_field", "content_type")
|
self.content_type_field_name = kwargs.pop("content_type_field", "content_type")
|
||||||
|
|
||||||
|
self.for_concrete_model = kwargs.pop("for_concrete_model", True)
|
||||||
|
|
||||||
kwargs['blank'] = True
|
kwargs['blank'] = True
|
||||||
kwargs['editable'] = False
|
kwargs['editable'] = False
|
||||||
kwargs['serialize'] = False
|
kwargs['serialize'] = False
|
||||||
|
@ -201,7 +205,7 @@ class GenericRelation(ForeignObject):
|
||||||
# Save a reference to which model this class is on for future use
|
# Save a reference to which model this class is on for future use
|
||||||
self.model = cls
|
self.model = cls
|
||||||
# Add the descriptor for the relation
|
# Add the descriptor for the relation
|
||||||
setattr(cls, self.name, ReverseGenericRelatedObjectsDescriptor(self))
|
setattr(cls, self.name, ReverseGenericRelatedObjectsDescriptor(self, self.for_concrete_model))
|
||||||
|
|
||||||
def contribute_to_related_class(self, cls, related):
|
def contribute_to_related_class(self, cls, related):
|
||||||
pass
|
pass
|
||||||
|
@ -216,7 +220,8 @@ class GenericRelation(ForeignObject):
|
||||||
"""
|
"""
|
||||||
Returns the content type associated with this field's model.
|
Returns the content type associated with this field's model.
|
||||||
"""
|
"""
|
||||||
return ContentType.objects.get_for_model(self.model)
|
return ContentType.objects.get_for_model(self.model,
|
||||||
|
for_concrete_model=self.for_concrete_model)
|
||||||
|
|
||||||
def get_extra_restriction(self, where_class, alias, remote_alias):
|
def get_extra_restriction(self, where_class, alias, remote_alias):
|
||||||
field = self.rel.to._meta.get_field_by_name(self.content_type_field_name)[0]
|
field = self.rel.to._meta.get_field_by_name(self.content_type_field_name)[0]
|
||||||
|
@ -232,7 +237,8 @@ class GenericRelation(ForeignObject):
|
||||||
"""
|
"""
|
||||||
return self.rel.to._base_manager.db_manager(using).filter(**{
|
return self.rel.to._base_manager.db_manager(using).filter(**{
|
||||||
"%s__pk" % self.content_type_field_name:
|
"%s__pk" % self.content_type_field_name:
|
||||||
ContentType.objects.db_manager(using).get_for_model(self.model).pk,
|
ContentType.objects.db_manager(using).get_for_model(
|
||||||
|
self.model, for_concrete_model=self.for_concrete_model).pk,
|
||||||
"%s__in" % self.object_id_field_name:
|
"%s__in" % self.object_id_field_name:
|
||||||
[obj.pk for obj in objs]
|
[obj.pk for obj in objs]
|
||||||
})
|
})
|
||||||
|
@ -247,8 +253,9 @@ class ReverseGenericRelatedObjectsDescriptor(object):
|
||||||
"article.publications", the publications attribute is a
|
"article.publications", the publications attribute is a
|
||||||
ReverseGenericRelatedObjectsDescriptor instance.
|
ReverseGenericRelatedObjectsDescriptor instance.
|
||||||
"""
|
"""
|
||||||
def __init__(self, field):
|
def __init__(self, field, for_concrete_model=True):
|
||||||
self.field = field
|
self.field = field
|
||||||
|
self.for_concrete_model = for_concrete_model
|
||||||
|
|
||||||
def __get__(self, instance, instance_type=None):
|
def __get__(self, instance, instance_type=None):
|
||||||
if instance is None:
|
if instance is None:
|
||||||
|
@ -261,7 +268,8 @@ class ReverseGenericRelatedObjectsDescriptor(object):
|
||||||
RelatedManager = create_generic_related_manager(superclass)
|
RelatedManager = create_generic_related_manager(superclass)
|
||||||
|
|
||||||
qn = connection.ops.quote_name
|
qn = connection.ops.quote_name
|
||||||
content_type = ContentType.objects.db_manager(instance._state.db).get_for_model(instance)
|
content_type = ContentType.objects.db_manager(instance._state.db).get_for_model(
|
||||||
|
instance, for_concrete_model=self.for_concrete_model)
|
||||||
|
|
||||||
join_cols = self.field.get_joining_columns(reverse_join=True)[0]
|
join_cols = self.field.get_joining_columns(reverse_join=True)[0]
|
||||||
manager = RelatedManager(
|
manager = RelatedManager(
|
||||||
|
@ -389,7 +397,8 @@ class BaseGenericInlineFormSet(BaseModelFormSet):
|
||||||
if queryset is None:
|
if queryset is None:
|
||||||
queryset = self.model._default_manager
|
queryset = self.model._default_manager
|
||||||
qs = queryset.filter(**{
|
qs = queryset.filter(**{
|
||||||
self.ct_field.name: ContentType.objects.get_for_model(self.instance),
|
self.ct_field.name: ContentType.objects.get_for_model(
|
||||||
|
self.instance, for_concrete_model=self.for_concrete_model),
|
||||||
self.ct_fk_field.name: self.instance.pk,
|
self.ct_fk_field.name: self.instance.pk,
|
||||||
})
|
})
|
||||||
super(BaseGenericInlineFormSet, self).__init__(
|
super(BaseGenericInlineFormSet, self).__init__(
|
||||||
|
@ -406,7 +415,8 @@ class BaseGenericInlineFormSet(BaseModelFormSet):
|
||||||
|
|
||||||
def save_new(self, form, commit=True):
|
def save_new(self, form, commit=True):
|
||||||
kwargs = {
|
kwargs = {
|
||||||
self.ct_field.get_attname(): ContentType.objects.get_for_model(self.instance).pk,
|
self.ct_field.get_attname(): ContentType.objects.get_for_model(
|
||||||
|
self.instance, for_concrete_model=self.for_concrete_model).pk,
|
||||||
self.ct_fk_field.get_attname(): self.instance.pk,
|
self.ct_fk_field.get_attname(): self.instance.pk,
|
||||||
}
|
}
|
||||||
new_obj = self.model(**kwargs)
|
new_obj = self.model(**kwargs)
|
||||||
|
@ -418,7 +428,8 @@ def generic_inlineformset_factory(model, form=ModelForm,
|
||||||
fields=None, exclude=None,
|
fields=None, exclude=None,
|
||||||
extra=3, can_order=False, can_delete=True,
|
extra=3, can_order=False, can_delete=True,
|
||||||
max_num=None,
|
max_num=None,
|
||||||
formfield_callback=None, validate_max=False):
|
formfield_callback=None, validate_max=False,
|
||||||
|
for_concrete_model=True):
|
||||||
"""
|
"""
|
||||||
Returns a ``GenericInlineFormSet`` for the given kwargs.
|
Returns a ``GenericInlineFormSet`` for the given kwargs.
|
||||||
|
|
||||||
|
@ -444,6 +455,7 @@ def generic_inlineformset_factory(model, form=ModelForm,
|
||||||
validate_max=validate_max)
|
validate_max=validate_max)
|
||||||
FormSet.ct_field = ct_field
|
FormSet.ct_field = ct_field
|
||||||
FormSet.ct_fk_field = fk_field
|
FormSet.ct_fk_field = fk_field
|
||||||
|
FormSet.for_concrete_model = for_concrete_model
|
||||||
return FormSet
|
return FormSet
|
||||||
|
|
||||||
class GenericInlineModelAdmin(InlineModelAdmin):
|
class GenericInlineModelAdmin(InlineModelAdmin):
|
||||||
|
|
|
@ -303,6 +303,15 @@ model:
|
||||||
:class:`~django.contrib.contenttypes.generic.GenericForeignKey` will
|
:class:`~django.contrib.contenttypes.generic.GenericForeignKey` will
|
||||||
look for.
|
look for.
|
||||||
|
|
||||||
|
.. attribute:: GenericForeignKey.for_concrete_model
|
||||||
|
|
||||||
|
.. versionadded:: 1.6
|
||||||
|
|
||||||
|
If ``False``, the field will be able to reference proxy models. Default
|
||||||
|
is ``True``. This mirrors the ``for_concrete_model`` argument to
|
||||||
|
:meth:`~django.contrib.contenttypes.models.ContentTypeManager.get_for_model`.
|
||||||
|
|
||||||
|
|
||||||
.. admonition:: Primary key type compatibility
|
.. admonition:: Primary key type compatibility
|
||||||
|
|
||||||
The "object_id" field doesn't have to be the same type as the
|
The "object_id" field doesn't have to be the same type as the
|
||||||
|
@ -492,7 +501,7 @@ information.
|
||||||
Subclasses of :class:`GenericInlineModelAdmin` with stacked and tabular
|
Subclasses of :class:`GenericInlineModelAdmin` with stacked and tabular
|
||||||
layouts, respectively.
|
layouts, respectively.
|
||||||
|
|
||||||
.. function:: generic_inlineformset_factory(model, form=ModelForm, formset=BaseGenericInlineFormSet, ct_field="content_type", fk_field="object_id", fields=None, exclude=None, extra=3, can_order=False, can_delete=True, max_num=None, formfield_callback=None, validate_max=False)
|
.. function:: generic_inlineformset_factory(model, form=ModelForm, formset=BaseGenericInlineFormSet, ct_field="content_type", fk_field="object_id", fields=None, exclude=None, extra=3, can_order=False, can_delete=True, max_num=None, formfield_callback=None, validate_max=False, for_concrete_model=True)
|
||||||
|
|
||||||
Returns a ``GenericInlineFormSet`` using
|
Returns a ``GenericInlineFormSet`` using
|
||||||
:func:`~django.forms.models.modelformset_factory`.
|
:func:`~django.forms.models.modelformset_factory`.
|
||||||
|
@ -502,3 +511,9 @@ information.
|
||||||
are similar to those documented in
|
are similar to those documented in
|
||||||
:func:`~django.forms.models.modelformset_factory` and
|
:func:`~django.forms.models.modelformset_factory` and
|
||||||
:func:`~django.forms.models.inlineformset_factory`.
|
:func:`~django.forms.models.inlineformset_factory`.
|
||||||
|
|
||||||
|
.. versionadded:: 1.6
|
||||||
|
|
||||||
|
The ``for_concrete_model`` argument corresponds to the
|
||||||
|
:class:`~django.contrib.contenttypes.generic.GenericForeignKey.for_concrete_model`
|
||||||
|
argument on ``GenericForeignKey``.
|
||||||
|
|
|
@ -261,6 +261,11 @@ Minor features
|
||||||
* :class:`~django.views.generic.base.View` and
|
* :class:`~django.views.generic.base.View` and
|
||||||
:class:`~django.views.generic.base.RedirectView` now support HTTP PATCH method.
|
:class:`~django.views.generic.base.RedirectView` now support HTTP PATCH method.
|
||||||
|
|
||||||
|
* :class:`GenericForeignKey <django.contrib.contenttypes.generic.GenericForeignKey>`
|
||||||
|
now takes an optional ``for_concrete_model`` argument, which when set to
|
||||||
|
``False`` allows the field to reference proxy models. The default is ``True``
|
||||||
|
to retain the old behavior.
|
||||||
|
|
||||||
Backwards incompatible changes in 1.6
|
Backwards incompatible changes in 1.6
|
||||||
=====================================
|
=====================================
|
||||||
|
|
||||||
|
|
|
@ -102,3 +102,22 @@ class Rock(Mineral):
|
||||||
class ManualPK(models.Model):
|
class ManualPK(models.Model):
|
||||||
id = models.IntegerField(primary_key=True)
|
id = models.IntegerField(primary_key=True)
|
||||||
tags = generic.GenericRelation(TaggedItem)
|
tags = generic.GenericRelation(TaggedItem)
|
||||||
|
|
||||||
|
|
||||||
|
class ForProxyModelModel(models.Model):
|
||||||
|
content_type = models.ForeignKey(ContentType)
|
||||||
|
object_id = models.PositiveIntegerField()
|
||||||
|
obj = generic.GenericForeignKey(for_concrete_model=False)
|
||||||
|
title = models.CharField(max_length=255, null=True)
|
||||||
|
|
||||||
|
class ForConcreteModelModel(models.Model):
|
||||||
|
content_type = models.ForeignKey(ContentType)
|
||||||
|
object_id = models.PositiveIntegerField()
|
||||||
|
obj = generic.GenericForeignKey()
|
||||||
|
|
||||||
|
class ConcreteRelatedModel(models.Model):
|
||||||
|
bases = generic.GenericRelation(ForProxyModelModel, for_concrete_model=False)
|
||||||
|
|
||||||
|
class ProxyRelatedModel(ConcreteRelatedModel):
|
||||||
|
class Meta:
|
||||||
|
proxy = True
|
||||||
|
|
|
@ -6,7 +6,9 @@ from django.contrib.contenttypes.models import ContentType
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from .models import (TaggedItem, ValuableTaggedItem, Comparison, Animal,
|
from .models import (TaggedItem, ValuableTaggedItem, Comparison, Animal,
|
||||||
Vegetable, Mineral, Gecko, Rock, ManualPK)
|
Vegetable, Mineral, Gecko, Rock, ManualPK,
|
||||||
|
ForProxyModelModel, ForConcreteModelModel,
|
||||||
|
ProxyRelatedModel, ConcreteRelatedModel)
|
||||||
|
|
||||||
|
|
||||||
class GenericRelationsTests(TestCase):
|
class GenericRelationsTests(TestCase):
|
||||||
|
@ -256,12 +258,120 @@ class TaggedItemForm(forms.ModelForm):
|
||||||
widgets = {'tag': CustomWidget}
|
widgets = {'tag': CustomWidget}
|
||||||
|
|
||||||
class GenericInlineFormsetTest(TestCase):
|
class GenericInlineFormsetTest(TestCase):
|
||||||
|
def test_generic_inlineformset_factory(self):
|
||||||
"""
|
"""
|
||||||
Regression for #14572: Using base forms with widgets
|
Regression for #14572: Using base forms with widgets
|
||||||
defined in Meta should not raise errors.
|
defined in Meta should not raise errors.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def test_generic_inlineformset_factory(self):
|
|
||||||
Formset = generic_inlineformset_factory(TaggedItem, TaggedItemForm)
|
Formset = generic_inlineformset_factory(TaggedItem, TaggedItemForm)
|
||||||
form = Formset().forms[0]
|
form = Formset().forms[0]
|
||||||
self.assertIsInstance(form['tag'].field.widget, CustomWidget)
|
self.assertIsInstance(form['tag'].field.widget, CustomWidget)
|
||||||
|
|
||||||
|
def test_save_new_for_proxy(self):
|
||||||
|
Formset = generic_inlineformset_factory(ForProxyModelModel,
|
||||||
|
fields='__all__', for_concrete_model=False)
|
||||||
|
|
||||||
|
instance = ProxyRelatedModel.objects.create()
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'form-TOTAL_FORMS': '1',
|
||||||
|
'form-INITIAL_FORMS': '0',
|
||||||
|
'form-MAX_NUM_FORMS': '',
|
||||||
|
'form-0-title': 'foo',
|
||||||
|
}
|
||||||
|
|
||||||
|
formset = Formset(data, instance=instance, prefix='form')
|
||||||
|
self.assertTrue(formset.is_valid())
|
||||||
|
|
||||||
|
new_obj, = formset.save()
|
||||||
|
self.assertEqual(new_obj.obj, instance)
|
||||||
|
|
||||||
|
def test_save_new_for_concrete(self):
|
||||||
|
Formset = generic_inlineformset_factory(ForProxyModelModel,
|
||||||
|
fields='__all__', for_concrete_model=True)
|
||||||
|
|
||||||
|
instance = ProxyRelatedModel.objects.create()
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'form-TOTAL_FORMS': '1',
|
||||||
|
'form-INITIAL_FORMS': '0',
|
||||||
|
'form-MAX_NUM_FORMS': '',
|
||||||
|
'form-0-title': 'foo',
|
||||||
|
}
|
||||||
|
|
||||||
|
formset = Formset(data, instance=instance, prefix='form')
|
||||||
|
self.assertTrue(formset.is_valid())
|
||||||
|
|
||||||
|
new_obj, = formset.save()
|
||||||
|
self.assertNotIsInstance(new_obj.obj, ProxyRelatedModel)
|
||||||
|
|
||||||
|
|
||||||
|
class ProxyRelatedModelTest(TestCase):
|
||||||
|
def test_default_behavior(self):
|
||||||
|
"""
|
||||||
|
The default for for_concrete_model should be True
|
||||||
|
"""
|
||||||
|
base = ForConcreteModelModel()
|
||||||
|
base.obj = rel = ProxyRelatedModel.objects.create()
|
||||||
|
base.save()
|
||||||
|
|
||||||
|
base = ForConcreteModelModel.objects.get(pk=base.pk)
|
||||||
|
rel = ConcreteRelatedModel.objects.get(pk=rel.pk)
|
||||||
|
self.assertEqual(base.obj, rel)
|
||||||
|
|
||||||
|
def test_works_normally(self):
|
||||||
|
"""
|
||||||
|
When for_concrete_model is False, we should still be able to get
|
||||||
|
an instance of the concrete class.
|
||||||
|
"""
|
||||||
|
base = ForProxyModelModel()
|
||||||
|
base.obj = rel = ConcreteRelatedModel.objects.create()
|
||||||
|
base.save()
|
||||||
|
|
||||||
|
base = ForProxyModelModel.objects.get(pk=base.pk)
|
||||||
|
self.assertEqual(base.obj, rel)
|
||||||
|
|
||||||
|
def test_proxy_is_returned(self):
|
||||||
|
"""
|
||||||
|
Instances of the proxy should be returned when
|
||||||
|
for_concrete_model is False.
|
||||||
|
"""
|
||||||
|
base = ForProxyModelModel()
|
||||||
|
base.obj = ProxyRelatedModel.objects.create()
|
||||||
|
base.save()
|
||||||
|
|
||||||
|
base = ForProxyModelModel.objects.get(pk=base.pk)
|
||||||
|
self.assertIsInstance(base.obj, ProxyRelatedModel)
|
||||||
|
|
||||||
|
def test_query(self):
|
||||||
|
base = ForProxyModelModel()
|
||||||
|
base.obj = rel = ConcreteRelatedModel.objects.create()
|
||||||
|
base.save()
|
||||||
|
|
||||||
|
self.assertEqual(rel, ConcreteRelatedModel.objects.get(bases__id=base.id))
|
||||||
|
|
||||||
|
def test_query_proxy(self):
|
||||||
|
base = ForProxyModelModel()
|
||||||
|
base.obj = rel = ProxyRelatedModel.objects.create()
|
||||||
|
base.save()
|
||||||
|
|
||||||
|
self.assertEqual(rel, ProxyRelatedModel.objects.get(bases__id=base.id))
|
||||||
|
|
||||||
|
def test_generic_relation(self):
|
||||||
|
base = ForProxyModelModel()
|
||||||
|
base.obj = ProxyRelatedModel.objects.create()
|
||||||
|
base.save()
|
||||||
|
|
||||||
|
base = ForProxyModelModel.objects.get(pk=base.pk)
|
||||||
|
rel = ProxyRelatedModel.objects.get(pk=base.obj.pk)
|
||||||
|
self.assertEqual(base, rel.bases.get())
|
||||||
|
|
||||||
|
def test_generic_relation_set(self):
|
||||||
|
base = ForProxyModelModel()
|
||||||
|
base.obj = ConcreteRelatedModel.objects.create()
|
||||||
|
base.save()
|
||||||
|
newrel = ConcreteRelatedModel.objects.create()
|
||||||
|
|
||||||
|
newrel.bases = [base]
|
||||||
|
newrel = ConcreteRelatedModel.objects.get(pk=newrel.pk)
|
||||||
|
self.assertEqual(base, newrel.bases.get())
|
||||||
|
|
Loading…
Reference in New Issue