forty_facets 0.0.7 → 0.0.8

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: 7d9c32f0c9730c55259649e32b7b38909d24da00
4
- data.tar.gz: 0b38e2588247108aa332113e2a50907bf89ca8d1
3
+ metadata.gz: 3667e156a13134b58e742270c9a19671cd6e81b0
4
+ data.tar.gz: 692413b311dc68334826c7fa9460fd9acab6f403
5
5
  SHA512:
6
- metadata.gz: 1260c88aba045ed9c89e1f8209e154b45cbef190f67183e13625705de727dc6bd54c3057075b5d1f4ab31dc93d540475a904cf14868dec24cce1416628e10909
7
- data.tar.gz: de80f703356346a84b8a2c3c776e8a23e70935ad31512e3ad7a94de9bdcc92961b8e78f85937a3ad7d05aa4c9235ceecd833bbbf52bd35a5b801ecf8d10aba95
6
+ metadata.gz: 6344847f3c58519bd965b018910c9146851c5276ebf0a678227b3295f35efc37d4abc9459ad09592ee048521941c8f22e08151de01ad8892d18417c4da0656df
7
+ data.tar.gz: 85db56a26bdc57ba0eded2681c7f9554e397feb175c6250a946a30a7d6b101f25a635f36200cf16371a6550a1e2a989d5821e78367cefaf1c2f5113a628673a6
data/README.md CHANGED
@@ -42,8 +42,8 @@ If you have Movies with a textual title, categotized by genre, studio and year .
42
42
 
43
43
  class Movie < ActiveRecord::Base
44
44
  belongs_to :year
45
- belongs_to :genre
46
45
  belongs_to :studio
46
+ has_and_belongs_to_many :genres
47
47
  end
48
48
 
49
49
  You can then declare the structure of your search like so:
@@ -55,9 +55,9 @@ class HomeController < ApplicationController
55
55
  model 'Movie' # which model to search for
56
56
  text :title # filter by a generic string entered by the user
57
57
  range :price, name: 'Price' # filter by ranges for decimal fields
58
- facet :genre, name: 'Genre' # generate a filter with all values of 'genre' occuring in the result
59
58
  facet :year, name: 'Releaseyear', order: :year # additionally order values in the year field
60
59
  facet :studio, name: 'Studio', order: :name
60
+ facet :genres, name: 'Genre' # generate a filter with all values of 'genre' occuring in the result
61
61
 
62
62
  orders 'Title' => :title,
63
63
  'price, cheap first' => "price asc",
@@ -121,9 +121,9 @@ end
121
121
 
122
122
  ## FAQ
123
123
 
124
- ### Can I create filter for `has_many` associations ?
124
+ ### What kind of associations can be searched/filtered for?
125
125
 
126
- No. At the moment only objects directly related via a `belongs_to` can be used as filter.
126
+ At the moment you can facet for entities mapped via a standard `belongs_to` or `has_and_belongs_to` association.
127
127
 
128
128
  ## Contributing
129
129
 
data/forty_facets.gemspec CHANGED
@@ -24,4 +24,5 @@ Gem::Specification.new do |spec|
24
24
  spec.add_development_dependency "sqlite3"
25
25
  spec.add_development_dependency "coveralls"
26
26
  spec.add_development_dependency "activerecord", ">= 4.0"
27
+ spec.add_development_dependency "byebug"
27
28
  end
@@ -32,8 +32,13 @@ module FortyFacets
32
32
  end
33
33
 
34
34
  def facet(model_field, opts = {})
35
- if self.root_scope.reflect_on_association(model_field)
36
- definitions << BelongsToFilterDefinition.new(self, model_field, opts)
35
+ reflection = self.root_scope.reflect_on_association(model_field)
36
+ if reflection
37
+ if reflection.macro == :belongs_to
38
+ definitions << BelongsToFilterDefinition.new(self, model_field, opts)
39
+ else
40
+ definitions << HasManyFilterDefinition.new(self, model_field, opts)
41
+ end
37
42
  else
