blacklight 7.14.1 → 7.15.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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/app/components/blacklight/advanced_search_form_component.html.erb +46 -0
  4. data/app/components/blacklight/advanced_search_form_component.rb +75 -0
  5. data/app/components/blacklight/constraint_component.html.erb +1 -1
  6. data/app/components/blacklight/constraints_component.rb +36 -17
  7. data/app/components/blacklight/document/thumbnail_component.html.erb +1 -1
  8. data/app/components/blacklight/document/thumbnail_component.rb +4 -1
  9. data/app/components/blacklight/document_component.rb +7 -2
  10. data/app/components/blacklight/facet_field_checkboxes_component.html.erb +23 -0
  11. data/app/components/blacklight/facet_field_checkboxes_component.rb +24 -0
  12. data/app/components/blacklight/facet_field_inclusive_constraint_component.html.erb +6 -0
  13. data/app/components/blacklight/facet_field_inclusive_constraint_component.rb +29 -0
  14. data/app/components/blacklight/facet_field_list_component.html.erb +1 -0
  15. data/app/components/blacklight/facet_item_component.rb +2 -0
  16. data/app/components/blacklight/search_bar_component.html.erb +4 -0
  17. data/app/components/blacklight/search_bar_component.rb +4 -2
  18. data/app/controllers/concerns/blacklight/catalog.rb +6 -0
  19. data/app/helpers/blacklight/render_constraints_helper_behavior.rb +2 -2
  20. data/app/presenters/blacklight/clause_presenter.rb +37 -0
  21. data/app/presenters/blacklight/document_presenter.rb +5 -1
  22. data/app/presenters/blacklight/facet_field_presenter.rb +4 -0
  23. data/app/presenters/blacklight/facet_grouped_item_presenter.rb +45 -0
  24. data/app/presenters/blacklight/facet_item_presenter.rb +32 -20
  25. data/app/presenters/blacklight/inclusive_facet_item_presenter.rb +16 -0
  26. data/app/presenters/blacklight/search_bar_presenter.rb +4 -0
  27. data/app/views/catalog/_advanced_search_form.html.erb +7 -0
  28. data/app/views/catalog/_advanced_search_help.html.erb +24 -0
  29. data/app/views/catalog/_search_form.html.erb +1 -0
  30. data/app/views/catalog/advanced_search.html.erb +17 -0
  31. data/blacklight.gemspec +1 -1
  32. data/config/i18n-tasks.yml +1 -0
  33. data/config/locales/blacklight.en.yml +17 -0
  34. data/lib/blacklight/configuration.rb +2 -1
  35. data/lib/blacklight/routes/searchable.rb +1 -0
  36. data/lib/blacklight/search_builder.rb +2 -0
  37. data/lib/blacklight/search_state.rb +5 -1
  38. data/lib/blacklight/search_state/filter_field.rb +17 -7
  39. data/lib/blacklight/solr/repository.rb +11 -2
  40. data/lib/blacklight/solr/search_builder_behavior.rb +87 -23
  41. data/spec/components/blacklight/advanced_search_form_component_spec.rb +51 -0
  42. data/spec/components/blacklight/document_component_spec.rb +15 -0
  43. data/spec/components/blacklight/facet_field_checkboxes_component_spec.rb +55 -0
  44. data/spec/components/blacklight/facet_field_list_component_spec.rb +39 -4
  45. data/spec/controllers/catalog_controller_spec.rb +9 -0
  46. data/spec/features/advanced_search_spec.rb +67 -0
  47. data/spec/lib/blacklight/search_state/filter_field_spec.rb +65 -0
  48. data/spec/models/blacklight/solr/repository_spec.rb +12 -0
  49. data/spec/models/blacklight/solr/search_builder_spec.rb +28 -0
  50. data/spec/presenters/blacklight/clause_presenter_spec.rb +34 -0
  51. data/spec/presenters/blacklight/document_presenter_spec.rb +13 -0
  52. data/spec/presenters/blacklight/facet_grouped_item_presenter_spec.rb +41 -0
  53. metadata +29 -7
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Blacklight::AdvancedSearchFormComponent, type: :component do
6
+ subject(:render) do
7
+ component.render_in(view_context)
8
+ end
9
+
10
+ let(:component) { described_class.new(url: '/whatever', response: response, params: params) }
11
+ let(:response) { Blacklight::Solr::Response.new({ facet_counts: { facet_fields: { format: { 'Book' => 10, 'CD' => 5 } } } }.with_indifferent_access, {}) }
12
+ let(:params) { {} }
13
+
14
+ let(:rendered) do
15
+ Capybara::Node::Simple.new(render)
16
+ end
17
+
18
+ let(:view_context) { controller.view_context }
19
+
20
+ before do
21
+ allow(view_context).to receive(:facet_limit_for).and_return(nil)
22
+ end
23
+
24
+ context 'with additional parameters' do
25
+ let(:params) { { some: :parameter, an_array: [1, 2] } }
26
+
27
+ it 'adds additional parameters as hidden fields' do
28
+ expect(rendered).to have_field 'some', with: 'parameter', type: :hidden
29
+ expect(rendered).to have_field 'an_array[]', with: '1', type: :hidden
30
+ expect(rendered).to have_field 'an_array[]', with: '2', type: :hidden
31
+ end
32
+ end
33
+
34
+ it 'has text fields for each search field' do
35
+ expect(rendered).to have_selector '.advanced-search-field', count: 4
36
+ expect(rendered).to have_field 'clause_0_field', with: 'all_fields', type: :hidden
37
+ expect(rendered).to have_field 'clause_1_field', with: 'title', type: :hidden
38
+ expect(rendered).to have_field 'clause_2_field', with: 'author', type: :hidden
39
+ expect(rendered).to have_field 'clause_3_field', with: 'subject', type: :hidden
40
+ end
41
+
42
+ it 'has filters' do
43
+ expect(rendered).to have_selector '.blacklight-format'
44
+ expect(rendered).to have_field 'f_inclusive[format][]', with: 'Book'
45
+ expect(rendered).to have_field 'f_inclusive[format][]', with: 'CD'
46
+ end
47
+
48
+ it 'has a sort field' do
49
+ expect(rendered).to have_select 'sort', options: %w[relevance year author title]
50
+ end
51
+ end
@@ -151,4 +151,19 @@ RSpec.describe Blacklight::DocumentComponent, type: :component do
151
151
  expect(rendered).to have_selector 'dd', text: 'Title'
