yesterday 0.2

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