blacklight-spotlight 2.9.0 → 2.10.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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/spotlight/custom_search_fields_controller.rb +65 -0
  3. data/app/controllers/spotlight/featured_images_controller.rb +4 -1
  4. data/app/models/spotlight/ability.rb +1 -0
  5. data/app/models/spotlight/blacklight_configuration.rb +13 -0
  6. data/app/models/spotlight/custom_search_field.rb +37 -0
  7. data/app/models/spotlight/exhibit.rb +1 -0
  8. data/app/models/spotlight/featured_image.rb +1 -2
  9. data/app/models/spotlight/resources/iiif_manifest.rb +80 -15
  10. data/app/models/spotlight/solr_document_sidecar.rb +4 -4
  11. data/app/presenters/spotlight/iiif_manifest_presenter.rb +4 -2
  12. data/app/services/spotlight/upload_solr_document_builder.rb +3 -2
  13. data/app/views/spotlight/custom_search_fields/_form.html.erb +13 -0
  14. data/app/views/spotlight/custom_search_fields/edit.html.erb +5 -0
  15. data/app/views/spotlight/custom_search_fields/new.html.erb +5 -0
  16. data/app/views/spotlight/search_configurations/_search_fields.html.erb +27 -0
  17. data/app/views/spotlight/search_configurations/edit.html.erb +0 -1
  18. data/config/i18n-tasks.yml +3 -1
  19. data/config/locales/spotlight.en.yml +22 -0
  20. data/config/routes.rb +1 -0
  21. data/db/migrate/20190910200927_create_spotlight_custom_search_fields.rb +12 -0
  22. data/lib/generators/spotlight/templates/config/initializers/spotlight_initializer.rb +34 -0
  23. data/lib/spotlight/engine.rb +32 -0
  24. data/lib/spotlight/upload_field_config.rb +21 -4
  25. data/lib/spotlight/version.rb +1 -1
  26. data/spec/controllers/spotlight/custom_search_fields_controller_spec.rb +60 -0
  27. data/spec/examples.txt +1326 -1282
  28. data/spec/factories/custom_search_fields.rb +9 -0
  29. data/spec/features/exhibits/custom_search_fields_spec.rb +55 -0
  30. data/spec/fixtures/iiif_responses.rb +24 -0
  31. data/spec/lib/spotlight/upload_field_config_spec.rb +16 -0
  32. data/spec/models/spotlight/blacklight_configuration_spec.rb +21 -0
  33. data/spec/models/spotlight/custom_search_field_spec.rb +54 -0
  34. data/spec/models/spotlight/resources/iiif_manifest_spec.rb +38 -2
  35. data/spec/presenters/spotlight/iiif_manifest_presenter_spec.rb +2 -4
  36. metadata +16 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8445fcedab2b6c93ecc559cb0ecc02c1c0d6e42c987d7467a7b17b1a04c86456
4
- data.tar.gz: 63e9bc33643a7db25d6b3e875c3dae7374085389c42224bfb71572a8e7995221
3
+ metadata.gz: 2148d0879e24d68085e54c6fb9569af9900383d7bdb12c00f1568cf846e6b3c0
4
+ data.tar.gz: e2ff46720e4f722e64a47eeb29ceaf4cc7a981731942134b81ae0f94eb376b02
5
5
  SHA512:
