forty_facets 0.0.3 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.coveralls.yml +1 -0
- data/.travis.yml +2 -0
- data/README.md +4 -0
- data/Rakefile +8 -0
- data/forty_facets.gemspec +4 -0
- data/lib/forty_facets/facet_search.rb +35 -193
- data/lib/forty_facets/filter/belongs_to.rb +80 -0
- data/lib/forty_facets/filter/range_filter_definition.rb +37 -0
- data/lib/forty_facets/filter/text_filter_definition.rb +23 -0
- data/lib/forty_facets/filter.rb +24 -0
- data/lib/forty_facets/filter_definition.rb +9 -0
- data/lib/forty_facets/order.rb +16 -0
- data/lib/forty_facets/order_definition.rb +15 -0
- data/lib/forty_facets/version.rb +1 -1
- data/lib/forty_facets.rb +7 -0
- data/test/smoke_test.rb +80 -0
- data/test/test_helper.rb +0 -0
- metadata +72 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 25afbbdd35eee3919efcdbf6766ef3173ad1a2fe
|
4
|
+
data.tar.gz: 864056c9c65d99a49a890177d7ff87440a18d62b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6f40a9650e2b73e552092a810e33c06390f81f554185e6d8af4749b881828ed5065a06e013ca1956b08eff121920bf19b613598a931cbc1fd60fe687da915187
|
7
|
+
data.tar.gz: 2e49716fd8a9b30ffe935d49976f2175068507c2c4448996913b2c08ea1e10e9714a3c91265560e3d573f0804f13c9100effc31a317f654418df3a20517d67e5
|
data/.coveralls.yml
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
service_name: travis-ci
|
data/.travis.yml
ADDED
data/README.md
CHANGED
@@ -1,3 +1,7 @@
|
|
1
|
+
[![Build Status](https://travis-ci.org/fortytools/forty_facets.svg)](https://travis-ci.org/fortytools/forty_facets)
|
2
|
+
[![Coverage Status](https://coveralls.io/repos/fortytools/forty_facets/badge.png?branch=master)](https://coveralls.io/r/fortytools/forty_facets?branch=master)
|
3
|
+
[![Code Climate](https://codeclimate.com/github/fortytools/forty_facets.png)](https://codeclimate.com/github/fortytools/forty_facets)
|
4
|
+
|
1
5
|
# FortyFacets
|
2
6
|
|
3
7
|
FortyFacets lets you easily build explorative search interfaces based on fields of your ActiveRecord models.
|
data/Rakefile
CHANGED
data/forty_facets.gemspec
CHANGED
@@ -20,4 +20,8 @@ Gem::Specification.new do |spec|
|
|
20
20
|
|
21
21
|
spec.add_development_dependency "bundler", "~> 1.5"
|
22
22
|
spec.add_development_dependency "rake"
|
23
|
+
spec.add_development_dependency "minitest"
|
24
|
+
spec.add_development_dependency "sqlite3"
|
25
|
+
spec.add_development_dependency "coveralls"
|
26
|
+
spec.add_development_dependency "activerecord", ">= 4.0"
|
23
27
|
end
|
@@ -1,203 +1,38 @@
|
|
1
1
|
module FortyFacets
|
2
|
+
# Inherit this class to create a custom search for your model
|
3
|
+
#
|
4
|
+
# class MovieSearch < FortyFacets::FacetSearch
|
5
|
+
# model 'Movie'
|
6
|
+
#
|
7
|
+
# text :title
|
8
|
+
# range :price
|
9
|
+
# facet :genre, name: 'Genre'
|
10
|
+
# facet :year, name: 'Releaseyear', order: :year
|
11
|
+
# facet :studio, name: 'Studio', order: :name
|
12
|
+
#
|
13
|
+
# orders 'Title' => :title,
|
14
|
+
# 'price, cheap first' => "price asc",
|
15
|
+
# 'price, expensive first' => {price: :desc, title: :desc}
|
16
|
+
#
|
17
|
+
# end
|
2
18
|
class FacetSearch
|
3
19
|
attr_reader :filters, :orders
|
4
20
|
|
5
|
-
|
6
|
-
FieldDefinition = Struct.new(:search, :model_field, :options) do
|
7
|
-
def request_param
|
8
|
-
model_field
|
9
|
-
end
|
10
|
-
end
|
11
|
-
|
12
|
-
Filter = Struct.new(:field_definition, :search_instance, :value) do
|
13
|
-
def name
|
14
|
-
field_definition.options[:name] || field_definition.model_field
|
15
|
-
end
|
16
|
-
|
17
|
-
def empty?
|
18
|
-
value.nil? || value == '' || value == []
|
19
|
-
end
|
20
|
-
|
21
|
-
# generate a search with this filter removed
|
22
|
-
def without
|
23
|
-
search = search_instance
|
24
|
-
return search if empty?
|
25
|
-
new_params = search_instance.params
|
26
|
-
new_params.delete(field_definition.request_param)
|
27
|
-
search_instance.class.new_unwrapped(new_params)
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
|
-
class RangeField < FieldDefinition
|
32
|
-
class RangeFilter < Filter
|
33
|
-
def build_scope
|
34
|
-
return Proc.new { |base| base } if empty?
|
35
|
-
Proc.new { |base| base.where("#{field_definition.model_field} >= ? AND #{field_definition.model_field} <= ? ", min_value, max_value ) }
|
36
|
-
end
|
37
|
-
|
38
|
-
def min_value
|
39
|
-
return nil if empty?
|
40
|
-
value.split(' - ').first
|
41
|
-
end
|
42
|
-
|
43
|
-
def max_value
|
44
|
-
return nil if empty?
|
45
|
-
value.split(' - ').last
|
46
|
-
end
|
47
|
-
|
48
|
-
def absolute_interval
|
49
|
-
@abosultes ||= without.result.select("min(#{field_definition.model_field}) as min, max(#{field_definition.model_field}) as max").first
|
50
|
-
end
|
51
|
-
|
52
|
-
def absolute_min
|
53
|
-
absolute_interval.min
|
54
|
-
end
|
55
|
-
|
56
|
-
def absolute_max
|
57
|
-
absolute_interval.max
|
58
|
-
end
|
59
|
-
|
60
|
-
end
|
61
|
-
|
62
|
-
def build_filter(search_instance, value)
|
63
|
-
RangeFilter.new(self, search_instance, value)
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
class TextField < FieldDefinition
|
68
|
-
class TextFilter < Filter
|
69
|
-
def build_scope
|
70
|
-
return Proc.new { |base| base } if empty?
|
71
|
-
like_value = expression_value(value)
|
72
|
-
Proc.new { |base| base.where("#{field_definition.model_field} like ?", like_value ) }
|
73
|
-
end
|
74
|
-
|
75
|
-
def expression_value(term)
|
76
|
-
if field_definition.options[:prefix]
|
77
|
-
"#{term}%"
|
78
|
-
else
|
79
|
-
"%#{term}%"
|
80
|
-
end
|
81
|
-
end
|
82
|
-
end
|
83
|
-
|
84
|
-
def build_filter(search_instance, value)
|
85
|
-
TextFilter.new(self, search_instance, value)
|
86
|
-
end
|
87
|
-
end
|
88
|
-
|
89
|
-
class FacetField < FieldDefinition
|
90
|
-
FacetValue = Struct.new(:entity, :count, :selected)
|
91
|
-
|
92
|
-
class FacetFilter < Filter
|
93
|
-
def association
|
94
|
-
field_definition.search.root_class.reflect_on_association(field_definition.model_field)
|
95
|
-
end
|
96
|
-
|
97
|
-
# class objects in this filter
|
98
|
-
def klass
|
99
|
-
association.klass
|
100
|
-
end
|
101
|
-
|
102
|
-
def values
|
103
|
-
@values ||= Array.wrap(value).sort.uniq
|
104
|
-
end
|
105
|
-
|
106
|
-
def selected
|
107
|
-
@selected ||= klass.find(values)
|
108
|
-
end
|
109
|
-
|
110
|
-
def build_scope
|
111
|
-
return Proc.new { |base| base } if empty?
|
112
|
-
Proc.new { |base| base.where(association.association_foreign_key => values) }
|
113
|
-
end
|
114
|
-
|
115
|
-
def facet
|
116
|
-
my_column = association.association_foreign_key
|
117
|
-
counts = without.result.select("#{my_column} as foreign_id, count(#{my_column}) as occurrences").group(my_column)
|
118
|
-
entities_by_id = klass.find(counts.map(&:foreign_id)).group_by(&:id)
|
119
|
-
facet = counts.inject([]) do |sum, count|
|
120
|
-
facet_entity = entities_by_id[count.foreign_id].first
|
121
|
-
is_selected = selected.include?(facet_entity)
|
122
|
-
sum << FacetValue.new(facet_entity, count.occurrences, is_selected)
|
123
|
-
end
|
124
|
-
|
125
|
-
order_accessor = field_definition.options[:order]
|
126
|
-
if order_accessor
|
127
|
-
facet.sort_by!{|facet_value| facet_value.entity.send(order_accessor) }
|
128
|
-
else
|
129
|
-
facet.sort_by!{|facet_value| -facet_value.count }
|
130
|
-
end
|
131
|
-
facet
|
132
|
-
|
133
|
-
end
|
134
|
-
|
135
|
-
def without
|
136
|
-
new_params = search_instance.params || {}
|
137
|
-
new_params.delete(field_definition.request_param)
|
138
|
-
search_instance.class.new_unwrapped(new_params)
|
139
|
-
end
|
140
|
-
|
141
|
-
def remove(value)
|
142
|
-
new_params = search_instance.params || {}
|
143
|
-
old_values = new_params[field_definition.request_param]
|
144
|
-
old_values.delete(value.id.to_s)
|
145
|
-
new_params.delete(field_definition.request_param) if old_values.empty?
|
146
|
-
search_instance.class.new_unwrapped(new_params)
|
147
|
-
end
|
148
|
-
|
149
|
-
def add(entity)
|
150
|
-
new_params = search_instance.params || {}
|
151
|
-
old_values = new_params[field_definition.request_param] ||= []
|
152
|
-
old_values << entity.id.to_s
|
153
|
-
search_instance.class.new_unwrapped(new_params)
|
154
|
-
end
|
155
|
-
|
156
|
-
end
|
157
|
-
|
158
|
-
def build_filter(search_instance, param_value)
|
159
|
-
FacetFilter.new(self, search_instance, param_value)
|
160
|
-
end
|
161
|
-
|
162
|
-
end
|
163
|
-
|
164
21
|
class << self
|
165
22
|
def model(model_name)
|
166
23
|
@model_name = model_name
|
167
24
|
end
|
168
25
|
|
169
26
|
def text(model_field, opts = {})
|
170
|
-
definitions <<
|
27
|
+
definitions << TextFilterDefinition.new(self, model_field, opts)
|
171
28
|
end
|
172
29
|
|
173
30
|
def range(model_field, opts = {})
|
174
|
-
definitions <<
|
31
|
+
definitions << RangeFilterDefintion.new(self, model_field, opts)
|
175
32
|
end
|
176
33
|
|
177
34
|
def facet(model_field, opts = {})
|
178
|
-
definitions <<
|
179
|
-
end
|
180
|
-
|
181
|
-
OrderDefinition = Struct.new(:title, :clause) do
|
182
|
-
def build(search, order_param)
|
183
|
-
Order.new(search, self, order_param == title.to_s)
|
184
|
-
end
|
185
|
-
|
186
|
-
def request_value
|
187
|
-
title
|
188
|
-
end
|
189
|
-
end
|
190
|
-
|
191
|
-
Order = Struct.new(:search, :definition, :active) do
|
192
|
-
def title
|
193
|
-
definition.title
|
194
|
-
end
|
195
|
-
|
196
|
-
def by
|
197
|
-
new_params = search.params || {}
|
198
|
-
new_params[:order] = definition.request_value
|
199
|
-
search.class.new_unwrapped(new_params)
|
200
|
-
end
|
35
|
+
definitions << BelongsToFilter.new(self, model_field, opts)
|
201
36
|
end
|
202
37
|
|
203
38
|
def orders(name_and_order_options)
|
@@ -226,16 +61,12 @@ module FortyFacets
|
|
226
61
|
end
|
227
62
|
|
228
63
|
def order_definitions
|
229
|
-
@order_definitions
|
64
|
+
@order_definitions ||= []
|
230
65
|
end
|
231
66
|
end
|
232
67
|
|
233
|
-
def initialize(request_params)
|
234
|
-
params =
|
235
|
-
request_params[self.class.request_param_name]
|
236
|
-
else
|
237
|
-
{}
|
238
|
-
end
|
68
|
+
def initialize(request_params = {})
|
69
|
+
params = request_to_search_params(request_params)
|
239
70
|
@filters = self.class.definitions.inject([]) do |filters, definition|
|
240
71
|
filters << definition.build_filter(self, params[definition.request_param])
|
241
72
|
end
|
@@ -251,7 +82,7 @@ module FortyFacets
|
|
251
82
|
end
|
252
83
|
|
253
84
|
def filter(filter_name)
|
254
|
-
filter = @filters.find { |f| f.
|
85
|
+
filter = @filters.find { |f| f.filter_definition.model_field == filter_name }
|
255
86
|
raise "unknown filter #{filter_name}" unless filter
|
256
87
|
filter
|
257
88
|
end
|
@@ -274,7 +105,7 @@ module FortyFacets
|
|
274
105
|
|
275
106
|
def params
|
276
107
|
params = @filters.inject({}) do |sum, filter|
|
277
|
-
sum[filter.
|
108
|
+
sum[filter.filter_definition.request_param] = filter.value.dup unless filter.empty?
|
278
109
|
sum
|
279
110
|
end
|
280
111
|
params[:order] = order.definition.request_value if order
|
@@ -288,6 +119,17 @@ module FortyFacets
|
|
288
119
|
def unfiltered?
|
289
120
|
@filters.reject(&:empty?).empty?
|
290
121
|
end
|
122
|
+
|
123
|
+
private
|
124
|
+
|
125
|
+
def request_to_search_params(request_params)
|
126
|
+
if request_params && request_params[self.class.request_param_name]
|
127
|
+
request_params[self.class.request_param_name]
|
128
|
+
else
|
129
|
+
{}
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
291
133
|
end
|
292
134
|
end
|
293
135
|
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module FortyFacets
|
2
|
+
class BelongsToFilter < FilterDefinition
|
3
|
+
FacetValue = Struct.new(:entity, :count, :selected)
|
4
|
+
|
5
|
+
class FacetFilter < Filter
|
6
|
+
def association
|
7
|
+
filter_definition.search.root_class.reflect_on_association(filter_definition.model_field)
|
8
|
+
end
|
9
|
+
|
10
|
+
# class objects in this filter
|
11
|
+
def klass
|
12
|
+
association.klass
|
13
|
+
end
|
14
|
+
|
15
|
+
def values
|
16
|
+
@values ||= Array.wrap(value).sort.uniq
|
17
|
+
end
|
18
|
+
|
19
|
+
def selected
|
20
|
+
@selected ||= klass.find(values)
|
21
|
+
end
|
22
|
+
|
23
|
+
def build_scope
|
24
|
+
return Proc.new { |base| base } if empty?
|
25
|
+
Proc.new { |base| base.where(association.association_foreign_key => values) }
|
26
|
+
end
|
27
|
+
|
28
|
+
def facet
|
29
|
+
my_column = association.association_foreign_key
|
30
|
+
counts = without.result.select("#{my_column} as foreign_id, count(#{my_column}) as occurrences").group(my_column)
|
31
|
+
entities_by_id = klass.find(counts.map(&:foreign_id)).group_by(&:id)
|
32
|
+
facet = counts.inject([]) do |sum, count|
|
33
|
+
facet_entity = entities_by_id[count.foreign_id].first
|
34
|
+
is_selected = selected.include?(facet_entity)
|
35
|
+
sum << FacetValue.new(facet_entity, count.occurrences, is_selected)
|
36
|
+
end
|
37
|
+
|
38
|
+
order_facet!(facet)
|
39
|
+
end
|
40
|
+
|
41
|
+
def without
|
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)
|
48
|
+
new_params = search_instance.params || {}
|
49
|
+
old_values = new_params[filter_definition.request_param]
|
50
|
+
old_values.delete(value.id.to_s)
|
51
|
+
new_params.delete(filter_definition.request_param) if old_values.empty?
|
52
|
+
search_instance.class.new_unwrapped(new_params)
|
53
|
+
end
|
54
|
+
|
55
|
+
def add(entity)
|
56
|
+
new_params = search_instance.params || {}
|
57
|
+
old_values = new_params[filter_definition.request_param] ||= []
|
58
|
+
old_values << entity.id.to_s
|
59
|
+
search_instance.class.new_unwrapped(new_params)
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def order_facet!(facet)
|
65
|
+
order_accessor = filter_definition.options[:order]
|
66
|
+
if order_accessor
|
67
|
+
facet.sort_by!{|facet_value| facet_value.entity.send(order_accessor) }
|
68
|
+
else
|
69
|
+
facet.sort_by!{|facet_value| -facet_value.count }
|
70
|
+
end
|
71
|
+
facet
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def build_filter(search_instance, param_value)
|
76
|
+
FacetFilter.new(self, search_instance, param_value)
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module FortyFacets
|
2
|
+
class RangeFilterDefintion < FilterDefinition
|
3
|
+
class RangeFilter < Filter
|
4
|
+
def build_scope
|
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 ) }
|
7
|
+
end
|
8
|
+
|
9
|
+
def min_value
|
10
|
+
return nil if empty?
|
11
|
+
value.split(' - ').first
|
12
|
+
end
|
13
|
+
|
14
|
+
def max_value
|
15
|
+
return nil if empty?
|
16
|
+
value.split(' - ').last
|
17
|
+
end
|
18
|
+
|
19
|
+
def absolute_interval
|
20
|
+
@abosultes ||= without.result.select("min(#{filter_definition.model_field}) as min, max(#{filter_definition.model_field}) as max").first
|
21
|
+
end
|
22
|
+
|
23
|
+
def absolute_min
|
24
|
+
absolute_interval.min
|
25
|
+
end
|
26
|
+
|
27
|
+
def absolute_max
|
28
|
+
absolute_interval.max
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
def build_filter(search_instance, value)
|
34
|
+
RangeFilter.new(self, search_instance, value)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module FortyFacets
|
2
|
+
class TextFilterDefinition < FilterDefinition
|
3
|
+
class TextFilter < Filter
|
4
|
+
def build_scope
|
5
|
+
return Proc.new { |base| base } if empty?
|
6
|
+
like_value = expression_value(value)
|
7
|
+
Proc.new { |base| base.where("#{filter_definition.model_field} like ?", like_value ) }
|
8
|
+
end
|
9
|
+
|
10
|
+
def expression_value(term)
|
11
|
+
if filter_definition.options[:prefix]
|
12
|
+
"#{term}%"
|
13
|
+
else
|
14
|
+
"%#{term}%"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def build_filter(search_instance, value)
|
20
|
+
TextFilter.new(self, search_instance, value)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module FortyFacets
|
2
|
+
# Base class for the objects representing a specific value for a specific
|
3
|
+
# type of filter. Most FilterDefinitions will have their own Filter subclass
|
4
|
+
# to control values for display and rendering to request parameters.
|
5
|
+
Filter = Struct.new(:filter_definition, :search_instance, :value) do
|
6
|
+
def name
|
7
|
+
filter_definition.options[:name] || filter_definition.model_field
|
8
|
+
end
|
9
|
+
|
10
|
+
def empty?
|
11
|
+
value.nil? || value == '' || value == []
|
12
|
+
end
|
13
|
+
|
14
|
+
# generate a search with this filter removed
|
15
|
+
def without
|
16
|
+
search = search_instance
|
17
|
+
return search if empty?
|
18
|
+
new_params = search_instance.params
|
19
|
+
new_params.delete(filter_definition.request_param)
|
20
|
+
search_instance.class.new_unwrapped(new_params)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module FortyFacets
|
2
|
+
# Represents the ordering for a specific search
|
3
|
+
Order = Struct.new(:search, :definition, :active) do
|
4
|
+
def title
|
5
|
+
definition.title
|
6
|
+
end
|
7
|
+
|
8
|
+
# Returns a search with the same filter ordered by this criteria
|
9
|
+
def by
|
10
|
+
new_params = search.params || {}
|
11
|
+
new_params[:order] = definition.request_value
|
12
|
+
search.class.new_unwrapped(new_params)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module FortyFacets
|
2
|
+
# Stores the parameters of a order criteria for a search.
|
3
|
+
OrderDefinition = Struct.new(:title, :clause) do
|
4
|
+
def build(search, order_param)
|
5
|
+
Order.new(search, self, order_param == title.to_s)
|
6
|
+
end
|
7
|
+
|
8
|
+
# Returns the value that is used in the request parameter to indicate that
|
9
|
+
# the search result is ordered by this criteria.
|
10
|
+
def request_value
|
11
|
+
title
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
data/lib/forty_facets/version.rb
CHANGED
data/lib/forty_facets.rb
CHANGED
@@ -3,4 +3,11 @@ require "forty_facets/version"
|
|
3
3
|
module FortyFacets
|
4
4
|
end
|
5
5
|
|
6
|
+
require "forty_facets/order_definition"
|
7
|
+
require "forty_facets/order"
|
8
|
+
require "forty_facets/filter"
|
9
|
+
require "forty_facets/filter_definition"
|
10
|
+
require "forty_facets/filter/belongs_to"
|
11
|
+
require "forty_facets/filter/range_filter_definition"
|
12
|
+
require "forty_facets/filter/text_filter_definition"
|
6
13
|
require "forty_facets/facet_search"
|
data/test/smoke_test.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'coveralls'
|
2
|
+
Coveralls.wear!
|
3
|
+
|
4
|
+
require "minitest/autorun"
|
5
|
+
require 'active_record'
|
6
|
+
require 'logger'
|
7
|
+
require_relative '../lib/forty_facets'
|
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.string :title
|
24
|
+
t.float :price
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
class Studio < ActiveRecord::Base
|
30
|
+
end
|
31
|
+
|
32
|
+
class Movie < ActiveRecord::Base
|
33
|
+
belongs_to :studio
|
34
|
+
end
|
35
|
+
|
36
|
+
class MovieSearch < FortyFacets::FacetSearch
|
37
|
+
model 'Movie'
|
38
|
+
|
39
|
+
text :title, name: 'Title'
|
40
|
+
facet :studio, name: 'Studio'
|
41
|
+
range :price, name: 'Price'
|
42
|
+
end
|
43
|
+
|
44
|
+
studios = []
|
45
|
+
%w{A B C D}.each do |suffix|
|
46
|
+
studios << Studio.create!(name: "Studio #{suffix}")
|
47
|
+
end
|
48
|
+
|
49
|
+
rand = Random.new
|
50
|
+
%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))
|
52
|
+
end
|
53
|
+
|
54
|
+
class SmokeTest < Minitest::Test
|
55
|
+
|
56
|
+
def test_it_finds_all_movies
|
57
|
+
search = MovieSearch.new({})
|
58
|
+
assert_equal Movie.all.size, search.result.size
|
59
|
+
end
|
60
|
+
|
61
|
+
def test_text_filter
|
62
|
+
search = MovieSearch.new({'search' => { title: 'ipsum' }})
|
63
|
+
assert_equal 1, search.result.size
|
64
|
+
assert_equal 'ipsum', search.result.first.title
|
65
|
+
end
|
66
|
+
|
67
|
+
def test_belongs_to_filter
|
68
|
+
blank_search = MovieSearch.new
|
69
|
+
first_facet_value = blank_search.filter(:studio).facet.first
|
70
|
+
studio = first_facet_value.entity
|
71
|
+
assert_kind_of Studio, studio
|
72
|
+
|
73
|
+
movies_with_studio = Movie.where(studio: studio)
|
74
|
+
search_with_studio = blank_search.filter(:studio).add(studio)
|
75
|
+
|
76
|
+
assert_equal movies_with_studio.size, search_with_studio.result.size
|
77
|
+
assert_equal movies_with_studio.size, first_facet_value.count
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
data/test/test_helper.rb
ADDED
File without changes
|
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.4
|
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-05-
|
11
|
+
date: 2014-05-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -38,6 +38,62 @@ dependencies:
|
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: minitest
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: sqlite3
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: coveralls
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: activerecord
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '4.0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '4.0'
|
41
97
|
description: FortyFacets lets you easily build explorative search interfaces based
|
42
98
|
on fields of your active_record models.
|
43
99
|
email:
|
@@ -46,7 +102,9 @@ executables: []
|
|
46
102
|
extensions: []
|
47
103
|
extra_rdoc_files: []
|
48
104
|
files:
|
105
|
+
- ".coveralls.yml"
|
49
106
|
- ".gitignore"
|
107
|
+
- ".travis.yml"
|
50
108
|
- Gemfile
|
51
109
|
- LICENSE.txt
|
52
110
|
- README.md
|
@@ -54,7 +112,16 @@ files:
|
|
54
112
|
- forty_facets.gemspec
|
55
113
|
- lib/forty_facets.rb
|
56
114
|
- lib/forty_facets/facet_search.rb
|
115
|
+
- lib/forty_facets/filter.rb
|
116
|
+
- lib/forty_facets/filter/belongs_to.rb
|
117
|
+
- lib/forty_facets/filter/range_filter_definition.rb
|
118
|
+
- lib/forty_facets/filter/text_filter_definition.rb
|
119
|
+
- lib/forty_facets/filter_definition.rb
|
120
|
+
- lib/forty_facets/order.rb
|
121
|
+
- lib/forty_facets/order_definition.rb
|
57
122
|
- lib/forty_facets/version.rb
|
123
|
+
- test/smoke_test.rb
|
124
|
+
- test/test_helper.rb
|
58
125
|
homepage: https://github.com/fortytools/forty_facets
|
59
126
|
licenses:
|
60
127
|
- MIT
|
@@ -79,4 +146,6 @@ rubygems_version: 2.2.0
|
|
79
146
|
signing_key:
|
80
147
|
specification_version: 4
|
81
148
|
summary: Library for building facet searches for active_record models
|
82
|
-
test_files:
|
149
|
+
test_files:
|
150
|
+
- test/smoke_test.rb
|
151
|
+
- test/test_helper.rb
|