blacklight 9.0.0.beta8 → 9.1.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 (185) hide show
  1. checksums.yaml +4 -4
  2. data/.env +3 -3
  3. data/.github/matrix.json +18 -34
  4. data/.github/workflows/lint.yml +1 -1
  5. data/.github/workflows/test.yml +4 -3
  6. data/.rubocop.yml +72 -7
  7. data/.rubocop_todo.yml +142 -268
  8. data/Gemfile +1 -1
  9. data/README.md +4 -11
  10. data/VERSION +1 -1
  11. data/app/assets/builds/blacklight.css +37 -23
  12. data/app/assets/images/blacklight/logo-dark.svg +184 -0
  13. data/app/assets/images/blacklight/logo.svg +184 -0
  14. data/app/assets/javascripts/blacklight/blacklight.esm.js +134 -48
  15. data/app/assets/javascripts/blacklight/blacklight.esm.js.map +1 -1
  16. data/app/assets/javascripts/blacklight/blacklight.js +134 -48
  17. data/app/assets/javascripts/blacklight/blacklight.js.map +1 -1
  18. data/app/assets/stylesheets/blacklight/_balanced_list.scss +1 -1
  19. data/app/assets/stylesheets/blacklight/_bootstrap_overrides.scss +17 -0
  20. data/app/assets/stylesheets/blacklight/_constraints.scss +2 -2
  21. data/app/assets/stylesheets/blacklight/_facets.scss +14 -10
  22. data/app/assets/stylesheets/blacklight/_header.scss +0 -4
  23. data/app/assets/stylesheets/blacklight/blacklight_defaults.scss +5 -9
  24. data/app/components/blacklight/facets/filters_component.rb +1 -1
  25. data/app/components/blacklight/facets/inclusive_constraint_component.html.erb +1 -1
  26. data/app/components/blacklight/facets/suggest_component.html.erb +17 -12
  27. data/app/components/blacklight/icons/checkmark_component.rb +16 -0
  28. data/app/components/blacklight/icons/circle_half_component.rb +16 -0
  29. data/app/components/blacklight/icons/moon_stars_component.rb +17 -0
  30. data/app/components/blacklight/icons/sun_component.rb +16 -0
  31. data/app/components/blacklight/metadata_field_component.rb +1 -1
  32. data/app/components/blacklight/search_navbar_component.html.erb +1 -1
  33. data/app/components/blacklight/theme_switcher_component.html.erb +31 -0
  34. data/app/components/blacklight/theme_switcher_component.rb +6 -0
  35. data/app/components/blacklight/top_navbar_component.html.erb +1 -1
  36. data/app/controllers/concerns/blacklight/bookmarks.rb +8 -14
  37. data/app/controllers/concerns/blacklight/catalog.rb +12 -14
  38. data/app/controllers/concerns/blacklight/controller.rb +54 -11
  39. data/app/controllers/concerns/blacklight/guest_user.rb +51 -0
  40. data/app/controllers/concerns/blacklight/search_context.rb +1 -1
  41. data/app/controllers/concerns/blacklight/search_history.rb +3 -1
  42. data/app/controllers/concerns/blacklight/searchable.rb +18 -0
  43. data/app/helpers/blacklight/layout_helper_behavior.rb +7 -0
  44. data/app/javascript/blacklight-frontend/checkbox_submit.js +2 -2
  45. data/app/javascript/blacklight-frontend/color_theme_switcher.js +85 -0
  46. data/app/javascript/blacklight-frontend/facet_suggest.js +4 -1
  47. data/app/javascript/blacklight-frontend/index.js +2 -0
  48. data/app/models/concerns/blacklight/document/dublin_core.rb +2 -2
  49. data/app/models/concerns/blacklight/document.rb +1 -1
  50. data/app/models/concerns/blacklight/user.rb +36 -0
  51. data/app/presenters/blacklight/field_presenter.rb +1 -1
  52. data/app/presenters/blacklight/rendering/microdata.rb +1 -0
  53. data/app/services/blacklight/bookmarks_search_builder.rb +4 -4
  54. data/app/services/blacklight/search_service.rb +50 -35
  55. data/app/values/blacklight/types.rb +1 -1
  56. data/app/views/catalog/_email_form.html.erb +3 -3
  57. data/app/views/catalog/_home_text.html.erb +1 -1
  58. data/app/views/catalog/_sms_form.html.erb +4 -4
  59. data/app/views/catalog/index.rss.builder +4 -4
  60. data/app/views/catalog/opensearch.json.jbuilder +3 -0
  61. data/app/views/catalog/opensearch.xml.builder +2 -2
  62. data/app/views/layouts/blacklight/base.html.erb +13 -1
  63. data/app/views/shared/_user_util_links.html.erb +18 -5
  64. data/blacklight.gemspec +3 -3
  65. data/config/locales/blacklight.ar.yml +5 -0
  66. data/config/locales/blacklight.ca.yml +5 -0
  67. data/config/locales/blacklight.de.yml +6 -2
  68. data/config/locales/blacklight.en.yml +5 -0
  69. data/config/locales/blacklight.es.yml +5 -0
  70. data/config/locales/blacklight.fr.yml +5 -0
  71. data/config/locales/blacklight.hu.yml +5 -0
  72. data/config/locales/blacklight.it.yml +6 -2
  73. data/config/locales/blacklight.nl.yml +5 -0
  74. data/config/locales/blacklight.pt-BR.yml +6 -2
  75. data/config/locales/blacklight.sq.yml +5 -0
  76. data/config/locales/blacklight.zh.yml +5 -0
  77. data/lib/blacklight/component.rb +1 -1
  78. data/lib/blacklight/configuration/action_config_map_entry.rb +31 -0
  79. data/lib/blacklight/configuration/display_field.rb +1 -0
  80. data/lib/blacklight/configuration/field.rb +6 -6
  81. data/lib/blacklight/configuration/fields.rb +1 -1
  82. data/lib/blacklight/configuration/sort_field.rb +2 -2
  83. data/lib/blacklight/configuration.rb +30 -23
  84. data/lib/blacklight/parameters.rb +1 -1
  85. data/lib/blacklight/routes/searchable.rb +1 -1
  86. data/lib/blacklight/search_builder.rb +208 -10
  87. data/lib/blacklight/search_state.rb +4 -0
  88. data/lib/blacklight/solr/request.rb +8 -1
  89. data/lib/blacklight/solr/response/group_response.rb +1 -1
  90. data/lib/blacklight/solr/response/spelling.rb +8 -8
  91. data/lib/blacklight/solr/response.rb +2 -21
  92. data/lib/blacklight/solr/search_builder_behavior.rb +59 -16
  93. data/lib/blacklight.rb +2 -2
  94. data/lib/generators/blacklight/assets/importmap_generator.rb +2 -2
  95. data/lib/generators/blacklight/assets_generator.rb +1 -1
  96. data/lib/generators/blacklight/controller_generator.rb +1 -1
  97. data/lib/generators/blacklight/document_generator.rb +1 -1
  98. data/lib/generators/blacklight/install_generator.rb +6 -4
  99. data/lib/generators/blacklight/models_generator.rb +1 -1
  100. data/lib/generators/blacklight/search_builder_generator.rb +1 -1
  101. data/lib/generators/blacklight/solr_generator.rb +1 -1
  102. data/lib/generators/blacklight/templates/catalog_controller.rb +21 -2
  103. data/lib/generators/blacklight/templates/solr/conf/schema.xml +264 -178
  104. data/lib/generators/blacklight/templates/solr/conf/solrconfig.xml +6 -14
  105. data/lib/generators/blacklight/test_support_generator.rb +1 -1
  106. data/lib/generators/blacklight/user_generator.rb +66 -5
  107. data/lib/railties/blacklight.rake +4 -4
  108. data/package.json +2 -2
  109. data/spec/components/blacklight/document/action_component_spec.rb +1 -1
  110. data/spec/components/blacklight/document/group_component_spec.rb +1 -1
  111. data/spec/components/blacklight/document_component_spec.rb +9 -9
  112. data/spec/components/blacklight/facet_component_spec.rb +2 -2
  113. data/spec/components/blacklight/facets/filters_component_spec.rb +1 -1
  114. data/spec/components/blacklight/facets/index_navigation_component_spec.rb +1 -2
  115. data/spec/components/blacklight/facets/list_component_spec.rb +1 -1
  116. data/spec/components/blacklight/facets/suggest_component_spec.rb +2 -3
  117. data/spec/components/blacklight/metadata_field_component_spec.rb +26 -0
  118. data/spec/components/blacklight/skip_link_component_spec.rb +2 -2
  119. data/spec/controllers/application_controller_spec.rb +71 -0
  120. data/spec/controllers/bookmarks_controller_spec.rb +18 -5
  121. data/spec/controllers/catalog_controller_spec.rb +7 -5
  122. data/spec/features/advanced_search_spec.rb +29 -16
  123. data/spec/features/alternate_controller_spec.rb +4 -4
  124. data/spec/features/axe_spec.rb +34 -22
  125. data/spec/features/bookmarks_spec.rb +13 -13
  126. data/spec/features/dark_mode_spec.rb +42 -0
  127. data/spec/features/did_you_mean_spec.rb +27 -27
  128. data/spec/features/facet_missing_spec.rb +3 -3
  129. data/spec/features/facets_spec.rb +9 -0
  130. data/spec/features/record_view_spec.rb +14 -14
  131. data/spec/features/search_context_spec.rb +8 -8
  132. data/spec/features/search_filters_spec.rb +16 -16
  133. data/spec/features/search_formats_spec.rb +2 -2
  134. data/spec/features/search_history_spec.rb +11 -11
  135. data/spec/features/search_pagination_spec.rb +17 -17
  136. data/spec/features/search_results_spec.rb +3 -3
  137. data/spec/features/search_sort_spec.rb +4 -4
  138. data/spec/features/search_spec.rb +20 -20
  139. data/spec/helpers/blacklight/url_helper_behavior_spec.rb +15 -15
  140. data/spec/helpers/blacklight_helper_spec.rb +1 -1
  141. data/spec/helpers/catalog_helper_spec.rb +7 -7
  142. data/spec/integration/generators/blacklight/user_generator_spec.rb +60 -0
  143. data/spec/lib/blacklight/search_state/pivot_filter_field_spec.rb +1 -1
  144. data/spec/models/blacklight/configurable_spec.rb +1 -1
  145. data/spec/models/blacklight/configuration_spec.rb +0 -19
  146. data/spec/models/blacklight/document/dublin_core_spec.rb +2 -2
  147. data/spec/models/blacklight/facet_paginator_spec.rb +1 -1
  148. data/spec/models/blacklight/search_builder_spec.rb +26 -4
  149. data/spec/models/blacklight/solr/repository_spec.rb +10 -2
  150. data/spec/models/blacklight/solr/search_builder_behavior_spec.rb +165 -7
  151. data/spec/models/blacklight/user_spec.rb +32 -5
  152. data/spec/models/bookmark_spec.rb +1 -1
  153. data/spec/models/record_mailer_spec.rb +8 -8
  154. data/spec/models/search_spec.rb +1 -1
  155. data/spec/models/solr_document_spec.rb +4 -0
  156. data/spec/presenters/blacklight/index_presenter_spec.rb +1 -0
  157. data/spec/presenters/blacklight/rendering/pipeline_spec.rb +1 -0
  158. data/spec/presenters/blacklight/show_presenter_spec.rb +1 -0
  159. data/spec/presenters/blacklight/thumbnail_presenter_spec.rb +1 -0
  160. data/spec/services/blacklight/search_service_spec.rb +7 -7
  161. data/spec/spec_helper.rb +3 -12
  162. data/spec/support/features/session_helpers.rb +13 -4
  163. data/spec/support/features/turbo_helpers.rb +13 -0
  164. data/spec/support/features.rb +2 -0
  165. data/spec/support/user_helpers.rb +24 -0
  166. data/spec/test_app_templates/lib/generators/test_app_generator.rb +1 -1
  167. data/spec/views/catalog/_document.html.erb_spec.rb +4 -4
  168. data/spec/views/catalog/_paginate_compact.html.erb_spec.rb +2 -2
  169. data/spec/views/catalog/_sort_and_per_page.html.erb_spec.rb +5 -5
  170. data/spec/views/catalog/index.html.erb_spec.rb +2 -2
  171. data/spec/views/catalog/show.html.erb_spec.rb +3 -4
  172. data/spec/views/shared/_user_util_links.html.erb_spec.rb +56 -0
  173. data/template.demo.rb +2 -1
  174. metadata +30 -29
  175. data/.hound.yml +0 -4
  176. data/.jshintrc +0 -22
  177. data/app/assets/images/blacklight/logo.png +0 -0
  178. data/app/controllers/concerns/blacklight/facetable.rb +0 -34
  179. data/app/models/facet_search_builder.rb +0 -5
  180. data/app/services/blacklight/facet_search_service.rb +0 -44
  181. data/lib/blacklight/abstract_search_builder.rb +0 -154
  182. data/lib/blacklight/facet_search_builder.rb +0 -18
  183. data/lib/blacklight/solr/facet_search_builder_behavior.rb +0 -62
  184. data/spec/models/blacklight/facet_search_builder_spec.rb +0 -19
  185. data/spec/models/blacklight/solr/facet_search_builder_behavior_spec.rb +0 -929
