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 | ... | ... |