blacklight 7.12.1 → 7.13.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -0
  3. data/VERSION +1 -1
  4. data/app/components/blacklight/constraints_component.rb +7 -5
  5. data/app/components/blacklight/document_component.html.erb +1 -0
  6. data/app/components/blacklight/document_component.rb +14 -1
  7. data/app/components/blacklight/facet_field_component.html.erb +1 -0
  8. data/app/controllers/concerns/blacklight/search_context.rb +1 -1
  9. data/app/controllers/concerns/blacklight/searchable.rb +1 -1
  10. data/app/helpers/blacklight/configuration_helper_behavior.rb +3 -9
  11. data/app/helpers/blacklight/facets_helper_behavior.rb +8 -2
  12. data/app/helpers/blacklight/render_constraints_helper_behavior.rb +7 -5
  13. data/app/presenters/blacklight/document_presenter.rb +4 -0
  14. data/app/presenters/blacklight/facet_item_presenter.rb +6 -2
  15. data/app/presenters/blacklight/index_presenter.rb +2 -2
  16. data/app/presenters/blacklight/rendering/link_to_facet.rb +3 -1
  17. data/app/presenters/blacklight/show_presenter.rb +0 -4
  18. data/app/services/blacklight/search_service.rb +13 -11
  19. data/app/views/catalog/_search_form.html.erb +1 -1
  20. data/app/views/catalog/index.json.jbuilder +3 -1
  21. data/lib/blacklight/configuration/facet_field.rb +7 -0
  22. data/lib/blacklight/configuration/search_field.rb +5 -0
  23. data/lib/blacklight/configuration/tool_config.rb +4 -0
  24. data/lib/blacklight/configuration/view_config.rb +12 -0
  25. data/lib/blacklight/nested_open_struct_with_hash_access.rb +1 -1
  26. data/lib/blacklight/search_builder.rb +13 -23
  27. data/lib/blacklight/search_state.rb +82 -70
  28. data/lib/blacklight/search_state/filter_field.rb +122 -0
  29. data/lib/blacklight/solr/search_builder_behavior.rb +71 -51
  30. data/package.json +4 -0
  31. data/spec/components/blacklight/document_component_spec.rb +15 -0
  32. data/spec/features/search_spec.rb +0 -5
  33. data/spec/helpers/blacklight/configuration_helper_behavior_spec.rb +1 -2
  34. data/spec/lib/blacklight/configuration/view_config_spec.rb +15 -0
  35. data/spec/lib/blacklight/nested_open_struct_with_hash_access_spec.rb +9 -0
  36. data/spec/lib/blacklight/search_state/filter_field_spec.rb +125 -0
  37. data/spec/lib/blacklight/search_state_spec.rb +132 -3
  38. data/spec/models/blacklight/configuration_spec.rb +8 -0
  39. data/spec/models/blacklight/solr/search_builder_spec.rb +32 -2
  40. metadata +7 -3
  41. data/.npmignore +0 -23
@@ -1,4 +1,7 @@
1
1
  # frozen_string_literal: true
2
+
3
+ require 'blacklight/search_state/filter_field'
4
+
2
5
  module Blacklight
3
6
  # This class encapsulates the search state as represented by the query
4
7
  # parameters namely: :f, :q, :page, :per_page and, :sort
@@ -78,7 +81,9 @@ module Blacklight
78
81
  deprecation_deprecate :[]
79
82
 
80
83
  def has_constraints?
81
- !(query_param.blank? && filter_params.blank?)
84
+ Deprecation.silence(Blacklight::SearchState) do
85
+ !(query_param.blank? && filter_params.blank? && filters.blank?)
86
+ end
82
87
  end
83
88
 
84
89
  def query_param
@@ -88,11 +93,18 @@ module Blacklight
88
93
  def filter_params
89
94
  params[:f] || {}
90
95
  end
96
+ deprecation_deprecate filter_params: 'Use #filters instead'
91
97
 
98
+ # @return [Blacklight::SearchState]
92
99
  def reset(params = nil)
