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 |