iiif_print 3.1.2 → 3.2.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 512bd4a70c6b11e94a4d82fb786264bb124c4ca38ca33549d34e3bcb6d0e37bc
4
- data.tar.gz: 0b363334d97c0a97bc5b4ed8685c9be41f9ab7124e027af6a1334f2f53f6e86c
3
+ metadata.gz: 2ccac310b7d9a84e367cec7ea055b4b2b9af2d9817cbef9ccfaf3c68d467ce40
4
+ data.tar.gz: 621bb63d8990014bd1a4842ef6897628386515e6a3bcc76f0d212ba59a318f0e
5
5
  SHA512:
6
- metadata.gz: 4e31e9b19c7ca850977bc1a04c912a98d49116d1b780e7ecdde7be43abbe447e44eceb20bc18be0d038887257b0432fc15ae79d20609ec9dfdea75e8946a82cc
7
- data.tar.gz: b9639133a35990cabfe146fb13866e9a948e418a48a36962de3d10a70b52794f2466de9e789d851b778d91fa165659f4b90560759aee0b421313d9326ef2329d
6
+ metadata.gz: 687953bee3a35e17c5ed41e34e29324c7166736397356131f40f3e56b7f623004f9c07a8ff55019347288811740e8f3064dd5673f71357e4d6f153a79dcf8bb6
7
+ data.tar.gz: cdb91f7cbe9a6fd603f3ad5e38058dac9e8da93f1d10bd932e0a843deae24cb830f090c533b9e7bfb4dbdd9eb1fcd8509377d5bc34db41df92cd5321e19ee9bc
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: /app/samvera/hyrax-webapp
3
3
  specs:
4
- iiif_print (3.1.2)
4
+ iiif_print (3.2.1)
5
5
  blacklight_iiif_search (>= 1.0, < 3.0)
6
6
  derivative-rodeo (~> 0.5)
7
7
  hyrax (>= 2.5, < 6)
@@ -65,7 +65,7 @@ GEM
65
65
  activemodel (= 5.2.8.1)
66
66
  activesupport (= 5.2.8.1)
67
67
  arel (>= 9.0)
68
- activerecord-import (2.2.0)
68
+ activerecord-import (2.3.0)
69
69
  activerecord (>= 4.2)
70
70
  activestorage (5.2.8.1)
71
71
  actionpack (= 5.2.8.1)
@@ -80,19 +80,19 @@ GEM
80
80
  public_suffix (>= 2.0.2, < 8.0)
81
81
  almond-rails (0.3.0)
82
82
  rails (>= 4.2)
83
- anonymous_loader (0.1.2)
84
- version_gem (~> 1.1, >= 1.1.13)
83
+ anonymous_loader (0.1.3)
84
+ version_gem (~> 1.1, >= 1.1.14)
85
85
  arel (9.0.0)
86
86
  ast (2.4.2)
87
- auth-sanitizer (0.2.2)
88
- version_gem (~> 1.1, >= 1.1.10)
87
+ auth-sanitizer (0.2.3)
88
+ version_gem (~> 1.1, >= 1.1.14)
89
89
  autoprefixer-rails (10.4.16.0)
90
90
  execjs (~> 2)
91
91
  awesome_nested_set (3.9.0)
92
92
  activerecord (>= 4.0.0, < 8.2)
93
93
  aws-eventstream (1.4.0)
94
- aws-partitions (1.1262.0)
95
- aws-sdk-core (3.252.0)
94
+ aws-partitions (1.1293.0)
95
+ aws-sdk-core (3.254.1)
96
96
  aws-eventstream (~> 1, >= 1.3.0)
97
97
  aws-partitions (~> 1, >= 1.992.0)
98
98
  aws-sigv4 (~> 1.9)
@@ -100,15 +100,15 @@ GEM
100
100
  bigdecimal
101
101
  jmespath (~> 1, >= 1.6.1)
102
102
  logger
103
- aws-sdk-kms (1.129.0)
104
- aws-sdk-core (~> 3, >= 3.248.0)
103
+ aws-sdk-kms (1.130.0)
104
+ aws-sdk-core (~> 3, >= 3.254.0)
105
105
  aws-sigv4 (~> 1.5)
106
- aws-sdk-s3 (1.226.0)
107
- aws-sdk-core (~> 3, >= 3.248.0)
106
+ aws-sdk-s3 (1.230.0)
107
+ aws-sdk-core (~> 3, >= 3.254.1)
108
108
  aws-sdk-kms (~> 1)
109
109
  aws-sigv4 (~> 1.5)
110
- aws-sdk-sqs (1.116.0)
111
- aws-sdk-core (~> 3, >= 3.248.0)
110
+ aws-sdk-sqs (1.117.0)
111
+ aws-sdk-core (~> 3, >= 3.254.0)
112
112
  aws-sigv4 (~> 1.5)
113
113
  aws-sigv4 (1.12.1)
114
114
  aws-eventstream (~> 1, >= 1.0.2)
@@ -122,7 +122,7 @@ GEM
122
122
  i18n
123
123
  bcrypt (3.1.21)
124
124
  benchmark (0.3.0)
125
- bigdecimal (4.1.2)
125
+ bigdecimal (4.1.3)
126
126
  bindex (0.8.1)
127
127
  bixby (5.0.2)
128
128
  rubocop (= 1.28.2)
@@ -192,7 +192,7 @@ GEM
192
192
  thor (>= 0.19.4, < 2.0)
193
193
  tins (~> 1.6)
194
194
  crass (1.0.6)
195
- csv (3.3.5)
195
+ csv (3.3.6)
196
196
  database_cleaner (1.99.0)
197
197
  date (3.3.4)
198
198
  declarative (0.0.20)
@@ -353,8 +353,8 @@ GEM
353
353
  logger
354
354
  hiredis (0.6.3)
355
355
  htmlentities (4.4.2)
356
- http_logger (1.0.2)
357
- httparty (0.24.2)
356
+ http_logger (1.0.3)
357
+ httparty (0.24.3)
358
358
  csv
