visitor.rb 3.99 KB
class Visitor < ActiveRecord::Base
  belongs_to :site, :class_name => "User", :foreign_key => "site_id"
  has_many :questions, :class_name => "Question", :foreign_key => "creator_id"
  has_many :votes, :class_name => "Vote", :foreign_key => "voter_id"
  has_many :skips, :class_name => "Skip", :foreign_key => "skipper_id"
  has_many :choices, :class_name => "Choice", :foreign_key => "creator_id"
  has_many :clicks
  has_many :appearances, :foreign_key => "voter_id"
  
  validates_presence_of :site, :on => :create, :message => "can't be blank"
# validates_uniqueness_of :identifier, :on => :create, :message => "must be unique", :scope => :site_id

 named_scope :with_tracking, lambda { |*args| {:include => :votes, :conditions => { :identifier => args.first } }}

  def owns?(question)
    questions.include? question
  end
  
  def vote_for!(options)
    return nil if !options || !options[:prompt] || !options[:direction]
    
    prompt = options.delete(:prompt)
    ordinality = (options.delete(:direction) == "left") ? 0 : 1

    if options.delete(:skip_fraud_protection)
       last_answered_appearance = self.appearances.find(:first,
			:conditions => ["appearances. question_id = ? AND appearances.answerable_id IS NOT NULL", prompt.question_id],
			:order => 'id DESC')
       if last_answered_appearance && last_answered_appearance.answerable_type == "Skip"
              options.merge!(:valid_record => false)
              options.merge!(:validity_information => "Fraud protection: last visitor action was a skip")
       end
    end

    if options.delete(:force_invalid_vote)
      options.merge!(:valid_record => false)
      options.merge!(:validity_information => "API call forced invalid vote")
    end

    associate_appearance = false
    if options[:appearance_lookup] 
       @appearance = prompt.appearances.find_by_lookup(options.delete(:appearance_lookup))
       return nil unless @appearance # don't allow people to fake appearance lookups
      associate_appearance = true
    end
    
    choice = prompt.choices[ordinality] #we need to guarantee that the choices are in the right order (by position)
    other_choices = prompt.choices - [choice]
    loser_choice = other_choices.first
    
    options.merge!(:question_id => prompt.question_id, :prompt => prompt, :voter => self, :choice => choice, :loser_choice => loser_choice) 

    v = votes.create!(options)
    safely_associate_appearance(v, @appearance) if associate_appearance
    v
  end

  def skip!(options)
    return nil if !options || !options[:prompt]

    prompt = options.delete(:prompt)

    associate_appearance = false
    if options[:appearance_lookup]
      @appearance = prompt.appearances.find_by_lookup(options.delete(:appearance_lookup))
      return nil unless @appearance
      associate_appearance = true
    end

    if options.delete(:force_invalid_vote)
      options.merge!(:valid_record => false)
      options.merge!(:validity_information => "API call forced invalid vote")
    end

    options.merge!(:question_id => prompt.question_id, :prompt_id => prompt.id, :skipper_id => self.id)
    prompt_skip = skips.create!(options)
    if associate_appearance
      safely_associate_appearance(prompt_skip, @appearance)
    end
    prompt_skip
  end

  # Safely associates appearance with object, but making sure no other object
  # is already associated wit this appearance. object is either vote or skip.
  def safely_associate_appearance(object, appearance)
    # Manually update Appearance with id to ensure no double votes for a
    # single appearance.  Only update the answerable_id if it is NULL.
    # If we can't find any rows to update, then this object should be invalid.
    rows_updated = Appearance.update_all("answerable_id = #{object.id}, answerable_type = '#{object.class.to_s}', updated_at = '#{Time.now.utc.to_s(:db)}'", "id = #{appearance.id} AND answerable_id IS NULL")
    if rows_updated != 1
      object.update_attributes!(:valid_record => false, :validity_information => "Appearance #{appearance.id} already answered")
    end
  end

end