~ubuntu-branches/ubuntu/quantal/python-django/quantal

« back to all changes in this revision

Viewing changes to django/contrib/contenttypes/generic.py

  • Committer: Bazaar Package Importer
  • Author(s): Scott James Remnant, Eddy Mulyono
  • Date: 2008-09-16 12:18:47 UTC
  • mfrom: (1.1.5 upstream) (4.1.1 lenny)
  • Revision ID: james.westby@ubuntu.com-20080916121847-mg225rg5mnsdqzr0
Tags: 1.0-1ubuntu1
* Merge from Debian (LP: #264191), remaining changes:
  - Run test suite on build.

[Eddy Mulyono]
* Update patch to workaround network test case failures.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
"""
 
2
Classes allowing "generic" relations through ContentType and object-id fields.
 
3
"""
 
4
 
 
5
from django.core.exceptions import ObjectDoesNotExist
 
6
from django.db import connection
 
7
from django.db.models import signals
 
8
from django.db import models
 
9
from django.db.models.fields.related import RelatedField, Field, ManyToManyRel
 
10
from django.db.models.loading import get_model
 
11
from django.forms import ModelForm
 
12
from django.forms.models import BaseModelFormSet, modelformset_factory, save_instance
 
13
from django.contrib.admin.options import InlineModelAdmin, flatten_fieldsets
 
14
from django.utils.encoding import smart_unicode
 
15
 
 
16
class GenericForeignKey(object):
 
17
    """
 
18
    Provides a generic relation to any object through content-type/object-id
 
19
    fields.
 
20
    """
 
21
 
 
22
    def __init__(self, ct_field="content_type", fk_field="object_id"):
 
23
        self.ct_field = ct_field
 
24
        self.fk_field = fk_field
 
25
 
 
26
    def contribute_to_class(self, cls, name):
 
27
        self.name = name
 
28
        self.model = cls
 
29
        self.cache_attr = "_%s_cache" % name
 
30
        cls._meta.add_virtual_field(self)
 
31
 
 
32
        # For some reason I don't totally understand, using weakrefs here doesn't work.
 
33
        signals.pre_init.connect(self.instance_pre_init, sender=cls, weak=False)
 
34
 
 
35
        # Connect myself as the descriptor for this field
 
36
        setattr(cls, name, self)
 
37
 
 
38
    def instance_pre_init(self, signal, sender, args, kwargs, **_kwargs):
 
39
        """
 
40
        Handles initializing an object with the generic FK instaed of
 
41
        content-type/object-id fields.
 
42
        """
 
43
        if self.name in kwargs:
 
44
            value = kwargs.pop(self.name)
 
45
            kwargs[self.ct_field] = self.get_content_type(obj=value)
 
46
            kwargs[self.fk_field] = value._get_pk_val()
 
47
 
 
48
    def get_content_type(self, obj=None, id=None):
 
49
        # Convenience function using get_model avoids a circular import when
 
50
        # using this model
 
51
        ContentType = get_model("contenttypes", "contenttype")
 
52
        if obj:
 
53
            return ContentType.objects.get_for_model(obj)
 
54
        elif id:
 
55
            return ContentType.objects.get_for_id(id)
 
56
        else:
 
57
            # This should never happen. I love comments like this, don't you?
 
58
            raise Exception("Impossible arguments to GFK.get_content_type!")
 
59
 
 
60
    def __get__(self, instance, instance_type=None):
 
61
        if instance is None:
 
62
            raise AttributeError, u"%s must be accessed via instance" % self.name
 
63
 
 
64
        try:
 
65
            return getattr(instance, self.cache_attr)
 
66
        except AttributeError:
 
67
            rel_obj = None
 
68
 
 
69
            # Make sure to use ContentType.objects.get_for_id() to ensure that
 
70
            # lookups are cached (see ticket #5570). This takes more code than
 
71
            # the naive ``getattr(instance, self.ct_field)``, but has better
 
72
            # performance when dealing with GFKs in loops and such.
 
73
            f = self.model._meta.get_field(self.ct_field)
 
74
            ct_id = getattr(instance, f.get_attname(), None)
 
75
            if ct_id:
 
76
                ct = self.get_content_type(id=ct_id)
 
77
                try:
 
78
                    rel_obj = ct.get_object_for_this_type(pk=getattr(instance, self.fk_field))
 
79
                except ObjectDoesNotExist:
 
80
                    pass
 
81
            setattr(instance, self.cache_attr, rel_obj)
 
82
            return rel_obj
 
83
 
 
84
    def __set__(self, instance, value):
 
85
        if instance is None:
 
86
            raise AttributeError, u"%s must be accessed via instance" % self.related.opts.object_name
 
87
 
 
88
        ct = None
 
89
        fk = None
 
90
        if value is not None:
 
91
            ct = self.get_content_type(obj=value)
 
92
            fk = value._get_pk_val()
 
93
 
 
94
        setattr(instance, self.ct_field, ct)
 
95
        setattr(instance, self.fk_field, fk)
 
96
        setattr(instance, self.cache_attr, value)
 
97
 
 
98
class GenericRelation(RelatedField, Field):
 
99
    """Provides an accessor to generic related objects (e.g. comments)"""
 
100
 
 
101
    def __init__(self, to, **kwargs):
 
102
        kwargs['verbose_name'] = kwargs.get('verbose_name', None)
 
103
        kwargs['rel'] = GenericRel(to,
 
104
                            related_name=kwargs.pop('related_name', None),
 
105
                            limit_choices_to=kwargs.pop('limit_choices_to', None),
 
106
                            symmetrical=kwargs.pop('symmetrical', True))
 
107
 
 
108
        # By its very nature, a GenericRelation doesn't create a table.
 
109
        self.creates_table = False
 
110
 
 
111
        # Override content-type/object-id field names on the related class
 
112
        self.object_id_field_name = kwargs.pop("object_id_field", "object_id")
 
113
        self.content_type_field_name = kwargs.pop("content_type_field", "content_type")
 
114
 
 
115
        kwargs['blank'] = True
 
116
        kwargs['editable'] = False
 
117
        kwargs['serialize'] = False
 
118
        Field.__init__(self, **kwargs)
 
119
 
 
120
    def get_choices_default(self):
 
121
        return Field.get_choices(self, include_blank=False)
 
122
 
 
123
    def value_to_string(self, obj):
 
124
        qs = getattr(obj, self.name).all()
 
125
        return smart_unicode([instance._get_pk_val() for instance in qs])
 
126
 
 
127
    def m2m_db_table(self):
 
128
        return self.rel.to._meta.db_table
 
129
 
 
130
    def m2m_column_name(self):
 
131
        return self.object_id_field_name
 
132
 
 
133
    def m2m_reverse_name(self):
 
134
        return self.model._meta.pk.column
 
135
 
 
136
    def contribute_to_class(self, cls, name):
 
137
        super(GenericRelation, self).contribute_to_class(cls, name)
 
138
 
 
139
        # Save a reference to which model this class is on for future use
 
140
        self.model = cls
 
141
 
 
142
        # Add the descriptor for the m2m relation
 
143
        setattr(cls, self.name, ReverseGenericRelatedObjectsDescriptor(self))
 
144
 
 
145
    def contribute_to_related_class(self, cls, related):
 
146
        pass
 
147
 
 
148
    def set_attributes_from_rel(self):
 
149
        pass
 
150
 
 
151
    def get_internal_type(self):
 
152
        return "ManyToManyField"
 
153
 
 
154
    def db_type(self):
 
155
        # Since we're simulating a ManyToManyField, in effect, best return the
 
156
        # same db_type as well.
 
157
        return None
 
158
 
 
159
    def extra_filters(self, pieces, pos, negate):
 
160
        """
 
161
        Return an extra filter to the queryset so that the results are filtered
 
162
        on the appropriate content type.
 
163
        """
 
164
        if negate:
 
165
            return []
 
166
        ContentType = get_model("contenttypes", "contenttype")
 
167
        content_type = ContentType.objects.get_for_model(self.model)
 
168
        prefix = "__".join(pieces[:pos + 1])
 
169
        return [("%s__%s" % (prefix, self.content_type_field_name),
 
170
            content_type)]
 
171
 
 
172
class ReverseGenericRelatedObjectsDescriptor(object):
 
173
    """
 
174
    This class provides the functionality that makes the related-object
 
175
    managers available as attributes on a model class, for fields that have
 
176
    multiple "remote" values and have a GenericRelation defined in their model
 
177
    (rather than having another model pointed *at* them). In the example
 
178
    "article.publications", the publications attribute is a
 
179
    ReverseGenericRelatedObjectsDescriptor instance.
 
180
    """
 
181
    def __init__(self, field):
 
182
        self.field = field
 
183
 
 
184
    def __get__(self, instance, instance_type=None):
 
185
        if instance is None:
 
186
            raise AttributeError, "Manager must be accessed via instance"
 
187
 
 
188
        # This import is done here to avoid circular import importing this module
 
189
        from django.contrib.contenttypes.models import ContentType
 
190
 
 
191
        # Dynamically create a class that subclasses the related model's
 
192
        # default manager.
 
193
        rel_model = self.field.rel.to
 
194
        superclass = rel_model._default_manager.__class__
 
195
        RelatedManager = create_generic_related_manager(superclass)
 
196
 
 
197
        qn = connection.ops.quote_name
 
198
 
 
199
        manager = RelatedManager(
 
200
            model = rel_model,
 
201
            instance = instance,
 
202
            symmetrical = (self.field.rel.symmetrical and instance.__class__ == rel_model),
 
203
            join_table = qn(self.field.m2m_db_table()),
 
204
            source_col_name = qn(self.field.m2m_column_name()),
 
205
            target_col_name = qn(self.field.m2m_reverse_name()),
 
206
            content_type = ContentType.objects.get_for_model(self.field.model),
 
207
            content_type_field_name = self.field.content_type_field_name,
 
208
            object_id_field_name = self.field.object_id_field_name
 
209
        )
 
210
 
 
211
        return manager
 
212
 
 
213
    def __set__(self, instance, value):
 
214
        if instance is None:
 
215
            raise AttributeError, "Manager must be accessed via instance"
 
216
 
 
217
        manager = self.__get__(instance)
 
218
        manager.clear()
 
219
        for obj in value:
 
220
            manager.add(obj)
 
221
 
 
222
def create_generic_related_manager(superclass):
 
223
    """
 
224
    Factory function for a manager that subclasses 'superclass' (which is a
 
225
    Manager) and adds behavior for generic related objects.
 
226
    """
 
227
 
 
228
    class GenericRelatedObjectManager(superclass):
 
229
        def __init__(self, model=None, core_filters=None, instance=None, symmetrical=None,
 
230
                     join_table=None, source_col_name=None, target_col_name=None, content_type=None,
 
231
                     content_type_field_name=None, object_id_field_name=None):
 
232
 
 
233
            super(GenericRelatedObjectManager, self).__init__()
 
234
            self.core_filters = core_filters or {}
 
235
            self.model = model
 
236
            self.content_type = content_type
 
237
            self.symmetrical = symmetrical
 
238
            self.instance = instance
 
239
            self.join_table = join_table
 
240
            self.join_table = model._meta.db_table
 
241
            self.source_col_name = source_col_name
 
242
            self.target_col_name = target_col_name
 
243
            self.content_type_field_name = content_type_field_name
 
244
            self.object_id_field_name = object_id_field_name
 
245
            self.pk_val = self.instance._get_pk_val()
 
246
 
 
247
        def get_query_set(self):
 
248
            query = {
 
249
                '%s__pk' % self.content_type_field_name : self.content_type.id,
 
250
                '%s__exact' % self.object_id_field_name : self.pk_val,
 
251
            }
 
252
            return superclass.get_query_set(self).filter(**query)
 
253
 
 
254
        def add(self, *objs):
 
255
            for obj in objs:
 
256
                setattr(obj, self.content_type_field_name, self.content_type)
 
257
                setattr(obj, self.object_id_field_name, self.pk_val)
 
258
                obj.save()
 
259
        add.alters_data = True
 
260
 
 
261
        def remove(self, *objs):
 
262
            for obj in objs:
 
263
                obj.delete()
 
264
        remove.alters_data = True
 
265
 
 
266
        def clear(self):
 
267
            for obj in self.all():
 
268
                obj.delete()
 
269
        clear.alters_data = True
 
270
 
 
271
        def create(self, **kwargs):
 
272
            kwargs[self.content_type_field_name] = self.content_type
 
273
            kwargs[self.object_id_field_name] = self.pk_val
 
274
            return super(GenericRelatedObjectManager, self).create(**kwargs)
 
275
        create.alters_data = True
 
276
 
 
277
    return GenericRelatedObjectManager
 
278
 
 
279
class GenericRel(ManyToManyRel):
 
280
    def __init__(self, to, related_name=None, limit_choices_to=None, symmetrical=True):
 
281
        self.to = to
 
282
        self.related_name = related_name
 
283
        self.limit_choices_to = limit_choices_to or {}
 
284
        self.symmetrical = symmetrical
 
285
        self.multiple = True
 
286
 
 
287
class 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
 
 
326
def 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
 
 
360
class 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
 
 
383
class GenericStackedInline(GenericInlineModelAdmin):
 
384
    template = 'admin/edit_inline/stacked.html'
 
385
 
 
386
class GenericTabularInline(GenericInlineModelAdmin):
 
387
    template = 'admin/edit_inline/tabular.html'