Add AlgoliaSearchable for User (#20869)
* Add AlgoliaSearchable::SearchableUser * Update spec * Refactor * Refactor * Add more test * Refactor * Update seeder to clear Algolia index if it exist * All Algolia directly in update_score * Fix broken spec * Remove new queue * Account for more user statuses * Remove unncessary test for now
This commit is contained in:
parent
2129ae145b
commit
d587ba962d
9 changed files with 176 additions and 0 deletions
|
|
@ -26,6 +26,10 @@ class Seeder
|
|||
|
||||
if klass.none?
|
||||
message = ["Creating", count, plural].compact.join(" ")
|
||||
if klass.respond_to?(:algolia_search) && Settings::General.algolia_search_enabled?
|
||||
puts " Algolia search enabled, clearing index for #{klass}..."
|
||||
klass.clear_index!
|
||||
end
|
||||
puts " #{@counter}. #{message}."
|
||||
yield
|
||||
else
|
||||
|
|
|
|||
14
app/models/concerns/algolia_searchable.rb
Normal file
14
app/models/concerns/algolia_searchable.rb
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
module AlgoliaSearchable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
DEFAULT_ALGOLIA_SETTINGS = {
|
||||
per_environment: true,
|
||||
disable_indexing: -> { Settings::General.algolia_search_enabled? == false },
|
||||
enqueue: :trigger_sidekiq_worker
|
||||
}.freeze
|
||||
|
||||
included do
|
||||
include AlgoliaSearch
|
||||
public_send :include, "AlgoliaSearchable::Searchable#{name}".constantize
|
||||
end
|
||||
end
|
||||
24
app/models/concerns/algolia_searchable/searchable_user.rb
Normal file
24
app/models/concerns/algolia_searchable/searchable_user.rb
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
module AlgoliaSearchable
|
||||
module SearchableUser
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
algoliasearch(**DEFAULT_ALGOLIA_SETTINGS, unless: :bad_actor?) do
|
||||
attribute :name, :username
|
||||
attribute :profile_image do
|
||||
profile_image_90
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class_methods do
|
||||
def trigger_sidekiq_worker(record, delete)
|
||||
AlgoliaSearch::SearchIndexWorker.perform_async(record.class.name, record.id, delete)
|
||||
end
|
||||
end
|
||||
|
||||
def bad_actor?
|
||||
score.negative? || banished? || spam_or_suspended?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -5,6 +5,7 @@ class User < ApplicationRecord
|
|||
include CloudinaryHelper
|
||||
|
||||
include Images::Profile.for(:profile_image_url)
|
||||
include AlgoliaSearchable
|
||||
|
||||
# NOTE: we are using an inline module to keep profile related things together.
|
||||
concerning :Profiles do
|
||||
|
|
@ -315,6 +316,7 @@ class User < ApplicationRecord
|
|||
calculated_score = (badge_achievements_count * 10) + user_reaction_points
|
||||
calculated_score -= 500 if spam?
|
||||
update_column(:score, calculated_score)
|
||||
AlgoliaSearch::SearchIndexWorker.perform_async(self.class.name, id, false)
|
||||
end
|
||||
|
||||
def path
|
||||
|
|
|
|||
19
app/workers/algolia_search/search_index_worker.rb
Normal file
19
app/workers/algolia_search/search_index_worker.rb
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
module AlgoliaSearch
|
||||
class SearchIndexWorker
|
||||
include Sidekiq::Worker
|
||||
sidekiq_options queue: :medium_priority, retry: 5, tags: ["algolia"]
|
||||
|
||||
def perform(klass, id, remove)
|
||||
return unless Settings::General.algolia_search_enabled?
|
||||
|
||||
record = klass.constantize
|
||||
|
||||
if remove
|
||||
index = AlgoliaSearch.client.init_index(record.index_name)
|
||||
index.delete_object(id)
|
||||
else
|
||||
record.find(id).index!
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1022,4 +1022,49 @@ RSpec.describe User do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when indexing with Algolia", :algolia do
|
||||
it "indexes the user on create" do
|
||||
allow(AlgoliaSearch::SearchIndexWorker).to receive(:perform_async)
|
||||
create(:user)
|
||||
expect(AlgoliaSearch::SearchIndexWorker).to have_received(:perform_async).with("User", kind_of(Integer), false)
|
||||
end
|
||||
|
||||
it "updates user index if user's name has changed" do
|
||||
user = create(:user)
|
||||
allow(AlgoliaSearch::SearchIndexWorker).to receive(:perform_async)
|
||||
user.update(name: "New Name")
|
||||
expect(AlgoliaSearch::SearchIndexWorker).to have_received(:perform_async).with("User", user.id, false)
|
||||
end
|
||||
|
||||
describe "#bad_actor?" do
|
||||
it "returns false to a regular user" do
|
||||
user = build(:user)
|
||||
expect(user.bad_actor?).to be(false)
|
||||
end
|
||||
|
||||
it "returns true if the user has negative score" do
|
||||
user = build(:user, score: -500)
|
||||
expect(user.bad_actor?).to be(true)
|
||||
end
|
||||
|
||||
it "returns true if the user has spam role" do
|
||||
user = build(:user)
|
||||
user.add_role(:spam)
|
||||
expect(user.bad_actor?).to be(true)
|
||||
end
|
||||
|
||||
it "return true if user is suspended" do
|
||||
user = build(:user)
|
||||
user.add_role(:suspended)
|
||||
expect(user.bad_actor?).to be(true)
|
||||
end
|
||||
|
||||
it "return true if user is banished" do
|
||||
user = build(:user)
|
||||
allow(user).to receive(:banished?).and_return(true)
|
||||
expect(user.bad_actor?).to be(true)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -128,6 +128,12 @@ RSpec.configure do |config|
|
|||
end
|
||||
end
|
||||
|
||||
config.before(:each, :algolia) do
|
||||
allow(Settings::General).to receive_messages(
|
||||
algolia_application_id: "on", algolia_search_only_api_key: "on", algolia_api_key: "on",
|
||||
)
|
||||
end
|
||||
|
||||
config.before(:suite) do
|
||||
# Set the TZ ENV variable with the current random timezone from zonebie
|
||||
# which we can then use to properly set the browser time for Capybara specs
|
||||
|
|
|
|||
37
spec/support/algolia_mock_requester.rb
Normal file
37
spec/support/algolia_mock_requester.rb
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# from https://github.com/algolia/algoliasearch-client-ruby/blob/master/test/algolia/integration/mocks/mock_requester.rb
|
||||
class AlgoliaMockRequester
|
||||
attr_accessor :requests
|
||||
|
||||
def initialize
|
||||
@connection = nil
|
||||
@requests = []
|
||||
end
|
||||
|
||||
def send_request(host, method, path, body, headers, timeout, connect_timeout)
|
||||
request = {
|
||||
host: host,
|
||||
method: method,
|
||||
path: path,
|
||||
body: body,
|
||||
headers: headers,
|
||||
timeout: timeout,
|
||||
connect_timeout: connect_timeout
|
||||
}
|
||||
|
||||
@requests.push(request)
|
||||
|
||||
Algolia::Http::Response.new(
|
||||
status: 200,
|
||||
body: '{"hits": [], "status": "published"}',
|
||||
headers: {},
|
||||
)
|
||||
end
|
||||
|
||||
def get_connection(host)
|
||||
@connection = host
|
||||
end
|
||||
|
||||
def build_url(host)
|
||||
host.protocol + host.url
|
||||
end
|
||||
end
|
||||
25
spec/workers/algolia_search/search_index_worker_spec.rb
Normal file
25
spec/workers/algolia_search/search_index_worker_spec.rb
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
require "rails_helper"
|
||||
# rubocop:disable RSpec/AnyInstance
|
||||
RSpec.describe AlgoliaSearch::SearchIndexWorker, :algolia, type: :worker do
|
||||
let(:user) { create(:user) }
|
||||
|
||||
before do
|
||||
mock_requester = AlgoliaMockRequester.new
|
||||
algolia_config = Algolia::Search::Config.new(AlgoliaSearch.configuration)
|
||||
mock_client = Algolia::Search::Client.new(algolia_config, http_requester: mock_requester)
|
||||
AlgoliaSearch.instance_variable_set(:@client, mock_client)
|
||||
end
|
||||
|
||||
it "remove the record from Algolia if record is deleted" do
|
||||
expect_any_instance_of(Algolia::Search::Index).to receive(:delete_object).with(user.id)
|
||||
described_class.new.perform(user.class.name, user.id, true)
|
||||
end
|
||||
|
||||
it "index the record in Algolia if record is created" do
|
||||
allow(User).to receive(:find).with(user.id).and_return(user)
|
||||
allow(user).to receive(:index!)
|
||||
described_class.new.perform(user.class.name, user.id, false)
|
||||
expect(user).to have_received(:index!)
|
||||
end
|
||||
end
|
||||
# rubocop:enable RSpec/AnyInstance
|
||||
Loading…
Add table
Reference in a new issue