blacklight-spotlight 5.1.0 → 5.2.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 (98) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +153 -48
  3. data/app/assets/javascripts/spotlight/spotlight.esm.js +33 -1
  4. data/app/assets/javascripts/spotlight/spotlight.esm.js.map +1 -1
  5. data/app/assets/javascripts/spotlight/spotlight.js +33 -1
  6. data/app/assets/javascripts/spotlight/spotlight.js.map +1 -1
  7. data/app/assets/stylesheets/spotlight/_admin_users.scss +28 -0
  8. data/app/assets/stylesheets/spotlight/_browse.scss +1 -1
  9. data/app/assets/stylesheets/spotlight/_featured_browse_categories_block.scss +1 -1
  10. data/app/assets/stylesheets/spotlight/_spotlight.scss +1 -0
  11. data/app/components/spotlight/admin_users/email_component.html.erb +5 -0
  12. data/app/components/spotlight/admin_users/email_component.rb +22 -0
  13. data/app/components/spotlight/admin_users/exhibit_roles_component.html.erb +28 -0
  14. data/app/components/spotlight/admin_users/exhibit_roles_component.rb +19 -0
  15. data/app/components/spotlight/admin_users/site_admin_component.html.erb +13 -0
  16. data/app/components/spotlight/admin_users/site_admin_component.rb +17 -0
  17. data/app/components/spotlight/analytics/aggregation_component.rb +1 -1
  18. data/app/components/spotlight/analytics/dashboard_component.rb +1 -1
  19. data/app/components/spotlight/breadcrumbs_component.rb +1 -1
  20. data/app/components/spotlight/bulk_action_component.rb +1 -1
  21. data/app/components/spotlight/edit_view_links_component.rb +1 -1
  22. data/app/components/spotlight/header_navigation_link_component.rb +1 -1
  23. data/app/components/spotlight/save_search_component.rb +1 -1
  24. data/app/components/spotlight/select_image_component.rb +1 -1
  25. data/app/components/spotlight/skip_link_component.rb +1 -1
  26. data/app/components/spotlight/solr_document_legacy_embed_component.rb +2 -2
  27. data/app/components/spotlight/tag_list_form_component.rb +1 -1
  28. data/app/components/spotlight/tag_selector_component.rb +1 -1
  29. data/app/components/spotlight/title_component.rb +1 -1
  30. data/app/components/spotlight/translations/subheading_component.rb +1 -1
  31. data/app/components/spotlight/uneditable_non_default_language_component.rb +1 -1
  32. data/app/controllers/spotlight/admin_users_controller.rb +11 -1
  33. data/app/controllers/spotlight/browse_controller.rb +2 -8
  34. data/app/controllers/spotlight/bulk_actions_controller.rb +1 -1
  35. data/app/controllers/spotlight/bulk_updates_controller.rb +22 -7
  36. data/app/controllers/spotlight/catalog_controller.rb +7 -15
  37. data/app/controllers/spotlight/dashboards_controller.rb +2 -6
  38. data/app/controllers/spotlight/exhibits_controller.rb +1 -0
  39. data/app/controllers/spotlight/home_pages_controller.rb +1 -1
  40. data/app/controllers/spotlight/searches_controller.rb +1 -1
  41. data/app/controllers/spotlight/solr_controller.rb +1 -0
  42. data/app/helpers/spotlight/main_app_helpers.rb +1 -5
  43. data/app/helpers/spotlight/rendering_helper.rb +4 -1
  44. data/app/javascript/spotlight/admin/blocks/pages_block.js +2 -0
  45. data/app/javascript/spotlight/user/carousel.js +32 -2
  46. data/app/jobs/spotlight/add_tags_job.rb +1 -0
  47. data/app/jobs/spotlight/add_uploads_from_csv.rb +1 -0
  48. data/app/jobs/spotlight/change_visibility_job.rb +1 -0
  49. data/app/jobs/spotlight/process_bulk_updates_csv_job.rb +1 -0
  50. data/app/jobs/spotlight/reindex_exhibit_job.rb +1 -0
  51. data/app/jobs/spotlight/reindex_job.rb +1 -0
  52. data/app/jobs/spotlight/remove_tags_job.rb +1 -0
  53. data/app/jobs/spotlight/rename_sidecar_field_job.rb +1 -0
  54. data/app/models/concerns/spotlight/user.rb +5 -0
  55. data/app/models/sir_trevor_rails/blocks/browse_block.rb +1 -1
  56. data/app/models/sir_trevor_rails/blocks/featured_pages_block.rb +1 -1
  57. data/app/models/sir_trevor_rails/blocks/solr_documents_block.rb +1 -0
  58. data/app/models/spotlight/ability.rb +1 -1
  59. data/app/models/spotlight/about_page.rb +1 -0
  60. data/app/models/spotlight/blacklight_configuration.rb +6 -7
  61. data/app/models/spotlight/contact.rb +2 -1
  62. data/app/models/spotlight/contact_email.rb +1 -0
  63. data/app/models/spotlight/custom_field.rb +1 -0
  64. data/app/models/spotlight/exhibit.rb +2 -9
  65. data/app/models/spotlight/feature_page.rb +1 -0
  66. data/app/models/spotlight/group.rb +2 -1
  67. data/app/models/spotlight/home_page.rb +1 -0
  68. data/app/models/spotlight/job_tracker.rb +1 -1
  69. data/app/models/spotlight/main_navigation.rb +1 -1
  70. data/app/models/spotlight/page.rb +28 -19
  71. data/app/models/spotlight/resource.rb +1 -0
  72. data/app/models/spotlight/resources/iiif_manifest.rb +0 -126
  73. data/app/models/spotlight/resources/iiif_manifest_metadata.rb +161 -0
  74. data/app/models/spotlight/resources/iiif_manifest_v3.rb +41 -0
  75. data/app/models/spotlight/resources/iiif_service.rb +25 -1
  76. data/app/models/spotlight/search.rb +2 -1
  77. data/app/services/spotlight/exhibit_import_export_service.rb +3 -1
  78. data/app/views/spotlight/admin_users/index.html.erb +16 -26
  79. data/app/views/spotlight/catalog/_document_admin_table.html.erb +2 -6
  80. data/app/views/spotlight/catalog/edit.html.erb +1 -6
  81. data/app/views/spotlight/exhibits/_exhibit_card.html.erb +1 -1
  82. data/app/views/spotlight/pages/_order_pages.html.erb +1 -1
  83. data/app/views/spotlight/pages/show.html.erb +4 -2
  84. data/app/views/spotlight/resources/csv_upload/_form.html.erb +2 -0
  85. data/app/views/spotlight/sir_trevor/blocks/_embedded_document.html.erb +2 -2
  86. data/app/views/spotlight/sir_trevor/blocks/_search_results_block.html.erb +2 -2
  87. data/app/views/spotlight/sir_trevor/blocks/_solr_documents_carousel_block.html.erb +5 -3
  88. data/app/views/spotlight/sir_trevor/blocks/_text_block.html.erb +1 -1
  89. data/config/initializers/devise_rails8_patch.rb +13 -0
  90. data/config/locales/spotlight.en.yml +34 -5
  91. data/config/routes.rb +5 -1
  92. data/lib/spotlight/engine.rb +10 -26
  93. data/lib/spotlight/version.rb +1 -1
  94. data/spec/fixtures/iiif_responses.rb +344 -0
  95. data/spec/support/features/test_features_helpers.rb +4 -6
  96. data/spec/support/stub_iiif_response.rb +1 -0
  97. data/spec/support/with_queue_adapter.rb +17 -0
  98. metadata +42 -29