@@ -47,7 +47,12 @@ module Blacklight
47
47
  property :header_component, default: Blacklight::HeaderComponent
48
48
  property :full_width_layout, default: false
49
49
 
50
- # bootstrap_version may be set to 4 or 5
50
+ # @!attribute dark_mode_support
51
+ # @since v9.1.0
52
+ # @return [Boolean] Whether to enable the light/dark mode them switcher.
53
+ # @note Requires Bootstrap v5.3+; see: https://getbootstrap.com/docs/5.3/customize/color-modes/#dark-mode
54
+ property :dark_mode_support, default: true
55
+
51
56
  bootstrap_version = ENV['BOOTSTRAP_VERSION'].presence || "5"
52
57
  property :bootstrap_version, default: /(\d)(?:\.\d){0,2}/.match(bootstrap_version)[1].to_i
53
58
 
@@ -104,9 +109,6 @@ module Blacklight
104
109
  # @!attribute search_builder_class
105
110
  # @return [Class] class for converting Blacklight parameters to request parameters for the repository_class
106
111
  property :search_builder_class, default: ::SearchBuilder
107
- # @!attribute search_builder_class
108
- # @return [Class] class for converting Blacklight parameters to request parameters for the repository_class
109
- property :facet_search_builder_class, default: ::FacetSearchBuilder
110
112
  # @!attribute response_model
