forty_facets 0.0.10 → 0.0.11

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c197c09ed3710813587e8fe9c937a581290470bd
4
- data.tar.gz: 5b26596384c560a48a4bd823f2895afcf540582d
3
+ metadata.gz: 008e646b5375a8907af87b307c754004ce77ff5f
4
+ data.tar.gz: c027d5938ed8a9b6c448d7252664a4b9446c864d
5
5
  SHA512:
6
- metadata.gz: 2ed2ec2abcd495e4bd0c0984f94be6164bc1f196907f07ae71e00ef28e836c43e8175ff65bf5bb1d82aa67fd65c9f6195e41489897784f05a0fa56049650fec9
7
- data.tar.gz: 3b6f68acb231546772f9cc886e651178929b72cdd42dfb0623203a9eddd1f29b62a78103724933d7db3e843fe4dce01a14c67bd0dab7f2d13cb78fe1b9e69e15
6
+ metadata.gz: 29c55e4685443146480d56e722fc8e4aa7ae4a38e981b76ee9058389a0cd00fd5b86fd2313a55898d673cba3fca8e2302e61ba961b5ca441fc05f90097705de8
7
+ data.tar.gz: ebf90e00aff854b1e07e49a4852a4dd10c40034285df247dd2e5ab92ac560b4039b9e48bc77f3c0765868f99757d82e3838cb29623f712c939b85c464ff4f1a4
@@ -19,33 +19,24 @@ module FortyFacets
19
19
  attr_reader :filters, :orders
20
20
 
21
21
  class << self
22
- def model(model_name)
23
- @model_name = model_name
22
+ def model(model)
23
+ if model.is_a? Class
24
+ @root_class = model
25
+ else
26
+ @root_class = Kernel.const_get(model)
27
+ end
24
28
  end
25
29
 
26
- def text(model_field, opts = {})
27
- definitions << TextFilterDefinition.new(self, model_field, opts)
30
+ def text(path, opts = {})
31
+ definitions << TextFilterDefinition.new(self, path, opts)
28
32
  end
29
33
 
30
- def range(model_field, opts = {})
31
- definitions << RangeFilterDefinition.new(self, model_field, opts)
34
+ def range(path, opts = {})
35
+ definitions << RangeFilterDefinition.new(self, path, opts)
32
36
  end
33
37
 
34
- def facet(model_field, opts = {})
35
- if model_field.is_a? Array
36
- definitions << BelongsToChainFilterDefinition.new(self, model_field, opts)
37
- else
38
- reflection = self.root_class.reflect_on_association(model_field)
39
- if reflection
40
- if reflection.macro == :belongs_to
41
- definitions << BelongsToFilterDefinition.new(self, model_field, opts)
42
- else
43
- definitions << HasManyFilterDefinition.new(self, model_field, opts)
44
- end
45
- else
46
- definitions << AttributeFilterDefinition.new(self, model_field, opts)
47
- end
48
- end
38
+ def facet(path, opts = {})
39
+ definitions << FacetFilterDefinition.new(self, path, opts)
49
40
  end
50
41
 
51
42
  def orders(name_and_order_options)
@@ -57,8 +48,7 @@ module FortyFacets
57
48
  end
58
49
 
59
50
  def root_class
60
- raise 'No model given' unless @model_name
61
- Kernel.const_get(@model_name)
51
+ @root_class || raise('No model class given')
62
52
  end
63
53
 
64
54
  def root_scope
@@ -96,7 +86,7 @@ module FortyFacets
96
86
  end
97
87
 
98
88
  def filter(filter_name)
99
- filter = @filters.find { |f| f.filter_definition.model_field == filter_name }
89
+ filter = @filters.find { |f| f.definition.path == [filter_name].flatten }
100
90
  raise "Unknown filter #{filter_name}" unless filter
101
91
  filter
102
92
  end
@@ -119,7 +109,7 @@ module FortyFacets
119
109
 
120
110
  def params
121
111
  params = @filters.inject({}) do |sum, filter|
122
- sum[filter.filter_definition.request_param] = filter.value.dup unless filter.empty?
112
+ sum[filter.definition.request_param] = filter.value.dup unless filter.empty?
123
113
  sum
124
114
  end
125
115
  params[:order] = order.definition.request_value if order
