blacklight 7.16.0 → 7.17.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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +4 -0
  3. data/VERSION +1 -1
  4. data/app/components/blacklight/advanced_search_form_component.html.erb +9 -3
  5. data/app/components/blacklight/advanced_search_form_component.rb +48 -35
  6. data/app/components/blacklight/constraints_component.html.erb +19 -3
  7. data/app/components/blacklight/constraints_component.rb +5 -1
  8. data/app/components/blacklight/content_areas_shim.rb +12 -0
  9. data/app/components/blacklight/document/action_component.rb +4 -0
  10. data/app/components/blacklight/document/actions_component.html.erb +3 -5
  11. data/app/components/blacklight/document/actions_component.rb +14 -1
  12. data/app/components/blacklight/document_component.html.erb +4 -7
  13. data/app/components/blacklight/document_component.rb +73 -73
  14. data/app/components/blacklight/document_metadata_component.html.erb +2 -2
  15. data/app/components/blacklight/document_metadata_component.rb +13 -2
  16. data/app/components/blacklight/document_title_component.html.erb +17 -0
  17. data/app/components/blacklight/document_title_component.rb +59 -0
  18. data/app/components/blacklight/facet_field_checkboxes_component.html.erb +2 -2
  19. data/app/components/blacklight/facet_field_component.rb +4 -1
  20. data/app/components/blacklight/facet_field_list_component.html.erb +2 -2
  21. data/app/components/blacklight/facet_field_no_layout_component.rb +4 -1
  22. data/app/components/blacklight/metadata_field_component.html.erb +2 -2
  23. data/app/components/blacklight/metadata_field_layout_component.html.erb +3 -1
  24. data/app/components/blacklight/metadata_field_layout_component.rb +26 -1
  25. data/app/components/blacklight/response/view_type_button_component.html.erb +4 -0
  26. data/app/components/blacklight/response/view_type_button_component.rb +36 -0
  27. data/app/components/blacklight/response/view_type_component.html.erb +2 -5
  28. data/app/components/blacklight/response/view_type_component.rb +9 -13
  29. data/app/components/blacklight/search_bar_component.rb +4 -1
  30. data/app/components/blacklight/system/dropdown_component.html.erb +4 -7
  31. data/app/components/blacklight/system/dropdown_component.rb +24 -0
  32. data/app/components/blacklight/system/flash_message_component.html.erb +1 -1
  33. data/app/components/blacklight/system/flash_message_component.rb +7 -1
  34. data/app/components/blacklight/system/modal_component.rb +7 -1
  35. data/app/views/catalog/_citation.html.erb +1 -1
  36. data/app/views/catalog/_document.html.erb +2 -2
  37. data/app/views/catalog/_facet_layout.html.erb +2 -2
  38. data/app/views/catalog/_show_main_content.html.erb +3 -3
  39. data/app/views/catalog/email.html.erb +2 -2
  40. data/app/views/catalog/email_success.html.erb +1 -1
  41. data/app/views/catalog/facet.html.erb +3 -3
  42. data/app/views/catalog/sms.html.erb +2 -2
  43. data/app/views/catalog/sms_success.html.erb +1 -1
  44. data/blacklight.gemspec +1 -1
  45. data/config/locales/blacklight.de.yml +2 -2
  46. data/lib/blacklight/engine.rb +3 -1
  47. data/lib/blacklight/solr/facet_paginator.rb +2 -0
  48. data/lib/blacklight/solr/request.rb +31 -0
  49. data/lib/blacklight/solr/response.rb +2 -16
  50. data/lib/blacklight/solr/response/facets.rb +76 -22
  51. data/lib/blacklight/solr/response/params.rb +104 -0
  52. data/lib/blacklight/solr/search_builder_behavior.rb +49 -29
  53. data/lib/generators/blacklight/assets_generator.rb +6 -2
  54. data/lib/generators/blacklight/user_generator.rb +1 -1
  55. data/spec/components/blacklight/document_component_spec.rb +3 -3
  56. data/spec/models/blacklight/solr/facet_paginator_spec.rb +4 -0
  57. data/spec/models/blacklight/solr/request_spec.rb +62 -29
  58. data/spec/models/blacklight/solr/response/facets_spec.rb +109 -0
  59. data/spec/models/blacklight/solr/response_spec.rb +10 -0
  60. data/spec/models/blacklight/solr/search_builder_spec.rb +17 -0
  61. metadata +14 -8
