Skip to content

Instantly share code, notes, and snippets.

@krtschmr
Last active September 12, 2023 14:19
Show Gist options
  • Save krtschmr/c9cb75ad364140ee5b6b953a1b1bb0cf to your computer and use it in GitHub Desktop.
Save krtschmr/c9cb75ad364140ee5b6b953a1b1bb0cf to your computer and use it in GitHub Desktop.
a great way to implement friendship AND blocklists for a rails-application. including specs for minitest.
class User
has_many :relations, class_name: "User::Relation"
has_many :friends,-> { where user_relations: {type: :friendship, state: :confirmed }}, through: :relations
has_many :pending_friendships,-> { where user_relations: {type: :friendship, state: :pending }}, through: :relations, source: :friend
has_many :blocked_users,-> { where user_relations: {type: :block }}, through: :relations, source: :friend
def request_frienship(friend)
raise Relation::ForeverAlone if friend == self
raise Relation::AlreadyConfirmed if friend_with?(friend)
raise Relation::AlreadyRequestedFriendship if pending_friend_request?(friend)
raise Relation::Blocked if blocked_user?(friend) || blocked_by?(friend)
self.transaction do
relations.create friend: friend, type: "friendship"
friend.relations.create friend: self, type: "friendship"
end
true
end
def confirm_friendship(friend)
raise Relation::AlreadyConfirmed if friend_with?(friend)
raise Relation::CantConfirmOwnRequest if initiated_request?(friend)
relations.friendship.pending.find_by(friend_id: friend.id).confirm!
end
def initiated_request?(friend)
relations.find_by(friend_id: friend.id).initiated?
end
def reject_friendship_request(friend)
raise Relation::AlreadyConfirmed if friend_with?(friend)
relations.friendship.pending.find_by(friend_id: friend.id).cancel!
true
end
alias_method :cancel_friendship_request, :reject_friendship_request
def update_friends_count
self.update_column :friends_count, friends.reload.count
end
def cancel_friendship(friend)
relations.find_by(friend_id: friend.id).cancel!
end
def friend_with?(friend)
friends.find_by(id: friend.id)
end
def pending_friend_request?(friend)
!!relations.friendship.pending.find_by(friend_id: friend.id)
end
def block_user(user)
raise Relation::ForeverAlone if user == self
raise Relation::Blocked if blocked_user?(user) || blocked_by?(user)
if relations.find_by(friend_id: user.id)
relations.find_by(friend_id: user.id).cancel!
end
relations.create friend: user, type: "block"
true
end
def unblock_user(user)
raise Relation::NotBlocked if !blocked_user?(user)
relations.find_by(friend_id: user.id).cancel!
end
def blocked_by?(user)
!!user.relations.find_by(friend_id: self.id).try(:block?)
end
def blocked_user?(user)
!! relations.find_by(friend_id: user.id).try(:block?)
end
end
class User::Relation < ApplicationRecord
class AlreadyConfirmed < Exception; end
class AlreadyRequestedFriendship < Exception; end
class Blocked < Exception; end
class CantConfirmOwnRequest < Exception; end
class ForeverAlone < Exception; end
class NotBlocked < Exception; end
self.inheritance_column = "_type"
belongs_to :user, required: true
belongs_to :friend, required: true, class_name: "User"
validates :type, inclusion: {in: ["block", "friendship"]}
validates :state, presence: true, if: :friendship?
validates :state, inclusion: {in: ["pending", "confirmed"]}, allow_nil: true
validates :user_id, uniqueness: {scope: :friend_id}
scope :friendship, -> { where(type: :friendship) }
scope :block, -> { where(type: :block) }
scope :pending, -> { where(state: :pending) }
scope :confirmed, -> { where(state: :confirmed) }
after_initialize {
self.state ||= "pending" if friendship?
}
def confirm!
self.transaction do
self.update(state: :confirmed)
other_relation.update(state: :confirmed)
user.update_friends_count
friend.update_friends_count
end
end
def cancel!
self.transaction do
other_relation.destroy if other_relation
self.destroy
end
end
def initiated?
if friendship?
# if the other relations was created AFTER our relation, then we initiated the request
self.id < other_relation.id
end
end
def other_relation
self.class.find_by(user_id: friend_id, friend_id: user_id)
end
def block?
type == "block"
end
def friendship?
type == "friendship"
end
end
require "test_helper"
describe "A relationship to another user" do
let(:user) { FactoryGirl.create :user}
let(:second_user) { FactoryGirl.create :second_user}
it "nobody has friends" do
user.friends.count.must_equal(0)
user.pending_friendships.count.must_equal(0)
user.blocked_users.count.must_equal(0)
second_user.friends.count.must_equal(0)
second_user.pending_friendships.count.must_equal(0)
second_user.blocked_users.count.must_equal(0)
end
describe "a user can request" do
it "friendship" do
assert_difference "User::Relation.count", 2 do
user.request_frienship(second_user)
end
user.reload.friends_count.must_equal(0)
user.pending_friendships.count.must_equal(1)
user.pending_friendships.first.must_equal(second_user)
second_user.pending_friendships.count.must_equal(1)
second_user.pending_friendships.first.must_equal(user)
user.wont_be :friend_with?, second_user
second_user.wont_be :friend_with?, user
user.must_be :pending_friend_request?, second_user
second_user.must_be :pending_friend_request?, user
end
it "friendship only once" do
user.request_frienship(second_user)
->{ user.request_frienship(second_user) }.must_raise User::Relation::AlreadyRequestedFriendship
end
it "friendship not if the other guy requested it aswell" do
user.request_frienship(second_user)
->{ second_user.request_frienship(user) }.must_raise User::Relation::AlreadyRequestedFriendship
end
it "friendship not with hisself" do
-> { user.request_frienship(user) }.must_raise User::Relation::ForeverAlone
end
end
describe "a friendship can" do
before(:each) { user.request_frienship(second_user) }
it "be confirmed" do
second_user.confirm_friendship(user)
user.friends.count.must_equal(1)
user.friends.first.must_equal(second_user)
user.pending_friendships.count.must_equal(0)
second_user.friends.count.must_equal(1)
second_user.friends.first.must_equal(user)
second_user.pending_friendships.count.must_equal(0)
user.must_be :friend_with?, second_user
second_user.must_be :friend_with?, user
user.wont_be :pending_friend_request?, second_user
second_user.wont_be :pending_friend_request?, user
end
it "be confrimed only from the other guy" do
->{ user.confirm_friendship(second_user) }.must_raise User::Relation::CantConfirmOwnRequest
end
it "be confirmed only once" do
second_user.confirm_friendship(user)
->{ second_user.confirm_friendship(user) }.must_raise User::Relation::AlreadyConfirmed
end
it "can be rejected from the other guy" do
user.pending_friendships.count.must_equal(1)
second_user.pending_friendships.count.must_equal(1)
assert_difference "User::Relation.count", -2 do
second_user.reject_friendship_request(user)
end
user.pending_friendships.count.must_equal(0)
second_user.pending_friendships.count.must_equal(0)
user.wont_be :friend_with?, second_user
second_user.wont_be :friend_with?, user
user.wont_be :pending_friend_request?, second_user
second_user.wont_be :pending_friend_request?, user
end
it "can be cancelled from initiator" do
user.pending_friendships.count.must_equal(1)
second_user.pending_friendships.count.must_equal(1)
assert_difference "User::Relation.count", -2 do
second_user.cancel_friendship_request(user)
end
user.pending_friendships.count.must_equal(0)
second_user.pending_friendships.count.must_equal(0)
user.wont_be :friend_with?, second_user
second_user.wont_be :friend_with?, user
user.wont_be :pending_friend_request?, second_user
second_user.wont_be :pending_friend_request?, user
end
it "be cancelled after confirmation from both guys" do
second_user.confirm_friendship(user)
assert_difference "User::Relation.count", -2 do
second_user.cancel_friendship(user)
end
user.friends.count.must_equal(0)
second_user.friends.count.must_equal(0)
user.pending_friendships.count.must_equal(0)
second_user.pending_friendships.count.must_equal(0)
user.wont_be :friend_with?, second_user
second_user.wont_be :friend_with?, user
user.wont_be :pending_friend_request?, second_user
second_user.wont_be :pending_friend_request?, user
end
end
it "can be a block" do
assert_difference "User::Relation.count", 1 do
user.block_user(second_user)
end
user.blocked_users.count.must_equal(1)
user.must_be :blocked_user?, second_user
second_user.must_be :blocked_by?, user
user.wont_be :friend_with?, second_user
second_user.wont_be :friend_with?, user
user.wont_be :pending_friend_request?, second_user
second_user.wont_be :pending_friend_request?, user
end
it "can be blocked only once" do
user.block_user(second_user)
->{ user.block_user(second_user) }.must_raise User::Relation::Blocked
end
it "a blocked user cant block the other one" do
user.block_user(second_user)
->{ user.block_user(second_user) }.must_raise User::Relation::Blocked
end
it "a blocked user can't request friendship" do
user.block_user(second_user)
->{ second_user.request_frienship(user) }.must_raise User::Relation::Blocked
end
it "a blocked user can't be friended" do
user.block_user(second_user)
->{ user.request_frienship(second_user) }.must_raise User::Relation::Blocked
end
it "can't block hisself" do
->{ user.block_user(user) }.must_raise User::Relation::ForeverAlone
end
it "cant be unblocked if there isnt a block" do
->{ user.unblock_user(second_user) }.must_raise User::Relation::NotBlocked
end
describe "a blocked user can be unblocked" do
before(:each) {user.block_user second_user }
it "" do
assert_difference "User::Relation.count", -1 do
user.unblock_user(second_user)
end
user.wont_be :blocked_user?, second_user
second_user.wont_be :blocked_by?, user
end
end
end
@krtschmr
Copy link
Author

krtschmr commented Mar 27, 2017

a user can have a friend, and can block people (who aren't even friends). pretty much the same way facebook does it. if somebody is blocked - friendship is over!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment