andoq-vestal_versions 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,4 @@
1
+ .DS_Store
2
+ pkg
3
+ rdoc
4
+ test/*.db
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Steve Richert
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.
@@ -0,0 +1,77 @@
1
+ = vestal_versions
2
+
3
+ Finally, DRY ActiveRecord versioning!
4
+
5
+ <tt>acts_as_versioned</tt>[http://github.com/technoweenie/acts_as_versioned] by technoweenie[http://github.com/technoweenie] was a great start, but it failed to keep up with ActiveRecord's introduction of dirty objects in version 2.1. Additionally, each versioned model needs its own versions table that duplicates most of the original table's columns. The versions table is then populated with records that often duplicate most of the original record's attributes. All in all, not very DRY.
6
+
7
+ <tt>simply_versioned</tt>[http://github.com/mmower/simply_versioned] by mmower[http://github.com/mmower] started to move in the right direction by removing a great deal of the duplication of acts_as_versioned. It requires only one versions table and no changes whatsoever to existing models. Its versions table stores all of the model attributes as a YAML hash in a single text column. But we could be DRYer!
8
+
9
+ <tt>vestal_versions</tt> keeps in the spirit of consolidating to one versions table, polymorphically associated with its parent models. But it goes one step further by storing a serialized hash of only the models' changes. Think modern version control systems. By traversing the record of changes, the models can be reverted to any point in time.
10
+
11
+ And that's just what <tt>vestal_versions</tt> does. Not only can a model be reverted to a previous version number but also to a date or time!
12
+
13
+ == Installation
14
+
15
+ In <tt>environment.rb</tt>:
16
+
17
+ Rails::Initializer.run do |config|
18
+ config.gem 'laserlemon-vestal_versions', :lib => 'vestal_versions', :source => 'http://gems.github.com'
19
+ end
20
+
21
+ At your application root, run:
22
+
23
+ $ sudo rake gems:install
24
+
25
+ Next, generate and run the first and last versioning migration you'll ever need:
26
+
27
+ $ script/generate vestal_versions_migration
28
+ $ rake db:migrate
29
+
30
+ == Example
31
+
32
+ To version an ActiveRecord model, simply add <tt>versioned</tt> to your class like so:
33
+
34
+ class User < ActiveRecord::Base
35
+ versioned
36
+
37
+ validates_presence_of :first_name, :last_name
38
+
39
+ def name
40
+ "#{first_name} #{last_name}"
41
+ end
42
+ end
43
+
44
+ It's that easy! Now watch it in action...
45
+
46
+ >> u = User.create(:first_name => 'Steve', :last_name => 'Richert')
47
+ => #<User first_name: "Steve", last_name: "Richert">
48
+ >> u.version
49
+ => 1
50
+ >> u.update_attribute(:first_name, 'Stephen')
51
+ => true
52
+ >> u.name
53
+ => "Stephen Richert"
54
+ >> u.version
55
+ => 2
56
+ >> u.revert_to(:first)
57
+ => 1
58
+ >> u.name
59
+ => "Steve Richert"
60
+ >> u.version
61
+ => 1
62
+ >> u.save
63
+ => true
64
+ >> u.version
65
+ => 3
66
+ >> u.update_attribute(:last_name, 'Jobs')
67
+ => true
68
+ >> u.name
69
+ => "Steve Jobs"
70
+ >> u.version
71
+ => 4
72
+ >> u.revert_to!(2)
73
+ => true
74
+ >> u.name
75
+ => "Stephen Richert"
76
+ >> u.version
77
+ => 5
@@ -0,0 +1,38 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rake/testtask'
4
+ require 'rake/rdoctask'
5
+
6
+ begin
7
+ require 'jeweler'
8
+ Jeweler::Tasks.new do |g|
9
+ g.name = 'vestal_versions'
10
+ g.summary = %(Keep a DRY history of your ActiveRecord models' changes)
11
+ g.description = %(Keep a DRY history of your ActiveRecord models' changes)
12
+ g.email = 'steve@laserlemon.com'
13
+ g.homepage = 'http://github.com/laserlemon/vestal_versions'
14
+ g.authors = %w(laserlemon)
15
+ g.add_development_dependency 'thoughtbot-shoulda'
16
+ g.rubyforge_project = 'laser-lemon'
17
+ end
18
+ Jeweler::RubyforgeTasks.new do |r|
19
+ r.doc_task = 'rdoc'
20
+ end
21
+ rescue LoadError
22
+ puts 'Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com'
23
+ end
24
+
25
+ Rake::TestTask.new do |t|
26
+ t.libs = %w(test)
27
+ t.pattern = 'test/**/*_test.rb'
28
+ end
29
+
30
+ task :default => :test
31
+
32
+ Rake::RDocTask.new do |r|
33
+ version = File.exist?('VERSION') ? File.read('VERSION') : nil
34
+ r.rdoc_dir = 'rdoc'
35
+ r.title = ['vestal_versions', version].compact.join(' ')
36
+ r.rdoc_files.include('README*')
37
+ r.rdoc_files.include('lib/**/*.rb')
38
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.6.1
@@ -0,0 +1,20 @@
1
+ class CreateVestalVersions < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :versions do |t|
4
+ t.belongs_to :versioned, :polymorphic => true
5
+ t.text :changes
6
+ t.integer :number
7
+ t.datetime :created_at
8
+ end
9
+
10
+ change_table :versions do |t|
11
+ t.index [:versioned_type, :versioned_id]
12
+ t.index :number
13
+ t.index :created_at
14
+ end
15
+ end
16
+
17
+ def self.down
18
+ drop_table :versions
19
+ end
20
+ end
@@ -0,0 +1,11 @@
1
+ class VestalVersionsMigrationGenerator < Rails::Generator::Base
2
+ def manifest
3
+ record do |m|
4
+ m.migration_template 'migration.rb', 'db/migrate'
5
+ end
6
+ end
7
+
8
+ def file_name
9
+ 'create_vestal_versions'
10
+ end
11
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'vestal_versions'
@@ -0,0 +1,13 @@
1
+ class Version < ActiveRecord::Base
2
+ include Comparable
3
+
4
+ belongs_to :versioned, :polymorphic => true
5
+
6
+ serialize :changes, Hash
7
+
8
+ alias_attribute :version, :number
9
+
10
+ def <=>(other)
11
+ number <=> other.number
12
+ end
13
+ end
@@ -0,0 +1,167 @@
1
+ require 'version'
2
+
3
+ module LaserLemon
4
+ module VestalVersions
5
+ def self.included(base)
6
+ base.extend ClassMethods
7
+ end
8
+
9
+ module ClassMethods
10
+
11
+ def has_many_versioned(association_id, options = {}, &extension)
12
+
13
+ if options[:through]
14
+ raise 'Versioning through relation ships is not supported. Version the join relationship instead.'
15
+ end
16
+ options[:after_remove] ||= []
17
+ options[:after_remove] << :remove_association
18
+ has_many association_id, options, &extension
19
+
20
+ #We can't use the after_add callback, because the object may not be saved, and then we don't have an ID to record.
21
+ #So we need to put the recording of the change on the associated model's after_save callback.s
22
+ #TODO: figure out how to get the change into the calling objects's changes
23
+ versioned_class = self
24
+
25
+ self.reflections[association_id].klass.send(:define_method, "vestal_version_#{self.reflections[association_id].name}_after_save_callback", Proc.new {
26
+ self.send((versioned_class.name.downcase).to_sym).send(:add_association, self)
27
+ self.send((versioned_class.name.downcase).to_sym).send(:save)
28
+ })
29
+ self.reflections[association_id].klass.send(:after_save, "vestal_version_#{self.reflections[association_id].name}_after_save_callback".to_sym)
30
+
31
+ end
32
+
33
+ def versioned
34
+ has_many :versions, :as => :versioned, :order => 'versions.number ASC', :dependent => :delete_all do
35
+ def between(from_value, to_value)
36
+ from, to = number_at(from_value), number_at(to_value)
37
+ return [] if from.nil? || to.nil?
38
+ condition = (from == to) ? to : Range.new(*[from, to].sort)
39
+ all(
40
+ :conditions => {:number => condition},
41
+ :order => "versions.number #{(from > to) ? 'DESC' : 'ASC'}"
42
+ )
43
+ end
44
+
45
+ def at(value)
46
+ case value
47
+ when Version then value
48
+ when Numeric then find_by_number(value.floor)
49
+ when Symbol then respond_to?(value) ? send(value) : nil
50
+ when Date, Time then last(:conditions => ['versions.created_at <= ?', value.to_time])
51
+ end
52
+ end
53
+
54
+ def number_at(value)
55
+ case value
56
+ when Version then value.number
57
+ when Numeric then value.floor
58
+ when Symbol, Date, Time then at(value).try(:number)
59
+ end
60
+ end
61
+ end
62
+
63
+ after_create :create_initial_version
64
+ after_update :create_initial_version, :if => :needs_initial_version?
65
+ after_update :create_version, :if => :needs_version?
66
+
67
+ include InstanceMethods
68
+ alias_method_chain :reload, :versions
69
+ end
70
+ end
71
+
72
+ module InstanceMethods
73
+ private
74
+ def needs_initial_version?
75
+ versions.empty?
76
+ end
77
+
78
+ def needs_version?
79
+ !revisable_changes.empty?
80
+ end
81
+
82
+ def reset_version(new_version = nil)
83
+ @last_version = nil if new_version.nil?
84
+ @version = new_version
85
+ end
86
+
87
+ def create_initial_version
88
+ versions.create(:changes => nil, :number => 1)
89
+ end
90
+
91
+ def create_version
92
+ versions.create(:changes => revisable_changes, :number => (last_version + 1))
93
+ reset_version
94
+ end
95
+
96
+ def add_association(association_object)
97
+ association_changes.merge!('association' => {:action => 'add', :name => association_object.class.name, :id => association_object.id})
98
+ end
99
+
100
+ def remove_association(association_object)
101
+ association_changes.merge!('association' => {:action => 'remove', :name => association_object.class.name, :id => association_object.id})
102
+ save #save here so that the version is recorded. This keeps it consistent w/ adding a association. If this is ever fixed on add, remove this save call
103
+ end
104
+
105
+ public
106
+ def version
107
+ @version ||= last_version
108
+ end
109
+
110
+ def last_version
111
+ @last_version ||= versions.maximum(:number)
112
+ end
113
+
114
+ def association_changes
115
+ @association_changes ||= {}
116
+ end
117
+
118
+ def revisable_changes
119
+ changes.merge!(association_changes)
120
+ end
121
+
122
+ def reverted?
123
+ version != last_version
124
+ end
125
+
126
+ def reload_with_versions(*args)
127
+ reset_version
128
+ reload_without_versions(*args)
129
+ end
130
+
131
+ def revert_to(value)
132
+ to_value = versions.number_at(value)
133
+ return version if to_value == version
134
+ chain = versions.between(version, to_value)
135
+ return version if chain.empty?
136
+
137
+ new_version = chain.last.number
138
+ backward = chain.first > chain.last
139
+ backward ? chain.pop : chain.shift
140
+
141
+ unrevertable_changes = %w(created_at created_on updated_at updated_on association)
142
+
143
+ chain.each do |version|
144
+ version.changes.except(*unrevertable_changes).each do |attribute, change|
145
+ new_value = backward ? change.first : change.last
146
+ write_attribute(attribute, new_value)
147
+ end
148
+ end
149
+
150
+ reset_version(new_version)
151
+ end
152
+
153
+ def revert_to!(value)
154
+ revert_to(value)
155
+ reset_version if saved = save
156
+ saved
157
+ end
158
+
159
+ def latest_changes
160
+ return {} if version.nil? || version == 1
161
+ versions.at(version).changes
162
+ end
163
+ end
164
+ end
165
+ end
166
+
167
+ ActiveRecord::Base.send(:include, LaserLemon::VestalVersions)
@@ -0,0 +1,36 @@
1
+ require 'test_helper'
2
+
3
+ class AssociationsTest < Test::Unit::TestCase
4
+ context "A model's change" do
5
+ setup do
6
+ @user = User.create(:name => 'Steve Richert')
7
+ end
8
+
9
+ should 'add a version when an has_many associations is added' do
10
+ project = Project.create(:name => 'Versioned Associations')
11
+
12
+ old_version_count = @user.versions.size
13
+ @user.user_projects.create!(:project => project)
14
+ @user.reload #needed for now, not sure how to get this object to reload it's versions after the after_save callback on the associated object
15
+ assert_equal(old_version_count + 1, @user.versions.size)
16
+ end
17
+
18
+ should 'add a version when an has_many associations is removed' do
19
+ project = Project.create(:name => 'Versioned Associations')
20
+
21
+ user_project = @user.user_projects.create!(:project => project)
22
+ @user.reload
23
+ old_version_count = @user.versions.size
24
+ @user.user_projects.delete(user_project)
25
+ @user.reload
26
+ assert_equal(old_version_count + 1, @user.versions.size)
27
+ end
28
+
29
+ should 'add a version when an has_many_through association is added and the :through relationship is versioned' do
30
+ old_version_count = @user.versions.size
31
+ @user.projects.create!(:name => 'Versioned Associations')
32
+ @user.reload
33
+ assert_equal(old_version_count + 1, @user.versions.size)
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,58 @@
1
+ require 'test_helper'
2
+
3
+ class BetweenTest < Test::Unit::TestCase
4
+ context 'The number of versions between' do
5
+ setup do
6
+ @user = User.create(:name => 'Steve Richert')
7
+ @version = @user.version
8
+ @valid = [@version, 0, 1_000_000, :first, :last, 1.day.since(@user.created_at), @user.versions.first]
9
+ @invalid = [nil, :bogus, 'bogus', Date.parse('0001-12-25')]
10
+ end
11
+
12
+ context 'the current version and the current version' do
13
+ should 'equal one' do
14
+ assert_equal 1, @user.versions.between(@version, @version).size
15
+ end
16
+ end
17
+
18
+ context 'the current version and a valid value' do
19
+ should 'not equal zero' do
20
+ @valid.each do |valid|
21
+ assert_not_equal 0, @user.versions.between(@version, valid).size
22
+ assert_not_equal 0, @user.versions.between(valid, @version).size
23
+ end
24
+ end
25
+ end
26
+
27
+ context 'the current version and an invalid value' do
28
+ should 'equal zero' do
29
+ @invalid.each do |invalid|
30
+ assert_equal 0, @user.versions.between(@version, invalid).size
31
+ assert_equal 0, @user.versions.between(invalid, @version).size
32
+ end
33
+ end
34
+ end
35
+
36
+ context 'two invalid values' do
37
+ should 'equal zero' do
38
+ @invalid.each do |first|
39
+ @invalid.each do |second|
40
+ assert_equal 0, @user.versions.between(first, second).size
41
+ assert_equal 0, @user.versions.between(second, first).size
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ context 'a valid value and an invalid value' do
48
+ should 'equal zero' do
49
+ @valid.each do |valid|
50
+ @invalid.each do |invalid|
51
+ assert_equal 0, @user.versions.between(valid, invalid).size
52
+ assert_equal 0, @user.versions.between(invalid, valid).size
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,35 @@
1
+ require 'test_helper'
2
+
3
+ class ChangesTest < Test::Unit::TestCase
4
+ context "A version's changes" do
5
+ setup do
6
+ @user = User.create(:name => 'Steve Richert')
7
+ end
8
+
9
+ should "initially be blank" do
10
+ assert @user.versions.first.changes.blank?
11
+ end
12
+
13
+ should 'contain all changed attributes' do
14
+ @user.name = 'Steve Jobs'
15
+ changes = @user.changes
16
+ @user.save
17
+ assert_equal changes, @user.versions.last.changes.slice(*changes.keys)
18
+ end
19
+
20
+ should 'contain timestamp changes when applicable' do
21
+ timestamp = 'updated_at'
22
+ @user.update_attribute(:name, 'Steve Jobs')
23
+ assert @user.class.content_columns.map(&:name).include?(timestamp)
24
+ assert_contains @user.versions.last.changes.keys, timestamp
25
+ end
26
+
27
+ should 'contain no more than the changed attributes and not timestamps' do
28
+ timestamps = %w(created_at created_on updated_at updated_on)
29
+ @user.name = 'Steve Jobs'
30
+ changes = @user.changes
31
+ @user.save
32
+ assert_equal changes, @user.versions.last.changes.except(*timestamps)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,35 @@
1
+ require 'test_helper'
2
+
3
+ class ComparableTest < Test::Unit::TestCase
4
+ context 'A comparable version' do
5
+ setup do
6
+ @version_1 = Version.new(:number => 1)
7
+ @version_2 = Version.new(:number => 2)
8
+ end
9
+
10
+ should 'equal itself' do
11
+ assert @version_1 == @version_1
12
+ assert @version_2 == @version_2
13
+ end
14
+
15
+ context 'with version number 1' do
16
+ should 'not equal a version with version number 2' do
17
+ assert @version_1 != @version_2
18
+ end
19
+
20
+ should 'be less than a version with version number 2' do
21
+ assert @version_1 < @version_2
22
+ end
23
+ end
24
+
25
+ context 'with version number 2' do
26
+ should 'not equal a version with version number 1' do
27
+ assert @version_2 != @version_1
28
+ end
29
+
30
+ should 'be greater than a version with version number 1' do
31
+ assert @version_2 > @version_1
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,70 @@
1
+ require 'test_helper'
2
+
3
+ class CreationTest < Test::Unit::TestCase
4
+ context 'The number of versions' do
5
+ setup do
6
+ @name = 'Steve Richert'
7
+ @user = User.create(:name => @name)
8
+ @count = @user.versions.count
9
+ end
10
+
11
+ should 'initially equal one' do
12
+ assert_equal 1, @count
13
+ end
14
+
15
+ should 'not increase when no changes are made in an update' do
16
+ @user.update_attribute(:name, @name)
17
+ assert_equal @count, @user.versions.count
18
+ end
19
+
20
+ should 'not increase when no changes are made before a save' do
21
+ @user.save
22
+ assert_equal @count, @user.versions.count
23
+ end
24
+
25
+ should 'not increase when reverting to the current version' do
26
+ @user.revert_to!(@user.version)
27
+ assert_equal @count, @user.versions.count
28
+ end
29
+
30
+ context 'after an update' do
31
+ setup do
32
+ @initial_count = @count
33
+ @name = 'Steve Jobs'
34
+ @user.update_attribute(:name, @name)
35
+ @count = @user.versions.count
36
+ end
37
+
38
+ should 'increase by one' do
39
+ assert_equal @initial_count + 1, @count
40
+ end
41
+
42
+ should 'increase by one when reverted' do
43
+ @user.revert_to!(:first)
44
+ assert_equal @count + 1, @user.versions.count
45
+ end
46
+
47
+ should 'not increase until a revert is saved' do
48
+ @user.revert_to(:first)
49
+ assert_equal @count, @user.versions.count
50
+ @user.save
51
+ assert_not_equal @count, @user.versions.count
52
+ end
53
+ end
54
+
55
+ context 'after multiple updates' do
56
+ setup do
57
+ @initial_count = @count
58
+ @new_name = 'Steve Jobs'
59
+ @user.update_attribute(:name, @new_name)
60
+ @user.update_attribute(:name, @name)
61
+ @count = @user.versions.count
62
+ end
63
+
64
+ should 'not increase when reverting to an identical version' do
65
+ @user.revert_to!(:first)
66
+ assert_equal @count, @user.versions.count
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,42 @@
1
+ require 'test_helper'
2
+
3
+ class LatestChangesTest < Test::Unit::TestCase
4
+ context "A created model's last changes" do
5
+ setup do
6
+ @user = User.create(:name => 'Steve Richert')
7
+ end
8
+
9
+ should 'be blank' do
10
+ assert @user.latest_changes.blank?
11
+ end
12
+ end
13
+
14
+ context "An updated model's last changes" do
15
+ setup do
16
+ @user = User.create(:name => 'Steve Richert')
17
+ @previous_attributes = @user.attributes
18
+ @user.update_attribute(:name, 'Steve Jobs')
19
+ @current_attributes = @user.attributes
20
+ end
21
+
22
+ should 'values of two-element arrays with unique values' do
23
+ @user.latest_changes.values.each do |value|
24
+ assert_kind_of Array, value
25
+ assert_equal 2, value.size
26
+ assert_equal value, value.uniq
27
+ end
28
+ end
29
+
30
+ should 'begin with the previous attribute values' do
31
+ changes = @user.latest_changes.inject({}){|h,(k,v)| h.update(k => v.first) }
32
+ previous = @previous_attributes.slice(*@user.latest_changes.keys)
33
+ assert_equal previous, changes
34
+ end
35
+
36
+ should 'end with the current attribute values' do
37
+ changes = @user.latest_changes.inject({}){|h,(k,v)| h.update(k => v.last) }
38
+ current = @current_attributes.slice(*@user.latest_changes.keys)
39
+ assert_equal current, changes
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,80 @@
1
+ require 'test_helper'
2
+
3
+ class RevertTest < Test::Unit::TestCase
4
+ context 'A model reversion' do
5
+ setup do
6
+ @user, @attributes, @times = User.new, {}, {}
7
+ names = ['Steve Richert', 'Stephen Richert', 'Stephen Jobs', 'Steve Jobs']
8
+ time = names.size.hours.ago
9
+ names.each do |name|
10
+ @user.update_attribute(:name, name)
11
+ @attributes[@user.version] = @user.attributes
12
+ time += 1.hour
13
+ @user.versions.last.update_attribute(:created_at, time)
14
+ @times[@user.version] = time
15
+ end
16
+ @user.reload.versions.reload
17
+ @first_version, @last_version = @attributes.keys.min, @attributes.keys.max
18
+ end
19
+
20
+ should 'do nothing for a non-existent version' do
21
+ attributes = @user.attributes
22
+ @user.revert_to!(nil)
23
+ assert_equal attributes, @user.attributes
24
+ end
25
+
26
+ should 'return the new version number' do
27
+ new_version = @user.revert_to(@first_version)
28
+ assert_equal @first_version, new_version
29
+ end
30
+
31
+ should 'change the version number when saved' do
32
+ current_version = @user.version
33
+ @user.revert_to!(@first_version)
34
+ assert_not_equal current_version, @user.version
35
+ end
36
+
37
+ should 'be able to target the first version' do
38
+ @user.revert_to(:first)
39
+ assert_equal @first_version, @user.version
40
+ end
41
+
42
+ should 'be able to target the last version' do
43
+ @user.revert_to(:last)
44
+ assert_equal @last_version, @user.version
45
+ end
46
+
47
+ should 'do nothing for a non-existent method name' do
48
+ current_version = @user.version
49
+ @user.revert_to(:bogus)
50
+ assert_equal current_version, @user.version
51
+ end
52
+
53
+ should 'be able to target a version number' do
54
+ @user.revert_to(1)
55
+ assert 1, @user.version
56
+ end
57
+
58
+ should 'be able to target a date and time' do
59
+ @times.each do |version, time|
60
+ @user.revert_to(time + 1.second)
61
+ assert_equal version, @user.version
62
+ end
63
+ end
64
+
65
+ should 'be able to target a version object' do
66
+ @user.versions.each do |version|
67
+ @user.revert_to(version)
68
+ assert_equal version.number, @user.version
69
+ end
70
+ end
71
+
72
+ should "correctly roll back the model's attributes" do
73
+ timestamps = %w(created_at created_on updated_at updated_on)
74
+ @attributes.each do |version, attributes|
75
+ @user.revert_to!(version)
76
+ assert_equal attributes.except(*timestamps), @user.attributes.except(*timestamps)
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,71 @@
1
+ ActiveRecord::Base.establish_connection(
2
+ :adapter => defined?(RUBY_ENGINE) && RUBY_ENGINE == 'jruby' ? 'jdbcsqlite3' : 'sqlite3',
3
+ :database => File.join(File.dirname(__FILE__), 'test.db')
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 :projects, :force => true do |t|
15
+ t.string :name
16
+ t.datetime :due_date, :default => 1.day.ago
17
+ t.timestamps
18
+ end
19
+
20
+ create_table :user_projects, :force => true do |t|
21
+ t.references :user
22
+ t.references :project
23
+ t.timestamps
24
+ end
25
+
26
+ create_table :versions, :force => true do |t|
27
+ t.belongs_to :versioned, :polymorphic => true
28
+ t.text :changes
29
+ t.integer :number
30
+ t.datetime :created_at
31
+ end
32
+ end
33
+ end
34
+
35
+ CreateSchema.suppress_messages do
36
+ CreateSchema.migrate(:up)
37
+ end
38
+
39
+ class UserProject < ActiveRecord::Base
40
+ belongs_to :user
41
+ belongs_to :project
42
+
43
+ def alert
44
+ raise 'UserProject'
45
+ end
46
+ end
47
+
48
+ class Project < ActiveRecord::Base
49
+ has_many :user_projects
50
+ has_many :users, :through => :user_projects
51
+
52
+ versioned
53
+ end
54
+
55
+
56
+ class User < ActiveRecord::Base
57
+ versioned
58
+
59
+ has_many_versioned :user_projects
60
+ has_many :projects, :through => :user_projects
61
+
62
+ def name
63
+ [first_name, last_name].compact.join(' ')
64
+ end
65
+
66
+ def name=(names)
67
+ self[:first_name], self[:last_name] = names.split(' ', 2)
68
+ end
69
+ end
70
+
71
+
@@ -0,0 +1,10 @@
1
+ $: << File.join(File.dirname(__FILE__), '..', 'lib')
2
+ $: << File.dirname(__FILE__)
3
+
4
+ require 'rubygems'
5
+ require 'test/unit'
6
+ require 'activerecord'
7
+ require 'shoulda'
8
+ require 'vestal_versions'
9
+ require 'schema'
10
+ begin; require 'redgreen'; rescue LoadError; end
@@ -0,0 +1,71 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run `rake gemspec`
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{vestal_versions}
8
+ s.version = "0.6.1"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["laserlemon"]
12
+ s.date = %q{2009-09-05}
13
+ s.description = %q{Keep a DRY history of your ActiveRecord models' changes}
14
+ s.email = %q{steve@laserlemon.com}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE",
17
+ "README.rdoc"
18
+ ]
19
+ s.files = [
20
+ ".gitignore",
21
+ "LICENSE",
22
+ "README.rdoc",
23
+ "Rakefile",
24
+ "VERSION",
25
+ "generators/vestal_versions_migration/templates/migration.rb",
26
+ "generators/vestal_versions_migration/vestal_versions_migration_generator.rb",
27
+ "init.rb",
28
+ "lib/version.rb",
29
+ "lib/vestal_versions.rb",
30
+ "test/associations_test.rb",
31
+ "test/between_test.rb",
32
+ "test/changes_test.rb",
33
+ "test/comparable_test.rb",
34
+ "test/creation_test.rb",
35
+ "test/latest_changes_test.rb",
36
+ "test/revert_test.rb",
37
+ "test/schema.rb",
38
+ "test/test_helper.rb",
39
+ "vestal_versions.gemspec"
40
+ ]
41
+ s.homepage = %q{http://github.com/laserlemon/vestal_versions}
42
+ s.rdoc_options = ["--charset=UTF-8"]
43
+ s.require_paths = ["lib"]
44
+ s.rubyforge_project = %q{laser-lemon}
45
+ s.rubygems_version = %q{1.3.5}
46
+ s.summary = %q{Keep a DRY history of your ActiveRecord models' changes}
47
+ s.test_files = [
48
+ "test/associations_test.rb",
49
+ "test/between_test.rb",
50
+ "test/changes_test.rb",
51
+ "test/comparable_test.rb",
52
+ "test/creation_test.rb",
53
+ "test/latest_changes_test.rb",
54
+ "test/revert_test.rb",
55
+ "test/schema.rb",
56
+ "test/test_helper.rb"
57
+ ]
58
+
59
+ if s.respond_to? :specification_version then
60
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
61
+ s.specification_version = 3
62
+
63
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
64
+ s.add_development_dependency(%q<thoughtbot-shoulda>, [">= 0"])
65
+ else
66
+ s.add_dependency(%q<thoughtbot-shoulda>, [">= 0"])
67
+ end
68
+ else
69
+ s.add_dependency(%q<thoughtbot-shoulda>, [">= 0"])
70
+ end
71
+ end
metadata ADDED
@@ -0,0 +1,91 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: andoq-vestal_versions
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.6.1
5
+ platform: ruby
6
+ authors:
7
+ - laserlemon
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-09-05 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: thoughtbot-shoulda
17
+ type: :development
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ description: Keep a DRY history of your ActiveRecord models' changes
26
+ email: steve@laserlemon.com
27
+ executables: []
28
+
29
+ extensions: []
30
+
31
+ extra_rdoc_files:
32
+ - LICENSE
33
+ - README.rdoc
34
+ files:
35
+ - .gitignore
36
+ - LICENSE
37
+ - README.rdoc
38
+ - Rakefile
39
+ - VERSION
40
+ - generators/vestal_versions_migration/templates/migration.rb
41
+ - generators/vestal_versions_migration/vestal_versions_migration_generator.rb
42
+ - init.rb
43
+ - lib/version.rb
44
+ - lib/vestal_versions.rb
45
+ - test/associations_test.rb
46
+ - test/between_test.rb
47
+ - test/changes_test.rb
48
+ - test/comparable_test.rb
49
+ - test/creation_test.rb
50
+ - test/latest_changes_test.rb
51
+ - test/revert_test.rb
52
+ - test/schema.rb
53
+ - test/test_helper.rb
54
+ - vestal_versions.gemspec
55
+ has_rdoc: false
56
+ homepage: http://github.com/laserlemon/vestal_versions
57
+ licenses:
58
+ post_install_message:
59
+ rdoc_options:
60
+ - --charset=UTF-8
61
+ require_paths:
62
+ - lib
63
+ required_ruby_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: "0"
68
+ version:
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: "0"
74
+ version:
75
+ requirements: []
76
+
77
+ rubyforge_project: laser-lemon
78
+ rubygems_version: 1.3.5
79
+ signing_key:
80
+ specification_version: 3
81
+ summary: Keep a DRY history of your ActiveRecord models' changes
82
+ test_files:
83
+ - test/associations_test.rb
84
+ - test/between_test.rb
85
+ - test/changes_test.rb
86
+ - test/comparable_test.rb
87
+ - test/creation_test.rb
88
+ - test/latest_changes_test.rb
89
+ - test/revert_test.rb
90
+ - test/schema.rb
91
+ - test/test_helper.rb