from functools import update_wrapper from django.contrib.admin import ModelAdmin, helpers from django.contrib.admin.exceptions import DisallowedModelAdminToField from django.contrib.admin.options import TO_FIELD_VAR from django.contrib.admin.utils import flatten_fieldsets, unquote from django.shortcuts import redirect from django.urls import reverse, path from django.core.exceptions import PermissionDenied, FieldDoesNotExist class CloneModelAdmin(ModelAdmin): clone_verbose_name = "Clone" change_form_template = 'admin/admin_change_form.html' cloneable_fields = [] def get_urls(self): # Not certain what this wrap() function is exactly. Just copied it from the django admin get_urls function def wrap(view): def wrapper(*args, **kwargs): return self.admin_site.admin_view(view)(*args, **kwargs) wrapper.model_admin = self return update_wrapper(wrapper, view) info = self.opts.app_label, self.opts.model_name new_urlpatterns = [ path( "/clone/", wrap(self.clone_view), name="%s_%s_clone" % info, ), ] original_urlpatterns = super(CloneModelAdmin, self).get_urls() # Important to add custom urls before the existing ones. # Last entry is /'> which will catch everything not already picked up return new_urlpatterns + original_urlpatterns def change_view(self, request, object_id, form_url='', extra_context=None): url = reverse("admin:{0}_{1}_clone".format(self.opts.app_label, self.opts.model_name), args=[object_id]) extra_context = extra_context or {} extra_context.update({ 'clone_verbose_name': self.clone_verbose_name, 'clone_link': url, }) return super(CloneModelAdmin, self).change_view(request, object_id, form_url, extra_context) def clone_view(self, request, object_id, form_url='', extra_context=None): to_field = request.POST.get(TO_FIELD_VAR, request.GET.get(TO_FIELD_VAR)) if to_field: print("To field in clone_view. What is this? ",to_field) if to_field and not self.to_field_allowed(request, to_field): raise DisallowedModelAdminToField( "The field %s cannot be referenced." % to_field ) if not self.has_add_permission(request): raise PermissionDenied original_obj = self.get_object(request, unquote(object_id), to_field) if original_obj is None: return self._get_obj_does_not_exist_redirect(request, self.opts, object_id) if not self.has_view_or_change_permission(request, original_obj): raise PermissionDenied fieldsets = self.get_fieldsets(request, original_obj) model_form = self.get_form(request, original_obj, change=False, fields=flatten_fieldsets(fieldsets)) if request.method == "POST": form = model_form(request.POST) formsets, inline_instances = self._create_formsets( request, original_obj, change=False, ) form_validated = form.is_valid() if form_validated: new_object = self.save_form(request, form, change=False) self.save_model(request, new_object, form, False) self.clone_related(request, original_obj, new_object) return redirect(reverse("admin:{0}_{1}_change".format(self.opts.app_label, self.opts.model_name), args=[new_object.pk])) else: new_obj = self.model() for field in self.cloneable_fields: setattr(new_obj, field, getattr(original_obj, field)) form = model_form(instance=new_obj) formsets, inline_instances = self._create_formsets(request, original_obj, change=False) admin_form = helpers.AdminForm( form, list(self.get_fieldsets(request)), self.get_prepopulated_fields(request), self.readonly_fields, model_admin=self ) media = self.media inline_formsets = self.get_inline_formsets(request, formsets, inline_instances) for inline_formset in inline_formsets: media += inline_formset.media title = u'{0} {1}'.format(self.clone_verbose_name, original_obj) context = { **self.admin_site.each_context(request), "title": title, "original": title, "adminform": admin_form, "is_popup": "_popup" in request.POST or "_popup" in request.GET, "show_delete": False, "media": media, "inline_admin_formsets": inline_formsets, "errors": helpers.AdminErrorList(form, formsets), **(extra_context or {}), } context.update(extra_context or {}) return self.render_change_form( request, context, form_url=form_url, change=False, add=True ) def clone_related(self, request, original_obj, new_obj): for inline in self.inlines: if not inline.clone_parent: continue try: inline.model._meta.get_field(inline.clone_parent) except FieldDoesNotExist: continue inline_objects = inline.model.objects.filter(**{inline.clone_parent: original_obj}) for inline_object in inline_objects: inline_object.pk = None setattr(inline_object, inline.clone_parent, new_obj) inline_object.save() class InlineAdminFormSetFakeOriginal(helpers.InlineAdminFormSet): def __iter__(self): # the template requires the AdminInlineForm to have an `original` # attribute, which is the model instance, in order to display the # 'Delete' checkbox # we don't have `original` because we are just providing initial # data to the form, so we attach a "fake original" (something that # evaluates to True) to fool the template and make is display # the 'Delete' checkbox # needless to say this is a terrible hack and will break in future # django versions :) for inline_form in super(InlineAdminFormSetFakeOriginal, self).__iter__(): if inline_form.form.initial: inline_form.original = True yield inline_form