blacklight 7.7.0 → 7.8.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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/.docker/app/Dockerfile +26 -0
  3. data/.docker/app/entrypoint.sh +6 -0
  4. data/.env +5 -0
  5. data/.rubocop_todo.yml +13 -13
  6. data/.travis.yml +15 -23
  7. data/Gemfile +4 -1
  8. data/README.md +4 -0
  9. data/VERSION +1 -1
  10. data/app/assets/stylesheets/blacklight/_pagination.scss +4 -0
  11. data/app/components/blacklight/constraint_layout_component.html.erb +21 -0
  12. data/app/components/blacklight/constraint_layout_component.rb +16 -0
  13. data/app/components/blacklight/facet_field_component.html.erb +25 -0
  14. data/app/components/blacklight/facet_field_component.rb +11 -0
  15. data/app/components/blacklight/facet_field_list_component.html.erb +18 -0
  16. data/app/components/blacklight/facet_field_list_component.rb +22 -0
  17. data/app/components/blacklight/facet_field_no_layout_component.rb +13 -0
  18. data/app/components/blacklight/facet_item_component.rb +120 -0
  19. data/app/helpers/blacklight/catalog_helper_behavior.rb +2 -2
  20. data/app/helpers/blacklight/facets_helper_behavior.rb +84 -48
  21. data/app/helpers/blacklight/render_constraints_helper_behavior.rb +64 -33
  22. data/app/javascript/blacklight/modal.js +1 -1
  23. data/app/models/concerns/blacklight/document/extensions.rb +3 -0
  24. data/app/models/concerns/blacklight/document/semantic_fields.rb +0 -4
  25. data/app/presenters/blacklight/facet_field_presenter.rb +57 -0
  26. data/app/presenters/blacklight/facet_item_presenter.rb +81 -0
  27. data/app/views/catalog/_citation.html.erb +1 -1
  28. data/app/views/catalog/_constraints.html.erb +1 -1
  29. data/app/views/catalog/_constraints_element.html.erb +5 -24
  30. data/app/views/catalog/_email_form.html.erb +1 -1
  31. data/app/views/catalog/_facet_layout.html.erb +8 -17
  32. data/app/views/catalog/_facet_limit.html.erb +3 -12
  33. data/app/views/catalog/_facet_pagination.html.erb +2 -2
  34. data/app/views/catalog/_facet_pivot.html.erb +4 -4
  35. data/app/views/catalog/_sms_form.html.erb +1 -1
  36. data/blacklight.gemspec +1 -0
  37. data/config/locales/blacklight.ar.yml +29 -25
  38. data/docker-compose.yml +35 -0
  39. data/lib/blacklight/engine.rb +2 -6
  40. data/lib/blacklight/search_state.rb +32 -0
  41. data/lib/blacklight/solr/response/facets.rb +2 -0
  42. data/lib/generators/blacklight/assets_generator.rb +10 -0
  43. data/spec/{views/catalog/_constraints_element.html.erb_spec.rb → components/blacklight/constraint_layout_component_spec.rb} +21 -11
  44. data/spec/components/blacklight/facet_field_list_component_spec.rb +108 -0
  45. data/spec/components/blacklight/facet_item_component_spec.rb +50 -0
  46. data/spec/features/facets_spec.rb +1 -1
  47. data/spec/helpers/blacklight/facets_helper_behavior_spec.rb +24 -12
  48. data/spec/helpers/blacklight/render_constraints_helper_behavior_spec.rb +4 -23
  49. data/spec/lib/blacklight/search_state_spec.rb +38 -0
  50. data/spec/models/blacklight/solr/response/facets_spec.rb +30 -1
  51. data/spec/presenters/blacklight/facet_field_presenter_spec.rb +109 -0
  52. data/spec/presenters/blacklight/facet_item_presenter_spec.rb +92 -0
  53. data/spec/spec_helper.rb +2 -0
  54. data/spec/support/presenter_test_helpers.rb +11 -0
  55. data/spec/views/catalog/_facet_group.html.erb_spec.rb +1 -0
  56. data/tasks/blacklight.rake +30 -23
  57. metadata +43 -5