93
100
  self.class.new(params || ActionController::Parameters.new, blacklight_config, controller)
94
101
  end
95
102
 
103
+ # @return [Blacklight::SearchState]
104
+ def reset_search(additional_params = {})
105
+ reset(reset_search_params.merge(additional_params))
106
+ end
107
+
96
108
  ##
97
109
  # Extension point for downstream applications
98
110
  # to provide more interesting routing to
@@ -115,23 +127,30 @@ module Blacklight
115
127
  p
116
128
  end
117
129
 
130
+ def filters
131
+ @filters ||= blacklight_config.facet_fields.each_value.map do |value|
132
+ f = filter(value)
133
+
134
+ f if f.any?
135
+ end.compact
136
+ end
137
+
138
+ def filter(field_key_or_field)
139
+ field = field_key_or_field if field_key_or_field.is_a? Blacklight::Configuration::Field
140
+ field ||= blacklight_config.facet_fields[field_key_or_field]
141
+ field ||= Blacklight::Configuration::NullField.new(key: field_key_or_field)
142
+
143
+ (field.filter_class || FilterField).new(field, self)
144
+ end
145
+
118
146
  # adds the value and/or field to params[:f]
119
147
  # Does NOT remove request keys and otherwise ensure that the hash
120
148
  # is suitable for a redirect. See
121
149
  # add_facet_params_and_redirect
122
150
  def add_facet_params(field, item)
123
- p = reset_search_params
124
-
125
- add_facet_param(p, field, item)
126
-
127
- if item && item.respond_to?(:fq) && item.fq
128
- Array(item.fq).each do |f, v|
129
- add_facet_param(p, f, v)
130
- end
131
- end
132
-
133
- p
151
+ filter(field).add(item).params
134
152
  end
153
+ deprecation_deprecate add_facet_params: 'Use filter(field).add(item) instead'
135
154
 
136
155
  # Used in catalog/facet action, facets.rb view, for a click
137
156
  # on a facet value. Add on the facet params to existing
@@ -141,7 +160,9 @@ module Blacklight
141
160
  # Change the action to 'index' to send them back to
142
161
  # catalog/index with their new facet choice.
143
162
  def add_facet_params_and_redirect(field, item)
144
- new_params = add_facet_params(field, item)
163
+ new_params = Deprecation.silence(self.class) do
164
+ add_facet_params(field, item)
165
+ end
145
166
 
146
167
  # Delete any request params from facet-specific action, needed
147
168
  # to redir to index action properly.
@@ -158,45 +179,18 @@ module Blacklight
158
179
  # @param [String] field
159
180
  # @param [String] item
160
181
  def remove_facet_params(field, item)
161
- if item.respond_to? :field
162
- field = item.field
163
- end
164
-
165
- facet_config = facet_configuration_for_field(field)
166
-
167
- url_field = facet_config.key
168
-
169
- value = facet_value_for_facet_item(item)
170
-
171
- p = reset_search_params
172
- # need to dup the facet values too,
173
- # if the values aren't dup'd, then the values
174
- # from the session will get remove in the show view...
175
- p[:f] = (p[:f] || {}).dup
176
- p[:f][url_field] = (p[:f][url_field] || []).dup
177
-
178
- collection = p[:f][url_field]
179
- # collection should be an array, because we link to ?f[key][]=value,
180
- # however, Facebook (and maybe some other PHP tools) tranform that parameters
181
- # into ?f[key][0]=value, which Rails interprets as a Hash.
182
- if collection.is_a? Hash
183
- collection = collection.values
184
- end
185
- p[:f][url_field] = collection - [value]
186
- p[:f].delete(url_field) if p[:f][url_field].empty?
187
- p.delete(:f) if p[:f].empty?
188
- p
182
+ filter(field).remove(item).params
189
183
  end
184
+ deprecation_deprecate remove_facet_params: 'Use filter(field).remove(item) instead'
190
185
 
191
186
  def has_facet?(config, value: nil)