38
43
  definitions << AttributeFilterDefinition.new(self, model_field, opts)
39
44
  end
@@ -1,6 +1,6 @@
1
1
  module FortyFacets
2
2
  class BelongsToFilterDefinition < FilterDefinition
3
- class BelonsToFilter < FacetFilter
3
+ class BelongsToFilter < FacetFilter
4
4
  def association
5
5
  filter_definition.search.root_class.reflect_on_association(filter_definition.model_field)
6
6
  end
@@ -51,7 +51,7 @@ module FortyFacets
51
51
  end
52
52
 
53
53
  def build_filter(search_instance, param_value)
54
- BelonsToFilter.new(self, search_instance, param_value)
54
+ BelongsToFilter.new(self, search_instance, param_value)
55
55
  end
56
56
 
57
57
  end
@@ -0,0 +1,74 @@
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)
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)
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
+
@@ -1,3 +1,3 @@
1
1
  module FortyFacets
2
- VERSION = "0.0.7"
2
+ VERSION = "0.0.8"
3
3
  end
data/lib/forty_facets.rb CHANGED
@@ -8,6 +8,7 @@ require "forty_facets/order"
8
8
  require "forty_facets/filter"
9
9
  require "forty_facets/filter_definition"
10
10
  require "forty_facets/filter/belongs_to_filter_definition"
11
+ require "forty_facets/filter/has_many_filter_definition"
11
12
  require "forty_facets/filter/range_filter_definition"
12
13
  require "forty_facets/filter/text_filter_definition"
13
14
  require "forty_facets/filter/attribute_filter_definition"
data/test/fixtures.rb ADDED
@@ -0,0 +1,110 @@
1
+ require 'active_record'
2
+
3
+ ActiveRecord::Migration.verbose = false
4
+ ActiveRecord::Base.logger = Logger.new(nil)
5
+ ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
6
+ ActiveRecord::Base.connection.instance_eval do
7
+
8
+ create_table :studios do |t|
9
+ t.string :name
10
+ end
11
+
12
+ create_table :actors do |t|
13
+ t.string :name
14
+ end
15
+
16
+ create_table :writers do |t|
17
+ t.string :name
18
+ end
19
+
20
+ create_table :genres do |t|
21
+ t.string :name
22
+ end
23
+
24
+ create_table :movies do |t|
25
+ t.integer :studio_id
26
+ t.integer :year
27
+ t.string :title
28
+ t.float :price
29
+ end
30
+
31
+ create_table :actors_movies do |t|
32
+ t.integer :movie_id
33
+ t.integer :actor_id
34
+ end
35
+
36
+ create_table :movies_writers do |t|
37
+ t.integer :movie_id
38
+ t.integer :writer_id
39
+ end
40
+
41
+ create_table :genres_movies do |t|
42
+ t.integer :movie_id
43
+ t.integer :genre_id
44
+ end
45
+
46
+ end
47
+
48
+ class Actor < ActiveRecord::Base
49
+ end
50
+
51
+ class Writer < ActiveRecord::Base
52
+ end
53
+
54
+ class Genre < ActiveRecord::Base
55
+ end
56
+
57
+ class Studio < ActiveRecord::Base
58
+ end
59
+
60
+ class Movie < ActiveRecord::Base
61
+ belongs_to :studio
62
+ has_and_belongs_to_many :genres
63
+ has_and_belongs_to_many :actors
64
+ has_and_belongs_to_many :writers
65
+ end
66
+
67
+ studios = []
68
+ %w{A B C D}.each do |suffix|
69
+ studios << Studio.create!(name: "Studio #{suffix}")
70
+ end
71
+
72
+ genres = []
73
+ %w{horror thriller drama comedy family action documentery}.each do |genre_name|
74
+ genres << Genre.create!(name: genre_name)
75
+ end
76
+
77
+ actors = []
78
+ %w{Matt Julie Tom Brad Tony Dustin Lucy Jenny}.each do |actor_name|
79
+ actors << Actor.create!(name: actor_name)
80
+ end
81
+
82
+ writers = []
83
+ %w{Matt Julie Tom Brad Tony Dustin Lucy Jenny}.each do |writer_name|
84
+ writers << Writer.create!(name: writer_name)
85
+ end
86
+
87
+ rand = Random.new
88
+ %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|
89
+ m = Movie.create!(title: title, studio: studios[index % studios.length], price: rand.rand(20.0), year: (index%3 + 2010) )
90
+ 3.times do
91
+ actor = actors[rand(actors.length)]
92
+ unless m.actors.include? actor
93
+ m.actors << actor
94
+ end
95
+ end
96
+ 3.times do
97
+ writer = writers[rand(writers.length)]
98
+ unless m.writers.include? writer
99
+ m.writers << writer
100
+ end
101
+ end
102
+ rand(6).to_i.times do
103
+ genre = genres[rand(genres.length)]
104
+ unless m.genres.include? genre
105
+ m.genres << genre
106
+ end
107
+ end
108
+ end
109
+
110
+
data/test/smoke_test.rb CHANGED
@@ -2,37 +2,13 @@ require 'coveralls'
2
2
  Coveralls.wear!