6
- metadata.gz: 0e7ecb7302c87a9006ffcd74aa4c2b3da56d17cded2cfd90f2803c9582fa11cb35385ad9edfbabee44bed698acb7191ee2dd22f48e19034c106d7bfa447071e7
7
- data.tar.gz: 9417a9864e63890323ace42f0b5d1b8f1d63449b2d437f22d47df9f4d882e7037713aa097910b24fd58a80b20b4939a4d39c4b2aee2c2a3d77b3329bbfa90dfd
6
+ metadata.gz: 9a8590c2c12ab0c5df2e61ea8e5608a465564dd33ee7b6fb849578f98fb5aa5116b7706240ec2955065b6e0d1c65d1db4fa0837e2d73083fc4a0cf06cff7c0b6
7
+ data.tar.gz: 290494323ae7ccb37ccfd9e80df582df82814f6129025bc95d42dad2611ed7d105cb3cdc0c1cd40239edb05aa3dff4177ee784980209e8100cb0bf82351aba58
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spotlight
4
+ # CRUD actions for exhibit custom search field management.
5
+ class CustomSearchFieldsController < ApplicationController
6
+ before_action :authenticate_user!
7
+
8
+ load_and_authorize_resource :exhibit, class: Spotlight::Exhibit
9
+ load_and_authorize_resource through: :exhibit
10
+ before_action :attach_breadcrumbs, only: [:new, :edit]
11
+
12
+ # GET /custom_search_fields/new
13
+ def new
14
+ add_breadcrumb t(:'helpers.action.spotlight/custom_search_field.create'), new_exhibit_custom_search_field_path(@exhibit)
15
+ end
16
+
17
+ # GET /custom_search_fields/1/edit
18
+ def edit
19
+ add_breadcrumb @custom_search_field.label, edit_exhibit_custom_search_field_path(@custom_search_field.exhibit, @custom_search_field)
20
+ end
21
+
22
+ # POST /custom_search_fields
23
+ def create
24
+ @custom_search_field.attributes = custom_search_field_params
25
+ @custom_search_field.exhibit = current_exhibit
26
+
27
+ if @custom_search_field.save
28
+ redirect_to edit_exhibit_search_configuration_path(@custom_search_field.exhibit),
29
+ notice: t(:'helpers.submit.custom_search_field.created', model: @custom_search_field.class.model_name.human.downcase)
30
+ else
31
+ render action: 'new'
32
+ end
33
+ end
34
+
35
+ # PATCH/PUT /custom_search_fields/1
36
+ def update
37
+ if @custom_search_field.update(custom_search_field_params)
38
+ redirect_to edit_exhibit_search_configuration_path(@custom_search_field.exhibit),
39
+ notice: t(:'helpers.submit.custom_search_field.updated', model: @custom_search_field.class.model_name.human.downcase)
40
+ else
41
+ render :edit
42
+ end
43
+ end
44
+
45
+ def destroy
46
+ @custom_search_field.destroy
47
+
48
+ redirect_to edit_exhibit_search_configuration_path(@custom_search_field.exhibit),
49
+ notice: t(:'helpers.submit.custom_search_field.destroyed', model: @custom_search_field.class.model_name.human.downcase)
50
+ end
51
+
52
+ private
53
+
54
+ def attach_breadcrumbs
55
+ add_breadcrumb t(:'spotlight.exhibits.breadcrumb', title: @exhibit.title), @exhibit
56
+ add_breadcrumb t(:'spotlight.configuration.sidebar.header'), exhibit_dashboard_path(@exhibit)
57
+ add_breadcrumb t(:'spotlight.configuration.sidebar.search_configuration'), edit_exhibit_search_configuration_path(@exhibit)
58
+ end
59
+
60
+ # Only allow a trusted parameter "white list" through.
61
+ def custom_search_field_params
62
+ params.require(:custom_search_field).permit(:slug, :field, :label)
63
+ end
64
+ end
65
+ end
@@ -16,7 +16,10 @@ module Spotlight
16
16
  private
17
17
 
18
18
  def tilesource
19
- riiif.info_url(@featured_image.id)
19
+ Spotlight::Engine.config.iiif_url_helpers.info_url(
20
+ @featured_image.id,
21
+ host: request.host_with_port
22
+ )
20
23
  end
21
24
 
22
25
  # The create action can be called from a number of different forms, so
@@ -29,6 +29,7 @@ module Spotlight
29
29
  Spotlight::Page,
30
30
  Spotlight::Contact,
31
31
  Spotlight::CustomField,
32
+ Spotlight::CustomSearchField,
32
33
  Translation
33
34
  ], exhibit_id: user.exhibit_roles.pluck(:resource_id)
34
35
 
@@ -136,6 +136,7 @@ module Spotlight
136
136
 
137
137
  config.show_fields = config.index_fields
138
138
 
