forty_facets 0.0.5 → 0.0.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/Gemfile +1 -0
- data/README.md +3 -2
- data/lib/forty_facets.rb +2 -1
- data/lib/forty_facets/facet_search.rb +6 -2
- data/lib/forty_facets/filter.rb +5 -1
- data/lib/forty_facets/filter/attribute_filter_definition.rb +44 -0
- data/lib/forty_facets/filter/{belongs_to.rb → belongs_to_filter_definition.rb} +6 -17
- data/lib/forty_facets/filter/range_filter_definition.rb +1 -1
- data/lib/forty_facets/filter_definition.rb +3 -0
- data/lib/forty_facets/version.rb +1 -1
- data/test/smoke_test.rb +40 -1
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f4d7a0229978d1d9133b0082e3924554cd5a0cbf
|
4
|
+
data.tar.gz: b054dd26585014ffe9aa8a7851e9d30c4e43d130
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4936450a29cb55d55562e94894db107defda556f29dd3690d3421f893f814a416f76dafeb84d09148375bf405e613f1ec76d104ed248b18bc0aa647f91ea4a56
|
7
|
+
data.tar.gz: f331f6c08b3aab3e1c51e633ed628633dea49ffdfa839dc777e79dc818a7dda41b9cc0f8ddc8a3daef6932705c844bfc3e9b9be8f9603a0d8194a6753eb64a2a
|
data/.gitignore
CHANGED
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -8,7 +8,8 @@ FortyFacets lets you easily build explorative search interfaces based on fields
|
|
8
8
|
|
9
9
|
![demo](demo.gif)
|
10
10
|
|
11
|
-
|
11
|
+
See it implemented in a [example rails application](https://github.com/fortytools/forty_facets_demo) or
|
12
|
+
try a [working demo](http://forty-facets-demo.herokuapp.com/ "Testinstallation on heroku")!
|
12
13
|
|
13
14
|
It offers a simple API to create an interactive UI to browse your data by iteratively adding
|
14
15
|
filter values.
|
@@ -55,7 +56,7 @@ class HomeController < ApplicationController
|
|
55
56
|
text :title # filter by a generic string entered by the user
|
56
57
|
range :price, name: 'Price' # filter by ranges for decimal fields
|
57
58
|
facet :genre, name: 'Genre' # generate a filter with all values of 'genre' occuring in the result
|
58
|
-
facet :year, name: 'Releaseyear', order: :year # additionally
|
59
|
+
facet :year, name: 'Releaseyear', order: :year # additionally order values in the year field
|
59
60
|
facet :studio, name: 'Studio', order: :name
|
60
61
|
|
61
62
|
orders 'Title' => :title,
|
data/lib/forty_facets.rb
CHANGED
@@ -7,7 +7,8 @@ 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/
|
10
|
+
require "forty_facets/filter/belongs_to_filter_definition"
|
11
11
|
require "forty_facets/filter/range_filter_definition"
|
12
12
|
require "forty_facets/filter/text_filter_definition"
|
13
|
+
require "forty_facets/filter/attribute_filter_definition"
|
13
14
|
require "forty_facets/facet_search"
|
@@ -28,11 +28,15 @@ module FortyFacets
|
|
28
28
|
end
|
29
29
|
|
30
30
|
def range(model_field, opts = {})
|
31
|
-
definitions <<
|
31
|
+
definitions << RangeFilterDefinition.new(self, model_field, opts)
|
32
32
|
end
|
33
33
|
|
34
34
|
def facet(model_field, opts = {})
|
35
|
-
definitions <<
|
35
|
+
definitions << BelongsToFilterDefinition.new(self, model_field, opts)
|
36
|
+
end
|
37
|
+
|
38
|
+
def facet_attr(model_field, opts = {})
|
39
|
+
definitions << AttributeFilterDefinition.new(self, model_field, opts)
|
36
40
|
end
|
37
41
|
|
38
42
|
def orders(name_and_order_options)
|
data/lib/forty_facets/filter.rb
CHANGED
@@ -7,6 +7,10 @@ module FortyFacets
|
|
7
7
|
filter_definition.options[:name] || filter_definition.model_field
|
8
8
|
end
|
9
9
|
|
10
|
+
def values
|
11
|
+
@values ||= Array.wrap(value).sort.uniq
|
12
|
+
end
|
13
|
+
|
10
14
|
def empty?
|
11
15
|
value.nil? || value == '' || value == []
|
12
16
|
end
|
@@ -15,7 +19,7 @@ module FortyFacets
|
|
15
19
|
def without
|
16
20
|
search = search_instance
|
17
21
|
return search if empty?
|
18
|
-
new_params = search_instance.params
|
22
|
+
new_params = search_instance.params || {}
|
19
23
|
new_params.delete(filter_definition.request_param)
|
20
24
|
search_instance.class.new_unwrapped(new_params)
|
21
25
|
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module FortyFacets
|
2
|
+
class AttributeFilterDefinition < FilterDefinition
|
3
|
+
class AttributeFilter < Filter
|
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
|
+
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
|
+
end
|
23
|
+
|
24
|
+
def remove(value)
|
25
|
+
new_params = search_instance.params || {}
|
26
|
+
old_values = new_params[filter_definition.request_param]
|
27
|
+
old_values.delete(value.to_s)
|
28
|
+
new_params.delete(filter_definition.request_param) if old_values.empty?
|
29
|
+
search_instance.class.new_unwrapped(new_params)
|
30
|
+
end
|
31
|
+
|
32
|
+
def add(value)
|
33
|
+
new_params = search_instance.params || {}
|
34
|
+
old_values = new_params[filter_definition.request_param] ||= []
|
35
|
+
old_values << value.to_s
|
36
|
+
search_instance.class.new_unwrapped(new_params)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def build_filter(search_instance, value)
|
41
|
+
AttributeFilter.new(self, search_instance, value)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -1,7 +1,5 @@
|
|
1
1
|
module FortyFacets
|
2
|
-
class
|
3
|
-
FacetValue = Struct.new(:entity, :count, :selected)
|
4
|
-
|
2
|
+
class BelongsToFilterDefinition < FilterDefinition
|
5
3
|
class FacetFilter < Filter
|
6
4
|
def association
|
7
5
|
filter_definition.search.root_class.reflect_on_association(filter_definition.model_field)
|
@@ -12,10 +10,6 @@ module FortyFacets
|
|
12
10
|
association.klass
|
13
11
|
end
|
14
12
|
|
15
|
-
def values
|
16
|
-
@values ||= Array.wrap(value).sort.uniq
|
17
|
-
end
|
18
|
-
|
19
13
|
def selected
|
20
14
|
@selected ||= klass.find(values)
|
21
15
|
end
|
@@ -29,25 +23,20 @@ module FortyFacets
|
|
29
23
|
my_column = association.association_foreign_key
|
30
24
|
counts = without.result.reorder('').select("#{my_column} as foreign_id, count(#{my_column}) as occurrences").group(my_column)
|
31
25
|
entities_by_id = klass.find(counts.map(&:foreign_id)).group_by(&:id)
|
32
|
-
|
26
|
+
|
27
|
+
facet = counts.map do |count|
|
33
28
|
facet_entity = entities_by_id[count.foreign_id].first
|
34
29
|
is_selected = selected.include?(facet_entity)
|
35
|
-
|
30
|
+
FacetValue.new(facet_entity, count.occurrences, is_selected)
|
36
31
|
end
|
37
32
|
|
38
33
|
order_facet!(facet)
|
39
34
|
end
|
40
35
|
|
41
|
-
def
|
42
|
-
new_params = search_instance.params || {}
|
43
|
-
new_params.delete(filter_definition.request_param)
|
44
|
-
search_instance.class.new_unwrapped(new_params)
|
45
|
-
end
|
46
|
-
|
47
|
-
def remove(value)
|
36
|
+
def remove(entity)
|
48
37
|
new_params = search_instance.params || {}
|
49
38
|
old_values = new_params[filter_definition.request_param]
|
50
|
-
old_values.delete(
|
39
|
+
old_values.delete(entity.id.to_s)
|
51
40
|
new_params.delete(filter_definition.request_param) if old_values.empty?
|
52
41
|
search_instance.class.new_unwrapped(new_params)
|
53
42
|
end
|
@@ -1,6 +1,9 @@
|
|
1
1
|
module FortyFacets
|
2
2
|
# Base class for the classes storing the definition of differently behaving filters
|
3
3
|
FilterDefinition = Struct.new(:search, :model_field, :options) do
|
4
|
+
|
5
|
+
FacetValue = Struct.new(:entity, :count, :selected)
|
6
|
+
|
4
7
|
def request_param
|
5
8
|
model_field
|
6
9
|
end
|
data/lib/forty_facets/version.rb
CHANGED
data/test/smoke_test.rb
CHANGED
@@ -20,6 +20,7 @@ ActiveRecord::Base.connection.instance_eval do
|
|
20
20
|
|
21
21
|
create_table :movies do |t|
|
22
22
|
t.integer :studio_id
|
23
|
+
t.integer :year
|
23
24
|
t.string :title
|
24
25
|
t.float :price
|
25
26
|
end
|
@@ -38,6 +39,7 @@ class MovieSearch < FortyFacets::FacetSearch
|
|
38
39
|
|
39
40
|
text :title, name: 'Title'
|
40
41
|
facet :studio, name: 'Studio'
|
42
|
+
facet_attr :year
|
41
43
|
range :price, name: 'Price'
|
42
44
|
end
|
43
45
|
|
@@ -48,7 +50,7 @@ end
|
|
48
50
|
|
49
51
|
rand = Random.new
|
50
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|
|
51
|
-
Movie.create!(title: title, studio: studios[index % studios.length], price: rand.rand(20.0))
|
53
|
+
Movie.create!(title: title, studio: studios[index % studios.length], price: rand.rand(20.0), year: (index%3 + 2010) )
|
52
54
|
end
|
53
55
|
|
54
56
|
class SmokeTest < Minitest::Test
|
@@ -64,6 +66,43 @@ class SmokeTest < Minitest::Test
|
|
64
66
|
assert_equal 'ipsum', search.result.first.title
|
65
67
|
end
|
66
68
|
|
69
|
+
def test_year_filter
|
70
|
+
search = MovieSearch.new({'search' => { year: '2011' }})
|
71
|
+
assert_equal [2011], search.result.map(&:year).uniq
|
72
|
+
|
73
|
+
facet = search.filter(:year).facet
|
74
|
+
assert_equal Movie.count, facet.map(&:count).sum
|
75
|
+
end
|
76
|
+
|
77
|
+
def test_year_add_remove_filter
|
78
|
+
|
79
|
+
search = MovieSearch.new()
|
80
|
+
|
81
|
+
search = search.filter(:year).add(2010)
|
82
|
+
assert_equal Movie.where(year: 2010).count, search.result.count
|
83
|
+
|
84
|
+
search = search.filter(:year).add(2011)
|
85
|
+
assert_equal Movie.where(year: [2010, 2011]).count, search.result.count
|
86
|
+
|
87
|
+
search = search.filter(:year).remove(2010)
|
88
|
+
assert_equal Movie.where(year: 2011).count, search.result.count
|
89
|
+
end
|
90
|
+
|
91
|
+
def test_selected_year_filter
|
92
|
+
search = MovieSearch.new()
|
93
|
+
|
94
|
+
search = search.filter(:year).add(2010)
|
95
|
+
assert_equal [2010], search.filter(:year).selected
|
96
|
+
|
97
|
+
search = search.filter(:year).add(2011)
|
98
|
+
assert_equal [2010, 2011], search.filter(:year).selected
|
99
|
+
|
100
|
+
facet = search.filter(:year).facet
|
101
|
+
assert facet.find{|fv| fv.entity == 2010}.selected
|
102
|
+
assert facet.find{|fv| fv.entity == 2011}.selected
|
103
|
+
assert !facet.find{|fv| fv.entity == 2012}.selected
|
104
|
+
end
|
105
|
+
|
67
106
|
def test_belongs_to_filter
|
68
107
|
blank_search = MovieSearch.new
|
69
108
|
first_facet_value = blank_search.filter(:studio).facet.first
|
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.6
|
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-
|
11
|
+
date: 2014-06-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -113,7 +113,8 @@ files:
|
|
113
113
|
- lib/forty_facets.rb
|
114
114
|
- lib/forty_facets/facet_search.rb
|
115
115
|
- lib/forty_facets/filter.rb
|
116
|
-
- lib/forty_facets/filter/
|
116
|
+
- lib/forty_facets/filter/attribute_filter_definition.rb
|
117
|
+
- lib/forty_facets/filter/belongs_to_filter_definition.rb
|
117
118
|
- lib/forty_facets/filter/range_filter_definition.rb
|
118
119
|
- lib/forty_facets/filter/text_filter_definition.rb
|
119
120
|
- lib/forty_facets/filter_definition.rb
|