Noosfero/noosfero

View on GitHub
plugins/pg_search/lib/pg_search_plugin.rb

Summary

Maintainability
D
2 days
Test Coverage
require "noosfero/friendly_mime"

class PgSearchPlugin < Noosfero::Plugin
  include SearchHelper

  def self.plugin_name
    "Postgres Full-Text Search"
  end

  def self.plugin_description
    _("Search engine that uses Postgres Full-Text Search.")
  end

  def stylesheet?
    true
  end

  def search_facets?
    true
  end

  def js_files
    ["search.js", "profile_search_block.js"]
  end

  def find_by_contents(asset, scope, query, paginate_options = {}, params = {})
    scope, facets, periods = active_filters asset, scope, params, query
    { results: scope.paginate(paginate_options), facets: facets, periods: periods }
  end

  def profile_search_block_extra_content(block, params)
    if block.advanced_search
      if params[:controller] == "profile_search"
        # Load facets and periods
        load_facets = true
        scope, facets, periods = active_filters(:articles, block.owner.articles, params)
        facets.reject!  { |f| !block.search_fields.include?(search_field_identifier(f[:name], f[:options].first[:type])) } if facets
        periods.reject! { |k, p| !block.search_fields.include?(search_field_identifier(k, :date)) } if periods
      else
        # Do not load the facets, let the user request so
        load_facets = false
      end

      -> {
        render partial: "profile_search/facets",
               locals: { search_block_facets: facets, search_block_periods: periods, load_facets: load_facets }
      }
    end
  end

  private

    def active_filters(asset, scope, params, query = nil)
      facets  = params[:facets] || {}
      periods = (params[:periods] || default_periods_for(asset)).to_h
      query ||= params[:query] || params[:q]
      block = Block.find_by id: params[:block]

      @asset, @base_scope = asset, scope

      scope = scope.send(params[:order]) if params[:order] && params[:order] != "more_relevant"

      scope = filter_by_periods(scope, periods) if params[:periods].present?
      scope = filter_by_facets(scope, facets) if params[:facets].present?

      if query.present?
        query_scope = @base_scope.pg_search_plugin_search(query)
        # TODO The reorder is necessary to avoid crashes with core scopes chain
        scope = query_scope.where(id: scope.map(&:id)).reorder("")
      end

      facets, facets_periods = facets_options(asset, scope, facets).partition do |f|
        f[:options].any? { |option| option[:type] != :date }
      end

      if facets
        facets_periods = facets_periods.map { |p| [p[:name], { is_metadata: true }] }.to_h
        periods.deep_merge! facets_periods

        if block
          facets.reject!  { |f| !block.search_fields.include?(search_field_identifier(f[:name], f[:options].first[:type])) }
          periods.reject! { |k, p| !block.search_fields.include?(search_field_identifier(k, :date)) }
        end
      end

      [scope, facets, periods]
    end

    def default_periods_for(asset)
      periods = { created_at: nil, updated_at: nil }
      periods[:published_at] = nil if asset == :articles
      periods
    end

    def filter_by_periods(scope, periods)
      periods.each do |attribute, period|
        next if period.blank?

        if !period["is_metadata"].blank? && period["is_metadata"] == "true"
          scope = scope.pg_search_plugin_by_metadata_period(attribute, period["start_date"], period["end_date"])
        else
          period["end_date"] += " 23:59:59" unless period["end_date"].blank?
          scope = scope.send(attribute, period["start_date"], period["end_date"])
        end
      end
      scope
    end

    def filter_by_facets(scope, facets)
      queries = []
      facets.each do |term, values|
        kind, klass = term.split("-")
        if kind == "attribute" || kind == "relation" || kind == "metadata"
          arguments = values.to_h.map { |value, check| value if check == "1" }.compact
          arguments.map! { |argument| argument == " " ? nil : argument }
        else
          next
        end
        facet_slug = klass.split("/").last
        arguments.each do |argument|
          if kind == "attribute"
            queries << scope.base_class.send("pg_search_plugin_by_attribute", facet_slug, argument).to_sql
          elsif kind == "relation"
            queries << scope.base_class.send("pg_search_plugin_by_#{facet_slug}", argument).to_sql
          elsif kind == "metadata"
            queries << scope.base_class.send("pg_search_plugin_by_metadata", facet_slug, argument).to_sql
          end
          register_search_facet_occurrence(environment, @asset, kind, facet_slug, argument)
        end
      end
      queries.blank? ? scope : scope.where(id: scope.base_class.find_by_sql(queries.join(" INTERSECT ")))
    end

    def facets_options(asset, scope, selected_facets)
      self.send("#{asset}_facets", scope, selected_facets).compact
    end

    def metadata_facets(klass, scope, selected_facets)
      custom_field_names(klass, scope).map do |name, type|
        metadata_facet(klass, scope, selected_facets, attribute: name.to_sym, type: type.to_sym)
      end
    end

    def articles_facets(scope, selected_facets)
      [
        attribute_facet(Article, scope, selected_facets, attribute: :type),
        attribute_facet(Article, scope, selected_facets, attribute: :content_type),
        relation_facet(Tag, scope, selected_facets),
        relation_facet(Category, scope, selected_facets, filter: :pg_search_plugin_articles_facets, count_filter: "DISTINCT(articles_categories.article_id)"),
        relation_facet(Region, scope, selected_facets, filter: :pg_search_plugin_articles_facets, count_filter: "DISTINCT(articles_categories.article_id)"),
        metadata_facets(Article, scope, selected_facets)
      ].flatten
    end

    def profiles_facets(scope, selected_facets)
      [
        relation_facet(Kind, scope, selected_facets),
        relation_facet(Tag, scope, selected_facets),
        relation_facet(Category, scope, selected_facets, filter: :pg_search_plugin_profiles_facets, count_filter: "DISTINCT(categories_profiles.profile_id)"),
        relation_facet(Region, scope, selected_facets, filter: :pg_search_plugin_profiles_facets, count_filter: "DISTINCT(categories_profiles.profile_id)"),
      ]
    end
    alias :people_facets :profiles_facets
    alias :communities_facets :profiles_facets
    alias :enterprises_facets :profiles_facets

    def method_missing(method, *args, &block)
      if method.to_s =~ /^(.+)_facets$/
        []
      else
        super
      end
    end

    def attribute_facet(klass, scope, selected_facets, params = {})
      generic_facet(klass, scope, selected_facets, :attribute, params)
    end

    def relation_facet(klass, scope, selected_facets, params = { filter: :pg_search_plugin_facets })
      generic_facet(klass, scope, selected_facets, :relation, params)
    end

    def metadata_facet(klass, scope, selected_facets, params = {})
      generic_facet(klass, scope, selected_facets, :metadata, params)
    end

    def generic_facet(klass, scope, selected_facets, kind, params = {})
      no_results = false
      results = self.send("#{kind}_results", klass, scope, params)
      if results.blank?
        no_results = true
        results = self.send("#{kind}_results", klass, @base_scope, params)
      end

      identifier = self.send("#{kind}_identifier", klass, params)
      options = results.map do |result|
        value = result[:value].blank? ? " " : result[:value].to_s
        name = self.send("#{kind}_option_name", result[:name], klass, params)
        enabled = selected_facets[identifier] && selected_facets[identifier][value] == "1"
        count = no_results ? 0 : result[:count]
        { label: name, value: value, count: count, enabled: enabled, identifier: identifier, type: params[:type], metadata: (kind == :metadata) }
      end.compact

      return if options.blank?

      { name: self.send("#{kind}_label", klass, params), options: options }
    end

    def attribute_identifier(klass, params)
      "attribute-#{params[:attribute]}"
    end

    def attribute_label(klass, params)
      case params[:attribute]
      when :content_type
        :extension
      when :type
        :content_type
      else
        params[:attribute]
      end.to_s.humanize.pluralize
    end

    def attribute_option_name(name, klass, params)
      return nil if name.blank?

      if params[:attribute].to_s == "content_type"
        Noosfero::FriendlyMIME.find(name)[1..-1].upcase
      elsif params[:attribute].to_s == "type"
        begin
          name.constantize.short_description
        rescue
          name
        end
      else
        name
      end
    end

    def attribute_results(klass, scope, params)
      results = klass.pg_search_plugin_attribute_facets(scope, params[:attribute]).count(:id)
      results.map do |name, count|
        { name: name, value: name, count: count }
      end
    end

    def relation_identifier(klass, params)
      "relation-#{klass.name.underscore}"
    end

    def relation_label(klass, params)
      klass.name.split("::").last.pluralize
    end

    def relation_option_name(name, klass, params)
      name
    end

    def relation_results(klass, scope, params)
      count_filter = params[:count_filter] || "#{klass.table_name}.*"
      results = klass.send(params[:filter], scope)
                     .select("#{klass.table_name}.*, count(#{count_filter}) as counts")
                     .order("counts DESC")

      results.map do |result|
        { name: relation_result_label(result), value: result.id, count: result.counts }
      end
    end

    def relation_result_label_for_category(result)
      result.full_name(" &rarr; ").html_safe
    end

    def relation_result_label(result)
      klass = result.class
      loop do
        method_name = "relation_result_label_for_#{klass.name.demodulize.underscore}"
        begin
          return self.send(method_name, result)
        rescue NoMethodError
          klass = klass.superclass
          return result.name if klass == ActiveRecord::Base
        end
      end
    end

    def metadata_identifier(klass, params)
      "metadata-#{params[:attribute]}"
    end

    def metadata_option_name(option_name, klass, params)
      if params[:type] == :boolean
        option_name == "1" ? _("True") : _("False")
      else
        option_name
      end
    end

    def metadata_label(klass, params)
      params[:attribute].to_s.humanize
    end

    def metadata_results(klass, scope, params)
      results = custom_field_values(klass, scope, params)
      results.map do |name, count|
        { name: name, value: name, count: count }
      end
    end

    def register_search_facet_occurrence(environment, asset, kind, facet_slug, argument)
      occurrence = PgSearchPlugin::SearchFacetOccurrence.new(environment: environment, asset: asset)
      case kind
      when "attribute"
        occurrence.attribute_name = facet_slug
        occurrence.value = argument
      when "relation"
        klass_name = facet_slug.classify
        occurrence.target = klass_name.constantize.where(id: argument).first
      else
        return
      end
      occurrence.save!
      occurrence
    end

    def translations
      _("Created at")
      _("Updated at")
      _("Extensions")
      _("Content types")
      _("Tags")
      _("Categories")
    end
end