Created
February 3, 2016 23:06
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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