set_vestal_versions 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/LICENSE +20 -0
- data/README.rdoc +196 -0
- data/lib/generators/vestal_versions.rb +11 -0
- data/lib/generators/vestal_versions/migration/migration_generator.rb +17 -0
- data/lib/generators/vestal_versions/migration/templates/initializer.rb +9 -0
- data/lib/generators/vestal_versions/migration/templates/migration.rb +28 -0
- data/lib/vestal_versions.rb +126 -0
- data/lib/vestal_versions/changes.rb +122 -0
- data/lib/vestal_versions/conditions.rb +57 -0
- data/lib/vestal_versions/control.rb +200 -0
- data/lib/vestal_versions/creation.rb +93 -0
- data/lib/vestal_versions/deletion.rb +39 -0
- data/lib/vestal_versions/options.rb +41 -0
- data/lib/vestal_versions/reload.rb +17 -0
- data/lib/vestal_versions/reset.rb +24 -0
- data/lib/vestal_versions/reversion.rb +82 -0
- data/lib/vestal_versions/users.rb +55 -0
- data/lib/vestal_versions/version.rb +80 -0
- data/lib/vestal_versions/version_num.rb +3 -0
- data/lib/vestal_versions/version_tagging.rb +50 -0
- data/lib/vestal_versions/versioned.rb +27 -0
- data/lib/vestal_versions/versions.rb +74 -0
- data/spec/spec_helper.rb +20 -0
- data/spec/support/models.rb +19 -0
- data/spec/support/schema.rb +25 -0
- data/spec/vestal_versions/changes_spec.rb +134 -0
- data/spec/vestal_versions/conditions_spec.rb +103 -0
- data/spec/vestal_versions/control_spec.rb +120 -0
- data/spec/vestal_versions/creation_spec.rb +90 -0
- data/spec/vestal_versions/deletion_spec.rb +86 -0
- data/spec/vestal_versions/options_spec.rb +45 -0
- data/spec/vestal_versions/reload_spec.rb +18 -0
- data/spec/vestal_versions/reset_spec.rb +111 -0
- data/spec/vestal_versions/reversion_spec.rb +103 -0
- data/spec/vestal_versions/users_spec.rb +21 -0
- data/spec/vestal_versions/version_spec.rb +61 -0
- data/spec/vestal_versions/version_tagging_spec.rb +39 -0
- data/spec/vestal_versions/versioned_spec.rb +16 -0
- data/spec/vestal_versions/versions_spec.rb +176 -0
- metadata +165 -0
@@ -0,0 +1,24 @@
|
|
1
|
+
module VestalVersions
|
2
|
+
# Adds the ability to "reset" (or hard revert) a versioned ActiveRecord::Base instance.
|
3
|
+
module Reset
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
# Adds the instance methods required to reset an object to a previous version.
|
7
|
+
module InstanceMethods
|
8
|
+
# Similar to +revert_to!+, the +reset_to!+ method reverts an object to a previous version,
|
9
|
+
# only instead of creating a new record in the version history, +reset_to!+ deletes all of
|
10
|
+
# the version history that occurs after the version reverted to.
|
11
|
+
#
|
12
|
+
# The action taken on each version record after the point of reversion is determined by the
|
13
|
+
# <tt>:dependent</tt> option given to the +versioned+ method. See the +versioned+ method
|
14
|
+
# documentation for more details.
|
15
|
+
def reset_to!(value)
|
16
|
+
if saved = skip_version{ revert_to!(value) }
|
17
|
+
versions.send(:delete_records, versions.after(value))
|
18
|
+
reset_version
|
19
|
+
end
|
20
|
+
saved
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module VestalVersions
|
2
|
+
# Enables versioned ActiveRecord::Base instances to revert to a previously saved version.
|
3
|
+
module Reversion
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
# Provides the base instance methods required to revert a versioned instance.
|
7
|
+
module InstanceMethods
|
8
|
+
# Returns the current version number for the versioned object.
|
9
|
+
def version
|
10
|
+
@version ||= last_version
|
11
|
+
end
|
12
|
+
|
13
|
+
# Accepts a value corresponding to a specific version record, builds a history of changes
|
14
|
+
# between that version and the current version, and then iterates over that history updating
|
15
|
+
# the object's attributes until the it's reverted to its prior state.
|
16
|
+
#
|
17
|
+
# The single argument should adhere to one of the formats as documented in the +at+ method of
|
18
|
+
# VestalVersions::Versions.
|
19
|
+
#
|
20
|
+
# After the object is reverted to the target version, it is not saved. In order to save the
|
21
|
+
# object after the reversion, use the +revert_to!+ method.
|
22
|
+
#
|
23
|
+
# The version number of the object will reflect whatever version has been reverted to, and
|
24
|
+
# the return value of the +revert_to+ method is also the target version number.
|
25
|
+
def revert_to(value)
|
26
|
+
to_number = versions.number_at(value)
|
27
|
+
|
28
|
+
changes_between(version, to_number).each do |attribute, change|
|
29
|
+
write_attribute(attribute, change.last)
|
30
|
+
end
|
31
|
+
|
32
|
+
reset_version(to_number)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Behaves similarly to the +revert_to+ method except that it automatically saves the record
|
36
|
+
# after the reversion. The return value is the success of the save.
|
37
|
+
def revert_to!(value)
|
38
|
+
revert_to(value)
|
39
|
+
reset_version if saved = save
|
40
|
+
saved
|
41
|
+
end
|
42
|
+
|
43
|
+
# Returns a boolean specifying whether the object has been reverted to a previous version or
|
44
|
+
# if the object represents the latest version in the version history.
|
45
|
+
def reverted?
|
46
|
+
version != last_version
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
# Mixes in the reverted_from value if it is currently within a revert
|
52
|
+
def version_attributes
|
53
|
+
attributes = super
|
54
|
+
|
55
|
+
if @reverted_from.nil?
|
56
|
+
attributes
|
57
|
+
else
|
58
|
+
attributes.merge(:reverted_from => @reverted_from)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Returns the number of the last created version in the object's version history.
|
63
|
+
#
|
64
|
+
# If no associated versions exist, the object is considered at version 1.
|
65
|
+
def last_version
|
66
|
+
@last_version ||= versions.maximum(:number) || 1
|
67
|
+
end
|
68
|
+
|
69
|
+
# Clears the cached version number instance variables so that they can be recalculated.
|
70
|
+
# Useful after a new version is created.
|
71
|
+
def reset_version(version = nil)
|
72
|
+
if version.nil?
|
73
|
+
@last_version = nil
|
74
|
+
@reverted_from = nil
|
75
|
+
else
|
76
|
+
@reverted_from = version
|
77
|
+
end
|
78
|
+
@version = version
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module VestalVersions
|
2
|
+
# Provides a way for information to be associated with specific versions as to who was
|
3
|
+
# responsible for the associated update to the parent.
|
4
|
+
module Users
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
attr_accessor :updated_by
|
9
|
+
Version.class_eval{ include VersionMethods }
|
10
|
+
end
|
11
|
+
|
12
|
+
# Methods added to versioned ActiveRecord::Base instances to enable versioning with additional
|
13
|
+
# user information.
|
14
|
+
module InstanceMethods
|
15
|
+
|
16
|
+
private
|
17
|
+
# Overrides the +version_attributes+ method to include user information passed into the
|
18
|
+
# parent object, by way of a +updated_by+ attr_accessor.
|
19
|
+
def version_attributes
|
20
|
+
super.merge(:user => updated_by)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Instance methods added to VestalVersions::Version to accomodate incoming user information.
|
25
|
+
module VersionMethods
|
26
|
+
extend ActiveSupport::Concern
|
27
|
+
|
28
|
+
included do
|
29
|
+
belongs_to :user, :polymorphic => true
|
30
|
+
|
31
|
+
alias_method_chain :user, :name
|
32
|
+
alias_method_chain :user=, :name
|
33
|
+
end
|
34
|
+
|
35
|
+
# Overrides the +user+ method created by the polymorphic +belongs_to+ user association. If
|
36
|
+
# the association is absent, defaults to the +user_name+ string column. This allows
|
37
|
+
# VestalVersions::Version#user to either return an ActiveRecord::Base object or a string,
|
38
|
+
# depending on what is sent to the +user_with_name=+ method.
|
39
|
+
def user_with_name
|
40
|
+
user_without_name || user_name
|
41
|
+
end
|
42
|
+
|
43
|
+
# Overrides the +user=+ method created by the polymorphic +belongs_to+ user association.
|
44
|
+
# Based on the class of the object given, either the +user+ association columns or the
|
45
|
+
# +user_name+ string column is populated.
|
46
|
+
def user_with_name=(value)
|
47
|
+
case value
|
48
|
+
when ActiveRecord::Base then self.user_without_name = value
|
49
|
+
else self.user_name = value
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'active_support/configurable'
|
3
|
+
|
4
|
+
module VestalVersions
|
5
|
+
# The ActiveRecord model representing versions.
|
6
|
+
class Version < ActiveRecord::Base
|
7
|
+
include Comparable
|
8
|
+
include ActiveSupport::Configurable
|
9
|
+
|
10
|
+
# Associate polymorphically with the parent record.
|
11
|
+
belongs_to :versioned, :polymorphic => true
|
12
|
+
|
13
|
+
# ActiveRecord::Base#changes is an existing method, so before serializing the +changes+ column,
|
14
|
+
# the existing +changes+ method is undefined. The overridden +changes+ method pertained to
|
15
|
+
# dirty attributes, but will not affect the partial updates functionality as that's based on
|
16
|
+
# an underlying +changed_attributes+ method, not +changes+ itself.
|
17
|
+
undef_method :changes
|
18
|
+
def changes
|
19
|
+
self[:modifications]
|
20
|
+
end
|
21
|
+
serialize :modifications, Hash
|
22
|
+
|
23
|
+
# In conjunction with the included Comparable module, allows comparison of version records
|
24
|
+
# based on their corresponding version numbers, creation timestamps and IDs.
|
25
|
+
def <=>(other)
|
26
|
+
[number, created_at, id].map(&:to_i) <=> [other.number, other.created_at, other.id].map(&:to_i)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Returns whether the version has a version number of 1. Useful when deciding whether to ignore
|
30
|
+
# the version during reversion, as initial versions have no serialized changes attached. Helps
|
31
|
+
# maintain backwards compatibility.
|
32
|
+
def initial?
|
33
|
+
number == 1
|
34
|
+
end
|
35
|
+
|
36
|
+
# Returns the original version number that this version was.
|
37
|
+
def original_number
|
38
|
+
if reverted_from.nil?
|
39
|
+
number
|
40
|
+
else
|
41
|
+
version = versioned.versions.at(reverted_from)
|
42
|
+
version.nil? ? 1 : version.original_number
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def restore!
|
47
|
+
model = restore
|
48
|
+
|
49
|
+
if model
|
50
|
+
model.save!
|
51
|
+
destroy
|
52
|
+
end
|
53
|
+
|
54
|
+
model
|
55
|
+
end
|
56
|
+
|
57
|
+
def restore
|
58
|
+
if tag == 'deleted'
|
59
|
+
attrs = modifications
|
60
|
+
|
61
|
+
class_name = attrs['type'].blank? ? versioned_type : attrs['type']
|
62
|
+
klass = class_name.constantize
|
63
|
+
model = klass.new
|
64
|
+
|
65
|
+
attrs.each do |k, v|
|
66
|
+
begin
|
67
|
+
model.send "#{k}=", v
|
68
|
+
rescue NoMethodError
|
69
|
+
logger.warn "Attribute #{k} does not exist on #{class_name} (Version id: #{id})." rescue nil
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
model
|
74
|
+
else
|
75
|
+
latest_version = self.class.find(:first, :conditions => {:versioned_id => versioned_id, :versioned_type => versioned_type, :tag => 'deleted'})
|
76
|
+
latest_version.nil? ? nil : latest_version.restore
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module VestalVersions
|
2
|
+
# Allows specific versions to be tagged with a custom string. Useful for assigning a more
|
3
|
+
# meaningful value to a version for the purpose of reversion.
|
4
|
+
module VersionTagging
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
# Adds an instance method which allows version tagging through the parent object.
|
8
|
+
module InstanceMethods
|
9
|
+
# Accepts a single string argument which is attached to the version record associated with
|
10
|
+
# the current version number of the parent object.
|
11
|
+
#
|
12
|
+
# Returns the given tag if successful, nil if not. Tags must be unique within the scope of
|
13
|
+
# the parent object. Tag creation will fail if non-unique.
|
14
|
+
#
|
15
|
+
# Version records corresponding to version number 1 are not typically created, but one will
|
16
|
+
# be built to house the given tag if the parent object's current version number is 1.
|
17
|
+
def tag_version(tag)
|
18
|
+
v = versions.at(version) || versions.build(:number => 1)
|
19
|
+
v.tag!(tag)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Instance methods included into VestalVersions::Version to enable version tagging.
|
24
|
+
module VersionMethods
|
25
|
+
extend ActiveSupport::Concern
|
26
|
+
|
27
|
+
included do
|
28
|
+
validates_uniqueness_of :tag, :scope => [:versioned_id, :versioned_type], :if => :validate_tags?
|
29
|
+
end
|
30
|
+
|
31
|
+
# Attaches the given string to the version tag column. If the uniqueness validation fails,
|
32
|
+
# nil is returned. Otherwise, the given string is returned.
|
33
|
+
def tag!(tag)
|
34
|
+
write_attribute(:tag, tag)
|
35
|
+
save ? tag : nil
|
36
|
+
end
|
37
|
+
|
38
|
+
# Simply returns a boolean signifying whether the version instance has a tag value attached.
|
39
|
+
def tagged?
|
40
|
+
!tag.nil?
|
41
|
+
end
|
42
|
+
|
43
|
+
def validate_tags?
|
44
|
+
tagged? && tag != 'deleted'
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
Version.class_eval{ include VersionMethods }
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module VestalVersions
|
2
|
+
# Simply adds a flag to determine whether a model class if versioned.
|
3
|
+
module Versioned
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
# Overrides the +versioned+ method to first define the +versioned?+ class method before
|
7
|
+
# deferring to the original +versioned+.
|
8
|
+
module ClassMethods
|
9
|
+
def versioned(*args)
|
10
|
+
super(*args)
|
11
|
+
|
12
|
+
class << self
|
13
|
+
def versioned?
|
14
|
+
true
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# For all ActiveRecord::Base models that do not call the +versioned+ method, the +versioned?+
|
20
|
+
# method will return false.
|
21
|
+
def versioned?
|
22
|
+
false
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module VestalVersions
|
2
|
+
# An extension module for the +has_many+ association with versions.
|
3
|
+
module Versions
|
4
|
+
# Returns all versions between (and including) the two given arguments. See documentation for
|
5
|
+
# the +at+ extension method for what arguments are valid. If either of the given arguments is
|
6
|
+
# invalid, an empty array is returned.
|
7
|
+
#
|
8
|
+
# The +between+ method preserves returns an array of version records, preserving the order
|
9
|
+
# given by the arguments. If the +from+ value represents a version before that of the +to+
|
10
|
+
# value, the array will be ordered from earliest to latest. The reverse is also true.
|
11
|
+
def between(from, to)
|
12
|
+
from_number, to_number = number_at(from), number_at(to)
|
13
|
+
return [] if from_number.nil? || to_number.nil?
|
14
|
+
|
15
|
+
condition = (from_number == to_number) ? to_number : Range.new(*[from_number, to_number].sort)
|
16
|
+
all(
|
17
|
+
:conditions => {:number => condition},
|
18
|
+
:order => "#{aliased_table_name}.#{connection.quote_column_name('number')} #{(from_number > to_number) ? 'DESC' : 'ASC'}"
|
19
|
+
)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Returns all version records created before the version associated with the given value.
|
23
|
+
def before(value)
|
24
|
+
return [] if (number = number_at(value)).nil?
|
25
|
+
all(:conditions => "#{aliased_table_name}.#{connection.quote_column_name('number')} < #{number}")
|
26
|
+
end
|
27
|
+
|
28
|
+
# Returns all version records created after the version associated with the given value.
|
29
|
+
#
|
30
|
+
# This is useful for dissociating records during use of the +reset_to!+ method.
|
31
|
+
def after(value)
|
32
|
+
return [] if (number = number_at(value)).nil?
|
33
|
+
all(:conditions => "#{aliased_table_name}.#{connection.quote_column_name('number')} > #{number}")
|
34
|
+
end
|
35
|
+
|
36
|
+
# Returns a single version associated with the given value. The following formats are valid:
|
37
|
+
# * A Date or Time object: When given, +to_time+ is called on the value and the last version
|
38
|
+
# record in the history created before (or at) that time is returned.
|
39
|
+
# * A Numeric object: Typically a positive integer, these values correspond to version numbers
|
40
|
+
# and the associated version record is found by a version number equal to the given value
|
41
|
+
# rounded down to the nearest integer.
|
42
|
+
# * A String: A string value represents a version tag and the associated version is searched
|
43
|
+
# for by a matching tag value. *Note:* Be careful with string representations of numbers.
|
44
|
+
# * A Symbol: Symbols represent association class methods on the +has_many+ versions
|
45
|
+
# association. While all of the built-in association methods require arguments, additional
|
46
|
+
# extension modules can be defined using the <tt>:extend</tt> option on the +versioned+
|
47
|
+
# method. See the +versioned+ documentation for more information.
|
48
|
+
# * A Version object: If a version object is passed to the +at+ method, it is simply returned
|
49
|
+
# untouched.
|
50
|
+
def at(value)
|
51
|
+
case value
|
52
|
+
when Date, Time then last(:conditions => ["#{aliased_table_name}.created_at <= ?", value.to_time])
|
53
|
+
when Numeric then find_by_number(value.floor)
|
54
|
+
when String then find_by_tag(value)
|
55
|
+
when Symbol then respond_to?(value) ? send(value) : nil
|
56
|
+
when Version then value
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Returns the version number associated with the given value. In many cases, this involves
|
61
|
+
# simply passing the value to the +at+ method and then returning the subsequent version number.
|
62
|
+
# Hoever, for Numeric values, the version number can be returned directly and for Date/Time
|
63
|
+
# values, a default value of 1 is given to ensure that times prior to the first version
|
64
|
+
# still return a valid version number (useful for reversion).
|
65
|
+
def number_at(value)
|
66
|
+
case value
|
67
|
+
when Date, Time then (v = at(value)) ? v.number : 1
|
68
|
+
when Numeric then value.floor
|
69
|
+
when String, Symbol then (v = at(value)) ? v.number : nil
|
70
|
+
when Version then value.number
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
Bundler.require
|
3
|
+
require 'rspec/core'
|
4
|
+
|
5
|
+
RSpec.configure do |c|
|
6
|
+
c.before(:suite) do
|
7
|
+
CreateSchema.suppress_messages{ CreateSchema.migrate(:up) }
|
8
|
+
end
|
9
|
+
|
10
|
+
c.after(:suite) do
|
11
|
+
FileUtils.rm_rf(File.expand_path('../test.db', __FILE__))
|
12
|
+
end
|
13
|
+
|
14
|
+
c.after(:each) do
|
15
|
+
VestalVersions::Version.config.clear
|
16
|
+
User.prepare_versioned_options({})
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
Dir[File.dirname(__FILE__) + '/support/*.rb'].each { |f| require f }
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class User < ActiveRecord::Base
|
2
|
+
versioned
|
3
|
+
|
4
|
+
def name
|
5
|
+
[first_name, last_name].compact.join(' ')
|
6
|
+
end
|
7
|
+
|
8
|
+
def name= names
|
9
|
+
self[:first_name], self[:last_name] = names.split(' ', 2)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class DeletedUser < ActiveRecord::Base
|
14
|
+
set_table_name 'users'
|
15
|
+
versioned :dependent => :tracking
|
16
|
+
end
|
17
|
+
|
18
|
+
class MyCustomVersion < VestalVersions::Version
|
19
|
+
end
|