Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

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.
  • Loading branch information...
commit 48424adaba74379eee311b3d1519f011212357ad 1 parent 8dda8a5
Gavin Wahl authored May 20, 2013 charettes committed May 23, 2013
1  AUTHORS
@@ -598,6 +598,7 @@ answer newbie questions, and generally made Django that much better:
598 598
     Milton Waddams
599 599
     Chris Wagner <cw264701@ohio.edu>
600 600
     Rick Wagner <rwagner@physics.ucsd.edu>
  601
+    Gavin Wahl <gavinwahl@gmail.com>
601 602
     wam-djangobug@wamber.net
602 603
     Wang Chun <wangchun@exoweb.net>
603 604
     Filip Wasilewski <filip.wasilewski@gmail.com>
32  django/contrib/contenttypes/generic.py
@@ -35,9 +35,10 @@ class GenericForeignKey(six.with_metaclass(RenameGenericForeignKeyMethods)):
35 35
     fields.
36 36
     """
37 37
 
38  
-    def __init__(self, ct_field="content_type", fk_field="object_id"):
  38
+    def __init__(self, ct_field="content_type", fk_field="object_id", for_concrete_model=True):
39 39
         self.ct_field = ct_field
40 40
         self.fk_field = fk_field
  41
+        self.for_concrete_model = for_concrete_model
41 42
 
42 43
     def contribute_to_class(self, cls, name):
43 44
         self.name = name
@@ -63,7 +64,8 @@ def instance_pre_init(self, signal, sender, args, kwargs, **_kwargs):
63 64
 
64 65
     def get_content_type(self, obj=None, id=None, using=None):
65 66
         if obj is not None:
66  
-            return ContentType.objects.db_manager(obj._state.db).get_for_model(obj)
  67
+            return ContentType.objects.db_manager(obj._state.db).get_for_model(
  68
+                obj, for_concrete_model=self.for_concrete_model)
67 69
         elif id:
68 70
             return ContentType.objects.db_manager(using).get_for_id(id)
69 71
         else:
@@ -160,6 +162,8 @@ def __init__(self, to, **kwargs):
160 162
         self.object_id_field_name = kwargs.pop("object_id_field", "object_id")
161 163
         self.content_type_field_name = kwargs.pop("content_type_field", "content_type")
162 164
 
  165
+        self.for_concrete_model = kwargs.pop("for_concrete_model", True)
  166
+
163 167
         kwargs['blank'] = True
164 168
         kwargs['editable'] = False
165 169
         kwargs['serialize'] = False
@@ -201,7 +205,7 @@ def contribute_to_class(self, cls, name):
201 205
         # Save a reference to which model this class is on for future use
202 206
         self.model = cls
203 207
         # Add the descriptor for the relation
204  
-        setattr(cls, self.name, ReverseGenericRelatedObjectsDescriptor(self))
  208
+        setattr(cls, self.name, ReverseGenericRelatedObjectsDescriptor(self, self.for_concrete_model))
205 209
 
206 210
     def contribute_to_related_class(self, cls, related):
207 211
         pass
@@ -216,7 +220,8 @@ def get_content_type(self):
216 220
         """
217 221
         Returns the content type associated with this field's model.
218 222
         """
219  
-        return ContentType.objects.get_for_model(self.model)
  223
+        return ContentType.objects.get_for_model(self.model,
  224
+                                                 for_concrete_model=self.for_concrete_model)
220 225
 
221 226
     def get_extra_restriction(self, where_class, alias, remote_alias):
222 227
         field = self.rel.to._meta.get_field_by_name(self.content_type_field_name)[0]
@@ -232,7 +237,8 @@ def bulk_related_objects(self, objs, using=DEFAULT_DB_ALIAS):
232 237
         """
233 238
         return self.rel.to._base_manager.db_manager(using).filter(**{
234 239
                 "%s__pk" % self.content_type_field_name:
235  
-                    ContentType.objects.db_manager(using).get_for_model(self.model).pk,
  240
+                    ContentType.objects.db_manager(using).get_for_model(
  241
+                        self.model, for_concrete_model=self.for_concrete_model).pk,
236 242
                 "%s__in" % self.object_id_field_name:
237 243
                     [obj.pk for obj in objs]
238 244
                 })
@@ -247,8 +253,9 @@ class ReverseGenericRelatedObjectsDescriptor(object):
247 253
     "article.publications", the publications attribute is a
