Zappatta
Zappatta

Reputation: 422

Django "Save as new" and keep Image fields

I have a Django model with multiple ImageFields.

On the ModelAdmin class I've set save_as = True, which means The admin page has a "Save as new" button, which allows for duplicating an existing item and saving it as new.

However when this button is used, the ImageFields are not duplicated and are left blank on the new item.

Looking at the POST request, I see that these fields are blank in the post data.

I've thought about overriding the Model class' save method, and copying the images from the old object by myself. But as far as I could figure out, I have no way to tell that the object is saved "as new". I also don't seem to have the ID of the old Item so I cannot get the old Images from it.

Is there a way to get these image fields to be duplicated as well?

Edit: Added Example code by request.

Created a minimalistic app with only one model. Verified problem still occurs.

Sample models.py:

from django.db import models

class Person(models.Model):
    face_image = models.ImageField(upload_to='images', 
                                   null=False, 
                                   blank=True)

Sample admin.py:

from django.contrib import admin
from testapp.models import Person

class PersonAdmin(admin.ModelAdmin):
    save_as = True

admin.site.register(Person, PersonAdmin)

Upvotes: 9

Views: 9066

Answers (5)

Esteban Feldman
Esteban Feldman

Reputation: 3397

This helped me a lot and I used the solution from @ShravaN and expanded it to also save images in the related inline models. I assume the code is not the best but it works. If you have any idea to improve it please do!

def save_model(self, request, obj, form, change):
    # Django always sends this when "Save as new is clicked"
    if '_saveasnew' in request.POST:
        # Get the ID from the admin URL
        original_pk = request.resolver_match.kwargs['object_id']
        # Get the original object
        original_obj = obj._meta.concrete_model.objects.get(id=original_pk)
        # Iterate through all it's properties
        self._copy_image_fields(obj, original_obj)

    obj.save()

def _copy_image_fields(self, obj, original_obj):
    for prop, value in vars(original_obj).items():
        # if the property is an Image
        # (don't forget to import ImageFieldFile!)
        if isinstance(getattr(original_obj, prop), ImageFieldFile):
            setattr(obj, prop, getattr(original_obj, prop))  # Copy it!

def save_related(self, request, form, formsets, change):
    if '_saveasnew' in request.POST:
        # Get the ID from the admin URL
        original_pk = request.resolver_match.kwargs['object_id']

        # Get the original object
        original_obj = form.instance._meta.concrete_model.objects.get(
            id=original_pk
        )
        form.save_m2m()
        for formset in formsets:
            instances = formset.save(commit=False)

            if instances:
                related = list(filter(lambda r: r.related_model == formset.model, original_obj._meta.related_objects))
                related = related[0] if related else None
                # related: ManyToOneRel
                if related:
                    field_name = f"{related.name}_set" if not related.related_name else related.related_name
                    related_set = getattr(original_obj, field_name)
                else:
                    # TODO: warning?
                    continue
                for ori, ni in zip(related_set.all(), instances):
                    # instance: Model
                    # we need to figure out which field is in the original
                    # object
                    self._copy_image_fields(ni, ori)
                    ni.save()
            formset.save_m2m()
    else:
        super(LiveEventAdmin, self).save_related(request, form, formsets, change)
    

Upvotes: 0

ShravaN
ShravaN

Reputation: 126

If your here in 2019 .. Updated answer for @nicolaslara This answer is Django 2+ and python 3

To get Url from Django admin we should use :

original_pk = request.resolver_match.kwargs['object_id']

and iteritems() won't work on python3 we have to use just items()

Final Code:

  def save_model(self, request, obj, form, change):
    # Django always sends this when "Save as new is clicked"
    if '_saveasnew' in request.POST:
        # Get the ID from the admin URL
        original_pk = request.resolver_match.kwargs['object_id']
        print(original_pk)

        # Get the original object
        original_obj = obj._meta.concrete_model.objects.get(id=original_pk)

        # Iterate through all it's properties
        for prop, value in vars(original_obj).items():
            # if the property is an Image (don't forget to import ImageFieldFile!)
            if isinstance(getattr(original_obj, prop), ImageFieldFile):
                setattr(obj, prop, getattr(original_obj, prop))  # Copy it!
    obj.save()

Upvotes: 4

Zappatta
Zappatta

Reputation: 422

I've managed to find some workaround:

I've overridden the original admin form (see here) to get it also include the old Model's ID in "save as new" POST request. I've did it by creating a special admin for of that model, and adding inside it a hidden input:

<input type="hidden" name="my_objectid" value="{{ object_id }}">

afterwards I've made the ModelAdmin class load that specific html. Then I overriden the AdminModel class' save_model method so it would copy the images as well.

So the new admin.py should look like this:

from django.contrib import admin
from testapp.models import Person

from django.db.models.fields.files import ImageFieldFile #added to be used later

class PersonAdmin(admin.ModelAdmin):
    save_as=True
    change_form_template = 'admin/person_change_form.html';
    def save_model(self, request, obj, form, change):       

        if '_saveasnew' in request.POST: #Django always sends this when "Save as new is clicked"
            origObjId = request.POST['my_objectid']; #Get the ID that is new posted after overriding the form. 
            originalPerson = Person.objects.get(id=origObjId); #Use the Id to get the old object
            for prop, value in vars(originalPerson).iteritems(): #iterate through all it's properties
                if isinstance(getattr(originalPerson,prop), ImageFieldFile): #if the property is an Image (don't forget to import ImageFieldFile!)
                    setattr(obj,prop,getattr(originalPerson,prop)) #Copy it!

        obj.save()

admin.site.register(Person, PersonAdmin)

Upvotes: 7

nicolaslara
nicolaslara

Reputation: 151

Building up on this response, here's a more generic way of achieving the same result:

from django.core.urlresolvers import resolve
from django.db.models.fields.files import FieldFile

class PersonAdmin(admin.ModelAdmin):
    save_as = True

    def save_model(self, request, obj, form, change):
        # Django always sends this when "Save as new is clicked"
        if '_saveasnew' in request.POST:
            # Get the ID from the admin URL
            original_pk = resolve(request.path).args[0]
            # Get the original object
            original_obj = obj._meta.concrete_model.objects.get(id=original_pk)

            # Iterate through all it's properties
            for prop, value in vars(original_obj).iteritems():
                # if the property is an Image (don't forget to import ImageFieldFile!)
                if isinstance(getattr(original_obj, prop), FieldFile):
                    setattr(obj,prop,getattr(original_obj, prop)) # Copy it!
        obj.save()

This should work with any model and any file type. It also doesn't require editing the form or the template. This is a workaround that should not be needed once the pull request gets merged: https://github.com/django/django/pull/2246.

Upvotes: 9

Dmitry
Dmitry

Reputation: 908

Here is a ticket that describes this very same problem: «Admin inlines with file/image field fails to save_as»

There is a pull request from 9 february 2014 that fixes this bug. Hope soon it will be merged.

Upvotes: 3

Related Questions