app/django/contrib/contenttypes/generic.py
changeset 323 ff1a9aa48cfd
parent 54 03e267d67478
--- a/app/django/contrib/contenttypes/generic.py	Tue Oct 14 12:36:55 2008 +0000
+++ b/app/django/contrib/contenttypes/generic.py	Tue Oct 14 16:00:59 2008 +0000
@@ -2,14 +2,16 @@
 Classes allowing "generic" relations through ContentType and object-id fields.
 """
 
-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.dispatch import dispatcher
-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
+from django.utils.encoding import smart_unicode
 
 class GenericForeignKey(object):
     """
@@ -22,19 +24,18 @@
         self.fk_field = fk_field
 
     def contribute_to_class(self, cls, name):
-        # Make sure the fields exist (these raise FieldDoesNotExist,
-        # which is a fine error to raise here)
         self.name = name
         self.model = cls
         self.cache_attr = "_%s_cache" % name
+        cls._meta.add_virtual_field(self)
 
         # For some reason I don't totally understand, using weakrefs here doesn't work.
-        dispatcher.connect(self.instance_pre_init, signal=signals.pre_init, sender=cls, weak=False)
+        signals.pre_init.connect(self.instance_pre_init, sender=cls, weak=False)
 
         # Connect myself as the descriptor for this field
         setattr(cls, name, self)
 
-    def instance_pre_init(self, signal, sender, args, kwargs):
+    def instance_pre_init(self, signal, sender, args, kwargs, **_kwargs):
         """
         Handles initializing an object with the generic FK instaed of
         content-type/object-id fields.
@@ -95,7 +96,7 @@
         setattr(instance, self.cache_attr, value)
 
 class GenericRelation(RelatedField, Field):
-    """Provides an accessor to generic related objects (i.e. comments)"""
+    """Provides an accessor to generic related objects (e.g. comments)"""
 
     def __init__(self, to, **kwargs):
         kwargs['verbose_name'] = kwargs.get('verbose_name', None)
@@ -104,6 +105,9 @@
                             limit_choices_to=kwargs.pop('limit_choices_to', None),
                             symmetrical=kwargs.pop('symmetrical', True))
 
+        # By its very nature, a GenericRelation doesn't create a table.
+        self.creates_table = False
+
         # Override content-type/object-id field names on the related class
         self.object_id_field_name = kwargs.pop("object_id_field", "object_id")
         self.content_type_field_name = kwargs.pop("content_type_field", "content_type")
@@ -113,19 +117,12 @@
         kwargs['serialize'] = False
         Field.__init__(self, **kwargs)
 
-    def get_manipulator_field_objs(self):
-        choices = self.get_choices_default()
-        return [curry(oldforms.SelectMultipleField, size=min(max(len(choices), 5), 15), choices=choices)]
-
     def get_choices_default(self):
         return Field.get_choices(self, include_blank=False)
 
-    def flatten_data(self, follow, obj = None):
-        new_data = {}
-        if obj:
-            instance_ids = [instance._get_pk_val() for instance in getattr(obj, self.name).all()]
-            new_data[self.name] = instance_ids
-        return new_data
+    def value_to_string(self, obj):
+        qs = getattr(obj, self.name).all()
+        return smart_unicode([instance._get_pk_val() for instance in qs])
 
     def m2m_db_table(self):
         return self.rel.to._meta.db_table
@@ -159,6 +156,19 @@
         # same db_type as well.
         return None
 
+    def extra_filters(self, pieces, pos, negate):
+        """
+        Return an extra filter to the queryset so that the results are filtered
+        on the appropriate content type.
+        """
+        if negate:
+            return []
+        ContentType = get_model("contenttypes", "contenttype")
+        content_type = ContentType.objects.get_for_model(self.model)
+        prefix = "__".join(pieces[:pos + 1])
+        return [("%s__%s" % (prefix, self.content_type_field_name),
+            content_type)]
+
 class ReverseGenericRelatedObjectsDescriptor(object):
     """
     This class provides the functionality that makes the related-object
@@ -261,9 +271,7 @@
         def create(self, **kwargs):
             kwargs[self.content_type_field_name] = self.content_type
             kwargs[self.object_id_field_name] = self.pk_val
-            obj = self.model(**kwargs)
-            obj.save()
-            return obj
+            return super(GenericRelatedObjectManager, self).create(**kwargs)
         create.alters_data = True
 
     return GenericRelatedObjectManager
@@ -271,13 +279,109 @@
 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'