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 +4 -4
- data/README.md +4 -4
- data/forty_facets.gemspec +1 -0
- data/lib/forty_facets/facet_search.rb +7 -2
- data/lib/forty_facets/filter/belongs_to_filter_definition.rb +2 -2
- data/lib/forty_facets/filter/has_many_filter_definition.rb +74 -0
- data/lib/forty_facets/version.rb +1 -1
- data/lib/forty_facets.rb +1 -0
- data/test/fixtures.rb +110 -0
- data/test/smoke_test.rb +70 -38
- metadata +19 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3667e156a13134b58e742270c9a19671cd6e81b0
|
4
|
+
data.tar.gz: 692413b311dc68334826c7fa9460fd9acab6f403
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
###
|
124
|
+
### What kind of associations can be searched/filtered for?
|
125
125
|
|
126
|
-
|
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
@@ -32,8 +32,13 @@ module FortyFacets
|
|
32
32
|
end
|
33
33
|
|
34
34
|
def facet(model_field, opts = {})
|
35
|
-
|
36
|
-
|
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
|
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
|
-
|
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
|
+
|
data/lib/forty_facets/version.rb
CHANGED
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
|
-
|
11
|
-
|
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
|
-
|
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.
|
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-
|
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
|