mountain-goat 1.0.1 → 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. data/.gitignore +2 -0
  2. data/README.md +5 -4
  3. data/generators/mg/mg_generator.rb +2 -2
  4. data/generators/mg/templates/update_mountain_goat_tables_v2.rb +73 -0
  5. data/lib/mountain-goat/controllers/mg/choices_controller.rb +81 -0
  6. data/lib/mountain-goat/controllers/mg/goals_controller.rb +136 -0
  7. data/lib/mountain-goat/controllers/mg/records_controller.rb +46 -0
  8. data/lib/mountain-goat/controllers/mg/tests_controller.rb +139 -0
  9. data/lib/mountain-goat/mg_core.rb +403 -0
  10. data/lib/mountain-goat/models/mg/choice.rb +50 -0
  11. data/lib/mountain-goat/models/mg/gi_meta.rb +10 -0
  12. data/lib/mountain-goat/models/mg/goal.rb +183 -0
  13. data/lib/mountain-goat/models/mg/goal_meta_type.rb +20 -0
  14. data/lib/mountain-goat/models/mg/gs_meta.rb +10 -0
  15. data/lib/mountain-goat/models/mg/record.rb +59 -0
  16. data/lib/mountain-goat/models/mg/test.rb +31 -0
  17. data/lib/mountain-goat/public/g-funnel.js +168 -0
  18. data/lib/mountain-goat/switch_choice.rb +32 -0
  19. data/lib/mountain-goat/version.rb +1 -1
  20. data/lib/mountain-goat/views/mountain_goat/layouts/.tmp_mountain_goat.html.erb.50680~ +76 -0
  21. data/lib/mountain-goat/views/mountain_goat/mg/choices/_choice_form.html.erb +37 -0
  22. data/lib/mountain-goat/views/mountain_goat/mg/choices/edit.html.erb +13 -0
  23. data/lib/mountain-goat/views/mountain_goat/mg/choices/index.html.erb +34 -0
  24. data/lib/mountain-goat/views/mountain_goat/mg/choices/new.html.erb +15 -0
  25. data/lib/mountain-goat/views/mountain_goat/mg/choices/show.html.erb +30 -0
  26. data/lib/mountain-goat/views/mountain_goat/mg/goals/.tmp__goal_form.html.erb.5027~ +0 -0
  27. data/lib/mountain-goat/views/mountain_goat/mg/goals/.tmp__goal_meta_type_form.html.erb.39992~ +0 -0
  28. data/lib/mountain-goat/views/mountain_goat/mg/goals/.tmp_edit.html.erb.55874~ +0 -0
  29. data/lib/mountain-goat/views/mountain_goat/mg/goals/.tmp_index.html.erb.97274~ +36 -0
  30. data/lib/mountain-goat/views/mountain_goat/mg/goals/_goal_form.html.erb +28 -0
  31. data/lib/mountain-goat/views/mountain_goat/mg/goals/_goal_meta_type_form.html.erb +34 -0
  32. data/lib/mountain-goat/views/mountain_goat/mg/goals/edit.html.erb +12 -0
  33. data/lib/mountain-goat/views/mountain_goat/mg/goals/index.html.erb +37 -0
  34. data/lib/mountain-goat/views/mountain_goat/mg/goals/new.html.erb +12 -0
  35. data/lib/mountain-goat/views/mountain_goat/mg/goals/show.html.erb +32 -0
  36. data/lib/mountain-goat/views/mountain_goat/mg/records/_record.html.erb +16 -0
  37. data/lib/mountain-goat/views/mountain_goat/mg/records/_records.html.erb +5 -0
  38. data/lib/mountain-goat/views/mountain_goat/mg/records/_records_form.html.erb +21 -0
  39. data/lib/mountain-goat/views/mountain_goat/mg/records/edit.html.erb +13 -0
  40. data/lib/mountain-goat/views/mountain_goat/mg/records/index.html.erb +17 -0
  41. data/lib/mountain-goat/views/mountain_goat/mg/records/new.html.erb +13 -0
  42. data/lib/mountain-goat/views/mountain_goat/mg/records/show.html.erb +14 -0
  43. data/lib/mountain-goat/views/mountain_goat/mg/report_items/.tmp__chart.html.erb.13419~ +18 -0
  44. data/lib/mountain-goat/views/mountain_goat/mg/report_items/.tmp__funnel.html.erb.60493~ +18 -0
  45. data/lib/mountain-goat/views/mountain_goat/mg/report_items/.tmp__report_item_form.html.erb.87420~ +10 -0
  46. data/lib/mountain-goat/views/mountain_goat/mg/report_items/.tmp__report_item_pivot_form.html.erb.77056~ +14 -0
  47. data/lib/mountain-goat/views/mountain_goat/mg/report_items/.tmp_edit.html.erb.31048~ +19 -0
  48. data/lib/mountain-goat/views/mountain_goat/mg/report_items/.tmp_new.html.erb.36371~ +17 -0
  49. data/lib/mountain-goat/views/mountain_goat/mg/report_items/_funnel.html.erb +13 -0
  50. data/lib/mountain-goat/views/mountain_goat/mg/reports/.tmp__report_form.html.erb.76535~ +21 -0
  51. data/lib/mountain-goat/views/mountain_goat/mg/reports/.tmp__report_report_items.html.erb.26030~ +5 -0
  52. data/lib/mountain-goat/views/mountain_goat/mg/reports/.tmp_edit.html.erb.78064~ +36 -0
  53. data/lib/mountain-goat/views/mountain_goat/mg/reports/.tmp_index.html.erb.74591~ +23 -0
  54. data/lib/mountain-goat/views/mountain_goat/mg/reports/.tmp_show.html.erb.58427~ +21 -0
  55. data/lib/mountain-goat/views/mountain_goat/mg/tests/_test_form.html.erb +25 -0
  56. data/lib/mountain-goat/views/mountain_goat/mg/tests/edit.html.erb +12 -0
  57. data/lib/mountain-goat/views/mountain_goat/mg/tests/index.html.erb +48 -0
  58. data/lib/mountain-goat/views/mountain_goat/mg/tests/new.html.erb +12 -0
  59. data/lib/mountain-goat/views/mountain_goat/mg/tests/show.html.erb +63 -0
  60. metadata +69 -6
  61. data/lib/mountain-goat/metric_tracking.rb +0 -401
  62. data/lib/mountain-goat/switch_variant.rb +0 -32