@@ -0,0 +1,163 @@
1
+ module FortyFacets
2
+ class FacetFilterDefinition < FilterDefinition
3
+
4
+ class FacetFilter < Filter
5
+ def values
6
+ @values ||= Array.wrap(value).sort.uniq
7
+ end
8
+
9
+ protected
10
+
11
+ def order_facet!(facet)
12
+ order_accessor = definition.options[:order]
13
+ if order_accessor
14
+ if order_accessor.is_a?(Proc)
15
+ facet.sort_by!{|facet_value| order_accessor.call(facet_value.entity) }
16
+ else
17
+ facet.sort_by!{|facet_value| facet_value.entity.send(order_accessor) }
18
+ end
19
+ else
20
+ facet.sort_by!{|facet_value| -facet_value.count }
21
+ end
22
+ facet
23
+ end
24
+ end
25
+
26
+ class AssociationFacetFilter < FacetFilter
27
+ def selected
28
+ @selected ||= definition.association.klass.find(values)
29
+ end
30
+
31
+ def remove(entity)
32
+ new_params = search_instance.params || {}
33
+ old_values = new_params[definition.request_param]
34
+ old_values.delete(entity.id.to_s)
35
+ new_params.delete(definition.request_param) if old_values.empty?
36
+ search_instance.class.new_unwrapped(new_params, search_instance.root)
37
+ end
38
+
39
+ def add(entity)
40
+ new_params = search_instance.params || {}
41
+
42
+ old_values = new_params[definition.request_param] ||= []
43
+ old_values << entity.id.to_s
44
+ search_instance.class.new_unwrapped(new_params, search_instance.root)
45
+ end
46
+ end
47
+
48
+ class AttributeFilter < FacetFilter
49
+ def selected
50
+ entity = definition.origin_class
51
+ column = entity.columns_hash[definition.attribute.to_s]
52
+ values.map{|v| column.type_cast(v)}
53
+ end
54
+
55
+ def build_scope
56
+ return Proc.new { |base| base } if empty?
57
+ Proc.new { |base| base.joins(definition.joins).where(definition.qualified_column_name => value) }
58
+ end
59
+
60
+ def facet
61
+ my_column = definition.qualified_column_name
62
+ query = "#{my_column} AS facet_value, count(#{my_column}) AS occurrences"
63
+ counts = without.result.reorder('').joins(definition.joins).select(query).group(my_column)
64
+ facet = counts.map do |c|
65
+ is_selected = selected.include?(c.facet_value)
66
+ FacetValue.new(c.facet_value, c.occurrences, is_selected)
67
+ end
68
+
69
+ order_facet!(facet)
70
+ end
71
+
72
+ def remove(value)
73
+ new_params = search_instance.params || {}
74
+ old_values = new_params[definition.request_param]
75
+ old_values.delete(value.to_s)
76
+ new_params.delete(definition.request_param) if old_values.empty?
77
+ search_instance.class.new_unwrapped(new_params, search_instance.root)
78
+ end
79
+
80
+ def add(value)
81
+ new_params = search_instance.params || {}
82
+ old_values = new_params[definition.request_param] ||= []
83
+ old_values << value.to_s
84
+ search_instance.class.new_unwrapped(new_params, search_instance.root)
85
+ end
86
+
87
+ end
88
+
89
+ class BelongsToFilter < AssociationFacetFilter
90
+ def build_scope
91
+ return Proc.new { |base| base } if empty?
92
+ Proc.new { |base| base.joins(definition.joins).where(definition.qualified_column_name => values) }
93
+ end
94
+
95
+ def facet
96
+ my_column = definition.qualified_column_name
97
+ query = "#{my_column} AS foreign_id, count(#{my_column}) AS occurrences"
98
+ counts = without.result.reorder('').joins(definition.joins).select(query).group(my_column)
99
+ entities_by_id = definition.association.klass.find(counts.map(&:foreign_id)).group_by(&:id)
100
+
101
+ facet = counts.map do |count|
102
+ facet_entity = entities_by_id[count.foreign_id].first
103
+ is_selected = selected.include?(facet_entity)
104
+ FacetValue.new(facet_entity, count.occurrences, is_selected)
105
+ end
106
+
107
+ order_facet!(facet)
108
+ end
109
+ end
110
+
111
+ class HasManyFilter < AssociationFacetFilter
112
+ def build_scope
113
+ return Proc.new { |base| base } if empty?
114
+ Proc.new do |base|
115
+ base_table = definition.origin_class.table_name
116
+ join_name = [definition.association.name.to_s, base_table.to_s].sort.join('_')
117
+
118
+ primary_key_column = "#{base_table}.#{definition.origin_class.primary_key}"
119
+
120
+ subquery = base.joins(definition.joins).select(primary_key_column)
121
+ .where("#{join_name}.#{definition.association.foreign_key}" => values).uniq
122
+
123
+ base.joins(definition.joins).where(primary_key_column => subquery.select(primary_key_column)).uniq
124
+ end
125
+ end
126
+
127
+ def facet
128
+ base_table = definition.search.root_class.table_name
129
+ join_name = [definition.association.name.to_s, base_table.to_s].sort.join('_')
130
+ foreign_id_col = definition.association.name.to_s.singularize + '_id'
131
+ my_column = join_name + '.' + foreign_id_col
132
+ counts = without.result
133
+ .reorder('')
134
+ .joins(definition.joins)
135
+ .select("#{my_column} as foreign_id, count(#{my_column}) as occurrences")
136
+ .group(my_column)
137
+ entities_by_id = definition.association.klass.find(counts.map(&:foreign_id)).group_by(&:id)
138
+
139
+ facet = counts.map do |count|
140
+ facet_entity = entities_by_id[count.foreign_id].first
141
+ is_selected = selected.include?(facet_entity)
142
+ FacetValue.new(facet_entity, count.occurrences, is_selected)
143
+ end
144
+
145
+ order_facet!(facet)
146
+ end
147
+ end
148
+
149
+ def build_filter(search_instance, param_value)
150
+ if association
151
+ if association.macro == :belongs_to
152
+ BelongsToFilter.new(self, search_instance, param_value)
153
+ elsif association.macro == :has_many
154
+ HasManyFilter.new(self, search_instance, param_value)
155
+ else
156
+ raise "Unsupported association type: #{association.macro}"
157
+ end
158
+ else
159
+ AttributeFilter.new(self, search_instance, param_value)
160
+ end
161
+ end
162
+ end
163
+ end
@@ -3,7 +3,11 @@ module FortyFacets
3
3
  class RangeFilter < Filter
