#ALL ADMIN IMPORTS from django.contrib.admin.options import * from django.utils.translation import ugettext_lazy as _, ugettext from django import forms from django.contrib.admin.util import NestedObjects from django.db import models from django.db.models.related import RelatedObject from django.utils.functional import curry class AdminReplacer(ModelAdmin): #START PATCH #START PATCH #START PATCH substitution_delete = True substitution_custom_handlers = tuple() def get_deletion_form(self, request, objs): class DeletionForm(forms.Form): substituted_object = forms.ModelChoiceField( empty_label = ugettext("Do not replace, delete the following objects"), label = ugettext("Object to replace"), queryset = self.queryset(request).exclude(pk__in = [obj.pk for obj in objs]), required = False, ) return DeletionForm def log_substitution(self, request, object, object_repr, substituted_object, substituted_object_repr): """ Log that an object has been successfully deleted. Note that since the object is deleted, it might no longer be safe to call *any* methods on the object, hence this method getting object_repr. The default implementation creates an admin LogEntry object. """ from django.contrib.admin.models import LogEntry, DELETION LogEntry.objects.log_action( user_id = request.user.id, content_type_id = ContentType.objects.get_for_model(self.model).pk, object_id = object.pk, object_repr = object_repr, action_flag = DELETION, change_message = u'Replaced with object with primary key %s "%s".' % (substituted_object.pk, substituted_object_repr) ) def substitution_m2m_handler(self, request, deleted_object, substituted_object, accessor_name, related = None): """ Default behavoiur for handling incoming M2M fields If a RelatedObject is defined, M2M relation in Inbound, otherwise it is outbound. """ manager = getattr(deleted_object, accessor_name) if related: """ Default behaviour: Incoming M2M relation will be preserved """ getattr(substituted_object, accessor_name).add(*manager.all()) return """ Default behaviour: Exiting M2M objects will be deleted """ return def substitution_handler(self, request, deleted_object, substituted_object, accessor_name, related): """ This is the default handler for a substitution, subclasses can define a new method. Related is an instance of django.db.models.related.RelatedObject Custom functions will receive (request, deleted_object, substituted_object, accessor_name) Default behaviour: Incoming ForeignKeys will be preserved, Subclass this method to add a custom behaviour, return None to delete incoming objects. """ manager = getattr(deleted_object, accessor_name) manager.all().update(**{related.field.name: substituted_object}) def get_substitution_handlers(self): """ The many-to-many version of get_fields_with_model(). """ try: return self._substitution_handlers_cache except AttributeError: self._fill_substitution_handlers_cache() return self._substitution_handlers_cache def _fill_substitution_handlers_cache(self): cache = SortedDict() accessor_names = [(related.get_accessor_name(), curry(self.substitution_handler, related = related)) for related in self.model._meta.get_all_related_objects()] accessor_names += [(related.get_accessor_name(), curry(self.substitution_m2m_handler, related = related)) for related in self.model._meta.get_all_related_many_to_many_objects()] accessor_names += [(field.name, self.substitution_m2m_handler) for field in self.model._meta.many_to_many] keys = [v for v, f in accessor_names] for (accessor_name, f) in accessor_names: for attr_name, handler_name in self.substitution_custom_handlers: if not attr_name in keys: raise KeyError, 'Accessor %s was not found. Choices are: %s' % (attr_name, ", ".join(keys)) if accessor_name == attr_name: f = handler_name if isinstance(handler_name, basestring): f = getattr(self, handler_name) break if f: cache[accessor_name] = f self._substitution_handlers_cache = cache def substitute_deleted_objects(self, request, deleted_objects, substituted_object): """ Move every related object to another instance You may need to define a list of model to ignore for two reason: 1. moving object with unique columns may cause integrity error 2. you could have business reasons """ handlers = self.get_substitution_handlers().items() for obj in deleted_objects: for accessor_name, handler in handlers: handler(request, obj, substituted_object, accessor_name) #END PATCH #END PATCH #END PATCH @csrf_protect_m def delete_view(self, request, object_id, extra_context=None): if not self.substitution_delete: return super(AdminReplacer, self).delete_view(request, object_id, extra_context=extra_context) "The 'delete' admin view for this model." opts = self.model._meta app_label = opts.app_label obj = self.get_object(request, unquote(object_id)) if not self.has_delete_permission(request, obj): raise PermissionDenied if obj is None: raise Http404(_('%(name)s object with primary key %(key)r does not exist.') % {'name': force_unicode(opts.verbose_name), 'key': escape(object_id)}) #START PATCH #START PATCH #START PATCH substituted_object = deletion_form = None if self.substitution_delete: DeletionForm = self.get_deletion_form(request, (obj,)) deletion_form = DeletionForm(request.POST or None, initial = request.GET) if deletion_form.is_valid(): substituted_object = deletion_form.cleaned_data["substituted_object"] #END PATCH #END PATCH #END PATCH # Populate deleted_objects, a data structure of all related objects that # will also be deleted. (deleted_objects, perms_needed) = get_deleted_objects((obj,), opts, request.user, self.admin_site) if request.POST and deletion_form and deletion_form.is_valid(): # The user has already confirmed the deletion. if perms_needed: raise PermissionDenied obj_display = force_unicode(obj) #START PATCH #START PATCH #START PATCH if self.substitution_delete and substituted_object: self.substitute_deleted_objects(request, [obj], substituted_object) self.log_substitution(request, obj, obj_display, substituted_object, force_unicode(substituted_object)) else: self.log_deletion(request, obj, obj_display) #END PATCH #END PATCH #END PATCH obj.delete() self.message_user(request, _('The %(name)s "%(obj)s" was deleted successfully.') % {'name': force_unicode(opts.verbose_name), 'obj': force_unicode(obj_display)}) if not self.has_change_permission(request, None): return HttpResponseRedirect("../../../../") return HttpResponseRedirect("../../") context = { "title": _("Are you sure?"), "object_name": force_unicode(opts.verbose_name), "object": obj, "deletion_form":deletion_form, "deleted_objects": deleted_objects, "perms_lacking": perms_needed, "opts": opts, "root_path": self.admin_site.root_path, "app_label": app_label, } context.update(extra_context or {}) context_instance = template.RequestContext(request, current_app=self.admin_site.name) return render_to_response(self.delete_confirmation_template or [ "admin/%s/%s/delete_confirmation.html" % (app_label, opts.object_name.lower()), "admin/%s/delete_confirmation.html" % app_label, "admin/delete_confirmation.html" ], context, context_instance=context_instance) --------------------- #HTML -> admin/delete_confirmation.html {% extends "admin/base_site.html" %} {% load i18n %} {% block breadcrumbs %} {% endblock %} {% block content %} {% if perms_lacking %}

{% blocktrans with object as escaped_object %}Deleting the {{ object_name }} '{{ escaped_object }}' would result in deleting related objects, but your account doesn't have permission to delete the following types of objects:{% endblocktrans %}

{% else %}
{% csrf_token %} {{ deletion_form.as_p }}

{% blocktrans with object as escaped_object %}Are you sure you want to delete the {{ object_name }} "{{ escaped_object }}"? All of the following related items will be deleted:{% endblocktrans %}

{% endif %} {% endblock %}