├── lib ├── amistad │ ├── version.rb │ ├── friendship_model.rb │ ├── config.rb │ ├── friendships.rb │ ├── friend_model.rb │ ├── mongoid_friend_model.rb │ ├── mongo_mapper_friend_model.rb │ ├── active_record_friendship_model.rb │ ├── mongo_friend_model.rb │ └── active_record_friend_model.rb ├── amistad.rb └── generators │ └── amistad │ └── install │ ├── templates │ └── create_friendships.rb │ └── install_generator.rb ├── Gemfile ├── .gitignore ├── spec ├── mongo_mapper │ ├── mongo_mapper_spec_helper.rb │ ├── friend_spec.rb │ └── friend_custom_model_spec.rb ├── mongoid │ ├── mongoid_spec_helper.rb │ ├── friend_spec.rb │ └── friend_custom_model_spec.rb ├── activerecord │ ├── friend_spec.rb │ ├── friendship_spec.rb │ ├── friend_custom_model_spec.rb │ ├── friendship_with_custom_friend_model_spec.rb │ └── activerecord_spec_helper.rb ├── support │ ├── parameterized_models.rb │ ├── activerecord │ │ ├── schema.rb │ │ └── friendship_examples.rb │ └── friend_examples.rb └── spec_helper.rb ├── LICENCE ├── amistad.gemspec ├── Rakefile ├── README.markdown └── Gemfile.lock /lib/amistad/version.rb: -------------------------------------------------------------------------------- 1 | module Amistad 2 | VERSION = "0.10.2" 3 | end 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | # Specify your gem's dependencies in amistad.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pkg/* 2 | *.gem 3 | .bundle 4 | spec/db/*.db 5 | nbproject 6 | .rvmrc 7 | .ruby-version 8 | .ruby-gemset 9 | .idea 10 | *.sw[a-z] 11 | .rspec 12 | database.yml 13 | -------------------------------------------------------------------------------- /spec/mongo_mapper/mongo_mapper_spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require 'mongo_mapper' 3 | 4 | MongoMapper.database = 'amistad_mongo_mapper_test' 5 | 6 | RSpec.configure do |config| 7 | config.before(:each) do 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/mongoid/mongoid_spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require 'mongoid' 3 | 4 | Mongoid.configure do |config| 5 | config.connect_to "amistad_mongoid_test" 6 | end 7 | 8 | RSpec.configure do |config| 9 | config.before(:each) do 10 | Mongoid.purge! 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/activerecord/friend_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + "/activerecord_spec_helper" 2 | 3 | describe "The friend model" do 4 | before(:all) do 5 | reload_environment 6 | User = Class.new(ActiveRecord::Base) 7 | 8 | end 9 | 10 | it_should_behave_like "friend with parameterized models" do 11 | let(:friend_model_param) { User } 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/activerecord/friendship_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + "/activerecord_spec_helper" 2 | 3 | describe 'Amistad friendship model' do 4 | 5 | before(:all) do 6 | reload_environment 7 | User = Class.new(ActiveRecord::Base) 8 | activate_amistad(User) 9 | create_users(User) 10 | end 11 | 12 | it_should_behave_like "the friendship model" 13 | end 14 | -------------------------------------------------------------------------------- /lib/amistad/friendship_model.rb: -------------------------------------------------------------------------------- 1 | module Amistad 2 | module FriendshipModel 3 | def self.included(receiver) 4 | if receiver.ancestors.map(&:to_s).include?("ActiveRecord::Base") 5 | receiver.class_exec do 6 | include Amistad::ActiveRecordFriendshipModel 7 | end 8 | else 9 | raise "Amistad only supports ActiveRecord and Mongoid" 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/mongoid/friend_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/mongoid_spec_helper' 2 | 3 | describe "The friend model" do 4 | before(:all) do 5 | reload_environment 6 | 7 | User = Class.new 8 | User.class_exec do 9 | include Mongoid::Document 10 | field :name 11 | end 12 | end 13 | 14 | it_should_behave_like "friend with parameterized models" do 15 | let(:friend_model_param) { User } 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/amistad/config.rb: -------------------------------------------------------------------------------- 1 | module Amistad 2 | class << self 3 | attr_accessor :friend_model 4 | 5 | def configure 6 | yield self 7 | end 8 | 9 | def friend_model 10 | @friend_model || 'User' 11 | end 12 | 13 | def friendship_model 14 | "#{self.friend_model}Friendship" 15 | end 16 | 17 | def friendship_class 18 | Amistad::Friendships.const_get(self.friendship_model) 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/mongo_mapper/friend_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/mongo_mapper_spec_helper' 2 | 3 | describe "The friend model" do 4 | before(:all) do 5 | reload_environment 6 | 7 | User = Class.new 8 | User.class_exec do 9 | include MongoMapper::Document 10 | key :name, String 11 | end 12 | end 13 | 14 | it_should_behave_like "friend with parameterized models" do 15 | let(:friend_model_param) { User } 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/amistad.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/concern' 2 | require 'active_support/dependencies/autoload' 3 | require 'amistad/config' 4 | 5 | module Amistad 6 | extend ActiveSupport::Autoload 7 | 8 | autoload :ActiveRecordFriendModel 9 | autoload :ActiveRecordFriendshipModel 10 | autoload :MongoFriendModel 11 | autoload :MongoidFriendModel 12 | autoload :MongoMapperFriendModel 13 | autoload :FriendshipModel 14 | autoload :FriendModel 15 | autoload :Friendships 16 | end 17 | -------------------------------------------------------------------------------- /lib/generators/amistad/install/templates/create_friendships.rb: -------------------------------------------------------------------------------- 1 | class CreateFriendships < ActiveRecord::Migration 2 | def self.up 3 | create_table :friendships do |t| 4 | t.integer :friendable_id 5 | t.integer :friend_id 6 | t.integer :blocker_id 7 | t.boolean :pending, :default => true 8 | end 9 | 10 | add_index :friendships, [:friendable_id, :friend_id], :unique => true 11 | end 12 | 13 | def self.down 14 | drop_table :friendships 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/activerecord/friend_custom_model_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + "/activerecord_spec_helper" 2 | 3 | describe 'Custom friend model' do 4 | before(:all) do 5 | reload_environment 6 | 7 | Amistad.configure do |config| 8 | config.friend_model = 'Profile' 9 | end 10 | 11 | Profile = Class.new(ActiveRecord::Base) 12 | end 13 | 14 | it_should_behave_like "friend with parameterized models" do 15 | let(:friend_model_param) { Profile } 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/activerecord/friendship_with_custom_friend_model_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + "/activerecord_spec_helper" 2 | 3 | describe 'Amistad friendship model whith custom friend model' do 4 | 5 | before(:all) do 6 | reload_environment 7 | 8 | Amistad.configure do |config| 9 | config.friend_model = 'Profile' 10 | end 11 | 12 | Profile = Class.new(ActiveRecord::Base) 13 | activate_amistad(Profile) 14 | create_users(Profile) 15 | end 16 | 17 | it_should_behave_like "the friendship model" 18 | end 19 | -------------------------------------------------------------------------------- /spec/activerecord/activerecord_spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'active_record' 3 | 4 | Dir[File.expand_path('../../support/activerecord/*.rb', __FILE__)].each{|f| require f } 5 | 6 | RSpec.configure do |config| 7 | config.before(:suite) do 8 | CreateSchema.suppress_messages{ CreateSchema.migrate(:up) } 9 | DatabaseCleaner.strategy = :transaction 10 | DatabaseCleaner.clean_with(:truncation) 11 | end 12 | 13 | config.before(:each) do 14 | DatabaseCleaner.start 15 | end 16 | 17 | config.after(:each) do 18 | DatabaseCleaner.clean 19 | end 20 | end 21 | 22 | -------------------------------------------------------------------------------- /spec/support/parameterized_models.rb: -------------------------------------------------------------------------------- 1 | shared_examples_for "friend with parameterized models" do 2 | context "When users are created after activating amistad" do 3 | before(:each) do 4 | activate_amistad(friend_model_param) 5 | create_users(friend_model_param) 6 | end 7 | 8 | it_should_behave_like "a friend model" 9 | end 10 | 11 | context "When users are created before activating amistad" do 12 | before(:each) do 13 | create_users(friend_model_param) 14 | activate_amistad(friend_model_param) 15 | end 16 | 17 | it_should_behave_like "a friend model" 18 | end 19 | end -------------------------------------------------------------------------------- /lib/amistad/friendships.rb: -------------------------------------------------------------------------------- 1 | module Amistad 2 | module Friendships 3 | if Object.const_defined? :ActiveRecord 4 | const_set Amistad.friendship_model, Class.new(ActiveRecord::Base) 5 | const_get(Amistad.friendship_model.to_sym).class_exec do 6 | include Amistad::FriendshipModel 7 | self.table_name = 'friendships' 8 | end 9 | elsif Object.const_defined? :Mongoid 10 | Friendship = Class.new 11 | Friendship.class_exec do 12 | include Mongoid::Document 13 | include Amistad::FriendshipModel 14 | end 15 | else 16 | raise "Amistad only supports ActiveRecord and Mongoid" 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/mongoid/friend_custom_model_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/mongoid_spec_helper' 2 | 3 | describe 'Custom friend model' do 4 | def reset_friendships 5 | %w(john jane david james peter mary victoria elisabeth).each do |var| 6 | eval "@#{var}.delete_all_friendships.should be_true" 7 | end 8 | end 9 | 10 | before(:all) do 11 | reload_environment 12 | 13 | Amistad.configure do |config| 14 | config.friend_model = 'Profile' 15 | end 16 | 17 | Profile = Class.new 18 | Profile.class_exec do 19 | include Mongoid::Document 20 | field :name 21 | end 22 | end 23 | 24 | it_should_behave_like "friend with parameterized models" do 25 | let(:friend_model_param) { Profile } 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/mongo_mapper/friend_custom_model_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/mongo_mapper_spec_helper' 2 | 3 | describe 'Custom friend model' do 4 | def reset_friendships 5 | %w(john jane david james peter mary victoria elisabeth).each do |var| 6 | eval "@#{var}.delete_all_friendships.should be_true" 7 | end 8 | end 9 | 10 | before(:all) do 11 | reload_environment 12 | 13 | Amistad.configure do |config| 14 | config.friend_model = 'Profile' 15 | end 16 | 17 | Profile = Class.new 18 | Profile.class_exec do 19 | include MongoMapper::Document 20 | key :name, String 21 | end 22 | end 23 | 24 | it_should_behave_like "friend with parameterized models" do 25 | let(:friend_model_param) { Profile } 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/generators/amistad/install/install_generator.rb: -------------------------------------------------------------------------------- 1 | require 'rails/generators' 2 | require 'rails/generators/migration' 3 | 4 | module Amistad 5 | module Generators 6 | class InstallGenerator < Rails::Generators::Base 7 | include Rails::Generators::Migration 8 | 9 | def self.source_root 10 | @source_root ||= File.expand_path(File.join(File.dirname(__FILE__), 'templates')) 11 | end 12 | 13 | def self.next_migration_number 14 | Time.now.utc.strftime("%Y%m%d%H%M%S") 15 | end 16 | 17 | desc "This generator creates a friendship model and its migration file" 18 | def create_friendship_model_files 19 | template 'create_friendships.rb', "db/migrate/#{self.class.next_migration_number}_create_friendships.rb" 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/amistad/friend_model.rb: -------------------------------------------------------------------------------- 1 | module Amistad 2 | module FriendModel 3 | def self.included(receiver) 4 | if receiver.ancestors.map(&:to_s).include?("ActiveRecord::Base") 5 | receiver.class_exec do 6 | include Amistad::ActiveRecordFriendModel 7 | end 8 | elsif receiver.ancestors.map(&:to_s).include?("Mongoid::Document") 9 | receiver.class_exec do 10 | include Amistad::MongoidFriendModel 11 | include Amistad::MongoFriendModel 12 | end 13 | elsif receiver.ancestors.map(&:to_s).include?("MongoMapper::Document") 14 | receiver.class_exec do 15 | include Amistad::MongoMapperFriendModel 16 | include Amistad::MongoFriendModel 17 | end 18 | else 19 | raise "Amistad only supports ActiveRecord, Mongoid and MongoMapper" 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/amistad/mongoid_friend_model.rb: -------------------------------------------------------------------------------- 1 | module Amistad 2 | module MongoidFriendModel 3 | extend ActiveSupport::Concern 4 | 5 | included do 6 | field :friend_ids, 7 | :type => Array, 8 | :default => [] 9 | 10 | field :inverse_friend_ids, 11 | :type => Array, 12 | :default => [] 13 | 14 | field :pending_friend_ids, 15 | :type => Array, 16 | :default => [] 17 | 18 | field :pending_inverse_friend_ids, 19 | :type => Array, 20 | :default => [] 21 | 22 | field :blocked_friend_ids, 23 | :type => Array, 24 | :default => [] 25 | 26 | field :blocked_inverse_friend_ids, 27 | :type => Array, 28 | :default => [] 29 | 30 | field :blocked_pending_friend_ids, 31 | :type => Array, 32 | :default => [] 33 | 34 | field :blocked_pending_inverse_friend_ids, 35 | :type => Array, 36 | :default => [] 37 | 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 Rawane ZOSSOU 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rspec' 2 | require 'active_record' 3 | require 'mongoid' 4 | require 'ap' 5 | require 'database_cleaner' 6 | Dir["#{File.dirname(__FILE__)}/support/*.rb"].each {|f| require f} 7 | 8 | def create_users(friend_model) 9 | friend_model.delete_all 10 | %w(John Jane David James Peter Mary Victoria Elisabeth).each do |name| 11 | instance_variable_set("@#{name.downcase}".to_sym, friend_model.create{ |fm| fm.name = name}) 12 | end 13 | end 14 | 15 | def activate_amistad(friend_model) 16 | friend_model.class_exec do 17 | include Amistad::FriendModel 18 | end 19 | end 20 | 21 | def reload_environment 22 | # ensure that the gem will be always required 23 | $".grep(/.*lib\/amistad.*/).each do |file| 24 | $".delete(file) 25 | end 26 | 27 | # delete all the classes 28 | if Object.const_defined?(:Amistad) 29 | Amistad.constants.each do |constant| 30 | Amistad.send(:remove_const, constant) 31 | end 32 | 33 | Object.send(:remove_const, :Amistad) 34 | end 35 | 36 | Object.send(:remove_const, :User) if Object.const_defined?(:User) 37 | Object.send(:remove_const, :Profile) if Object.const_defined?(:Profile) 38 | 39 | # require the gem 40 | require "amistad" 41 | end 42 | -------------------------------------------------------------------------------- /spec/support/activerecord/schema.rb: -------------------------------------------------------------------------------- 1 | def db_config(type) 2 | YAML.load_file(File.expand_path('../database.yml', __FILE__))[type] 3 | end 4 | 5 | def connect_server(type) 6 | config = db_config type 7 | database = config['database'] 8 | 9 | ActiveRecord::Base.establish_connection config.merge('database' => (type == 'postgresql' ? 'postgres' : nil)) 10 | ActiveRecord::Base.connection.recreate_database(database) 11 | ActiveRecord::Base.establish_connection(config) 12 | end 13 | 14 | case ENV['RDBM'] 15 | when 'mysql' then connect_server 'mysql' 16 | when 'postgresql' then connect_server 'postgresql' 17 | else 18 | ActiveRecord::Base.establish_connection db_config('sqlite') 19 | end 20 | 21 | class CreateSchema < ActiveRecord::Migration 22 | def self.up 23 | create_table :users, :force => true do |t| 24 | t.string :name, :null => false 25 | end 26 | 27 | create_table :profiles, :force => true do |t| 28 | t.string :name, :null => false 29 | end 30 | 31 | create_table :friendships, :force => true do |t| 32 | t.integer :friendable_id 33 | t.integer :friend_id 34 | t.integer :blocker_id 35 | t.boolean :pending, :default => true 36 | end 37 | 38 | add_index :friendships, [:friendable_id, :friend_id], :unique => true 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/amistad/mongo_mapper_friend_model.rb: -------------------------------------------------------------------------------- 1 | module Amistad 2 | module MongoMapperFriendModel 3 | extend ActiveSupport::Concern 4 | 5 | included do 6 | key :friend_ids, 7 | Array, 8 | :default => [] 9 | 10 | key :inverse_friend_ids, 11 | Array, 12 | :default => [] 13 | 14 | key :pending_friend_ids, 15 | Array, 16 | :default => [] 17 | 18 | key :pending_inverse_friend_ids, 19 | Array, 20 | :default => [] 21 | 22 | key :blocked_friend_ids, 23 | Array, 24 | :default => [] 25 | 26 | key :blocked_inverse_friend_ids, 27 | Array, 28 | :default => [] 29 | 30 | key :blocked_pending_friend_ids, 31 | Array, 32 | :default => [] 33 | 34 | key :blocked_pending_inverse_friend_ids, 35 | Array, 36 | :default => [] 37 | 38 | %w(friend_ids inverse_friend_ids pending_friend_ids pending_inverse_friend_ids blocked_friend_ids blocked_inverse_friend_ids blocked_pending_friend_ids blocked_pending_inverse_friend_ids).each do |attribute| 39 | define_method(attribute.to_sym) do 40 | value = read_attribute(attribute) 41 | write_attribute(attribute, value = []) if value.nil? 42 | value 43 | end 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /amistad.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "amistad/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "amistad" 7 | s.version = Amistad::VERSION 8 | s.platform = Gem::Platform::RUBY 9 | s.authors = ["Rawane ZOSSOU"] 10 | s.email = ["dev@raw1z.fr"] 11 | s.homepage = "https://github.com/raw1z/amistad/wiki" 12 | s.summary = %q{Adds friendships management into a rails 3.0 application} 13 | s.description = %q{Extends your user model with friendships management methods} 14 | 15 | s.rubyforge_project = "amistad" 16 | 17 | s.add_development_dependency "bundler" 18 | s.add_development_dependency "rake" 19 | s.add_development_dependency "rspec", "~> 3.1.0" 20 | s.add_development_dependency "activerecord", ">= 4.0.0" 21 | s.add_development_dependency "mysql2", ">= 0.3.16" 22 | s.add_development_dependency "pg", ">= 0.17.1" 23 | s.add_development_dependency "database_cleaner" 24 | s.add_development_dependency "sqlite3", ">= 1.3.9" 25 | s.add_development_dependency "mongoid", ">= 4.0.0" 26 | s.add_development_dependency "bson_ext" 27 | s.add_development_dependency "fuubar" 28 | s.add_development_dependency "awesome_print" 29 | s.add_development_dependency "mongo_mapper" 30 | 31 | s.files = `git ls-files`.split("\n") 32 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 33 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 34 | s.require_paths = ["lib"] 35 | end 36 | -------------------------------------------------------------------------------- /lib/amistad/active_record_friendship_model.rb: -------------------------------------------------------------------------------- 1 | module Amistad 2 | module ActiveRecordFriendshipModel 3 | extend ActiveSupport::Concern 4 | 5 | included do 6 | belongs_to :friendable, 7 | :class_name => Amistad.friend_model, 8 | :foreign_key => "friendable_id" 9 | 10 | belongs_to :friend, 11 | :class_name => Amistad.friend_model, 12 | :foreign_key => "friend_id" 13 | 14 | belongs_to :blocker, 15 | :class_name => Amistad.friend_model, 16 | :foreign_key => "blocker_id" 17 | 18 | validates_presence_of :friendable_id, :friend_id 19 | validates_uniqueness_of :friend_id, :scope => :friendable_id 20 | end 21 | 22 | # returns true if a friendship has been approved, else false 23 | def approved? 24 | !self.pending 25 | end 26 | 27 | # returns true if a friendship has not been approved, else false 28 | def pending? 29 | self.pending 30 | end 31 | 32 | # returns true if a friendship has been blocked, else false 33 | def blocked? 34 | self.blocker_id.present? 35 | end 36 | 37 | # returns true if a friendship has not beed blocked, else false 38 | def active? 39 | self.blocker_id.nil? 40 | end 41 | 42 | # returns true if a friendship can be blocked by given friendable 43 | def can_block?(friendable) 44 | active? && (approved? || (pending? && self.friend_id == friendable.id && friendable.class.to_s == Amistad.friend_model)) 45 | end 46 | 47 | # returns true if a friendship can be unblocked by given friendable 48 | def can_unblock?(friendable) 49 | blocked? && self.blocker_id == friendable.id && friendable.class.to_s == Amistad.friend_model 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | Bundler::GemHelper.install_tasks 3 | 4 | require 'rspec' 5 | require 'rspec/core/rake_task' 6 | 7 | namespace :spec do 8 | desc "Run Rspec tests for ActiveRecord (with sqlite as RDBM)" 9 | RSpec::Core::RakeTask.new(:activerecord) do |t| 10 | t.pattern = "./spec/activerecord/**/*_spec.rb" 11 | t.rspec_opts = "--format Fuubar" 12 | end 13 | 14 | desc "Run Rspec tests for Mongoid" 15 | RSpec::Core::RakeTask.new(:mongoid) do |t| 16 | t.pattern = "./spec/mongoid/**/*_spec.rb" 17 | t.rspec_opts = "--format Fuubar" 18 | end 19 | 20 | desc "Run Rspec tests for MongoMapper" 21 | RSpec::Core::RakeTask.new(:mongo_mapper) do |t| 22 | t.pattern = "./spec/mongo_mapper/**/*_spec.rb" 23 | t.rspec_opts = "--format Fuubar" 24 | end 25 | 26 | namespace :activerecord do 27 | desc "run activerecord tests on a sqlite database" 28 | task :sqlite do 29 | ENV['RDBM'] = 'sqlite' 30 | Rake::Task['spec:activerecord'].reenable 31 | Rake::Task['spec:activerecord'].invoke 32 | end 33 | 34 | desc "run activerecord tests on a mysql database" 35 | task :mysql do 36 | ENV['RDBM'] = 'mysql' 37 | Rake::Task['spec:activerecord'].reenable 38 | Rake::Task['spec:activerecord'].invoke 39 | end 40 | 41 | desc "run activerecord tests on a postgresql database" 42 | task :postgresql do 43 | ENV['RDBM'] = 'postgresql' 44 | Rake::Task['spec:activerecord'].reenable 45 | Rake::Task['spec:activerecord'].invoke 46 | end 47 | end 48 | end 49 | 50 | task :default do 51 | Rake::Task['spec:activerecord:sqlite'].invoke 52 | Rake::Task['spec:activerecord:mysql'].invoke 53 | Rake::Task['spec:activerecord:postgresql'].invoke 54 | Rake::Task['spec:mongoid'].invoke 55 | Rake::Task['spec:mongo_mapper'].invoke 56 | end 57 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # amistad # 2 | 3 | Amistad adds friendships management into a rails application. it supports ActiveRecord, Mongoid and MongoMapper. 4 | 5 | ## Installation ## 6 | 7 | Add the following line in your Gemfile: 8 | 9 | gem 'amistad' 10 | 11 | Then run: 12 | 13 | bundle install 14 | 15 | ## Usage ## 16 | 17 | Refer to the [wiki pages](https://github.com/raw1z/amistad/wiki) for usage and friendships management documentation. 18 | 19 | ## Testing ## 20 | 21 | There are rake tasks available which allow you to run the activerecord tests for three rdbms: 22 | 23 | rake spec:activerecord:sqlite 24 | rake spec:activerecord:mysql 25 | rake spec:activerecord:postgresql 26 | 27 | In order to run these tasks you need to create a confiuration file for the databases connections: 28 | 29 | spec/support/activerecord/database.yml 30 | 31 | sqlite: 32 | adapter: "sqlite3" 33 | database: ":memory:" 34 | 35 | mysql: 36 | adapter: mysql2 37 | encoding: utf8 38 | database: 39 | username: 40 | password: 41 | 42 | postgresql: 43 | adapter: postgresql 44 | encoding: unicode 45 | database: 46 | username: 47 | password: 48 | 49 | Of course there are some tasks for running mongodb orms based tests: 50 | 51 | rake spec:mongoid 52 | rake spec:mongo_mapper 53 | 54 | The default rake tasks runs the ActiveRecord tests for the three rdbms followed by the Mongoid tests. 55 | 56 | ## Contributors ## 57 | 58 | * David Czarnecki : block friendships (and many other improvements) 59 | * Adrian Dulić : unblock friendships (and many other improvements) 60 | 61 | ## Note on Patches/Pull Requests ## 62 | 63 | * Fork the project. 64 | * Make your feature addition or bug fix. 65 | * Add tests for it. This is important so I don't break it in a future version unintentionally. 66 | * Commit, do not mess with rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull) 67 | * Send me a pull request. Bonus points for topic branches. 68 | 69 | ## Copyright ## 70 | 71 | Copyright © 2010 Rawane ZOSSOU. See LICENSE for details. 72 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | amistad (0.10.2) 5 | 6 | GEM 7 | remote: http://rubygems.org/ 8 | specs: 9 | activemodel (4.1.6) 10 | activesupport (= 4.1.6) 11 | builder (~> 3.1) 12 | activerecord (4.1.6) 13 | activemodel (= 4.1.6) 14 | activesupport (= 4.1.6) 15 | arel (~> 5.0.0) 16 | activesupport (4.1.6) 17 | i18n (~> 0.6, >= 0.6.9) 18 | json (~> 1.7, >= 1.7.7) 19 | minitest (~> 5.1) 20 | thread_safe (~> 0.1) 21 | tzinfo (~> 1.1) 22 | arel (5.0.1.20140414130214) 23 | awesome_print (1.2.0) 24 | bson (2.2.0) 25 | bson_ext (1.5.1) 26 | builder (3.2.2) 27 | connection_pool (2.0.0) 28 | database_cleaner (1.3.0) 29 | diff-lcs (1.2.5) 30 | fuubar (2.0.0) 31 | rspec (~> 3.0) 32 | ruby-progressbar (~> 1.4) 33 | i18n (0.6.11) 34 | jnunemaker-validatable (1.8.4) 35 | activesupport (>= 2.3.4) 36 | json (1.8.1) 37 | minitest (5.4.1) 38 | mongo (1.3.1) 39 | bson (>= 1.3.1) 40 | mongo_mapper (0.8.6) 41 | activesupport (>= 2.3.4) 42 | jnunemaker-validatable (~> 1.8.4) 43 | plucky (~> 0.3.6) 44 | mongoid (4.0.0) 45 | activemodel (~> 4.0) 46 | moped (~> 2.0.0) 47 | origin (~> 2.1) 48 | tzinfo (>= 0.3.37) 49 | moped (2.0.0) 50 | bson (~> 2.2) 51 | connection_pool (~> 2.0) 52 | optionable (~> 0.2.0) 53 | mysql2 (0.3.16) 54 | optionable (0.2.0) 55 | origin (2.1.1) 56 | pg (0.17.1) 57 | plucky (0.3.8) 58 | mongo (~> 1.3) 59 | rake (10.3.2) 60 | rspec (3.1.0) 61 | rspec-core (~> 3.1.0) 62 | rspec-expectations (~> 3.1.0) 63 | rspec-mocks (~> 3.1.0) 64 | rspec-core (3.1.4) 65 | rspec-support (~> 3.1.0) 66 | rspec-expectations (3.1.1) 67 | diff-lcs (>= 1.2.0, < 2.0) 68 | rspec-support (~> 3.1.0) 69 | rspec-mocks (3.1.1) 70 | rspec-support (~> 3.1.0) 71 | rspec-support (3.1.0) 72 | ruby-progressbar (1.6.0) 73 | sqlite3 (1.3.9) 74 | thread_safe (0.3.4) 75 | tzinfo (1.2.2) 76 | thread_safe (~> 0.1) 77 | 78 | PLATFORMS 79 | ruby 80 | 81 | DEPENDENCIES 82 | activerecord (>= 4.0.0) 83 | amistad! 84 | awesome_print 85 | bson_ext 86 | bundler 87 | database_cleaner 88 | fuubar 89 | mongo_mapper 90 | mongoid (>= 4.0.0) 91 | mysql2 (>= 0.3.16) 92 | pg (>= 0.17.1) 93 | rake 94 | rspec (~> 3.1.0) 95 | sqlite3 (>= 1.3.9) 96 | -------------------------------------------------------------------------------- /spec/support/activerecord/friendship_examples.rb: -------------------------------------------------------------------------------- 1 | shared_examples_for "the friendship model" do 2 | it "should validate presence of the user's id and the friend's id" do 3 | friendship = Amistad.friendship_class.new 4 | expect(friendship.valid?).to eq(false) 5 | expect(friendship.errors).to include(:friendable_id) 6 | expect(friendship.errors).to include(:friend_id) 7 | expect(friendship.errors.size).to eq(2) 8 | end 9 | 10 | context "when creating friendship" do 11 | before do 12 | @jane.invite(@david) 13 | @friendship = Amistad.friendship_class.first 14 | end 15 | 16 | it "should be pending" do 17 | expect(@friendship.pending?).to eq(true) 18 | end 19 | 20 | it "should not be approved" do 21 | expect(@friendship.approved?).to eq(false) 22 | end 23 | 24 | it "should be active" do 25 | expect(@friendship.active?).to eq(true) 26 | end 27 | 28 | it "should not be blocked" do 29 | expect(@friendship.blocked?).to eq(false) 30 | end 31 | 32 | it "should be available to block only by invited user" do 33 | expect(@friendship.can_block?(@david)).to eq(true) 34 | expect(@friendship.can_block?(@jane)).to eq(false) 35 | end 36 | 37 | it "should not be available to unblock" do 38 | expect(@friendship.can_unblock?(@jane)).to eq(false) 39 | expect(@friendship.can_unblock?(@david)).to eq(false) 40 | end 41 | end 42 | 43 | context "when approving friendship" do 44 | before do 45 | @jane.invite(@david) 46 | @david.approve(@jane) 47 | @friendship = Amistad.friendship_class.first 48 | end 49 | 50 | it "should be approved" do 51 | expect(@friendship.approved?).to eq(true) 52 | end 53 | 54 | it "should not be pending" do 55 | expect(@friendship.pending?).to eq(false) 56 | end 57 | 58 | it "should be active" do 59 | expect(@friendship.active?).to eq(true) 60 | end 61 | 62 | it "should not be blocked" do 63 | expect(@friendship.blocked?).to eq(false) 64 | end 65 | 66 | it "should be available to block by both users" do 67 | expect(@friendship.can_block?(@jane)).to eq(true) 68 | expect(@friendship.can_block?(@david)).to eq(true) 69 | end 70 | 71 | it "should not be availabel to unblock" do 72 | expect(@friendship.can_unblock?(@jane)).to eq(false) 73 | expect(@friendship.can_unblock?(@david)).to eq(false) 74 | end 75 | end 76 | 77 | context "when blocking friendship" do 78 | before do 79 | @jane.invite(@david) 80 | @david.block(@jane) 81 | @friendship = Amistad.friendship_class.first 82 | end 83 | 84 | it "should not be approved" do 85 | expect(@friendship.approved?).to eq(false) 86 | end 87 | 88 | it "should be pending" do 89 | expect(@friendship.pending?).to eq(true) 90 | end 91 | 92 | it "should not be active" do 93 | expect(@friendship.active?).to eq(false) 94 | end 95 | 96 | it "should be blocked" do 97 | expect(@friendship.blocked?).to eq(true) 98 | end 99 | 100 | it "should not be available to block" do 101 | expect(@friendship.can_block?(@jane)).to eq(false) 102 | expect(@friendship.can_block?(@david)).to eq(false) 103 | end 104 | 105 | it "should be available to unblock only by user who blocked it" do 106 | expect(@friendship.can_unblock?(@david)).to eq(true) 107 | expect(@friendship.can_unblock?(@jane)).to eq(false) 108 | end 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /lib/amistad/mongo_friend_model.rb: -------------------------------------------------------------------------------- 1 | module Amistad 2 | module MongoFriendModel 3 | # suggest a user to become a friend. If the operation succeeds, the method returns true, else false 4 | def invite(user) 5 | return false if friendshiped_with?(user) or user == self or blocked?(user) 6 | pending_friend_ids << user.id 7 | user.pending_inverse_friend_ids << self.id 8 | self.save && user.save 9 | end 10 | 11 | # approve a friendship invitation. If the operation succeeds, the method returns true, else false 12 | def approve(user) 13 | return false unless pending_inverse_friend_ids.include?(user.id) && user.pending_friend_ids.include?(self.id) 14 | pending_inverse_friend_ids.delete(user.id) 15 | user.pending_friend_ids.delete(self.id) 16 | inverse_friend_ids << user.id 17 | user.friend_ids << self.id 18 | self.save && user.save 19 | end 20 | 21 | # returns the list of approved friends 22 | def friends 23 | self.invited + self.invited_by 24 | end 25 | 26 | # total # of invited and invited_by without association loading 27 | def total_friends 28 | (friend_ids + inverse_friend_ids).count 29 | end 30 | 31 | # return the list of invited friends 32 | def invited 33 | self.class.find(friend_ids) 34 | end 35 | 36 | # return the list of friends who invited 37 | def invited_by 38 | self.class.find(inverse_friend_ids) 39 | end 40 | 41 | # return the list of pending invited friends 42 | def pending_invited 43 | self.class.find(pending_friend_ids) 44 | end 45 | 46 | # return the list of pending friends who invited 47 | def pending_invited_by 48 | self.class.find(pending_inverse_friend_ids) 49 | end 50 | 51 | # return the list of the ones among its friends which are also friend with the given use 52 | def common_friends_with(user) 53 | self.friends & user.friends 54 | end 55 | 56 | # checks if a user is a friend 57 | def friend_with?(user) 58 | return false if user == self 59 | (friend_ids + inverse_friend_ids).include?(user.id) 60 | end 61 | 62 | # checks if a current user is connected to given user 63 | def connected_with?(user) 64 | friendshiped_with?(user) 65 | end 66 | 67 | # checks if a current user received invitation from given user 68 | def invited_by?(user) 69 | user.friend_ids.include?(self.id) or user.pending_friend_ids.include?(self.id) 70 | end 71 | 72 | # checks if a current user invited given user 73 | def invited?(user) 74 | self.friend_ids.include?(user.id) or self.pending_friend_ids.include?(user.id) 75 | end 76 | 77 | # deletes a friendship 78 | def remove_friendship(user) 79 | friend_ids.delete(user.id) 80 | user.inverse_friend_ids.delete(self.id) 81 | inverse_friend_ids.delete(user.id) 82 | user.friend_ids.delete(self.id) 83 | pending_friend_ids.delete(user.id) 84 | user.pending_inverse_friend_ids.delete(self.id) 85 | pending_inverse_friend_ids.delete(user.id) 86 | user.pending_friend_ids.delete(self.id) 87 | self.save && user.save 88 | end 89 | 90 | # blocks a friendship 91 | def block(user) 92 | if inverse_friend_ids.include?(user.id) 93 | inverse_friend_ids.delete(user.id) 94 | user.friend_ids.delete(self.id) 95 | blocked_inverse_friend_ids << user.id 96 | elsif pending_inverse_friend_ids.include?(user.id) 97 | pending_inverse_friend_ids.delete(user.id) 98 | user.pending_friend_ids.delete(self.id) 99 | blocked_pending_inverse_friend_ids << user.id 100 | elsif friend_ids.include?(user.id) 101 | friend_ids.delete(user.id) 102 | user.inverse_friend_ids.delete(user.id) 103 | blocked_friend_ids << user.id 104 | else 105 | return false 106 | end 107 | 108 | self.save 109 | end 110 | 111 | # unblocks a friendship 112 | def unblock(user) 113 | if blocked_inverse_friend_ids.include?(user.id) 114 | blocked_inverse_friend_ids.delete(user.id) 115 | user.blocked_friend_ids.delete(self.id) 116 | inverse_friend_ids << user.id 117 | user.friend_ids << self.id 118 | elsif blocked_pending_inverse_friend_ids.include?(user.id) 119 | blocked_pending_inverse_friend_ids.delete(user.id) 120 | pending_inverse_friend_ids << user.id 121 | user.pending_friend_ids << self.id 122 | elsif blocked_friend_ids.include?(user.id) 123 | blocked_friend_ids.delete(user.id) 124 | user.blocked_inverse_friend_ids.delete(self.id) 125 | friend_ids << user.id 126 | user.inverse_friend_ids << self.id 127 | else 128 | return false 129 | end 130 | 131 | self.save && user.save 132 | end 133 | 134 | # returns the list of blocked friends 135 | def blocked 136 | blocked_ids = blocked_friend_ids + blocked_inverse_friend_ids + blocked_pending_inverse_friend_ids 137 | self.class.find(blocked_ids) 138 | end 139 | 140 | # total # of blockades and blockedes_by without association loading 141 | def total_blocked 142 | (blocked_friend_ids + blocked_inverse_friend_ids + blocked_pending_inverse_friend_ids).count 143 | end 144 | 145 | # checks if a user is blocked 146 | def blocked?(user) 147 | (blocked_friend_ids + blocked_inverse_friend_ids + blocked_pending_inverse_friend_ids).include?(user.id) or user.blocked_pending_inverse_friend_ids.include?(self.id) 148 | end 149 | 150 | # check if any friendship exists with another user 151 | def friendshiped_with?(user) 152 | (friend_ids + inverse_friend_ids + pending_friend_ids + pending_inverse_friend_ids + blocked_friend_ids).include?(user.id) 153 | end 154 | 155 | # deletes all the friendships 156 | def delete_all_friendships 157 | friend_ids.clear 158 | inverse_friend_ids.clear 159 | pending_friend_ids.clear 160 | pending_inverse_friend_ids.clear 161 | blocked_friend_ids.clear 162 | blocked_inverse_friend_ids.clear 163 | blocked_pending_friend_ids.clear 164 | blocked_pending_inverse_friend_ids.clear 165 | self.save 166 | end 167 | end 168 | end 169 | -------------------------------------------------------------------------------- /lib/amistad/active_record_friend_model.rb: -------------------------------------------------------------------------------- 1 | module Amistad 2 | module ActiveRecordFriendModel 3 | extend ActiveSupport::Concern 4 | 5 | included do 6 | ##################################################################################### 7 | # friendships 8 | ##################################################################################### 9 | has_many :friendships, 10 | :class_name => "Amistad::Friendships::#{Amistad.friendship_model}", 11 | :foreign_key => "friendable_id" 12 | 13 | 14 | has_many :pending_invited, -> { where(:'friendships.pending' => true, :'friendships.blocker_id' => nil) }, :through => :friendships, :source => :friend 15 | has_many :invited, -> { where(:'friendships.pending' => false, :'friendships.blocker_id' => nil) }, :through => :friendships, :source => :friend 16 | 17 | ##################################################################################### 18 | # inverse friendships 19 | ##################################################################################### 20 | has_many :inverse_friendships, 21 | :class_name => "Amistad::Friendships::#{Amistad.friendship_model}", 22 | :foreign_key => "friend_id" 23 | 24 | has_many :pending_invited_by, -> { where(:'friendships.pending' => true, :'friendships.blocker_id' => nil) }, :through => :inverse_friendships, :source => :friendable 25 | has_many :invited_by, -> { where(:'friendships.pending' => false, :'friendships.blocker_id' => nil) }, :through => :inverse_friendships, :source => :friendable 26 | 27 | ##################################################################################### 28 | # blocked friendships 29 | ##################################################################################### 30 | has_many :blocked_friendships, 31 | :class_name => "Amistad::Friendships::#{Amistad.friendship_model}", 32 | :foreign_key => "blocker_id" 33 | 34 | has_many :blockades, -> {where("friend_id <> blocker_id")}, :through => :blocked_friendships, :source => :friend 35 | has_many :blockades_by, -> {where("friendable_id <> blocker_id")} , :through => :blocked_friendships, :source => :friendable 36 | 37 | end 38 | 39 | # suggest a user to become a friend. If the operation succeeds, the method returns true, else false 40 | def invite(user) 41 | return false if user == self || find_any_friendship_with(user) 42 | Amistad.friendship_class.new{ |f| f.friendable = self ; f.friend = user }.save 43 | end 44 | 45 | # approve a friendship invitation. If the operation succeeds, the method returns true, else false 46 | def approve(user) 47 | friendship = find_any_friendship_with(user) 48 | return false if friendship.nil? || invited?(user) 49 | friendship.update_attribute(:pending, false) 50 | end 51 | 52 | # deletes a friendship 53 | def remove_friendship(user) 54 | friendship = find_any_friendship_with(user) 55 | return false if friendship.nil? 56 | friendship.destroy 57 | self.reload && user.reload if friendship.destroyed? 58 | true 59 | end 60 | 61 | # returns the list of approved friends 62 | def friends 63 | friendship_model = Amistad::Friendships.const_get(:"#{Amistad.friendship_model}") 64 | 65 | approved_friendship = friendship_model.where(friendable_id: id, pending: false, blocker_id: nil).select(:friend_id).to_sql 66 | approved_inverse_friendship = friendship_model.where(friend_id: id, pending: false, blocker_id: nil).select(:friendable_id).to_sql 67 | 68 | self.class.where("id in (#{approved_friendship}) OR id in (#{approved_inverse_friendship})") 69 | end 70 | 71 | # total # of invited and invited_by without association loading 72 | def total_friends 73 | self.invited(false).count + self.invited_by(false).count 74 | end 75 | 76 | # blocks a friendship 77 | def block(user) 78 | friendship = find_any_friendship_with(user) 79 | return false if friendship.nil? || !friendship.can_block?(self) 80 | friendship.update_attribute(:blocker, self) 81 | end 82 | 83 | # unblocks a friendship 84 | def unblock(user) 85 | friendship = find_any_friendship_with(user) 86 | return false if friendship.nil? || !friendship.can_unblock?(self) 87 | friendship.update_attribute(:blocker, nil) 88 | end 89 | 90 | # returns the list of blocked friends 91 | def blocked 92 | self.reload 93 | self.blockades + self.blockades_by 94 | end 95 | 96 | # total # of blockades and blockedes_by without association loading 97 | def total_blocked 98 | self.blockades(false).count + self.blockades_by(false).count 99 | end 100 | 101 | # checks if a user is blocked 102 | def blocked?(user) 103 | blocked.include?(user) 104 | end 105 | 106 | # checks if a user is a friend 107 | def friend_with?(user) 108 | friends.include?(user) 109 | end 110 | 111 | # checks if a current user is connected to given user 112 | def connected_with?(user) 113 | find_any_friendship_with(user).present? 114 | end 115 | 116 | # checks if a current user received invitation from given user 117 | def invited_by?(user) 118 | friendship = find_any_friendship_with(user) 119 | return false if friendship.nil? 120 | friendship.friendable_id == user.id 121 | end 122 | 123 | # checks if a current user invited given user 124 | def invited?(user) 125 | friendship = find_any_friendship_with(user) 126 | return false if friendship.nil? 127 | friendship.friend_id == user.id 128 | end 129 | 130 | # return the list of the ones among its friends which are also friend with the given use 131 | def common_friends_with(user) 132 | self.friends & user.friends 133 | end 134 | 135 | # returns friendship with given user or nil 136 | def find_any_friendship_with(user) 137 | friendship = Amistad.friendship_class.where(:friendable_id => self.id, :friend_id => user.id).first 138 | if friendship.nil? 139 | friendship = Amistad.friendship_class.where(:friendable_id => user.id, :friend_id => self.id).first 140 | end 141 | friendship 142 | end 143 | end 144 | end 145 | -------------------------------------------------------------------------------- /spec/support/friend_examples.rb: -------------------------------------------------------------------------------- 1 | shared_examples_for "a friend model" do 2 | context "when creating friendships" do 3 | it "should invite other users to friends" do 4 | expect(@john.invite(@jane)).to eq(true) 5 | expect(@victoria.invite(@john)).to eq(true) 6 | end 7 | 8 | it "should approve only friendships requested by other users" do 9 | expect(@john.invite(@jane)).to eq(true) 10 | expect(@jane.approve(@john)).to eq(true) 11 | expect(@victoria.invite(@john)).to eq(true) 12 | expect(@john.approve(@victoria)).to eq(true) 13 | end 14 | 15 | it "should not invite an already invited user" do 16 | expect(@john.invite(@jane)).to eq(true) 17 | expect(@john.invite(@jane)).to eq(false) 18 | expect(@jane.invite(@john)).to eq(false) 19 | end 20 | 21 | it "should not invite an already approved user" do 22 | expect(@john.invite(@jane)).to eq(true) 23 | expect(@jane.approve(@john)).to eq(true) 24 | expect(@jane.invite(@john)).to eq(false) 25 | expect(@john.invite(@jane)).to eq(false) 26 | end 27 | 28 | it "should not invite an already blocked user" do 29 | expect(@john.invite(@jane)).to eq(true) 30 | expect(@jane.block(@john)).to eq(true) 31 | expect(@jane.invite(@john)).to eq(false) 32 | expect(@john.invite(@jane)).to eq(false) 33 | end 34 | 35 | it "should not approve a self requested friendship" do 36 | expect(@john.invite(@jane)).to eq(true) 37 | expect(@john.approve(@jane)).to eq(false) 38 | expect(@victoria.invite(@john)).to eq(true) 39 | expect(@victoria.approve(@john)).to eq(false) 40 | end 41 | 42 | it "should not create a friendship with himself" do 43 | expect(@john.invite(@john)).to eq(false) 44 | end 45 | 46 | it "should not approve a non-existent friendship" do 47 | expect(@peter.approve(@john)).to eq(false) 48 | end 49 | end 50 | 51 | context "when listing friendships" do 52 | before(:each) do 53 | expect(@john.invite(@jane)).to eq(true) 54 | expect(@peter.invite(@john)).to eq(true) 55 | expect(@john.invite(@james)).to eq(true) 56 | expect(@james.approve(@john)).to eq(true) 57 | expect(@mary.invite(@john)).to eq(true) 58 | expect(@john.approve(@mary)).to eq(true) 59 | end 60 | 61 | it "should list all the friends" do 62 | expect(@john.friends).to match_array([@mary, @james]) 63 | end 64 | 65 | it "should not list non-friended users" do 66 | expect(@victoria.friends).to be_empty 67 | expect(@john.friends).to match_array([@mary, @james]) 68 | expect(@john.friends).to_not include(@peter) 69 | expect(@john.friends).to_not include(@victoria) 70 | end 71 | 72 | it "should list the friends who invited him" do 73 | expect(@john.invited_by).to eq([@mary]) 74 | end 75 | 76 | it "should list the friends who were invited by him" do 77 | expect(@john.invited).to eq([@james]) 78 | end 79 | 80 | it "should list the pending friends who invited him" do 81 | expect(@john.pending_invited_by).to eq([@peter]) 82 | end 83 | 84 | it "should list the pending friends who were invited by him" do 85 | expect(@john.pending_invited).to eq([@jane]) 86 | end 87 | 88 | it "should list the friends he has in common with another user" do 89 | expect(@james.common_friends_with(@mary)).to eq([@john]) 90 | end 91 | 92 | it "should not list the friends he does not have in common" do 93 | expect(@john.common_friends_with(@mary).count).to eq(0) 94 | expect(@john.common_friends_with(@mary)).to_not include(@james) 95 | expect(@john.common_friends_with(@peter).count).to eq(0) 96 | expect(@john.common_friends_with(@peter)).to_not include(@jane) 97 | end 98 | 99 | it "should check if a user is a friend" do 100 | expect(@john.friend_with?(@mary)).to eq(true) 101 | expect(@mary.friend_with?(@john)).to eq(true) 102 | expect(@john.friend_with?(@james)).to eq(true) 103 | expect(@james.friend_with?(@john)).to eq(true) 104 | end 105 | 106 | it "should check if a user is not a friend" do 107 | expect(@john.friend_with?(@jane)).to eq(false) 108 | expect(@jane.friend_with?(@john)).to eq(false) 109 | expect(@john.friend_with?(@peter)).to eq(false) 110 | expect(@peter.friend_with?(@john)).to eq(false) 111 | end 112 | 113 | it "should check if a user has any connections with another user" do 114 | expect(@john.connected_with?(@jane)).to eq(true) 115 | expect(@jane.connected_with?(@john)).to eq(true) 116 | expect(@john.connected_with?(@peter)).to eq(true) 117 | expect(@peter.connected_with?(@john)).to eq(true) 118 | end 119 | 120 | it "should check if a user does not have any connections with another user" do 121 | expect(@victoria.connected_with?(@john)).to eq(false) 122 | expect(@john.connected_with?(@victoria)).to eq(false) 123 | end 124 | 125 | it "should check if a user was invited by another" do 126 | expect(@jane.invited_by?(@john)).to eq(true) 127 | expect(@james.invited_by?(@john)).to eq(true) 128 | end 129 | 130 | it "should check if a user was not invited by another" do 131 | expect(@john.invited_by?(@jane)).to eq(false) 132 | expect(@victoria.invited_by?(@john)).to eq(false) 133 | end 134 | 135 | it "should check if a user has invited another user" do 136 | expect(@john.invited?(@jane)).to eq(true) 137 | expect(@john.invited?(@james)).to eq(true) 138 | end 139 | 140 | it "should check if a user did not invite another user" do 141 | expect(@jane.invited?(@john)).to eq(false) 142 | expect(@james.invited?(@john)).to eq(false) 143 | expect(@john.invited?(@victoria)).to eq(false) 144 | expect(@victoria.invited?(@john)).to eq(false) 145 | end 146 | end 147 | 148 | context "when removing friendships" do 149 | before(:each) do 150 | expect(@jane.invite(@james)).to eq(true) 151 | expect(@james.approve(@jane)).to eq(true) 152 | expect(@james.invite(@victoria)).to eq(true) 153 | expect(@victoria.approve(@james)).to eq(true) 154 | expect(@victoria.invite(@mary)).to eq(true) 155 | expect(@mary.approve(@victoria)).to eq(true) 156 | expect(@victoria.invite(@john)).to eq(true) 157 | expect(@john.approve(@victoria)).to eq(true) 158 | expect(@peter.invite(@victoria)).to eq(true) 159 | expect(@victoria.invite(@elisabeth)).to eq(true) 160 | end 161 | 162 | it "should remove the friends invited by him" do 163 | expect(@victoria.friends.size).to eq(3) 164 | expect(@victoria.friends).to include(@mary) 165 | expect(@victoria.invited).to include(@mary) 166 | expect(@mary.friends.size).to eq(1) 167 | expect(@mary.friends).to include(@victoria) 168 | expect(@mary.invited_by).to include(@victoria) 169 | 170 | expect(@victoria.remove_friendship(@mary)).to eq(true) 171 | expect(@victoria.friends.size).to eq(2) 172 | expect(@victoria.friends).to_not include(@mary) 173 | expect(@victoria.invited).to_not include(@mary) 174 | expect(@mary.friends.size).to eq(0) 175 | expect(@mary.friends).to_not include(@victoria) 176 | expect(@mary.invited_by).to_not include(@victoria) 177 | end 178 | 179 | it "should remove the friends who invited him" do 180 | expect(@victoria.friends.size).to eq(3) 181 | expect(@victoria.friends).to include(@james) 182 | expect(@victoria.invited_by).to include(@james) 183 | expect(@james.friends.size).to eq(2) 184 | expect(@james.friends).to include(@victoria) 185 | expect(@james.invited).to include(@victoria) 186 | 187 | expect(@victoria.remove_friendship(@james)).to eq(true) 188 | expect(@victoria.friends.size).to eq(2) 189 | expect(@victoria.friends).to_not include(@james) 190 | expect(@victoria.invited_by).to_not include(@james) 191 | expect(@james.friends.size).to eq(1) 192 | expect(@james.friends).to_not include(@victoria) 193 | expect(@james.invited).to_not include(@victoria) 194 | end 195 | 196 | it "should remove the pending friends invited by him" do 197 | expect(@victoria.pending_invited.size).to eq(1) 198 | expect(@victoria.pending_invited).to include(@elisabeth) 199 | expect(@elisabeth.pending_invited_by.size).to eq(1) 200 | expect(@elisabeth.pending_invited_by).to include(@victoria) 201 | expect(@victoria.remove_friendship(@elisabeth)).to eq(true) 202 | [@victoria, @elisabeth].map(&:reload) 203 | expect(@victoria.pending_invited.size).to eq(0) 204 | expect(@victoria.pending_invited).to_not include(@elisabeth) 205 | expect(@elisabeth.pending_invited_by.size).to eq(0) 206 | expect(@elisabeth.pending_invited_by).to_not include(@victoria) 207 | end 208 | 209 | it "should remove the pending friends who invited him" do 210 | expect(@victoria.pending_invited_by.count).to eq(1) 211 | expect(@victoria.pending_invited_by).to include(@peter) 212 | expect(@peter.pending_invited.count).to eq(1) 213 | expect(@peter.pending_invited).to include(@victoria) 214 | expect(@victoria.remove_friendship(@peter)).to eq(true) 215 | [@victoria, @peter].map(&:reload) 216 | expect(@victoria.pending_invited_by.count).to eq(0) 217 | expect(@victoria.pending_invited_by).to_not include(@peter) 218 | expect(@peter.pending_invited.count).to eq(0) 219 | expect(@peter.pending_invited).to_not include(@victoria) 220 | end 221 | end 222 | 223 | context "when blocking friendships" do 224 | before(:each) do 225 | expect(@john.invite(@james)).to eq(true) 226 | expect(@james.approve(@john)).to eq(true) 227 | expect(@james.block(@john)).to eq(true) 228 | expect(@mary.invite(@victoria)).to eq(true) 229 | expect(@victoria.approve(@mary)).to eq(true) 230 | expect(@victoria.block(@mary)).to eq(true) 231 | expect(@victoria.invite(@david)).to eq(true) 232 | expect(@david.block(@victoria)).to eq(true) 233 | expect(@john.invite(@david)).to eq(true) 234 | expect(@david.block(@john)).to eq(true) 235 | expect(@peter.invite(@elisabeth)).to eq(true) 236 | expect(@elisabeth.block(@peter)).to eq(true) 237 | expect(@jane.invite(@john)).to eq(true) 238 | expect(@jane.invite(@james)).to eq(true) 239 | expect(@james.approve(@jane)).to eq(true) 240 | expect(@victoria.invite(@jane)).to eq(true) 241 | expect(@victoria.invite(@james)).to eq(true) 242 | expect(@james.approve(@victoria)).to eq(true) 243 | end 244 | 245 | it "should allow to block author of the invitation by invited user" do 246 | expect(@john.block(@jane)).to eq(true) 247 | expect(@jane.block(@victoria)).to eq(true) 248 | end 249 | 250 | it "should not allow to block invited user by invitation author" do 251 | expect(@jane.block(@john)).to eq(false) 252 | expect(@victoria.block(@jane)).to eq(false) 253 | end 254 | 255 | it "should allow to block approved users on both sides" do 256 | expect(@james.block(@jane)).to eq(true) 257 | expect(@victoria.block(@james)).to eq(true) 258 | end 259 | 260 | it "should not allow to block not connected user" do 261 | expect(@david.block(@peter)).to eq(false) 262 | expect(@peter.block(@david)).to eq(false) 263 | end 264 | 265 | it "should not allow to block already blocked user" do 266 | expect(@john.block(@jane)).to eq(true) 267 | expect(@john.block(@jane)).to eq(false) 268 | expect(@james.block(@jane)).to eq(true) 269 | expect(@james.block(@jane)).to eq(false) 270 | end 271 | 272 | it "should list the blocked users" do 273 | expect(@jane.blocked).to be_empty 274 | expect(@peter.blocked).to be_empty 275 | expect(@james.blocked).to eq([@john]) 276 | expect(@victoria.blocked).to eq([@mary]) 277 | expect(@david.blocked).to match_array([@john, @victoria]) 278 | end 279 | 280 | it "should not list blocked users in friends" do 281 | expect(@james.friends).to match_array([@jane, @victoria]) 282 | @james.blocked.each do |user| 283 | expect(@james.friends).to_not include(user) 284 | expect(user.friends).to_not include(@james) 285 | end 286 | end 287 | 288 | it "should not list blocked users in invited" do 289 | expect(@victoria.invited).to eq([@james]) 290 | @victoria.blocked.each do |user| 291 | expect(@victoria.invited).to_not include(user) 292 | expect(user.invited_by).to_not include(@victoria) 293 | end 294 | end 295 | 296 | it "should not list blocked users in invited pending by" do 297 | expect(@david.pending_invited_by).to be_empty 298 | @david.blocked.each do |user| 299 | expect(@david.pending_invited_by).to_not include(user) 300 | expect(user.pending_invited).to_not include(@david) 301 | end 302 | end 303 | 304 | it "should check if a user is blocked" do 305 | expect(@james.blocked?(@john)).to eq(true) 306 | expect(@victoria.blocked?(@mary)).to eq(true) 307 | expect(@david.blocked?(@john)).to eq(true) 308 | expect(@david.blocked?(@victoria)).to eq(true) 309 | end 310 | end 311 | 312 | context "when unblocking friendships" do 313 | before(:each) do 314 | expect(@john.invite(@james)).to eq(true) 315 | expect(@james.approve(@john)).to eq(true) 316 | expect(@john.block(@james)).to eq(true) 317 | expect(@john.unblock(@james)).to eq(true) 318 | expect(@mary.invite(@victoria)).to eq(true) 319 | expect(@victoria.approve(@mary)).to eq(true) 320 | expect(@victoria.block(@mary)).to eq(true) 321 | expect(@victoria.unblock(@mary)).to eq(true) 322 | expect(@victoria.invite(@david)).to eq(true) 323 | expect(@david.block(@victoria)).to eq(true) 324 | expect(@david.unblock(@victoria)).to eq(true) 325 | expect(@john.invite(@david)).to eq(true) 326 | expect(@david.block(@john)).to eq(true) 327 | expect(@peter.invite(@elisabeth)).to eq(true) 328 | expect(@elisabeth.block(@peter)).to eq(true) 329 | expect(@jane.invite(@john)).to eq(true) 330 | expect(@jane.invite(@james)).to eq(true) 331 | expect(@james.approve(@jane)).to eq(true) 332 | expect(@victoria.invite(@jane)).to eq(true) 333 | expect(@victoria.invite(@james)).to eq(true) 334 | expect(@james.approve(@victoria)).to eq(true) 335 | end 336 | 337 | it "should allow to unblock prevoiusly blocked user" do 338 | expect(@david.unblock(@john)).to eq(true) 339 | expect(@elisabeth.unblock(@peter)).to eq(true) 340 | end 341 | 342 | it "should not allow to unblock not prevoiusly blocked user" do 343 | expect(@john.unblock(@jane)).to eq(false) 344 | expect(@james.unblock(@jane)).to eq(false) 345 | expect(@victoria.unblock(@jane)).to eq(false) 346 | expect(@james.unblock(@victoria)).to eq(false) 347 | end 348 | 349 | it "should not allow to unblock blocked user by himself" do 350 | expect(@john.unblock(@david)).to eq(false) 351 | expect(@peter.unblock(@elisabeth)).to eq(false) 352 | end 353 | 354 | it "should list unblocked users in friends" do 355 | expect(@john.friends).to eq([@james]) 356 | expect(@mary.friends).to eq([@victoria]) 357 | expect(@victoria.friends).to match_array([@mary, @james]) 358 | expect(@james.friends).to match_array([@john, @jane, @victoria]) 359 | end 360 | 361 | it "should list unblocked users in invited" do 362 | expect(@john.invited).to eq([@james]) 363 | expect(@mary.invited).to eq([@victoria]) 364 | end 365 | 366 | it "should list unblocked users in invited by" do 367 | expect(@victoria.invited_by).to eq([@mary]) 368 | expect(@james.invited_by).to match_array([@john, @jane, @victoria]) 369 | end 370 | 371 | it "should list unblocked users in pending invited" do 372 | expect(@victoria.pending_invited).to match_array([@jane, @david]) 373 | end 374 | 375 | it "should list unblocked users in pending invited by" do 376 | expect(@david.pending_invited_by).to eq([@victoria]) 377 | end 378 | end 379 | 380 | context "when counting friendships and blocks" do 381 | before do 382 | expect(@john.invite(@james)).to eq(true) 383 | expect(@james.approve(@john)).to eq(true) 384 | expect(@john.invite(@victoria)).to eq(true) 385 | expect(@victoria.approve(@john)).to eq(true) 386 | expect(@elisabeth.invite(@john)).to eq(true) 387 | expect(@john.approve(@elisabeth)).to eq(true) 388 | 389 | expect(@victoria.invite(@david)).to eq(true) 390 | expect(@david.block(@victoria)).to eq(true) 391 | expect(@mary.invite(@victoria)).to eq(true) 392 | expect(@victoria.block(@mary)).to eq(true) 393 | end 394 | 395 | it "should return the correct count for total_friends" do 396 | expect(@john.total_friends).to eq(3) 397 | expect(@elisabeth.total_friends).to eq(1) 398 | expect(@james.total_friends).to eq(1) 399 | expect(@victoria.total_friends).to eq(1) 400 | end 401 | 402 | it "should return the correct count for total_blocked" do 403 | expect(@david.total_blocked).to eq(1) 404 | expect(@victoria.total_blocked).to eq(1) 405 | end 406 | end 407 | end 408 | --------------------------------------------------------------------------------