139
+ config.search_fields.merge! custom_search_fields(config)
139
140
  unless search_fields.blank?
140
141
  config.search_fields = Hash[config.search_fields.sort_by { |k, _v| field_weight(search_fields, k) }]
141
142
 
@@ -227,6 +228,18 @@ module Spotlight
227
228
  end]
228
229
  end
229
230
 
231
+ def custom_search_fields(blacklight_config)
232
+ Hash[exhibit.custom_search_fields.reject(&:new_record?).map do |custom_field|
233
+ original_config = blacklight_config.search_fields[custom_field.field] || {}
234
+ field = Blacklight::Configuration::SearchField.new original_config.merge(
235
+ custom_field.configuration.merge(
236
+ key: custom_field.slug, solr_parameters: { qf: custom_field.field }, custom_field: true
237
+ )
238
+ )
239
+ [custom_field.slug, field]
240
+ end]
241
+ end
242
+
230
243
  ##
231
244
  # Get the "upstream" blacklight configuration to use
232
245
  def default_blacklight_config
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spotlight
4
+ # Exhibit-specific custom search fields
5
+ class CustomSearchField < ApplicationRecord
6
+ serialize :configuration, Hash
7
+ belongs_to :exhibit
8
+
9
+ def label=(label)
10
+ configuration['label'] = label
11
+
12
+ update_blacklight_configuration_label label
13
+ end
14
+
15
+ def label
16
+ conf = if slug && blacklight_configuration && blacklight_configuration.search_fields.key?(slug)
17
+ blacklight_configuration.search_fields[slug].reverse_merge(configuration)
18
+ else
19
+ configuration
20
+ end
21
+ conf['label']
22
+ end
23
+
24
+ protected
25
+
26
+ def blacklight_configuration
27
+ exhibit&.blacklight_configuration
28
+ end
29
+
30
+ def update_blacklight_configuration_label(label)
31
+ return unless slug && blacklight_configuration && blacklight_configuration.search_fields.key?(slug)
32
+
33
+ blacklight_configuration.search_fields[slug]['label'] = label
34
+ blacklight_configuration.save
35
+ end
36
+ end
37
+ end
@@ -46,6 +46,7 @@ module Spotlight
46
46
  end]
47
47
  end
48
48
  end
49
+ has_many :custom_search_fields, dependent: :delete_all
49
50
 
50
51
  has_many :feature_pages, -> { for_default_locale }, extend: FriendlyId::FinderMethods
51
52
  has_many :main_navigations, dependent: :delete_all
@@ -53,8 +53,7 @@ module Spotlight
53
53
  def set_tilesource_from_uploaded_resource
54
54
  return if iiif_tilesource
55
55
 
56
- riiif = Riiif::Engine.routes.url_helpers
57
- self.iiif_tilesource = riiif.info_path(id)
56
+ self.iiif_tilesource = Spotlight::Engine.config.iiif_url_helpers.info_path(id)
58
57
  save
59
58
  end
60
59
 
@@ -70,7 +70,7 @@ module Spotlight
70
70
  return unless title_fields.present? && manifest.try(:label)
71
71
 
72
72
  Array.wrap(title_fields).each do |field|
73
- solr_hash[field] = json_ld_value(manifest.label)
73
+ solr_hash[field] = metadata_class.new(manifest).label
74
74
  end
75
75
  end
76
76
 
@@ -96,13 +96,6 @@ module Spotlight
96
96
  end
97
97
  end
98
98
 
99
- def json_ld_value(value)
100
- return value['@value'] if value.is_a?(Hash)
101
- return value.find { |v| v['@language'] == default_json_ld_language }.try(:[], '@value') if value.is_a?(Array)
102
-
103
- value
104
- end
105
-
106
99
  def create_sidecars_for(*keys)
107
100
  missing_keys(keys).each do |k|
108
101
  exhibit.custom_fields.create! label: k, readonly_field: true
@@ -164,10 +157,6 @@ module Spotlight
164
157
  Spotlight::Engine.config.iiif_title_fields || blacklight_config.index.try(:title_field)
165
158
  end
