Fixed #10109 -- Removed the use of raw SQL in many-to-many fields by introducing an autogenerated through model.
This is the first part of Alex Gaynor's GSoC project to add Multi-db support to Django. git-svn-id: http://code.djangoproject.com/svn/django/trunk@11710 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
parent
aba5389326
commit
585b7acaa3
|
@ -153,8 +153,9 @@ class BaseModelAdmin(object):
|
||||||
"""
|
"""
|
||||||
Get a form Field for a ManyToManyField.
|
Get a form Field for a ManyToManyField.
|
||||||
"""
|
"""
|
||||||
# If it uses an intermediary model, don't show field in admin.
|
# If it uses an intermediary model that isn't auto created, don't show
|
||||||
if db_field.rel.through is not None:
|
# a field in admin.
|
||||||
|
if not db_field.rel.through._meta.auto_created:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if db_field.name in self.raw_id_fields:
|
if db_field.name in self.raw_id_fields:
|
||||||
|
|
|
@ -105,8 +105,6 @@ class GenericRelation(RelatedField, Field):
|
||||||
limit_choices_to=kwargs.pop('limit_choices_to', None),
|
limit_choices_to=kwargs.pop('limit_choices_to', None),
|
||||||
symmetrical=kwargs.pop('symmetrical', True))
|
symmetrical=kwargs.pop('symmetrical', True))
|
||||||
|
|
||||||
# By its very nature, a GenericRelation doesn't create a table.
|
|
||||||
self.creates_table = False
|
|
||||||
|
|
||||||
# Override content-type/object-id field names on the related class
|
# Override content-type/object-id field names on the related class
|
||||||
self.object_id_field_name = kwargs.pop("object_id_field", "object_id")
|
self.object_id_field_name = kwargs.pop("object_id_field", "object_id")
|
||||||
|
|
|
@ -57,12 +57,15 @@ class Command(NoArgsCommand):
|
||||||
# Create the tables for each model
|
# Create the tables for each model
|
||||||
for app in models.get_apps():
|
for app in models.get_apps():
|
||||||
app_name = app.__name__.split('.')[-2]
|
app_name = app.__name__.split('.')[-2]
|
||||||
model_list = models.get_models(app)
|
model_list = models.get_models(app, include_auto_created=True)
|
||||||
for model in model_list:
|
for model in model_list:
|
||||||
# Create the model's database table, if it doesn't already exist.
|
# Create the model's database table, if it doesn't already exist.
|
||||||
if verbosity >= 2:
|
if verbosity >= 2:
|
||||||
print "Processing %s.%s model" % (app_name, model._meta.object_name)
|
print "Processing %s.%s model" % (app_name, model._meta.object_name)
|
||||||
if connection.introspection.table_name_converter(model._meta.db_table) in tables:
|
opts = model._meta
|
||||||
|
if (connection.introspection.table_name_converter(opts.db_table) in tables or
|
||||||
|
(opts.auto_created and
|
||||||
|
connection.introspection.table_name_converter(opts.auto_created._meta.db_table in tables))):
|
||||||
continue
|
continue
|
||||||
sql, references = connection.creation.sql_create_model(model, self.style, seen_models)
|
sql, references = connection.creation.sql_create_model(model, self.style, seen_models)
|
||||||
seen_models.add(model)
|
seen_models.add(model)
|
||||||
|
@ -78,19 +81,6 @@ class Command(NoArgsCommand):
|
||||||
cursor.execute(statement)
|
cursor.execute(statement)
|
||||||
tables.append(connection.introspection.table_name_converter(model._meta.db_table))
|
tables.append(connection.introspection.table_name_converter(model._meta.db_table))
|
||||||
|
|
||||||
# Create the m2m tables. This must be done after all tables have been created
|
|
||||||
# to ensure that all referred tables will exist.
|
|
||||||
for app in models.get_apps():
|
|
||||||
app_name = app.__name__.split('.')[-2]
|
|
||||||
model_list = models.get_models(app)
|
|
||||||
for model in model_list:
|
|
||||||
if model in created_models:
|
|
||||||
sql = connection.creation.sql_for_many_to_many(model, self.style)
|
|
||||||
if sql:
|
|
||||||
if verbosity >= 2:
|
|
||||||
print "Creating many-to-many tables for %s.%s model" % (app_name, model._meta.object_name)
|
|
||||||
for statement in sql:
|
|
||||||
cursor.execute(statement)
|
|
||||||
|
|
||||||
transaction.commit_unless_managed()
|
transaction.commit_unless_managed()
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,7 @@ def sql_create(app, style):
|
||||||
# We trim models from the current app so that the sqlreset command does not
|
# We trim models from the current app so that the sqlreset command does not
|
||||||
# generate invalid SQL (leaving models out of known_models is harmless, so
|
# generate invalid SQL (leaving models out of known_models is harmless, so
|
||||||
# we can be conservative).
|
# we can be conservative).
|
||||||
app_models = models.get_models(app)
|
app_models = models.get_models(app, include_auto_created=True)
|
||||||
final_output = []
|
final_output = []
|
||||||
tables = connection.introspection.table_names()
|
tables = connection.introspection.table_names()
|
||||||
known_models = set([model for model in connection.introspection.installed_models(tables) if model not in app_models])
|
known_models = set([model for model in connection.introspection.installed_models(tables) if model not in app_models])
|
||||||
|
@ -40,10 +40,6 @@ def sql_create(app, style):
|
||||||
# Keep track of the fact that we've created the table for this model.
|
# Keep track of the fact that we've created the table for this model.
|
||||||
known_models.add(model)
|
known_models.add(model)
|
||||||
|
|
||||||
# Create the many-to-many join tables.
|
|
||||||
for model in app_models:
|
|
||||||
final_output.extend(connection.creation.sql_for_many_to_many(model, style))
|
|
||||||
|
|
||||||
# Handle references to tables that are from other apps
|
# Handle references to tables that are from other apps
|
||||||
# but don't exist physically.
|
# but don't exist physically.
|
||||||
not_installed_models = set(pending_references.keys())
|
not_installed_models = set(pending_references.keys())
|
||||||
|
@ -82,7 +78,7 @@ def sql_delete(app, style):
|
||||||
to_delete = set()
|
to_delete = set()
|
||||||
|
|
||||||
references_to_delete = {}
|
references_to_delete = {}
|
||||||
app_models = models.get_models(app)
|
app_models = models.get_models(app, include_auto_created=True)
|
||||||
for model in app_models:
|
for model in app_models:
|
||||||
if cursor and connection.introspection.table_name_converter(model._meta.db_table) in table_names:
|
if cursor and connection.introspection.table_name_converter(model._meta.db_table) in table_names:
|
||||||
# The table exists, so it needs to be dropped
|
# The table exists, so it needs to be dropped
|
||||||
|
@ -97,13 +93,6 @@ def sql_delete(app, style):
|
||||||
if connection.introspection.table_name_converter(model._meta.db_table) in table_names:
|
if connection.introspection.table_name_converter(model._meta.db_table) in table_names:
|
||||||
output.extend(connection.creation.sql_destroy_model(model, references_to_delete, style))
|
output.extend(connection.creation.sql_destroy_model(model, references_to_delete, style))
|
||||||
|
|
||||||
# Output DROP TABLE statements for many-to-many tables.
|
|
||||||
for model in app_models:
|
|
||||||
opts = model._meta
|
|
||||||
for f in opts.local_many_to_many:
|
|
||||||
if cursor and connection.introspection.table_name_converter(f.m2m_db_table()) in table_names:
|
|
||||||
output.extend(connection.creation.sql_destroy_many_to_many(model, f, style))
|
|
||||||
|
|
||||||
# Close database connection explicitly, in case this output is being piped
|
# Close database connection explicitly, in case this output is being piped
|
||||||
# directly into a database client, to avoid locking issues.
|
# directly into a database client, to avoid locking issues.
|
||||||
if cursor:
|
if cursor:
|
||||||
|
|
|
@ -79,6 +79,7 @@ def get_validation_errors(outfile, app=None):
|
||||||
rel_opts = f.rel.to._meta
|
rel_opts = f.rel.to._meta
|
||||||
rel_name = RelatedObject(f.rel.to, cls, f).get_accessor_name()
|
rel_name = RelatedObject(f.rel.to, cls, f).get_accessor_name()
|
||||||
rel_query_name = f.related_query_name()
|
rel_query_name = f.related_query_name()
|
||||||
|
if not f.rel.is_hidden():
|
||||||
for r in rel_opts.fields:
|
for r in rel_opts.fields:
|
||||||
if r.name == rel_name:
|
if r.name == rel_name:
|
||||||
e.add(opts, "Accessor for field '%s' clashes with field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name))
|
e.add(opts, "Accessor for field '%s' clashes with field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name))
|
||||||
|
@ -117,48 +118,80 @@ def get_validation_errors(outfile, app=None):
|
||||||
if f.unique:
|
if f.unique:
|
||||||
e.add(opts, "ManyToManyFields cannot be unique. Remove the unique argument on '%s'." % f.name)
|
e.add(opts, "ManyToManyFields cannot be unique. Remove the unique argument on '%s'." % f.name)
|
||||||
|
|
||||||
if getattr(f.rel, 'through', None) is not None:
|
if f.rel.through is not None and not isinstance(f.rel.through, basestring):
|
||||||
if hasattr(f.rel, 'through_model'):
|
|
||||||
from_model, to_model = cls, f.rel.to
|
from_model, to_model = cls, f.rel.to
|
||||||
if from_model == to_model and f.rel.symmetrical:
|
if from_model == to_model and f.rel.symmetrical and not f.rel.through._meta.auto_created:
|
||||||
e.add(opts, "Many-to-many fields with intermediate tables cannot be symmetrical.")
|
e.add(opts, "Many-to-many fields with intermediate tables cannot be symmetrical.")
|
||||||
seen_from, seen_to, seen_self = False, False, 0
|
seen_from, seen_to, seen_self = False, False, 0
|
||||||
for inter_field in f.rel.through_model._meta.fields:
|
for inter_field in f.rel.through._meta.fields:
|
||||||
rel_to = getattr(inter_field.rel, 'to', None)
|
rel_to = getattr(inter_field.rel, 'to', None)
|
||||||
if from_model == to_model: # relation to self
|
if from_model == to_model: # relation to self
|
||||||
if rel_to == from_model:
|
if rel_to == from_model:
|
||||||
seen_self += 1
|
seen_self += 1
|
||||||
if seen_self > 2:
|
if seen_self > 2:
|
||||||
e.add(opts, "Intermediary model %s has more than two foreign keys to %s, which is ambiguous and is not permitted." % (f.rel.through_model._meta.object_name, from_model._meta.object_name))
|
e.add(opts, "Intermediary model %s has more than "
|
||||||
|
"two foreign keys to %s, which is ambiguous "
|
||||||
|
"and is not permitted." % (
|
||||||
|
f.rel.through._meta.object_name,
|
||||||
|
from_model._meta.object_name
|
||||||
|
)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
if rel_to == from_model:
|
if rel_to == from_model:
|
||||||
if seen_from:
|
if seen_from:
|
||||||
e.add(opts, "Intermediary model %s has more than one foreign key to %s, which is ambiguous and is not permitted." % (f.rel.through_model._meta.object_name, from_model._meta.object_name))
|
e.add(opts, "Intermediary model %s has more "
|
||||||
|
"than one foreign key to %s, which is "
|
||||||
|
"ambiguous and is not permitted." % (
|
||||||
|
f.rel.through._meta.object_name,
|
||||||
|
from_model._meta.object_name
|
||||||
|
)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
seen_from = True
|
seen_from = True
|
||||||
elif rel_to == to_model:
|
elif rel_to == to_model:
|
||||||
if seen_to:
|
if seen_to:
|
||||||
e.add(opts, "Intermediary model %s has more than one foreign key to %s, which is ambiguous and is not permitted." % (f.rel.through_model._meta.object_name, rel_to._meta.object_name))
|
e.add(opts, "Intermediary model %s has more "
|
||||||
|
"than one foreign key to %s, which is "
|
||||||
|
"ambiguous and is not permitted." % (
|
||||||
|
f.rel.through._meta.object_name,
|
||||||
|
rel_to._meta.object_name
|
||||||
|
)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
seen_to = True
|
seen_to = True
|
||||||
if f.rel.through_model not in models.get_models():
|
if f.rel.through not in models.get_models(include_auto_created=True):
|
||||||
e.add(opts, "'%s' specifies an m2m relation through model %s, which has not been installed." % (f.name, f.rel.through))
|
e.add(opts, "'%s' specifies an m2m relation through model "
|
||||||
signature = (f.rel.to, cls, f.rel.through_model)
|
"%s, which has not been installed." % (f.name, f.rel.through)
|
||||||
|
)
|
||||||
|
signature = (f.rel.to, cls, f.rel.through)
|
||||||
if signature in seen_intermediary_signatures:
|
if signature in seen_intermediary_signatures:
|
||||||
e.add(opts, "The model %s has two manually-defined m2m relations through the model %s, which is not permitted. Please consider using an extra field on your intermediary model instead." % (cls._meta.object_name, f.rel.through_model._meta.object_name))
|
e.add(opts, "The model %s has two manually-defined m2m "
|
||||||
|
"relations through the model %s, which is not "
|
||||||
|
"permitted. Please consider using an extra field on "
|
||||||
|
"your intermediary model instead." % (
|
||||||
|
cls._meta.object_name,
|
||||||
|
f.rel.through._meta.object_name
|
||||||
|
)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
seen_intermediary_signatures.append(signature)
|
seen_intermediary_signatures.append(signature)
|
||||||
seen_related_fk, seen_this_fk = False, False
|
seen_related_fk, seen_this_fk = False, False
|
||||||
for field in f.rel.through_model._meta.fields:
|
for field in f.rel.through._meta.fields:
|
||||||
if field.rel:
|
if field.rel:
|
||||||
if not seen_related_fk and field.rel.to == f.rel.to:
|
if not seen_related_fk and field.rel.to == f.rel.to:
|
||||||
seen_related_fk = True
|
seen_related_fk = True
|
||||||
elif field.rel.to == cls:
|
elif field.rel.to == cls:
|
||||||
seen_this_fk = True
|
seen_this_fk = True
|
||||||
if not seen_related_fk or not seen_this_fk:
|
if not seen_related_fk or not seen_this_fk:
|
||||||
e.add(opts, "'%s' has a manually-defined m2m relation through model %s, which does not have foreign keys to %s and %s" % (f.name, f.rel.through, f.rel.to._meta.object_name, cls._meta.object_name))
|
e.add(opts, "'%s' has a manually-defined m2m relation "
|
||||||
else:
|
"through model %s, which does not have foreign keys "
|
||||||
e.add(opts, "'%s' specifies an m2m relation through model %s, which has not been installed" % (f.name, f.rel.through))
|
"to %s and %s" % (f.name, f.rel.through._meta.object_name,
|
||||||
|
f.rel.to._meta.object_name, cls._meta.object_name)
|
||||||
|
)
|
||||||
|
elif isinstance(f.rel.through, basestring):
|
||||||
|
e.add(opts, "'%s' specifies an m2m relation through model %s, "
|
||||||
|
"which has not been installed" % (f.name, f.rel.through)
|
||||||
|
)
|
||||||
|
|
||||||
rel_opts = f.rel.to._meta
|
rel_opts = f.rel.to._meta
|
||||||
rel_name = RelatedObject(f.rel.to, cls, f).get_accessor_name()
|
rel_name = RelatedObject(f.rel.to, cls, f).get_accessor_name()
|
||||||
|
|
|
@ -56,7 +56,7 @@ class Serializer(base.Serializer):
|
||||||
self._current[field.name] = smart_unicode(related, strings_only=True)
|
self._current[field.name] = smart_unicode(related, strings_only=True)
|
||||||
|
|
||||||
def handle_m2m_field(self, obj, field):
|
def handle_m2m_field(self, obj, field):
|
||||||
if field.creates_table:
|
if field.rel.through._meta.auto_created:
|
||||||
self._current[field.name] = [smart_unicode(related._get_pk_val(), strings_only=True)
|
self._current[field.name] = [smart_unicode(related._get_pk_val(), strings_only=True)
|
||||||
for related in getattr(obj, field.name).iterator()]
|
for related in getattr(obj, field.name).iterator()]
|
||||||
|
|
||||||
|
|
|
@ -98,7 +98,7 @@ class Serializer(base.Serializer):
|
||||||
serialized as references to the object's PK (i.e. the related *data*
|
serialized as references to the object's PK (i.e. the related *data*
|
||||||
is not dumped, just the relation).
|
is not dumped, just the relation).
|
||||||
"""
|
"""
|
||||||
if field.creates_table:
|
if field.rel.through._meta.auto_created:
|
||||||
self._start_relational_field(field)
|
self._start_relational_field(field)
|
||||||
for relobj in getattr(obj, field.name).iterator():
|
for relobj in getattr(obj, field.name).iterator():
|
||||||
self.xml.addQuickElement("object", attrs={"pk" : smart_unicode(relobj._get_pk_val())})
|
self.xml.addQuickElement("object", attrs={"pk" : smart_unicode(relobj._get_pk_val())})
|
||||||
|
@ -233,4 +233,3 @@ def getInnerText(node):
|
||||||
else:
|
else:
|
||||||
pass
|
pass
|
||||||
return u"".join(inner_text)
|
return u"".join(inner_text)
|
||||||
|
|
||||||
|
|
|
@ -434,7 +434,7 @@ class Model(object):
|
||||||
else:
|
else:
|
||||||
meta = cls._meta
|
meta = cls._meta
|
||||||
|
|
||||||
if origin:
|
if origin and not meta.auto_created:
|
||||||
signals.pre_save.send(sender=origin, instance=self, raw=raw)
|
signals.pre_save.send(sender=origin, instance=self, raw=raw)
|
||||||
|
|
||||||
# If we are in a raw save, save the object exactly as presented.
|
# If we are in a raw save, save the object exactly as presented.
|
||||||
|
@ -507,7 +507,7 @@ class Model(object):
|
||||||
setattr(self, meta.pk.attname, result)
|
setattr(self, meta.pk.attname, result)
|
||||||
transaction.commit_unless_managed()
|
transaction.commit_unless_managed()
|
||||||
|
|
||||||
if origin:
|
if origin and not meta.auto_created:
|
||||||
signals.post_save.send(sender=origin, instance=self,
|
signals.post_save.send(sender=origin, instance=self,
|
||||||
created=(not record_exists), raw=raw)
|
created=(not record_exists), raw=raw)
|
||||||
|
|
||||||
|
@ -544,7 +544,12 @@ class Model(object):
|
||||||
rel_descriptor = cls.__dict__[rel_opts_name]
|
rel_descriptor = cls.__dict__[rel_opts_name]
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
|
# in the case of a hidden fkey just skip it, it'll get
|
||||||
|
# processed as an m2m
|
||||||
|
if not related.field.rel.is_hidden():
|
||||||
raise AssertionError("Should never get here.")
|
raise AssertionError("Should never get here.")
|
||||||
|
else:
|
||||||
|
continue
|
||||||
delete_qs = rel_descriptor.delete_manager(self).all()
|
delete_qs = rel_descriptor.delete_manager(self).all()
|
||||||
for sub_obj in delete_qs:
|
for sub_obj in delete_qs:
|
||||||
sub_obj._collect_sub_objects(seen_objs, self.__class__, related.field.null)
|
sub_obj._collect_sub_objects(seen_objs, self.__class__, related.field.null)
|
||||||
|
|
|
@ -58,6 +58,10 @@ def add_lazy_relation(cls, field, relation, operation):
|
||||||
# If we can't split, assume a model in current app
|
# If we can't split, assume a model in current app
|
||||||
app_label = cls._meta.app_label
|
app_label = cls._meta.app_label
|
||||||
model_name = relation
|
model_name = relation
|
||||||
|
except AttributeError:
|
||||||
|
# If it doesn't have a split it's actually a model class
|
||||||
|
app_label = relation._meta.app_label
|
||||||
|
model_name = relation._meta.object_name
|
||||||
|
|
||||||
# Try to look up the related model, and if it's already loaded resolve the
|
# Try to look up the related model, and if it's already loaded resolve the
|
||||||
# string right away. If get_model returns None, it means that the related
|
# string right away. If get_model returns None, it means that the related
|
||||||
|
@ -96,7 +100,7 @@ class RelatedField(object):
|
||||||
self.rel.related_name = self.rel.related_name % {'class': cls.__name__.lower()}
|
self.rel.related_name = self.rel.related_name % {'class': cls.__name__.lower()}
|
||||||
|
|
||||||
other = self.rel.to
|
other = self.rel.to
|
||||||
if isinstance(other, basestring):
|
if isinstance(other, basestring) or other._meta.pk is None:
|
||||||
def resolve_related_class(field, model, cls):
|
def resolve_related_class(field, model, cls):
|
||||||
field.rel.to = model
|
field.rel.to = model
|
||||||
field.do_related_class(model, cls)
|
field.do_related_class(model, cls)
|
||||||
|
@ -401,22 +405,22 @@ class ForeignRelatedObjectsDescriptor(object):
|
||||||
|
|
||||||
return manager
|
return manager
|
||||||
|
|
||||||
def create_many_related_manager(superclass, through=False):
|
def create_many_related_manager(superclass, rel=False):
|
||||||
"""Creates a manager that subclasses 'superclass' (which is a Manager)
|
"""Creates a manager that subclasses 'superclass' (which is a Manager)
|
||||||
and adds behavior for many-to-many related objects."""
|
and adds behavior for many-to-many related objects."""
|
||||||
|
through = rel.through
|
||||||
class ManyRelatedManager(superclass):
|
class ManyRelatedManager(superclass):
|
||||||
def __init__(self, model=None, core_filters=None, instance=None, symmetrical=None,
|
def __init__(self, model=None, core_filters=None, instance=None, symmetrical=None,
|
||||||
join_table=None, source_col_name=None, target_col_name=None):
|
join_table=None, source_field_name=None, target_field_name=None):
|
||||||
super(ManyRelatedManager, self).__init__()
|
super(ManyRelatedManager, self).__init__()
|
||||||
self.core_filters = core_filters
|
self.core_filters = core_filters
|
||||||
self.model = model
|
self.model = model
|
||||||
self.symmetrical = symmetrical
|
self.symmetrical = symmetrical
|
||||||
self.instance = instance
|
self.instance = instance
|
||||||
self.join_table = join_table
|
self.source_field_name = source_field_name
|
||||||
self.source_col_name = source_col_name
|
self.target_field_name = target_field_name
|
||||||
self.target_col_name = target_col_name
|
|
||||||
self.through = through
|
self.through = through
|
||||||
self._pk_val = self.instance._get_pk_val()
|
self._pk_val = self.instance.pk
|
||||||
if self._pk_val is None:
|
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__)
|
raise ValueError("%r instance needs to have a primary key value before a many-to-many relationship can be used." % instance.__class__.__name__)
|
||||||
|
|
||||||
|
@ -425,36 +429,37 @@ def create_many_related_manager(superclass, through=False):
|
||||||
|
|
||||||
# If the ManyToMany relation has an intermediary model,
|
# If the ManyToMany relation has an intermediary model,
|
||||||
# the add and remove methods do not exist.
|
# the add and remove methods do not exist.
|
||||||
if through is None:
|
if rel.through._meta.auto_created:
|
||||||
def add(self, *objs):
|
def add(self, *objs):
|
||||||
self._add_items(self.source_col_name, self.target_col_name, *objs)
|
self._add_items(self.source_field_name, self.target_field_name, *objs)
|
||||||
|
|
||||||
# If this is a symmetrical m2m relation to self, add the mirror entry in the m2m table
|
# If this is a symmetrical m2m relation to self, add the mirror entry in the m2m table
|
||||||
if self.symmetrical:
|
if self.symmetrical:
|
||||||
self._add_items(self.target_col_name, self.source_col_name, *objs)
|
self._add_items(self.target_field_name, self.source_field_name, *objs)
|
||||||
add.alters_data = True
|
add.alters_data = True
|
||||||
|
|
||||||
def remove(self, *objs):
|
def remove(self, *objs):
|
||||||
self._remove_items(self.source_col_name, self.target_col_name, *objs)
|
self._remove_items(self.source_field_name, self.target_field_name, *objs)
|
||||||
|
|
||||||
# If this is a symmetrical m2m relation to self, remove the mirror entry in the m2m table
|
# If this is a symmetrical m2m relation to self, remove the mirror entry in the m2m table
|
||||||
if self.symmetrical:
|
if self.symmetrical:
|
||||||
self._remove_items(self.target_col_name, self.source_col_name, *objs)
|
self._remove_items(self.target_field_name, self.source_field_name, *objs)
|
||||||
remove.alters_data = True
|
remove.alters_data = True
|
||||||
|
|
||||||
def clear(self):
|
def clear(self):
|
||||||
self._clear_items(self.source_col_name)
|
self._clear_items(self.source_field_name)
|
||||||
|
|
||||||
# If this is a symmetrical m2m relation to self, clear the mirror entry in the m2m table
|
# If this is a symmetrical m2m relation to self, clear the mirror entry in the m2m table
|
||||||
if self.symmetrical:
|
if self.symmetrical:
|
||||||
self._clear_items(self.target_col_name)
|
self._clear_items(self.target_field_name)
|
||||||
clear.alters_data = True
|
clear.alters_data = True
|
||||||
|
|
||||||
def create(self, **kwargs):
|
def create(self, **kwargs):
|
||||||
# This check needs to be done here, since we can't later remove this
|
# This check needs to be done here, since we can't later remove this
|
||||||
# from the method lookup table, as we do with add and remove.
|
# from the method lookup table, as we do with add and remove.
|
||||||
if through is not None:
|
if not rel.through._meta.auto_created:
|
||||||
raise AttributeError, "Cannot use create() on a ManyToManyField which specifies an intermediary model. Use %s's Manager instead." % through
|
opts = through._meta
|
||||||
|
raise AttributeError, "Cannot use create() on a ManyToManyField which specifies an intermediary model. Use %s.%s's Manager instead." % (opts.app_label, opts.object_name)
|
||||||
new_obj = super(ManyRelatedManager, self).create(**kwargs)
|
new_obj = super(ManyRelatedManager, self).create(**kwargs)
|
||||||
self.add(new_obj)
|
self.add(new_obj)
|
||||||
return new_obj
|
return new_obj
|
||||||
|
@ -470,41 +475,38 @@ def create_many_related_manager(superclass, through=False):
|
||||||
return obj, created
|
return obj, created
|
||||||
get_or_create.alters_data = True
|
get_or_create.alters_data = True
|
||||||
|
|
||||||
def _add_items(self, source_col_name, target_col_name, *objs):
|
def _add_items(self, source_field_name, target_field_name, *objs):
|
||||||
# join_table: name of the m2m link table
|
# join_table: name of the m2m link table
|
||||||
# source_col_name: the PK colname in join_table for the source object
|
# source_field_name: the PK fieldname in join_table for the source object
|
||||||
# target_col_name: the PK colname in join_table for the target object
|
# target_col_name: the PK fieldname in join_table for the target object
|
||||||
# *objs - objects to add. Either object instances, or primary keys of object instances.
|
# *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 there aren't any objects, there is nothing to do.
|
||||||
|
from django.db.models import Model
|
||||||
if objs:
|
if objs:
|
||||||
from django.db.models.base import Model
|
|
||||||
# Check that all the objects are of the right type
|
|
||||||
new_ids = set()
|
new_ids = set()
|
||||||
for obj in objs:
|
for obj in objs:
|
||||||
if isinstance(obj, self.model):
|
if isinstance(obj, self.model):
|
||||||
new_ids.add(obj._get_pk_val())
|
new_ids.add(obj.pk)
|
||||||
elif isinstance(obj, Model):
|
elif isinstance(obj, Model):
|
||||||
raise TypeError, "'%s' instance expected" % self.model._meta.object_name
|
raise TypeError, "'%s' instance expected" % self.model._meta.object_name
|
||||||
else:
|
else:
|
||||||
new_ids.add(obj)
|
new_ids.add(obj)
|
||||||
# Add the newly created or already existing objects to the join table.
|
vals = self.through._default_manager.values_list(target_field_name, flat=True)
|
||||||
# First find out which items are already added, to avoid adding them twice
|
vals = vals.filter(**{
|
||||||
cursor = connection.cursor()
|
source_field_name: self._pk_val,
|
||||||
cursor.execute("SELECT %s FROM %s WHERE %s = %%s AND %s IN (%s)" % \
|
'%s__in' % target_field_name: new_ids,
|
||||||
(target_col_name, self.join_table, source_col_name,
|
})
|
||||||
target_col_name, ",".join(['%s'] * len(new_ids))),
|
vals = set(vals)
|
||||||
[self._pk_val] + list(new_ids))
|
|
||||||
existing_ids = set([row[0] for row in cursor.fetchall()])
|
|
||||||
|
|
||||||
# Add the ones that aren't there already
|
# Add the ones that aren't there already
|
||||||
for obj_id in (new_ids - existing_ids):
|
for obj_id in (new_ids - vals):
|
||||||
cursor.execute("INSERT INTO %s (%s, %s) VALUES (%%s, %%s)" % \
|
self.through._default_manager.create(**{
|
||||||
(self.join_table, source_col_name, target_col_name),
|
'%s_id' % source_field_name: self._pk_val,
|
||||||
[self._pk_val, obj_id])
|
'%s_id' % target_field_name: obj_id,
|
||||||
transaction.commit_unless_managed()
|
})
|
||||||
|
|
||||||
def _remove_items(self, source_col_name, target_col_name, *objs):
|
def _remove_items(self, source_field_name, target_field_name, *objs):
|
||||||
# source_col_name: the PK colname in join_table for the source object
|
# 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
|
# target_col_name: the PK colname in join_table for the target object
|
||||||
# *objs - objects to remove
|
# *objs - objects to remove
|
||||||
|
@ -515,24 +517,20 @@ def create_many_related_manager(superclass, through=False):
|
||||||
old_ids = set()
|
old_ids = set()
|
||||||
for obj in objs:
|
for obj in objs:
|
||||||
if isinstance(obj, self.model):
|
if isinstance(obj, self.model):
|
||||||
old_ids.add(obj._get_pk_val())
|
old_ids.add(obj.pk)
|
||||||
else:
|
else:
|
||||||
old_ids.add(obj)
|
old_ids.add(obj)
|
||||||
# Remove the specified objects from the join table
|
# Remove the specified objects from the join table
|
||||||
cursor = connection.cursor()
|
self.through._default_manager.filter(**{
|
||||||
cursor.execute("DELETE FROM %s WHERE %s = %%s AND %s IN (%s)" % \
|
source_field_name: self._pk_val,
|
||||||
(self.join_table, source_col_name,
|
'%s__in' % target_field_name: old_ids
|
||||||
target_col_name, ",".join(['%s'] * len(old_ids))),
|
}).delete()
|
||||||
[self._pk_val] + list(old_ids))
|
|
||||||
transaction.commit_unless_managed()
|
|
||||||
|
|
||||||
def _clear_items(self, source_col_name):
|
def _clear_items(self, source_field_name):
|
||||||
# source_col_name: the PK colname in join_table for the source object
|
# source_col_name: the PK colname in join_table for the source object
|
||||||
cursor = connection.cursor()
|
self.through._default_manager.filter(**{
|
||||||
cursor.execute("DELETE FROM %s WHERE %s = %%s" % \
|
source_field_name: self._pk_val
|
||||||
(self.join_table, source_col_name),
|
}).delete()
|
||||||
[self._pk_val])
|
|
||||||
transaction.commit_unless_managed()
|
|
||||||
|
|
||||||
return ManyRelatedManager
|
return ManyRelatedManager
|
||||||
|
|
||||||
|
@ -554,17 +552,15 @@ class ManyRelatedObjectsDescriptor(object):
|
||||||
# model's default manager.
|
# model's default manager.
|
||||||
rel_model = self.related.model
|
rel_model = self.related.model
|
||||||
superclass = rel_model._default_manager.__class__
|
superclass = rel_model._default_manager.__class__
|
||||||
RelatedManager = create_many_related_manager(superclass, self.related.field.rel.through)
|
RelatedManager = create_many_related_manager(superclass, self.related.field.rel)
|
||||||
|
|
||||||
qn = connection.ops.quote_name
|
|
||||||
manager = RelatedManager(
|
manager = RelatedManager(
|
||||||
model=rel_model,
|
model=rel_model,
|
||||||
core_filters={'%s__pk' % self.related.field.name: instance._get_pk_val()},
|
core_filters={'%s__pk' % self.related.field.name: instance._get_pk_val()},
|
||||||
instance=instance,
|
instance=instance,
|
||||||
symmetrical=False,
|
symmetrical=False,
|
||||||
join_table=qn(self.related.field.m2m_db_table()),
|
source_field_name=self.related.field.m2m_reverse_field_name(),
|
||||||
source_col_name=qn(self.related.field.m2m_reverse_name()),
|
target_field_name=self.related.field.m2m_field_name()
|
||||||
target_col_name=qn(self.related.field.m2m_column_name())
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return manager
|
return manager
|
||||||
|
@ -573,9 +569,9 @@ class ManyRelatedObjectsDescriptor(object):
|
||||||
if instance is None:
|
if instance is None:
|
||||||
raise AttributeError, "Manager must be accessed via instance"
|
raise AttributeError, "Manager must be accessed via instance"
|
||||||
|
|
||||||
through = getattr(self.related.field.rel, 'through', None)
|
if not self.related.field.rel.through._meta.auto_created:
|
||||||
if through is not None:
|
opts = self.related.field.rel.through._meta
|
||||||
raise AttributeError, "Cannot set values on a ManyToManyField which specifies an intermediary model. Use %s's Manager instead." % through
|
raise AttributeError, "Cannot set values on a ManyToManyField which specifies an intermediary model. Use %s.%s's Manager instead." % (opts.app_label, opts.object_name)
|
||||||
|
|
||||||
manager = self.__get__(instance)
|
manager = self.__get__(instance)
|
||||||
manager.clear()
|
manager.clear()
|
||||||
|
@ -599,17 +595,15 @@ class ReverseManyRelatedObjectsDescriptor(object):
|
||||||
# model's default manager.
|
# model's default manager.
|
||||||
rel_model=self.field.rel.to
|
rel_model=self.field.rel.to
|
||||||
superclass = rel_model._default_manager.__class__
|
superclass = rel_model._default_manager.__class__
|
||||||
RelatedManager = create_many_related_manager(superclass, self.field.rel.through)
|
RelatedManager = create_many_related_manager(superclass, self.field.rel)
|
||||||
|
|
||||||
qn = connection.ops.quote_name
|
|
||||||
manager = RelatedManager(
|
manager = RelatedManager(
|
||||||
model=rel_model,
|
model=rel_model,
|
||||||
core_filters={'%s__pk' % self.field.related_query_name(): instance._get_pk_val()},
|
core_filters={'%s__pk' % self.field.related_query_name(): instance._get_pk_val()},
|
||||||
instance=instance,
|
instance=instance,
|
||||||
symmetrical=(self.field.rel.symmetrical and isinstance(instance, rel_model)),
|
symmetrical=(self.field.rel.symmetrical and isinstance(instance, rel_model)),
|
||||||
join_table=qn(self.field.m2m_db_table()),
|
source_field_name=self.field.m2m_field_name(),
|
||||||
source_col_name=qn(self.field.m2m_column_name()),
|
target_field_name=self.field.m2m_reverse_field_name()
|
||||||
target_col_name=qn(self.field.m2m_reverse_name())
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return manager
|
return manager
|
||||||
|
@ -618,9 +612,9 @@ class ReverseManyRelatedObjectsDescriptor(object):
|
||||||
if instance is None:
|
if instance is None:
|
||||||
raise AttributeError, "Manager must be accessed via instance"
|
raise AttributeError, "Manager must be accessed via instance"
|
||||||
|
|
||||||
through = getattr(self.field.rel, 'through', None)
|
if not self.field.rel.through._meta.auto_created:
|
||||||
if through is not None:
|
opts = self.field.rel.through._meta
|
||||||
raise AttributeError, "Cannot set values on a ManyToManyField which specifies an intermediary model. Use %s's Manager instead." % through
|
raise AttributeError, "Cannot set values on a ManyToManyField which specifies an intermediary model. Use %s.%s's Manager instead." % (opts.app_label, opts.object_name)
|
||||||
|
|
||||||
manager = self.__get__(instance)
|
manager = self.__get__(instance)
|
||||||
manager.clear()
|
manager.clear()
|
||||||
|
@ -642,6 +636,10 @@ class ManyToOneRel(object):
|
||||||
self.multiple = True
|
self.multiple = True
|
||||||
self.parent_link = parent_link
|
self.parent_link = parent_link
|
||||||
|
|
||||||
|
def is_hidden(self):
|
||||||
|
"Should the related object be hidden?"
|
||||||
|
return self.related_name and self.related_name[-1] == '+'
|
||||||
|
|
||||||
def get_related_field(self):
|
def get_related_field(self):
|
||||||
"""
|
"""
|
||||||
Returns the Field in the 'to' object to which this relationship is
|
Returns the Field in the 'to' object to which this relationship is
|
||||||
|
@ -673,6 +671,10 @@ class ManyToManyRel(object):
|
||||||
self.multiple = True
|
self.multiple = True
|
||||||
self.through = through
|
self.through = through
|
||||||
|
|
||||||
|
def is_hidden(self):
|
||||||
|
"Should the related object be hidden?"
|
||||||
|
return self.related_name and self.related_name[-1] == '+'
|
||||||
|
|
||||||
def get_related_field(self):
|
def get_related_field(self):
|
||||||
"""
|
"""
|
||||||
Returns the field in the to' object to which this relationship is tied
|
Returns the field in the to' object to which this relationship is tied
|
||||||
|
@ -690,7 +692,6 @@ class ForeignKey(RelatedField, Field):
|
||||||
assert isinstance(to, basestring), "%s(%r) is invalid. First parameter to ForeignKey must be either a model, a model name, or the string %r" % (self.__class__.__name__, to, RECURSIVE_RELATIONSHIP_CONSTANT)
|
assert isinstance(to, basestring), "%s(%r) is invalid. First parameter to ForeignKey must be either a model, a model name, or the string %r" % (self.__class__.__name__, to, RECURSIVE_RELATIONSHIP_CONSTANT)
|
||||||
else:
|
else:
|
||||||
assert not to._meta.abstract, "%s cannot define a relation with abstract class %s" % (self.__class__.__name__, to._meta.object_name)
|
assert not to._meta.abstract, "%s cannot define a relation with abstract class %s" % (self.__class__.__name__, to._meta.object_name)
|
||||||
to_field = to_field or to._meta.pk.name
|
|
||||||
kwargs['verbose_name'] = kwargs.get('verbose_name', None)
|
kwargs['verbose_name'] = kwargs.get('verbose_name', None)
|
||||||
|
|
||||||
kwargs['rel'] = rel_class(to, to_field,
|
kwargs['rel'] = rel_class(to, to_field,
|
||||||
|
@ -743,7 +744,12 @@ class ForeignKey(RelatedField, Field):
|
||||||
cls._meta.duplicate_targets[self.column] = (target, "o2m")
|
cls._meta.duplicate_targets[self.column] = (target, "o2m")
|
||||||
|
|
||||||
def contribute_to_related_class(self, cls, related):
|
def contribute_to_related_class(self, cls, related):
|
||||||
|
# Internal FK's - i.e., those with a related name ending with '+' -
|
||||||
|
# don't get a related descriptor.
|
||||||
|
if not self.rel.is_hidden():
|
||||||
setattr(cls, related.get_accessor_name(), ForeignRelatedObjectsDescriptor(related))
|
setattr(cls, related.get_accessor_name(), ForeignRelatedObjectsDescriptor(related))
|
||||||
|
if self.rel.field_name is None:
|
||||||
|
self.rel.field_name = cls._meta.pk.name
|
||||||
|
|
||||||
def formfield(self, **kwargs):
|
def formfield(self, **kwargs):
|
||||||
defaults = {
|
defaults = {
|
||||||
|
@ -790,6 +796,43 @@ class OneToOneField(ForeignKey):
|
||||||
return None
|
return None
|
||||||
return super(OneToOneField, self).formfield(**kwargs)
|
return super(OneToOneField, self).formfield(**kwargs)
|
||||||
|
|
||||||
|
def create_many_to_many_intermediary_model(field, klass):
|
||||||
|
from django.db import models
|
||||||
|
managed = True
|
||||||
|
if isinstance(field.rel.to, basestring) and field.rel.to != RECURSIVE_RELATIONSHIP_CONSTANT:
|
||||||
|
to = field.rel.to
|
||||||
|
to_model = field.rel.to
|
||||||
|
def set_managed(field, model, cls):
|
||||||
|
field.rel.through._meta.managed = model._meta.managed or cls._meta.managed
|
||||||
|
add_lazy_relation(klass, field, to_model, set_managed)
|
||||||
|
elif isinstance(field.rel.to, basestring):
|
||||||
|
to = klass._meta.object_name
|
||||||
|
to_model = klass
|
||||||
|
managed = klass._meta.managed
|
||||||
|
else:
|
||||||
|
to = field.rel.to._meta.object_name
|
||||||
|
to_model = field.rel.to
|
||||||
|
managed = klass._meta.managed or to_model._meta.managed
|
||||||
|
name = '%s_%s' % (klass._meta.object_name, field.name)
|
||||||
|
if field.rel.to == RECURSIVE_RELATIONSHIP_CONSTANT or field.rel.to == klass._meta.object_name:
|
||||||
|
from_ = 'from_%s' % to.lower()
|
||||||
|
to = to.lower()
|
||||||
|
else:
|
||||||
|
from_ = klass._meta.object_name.lower()
|
||||||
|
to = to.lower()
|
||||||
|
meta = type('Meta', (object,), {
|
||||||
|
'db_table': field._get_m2m_db_table(klass._meta),
|
||||||
|
'managed': managed,
|
||||||
|
'auto_created': klass,
|
||||||
|
'unique_together': (from_, to)
|
||||||
|
})
|
||||||
|
return type(name, (models.Model,), {
|
||||||
|
'Meta': meta,
|
||||||
|
'__module__': klass.__module__,
|
||||||
|
from_: models.ForeignKey(klass, related_name='%s+' % name),
|
||||||
|
to: models.ForeignKey(to_model, related_name='%s+' % name)
|
||||||
|
})
|
||||||
|
|
||||||
class ManyToManyField(RelatedField, Field):
|
class ManyToManyField(RelatedField, Field):
|
||||||
def __init__(self, to, **kwargs):
|
def __init__(self, to, **kwargs):
|
||||||
try:
|
try:
|
||||||
|
@ -806,10 +849,7 @@ class ManyToManyField(RelatedField, Field):
|
||||||
|
|
||||||
self.db_table = kwargs.pop('db_table', None)
|
self.db_table = kwargs.pop('db_table', None)
|
||||||
if kwargs['rel'].through is not None:
|
if kwargs['rel'].through is not None:
|
||||||
self.creates_table = False
|
|
||||||
assert self.db_table is None, "Cannot specify a db_table if an intermediary model is used."
|
assert self.db_table is None, "Cannot specify a db_table if an intermediary model is used."
|
||||||
else:
|
|
||||||
self.creates_table = True
|
|
||||||
|
|
||||||
Field.__init__(self, **kwargs)
|
Field.__init__(self, **kwargs)
|
||||||
|
|
||||||
|
@ -822,40 +862,30 @@ class ManyToManyField(RelatedField, Field):
|
||||||
def _get_m2m_db_table(self, opts):
|
def _get_m2m_db_table(self, opts):
|
||||||
"Function that can be curried to provide the m2m table name for this relation"
|
"Function that can be curried to provide the m2m table name for this relation"
|
||||||
if self.rel.through is not None:
|
if self.rel.through is not None:
|
||||||
return self.rel.through_model._meta.db_table
|
return self.rel.through._meta.db_table
|
||||||
elif self.db_table:
|
elif self.db_table:
|
||||||
return self.db_table
|
return self.db_table
|
||||||
else:
|
else:
|
||||||
return util.truncate_name('%s_%s' % (opts.db_table, self.name),
|
return util.truncate_name('%s_%s' % (opts.db_table, self.name),
|
||||||
connection.ops.max_name_length())
|
connection.ops.max_name_length())
|
||||||
|
|
||||||
def _get_m2m_column_name(self, related):
|
def _get_m2m_attr(self, related, attr):
|
||||||
"Function that can be curried to provide the source column name for the m2m table"
|
"Function that can be curried to provide the source column name for the m2m table"
|
||||||
try:
|
cache_attr = '_m2m_%s_cache' % attr
|
||||||
return self._m2m_column_name_cache
|
if hasattr(self, cache_attr):
|
||||||
except:
|
return getattr(self, cache_attr)
|
||||||
if self.rel.through is not None:
|
for f in self.rel.through._meta.fields:
|
||||||
for f in self.rel.through_model._meta.fields:
|
|
||||||
if hasattr(f,'rel') and f.rel and f.rel.to == related.model:
|
if hasattr(f,'rel') and f.rel and f.rel.to == related.model:
|
||||||
self._m2m_column_name_cache = f.column
|
setattr(self, cache_attr, getattr(f, attr))
|
||||||
break
|
return getattr(self, cache_attr)
|
||||||
# If this is an m2m relation to self, avoid the inevitable name clash
|
|
||||||
elif related.model == related.parent_model:
|
|
||||||
self._m2m_column_name_cache = 'from_' + related.model._meta.object_name.lower() + '_id'
|
|
||||||
else:
|
|
||||||
self._m2m_column_name_cache = related.model._meta.object_name.lower() + '_id'
|
|
||||||
|
|
||||||
# Return the newly cached value
|
def _get_m2m_reverse_attr(self, related, attr):
|
||||||
return self._m2m_column_name_cache
|
|
||||||
|
|
||||||
def _get_m2m_reverse_name(self, related):
|
|
||||||
"Function that can be curried to provide the related column name for the m2m table"
|
"Function that can be curried to provide the related column name for the m2m table"
|
||||||
try:
|
cache_attr = '_m2m_reverse_%s_cache' % attr
|
||||||
return self._m2m_reverse_name_cache
|
if hasattr(self, cache_attr):
|
||||||
except:
|
return getattr(self, cache_attr)
|
||||||
if self.rel.through is not None:
|
|
||||||
found = False
|
found = False
|
||||||
for f in self.rel.through_model._meta.fields:
|
for f in self.rel.through._meta.fields:
|
||||||
if hasattr(f,'rel') and f.rel and f.rel.to == related.parent_model:
|
if hasattr(f,'rel') and f.rel and f.rel.to == related.parent_model:
|
||||||
if related.model == related.parent_model:
|
if related.model == related.parent_model:
|
||||||
# If this is an m2m-intermediate to self,
|
# If this is an m2m-intermediate to self,
|
||||||
|
@ -863,21 +893,14 @@ class ManyToManyField(RelatedField, Field):
|
||||||
# the source column. Keep searching for
|
# the source column. Keep searching for
|
||||||
# the second foreign key.
|
# the second foreign key.
|
||||||
if found:
|
if found:
|
||||||
self._m2m_reverse_name_cache = f.column
|
setattr(self, cache_attr, getattr(f, attr))
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
found = True
|
found = True
|
||||||
else:
|
else:
|
||||||
self._m2m_reverse_name_cache = f.column
|
setattr(self, cache_attr, getattr(f, attr))
|
||||||
break
|
break
|
||||||
# If this is an m2m relation to self, avoid the inevitable name clash
|
return getattr(self, cache_attr)
|
||||||
elif related.model == related.parent_model:
|
|
||||||
self._m2m_reverse_name_cache = 'to_' + related.parent_model._meta.object_name.lower() + '_id'
|
|
||||||
else:
|
|
||||||
self._m2m_reverse_name_cache = related.parent_model._meta.object_name.lower() + '_id'
|
|
||||||
|
|
||||||
# Return the newly cached value
|
|
||||||
return self._m2m_reverse_name_cache
|
|
||||||
|
|
||||||
def isValidIDList(self, field_data, all_data):
|
def isValidIDList(self, field_data, all_data):
|
||||||
"Validates that the value is a valid list of foreign keys"
|
"Validates that the value is a valid list of foreign keys"
|
||||||
|
@ -919,10 +942,17 @@ class ManyToManyField(RelatedField, Field):
|
||||||
# specify *what* on my non-reversible relation?!"), so we set it up
|
# specify *what* on my non-reversible relation?!"), so we set it up
|
||||||
# automatically. The funky name reduces the chance of an accidental
|
# automatically. The funky name reduces the chance of an accidental
|
||||||
# clash.
|
# clash.
|
||||||
if self.rel.symmetrical and self.rel.to == "self" and self.rel.related_name is None:
|
if self.rel.symmetrical and (self.rel.to == "self" or self.rel.to == cls._meta.object_name):
|
||||||
self.rel.related_name = "%s_rel_+" % name
|
self.rel.related_name = "%s_rel_+" % name
|
||||||
|
|
||||||
super(ManyToManyField, self).contribute_to_class(cls, name)
|
super(ManyToManyField, self).contribute_to_class(cls, name)
|
||||||
|
|
||||||
|
# The intermediate m2m model is not auto created if:
|
||||||
|
# 1) There is a manually specified intermediate, or
|
||||||
|
# 2) The class owning the m2m field is abstract.
|
||||||
|
if not self.rel.through and not cls._meta.abstract:
|
||||||
|
self.rel.through = create_many_to_many_intermediary_model(self, cls)
|
||||||
|
|
||||||
# Add the descriptor for the m2m relation
|
# Add the descriptor for the m2m relation
|
||||||
setattr(cls, self.name, ReverseManyRelatedObjectsDescriptor(self))
|
setattr(cls, self.name, ReverseManyRelatedObjectsDescriptor(self))
|
||||||
|
|
||||||
|
@ -933,11 +963,8 @@ class ManyToManyField(RelatedField, Field):
|
||||||
# work correctly.
|
# work correctly.
|
||||||
if isinstance(self.rel.through, basestring):
|
if isinstance(self.rel.through, basestring):
|
||||||
def resolve_through_model(field, model, cls):
|
def resolve_through_model(field, model, cls):
|
||||||
field.rel.through_model = model
|
field.rel.through = model
|
||||||
add_lazy_relation(cls, self, self.rel.through, resolve_through_model)
|
add_lazy_relation(cls, self, self.rel.through, resolve_through_model)
|
||||||
elif self.rel.through:
|
|
||||||
self.rel.through_model = self.rel.through
|
|
||||||
self.rel.through = self.rel.through._meta.object_name
|
|
||||||
|
|
||||||
if isinstance(self.rel.to, basestring):
|
if isinstance(self.rel.to, basestring):
|
||||||
target = self.rel.to
|
target = self.rel.to
|
||||||
|
@ -946,15 +973,17 @@ class ManyToManyField(RelatedField, Field):
|
||||||
cls._meta.duplicate_targets[self.column] = (target, "m2m")
|
cls._meta.duplicate_targets[self.column] = (target, "m2m")
|
||||||
|
|
||||||
def contribute_to_related_class(self, cls, related):
|
def contribute_to_related_class(self, cls, related):
|
||||||
# m2m relations to self do not have a ManyRelatedObjectsDescriptor,
|
# Internal M2Ms (i.e., those with a related name ending with '+')
|
||||||
# as it would be redundant - unless the field is non-symmetrical.
|
# don't get a related descriptor.
|
||||||
if related.model != related.parent_model or not self.rel.symmetrical:
|
if not self.rel.is_hidden():
|
||||||
# Add the descriptor for the m2m relation
|
|
||||||
setattr(cls, related.get_accessor_name(), ManyRelatedObjectsDescriptor(related))
|
setattr(cls, related.get_accessor_name(), ManyRelatedObjectsDescriptor(related))
|
||||||
|
|
||||||
# Set up the accessors for the column names on the m2m table
|
# Set up the accessors for the column names on the m2m table
|
||||||
self.m2m_column_name = curry(self._get_m2m_column_name, related)
|
self.m2m_column_name = curry(self._get_m2m_attr, related, 'column')
|
||||||
self.m2m_reverse_name = curry(self._get_m2m_reverse_name, related)
|
self.m2m_reverse_name = curry(self._get_m2m_reverse_attr, related, 'column')
|
||||||
|
|
||||||
|
self.m2m_field_name = curry(self._get_m2m_attr, related, 'name')
|
||||||
|
self.m2m_reverse_field_name = curry(self._get_m2m_reverse_attr, related, 'name')
|
||||||
|
|
||||||
def set_attributes_from_rel(self):
|
def set_attributes_from_rel(self):
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -131,18 +131,24 @@ class AppCache(object):
|
||||||
self._populate()
|
self._populate()
|
||||||
return self.app_errors
|
return self.app_errors
|
||||||
|
|
||||||
def get_models(self, app_mod=None):
|
def get_models(self, app_mod=None, include_auto_created=False):
|
||||||
"""
|
"""
|
||||||
Given a module containing models, returns a list of the models.
|
Given a module containing models, returns a list of the models.
|
||||||
Otherwise returns a list of all installed models.
|
Otherwise returns a list of all installed models.
|
||||||
|
|
||||||
|
By default, auto-created models (i.e., m2m models without an
|
||||||
|
explicit intermediate table) are not included. However, if you
|
||||||
|
specify include_auto_created=True, they will be.
|
||||||
"""
|
"""
|
||||||
self._populate()
|
self._populate()
|
||||||
if app_mod:
|
if app_mod:
|
||||||
return self.app_models.get(app_mod.__name__.split('.')[-2], SortedDict()).values()
|
model_list = self.app_models.get(app_mod.__name__.split('.')[-2], SortedDict()).values()
|
||||||
else:
|
else:
|
||||||
model_list = []
|
model_list = []
|
||||||
for app_entry in self.app_models.itervalues():
|
for app_entry in self.app_models.itervalues():
|
||||||
model_list.extend(app_entry.values())
|
model_list.extend(app_entry.values())
|
||||||
|
if not include_auto_created:
|
||||||
|
return filter(lambda o: not o._meta.auto_created, model_list)
|
||||||
return model_list
|
return model_list
|
||||||
|
|
||||||
def get_model(self, app_label, model_name, seed_cache=True):
|
def get_model(self, app_label, model_name, seed_cache=True):
|
||||||
|
|
|
@ -21,7 +21,7 @@ get_verbose_name = lambda class_name: re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|
|
||||||
DEFAULT_NAMES = ('verbose_name', 'db_table', 'ordering',
|
DEFAULT_NAMES = ('verbose_name', 'db_table', 'ordering',
|
||||||
'unique_together', 'permissions', 'get_latest_by',
|
'unique_together', 'permissions', 'get_latest_by',
|
||||||
'order_with_respect_to', 'app_label', 'db_tablespace',
|
'order_with_respect_to', 'app_label', 'db_tablespace',
|
||||||
'abstract', 'managed', 'proxy')
|
'abstract', 'managed', 'proxy', 'auto_created')
|
||||||
|
|
||||||
class Options(object):
|
class Options(object):
|
||||||
def __init__(self, meta, app_label=None):
|
def __init__(self, meta, app_label=None):
|
||||||
|
@ -47,6 +47,7 @@ class Options(object):
|
||||||
self.proxy_for_model = None
|
self.proxy_for_model = None
|
||||||
self.parents = SortedDict()
|
self.parents = SortedDict()
|
||||||
self.duplicate_targets = {}
|
self.duplicate_targets = {}
|
||||||
|
self.auto_created = False
|
||||||
|
|
||||||
# To handle various inheritance situations, we need to track where
|
# To handle various inheritance situations, we need to track where
|
||||||
# managers came from (concrete or abstract base classes).
|
# managers came from (concrete or abstract base classes).
|
||||||
|
@ -487,4 +488,3 @@ class Options(object):
|
||||||
Returns the index of the primary key field in the self.fields list.
|
Returns the index of the primary key field in the self.fields list.
|
||||||
"""
|
"""
|
||||||
return self.fields.index(self.pk)
|
return self.fields.index(self.pk)
|
||||||
|
|
||||||
|
|
|
@ -1028,6 +1028,7 @@ def delete_objects(seen_objs):
|
||||||
|
|
||||||
# Pre-notify all instances to be deleted.
|
# Pre-notify all instances to be deleted.
|
||||||
for pk_val, instance in items:
|
for pk_val, instance in items:
|
||||||
|
if not cls._meta.auto_created:
|
||||||
signals.pre_delete.send(sender=cls, instance=instance)
|
signals.pre_delete.send(sender=cls, instance=instance)
|
||||||
|
|
||||||
pk_list = [pk for pk,instance in items]
|
pk_list = [pk for pk,instance in items]
|
||||||
|
@ -1062,6 +1063,7 @@ def delete_objects(seen_objs):
|
||||||
if field.rel and field.null and field.rel.to in seen_objs:
|
if field.rel and field.null and field.rel.to in seen_objs:
|
||||||
setattr(instance, field.attname, None)
|
setattr(instance, field.attname, None)
|
||||||
|
|
||||||
|
if not cls._meta.auto_created:
|
||||||
signals.post_delete.send(sender=cls, instance=instance)
|
signals.post_delete.send(sender=cls, instance=instance)
|
||||||
setattr(instance, cls._meta.pk.attname, None)
|
setattr(instance, cls._meta.pk.attname, None)
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,9 @@ their deprecation, as per the :ref:`Django deprecation policy
|
||||||
* ``SMTPConnection``. The 1.2 release deprecated the ``SMTPConnection``
|
* ``SMTPConnection``. The 1.2 release deprecated the ``SMTPConnection``
|
||||||
class in favor of a generic E-mail backend API.
|
class in favor of a generic E-mail backend API.
|
||||||
|
|
||||||
|
* The many to many SQL generation functions on the database backends
|
||||||
|
will be removed. These have been deprecated since the 1.2 release.
|
||||||
|
|
||||||
* 2.0
|
* 2.0
|
||||||
* ``django.views.defaults.shortcut()``. This function has been moved
|
* ``django.views.defaults.shortcut()``. This function has been moved
|
||||||
to ``django.contrib.contenttypes.views.shortcut()`` as part of the
|
to ``django.contrib.contenttypes.views.shortcut()`` as part of the
|
||||||
|
|
|
@ -182,6 +182,7 @@ class UniqueM2M(models.Model):
|
||||||
""" Model to test for unique ManyToManyFields, which are invalid. """
|
""" Model to test for unique ManyToManyFields, which are invalid. """
|
||||||
unique_people = models.ManyToManyField( Person, unique=True )
|
unique_people = models.ManyToManyField( Person, unique=True )
|
||||||
|
|
||||||
|
|
||||||
model_errors = """invalid_models.fielderrors: "charfield": CharFields require a "max_length" attribute.
|
model_errors = """invalid_models.fielderrors: "charfield": CharFields require a "max_length" attribute.
|
||||||
invalid_models.fielderrors: "decimalfield": DecimalFields require a "decimal_places" attribute.
|
invalid_models.fielderrors: "decimalfield": DecimalFields require a "decimal_places" attribute.
|
||||||
invalid_models.fielderrors: "decimalfield": DecimalFields require a "max_digits" attribute.
|
invalid_models.fielderrors: "decimalfield": DecimalFields require a "max_digits" attribute.
|
||||||
|
|
|
@ -133,7 +133,7 @@ AttributeError: 'ManyRelatedManager' object has no attribute 'add'
|
||||||
>>> rock.members.create(name='Anne')
|
>>> rock.members.create(name='Anne')
|
||||||
Traceback (most recent call last):
|
Traceback (most recent call last):
|
||||||
...
|
...
|
||||||
AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead.
|
AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model. Use m2m_through.Membership's Manager instead.
|
||||||
|
|
||||||
# Remove has similar complications, and is not provided either.
|
# Remove has similar complications, and is not provided either.
|
||||||
>>> rock.members.remove(jim)
|
>>> rock.members.remove(jim)
|
||||||
|
@ -160,7 +160,7 @@ AttributeError: 'ManyRelatedManager' object has no attribute 'remove'
|
||||||
>>> rock.members = backup
|
>>> rock.members = backup
|
||||||
Traceback (most recent call last):
|
Traceback (most recent call last):
|
||||||
...
|
...
|
||||||
AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead.
|
AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model. Use m2m_through.Membership's Manager instead.
|
||||||
|
|
||||||
# Let's re-save those instances that we've cleared.
|
# Let's re-save those instances that we've cleared.
|
||||||
>>> m1.save()
|
>>> m1.save()
|
||||||
|
@ -184,7 +184,7 @@ AttributeError: 'ManyRelatedManager' object has no attribute 'add'
|
||||||
>>> bob.group_set.create(name='Funk')
|
>>> bob.group_set.create(name='Funk')
|
||||||
Traceback (most recent call last):
|
Traceback (most recent call last):
|
||||||
...
|
...
|
||||||
AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead.
|
AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model. Use m2m_through.Membership's Manager instead.
|
||||||
|
|
||||||
# Remove has similar complications, and is not provided either.
|
# Remove has similar complications, and is not provided either.
|
||||||
>>> jim.group_set.remove(rock)
|
>>> jim.group_set.remove(rock)
|
||||||
|
@ -209,7 +209,7 @@ AttributeError: 'ManyRelatedManager' object has no attribute 'remove'
|
||||||
>>> jim.group_set = backup
|
>>> jim.group_set = backup
|
||||||
Traceback (most recent call last):
|
Traceback (most recent call last):
|
||||||
...
|
...
|
||||||
AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead.
|
AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model. Use m2m_through.Membership's Manager instead.
|
||||||
|
|
||||||
# Let's re-save those instances that we've cleared.
|
# Let's re-save those instances that we've cleared.
|
||||||
>>> m1.save()
|
>>> m1.save()
|
||||||
|
|
|
@ -84,22 +84,22 @@ __test__ = {'API_TESTS':"""
|
||||||
>>> bob.group_set = []
|
>>> bob.group_set = []
|
||||||
Traceback (most recent call last):
|
Traceback (most recent call last):
|
||||||
...
|
...
|
||||||
AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead.
|
AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model. Use m2m_through_regress.Membership's Manager instead.
|
||||||
|
|
||||||
>>> roll.members = []
|
>>> roll.members = []
|
||||||
Traceback (most recent call last):
|
Traceback (most recent call last):
|
||||||
...
|
...
|
||||||
AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead.
|
AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model. Use m2m_through_regress.Membership's Manager instead.
|
||||||
|
|
||||||
>>> rock.members.create(name='Anne')
|
>>> rock.members.create(name='Anne')
|
||||||
Traceback (most recent call last):
|
Traceback (most recent call last):
|
||||||
...
|
...
|
||||||
AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead.
|
AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model. Use m2m_through_regress.Membership's Manager instead.
|
||||||
|
|
||||||
>>> bob.group_set.create(name='Funk')
|
>>> bob.group_set.create(name='Funk')
|
||||||
Traceback (most recent call last):
|
Traceback (most recent call last):
|
||||||
...
|
...
|
||||||
AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead.
|
AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model. Use m2m_through_regress.Membership's Manager instead.
|
||||||
|
|
||||||
# Now test that the intermediate with a relationship outside
|
# Now test that the intermediate with a relationship outside
|
||||||
# the current app (i.e., UserMembership) workds
|
# the current app (i.e., UserMembership) workds
|
||||||
|
|
|
@ -110,6 +110,36 @@ class DerivedM(BaseM):
|
||||||
return "PK = %d, base_name = %s, derived_name = %s" \
|
return "PK = %d, base_name = %s, derived_name = %s" \
|
||||||
% (self.customPK, self.base_name, self.derived_name)
|
% (self.customPK, self.base_name, self.derived_name)
|
||||||
|
|
||||||
|
# Check that abstract classes don't get m2m tables autocreated.
|
||||||
|
class Person(models.Model):
|
||||||
|
name = models.CharField(max_length=100)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ('name',)
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
class AbstractEvent(models.Model):
|
||||||
|
name = models.CharField(max_length=100)
|
||||||
|
attendees = models.ManyToManyField(Person, related_name="%(class)s_set")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
ordering = ('name',)
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
class BirthdayParty(AbstractEvent):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class BachelorParty(AbstractEvent):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class MessyBachelorParty(BachelorParty):
|
||||||
|
pass
|
||||||
|
|
||||||
__test__ = {'API_TESTS':"""
|
__test__ = {'API_TESTS':"""
|
||||||
# Regression for #7350, #7202
|
# Regression for #7350, #7202
|
||||||
# Check that when you create a Parent object with a specific reference to an
|
# Check that when you create a Parent object with a specific reference to an
|
||||||
|
@ -318,5 +348,41 @@ True
|
||||||
>>> ParkingLot3._meta.get_ancestor_link(Place).name # the child->parent link
|
>>> ParkingLot3._meta.get_ancestor_link(Place).name # the child->parent link
|
||||||
"parent"
|
"parent"
|
||||||
|
|
||||||
|
# Check that many-to-many relations defined on an abstract base class
|
||||||
|
# are correctly inherited (and created) on the child class.
|
||||||
|
>>> p1 = Person.objects.create(name='Alice')
|
||||||
|
>>> p2 = Person.objects.create(name='Bob')
|
||||||
|
>>> p3 = Person.objects.create(name='Carol')
|
||||||
|
>>> p4 = Person.objects.create(name='Dave')
|
||||||
|
|
||||||
|
>>> birthday = BirthdayParty.objects.create(name='Birthday party for Alice')
|
||||||
|
>>> birthday.attendees = [p1, p3]
|
||||||
|
|
||||||
|
>>> bachelor = BachelorParty.objects.create(name='Bachelor party for Bob')
|
||||||
|
>>> bachelor.attendees = [p2, p4]
|
||||||
|
|
||||||
|
>>> print p1.birthdayparty_set.all()
|
||||||
|
[<BirthdayParty: Birthday party for Alice>]
|
||||||
|
|
||||||
|
>>> print p1.bachelorparty_set.all()
|
||||||
|
[]
|
||||||
|
|
||||||
|
>>> print p2.bachelorparty_set.all()
|
||||||
|
[<BachelorParty: Bachelor party for Bob>]
|
||||||
|
|
||||||
|
# Check that a subclass of a subclass of an abstract model
|
||||||
|
# doesn't get it's own accessor.
|
||||||
|
>>> p2.messybachelorparty_set.all()
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
AttributeError: 'Person' object has no attribute 'messybachelorparty_set'
|
||||||
|
|
||||||
|
# ... but it does inherit the m2m from it's parent
|
||||||
|
>>> messy = MessyBachelorParty.objects.create(name='Bachelor party for Dave')
|
||||||
|
>>> messy.attendees = [p4]
|
||||||
|
|
||||||
|
>>> p4.bachelorparty_set.all()
|
||||||
|
[<BachelorParty: Bachelor party for Bob>, <BachelorParty: Bachelor party for Dave>]
|
||||||
|
|
||||||
"""}
|
"""}
|
||||||
|
|
||||||
|
|
|
@ -105,6 +105,9 @@ class Anchor(models.Model):
|
||||||
|
|
||||||
data = models.CharField(max_length=30)
|
data = models.CharField(max_length=30)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ('id',)
|
||||||
|
|
||||||
class UniqueAnchor(models.Model):
|
class UniqueAnchor(models.Model):
|
||||||
"""This is a model that can be used as
|
"""This is a model that can be used as
|
||||||
something for other models to point at"""
|
something for other models to point at"""
|
||||||
|
@ -252,4 +255,3 @@ class InheritBaseModel(BaseModel):
|
||||||
class ExplicitInheritBaseModel(BaseModel):
|
class ExplicitInheritBaseModel(BaseModel):
|
||||||
parent = models.OneToOneField(BaseModel)
|
parent = models.OneToOneField(BaseModel)
|
||||||
child_data = models.IntegerField()
|
child_data = models.IntegerField()
|
||||||
|
|
|
@ -0,0 +1,89 @@
|
||||||
|
"""
|
||||||
|
Testing signals before/after saving and deleting.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
class Author(models.Model):
|
||||||
|
name = models.CharField(max_length=20)
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
class Book(models.Model):
|
||||||
|
name = models.CharField(max_length=20)
|
||||||
|
authors = models.ManyToManyField(Author)
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def pre_save_test(signal, sender, instance, **kwargs):
|
||||||
|
print 'pre_save signal,', instance
|
||||||
|
if kwargs.get('raw'):
|
||||||
|
print 'Is raw'
|
||||||
|
|
||||||
|
def post_save_test(signal, sender, instance, **kwargs):
|
||||||
|
print 'post_save signal,', instance
|
||||||
|
if 'created' in kwargs:
|
||||||
|
if kwargs['created']:
|
||||||
|
print 'Is created'
|
||||||
|
else:
|
||||||
|
print 'Is updated'
|
||||||
|
if kwargs.get('raw'):
|
||||||
|
print 'Is raw'
|
||||||
|
|
||||||
|
def pre_delete_test(signal, sender, instance, **kwargs):
|
||||||
|
print 'pre_delete signal,', instance
|
||||||
|
print 'instance.id is not None: %s' % (instance.id != None)
|
||||||
|
|
||||||
|
def post_delete_test(signal, sender, instance, **kwargs):
|
||||||
|
print 'post_delete signal,', instance
|
||||||
|
print 'instance.id is not None: %s' % (instance.id != None)
|
||||||
|
|
||||||
|
__test__ = {'API_TESTS':"""
|
||||||
|
|
||||||
|
# Save up the number of connected signals so that we can check at the end
|
||||||
|
# that all the signals we register get properly unregistered (#9989)
|
||||||
|
>>> pre_signals = (len(models.signals.pre_save.receivers),
|
||||||
|
... len(models.signals.post_save.receivers),
|
||||||
|
... len(models.signals.pre_delete.receivers),
|
||||||
|
... len(models.signals.post_delete.receivers))
|
||||||
|
|
||||||
|
>>> models.signals.pre_save.connect(pre_save_test)
|
||||||
|
>>> models.signals.post_save.connect(post_save_test)
|
||||||
|
>>> models.signals.pre_delete.connect(pre_delete_test)
|
||||||
|
>>> models.signals.post_delete.connect(post_delete_test)
|
||||||
|
|
||||||
|
>>> a1 = Author(name='Neal Stephenson')
|
||||||
|
>>> a1.save()
|
||||||
|
pre_save signal, Neal Stephenson
|
||||||
|
post_save signal, Neal Stephenson
|
||||||
|
Is created
|
||||||
|
|
||||||
|
>>> b1 = Book(name='Snow Crash')
|
||||||
|
>>> b1.save()
|
||||||
|
pre_save signal, Snow Crash
|
||||||
|
post_save signal, Snow Crash
|
||||||
|
Is created
|
||||||
|
|
||||||
|
# Assigning to m2m shouldn't generate an m2m signal
|
||||||
|
>>> b1.authors = [a1]
|
||||||
|
|
||||||
|
# Removing an author from an m2m shouldn't generate an m2m signal
|
||||||
|
>>> b1.authors = []
|
||||||
|
|
||||||
|
>>> models.signals.post_delete.disconnect(post_delete_test)
|
||||||
|
>>> models.signals.pre_delete.disconnect(pre_delete_test)
|
||||||
|
>>> models.signals.post_save.disconnect(post_save_test)
|
||||||
|
>>> models.signals.pre_save.disconnect(pre_save_test)
|
||||||
|
|
||||||
|
# Check that all our signals got disconnected properly.
|
||||||
|
>>> post_signals = (len(models.signals.pre_save.receivers),
|
||||||
|
... len(models.signals.post_save.receivers),
|
||||||
|
... len(models.signals.pre_delete.receivers),
|
||||||
|
... len(models.signals.post_delete.receivers))
|
||||||
|
|
||||||
|
>>> pre_signals == post_signals
|
||||||
|
True
|
||||||
|
|
||||||
|
"""}
|
Loading…
Reference in New Issue