Commit abd621ab9f2695794acbaac06038fe581efaae06
1 parent
ff34c958
Exists in
master
and in
1 other branch
Non-votes files, tweaks to other csv files, better tests for csvs
Showing
4 changed files
with
70 additions
and
25 deletions
Show diff stats
app/models/appearance.rb
| @@ -2,5 +2,9 @@ class Appearance < ActiveRecord::Base | @@ -2,5 +2,9 @@ class Appearance < ActiveRecord::Base | ||
| 2 | belongs_to :voter, :class_name => "Visitor", :foreign_key => 'voter_id' | 2 | belongs_to :voter, :class_name => "Visitor", :foreign_key => 'voter_id' |
| 3 | belongs_to :prompt | 3 | belongs_to :prompt |
| 4 | belongs_to :question | 4 | belongs_to :question |
| 5 | + | ||
| 6 | + #technically, an appearance should either one vote or one skip, not one of both objects, but these declarations provide some useful helper methods | ||
| 7 | + # we could refactor this to use rails polymorphism, but currently the foreign key is stored in the vote and skip object | ||
| 5 | has_one :vote | 8 | has_one :vote |
| 9 | + has_one :skip | ||
| 6 | end | 10 | end |
app/models/question.rb
| @@ -350,7 +350,7 @@ class Question < ActiveRecord::Base | @@ -350,7 +350,7 @@ class Question < ActiveRecord::Base | ||
| 350 | outfile = "ideamarketplace_#{self.id}_votes.csv" | 350 | outfile = "ideamarketplace_#{self.id}_votes.csv" |
| 351 | 351 | ||
| 352 | headers = ['Vote ID', 'Session ID', 'Question ID','Winner ID', 'Winner Text', 'Loser ID', 'Loser Text', | 352 | headers = ['Vote ID', 'Session ID', 'Question ID','Winner ID', 'Winner Text', 'Loser ID', 'Loser Text', |
| 353 | - 'Prompt ID', 'Left Choice ID', 'Right Choice ID', 'Created at', 'Updated at', | 353 | + 'Prompt ID', 'Left Choice ID', 'Right Choice ID', 'Created at', 'Updated at', 'Appearance ID', |
| 354 | 'Response Time (s)', 'Session Identifier'] | 354 | 'Response Time (s)', 'Session Identifier'] |
| 355 | 355 | ||
| 356 | when 'ideas' | 356 | when 'ideas' |
| @@ -358,11 +358,11 @@ class Question < ActiveRecord::Base | @@ -358,11 +358,11 @@ class Question < ActiveRecord::Base | ||
| 358 | headers = ['Ideamarketplace ID','Idea ID', 'Idea Text', 'Wins', 'Losses', 'Times involved in Cant Decide', 'Score', | 358 | headers = ['Ideamarketplace ID','Idea ID', 'Idea Text', 'Wins', 'Losses', 'Times involved in Cant Decide', 'Score', |
| 359 | 'User Submitted', 'Session ID', 'Created at', 'Last Activity', 'Active', | 359 | 'User Submitted', 'Session ID', 'Created at', 'Last Activity', 'Active', |
| 360 | 'Appearances on Left', 'Appearances on Right'] | 360 | 'Appearances on Left', 'Appearances on Right'] |
| 361 | - when 'skips' | ||
| 362 | - outfile = "ideamarketplace_#{self.id}_skips.csv" | ||
| 363 | - headers = ['Skip ID', 'Session ID', 'Question ID','Left Choice ID', 'Left Choice Text', | 361 | + when 'non_votes' |
| 362 | + outfile = "ideamarketplace_#{self.id}_non_votes.csv" | ||
| 363 | + headers = ['Record Type', 'Record ID', 'Session ID', 'Question ID','Left Choice ID', 'Left Choice Text', | ||
| 364 | 'Right Choice ID', 'Right Choice Text', 'Prompt ID', 'Appearance ID', 'Reason', | 364 | 'Right Choice ID', 'Right Choice Text', 'Prompt ID', 'Appearance ID', 'Reason', |
| 365 | - 'Created at', 'Updated at', 'Response Time (ms)'] | 365 | + 'Created at', 'Updated at', 'Response Time (s)', 'Session Identifier'] |
| 366 | else | 366 | else |
| 367 | raise "Unsupported export type: #{type}" | 367 | raise "Unsupported export type: #{type}" |
| 368 | end | 368 | end |
| @@ -377,15 +377,15 @@ class Question < ActiveRecord::Base | @@ -377,15 +377,15 @@ class Question < ActiveRecord::Base | ||
| 377 | case type | 377 | case type |
| 378 | when 'votes' | 378 | when 'votes' |
| 379 | 379 | ||
| 380 | - self.votes.find_each(:include => [:prompt, :choice, :loser_choice]) do |v| | 380 | + self.votes.find_each(:include => [:prompt, :choice, :loser_choice, :voter]) do |v| |
| 381 | prompt = v.prompt | 381 | prompt = v.prompt |
| 382 | # these may not exist | 382 | # these may not exist |
| 383 | loser_data = v.loser_choice.nil? ? "" : "'#{v.loser_choice.data.strip}'" | 383 | loser_data = v.loser_choice.nil? ? "" : "'#{v.loser_choice.data.strip}'" |
| 384 | left_id = v.prompt.nil? ? "" : v.prompt.left_choice_id | 384 | left_id = v.prompt.nil? ? "" : v.prompt.left_choice_id |
| 385 | right_id = v.prompt.nil? ? "" : v.prompt.right_choice_id | 385 | right_id = v.prompt.nil? ? "" : v.prompt.right_choice_id |
| 386 | 386 | ||
| 387 | - csv << [ v.id, v.voter_id, v.question_id, v.choice_id, "\'#{v.choice.data.strip}'", v.loser_choice_id, loser_data, | ||
| 388 | - v.prompt_id, left_id, right_id, v.created_at, v.updated_at, | 387 | + csv << [ v.id, v.voter_id, v.question_id, v.choice_id, "'#{v.choice.data.strip}'", v.loser_choice_id, loser_data, |
| 388 | + v.prompt_id, left_id, right_id, v.created_at, v.updated_at, v.appearance_id, | ||
| 389 | v.time_viewed.to_f / 1000.0 , v.voter.identifier] | 389 | v.time_viewed.to_f / 1000.0 , v.voter.identifier] |
| 390 | end | 390 | end |
| 391 | 391 | ||
| @@ -404,14 +404,30 @@ class Question < ActiveRecord::Base | @@ -404,14 +404,30 @@ class Question < ActiveRecord::Base | ||
| 404 | user_submitted , c.item.creator_id, c.created_at, c.updated_at, c.active, | 404 | user_submitted , c.item.creator_id, c.created_at, c.updated_at, c.active, |
| 405 | left_appearances, right_appearances] | 405 | left_appearances, right_appearances] |
| 406 | end | 406 | end |
| 407 | - when 'skips' | 407 | + when 'non_votes' |
| 408 | 408 | ||
| 409 | - self.skips.find_each(:include => :prompt) do |s| | ||
| 410 | - prompt = s.prompt | ||
| 411 | - csv << [ s.id, s.skipper_id, s.question_id, s.prompt.left_choice.id, s.prompt.left_choice.data.strip, | ||
| 412 | - s.prompt.right_choice.id, s.prompt.right_choice.data.strip, s.prompt_id, s.appearance_id, s.skip_reason, | ||
| 413 | - s.created_at, s.updated_at, s.time_viewed] | ||
| 414 | - end | 409 | + self.appearances.find_each(:include => [:skip, :vote, :voter]) do |a| |
| 410 | + # we only display skips and orphaned appearances in this csv file | ||
| 411 | + unless a.vote.nil? | ||
| 412 | + next | ||
| 413 | + end | ||
| 414 | + | ||
| 415 | + #If no skip and no vote, this is an orphaned appearance | ||
| 416 | + if a.skip.nil? | ||
| 417 | + prompt = a.prompt | ||
| 418 | + csv << [ "Orphaned Appearance", a.id, a.voter_id, a.question_id, a.prompt.left_choice.id, a.prompt.left_choice.data.strip, | ||
| 419 | + a.prompt.right_choice.id, a.prompt.right_choice.data.strip, a.prompt_id, 'N/A', 'N/A', | ||
| 420 | + a.created_at, a.updated_at, 'N/A', a.voter.identifier] | ||
| 421 | + | ||
| 422 | + else | ||
| 423 | + #If this appearance belongs to a skip, show information on the skip instead | ||
| 424 | + s = a.skip | ||
| 425 | + prompt = s.prompt | ||
| 426 | + csv << [ "Skip", s.id, s.skipper_id, s.question_id, s.prompt.left_choice.id, s.prompt.left_choice.data.strip, | ||
| 427 | + s.prompt.right_choice.id, s.prompt.right_choice.data.strip, s.prompt_id, s.appearance_id, s.skip_reason, | ||
| 428 | + s.created_at, s.updated_at, s.time_viewed.to_f / 1000.0 , s.skipper.identifier] | ||
| 429 | + end | ||
| 430 | + end | ||
| 415 | end | 431 | end |
| 416 | 432 | ||
| 417 | end | 433 | end |
| @@ -421,9 +437,9 @@ class Question < ActiveRecord::Base | @@ -421,9 +437,9 @@ class Question < ActiveRecord::Base | ||
| 421 | if options[:redis_key].nil? | 437 | if options[:redis_key].nil? |
| 422 | raise "No :redis_key specified" | 438 | raise "No :redis_key specified" |
| 423 | end | 439 | end |
| 424 | - | 440 | + #The client should use blpop to listen for a key |
| 441 | + #The client is responsible for deleting the redis key (auto expiration results failure in testing) | ||
| 425 | $redis.lpush(options[:redis_key], filename) | 442 | $redis.lpush(options[:redis_key], filename) |
| 426 | - $redis.expire(options[:redis_key], 24*60*60 * 3) #Expire in three days | ||
| 427 | #TODO implement response_type == 'email' for use by customers of the API (not local) | 443 | #TODO implement response_type == 'email' for use by customers of the API (not local) |
| 428 | end | 444 | end |
| 429 | 445 |
app/models/visitor.rb
| @@ -43,6 +43,7 @@ class Visitor < ActiveRecord::Base | @@ -43,6 +43,7 @@ class Visitor < ActiveRecord::Base | ||
| 43 | 43 | ||
| 44 | skip_create_options = { :question_id => prompt.question_id, :prompt_id => prompt.id, :skipper_id=> self.id, :time_viewed => time_viewed, :appearance_id => @a.id} | 44 | skip_create_options = { :question_id => prompt.question_id, :prompt_id => prompt.id, :skipper_id=> self.id, :time_viewed => time_viewed, :appearance_id => @a.id} |
| 45 | 45 | ||
| 46 | + #the most common optional reason is 'skip_reason', probably want to refactor to make time viewed an optional parameter | ||
| 46 | prompt_skip = skips.create!(skip_create_options.merge(options)) | 47 | prompt_skip = skips.create!(skip_create_options.merge(options)) |
| 47 | 48 | ||
| 48 | end | 49 | end |
spec/models/question_spec.rb
| @@ -62,7 +62,7 @@ describe Question do | @@ -62,7 +62,7 @@ describe Question do | ||
| 62 | @catchup_q.save! | 62 | @catchup_q.save! |
| 63 | 63 | ||
| 64 | 100.times.each do |num| | 64 | 100.times.each do |num| |
| 65 | - user.create_choice("visitor identifier", @catchup_q, {:data => num, :local_identifier => "exmaple"}) | 65 | + user.create_choice("visitor identifier", @catchup_q, {:data => num.to_s, :local_identifier => "exmaple"}) |
| 66 | end | 66 | end |
| 67 | end | 67 | end |
| 68 | it "should choose an active prompt using catchup algorithm on a large number of choices" do | 68 | it "should choose an active prompt using catchup algorithm on a large number of choices" do |
| @@ -131,10 +131,32 @@ describe Question do | @@ -131,10 +131,32 @@ describe Question do | ||
| 131 | end | 131 | end |
| 132 | 132 | ||
| 133 | context "exporting data" do | 133 | context "exporting data" do |
| 134 | - before(:each) do | 134 | + before(:all) do |
| 135 | user = Factory.create(:user) | 135 | user = Factory.create(:user) |
| 136 | @question = Factory.create(:aoi_question, :site => user, :creator => user.default_visitor) | 136 | @question = Factory.create(:aoi_question, :site => user, :creator => user.default_visitor) |
| 137 | + @question.it_should_autoactivate_ideas = true | ||
| 138 | + @question.save! | ||
| 139 | + | ||
| 140 | + visitor = user.visitors.find_or_create_by_identifier('visitor identifier') | ||
| 141 | + 100.times.each do |num| | ||
| 142 | + user.create_choice(visitor.identifier, @question, {:data => num.to_s, :local_identifier => "example creator"}) | ||
| 143 | + end | ||
| 144 | + | ||
| 145 | + 200.times.each do |num| | ||
| 146 | + @p = @question.picked_prompt | ||
| 137 | 147 | ||
| 148 | + @a = user.record_appearance(visitor, @p) | ||
| 149 | + | ||
| 150 | + choice = rand(3) | ||
| 151 | + case choice | ||
| 152 | + when 0 | ||
| 153 | + user.record_vote(visitor.identifier, @a.lookup, @p, rand(2), rand(1000)) | ||
| 154 | + when 1 | ||
| 155 | + user.record_skip(visitor.identifier, @a.lookup, @p, rand(1000)) | ||
| 156 | + when 2 | ||
| 157 | + #this is an orphaned appearance, so do nothing | ||
| 158 | + end | ||
| 159 | + end | ||
| 138 | end | 160 | end |
| 139 | 161 | ||
| 140 | 162 | ||
| @@ -162,27 +184,28 @@ describe Question do | @@ -162,27 +184,28 @@ describe Question do | ||
| 162 | filename.should match /.*ideamarketplace_#{@question.id}_votes[.]csv$/ | 184 | filename.should match /.*ideamarketplace_#{@question.id}_votes[.]csv$/ |
| 163 | File.exists?(filename).should be_true | 185 | File.exists?(filename).should be_true |
| 164 | $redis.lpop(redis_key).should == filename | 186 | $redis.lpop(redis_key).should == filename |
| 165 | - | ||
| 166 | - # Not specifying exact file syntax, it's likely to change frequently | ||
| 167 | $redis.del(redis_key) # clean up | 187 | $redis.del(redis_key) # clean up |
| 188 | + File.delete(filename).should be_true | ||
| 168 | 189 | ||
| 169 | end | 190 | end |
| 170 | it "should email question owner after completing an export, if email option set" do | 191 | it "should email question owner after completing an export, if email option set" do |
| 171 | #TODO | 192 | #TODO |
| 172 | end | 193 | end |
| 173 | 194 | ||
| 174 | - it "should export skip data to a csv file" do | ||
| 175 | - filename = @question.export('skips') | 195 | + it "should export non vote data to a csv file" do |
| 196 | + filename = @question.export('non_votes') | ||
| 176 | 197 | ||
| 177 | filename.should_not be nil | 198 | filename.should_not be nil |
| 178 | - filename.should match /.*ideamarketplace_#{@question.id}_skips[.]csv$/ | 199 | + filename.should match /.*ideamarketplace_#{@question.id}_non_votes[.]csv$/ |
| 179 | File.exists?(filename).should be_true | 200 | File.exists?(filename).should be_true |
| 180 | 201 | ||
| 181 | # Not specifying exact file syntax, it's likely to change frequently | 202 | # Not specifying exact file syntax, it's likely to change frequently |
| 182 | # | 203 | # |
| 183 | rows = FasterCSV.read(filename) | 204 | rows = FasterCSV.read(filename) |
| 184 | - rows.first.should include("Skip ID") | 205 | + rows.first.should include("Record ID") |
| 206 | + rows.first.should include("Record Type") | ||
| 185 | rows.first.should_not include("Idea ID") | 207 | rows.first.should_not include("Idea ID") |
| 208 | + puts filename | ||
| 186 | File.delete(filename).should_not be_nil | 209 | File.delete(filename).should_not be_nil |
| 187 | 210 | ||
| 188 | 211 | ||
| @@ -199,6 +222,7 @@ describe Question do | @@ -199,6 +222,7 @@ describe Question do | ||
| 199 | rows = FasterCSV.read(filename) | 222 | rows = FasterCSV.read(filename) |
| 200 | rows.first.should include("Idea ID") | 223 | rows.first.should include("Idea ID") |
| 201 | rows.first.should_not include("Skip ID") | 224 | rows.first.should_not include("Skip ID") |
| 225 | + puts filename | ||
| 202 | File.delete(filename).should_not be_nil | 226 | File.delete(filename).should_not be_nil |
| 203 | 227 | ||
| 204 | end | 228 | end |