@@ -66,7 +66,7 @@ module Blacklight::Solr
66
66
  elsif search_state.query_param.is_a? Hash
67
67
  add_additional_filters(solr_parameters, search_state.query_param)
68
68
  elsif search_state.query_param
69
- solr_parameters[:q] = search_state.query_param
69
+ solr_parameters.append_query search_state.query_param
70
70
  end
71
71
  end
72
72
 
@@ -75,14 +75,16 @@ module Blacklight::Solr
75
75
 
76
76
  return if q.blank?
77
77
 
78
- solr_parameters[:q] = if q.values.any?(&:blank?)
79
- # if any field parameters are empty, exclude _all_ results
80
- "{!lucene}NOT *:*"
81
- else
82
- "{!lucene}" + q.map do |field, values|
83
- "#{field}:(#{Array(values).map { |x| solr_param_quote(x) }.join(' OR ')})"
84
- end.join(" AND ")
85
- end
78
+ if q.values.any?(&:blank?)
79
+ # if any field parameters are empty, exclude _all_ results
80
+ solr_parameters.append_query "{!lucene}NOT *:*"
81
+ else
82
+ composed_query = q.map do |field, values|
83
+ "#{field}:(#{Array(values).map { |x| solr_param_quote(x) }.join(' OR ')})"
84
+ end.join(" AND ")
85
+
86
+ solr_parameters.append_query "{!lucene}#{composed_query}"
87
+ end
86
88
 
87
89
  solr_parameters[:defType] = 'lucene'
88
90
  solr_parameters[:spellcheck] = 'false'
@@ -91,9 +93,7 @@ module Blacklight::Solr
91
93
  def add_search_field_with_json_query_parameters(solr_parameters)
92
94
  bool_query = search_field.clause_params.transform_values { |v| v.merge(query: search_state.query_param) }
93
95
 
94
- solr_parameters[:json] ||= { query: { bool: { must: [] } } }
95
- solr_parameters[:json][:query] ||= { bool: { must: [] } }
96
- solr_parameters[:json][:query][:bool][:must] << bool_query
96
+ solr_parameters.append_boolean_query(:must, bool_query)
97
97
  end
98
98
 
99
99
  # Transform "clause" parameters into the Solr JSON Query DSL
@@ -101,21 +101,15 @@ module Blacklight::Solr
101
101
  return if search_state.clause_params.blank?
102
102
 
103
103
  defaults = { must: [], must_not: [], should: [] }
104
- bool_query = (solr_parameters.dig(:json, :query, :bool) || {}).reverse_merge(defaults)
105
-
106
104
  default_op = blacklight_params[:op]&.to_sym || :must
105
+ solr_parameters[:mm] = 1 if default_op == :should && search_state.clause_params.values.any? { |clause| }
107
106
 
108
107
  search_state.clause_params.each_value do |clause|
109
108
  op, query = adv_search_clause(clause, default_op)
110
- bool_query[op] << query if defaults.key?(op) && query
111
- end
112
-
113
- return if bool_query.values.all?(&:blank?)
109
+ next unless defaults.key?(op)
114
110
 
115
- solr_parameters[:mm] = 1 if default_op == :should
116
- solr_parameters[:json] ||= { query: { bool: {} } }
117
- solr_parameters[:json][:query] ||= { bool: {} }
118
- solr_parameters[:json][:query][:bool] = bool_query.reject { |_k, v| v.blank? }
111
+ solr_parameters.append_boolean_query(op, query)
112
+ end
119
113
  end
120
114
 
121
115
  # @return [Array] the first element is the query operator and the second is the value to add
@@ -141,7 +135,7 @@ module Blacklight::Solr
141
135
  if filter.config.filter_query_builder
142
136
  filter_query, subqueries = filter.config.filter_query_builder.call(self, filter, solr_parameters)
143
137
 