166
159
 
167
- def default_json_ld_language
168
- Spotlight::Engine.config.default_json_ld_language
169
- end
170
-
171
160
  def sidecar
172
161
  @sidecar ||= document_model.new(id: compound_id).sidecar(exhibit)
173
162
  end
@@ -195,6 +184,12 @@ module Spotlight
195
184
  metadata_hash.merge(manifest_level_metadata)
196
185
  end
197
186
 
187
+ def label
188
+ return unless manifest.try(:label)
189
+
190
+ Array(json_ld_value(manifest.label)).map { |v| html_sanitize(v) }.first
191
+ end
192
+
198
193
  private
199
194
 
200
195
  attr_reader :manifest
@@ -210,8 +205,10 @@ module Spotlight
210
205
  metadata.each_with_object({}) do |md, hash|
211
206
  next unless md['label'] && md['value']
212
207
 
213
- hash[md['label']] ||= []
214
- hash[md['label']] += Array(md['value'])
208
+ label = Array(json_ld_value(md['label'])).first
209
+
210
+ hash[label] ||= []
211
+ hash[label] += Array(json_ld_value(md['value'])).map { |v| html_sanitize(v) }
215
212
  end
216
213
  end
217
214
 
@@ -221,13 +218,81 @@ module Spotlight
221
218
  manifest.send(field).present?
222
219
 
223
220
  hash[field.capitalize] ||= []
224
- hash[field.capitalize] += Array(manifest.send(field))
221
+ hash[field.capitalize] += Array(json_ld_value(manifest.send(field))).map { |v| html_sanitize(v) }
225
222
  end
226
223
  end
227
224
 
228
225
  def manifest_fields
229
226
  %w(attribution description license)
230
227
  end
228
+
229
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity
230
+ def json_ld_value(value)
231
+ case value
232
+ # In the case where multiple values are supplied, clients must use the following algorithm to determine which values to display to the user.
233
+ when Array
234
+ # IIIF v2, multivalued monolingual, or multivalued multilingual values
235
+
236
+ # If none of the values have a language associated with them, the client must display all of the values.
237
+ if value.none? { |v| v.is_a?(Hash) && v.key?('@language') }
238
+ value.map { |v| json_ld_value(v) }
239
+ # If any of the values have a language associated with them, the client must display all of the values associated with the language that best
240
+ # matches the language preference.
241
+ elsif value.any? { |v| v.is_a?(Hash) && v['@language'] == default_json_ld_language }
242
+ value.select { |v| v.is_a?(Hash) && v['@language'] == default_json_ld_language }.map { |v| v['@value'] }
243
+ # If all of the values have a language associated with them, and none match the language preference, the client must select a language
244
+ # and display all of the values associated with that language.
245
+ elsif value.all? { |v| v.is_a?(Hash) && v.key?('@language') }
246
+ selected_json_ld_language = value.find { |v| v.is_a?(Hash) && v.key?('@language') }
247
+
248
+ value.select { |v| v.is_a?(Hash) && v['@language'] == selected_json_ld_language['@language'] }
249
+ .map { |v| v['@value'] }
250
+ # If some of the values have a language associated with them, but none match the language preference, the client must display all of the values
251
+ # that do not have a language associated with them.
252
+ else
253
+ value.select { |v| !v.is_a?(Hash) || !v.key?('@language') }.map { |v| json_ld_value(v) }
254
+ end
255
+ when Hash
256
+ # IIIF v2 single-valued value
257
+ if value.key? '@value'
258
+ value['@value']
259
+ # IIIF v3 multilingual(?), multivalued(?) values
260
+ # If all of the values are associated with the none key, the client must display all of those values.
261
+ elsif value.keys == ['none']
262
+ value['none']
263
+ # If any of the values have a language associated with them, the client must display all of the values associated with the language
264
+ # that best matches the language preference.
265
+ elsif value.key? default_json_ld_language
266
+ value[default_json_ld_language]
267
+ # If some of the values have a language associated with them, but none match the language preference, the client must display all
268
+ # of the values that do not have a language associated with them.
269
+ elsif value.key? 'none'
270
+ value['none']
271
+ # If all of the values have a language associated with them, and none match the language preference, the client must select a
272
+ # language and display all of the values associated with that language.
273
+ else
274
+ value.values.first
275
+ end
276
+ else
277
+ # plain old string/number/boolean
278
+ value
279
+ end
280
+ end
281
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity
282
+
283
+ def html_sanitize(value)
284
+ return value unless value.is_a? String
285
+
286
+ html_sanitizer.sanitize(value)
287
+ end
288
+
289
+ def html_sanitizer
290
+ @html_sanitizer ||= Rails::Html::FullSanitizer.new
291
+ end
292
+
293
+ def default_json_ld_language
294
+ Spotlight::Engine.config.default_json_ld_language
295
+ end
231
296
  end