4
4
  def build_scope
5
5
  return Proc.new { |base| base } if empty?
6
- Proc.new { |base| base.where("#{filter_definition.model_field} >= ? AND #{filter_definition.model_field} <= ? ", min_value, max_value ) }
6
+
7
+ Proc.new do |base|
8
+ base.joins(definition.joins)
9
+ .where("#{definition.qualified_column_name} >= ? AND #{definition.qualified_column_name} <= ? ", min_value, max_value )
10
+ end
7
11
  end
8
12
 
9
13
  def min_value
@@ -17,7 +21,7 @@ module FortyFacets
17
21
  end
18
22
 
19
23
  def absolute_interval
20
- @abosultes ||= without.result.reorder('').select("min(#{filter_definition.model_field}) as min, max(#{filter_definition.model_field}) as max").first
24
+ @abosultes ||= without.result.reorder('').select("min(#{definition.qualified_column_name}) AS min, max(#{definition.qualified_column_name}) as max").first
21
25
  end
22
26
 
23
27
  def absolute_min
@@ -4,11 +4,12 @@ module FortyFacets
4
4
  def build_scope
5
5
  return Proc.new { |base| base } if empty?
6
6
  like_value = expression_value(value)
7
- Proc.new { |base| base.where("#{filter_definition.model_field} like ?", like_value ) }
7
+ operator = definition.options[:ignore_case] ? 'ILIKE' : 'LIKE'
8
+ Proc.new { |base| base.joins(definition.joins).where("#{definition.qualified_column_name} #{operator} ?", like_value ) }
8
9
  end
9
10
 
10
11
  def expression_value(term)
11
- if filter_definition.options[:prefix]
12
+ if definition.options[:prefix]
12
13
  "#{term}%"
13
14
  else
14
15
  "%#{term}%"
@@ -2,9 +2,12 @@ module FortyFacets
2
2
  # Base class for the objects representing a specific value for a specific
3
3
  # type of filter. Most FilterDefinitions will have their own Filter subclass
4
4
  # to control values for display and rendering to request parameters.
5
- Filter = Struct.new(:filter_definition, :search_instance, :value) do
5
+ Filter = Struct.new(:definition, :search_instance, :value) do
6
+
7
+ FacetValue = Struct.new(:entity, :count, :selected)
8
+
6
9
  def name
7
- filter_definition.options[:name] || filter_definition.model_field
10
+ definition.options[:name] || definition.path.join(' ')
8
11
  end
9
12
 
10
13
  def empty?
@@ -16,33 +19,10 @@ module FortyFacets
16
19
  search = search_instance
17
20
  return search if empty?
18
21
  new_params = search_instance.params || {}
