A generic copy/clone action for django 1.1

May 23rd, 2009 2 comments

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:

Object list
Action selection
Copied object

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
Tags: ,

Danger with django ModelForms and related names

May 21st, 2009 1 comment

There is a danger in extending your ModelForms with fields if you don’t take care of your naming. Take for example these models. The forms represents a simple batch emailing module.

#!/usr/bin/python/

class EmailBatch(models.Model):
    body = models.TextField()
    subject = models.CharField(max_length=400)

class Email(models.Model):
    batch = models.ForeignKey(EmailBatch, null=False, related_name="emails")
    recipient = models.EmailField()

I wrote a simple form field called MultipleEmailField so the users can input several email addresses in a simple textarea. The resulting form is something like this:

#!/usr/bin/python/

class SendingForm(forms.ModelForm):
    emails = MultiEmailField(required=True)

    class EmailBatch:
        model = Delivery
        fields = ('body', 'subject', )

This appears to be alright, but isn’t. If you use this form you’ll get an AttributeError. The problem is in handling the “emails” field on the form. It’s not bound to any model, but the model actually contains something called emails (it’s set in the related_name part of the Email model). This can take some time to find :(

The bug is already fixed in django trunk, but for those of us trying to use a stable version we’ll just have to wait a little longer.

A simple fix is just to rename your field or related name

Tags:

Off to a bad start!

May 21st, 2009 No comments

A picture says more than a thousand words:

picture-3

That’s way to many support requests for one morning. This will take me forever

Simple idea – fantastic product

May 16th, 2009 No comments

This is truly a fantastic product. I hope the inventors makes a lot of money on this:

Tags:

TVs impact on people.

May 13th, 2009 4 comments

I just finished the 20th episode of House M.D. season 5. It’s pretty sad as one of the main characters commits suicide. So, put in crying people, sad music to a truly sad history viewers are bound to leave the episode less happy than normally. Despite of this I was amazed by the end screen:

If you are in crisis and thinking about suicide, call 1-800-273-TALK (8255). For general information about mental health issues, visit the National Alliance on Mental Illness.

I’m amazed, not only that TV can actually have that big an impact but also that the producers are actually putting the screen on. It’s a bit dramatic. Wouldn’t it just be easier to write the truth about the episode. The actor wanted out of the serie so they killed him. It’s normal.

Tags: