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.
- checksums.yaml +4 -4
- data/README.md +153 -48
- data/app/assets/javascripts/spotlight/spotlight.esm.js +33 -1
- data/app/assets/javascripts/spotlight/spotlight.esm.js.map +1 -1
- data/app/assets/javascripts/spotlight/spotlight.js +33 -1
- data/app/assets/javascripts/spotlight/spotlight.js.map +1 -1
- data/app/assets/stylesheets/spotlight/_admin_users.scss +28 -0
- data/app/assets/stylesheets/spotlight/_browse.scss +1 -1
- data/app/assets/stylesheets/spotlight/_featured_browse_categories_block.scss +1 -1
- data/app/assets/stylesheets/spotlight/_spotlight.scss +1 -0
- data/app/components/spotlight/admin_users/email_component.html.erb +5 -0
- data/app/components/spotlight/admin_users/email_component.rb +22 -0
- data/app/components/spotlight/admin_users/exhibit_roles_component.html.erb +28 -0
- data/app/components/spotlight/admin_users/exhibit_roles_component.rb +19 -0
- data/app/components/spotlight/admin_users/site_admin_component.html.erb +13 -0
- data/app/components/spotlight/admin_users/site_admin_component.rb +17 -0
- data/app/components/spotlight/analytics/aggregation_component.rb +1 -1
- data/app/components/spotlight/analytics/dashboard_component.rb +1 -1
- data/app/components/spotlight/breadcrumbs_component.rb +1 -1
- data/app/components/spotlight/bulk_action_component.rb +1 -1
- data/app/components/spotlight/edit_view_links_component.rb +1 -1
- data/app/components/spotlight/header_navigation_link_component.rb +1 -1
- data/app/components/spotlight/save_search_component.rb +1 -1
- data/app/components/spotlight/select_image_component.rb +1 -1
- data/app/components/spotlight/skip_link_component.rb +1 -1
- data/app/components/spotlight/solr_document_legacy_embed_component.rb +2 -2
- data/app/components/spotlight/tag_list_form_component.rb +1 -1
- data/app/components/spotlight/tag_selector_component.rb +1 -1
- data/app/components/spotlight/title_component.rb +1 -1
- data/app/components/spotlight/translations/subheading_component.rb +1 -1
- data/app/components/spotlight/uneditable_non_default_language_component.rb +1 -1
- data/app/controllers/spotlight/admin_users_controller.rb +11 -1
- data/app/controllers/spotlight/browse_controller.rb +2 -8
- data/app/controllers/spotlight/bulk_actions_controller.rb +1 -1
- data/app/controllers/spotlight/bulk_updates_controller.rb +22 -7
- data/app/controllers/spotlight/catalog_controller.rb +7 -15
- data/app/controllers/spotlight/dashboards_controller.rb +2 -6
- data/app/controllers/spotlight/exhibits_controller.rb +1 -0
- data/app/controllers/spotlight/home_pages_controller.rb +1 -1
- data/app/controllers/spotlight/searches_controller.rb +1 -1
- data/app/controllers/spotlight/solr_controller.rb +1 -0
- data/app/helpers/spotlight/main_app_helpers.rb +1 -5
- data/app/helpers/spotlight/rendering_helper.rb +4 -1
- data/app/javascript/spotlight/admin/blocks/pages_block.js +2 -0
- data/app/javascript/spotlight/user/carousel.js +32 -2
- data/app/jobs/spotlight/add_tags_job.rb +1 -0
- data/app/jobs/spotlight/add_uploads_from_csv.rb +1 -0
- data/app/jobs/spotlight/change_visibility_job.rb +1 -0
- data/app/jobs/spotlight/process_bulk_updates_csv_job.rb +1 -0
- data/app/jobs/spotlight/reindex_exhibit_job.rb +1 -0
- data/app/jobs/spotlight/reindex_job.rb +1 -0
- data/app/jobs/spotlight/remove_tags_job.rb +1 -0
- data/app/jobs/spotlight/rename_sidecar_field_job.rb +1 -0
- data/app/models/concerns/spotlight/user.rb +5 -0
- data/app/models/sir_trevor_rails/blocks/browse_block.rb +1 -1
- data/app/models/sir_trevor_rails/blocks/featured_pages_block.rb +1 -1
- data/app/models/sir_trevor_rails/blocks/solr_documents_block.rb +1 -0
- data/app/models/spotlight/ability.rb +1 -1
- data/app/models/spotlight/about_page.rb +1 -0
- data/app/models/spotlight/blacklight_configuration.rb +6 -7
- data/app/models/spotlight/contact.rb +2 -1
- data/app/models/spotlight/contact_email.rb +1 -0
- data/app/models/spotlight/custom_field.rb +1 -0
- data/app/models/spotlight/exhibit.rb +2 -9
- data/app/models/spotlight/feature_page.rb +1 -0
- data/app/models/spotlight/group.rb +2 -1
- data/app/models/spotlight/home_page.rb +1 -0
- data/app/models/spotlight/job_tracker.rb +1 -1
- data/app/models/spotlight/main_navigation.rb +1 -1
- data/app/models/spotlight/page.rb +28 -19
- data/app/models/spotlight/resource.rb +1 -0
- data/app/models/spotlight/resources/iiif_manifest.rb +0 -126
- data/app/models/spotlight/resources/iiif_manifest_metadata.rb +161 -0
- data/app/models/spotlight/resources/iiif_manifest_v3.rb +41 -0
- data/app/models/spotlight/resources/iiif_service.rb +25 -1
- data/app/models/spotlight/search.rb +2 -1
- data/app/services/spotlight/exhibit_import_export_service.rb +3 -1
- data/app/views/spotlight/admin_users/index.html.erb +16 -26
- data/app/views/spotlight/catalog/_document_admin_table.html.erb +2 -6
- data/app/views/spotlight/catalog/edit.html.erb +1 -6
- data/app/views/spotlight/exhibits/_exhibit_card.html.erb +1 -1
- data/app/views/spotlight/pages/_order_pages.html.erb +1 -1
- data/app/views/spotlight/pages/show.html.erb +4 -2
- data/app/views/spotlight/resources/csv_upload/_form.html.erb +2 -0
- data/app/views/spotlight/sir_trevor/blocks/_embedded_document.html.erb +2 -2
- data/app/views/spotlight/sir_trevor/blocks/_search_results_block.html.erb +2 -2
- data/app/views/spotlight/sir_trevor/blocks/_solr_documents_carousel_block.html.erb +5 -3
- data/app/views/spotlight/sir_trevor/blocks/_text_block.html.erb +1 -1
- data/config/initializers/devise_rails8_patch.rb +13 -0
- data/config/locales/spotlight.en.yml +34 -5
- data/config/routes.rb +5 -1
- data/lib/spotlight/engine.rb +10 -26
- data/lib/spotlight/version.rb +1 -1
- data/spec/fixtures/iiif_responses.rb +344 -0
- data/spec/support/features/test_features_helpers.rb +4 -6
- data/spec/support/stub_iiif_response.rb +1 -0
- data/spec/support/with_queue_adapter.rb +17 -0
- metadata +42 -29
@@ -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
|
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
|
@@ -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
|
@@ -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
|
-
|
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),
|
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(
|
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
|
@@ -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(
|
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
|
@@ -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(
|
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
|
@@ -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(
|
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(
|
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(
|
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(
|
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(
|
155
|
-
dup.tap do |
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
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
|
-
|
162
|
-
|
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
|
172
|
-
|
172
|
+
def default_locale?
|
173
|
+
locale.to_sym == I18n.default_locale
|
174
|
+
end
|
173
175
|
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
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
|
@@ -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
|