19
- new_params.delete(filter_definition.request_param)
22
+ new_params.delete(definition.request_param)
20
23
  search_instance.class.new_unwrapped(new_params, search_instance.root)
21
24
  end
22
25
  end
23
26
 
24
- # Base class for filter with multiple values and grouped facet values
25
- class FacetFilter < Filter
26
- def values
27
- @values ||= Array.wrap(value).sort.uniq
28
- end
29
-
30
- protected
31
-
32
- def order_facet!(facet)
33
- order_accessor = filter_definition.options[:order]
34
- if order_accessor
35
- if order_accessor.is_a?(Proc)
36
- facet.sort_by!{|facet_value| order_accessor.call(facet_value.entity) }
37
- else
38
- facet.sort_by!{|facet_value| facet_value.entity.send(order_accessor) }
39
- end
40
- else
41
- facet.sort_by!{|facet_value| -facet_value.count }
42
- end
43
- facet
44
- end
45
-
46
- end
47
27
  end
48
28
 
@@ -1,11 +1,57 @@
1
1
  module FortyFacets
2
2
  # Base class for the classes storing the definition of differently behaving filters
3
- FilterDefinition = Struct.new(:search, :model_field, :options) do
3
+ class FilterDefinition
4
4
 
5
- FacetValue = Struct.new(:entity, :count, :selected)
5
+ attr(:search, :path, :options, :joins, :table_name, :column_name,
6
+ :origin_class, :association, :attribute)
7
+
8
+ def initialize search, path, options
9
+ @search = search
10
+ @path = [path].flatten
11
+ @options = options
12
+
13
+ init_associations
14
+ end
6
15
 
7
16
  def request_param
8
- model_field
17
+ path.join('-')
18
+ end
19
+
20
+ def qualified_column_name
21
+ "#{table_name}.#{column_name}"
22
+ end
23
+
24
+ protected
25
+
26
+ # Walk the association path and gather required joins, table names etc.
27
+ def init_associations
28
+ current_class = search.root_class
29
+ current_association = nil
30
+
31
+ joins = []
32
+
33
+ path.each do |current_attribute|
34
+ current_association = current_class.reflect_on_association(current_attribute)
35
+
36
+ if current_attribute == path.last
37
+ if current_association
38
+ joins << current_attribute
39
+ @column_name = current_association.foreign_key
40
+ else
41
+ @column_name = current_attribute.to_s
42
+ end
43
+ else
44
+ joins << current_attribute
45
+ current_class = current_association.klass
46
+ end
47
+ end
48
+
49
+ @table_name = current_class.table_name
50
+ @origin_class = current_class
51
+ @association = current_association
52
+ @attribute = path.last
53
+
54
+ @joins = joins.reverse.drop(1).inject(joins.last) { |a, n| { n => a } }
9
55
  end
10
56
  end
11
57
  end
@@ -1,3 +1,3 @@
1
1
  module FortyFacets
2
- VERSION = "0.0.10"
2
+ VERSION = "0.0.11"
3
3
  end
data/lib/forty_facets.rb CHANGED
@@ -7,10 +7,7 @@ require "forty_facets/order_definition"
7
7
  require "forty_facets/order"
8
8
  require "forty_facets/filter"
9
9
  require "forty_facets/filter_definition"
10
- require "forty_facets/filter/belongs_to_filter_definition"
11
- require "forty_facets/filter/belongs_to_chain_filter_definition"
12
- require "forty_facets/filter/has_many_filter_definition"
13
10
  require "forty_facets/filter/range_filter_definition"
14
11
  require "forty_facets/filter/text_filter_definition"
15
- require "forty_facets/filter/attribute_filter_definition"
12
+ require "forty_facets/filter/facet_filter_definition"
16
13
  require "forty_facets/facet_search"
data/test/fixtures.rb CHANGED
@@ -11,6 +11,12 @@ ActiveRecord::Schema.define do
11
11
 
12
12
  create_table :studios do |t|
13
13
  t.integer :country_id
14
+ t.string :status
15
+ t.string :name
16
+ t.string :description
17
+ end
18
+
19
+ create_table :producers do |t|
14
20
  t.string :name
15
21
  end
16
22
 
@@ -48,6 +54,14 @@ ActiveRecord::Schema.define do
48
54
  t.integer :genre_id
49
55
  end
50
56
 
57
+ create_table :producers_studios do |t|
58
+ t.integer :producer_id
59
+ t.integer :studio_id
60
+ end
61
+
62
+ end
63
+
64
+ class Producer < ActiveRecord::Base
51
65
  end
