class ForeignRelatedObjectsDescriptor(object): # This class provides the functionality that makes the related-object # managers available as attributes on a model class, for fields that have # multiple "remote" values and have a ForeignKey pointed at them by # some other model. In the example "poll.choice_set", the choice_set # attribute is a ForeignRelatedObjectsDescriptor instance. def __init__(self, related): self.related = related # RelatedObject instance def __get__(self, instance, instance_type=None): if instance is None: raise AttributeError, "Manager must be accessed via instance" rel_field = self.related.field rel_model = self.related.model # Dynamically create a class that subclasses the related # model's default manager. superclass = self.related.model._default_manager.__class__ class RelatedManager(superclass): def get_query_set(self): return superclass.get_query_set(self).filter(**(self.core_filters)) def add(self, *objs): for obj in objs: setattr(obj, rel_field.name, instance) obj.save() add.alters_data = True def create(self, **kwargs): new_obj = self.model(**kwargs) self.add(new_obj) return new_obj create.alters_data = True # remove() and clear() are only provided if the ForeignKey can have a value of null. if rel_field.null: def remove(self, *objs): val = getattr(instance, rel_field.rel.get_related_field().attname) for obj in objs: # Is obj actually part of this descriptor set? if getattr(obj, rel_field.attname) == val: setattr(obj, rel_field.name, None) obj.save() else: raise rel_field.rel.to.DoesNotExist, "%r is not related to %r." % (obj, instance) remove.alters_data = True def clear(self): for obj in self.all(): setattr(obj, rel_field.name, None) obj.save() clear.alters_data = True manager = RelatedManager() attname = rel_field.rel.get_related_field().name manager.core_filters = {'%s__%s' % (rel_field.name, attname): getattr(instance, attname)} manager.model = self.related.model return manager def __set__(self, instance, value): if instance is None: raise AttributeError, "Manager must be accessed via instance" manager = self.__get__(instance) # If the foreign key can support nulls, then completely clear the related set. # Otherwise, just move the named objects into the set. if self.related.field.null: manager.clear() manager.add(*value) def create_many_related_manager(superclass): """Creates a manager that subclasses 'superclass' (which is a Manager) and adds behavior for many-to-many related objects.""" class ManyRelatedManager(superclass): def __init__(self, model=None, core_filters=None, instance=None, symmetrical=None, join_table=None, source_col_name=None, target_col_name=None, field_name=None): super(ManyRelatedManager, self).__init__() self.core_filters = core_filters self.model = model self.symmetrical = symmetrical self.instance = instance self.join_table = join_table self.source_col_name = source_col_name self.target_col_name = target_col_name self._pk_val = self.instance._get_pk_val() self.field_name = field_name if self._pk_val is None: raise ValueError("%r instance needs to have a primary key value before a many-to-many relationship can be used." % instance.__class__.__name__) def get_query_set(self): return superclass.get_query_set(self).filter(**(self.core_filters)) def add(self, *objs): self._add_items(self.source_col_name, self.target_col_name, *objs) # If this is a symmetrical m2m relation to self, add the mirror entry in the m2m table if self.symmetrical: self._add_items(self.target_col_name, self.source_col_name, *objs) add.alters_data = True def remove(self, *objs): self._remove_items(self.source_col_name, self.target_col_name, *objs) # If this is a symmetrical m2m relation to self, remove the mirror entry in the m2m table if self.symmetrical: self._remove_items(self.target_col_name, self.source_col_name, *objs) remove.alters_data = True def clear(self): self._clear_items(self.source_col_name) # If this is a symmetrical m2m relation to self, clear the mirror entry in the m2m table if self.symmetrical: self._clear_items(self.target_col_name) clear.alters_data = True def create(self, **kwargs): new_obj = self.model(**kwargs) new_obj.save() self.add(new_obj) return new_obj create.alters_data = True def _add_items(self, source_col_name, target_col_name, *objs): # join_table: name of the m2m link table # source_col_name: the PK colname in join_table for the source object # target_col_name: the PK colname in join_table for the target object # *objs - objects to add. Either object instances, or primary keys of object instances. # If there aren't any objects, there is nothing to do. if objs: # Check that all the objects are of the right type new_ids = set() for obj in objs: if isinstance(obj, self.model): new_ids.add(obj._get_pk_val()) else: new_ids.add(obj) # Add the newly created or already existing objects to the join table. # First find out which items are already added, to avoid adding them twice cursor = connection.cursor() cursor.execute("SELECT %s FROM %s WHERE %s = %%s AND %s IN (%s)" % \ (target_col_name, self.join_table, source_col_name, target_col_name, ",".join(['%s'] * len(new_ids))), [self._pk_val] + list(new_ids)) existing_ids = set([row[0] for row in cursor.fetchall()]) # Add the ones that aren't there already new_ids = new_ids - existing_ids for obj_id in new_ids: cursor.execute("INSERT INTO %s (%s, %s) VALUES (%%s, %%s)" % \ (self.join_table, source_col_name, target_col_name), [self._pk_val, obj_id]) added_objs = [obj for obj in objs if (isinstance(obj, self.model) and obj._get_pk_val() in new_ids) or obj in new_ids] dispatcher.send(signal=signals.m2m_add_items, sender=self.model, instance=self.instance, field_name=self.field_name, objs=added_objs) transaction.commit_unless_managed() def _remove_items(self, source_col_name, target_col_name, *objs): # source_col_name: the PK colname in join_table for the source object # target_col_name: the PK colname in join_table for the target object # *objs - objects to remove # If there aren't any objects, there is nothing to do. if objs: # Check that all the objects are of the right type old_ids = set() for obj in objs: if isinstance(obj, self.model): old_ids.add(obj._get_pk_val()) else: old_ids.add(obj) # Remove the specified objects from the join table cursor = connection.cursor() cursor.execute("DELETE FROM %s WHERE %s = %%s AND %s IN (%s)" % \ (self.join_table, source_col_name, target_col_name, ",".join(['%s'] * len(old_ids))), [self._pk_val] + list(old_ids)) dispatcher.send(signal=signals.m2m_remove_items, sender=self.model, instance=self.instance, field_name=self.field_name, objs=objs) transaction.commit_unless_managed() def _clear_items(self, source_col_name): # source_col_name: the PK colname in join_table for the source object dispatcher.send(signal=signals.m2m_clear_items, sender=self.model, instance=self.instance, field_name=self.field_name) cursor = connection.cursor() cursor.execute("DELETE FROM %s WHERE %s = %%s" % \ (self.join_table, source_col_name), [self._pk_val]) transaction.commit_unless_managed() return ManyRelatedManager