mirror of https://github.com/django/django.git
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 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:
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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
|
||||
=====================
|
||||
|
||||
|
|
|
@ -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/
|
||||
|
|
|
@ -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>
|
||||
|
||||
"""}
|
||||
|
|
Loading…
Reference in New Issue