diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index 90a817c0f12..a599fec83ec 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -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: diff --git a/django/contrib/contenttypes/generic.py b/django/contrib/contenttypes/generic.py index 3d1980a9829..9c85af2f8fa 100644 --- a/django/contrib/contenttypes/generic.py +++ b/django/contrib/contenttypes/generic.py @@ -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' + diff --git a/docs/admin.txt b/docs/admin.txt index d9b3523c047..dcff894c51b 100644 --- a/docs/admin.txt +++ b/docs/admin.txt @@ -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 ===================== diff --git a/docs/contenttypes.txt b/docs/contenttypes.txt index a4fc0457141..4d5fcc6cb80 100644 --- a/docs/contenttypes.txt +++ b/docs/contenttypes.txt @@ -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/ diff --git a/tests/modeltests/generic_relations/models.py b/tests/modeltests/generic_relations/models.py index d6a7c38e633..401b616efc9 100644 --- a/tests/modeltests/generic_relations/models.py +++ b/tests/modeltests/generic_relations/models.py @@ -191,4 +191,24 @@ __test__ = {'API_TESTS':""" >>> cheetah.delete() >>> Comparison.objects.all() [] + +# 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() +

+

+ +>>> formset = GenericFormSet(instance=platypus) +>>> for form in formset.forms: +... print form.as_p() +

+

+

+

+ """}