232
297
  end
233
298
  end
@@ -74,11 +74,11 @@ module Spotlight
74
74
  field_name = field.field_name.to_s
75
75
  next unless configured_fields && configured_fields[field_name].present?
76
76
 
77
- solr_fields = field.solr_fields || Array(field.solr_field || field.field_name)
77
+ value = configured_fields[field_name]
78
+ field_data = field.data_to_solr(value)
78
79
 
79
- solr_fields.each do |solr_field|
80
- solr_hash[solr_field] = configured_fields[field_name]
81
- end
80
+ # merge duplicate field mappings into a multivalued field
81
+ solr_hash.merge!(field_data) { |_key, v1, v2| Array(v1) + Array(v2) }
82
82
  end
83
83
  end
84
84
 
@@ -74,8 +74,10 @@ module Spotlight
74
74
  end
75
75
 
76
76
  def iiif_url
77
- # yes this is hacky, and we are appropriately ashamed.
78
- controller.riiif.info_url(uploaded_resource.upload.id).sub(%r{/info\.json\Z}, '')
77
+ Spotlight::Engine.config.iiif_url_helpers.info_url(
78
+ uploaded_resource.upload.id,
79
+ host: controller.request.host_with_port
80
+ ).sub(%r{/info\.json\Z}, '')
79
81
  end
80
82
  end
81
83
  end
@@ -31,7 +31,8 @@ module Spotlight
31
31
  end
32
32
 
33
33
  def add_file_versions(solr_hash)
34
- solr_hash[Spotlight::Engine.config.thumbnail_field] = riiif.image_path(resource.upload_id, size: '!400,400')
34
+ solr_hash[Spotlight::Engine.config.thumbnail_field] =
35
+ Spotlight::Engine.config.iiif_url_helpers.image_path(resource.upload_id, size: '!400,400')
35
36
  end
36
37
 
37
38
  def add_sidecar_fields(solr_hash)
@@ -47,7 +48,7 @@ module Spotlight
47
48
  end
48
49
 
49
50
  def riiif
50
- Riiif::Engine.routes.url_helpers
51
+ Spotlight::Engine.config.iiif_url_helpers
51
52
  end
52
53
 
53
54
  def attached_file?
@@ -0,0 +1,13 @@
1
+ <%= bootstrap_form_for @custom_search_field.new_record? ? [current_exhibit, @custom_search_field] : [@custom_search_field.exhibit, @custom_search_field], layout: :horizontal, label_col: 'col-md-3', control_col: 'col-md-9', html: {class: 'col-md-9', id: 'edit-search-field'} do |f| %>
2
+
3
+ <%= f.text_field :slug %>
4
+ <%= f.text_field :field, help: t('.field.help') %>
5
+ <%= f.text_field :label %>
6
+
7
+ <div class="form-actions">
8
+ <div class="primary-actions">
9
+ <%= link_to t(:"cancel"), edit_exhibit_search_configuration_path(current_exhibit), class: "btn btn-link" %>
10
+ <%= f.submit nil, class: 'btn btn-primary' %>
11
+ </div>
12
+ </div>
13
+ <% end %>
@@ -0,0 +1,5 @@
1
+ <%= render 'spotlight/shared/exhibit_sidebar' %>
2
+ <div id="content" class="col-md-9">
3
+ <%= configuration_page_title %>
4
+ <%= render 'form' %>
5
+ </div>
@@ -0,0 +1,5 @@
1
+ <%= render 'spotlight/shared/exhibit_sidebar' %>
2
+ <div id="content" class="col-md-9">
3
+ <%= configuration_page_title %>
4
+ <%= render 'form' %>
5
+ </div>
@@ -55,3 +55,30 @@
55
55
  </div>
