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.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +22 -0
  3. data/.travis.yml +22 -0
  4. data/CHANGELOG.md +7 -0
  5. data/Gemfile +10 -0
  6. data/LICENSE +20 -0
  7. data/README.rdoc +206 -0
  8. data/Rakefile +6 -0
  9. data/gemfiles/activerecord_3_0.gemfile +10 -0
  10. data/gemfiles/activerecord_3_1.gemfile +10 -0
  11. data/gemfiles/activerecord_3_2.gemfile +10 -0
  12. data/gemfiles/activerecord_4_0.gemfile +10 -0
  13. data/lib/generators/vestal_versions/migration/migration_generator.rb +17 -0
  14. data/lib/generators/vestal_versions/migration/templates/initializer.rb +9 -0
  15. data/lib/generators/vestal_versions/migration/templates/migration.rb +28 -0
  16. data/lib/generators/vestal_versions.rb +11 -0
  17. data/lib/vestal_versions/changes.rb +121 -0
  18. data/lib/vestal_versions/conditions.rb +57 -0
  19. data/lib/vestal_versions/control.rb +199 -0
  20. data/lib/vestal_versions/creation.rb +93 -0
  21. data/lib/vestal_versions/deletion.rb +37 -0
  22. data/lib/vestal_versions/options.rb +41 -0
  23. data/lib/vestal_versions/reload.rb +16 -0
  24. data/lib/vestal_versions/reset.rb +24 -0
  25. data/lib/vestal_versions/reversion.rb +81 -0
  26. data/lib/vestal_versions/users.rb +54 -0
  27. data/lib/vestal_versions/version.rb +84 -0
  28. data/lib/vestal_versions/version_tagging.rb +51 -0
  29. data/lib/vestal_versions/versioned.rb +27 -0
  30. data/lib/vestal_versions/versions.rb +89 -0
  31. data/lib/vestal_versions.rb +126 -0
  32. data/spec/spec_helper.rb +28 -0
  33. data/spec/support/models.rb +19 -0
  34. data/spec/support/schema.rb +25 -0
  35. data/spec/vestal_versions/changes_spec.rb +134 -0
  36. data/spec/vestal_versions/conditions_spec.rb +103 -0
  37. data/spec/vestal_versions/control_spec.rb +120 -0
  38. data/spec/vestal_versions/creation_spec.rb +90 -0
  39. data/spec/vestal_versions/deletion_spec.rb +86 -0
  40. data/spec/vestal_versions/options_spec.rb +45 -0
  41. data/spec/vestal_versions/reload_spec.rb +18 -0
  42. data/spec/vestal_versions/reset_spec.rb +111 -0
  43. data/spec/vestal_versions/reversion_spec.rb +103 -0
  44. data/spec/vestal_versions/users_spec.rb +21 -0
  45. data/spec/vestal_versions/version_spec.rb +61 -0
  46. data/spec/vestal_versions/version_tagging_spec.rb +39 -0
  47. data/spec/vestal_versions/versioned_spec.rb +16 -0
  48. data/spec/vestal_versions/versions_spec.rb +176 -0
  49. data/vestal_versions.gemspec +23 -0
  50. 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