Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #4667 -- Added support for inline generic relations in the admi…

…n. Thanks to Honza Král and Alex Gaynor for their work on this ticket.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@8279 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit 02cc59187be963b6c1ede084a7c9912ea6a1a338 1 parent f6670e1
Brian Rosner authored August 10, 2008
2  django/contrib/admin/options.py
@@ -132,7 +132,7 @@ def formfield_for_dbfield(self, db_field, **kwargs):
132 132
 
133 133
         If kwargs are given, they're passed to the form Field's constructor.
134 134
         """
135  
-
  135
+        
136 136
         # If the field specifies choices, we don't need to look for special
137 137
         # admin widgets - we just need to use a select widget of some kind.
138 138
         if db_field.choices:
113  django/contrib/contenttypes/generic.py
@@ -6,10 +6,15 @@
6 6
 from django.core.exceptions import ObjectDoesNotExist
7 7
 from django.db import connection
8 8
 from django.db.models import signals
  9
+from django.db import models
9 10
 from django.db.models.fields.related import RelatedField, Field, ManyToManyRel
10 11
 from django.db.models.loading import get_model
11 12
 from django.utils.functional import curry
12 13
 
  14
+from django.forms import ModelForm
  15
+from django.forms.models import BaseModelFormSet, modelformset_factory, save_instance
  16
+from django.contrib.admin.options import InlineModelAdmin, flatten_fieldsets
  17
+
13 18
 class GenericForeignKey(object):
14 19
     """
15 20
     Provides a generic relation to any object through content-type/object-id
@@ -273,13 +278,111 @@ def create(self, **kwargs):
273 278
 class GenericRel(ManyToManyRel):
274 279
     def __init__(self, to, related_name=None, limit_choices_to=None, symmetrical=True):
275 280
         self.to = to
276  
-        self.num_in_admin = 0
277 281
         self.related_name = related_name
278  
-        self.filter_interface = None
279 282
         self.limit_choices_to = limit_choices_to or {}
280 283
         self.edit_inline = False
281  
-        self.raw_id_admin = False
282 284
         self.symmetrical = symmetrical
283 285
         self.multiple = True
284  
-        assert not (self.raw_id_admin and self.filter_interface), \
285  
-            "Generic relations may not use both raw_id_admin and filter_interface"
  286
+
  287
+class BaseGenericInlineFormSet(BaseModelFormSet):
  288
+    """
  289
+    A formset for generic inline objects to a parent.
  290
+    """
  291
+    ct_field_name = "content_type"
  292
+    ct_fk_field_name = "object_id"
  293
+    
  294
+    def __init__(self, data=None, files=None, instance=None, save_as_new=None):
  295
+        opts = self.model._meta
  296
+        self.instance = instance
  297
+        self.rel_name = '-'.join((
  298
+            opts.app_label, opts.object_name.lower(),
  299
+            self.ct_field.name, self.ct_fk_field.name,
  300
+        ))
  301
+        super(BaseGenericInlineFormSet, self).__init__(
  302
+            queryset=self.get_queryset(), data=data, files=files,
  303
+            prefix=self.rel_name
  304
+        )
  305
+
  306
+    def get_queryset(self):
  307
+        # Avoid a circular import.
  308
+        from django.contrib.contenttypes.models import ContentType
  309
+        if self.instance is None:
  310
+            return self.model._default_manager.empty()
  311
+        return self.model._default_manager.filter(**{
  312
+            self.ct_field.name: ContentType.objects.get_for_model(self.instance),
  313
+            self.ct_fk_field.name: self.instance.pk,
  314
+        })
  315
+
  316
+    def save_new(self, form, commit=True):
  317
+        # Avoid a circular import.
  318
+        from django.contrib.contenttypes.models import ContentType
  319
+        kwargs = {
  320
+            self.ct_field.get_attname(): ContentType.objects.get_for_model(self.instance).pk,
  321
+            self.ct_fk_field.get_attname(): self.instance.pk,
  322
+        }
  323
+        new_obj = self.model(**kwargs)
  324
+        return save_instance(form, new_obj, commit=commit)
  325
+
  326
+def generic_inlineformset_factory(model, form=ModelForm,
  327
+                                  formset=BaseGenericInlineFormSet,
  328
+                                  ct_field="content_type", fk_field="object_id",
  329
+                                  fields=None, exclude=None,
  330
+                                  extra=3, can_order=False, can_delete=True,
  331
+                                  max_num=0,
  332
+                                  formfield_callback=lambda f: f.formfield()):
  333
+    """
  334
+    Returns an ``GenericInlineFormSet`` for the given kwargs.
  335
+
  336
+    You must provide ``ct_field`` and ``object_id`` if they different from the
  337
+    defaults ``content_type`` and ``object_id`` respectively.
  338
+    """
  339
+    opts = model._meta
  340
+    # Avoid a circular import.
  341
+    from django.contrib.contenttypes.models import ContentType
  342
+    # if there is no field called `ct_field` let the exception propagate
  343