111
113
  # model that maps index responses to the blacklight response model
112
114
  # @return [Class]
@@ -212,14 +214,14 @@ module Blacklight
212
214
 
213
215
  # @!attribute action_mapping
214
216
  # @since v7.16.0
215
- # @return [Hash{Symbol => Blacklight::Configuration::ViewConfig}]
217
+ # @return [Hash{Symbol => Blacklight::Configuration::ActionConfigMapEntry}]
216
218
  property :action_mapping, default: NestedOpenStructWithHashAccess.new(
217
- ViewConfig,
218
- default: { top_level_config: :index },
219
- show: { top_level_config: :show },
220
- citation: { parent_config: :show },
221
- email_record: { top_level_config: :email },
222
- sms_record: { top_level_config: :sms }
219
+ ActionConfigMapEntry,
220
+ default: { blacklight_config_property: :index, default: [:index] },
221
+ show: { blacklight_config_property: :show },
222
+ citation: { parent_action_key: :show },
223
+ email_record: { blacklight_config_property: :email },
224
+ sms_record: { blacklight_config_property: :sms }
223
225
  )
224
226
 
225
227
  # @!attribute sms
@@ -552,7 +554,7 @@ module Blacklight
552
554
  conf.klass = klass
553
555
  end
554
556
  end