144
- solr_parameters.append_filter_query(filter_query)
138
+ solr_parameters.append_filter_query(filter_query) if filter_query
145
139
  solr_parameters.merge!(subqueries) if subqueries
146
140
  else
147
141
  filter.values.reject(&:blank?).each do |value|
@@ -158,6 +152,21 @@ module Blacklight::Solr
158
152
  end
159
153
  end
160
154
 
155
+ def add_solr_facet_json_params(solr_parameters, field_name, facet, **additional_parameters)
156
+ solr_parameters[:json] ||= { facet: {} }
157
+ solr_parameters[:json][:facet] ||= {}
158
+
159
+ field_config = facet.json.respond_to?(:reverse_merge) ? facet.json : {}
160
+
161
+ field_config = field_config.reverse_merge(
162
+ type: 'terms',
163
+ field: facet.field,
164
+ limit: facet_limit_with_pagination(field_name)
165
+ ).merge(additional_parameters)
166
+
167
+ solr_parameters[:json][:facet][field_name] = field_config.select { |_k, v| v.present? }
168
+ end
169
+
161
170
  ##
162
171
  # Add appropriate Solr facetting directives in, including
163
172
  # taking account of our facet paging/'more'. This is not
@@ -166,6 +175,11 @@ module Blacklight::Solr
166
175
  facet_fields_to_include_in_request.each do |field_name, facet|
167
176
  solr_parameters[:facet] ||= true
168
177
 
178
+ if facet.json
179
+ add_solr_facet_json_params(solr_parameters, field_name, facet)
180
+ next
181
+ end
182
+
169
183
  if facet.pivot
170
184
  solr_parameters.append_facet_pivot with_ex_local_param(facet.ex, facet.pivot.join(","))
171
185
  elsif facet.query
@@ -235,9 +249,7 @@ module Blacklight::Solr
235
249
 
236
250
  facet_config = blacklight_config.facet_fields[facet]
237
251
 
238
- # Now override with our specific things for fetching facet values
239
- facet_ex = facet_config.respond_to?(:ex) ? facet_config.ex : nil
240
- solr_params[:"facet.field"] = with_ex_local_param(facet_ex, facet_config.field)
252
+ solr_params[:rows] = 0
241
253
 
242
254
  limit = if solr_params["facet.limit"]
243
255
  solr_params["facet.limit"].to_i
@@ -250,13 +262,21 @@ module Blacklight::Solr
250
262
  prefix = search_state.facet_prefix
251
263
  offset = (page - 1) * limit
252
264
 
265
+ if facet_config.json
266
+ add_solr_facet_json_params(solr_parameters, facet, facet_config, limit: limit + 1, offset: offset, sort: sort, prefix: prefix)
267
+ return
268
+ end
269
+
270
+ # Now override with our specific things for fetching facet values
271
+ facet_ex = facet_config.respond_to?(:ex) ? facet_config.ex : nil
272
+ solr_params[:"facet.field"] = with_ex_local_param(facet_ex, facet_config.field)
273
+
253
274
  # Need to set as f.facet_field.facet.* to make sure we
254
275
  # override any field-specific default in the solr request handler.
255
276
  solr_params[:"f.#{facet_config.field}.facet.limit"] = limit + 1
256
277
  solr_params[:"f.#{facet_config.field}.facet.offset"] = offset
257
278
  solr_params[:"f.#{facet_config.field}.facet.sort"] = sort if sort
258
279
  solr_params[:"f.#{facet_config.field}.facet.prefix"] = prefix if prefix
259
- solr_params[:rows] = 0
260
280
  end
261
281
 
262
282
  def with_ex_local_param(ex, value)
@@ -395,7 +415,7 @@ module Blacklight::Solr
395
415
  def add_search_field_query_builder_params(solr_parameters)
396
416
  q, additional_parameters = search_field.query_builder.call(self, search_field, solr_parameters)
397
417
 
398
- solr_parameters[:q] = q
418
+ solr_parameters.append_query q
399
419
  solr_parameters.merge!(additional_parameters) if additional_parameters
400
420
  end
401
421
 