@@ -0,0 +1,35 @@
1
+ version: "3.7"
2
+
3
+ services:
4
+ app:
5
+ build:
6
+ context: .
7
+ dockerfile: .docker/app/Dockerfile
8
+ args:
9
+ - ALPINE_RUBY_VERSION
10
+ volumes:
11
+ - .:/app
12
+ depends_on:
13
+ - solr
14
+ ports:
15
+ - "3000:3000"
16
+ environment:
17
+ - SOLR_URL # Set via environment variable or use default defined in .env file
18
+ - RAILS_VERSION # Set via environment variable or use default defined in .env file
19
+
20
+ solr:
21
+ environment:
22
+ - SOLR_PORT # Set via environment variable or use default defined in .env file
23
+ - SOLR_VERSION # Set via environment variable or use default defined in .env file
24
+ image: "solr:${SOLR_VERSION}"
25
+ volumes:
26
+ - $PWD/lib/generators/blacklight/templates/solr/conf:/opt/solr/conf
27
+ ports:
28
+ - "${SOLR_PORT}:8983"
29
+ entrypoint:
30
+ - docker-entrypoint.sh
31
+ - solr-precreate
32
+ - blacklight-core
33
+ - /opt/solr/conf
34
+ - "-Xms256m"
35
+ - "-Xmx512m"
@@ -1,4 +1,6 @@
1
1
  # frozen_string_literal: true
2
+ require 'view_component/engine'
3
+
2
4
  module Blacklight
3
5
  class Engine < Rails::Engine
4
6
  engine_name "blacklight"
@@ -9,12 +11,6 @@ module Blacklight
9
11
  ActionView::Base.send :include, BlacklightHelper
10
12
  end
11
13
 
12
- config.autoload_paths += %W(
13
- #{config.root}/app/presenters
14
- #{config.root}/app/controllers/concerns
15
- #{config.root}/app/models/concerns
16
- )
17
-
18
14
  # This makes our rake tasks visible.
19
15
  rake_tasks do
20
16
  Dir.chdir(File.expand_path(File.join(File.dirname(__FILE__), '..'))) do
@@ -37,6 +37,22 @@ module Blacklight
37
37
  end
38
38
  alias to_h to_hash
39
39
 
40
+ # Tiny shim to make it easier to migrate raw params access to using this class
41
+ delegate :[], to: :params
42
+ deprecation_deprecate :[]
43
+
44
+ def has_constraints?
45
+ !(query_param.blank? && filter_params.blank?)
46
+ end
47
+
48
+ def query_param
49
+ params[:q]
50
+ end
51
+
52
+ def filter_params
53
+ params[:f] || {}
54
+ end
55
+
40
56
  def reset(params = nil)
41
57
  self.class.new(params || ActionController::Parameters.new, blacklight_config, controller)
42
58
  end
@@ -57,6 +73,12 @@ module Blacklight
57
73
  end
58
74
  end
59
75
 
76
+ def remove_query_params
77
+ p = reset_search_params
78
+ p.delete(:q)
79
+ p
80
+ end
81
+
60
82
  # adds the value and/or field to params[:f]
61
83
  # Does NOT remove request keys and otherwise ensure that the hash
62
84
  # is suitable for a redirect. See
@@ -130,6 +152,16 @@ module Blacklight
130
152
  p
131
153
  end
132
154
 
155
+ def has_facet?(config, value: nil)
156
+ facet = params&.dig(:f, config.key)
157
+
158
+ if value
159
+ (facet || []).include? value
160
+ else
161
+ facet.present?
162
+ end
163
+ end
164
+
133
165
  # Merge the source params with the params_to_merge hash
134
166
  # @param [Hash] params_to_merge to merge into above
135
167
  # @return [ActionController::Parameters] the current search parameters after being sanitized by Blacklight::Parameters.sanitize
@@ -210,6 +210,8 @@ module Blacklight::Solr::Response::Facets
210
210
  Blacklight::Solr::Response::Facets::FacetItem.new(value: key, hits: hits, label: facet_field.query[key][:label])