555
- alias_method :inheritable_copy, :build
557
+ alias inheritable_copy build
556
558
 
557
559
  # Get a view configuration for the given view type + action. The effective
558
560
  # view configuration is inherited from:
@@ -574,12 +576,7 @@ module Blacklight
574
576
  view_type = nil
575
577
  end
576
578
 
577
- @view_config[[view_type, action_name]] ||= if view_type.nil?
578
- action_config(action_name)
579
- else
580
- base_config = action_config(action_name)
581
- base_config.merge(view.fetch(view_type, {}))
582
- end
579
+ @view_config[[view_type, action_name]] ||= action_config(action_name, (view.fetch(view_type, nil) if view_type))
583
580
  end
584
581
 
585
582
  # YARD will include inline disabling as docs, cannot do multiline inside @!macro. AND this must be separate from doc block.
@@ -692,8 +689,8 @@ module Blacklight
692
689
  end
693
690
  end
694
691
 
695
- def action_config(action, default: :index)
696
- action_config = action_mapping[action]
692
+ def action_config(action_name, view_type_specific_config, default: :index)
693
+ action_config = action_mapping[action_name]
697
694
  action_config ||= action_mapping[:default]
698
695
 
699
696
  if action_config.parent_config && action_config.parent_config != :default
@@ -704,9 +701,19 @@ module Blacklight
704
701
  end
705
702
  action_config = action_config.reverse_merge(action_mapping[:default]) if action_config != action_mapping[:default]
706
703
 
707
- action_config = action_config.reverse_merge(self[action_config.top_level_config]) if action_config.top_level_config
708
- action_config = action_config.reverse_merge(show) if default == :show && action_config.top_level_config != :show
709
- action_config.reverse_merge(index)
704
+ view_config = if action_config.blacklight_config_property
705
+ self[action_config.blacklight_config_property]
706
+ else
707
+ self[default]
708
+ end
709
+
710
+ view_config = Array(action_config.default - [action_config.blacklight_config_property || default]).inject(view_config) do |config, top_level_config|
711
+ config.reverse_merge(self[top_level_config])
712
+ end
713
+
714
+ view_config = view_config.merge(view_type_specific_config) if view_type_specific_config
715
+
716
+ view_config
710
717
  end
711
718
  end
712
719
  end
@@ -79,7 +79,7 @@ module Blacklight
79
79
  # Facebook's crawler turns array query parameters into a hash with numeric keys. Once we know
80
80
  # the expected parameter structure, we can unmangle those parameters to match our expected values.
81
81
  def deep_unmangle_params!(params, permitted_params)
82
- permitted_params.select { |p| p.is_a?(Hash) }.each do |permission|
82
+ permitted_params.grep(Hash).each do |permission|
83
83
  permission.each do |key, permitted_value|
84
84
  next unless params[key].is_a?(ActionController::Parameters)
85
85
 
@@ -18,7 +18,7 @@ module Blacklight
18
18
  mapper.get "opensearch"
19
19
  mapper.get 'suggest', as: 'suggest_index'
20
20
  mapper.get "facet/:id", action: 'facet', as: 'facet'
21
- mapper.get "facet_suggest/:id/(:query_fragment)", action: 'facet', as: 'facet_suggest', defaults: { only_values: true }
21
+ mapper.get "facet_suggest/:id", action: 'facet', as: 'facet_suggest', defaults: { only_values: true }
22
22
  end
23
23
  end
24
24
  end
@@ -5,7 +5,54 @@ module Blacklight
5
5
  # Blacklight's SearchBuilder converts blacklight request parameters into
6
6
  # query parameters appropriate for search index. It does so by evaluating a
7
7
  # chain of processing methods to populate a result hash (see {#to_hash}).
8
- class SearchBuilder < AbstractSearchBuilder
8
+ class SearchBuilder
9
+ class_attribute :default_processor_chain
10
+ self.default_processor_chain = []
11
+
12
+ attr_reader :processor_chain, :blacklight_config
13
+
14
+ # @overload initialize(scope)
15
+ # @param [Object] scope the scope where the filter methods reside in.
16
+ # @overload initialize(processor_chain, scope)
17
+ # @param [List<Symbol>,TrueClass] processor_chain options a list of filter methods to run or true, to use the default methods
18
+ # @param [Object] scope the scope where the filter methods reside in.
19
+ def initialize(*options, blacklight_config: nil)
20
+ case options.size
21
+ when 1
22
+ @processor_chain = default_processor_chain.dup
23
+ @scope = options.first
24
+ when 2
25
+ @processor_chain, @scope = options
26
+ else
27
+ raise ArgumentError, "wrong number of arguments. (#{options.size} for 1..2)"
28
+ end
29
+
30
+ @blacklight_config = blacklight_config || @scope&.blacklight_config
31
+ @additional_filters = {}
32
+ @merged_params = {}
33
+ @reverse_merged_params = {}
34
+ end
35
+
36
+ def search_state
37
+ @search_state ||= begin
38
+ search_state_class = @scope.try(:search_state_class) || Blacklight::SearchState
39
+ @search_state = search_state_class.new({}, blacklight_config, @scope)
40
+ end
41
+ end
42
+
43
+ def blacklight_params
44
+ search_state.params
45
+ end
46
+ Blacklight.deprecation.deprecate_methods(self, blacklight_params: 'Use search_state.params instead of blacklight_params directly')
47
+
48
+ ##
49
+ # Set the parameters to pass through the processor chain
50
+ def with(blacklight_params_or_search_state = {})
51
+ params_will_change!
52
+ @search_state = blacklight_params_or_search_state.is_a?(Blacklight::SearchState) ? blacklight_params_or_search_state : search_state.reset(blacklight_params_or_search_state)
53
+ self
54
+ end
55
+
9
56
  ##
10
57
  # Update the :q (query) parameter
11
58
  # @param [Hash<Symbol,Object>] conditions the field and values to query on
@@ -13,14 +60,14 @@ module Blacklight
13
60
  # search_builder.where(id: [1,2,3]) # produces: q:"{!lucene}id:(1 OR 2 OR 3)"
14
61
  def where(conditions)
15
62
  params_will_change!
16
- @search_state = @search_state.reset(@search_state.params.merge(q: conditions))
17
- @blacklight_params = @search_state.params
63
+ @search_state = search_state.reset(search_state.params.merge(q: conditions))
18
64
  @additional_filters = conditions
19
65
  self
20
66
  end
21
67
 
22
68
  ##
23
69
  # Append additional processor chain directives
70
+ # This is used in blacklight_range_limit
24
71
  def append(*addl_processor_chain)
25
72
  params_will_change!
26
73
  builder = self.class.new(processor_chain + addl_processor_chain, scope)
@@ -55,14 +102,103 @@ module Blacklight
55
102
  builder
56
103
  end
57
104
 
105
+ ##
106
+ # Merge additional, repository-specific parameters
107
+ def merge(extra_params, &)
108
+ if extra_params
109
+ params_will_change!
110
+ @merged_params.merge!(extra_params.to_hash, &)
111
+ end
112
+ self
113
+ end
114
+
115
+ ##
116
+ # "Reverse merge" additional, repository-specific parameters
117
+ def reverse_merge(extra_params, &)
118
+ if extra_params
119
+ params_will_change!
120
+ @reverse_merged_params.reverse_merge!(extra_params.to_hash, &)
121
+ end
122
+ self
123
+ end
124
+
125
+ delegate :[], :key?, to: :to_hash
126
+
127
+ # a solr query method
128
+ # @return [Blacklight::Solr::Response] the solr response object
129
+ def to_hash
130
+ return @params unless params_need_update?
131
+
132
+ @params = processed_parameters
133
+ .reverse_merge(@reverse_merged_params)
134
+ .merge(@merged_params)
135
+ .tap { clear_changes }
136
+ end
137
+
138
+ alias query to_hash
139
+ alias to_h to_hash
140
+
141
+ # The CatalogController #index action uses this.
142
+ # Solr parameters can come from a number of places. From lowest
143
+ # precedence to highest:
144
+ # 1. General defaults in blacklight config (are trumped by)
145
+ # 2. defaults for the particular search field identified by params[:search_field] (are trumped by)
146
+ # 3. certain parameters directly on input HTTP query params
147
+ # * not just any parameter is grabbed willy nilly, only certain ones are allowed by HTTP input)
148
+ # * for legacy reasons, qt in http query does not over-ride qt in search field definition default.
149
+ # 4. extra parameters passed in as argument.
150
+ #
151
+ # spellcheck.q will be supplied with the [:q] value unless specifically
152
+ # specified otherwise.
153
+ #
154
+ # Incoming parameter :f is mapped to :fq solr parameter.
155
+ #
156
+ # @return a params hash for searching solr.
157
+ def processed_parameters
158
+ request.tap do |request_parameters|
159
+ processor_chain.each do |method_name|
160
+ send(method_name, request_parameters)
161
+ end
162
+ end
163
+ end
164
+
165
+ def default_document_pagination_params
166
+ { fl: blacklight_config.document_model.unique_key }
167
+ end
168
+
169
+ def for_previous_and_next_documents(index, window = 1)
170
+ document_pagination_params = blacklight_config.document_pagination_params.dup
171
+
172
+ if document_pagination_params.empty?
173
+ merge(default_document_pagination_params)
174
+ else
175
+ merge(document_pagination_params)
176
+ end
177
+
178
+ if index > 0
179
+ self.start = [index - window, 0].max
180
+ self.rows = (2 * window) + 1
181
+ else
182
+ self.start = 0
183
+ self.rows = 2 * window
184
+ end
185
+
186
+ self
187
+ end
188
+
189
+ # An undefined value that can be used to detect if a parameter was passed in or not, since nil and false may be valid values for some parameters.
190
+ UNDEFINED = Object.new.freeze
191
+
58
192
  def start=(value)
193
+ return if value.nil?
194
+
59
195
  params_will_change!
60
196
  @start = value.to_i
61
197
  end
62
198
 
63
199
  # @param [#to_i] value
64
- def start(value = nil)
65
- if value
200
+ def start(value = Blacklight::SearchBuilder::UNDEFINED)
201
+ if value != Blacklight::SearchBuilder::UNDEFINED
66
202
  self.start = value
67
203
  return self
68
204
  end
@@ -71,17 +207,19 @@ module Blacklight
71
207
  val = 0 if @start < 0
72
208
  val
73
209
  end
74
- alias_method :padding, :start
210
+ alias padding start
75
211
 
76
212
  def page=(value)
213
+ return if value.nil?
214
+
77
215
  params_will_change!