@@ -403,7 +423,7 @@ module Blacklight::Solr
403
423
  local_params = search_field.solr_local_parameters.map do |key, val|
404
424
  key.to_s + "=" + solr_param_quote(val, quote: "'")
405
425
  end.join(" ")
406
- solr_parameters[:q] = "{!#{local_params}}#{search_state.query_param}"
426
+ solr_parameters.append_query "{!#{local_params}}#{search_state.query_param}"
407
427
 
408
428
  ##
409
429
  # Set Solr spellcheck.q to be original user-entered query, without
@@ -68,11 +68,15 @@ module Blacklight
68
68
  private
69
69
 
70
70
  def turbolinks?
71
- @turbolinks ||= IO.read("app/assets/javascripts/application.js").include?('turbolinks')
71
+ @turbolinks ||= application_js.include?('turbolinks')
72
72
  end
73
73
 
74
74
  def has_blacklight_assets?
75
- IO.read("app/assets/javascripts/application.js").include?('blacklight/blacklight')
75
+ application_js.include?('blacklight/blacklight')
76
+ end
77
+
78
+ def application_js
79
+ IO.read(File.expand_path("app/assets/javascripts/application.js", destination_root))
76
80
  end
77
81
  end
78
82
  end
@@ -47,7 +47,7 @@ module Blacklight
47
47
  # Add Blacklight to the user model
48
48
  def inject_blacklight_user_behavior
49
49
  file_path = "app/models/#{model_name.underscore}.rb"
50
- if File.exist?(file_path)
50
+ if File.exist?(File.expand_path(file_path, destination_root))
51
51
  inject_into_class file_path, model_name.classify do
52
52
  "\n # Connects this user object to Blacklights Bookmarks." \
53
53
  "\n include Blacklight::User\n"
@@ -3,7 +3,7 @@
3
3
  require 'spec_helper'
4
4
 
5
5
  RSpec.describe Blacklight::DocumentComponent, type: :component do
6
- subject(:component) { described_class.new(document: document, **attr) }
6
+ subject(:component) { described_class.new(document: document, presenter: view_context.document_presenter(document), **attr) }
7
7
 
8
8
  let(:attr) { {} }
9
9
  let(:view_context) { controller.view_context }
@@ -47,7 +47,7 @@ RSpec.describe Blacklight::DocumentComponent, type: :component do
47
47
  component.with(:embed, 'Embed')
48
48
  component.with(:metadata, 'Metadata')
49
49
  component.with(:thumbnail, 'Thumbnail')
50
- component.with(:actions, 'Actions')
50
+ component.with(:actions) { 'Actions' }
51
51
 
52
52
  expect(rendered).to have_content 'Title'
53
53
  expect(rendered).to have_content 'Embed'
@@ -65,7 +65,7 @@ RSpec.describe Blacklight::DocumentComponent, type: :component do
65
65
 
66
66
  context 'with a provided body' do
67
67
  it 'opts-out of normal component content' do
68
- component.with(:body, 'Body content')
68
+ component.with(:body) { 'Body content' }
69
69
 
70
70
  expect(rendered).to have_content 'Body content'
71
71
  expect(rendered).not_to have_selector 'header'
@@ -20,5 +20,9 @@ RSpec.describe Blacklight::Solr::FacetPaginator, api: true do
20
20
  it 'defaults to "index" if no limit is given' do
21
21
  expect(described_class.new([]).sort).to eq 'index'
22
22
  end
23
+
24
+ it 'handles json facet api-style parameter sorts' do
25
+ expect(described_class.new([], sort: { count: :desc }).sort).to eq 'count'
26
+ end
23
27
  end
24
28
  end