@@ -6,6 +6,7 @@ module Spotlight
6
6
  ###
7
7
  class ProcessBulkUpdatesCsvJob < Spotlight::ApplicationJob
8
8
  include Spotlight::JobTracking
9
+
9
10
  with_job_tracking(resource: ->(job) { job.arguments.first })
10
11
 
11
12
  # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
@@ -5,6 +5,7 @@ module Spotlight
5
5
  # Reindex an exhibit by parallelizing resource indexing into multiple batches of reindex jobs
6
6
  class ReindexExhibitJob < Spotlight::ApplicationJob
7
7
  include Spotlight::JobTracking
8
+
8
9
  with_job_tracking(resource: ->(job) { job.arguments.first })
9
10
 
10
11
  include Spotlight::LimitConcurrency
@@ -5,6 +5,7 @@ module Spotlight
5
5
  # Reindex the given resources or exhibits
6
6
  class ReindexJob < Spotlight::ApplicationJob
7
7
  include Spotlight::JobTracking
8
+
8
9
  with_job_tracking(resource: ->(job) { job.exhibit })
9
10
 
10
11
  include Spotlight::LimitConcurrency
@@ -5,6 +5,7 @@ module Spotlight
5
5
  class RemoveTagsJob < Spotlight::ApplicationJob
