Ticket #4027: django-model-copying.diff
File django-model-copying.diff, 10.2 KB (added by , 15 years ago) |
---|
-
django/db/models/base.py
=== modified file 'django/db/models/base.py'
11 11 import django.db.models.manager # Imported to register signal handler. 12 12 from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned, FieldError 13 13 from django.db.models.fields import AutoField, FieldDoesNotExist 14 from django.db.models.fields.related import OneToOneRel, ManyToOneRel, OneToOneField 14 from django.db.models.fields.related import OneToOneRel, ManyToOneRel, OneToOneField, ForeignKey 15 15 from django.db.models.query import delete_objects, Q 16 16 from django.db.models.query_utils import CollectedObjects, DeferredAttribute 17 17 from django.db.models.options import Options … … 606 606 def prepare_database_save(self, unused): 607 607 return self.pk 608 608 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 609 734 610 735 ############################################ 611 736 # 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'
1 try: 2 set 3 except NameError: 4 from sets import Set as set 5 6 from django.db import models 7 8 9 class User(models.Model): 10 username = models.CharField(max_length = 20) 11 12 def __unicode__(self): 13 return self.username 14 15 16 class 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 42 1L 43 44 # Test non-recursive copying: 45 >>> two = one.copy() 46 >>> two 47 <Issue: #2: A wild and crazy issue> 48 >>> two.client == foo 49 True 50 >>> two.summary == one.summary 51 True 52 >>> list(two.cc.all()) == list(one.cc.all()) 53 True 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 70 True 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()) 76 True 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 95 True 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()) 99 True 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 """}