Code

Ticket #9475: 9475-r11858.diff

File 9475-r11858.diff, 17.8 KB (added by Travis Cline <travis.cline@…>, 5 years ago)
Line 
1diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
2index c055f4e..07c23b1 100644
3--- a/django/contrib/admin/options.py
4+++ b/django/contrib/admin/options.py
5@@ -154,9 +154,9 @@ class BaseModelAdmin(object):
6         """
7         Get a form Field for a ManyToManyField.
8         """
9-        # If it uses an intermediary model that isn't auto created, don't show
10-        # a field in admin.
11-        if not db_field.rel.through._meta.auto_created:
12+        # If it uses an intermediary model that isn't insertable with just the
13+        # related models, don't show a field in admin.
14+        if not db_field.rel.through._meta.insertable_with_only_relationships:
15             return None
16 
17         if db_field.name in self.raw_id_fields:
18diff --git a/django/contrib/admin/validation.py b/django/contrib/admin/validation.py
19index 726da65..937fcfd 100644
20--- a/django/contrib/admin/validation.py
21+++ b/django/contrib/admin/validation.py
22@@ -197,10 +197,10 @@ def validate_base(cls, model):
23         for field in cls.fields:
24             check_formfield(cls, model, opts, 'fields', field)
25             f = get_field(cls, model, opts, 'fields', field)
26-            if isinstance(f, models.ManyToManyField) and not f.rel.through._meta.auto_created:
27+            if isinstance(f, models.ManyToManyField) and not f.rel.through._meta.insertable_with_only_relationships:
28                 raise ImproperlyConfigured("'%s.fields' can't include the ManyToManyField "
29-                    "field '%s' because '%s' manually specifies "
30-                    "a 'through' model." % (cls.__name__, field, field))
31+                    "field '%s' because '%s' has a 'through' model that is not marked "
32+                    "as safe to insert." % (cls.__name__, field, field))
33         if cls.fieldsets:
34             raise ImproperlyConfigured('Both fieldsets and fields are specified in %s.' % cls.__name__)
35         if len(cls.fields) > len(set(cls.fields)):
36@@ -228,11 +228,11 @@ def validate_base(cls, model):
37                     check_formfield(cls, model, opts, "fieldsets[%d][1]['fields']" % idx, field)
38                     try:
39                         f = opts.get_field(field)
40-                        if isinstance(f, models.ManyToManyField) and not f.rel.through._meta.auto_created:
41+                        if isinstance(f, models.ManyToManyField) and not f.rel.through._meta.insertable_with_only_relationships:
42                             raise ImproperlyConfigured("'%s.fieldsets[%d][1]['fields']' "
43                                 "can't include the ManyToManyField field '%s' because "
44-                                "'%s' manually specifies a 'through' model." % (
45-                                    cls.__name__, idx, field, field))
46+                                "'%s' has a 'through' model that is not marked as safe "
47+                                "to insert." % (cls.__name__, idx, field, field))
48                     except models.FieldDoesNotExist:
49                         # If we can't find a field on the model that matches,
50                         # it could be an extra field on the form.
51diff --git a/django/core/management/validation.py b/django/core/management/validation.py
52index 97164d7..bc0c029 100644
53--- a/django/core/management/validation.py
54+++ b/django/core/management/validation.py
55@@ -120,8 +120,9 @@ def get_validation_errors(outfile, app=None):
56 
57             if f.rel.through is not None and not isinstance(f.rel.through, basestring):
58                 from_model, to_model = cls, f.rel.to
59-                if from_model == to_model and f.rel.symmetrical and not f.rel.through._meta.auto_created:
60-                    e.add(opts, "Many-to-many fields with intermediate tables cannot be symmetrical.")
61+                if from_model == to_model and f.rel.symmetrical and not f.rel.through._meta.insertable_with_only_relationships:
62+                    e.add(opts, "Many-to-many fields with 'through' models that are not marked "
63+                          "as safe to insert cannot be symmetrical.")
64                 seen_from, seen_to, seen_self = False, False, 0
65                 for inter_field in f.rel.through._meta.fields:
66                     rel_to = getattr(inter_field.rel, 'to', None)
67diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py
68index fcdda22..e4724d3 100644
69--- a/django/db/models/fields/related.py
70+++ b/django/db/models/fields/related.py
71@@ -427,9 +427,9 @@ def create_many_related_manager(superclass, rel=False):
72         def get_query_set(self):
73             return superclass.get_query_set(self)._next_is_sticky().filter(**(self.core_filters))
74 
75-        # If the ManyToMany relation has an intermediary model,
76-        # the add and remove methods do not exist.
77-        if rel.through._meta.auto_created:
78+        # Check that the through model can be created without additional fields
79+        # before attaching teh add and remove methods
80+        if through._meta.insertable_with_only_relationships:
81             def add(self, *objs):
82                 self._add_items(self.source_field_name, self.target_field_name, *objs)
83 
84@@ -457,9 +457,9 @@ def create_many_related_manager(superclass, rel=False):
85         def create(self, **kwargs):
86             # This check needs to be done here, since we can't later remove this
87             # from the method lookup table, as we do with add and remove.
88-            if not rel.through._meta.auto_created:
89-                opts = through._meta
90-                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)
91+            opts = through._meta
92+            if not opts.insertable_with_only_relationships:
93+                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)
94             new_obj = super(ManyRelatedManager, self).create(**kwargs)
95             self.add(new_obj)
96             return new_obj
97@@ -569,9 +569,9 @@ class ManyRelatedObjectsDescriptor(object):
98         if instance is None:
99             raise AttributeError, "Manager must be accessed via instance"
100 
101-        if not self.related.field.rel.through._meta.auto_created:
102-            opts = self.related.field.rel.through._meta
103-            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)
104+        opts = self.related.field.rel.through._meta
105+        if not opts.insertable_with_only_relationships:
106+            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)
107 
108         manager = self.__get__(instance)
109         manager.clear()
110@@ -619,9 +619,9 @@ class ReverseManyRelatedObjectsDescriptor(object):
111         if instance is None:
112             raise AttributeError, "Manager must be accessed via instance"
113 
114-        if not self.field.rel.through._meta.auto_created:
115-            opts = self.field.rel.through._meta
116-            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)
117+        opts = self.field.rel.through._meta
118+        if not opts.insertable_with_only_relationships:
119+            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)
120 
121         manager = self.__get__(instance)
122         manager.clear()
123diff --git a/django/db/models/options.py b/django/db/models/options.py
124index 05ff54a..f58a8f3 100644
125--- a/django/db/models/options.py
126+++ b/django/db/models/options.py
127@@ -18,10 +18,10 @@ from django.utils.datastructures import SortedDict
128 # Calculate the verbose_name by converting from InitialCaps to "lowercase with spaces".
129 get_verbose_name = lambda class_name: re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', ' \\1', class_name).lower().strip()
130 
131-DEFAULT_NAMES = ('verbose_name', 'db_table', 'ordering',
132-                 'unique_together', 'permissions', 'get_latest_by',
133-                 'order_with_respect_to', 'app_label', 'db_tablespace',
134-                 'abstract', 'managed', 'proxy', 'auto_created')
135+DEFAULT_NAMES = ('verbose_name', 'db_table', 'ordering', 'unique_together',
136+                 'permissions', 'get_latest_by', 'order_with_respect_to',
137+                 'app_label', 'db_tablespace', 'abstract', 'managed', 'proxy',
138+                 'auto_created', 'insertable_with_only_relationships')
139 
140 class Options(object):
141     def __init__(self, meta, app_label=None):
142@@ -48,6 +48,7 @@ class Options(object):
143         self.parents = SortedDict()
144         self.duplicate_targets = {}
145         self.auto_created = False
146+        self.insertable_with_only_relationships = None
147 
148         # To handle various inheritance situations, we need to track where
149         # managers came from (concrete or abstract base classes).
150@@ -104,6 +105,8 @@ class Options(object):
151             self.db_table = "%s_%s" % (self.app_label, self.module_name)
152             self.db_table = truncate_name(self.db_table, connection.ops.max_name_length())
153 
154+        if self.insertable_with_only_relationships == None:
155+            self.insertable_with_only_relationships = self.auto_created
156 
157     def _prepare(self, model):
158         if self.order_with_respect_to:
159diff --git a/docs/ref/models/options.txt b/docs/ref/models/options.txt
160index d74f835..8b65989 100644
161--- a/docs/ref/models/options.txt
162+++ b/docs/ref/models/options.txt
163@@ -86,6 +86,18 @@ Example::
164 
165 See the docs for :meth:`~django.db.models.QuerySet.latest` for more.
166 
167+``insertable_with_only_relationships``
168+-----------------------
169+
170+.. attribute:: Options.insertable_with_only_relationships
171+
172+.. versionadded:: 1.2
173+
174+By default if you specify an explicit model for :attr:`ManyToManyField.through`
175+you lose the convience methods of .add and .remove on field as Django can't be
176+sure it is safe to create new records with only the two model relationships. If
177+you want to retain this behavior you can set this to True.
178+
179 ``managed``
180 -----------------------
181 
182diff --git a/tests/modeltests/invalid_models/models.py b/tests/modeltests/invalid_models/models.py
183index af19963..10657d2 100644
184--- a/tests/modeltests/invalid_models/models.py
185+++ b/tests/modeltests/invalid_models/models.py
186@@ -273,9 +273,9 @@ invalid_models.grouptwo: 'secondary' has a manually-defined m2m relation through
187 invalid_models.missingmanualm2mmodel: 'missing_m2m' specifies an m2m relation through model MissingM2MModel, which has not been installed
188 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.
189 invalid_models.group: Intermediary model RelationshipDoubleFK has more than one foreign key to Person, which is ambiguous and is not permitted.
190-invalid_models.personselfrefm2m: Many-to-many fields with intermediate tables cannot be symmetrical.
191+invalid_models.personselfrefm2m: Many-to-many fields with 'through' models that are not marked as safe to insert cannot be symmetrical.
192 invalid_models.personselfrefm2m: Intermediary model RelationshipTripleFK has more than two foreign keys to PersonSelfRefM2M, which is ambiguous and is not permitted.
193-invalid_models.personselfrefm2mexplicit: Many-to-many fields with intermediate tables cannot be symmetrical.
194+invalid_models.personselfrefm2mexplicit: Many-to-many fields with 'through' models that are not marked as safe to insert cannot be symmetrical.
195 invalid_models.abstractrelationmodel: 'fk1' has a relation with model AbstractModel, which has either not been installed or is abstract.
196 invalid_models.abstractrelationmodel: 'fk2' has an m2m relation with model AbstractModel, which has either not been installed or is abstract.
197 invalid_models.uniquem2m: ManyToManyFields cannot be unique.  Remove the unique argument on 'unique_people'.
198diff --git a/tests/modeltests/m2m_through/models.py b/tests/modeltests/m2m_through/models.py
199index 16f303d..ad35fca 100644
200--- a/tests/modeltests/m2m_through/models.py
201+++ b/tests/modeltests/m2m_through/models.py
202@@ -133,7 +133,7 @@ AttributeError: 'ManyRelatedManager' object has no attribute 'add'
203 >>> rock.members.create(name='Anne')
204 Traceback (most recent call last):
205 ...
206-AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model. Use m2m_through.Membership's Manager instead.
207+AttributeError: Cannot use create() on this ManyToManyField. Use m2m_through.Membership's Manager or set insertable_with_only_relationships on its Meta if appropriate.
208 
209 # Remove has similar complications, and is not provided either.
210 >>> rock.members.remove(jim)
211@@ -160,7 +160,7 @@ AttributeError: 'ManyRelatedManager' object has no attribute 'remove'
212 >>> rock.members = backup
213 Traceback (most recent call last):
214 ...
215-AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model.  Use m2m_through.Membership's Manager instead.
216+AttributeError: Cannot set values on this ManyToManyField. Use m2m_through.Membership's Manager or set insertable_with_only_relationships on its Meta if appropriate.
217 
218 # Let's re-save those instances that we've cleared.
219 >>> m1.save()
220@@ -184,7 +184,7 @@ AttributeError: 'ManyRelatedManager' object has no attribute 'add'
221 >>> bob.group_set.create(name='Funk')
222 Traceback (most recent call last):
223 ...
224-AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model. Use m2m_through.Membership's Manager instead.
225+AttributeError: Cannot use create() on this ManyToManyField. Use m2m_through.Membership's Manager or set insertable_with_only_relationships on its Meta if appropriate.
226 
227 # Remove has similar complications, and is not provided either.
228 >>> jim.group_set.remove(rock)
229@@ -209,7 +209,7 @@ AttributeError: 'ManyRelatedManager' object has no attribute 'remove'
230 >>> jim.group_set = backup
231 Traceback (most recent call last):
232 ...
233-AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model.  Use m2m_through.Membership's Manager instead.
234+AttributeError: Cannot set values on this ManyToManyField. Use m2m_through.Membership's Manager or set insertable_with_only_relationships on its Meta if appropriate.
235 
236 # Let's re-save those instances that we've cleared.
237 >>> m1.save()
238diff --git a/tests/regressiontests/admin_validation/models.py b/tests/regressiontests/admin_validation/models.py
239index eb53a9d..a69bb2f 100644
240--- a/tests/regressiontests/admin_validation/models.py
241+++ b/tests/regressiontests/admin_validation/models.py
242@@ -120,7 +120,7 @@ Exception: <class 'regressiontests.admin_validation.models.TwoAlbumFKAndAnE'> ha
243 >>> validate(BookAdmin, Book)
244 Traceback (most recent call last):
245     ...
246-ImproperlyConfigured: 'BookAdmin.fields' can't include the ManyToManyField field 'authors' because 'authors' manually specifies a 'through' model.
247+ImproperlyConfigured: 'BookAdmin.fields' can't include the ManyToManyField field 'authors' because 'authors' has a 'through' model that is not marked as safe to insert.
248 
249 >>> class FieldsetBookAdmin(admin.ModelAdmin):
250 ...     fieldsets = (
251@@ -131,7 +131,7 @@ ImproperlyConfigured: 'BookAdmin.fields' can't include the ManyToManyField field
252 >>> validate(FieldsetBookAdmin, Book)
253 Traceback (most recent call last):
254    ...
255-ImproperlyConfigured: 'FieldsetBookAdmin.fieldsets[1][1]['fields']' can't include the ManyToManyField field 'authors' because 'authors' manually specifies a 'through' model.
256+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.
257 
258 >>> class NestedFieldsetAdmin(admin.ModelAdmin):
259 ...    fieldsets = (
260diff --git a/tests/regressiontests/m2m_through_regress/models.py b/tests/regressiontests/m2m_through_regress/models.py
261index 56aecd6..9c5b50d 100644
262--- a/tests/regressiontests/m2m_through_regress/models.py
263+++ b/tests/regressiontests/m2m_through_regress/models.py
264@@ -84,22 +84,22 @@ __test__ = {'API_TESTS':"""
265 >>> bob.group_set = []
266 Traceback (most recent call last):
267 ...
268-AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model.  Use m2m_through_regress.Membership's Manager instead.
269+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.
270 
271 >>> roll.members = []
272 Traceback (most recent call last):
273 ...
274-AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model.  Use m2m_through_regress.Membership's Manager instead.
275+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.
276 
277 >>> rock.members.create(name='Anne')
278 Traceback (most recent call last):
279 ...
280-AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model.  Use m2m_through_regress.Membership's Manager instead.
281+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.
282 
283 >>> bob.group_set.create(name='Funk')
284 Traceback (most recent call last):
285 ...
286-AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model.  Use m2m_through_regress.Membership's Manager instead.
287+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.
288 
289 # Now test that the intermediate with a relationship outside
290 # the current app (i.e., UserMembership) workds