has_many_versions 0.0.7

Sign up to get free protection for your applications and to get access to all the features.
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,4 @@
1
+ ---
2
+ :patch: 7
3
+ :major: 0
4
+ :minor: 0
@@ -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
@@ -0,0 +1,2 @@
1
+ adapter: sqlite3
2
+ database: test.db
@@ -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
@@ -0,0 +1,7 @@
1
+ --colour
2
+ --format
3
+ specdoc
4
+ --loadby
5
+ mtime
6
+ --reverse
7
+ --backtrace
@@ -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
@@ -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