6
6
  include Spotlight::JobTracking
7
7
  include Spotlight::GatherDocuments
8
+
8
9
  with_job_tracking(resource: ->(job) { job.arguments.last[:exhibit] })
9
10
 
10
11
  # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
@@ -6,6 +6,7 @@ module Spotlight
6
6
  # need to update the sidecars that may contain that field
7
7
  class RenameSidecarFieldJob < Spotlight::ApplicationJob
8
8
  include Spotlight::JobTracking
9
+
9
10
  with_job_tracking(resource: ->(job) { job.arguments.first })
10
11
 
11
12
  def perform(exhibit, old_field, new_field, old_slug = nil, new_slug = nil)
@@ -5,6 +5,7 @@ module Spotlight
5
5
  # Spotlight user mixins for roles
6
6
  module User
7
7
  extend ActiveSupport::Concern
8
+
8
9
  included do
9
10
  has_many :roles, class_name: 'Spotlight::Role', dependent: :destroy
10
11
  has_many :exhibits, class_name: 'Spotlight::Exhibit', through: :roles, source: 'resource', source_type: 'Spotlight::Exhibit'
@@ -18,6 +19,10 @@ module Spotlight
18
19
  roles.where(role: 'admin', resource: Spotlight::Site.instance).any?
19
20
  end
20
21
 
22
+ def all_exhibit_roles
23
+ roles.includes(:resource).where(resource_type: 'Spotlight::Exhibit')
24
+ end
25
+
21
26
  def exhibit_roles
22
27
  roles.where(resource_type: 'Spotlight::Exhibit').where.not(role: 'viewer')
23
28
  end
@@ -14,7 +14,7 @@ module SirTrevorRails
14
14
  end
15
15
 
16
16
  def search_options(id)
17
- (items.detect { |x| x[:id] == id }) || {}
17
+ items.detect { |x| x[:id] == id } || {}
18
18
  end
19
19
 
20
20
  def searches
@@ -8,7 +8,7 @@ module SirTrevorRails
8
8
  include Displayable
9
9
 
10
10
  def page_options(id)
11
- (items.detect { |x| x[:id] == id }) || {}
11
+ items.detect { |x| x[:id] == id } || {}
12
12
  end
13
13
 
14
14
  def pages
@@ -7,6 +7,7 @@ module SirTrevorRails
7
7
  class SolrDocumentsBlock < SirTrevorRails::Block
8
8
  include Textable
9
9
  include Displayable
10
+
10
11
  attr_reader :solr_helper
11
12
 
12
13
  def with_solr_helper(solr_helper)
@@ -42,7 +42,7 @@ module Spotlight
42
42
  can %i[read curate tag bulk_update], Spotlight::Exhibit, id: user.exhibit_roles.pluck(:resource_id)
43
43
 
44
44
  # public
45
- can :read, Spotlight::HomePage
45
+ can :read, Spotlight::HomePage, published: true
46
46
  can :read, Spotlight::Exhibit, published: true
47
47
  can :read, Spotlight::Page, published: true
48
48
  can :read, Spotlight::Search, published: true
@@ -5,6 +5,7 @@ module Spotlight
5
5
  # About pages
6
6
  class AboutPage < Spotlight::Page
7
7
  extend FriendlyId
8
+
8
9
  friendly_id :title, use: %i[slugged scoped finders history], scope: %i[exhibit locale] do |config|
9
10
  config.reserved_words&.concat(%w[update_all contacts])
10
11
  end
@@ -121,11 +121,7 @@ module Spotlight
121
121
  config.default_per_page = default_per_page if default_per_page
122
122
 
123
123
  config.view.embed!
124
- # This is blacklight-gallery's openseadragon partial
125
- unless config.view.embed.document_component
126
- config.view.embed.partials ||= ['openseadragon']
127
- config.view.embed.document_component = Spotlight::SolrDocumentLegacyEmbedComponent
128
- end
124
+ config.view.embed.document_component = Spotlight::SolrDocumentLegacyEmbedComponent unless config.view.embed.document_component
129
125
  config.view.embed.if = false
