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 155 end
156 156 def export
157 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 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 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 178 # export_type = params[:export_type]
167 179 # export_format = params[:export_format] #CSV always now, could expand to xml later
168 180 end
... ... @@ -326,57 +338,6 @@ class QuestionsController < InheritedResources::Base
326 338 end
327 339  
328 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 341 end
381 342  
382 343 class String
... ...
app/models/prompt.rb
... ... @@ -18,6 +18,7 @@ class Prompt &lt; ActiveRecord::Base
18 18 #named_scope :voted_on_by, proc {|u| { :conditions => { :methodology => methodology } } }
19 19  
20 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 24 def self.voted_on_by(u)
... ...
app/models/question.rb
... ... @@ -17,6 +17,7 @@ class Question &lt; ActiveRecord::Base
17 17 end
18 18 end
19 19 has_many :votes
  20 + has_many :skips
20 21 has_many :densities
21 22 has_many :appearances
22 23  
... ... @@ -334,6 +335,98 @@ class Question &lt; ActiveRecord::Base
334 335 $redis.get(self.pq_key + "_" + date.to_s + "_"+ "hits")
335 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 432 end
... ...
db/migrate/20100505235409_add_choice_indexes_to_prompts.rb 0 → 100644
... ... @@ -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 @@
  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 51 prompt = q.catchup_choose_prompt
52 52 prompt.active?.should == true
53 53 end
  54 +
54 55  
55 56 context "catchup algorithm" do
56 57 before(:all) do
... ... @@ -127,10 +128,95 @@ describe Question do
127 128  
128 129 @catchup_q.pop_prompt_queue.should == nil
129 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 220 end
133 221  
134   -
135   - #q = @aoi_clone.create_question("foobarbaz", {:name => 'foo'})
136 222 end
... ...