Skip to content

Commit

Permalink
Add sql based filters
Browse files Browse the repository at this point in the history
  • Loading branch information
TobiasBales committed Mar 23, 2015
1 parent d67f905 commit 4cdd532
Show file tree
Hide file tree
Showing 6 changed files with 153 additions and 2 deletions.
2 changes: 1 addition & 1 deletion forty_facets.gemspec
Expand Up @@ -24,5 +24,5 @@ Gem::Specification.new do |spec|
spec.add_development_dependency "sqlite3"
spec.add_development_dependency "coveralls"
spec.add_development_dependency "activerecord", "= 4.1.0"
# spec.add_development_dependency "byebug" # travis doenst like byebug
spec.add_development_dependency "byebug" # travis doenst like byebug
end
1 change: 1 addition & 0 deletions lib/forty_facets.rb
Expand Up @@ -10,4 +10,5 @@ module FortyFacets
require "forty_facets/filter/range_filter_definition"
require "forty_facets/filter/text_filter_definition"
require "forty_facets/filter/facet_filter_definition"
require "forty_facets/filter/sql_facet_filter_definition"
require "forty_facets/facet_search"
4 changes: 4 additions & 0 deletions lib/forty_facets/facet_search.rb
Expand Up @@ -40,6 +40,10 @@ def facet(path, opts = {})
definitions << FacetFilterDefinition.new(self, path, opts)
end

def sql_facet(queries, opts = {})
definitions << SqlFacetFilterDefinition.new(self, queries, opts)
end

def orders(name_and_order_options)
@order_definitions = name_and_order_options.to_a.inject([]) {|ods, no| ods << OrderDefinition.new(no.first, no.last)}
end
Expand Down
94 changes: 94 additions & 0 deletions lib/forty_facets/filter/sql_facet_filter_definition.rb
@@ -0,0 +1,94 @@
module FortyFacets
class SqlFacetFilterDefinition < FilterDefinition
attr_reader(:queries)

def initialize(search, queries, opts)
@search = search
@queries = queries
@path = Array(opts[:path]) if opts[:path].present?
@path ||= @queries.keys
@options = opts
end

def request_param
path.join("-")
end

def build_filter(search_instance, param_value)
ScopeFacetFilter.new(self, search_instance, param_value)
end

class ScopeFacetFilter < Filter
def values
@values ||= Array.wrap(value).sort.uniq
end

def build_scope
return Proc.new { |base| base } if empty?

Proc.new do |base|
# intersection of values and definition queries
base.where(selected_queries.values.map do |query|
"(#{query})"
end.join(" OR "))
end
end

def selected
values
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

def facet
query = definition.queries.map do |key, sql_query|
"(#{sql_query}) as #{key}"
end.join(", ")
query += ", count(*) as occurrences"

counts = without.result.reorder("")
.select(query)
.group(definition.queries.keys)
counts.includes_values = []

result = {}

counts.map do |count|
definition.queries.each do |key, _|
result[key] ||= 0
if [1, "1", true].include?(count[key])
result[key] += count.occurrences
end
end
end

result.map do |key, count|
key = key.to_sym
is_selected = selected_queries.keys.include?(key)
FacetValue.new(key, count, is_selected)
end
end

private

def selected_queries
@selected_queries ||= definition.queries.select do |key, _|
values.map(&:to_sym).include? key
end
end
end
end
end
6 changes: 5 additions & 1 deletion test/fixtures.rb
Expand Up @@ -86,6 +86,9 @@ class Movie < ActiveRecord::Base
has_and_belongs_to_many :genres
has_and_belongs_to_many :actors
has_and_belongs_to_many :writers

scope :classics, -> { where("year <= ?", 1980) }
scope :non_classics, -> { where("year > ?", 1980) }
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}
Expand Down Expand Up @@ -133,7 +136,8 @@ class Movie < ActiveRecord::Base