192
- facet = params&.dig(:f, config.key)
193
-
194
187
  if value
195
- (facet || []).include? value
188
+ filter(config).include?(value)
196
189
  else
197
- facet.present?
190
+ filter(config).any?
198
191
  end
199
192
  end
193
+ deprecation_deprecate has_facet?: 'Use filter(field).include?(value) or .any? instead'
200
194
 
201
195
  # Merge the source params with the params_to_merge hash
202
196
  # @param [Hash] params_to_merge to merge into above
@@ -217,44 +211,62 @@ module Blacklight
217
211
  Parameters.sanitize(my_params)
218
212
  end
219
213
 
220
- private
214
+ def page
215
+ [params[:page].to_i, 1].max
216
+ end
221
217
 
222
- ##
223
- # Reset any search parameters that store search context
224
- # and need to be reset when e.g. constraints change
225
- # @return [ActionController::Parameters]
226
- def reset_search_params
227
- Parameters.sanitize(params).except(:page, :counter)
218
+ def per_page
219
+ params[:rows].presence&.to_i ||
220
+ params[:per_page].presence&.to_i ||
221
+ blacklight_config.default_per_page
228
222
  end
229
223
 
230
- # TODO: this code is duplicated in Blacklight::FacetsHelperBehavior
231
- def facet_value_for_facet_item item
232
- if item.respond_to? :value
233
- item.value
224
+ def sort_field
225
+ if sort_field_key.blank?
226
+ # no sort param provided, use default
227
+ blacklight_config.default_sort_field
234
228
  else
235
- item
229
+ # check for sort field key
230
+ blacklight_config.sort_fields[sort_field_key]
236
231
  end
237
232
  end
238
233
 
239
- def add_facet_param(p, field, item)
240
- if item.respond_to? :field
241
- field = item.field
242
- end
234
+ def search_field
235
+ blacklight_config.search_fields[search_field_key]
236
+ end
243
237
 
244
- facet_config = facet_configuration_for_field(field)
238
+ def facet_page
239
+ [params[facet_request_keys[:page]].to_i, 1].max
240
+ end
245
241
 
246
- url_field = facet_config.key
242
+ def facet_sort
243
+ params[facet_request_keys[:sort]]
244
+ end
247
245
 
248
- value = facet_value_for_facet_item(item)
246
+ def facet_prefix
247
+ params[facet_request_keys[:prefix]]
248
+ end
249
249
 
250
- p[:f] = (p[:f] || {}).dup # the command above is not deep in rails3, !@#$!@#$
251
- p[:f][url_field] = (p[:f][url_field] || []).dup
250
+ private
252
251
 
253
- if facet_config.single && p[:f][url_field].present?
254
- p[:f][url_field] = []
255
- end
252
+ def search_field_key
253
+ params[:search_field]
254
+ end
255
+
256
+ def sort_field_key
257
+ params[:sort]
258
+ end
259
+
260
+ def facet_request_keys
261
+ blacklight_config.facet_paginator_class.request_keys
262
+ end
256
263
 
257
- p[:f][url_field].push(value)
264
+ ##
265
+ # Reset any search parameters that store search context
266
+ # and need to be reset when e.g. constraints change
267
+ # @return [ActionController::Parameters]
268
+ def reset_search_params
269
+ Parameters.sanitize(params).except(:page, :counter)
258
270
  end
259
271
  end
260
272
  end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Blacklight