211
211
  end
212
212
 
213
+ items = items.sort_by(&:hits).reverse if facet_field.sort && facet_field.sort.to_sym == :count
214
+
213
215
  hash[field_name] = Blacklight::Solr::Response::Facets::FacetField.new field_name, items
214
216
  end
215
217
  end
@@ -22,6 +22,16 @@ module Blacklight
22
22
  end
23
23
  end
24
24
 
25
+ ##
26
+ # Remove the empty generated app/assets/images directory. Without doing this,
27
+ # the default Sprockets 4 manifest will raise an exception.
28
+ def appease_sprockets4
29
+ return if !defined?(Sprockets::VERSION) || Sprockets::VERSION < '4'
30
+
31
+ append_to_file 'app/assets/config/manifest.js', "\n//= link application.js"
32
+ empty_directory 'app/assets/images'
33
+ end
34
+
25
35
  def assets
26
36
  copy_file "blacklight.scss", "app/assets/stylesheets/blacklight.scss"
27
37
 
@@ -1,24 +1,34 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- RSpec.describe "catalog/_constraints_element.html.erb" do
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Blacklight::ConstraintLayoutComponent, type: :component do
6
+ subject(:render) do
7
+ render_inline(described_class.new(params))
8
+ end
9
+
10
+ let(:rendered) do
11
+ Capybara::Node::Simple.new(render)
12
+ end
13
+
4
14
  describe "for simple display" do
5
- before do
6
- render partial: "catalog/constraints_element", locals: { label: "my label", value: "my value" }
15
+ let(:params) do
16
+ { label: "my label", value: "my value" }
7
17
  end
8
18
 
9
19
  it "renders label and value" do
10
20
  expect(rendered).to have_selector("span.applied-filter.constraint") do |s|
11
21
  expect(s).to have_css("span.constraint-value")
12
22
  expect(s).not_to have_css("a.constraint-value")
13
- expect(s).to have_selector "span.filter-name", content: "my label"
14
- expect(s).to have_selector "span.filter-value", content: "my value"
23
+ expect(s).to have_selector "span.filter-name", text: "my label"
24
+ expect(s).to have_selector "span.filter-value", text: "my value"
15
25
  end
16
26
  end
17
27
  end
18
28
 
19
29
  describe "with remove link" do
20
- before do
21
- render partial: "catalog/constraints_element", locals: { label: "my label", value: "my value", options: { remove: "http://remove" } }
30
+ let(:params) do
31
+ { label: "my label", value: "my value", remove_path: "http://remove" }
22
32
  end
23
33
 
24
34
  it "includes remove link" do
@@ -34,8 +44,8 @@ RSpec.describe "catalog/_constraints_element.html.erb" do
34
44
  end
35
45
 
36
46
  describe "with custom classes" do
37
- before do
38
- render partial: "catalog/constraints_element", locals: { label: "my label", value: "my value", options: { classes: %w[class1 class2] } }
47
+ let(:params) do
48
+ { label: "my label", value: "my value", classes: %w[class1 class2] }
39
49
  end
40
50
 
41
51
  it "includes them" do
@@ -44,8 +54,8 @@ RSpec.describe "catalog/_constraints_element.html.erb" do
44
54
  end
45
55
 
46
56
  describe "with no escaping" do
47
- before do
48
- render(partial: "catalog/constraints_element", locals: { label: "<span class='custom_label'>my label</span>".html_safe, value: "<span class='custom_value'>my value</span>".html_safe })
57
+ let(:params) do
58
+ { label: "<span class='custom_label'>my label</span>".html_safe, value: "<span class='custom_value'>my value</span>".html_safe }
49
59
  end
50
60
 
