Django

Code

root/django/branches/newforms-admin/django/contrib/contenttypes/generic.py

Revision 7479, 10.7 kB (checked in by brosner, 7 months ago)

newforms-admin: Merged from trunk up to [7478]

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