130
126
 
131
127
  # blacklight-gallery requires tile_source_field
@@ -336,11 +332,14 @@ module Spotlight
336
332
  unless config.show_fields.include? :exhibit_tags
337
333
  config.add_show_field :exhibit_tags, field: config.document_model.solr_field_for_tagger(exhibit),
338
334
  link_to_facet: true,
339
- separator_options: { words_connector: nil, two_words_connector: nil, last_word_connector: nil }
335
+ separator_options: { words_connector: nil, two_words_connector: nil, last_word_connector: nil },
336
+ label: I18n.t('spotlight.search.fields.exhibit_tags', default: 'Exhibit tags')
340
337
  end
341
338
 
342
339
  unless config.facet_fields.include? :exhibit_tags
343
- config.add_facet_field :exhibit_tags, field: config.document_model.solr_field_for_tagger(exhibit), limit: true
340
+ config.add_facet_field :exhibit_tags, field: config.document_model.solr_field_for_tagger(exhibit),
341
+ label: I18n.t('spotlight.search.fields.facet.exhibit_tags', default: 'Exhibit tags'),
342
+ limit: true
344
343
  end
345
344
  # rubocop:enable Style/GuardClause
346
345
  end
@@ -6,13 +6,14 @@ module Spotlight
6
6
  class Contact < ActiveRecord::Base
7
7
  belongs_to :exhibit, touch: true, optional: true
8
8
  scope :published, -> { where(show_in_sidebar: true) }
9
- default_scope { order('weight ASC') }
9
+ default_scope { order(:weight) }
10
10
  if Rails.version > '7.1'
11
11
  serialize :contact_info, type: Hash, coder: YAML
12
12
  else
13
13
  serialize :contact_info, Hash, coder: YAML
14
14
  end
15
15
  extend FriendlyId
16
+
16
17
  friendly_id :name, use: %i[slugged scoped finders], scope: :exhibit
17
18
 
18
19
  belongs_to :avatar, class_name: 'Spotlight::ContactImage', dependent: :destroy, optional: true
@@ -5,6 +5,7 @@ module Spotlight
5
5
  # Exhibit feedback contacts
6
6
  class ContactEmail < ActiveRecord::Base
7
7
  extend Devise::Models
8
+
8
9
  devise :confirmable
9
10
  belongs_to :exhibit
10
11
  validate :valid_email
@@ -12,6 +12,7 @@ module Spotlight
12
12
  belongs_to :exhibit, optional: true
13
13
 
14
14
  extend FriendlyId
15
+
15
16
  friendly_id :slug_candidates, use: %i[slugged scoped finders], scope: :exhibit
16
17
 
17
18
  scope :facetable, -> { where(field_type: Spotlight::Engine.config.custom_field_types.select { |_k, v| v[:facetable] }.keys) }
@@ -17,11 +17,12 @@ module Spotlight
17
17
 
18
18
  scope :published, -> { where(published: true) }
19
19
  scope :unpublished, -> { where(published: false) }
20
- scope :ordered_by_weight, -> { order('weight ASC') }
20
+ scope :ordered_by_weight, -> { order(:weight) }
21
21
 
22
22
  paginates_per 48
23
23
 
24
24
  extend FriendlyId
25
+
25
26
  friendly_id :title, use: %i[slugged finders] do |config|
26
27
  config.reserved_words&.concat(%w[site])
27
28
  end
@@ -87,8 +88,6 @@ module Spotlight
87
88
  accepts_nested_attributes_for :contact_emails, reject_if: proc { |attr| attr['email'].blank? }
88
89
  accepts_nested_attributes_for :roles, allow_destroy: true, reject_if: proc { |attr| attr['user_key'].blank? && attr['id'].blank? }
89
90
 
90
- before_save :sanitize_description, if: :description_changed?
91
-
92
91
  def main_about_page
93
92
  @main_about_page ||= about_pages.for_locale.published.first
94
93
  end
@@ -146,12 +145,6 @@ module Spotlight
146
145
  @available_locales ||= languages.pluck(:locale)
147
146
  end
148
147
 
149
- protected
150
-
151
- def sanitize_description
152
- self.description = ::Rails::Html::FullSanitizer.new.sanitize(description)
153
- end
154
-
155
148
  private
156
149
 