359
359
  mini_mime (>= 1.0.0)
360
360
  multi_xml (>= 0.5.2)
@@ -467,7 +467,7 @@ GEM
467
467
  activesupport (>= 3.2.18)
468
468
  faraday (>= 0.9)
469
469
  json
470
- iiif_manifest (1.6.0)
470
+ iiif_manifest (1.7.0)
471
471
  activesupport (>= 4)
472
472
  jaro_winkler (1.5.6)
473
473
  jbuilder (2.12.0)
@@ -497,7 +497,7 @@ GEM
497
497
  json-ld-preloaded (3.1.6)
498
498
  json-ld (~> 3.1)
499
499
  rdf (~> 3.1)
500
- json-schema (6.2.0)
500
+ json-schema (6.2.1)
501
501
  addressable (~> 2.8)
502
502
  bigdecimal (>= 3.1, < 5)
503
503
  jwt (2.10.3)
@@ -589,7 +589,7 @@ GEM
589
589
  mime-types (3.7.0)
590
590
  logger
591
591
  mime-types-data (~> 3.2025, >= 3.2025.0507)
592
- mime-types-data (3.2026.0414)
592
+ mime-types-data (3.2026.1006)
593
593
  mini_magick (4.13.2)
594
594
  mini_mime (1.1.5)
595
595
  mini_portile2 (2.8.5)
@@ -621,16 +621,17 @@ GEM
621
621
  nokogiri (1.15.5)
622
622
  mini_portile2 (~> 2.8.2)
623
623
  racc (~> 1.4)
624
- oauth (1.1.7)
625
- auth-sanitizer (~> 0.2, >= 0.2.1)
624
+ oauth (1.1.8)
625
+ anonymous_loader (~> 0.1, >= 0.1.3)
626
+ auth-sanitizer (~> 0.2, >= 0.2.3)
626
627
  base64 (~> 0.1)
627
- oauth-tty (~> 1.0, >= 1.0.10)
628
- snaky_hash (~> 2.0, >= 2.0.5)
629
- version_gem (~> 1.1, >= 1.1.12)
630
- oauth-tty (1.0.12)
631
- anonymous_loader (~> 0.1, >= 0.1.1)
632
- auth-sanitizer (~> 0.2, >= 0.2.2)
633
- version_gem (~> 1.1, >= 1.1.12)
628
+ oauth-tty (~> 1.0, >= 1.0.13)
629
+ snaky_hash (~> 2.0, >= 2.0.7)
630
+ version_gem (~> 1.1, >= 1.1.14)
631
+ oauth-tty (1.0.13)
632
+ anonymous_loader (~> 0.1, >= 0.1.3)
633
+ auth-sanitizer (~> 0.2, >= 0.2.3)
634
+ version_gem (~> 1.1, >= 1.1.14)
634
635
  oauth2 (1.4.11)
635
636
  faraday (>= 0.17.3, < 3.0)
636
637
  jwt (>= 1.0, < 3.0)
@@ -890,9 +891,9 @@ GEM
890
891
  simplecov-html (~> 0.10.0)
891
892
  simplecov-html (0.10.2)
892
893
  slop (4.10.1)
893
- snaky_hash (2.0.6)
894
+ snaky_hash (2.0.7)
894
895
  hashie (>= 0.1.0, < 6)
895
- version_gem (>= 1.1.8, < 3)
896
+ version_gem (~> 1.1, >= 1.1.14)
896
897
  solargraph (0.48.0)
897
898
  backport (~> 1.2)
898
899
  benchmark
@@ -943,7 +944,7 @@ GEM
943
944
  matrix (~> 0.4)
944
945
  rdf (~> 3.2)
945
946
  sync (0.5.0)
946
- temple (0.10.4)
947
+ temple (0.10.7)
947
948
  term-ansicolor (1.7.1)
948
949
  tins (~> 1.0)
949
950
  terminal-table (4.0.0)
@@ -971,7 +972,7 @@ GEM
971
972
  uglifier (4.2.0)
972
973
  execjs (>= 0.3.0, < 3)
973
974
  unicode-display_width (2.5.0)
974
- unicode-types (1.11.0)
975
+ unicode-types (1.12.0)
975
976
  valkyrie (2.2.0)
976
977
  activemodel
977
978
  activesupport
@@ -987,7 +988,7 @@ GEM
987
988
  rdf-vocab
988
989
  reform (~> 2.2)
989
990
  reform-rails
990
- version_gem (1.1.13)
991
+ version_gem (1.1.15)
991
992
  warden (1.2.9)
992
993
  rack (>= 2.0.9)
993
994
  web-console (3.7.0)
@@ -1,7 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module IiifPrint
4
+ # @deprecated IiifPrint has not applied this since 3.0; Hyrax::WorkShowPresenter#iiif_viewer? now counts
5
+ # images on child works. Removed in IiifPrint 4.0.
4
6
  module WorkShowPresenterDecorator
7
+ DEPRECATION_MESSAGE = "IiifPrint::WorkShowPresenterDecorator is deprecated and will be removed in IiifPrint 4.0; " \
8
+ "Hyrax::WorkShowPresenter#iiif_viewer? now counts images on child works."
9
+
10
+ def self.prepended(_base)
11
+ Deprecation.warn(self, DEPRECATION_MESSAGE)
12
+ end
13
+
14
+ def self.included(_base)
15
+ Deprecation.warn(self, DEPRECATION_MESSAGE)
16
+ end
17
+
5
18
  delegate :member_ids, to: :solr_document
6
19
  alias file_set_ids member_ids
7
20
 
@@ -65,14 +65,19 @@ module IiifPrint
65
65
  end
66
66
 
67
67
  def sanitize_v3(hash:, presenter:, solr_doc_hits:)
68
- hash['label']['none'].map! { |text| CGI.unescapeHTML(sanitize_value(text)) } if hash.key('label')
68
+ sanitize_v3_label(hash)
69
69
  hash['items'].each do |canvas|