78
216
  @page = value.to_i
79
217
  @page = 1 if @page < 1
80
218
  end
81
219
 
82
220
  # @param [#to_i] value
83
- def page(value = nil)
84
- if value
221
+ def page(value = Blacklight::SearchBuilder::UNDEFINED)
222
+ if value != Blacklight::SearchBuilder::UNDEFINED
85
223
  self.page = value
86
224
  return self
87
225
  end
@@ -89,13 +227,15 @@ module Blacklight
89
227
  end
90
228
 
91
229
  def rows=(value)
230
+ return if value.nil?
231
+
92
232
  params_will_change!
93
233
  @rows = [value, blacklight_config.max_per_page].map(&:to_i).min
94
234
  end
95
235
 
96
236
  # @param [#to_i] value
97
- def rows(value = nil)
98
- if value
237
+ def rows(value = Blacklight::SearchBuilder::UNDEFINED)
238
+ if value != Blacklight::SearchBuilder::UNDEFINED
99
239
  self.rows = value
100
240
  return self
101
241
  end
@@ -109,6 +249,34 @@ module Blacklight
109
249
 
110
250
  alias per rows
111
251
 
252
+ # sets the facet that this query pertains to, for the purpose of facet pagination
253
+ def facet=(value)
254
+ params_will_change!
255
+ @facet = value
256
+ end
257
+
258
+ # @param [Object] value
259
+ def facet(value = nil)
260
+ if value
261
+ self.facet = value
262
+ return self
263
+ end
264
+ @facet
265
+ end
266
+
267
+ def facet_suggestion_query=(value)
268
+ params_will_change!
269
+ @facet_suggestion_query = value
270
+ end
271
+
272
+ def facet_suggestion_query(value = nil)
273
+ if value
274
+ self.facet_suggestion_query = value
275
+ return self
276
+ end
277
+ @facet_suggestion_query
278
+ end
279
+
112
280
  # Decode the user provided 'sort' parameter into a sort string that can be
113
281
  # passed to the search. This sanitizes the input by ensuring only
114
282
  # configured search values are passed through to the search.
@@ -116,5 +284,35 @@ module Blacklight
116
284
  def sort
117
285
  search_state.sort_field&.sort
118
286
  end
287
+
288
+ delegate :search_field, to: :search_state
289
+
290
+ private
291
+
292
+ def request
293
+ Blacklight::Solr::Request.new
294
+ end
295
+
296
+ def should_add_field_to_request? _field_name, field
297
+ field.include_in_request || (field.include_in_request.nil? && blacklight_config.add_field_configuration_to_solr_request)
298
+ end
299
+
300
+ attr_reader :scope
301
+
302
+ def params_will_change!
303
+ @dirty = true
304
+ end
305
+
306
+ def params_changed?
307
+ !!@dirty
308
+ end
309
+
310
+ def params_need_update?
311
+ params_changed? || @params.nil?
312
+ end
313
+
314
+ def clear_changes
315
+ @dirty = false
316
+ end
119
317
  end
120
318
  end
@@ -37,6 +37,10 @@ module Blacklight
37
37
  params[:q]
38
38
  end
39
39
 
40
+ def query_boolean_operator
41
+ params[:op]
42
+ end
43
+
40
44
  def clause_params
41
45
  params[:clause] || {}
42
46
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  class Blacklight::Solr::InvalidParameter < ArgumentError; end
4
4
 
5
- class Blacklight::Solr::Request < ActiveSupport::HashWithIndifferentAccess
5
+ class Blacklight::Solr::Request < ActiveSupport::HashWithIndifferentAccess # rubocop:disable Style/OneClassPerFile
6
6
  def initialize(constructor = {})
7
7
  if constructor.is_a?(Hash)
8
8
  super()
@@ -52,6 +52,13 @@ class Blacklight::Solr::Request < ActiveSupport::HashWithIndifferentAccess
52
52
  self['fq'] << query
53
53
  end
54
54
 
55
+ # Filter the results to a specific set of values in the given field.
56
+ # @param [String] field
57
+ # @param [Array<String>] values
58
+ def append_ids_filter(field, values)
59
+ append_filter_query("{!terms f=#{field}}#{Array(values).join(',')}")
60
+ end
61
+
55
62
  def append_facet_fields(values)
56
63
  self['facet.field'] ||= []
57
64
  self['facet.field'] += Array(values)
@@ -11,7 +11,7 @@ class Blacklight::Solr::Response::GroupResponse
11
11
  @response = response
12
12
  end
13
13
 
14
- alias_method :group_field, :key
14
+ alias group_field key
15
15
 
16
16
  def groups
17
17
  @groups ||= group["groups"].map do |g|
@@ -53,14 +53,14 @@ module Blacklight::Solr::Response::Spelling
53
53
  # suggestion => [{ frequency =>, word => }] # for extended results
54
54
  # suggestion => ['word'] # for non-extended results
55
55
  orig_freq = term_info['origFreq']
56
- if term_info['suggestion'].first.is_a?(Hash)
57
- word_suggestions << term_info['suggestion'].map do |suggestion|
58
- suggestion['word'] if suggestion['freq'] > orig_freq
59
- end
60
- else
61
- # only extended suggestions have frequency so we just return all suggestions
62
- word_suggestions << term_info['suggestion']
63
- end
56
+ word_suggestions << if term_info['suggestion'].first.is_a?(Hash)
57
+ term_info['suggestion'].map do |suggestion|
58
+ suggestion['word'] if suggestion['freq'] > orig_freq
59
+ end
60
+ else
61
+ # only extended suggestions have frequency so we just return all suggestions
62
+ term_info['suggestion']
63
+ end
64
64
  end
