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 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