PageRenderTime 47ms CodeModel.GetById 20ms app.highlight 22ms RepoModel.GetById 1ms app.codeStats 1ms

/django/contrib/contenttypes/generic.py

https://code.google.com/p/mango-py/
Python | 432 lines | 431 code | 0 blank | 1 comment | 1 complexity | 4385e3f588f972cd5ab0cf6c0f051efe MD5 | raw file
  1"""
  2Classes allowing "generic" relations through ContentType and object-id fields.
  3"""
  4
  5from django.core.exceptions import ObjectDoesNotExist
  6from django.db import connection
  7from django.db.models import signals
  8from django.db import models, router, DEFAULT_DB_ALIAS
  9from django.db.models.fields.related import RelatedField, Field, ManyToManyRel
 10from django.db.models.loading import get_model
 11from django.forms import ModelForm
 12from django.forms.models import BaseModelFormSet, modelformset_factory, save_instance
 13from django.contrib.admin.options import InlineModelAdmin, flatten_fieldsets
 14from django.utils.encoding import smart_unicode
 15from django.utils.functional import curry
 16
 17from django.contrib.contenttypes.models import ContentType
 18
 19
 20class GenericForeignKey(object):
 21    """
 22    Provides a generic relation to any object through content-type/object-id
 23    fields.
 24    """
 25
 26    def __init__(self, ct_field="content_type", fk_field="object_id"):
 27        self.ct_field = ct_field
 28        self.fk_field = fk_field
 29
 30    def contribute_to_class(self, cls, name):
 31        self.name = name
 32        self.model = cls
 33        self.cache_attr = "_%s_cache" % name
 34        cls._meta.add_virtual_field(self)
 35
 36        # For some reason I don't totally understand, using weakrefs here doesn't work.
 37        signals.pre_init.connect(self.instance_pre_init, sender=cls, weak=False)
 38
 39        # Connect myself as the descriptor for this field
 40        setattr(cls, name, self)
 41
 42    def instance_pre_init(self, signal, sender, args, kwargs, **_kwargs):
 43        """
 44        Handles initializing an object with the generic FK instaed of
 45        content-type/object-id fields.
 46        """
 47        if self.name in kwargs:
 48            value = kwargs.pop(self.name)
 49            kwargs[self.ct_field] = self.get_content_type(obj=value)
 50            kwargs[self.fk_field] = value._get_pk_val()
 51
 52    def get_content_type(self, obj=None, id=None, using=None):
 53        # Convenience function using get_model avoids a circular import when
 54        # using this model
 55        ContentType = get_model("contenttypes", "contenttype")
 56        if obj:
 57             return ContentType.objects.db_manager(obj._state.db).get_for_model(obj)
 58        elif id:
 59             return ContentType.objects.db_manager(using).get_for_id(id)
 60        else:
 61            # This should never happen. I love comments like this, don't you?
 62            raise Exception("Impossible arguments to GFK.get_content_type!")
 63
 64    def __get__(self, instance, instance_type=None):
 65        if instance is None:
 66            return self
 67
 68        try:
 69            return getattr(instance, self.cache_attr)
 70        except AttributeError:
 71            rel_obj = None
 72
 73            # Make sure to use ContentType.objects.get_for_id() to ensure that
 74            # lookups are cached (see ticket #5570). This takes more code than
 75            # the naive ``getattr(instance, self.ct_field)``, but has better
 76            # performance when dealing with GFKs in loops and such.
 77            f = self.model._meta.get_field(self.ct_field)
 78            ct_id = getattr(instance, f.get_attname(), None)
 79            if ct_id:
 80                ct = self.get_content_type(id=ct_id, using=instance._state.db)
 81                try:
 82                    rel_obj = ct.get_object_for_this_type(pk=getattr(instance, self.fk_field))
 83                except ObjectDoesNotExist:
 84                    pass
 85            setattr(instance, self.cache_attr, rel_obj)
 86            return rel_obj
 87
 88    def __set__(self, instance, value):
 89        if instance is None:
 90            raise AttributeError(u"%s must be accessed via instance" % self.related.opts.object_name)
 91
 92        ct = None
 93        fk = None
 94        if value is not None:
 95            ct = self.get_content_type(obj=value)
 96            fk = value._get_pk_val()
 97
 98        setattr(instance, self.ct_field, ct)
 99        setattr(instance, self.fk_field, fk)
