app/controllers/search_controller.rb

Summary

Maintainability
F
5 days
Test Coverage
class SearchController < ApplicationController
  # Walter McGinnis, 2008-02-07
  # search forms never add anything to db
  # so don't need csrf protection, which is problematic with search forms
  # in kete
  skip_before_filter :verify_authenticity_token

  # James - 2008-09-03
  # Check for access before running private searches
  before_filter :require_login_if_private_search, only: %i[rss for all]
  before_filter :private_search_authorisation, only: %i[rss for all]

  # RSS caching has now moved to fragement caching
  # for finer grain caching (and to suit a larger number of cases)
  # after_filter :write_rss_cache, :only => [:rss]

  # Reset slideshow object on new searches
  before_filter :reset_slideshow, only: %i[for all]

  # After running a search, store the results in a session
  # for slideshow functionality.
  after_filter :store_results_for_slideshow, only: %i[for all]

  def for
    query = SearchQuery.new(params)

    if query.missing_search_terms?
      flash[:notice] = t('search_controller.for.no_search_terms')
      @scope = SearchPresenter.new(query: query)
      render :for
    end

    relation = Searcher.new(query: query).run
    @scope = SearchPresenter.new(query: query, results: relation)
  end

  def all
    query = SearchQuery.new(params)
    relation = Searcher.new(query: query).all
    @scope = SearchPresenter.new(query: query, results: relation)
  end

  def tagged
    query = SearchQuery.new(params)
    relation = Searcher.new(query: query).tagged
    @scope = SearchPresenter.new(query: query, results: relation)
  end

  def related_to
    query = SearchQuery.new(params)
    relation = Searcher.new(query: query).related_to
    @scope = SearchPresenter.new(query: query, results: relation)
  end

  def contributed_by
    query = SearchQuery.new(params)
    relation = Searcher.new(query: query).contributed_by
    @scope = SearchPresenter.new(query: query, results: relation)
  end

  def index
  end

  def list
  end

  def clear
    if params[:search_id].to_i > 0
      clear_users_previous_searches(params[:search_id].to_i)
      flash[:notice] = t('search_controller.clear.selected_search_removed')
      redirect_to action: 'list'
    else
      clear_users_previous_searches
      flash[:notice] = t('search_controller.clear.previous_searches_removed')
      redirect_to '/' # go to the homepage to avoid another search
    end
  end

  # query our ZoomDbs for results, grab only the xml records for the results we need
  # all returns all results for a class, contributor_id, or source_item (i.e. all related items to source)
  # it is the default if the search_terms parameter is not defined
  # def all
  #   @search_terms = params[:search_terms]
  #   if @search_terms.nil?
  #     setup_rss
  #     search
  #   else
  #     # TODO: redirect_to search form of the same url
  #   end

  #   # if zoom class isn't valid, @results is nil,
  #   # so lets rescue with a 404 in this case
  #   rescue_404 if @results.nil?

  #   # if everything went well, lets save this search for the current_user
  #   save_current_search
  # end

  # this action is the action that relies on search_terms being defined
  # it can be thought of as "for/search_terms"
  # def legacy_for
  #   # setup our variables derived from the url
  #   # several of these are valid if nil
  #   @search_terms = params[:search_terms]
  #   if @search_terms.nil?
  #     flash[:notice] = t('search_controller.for.no_search_terms')
  #   else
  #     setup_rss
  #     search
  #   end
  #
  #   # if zoom class isn't valid, @results is nil,
  #   # so lets rescue with a 404 in this case
  #   rescue_404 if @results.nil?
  #
  #   # if everything went well, lets save this search for the current_user
  #   save_current_search
  # end

  # search method now is smart enough to handle rss situation
  # especially now that we have pagination in rss (ur, atom?)
  # def rss
  #   @search_terms = params[:search_terms]
  #
  #   # set up the cache key, which handles our params beyond basket, action, and controller
  #   @cache_key_hash = Hash.new
  #
  #   @cache_key_hash[:page] = (params[:page] || 1).to_i
  #   @cache_key_hash[:number_per_page] = (params[:count] || 50).to_i
  #
  #   @cache_key_hash[:privacy] = "private" if is_a_private_search?
  #
  #   # set the following, if they exist in params
  #   relevant_keys = %w( search_terms_slug search_terms tag contributor limit_to_choice source_controller_singular source_item )
  #   relevant_keys.each do |key|
  #     key = key.to_sym
  #     @cache_key_hash[key] = params[key] unless params[key].blank?
  #   end
  #
  #   @search = Search.new
  #   search
  #
  #   respond_to do |format|
  #     format.xml
  #   end
  # end

  # EOIN: does this method need to be public?
  def search
    @search = Search.new

    # all returns all results for a class, contributor_id, or source_item (i.e. all related items to source)
    # it is the default if the search_terms parameter is not defined
    # however, if search_terms is defined (but not necessarily populated)
    # i.e. search_terms is not nil, but possibly blank
    # it overrides :all
    # in the case of search_terms and contributor_id or source_item both being present
    # the search is done with the limitations of the contributor_id or source_item
    # i.e. search for 'bob smith' within topics related to source_item 'daddy smith'

    @controller_name_for_zoom_class = params[:controller_name_for_zoom_class] || zoom_class_controller(SystemSetting.default_search_class)

    @current_class = zoom_class_from_controller(@controller_name_for_zoom_class)

    @source_controller_singular = params[:source_controller_singular]

    if !@source_controller_singular.nil?
      @source_class = zoom_class_from_controller(@source_controller_singular.pluralize)
      @source_item = Module.class_eval(@source_class).find(params[:source_item])
    else
      @source_class = nil
      @source_item = nil
    end

    @tag = params[:tag] ? Tag.find(params[:tag]) : nil

    @contributor = params[:contributor] ? User.find(params[:contributor]) : nil

    @limit_to_choice = Choice.from_id_or_value(params[:limit_to_choice]) if params[:limit_to_choice]

    @extended_field = ExtendedField.from_id_or_label(params[:extended_field]) if params[:extended_field]
    @all_choices = true unless @extended_field

    @topic_type = TopicType.from_urlified_name(params[:topic_type]).first if params[:topic_type]

    @date_since = params[:date_since].blank? ? nil : params[:date_since]
    @date_until = params[:date_until].blank? ? nil : params[:date_until]

    # calculate where to start and end based on page
    @current_page = params[:page] && params[:page].to_i > 0 ? params[:page].to_i : 1
    @next_page = @current_page + 1
    @previous_page = @current_page - 1

    # rss is always is set at 50 per page unless limit if specified
    if is_rss?
      @number_per_page = (params[:count] || 50).to_i
    else
      # otherwise we fallback to default constant
      # unless user has specifically chosen a different number
      if params[:number_of_results_per_page].blank?
        @number_per_page = session[:number_of_results_per_page] ? session[:number_of_results_per_page].to_i : SystemSetting.default_records_per_page
      else
        @number_per_page = params[:number_of_results_per_page].to_i
      end
    end

    # update session with user preference for number of results per page
    store_number_of_results_per_page unless is_rss?

    # 0 is the first index, so it's valid for start
    @start_record = @number_per_page * @current_page - @number_per_page
    @start = @start_record + 1
    @end_record = @number_per_page * @current_page

    # James Stradling <james@katipo.co.nz> - 2008-05-02
    # Only allow private search if permitted
    @privacy = 'private' if is_a_private_search?

    # Load the correct zoom_db instance and connect to it
    @search.zoom_db = ZoomDb.find_by_database_name(zoom_database)
    @zoom_connection = @search.zoom_db.open_connection

    @result_sets = {}

    # EOIN: put our new search stuff here

    # iterate through all record types and build up a result set for each
    if params[:related_class].nil?
      # rss doesn't use :related_class
      # but is limited to querying one class
      if is_rss?
        populate_result_sets_for(@current_class)
      else
        ZOOM_CLASSES.each do |zoom_class|
          populate_result_sets_for(zoom_class)
        end
      end
    else
      # populate_result_sets_for(relate_to_class)
      populate_result_sets_for(only_valid_zoom_class(params[:related_class]).name)
    end
  end

  def load_results(from_result_set)
    @results = []

    # protect against malformed requests
    # for a start record that is more than the numbers of matching records, return a 404
    # since it only seems to be bots that make the malformed request
    @end_record = from_result_set.size if from_result_set.size < @end_record
    if @start_record > @end_record
      rescue_404
      return false
    end

    if from_result_set.size > 0
      # get the raw xml results from zoom
      raw_results = from_result_set.records_from(
        start_record: @start_record,
        end_record: @end_record
      )
      # create a hash of link, title, description for each record
      raw_results.each do |raw_record|
        result_from_xml_hash = parse_from_xml_in(raw_record)
        @results << result_from_xml_hash
      end
    end
    @results = WillPaginate::Collection.new(
      @current_page,
      @number_per_page,
      from_result_set.size
    ).concat(@results)
  end

  def populate_result_sets_for(zoom_class)
    # potential elements of query
    # zoom_class and optionally basket
    # search_terms which search both title attribute and all content attribute
    # source_item for things related to item
    # tag for things tagged with the tag/subject
    # contributor for things contributed to or created by a user
    # sort_type for last_modified

    # limit query to within our zoom_class
    unless zoom_class == 'Combined'
      @search.pqf_query.kind_is(zoom_class, operator: 'none')
    else
      # we have to put something into this inorder to get results
      @search.pqf_query.kind_is('oai', operator: 'none')
    end

    # limit baskets searched within
    if searching_for_related_items?
      @search.pqf_query.within(authorised_basket_names) if is_a_private_search? && !@site_admin
    else
      @topics_outside_of_this_basket ||= true
      if @current_basket != @site_basket || !@topics_outside_of_this_basket
        @search.pqf_query.within(@current_basket.urlified_name)
      else
        @search.pqf_query.within(authorised_basket_names) if is_a_private_search? && !@site_admin
      end
    end

    # this looks in the dc_relation index in the z30.50 server
    # must be exact string
    # get the item
    # we use should_be_exact rather than _equals_completely method here
    # because relations have a key index on them and should be exact uses that
    @search.pqf_query.relations_include(url_for_dc_identifier(@source_item, { force_http: true, minimal: true }), should_be_exact: true) if !@source_item.nil?

    # this looks in the dc_subject index in the z30.50 server
    @search.pqf_query.subjects_equals_completely(@tag.name.to_s) if !@tag.nil?

    # this should go last because of "or contributor"
    # this looks in the dc_creator and dc_contributors indexes in the z30.50 server
    # must be exact string
    @search.pqf_query.creators_or_contributors_equals_completely("'#{@contributor.login}'") if !@contributor.nil?

    # James
    # Extended Field choice searching mechanisms

    # Handle searching against a specific extended field.
    begin
      dc_element = @extended_field ? @extended_field.xml_element_name.gsub(/^(dc:)/, '') : nil
    rescue
      # We need to handle the case where no xml_element_name has been given.
      dc_element = 'description'
    end

    plural_aliased_dc_methods = %w(relation subject creator contributor)

    if plural_aliased_dc_methods.member?(dc_element)

      # Since these attributes are mapped as plural, we need to ensure we use the correct method in the PqfQuery model.
      @search.pqf_query.send("#{dc_element.pluralize}_include", "':#{@limit_to_choice.value}:'") unless @limit_to_choice.blank?

    elsif PqfQuery::ATTRIBUTE_SPECS.member?(dc_element)
      @search.pqf_query.send("#{dc_element}_include", "':#{@limit_to_choice.value}:'") unless @limit_to_choice.blank?
    else

      # Since the DC attribute is either bogus or non-existent, do the search against all search with demarcated terms
      @search.pqf_query.any_text_include("':#{@limit_to_choice.value}:'") unless @limit_to_choice.blank?
    end

    @search.pqf_query.coverage_equals_completely(@topic_type.name.to_s) if !@topic_type.nil?

    @search.pqf_query.date_on_or_after(parse_date_into_zoom_compatible_format(@date_since, :beginning)) if !@date_since.nil?
    # until means "up to this date" so beginning of year or month is what we want
    # previously this was "on or before", which really meant "through the end of this year or month"
    @search.pqf_query.date_before(parse_date_into_zoom_compatible_format(@date_until, :beginning)) if !@date_until.nil?

    # Normal search terms..

    if !@search_terms.blank?
      # add the actual text search if there are search terms
      @search.pqf_query.title_or_any_text_includes(@search_terms)

      # this make searching for urls work
      @search.pqf_query.add_web_link_specific_query if zoom_class == 'WebLink'
    end

    # Date searching is a special thing,
    # but existing sort params take precendence
    # then search term sorting
    if @date_since.present? || @date_until.present?
      if @search_terms.blank?
        params[:sort_type] ||= 'date'

        # date based search direction has some specific logic
        # depending on combination of parameters
        if @date_since.present?
          # we want oldest first if since is specified
          # either in combination with until
          # or on its own
          params[:sort_direction] ||= 'reverse'
        elsif @date_since.blank? && @date_until.present?
          # we want youngest results if only until is specified
          # as they will be closest to the until parameter date
          params[:sort_direction] ||= nil
        end
      end
    end

    sort_type = @current_basket.setting(:sort_order_default)
    sort_direction = @current_basket.setting(:sort_direction_reversed_default)
    search_sort_type = params[:sort_type].blank? and !sort_type.blank? ? sort_type : params[:sort_type]
    search_sort_direction = params[:sort_type].blank? and !sort_direction.blank? ? sort_direction : params[:sort_direction]
    @search.add_sort_to_query_if_needed(
      user_specified: search_sort_type,
      direction: search_sort_direction,
      action: params[:action],
      search_terms: @search_terms
    )

    logger.debug('what is query: ' + @search.pqf_query.to_s.inspect)

    this_result_set = @search.zoom_db.process_query(query_options)

    @result_sets[zoom_class] = this_result_set

    # results are limited to this page's display of search results, or to the related
    # class, if passed in.
    if zoom_class == @current_class || !params[:related_class].blank?

      # grab them from zoom
      load_results(this_result_set)
    end

    # now that we have results, reset pqf_query
    @search.pqf_query = PqfQuery.new
  end

  def redirect_to_default_all
    redirect_to basket_all_url(controller_name_for_zoom_class: zoom_class_controller(SystemSetting.default_search_class))
  end

  # takes search_terms from form
  # and redirects to .../for/seach-term1-and-search-term2 url
  def terms_to_page_url_redirect
    basket_name =
      params[:target_basket].nil? ? \
           params[:urlified_name] : params[:target_basket]

    controller_name =
      params[:controller_name_for_zoom_class].nil? ? \
           zoom_class_controller(SystemSetting.default_search_class) : params[:controller_name_for_zoom_class]

    location_hash = {
      urlified_name: basket_name,
      controller_name_for_zoom_class: controller_name,
      existing_array_string: params[:existing_array_string],

      # sort_direction is a boolean, so we need to force a blank value if not
      # sent through to ensure the users choice of direction is maintained
      sort_direction: params[:sort_direction] || '',

      sort_type: params[:sort_type],
      limit_to_choice: params[:limit_to_choice],
      extended_field: params[:extended_field],
      authenticity_token: nil
    }

    if is_a_private_search?
      location_hash[:privacy_type] = params[:privacy_type]
    end

    if !params[:search_terms].blank?
      # we are searching
      location_hash.merge!({
                             search_terms_slug: to_search_terms_slug(params[:search_terms]),
                             search_terms: params[:search_terms],
                             action: 'for'
                           })
    else
      # we are viewing all
      location_hash[:action] = 'all'
    end

    # If we're searching by tag, this will be set
    if !params[:tag].blank?
      location_hash[:tag] = params[:tag]
    end

    # If we're searching by contributor, this will be set
    if !params[:contributor].blank?
      location_hash[:contributor] = params[:contributor]
    end

    # If we're searching by relation, these will be set
    if !params[:source_controller_singular].blank?
      location_hash[:source_controller_singular] = params[:source_controller_singular]
    end
    if !params[:source_item].blank?
      location_hash[:source_item] = params[:source_item]
    end

    # James
    # Handle choice specific searching.
    if !params[:limit_to_choice].blank?
      location_hash[:limit_to_choice] = params[:limit_to_choice]
    end
    if !params[:extended_field].blank?
      location_hash.merge({ extended_field: params[:extended_field] })
    end

    if !params[:topic_type].blank?
      location_hash[:topic_type] = params[:topic_type]
    end

    if !params[:date_since].blank?
      location_hash[:date_since] = params[:date_since]
    end
    if !params[:date_until].blank?
      location_hash[:date_until] = params[:date_until]
    end

    logger.debug('terms_to_page_url_redirect hash: ' + location_hash.inspect)

    redirect_to url_for(location_hash)
  end

  def to_search_terms_slug(search_terms)
    # This method should return the following:
    #  quotes phrases:       word_after_word
    #  non-quoted phrases:   word+after+word
    # Join both types with +. i.e.
    #  this 'is quoted' you see -> this+you+see+is_quoted

    # For multi lingual URLs
    require 'unicode'

    # Find all phrases enclosed in quotes and pull them into a flat array of phrases
    search_terms = search_terms.to_s

    # While we need to preserver ampersands in the search terms within single/double quotes,
    # in the search slug, this isn't necessary, and without it, you end up with this-_-that
    # rather than the desired this-and-that, so before splitting it up, convert & to and
    search_terms = search_terms.gsub('&', 'and')

    double_phrases = search_terms.scan(/"(.*?)"/).flatten
    single_phrases = search_terms.scan(/'(.*?)'/).flatten

    # Remove those phrases from the original string
    left_over = search_terms.gsub(/"(.*?)"/, '').squeeze(' ').strip
    left_over = left_over.gsub(/'(.*?)'/, '').squeeze(' ').strip

    # Break up the remaining keywords on whitespace
    keywords = left_over.split(/ /)

    # join everything into one big terms array
    terms = keywords + double_phrases + single_phrases

    # Join each search term with +, change everything that isn't a number into an underscore
    # and cut mulitple underscores in a row down to just one
    slug = terms.join('+').gsub(/[^\w\+_-]/, '_').gsub(/_+/, '_')

    # Lets escape any characters that might cause invalid urls, normalize it, and downcase
    Unicode::normalize_KD(slug.escape).downcase
  end

  # actions for rebuilding search records
  # see filters and permissions for security towards top of code
  include ZoomControllerActions

  # used to choose a topic as homepage for a basket
  # Kieran - 2008-07-07
  # SLOW. Not sure why at this point, but it's 99% rendering, not DB.
  def find_index
    @current_basket = Basket.find(params[:current_basket_id])
    @topics_outside_of_this_basket = false # turn this to true if you want to allow the Site basket
    # homepage to be an About basket topic for example
    @current_homepage = @current_basket.index_topic

    case params[:function]
    when 'find'
      @results = []
      @search_terms = params[:search_terms]
      search unless @search_terms.blank?
      unless @results.empty?
        if !@current_homepage.nil?
          @results.reject! { |result| (result[:id].to_i == @current_homepage.id) }
        end
        @results.collect! { |result| Module.class_eval(result[:class]).find(result[:id]) }
      end
    when 'change'
      @new_homepage_topic = Topic.find(params[:homepage_topic_id])
      version_after_update = @new_homepage_topic.max_version + 1
      @homepage_different = (@current_homepage != @new_homepage_topic)
      # if the homepage isn't different, don't do anything or it mucks up versions, just return true so it passes
      if @homepage_different
        # update the homepage topic. Unfortunatly, the method created new topic versions for both the old and new homepage topics
        # and because the basket doesn't have flagging controls, we can't save it without the version
        # so instead, we have to make sure we assign contributors below, one for the old homepage topic, and one for the new
        # homepage topic, which is taken care of in the method after_successful_zoom_item_update
        @current_basket.update_index_topic(@new_homepage_topic)
        unless @current_homepage.nil?
          # if there was a previous homepage topic, it'll be given a new version by update_index_topic
          # so we need to add a contributor to prevent 500 errors on the history page of the topic
          # we also have to pass in the version + 1, because we are working from an old copy of the previous homepage topic
          # and the version is out of date by this stage, and calling reload on it will load the new homepage topic, not the old
          @current_homepage.add_as_contributor(current_user, (@current_homepage.version + 1))
        end
        # this adds a contributor to the new homepage topic version, and clears the relevant show caches.
        # clearing of the basket homepage index page caches are taken care of in a before filter in application.rb
        after_successful_zoom_item_update(@new_homepage_topic, version_after_update)
        @successful = true
      else
        @successful = true
      end
      if @successful
        flash[:notice] = t('search_controller.find_index.changed')
        # we assign the current_homepage instance var to the new homepage,
        # because its used in the template we populate the search fields and links
        @current_homepage = @new_homepage_topic
      else
        flash[:error] = t('search_controller.find_index.failed')
      end
    end
    render action: 'homepage_topic_form', layout: 'popup_dialog'
  end

  # #related_items
  # * Things we know about this method:
  # * It is hit from a popup window created by clicking on the 'Link existing' in the related items section of a topic.
  #
  # Example URL:
  # http://localhost:3000/en/site/search/find_related?function=add&relate_to_item=2506-herbert-denton&relate_to_type=Topic
  #
  # Params:
  # =======
  #
  # function:
  #     values: 'add'|'remove'|'restore'
  #     * => it seems this method is hit from all the related item popup windows
  # relate_to_item:
  #     a unique id (but not just the database ID) of the item that we are linking **to**
  # relate_to_type:
  #     the name of the class of of item that we are linking **to**
  # related_class:
  #     the name of the type of item we should restrict the search within
  #     optional, it is only present if the user clicked on one of the 'Topic, Image etc.' links in the popup window
  #     * Looking at the code below it seems to default to 'Topic' if it is missing
  # privacy_type
  #   optional, only passed in if user clicked on 'search private' link in popup window
  #   values: nil|'public'|'private'
  # search_terms
  #   optional
  #   * if it exists then this method will do a search, otherwise just show the search form/popup window
  #
  #
  def find_related
    # related items are now shown on basket homepage topics, small change to allow linking here
    params[:relate_to_type] = 'Topic' if params[:relate_to_type] == 'IndexPage'

    @relate_to_item = params[:relate_to_type].constantize.find(params[:relate_to_item])
    @related_class = (params[:related_class] || 'Topic')

    related_class_is_topic = @related_class == 'Topic'
    # this will throw exception if passed in related_class isn't valid
    related_class = only_valid_zoom_class(@related_class)
    related_class_name = related_class.name

    # there is an instance variable for each zoom_class
    # that can be related to a topic through content_item_relations
    # topics related to topics are a special case
    # the method name is called 'related_topics'
    method_name_for_related_items = params[:relate_to_type] == 'Topic' && related_class_is_topic ? 'related_topics' : related_class_name.tableize

    # Look up existing relationships, we use these in 2 out of three functions
    existing = @relate_to_item.send(method_name_for_related_items) unless params[:function] == 'restore'

    case params[:function]
    when 'remove'
      @verb = t('search_controller.find_related.remove')
      @next_action = 'unlink'
      @results = existing
    when 'restore'
      @verb = t('search_controller.find_related.restore')
      @next_action = 'link'

      # Find resulting items through deleted relationships
      @results =
        ContentItemRelation::Deleted.find_all_by_topic_id_and_related_item_type(@relate_to_item, related_class_name) \
                                    .collect { |r| related_class.find(r.related_item_id) }
      if related_class_is_topic
        @results +=
          ContentItemRelation::Deleted.find_all_by_related_item_id_and_related_item_type(@relate_to_item, 'Topic') \
                                      .collect { |r| Topic.find(r.topic_id) }
      end
    when 'add'
      @verb = t('search_controller.find_related.add')
      @next_action = 'link'
      @results = []

      # Run a search if necessary
      # this will update @results
      @search_terms = params[:search_terms]
      if @search_terms.present?
        related_items_search
        current_page = params[:page] && params[:page].to_i > 0 ? params[:page].to_i : 1
        @results = @results.paginate(page: current_page)

        # Store pagination information, we'll need this later
        pagination_methods = [
          'total_entries', 'total_pages', 'current_page',
          'previous_page', 'next_page']

        pagination_methods =
          pagination_methods.inject({}) do |hash, method_name|
            hash[method_name] = @results.send(method_name)
            hash
          end
        # EOIN: pagination_methods is a hash
        #   key = one of ['total_entries', 'total_pages', 'current_page', 'previous_page', 'next_page']
        #   value = the result of sending the method name in the key to the @results collection
      end

      # existing is all one class
      # compare against results ids
      # EOIN: @existing_ids are the Ids of items that we already have a
      #   relationship with. We identify them so we can exclude them from the
      #   results we show the user.
      @existing_ids = existing.collect { |existing_item| existing_item.id }

      # Ensure results do not include already linked items or the current item.
      unless @results.empty?
        # grab result ids to optimize look up of local objects
        valid_result_ids = @results.collect { |result| result[:id].to_i }
        @results = related_class.find(valid_result_ids)

        # Don't include the current topic in the results
        @results.reject! { |obj| obj == @relate_to_item } if params[:relate_to_type] == 'Topic' && related_class_is_topic

        # Define pagination methods in the array again.
        pagination_methods.each_key do |method_name|
          eval \
            "class << @results
              define_method('#{method_name}') do
                #{pagination_methods[method_name]}
              end
            end"
        end
      end
    end

    render action: 'related_form', layout: 'popup_dialog'
  end

  # keep the user's preference for number of results per page
  # stored in a session cookie
  # so they don't have to reset it everytime they go to new search results
  def store_number_of_results_per_page
    session[:number_of_results_per_page] = @number_per_page
  end

  # James - 2008-07-04
  # Specialist method for slideshow paging functionality..
  def slideshow_page_load
    @search_terms = params[:search_terms]

    # Make sure the search method knows which search action we're mimicking.
    params[:action] = slideshow.search_params[:search_action]

    # Run the search
    search

    # Update the slideshow object in session
    store_results_for_slideshow

    # Redirect to the first or last object on the page, depending on the direction we're going..
    url = params[:direction] == 'up' ? session[:slideshow][:results].first : session[:slideshow][:results].last

    # Preserve the image view size when redirecting to the next page.
    url = append_options_to_url(url, "view_size=#{slideshow.image_view_size}") if slideshow.image_view_size
    redirect_to(url)
  end

  def clear_slideshow
    session[:slideshow] = nil
    redirect_to url_for(params[:return_to])
  end

  private

  # James Stradling <james@katipo.co.nz> - 2008-05-02
  # Refactored to use acts_as_zoom#has_appropriate_records?
  #### DEPRECIATED?
  def zoom_update_and_test(item, zoom_db)
    item_class = item.class.name

    if !session[:skip_existing].nil? && (session[:skip_existing] == true)
      # test if it's in there first
      if item.has_appropriate_zoom_records?
        return "skipping existing: search record exists: #{item_class} : #{item.id}"
      end
    end

    # if not, add it
    item.prepare_and_save_to_zoom

    # confirm that it's now available
    if item.has_appropriate_zoom_records?
      return "successfully updated search: #{item_class} : #{item.id}"
    else
      return "failed to add to search: #{item_class} : #{item.id} not found in search index or perhaps the item is pending."
    end
  end

  # Check whether we are searching for candidate related items or not
  def searching_for_related_items?
    params[:controller] == 'search' and params[:action] == 'find_related'
  end

  def searching_for_index_topics?
    params[:controller] == 'search' and params[:action] == 'find_index'
  end

  # James - 2008-07-04
  # Store the elements need to reproduce the search in a session
  def store_results_for_slideshow
    # if @results_sets is emtpy, then @result_sets[@current_class] is nil so we have
    # to stop here if thats the case, or we get a 500 error calling .size below
    return if @result_sets.nil? || @result_sets[@current_class].nil? || @displaying_error

    results = @results.map { |r| r[:url] }

    total_results = @result_sets[@current_class].size

    # We want to retain the original search action name for future use
    altered_params = params
    altered_params[:search_action] = params[:action] unless params[:action] == 'slideshow_page_load'

    if slideshow.results.nil?
      slideshow.search_params = { 'page' => '1' }
    end

    slideshow.results         = results
    slideshow.total           = total_results
    slideshow.total_pages     = @results.total_pages
    slideshow.current_page    = @results.current_page
    slideshow.number_per_page = @number_per_page
    slideshow.search_params   = altered_params

    logger.debug("Stored results for page #{slideshow.current_page}: #{slideshow.results.join(", ")}")
    logger.debug("Original parameters where: #{params.inspect}")
    logger.debug("Storing parameters for search: #{slideshow.search_params.inspect}")
  end

  def reset_slideshow
    session[:slideshow] = nil
  end

  # Walter McGinnis, 2008-09-29
  # separate out login filter to better handle http auth for rss
  def require_login_if_private_search
    if is_a_private_search?
      login_required
    else
      # this is a public search, no login required
      true
    end
  end

  # James - 2008-09-03
  # Check for authorisation when performing private searches
  def private_search_authorisation
    # Allow all public searches
    return true unless is_a_private_search?

    if (@current_basket == @site_basket) && !authorised_basket_names.empty?

      # In the case of the site basket, the only baskets that are searched privately are those
      # which the user is a member of (using the same logic as above).
      # For this reason, no unauthorised searching will take place, so it is safe to continue.

      true
    elsif authorised_basket_names.member?(@current_basket.urlified_name)

      # In the case of a specific, non-site basket, the search is limited to this basket, and
      # we're checking if they're a member here. So, it is safe to continue now.

      true
    else

      # Otherwise, they do not have permission and have probably forgotten to log in.
      logger.info "A user who was #{logged_in? ? "" : "not "}logged was denied access to a private search."

      respond_to do |format|
        format.html do
          redirect_to DEFAULT_REDIRECTION_HASH
        end
        format.xml do
          render text: "<error>#{t('search_controller.private_search_authorisation.forbidden')}</error>", status: 403
        end
      end

    end
  end

  # Check if we are meant to be running a private search #=> Boolean
  def is_a_private_search?
    @private_search ||= params[:privacy_type] == 'private'
  end

  # Which zoom database to use #=> String (public/private)
  def zoom_database
    @zoom_database ||= is_a_private_search? ? 'private' : 'public'
  end

  # Ensure RSS errors are handled with a suitable response
  # For instance, RSS should trigger a http_auth_basic response, while normal searches
  # should trigger a redirect to login page
  def set_xml_format_before_auth
    request.format = :xml
  end

  def is_rss?
    params[:action] == 'rss'
  end

  helper_method :is_rss?

  private

  # set up the results specific rss links
  # we now have combined version of each search/browse results RSS
  # in addition to the zoom class specific rss
  def setup_rss
    # EOIN: TODO: re-enable these sometime
    # @rss_tag_auto = [rss_tag, rss_tag(:combined => true)]
    # @rss_tag_link = [rss_tag(:auto_detect => false), rss_tag(:auto_detect => false, :combined => true)]
  end

  def related_items_search
    # @results
    #   Array
    #   put results in here
    # @search_terms
    #     String
    #     the search term from the user
    # @related_class
    #     String
    #     restrict search to just this content type
    #     will always be present - is set to 'Topic' if user did not specify
    # #is_a_private_search?
    #   returns Bool that says whether we should search private records

    @results = @related_class.constantize.where('title ILIKE ?', "%#{@search_terms}%")

    # NOTE: the return value of this method is ignored
  end

  def authorised_basket_names
    @basket_access_hash.keys.collect { |key| key.to_s }
  end
end