diff options
| author | Daniel Izquierdo <daniel@makeleaps.com> | 2016-10-10 16:23:35 +0900 |
|---|---|---|
| committer | Mariusz Felisiak <felisiak.mariusz@gmail.com> | 2019-11-19 10:55:05 +0100 |
| commit | 89abecc75d7eadab681f29be5237972c6c2f997b (patch) | |
| tree | 0f0f5bcf8480308dec2899536ae9ce11b3bb555a /django | |
| parent | 4e1d809aa570c0e0736587672607f9d6c22e42c9 (diff) | |
Fixed #27272 -- Added an on_delete RESTRICT handler to allow cascading deletions while protecting direct ones.
Diffstat (limited to 'django')
| -rw-r--r-- | django/contrib/admin/utils.py | 2 | ||||
| -rw-r--r-- | django/db/models/__init__.py | 7 | ||||
| -rw-r--r-- | django/db/models/deletion.py | 74 |
3 files changed, 75 insertions, 8 deletions
diff --git a/django/contrib/admin/utils.py b/django/contrib/admin/utils.py index d3c62f6b0e..06f85d41f9 100644 --- a/django/contrib/admin/utils.py +++ b/django/contrib/admin/utils.py @@ -181,6 +181,8 @@ class NestedObjects(Collector): return super().collect(objs, source_attr=source_attr, **kwargs) except models.ProtectedError as e: self.protected.update(e.protected_objects) + except models.RestrictedError as e: + self.protected.update(e.restricted_objects) def related_objects(self, related_model, related_fields, objs): qs = super().related_objects(related_model, related_fields, objs) diff --git a/django/db/models/__init__.py b/django/db/models/__init__.py index cca87ed0e7..06bd72f8b9 100644 --- a/django/db/models/__init__.py +++ b/django/db/models/__init__.py @@ -5,7 +5,8 @@ from django.db.models.aggregates import __all__ as aggregates_all from django.db.models.constraints import * # NOQA from django.db.models.constraints import __all__ as constraints_all from django.db.models.deletion import ( - CASCADE, DO_NOTHING, PROTECT, SET, SET_DEFAULT, SET_NULL, ProtectedError, + CASCADE, DO_NOTHING, PROTECT, RESTRICT, SET, SET_DEFAULT, SET_NULL, + ProtectedError, RestrictedError, ) from django.db.models.enums import * # NOQA from django.db.models.enums import __all__ as enums_all @@ -37,8 +38,8 @@ from django.db.models.fields.related import ( # isort:skip __all__ = aggregates_all + constraints_all + enums_all + fields_all + indexes_all __all__ += [ 'ObjectDoesNotExist', 'signals', - 'CASCADE', 'DO_NOTHING', 'PROTECT', 'SET', 'SET_DEFAULT', 'SET_NULL', - 'ProtectedError', + 'CASCADE', 'DO_NOTHING', 'PROTECT', 'RESTRICT', 'SET', 'SET_DEFAULT', + 'SET_NULL', 'ProtectedError', 'RestrictedError', 'Case', 'Exists', 'Expression', 'ExpressionList', 'ExpressionWrapper', 'F', 'Func', 'OuterRef', 'RowRange', 'Subquery', 'Value', 'ValueRange', 'When', 'Window', 'WindowFrame', diff --git a/django/db/models/deletion.py b/django/db/models/deletion.py index 3ad9ca0b35..603411cfc8 100644 --- a/django/db/models/deletion.py +++ b/django/db/models/deletion.py @@ -14,9 +14,17 @@ class ProtectedError(IntegrityError): super().__init__(msg, protected_objects) +class RestrictedError(IntegrityError): + def __init__(self, msg, restricted_objects): + self.restricted_objects = restricted_objects + super().__init__(msg, restricted_objects) + + def CASCADE(collector, field, sub_objs, using): - collector.collect(sub_objs, source=field.remote_field.model, - source_attr=field.name, nullable=field.null) + collector.collect( + sub_objs, source=field.remote_field.model, source_attr=field.name, + nullable=field.null, fail_on_restricted=False, + ) if field.null and not connections[using].features.can_defer_constraint_checks: collector.add_field_update(field, None, sub_objs) @@ -31,6 +39,11 @@ def PROTECT(collector, field, sub_objs, using): ) +def RESTRICT(collector, field, sub_objs, using): + collector.add_restricted_objects(field, sub_objs) + collector.add_dependency(field.remote_field.model, field.model) + + def SET(value): if callable(value): def set_on_delete(collector, field, sub_objs, using): @@ -70,6 +83,8 @@ class Collector: self.data = defaultdict(set) # {model: {(field, value): {instances}}} self.field_updates = defaultdict(partial(defaultdict, set)) + # {model: {field: {instances}}} + self.restricted_objects = defaultdict(partial(defaultdict, set)) # fast_deletes is a list of queryset-likes that can be deleted without # fetching the objects into memory. self.fast_deletes = [] @@ -121,6 +136,26 @@ class Collector: model = objs[0].__class__ self.field_updates[model][field, value].update(objs) + def add_restricted_objects(self, field, objs): + if objs: + model = objs[0].__class__ + self.restricted_objects[model][field].update(objs) + + def clear_restricted_objects_from_set(self, model, objs): + if model in self.restricted_objects: + self.restricted_objects[model] = { + field: items - objs + for field, items in self.restricted_objects[model].items() + } + + def clear_restricted_objects_from_queryset(self, model, qs): + if model in self.restricted_objects: + objs = set(qs.filter(pk__in=[ + obj.pk + for objs in self.restricted_objects[model].values() for obj in objs + ])) + self.clear_restricted_objects_from_set(model, objs) + def _has_signal_listeners(self, model): return ( signals.pre_delete.has_listeners(model) or @@ -177,7 +212,8 @@ class Collector: return [objs] def collect(self, objs, source=None, nullable=False, collect_related=True, - source_attr=None, reverse_dependency=False, keep_parents=False): + source_attr=None, reverse_dependency=False, keep_parents=False, + fail_on_restricted=True): """ Add 'objs' to the collection of objects to be deleted as well as all parent instances. 'objs' must be a homogeneous iterable collection of @@ -194,6 +230,12 @@ class Collector: direction of an FK rather than the reverse direction.) If 'keep_parents' is True, data of parent model's will be not deleted. + + If 'fail_on_restricted' is False, error won't be raised even if it's + prohibited to delete such objects due to RESTRICT, that defers + restricted object checking in recursive calls where the top-level call + may need to collect more objects to determine whether restricted ones + can be deleted. """ if self.can_fast_delete(objs): self.fast_deletes.append(objs) @@ -215,7 +257,8 @@ class Collector: self.collect(parent_objs, source=model, source_attr=ptr.remote_field.related_name, collect_related=False, - reverse_dependency=True) + reverse_dependency=True, + fail_on_restricted=False) if not collect_related: return @@ -259,7 +302,28 @@ class Collector: if hasattr(field, 'bulk_related_objects'): # It's something like generic foreign key. sub_objs = field.bulk_related_objects(new_objs, self.using) - self.collect(sub_objs, source=model, nullable=True) + self.collect(sub_objs, source=model, nullable=True, fail_on_restricted=False) + + if fail_on_restricted: + # Raise an error if collected restricted objects (RESTRICT) aren't + # candidates for deletion also collected via CASCADE. + for model, instances in self.data.items(): + self.clear_restricted_objects_from_set(model, instances) + for qs in self.fast_deletes: + self.clear_restricted_objects_from_queryset(qs.model, qs) + for model, fields in self.restricted_objects.items(): + for field, objs in fields.items(): + for obj in objs: + raise RestrictedError( + "Cannot delete some instances of model '%s' " + "because they are referenced through a restricted " + "foreign key: '%s.%s'." % ( + field.remote_field.model.__name__, + obj.__class__.__name__, + field.name, + ), + objs, + ) def related_objects(self, related_model, related_fields, objs): """ |
