2 Classes allowing "generic" relations through ContentType and object-id fields.
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):
16 Provides a generic relation to any object through content-type/object-id
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)
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):
52 raise AttributeError, u
"%s must be accessed via instance" % self
.name
55 return getattr(instance
, self
.cache_attr
)
56 except AttributeError:
58 ct
= getattr(instance
, self
.ct_field
)
61 rel_obj
= ct
.get_object_for_this_type(pk
=getattr(instance
, self
.fk_field
))
62 except ObjectDoesNotExist
:
64 setattr(instance
, self
.cache_attr
, rel_obj
)
67 def __set__(self
, instance
, value
):
69 raise AttributeError, u
"%s must be accessed via instance" % self
.related
.opts
.object_name
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):
110 instance_ids
= [instance
._get
_pk
_val
() for instance
in getattr(obj
, self
.name
).all()]
111 new_data
[self
.name
] = instance_ids
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
129 # Add the descriptor for the m2m relation
130 setattr(cls
, self
.name
, ReverseGenericRelatedObjectsDescriptor(self
))
132 def contribute_to_related_class(self
, cls
, related
):
135 def set_attributes_from_rel(self
):
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
):
153 def __get__(self
, instance
, instance_type
=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
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(
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
182 def __set__(self
, instance
, value
):
184 raise AttributeError, "Manager must be accessed via instance"
186 manager
= self
.__get
__(instance
)
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 {}
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
):
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
):
225 setattr(obj
, self
.content_type_field_name
, self
.content_type
)
226 setattr(obj
, self
.object_id_field_name
, self
.pk_val
)
228 add
.alters_data
= True
230 def remove(self
, *objs
):
233 remove
.alters_data
= True
236 for obj
in self
.all():
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
)
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):
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
261 assert not (self
.raw_id_admin
and self
.filter_interface
), \
262 "Generic relations may not use both raw_id_admin and filter_interface"