Skip to content

Instantly share code, notes, and snippets.

@jmurty
Created February 3, 2016 23:06
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jmurty/2034c24b6f91a3eaf51a to your computer and use it in GitHub Desktop.
Save jmurty/2034c24b6f91a3eaf51a to your computer and use it in GitHub Desktop.
Django Proxy Delete Hack: monkey patch Django 1.7 / 1.8 deletion collectors to include proxy ancestors
# USAGE: To apply to your project, call the appropriate APPLY_ method for your
# version of Django at the start of your project's `AppConfig.ready` method:
# - APPLY_hack_django_17_collector_collect
# - APPLY_hack_django_18_get_candidate_relations_to_delete
#
# See comment notes below for references to related Django issues.
# The underlying issue is fixed in Django 1.9+
#
# This gist is intended to help others encountering this issue, and as a
# placeholder until a better work-around and/or a more easily installed
# version of this work-around is created. Please see discussion at
# https://github.com/chrisglass/django_polymorphic/issues/34
#
#
# WARNING: This is clearly an awful hack, use at your own peril!
# - tested and confirmed working with latest Django 1.7 (at time of writing)
# - only *very* lightly tested with Django 1.8, take special care with
# this version and test test test...
# Hack to override Django 1.8's default `get_candidate_relations_to_delete`
# function to make it include proxy ancestor models, not just concrete parent
# models, when building a set of models to check for relationships for
# deletion.
# This hack replaces the function with a direct copy, except with our custom
# behaviour in the middle.
def hack_django_18_get_candidate_relations_to_delete(opts):
from itertools import chain
# Collect models that contain candidate relations to delete. This may include
# relations coming from proxy models.
candidate_models = {opts}
candidate_models = candidate_models.union(opts.concrete_model._meta.proxied_children)
# HACK: Find all relevant ancestor proxy models, working down from the top-
# most parent concrete models, and include as candidates with interesting
# relationships.
for parent in opts.parents:
for pc_opts in parent._meta.proxied_children:
if issubclass(opts.model, pc_opts.model):
candidate_models.add(pc_opts)
# END OF HACK
# For each model, get all candidate fields.
candidate_model_fields = chain.from_iterable(
opts.get_fields(include_hidden=True) for opts in candidate_models
)
# The candidate relations are the ones that come from N-1 and 1-1 relations.
# N-N (i.e., many-to-many) relations aren't candidates for deletion.
return (
f for f in candidate_model_fields
if f.auto_created and not f.concrete and (f.one_to_one or f.one_to_many)
)
def APPLY_hack_django_18_get_candidate_relations_to_delete():
from django.db.models import deletion
# Check for method available in Django 1.8+ but not before
if hasattr(deletion, 'get_candidate_relations_to_delete'):
deletion.get_candidate_relations_to_delete = \
hack_django_18_get_candidate_relations_to_delete
# Hack to extend the Django 1.7 Collection.collect method that collects objects
# and their relationships for deletion, to fix the process so that reverse FK
# relationships on proxy ancestor objects are included in the deletion: #339.
# This works around a Django core bug, see issue #18012 [1] and related issues
# #23076 [2] and a potential fix to Django master [3] which might get back-
# ported if we're lucky (though probably not).
#
# [1]: https://code.djangoproject.com/ticket/18012
# [2]: https://code.djangoproject.com/ticket/23076
# [3]: https://github.com/django/django/pull/5378
from django.db.models import DO_NOTHING
def hack_django_17_collector_collect(self, objs, *args, **kwargs):
# Call original collect to do the standard Django work
self._original_collect(objs, *args, **kwargs)
# Collect objects via M2M relationships to proxy models
if not objs or not kwargs.get('collect_related', True):
return
for model in [o._meta.model for o in objs]:
for proxy_ancestor_cls in get_proxy_ancestor_classes(model):
opts = proxy_ancestor_cls._meta
for rel_obj in opts.get_all_related_objects(
local_only=True, include_hidden=True):
rel = rel_obj.field.rel
if not rel:
continue
if not rel.multiple:
continue
if rel.to != proxy_ancestor_cls:
continue
if rel.on_delete == DO_NOTHING:
continue
fk_f = rel_obj.field
sub_objs = fk_f.model._base_manager.using(self.using) \
.filter(**{"%s__in" % fk_f.name: objs})
if self.can_fast_delete(sub_objs, from_field=fk_f):
self.fast_deletes.append(sub_objs)
elif sub_objs:
fk_f.rel.on_delete(self, fk_f, sub_objs, self.using)
def get_proxy_ancestor_classes(klass):
"""
Return a set containing all the proxy model classes that are ancestors
of the given class.
NOTE: This implementation is for Django 1.7, it might need to work
differently for other versions especially 1.8+.
"""
proxy_ancestor_classes = set()
for superclass in klass.__bases__:
if hasattr(superclass, '_meta') and superclass._meta.proxy:
proxy_ancestor_classes.add(superclass)
proxy_ancestor_classes.update(
get_proxy_ancestor_classes(superclass))
return proxy_ancestor_classes
def APPLY_hack_django_17_collector_collect():
from django.db.models import deletion
# Check for method available in Django 1.8+ but not before
if not hasattr(deletion, 'get_candidate_relations_to_delete'):
deletion.Collector._original_collect = deletion.Collector.collect
deletion.Collector.collect = hack_django_17_collector_collect
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment