iiif_print 3.1.2 → 3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 512bd4a70c6b11e94a4d82fb786264bb124c4ca38ca33549d34e3bcb6d0e37bc
4
- data.tar.gz: 0b363334d97c0a97bc5b4ed8685c9be41f9ab7124e027af6a1334f2f53f6e86c
3
+ metadata.gz: 01fd18bb505838305103b589e56d196fbc346eb00390de671c53bb15083a39d6
4
+ data.tar.gz: c7bf87ae11b5ae00b597076254d897c9519fc2bf9facd777763861dcca511adf
5
5
  SHA512:
6
- metadata.gz: 4e31e9b19c7ca850977bc1a04c912a98d49116d1b780e7ecdde7be43abbe447e44eceb20bc18be0d038887257b0432fc15ae79d20609ec9dfdea75e8946a82cc
7
- data.tar.gz: b9639133a35990cabfe146fb13866e9a948e418a48a36962de3d10a70b52794f2466de9e789d851b778d91fa165659f4b90560759aee0b421313d9326ef2329d
6
+ metadata.gz: 3eee1a13ecdf5693e0dd987a9331d527fcf884d0c8dc10ea12b14f7b99165a3f13b712ac5cfddb6d1579fbd33ce6fc0945c1bc2c3195ff1aff7c56637e3aee46
7
+ data.tar.gz: 1a85675fa01612c0fc9115e2f1cf524fae745d6a2c4b8b897b283c4272bd1583ef3c40e5e9b605591cc36a641a4f8aedb8c5b022a90534bbb31f725aa670e8be
data/Gemfile.lock CHANGED
@@ -1,12 +1,13 @@
1
1
  PATH
2
2
  remote: /app/samvera/hyrax-webapp
3
3
  specs:
4
- iiif_print (3.1.2)
4
+ iiif_print (3.2.0)
5
5
  blacklight_iiif_search (>= 1.0, < 3.0)
6
6
  derivative-rodeo (~> 0.5)
7
7
  hyrax (>= 2.5, < 6)
8
8
  nokogiri (>= 1.13.2)
9
9
  rdf-vocab (~> 3.0)
10
+ request_store (>= 1.0)
10
11
 
11
12
  GEM
12
13
  remote: https://rubygems.org/
data/iiif_print.gemspec CHANGED
@@ -25,6 +25,7 @@ SUMMARY
25
25
  spec.add_dependency 'hyrax', '>= 2.5', '< 6'
26
26
  spec.add_dependency 'nokogiri', '>=1.13.2'
27
27
  spec.add_dependency 'rdf-vocab', '~> 3.0'
28
+ spec.add_dependency 'request_store', '>= 1.0'
28
29
 
29
30
  spec.add_development_dependency 'bixby'
30
31
  spec.add_development_dependency 'database_cleaner', '~> 1.3'
@@ -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
@@ -2,6 +2,7 @@ require 'active_fedora'
2
2
  require 'hyrax'
3
3
  require 'blacklight_iiif_search'
4
4
  require 'derivative_rodeo'
5
+ require 'request_store'
5
6
 
6
7
  module IiifPrint
7
8
  # module constants:
@@ -0,0 +1,66 @@
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
+ def label(name, options)
56
+ labels = options[:display_label] || {}
57
+ label = (labels[I18n.locale.to_s] || labels['default'] || name).to_s
58
+ translated = I18n.t(label, default: label) if label.include?('.')
59
+ return translated if translated && translated != label
60
+
61
+ I18n.t(:"blacklight.search.fields.index.#{label}",
62
+ default: [:"blacklight.search.fields.show.#{label}", :"blacklight.search.fields.#{label}", label.humanize])
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,148 @@
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 dropped
26
+ def raw(field_name)
27
+ Array(@presenter.try(field_name)).reject(&:blank?)
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 [values.map { |entry| "<p>#{compound_value(field_name, entry)}</p>" }.join] 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
+ # One entry as Hyrax's compound renderer, and any decorator the app gives it, renders it on the show page, with
131
+ # a line break where each innermost block ended, since IIIF allows no blocks.
132
+ def compound_value(field_name, entry)
133
+ renderer = Hyrax::Renderers::CompoundAttributeRenderer.new(field_name, [entry], subproperties: compound_subproperties(field_name))
134
+ fragment = Nokogiri::HTML.fragment(renderer.render_value)
135
+ fragment.css('div').each { |div| div.add_next_sibling('<br>') unless div.at_css('div') }
136
+ sanitize_html(fragment.to_html).gsub(/\s*<br>\s*/, '<br>').sub(/(<br>)+\z/, '')
137
+ end
138
+
139
+ def compound_subproperties(field_name)
140
+ @presenter.send(:compound_subproperties_for, field_name) if @presenter.respond_to?(:compound_subproperties_for, true)
141
+ end
142
+
143
+ def link(href, text)
144
+ %(<a href="#{ERB::Util.h(href)}">#{ERB::Util.h(text)}</a>)
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "iiif_print/flexibility/fields"
4
+ require "iiif_print/flexibility/values"
5
+
6
+ module IiifPrint
7
+ ##
8
+ # IIIF manifest metadata for records indexed under an M3 profile, built the way their show page is: which fields
9
+ # from {IiifPrint::Flexibility::Fields}, and how each value renders from {IiifPrint::Flexibility::Values}.
10
+ module Flexibility
11
+ class << self
12
+ ##
13
+ # @return [Boolean] whether the record's metadata comes from its M3 profile
14
+ def applies_to?(document)
15
+ Hyrax.config.try(:flexible?) && document.try(:flexible?)
16
+ end
17
+
18
+ def presenter_for(document, ability)
19
+ return Hyrax::FileSetPresenter.new(document, ability) if document.file_set?
20
+
21
+ controller = "Hyrax::#{document.hydra_model.model_name.collection.camelize}Controller".safe_constantize
22
+ (controller&.show_presenter || Hyrax::WorkShowPresenter).new(document, ability)
23
+ end
24
+
25
+ ##
26
+ # The profile's view definitions for the record, plus its title, description and abstract when the profile
27
+ # gives them no view block, since the show page shows those outside its field list. Read once per request
28
+ # for each tenant, schema, version and context; each tenant numbers its own profile versions.
29
+ def view_definitions_for(document)
30
+ cache = RequestStore.store[:iiif_print_view_definitions] ||= {}
31
+ version = document.try(:schema_version)
32
+ contexts = document.try(:contexts)
33
+ tenant = Apartment::Tenant.current if defined?(Apartment::Tenant)
34
+ cache[[tenant, schema_name(document), version, contexts]] ||= begin
35
+ definitions = Hyrax::Schema.m3_schema_loader.view_definitions_for(schema: schema_name(document), version: version, contexts: contexts)
36
+ lead_definitions(document, definitions).merge(definitions.to_h.symbolize_keys)
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ LEAD_FIELDS = %i[title description abstract].freeze
43
+
44
+ def lead_definitions(document, definitions)
45
+ attributes = profile_attributes(document)
46
+ (LEAD_FIELDS - definitions.to_h.keys.map(&:to_sym)).each_with_object({}) do |name, lead|
47
+ config = attributes[name.to_s]
48
+ lead[name] = Hyrax::SchemaLoader::AttributeDefinition.new(name.to_s, config).view_options if config
49
+ end
50
+ end
51
+
52
+ def profile_attributes(document)
53
+ schema = Hyrax::FlexibleSchema.find_by(id: document.try(:schema_version)) || Hyrax::FlexibleSchema.order(:created_at).last
54
+ schema.try(:attributes_for, schema_name(document)) || {}
55
+ end
56
+
57
+ def schema_name(document)
58
+ Valkyrie.config.resource_class_resolver.call(document.hydra_model_name).to_s
59
+ rescue NameError
60
+ document.hydra_model.to_s
61
+ end
62
+ end
63
+ end
64
+ 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
 
@@ -1,3 +1,3 @@
1
1
  module IiifPrint
2
- VERSION = '3.1.2'.freeze
2
+ VERSION = '3.2.0'.freeze
3
3
  end
data/lib/iiif_print.rb CHANGED
@@ -13,6 +13,7 @@ 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
+ require "iiif_print/flexibility"
16
17
  require "iiif_print/split_pdfs/base_splitter"
17
18
  require "iiif_print/split_pdfs/child_work_creation_from_pdf_service"
18
19
  require "iiif_print/split_pdfs/derivative_rodeo_splitter"
@@ -203,8 +204,27 @@ module IiifPrint
203
204
  def self.manifest_metadata_from(work:, presenter:)
204
205
  current_ability = presenter.try(:ability) || presenter.try(:current_ability)
205
206
  base_url = presenter.try(:base_url) || presenter.try(:request)&.base_url
207
+ document = work.is_a?(::SolrDocument) ? work : ::SolrDocument.new(work)
208
+ return flexible_manifest_metadata_for(document, base_url: base_url) if Flexibility.applies_to?(document)
209
+
206
210
  IiifPrint.manifest_metadata_for(work: work, current_ability: current_ability, base_url: base_url)
207
211
  end
212
+
213
+ # @api private
214
+ #
215
+ # A flexible work's metadata lists the fields an anonymous visitor would see on its show page, whoever asked
216
+ # and whether or not the work itself is public.
217
+ def self.flexible_manifest_metadata_for(document, base_url:)
218
+ ability = anonymous_ability
219
+ manifest_metadata_for(work: document, fields: Flexibility::Fields.new(document, ability).to_a, current_ability: ability, base_url: base_url)
220
+ end
221
+
222
+ # One per request: an Ability is costly to build, and a manifest can ask once for every page.
223
+ def self.anonymous_ability
224
+ RequestStore.store[:iiif_print_anonymous_ability] ||= ::Ability.new(nil)
225
+ end
226
+ private_class_method :flexible_manifest_metadata_for, :anonymous_ability
227
+
208
228
  # Hash is an arbitrary attribute key/value pairs
209
229
  # Struct is a defined set of attribute "keys". When we favor defined values,
210
230
  # then we are naming the concept and defining the range of potential values.
@@ -273,10 +293,10 @@ module IiifPrint
273
293
  def self.sort_af_fields!(fields, sort_order:)
274
294
  return fields if sort_order.blank?
275
295
 
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
296
+ remaining = sort_order.index(:remaining) || sort_order.length
297
+ fields.each_with_index.sort_by do |field, index|
298
+ [sort_order.index(field.name.to_sym) || remaining, index]
299
+ end.map(&:first)
280
300
  end
281
301
 
282
302
  ##
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.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sean Upton
@@ -97,6 +97,20 @@ dependencies:
97
97
  - - "~>"
98
98
  - !ruby/object:Gem::Version
99
99
  version: '3.0'
100
+ - !ruby/object:Gem::Dependency
101
+ name: request_store
102
+ requirement: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: '1.0'
107
+ type: :runtime
108
+ prerelease: false
109
+ version_requirements: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - ">="
112
+ - !ruby/object:Gem::Version
113
+ version: '1.0'
100
114
  - !ruby/object:Gem::Dependency
101
115
  name: bixby
102
116
  requirement: !ruby/object:Gem::Requirement
@@ -456,6 +470,9 @@ files:
456
470
  - lib/iiif_print/data/work_files.rb
457
471
  - lib/iiif_print/engine.rb
458
472
  - lib/iiif_print/errors.rb
473
+ - lib/iiif_print/flexibility.rb
474
+ - lib/iiif_print/flexibility/fields.rb
475
+ - lib/iiif_print/flexibility/values.rb
459
476
  - lib/iiif_print/homepage_search_builder.rb
460
477
  - lib/iiif_print/image_tool.rb
461
478
  - lib/iiif_print/jp2_derivative_service.rb