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 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 File

@ -6,10 +6,15 @@ from django import oldforms
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_generic_related_manager(superclass):
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 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
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 File

@ -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 File

@ -191,4 +191,24 @@ __test__ = {'API_TESTS':"""
>>> 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>
"""}