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