51
61
  it "does not escape key and value" do
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Blacklight::FacetFieldListComponent, 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
+ paginator: paginator,
18
+ key: 'field',
19
+ label: 'Field',
20
+ active?: false,
21
+ collapsed?: false,
22
+ modal_path: nil,
23
+ html_id: 'facet-field'
24
+ )
25
+ end
26
+
27
+ let(:paginator) do
28
+ instance_double(Blacklight::FacetPaginator, items: [
29
+ double(label: 'x', hits: 10),
30
+ double(label: 'y', hits: 33)
31
+ ])
32
+ end
33
+
34
+ it 'renders a collapsible card' do
35
+ expect(rendered).to have_selector '.card'
36
+ expect(rendered).to have_button 'Field'
37
+ expect(rendered).to have_selector 'button[data-target="#facet-field"]'
38
+ expect(rendered).to have_selector '#facet-field.collapse.show'
39
+ end
40
+
41
+ it 'renders the facet items' do
42
+ expect(rendered).to have_selector 'ul.facet-values'
43
+ expect(rendered).to have_selector 'li', count: 2
44
+ end
45
+
46
+ context 'with an active facet' do
47
+ let(:facet_field) do
48
+ instance_double(
49
+ Blacklight::FacetFieldPresenter,
50
+ paginator: paginator,
51
+ key: 'field',
52
+ label: 'Field',
53
+ active?: true,
54
+ collapsed?: false,
55
+ modal_path: nil,
56
+ html_id: 'facet-field'
57
+ )
58
+ end
59
+
60
+ it 'adds the facet-limit-active class' do
61
+ expect(rendered).to have_selector 'div.facet-limit-active'
62
+ end
63
+ end
64
+
65
+ context 'with a collapsed facet' do
66
+ let(:facet_field) do
67
+ instance_double(
68
+ Blacklight::FacetFieldPresenter,
69
+ paginator: paginator,
70
+ key: 'field',
71
+ label: 'Field',
72
+ active?: false,
73
+ collapsed?: true,
74
+ modal_path: nil,
75
+ html_id: 'facet-field'
76
+ )
77
+ end
78
+
79
+ it 'renders a collapsed facet' do
80
+ expect(rendered).to have_selector '.facet-content.collapse'
81
+ expect(rendered).not_to have_selector '.facet-content.collapse.show'
82
+ end
83
+
84
+ it 'renders the toggle button in the collapsed state' do
85
+ expect(rendered).to have_selector '.btn.collapsed'
86
+ expect(rendered).to have_selector '.btn[aria-expanded="false"]'
87
+ end
88
+ end
89
+
90
+ context 'with a modal_path' do
91
+ let(:facet_field) do
92
+ instance_double(
93
+ Blacklight::FacetFieldPresenter,
94
+ paginator: paginator,
95
+ key: 'field',
96
+ label: 'Field',
97
+ active?: false,
98
+ collapsed?: false,
99
+ modal_path: '/catalog/facet/modal',
100
+ html_id: 'facet-field'
101
+ )
102
+ end
103
+
104
+ it 'renders a link to the modal' do
105
+ expect(rendered).to have_link 'more Field', href: '/catalog/facet/modal'
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Blacklight::FacetItemComponent, type: :component do
6
+ subject(:render) do
7
+ render_inline(described_class.new(facet_item: facet_item))
8
+ end
9
+
10
+ let(:rendered) do
11
+ Capybara::Node::Simple.new(render)
12
+ end
13
+
14
+ let(:facet_item) do
15
+ instance_double(
16
+ Blacklight::FacetItemPresenter,
17
+ facet_config: Blacklight::Configuration::FacetField.new,
18
+ label: 'x',
19
+ hits: 10,
20
+ href: '/catalog?f=x',
21
+ selected?: false
22
+ )
23
+ end
24
+
25
+ it 'links to the facet and shows the number of hits' do
26
+ expect(rendered).to have_selector 'li'
27
+ expect(rendered).to have_link 'x', href: '/catalog?f=x'
28
+ expect(rendered).to have_selector '.facet-count', text: '10'
29
+ end
30
+
31
+ context 'with a selected facet' do
32
+ let(:facet_item) do
33
+ instance_double(
34
+ Blacklight::FacetItemPresenter,
35
+ facet_config: Blacklight::Configuration::FacetField.new,
36
+ label: 'x',
37
+ hits: 10,
38
+ href: '/catalog',
39
+ selected?: true
40
+ )
41
+ end
42
+
43
+ it 'links to the facet and shows the number of hits' do
44
+ expect(rendered).to have_selector 'li'
45
+ expect(rendered).to have_selector '.selected', text: 'x'
46
+ expect(rendered).to have_link '[remove]', href: '/catalog'
47
+ expect(rendered).to have_selector '.selected.facet-count', text: '10'
48
+ end
49
+ end
50
+ end
@@ -88,7 +88,7 @@ RSpec.describe "Facets" do
88
88
  it 'has default more link with sr-only text' do