4
+ class SearchState
5
+ # Modeling access to filter query parameters
6
+ class FilterField
7
+ # @param [Blacklight::Configuration::FacetField] config
8
+ attr_reader :config
9
+
10
+ # @param [Blacklight::SearchState] search_state
11
+ attr_reader :search_state
12
+
13
+ # @return [String,Symbol]
14
+ delegate :key, to: :config
15
+
16
+ # @param [Blacklight::Configuration::FacetField] config
17
+ # @param [Blacklight::SearchState] search_state
18
+ def initialize(config, search_state)
19
+ @config = config
20
+ @search_state = search_state
21
+ end
22
+
23
+ # @param [String,#value] a filter item to add to the url
24
+ # @return [Blacklight::SearchState] new state
25
+ def add(item)
26
+ new_state = search_state.reset_search
27
+
28
+ if item.respond_to?(:fq)
29
+ Array(item.fq).each do |f, v|
30
+ new_state = new_state.filter(f).add(v)
31
+ end
32
+ end
33
+
34
+ if item.respond_to?(:field) && item.field != key
35
+ return new_state.filter(item.field).add(item)
36
+ end
37
+
38
+ params = new_state.params
39
+ value = as_url_parameter(item)
40
+
41
+ # value could be a string
42
+ params[param] = (params[param] || {}).dup
43
+
44
+ if config.single
45
+ params[param][key] = [value]
46
+ else
47
+ params[param][key] = Array(params[param][key] || []).dup
48
+ params[param][key].push(value)
49
+ end
50
+
51
+ new_state.reset(params)
52
+ end
53
+
54
+ # @param [String,#value] a filter to remove from the url
55
+ # @return [Blacklight::SearchState] new state
56
+ def remove(item)
57
+ new_state = search_state.reset_search
58
+ if item.respond_to?(:field) && item.field != key
59
+ return new_state.filter(item.field).remove(item)
60
+ end
61
+
62
+ params = new_state.params
63
+ value = as_url_parameter(item)
64
+
65
+ # need to dup the facet values too,
66
+ # if the values aren't dup'd, then the values
67
+ # from the session will get remove in the show view...
68
+ params[param] = (params[param] || {}).dup
69
+ params[param][key] = (params[param][key] || []).dup
70
+
71
+ collection = params[param][key]
72
+ # collection should be an array, because we link to ?f[key][]=value,
73
+ # however, Facebook (and maybe some other PHP tools) tranform that parameters
74
+ # into ?f[key][0]=value, which Rails interprets as a Hash.
75
+ if collection.is_a? Hash
76
+ Deprecation.warn(self, 'Normalizing parameters in FilterField#remove is deprecated')
77
+ collection = collection.values
78
+ end
79
+ params[param][key] = collection - Array(value)
80
+ params[param].delete(key) if params[param][key].empty?
81
+ params.delete(param) if params[param].empty?
82
+
83
+ new_state.reset(params)
84
+ end
85
+
86
+ # @return [Array] an array of applied filters
87
+ def values
88
+ params = search_state.params
89
+ Array(params.dig(param, key)) || []
90
+ end
91
+ delegate :any?, to: :values
92
+
93
+ # @param [String,#value] a filter to remove from the url
94
+ # @return [Boolean] whether the provided filter is currently applied/selected
95
+ def include?(item)
96
+ if item.respond_to?(:field) && item.field != key
97
+ return search_state.filter(item.field).selected?(item)
98
+ end
99
+
100
+ value = as_url_parameter(item)
101
+ params = search_state.params
102
+
103
+ (params.dig(param, key) || []).include?(value)
104
+ end
105
+
106
+ private
107
+
108
+ def param
109
+ :f
110
+ end
111
+
112
+ # TODO: this code is duplicated in Blacklight::FacetsHelperBehavior
113
+ def as_url_parameter(item)
114
+ if item.respond_to? :value
115
+ item.value
116
+ else
117
+ item
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
@@ -32,18 +32,22 @@ module Blacklight::Solr
32
32
  # including config's "search field" params for current search field.
33
33
  # also include setting spellcheck.q.
34
34
  def add_query_to_solr(solr_parameters)
35
- ###
36
- # Merge in search field configured values, if present, over-writing general
37
- # defaults
38
35
  ###
39
36
  # legacy behavior of user param :qt is passed through, but over-ridden
40
37
  # by actual search field config if present. We might want to remove
41
38
  # this legacy behavior at some point. It does not seem to be currently
42
39
  # rspec'd.
