Last active
September 12, 2023 14:19
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
a
user
can have afriend
, and canblock
people (who aren't even friends). pretty much the same way facebook does it. if somebody is blocked - friendship is over!