157
150
  def move_friendly_id_error_to_slug
@@ -5,6 +5,7 @@ module Spotlight
5
5
  # Feature pages
6
6
  class FeaturePage < Spotlight::Page
7
7
  extend FriendlyId
8
+
8
9
  friendly_id :title, use: %i[slugged scoped finders history], scope: %i[exhibit locale] do |config|
9
10
  config.reserved_words&.concat(%w[update_all])
10
11
  end
@@ -7,6 +7,7 @@ module Spotlight
7
7
  include Spotlight::Translatables
8
8
 
9
9
  extend FriendlyId
10
+
10
11
  friendly_id :title, use: %i[slugged scoped finders history], scope: [:exhibit]
11
12
  translates :title
12
13
 
@@ -14,7 +15,7 @@ module Spotlight
14
15
  belongs_to :exhibit
15
16
  has_many :group_members
16
17
  has_many :searches, through: :group_members, source: :member, source_type: 'Spotlight::Search'
17
- default_scope { order('weight ASC') }
18
+ default_scope { order(:weight) }
18
19
  scope :published, -> { where(published: true) }
19
20
  accepts_nested_attributes_for :group_members
20
21
  accepts_nested_attributes_for :searches
@@ -5,6 +5,7 @@ module Spotlight
5
5
  # Exhibit home page
6
6
  class HomePage < Spotlight::Page
7
7
  extend FriendlyId
8
+
8
9
  friendly_id :title, use: %i[slugged scoped finders history], scope: %i[exhibit locale] do |config|
9
10
  config.reserved_words&.concat(%w[update_all])
10
11
  end
@@ -3,7 +3,7 @@
3
3
  module Spotlight
4
4
  # Associate background jobs with records
5
5
  class JobTracker < ActiveRecord::Base
6
- scope :recent, -> { order('updated_at DESC').limit(5) }
6
+ scope :recent, -> { order(updated_at: :desc).limit(5) }
7
7
  scope :in_progress, -> { where.not(status: %w[completed failed]) }
8
8
  scope :completed, -> { where(status: %w[completed failed]) }
9
9
 
@@ -7,7 +7,7 @@ module Spotlight
7
7
  include Spotlight::Translatables
8
8
 
9
9
  belongs_to :exhibit, touch: true
10
- default_scope { order('weight ASC') }
10
+ default_scope { order(:weight) }
11
11
  scope :browse, -> { find_by(nav_type: 'browse') }
12
12
  scope :about, -> { find_by(nav_type: 'about') }
13
13
  scope :curated_features, -> { find_by(nav_type: 'curated_features') }
@@ -7,6 +7,7 @@ module Spotlight
7
7
  MAX_PAGES = Spotlight::Engine.config.max_pages
8
8
 
9
9
  extend FriendlyId
10
+
10
11
  # NOTE: This configuration also needs to be duplicated on the
11
12
  # STI models ({Spotlight::AboutPage}, {Spotlight::FeaturePage}, {Spotlight::HomePage})
12
13
  friendly_id :title, use: %i[slugged scoped finders history], scope: %i[exhibit locale], treat_reserved_as_conflict: true do |config|
@@ -27,10 +28,10 @@ module Spotlight
27
28
 
28
29
  validates :weight, allow_nil: true, numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: Spotlight::Page::MAX_PAGES }
29
30
 
30
- default_scope { order('weight ASC') }
31
+ default_scope { order(:weight) }
31
32
  scope :at_top_level, -> { where(parent_page_id: nil) }
32
33
  scope :published, -> { where(published: true) }
33
- scope :recent, -> { order('updated_at DESC').limit(10) }
34
+ scope :recent, -> { order(updated_at: :desc).limit(10) }
34
35
  scope :for_locale, ->(locale = I18n.locale) { unscope(where: :locale).where(locale:) }
35
36
  scope :for_default_locale, -> { for_locale(I18n.default_locale) }
36
37
 
@@ -151,33 +152,41 @@ module Spotlight
151
152
  translated_pages.for_locale(locale).first
152
153
  end
153
154
 
154
- def clone_for_locale(locale)
155
- dup.tap do |np|
156
- np.locale = locale
157
- np.default_locale_page = self
158
- np.published = false
159
- np.slug = slug
155
+ def clone_for_locale(clone_locale)
156
+ dup.tap do |new_page|
157
+ new_page.locale = clone_locale
158
+ new_page.default_locale_page = self
159
+ new_page.published = false
160
+ new_page.slug = slug
161
+ new_page.parent_page = parent_page_for(clone_locale) unless top_level_page?
162
+ next unless respond_to?(:child_pages)
160
163
 
