question_spec.rb
16.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
describe Question do
include DBSupport
it {should belong_to :creator}
it {should belong_to :site}
it {should have_many :choices}
it {should have_many :prompts}
it {should have_many :votes}
it {should have_many :densities}
it {should have_many :appearances}
it {should validate_presence_of :site}
it {should validate_presence_of :creator}
before(:each) do
@question = Factory.create(:aoi_question)
@aoi_clone = @question.site
end
it "should have 2 active choices" do
@question.choices.active.reload.size.should == 2
end
it "should create a new instance given valid attributes" do
# Factory.attributes_for does not return associations, this is a good enough substitute
Question.create!(Factory.build(:question).attributes.symbolize_keys)
end
it "should not create two default choices if none are provided" do
q = @aoi_clone.create_question("foobarbaz", {:name => 'foo'})
q.choices(true).size.should == 0
end
#it "should generate prompts after choices are added" do
#@question.prompts(true).size.should == 2
#end
it "should choose an active prompt randomly" do
prompt = @question.picked_prompt
prompt.active?.should == true
end
it "should choose an active prompt using catchup algorithm" do
prompt = @question.catchup_choose_prompt
prompt.active?.should == true
end
it "should raise runtime exception if there is no possible prompt to choose" do
@question.choices.first.deactivate!
@question.reload
lambda { @question.choose_prompt}.should raise_error(RuntimeError)
end
it "should return nil if optional parameters are empty" do
@question_optional_information = @question.get_optional_information(nil)
@question_optional_information.should be_empty
end
it "should return nil if optional parameters are nil" do
params = {"id" => '37'}
@question_optional_information = @question.get_optional_information(params)
@question_optional_information.should be_empty
end
it "should return a hash with an prompt id when optional parameters contains 'with_prompt'" do
params = {:id => 124, :with_prompt => true}
@question_optional_information = @question.get_optional_information(params)
@question_optional_information.should include(:picked_prompt_id)
@question_optional_information[:picked_prompt_id].should be_an_instance_of(Fixnum)
end
it "should return a hash with an appearance hash when optional parameters contains 'with_appearance'" do
params = {:id => 124, :with_prompt => true, :with_appearance=> true, :visitor_identifier => 'jim'}
@question_optional_information = @question.get_optional_information(params)
@question_optional_information.should include(:appearance_id)
@question_optional_information[:appearance_id].should be_an_instance_of(String)
end
it "should return a hash with two visitor stats when optional parameters contains 'with_visitor_stats'" do
params = {:id => 124, :with_visitor_stats=> true, :visitor_identifier => "jim"}
@question_optional_information = @question.get_optional_information(params)
@question_optional_information.should include(:visitor_votes)
@question_optional_information.should include(:visitor_ideas)
@question_optional_information[:visitor_votes].should be_an_instance_of(Fixnum)
@question_optional_information[:visitor_ideas].should be_an_instance_of(Fixnum)
end
it "should return a hash when optional parameters have more than one optional param " do
params = {:id => 124, :with_visitor_stats=> true, :visitor_identifier => "jim", :with_prompt => true, :with_appearance => true}
@question_optional_information = @question.get_optional_information(params)
@question_optional_information.should include(:visitor_votes)
@question_optional_information.should include(:visitor_ideas)
@question_optional_information[:visitor_votes].should be_an_instance_of(Fixnum)
@question_optional_information[:visitor_ideas].should be_an_instance_of(Fixnum)
@question_optional_information.should include(:picked_prompt_id)
@question_optional_information[:picked_prompt_id].should be_an_instance_of(Fixnum)
@question_optional_information.should include(:appearance_id)
@question_optional_information[:appearance_id].should be_an_instance_of(String)
end
it "should return the same appearance when a visitor requests two prompts without voting" do
params = {:id => 124, :with_visitor_stats=> true, :visitor_identifier => "jim", :with_prompt => true, :with_appearance => true}
@question_optional_information = @question.get_optional_information(params)
@question_optional_information[:appearance_id].should be_an_instance_of(String)
@question_optional_information[:picked_prompt_id].should be_an_instance_of(Fixnum)
saved_appearance_id = @question_optional_information[:appearance_id]
saved_prompt_id = @question_optional_information[:picked_prompt_id]
@question_optional_information = @question.get_optional_information(params)
@question_optional_information[:appearance_id].should == saved_appearance_id
@question_optional_information[:picked_prompt_id].should == saved_prompt_id
end
it "should return future prompts for a given visitor when future prompt param is passed" do
params = {:id => 124, :visitor_identifier => "jim", :with_prompt => true, :with_appearance => true, :future_prompts => {:number => 1} }
@question_optional_information = @question.get_optional_information(params)
appearance_id= @question_optional_information[:appearance_id]
future_appearance_id_1 = @question_optional_information[:future_appearance_id_1]
future_prompt_id_1 = @question_optional_information[:future_prompt_id_1]
#check that required attributes are included
appearance_id.should be_an_instance_of(String)
future_appearance_id_1.should be_an_instance_of(String)
future_prompt_id_1.should be_an_instance_of(Fixnum)
#appearances should have unique lookups
appearance_id.should_not == future_appearance_id_1
# check that all required parameters for choices are available
['left', 'right'].each do |side|
['text', 'id'].each do |param|
the_type = (param == 'text') ? String : Fixnum
@question_optional_information["future_#{side}_choice_#{param}_1".to_sym].should be_an_instance_of(the_type)
end
end
end
it "should return the same appearance for future prompts when future prompt param is passed" do
params = {:id => 124, :visitor_identifier => "jim", :with_prompt => true, :with_appearance => true, :future_prompts => {:number => 1} }
@question_optional_information = @question.get_optional_information(params)
saved_appearance_id = @question_optional_information[:appearance_id]
saved_future_appearance_id_1 = @question_optional_information[:future_appearance_id_1]
@question_optional_information = @question.get_optional_information(params)
@question_optional_information[:appearance_id].should == saved_appearance_id
@question_optional_information[:future_appearance_id_1].should == saved_future_appearance_id_1
end
it "should return the next future appearance in future prompts sequence after a vote is made" do
params = {:id => 124, :visitor_identifier => "jim", :with_prompt => true, :with_appearance => true, :future_prompts => {:number => 1} }
@question_optional_information = @question.get_optional_information(params)
appearance_id = @question_optional_information[:appearance_id]
prompt_id = @question_optional_information[:picked_prompt_id]
future_appearance_id_1 = @question_optional_information[:future_appearance_id_1]
future_prompt_id_1 = @question_optional_information[:future_prompt_id_1]
vote_options = {:visitor_identifier => "jim",
:appearance_lookup => appearance_id,
:prompt => Prompt.find(prompt_id),
:direction => "left"}
@aoi_clone.record_vote(vote_options)
@question_optional_information = @question.get_optional_information(params)
@question_optional_information[:appearance_id].should_not == appearance_id
@question_optional_information[:appearance_id].should == future_appearance_id_1
@question_optional_information[:picked_prompt_id].should == future_prompt_id_1
@question_optional_information[:future_appearance_id_1].should_not == future_appearance_id_1
end
it "should provide average voter information" do
params = {:id => 124, :visitor_identifier => "jim", :with_prompt => true, :with_appearance => true, :with_average_votes => true }
@question_optional_information = @question.get_optional_information(params)
@question_optional_information[:average_votes].should be_an_instance_of(Fixnum)
@question_optional_information[:average_votes].should be_close(0.0, 0.1)
vote_options = {:visitor_identifier => "jim",
:appearance_lookup => @question_optional_information[:appearance_id],
:prompt => Prompt.find(@question_optional_information[:picked_prompt_id]),
:direction => "left"}
@aoi_clone.record_vote(vote_options)
@question_optional_information = @question.get_optional_information(params)
@question_optional_information[:average_votes].should be_close(1.0, 0.1)
end
it "should properly handle tracking the prompt cache hit rate when returning the same appearance when a visitor requests two prompts without voting" do
params = {:id => 124, :with_visitor_stats=> true, :visitor_identifier => "jim", :with_prompt => true, :with_appearance => true}
@question.clear_prompt_queue
@question.reset_cache_tracking_keys(Date.today)
@question.get_optional_information(params)
@question.get_prompt_cache_misses(Date.today).should == "1"
@question.get_optional_information(params)
@question.get_prompt_cache_misses(Date.today).should == "1"
end
it "should auto create ideas when 'ideas' attribute is set" do
@question = Factory.build(:question)
@question.ideas = %w(one two three)
@question.save
@question.choices.count.should == 3
end
context "catchup algorithm" do
before(:all) do
@catchup_q = Factory.create(:aoi_question)
@catchup_q.it_should_autoactivate_ideas = true
@catchup_q.uses_catchup = true
@catchup_q.save!
# 2 ideas already exist, so this will make an even hundred
98.times.each do |num|
@catchup_q.site.create_choice("visitor identifier", @catchup_q, {:data => num.to_s, :local_identifier => "exmaple"})
end
@catchup_q.reload
end
it "should create a delayed job after requesting a prompt" do
proc { @catchup_q.choose_prompt}.should change(Delayed::Job, :count).by(1)
end
it "should choose an active prompt using catchup algorithm on a large number of choices" do
@catchup_q.reload
# Sanity check
@catchup_q.choices.size.should == 100
prompt = @catchup_q.catchup_choose_prompt
prompt.active?.should == true
end
it "should have a normalized vector of weights to support the catchup algorithm" do
weights = @catchup_q.catchup_prompts_weights
sum = 0
weights.each{|k,v| sum+=v}
(sum - 1.0).abs.should < 0.000001
end
it "should allow the prompt queue to be cleared" do
@catchup_q.add_prompt_to_queue
@catchup_q.clear_prompt_queue
@catchup_q.pop_prompt_queue.should == nil
end
it "should allow a prompt to be added to the prompt queue" do
@catchup_q.clear_prompt_queue
@catchup_q.pop_prompt_queue.should == nil
@catchup_q.add_prompt_to_queue
prompt = @catchup_q.pop_prompt_queue
prompt.should_not == nil
prompt.active?.should == true
end
it "should return prompts from the queue in FIFO order" do
@catchup_q.clear_prompt_queue
@catchup_q.pop_prompt_queue.should == nil
prompt1 = @catchup_q.add_prompt_to_queue
prompt2 = @catchup_q.add_prompt_to_queue
prompt3 = @catchup_q.add_prompt_to_queue
prompt_1 = @catchup_q.pop_prompt_queue
prompt_2 = @catchup_q.pop_prompt_queue
prompt_3 = @catchup_q.pop_prompt_queue
prompt_1.should == prompt1
prompt_2.should == prompt2
prompt_3.should == prompt3
# there is a small probability that the catchup algorithm
# choose two prompts that are indeed equal
prompt_1.should_not == prompt_2
prompt_1.should_not == prompt_3
prompt_2.should_not == prompt_3
@catchup_q.pop_prompt_queue.should == nil
end
it "should not return prompts from queue that are deactivated" do
@catchup_q.clear_prompt_queue
@catchup_q.pop_prompt_queue.should == nil
prompt1 = @catchup_q.add_prompt_to_queue
prompt = Prompt.find(prompt1)
prompt.left_choice.deactivate!
@catchup_q.choose_prompt.should_not == prompt1
end
after(:all) { truncate_all }
end
context "exporting data" do
before(:all) do
@question = Factory.create(:aoi_question)
user = @question.site
@question.it_should_autoactivate_ideas = true
@question.save!
visitor = user.visitors.find_or_create_by_identifier('visitor identifier')
100.times.each do |num|
user.create_choice(visitor.identifier, @question, {:data => num.to_s, :local_identifier => "example creator"})
end
200.times.each do |num|
@p = @question.picked_prompt
@a = user.record_appearance(visitor, @p)
vote_options = {:visitor_identifier => visitor.identifier,
:appearance_lookup => @a.lookup,
:prompt => @p,
:time_viewed => rand(1000),
:direction => (rand(2) == 0) ? "left" : "right"}
skip_options = {:visitor_identifier => visitor.identifier,
:appearance_lookup => @a.lookup,
:prompt => @p,
:time_viewed => rand(1000),
:skip_reason => "some reason"}
choice = rand(3)
case choice
when 0
user.record_vote(vote_options)
when 1
user.record_skip(skip_options)
when 2
#this is an orphaned appearance, so do nothing
end
end
end
it "should export vote data to a csv file" do
filename = @question.export('votes')
filename.should_not be nil
filename.should match /.*ideamarketplace_#{@question.id}_votes[.]csv$/
File.exists?(filename).should be_true
# Not specifying exact file syntax, it's likely to change frequently
#
rows = FasterCSV.read(filename)
rows.first.should include("Vote ID")
rows.first.should_not include("Idea ID")
File.delete(filename).should be_true
end
it "should notify redis after completing an export, if redis option set" do
redis_key = "test_key123"
$redis.del(redis_key) # clear if key exists already
filename = @question.export('votes', :response_type => 'redis', :redis_key => redis_key)
filename.should_not be nil
filename.should match /.*ideamarketplace_#{@question.id}_votes[.]csv$/
File.exists?(filename).should be_true
$redis.lpop(redis_key).should == filename
$redis.del(redis_key) # clean up
File.delete(filename).should be_true
end
it "should email question owner after completing an export, if email option set" do
#TODO
end
it "should export non vote data to a csv file" do
filename = @question.export('non_votes')
filename.should_not be nil
filename.should match /.*ideamarketplace_#{@question.id}_non_votes[.]csv$/
File.exists?(filename).should be_true
# Not specifying exact file syntax, it's likely to change frequently
#
rows = FasterCSV.read(filename)
rows.first.should include("Record ID")
rows.first.should include("Record Type")
rows.first.should_not include("Idea ID")
File.delete(filename).should_not be_nil
end
it "should export idea data to a csv file" do
filename = @question.export('ideas')
filename.should_not be nil
filename.should match /.*ideamarketplace_#{@question.id}_ideas[.]csv$/
File.exists?(filename).should be_true
# Not specifying exact file syntax, it's likely to change frequently
#
rows = FasterCSV.read(filename)
rows.first.should include("Idea ID")
rows.first.should_not include("Skip ID")
File.delete(filename).should_not be_nil
end
it "should raise an error when given an unsupported export type" do
lambda { @question.export("blahblahblah") }.should raise_error
end
it "should export data and schedule a job to delete export after X days" do
Delayed::Job.delete_all
filename = @question.export_and_delete('votes', :delete_at => 2.days.from_now)
Delayed::Job.count.should == 1
Delayed::Job.delete_all
File.delete(filename).should_not be_nil
end
after(:all) { truncate_all }
end
end