graph_mediator 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,50 @@
1
+ module GraphMediator
2
+ # Overrides to ActiveRecord::Optimistic::Locking to ensure that lock_column is updated
3
+ # during the +versioning+ phase of a mediated transaction.
4
+ module Locking
5
+
6
+ def self.included(base)
7
+ base.extend(ClassMethods)
8
+ base.send(:alias_method, :locking_enabled_without_mediation?, :locking_enabled?)
9
+ base.send(:include, InstanceMethods)
10
+ end
11
+
12
+ module ClassMethods
13
+ # Overrides ActiveRecord::Base.update_counters to skip locking if currently mediating
14
+ # the passed id.
15
+ def update_counters(ids, counters)
16
+ # id may be an array of ids...
17
+ unless currently_mediating?(ids)
18
+ # if none are being mediated can proceed as normal
19
+ super
20
+ else
21
+ # we have to go one by one unfortunately
22
+ Array(ids).each do |id|
23
+ currently_mediating?(id) ?
24
+ update_counters_without_lock(id, counters) :
25
+ super
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ module InstanceMethods
32
+ # Overrides ActiveRecord::Locking::Optimistic#locking_enabled?
33
+ #
34
+ # * True if we are not in a mediated_transaction and lock_enabled? is true
35
+ # per ActiveRecord (lock_column exists and lock_optimistically? true)
36
+ # * True if we are in a mediated_transaction and lock_enabled? is true per
37
+ # ActiveRecord and we are in the midst of the version bumping phase of the transaction.
38
+ #
39
+ # Effectively this ensures that an optimistic lock check and version bump
40
+ # occurs as usual outside of mediation but only at the end of the
41
+ # transaction within mediation.
42
+ def locking_enabled?
43
+ locking_enabled = locking_enabled_without_mediation?
44
+ locking_enabled &&= current_mediation_phase == :versioning if mediation_enabled? && currently_mediating?
45
+ return locking_enabled
46
+ end
47
+ end
48
+
49
+ end
50
+ end
@@ -0,0 +1,260 @@
1
+ require 'aasm'
2
+
3
+ module GraphMediator
4
+ # Instances of this class perform the actual mediation work on behalf of a
5
+ # Proxy#mediated_transaction.
6
+ class Mediator
7
+ include AASM
8
+
9
+ class IndexedHash < Hash
10
+ attr_reader :index
11
+ attr_reader :klass
12
+
13
+ def initialize(*args, &block)
14
+ @index = {}
15
+ super
16
+ end
17
+
18
+ def <<(ar_instance, klass, changes)
19
+ add_to_index(changes)
20
+ case
21
+ when ar_instance.new_record? then
22
+ created_array = self[:_created] ||= []
23
+ created_array << changes
24
+ when ar_instance.destroyed? then
25
+ destroyed_array = self[:_destroyed] ||= []
26
+ destroyed_array << ar_instance.id
27
+ else self[ar_instance.id] = changes
28
+ end
29
+ end
30
+
31
+ def add_to_index(changes)
32
+ index.merge!(changes)
33
+ end
34
+ end
35
+
36
+ class ChangesHash < IndexedHash
37
+
38
+ def <<(ar_instance)
39
+ raise(ArgumentError, "Expected an ActiveRecord::Dirty instance: #{ar_instance}") unless ar_instance.respond_to?(:changed?)
40
+ klass = ar_instance.class.base_class
41
+ changes = ar_instance.changes
42
+ add_to_index(changes)
43
+ klass_hash = self[klass] ||= IndexedHash.new
44
+ klass_hash.<<(ar_instance, klass, changes)
45
+ return self
46
+ end
47
+
48
+ # True if the given attribute was changed in root or a dependent.
49
+ #
50
+ # * attribute - symbol or string for attribute to lookup
51
+ # * klass - optionally, restrict lookup to changes for a particular class.
52
+ #
53
+ # Shortcut:
54
+ # changed_#{attribute}?
55
+ # #{my_class}_changed_#{attribute}?
56
+ #
57
+ def attribute_changed?(attribute, klass = nil)
58
+ (klass ? _class_hash(klass) : self).index.key?(attribute.to_s)
59
+ end
60
+
61
+ # True if all the passed attributes were changed in root or a dependent.
62
+ def all_changed?(*attributes)
63
+ attributes.all? { |a| attribute_changed?(a) }
64
+ end
65
+
66
+ # True if any of the passed attributes were changed in root or a dependent.
67
+ def any_changed?(*attributes)
68
+ attributes.any? { |a| attribute_changed?(a) }
69
+ end
70
+
71
+ # True if a dependent of the given class was added.
72
+ def added_dependent?(klass)
73
+ _class_hash(klass).key?(:_created)
74
+ end
75
+
76
+ # True if a dependent of the given class was destroyed.
77
+ def destroyed_dependent?(klass)
78
+ _class_hash(klass).key?(:_destroyed)
79
+ end
80
+
81
+ # True if an existing dependent of the given class was updated.
82
+ def altered_dependent?(klass)
83
+ !_class_hash(klass).reject { |k,v| k == :_created || k == :_destroyed }.empty?
84
+ end
85
+
86
+ # True only if a dependent of the given class was added or destroyed.
87
+ def added_or_destroyed_dependent?(klass)
88
+ added_dependent?(klass) || destroyed_dependent?(klass)
89
+ end
90
+
91
+ # TODO this and altered_dependent may give false positive for empty change hashes (if a record is saved with no changes, there will be a +record_id+ => {} entry)
92
+ # True if a dependent of the given class as added, destroyed or updated.
93
+ def touched_any_dependent?(klass)
94
+ !_class_hash(klass).empty?
95
+ end
96
+
97
+ # TODO raise an error if class does not respond to method? but what about general changed_foo? calls? The point is that this syntax can give you false negatives, because changed_foo? will be false even foo isn't even an attribute -- misspelling an attribute can lead to difficult bugs. This helper may not be a good idea...
98
+ def method_missing(method)
99
+ case method.to_s
100
+ when /(?:(.*)_)?changed_(.*)\?/
101
+ then
102
+ klass = $1
103
+ klass = klass.classify.constantize if klass
104
+ attribute = $2
105
+ # XXX Don't define a method here, or you run into issues with Rails class reloading.
106
+ # After the first call, you hold a reference to an old Class which will no longer work as
107
+ # a key in a new changes hash.
108
+ # self.class.__send__(:define_method, method) do
109
+ return attribute_changed?(attribute, klass)
110
+ # end
111
+ # return send(method)
112
+ else super
113
+ end
114
+ end
115
+
116
+ private
117
+
118
+ def _class_hash(klass)
119
+ self.fetch(klass.base_class, nil) || IndexedHash.new
120
+ end
121
+ end
122
+
123
+ # An instance of the root ActiveRecord object currently under mediation.
124
+ attr_accessor :mediated_instance
125
+
126
+ # Changes made to mediated_instance or dependents during a transaction.
127
+ attr_accessor :changes
128
+
129
+ # Tracks nested transactions
130
+ attr_accessor :stack
131
+
132
+ aasm_initial_state :idle
133
+ aasm_state :idle
134
+ aasm_state :mediating
135
+ aasm_state :versioning
136
+ aasm_state :disabled
137
+
138
+ aasm_event :start do
139
+ transitions :from => :idle, :to => :mediating
140
+ end
141
+ aasm_event :bump do
142
+ transitions :from => :mediating, :to => :versioning
143
+ end
144
+ aasm_event :disable do
145
+ transitions :from => :idle, :to => :disabled
146
+ end
147
+ aasm_event :done do
148
+ transitions :from => [:idle, :mediating, :versioning, :disabled], :to => :idle
149
+ end
150
+
151
+ def initialize(instance)
152
+ raise(ArgumentError, "Given instance has not been initialized for mediation: #{instance}") unless instance.kind_of?(GraphMediator)
153
+ self.mediated_instance = instance
154
+ self.changes = ChangesHash.new
155
+ self.stack = []
156
+ end
157
+
158
+ # Mediation may be disabled at either the Class or instance level.
159
+ # TODO - global module setting?
160
+ def mediation_enabled?
161
+ mediated_instance.mediation_enabled?
162
+ end
163
+
164
+ # The id of the instance we are mediating.
165
+ def mediated_id
166
+ mediated_instance.try(:id)
167
+ end
168
+
169
+ # Record the ActiveRecord changes state of the current object. This allows
170
+ # us to make decisions in after_mediation callbacks based on changed state.
171
+ def track_changes_for(ar_instance)
172
+ changes << ar_instance
173
+ end
174
+
175
+ # True if we are currently in a nested mediated transaction call.
176
+ def nested?
177
+ stack.size > 1
178
+ end
179
+
180
+ def mediate(&block)
181
+ debug("mediate called")
182
+ stack.push(self)
183
+ result = if idle?
184
+ begin_transaction &block
185
+ else
186
+ debug("nested transaction; mediate yielding instead")
187
+ yield self
188
+ end
189
+ debug("mediate finished successfully")
190
+ return result
191
+
192
+ ensure
193
+ done! unless nested? # very important, so that calling methods can ensure cleanup
194
+ stack.pop
195
+ end
196
+
197
+ [:debug, :info, :warn, :error, :fatal].each do |level|
198
+ define_method(level) do |message|
199
+ mediated_instance.send("m_#{level}", "\e[4;32;1m#{self} - #{aasm_current_state} :\e[0m #{message}")
200
+ end
201
+ end
202
+
203
+ # Reload them mediated instance. Throws an ActiveRecord::StaleObjectError
204
+ # if lock_column has been updated outside of transaction.
205
+ def refresh_mediated_instance
206
+ debug "refresh_mediated_instance called"
207
+ unless mediated_instance.new_record?
208
+ if mediated_instance.locking_enabled_without_mediation?
209
+ locking_column = mediated_instance.class.locking_column
210
+ current_lock_version = mediated_instance.send(locking_column) if locking_column
211
+ end
212
+ debug("reloading")
213
+ mediated_instance.reload
214
+ raise(ActiveRecord::StaleObjectError) if current_lock_version && current_lock_version != mediated_instance.send(locking_column)
215
+ end
216
+ end
217
+
218
+ private
219
+
220
+ def begin_transaction(&block)
221
+ debug("begin_transaction called")
222
+ result = if mediation_enabled?
223
+ start!
224
+ _wrap_in_callbacks &block
225
+ else
226
+ disable!
227
+ debug("mediation disabled; begin_transaction yielding instead")
228
+ yield self
229
+ end
230
+ debug("begin_transaction finished successfully")
231
+ return result
232
+ end
233
+
234
+ def _wrap_in_callbacks
235
+ debug("_wrap_in_callbacks called")
236
+ debug("_wrap_in_callbacks before_mediation")
237
+ mediated_instance.run_callbacks(:before_mediation)
238
+ debug("_wrap_in_callbacks before_mediation completed")
239
+ debug("_wrap_in_callbacks yielding")
240
+ result = yield self
241
+ # skip after_mediation if failed validation
242
+ unless !result.nil? && result == false
243
+ debug("_wrap_in_callbacks yielding completed")
244
+ debug("_wrap_in_callbacks mediate_reconciles")
245
+ mediated_instance.run_callbacks(:mediate_reconciles)
246
+ refresh_mediated_instance # after having reconciled
247
+ debug("_wrap_in_callbacks mediate_reconciles completed")
248
+ debug("_wrap_in_callbacks mediate_caches")
249
+ mediated_instance.run_callbacks(:mediate_caches)
250
+ debug("_wrap_in_callbacks mediate_caches completed")
251
+ debug("_wrap_in_callbacks bumping")
252
+ bump!
253
+ mediated_instance.touch if mediated_instance.class.locking_enabled?
254
+ debug("_wrap_in_callbacks bumping done")
255
+ refresh_mediated_instance # after having cached and versioned
256
+ end
257
+ return result
258
+ end
259
+ end
260
+ end
@@ -0,0 +1,3 @@
1
+ module GraphMediator
2
+ VERSION = "0.2.1"
3
+ end
data/spec/database.rb ADDED
@@ -0,0 +1,12 @@
1
+ require 'rubygems'
2
+
3
+ gem 'activerecord', '>=2.3.5'
4
+ require 'active_record'
5
+
6
+ ActiveRecord::Base.establish_connection({'adapter' => 'sqlite3', 'database' => ':memory:'})
7
+ ActiveRecord::Base.logger = Logger.new("#{File.dirname(__FILE__)}/active_record.log")
8
+
9
+ def create_schema(&block)
10
+ connection = ActiveRecord::Base.connection
11
+ yield connection if block_given?
12
+ end
@@ -0,0 +1,91 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper'))
2
+
3
+ create_schema do |conn|
4
+ conn.create_table(:people, :force => true) do |t|
5
+ t.string :name
6
+ t.string :type
7
+ end
8
+
9
+ conn.create_table(:rooms, :force => true) do |t|
10
+ t.string :building
11
+ t.string :number
12
+ end
13
+
14
+ conn.create_table(:courses, :force => true) do |t|
15
+ t.string :name
16
+ t.string :term
17
+ t.integer :year
18
+ t.belongs_to :people
19
+ t.belongs_to :room
20
+ t.integer :session_max
21
+ t.integer :course_version
22
+ end
23
+
24
+ conn.create_table(:schedules, :force => true) do |t|
25
+ t.belongs_to :room
26
+ t.belongs_to :course
27
+ t.belongs_to :session
28
+ t.string :day_of_the_week
29
+ t.time :start_time
30
+ t.time :end_time
31
+ end
32
+
33
+ conn.create_table(:assistants, :force => true) do |t|
34
+ t.belongs_to :course
35
+ t.belongs_to :people
36
+ end
37
+
38
+ conn.create_table(:sessions, :force => true) do |t|
39
+ t.belongs_to :assistant
40
+ t.belongs_to :room
41
+ end
42
+
43
+ conn.create_table(:students, :force => true) do |t|
44
+ t.belongs_to :people
45
+ t.belongs_to :course
46
+ t.string :grade
47
+ end
48
+
49
+ conn.create_table(:session_students, :force => true) do |t|
50
+ t.belongs_to :session
51
+ t.belongs_to :student
52
+ end
53
+ end
54
+
55
+ class Person < ActiveRecord::Base; end
56
+ class Lecturer < Person; end
57
+ class Student < Person; end
58
+ class GraduateStudent < Student; end
59
+
60
+ class Room < ActiveRecord::Base; end
61
+
62
+ class Course < ActiveRecord::Base
63
+
64
+ belongs_to :lecturer
65
+ belongs_to :room
66
+ has_many :schedules
67
+ has_many :assistants
68
+ has_many :sessions, :through => :assistants
69
+ has_many :students
70
+
71
+ # mediate :reconciliation => :adjust_bars, :bumping => :meeting_version
72
+
73
+ end
74
+
75
+ class Schedule < ActiveRecord::Base
76
+ belongs_to :course
77
+ belongs_to :room
78
+ end
79
+
80
+ class Assistant < ActiveRecord::Base
81
+ belongs_to :course
82
+ belongs_to :grad_student
83
+ has_many :sessions
84
+ has_many :schedule, :through => :session
85
+ end
86
+
87
+ class Session < ActiveRecord::Base
88
+ belongs_to :assistant
89
+ belongs_to :room
90
+ # has_many :schedule, :foreign_key =>
91
+ end
@@ -0,0 +1,288 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper'))
2
+ require 'aasm'
3
+
4
+ # Okay I lied. This example has dingos.
5
+
6
+ create_schema do |conn|
7
+
8
+ conn.create_table(:dingo_pens, :force => true) do |t|
9
+ t.integer :pen_number
10
+ t.integer :dingos_count
11
+ t.integer :feed_rate
12
+ t.integer :biscuit_minimum
13
+ t.integer :biscuit_maximum
14
+ t.float :total_biscuits
15
+ t.float :total_biscuit_weight
16
+ t.integer :lock_version, :default => 0
17
+ t.timestamps
18
+ end
19
+
20
+ conn.create_table(:dingos, :force => true) do |t|
21
+ t.belongs_to :dingo_pen
22
+ t.string :name
23
+ t.string :breed
24
+ t.integer :voracity
25
+ t.integer :belly
26
+ t.string :aasm_state
27
+ t.integer :lock_version, :default => 0
28
+ t.timestamps
29
+ end
30
+
31
+ conn.create_table(:biscuits, :force => true) do |t|
32
+ t.belongs_to :dingo_pen
33
+ t.string :type
34
+ t.float :weight
35
+ t.integer :amount
36
+ t.integer :lock_version, :default => 0
37
+ t.timestamps
38
+ end
39
+
40
+ end
41
+
42
+ # A dingo.
43
+ class Dingo < ActiveRecord::Base
44
+ belongs_to :dingo_pen, :counter_cache => true
45
+ include AASM
46
+ aasm_initial_state :hungry
47
+ aasm_state :hungry, :exit => :eat_biscuits!
48
+ aasm_state :satiated, :exit => :burn_biscuits!
49
+ aasm_event :eat do
50
+ transitions :from => :hungry, :to => :satiated, :guard => :full?
51
+ end
52
+ aasm_event :run do
53
+ transitions :from => :satiated, :to => :hungry
54
+ end
55
+
56
+ def eat_biscuits!
57
+ # puts "#{self}.eat_biscuits"
58
+ update_attributes(:belly => (belly || 0) + dingo_pen.eat_biscuits(voracity))
59
+ end
60
+
61
+ def burn_biscuits!
62
+ # puts "#{self}.burn_biscuits"
63
+ update_attributes(:belly => 0)
64
+ end
65
+
66
+ def full?
67
+ # puts "#{self}.full? #{belly}, #{voracity}"
68
+ belly >= voracity
69
+ end
70
+ end
71
+
72
+ # A bunch of biscuits.
73
+ class Biscuit < ActiveRecord::Base
74
+ belongs_to :dingo_pen
75
+
76
+ def consume_weight!(weight_to_consume)
77
+ amount_to_consume = (weight_to_consume/weight).round
78
+ amount_consumed = nil
79
+ if amount >= amount_to_consume
80
+ self.amount -= amount_to_consume
81
+ amount_consumed = amount_to_consume
82
+ else
83
+ amount_consumed = amount
84
+ self.amount = 0
85
+ end
86
+ save!
87
+ return amount_consumed * weight
88
+ end
89
+ end
90
+
91
+ class BigBiscuit < Biscuit; end
92
+ class LittleBiscuit < Biscuit; end
93
+
94
+ class DingoPen < ActiveRecord::Base
95
+ has_many :dingos
96
+ has_many :biscuits
97
+
98
+ include GraphMediator
99
+
100
+ def purchase_biscuits
101
+ puts :purchase_biscuits
102
+ end
103
+
104
+ mediate :purchase_biscuits,
105
+ :dependencies => [Dingo, Biscuit],
106
+ :when_reconciling => [:adjust_biscuit_supply, :feed_dingos],
107
+ :when_cacheing => :calculate_biscuit_totals
108
+
109
+ def adjust_biscuit_supply
110
+ #puts "\n* adjusting_biscuit_supply"
111
+ biscuits.each do |b|
112
+ b.update_attributes(:amount => DingoPen.shovel_biscuits((biscuit_minimum + biscuit_maximum)/2)) if b.amount < biscuit_minimum
113
+ end
114
+ end
115
+
116
+ def feed_dingos
117
+ #puts "** feed_dingos #{dingos.inspect}"
118
+ dingos.each { |d| d.eat! if d.hungry? }
119
+ end
120
+
121
+ def eat_biscuits(weight_desired)
122
+ #puts "** eat_biscuits #{weight_desired}"
123
+ total_weight_consumed = 0
124
+ weight_left_to_consume = weight_desired
125
+ biscuits.each do |b|
126
+ weight_consumed_from_bin = b.consume_weight!(weight_left_to_consume)
127
+ #puts "weight_consumed_from_bin #{b.inspect}: #{weight_consumed_from_bin}"
128
+ total_weight_consumed += weight_consumed_from_bin
129
+ weight_left_to_consume -= weight_consumed_from_bin
130
+ break if weight_left_to_consume <= 0
131
+ end
132
+ #puts "total_weight_consumed: #{total_weight_consumed}"
133
+ return total_weight_consumed
134
+ end
135
+
136
+ def calculate_biscuit_totals
137
+ #puts "** calculate_biscuit_totals #{biscuits.inspect}"
138
+ update_attributes(
139
+ :total_biscuits => biscuits.sum('amount'),
140
+ :total_biscuit_weight => biscuits.sum('weight * amount')
141
+ )
142
+ end
143
+
144
+ # Class methods
145
+ class << self
146
+ # simulates the shoveling of biscuits into DingoPen feeders from the
147
+ # theoretically BiscuitStore
148
+ def shovel_biscuits(amount)
149
+ return amount
150
+ end
151
+ end
152
+ end
153
+
154
+ describe "DingoPen" do
155
+
156
+ before(:each) do
157
+ @dingo_pen_attributes = {
158
+ :pen_number => 42,
159
+ :feed_rate => 10,
160
+ :biscuit_minimum => 50,
161
+ :biscuit_maximum => 100,
162
+ }
163
+ end
164
+
165
+ it "should initialize" do
166
+ dp = DingoPen.new
167
+ end
168
+
169
+ it "should create" do
170
+ dp = DingoPen.create!(@dingo_pen_attributes)
171
+ dp.lock_version.should == 1
172
+ end
173
+
174
+ it "should create with dingos and biscuits" do
175
+ dp = DingoPen.new(@dingo_pen_attributes)
176
+ dp.dingos << Dingo.new(:name => "Spot", :breed => "Patagonian Leopard Dingo", :voracity => 10)
177
+ dp.dingos << Dingo.new(:name => "Foo", :breed => "Theoretical Testing Dingo", :voracity => 5)
178
+ dp.biscuits << BigBiscuit.new(:amount => 35, :weight => 2.0)
179
+ dp.biscuits << LittleBiscuit.new(:amount => 75, :weight => 0.5)
180
+ dp.save!
181
+ dp.lock_version.should == 1
182
+ end
183
+
184
+ context "on create" do
185
+
186
+ it "should succeed" do
187
+ dp = DingoPen.new(@dingo_pen_attributes)
188
+ dp.dingos << Dingo.new(:name => "Spot", :breed => "Patagonian Leopard Dingo", :voracity => 10)
189
+ dp.dingos << Dingo.new(:name => "Foo", :breed => "Theoretical Testing Dingo", :voracity => 5)
190
+ dp.biscuits << BigBiscuit.new(:amount => 35, :weight => 2.0)
191
+ dp.biscuits << LittleBiscuit.new(:amount => 75, :weight => 0.5)
192
+ dp.save!
193
+ dp.reload
194
+ dp.dingos_count.should == 2
195
+ dp.dingos[0].belly.should == 10
196
+ dp.dingos[1].belly.should == 6
197
+ # biscuit amounts adjusted and dingos ate
198
+ dp.total_biscuits.should == 142
199
+ dp.total_biscuit_weight.should == 67 * 2 + 75 * 0.5
200
+ dp.lock_version.should == 1
201
+ end
202
+
203
+ it "should succed without mediation" do
204
+ begin
205
+ DingoPen.disable_all_mediation!
206
+ dp = DingoPen.new(@dingo_pen_attributes)
207
+ dp.dingos << Dingo.new(:name => "Spot", :breed => "Patagonian Leopard Dingo", :voracity => 10)
208
+ dp.dingos << Dingo.new(:name => "Foo", :breed => "Theoretical Testing Dingo", :voracity => 5)
209
+ dp.biscuits << BigBiscuit.new(:amount => 35, :weight => 2.0)
210
+ dp.biscuits << LittleBiscuit.new(:amount => 75, :weight => 0.5)
211
+ dp.save!
212
+ dp.reload
213
+ dp.dingos_count.should == 2
214
+ dp.total_biscuits.should be_nil
215
+ dp.total_biscuit_weight.should be_nil
216
+ dp.lock_version.should == 0
217
+ ensure
218
+ DingoPen.enable_all_mediation!
219
+ end
220
+ end
221
+
222
+ end
223
+
224
+ context "on update" do
225
+ it "should update_calculations after every child" do
226
+ dp = DingoPen.create!(@dingo_pen_attributes)
227
+ dp.dingos << Dingo.new(:name => "Spot", :breed => "Patagonian Leopard Dingo", :voracity => 10)
228
+ dp.dingos << Dingo.new(:name => "Foo", :breed => "Theoretical Testing Dingo", :voracity => 5)
229
+ dp.biscuits << BigBiscuit.new(:amount => 35, :weight => 2.0)
230
+ dp.biscuits << LittleBiscuit.new(:amount => 75, :weight => 0.5)
231
+ dp.reload
232
+ dp.dingos_count.should == 2
233
+ dp.dingos[0].belly.should == 10
234
+ dp.dingos[1].belly.should == 6
235
+ # biscuit amounts adjusted and dingos ate
236
+ dp.total_biscuits.should == 142
237
+ dp.total_biscuit_weight.should == 67 * 2 + 75 * 0.5
238
+ dp.lock_version.should == 5
239
+ end
240
+
241
+ it "should have updated calculations only once within a mediated transaction" do
242
+ dp = DingoPen.create!(@dingo_pen_attributes)
243
+ dp.mediated_transaction do
244
+ dp.dingos << Dingo.new(:name => "Spot", :breed => "Patagonian Leopard Dingo", :voracity => 10)
245
+ dp.dingos << Dingo.new(:name => "Foo", :breed => "Theoretical Testing Dingo", :voracity => 5)
246
+ dp.biscuits << BigBiscuit.new(:amount => 35, :weight => 2.0)
247
+ dp.biscuits << LittleBiscuit.new(:amount => 75, :weight => 0.5)
248
+ dp.save!
249
+ end
250
+ dp.reload
251
+ dp.dingos_count.should == 2
252
+ dp.dingos[0].belly.should == 10
253
+ dp.dingos[1].belly.should == 6
254
+ # biscuit amounts adjusted, and dingos ate
255
+ dp.total_biscuits.should == 142
256
+ dp.total_biscuit_weight.should == 67 * 2 + 75 * 0.5
257
+ dp.lock_version.should == 2
258
+ end
259
+ end
260
+
261
+ context "on delete" do
262
+ before(:each) do
263
+ @dp = DingoPen.new(@dingo_pen_attributes)
264
+ @dp.dingos << @spot = Dingo.new(:name => "Spot", :breed => "Patagonian Leopard Dingo", :voracity => 10)
265
+ @dp.dingos << Dingo.new(:name => "Foo", :breed => "Theoretical Testing Dingo", :voracity => 5)
266
+ @dp.biscuits << @big = BigBiscuit.new(:amount => 35, :weight => 2.0)
267
+ @dp.biscuits << LittleBiscuit.new(:amount => 75, :weight => 0.5)
268
+ @dp.save!
269
+ @spot.reload
270
+ @big.reload
271
+ end
272
+
273
+ it "should increment graph version after deletion of a child" do
274
+ lambda {
275
+ @spot.destroy
276
+ @dp.reload
277
+ }.should change(@dp, :lock_version).by(1)
278
+ end
279
+
280
+ it "should update_calculations after deletion of a child" do
281
+ @big.destroy
282
+ @dp.reload
283
+ @dp.total_biscuits.should == 75
284
+ @dp.total_biscuit_weight.should == 75 * 0.5
285
+ end
286
+
287
+ end
288
+ end