18F/e-manifest

View on GitHub
app/search/query_dsl.rb

Summary

Maintainability
A
2 hrs
Test Coverage
require 'elasticsearch/dsl'

module Search
  class QueryDSL
    include Elasticsearch::DSL

    MAX_RESULTS = 100

    attr_reader :params, :user, :query_str

    def initialize(args)
      @user = args[:user]
      @params = args[:params]
      @query_str = @params[:q]
      build_dsl
    end

    def to_hash
      @dsl.to_hash
    end

    def default_operator
      params[:operator] || "and"
    end

    def apply_authz?
      if user == nil
        false
      else
        !(user.epa_data_download? || user.state_data_download?)
      end
    end

    def apply_state_authz?
      user && user.state_data_download_states.any? && user.state_data_download?
    end

    def apply_public_filter?
      params[:public] || apply_state_authz?
    end

    def composite_query_string
      stringify_clauses [query_str, client_query].select(&:present?)
    end

    def humanized_query_string
      stringify_clauses [query_str, client_query_humanized].select(&:present?)
    end

    def client_query
      fielded = params[:aq]
      munge_fielded_params(fielded) if fielded
      FieldedQuery.new(fielded)
    end

    private

    def stringify_clauses(clauses)
      if clauses.length == 2
        clauses.map { |c| "(#{c})" }.join(" AND ")
      elsif clauses.length == 1
        clauses[0].to_s
      else
        ""
      end
    end

    def client_query_humanized
      client_query.humanized(user.client_model)
    end

    def munge_fielded_params(fielded)
      if fielded[:created_at].present? && fielded[:created_within].present?
        munge_created_at_field(fielded)
      end
      # do not calculate more than once, or when created_at is null
      fielded.delete(:created_within)
    end

    def munge_created_at_field(fielded)
      high_end_range = Time.zone.parse(fielded[:created_at])
      within_parsed = fielded[:created_within].match(/^(\d+) (\w+)/)
      return unless high_end_range && within_parsed
      low_end_range = high_end_range.utc - within_parsed[1].to_i.send(within_parsed[2])
      fielded[:created_at] = "[#{low_end_range} TO #{high_end_range.utc}]"
    end

    def build_dsl
      @dsl = Elasticsearch::DSL::Search::Search.new
      add_query
      add_filter
      add_sort
      add_pagination
    end

    def add_query
      searchdsl = self
      return unless composite_query_string.present?
      @dsl.query = Query.new
      @dsl.query do
        query_string do
          query searchdsl.composite_query_string
          default_operator searchdsl.default_operator
        end
      end
    end

    def add_filter
      bools = build_filters

      if bools.any?
        @dsl.filter = Filter.new
        @dsl.filter.bool do
          bools.each do |should_filter|
            filter_block = should_filter.instance_variable_get(:@block)
            should &filter_block
          end
        end
      end
    end

    def build_filters
      bools = []
      if apply_authz?
        bools.push authz_filter
      end
      if apply_state_authz?
        bools.push state_authz_filter
      end
      if apply_public_filter?
        bools.push public_filter
      end
      bools.flatten
    end

    def authz_filter
      searchdsl = self
      Filter.new do
        term user_id: searchdsl.user.id
      end
    end

    def public_filter
      Filter.new do
        range :created_at do
          lt 'now-90d'
        end
      end
    end

    def state_authz_filter
      searchdsl = self
      state_filters = []
      @user.state_data_download_states.each do |user_state|
        Manifest.state_fields.each do |field|
          state_filters << Filter.new do
            term "content.#{field}" => user_state.downcase
          end
        end
      end
      state_filters
    end

    def add_sort
      if params[:sort]
        @dsl.sort(params[:sort].map{ |pair| [pair.split(':')].to_h })
      end
    end

    def add_pagination
      return if params[:size] == :all
      calculate_from_size if params[:page]
      add_from
      add_size
    end

    def add_from
      if @from
        @dsl.from = @from
      elsif params[:from]
        @dsl.from = params[:from].to_i
      else
        @dsl.from = 0
      end
    end

    def add_size
      if @size
        @dsl.size = @size
      elsif params[:size]
        @dsl.size = params[:size].to_i
      else
        @dsl.size = MAX_RESULTS
      end
    end

    def calculate_from_size
      page = params[:page].to_i
      @size ||= (params[:size] || MAX_RESULTS).to_i
      @from = (page - 1) * @size.to_i
    end
  end
end