Django

Code

Changeset 8279

Show
Ignore:
Timestamp:
08/09/08 23:03:01 (1 year ago)
Author:
brosner
Message:

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.

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • django/trunk/django/contrib/admin/options.py

    r8274 r8279  
    133133        If kwargs are given, they're passed to the form Field's constructor. 
    134134        """ 
    135  
     135         
    136136        # If the field specifies choices, we don't need to look for special 
    137137        # admin widgets - we just need to use a select widget of some kind. 
  • django/trunk/django/contrib/contenttypes/generic.py

    r8223 r8279  
    77from django.db import connection 
    88from django.db.models import signals 
     9from django.db import models 
    910from django.db.models.fields.related import RelatedField, Field, ManyToManyRel 
    1011from django.db.models.loading import get_model 
    1112from django.utils.functional import curry 
     13 
     14from django.forms import ModelForm 
     15from django.forms.models import BaseModelFormSet, modelformset_factory, save_instance 
     16from django.contrib.admin.options import InlineModelAdmin, flatten_fieldsets 
    1217 
    1318class GenericForeignKey(object): 
     
    274279    def __init__(self, to, related_name=None, limit_choices_to=None, symmetrical=True): 
    275280        self.to = to 
    276         self.num_in_admin = 0 
    277281        self.related_name = related_name 
    278         self.filter_interface = None 
    279282        self.limit_choices_to = limit_choices_to or {} 
    280283        self.edit_inline = False 
    281         self.raw_id_admin = False 
    282284        self.symmetrical = symmetrical 
    283285        self.multiple = True 
    284         assert not (self.raw_id_admin and self.filter_interface), \ 
    285             "Generic relations may not use both raw_id_admin and filter_interface" 
     286 
     287class BaseGenericInlineFormSet(BaseModelFormSet): 
     288    """ 
     289    A formset for generic inline objects to a parent. 
     290    """ 
     291    ct_field_name = "content_type" 
     292    ct_fk_field_name = "object_id" 
     293     
     294    def __init__(self, data=None, files=None, instance=None, save_as_new=None): 
     295        opts = self.model._meta 
     296        self.instance = instance 
     297        self.rel_name = '-'.join(( 
     298            opts.app_label, opts.object_name.lower(), 
     299            self.ct_field.name, self.ct_fk_field.name, 
     300        )) 
     301        super(BaseGenericInlineFormSet, self).__init__( 
     302            queryset=self.get_queryset(), data=data, files=files, 
     303            prefix=self.rel_name 
     304        ) 
     305 
     306    def get_queryset(self): 
     307        # Avoid a circular import. 
     308        from django.contrib.contenttypes.models import ContentType 
     309        if self.instance is None: 
     310            return self.model._default_manager.empty() 
     311        return self.model._default_manager.filter(**{ 
     312            self.ct_field.name: ContentType.objects.get_for_model(self.instance), 
     313            self.ct_fk_field.name: self.instance.pk, 
     314        }) 
     315 
     316    def save_new(self, form, commit=True): 
     317        # Avoid a circular import. 
     318        from django.contrib.contenttypes.models import ContentType 
     319        kwargs = { 
     320            self.ct_field.get_attname(): ContentType.objects.get_for_model(self.instance).pk, 
     321            self.ct_fk_field.get_attname(): self.instance.pk, 
     322        } 
     323        new_obj = self.model(**kwargs) 
     324        return save_instance(form, new_obj, commit=commit) 
     325 
     326def generic_inlineformset_factory(model, form=ModelForm, 
     327                                  formset=BaseGenericInlineFormSet, 
     328                                  ct_field="content_type", fk_field="object_id", 
     329                                  fields=None, exclude=None, 
     330                                  extra=3, can_order=False, can_delete=True, 
     331                                  max_num=0, 
     332                                  formfield_callback=lambda f: f.formfield()): 
     333    """ 
     334    Returns an ``GenericInlineFormSet`` for the given kwargs. 
     335 
     336    You must provide ``ct_field`` and ``object_id`` if they different from the 
     337    defaults ``content_type`` and ``object_id`` respectively. 
     338    """ 
     339    opts = model._meta 
     340    # Avoid a circular import. 
     341    from django.contrib.contenttypes.models import ContentType 
     342    # if there is no field called `ct_field` let the exception propagate 
     343    ct_field = opts.get_field(ct_field) 
     344    if not isinstance(ct_field, models.ForeignKey) or ct_field.rel.to != ContentType: 
     345        raise Exception("fk_name '%s' is not a ForeignKey to ContentType" % ct_field) 
     346    fk_field = opts.get_field(fk_field) # let the exception propagate 
     347    if exclude is not None: 
     348        exclude.extend([ct_field.name, fk_field.name]) 
     349    else: 
     350        exclude = [ct_field.name, fk_field.name] 
     351    FormSet = modelformset_factory(model, form=form, 
     352                                   formfield_callback=formfield_callback, 
     353                                   formset=formset, 
     354                                   extra=extra, can_delete=can_delete, can_order=can_order, 
     355                                   fields=fields, exclude=exclude, max_num=max_num) 
     356    FormSet.ct_field = ct_field 
     357    FormSet.ct_fk_field = fk_field 
     358    return FormSet 
     359 
     360class GenericInlineModelAdmin(InlineModelAdmin): 
     361    ct_field = "content_type" 
     362    ct_fk_field = "object_id" 
     363    formset = BaseGenericInlineFormSet 
     364 
     365    def get_formset(self, request, obj=None): 
     366        if self.declared_fieldsets: 
     367            fields = flatten_fieldsets(self.declared_fieldsets) 
     368        else: 
     369            fields = None 
     370        defaults = { 
     371            "ct_field": self.ct_field, 
     372            "fk_field": self.ct_fk_field, 
     373            "form": self.form, 
     374            "formfield_callback": self.formfield_for_dbfield, 
     375            "formset": self.formset, 
     376            "extra": self.extra, 
     377            "can_delete": True, 
     378            "can_order": False, 
     379            "fields": fields, 
     380        } 
     381        return generic_inlineformset_factory(self.model, **defaults) 
     382 
     383class GenericStackedInline(GenericInlineModelAdmin): 
     384    template = 'admin/edit_inline/stacked.html' 
     385 
     386class GenericTabularInline(GenericInlineModelAdmin): 
     387    template = 'admin/edit_inline/tabular.html' 
     388 
  • django/trunk/docs/admin.txt

    r8273 r8279  
    786786either the ``Person`` or the ``Group`` detail pages. 
    787787 
     788Using generic relations as an inline 
     789------------------------------------ 
     790 
     791It is possible to use an inline with generically related objects. Let's say 
     792you have the following models:: 
     793 
     794    class Image(models.Model): 
     795        image = models.ImageField(upload_to="images") 
     796        content_type = models.ForeignKey(ContentType) 
     797        object_id = models.PositiveIntegerField() 
     798        content_object = generic.GenericForeignKey("content_type", "object_id") 
     799     
     800    class Product(models.Model): 
     801        name = models.CharField(max_length=100) 
     802 
     803If you want to allow editing and creating ``Image`` instance on the ``Product`` 
     804add/change views you can simply use ``GenericInlineModelAdmin`` provided by 
     805``django.contrib.contenttypes.generic``. In your ``admin.py`` for this 
     806example app:: 
     807 
     808    from django.contrib import admin 
     809    from django.contrib.contenttypes import generic 
     810     
     811    from myproject.myapp.models import Image, Product 
     812     
     813    class ImageInline(generic.GenericTabularInline): 
     814        model = Image 
     815     
     816    class ProductAdmin(admin.ModelAdmin): 
     817        inlines = [ 
     818            ImageInline, 
     819        ] 
     820     
     821    admin.site.register(Product, ProductAdmin) 
     822 
     823``django.contrib.contenttypes.generic`` provides both a ``GenericTabularInline`` 
     824and ``GenericStackedInline`` and behave just like any other inline. See the 
     825`contenttypes documentation`_ for more specific information. 
     826 
     827.. _contenttypes documentation: ../contenttypes/ 
     828 
    788829``AdminSite`` objects 
    789830===================== 
  • django/trunk/docs/contenttypes.txt

    r8042 r8279  
    7373 
    7474Let's look at an example to see how this works. If you already have 
    75 the contenttypes application installed, and then add `the sites 
    76 application`_ to your ``INSTALLED_APPS`` setting and run ``manage.py 
    77 syncdb`` to install it, the model ``django.contrib.sites.models.Site`` 
    78 will be installed into your database. Along with it a new instance 
    79 of ``ContentType`` will be created with the following values: 
     75the contenttypes application installed, and then add `the sites application`_ 
     76to your ``INSTALLED_APPS`` setting and run ``manage.py syncdb`` to install it, 
     77the model ``django.contrib.sites.models.Site`` will be installed into your 
     78database. Along with it a new instance of ``ContentType`` will be created with 
     79the following values: 
    8080 
    8181    * ``app_label`` will be set to ``'sites'`` (the last part of the Python 
     
    262262the example above, this means that if a ``Bookmark`` object were deleted, any 
    263263``TaggedItem`` objects pointing at it would be deleted at the same time. 
     264 
     265Generic relations in forms and admin 
     266------------------------------------ 
     267 
     268``django.contrib.contenttypes.genric`` provides both a ``GenericInlineFormSet`` 
     269and ``GenericInlineModelAdmin``. This enables the use of generic relations in 
     270forms and the admin. See the `model formset`_ and `admin`_ documentation for 
     271more information. 
     272 
     273``GenericInlineModelAdmin`` options 
     274~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
     275 
     276The ``GenericInlineModelAdmin`` class inherits all properties from an 
     277``InlineModelAdmin`` class. However, it adds a couple of its own for working 
     278with the generic relation: 
     279 
     280    * ``ct_field`` - The name of the ``ContentType`` foreign key field on the 
     281      model. Defaults to ``content_type``. 
     282     
     283    * ``ct_fk_field`` - The name of the integer field that represents the ID 
     284      of the related object. Defaults to ``object_id``. 
     285 
     286.. _model formset: ../modelforms/ 
     287.. _admin: ../admin/ 
  • django/trunk/tests/modeltests/generic_relations/models.py

    r8170 r8279  
    192192>>> Comparison.objects.all() 
    193193[<Comparison: tiger is stronger than None>] 
     194 
     195# GenericInlineFormSet tests ################################################## 
     196 
     197>>> from django.contrib.contenttypes.generic import generic_inlineformset_factory 
     198 
     199>>> GenericFormSet = generic_inlineformset_factory(TaggedItem, extra=1) 
     200>>> formset = GenericFormSet(instance=Animal()) 
     201>>> for form in formset.forms: 
     202...     print form.as_p() 
     203<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> 
     204<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> 
     205 
     206>>> formset = GenericFormSet(instance=platypus) 
     207>>> for form in formset.forms: 
     208...     print form.as_p() 
     209<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> 
     210<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> 
     211<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> 
     212<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> 
     213 
    194214"""}