Commit 0e03ea330c6dfb37a06256e9e1debb093876e57c

Authored by Dhruv Kapadia
1 parent 46ed7dca

Refactoring data export to run in background, notify redis on completion

app/controllers/questions_controller.rb
@@ -155,14 +155,26 @@ class QuestionsController < InheritedResources::Base @@ -155,14 +155,26 @@ class QuestionsController < InheritedResources::Base
155 end 155 end
156 def export 156 def export
157 type = params[:type] 157 type = params[:type]
  158 + response_type = params[:response_type]
158 159
159 - if type == 'votes'  
160 - export_votes  
161 - elsif type == 'items'  
162 - export_items 160 + if response_type == 'redis'
  161 + redis_key = params[:redis_key]
163 else 162 else
164 - render :text => "Error! Specify a type of export" 163 + render :text => "Error! The only export type supported currently is local through redis!" and return
165 end 164 end
  165 +
  166 + if type.nil?
  167 + render :text => "Error! Specify a type of export" and return
  168 + end
  169 +
  170 + @question = current_user.questions.find(params[:id])
  171 +
  172 + @question.send_later :export_and_delete, type,
  173 + :response_type => response_type, :redis_key => redis_key, :delete_at => 3.days.from_now
  174 +
  175 +
  176 + render :text => "Ok! Please wait for the response (as specified by your response_type)"
  177 +
166 # export_type = params[:export_type] 178 # export_type = params[:export_type]
167 # export_format = params[:export_format] #CSV always now, could expand to xml later 179 # export_format = params[:export_format] #CSV always now, could expand to xml later
168 end 180 end
@@ -326,57 +338,6 @@ class QuestionsController < InheritedResources::Base @@ -326,57 +338,6 @@ class QuestionsController < InheritedResources::Base
326 end 338 end
327 339
328 protected 340 protected
329 - def export_votes  
330 - @question = Question.find(params[:id])  
331 -  
332 - outfile = "ideamarketplace_#{@question.id}_votes" + Time.now.strftime("%m-%d-%Y") + ".csv"  
333 - headers = ['Vote ID', 'Session ID', 'Question ID','Winner ID', 'Winner Text', 'Loser ID', 'Loser Text',  
334 - 'Prompt ID', 'Left Choice ID', 'Right Choice ID', 'Created at', 'Updated at']  
335 - @votes = @question.votes  
336 - csv_data = FasterCSV.generate do |csv|  
337 - csv << headers  
338 - @votes.each do |v|  
339 - prompt = v.prompt  
340 - # these may not exist  
341 - loser_data = v.loser_choice.nil? ? "" : "'#{v.loser_choice.data.strip}'"  
342 - left_id = v.prompt.nil? ? "" : v.prompt.left_choice_id  
343 - right_id = v.prompt.nil? ? "" : v.prompt.right_choice_id  
344 -  
345 - csv << [ v.id, v.voter_id, v.question_id, v.choice_id, "\'#{v.choice.data.strip}'", v.loser_choice_id, loser_data,  
346 - v.prompt_id, left_id, right_id, v.created_at, v.updated_at]  
347 - end  
348 - end  
349 -  
350 - send_data(csv_data,  
351 - :type => 'text/csv; charset=iso-8859-1; header=present',  
352 - :disposition => "attachment; filename=#{outfile}")  
353 - end  
354 -  
355 - def export_items  
356 - @question = Question.find(params[:id], :include => [:choices, :prompts])  
357 -  
358 - outfile = "ideamarketplace_#{@question.id}_ideas_" + Time.now.strftime("%m-%d-%Y") + ".csv"  
359 - headers = ['Ideamarketplace ID','Idea ID', 'Idea Text', 'Wins', 'Losses', 'Score','User Submitted', 'Idea Creator ID',  
360 - 'Created at', 'Last Activity', 'Active', 'Local Identifier',  
361 - 'Prompts on Left', 'Prompts on Right', 'Prompts Count']  
362 -  
363 - csv_data = FasterCSV.generate do |csv|  
364 - csv << headers  
365 -  
366 - #ensure capital format for true and false  
367 - @question.choices.each do |c|  
368 - user_submitted = (c.item.creator != @question.creator) ? "TRUE" : "FALSE"  
369 -  
370 - csv << [c.question_id, c.id, "'#{c.data.strip}'", c.wins, c.losses, c.score, user_submitted , c.item.creator_id,  
371 - c.created_at, c.updated_at, c.active, c.local_identifier,  
372 - c.prompts_on_the_left(true).size, c.prompts_on_the_right(true).size, c.prompts_count]  
373 - end  
374 - end  
375 -  
376 - send_data(csv_data,  
377 - :type => 'text/csv; charset=iso-8859-1; header=present',  
378 - :disposition => "attachment; filename=#{outfile}")  
379 - end  
380 end 341 end
381 342
382 class String 343 class String
app/models/prompt.rb
@@ -18,6 +18,7 @@ class Prompt &lt; ActiveRecord::Base @@ -18,6 +18,7 @@ class Prompt &lt; ActiveRecord::Base
18 #named_scope :voted_on_by, proc {|u| { :conditions => { :methodology => methodology } } } 18 #named_scope :voted_on_by, proc {|u| { :conditions => { :methodology => methodology } } }
19 19
20 named_scope :active, :include => [:left_choice, :right_choice], :conditions => { 'left_choice.active' => true, 'right_choice.active' => true } 20 named_scope :active, :include => [:left_choice, :right_choice], :conditions => { 'left_choice.active' => true, 'right_choice.active' => true }
  21 + named_scope :ids_only, :select => 'id'
