Commit 2e9e5c92327e227f4e1d40449fbfd0726e9ab6f5

Authored by Luke Baker
1 parent 9675033d

add atomic appearance updates on vote / skip

app/models/visitor.rb
@@ -21,18 +21,6 @@ class Visitor < ActiveRecord::Base @@ -21,18 +21,6 @@ class Visitor < ActiveRecord::Base
21 21
22 prompt = options.delete(:prompt) 22 prompt = options.delete(:prompt)
23 ordinality = (options.delete(:direction) == "left") ? 0 : 1 23 ordinality = (options.delete(:direction) == "left") ? 0 : 1
24 -  
25 - if options[:appearance_lookup]  
26 - @appearance = prompt.appearances.find_by_lookup(options.delete(:appearance_lookup))  
27 - return nil unless @appearance # don't allow people to fake appearance lookups  
28 -  
29 - if @appearance.answered?  
30 - options.merge!(:valid_record => false)  
31 - options.merge!(:validity_information => "Appearance #{@appearance.id} already answered")  
32 - else  
33 - options.merge!(:appearance => @appearance)  
34 - end  
35 - end  
36 24
37 if options.delete(:skip_fraud_protection) 25 if options.delete(:skip_fraud_protection)
38 last_answered_appearance = self.appearances.find(:first, 26 last_answered_appearance = self.appearances.find(:first,
@@ -43,6 +31,13 @@ class Visitor < ActiveRecord::Base @@ -43,6 +31,13 @@ class Visitor < ActiveRecord::Base
43 options.merge!(:validity_information => "Fraud protection: last visitor action was a skip") 31 options.merge!(:validity_information => "Fraud protection: last visitor action was a skip")
44 end 32 end
45 end 33 end
  34 +
  35 + associate_appearance = false
  36 + if options[:appearance_lookup]
  37 + @appearance = prompt.appearances.find_by_lookup(options.delete(:appearance_lookup))
  38 + return nil unless @appearance # don't allow people to fake appearance lookups
  39 + associate_appearance = true
  40 + end
46 41
47 choice = prompt.choices[ordinality] #we need to guarantee that the choices are in the right order (by position) 42 choice = prompt.choices[ordinality] #we need to guarantee that the choices are in the right order (by position)
48 other_choices = prompt.choices - [choice] 43 other_choices = prompt.choices - [choice]
@@ -51,6 +46,8 @@ class Visitor < ActiveRecord::Base @@ -51,6 +46,8 @@ class Visitor < ActiveRecord::Base
51 options.merge!(:question_id => prompt.question_id, :prompt_id => prompt.id, :voter_id=> self.id, :choice_id => choice.id, :loser_choice_id => loser_choice.id) 46 options.merge!(:question_id => prompt.question_id, :prompt_id => prompt.id, :voter_id=> self.id, :choice_id => choice.id, :loser_choice_id => loser_choice.id)
52 47
53 v = votes.create!(options) 48 v = votes.create!(options)
  49 + safely_associate_appearance(v, @appearance) if associate_appearance
  50 + v
54 end 51 end
55 52
56 def skip!(options) 53 def skip!(options)
@@ -58,18 +55,35 @@ class Visitor < ActiveRecord::Base @@ -58,18 +55,35 @@ class Visitor < ActiveRecord::Base
58 55
59 prompt = options.delete(:prompt) 56 prompt = options.delete(:prompt)
60 57
  58 + associate_appearance = false
61 if options[:appearance_lookup] 59 if options[:appearance_lookup]
62 @appearance = prompt.appearances.find_by_lookup(options.delete(:appearance_lookup)) 60 @appearance = prompt.appearances.find_by_lookup(options.delete(:appearance_lookup))
63 return nil unless @appearance 61 return nil unless @appearance
64 - if @appearance.answered?  
65 - options.merge!(:valid_record => false)  
66 - options.merge!(:validity_information => "Appearance #{@appearance.id} already answered")  
67 - else  
68 - options.merge!(:appearance => @appearance)  
69 - end 62 + associate_appearance = true
70 end 63 end
71 64
72 options.merge!(:question_id => prompt.question_id, :prompt_id => prompt.id, :skipper_id => self.id) 65 options.merge!(:question_id => prompt.question_id, :prompt_id => prompt.id, :skipper_id => self.id)
73 prompt_skip = skips.create!(options) 66 prompt_skip = skips.create!(options)
  67 + if associate_appearance
  68 + safely_associate_appearance(prompt_skip, @appearance)
  69 + end
  70 + prompt_skip
74 end 71 end
  72 +
  73 + # Safely associates appearance with object, but making sure no other object
  74 + # is already associated wit this appearance. object is either vote or skip.
  75 + def safely_associate_appearance(object, appearance)
  76 + # Manually update Appearance with id to ensure no double votes for a
  77 + # single appearance. Only update the answerable_id if it is NULL.
  78 + # If we can't find any rows to update, then this object should be invalid.
  79 + rows_updated = Appearance.update_all("answerable_id = #{object.id}, answerable_type = '#{object.class.to_s}'", "id = #{appearance.id} AND answerable_id IS NULL")
  80 + if rows_updated === 1
  81 + # update relationship the ActiveRecord way, now
  82 + # that we know it is safe to do so
  83 + object.update_attributes!(:appearance => appearance)
  84 + else
  85 + object.update_attributes!(:valid_record => false, :validity_information => "Appearance #{appearance.id} already answered")
  86 + end
  87 + end
  88 +
75 end 89 end
db/migrate/20111017171903_add_index_on_answerable_to_appearances.rb 0 → 100644
@@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
  1 +class AddIndexOnAnswerableToAppearances < ActiveRecord::Migration
  2 + def self.up
  3 + add_index :appearances, [:answerable_id, :answerable_type]
  4 + end
  5 +
  6 + def self.down
  7 + remove_index :appearances, [:answerable_id, :answerable_type]
  8 + end
  9 +end
@@ -9,7 +9,7 @@ @@ -9,7 +9,7 @@
9 # 9 #
10 # It's strongly recommended to check this file into your version control system. 10 # It's strongly recommended to check this file into your version control system.
11 11
12 -ActiveRecord::Schema.define(:version => 20110628160608) do 12 +ActiveRecord::Schema.define(:version => 20111017171903) do
13 13
14 create_table "appearances", :force => true do |t| 14 create_table "appearances", :force => true do |t|
15 t.integer "voter_id" 15 t.integer "voter_id"
@@ -25,6 +25,7 @@ ActiveRecord::Schema.define(:version =&gt; 20110628160608) do @@ -25,6 +25,7 @@ ActiveRecord::Schema.define(:version =&gt; 20110628160608) do
25 t.string "validity_information" 25 t.string "validity_information"
26 end 26 end
27 27
  28 + add_index "appearances", ["answerable_id", "answerable_type"], :name => "index_appearances_on_answerable_id_and_answerable_type"
28 add_index "appearances", ["lookup"], :name => "index_appearances_on_lookup" 29 add_index "appearances", ["lookup"], :name => "index_appearances_on_lookup"
29 add_index "appearances", ["prompt_id"], :name => "index_appearances_on_prompt_id" 30 add_index "appearances", ["prompt_id"], :name => "index_appearances_on_prompt_id"
30 add_index "appearances", ["question_id", "voter_id"], :name => "index_appearances_on_question_id_voter_id" 31 add_index "appearances", ["question_id", "voter_id"], :name => "index_appearances_on_question_id_voter_id"