mountain-goat 1.0.1 → 1.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.
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