Reputation: 422
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
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
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
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
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
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