houston-vestal_versions 2.0.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 (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
@@ -0,0 +1,27 @@
1
+ module VestalVersions
2
+ # Simply adds a flag to determine whether a model class if versioned.
3
+ module Versioned
4
+ extend ActiveSupport::Concern
5
+
6
+ # Overrides the +versioned+ method to first define the +versioned?+ class method before
7
+ # deferring to the original +versioned+.
8
+ module ClassMethods
9
+ def versioned(*args)
10
+ super(*args)
11
+
12
+ class << self
13
+ def versioned?
14
+ true
15
+ end
16
+ end
17
+ end
18
+
19
+ # For all ActiveRecord::Base models that do not call the +versioned+ method, the +versioned?+
20
+ # method will return false.
21
+ def versioned?
22
+ false
23
+ end
24
+ end
25
+
26
+ end
27
+ end
@@ -0,0 +1,89 @@
1
+ # See: https://github.com/rails/rails/issues/11026
2
+ if ActiveRecord::VERSION::MAJOR == 3 && ActiveRecord::VERSION::MINOR == 0 && RUBY_VERSION >= "2.0.0"
3
+ module ActiveRecord
4
+ module Associations
5
+ class AssociationProxy
6
+ def send(method, *args)
7
+ if proxy_respond_to?(method, true)
8
+ super
9
+ else
10
+ load_target
11
+ @target.send(method, *args)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ module VestalVersions
20
+ # An extension module for the +has_many+ association with versions.
21
+ module Versions
22
+ # Returns all versions between (and including) the two given arguments. See documentation for
23
+ # the +at+ extension method for what arguments are valid. If either of the given arguments is
24
+ # invalid, an empty array is returned.
25
+ #
26
+ # The +between+ method preserves returns an array of version records, preserving the order
27
+ # given by the arguments. If the +from+ value represents a version before that of the +to+
28
+ # value, the array will be ordered from earliest to latest. The reverse is also true.
29
+ def between(from, to)
30
+ from_number, to_number = number_at(from), number_at(to)
31
+ return [] if from_number.nil? || to_number.nil?
32
+
33
+ condition = (from_number == to_number) ? to_number : Range.new(*[from_number, to_number].sort)
34
+ where(:number => condition).order("#{table_name}.#{connection.quote_column_name('number')} #{(from_number > to_number) ? 'DESC' : 'ASC'}").to_a
35
+ end
36
+
37
+ # Returns all version records created before the version associated with the given value.
38
+ def before(value)
39
+ return [] if (number = number_at(value)).nil?
40
+ where("#{table_name}.#{connection.quote_column_name('number')} < #{number}").to_a
41
+ end
42
+
43
+ # Returns all version records created after the version associated with the given value.
44
+ #
45
+ # This is useful for dissociating records during use of the +reset_to!+ method.
46
+ def after(value)
47
+ return [] if (number = number_at(value)).nil?
48
+ where("#{table_name}.#{connection.quote_column_name('number')} > #{number}").to_a
49
+ end
50
+
51
+ # Returns a single version associated with the given value. The following formats are valid:
52
+ # * A Date or Time object: When given, +to_time+ is called on the value and the last version
53
+ # record in the history created before (or at) that time is returned.
54
+ # * A Numeric object: Typically a positive integer, these values correspond to version numbers
55
+ # and the associated version record is found by a version number equal to the given value
56
+ # rounded down to the nearest integer.
57
+ # * A String: A string value represents a version tag and the associated version is searched
58
+ # for by a matching tag value. *Note:* Be careful with string representations of numbers.
59
+ # * A Symbol: Symbols represent association class methods on the +has_many+ versions
60
+ # association. While all of the built-in association methods require arguments, additional
61
+ # extension modules can be defined using the <tt>:extend</tt> option on the +versioned+
62
+ # method. See the +versioned+ documentation for more information.
63
+ # * A Version object: If a version object is passed to the +at+ method, it is simply returned
64
+ # untouched.
65
+ def at(value)
66
+ case value
67
+ when Date, Time then where("#{table_name}.created_at <= ?", value.to_time).last
68
+ when Numeric then find_by_number(value.floor)
69
+ when String then find_by_tag(value)
70
+ when Symbol then respond_to?(value) ? send(value) : nil
71
+ when Version then value
72
+ end
73
+ end
74
+
75
+ # Returns the version number associated with the given value. In many cases, this involves
76
+ # simply passing the value to the +at+ method and then returning the subsequent version number.
77
+ # Hoever, for Numeric values, the version number can be returned directly and for Date/Time
78
+ # values, a default value of 1 is given to ensure that times prior to the first version
79
+ # still return a valid version number (useful for reversion).
80
+ def number_at(value)
81
+ case value
82
+ when Date, Time then (v = at(value)) ? v.number : 1
83
+ when Numeric then value.floor
84
+ when String, Symbol then (v = at(value)) ? v.number : nil
85
+ when Version then value.number
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,126 @@
1
+ require 'active_support/concern'
2
+ require 'active_support/dependencies/autoload'
3
+ require 'active_support/core_ext/module/delegation'
4
+ require 'active_record'
5
+
6
+ # +vestal_versions+ keeps track of updates to ActiveRecord models, leveraging the introduction of
7
+ # dirty attributes in Rails 2.1. By storing only the updated attributes in a serialized column of a
8
+ # single version model, the history is kept DRY and no additional schema changes are necessary.
9
+ #
10
+ # Author:: Steve Richert
11
+ # Copyright:: Copyright (c) 2009 Steve Richert
12
+ # License:: MIT License (http://www.opensource.org/licenses/mit-license.php)
13
+ #
14
+ # To enable versioning on a model, simply use the +versioned+ method:
15
+ #
16
+ # class User < ActiveRecord::Base
17
+ # versioned
18
+ # end
19
+ #
20
+ # user = User.create(:name => "Steve Richert")
21
+ # user.version # => 1
22
+ # user.update_attribute(:name, "Steve Jobs")
23
+ # user.version # => 2
24
+ # user.revert_to(1)
25
+ # user.name # => "Steve Richert"
26
+ #
27
+ # See the +versioned+ documentation for more details.
28
+
29
+ # The base module that gets included in ActiveRecord::Base. See the documentation for
30
+ # VestalVersions::ClassMethods for more useful information.
31
+ module VestalVersions
32
+ extend ActiveSupport::Concern
33
+ extend ActiveSupport::Autoload
34
+
35
+ autoload :Changes
36
+ autoload :Conditions
37
+ autoload :Control
38
+ autoload :Creation
39
+ autoload :Deletion
40
+ autoload :Options
41
+ autoload :Reload
42
+ autoload :Reset
43
+ autoload :Reversion
44
+ autoload :Users
45
+ autoload :Version
46
+ autoload :VERSION, 'vestal_versions/version_num'
47
+ autoload :VersionTagging
48
+ autoload :Versioned
49
+ autoload :Versions
50
+
51
+ class << self
52
+ delegate :config, :configure, :to => Version
53
+ end
54
+
55
+ included do
56
+ include Versioned
57
+ end
58
+
59
+ module ClassMethods
60
+ # +versioned+ associates an ActiveRecord model with many versions. When the object is updated,
61
+ # a new version containing the changes is created. There are several options available to the
62
+ # +versioned+ method, most of which are passed to the +has_many+ association itself:
63
+ # * <tt>:class_name</tt>: The class name of the version model to use for the association. By
64
+ # default, this is set to "VestalVersions::Version", representing the built-in version class.
65
+ # By specifying this option, you can override the version class, to include custom version
66
+ # behavior. It's recommended that a custom version inherit from VestalVersions::Version.
67
+ # * <tt>:dependent</tt>: Also common to +has_many+ associations, this describes the behavior of
68
+ # version records when the parent object is destroyed. This defaults to :delete_all, which
69
+ # will permanently remove all associated versions *without* triggering any destroy callbacks.
70
+ # Other options are :destroy which removes the associated versions *with* callbacks, or
71
+ # :nullify which leaves the version records in the database, but dissociates them from the
72
+ # parent object by setting the foreign key columns to +nil+ values. Setting this option to
73
+ # :tracking will perform a soft delete on destroy and create a new version record preserving
74
+ # details of this record for later restoration.
75
+ # * <tt>:except</tt>: An update will trigger version creation as long as at least one column
76
+ # outside those specified here was updated. Also, upon version creation, the columns
77
+ # specified here will be excluded from the change history. This is useful when dealing with
78
+ # unimportant, constantly changing, or sensitive information. This option accepts a symbol,
79
+ # string or an array of either, representing column names to exclude. It is completely
80
+ # optional and defaults to +nil+, allowing all columns to be versioned. This option is also
81
+ # ignored if the +only+ option is used.
82
+ # * <tt>:extend</tt>: This option allows you to extend the +has_many+ association proxy with a
83
+ # module or an array of modules. Any methods defined in those modules become available on the
84
+ # +versions+ association. The VestalVersions::Versions module is essential to the
85
+ # functionality of +vestal_versions+ and so is prepended to any additional modules that you
86
+ # might specify here.
87
+ # * <tt>:if</tt>: Accepts a symbol, a proc or an array of either to be evaluated when the parent
88
+ # object is updated to determine whether a new version should be created. +to_proc+ is called
89
+ # on any symbols given and the resulting procs are called, passing in the object itself. If
90
+ # an array is given, all must be evaluate to +true+ in order for a version to be created.
91
+ # * <tt>:initial_version</tt>: When set to true, an initial version is always created when the
92
+ # parent object is created. This initial version will have nil changes however it can be
93
+ # used to store who created the original version.
94
+ # * <tt>:only</tt>: An update will trigger version creation as long as at least one updated
95
+ # column falls within those specified here. Also, upon version creation, only the columns
96
+ # specified here will be included in the change history. This option accepts a symbol, string
97
+ # or an array of either, representing column names to include. It is completely optional and
98
+ # defaults to +nil+, allowing all columns to be versioned. This option takes precedence over
99
+ # the +except+ option if both are specified.
100
+ # * <tt>:unless</tt>: Accepts a symbol, a proc or an array of either to be evaluated when the
101
+ # parent object is updated to determine whether version creation should be skipped. +to_proc+
102
+ # is called on any symbols given and the resulting procs are called, passing in the object
103
+ # itself. If an array is given and any element evaluates as +true+, the version creation will
104
+ # be skipped.
105
+ def versioned(options = {}, &block)
106
+ return if versioned?
107
+
108
+ include Options
109
+ include Changes
110
+ include Creation
111
+ include Users
112
+ include Reversion
113
+ include Reset
114
+ include Conditions
115
+ include Control
116
+ include VersionTagging
117
+ include Reload
118
+ include Deletion
119
+
120
+ prepare_versioned_options(options)
121
+ has_many :versions, options, &block
122
+ end
123
+ end
124
+ end
125
+
126
+ ActiveRecord::Base.class_eval{ include VestalVersions }
@@ -0,0 +1,28 @@
1
+ if ENV['COVERAGE']
2
+ require 'coveralls'
3
+ Coveralls.wear!
4
+ end
5
+
6
+ require 'vestal_versions'
7
+
8
+ require 'bundler'
9
+ Bundler.require(:test)
10
+
11
+ RSpec.configure do |c|
12
+ c.before(:suite) do
13
+ CreateSchema.suppress_messages{ CreateSchema.migrate(:up) }
14
+ end
15
+
16
+ c.after(:suite) do
17
+ FileUtils.rm_rf(File.expand_path('../test.db', __FILE__))
18
+ end
19
+
20
+ c.after(:each) do
21
+ VestalVersions::Version.config.clear
22
+ User.prepare_versioned_options({})
23
+ end
24
+
25
+ c.order = 'random'
26
+ end
27
+
28
+ Dir[File.expand_path('../support/*.rb', __FILE__)].each{|f| require f }
@@ -0,0 +1,19 @@
1
+ class User < ActiveRecord::Base
2
+ versioned
3
+
4
+ def name
5
+ [first_name, last_name].compact.join(' ')
6
+ end
7
+
8
+ def name= names
9
+ self[:first_name], self[:last_name] = names.split(' ', 2)
10
+ end
11
+ end
12
+
13
+ class DeletedUser < ActiveRecord::Base
14
+ self.table_name = 'users'
15
+ versioned :dependent => :tracking
16
+ end
17
+
18
+ class MyCustomVersion < VestalVersions::Version
19
+ end
@@ -0,0 +1,25 @@
1
+ ActiveRecord::Base.establish_connection(
2
+ :adapter => 'sqlite3',
3
+ :database => File.expand_path('../../test.db', __FILE__)
4
+ )
5
+
6
+ class CreateSchema < ActiveRecord::Migration
7
+ def self.up
8
+ create_table :users, :force => true do |t|
9
+ t.string :first_name
10
+ t.string :last_name
11
+ t.timestamps
12
+ end
13
+
14
+ create_table :versions, :force => true do |t|
15
+ t.belongs_to :versioned, :polymorphic => true
16
+ t.belongs_to :user, :polymorphic => true
17
+ t.string :user_name
18
+ t.text :modifications
19
+ t.integer :number
20
+ t.integer :reverted_from
21
+ t.string :tag
22
+ t.timestamps
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,134 @@
1
+ require 'spec_helper'
2
+
3
+ describe VestalVersions::Changes do
4
+ context "a version's changes" do
5
+ let(:user){ User.create(:name => 'Steve Richert') }
6
+ subject{ user.versions.last.changes }
7
+
8
+ before do
9
+ user.update_attribute(:last_name, 'Jobs')
10
+ end
11
+
12
+ it { should be_a(Hash) }
13
+ it { should_not be_empty }
14
+
15
+ it 'has string keys' do
16
+ subject.keys.each{ |key| key.should be_a(String) }
17
+ end
18
+
19
+ it 'has two-element array values' do
20
+ subject.values.each do |key|
21
+ key.should be_a(Array)
22
+ key.size.should == 2
23
+ end
24
+ end
25
+
26
+ it 'has unique-element values' do
27
+ subject.values.each{ |v| v.uniq.should == v }
28
+ end
29
+
30
+ it "equals the model's changes" do
31
+ user.first_name = 'Stephen'
32
+ model_changes = user.changes
33
+ user.save
34
+ changes = user.versions.last.changes
35
+
36
+ model_changes.should == changes
37
+ end
38
+ end
39
+
40
+ context 'a hash of changes' do
41
+ let(:changes){ {'first_name' => ['Steve', 'Stephen']} }
42
+ let(:other){ {'first_name' => ['Catie', 'Catherine']} }
43
+
44
+ it 'properly appends other changes' do
45
+ expected = {'first_name' => ['Steve', 'Catherine']}
46
+
47
+ changes.append_changes(other).should == expected
48
+
49
+ changes.append_changes!(other)
50
+ changes.should == expected
51
+ end
52
+
53
+ it 'properly prepends other changes' do
54
+ expected = {'first_name' => ['Catie', 'Stephen']}
55
+
56
+ changes.prepend_changes(other).should == expected
57
+
58
+ changes.prepend_changes!(other)
59
+ changes.should == expected
60
+ end
61
+
62
+ it 'is reversible' do
63
+ expected = {'first_name' => ['Stephen', 'Steve']}
64
+
65
+ changes.reverse_changes.should == expected
66
+
67
+ changes.reverse_changes!
68
+ changes.should == expected
69
+ end
70
+ end
71
+
72
+ context 'the changes between two versions' do
73
+ let(:name){ 'Steve Richert' }
74
+ let(:user){ User.create(:name => name) } # 1
75
+ let(:version){ user.version }
76
+
77
+ before do
78
+ user.update_attribute(:last_name, 'Jobs') # 2
79
+ user.update_attribute(:first_name, 'Stephen') # 3
80
+ user.update_attribute(:last_name, 'Richert') # 4
81
+ user.update_attribute(:name, name) # 5
82
+ end
83
+
84
+ it 'is a hash' do
85
+ 1.upto(version) do |i|
86
+ 1.upto(version) do |j|
87
+ user.changes_between(i, j).should be_a(Hash)
88
+ end
89
+ end
90
+ end
91
+
92
+ it 'has string keys' do
93
+ 1.upto(version) do |i|
94
+ 1.upto(version) do |j|
95
+ user.changes_between(i, j).keys.each{ |key| key.should be_a(String) }
96
+ end
97
+ end
98
+ end
99
+
100
+ it 'has two-element arrays with unique values' do
101
+ 1.upto(version) do |i|
102
+ 1.upto(version) do |j|
103
+ user.changes_between(i, j).values.each do |value|
104
+ value.should be_a(Array)
105
+ value.size.should == 2
106
+ value.uniq.should == value
107
+ end
108
+ end
109
+ end
110
+ end
111
+
112
+ it 'is empty between identical versions' do
113
+ user.changes_between(1, version).should be_empty
114
+ user.changes_between(version, 1).should be_empty
115
+ end
116
+
117
+ it 'is should reverse with direction' do
118
+ 1.upto(version) do |i|
119
+ i.upto(version) do |j|
120
+ up = user.changes_between(i, j)
121
+ down = user.changes_between(j, i)
122
+ up.should == down.reverse_changes
123
+ end
124
+ end
125
+ end
126
+
127
+ it 'is empty with invalid arguments' do
128
+ 1.upto(version) do |i|
129
+ user.changes_between(i, nil).should be_blank
130
+ user.changes_between(nil, i).should be_blank
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,103 @@
1
+ require 'spec_helper'
2
+
3
+ describe VestalVersions::Conditions do
4
+ shared_examples_for 'a conditional option' do |option|
5
+ before do
6
+ User.class_eval do
7
+ def true; true; end
8
+ end
9
+ end
10
+
11
+ it 'is an array' do
12
+ User.vestal_versions_options[option].should be_a(Array)
13
+ User.prepare_versioned_options(option => :true)
14
+ User.vestal_versions_options[option].should be_a(Array)
15
+ end
16
+
17
+ it 'has proc values' do
18
+ User.prepare_versioned_options(option => :true)
19
+ User.vestal_versions_options[option].each{|i| i.should be_a(Proc) }
20
+ end
21
+ end
22
+
23
+ it_should_behave_like 'a conditional option', :if
24
+ it_should_behave_like 'a conditional option', :unless
25
+
26
+ context 'a new version' do
27
+ subject{ User.create(:name => 'Steve Richert') }
28
+ let(:count){ subject.versions.count }
29
+
30
+ before do
31
+ User.class_eval do
32
+ def true; true; end
33
+ def false; false; end
34
+ end
35
+ count # memoize this value
36
+ end
37
+
38
+ after do
39
+ User.prepare_versioned_options(:if => [], :unless => [])
40
+ end
41
+
42
+ context 'with :if conditions' do
43
+ context 'that pass' do
44
+ before do
45
+ User.prepare_versioned_options(:if => [:true])
46
+ subject.update_attribute(:last_name, 'Jobs')
47
+ end
48
+
49
+ its('versions.count'){ should == count + 1 }
50
+ end
51
+
52
+ context 'that fail' do
53
+ before do
54
+ User.prepare_versioned_options(:if => [:false])
55
+ subject.update_attribute(:last_name, 'Jobs')
56
+ end
57
+
58
+ its('versions.count'){ should == count }
59
+ end
60
+ end
61
+
62
+ context 'with :unless conditions' do
63
+ context 'that pass' do
64
+ before do
65
+ User.prepare_versioned_options(:unless => [:true])
66
+ subject.update_attribute(:last_name, 'Jobs')
67
+ end
68
+
69
+ its('versions.count'){ should == count }
70
+ end
71
+
72
+ context 'that fail' do
73
+ before do
74
+ User.prepare_versioned_options(:unless => [:false])
75
+ subject.update_attribute(:last_name, 'Jobs')
76
+ end
77
+
78
+ its('versions.count'){ should == count + 1 }
79
+ end
80
+ end
81
+
82
+ context 'with :if and :unless conditions' do
83
+ context 'that pass' do
84
+ before do
85
+ User.prepare_versioned_options(:if => [:true], :unless => [:true])
86
+ subject.update_attribute(:last_name, 'Jobs')
87
+ end
88
+
89
+ its('versions.count'){ should == count }
90
+ end
91
+
92
+ context 'that fail' do
93
+ before do
94
+ User.prepare_versioned_options(:if => [:false], :unless => [:false])
95
+ subject.update_attribute(:last_name, 'Jobs')
96
+ end
97
+
98
+ its('versions.count'){ should == count }
99
+ end
100
+ end
101
+
102
+ end
103
+ end
@@ -0,0 +1,120 @@
1
+ require 'spec_helper'
2
+
3
+ describe VestalVersions::Control do
4
+ let(:user){ User.create(:name => 'Steve Richert') }
5
+ let(:other_user){ User.create(:name => 'Michael Rossin') }
6
+ before do
7
+ @count = user.versions.count
8
+ end
9
+
10
+ shared_examples_for 'a version preserver' do |method|
11
+ it 'creates one version with a model update' do
12
+ user.send(method){ user.update_attribute(:last_name, 'Jobs') }
13
+
14
+ user.versions.count.should == @count
15
+ end
16
+
17
+ it 'creates one version with multiple model updates' do
18
+ user.send(method) do
19
+ user.update_attribute(:first_name, 'Stephen')
20
+ user.update_attribute(:last_name, 'Jobs')
21
+ user.update_attribute(:first_name, 'Steve')
22
+ end
23
+
24
+ user.versions.count.should == @count
25
+ end
26
+
27
+ end
28
+
29
+ shared_examples_for 'a version incrementer' do |method|
30
+ it 'creates one version with a model update' do
31
+ user.send(method){ user.update_attribute(:last_name, 'Jobs') }
32
+
33
+ user.versions.count.should == @count + 1
34
+ end
35
+
36
+ it 'creates one version with multiple model updates' do
37
+ user.send(method) do
38
+ user.update_attribute(:first_name, 'Stephen')
39
+ user.update_attribute(:last_name, 'Jobs')
40
+ user.update_attribute(:first_name, 'Steve')
41
+ end
42
+
43
+ user.versions.count.should == @count + 1
44
+ end
45
+
46
+ end
47
+
48
+ it_should_behave_like 'a version preserver', :skip_version
49
+ it_should_behave_like 'a version incrementer', :merge_version
50
+
51
+ context "when operating on the class level" do
52
+ before do
53
+ @count = user.versions.count
54
+ @other_user_count = other_user.versions.count
55
+ end
56
+ it 'skip_version doesn\' create versions on multiple models' do
57
+ other_user_count = other_user.versions.count
58
+
59
+ User.skip_version do
60
+ user.update_attribute(:first_name, 'Stephen')
61
+ user.update_attribute(:last_name, 'Jobs')
62
+ user.update_attribute(:first_name, 'Steve')
63
+
64
+ other_user.update_attribute(:first_name, 'Stephen')
65
+ other_user.update_attribute(:last_name, 'Jobs')
66
+ other_user.update_attribute(:first_name, 'Steve')
67
+ end
68
+ user.versions.count.should == @count
69
+ other_user.versions.count.should == @other_user_count
70
+ end
71
+
72
+ end
73
+
74
+ context 'within a append_version block' do
75
+
76
+ context 'when no versions exist' do
77
+ it_should_behave_like 'a version incrementer', :append_version
78
+ end
79
+
80
+ context 'when versions exist' do
81
+ let(:last_version){ user.versions.last }
82
+
83
+ before do
84
+ user.update_attribute(:last_name, 'Jobs')
85
+ user.update_attribute(:last_name, 'Richert')
86
+
87
+ @count = user.versions.count
88
+ end
89
+
90
+ it_should_behave_like 'a version preserver', :append_version
91
+
92
+ it "updates the last version with one update" do
93
+ original_id = last_version.id
94
+ original_attrs = last_version.attributes
95
+
96
+ user.append_version{ user.update_attribute(:last_name, 'Jobs') }
97
+
98
+ other_last_version = user.versions(true).last
99
+ other_last_version.id.should == original_id
100
+ other_last_version.attributes.should_not == original_attrs
101
+ end
102
+
103
+ it "updates the last version with multiple updates" do
104
+ original_id = last_version.id
105
+ original_attrs = last_version.attributes
106
+
107
+ user.append_version do
108
+ user.update_attribute(:first_name, 'Stephen')
109
+ user.update_attribute(:last_name, 'Jobs')
110
+ user.update_attribute(:first_name, 'Steve')
111
+ end
112
+
113
+ other_last_version = user.versions(true).last
114
+ other_last_version.id.should == original_id
115
+ other_last_version.attributes.should_not == original_attrs
116
+ end
117
+
118
+ end
119
+ end
120
+ end