152
152
  expect(rendered).not_to have_selector 'dt', text: 'ISBN:'
153
153
  end
154
+
155
+ context 'with a thumbnail component' do
156
+ let(:attr) { { thumbnail_component: thumbnail_component_class } }
157
+ let(:thumbnail_component_class) do
158
+ Class.new(ViewComponent::Base) do
159
+ def render_in(view_context)
160
+ view_context.capture { 'Thumb!' }
161
+ end
162
+ end
163
+ end
164
+
165
+ it 'uses the provided thumbnail component' do
166
+ expect(rendered).to have_content 'Thumb!'
167
+ end
168
+ end
154
169
  end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Blacklight::FacetFieldCheckboxesComponent, type: :component do
6
+ subject(:render) do
7
+ render_inline(described_class.new(facet_field: facet_field))
8
+ end
9
+
10
+ let(:rendered) do
11
+ Capybara::Node::Simple.new(render)
12
+ end
13
+
14
+ let(:facet_field) do
15
+ instance_double(
16
+ Blacklight::FacetFieldPresenter,
17
+ facet_field: Blacklight::Configuration::NullField.new(key: 'field'),
18
+ paginator: paginator,
19
+ key: 'field',
20
+ label: 'Field',
21
+ active?: false,
22
+ collapsed?: false,
23
+ modal_path: nil,
24
+ html_id: 'facet-field',
25
+ search_state: search_state
26
+ )
27
+ end
28
+
29
+ let(:paginator) do
30
+ instance_double(Blacklight::FacetPaginator, items: [
31
+ double(label: 'a', hits: 10, value: 'a'),
32
+ double(label: 'b', hits: 33, value: 'b'),
33
+ double(label: 'c', hits: 3, value: 'c')
34
+ ])
35
+ end
36
+
37
+ let(:search_state) { Blacklight::SearchState.new(params.with_indifferent_access, Blacklight::Configuration.new) }
38
+ let(:params) { { f: { field: ['a'] } } }
39
+
40
+ it 'renders a collapsible card' do
41
+ expect(rendered).to have_selector '.card'
42
+ expect(rendered).to have_button 'Field'
43
+ expect(rendered).to have_selector 'button[data-target="#facet-field"]'
44
+ expect(rendered).to have_selector '#facet-field.collapse.show'
45
+ end
46
+
47
+ it 'renders the facet items' do
48
+ expect(rendered).to have_selector 'ul.facet-values'
49
+ expect(rendered).to have_selector 'li', count: 3
50
+
51
+ expect(rendered).to have_field 'f_inclusive[field][]', with: 'a'
52
+ expect(rendered).to have_field 'f_inclusive[field][]', with: 'b'
53
+ expect(rendered).to have_field 'f_inclusive[field][]', with: 'c'
54
+ end
55
+ end
@@ -20,7 +20,8 @@ RSpec.describe Blacklight::FacetFieldListComponent, type: :component do
20
20
  active?: false,
