Duplicating an object with a form


Just trying to implement a duplicate function for an object - a Product, which has a formset of ProductSuppliers. E.g. the ProductSupplier object has a foreign key to the Product (and to a Supplier).

I already have an ProductUpdateView implementation that manages the ProductSupplier formset well. Subclassing that ProductUpdateView, I’m implementing a ProductDuplicateView. Which is essentially the same thing - I just need to save the Product object with a different pk. Great but…

… but the inline formset still have a reference to the initial (to be duplicated) object. I cannot seem to find a good way to manage this.

Let’s consider:

class ProductUpdateView:
    def post(self, request, *args, **kwargs):
        self.object = self.get_object()         
        form_class = self.get_form_class()
        form = self.get_form(form_class)
        formset = SupplierProdFormset(self.request.POST, instance=self.object)
        if (form.is_valid() and formset.is_valid()):
            return self.form_valid(form, formset)
            return self.form_invalid(form, formset)

I’ll obviously need to override this post method, something like that:

class ProductDuplicateView(ProductUpdateView):
    def post(self, request, *args, **kwargs):
        self.object = None                      # since the "old" object with some pk isn't the "new" one after duplication
        form_class = self.get_form_class()
        form = form_class(request.POST)   
        formset = SupplierProdFormset(self.request.POST)     # not passing in instance=... because the new object has not yet been created
        if (form.is_valid() and formset.is_valid()):
            return self.form_valid(form, formset)
            return self.form_invalid(form, formset)

The above formset is NOT valid, because:

  1. request.POST still has the id of the original object for its foreign key. I’m unsure what it would expected (e.g. when creating a new object with inline formsets… guessing “None”?). But it has a value, that doesn’t match the current object (which is None) and so doesn’t validate
  2. I don’t have a new object yet to assign as the parent instance. E.g. if I do say tmp_obj = form.save(commit=False), that gives me a (uncommited) new object instance. But it still doesn’t have a pk… guess I should do that in form_valid()?

What should I do? Should I iterate on the request.POST values for the foreign_key field of each of the forms in the inline formest to update them to… (nothing? so that Django sees it as a new parent form, as in the CreateView?)? Feels hacky. There has to be a more natural way to manage this.

Or am I wrong to decide to see this “DuplicateView” as a variation of an UpdateView? Would it be easier to see it as a variation on a CreateView?

That’s my reaction to this. You’re creating new instances of your model, you’re just getting your initial values from an existing model.

1 Like

I could be making assumptions here about exactly what you want to achieve, but from the description of your issue my instinct would be to implement the following:

  1. Add a “duplicate product” button to the Update view.
  2. Catch this button press in the view (as being something other than a regular “submit”), and just copy the record to a new ID.

That would do away with the need for a new view entirely, or - if you needed distinct views - allow you with a couple of {% if % } statements in the template to eg. change the title - to use the same view and template for both the update and the duplicate, making for a more DRY solution and less risk of issues in the future.

1 Like

I think Ken is right - it actually makes more sense and is more convenient to see a duplicate as a CreateView than as an UpdateView.

The devil is still in the details, and some time stepping thru django’s source code was needed to figure out the finer points. So I thought I’d provide some pointers…

Similar to your UpdateView if you have one, you’ll need to get the current object whereevber you instantiate your formsets (get_context_data for me, though I’ve seen code do it in the get/post method as well). You cannot simply use self.get_object() if you’re extending the CreateView as I ended up doing. My duplicate url is of the form …/products/int:pk/duplicate, thus I can fetch the id of the base object to duplicate from the kwargs:

    def get_context_data(self, **kwargs):
        """ somewhat like the UpdateView - but we have to retrieve the pk in the url, and the object from that
        ctx = super().get_context_data(**kwargs)
        self.object = self.model.objects.get(pk=self.kwargs["pk"])
        if self.request.POST:
           (... same as UpdateView...)
            fourprodfrmset.management_form.initial["INITIAL_FORMS"] = 0     # key bit here

            ctx["formset_fourprod"] = fourprodfrmset
            ctx["formset_case"] = casefrmset

        return ctx

Most of the method is the same as an UpdateView. However, the key bit is setting the INITIAL_FORMS of the management form to 0 (this is most likely what you want). The reason is that when django saves the forms in the formset, it will call a method save_existing(…) for the first “n” forms (e.g. from form 0 up to whatever INITIAL_FORMS value is). That method will NOT create a new object for the model from which your inline formset derives - it will update the existing one (that, presumably, was related to the base object you duplicated). Therefore there will be no inline object for the duplicated object if def save_existing_objects(self, commit=True): is called. For all the other forms (that were added on top of the initial ones), it will call a different method, def save_new_objects(self, commit=True):. See django.forms.models.py: BaseModelFormSet for details. None of that is explicit in the docs, so it takes some time to figure out (but then duplicating objects isn’t really standard django either).

The last thing you need to do (this is in the docs however) is set the current object’s pk to None before saving it. This forces django to save as a new db instance, instead of updating an existing one. You can then re-assign the saved instance as self.object, e.g.:

def form_valid(self, form, formset_fourprod, formset_case):
    self.object.pk = None           # will save a new obj. in db
    self.object = form.save()
    formset.instance = self.object
    return HttpResponseRedirect(self.get_success_url())

Just changing the INITIAL_FORMS number this way felt kinda hacky at first. However, it actually solves quite a few issue with a single line of code, apparently in a quite natural fashion for django:

  • If you’re using empty_forms (to dynamically add new empty forms to your formset), the value for the foreign_key field is really hard to deal with (it is set to the previous object’s pk, and that’s quite hard to deal with actually, and not elegant at all)
  • The initial values for fields displayed for the existing forms instances from the base object are retained… so you have to write extra code to prevent those from being assigned to the previous object upon saving.
1 Like

Interesting idea, though I’m not sure it would actually work (or actually be easier than a dedicated view), after implementing it as summarized below. The inlines are really a bit of a pain to deal with. They would still point to the base object’s instance, not the new one. Django is a little stubborn about those apparently.

I mean sure, you could catch the button press in the view. But you’d end up having to write a fair bit of code in that “catch” in order to ensure that the copied inlines from the base object AND any dynamically added forms (they do behaves differently wrt to foreign keys), and that code would end up diving a little bit deep in the internals of django… To the point that it does feel like django is telling you “just write another view & deal with it there)”. The other minor point is that from a user perspective, it may be the case (or not) that they want to duplicate an item strickly in the UpdateView. Perhaps they’d rather like to have a list of items, & be afforded the opportunity to select one in the list & then click Duplicate/Update/Edit… in that case a dedicated view yields that opportunity at no cost (well a bit of JS code perhaps).

Both views are using the same template, that being said, which I agree is DRYier.