3
3
 
4
4
  require "minitest/autorun"
5
- require 'active_record'
6
5
  require 'logger'
6
+ require 'byebug'
7
7
  require_relative '../lib/forty_facets'
8
8
 
9
- silence_warnings do
10
- ActiveRecord::Migration.verbose = false
11
- ActiveRecord::Base.logger = Logger.new(nil)
12
- ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
13
- end
14
-
15
- ActiveRecord::Base.connection.instance_eval do
16
-
17
- create_table :studios do |t|
18
- t.string :name
19
- end
20
-
21
- create_table :movies do |t|
22
- t.integer :studio_id
23
- t.integer :year
24
- t.string :title
25
- t.float :price
26
- end
27
-
28
- end
29
-
30
- class Studio < ActiveRecord::Base
31
- end
32
-
33
- class Movie < ActiveRecord::Base
34
- belongs_to :studio
35
- end
9
+ #silence_warnings do
10
+ require_relative 'fixtures'
11
+ #end
36
12
 
37
13
  class MovieSearch < FortyFacets::FacetSearch
38
14
  model 'Movie'
@@ -40,17 +16,10 @@ class MovieSearch < FortyFacets::FacetSearch
40
16
  text :title, name: 'Title'
41
17
  facet :studio, name: 'Studio'
42
18
  facet :year, order: Proc.new {|year| -year}
19
+ facet :genres, name: 'Genre'
20
+ facet :actors, name: 'Actor'
43
21
  range :price, name: 'Price'
44
- end
45
-
46
- studios = []
47
- %w{A B C D}.each do |suffix|
48
- studios << Studio.create!(name: "Studio #{suffix}")
49
- end
50
-
51
- rand = Random.new
52
- %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|
53
- Movie.create!(title: title, studio: studios[index % studios.length], price: rand.rand(20.0), year: (index%3 + 2010) )
22
+ facet :writers, name: 'Writer'
54
23
  end
55
24
 
56
25
  class SmokeTest < Minitest::Test
@@ -122,4 +91,67 @@ class SmokeTest < Minitest::Test
122
91
  assert_equal Movie.all.map(&:year).sort.uniq.reverse, facet_entities
123
92
  end
124
93
 