161
- if !top_level_page? && (parent_translation = parent_page.translated_page_for(locale)).present?
162
- np.parent_page = parent_translation
164
+ child_pages.find_each do |default_locale_child_page|
165
+ default_locale_child_page.translated_page_for(clone_locale)&.update(parent_page: new_page)
163
166
  end
164
-
165
- child_pages.for_locale(locale).update(parent_page: np) if top_level_page? && respond_to?(:child_pages)
166
167
  end
167
168
  end
168
169
 
169
170
  private
170
171
 
171
- def update_translated_pages_weights_and_parent_page
172
- return unless locale.to_sym == I18n.default_locale
172
+ def default_locale?
173
+ locale.to_sym == I18n.default_locale
174
+ end
173
175
 
174
- if saved_change_to_parent_page_id?
175
- translated_pages.find_each do |translated_page|
176
- parent_translation = parent_page&.translated_page_for(translated_page.locale)
177
- translated_page.update(parent_page_id: parent_translation&.id)
178
- end
176
+ def parent_page_for(locale)
177
+ parent_page&.translated_page_for(locale)
178
+ end
179
+
180
+ def update_translated_pages_parents
181
+ translated_pages.find_each do |translated_page|
182
+ translated_page.update(parent_page_id: parent_page_for(translated_page.locale)&.id)
179
183
  end
184
+ end
185
+
186
+ def update_translated_pages_weights_and_parent_page
187
+ return unless default_locale?
180
188
 
189
+ update_translated_pages_parents if saved_change_to_parent_page_id?
181
190
  translated_pages.update(weight:) if saved_change_to_weight?
182
191
  end
183
192
  end
@@ -17,6 +17,7 @@ module Spotlight
17
17
  end)
18
18
 
19
19
  extend ActiveModel::Callbacks
20
+
20
21
  define_model_callbacks :index
21
22
 
22
23
  class_attribute :weight
@@ -172,132 +172,6 @@ module Spotlight
172
172
  def metadata_class
173
173
  Spotlight::Engine.config.iiif_metadata_class.call
174
174
  end