43
- solr_parameters[:qt] = blacklight_params[:qt] if blacklight_params[:qt]
40
+ if search_state.params[:qt]
41
+ Deprecation.warn(Blacklight::Solr::SearchBuilderBehavior, 'Passing the Solr qt as a parameter is deprecated.')
42
+ solr_parameters[:qt] = blacklight_params[:qt]
43
+ end
44
+
45
+ ###
46
+ # Merge in search field configured values, if present, over-writing general
47
+ # defaults
44
48
 
45
49
  if search_field
46
- solr_parameters[:qt] = search_field.qt
50
+ solr_parameters[:qt] = search_field.qt if search_field.qt
47
51
  solr_parameters.merge!(search_field.solr_parameters) if search_field.solr_parameters
48
52
  end
49
53
 
@@ -52,32 +56,14 @@ module Blacklight::Solr
52
56
  # solr LocalParams in config, using solr LocalParams syntax.
53
57
  # http://wiki.apache.org/solr/LocalParams
54
58
  ##
55
- if search_field && search_field.solr_local_parameters.present?
56
- local_params = search_field.solr_local_parameters.map do |key, val|
57
- key.to_s + "=" + solr_param_quote(val, quote: "'")
58
- end.join(" ")
59
- solr_parameters[:q] = "{!#{local_params}}#{blacklight_params[:q]}"
60
-
61
- ##
62
- # Set Solr spellcheck.q to be original user-entered query, without
63
- # our local params, otherwise it'll try and spellcheck the local
64
- # params!
65
- solr_parameters["spellcheck.q"] ||= blacklight_params[:q]
66
- elsif blacklight_params[:q].is_a? Hash
67
- q = blacklight_params[:q]
68
- solr_parameters[:q] = if q.values.any?(&:blank?)
69
- # if any field parameters are empty, exclude _all_ results
70
- "{!lucene}NOT *:*"
71
- else
72
- "{!lucene}" + q.map do |field, values|
73
- "#{field}:(#{Array(values).map { |x| solr_param_quote(x) }.join(' OR ')})"
74
- end.join(" AND ")
75
- end
76
-
77
- solr_parameters[:defType] = 'lucene'
78
- solr_parameters[:spellcheck] = 'false'
59
+ if search_field&.query_builder.present?
60
+ add_search_field_query_builder_params(solr_parameters)
61
+ elsif search_field&.solr_local_parameters.present?
62
+ add_search_field_with_local_parameters(solr_parameters)
63
+ elsif search_state.query_param.is_a? Hash
64
+ add_multifield_search_query(solr_parameters)
79
65
  elsif blacklight_params[:q]
80
- solr_parameters[:q] = blacklight_params[:q]
66
+ solr_parameters[:q] = search_state.query_param
81
67
  end
82
68
  end
83
69
 
@@ -90,13 +76,17 @@ module Blacklight::Solr
90
76
  solr_parameters[:fq] = [solr_parameters[:fq]]
91
77
  end
92
78
 
93
- # :fq, map from :f.
94
- if blacklight_params[:f]
95
- f_request_params = blacklight_params[:f]
79
+ search_state.filters.each do |filter|
80
+ if filter.config.filter_query_builder
81
+ filter_query, subqueries = filter.config.filter_query_builder.call(self, filter, solr_parameters)
96
82
 
97
- f_request_params.each_pair do |facet_field, value_list|
98
- Array(value_list).reject(&:blank?).each do |value|
99
- solr_parameters.append_filter_query facet_value_to_fq_string(facet_field, value)
83
+ solr_parameters.append_filter_query(filter_query)
84
+ solr_parameters.merge!(subqueries) if subqueries
85
+ else
86
+ filter.values.reject(&:blank?).each do |value|
87
+ filter_query, subqueries = facet_value_to_fq_string(filter.config.key, value)
88
+ solr_parameters.append_filter_query(filter_query)
89
+ solr_parameters.merge!(subqueries) if subqueries
100
90
  end
101
91
  end
102
92
  end
@@ -171,9 +161,7 @@ module Blacklight::Solr
171
161
 
