bitfluent-vestal_versions 1.1.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 (42) hide show
  1. data/.gitignore +23 -0
  2. data/LICENSE +20 -0
  3. data/README.rdoc +175 -0
  4. data/Rakefile +45 -0
  5. data/VERSION +1 -0
  6. data/generators/vestal_versions/templates/initializer.rb +9 -0
  7. data/generators/vestal_versions/templates/migration.rb +27 -0
  8. data/generators/vestal_versions/vestal_versions_generator.rb +24 -0
  9. data/init.rb +1 -0
  10. data/lib/vestal_versions.rb +103 -0
  11. data/lib/vestal_versions/changes.rb +125 -0
  12. data/lib/vestal_versions/conditions.rb +69 -0
  13. data/lib/vestal_versions/configuration.rb +40 -0
  14. data/lib/vestal_versions/control.rb +175 -0
  15. data/lib/vestal_versions/creation.rb +100 -0
  16. data/lib/vestal_versions/options.rb +45 -0
  17. data/lib/vestal_versions/reload.rb +23 -0
  18. data/lib/vestal_versions/reset.rb +28 -0
  19. data/lib/vestal_versions/reversion.rb +69 -0
  20. data/lib/vestal_versions/tagging.rb +50 -0
  21. data/lib/vestal_versions/users.rb +57 -0
  22. data/lib/vestal_versions/version.rb +32 -0
  23. data/lib/vestal_versions/versioned.rb +30 -0
  24. data/lib/vestal_versions/versions.rb +74 -0
  25. data/test/changes_test.rb +169 -0
  26. data/test/conditions_test.rb +137 -0
  27. data/test/configuration_test.rb +39 -0
  28. data/test/control_test.rb +152 -0
  29. data/test/creation_test.rb +148 -0
  30. data/test/options_test.rb +52 -0
  31. data/test/reload_test.rb +19 -0
  32. data/test/reset_test.rb +112 -0
  33. data/test/reversion_test.rb +68 -0
  34. data/test/schema.rb +43 -0
  35. data/test/tagging_test.rb +39 -0
  36. data/test/test_helper.rb +12 -0
  37. data/test/users_test.rb +25 -0
  38. data/test/version_test.rb +43 -0
  39. data/test/versioned_test.rb +18 -0
  40. data/test/versions_test.rb +172 -0
  41. data/vestal_versions.gemspec +105 -0
  42. metadata +167 -0
