bitfluent-vestal_versions 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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