89
89
  visit root_path
90
90
  within '#facet-language_ssim' do
91
- expect(page).to have_css 'li.more_facets', text: 'more Language'
91
+ expect(page).to have_css 'div.more_facets', text: 'more Language'
92
92
  end
93
93
  end
94
94
  end
@@ -214,7 +214,11 @@ RSpec.describe Blacklight::FacetsHelperBehavior do
214
214
 
215
215
  context "when one of the facet items is rendered as nil" do
216
216
  # An app may override render_facet_item to filter out some undesired facet items by returning nil.
217
- before { allow(helper).to receive(:render_facet_item).and_return('<a class="facet-select">Book</a>'.html_safe, nil) }
217
+ before do
218
+ allow(helper.method(:render_facet_item)).to receive(:owner).and_return(self.class)
219
+ # allow_any_instance_of(Blacklight::FacetItemComponent).to receive(:overridden_helper_methods?).and_return(true)
220
+ allow(helper).to receive(:render_facet_item).and_return('<a class="facet-select">Book</a>'.html_safe, nil)
221
+ end
218
222
 
219
223
  it "draws a list of elements" do
220
224
  expect(subject).to have_selector 'li', count: 1
@@ -246,36 +250,44 @@ RSpec.describe Blacklight::FacetsHelperBehavior do
246
250
  end
247
251
 
248
252
  describe "facet_field_in_params?" do
253
+ let(:search_state) { double }
254
+
255
+ before do
256
+ allow(helper).to receive_messages(search_state: search_state)
257
+ end
258
+
249
259
  it "checks if any value is selected for a given facet" do
250
- allow(helper).to receive_messages(facet_params: ["x"])
260
+ allow(search_state).to receive(:has_facet?).with(having_attributes(key: 'some-facet')).and_return(true)
251
261
  expect(helper.facet_field_in_params?("some-facet")).to eq true
252
262
  end
253
263
 
254
264
  it "is false if no value for facet is selected" do
255
- allow(helper).to receive_messages(facet_params: nil)
265
+ allow(search_state).to receive(:has_facet?).with(having_attributes(key: 'some-facet')).and_return(false)
256
266
  expect(helper.facet_field_in_params?("some-facet")).to eq false
257
267
  end
258
268
  end
259
269
 
260
270
  describe "facet_in_params?" do
271
+ let(:search_state) { double }
272
+
273
+ before do
274
+ allow(helper).to receive_messages(search_state: search_state)
275
+ allow(search_state).to receive(:has_facet?).with(having_attributes(key: 'some-facet'), value: 'x').and_return(true)
276
+ allow(search_state).to receive(:has_facet?).with(having_attributes(key: 'some-facet'), value: 'y').and_return(false)
277
+ end
278
+
261
279
  it "checks if a particular value is set in the facet params" do
262
- allow(helper).to receive_messages(facet_params: ["x"])
263
280
  expect(helper.facet_in_params?("some-facet", "x")).to eq true
264
281
  expect(helper.facet_in_params?("some-facet", "y")).to eq false
265
282
  end
266
-
267
- it "is false if no value for facet is selected" do
268
- allow(helper).to receive_messages(facet_params: nil)
269
- expect(helper.facet_in_params?("some-facet", "x")).to eq false
270
- end
271
283
  end
272
284
 
273
285
  describe "render_facet_value" do
274
286
  let(:item) { double(value: 'A', hits: 10) }
275
- let(:search_state) { double(add_facet_params_and_redirect: { controller: 'catalog' }) }
287
+ let(:search_state) { double(has_facet?: false, add_facet_params_and_redirect: { controller: 'catalog' }) }
276
288
 
