diff options
Diffstat (limited to 'webapp/django/contrib/contenttypes/generic.py')
-rw-r--r-- | webapp/django/contrib/contenttypes/generic.py | 388 |
1 files changed, 388 insertions, 0 deletions
diff --git a/webapp/django/contrib/contenttypes/generic.py b/webapp/django/contrib/contenttypes/generic.py new file mode 100644 index 0000000000..9c85af2f8f --- /dev/null +++ b/webapp/django/contrib/contenttypes/generic.py @@ -0,0 +1,388 @@ +""" +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.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 + fields. + """ + + def __init__(self, ct_field="content_type", fk_field="object_id"): + self.ct_field = ct_field + 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 + + # For some reason I don't totally understand, using weakrefs here doesn't work. + 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, **_kwargs): + """ + Handles initializing an object with the generic FK instaed of + content-type/object-id fields. + """ + if self.name in kwargs: + value = kwargs.pop(self.name) + kwargs[self.ct_field] = self.get_content_type(obj=value) + kwargs[self.fk_field] = value._get_pk_val() + + def get_content_type(self, obj=None, id=None): + # Convenience function using get_model avoids a circular import when + # using this model + ContentType = get_model("contenttypes", "contenttype") + if obj: + return ContentType.objects.get_for_model(obj) + elif id: + return ContentType.objects.get_for_id(id) + else: + # This should never happen. I love comments like this, don't you? + raise Exception("Impossible arguments to GFK.get_content_type!") + + def __get__(self, instance, instance_type=None): + if instance is None: + raise AttributeError, u"%s must be accessed via instance" % self.name + + try: + return getattr(instance, self.cache_attr) + except AttributeError: + rel_obj = None + + # Make sure to use ContentType.objects.get_for_id() to ensure that + # lookups are cached (see ticket #5570). This takes more code than + # the naive ``getattr(instance, self.ct_field)``, but has better + # performance when dealing with GFKs in loops and such. + f = self.model._meta.get_field(self.ct_field) + ct_id = getattr(instance, f.get_attname(), None) + if ct_id: + ct = self.get_content_type(id=ct_id) + try: + rel_obj = ct.get_object_for_this_type(pk=getattr(instance, self.fk_field)) + except ObjectDoesNotExist: + pass + setattr(instance, self.cache_attr, rel_obj) + return rel_obj + + def __set__(self, instance, value): + if instance is None: + raise AttributeError, u"%s must be accessed via instance" % self.related.opts.object_name + + ct = None + fk = None + if value is not None: + ct = self.get_content_type(obj=value) + fk = value._get_pk_val() + + setattr(instance, self.ct_field, ct) + setattr(instance, self.fk_field, fk) + setattr(instance, self.cache_attr, value) + +class GenericRelation(RelatedField, Field): + """Provides an accessor to generic related objects (e.g. comments)""" + + def __init__(self, to, **kwargs): + kwargs['verbose_name'] = kwargs.get('verbose_name', None) + kwargs['rel'] = GenericRel(to, + related_name=kwargs.pop('related_name', None), + 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") + + kwargs['blank'] = True + kwargs['editable'] = False + 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 m2m_db_table(self): + return self.rel.to._meta.db_table + + def m2m_column_name(self): + return self.object_id_field_name + + def m2m_reverse_name(self): + return self.model._meta.pk.column + + def contribute_to_class(self, cls, name): + super(GenericRelation, self).contribute_to_class(cls, name) + + # Save a reference to which model this class is on for future use + self.model = cls + + # Add the descriptor for the m2m relation + setattr(cls, self.name, ReverseGenericRelatedObjectsDescriptor(self)) + + def contribute_to_related_class(self, cls, related): + pass + + def set_attributes_from_rel(self): + pass + + def get_internal_type(self): + return "ManyToManyField" + + def db_type(self): + # Since we're simulating a ManyToManyField, in effect, best return the + # same db_type as well. + return None + +class ReverseGenericRelatedObjectsDescriptor(object): + """ + This class provides the functionality that makes the related-object + managers available as attributes on a model class, for fields that have + multiple "remote" values and have a GenericRelation defined in their model + (rather than having another model pointed *at* them). In the example + "article.publications", the publications attribute is a + ReverseGenericRelatedObjectsDescriptor instance. + """ + def __init__(self, field): + self.field = field + + def __get__(self, instance, instance_type=None): + if instance is None: + raise AttributeError, "Manager must be accessed via instance" + + # This import is done here to avoid circular import importing this module + from django.contrib.contenttypes.models import ContentType + + # Dynamically create a class that subclasses the related model's + # default manager. + rel_model = self.field.rel.to + superclass = rel_model._default_manager.__class__ + RelatedManager = create_generic_related_manager(superclass) + + qn = connection.ops.quote_name + + manager = RelatedManager( + model = rel_model, + instance = instance, + symmetrical = (self.field.rel.symmetrical and instance.__class__ == rel_model), + join_table = qn(self.field.m2m_db_table()), + source_col_name = qn(self.field.m2m_column_name()), + target_col_name = qn(self.field.m2m_reverse_name()), + content_type = ContentType.objects.get_for_model(self.field.model), + content_type_field_name = self.field.content_type_field_name, + object_id_field_name = self.field.object_id_field_name + ) + + return manager + + def __set__(self, instance, value): + if instance is None: + raise AttributeError, "Manager must be accessed via instance" + + manager = self.__get__(instance) + manager.clear() + for obj in value: + manager.add(obj) + +def create_generic_related_manager(superclass): + """ + Factory function for a manager that subclasses 'superclass' (which is a + Manager) and adds behavior for generic related objects. + """ + + class GenericRelatedObjectManager(superclass): + def __init__(self, model=None, core_filters=None, instance=None, symmetrical=None, + join_table=None, source_col_name=None, target_col_name=None, content_type=None, + content_type_field_name=None, object_id_field_name=None): + + super(GenericRelatedObjectManager, self).__init__() + self.core_filters = core_filters or {} + self.model = model + self.content_type = content_type + self.symmetrical = symmetrical + self.instance = instance + self.join_table = join_table + self.join_table = model._meta.db_table + self.source_col_name = source_col_name + self.target_col_name = target_col_name + self.content_type_field_name = content_type_field_name + self.object_id_field_name = object_id_field_name + self.pk_val = self.instance._get_pk_val() + + def get_query_set(self): + query = { + '%s__pk' % self.content_type_field_name : self.content_type.id, + '%s__exact' % self.object_id_field_name : self.pk_val, + } + return superclass.get_query_set(self).filter(**query) + + def add(self, *objs): + for obj in objs: + setattr(obj, self.content_type_field_name, self.content_type) + setattr(obj, self.object_id_field_name, self.pk_val) + obj.save() + add.alters_data = True + + def remove(self, *objs): + for obj in objs: + obj.delete() + remove.alters_data = True + + def clear(self): + for obj in self.all(): + obj.delete() + clear.alters_data = True + + 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 + create.alters_data = True + + return GenericRelatedObjectManager + +class GenericRel(ManyToManyRel): + def __init__(self, to, related_name=None, limit_choices_to=None, symmetrical=True): + self.to = to + self.related_name = related_name + self.limit_choices_to = limit_choices_to or {} + self.edit_inline = False + self.symmetrical = symmetrical + self.multiple = True + +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' + |