Make relationships polymorphic

This commit is contained in:
Karina Kwiatek 2021-12-31 22:19:21 +01:00 committed by Karina Kwiatek
parent ee6ba8f9b7
commit ce951453a2
13 changed files with 122 additions and 54 deletions

View File

@ -10,7 +10,7 @@ class UserController < ApplicationController
@more_data_available = !@user.cursored_answers(last_id: @answers_last_id, size: 1).count.zero? @more_data_available = !@user.cursored_answers(last_id: @answers_last_id, size: 1).count.zero?
if user_signed_in? if user_signed_in?
notif = Notification.where(target_type: "Relationship", target_id: @user.active_relationships.where(target_id: current_user.id).pluck(:id), recipient_id: current_user.id, new: true).first notif = Notification.where(target_type: "Relationship", target_id: @user.active_follow_relationships.where(target_id: current_user.id).pluck(:id), recipient_id: current_user.id, new: true).first
unless notif.nil? unless notif.nil?
notif.new = false notif.new = false
notif.save notif.save
@ -96,12 +96,12 @@ class UserController < ApplicationController
end end
end end
def friends def followings
@title = 'Following' @title = 'Following'
@user = User.where('LOWER(screen_name) = ?', params[:username].downcase).includes(:profile).first! @user = User.where('LOWER(screen_name) = ?', params[:username].downcase).includes(:profile).first!
@users = @user.cursored_friends(last_id: params[:last_id]) @users = @user.cursored_followings(last_id: params[:last_id])
@users_last_id = @users.map(&:id).min @users_last_id = @users.map(&:id).min
@more_data_available = !@user.cursored_friends(last_id: @users_last_id, size: 1).count.zero? @more_data_available = !@user.cursored_followings(last_id: @users_last_id, size: 1).count.zero?
@type = :friend @type = :friend
respond_to do |format| respond_to do |format|

View File

@ -5,24 +5,4 @@ class Relationship < ApplicationRecord
validates :target_id, presence: true validates :target_id, presence: true
default_scope { order(created_at: :asc) } default_scope { order(created_at: :asc) }
after_create do
Notification.notify target, self
# increment counts
source.increment! :friend_count
target.increment! :follower_count
end
before_destroy do
Notification.denotify target, self
# decrement counts
source.decrement! :friend_count
target.decrement! :follower_count
end
def notification_type(*_args)
Notifications::StartedFollowing
end
end end

View File

@ -0,0 +1,19 @@
class Relationships::Follow < Relationship
after_create do
Notification.notify target, self
# increment counts
source.increment! :friend_count
target.increment! :follower_count
end
before_destroy do
Notification.denotify target, self
# decrement counts
source.decrement! :friend_count
target.decrement! :follower_count
end
def notification_type(*_args)
Notifications::StartedFollowing
end
end

View File

@ -1,4 +1,6 @@
class User < ApplicationRecord class User < ApplicationRecord
include User::Relationship
include User::Relationship::Follow
include User::AnswerMethods include User::AnswerMethods
include User::InboxMethods include User::InboxMethods
include User::QuestionMethods include User::QuestionMethods
@ -24,14 +26,6 @@ class User < ApplicationRecord
has_many :answers, dependent: :destroy has_many :answers, dependent: :destroy
has_many :comments, dependent: :destroy has_many :comments, dependent: :destroy
has_many :inboxes, dependent: :destroy has_many :inboxes, dependent: :destroy
has_many :active_relationships, class_name: 'Relationship',
foreign_key: 'source_id',
dependent: :destroy
has_many :passive_relationships, class_name: 'Relationship',
foreign_key: 'target_id',
dependent: :destroy
has_many :friends, through: :active_relationships, source: :target
has_many :followers, through: :passive_relationships, source: :source
has_many :smiles, dependent: :destroy has_many :smiles, dependent: :destroy
has_many :comment_smiles, dependent: :destroy has_many :comment_smiles, dependent: :destroy
has_many :services, dependent: :destroy has_many :services, dependent: :destroy
@ -111,11 +105,6 @@ class User < ApplicationRecord
active_relationships.find_by(target: target_user).destroy active_relationships.find_by(target: target_user).destroy
end end
# @return [Boolean] true if +self+ is following +target_user+
def following?(target_user)
friends.include? target_user
end
# @param list [List] # @param list [List]
# @return [Boolean] true if +self+ is a member of +list+ # @return [Boolean] true if +self+ is a member of +list+
def member_of?(list) def member_of?(list)

View File

@ -0,0 +1,23 @@
# frozen_string_literal: true
class User
module Relationship
extend ActiveSupport::Concern
private
# Create a relationship for `type` with `target_user` as target.
def create_relationship(type, target_user)
type.create(target: target_user)
end
# Destroy a relationship for `type` with `target_user` as target.
def destroy_relationship(type, target_user)
type.find_by(target: target_user)&.destroy
end
def relationship_active?(type, target_user)
type.include?(target_user)
end
end
end

