houston-vestal_versions 2.0.0
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.
- checksums.yaml +7 -0
- data/.gitignore +22 -0
- data/.travis.yml +22 -0
- data/CHANGELOG.md +7 -0
- data/Gemfile +10 -0
- data/LICENSE +20 -0
- data/README.rdoc +206 -0
- data/Rakefile +6 -0
- data/gemfiles/activerecord_3_0.gemfile +10 -0
- data/gemfiles/activerecord_3_1.gemfile +10 -0
- data/gemfiles/activerecord_3_2.gemfile +10 -0
- data/gemfiles/activerecord_4_0.gemfile +10 -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/generators/vestal_versions.rb +11 -0
- data/lib/vestal_versions/changes.rb +121 -0
- data/lib/vestal_versions/conditions.rb +57 -0
- data/lib/vestal_versions/control.rb +199 -0
- data/lib/vestal_versions/creation.rb +93 -0
- data/lib/vestal_versions/deletion.rb +37 -0
- data/lib/vestal_versions/options.rb +41 -0
- data/lib/vestal_versions/reload.rb +16 -0
- data/lib/vestal_versions/reset.rb +24 -0
- data/lib/vestal_versions/reversion.rb +81 -0
- data/lib/vestal_versions/users.rb +54 -0
- data/lib/vestal_versions/version.rb +84 -0
- data/lib/vestal_versions/version_tagging.rb +51 -0
- data/lib/vestal_versions/versioned.rb +27 -0
- data/lib/vestal_versions/versions.rb +89 -0
- data/lib/vestal_versions.rb +126 -0
- data/spec/spec_helper.rb +28 -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
- data/vestal_versions.gemspec +23 -0
- metadata +181 -0
@@ -0,0 +1,199 @@
|
|
1
|
+
module VestalVersions
|
2
|
+
# The control feature allows use of several code blocks that provide finer control over whether
|
3
|
+
# a new version is created, or a previous version is updated.
|
4
|
+
module Control
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
class_attribute :_skip_version, :instance_writer => false
|
9
|
+
end
|
10
|
+
|
11
|
+
|
12
|
+
# Control blocks are called on ActiveRecord::Base instances as to not cause any conflict with
|
13
|
+
# other instances of the versioned class whose behavior could be inadvertently altered within
|
14
|
+
# a control block.
|
15
|
+
|
16
|
+
# The +skip_version+ block simply allows for updates to be made to an instance of a versioned
|
17
|
+
# ActiveRecord model while ignoring all new version creation. The <tt>:if</tt> and
|
18
|
+
# <tt>:unless</tt> conditions (if given) will not be evaulated inside a +skip_version+ block.
|
19
|
+
#
|
20
|
+
# When the block closes, the instance is automatically saved, so explicitly saving the
|
21
|
+
# object within the block is unnecessary.
|
22
|
+
#
|
23
|
+
# == Example
|
24
|
+
#
|
25
|
+
# user = User.find_by_first_name("Steve")
|
26
|
+
# user.version # => 1
|
27
|
+
# user.skip_version do
|
28
|
+
# user.first_name = "Stephen"
|
29
|
+
# end
|
30
|
+
# user.version # => 1
|
31
|
+
def skip_version
|
32
|
+
_with_version_flag(:_skip_version) do
|
33
|
+
yield if block_given?
|
34
|
+
save
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Behaving almost identically to the +skip_version+ block, the only difference with the
|
39
|
+
# +skip_version!+ block is that the save automatically performed at the close of the block
|
40
|
+
# is a +save!+, meaning that an exception will be raised if the object cannot be saved.
|
41
|
+
def skip_version!
|
42
|
+
_with_version_flag(:_skip_version) do
|
43
|
+
yield if block_given?
|
44
|
+
save!
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Merging versions with the +merge_version+ block will take all of the versions that would
|
49
|
+
# be created within the block and merge them into one version and pushing that single version
|
50
|
+
# onto the ActiveRecord::Base instance's version history. A new version will be created and
|
51
|
+
# the instance's version number will be incremented.
|
52
|
+
#
|
53
|
+
# == Example
|
54
|
+
#
|
55
|
+
# user = User.find_by_first_name("Steve")
|
56
|
+
# user.version # => 1
|
57
|
+
# user.merge_version do
|
58
|
+
# user.update_attributes(:first_name => "Steven", :last_name => "Tyler")
|
59
|
+
# user.update_attribute(:first_name, "Stephen")
|
60
|
+
# user.update_attribute(:last_name, "Richert")
|
61
|
+
# end
|
62
|
+
# user.version # => 2
|
63
|
+
# user.versions.last.changes
|
64
|
+
# # => {"first_name" => ["Steve", "Stephen"], "last_name" => ["Jobs", "Richert"]}
|
65
|
+
#
|
66
|
+
# See VestalVersions::Changes for an explanation on how changes are appended.
|
67
|
+
def merge_version
|
68
|
+
_with_version_flag(:merge_version) do
|
69
|
+
yield if block_given?
|
70
|
+
end
|
71
|
+
save
|
72
|
+
end
|
73
|
+
|
74
|
+
# Behaving almost identically to the +merge_version+ block, the only difference with the
|
75
|
+
# +merge_version!+ block is that the save automatically performed at the close of the block
|
76
|
+
# is a +save!+, meaning that an exception will be raised if the object cannot be saved.
|
77
|
+
def merge_version!
|
78
|
+
_with_version_flag(:merge_version) do
|
79
|
+
yield if block_given?
|
80
|
+
end
|
81
|
+
save!
|
82
|
+
end
|
83
|
+
|
84
|
+
# A convenience method for determining whether a versioned instance is set to merge its next
|
85
|
+
# versions into one before version creation.
|
86
|
+
def merge_version?
|
87
|
+
!!@merge_version
|
88
|
+
end
|
89
|
+
|
90
|
+
# Appending versions with the +append_version+ block acts similarly to the +merge_version+
|
91
|
+
# block in that all would-be version creations within the block are defered until the block
|
92
|
+
# closes. The major difference is that with +append_version+, a new version is not created.
|
93
|
+
# Rather, the cumulative changes are appended to the serialized changes of the instance's
|
94
|
+
# last version. A new version is not created, so the version number is not incremented.
|
95
|
+
#
|
96
|
+
# == Example
|
97
|
+
#
|
98
|
+
# user = User.find_by_first_name("Steve")
|
99
|
+
# user.version # => 2
|
100
|
+
# user.versions.last.changes
|
101
|
+
# # => {"first_name" => ["Stephen", "Steve"]}
|
102
|
+
# user.append_version do
|
103
|
+
# user.last_name = "Jobs"
|
104
|
+
# end
|
105
|
+
# user.versions.last.changes
|
106
|
+
# # => {"first_name" => ["Stephen", "Steve"], "last_name" => ["Richert", "Jobs"]}
|
107
|
+
# user.version # => 2
|
108
|
+
#
|
109
|
+
# See VestalVersions::Changes for an explanation on how changes are appended.
|
110
|
+
def append_version
|
111
|
+
_with_version_flag(:merge_version) do
|
112
|
+
yield if block_given?
|
113
|
+
end
|
114
|
+
|
115
|
+
_with_version_flag(:append_version) do
|
116
|
+
save
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# Behaving almost identically to the +append_version+ block, the only difference with the
|
121
|
+
# +append_version!+ block is that the save automatically performed at the close of the block
|
122
|
+
# is a +save!+, meaning that an exception will be raised if the object cannot be saved.
|
123
|
+
def append_version!
|
124
|
+
_with_version_flag(:merge_version) do
|
125
|
+
yield if block_given?
|
126
|
+
end
|
127
|
+
|
128
|
+
_with_version_flag(:append_version) do
|
129
|
+
save!
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
# A convenience method for determining whether a versioned instance is set to append its next
|
134
|
+
# version's changes into the last version changes.
|
135
|
+
def append_version?
|
136
|
+
!!@append_version
|
137
|
+
end
|
138
|
+
|
139
|
+
# Used for each control block, the +_with_version_flag+ method sets a given variable to
|
140
|
+
# true and then executes the given block, ensuring that the variable is returned to a nil
|
141
|
+
# value before returning. This is useful to be certain that one of the control flag
|
142
|
+
# instance variables isn't inadvertently left in the "on" position by execution within the
|
143
|
+
# block raising an exception.
|
144
|
+
def _with_version_flag(flag)
|
145
|
+
instance_variable_set("@#{flag}", true)
|
146
|
+
yield
|
147
|
+
ensure
|
148
|
+
remove_instance_variable("@#{flag}")
|
149
|
+
end
|
150
|
+
|
151
|
+
# Overrides the basal +create_version?+ method to make sure that new versions are not
|
152
|
+
# created when inside any of the control blocks (until the block terminates).
|
153
|
+
def create_version?
|
154
|
+
!_skip_version? && !merge_version? && !append_version? && super
|
155
|
+
end
|
156
|
+
|
157
|
+
# Overrides the basal +update_version?+ method to allow the last version of an versioned
|
158
|
+
# ActiveRecord::Base instance to be updated at the end of an +append_version+ block.
|
159
|
+
def update_version?
|
160
|
+
append_version?
|
161
|
+
end
|
162
|
+
|
163
|
+
module ClassMethods
|
164
|
+
# The +skip_version+ block simply allows for updates to be made to an instance of a versioned
|
165
|
+
# ActiveRecord model while ignoring all new version creation. The <tt>:if</tt> and
|
166
|
+
# <tt>:unless</tt> conditions (if given) will not be evaulated inside a +skip_version+ block.
|
167
|
+
#
|
168
|
+
# When the block closes, the instance is automatically saved, so explicitly saving the
|
169
|
+
# object within the block is unnecessary.
|
170
|
+
#
|
171
|
+
# == Example
|
172
|
+
#
|
173
|
+
# user = User.find_by_first_name("Steve")
|
174
|
+
# user.version # => 1
|
175
|
+
# user.skip_version do
|
176
|
+
# user.first_name = "Stephen"
|
177
|
+
# end
|
178
|
+
# user.version # => 1
|
179
|
+
def skip_version
|
180
|
+
_with_version_flag(:_skip_version) do
|
181
|
+
yield if block_given?
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
# Used for each control block, the +with_version_flag+ method sets a given variable to
|
186
|
+
# true and then executes the given block, ensuring that the variable is returned to a nil
|
187
|
+
# value before returning. This is useful to be certain that one of the control flag
|
188
|
+
# instance variables isn't inadvertently left in the "on" position by execution within the
|
189
|
+
# block raising an exception.
|
190
|
+
def _with_version_flag(flag)
|
191
|
+
self.send("#{flag}=", true)
|
192
|
+
yield
|
193
|
+
ensure
|
194
|
+
self.send("#{flag}=", nil)
|
195
|
+
end
|
196
|
+
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
module VestalVersions
|
2
|
+
# Adds the functionality necessary to control version creation on a versioned instance of
|
3
|
+
# ActiveRecord::Base.
|
4
|
+
module Creation
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
after_create :create_initial_version, :if => :create_initial_version?
|
9
|
+
after_update :create_version, :if => :create_version?
|
10
|
+
after_update :update_version, :if => :update_version?
|
11
|
+
end
|
12
|
+
|
13
|
+
# Class methods added to ActiveRecord::Base to facilitate the creation of new versions.
|
14
|
+
module ClassMethods
|
15
|
+
# Overrides the basal +prepare_versioned_options+ method defined in VestalVersions::Options
|
16
|
+
# to extract the <tt>:only</tt>, <tt>:except</tt> and <tt>:initial_version</tt> options
|
17
|
+
# into +vestal_versions_options+.
|
18
|
+
def prepare_versioned_options(options)
|
19
|
+
result = super(options)
|
20
|
+
|
21
|
+
self.vestal_versions_options[:only] = Array(options.delete(:only)).map(&:to_s).uniq if options[:only]
|
22
|
+
self.vestal_versions_options[:except] = Array(options.delete(:except)).map(&:to_s).uniq if options[:except]
|
23
|
+
self.vestal_versions_options[:initial_version] = options.delete(:initial_version)
|
24
|
+
|
25
|
+
result
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Instance methods that determine whether to save a version and actually perform the save.
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
# Returns whether an initial version should be created upon creation of the parent record.
|
34
|
+
def create_initial_version?
|
35
|
+
vestal_versions_options[:initial_version] == true
|
36
|
+
end
|
37
|
+
|
38
|
+
# Creates an initial version upon creation of the parent record.
|
39
|
+
def create_initial_version
|
40
|
+
versions.create(version_attributes.merge(:number => 1))
|
41
|
+
reset_version_changes
|
42
|
+
reset_version
|
43
|
+
end
|
44
|
+
|
45
|
+
# Returns whether a new version should be created upon updating the parent record.
|
46
|
+
def create_version?
|
47
|
+
!version_changes.blank?
|
48
|
+
end
|
49
|
+
|
50
|
+
# Creates a new version upon updating the parent record.
|
51
|
+
def create_version(attributes = nil)
|
52
|
+
versions.create(attributes || version_attributes)
|
53
|
+
reset_version_changes
|
54
|
+
reset_version
|
55
|
+
end
|
56
|
+
|
57
|
+
# Returns whether the last version should be updated upon updating the parent record.
|
58
|
+
# This method is overridden in VestalVersions::Control to account for a control block that
|
59
|
+
# merges changes onto the previous version.
|
60
|
+
def update_version?
|
61
|
+
false
|
62
|
+
end
|
63
|
+
|
64
|
+
# Updates the last version's changes by appending the current version changes.
|
65
|
+
def update_version
|
66
|
+
return create_version unless v = versions.last
|
67
|
+
v.modifications_will_change!
|
68
|
+
v.update_attribute(:modifications, v.changes.append_changes(version_changes))
|
69
|
+
reset_version_changes
|
70
|
+
reset_version
|
71
|
+
end
|
72
|
+
|
73
|
+
# Returns an array of column names that should be included in the changes of created
|
74
|
+
# versions. If <tt>vestal_versions_options[:only]</tt> is specified, only those columns
|
75
|
+
# will be versioned. Otherwise, if <tt>vestal_versions_options[:except]</tt> is specified,
|
76
|
+
# all columns will be versioned other than those specified. Without either option, the
|
77
|
+
# default is to version all columns. At any rate, the four "automagic" timestamp columns
|
78
|
+
# maintained by Rails are never versioned.
|
79
|
+
def versioned_columns
|
80
|
+
case
|
81
|
+
when vestal_versions_options[:only] then self.class.column_names & vestal_versions_options[:only]
|
82
|
+
when vestal_versions_options[:except] then self.class.column_names - vestal_versions_options[:except]
|
83
|
+
else self.class.column_names
|
84
|
+
end - %w(created_at created_on updated_at updated_on)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Specifies the attributes used during version creation. This is separated into its own
|
88
|
+
# method so that it can be overridden by the VestalVersions::Users feature.
|
89
|
+
def version_attributes
|
90
|
+
{:modifications => version_changes, :number => last_version + 1}
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module VestalVersions
|
2
|
+
# Allows version creation to occur conditionally based on given <tt>:if</tt> and/or
|
3
|
+
# <tt>:unless</tt> options.
|
4
|
+
module Deletion
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
before_destroy :create_destroyed_version, :if => :delete_version?
|
9
|
+
end
|
10
|
+
|
11
|
+
# Class methods on ActiveRecord::Base
|
12
|
+
module ClassMethods
|
13
|
+
# After the original +prepare_versioned_options+ method cleans the given options, this alias
|
14
|
+
# also extracts the <tt>:depedent</tt> if it set to <tt>:tracking</tt>
|
15
|
+
def prepare_versioned_options(options)
|
16
|
+
result = super(options)
|
17
|
+
if result[:dependent] == :tracking
|
18
|
+
self.vestal_versions_options[:track_destroy] = true
|
19
|
+
options.delete(:dependent)
|
20
|
+
end
|
21
|
+
|
22
|
+
result
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def delete_version?
|
29
|
+
vestal_versions_options[:track_destroy]
|
30
|
+
end
|
31
|
+
|
32
|
+
def create_destroyed_version
|
33
|
+
create_version({:modifications => attributes, :number => last_version + 1, :tag => 'deleted'})
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module VestalVersions
|
2
|
+
# Provides +versioned+ options conversion and cleanup.
|
3
|
+
module Options
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
# Class methods that provide preparation of options passed to the +versioned+ method.
|
7
|
+
module ClassMethods
|
8
|
+
# The +prepare_versioned_options+ method has three purposes:
|
9
|
+
# 1. Populate the provided options with default values where needed
|
10
|
+
# 2. Prepare options for use with the +has_many+ association
|
11
|
+
# 3. Save user-configurable options in a class-level variable
|
12
|
+
#
|
13
|
+
# Options are given priority in the following order:
|
14
|
+
# 1. Those passed directly to the +versioned+ method
|
15
|
+
# 2. Those specified in an initializer +configure+ block
|
16
|
+
# 3. Default values specified in +prepare_versioned_options+
|
17
|
+
#
|
18
|
+
# The method is overridden in feature modules that require specific options outside the
|
19
|
+
# standard +has_many+ associations.
|
20
|
+
def prepare_versioned_options(options)
|
21
|
+
options.symbolize_keys!
|
22
|
+
options.reverse_merge!(VestalVersions.config)
|
23
|
+
options.reverse_merge!(
|
24
|
+
:class_name => 'VestalVersions::Version',
|
25
|
+
:dependent => :delete_all
|
26
|
+
)
|
27
|
+
# options.reverse_merge!(
|
28
|
+
# :order => "#{options[:class_name].constantize.table_name}.#{connection.quote_column_name('number')} ASC"
|
29
|
+
# )
|
30
|
+
|
31
|
+
class_attribute :vestal_versions_options
|
32
|
+
self.vestal_versions_options = options.dup
|
33
|
+
|
34
|
+
options.merge!(
|
35
|
+
:as => :versioned,
|
36
|
+
:extend => Array(options[:extend]).unshift(Versions)
|
37
|
+
)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module VestalVersions
|
2
|
+
# Ties into the existing ActiveRecord::Base#reload method to ensure that version information
|
3
|
+
# is properly reset.
|
4
|
+
module Reload
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
# Adds instance methods into ActiveRecord::Base to tap into the +reload+ method.
|
8
|
+
|
9
|
+
# Overrides ActiveRecord::Base#reload, resetting the instance-variable-cached version number
|
10
|
+
# before performing the original +reload+ method.
|
11
|
+
def reload(*args)
|
12
|
+
reset_version
|
13
|
+
super
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -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
|
+
|
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, versions.after(value))
|
18
|
+
reset_version
|
19
|
+
end
|
20
|
+
saved
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,81 @@
|
|
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
|
+
|
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
|
@@ -0,0 +1,54 @@
|
|
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 UserVersionMethods }
|
10
|
+
end
|
11
|
+
|
12
|
+
# Methods added to versioned ActiveRecord::Base instances to enable versioning with additional
|
13
|
+
# user information.
|
14
|
+
|
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 UserVersionMethods
|
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
|
+
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,84 @@
|
|
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
|
+
if ActiveRecord::VERSION::MAJOR == 3
|
14
|
+
attr_accessible :modifications, :number, :user, :tag, :reverted_from
|
15
|
+
end
|
16
|
+
|
17
|
+
# ActiveRecord::Base#changes is an existing method, so before serializing the +changes+ column,
|
18
|
+
# the existing +changes+ method is undefined. The overridden +changes+ method pertained to
|
19
|
+
# dirty attributes, but will not affect the partial updates functionality as that's based on
|
20
|
+
# an underlying +changed_attributes+ method, not +changes+ itself.
|
21
|
+
undef_method :changes
|
22
|
+
def changes
|
23
|
+
self[:modifications]
|
24
|
+
end
|
25
|
+
serialize :modifications, Hash
|
26
|
+
|
27
|
+
# In conjunction with the included Comparable module, allows comparison of version records
|
28
|
+
# based on their corresponding version numbers, creation timestamps and IDs.
|
29
|
+
def <=>(other)
|
30
|
+
[number, created_at, id].map(&:to_i) <=> [other.number, other.created_at, other.id].map(&:to_i)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Returns whether the version has a version number of 1. Useful when deciding whether to ignore
|
34
|
+
# the version during reversion, as initial versions have no serialized changes attached. Helps
|
35
|
+
# maintain backwards compatibility.
|
36
|
+
def initial?
|
37
|
+
number == 1
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns the original version number that this version was.
|
41
|
+
def original_number
|
42
|
+
if reverted_from.nil?
|
43
|
+
number
|
44
|
+
else
|
45
|
+
version = versioned.versions.at(reverted_from)
|
46
|
+
version.nil? ? 1 : version.original_number
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def restore!
|
51
|
+
model = restore
|
52
|
+
|
53
|
+
if model
|
54
|
+
model.save!
|
55
|
+
destroy
|
56
|
+
end
|
57
|
+
|
58
|
+
model
|
59
|
+
end
|
60
|
+
|
61
|
+
def restore
|
62
|
+
if tag == 'deleted'
|
63
|
+
attrs = modifications
|
64
|
+
|
65
|
+
class_name = attrs['type'].blank? ? versioned_type : attrs['type']
|
66
|
+
klass = class_name.constantize
|
67
|
+
model = klass.new
|
68
|
+
|
69
|
+
attrs.each do |k, v|
|
70
|
+
begin
|
71
|
+
model.send "#{k}=", v
|
72
|
+
rescue NoMethodError
|
73
|
+
logger.warn "Attribute #{k} does not exist on #{class_name} (Version id: #{id})." rescue nil
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
model
|
78
|
+
else
|
79
|
+
latest_version = self.class.where(:versioned_id => versioned_id, :versioned_type => versioned_type, :tag => 'deleted').first
|
80
|
+
latest_version.nil? ? nil : latest_version.restore
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,51 @@
|
|
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
|
+
|
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
|
+
t = v.tag!(tag)
|
20
|
+
versions.reload
|
21
|
+
t
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Instance methods included into VestalVersions::Version to enable version tagging.
|
26
|
+
module TaggingVersionMethods
|
27
|
+
extend ActiveSupport::Concern
|
28
|
+
|
29
|
+
included do
|
30
|
+
validates_uniqueness_of :tag, :scope => [:versioned_id, :versioned_type], :if => :validate_tags?
|
31
|
+
end
|
32
|
+
|
33
|
+
# Attaches the given string to the version tag column. If the uniqueness validation fails,
|
34
|
+
# nil is returned. Otherwise, the given string is returned.
|
35
|
+
def tag!(tag)
|
36
|
+
write_attribute(:tag, tag)
|
37
|
+
save ? tag : nil
|
38
|
+
end
|
39
|
+
|
40
|
+
# Simply returns a boolean signifying whether the version instance has a tag value attached.
|
41
|
+
def tagged?
|
42
|
+
!tag.nil?
|
43
|
+
end
|
44
|
+
|
45
|
+
def validate_tags?
|
46
|
+
tagged? && tag != 'deleted'
|
47
|
+
end
|
48
|
+
|
49
|
+
Version.class_eval{ include TaggingVersionMethods }
|
50
|
+
end
|
51
|
+
end
|