blacklight 7.12.1 → 7.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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