Ticket #4027: django-model-copying.diff

File django-model-copying.diff, 10.2 KB (added by forest, 6 years ago)

New patch implementing Model.copy with recursion. Includes tests.

  • django/db/models/base.py

    === modified file 'django/db/models/base.py'
     
    1111import django.db.models.manager     # Imported to register signal handler.
    1212from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned, FieldError
    1313from django.db.models.fields import AutoField, FieldDoesNotExist
    14 from django.db.models.fields.related import OneToOneRel, ManyToOneRel, OneToOneField
     14from django.db.models.fields.related import OneToOneRel, ManyToOneRel, OneToOneField, ForeignKey
    1515from django.db.models.query import delete_objects, Q
    1616from django.db.models.query_utils import CollectedObjects, DeferredAttribute
    1717from django.db.models.options import Options
     
    606606    def prepare_database_save(self, unused):
    607607        return self.pk
    608608
     609    def _copy(self, recurse, recurse_m2m, copied_objects):
     610        if recurse_m2m is None:
     611            recurse_m2m = recurse
     612
     613        # Handle non-m2m fields.
     614        self_referencing_fields = []
     615        foreign_keys_to_recurse = {}
     616        values = {}
     617        for field in self._meta.fields:
     618            if field.name == self._meta.pk.name:
     619                continue
     620
     621            value = getattr(self, field.name)
     622
     623            if isinstance(field, ForeignKey) and (value is not None):
     624                if value == self:
     625                    # This is a reference to the self instance.  Make a note of
     626                    # it so that we can correctly refer to the copy of self
     627                    # that we're making.  We temporarily store a reference to
     628                    # the old self instance to avoid issues where the foreign
     629                    # key is not allowed to be NULL.
     630                    values[field.name ] = self
     631                    self_referencing_fields.append(field)
     632                    continue
     633
     634                if value in copied_objects:
     635                    # This object has already been copied.  Store a reference
     636                    # to the copy.
     637                    values[field.name] = copied_objects[value]
     638                    continue
     639
     640                if recurse:
     641                    # We are recursing and this object has not been previously
     642                    # copied.  We will make a new copy once the copy of self
     643                    # has been created (we need to add that to copied_objects
     644                    # before we start recursing).  For now, we store a
     645                    # reference to the old value (to avoid issues with NOT
     646                    # NULL) and make a note that we need to finish handling
     647                    # this field.
     648                    values[field.name] = value
     649                    foreign_keys_to_recurse[field] = value
     650                    continue
     651
     652            # This is not a foreign key (or it is but its value is None).
     653            values[field.name] = value
     654
     655        copied_self = self.__class__.objects.create(**values)
     656        copied_objects[self] = copied_self
     657
     658        # Now that we have an id for the copy we are making, fill in the values
     659        # for self-referencing foreign keys.
     660        if self_referencing_fields:
     661            for field in self_referencing_fields:
     662                setattr(copied_self, field.name, copied_self)
     663
     664        # self is now in copied_objects, so we can freely recurse on foreign
     665        # keys.
     666        if foreign_keys_to_recurse:
     667            for field, value in foreign_keys_to_recurse.items():
     668                copied_value = value._copy(
     669                  recurse, recurse_m2m, copied_objects)
     670                setattr(copied_self, field.name, copied_value)
     671
     672        if self_referencing_fields or foreign_keys_to_recurse:
     673            copied_self.save()
     674
     675        # Handle many-to-many relationships.
     676        for m2m_field in self._meta.many_to_many:
     677            value = getattr(self, m2m_field.attname).all()
     678
     679            new_value = []
     680            for item in value:
     681                if item == self:
     682                    # This is a link back to the self instance.  Store a
     683                    # reference to the copy.
     684                    new_value.append(copied_self)
     685                    continue
     686
     687                if (m2m_field.rel.to is self.__class__) and (
     688                  item in copied_objects):
     689                    # This m2m relationship is to self and we've already begun
     690                    # copying the item refered to.  No need to copy the
     691                    # reference to that item, as the relationship only needs to
     692                    # be specified from one side.
     693                    continue
     694
     695                if item in copied_objects:
     696                    # This item has previously been copied.  Store a reference
     697                    # to the copy.
     698                    new_value.append(copied_objects[item])
     699                    continue
     700
     701                if recurse_m2m:
     702                    # This item has not previously been copied and we're
     703                    # recursing.  We need a new copy of this item.
     704                    new_value.append(
     705                      item._copy(recurse, recurse_m2m, copied_objects))
     706                else:
     707                    # This item has not previously been copied and we're not
     708                    # recursing.  Copy the m2m reference to the same item.
     709                    new_value.append(item)
     710
     711            setattr(copied_self, m2m_field.attname, new_value)
     712
     713        return copied_self
     714
     715    def copy(self, recurse = False, recurse_m2m = None):
     716        '''
     717        Creates a copy of this object in the database.
     718
     719        If recurse is True, objects referenced by foreign keys and many-to-many
     720        fields will be recursively copied.  For each object referenced, exactly
     721        one copy will be made.  References will be updated such that references
     722        to objects that were copied bill be updated to point to the copies.
     723
     724        If recurse is True but recurse_m2m is False, only objects referenced by
     725        foreign keys will be recursed into (objects referenced by many-to-many
     726        fields will be left uncopied).  If recurse_m2m is True but recurse is
     727        False, only many-to-many fields will be recursed into.
     728
     729        recurse defaults to False.  recurse_m2m defaults to the value of
     730        recurse.  Typically, recurse_m2m will be left unspecified.
     731        '''
     732        return self._copy(recurse, recurse_m2m, {})
     733
    609734
    610735############################################
    611736# HELPER FUNCTIONS (CURRIED MODEL METHODS) #
  • tests/modeltests/model_copying/models.py

    === added directory 'tests/modeltests/model_copying'
    === added file 'tests/modeltests/model_copying/__init__.py'
    === added file 'tests/modeltests/model_copying/models.py'
     
     1try:
     2    set
     3except NameError:
     4    from sets import Set as set
     5
     6from django.db import models
     7
     8
     9class User(models.Model):
     10    username = models.CharField(max_length = 20)
     11
     12    def __unicode__(self):
     13        return self.username
     14
     15
     16class Issue(models.Model):
     17    summary = models.CharField(max_length = 256)
     18    cc = models.ManyToManyField(
     19      User,
     20      blank = True,
     21      related_name = 'test_issue_cc',
     22    )
     23    client = models.ForeignKey(User, related_name = 'test_issue_client')
     24    duplicate_of = models.ForeignKey('self', blank = True, null = True)
     25    related_issues = models.ManyToManyField('self', blank = True)
     26
     27    def __unicode__(self):
     28        return u'#%u: %s' % (self.id, self.summary)
     29
     30    class Meta:
     31        ordering = ('id',)
     32
     33
     34__test__ = {'API_TESTS': """
     35>>> foo = User.objects.create(username = 'foo')
     36>>> bar = User.objects.create(username = 'bar')
     37>>> baz = User.objects.create(username = 'baz')
     38
     39>>> one = Issue.objects.create(summary = 'A wild and crazy issue', client = foo)
     40>>> one.cc = [bar, baz]
     41>>> one.id
     421L
     43
     44# Test non-recursive copying:
     45>>> two = one.copy()
     46>>> two
     47<Issue: #2: A wild and crazy issue>
     48>>> two.client == foo
     49True
     50>>> two.summary == one.summary
     51True
     52>>> list(two.cc.all()) == list(one.cc.all())
     53True
     54
     55>>> two.delete()
     56>>> Issue.objects.all()
     57[<Issue: #1: A wild and crazy issue>]
     58>>> User.objects.all()
     59[<User: foo>, <User: bar>, <User: baz>]
     60
     61# Test full recursion:
     62>>> two = one.copy(recurse = True)
     63>>> two
     64<Issue: #2: A wild and crazy issue>
     65
     66# two.client is a copy of foo; it is unequal because it has a different pk
     67>>> two.client
     68<User: foo>
     69>>> two.client != foo
     70True
     71
     72# two.cc is a set of copies of one.cc; they are unequal
     73>>> two.cc.all()
     74[<User: bar>, <User: baz>]
     75>>> set(one.cc.all()) != set(two.cc.all())
     76True
     77
     78>>> two.cc.all().delete()
     79>>> two.client.delete()
     80>>> two.delete()
     81>>> Issue.objects.all()
     82[<Issue: #1: A wild and crazy issue>]
     83>>> User.objects.all()
     84[<User: foo>, <User: bar>, <User: baz>]
     85
     86# Test recursion with foreign keys only:
     87>>> two = one.copy(recurse = True, recurse_m2m = False)
     88>>> two
     89<Issue: #2: A wild and crazy issue>
     90
     91# two.client is a copy of foo; it is unequal because it has a different pk
     92>>> two.client
     93<User: foo>
     94>>> two.client != foo
     95True
     96
     97# two.cc is equal to one.cc, because m2m fields were not recursed into
     98>>> set(one.cc.all()) == set(two.cc.all())
     99True
     100
     101>>> two.client.delete()
     102>>> two.delete()
     103>>> Issue.objects.all()
     104[<Issue: #1: A wild and crazy issue>]
     105>>> User.objects.all()
     106[<User: foo>, <User: bar>, <User: baz>]
     107
     108# Test foreign key recursion with reference to self (this is bogus data and it
     109# doesn't make much sense, but it does test the scenario correctly):
     110>>> one.duplicate_of = one
     111>>> one.save()
     112>>> two = one.copy(recurse = True, recurse_m2m = False)
     113
     114# two.duplicate_of should refer to itself, *not* to one:
     115>>> two.duplicate_of
     116<Issue: #2: A wild and crazy issue>
     117
     118>>> two.client.delete()
     119>>> two.delete()
     120>>> Issue.objects.all()
     121[<Issue: #1: A wild and crazy issue>]
     122>>> User.objects.all()
     123[<User: foo>, <User: bar>, <User: baz>]
     124
     125# Restore one to its original state:
     126>>> one.duplicate_of = None
     127>>> one.save()
     128
     129# Test m2m recursion with reference to self (again, pretty bogus data):
     130>>> one.related_issues = [one]
     131
     132>>> two = one.copy(recurse = False, recurse_m2m = True)
     133
     134# Again, two should refer to itself, *not* to one:
     135>>> two.related_issues.all()
     136[<Issue: #2: A wild and crazy issue>]
     137"""}
Back to Top