Commit 0e03ea330c6dfb37a06256e9e1debb093876e57c
1 parent
46ed7dca
Exists in
master
and in
1 other branch
Refactoring data export to run in background, notify redis on completion
Showing
6 changed files
with
221 additions
and
58 deletions
Show diff stats
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 < 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 < 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 < 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 | ... | ... |
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 | ... | ... |