172
162
  # Remove the group parameter if we've faceted on the group field (e.g. for the full results for a group)
173
163
  def add_group_config_to_solr solr_parameters
174
- if blacklight_params[:f] && blacklight_params[:f][grouped_key_for_results]
175
- solr_parameters[:group] = false
176
- end
164
+ solr_parameters[:group] = false if search_state.filter(grouped_key_for_results).any?
177
165
  end
178
166
 
179
167
  def add_facet_paging_to_solr(solr_params)
@@ -191,22 +179,17 @@ module Blacklight::Solr
191
179
  facet_config.fetch(:more_limit, blacklight_config.default_more_limit)
192
180
  end
193
181
 
194
- page = blacklight_params.fetch(request_keys[:page], 1).to_i
182
+ page = search_state.facet_page
183
+ sort = search_state.facet_sort
184
+ prefix = search_state.facet_prefix
195
185
  offset = (page - 1) * limit
196
186
 
197
- sort = blacklight_params[request_keys[:sort]]
198
- prefix = blacklight_params[request_keys[:prefix]]
199
-
200
187
  # Need to set as f.facet_field.facet.* to make sure we
201
188
  # override any field-specific default in the solr request handler.
202
189
  solr_params[:"f.#{facet_config.field}.facet.limit"] = limit + 1
203
190
  solr_params[:"f.#{facet_config.field}.facet.offset"] = offset
204
- if blacklight_params[request_keys[:sort]]
205
- solr_params[:"f.#{facet_config.field}.facet.sort"] = sort
206
- end
207
- if blacklight_params[request_keys[:prefix]]
208
- solr_params[:"f.#{facet_config.field}.facet.prefix"] = prefix
209
- end
191
+ solr_params[:"f.#{facet_config.field}.facet.sort"] = sort if sort
192
+ solr_params[:"f.#{facet_config.field}.facet.prefix"] = prefix if prefix
210
193
  solr_params[:rows] = 0
211
194
  end
212
195
 
@@ -314,8 +297,45 @@ module Blacklight::Solr
314
297
  end
315
298
  end
316
299
 
317
- def request_keys
318
- blacklight_config.facet_paginator_class.request_keys
300
+ def search_state
301
+ return super if defined?(super)
302
+
303
+ @search_state ||= Blacklight::SearchState.new(blacklight_params, blacklight_config)
304
+ end
305
+
306
+ def add_search_field_query_builder_params(solr_parameters)
307
+ q, additional_parameters = search_field.query_builder.call(self, search_field, solr_parameters)
308
+
309
+ solr_parameters[:q] = q
310
+ solr_parameters.merge!(additional_parameters) if additional_parameters
311
+ end
312
+
313
+ def add_search_field_with_local_parameters(solr_parameters)
314
+ local_params = search_field.solr_local_parameters.map do |key, val|
315
+ key.to_s + "=" + solr_param_quote(val, quote: "'")
316
+ end.join(" ")
317
+ solr_parameters[:q] = "{!#{local_params}}#{search_state.query_param}"
318
+
319
+ ##
320
+ # Set Solr spellcheck.q to be original user-entered query, without
321
+ # our local params, otherwise it'll try and spellcheck the local
322
+ # params!
323
+ solr_parameters["spellcheck.q"] ||= search_state.query_param
324
+ end
325
+
326
+ def add_multifield_search_query(solr_parameters)
327
+ q = search_state.query_param
328
+ solr_parameters[:q] = if q.values.any?(&:blank?)
329
+ # if any field parameters are empty, exclude _all_ results
330
+ "{!lucene}NOT *:*"
331
+ else
332
+ "{!lucene}" + q.map do |field, values|
333
+ "#{field}:(#{Array(values).map { |x| solr_param_quote(x) }.join(' OR ')})"
334
+ end.join(" AND ")
335
+ end
336
+
337
+ solr_parameters[:defType] = 'lucene'
338
+ solr_parameters[:spellcheck] = 'false'
319
339
  end
320
340
  end
321
341
  end