otwarchive-symphonyarchive/app/controllers/autocomplete_controller.rb
2026-03-11 22:22:11 +00:00

224 lines
8.9 KiB
Ruby
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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