52
66
 
53
67
  class Actor < ActiveRecord::Base
@@ -64,6 +78,7 @@ end
64
78
 
65
79
  class Studio < ActiveRecord::Base
66
80
  belongs_to :country
81
+ has_and_belongs_to_many :producers
67
82
  end
68
83
 
69
84
  class Movie < ActiveRecord::Base
@@ -73,14 +88,32 @@ class Movie < ActiveRecord::Base
73
88
  has_and_belongs_to_many :writers
74
89
  end
75
90
 
91
+ 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}
92
+
76
93
  countries = []
77
94
  %w{US UK}.each do |code|
78
95
  countries << Country.create!(name: code)
79
96
  end
80
97
 
98
+ producers = []
99
+ %w(Smith Logan Kelly Anderson Hendricks Bush).each do |name|
100
+ producers << Producer.create!(name: name)
101
+ end
102
+
103
+
81
104
  studios = []
82
105
  %w{A B C D}.each_with_index do |suffix, index|
83
- studios << Studio.create!(name: "Studio #{suffix}", country: countries[index % countries.length])
106
+ studio = Studio.create!(name: "Studio #{suffix}", status: %w(active inactive)[index % 2],
107
+ country: countries[index % countries.length], description: LOREM.shuffle.take(5).join(' '))
108
+
109
+ 3.times do
110
+ producer = producers[rand(producers.length)]
111
+ unless studio.producers.include? producer
112
+ studio.producers << producer
113
+ end
114
+ end
115
+
116
+ studios << studio
84
117
  end
85
118
 
86
119
  genres = []
@@ -99,7 +132,7 @@ writers = []
99
132
  end
100
133
 
101
134
  rand = Random.new
102
- %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|
135
+ LOREM.each_with_index do |title, index|
103
136
  m = Movie.create!(title: title, studio: studios[index % studios.length], price: rand.rand(20.0), year: (index%3 + 2010) )
104
137
  3.times do
105
138
  actor = actors[rand(actors.length)]
data/test/smoke_test.rb CHANGED
@@ -21,6 +21,9 @@ class MovieSearch < FortyFacets::FacetSearch
21
21
  range :price, name: 'Price'
22
22
  facet :writers, name: 'Writer'
23
23
  facet [:studio, :country], name: 'Country'
24
+ facet [:studio, :status], name: 'Studio status'
25
+ facet [:studio, :producers], name: 'Producers'
26
+ text [:studio, :description], name: 'Studio Description'
24
27
  end
25
28
 
26
29
  class SmokeTest < Minitest::Test
@@ -31,19 +34,35 @@ class SmokeTest < Minitest::Test
31
34
  end
32
35
 
33
36
  def test_text_filter
34
- search = MovieSearch.new({'search' => { title: 'ipsum' }})
37
+ search = MovieSearch.new({'search' => { 'title' => 'ipsum' }})
35
38
  assert_equal 1, search.result.size
36
39
  assert_equal 'ipsum', search.result.first.title
37
40
  end
38
41
 
39
42
  def test_year_filter
40
- search = MovieSearch.new({'search' => { year: '2011' }})
43
+ search = MovieSearch.new({'search' => { 'year' => '2011' }})
41
44
  assert_equal [2011], search.result.map(&:year).uniq
42
45
 
43
46
  facet = search.filter(:year).facet
44
47
  assert_equal Movie.count, facet.map(&:count).sum
45
48
  end
46
49
 
50
+ def test_range_filter
51
+ search = MovieSearch.new({'search' => {'price' => '0 - 20'}})
52
+ assert_equal Movie.count, search.result.size
53
+
54
+ search = MovieSearch.new({'search' => {'price' => '0 - 10'}})
55
+ assert_equal Movie.all.reject{|m| m.price > 10}.size, search.result.size
56
+ end
57
+
58
+ def test_text_filter_via_belongs_to
59
+ description = Studio.first.description
60
+ search = MovieSearch.new({'search' => { 'studio-description' => description }})
61
+
62
+ assert_equal Movie.all.reject{|m| m.studio.description != description}.size, search.result.size
63
+ assert_equal description, search.result.first.studio.description
64
+ end
65
+
47
66
  def test_country_filter
48
67
  search = MovieSearch.new('search' => { 'studio-country' => Country.first.id.to_s})
49
68
  assert_equal [Country.first], search.result.map{|m| m.studio.country}.uniq
@@ -57,11 +76,21 @@ class SmokeTest < Minitest::Test
57
76
  def test_selected_country_filter
