Fixed #4667 -- Added support for inline generic relations in the admin. 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
This commit is contained in:
Brian Rosner 2008-08-10 04:03:01 +00:00
parent f6670e1341
commit 02cc59187b
5 changed files with 199 additions and 11 deletions

View File

@ -132,7 +132,7 @@ class BaseModelAdmin(object):
If kwargs are given, they're passed to the form Field's constructor. 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 # 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. # admin widgets - we just need to use a select widget of some kind.
if db_field.choices: if db_field.choices:

View File

@ -6,10 +6,15 @@ from django import oldforms
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.db import connection from django.db import connection
from django.db.models import signals 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.fields.related import RelatedField, Field, ManyToManyRel
from django.db.models.loading import get_model from django.db.models.loading import get_model
from django.utils.functional import curry 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): class GenericForeignKey(object):
""" """
Provides a generic relation to any object through content-type/object-id Provides a generic relation to any object through content-type/object-id
@ -273,13 +278,111 @@ def create_generic_related_manager(superclass):
class GenericRel(ManyToManyRel): class GenericRel(ManyToManyRel):
def __init__(self, to, related_name=None, limit_choices_to=None, symmetrical=True): def __init__(self, to, related_name=None, limit_choices_to=None, symmetrical=True):
self.to = to self.to = to
self.num_in_admin = 0
self.related_name = related_name self.related_name = related_name
self.filter_interface = None
self.limit_choices_to = limit_choices_to or {} self.limit_choices_to = limit_choices_to or {}
self.edit_inline = False self.edit_inline = False
self.raw_id_admin = False
self.symmetrical = symmetrical self.symmetrical = symmetrical
self.multiple = True 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 File

@ -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 Now your admin site is set up to edit ``Membership`` objects inline from
either the ``Person`` or the ``Group`` detail pages. 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 ``AdminSite`` objects
===================== =====================

View File

@ -72,11 +72,11 @@ together, uniquely describe an installed model:
`the verbose_name attribute`_ of the model. `the verbose_name attribute`_ of the model.
Let's look at an example to see how this works. If you already have Let's look at an example to see how this works. If you already have
the contenttypes application installed, and then add `the sites the contenttypes application installed, and then add `the sites application`_
application`_ to your ``INSTALLED_APPS`` setting and run ``manage.py to your ``INSTALLED_APPS`` setting and run ``manage.py syncdb`` to install it,
syncdb`` to install it, the model ``django.contrib.sites.models.Site`` the model ``django.contrib.sites.models.Site`` will be installed into your
will be installed into your database. Along with it a new instance database. Along with it a new instance of ``ContentType`` will be created with
of ``ContentType`` will be created with the following values: the following values:
* ``app_label`` will be set to ``'sites'`` (the last part of the Python * ``app_label`` will be set to ``'sites'`` (the last part of the Python
path "django.contrib.sites"). 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 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 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. ``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 File

@ -191,4 +191,24 @@ __test__ = {'API_TESTS':"""
>>> cheetah.delete() >>> cheetah.delete()
>>> Comparison.objects.all() >>> Comparison.objects.all()
[<Comparison: tiger is stronger than None>] [<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>
"""} """}