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:
parent
f6670e1341
commit
02cc59187b
|
@ -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:
|
||||||
|
|
|
@ -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'
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
=====================
|
=====================
|
||||||
|
|
||||||
|
|
|
@ -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/
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
"""}
|
"""}
|
||||||
|
|
Loading…
Reference in New Issue