├── VERSION ├── .autotest ├── .rspec ├── autotest └── discover.rb ├── .gitignore ├── lib ├── likeable │ ├── adapters │ │ ├── ohm_adapter.rb │ │ ├── mongoid_adapter.rb │ │ └── default_adapter.rb │ ├── user_methods.rb │ ├── like.rb │ ├── facepile.rb │ └── module_methods.rb └── likeable.rb ├── Gemfile ├── spec ├── likeable │ ├── adapters │ │ ├── mongoid_adapter_spec.rb │ │ └── ohm_adapter_spec.rb │ ├── setup_spec.rb │ ├── like_spec.rb │ ├── module_methods_spec.rb │ ├── user_methods_spec.rb │ └── facepile_spec.rb ├── spec_helper.rb └── likeable_spec.rb ├── Rakefile ├── license.txt ├── likeable.gemspec └── README.md /VERSION: -------------------------------------------------------------------------------- 1 | 0.1.2 -------------------------------------------------------------------------------- /.autotest: -------------------------------------------------------------------------------- 1 | require 'autotest/growl' -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format nested 2 | --color -------------------------------------------------------------------------------- /autotest/discover.rb: -------------------------------------------------------------------------------- 1 | Autotest.add_discovery { "rspec2" } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Gemfile.lock 2 | /pkg 3 | /vendor 4 | .rvmrc 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /lib/likeable/adapters/ohm_adapter.rb: -------------------------------------------------------------------------------- 1 | module Likeable 2 | module OhmAdapter 3 | def self.find_one 4 | lambda { |klass, id| 5 | klass[id] 6 | } 7 | end 8 | 9 | def self.find_many 10 | lambda { |klass, ids| 11 | Array(ids).collect do |id| 12 | klass[id] 13 | end.compact 14 | } 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/likeable/adapters/mongoid_adapter.rb: -------------------------------------------------------------------------------- 1 | module Likeable 2 | module MongoidAdapter 3 | def self.cast_id 4 | lambda { |id| id.to_s } 5 | end 6 | 7 | def self.find_one 8 | lambda { |klass, id| 9 | klass.find id 10 | } 11 | end 12 | 13 | def self.find_many 14 | lambda { |klass, ids| 15 | klass.find ids 16 | } 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/likeable/adapters/default_adapter.rb: -------------------------------------------------------------------------------- 1 | module Likeable 2 | module DefaultAdapter 3 | def self.cast_id 4 | lambda { |id| id.to_i } 5 | end 6 | 7 | def self.find_one 8 | lambda { |klass, id| 9 | klass.where(:id => id).first 10 | } 11 | end 12 | 13 | def self.find_many 14 | lambda { |klass, ids| 15 | klass.where(:id => ids) 16 | } 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | gem 'activesupport' 3 | gem 'keytar', '>=1.5.2' 4 | gem 'redis' 5 | 6 | 7 | group :development, :test do 8 | gem 'activerecord', '~>3.0.4' ## not needed if you're just using KeyBuilder 9 | gem 'rake', '~>0.8.7' 10 | gem 'jeweler' 11 | gem "autotest-standalone" 12 | gem "autotest-growl" 13 | end 14 | 15 | group :test do 16 | gem 'sqlite3', '~> 1.3.3' 17 | gem 'rspec', '~> 2.5' 18 | end 19 | -------------------------------------------------------------------------------- /spec/likeable/adapters/mongoid_adapter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Likeable::MongoidAdapter do 4 | let(:klass) { Class.new } 5 | 6 | before do 7 | Likeable.adapter = Likeable::MongoidAdapter 8 | end 9 | 10 | after do 11 | default_adapter! 12 | end 13 | 14 | it "finds one by passing the id to find" do 15 | klass.should_receive(:find).with(42) 16 | Likeable.find_one(klass, 42) 17 | end 18 | 19 | it "finds many by passing the ids array find" do 20 | klass.should_receive(:find).with([1, 42]) 21 | Likeable.find_many(klass, [1, 42]) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/likeable/adapters/ohm_adapter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Likeable::OhmAdapter do 4 | let(:klass) { Class.new } 5 | 6 | before do 7 | Likeable.adapter = Likeable::OhmAdapter 8 | end 9 | 10 | after do 11 | default_adapter! 12 | end 13 | 14 | it "finds one by passing the id to find" do 15 | klass.should_receive(:[]).with(42) 16 | Likeable.find_one(klass, 42) 17 | end 18 | 19 | it "finds many by passing the ids array find" do 20 | klass.should_receive(:[]).with(1).ordered 21 | klass.should_receive(:[]).with(42).ordered 22 | Likeable.find_many(klass, [1, 42]) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/likeable/user_methods.rb: -------------------------------------------------------------------------------- 1 | module Likeable::UserMethods 2 | extend ActiveSupport::Concern 3 | 4 | included do 5 | include Keytar 6 | define_key :like, :key_case => nil 7 | end 8 | 9 | 10 | def like!(obj) 11 | obj.add_like_from(self) 12 | end 13 | 14 | def unlike!(obj) 15 | obj.remove_like_from(self) 16 | end 17 | 18 | def like?(obj) 19 | obj.liked_by?(self) 20 | end 21 | alias :likes? :like? 22 | 23 | def friend_ids_that_like(obj) 24 | obj.liked_friend_ids(self) 25 | end 26 | 27 | def friends_that_like(obj, limit = nil) 28 | obj.liked_friends(self, limit) 29 | end 30 | 31 | # @user.liked(Spot) 32 | # will return all spots that user has liked 33 | def all_liked(klass) 34 | klass.all_liked_by(self) 35 | end 36 | end -------------------------------------------------------------------------------- /lib/likeable/like.rb: -------------------------------------------------------------------------------- 1 | require 'digest/sha1' 2 | 3 | class Likeable::Like 4 | attr_accessor :created_at, :target, :like_user, :user_id 5 | 6 | def initialize(options = {}) 7 | self.created_at = Time.at(options[:time].try(:to_f)||Time.now) 8 | self.target = options[:target] 9 | self.user_id = options[:user].try(:id) || options[:user_id] 10 | self.like_user = options[:user] 11 | end 12 | 13 | def id 14 | Digest::SHA1.hexdigest("#{user_id}#{target.class}#{target.id}#{created_at}") 15 | end 16 | 17 | def user 18 | @user ||= like_user 19 | @user ||= Likeable.find_one(Likeable.user_class, user_id) 20 | @user 21 | end 22 | 23 | def to_hash(type=:full) 24 | { 25 | :created_at => created_at.iso8601, 26 | :type => target.class.name.gsub(/^[A-Za-z]+::/, '').underscore.downcase.to_sym, 27 | :target => target.to_hash(type), 28 | :user => user.to_hash(type) 29 | } 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/likeable/facepile.rb: -------------------------------------------------------------------------------- 1 | module Likeable 2 | module Facepile 3 | # returns friend of user who like target 4 | def liked_friends(user, limit = nil) 5 | friend_ids = liked_friend_ids(user) 6 | friend_ids = friend_ids.sample(limit) unless limit.blank? 7 | @liked_friends ||= Likeable.find_many(User, friend_ids) 8 | end 9 | 10 | def liked_friend_ids(user) 11 | @liked_friend_ids ||= like_user_ids & user.friend_ids 12 | end 13 | 14 | def ids_for_facepile(user, limit = Likeable.facepile_default_limit) 15 | ids = liked_friend_ids(user).shuffle + like_user_ids.shuffle # show friends first 16 | ids.uniq.first(limit) 17 | end 18 | 19 | def users_for_facepile(user, limit = Likeable.facepile_default_limit) 20 | return [] if user.blank? 21 | @facepile ||= begin 22 | return nil unless ids = ids_for_facepile(user, limit) 23 | Likeable.find_many(User, ids) 24 | end 25 | end 26 | end 27 | end -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler' 3 | begin 4 | Bundler.setup(:default, :development, :test) 5 | rescue Bundler::BundlerError => e 6 | $stderr.puts e.message 7 | $stderr.puts "Run `bundle install` to install missing gems" 8 | exit e.status_code 9 | end 10 | require 'rake' 11 | 12 | require 'jeweler' 13 | Jeweler::Tasks.new do |gem| 14 | # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options 15 | gem.name = "likeable" 16 | gem.homepage = "https://github.com/schneems/Likeable" 17 | gem.license = "MIT" 18 | gem.summary = %Q{Like ruby objects backed by redis} 19 | gem.description = %Q{ 20 | Likeable allows you to make your models...well...likeable using redis. 21 | } 22 | gem.email = "richard.schneeman@gmail.com" 23 | gem.authors = ["Schneems"] 24 | gem.add_development_dependency "rspec" 25 | end 26 | Jeweler::RubygemsDotOrgTasks.new 27 | 28 | require 'rspec/core/rake_task' 29 | RSpec::Core::RakeTask.new(:spec) 30 | 31 | task :default => [:spec] 32 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | licensed under MIT License: 2 | 3 | Copyright (c) 2011 Schneems 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /spec/likeable/setup_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | 4 | describe Likeable do 5 | describe "setup" do 6 | context "when the User class is defined" do 7 | before(:each) do 8 | reload_user! 9 | Likeable.user_class = User 10 | @user = User.new 11 | @target = CleanTestClassForLikeable.new 12 | end 13 | 14 | it "" do 15 | result = "foo" 16 | Likeable.setup 17 | 18 | Likeable.after_like do |like| 19 | result = "after_like_called_successfully" 20 | end 21 | 22 | @user.like! @target 23 | result.should == "after_like_called_successfully" 24 | end 25 | end 26 | 27 | context "when the User class doesn't exist" do 28 | before do 29 | # Need a cleaner way to do this, but the setter 30 | # prevents it 31 | Likeable.instance_variable_set(:@user_class, nil) 32 | unload_user! 33 | end 34 | 35 | after do 36 | build_user! 37 | Likeable.setup 38 | end 39 | 40 | it "won't raise an exception" do 41 | lambda { Likeable.setup }.should_not raise_error 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /spec/likeable/like_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | describe Likeable::Like do 3 | before do 4 | @time = Time.now 5 | @user = User.new 6 | end 7 | describe 'attributes' do 8 | it 'stores target, user, and created_at' do 9 | like = Likeable::Like.new(:target => @target, :user => @user, :time => @time) 10 | like.user.should eq(@user) 11 | like.target.should eq(@target) 12 | # Times often fail equality checks due to microsec precision 13 | like.created_at.should be_within(1).of(@time) 14 | end 15 | 16 | it 'converts float time to propper Time object' do 17 | like = Likeable::Like.new(:time => @time.to_f) 18 | like.created_at.should be_within(1).of(@time) 19 | end 20 | end 21 | describe "#user" do 22 | it "returns like_user if available" do 23 | like = Likeable::Like.new(:target => @target, :user => @user, :time => @time) 24 | like.user.should == @user 25 | end 26 | it "finds the user in the Likeable::user_model if the like was initialized without a user" do 27 | like = Likeable::Like.new(:target => @target, :user => nil, :user_id => 100, :time => @time) 28 | Account = stub() 29 | Likeable.stub(:user_class).and_return(Account) 30 | Likeable.should_receive(:find_one).with(Account, 100) 31 | like.user 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/likeable/module_methods_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | class CleanTestClassForLikeable 4 | include Likeable 5 | def like_key 6 | "like_key" 7 | end 8 | 9 | def to_hash(*args); {} end 10 | 11 | def foo 12 | end 13 | 14 | def id 15 | @id ||= rand(100) 16 | end 17 | end 18 | 19 | class LikeableIncludedInSetup 20 | def like_key 21 | "like_key" 22 | end 23 | 24 | def id 25 | @id ||= rand(100) 26 | end 27 | end 28 | 29 | describe Likeable::Facepile do 30 | before(:each) do 31 | @user = User.new 32 | @target = CleanTestClassForLikeable.new 33 | end 34 | 35 | describe "module methods" do 36 | describe ".model" do 37 | it "takes a valid class string and turns it into a class" do 38 | klass = CleanTestClassForLikeable 39 | klass_name = klass.to_s 40 | Likeable.model(klass_name).should eq(klass) 41 | end 42 | end 43 | 44 | describe ".find_by_resource_id" do 45 | it "finds an active-record based object on a valid model and id" do 46 | klass = CleanTestClassForLikeable 47 | klass_name = klass.to_s 48 | id = rand(1000) 49 | klass.should_receive(:where).with(:id => id).and_return([]) 50 | Likeable.find_by_resource_id(klass_name, id) 51 | end 52 | 53 | it "will return nil for an invalid object" do 54 | klass = CleanTestClassForLikeable 55 | klass_name = klass.to_s + "this makes this klass_name invalid" 56 | 57 | Likeable.find_by_resource_id(klass_name, rand(1000)).should be_blank 58 | end 59 | end 60 | 61 | end 62 | 63 | end 64 | -------------------------------------------------------------------------------- /spec/likeable/user_methods_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | class CleanTestClassForLikeable 4 | include Likeable 5 | def like_key 6 | "like_key" 7 | end 8 | 9 | def to_hash(*args); {} end 10 | 11 | def foo 12 | end 13 | 14 | def id 15 | @id ||= rand(100) 16 | end 17 | end 18 | 19 | Likeable.setup 20 | 21 | describe Likeable::UserMethods do 22 | before(:each) do 23 | @user = User.new 24 | @target = CleanTestClassForLikeable.new 25 | end 26 | 27 | describe '#like!' do 28 | it "calls add_like_from in target" do 29 | @target.should_receive(:add_like_from).with(@user) 30 | @user.like! @target 31 | end 32 | end 33 | 34 | describe '#unlike!' do 35 | it "calls remove_like_from in target" do 36 | @target.should_receive(:remove_like_from).with(@user) 37 | @user.unlike! @target 38 | end 39 | end 40 | 41 | describe '#like?' do 42 | it "calls liked_by? in target" do 43 | @target.should_receive(:liked_by?).with(@user) 44 | @user.like? @target 45 | end 46 | end 47 | 48 | describe '#like?' do 49 | it "calls liked_by? in target" do 50 | @target.should_receive(:liked_by?).with(@user) 51 | @user.like? @target 52 | end 53 | end 54 | 55 | describe '#friend_ids_that_like' do 56 | it "calls liked_friend_ids? in target" do 57 | @target.should_receive(:liked_friend_ids).with(@user) 58 | @user.friend_ids_that_like @target 59 | end 60 | end 61 | 62 | describe '#friends_that_like' do 63 | it "calls liked_friends? in target" do 64 | @target.should_receive(:liked_friends).with(@user, nil) 65 | @user.friends_that_like @target 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /spec/likeable/facepile_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | class CleanTestClassForLikeable 4 | include Likeable 5 | def like_key 6 | "like_key" 7 | end 8 | 9 | def to_hash(*args); {} end 10 | 11 | def foo 12 | end 13 | 14 | def id 15 | @id ||= rand(100) 16 | end 17 | end 18 | 19 | 20 | describe Likeable::Facepile do 21 | before(:each) do 22 | @user = User.new 23 | @target = CleanTestClassForLikeable.new 24 | end 25 | 26 | describe 'facepile' do 27 | before do 28 | @friend_ids = [1,2,3,4] 29 | @like_ids = [3,4,5,6,7,8,9,10,11,12] 30 | @intersection = @friend_ids & @like_ids 31 | end 32 | describe '#ids_for_facepile' do 33 | it 'builds a array of ids with friend ids and randoms if they have liked the object up to the limit' do 34 | @target.should_receive(:liked_friend_ids).with(@user).and_return(@friend_ids) 35 | @target.should_receive(:like_user_ids).and_return(@like_ids) 36 | @target.ids_for_facepile(@user).should include(@intersection.sample) 37 | end 38 | 39 | it 'can be limited' do 40 | limit = 3 41 | @target.should_receive(:liked_friend_ids).and_return(@friend_ids) 42 | @target.should_receive(:like_user_ids).and_return(@like_ids) 43 | @target.ids_for_facepile(@user, limit).count.should eq(limit) 44 | end 45 | end 46 | 47 | describe '#users_for_facepile' do 48 | it 'builds a array of users if they have liked the object' do 49 | @target.should_receive(:ids_for_facepile).and_return(@friend_ids) 50 | User.should_receive(:where).with(:id => @friend_ids) 51 | @target.users_for_facepile(@user) 52 | end 53 | end 54 | 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'active_record' 3 | require 'singleton' 4 | require 'tempfile' 5 | 6 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 7 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '../..', 'lib')) 8 | 9 | require 'likeable' 10 | 11 | module UserHelperMethods 12 | 13 | def build_user! 14 | eval %Q{ 15 | class ::User 16 | include Likeable::UserMethods 17 | def id 18 | @time ||= Time.now.to_f.to_s.tr('.', '').to_i 19 | end 20 | 21 | def self.where(*args) 22 | end 23 | 24 | def friends_ids 25 | [] 26 | end 27 | 28 | def self.after_destroy 29 | end 30 | end 31 | } 32 | end 33 | 34 | def unload_user! 35 | Object.instance_eval{ remove_const :User } 36 | end 37 | 38 | def reload_user! 39 | unload_user! 40 | build_user! 41 | end 42 | 43 | def default_adapter! 44 | Likeable.adapter = Likeable::DefaultAdapter 45 | end 46 | 47 | end 48 | 49 | RSpec.configure do |c| 50 | 51 | c.include UserHelperMethods 52 | 53 | c.treat_symbols_as_metadata_keys_with_true_values = true 54 | 55 | c.before :suite do 56 | Likeable.redis = NullRedis.new 57 | end 58 | 59 | c.before :each do 60 | build_user! 61 | end 62 | 63 | c.around :each, :integration do |example| 64 | IntegrationTestRedis.instance.start 65 | Likeable.redis = IntegrationTestRedis.instance.client 66 | Likeable.redis.flushdb 67 | example.run 68 | IntegrationTestRedis.instance.stop 69 | Likeable.redis = NullRedis.new 70 | end 71 | 72 | end 73 | 74 | class NullRedis 75 | 76 | def method_missing(*args) 77 | self 78 | end 79 | 80 | def to_s 81 | "Null Redis" 82 | end 83 | 84 | end 85 | 86 | class IntegrationTestRedis 87 | 88 | include ::Singleton 89 | 90 | PORT = 9737 91 | PIDFILE = Tempfile.new('likeable-integration-test-redis-pid') 92 | 93 | def start 94 | install_at_exit_handler 95 | system("echo '#{options}' | redis-server -") 96 | end 97 | 98 | def stop 99 | system("if [ -e #{PIDFILE.path} ]; then kill -QUIT $(cat #{PIDFILE.path}) 2>/dev/null; fi") 100 | end 101 | 102 | def client 103 | return Redis.new(:port => PORT, :db => 15) 104 | end 105 | 106 | private 107 | 108 | def options 109 | { 110 | 'daemonize' => 'yes', 111 | 'pidfile' => PIDFILE.path, 112 | 'bind' => '127.0.0.1', 113 | 'port' => PORT, 114 | 'timeout' => 300, 115 | 'dir' => '/tmp', 116 | 'loglevel' => 'debug', 117 | 'logfile' => 'stdout', 118 | 'databases' => 16 119 | }.map { |k, v| "#{k} #{v}" }.join("\n") 120 | end 121 | 122 | def install_at_exit_handler 123 | at_exit { 124 | IntegrationTestRedis.instance.stop 125 | } 126 | end 127 | 128 | end 129 | 130 | class CleanTestClassForLikeable 131 | 132 | include Likeable 133 | 134 | def like_key 135 | "like_key" 136 | end 137 | 138 | def to_hash(*args); 139 | Hash.new 140 | end 141 | 142 | def foo 143 | nil 144 | end 145 | 146 | def id 147 | @id ||= rand(100) 148 | end 149 | 150 | end 151 | -------------------------------------------------------------------------------- /likeable.gemspec: -------------------------------------------------------------------------------- 1 | # Generated by jeweler 2 | # DO NOT EDIT THIS FILE DIRECTLY 3 | # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec' 4 | # -*- encoding: utf-8 -*- 5 | 6 | Gem::Specification.new do |s| 7 | s.name = "likeable" 8 | s.version = "0.1.2" 9 | 10 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 11 | s.authors = ["Schneems"] 12 | s.date = "2012-05-24" 13 | s.description = "\n Likeable allows you to make your models...well...likeable using redis.\n " 14 | s.email = "richard.schneeman@gmail.com" 15 | s.extra_rdoc_files = [ 16 | "README.md" 17 | ] 18 | s.files = [ 19 | ".autotest", 20 | ".rspec", 21 | "Gemfile", 22 | "README.md", 23 | "Rakefile", 24 | "VERSION", 25 | "autotest/discover.rb", 26 | "lib/likeable.rb", 27 | "lib/likeable/adapters/default_adapter.rb", 28 | "lib/likeable/adapters/mongoid_adapter.rb", 29 | "lib/likeable/adapters/ohm_adapter.rb", 30 | "lib/likeable/facepile.rb", 31 | "lib/likeable/like.rb", 32 | "lib/likeable/module_methods.rb", 33 | "lib/likeable/user_methods.rb", 34 | "license.txt", 35 | "likeable.gemspec", 36 | "spec/likeable/adapters/mongoid_adapter_spec.rb", 37 | "spec/likeable/adapters/ohm_adapter_spec.rb", 38 | "spec/likeable/facepile_spec.rb", 39 | "spec/likeable/like_spec.rb", 40 | "spec/likeable/module_methods_spec.rb", 41 | "spec/likeable/setup_spec.rb", 42 | "spec/likeable/user_methods_spec.rb", 43 | "spec/likeable_spec.rb", 44 | "spec/spec_helper.rb" 45 | ] 46 | s.homepage = "https://github.com/schneems/Likeable" 47 | s.licenses = ["MIT"] 48 | s.require_paths = ["lib"] 49 | s.rubygems_version = "1.8.10" 50 | s.summary = "Like ruby objects backed by redis" 51 | 52 | if s.respond_to? :specification_version then 53 | s.specification_version = 3 54 | 55 | if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then 56 | s.add_runtime_dependency(%q, [">= 0"]) 57 | s.add_runtime_dependency(%q, [">= 1.5.2"]) 58 | s.add_runtime_dependency(%q, [">= 0"]) 59 | s.add_development_dependency(%q, ["~> 3.0.4"]) 60 | s.add_development_dependency(%q, ["~> 0.8.7"]) 61 | s.add_development_dependency(%q, [">= 0"]) 62 | s.add_development_dependency(%q, [">= 0"]) 63 | s.add_development_dependency(%q, [">= 0"]) 64 | s.add_development_dependency(%q, [">= 0"]) 65 | else 66 | s.add_dependency(%q, [">= 0"]) 67 | s.add_dependency(%q, [">= 1.5.2"]) 68 | s.add_dependency(%q, [">= 0"]) 69 | s.add_dependency(%q, ["~> 3.0.4"]) 70 | s.add_dependency(%q, ["~> 0.8.7"]) 71 | s.add_dependency(%q, [">= 0"]) 72 | s.add_dependency(%q, [">= 0"]) 73 | s.add_dependency(%q, [">= 0"]) 74 | s.add_dependency(%q, [">= 0"]) 75 | end 76 | else 77 | s.add_dependency(%q, [">= 0"]) 78 | s.add_dependency(%q, [">= 1.5.2"]) 79 | s.add_dependency(%q, [">= 0"]) 80 | s.add_dependency(%q, ["~> 3.0.4"]) 81 | s.add_dependency(%q, ["~> 0.8.7"]) 82 | s.add_dependency(%q, [">= 0"]) 83 | s.add_dependency(%q, [">= 0"]) 84 | s.add_dependency(%q, [">= 0"]) 85 | s.add_dependency(%q, [">= 0"]) 86 | end 87 | end 88 | 89 | -------------------------------------------------------------------------------- /lib/likeable/module_methods.rb: -------------------------------------------------------------------------------- 1 | require 'redis' 2 | 3 | module Likeable 4 | mattr_accessor :facepile_default_limit 5 | self.facepile_default_limit = 9 6 | 7 | 8 | ### Module Methods ### 9 | # ------------------ # 10 | class << self 11 | attr_writer :cast_id, :find_one, :find_many 12 | 13 | def classes 14 | (@classes||[]).flatten 15 | end 16 | 17 | def classes=(*args) 18 | @classes = args 19 | end 20 | 21 | # Likeable.model("Highlight") 22 | # ------------------------- # 23 | # turns a string into a model 24 | # "Highlight".constantize # => Highlight; "Hi1i6ht".constantize = #=> false 25 | def model(target_model) 26 | target_model.camelcase.constantize 27 | rescue NameError => ex 28 | return false 29 | end 30 | 31 | # Likeable.find_by_resource_id("highlight", 22) 32 | # ---------------------------------------- # 33 | # # id)} 116 | # like.find_many = lambda {|klass, ids| klass.where(:id => ids)} 117 | # end 118 | def setup(&block) 119 | yield self unless block.blank? 120 | true 121 | end 122 | end 123 | end 124 | -------------------------------------------------------------------------------- /lib/likeable.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/concern' 2 | require 'keytar' 3 | 4 | module Likeable 5 | extend ActiveSupport::Concern 6 | 7 | included do 8 | include Keytar 9 | include Likeable::Facepile 10 | define_key :like, :key_case => nil 11 | 12 | if self.respond_to?(:after_destroy) 13 | after_destroy :destroy_all_likes 14 | else 15 | warn "#{self} doesn't support after_destroy callback, likes will not be cleared automatically when object is destroyed" 16 | end 17 | end 18 | 19 | def destroy_all_likes 20 | liked_users.each {|user| self.remove_like_from(user) } 21 | end 22 | 23 | # create a like 24 | # the user who created the like has a reference to the object liked 25 | def add_like_from(user, time = Time.now.to_f) 26 | Likeable.redis.hset(like_key, user.id, time) 27 | Likeable.redis.hset(user.like_key(self.class.to_s.downcase), self.id, time) 28 | like = Like.new(:target => self, :user => user, :time => time) 29 | after_like(like) 30 | clear_memoized_methods(:like_count, :like_user_ids, :liked_user_ids, :liked_users, :likes) 31 | like 32 | end 33 | 34 | def clear_memoized_methods(*methods) 35 | methods.each do |method| 36 | eval("@#{method} = nil") 37 | end 38 | end 39 | 40 | def after_like(like) 41 | Likeable.after_like.call(like) 42 | end 43 | 44 | # removes a like 45 | def remove_like_from(user) 46 | if Likeable.redis.hexists(like_key, user.id) 47 | Likeable.redis.hdel(like_key, user.id) 48 | Likeable.redis.hdel(user.like_key(self.class.to_s.downcase), self.id) 49 | after_unlike(user) 50 | clear_memoized_methods(:like_count, :like_user_ids, :liked_user_ids, :liked_users) 51 | end 52 | end 53 | 54 | def after_unlike(user) 55 | Likeable.after_unlike.call(user) 56 | end 57 | 58 | def like_count 59 | @like_count ||= @like_user_ids.try(:count) || @likes.try(:count) || Likeable.redis.hlen(like_key) 60 | end 61 | 62 | # get all user ids that have liked a target object 63 | def like_user_ids 64 | @like_user_ids ||= (Likeable.redis.hkeys(like_key)||[]).map {|id| Likeable.cast_id(id)} 65 | end 66 | 67 | def liked_users(limit = nil) 68 | @liked_users ||= Likeable.find_many(Likeable.user_class, like_user_ids) 69 | end 70 | 71 | def likes 72 | @likes ||= begin 73 | Likeable.redis.hgetall(like_key).collect do |user_id, time| 74 | Like.new(:user_id => user_id, :time => time, :target => self) 75 | end 76 | end 77 | end 78 | 79 | # did given user like the object 80 | def liked_by?(user) 81 | return false unless user 82 | liked_by = @like_user_ids.include?(Likeable.cast_id(user.id)) if @like_user_ids 83 | liked_by ||= true & Likeable.redis.hexists(like_key, user.id) 84 | end 85 | 86 | 87 | def likeable_resource_name 88 | Likeable.get_resource_name_for_class(self.class) 89 | end 90 | 91 | 92 | ### Class Methods ### 93 | # ----------------- # 94 | # allows us to setup callbacks when creating likes 95 | # after_like :notify_users 96 | # allows us to setup callbacks when destroying likes 97 | # after_unlike :notify_users 98 | module ClassMethods 99 | 100 | def all_liked_ids_by(user) 101 | key = user.like_key(self.to_s.downcase) 102 | ids = (Likeable.redis.hkeys(key)||[]).map {|id| Likeable.cast_id(id)} 103 | end 104 | 105 | def all_liked_by(user) 106 | ids = all_liked_ids_by(user) 107 | Likeable.find_many(self, ids) 108 | end 109 | 110 | def after_like(*methods) 111 | define_method(:after_like) do |like| 112 | methods.each do |method| 113 | eval("#{method}(like)") 114 | end 115 | end 116 | end 117 | 118 | def after_unlike(*methods) 119 | define_method(:after_unlike) do |user| 120 | methods.each do |method| 121 | eval("#{method}(user)") 122 | end 123 | end 124 | end 125 | end 126 | 127 | autoload :DefaultAdapter , "likeable/adapters/default_adapter" 128 | autoload :MongoidAdapter , "likeable/adapters/mongoid_adapter" 129 | autoload :OhmAdapter , "likeable/adapters/ohm_adapter" 130 | end 131 | 132 | require 'likeable/like' 133 | require 'likeable/facepile' 134 | require 'likeable/user_methods' 135 | require 'likeable/module_methods' 136 | -------------------------------------------------------------------------------- /spec/likeable_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Likeable do 4 | 5 | before(:all) do 6 | Likeable.setup do |like| 7 | like.find_one = lambda { |klass, id| klass.where(:id => id) } 8 | end 9 | end 10 | 11 | before(:each) do 12 | @user = User.new 13 | @target = CleanTestClassForLikeable.new 14 | end 15 | 16 | describe 'instance methods' do 17 | 18 | describe "#add_like_from" do 19 | 20 | it "creates a like" do 21 | target_class = @target.class.to_s.downcase 22 | user_like_key = "users:like:#{@user.id}:#{target_class}" 23 | time = Time.now.to_f 24 | @user.should_receive(:like_key).with(target_class).and_return(user_like_key) 25 | Likeable.redis.should_receive(:hset).with("like_key", @user.id, time).once 26 | Likeable.redis.should_receive(:hset).with(user_like_key, @target.id, time).once 27 | @target.add_like_from(@user, time) 28 | end 29 | 30 | end 31 | 32 | describe "#remove_like_from" do 33 | 34 | it "removes a like" do 35 | target_class = @target.class.to_s.downcase 36 | user_like_key = "users:like:#{@user.id}:#{target_class}" 37 | Likeable.redis.should_receive(:hexists).with("like_key", @user.id).and_return(true) 38 | @user.should_receive(:like_key).with(target_class).and_return(user_like_key) 39 | Likeable.redis.should_receive(:hdel).with("like_key", @user.id).once 40 | Likeable.redis.should_receive(:hdel).with(user_like_key, @target.id) 41 | @target.remove_like_from(@user) 42 | end 43 | 44 | it "doesn't call after_unlike if like didn't exist" do 45 | CleanTestClassForLikeable.after_unlike(:foo) 46 | Likeable.redis.should_receive(:hexists).with("like_key", @user.id).and_return(false) 47 | @target = CleanTestClassForLikeable.new 48 | @target.should_not_receive(:foo) 49 | @target.remove_like_from(@user) 50 | end 51 | 52 | end 53 | 54 | describe "#liked_users" do 55 | 56 | it "finds the users that like it", :integration do 57 | user1 = User.new :name => "user1" 58 | user2 = User.new :name => "user2" 59 | user1.like! @target 60 | user2.like! @target 61 | User.should_receive(:where).with(:id => [user1.id, user2.id]).and_return([user1, user2]) 62 | @target.liked_users.should =~ [user1, user2] 63 | end 64 | 65 | it "supports user id models where the id is a hash string", :integration do 66 | Likeable.cast_id = lambda { |id| id.to_s } 67 | user_id = "ce7961bd9ca9de6753b6e04754c1c615" 68 | @user.should_receive(:id).at_least(:once).and_return(user_id) 69 | @user.like! @target 70 | User.should_receive(:where).with(:id => [user_id]).and_return([@user]) 71 | @target.liked_users.should =~ [@user] 72 | end 73 | 74 | end 75 | 76 | describe "#likes" do 77 | 78 | it "returns set of likes" do 79 | Likeable.redis.should_receive(:hkeys).with("like_key").once 80 | @target.like_user_ids 81 | end 82 | 83 | end 84 | 85 | describe "#liked_by?" do 86 | 87 | it "will answer if current user likes target", :integration do 88 | @target.should_not be_liked_by(@user) 89 | @user.like! @target 90 | @target.should be_liked_by(@user) 91 | end 92 | 93 | it "works with hash string based user ids", :integration do 94 | user_id = "fa7961bd9ca9de6753b6e04754c1c615" 95 | @user.should_receive(:id).at_least(:once).and_return(user_id) 96 | @target.should_not be_liked_by(@user) 97 | @user.like! @target 98 | @target.should be_liked_by(@user) 99 | end 100 | 101 | end 102 | 103 | describe "#liked_friend_ids" do 104 | 105 | it "will return all friend ids of user who like target" do 106 | common_value = 3 107 | @target.should_receive(:like_user_ids).and_return([1,2, common_value]) 108 | @user.should_receive(:friend_ids).and_return([common_value]) 109 | @target.liked_friend_ids(@user).should == [common_value] 110 | end 111 | 112 | end 113 | 114 | describe "#liked_friends" do 115 | 116 | it "will return all friends who like object" do 117 | values = [1] 118 | @target.should_receive(:liked_friend_ids).with(@user).and_return(values) 119 | User.should_receive(:where).with(:id => values) 120 | @target.liked_friends(@user) 121 | end 122 | 123 | end 124 | 125 | end 126 | 127 | describe "class methods" do 128 | 129 | describe 'after_like' do 130 | 131 | it 'should be a class method when included' do 132 | CleanTestClassForLikeable.respond_to?(:after_like).should be_true 133 | end 134 | 135 | it 'is called after a like is created' do 136 | CleanTestClassForLikeable.after_like(:foo) 137 | @target.should_receive(:foo) 138 | @target.add_like_from(@user) 139 | end 140 | 141 | end 142 | 143 | describe 'after_unlike' do 144 | 145 | it 'should be a class method when included' do 146 | CleanTestClassForLikeable.respond_to?(:after_unlike).should be_true 147 | end 148 | 149 | it 'is called after a like is destroyed' do 150 | CleanTestClassForLikeable.after_unlike(:foo) 151 | Likeable.redis.should_receive(:hexists).and_return(true) 152 | @target.should_receive(:foo) 153 | @target.remove_like_from(@user) 154 | end 155 | 156 | end 157 | 158 | end 159 | 160 | end 161 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Use Redis to Make your Ruby objects Likeable! 2 | ====== 3 | 4 | I no longer use this gem in production (it was written for Gowalla), if you do and want to help me maintain it, let me know [@schneems](http://twitter.com/schneems). 5 | 6 | You like this 7 | ------------- 8 | Likeable will allow your models to be liked by users, just drop a few lines of code into your model and you're good to go. 9 | 10 | ```ruby 11 | 12 | class Comment 13 | include Likeable 14 | 15 | # ... 16 | end 17 | 18 | class User 19 | include Likeable::UserMethods 20 | 21 | # ... 22 | end 23 | 24 | Likeable.setup do |likeable| 25 | likeable.redis = Redis.new 26 | end 27 | 28 | comment = Comment.find(15) 29 | comment.like_count # => 0 30 | current_user.like!(comment) # => # 31 | comment.like_count # => 1 32 | comment.likes # => [#] 33 | comment.likes.last.user # => # 34 | comment.likes.last.created_at # => Wed Jul 27 19:34:32 -0500 2011 35 | 36 | comment.liked_by?(current_user) # => true 37 | 38 | current_user.all_liked(Comment) # => [#, ...] 39 | 40 | liked_comment = Likeable.find_by_resource_id("Comment", 15) 41 | liked_comment == comment # => true 42 | 43 | ``` 44 | 45 | This library doesn't do dislikes, if you want something with more flexibility check out [opinions](https://github.com/leehambley/opinions). 46 | 47 | ## Screencast 48 | 49 | You can view a [screencast of likeable in action on youtube](http://youtu.be/iJoMXUQ33Jw?hd=1). There is also an example [Likeable rails application](https://github.com/schneems/likeable_example) that you can use to follow along. 50 | 51 | 52 | 53 | Setup 54 | ======= 55 | Gemfile: 56 | 57 | gem 'likeable' 58 | 59 | Next set up your Redis connection in initializers/likeable.rb: 60 | 61 | ```ruby 62 | 63 | Likeable.setup do |likeable| 64 | likeable.redis = Redis.new 65 | end 66 | ``` 67 | 68 | Then add the `Likeable::UserMethods` module to models/user.rb: 69 | 70 | ```ruby 71 | 72 | class User 73 | include Likeable::UserMethods 74 | end 75 | ``` 76 | 77 | Finally add `Likeable` module to any model you want to be liked: 78 | 79 | ```ruby 80 | 81 | class Comment 82 | include Likeable 83 | end 84 | ``` 85 | 86 | ## Rails Info 87 | If you're using Likeable in Rails this should help you get started 88 | 89 | controllers/likes_controller.rb 90 | 91 | ```ruby 92 | 93 | class LikesController < ApplicationController 94 | 95 | def create 96 | target = Likeable.find_by_resource_id(params[:resource_name], params[:resource_id]) 97 | current_user.like!(target) 98 | redirect_to :back, :notice => 'success' 99 | end 100 | 101 | def destroy 102 | target = Likeable.find_by_resource_id(params[:resource_name], params[:resource_id]) 103 | current_user.unlike!(target) 104 | redirect_to :back, :notice => 'success' 105 | end 106 | end 107 | 108 | ``` 109 | 110 | config/routes.rb 111 | 112 | ```ruby 113 | 114 | delete 'likes/:resource_name/:resource_id' => "likes#destroy", :as => 'like' 115 | post 'likes/:resource_name/:resource_id' => "likes#create", :as => 'like' 116 | 117 | ``` 118 | 119 | helpers/like_helper.rb 120 | 121 | ```ruby 122 | 123 | def like_link_for(target) 124 | link_to "like it!!", like_path(:resource_name => target .class, :resource_id => target.id), :method => :post 125 | end 126 | 127 | def unlike_link_for(target) 128 | link_to "unlike it!!", like_path(:resource_name => target.class, :resource_id => target.id), :method => :delete 129 | end 130 | 131 | ``` 132 | 133 | Then in any view you can simply call the helper methods to give your user a link 134 | 135 | ```ruby 136 | 137 | <%- if @user.likes? @comment -%> 138 | <%= unlike_link_for @comment %> 139 | <%- else -%> 140 | <%= like_link_for @comment %> 141 | <%- end -%> 142 | 143 | 144 | ``` 145 | 146 | Why 147 | === 148 | 149 | We chose Redis because it is screaming fast, and very simple to work with. By using redis for likeable we take load off of our relational database and speed up individual calls retrieve information about the "liked" state of an object. If you're not using redis in production, and don't want to, there are many other great liking/voting libraries out there such as [thumbs up](https://github.com/brady8/thumbs_up). 150 | 151 | 152 | RedisRed RedisRedi 153 | RedisRedisRedi RedisRedisRedisR 154 | RedisRedisRedisRedi RedisRedisRedisRedi 155 | RedisRedisRedisRedisRedisRedisRe Redi 156 | RedisRedisRedisRedisRedisRedisRe Redi 157 | RedisRedisRedisRedisRedisRedisRedisR Redi 158 | RedisRedisRedisRedisRedisRedisRedisRedis R 159 | RedisRedisRedisRedisRedisRedisRedisRedisRedi Red 160 | RedisRedisRedisRedisRedisRedisRedisRedisRedisRe R 161 | RedisRedisRedisRedisRedisRedisRedisRedisRedisRedi 162 | RedisRedisRedisRedisRedisRedisRedisRedisRedisRedi 163 | RedisRedisRedisRedisRedisRedisRedisRedisRedisRe 164 | RedisRedisRedisRedisRedisRedisRedisRedisRedis 165 | RedisRedisRedisRedisRedisRedisRedisRedisRe 166 | RedisRedisRedisRedisRedisRedisRedisRe 167 | RedisRedisRedisRedisRedisRedisR 168 | RedisRedisRedisRedisRedis 169 | RedisRedisRedisRedis 170 | RedisRedisRed 171 | RedisRedi 172 | RedisR 173 | Redi 174 | Re 175 | Authors 176 | ======= 177 | [Richard Schneeman](http://schneems.com) for [Gowalla](http://gowalla.com) <3 178 | 179 | 180 | Contribution 181 | ============ 182 | 183 | Fork away. If you want to chat about a feature idea, or a question you can find me on the twitters [@schneems](http://twitter.com/schneems). Put any major changes into feature branches. Make sure all tests stay green, and make sure your changes are covered. 184 | 185 | 186 | licensed under MIT License 187 | Copyright (c) 2011 Schneems. See LICENSE.txt for 188 | further details. 189 | --------------------------------------------------------------------------------