示例#1
0
    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)
示例#2
0
    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
示例#3
0
 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)
示例#4
0
    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
示例#5
0
    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)
示例#6
0
 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)
示例#7
0
    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())
示例#8
0
    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())
示例#9
0
    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)
示例#10
0
    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)
示例#11
0
    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
示例#12
0
    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
示例#13
0
    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))
示例#14
0
    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))
示例#15
0
    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
示例#16
0
    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
示例#17
0
    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
示例#18
0
    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