blacklight 7.14.1 → 7.15.0

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