@@ -1,36 +1,69 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  RSpec.describe Blacklight::Solr::Request, api: true do
4
- before do
5
- subject[:qt] = 'hey'
6
- subject[:fq] = ["what's up.", "dood"]
7
- subject['q'] = "what's"
8
- subject[:wt] = "going"
9
- subject[:start] = "on"
10
- subject[:rows] = "Man"
11
- subject['hl'] = "I"
12
- subject['hl.fl'] = "wish"
13
- subject['group'] = "I"
14
- subject['defType'] = "had"
15
- subject['spellcheck'] = "a"
16
- subject['spellcheck.q'] = "fleece"
17
- subject['f.title_facet.facet.limit'] = "vest"
18
- subject['facet.field'] = []
4
+ context 'with some solr parameter keys' do
5
+ before do
6
+ subject[:qt] = 'hey'
7
+ subject[:fq] = ["what's up.", "dood"]
8
+ subject['q'] = "what's"
9
+ subject[:wt] = "going"
10
+ subject[:start] = "on"
11
+ subject[:rows] = "Man"
12
+ subject['hl'] = "I"
13
+ subject['hl.fl'] = "wish"
14
+ subject['group'] = "I"
15
+ subject['defType'] = "had"
16
+ subject['spellcheck'] = "a"
17
+ subject['spellcheck.q'] = "fleece"
18
+ subject['f.title_facet.facet.limit'] = "vest"
19
+ subject['facet.field'] = []
20
+ end
21
+
22
+ it "accepts valid parameters" do
23
+ expect(subject.to_hash).to eq("defType" => "had",
24
+ "f.title_facet.facet.limit" => "vest",
25
+ "fq" => ["what's up.", "dood"],
26
+ "group" => "I",
27
+ "hl" => "I",
28
+ "hl.fl" => "wish",
29
+ "q" => "what's",
30
+ "qt" => "hey",
31
+ "rows" => "Man",
32
+ "spellcheck" => "a",
33
+ "spellcheck.q" => "fleece",
34
+ "start" => "on",
35
+ "wt" => "going")
36
+ end
37
+ end
38
+
39
+ describe '#append_query' do
40
+ it 'populates the q parameter' do
41
+ subject.append_query 'this is my query'
42
+ expect(subject['q']).to eq 'this is my query'
43
+ end
44
+
45
+ it 'handles multiple queries by converting it to a boolean query' do
46
+ subject.append_query 'this is my query'
47
+ subject.append_query 'another:query'
48
+ expect(subject).not_to have_key 'q'
49
+ expect(subject.dig('json', 'query', 'bool', 'must')).to match_array ['this is my query', 'another:query']
50
+ end
19
51
  end
20
52
 
21
- it "accepts valid parameters" do
22
- expect(subject.to_hash).to eq("defType" => "had",
23
- "f.title_facet.facet.limit" => "vest",
24
- "fq" => ["what's up.", "dood"],
25
- "group" => "I",
26
- "hl" => "I",
27
- "hl.fl" => "wish",
28
- "q" => "what's",
29
- "qt" => "hey",
30
- "rows" => "Man",
31
- "spellcheck" => "a",
32
- "spellcheck.q" => "fleece",
33
- "start" => "on",
34
- "wt" => "going")
53
+ describe '#append_boolean_query' do
54
+ it 'populates the boolean query with the queries' do
55
+ subject.append_boolean_query :must, 'required'
56
+ subject.append_boolean_query :should, 'optional'
57
+ subject.append_boolean_query :should, 'also optional'
58
+
59
+ expect(subject.dig('json', 'query', 'bool')).to include should: ['optional', 'also optional'], must: ['required']
60
+ end
61
+
62
+ it 'converts existing q parameters to a boolean query' do
63
+ subject['q'] = 'some query'
64
+ subject.append_boolean_query :must, 'also required'
65
+
66
+ expect(subject.dig('json', 'query', 'bool', 'must')).to match_array ['some query', 'also required']
67
+ end
35
68
  end
36
69
  end
@@ -160,6 +160,12 @@ RSpec.describe Blacklight::Solr::Response::Facets, api: true do
160
160
  expect(missing.label).to eq "[Missing]"
161
161
  expect(missing.fq).to eq "-some_field:[* TO *]"
162
162
  end
163
+
164
+ it 'extracts the missing field data to a separate facet field attribute' do
165
+ missing = subject.aggregations["some_field"].missing
166
+
167
+ expect(missing).to have_attributes(label: '[Missing]', hits: 2)
168
+ end
163
169
  end
164
170
 
165
171
  describe "query facets" do
