diff --git a/lib/forty_facets.rb b/lib/forty_facets.rb index ae6ac2e..545c0ab 100644 --- a/lib/forty_facets.rb +++ b/lib/forty_facets.rb @@ -7,10 +7,7 @@ module FortyFacets require "forty_facets/order" require "forty_facets/filter" require "forty_facets/filter_definition" -require "forty_facets/filter/belongs_to_filter_definition" -require "forty_facets/filter/belongs_to_chain_filter_definition" -require "forty_facets/filter/has_many_filter_definition" require "forty_facets/filter/range_filter_definition" require "forty_facets/filter/text_filter_definition" -require "forty_facets/filter/attribute_filter_definition" +require "forty_facets/filter/facet_filter_definition" require "forty_facets/facet_search" diff --git a/lib/forty_facets/facet_search.rb b/lib/forty_facets/facet_search.rb index 5fb88cc..90990f7 100644 --- a/lib/forty_facets/facet_search.rb +++ b/lib/forty_facets/facet_search.rb @@ -19,33 +19,24 @@ class FacetSearch attr_reader :filters, :orders class << self - def model(model_name) - @model_name = model_name + def model(model) + if model.is_a? Class + @root_class = model + else + @root_class = Kernel.const_get(model) + end end - def text(model_field, opts = {}) - definitions << TextFilterDefinition.new(self, model_field, opts) + def text(path, opts = {}) + definitions << TextFilterDefinition.new(self, path, opts) end - def range(model_field, opts = {}) - definitions << RangeFilterDefinition.new(self, model_field, opts) + def range(path, opts = {}) + definitions << RangeFilterDefinition.new(self, path, opts) end - def facet(model_field, opts = {}) - if model_field.is_a? Array - definitions << BelongsToChainFilterDefinition.new(self, model_field, opts) - else - reflection = self.root_class.reflect_on_association(model_field) - if reflection - if reflection.macro == :belongs_to - definitions << BelongsToFilterDefinition.new(self, model_field, opts) - else - definitions << HasManyFilterDefinition.new(self, model_field, opts) - end - else - definitions << AttributeFilterDefinition.new(self, model_field, opts) - end - end + def facet(path, opts = {}) + definitions << FacetFilterDefinition.new(self, path, opts) end def orders(name_and_order_options) @@ -57,8 +48,7 @@ def definitions end def root_class - raise 'No model given' unless @model_name - Kernel.const_get(@model_name) + @root_class || raise('No model class given') end def root_scope @@ -96,7 +86,7 @@ def self.new_unwrapped(params, root) end def filter(filter_name) - filter = @filters.find { |f| f.filter_definition.model_field == filter_name } + filter = @filters.find { |f| f.definition.path == [filter_name].flatten } raise "Unknown filter #{filter_name}" unless filter filter end @@ -119,7 +109,7 @@ def wrapped_params def params params = @filters.inject({}) do |sum, filter| - sum[filter.filter_definition.request_param] = filter.value.dup unless filter.empty? + sum[filter.definition.request_param] = filter.value.dup unless filter.empty? sum end params[:order] = order.definition.request_value if order diff --git a/lib/forty_facets/filter.rb b/lib/forty_facets/filter.rb index ba2645a..41fb50d 100644 --- a/lib/forty_facets/filter.rb +++ b/lib/forty_facets/filter.rb @@ -2,9 +2,12 @@ module FortyFacets # Base class for the objects representing a specific value for a specific # type of filter. Most FilterDefinitions will have their own Filter subclass # to control values for display and rendering to request parameters. - Filter = Struct.new(:filter_definition, :search_instance, :value) do + Filter = Struct.new(:definition, :search_instance, :value) do + + FacetValue = Struct.new(:entity, :count, :selected) + def name - filter_definition.options[:name] || filter_definition.model_field + definition.options[:name] || definition.path.join(' ') end def empty? @@ -16,33 +19,10 @@ def without search = search_instance return search if empty? new_params = search_instance.params || {} - new_params.delete(filter_definition.request_param) + new_params.delete(definition.request_param) search_instance.class.new_unwrapped(new_params, search_instance.root) end end - # Base class for filter with multiple values and grouped facet values - class FacetFilter < Filter - def values - @values ||= Array.wrap(value).sort.uniq - end - - protected - - def order_facet!(facet) - order_accessor = filter_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 end diff --git a/lib/forty_facets/filter/attribute_filter_definition.rb b/lib/forty_facets/filter/attribute_filter_definition.rb deleted file mode 100644 index 5c57e20..0000000 --- a/lib/forty_facets/filter/attribute_filter_definition.rb +++ /dev/null @@ -1,46 +0,0 @@ -module FortyFacets - class AttributeFilterDefinition < FilterDefinition - class AttributeFilter < FacetFilter - def selected - entity = search_instance.class.root_class - column = entity.columns_hash[filter_definition.model_field.to_s] - values.map{|v| column.type_cast(v)} - end - - def build_scope - return Proc.new { |base| base } if empty? - Proc.new { |base| base.where(filter_definition.model_field => value) } - end - - def facet - my_column = filter_definition.model_field - counts = without.result.reorder('').select("#{my_column} AS facet_value, count(#{my_column}) as occurrences").group(my_column) - 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[filter_definition.request_param] - old_values.delete(value.to_s) - new_params.delete(filter_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[filter_definition.request_param] ||= [] - old_values << value.to_s - search_instance.class.new_unwrapped(new_params, search_instance.root) - end - end - - def build_filter(search_instance, value) - AttributeFilter.new(self, search_instance, value) - end - end -end diff --git a/lib/forty_facets/filter/belongs_to_chain_filter_definition.rb b/lib/forty_facets/filter/belongs_to_chain_filter_definition.rb deleted file mode 100644 index 6904055..0000000 --- a/lib/forty_facets/filter/belongs_to_chain_filter_definition.rb +++ /dev/null @@ -1,86 +0,0 @@ -module FortyFacets - class BelongsToChainFilterDefinition < FilterDefinition - class BelongsToChainFilter < FacetFilter - def association - current_association = nil - current_class = filter_definition.search.root_class - - filter_definition.model_field.each do |field| - current_association = current_class.reflect_on_association(field) - current_class = current_association.klass - end - - current_association - end - - # class objects in this filter - def klass - association.klass - end - - def selected - @selected ||= klass.find(values) - end - - def joins - fields = filter_definition.model_field - fields.reverse.drop(1).inject(fields.last) { |a, n| { n => a } } - end - - def build_scope - return Proc.new { |base| base } if empty? - - Proc.new do |base| - condition = {association.klass.table_name => {id: values}} - base.joins(joins).where(condition) - end - end - - def facet - current_association = nil - current_class = filter_definition.search.root_class - filter_definition.model_field.reverse.drop(1).reverse.each do |field| - current_association = current_class.reflect_on_association(field) - current_class = current_association.klass - end - - my_column = "#{current_class.table_name}.#{association.association_foreign_key}" - counts = without.result.reorder('').joins(joins).select("#{my_column} AS foreign_id, count(#{my_column}) AS occurrences").group(my_column) - entities_by_id = klass.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 - - def remove(entity) - new_params = search_instance.params || {} - old_values = new_params[filter_definition.request_param] - old_values.delete(entity.id.to_s) - new_params.delete(filter_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[filter_definition.request_param] ||= [] - old_values << entity.id.to_s - search_instance.class.new_unwrapped(new_params, search_instance.root) - end - - end - - def build_filter(search_instance, param_value) - BelongsToChainFilter.new(self, search_instance, param_value) - end - - def request_param - model_field.join('-') - end - - end -end diff --git a/lib/forty_facets/filter/belongs_to_filter_definition.rb b/lib/forty_facets/filter/belongs_to_filter_definition.rb deleted file mode 100644 index 2f7ad26..0000000 --- a/lib/forty_facets/filter/belongs_to_filter_definition.rb +++ /dev/null @@ -1,58 +0,0 @@ -module FortyFacets - class BelongsToFilterDefinition < FilterDefinition - class BelongsToFilter < FacetFilter - def association - filter_definition.search.root_class.reflect_on_association(filter_definition.model_field) - end - - # class objects in this filter - def klass - association.klass - end - - def selected - @selected ||= klass.find(values) - end - - def build_scope - return Proc.new { |base| base } if empty? - Proc.new { |base| base.where(association.association_foreign_key => values) } - end - - def facet - my_column = "#{filter_definition.search.root_class.table_name}.#{association.association_foreign_key}" - counts = without.result.reorder('').select("#{my_column} as foreign_id, count(#{my_column}) as occurrences").group(my_column) - entities_by_id = klass.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 - - def remove(entity) - new_params = search_instance.params || {} - old_values = new_params[filter_definition.request_param] - old_values.delete(entity.id.to_s) - new_params.delete(filter_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[filter_definition.request_param] ||= [] - old_values << entity.id.to_s - search_instance.class.new_unwrapped(new_params, search_instance.root) - end - - end - - def build_filter(search_instance, param_value) - BelongsToFilter.new(self, search_instance, param_value) - end - - end -end diff --git a/lib/forty_facets/filter/facet_filter_definition.rb b/lib/forty_facets/filter/facet_filter_definition.rb new file mode 100644 index 0000000..fd1b2c8 --- /dev/null +++ b/lib/forty_facets/filter/facet_filter_definition.rb @@ -0,0 +1,163 @@ +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.find(values) + 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] + values.map{|v| column.type_cast(v)} + end + + def build_scope + return Proc.new { |base| base } if empty? + Proc.new { |base| base.joins(definition.joins).where(definition.qualified_column_name => value) } + end + + def facet + my_column = definition.qualified_column_name + query = "#{my_column} AS facet_value, count(#{my_column}) AS occurrences" + counts = without.result.reorder('').joins(definition.joins).select(query).group(my_column) + 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.new { |base| base } if empty? + Proc.new { |base| base.joins(definition.joins).where(definition.qualified_column_name => values) } + end + + def facet + my_column = definition.qualified_column_name + query = "#{my_column} AS foreign_id, count(#{my_column}) AS occurrences" + counts = without.result.reorder('').joins(definition.joins).select(query).group(my_column) + entities_by_id = definition.association.klass.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.new { |base| base } if empty? + Proc.new do |base| + base_table = definition.origin_class.table_name + join_name = [definition.association.name.to_s, base_table.to_s].sort.join('_') + + primary_key_column = "#{base_table}.#{definition.origin_class.primary_key}" + + subquery = base.joins(definition.joins).select(primary_key_column) + .where("#{join_name}.#{definition.association.foreign_key}" => values).uniq + + base.joins(definition.joins).where(primary_key_column => subquery.select(primary_key_column)).uniq + 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 + .reorder('') + .joins(definition.joins) + .select("#{my_column} as foreign_id, count(#{my_column}) as occurrences") + .group(my_column) + entities_by_id = definition.association.klass.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 + if association.macro == :belongs_to + BelongsToFilter.new(self, search_instance, param_value) + elsif association.macro == :has_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 diff --git a/lib/forty_facets/filter/has_many_filter_definition.rb b/lib/forty_facets/filter/has_many_filter_definition.rb deleted file mode 100644 index faa7433..0000000 --- a/lib/forty_facets/filter/has_many_filter_definition.rb +++ /dev/null @@ -1,74 +0,0 @@ -module FortyFacets - class HasManyFilterDefinition < FilterDefinition - class HasManyFilter < FacetFilter - def association - filter_definition.search.root_class.reflect_on_association(filter_definition.model_field) - end - - # class objects in this filter - def klass - association.klass - end - - def selected - @selected ||= klass.find(values) - end - - def build_scope - return Proc.new { |base| base } if empty? - Proc.new do |base| - base_table = filter_definition.search.root_class.table_name - join_name = [association.name.to_s, base_table.to_s].sort.join('_') - foreign_id_col = association.name.to_s.singularize + '_id' - # this will actually generate a subquery - base.where(id: base.joins(association.options[:through]) - .where(join_name + '.' + foreign_id_col => values) - .group(base_table + '.id').select(base_table + '.id')) - end - end - - def facet - base_table = filter_definition.search.root_class.table_name - join_name = [association.name.to_s, base_table.to_s].sort.join('_') - foreign_id_col = association.name.to_s.singularize + '_id' - my_column = join_name + '.' + foreign_id_col - counts = without.result - .reorder('') - .joins(association.options[:through]) - .select("#{my_column} as foreign_id, count(#{my_column}) as occurrences") - .group(my_column) - entities_by_id = klass.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 - - def remove(entity) - new_params = search_instance.params || {} - old_values = new_params[filter_definition.request_param] - old_values.delete(entity.id.to_s) - new_params.delete(filter_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[filter_definition.request_param] ||= [] - old_values << entity.id.to_s - search_instance.class.new_unwrapped(new_params, search_instance.root) - end - - end - - def build_filter(search_instance, param_value) - HasManyFilter.new(self, search_instance, param_value) - end - - end -end - diff --git a/lib/forty_facets/filter/range_filter_definition.rb b/lib/forty_facets/filter/range_filter_definition.rb index d4f05b8..15c3f37 100644 --- a/lib/forty_facets/filter/range_filter_definition.rb +++ b/lib/forty_facets/filter/range_filter_definition.rb @@ -3,7 +3,11 @@ class RangeFilterDefinition < FilterDefinition class RangeFilter < Filter def build_scope return Proc.new { |base| base } if empty? - Proc.new { |base| base.where("#{filter_definition.model_field} >= ? AND #{filter_definition.model_field} <= ? ", min_value, max_value ) } + + Proc.new do |base| + base.joins(definition.joins) + .where("#{definition.qualified_column_name} >= ? AND #{definition.qualified_column_name} <= ? ", min_value, max_value ) + end end def min_value @@ -17,7 +21,7 @@ def max_value end def absolute_interval - @abosultes ||= without.result.reorder('').select("min(#{filter_definition.model_field}) as min, max(#{filter_definition.model_field}) as max").first + @abosultes ||= without.result.reorder('').select("min(#{definition.qualified_column_name}) AS min, max(#{definition.qualified_column_name}) as max").first end def absolute_min diff --git a/lib/forty_facets/filter/text_filter_definition.rb b/lib/forty_facets/filter/text_filter_definition.rb index 24fde8e..e27f6b3 100644 --- a/lib/forty_facets/filter/text_filter_definition.rb +++ b/lib/forty_facets/filter/text_filter_definition.rb @@ -4,11 +4,12 @@ class TextFilter < Filter def build_scope return Proc.new { |base| base } if empty? like_value = expression_value(value) - Proc.new { |base| base.where("#{filter_definition.model_field} like ?", like_value ) } + operator = definition.options[:ignore_case] ? 'ILIKE' : 'LIKE' + Proc.new { |base| base.joins(definition.joins).where("#{definition.qualified_column_name} #{operator} ?", like_value ) } end def expression_value(term) - if filter_definition.options[:prefix] + if definition.options[:prefix] "#{term}%" else "%#{term}%" diff --git a/lib/forty_facets/filter_definition.rb b/lib/forty_facets/filter_definition.rb index b14e031..d471334 100644 --- a/lib/forty_facets/filter_definition.rb +++ b/lib/forty_facets/filter_definition.rb @@ -1,11 +1,57 @@ module FortyFacets # Base class for the classes storing the definition of differently behaving filters - FilterDefinition = Struct.new(:search, :model_field, :options) do + class FilterDefinition - FacetValue = Struct.new(:entity, :count, :selected) + attr(:search, :path, :options, :joins, :table_name, :column_name, + :origin_class, :association, :attribute) + + def initialize search, path, options + @search = search + @path = [path].flatten + @options = options + + init_associations + end def request_param - model_field + path.join('-') + end + + def qualified_column_name + "#{table_name}.#{column_name}" + end + + protected + + # Walk the association path and gather required joins, table names etc. + def init_associations + current_class = search.root_class + current_association = nil + + joins = [] + + path.each do |current_attribute| + current_association = current_class.reflect_on_association(current_attribute) + + if current_attribute == path.last + if current_association + joins << current_attribute + @column_name = current_association.foreign_key + else + @column_name = current_attribute.to_s + end + else + joins << current_attribute + current_class = current_association.klass + end + end + + @table_name = current_class.table_name + @origin_class = current_class + @association = current_association + @attribute = path.last + + @joins = joins.reverse.drop(1).inject(joins.last) { |a, n| { n => a } } end end end diff --git a/lib/forty_facets/version.rb b/lib/forty_facets/version.rb index 86acac9..9b81c7c 100644 --- a/lib/forty_facets/version.rb +++ b/lib/forty_facets/version.rb @@ -1,3 +1,3 @@ module FortyFacets - VERSION = "0.0.10" + VERSION = "0.0.11" end diff --git a/test/fixtures.rb b/test/fixtures.rb index 9fb30e0..5c3810d 100644 --- a/test/fixtures.rb +++ b/test/fixtures.rb @@ -11,6 +11,12 @@ create_table :studios do |t| t.integer :country_id + t.string :status + t.string :name + t.string :description + end + + create_table :producers do |t| t.string :name end @@ -48,6 +54,14 @@ t.integer :genre_id end + create_table :producers_studios do |t| + t.integer :producer_id + t.integer :studio_id + end + +end + +class Producer < ActiveRecord::Base end class Actor < ActiveRecord::Base @@ -64,6 +78,7 @@ class Country < ActiveRecord::Base class Studio < ActiveRecord::Base belongs_to :country + has_and_belongs_to_many :producers end class Movie < ActiveRecord::Base @@ -73,14 +88,32 @@ class Movie < ActiveRecord::Base has_and_belongs_to_many :writers end +LOREM = %w{Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren} + countries = [] %w{US UK}.each do |code| countries << Country.create!(name: code) end +producers = [] +%w(Smith Logan Kelly Anderson Hendricks Bush).each do |name| + producers << Producer.create!(name: name) +end + + studios = [] %w{A B C D}.each_with_index do |suffix, index| - studios << Studio.create!(name: "Studio #{suffix}", country: countries[index % countries.length]) + studio = Studio.create!(name: "Studio #{suffix}", status: %w(active inactive)[index % 2], + country: countries[index % countries.length], description: LOREM.shuffle.take(5).join(' ')) + + 3.times do + producer = producers[rand(producers.length)] + unless studio.producers.include? producer + studio.producers << producer + end + end + + studios << studio end genres = [] @@ -99,7 +132,7 @@ class Movie < ActiveRecord::Base end rand = Random.new -%w{Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren}.each_with_index do |title, index| +LOREM.each_with_index do |title, index| m = Movie.create!(title: title, studio: studios[index % studios.length], price: rand.rand(20.0), year: (index%3 + 2010) ) 3.times do actor = actors[rand(actors.length)] diff --git a/test/smoke_test.rb b/test/smoke_test.rb index 4e0c6f6..881a3ed 100644 --- a/test/smoke_test.rb +++ b/test/smoke_test.rb @@ -21,6 +21,9 @@ class MovieSearch < FortyFacets::FacetSearch range :price, name: 'Price' facet :writers, name: 'Writer' facet [:studio, :country], name: 'Country' + facet [:studio, :status], name: 'Studio status' + facet [:studio, :producers], name: 'Producers' + text [:studio, :description], name: 'Studio Description' end class SmokeTest < Minitest::Test @@ -31,19 +34,35 @@ def test_it_finds_all_movies end def test_text_filter - search = MovieSearch.new({'search' => { title: 'ipsum' }}) + search = MovieSearch.new({'search' => { 'title' => 'ipsum' }}) assert_equal 1, search.result.size assert_equal 'ipsum', search.result.first.title end def test_year_filter - search = MovieSearch.new({'search' => { year: '2011' }}) + search = MovieSearch.new({'search' => { 'year' => '2011' }}) assert_equal [2011], search.result.map(&:year).uniq facet = search.filter(:year).facet assert_equal Movie.count, facet.map(&:count).sum end + def test_range_filter + search = MovieSearch.new({'search' => {'price' => '0 - 20'}}) + assert_equal Movie.count, search.result.size + + search = MovieSearch.new({'search' => {'price' => '0 - 10'}}) + assert_equal Movie.all.reject{|m| m.price > 10}.size, search.result.size + end + + def test_text_filter_via_belongs_to + description = Studio.first.description + search = MovieSearch.new({'search' => { 'studio-description' => description }}) + + assert_equal Movie.all.reject{|m| m.studio.description != description}.size, search.result.size + assert_equal description, search.result.first.studio.description + end + def test_country_filter search = MovieSearch.new('search' => { 'studio-country' => Country.first.id.to_s}) assert_equal [Country.first], search.result.map{|m| m.studio.country}.uniq @@ -57,11 +76,21 @@ def test_country_filter def test_selected_country_filter search = MovieSearch.new('search' => { 'studio-country' => Country.first.id.to_s}) filter = search.filter([:studio, :country]) + assert_equal FortyFacets::FacetFilterDefinition::BelongsToFilter, filter.class assert_equal [Country.first], filter.selected assert_equal Movie.count / 2, filter.facet.reject(&:selected).first.count end + def test_studio_status_filter + search = MovieSearch.new('search' => { 'studio-status' => 'active'}) + assert_equal ['active'], search.result.map{|m| m.studio.status}.uniq + assert_equal Movie.count / 2, search.result.count + + filter = search.filter([:studio, :status]) + assert_equal ['active'], filter.selected + end + def test_year_add_remove_filter search = MovieSearch.new() @@ -114,18 +143,29 @@ def test_has_many blank_search = MovieSearch.new genre = Genre.first expected = Movie.order(:id).select{|m| m.genres.include?(genre)} - assert blank_search.filter(:genres).is_a?(FortyFacets::HasManyFilterDefinition::HasManyFilter) + assert blank_search.filter(:genres).is_a?(FortyFacets::FacetFilterDefinition::HasManyFilter) search = blank_search.filter(:genres).add(genre) actual = search.result assert_equal expected.size, actual.size end + def test_hast_many_via_belongs_to + blank_search = MovieSearch.new + producer = Producer.first + expected = Movie.order(:id).select{|m| m.studio.producers.include? producer} + assert blank_search.filter([:studio, :producers]).is_a?(FortyFacets::FacetFilterDefinition::HasManyFilter) + search = blank_search.filter([:studio, :producers]).add(producer) + actual = search.result + + assert_equal expected.size, actual.size + end + def test_has_many_writers blank_search = MovieSearch.new writer = Writer.first expected = Movie.order(:id).select{|m| m.writers.include?(writer)} - assert blank_search.filter(:writers).is_a?(FortyFacets::HasManyFilterDefinition::HasManyFilter) + assert blank_search.filter(:writers).is_a?(FortyFacets::FacetFilterDefinition::HasManyFilter) search = blank_search.filter(:writers).add(writer) actual = search.result @@ -139,7 +179,7 @@ def test_has_many_combo expected = Movie.order(:id) .select{|m| m.genres.include?(genre)} .select{|m| m.actors.include?(actor)} - assert blank_search.filter(:genres).is_a?(FortyFacets::HasManyFilterDefinition::HasManyFilter) + assert blank_search.filter(:genres).is_a?(FortyFacets::FacetFilterDefinition::HasManyFilter) search_with_genre = blank_search.filter(:genres).add(genre) search_with_genre_and_actor = search_with_genre.filter(:actors).add(actor) actual = search_with_genre_and_actor.result