70
- canvas['label']['none'].map! { |text| CGI.unescapeHTML(sanitize_value(text)) }
70
+ sanitize_v3_label(canvas)
71
71
  apply_metadata_to_canvas(canvas: canvas, presenter: presenter, solr_doc_hits: solr_doc_hits)
72
72
  end
73
73
  hash
74
74
  end
75
75
 
76
+ # A v3 label is a language map: { 'none' => [...] }, { 'en' => [...] }, ...
77
+ def sanitize_v3_label(node)
78
+ node['label']&.each_value { |texts| texts.map! { |text| CGI.unescapeHTML(sanitize_value(text)) } }
79
+ end
80
+
76
81
  def apply_metadata_to_canvas(canvas:, presenter:, solr_doc_hits:)
77
82
  return if @child_works.empty?
78
83
 
@@ -152,6 +157,6 @@ module IiifPrint
152
157
  results
153
158
  end
154
159
  end
155
- # rubocop:enable Metrics/ClassLength
160
+ # rubocop:enable Metrics/ModuleLength
156
161
  end
157
162
  Hyrax::ManifestBuilderService.prepend(IiifPrint::ManifestBuilderServiceDecorator)
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # OVERRIDE Hyrax 5.0.0rc2 to add file_set.iiif_print_conditionally_destroy_spawned_children with user args
3
+ # OVERRIDE Hyrax 5.3.1
4
+ # - add file_set.iiif_print_conditionally_destroy_spawned_children with user args
5
+ # - skip file_set.remove_from_work when WorkDestroy can repair the work itself (samvera/hyrax#7682)
6
+ # - fail without a user, as Hyrax does from samvera/hyrax#7682
4
7
 
5
8
  module Hyrax
6
9
  module Transactions
@@ -16,19 +19,39 @@ module Hyrax
16
19
  def call(resource, user: nil)
17
20
  return Failure(:resource_not_persisted) unless resource.persisted?
18
21
 
22
+ # OVERRIDE: iiif_print step args, and remove_from_work only when needed
23
+ destroy = iiif_print_file_set_destroy(user: user)
19
24
  @query_service.custom_queries.find_child_file_sets(resource: resource).each do |file_set|
20
- return Failure[:failed_to_delete_file_set, file_set] unless
21
- Hyrax::Transactions::Container['file_set.destroy']
22
- .with_step_args('file_set.remove_from_work' => { user: user },
23
- 'file_set.delete' => { user: user },
24
- 'file_set.iiif_print_conditionally_destroy_spawned_children' => { user: user })
25
- .call(file_set).success?
25
+ # OVERRIDE: require a user
26
+ return Failure[:failed_to_delete_file_set, file_set] unless user && destroy.call(file_set).success?
26
27
  rescue ::Ldp::Gone
27
28
  nil
28
29
  end
29
30
 
30
31
  Success(resource)
31
32
  end
33
+
34
+ private
35
+
36
+ def iiif_print_file_set_destroy(user:)
37
+ transaction = Hyrax::Transactions::Container['file_set.destroy'].dup
38
+ step_args = { 'file_set.delete' => { user: user },
39
+ 'file_set.iiif_print_conditionally_destroy_spawned_children' => { user: user } }
40
+ if work_destroy_repairs_members?
41
+ # The work is about to be deleted; removing each file set from it would only re-save
42
+ # and re-index it, and queue update events for an object that will be gone.
43
+ transaction.steps -= ['file_set.remove_from_work']
44
+ else
45
+ step_args['file_set.remove_from_work'] = { user: user }
46
+ end
47
+ transaction.with_step_args(**step_args)
48
+ end
49
+
50
+ # Without this repair, a failed later step would leave the surviving work listing
51
+ # file sets that no longer exist, so they must be removed from it as they go.
52
+ def work_destroy_repairs_members?
53
+ Hyrax::Transactions::WorkDestroy.private_method_defined?(:remove_missing_members)
54
+ end
32
55
  end
33
56
  end
34
57
  end
@@ -24,12 +24,10 @@ module IiifPrint
24
24
 
25
25
  # rubocop:enable Naming/PredicateName
26
26
  def show_parents_only(solr_parameters)
27
- query = if blacklight_params["include_child_works"] == 'true'
28
- IiifPrint.solr_construct_query(is_child_bsi: 'true')
29
- else
30
- IiifPrint.solr_construct_query(is_child_bsi: nil)
31
- end
32
- solr_parameters[:fq] += [query]
27
+ return if blacklight_params["include_child_works"] == 'true'
28
+
29
+ # Only child works are left out; a work indexed with is_child_bsi false is not a child.
30
+ solr_parameters[:fq] += ['-is_child_bsi:true']
33
31
  end
34
32
  end
35
33
  end
@@ -289,9 +289,11 @@ module IiifPrint
289
289
  ##
290
290
  # This is the default sorter for the metadata. It will sort by the order of the keys specificied.
291
291
  # By default, this is turned off as it returns nil. If you want to turn it on, you can set this
292
- # this to an array of symbols the properties on the work.
292
+ # to an array of symbols for the properties on the work. Properties not listed follow the listed
293
+ # ones, or take the place of `:remaining` when it is listed.
293
294
  #
294
295
  # @example [:title, :description, :date_created]
296
+ # @example [:title, :collection, :remaining, :rights_statement]
295
297
  # @return [Array<Symbol>]
296
298
  def iiif_metadata_field_presentation_order
297
299
  @iiif_metadata_field_presentation_order || nil
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IiifPrint
4
+ ##
5
+ # Per-request state. Rails resets it around every request and job, where RequestStore was only cleared by its
6
+ # Rack middleware and so lived as long as a job's thread.
7
+ class Current < ActiveSupport::CurrentAttributes
8
+ attribute :anonymous_ability, :presenters, :view_definitions
9
+ end
10
+ end
@@ -42,11 +42,10 @@ module IiifPrint
42
42
  # To configure specific services, inject each service, in desired order
43
43
  # to IiifPrint::PluggableDerivativeService.plugins array.
44
44
 
