diff --git a/app/models/question.rb b/app/models/question.rb index 2f776db8..9d99b51f 100644 --- a/app/models/question.rb +++ b/app/models/question.rb @@ -1,13 +1,13 @@ class Question < ActiveRecord::Base acts_as_versioned - + require 'set' include Utility extend ActiveSupport::Memoizable belongs_to :creator, :class_name => "Visitor", :foreign_key => "creator_id" belongs_to :site, :class_name => "User", :foreign_key => "site_id" - + has_many :choices, :order => 'score DESC' has_many :prompts do def pick(algorithm = nil) @@ -54,7 +54,7 @@ def create_choices_from_ideas end end end - + def item_count choices.size end @@ -70,29 +70,76 @@ def median_votes_per_session end def choose_prompt(options = {}) - - # if there is one or fewer active choices, we won't be able to find a prompt - if self.choices.size - self.inactive_choices_count <= 1 + # if there is one or fewer active choices, we won't be able to find a prompt + if self.choices.size - self.inactive_choices_count <= 1 raise RuntimeError, "More than one choice needs to be active" end - if self.uses_catchup? || options[:algorithm] == "catchup" - logger.info("Question #{self.id} is using catchup algorithm!") - next_prompt = self.pop_prompt_queue - if next_prompt.nil? - logger.info("DEBUG Catchup prompt cache miss! Nothing in prompt_queue") - next_prompt = self.simple_random_choose_prompt - record_prompt_cache_miss + if options[:algorithm] == "all-combos" && options[:visitor] + self.all_combos_prompt_selection(options[:visitor]) + elsif self.uses_catchup? || options[:algorithm] == "catchup" + logger.info("Question #{self.id} is using catchup algorithm!") + next_prompt = self.pop_prompt_queue + if next_prompt.nil? + logger.info("DEBUG Catchup prompt cache miss! Nothing in prompt_queue") + next_prompt = self.simple_random_choose_prompt + record_prompt_cache_miss + else + record_prompt_cache_hit + end + self.delay.add_prompt_to_queue + return next_prompt + else + #Standard choose prompt at random + return self.simple_random_choose_prompt + end + + end + + # This prompt selection algorithm will show a visitor all combinations + # (disregarding order of display) of choices before showing repeats. Once + # all combinations have been shown, a "round" has been completed and another + # round will begin, showing all combinations again without repeats in that + # round. + def all_combos_prompt_selection(visitor) + active_choice_ids = choices.find(:all, :select => 'id', :conditions => { :active => true }).map { |c| c.id } + choice_counts = Hash[active_choice_ids.product([0])] + visitor_prompts = visitor.prompts.find(:all, :conditions => { :question_id => self }) + + sample_least = lambda do |sorted_choice_counts| + least = nil + choice_ids = [] + sorted_choice_counts.each do |choice_id, count| + if least == nil or least == count + choice_ids << choice_id else - record_prompt_cache_hit + break end - self.delay.add_prompt_to_queue - return next_prompt - else - #Standard choose prompt at random - return self.simple_random_choose_prompt + least = count + end + choice_ids.sample + end + + visitor_prompts.each do |p| + ['left_choice_id', 'right_choice_id'].each do |choice_direction| + choice_counts[p[choice_direction]] += 1 if choice_counts.has_key?(p[choice_direction]) + end + end + least_seen_choice = sample_least.call(choice_counts.sort_by {|key, value| value}) + least_seen_pair_counts = Hash[active_choice_ids.product([0])] + least_seen_pair_counts.delete(least_seen_choice) + visitor_prompts.each do |p| + if p.left_choice_id == least_seen_choice + least_seen_pair_counts[p.right_choice_id] += 1 + elsif p.right_choice_id == least_seen_choice + least_seen_pair_counts[p.left_choice_id] += 1 + end end - + least_seen_pair = sample_least.call(least_seen_pair_counts.sort_by {|key, value| value}) + prompt = prompts.find_or_initialize_by_left_choice_id_and_right_choice_id(least_seen_choice, least_seen_pair) + prompt.save + prompt.algorithm = {:name => 'all-combos'} + prompt end #TODO: generalize for prompts of rank > 2 @@ -117,7 +164,7 @@ def catchup_choose_prompt(num=1000) num.times do prompt = nil until prompt && prompt.active? - target = rand + target = rand left_choice_id = right_choice_id = nil weighted.each do |item, weight| @@ -157,7 +204,7 @@ def catchup_prompts_weights(tau=0.05, alpha=1) sum += value end - # This will not run once all prompts have been generated, + # This will not run once all prompts have been generated, # but it prevents us from having to pregenerate all possible prompts if weights.size < active_choices.size ** 2 - active_choices.size active_choices.each do |l| @@ -186,7 +233,7 @@ def get_optional_information(params) current_user = self.site if params[:with_prompt] - + if params[:with_appearance] && visitor_identifier.present? visitor = current_user.visitors.find_or_create_by_identifier(visitor_identifier) @@ -208,7 +255,7 @@ def get_optional_information(params) ["text", "id"].each do |param| choice = (side == "left") ? @future_prompt.left_choice : @future_prompt.right_choice param_val = (param == "text") ? choice.data : choice.id - + result.merge!({"future_#{side}_choice_#{param}_#{offset}".to_sym => param_val}) end end @@ -220,25 +267,25 @@ def get_optional_information(params) result.merge!({:appearance_id => @appearance.lookup}) else # throw some error - end - - if !@prompt + end + + if !@prompt @prompt = choose_prompt(:algorithm => params[:algorithm]) end result.merge!({:picked_prompt_id => @prompt.id}) - end + end if params[:with_visitor_stats] visitor = current_user.visitors.find_or_create_by_identifier(visitor_identifier) result.merge!(:visitor_votes => Vote.find_without_default_scope(:all, :conditions => {:voter_id => visitor, :question_id => self.id}).length) result.merge!(:visitor_ideas => visitor.choices.count) end - + # this might get cpu intensive if used too often. If so, store the calculated value in redis # and expire after X minutes if params[:with_average_votes] votes_by_visitors = self.votes.count(:group => 'voter_id') - + if votes_by_visitors.size > 0 average = votes_by_visitors.inject(0){|total, (k,v)| total = total + v}.to_f / votes_by_visitors.size.to_f else @@ -260,13 +307,13 @@ def normalize!(weighted, sum=nil) end sum = sum.to_f end - weighted.each do |item, weight| - weighted[item] = weight/sum + weighted.each do |item, weight| + weighted[item] = weight/sum weighted[item] = 0.0 unless weighted[item].finite? end elsif weighted.instance_of?(Array) sum = weighted.inject(0) {|sum, item| sum += item} if sum.nil? - weighted.each_with_index do |item, i| + weighted.each_with_index do |item, i| weighted[i] = item/sum weighted[i] = 0.0 unless weighted[i].finite? end @@ -286,7 +333,7 @@ def bradley_terry_probs the_prompts = prompts_hash_by_choice_ids # Initial probabilities chosen at random - the_choices.size.times do + the_choices.size.times do probs << rand prev_probs << rand end @@ -295,7 +342,7 @@ def bradley_terry_probs probs_size = probs.size difference = 1 - + # probably want to add a fuzz here to account for floating rounding while difference > fuzz do s = t % probs_size @@ -316,7 +363,7 @@ def bradley_terry_probs denominator+= (wins_and_losses).to_f / (prev_probs[s] + prev_probs[index]) end - probs[s] = numerator / denominator + probs[s] = numerator / denominator # avoid divide by zero NaN probs[s] = 0.0 unless probs[s].finite? normalize!(probs) @@ -328,15 +375,15 @@ def bradley_terry_probs end puts difference end - + probs_hash = {} - probs.each_with_index do |item, index| + probs.each_with_index do |item, index| probs_hash[the_choices[index].id] = item end probs_hash end - + def all_bt_scores btprobs = bradley_terry_probs btprobs.each do |key, value| @@ -354,7 +401,7 @@ def prompts_hash_by_choice_ids the_prompts end - + def distinct_array_of_choice_ids(params={}) params = { :rank => 2, @@ -363,18 +410,18 @@ def distinct_array_of_choice_ids(params={}) rank = params[:rank] only_active = params[:only_active] count = (only_active) ? choices.active.count : choices.count - + found_choices = [] # select only active choices? conditions = (only_active) ? ['active = ?', true] : ['1=1'] - rank.times do + rank.times do # if we've already found some, make sure we don't find them again if found_choices.count > 0 conditions[0] += ' AND id NOT IN (?)' conditions.push found_choices end - + found_choices.push choices.find(:first, :select => 'id', :conditions => conditions, @@ -383,11 +430,11 @@ def distinct_array_of_choice_ids(params={}) end return found_choices end - + def picked_prompt_id simple_random_choose_prompt.id end - + def self.voted_on_by(u) select {|z| z.voted_on_by_user?(u)} end @@ -395,14 +442,14 @@ def self.voted_on_by(u) def voted_on_by_user?(u) u.questions_voted_on.include? self end - + def should_autoactivate_ideas? it_should_autoactivate_ideas? end - + validates_presence_of :site, :on => :create, :message => "can't be blank" validates_presence_of :creator, :on => :create, :message => "can't be blank" - + def density # slow code, only to be run by cron job once at night @@ -414,7 +461,7 @@ def density nonseed_seed_sum= 0 nonseed_seed_total= 0 - + nonseed_nonseed_sum= 0 nonseed_nonseed_total= 0 @@ -461,7 +508,7 @@ def density densities[:seed_nonseed] = seed_nonseed_sum.to_f / seed_nonseed_total.to_f densities[:nonseed_seed] = nonseed_seed_sum.to_f / nonseed_seed_total.to_f densities[:nonseed_nonseed] = nonseed_nonseed_sum.to_f / nonseed_nonseed_total.to_f - + puts "Seed_seed sum: #{seed_seed_sum}, seed_seed total num: #{seed_seed_total}" puts "Seed_nonseed sum: #{seed_nonseed_sum}, seed_nonseed total num: #{seed_nonseed_total}" puts "Nonseed_seed sum: #{nonseed_seed_sum}, nonseed_seed total num: #{nonseed_seed_total}" @@ -492,7 +539,7 @@ def clear_prompt_queue $redis.del(self.pq_key) end - + # make prompt queue less than @@precent_full def mark_prompt_queue_for_refill # 2 because redis starts indexes at 0 @@ -575,12 +622,12 @@ def to_csv(type) when 'votes' headers = ['Vote ID', 'Session ID', 'Wikisurvey ID','Winner ID', 'Winner Text', 'Loser ID', 'Loser Text', 'Prompt ID', 'Appearance ID', 'Left Choice ID', 'Right Choice ID', 'Created at', 'Updated at', 'Response Time (s)', 'Missing Response Time Explanation', 'Session Identifier', 'Valid'] - + when 'ideas' headers = ['Wikisurvey ID','Idea ID', 'Idea Text', 'Wins', 'Losses', 'Times involved in Cant Decide', 'Score', 'User Submitted', 'Session ID', 'Created at', 'Last Activity', 'Active', 'Appearances on Left', 'Appearances on Right', 'Session Identifier'] when 'non_votes' headers = ['Record Type', 'Skip ID', 'Appearance ID', 'Session ID', 'Wikisurvey ID','Left Choice ID', 'Left Choice Text', 'Right Choice ID', 'Right Choice Text', 'Prompt ID', 'Reason', 'Created at', 'Updated at', 'Response Time (s)', 'Missing Response Time Explanation', 'Session Identifier', 'Valid'] - else + else raise "Unsupported export type: #{type}" end @@ -623,9 +670,9 @@ def to_csv(type) end when 'non_votes' - + self.appearances.find_each(:include => [:voter], :conditions => ['answerable_type <> ? OR answerable_type IS NULL', 'Vote']) do |a| - + if a.answerable_type == 'Skip' # If this appearance belongs to a skip, show information on the skip instead s = a.answerable @@ -633,7 +680,7 @@ def to_csv(type) time_viewed = s.time_viewed.nil? ? "NA": s.time_viewed.to_f / 1000.0 prompt = s.prompt y.yield [ "Skip", s.id, a.id, s.skipper_id, s.question_id, s.prompt.left_choice.id, s.prompt.left_choice.data.strip, s.prompt.right_choice.id, s.prompt.right_choice.data.strip, s.prompt_id, s.skip_reason, s.created_at, s.updated_at, time_viewed , s.missing_response_time_exp, s.skipper.identifier,valid].to_csv - + else # If no skip and no vote, this is an orphaned appearance prompt = a.prompt @@ -704,7 +751,7 @@ def create_or_find_next_appearance(visitor, params, offset=0) # Only choose prompt if we don't already have one. If we had to # retry this transaction due to a deadlock, a prompt may have been # selected previously. - prompt = choose_prompt(:algorithm => params[:algorithm]) unless prompt + prompt = choose_prompt(:algorithm => params[:algorithm], :visitor => visitor) unless prompt appearance = self.site.record_appearance(visitor, prompt) end end diff --git a/app/models/visitor.rb b/app/models/visitor.rb index 4d4f244c..6e6586c9 100644 --- a/app/models/visitor.rb +++ b/app/models/visitor.rb @@ -6,7 +6,8 @@ class Visitor < ActiveRecord::Base has_many :choices, :class_name => "Choice", :foreign_key => "creator_id" has_many :clicks has_many :appearances, :foreign_key => "voter_id" - + has_many :prompts, :through => :appearances + validates_presence_of :site, :on => :create, :message => "can't be blank" # validates_uniqueness_of :identifier, :on => :create, :message => "must be unique", :scope => :site_id @@ -15,10 +16,10 @@ class Visitor < ActiveRecord::Base def owns?(question) questions.include? question end - + def vote_for!(options) return nil if !options || !options[:prompt] || !options[:direction] - + prompt = options.delete(:prompt) ordinality = (options.delete(:direction) == "left") ? 0 : 1 @@ -40,7 +41,7 @@ def vote_for!(options) old_visitor_identifier = options.delete(:old_visitor_identifier) associate_appearance = false - if options[:appearance_lookup] + if options[:appearance_lookup] @appearance = prompt.appearances.find_by_lookup(options.delete(:appearance_lookup)) return nil unless @appearance # don't allow people to fake appearance lookups @@ -51,12 +52,12 @@ def vote_for!(options) end associate_appearance = true end - + choice = prompt.choices[ordinality] #we need to guarantee that the choices are in the right order (by position) other_choices = prompt.choices - [choice] loser_choice = other_choices.first - - options.merge!(:question_id => prompt.question_id, :prompt => prompt, :voter => self, :choice => choice, :loser_choice => loser_choice) + + options.merge!(:question_id => prompt.question_id, :prompt => prompt, :voter => self, :choice => choice, :loser_choice => loser_choice) v = votes.create!(options) safely_associate_appearance(v, @appearance, old_visitor_identifier) if associate_appearance diff --git a/spec/integration/prompts_spec.rb b/spec/integration/prompts_spec.rb index 7ae68d3e..99760d3a 100644 --- a/spec/integration/prompts_spec.rb +++ b/spec/integration/prompts_spec.rb @@ -43,7 +43,7 @@ it "should correctly set the optional attributes of the skip object" do pending("shouldn\'t this set appearance_id?") do params = { - :skip => { + :skip => { :visitor_identifier => @visitor.identifier, :skip_reason => "bar", :appearance_lookup => @appearance_id, @@ -80,7 +80,63 @@ end end - + + describe "all combinations algorithm" do + before do + @visitor = Factory.create(:visitor, :site => @api_user, :identifier => "foo") + @question = Factory.create(:aoi_question, + :site => @api_user, + :choices => [], + :prompts => []) + @num_choices = 4 + @num_choices.times{ Factory.create(:choice, :question => @question).activate! } + end + + it "should show all combinations with no duplicates unil all have been seen" do + seen_choices = [] + params = { + :visitor_identifier => @visitor.identifier, + :algorithm => 'all-combos', + :with_prompt => true, + :with_appearance => true, + :with_visitor_stats => true } + get_auth question_path(@question), params + response_hash = Hash.from_xml(response.body) + prompt = Prompt.find(response_hash["question"]["picked_prompt_id"]) + appearance_id = response_hash["question"]["appearance_id"] + # Completes one full round of votes. + votes_in_round = (@num_choices * (@num_choices - 1)) / 2 + votes_in_round.times do + a = Appearance.find_by_lookup(appearance_id) + a.algorithm_name.should == "all-combos" + seen_choices << [prompt.left_choice_id, prompt.right_choice_id].sort + params = { + :vote => { + :visitor_identifier => @visitor.identifier, + :appearance_lookup => appearance_id, + :direction => "left" }, + :next_prompt => { + :visitor_identifier => @visitor.identifier, + :with_appearance => true, + :algorithm => "all-combos", + :with_visitor_stats => true } } + post_auth vote_question_prompt_path(@question, prompt.id), params + response_hash = Hash.from_xml(response.body) + prompt = Prompt.find(response_hash["prompt"]["id"]) + appearance_id = response_hash["prompt"]["appearance_id"] + end + seen_choices.length.should == seen_choices.uniq.length + choice_counts = seen_choices.flatten.inject(Hash.new(0)) { |h, e| h[e] += 1 ; h } + choice_counts.each_value do |count| + count.should == @num_choices - 1 + end + + # Should have duplicate now. + seen_choices << [prompt.left_choice_id, prompt.right_choice_id].sort + seen_choices.length.should_not == seen_choices.uniq.length + end + end + describe "POST 'vote'" do before do # dry this up @@ -100,7 +156,7 @@ it "should fail without the required 'direction' parameter" do post_auth vote_question_prompt_path(@question.id, @picked_prompt_id) response.should_not be_success - end + end it "should return a new vote object given no optional parameters" do params = { :vote => { :direction => "left" } } @@ -112,7 +168,7 @@ it "should correctly set the optional attributes of the vote object" do pending("also has nil appearance id") do params = { - :vote => { + :vote => { :visitor_identifier => @visitor.identifier, :direction => "right", :appearance_lookup => @appearance_id, diff --git a/spec/models/question_spec.rb b/spec/models/question_spec.rb index d4ce7971..efe9f536 100644 --- a/spec/models/question_spec.rb +++ b/spec/models/question_spec.rb @@ -12,12 +12,12 @@ 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 @@ -42,12 +42,12 @@ # 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 @@ -67,10 +67,10 @@ c.active?.should == true end end - + end - it "should choose an active prompt using catchup algorithm" do + it "should choose an active prompt using catchup algorithm" do prompt = @question.catchup_choose_prompt(1).first prompt.active?.should == true end @@ -87,7 +87,7 @@ end - it "should return nil if optional parameters are empty" do + 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 @@ -98,42 +98,42 @@ @question_optional_information.should be_empty end - it "should return a hash with an prompt id when optional parameters contains 'with_prompt'" do + 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.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.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.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.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.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.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) @@ -146,15 +146,15 @@ @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 + + #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) @@ -171,18 +171,18 @@ 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) @@ -190,27 +190,27 @@ 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]), @@ -220,7 +220,7 @@ @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 @@ -230,7 +230,7 @@ @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) @@ -417,7 +417,21 @@ @q.vote_rate.should == 0.4 end end - context "catchup algorithm" do + context "all combinations algorithm" do + before(:all) do + @all_combos_q = Factory.create(:aoi_question) + user = @all_combos_q.site + @voter = user.visitors.find_or_create_by_identifier('voter visitor identifier') + end + + it "should set the algorithm attribute on prompt after choice" do + @all_combos_q.add_prompt_to_queue + prompt = @all_combos_q.choose_prompt(:algorithm => 'all-combos', :visitor => @voter) + prompt.algorithm.should == {:name => "all-combos"} + end + + end + context "catchup algorithm" do before(:all) do @catchup_q = Factory.create(:aoi_question) @@ -450,7 +464,7 @@ end - it "should choose an active prompt using catchup algorithm on a large number of choices" do + 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 @@ -499,10 +513,10 @@ @catchup_q.clear_prompt_queue @catchup_q.pop_prompt_queue.should == nil prompt1 = @catchup_q.add_prompt_to_queue.first - + prompt = Prompt.find(prompt1) prompt.left_choice.deactivate! - @catchup_q.choose_prompt.should_not == prompt1 + @catchup_q.choose_prompt.should_not == prompt1 end after(:all) { truncate_all } end @@ -531,7 +545,7 @@ :prompt => @p, :time_viewed => rand(1000), :direction => (rand(2) == 0) ? "left" : "right"} - + skip_options = {:visitor_identifier => visitor.identifier, :appearance_lookup => @a.lookup, :prompt => @p, @@ -549,7 +563,7 @@ end end end - + it "should export vote data to a csv file" do csv = '' @@ -582,10 +596,10 @@ end it "should email question owner after completing an export, if email option set" do - #TODO + #TODO end - it "should export non vote data to a string" do + it "should export non vote data to a string" do csv = '' @aoi_question.to_csv('non_votes').each do |row| csv << row @@ -651,7 +665,7 @@ :time_viewed => rand(1000), :direction => (rand(2) == 0) ? "left" : "right" } - + skip_options = {:visitor_identifier => visitor.identifier, :appearance_lookup => @a.lookup, :prompt => @p,