65
65
  end
66
66
  end
@@ -18,7 +18,7 @@ class Blacklight::Solr::Response < ActiveSupport::HashWithIndifferentAccess
18
18
  def initialize(data, request_params, options = {})
19
19
  @search_builder = request_params if request_params.is_a?(Blacklight::SearchBuilder)
20
20
 
21
- super(force_to_utf8(ActiveSupport::HashWithIndifferentAccess.new(data)))
21
+ super(ActiveSupport::HashWithIndifferentAccess.new(data))
22
22
  @request_params = ActiveSupport::HashWithIndifferentAccess.new(request_params)
23
23
  self.blacklight_config = options[:blacklight_config]
24
24
  self.options = options
@@ -31,7 +31,7 @@ class Blacklight::Solr::Response < ActiveSupport::HashWithIndifferentAccess
31
31
  def documents
32
32
  @documents ||= (response['docs'] || []).collect { |doc| document_factory.build(doc, self, options) }
33
33
  end
34
- alias_method :docs, :documents
34
+ alias docs documents
35
35
 
36
36
  def grouped
37
37
  @groups ||= self["grouped"].map do |field, group|
@@ -61,23 +61,4 @@ class Blacklight::Solr::Response < ActiveSupport::HashWithIndifferentAccess
61
61
  def export_formats
62
62
  documents.map { |x| x.export_formats.keys }.flatten.uniq
63
63
  end
64
-
65
- private
66
-
67
- def force_to_utf8(value)
68
- case value
69
- when Hash
70
- value.each { |k, v| value[k] = force_to_utf8(v) }
71
- when Array
72
- value.each { |v| force_to_utf8(v) }
73
- when String
74
- if value.encoding == Encoding::UTF_8
75
- value
76
- else
77
- Blacklight.logger&.warn "Found a non utf-8 value in Blacklight::Solr::Response. \"#{value}\" Encoding is #{value.encoding}"
78
- value.dup.force_encoding('UTF-8')
79
- end
80
- end
81
- value
82
- end
83
64
  end
@@ -1,16 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Blacklight::Solr
4
+ # This depends on being imported into a class that extends AbstractSearchBuilder
4
5
  module SearchBuilderBehavior
5
6
  extend ActiveSupport::Concern
6
7
 
7
8
  included do
8
9
  self.default_processor_chain = [
9
- :default_solr_parameters, :add_search_field_default_parameters,
10
- :add_query_to_solr, :add_facet_fq_to_solr,
11
- :add_facetting_to_solr, :add_solr_fields_to_query, :add_paging_to_solr,
12
- :add_sorting_to_solr, :add_group_config_to_solr,
13
- :add_adv_search_clauses, :add_facets_for_advanced_search_form,
10
+ :default_solr_parameters,
11
+ :add_search_field_default_parameters,
12
+ :add_query_to_solr,
13
+ :add_facet_fq_to_solr,
14
+ :add_facetting_to_solr,
15
+ :add_solr_fields_to_query,
16
+ :add_paging_to_solr,
17
+ :add_sorting_to_solr,
18
+ :add_group_config_to_solr,
19
+ :add_adv_search_clauses,
20
+ :add_facets_for_advanced_search_form,
21
+ :add_facet_paging_to_solr,
22
+ :add_facet_suggestion_parameters,
14
23
  :add_additional_filters
15
24
  ]
16
25
  end
@@ -93,7 +102,7 @@ module Blacklight::Solr
93
102
 
94
103
  # Transform "clause" parameters into the Solr JSON Query DSL
95
104
  def add_adv_search_clauses(solr_parameters)
96
- return if search_state.clause_params.blank?
105
+ return if search_state.clause_params.blank? || search_state.clause_params.all? { |_k, v| v[:query].blank? }
97
106
 
98
107
  # We need to specify lucene as the top-level defType when using JSON Query DSL in Solr versions
99
108
  # between 7.2.0 & 9.4.0. After 9.4.0 this is no longer necessary, but also not harmful to include.
@@ -103,7 +112,7 @@ module Blacklight::Solr
103
112
  solr_parameters[:spellcheck] = 'false'
104
113
 
105
114
  defaults = { must: [], must_not: [], should: [] }
106
- default_op = blacklight_params[:op]&.to_sym || :must
115
+ default_op = search_state.query_boolean_operator&.to_sym || :must
107
116
  solr_parameters[:mm] = 1 if default_op == :should && search_state.clause_params.values.any? { |clause| }
108
117
 
109
118
  search_state.clause_params.each_value do |clause|
@@ -191,7 +200,7 @@ module Blacklight::Solr
191
200
  if facet.pivot
192
201
  solr_parameters.append_facet_pivot with_ex_local_param(facet.ex, facet.pivot.join(","))
193
202
  elsif facet.query
194
- solr_parameters.append_facet_query facet.query.values.map { |x| with_ex_local_param(facet.ex, x[:fq]) }
203
+ solr_parameters.append_facet_query(facet.query.values.map { |x| with_ex_local_param(facet.ex, x[:fq]) })
195
204
  else