248 254
     ReverseGenericRelatedObjectsDescriptor instance.
249 255
     """
250  
-    def __init__(self, field):
  256
+    def __init__(self, field, for_concrete_model=True):
251 257
         self.field = field
  258
+        self.for_concrete_model = for_concrete_model
252 259
 
253 260
     def __get__(self, instance, instance_type=None):
254 261
         if instance is None:
@@ -261,7 +268,8 @@ def __get__(self, instance, instance_type=None):
261 268
         RelatedManager = create_generic_related_manager(superclass)
262 269
 
263 270
         qn = connection.ops.quote_name
264  
-        content_type = ContentType.objects.db_manager(instance._state.db).get_for_model(instance)
  271
+        content_type = ContentType.objects.db_manager(instance._state.db).get_for_model(
  272
+            instance, for_concrete_model=self.for_concrete_model)
265 273
 
266 274
         join_cols = self.field.get_joining_columns(reverse_join=True)[0]
267 275
         manager = RelatedManager(
@@ -389,7 +397,8 @@ def __init__(self, data=None, files=None, instance=None, save_as_new=None,
389 397
             if queryset is None:
390 398
                 queryset = self.model._default_manager
391 399
             qs = queryset.filter(**{
392  
-                self.ct_field.name: ContentType.objects.get_for_model(self.instance),
  400
+                self.ct_field.name: ContentType.objects.get_for_model(
  401
+                    self.instance, for_concrete_model=self.for_concrete_model),
393 402
                 self.ct_fk_field.name: self.instance.pk,
394 403
             })
395 404
         super(BaseGenericInlineFormSet, self).__init__(
@@ -406,7 +415,8 @@ def get_default_prefix(cls):
406 415
 
407 416
     def save_new(self, form, commit=True):
408 417
         kwargs = {
409  
-            self.ct_field.get_attname(): ContentType.objects.get_for_model(self.instance).pk,
  418
+            self.ct_field.get_attname(): ContentType.objects.get_for_model(
  419
+                self.instance, for_concrete_model=self.for_concrete_model).pk,
410 420
             self.ct_fk_field.get_attname(): self.instance.pk,
411 421
         }
412 422
         new_obj = self.model(**kwargs)
@@ -418,7 +428,8 @@ def generic_inlineformset_factory(model, form=ModelForm,
418 428
                                   fields=None, exclude=None,
419 429
                                   extra=3, can_order=False, can_delete=True,
420 430
                                   max_num=None,
421  
-                                  formfield_callback=None, validate_max=False):
  431
+                                  formfield_callback=None, validate_max=False,
  432
+                                  for_concrete_model=True):
422 433
     """
423 434
     Returns a ``GenericInlineFormSet`` for the given kwargs.
424 435
 
@@ -444,6 +455,7 @@ def generic_inlineformset_factory(model, form=ModelForm,
444 455
                                    validate_max=validate_max)
445 456
     FormSet.ct_field = ct_field
446 457
     FormSet.ct_fk_field = fk_field
  458
+    FormSet.for_concrete_model = for_concrete_model
447 459
     return FormSet
448 460
 
449 461
 class GenericInlineModelAdmin(InlineModelAdmin):
17  docs/ref/contrib/contenttypes.txt
@@ -303,6 +303,15 @@ model:
303 303
        :class:`~django.contrib.contenttypes.generic.GenericForeignKey` will
304 304
        look for.
305 305
 
  306
+    .. attribute:: GenericForeignKey.for_concrete_model
  307
+
  308
+       .. versionadded:: 1.6
  309
+
  310
+       If ``False``, the field will be able to reference proxy models. Default
  311
+       is ``True``. This mirrors the ``for_concrete_model`` argument to
  312
+       :meth:`~django.contrib.contenttypes.models.ContentTypeManager.get_for_model`.
  313
+
  314
+
306 315
 .. admonition:: Primary key type compatibility
307 316
 
308 317
    The "object_id" field doesn't have to be the same type as the
@@ -492,7 +501,7 @@ information.
492 501
     Subclasses of :class:`GenericInlineModelAdmin` with stacked and tabular
493 502
     layouts, respectively.
494 503
 
495  
-.. 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)
  504
+.. 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)
496 505
 
497 506
     Returns a ``GenericInlineFormSet`` using
