new PostController#mentionable_in_comment action
This commit is contained in:
parent
ac136030c6
commit
1fb6040344
12 changed files with 458 additions and 45 deletions
|
|
@ -3,7 +3,7 @@
|
||||||
# the COPYRIGHT file.
|
# the COPYRIGHT file.
|
||||||
|
|
||||||
class PostsController < ApplicationController
|
class PostsController < ApplicationController
|
||||||
before_action :authenticate_user!, only: :destroy
|
before_action :authenticate_user!, only: %i(destroy mentionable)
|
||||||
before_action :set_format_if_malformed_from_status_net, only: :show
|
before_action :set_format_if_malformed_from_status_net, only: :show
|
||||||
|
|
||||||
respond_to :html, :mobile, :json, :xml
|
respond_to :html, :mobile, :json, :xml
|
||||||
|
|
@ -50,6 +50,21 @@ class PostsController < ApplicationController
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def mentionable
|
||||||
|
respond_to do |format|
|
||||||
|
format.json {
|
||||||
|
if params[:id].present? && params[:q].present?
|
||||||
|
render json: post_service.mentionable_in_comment(params[:id], params[:q])
|
||||||
|
else
|
||||||
|
render nothing: true, status: 204
|
||||||
|
end
|
||||||
|
}
|
||||||
|
format.any { render nothing: true, status: 406 }
|
||||||
|
end
|
||||||
|
rescue ActiveRecord::RecordNotFound
|
||||||
|
render nothing: true, status: 404
|
||||||
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
post_service.destroy(params[:id])
|
post_service.destroy(params[:id])
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
|
|
|
||||||
|
|
@ -54,14 +54,6 @@ class Comment < ActiveRecord::Base
|
||||||
self[:text] = text.to_s.strip #to_s if for nil, for whatever reason
|
self[:text] = text.to_s.strip #to_s if for nil, for whatever reason
|
||||||
end
|
end
|
||||||
|
|
||||||
def people_allowed_to_be_mentioned
|
|
||||||
if parent.public?
|
|
||||||
:all
|
|
||||||
else
|
|
||||||
[*parent.comments.pluck(:author_id), *parent.likes.pluck(:author_id), parent.author_id].uniq
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def add_mention_subscribers?
|
def add_mention_subscribers?
|
||||||
super && parent.author.local?
|
super && parent.author.local?
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,7 @@ module Notifications
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.filter_mentions(mentions, mentionable, _recipient_user_ids)
|
def self.filter_mentions(mentions, mentionable, _recipient_user_ids)
|
||||||
people = mentionable.people_allowed_to_be_mentioned
|
mentions.joins(:person).merge(Person.allowed_to_be_mentioned_in_a_comment_to(mentionable.parent))
|
||||||
if people == :all
|
|
||||||
mentions
|
|
||||||
else
|
|
||||||
mentions.where(person_id: people)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def mail_job
|
def mail_job
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,8 @@ class Person < ActiveRecord::Base
|
||||||
scope :remote, -> { where('people.owner_id IS NULL') }
|
scope :remote, -> { where('people.owner_id IS NULL') }
|
||||||
scope :local, -> { where('people.owner_id IS NOT NULL') }
|
scope :local, -> { where('people.owner_id IS NOT NULL') }
|
||||||
scope :for_json, -> {
|
scope :for_json, -> {
|
||||||
select('DISTINCT people.id, people.guid, people.diaspora_handle')
|
select("people.id, people.guid, people.diaspora_handle")
|
||||||
|
.distinct
|
||||||
.includes(:profile)
|
.includes(:profile)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -94,6 +95,78 @@ class Person < ActiveRecord::Base
|
||||||
.where(:posts => {:root_guid => StatusMessage.guids_for_author(user.person), :type => 'Reshare'} )
|
.where(:posts => {:root_guid => StatusMessage.guids_for_author(user.person), :type => 'Reshare'} )
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# This scope selects people where the full name contains the search_str or diaspora ID
|
||||||
|
# starts with the search_str.
|
||||||
|
# However, if the search_str doesn't have more than 1 non-whitespace character, it'll return an empty set.
|
||||||
|
# @param [String] search substring
|
||||||
|
# @return [Person::ActiveRecord_Relation]
|
||||||
|
scope :find_by_substring, ->(search_str) {
|
||||||
|
search_str.strip!
|
||||||
|
return none if search_str.blank? || search_str.size < 2
|
||||||
|
|
||||||
|
sql, tokens = search_query_string(search_str)
|
||||||
|
joins(:profile).where(sql, *tokens)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Left joins likes and comments to a specific post where people are authors of these comments and likes
|
||||||
|
# @param [String, Integer] post ID for which comments and likes should be joined
|
||||||
|
# @return [Person::ActiveRecord_Relation]
|
||||||
|
scope :left_join_visible_post_interactions_on_authorship, ->(post_id) {
|
||||||
|
comments_sql = <<-SQL
|
||||||
|
LEFT OUTER JOIN comments ON
|
||||||
|
comments.author_id = people.id AND comments.commentable_type = 'Post' AND comments.commentable_id = #{post_id}
|
||||||
|
SQL
|
||||||
|
|
||||||
|
likes_sql = <<-SQL
|
||||||
|
LEFT OUTER JOIN likes ON
|
||||||
|
likes.author_id = people.id AND likes.target_type = 'Post' AND likes.target_id = #{post_id}
|
||||||
|
SQL
|
||||||
|
|
||||||
|
joins(comments_sql).joins(likes_sql)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Selects people who can be mentioned in a comment to a specific post. For public posts all people
|
||||||
|
# are allowed, so no additional constraints are added. For private posts selection is limited to
|
||||||
|
# people who have posted comments or likes for this post.
|
||||||
|
# @param [Post] the post for which we query mentionable in comments people
|
||||||
|
# @return [Person::ActiveRecord_Relation]
|
||||||
|
scope :allowed_to_be_mentioned_in_a_comment_to, ->(post) {
|
||||||
|
if post.public?
|
||||||
|
all
|
||||||
|
else
|
||||||
|
left_join_visible_post_interactions_on_authorship(post.id)
|
||||||
|
.where("comments.id IS NOT NULL OR likes.id IS NOT NULL OR people.id = #{post.author_id}")
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
# This scope adds sorting of people in the order, appropriate for suggesting to a user (current user) who
|
||||||
|
# has requested a list of the people mentionable in a comment for a specific post.
|
||||||
|
# Sorts people in the following priority: post author > commenters > likers > contacts > non-contacts
|
||||||
|
# @param [Post] post for which the mentionable in comment people list is requested
|
||||||
|
# @param [User] user who requests the people list
|
||||||
|
# @return [Person::ActiveRecord_Relation]
|
||||||
|
scope :sort_for_mention_suggestion, ->(post, user) {
|
||||||
|
left_join_visible_post_interactions_on_authorship(post.id)
|
||||||
|
.joins("LEFT OUTER JOIN contacts ON people.id = contacts.person_id AND contacts.user_id = #{user.id}")
|
||||||
|
.joins(:profile)
|
||||||
|
.select(<<-SQL
|
||||||
|
people.id = #{unscoped { post.author_id }} AS is_author,
|
||||||
|
comments.id IS NOT NULL AS is_commenter,
|
||||||
|
likes.id IS NOT NULL AS is_liker,
|
||||||
|
contacts.id IS NOT NULL AS is_contact
|
||||||
|
SQL
|
||||||
|
)
|
||||||
|
.order(<<-SQL
|
||||||
|
is_author DESC,
|
||||||
|
is_commenter DESC,
|
||||||
|
is_liker DESC,
|
||||||
|
is_contact DESC,
|
||||||
|
profiles.full_name,
|
||||||
|
people.diaspora_handle
|
||||||
|
SQL
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
def self.community_spotlight
|
def self.community_spotlight
|
||||||
Person.joins(:roles).where(:roles => {:name => 'spotlight'})
|
Person.joins(:roles).where(:roles => {:name => 'spotlight'})
|
||||||
end
|
end
|
||||||
|
|
@ -128,7 +201,7 @@ class Person < ActiveRecord::Base
|
||||||
self.guid
|
self.guid
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.search_query_string(query)
|
private_class_method def self.search_query_string(query)
|
||||||
query = query.downcase
|
query = query.downcase
|
||||||
like_operator = AppConfig.postgres? ? "ILIKE" : "LIKE"
|
like_operator = AppConfig.postgres? ? "ILIKE" : "LIKE"
|
||||||
|
|
||||||
|
|
@ -146,15 +219,13 @@ class Person < ActiveRecord::Base
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.search(search_str, user, only_contacts: false, mutual: false)
|
def self.search(search_str, user, only_contacts: false, mutual: false)
|
||||||
search_str.strip!
|
query = find_by_substring(search_str)
|
||||||
return none if search_str.blank? || search_str.size < 2
|
return query if query.is_a?(ActiveRecord::NullRelation)
|
||||||
|
|
||||||
sql, tokens = search_query_string(search_str)
|
|
||||||
|
|
||||||
query = if only_contacts
|
query = if only_contacts
|
||||||
joins(:contacts).where(contacts: {user_id: user.id})
|
query.joins(:contacts).where(contacts: {user_id: user.id})
|
||||||
else
|
else
|
||||||
joins(
|
query.joins(
|
||||||
"LEFT OUTER JOIN contacts ON contacts.user_id = #{user.id} AND contacts.person_id = people.id"
|
"LEFT OUTER JOIN contacts ON contacts.user_id = #{user.id} AND contacts.person_id = people.id"
|
||||||
).searchable(user)
|
).searchable(user)
|
||||||
end
|
end
|
||||||
|
|
@ -162,8 +233,6 @@ class Person < ActiveRecord::Base
|
||||||
query = query.where(contacts: {sharing: true, receiving: true}) if mutual
|
query = query.where(contacts: {sharing: true, receiving: true}) if mutual
|
||||||
|
|
||||||
query.where(closed_account: false)
|
query.where(closed_account: false)
|
||||||
.where(sql, *tokens)
|
|
||||||
.includes(:profile)
|
|
||||||
.order(["contacts.user_id IS NULL", "profiles.last_name ASC", "profiles.first_name ASC"])
|
.order(["contacts.user_id IS NULL", "profiles.last_name ASC", "profiles.first_name ASC"])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,8 @@ class StatusMessage < Post
|
||||||
photos.each {|photo| photo.receive(recipient_user_ids) }
|
photos.each {|photo| photo.receive(recipient_user_ids) }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Note: the next two methods can be safely removed once changes from #6818 are deployed on every pod
|
||||||
|
# see StatusMessageCreationService#dispatch
|
||||||
# Only includes those people, to whom we're going to send a federation entity
|
# Only includes those people, to whom we're going to send a federation entity
|
||||||
# (and doesn't define exhaustive list of people who can receive it)
|
# (and doesn't define exhaustive list of people who can receive it)
|
||||||
def people_allowed_to_be_mentioned
|
def people_allowed_to_be_mentioned
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,17 @@ class PostService
|
||||||
user.retract(post)
|
user.retract(post)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def mentionable_in_comment(post_id, query)
|
||||||
|
post = find!(post_id)
|
||||||
|
Person
|
||||||
|
.allowed_to_be_mentioned_in_a_comment_to(post)
|
||||||
|
.where.not(id: user.person_id)
|
||||||
|
.find_by_substring(query)
|
||||||
|
.sort_for_mention_suggestion(post, user)
|
||||||
|
.for_json
|
||||||
|
.limit(15)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
attr_reader :user
|
attr_reader :user
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ Diaspora::Application.routes.draw do
|
||||||
resources :posts, only: %i(show destroy) do
|
resources :posts, only: %i(show destroy) do
|
||||||
member do
|
member do
|
||||||
get :interactions
|
get :interactions
|
||||||
|
get :mentionable
|
||||||
end
|
end
|
||||||
|
|
||||||
resource :participation, only: %i(create destroy)
|
resource :participation, only: %i(create destroy)
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
describe PostsController, type: :controller do
|
describe PostsController, type: :controller do
|
||||||
let(:post) { alice.post(:status_message, text: "ohai", to: alice.aspects.first) }
|
let(:post) { alice.post(:status_message, text: "ohai", to: alice.aspects.first) }
|
||||||
|
let(:post_service) { controller.send(:post_service) }
|
||||||
|
|
||||||
describe "#show" do
|
describe "#show" do
|
||||||
context "user signed in" do
|
context "user signed in" do
|
||||||
|
|
@ -166,6 +167,48 @@ describe PostsController, type: :controller do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "#mentionable" do
|
||||||
|
context "with a user signed in" do
|
||||||
|
before do
|
||||||
|
sign_in alice
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns status 204 without a :q parameter" do
|
||||||
|
get :mentionable, id: post.id, format: :json
|
||||||
|
expect(response.status).to eq(204)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "responses status 406 (not acceptable) on html request" do
|
||||||
|
get :mentionable, id: post.id, q: "whatever", format: :html
|
||||||
|
expect(response.status).to eq(406)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "responses status 404 when the post can't be found" do
|
||||||
|
expect(post_service).to receive(:find!) do
|
||||||
|
raise ActiveRecord::RecordNotFound
|
||||||
|
end
|
||||||
|
get :mentionable, id: post.id, q: "whatever", format: :json
|
||||||
|
expect(response.status).to eq(404)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "calls PostService#mentionable_in_comment and passes the result as a response" do
|
||||||
|
expect(post_service).to receive(:mentionable_in_comment).with(post.id.to_s, "whatever").and_return([bob.person])
|
||||||
|
get :mentionable, id: post.id, q: "whatever", format: :json
|
||||||
|
expect(response.status).to eq(200)
|
||||||
|
expect(response.body).to eq([bob.person].to_json)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "without a user signed in" do
|
||||||
|
it "returns 401" do
|
||||||
|
allow(post_service).to receive(:mentionable_in_comment).and_return([])
|
||||||
|
get :mentionable, id: post.id, q: "whatever", format: :json
|
||||||
|
expect(response.status).to eq(401)
|
||||||
|
expect(JSON.parse(response.body)["error"]).to eq(I18n.t("devise.failure.unauthenticated"))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "#destroy" do
|
describe "#destroy" do
|
||||||
context "own post" do
|
context "own post" do
|
||||||
before do
|
before do
|
||||||
|
|
|
||||||
|
|
@ -32,12 +32,17 @@ FactoryGirl.define do
|
||||||
end
|
end
|
||||||
|
|
||||||
factory(:person, aliases: %i(author)) do
|
factory(:person, aliases: %i(author)) do
|
||||||
|
transient do
|
||||||
|
first_name nil
|
||||||
|
end
|
||||||
|
|
||||||
sequence(:diaspora_handle) {|n| "bob-person-#{n}#{r_str}@example.net" }
|
sequence(:diaspora_handle) {|n| "bob-person-#{n}#{r_str}@example.net" }
|
||||||
pod { Pod.find_or_create_by(url: "http://example.net") }
|
pod { Pod.find_or_create_by(url: "http://example.net") }
|
||||||
serialized_public_key OpenSSL::PKey::RSA.generate(1024).public_key.export
|
serialized_public_key OpenSSL::PKey::RSA.generate(1024).public_key.export
|
||||||
after(:build) do |person|
|
after(:build) do |person, evaluator|
|
||||||
unless person.profile.first_name.present?
|
unless person.profile.first_name.present?
|
||||||
person.profile = FactoryGirl.build(:profile, person: person)
|
person.profile = FactoryGirl.build(:profile, person: person)
|
||||||
|
person.profile.first_name = evaluator.first_name if evaluator.first_name
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
after(:create) do |person|
|
after(:create) do |person|
|
||||||
|
|
|
||||||
|
|
@ -23,25 +23,6 @@ describe Comment, type: :model do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "#people_allowed_to_be_mentioned" do
|
|
||||||
let(:kate) { FactoryGirl.create(:user_with_aspect, friends: [bob]) }
|
|
||||||
let(:olga) { FactoryGirl.create(:user_with_aspect, friends: [bob]) }
|
|
||||||
|
|
||||||
it "returns the author and people who have commented or liked the private post" do
|
|
||||||
eve.comment!(status_bob, "comment text")
|
|
||||||
kate.like!(status_bob)
|
|
||||||
olga.participate!(status_bob)
|
|
||||||
status_bob.reload
|
|
||||||
expect(comment_alice.people_allowed_to_be_mentioned).to match_array([alice, bob, eve, kate].map(&:person_id))
|
|
||||||
end
|
|
||||||
|
|
||||||
it "returns :all for public posts" do
|
|
||||||
# set parent public
|
|
||||||
status_bob.update(public: true)
|
|
||||||
expect(comment_alice.people_allowed_to_be_mentioned).to eq(:all)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "#subscribers" do
|
describe "#subscribers" do
|
||||||
let(:status_bob) { FactoryGirl.create(:status_message, public: true, author: bob.person) }
|
let(:status_bob) { FactoryGirl.create(:status_message, public: true, author: bob.person) }
|
||||||
let(:comment_alice) {
|
let(:comment_alice) {
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,89 @@ describe Person, :type => :model do
|
||||||
expect(Person.who_have_reshared_a_users_posts(alice)).to eq([reshare.author])
|
expect(Person.who_have_reshared_a_users_posts(alice)).to eq([reshare.author])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe ".find_by_substring" do
|
||||||
|
it "returns \"none\" when the substring is less than 1 non-space character" do
|
||||||
|
expect(Person.find_by_substring("R")).to eq(Person.none)
|
||||||
|
expect(Person.find_by_substring("R ")).to eq(Person.none)
|
||||||
|
expect(Person.find_by_substring("")).to eq(Person.none)
|
||||||
|
expect(Person.find_by_substring(" ")).to eq(Person.none)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "finds a person with a profile name containing the substring" do
|
||||||
|
substring = r_str
|
||||||
|
person = FactoryGirl.create(:person, first_name: "A#{substring}A")
|
||||||
|
expect(Person.find_by_substring(substring)).to include(person)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "finds a person with a diaspora ID starting with the substring" do
|
||||||
|
substring = r_str
|
||||||
|
person = FactoryGirl.create(:person, diaspora_handle: "#{substring}A@pod.tld")
|
||||||
|
expect(Person.find_by_substring(substring)).to include(person)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe ".allowed_to_be_mentioned_in_a_comment_to" do
|
||||||
|
let(:status_bob) { bob.post(:status_message, text: "hello", to: bob.aspects.first.id) }
|
||||||
|
|
||||||
|
it "returns the author and people who have commented or liked the private post" do
|
||||||
|
kate = FactoryGirl.create(:user_with_aspect, friends: [bob])
|
||||||
|
olga = FactoryGirl.create(:user_with_aspect, friends: [bob])
|
||||||
|
alice.comment!(status_bob, "why so formal?")
|
||||||
|
eve.comment!(status_bob, "comment text")
|
||||||
|
kate.like!(status_bob)
|
||||||
|
olga.participate!(status_bob)
|
||||||
|
expect(
|
||||||
|
Person.allowed_to_be_mentioned_in_a_comment_to(status_bob).ids
|
||||||
|
).to match_array([alice, bob, eve, kate].map(&:person_id))
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns all for public posts" do
|
||||||
|
status_bob.update(public: true) # set parent public
|
||||||
|
expect(Person.allowed_to_be_mentioned_in_a_comment_to(status_bob)).to eq(Person.all)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe ".sort_for_mention_suggestion" do
|
||||||
|
let(:status_message) { FactoryGirl.create(:status_message) }
|
||||||
|
|
||||||
|
it "returns people sorted in the order: post author > commenters > likers > contacts" do
|
||||||
|
like = FactoryGirl.create(:like, target: status_message)
|
||||||
|
comment = FactoryGirl.create(:comment, post: status_message)
|
||||||
|
current_user = FactoryGirl.create(:user_with_aspect, friends: [alice])
|
||||||
|
result = Person.select(:id, :guid).sort_for_mention_suggestion(status_message, current_user)
|
||||||
|
expect(result[0]).to eq(status_message.author)
|
||||||
|
expect(result[1]).to eq(comment.author)
|
||||||
|
expect(result[2]).to eq(like.author)
|
||||||
|
expect(result[3]).to eq(alice.person) # a contact of the current user
|
||||||
|
end
|
||||||
|
|
||||||
|
it "sorts people of the same priority by profile name" do
|
||||||
|
current_user = FactoryGirl.create(:user_with_aspect)
|
||||||
|
person1 = FactoryGirl.create(:person, first_name: "x2")
|
||||||
|
person2 = FactoryGirl.create(:person, first_name: "x1")
|
||||||
|
result = Person
|
||||||
|
.select(:id, :guid)
|
||||||
|
.where(id: [person1.id, person2.id])
|
||||||
|
.sort_for_mention_suggestion(status_message, current_user)
|
||||||
|
expect(result[0].id).to eq(person2.id)
|
||||||
|
expect(result[1].id).to eq(person1.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "sorts people of the same priority and same names by diaspora ID" do
|
||||||
|
current_user = FactoryGirl.create(:user_with_aspect)
|
||||||
|
person1 = FactoryGirl.create(:person, diaspora_handle: "x2@pod.tld")
|
||||||
|
person1.profile.update(first_name: "John", last_name: "Doe")
|
||||||
|
person2 = FactoryGirl.create(:person, diaspora_handle: "x1@pod.tld")
|
||||||
|
person2.profile.update(first_name: "John", last_name: "Doe")
|
||||||
|
result = Person
|
||||||
|
.select(:id, :guid)
|
||||||
|
.where(id: [person1.id, person2.id])
|
||||||
|
.sort_for_mention_suggestion(status_message, current_user)
|
||||||
|
expect(result[0].id).to eq(person2.id)
|
||||||
|
expect(result[1].id).to eq(person1.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "delegating" do
|
describe "delegating" do
|
||||||
|
|
|
||||||
|
|
@ -193,4 +193,220 @@ describe PostService do
|
||||||
expect(StatusMessage.find_by_id(post.id)).not_to be_nil
|
expect(StatusMessage.find_by_id(post.id)).not_to be_nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "#mentionable_in_comment" do
|
||||||
|
describe "semi-integration test" do
|
||||||
|
let(:post_author_attributes) { {first_name: "Ro#{r_str}"} }
|
||||||
|
let(:post_author) { FactoryGirl.create(:person, post_author_attributes) }
|
||||||
|
let(:current_user) { FactoryGirl.create(:user_with_aspect) }
|
||||||
|
let(:post_service) { PostService.new(current_user) }
|
||||||
|
|
||||||
|
shared_context "with commenters and likers" do
|
||||||
|
# randomize ids of the created people so that the test doesn't pass just because of
|
||||||
|
# the id sequence matched against the expected ordering
|
||||||
|
let(:ids) { (1..4).map {|i| Person.maximum(:id) + i }.shuffle }
|
||||||
|
|
||||||
|
before do
|
||||||
|
# in case when post_author has't been instantiated before this context, specify id
|
||||||
|
# in order to avoid id conflict with the people generated here
|
||||||
|
post_author_attributes.merge!(id: ids.max + 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
let!(:commenter1) {
|
||||||
|
FactoryGirl.create(:person, id: ids.shift, first_name: "Ro1#{r_str}").tap {|person|
|
||||||
|
FactoryGirl.create(:comment, author: person, post: post)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let!(:commenter2) {
|
||||||
|
FactoryGirl.create(:person, id: ids.shift, first_name: "Ro2#{r_str}").tap {|person|
|
||||||
|
FactoryGirl.create(:comment, author: person, post: post)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let!(:liker1) {
|
||||||
|
FactoryGirl.create(:person, id: ids.shift, first_name: "Ro1#{r_str}").tap {|person|
|
||||||
|
FactoryGirl.create(:like, author: person, target: post)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let!(:liker2) {
|
||||||
|
FactoryGirl.create(:person, id: ids.shift, first_name: "Ro2#{r_str}").tap {|person|
|
||||||
|
FactoryGirl.create(:like, author: person, target: post)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_context "with a current user's friend" do
|
||||||
|
let!(:current_users_friend) {
|
||||||
|
FactoryGirl.create(:person).tap {|friend|
|
||||||
|
current_user.contacts.create!(
|
||||||
|
person: friend,
|
||||||
|
aspects: [current_user.aspects.first],
|
||||||
|
sharing: true,
|
||||||
|
receiving: true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with private post" do
|
||||||
|
let(:post) { FactoryGirl.create(:status_message, text: "ohai", author: post_author) }
|
||||||
|
|
||||||
|
context "when the post doesn't have a visibility for the current user" do
|
||||||
|
it "doesn't find a post and raises an exception" do
|
||||||
|
expect {
|
||||||
|
post_service.mentionable_in_comment(post.id, "Ro")
|
||||||
|
}.to raise_error(ActiveRecord::RecordNotFound)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when the post has a visibility for the current user" do
|
||||||
|
before do
|
||||||
|
ShareVisibility.batch_import([current_user.id], post)
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with commenters and likers" do
|
||||||
|
include_context "with commenters and likers"
|
||||||
|
|
||||||
|
it "returns mention suggestions in the correct order" do
|
||||||
|
expected_suggestions = [
|
||||||
|
post_author, commenter1, commenter2, liker1, liker2
|
||||||
|
]
|
||||||
|
expect(post_service.mentionable_in_comment(post.id, "Ro")).to eq(expected_suggestions)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with a current user's friend" do
|
||||||
|
include_context "with a current user's friend"
|
||||||
|
|
||||||
|
it "doesn't include a contact" do
|
||||||
|
expect(post_service.mentionable_in_comment(post.id, current_users_friend.first_name)).to be_empty
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't include a non contact" do
|
||||||
|
expect(post_service.mentionable_in_comment(post.id, eve.person.first_name)).to be_empty
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with public post" do
|
||||||
|
let(:post) { FactoryGirl.create(:status_message, text: "ohai", public: true, author: post_author) }
|
||||||
|
|
||||||
|
context "with commenters and likers and with a current user's friend" do
|
||||||
|
include_context "with commenters and likers"
|
||||||
|
include_context "with a current user's friend"
|
||||||
|
|
||||||
|
it "returns mention suggestions in the correct order" do
|
||||||
|
result = post_service.mentionable_in_comment(post.id, "Ro")
|
||||||
|
expect(result.size).to be > 7
|
||||||
|
# participants: post author, comments, likers
|
||||||
|
expect(result[0..4]).to eq([post_author, commenter1, commenter2, liker1, liker2])
|
||||||
|
# contacts
|
||||||
|
expect(result[5]).to eq(current_users_friend)
|
||||||
|
# non-contacts
|
||||||
|
result[6..-1].each {|person|
|
||||||
|
expect(person.contacts.where(user_id: current_user.id)).to be_empty
|
||||||
|
expect(person.profile.first_name).to include("Ro")
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't include people with non-matching names" do
|
||||||
|
commenter = FactoryGirl.create(:person, first_name: "RRR#{r_str}")
|
||||||
|
FactoryGirl.create(:comment, author: commenter)
|
||||||
|
liker = FactoryGirl.create(:person, first_name: "RRR#{r_str}")
|
||||||
|
FactoryGirl.create(:like, author: liker)
|
||||||
|
friend = FactoryGirl.create(:person, first_name: "RRR#{r_str}")
|
||||||
|
current_user.contacts.create!(
|
||||||
|
person: friend,
|
||||||
|
aspects: [current_user.aspects.first],
|
||||||
|
sharing: true,
|
||||||
|
receiving: true
|
||||||
|
)
|
||||||
|
|
||||||
|
result = post_service.mentionable_in_comment(post.id, "Ro")
|
||||||
|
expect(result).not_to include(commenter)
|
||||||
|
expect(result).not_to include(liker)
|
||||||
|
expect(result).not_to include(friend)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_examples "current user can't mention himself" do
|
||||||
|
before do
|
||||||
|
current_user.profile.update(first_name: "Ro#{r_str}")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't include current user" do
|
||||||
|
expect(post_service.mentionable_in_comment(post.id, "Ro")).not_to include(current_user.person)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when current user is a post author" do
|
||||||
|
let(:post_author) { current_user.person }
|
||||||
|
|
||||||
|
include_examples "current user can't mention himself"
|
||||||
|
end
|
||||||
|
|
||||||
|
context "current user is a participant" do
|
||||||
|
before do
|
||||||
|
current_user.like!(post)
|
||||||
|
current_user.comment!(post, "hello")
|
||||||
|
end
|
||||||
|
|
||||||
|
include_examples "current user can't mention himself"
|
||||||
|
end
|
||||||
|
|
||||||
|
context "current user is a stranger matching a search pattern" do
|
||||||
|
include_examples "current user can't mention himself"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't fail when the post author doesn't match the requested pattern" do
|
||||||
|
expect(post_service.mentionable_in_comment(post.id, "#{r_str}#{r_str}#{r_str}")).to be_empty
|
||||||
|
end
|
||||||
|
|
||||||
|
it "renders a commenter with multiple comments only once" do
|
||||||
|
person = FactoryGirl.create(:person, first_name: "Ro2#{r_str}")
|
||||||
|
2.times { FactoryGirl.create(:comment, author: person, post: post) }
|
||||||
|
expect(post_service.mentionable_in_comment(post.id, person.first_name).length).to eq(1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "unit test" do
|
||||||
|
let(:post_service) { PostService.new(alice) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
expect(post_service).to receive(:find!).and_return(post)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "calls Person.allowed_to_be_mentioned_in_a_comment_to" do
|
||||||
|
expect(Person).to receive(:allowed_to_be_mentioned_in_a_comment_to).with(post).and_call_original
|
||||||
|
post_service.mentionable_in_comment(post.id, "whatever")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "calls Person.find_by_substring" do
|
||||||
|
expect(Person).to receive(:find_by_substring).with("whatever").and_call_original
|
||||||
|
post_service.mentionable_in_comment(post.id, "whatever")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "calls Person.sort_for_mention_suggestion" do
|
||||||
|
expect(Person).to receive(:sort_for_mention_suggestion).with(post, alice).and_call_original
|
||||||
|
post_service.mentionable_in_comment(post.id, "whatever")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "calls Person.limit" do
|
||||||
|
expect_any_instance_of(Person::ActiveRecord_Relation).to receive(:limit).with(15).and_call_original
|
||||||
|
post_service.mentionable_in_comment(post.id, "whatever")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "contains a constraint on a current user" do
|
||||||
|
expect(Person).to receive(:allowed_to_be_mentioned_in_a_comment_to) { Person.all }
|
||||||
|
expect(Person).to receive(:find_by_substring) { Person.all }
|
||||||
|
expect(Person).to receive(:sort_for_mention_suggestion) { Person.all }
|
||||||
|
expect(post_service.mentionable_in_comment(post.id, alice.person.first_name))
|
||||||
|
.not_to include(alice.person)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue