twitter/activerecord-reputation-system

View on GitHub
lib/reputation_system/models/reputation.rb

Summary

Maintainability
B
6 hrs
Test Coverage
##
#  Copyright 2012 Twitter, Inc
#
#  Licensed under the Apache License, Version 2.0 (the "License");
#  you may not use this file except in compliance with the License.
#  You may obtain a copy of the License at
#
#  http://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS,
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#  See the License for the specific language governing permissions and
#  limitations under the License.
##

module ReputationSystem
  class Reputation < ActiveRecord::Base
    self.table_name = 'rs_reputations'

    belongs_to :target, :polymorphic => true
    has_many :received_messages, :class_name => 'ReputationSystem::ReputationMessage', :foreign_key => :receiver_id, :dependent => :destroy do
      def from(sender)
        self.find_by_sender_id_and_sender_type(sender.id, sender.class.to_s)
      end
    end
    has_many :sent_messages, :as => :sender, :class_name => 'ReputationSystem::ReputationMessage', :dependent => :destroy

    before_validation :set_target_type_for_sti
    before_save :change_zero_value_in_case_of_product_process

    validates_uniqueness_of :reputation_name, :scope => [:target_id, :target_type]

    serialize :data, Hash

    def self.find_by_reputation_name_and_target(reputation_name, target)
      target_type = get_target_type_for_sti(target, reputation_name)
      ReputationSystem::Reputation.find_by_reputation_name_and_target_id_and_target_type(reputation_name.to_s, target.id, target_type)
    end

    # All external access to reputation should use this since they are created lazily.
    def self.find_or_create_reputation(reputation_name, target, process)
      rep = find_by_reputation_name_and_target(reputation_name, target)
      rep ? rep : create_reputation(reputation_name, target, process)
    end

    def self.create_reputation(reputation_name, target, process)
      create_options = {:reputation_name => reputation_name.to_s, :target_id => target.id,
                        :target_type => target.class.name, :aggregated_by => process.to_s}
      rep = create(create_options)
      initialize_reputation_value(rep, target, process)
    end

    def self.update_reputation_value_with_new_source(rep, source, weight, process)
      weight ||= 1 # weight is 1 by default.
      size = rep.received_messages.size
      valueBeforeUpdate = size > 0 ? rep.value : nil
      newValue = source.value
      case process.to_sym
      when :sum
        rep.value += (newValue * weight)
      when :average
        rep.value = (rep.value * size + newValue * weight) / (size + 1)
      when :product
        rep.value *= (newValue * weight)
      else
        if source.target.respond_to?(process)
          rep.value = source.target.send(process, rep, source, weight)
        else
          raise ArgumentError, "#{process} process is not supported yet"
        end
      end
      save_succeeded = rep.save
      ReputationSystem::ReputationMessage.add_reputation_message_if_not_exist(source, rep)
      propagate_updated_reputation_value(rep, valueBeforeUpdate) if rep.target
      save_succeeded
    end

    def self.update_reputation_value_with_updated_source(rep, source, oldValue, newSize, weight, process)
      weight ||= 1 # weight is 1 by default.
      oldSize = rep.received_messages.size
      valueBeforeUpdate = oldSize > 0 ? rep.value : nil
      newValue = source.value
      if newSize == 0
        rep.value = process.to_sym == :product ? 1 : 0
      else
        case process.to_sym
        when :sum
          rep.value += (newValue - oldValue) * weight
        when :average
          rep.value = (rep.value * oldSize + (newValue - oldValue) * weight) / newSize
        when :product
          rep.value = (rep.value * newValue) / oldValue
        else
          if source.target.respond_to?(process)
            rep.value = source.target.send(process, rep, source, weight, oldValue, newSize)
          else
            raise ArgumentError, "#{process} process is not supported yet"
          end
        end
      end
      save_succeeded = rep.save
      propagate_updated_reputation_value(rep, valueBeforeUpdate) if rep.target
      save_succeeded
    end

    def normalized_value
      if self.active == 1 || self.active == true
        max = ReputationSystem::Reputation.max(self.reputation_name, self.target_type)
        min = ReputationSystem::Reputation.min(self.reputation_name, self.target_type)
        if max && min
          range = max - min
          range == 0 ? 0 : (self.value - min) / range
        else
          0
        end
      else
        0
      end
    end

    protected

      # Updates reputation value for new reputation if its source already exist.
      def self.initialize_reputation_value(receiver, target, process)
        name = receiver.reputation_name
        unless ReputationSystem::Network.is_primary_reputation?(target.class.name, name)
          sender_defs = ReputationSystem::Network.get_reputation_def(target.class.name, name)[:source]
          sender_defs.each do |sd|
            sender_targets = target.get_attributes_of(sd)
            sender_targets.each do |st|
              update_reputation_if_source_exist(sd, st, receiver, process) if receiver.target
            end
          end
        end
        receiver
      end

      # Propagates updated reputation value to the reputations whose source is the updated reputation.
      def self.propagate_updated_reputation_value(sender, oldValue)
        receiver_defs = ReputationSystem::Network.get_reputation_def(sender.target.class.name, sender.reputation_name)[:source_of]
        receiver_defs.each do |rd|
          targets = sender.target.get_attributes_of(rd)
          targets.each do |target|
            scope = sender.target.evaluate_reputation_scope(rd[:scope])
            send_reputation_message_to_receiver(rd[:reputation], sender, target, scope, oldValue)
          end
        end if receiver_defs
      end

      def self.send_reputation_message_to_receiver(reputation_name, sender, target, scope, oldValue)
        srn = ReputationSystem::Network.get_scoped_reputation_name(target.class.name, reputation_name, scope)
        process = ReputationSystem::Network.get_reputation_def(target.class.name, srn)[:aggregated_by]
        receiver = find_by_reputation_name_and_target(srn, target)
        if receiver
          weight = ReputationSystem::Network.get_weight_of_source_from_reputation_name_of_target(target, sender.reputation_name, srn)
          update_reputation_value(receiver, sender, weight, process, oldValue)
        # If r is new then value update will be done when it is initialized.
        else
          create_reputation(srn, target, process)
        end
      end

      def self.update_reputation_value(receiver, sender, weight, process, oldValue)
        unless oldValue
          update_reputation_value_with_new_source(receiver, sender, weight, process)
        else
          newSize = receiver.received_messages.size
          update_reputation_value_with_updated_source(receiver, sender, oldValue, newSize, weight, process)
        end
      end

      def self.update_reputation_if_source_exist(sd, st, receiver, process)
        scope = receiver.target.evaluate_reputation_scope(sd[:scope])
        srn = ReputationSystem::Network.get_scoped_reputation_name(st.class.name, sd[:reputation], scope)
        source = find_by_reputation_name_and_target(srn, st)
        if source
          update_reputation_value_with_new_source(receiver, source, sd[:weight], process)
          ReputationSystem::ReputationMessage.add_reputation_message_if_not_exist(source, receiver)
        end
      end

      def self.max(reputation_name, target_type)
        ReputationSystem::Reputation.where(:reputation_name => reputation_name.to_s, :target_type => target_type, :active => true).maximum(:value)
      end

      def self.min(reputation_name, target_type)
        ReputationSystem::Reputation.where(:reputation_name => reputation_name.to_s, :target_type => target_type, :active => true).minimum(:value)
      end

      def self.get_target_type_for_sti(target, reputation_name)
        target_class = target.class
        defs = ReputationSystem::Network.get_reputation_defs(target_class.name)[reputation_name.to_sym]
        while target_class && target_class.name != "ActiveRecord::Base" && defs && defs.empty?
          target_class = target_class.superclass
          defs = ReputationSystem::Network.get_reputation_defs(target_class.name)[reputation_name.to_sym]
        end
        target_class ? target_class.name : nil
      end

      def set_target_type_for_sti
        sti_target_type = self.class.get_target_type_for_sti(target, reputation_name)
        self.target_type = sti_target_type if sti_target_type
      end

      def change_zero_value_in_case_of_product_process
        self.value = 1 if self.value == 0 && self.aggregated_by == "product"
      end

      def remove_associated_messages
        ReputationSystem::ReputationMessage.delete_all(:sender_type => self.class.name, :sender_id => self.id)
        ReputationSystem::ReputationMessage.delete_all(:receiver_id => self.id)
      end
  end
end