21
21
  collapsed?: false,
22
22
  modal_path: nil,
23
- html_id: 'facet-field'
23
+ html_id: 'facet-field',
24
+ values: []
24
25
  )
25
26
  end
26
27
 
@@ -53,7 +54,8 @@ RSpec.describe Blacklight::FacetFieldListComponent, type: :component do
53
54
  active?: true,
54
55
  collapsed?: false,
55
56
  modal_path: nil,
56
- html_id: 'facet-field'
57
+ html_id: 'facet-field',
58
+ values: []
57
59
  )
58
60
  end
59
61
 
@@ -72,7 +74,8 @@ RSpec.describe Blacklight::FacetFieldListComponent, type: :component do
72
74
  active?: false,
73
75
  collapsed?: true,
74
76
  modal_path: nil,
75
- html_id: 'facet-field'
77
+ html_id: 'facet-field',
78
+ values: []
76
79
  )
77
80
  end
78
81
 
@@ -97,7 +100,8 @@ RSpec.describe Blacklight::FacetFieldListComponent, type: :component do
97
100
  active?: false,
98
101
  collapsed?: false,
99
102
  modal_path: '/catalog/facet/modal',
100
- html_id: 'facet-field'
103
+ html_id: 'facet-field',
104
+ values: []
101
105
  )
102
106
  end
103
107
 
@@ -105,4 +109,35 @@ RSpec.describe Blacklight::FacetFieldListComponent, type: :component do
105
109
  expect(rendered).to have_link 'more Field', href: '/catalog/facet/modal'
106
110
  end
107
111
  end