+    ct_field = opts.get_field(ct_field)
  344
+    if not isinstance(ct_field, models.ForeignKey) or ct_field.rel.to != ContentType:
  345
+        raise Exception("fk_name '%s' is not a ForeignKey to ContentType" % ct_field)
  346
+    fk_field = opts.get_field(fk_field) # let the exception propagate
  347
+    if exclude is not None:
  348
+        exclude.extend([ct_field.name, fk_field.name])
  349
+    else:
  350
+        exclude = [ct_field.name, fk_field.name]
  351
+    FormSet = modelformset_factory(model, form=form,
  352
+                                   formfield_callback=formfield_callback,
  353
+                                   formset=formset,
  354
+                                   extra=extra, can_delete=can_delete, can_order=can_order,
  355
+                                   fields=fields, exclude=exclude, max_num=max_num)
  356
+    FormSet.ct_field = ct_field
  357
+    FormSet.ct_fk_field = fk_field
  358
+    return FormSet
  359
+
  360
+class GenericInlineModelAdmin(InlineModelAdmin):
  361
+    ct_field = "content_type"
  362
+    ct_fk_field = "object_id"
  363
+    formset = BaseGenericInlineFormSet
  364
+
  365
+    def get_formset(self, request, obj=None):
  366
+        if self.declared_fieldsets:
  367
+            fields = flatten_fieldsets(self.declared_fieldsets)
  368
+        else:
  369
+            fields = None
  370
+        defaults = {
  371
+            "ct_field": self.ct_field,
  372
+            "fk_field": self.ct_fk_field,
  373
+            "form": self.form,
  374
+            "formfield_callback": self.formfield_for_dbfield,
  375
+            "formset": self.formset,
  376
+            "extra": self.extra,
  377
+            "can_delete": True,
  378
+            "can_order": False,
  379
+            "fields": fields,
  380
+        }
  381
+        return generic_inlineformset_factory(self.model, **defaults)
  382
+
  383
+class GenericStackedInline(GenericInlineModelAdmin):
  384
+    template = 'admin/edit_inline/stacked.html'
  385
+
  386
+class GenericTabularInline(GenericInlineModelAdmin):
  387
+    template = 'admin/edit_inline/tabular.html'
  388
+
41  docs/admin.txt
@@ -785,6 +785,47 @@ Finally, register your ``Person`` and ``Group`` models with the admin site::
785 785
 Now your admin site is set up to edit ``Membership`` objects inline from
786 786
 either the ``Person`` or the ``Group`` detail pages.
787 787
 
  788
+Using generic relations as an inline
  789
+------------------------------------
  790
+
  791
+It is possible to use an inline with generically related objects. Let's say
  792
+you have the following models::
  793
+
  794
+    class Image(models.Model):
  795
+        image = models.ImageField(upload_to="images")
  796
+        content_type = models.ForeignKey(ContentType)
  797
+        object_id = models.PositiveIntegerField()
  798
+        content_object = generic.GenericForeignKey("content_type", "object_id")
  799
+    
  800
+    class Product(models.Model):
  801
+        name = models.CharField(max_length=100)
  802
+
  803
+If you want to allow editing and creating ``Image`` instance on the ``Product``
  804
+add/change views you can simply use ``GenericInlineModelAdmin`` provided by
  805
+``django.contrib.contenttypes.generic``. In your ``admin.py`` for this
  806
+example app::
  807
+
  808
+    from django.contrib import admin
  809
+    from django.contrib.contenttypes import generic
  810
+    
  811
+    from myproject.myapp.models import Image, Product
  812
+    
  813
+    class ImageInline(generic.GenericTabularInline):
  814
+        model = Image
  815
+    
  816
+    class ProductAdmin(admin.ModelAdmin):
  817
+        inlines = [
  818
+            ImageInline,
  819
+        ]
  820
+    
  821
+    admin.site.register(Product, ProductAdmin)
  822
+
  823
+``django.contrib.contenttypes.generic`` provides both a ``GenericTabularInline``
  824
+and ``GenericStackedInline`` and behave just like any other inline. See the
  825
+`contenttypes documentation`_ for more specific information.
  826
+
  827
+.. _contenttypes documentation: ../contenttypes/
  828
+
788 829
 ``AdminSite`` objects
789 830
 =====================
790 831
 
34  docs/contenttypes.txt
@@ -72,11 +72,11 @@ together, uniquely describe an installed model:
72 72
         `the verbose_name attribute`_ of the model.
73 73
 
74 74
 Let's look at an example to see how this works. If you already have
75  
-the contenttypes application installed, and then add `the sites
76  
-application`_ to your ``INSTALLED_APPS`` setting and run ``manage.py
77  
-syncdb`` to install it, the model ``django.contrib.sites.models.Site``
78  
-will be installed into your database. Along with it a new instance
79  
-of ``ContentType`` will be created with the following values:
  75
+the contenttypes application installed, and then add `the sites application`_
  76
