houston-vestal_versions 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: bb8143287729c95ca8970beb1f6b36228a30cd01
4
+ data.tar.gz: cf8e8d7cbb7398c6852f148856022c4cc03cf8c9
5
+ SHA512:
6
+ metadata.gz: bb2a468aaec1cd1c3177c42efd7fc316aa49e1af3584068e74997835c464ad94a5768b38b454edee16cb4025c3656cde8636c0fadae8ff45dfccdf222209555d
7
+ data.tar.gz: 2e6786d698cc096065fc646476d4da7d6c56e7a78515d28ea50a09252b1d4596f2b1f8dabf5b395bf545e48cce986d73fbf46b5398d52dd0eb8f087ed6f1e048
data/.gitignore ADDED
@@ -0,0 +1,22 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .rvmrc
6
+ .yardoc
7
+ coverage
8
+ doc/
9
+ Gemfile.lock
10
+ gemfiles/*.lock
11
+ InstalledFiles
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
19
+ _yardoc
20
+ .rspec
21
+ .ruby-gemset
22
+ .ruby-version
data/.travis.yml ADDED
@@ -0,0 +1,22 @@
1
+ branches:
2
+ only:
3
+ - master
4
+ gemfile:
5
+ - gemfiles/activerecord_3_0.gemfile
6
+ - gemfiles/activerecord_3_1.gemfile
7
+ - gemfiles/activerecord_3_2.gemfile
8
+ - gemfiles/activerecord_4_0.gemfile
9
+ language: ruby
10
+ matrix:
11
+ allow_failures:
12
+ - rvm: ruby-head
13
+ include:
14
+ - env: COVERAGE=1
15
+ gemfile: Gemfile
16
+ rvm: 2.1.0
17
+ rvm:
18
+ - 1.9.3
19
+ - 2.0.0
20
+ - 2.1.0
21
+ - ruby-head
22
+ script: bundle exec rspec
data/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ # 2.0.0 / 2014-01-20
2
+
3
+ * [ENHANCEMENT] Ruby 2.1.0 compatibility
4
+ * [ENHANCEMENT] Ruby 2.0.0 compatibility
5
+ * [ENHANCEMENT] Ruby 1.9.3 compatibility
6
+ * [ENHANCEMENT] Initial Rails 4 compatibility
7
+ * [ENHANCEMENT] Initial Rails 3 compatibility
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ group :test do
6
+ gem 'coveralls', '~> 0.7', :require => false
7
+ gem 'rspec', '~> 2.0'
8
+ gem 'sqlite3', '~> 1.0'
9
+ end
10
+
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Steve Richert
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,206 @@
1
+ = Vestal Versions
2
+
3
+ {<img src="https://badge.fury.io/rb/vestal_versions.png" alt="Gem Version" />}[http://badge.fury.io/rb/vestal_versions]
4
+ {<img src="https://travis-ci.org/laserlemon/vestal_versions.png?branch=master" alt="Build Status" />}[https://travis-ci.org/laserlemon/vestal_versions]
5
+ {<img src="https://codeclimate.com/github/laserlemon/vestal_versions.png" alt="Code Climate" />}[https://codeclimate.com/github/laserlemon/vestal_versions]
6
+ {<img src="https://coveralls.io/repos/laserlemon/vestal_versions/badge.png" alt="Coverage Status" />}[https://coveralls.io/r/laserlemon/vestal_versions]
7
+ {<img src="https://gemnasium.com/laserlemon/vestal_versions.png" alt="Dependency Status" />}[https://gemnasium.com/laserlemon/vestal_versions]
8
+
9
+ Finally, DRY ActiveRecord versioning!
10
+
11
+ <tt>acts_as_versioned</tt>[http://github.com/technoweenie/acts_as_versioned] by technoweenie[http://github.com/technoweenie] was a great start, but it failed to keep up with ActiveRecord's introduction of dirty objects in version 2.1. Additionally, each versioned model needs its own versions table that duplicates most of the original table's columns. The versions table is then populated with records that often duplicate most of the original record's attributes. All in all, not very DRY.
12
+
13
+ <tt>vestal_versions</tt>[http://github.com/laserlemon/vestal_versions] requires only one versions table (polymorphically associated with its parent models) and no changes whatsoever to existing tables. But it goes one step DRYer by storing a serialized hash of _only_ the models' changes. Think modern version control systems. By traversing the record of changes, the models can be reverted to any point in time.
14
+
15
+ And that's just what <tt>vestal_versions</tt> does. Not only can a model be reverted to a previous version number but also to a date or time!
16
+
17
+ == Compatibility
18
+
19
+ Tested with Active Record 3.2.16 with Ruby 1.9.3 and 1.9.2.
20
+
21
+ == Installation
22
+
23
+ In the Gemfile:
24
+
25
+ ** Note: I am giving this project some much needed love to keep her relevant in a post Rails 3 world. I will be finalizing a version to support 1.9.2+ and Rails 3.2+ soon and pushing the gem, till then, use the git repo:
26
+ ~dreamr
27
+
28
+ gem 'vestal_versions', :git => 'git://github.com/laserlemon/vestal_versions'
29
+
30
+
31
+ Next, generate and run the first and last versioning migration you'll ever need:
32
+
33
+ $ rails generate vestal_versions:migration
34
+ $ rake db:migrate
35
+
36
+ == Example
37
+
38
+ To version an ActiveRecord model, simply add <tt>versioned</tt> to your class like so:
39
+
40
+ class User < ActiveRecord::Base
41
+ versioned
42
+
43
+ validates_presence_of :first_name, :last_name
44
+
45
+ def name
46
+ "#{first_name} #{last_name}"
47
+ end
48
+ end
49
+
50
+ It's that easy! Now watch it in action...
51
+
52
+ >> u = User.create(:first_name => "Steve", :last_name => "Richert")
53
+ => #<User first_name: "Steve", last_name: "Richert">
54
+ >> u.version
55
+ => 1
56
+ >> u.update_attribute(:first_name, "Stephen")
57
+ => true
58
+ >> u.name
59
+ => "Stephen Richert"
60
+ >> u.version
61
+ => 2
62
+ >> u.revert_to(10.seconds.ago)
63
+ => 1
64
+ >> u.name
65
+ => "Steve Richert"
66
+ >> u.version
67
+ => 1
68
+ >> u.save
69
+ => true
70
+ >> u.version
71
+ => 3
72
+ >> u.update_attribute(:last_name, "Jobs")
73
+ => true
74
+ >> u.name
75
+ => "Steve Jobs"
76
+ >> u.version
77
+ => 4
78
+ >> u.revert_to!(2)
79
+ => true
80
+ >> u.name
81
+ => "Stephen Richert"
82
+ >> u.version
83
+ => 5
84
+
85
+ == Upgrading to 1.0
86
+
87
+ For the most part, version 1.0 of <tt>vestal_versions</tt> is backwards compatible, with just a few notable changes:
88
+
89
+ * The versions table has been beefed up. You'll need to add the following columns (and indexes, if you feel so inclined):
90
+
91
+ change_table :versions do |t|
92
+ t.belongs_to :user, :polymorphic => true
93
+ t.string :user_name
94
+ t.string :tag
95
+ end
96
+
97
+ change_table :versions do |t|
98
+ t.index [:user_id, :user_type]
99
+ t.index :user_name
100
+ t.index :tag
101
+ end
102
+
103
+ * When a model is created (or updated the first time after being versioned), an initial version record with a number of 1 is no longer created. These aren't used during reversion and so they end up just being dead weight. Feel free to scrap all your versions where <tt>number == 1</tt> after the upgrade if you'd like to free up some room in your database (but you don't have to).
104
+
105
+ * Models that have no version records in the database will return a <tt>@user.version</tt> of 1. In the past, this would have returned <tt>nil</tt> instead.
106
+
107
+ * <tt>Version</tt> has moved to <tt>VestalVersions::Version</tt> to make way for custom version classes.
108
+
109
+ * <tt>Version#version</tt> did not survive the move to <tt>VestalVersions::Version#version</tt>. That alias was dropped (too confusing). Use <tt>VestalVersions::Version#number</tt>.
110
+
111
+ == New to 1.0
112
+
113
+ There are a handful of exciting new additions in version 1.0 of <tt>vestal_versions</tt>. A lot has changed in the code: much better documentation, more modular organization of features, and a more exhaustive test suite. But there are also a number of new features that are available in this release of <tt>vestal_versions</tt>:
114
+
115
+ * The ability to completely skip versioning within a new <tt>skip_version</tt> block:
116
+
117
+ @user.version # => 1
118
+ @user.skip_version do
119
+ @user.update_attribute(:first_name, "Stephen")
120
+ @user.first_name = "Steve"
121
+ @user.save
122
+ @user.update_attributes(:last_name => "Jobs")
123
+ end
124
+ @user.version # => 1
125
+
126
+ Also available, are <tt>merge_version</tt> and <tt>append_version</tt> blocks. The <tt>merge_version</tt> block will compile the possibly multiple versions that would result from the updates inside the block into one summary version. The single resulting version is then tacked onto the version history as usual. The <tt>append_version</tt> block works similarly except that the resulting single version is combined with the most recent version in the history and saved.
127
+
128
+ * Version tagging. Any version can have a tag attached to it (must be unique within the scope of the versioned parent) and that tag can be used for reversion.
129
+
130
+ @user.name # => "Steve Richert"
131
+ @user.update_attribute(:last_name, "Jobs")
132
+ @user.name # => "Steve Jobs"
133
+ @user.tag_version("apple")
134
+ @user.update_attribute(:last_name, "Richert")
135
+ @user.name # => "Steve Richert"
136
+ @user.revert_to("apple")
137
+ @user.name # => "Steve Jobs"
138
+
139
+ So if you're not big on version numbers, you could just tag your versions and avoid the numbers altogether.
140
+
141
+ * Resetting. This is basically a hard revert. The new <tt>reset_to!</tt> instance method behaves just like the <tt>revert_to!</tt> method except that after the reversion, it will also scrap all the versions that came after that target version.
142
+
143
+ @user.name # => "Steve Richert"
144
+ @user.version # => 1
145
+ @user.versions.count # => 0
146
+ @user.update_attribute(:last_name, "Jobs")
147
+ @user.name # => "Steve Jobs"
148
+ @user.version # => 2
149
+ @user.versions.count # => 1
150
+ @user.reset_to!(1)
151
+ @user.name # => "Steve Richert"
152
+ @user.version # => 1
153
+ @user.versions.count # => 0
154
+
155
+ * Storing which user is responsible for a revision. Rather than introduce a lot of controller magic to guess what to store, you can simply update an additional attribute on your versioned model: <tt>updated_by</tt>.
156
+
157
+ @user.update_attributes(:last_name => "Jobs", :updated_by => "Tyler")
158
+ @user.versions.last.user # => "Tyler"
159
+
160
+ Instead of passing a simple string to the <tt>updated_by</tt> setter, you can pass a model instance, such as an ActiveRecord user or administrator. The association will be saved polymorphically alongside the version.
161
+
162
+ @user.update_attributes(:last_name => "Jobs", :updated_by => current_user)
163
+ @user.versions.last.user # => #<User first_name: "Steven", last_name: "Tyler">
164
+
165
+ * Global configuration. The new <tt>vestal_versions</tt> Rails generator also writes an initializer with instructions on how to set application-wide options for the <tt>versioned</tt> method.
166
+
167
+ * Conditional version creation. The <tt>versioned</tt> method now accepts <tt>:if</tt> and <tt>:unless</tt> options. Each expects a symbol representing an instance method or a proc that will be evaluated to determine whether or not to create a new version after an update. An array containing any combination of symbols and procs can also be given.
168
+
169
+ class User < ActiveRecord::Base
170
+ versioned :if => :really_create_a_version?
171
+ end
172
+
173
+ * Custom version classes. By passing a <tt>:class_name</tt> option to the <tt>versioned</tt> method, you can specify your own ActiveRecord version model. <tt>VestalVersions::Version</tt> is the default, but feel free to stray from that. I recommend that your custom model inherit from <tt>VestalVersions::Version</tt>, but that's up to you!
174
+
175
+ * A <tt>versioned?</tt> convenience class method. If your user model is versioned, <tt>User.versioned?</tt> will let you know.
176
+
177
+ * Soft Deletes & Restoration. By setting <tt>:dependent</tt> to <tt>:tracking</tt> destroys will be tracked. On destroy a new version will be created storing the complete details of the object with a tag of 'deleted'. The object can later be restored using the <tt>restore!</tt> method on the VestalVersions::Version record. The attributes of the restored object will be set using the attribute writer methods. After a restore! is performed the version record with the 'deleted' tag is removed from the history.
178
+
179
+ class User < ActiveRecord::Base
180
+ versioned :dependent => :tracking
181
+ end
182
+
183
+ >> @user.version
184
+ => 2
185
+ >> @user.destroy
186
+ => <User id: 2, first_name: "Steve", last_name: "Jobs", ... >
187
+ >> User.find(2)
188
+ => ActiveRecord::RecordNotFound: Couldn't find User with ID=2
189
+ >> VestalVersions::Version.last
190
+ => <VestalVersions::Version id: 4, versioned_id: 2, versioned_type: "User", user_id: nil, user_type: nil, user_name: nil, modifications: {"created_at"=>Sun Aug 01 18:39:57 UTC 2010, "updated_at"=>Sun Aug 01 18:42:28 UTC 2010, "id"=>2, "last_name"=>"Jobs", "first_name"=>"Stephen"}, number: 3, tag: "deleted", created_at: "2010-08-01 18:42:43", updated_at: "2010-08-01 18:42:43">
191
+ >> VestalVersions::Version.last.restore!
192
+ => <User id: 2, first_name => "Steven", last_name: "Jobs", ... >
193
+ >> @user = User.find(2)
194
+ => <User id: 2, first_name => "Steven", last_name: "Jobs", ... >
195
+ >> @user.version
196
+ => 2
197
+
198
+ == Thanks!
199
+
200
+ Thank you to all those who post {issues and suggestions}[http://github.com/laserlemon/vestal_versions/issues]. And special thanks to:
201
+
202
+ * splattael[http://github.com/splattael], who first bugged (and helped) me to write some tests for this thing
203
+ * snaury[http://github.com/snaury], who helped out early on with the <tt>between</tt> association method, the <tt>:dependent</tt> option and a conflict from using a method called <tt>changes</tt>
204
+ * sthapit[http://github.com/sthapit], who was responsible for the <tt>:only</tt> and <tt>:except</tt> options as well as showing me that I'm a dummy for storing a useless first version
205
+
206
+ To contribute to <tt>vestal_versions</tt>, please fork, hack away in the integration[http://github.com/laserlemon/vestal_versions/tree/integration] branch and send me a pull request. Remember your tests!
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,10 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'activerecord', '~> 3.0.0'
4
+
5
+ gemspec :path => '..'
6
+
7
+ group :test do
8
+ gem 'rspec', '~> 2.0'
9
+ gem 'sqlite3', '~> 1.0'
10
+ end
@@ -0,0 +1,10 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'activerecord', '~> 3.1.0'
4
+
5
+ gemspec :path => '..'
6
+
7
+ group :test do
8
+ gem 'rspec', '~> 2.0'
9
+ gem 'sqlite3', '~> 1.0'
10
+ end
@@ -0,0 +1,10 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'activerecord', '~> 3.2.0'
4
+
5
+ gemspec :path => '..'
6
+
7
+ group :test do
8
+ gem 'rspec', '~> 2.0'
9
+ gem 'sqlite3', '~> 1.0'
10
+ end
@@ -0,0 +1,10 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'activerecord', '~> 4.0.0'
4
+
5
+ gemspec :path => '..'
6
+
7
+ group :test do
8
+ gem 'rspec', '~> 2.0'
9
+ gem 'sqlite3', '~> 1.0'
10
+ end
@@ -0,0 +1,17 @@
1
+ require 'generators/vestal_versions'
2
+ require 'rails/generators/active_record'
3
+
4
+ module VestalVersions
5
+ module Generators
6
+ class MigrationGenerator < ActiveRecord::Generators::Base
7
+ extend Base
8
+
9
+ argument :name, :type => :string, :default => 'create_vestal_versions'
10
+
11
+ def generate_files
12
+ migration_template 'migration.rb', "db/migrate/#{name}"
13
+ template 'initializer.rb', 'config/initializers/vestal_versions.rb'
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,9 @@
1
+ VestalVersions.configure do |config|
2
+ # Place any global options here. For example, in order to specify your own version model to use
3
+ # throughout the application, simply specify:
4
+ #
5
+ # config.class_name = "MyCustomVersion"
6
+ #
7
+ # Any options passed to the "versioned" method in the model itself will override this global
8
+ # configuration.
9
+ end
@@ -0,0 +1,28 @@
1
+ class CreateVestalVersions < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :versions do |t|
4
+ t.belongs_to :versioned, :polymorphic => true
5
+ t.belongs_to :user, :polymorphic => true
6
+ t.string :user_name
7
+ t.text :modifications
8
+ t.integer :number
9
+ t.integer :reverted_from
10
+ t.string :tag
11
+
12
+ t.timestamps
13
+ end
14
+
15
+ change_table :versions do |t|
16
+ t.index [:versioned_id, :versioned_type]
17
+ t.index [:user_id, :user_type]
18
+ t.index :user_name
19
+ t.index :number
20
+ t.index :tag
21
+ t.index :created_at
22
+ end
23
+ end
24
+
25
+ def self.down
26
+ drop_table :versions
27
+ end
28
+ end
@@ -0,0 +1,11 @@
1
+ require 'rails/generators/named_base'
2
+
3
+ module VestalVersions
4
+ module Generators
5
+ module Base
6
+ def source_root
7
+ @_vestal_versions_source_root ||= File.expand_path(File.join('../vestal_versions', generator_name, 'templates'), __FILE__)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,121 @@
1
+ module VestalVersions
2
+ # Provides the ability to manipulate hashes in the specific format that ActiveRecord gives to
3
+ # dirty attribute changes: string keys and unique, two-element array values.
4
+ module Changes
5
+ extend ActiveSupport::Concern
6
+ included do
7
+ Hash.class_eval{ include HashMethods }
8
+
9
+ after_update :merge_version_changes
10
+ end
11
+
12
+ # Methods available to versioned ActiveRecord::Base instances in order to manage changes used
13
+ # for version creation.
14
+
15
+ # Collects an array of changes from a record's versions between the given range and compiles
16
+ # them into one summary hash of changes. The +from+ and +to+ arguments can each be either a
17
+ # version number, a symbol representing an association proxy method, a string representing a
18
+ # version tag or a version object itself.
19
+ def changes_between(from, to)
20
+ from_number, to_number = versions.number_at(from), versions.number_at(to)
21
+ return {} if from_number == to_number
22
+ chain = versions.between(from_number, to_number).reject(&:initial?)
23
+ return {} if chain.empty?
24
+
25
+ backward = from_number > to_number
26
+ backward ? chain.pop : chain.shift unless from_number == 1 || to_number == 1
27
+
28
+ chain.inject({}) do |changes, version|
29
+ changes.append_changes!(backward ? version.changes.reverse_changes : version.changes)
30
+ end
31
+ end
32
+
33
+ private
34
+ # Before a new version is created, the newly-changed attributes are appended onto a hash
35
+ # of previously-changed attributes. Typically the previous changes will be empty, except in
36
+ # the case that a control block is used where versions are to be merged. See
37
+ # VestalVersions::Control for more information.
38
+ def merge_version_changes
39
+ version_changes.append_changes!(incremental_version_changes)
40
+ end
41
+
42
+ # Stores the cumulative changes that are eventually used for version creation.
43
+ def version_changes
44
+ @version_changes ||= {}
45
+ end
46
+
47
+ # Stores the incremental changes that are appended to the cumulative changes before version
48
+ # creation. Incremental changes are reset when the record is saved because they represent
49
+ # a subset of the dirty attribute changes, which are reset upon save.
50
+ def incremental_version_changes
51
+ changes.slice(*versioned_columns)
52
+ end
53
+
54
+ # Simply resets the cumulative changes after version creation.
55
+ def reset_version_changes
56
+ @version_changes = nil
57
+ end
58
+
59
+ # Instance methods included into Hash for dealing with manipulation of hashes in the specific
60
+ # format of ActiveRecord::Base#changes.
61
+ module HashMethods
62
+ # When called on a hash of changes and given a second hash of changes as an argument,
63
+ # +append_changes+ will run the second hash on top of the first, updating the last element
64
+ # of each array value with its own, or creating its own key/value pair for missing keys.
65
+ # Resulting non-unique array values are removed.
66
+ #
67
+ # == Example
68
+ #
69
+ # first = {
70
+ # "first_name" => ["Steve", "Stephen"],
71
+ # "age" => [25, 26]
72
+ # }
73
+ # second = {
74
+ # "first_name" => ["Stephen", "Steve"],
75
+ # "last_name" => ["Richert", "Jobs"],
76
+ # "age" => [26, 54]
77
+ # }
78
+ # first.append_changes(second)
79
+ # # => {
80
+ # "last_name" => ["Richert", "Jobs"],
81
+ # "age" => [25, 54]
82
+ # }
83
+ def append_changes(changes)
84
+ changes.inject(self) do |new_changes, (attribute, change)|
85
+ new_change = [new_changes.fetch(attribute, change).first, change.last]
86
+ new_changes.merge(attribute => new_change)
87
+ end.reject do |attribute, change|
88
+ change.first == change.last
89
+ end
90
+ end
91
+
92
+ # Destructively appends a given hash of changes onto an existing hash of changes.
93
+ def append_changes!(changes)
94
+ replace(append_changes(changes))
95
+ end
96
+
97
+ # Appends the existing hash of changes onto a given hash of changes. Relates to the
98
+ # +append_changes+ method in the same way that Hash#reverse_merge relates to
99
+ # Hash#merge.
100
+ def prepend_changes(changes)
101
+ changes.append_changes(self)
102
+ end
103
+
104
+ # Destructively prepends a given hash of changes onto an existing hash of changes.
105
+ def prepend_changes!(changes)
106
+ replace(prepend_changes(changes))
107
+ end
108
+
109
+ # Reverses the array values of a hash of changes. Useful for reversion both backward and
110
+ # forward through a record's history of changes.
111
+ def reverse_changes
112
+ inject({}){|nc,(a,c)| nc.merge!(a => c.reverse) }
113
+ end
114
+
115
+ # Destructively reverses the array values of a hash of changes.
116
+ def reverse_changes!
117
+ replace(reverse_changes)
118
+ end
119
+ end
120
+ end
121
+ end
@@ -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
+
29
+
30
+ private
31
+ # After first determining whether the <tt>:if</tt> and <tt>:unless</tt> conditions are
32
+ # satisfied, the original, unaliased +create_version?+ method is called to determine
33
+ # whether a new version should be created upon update of the ActiveRecord::Base instance.
34
+ def create_version?
35
+ version_conditions_met? && super
36
+ end
37
+
38
+ # After first determining whether the <tt>:if</tt> and <tt>:unless</tt> conditions are
39
+ # satisfied, the original, unaliased +update_version?+ method is called to determine
40
+ # whther the last version should be updated to include changes merged from the current
41
+ # ActiveRecord::Base instance update.
42
+ #
43
+ # The overridden +update_version?+ method simply returns false, effectively delegating
44
+ # the decision to whether the <tt>:if</tt> and <tt>:unless</tt> conditions are met.
45
+ def update_version?
46
+ version_conditions_met? && super
47
+ end
48
+
49
+ # Simply checks whether the <tt>:if</tt> and <tt>:unless</tt> conditions given in the
50
+ # +versioned+ options are met: meaning that all procs in the <tt>:if</tt> array must
51
+ # evaluate to a non-false, non-nil value and that all procs in the <tt>:unless</tt> array
52
+ # must all evaluate to either false or nil.
53
+ def version_conditions_met?
54
+ vestal_versions_options[:if].all?{|p| p.call(self) } && !vestal_versions_options[:unless].any?{|p| p.call(self) }
55
+ end
56
+ end
57
+ end