112
+
113
+ context 'with inclusive facets' do
114
+ let(:facet_field) do
115
+ instance_double(
116
+ Blacklight::FacetFieldPresenter,
117
+ paginator: paginator,
118
+ facet_field: Blacklight::Configuration::NullField.new(key: 'field'),
119
+ key: 'field',
120
+ label: 'Field',
121
+ active?: false,
122
+ collapsed?: false,
123
+ modal_path: nil,
124
+ html_id: 'facet-field',
125
+ values: [%w[a b c]],
126
+ search_state: search_state
127
+ )
128
+ end
129
+
130
+ let(:search_state) { Blacklight::SearchState.new(params.with_indifferent_access, Blacklight::Configuration.new) }
131
+ let(:params) { { f_inclusive: { field: %w[a b c] } } }
132
+
133
+ it 'displays the constraint above the list' do
134
+ expect(rendered).to have_content 'Any of:'
135
+ expect(rendered).to have_selector '.inclusive_or .facet-label', text: 'a'
136
+ expect(rendered).to have_link '[remove]', href: 'http://test.host/catalog?f_inclusive%5Bfield%5D%5B%5D=b&f_inclusive%5Bfield%5D%5B%5D=c'
137
+ expect(rendered).to have_selector '.inclusive_or .facet-label', text: 'b'
138
+ expect(rendered).to have_link '[remove]', href: 'http://test.host/catalog?f_inclusive%5Bfield%5D%5B%5D=a&f_inclusive%5Bfield%5D%5B%5D=c'
139
+ expect(rendered).to have_selector '.inclusive_or .facet-label', text: 'c'
140
+ expect(rendered).to have_link '[remove]', href: 'http://test.host/catalog?f_inclusive%5Bfield%5D%5B%5D=a&f_inclusive%5Bfield%5D%5B%5D=b'
141
+ end
142
+ end
108
143
  end
@@ -301,6 +301,15 @@ RSpec.describe CatalogController, api: true do
301
301
  end
302
302
  end
303
303
 
304
+ describe 'GET advanced_search' do
305
+ it 'renders an advanced search form' do
306
+ get :advanced_search
307
+ expect(response).to be_successful
308
+
309
+ assert_facets_have_values(assigns(:response).aggregations)
310
+ end
311
+ end
312
+
304
313
  # SHOW ACTION
305
314
  describe "show action" do
306
315
  describe "with format :html" do
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe "Blacklight Advanced Search Form" do
6
+ describe "advanced search form" do
7
+ before do
8
+ visit '/catalog/advanced?hypothetical_existing_param=true&q=ignore+this+existing+query'
9
+ end
10
+
11
+ it "has field and facet blocks" do
12
+ expect(page).to have_selector('.query-criteria')
13
+ expect(page).to have_selector('.limit-criteria')
14
+ end
15
+
16
+ describe "query column" do
17
+ it "gives the user a choice between and/or queries" do
18
+ expect(page).to have_selector('#op')
19
+ within('#op') do
20
+ expect(page).to have_selector('option[value="must"]')
21
+ expect(page).to have_selector('option[value="should"]')
22
+ end
23
+ end
24
+
25
+ it "lists the configured search fields" do
26
+ expect(page).to have_field 'All Fields'
27
+ expect(page).to have_field 'Title'
28
+ expect(page).to have_field 'Author'
29
+ expect(page).to have_field 'Subject'
30
+ end
31
+ end
32
+
33
+ describe "facet column" do
34
+ it "lists facets" do
35
+ expect(page).to have_selector('.blacklight-language_ssim')
36
+
37
+ within('.blacklight-language_ssim') do
38
+ expect(page).to have_content 'Language'
39
+ end
40
+ end
41
+ end
42
+
43
+ it 'scopes searches to fields' do
44
+ fill_in 'Title', with: 'Medicine'
45
+ click_on 'advanced-search-submit'
46
+ expect(page).to have_content 'Remove constraint Title: Medicine'
47
+ expect(page).to have_content 'Strong Medicine speaks'
48
+ end
49
+ end
50
+
51
+ describe "prepopulated advanced search form" do
52
+ before do
53
+ visit '/catalog/advanced?op=must&clause[0][field]=title&clause[0]query=medicine'
54
+ end
55
+
56
+ it "does not create hidden inputs for search fields" do
57
+ expect(page).to have_field 'Title', with: 'medicine'
58
+ end
59
+
60
+ it "does not have multiple parameters for a search field" do
61
+ fill_in 'Title', with: 'bread'
62
+ click_on 'advanced-search-submit'
63
+ expect(page.current_url).to match(/bread/)
64
+ expect(page.current_url).not_to match(/medicine/)
65
+ end
66
+ end
67
+ end
@@ -72,6 +72,33 @@ RSpec.describe Blacklight::SearchState::FilterField do
72
72
  expect(new_state.filter('some_field').values).to eq %w[1 2 4]