@@ -278,4 +284,107 @@ RSpec.describe Blacklight::Solr::Response::Facets, api: true do
278
284
  expect(field.items.first.items.first.fq).to eq('field_a' => 'a')
279
285
  end
280
286
  end
287
+
288
+ describe 'json facets' do
289
+ subject { Blacklight::Solr::Response.new(response, {}, blacklight_config: blacklight_config) }
290
+
291
+ let(:response) do
292
+ {
293
+ facets: {
294
+ "count": 32,
295
+ "categories": {
296
+ "buckets": [
297
+ {
298
+ "val": "electronics",
299
+ "count": 12,
300
+ "max_price": 60
301
+ },
302
+ {
303
+ "val": "currency",
304
+ "count": 4
305
+ },
306
+ {
307
+ "val": "memory",
308
+ "count": 3
309
+ }
310
+ ]
311
+ }
312
+ }
313
+ }
314
+ end
315
+ let(:facet_config) do
316
+ Blacklight::Configuration::FacetField.new(key: 'categories', json: true, query: false)
317
+ end
318
+
319
+ let(:blacklight_config) { double(facet_fields: { 'categories' => facet_config }) }
320
+ let(:field) { subject.aggregations['categories'] }
321
+
322
+ it 'has access to the original response data' do
323
+ expect(field.data).to include 'buckets'
324
+ end
325
+
326
+ it 'converts buckets into facet items' do
327
+ expect(field.items.length).to eq 3
328
+ end
329
+
330
+ context 'with nested buckets' do
331
+ let(:response) do
332
+ {
333
+ facets: {
334
+ "categories": {
335
+ "buckets": [
336
+ {
337
+ "val": "electronics",
338
+ "count": 12,
339
+ "top_manufacturer": {
340
+ "buckets": [{
341
+ "val": "corsair",
342
+ "count": 3
343
+ }]
344
+ }
345
+ },
346
+ {
347
+ "val": "currency",
348
+ "count": 4,
349
+ "top_manufacturer": {
350
+ "buckets": [{
351
+ "val": "boa",
352
+ "count": 1
353
+ }]
354
+ }
355
+ }
356
+ ]
357
+ }
358
+ }
359
+ }
360
+ end
361
+
362
+ it 'converts nested buckets into pivot facets' do
363
+ expect(field.items.first).to have_attributes hits: 12
364
+ expect(field.items.first.items.first).to have_attributes field: 'top_manufacturer', value: 'corsair', hits: 3, fq: { "categories" => "electronics" }
365
+ end
366
+ end
367
+
368
+ context 'with missing values' do
369
+ let(:response) do
370
+ {
371
+ facets: {
372
+ "categories": {
373
+ "missing" => { "count" => 13 },
374
+ "buckets" => [{ "val" => "India", "count" => 2 }, { "val" => "Iran", "count" => 2 }]
375
+ }
376
+ }
377
+ }
378
+ end
379
+
380
+ it 'converts "missing" facet data into a missing facet item' do
381
+ expect(field.items.length).to eq 2
382
+ expect(field.missing).to have_attributes(hits: 13)
383
+ end
384
+ end
385
+
386
+ it 'exposes any extra query function results' do
387
+ expect(field.items.first.data).to include 'max_price' => 60
388
+ end
389
+ end
281
390
  end
@@ -120,6 +120,16 @@ RSpec.describe Blacklight::Solr::Response, api: true do
120
120
  expect(r.params['test']).to eq :test
121
121
  end
122
122
 
123
+ it 'extracts json params' do
124
+ raw_response = eval(mock_query_response)
125
+ raw_response['responseHeader']['params']['test'] = 'from query'
126
+ raw_response['responseHeader']['params'].delete('rows')
127
+ raw_response['responseHeader']['params']['json'] = { limit: 5, params: { test: 'from json params' } }.to_json
128
+ r = described_class.new(raw_response, raw_response['params'])
129
+ expect(r.params['test']).to eq 'from query'
130
+ expect(r.rows).to eq 5
131
+ end
132
+
123
133
  it 'provides the solr-returned params and "rows" should be 11' do
124
134
  raw_response = eval(mock_query_response)
125
135
  r = described_class.new(raw_response, {})