1module Browseable |
|
2 extend ActiveSupport::Concern |
|
4 included do |
|
5 class_attribute :facet_definitions, instance_writer: false |
|
6 self.facet_definitions = {} |
|
8 class_attribute :filter_definitions, instance_writer: false |
|
9 self.filter_definitions = [] |
|
10 end |
|
12 class Facets |
|
13 include Enumerable |
|
15 attr_reader :klass |
|
17 delegate :facet_definitions, to: :klass |
|
18 delegate :key?, :has_key?, :keys, to: :facet_definitions |
|
|
20 def initialize(klass) |
21 @klass = klass |
|
22 end |
|
|
24 def [](key) |
25 facet_counts[key]
|
|
26 end |
|
|
28 def each(&block) |
29 keys.each do |key| |
|
30 yield key, self[key] |
|
31 end |
|
32 end |
|
|
34 def slice(*only_these_keys) |
35 only_these_keys.each_with_object({}) do |key, hash| |
|
36 hash[key] = self[key] if has_key?(key) |
|
37 end |
|
38 end |
|
40 private
|
|
|
42 def facet_counts |
43 @facet_counts ||= Hash.new(&facet_count_query) |
|
44 end |
|
|
46 def facet_count_query |
47 lambda do |hash, key| |
|
48 unless facet_definitions.key?(key) |
|
49 raise ArgumentError, "Unsupported facet: #{key.inspect}" |
|
50 end |
|
52 hash[key] = facet_scope(key).count
|
|
53 end |
|
54 end |
|
|
56 def facet_scope(key) |
57 klass.instance_exec(&facet_definitions.fetch(key))
|
|
58 end |
|
59 end |
|
61 class Filters |
|
62 attr_reader :klass, :params |
|
63 delegate :filter_definitions, to: :klass |
|
|
65 def initialize(klass, params) |
66 @klass, @params = klass, params |
|
67 end |
|
|
69 def to_hash |
70 params.slice(*filter_definitions)
|
|
71 end |
|
72 end |
|
74 class Search |
|
75 include Enumerable |
|
77 attr_reader :klass, :params |
|
79 delegate :offset, :out_of_bounds?, to: :results |
|
80 delegate :next_page, :previous_page, to: :results |
|
81 delegate :total_entries, :total_pages, to: :results |
|
82 delegate :each, :empty?, :map, :to_a, to: :results |
|
|
84 def initialize(klass, params = {}) |
85 @klass, @params = klass, params |
|
86 end |
|
|
88 def current_page |
89 @current_page ||= [params[:page].to_i, 1].max |
|
90 end |
|
|
92 def each(&block) |
93 results.each(&block)
|
|
94 end |
|
|
96 def find_each(&block) |
97 execute_search.find_each(&block)
|
|
98 end |
|
|
100 def facets |
101 @facets ||= Facets.new(klass) |
|
102 end |
|
|
104 def filters |
105 @filters ||= Filters.new(klass, params) |
|
106 end |
|
|
108 def first_page? |
109 current_page <= 1 |
|
110 end |
|
|
112 def second_page? |
113 current_page == 2 |
|
114 end |
|
|
116 def last_page? |
117 current_page >= total_pages
|
|
118 end |
|
|
120 def query |
121 @query ||= params[:q].to_s |
|
122 end |
|
|
124 def page_size |
125 @page_size ||= [[params.fetch(:count, 50).to_i, 50].min, 1].max |
|
126 end |
|
|
128 def previous_params |
129 new_params(previous_page)
|
|
130 end |
|
|
132 def next_params |
133 new_params(next_page)
|
|
134 end |
|
|
136 def scope |
137 @scope ||= facets.keys.detect(-> { :all }){ |key| key.to_s == params[:state] } |
|
138 end |
|
|
140 def scoped? |
141 scope != :all |
|
142 end |
|
|
144 def search? |
145 query.present?
|
|
146 end |
|
|
148 def to_a |
149 results.to_a
|
|
150 end |
|
|
152 def in_batches(&block) |
153 execute_search.find_each do |obj| |
|
154 block.call obj
|
|
155 end |
|
156 end |
|
|
158 def inspect |
159 [].tap do |parts| |
|
160 parts << "#<#{self.class.name}:#{object_id}" |
|
161 parts << " class: #{klass.klass.to_s.inspect}" |
|
162 parts << " scope: #{scope.to_s.inspect}" if scoped? |
|
163 parts << " query: #{query.inspect}" if search? |
|
164 parts << " size: #{total_entries}" |
|
165 parts << ">" |
|
166 end.join |
|
167 end |
|
|
169 def model |
170 klass.klass
|
|
171 end |
|
173 private
|
|
|
175 def new_params(page) |
176 {}.tap do |params| |
|
177 params[:q] = query if query.present? |
|
178 params[:state] = scope |
|
179 params[:page] = page |
|
180 params.merge!(filters)
|
|
181 end |
|
182 end |
|
|
184 def results |
185 @results ||= execute_search_with_pagination |
|
186 end |
|
|
188 def execute_search_with_pagination |
189 execute_search.paginate(page: current_page, per_page: page_size) |
|
190 end |
|
|
192 def execute_search |
193 if search? |
|
194 relation = klass.basic_search(query)
|
|
195 relation = relation.except(:select).select(star) |
|
196 relation = relation.except(:order) |
|
197 else |
|
198 relation = klass
|
|
199 end |
|
201 relation.instance_exec(&klass.facet_definitions[scope])
|
|
202 end |
|
|
204 def star |
205 klass.arel_table[Arel.star] |
|
206 end |
|
207 end |
|
209 module ClassMethods |
|
210 def facet(key, scope) |
|
211 self.facet_definitions[key] = scope |
|
212 end |
|
214 def filter(key) |
|
215 self.filter_definitions << key |
|
216 end |
|
218 def search(params) |
|
219 Search.new(all, params) |
|
220 end |
|
221 end |
|
222end |