def _create_at(self, timestamp=None, id=None, forced_identity=None, **kwargs): """ WARNING: Only for internal use and testing. Create a Versionable having a version_start_date and version_birth_date set to some pre-defined timestamp :param timestamp: point in time at which the instance has to be created :param id: version 4 UUID unicode object. Usually this is not specified, it will be automatically created. :param forced_identity: version 4 UUID unicode object. For internal use only. :param kwargs: arguments needed for initializing the instance :return: an instance of the class """ id = Versionable.uuid(id) if forced_identity: ident = Versionable.uuid(forced_identity) else: ident = id if timestamp is None: timestamp = get_utc_now() kwargs['id'] = id kwargs['identity'] = ident kwargs['version_start_date'] = timestamp kwargs['version_birth_date'] = timestamp return super(VersionManager, self).create(**kwargs)
def delete(self): """ Deletes the records in the QuerySet. """ assert self.query.can_filter(), \ "Cannot use 'limit' or 'offset' with delete." # Ensure that only current objects are selected. del_query = self.filter(version_end_date__isnull=True) # The delete is actually 2 queries - one to find related objects, # and one to delete. Make sure that the discovery of related # objects is performed on the same database as the deletion. del_query._for_write = True # Disable non-supported fields. del_query.query.select_for_update = False del_query.query.select_related = False del_query.query.clear_ordering(force_empty=True) collector_class = get_versioned_delete_collector_class() collector = collector_class(using=del_query.db) collector.collect(del_query) collector.delete(get_utc_now()) # Clear the result cache, in case this QuerySet gets reused. self._result_cache = None
def _remove_items_at(self, timestamp, source_field_name, target_field_name, *objs): if objs: if timestamp is None: timestamp = get_utc_now() old_ids = set() for obj in objs: if isinstance(obj, self.model): # The Django 1.7-way is preferred if hasattr(self, 'target_field'): fk_val = \ self.target_field \ .get_foreign_related_value(obj)[0] else: raise TypeError( "We couldn't find the value of the foreign " "key, this might be due to the use of an " "unsupported version of Django") old_ids.add(fk_val) else: old_ids.add(obj) db = router.db_for_write(self.through, instance=self.instance) qs = self.through._default_manager.using(db).filter(**{ source_field_name: self.instance.id, '%s__in' % target_field_name: old_ids }).as_of(timestamp) for relation in qs: relation._delete_at(timestamp)
def delete(self): """ Deletes the records in the QuerySet. """ assert self.query.can_filter(), \ "Cannot use 'limit' or 'offset' with delete." # Ensure that only current objects are selected. del_query = self.filter(version_end_date__isnull=True) # The delete is actually 2 queries - one to find related objects, # and one to delete. Make sure that the discovery of related # objects is performed on the same database as the deletion. del_query._for_write = True # Disable non-supported fields. del_query.query.select_for_update = False del_query.query.select_related = False del_query.query.clear_ordering(force_empty=True) collector_class = get_versioned_delete_collector_class() collector = collector_class(using=del_query.db) collector.collect(del_query) collector.delete(get_utc_now()) # Clear the result cache, in case this QuerySet gets reused. self._result_cache = None
def _create_at(self, timestamp=None, id=None, forced_identity=None, **kwargs): """ WARNING: Only for internal use and testing. Create a Versionable having a version_start_date and version_birth_date set to some pre-defined timestamp :param timestamp: point in time at which the instance has to be created :param id: version 4 UUID unicode object. Usually this is not specified, it will be automatically created. :param forced_identity: version 4 UUID unicode object. For internal use only. :param kwargs: arguments needed for initializing the instance :return: an instance of the class """ id = Versionable.uuid(id) if forced_identity: ident = Versionable.uuid(forced_identity) else: ident = id if timestamp is None: timestamp = get_utc_now() kwargs['id'] = id kwargs['identity'] = ident kwargs['version_start_date'] = timestamp kwargs['version_birth_date'] = timestamp return super(VersionManager, self).create(**kwargs)
def _remove_items_at(self, timestamp, source_field_name, target_field_name, *objs): if objs: if timestamp is None: timestamp = get_utc_now() old_ids = set() for obj in objs: if isinstance(obj, self.model): # The Django 1.7-way is preferred if hasattr(self, 'target_field'): fk_val = \ self.target_field \ .get_foreign_related_value(obj)[0] else: raise TypeError( "We couldn't find the value of the foreign " "key, this might be due to the use of an " "unsupported version of Django") old_ids.add(fk_val) else: old_ids.add(obj) db = router.db_for_write(self.through, instance=self.instance) qs = self.through._default_manager.using(db).filter( **{ source_field_name: self.instance.id, '%s__in' % target_field_name: old_ids }).as_of(timestamp) for relation in qs: relation._delete_at(timestamp)
def delete(self, using=None, keep_parents=False): using = using or router.db_for_write(self.__class__, instance=self) assert self._get_pk_val() is not None, \ "{} object can't be deleted because its {} attribute is set to " \ "None.".format(self._meta.object_name, self._meta.pk.attname) collector_class = get_versioned_delete_collector_class() collector = collector_class(using=using) collector.collect([self], keep_parents=keep_parents) collector.delete(get_utc_now())
def delete(self, using=None, keep_parents=False): using = using or router.db_for_write(self.__class__, instance=self) assert self._get_pk_val() is not None, \ "{} object can't be deleted because its {} attribute is set to " \ "None.".format(self._meta.object_name, self._meta.pk.attname) collector_class = get_versioned_delete_collector_class() collector = collector_class(using=using) collector.collect([self], keep_parents=keep_parents) collector.delete(get_utc_now())
def __set__(self, instance, value): """ Completely overridden to avoid bulk deletion that happens when the parent method calls clear(). The parent method's logic is basically: clear all in bulk, then add the given objects in bulk. Instead, we figure out which ones are being added and removed, and call add and remove for these values. This lets us retain the versioning information. Since this is a many-to-many relationship, it is assumed here that the django.db.models.deletion.Collector logic, that is used in clear(), is not necessary here. Collector collects related models, e.g. ones that should also be deleted because they have a ON CASCADE DELETE relationship to the object, or, in the case of "Multi-table inheritance", are parent objects. :param instance: The instance on which the getter was called :param value: iterable of items to set """ if not instance.is_current: raise SuspiciousOperation( "Related values can only be directly set on the current " "version of an object") if not self.field.remote_field.through._meta.auto_created: opts = self.field.rel.through._meta raise AttributeError(( "Cannot set values on a ManyToManyField " "which specifies an intermediary model. " "Use %s.%s's Manager instead.") % ( opts.app_label, opts.object_name)) manager = self.__get__(instance) # Below comment is from parent __set__ method. We'll force # evaluation, too: # clear() can change expected output of 'value' queryset, we force # evaluation of queryset before clear; ticket #19816 value = tuple(value) being_removed, being_added = self.get_current_m2m_diff(instance, value) timestamp = get_utc_now() manager.remove_at(timestamp, *being_removed) manager.add_at(timestamp, *being_added)
def __set__(self, instance, value): """ Completely overridden to avoid bulk deletion that happens when the parent method calls clear(). The parent method's logic is basically: clear all in bulk, then add the given objects in bulk. Instead, we figure out which ones are being added and removed, and call add and remove for these values. This lets us retain the versioning information. Since this is a many-to-many relationship, it is assumed here that the django.db.models.deletion.Collector logic, that is used in clear(), is not necessary here. Collector collects related models, e.g. ones that should also be deleted because they have a ON CASCADE DELETE relationship to the object, or, in the case of "Multi-table inheritance", are parent objects. :param instance: The instance on which the getter was called :param value: iterable of items to set """ if not instance.is_current: raise SuspiciousOperation( "Related values can only be directly set on the current " "version of an object") if not self.field.rel.through._meta.auto_created: opts = self.field.rel.through._meta raise AttributeError(("Cannot set values on a ManyToManyField " "which specifies an intermediary model. " "Use %s.%s's Manager instead.") % (opts.app_label, opts.object_name)) manager = self.__get__(instance) # Below comment is from parent __set__ method. We'll force # evaluation, too: # clear() can change expected output of 'value' queryset, we force # evaluation of queryset before clear; ticket #19816 value = tuple(value) being_removed, being_added = self.get_current_m2m_diff(instance, value) timestamp = get_utc_now() manager.remove_at(timestamp, *being_removed) manager.add_at(timestamp, *being_added)
def detach(self): """ Detaches the instance from its history. Similar to creating a new object with the same field values. The id and identity fields are set to a new value. The returned object has not been saved, call save() afterwards when you are ready to persist the object. ManyToMany and reverse ForeignKey relations are lost for the detached object. :return: Versionable """ self.id = self.identity = self.uuid() self.version_start_date = self.version_birth_date = get_utc_now() self.version_end_date = None return self
def detach(self): """ Detaches the instance from its history. Similar to creating a new object with the same field values. The id and identity fields are set to a new value. The returned object has not been saved, call save() afterwards when you are ready to persist the object. ManyToMany and reverse ForeignKey relations are lost for the detached object. :return: Versionable """ self.id = self.identity = self.uuid() self.version_start_date = self.version_birth_date = get_utc_now() self.version_end_date = None return self
def __init__(self, *args, **kwargs): super(Versionable, self).__init__(*args, **kwargs) # _querytime is for library-internal use. self._querytime = QueryTime(time=None, active=False) # Ensure that the versionable field values are set. # If there are any deferred fields, then this instance is being # initialized from data in the database, and thus these values will # already be set (unless the fields are deferred, in which case they # should not be set here). if not self.get_deferred_fields(): if not getattr(self, 'version_start_date', None): setattr(self, 'version_start_date', get_utc_now()) if not getattr(self, 'version_birth_date', None): setattr(self, 'version_birth_date', self.version_start_date) if not getattr(self, self.VERSION_IDENTIFIER_FIELD, None): setattr(self, self.VERSION_IDENTIFIER_FIELD, self.uuid()) if not getattr(self, self.OBJECT_IDENTIFIER_FIELD, None): setattr(self, self.OBJECT_IDENTIFIER_FIELD, getattr(self, self.VERSION_IDENTIFIER_FIELD))
def __init__(self, *args, **kwargs): super(Versionable, self).__init__(*args, **kwargs) # _querytime is for library-internal use. self._querytime = QueryTime(time=None, active=False) # Ensure that the versionable field values are set. # If there are any deferred fields, then this instance is being # initialized from data in the database, and thus these values will # already be set (unless the fields are deferred, in which case they # should not be set here). if not self.get_deferred_fields(): if not getattr(self, 'version_start_date', None): setattr(self, 'version_start_date', get_utc_now()) if not getattr(self, 'version_birth_date', None): setattr(self, 'version_birth_date', self.version_start_date) if not getattr(self, self.VERSION_IDENTIFIER_FIELD, None): setattr(self, self.VERSION_IDENTIFIER_FIELD, self.uuid()) if not getattr(self, self.OBJECT_IDENTIFIER_FIELD, None): setattr(self, self.OBJECT_IDENTIFIER_FIELD, getattr(self, self.VERSION_IDENTIFIER_FIELD))
def restore(self, **kwargs): """ Restores this version as a new version, and returns this new version. If a current version already exists, it will be terminated before restoring this version. Relations (foreign key, reverse foreign key, many-to-many) are not restored with the old version. If provided in kwargs, (Versioned)ForeignKey fields will be set to the provided values. If passing an id for a (Versioned)ForeignKey, use the field.attname. For example: restore(team_id=myteam.pk) If passing an object, simply use the field name, e.g.: restore(team=myteam) If a (Versioned)ForeignKey is not nullable and no value is provided for it in kwargs, a ForeignKeyRequiresValueError will be raised. :param kwargs: arguments used to initialize the class instance :return: Versionable """ if not self.pk: raise ValueError( 'Instance must be saved and terminated before it can be ' 'restored.') if self.is_current: raise ValueError( 'This is the current version, no need to restore it.') if self.get_deferred_fields(): # It would be necessary to fetch the record from the database # again for this to succeed. # Alternatively, perhaps it would be possible to create a copy # of the object after fetching the missing fields. # Doing so may be unexpected by the calling code, so raise an # exception: the calling code should be adapted if necessary. raise ValueError( 'Can not restore a model instance that has deferred fields') cls = self.__class__ now = get_utc_now() restored = copy.copy(self) restored.version_end_date = None restored.version_start_date = now fields = [ f for f in cls._meta.local_fields if f.name not in Versionable.VERSIONABLE_FIELDS ] for field in fields: if field.attname in kwargs: setattr(restored, field.attname, kwargs[field.attname]) elif field.name in kwargs: setattr(restored, field.name, kwargs[field.name]) elif isinstance(field, ForeignKey): # Set all non-provided ForeignKeys to None. If required, # raise an error. try: setattr(restored, field.name, None) # Check for non null foreign key removed since Django 1.10 # https://docs.djangoproject.com/en/1.10/releases/1.10/ # #removed-null-assignment-check-for-non-null-foreign- # key-fields if not field.null: raise ValueError except ValueError: raise ForeignKeyRequiresValueError self.id = self.uuid() with transaction.atomic(): # If this is not the latest version, terminate the latest version latest = cls.objects.current_version(self, check_db=True) if latest and latest != self: latest.delete() restored.version_start_date = latest.version_end_date self.save() restored.save() # Update ManyToMany relations to point to the old version's id # instead of the restored version's id. for field_name in self.get_all_m2m_field_names(): manager = getattr(restored, field_name) # returns a VersionedRelatedManager instance manager.through.objects.filter( **{ manager.source_field.attname: restored.id }).update(**{manager.source_field_name: self}) return restored
def clone(self, forced_version_date=None, in_bulk=False): """ Clones a Versionable and returns a fresh copy of the original object. Original source: ClonableMixin snippet (http://djangosnippets.org/snippets/1271), with the pk/id change suggested in the comments :param forced_version_date: a timestamp including tzinfo; this value is usually set only internally! :param in_bulk: whether not to write this objects to the database already, if not necessary; this value is usually set only internally for performance optimization :return: returns a fresh clone of the original object (with adjusted relations) """ if not self.pk: raise ValueError('Instance must be saved before it can be cloned') if self.version_end_date: raise ValueError( 'This is a historical item and can not be cloned.') if forced_version_date: if not self.version_start_date <= forced_version_date <= \ get_utc_now(): raise ValueError( 'The clone date must be between the version start date ' 'and now.') else: forced_version_date = get_utc_now() if self.get_deferred_fields(): # It would be necessary to fetch the record from the database # again for this to succeed. # Alternatively, perhaps it would be possible to create a copy of # the object after fetching the missing fields. # Doing so may be unexpected by the calling code, so raise an # exception: the calling code should be adapted if necessary. raise ValueError( 'Can not clone a model instance that has deferred fields') earlier_version = self later_version = copy.copy(earlier_version) later_version.version_end_date = None later_version.version_start_date = forced_version_date # set earlier_version's ID to a new UUID so the clone (later_version) # can get the old one -- this allows 'head' to always have the original # id allowing us to get at all historic foreign key relationships earlier_version.id = self.uuid() earlier_version.version_end_date = forced_version_date if not in_bulk: # This condition might save us a lot of database queries if we are # being called from a loop like in .clone_relations earlier_version.save() later_version.save() else: earlier_version._not_created = True # re-create ManyToMany relations for field_name in self.get_all_m2m_field_names(): earlier_version.clone_relations(later_version, field_name, forced_version_date) return later_version
def clone(self, forced_version_date=None, in_bulk=False): """ Clones a Versionable and returns a fresh copy of the original object. Original source: ClonableMixin snippet (http://djangosnippets.org/snippets/1271), with the pk/id change suggested in the comments :param forced_version_date: a timestamp including tzinfo; this value is usually set only internally! :param in_bulk: whether not to write this objects to the database already, if not necessary; this value is usually set only internally for performance optimization :return: returns a fresh clone of the original object (with adjusted relations) """ if not self.pk: raise ValueError('Instance must be saved before it can be cloned') if self.version_end_date: raise ValueError( 'This is a historical item and can not be cloned.') if forced_version_date: if not self.version_start_date <= forced_version_date <= \ get_utc_now(): raise ValueError( 'The clone date must be between the version start date ' 'and now.') else: forced_version_date = get_utc_now() if self.get_deferred_fields(): # It would be necessary to fetch the record from the database # again for this to succeed. # Alternatively, perhaps it would be possible to create a copy of # the object after fetching the missing fields. # Doing so may be unexpected by the calling code, so raise an # exception: the calling code should be adapted if necessary. raise ValueError( 'Can not clone a model instance that has deferred fields') earlier_version = self later_version = copy.copy(earlier_version) later_version.version_end_date = None later_version.version_start_date = forced_version_date # set earlier_version's ID to a new UUID so the clone (later_version) # can get the old one -- this allows 'head' to always have the original # id allowing us to get at all historic foreign key relationships earlier_version.id = self.uuid() earlier_version.version_end_date = forced_version_date if not in_bulk: # This condition might save us a lot of database queries if we are # being called from a loop like in .clone_relations earlier_version.save() later_version.save() else: earlier_version._not_created = True # re-create ManyToMany relations for field_name in self.get_all_m2m_field_names(): earlier_version.clone_relations(later_version, field_name, forced_version_date) return later_version
def restore(self, **kwargs): """ Restores this version as a new version, and returns this new version. If a current version already exists, it will be terminated before restoring this version. Relations (foreign key, reverse foreign key, many-to-many) are not restored with the old version. If provided in kwargs, (Versioned)ForeignKey fields will be set to the provided values. If passing an id for a (Versioned)ForeignKey, use the field.attname. For example: restore(team_id=myteam.pk) If passing an object, simply use the field name, e.g.: restore(team=myteam) If a (Versioned)ForeignKey is not nullable and no value is provided for it in kwargs, a ForeignKeyRequiresValueError will be raised. :param kwargs: arguments used to initialize the class instance :return: Versionable """ if not self.pk: raise ValueError( 'Instance must be saved and terminated before it can be ' 'restored.') if self.is_current: raise ValueError( 'This is the current version, no need to restore it.') if self.get_deferred_fields(): # It would be necessary to fetch the record from the database # again for this to succeed. # Alternatively, perhaps it would be possible to create a copy # of the object after fetching the missing fields. # Doing so may be unexpected by the calling code, so raise an # exception: the calling code should be adapted if necessary. raise ValueError( 'Can not restore a model instance that has deferred fields') cls = self.__class__ now = get_utc_now() restored = copy.copy(self) restored.version_end_date = None restored.version_start_date = now fields = [f for f in cls._meta.local_fields if f.name not in Versionable.VERSIONABLE_FIELDS] for field in fields: if field.attname in kwargs: # Fake an object in order to avoid a DB roundtrip # This was made necessary, since assigning to the field's # attname did not work anymore with Django 2.0 obj = field.remote_field.model(id=kwargs[field.attname]) setattr(restored, field.name, obj) elif field.name in kwargs: setattr(restored, field.name, kwargs[field.name]) elif isinstance(field, ForeignKey): # Set all non-provided ForeignKeys to None. If required, # raise an error. try: setattr(restored, field.name, None) # Check for non null foreign key removed since Django 1.10 # https://docs.djangoproject.com/en/1.10/releases/1.10/ # #removed-null-assignment-check-for-non-null-foreign- # key-fields if not field.null: raise ValueError except ValueError: raise ForeignKeyRequiresValueError self.id = self.uuid() with transaction.atomic(): # If this is not the latest version, terminate the latest version latest = cls.objects.current_version(self, check_db=True) if latest and latest != self: latest.delete() restored.version_start_date = latest.version_end_date self.save() restored.save() # Update ManyToMany relations to point to the old version's id # instead of the restored version's id. for field_name in self.get_all_m2m_field_names(): manager = getattr(restored, field_name) # returns a VersionedRelatedManager instance manager.through.objects.filter( **{manager.source_field.attname: restored.id}).update( **{manager.source_field_name: self}) return restored