iiif_print 3.1.1 → 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: d1fb1a28565daaa498895ea28019b1a5d9cb16e01558872b4840e19ab002fb07
4
- data.tar.gz: 76a5d7afeda6cb264d2a77e31ff0e60be1c8d2a416917907a4d848e3b8975e63
3
+ metadata.gz: 01fd18bb505838305103b589e56d196fbc346eb00390de671c53bb15083a39d6
4
+ data.tar.gz: c7bf87ae11b5ae00b597076254d897c9519fc2bf9facd777763861dcca511adf
5
5
  SHA512:
6
- metadata.gz: '08d25b39fccf79fe3b147b35dd8eb7fff99ac9d66bb91426ea4c1b98a16009b539f48283850225725f8ebd1d62876f3d30ff61508722f09def19eb02b67f4b63'
7
- data.tar.gz: ef3c25dca1cdc1ab648d90ec303ad56df9a26fc6df4358e610154634eccd934b64295d99c2611fd454b8321d761f7aca9bcde4263495ac2fdfc58cca8447bc61
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.1)
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/
@@ -26,7 +26,7 @@ module IiifPrint
26
26
  return { q: 'nil:nil' } unless q
27
27
 
28
28
  {
29
- q: "#{q} AND has_model_ssim:FileSet AND (#{iiif_config[:object_relation_field]}:\"#{parent_document.id}\" OR id:\"#{parent_document.id}\")",
29
+ q: "(#{q}) AND has_model_ssim:FileSet AND (#{iiif_config[:object_relation_field]}:\"#{parent_document.id}\" OR id:\"#{parent_document.id}\")",
30
30
  rows: rows,
31
31
  page: page
32
32
  }
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'
@@ -36,8 +36,9 @@ module IiifPrint
36
36
 
37
37
  query_terms = sanitized_query.split(' ')
38
38
 
39
+ terms_pattern = Regexp.union(query_terms)
39
40
  matches = coords_json['coords'].select do |k, _v|
40
- k.downcase =~ /(#{query_terms.join('|')})/
41
+ k.downcase.match?(terms_pattern)
41
42
  end
42
43
  return default_coords if matches.blank?
43
44
 
@@ -113,16 +114,16 @@ module IiifPrint
113
114
 
114
115
  ##
115
116
  # This method is a workaround to compensate for overriding the solr_params method in
116
- # BlacklightIiifSearch::IiifSearch. In the override, the solr_params method adds an additional filter to the query
117
- # to include either the object_relation_field OR the parent document's id and removes the :f parameter from the
118
- # query. This resulted in the query split here returning more than the actual query term.
117
+ # BlacklightIiifSearch::IiifSearch. In the override, the solr_params method wraps the user's query in
118
+ # parentheses and adds filters for the FileSet model and the object_relation_field OR the parent document's id.
119
+ # This extracts the user's query from inside those parentheses so the filters are not treated as search terms.
119
120
  #
120
121
  # @see IiifPrint::IiifSearchDecorator#solr_params
121
- # @return [Regexp] A regular expression to find the last AND and everything after it
122
+ # @return [Regexp] A regular expression capturing the user's query
122
123
  # @example
123
- # 'foo AND (is_page_of_ssim:\"123123\" OR id:\"123123\")' #=> 'foo'
124
+ # '(foo) AND has_model_ssim:FileSet AND (is_page_of_ssim:\"123123\" OR id:\"123123\")' #=> 'foo'
124
125
  def additional_query_terms_regex
125
- /(.*)(?= AND (\(.+\)|\w+)$)/
126
+ /\A\((.*)\) AND has_model_ssim/m
126
127
  end
127
128
 
128
129
  ##
@@ -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
 
@@ -96,7 +96,7 @@ module IiifPrint
96
96
  # of finding child works by title.
97
97
  # rubocop:disable Lint/UnusedMethodArgument, Metrics/AbcSize, Metrics/MethodLength
98
98
  def self.destroy_children_split_from(file_set:, work:, model:, user:)
99
- all_child_works = Hyrax.custom_queries.find_child_works(resource: work)
99
+ all_child_works = Hyrax.custom_queries.find_child_works(resource: work).to_a
100
100
  return if all_child_works.blank?
101
101
  # look first for children by the file set id they were split from
102
102
  children = all_child_works.select { |m| m.split_from_pdf_id == file_set.id }
@@ -1,3 +1,3 @@
1
1
  module IiifPrint
2
- VERSION = '3.1.1'.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.1
4
+ version: 3.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sean Upton
@@ -11,10 +11,9 @@ authors:
11
11
  - Kirk Wang
12
12
  - LaRita Robinson
13
13
  - Jeremy Friesen
14
- autorequire:
15
14
  bindir: bin
16
15
  cert_chain: []
17
- date: 2026-06-23 00:00:00.000000000 Z
16
+ date: 1980-01-02 00:00:00.000000000 Z
18
17
  dependencies:
19
18
  - !ruby/object:Gem::Dependency
20
19
  name: blacklight_iiif_search
@@ -98,6 +97,20 @@ dependencies:
98
97
  - - "~>"
99
98
  - !ruby/object:Gem::Version
100
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'
101
114
  - !ruby/object:Gem::Dependency
102
115
  name: bixby
103
116
  requirement: !ruby/object:Gem::Requirement
@@ -457,6 +470,9 @@ files:
457
470
  - lib/iiif_print/data/work_files.rb
458
471
  - lib/iiif_print/engine.rb
459
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
460
476
  - lib/iiif_print/homepage_search_builder.rb
461
477
  - lib/iiif_print/image_tool.rb
462
478
  - lib/iiif_print/jp2_derivative_service.rb
@@ -497,7 +513,6 @@ homepage: https://github.com/notch8/iiif_print/
497
513
  licenses:
498
514
  - Apache-2.0
499
515
  metadata: {}
500
- post_install_message:
501
516
  rdoc_options: []
502
517
  require_paths:
503
518
  - lib
@@ -512,8 +527,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
512
527
  - !ruby/object:Gem::Version
513
528
  version: '0'
514
529
  requirements: []
515
- rubygems_version: 3.3.20
516
- signing_key:
530
+ rubygems_version: 3.6.7
517
531
  specification_version: 4
518
532
  summary: IiifPrint is a gem (Rails "engine") for Hyrax-based digital repository applications
519
533
  to support displaying parent/child works in the same viewer (Universal Viewer) and