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.
Files changed (40) hide show
  1. data/LICENSE +20 -0
  2. data/README.rdoc +196 -0
  3. data/lib/generators/vestal_versions.rb +11 -0
  4. data/lib/generators/vestal_versions/migration/migration_generator.rb +17 -0
  5. data/lib/generators/vestal_versions/migration/templates/initializer.rb +9 -0
  6. data/lib/generators/vestal_versions/migration/templates/migration.rb +28 -0
  7. data/lib/vestal_versions.rb +126 -0
  8. data/lib/vestal_versions/changes.rb +122 -0
  9. data/lib/vestal_versions/conditions.rb +57 -0
  10. data/lib/vestal_versions/control.rb +200 -0
  11. data/lib/vestal_versions/creation.rb +93 -0
  12. data/lib/vestal_versions/deletion.rb +39 -0
  13. data/lib/vestal_versions/options.rb +41 -0
  14. data/lib/vestal_versions/reload.rb +17 -0
  15. data/lib/vestal_versions/reset.rb +24 -0
  16. data/lib/vestal_versions/reversion.rb +82 -0
  17. data/lib/vestal_versions/users.rb +55 -0
  18. data/lib/vestal_versions/version.rb +80 -0
  19. data/lib/vestal_versions/version_num.rb +3 -0
  20. data/lib/vestal_versions/version_tagging.rb +50 -0
  21. data/lib/vestal_versions/versioned.rb +27 -0
  22. data/lib/vestal_versions/versions.rb +74 -0
  23. data/spec/spec_helper.rb +20 -0
  24. data/spec/support/models.rb +19 -0
  25. data/spec/support/schema.rb +25 -0
  26. data/spec/vestal_versions/changes_spec.rb +134 -0
  27. data/spec/vestal_versions/conditions_spec.rb +103 -0
  28. data/spec/vestal_versions/control_spec.rb +120 -0
  29. data/spec/vestal_versions/creation_spec.rb +90 -0
  30. data/spec/vestal_versions/deletion_spec.rb +86 -0
  31. data/spec/vestal_versions/options_spec.rb +45 -0
  32. data/spec/vestal_versions/reload_spec.rb +18 -0
  33. data/spec/vestal_versions/reset_spec.rb +111 -0
  34. data/spec/vestal_versions/reversion_spec.rb +103 -0
  35. data/spec/vestal_versions/users_spec.rb +21 -0
  36. data/spec/vestal_versions/version_spec.rb +61 -0
  37. data/spec/vestal_versions/version_tagging_spec.rb +39 -0
  38. data/spec/vestal_versions/versioned_spec.rb +16 -0
  39. data/spec/vestal_versions/versions_spec.rb +176 -0
  40. metadata +165 -0
