mirror of
https://github.com/django/django.git
synced 2025-10-24 22:26:08 +00:00
Fixed #18823 -- Ensured m2m.clear() works when using through+to_field
There was a potential data-loss issue involved -- when clearing instance's m2m assignments it was possible some other instance's m2m data was deleted instead. This commit also improved None handling for to_field cases.
This commit is contained in:
@@ -573,9 +573,31 @@ def create_many_related_manager(superclass, rel):
|
||||
self.reverse = reverse
|
||||
self.through = through
|
||||
self.prefetch_cache_name = prefetch_cache_name
|
||||
self._pk_val = self.instance.pk
|
||||
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__)
|
||||
self._fk_val = self._get_fk_val(instance, source_field_name)
|
||||
if self._fk_val is None:
|
||||
raise ValueError('"%r" needs to have a value for field "%s" before '
|
||||
'this many-to-many relationship can be used.' %
|
||||
(instance, source_field_name))
|
||||
# Even if this relation is not to pk, we require still pk value.
|
||||
# The wish is that the instance has been already saved to DB,
|
||||
# although having a pk value isn't a guarantee of that.
|
||||
if instance.pk is None:
|
||||
raise ValueError("%r instance needs to have a primary key value before "
|
||||
"a many-to-many relationship can be used." %
|
||||
instance.__class__.__name__)
|
||||
|
||||
|
||||
def _get_fk_val(self, obj, field_name):
|
||||
"""
|
||||
Returns the correct value for this relationship's foreign key. This
|
||||
might be something else than pk value when to_field is used.
|
||||
"""
|
||||
fk = self.through._meta.get_field(field_name)
|
||||
if fk.rel.field_name and fk.rel.field_name != fk.rel.to._meta.pk.attname:
|
||||
attname = fk.rel.get_related_field().get_attname()
|
||||
return fk.get_prep_lookup('exact', getattr(obj, attname))
|
||||
else:
|
||||
return obj.pk
|
||||
|
||||
def get_query_set(self):
|
||||
try:
|
||||
@@ -677,7 +699,11 @@ def create_many_related_manager(superclass, rel):
|
||||
if not router.allow_relation(obj, self.instance):
|
||||
raise ValueError('Cannot add "%r": instance is on database "%s", value is on database "%s"' %
|
||||
(obj, self.instance._state.db, obj._state.db))
|
||||
new_ids.add(obj.pk)
|
||||
fk_val = self._get_fk_val(obj, target_field_name)
|
||||
if fk_val is None:
|
||||
raise ValueError('Cannot add "%r": the value for field "%s" is None' %
|
||||
(obj, target_field_name))
|
||||
new_ids.add(self._get_fk_val(obj, target_field_name))
|
||||
elif isinstance(obj, Model):
|
||||
raise TypeError("'%s' instance expected, got %r" % (self.model._meta.object_name, obj))
|
||||
else:
|
||||
@@ -685,7 +711,7 @@ def create_many_related_manager(superclass, rel):
|
||||
db = router.db_for_write(self.through, instance=self.instance)
|
||||
vals = self.through._default_manager.using(db).values_list(target_field_name, flat=True)
|
||||
vals = vals.filter(**{
|
||||
source_field_name: self._pk_val,
|
||||
source_field_name: self._fk_val,
|
||||
'%s__in' % target_field_name: new_ids,
|
||||
})
|
||||
new_ids = new_ids - set(vals)
|
||||
@@ -699,11 +725,12 @@ def create_many_related_manager(superclass, rel):
|
||||
# Add the ones that aren't there already
|
||||
self.through._default_manager.using(db).bulk_create([
|
||||
self.through(**{
|
||||
'%s_id' % source_field_name: self._pk_val,
|
||||
'%s_id' % source_field_name: self._fk_val,
|
||||
'%s_id' % target_field_name: obj_id,
|
||||
})
|
||||
for obj_id in new_ids
|
||||
])
|
||||
|
||||
if self.reverse or source_field_name == self.source_field_name:
|
||||
# Don't send the signal when we are inserting the
|
||||
# duplicate data row for symmetrical reverse entries.
|
||||
@@ -722,7 +749,7 @@ def create_many_related_manager(superclass, rel):
|
||||
old_ids = set()
|
||||
for obj in objs:
|
||||
if isinstance(obj, self.model):
|
||||
old_ids.add(obj.pk)
|
||||
old_ids.add(self._get_fk_val(obj, target_field_name))
|
||||
else:
|
||||
old_ids.add(obj)
|
||||
# Work out what DB we're operating on
|
||||
@@ -736,7 +763,7 @@ def create_many_related_manager(superclass, rel):
|
||||
model=self.model, pk_set=old_ids, using=db)
|
||||
# Remove the specified objects from the join table
|
||||
self.through._default_manager.using(db).filter(**{
|
||||
source_field_name: self._pk_val,
|
||||
source_field_name: self._fk_val,
|
||||
'%s__in' % target_field_name: old_ids
|
||||
}).delete()
|
||||
if self.reverse or source_field_name == self.source_field_name:
|
||||
@@ -756,7 +783,7 @@ def create_many_related_manager(superclass, rel):
|
||||
instance=self.instance, reverse=self.reverse,
|
||||
model=self.model, pk_set=None, using=db)
|
||||
self.through._default_manager.using(db).filter(**{
|
||||
source_field_name: self._pk_val
|
||||
source_field_name: self._fk_val
|
||||
}).delete()
|
||||
if self.reverse or source_field_name == self.source_field_name:
|
||||
# Don't send the signal when we are clearing the
|
||||
|
||||
Reference in New Issue
Block a user