196
205
  solr_parameters.append_facet_fields with_ex_local_param(facet.ex, facet.field)
197
206
  end
@@ -232,8 +241,6 @@ module Blacklight::Solr
232
241
  # copy paging params from BL app over to solr, changing
233
242
  # app level per_page and page to Solr rows and start.
234
243
  def add_paging_to_solr(solr_params)
235
- rows(solr_params[:rows] || 10) if rows.nil?
236
-
237
244
  solr_params[:rows] = rows
238
245
 
239
246
  solr_params[:start] = start if start.nonzero?
@@ -250,6 +257,48 @@ module Blacklight::Solr
250
257
  solr_parameters[:group] = false if search_state.filter(grouped_key_for_results).any?
251
258
  end
252
259
 
260
+ def add_facet_paging_to_solr(solr_params)
261
+ return if facet.blank?
262
+
263
+ facet_config = blacklight_config.facet_fields[facet]
264
+
265
+ solr_params[:rows] = 0
266
+
267
+ limit = if solr_params["facet.limit"]
268
+ solr_params["facet.limit"].to_i
269
+ else
270
+ facet_config.fetch(:more_limit, blacklight_config.default_more_limit)
271
+ end
272
+
273
+ page = search_state.facet_page
274
+ sort = search_state.facet_sort
275
+ prefix = search_state.facet_prefix
276
+ offset = (page - 1) * limit
277
+
278
+ if facet_config.json
279
+ add_solr_facet_json_params(solr_parameters, facet, facet_config, limit: limit + 1, offset: offset, sort: sort, prefix: prefix)
280
+ return
281
+ end
282
+
283
+ # Now override with our specific things for fetching facet values
284
+ facet_ex = facet_config.respond_to?(:ex) ? facet_config.ex : nil
285
+ solr_params[:'facet.field'] = with_ex_local_param(facet_ex, facet_config.field)
286
+
287
+ # Need to set as f.facet_field.facet.* to make sure we
288
+ # override any field-specific default in the solr request handler.
289
+ solr_params[:"f.#{facet_config.field}.facet.limit"] = limit + 1
290
+ solr_params[:"f.#{facet_config.field}.facet.offset"] = offset
291
+ solr_params[:"f.#{facet_config.field}.facet.sort"] = sort if sort
292
+ solr_params[:"f.#{facet_config.field}.facet.prefix"] = prefix if prefix
293
+ end
294
+
295
+ def add_facet_suggestion_parameters(solr_params)
296
+ return if facet.blank? || facet_suggestion_query.blank?
297
+
298
+ solr_params[:'facet.contains'] = facet_suggestion_query[0..50]
299
+ solr_params[:'facet.contains.ignoreCase'] = true
300
+ end
301
+
253
302
  def with_ex_local_param(ex, value)
254
303
  if ex
255
304
  "{!ex=#{ex}}#{value}"
@@ -318,12 +367,6 @@ module Blacklight::Solr
318
367
  end
319
368
  end
320
369
 
321
- def search_state
322
- return super if defined?(super)
323
-
324
- @search_state ||= Blacklight::SearchState.new(blacklight_params, blacklight_config)
325
- end
326
-
327
370
  def add_search_field_query_builder_params(solr_parameters)
328
371
  q, additional_parameters = search_field.query_builder.call(self, search_field, solr_parameters)
329
372
 
data/lib/blacklight.rb CHANGED
@@ -85,7 +85,7 @@ module Blacklight
85
85
  else
86
86
  YAML.safe_load(blacklight_erb, [], [], true)
87
87
  end
88
- rescue => e
88
+ rescue StandardError => e
89
89
  raise("#{blacklight_config_file} was found, but could not be parsed.\n#{e.inspect}")
90
90
  end
91
91
 
@@ -122,6 +122,6 @@ module Blacklight
122
122
  end
123
123
 
124
124
  def self.deprecation
125
- @deprecation ||= ActiveSupport::Deprecation.new('9.0', 'Blacklight')
125
+ @deprecation ||= ActiveSupport::Deprecation.new('10.0', 'Blacklight')
126
126
  end
127
127
  end
@@ -8,7 +8,7 @@ module Blacklight
8
8
  def import_javascript_assets
9
9
  append_to_file 'config/importmap.rb' do
10
10
  <<~CONTENT
11
- pin "@github/auto-complete-element", to: "https://cdn.skypack.dev/@github/auto-complete-element"
11
+ pin "@github/auto-complete-element", to: "https://cdn.jsdelivr.net/npm/@github/auto-complete-element@3.8.0/+esm"
12
12
  pin "@popperjs/core", to: "https://ga.jspm.io/npm:@popperjs/core@2.11.6/dist/umd/popper.min.js"
13
13
  pin "bootstrap", to: "https://ga.jspm.io/npm:bootstrap@#{(defined?(Bootstrap) && Bootstrap::VERSION) || '5.3.5'}/dist/js/bootstrap.js"
14
14
  CONTENT
@@ -49,7 +49,7 @@ module Blacklight
49
49
  else
50
50
  append_to_file 'app/assets/stylesheets/application.css' do
51
51
  <<~CONTENT
52
- @import url(https://cdn.jsdelivr.net/npm/bootstrap@5.3.5/dist/css/bootstrap.min.css);
52
+ @import url(https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css);
53
53
  @import url("blacklight.css");
54
54
  CONTENT
55
55
  end