224 lines
8.9 KiB
Ruby
224 lines
8.9 KiB
Ruby
class AutocompleteController < ApplicationController
|
||
respond_to :json
|
||
|
||
skip_before_action :store_location
|
||
skip_around_action :set_current_user, except: [:collection_parent_name, :owned_tag_sets, :site_skins]
|
||
skip_before_action :sanitize_ac_params # can we dare!
|
||
|
||
#### DO WE NEED THIS AT ALL? IF IT FIRES WITHOUT A TERM AND 500s BECAUSE USER DID SOMETHING WACKY SO WHAT
|
||
# # If you have an autocomplete that should fire without a term add it here
|
||
# before_action :require_term, except: [:tag_in_fandom, :relationship_in_fandom, :character_in_fandom, :nominated_parents]
|
||
#
|
||
# def require_term
|
||
# if params[:term].blank?
|
||
# flash[:error] = ts("What were you trying to autocomplete?")
|
||
# redirect_to(request.env["HTTP_REFERER"] || root_path) and return
|
||
# end
|
||
# end
|
||
#
|
||
#########################################
|
||
############# LOOKUP ACTIONS GO HERE
|
||
|
||
# PSEUDS
|
||
def pseud
|
||
return if params[:term].blank?
|
||
|
||
render_output(Pseud.autocomplete_lookup(search_param: params[:term], autocomplete_prefix: "autocomplete_pseud").map { |res| Pseud.fullname_from_autocomplete(res) })
|
||
end
|
||
|
||
## TAGS
|
||
private
|
||
def tag_output(search_param, tag_type)
|
||
tags = Tag.autocomplete_lookup(search_param: search_param, autocomplete_prefix: "autocomplete_tag_#{tag_type}")
|
||
render_output tags.map {|r| Tag.name_from_autocomplete(r)}
|
||
end
|
||
public
|
||
# these are all basically duplicates but make our calls to autocomplete more readable
|
||
def tag; tag_output(params[:term], params[:type] || "all"); end
|
||
def fandom; tag_output(params[:term], "fandom"); end
|
||
def character; tag_output(params[:term], "character"); end
|
||
def relationship; tag_output(params[:term], "relationship"); end
|
||
def freeform; tag_output(params[:term], "freeform"); end
|
||
|
||
|
||
## TAGS IN FANDOMS
|
||
private
|
||
def tag_in_fandom_output(params)
|
||
render_output(Tag.autocomplete_fandom_lookup(params).map {|r| Tag.name_from_autocomplete(r)})
|
||
end
|
||
public
|
||
def character_in_fandom; tag_in_fandom_output(params.merge({tag_type: "character"})); end
|
||
def relationship_in_fandom; tag_in_fandom_output(params.merge({tag_type: "relationship"})); end
|
||
|
||
|
||
## TAGS IN SETS
|
||
#
|
||
# Note that only tagsets in OwnedTagSets are in autocomplete
|
||
#
|
||
|
||
# expects the following params:
|
||
# :tag_set - tag set ids comma-separated
|
||
# :tag_type - tag type as a string unless "all" desired
|
||
# :in_any - set to false if only want tags in ALL specified sets
|
||
# :term - the search term
|
||
def tags_in_sets
|
||
results = TagSet.autocomplete_lookup(params)
|
||
render_output(results.map {|r| Tag.name_from_autocomplete(r)})
|
||
end
|
||
|
||
# expects the following params:
|
||
# :fandom - fandom name(s) as an array or a comma-separated string
|
||
# :tag_set - tag set id(s) as an array or a comma-separated string
|
||
# :tag_type - tag type as a string unless "all" desired
|
||
# :include_wrangled - set to false if you only want tags from the set associations and NOT tags wrangled into the fandom
|
||
# :fallback - set to false to NOT do
|
||
# :term - the search term
|
||
def associated_tags
|
||
if params[:fandom].blank?
|
||
render_output([ts("Please select a fandom first!")])
|
||
else
|
||
results = TagSetAssociation.autocomplete_lookup(params)
|
||
render_output(results.map {|r| Tag.name_from_autocomplete(r)})
|
||
end
|
||
end
|
||
|
||
## NONCANONICAL TAGS
|
||
def noncanonical_tag
|
||
search_param = Query.new.escape_reserved_characters(params[:term])
|
||
raise "Redshirt: Attempted to constantize invalid class initialize noncanonical_tag #{params[:type].classify}" unless Tag::TYPES.include?(params[:type].classify)
|
||
|
||
tag_class = params[:type].classify.constantize
|
||
one_tag = tag_class.find_by(canonical: false, name: params[:term]) if params[:term].present?
|
||
# If there is an exact match in the database, ensure it is the first thing suggested.
|
||
match = if one_tag
|
||
[one_tag.name]
|
||
else
|
||
[]
|
||
end
|
||
|
||
# As explained in https://stackoverflow.com/a/54080114, the Elasticsearch suggestion suggester does not support
|
||
# matches in the middle of a series of words. Therefore, we break the autocomplete query into its individual
|
||
# words – based on whitespace – except for the last word, which could be incomplete, so a prefix match is
|
||
# appropriate. This emulates the behavior of SQL `LIKE '%text%'.
|
||
|
||
word_list = search_param.split
|
||
last_word = word_list.pop
|
||
search_list = word_list.map { |w| { term: { name: { value: w, case_insensitive: true } } } } + [{ prefix: { name: { value: last_word, case_insensitive: true } } }]
|
||
begin
|
||
# Size is chosen so we get enough search results from each shard.
|
||
search_results = $elasticsearch.search(
|
||
index: TagIndexer.index_name,
|
||
body: { size: "100", query: { bool: { filter: [{ match: { tag_type: params[:type].capitalize } }, { match: { canonical: false } }], must: search_list } } }
|
||
)
|
||
render_output((match + search_results["hits"]["hits"].first(10).map { |t| t["_source"]["name"] }).uniq)
|
||
rescue Elastic::Transport::Transport::Errors::BadRequest
|
||
render_output(match)
|
||
end
|
||
end
|
||
|
||
# more-specific autocompletes should be added below here when they can't be avoided
|
||
|
||
# look up collections ranked by number of items they contain
|
||
|
||
def collection_fullname
|
||
results = Collection.autocomplete_lookup(search_param: params[:term], autocomplete_prefix: "autocomplete_collection_all").map {|res| Collection.fullname_from_autocomplete(res)}
|
||
render_output(results)
|
||
end
|
||
|
||
# return collection names
|
||
|
||
def open_collection_names
|
||
# in this case we want different ids from names so we can display the title but only put in the name
|
||
results = Collection.autocomplete_lookup(search_param: params[:term], autocomplete_prefix: "autocomplete_collection_open").map do |str|
|
||
{id: (whole_name = Collection.name_from_autocomplete(str)), name: Collection.title_from_autocomplete(str) + " (#{whole_name})" }
|
||
end
|
||
respond_with(results)
|
||
end
|
||
|
||
# For creating collections, autocomplete the name of a parent collection owned by the user only
|
||
def collection_parent_name
|
||
render_output(current_user.maintained_collections.top_level.with_name_like(params[:term]).pluck(:name).sort)
|
||
end
|
||
|
||
# for looking up existing urls for external works to avoid duplication
|
||
def external_work
|
||
render_output(ExternalWork.where(["url LIKE ?", '%' + params[:term] + '%']).limit(10).order(:url).pluck(:url))
|
||
end
|
||
|
||
# the pseuds of the potential matches who could fulfill the requests in the given signup
|
||
def potential_offers
|
||
potential_matches(false)
|
||
end
|
||
|
||
# the pseuds of the potential matches who want the offers in the given signup
|
||
def potential_requests
|
||
potential_matches(true)
|
||
end
|
||
|
||
# Return matching potential requests or offers
|
||
def potential_matches(return_requests=true)
|
||
search_param = params[:term]
|
||
signup_id = params[:signup_id]
|
||
signup = ChallengeSignup.find(signup_id)
|
||
pmatches = return_requests ?
|
||
signup.offer_potential_matches.sort.reverse.map {|pm| pm.request_signup.pseud.byline} :
|
||
signup.request_potential_matches.sort.reverse.map {|pm| pm.offer_signup.pseud.byline}
|
||
pmatches.select! { |pm| pm.match(/#{Regexp.escape(search_param)}/) } if search_param.present?
|
||
render_output(pmatches)
|
||
end
|
||
|
||
# owned tag sets that are usable by all
|
||
def owned_tag_sets
|
||
if params[:term].length > 0
|
||
search_param = '%' + params[:term] + '%'
|
||
render_output(OwnedTagSet.limit(10).order(:title).usable.where("owned_tag_sets.title LIKE ?", search_param).collect(&:title))
|
||
end
|
||
end
|
||
|
||
# skins for parenting
|
||
def site_skins
|
||
if params[:term].present?
|
||
search_param = '%' + params[:term] + '%'
|
||
query = Skin.site_skins.where("title LIKE ?", search_param).limit(15).sort_by_recent
|
||
if logged_in?
|
||
query = query.approved_or_owned_by(current_user)
|
||
else
|
||
query = query.approved_skins
|
||
end
|
||
render_output(query.pluck(:title))
|
||
end
|
||
end
|
||
|
||
# admin posts for translations, formatted as Admin Post Title (Post #id)
|
||
def admin_posts
|
||
if params[:term].present?
|
||
search_param = '%' + params[:term] + '%'
|
||
results = AdminPost.non_translated.where("title LIKE ?", search_param).limit(ArchiveConfig.MAX_RECENT).map do |result|
|
||
{id: (post_id = result.id),
|
||
name: result.title + " (Post ##{post_id})" }
|
||
end
|
||
respond_with(results)
|
||
end
|
||
end
|
||
|
||
def admin_post_tags
|
||
if params[:term].present?
|
||
search_param = '%' + params[:term].strip + '%'
|
||
query = AdminPostTag.where("name LIKE ?", search_param).limit(ArchiveConfig.MAX_RECENT)
|
||
render_output(query.pluck(:name))
|
||
end
|
||
end
|
||
|
||
private
|
||
|
||
# Because of the respond_to :json at the top of the controller, this will return a JSON-encoded
|
||
# response which the autocomplete javascript on the other end should be able to handle :)
|
||
def render_output(result_strings)
|
||
if result_strings.first.is_a?(String)
|
||
respond_with(result_strings.map {|str| {id: str, name: str}})
|
||
else
|
||
respond_with(result_strings)
|
||
end
|
||
end
|
||
|
||
end
|