21 22
22 23
23 def self.voted_on_by(u) 24 def self.voted_on_by(u)
app/models/question.rb
@@ -17,6 +17,7 @@ class Question &lt; ActiveRecord::Base @@ -17,6 +17,7 @@ class Question &lt; ActiveRecord::Base
17 end 17 end
18 end 18 end
19 has_many :votes 19 has_many :votes
  20 + has_many :skips
20 has_many :densities 21 has_many :densities
21 has_many :appearances 22 has_many :appearances
22 23
@@ -334,6 +335,98 @@ class Question &lt; ActiveRecord::Base @@ -334,6 +335,98 @@ class Question &lt; ActiveRecord::Base
334 $redis.get(self.pq_key + "_" + date.to_s + "_"+ "hits") 335 $redis.get(self.pq_key + "_" + date.to_s + "_"+ "hits")
335 end 336 end
336 337
  338 + def export_and_delete(type, options={})
  339 + delete_at = options.delete(:delete_at)
  340 + filename = export(type, options)
  341 +
  342 + File.send_at(delete_at, :delete, filename)
  343 + filename
  344 + end
  345 +
  346 + def export(type, options = {})
  347 +
  348 + case type
  349 + when 'votes'
  350 + outfile = "ideamarketplace_#{self.id}_votes.csv"
  351 +
  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', 'Response Time (ms)']
  354 +
  355 + when 'ideas'
  356 + outfile = "ideamarketplace_#{self.id}_ideas.csv"
  357 + headers = ['Ideamarketplace ID','Idea ID', 'Idea Text', 'Wins', 'Losses', 'Times involved in Cant Decide', 'Score',
  358 + 'User Submitted', 'Idea Creator ID', 'Created at', 'Last Activity', 'Active', 'Local Identifier',
  359 + 'Appearances on Left', 'Appearances on Right']
  360 + when 'skips'
  361 + outfile = "ideamarketplace_#{self.id}_skips.csv"
  362 + headers = ['Skip ID', 'Session ID', 'Question ID','Left Choice ID', 'Left Choice Text',
  363 + 'Right Choice ID', 'Right Choice Text', 'Prompt ID', 'Appearance ID', 'Reason',
  364 + 'Created at', 'Updated at', 'Response Time (ms)']
  365 + else
  366 + raise "Unsupported export type: #{type}"
  367 + end
  368 +
  369 + filename = File.join(File.expand_path(Rails.root), "public", "system", "exports",
  370 + self.id.to_s, Digest::SHA1.hexdigest(outfile + rand(10000000).to_s) + "_" + outfile)
  371 +
  372 + FileUtils::mkdir_p(File.dirname(filename))
  373 + csv_data = FasterCSV.open(filename, "w") do |csv|
  374 + csv << headers
  375 +
  376 + case type
  377 + when 'votes'
  378 +
  379 + self.votes.find_each(:include => [:prompt, :choice, :loser_choice]) do |v|
  380 + prompt = v.prompt
  381 + # these may not exist
  382 + loser_data = v.loser_choice.nil? ? "" : "'#{v.loser_choice.data.strip}'"
  383 + left_id = v.prompt.nil? ? "" : v.prompt.left_choice_id
  384 + right_id = v.prompt.nil? ? "" : v.prompt.right_choice_id
  385 +
  386 + csv << [ v.id, v.voter_id, v.question_id, v.choice_id, "\'#{v.choice.data.strip}'", v.loser_choice_id, loser_data,
  387 + v.prompt_id, left_id, right_id, v.created_at, v.updated_at, v.time_viewed]
  388 + end
  389 +
  390 + when 'ideas'
  391 + self.choices.each do |c|
  392 + user_submitted = c.user_created ? "TRUE" : "FALSE"
  393 + left_prompts_ids = c.prompts_on_the_left.ids_only
  394 + right_prompts_ids = c.prompts_on_the_right.ids_only
  395 +
  396 + left_appearances = self.appearances.count(:conditions => {:prompt_id => left_prompts_ids})
  397 + right_appearances = self.appearances.count(:conditions => {:prompt_id => right_prompts_ids})
  398 +
  399 + num_skips = self.skips.count(:conditions => {:prompt_id => left_prompts_ids + right_prompts_ids})
  400 +
  401 + csv << [c.question_id, c.id, "'#{c.data.strip}'", c.wins, c.losses, num_skips, c.score,
  402 + user_submitted , c.item.creator_id, c.created_at, c.updated_at, c.active, c.local_identifier,
  403 + left_appearances, right_appearances]
  404 + end
  405 + when 'skips'
  406 +
  407 + self.skips.find_each(:include => :prompt) do |s|
  408 + prompt = s.prompt
  409 + csv << [ s.id, s.skipper_id, s.question_id, s.prompt.left_choice.id, s.prompt.left_choice.data.strip,
  410 + s.prompt.right_choice.id, s.prompt.right_choice.data.strip, s.prompt_id, s.appearance_id, s.skip_reason,
  411 + s.created_at, s.updated_at, s.time_viewed]
  412 + end
  413 + end
  414 +
  415 + end
  416 +
  417 + if options[:response_type] == 'redis'
  418 +
  419 + if options[:redis_key].nil?
  420 + raise "No :redis_key specified"
  421 + end
  422 +
  423 + $redis.lpush(options[:redis_key], filename)
  424 + #TODO implement response_type == 'email' for use by customers of the API (not local)
  425 + end
  426 +
  427 + filename
  428 + end
  429 +
