The last couple of days I’ve been working on customizing the django 1.1 administration to handle a number of specific permission and functionality wise things. Tonight I wrote a little function that should make your life much easier in the administration. I find, when I do administrative stuff, that it’s easier to copy something existing and work on that as a base. This is also true for my customers so why not make it possible to do so in the built in administration.
The idea is to write an admin action that takes a number of objects and clone them. The action function can be defined as:
#!/usr/bin/python/
from django.db.models.fields import CharField
def clone_objects(objects):
def clone(from_object):
args = dict([(fld.name, getattr(from_object, fld.name))
for fld in from_object._meta.fields
if fld is not from_object._meta.pk]);
return from_object.__class__.objects.create(**args)
if not hasattr(objects,'__iter__'):
objects = [ objects ]
# We always have the objects in a list now
objs = []
for object in objects:
obj = clone(object)
obj.save()
objs.append(obj)
def action_clone(modeladmin, request, queryset):
objs = clone_objects(queryset)
action_clone.short_description = "Copy the selected objects"
It’s not that much code. And I made the basic clone_objects function so it can also take a single object if you want to use it somewhere else. Now you can just put the action in the ModelAdmins you’ll like or enable it site-wide.
The next thing is to make it possible to recognize the objects you have just copied. The method presented below is not very elegant but it works for most cases. I do it by looking for CharFields that have a specific name, like “title” or “name”:
#!/usr/bin/python/
from django.db.models.fields import CharField
def clone_objects(objects, title_fieldnames):
def clone(from_object, title_fieldnames):
args = dict([(fld.name, getattr(from_object, fld.name))
for fld in from_object._meta.fields
if fld is not from_object._meta.pk]);
for field in from_object._meta.fields:
if field.name in title_fieldnames:
if isinstance(field, CharField):
args[field.name] = getattr(from_object, field.name) + " (copy) "
return from_object.__class__.objects.create(**args)
if not hasattr(objects,'__iter__'):
objects = [ objects ]
# We always have the objects in a list now
objs = []
for object in objects:
obj = clone(object, title_fieldnames)
obj.save()
objs.append(obj)
def action_clone(modeladmin, request, queryset):
objs = clone_objects(queryset, ("name", "title"))
action_clone.short_description = "Copy the selected objects"
So now, if you clone a object with “Look ponies” as the title, the cloned objects will have “Look ponies (copy)” as it’s title. Here are some screen shots to show it in action:



Another thing you could do is to create a column in the admin list pages with a copy link that would copy-and-edit the object. This should fit a normal use case where you want to edit the object and actually differentiate the new object from the old one. It should be pretty basic to write, but it includes a view function so I won’t include it here. If you need such a link on a lot of models consider using an BaseAdmin class that all your admin classes inherit from. This way to can write the view function and link function there and have it automatic on all your admins.
Now this code has a number of drawbacks, and maybe more than I have just listed here:
- The actual clone functionality will fail if the object you’re cloning don’t have an AutoField as it’s primary key
- The “(copy)” append functionality is very limited and you’ll properly need change the tuple manually for every model you use unless you’re very consistent with your field naming
- The clone functionality will properly fail hard if some of the fields are unique. The method could be made more robust by adding stuff to unique fields
Often when we’re doing demos we duplicate production objects to show clients how things work. We wanted to do that without wrecking real objects so I played around with some code for duplicating objects and all their related objects:
http://github.com/johnboxall/django_usertools/blob/28c1f243a4882da1e63b60d54a86947db4847cf6/helpers.py#L23
It reuses a lot of the Django core code for identifying objects to delete :)
Looks very cool. I’ll rewrite the clone method to resemble yours a bit more :) I’m glad I mentioned the limited functionality of it before you commented :)
Nice concept! It doesn’t handle cloning File and ImageFields though.
Nope, it’s not that bright. I’ll look into rewritting it so File and ImageFields are handled nicely.
Thank you very much! I needed to do something similar so I re-wrote the return statement to clone the object but waiting to make it persistent. It looks like this:
return from_object.__class__(**args)