175
-
176
- ###
177
- # A simple class to map the metadata field
178
- # in a IIIF document to label/value pairs
179
- # This is intended to be overriden by an
180
- # application if a different metadata
181
- # strucure is used by the consumer
182
- class Metadata
183
- def initialize(manifest)
184
- @manifest = manifest
185
- end
186
-
187
- def to_solr
188
- metadata_hash.merge(manifest_level_metadata)
189
- end
190
-
191
- def label
192
- return unless manifest&.label
193
-
194
- Array(json_ld_value(manifest.label)).map { |v| html_sanitize(v) }.first
195
- end
196
-
197
- private
198
-
199
- attr_reader :manifest
200
-
201
- def metadata
202
- manifest&.metadata || []
203
- end
204
-
205
- def metadata_hash
206
- return {} if metadata.blank?
207
- return {} unless metadata.is_a?(Array)
208
-
209
- metadata.each_with_object({}) do |md, hash|
210
- next unless md['label'] && md['value']
211
-
212
- label = Array(json_ld_value(md['label'])).first
213
-
214
- hash[label] ||= []
215
- hash[label] += Array(json_ld_value(md['value'])).map { |v| html_sanitize(v) }
216
- end
217
- end
218
-
219
- def manifest_level_metadata
220
- manifest_fields.each_with_object({}) do |field, hash|
221
- next unless manifest.respond_to?(field) &&
222
- manifest.send(field).present?
223
-
224
- hash[field.capitalize] ||= []
225
- hash[field.capitalize] += Array(json_ld_value(manifest.send(field))).map { |v| html_sanitize(v) }
226
- end
227
- end
228
-
229
- def manifest_fields
230
- %w[attribution description license]
231
- end
232
-
233
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity
234
- def json_ld_value(value)
235
- case value
236
- # In the case where multiple values are supplied, clients must use the following algorithm to determine which values to display to the user.
237
- when Array
238
- # IIIF v2, multivalued monolingual, or multivalued multilingual values
239
-
240
- # If none of the values have a language associated with them, the client must display all of the values.
241
- if value.none? { |v| v.is_a?(Hash) && v.key?('@language') }
242
- value.map { |v| json_ld_value(v) }
243
- # 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
244
- # matches the language preference.
245
- elsif value.any? { |v| v.is_a?(Hash) && v['@language'] == default_json_ld_language }
246
- value.select { |v| v.is_a?(Hash) && v['@language'] == default_json_ld_language }.pluck('@value')
247
- # If all of the values have a language associated with them, and none match the language preference, the client must select a language
248
- # and display all of the values associated with that language.
249
- elsif value.all? { |v| v.is_a?(Hash) && v.key?('@language') }
250
- selected_json_ld_language = value.find { |v| v.is_a?(Hash) && v.key?('@language') }
251
-
252
- value.select { |v| v.is_a?(Hash) && v['@language'] == selected_json_ld_language['@language'] }
253
- .pluck('@value')
254
- # 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
255
- # that do not have a language associated with them.
256
- else
257
- value.select { |v| !v.is_a?(Hash) || !v.key?('@language') }.map { |v| json_ld_value(v) }
258
- end
259
- when Hash
260
- # IIIF v2 single-valued value
261
- if value.key? '@value'
262
- value['@value']
263
- # IIIF v3 multilingual(?), multivalued(?) values
264
- # If all of the values are associated with the none key, the client must display all of those values.
265
- elsif value.keys == ['none']
266
- value['none']
267
- # If any of the values have a language associated with them, the client must display all of the values associated with the language
268
- # that best matches the language preference.
269
- elsif value.key? default_json_ld_language
270
- value[default_json_ld_language]
271
- # If some of the values have a language associated with them, but none match the language preference, the client must display all
272
- # of the values that do not have a language associated with them.
273
- elsif value.key? 'none'
274
- value['none']
275
- # If all of the values have a language associated with them, and none match the language preference, the client must select a
276
- # language and display all of the values associated with that language.
277
- else
278
- value.values.first
279
- end
280
- else
281
- # plain old string/number/boolean
282
- value
283
- end
284
- end
285
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity
286
-
287
- def html_sanitize(value)
288
- return value unless value.is_a? String
289
-
290
- html_sanitizer.sanitize(value)
291
- end
292
-
293
- def html_sanitizer
294
- @html_sanitizer ||= Rails::Html::FullSanitizer.new
295
- end
296
-
297
- def default_json_ld_language
298
- Spotlight::Engine.config.default_json_ld_language
299
- end
300
- end
301
175
  end
302
176
  end