277
289
  before do
278
- allow(helper).to receive(:facet_configuration_for_field).with('simple_field').and_return(double(query: nil, date: nil, helper_method: nil, single: false, url_method: nil))
290
+ allow(helper).to receive(:facet_configuration_for_field).with('simple_field').and_return(Blacklight::Configuration::FacetField.new(key: 'simple_field', query: nil, date: nil, helper_method: nil, single: false, url_method: nil))
279
291
  allow(helper).to receive(:facet_display_value).and_return('Z')
280
292
  allow(helper).to receive(:search_state).and_return(search_state)
281
293
  allow(helper).to receive(:search_action_path) do |*args|
@@ -296,7 +308,7 @@ RSpec.describe Blacklight::FacetsHelperBehavior do
296
308
  let(:expected_html) { '<span class="facet-label"><a class="facet-select" href="/blabla">Z</a></span><span class="facet-count">10</span>' }
297
309
 
298
310
  it "uses that method" do
299
- allow(helper).to receive(:facet_configuration_for_field).with('simple_field').and_return(double(query: nil, date: nil, helper_method: nil, single: false, url_method: :test_method))
311
+ allow(helper).to receive(:facet_configuration_for_field).with('simple_field').and_return(Blacklight::Configuration::FacetField.new(key: 'simple_field', query: nil, date: nil, helper_method: nil, single: false, url_method: :test_method))
300
312
  allow(helper).to receive(:test_method).with('simple_field', item).and_return('/blabla')
301
313
  result = helper.render_facet_value('simple_field', item)
302
314
  expect(result).to be_equivalent_to(expected_html).respecting_element_order
@@ -13,6 +13,9 @@ RSpec.describe Blacklight::RenderConstraintsHelperBehavior do
13
13
  allow(helper).to receive(:search_action_path) do |*args|
14
14
  search_catalog_path *args
15
15
  end
16
+
17
+ allow(helper).to receive(:blacklight_config).and_return(config)
18
+ allow(controller).to receive(:search_state_class).and_return(Blacklight::SearchState)
16
19
  end
17
20
 
18
21
  describe '#render_constraints_query' do
@@ -22,24 +25,7 @@ RSpec.describe Blacklight::RenderConstraintsHelperBehavior do
22
25
  let(:params) { ActionController::Parameters.new(q: 'foobar', f: { type: 'journal' }) }
23
26
 
24
27
  it "has a link relative to the current url" do
25
- expect(subject).to have_selector "a[href='/?f%5Btype%5D=journal']"
26
- end
27
-
28
- context 'with an ordinary hash' do
29
- let(:params) { { q: 'foobar', f: { type: 'journal' } } }
30
-
31
- it "has a link relative to the current url" do
32
- expect(subject).to have_selector "a[href='/?f%5Btype%5D=journal']"
33
- end
34
- end
35
-
36
- context "with a route_set" do
37
- let(:params) { ActionController::Parameters.new(q: 'foobar', f: { type: 'journal' }, route_set: my_engine) }
38
-
39
- it "accepts an optional route set" do
40
- expect(my_engine).to receive(:url_for).and_return('/?f%5Btype%5D=journal')
41
- expect(subject).to have_selector "a[href='/?f%5Btype%5D=journal']"
42
- end
28
+ expect(subject).to have_link 'Remove constraint', href: '/catalog?f%5Btype%5D=journal'
43
29
  end
44
30
  end
45
31
 
@@ -73,11 +59,6 @@ RSpec.describe Blacklight::RenderConstraintsHelperBehavior do
73
59
 
74
60
  let(:params) { ActionController::Parameters.new f: { 'type' => [''] } }
75
61
 
76
- before do
77
- allow(helper).to receive(:blacklight_config).and_return(config)
78
- allow(controller).to receive(:search_state_class).and_return(Blacklight::SearchState)
79
- end
80
-
81
62
  it "renders nothing for empty facet limit param" do
82
63
  expect(subject).to be_blank
83
64
  end