blacklight-spotlight 5.0.1 → 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 (108) 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.html.erb +5 -0
  32. data/app/components/spotlight/uneditable_non_default_language_component.rb +25 -0
  33. data/app/controllers/spotlight/admin_users_controller.rb +11 -1
  34. data/app/controllers/spotlight/browse_controller.rb +2 -8
  35. data/app/controllers/spotlight/bulk_actions_controller.rb +1 -1
  36. data/app/controllers/spotlight/bulk_updates_controller.rb +22 -7
  37. data/app/controllers/spotlight/catalog_controller.rb +8 -16
  38. data/app/controllers/spotlight/dashboards_controller.rb +2 -6
  39. data/app/controllers/spotlight/exhibits_controller.rb +1 -0
  40. data/app/controllers/spotlight/home_pages_controller.rb +1 -1
  41. data/app/controllers/spotlight/searches_controller.rb +1 -1
  42. data/app/controllers/spotlight/solr_controller.rb +1 -0
  43. data/app/helpers/spotlight/main_app_helpers.rb +1 -5
  44. data/app/helpers/spotlight/rendering_helper.rb +4 -1
  45. data/app/javascript/spotlight/admin/blocks/pages_block.js +2 -0
  46. data/app/javascript/spotlight/user/carousel.js +32 -2
  47. data/app/jobs/spotlight/add_tags_job.rb +1 -0
  48. data/app/jobs/spotlight/add_uploads_from_csv.rb +1 -0
  49. data/app/jobs/spotlight/change_visibility_job.rb +1 -0
  50. data/app/jobs/spotlight/process_bulk_updates_csv_job.rb +1 -0
  51. data/app/jobs/spotlight/reindex_exhibit_job.rb +1 -0
  52. data/app/jobs/spotlight/reindex_job.rb +1 -0
  53. data/app/jobs/spotlight/remove_tags_job.rb +1 -0
  54. data/app/jobs/spotlight/rename_sidecar_field_job.rb +1 -0
  55. data/app/models/concerns/spotlight/user.rb +5 -0
  56. data/app/models/sir_trevor_rails/blocks/browse_block.rb +1 -1
  57. data/app/models/sir_trevor_rails/blocks/featured_pages_block.rb +1 -1
  58. data/app/models/sir_trevor_rails/blocks/solr_documents_block.rb +1 -0
  59. data/app/models/spotlight/ability.rb +1 -1
  60. data/app/models/spotlight/about_page.rb +1 -0
  61. data/app/models/spotlight/blacklight_configuration.rb +6 -7
  62. data/app/models/spotlight/contact.rb +2 -1
  63. data/app/models/spotlight/contact_email.rb +1 -0
  64. data/app/models/spotlight/custom_field.rb +1 -0
  65. data/app/models/spotlight/exhibit.rb +2 -9
  66. data/app/models/spotlight/feature_page.rb +1 -0
  67. data/app/models/spotlight/group.rb +2 -1
  68. data/app/models/spotlight/home_page.rb +1 -0
  69. data/app/models/spotlight/job_tracker.rb +1 -1
  70. data/app/models/spotlight/main_navigation.rb +1 -1
  71. data/app/models/spotlight/page.rb +35 -19
  72. data/app/models/spotlight/resource.rb +1 -0
  73. data/app/models/spotlight/resources/iiif_manifest.rb +0 -126
  74. data/app/models/spotlight/resources/iiif_manifest_metadata.rb +161 -0
  75. data/app/models/spotlight/resources/iiif_manifest_v3.rb +41 -0
  76. data/app/models/spotlight/resources/iiif_service.rb +25 -1
  77. data/app/models/spotlight/search.rb +2 -1
  78. data/app/services/spotlight/exhibit_import_export_service.rb +3 -1
  79. data/app/views/spotlight/accessibility/alt_text.html.erb +3 -0
  80. data/app/views/spotlight/admin_users/index.html.erb +16 -26
  81. data/app/views/spotlight/appearances/edit.html.erb +14 -9
  82. data/app/views/spotlight/catalog/_document_admin_table.html.erb +2 -6
  83. data/app/views/spotlight/catalog/edit.html.erb +1 -6
  84. data/app/views/spotlight/exhibits/_exhibit_card.html.erb +1 -1
  85. data/app/views/spotlight/exhibits/_form.html.erb +2 -2
  86. data/app/views/spotlight/metadata_configurations/edit.html.erb +47 -44
  87. data/app/views/spotlight/pages/_order_pages.html.erb +34 -29
  88. data/app/views/spotlight/pages/show.html.erb +4 -2
  89. data/app/views/spotlight/resources/csv_upload/_form.html.erb +2 -0
  90. data/app/views/spotlight/search_configurations/edit.html.erb +40 -34
  91. data/app/views/spotlight/searches/index.html.erb +4 -2
  92. data/app/views/spotlight/sir_trevor/blocks/_embedded_document.html.erb +2 -2
  93. data/app/views/spotlight/sir_trevor/blocks/_search_results_block.html.erb +2 -2
  94. data/app/views/spotlight/sir_trevor/blocks/_solr_documents_carousel_block.html.erb +5 -3
  95. data/app/views/spotlight/sir_trevor/blocks/_text_block.html.erb +1 -1
  96. data/app/views/spotlight/translations/_page.html.erb +1 -1
  97. data/config/initializers/devise_rails8_patch.rb +13 -0
  98. data/config/locales/spotlight.en.yml +39 -5
  99. data/config/routes.rb +5 -1
  100. data/lib/spotlight/engine.rb +10 -26
  101. data/lib/spotlight/version.rb +1 -1
  102. data/spec/fixtures/iiif_responses.rb +344 -0
  103. data/spec/support/features/test_features_helpers.rb +4 -6
  104. data/spec/support/stub_iiif_response.rb +1 -0
  105. data/spec/support/with_queue_adapter.rb +17 -0
  106. metadata +44 -31
  107. data/app/components/spotlight/blocks/heading_block_component.erb +0 -2
  108. data/app/components/spotlight/blocks/heading_block_component.rb +0 -36
