abongo 0.0.1

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,20 @@
1
+ Copyright (c) 2009 Patrick McKenzie
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1 @@
1
+ Details to come
@@ -0,0 +1,292 @@
1
+ class Abongo
2
+ @@VERSION = '0.0.1'
3
+ def self.VERSION; @@VERSION; end
4
+ @@MAJOR_VERSION = '0'
5
+ def self.MAJOR_VERSION; @@MAJOR_VERSION; end
6
+
7
+ @@options ||= {}
8
+ def self.options; @@options; end
9
+ def self.options=(options); @@options = options; end
10
+
11
+ @@salt = 'Not really necessary.'
12
+ def self.salt; @@salt; end
13
+ def self.salt=(salt); @@salt = salt; end
14
+
15
+ def self.db; @@db; end
16
+ def self.db=(db)
17
+ @@db = db
18
+ @@experiments = db['abongo_experiments']
19
+ @@conversions = db['abongo_conversions']
20
+ @@participants = db['abongo_participants']
21
+ @@alternatives = db['abongo_alternatives']
22
+ end
23
+
24
+ def self.identity=(new_identity)
25
+ @@identity = new_identity.to_s
26
+ end
27
+
28
+ def self.identity
29
+ @@identity ||= rand(10 ** 10)
30
+ end
31
+
32
+ def self.flip(test_name, options = {})
33
+ if block_given?
34
+ yield(self.test(test_name, [true, false], options))
35
+ else
36
+ self.test(test_name, [true, false], options)
37
+ end
38
+ end
39
+
40
+ def self.test(test_name, alternatives, options = {})
41
+ # Test for short-circuit (the test has been ended)
42
+ test = Abongo.get_test(test_name)
43
+ return test['final'] unless test.nil? or test['final'].nil?
44
+
45
+ # Create the test (if necessary)
46
+ unless test
47
+ conversion_name = options[:conversion] || options[:conversion_name]
48
+ test = Abongo.start_experiment!(test_name, self.parse_alternatives(alternatives), conversion_name)
49
+ end
50
+
51
+ # Should expired be part of the find_participant?
52
+ participant = Abongo.find_participant(Abongo.identity)
53
+ expired = participant['expires'] ? (participant['expires'] < Time.now) : false
54
+
55
+ choice = self.find_alternative_for_user(Abongo.identity, test)
56
+ participating_tests = participant['tests']
57
+
58
+ # TODO: Pull participation add out
59
+ if options[:multiple_participation] || !participating_tests.include?(test['_id']) || expired
60
+ unless participating_tests.include?(test['_id'])
61
+ Abongo.add_participation(identity, test['_id'], self.expires_in(participant['human']))
62
+ end
63
+
64
+ # Small timing issue in here
65
+ if (!@@options[:count_humans_only] || participant['human'])
66
+ Abongo.alternatives.update({:content => choice, :test => test['_id']}, {:$inc => {:participants => 1}})
67
+ Abongo.experiments.update({:_id => test['_id']}, {'$inc' => {:participants => 1}})
68
+ end
69
+ end
70
+
71
+ if block_given?
72
+ yield(choice)
73
+ else
74
+ choice
75
+ end
76
+ end
77
+
78
+ def self.bongo!(name = nil, options = {})
79
+ if name.kind_of? Array
80
+ name.map do |single_test|
81
+ self.bongo!(single_test, options)
82
+ end
83
+ else
84
+ if name.nil?
85
+ # Score all participating tests
86
+ participant = Abongo.find_participant(Abongo.identity)
87
+ participating_tests = participant['tests']
88
+ participating_tests.each do |participating_test|
89
+ self.bongo!(participating_test, options)
90
+ end
91
+ else # Could be a test name or conversion name
92
+ tests_listening_to_conversion = Abongo.tests_listening_to_conversion(name)
93
+ if tests_listening_to_conversion
94
+ tests_listening_to_conversion.each do |test|
95
+ self.score_conversion!(test)
96
+ end
97
+ else # No tests listening for this conversion. Assume it is just a test name
98
+ if name.kind_of? BSON::ObjectId
99
+ self.score_conversion!(name)
100
+ else
101
+ self.score_conversion!(name.to_s)
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+
108
+ def self.score_conversion!(test_name)
109
+ if test_name.kind_of? BSON::ObjectId
110
+ participant = Abongo.find_participant(Abongo.identity)
111
+ expired = participant['expires'] ? (participant['expires'] < Time.now) : false
112
+ if options[:assume_participation] || participant['tests'].include?(test_name)
113
+ if options[:multiple_conversions] || !participant['conversions'].include?(test_name) || expired
114
+ Abongo.add_conversion(Abongo.identity, test_name)
115
+ if !options[:count_humans_only] || participant['human']
116
+ test = Abongo.experiments.find_one(:_id => test_name)
117
+ viewed_alternative = Abongo.find_alternative_for_user(Abongo.identity, test)
118
+ Abongo.alternatives.update({:content => viewed_alternative, :test => test['_id']}, {'$inc' => {:conversions => 1}})
119
+ Abongo.experiments.update({:_id => test_name}, {'$inc' => {:conversions => 1}})
120
+ end
121
+ end
122
+ end
123
+ else
124
+ Abongo.score_conversion!(Abongo.get_test(test_name)['_id'])
125
+ end
126
+ end
127
+
128
+ def self.expires_in(known_human = false)
129
+ expires_in = nil
130
+ if (@@options[:expires_in])
131
+ expires_in = @@options[:expires_in]
132
+ end
133
+ if (@@options[:count_humans_only] && @@options[:expires_in_for_bots] && !known_human)
134
+ expires_in = @@options[:expires_in_for_bots]
135
+ end
136
+ expires_in
137
+ end
138
+
139
+
140
+ def self.participating_tests(only_current = true, identity = nil)
141
+ identity ||= Abongo.identity
142
+ participating_tests = (Abongo.participants.find_one({:identity => identity}) || {} )['tests']
143
+ return {} if participating_tests.nil?
144
+ tests_and_alternatives = participating_tests.inject({}) do |acc, test_id|
145
+ test = Abongo.experiments.find_one(test_id)
146
+ if !only_current or (test['final'].nil? or !test['final'])
147
+ alternative = Abongo.find_alternative_for_user(identity, test)
148
+ acc[test['name']] = alternative
149
+ end
150
+ acc
151
+ end
152
+
153
+ tests_and_alternatives
154
+ end
155
+
156
+ def self.human!(identity = nil)
157
+ identity ||= Abongo.identity
158
+ begin
159
+ previous = Abongo.participants.find_and_modify({'query' => {:identity => identity}, 'update' => {'$set' => {:human => true}}, 'upsert' => true})
160
+ rescue Mongo::OperationFailure
161
+ Abongo.participants.update({:identity => identity}, {'$set' => {:human => true}}, :upsert => true)
162
+ previous = Abongo.participants.find_one(:identity => identity)
163
+ end
164
+
165
+ if !previous['human'] and options[:count_humans_only]
166
+ if options[:expires_in_for_bots] and previous['tests']
167
+ Abongo.set_expiration(Abongo.identity, expires_in(true))
168
+ end
169
+
170
+ if previous['tests']
171
+ previous['tests'].each do |test_id|
172
+ test = Abongo.experiments.find_one(test_id)
173
+ choice = Abongo.find_alternative_for_user(identity, test)
174
+ Abongo.alternatives.update({:content => choice, :test => test_id}, {:$inc => {:participants => 1}})
175
+ Abongo.experiments.update({:_id => test_id}, {'$inc' => {:participants => 1}})
176
+ end
177
+ end
178
+
179
+ if previous['conversions']
180
+ previous['conversions'].each do |test_id|
181
+ test = Abongo.experiments.find_one(:_id => test_id)
182
+ viewed_alternative = Abongo.find_alternative_for_user(identity, test)
183
+ Abongo.alternatives.update({:content => viewed_alternative, :test => test_id}, {'$inc' => {:conversions => 1}})
184
+ Abongo.experiments.update({:_id => test_id}, {'$inc' => {:conversions => 1}})
185
+ end
186
+ end
187
+ end
188
+ end
189
+
190
+ def self.end_experiment!(test_name, final_alternative, conversion_name = nil)
191
+ warn("conversion_name is deprecated") if conversion_name
192
+ Abongo.experiments.update({:name => test_name}, {'$set' => { :final => final_alternative}}, :upsert => true, :safe => true)
193
+ end
194
+
195
+ protected
196
+ def self.experiments; @@experiments; end
197
+ def self.conversions; @@conversions; end
198
+ def self.participants; @@participants; end
199
+ def self.alternatives; @@alternatives; end
200
+
201
+ def self.find_alternative_for_user(identity, test)
202
+ test['alternatives'][self.modulo_choice(test['name'], test['alternatives'].size)]
203
+ end
204
+
205
+ def self.modulo_choice(test_name, choices_count)
206
+ Digest::MD5.hexdigest(Abongo.salt.to_s + test_name + Abongo.identity.to_s).to_i(16) % choices_count
207
+ end
208
+
209
+ def self.parse_alternatives(alternatives)
210
+ if alternatives.kind_of? Array
211
+ return alternatives
212
+ elsif alternatives.kind_of? Integer
213
+ return (1..alternatives).to_a
214
+ elsif alternatives.kind_of? Range
215
+ return alternatives.to_a
216
+ elsif alternatives.kind_of? Hash
217
+ alternatives_array = []
218
+ alternatives.each do |key, value|
219
+ if value.kind_of? Integer
220
+ alternatives_array += [key] * value
221
+ else
222
+ raise "You gave a hash with #{key} => #{value} as an element. The value must be an integral weight."
223
+ end
224
+ end
225
+ return alternatives_array
226
+ else
227
+ raise "I don't know how to turn [#{alternatives}] into an array of alternatives."
228
+ end
229
+ end
230
+
231
+ def self.all_tests
232
+ Abongo.experiments.find.to_a
233
+ end
234
+
235
+ def self.get_test(test)
236
+ Abongo.experiments.find_one({:name => test}) || Abongo.experiments.find_one({:_id => test}) || nil
237
+ end
238
+
239
+ def self.get_alternatives(test_id)
240
+ Abongo.alternatives.find({:test => test_id})
241
+ end
242
+
243
+ def self.get_alternative(alternative_id)
244
+ Abongo.alternatives.find_one({:_id => BSON::ObjectId(alternative_id)})
245
+ end
246
+
247
+ def self.tests_listening_to_conversion(conversion)
248
+ conversions = Abongo.conversions.find_one({:name => conversion})
249
+ return nil unless conversions
250
+ conversions['tests']
251
+ end
252
+
253
+ def self.start_experiment!(test_name, alternatives_array, conversion_name = nil)
254
+ conversion_name ||= test_name
255
+
256
+ Abongo.experiments.update({:name => test_name}, {:$set => {:alternatives => alternatives_array}, :$inc => {:participants => 0, :conversions => 0}}, :upsert => true, :safe => true)
257
+ test = Abongo.experiments.find_one({:name => test_name})
258
+
259
+ # This could be a lot more elegant
260
+ cloned_alternatives_array = alternatives_array.clone
261
+ while (cloned_alternatives_array.size > 0)
262
+ alt = cloned_alternatives_array[0]
263
+ weight = cloned_alternatives_array.size - (cloned_alternatives_array - [alt]).size
264
+ Abongo.alternatives.update({:test => test['_id'], :content => alt}, {'$set' => {:weight => weight}, '$inc' => {:participants => 0, :conversions => 0}}, :upsert => true, :safe => true)
265
+ cloned_alternatives_array -= [alt]
266
+ end
267
+
268
+ Abongo.conversions.update({'name' => conversion_name}, {'$addToSet' => { 'tests' => test['_id'] }}, :upsert => true, :safe => true)
269
+
270
+ test
271
+ end
272
+
273
+ def self.find_participant(identity)
274
+ {'identity' => identity, 'tests' => [], 'conversions' => [], 'human' => false}.merge(Abongo.participants.find_one({'identity' => identity})||{})
275
+ end
276
+
277
+ def self.add_conversion(identity, test_id)
278
+ Abongo.participants.update({:identity => identity}, {'$addToSet' => {:conversions => test_id}}, :upsert => true, :safe => true)
279
+ end
280
+
281
+ def self.add_participation(identity, test_id, expires_in = nil)
282
+ if expires_in.nil?
283
+ Abongo.participants.update({:identity => identity}, {'$addToSet' => {:tests => test_id}}, :upsert => true)
284
+ else
285
+ Abongo.participants.update({:identity => identity}, {'$addToSet' => {:tests => test_id}, '$set' => {:expires => Time.now + expires_in}}, :upsert => true)
286
+ end
287
+ end
288
+
289
+ def self.set_expiration(identity, expires_in)
290
+ Abongo.participants.update({:identity => identity}, {'$set' => {:expires => Time.now + expires_in}}, :upsert => true)
291
+ end
292
+ end
@@ -0,0 +1,282 @@
1
+ class Abongo
2
+ @@VERSION = '1.0.0'
3
+ def self.VERSION; @@VERSION; end
4
+ @@MAJOR_VERSION = '1.0'
5
+ def self.MAJOR_VERSION; @@MAJOR_VERSION; end
6
+
7
+ @@options ||= {}
8
+ def self.options; @@options; end
9
+ def self.options=(options); @@options = options; end
10
+
11
+ @@salt = 'Not really necessary.'
12
+ def self.salt; @@salt; end
13
+ def self.salt=(salt); @@salt = salt; end
14
+
15
+ def self.db; @@db; end
16
+ def self.db=(db)
17
+ @@db = db
18
+ @@experiments = db['abongo_experiments']
19
+ @@conversions = db['abongo_conversions']
20
+ @@participants = db['abongo_participants']
21
+ @@alternatives = db['abongo_alternatives']
22
+ end
23
+
24
+ def self.identity=(new_identity)
25
+ @@identity = new_identity.to_s
26
+ end
27
+
28
+ def self.identity
29
+ @@identity ||= rand(10 ** 10)
30
+ end
31
+
32
+ # TODO: add options
33
+ def self.flip(test_name, options = {})
34
+ if block_given?
35
+ yield(self.test(test_name, [true, false], options))
36
+ else
37
+ self.test(test_name, [true, false], options)
38
+ end
39
+ end
40
+
41
+ def self.test(test_name, alternatives, options = {})
42
+ # Test for short-circuit (the test has been ended)
43
+ test = Abongo.get_test(test_name)
44
+ return test['final'] unless test.nil? or test['final'].nil?
45
+
46
+ # Create the test (if necessary)
47
+ unless test
48
+ conversion_name = options[:conversion] || options[:conversion_name]
49
+ test = Abongo.start_experiment!(test_name, self.parse_alternatives(alternatives), conversion_name)
50
+ end
51
+
52
+ # Should expired be part of the find_participant?
53
+ participant = Abongo.find_participant(Abongo.identity)
54
+ expired = participant['expires'] ? (participant['expires'] < Time.now) : false
55
+
56
+ choice = self.find_alternative_for_user(Abongo.identity, test)
57
+ participating_tests = participant['tests']
58
+
59
+ # TODO: Pull participation add out
60
+ if options[:multiple_participation] || !participating_tests.include?(test['_id']) || expired
61
+ unless participating_tests.include?(test['_id'])
62
+ Abongo.add_participation(identity, test['_id'], self.expires_in(participant['human']))
63
+ end
64
+
65
+ # Small timing issue in here
66
+ if (!@@options[:count_humans_only] || participant['human'])
67
+ Abongo.alternatives.update({:content => choice, :test => test['_id']}, {:$inc => {:participants => 1}})
68
+ end
69
+ end
70
+
71
+ if block_given?
72
+ yield(choice)
73
+ else
74
+ choice
75
+ end
76
+ end
77
+
78
+ def self.bongo!(name = nil, options = {})
79
+ if name.kind_of? Array
80
+ name.map do |single_test|
81
+ self.bongo!(single_test, options)
82
+ end
83
+ else
84
+ if name.nil?
85
+ # Score all participating tests
86
+ participant = Abongo.find_participant(Abongo.identity)
87
+ participating_tests = participant['tests']
88
+ participating_tests.each do |participating_test|
89
+ self.bongo!(participating_test, options)
90
+ end
91
+ else # Could be a test name or conversion name
92
+ tests_listening_to_conversion = Abongo.tests_listening_to_conversion(name)
93
+ if tests_listening_to_conversion
94
+ tests_listening_to_conversion.each do |test|
95
+ self.score_conversion!(test)
96
+ end
97
+ else # No tests listening for this conversion. Assume it is just a test name
98
+ puts name.inspect
99
+ if name.kind_of? BSON::ObjectId
100
+ self.score_conversion!(name)
101
+ else
102
+ self.score_conversion!(name.to_s)
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
108
+
109
+ def self.score_conversion!(test_name)
110
+ if test_name.kind_of? BSON::ObjectId
111
+ participant = Abongo.find_participant(Abongo.identity)
112
+ expired = participant['expires'] ? (participant['expires'] < Time.now) : false
113
+ if options[:assume_participation] || participant['tests'].include?(test_name)
114
+ if options[:multiple_conversions] || !participant['conversions'].include?(test_name) || expired
115
+ Abongo.add_conversion(Abongo.identity, test_name)
116
+ if !options[:count_humans_only] || participant['human']
117
+ test = Abongo.experiments.find_one(:_id => test_name)
118
+ viewed_alternative = Abongo.find_alternative_for_user(Abongo.identity, test)
119
+ Abongo.alternatives.update({:content => viewed_alternative, :test => test['_id']}, {'$inc' => {:conversions => 1}})
120
+ end
121
+ end
122
+ end
123
+ else
124
+ Abongo.score_conversion!(Abongo.get_test(test_name)['_id'])
125
+ end
126
+ end
127
+
128
+ def self.expires_in(known_human = false)
129
+ expires_in = nil
130
+ if (@@options[:expires_in])
131
+ expires_in = @@options[:expires_in]
132
+ end
133
+ if (@@options[:count_humans_only] && @@options[:expires_in_for_bots] && !known_human)
134
+ expires_in = @@options[:expires_in_for_bots]
135
+ end
136
+ expires_in
137
+ end
138
+
139
+
140
+ def self.participating_tests(only_current = true, identity = nil)
141
+ identity ||= Abongo.identity
142
+ participating_tests = (Abongo.participants.find_one({:identity => identity}) || {} )['tests']
143
+ return {} if participating_tests.nil?
144
+ tests_and_alternatives = participating_tests.inject({}) do |acc, test_id|
145
+ test = Abongo.experiments.find_one(test_id)
146
+ if !only_current or (test['final'].nil? or !test['final'])
147
+ alternative = Abongo.find_alternative_for_user(identity, test)
148
+ acc[test['name']] = alternative
149
+ end
150
+ acc
151
+ end
152
+
153
+ tests_and_alternatives
154
+ end
155
+
156
+ def self.human!(identity = nil)
157
+ identity ||= Abongo.identity
158
+ begin
159
+ previous = Abongo.participants.find_and_modify({'query' => {:identity => identity}, 'update' => {'$set' => {:human => true}}, 'upsert' => true})
160
+ rescue Mongo::OperationFailure
161
+ Abongo.participants.update({:identity => identity}, {'$set' => {:human => true}}, :upsert => true, :safe => true)
162
+ previous = Abongo.participants.find_one(:identity => identity)
163
+ end
164
+
165
+ unless previous['human']
166
+ if options[:expires_in_for_bots] and previous['tests']
167
+ Abongo.set_expiration(Abongo.identity, expires_in(true))
168
+ end
169
+
170
+ if previous['tests']
171
+ previous['tests'].each do |test_id|
172
+ test = Abongo.experiments.find_one(test_id)
173
+ choice = Abongo.find_alternative_for_user(identity, test)
174
+ Abongo.alternatives.update({:content => choice, :test => test['_id']}, {:$inc => {:participants => 1}})
175
+ end
176
+ end
177
+
178
+ if previous['conversions']
179
+ previous['conversions'].each do |test_id|
180
+ test = Abongo.experiments.find_one(:_id => test_id)
181
+ viewed_alternative = Abongo.find_alternative_for_user(identity, test)
182
+ Abongo.alternatives.update({:content => viewed_alternative, :test => test['_id']}, {'$inc' => {:conversions => 1}})
183
+ end
184
+ end
185
+ end
186
+ end
187
+
188
+ def self.end_experiment!(test_name, final_alternative, conversion_name = nil)
189
+ warn("conversion_name is deprecated") if conversion_name
190
+ Abongo.experiments.update({:name => test_name}, {:$set => { :final => final_alternative}}, :upsert => true, :safe => true)
191
+ end
192
+
193
+ protected
194
+ def self.experiments; @@experiments; end
195
+ def self.conversions; @@conversions; end
196
+ def self.participants; @@participants; end
197
+ def self.alternatives; @@alternatives; end
198
+
199
+ def self.find_alternative_for_user(identity, test)
200
+ test['alternatives'][self.modulo_choice(test['name'], test['alternatives'].size)]
201
+ end
202
+
203
+ def self.modulo_choice(test_name, choices_count)
204
+ Digest::MD5.hexdigest(Abongo.salt.to_s + test_name + Abongo.identity.to_s).to_i(16) % choices_count
205
+ end
206
+
207
+ def self.parse_alternatives(alternatives)
208
+ if alternatives.kind_of? Array
209
+ return alternatives
210
+ elsif alternatives.kind_of? Integer
211
+ return (1..alternatives).to_a
212
+ elsif alternatives.kind_of? Range
213
+ return alternatives.to_a
214
+ elsif alternatives.kind_of? Hash
215
+ alternatives_array = []
216
+ alternatives.each do |key, value|
217
+ if value.kind_of? Integer
218
+ alternatives_array += [key] * value
219
+ else
220
+ raise "You gave a hash with #{key} => #{value} as an element. The value must be an integral weight."
221
+ end
222
+ end
223
+ return alternatives_array
224
+ else
225
+ raise "I don't know how to turn [#{alternatives}] into an array of alternatives."
226
+ end
227
+ end
228
+
229
+ def self.all_tests
230
+ Abongo.experiments.find.to_a
231
+ end
232
+
233
+ def self.get_test(test_name)
234
+ Abongo.experiments.find_one({:name => test_name}) || nil
235
+ end
236
+
237
+ def self.tests_listening_to_conversion(conversion)
238
+ conversions = Abongo.conversions.find_one({:name => conversion})
239
+ return nil unless conversions
240
+ conversions['tests']
241
+ end
242
+
243
+ def self.start_experiment!(test_name, alternatives_array, conversion_name = nil)
244
+ conversion_name ||= test_name
245
+
246
+ Abongo.experiments.update({:name => test_name}, {:$set => { :alternatives => alternatives_array}}, :upsert => true, :safe => true)
247
+ test = Abongo.experiments.find_one({:name => test_name})
248
+
249
+ # This could be a lot more elegant
250
+ cloned_alternatives_array = alternatives_array.clone
251
+ while (cloned_alternatives_array.size > 0)
252
+ alt = cloned_alternatives_array[0]
253
+ weight = cloned_alternatives_array.size - (cloned_alternatives_array - [alt]).size
254
+ Abongo.alternatives.update({:test => test['_id'], :content => alt}, {'$set' => {:weight => weight}, '$inc' => {:participants => 0, :conversions => 0}}, :upsert => true, :safe => true)
255
+ cloned_alternatives_array -= [alt]
256
+ end
257
+
258
+ Abongo.conversions.update({'name' => conversion_name}, {'$addToSet' => { 'tests' => test['_id'] }}, :upsert => true, :safe => true)
259
+
260
+ test
261
+ end
262
+
263
+ def self.find_participant(identity)
264
+ {'identity' => identity, 'tests' => [], 'conversions' => [], 'human' => false}.merge(Abongo.participants.find_one({'identity' => identity})||{})
265
+ end
266
+
267
+ def self.add_conversion(identity, test_id)
268
+ Abongo.participants.update({:identity => identity}, {'$addToSet' => {:conversions => test_id}}, :upsert => true, :safe => true)
269
+ end
270
+
271
+ def self.add_participation(identity, test_id, expires_in = nil)
272
+ if expires_in.nil?
273
+ Abongo.participants.update({:identity => identity}, {'$addToSet' => {:tests => test_id}}, :upsert => true, :safe => true)
274
+ else
275
+ Abongo.participants.update({:identity => identity}, {'$addToSet' => {:tests => test_id}, '$set' => {:expires => Time.now + expires_in}}, :upsert => true, :safe => true)
276
+ end
277
+ end
278
+
279
+ def self.set_expiration(identity, expires_in)
280
+ Abongo.participants.update({:identity => identity}, {'$set' => {:expires => Time.now + expires_in}}, :upsert => true, :safe => true)
281
+ end
282
+ end