diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
index c055f4e..07c23b1 100644
a
|
b
|
class BaseModelAdmin(object):
|
154 | 154 | """ |
155 | 155 | Get a form Field for a ManyToManyField. |
156 | 156 | """ |
157 | | # If it uses an intermediary model that isn't auto created, don't show |
158 | | # a field in admin. |
159 | | if not db_field.rel.through._meta.auto_created: |
| 157 | # If it uses an intermediary model that isn't insertable with just the |
| 158 | # related models, don't show a field in admin. |
| 159 | if not db_field.rel.through._meta.insertable_with_only_relationships: |
160 | 160 | return None |
161 | 161 | |
162 | 162 | if db_field.name in self.raw_id_fields: |
diff --git a/django/contrib/admin/validation.py b/django/contrib/admin/validation.py
index 726da65..937fcfd 100644
a
|
b
|
def validate_base(cls, model):
|
197 | 197 | for field in cls.fields: |
198 | 198 | check_formfield(cls, model, opts, 'fields', field) |
199 | 199 | f = get_field(cls, model, opts, 'fields', field) |
200 | | if isinstance(f, models.ManyToManyField) and not f.rel.through._meta.auto_created: |
| 200 | if isinstance(f, models.ManyToManyField) and not f.rel.through._meta.insertable_with_only_relationships: |
201 | 201 | raise ImproperlyConfigured("'%s.fields' can't include the ManyToManyField " |
202 | | "field '%s' because '%s' manually specifies " |
203 | | "a 'through' model." % (cls.__name__, field, field)) |
| 202 | "field '%s' because '%s' has a 'through' model that is not marked " |
| 203 | "as safe to insert." % (cls.__name__, field, field)) |
204 | 204 | if cls.fieldsets: |
205 | 205 | raise ImproperlyConfigured('Both fieldsets and fields are specified in %s.' % cls.__name__) |
206 | 206 | if len(cls.fields) > len(set(cls.fields)): |
… |
… |
def validate_base(cls, model):
|
228 | 228 | check_formfield(cls, model, opts, "fieldsets[%d][1]['fields']" % idx, field) |
229 | 229 | try: |
230 | 230 | f = opts.get_field(field) |
231 | | if isinstance(f, models.ManyToManyField) and not f.rel.through._meta.auto_created: |
| 231 | if isinstance(f, models.ManyToManyField) and not f.rel.through._meta.insertable_with_only_relationships: |
232 | 232 | raise ImproperlyConfigured("'%s.fieldsets[%d][1]['fields']' " |
233 | 233 | "can't include the ManyToManyField field '%s' because " |
234 | | "'%s' manually specifies a 'through' model." % ( |
235 | | cls.__name__, idx, field, field)) |
| 234 | "'%s' has a 'through' model that is not marked as safe " |
| 235 | "to insert." % (cls.__name__, idx, field, field)) |
236 | 236 | except models.FieldDoesNotExist: |
237 | 237 | # If we can't find a field on the model that matches, |
238 | 238 | # it could be an extra field on the form. |
diff --git a/django/core/management/validation.py b/django/core/management/validation.py
index 97164d7..bc0c029 100644
a
|
b
|
def get_validation_errors(outfile, app=None):
|
120 | 120 | |
121 | 121 | if f.rel.through is not None and not isinstance(f.rel.through, basestring): |
122 | 122 | from_model, to_model = cls, f.rel.to |
123 | | if from_model == to_model and f.rel.symmetrical and not f.rel.through._meta.auto_created: |
124 | | e.add(opts, "Many-to-many fields with intermediate tables cannot be symmetrical.") |
| 123 | if from_model == to_model and f.rel.symmetrical and not f.rel.through._meta.insertable_with_only_relationships: |
| 124 | e.add(opts, "Many-to-many fields with 'through' models that are not marked " |
| 125 | "as safe to insert cannot be symmetrical.") |
125 | 126 | seen_from, seen_to, seen_self = False, False, 0 |
126 | 127 | for inter_field in f.rel.through._meta.fields: |
127 | 128 | rel_to = getattr(inter_field.rel, 'to', None) |
diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py
index fcdda22..e4724d3 100644
a
|
b
|
def create_many_related_manager(superclass, rel=False):
|
427 | 427 | def get_query_set(self): |
428 | 428 | return superclass.get_query_set(self)._next_is_sticky().filter(**(self.core_filters)) |
429 | 429 | |
430 | | # If the ManyToMany relation has an intermediary model, |
431 | | # the add and remove methods do not exist. |
432 | | if rel.through._meta.auto_created: |
| 430 | # Check that the through model can be created without additional fields |
| 431 | # before attaching teh add and remove methods |
| 432 | if through._meta.insertable_with_only_relationships: |
433 | 433 | def add(self, *objs): |
434 | 434 | self._add_items(self.source_field_name, self.target_field_name, *objs) |
435 | 435 | |
… |
… |
def create_many_related_manager(superclass, rel=False):
|
457 | 457 | def create(self, **kwargs): |
458 | 458 | # This check needs to be done here, since we can't later remove this |
459 | 459 | # from the method lookup table, as we do with add and remove. |
460 | | if not rel.through._meta.auto_created: |
461 | | opts = through._meta |
462 | | 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) |
| 460 | opts = through._meta |
| 461 | if not opts.insertable_with_only_relationships: |
| 462 | raise AttributeError, "Cannot use create() on this ManyToManyField. Use %s.%s's Manager or set insertable_with_only_relationships on its Meta if appropriate." % (opts.app_label, opts.object_name) |
463 | 463 | new_obj = super(ManyRelatedManager, self).create(**kwargs) |
464 | 464 | self.add(new_obj) |
465 | 465 | return new_obj |
… |
… |
class ManyRelatedObjectsDescriptor(object):
|
569 | 569 | if instance is None: |
570 | 570 | raise AttributeError, "Manager must be accessed via instance" |
571 | 571 | |
572 | | if not self.related.field.rel.through._meta.auto_created: |
573 | | opts = self.related.field.rel.through._meta |
574 | | 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) |
| 572 | opts = self.related.field.rel.through._meta |
| 573 | if not opts.insertable_with_only_relationships: |
| 574 | raise AttributeError, "Cannot set values on this ManyToManyField. Use %s.%s's Manager or set insertable_with_only_relationships on its Meta if appropriate." % (opts.app_label, opts.object_name) |
575 | 575 | |
576 | 576 | manager = self.__get__(instance) |
577 | 577 | manager.clear() |
… |
… |
class ReverseManyRelatedObjectsDescriptor(object):
|
619 | 619 | if instance is None: |
620 | 620 | raise AttributeError, "Manager must be accessed via instance" |
621 | 621 | |
622 | | if not self.field.rel.through._meta.auto_created: |
623 | | opts = self.field.rel.through._meta |
624 | | 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) |
| 622 | opts = self.field.rel.through._meta |
| 623 | if not opts.insertable_with_only_relationships: |
| 624 | raise AttributeError, "Cannot set values on this ManyToManyField. Use %s.%s's Manager or set insertable_with_only_relationships on its Meta if appropriate." % (opts.app_label, opts.object_name) |
625 | 625 | |
626 | 626 | manager = self.__get__(instance) |
627 | 627 | manager.clear() |
diff --git a/django/db/models/options.py b/django/db/models/options.py
index 05ff54a..f58a8f3 100644
a
|
b
|
from django.utils.datastructures import SortedDict
|
18 | 18 | # Calculate the verbose_name by converting from InitialCaps to "lowercase with spaces". |
19 | 19 | get_verbose_name = lambda class_name: re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', ' \\1', class_name).lower().strip() |
20 | 20 | |
21 | | DEFAULT_NAMES = ('verbose_name', 'db_table', 'ordering', |
22 | | 'unique_together', 'permissions', 'get_latest_by', |
23 | | 'order_with_respect_to', 'app_label', 'db_tablespace', |
24 | | 'abstract', 'managed', 'proxy', 'auto_created') |
| 21 | DEFAULT_NAMES = ('verbose_name', 'db_table', 'ordering', 'unique_together', |
| 22 | 'permissions', 'get_latest_by', 'order_with_respect_to', |
| 23 | 'app_label', 'db_tablespace', 'abstract', 'managed', 'proxy', |
| 24 | 'auto_created', 'insertable_with_only_relationships') |
25 | 25 | |
26 | 26 | class Options(object): |
27 | 27 | def __init__(self, meta, app_label=None): |
… |
… |
class Options(object):
|
48 | 48 | self.parents = SortedDict() |
49 | 49 | self.duplicate_targets = {} |
50 | 50 | self.auto_created = False |
| 51 | self.insertable_with_only_relationships = None |
51 | 52 | |
52 | 53 | # To handle various inheritance situations, we need to track where |
53 | 54 | # managers came from (concrete or abstract base classes). |
… |
… |
class Options(object):
|
104 | 105 | self.db_table = "%s_%s" % (self.app_label, self.module_name) |
105 | 106 | self.db_table = truncate_name(self.db_table, connection.ops.max_name_length()) |
106 | 107 | |
| 108 | if self.insertable_with_only_relationships == None: |
| 109 | self.insertable_with_only_relationships = self.auto_created |
107 | 110 | |
108 | 111 | def _prepare(self, model): |
109 | 112 | if self.order_with_respect_to: |
diff --git a/docs/ref/models/options.txt b/docs/ref/models/options.txt
index d74f835..8b65989 100644
a
|
b
|
Example::
|
86 | 86 | |
87 | 87 | See the docs for :meth:`~django.db.models.QuerySet.latest` for more. |
88 | 88 | |
| 89 | ``insertable_with_only_relationships`` |
| 90 | ----------------------- |
| 91 | |
| 92 | .. attribute:: Options.insertable_with_only_relationships |
| 93 | |
| 94 | .. versionadded:: 1.2 |
| 95 | |
| 96 | By default if you specify an explicit model for :attr:`ManyToManyField.through` |
| 97 | you lose the convience methods of .add and .remove on field as Django can't be |
| 98 | sure it is safe to create new records with only the two model relationships. If |
| 99 | you want to retain this behavior you can set this to True. |
| 100 | |
89 | 101 | ``managed`` |
90 | 102 | ----------------------- |
91 | 103 | |
diff --git a/tests/modeltests/invalid_models/models.py b/tests/modeltests/invalid_models/models.py
index af19963..10657d2 100644
a
|
b
|
invalid_models.grouptwo: 'secondary' has a manually-defined m2m relation through
|
273 | 273 | invalid_models.missingmanualm2mmodel: 'missing_m2m' specifies an m2m relation through model MissingM2MModel, which has not been installed |
274 | 274 | invalid_models.group: The model Group has two manually-defined m2m relations through the model Membership, which is not permitted. Please consider using an extra field on your intermediary model instead. |
275 | 275 | invalid_models.group: Intermediary model RelationshipDoubleFK has more than one foreign key to Person, which is ambiguous and is not permitted. |
276 | | invalid_models.personselfrefm2m: Many-to-many fields with intermediate tables cannot be symmetrical. |
| 276 | invalid_models.personselfrefm2m: Many-to-many fields with 'through' models that are not marked as safe to insert cannot be symmetrical. |
277 | 277 | invalid_models.personselfrefm2m: Intermediary model RelationshipTripleFK has more than two foreign keys to PersonSelfRefM2M, which is ambiguous and is not permitted. |
278 | | invalid_models.personselfrefm2mexplicit: Many-to-many fields with intermediate tables cannot be symmetrical. |
| 278 | invalid_models.personselfrefm2mexplicit: Many-to-many fields with 'through' models that are not marked as safe to insert cannot be symmetrical. |
279 | 279 | invalid_models.abstractrelationmodel: 'fk1' has a relation with model AbstractModel, which has either not been installed or is abstract. |
280 | 280 | invalid_models.abstractrelationmodel: 'fk2' has an m2m relation with model AbstractModel, which has either not been installed or is abstract. |
281 | 281 | invalid_models.uniquem2m: ManyToManyFields cannot be unique. Remove the unique argument on 'unique_people'. |
diff --git a/tests/modeltests/m2m_through/models.py b/tests/modeltests/m2m_through/models.py
index 16f303d..ad35fca 100644
a
|
b
|
AttributeError: 'ManyRelatedManager' object has no attribute 'add'
|
133 | 133 | >>> rock.members.create(name='Anne') |
134 | 134 | Traceback (most recent call last): |
135 | 135 | ... |
136 | | AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model. Use m2m_through.Membership's Manager instead. |
| 136 | AttributeError: Cannot use create() on this ManyToManyField. Use m2m_through.Membership's Manager or set insertable_with_only_relationships on its Meta if appropriate. |
137 | 137 | |
138 | 138 | # Remove has similar complications, and is not provided either. |
139 | 139 | >>> rock.members.remove(jim) |
… |
… |
AttributeError: 'ManyRelatedManager' object has no attribute 'remove'
|
160 | 160 | >>> rock.members = backup |
161 | 161 | Traceback (most recent call last): |
162 | 162 | ... |
163 | | AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model. Use m2m_through.Membership's Manager instead. |
| 163 | AttributeError: Cannot set values on this ManyToManyField. Use m2m_through.Membership's Manager or set insertable_with_only_relationships on its Meta if appropriate. |
164 | 164 | |
165 | 165 | # Let's re-save those instances that we've cleared. |
166 | 166 | >>> m1.save() |
… |
… |
AttributeError: 'ManyRelatedManager' object has no attribute 'add'
|
184 | 184 | >>> bob.group_set.create(name='Funk') |
185 | 185 | Traceback (most recent call last): |
186 | 186 | ... |
187 | | AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model. Use m2m_through.Membership's Manager instead. |
| 187 | AttributeError: Cannot use create() on this ManyToManyField. Use m2m_through.Membership's Manager or set insertable_with_only_relationships on its Meta if appropriate. |
188 | 188 | |
189 | 189 | # Remove has similar complications, and is not provided either. |
190 | 190 | >>> jim.group_set.remove(rock) |
… |
… |
AttributeError: 'ManyRelatedManager' object has no attribute 'remove'
|
209 | 209 | >>> jim.group_set = backup |
210 | 210 | Traceback (most recent call last): |
211 | 211 | ... |
212 | | AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model. Use m2m_through.Membership's Manager instead. |
| 212 | AttributeError: Cannot set values on this ManyToManyField. Use m2m_through.Membership's Manager or set insertable_with_only_relationships on its Meta if appropriate. |
213 | 213 | |
214 | 214 | # Let's re-save those instances that we've cleared. |
215 | 215 | >>> m1.save() |
diff --git a/tests/regressiontests/admin_validation/models.py b/tests/regressiontests/admin_validation/models.py
index eb53a9d..a69bb2f 100644
a
|
b
|
Exception: <class 'regressiontests.admin_validation.models.TwoAlbumFKAndAnE'> ha
|
120 | 120 | >>> validate(BookAdmin, Book) |
121 | 121 | Traceback (most recent call last): |
122 | 122 | ... |
123 | | ImproperlyConfigured: 'BookAdmin.fields' can't include the ManyToManyField field 'authors' because 'authors' manually specifies a 'through' model. |
| 123 | ImproperlyConfigured: 'BookAdmin.fields' can't include the ManyToManyField field 'authors' because 'authors' has a 'through' model that is not marked as safe to insert. |
124 | 124 | |
125 | 125 | >>> class FieldsetBookAdmin(admin.ModelAdmin): |
126 | 126 | ... fieldsets = ( |
… |
… |
ImproperlyConfigured: 'BookAdmin.fields' can't include the ManyToManyField field
|
131 | 131 | >>> validate(FieldsetBookAdmin, Book) |
132 | 132 | Traceback (most recent call last): |
133 | 133 | ... |
134 | | ImproperlyConfigured: 'FieldsetBookAdmin.fieldsets[1][1]['fields']' can't include the ManyToManyField field 'authors' because 'authors' manually specifies a 'through' model. |
| 134 | ImproperlyConfigured: 'FieldsetBookAdmin.fieldsets[1][1]['fields']' can't include the ManyToManyField field 'authors' because 'authors' has a 'through' model that is not marked as safe to insert. |
135 | 135 | |
136 | 136 | >>> class NestedFieldsetAdmin(admin.ModelAdmin): |
137 | 137 | ... fieldsets = ( |
diff --git a/tests/regressiontests/m2m_through_regress/models.py b/tests/regressiontests/m2m_through_regress/models.py
index 56aecd6..9c5b50d 100644
a
|
b
|
__test__ = {'API_TESTS':"""
|
84 | 84 | >>> bob.group_set = [] |
85 | 85 | Traceback (most recent call last): |
86 | 86 | ... |
87 | | AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model. Use m2m_through_regress.Membership's Manager instead. |
| 87 | AttributeError: Cannot set values on this ManyToManyField. Use m2m_through_regress.Membership's Manager or set insertable_with_only_relationships on its Meta if appropriate. |
88 | 88 | |
89 | 89 | >>> roll.members = [] |
90 | 90 | Traceback (most recent call last): |
91 | 91 | ... |
92 | | AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model. Use m2m_through_regress.Membership's Manager instead. |
| 92 | AttributeError: Cannot set values on this ManyToManyField. Use m2m_through_regress.Membership's Manager or set insertable_with_only_relationships on its Meta if appropriate. |
93 | 93 | |
94 | 94 | >>> rock.members.create(name='Anne') |
95 | 95 | Traceback (most recent call last): |
96 | 96 | ... |
97 | | AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model. Use m2m_through_regress.Membership's Manager instead. |
| 97 | AttributeError: Cannot use create() on this ManyToManyField. Use m2m_through_regress.Membership's Manager or set insertable_with_only_relationships on its Meta if appropriate. |
98 | 98 | |
99 | 99 | >>> bob.group_set.create(name='Funk') |
100 | 100 | Traceback (most recent call last): |
101 | 101 | ... |
102 | | AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model. Use m2m_through_regress.Membership's Manager instead. |
| 102 | AttributeError: Cannot use create() on this ManyToManyField. Use m2m_through_regress.Membership's Manager or set insertable_with_only_relationships on its Meta if appropriate. |
103 | 103 | |
104 | 104 | # Now test that the intermediate with a relationship outside |
105 | 105 | # the current app (i.e., UserMembership) workds |