View File

@ -0,0 +1,39 @@
# frozen_string_literal: true
require "errors"
class User
module Relationship
module Follow
extend ActiveSupport::Concern
included do
has_many :active_follow_relationships, class_name: "Relationships::Follow",
foreign_key: "source_id",
dependent: :destroy
has_many :passive_follow_relationships, class_name: "Relationships::Follow",
foreign_key: "target_id",
dependent: :destroy
has_many :followings, through: :active_follow_relationships, source: :target
has_many :followers, through: :passive_follow_relationships, source: :source
end
# Follow an user
def follow(target_user)
raise Justask::Errors::FollowingOtherBlockedSelf if target_user.blocking?(self)
raise Justask::Errors::FollowingSelfBlockedOther if blocking?(target_user)
raise Justask::Errors::FollowingSelf if target_user == self
create_relationship(active_follow_relationships, target_user)
end
# Unfollow an user
def unfollow(target_user)
destroy_relationship(active_follow_relationships, target_user)
end
def following?(target_user)
relationship_active?(followings, target_user)
end
end
end
end

View File

@ -3,11 +3,11 @@
module User::RelationshipMethods module User::RelationshipMethods
include CursorPaginatable include CursorPaginatable
define_cursor_paginator :cursored_friends, :ordered_friends define_cursor_paginator :cursored_followings, :ordered_followings
define_cursor_paginator :cursored_followers, :ordered_followers define_cursor_paginator :cursored_followers, :ordered_followers
def ordered_friends def ordered_followings
friends.reverse_order.includes(:profile) followings.reverse_order.includes(:profile)
end end
def ordered_followers def ordered_followers

View File

@ -7,6 +7,6 @@ module User::TimelineMethods
# @return [Array] the users' timeline # @return [Array] the users' timeline
def timeline def timeline
Answer.where('user_id in (?) OR user_id = ?', friend_ids, id).order(:created_at).reverse_order.includes(comments: [:user, :smiles], question: [:user], user: [:profile], smiles: [:user]) Answer.where('user_id in (?) OR user_id = ?', following_ids, id).order(:created_at).reverse_order.includes(comments: [:user, :smiles], question: [:user], user: [:profile], smiles: [:user])
end end
end end

View File

@ -2,5 +2,5 @@
.list-group.list-group-horizontal-sm.text-center .list-group.list-group-horizontal-sm.text-center
= list_group_item 'Answers', show_user_profile_path(user.screen_name), badge: user.answered_count = list_group_item 'Answers', show_user_profile_path(user.screen_name), badge: user.answered_count
= list_group_item 'Questions', show_user_questions_path(user.screen_name), badge: user.asked_count = list_group_item 'Questions', show_user_questions_path(user.screen_name), badge: user.asked_count
= list_group_item 'Followers', show_user_followers_path(user.screen_name), badge: user.follower_count = list_group_item 'Followers', show_user_followers_path(user.screen_name), badge: user.followers.count
= list_group_item 'Following', show_user_friends_path(user.screen_name), badge: user.friend_count = list_group_item 'Following', show_user_followings_path(user.screen_name), badge: user.followings.count

View File

@ -135,12 +135,12 @@ Rails.application.routes.draw do
match '/@:username/a/:id', to: 'answer#show', via: 'get', as: :show_user_answer_alt match '/@:username/a/:id', to: 'answer#show', via: 'get', as: :show_user_answer_alt
match '/@:username/q/:id', to: 'question#show', via: 'get', as: :show_user_question_alt match '/@:username/q/:id', to: 'question#show', via: 'get', as: :show_user_question_alt
match '/@:username/followers(/p/:page)', to: 'user#followers', via: 'get', as: :show_user_followers_alt, defaults: {page: 1} match '/@:username/followers(/p/:page)', to: 'user#followers', via: 'get', as: :show_user_followers_alt, defaults: {page: 1}
match '/@:username/friends(/p/:page)', to: 'user#friends', via: 'get', as: :show_user_friends_alt, defaults: {page: 1} match '/@:username/followings(/p/:page)', to: 'user#followings', via: 'get', as: :show_user_followings_alt, defaults: {page: 1}
match '/:username(/p/:page)', to: 'user#show', via: 'get', as: :show_user_profile, defaults: {page: 1} match '/:username(/p/:page)', to: 'user#show', via: 'get', as: :show_user_profile, defaults: {page: 1}
match '/:username/a/:id', to: 'answer#show', via: 'get', as: :show_user_answer match '/:username/a/:id', to: 'answer#show', via: 'get', as: :show_user_answer
match '/:username/q/:id', to: 'question#show', via: 'get', as: :show_user_question match '/:username/q/:id', to: 'question#show', via: 'get', as: :show_user_question
match '/:username/followers(/p/:page)', to: 'user#followers', via: 'get', as: :show_user_followers, defaults: {page: 1} match '/:username/followers(/p/:page)', to: 'user#followers', via: 'get', as: :show_user_followers, defaults: {page: 1}
match '/:username/friends(/p/:page)', to: 'user#friends', via: 'get', as: :show_user_friends, defaults: {page: 1} match '/:username/followings(/p/:page)', to: 'user#followings', via: 'get', as: :show_user_followings, defaults: {page: 1}
match '/:username/lists(/p/:page)', to: 'user#lists', via: 'get', as: :show_user_lists, defaults: {page: 1} match '/:username/lists(/p/:page)', to: 'user#lists', via: 'get', as: :show_user_lists, defaults: {page: 1}
match '/:username/questions(/p/:page)', to: 'user#questions', via: 'get', as: :show_user_questions, defaults: {page: 1} match '/:username/questions(/p/:page)', to: 'user#questions', via: 'get', as: :show_user_questions, defaults: {page: 1}

