blacklight 7.16.0 → 7.17.0

Sign up to get free protection for your applications and to get access to all the features.
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, {})