rand = Random.new
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) )
m = Movie.create!(title: title, studio: studios[index % studios.length],
price: rand.rand(20.0), year: (index + 1975))
3.times do
actor = actors[rand(actors.length)]
unless m.actors.include? actor
Expand Down
48 changes: 48 additions & 0 deletions test/smoke_test.rb
Expand Up @@ -23,6 +23,10 @@ class MovieSearch < FortyFacets::FacetSearch
facet [:studio, :country], name: 'Country'
facet [:studio, :status], name: 'Studio status'
facet [:studio, :producers], name: 'Producers'
sql_facet({ classic: "year <= 1980", non_classic: "year > 1980" },
{ name: "Classic", path: :classic })
sql_facet({ classic: "year <= 1980", non_classic: "year > 1980" },
{ name: "Classic" })
text [:studio, :description], name: 'Studio Description'
end

Expand All @@ -33,6 +37,50 @@ def test_it_finds_all_movies
assert_equal Movie.all.size, search.result.size
end

def test_scope_filter
search = MovieSearch.new("search" => {})
assert_equal 40, search.result.size
assert search.filter(:classic).facet.include? FortyFacets::FacetValue.new(:classic, 6, false)
assert search.filter(:classic).facet.include? FortyFacets::FacetValue.new(:non_classic, 34, false)

search = MovieSearch.new("search" => { "classic" => "classic" })
assert_equal 6, search.result.size
assert search.filter(:classic).facet.include? FortyFacets::FacetValue.new(:classic, 6, true)
assert search.filter(:classic).facet.include? FortyFacets::FacetValue.new(:non_classic, 34, false)

search = MovieSearch.new("search" => { "classic" => "non_classic" })
assert_equal 34, search.result.size
assert search.filter(:classic).facet.include? FortyFacets::FacetValue.new(:classic, 6, false)
assert search.filter(:classic).facet.include? FortyFacets::FacetValue.new(:non_classic, 34, true)

search = MovieSearch.new("search" => { "classic" => ["non_classic", "classic"] })
assert_equal 40, search.result.size
assert search.filter(:classic).facet.include? FortyFacets::FacetValue.new(:classic, 6, true)
assert search.filter(:classic).facet.include? FortyFacets::FacetValue.new(:non_classic, 34, true)
end

def test_scope_filter_without_path
search = MovieSearch.new("search" => {})
assert_equal 40, search.result.size
assert search.filter([:classic, :non_classic]).facet.include? FortyFacets::FacetValue.new(:classic, 6, false)
assert search.filter([:classic, :non_classic]).facet.include? FortyFacets::FacetValue.new(:non_classic, 34, false)

search = MovieSearch.new("search" => { "classic-non_classic" => "classic" })
assert_equal 6, search.result.size
assert search.filter([:classic, :non_classic]).facet.include? FortyFacets::FacetValue.new(:classic, 6, true)
assert search.filter([:classic, :non_classic]).facet.include? FortyFacets::FacetValue.new(:non_classic, 34, false)

search = MovieSearch.new("search" => { "classic-non_classic" => "non_classic" })
assert_equal 34, search.result.size
assert search.filter([:classic, :non_classic]).facet.include? FortyFacets::FacetValue.new(:classic, 6, false)
assert search.filter([:classic, :non_classic]).facet.include? FortyFacets::FacetValue.new(:non_classic, 34, true)

search = MovieSearch.new("search" => { "classic-non_classic" => ["non_classic", "classic"] })
assert_equal 40, search.result.size
assert search.filter([:classic, :non_classic]).facet.include? FortyFacets::FacetValue.new(:classic, 6, true)
assert search.filter([:classic, :non_classic]).facet.include? FortyFacets::FacetValue.new(:non_classic, 34, true)
end

def test_text_filter
search = MovieSearch.new({'search' => { 'title' => 'ipsum' }})
assert_equal 1, search.result.size
Expand Down

0 comments on commit 4cdd532

Please sign in to comment.