+to your ``INSTALLED_APPS`` setting and run ``manage.py syncdb`` to install it,
  77
+the model ``django.contrib.sites.models.Site`` will be installed into your
  78
+database. Along with it a new instance of ``ContentType`` will be created with
  79
+the following values:
80 80
 
81 81
     * ``app_label`` will be set to ``'sites'`` (the last part of the Python
82 82
       path "django.contrib.sites").
@@ -261,3 +261,27 @@ Note that if you delete an object that has a ``GenericRelation``, any objects
261 261
 which have a ``GenericForeignKey`` pointing at it will be deleted as well. In
262 262
 the example above, this means that if a ``Bookmark`` object were deleted, any
263 263
 ``TaggedItem`` objects pointing at it would be deleted at the same time.
  264
+
  265
+Generic relations in forms and admin
  266
+------------------------------------
  267
+
  268
+``django.contrib.contenttypes.genric`` provides both a ``GenericInlineFormSet``
  269
+and ``GenericInlineModelAdmin``. This enables the use of generic relations in
  270
+forms and the admin. See the `model formset`_ and `admin`_ documentation for
  271
+more information.
  272
+
  273
+``GenericInlineModelAdmin`` options
  274
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  275
+
  276
+The ``GenericInlineModelAdmin`` class inherits all properties from an
  277
+``InlineModelAdmin`` class. However, it adds a couple of its own for working
  278
+with the generic relation:
  279
+
  280
+    * ``ct_field`` - The name of the ``ContentType`` foreign key field on the
  281
+      model. Defaults to ``content_type``.
  282
+    
  283
+    * ``ct_fk_field`` - The name of the integer field that represents the ID
  284
+      of the related object. Defaults to ``object_id``.
  285
+
  286
+.. _model formset: ../modelforms/
  287
+.. _admin: ../admin/
20  tests/modeltests/generic_relations/models.py
@@ -191,4 +191,24 @@ def __unicode__(self):
191 191
 >>> cheetah.delete()
192 192
 >>> Comparison.objects.all()
193 193
 [<Comparison: tiger is stronger than None>]
  194
+
  195
+# GenericInlineFormSet tests ##################################################
  196
+
  197
+>>> from django.contrib.contenttypes.generic import generic_inlineformset_factory
  198
+
  199
+>>> GenericFormSet = generic_inlineformset_factory(TaggedItem, extra=1)
  200
+>>> formset = GenericFormSet(instance=Animal())
  201
+>>> for form in formset.forms:
  202
+...     print form.as_p()
  203
+<p><label for="id_generic_relations-taggeditem-content_type-object_id-0-tag">Tag:</label> <input id="id_generic_relations-taggeditem-content_type-object_id-0-tag" type="text" name="generic_relations-taggeditem-content_type-object_id-0-tag" maxlength="50" /></p>
  204
+<p><label for="id_generic_relations-taggeditem-content_type-object_id-0-DELETE">Delete:</label> <input type="checkbox" name="generic_relations-taggeditem-content_type-object_id-0-DELETE" id="id_generic_relations-taggeditem-content_type-object_id-0-DELETE" /><input type="hidden" name="generic_relations-taggeditem-content_type-object_id-0-id" id="id_generic_relations-taggeditem-content_type-object_id-0-id" /></p>
  205
+
  206
+>>> formset = GenericFormSet(instance=platypus)
  207
+>>> for form in formset.forms:
  208
+...     print form.as_p()
  209
+<p><label for="id_generic_relations-taggeditem-content_type-object_id-0-tag">Tag:</label> <input id="id_generic_relations-taggeditem-content_type-object_id-0-tag" type="text" name="generic_relations-taggeditem-content_type-object_id-0-tag" value="shiny" maxlength="50" /></p>
  210
+<p><label for="id_generic_relations-taggeditem-content_type-object_id-0-DELETE">Delete:</label> <input type="checkbox" name="generic_relations-taggeditem-content_type-object_id-0-DELETE" id="id_generic_relations-taggeditem-content_type-object_id-0-DELETE" /><input type="hidden" name="generic_relations-taggeditem-content_type-object_id-0-id" value="5" id="id_generic_relations-taggeditem-content_type-object_id-0-id" /></p>
  211
+<p><label for="id_generic_relations-taggeditem-content_type-object_id-1-tag">Tag:</label> <input id="id_generic_relations-taggeditem-content_type-object_id-1-tag" type="text" name="generic_relations-taggeditem-content_type-object_id-1-tag" maxlength="50" /></p>
  212
+<p><label for="id_generic_relations-taggeditem-content_type-object_id-1-DELETE">Delete:</label> <input type="checkbox" name="generic_relations-taggeditem-content_type-object_id-1-DELETE" id="id_generic_relations-taggeditem-content_type-object_id-1-DELETE" /><input type="hidden" name="generic_relations-taggeditem-content_type-object_id-1-id" id="id_generic_relations-taggeditem-content_type-object_id-1-id" /></p>
  213
+
194 214
 """}

0 notes on commit 02cc591

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