@@ -0,0 +1,57 @@
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
+ module InstanceMethods
29
+ private
30
+ # After first determining whether the <tt>:if</tt> and <tt>:unless</tt> conditions are
31
+ # satisfied, the original, unaliased +create_version?+ method is called to determine
32
+ # whether a new version should be created upon update of the ActiveRecord::Base instance.
33
+ def create_version?
34
+ version_conditions_met? && super
35
+ end
36
+
37
+ # After first determining whether the <tt>:if</tt> and <tt>:unless</tt> conditions are
38
+ # satisfied, the original, unaliased +update_version?+ method is called to determine
39
+ # whther the last version should be updated to include changes merged from the current
40
+ # ActiveRecord::Base instance update.
41
+ #
42
+ # The overridden +update_version?+ method simply returns false, effectively delegating
43
+ # the decision to whether the <tt>:if</tt> and <tt>:unless</tt> conditions are met.
44
+ def update_version?
45
+ version_conditions_met? && super
46
+ end
47
+
48
+ # Simply checks whether the <tt>:if</tt> and <tt>:unless</tt> conditions given in the
49
+ # +versioned+ options are met: meaning that all procs in the <tt>:if</tt> array must
50
+ # evaluate to a non-false, non-nil value and that all procs in the <tt>:unless</tt> array
51
+ # must all evaluate to either false or nil.
52
+ def version_conditions_met?
53
+ vestal_versions_options[:if].all?{|p| p.call(self) } && !vestal_versions_options[:unless].any?{|p| p.call(self) }
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,200 @@
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
+ module InstanceMethods
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
+ end
164
+ module ClassMethods
165
+ # The +skip_version+ block simply allows for updates to be made to an instance of a versioned
166
+ # ActiveRecord model while ignoring all new version creation. The <tt>:if</tt> and
167
+ # <tt>:unless</tt> conditions (if given) will not be evaulated inside a +skip_version+ block.
168
+ #
169
+ # When the block closes, the instance is automatically saved, so explicitly saving the
170
+ # object within the block is unnecessary.
171
+ #
172
+ # == Example
173
+ #
174
+ # user = User.find_by_first_name("Steve")
175
+ # user.version # => 1
176
+ # user.skip_version do
177
+ # user.first_name = "Stephen"
178
+ # end
179
+ # user.version # => 1
180
+ def skip_version
181
+ _with_version_flag(:_skip_version) do
182
+ yield if block_given?
183
+ end
184
+ end
185
+
186
+ # Used for each control block, the +with_version_flag+ method sets a given variable to
187
+ # true and then executes the given block, ensuring that the variable is returned to a nil
188
+ # value before returning. This is useful to be certain that one of the control flag
189
+ # instance variables isn't inadvertently left in the "on" position by execution within the
190
+ # block raising an exception.
191
+ def _with_version_flag(flag)
192
+ self.send("#{flag}=", true)
193
+ yield
194
+ ensure
195
+ self.send("#{flag}=", nil)
196
+ end
197
+
198
+ end
199
+ end
200
+ 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
+ module InstanceMethods
31
+ private
32
+ # Returns whether an initial version should be created upon creation of the parent record.
33
+ def create_initial_version?
34
+ vestal_versions_options[:initial_version] == true
35
+ end
36
+
37
+ # Creates an initial version upon creation of the parent record.
38
+ def create_initial_version
39
+ versions.create(version_attributes.merge(:number => 1))
40
+ reset_version_changes
41
+ reset_version
42
+ end
43
+
44
+ # Returns whether a new version should be created upon updating the parent record.
45
+ def create_version?
46
+ !version_changes.blank?
47
+ end
48
+
49
+ # Creates a new version upon updating the parent record.
50
+ def create_version(attributes = nil)
51
+ versions.create(attributes || version_attributes)
52
+ reset_version_changes
53
+ reset_version
54
+ end
55
+
56
+ # Returns whether the last version should be updated upon updating the parent record.
57
+ # This method is overridden in VestalVersions::Control to account for a control block that
58
+ # merges changes onto the previous version.
59
+ def update_version?
60
+ false
61
+ end
62
+
63
+ # Updates the last version's changes by appending the current version changes.
64
+ def update_version
65
+ return create_version unless v = versions.last
66
+ v.modifications_will_change!
67
+ v.update_attribute(:modifications, v.changes.append_changes(version_changes))
68
+ reset_version_changes
69
+ reset_version
70
+ end
71
+
72
+ # Returns an array of column names that should be included in the changes of created
73
+ # versions. If <tt>vestal_versions_options[:only]</tt> is specified, only those columns
74
+ # will be versioned. Otherwise, if <tt>vestal_versions_options[:except]</tt> is specified,
75
+ # all columns will be versioned other than those specified. Without either option, the
76
+ # default is to version all columns. At any rate, the four "automagic" timestamp columns
77
+ # maintained by Rails are never versioned.
78
+ def versioned_columns
79
+ case
80
+ when vestal_versions_options[:only] then self.class.column_names & vestal_versions_options[:only]
81
+ when vestal_versions_options[:except] then self.class.column_names - vestal_versions_options[:except]
82
+ else self.class.column_names
83
+ end - %w(created_at created_on updated_at updated_on)
84
+ end
85
+
86
+ # Specifies the attributes used during version creation. This is separated into its own
87
+ # method so that it can be overridden by the VestalVersions::Users feature.
88
+ def version_attributes
89
+ {:modifications => version_changes, :number => last_version + 1}
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,39 @@
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
+ module InstanceMethods
27
+ private
28
+
29
+ def delete_version?
30
+ vestal_versions_options[:track_destroy]
31
+ end
32
+
33
+ def create_destroyed_version
34
+ create_version({:modifications => attributes, :number => last_version + 1, :tag => 'deleted'})
35
+ end
36
+
37
+ end
38
+ end
39
+ 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_inheritable_accessor :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,17 @@
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
+ module InstanceMethods
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
17
+ end