100        setattr(instance, self.cache_attr, value)
101
102class GenericRelation(RelatedField, Field):
103    """Provides an accessor to generic related objects (e.g. comments)"""
104
105    def __init__(self, to, **kwargs):
106        kwargs['verbose_name'] = kwargs.get('verbose_name', None)
107        kwargs['rel'] = GenericRel(to,
108                            related_name=kwargs.pop('related_name', None),
109                            limit_choices_to=kwargs.pop('limit_choices_to', None),
110                            symmetrical=kwargs.pop('symmetrical', True))
111
112
113        # Override content-type/object-id field names on the related class
114        self.object_id_field_name = kwargs.pop("object_id_field", "object_id")
115        self.content_type_field_name = kwargs.pop("content_type_field", "content_type")
116
117        kwargs['blank'] = True
118        kwargs['editable'] = False
119        kwargs['serialize'] = False
120        Field.__init__(self, **kwargs)
121
122    def get_choices_default(self):
123        return Field.get_choices(self, include_blank=False)
124
125    def value_to_string(self, obj):
126        qs = getattr(obj, self.name).all()
127        return smart_unicode([instance._get_pk_val() for instance in qs])
128
129    def m2m_db_table(self):
130        return self.rel.to._meta.db_table
131
132    def m2m_column_name(self):
133        return self.object_id_field_name
134
135    def m2m_reverse_name(self):
136        return self.rel.to._meta.pk.column
137
138    def m2m_target_field_name(self):
139        return self.model._meta.pk.name
140
141    def m2m_reverse_target_field_name(self):
142        return self.rel.to._meta.pk.name
143
144    def contribute_to_class(self, cls, name):
145        super(GenericRelation, self).contribute_to_class(cls, name)
146
147        # Save a reference to which model this class is on for future use
148        self.model = cls
149
150        # Add the descriptor for the m2m relation
151        setattr(cls, self.name, ReverseGenericRelatedObjectsDescriptor(self))
152
153    def contribute_to_related_class(self, cls, related):
154        pass
155
156    def set_attributes_from_rel(self):
157        pass
158
159    def get_internal_type(self):
160        return "ManyToManyField"
161
162    def db_type(self, connection):
163        # Since we're simulating a ManyToManyField, in effect, best return the
164        # same db_type as well.
165        return None
166
167    def extra_filters(self, pieces, pos, negate):
168        """
169        Return an extra filter to the queryset so that the results are filtered
170        on the appropriate content type.
171        """
172        if negate:
173            return []
174        ContentType = get_model("contenttypes", "contenttype")
175        content_type = ContentType.objects.get_for_model(self.model)
176        prefix = "__".join(pieces[:pos + 1])
177        return [("%s__%s" % (prefix, self.content_type_field_name),
178            content_type)]
179
180    def bulk_related_objects(self, objs, using=DEFAULT_DB_ALIAS):
181        """
182        Return all objects related to ``objs`` via this ``GenericRelation``.
183
184        """
185        return self.rel.to._base_manager.db_manager(using).filter(**{
186                "%s__pk" % self.content_type_field_name:
187                    ContentType.objects.db_manager(using).get_for_model(self.model).pk,
188                "%s__in" % self.object_id_field_name:
189                    [obj.pk for obj in objs]
190                })
191
192
193class ReverseGenericRelatedObjectsDescriptor(object):
194    """
195    This class provides the functionality that makes the related-object
196    managers available as attributes on a model class, for fields that have
197    multiple "remote" values and have a GenericRelation defined in their model
198    (rather than having another model pointed *at* them). In the example
199    "article.publications", the publications attribute is a
200    ReverseGenericRelatedObjectsDescriptor instance.
201    """
202    def __init__(self, field):
203        self.field = field
204
205    def __get__(self, instance, instance_type=None):
206        if instance is None:
207            return self
208
209        # This import is done here to avoid circular import importing this module
210        from django.contrib.contenttypes.models import ContentType
211
212        # Dynamically create a class that subclasses the related model's
213        # default manager.
214        rel_model = self.field.rel.to
215        superclass = rel_model._default_manager.__class__
216        RelatedManager = create_generic_related_manager(superclass)
217
218        qn = connection.ops.quote_name
219
220        manager = RelatedManager(
221            model = rel_model,
222            instance = instance,
223            symmetrical = (self.field.rel.symmetrical and instance.__class__ == rel_model),
224            join_table = qn(self.field.m2m_db_table()),
225            source_col_name = qn(self.field.m2m_column_name()),
226            target_col_name = qn(self.field.m2m_reverse_name()),
227            content_type = ContentType.objects.db_manager(instance._state.db).get_for_model(instance),
228            content_type_field_name = self.field.content_type_field_name,
229            object_id_field_name = self.field.object_id_field_name
230        )
231
232        return manager
233
234    def __set__(self, instance, value):
235        if instance is None:
236            raise AttributeError("Manager must be accessed via instance")
237
238        manager = self.__get__(instance)
239        manager.clear()
240        for obj in value:
241            manager.add(obj)
242
243def create_generic_related_manager(superclass):
244    """
245    Factory function for a manager that subclasses 'superclass' (which is a
246    Manager) and adds behavior for generic related objects.
247    """
248
249    class GenericRelatedObjectManager(superclass):
250        def __init__(self, model=None, core_filters=None, instance=None, symmetrical=None,
251                     join_table=None, source_col_name=None, target_col_name=None, content_type=None,
252                     content_type_field_name=None, object_id_field_name=None):
253
254            super(GenericRelatedObjectManager, self).__init__()
255            self.core_filters = core_filters or {}
256            self.model = model
257            self.content_type = content_type
258            self.symmetrical = symmetrical
259            self.instance = instance
260            self.join_table = join_table
261            self.join_table = model._meta.db_table
262            self.source_col_name = source_col_name
263            self.target_col_name = target_col_name
264            self.content_type_field_name = content_type_field_name
265            self.object_id_field_name = object_id_field_name
266            self.pk_val = self.instance._get_pk_val()
267
268        def get_query_set(self):
269            db = self._db or router.db_for_read(self.model, instance=self.instance)
270            query = {
271                '%s__pk' % self.content_type_field_name : self.content_type.id,
272                '%s__exact' % self.object_id_field_name : self.pk_val,
273            }
274            return superclass.get_query_set(self).using(db).filter(**query)
275
276        def add(self, *objs):
277            for obj in objs:
278                if not isinstance(obj, self.model):
279                    raise TypeError("'%s' instance expected" % self.model._meta.object_name)
280                setattr(obj, self.content_type_field_name, self.content_type)
281                setattr(obj, self.object_id_field_name, self.pk_val)
282                obj.save()
283        add.alters_data = True
284
285        def remove(self, *objs):
286            db = router.db_for_write(self.model, instance=self.instance)
287            for obj in objs:
288                obj.delete(using=db)
289        remove.alters_data = True
290
291        def clear(self):
292            db = router.db_for_write(self.model, instance=self.instance)
293            for obj in self.all():
294                obj.delete(using=db)
295        clear.alters_data = True
296
297        def create(self, **kwargs):
298            kwargs[self.content_type_field_name] = self.content_type
299            kwargs[self.object_id_field_name] = self.pk_val
300            db = router.db_for_write(self.model, instance=self.instance)
301            return super(GenericRelatedObjectManager, self).using(db).create(**kwargs)
302        create.alters_data = True
303
304    return GenericRelatedObjectManager
305
306class GenericRel(ManyToManyRel):
307    def __init__(self, to, related_name=None, limit_choices_to=None, symmetrical=True):
308        self.to = to
309        self.related_name = related_name
310        self.limit_choices_to = limit_choices_to or {}
311        self.symmetrical = symmetrical
312        self.multiple = True
313        self.through = None
314
315class BaseGenericInlineFormSet(BaseModelFormSet):
316    """
317    A formset for generic inline objects to a parent.
318    """
319
320    def __init__(self, data=None, files=None, instance=None, save_as_new=None,
321                 prefix=None, queryset=None):
322        # Avoid a circular import.
323        from django.contrib.contenttypes.models import ContentType
324        opts = self.model._meta
325        self.instance = instance
326        self.rel_name = '-'.join((
327            opts.app_label, opts.object_name.lower(),
328            self.ct_field.name, self.ct_fk_field.name,
329        ))
330        if self.instance is None or self.instance.pk is None:
331            qs = self.model._default_manager.none()
332        else:
333            if queryset is None:
334                queryset = self.model._default_manager
335            qs = queryset.filter(**{
336                self.ct_field.name: ContentType.objects.get_for_model(self.instance),
337                self.ct_fk_field.name: self.instance.pk,
338            })
339        super(BaseGenericInlineFormSet, self).__init__(
340            queryset=qs, data=data, files=files,
341            prefix=prefix
342        )
343
344    #@classmethod
345    def get_default_prefix(cls):
346        opts = cls.model._meta
347        return '-'.join((opts.app_label, opts.object_name.lower(),
348                        cls.ct_field.name, cls.ct_fk_field.name,
349        ))
350    get_default_prefix = classmethod(get_default_prefix)
351
352    def save_new(self, form, commit=True):
353        # Avoid a circular import.
354        from django.contrib.contenttypes.models import ContentType
355        kwargs = {
356            self.ct_field.get_attname(): ContentType.objects.get_for_model(self.instance).pk,
357            self.ct_fk_field.get_attname(): self.instance.pk,
358        }
359        new_obj = self.model(**kwargs)
360        return save_instance(form, new_obj, commit=commit)
361
362def generic_inlineformset_factory(model, form=ModelForm,
363                                  formset=BaseGenericInlineFormSet,
364                                  ct_field="content_type", fk_field="object_id",
365                                  fields=None, exclude=None,
366                                  extra=3, can_order=False, can_delete=True,
367                                  max_num=None,
368                                  formfield_callback=lambda f: f.formfield()):
369    """
370    Returns an ``GenericInlineFormSet`` for the given kwargs.
371
372    You must provide ``ct_field`` and ``object_id`` if they different from the
373    defaults ``content_type`` and ``object_id`` respectively.
374    """
375    opts = model._meta
376    # Avoid a circular import.
377    from django.contrib.contenttypes.models import ContentType
378    # if there is no field called `ct_field` let the exception propagate
379    ct_field = opts.get_field(ct_field)
380    if not isinstance(ct_field, models.ForeignKey) or ct_field.rel.to != ContentType:
381        raise Exception("fk_name '%s' is not a ForeignKey to ContentType" % ct_field)
382    fk_field = opts.get_field(fk_field) # let the exception propagate
383    if exclude is not None:
384        exclude = list(exclude)
385        exclude.extend([ct_field.name, fk_field.name])
386    else:
387        exclude = [ct_field.name, fk_field.name]
388    FormSet = modelformset_factory(model, form=form,
389                                   formfield_callback=formfield_callback,
390                                   formset=formset,
391                                   extra=extra, can_delete=can_delete, can_order=can_order,
392                                   fields=fields, exclude=exclude, max_num=max_num)
393    FormSet.ct_field = ct_field
394    FormSet.ct_fk_field = fk_field
395    return FormSet
396
397class GenericInlineModelAdmin(InlineModelAdmin):
398    ct_field = "content_type"
399    ct_fk_field = "object_id"
400    formset = BaseGenericInlineFormSet
401
402    def get_formset(self, request, obj=None):
403        if self.declared_fieldsets:
404            fields = flatten_fieldsets(self.declared_fieldsets)
405        else:
406            fields = None
407        if self.exclude is None:
408            exclude = []
409        else:
410            exclude = list(self.exclude)
411        exclude.extend(self.get_readonly_fields(request, obj))
412        exclude = exclude or None
413        defaults = {
414            "ct_field": self.ct_field,
415            "fk_field": self.ct_fk_field,
416            "form": self.form,
417            "formfield_callback": curry(self.formfield_for_dbfield, request=request),
418            "formset": self.formset,
419            "extra": self.extra,
420            "can_delete": self.can_delete,
421            "can_order": False,
422            "fields": fields,
423            "max_num": self.max_num,
424            "exclude": exclude
425        }
426        return generic_inlineformset_factory(self.model, **defaults)
427
428class GenericStackedInline(GenericInlineModelAdmin):
429    template = 'admin/edit_inline/stacked.html'
430
431class GenericTabularInline(GenericInlineModelAdmin):
432    template = 'admin/edit_inline/tabular.html'