Created
December 17, 2012 12:38
-
-
Save anonymous/4317994 to your computer and use it in GitHub Desktop.
Basic Mongoid versioning extension, where version number is timestamp. This allows to use method version_at to get object at specific time
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# encoding: utf-8 | |
module Mongoid | |
# Include this module to get automatic versioning of root level documents. | |
# This will add a version field to the +Document+ and a has_many association | |
# with all the versions contained in it. | |
module TimedVersioning | |
extend ActiveSupport::Concern | |
included do | |
field :version, type: Time, default: ->{ Time.now } | |
embeds_many \ | |
:versions, | |
class_name: self.name, | |
validate: false, | |
cyclic: true, | |
inverse_of: nil, | |
versioned: true | |
set_callback :save, :before, :revise, if: :revisable? | |
class_attribute :version_max | |
self.cyclic = true | |
end | |
# Create a new version of the +Document+. This will load the previous | |
# document from the database and set it as the next version before saving | |
# the current document. It then increments the version number. If a #max_versions | |
# limit is set in the model and it's exceeded, the oldest version gets discarded. | |
# | |
# @example Revise the document. | |
# person.revise | |
# | |
# @since 1.0.0 | |
def revise | |
previous = previous_revision | |
if previous && versioned_attributes_changed? | |
new_version = versions.build( | |
previous.versioned_attributes, without_protection: true | |
) | |
new_version._id = nil | |
if version_max.present? && versions.length > version_max | |
deleted = versions.first | |
if deleted.paranoid? | |
versions.delete_one(deleted) | |
collection.find(atomic_selector). | |
update({ "$pull" => { "versions" => { "version" => deleted.version }}}) | |
else | |
versions.delete(deleted) | |
end | |
end | |
self.version = Time.now | |
end | |
end | |
# Forces the creation of a new version of the +Document+, regardless of | |
# whether a change was actually made. | |
# | |
# @example Revise the document. | |
# person.revise! | |
# | |
# @since 2.2.1 | |
def revise! | |
versions.build( | |
(previous_revision || self).versioned_attributes, without_protection: true | |
) | |
versions.shift if version_max.present? && versions.length > version_max | |
self.version = Time.now | |
save | |
end | |
# Filters the results of +changes+ by removing any fields that should | |
# not be versioned. | |
# | |
# @return [ Hash ] A hash of versioned changed attributes. | |
# | |
# @since 2.1.0 | |
def versioned_changes | |
only_versioned_attributes(changes.except("updated_at")) | |
end | |
# Filters the results of +attributes+ by removing any fields that should | |
# not be versioned. | |
# | |
# @return [ Hash ] A hash of versioned attributes. | |
# | |
# @since 2.1.0 | |
def versioned_attributes | |
only_versioned_attributes(attributes) | |
end | |
# Check if any versioned fields have been modified. This is similar | |
# to +changed?+, except this method also ignores fields set to be | |
# ignored by versioning. | |
# | |
# @return [ Boolean ] Whether fields that will be versioned have changed. | |
# | |
# @since 2.1.0 | |
def versioned_attributes_changed? | |
!versioned_changes.empty? | |
end | |
# Executes a block that temporarily disables versioning. This is for cases | |
# where you do not want to version on every save. | |
# | |
# @example Execute a save without versioning. | |
# person.versionless(&:save) | |
# | |
# @return [ Object ] The document or result of the block execution. | |
# | |
# @since 2.0.0 | |
def versionless | |
@versionless = true | |
result = yield(self) if block_given? | |
@versionless = false | |
result || self | |
end | |
def version_at time = nil | |
return self unless time | |
version_times = (versions.map(&:version)).sort | |
return versions.first if time <= version_times.min | |
return self if time >= version_times.max | |
versions.gte(version: time).asc(:version).first | |
end | |
private | |
# Find the previous version of this document in the database, or if the | |
# document had been saved without versioning return the persisted one. | |
# | |
# @example Find the last version. | |
# document.find_last_version | |
# | |
# @return [ Document, nil ] The previously saved document. | |
# | |
# @since 2.0.0 | |
def previous_revision | |
_loading_revision do | |
self.class.unscoped. | |
where(_id: id). | |
any_of({ version: version }, { version: nil }).first | |
end | |
end | |
# Is the document able to be revised? This is true if the document has | |
# changed and we have not explicitly told it not to version. | |
# | |
# @example Is the document revisable? | |
# document.revisable? | |
# | |
# @return [ true, false ] If the document is revisable. | |
# | |
# @since 2.0.0 | |
def revisable? | |
versioned_attributes_changed? && !versionless? | |
end | |
# Are we in versionless mode? This is true if in a versionless block on the | |
# document. | |
# | |
# @example Is the document in versionless mode? | |
# document.versionless? | |
# | |
# @return [ true, false ] Is the document not currently versioning. | |
# | |
# @since 2.0.0 | |
def versionless? | |
@versionless ||= false | |
end | |
# Filters fields that should not be versioned out of an attributes hash. | |
# Dynamic attributes are always versioned. | |
# | |
# @param [ Hash ] A hash with field names as keys. | |
# | |
# @return [ Hash ] The hash without non-versioned columns. | |
# | |
# @since 2.1.0 | |
def only_versioned_attributes(hash) | |
versioned = {} | |
hash.except("versions").each_pair do |name, value| | |
add_versioned_attribute(versioned, name, value) | |
end | |
versioned | |
end | |
# Add the versioned attribute. Will work now for localized fields. | |
# | |
# @api private | |
# | |
# @example Add the versioned attribute. | |
# model.add_versioned_attribute({}, "name", "test") | |
# | |
# @param [ Hash ] versioned The versioned attributes. | |
# @param [ String ] name The name of the field. | |
# @param [ Object ] value The value for the field. | |
# | |
# @since 3.0.10 | |
def add_versioned_attribute(versioned, name, value) | |
field = fields[name] | |
if field && field.localized? | |
versioned["#{name}_translations"] = value | |
else | |
versioned[name] = value if !field || field.versioned? | |
end | |
end | |
module ClassMethods | |
# Sets the maximum number of versions to store. | |
# | |
# @example Set the maximum. | |
# Person.max_versions(5) | |
# | |
# @param [ Integer ] number The maximum number to store. | |
# | |
# @return [ Integer ] The max number of versions. | |
def max_versions(number) | |
self.version_max = number.to_i | |
end | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment