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:
Gavin Wahl 2013-05-20 11:51:05 -06:00 committed by Simon Charette
parent 8dda8a5ecc
commit 48424adaba
6 changed files with 179 additions and 17 deletions

View File

@ -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>

View File

@ -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):

View File

@ -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``.

View File

@ -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
===================================== =====================================

View File

@ -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

View File

@ -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())