geothird_vestal_versions 1.2.3

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 (41) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +20 -0
  3. data/README.rdoc +196 -0
  4. data/lib/generators/vestal_versions.rb +11 -0
  5. data/lib/generators/vestal_versions/migration/migration_generator.rb +17 -0
  6. data/lib/generators/vestal_versions/migration/templates/initializer.rb +9 -0
  7. data/lib/generators/vestal_versions/migration/templates/migration.rb +28 -0
  8. data/lib/vestal_versions.rb +126 -0
  9. data/lib/vestal_versions/changes.rb +120 -0
  10. data/lib/vestal_versions/conditions.rb +55 -0
  11. data/lib/vestal_versions/control.rb +197 -0
  12. data/lib/vestal_versions/creation.rb +91 -0
  13. data/lib/vestal_versions/deletion.rb +37 -0
  14. data/lib/vestal_versions/options.rb +41 -0
  15. data/lib/vestal_versions/reload.rb +15 -0
  16. data/lib/vestal_versions/reset.rb +22 -0
  17. data/lib/vestal_versions/reversion.rb +80 -0
  18. data/lib/vestal_versions/users.rb +53 -0
  19. data/lib/vestal_versions/version.rb +81 -0
  20. data/lib/vestal_versions/version_num.rb +3 -0
  21. data/lib/vestal_versions/version_tagging.rb +48 -0
  22. data/lib/vestal_versions/versioned.rb +27 -0
  23. data/lib/vestal_versions/versions.rb +74 -0
  24. data/spec/spec_helper.rb +20 -0
  25. data/spec/support/models.rb +19 -0
  26. data/spec/support/schema.rb +25 -0
  27. data/spec/vestal_versions/changes_spec.rb +134 -0
  28. data/spec/vestal_versions/conditions_spec.rb +103 -0
  29. data/spec/vestal_versions/control_spec.rb +120 -0
  30. data/spec/vestal_versions/creation_spec.rb +90 -0
  31. data/spec/vestal_versions/deletion_spec.rb +86 -0
  32. data/spec/vestal_versions/options_spec.rb +45 -0
  33. data/spec/vestal_versions/reload_spec.rb +18 -0
  34. data/spec/vestal_versions/reset_spec.rb +111 -0
  35. data/spec/vestal_versions/reversion_spec.rb +103 -0
  36. data/spec/vestal_versions/users_spec.rb +21 -0
  37. data/spec/vestal_versions/version_spec.rb +61 -0
  38. data/spec/vestal_versions/version_tagging_spec.rb +39 -0
  39. data/spec/vestal_versions/versioned_spec.rb +16 -0
  40. data/spec/vestal_versions/versions_spec.rb +176 -0
  41. metadata +169 -0