337 430
338 431
339 end 432 end
db/migrate/20100505235409_add_choice_indexes_to_prompts.rb 0 → 100644
@@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
  1 +class AddChoiceIndexesToPrompts < ActiveRecord::Migration
  2 + def self.up
  3 + add_index :prompts, :left_choice_id
  4 + add_index :prompts, :right_choice_id
  5 + end
  6 +
  7 + def self.down
  8 + remove_index :prompts, :left_choice_id
  9 + remove_index :prompts, :right_choice_id
  10 + end
  11 +end
db/migrate/20100506003102_add_indexes_to_skips.rb 0 → 100644
@@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
  1 +class AddIndexesToSkips < ActiveRecord::Migration
  2 + def self.up
  3 + add_index :skips, :question_id
  4 + add_index :skips, :prompt_id
  5 + end
  6 +
  7 + def self.down
  8 + remove_index :skips, :question_id
  9 + remove_index :skips, :prompt_id
  10 + end
  11 +end
spec/models/question_spec.rb
@@ -51,6 +51,7 @@ describe Question do @@ -51,6 +51,7 @@ describe Question do
51 prompt = q.catchup_choose_prompt 51 prompt = q.catchup_choose_prompt
52 prompt.active?.should == true 52 prompt.active?.should == true
53 end 53 end
  54 +
