has_many_versions 0.0.7
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.
- data/LICENSE +20 -0
- data/README.rdoc +132 -0
- data/Rakefile +43 -0
- data/VERSION.yml +4 -0
- data/lib/has_many_versions.rb +120 -0
- data/rails/init.rb +1 -0
- data/spec/add_spec.rb +36 -0
- data/spec/database.yml +2 -0
- data/spec/delete_spec.rb +20 -0
- data/spec/history_spec.rb +37 -0
- data/spec/rollback_spec.rb +63 -0
- data/spec/spec.opts +7 -0
- data/spec/spec_helper.rb +45 -0
- data/spec/update_spec.rb +60 -0
- metadata +74 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Joshua Hull
|
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,132 @@
|
|
1
|
+
= has_many_versions
|
2
|
+
|
3
|
+
This attempts to provide versioning for 'has many' relationships within ActiveRecord.
|
4
|
+
|
5
|
+
== Installation
|
6
|
+
|
7
|
+
script/plugin install git://github.com/joshbuddy/has_many_versions.git
|
8
|
+
|
9
|
+
== Usage
|
10
|
+
|
11
|
+
Here is a pretty typical relationship.
|
12
|
+
|
13
|
+
class Book < ActiveRecord::Base
|
14
|
+
belongs_to :author
|
15
|
+
validates_associated :author
|
16
|
+
end
|
17
|
+
|
18
|
+
class Author < ActiveRecord::Base
|
19
|
+
has_many :books
|
20
|
+
end
|
21
|
+
|
22
|
+
Lets say we want to include versioning now to modifications made to the collection of books. We can modify the +has_many+ relationship the following way:
|
23
|
+
|
24
|
+
has_many :books, :extend => HasManyVersions
|
25
|
+
|
26
|
+
Now changes made to the relationship with the normal <tt><<</tt> and +delete+ methods will be tracked. There are some modifications needed for both tables for this to work. The owner of the relationship (in this case +Author+) needs an integer column named +version+ (with a default value of one). The collection (in this case +Book+) needs two columns, +initial_version+ and +version+.
|
27
|
+
|
28
|
+
Assuming we've done all that, lets try out some magical versioning to see how it all works.
|
29
|
+
|
30
|
+
We'll start by defining some books.
|
31
|
+
the_eyre_affair = Book.new(:name => 'The Eyre Affair')
|
32
|
+
=> #<Book id: nil, initial_version: nil, version: nil, author_id: nil, name: "The Eyre Affair">
|
33
|
+
lost_in_a_good_book = Book.new(:name => 'Lost in a Good Book')
|
34
|
+
=> #<Book id: nil, initial_version: nil, version: nil, author_id: nil, name: "Lost in a Good Book">
|
35
|
+
the_well_of_lost_plots = Book.new(:name => 'The Well of Lost Plots')
|
36
|
+
=> #<Book id: nil, initial_version: nil, version: nil, author_id: nil, name: "The Well of Lost Plots">
|
37
|
+
something_rotten = Book.new(:name => 'Something Rotten')
|
38
|
+
=> #<Book id: nil, initial_version: nil, version: nil, author_id: nil, name: "Something Rotten">
|
39
|
+
|
40
|
+
And an author to take them.
|
41
|
+
jasper = Author.new(:name => 'Jasper Fforde')
|
42
|
+
=> #<Author id: nil, version: 1, name: "Jasper Fforde">
|
43
|
+
|
44
|
+
That sounds good. Just so we're all on the same page, how does Jasper look at the moment?
|
45
|
+
jasper.version
|
46
|
+
=> 1
|
47
|
+
jasper.books
|
48
|
+
=> []
|
49
|
+
|
50
|
+
Version 1 is where all versioned objects initially start.
|
51
|
+
|
52
|
+
Now, lets start adding some books.
|
53
|
+
jasper.books.push(the_eyre_affair, something_rotten)
|
54
|
+
|
55
|
+
And our new version?
|
56
|
+
jasper.version
|
57
|
+
=> 2
|
58
|
+
|
59
|
+
And our books?
|
60
|
+
jasper.books
|
61
|
+
=> [#<Book id: 1, initial_version: 2, version: 2, author_id: 1, name: "The Eyre Affair">,
|
62
|
+
#<Book id: 2, initial_version: 2, version: 2, author_id: 1, name: "Something Rotten">]
|
63
|
+
|
64
|
+
Yeah! Everything is good and green.
|
65
|
+
|
66
|
+
Lets add on some more books.
|
67
|
+
jasper.books.push(lost_in_a_good_book)
|
68
|
+
jasper.version
|
69
|
+
=> 3
|
70
|
+
jasper.books.push(the_well_of_lost_plots)
|
71
|
+
jasper.version
|
72
|
+
=> 4
|
73
|
+
|
74
|
+
Because we did them one at a time, the version number incremented for each one.
|
75
|
+
|
76
|
+
Now, Jasper has a *lot* of books.
|
77
|
+
jasper.books
|
78
|
+
=> [#<Book id: 1, initial_version: 2, version: 4, author_id: 1, name: "The Eyre Affair">,
|
79
|
+
#<Book id: 2, initial_version: 2, version: 4, author_id: 1, name: "Something Rotten">,
|
80
|
+
#<Book id: 3, initial_version: 3, version: 4, author_id: 1, name: "Lost in a Good Book">,
|
81
|
+
#<Book id: 4, initial_version: 4, version: 4, author_id: 1, name: "The Well of Lost Plots">]
|
82
|
+
|
83
|
+
Lets take one away...
|
84
|
+
jasper.books.delete(lost_in_a_good_book)
|
85
|
+
|
86
|
+
And the version has incremented
|
87
|
+
jasper.version
|
88
|
+
=> 5
|
89
|
+
jasper.books
|
90
|
+
=> [#<Book id: 1, initial_version: 2, version: 5, author_id: 1, name: "The Eyre Affair">,
|
91
|
+
#<Book id: 2, initial_version: 2, version: 5, author_id: 1, name: "Something Rotten">,
|
92
|
+
#<Book id: 4, initial_version: 4, version: 5, author_id: 1, name: "The Well of Lost Plots">]
|
93
|
+
|
94
|
+
But thats not right, he *did* write that book. Lets go back in time.
|
95
|
+
jasper.books.rollback
|
96
|
+
|
97
|
+
And the version get incremented...
|
98
|
+
jasper.version
|
99
|
+
=> 6
|
100
|
+
|
101
|
+
And the books...
|
102
|
+
jasper.books
|
103
|
+
=> [#<Book id: 5, initial_version: 6, version: 6, author_id: 1, name: "The Eyre Affair">,
|
104
|
+
#<Book id: 6, initial_version: 6, version: 6, author_id: 1, name: "Something Rotten">,
|
105
|
+
#<Book id: 7, initial_version: 6, version: 6, author_id: 1, name: "Lost in a Good Book">,
|
106
|
+
#<Book id: 8, initial_version: 6, version: 6, author_id: 1, name: "The Well of Lost Plots">]
|
107
|
+
|
108
|
+
Are right where we left them!
|
109
|
+
|
110
|
+
In fact, lets roll it right back to version 2.
|
111
|
+
|
112
|
+
jasper.books.rollback(2)
|
113
|
+
jasper.version
|
114
|
+
=> 7
|
115
|
+
jasper.books
|
116
|
+
=> [#<Book id: 9, initial_version: 7, version: 7, author_id: 1, name: "The Eyre Affair">,
|
117
|
+
#<Book id: 10, initial_version: 7, version: 7, author_id: 1, name: "Something Rotten">]
|
118
|
+
|
119
|
+
And what would the world look like if Jasper Fforde hadn't written anything?
|
120
|
+
jasper.books.rollback(1) #back to version one
|
121
|
+
jasper.version
|
122
|
+
=> 8
|
123
|
+
jasper.books
|
124
|
+
=> []
|
125
|
+
|
126
|
+
Of course, it's always fun to reminisce
|
127
|
+
jasper.books.at(3)
|
128
|
+
=> [#<Book id: 27, author_id: 3, initial_version: 2, version: 5, name: "The Eyre Affair">,
|
129
|
+
#<Book id: 28, author_id: 3, initial_version: 2, version: 5, name: "Something Rotten">,
|
130
|
+
#<Book id: 29, author_id: 3, initial_version: 3, version: 4, name: "Lost in a Good Book">]
|
131
|
+
jasper.books
|
132
|
+
=> []
|
data/Rakefile
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "has_many_versions"
|
8
|
+
gem.summary = %Q{Versioning for has_many relationships}
|
9
|
+
gem.email = "joshbuddy@gmail.com"
|
10
|
+
gem.homepage = "http://github.com/joshbuddy/has_many_versions"
|
11
|
+
gem.authors = ["Joshua Hull"]
|
12
|
+
gem.files = FileList["[A-Z]*", "{lib,spec,rails,bin}/**/*.rb", 'spec/database.yml', 'spec/spec.opts']
|
13
|
+
end
|
14
|
+
Jeweler::GemcutterTasks.new
|
15
|
+
|
16
|
+
rescue LoadError
|
17
|
+
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
18
|
+
end
|
19
|
+
|
20
|
+
require 'rake/rdoctask'
|
21
|
+
Rake::RDocTask.new do |rdoc|
|
22
|
+
rdoc.rdoc_dir = 'rdoc'
|
23
|
+
rdoc.title = 'has_many_versions'
|
24
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
25
|
+
rdoc.rdoc_files.include('README*')
|
26
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
27
|
+
end
|
28
|
+
|
29
|
+
require 'spec/rake/spectask'
|
30
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
31
|
+
spec.libs << 'lib' << 'spec'
|
32
|
+
spec.spec_opts << "--options" << "spec/spec.opts"
|
33
|
+
spec.spec_files = FileList['spec/**/*_spec.rb']
|
34
|
+
end
|
35
|
+
|
36
|
+
desc "Run all examples with RCov"
|
37
|
+
Spec::Rake::SpecTask.new('spec_with_rcov') do |t|
|
38
|
+
t.spec_files = FileList['spec/**/*.rb']
|
39
|
+
t.rcov = true
|
40
|
+
t.rcov_opts = ['--exclude', 'spec']
|
41
|
+
end
|
42
|
+
|
43
|
+
task :default => :spec
|
data/VERSION.yml
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Associations
|
3
|
+
class AssociationCollection
|
4
|
+
alias_method :__concat__, :<<
|
5
|
+
end
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
module HasManyVersions
|
10
|
+
|
11
|
+
HistoryItem = Struct.new(:version, :state)
|
12
|
+
|
13
|
+
def upgrade_proxy_object
|
14
|
+
proxy_owner.transaction do
|
15
|
+
new_version = proxy_owner.version + 1
|
16
|
+
proxy_owner.version = new_version
|
17
|
+
proxy_owner.class.update_all ['version = ?', new_version], ["#{proxy_reflection.klass.primary_key} = ?", proxy_owner.id]
|
18
|
+
proxy_owner.save!
|
19
|
+
yield new_version
|
20
|
+
proxy_owner.reload
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def replace(other_array)
|
25
|
+
other_array.each { |val| raise_on_type_mismatch(val) }
|
26
|
+
|
27
|
+
load_target
|
28
|
+
other = other_array.size < 100 ? other_array : other_array.to_set
|
29
|
+
current = @target.size < 100 ? @target : @target.to_set
|
30
|
+
|
31
|
+
upgrade_proxy_object do |new_version|
|
32
|
+
delete_records_without_versioning_transaction(new_version, @target.select { |v| !other.include?(v) })
|
33
|
+
add_records_without_versioning_transaction(new_version, other_array.select { |v| !current.include?(v) }, @target.select { |v| !other.include?(v) }.collect(&:id))
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def add_records_without_versioning_transaction(new_version, records, excluded_ids = [])
|
38
|
+
changing_records = flatten_deeper(records).select{ |r|
|
39
|
+
(!r.new_record? && (r.changed? || (r.version != (new_version - 1))))
|
40
|
+
}
|
41
|
+
excluded_ids.concat(changing_records.collect(&:id)) unless changing_records.empty?
|
42
|
+
excluded_ids.empty? ?
|
43
|
+
proxy_reflection.klass.update_all(
|
44
|
+
['version = ?', new_version],
|
45
|
+
["#{proxy_reflection.primary_key_name} = ? and version = ?", proxy_owner.id, new_version - 1]
|
46
|
+
) : proxy_reflection.klass.update_all(
|
47
|
+
['version = ?', new_version],
|
48
|
+
["#{proxy_reflection.primary_key_name} = ? and version = ? and #{proxy_reflection.klass.primary_key} not in (?)", proxy_owner.id, new_version - 1, excluded_ids]
|
49
|
+
)
|
50
|
+
records = flatten_deeper(records).collect do |r|
|
51
|
+
if changing_records.include?(r)
|
52
|
+
new_r = r.clone
|
53
|
+
new_r.from_version = r.id if new_r.respond_to?(:from_version=)
|
54
|
+
new_r
|
55
|
+
else
|
56
|
+
r
|
57
|
+
end
|
58
|
+
end
|
59
|
+
flatten_deeper(records).each do |record|
|
60
|
+
record.initial_version = proxy_owner.version if record.initial_version.nil? or record.new_record?
|
61
|
+
record.version = proxy_owner.version
|
62
|
+
end
|
63
|
+
__concat__(*records)
|
64
|
+
end
|
65
|
+
|
66
|
+
|
67
|
+
def <<(*records)
|
68
|
+
upgrade_proxy_object do |new_version|
|
69
|
+
add_records_without_versioning_transaction(new_version, records)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
alias_method :push, :<<
|
74
|
+
alias_method :concat, :<<
|
75
|
+
|
76
|
+
def delete_records_without_versioning_transaction(new_version, records)
|
77
|
+
proxy_reflection.klass.update_all ['version = ?', new_version], ["#{proxy_reflection.primary_key_name} = ? and version = ? and #{proxy_reflection.klass.primary_key} not in (?)", proxy_owner.id, new_version - 1, records.collect(&proxy_reflection.klass.primary_key.to_sym)]
|
78
|
+
end
|
79
|
+
|
80
|
+
def delete_records(records)
|
81
|
+
upgrade_proxy_object do |new_version|
|
82
|
+
delete_records_without_versioning_transaction(new_version, records)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def conditions
|
87
|
+
interpolate_sql(@reflection.sanitized_conditions ?
|
88
|
+
'%s AND %s.version = #{version}' % [@reflection.sanitized_conditions, proxy_reflection.quoted_table_name] :
|
89
|
+
'%s.version = #{version}' % [proxy_reflection.quoted_table_name])
|
90
|
+
end
|
91
|
+
|
92
|
+
def history(from = [proxy_owner.version - 10, 1].max, to = proxy_owner.version)
|
93
|
+
history_items = proxy_reflection.klass.find(:all, :conditions => ["#{proxy_reflection.primary_key_name} = ? and version >= ? and initial_version <= ?", proxy_owner.id, from, to])
|
94
|
+
to = proxy_owner.version if to > proxy_owner.version
|
95
|
+
from = 1 if from < 1
|
96
|
+
(from..to).collect do |version|
|
97
|
+
HistoryItem.new(version, history_items.select { |item|
|
98
|
+
item.initial_version <= version && item.version >= version
|
99
|
+
})
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def rollback(target_version = proxy_owner.version - 1)
|
104
|
+
return if target_version == proxy_owner.version
|
105
|
+
upgrade_proxy_object do |new_version|
|
106
|
+
proxy_reflection.klass.find(:all, :conditions => ["#{proxy_reflection.primary_key_name} = ? and initial_version <= ? and version >= ?", proxy_owner.id, target_version, target_version]).each do |new_record|
|
107
|
+
new_record = new_record.clone
|
108
|
+
new_record.initial_version = new_version
|
109
|
+
new_record.version = new_version
|
110
|
+
new_record.save!
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
|
116
|
+
def at(target_version)
|
117
|
+
proxy_reflection.klass.find(:all, :conditions => ["#{proxy_reflection.primary_key_name} = ? and initial_version <= ? and version >= ?", proxy_owner.id, target_version, target_version])
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
data/rails/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
autoload :HasManyVersions, 'lib/has_many_versions'
|
data/spec/add_spec.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'spec_helper')
|
2
|
+
|
3
|
+
describe "HasManyVersions adding" do
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
Database.reset!
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should start with a version of 1" do
|
10
|
+
jasper = Author.new(:name => 'Jasper Fforde')
|
11
|
+
jasper.save!
|
12
|
+
jasper.version.should == 1
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should increment the version and the associated object should match it" do
|
16
|
+
jasper = Author.new(:name => 'Jasper Fforde')
|
17
|
+
eyre_affair = Book.new(:name => "The Eyre Affair")
|
18
|
+
jasper.books << eyre_affair
|
19
|
+
jasper.save!
|
20
|
+
jasper.version.should == eyre_affair.version
|
21
|
+
eyre_affair.initial_version.should == eyre_affair.version
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should be able to get two books and return them" do
|
25
|
+
jasper = Author.new(:name => 'Jasper Fforde')
|
26
|
+
eyre_affair = Book.new(:name => "The Eyre Affair")
|
27
|
+
shades_of_grey = Book.new(:name => "Shades of Grey")
|
28
|
+
jasper.save!
|
29
|
+
old_version = jasper.version
|
30
|
+
jasper.books << eyre_affair
|
31
|
+
jasper.books << shades_of_grey
|
32
|
+
jasper.books.should == [eyre_affair, shades_of_grey]
|
33
|
+
jasper.books.each {|b| b.version.should == (old_version + 2) }
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
data/spec/database.yml
ADDED
data/spec/delete_spec.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'spec_helper')
|
2
|
+
|
3
|
+
describe "HasManyVersions deleting" do
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
Database.reset!
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should delete from the associations and increment the version" do
|
10
|
+
jasper = Author.new(:name => 'Jasper Fforde')
|
11
|
+
eyre_affair = Book.new(:name => "The Eyre Affair")
|
12
|
+
shades_of_grey = Book.new(:name => "Shades of Grey")
|
13
|
+
jasper.save!
|
14
|
+
jasper.books.push(eyre_affair, shades_of_grey)
|
15
|
+
initial_version = jasper.version
|
16
|
+
jasper.books.delete(eyre_affair)
|
17
|
+
jasper.books.should == [shades_of_grey]
|
18
|
+
jasper.books.first.version.should == 3
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'spec_helper')
|
2
|
+
|
3
|
+
describe "HasManyVersions history" do
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
Database.reset!
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should give you a history" do
|
10
|
+
Database.reset!(true)
|
11
|
+
jasper = Author.new(:name => 'Jasper Fforde')
|
12
|
+
|
13
|
+
books = (1..100).collect do |i|
|
14
|
+
book = Book.new(:name => "Book #{i}", :id => i)
|
15
|
+
book.save!
|
16
|
+
book
|
17
|
+
end
|
18
|
+
jasper.save!
|
19
|
+
titles = [[]]
|
20
|
+
jasper.books = [Book.find(1), Book.find(10), Book.find(11), Book.find(23), Book.find(99)]
|
21
|
+
titles << jasper.books.collect(&:name)
|
22
|
+
jasper.books = [Book.find(1), Book.find(10), Book.find(12), Book.find(25), Book.find(94)]
|
23
|
+
titles << jasper.books.collect(&:name)
|
24
|
+
jasper.books = [Book.find(1), Book.find(10), Book.find(12), Book.find(25), Book.find(94), Book.find(87)]
|
25
|
+
titles << jasper.books.collect(&:name)
|
26
|
+
jasper.books = [Book.find(2), Book.find(4), Book.find(5)]
|
27
|
+
titles << jasper.books.collect(&:name)
|
28
|
+
jasper.books = [Book.find(1), Book.find(10), Book.find(11), Book.find(23), Book.find(99)]
|
29
|
+
titles << jasper.books.collect(&:name)
|
30
|
+
|
31
|
+
jasper.books.history.each do |history|
|
32
|
+
history.state.collect(&:name).should == titles.shift
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'spec_helper')
|
2
|
+
|
3
|
+
describe "HasManyVersions rollbacks" do
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
Database.reset!
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should rollback one step" do
|
10
|
+
jasper = Author.new(:name => 'Jasper Fforde')
|
11
|
+
eyre_affair = Book.new(:name => "The Eyre Affair")
|
12
|
+
shades_of_grey = Book.new(:name => "Shades of Grey")
|
13
|
+
jasper.save!
|
14
|
+
jasper.books.push(eyre_affair, shades_of_grey)
|
15
|
+
eyre_affair2 = Book.new(:name => "The Eyre Affair 2")
|
16
|
+
shades_of_grey2 = Book.new(:name => "Shades of Grey 2")
|
17
|
+
jasper.books.push(eyre_affair2, shades_of_grey2)
|
18
|
+
eyre_affair3 = Book.new(:name => "The Eyre Affair 3")
|
19
|
+
jasper.books.push(eyre_affair3)
|
20
|
+
jasper.books.should == [eyre_affair, shades_of_grey, eyre_affair2, shades_of_grey2, eyre_affair3]
|
21
|
+
jasper.books.rollback
|
22
|
+
jasper.books.collect(&:name).should == [eyre_affair.name, shades_of_grey.name, eyre_affair2.name, shades_of_grey2.name]
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should any number of steps" do
|
26
|
+
jasper = Author.new(:name => 'Jasper Fforde')
|
27
|
+
eyre_affair = Book.new(:name => "The Eyre Affair")
|
28
|
+
shades_of_grey = Book.new(:name => "Shades of Grey")
|
29
|
+
jasper.save!
|
30
|
+
jasper.books.push(eyre_affair, shades_of_grey)
|
31
|
+
eyre_affair2 = Book.new(:name => "The Eyre Affair 2")
|
32
|
+
shades_of_grey2 = Book.new(:name => "Shades of Grey 2")
|
33
|
+
jasper.books.push(eyre_affair2, shades_of_grey2)
|
34
|
+
eyre_affair3 = Book.new(:name => "The Eyre Affair 3")
|
35
|
+
jasper.books.push(eyre_affair3)
|
36
|
+
jasper.books.should == [eyre_affair, shades_of_grey, eyre_affair2, shades_of_grey2, eyre_affair3]
|
37
|
+
jasper.books.rollback(2)
|
38
|
+
jasper.books.collect(&:name).should == [eyre_affair.name, shades_of_grey.name]
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should rollback multiple times" do
|
42
|
+
jasper = Author.new(:name => 'Jasper Fforde')
|
43
|
+
eyre_affair = Book.new(:name => "The Eyre Affair")
|
44
|
+
shades_of_grey = Book.new(:name => "Shades of Grey")
|
45
|
+
jasper.save!
|
46
|
+
jasper.books.push(eyre_affair, shades_of_grey)
|
47
|
+
eyre_affair2 = Book.new(:name => "The Eyre Affair 2")
|
48
|
+
shades_of_grey2 = Book.new(:name => "Shades of Grey 2")
|
49
|
+
jasper.books.push(eyre_affair2, shades_of_grey2)
|
50
|
+
eyre_affair3 = Book.new(:name => "The Eyre Affair 3")
|
51
|
+
jasper.books.push(eyre_affair3)
|
52
|
+
jasper.books.collect(&:name).should == [eyre_affair.name, shades_of_grey.name, eyre_affair2.name, shades_of_grey2.name, eyre_affair3.name]
|
53
|
+
jasper.books.rollback(2)
|
54
|
+
jasper.books.collect(&:name).should == [eyre_affair.name, shades_of_grey.name]
|
55
|
+
jasper.books.rollback
|
56
|
+
jasper.books.collect(&:name).should == [eyre_affair.name, shades_of_grey.name, eyre_affair2.name, shades_of_grey2.name, eyre_affair3.name]
|
57
|
+
jasper.books.rollback(3)
|
58
|
+
jasper.books.collect(&:name).should == [eyre_affair.name, shades_of_grey.name, eyre_affair2.name, shades_of_grey2.name]
|
59
|
+
jasper.books.at(2).collect(&:name).should == [eyre_affair.name, shades_of_grey.name]
|
60
|
+
jasper.books.collect(&:name).should == [eyre_affair.name, shades_of_grey.name, eyre_affair2.name, shades_of_grey2.name]
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
data/spec/spec.opts
ADDED
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'spec'
|
3
|
+
require 'activerecord'
|
4
|
+
require 'rails/init'
|
5
|
+
|
6
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
7
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
8
|
+
|
9
|
+
Spec::Runner.configure do |config|
|
10
|
+
|
11
|
+
end
|
12
|
+
|
13
|
+
ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + '/debug.log')
|
14
|
+
ActiveRecord::Base.establish_connection(YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml')))
|
15
|
+
|
16
|
+
class Database
|
17
|
+
def self.reset!(with_from_version = false)
|
18
|
+
ActiveRecord::Schema.define :version => 0 do
|
19
|
+
create_table :books, :force => true do |t|
|
20
|
+
t.integer :author_id
|
21
|
+
t.integer :initial_version
|
22
|
+
t.integer :from_version if with_from_version
|
23
|
+
t.integer :version
|
24
|
+
t.string :name
|
25
|
+
end
|
26
|
+
|
27
|
+
create_table :authors, :force => true do |t|
|
28
|
+
t.integer :version, :default => 1
|
29
|
+
t.string :name
|
30
|
+
end
|
31
|
+
|
32
|
+
Book.reset_column_information
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
class Book < ActiveRecord::Base
|
39
|
+
belongs_to :author
|
40
|
+
validates_associated :author
|
41
|
+
end
|
42
|
+
|
43
|
+
class Author < ActiveRecord::Base
|
44
|
+
has_many :books, :extend => HasManyVersions
|
45
|
+
end
|
data/spec/update_spec.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'spec_helper')
|
2
|
+
|
3
|
+
describe "HasManyVersions updating" do
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
Database.reset!
|
7
|
+
end
|
8
|
+
|
9
|
+
it "interpret changed? objects as new and allow rolling back to that state" do
|
10
|
+
jasper = Author.new(:name => 'Jasper Fforde')
|
11
|
+
eyre_affair = Book.new(:name => "The Eyre Affair")
|
12
|
+
jasper.books << eyre_affair
|
13
|
+
jasper.save!
|
14
|
+
jasper.version.should == eyre_affair.version
|
15
|
+
eyre_affair.initial_version.should == eyre_affair.version
|
16
|
+
eyre_affair.name = 'The EYRE affair'
|
17
|
+
jasper.books << eyre_affair
|
18
|
+
jasper.books.first.name.should == 'The EYRE affair'
|
19
|
+
jasper.books.rollback
|
20
|
+
jasper.books.first.name.should == 'The Eyre Affair'
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should record the 'from version' if that column exists" do
|
24
|
+
Database.reset!(true)
|
25
|
+
jasper = Author.new(:name => 'Jasper Fforde')
|
26
|
+
eyre_affair = Book.new(:name => "The Eyre Affair")
|
27
|
+
jasper.books << eyre_affair
|
28
|
+
jasper.save!
|
29
|
+
jasper.version.should == eyre_affair.version
|
30
|
+
eyre_affair.initial_version.should == eyre_affair.version
|
31
|
+
eyre_affair.name = 'The EYRE affair'
|
32
|
+
jasper.books << eyre_affair
|
33
|
+
jasper.books.first.from_version.should == eyre_affair.id
|
34
|
+
jasper.books.first.id.should == 2
|
35
|
+
jasper.books.first.name.should == 'The EYRE affair'
|
36
|
+
jasper.books.rollback
|
37
|
+
jasper.books.first.name.should == 'The Eyre Affair'
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should record add, delete and update events all at the same time" do
|
41
|
+
Database.reset!(true)
|
42
|
+
jasper = Author.new(:name => 'Jasper Fforde')
|
43
|
+
eyre_affair = Book.new(:name => "The Eyre Affair")
|
44
|
+
shades_of_grey = Book.new(:name => "Shades of Grey")
|
45
|
+
eyre_affair2 = Book.new(:name => "The Eyre Affair 2")
|
46
|
+
shades_of_grey2 = Book.new(:name => "Shades of Grey 2")
|
47
|
+
|
48
|
+
jasper.version.should == 1
|
49
|
+
jasper.books = [eyre_affair, shades_of_grey]
|
50
|
+
jasper.version.should == 2
|
51
|
+
|
52
|
+
jasper.books = [shades_of_grey2, eyre_affair]
|
53
|
+
jasper.version.should == 3
|
54
|
+
jasper.books.collect(&:name).should == ['The Eyre Affair', 'Shades of Grey 2']
|
55
|
+
jasper.books.rollback
|
56
|
+
jasper.books.collect(&:name).should == ['The Eyre Affair', 'Shades of Grey']
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
metadata
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: has_many_versions
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.7
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Joshua Hull
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-12-03 00:00:00 -05:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description:
|
17
|
+
email: joshbuddy@gmail.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- LICENSE
|
24
|
+
- README.rdoc
|
25
|
+
files:
|
26
|
+
- LICENSE
|
27
|
+
- README.rdoc
|
28
|
+
- Rakefile
|
29
|
+
- VERSION.yml
|
30
|
+
- lib/has_many_versions.rb
|
31
|
+
- rails/init.rb
|
32
|
+
- spec/add_spec.rb
|
33
|
+
- spec/database.yml
|
34
|
+
- spec/delete_spec.rb
|
35
|
+
- spec/history_spec.rb
|
36
|
+
- spec/rollback_spec.rb
|
37
|
+
- spec/spec.opts
|
38
|
+
- spec/spec_helper.rb
|
39
|
+
- spec/update_spec.rb
|
40
|
+
has_rdoc: true
|
41
|
+
homepage: http://github.com/joshbuddy/has_many_versions
|
42
|
+
licenses: []
|
43
|
+
|
44
|
+
post_install_message:
|
45
|
+
rdoc_options:
|
46
|
+
- --charset=UTF-8
|
47
|
+
require_paths:
|
48
|
+
- lib
|
49
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: "0"
|
54
|
+
version:
|
55
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: "0"
|
60
|
+
version:
|
61
|
+
requirements: []
|
62
|
+
|
63
|
+
rubyforge_project:
|
64
|
+
rubygems_version: 1.3.5
|
65
|
+
signing_key:
|
66
|
+
specification_version: 3
|
67
|
+
summary: Versioning for has_many relationships
|
68
|
+
test_files:
|
69
|
+
- spec/add_spec.rb
|
70
|
+
- spec/delete_spec.rb
|
71
|
+
- spec/history_spec.rb
|
72
|
+
- spec/rollback_spec.rb
|
73
|
+
- spec/spec_helper.rb
|
74
|
+
- spec/update_spec.rb
|