56
56
  <% end %>
57
57
  <% end %>
58
+
59
+ <% if can? :manage, Spotlight::CustomSearchField.new(exhibit: current_exhibit) %>
60
+ <h3><%= t(:'.exhibit_specific.header') %></h3>
61
+ <p class="instructions"><%= t(:'.exhibit_specific.instructions') %></p>
62
+
63
+ <table class="table table-striped" id="exhibit-specific-fields">
64
+ <tbody>
65
+ <% @exhibit.custom_search_fields.each do |field| %>
66
+ <tr>
67
+ <td>
68
+ <div class="field-label"><%= field.label %></div>
69
+ <div class="actions">
70
+ <%= exhibit_edit_link field, class: 'btn btn-link' %> &middot;
71
+ <%= exhibit_delete_link field, class: 'btn btn-link' %>
72
+ </div>
73
+ </td>
74
+ <td class="field-description">
75
+ <%= field.field %>
76
+ </td>
77
+ </tr>
78
+ <% end %>
79
+
80
+ </tbody>
81
+ </table>
82
+
83
+ <%= exhibit_create_link Spotlight::CustomSearchField.new, class: 'btn btn-primary' %>
84
+ <% end %>
@@ -41,6 +41,5 @@
41
41
  <%= f.submit nil, class: 'btn btn-primary' %>
42
42
  </div>
43
43
  </div>
44
-
45
44
  <% end %>
46
45
  </div>
@@ -37,6 +37,7 @@ ignore_unused:
37
37
  - activerecord.attributes.spotlight/exhibit.published # app/views/spotlight/sites/_exhibit.html.erb
38
38
  - activerecord.attributes.spotlight/masthead.display # app/views/spotlight/appearances/edit.html.erb
39
39
  - activerecord.attributes.spotlight/custom_field.is_multiple # app/views/spotlight/custom_fields/_form.html.erb
40
+ - activerecord.attributes.spotlight/custom_search_field.field # app/views/spotlight/custom_search_fields/_form.html.erb
40
41
  - helpers.label.spotlight/filter.{field,value} # app/views/spotlight/filters/_form.html.erb
41
42
  - spotlight.catalog.admin.{title,header} # app/helpers/spotlight/title_helper.rb
42
43
  - spotlight.{contacts,pages,searches}.edit.{title,header} # app/helpers/spotlight/title_helper.rb
@@ -46,8 +47,9 @@ ignore_unused:
46
47
  - spotlight.metadata_configurations.edit.{select_all,deselect_all} # app/helpers/spotlight/application_helper.rb
47
48
  - spotlight.featured_images.upload_form.{non_iiif_alert_html,source.exhibit.help,source.exhibit.label} # app/views/spotlight/featured_images/_form.html.erb
48
49
  - spotlight.feature_pages.page_options.published # app/views/spotlight/feature_pages/_page_options.html.erb
49
- - spotlight.{exhibits,custom_fields}.{new,edit}.header # configuration_page_title
50
+ - spotlight.{exhibits,custom_fields,custom_search_fields}.{new,edit}.header # configuration_page_title
50
51
  - helpers.submit.custom_field.{batch_error,batch_updated,create,submit,update} # Generic repeated template
52
+ - helpers.submit.custom_search_field.{batch_error,batch_updated,create,submit,update} # Generic repeated template
51
53
  - helpers.submit.exhibit.{batch_error,batch_updated,create,submit,update} # Generic repeated template
52
54
  - helpers.submit.search.{create,submit,update} # Generic repeated template
53
55
  - helpers.submit.site.{batch_error,batch_updated,create,created,destroyed,submit,update} # Generic repeated template