73
73
  end
74
74
  end
75
+
76
+ context 'with an array' do
77
+ let(:params) do
78
+ { f: { another_field: ['3'] }, f_inclusive: { some_field: %w[a b c] } }
79
+ end
80
+
81
+ it 'creates a new group with the new values' do
82
+ filter = search_state.filter('new_field')
83
+ new_state = filter.add(%w[x y z])
84
+
85
+ expect(new_state.filter('new_field').values).to eq [%w[x y z]]
86
+ end
87
+
88
+ it 'updates any existing groups with the new values' do
89
+ filter = search_state.filter('some_field')
90
+ new_state = filter.add(%w[x y z])
91
+
92
+ expect(new_state.filter('some_field').values).to eq [%w[x y z]]
93
+ end
94
+
95
+ it 'leaves existing filters alone' do
96
+ filter = search_state.filter('another_field')
97
+ new_state = filter.add(%w[x y z])
98
+
99
+ expect(new_state.filter('another_field').values).to eq ['3', %w[x y z]]
100
+ end
101
+ end
75
102
  end
76
103
 
77
104
  describe '#remove' do
@@ -104,12 +131,50 @@ RSpec.describe Blacklight::SearchState::FilterField do
104
131
 
105
132
  expect(new_state.filter('some_field').values).to eq ['2']
106
133
  end
134
+
135
+ context 'with an array' do
136
+ let(:params) do
137
+ { f: { another_field: ['3'] }, f_inclusive: { some_field: %w[a b c], another_field: %w[x y z] } }
138
+ end
139
+
140
+ it 'removes groups of values' do
141
+ filter = search_state.filter('some_field')
142
+ new_state = filter.remove(%w[a b c])
143
+
144
+ expect(new_state.params[:f_inclusive]).not_to include :some_field
145
+ expect(new_state.filter('some_field').values).to eq []
146
+ end
147
+
148
+ it 'can remove single values' do
149
+ filter = search_state.filter('some_field')
150
+ new_state = filter.remove(%w[a])
151
+
152
+ expect(new_state.filter('some_field').values).to eq [%w[b c]]
153
+ end
154
+
155
+ it 'leaves existing filters alone' do
156
+ filter = search_state.filter('another_field')
157
+ new_state = filter.remove(%w[x y z])
158
+
159
+ expect(new_state.filter('another_field').values).to eq ['3']
160
+ end
161
+ end
107
162
  end
108
163
 
109
164
  describe '#values' do
110
165
  it 'returns the currently selected values of the filter' do
111
166
  expect(search_state.filter('some_field').values).to eq %w[1 2]
112
167
  end
168
+
169
+ context 'with an array' do
170
+ let(:params) do
171
+ { f: { some_field: ['3'] }, f_inclusive: { some_field: %w[a b c] } }
172
+ end
173
+
174
+ it 'combines the exclusive and inclusive values' do
175
+ expect(search_state.filter('some_field').values).to eq ['3', %w[a b c]]
176
+ end
177
+ end
113
178
  end
114
179
 
115
180
  describe '#include?' do
@@ -134,6 +134,18 @@ RSpec.describe Blacklight::Solr::Repository, api: true do
134
134
  end
135
135
  end
136
136
  end
137
+
138
+ context 'with json parameters' do
139
+ it 'sends a post request with some json' do
140
+ allow(subject.connection).to receive(:send_and_receive) do |path, params|
141
+ expect(path).to eq 'select'
142
+ expect(params[:method]).to eq :post
143
+ expect(JSON.parse(params[:data]).with_indifferent_access).to include(query: { bool: {} })
144
+ expect(params[:headers]).to include({ 'Content-Type' => 'application/json' })
145
+ end.and_return('response' => { 'docs' => [] })
146
+ subject.search(json: { query: { bool: {} } })
147
+ end
148
+ end
137
149
  end