45
- Hyrax::DerivativeService.services.unshift(
46
- IiifPrint::PluggableDerivativeService
47
- )
45
+ derivative_services = Hyrax::DerivativeService.services
46
+ derivative_services.unshift(IiifPrint::PluggableDerivativeService) unless derivative_services.include?(IiifPrint::PluggableDerivativeService)
48
47
 
49
- Hyrax.publisher.subscribe(IiifPrint::Listener.new) if Hyrax.respond_to?(:publisher)
48
+ IiifPrint.subscribe_listener if Hyrax.respond_to?(:publisher)
50
49
 
51
50
  # Load locales early so decorators can use them during initialization
52
51
  I18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.yml'),
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IiifPrint
4
+ module Flexibility
5
+ ##
6
+ # A compound field's entries, one paragraph each, rendered the way its show page renders them.
7
+ class CompoundValues
8
+ # @return [Boolean] whether the entry has no value to show
9
+ def self.blank_entry?(entry)
10
+ Array(entry.try(:values) || entry).none?(&:present?)
11
+ end
12
+
13
+ # @param field_name [Symbol]
14
+ # @param presenter [#solr_document] the record's show presenter
15
+ # @param sanitize [#call] makes rendered html IIIF-safe
16
+ def initialize(field_name, presenter:, sanitize:)
17
+ @field_name = field_name
18
+ @presenter = presenter
19
+ @sanitize = sanitize
20
+ end
21
+
22
+ # @param entries [Array<Hash>]
23
+ # @return [String] html
24
+ def to_html(entries)
25
+ subproperties = compound_subproperties
26
+ entries.reject { |entry| self.class.blank_entry?(entry) }
27
+ .map { |entry| "<p>#{entry_html(entry, subproperties)}</p>" }.join
28
+ end
29
+
30
+ private
31
+
32
+ # As Hyrax's compound renderer, and any decorator the app gives it, renders the entry, with a line break where
33
+ # each innermost block ended, since IIIF allows no blocks.
34
+ def entry_html(entry, subproperties)
35
+ return plain_html(entry) unless defined?(Hyrax::Renderers::CompoundAttributeRenderer)
36
+
37
+ renderer = Hyrax::Renderers::CompoundAttributeRenderer.new(@field_name, [entry], subproperties: subproperties)
38
+ fragment = Nokogiri::HTML.fragment(renderer.render_value)
39
+ fragment.css('div').each { |div| div.add_next_sibling('<br>') unless div.at_css('div') }
40
+ @sanitize.call(fragment.to_html).gsub(/\s*<br>\s*/, '<br>').sub(/(<br>)+\z/, '')
41
+ end
42
+
43
+ def plain_html(entry)
44
+ Array(entry.try(:values) || entry).reject(&:blank?).map { |value| ERB::Util.h(value) }.join('<br>')
45
+ end
46
+
47
+ # The lookup Hyrax's PresentsAttributes#compound_subproperties_for makes, which is private.
48
+ def compound_subproperties
49
+ return unless defined?(Hyrax::CompoundSchema) && @presenter.respond_to?(:solr_document)
50
+
51
+ Hyrax::CompoundSchema.for_solr_document(@presenter.solr_document).definition_for(@field_name)&.fetch(:subproperties, nil)
52
+ rescue StandardError => e
53
+ Rails.logger.debug("IiifPrint could not read #{@field_name}'s compound sub-properties: #{e.message}")
54
+ nil
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IiifPrint
4
+ module Flexibility
5
+ ##
6
+ # The fields a flexible record's show page lists, from its M3 profile: those an anonymous visitor may see and
7
+ # its show presenter can render, labeled and ordered as the profile says, plus its collections. Each carries
8
+ # its view options, which {IiifPrint::Flexibility::Values} renders it by.
9
+ class Fields
10
+ # The show page leads with the title and description, then the work's collections.
11
+ DEFAULT_ORDER = %i[title description abstract collection remaining].freeze
12
+
13
+ # @param document [SolrDocument]
14
+ # @param ability [Ability]
15
+ # @param sort_order [Array<Symbol>] see {IiifPrint::Configuration#iiif_metadata_field_presentation_order};
16
+ # {DEFAULT_ORDER} when unset
17
+ def initialize(document, ability, sort_order: IiifPrint.config.iiif_metadata_field_presentation_order)
18
+ @document = document
19
+ @ability = ability
20
+ @sort_order = sort_order || DEFAULT_ORDER
21
+ end
22
+
23
+ # @return [Array<IiifPrint::Field>]
24
+ def to_a
25
+ presenter = Flexibility.presenter_for(@document, @ability)
26
+ definitions = Flexibility.view_definitions_for(@document)
27
+ fields = definitions.filter_map do |name, options|
28
+ options = options.to_h.symbolize_keys
29
+ term = (options[:render_term] || name).to_sym
30
+ next unless public?(name, options) && (term == :collection || presenter.respond_to?(term))
31
+
32
+ Field.new(name: term, label: label(term, options), options: options)
33
+ end
34
+ fields << collection_field unless collection_defined?(definitions)
35
+ IiifPrint.sort_af_fields!(fields, sort_order: @sort_order)
36
+ end
37
+
38
+ private
39
+
40
+ def collection_field
41
+ Field.new(name: :collection, label: Hyrax::Renderers::AttributeRenderer.new(:collection, nil).label)
42
+ end
43
+
44
+ def collection_defined?(definitions)
45
+ definitions.any? { |name, options| [name.to_s, options.to_h.symbolize_keys[:render_term].to_s].include?('collection') }
46
+ end
47
+
48
+ # The show page lists an admin note for editors only, whatever the profile says.
49
+ def public?(name, options)
50
+ return false if name.to_s == 'admin_note'
51
+
52
+ !(options[:admin_only] || options[:editor_only] || options[:show_page] == false)
53
+ end
54
+
55
+ # Mirrors Hyrax's AttributesHelper#conform_options, so a field is labeled as its show page labels it.
56
+ def label(name, options)
57
+ labels = options[:display_label] || {}
58
+ label = (labels[I18n.locale.to_s] || labels['default'] || name).to_s
59
+ translated = I18n.t(label, default: label) if label.include?('.')
60
+ return translated if translated && translated != label
61
+
62
+ I18n.t(:"blacklight.search.fields.index.#{label}",
63
+ default: [:"blacklight.search.fields.show.#{label}", :"blacklight.search.fields.#{label}", label.humanize])
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IiifPrint
4
+ module Flexibility
5
+ ##
6
+ # A flexible field's values, read from the show presenter and rendered the way its profile's `render_as` renders
7
+ # them on the show page.
8
+ class Values
9
+ # What IIIF Presentation 3.0 allows in a metadata value (section 4.5).
10
+ IIIF_TAGS = %w[a b br i img p small span sub sup].freeze
11
+ IIIF_ATTRIBUTES = %w[href src alt].freeze
12
+ RIGHTS_SERVICES = %w[rights_statement license].freeze
13
+
14
+ # @param work [SolrDocument, SolrHit]
15
+ # @param presenter [#display_values_for] the record's show presenter
16
+ # @param base_url [String]
17
+ # @param autolink [#call] turns URLs in plain text into links
18
+ def initialize(work:, presenter:, base_url:, autolink:)
19
+ @work = work
20
+ @presenter = presenter
21
+ @base_url = base_url
22
+ @autolink = autolink
23
+ end
24
+
25
+ # @return [Array] the field's values as stored, blanks and all-blank compound entries dropped
26
+ def raw(field_name)
27
+ Array(@presenter.try(field_name)).reject { |value| CompoundValues.blank_entry?(value) }
28
+ end
29
+
30
+ # @return [Array<String>] the field's values as html
31
+ def rendered(field_name, options)
32
+ options ||= {}
33
+ values = Array(@presenter.try(field_name)).reject(&:blank?)
34
+ return [compound_values(field_name, values)] if options[:render_as].to_s == 'compound'
35
+
36
+ labels = labels_for(field_name, values)
37
+ values.map { |value| value(field_name, value.to_s, labels[value.to_s], options) }
38
+ end
39
+
40
+ private
41
+
42
+ # Keyed by value, as the show page's renderers get them in `options[:labels]`, and like them treating a blank
43
+ # label, or one equal to its value, as none.
44
+ def labels_for(field_name, values)
45
+ labels = @presenter.try(:controlled_labels_for, field_name, values) || display_labels(field_name, values)
46
+ labels.to_h.transform_keys(&:to_s).reject { |value, label| label.blank? || label.to_s == value }
47
+ end
48
+
49
+ # TODO: drop once Hyrax's PresentsAttributes#controlled_labels_for is public
50
+ def display_labels(field_name, values)
51
+ display = Array(@presenter.try(:display_values_for, field_name)).reject(&:blank?)
52
+ return {} unless display.size == values.size
53
+
54
+ values.map(&:to_s).zip(display.map(&:to_s)).to_h
55
+ end
56
+
57
+ def value(field_name, value, label, options)
58
+ return search_value(options[:search_field] || field_name, value, label, options[:render_as].to_s) if %w[faceted linked].include?(options[:render_as].to_s)
59
+
60
+ label = rights_label(field_name, options[:render_as].to_s, value, label)
61
+ case options[:render_as].to_s
62
+ when 'redirects_label' then link(File.join(@base_url, value), File.join(@base_url, value))
63
+ when 'html' then sanitize_html(value)
64
+ when 'date' then ERB::Util.h(formatted_date(value))
65
+ else labeled_value(value, label)
66
+ end
67
+ end
68
+
69
+ # Like Hyrax's faceted and linked renderers: a controlled term links by its label, through the label facet
70
+ # when the catalog registers one.
71
+ def search_value(field, value, label, render_as)
72
+ return search_link({ search_field: field, q: label || value }, label || value) if render_as == 'linked'
73
+
74
+ by_label = label.present? && label_facet?(field)
75
+ search_link({ "f[#{field}#{'_label' if by_label}_sim][]": by_label ? label : value }, label || value)
76
+ end
77
+
78
+ def label_facet?(field)
79
+ config = 'CatalogController'.safe_constantize.try(:blacklight_config)
80
+ config.respond_to?(:facet_fields) && config.facet_fields.key?("#{field}_label_sim")
81
+ end
82
+
83
+ def search_link(params, text)
84
+ params = params.merge(include_child_works: true) if @work["is_child_bsi"] == true
85
+ link(File.join(@base_url, Rails.application.routes.url_helpers.search_catalog_path(**params, locale: I18n.locale)), text)
86
+ end
87
+
88
+ def labeled_value(value, label)
89
+ return @autolink.call([ERB::Util.h(value)]).first if label.blank?
90
+ return ERB::Util.h(label) unless value.match?(%r{\Ahttps?://\S+\z})
91
+
92
+ link(value, label)
93
+ end
94
+
95
+ def formatted_date(value)
96
+ Date.parse(value).to_formatted_s(:standard)
97
+ rescue ArgumentError
98
+ value
99
+ end
100
+
101
+ # A rights statement or license rendered as such takes its label from its Hyrax service, as the show page's
102
+ # renderers do; one rendered otherwise falls back to the service only when no label was indexed.
103
+ def rights_label(field_name, render_as, value, label)
104
+ return service_label(render_as, value) || label if RIGHTS_SERVICES.include?(render_as)
105
+ return label || service_label(field_name.to_s, value) if RIGHTS_SERVICES.include?(field_name.to_s)
106
+
107
+ label
108
+ end
109
+
110
+ def service_label(service, value)
111
+ Hyrax.config.try("#{service}_service_class")&.new&.label(value) { nil }
112
+ end
113
+
114
+ def sanitize_html(value)
115
+ fragment = Nokogiri::HTML.fragment(value.to_s)
116
+ { 'a' => 'href', 'img' => 'src' }.each do |tag, attribute|
117
+ fragment.css("#{tag}[#{attribute}]").each { |node| node[attribute] = absolute_url(node[attribute]) }
118
+ end
119
+ Rails::Html::SafeListSanitizer.new.sanitize(fragment.to_html, tags: IIIF_TAGS, attributes: IIIF_ATTRIBUTES).strip
120
+ end
121
+
122
+ def absolute_url(url)
123
+ return url if url.blank? || url.match?(/\A[a-z][a-z0-9+.-]*:/i)
124
+
125
+ URI.join("#{@base_url.to_s.chomp('/')}/", url).to_s
126
+ rescue URI::Error
127
+ url
128
+ end
129
+
130
+ def compound_values(field_name, entries)
131
+ CompoundValues.new(field_name, presenter: @presenter, sanitize: method(:sanitize_html)).to_html(entries)
132
+ end
133
+
134
+ def link(href, text)
135
+ %(<a href="#{ERB::Util.h(href)}">#{ERB::Util.h(text)}</a>)
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "iiif_print/flexibility/fields"
4
+ require "iiif_print/flexibility/compound_values"
5
+ require "iiif_print/flexibility/values"
6
+
7
+ module IiifPrint
8
+ ##
9
+ # IIIF manifest metadata for records indexed under an M3 profile, built the way their show page is: which fields
10
+ # from {IiifPrint::Flexibility::Fields}, and how each value renders from {IiifPrint::Flexibility::Values}.
11
+ module Flexibility
12
+ class << self
13
+ ##
14
+ # @return [Boolean] whether the record's metadata comes from its M3 profile
15
+ def applies_to?(document)
16
+ Hyrax.config.try(:flexible?) && document.try(:flexible?)
17
+ end
18
+
19
+ ##
20
+ # One per request for each record and ability, since both the fields and their values read from it.
21
+ def presenter_for(document, ability)
22
+ presenters = Current.presenters ||= {}
23
+ presenters[[document.id, ability]] ||= build_presenter(document, ability)
24
+ end
25
+
26
+ ##
27
+ # The profile's view definitions for the record, plus its title, description and abstract when the profile
28
+ # gives them no view block and allows them in the record's contexts, since the show page shows those outside
29
+ # its field list. Read once per request
30
+ # for each tenant, schema, version and context; each tenant numbers its own profile versions.
31
+ def view_definitions_for(document)
32
+ cache = Current.view_definitions ||= {}
33
+ version = document.try(:schema_version)
34
+ contexts = document.try(:contexts)
35
+ tenant = Apartment::Tenant.current if defined?(Apartment::Tenant)
36
+ cache[[tenant, schema_name(document), version, contexts]] ||= begin
37
+ definitions = Hyrax::Schema.m3_schema_loader.view_definitions_for(schema: schema_name(document), version: version, contexts: contexts)
38
+ lead_definitions(document, definitions, contexts).merge(definitions.to_h.symbolize_keys)
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def build_presenter(document, ability)
45
+ return Hyrax::FileSetPresenter.new(document, ability) if document.file_set?
46
+
47
+ controller = "Hyrax::#{document.hydra_model.model_name.collection.camelize}Controller".safe_constantize
48
+ (controller&.show_presenter || Hyrax::WorkShowPresenter).new(document, ability)
49
+ end
50
+
51
+ LEAD_FIELDS = %i[title description abstract].freeze
52
+
53
+ def lead_definitions(document, definitions, contexts)
54
+ attributes = profile_attributes(document)
55
+ (LEAD_FIELDS - definitions.to_h.keys.map(&:to_sym)).each_with_object({}) do |name, lead|
56
+ config = attributes[name.to_s]
57
+ next unless config && in_context?(config, contexts)
58
+
59
+ lead[name] = Hyrax::SchemaLoader::AttributeDefinition.new(name.to_s, config).view_options
60
+ end
61
+ end
62
+
63
+ # The filter Hyrax's M3SchemaLoader applies to every field the profile gives a view block.
64
+ def in_context?(config, contexts)
65
+ return true if config['context'].blank?
66
+
67
+ (Array(contexts) & Array(config['context'])).any?
68
+ end
69
+
70
+ def profile_attributes(document)
71
+ schema = Hyrax::FlexibleSchema.find_by(id: document.try(:schema_version)) || Hyrax::FlexibleSchema.order(:created_at).last
72
+ schema.try(:attributes_for, schema_name(document)) || {}
73
+ end
74
+
75
+ def schema_name(document)
76
+ Valkyrie.config.resource_class_resolver.call(document.hydra_model_name).to_s
77
+ rescue NameError
78
+ document.hydra_model.to_s
79
+ end
80
+ end
81
+ end
82
+ end
@@ -6,12 +6,10 @@ module IiifPrint
6
6
  self.default_processor_chain += [:show_parents_only]
7
7
 
8
8
  def show_parents_only(solr_parameters)
9
- query = if blacklight_params["include_child_works"] == 'true'
10
- IiifPrint.solr_construct_query(is_child_bsi: 'true')
11
- else
12
- IiifPrint.solr_construct_query(is_child_bsi: nil)
13
- end
14
- solr_parameters[:fq] += [query]
9
+ return if blacklight_params["include_child_works"] == 'true'
10
+
11
+ # Only child works are left out; a work indexed with is_child_bsi false is not a child.
12
+ solr_parameters[:fq] += ['-is_child_bsi:true']
15
13
  end
16
14
  end
17
15
  end
@@ -38,13 +38,13 @@ module IiifPrint
38
38
  if version == 2
39
39
  case property
40
40
  when :label then field.label
41
- when :value then cast_to_value(field_name: field.name, options: field.options)
41
+ when :value then rendered_values_for(field)
42
42
  when :collection then make_collection_link(viewable_collections)
43
43
  end
44
44
  elsif version == 3
45
45
  case property
46
46
  when :label then { I18n.locale.to_s => [field.label] }
47
- when :value then { 'none' => cast_to_value(field_name: field.name, options: field.options) }
47
+ when :value then { 'none' => rendered_values_for(field) }
48
48
  when :collection then { 'none' => make_collection_link(viewable_collections) }
49
49
  end
50
50
  end
@@ -64,6 +64,12 @@ module IiifPrint
64
64
  Loofah.fragment(value).scrub!(:whitewash).to_s
65
65
  end
66
66
 
67
+ def rendered_values_for(field)
68
+ return flexible_values.rendered(field.name, field.options) if flexible?
69
+
70
+ cast_to_value(field_name: field.name, options: field.options)
71
+ end
72
+
67
73
  def cast_to_value(field_name:, options:)
68
74
  if options&.[](:render_as) == :faceted
69
75
  faceted_values_for(field_name: field_name)
@@ -98,14 +104,25 @@ module IiifPrint
98
104
  end
99
105
 
100
106
  def values_for(field_name:)
107
+ return flexible_values.raw(field_name.try(:name) || field_name) if flexible?
108
+
101
109
  field_name = field_name.try(:name) || field_name
102
110
  # TODO: we are assuming tesim or dtsi (for dates), might want to account for other suffixes in the future
103
111
  Array(work["#{field_name}_tesim"] || work["#{field_name}_dtsi"]&.to_date.try(:to_formatted_s, :standard))
104
112
  end
105
113
 
114
+ def flexible?
115
+ Flexibility.applies_to?(work)
116
+ end
117
+
118
+ def flexible_values
119
+ @flexible_values ||= Flexibility::Values.new(work: work, presenter: Flexibility.presenter_for(work, current_ability),
120
+ base_url: @base_url, autolink: method(:make_link))
121
+ end
122
+
106
123
  def make_collection_link(collection_documents)
107
124
  collection_documents.map do |collection|
108
- "<a href='#{File.join(@base_url, 'collections', collection.id)}'>#{collection.title.first}</a>"
125
+ "<a href='#{File.join(@base_url, 'collections', collection.id)}'>#{ERB::Util.h(collection.title.first)}</a>"
109
126
  end
110
127
  end
111
128
 
@@ -60,7 +60,12 @@ module IiifPrint
60
60
  parent_of_file_set&.member_of&.find(&:work?)
61
61
  end
62
62
 
63
+ # @deprecated Use Hyrax::SolrQueryBuilderService.construct_query, or ActiveFedora::SolrQueryBuilder.construct_query
64
+ # on a Hyrax without it; removed in IiifPrint 4.0.
63
65
  def self.solr_construct_query(*args)
66
+ Deprecation.warn(self, "solr_construct_query is deprecated and will be removed in IiifPrint 4.0; " \
67
+ "use Hyrax::SolrQueryBuilderService.construct_query instead, or " \
68
+ "ActiveFedora::SolrQueryBuilder.construct_query on a Hyrax without it.")
64
69
  if defined?(Hyrax::SolrQueryBuilderService)
65
70
  Hyrax::SolrQueryBuilderService.construct_query(*args)
66
71
  else
@@ -68,7 +68,10 @@ module IiifPrint
68
68
  Hyrax.query_service.find_parents(resource: parent).first
69
69
  end
70
70
 
71
+ # @deprecated Use Hyrax::SolrQueryBuilderService.construct_query; removed in IiifPrint 4.0.
71
72
  def self.solr_construct_query(*args)
73
+ Deprecation.warn(self, "solr_construct_query is deprecated and will be removed in IiifPrint 4.0; " \
74
+ "use Hyrax::SolrQueryBuilderService.construct_query instead.")
72
75
  Hyrax::SolrQueryBuilderService.construct_query(*args)
73
76
  end
74
77
 
@@ -94,7 +97,7 @@ module IiifPrint
94
97
  # Building a custom query to find these child works directly via the attribute would be more efficient.
95
98
  # However, it would require more effort for a lesser-used feature, and would not allow for the fallback
96
99
  # of finding child works by title.
97
- # rubocop:disable Lint/UnusedMethodArgument, Metrics/AbcSize, Metrics/MethodLength
100
+ # rubocop:disable Lint/UnusedMethodArgument
98
101
  def self.destroy_children_split_from(file_set:, work:, model:, user:)
99
102
  all_child_works = Hyrax.custom_queries.find_child_works(resource: work).to_a
100
103
  return if all_child_works.blank?
@@ -114,14 +117,20 @@ module IiifPrint
114
117
  Hyrax.index_adapter.save(resource: work)
115
118
  Hyrax.publisher.publish('object.membership.updated', object: work, user: user)
116
119
 
117
- children.each do |rcd|
118
- Hyrax.persister.delete(resource: rcd)
119
- Hyrax.index_adapter.delete(resource: rcd)
120
- Hyrax.publisher.publish('object.deleted', object: rcd, user: user)
121
- end
120
+ children.each { |child| destroy_child_work(child, user: user || ::User.system_user) }
122
121
  true
123
122
  end
124
- # rubocop:enable Lint/UnusedMethodArgument, Metrics/AbcSize, Metrics/MethodLength
123
+ # rubocop:enable Lint/UnusedMethodArgument
124
+
125
+ # Not persister.delete: the work transaction also deletes the child's own file sets.
126
+ def self.destroy_child_work(child, user:)
127
+ result = Hyrax::Transactions::Container['work_resource.destroy']
128
+ .with_step_args('work_resource.delete_all_file_sets' => { user: user },
129
+ 'work_resource.delete' => { user: user })
130
+ .call(child)
131
+ Rails.logger.error("IiifPrint could not destroy split child work #{child.id}: #{result.failure.inspect}") if result.failure?
132
+ result
133
+ end
125
134
 
126
135
  def self.pdf?(file_set)
127
136
  file_set.original_file&.pdf?
@@ -1,3 +1,3 @@
1
1
  module IiifPrint
2
- VERSION = '3.1.2'.freeze
2
+ VERSION = '3.2.1'.freeze
3
3
  end
data/lib/iiif_print.rb CHANGED
@@ -13,6 +13,9 @@ require "iiif_print/text_formats_from_alto_service"
13
13
  require "iiif_print/tiff_derivative_service"
14
14
  require "iiif_print/lineage_service"
15
15
  require "iiif_print/metadata"
16
+ # ActiveSupport::CurrentAttributes arrived in Rails 5.2; only the flexible metadata path, which needs a newer Hyrax, uses it.
17
+ require "iiif_print/current" if defined?(ActiveSupport::CurrentAttributes)
18
+ require "iiif_print/flexibility"
16
19
  require "iiif_print/split_pdfs/base_splitter"
17
20
  require "iiif_print/split_pdfs/child_work_creation_from_pdf_service"
18
21
  require "iiif_print/split_pdfs/derivative_rodeo_splitter"
@@ -41,6 +44,16 @@ module IiifPrint
41
44
  @config
42
45
  end
43
46
 
47
+ ##
48
+ # Subscribes a new IiifPrint::Listener in place of the one an earlier prepare subscribed.
49
+ # Dry::Events neither deduplicates subscriptions nor unsubscribes by class, and after a code
50
+ # reload the earlier listener's class is the unloaded one, so it is tracked here.
51
+ def self.subscribe_listener(publisher: Hyrax.publisher)
52
+ publisher.unsubscribe(@listener) if @listener
53
+ @listener = IiifPrint::Listener.new
54
+ publisher.subscribe(@listener)
55
+ end
56
+
44
57
  class << self
45
58
  delegate(
46
59
  :persistence_adapter,
@@ -203,8 +216,27 @@ module IiifPrint
203
216
  def self.manifest_metadata_from(work:, presenter:)
204
217
  current_ability = presenter.try(:ability) || presenter.try(:current_ability)
205
218
  base_url = presenter.try(:base_url) || presenter.try(:request)&.base_url
219
+ document = work.is_a?(::SolrDocument) ? work : ::SolrDocument.new(work)
220
+ return flexible_manifest_metadata_for(document, base_url: base_url) if Flexibility.applies_to?(document)
221
+
206
222
  IiifPrint.manifest_metadata_for(work: work, current_ability: current_ability, base_url: base_url)
207
223
  end
224
+
225
+ # @api private
226
+ #
227
+ # A flexible work's metadata lists the fields an anonymous visitor would see on its show page, whoever asked
228
+ # and whether or not the work itself is public.
229
+ def self.flexible_manifest_metadata_for(document, base_url:)
230
+ ability = anonymous_ability
231
+ manifest_metadata_for(work: document, fields: Flexibility::Fields.new(document, ability).to_a, current_ability: ability, base_url: base_url)
232
+ end
233
+
234
+ # One per request: an Ability is costly to build, and a manifest can ask once for every page.
235
+ def self.anonymous_ability
236
+ Current.anonymous_ability ||= ::Ability.new(nil)
237
+ end
238
+ private_class_method :flexible_manifest_metadata_for, :anonymous_ability
239
+
208
240
  # Hash is an arbitrary attribute key/value pairs
209
241
  # Struct is a defined set of attribute "keys". When we favor defined values,
210
242
  # then we are naming the concept and defining the range of potential values.
@@ -273,10 +305,10 @@ module IiifPrint
273
305
  def self.sort_af_fields!(fields, sort_order:)
274
306
  return fields if sort_order.blank?
275
307
 
276
- fields.sort_by do |field|
277
- sort_order_index = sort_order.index(field.name.to_sym)
278
- sort_order_index.nil? ? sort_order.length : sort_order_index
279
- end
308
+ remaining = sort_order.index(:remaining) || sort_order.length
309
+ fields.each_with_index.sort_by do |field, index|
310
+ [sort_order.index(field.name.to_sym) || remaining, index]
311
+ end.map(&:first)
280
312
  end
281
313
 
282
314
  ##
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: iiif_print
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.1.2
4
+ version: 3.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sean Upton
@@ -11,9 +11,10 @@ authors:
11
11
  - Kirk Wang
12
12
  - LaRita Robinson
13
13
  - Jeremy Friesen
14
+ autorequire:
14
15
  bindir: bin
15
16
  cert_chain: []
16
- date: 1980-01-02 00:00:00.000000000 Z
17
+ date: 2026-10-09 00:00:00.000000000 Z
17
18
  dependencies:
18
19
  - !ruby/object:Gem::Dependency
19
20
  name: blacklight_iiif_search
@@ -448,6 +449,7 @@ files:
448
449
  - lib/iiif_print/blacklight_iiif_search/annotation_decorator.rb
449
450
  - lib/iiif_print/catalog_search_builder.rb
450
451
  - lib/iiif_print/configuration.rb
452
+ - lib/iiif_print/current.rb
451
453
  - lib/iiif_print/data.rb
452
454
  - lib/iiif_print/data/fileset_helper.rb
453
455
  - lib/iiif_print/data/path_helper.rb
@@ -456,6 +458,10 @@ files:
456
458
  - lib/iiif_print/data/work_files.rb
457
459
  - lib/iiif_print/engine.rb
458
460
  - lib/iiif_print/errors.rb
461
+ - lib/iiif_print/flexibility.rb
462
+ - lib/iiif_print/flexibility/compound_values.rb
463
+ - lib/iiif_print/flexibility/fields.rb
464
+ - lib/iiif_print/flexibility/values.rb
459
465
  - lib/iiif_print/homepage_search_builder.rb
460
466
  - lib/iiif_print/image_tool.rb
461
467
  - lib/iiif_print/jp2_derivative_service.rb
@@ -496,6 +502,7 @@ homepage: https://github.com/notch8/iiif_print/
496
502
  licenses:
497
503
  - Apache-2.0
498
504
  metadata: {}
505
+ post_install_message:
499
506
  rdoc_options: []
500
507
  require_paths:
501
508
  - lib
@@ -510,7 +517,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
510
517
  - !ruby/object:Gem::Version
511
518
  version: '0'
512
519
  requirements: []
513
- rubygems_version: 3.6.7
520
+ rubygems_version: 3.3.20
521
+ signing_key:
514
522
  specification_version: 4
515
523
  summary: IiifPrint is a gem (Rails "engine") for Hyrax-based digital repository applications
516
524
  to support displaying parent/child works in the same viewer (Universal Viewer) and