@@ -1,7 +1,37 @@
1
1
  export default class {
2
2
  connect() {
3
3
  if ($.fn.carousel) {
4
- $('.carousel').carousel();
4
+ const $carousel = $('.carousel');
5
+
6
+ // updates the aria-describedby on the next and prev btns
7
+ const updateAriaDescribedBy = function ($carousel) {
8
+ const $activeItem = $carousel.find('.carousel-item.active');
9
+ const $items = $carousel.find('.carousel-item');
10
+ const curIndex = $items.index($activeItem);
11
+ const prevIndex = (curIndex - 1 + $items.length) % $items.length;
12
+ const nextIndex = (curIndex + 1) % $items.length;
13
+
14
+ const prevDataId = $items.eq(prevIndex).data('id');
15
+ const nextDataId = $items.eq(nextIndex).data('id');
16
+ if (prevDataId) {
17
+ $carousel.find('.carousel-control-prev').attr('aria-describedby', 'carousel-caption-' + prevDataId);
18
+ }
19
+ if (nextDataId) {
20
+ $carousel.find('.carousel-control-next').attr('aria-describedby', 'carousel-caption-' + nextDataId);
21
+ }
22
+ };
23
+
24
+ // on initial page load, set the aria-describedby on the btns for each carousel
25
+ $carousel.each(function () {
26
+ const $this = $(this);
27
+ $this.carousel();
28
+ updateAriaDescribedBy($this);
29
+ });
30
+
31
+ // on slide change
32
+ $carousel.on('slid.bs.carousel', function () {
33
+ updateAriaDescribedBy($(this));
34
+ });
5
35
  }
6
36
  }
7
- }
37
+ }
@@ -5,6 +5,7 @@ module Spotlight
5
5
  class AddTagsJob < 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
@@ -5,6 +5,7 @@ module Spotlight
5
5
  # Process a CSV upload into new Spotlight::Resource::Upload objects
6
6
  class AddUploadsFromCsv < Spotlight::ApplicationJob
7
7
  include Spotlight::JobTracking
8
+
8
9
  with_job_tracking(resource: ->(job) { job.arguments[1] })
9
10
 
10
11
  attr_reader :count, :errors
@@ -5,6 +5,7 @@ module Spotlight
5
5
  class ChangeVisibilityJob < 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/MethodLength
@@ -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
 
@@ -52,6 +53,13 @@ module Spotlight
52
53
  translated_page_for(I18n.locale)&.title || super
53
54
  end
54
55
 
56
+ # Returns the title of the page in the default locale regardless of the current locale.
57
+ def default_locale_title
58
+ return self[:title] if I18n.locale == I18n.default_locale || default_locale_page.blank?
59
+
60
+ default_locale_page[:title]
61
+ end
62
+
55
63
  def content_changed!
56
64
  @content = nil
57
65
  end
@@ -144,33 +152,41 @@ module Spotlight
144
152
  translated_pages.for_locale(locale).first
145
153
  end
146
154
 
147
- def clone_for_locale(locale)
148
- dup.tap do |np|
149
- np.locale = locale
150
- np.default_locale_page = self
151
- np.published = false
152
- 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)
153
163
 
154
- if !top_level_page? && (parent_translation = parent_page.translated_page_for(locale)).present?
155
- 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)
156
166
  end
157
-
158
- child_pages.for_locale(locale).update(parent_page: np) if top_level_page? && respond_to?(:child_pages)
159
167
  end
160
168
  end
161
169
 
162
170
  private
163
171
 
164
- def update_translated_pages_weights_and_parent_page
165
- return unless locale.to_sym == I18n.default_locale
172
+ def default_locale?
173
+ locale.to_sym == I18n.default_locale
174
+ end
166
175
 
167
- if saved_change_to_parent_page_id?
168
- translated_pages.find_each do |translated_page|
169
- parent_translation = parent_page&.translated_page_for(translated_page.locale)
170
- translated_page.update(parent_page_id: parent_translation&.id)
171
- 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)
172
183
  end
184
+ end
185
+
186
+ def update_translated_pages_weights_and_parent_page
187
+ return unless default_locale?
173
188
 
189
+ update_translated_pages_parents if saved_change_to_parent_page_id?
174
190
  translated_pages.update(weight:) if saved_change_to_weight?
175
191
  end
176
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