views:

1857

answers:

5

I've models for Books, Chapters and Pages. They are all written by a User:

from django.db import models

class Book(models.Model)
    author = models.ForeignKey('auth.User')

class Chapter(models.Model)
    author = models.ForeignKey('auth.User')
    book = models.ForeignKey(Book)

class Page(models.Model)
    author = models.ForeignKey('auth.User')
    book = models.ForeignKey(Book)
    chapter = models.ForeignKey(Chapter)

What I'd like to do is duplicate an existing Book and update it's User to someone else. The wrinkle is I would also like to duplicate all related model instances to the Book - all it's Chapters and Pages as well!

Things get really tricky when look at a Page - not only will the new Pages need to have their author field updated but they will also need to point to the new Chapter objects!

Does Django support an out of the box way of doing this? What would a generic algorithm for duplicating a model look like?

Cheers,

John


Update:

The classes given above are just an example to illustrate the problem I'm having!

+4  A: 

I haven't tried it in django but python's deepcopy might just work for you

EDIT:

You can define custom copy behavior for your models if you implement functions:

__copy__() and __deepcopy__()
deepcopy works well for this sort of thing. +1 for being able to override the functionality with your own copy routines, good find.
Soviut
+1  A: 

I think you'd be happier with a simpler data model, also.

Is it really true that a Page is in some Chapter but a different book?

userMe = User( username="me" )
userYou= User( username="you" )
bookMyA = Book( userMe )
bookYourB = Book( userYou )

chapterA1 = Chapter( book= bookMyA, author=userYou ) # "me" owns the Book, "you" owns the chapter?

chapterB2 = Chapter( book= bookYourB, author=userMe ) # "you" owns the book, "me" owns the chapter?

page1 = Page( book= bookMyA, chapter= chapterB2, author=userMe ) # Book and Author aggree, chapter doesn't?

It seems like your model is too complex.

I think you'd be happier with something simpler. I'm just guessing at this, since I don't your know entire problem.

class Book(models.Model)
    name = models.CharField(...)

class Chapter(models.Model)
    name = models.CharField(...)
    book = models.ForeignKey(Book)

class Page(models.Model)
    author = models.ForeignKey('auth.User')
    chapter = models.ForeignKey(Chapter)

Each page has distinct authorship. Each chapter, then, has a collection of authors, as does the book. Now you can duplicate Book, Chapter and Pages, assigning the cloned Pages to the new Author.

Indeed, you might want to have a many-to-many relationship between Page and Chapter, allowing you to have multiple copies of just the Page, without cloning book and Chapter.

S.Lott
Hey Lott - the classes are just trivial example I made up to illustrate the problem I'm having. As for having the `author` in every page - all my tables are denormalized so I can get a full picture from any piece of the puzzle.
jb
@bisharty: That kind of FK denormalization you've shown is the cause of your problem. It isn't helpful to have all those extraneous foreign keys with potentially contradictory values. And it makes simple "cloning" far more complex than it needs to be.
S.Lott
+1  A: 

I posted my solution on Django Snippets. It's based heavily on the django.db.models.query.CollectedObject code used for deleting objects:

from django.db.models.query import CollectedObjects
from django.db.models.fields.related import ForeignKey

def duplicate(obj, value, field):
    """
    Duplicate all related objects of `obj` setting
    `field` to `value`. If one of the duplicate
    objects has an FK to another duplicate object
    update that as well. Return the duplicate copy
    of `obj`.  
    """
    collected_objs = CollectedObjects()
    obj._collect_sub_objects(collected_objs)
    related_models = collected_objs.keys()
    root_obj = None
    # Traverse the related models in reverse deletion order.    
    for model in reversed(related_models):
        # Find all FKs on `model` that point to a `related_model`.
        fks = []
        for f in model._meta.fields:
            if isinstance(f, ForeignKey) and f.rel.to in related_models:
                fks.append(f)
        # Replace each `sub_obj` with a duplicate.
        sub_obj = collected_objs[model]
        for pk_val, obj in sub_obj.iteritems():
            for fk in fks:
                fk_value = getattr(obj, "%s_id" % fk.name)
                # If this FK has been duplicated then point to the duplicate.
                if fk_value in collected_objs[fk.rel.to]:
                    dupe_obj = collected_objs[fk.rel.to][fk_value]
                    setattr(obj, fk.name, dupe_obj)
            # Duplicate the object and save it.
            obj.id = None
            setattr(obj, field, value)
            obj.save()
            if root_obj is None:
                root_obj = obj
    return root_obj
jb
A: 

If there's just a couple copies in the database you're building, I've found you can just use the back button in the admin interface, change the necessary fields and save the instance again. This has worked for me in cases where, for instance, I need to build a "gimlet" and a "vodka gimlet" cocktail where the only difference is replacing the name and an ingredient. Obviously, this requires a little foresight of the data and isn't as powerful as overriding django's copy/deepcopy - but it may do the trick for some.

pragmar
+2  A: 

Here's an easy way to copy your object.

Basically:

(1) set the id of your original object to None:

book_to_copy.id = None

(2) change the 'author' attribute and save the ojbect:

book_to_copy.author = new_author

book_to_copy.save()

(3) INSERT performed instead of UPDATE

(It doesn't address changing the author in the Page--I agree with the comments regarding re-structuring the models)