58
77
  search = MovieSearch.new('search' => { 'studio-country' => Country.first.id.to_s})
59
78
  filter = search.filter([:studio, :country])
79
+ assert_equal FortyFacets::FacetFilterDefinition::BelongsToFilter, filter.class
60
80
  assert_equal [Country.first], filter.selected
61
81
 
62
82
  assert_equal Movie.count / 2, filter.facet.reject(&:selected).first.count
63
83
  end
64
84
 
85
+ def test_studio_status_filter
86
+ search = MovieSearch.new('search' => { 'studio-status' => 'active'})
87
+ assert_equal ['active'], search.result.map{|m| m.studio.status}.uniq
88
+ assert_equal Movie.count / 2, search.result.count
89
+
90
+ filter = search.filter([:studio, :status])
91
+ assert_equal ['active'], filter.selected
92
+ end
93
+
65
94
  def test_year_add_remove_filter
66
95
 
67
96
  search = MovieSearch.new()
@@ -114,18 +143,29 @@ class SmokeTest < Minitest::Test
114
143
  blank_search = MovieSearch.new
115
144
  genre = Genre.first
116
145
  expected = Movie.order(:id).select{|m| m.genres.include?(genre)}
117
- assert blank_search.filter(:genres).is_a?(FortyFacets::HasManyFilterDefinition::HasManyFilter)
146
+ assert blank_search.filter(:genres).is_a?(FortyFacets::FacetFilterDefinition::HasManyFilter)
118
147
  search = blank_search.filter(:genres).add(genre)
119
148
  actual = search.result
120
149
 
121
150
  assert_equal expected.size, actual.size
122
151
  end
123
152
 
153
+ def test_hast_many_via_belongs_to
154
+ blank_search = MovieSearch.new
155
+ producer = Producer.first
156
+ expected = Movie.order(:id).select{|m| m.studio.producers.include? producer}
157
+ assert blank_search.filter([:studio, :producers]).is_a?(FortyFacets::FacetFilterDefinition::HasManyFilter)
158
+ search = blank_search.filter([:studio, :producers]).add(producer)
159
+ actual = search.result
160
+
161
+ assert_equal expected.size, actual.size
162
+ end
163
+
124
164
  def test_has_many_writers
125
165
  blank_search = MovieSearch.new
126
166
  writer = Writer.first
127
167
  expected = Movie.order(:id).select{|m| m.writers.include?(writer)}
128
- assert blank_search.filter(:writers).is_a?(FortyFacets::HasManyFilterDefinition::HasManyFilter)
168
+ assert blank_search.filter(:writers).is_a?(FortyFacets::FacetFilterDefinition::HasManyFilter)
129
169
  search = blank_search.filter(:writers).add(writer)
130
170
  actual = search.result
131
171
 
@@ -139,7 +179,7 @@ class SmokeTest < Minitest::Test
139
179
  expected = Movie.order(:id)
140
180
  .select{|m| m.genres.include?(genre)}
141
181
  .select{|m| m.actors.include?(actor)}
142
- assert blank_search.filter(:genres).is_a?(FortyFacets::HasManyFilterDefinition::HasManyFilter)
182
+ assert blank_search.filter(:genres).is_a?(FortyFacets::FacetFilterDefinition::HasManyFilter)
143
183
  search_with_genre = blank_search.filter(:genres).add(genre)
144
184
  search_with_genre_and_actor = search_with_genre.filter(:actors).add(actor)
145
185
  actual = search_with_genre_and_actor.result
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: forty_facets
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.10
4
+ version: 0.0.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - Axel Tetzlaff
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-09-18 00:00:00.000000000 Z
11
+ date: 2014-09-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -115,10 +115,7 @@ files:
115
115
  - lib/forty_facets.rb
116
116
  - lib/forty_facets/facet_search.rb
117
117
  - lib/forty_facets/filter.rb
118
- - lib/forty_facets/filter/attribute_filter_definition.rb
119
- - lib/forty_facets/filter/belongs_to_chain_filter_definition.rb
120
- - lib/forty_facets/filter/belongs_to_filter_definition.rb
121
- - lib/forty_facets/filter/has_many_filter_definition.rb
118
+ - lib/forty_facets/filter/facet_filter_definition.rb
122
119
  - lib/forty_facets/filter/range_filter_definition.rb
123
120
  - lib/forty_facets/filter/text_filter_definition.rb
124
121
  - lib/forty_facets/filter_definition.rb
