abongo 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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