138
150
 
139
151
  describe "http_method configuration", integration: true do
@@ -259,6 +259,17 @@ RSpec.describe Blacklight::Solr::SearchBuilderBehavior, api: true do
259
259
  end
260
260
  end
261
261
 
262
+ describe 'with multi-valued facets' do
263
+ let(:user_params) { { f_inclusive: { format: %w[Book Movie CD] } } }
264
+
265
+ it "has proper solr parameters" do
266
+ expect(subject[:fq]).to include('{!lucene}{!query v=$f_inclusive.format.0} OR {!query v=$f_inclusive.format.1} OR {!query v=$f_inclusive.format.2}')
267
+ expect(subject['f_inclusive.format.0']).to eq '{!term f=format}Book'
268
+ expect(subject['f_inclusive.format.1']).to eq '{!term f=format}Movie'
269
+ expect(subject['f_inclusive.format.2']).to eq '{!term f=format}CD'
270
+ end
271
+ end
272
+
262
273
  describe "solr parameters for a field search from config (subject)" do
263
274
  let(:user_params) { subject_search_params }
264
275
 
@@ -723,4 +734,21 @@ RSpec.describe Blacklight::Solr::SearchBuilderBehavior, api: true do
723
734
  expect(subject.with_ex_local_param(nil, "some-value")).to eq "some-value"
724
735
  end
725
736
  end
737
+
738
+ context 'with advanced search clause parameters' do
739
+ before do
740
+ blacklight_config.search_fields.each_value do |v|
741
+ v.clause_params = { edismax: v.solr_parameters.dup }
742
+ end
743
+ end
744
+
745
+ let(:user_params) { { op: 'must', clause: { '0': { field: 'title', query: 'the book' }, '1': { field: 'author', query: 'the person' } } } }
746
+
747
+ it "has proper solr parameters" do
748
+ expect(subject.to_hash.with_indifferent_access.dig(:json, :query, :bool, :must, 0, :edismax, :query)).to eq 'the book'
749
+ expect(subject.to_hash.with_indifferent_access.dig(:json, :query, :bool, :must, 0, :edismax, :qf)).to eq '${title_qf}'
750
+ expect(subject.to_hash.with_indifferent_access.dig(:json, :query, :bool, :must, 1, :edismax, :query)).to eq 'the person'
751
+ expect(subject.to_hash.with_indifferent_access.dig(:json, :query, :bool, :must, 1, :edismax, :qf)).to eq '${author_qf}'
752
+ end
753
+ end
726
754
  end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+ require 'spec_helper'
3
+
4
+ RSpec.describe Blacklight::ClausePresenter, type: :presenter do
5
+ subject(:presenter) do
6
+ described_class.new('0', params.with_indifferent_access.dig(:clause, '0'), field_config, controller.view_context, search_state)
7
+ end
8
+
9
+ let(:field_config) { Blacklight::Configuration::NullField.new key: 'some_field' }
10
+ let(:search_state) { Blacklight::SearchState.new(params.with_indifferent_access, Blacklight::Configuration.new) }
11
+ let(:params) { {} }
12
+
13
+ describe '#field_label' do
14
+ it 'returns a label for the field' do
15
+ expect(subject.field_label).to eq 'Some Field'
16
+ end
17
+ end
18
+
19
+ describe '#label' do
20
+ let(:params) { { clause: { '0' => { query: 'some search string' } } } }
21
+
22
+ it 'returns the query value for the clause' do
23
+ expect(subject.label).to eq 'some search string'
24
+ end
25
+ end
26
+
27
+ describe '#remove_href' do
28
+ let(:params) { { clause: { '0' => { query: 'some_search_string' } } } }
29
+
30
+ it 'returns the href to remove the search clause' do
31
+ expect(subject.remove_href).not_to include 'some_search_string'
32
+ end
33
+ end
34
+ end