module Recommendable::Ratable::ClassMethods

Public Class Methods

ratable_class() click to toggle source

Returns the class that has been explicitly been made ratable, whether it is this class or a superclass. This allows a ratable class and all of its subclasses to be considered the same type of ratable and give recommendations from the base class or any of the subclasses.

# File lib/recommendable/ratable.rb, line 66
def self.ratable_class
  ancestors.find { |klass| Recommendable.config.ratable_classes.include?(klass) }
end
recommendable?() click to toggle source

Whether or not items belonging to this class can be recommended.

@return true if a user class `recommends :this`

# File lib/recommendable/ratable.rb, line 35
def self.recommendable?() true end
top(options = {}) click to toggle source

Query for the top-N items sorted by score

@param [Hash] options a hash of options to modify which items are returned @option options [Integer] :count the number of items to fetch (defaults to 1) @option options [Integer] :offset an offset to allow paging through results @return [Array] the top items belonging to this class, sorted by score

# File lib/recommendable/ratable.rb, line 50
def self.top(options = {})
  if options.is_a?(Integer)
    options = { :count => options }
    warn "[DEPRECATION] Recommenable::Ratable.top now takes an options hash. Please call `.top(count: #{options[:count]})` instead of just `.top(#{options[:count]})`"
  end
  options.reverse_merge!(:count => 1, :offset => 0)
  score_set = Recommendable::Helpers::RedisKeyMapper.score_set_for(self)
  ids = Recommendable.redis.zrevrange(score_set, options[:offset], options[:offset] + options[:count] - 1)

  Recommendable.query(self, ids).sort_by { |item| ids.index(item.id.to_s) }
end

Public Instance Methods

before_destroy() click to toggle source
Calls superclass method
# File lib/recommendable/ratable.rb, line 20
def before_destroy() super and remove_from_recommendable! end
make_recommendable!() click to toggle source
# File lib/recommendable/ratable.rb, line 11
def make_recommendable!
  Recommendable.configure { |config| config.ratable_classes << self }

  class_eval do
    include Likable
    include Dislikable

    case
    when defined?(Sequel::Model) && ancestors.include?(Sequel::Model)
      def before_destroy() super and remove_from_recommendable! end
    when defined?(ActiveRecord::Base)            && ancestors.include?(ActiveRecord::Base),
         defined?(Mongoid::Document)             && ancestors.include?(Mongoid::Document),
         defined?(MongoMapper::Document)         && ancestors.include?(MongoMapper::Document),
         defined?(MongoMapper::EmbeddedDocument) && ancestors.include?(MongoMapper::EmbeddedDocument)
      before_destroy :remove_from_recommendable!
    when defined?(DataMapper::Resource) && ancestors.include?(DataMapper::Resource)
      before :destroy, :remove_from_recommendable!
    else
      warn "Model #{self} is not using a supported ORM. You must handle removal from Redis manually when destroying instances."
    end

    # Whether or not items belonging to this class can be recommended.
    #
    # @return true if a user class `recommends :this`
    def self.recommendable?() true end

    # Check to see if anybody has rated (liked or disliked) this object
    #
    # @return true if anybody has liked/disliked this
    def rated?
      liked_by_count > 0 || disliked_by_count > 0
    end

    # Query for the top-N items sorted by score
    #
    # @param [Hash] options a hash of options to modify which items are returned
    # @option options [Integer] :count the number of items to fetch (defaults to 1)
    # @option options [Integer] :offset an offset to allow paging through results
    # @return [Array] the top items belonging to this class, sorted by score
    def self.top(options = {})
      if options.is_a?(Integer)
        options = { :count => options }
        warn "[DEPRECATION] Recommenable::Ratable.top now takes an options hash. Please call `.top(count: #{options[:count]})` instead of just `.top(#{options[:count]})`"
      end
      options.reverse_merge!(:count => 1, :offset => 0)
      score_set = Recommendable::Helpers::RedisKeyMapper.score_set_for(self)
      ids = Recommendable.redis.zrevrange(score_set, options[:offset], options[:offset] + options[:count] - 1)

      Recommendable.query(self, ids).sort_by { |item| ids.index(item.id.to_s) }
    end

    # Returns the class that has been explicitly been made ratable, whether it is this
    # class or a superclass. This allows a ratable class and all of its subclasses to be
    # considered the same type of ratable and give recommendations from the base class
    # or any of the subclasses.
    def self.ratable_class
      ancestors.find { |klass| Recommendable.config.ratable_classes.include?(klass) }
    end

    private

    # Completely removes this item from redis. Called from a before_destroy hook.
    # @private
    def remove_from_recommendable!
      sets  = [] # SREM needed
      zsets = [] # ZREM needed
      keys  = [] # DEL  needed
      # Remove this item from the score zset
      zsets << Recommendable::Helpers::RedisKeyMapper.score_set_for(self.class)

      # Remove this item's liked_by/disliked_by sets
      keys << Recommendable::Helpers::RedisKeyMapper.liked_by_set_for(self.class, id)
      keys << Recommendable::Helpers::RedisKeyMapper.disliked_by_set_for(self.class, id)

      # Remove this item from any user's like/dislike/hidden/bookmark sets
      %w[liked disliked hidden bookmarked].each do |action|
        sets += Recommendable.redis.keys(Recommendable::Helpers::RedisKeyMapper.send("#{action}_set_for", self.class, '*'))
      end

      # Remove this item from any user's recommendation zset
      zsets += Recommendable.redis.keys(Recommendable::Helpers::RedisKeyMapper.recommended_set_for(self.class, '*'))

      Recommendable.redis.pipelined do |redis|
        sets.each { |set| redis.srem(set, id) }
        zsets.each { |zset| redis.zrem(zset, id) }
        redis.del(*keys)
      end
    end
  end
end
rated?() click to toggle source

Check to see if anybody has rated (liked or disliked) this object

@return true if anybody has liked/disliked this

# File lib/recommendable/ratable.rb, line 40
def rated?
  liked_by_count > 0 || disliked_by_count > 0
end
recommendable?() click to toggle source

Whether or not items belonging to this class can be recommended.

@return true if a user class `recommends :this`

# File lib/recommendable/ratable.rb, line 105
def recommendable?() false end
remove_from_recommendable!() click to toggle source

Completely removes this item from redis. Called from a before_destroy hook. @private

# File lib/recommendable/ratable.rb, line 74
def remove_from_recommendable!
  sets  = [] # SREM needed
  zsets = [] # ZREM needed
  keys  = [] # DEL  needed
  # Remove this item from the score zset
  zsets << Recommendable::Helpers::RedisKeyMapper.score_set_for(self.class)

  # Remove this item's liked_by/disliked_by sets
  keys << Recommendable::Helpers::RedisKeyMapper.liked_by_set_for(self.class, id)
  keys << Recommendable::Helpers::RedisKeyMapper.disliked_by_set_for(self.class, id)

  # Remove this item from any user's like/dislike/hidden/bookmark sets
  %w[liked disliked hidden bookmarked].each do |action|
    sets += Recommendable.redis.keys(Recommendable::Helpers::RedisKeyMapper.send("#{action}_set_for", self.class, '*'))
  end

  # Remove this item from any user's recommendation zset
  zsets += Recommendable.redis.keys(Recommendable::Helpers::RedisKeyMapper.recommended_set_for(self.class, '*'))

  Recommendable.redis.pipelined do |redis|
    sets.each { |set| redis.srem(set, id) }
    zsets.each { |zset| redis.zrem(zset, id) }
    redis.del(*keys)
  end
end