@@ -0,0 +1,69 @@
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
+ def self.included(base) # :nodoc:
6
+ base.class_eval do
7
+ extend ClassMethods
8
+ include InstanceMethods
9
+
10
+ alias_method_chain :create_version?, :conditions
11
+ alias_method_chain :update_version?, :conditions
12
+
13
+ class << self
14
+ alias_method_chain :prepare_versioned_options, :conditions
15
+ end
16
+ end
17
+ end
18
+
19
+ # Class methods on ActiveRecord::Base to prepare the <tt>:if</tt> and <tt>:unless</tt> options.
20
+ module ClassMethods
21
+ # After the original +prepare_versioned_options+ method cleans the given options, this alias
22
+ # also extracts the <tt>:if</tt> and <tt>:unless</tt> options, chaning them into arrays
23
+ # and converting any symbols to procs. Procs are called with the ActiveRecord model instance
24
+ # as the sole argument.
25
+ #
26
+ # If all of the <tt>:if</tt> conditions are met and none of the <tt>:unless</tt> conditions
27
+ # are unmet, than version creation will proceed, assuming all other conditions are also met.
28
+ def prepare_versioned_options_with_conditions(options)
29
+ result = prepare_versioned_options_without_conditions(options)
30
+
31
+ self.vestal_versions_options[:if] = Array(options.delete(:if)).map(&:to_proc)
32
+ self.vestal_versions_options[:unless] = Array(options.delete(:unless)).map(&:to_proc)
33
+
34
+ result
35
+ end
36
+ end
37
+
38
+ # Instance methods that determine based on the <tt>:if</tt> and <tt>:unless</tt> conditions,
39
+ # whether a version is to be create or updated.
40
+ module InstanceMethods
41
+ private
42
+ # After first determining whether the <tt>:if</tt> and <tt>:unless</tt> conditions are
43
+ # satisfied, the original, unaliased +create_version?+ method is called to determine
44
+ # whether a new version should be created upon update of the ActiveRecord::Base instance.
45
+ def create_version_with_conditions?
46
+ version_conditions_met? && create_version_without_conditions?
47
+ end
48
+
49
+ # After first determining whether the <tt>:if</tt> and <tt>:unless</tt> conditions are
50
+ # satisfied, the original, unaliased +update_version?+ method is called to determine
51
+ # whther the last version should be updated to include changes merged from the current
52
+ # ActiveRecord::Base instance update.
53
+ #
54
+ # The overridden +update_version?+ method simply returns false, effectively delegating
55
+ # the decision to whether the <tt>:if</tt> and <tt>:unless</tt> conditions are met.
56
+ def update_version_with_conditions?
57
+ version_conditions_met? && update_version_without_conditions?
58
+ end
59
+
60
+ # Simply checks whether the <tt>:if</tt> and <tt>:unless</tt> conditions given in the
61
+ # +versioned+ options are met: meaning that all procs in the <tt>:if</tt> array must
62
+ # evaluate to a non-false, non-nil value and that all procs in the <tt>:unless</tt> array
63
+ # must all evaluate to either false or nil.
64
+ def version_conditions_met?
65
+ vestal_versions_options[:if].all?{|p| p.call(self) } && !vestal_versions_options[:unless].any?{|p| p.call(self) }
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,40 @@
1
+ module VestalVersions
2
+ # Allows for easy application-wide configuration of options passed into the +versioned+ method.
3
+ module Configuration
4
+ # The VestalVersions module is extended by VestalVersions::Configuration, allowing the
5
+ # +configure method+ to be used as follows in a Rails initializer:
6
+ #
7
+ # VestalVersions.configure do |config|
8
+ # config.class_name = "MyCustomVersion"
9
+ # config.dependent = :destroy
10
+ # end
11
+ #
12
+ # Each variable assignment in the +configure+ block corresponds directly with the options
13
+ # available to the +versioned+ method. Assigning common options in an initializer can keep your
14
+ # models tidy.
15
+ #
16
+ # If an option is given in both an initializer and in the options passed to +versioned+, the
17
+ # value given in the model itself will take precedence.
18
+ def configure
19
+ yield Configuration
20
+ end
21
+
22
+ class << self
23
+ # Simply stores a hash of options given to the +configure+ block.
24
+ def options
25
+ @options ||= {}
26
+ end
27
+
28
+ # If given a setter method name, will assign the first argument to the +options+ hash with
29
+ # the method name (sans "=") as the key. If given a getter method name, will attempt to
30
+ # a value from the +options+ hash for that key. If the key doesn't exist, defers to +super+.
31
+ def method_missing(symbol, *args)
32
+ if (method = symbol.to_s).sub!(/\=$/, '')
33
+ options[method.to_sym] = args.first
34
+ else
35
+ options.fetch(method.to_sym, super)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,175 @@
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
+ def self.included(base) # :nodoc:
6
+ base.class_eval do
7
+ include InstanceMethods
8
+
9
+ alias_method_chain :create_version?, :control
10
+ alias_method_chain :update_version?, :control
11
+ end
12
+ end
13
+
14
+ # Control blocks are called on ActiveRecord::Base instances as to not cause any conflict with
15
+ # other instances of the versioned class whose behavior could be inadvertently altered within
16
+ # a control block.
17
+ module InstanceMethods
18
+ # The +skip_version+ block simply allows for updates to be made to an instance of a versioned
19
+ # ActiveRecord model while ignoring all new version creation. The <tt>:if</tt> and
20
+ # <tt>:unless</tt> conditions (if given) will not be evaulated inside a +skip_version+ block.
21
+ #
22
+ # When the block closes, the instance is automatically saved, so explicitly saving the
23
+ # object within the block is unnecessary.
24
+ #
25
+ # == Example
26
+ #
27
+ # user = User.find_by_first_name("Steve")
28
+ # user.version # => 1
29
+ # user.skip_version do
30
+ # user.first_name = "Stephen"
31
+ # end
32
+ # user.version # => 1
33
+ def skip_version
34
+ with_version_flag(:skip_version) do
35
+ yield if block_given?
36
+ save
37
+ end
38
+ end
39
+
40
+ # Behaving almost identically to the +skip_version+ block, the only difference with the
41
+ # +skip_version!+ block is that the save automatically performed at the close of the block
42
+ # is a +save!+, meaning that an exception will be raised if the object cannot be saved.
43
+ def skip_version!
44
+ with_version_flag(:skip_version) do
45
+ yield if block_given?
46
+ save!
47
+ end
48
+ end
49
+
50
+ # A convenience method for determining whether a versioned instance is set to skip its next
51
+ # version creation.
52
+ def skip_version?
53
+ !!@skip_version
54
+ end
55
+
56
+ # Merging versions with the +merge_version+ block will take all of the versions that would
57
+ # be created within the block and merge them into one version and pushing that single version
58
+ # onto the ActiveRecord::Base instance's version history. A new version will be created and
59
+ # the instance's version number will be incremented.
60
+ #
61
+ # == Example
62
+ #
63
+ # user = User.find_by_first_name("Steve")
64
+ # user.version # => 1
65
+ # user.merge_version do
66
+ # user.update_attributes(:first_name => "Steven", :last_name => "Tyler")
67
+ # user.update_attribute(:first_name, "Stephen")
68
+ # user.update_attribute(:last_name, "Richert")
69
+ # end
70
+ # user.version # => 2
71
+ # user.versions.last.changes
72
+ # # => {"first_name" => ["Steve", "Stephen"], "last_name" => ["Jobs", "Richert"]}
73
+ #
74
+ # See VestalVersions::Changes for an explanation on how changes are appended.
75
+ def merge_version
76
+ with_version_flag(:merge_version) do
77
+ yield if block_given?
78
+ end
79
+ save
80
+ end
81
+
82
+ # Behaving almost identically to the +merge_version+ block, the only difference with the
83
+ # +merge_version!+ block is that the save automatically performed at the close of the block
84
+ # is a +save!+, meaning that an exception will be raised if the object cannot be saved.
85
+ def merge_version!
86
+ with_version_flag(:merge_version) do
87
+ yield if block_given?
88
+ end
89
+ save!
90
+ end
91
+
92
+ # A convenience method for determining whether a versioned instance is set to merge its next
93
+ # versions into one before version creation.
94
+ def merge_version?
95
+ !!@merge_version
96
+ end
97
+
98
+ # Appending versions with the +append_version+ block acts similarly to the +merge_version+
99
+ # block in that all would-be version creations within the block are defered until the block
100
+ # closes. The major difference is that with +append_version+, a new version is not created.
101
+ # Rather, the cumulative changes are appended to the serialized changes of the instance's
102
+ # last version. A new version is not created, so the version number is not incremented.
103
+ #
104
+ # == Example
105
+ #
106
+ # user = User.find_by_first_name("Steve")
107
+ # user.version # => 2
108
+ # user.versions.last.changes
109
+ # # => {"first_name" => ["Stephen", "Steve"]}
110
+ # user.append_version do
111
+ # user.last_name = "Jobs"
112
+ # end
113
+ # user.versions.last.changes
114
+ # # => {"first_name" => ["Stephen", "Steve"], "last_name" => ["Richert", "Jobs"]}
115
+ # user.version # => 2
116
+ #
117
+ # See VestalVersions::Changes for an explanation on how changes are appended.
118
+ def append_version
119
+ with_version_flag(:merge_version) do
120
+ yield if block_given?
121
+ end
122
+
123
+ with_version_flag(:append_version) do
124
+ save
125
+ end
126
+ end
127
+
128
+ # Behaving almost identically to the +append_version+ block, the only difference with the
129
+ # +append_version!+ block is that the save automatically performed at the close of the block
130
+ # is a +save!+, meaning that an exception will be raised if the object cannot be saved.
131
+ def append_version!
132
+ with_version_flag(:merge_version) do
133
+ yield if block_given?
134
+ end
135
+
136
+ with_version_flag(:append_version) do
137
+ save!
138
+ end
139
+ end
140
+
141
+ # A convenience method for determining whether a versioned instance is set to append its next
142
+ # version's changes into the last version changes.
143
+ def append_version?
144
+ !!@append_version
145
+ end
146
+
147
+ private
148
+ # Used for each control block, the +with_version_flag+ method sets a given variable to
149
+ # true and then executes the given block, ensuring that the variable is returned to a nil
150
+ # value before returning. This is useful to be certain that one of the control flag
151
+ # instance variables isn't inadvertently left in the "on" position by execution within the
152
+ # block raising an exception.
153
+ def with_version_flag(flag)
154
+ begin
155
+ instance_variable_set("@#{flag}", true)
156
+ yield
157
+ ensure
158
+ instance_variable_set("@#{flag}", nil)
159
+ end
160
+ end
161
+
162
+ # Overrides the basal +create_version?+ method to make sure that new versions are not
163
+ # created when inside any of the control blocks (until the block terminates).
164
+ def create_version_with_control?
165
+ !skip_version? && !merge_version? && !append_version? && create_version_without_control?
166
+ end
167
+
168
+ # Overrides the basal +update_version?+ method to allow the last version of an versioned
169
+ # ActiveRecord::Base instance to be updated at the end of an +append_version+ block.
170
+ def update_version_with_control?
171
+ append_version?
172
+ end
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,100 @@
1
+ module VestalVersions
2
+ # Adds the functionality necessary to control version creation on a versioned instance of
3
+ # ActiveRecord::Base.
4
+ module Creation
5
+ def self.included(base) # :nodoc:
6
+ base.class_eval do
7
+ extend ClassMethods
8
+ include InstanceMethods
9
+
10
+ after_create :create_initial_version, :if => :create_initial_version?
11
+ after_update :create_version, :if => :create_version?
12
+ after_update :update_version, :if => :update_version?
13
+
14
+ class << self
15
+ alias_method_chain :prepare_versioned_options, :creation
16
+ end
17
+ end
18
+ end
19
+
20
+ # Class methods added to ActiveRecord::Base to facilitate the creation of new versions.
21
+ module ClassMethods
22
+ # Overrides the basal +prepare_versioned_options+ method defined in VestalVersions::Options
23
+ # to extract the <tt>:only</tt>, <tt>:except</tt> and <tt>:initial_version</tt> options
24
+ # into +vestal_versions_options+.
25
+ def prepare_versioned_options_with_creation(options)
26
+ result = prepare_versioned_options_without_creation(options)
27
+
28
+ self.vestal_versions_options[:only] = Array(options.delete(:only)).map(&:to_s).uniq if options[:only]
29
+ self.vestal_versions_options[:except] = Array(options.delete(:except)).map(&:to_s).uniq if options[:except]
30
+ self.vestal_versions_options[:initial_version] = options.delete(:initial_version)
31
+
32
+ result
33
+ end
34
+ end
35
+
36
+ # Instance methods that determine whether to save a version and actually perform the save.
37
+ module InstanceMethods
38
+ private
39
+ # Returns whether an initial version should be created upon creation of the parent record.
40
+ def create_initial_version?
41
+ vestal_versions_options[:initial_version] == true
42
+ end
43
+
44
+ # Creates an initial version upon creation of the parent record.
45
+ def create_initial_version
46
+ versions.create(version_attributes.merge(:number => 1))
47
+ reset_version_changes
48
+ reset_version
49
+ end
50
+
51
+ # Returns whether a new version should be created upon updating the parent record.
52
+ def create_version?
53
+ !version_changes.blank?
54
+ end
55
+
56
+ # Creates a new version upon updating the parent record.
57
+ def create_version
58
+ versions.create(version_attributes)
59
+ reset_version_changes
60
+ reset_version
61
+ end
62
+
63
+ # Returns whether the last version should be updated upon updating the parent record.
64
+ # This method is overridden in VestalVersions::Control to account for a control block that
65
+ # merges changes onto the previous version.
66
+ def update_version?
67
+ false
68
+ end
69
+
70
+ # Updates the last version's changes by appending the current version changes.
71
+ def update_version
72
+ return create_version unless v = versions.last
73
+ v.modifications_will_change!
74
+ v.update_attribute(:modifications, v.changes.append_changes(version_changes))
75
+ reset_version_changes
76
+ reset_version
77
+ end
78
+
79
+ # Returns an array of column names that should be included in the changes of created
80
+ # versions. If <tt>vestal_versions_options[:only]</tt> is specified, only those columns
81
+ # will be versioned. Otherwise, if <tt>vestal_versions_options[:except]</tt> is specified,
82
+ # all columns will be versioned other than those specified. Without either option, the
83
+ # default is to version all columns. At any rate, the four "automagic" timestamp columns
84
+ # maintained by Rails are never versioned.
85
+ def versioned_columns
86
+ case
87
+ when vestal_versions_options[:only] then self.class.column_names & vestal_versions_options[:only]
88
+ when vestal_versions_options[:except] then self.class.column_names - vestal_versions_options[:except]
89
+ else self.class.column_names
90
+ end - %w(created_at created_on updated_at updated_on)
91
+ end
92
+
93
+ # Specifies the attributes used during version creation. This is separated into its own
94
+ # method so that it can be overridden by the VestalVersions::Users feature.
95
+ def version_attributes
96
+ {:modifications => version_changes, :number => last_version + 1}
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,45 @@
1
+ module VestalVersions
2
+ # Provides +versioned+ options conversion and cleanup.
3
+ module Options
4
+ def self.included(base) # :nodoc:
5
+ base.class_eval do
6
+ extend ClassMethods
7
+ end
8
+ end
9
+
10
+ # Class methods that provide preparation of options passed to the +versioned+ method.
11
+ module ClassMethods
12
+ # The +prepare_versioned_options+ method has three purposes:
13
+ # 1. Populate the provided options with default values where needed
14
+ # 2. Prepare options for use with the +has_many+ association
15
+ # 3. Save user-configurable options in a class-level variable
16
+ #
17
+ # Options are given priority in the following order:
18
+ # 1. Those passed directly to the +versioned+ method
19
+ # 2. Those specified in an initializer +configure+ block
20
+ # 3. Default values specified in +prepare_versioned_options+
21
+ #
22
+ # The method is overridden in feature modules that require specific options outside the
23
+ # standard +has_many+ associations.
24
+ def prepare_versioned_options(options)
25
+ options.symbolize_keys!
26
+ options.reverse_merge!(Configuration.options)
27
+ options.reverse_merge!(
28
+ :class_name => 'VestalVersions::Version',
29
+ :dependent => :delete_all
30
+ )
31
+ options.reverse_merge!(
32
+ :order => "#{options[:class_name].constantize.table_name}.number ASC"
33
+ )
34
+
35
+ class_inheritable_accessor :vestal_versions_options
36
+ self.vestal_versions_options = options.dup
37
+
38
+ options.merge!(
39
+ :as => :versioned,
40
+ :extend => Array(options[:extend]).unshift(Versions)
41
+ )
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,23 @@
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
+ def self.included(base) # :nodoc:
6
+ base.class_eval do
7
+ include InstanceMethods
8
+
9
+ alias_method_chain :reload, :versions
10
+ end
11
+ end
12
+
13
+ # Adds instance methods into ActiveRecord::Base to tap into the +reload+ method.
14
+ module InstanceMethods
15
+ # Overrides ActiveRecord::Base#reload, resetting the instance-variable-cached version number
16
+ # before performing the original +reload+ method.
17
+ def reload_with_versions(*args)
18
+ reset_version
19
+ reload_without_versions(*args)
20
+ end
21
+ end
22
+ end
23
+ end