Skip to content

Commit

Permalink
Fixed #4667 -- Added support for inline generic relations in the admi…
Browse files Browse the repository at this point in the history
…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
brosner committed Aug 10, 2008
1 parent f6670e1 commit 02cc591
Show file tree
Hide file tree
Showing 5 changed files with 199 additions and 11 deletions.
2 changes: 1 addition & 1 deletion django/contrib/admin/options.py
Expand Up @@ -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:
Expand Down
113 changes: 108 additions & 5 deletions django/contrib/contenttypes/generic.py
Expand Up @@ -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
Expand Down Expand Up @@ -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'

41 changes: 41 additions & 0 deletions docs/admin.txt
Expand Up @@ -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
=====================

Expand Down
34 changes: 29 additions & 5 deletions docs/contenttypes.txt
Expand Up @@ -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").
Expand Down Expand Up @@ -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/
20 changes: 20 additions & 0 deletions tests/modeltests/generic_relations/models.py
Expand Up @@ -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.