303
177
  end
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spotlight
4
+ module Resources
5
+ ###
6
+ # A simple class to map the metadata field
7
+ # in a IIIF document to label/value pairs
8
+ # This is intended to be overriden by an
9
+ # application if a different metadata
10
+ # strucure is used by the consumer
11
+ # override with Spotlight::Engine.config.iiif_metadata_class
12
+ class IiifManifestMetadata
13
+ def initialize(manifest)
14
+ @manifest = manifest
15
+ end
16
+
17
+ def to_solr
18
+ metadata_hash.merge(manifest_level_metadata)
19
+ end
20
+
21
+ def label
22
+ return unless manifest&.label
23
+
24
+ Array(json_ld_value(manifest.label)).map { |v| html_sanitize(v) }.first
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :manifest
30
+
31
+ def metadata
32
+ manifest&.metadata || []
33
+ end
34
+
35
+ def metadata_hash
36
+ return {} if metadata.blank?
37
+ return {} unless metadata.is_a?(Array)
38
+
39
+ metadata.each_with_object({}) do |md, hash|
40
+ next unless md['label'] && md['value']
41
+
42
+ label = Array(json_ld_value(md['label'])).first
43
+
44
+ hash[label] ||= []
45
+ hash[label] += Array(json_ld_value(md['value'])).map { |v| html_sanitize(v) }
46
+ end
47
+ end
48
+
49
+ def manifest_level_metadata
50
+ return manifest_level_metadata_v2 unless manifest_level_metadata_v2.empty?
51
+
52
+ manifest_level_metadata_v3
53
+ end
54
+
55
+ def manifest_level_metadata_v2
56
+ @manifest_level_metadata_v2 ||=
57
+ manifest2_fields.each_with_object({}) do |field, hash|
58
+ next unless manifest.respond_to?(field) && manifest.send(field).present?
59
+
60
+ hash[field.capitalize] ||= []
61
+ hash[field.capitalize] += Array(json_ld_value(manifest.send(field))).map { |v| html_sanitize(v) }
62
+ end
63
+ end
64
+
65
+ def manifest2_fields
66
+ %w[attribution description license]
67
+ end
68
+
69
+ def manifest_level_metadata_v3
70
+ manifest3_fields.each_with_object({}) do |field, hash|
71
+ manifest_key, solr_key = field
72
+ next unless manifest.respond_to?(manifest_key) && manifest.send(manifest_key).present?
73
+
74
+ hash[solr_key.capitalize] ||= []
75
+ hash[solr_key.capitalize] += Array(json_ld_value(manifest.send(manifest_key))).map { |v| html_sanitize(v) }
76
+ end.merge(attribution_v3)
77
+ end
78
+
79
+ def manifest3_fields
80
+ { rights: 'license', summary: 'description' }
81
+ end
82
+
83
+ def attribution_v3
84
+ rs = manifest['required_statement']
85
+ return {} if rs.blank?
86
+
87
+ key = json_ld_value(rs['label']).first
88
+ val = json_ld_value(rs['value'])
89
+ { key => val }
90
+ end
91
+
92
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity
93
+ def json_ld_value(value)
94
+ case value
95
+ # In the case where multiple values are supplied, clients must use the following algorithm to determine which values to display to the user.
96
+ when Array
97
+ # IIIF v2, multivalued monolingual, or multivalued multilingual values
98
+
99
+ # If none of the values have a language associated with them, the client must display all of the values.
100
+ if value.none? { |v| v.is_a?(Hash) && v.key?('@language') }
101
+ value.map { |v| json_ld_value(v) }
102
+ # 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
103
+ # matches the language preference.
104
+ elsif value.any? { |v| v.is_a?(Hash) && v['@language'] == default_json_ld_language }
105
+ value.select { |v| v.is_a?(Hash) && v['@language'] == default_json_ld_language }.pluck('@value')
106
+ # If all of the values have a language associated with them, and none match the language preference, the client must select a language
107
+ # and display all of the values associated with that language.
108
+ elsif value.all? { |v| v.is_a?(Hash) && v.key?('@language') }
109
+ selected_json_ld_language = value.find { |v| v.is_a?(Hash) && v.key?('@language') }
110
+
111
+ value.select { |v| v.is_a?(Hash) && v['@language'] == selected_json_ld_language['@language'] }
112
+ .pluck('@value')
113
+ # 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
114
+ # that do not have a language associated with them.
115
+ else
116
+ value.select { |v| !v.is_a?(Hash) || !v.key?('@language') }.map { |v| json_ld_value(v) }
117
+ end
118
+ when Hash
119
+ # IIIF v2 single-valued value
120
+ if value.key? '@value'
121
+ value['@value']
122
+ # IIIF v3 multilingual(?), multivalued(?) values
123
+ # If all of the values are associated with the none key, the client must display all of those values.
124
+ elsif value.keys == ['none']
125
+ value['none']
126
+ # If any of the values have a language associated with them, the client must display all of the values associated with the language
127
+ # that best matches the language preference.
128
+ elsif value.key? default_json_ld_language
129
+ value[default_json_ld_language]
130
+ # If some of the values have a language associated with them, but none match the language preference, the client must display all
131
+ # of the values that do not have a language associated with them.
132
+ elsif value.key? 'none'
133
+ value['none']
134
+ # If all of the values have a language associated with them, and none match the language preference, the client must select a
135
+ # language and display all of the values associated with that language.
136
+ else
137
+ value.values.first
138
+ end
139
+ else
140
+ # plain old string/number/boolean
141
+ value
142
+ end
143
+ end
144
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity
145
+
146
+ def html_sanitize(value)
147
+ return value unless value.is_a? String
148
+
149
+ html_sanitizer.sanitize(value)
150
+ end
151
+
152
+ def html_sanitizer
153
+ @html_sanitizer ||= Rails::Html::FullSanitizer.new
154
+ end
155
+
156
+ def default_json_ld_language
157
+ Spotlight::Engine.config.default_json_ld_language
158
+ end
159
+ end
160
+ end
161
+ end