fortytools/forty_facets

View on GitHub
lib/forty_facets/filter/facet_filter_definition.rb

Summary

Maintainability
A
25 mins
Test Coverage
# frozen_string_literal: true

module FortyFacets
  class FacetFilterDefinition < FilterDefinition
    class FacetFilter < Filter
      def values
        @values ||= Array.wrap(value).sort.uniq
      end

      protected

      def order_facet!(facet)
        order_accessor = definition.options[:order]
        if order_accessor
          if order_accessor.is_a?(Proc)
            facet.sort_by! { |facet_value| order_accessor.call(facet_value.entity) }
          else
            facet.sort_by! { |facet_value| facet_value.entity.send(order_accessor) }
          end
        else
          facet.sort_by! { |facet_value| -facet_value.count }
        end
        facet
      end
    end

    class AssociationFacetFilter < FacetFilter
      def selected
        @selected ||= definition.association.klass.unscoped.find(Array.wrap(values).reject(&:blank?))
      end

      def remove(entity)
        new_params = search_instance.params || {}
        old_values = new_params[definition.request_param]
        old_values.delete(entity.id.to_s)
        new_params.delete(definition.request_param) if old_values.empty?
        search_instance.class.new_unwrapped(new_params, search_instance.root)
      end

      def add(entity)
        new_params = search_instance.params || {}

        old_values = new_params[definition.request_param] ||= []
        old_values << entity.id.to_s
        search_instance.class.new_unwrapped(new_params, search_instance.root)
      end
    end

    class AttributeFilter < FacetFilter
      def selected
        entity = definition.origin_class
        column = entity.columns_hash[definition.attribute.to_s]
        type = entity.connection.lookup_cast_type_from_column(column)
        values.map { |value| type.serialize(value) }
      end

      def build_scope
        return proc { |base| base } if empty?

        proc do |base|
          base.joins(definition.joins).where(definition.qualified_column_name => value)
        end
      end

      def facet
        my_column = definition.qualified_column_name
        query = "#{my_column} AS facet_value, count(#{my_column}) AS occurrences"
        counts = without.result(skip_ordering: true).distinct.joins(definition.joins).select(query).group(my_column)
        counts.includes_values = []
        facet = counts.map do |c|
          is_selected = selected.include?(c.facet_value)
          FacetValue.new(c.facet_value, c.occurrences, is_selected)
        end

        order_facet!(facet)
      end

      def remove(value)
        new_params = search_instance.params || {}
        old_values = new_params[definition.request_param]
        old_values.delete(value.to_s)
        new_params.delete(definition.request_param) if old_values.empty?
        search_instance.class.new_unwrapped(new_params, search_instance.root)
      end

      def add(value)
        new_params = search_instance.params || {}
        old_values = new_params[definition.request_param] ||= []
        old_values << value.to_s
        search_instance.class.new_unwrapped(new_params, search_instance.root)
      end
    end

    class BelongsToFilter < AssociationFacetFilter
      def build_scope
        return proc { |base| base } if empty?

        proc do |base|
          base.joins(definition.joins).where(definition.qualified_column_name => values)
        end
      end

      def facet
        my_column = definition.qualified_column_name
        query = "#{my_column} AS foreign_id, count(#{my_column}) AS occurrences"
        counts = without.result(skip_ordering: true).distinct.joins(definition.joins).select(query).group(my_column)
        counts.includes_values = []
        entities_by_id = definition.association.klass.unscoped.find(counts.map(&:foreign_id)).group_by(&:id)

        facet = counts.map do |count|
          facet_entity = entities_by_id[count.foreign_id].first
          is_selected = selected.include?(facet_entity)
          FacetValue.new(facet_entity, count.occurrences, is_selected)
        end

        order_facet!(facet)
      end
    end

    class HasManyFilter < AssociationFacetFilter
      def build_scope
        return proc { |base| base } if empty?

        proc do |base|
          base_table = definition.origin_class.table_name

          primary_key_column = "#{base_table}.#{definition.origin_class.primary_key}"

          matches_from_facet = base.joins(definition.joins).where("#{definition.association.klass.table_name}.#{definition.association.klass.primary_key}" => values).select(primary_key_column)

          base.joins(definition.joins).where(primary_key_column => matches_from_facet)
        end
      end

      def facet
        base_table = definition.search.root_class.table_name
        join_name =  [definition.association.name.to_s, base_table.to_s].sort.join('_')
        foreign_id_col = "#{definition.association.name.to_s.singularize}_id"
        my_column = "#{join_name}.#{foreign_id_col}"
        counts = without.result(skip_ordering: true)
                        .distinct
                        .joins(definition.joins)
                        .select("#{my_column} as foreign_id, count(#{my_column}) as occurrences")
                        .group(my_column)
        counts.includes_values = []
        entities_by_id = definition.association.klass.unscoped.find(counts.map(&:foreign_id)).group_by(&:id)

        facet = counts.map do |count|
          facet_entity = entities_by_id[count.foreign_id].first
          is_selected = selected.include?(facet_entity)
          FacetValue.new(facet_entity, count.occurrences, is_selected)
        end

        order_facet!(facet)
      end
    end

    def build_filter(search_instance, param_value)
      if association
        case association.macro
        when :belongs_to
          BelongsToFilter.new(self, search_instance, param_value)
        when :has_many
          HasManyFilter.new(self, search_instance, param_value)
        when :has_and_belongs_to_many
          HasManyFilter.new(self, search_instance, param_value)
        else
          raise "Unsupported association type: #{association.macro}"
        end
      else
        AttributeFilter.new(self, search_instance, param_value)
      end
    end
  end
end