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.
- checksums.yaml +7 -0
- data/.gitignore +22 -0
- data/.travis.yml +22 -0
- data/CHANGELOG.md +7 -0
- data/Gemfile +10 -0
- data/LICENSE +20 -0
- data/README.rdoc +206 -0
- data/Rakefile +6 -0
- data/gemfiles/activerecord_3_0.gemfile +10 -0
- data/gemfiles/activerecord_3_1.gemfile +10 -0
- data/gemfiles/activerecord_3_2.gemfile +10 -0
- data/gemfiles/activerecord_4_0.gemfile +10 -0
- data/lib/generators/vestal_versions/migration/migration_generator.rb +17 -0
- data/lib/generators/vestal_versions/migration/templates/initializer.rb +9 -0
- data/lib/generators/vestal_versions/migration/templates/migration.rb +28 -0
- data/lib/generators/vestal_versions.rb +11 -0
- data/lib/vestal_versions/changes.rb +121 -0
- data/lib/vestal_versions/conditions.rb +57 -0
- data/lib/vestal_versions/control.rb +199 -0
- data/lib/vestal_versions/creation.rb +93 -0
- data/lib/vestal_versions/deletion.rb +37 -0
- data/lib/vestal_versions/options.rb +41 -0
- data/lib/vestal_versions/reload.rb +16 -0
- data/lib/vestal_versions/reset.rb +24 -0
- data/lib/vestal_versions/reversion.rb +81 -0
- data/lib/vestal_versions/users.rb +54 -0
- data/lib/vestal_versions/version.rb +84 -0
- data/lib/vestal_versions/version_tagging.rb +51 -0
- data/lib/vestal_versions/versioned.rb +27 -0
- data/lib/vestal_versions/versions.rb +89 -0
- data/lib/vestal_versions.rb +126 -0
- data/spec/spec_helper.rb +28 -0
- data/spec/support/models.rb +19 -0
- data/spec/support/schema.rb +25 -0
- data/spec/vestal_versions/changes_spec.rb +134 -0
- data/spec/vestal_versions/conditions_spec.rb +103 -0
- data/spec/vestal_versions/control_spec.rb +120 -0
- data/spec/vestal_versions/creation_spec.rb +90 -0
- data/spec/vestal_versions/deletion_spec.rb +86 -0
- data/spec/vestal_versions/options_spec.rb +45 -0
- data/spec/vestal_versions/reload_spec.rb +18 -0
- data/spec/vestal_versions/reset_spec.rb +111 -0
- data/spec/vestal_versions/reversion_spec.rb +103 -0
- data/spec/vestal_versions/users_spec.rb +21 -0
- data/spec/vestal_versions/version_spec.rb +61 -0
- data/spec/vestal_versions/version_tagging_spec.rb +39 -0
- data/spec/vestal_versions/versioned_spec.rb +16 -0
- data/spec/vestal_versions/versions_spec.rb +176 -0
- data/vestal_versions.gemspec +23 -0
- 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 }
|
data/spec/spec_helper.rb
ADDED
@@ -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
|