54 55
55 context "catchup algorithm" do 56 context "catchup algorithm" do
56 before(:all) do 57 before(:all) do
@@ -127,10 +128,95 @@ describe Question do @@ -127,10 +128,95 @@ describe Question do
127 128
128 @catchup_q.pop_prompt_queue.should == nil 129 @catchup_q.pop_prompt_queue.should == nil
129 end 130 end
  131 + end
  132 +
  133 + context "exporting data" do
  134 + before(:each) do
  135 + user = Factory.create(:user)
  136 + @question = Factory.create(:aoi_question, :site => user, :creator => user.default_visitor)
  137 +
  138 + end
  139 +
  140 +
  141 + it "should export vote data to a csv file" do
  142 + filename = @question.export('votes')
  143 +
  144 + filename.should_not be nil
  145 + filename.should match /.*ideamarketplace_#{@question.id}_votes[.]csv$/
  146 + File.exists?(filename).should be_true
  147 + # Not specifying exact file syntax, it's likely to change frequently
  148 + #
  149 + rows = FasterCSV.read(filename)
  150 + rows.first.should include("Vote ID")
  151 + rows.first.should_not include("Idea ID")
  152 + File.delete(filename).should be_true
  153 +
  154 + end
  155 +
  156 + it "should notify redis after completing an export, if redis option set" do
  157 + redis_key = "test_key123"
  158 + $redis.del(redis_key) # clear if key exists already
  159 + filename = @question.export('votes', :response_type => 'redis', :redis_key => redis_key)
  160 +
  161 + filename.should_not be nil
  162 + filename.should match /.*ideamarketplace_#{@question.id}_votes[.]csv$/
  163 + File.exists?(filename).should be_true
  164 + $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
  168 +
  169 + end
  170 + it "should email question owner after completing an export, if email option set" do
  171 + #TODO
  172 + end
130 173
  174 + it "should export skip data to a csv file" do
  175 + filename = @question.export('skips')
  176 +
  177 + filename.should_not be nil
  178 + filename.should match /.*ideamarketplace_#{@question.id}_skips[.]csv$/
  179 + File.exists?(filename).should be_true
  180 +
  181 + # Not specifying exact file syntax, it's likely to change frequently
  182 + #
  183 + rows = FasterCSV.read(filename)
  184 + rows.first.should include("Skip ID")
  185 + rows.first.should_not include("Idea ID")
  186 + File.delete(filename).should_not be_nil
  187 +
  188 +
  189 + end
  190 +
  191 + it "should export idea data to a csv file" do
  192 + filename = @question.export('ideas')
  193 +
  194 + filename.should_not be nil
  195 + filename.should match /.*ideamarketplace_#{@question.id}_ideas[.]csv$/
  196 + File.exists?(filename).should be_true
  197 + # Not specifying exact file syntax, it's likely to change frequently
  198 + #
  199 + rows = FasterCSV.read(filename)
  200 + rows.first.should include("Idea ID")
  201 + rows.first.should_not include("Skip ID")
  202 + File.delete(filename).should_not be_nil
  203 +
  204 + end
  205 +
  206 + it "should raise an error when given an unsupported export type" do
  207 + lambda { @question.export("blahblahblah") }.should raise_error
  208 + end
  209 +
  210 + it "should export data and schedule a job to delete export after X days" do
  211 + Delayed::Job.delete_all
  212 + filename = @question.export_and_delete('votes', :delete_at => 2.days.from_now)
  213 +
  214 + Delayed::Job.count.should == 1
  215 + Delayed::Job.delete_all
  216 + File.delete(filename).should_not be_nil
  217 +
  218 + end
131 219
132 end 220 end
133 221
134 -  
135 - #q = @aoi_clone.create_question("foobarbaz", {:name => 'foo'})  
136 end 222 end