498 507
     :func:`~django.forms.models.modelformset_factory`.
@@ -502,3 +511,9 @@ information.
502 511
     are similar to those documented in
503 512
     :func:`~django.forms.models.modelformset_factory` and
504 513
     :func:`~django.forms.models.inlineformset_factory`.
  514
+
  515
+    .. versionadded:: 1.6
  516
+
  517
+        The ``for_concrete_model`` argument corresponds to the
  518
+        :class:`~django.contrib.contenttypes.generic.GenericForeignKey.for_concrete_model`
  519
+        argument on ``GenericForeignKey``.
5  docs/releases/1.6.txt
@@ -261,6 +261,11 @@ Minor features
261 261
 * :class:`~django.views.generic.base.View` and
262 262
   :class:`~django.views.generic.base.RedirectView` now support HTTP PATCH method.
263 263
 
  264
+* :class:`GenericForeignKey <django.contrib.contenttypes.generic.GenericForeignKey>`
  265
+  now takes an optional ``for_concrete_model`` argument, which when set to
  266
+  ``False`` allows the field to reference proxy models. The default is ``True``
  267
+  to retain the old behavior.
  268
+
264 269
 Backwards incompatible changes in 1.6
265 270
 =====================================
266 271
 
19  tests/generic_relations/models.py
@@ -102,3 +102,22 @@ class Rock(Mineral):
102 102
 class ManualPK(models.Model):
103 103
     id = models.IntegerField(primary_key=True)
104 104
     tags = generic.GenericRelation(TaggedItem)
  105
+
  106
+
  107
+class ForProxyModelModel(models.Model):
  108
+    content_type = models.ForeignKey(ContentType)
  109
+    object_id = models.PositiveIntegerField()
  110
+    obj = generic.GenericForeignKey(for_concrete_model=False)
  111
+    title = models.CharField(max_length=255, null=True)
  112
+
  113
+class ForConcreteModelModel(models.Model):
  114
+    content_type = models.ForeignKey(ContentType)
  115
+    object_id = models.PositiveIntegerField()
  116
+    obj = generic.GenericForeignKey()
  117
+
  118
+class ConcreteRelatedModel(models.Model):
  119
+    bases = generic.GenericRelation(ForProxyModelModel, for_concrete_model=False)
  120
+
  121
+class ProxyRelatedModel(ConcreteRelatedModel):
  122
+    class Meta:
  123
+        proxy = True
122  tests/generic_relations/tests.py
@@ -6,7 +6,9 @@
6 6
 from django.test import TestCase
7 7
 
8 8
 from .models import (TaggedItem, ValuableTaggedItem, Comparison, Animal,
9  
-    Vegetable, Mineral, Gecko, Rock, ManualPK)
  9
+                     Vegetable, Mineral, Gecko, Rock, ManualPK,
  10
+                     ForProxyModelModel, ForConcreteModelModel,
  11
+                     ProxyRelatedModel, ConcreteRelatedModel)
10 12
 
11 13
 
12 14
 class GenericRelationsTests(TestCase):
@@ -256,12 +258,120 @@ class Meta:
256 258
         widgets = {'tag': CustomWidget}
257 259
 
258 260
 class GenericInlineFormsetTest(TestCase):
259  
-    """
260  
-    Regression for #14572: Using base forms with widgets
261  
-    defined in Meta should not raise errors.
262  
-    """
263  
-
264 261
     def test_generic_inlineformset_factory(self):
  262
+        """
  263
+        Regression for #14572: Using base forms with widgets
  264
+        defined in Meta should not raise errors.
  265
+        """
265 266
         Formset = generic_inlineformset_factory(TaggedItem, TaggedItemForm)
266 267
         form = Formset().forms[0]
267 268
         self.assertIsInstance(form['tag'].field.widget, CustomWidget)
  269
+
  270
+    def test_save_new_for_proxy(self):
  271
+        Formset = generic_inlineformset_factory(ForProxyModelModel,
  272
+            fields='__all__', for_concrete_model=False)
  273
+
  274
+        instance = ProxyRelatedModel.objects.create()
  275
+
  276
+        data = {
  277
+            'form-TOTAL_FORMS': '1',
  278
+            'form-INITIAL_FORMS': '0',
  279
+            'form-MAX_NUM_FORMS': '',
  280
+            'form-0-title': 'foo',
  281
+        }
  282
