Merge pull request #6951 from svbergerem/contacts-search
Modify search to include contacts
This commit is contained in:
commit
8e887f884e
9 changed files with 119 additions and 57 deletions
|
|
@ -169,6 +169,7 @@ before.
|
||||||
* Add 'Be excellent to each other!' to the sidebar [#6914](https://github.com/diaspora/diaspora/pull/6910)
|
* Add 'Be excellent to each other!' to the sidebar [#6914](https://github.com/diaspora/diaspora/pull/6910)
|
||||||
* Expose Sidekiq dead queue configuration options
|
* Expose Sidekiq dead queue configuration options
|
||||||
* Properly support pluralization in timeago strings [#6926](https://github.com/diaspora/diaspora/pull/6926)
|
* Properly support pluralization in timeago strings [#6926](https://github.com/diaspora/diaspora/pull/6926)
|
||||||
|
* Return all contacts in people search [#6951](https://github.com/diaspora/diaspora/pull/6951)
|
||||||
|
|
||||||
# 0.5.11.0
|
# 0.5.11.0
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,8 @@ app.views.PublisherMention = app.views.SearchBase.extend({
|
||||||
app.views.SearchBase.prototype.initialize.call(this, {
|
app.views.SearchBase.prototype.initialize.call(this, {
|
||||||
typeaheadInput: this.typeaheadInput,
|
typeaheadInput: this.typeaheadInput,
|
||||||
customSearch: true,
|
customSearch: true,
|
||||||
autoselect: true
|
autoselect: true,
|
||||||
|
remoteRoute: "/contacts"
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,11 +25,6 @@ app.views.SearchBase = app.views.Base.extend({
|
||||||
return this.bloodhoundTokenizer(datum.name).concat(datum.handle);
|
return this.bloodhoundTokenizer(datum.name).concat(datum.handle);
|
||||||
}.bind(this),
|
}.bind(this),
|
||||||
queryTokenizer: Bloodhound.tokenizers.whitespace,
|
queryTokenizer: Bloodhound.tokenizers.whitespace,
|
||||||
prefetch: {
|
|
||||||
url: "/contacts.json",
|
|
||||||
transform: this.transformBloodhoundResponse,
|
|
||||||
cache: false
|
|
||||||
},
|
|
||||||
sufficient: 5
|
sufficient: 5
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,11 +14,10 @@ class ContactsController < ApplicationController
|
||||||
# Used by the mobile site
|
# Used by the mobile site
|
||||||
format.mobile { set_up_contacts_mobile }
|
format.mobile { set_up_contacts_mobile }
|
||||||
|
|
||||||
# Used to populate mentions in the publisher
|
# Used for mentions in the publisher
|
||||||
format.json {
|
format.json {
|
||||||
aspect_ids = params[:aspect_ids] || current_user.aspects.map(&:id)
|
@people = Person.search(params[:q], current_user, only_contacts: true).limit(15)
|
||||||
@people = Person.all_from_aspects(aspect_ids, current_user).for_json
|
render json: @people
|
||||||
render :json => @people.to_json
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,9 @@ class Person < ActiveRecord::Base
|
||||||
validates :serialized_public_key, :presence => true
|
validates :serialized_public_key, :presence => true
|
||||||
validates :diaspora_handle, :uniqueness => true
|
validates :diaspora_handle, :uniqueness => true
|
||||||
|
|
||||||
scope :searchable, -> { joins(:profile).where(:profiles => {:searchable => true}) }
|
scope :searchable, -> (user) {
|
||||||
|
joins(:profile).where("profiles.searchable = true OR contacts.user_id = ?", user.id)
|
||||||
|
}
|
||||||
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, -> {
|
||||||
|
|
@ -143,27 +145,23 @@ class Person < ActiveRecord::Base
|
||||||
[where_clause, q_tokens]
|
[where_clause, q_tokens]
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.search(query, user)
|
def self.search(search_str, user, only_contacts: false)
|
||||||
return self.where("1 = 0") if query.to_s.blank? || query.to_s.length < 2
|
search_str.strip!
|
||||||
|
return none if search_str.blank? || search_str.size < 2
|
||||||
|
|
||||||
sql, tokens = self.search_query_string(query)
|
sql, tokens = search_query_string(search_str)
|
||||||
|
|
||||||
Person.searchable.where(sql, *tokens).joins(
|
query = if only_contacts
|
||||||
"LEFT OUTER JOIN contacts ON contacts.user_id = #{user.id} AND contacts.person_id = people.id"
|
joins(:contacts).where(contacts: {user_id: user.id})
|
||||||
).includes(:profile
|
else
|
||||||
).order(search_order)
|
joins(
|
||||||
end
|
"LEFT OUTER JOIN contacts ON contacts.user_id = #{user.id} AND contacts.person_id = people.id"
|
||||||
|
).searchable(user)
|
||||||
|
end
|
||||||
|
|
||||||
# @return [Array<String>] postgreSQL and mysql deal with null values in orders differently, it seems.
|
query.where(sql, *tokens)
|
||||||
def self.search_order
|
.includes(:profile)
|
||||||
@search_order ||= Proc.new {
|
.order(["contacts.user_id IS NULL", "profiles.last_name ASC", "profiles.first_name ASC"])
|
||||||
order = if AppConfig.postgres?
|
|
||||||
"ASC"
|
|
||||||
else
|
|
||||||
"DESC"
|
|
||||||
end
|
|
||||||
["contacts.user_id #{order}", "profiles.last_name ASC", "profiles.first_name ASC"]
|
|
||||||
}.call
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def name(opts = {})
|
def name(opts = {})
|
||||||
|
|
|
||||||
|
|
@ -49,25 +49,26 @@ describe ContactsController, :type => :controller do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'format json' do
|
context "format json" do
|
||||||
it 'assumes all aspects if none are specified' do
|
before do
|
||||||
get :index, :format => 'json'
|
@person1 = FactoryGirl.create(:person)
|
||||||
expect(assigns[:people].map(&:id)).to match_array(bob.contacts.map { |c| c.person.id })
|
bob.share_with(@person1, bob.aspects.first)
|
||||||
|
@person2 = FactoryGirl.create(:person)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "succeeds" do
|
||||||
|
get :index, q: @person1.first_name, format: "json"
|
||||||
expect(response).to be_success
|
expect(response).to be_success
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns the contacts for multiple aspects' do
|
it "responds with json" do
|
||||||
get :index, :aspect_ids => bob.aspect_ids, :format => 'json'
|
get :index, q: @person1.first_name, format: "json"
|
||||||
expect(assigns[:people].map(&:id)).to match_array(bob.contacts.map { |c| c.person.id })
|
expect(response.body).to eq([@person1].to_json)
|
||||||
expect(response).to be_success
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not return duplicate contacts' do
|
it "only returns contacts" do
|
||||||
aspect = bob.aspects.create(:name => 'hilarious people')
|
get :index, q: @person2.first_name, format: "json"
|
||||||
aspect.contacts << bob.contact_for(eve.person)
|
expect(response.body).to eq([].to_json)
|
||||||
get :index, :format => 'json', :aspect_ids => bob.aspect_ids
|
|
||||||
expect(assigns[:people].map { |p| p.id }.uniq).to eq(assigns[:people].map { |p| p.id })
|
|
||||||
expect(assigns[:people].map(&:id)).to match_array(bob.contacts.map { |c| c.person.id })
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -52,12 +52,6 @@ FactoryGirl.define do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
factory :searchable_person, :parent => :person do
|
|
||||||
after(:build) do |person|
|
|
||||||
person.profile = FactoryGirl.build(:profile, :person => person, :searchable => true)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
factory :like do
|
factory :like do
|
||||||
association :author, :factory => :person
|
association :author, :factory => :person
|
||||||
association :target, :factory => :status_message
|
association :target, :factory => :status_message
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ describe("app.views.PublisherMention", function() {
|
||||||
expect(call.args[0].typeaheadInput.selector).toBe("#publisher .typeahead-mention-box");
|
expect(call.args[0].typeaheadInput.selector).toBe("#publisher .typeahead-mention-box");
|
||||||
expect(call.args[0].customSearch).toBeTruthy();
|
expect(call.args[0].customSearch).toBeTruthy();
|
||||||
expect(call.args[0].autoselect).toBeTruthy();
|
expect(call.args[0].autoselect).toBeTruthy();
|
||||||
|
expect(call.args[0].remoteRoute).toBe("/contacts");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("calls bindTypeaheadEvents", function() {
|
it("calls bindTypeaheadEvents", function() {
|
||||||
|
|
|
||||||
|
|
@ -294,10 +294,11 @@ describe Person, :type => :model do
|
||||||
user_profile.last_name = "asdji"
|
user_profile.last_name = "asdji"
|
||||||
user_profile.save
|
user_profile.save
|
||||||
|
|
||||||
@robert_grimm = FactoryGirl.build(:searchable_person)
|
@robert_grimm = FactoryGirl.build(:person)
|
||||||
@eugene_weinstein = FactoryGirl.build(:searchable_person)
|
@eugene_weinstein = FactoryGirl.build(:person)
|
||||||
@yevgeniy_dodis = FactoryGirl.build(:searchable_person)
|
@yevgeniy_dodis = FactoryGirl.build(:person)
|
||||||
@casey_grippi = FactoryGirl.build(:searchable_person)
|
@casey_grippi = FactoryGirl.build(:person)
|
||||||
|
@invisible_person = FactoryGirl.build(:person)
|
||||||
|
|
||||||
@robert_grimm.profile.first_name = "Robert"
|
@robert_grimm.profile.first_name = "Robert"
|
||||||
@robert_grimm.profile.last_name = "Grimm"
|
@robert_grimm.profile.last_name = "Grimm"
|
||||||
|
|
@ -318,7 +319,14 @@ describe Person, :type => :model do
|
||||||
@casey_grippi.profile.last_name = "Grippi"
|
@casey_grippi.profile.last_name = "Grippi"
|
||||||
@casey_grippi.profile.save
|
@casey_grippi.profile.save
|
||||||
@casey_grippi.reload
|
@casey_grippi.reload
|
||||||
|
|
||||||
|
@invisible_person.profile.first_name = "Johnson"
|
||||||
|
@invisible_person.profile.last_name = "Invisible"
|
||||||
|
@invisible_person.profile.searchable = false
|
||||||
|
@invisible_person.profile.save
|
||||||
|
@invisible_person.reload
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'orders results by last name' do
|
it 'orders results by last name' do
|
||||||
@robert_grimm.profile.first_name = "AAA"
|
@robert_grimm.profile.first_name = "AAA"
|
||||||
@robert_grimm.profile.save!
|
@robert_grimm.profile.save!
|
||||||
|
|
@ -367,10 +375,15 @@ describe Person, :type => :model do
|
||||||
expect(people.first).to eq(@casey_grippi)
|
expect(people.first).to eq(@casey_grippi)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'only displays searchable people' do
|
it "doesn't display people that are neither searchable nor contacts" do
|
||||||
invisible_person = FactoryGirl.build(:person, :profile => FactoryGirl.build(:profile, :searchable => false, :first_name => "johnson"))
|
expect(Person.search("Johnson", @user)).to be_empty
|
||||||
expect(Person.search("johnson", @user)).not_to include invisible_person
|
end
|
||||||
expect(Person.search("", @user)).not_to include invisible_person
|
|
||||||
|
it "displays contacts that are not searchable" do
|
||||||
|
@user.contacts.create(person: @invisible_person, aspects: [@user.aspects.first])
|
||||||
|
people = Person.search("Johnson", @user)
|
||||||
|
expect(people.count).to eq(1)
|
||||||
|
expect(people.first).to eq(@invisible_person)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns results for Diaspora handles' do
|
it 'returns results for Diaspora handles' do
|
||||||
|
|
@ -396,6 +409,65 @@ describe Person, :type => :model do
|
||||||
people = Person.search("AAA", @user)
|
people = Person.search("AAA", @user)
|
||||||
expect(people.map { |p| p.name }).to eq([@casey_grippi, @yevgeniy_dodis, @robert_grimm, @eugene_weinstein].map { |p| p.name })
|
expect(people.map { |p| p.name }).to eq([@casey_grippi, @yevgeniy_dodis, @robert_grimm, @eugene_weinstein].map { |p| p.name })
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "only contacts" do
|
||||||
|
before do
|
||||||
|
@robert_contact = @user.contacts.create(person: @robert_grimm, aspects: [@user.aspects.first])
|
||||||
|
@eugene_contact = @user.contacts.create(person: @eugene_weinstein, aspects: [@user.aspects.first])
|
||||||
|
@invisible_contact = @user.contacts.create(person: @invisible_person, aspects: [@user.aspects.first])
|
||||||
|
end
|
||||||
|
|
||||||
|
it "orders results by last name" do
|
||||||
|
@robert_grimm.profile.first_name = "AAA"
|
||||||
|
@robert_grimm.profile.save!
|
||||||
|
|
||||||
|
@eugene_weinstein.profile.first_name = "AAA"
|
||||||
|
@eugene_weinstein.profile.save!
|
||||||
|
|
||||||
|
@casey_grippi.profile.first_name = "AAA"
|
||||||
|
@casey_grippi.profile.save!
|
||||||
|
|
||||||
|
people = Person.search("AAA", @user, only_contacts: true)
|
||||||
|
expect(people.map(&:name)).to eq([@robert_grimm, @eugene_weinstein].map(&:name))
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns nothing on an empty query" do
|
||||||
|
people = Person.search("", @user, only_contacts: true)
|
||||||
|
expect(people).to be_empty
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns nothing on a one-character query" do
|
||||||
|
people = Person.search("i", @user, only_contacts: true)
|
||||||
|
expect(people).to be_empty
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns results for partial names" do
|
||||||
|
people = Person.search("Eug", @user, only_contacts: true)
|
||||||
|
expect(people.count).to eq(1)
|
||||||
|
expect(people.first).to eq(@eugene_weinstein)
|
||||||
|
|
||||||
|
people = Person.search("wEi", @user, only_contacts: true)
|
||||||
|
expect(people.count).to eq(1)
|
||||||
|
expect(people.first).to eq(@eugene_weinstein)
|
||||||
|
|
||||||
|
@user.contacts.create(person: @casey_grippi, aspects: [@user.aspects.first])
|
||||||
|
people = Person.search("gri", @user, only_contacts: true)
|
||||||
|
expect(people.count).to eq(2)
|
||||||
|
expect(people.first).to eq(@robert_grimm)
|
||||||
|
expect(people.second).to eq(@casey_grippi)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns results for full names" do
|
||||||
|
people = Person.search("Robert Grimm", @user, only_contacts: true)
|
||||||
|
expect(people.count).to eq(1)
|
||||||
|
expect(people.first).to eq(@robert_grimm)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns results for Diaspora handles" do
|
||||||
|
people = Person.search(@robert_grimm.diaspora_handle, @user, only_contacts: true)
|
||||||
|
expect(people).to eq([@robert_grimm])
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'people finders for webfinger' do
|
context 'people finders for webfinger' do
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue