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
@@ -10,6 +10,10 @@
10
10
  "type": "git",
11
11
  "url": "git+https://github.com/projectblacklight/blacklight.git"
12
12
  },
13
+ "files": [
14
+ "app/assets",
15
+ "app/javascript"
16
+ ],
13
17
  "author": "",
14
18
  "license": "Apache-2.0",
15
19
  "bugs": {
@@ -42,11 +42,13 @@ RSpec.describe Blacklight::DocumentComponent, type: :component do
42
42
 
43
43
  it 'has some defined content areas' do
44
44
  component.with(:title, 'Title')
45
+ component.with(:embed, 'Embed')
45
46
  component.with(:metadata, 'Metadata')
46
47
  component.with(:thumbnail, 'Thumbnail')
47
48
  component.with(:actions, 'Actions')
48
49
 
49
50
  expect(rendered).to have_content 'Title'
51
+ expect(rendered).to have_content 'Embed'
50
52
  expect(rendered).to have_content 'Metadata'
51
53
  expect(rendered).to have_content 'Thumbnail'
52
54
  expect(rendered).to have_content 'Actions'
@@ -126,6 +128,19 @@ RSpec.describe Blacklight::DocumentComponent, type: :component do
126
128
  expect(rendered).to have_selector 'dt', text: 'ISBN:'
127
129
  expect(rendered).to have_selector 'dd', text: 'Value'
128
130
  end
131
+
132
+ it 'renders an embed' do
133
+ stub_const('StubComponent', Class.new(ViewComponent::Base) do
134
+ def initialize(**); end
135
+
136
+ def call
137
+ 'embed'
138
+ end
139
+ end)
140
+
141
+ blacklight_config.show.embed_component = StubComponent
142
+ expect(rendered).to have_content 'embed'
143
+ end
129
144
  end
130
145
 
131
146
  it 'renders metadata' do
@@ -115,9 +115,4 @@ RSpec.describe "Search Page" do
115
115
  expect(page).to have_content "Welcome!"
116
116
  expect(page).not_to have_selector "#q[value='history']"
117
117
  end
118
-
119
- it "handles searches with invalid facet parameters" do
120
- visit root_path f: { missing_s: [1] }
121
- expect(page).to have_content "No results found for your search"
122
- end
123
118
  end
@@ -122,8 +122,7 @@ RSpec.describe Blacklight::ConfigurationHelperBehavior do
122
122
 
123
123
  describe "#view_label" do
124
124
  it "looks up the label to display for the view" do
125
- allow(blacklight_config).to receive(:view).and_return("my_view" => double(label: "some label", title: nil))
126
- allow(helper).to receive(:field_label).with(:"blacklight.search.view_title.my_view", :"blacklight.search.view.my_view", "some label", nil, "My view")
125
+ allow(blacklight_config).to receive(:view).and_return("my_view" => double(display_label: "some label"))
127
126
 
128
127
  helper.view_label "my_view"
129
128
  end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Blacklight::Configuration::ViewConfig do
4
+ subject { described_class.new(key: key, label: label) }
5
+
6
+ let(:key) { 'my_view' }
7
+ let(:label) { 'some label' }
8
+
9
+ describe '#display_label' do
10
+ it "looks up the label to display for the given document and field" do
11
+ allow(I18n).to receive(:t).with(:"blacklight.search.view_title.my_view", default: [:"blacklight.search.view.my_view", label, nil, "My view"]).and_return('x')
12
+ expect(subject.display_label(key)).to eq 'x'
13
+ end
14
+ end
15
+ end
@@ -14,4 +14,13 @@ RSpec.describe Blacklight::NestedOpenStructWithHashAccess do
14
14
  expect(copy.a[:b]).to eq 1
15
15
  end
16
16
  end
17
+
18
+ describe '#<<' do
19
+ subject { described_class.new(Blacklight::Configuration::Field) }
20
+
21
+ it 'includes the key in the hash' do
22
+ subject << :blah
23
+ expect(subject.blah).to have_attributes(key: :blah)
24
+ end
25
+ end
17
26
  end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Blacklight::SearchState::FilterField do
4
+ let(:search_state) { Blacklight::SearchState.new(params.with_indifferent_access, blacklight_config, controller) }
5
+
6
+ let(:params) { { f: { some_field: %w[1 2], another_field: ['3'] } } }
7
+ let(:blacklight_config) do
8
+ Blacklight::Configuration.new.configure do |config|
9
+ config.add_facet_field 'some_field'
10
+ config.add_facet_field 'another_field', single: true
11
+ end
12
+ end
13
+ let(:controller) { double }
14
+
15
+ describe '#add' do
16
+ it 'adds the parameter to the filter list' do
17
+ filter = search_state.filter('some_field')
18
+ new_state = filter.add('4')
19
+
20
+ expect(new_state.filter('some_field').values).to eq %w[1 2 4]
21
+ end
22
+
23
+ it 'creates new parameter as needed' do
24
+ filter = search_state.filter('unknown_field')
25
+ new_state = filter.add('4')
26
+
27
+ expect(new_state.filter('unknown_field').values).to eq %w[4]
28
+ expect(new_state.params[:f]).to include(:unknown_field)
29
+ end
30
+
31
+ context 'without any parameters in the url' do
32
+ let(:params) { {} }
33
+
34
+ it 'adds the necessary structure' do
35
+ filter = search_state.filter('some_field')
36
+ new_state = filter.add('1')
37
+
38
+ expect(new_state.filter('some_field').values).to eq %w[1]
39
+ expect(new_state.params).to include(:f)
40
+ end
41
+ end
42
+
43
+ context 'with a single-valued field' do
44
+ it 'replaces any existing parameter from the filter list' do
45
+ filter = search_state.filter('another_field')
46
+ new_state = filter.add('5')
47
+
48
+ expect(new_state.filter('another_field').values).to eq %w[5]
49
+ end
50
+ end
51
+
52
+ context 'with a pivot facet-type item' do
53
+ it 'includes the pivot facet fqs' do
54
+ filter = search_state.filter('some_field')
55
+ new_state = filter.add(OpenStruct.new(fq: { some_other_field: '5' }, value: '4'))
56
+
57
+ expect(new_state.filter('some_field').values).to eq %w[1 2 4]
58
+ expect(new_state.filter('some_other_field').values).to eq %w[5]
59
+ end
60
+
61
+ it 'handles field indirection' do
62
+ filter = search_state.filter('some_field')
63
+ new_state = filter.add(OpenStruct.new(field: 'some_other_field', value: '4'))
64
+
65
+ expect(new_state.filter('some_other_field').values).to eq %w[4]
66
+ end
67
+
68
+ it 'handles value indirection' do
69
+ filter = search_state.filter('some_field')
70
+ new_state = filter.add(OpenStruct.new(value: '4'))
71
+
72
+ expect(new_state.filter('some_field').values).to eq %w[1 2 4]
73
+ end
74
+ end
75
+ end
76
+
77
+ describe '#remove' do
78
+ it 'returns a search state without the given filter applied' do
79
+ filter = search_state.filter('some_field')
80
+ new_state = filter.remove('1')
81
+
82
+ expect(new_state.filter('some_field').values).to eq ['2']
83
+ end
84
+
85
+ it 'removes the whole field if there are no filter left for the field' do
86
+ filter = search_state.filter('another_field')
87
+ new_state = filter.remove('3')
88
+
89
+ expect(new_state.filter('another_field').values).to eq []
90
+ expect(new_state.params[:f]).not_to include :another_field
91
+ end
92
+
93
+ it 'removes the filter parameter entirely if there are no filters left' do
94
+ new_state = search_state.filter('some_field').remove('1')
95
+ new_state = new_state.filter('some_field').remove('2')
96
+ new_state = new_state.filter('another_field').remove('3')
97
+
98
+ expect(new_state.params).not_to include :f
99
+ end
100
+
101
+ it 'handles value indirection' do
102
+ filter = search_state.filter('some_field')
103
+ new_state = filter.remove(OpenStruct.new(value: '1'))
104
+
105
+ expect(new_state.filter('some_field').values).to eq ['2']
106
+ end
107
+ end
108
+
109
+ describe '#values' do
110
+ it 'returns the currently selected values of the filter' do
111
+ expect(search_state.filter('some_field').values).to eq %w[1 2]
112
+ end
113
+ end
114
+
115
+ describe '#include?' do
116
+ it 'checks whether the value is currently selected' do
117
+ expect(search_state.filter('some_field').include?('1')).to eq true
118
+ expect(search_state.filter('some_field').include?('3')).to eq false
119
+ end
120
+
121
+ it 'handles value indirection' do
122
+ expect(search_state.filter('some_field').include?(OpenStruct.new(value: '1'))).to eq true
123
+ end
124
+ end
125
+ end
@@ -3,6 +3,8 @@
3
3
  RSpec.describe Blacklight::SearchState do
4
4
  subject(:search_state) { described_class.new(params, blacklight_config, controller) }
5
5
 
6
+ around { |test| Deprecation.silence(described_class) { test.call } }
7
+
6
8
  let(:blacklight_config) do
7
9
  Blacklight::Configuration.new.configure do |config|
8
10
  config.index.title_field = 'title_tsim'
@@ -242,9 +244,9 @@ RSpec.describe Blacklight::SearchState do
242
244
  end
243
245
 
244
246
  it "uses the facet's key in the url" do
245
- allow(search_state).to receive(:facet_configuration_for_field).with('some_field').and_return(double(single: true, field: "a_solr_field", key: "some_key"))
247
+ blacklight_config.add_facet_field 'some_key', single: true, field: "a_solr_field"
246
248
 
247
- result_params = search_state.add_facet_params('some_field', 'my_value')
249
+ result_params = search_state.add_facet_params('some_key', 'my_value')
248
250
 
249
251
  expect(result_params[:f]['some_key']).to have(1).item
250
252
  expect(result_params[:f]['some_key'].first).to eq 'my_value'
@@ -284,7 +286,7 @@ RSpec.describe Blacklight::SearchState do
284
286
  let(:params) { parameter_class.new f: { 'single_value_facet_field' => 'other_value' } }
285
287
 
286
288
  it "replaces facets configured as single" do
287
- allow(search_state).to receive(:facet_configuration_for_field).with('single_value_facet_field').and_return(double(single: true, key: "single_value_facet_field"))
289
+ blacklight_config.add_facet_field 'single_value_facet_field', single: true
288
290
  result_params = search_state.add_facet_params('single_value_facet_field', 'my_value')
289
291
 
290
292
  expect(result_params[:f]['single_value_facet_field']).to have(1).item
@@ -415,4 +417,131 @@ RSpec.describe Blacklight::SearchState do
415
417
  expect(new_state.to_hash).to eq('a' => 1)
416
418
  end
417
419
  end
420
+
421
+ describe '#page' do
422
+ context 'with a page' do
423
+ let(:params) { { 'page' => '3' } }
424
+
425
+ it 'is mapped from page' do
426
+ expect(search_state.page).to eq 3
427
+ end
428
+ end
429
+
430
+ context 'without a page' do
431
+ let(:params) { {} }
432
+
433
+ it 'is defaults to page 1' do
434
+ expect(search_state.page).to eq 1
435
+ end
436
+
437
+ context 'with negative numbers or other bad data' do
438
+ let(:params) { { 'page' => '-3' } }
439
+
440
+ it 'is defaults to page 1' do
441
+ expect(search_state.page).to eq 1
442
+ end
443
+ end
444
+ end
445
+ end
446
+
447
+ describe '#per_page' do
448
+ context 'with rows' do
449
+ let(:params) { { rows: '30' } }
450
+
451
+ it 'maps from rows' do
452
+ expect(search_state.per_page).to eq 30
453
+ end
454
+ end
455
+
456
+ context 'with per_page' do
457
+ let(:params) { { per_page: '14' } }
458
+
459
+ it 'maps from rows' do
460
+ expect(search_state.per_page).to eq 14
461
+ end
462
+ end
463
+
464
+ context 'it defaults to the configured value' do
465
+ let(:params) { {} }
466
+
467
+ it 'maps from rows' do
468
+ expect(search_state.per_page).to eq 10
469
+ end
470
+ end
471
+ end
472
+
473
+ describe '#sort_field' do
474
+ let(:params) { { 'sort' => 'author' } }
475
+
476
+ before do
477
+ blacklight_config.add_sort_field 'relevancy', label: 'relevance'
478
+ blacklight_config.add_sort_field 'author', label: 'asd'
479
+ end
480
+
481
+ it 'returns the current search field' do
482
+ expect(search_state.sort_field).to have_attributes(key: 'author')
483
+ end
484
+
485
+ context 'without a search field' do
486
+ let(:params) { {} }
487
+
488
+ it 'returns the current search field' do
489
+ expect(search_state.sort_field).to have_attributes(key: 'relevancy')
490
+ end
491
+ end
492
+ end
493
+
494
+ describe '#search_field' do
495
+ let(:params) { { 'search_field' => 'author' } }
496
+
497
+ before do
498
+ blacklight_config.add_search_field 'author', label: 'asd'
499
+ end
500
+
501
+ it 'returns the current search field' do
502
+ expect(search_state.search_field).to have_attributes(key: 'author')
503
+ end
504
+ end
505
+
506
+ describe '#facet_page' do
507
+ context 'with a page' do
508
+ let(:params) { { 'facet.page' => '3' } }
509
+
510
+ it 'is mapped from facet.page' do
511
+ expect(search_state.facet_page).to eq 3
512
+ end
513
+ end
514
+
515
+ context 'without a page' do
516
+ let(:params) { {} }
517
+
518
+ it 'is defaults to page 1' do
519
+ expect(search_state.facet_page).to eq 1
520
+ end
521
+ end
522
+
523
+ context 'with negative numbers or other bad data' do
524
+ let(:params) { { 'facet.page' => '-3' } }
525
+
526
+ it 'is defaults to page 1' do
527
+ expect(search_state.facet_page).to eq 1
528
+ end
529
+ end
530
+ end
531
+
532
+ describe '#facet_sort' do
533
+ let(:params) { { 'facet.sort' => 'index' } }
534
+
535
+ it 'is mapped from facet.sort' do
536
+ expect(search_state.facet_sort).to eq 'index'
537
+ end
538
+ end
539
+
540
+ describe '#facet_prefix' do
541
+ let(:params) { { 'facet.prefix' => 'A' } }
542
+
543
+ it 'is mapped from facet.prefix' do
544
+ expect(search_state.facet_prefix).to eq 'A'
545
+ end
546
+ end
418
547
  end
@@ -62,6 +62,14 @@ RSpec.describe "Blacklight::Configuration", api: true do
62
62
  end
63
63
  end
64
64
 
65
+ describe 'config.index.document_actions' do
66
+ it 'allows you to use the << operator' do
67
+ config.index.document_actions << :blah
68
+ expect(config.index.document_actions.blah).to have_attributes key: :blah
69
+ expect(config.index.document_actions.blah.name).to eq :blah
70
+ end
71
+ end
72
+
65
73
  describe "config.index.respond_to" do
66
74
  it "has a list of additional formats for index requests to respond to" do
67
75
  config.index.respond_to.xml = true
@@ -186,9 +186,9 @@ RSpec.describe Blacklight::Solr::SearchBuilderBehavior, api: true do
186
186
  end
187
187
 
188
188
  describe "for request params also passed in as argument" do
189
- let(:user_params) { { q: "some query", 'q' => 'another value' } }
189
+ let(:user_params) { { 'q' => 'another value', q: "some query" } }
190
190
 
191
- it "onlies have one value for the key 'q' regardless if a symbol or string" do
191
+ it "only has one value for the key 'q' regardless if a symbol or string" do
192
192
  expect(subject[:q]).to eq 'some query'
193
193
  expect(subject['q']).to eq 'some query'
194
194
  end
@@ -244,6 +244,21 @@ RSpec.describe Blacklight::Solr::SearchBuilderBehavior, api: true do
244
244
  end
245
245
  end
246
246
 
247
+ describe 'with a facet with a custom filter query builder' do
248
+ let(:user_params) { { f: { some: ['value'] } }.with_indifferent_access }
249
+
250
+ before do
251
+ blacklight_config.add_facet_field 'some', filter_query_builder: (lambda do |*_args|
252
+ ['some:filter', { qq1: 'abc' }]
253
+ end)
254
+ end
255
+
256
+ it "has proper solr parameters" do
257
+ expect(subject[:fq]).to include('some:filter')
258
+ expect(subject[:qq1]).to include('abc')
259
+ end
260
+ end
261
+
247
262
  describe "solr parameters for a field search from config (subject)" do
248
263
  let(:user_params) { subject_search_params }
249
264
 
@@ -384,6 +399,21 @@ RSpec.describe Blacklight::Solr::SearchBuilderBehavior, api: true do
384
399
  end
385
400
  end
386
401
 
402
+ describe 'the search field query_builder config' do
403
+ let(:blacklight_config) do
404
+ Blacklight::Configuration.new do |config|
405
+ config.add_search_field('built_query', query_builder: ->(builder, *_args) { [builder.blacklight_params[:q].reverse, qq1: 'xyz'] })
406
+ end
407
+ end
408
+
409
+ let(:user_params) { { search_field: 'built_query', q: 'value' } }
410
+
411
+ it 'uses the provided query builder' do
412
+ expect(subject[:q]).to eq 'eulav'
413
+ expect(subject[:qq1]).to eq 'xyz'
414
+ end
415
+ end
416
+
387
417
  describe "mapping facet.field" do
388
418
  let(:blacklight_config) do
389
419
  Blacklight::Configuration.new do |config|