yesterday 0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,206 @@
1
+ require 'yesterday'
2
+
3
+ describe Yesterday::Differ do
4
+ it 'should detect changes' do
5
+ from = {
6
+ 'id' => 1,
7
+ 'description' => 'Some description',
8
+ 'contacts' => [
9
+ {
10
+ 'id' => 1,
11
+ 'name' => 'Harold',
12
+ 'addresses' => [
13
+ { 'id' => 1,
14
+ 'address' => 'Sesamstreet 1' },
15
+ { 'id' => 2,
16
+ 'address' => 'Sesamstreet 2' }
17
+ ]
18
+ }
19
+ ]
20
+ }
21
+
22
+ to = {
23
+ 'id' => 1,
24
+ 'description' => 'Other description',
25
+ 'contacts' => [
26
+ {
27
+ 'id' => 1,
28
+ 'name' => 'Peter',
29
+ 'addresses' => [
30
+ { 'id' => 1,
31
+ 'address' => 'Foobar 1' },
32
+ { 'id' => 2,
33
+ 'address' => 'Foobar 2' }
34
+ ]
35
+ }
36
+ ]
37
+ }
38
+
39
+ Yesterday::Differ.new(from, to).diff.should == {
40
+ 'id' => 1,
41
+ 'description' => ['Some description', 'Other description'],
42
+ 'contacts' => [
43
+ {
44
+ 'id' => 1,
45
+ 'name' => ['Harold', 'Peter'],
46
+ 'addresses' => [
47
+ { 'id' => 1,
48
+ 'address' => ['Sesamstreet 1', 'Foobar 1'] },
49
+ { 'id' => 2,
50
+ 'address' => ['Sesamstreet 2', 'Foobar 2'] }
51
+ ]
52
+ }
53
+ ]
54
+ }
55
+ end
56
+
57
+ it 'should detect destroyed items' do
58
+ from = {
59
+ 'id' => 1,
60
+ 'description' => 'Some description',
61
+ 'contacts' => [
62
+ {
63
+ 'id' => 1,
64
+ 'name' => 'Harold',
65
+ 'addresses' => [
66
+ { 'id' => 1,
67
+ 'address' => 'Sesamstreet 1' },
68
+ { 'id' => 2,
69
+ 'address' => 'Sesamstreet 2' }
70
+ ]
71
+ },
72
+ {
73
+ 'id' => 2,
74
+ 'name' => 'Peter',
75
+ 'addresses' => [
76
+ { 'id' => 1,
77
+ 'address' => 'Sesamstreet 1' },
78
+ { 'id' => 2,
79
+ 'address' => 'Sesamstreet 2' }
80
+ ]
81
+ }
82
+ ]
83
+ }
84
+
85
+ to = {
86
+ 'id' => 1,
87
+ 'description' => 'Some description',
88
+ 'contacts' => [
89
+ {
90
+ 'id' => 1,
91
+ 'name' => 'Harold',
92
+ 'addresses' => [
93
+ { 'id' => 2,
94
+ 'address' => 'Sesamstreet 2' }
95
+ ]
96
+ }
97
+ ]
98
+ }
99
+
100
+ Yesterday::Differ.new(from, to).diff.should == {
101
+ 'id' => 1,
102
+ 'description' => ['Some description', 'Some description'],
103
+ 'contacts' => [
104
+ {
105
+ 'id' => 1,
106
+ 'name' => ['Harold', 'Harold'],
107
+ 'addresses' => [
108
+ { 'id' => 2,
109
+ 'address' => ['Sesamstreet 2', 'Sesamstreet 2'] }
110
+ ],
111
+ 'destroyed_addresses' => [
112
+ { 'id' => 1,
113
+ 'address' => 'Sesamstreet 1' }
114
+ ]
115
+ }
116
+ ],
117
+ 'destroyed_contacts' => [
118
+ {
119
+ 'id' => 2,
120
+ 'name' => 'Peter',
121
+ 'addresses' => [
122
+ { 'id' => 1,
123
+ 'address' => 'Sesamstreet 1' },
124
+ { 'id' => 2,
125
+ 'address' => 'Sesamstreet 2' }
126
+ ]
127
+ }
128
+ ]
129
+ }
130
+ end
131
+
132
+ it 'should detect created items' do
133
+ from = {
134
+ 'id' => 1,
135
+ 'description' => 'Some description',
136
+ 'contacts' => [
137
+ {
138
+ 'id' => 1,
139
+ 'name' => 'Harold',
140
+ 'addresses' => [
141
+ { 'id' => 2,
142
+ 'address' => 'Sesamstreet 2' }
143
+ ]
144
+ }
145
+ ]
146
+ }
147
+
148
+ to = {
149
+ 'id' => 1,
150
+ 'description' => 'Some description',
151
+ 'contacts' => [
152
+ {
153
+ 'id' => 1,
154
+ 'name' => 'Harold',
155
+ 'addresses' => [
156
+ { 'id' => 1,
157
+ 'address' => 'Sesamstreet 1' },
158
+ { 'id' => 2,
159
+ 'address' => 'Sesamstreet 2' }
160
+ ]
161
+ },
162
+ {
163
+ 'id' => 2,
164
+ 'name' => 'Peter',
165
+ 'addresses' => [
166
+ { 'id' => 1,
167
+ 'address' => 'Sesamstreet 1' },
168
+ { 'id' => 2,
169
+ 'address' => 'Sesamstreet 2' }
170
+ ]
171
+ }
172
+ ]
173
+ }
174
+
175
+ Yesterday::Differ.new(from, to).diff.should == {
176
+ 'id' => 1,
177
+ 'description' => ['Some description', 'Some description'],
178
+ 'contacts' => [
179
+ {
180
+ 'id' => 1,
181
+ 'name' => ['Harold', 'Harold'],
182
+ 'addresses' => [
183
+ { 'id' => 2,
184
+ 'address' => ['Sesamstreet 2', 'Sesamstreet 2'] }
185
+ ],
186
+ 'created_addresses' => [
187
+ { 'id' => 1,
188
+ 'address' => 'Sesamstreet 1' }
189
+ ]
190
+ }
191
+ ],
192
+ 'created_contacts' => [
193
+ {
194
+ 'id' => 2,
195
+ 'name' => 'Peter',
196
+ 'addresses' => [
197
+ { 'id' => 1,
198
+ 'address' => 'Sesamstreet 1' },
199
+ { 'id' => 2,
200
+ 'address' => 'Sesamstreet 2' }
201
+ ]
202
+ }
203
+ ]
204
+ }
205
+ end
206
+ end
data/spec/models.rb ADDED
@@ -0,0 +1,12 @@
1
+ class Contact < ActiveRecord::Base
2
+ has_and_belongs_to_many :addresses
3
+ tracks_changes
4
+ end
5
+
6
+ class Address < ActiveRecord::Base
7
+ end
8
+
9
+ class Company < ActiveRecord::Base
10
+ tracks_changes
11
+ exclude_tracking_for :attributes => [:created_at, :updated_at]
12
+ end
data/spec/schema.rb ADDED
@@ -0,0 +1,41 @@
1
+ ActiveRecord::Schema.define(:version => 1) do
2
+
3
+ create_table 'contacts', :force => true do |t|
4
+ t.string 'first_name'
5
+ t.string 'middle_name'
6
+ t.string 'last_name'
7
+ t.datetime 'created_at'
8
+ t.datetime 'updated_at'
9
+ end
10
+
11
+ create_table 'addresses', :force => true do |t|
12
+ t.string 'street'
13
+ t.string 'house_number'
14
+ t.string 'zipcode'
15
+ t.string 'city'
16
+ t.string 'country'
17
+ t.datetime 'created_at'
18
+ t.datetime 'updated_at'
19
+ end
20
+
21
+ create_table 'addresses_contacts', :id => false, :force => true do |t|
22
+ t.integer 'contact_id'
23
+ t.integer 'address_id'
24
+ end
25
+
26
+ create_table 'changesets', :force => true do |t|
27
+ t.integer 'changed_object_id'
28
+ t.string 'changed_object_type'
29
+ t.text 'object_attributes'
30
+ t.integer 'version_number'
31
+ t.datetime 'created_at'
32
+ t.datetime 'updated_at'
33
+ end
34
+
35
+ create_table 'companies', :force => true do |t|
36
+ t.string 'name'
37
+ t.datetime 'created_at'
38
+ t.datetime 'updated_at'
39
+ end
40
+
41
+ end
@@ -0,0 +1,66 @@
1
+ require 'yesterday'
2
+
3
+ describe Yesterday::Serializer do
4
+ it 'should serialize an activerecord object to a hash' do
5
+ contact = Contact.create(:first_name => 'foo', :last_name => 'bar')
6
+
7
+ address1 = Address.create(
8
+ :street => 'street',
9
+ :house_number => 1,
10
+ :zipcode => '1234AA',
11
+ :city => 'Armadillo',
12
+ :country => 'FooCountry'
13
+ )
14
+
15
+ address2 = Address.create(
16
+ :street => 'lane',
17
+ :house_number => 1337,
18
+ :zipcode => '2211AB',
19
+ :city => 'Cougar town',
20
+ :country => 'BarCountry'
21
+ )
22
+
23
+ contact.addresses << address1
24
+ contact.addresses << address2
25
+
26
+ Yesterday::Serializer.new(contact).to_hash.should == {
27
+ 'id' => contact.id,
28
+ 'first_name' => 'foo',
29
+ 'middle_name' => nil,
30
+ 'last_name' => 'bar',
31
+ 'created_at' => contact.created_at,
32
+ 'updated_at' => contact.updated_at,
33
+ 'addresses' => [
34
+ {
35
+ 'id' => address1.id,
36
+ 'street' => 'street',
37
+ 'house_number' => 1,
38
+ 'zipcode' => '1234AA',
39
+ 'city' => 'Armadillo',
40
+ 'country' => 'FooCountry',
41
+ 'created_at' => address1.created_at,
42
+ 'updated_at' => address1.updated_at
43
+ },
44
+ {
45
+ 'id' => address2.id,
46
+ 'street' => 'lane',
47
+ 'house_number' => 1337,
48
+ 'zipcode' => '2211AB',
49
+ 'city' => 'Cougar town',
50
+ 'country' => 'BarCountry',
51
+ 'created_at' => address2.created_at,
52
+ 'updated_at' => address2.updated_at
53
+ }
54
+ ]
55
+ }
56
+ end
57
+
58
+ it 'should not serialize every attribute or association if exclude_tracking_for is used' do
59
+ company = Company.create :name => 'foobar'
60
+
61
+ Yesterday::Serializer.new(company).to_hash.should == {
62
+ 'id' => company.id,
63
+ 'name' => 'foobar'
64
+ }
65
+ end
66
+ end
@@ -0,0 +1,41 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3
+ require 'rubygems'
4
+ require 'active_record'
5
+ require 'yaml'
6
+ require 'rspec'
7
+
8
+ RSpec.configure do |config|
9
+ config.before(:each) do
10
+ Contact.delete_all
11
+ Address.delete_all
12
+ Yesterday::Changeset.delete_all
13
+ end
14
+ end
15
+
16
+ $enabled = false
17
+
18
+ ActiveSupport::Notifications.subscribe(/sql/i) do |*args|
19
+ if $enabled
20
+ event = ActiveSupport::Notifications::Event.new(*args)
21
+ puts "SQL #{event.duration}: #{event.payload[:sql]}"
22
+ end
23
+ end
24
+
25
+ def connect(environment)
26
+ conf = YAML::load(File.open(File.dirname(__FILE__) + '/database.yml'))
27
+ ActiveRecord::Base.establish_connection(conf[environment])
28
+ end
29
+
30
+ # Open ActiveRecord connection
31
+ connect('test')
32
+
33
+ original_stdout = $stdout
34
+ $stdout = StringIO.new
35
+
36
+ begin
37
+ load(File.dirname(__FILE__) + "/schema.rb")
38
+ ensure
39
+ $stdout = original_stdout
40
+ end
41
+
@@ -0,0 +1,41 @@
1
+ require 'yesterday'
2
+
3
+ describe Yesterday::VersionedObjectCreator do
4
+ it 'should deserialize a hash into a structured set of historical item classes' do
5
+ from = {
6
+ 'id' => 1,
7
+ 'description' => ['Old description', 'New description'],
8
+ 'contacts' => [
9
+ {
10
+ 'id' => 1,
11
+ 'name' => ['Peter', 'Harold'],
12
+ 'addresses' => [
13
+ { 'id' => 1,
14
+ 'address' => ['Sesamstreet 1', 'Sesamstreet 1'] },
15
+ { 'id' => 2,
16
+ 'address' => ['Sesamstreet 2', 'Sesamstreet 2'] }
17
+ ]
18
+ }
19
+ ]
20
+ }
21
+
22
+ object = Yesterday::VersionedObjectCreator.new(from).to_object
23
+
24
+ require 'pp'
25
+
26
+ object.description.current.should == 'New description'
27
+ object.id.should == 1
28
+
29
+ object.contacts.should be_a(Array)
30
+ object.contacts.size.should == 1
31
+
32
+ object.contacts.first.id.should == 1
33
+ object.contacts.first.name.to_s.should == 'Harold'
34
+ object.contacts.first.name.previous.should == 'Peter'
35
+ object.contacts.first.addresses.size.should == 2
36
+ object.contacts.first.addresses.first.address.previous.should == 'Sesamstreet 1'
37
+ object.contacts.first.addresses.first.address.current.should == 'Sesamstreet 1'
38
+ object.contacts.first.addresses.last.address.previous.should == 'Sesamstreet 2'
39
+ object.contacts.first.addresses.last.address.current.should == 'Sesamstreet 2'
40
+ end
41
+ end
@@ -0,0 +1,195 @@
1
+ require 'spec_helper'
2
+ require 'yesterday'
3
+ require 'models'
4
+
5
+ describe Yesterday::Model do
6
+
7
+ it 'should have mixed in yesterday::model' do
8
+ Contact.new.should respond_to :version_number
9
+ Address.new.should_not respond_to :version_number
10
+ end
11
+
12
+ it 'should create a new changeset when saved' do
13
+ Yesterday::Changeset.count.should == 0
14
+ Contact.create :first_name => 'foo', :last_name => 'bar'
15
+ Yesterday::Changeset.count.should == 1
16
+ end
17
+
18
+ it 'should return all related changesets' do
19
+ contact = Contact.create(:first_name => 'foo', :last_name => 'bar')
20
+ contact.changesets.size.should == 1
21
+ contact.changesets.first.changed_object_type.should == 'Contact'
22
+ end
23
+
24
+ describe 'returning version numbers' do
25
+ it 'should return version number when new record' do
26
+ contact = Contact.new(:first_name => 'foo', :last_name => 'bar')
27
+ contact.version_number.should == 0
28
+ end
29
+
30
+ it 'should return version number after first save' do
31
+ contact = Contact.create(:first_name => 'foo', :last_name => 'bar')
32
+ contact.version_number.should == 1
33
+ end
34
+
35
+ it 'should return version number after update' do
36
+ contact = Contact.create(:first_name => 'foo', :last_name => 'bar')
37
+ contact.last_name = 'baz'
38
+ contact.save!
39
+
40
+ contact.version_number.should == 2
41
+ end
42
+ end
43
+
44
+ describe' returning older versions' do
45
+ it 'should not return and old version when there is no history' do
46
+ Contact.new
47
+ Contact.version(0).should == nil
48
+ end
49
+
50
+ it 'should return a historical item' do
51
+ contact = Contact.create!(:first_name => 'foo', :last_name => 'bar')
52
+ Contact.where(:id => contact.id).version(0).should == nil
53
+ Contact.where(:id => contact.id).version(1).should be_a(Yesterday::VersionedObject)
54
+ end
55
+
56
+ describe 'return a historical view of a version' do
57
+ before do
58
+ @contact = Contact.create!(:first_name => 'foo', :last_name => 'bar')
59
+ @contact.update_attributes :first_name => 'john', :last_name => 'dough'
60
+ @contact.update_attributes :first_name => 'ronald', :last_name => 'mcdonald'
61
+ end
62
+
63
+ it 'using scopes' do
64
+ @contact.version_number.should == 3
65
+
66
+ Contact.where(:id => @contact.id).version(2).first_name.should == 'john'
67
+ Contact.where(:id => @contact.id).version(2).last_name.should == 'dough'
68
+
69
+ Contact.where(:id => @contact.id).version(1).first_name.should == 'foo'
70
+ Contact.where(:id => @contact.id).version(1).last_name.should == 'bar'
71
+ end
72
+
73
+ it 'using instance method' do
74
+ @contact.version(2).first_name.should == 'john'
75
+ @contact.version(2).last_name.should == 'dough'
76
+
77
+ @contact.version(1).first_name.should == 'foo'
78
+ @contact.version(1).last_name.should == 'bar'
79
+ end
80
+ end
81
+ end
82
+
83
+ describe 'diffing two versions' do
84
+ it 'should diff version 1 with version 2' do
85
+ contact = Contact.create(:first_name => 'foo', :last_name => 'bar')
86
+ contact.update_attributes :first_name => 'john', :last_name => 'do'
87
+
88
+ diff = contact.diff_version(1, 2)
89
+ diff.should be_a(Yesterday::VersionedObject)
90
+
91
+ diff.first_name.current.should == 'john'
92
+ diff.first_name.previous.should == 'foo'
93
+
94
+ diff.last_name.current.should == 'do'
95
+ diff.last_name.previous.should == 'bar'
96
+ end
97
+
98
+ it 'should diff version 1 with version 3' do
99
+ contact = Contact.create(:first_name => 'foo', :last_name => 'bar')
100
+ contact.update_attributes :first_name => 'john', :last_name => 'do'
101
+ contact.update_attributes :first_name => 'edward', :last_name => 'foo'
102
+
103
+ diff = contact.diff_version(1, 3)
104
+ diff.should be_a(Yesterday::VersionedObject)
105
+
106
+ diff.first_name.current.should == 'edward'
107
+ diff.first_name.previous.should == 'foo'
108
+
109
+ diff.last_name.current.should == 'foo'
110
+ diff.last_name.previous.should == 'bar'
111
+ end
112
+
113
+ it 'should detect that new associations are created' do
114
+ contact = Contact.create(:first_name => 'foo', :last_name => 'bar')
115
+
116
+ contact.version_number.should == 1
117
+
118
+ address1 = Address.create(
119
+ :street => 'street',
120
+ :house_number => 1,
121
+ :zipcode => '1234AA',
122
+ :city => 'Armadillo',
123
+ :country => 'FooCountry'
124
+ )
125
+
126
+ address2 = Address.create(
127
+ :street => 'lane',
128
+ :house_number => 1337,
129
+ :zipcode => '2211AB',
130
+ :city => 'Cougar town',
131
+ :country => 'BarCountry'
132
+ )
133
+
134
+ contact.addresses = [address1, address2]
135
+ contact.save!
136
+
137
+ contact.version_number.should == 2
138
+
139
+ diff = contact.diff_version(1, 2)
140
+ diff.created_addresses.count.should == 2
141
+ diff.created_addresses.first.id.should == address1.id
142
+ diff.created_addresses.last.id.should == address2.id
143
+ end
144
+
145
+ describe 'association removal' do
146
+ before do
147
+ @contact = Contact.create(:first_name => 'foo', :last_name => 'bar')
148
+
149
+ @address1 = Address.create(
150
+ :street => 'street',
151
+ :house_number => 1,
152
+ :zipcode => '1234AA',
153
+ :city => 'Armadillo',
154
+ :country => 'FooCountry'
155
+ )
156
+
157
+ @address2 = Address.create(
158
+ :street => 'lane',
159
+ :house_number => 1337,
160
+ :zipcode => '2211AB',
161
+ :city => 'Cougar town',
162
+ :country => 'BarCountry'
163
+ )
164
+
165
+ @contact.addresses = [@address1, @address2]
166
+ @contact.save!
167
+
168
+ @contact.addresses = [@address1]
169
+ @address2.destroy
170
+
171
+ @contact.save!
172
+
173
+ @contact.version_number.should == 3
174
+ end
175
+
176
+ it 'should detect that associations are destroyed when diffing version 2 with 3' do
177
+
178
+ diff = @contact.diff_version(2, 3)
179
+ diff.addresses.count.should == 1
180
+ diff.destroyed_addresses.count.should == 1
181
+
182
+ diff.destroyed_addresses.first.id.should == @address2.id
183
+ diff.addresses.first.id.should == @address1.id
184
+ end
185
+
186
+ it 'should detect that associations are created when diffing version 1 with 3' do
187
+ diff = @contact.diff_version(1, 3)
188
+ diff.should_not respond_to(:addresses)
189
+ diff.created_addresses.count.should == 1
190
+ diff.created_addresses.first.id.should == @address1.id
191
+ end
192
+ end
193
+ end
194
+
195
+ end
data/yesterday.gemspec ADDED
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "yesterday/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "yesterday"
7
+ s.version = Yesterday::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Diederick Lawson"]
10
+ s.email = ["webmaster@altovista.nl"]
11
+ s.homepage = ""
12
+ s.summary = "Track history in your active record models"
13
+ s.description = "Track history and view any made changes in your active record models"
14
+
15
+ s.rubyforge_project = "yesterday"
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.require_paths = ["lib"]
21
+
22
+ s.add_development_dependency 'rspec'
23
+ s.add_development_dependency 'sqlite3'
24
+ s.add_dependency 'rails'
25
+ end