Fixed #3906 -- Fixed the reverse_m2m_name for a generic relation. Refs #2749.
[django.git] / django / contrib / contenttypes / generic.py
blobe496ed1af8304b21c4391cffd282772e67119265
1 """
2 Classes allowing "generic" relations through ContentType and object-id fields.
3 """
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
14 class GenericForeignKey(object):
15 """
16 Provides a generic relation to any object through content-type/object-id
17 fields.
18 """
20 def __init__(self, ct_field="content_type", fk_field="object_id"):
21 self.ct_field = ct_field
22 self.fk_field = fk_field
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
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)
34 # Connect myself as the descriptor for this field
35 setattr(cls, name, self)
37 def instance_pre_init(self, signal, sender, args, kwargs):
38 # Handle initalizing an object with the generic FK instaed of
39 # content-type/object-id fields.
40 if self.name in kwargs:
41 value = kwargs.pop(self.name)
42 kwargs[self.ct_field] = self.get_content_type(value)
43 kwargs[self.fk_field] = value._get_pk_val()
45 def get_content_type(self, obj):
46 # Convenience function using get_model avoids a circular import when using this model
47 ContentType = get_model("contenttypes", "contenttype")
48 return ContentType.objects.get_for_model(obj)
50 def __get__(self, instance, instance_type=None):
51 if instance is None:
52 raise AttributeError, u"%s must be accessed via instance" % self.name
54 try:
55 return getattr(instance, self.cache_attr)
56 except AttributeError:
57 rel_obj = None
58 ct = getattr(instance, self.ct_field)
59 if ct:
60 try:
61 rel_obj = ct.get_object_for_this_type(pk=getattr(instance, self.fk_field))
62 except ObjectDoesNotExist:
63 pass
64 setattr(instance, self.cache_attr, rel_obj)
65 return rel_obj
67 def __set__(self, instance, value):
68 if instance is None:
69 raise AttributeError, u"%s must be accessed via instance" % self.related.opts.object_name
71 ct = None
72 fk = None
73 if value is not None:
74 ct = self.get_content_type(value)
75 fk = value._get_pk_val()
77 setattr(instance, self.ct_field, ct)
78 setattr(instance, self.fk_field, fk)
79 setattr(instance, self.cache_attr, value)
81 class GenericRelation(RelatedField, Field):
82 """Provides an accessor to generic related objects (i.e. comments)"""
84 def __init__(self, to, **kwargs):
85 kwargs['verbose_name'] = kwargs.get('verbose_name', None)
86 kwargs['rel'] = GenericRel(to,
87 related_name=kwargs.pop('related_name', None),
88 limit_choices_to=kwargs.pop('limit_choices_to', None),
89 symmetrical=kwargs.pop('symmetrical', True))
91 # Override content-type/object-id field names on the related class
92 self.object_id_field_name = kwargs.pop("object_id_field", "object_id")
93 self.content_type_field_name = kwargs.pop("content_type_field", "content_type")
95 kwargs['blank'] = True
96 kwargs['editable'] = False
97 kwargs['serialize'] = False
98 Field.__init__(self, **kwargs)
100 def get_manipulator_field_objs(self):
101 choices = self.get_choices_default()
102 return [curry(oldforms.SelectMultipleField, size=min(max(len(choices), 5), 15), choices=choices)]
104 def get_choices_default(self):
105 return Field.get_choices(self, include_blank=False)
107 def flatten_data(self, follow, obj = None):
108 new_data = {}
109 if obj:
110 instance_ids = [instance._get_pk_val() for instance in getattr(obj, self.name).all()]
111 new_data[self.name] = instance_ids
112 return new_data
114 def m2m_db_table(self):
115 return self.rel.to._meta.db_table
117 def m2m_column_name(self):
118 return self.object_id_field_name
120 def m2m_reverse_name(self):
121 return self.model._meta.pk.column
123 def contribute_to_class(self, cls, name):
124 super(GenericRelation, self).contribute_to_class(cls, name)
126 # Save a reference to which model this class is on for future use
127 self.model = cls
129 # Add the descriptor for the m2m relation
130 setattr(cls, self.name, ReverseGenericRelatedObjectsDescriptor(self))
132 def contribute_to_related_class(self, cls, related):
133 pass
135 def set_attributes_from_rel(self):
136 pass
138 def get_internal_type(self):
139 return "ManyToManyField"
141 class ReverseGenericRelatedObjectsDescriptor(object):
143 This class provides the functionality that makes the related-object
144 managers available as attributes on a model class, for fields that have
145 multiple "remote" values and have a GenericRelation defined in their model
146 (rather than having another model pointed *at* them). In the example
147 "article.publications", the publications attribute is a
148 ReverseGenericRelatedObjectsDescriptor instance.
150 def __init__(self, field):
151 self.field = field
153 def __get__(self, instance, instance_type=None):
154 if instance is None:
155 raise AttributeError, "Manager must be accessed via instance"
157 # This import is done here to avoid circular import importing this module
158 from django.contrib.contenttypes.models import ContentType
160 # Dynamically create a class that subclasses the related model's
161 # default manager.
162 rel_model = self.field.rel.to
163 superclass = rel_model._default_manager.__class__
164 RelatedManager = create_generic_related_manager(superclass)
166 qn = connection.ops.quote_name
168 manager = RelatedManager(
169 model = rel_model,
170 instance = instance,
171 symmetrical = (self.field.rel.symmetrical and instance.__class__ == rel_model),
172 join_table = qn(self.field.m2m_db_table()),
173 source_col_name = qn(self.field.m2m_column_name()),
174 target_col_name = qn(self.field.m2m_reverse_name()),
175 content_type = ContentType.objects.get_for_model(self.field.model),
176 content_type_field_name = self.field.content_type_field_name,
177 object_id_field_name = self.field.object_id_field_name
180 return manager
182 def __set__(self, instance, value):
183 if instance is None:
184 raise AttributeError, "Manager must be accessed via instance"
186 manager = self.__get__(instance)
187 manager.clear()
188 for obj in value:
189 manager.add(obj)
191 def create_generic_related_manager(superclass):
193 Factory function for a manager that subclasses 'superclass' (which is a
194 Manager) and adds behavior for generic related objects.
197 class GenericRelatedObjectManager(superclass):
198 def __init__(self, model=None, core_filters=None, instance=None, symmetrical=None,
199 join_table=None, source_col_name=None, target_col_name=None, content_type=None,
200 content_type_field_name=None, object_id_field_name=None):
202 super(GenericRelatedObjectManager, self).__init__()
203 self.core_filters = core_filters or {}
204 self.model = model
205 self.content_type = content_type
206 self.symmetrical = symmetrical
207 self.instance = instance
208 self.join_table = join_table
209 self.join_table = model._meta.db_table
210 self.source_col_name = source_col_name
211 self.target_col_name = target_col_name
212 self.content_type_field_name = content_type_field_name
213 self.object_id_field_name = object_id_field_name
214 self.pk_val = self.instance._get_pk_val()
216 def get_query_set(self):
217 query = {
218 '%s__pk' % self.content_type_field_name : self.content_type.id,
219 '%s__exact' % self.object_id_field_name : self.pk_val,
221 return superclass.get_query_set(self).filter(**query)
223 def add(self, *objs):
224 for obj in objs:
225 setattr(obj, self.content_type_field_name, self.content_type)
226 setattr(obj, self.object_id_field_name, self.pk_val)
227 obj.save()
228 add.alters_data = True
230 def remove(self, *objs):
231 for obj in objs:
232 obj.delete()
233 remove.alters_data = True
235 def clear(self):
236 for obj in self.all():
237 obj.delete()
238 clear.alters_data = True
240 def create(self, **kwargs):
241 kwargs[self.content_type_field_name] = self.content_type
242 kwargs[self.object_id_field_name] = self.pk_val
243 obj = self.model(**kwargs)
244 obj.save()
245 return obj
246 create.alters_data = True
248 return GenericRelatedObjectManager
250 class GenericRel(ManyToManyRel):
251 def __init__(self, to, related_name=None, limit_choices_to=None, symmetrical=True):
252 self.to = to
253 self.num_in_admin = 0
254 self.related_name = related_name
255 self.filter_interface = None
256 self.limit_choices_to = limit_choices_to or {}
257 self.edit_inline = False
258 self.raw_id_admin = False
259 self.symmetrical = symmetrical
260 self.multiple = True
261 assert not (self.raw_id_admin and self.filter_interface), \
262 "Generic relations may not use both raw_id_admin and filter_interface"