Login

Duplicate related objects of model instance

Author:
johnboxall
Posted:
January 13, 2009
Language:
Python
Version:
1.0
Tags:
copy duplicate denormalize
Score:
2 (after 2 ratings)

I've got a bunch of Models that form a tree like structure. I'd like to duplicate them all changing one field to something else.

Say for example I've got a Website which has Pages and Links and all kinds of other Models. Each one of these belong to a User (through a foreign key relation). I could use duplicate to create a copy of an entire website and give it to another User with something like this:

class Website(Model):
  owner = ForeignKey('auth.User')
  ...

class Link(Model):
  owner = ForeignKey('auth.User')
  ...

class Page(Model):
  owner = ForeignKey('auth.User')
  ...

##################################

website = Website.objects.get(pk=1)
new_owner = User.objects.get(pk=1)
duplicate(website, new_owner, 'owner')

For a in depth example of the problem see: Duplicating Model Instances @ STO

Note

  • Not tested with anything but simple Foreign Key relations - the model ordering is very naive.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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

More like this

  1. Model merging function by xaralis 4 years, 8 months ago
  2. Replace model select widget in admin with a readonly link to the related object by ekellner 6 years, 11 months ago
  3. URL models by diverman 5 years, 9 months ago
  4. Dump a model instance and related objects as a Python data structure by akaihola 3 years, 6 months ago
  5. Display arbitrary models by bjornkri 7 years ago

Comments

michelts (on August 13, 2009):

Great snippet! I would make just one fix to be sure that images and file fields copy the files too. See:

        ...
        dupe_obj = collected_objs[fk.rel.to][fk_value]
        setattr(obj, fk.name, dupe_obj)
# Duplicate files related to image fields and file fields
for field in obj._meta.fields:
    if isinstance(field, FileField):
        field_instance = field.value_from_object(obj)
        if field_instance:
            field_instance.save(field_instance.name, field_instance.file, save=False)
# Duplicate the object and save it.
obj.id = None
...

#

jkafader (on May 19, 2011):

Hi I also posted this on stackoverflow in the related Q/A.

I reworked this a bit to be compatible with the Collector which replaced CollectedObjects in 1.3.

I didn't really test this too heavily, but did test it with an object with about 20,000 sub-objects, but in only about three layers of foreign-key depth. Use at your own risk of course.

For the ambitious guy who reads this post, you should consider subclassing Collector (or copying the entire class to remove this dependency on this unpublished section of the django API) to a class called something like "DuplicateCollector" and writing a .duplicate method that works similarly to the .delete method. that would solve this problem in a real way.

from django.db.models.deletion import Collector
from django.db.models.fields.related import ForeignKey

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.
    """
    collector = Collector({})
    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.rel.to 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.rel.to]
                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

#

Please login first before commenting.