blacklight-spotlight 2.9.0 → 2.10.0

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