+
  283
+        formset = Formset(data, instance=instance, prefix='form')
  284
+        self.assertTrue(formset.is_valid())
  285
+
  286
+        new_obj, = formset.save()
  287
+        self.assertEqual(new_obj.obj, instance)
  288
+
  289
+    def test_save_new_for_concrete(self):
  290
+        Formset = generic_inlineformset_factory(ForProxyModelModel,
  291
+            fields='__all__', for_concrete_model=True)
  292
+
  293
+        instance = ProxyRelatedModel.objects.create()
  294
+
  295
+        data = {
  296
+            'form-TOTAL_FORMS': '1',
  297
+            'form-INITIAL_FORMS': '0',
  298
+            'form-MAX_NUM_FORMS': '',
  299
+            'form-0-title': 'foo',
  300
+        }
  301
+
  302
+        formset = Formset(data, instance=instance, prefix='form')
  303
+        self.assertTrue(formset.is_valid())
  304
+
  305
+        new_obj, = formset.save()
  306
+        self.assertNotIsInstance(new_obj.obj, ProxyRelatedModel)
  307
+
  308
+
  309
+class ProxyRelatedModelTest(TestCase):
  310
+    def test_default_behavior(self):
  311
+        """
  312
+        The default for for_concrete_model should be True
  313
+        """
  314
+        base = ForConcreteModelModel()
  315
+        base.obj = rel = ProxyRelatedModel.objects.create()
  316
+        base.save()
  317
+
  318
+        base = ForConcreteModelModel.objects.get(pk=base.pk)
  319
+        rel = ConcreteRelatedModel.objects.get(pk=rel.pk)
  320
+        self.assertEqual(base.obj, rel)
  321
+
  322
+    def test_works_normally(self):
  323
+        """
  324
+        When for_concrete_model is False, we should still be able to get
  325
+        an instance of the concrete class.
  326
+        """
  327
+        base = ForProxyModelModel()
  328
+        base.obj = rel = ConcreteRelatedModel.objects.create()
  329
+        base.save()
  330
+
  331
+        base = ForProxyModelModel.objects.get(pk=base.pk)
  332
+        self.assertEqual(base.obj, rel)
  333
+
  334
+    def test_proxy_is_returned(self):
  335
+        """
  336
+        Instances of the proxy should be returned when
  337
+        for_concrete_model is False.
  338
+        """
  339
+        base = ForProxyModelModel()
  340
+        base.obj = ProxyRelatedModel.objects.create()
  341
+        base.save()
  342
+
  343
+        base = ForProxyModelModel.objects.get(pk=base.pk)
  344
+        self.assertIsInstance(base.obj, ProxyRelatedModel)
  345
+
  346
+    def test_query(self):
  347
+        base = ForProxyModelModel()
  348
+        base.obj = rel = ConcreteRelatedModel.objects.create()
  349
+        base.save()
  350
+
  351
+        self.assertEqual(rel, ConcreteRelatedModel.objects.get(bases__id=base.id))
  352
+
  353
+    def test_query_proxy(self):
  354
+        base = ForProxyModelModel()
  355
+        base.obj = rel = ProxyRelatedModel.objects.create()
  356
+        base.save()
  357
+
  358
+        self.assertEqual(rel, ProxyRelatedModel.objects.get(bases__id=base.id))
  359
+
  360
+    def test_generic_relation(self):
  361
+        base = ForProxyModelModel()
  362
+        base.obj = ProxyRelatedModel.objects.create()
  363
+        base.save()
  364
+
  365
+        base = ForProxyModelModel.objects.get(pk=base.pk)
  366
+        rel = ProxyRelatedModel.objects.get(pk=base.obj.pk)
  367
+        self.assertEqual(base, rel.bases.get())
  368
+
  369
+    def test_generic_relation_set(self):
  370
+        base = ForProxyModelModel()
  371
+        base.obj = ConcreteRelatedModel.objects.create()
  372
+        base.save()
  373
+        newrel = ConcreteRelatedModel.objects.create()
  374
+
  375
+        newrel.bases = [base]
  376
+        newrel = ConcreteRelatedModel.objects.get(pk=newrel.pk)
  377
+        self.assertEqual(base, newrel.bases.get())

0 notes on commit 48424ad

Please sign in to comment.
Something went wrong with that request. Please try again.