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
def skip(self): """ Determine whether or not this object should be skipped. If this model is a parent of a single subclassed instance, skip it. The subclassed instance will create this parent instance for us. TODO: Allow the user to force its creation? """ if self.skip_me is not None: return self.skip_me try: # Django trunk since r7722 uses CollectedObjects instead of dict from django.db.models.query import CollectedObjects sub_objects = CollectedObjects() except ImportError: # previous versions don't have CollectedObjects sub_objects = {} self.instance._collect_sub_objects(sub_objects) if reduce( lambda x, y: x + y, [self.model in so._meta.parents for so in sub_objects.keys()]) == 1: pk_name = self.instance._meta.pk.name key = '%s_%s' % (self.model.__name__, getattr(self.instance, pk_name)) self.context[key] = None self.skip_me = True else: self.skip_me = False return self.skip_me
def _publisher_delete_marked(self, collect=True): """If this instance, or some remote instances are marked for deletion kill them. """ if self.publisher_is_draft: # escape soon from draft models return if collect: from django.db.models.query import CollectedObjects seen = CollectedObjects() self._collect_delete_marked_sub_objects(seen) for cls in seen.unordered_keys(): items = seen[cls] if issubclass(cls, Publisher): for item in items.values(): item._publisher_delete_marked(collect=False) if self.publisher_state == Publisher.PUBLISHER_STATE_DELETE: try: self.delete() except AttributeError: # this exception may happen because of the plugin relations # to CMSPlugin and mppt way of _meta assignment pass
def skip(self): """ Determine whether or not this object should be skipped. If this model is a parent of a single subclassed instance, skip it. The subclassed instance will create this parent instance for us. TODO: Allow the user to force its creation? """ if self.skip_me is not None: return self.skip_me try: # Django trunk since r7722 uses CollectedObjects instead of dict from django.db.models.query import CollectedObjects sub_objects = CollectedObjects() except ImportError: # previous versions don't have CollectedObjects sub_objects = {} self.instance._collect_sub_objects(sub_objects) if reduce(lambda x, y: x+y, [self.model in so._meta.parents for so in sub_objects.keys()]) == 1: pk_name = self.instance._meta.pk.name # MMH: print "*** pk_name = %s" % (pk_name) key = '%s_%s' % (self.model.__name__, getattr(self.instance, pk_name)) self.context[key] = None self.skip_me = True else: self.skip_me = False return self.skip_me
def _publisher_delete_marked(self, collect=True): """If this instance, or some remote instances are marked for deletion kill them. """ if self.publisher_is_draft: # escape soon from draft models return if collect: from django.db.models.query import CollectedObjects seen = CollectedObjects() self._collect_delete_marked_sub_objects(seen) for cls in seen.unordered_keys(): items = seen[cls] if issubclass(cls, Page): for item in items.values(): item._publisher_delete_marked(collect=False) if self.publisher_state == self.PUBLISHER_STATE_DELETE: try: self.delete() except AttributeError: # this exception may happen because of the plugin relations # to CMSPlugin and mppt way of _meta assignment pass
def duplicate(obj, update=None, model_order=None): """ Duplicate all related objects of obj updating attributes using dict update. If a duplicated objects has a FK to another duplicated object then update that. Returns the duplicate copy of obj. model_order is a list of models which specify in which order objects should be saved. This function offers acceptable performance on small object trees. """ #TODO: The reference to obj is lost - can we save it somehow? #TODO: What about m2m relationships? # We lose the reference to obj so store this so we can look it up again. obj_pk = getattr(obj, obj._meta.pk.name) collected_objs = CollectedObjects() obj._collect_sub_objects(collected_objs) related_models = collected_objs.keys() root_obj = None # Sometimes it's good enough just to save in reverse deletion order. if model_order is None: model_order = reversed(related_models) for model in model_order: if model not in collected_objs: continue # 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_objs = collected_objs[model] for pk_val, sub_obj in sub_objs.iteritems(): for fk in fks: fk_value = getattr(sub_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(sub_obj, fk.name, dupe_obj) # Duplicate the object and save it. sub_obj.id = None for k, v in update.items(): setattr(sub_obj, k, v) sub_obj.save() if root_obj is None: root_obj = sub_obj # Restore the reference to obj. obj = obj._default_manager.get(pk=obj_pk) return root_obj
def update_related_fields(obj, update): """ Update all attributes in dict update for all objects related to obj. Based on the delete object code: http://bit.ly/osrZf """ collected_objs = CollectedObjects() obj._collect_sub_objects(collected_objs) models = collected_objs.keys() for model in models: instance_tuple = collected_objs[model].items() pk_list = [pk for pk, instance in instance_tuple] # Update fields in the model. updates = update.copy() [updates.pop(fn) for fn in updates.keys() if not _has_field(model, fn)] model._default_manager.filter(id__in=pk_list).update(**updates)
def delete(self): assert self._get_pk_val() is not None, "%s object can't be deleted because its %s attribute is set to None." % (self._meta.object_name, self._meta.pk.attname) # Find all the objects than need to be deleted. seen_objs = CollectedObjects() self._collect_sub_objects(seen_objs) # Actually delete the objects. delete_objects(seen_objs)
def test_delete(self): ## Second, test the usage of CollectedObjects by Model.delete() # Due to the way that transactions work in the test harness, doing # m.delete() here can work but fail in a real situation, since it may # delete all objects, but not in the right order. So we manually check # that the order of deletion is correct. # Also, it is possible that the order is correct 'accidentally', due # solely to order of imports etc. To check this, we set the order that # 'get_models()' will retrieve to a known 'nice' order, and then try # again with a known 'tricky' order. Slightly naughty access to # internals here :-) # If implementation changes, then the tests may need to be simplified: # - remove the lines that set the .keyOrder and clear the related # object caches # - remove the second set of tests (with a2, b2 etc) a1 = A.objects.create() b1 = B.objects.create(a=a1) c1 = C.objects.create(b=b1) d1 = D.objects.create(c=c1, a=a1) o = CollectedObjects() a1._collect_sub_objects(o) self.assertEqual(o.keys(), [D, C, B, A]) a1.delete() # Same again with a known bad order self.order_models("d", "c", "b", "a") self.clear_rel_obj_caches(A, B, C, D) a2 = A.objects.create() b2 = B.objects.create(a=a2) c2 = C.objects.create(b=b2) d2 = D.objects.create(c=c2, a=a2) o = CollectedObjects() a2._collect_sub_objects(o) self.assertEqual(o.keys(), [D, C, B, A]) a2.delete()
def test_nullable_related_fields_collected_objects_model_delete(self): ## Second, test the usage of CollectedObjects by Model.delete() e1 = E() e1.save() f1 = F(e=e1) f1.save() e1.f = f1 e1.save() # Since E.f is nullable, we should delete F first (after nulling out # the E.f field), then E. o = CollectedObjects() e1._collect_sub_objects(o) self.assertQuerysetEqual(o.keys(), ["<class 'modeltests.delete.models.F'>", "<class 'modeltests.delete.models.E'>"]) # temporarily replace the UpdateQuery class to verify that E.f # is actually nulled out first original_class = django.db.models.sql.UpdateQuery django.db.models.sql.UpdateQuery = LoggingUpdateQuery # this is ugly, but it works global test_last_cleared_field test_last_cleared_field = '' e1.delete() self.assertEqual(test_last_cleared_field, 'f') e2 = E() e2.save() f2 = F(e=e2) f2.save() e2.f = f2 e2.save() # Same deal as before, though we are starting from the other object. o = CollectedObjects() f2._collect_sub_objects(o) o.keys() ["<class 'modeltests.delete.models.F'>", "<class 'modeltests.delete.models.E'>"] test_last_cleared_field = '' f2.delete() self.assertEqual(test_last_cleared_field, 'f') # Put this back to normal django.db.models.sql.UpdateQuery = original_class # Restore the app cache to previous condition so that all # models are accounted for. cache.app_models['delete'].keyOrder = ['a', 'b', 'c', 'd', 'e', 'f'] clear_rel_obj_caches([A, B, C, D, E, F])
def test_delete_nullable(self): e1 = E.objects.create() f1 = F.objects.create(e=e1) e1.f = f1 e1.save() # Since E.f is nullable, we should delete F first (after nulling out # the E.f field), then E. o = CollectedObjects() e1._collect_sub_objects(o) self.assertEqual(o.keys(), [F, E]) # temporarily replace the UpdateQuery class to verify that E.f is # actually nulled out first logged = [] class LoggingUpdateQuery(sql.UpdateQuery): def clear_related(self, related_field, pk_list, using): logged.append(related_field.name) return super(LoggingUpdateQuery, self).clear_related(related_field, pk_list, using) original = sql.UpdateQuery sql.UpdateQuery = LoggingUpdateQuery e1.delete() self.assertEqual(logged, ["f"]) logged = [] e2 = E.objects.create() f2 = F.objects.create(e=e2) e2.f = f2 e2.save() # Same deal as before, though we are starting from the other object. o = CollectedObjects() f2._collect_sub_objects(o) self.assertEqual(o.keys(), [F, E]) f2.delete() self.assertEqual(logged, ["f"]) logged = [] sql.UpdateQuery = original
def test_collected_objects_data_structure(self): ## Test the CollectedObjects data structure directly g = CollectedObjects() self.assertFalse(g.add("key1", 1, "item1", None)) self.assertEqual(g["key1"], {1: 'item1'}) self.assertFalse(g.add("key2", 1, "item1", "key1")) self.assertFalse(g.add("key2", 2, "item2", "key1")) self.assertEqual(g["key2"], {1: 'item1', 2: 'item2'}) self.assertFalse(g.add("key3", 1, "item1", "key1")) self.assertTrue(g.add("key3", 1, "item1", "key2")) self.assertEqual(g.ordered_keys(), ['key3', 'key2', 'key1']) self.assertTrue(g.add("key2", 1, "item1", "key3")) self.assertRaises(CyclicDependency, g.ordered_keys)
def test_nullable_related_fields_collected_objects(self): ## First, test the CollectedObjects data structure directly g = CollectedObjects() self.assertFalse(g.add("key1", 1, "item1", None)) self.assertFalse(g.add("key2", 1, "item1", "key1", nullable=True)) self.assertTrue(g.add("key1", 1, "item1", "key2")) self.assertEqual(g.ordered_keys(), ['key1', 'key2'])
def test_collected_objects(self): g = CollectedObjects() self.assertFalse(g.add("key1", 1, "item1", None)) self.assertEqual(g["key1"], {1: "item1"}) self.assertFalse(g.add("key2", 1, "item1", "key1")) self.assertFalse(g.add("key2", 2, "item2", "key1")) self.assertEqual(g["key2"], {1: "item1", 2: "item2"}) self.assertFalse(g.add("key3", 1, "item1", "key1")) self.assertTrue(g.add("key3", 1, "item1", "key2")) self.assertEqual(g.ordered_keys(), ["key3", "key2", "key1"]) self.assertTrue(g.add("key2", 1, "item1", "key3")) self.assertRaises(CyclicDependency, g.ordered_keys)
def skip(self): """ Determine whether or not this object should be skipped. If this model instance is a parent of a single subclassed instance, skip it. The subclassed instance will create this parent instance for us. TODO: Allow the user to force its creation? """ if self.skip_me is not None: return self.skip_me def get_skip_version(): """ Return which version of the skip code should be run Django's deletion code was refactored in r14507 which was just two days before 1.3 alpha 1 (r14519) """ if not hasattr(self, '_SKIP_VERSION'): version = django.VERSION # no, it isn't lisp. I swear. self._SKIP_VERSION = ( version[0] > 1 or ( # django 2k... someday :) version[0] == 1 and ( # 1.x version[1] >= 4 or # 1.4+ version[1] == 3 and not ( # 1.3.x (version[3] == 'alpha' and version[1] == 0)))) ) and 2 or 1 # NOQA return self._SKIP_VERSION if get_skip_version() == 1: try: # Django trunk since r7722 uses CollectedObjects instead of dict from django.db.models.query import CollectedObjects sub_objects = CollectedObjects() except ImportError: # previous versions don't have CollectedObjects sub_objects = {} self.instance._collect_sub_objects(sub_objects) sub_objects = sub_objects.keys() elif get_skip_version() == 2: from django.db.models.deletion import Collector from django.db import router cls = self.instance.__class__ using = router.db_for_write(cls, instance=self.instance) collector = Collector(using=using) collector.collect([self.instance], collect_related=False) # collector stores its instances in two places. I *think* we # only need collector.data, but using the batches is needed # to perfectly emulate the old behaviour # TODO: check if batches are really needed. If not, remove them. sub_objects = sum([list(i) for i in collector.data.values()], []) for batch in collector.batches.values(): # batch.values can be sets, which must be converted to lists sub_objects += sum([list(i) for i in batch.values()], []) sub_objects_parents = [so._meta.parents for so in sub_objects] if [self.model in p for p in sub_objects_parents].count(True) == 1: # since this instance isn't explicitly created, it's variable name # can't be referenced in the script, so record None in context dict pk_name = self.instance._meta.pk.name key = '%s_%s' % (self.model.__name__, getattr(self.instance, pk_name)) self.context[key] = None self.skip_me = True else: self.skip_me = False return self.skip_me
def test_collected_objects_null(self): g = CollectedObjects() self.assertFalse(g.add("key1", 1, "item1", None)) self.assertFalse(g.add("key2", 1, "item1", "key1", nullable=True)) self.assertTrue(g.add("key1", 1, "item1", "key2")) self.assertEqual(g.ordered_keys(), ["key1", "key2"])
def skip(self): """ Determine whether or not this object should be skipped. If this model instance is a parent of a single subclassed instance, skip it. The subclassed instance will create this parent instance for us. TODO: Allow the user to force its creation? """ if self.skip_me is not None: return self.skip_me def get_skip_version(): """ Return which version of the skip code should be run Django's deletion code was refactored in r14507 which was just two days before 1.3 alpha 1 (r14519) """ if not hasattr(self, '_SKIP_VERSION'): version = django.VERSION # no, it isn't lisp. I swear. self._SKIP_VERSION = ( version[0] > 1 or ( # django 2k... someday :) version[0] == 1 and ( # 1.x version[1] >= 4 or # 1.4+ version[1] == 3 and not ( # 1.3.x (version[3] == 'alpha' and version[1] == 0) ) ) ) ) and 2 or 1 # NOQA return self._SKIP_VERSION if get_skip_version() == 1: try: # Django trunk since r7722 uses CollectedObjects instead of dict from django.db.models.query import CollectedObjects sub_objects = CollectedObjects() except ImportError: # previous versions don't have CollectedObjects sub_objects = {} self.instance._collect_sub_objects(sub_objects) sub_objects = sub_objects.keys() elif get_skip_version() == 2: from django.db.models.deletion import Collector from django.db import router cls = self.instance.__class__ using = router.db_for_write(cls, instance=self.instance) collector = Collector(using=using) collector.collect([self.instance], collect_related=False) # collector stores its instances in two places. I *think* we # only need collector.data, but using the batches is needed # to perfectly emulate the old behaviour # TODO: check if batches are really needed. If not, remove them. sub_objects = sum([list(i) for i in collector.data.values()], []) if hasattr(collector, 'batches'): # Django 1.6 removed batches for being dead code # https://github.com/django/django/commit/a170c3f755351beb35f8166ec3c7e9d524d9602 for batch in collector.batches.values(): # batch.values can be sets, which must be converted to lists sub_objects += sum([list(i) for i in batch.values()], []) sub_objects_parents = [so._meta.parents for so in sub_objects] if [self.model in p for p in sub_objects_parents].count(True) == 1: # since this instance isn't explicitly created, it's variable name # can't be referenced in the script, so record None in context dict pk_name = self.instance._meta.pk.name key = '%s_%s' % (self.model.__name__, getattr(self.instance, pk_name)) self.context[key] = None self.skip_me = True else: self.skip_me = False return self.skip_me
def test_collected_objects_by_model_delete(self): ## Test the usage of CollectedObjects by Model.delete() # Due to the way that transactions work in the test harness, # doing m.delete() here can work but fail in a real situation, # since it may delete all objects, but not in the right order. # So we manually check that the order of deletion is correct. # Also, it is possible that the order is correct 'accidentally', due # solely to order of imports etc. To check this, we set the order # that 'get_models()' will retrieve to a known 'nice' order, and # then try again with a known 'tricky' order. Slightly naughty # access to internals here :-) # If implementation changes, then the tests may need to be simplified: # - remove the lines that set the .keyOrder and clear the related # object caches # - remove the second set of tests (with a2, b2 etc) # Nice order cache.app_models['delete'].keyOrder = ['a', 'b', 'c', 'd'] clear_rel_obj_caches([A, B, C, D]) a1 = A() a1.save() b1 = B(a=a1) b1.save() c1 = C(b=b1) c1.save() d1 = D(c=c1, a=a1) d1.save() o = CollectedObjects() a1._collect_sub_objects(o) self.assertQuerysetEqual(o.keys(), ["<class 'modeltests.delete.models.D'>", "<class 'modeltests.delete.models.C'>", "<class 'modeltests.delete.models.B'>", "<class 'modeltests.delete.models.A'>"]) a1.delete() # Same again with a known bad order cache.app_models['delete'].keyOrder = ['d', 'c', 'b', 'a'] clear_rel_obj_caches([A, B, C, D]) a2 = A() a2.save() b2 = B(a=a2) b2.save() c2 = C(b=b2) c2.save() d2 = D(c=c2, a=a2) d2.save() o = CollectedObjects() a2._collect_sub_objects(o) self.assertQuerysetEqual(o.keys(), ["<class 'modeltests.delete.models.D'>", "<class 'modeltests.delete.models.C'>", "<class 'modeltests.delete.models.B'>", "<class 'modeltests.delete.models.A'>"]) a2.delete()