94
+ def test_has_many
95
+ blank_search = MovieSearch.new
96
+ genre = Genre.first
97
+ expected = Movie.order(:id).select{|m| m.genres.include?(genre)}
98
+ assert blank_search.filter(:genres).is_a?(FortyFacets::HasManyFilterDefinition::HasManyFilter)
99
+ search = blank_search.filter(:genres).add(genre)
100
+ actual = search.result
101
+
102
+ assert_equal expected.size, actual.size
103
+ end
104
+
105
+ def test_has_many_writers
106
+ blank_search = MovieSearch.new
107
+ writer = Writer.first
108
+ expected = Movie.order(:id).select{|m| m.writers.include?(writer)}
109
+ assert blank_search.filter(:writers).is_a?(FortyFacets::HasManyFilterDefinition::HasManyFilter)
110
+ search = blank_search.filter(:writers).add(writer)
111
+ actual = search.result
112
+
113
+ assert_equal expected.size, actual.size
114
+ end
115
+
116
+ def test_has_many_combo
117
+ blank_search = MovieSearch.new
118
+ genre = Genre.first
119
+ actor = Actor.first
120
+ expected = Movie.order(:id)
121
+ .select{|m| m.genres.include?(genre)}
122
+ .select{|m| m.actors.include?(actor)}
123
+ assert blank_search.filter(:genres).is_a?(FortyFacets::HasManyFilterDefinition::HasManyFilter)
124
+ search_with_genre = blank_search.filter(:genres).add(genre)
125
+ search_with_genre_and_actor = search_with_genre.filter(:actors).add(actor)
126
+ actual = search_with_genre_and_actor.result
127
+
128
+ assert_equal expected.size, actual.size
129
+ end
130
+
131
+ def test_has_many_facet_values_writers
132
+ selected_writer = Writer.first
133
+ search = MovieSearch.new.filter(:writers).add(selected_writer)
134
+
135
+ search.filter(:writers).facet.each do |facet_value|
136
+ writer = facet_value.entity
137
+ expected = Movie.order(:id).select{|m| m.writers.include?(writer)}.count
138
+ assert_equal expected, facet_value.count, "The amount of movies for a writer should match the number indicated in the facet"
139
+ assert_equal writer.id == selected_writer.id, facet_value.selected
140
+ end
141
+
142
+ end
143
+
144
+ def test_has_many_facet_values_genres
145
+ selected_genre = Genre.first
146
+ search = MovieSearch.new.filter(:genres).add(selected_genre)
147
+
148
+ search.filter(:genres).facet.each do |facet_value|
149
+ genre = facet_value.entity
150
+ expected = Movie.order(:id).select{|m| m.genres.include?(genre)}.count
151
+ assert_equal expected, facet_value.count, "The amount of movies for a genre should match the number indicated in the facet"
152
+ assert_equal genre.id == selected_genre.id, facet_value.selected
153
+ end
154
+
155
+ end
156
+
125
157
  end
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.7
4
+ version: 0.0.8
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-06-02 00:00:00.000000000 Z
11
+ date: 2014-06-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -94,6 +94,20 @@ dependencies:
94
94
  - - ">="
95
95
  - !ruby/object:Gem::Version
96
96
  version: '4.0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: byebug
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
97
111
  description: FortyFacets lets you easily build explorative search interfaces based
98
112
  on fields of your active_record models.
99
113
  email:
@@ -115,12 +129,14 @@ files:
115
129
  - lib/forty_facets/filter.rb
116
130
  - lib/forty_facets/filter/attribute_filter_definition.rb
117
131
  - lib/forty_facets/filter/belongs_to_filter_definition.rb
132
+ - lib/forty_facets/filter/has_many_filter_definition.rb
118
133
  - lib/forty_facets/filter/range_filter_definition.rb
119
134
  - lib/forty_facets/filter/text_filter_definition.rb
120
135
  - lib/forty_facets/filter_definition.rb
121
136
  - lib/forty_facets/order.rb
122
137
  - lib/forty_facets/order_definition.rb
123
138
  - lib/forty_facets/version.rb
139
+ - test/fixtures.rb
124
140
  - test/smoke_test.rb
125
141
  - test/test_helper.rb
126
142
  homepage: https://github.com/fortytools/forty_facets
@@ -148,5 +164,6 @@ signing_key:
148
164
  specification_version: 4
149
165
  summary: Library for building facet searches for active_record models
150
166
  test_files:
167
+ - test/fixtures.rb
151
168
  - test/smoke_test.rb
152
169
  - test/test_helper.rb