@@ -0,0 +1,403 @@
1
+ require File.join([File.dirname(__FILE__), 'switch_choice'])
2
+
3
+ module MgCore
4
+
5
+ #Metric Tracking routes
6
+ class << ActionController::Routing::Routes;self;end.class_eval do
7
+ define_method :clear!, lambda {}
8
+ end
9
+
10
+ Mime::Type.register "application/xhtml+xml", :xhtml
11
+
12
+ ActionController::Routing::Routes.draw do |map|
13
+ map.namespace :mg do |mg|
14
+ mg.mg '/mg', :controller => :goals, :action => :index, :path_prefix => ""
15
+ mg.login '/login', :controller => :mountain_goat, :action => :login
16
+ mg.login_create '/login/create', :controller => :mountain_goat, :action => :login_create
17
+ mg.resources :choices
18
+ mg.resources :goals, :has_many => [ :record ], :member => { :hide => :get, :unhide => :get }
19
+ mg.resources :tests, :has_many => :choices, :member => { :hide => :get, :unhide => :get }
20
+ mg.resources :records, :collection => { :new_records => :get }
21
+ mg.resources :reports, :has_many => :report_items, :member => { :show_svg => :get, :hide => :get, :unhide => :get }
22
+ mg.resources :report_items, :member => { :destroy => :get, :update => :post }, :collection => { :get_extra => :get }
23
+ mg.resources :playground, :collection => { :test => :get }
24
+ mg.new_records '/records/new', :controller => :records, :action => :new_records
25
+ mg.fresh_choices '/fresh-choices', :controller => :tests, :action => :fresh_choices
26
+ mg.connect '/public/:file', :controller => :mountain_goat, :action => :fetch
27
+ end
28
+ end
29
+
30
+ module Controller
31
+
32
+ #This is just for testing
33
+ def mg_rand(evaluate = false)
34
+ return "(SELECT #{@mg_i.nil? ? 1 : @mg_i.to_f})" if defined?(MOUNTAIN_GOAT_TEST) && MOUNTAIN_GOAT_TEST
35
+ evaluate ? rand.to_f : "RAND()"
36
+ end
37
+
38
+ def mg_epsilon
39
+ if @mg_epsilon.nil?
40
+ @mg_epsilon = 0.1 #default
41
+ mg_yml = nil
42
+ begin
43
+ mg_yml = YAML::load(File.open("#{RAILS_ROOT}/config/mountain-goat.yml"))
44
+ rescue
45
+ end
46
+ if mg_yml
47
+ if mg_yml.has_key?(RAILS_ENV) && mg_yml[RAILS_ENV].has_key?('epsilon')
48
+ @mg_epsilon = mg_yml[RAILS_ENV]['epsilon'].to_f
49
+ elsif mg_yml.has_key?('settings') && mg_yml['settings'].has_key?('epsilon')
50
+ @mg_epsilon = mg_yml['settings']['epsilon'].to_f
51
+ end
52
+ end
53
+ end
54
+ return @mg_epsilon
55
+ end
56
+
57
+ def mg_strategy
58
+ if @mg_strategy.nil?
59
+ @mg_strategy = 'e-greedy' #default
60
+ mg_yml = nil
61
+ begin
62
+ mg_yml = YAML::load(File.open("#{RAILS_ROOT}/config/mountain-goat.yml"))
63
+ rescue
64
+ end
65
+ if mg_yml
66
+ if mg_yml.has_key?(RAILS_ENV) && mg_yml[RAILS_ENV].has_key?('strategy')
67
+ @mg_strategy = mg_yml[RAILS_ENV]['strategy']
68
+ elsif mg_yml.has_key?('settings') && mg_yml['settings'].has_key?('strategy')
69
+ @mg_strategy = mg_yml['settings']['strategy']
70
+ end
71
+ end
72
+ end
73
+ return @mg_strategy
74
+ end
75
+
76
+ def mg_apply_strategy(test)
77
+ case mg_strategy.downcase
78
+ when 'e-greedy'
79
+ logger.warn Mg::Choice.all(:order => "CASE WHEN served = 0 THEN 1 ELSE 0 END DESC, CASE WHEN #{mg_rand(true).to_f} < #{mg_epsilon.to_f} THEN #{mg_rand} ELSE CASE WHEN served = 0 THEN -1 ELSE reward / served END END DESC, #{mg_rand} DESC", :conditions => { :mg_test_id => test.id } )
80
+ return Mg::Choice.first(:order => "CASE WHEN served = 0 THEN 1 ELSE 0 END DESC, CASE WHEN #{mg_rand(true).to_f} < #{mg_epsilon.to_f} THEN #{mg_rand} ELSE CASE WHEN served = 0 THEN -1 ELSE reward / served END END DESC, #{mg_rand} DESC", :conditions => { :mg_test_id => test.id } )
81
+ when 'e-greedy-decreasing'
82
+ return Mg::Choice.first(:order => "CASE WHEN served = 0 THEN 1 ELSE 0 END DESC,
83
+ CASE WHEN #{mg_rand(true).to_f} < #{mg_epsilon.to_f} / ( select sum(served) from mg_metric_variants where metric_id = #{ metric.id.to_i } ) THEN #{mg_rand} ELSE CASE WHEN served = 0 THEN -1 ELSE reward / served END END DESC,
84
+ #{mg_rand} DESC", :conditions => { :mg_test_id => test.id } ) # * log( ( select sum(served) from mg_metric_variants where metric_id = #{ metric.id.to_i } ) )
85
+ when 'a/b'
86
+ return Mg::Choice.first(:order => "#{mg_rand} DESC", :conditions => { :mg_test_id => test.id } )
87
+ else
88
+ raise "Invalid strategy #{mg_strategy}"
89
+ end
90
+ end
91
+
92
+ def mg_storage
93
+ if @mg_storage.nil?
94
+ @mg_storage = defined?(cookies) ? cookies : nil
95
+
96
+ mg_yml = nil
97
+ begin
98
+ mg_yml = YAML::load(File.open("#{RAILS_ROOT}/config/mountain-goat.yml"))
99
+ rescue
100
+ end
101
+ if mg_yml
102
+ if mg_yml.has_key?(RAILS_ENV) && mg_yml[RAILS_ENV].has_key?('storage')
103
+ uc = mg_yml[RAILS_ENV]['storage'].strip
104
+ @mg_storage = ( uc == "cookies" && defined?(cookies) ) ? cookies : ( uc == "session" && defined?(session) ) ? session : nil
105
+ elsif mg_yml.has_key?('settings') && mg_yml['settings'].has_key?('storage')
106
+ uc = mg_yml['settings']['storage'].strip
107
+ @mg_storage = ( uc == "cookies" && defined?(cookies) ) ? cookies : ( uc == "session" && defined?(session) ) ? session : nil
108
+ end
109
+ end
110
+ end
111
+ @mg_storage = {} if @mg_storage.nil? #'none'
112
+ return @mg_storage
113
+ end
114
+
115
+ ######################
116
+ # Bandit Tracking #
117
+ ######################
118
+
119
+ def bds(test_type, &block)
120
+ raise ArgumentError, "Switch choice needs block" if !block_given?
121
+ test = get_test( test_type, true )
122
+ block.call(SwitchChoice.new( logger, test, nil ) )
123
+
124
+ var = get_switch_choice( test_type )
125
+ block.call(SwitchChoice.new( logger, test, var ) )
126
+ end
127
+
128
+ def bd(test_type, default, opts = {}, opt = nil)
129
+ return get_choice(test_type, default, opts, opt)[:value]
130
+ end
131
+
132
+ def bdd(test_type, default, opts = {}, opt = nil)
133
+ return get_choice(test_type, default, opts, opt)
134
+ end
135
+
136
+ #Legacy
137
+ def sv(test_type, goal_type, &block)
138
+ bds(test_type, &block)
139
+ end
140
+
141
+ def mv(test_type, goal_type, default, opts = {}, opt = nil)
142
+ bd(test_type, default, opts, opt)
143
+ end
144
+
145
+ def mv_detailed(test_type, goal_type, default, opts = {}, opt = nil)
146
+ bdd(test_type, default, opts, opt)
147
+ end
148
+
149
+ #shorthand
150
+ def rw(goal_type, reward, options = {})
151
+ self.bandit_reward(goal_type, reward, options)
152
+ end
153
+
154
+ def rc(goal_type, options = {})
155
+ self.bandit_reward(goal_type, 1, options)
156
+ end
157
+
158
+ def record_conversion(goal_type, options = {})
159
+ self.bandit_reward(goal_type, 1, options)
160
+ end
161
+
162
+ #allows bandit_reward(goal, options)
163
+ def bandit_reward(goal_type, reward, options = {})
164
+
165
+ if reward.is_a?(Hash) #allow arguments bandit_reward(test, options)
166
+ options = reward
167
+ reward = 0
168
+ end
169
+
170
+ tests = {} #for user-defined metrics
171
+ options = options.with_indifferent_access
172
+
173
+ MountainGoat.get_meta_options.each do |k, v|
174
+ if options.include?(k) && options[k]
175
+ options.delete(k)
176
+ res = v.call(self)
177
+ options.merge!( res ) if !res.nil? && res.instance_of?(Hash)
178
+ end
179
+ end
180
+
181
+ options.each do |k, v|
182
+ if k.to_s =~ /^test_(\w+)$/i
183
+ options.delete k
184
+ tests.merge!({ $1, v })
185
+ end
186
+ end
187
+
188
+ logger.warn "Recording goal #{goal_type.to_s} with options #{options.inspect}"
189
+
190
+ goal = Mg::Goal.first( :conditions => { :goal_type => goal_type.to_s } )
191
+
192
+ # Now, we just create the goal if we don't have one
193
+ goal = Mg::Goal.create!( :goal_type => goal_type.to_s, :name => goal_type.to_s, :rewards_total => reward, :rewards_given => 1 ) if goal.nil?
194
+
195
+ # First, let's tally for the goal itself
196
+ goal.tally_reward_given( reward )
197
+
198
+ # We need to see what meta information we should fill based on the goal type
199
+ Mg::Record.create!( { :mg_goal_id => goal.id, :reward => reward } ).set_meta_data(options)
200
+
201
+ # User-defined test tallies
202
+ tests.each do |test_type, choice_id|
203
+ t = Mg::Test.find_by_test_type(test_type)
204
+ if t.nil?
205
+ logger.warn "Missing user-defined test #{test_type}"
206
+ next
207
+ end
208
+
209
+ c = t.choices.first( :conditions => { :id => choice_id } ) #make sure everything matches up
210
+
211
+ if c.nil?
212
+ logger.warn "Choice #{choice_id} not in choices for #{t.title}"
213
+ next
214
+ end
215
+
216
+ logger.warn "Tallying goal #{goal.name} for #{t.title} - #{c.name} (#{c.value} - #{c.id})"
217
+ c.tally_goal(goal, reward)
218
+ end
219
+
220
+ if !mg_storage.nil?
221
+ #we just converted, let's tally each of our metrics (from cookies or session)
222
+ Mg::Test.all.each do |test|
223
+ test_sym = "test_#{test.test_type}".to_sym
224
+ choice_sym = "test_#{test.test_type}_choice".to_sym
225
+
226
+ value = mg_storage[test_sym]
227
+ choice_id = mg_storage[choice_sym]
228
+
229
+ #logger.warn "Value: #{metric_sym} - #{value}"
230
+ #logger.warn "Value: #{metric_variant_sym} - #{variant_id}"
231
+
232
+ if choice_id.blank? #the user just doesn't have this set
233
+ #This is now common-case
234
+ next
235
+ end
236
+
237
+ choice = Mg::Choice.first(:conditions => { :id => choice_id.to_i } )
238
+
239
+ if choice.nil?
240
+ logger.error "Choice #{choice_id} not in choices for #{test.title}"
241
+ next
242
+ end
243
+
244
+ if choice.value != value
245
+ logger.warn "Choice #{choice.name} values differ for test #{test.title}. '#{choice.value}' != '#{value}'!"
246
+ end
247
+
248
+ logger.warn "Tallying goal #{goal.name} for #{test.title} - #{choice.name} (#{choice.value} - #{choice.id})"
249
+ choice.tally_goal(goal, reward)
250
+ end
251
+ end
252
+ end
253
+
254
+ private
255
+
256
+ #returns a map { :value => value, :choice_id => id }
257
+ def get_choice(test_type, default, opts = {}, opt = nil)
258
+ test_sym = "test_#{test_type}#{ opt.nil? ? "" : '_' + opt.to_s }".to_sym
259
+ choice_sym = "test_#{test_type}_choice".to_sym
260
+
261
+ #first, we'll check for a cookie value
262
+ if !mg_storage.nil? && mg_storage[test_sym] && !mg_storage[test_sym].blank?
263
+ #we have the cookie
264
+ choice_id = mg_storage[choice_sym]
265
+ choice = Mg::Choice.first(:conditions => { :id => choice_id.to_i } )
266
+ if !choice.nil?
267
+ if choice.mg_test.tally_each_serve
268
+ choice.tally_serve
269
+ end
270
+ else
271
+ logger.warn "Serving test #{test_type} #{ opt.nil? ? "" : opt.to_s } without finding / tallying choice."
272
+ end
273
+
274
+ return { :value => mg_storage[test_sym], :choice_id => mg_storage[choice_sym] } #it's the best we can do
275
+ else
276
+ #we don't have the cookie, let's find a value to set
277
+ test = get_test( test_type, false )
278
+
279
+ choice = mg_apply_strategy(test)
280
+
281
+ if choice.nil?
282
+ logger.warn "Missing choices for #{test_type}"
283
+ choice = Mg::Choice.create!( { :mg_test_id => test.id, :value => default, :name => default }.merge(opts) )
284
+ end
285
+
286
+ if choice.mg_test.tally_each_serve
287
+ choice.tally_serve # denote we served this to a user
288
+ end
289
+
290
+ value = choice.read_attribute( opt.nil? ? :value : opt )
291
+ logger.debug "Serving #{choice.name} (#{value}) for #{test_sym}"
292
+ #good, we have a variant, let's store it in session
293
+
294
+ if !mg_storage.nil?
295
+ mg_storage[test_sym] = value #, :domain => WILD_DOMAIN
296
+ mg_storage[choice_sym] = choice.id #, :domain => WILD_DOMAIN
297
+ end
298
+
299
+ return { :value => value, :choice_id => choice.id }
300
+ end
301
+ end
302
+
303
+ def get_switch_choice(test_type)
304
+ choice_sym = "test_#{test_type}_choice".to_sym
305
+
306
+ #first, we'll check for a cookie selection
307
+ if !mg_storage.nil? && mg_storage[choice_sym] && !mg_storage[choice_sym].blank?
308
+ #we have the cookie
309
+
310
+ choice_id = mg_storage[choice_sym]
311
+ choice = Mg::Choice.first(:conditions => { :id => choice_id.to_i } )
312
+
313
+ if !choice.nil?
314
+ if choice.mg_test.tally_each_serve
315
+ choice.tally_serve
316
+ end
317
+
318
+ return choice
319
+
320
+ end
321
+
322
+ #otherwise, it's a big wtf? let's just move on
323
+ logger.warn "Missing choice for #{test_type} (switch-type), reassigning..."
324
+ end
325
+
326
+ #we don't have the cookie, let's find a value to set
327
+ test = get_test( test_type, true )
328
+
329
+ choice = mg_apply_strategy(test)
330
+
331
+ if choice.nil?
332
+ logger.warn "Missing choices for #{test_type}"
333
+ raise ArgumentError, "Missing choices for switch-type #{test_type}"
334
+ end
335
+
336
+ if choice.mg_test.tally_each_serve
337
+ choice.tally_serve # denote we served this to a user
338
+ end
339
+
340
+ logger.debug "Serving #{choice.name} (#{choice.switch_type}) for #{test.title} (switch-type)"
341
+ #good, we have a variant, let's store it in session (not the value, just the selection)
342
+ if !mg_storage.nil?
343
+ mg_storage[choice_sym] = choice.id #, :domain => WILD_DOMAIN
344
+ end
345
+
346
+ return choice
347
+ end
348
+
349
+ def get_test(test_type, is_switch = false)
350
+
351
+ test = Mg::Test.first(:conditions => { :test_type => test_type.to_s } )
352
+
353
+ if test.nil? #we don't have a metric of this type
354
+ logger.warn "Missing test type #{test_type.to_s} -- creating"
355
+ test = Mg::Test.create( :test_type => test_type.to_s, :title => test_type.to_s, :is_switch => is_switch )
356
+ end
357
+
358
+ return test
359
+ end
360
+ end
361
+
362
+ module View
363
+ def mv(*args, &block)
364
+ @controller.send(:mv, *args, &block)
365
+ end
366
+
367
+ def mv_detailed(*args, &block)
368
+ @controller.send(:mv_detailed, *args, &block)
369
+ end
370
+
371
+ def sv(*args, &block)
372
+ @controller.send(:sv, *args, &block)
373
+ end
374
+
375
+ def bd(*args, &block)
376
+ @controller.send(:bd, *args, &block)
377
+ end
378
+
379
+ def bdd(*args, &block)
380
+ @controller.send(:bdd, *args, &block)
381
+ end
382
+
383
+ def bds(*args, &block)
384
+ @controller.send(:bds, *args, &block)
385
+ end
386
+ end
387
+ end
388
+
389
+ class ActionController::Base
390
+ include MgCore::Controller
391
+ end
392
+
393
+ class ActionView::Base
394
+ include MgCore::View
395
+ end
396
+
397
+ class ActionMailer::Base
398
+ include MgCore::Controller
399
+ end
400
+
401
+ class ActiveRecord::Base
402
+ include MgCore::Controller
403
+ end
@@ -0,0 +1,50 @@
1
+ # Mg::Choice represents a split of an a/b test
2
+ #
3
+ # Attributes
4
+ # mg_test_id:: ID of Mg::Test which this is a choice for
5
+ # name::
6
+ # value:: The value to serve when this choice comes up (assuming non-switch)
7
+ # opt1:: Optional additional data
8
+ # opt2:: Optional additional data
9
+ # served:: Number of times this choice has been served
10
+ # reward:: Total accumulated reward for this choice
11
+ # reward_count:: How many rewards factor into this total
12
+ # switch_type:: Is this a switch-type choice
13
+ # deleted_at:: Is this choice still active?
14
+ class Mg::Choice < ActiveRecord::Base
15
+ set_table_name :mg_choices
16
+
17
+ # ActiveRecord Associations
18
+ belongs_to :mg_test, :class_name => "Mg::Test"
19
+
20
+ # Validations
21
+ validates_presence_of :name
22
+ validates_presence_of :mg_test_id
23
+
24
+ # Member Functions
25
+
26
+ # Mark that we have served this choice
27
+ def tally_serve
28
+ self.transaction do
29
+ self.update_attribute(:reward, 0) if self.reward.nil? #we should merge this with the next line, but whatever
30
+ Mg::Choice.update_counters(self.id, :served => 1)
31
+ end
32
+
33
+ return self.reload
34
+ end
35
+
36
+ # Reward has a "default" or adjustable setting
37
+ def tally_goal(goal, reward)
38
+ self.transaction do
39
+ Mg::Choice.update_counters(self.id, :reward_count => 1, :reward => reward)
40
+ end
41
+
42
+ return self.reload
43
+ end
44
+
45
+ # What is the average reward given to this choice
46
+ def reward_rate
47
+ return nil if self.reward_count == 0 || self.reward_count.nil? || self.reward.nil?
48
+ return self.reward / self.reward_count.to_f
49
+ end
50
+ end