View File

@ -0,0 +1,16 @@
class AddTypeToRelationships < ActiveRecord::Migration[5.2]
def up
add_column :relationships, :type, :string
execute %(update relationships set type = 'Relationships::Follow')
change_column_null :relationships, :type, false
add_index :relationships, :type
end
def down
execute %(delete from relationships where type <> 'Relationships::Follow')
remove_column :relationships, :type
end
end

View File

@ -157,9 +157,11 @@ ActiveRecord::Schema.define(version: 2022_01_05_171216) do
t.bigint "target_id" t.bigint "target_id"
t.datetime "created_at" t.datetime "created_at"
t.datetime "updated_at" t.datetime "updated_at"
t.string "type", null: false
t.index ["source_id", "target_id"], name: "index_relationships_on_source_id_and_target_id", unique: true t.index ["source_id", "target_id"], name: "index_relationships_on_source_id_and_target_id", unique: true
t.index ["source_id"], name: "index_relationships_on_source_id" t.index ["source_id"], name: "index_relationships_on_source_id"
t.index ["target_id"], name: "index_relationships_on_target_id" t.index ["target_id"], name: "index_relationships_on_target_id"
t.index ["type"], name: "index_relationships_on_type"
end end
create_table "reports", id: :serial, force: :cascade do |t| create_table "reports", id: :serial, force: :cascade do |t|

View File

@ -28,9 +28,9 @@ describe Ajax::FriendController, :ajax_controller, type: :controller do
end end
it "creates a follow relationship" do it "creates a follow relationship" do
expect(user.friends.ids).not_to include(target_user.id) expect(user.followings.ids).not_to include(target_user.id)
expect { subject }.to(change { user.friends.count }.by(1)) expect { subject }.to(change { user.followings.count }.by(1))
expect(user.friends.ids).to include(target_user.id) expect(user.followings.ids).to include(target_user.id)
end end
include_examples "returns the expected response" include_examples "returns the expected response"
@ -48,7 +48,7 @@ describe Ajax::FriendController, :ajax_controller, type: :controller do
end end
it "does not create a follow relationship" do it "does not create a follow relationship" do
expect { subject }.not_to(change { user.friends.count }) expect { subject }.not_to(change { user.followings.count })
end end
include_examples "returns the expected response" include_examples "returns the expected response"
@ -99,9 +99,9 @@ describe Ajax::FriendController, :ajax_controller, type: :controller do
before(:each) { user.follow target_user } before(:each) { user.follow target_user }
it "destroys a follow relationship" do it "destroys a follow relationship" do
expect(user.friends.ids).to include(target_user.id) expect(user.followings.ids).to include(target_user.id)
expect { subject }.to(change { user.friends.count }.by(-1)) expect { subject }.to(change { user.followings.count }.by(-1))
expect(user.friends.ids).not_to include(target_user.id) expect(user.followings.ids).not_to include(target_user.id)
end end
include_examples "returns the expected response" include_examples "returns the expected response"
@ -117,7 +117,7 @@ describe Ajax::FriendController, :ajax_controller, type: :controller do
end end
it "does not destroy a follow relationship" do it "does not destroy a follow relationship" do
expect { subject }.not_to(change { user.friends.count }) expect { subject }.not_to(change { user.followings.count })
end end
include_examples "returns the expected response" include_examples "returns the expected response"
@ -136,7 +136,7 @@ describe Ajax::FriendController, :ajax_controller, type: :controller do
end end
it "does not destroy a follow relationship" do it "does not destroy a follow relationship" do
expect { subject }.not_to(change { user.friends.count }) expect { subject }.not_to(change { user.followings.count })
end end
include_examples "returns the expected response" include_examples "returns the expected response"