@@ -1,46 +0,0 @@
1
- module FortyFacets
2
- class AttributeFilterDefinition < FilterDefinition
3
- class AttributeFilter < FacetFilter
4
- def selected
5
- entity = search_instance.class.root_class
6
- column = entity.columns_hash[filter_definition.model_field.to_s]
7
- values.map{|v| column.type_cast(v)}
8
- end
9
-
10
- def build_scope
11
- return Proc.new { |base| base } if empty?
12
- Proc.new { |base| base.where(filter_definition.model_field => value) }
13
- end
14
-
15
- def facet
16
- my_column = filter_definition.model_field
17
- counts = without.result.reorder('').select("#{my_column} AS facet_value, count(#{my_column}) as occurrences").group(my_column)
18
- facet = counts.map do |c|
19
- is_selected = selected.include?(c.facet_value)
20
- FacetValue.new(c.facet_value, c.occurrences, is_selected)
21
- end
22
-
23
- order_facet!(facet)
24
- end
25
-
26
- def remove(value)
27
- new_params = search_instance.params || {}
28
- old_values = new_params[filter_definition.request_param]
29
- old_values.delete(value.to_s)
30
- new_params.delete(filter_definition.request_param) if old_values.empty?
31
- search_instance.class.new_unwrapped(new_params, search_instance.root)
32
- end
33
-
34
- def add(value)
35
- new_params = search_instance.params || {}
36
- old_values = new_params[filter_definition.request_param] ||= []
37
- old_values << value.to_s
38
- search_instance.class.new_unwrapped(new_params, search_instance.root)
39
- end
40
- end
41
-
42
- def build_filter(search_instance, value)
43
- AttributeFilter.new(self, search_instance, value)
44
- end
45
- end
46
- end
@@ -1,79 +0,0 @@
1
- module FortyFacets
2
- class BelongsToChainFilterDefinition < FilterDefinition
3
- class BelongsToChainFilter < FacetFilter
4
- def association
5
- current_association = nil
6
- current_class = filter_definition.search.root_class
7
-
8
- filter_definition.model_field.each do |field|
9
- current_association = current_class.reflect_on_association(field)
10
- current_class = current_association.klass
11
- end
12
-
13
- current_association
14
- end
15
-
16
- # class objects in this filter
17
- def klass
18
- association.klass
19
- end
20
-
21
- def selected
22
- @selected ||= klass.find(values)
23
- end
24
-
25
- def joins
26
- fields = filter_definition.model_field
27
- fields.reverse.drop(1).inject(fields.last) { |a, n| { n => a } }
28
- end
29
-
30
- def build_scope
31
- return Proc.new { |base| base } if empty?
32
-
33
- Proc.new do |base|
34
- condition = {association.klass.table_name => {id: values}}
35
- base.joins(joins).where(condition)
36
- end
37
- end
38
-
39
- def facet
40
- my_column = association.association_foreign_key
41
- counts = without.result.reorder('').joins(joins).select("#{my_column} AS foreign_id, count(#{my_column}) AS occurrences").group(my_column)
42
- entities_by_id = klass.find(counts.map(&:foreign_id)).group_by(&:id)
43
-
44
- facet = counts.map do |count|
45
- facet_entity = entities_by_id[count.foreign_id].first
46
- is_selected = selected.include?(facet_entity)
47
- FacetValue.new(facet_entity, count.occurrences, is_selected)
48
- end
49
-
50
- order_facet!(facet)
51
- end
52
-
53
- def remove(entity)
54
- new_params = search_instance.params || {}
55
- old_values = new_params[filter_definition.request_param]
56
- old_values.delete(entity.id.to_s)
57
- new_params.delete(filter_definition.request_param) if old_values.empty?
58
- search_instance.class.new_unwrapped(new_params, search_instance.root)
59
- end
60
-
61
- def add(entity)
62
- new_params = search_instance.params || {}
63
- old_values = new_params[filter_definition.request_param] ||= []
64
- old_values << entity.id.to_s
65
- search_instance.class.new_unwrapped(new_params, search_instance.root)
66
- end
67
-
68
- end
69
-
70
- def build_filter(search_instance, param_value)
71
- BelongsToChainFilter.new(self, search_instance, param_value)
72
- end
73
-
74
- def request_param
75
- model_field.join('-')
76
- end
77
-
78
- end
79
- end
@@ -1,58 +0,0 @@
1
- module FortyFacets
2
- class BelongsToFilterDefinition < FilterDefinition
3
- class BelongsToFilter < FacetFilter
4
- def association
5
- filter_definition.search.root_class.reflect_on_association(filter_definition.model_field)
6
- end
7
-
8
- # class objects in this filter
9
- def klass
10
- association.klass
11
- end
12
-
13
- def selected
14
- @selected ||= klass.find(values)
15
- end
16
-
17
- def build_scope
18
- return Proc.new { |base| base } if empty?
19
- Proc.new { |base| base.where(association.association_foreign_key => values) }
20
- end
21
-
22
- def facet
23
- my_column = association.association_foreign_key
24
- counts = without.result.reorder('').select("#{my_column} as foreign_id, count(#{my_column}) as occurrences").group(my_column)
25
- entities_by_id = klass.find(counts.map(&:foreign_id)).group_by(&:id)
26
-
27
- facet = counts.map do |count|
28
- facet_entity = entities_by_id[count.foreign_id].first
29
- is_selected = selected.include?(facet_entity)
30
- FacetValue.new(facet_entity, count.occurrences, is_selected)
31
- end
32
-
33
- order_facet!(facet)
34
- end
35
-
36
- def remove(entity)
37
- new_params = search_instance.params || {}
38
- old_values = new_params[filter_definition.request_param]
39
- old_values.delete(entity.id.to_s)
40
- new_params.delete(filter_definition.request_param) if old_values.empty?
41
- search_instance.class.new_unwrapped(new_params, search_instance.root)
42
- end
43
-
44
- def add(entity)
45
- new_params = search_instance.params || {}
46
- old_values = new_params[filter_definition.request_param] ||= []
47
- old_values << entity.id.to_s
48
- search_instance.class.new_unwrapped(new_params, search_instance.root)
49
- end
50
-
51
- end
52
-
53
- def build_filter(search_instance, param_value)
54
- BelongsToFilter.new(self, search_instance, param_value)
55
- end
56
-
57
- end
58
- end
@@ -1,74 +0,0 @@
1
- module FortyFacets
2
- class HasManyFilterDefinition < FilterDefinition
3
- class HasManyFilter < FacetFilter
4
- def association
5
- filter_definition.search.root_class.reflect_on_association(filter_definition.model_field)
6
- end
7
-
8
- # class objects in this filter
9
- def klass
10
- association.klass
11
- end
12
-
13
- def selected
14
- @selected ||= klass.find(values)
15
- end
16
-
17
- def build_scope
18
- return Proc.new { |base| base } if empty?
19
- Proc.new do |base|
20
- base_table = filter_definition.search.root_class.table_name
21
- join_name = [association.name.to_s, base_table.to_s].sort.join('_')
22
- foreign_id_col = association.name.to_s.singularize + '_id'
23
- # this will actually generate a subquery
24
- base.where(id: base.joins(association.options[:through])
25
- .where(join_name + '.' + foreign_id_col => values)
26
- .group(base_table + '.id').select(base_table + '.id'))
27
- end
28
- end
29
-
30
- def facet
31
- base_table = filter_definition.search.root_class.table_name
32
- join_name = [association.name.to_s, base_table.to_s].sort.join('_')
33
- foreign_id_col = association.name.to_s.singularize + '_id'
34
- my_column = join_name + '.' + foreign_id_col
35
- counts = without.result
36
- .reorder('')
37
- .joins(association.options[:through])
38
- .select("#{my_column} as foreign_id, count(#{my_column}) as occurrences")
39
- .group(my_column)
40
- entities_by_id = klass.find(counts.map(&:foreign_id)).group_by(&:id)
41
-
42
- facet = counts.map do |count|
43
- facet_entity = entities_by_id[count.foreign_id].first
44
- is_selected = selected.include?(facet_entity)
45
- FacetValue.new(facet_entity, count.occurrences, is_selected)
46
- end
47
-
48
- order_facet!(facet)
49
- end
50
-
51
- def remove(entity)
52
- new_params = search_instance.params || {}
53
- old_values = new_params[filter_definition.request_param]
54
- old_values.delete(entity.id.to_s)
55
- new_params.delete(filter_definition.request_param) if old_values.empty?
56
- search_instance.class.new_unwrapped(new_params, search_instance.root)
57
- end
58
-
59
- def add(entity)
60
- new_params = search_instance.params || {}
61
- old_values = new_params[filter_definition.request_param] ||= []
62
- old_values << entity.id.to_s
63
- search_instance.class.new_unwrapped(new_params, search_instance.root)
64
- end
65
-
66
- end
67
-
68
- def build_filter(search_instance, param_value)
69
- HasManyFilter.new(self, search_instance, param_value)
70
- end
71
-
72
- end
73
- end
74
-