Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Duplicating model instances and their related objects in Django / Algorithm for recusrively duplicating an object

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!

like image 243
jb. Avatar asked Jan 12 '09 21:01

jb.


1 Answers

This no longer works in Django 1.3 as CollectedObjects was removed. See changeset 14507

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 

For django >= 2 there should be some minimal changes. so the output will be like this:

def duplicate(obj, value=None, field=None, duplicate_order=None):     """     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.     duplicate_order is a list of models which specify how     the duplicate objects are saved. For complex objects     this can matter. Check to save if objects are being     saved correctly and if not just pass in related objects     in the order that they should be saved.     """     from django.db.models.deletion import Collector     from django.db.models.fields.related import ForeignKey      collector = Collector(using='default')     collector.collect([obj])     collector.sort()     related_models = collector.data.keys()     data_snapshot = {}     for key in collector.data.keys():         data_snapshot.update(             {key: dict(zip([item.pk for item in collector.data[key]], [item for item in collector.data[key]]))})     root_obj = None      # Sometimes it's good enough just to save in reverse deletion order.     if duplicate_order is None:         duplicate_order = reversed(related_models)      for model in duplicate_order:         # Find all FKs on model that point to a related_model.         fks = []         for f in model._meta.fields:             if isinstance(f, ForeignKey) and f.remote_field.related_model in related_models:                 fks.append(f)         # Replace each `sub_obj` with a duplicate.         if model not in collector.data:             continue         sub_objects = collector.data[model]         for obj in sub_objects:             for fk in fks:                 fk_value = getattr(obj, "%s_id" % fk.name)                 # If this FK has been duplicated then point to the duplicate.                 fk_rel_to = data_snapshot[fk.remote_field.related_model]                 if fk_value in fk_rel_to:                     dupe_obj = fk_rel_to[fk_value]                     setattr(obj, fk.name, dupe_obj)             # Duplicate the object and save it.             obj.id = None             if field is not None:                 setattr(obj, field, value)             obj.save()             if root_obj is None:                 root_obj = obj     return root_obj 
like image 115
jb. Avatar answered Oct 11 '22 23:10

jb.