@@ -0,0 +1,55 @@
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 Conditions
5
+ extend ActiveSupport::Concern
6
+
7
+ # Class methods on ActiveRecord::Base to prepare the <tt>:if</tt> and <tt>:unless</tt> options.
8
+ module ClassMethods
9
+ # After the original +prepare_versioned_options+ method cleans the given options, this alias
10
+ # also extracts the <tt>:if</tt> and <tt>:unless</tt> options, chaning them into arrays
11
+ # and converting any symbols to procs. Procs are called with the ActiveRecord model instance
12
+ # as the sole argument.
13
+ #
14
+ # If all of the <tt>:if</tt> conditions are met and none of the <tt>:unless</tt> conditions
15
+ # are unmet, than version creation will proceed, assuming all other conditions are also met.
16
+ def prepare_versioned_options(options)
17
+ result = super(options)
18
+
19
+ vestal_versions_options[:if] = Array(options.delete(:if)).map(&:to_proc)
20
+ vestal_versions_options[:unless] = Array(options.delete(:unless)).map(&:to_proc)
21
+
22
+ result
23
+ end
24
+ end
25
+
26
+ # Instance methods that determine based on the <tt>:if</tt> and <tt>:unless</tt> conditions,
27
+ # whether a version is to be create or updated.
28
+ private
29
+ # After first determining whether the <tt>:if</tt> and <tt>:unless</tt> conditions are
30
+ # satisfied, the original, unaliased +create_version?+ method is called to determine
31
+ # whether a new version should be created upon update of the ActiveRecord::Base instance.
32
+ def create_version?
33
+ version_conditions_met? && super
34
+ end
35
+
36
+ # After first determining whether the <tt>:if</tt> and <tt>:unless</tt> conditions are
37
+ # satisfied, the original, unaliased +update_version?+ method is called to determine
38
+ # whther the last version should be updated to include changes merged from the current
39
+ # ActiveRecord::Base instance update.
40
+ #
41
+ # The overridden +update_version?+ method simply returns false, effectively delegating
42
+ # the decision to whether the <tt>:if</tt> and <tt>:unless</tt> conditions are met.
43
+ def update_version?
44
+ version_conditions_met? && super
45
+ end
46
+
47
+ # Simply checks whether the <tt>:if</tt> and <tt>:unless</tt> conditions given in the
48
+ # +versioned+ options are met: meaning that all procs in the <tt>:if</tt> array must
49
+ # evaluate to a non-false, non-nil value and that all procs in the <tt>:unless</tt> array
50
+ # must all evaluate to either false or nil.
51
+ def version_conditions_met?
52
+ vestal_versions_options[:if].all?{|p| p.call(self) } && !vestal_versions_options[:unless].any?{|p| p.call(self) }
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,197 @@
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
+ # Control blocks are called on ActiveRecord::Base instances as to not cause any conflict with
12
+ # other instances of the versioned class whose behavior could be inadvertently altered within
13
+ # a control block.
14
+ # The +skip_version+ block simply allows for updates to be made to an instance of a versioned
15
+ # ActiveRecord model while ignoring all new version creation. The <tt>:if</tt> and
16
+ # <tt>:unless</tt> conditions (if given) will not be evaulated inside a +skip_version+ block.
17
+ #
18
+ # When the block closes, the instance is automatically saved, so explicitly saving the
19
+ # object within the block is unnecessary.
20
+ #
21
+ # == Example
22
+ #
23
+ # user = User.find_by_first_name("Steve")
24
+ # user.version # => 1
25
+ # user.skip_version do
26
+ # user.first_name = "Stephen"
27
+ # end
28
+ # user.version # => 1
29
+ def skip_version
30
+ _with_version_flag(:_skip_version) do
31
+ yield if block_given?
32
+ save
33
+ end
34
+ end
35
+
36
+ # Behaving almost identically to the +skip_version+ block, the only difference with the
37
+ # +skip_version!+ block is that the save automatically performed at the close of the block
38
+ # is a +save!+, meaning that an exception will be raised if the object cannot be saved.
39
+ def skip_version!
40
+ _with_version_flag(:_skip_version) do
41
+ yield if block_given?
42
+ save!
43
+ end
44
+ end
45
+
46
+ # Merging versions with the +merge_version+ block will take all of the versions that would
47
+ # be created within the block and merge them into one version and pushing that single version
48
+ # onto the ActiveRecord::Base instance's version history. A new version will be created and
49
+ # the instance's version number will be incremented.
50
+ #
51
+ # == Example
52
+ #
53
+ # user = User.find_by_first_name("Steve")
54
+ # user.version # => 1
55
+ # user.merge_version do
56
+ # user.update_attributes(:first_name => "Steven", :last_name => "Tyler")
57
+ # user.update_attribute(:first_name, "Stephen")
58
+ # user.update_attribute(:last_name, "Richert")
59
+ # end
60
+ # user.version # => 2
61
+ # user.versions.last.changes
62
+ # # => {"first_name" => ["Steve", "Stephen"], "last_name" => ["Jobs", "Richert"]}
63
+ #
64
+ # See VestalVersions::Changes for an explanation on how changes are appended.
65
+ def merge_version
66
+ _with_version_flag(:merge_version) do
67
+ yield if block_given?
68
+ end
69
+ save
70
+ end
71
+
72
+ # Behaving almost identically to the +merge_version+ block, the only difference with the
73
+ # +merge_version!+ block is that the save automatically performed at the close of the block
74
+ # is a +save!+, meaning that an exception will be raised if the object cannot be saved.
75
+ def merge_version!
76
+ _with_version_flag(:merge_version) do
77
+ yield if block_given?
78
+ end
79
+ save!
80
+ end
81
+
82
+ # A convenience method for determining whether a versioned instance is set to merge its next
83
+ # versions into one before version creation.
84
+ def merge_version?
85
+ !!@merge_version
86
+ end
87
+
88
+ # Appending versions with the +append_version+ block acts similarly to the +merge_version+
89
+ # block in that all would-be version creations within the block are defered until the block
90
+ # closes. The major difference is that with +append_version+, a new version is not created.
91
+ # Rather, the cumulative changes are appended to the serialized changes of the instance's
92
+ # last version. A new version is not created, so the version number is not incremented.
93
+ #
94
+ # == Example
95
+ #
96
+ # user = User.find_by_first_name("Steve")
97
+ # user.version # => 2
98
+ # user.versions.last.changes
99
+ # # => {"first_name" => ["Stephen", "Steve"]}
100
+ # user.append_version do
101
+ # user.last_name = "Jobs"
102
+ # end
103
+ # user.versions.last.changes
104
+ # # => {"first_name" => ["Stephen", "Steve"], "last_name" => ["Richert", "Jobs"]}
105
+ # user.version # => 2
106
+ #
107
+ # See VestalVersions::Changes for an explanation on how changes are appended.
108
+ def append_version
109
+ _with_version_flag(:merge_version) do
110
+ yield if block_given?
111
+ end
112
+
113
+ _with_version_flag(:append_version) do
114
+ save
115
+ end
116
+ end
117
+
118
+ # Behaving almost identically to the +append_version+ block, the only difference with the
119
+ # +append_version!+ block is that the save automatically performed at the close of the block
120
+ # is a +save!+, meaning that an exception will be raised if the object cannot be saved.
121
+ def append_version!
122
+ _with_version_flag(:merge_version) do
123
+ yield if block_given?
124
+ end
125
+
126
+ _with_version_flag(:append_version) do
127
+ save!
128
+ end
129
+ end
130
+
131
+ # A convenience method for determining whether a versioned instance is set to append its next
132
+ # version's changes into the last version changes.
133
+ def append_version?
134
+ !!@append_version
135
+ end
136
+
137
+ # Used for each control block, the +_with_version_flag+ method sets a given variable to
138
+ # true and then executes the given block, ensuring that the variable is returned to a nil
139
+ # value before returning. This is useful to be certain that one of the control flag
140
+ # instance variables isn't inadvertently left in the "on" position by execution within the
141
+ # block raising an exception.
142
+ def _with_version_flag(flag)
143
+ instance_variable_set("@#{flag}", true)
144
+ yield
145
+ ensure
146
+ remove_instance_variable("@#{flag}")
147
+ end
148
+
149
+ # Overrides the basal +create_version?+ method to make sure that new versions are not
150
+ # created when inside any of the control blocks (until the block terminates).
151
+ def create_version?
152
+ !_skip_version? && !merge_version? && !append_version? && super
153
+ end
154
+
155
+ # Overrides the basal +update_version?+ method to allow the last version of an versioned
156
+ # ActiveRecord::Base instance to be updated at the end of an +append_version+ block.
157
+ def update_version?
158
+ append_version?
159
+ end
160
+
161
+ end
162
+ module ClassMethods
163
+ # The +skip_version+ block simply allows for updates to be made to an instance of a versioned
164
+ # ActiveRecord model while ignoring all new version creation. The <tt>:if</tt> and
165
+ # <tt>:unless</tt> conditions (if given) will not be evaulated inside a +skip_version+ block.
166
+ #
167
+ # When the block closes, the instance is automatically saved, so explicitly saving the
168
+ # object within the block is unnecessary.
169
+ #
170
+ # == Example
171
+ #
172
+ # user = User.find_by_first_name("Steve")
173
+ # user.version # => 1
174
+ # user.skip_version do
175
+ # user.first_name = "Stephen"
176
+ # end
177
+ # user.version # => 1
178
+ def skip_version
179
+ _with_version_flag(:_skip_version) do
180
+ yield if block_given?
181
+ end
182
+ end
183
+
184
+ # Used for each control block, the +with_version_flag+ method sets a given variable to
185
+ # true and then executes the given block, ensuring that the variable is returned to a nil
186
+ # value before returning. This is useful to be certain that one of the control flag
187
+ # instance variables isn't inadvertently left in the "on" position by execution within the
188
+ # block raising an exception.
189
+ def _with_version_flag(flag)
190
+ self.send("#{flag}=", true)
191
+ yield
192
+ ensure
193
+ self.send("#{flag}=", nil)
194
+ end
195
+
196
+ end
197
+ end
@@ -0,0 +1,91 @@
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
+ private
31
+ # Returns whether an initial version should be created upon creation of the parent record.
32
+ def create_initial_version?
33
+ vestal_versions_options[:initial_version] == true
34
+ end
35
+
36
+ # Creates an initial version upon creation of the parent record.
37
+ def create_initial_version
38
+ versions.create(version_attributes.merge(:number => 1))
39
+ reset_version_changes
40
+ reset_version
41
+ end
42
+
43
+ # Returns whether a new version should be created upon updating the parent record.
44
+ def create_version?
45
+ !version_changes.blank?
46
+ end
47
+
48
+ # Creates a new version upon updating the parent record.
49
+ def create_version(attributes = nil)
50
+ versions.create(attributes || version_attributes)
51
+ reset_version_changes
52
+ reset_version
53
+ end
54
+
55
+ # Returns whether the last version should be updated upon updating the parent record.
56
+ # This method is overridden in VestalVersions::Control to account for a control block that
57
+ # merges changes onto the previous version.
58
+ def update_version?
59
+ false
60
+ end
61
+
62
+ # Updates the last version's changes by appending the current version changes.
63
+ def update_version
64
+ return create_version unless v = versions.last
65
+ v.modifications_will_change!
66
+ v.update_attribute(:modifications, v.changes.append_changes(version_changes))
67
+ reset_version_changes
68
+ reset_version
69
+ end
70
+
71
+ # Returns an array of column names that should be included in the changes of created
72
+ # versions. If <tt>vestal_versions_options[:only]</tt> is specified, only those columns
73
+ # will be versioned. Otherwise, if <tt>vestal_versions_options[:except]</tt> is specified,
74
+ # all columns will be versioned other than those specified. Without either option, the
75
+ # default is to version all columns. At any rate, the four "automagic" timestamp columns
76
+ # maintained by Rails are never versioned.
77
+ def versioned_columns
78
+ case
79
+ when vestal_versions_options[:only] then self.class.column_names & vestal_versions_options[:only]
80
+ when vestal_versions_options[:except] then self.class.column_names - vestal_versions_options[:except]
81
+ else self.class.column_names
82
+ end - %w(created_at created_on updated_at updated_on)
83
+ end
84
+
85
+ # Specifies the attributes used during version creation. This is separated into its own
86
+ # method so that it can be overridden by the VestalVersions::Users feature.
87
+ def version_attributes
88
+ {:modifications => version_changes, :number => last_version + 1}
89
+ end
90
+ end
91
+ 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,15 @@
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
+ # Overrides ActiveRecord::Base#reload, resetting the instance-variable-cached version number
9
+ # before performing the original +reload+ method.
10
+ def reload(*args)
11
+ reset_version
12
+ super
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,22 @@
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
+ # Similar to +revert_to!+, the +reset_to!+ method reverts an object to a previous version,
8
+ # only instead of creating a new record in the version history, +reset_to!+ deletes all of
9
+ # the version history that occurs after the version reverted to.
10
+ #
11
+ # The action taken on each version record after the point of reversion is determined by the
12
+ # <tt>:dependent</tt> option given to the +versioned+ method. See the +versioned+ method
13
+ # documentation for more details.
14
+ def reset_to!(value)
15
+ if saved = skip_version{ revert_to!(value) }
16
+ association(:versions).send(:delete_records, versions.after(value), self.class.reflect_on_association(:versions).options[:dependent])
17
+ reset_version
18
+ end
19
+ saved
20
+ end
21
+ end
22
+ end