Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

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
@brosner brosner authored
View
2  django/contrib/admin/options.py
@@ -132,7 +132,7 @@ def formfield_for_dbfield(self, db_field, **kwargs):
If kwargs are given, they're passed to the form Field's constructor.
"""
-
+
# If the field specifies choices, we don't need to look for special
# admin widgets - we just need to use a select widget of some kind.
if db_field.choices:
View
113 django/contrib/contenttypes/generic.py
@@ -6,10 +6,15 @@
from django.core.exceptions import ObjectDoesNotExist
from django.db import connection
from django.db.models import signals
+from django.db import models
from django.db.models.fields.related import RelatedField, Field, ManyToManyRel
from django.db.models.loading import get_model
from django.utils.functional import curry
+from django.forms import ModelForm
+from django.forms.models import BaseModelFormSet, modelformset_factory, save_instance
+from django.contrib.admin.options import InlineModelAdmin, flatten_fieldsets
+
class GenericForeignKey(object):
"""
Provides a generic relation to any object through content-type/object-id
@@ -273,13 +278,111 @@ def create(self, **kwargs):
class GenericRel(ManyToManyRel):
def __init__(self, to, related_name=None, limit_choices_to=None, symmetrical=True):
self.to = to
- self.num_in_admin = 0
self.related_name = related_name
- self.filter_interface = None
self.limit_choices_to = limit_choices_to or {}
self.edit_inline = False
- self.raw_id_admin = False
self.symmetrical = symmetrical
self.multiple = True
- assert not (self.raw_id_admin and self.filter_interface), \
- "Generic relations may not use both raw_id_admin and filter_interface"
+
+class BaseGenericInlineFormSet(BaseModelFormSet):
+ """
+ A formset for generic inline objects to a parent.
+ """
+ ct_field_name = "content_type"
+ ct_fk_field_name = "object_id"
+
+ def __init__(self, data=None, files=None, instance=None, save_as_new=None):
+ opts = self.model._meta
+ self.instance = instance
+ self.rel_name = '-'.join((
+ opts.app_label, opts.object_name.lower(),
+ self.ct_field.name, self.ct_fk_field.name,
+ ))
+ super(BaseGenericInlineFormSet, self).__init__(
+ queryset=self.get_queryset(), data=data, files=files,
+ prefix=self.rel_name
+ )
+
+ def get_queryset(self):
+ # Avoid a circular import.
+ from django.contrib.contenttypes.models import ContentType
+ if self.instance is None:
+ return self.model._default_manager.empty()
+ return self.model._default_manager.filter(**{
+ self.ct_field.name: ContentType.objects.get_for_model(self.instance),
+ self.ct_fk_field.name: self.instance.pk,
+ })
+
+ def save_new(self, form, commit=True):
+ # Avoid a circular import.
+ from django.contrib.contenttypes.models import ContentType
+ kwargs = {
+ self.ct_field.get_attname(): ContentType.objects.get_for_model(self.instance).pk,
+ self.ct_fk_field.get_attname(): self.instance.pk,
+ }
+ new_obj = self.model(**kwargs)
+ return save_instance(form, new_obj, commit=commit)
+
+def 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=0,
+ formfield_callback=lambda f: f.formfield()):
+ """
+ Returns an ``GenericInlineFormSet`` for the given kwargs.
+
+ You must provide ``ct_field`` and ``object_id`` if they different from the
+ defaults ``content_type`` and ``object_id`` respectively.
+ """
+ opts = model._meta
+ # Avoid a circular import.
+ from django.contrib.contenttypes.models import ContentType
+ # if there is no field called `ct_field` let the exception propagate
+ ct_field = opts.get_field(ct_field)
+ if not isinstance(ct_field, models.ForeignKey) or ct_field.rel.to != ContentType:
+ raise Exception("fk_name '%s' is not a ForeignKey to ContentType" % ct_field)
+ fk_field = opts.get_field(fk_field) # let the exception propagate
+ if exclude is not None:
+ exclude.extend([ct_field.name, fk_field.name])
+ else:
+ exclude = [ct_field.name, fk_field.name]
+ FormSet = modelformset_factory(model, form=form,
+ formfield_callback=formfield_callback,
+ formset=formset,
+ extra=extra, can_delete=can_delete, can_order=can_order,
+ fields=fields, exclude=exclude, max_num=max_num)
+ FormSet.ct_field = ct_field
+ FormSet.ct_fk_field = fk_field
+ return FormSet
+
+class GenericInlineModelAdmin(InlineModelAdmin):
+ ct_field = "content_type"
+ ct_fk_field = "object_id"
+ formset = BaseGenericInlineFormSet
+
+ def get_formset(self, request, obj=None):
+ if self.declared_fieldsets:
+ fields = flatten_fieldsets(self.declared_fieldsets)
+ else:
+ fields = None
+ defaults = {
+ "ct_field": self.ct_field,
+ "fk_field": self.ct_fk_field,
+ "form": self.form,
+ "formfield_callback": self.formfield_for_dbfield,
+ "formset": self.formset,
+ "extra": self.extra,
+ "can_delete": True,
+ "can_order": False,
+ "fields": fields,
+ }
+ return generic_inlineformset_factory(self.model, **defaults)
+
+class GenericStackedInline(GenericInlineModelAdmin):
+ template = 'admin/edit_inline/stacked.html'
+
+class GenericTabularInline(GenericInlineModelAdmin):
+ template = 'admin/edit_inline/tabular.html'
+
View
41 docs/admin.txt
@@ -785,6 +785,47 @@ Finally, register your ``Person`` and ``Group`` models with the admin site::
Now your admin site is set up to edit ``Membership`` objects inline from
either the ``Person`` or the ``Group`` detail pages.
+Using generic relations as an inline
+------------------------------------
+
+It is possible to use an inline with generically related objects. Let's say
+you have the following models::
+
+ class Image(models.Model):
+ image = models.ImageField(upload_to="images")
+ content_type = models.ForeignKey(ContentType)
+ object_id = models.PositiveIntegerField()
+ content_object = generic.GenericForeignKey("content_type", "object_id")
+
+ class Product(models.Model):
+ name = models.CharField(max_length=100)
+
+If you want to allow editing and creating ``Image`` instance on the ``Product``
+add/change views you can simply use ``GenericInlineModelAdmin`` provided by
+``django.contrib.contenttypes.generic``. In your ``admin.py`` for this
+example app::
+
+ from django.contrib import admin
+ from django.contrib.contenttypes import generic
+
+ from myproject.myapp.models import Image, Product
+
+ class ImageInline(generic.GenericTabularInline):
+ model = Image
+
+ class ProductAdmin(admin.ModelAdmin):
+ inlines = [
+ ImageInline,
+ ]
+
+ admin.site.register(Product, ProductAdmin)
+
+``django.contrib.contenttypes.generic`` provides both a ``GenericTabularInline``
+and ``GenericStackedInline`` and behave just like any other inline. See the
+`contenttypes documentation`_ for more specific information.
+
+.. _contenttypes documentation: ../contenttypes/
+
``AdminSite`` objects
=====================
View
34 docs/contenttypes.txt
@@ -72,11 +72,11 @@ together, uniquely describe an installed model:
`the verbose_name attribute`_ of the model.
Let's look at an example to see how this works. If you already have
-the contenttypes application installed, and then add `the sites
-application`_ to your ``INSTALLED_APPS`` setting and run ``manage.py
-syncdb`` to install it, the model ``django.contrib.sites.models.Site``
-will be installed into your database. Along with it a new instance
-of ``ContentType`` will be created with the following values:
+the contenttypes application installed, and then add `the sites application`_
+to your ``INSTALLED_APPS`` setting and run ``manage.py syncdb`` to install it,
+the model ``django.contrib.sites.models.Site`` will be installed into your
+database. Along with it a new instance of ``ContentType`` will be created with
+the following values:
* ``app_label`` will be set to ``'sites'`` (the last part of the Python
path "django.contrib.sites").
@@ -261,3 +261,27 @@ Note that if you delete an object that has a ``GenericRelation``, any objects
which have a ``GenericForeignKey`` pointing at it will be deleted as well. In
the example above, this means that if a ``Bookmark`` object were deleted, any
``TaggedItem`` objects pointing at it would be deleted at the same time.
+
+Generic relations in forms and admin
+------------------------------------
+
+``django.contrib.contenttypes.genric`` provides both a ``GenericInlineFormSet``
+and ``GenericInlineModelAdmin``. This enables the use of generic relations in
+forms and the admin. See the `model formset`_ and `admin`_ documentation for
+more information.
+
+``GenericInlineModelAdmin`` options
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The ``GenericInlineModelAdmin`` class inherits all properties from an
+``InlineModelAdmin`` class. However, it adds a couple of its own for working
+with the generic relation:
+
+ * ``ct_field`` - The name of the ``ContentType`` foreign key field on the
+ model. Defaults to ``content_type``.
+
+ * ``ct_fk_field`` - The name of the integer field that represents the ID
+ of the related object. Defaults to ``object_id``.
+
+.. _model formset: ../modelforms/
+.. _admin: ../admin/
View
20 tests/modeltests/generic_relations/models.py
@@ -191,4 +191,24 @@ def __unicode__(self):
>>> cheetah.delete()
>>> Comparison.objects.all()
[<Comparison: tiger is stronger than None>]
+
+# GenericInlineFormSet tests ##################################################
+
+>>> from django.contrib.contenttypes.generic import generic_inlineformset_factory
+
+>>> GenericFormSet = generic_inlineformset_factory(TaggedItem, extra=1)
+>>> formset = GenericFormSet(instance=Animal())
+>>> for form in formset.forms:
+... print form.as_p()
+<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>
+<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>
+
+>>> formset = GenericFormSet(instance=platypus)
+>>> for form in formset.forms:
+... print form.as_p()
+<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>
+<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>
+<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>
+<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>
+
"""}

0 comments on commit 02cc591

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