Commit 02cc5918 authored by Brian Rosner's avatar Brian Rosner
Browse files

Fixed #4667 -- Added support for inline generic relations in the admin. Thanks...

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
parent f6670e13
Loading
Loading
Loading
Loading
+108 −5
Original line number Diff line number Diff line
@@ -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'
+41 −0
Original line number Diff line number Diff line
@@ -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
=====================

+29 −5
Original line number Diff line number Diff line
@@ -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/
+20 −0
Original line number Diff line number Diff line
@@ -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>

"""}
+1 −1

File changed.

Contains only whitespace changes.